From f624414701d9499537c02f03fe6c0cffef2756d3 Mon Sep 17 00:00:00 2001 From: Chris Shyi Date: Thu, 28 Jun 2018 21:52:24 -0400 Subject: [PATCH] Refactor audio features graph code Closes #44. Audio features graph code is now in an external .js file. Also closes #45. --- .../static/spotifyvis/scripts/artist_graph.js | 24 +++- .../spotifyvis/scripts/audio_feat_graph.js | 105 +++++++++++++++ .../templates/spotifyvis/artist_graph.html | 6 +- .../templates/spotifyvis/audio_features.html | 124 ++---------------- spotifyvis/urls.py | 4 +- 5 files changed, 136 insertions(+), 127 deletions(-) create mode 100644 spotifyvis/static/spotifyvis/scripts/audio_feat_graph.js diff --git a/spotifyvis/static/spotifyvis/scripts/artist_graph.js b/spotifyvis/static/spotifyvis/scripts/artist_graph.js index c1f48cb..023dfd9 100644 --- a/spotifyvis/static/spotifyvis/scripts/artist_graph.js +++ b/spotifyvis/static/spotifyvis/scripts/artist_graph.js @@ -5,13 +5,25 @@ */ function drawArtistGraph(artistData, parentElem) { let margin = {top: 20, right: 30, bottom: 30, left: 40}; - let width = 960 - margin.right - margin.left; - let height = 540 - margin.top - margin.bottom; + let width = 1000 - margin.right - margin.left; + let height = 1000 - margin.top - margin.bottom; let color = d3.scaleOrdinal(d3.schemeCategory10); + /* + ** Next four variables were part of an attempt to make bubbles larger, + ** didn't work + */ + let songCounts = artistData.children.map(function(artist) { return artist.num_songs; }); // array of counts + let songCountExtent = d3.extent(songCounts); // [min song count, max song count] + let circleSize = { + min: 45, + max: 75 + }; + let circleRadiusScale = d3.scaleSqrt().domain(songCountExtent).range([circleSize.min, circleSize.max]); + let bubble = d3.pack(artistData) - .size([width, height]) - .padding(1.5); + .size([width + 100, height + 100]) + .padding(0.2); let svg = d3.select(parentElem) .append("svg") @@ -23,7 +35,7 @@ function drawArtistGraph(artistData, parentElem) { .sum(function(d) { return d.num_songs; }); let node = svg.selectAll(".node") - .data(bubble(nodes).descendants()) + .data(bubble(nodes).leaves()) .enter() .filter(function(d) { return !d.children; @@ -36,7 +48,7 @@ function drawArtistGraph(artistData, parentElem) { node.append("title") .text(function(d) { - return `${d.name}: ${d.num_songs}`; + return d.data.name + ": " + d.data.num_songs; }); node.append("circle") diff --git a/spotifyvis/static/spotifyvis/scripts/audio_feat_graph.js b/spotifyvis/static/spotifyvis/scripts/audio_feat_graph.js new file mode 100644 index 0000000..1fab293 --- /dev/null +++ b/spotifyvis/static/spotifyvis/scripts/audio_feat_graph.js @@ -0,0 +1,105 @@ +/** Queries the backend for audio feature data, draws the bar chart + * illustrating the frequencies of values, and appends the chart to + * a designated parent element + * + * @param audioFeature: the name of the audio feature (string) + * @param intervalEndPoints: a sorted array of 5 real numbers defining the intervals (categories) of values, + * for example: + * [0, 0.25, 0.5, 0.75, 1.0] for instrumentalness would define ranges + * (0-0.25), (0.25-0.5), (0.5-0.75), (0.75-1.0) + * @param parentElem: the DOM element to append the graph to (a selector string) + * @param userSecret: the user secret string for identification + * @return None + */ +function drawAudioFeatGraph(audioFeature, intervalEndPoints, parentElem, userSecret) { + // TODO: Not hard code the dimensions? + let margin = {top: 20, right: 30, bottom: 30, left: 40}; + let width = 480 - margin.left - margin.right, + height = 270 - margin.top - margin.bottom; + + let featureData = {}; + // Create the keys first in order + for (let index = 0; index < intervalEndPoints.length - 1; index++) { + let key = `${intervalEndPoints[index]} ~ ${intervalEndPoints[index + 1]}`; + featureData[key] = 0; + } + // define the vertical scaling function + let vScale = d3.scaleLinear().range([height, 0]); + + d3.json(`/api/audio_features/${audioFeature}/${userSecret}`) + .then(function(response) { + // categorize the data points + for (let dataPoint of response.data_points) { + dataPoint = parseFloat(dataPoint); + let index = intervalEndPoints.length - 2; + // find the index of the first element greater than dataPoint + while (dataPoint < intervalEndPoints[index]) { + index -= 1; + } + let key = `${intervalEndPoints[index]} ~ ${intervalEndPoints[index + 1]}`; + featureData[key] += 1; + } + + let dataSet = Object.values(featureData); + let dataRanges = Object.keys(featureData); // Ranges of audio features, e.g. 0-0.25, 0.25-0.5, etc + let dataArr = []; + // turn the counts into an array of objects, e.g. {range: "0-0.25", counts: 5} + for (let i = 0; i < dataRanges.length; i++) { + dataArr.push({ + range: dataRanges[i], + counts: featureData[dataRanges[i]] + }); + } + vScale.domain([0, d3.max(dataSet)]).nice(); + + let hScale = d3.scaleBand().domain(dataRanges).rangeRound([0, width]).padding(0.5); + + let xAxis = d3.axisBottom().scale(hScale); + let yAxis = d3.axisLeft().scale(vScale); + + let featureSVG = d3.select(parentElem) + .append('svg').attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom); + + let featureGraph = featureSVG.append("g") + .attr("transform", `translate(${margin.left}, ${margin.top})`) + .attr("fill", "teal"); + + featureGraph.selectAll(".bar") + .data(dataArr) + .enter().append('rect') + .attr('class', 'bar') + .attr('x', function(d) { return hScale(d.range); }) + .attr('y', function(d) { return vScale(d.counts); }) + .attr("height", function(d) { return height - vScale(d.counts); }) + .attr("width", hScale.bandwidth()); + + // function(d) { return hScale(d.range); } + + featureGraph.append('g') + .attr('class', 'axis') + .attr('transform', `translate(0, ${height})`) + .call(xAxis); + + featureGraph.append('g') + .attr('class', 'axis') + .call(yAxis); + + featureSVG.append("text") + .attr('x', (width / 2)) + .attr('y', (margin.top / 2)) + .attr('text-anchor', 'middle') + .style('font-size', '14px') + .text(`${capFeatureStr(audioFeature)}`); + + }); +} + +/** + * Returns the audio feature name string with the first letter capitalized + * @param audioFeature: the name of the audio feature + * @returns the audio feature name string with the first letter capitalized + */ +function capFeatureStr(audioFeature) { + return audioFeature.charAt(0).toUpperCase() + audioFeature.slice(1); +} \ No newline at end of file diff --git a/spotifyvis/templates/spotifyvis/artist_graph.html b/spotifyvis/templates/spotifyvis/artist_graph.html index 433e39b..0e5d49a 100644 --- a/spotifyvis/templates/spotifyvis/artist_graph.html +++ b/spotifyvis/templates/spotifyvis/artist_graph.html @@ -11,13 +11,9 @@ + diff --git a/spotifyvis/urls.py b/spotifyvis/urls.py index c28cfcd..f0f8319 100644 --- a/spotifyvis/urls.py +++ b/spotifyvis/urls.py @@ -9,11 +9,11 @@ urlpatterns = [ path('user_data', user_data, name='user_data'), path('admin_graphs', admin_graphs, name='admin_graphs'), path('api/user_artists/', get_artist_data, name='get_artist_data'), - path('artists/', artist_data, name='display_artist_graph'), + path('graphs/artists/', artist_data, name='display_artist_graph'), path('api/user_genres/', get_genre_data, name='get_genre_data'), path('graphs/genre/', display_genre_graph, name='display_genre_graph'), - path('audio_features/', audio_features, name='display_audio_features'), + path('graphs/audio_features/', audio_features, name='display_audio_features'), path('api/audio_features//', get_audio_feature_data, name='get_audio_feature_data'), ]