D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
ienwhang
Full window
Github gist
Spotify Radial Line Map
<!DOCTYPE html> <html lang="en"> <meta charset="utf-8"> <head> <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet"> <title>Radial Map</title> </head> <style type="text/css"> .outerBoundary { fill: white; stroke: rgb(169,169,169); /* stroke: none; */ stroke-width: 1; } .innerBoundary { fill: white; stroke: rgb(169,169,169); /* stroke: none; */ stroke-width: 1; } .chartTitle { font-family: 'Source Sans Pro', sans-serif; font-weight: 600; font-size: 25px; /*font-style: italic;*/ } .title { font-family: 'Source Sans Pro', sans-serif; font-weight: 600; font-size: 18.5px; } .artist { font-family: 'Source Sans Pro', sans-serif; font-weight: 400; font-size: 14px; } .rank { font-family: 'Source Sans Pro', sans-serif; font-weight: 400; font-size: 14px; } .line { fill: none; /*stroke: orange;*/ stroke-width: 4; opacity: 0.7; } .keyCircle { fill: none; stroke: rgb(169,169,169); stroke-width: 4; opacity: 0.7; } .labels { font-family: 'Source Sans Pro', sans-serif; font-weight: 600; font-size: 10px; } .nextButton, .prevButton { fill: rgb(40,70,70); } .prevButtonRadius, .nextButtonRadius { fill: white; stroke: rgb(169,169,169); stroke-width: 1; } </style> <script src="https://d3js.org/d3.v4.min.js"></script> <body> <script type="text/javascript"> function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } var canvasWidth = 1000, canvasHeight = 600, h = 500, w = 900, mid = w/2; pad = 100, innerRad = 75, interval = 95; // total to be 170 var svg = d3.select("body") .append("svg") .attr("height", canvasHeight) .attr("width", canvasWidth); svg.append("circle") .attr("class", "outerBoundary") .attr("cy", h/2) .attr("cx", w/2) .attr("r", innerRad + interval); svg.append("circle") .attr("class", "innerBoundary") .attr("cy", h/2) .attr("cx", w/2) .attr("r", innerRad); svg.append("text") .attr("class", "chartTitle") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", h/2 - 16) .text("Spotify"); svg.append("text") .attr("class", "chartTitle") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", h/2 + 8) .text("Top 100"); svg.append("text") .attr("class", "chartTitle") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", h/2 + 32) .text("2017"); var line = d3.line() .x(function(d) { return d[0]; }) .y(function(d) { return d[1]; }) .curve(d3.curveCardinalClosed.tension(0.1)); var buttonLine = d3.line() .x(function(d) { return d[0]; }) .y(function(d) { return d[1]; }); var triH = 21, // triangle dims triW = 14, rectW = 5, buttonRadius = 23; var forTri = [[0,0], [0, triH], [triW, triH/2]], // forward triangle backTri = [[0,0], [0, triH], [-triW, triH/2]]; // backward triangle var forTriX = w/2 + 45, forRectX = forTriX + triW, forTriY = canvasHeight - 10 - triH/2 - 20, backTriX = w/2 - 45, backRectX = backTriX - triW, backTriY = canvasHeight - 10 - triH/2 - 20; // append buttons svg.append("circle") .attr("class", "nextButtonRadius") .attr("cy", forTriY + triH/2) .attr("cx", forTriX + ((triW + rectW)/2)) .attr("r", buttonRadius); svg.append("path") .datum(forTri) .attr("transform", "translate(" + forTriX + "," + forTriY + ")") .attr("fill", "black") .attr("class", "nextButton") .attr("d", buttonLine); svg.append("rect") .attr("class", "nextButton") .attr("transform", "translate(" + forRectX + "," + forTriY + ")") .attr("x", 0) .attr("y", 0) .attr("height", triH) .attr("width", rectW); svg.append("circle") .attr("class", "prevButtonRadius") .attr("cy", backTriY + triH/2) .attr("cx", backTriX - ((triW + rectW)/2)) .attr("r", buttonRadius); svg.append("path") .datum(backTri) .attr("transform", "translate(" + backTriX + "," + backTriY + ")") .attr("fill", "black") .attr("class", "prevButton") .attr("d", buttonLine); svg.append("rect") .attr("class", "prevButton") .attr("transform", "translate(" + backRectX + "," + backTriY + ")") .attr("x", 0 - rectW) .attr("y", 0) .attr("height", triH) .attr("width", rectW); // load in data d3.csv("featuresdf.csv", function(dataset) { // make strings numeric dataset.forEach(function(d) { d.acousticness = +d.acousticness; d.danceability = +d.danceability; d.duration_ms = +d.duration_ms; d.energy = +d.energy; d.instrumentalness = +d.instrumentalness; d.key = +d.key; d.liveness = +d.liveness; d.loudness = +d.loudness; d.mode = +d.mode; d.speechiness = +d.speechiness; d.tempo = +d.tempo; d.time_signature = +d.time_signature; d.valence = +d.valence; }); var features = ["acousticness", "danceability", "energy", "instrumentalness", "tempo", "liveness", "speechiness", "valence"]; // append labels to radius for (var i = 0; i < features.length; i++) { svg.append("text") .attr("class", "labels") .attr("transform", "translate(" + w/2 + "," + h/2 + ")rotate("+ (360/features.length) * i +")") .text(capitalizeFirstLetter(features[i])) .attr("x", innerRad + interval + 10) .attr("y", 0); } // function to generate coordinates in circle function getCoordinates(song, features) { var pairs = []; // array of coordinate pairs for (var i = 0; i < features.length; i++) { // console.log(song[features[i]]); // reference value var point = []; // array of single pair var angle = (2*Math.PI)/features.length; var scale = d3.scaleLinear().domain([d3.min(dataset, function(d) { return d[features[i]]}), d3.max(dataset, function(d) { return d[features[i]]})]).range([0, interval]); var x = (innerRad + scale(song[features[i]])) * (Math.cos(angle * i)), y = (innerRad + scale(song[features[i]])) * (Math.sin(angle * i)), pair = [x,y]; pairs.push(pair); } return pairs; // return pairs array } var songIndex = 0; // initialise starting index var song = dataset[songIndex]; // song reference var coordinates = getCoordinates(song, features); // get coordinates for song function getLineColour(mode) { if (mode == 1) { return "rgb(255,215,0)"; } else { return "rgb(255,99,71)"; } } // draw line between points var songLine = svg.append("path") .datum(coordinates) .attr("transform", "translate(" + w/2 + "," + h/2 + ")") .attr("class", "line") .attr("d", line) .style("stroke", getLineColour(song.mode)); // append title var songTitle = svg.append("text") .attr("class", "title") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", forTriY + triH/2 - 65) .text(song.name); // append artist var songArtist = svg.append("text") .attr("class", "artist") .text(song.artists) .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", forTriY + triH/2 - 45); // append ranking var rank = svg.append("text") .attr("class", "rank") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", forTriY + triH/2 + 4) .text(songIndex + 1 + "/100"); // key ring var scaleKey = d3.scaleLinear() .domain([d3.min(dataset, function(d) { return d.key}), d3.max(dataset, function(d) { return d.key})]) .range([innerRad, innerRad + interval]); var keyCircle = svg.append("circle") .attr("class", "keyCircle") .attr("cy", h/2) .attr("cx", w/2) .attr("r", scaleKey(song.key)); //On click, update with new data d3.selectAll(".nextButtonRadius, .nextButton") .on("click", function() { if (songIndex == 99) { songIndex = 0; // jump to start again } else { songIndex++; // increment song index } var nextSongCoordinates = getCoordinates(dataset[songIndex], features); // get song coordinates // change song line songLine.datum(nextSongCoordinates) .transition(300) .ease(d3.easeBack) .duration(500) .attr("transform", "translate(" + w/2 + "," + h/2 + ")") .attr("class", "line") .attr("d", line) .style("stroke", getLineColour(dataset[songIndex].mode)); // change song title songTitle.attr( "fill-opacity", 0) .transition(300) .attr( "fill-opacity", 1) .text(dataset[songIndex].name) .attr("class", "title") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", forTriY + triH/2 - 65); // change song artist songArtist.attr("fill-opacity", 0) .transition(300) .attr("fill-opacity", 1) .text(dataset[songIndex].artists) .attr("class", "artist") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", forTriY + triH/2 - 45); // change key keyCircle.transition(300) .ease(d3.easeBack) .duration(500) .attr("class", "keyCircle") .attr("cy", h/2) .attr("cx", w/2) .attr("r", scaleKey(dataset[songIndex].key)); // change rank rank.attr("fill-opacity", 0) .transition(300) .attr("fill-opacity", 1) .attr("class", "rank") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", forTriY + triH/2 + 4) .text(songIndex + 1 + "/100"); }); d3.selectAll(".prevButtonRadius, .prevButton") .on("click", function() { if (songIndex == 0) { songIndex = 99; // jump to end } else { songIndex--; // decrement song index } var lastSongCoordinates = getCoordinates(dataset[songIndex], features); // get song coordinates // change song line songLine.datum(lastSongCoordinates) .transition(300) .ease(d3.easeBack) .duration(500) .attr("transform", "translate(" + w/2 + "," + h/2 + ")") .attr("class", "line") .attr("d", line) .style("stroke", getLineColour(dataset[songIndex].mode)); // change song title songTitle.attr( "fill-opacity", 0) .transition(300) .attr( "fill-opacity", 1) .text(dataset[songIndex].name) .attr("class", "title") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", forTriY + triH/2 - 65); // change song artist songArtist.attr("fill-opacity", 0) .transition(300) .attr("fill-opacity", 1) .text(dataset[songIndex].artists) .attr("class", "artist") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", forTriY + triH/2 - 45); // change key keyCircle.transition(300) .ease(d3.easeBack) .duration(500) .attr("class", "keyCircle") .attr("cy", h/2) .attr("cx", w/2) .attr("r", scaleKey(dataset[songIndex].key)); // change rank rank.attr("fill-opacity", 0) .transition(300) .attr("fill-opacity", 1) .attr("class", "rank") .attr("text-anchor", "middle") .attr("x", w/2) .attr("y", forTriY + triH/2 + 4) .text(songIndex + 1 + "/100"); }); }); </script> </body> </html>
https://d3js.org/d3.v4.min.js