D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
cpang4
Full window
Github gist
Circular layout
<!DOCTYPE html> <head> <meta charset="utf-8"> <script src="https://d3js.org/d3.v4.min.js"></script> <style type="text/css"> .line { fill: none; stroke: steelblue; stroke-width: 0.5; opacity: 0.2; } .line-hover { fill: none; stroke: red; stroke-width: 0.5; opacity: 0.9; } </style> </head> <body> <svg id="viz" height="600" width="1000"></svg> <script src="d3-scale-radial.js"></script> <script> var margin = {top: 10, right: 30, bottom: 20, left: 10}, width = 1000 - margin.left - margin.right, height = 600 - margin.top - margin.bottom; var svg = d3.select("#viz") .attr("width", width) .attr("height", height) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")") .attr("transform", "translate(" + (width/2) + "," + (height/2) + ")"); var links = svg.append("g"); d3.queue() .defer(d3.csv, 'tweetInfo.csv') .defer(d3.csv, 'tweets-withcategory-noblank.csv') .await(makeViz); function makeViz(error, info, tweets){ //date in file is in format: Sat Oct 06 18:23:46 +0000 2018 var parseDate = d3.timeParse("%a %b %e %H:%M:%S +0000 %Y"); var formatDate = d3.timeFormat("%a %b %e %H:%M:%S %Y"); var tickFormat = d3.timeFormat("%a %b %e %H:%M:%S"); var tickShort = d3.timeFormat("%H:%M:%S"); info.forEach(function(d) { d.time = parseDate(d.time); d.count = +d.count; }); tweets.forEach(function(d) { d.time = parseDate(d.time); }); var makeTimeScale = function(){ var legendData = []; var i=0; var radius = 65; var newScale = d3.scaleTime() .domain([d3.min(tweets, function(d){return d.time;}), d3.timeDay.floor(d3.max(tweets, function(d){return d.time;}))]); var ticks = newScale.ticks(9); while (radius <= 265){ if (i == ticks.length){ legendData.push({"size": radius, "value":""}); } else if (i == 0){ legendData.push({"size": radius, "value":tickFormat(ticks[i])}); i++; } else if (ticks[i].getDay() != ticks[i-1].getDay()){ legendData.push({"size": radius, "value":tickFormat(ticks[i])}); i++; }else{ legendData.push({"size": radius, "value":tickShort(ticks[i])}); i++; } radius = radius + 25; } var legend = svg.append("g") .attr("class", "legend") .selectAll("g") .data(legendData) .enter() .append("g"); legend.append("circle") .attr("r", function(d){return +d.size}) .attr("fill", "none") .style("stroke", "gray") .style("stroke-dasharray", function(d){ if (+d.size == 265){ return null; } else { return ("7,3"); } }) .style("stroke-opacity", 0.3); legend.append("text") .attr("y", function(d){ return -(+d.size);}) .attr("dy", "0.3em") .style("font", "10px Arial") .style("fill", "gray") .attr("text-anchor", "middle") .text(function(d) {return d.value}); } // end maketimescale makeTimeScale(); var countries = ["Africa", "Argentina", "Australia", "Belgium", "Brazil", "Canada", "Colombia", "France", "Germany", "Greece", "Hong Kong", "India", "Indonesia", "Ireland", "Italy", "Japan", "Malaysia", "Mexico", "Netherlands", "New Zealand", "Pakistan", "Scotland", "Spain", "UAE", "UK", "USA", "Other/Asia", "Other/Central America", "Other/Europe", "Other/E. Europe", "Other/Middle East", "Other/S. America", "Other/Oceania"]; var createLabels = function (numNodes, radius, dataset) { var nodes = [], angle, x, y, i; for (i=0; i<numNodes; i++) { angle = (i / (numNodes/2)) * (Math.PI)+5; // Calculate the angle at which the element will be placed. 0.05 adjusts the position of the labels near the middle x = (radius * Math.cos(angle)); y = (radius * Math.sin(angle)); nodes.push({'label': dataset[i], 'x': x, 'y': y}); } return nodes; } var createLabelsMap = function (numNodes, radius, dataset) { var nodes = [], angle, x, y, i; for (i=0; i<numNodes; i++) { angle = (i / (numNodes/2)) * (Math.PI)+5; // Calculate the angle at which the element will be placed. 0.05 adjusts the position of the labels near the middle x = (radius * Math.cos(angle)); y = (radius * Math.sin(angle)); nodes[dataset[i]] = {'x': x, 'y': y, 'index':i}; } return nodes; } label_info = createLabels(countries.length, 275, countries); labelsMap = createLabelsMap(countries.length, 265, countries); svg.selectAll("labels") .data(label_info) .enter() .append("text") .attr("x", function(d){return d.x;}) .attr("y", function(d){return d.y;}) .attr("text-anchor", function(d){ if (d.x < 0) return "end"; else return "start"; }) .attr("font-family", "Arial") .attr("font-size", "8px") .text(function(d){ return d.label; }) var createNodesMap = function (numNodes, radius) { var nodes = [], angle, x, y, i, name; for (i=0; i<numNodes; i++) { angle = (i / (numNodes/2)) * Math.PI; // Calculate the angle at which the element will be placed. x = (radius * Math.cos(angle)); y = (radius * Math.sin(angle)); name = info[i].name; nodes[name] = {'index': i, 'data': info[i], 'x': x, 'y': y}; } return nodes; } var createNodes = function (numNodes, radius) { var nodes = [], angle, x, y, i, name; for (i=0; i<numNodes; i++) { angle = (i / (numNodes/2)) * Math.PI; // Calculate the angle at which the element will be placed. x = (radius * Math.cos(angle)); y = (radius * Math.sin(angle)); nodes.push({'index': i, 'data': info[i], 'x': x, 'y': y}); } return nodes; } // where data.length = number of tweets, 40 = radius of inside circle // should not be edited nodes = createNodes(info.length, 40); nodesMap = createNodesMap(info.length, 40); var shown = {}; Object.keys(nodesMap).forEach(function(key) { shown[key] = false; }); var getRTPositions = function (dataset, numNodes) { var result = []; var ind = 0, radius, location; for (ind=0; ind < dataset.length; ind++) { location = dataset[ind].category; radius = radialScale(dataset[ind].time); var i = labelsMap[location].index; angle = (i / (numNodes/2)) * Math.PI+5; x = (radius * Math.cos(angle)); y = (radius * Math.sin(angle)); result.push({'category': location, 'x': x, 'y': y, 'time': dataset[ind].time}); } return result; } var retweetLines = function(dataset, news){ var result = {}, loc; for (i=0; i < dataset.length; i++) { loc = dataset[i].category; if (loc in result){ result[loc].push({'x': dataset[i].x, 'y': dataset[i].y}); } else{ result[loc] = []; // add connection to tweet result[loc].push({'x': nodesMap[news].x, 'y': nodesMap[news].y}); // then add RT location result[loc].push({'x': dataset[i].x, 'y':dataset[i].y}); } } //at end, end connection to category edge Object.keys(result).forEach(function(key) { result[key].push({'x': labelsMap[key].x, 'y':labelsMap[key].y}); }); return result; } //Define line generator line = d3.line() .x(function(d) { return (d.x); }) .y(function(d) { return (d.y); }); // for RT's var radialScale = d3.scaleLinear() .domain([d3.min(tweets, function(d){return d.time;}), d3.timeDay.floor(d3.max(tweets, function(d){return d.time;}))]) .range([65, 240]); var plotRetweets = function(news){ // sort the data so the line connects properly var filteredData = tweets.filter(function(d){return d.news == news}); filteredData.sort(function(a, b) { return d3.ascending(a.time, b.time); }); retweetData = getRTPositions(filteredData, countries.length); d3.selectAll("#retweetPoints-line" + news).remove(); d3.selectAll("#retweetPoints-points" + news).remove(); retweetMap = retweetLines(retweetData, news); Object.keys(retweetMap).forEach(function(key) { links.append("path") .datum(retweetMap[key]) .attr("id", "retweetPoints-line" + news) .attr("class", "line") .attr("d", line); }); svg.selectAll("retweets") .data(retweetData) .enter() .append("circle") .attr("id", "retweetPoints-points" + news) .attr("cx", function(d){ return d.x; }) .attr("cy", function(d){ return d.y; }) .attr("r", 2) .on("mouseover", function(d){ var xPosition = parseFloat(d3.select(this).attr("cx")); var yPosition = parseFloat(d3.select(this).attr("cy")) - 10; // apply white background so text is visible svg.append('text') .attr("id", "rttooltip" + news) .attr("x", xPosition) .attr("y", yPosition) .attr("font-family", "Arial") .attr("text-anchor", "middle") .attr("font-size", '8px') .style("stroke", "white") .style("stroke-width", "3.5px") .style("opacity", 0.8) .text(tickFormat(d.time)); // apply actual black text svg.append('text') .attr("id", "rttooltip" + news) .attr("x", xPosition) .attr("y", yPosition) .attr("font-family", "Arial") .attr("text-anchor", "middle") .attr("font-size", '8px') .text(tickFormat(d.time)); }) .on("mouseout", function(d){ d3.selectAll("#rttooltip" + news).remove(); }); } var nodes = svg.append("g") .selectAll("tweet-circles") .data(nodes) .enter() .append("circle") .attr("cx", function(d){return d.x;}) .attr("cy", function(d){return d.y;}) .attr("r", 5) .on("click", function(d){ if (shown[d.data.name]){ // already shown, remove d3.selectAll("#retweetPoints-line" + d.data.name).remove(); d3.selectAll("#retweetPoints-points" + d.data.name).remove(); shown[d.data.name] = false; d3.select(this).attr("fill", "black"); makeTotalCircles(); } else{ shown[d.data.name] = true; d3.select(this).attr("fill", "steelblue"); plotRetweets(d.data.name); makeTotalCircles(); } }) .on("mouseover", function(d){ d3.select(this).attr("fill", "red"); if (shown[d.data.name]){ d3.selectAll("#retweetPoints-line" + d.data.name).attr("class", "line-hover"); d3.selectAll("#retweetPoints-points" + d.data.name).attr("fill", "red"); } if (+d.index > 5 && +d.index < 14){ var xPosition = parseFloat(d3.select(this).attr("cx")) - 205; var yPosition = parseFloat(d3.select(this).attr("cy")) + 5; } else if (+d.index >= 14 && +d.index <= 16){ var xPosition = parseFloat(d3.select(this).attr("cx")) - 230; var yPosition = parseFloat(d3.select(this).attr("cy")) + 5; }else { var xPosition = parseFloat(d3.select(this).attr("cx")) + 10; var yPosition = parseFloat(d3.select(this).attr("cy")) + 10; } var tooltipStuff = svg.append("g"); var length = Math.ceil(d.data.text.length/30); tooltipStuff.append("rect") .attr("class", "tooltip") .attr("x", xPosition) .attr("y", yPosition) .attr("height", 25+(length*12)) .attr("width", 200) .attr("rx", 4) .attr("ry", 4) .attr("fill", "white") .style("fill-opacity", 0.9); tooltipStuff.append("svg:image") .attr("class", "tooltip") .attr("x", xPosition+5) .attr("y", yPosition+5) .attr("width", 40) .attr("height", 40) .attr("xlink:href", d.data.url); var htmlInput = "<b>" + d.data.name + "</b><br>" + formatDate(d.data.time) + "<br>" + d.data.text; tooltipStuff.append("foreignObject") .attr("class", "tooltip") .attr("width", 150) .attr("height", 25+(length*12)) .attr("x", xPosition+50) .attr("y", yPosition+3) .style("font", "10px 'Arial'") .style("color", "black") .html(htmlInput); }) .on("mouseout", function(d){ d3.selectAll(".tooltip").remove(); if (shown[d.data.name]){ d3.select(this).attr("fill", "steelblue"); } else{ d3.select(this).attr("fill", "black"); } d3.selectAll("#retweetPoints-line" + d.data.name).attr("class", "line"); d3.selectAll("#retweetPoints-points" + d.data.name).attr("fill", "black"); }); var countryMap = {}; var circleInfo = svg.append("g"); circleInfo.selectAll("totalCircles") .data(countries) .enter() .append("circle") .attr("class", "totalCircles") .attr("cx", function(d){return labelsMap[d].x;}) .attr("cy", function(d){return labelsMap[d].y;}) .attr("r", 0); circleInfo.selectAll("totalCircles-text") .data(countries) .enter() .append("text") .attr("class", "totalCircles-text") .attr("text-anchor", "middle") .attr("font-family", "Arial") .attr("font-size", '8px'); var makeTotalCircles = function(){ for (var i in countries){ countryMap[countries[i]] = 0; } Object.keys(shown).forEach(function(key) { if (shown[key]){ var filteredData = tweets.filter(function(d){return d.news == key}); for (var k in filteredData){ countryMap[filteredData[k].category] = countryMap[filteredData[k].category] + 1; } } }); d3.selectAll(".totalCircles") .data(countries) .transition() .attr("cx", function(d){return labelsMap[d].x;}) .attr("cy", function(d){return labelsMap[d].y;}) .attr("r", function(d){ if (countryMap[d] == 0){ return 0; }else{ return 10; }}) .attr("fill-opacity", 0.1); d3.selectAll(".totalCircles-text") .data(countries) .transition() .attr("x", function(d){return labelsMap[d].x;}) .attr("y", function(d){return labelsMap[d].y;}) .text(function(d){ if (countryMap[d] == 0){ return ""; }else{ return countryMap[d]; } }); } }; // end make viz </script> </body>
https://d3js.org/d3.v4.min.js