time series, artist breakdown, start and end date

This commit is contained in:
andy 2022-06-23 19:26:36 +01:00
parent 079e126648
commit a1bd1ab62d
8 changed files with 262 additions and 35 deletions

View File

@ -22,19 +22,37 @@
:track="lastfmTrack"
:username="playCount.username"
v-if="playCount !== null && playCount !== undefined"></play-count-card>
<info-card v-for="card in cards" :html="card.content"></info-card>
<artist-breakdown :play_count="playCount"
v-if="playCount !== null && playCount !== undefined"></artist-breakdown>
<play-count-chart-card-comb :data_points="combinedData"
:chart_id="'combined'"
:earliest_date="earliestDate"
v-if="showArtistChart"></play-count-chart-card-comb>
<play-count-chart-card :data_points="playCount.trackCountData"
:title="currentlyPlaying.track.name"
:title="trackGraphTitle"
:chart_id="'track'"
:earliest_date="earliestDate"
:latest_date="latestDate"
:colour="'#7a99c2'"
v-if="showTrackChart"></play-count-chart-card>
<play-count-chart-card :data_points="playCount.albumCountData"
:title="currentlyPlaying.track.album.name"
:title="albumGraphTitle"
:chart_id="'album'"
:earliest_date="earliestDate"
:latest_date="latestDate"
:colour="'#a34c77'"
v-if="showAlbumChart"></play-count-chart-card>
<play-count-chart-card :data_points="playCount.artistCountData"
:title="lastfmArtist"
:title="artistGraphTitle"
:chart_id="'artist'"
:earliest_date="earliestDate"
:latest_date="latestDate"
:colour="'#598556'"
v-if="showArtistChart"></play-count-chart-card>
<info-card v-for="card in cards" :html="card.content"></info-card>
</div>
</div>

View File

@ -9,6 +9,11 @@
"Selector": {
"Redis": {
"enabled": true
},
"Now": {
"ArtistResampleWindow": "14.00:00:00",
"AlbumResampleWindow": "14.00:00:00",
"TrackResampleWindow": "14.00:00:00"
}
},
"AllowedHosts": "*"

View File

@ -12,9 +12,12 @@
"@microsoft/signalr": "^6.0.0",
"bootstrap": "^5.1.3",
"chart.js": "^3.6.0",
"chartjs-adapter-luxon": "^1.1.0",
"luxon": "^2.4.0",
"vue": "^3.2.22"
},
"devDependencies": {
"@types/luxon": "^2.3.2",
"clean-webpack-plugin": "^4.0.0",
"file-loader": "^6.2.0",
"sass": "^1.44.0",
@ -111,6 +114,12 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"node_modules/@types/luxon": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz",
"integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==",
"dev": true
},
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@ -707,6 +716,15 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg=="
},
"node_modules/chartjs-adapter-luxon": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.1.0.tgz",
"integrity": "sha512-CS+xBWEyXYVLBZ3dSY/MwlSXhz8er4JjkApazY84ft/++oOLsmkt6TaXBCsUFudum7QdoYmpxiL/gSp20+emkw==",
"peerDependencies": {
"chart.js": "^3.0.0",
"luxon": "^1.0.0 || ^2.0.0"
}
},
"node_modules/chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
@ -1568,6 +1586,14 @@
"node": ">=10"
}
},
"node_modules/luxon": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz",
"integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA==",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@ -2879,6 +2905,12 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"@types/luxon": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz",
"integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==",
"dev": true
},
"@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@ -3385,6 +3417,12 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg=="
},
"chartjs-adapter-luxon": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.1.0.tgz",
"integrity": "sha512-CS+xBWEyXYVLBZ3dSY/MwlSXhz8er4JjkApazY84ft/++oOLsmkt6TaXBCsUFudum7QdoYmpxiL/gSp20+emkw==",
"requires": {}
},
"chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
@ -4026,6 +4064,11 @@
"yallist": "^4.0.0"
}
},
"luxon": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz",
"integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA=="
},
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",

View File

