Skip to content

Instantly share code, notes, and snippets.

@rin9424
Created April 27, 2022 10:49
Show Gist options
  • Save rin9424/86c74450c6e05a83c374ac9291f60f60 to your computer and use it in GitHub Desktop.
Save rin9424/86c74450c6e05a83c374ac9291f60f60 to your computer and use it in GitHub Desktop.
export type BoxPlotConfig = {
figure_id: string;
domain_lower_limit: number;
domain_upper_limit: number;
dot_color:string;
feature_name: AllFeatures
}
export type AllFeatures = 'danceability' |
'energy' |
'speechiness' |
'acousticness' |
'instrumentalness' |
'liveness' |
'valence' |
'tempo' |
'loudness'|
'time_signature';
export type keys = 'danceability' | 'energy' | 'speechiness' | 'acousticness' | 'instrumentalness' | 'liveness' | 'valence'
export type summaryValue = {
q1:number;
median:number;
q3:number;
interQuantileRange:number;
min:number;
max:number;
}
export type summaryObject = {
key: keys;
value: summaryValue
}
<div class="container">
<div class="row gx-1">
<div class="col-xs-3">
<app-playlists-sidebar [playlists]="playlists"></app-playlists-sidebar>
</div>
<div class="col-xs-4">
<router-outlet></router-outlet>
</div>
<div class="col-xs-5">
<app-track></app-track>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="h2">
Number of Tracks Per Playlist
</div>
<figure id="bar-chart"></figure>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="h2">
Audio Features with [0,1] domain
</div>
<button type="button" class="btn btn-primary" (click)="toogleDistribution()"> Toggle Distribution </button>
<figure id="multi-box-plot"></figure>
</div>
</div>
<div class="row">
<div class="col-xs-4">
<div class="h2">
Tempo
</div>
<figure id="box-plot-tempo"></figure>
</div>
<div class="col-xs-4">
<div class="h2">
Loudness
</div>
<figure id="box-plot-loudness"></figure>
</div>
<div class="col-xs-4">
<div class="h2">
Time Signature
</div>
<figure id="box-plot-time-signature"></figure>
</div>
</div>
<div class="row">
<div class="col-xs-4">
<div class="h2">
Mode (0 | 1)
</div>
<figure id="bar-chart-mode"></figure>
</div>
</div>
</div>
.hidden{
display: none;
}
import { Component, OnDestroy, OnInit } from '@angular/core';
import * as d3 from 'd3';
import { SpotifyService } from '../core/services/spotify.service';
import { catchError, concatMap, delay, forkJoin, from, map, mergeMap, Observable, of } from 'rxjs';
import { flatten, pick } from 'lodash';
import { audioFeatures, BoxPlotConfig, keys, summaryObject } from '../core/classes/audioFeatures';
@Component({
selector: 'app-playlists',
templateUrl: './playlists.component.html',
styleUrls: ['./playlists.component.scss']
})
export class PlaylistsComponent implements OnInit, OnDestroy {
playlistSub:any;
playlists: any;
barChartData: any;
savedTracks:any;
totalSavedTracks:any;
savedTracks$:Observable<any> | undefined;
audioFeaturesSub:any;
distributionShown!:boolean;
svgsForBoxPlot:Array<any> = [];
private svg:any;
private margin = 50;
private width = 750 - (this.margin * 2);
private height = 400 - (this.margin * 2);
savedTracksSub: any;
multiBoxPlotSvg:any;
constructor(
private spotify:SpotifyService
) {
}
ngOnInit(): void {
this.loadPlaylists();
this.loadTotalOfSavedTracks();
this.drawMultiBoxPlot();
this.drawOtherBoxPlots();
this.distributionShown = true;
this.drawModeBarChart();
}
ngOnDestroy(): void {
this?.playlistSub?.unsubscribe();
}
loadPlaylists(){
this.playlistSub = this.spotify.perform("endpoint-get-a-list-of-current-users-playlists",{
queryParams:{
limit: 50
}
})
.subscribe(
(data:any) => {
this.playlists = data;
this.barChartData = data.items.map( (item:any) => {
return {
playlistName: item.name,
totalTracks: item.tracks.total
}
})
this.drawChart();
}
);
}
loadTotalOfSavedTracks(){
this.savedTracksSub = this.spotify.perform("endpoint-get-users-saved-tracks",{
queryParams:{
limit:50
}
}).subscribe({
next:(savedTracks:any) => {
this.totalSavedTracks = savedTracks.total
this.loadSavedTracks();
}
})
}
loadSavedTracks(){
console.log("Loading ",this.totalSavedTracks, " tracks.")
let index = Math.ceil(this.totalSavedTracks/50)
let obs = [];
if(index){
for(let i = 0; i < index; i++){
let observable = this.spotify.perform("endpoint-get-users-saved-tracks",{
queryParams:{
limit:50,
offset: i*50
}
}).pipe(
map( (tracks:any) => {
return tracks.items
})
)
obs.push(observable);
}
forkJoin(obs).subscribe(
(savedTracks:any) => {
this.savedTracks = flatten(savedTracks);
console.log(this.savedTracks);
// this.loadAudioFeatures();
}
)
}
}
loadAudioFeatures(){
this.savedTracks$ = from(this.savedTracks).pipe(
concatMap(x => of(x)
.pipe(
delay(125))
)
)
this.audioFeaturesSub = this.savedTracks$.pipe(
mergeMap( (track:any) => {
let audioFeatures$;
audioFeatures$ = this.spotify.perform("endpoint-get-audio-features",{
route_params:{
id:track.track.id
}
})
return audioFeatures$.pipe(
catchError((err:any) => {
return of(err)
})
)
},
10
)
)
.subscribe({
next: (track:any) => {
let index = this.savedTracks.map((track:any) => track.track.id).indexOf(track.id)
console.log(index);
console.log(track)
this.savedTracks[index].audioFeatures = track
},
error: (error:any) => {
console.log(error)
},
complete: () => {
console.log("Complete")
console.log(this.savedTracks)
}
})
}
createSVG(){
this.svg = d3.select("figure#bar-chart")
.append("svg")
.attr("width", this.width + (this.margin * 2))
.attr("height", this.height + (this.margin * 2))
.append("g")
.attr("transform", "translate(" + this.margin + "," + this.margin + ")");
}
drawBars(data:any){
const x = d3.scaleBand()
.range([0, this.width])
.domain(data.map((d:any) => d.playlistName))
.padding(0.2);
// Draw the X-axis on the DOM
this.svg.append("g")
.attr("transform", "translate(0," + this.height + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "translate(-10,0)rotate(-45)")
.style("text-anchor", "end");
// Create the Y-axis band scale
const y = d3.scaleLinear()
.domain([0, 150])
.range([this.height, 0]);
// Draw the Y-axis on the DOM
this.svg.append("g")
.call(d3.axisLeft(y));
// Create and fill the bars
this.svg.selectAll("bars")
.data(data)
.enter()
.append("rect")
.attr("x", (d:any) => x(d.playlistName))
.attr("y", (d:any) => y(d.totalTracks))
.attr("width", x.bandwidth())
.attr("height", (d:any) => this.height - y(d.totalTracks))
.attr("fill", "#d04a35");
}
drawChart(){
this.createSVG();
this.drawBars(this.barChartData);
}
drawBoxPlot(config:BoxPlotConfig){
var margin = {top: 10, right: 30, bottom: 30, left: 40};
var width = 300 - margin.left - margin.right;
var height = 300 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select(`#${config.figure_id}`)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
var data = audioFeatures.map((obj:any) => obj[config.feature_name]);
var data_sorted = data.sort(d3.ascending);
var q1:any = d3.quantile(data_sorted, .25)
var median = d3.quantile(data_sorted, .5)
var q3:any = d3.quantile(data_sorted, .75)
var interQuantileRange = q3 - q1
var min = q1 - 1.5 * interQuantileRange
var max = q1 + 1.5 * interQuantileRange
// Show the Y scale
var y = d3.scaleLinear()
.domain([config.domain_lower_limit,config.domain_upper_limit])
.range([height, 0]);
svg.call(d3.axisLeft(y))
// a few features for the box
var center = 150
var width = 100
// Show the main vertical line
svg
.append("line")
.attr("x1", center)
.attr("x2", center)
.attr("y1", y(min) )
.attr("y2", y(max) )
.attr("stroke", "black")
// Show the box
svg
.append("rect")
.attr("x", center - width/2)
.attr("y", y(q3) )
.attr("height", (y(q1)-y(q3)) )
.attr("width", width )
.attr("stroke", "black")
.style("fill", "#69b3a2")
// show median, min and max horizontal lines
svg
.selectAll("toto")
.data([min, median, max])
.enter()
.append("line")
.attr("x1", center-width/2)
.attr("x2", center+width/2)
.attr("y1", function(d){ return(y(d))} )
.attr("y2", function(d){ return(y(d))} )
.attr("stroke", "black")
// add dots
var jitterWidth = 50
svg
.selectAll(".indPoints")
.data(data_sorted)
.enter()
.append("circle")
.attr("cx", function(d){return(center - jitterWidth/2 + Math.random()*jitterWidth )})
.attr("cy", function(d){return(y(d))})
.attr("r", 4)
.style("fill", config.dot_color)
.attr("stroke", "black")
.classed(`${config.feature_name}`,true)
.classed("point",true)
.classed("shown",true)
this.svgsForBoxPlot.push(svg);
}
drawMultiBoxPlot(){
let keys_of_interests:Array<keys> = ['danceability', 'energy', 'speechiness', 'acousticness', 'instrumentalness', 'liveness', 'valence'];
let keyVals:Array<summaryObject> = [
]
let audioFeaturesForDots = audioFeatures.map((obj:any) => pick(obj,...keys_of_interests));
keys_of_interests.forEach((key:keys) => {
let featureValues = audioFeaturesForDots.map((featureObj:any) => featureObj[key]);
let sortedFeatureValues = featureValues.sort(d3.ascending);
var q1:any= d3.quantile(sortedFeatureValues, .25)
var median:any = d3.quantile(sortedFeatureValues, .5)
var q3:any = d3.quantile(sortedFeatureValues, .75)
var interQuantileRange:any = q3 - q1
var min:any = q1 - 1.5 * interQuantileRange
var max:any = q1 + 1.5 * interQuantileRange
let summaryObj:summaryObject = {
key: key,
value: {
q1: q1,
median:median,
q3:q3,
interQuantileRange:interQuantileRange,
min:min,
max:max
}
}
keyVals.push(summaryObj);
})
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 40},
width = 1000 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#multi-box-plot")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Show the X scale
var x:any = d3.scaleBand()
.range([ 0, width ])
.domain(keys_of_interests)
.paddingInner(1)
.paddingOuter(.5)
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
// Show the Y scale
var y:any = d3.scaleLinear()
.domain([0,1])
.range([height, 0])
svg.append("g").call(d3.axisLeft(y))
// Show the main vertical line
svg
.selectAll(".vertLines")
.data(keyVals)
.enter()
.append("line")
.attr("x1", (d:summaryObject) => {
return x(d.key)
})
.attr("x2", (d:summaryObject) => {
return x(d.key)
})
.attr("y1",(d:any) => {
return y(d.value.min)
})
.attr("y2", (d:any) => {
return y(d.value.max)
})
.attr("stroke", "black")
.style("width", 40)
// rectangle for the main box
var boxWidth = 100
svg
.selectAll("boxes")
.data(keyVals)
.enter()
.append("rect")
.attr("x", function(d:any){return(x(d.key) as any-boxWidth/2)})
.attr("y", function(d:any){return(y(d.value.q3))})
.attr("height", function(d:any){return(y(d.value.q1)-y(d.value.q3))})
.attr("width", boxWidth )
.attr("stroke", "black")
.style("fill", "#69b3a2")
// Show the median
svg
.selectAll("medianLines")
.data(keyVals)
.enter()
.append("line")
.attr("x1", function(d:any){return(x(d.key) as any -boxWidth/2) })
.attr("x2", function(d:any){return(x(d.key) as any +boxWidth/2) })
.attr("y1", function(d:any){return(y(d.value.median))})
.attr("y2", function(d:any){return(y(d.value.median))})
.attr("stroke", "black")
.style("width", 80)
// Add individual points with jitter
let dotValues:any = {};
dotValues.danceability = audioFeaturesForDots.map((obj:any) => obj.danceability)
dotValues.energy = audioFeaturesForDots.map((obj:any) => obj.energy)
dotValues.speechiness = audioFeaturesForDots.map((obj:any) => obj.speechiness)
dotValues.acousticness = audioFeaturesForDots.map((obj:any) => obj.acousticness)
dotValues.instrumentalness = audioFeaturesForDots.map((obj:any) => obj.instrumentalness)
dotValues.liveness = audioFeaturesForDots.map((obj:any) => obj.liveness)
dotValues.valence = audioFeaturesForDots.map((obj:any) => obj.valence)
let dotFills:any = {
danceability: "maroon",
energy:"purple",
speechiness: "fuchsia",
acousticness: "olive",
instrumentalness: "blue",
liveness: "teal",
valence:"aqua"
}
keys_of_interests.forEach((key:string) => {
var jitterWidth = 50
svg
.selectAll(".indPoints")
.data(dotValues[key])
.enter()
.append("circle")
.attr("cx", function(d){return(x(key) - jitterWidth/2 + Math.random()*jitterWidth )})
.attr("cy", function(d){return(y(d))})
.attr("r", 4)
.style("fill", dotFills[key])
.attr("stroke", "black")
.classed(`${key}`,true)
.classed("point",true)
.classed("shown",true)
})
this.svgsForBoxPlot.push(svg);
}
toogleDistribution(){
if(this.distributionShown){
this.svgsForBoxPlot.forEach((svg:any) => {
svg.selectAll(".point.shown")
.classed("shown",false)
.classed("hidden",true);
})
this.distributionShown = false;
}else{
this.svgsForBoxPlot.forEach((svg:any) => {
svg.selectAll(".point.hidden")
.classed("hidden",false)
.classed("shown",true);
})
this.distributionShown = true;
}
}
drawOtherBoxPlots(){
let tempo_config:BoxPlotConfig = {
figure_id: "box-plot-tempo",
domain_lower_limit: 0,
domain_upper_limit: 200,
dot_color:"blue",
feature_name: "tempo",
}
let time_signature_config:BoxPlotConfig = {
figure_id: "box-plot-time-signature",
domain_lower_limit: 3,
domain_upper_limit: 7,
dot_color:"maroon",
feature_name: "time_signature",
}
let loudness_config:BoxPlotConfig = {
figure_id: "box-plot-loudness",
domain_lower_limit: -60,
domain_upper_limit: 0,
dot_color:"purple",
feature_name: "loudness",
}
this.drawBoxPlot(tempo_config);
this.drawBoxPlot(time_signature_config);
this.drawBoxPlot(loudness_config);
}
drawModeBarChart(){
var svg = d3.select("figure#bar-chart-mode")
.append("svg")
.attr("width", this.width + (this.margin * 2))
.attr("height", this.height + (this.margin * 2))
.append("g")
.attr("transform", "translate(" + this.margin + "," + this.margin + ")");
let data = [];
let stats = audioFeatures.map((track:any) => track.mode);
let zero_values = stats.filter((mode:number) => mode === 0).length;
let one_values = stats.filter((mode:number) => mode === 1).length;
data.push({
mode: 0,
count: zero_values
})
data.push({
mode: 1,
count: one_values
})
const x:any = d3.scaleBand()
.range([0, this.width])
.domain(data.map((d:any) => d.mode))
.padding(0.2);
// Draw the X-axis on the DOM
svg.append("g")
.attr("transform", "translate(0," + this.height + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "end");
// Create the Y-axis band scale
const y:any = d3.scaleLinear()
.domain([0, 150])
.range([this.height, 0]);
// Draw the Y-axis on the DOM
svg.append("g")
.call(d3.axisLeft(y));
// Create and fill the bars
svg.selectAll("bars")
.data(data)
.enter()
.append("rect")
.attr("x", (d:any) => x(d.mode))
.attr("y", (d:any) => y(d.count))
.attr("width", x.bandwidth())
.attr("height", (d:any) => this.height - y(d.count))
.attr("fill", "maroon");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment