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" :track="lastfmTrack"
:username="playCount.username" :username="playCount.username"
v-if="playCount !== null && playCount !== undefined"></play-count-card> 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" <play-count-chart-card :data_points="playCount.trackCountData"
:title="currentlyPlaying.track.name" :title="trackGraphTitle"
:chart_id="'track'" :chart_id="'track'"
:earliest_date="earliestDate"
:latest_date="latestDate"
:colour="'#7a99c2'"
v-if="showTrackChart"></play-count-chart-card> v-if="showTrackChart"></play-count-chart-card>
<play-count-chart-card :data_points="playCount.albumCountData" <play-count-chart-card :data_points="playCount.albumCountData"
:title="currentlyPlaying.track.album.name" :title="albumGraphTitle"
:chart_id="'album'" :chart_id="'album'"
:earliest_date="earliestDate"
:latest_date="latestDate"
:colour="'#a34c77'"
v-if="showAlbumChart"></play-count-chart-card> v-if="showAlbumChart"></play-count-chart-card>
<play-count-chart-card :data_points="playCount.artistCountData" <play-count-chart-card :data_points="playCount.artistCountData"
:title="lastfmArtist" :title="artistGraphTitle"
:chart_id="'artist'" :chart_id="'artist'"
:earliest_date="earliestDate"
:latest_date="latestDate"
:colour="'#598556'"
v-if="showArtistChart"></play-count-chart-card> v-if="showArtistChart"></play-count-chart-card>
<info-card v-for="card in cards" :html="card.content"></info-card>
</div> </div>
</div> </div>

View File

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

View File

@ -12,9 +12,12 @@
"@microsoft/signalr": "^6.0.0", "@microsoft/signalr": "^6.0.0",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"chart.js": "^3.6.0", "chart.js": "^3.6.0",
"chartjs-adapter-luxon": "^1.1.0",
"luxon": "^2.4.0",
"vue": "^3.2.22" "vue": "^3.2.22"
}, },
"devDependencies": { "devDependencies": {
"@types/luxon": "^2.3.2",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.44.0", "sass": "^1.44.0",
@ -111,6 +114,12 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true "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": { "node_modules/@types/minimatch": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", "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", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg==" "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": { "node_modules/chokidar": {
"version": "3.5.2", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
@ -1568,6 +1586,14 @@
"node": ">=10" "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": { "node_modules/magic-string": {
"version": "0.25.7", "version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "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==", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true "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": { "@types/minimatch": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", "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", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg==" "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": { "chokidar": {
"version": "3.5.2", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
@ -4026,6 +4064,11 @@
"yallist": "^4.0.0" "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": { "magic-string": {
"version": "0.25.7", "version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",

View File

@ -23,9 +23,12 @@
"@microsoft/signalr": "^6.0.0", "@microsoft/signalr": "^6.0.0",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"chart.js": "^3.6.0", "chart.js": "^3.6.0",
"chartjs-adapter-luxon": "^1.1.0",
"luxon": "^2.4.0",
"vue": "^3.2.22" "vue": "^3.2.22"
}, },
"devDependencies": { "devDependencies": {
"@types/luxon": "^2.3.2",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.44.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 * as Vue from "vue";
import { Chart, PointElement, LineElement, LineController, CategoryScale, LinearScale, TimeSeriesScale } from "chart.js"; import { Chart, PointElement, LineElement, LineController, CategoryScale, LinearScale, TimeSeriesScale } from "chart.js";
import 'chartjs-adapter-luxon';
import { CountSample } from "scripts/HubInterfaces"; import { CountSample } from "scripts/HubInterfaces";
import { ScrobbleDataSeries } from "scripts/now";
Chart.register(LineController, CategoryScale, LinearScale, TimeSeriesScale, PointElement, LineElement); Chart.register(LineController, CategoryScale, LinearScale, TimeSeriesScale, PointElement, LineElement);
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]; const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
export let PlayCountChartCard: Vue.Component = { 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() { data() {
return { return {
chartData: { chartData: {
labels: this.data_points.map((e: CountSample) => {
var date = new Date(e.timeStamp);
return `${months[date.getMonth()]} ${date.getFullYear()}`;
}),
datasets: [{ datasets: [{
// label: '# of Votes', data: this.data_points.map((e: CountSample) => {
data: this.data_points.map((e: CountSample) => e.value), return {x: e.timeStamp, y: e.value};
}),
}] }]
} }
} }
@ -43,22 +41,82 @@ export let PlayCountChartCard: Vue.Component = {
options: { options: {
elements: { elements: {
line: { line: {
borderWidth: 4, borderWidth: 5,
borderColor: "#a34c77", borderColor: this.colour,
backgroundColor: "#727272",
borderCapStyle: "round", borderCapStyle: "round",
borderJoinStyle: "round" borderJoinStyle: "round"
}, },
// point: { point: {
// radius: 4, radius: 0
// pointStyle: "circle", }
// borderColor: "black",
// backgroundColor: "white"
// }
}, },
scales: { scales: {
yAxis: { yAxis: {
suggestedMin: 0 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 signalR from "@microsoft/signalr";
import * as Vue from "vue"; import * as Vue from "vue";
import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO } from "./HubInterfaces"; import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO, CountSample } from "./HubInterfaces";
import NowPlayingCard from "./Now/NowPlayingCard"; import NowPlayingCard from "./Now/NowPlayingCard";
import { AudioFeatureCard, AudioFeatureChartCard, PopularityCard, SpotifyLogoLink } from "./Now/Spotify"; 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 { PlayCountCard, LastFmLogoLink } from "./Now/LastFm";
import BaseInfoCard from "./Now/BaseInfoCard"; import BaseInfoCard from "./Now/BaseInfoCard";
import { DateTime } from "luxon";
const connection = new signalR.HubConnectionBuilder() const connection = new signalR.HubConnectionBuilder()
.withUrl("/hub") .withUrl("/hub")
@ -28,6 +30,12 @@ interface NowPlaying {
cards: InfoCard[] cards: InfoCard[]
} }
export interface ScrobbleDataSeries {
label: string,
colour: string,
data: CountSample[]
}
const app = Vue.createApp({ const app = Vue.createApp({
data() { data() {
return { return {
@ -46,14 +54,6 @@ const app = Vue.createApp({
album_artist: this.currentlyPlaying.track.album.artists[0].name, 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(){ showArtistChart(){
return this.playCount !== null && this.playCount !== undefined && this.playCount.artistCountData.length > 3; return this.playCount !== null && this.playCount !== undefined && this.playCount.artistCountData.length > 3;
}, },
@ -62,6 +62,33 @@ const app = Vue.createApp({
}, },
showTrackChart(){ showTrackChart(){
return this.playCount !== null && this.playCount !== undefined && this.playCount.trackCountData.length > 3; 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() { created() {
@ -123,4 +150,6 @@ app.component("spotify-logo", SpotifyLogoLink);
app.component("lastfm-logo", LastFmLogoLink); app.component("lastfm-logo", LastFmLogoLink);
app.component("play-count-card", PlayCountCard); app.component("play-count-card", PlayCountCard);
app.component("play-count-chart-card", PlayCountChartCard); 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'); const vm = app.mount('#app');

View File

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