@ -23,9 +23,12 @@
"@microsoft/signalr": "^6.0.0",
"bootstrap": "^5.1.3",
"chart.js": "^3.6.0",
"chartjs-adapter-luxon": "^1.1.0",
"luxon": "^2.4.0",
"vue": "^3.2.22"
},
"devDependencies": {
"@types/luxon": "^2.3.2",
"clean-webpack-plugin": "^4.0.0",
"file-loader": "^6.2.0",
"sass": "^1.44.0",

View File

@ -0,0 +1,71 @@
import * as Vue from "vue";
import { Chart, DoughnutController, ArcElement } from "chart.js";
import { PlayCount } from "scripts/HubInterfaces";
Chart.register(DoughnutController, ArcElement);
const pieColours = ['#7a99c2',
'#a34c77',
'#598556',
];
export let ArtistBreakdownChartCard: Vue.Component = {
props: ['play_count'],
data() {
return {
}
},
computed: {
trackPercent() {
return this.play_count.track * 100 / this.play_count.artist
},
albumPercent() {
return this.play_count.album * 100 / this.play_count.artist
},
albumDiff() {
return this.albumPercent - this.trackPercent;
},
artistPercent() {
return 100 - this.albumDiff + this.trackPercent;
}
},
template:
`
<div class="card info-card">
<canvas id="artist-breakdown"></canvas>
<lastfm-logo />
</div>
`,
mounted() {
new Chart(`artist-breakdown`, {
type: "doughnut",
data: {
labels: [ "track", "album", "artist" ],
datasets: [{
data: [ this.trackPercent, this.albumDiff, this.artistPercent ],
}]
},
options: {
plugins: {
legend : {
display : true,
labels: {
color: 'white'
}
}
},
layout: {
padding: 20
},
elements: {
arc : {
backgroundColor: pieColours,
borderWidth: 2,
borderColor: 'rgb(0, 0, 0)'
}
}
}
})
}
}

View File

@ -1,24 +1,22 @@
import * as Vue from "vue";
import { Chart, PointElement, LineElement, LineController, CategoryScale, LinearScale, TimeSeriesScale } from "chart.js";
import 'chartjs-adapter-luxon';
import { CountSample } from "scripts/HubInterfaces";
import { ScrobbleDataSeries } from "scripts/now";
Chart.register(LineController, CategoryScale, LinearScale, TimeSeriesScale, PointElement, LineElement);
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
export let PlayCountChartCard: Vue.Component = {
props: ['data_points', 'title', 'chart_id', 'link'],
props: ['data_points', 'title', 'chart_id', 'link', 'earliest_date', 'latest_date', 'colour'],
data() {
return {
chartData: {
labels: this.data_points.map((e: CountSample) => {
var date = new Date(e.timeStamp);
return `${months[date.getMonth()]} ${date.getFullYear()}`;
}),
datasets: [{
// label: '# of Votes',
data: this.data_points.map((e: CountSample) => e.value),
data: this.data_points.map((e: CountSample) => {
return {x: e.timeStamp, y: e.value};
}),
}]
}
}
@ -43,25 +41,85 @@ export let PlayCountChartCard: Vue.Component = {
options: {
elements: {
line: {
borderWidth: 4,
borderColor: "#a34c77",
backgroundColor: "#727272",
borderWidth: 5,
borderColor: this.colour,
borderCapStyle: "round",
borderJoinStyle: "round"
},
// point: {
// radius: 4,
// pointStyle: "circle",
// borderColor: "black",
// backgroundColor: "white"
// }
point: {
radius: 0
}
},
scales: {
yAxis: {
suggestedMin: 0
},
xAxis: {
type: 'time',
min: this.earliest_date,
max: this.latest_date
}
}
}
})
}
}
}
export let CombinedPlayCountChartCard: Vue.Component = {
props: ['data_points', 'title', 'chart_id', 'link', 'earliest_date'],
data() {
return {
chartData: {
datasets: this.data_points.map((series: ScrobbleDataSeries) => {
return {
label: series.label,
borderColor: series.colour,
data: series.data.map((e: CountSample) => {
return {x: e.timeStamp, y: e.value};
})
}
})
}
}
},
computed: {
chartId() {
return "play-count-chart-" + this.chart_id;
}
},
template:
`
<div class="card info-card chart-card">
<canvas :id="chartId"></canvas>
<lastfm-logo :link="link" />
</div>
`,
mounted() {
new Chart(`play-count-chart-${this.chart_id}`, {
type: "line",
data: this.chartData,
options: {
elements: {
line: {
borderWidth: 5,
borderColor: "#a34c77",
borderCapStyle: "round",
borderJoinStyle: "round"
},
point: {
radius: 0
}
},
scales: {
yAxis: {
suggestedMin: 0
},
xAxis: {
type: 'time',
min: this.earliest_date
}
}
}
})
}
}

