time series, artist breakdown, start and end date
This commit is contained in:
parent
079e126648
commit
a1bd1ab62d
@ -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>
|
||||
|
||||
|
@ -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": "*"
|
||||
|
43
Selector.Web/package-lock.json
generated
43
Selector.Web/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
71
Selector.Web/scripts/Now/ArtistBreakdownGraph.ts
Normal file
71
Selector.Web/scripts/Now/ArtistBreakdownGraph.ts
Normal 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)'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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,22 +41,82 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user