View File

@ -1,11 +1,13 @@
import * as signalR from "@microsoft/signalr";
import * as Vue from "vue";
import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO } from "./HubInterfaces";
import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO, CountSample } from "./HubInterfaces";
import NowPlayingCard from "./Now/NowPlayingCard";
import { AudioFeatureCard, AudioFeatureChartCard, PopularityCard, SpotifyLogoLink } from "./Now/Spotify";
import { PlayCountChartCard } from "./Now/PlayCountGraph";
import { PlayCountChartCard, CombinedPlayCountChartCard } from "./Now/PlayCountGraph";
import { ArtistBreakdownChartCard } from "./Now/ArtistBreakdownGraph";
import { PlayCountCard, LastFmLogoLink } from "./Now/LastFm";
import BaseInfoCard from "./Now/BaseInfoCard";
import { DateTime } from "luxon";
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hub")
@ -28,6 +30,12 @@ interface NowPlaying {
cards: InfoCard[]
}
export interface ScrobbleDataSeries {
label: string,
colour: string,
data: CountSample[]
}
const app = Vue.createApp({
data() {
return {
@ -46,14 +54,6 @@ const app = Vue.createApp({
album_artist: this.currentlyPlaying.track.album.artists[0].name,
};
},
lastfmArtist(){
// if(this.currentlyPlaying.track.artists[0].length > 0)
{
return this.currentlyPlaying.track.artists[0].name;
}
return "";
},
showArtistChart(){
return this.playCount !== null && this.playCount !== undefined && this.playCount.artistCountData.length > 3;
},
@ -62,6 +62,33 @@ const app = Vue.createApp({
},
showTrackChart(){
return this.playCount !== null && this.playCount !== undefined && this.playCount.trackCountData.length > 3;
},
earliestDate(){
return this.playCount.artistCountData[0].timeStamp;
},
latestDate(){
return this.playCount.artistCountData.at(-1).timeStamp;
},
trackGraphTitle() { return `${this.currentlyPlaying.track.name} 🎵`},
albumGraphTitle() { return `${this.currentlyPlaying.track.album.name} 💿`},
artistGraphTitle() { return `${this.currentlyPlaying.track.artists[0].name} 🎤`},
combinedData(){
return [
{
label: "artist",
colour: "#598556",
data: this.playCount.artistCountData
},
{
label: "album",
colour: "#a34c77",
data: this.playCount.albumCountData
},
{
label: "track",
colour: "#7a99c2",
data: this.playCount.trackCountData
}];
}
},
created() {
@ -123,4 +150,6 @@ app.component("spotify-logo", SpotifyLogoLink);
app.component("lastfm-logo", LastFmLogoLink);
app.component("play-count-card", PlayCountCard);
app.component("play-count-chart-card", PlayCountChartCard);
app.component("play-count-chart-card-comb", CombinedPlayCountChartCard);
app.component("artist-breakdown", ArtistBreakdownChartCard);
const vm = app.mount('#app');

View File

@ -5,9 +5,9 @@ namespace Selector
{
public const string Key = "Now";
public TimeSpan ArtistResampleWindow { get; set; } = TimeSpan.FromDays(30);
public TimeSpan AlbumResampleWindow { get; set; } = TimeSpan.FromDays(30);
public TimeSpan TrackResampleWindow { get; set; } = TimeSpan.FromDays(30);
public TimeSpan ArtistResampleWindow { get; set; } = TimeSpan.FromDays(7);
public TimeSpan AlbumResampleWindow { get; set; } = TimeSpan.FromDays(7);
public TimeSpan TrackResampleWindow { get; set; } = TimeSpan.FromDays(7);
public TimeSpan ArtistDensityWindow { get; set; } = TimeSpan.FromDays(10);
public decimal ArtistDensityThreshold { get; set; } = 5;