var width = 500, height = 500, start = 0, end = 2.25, numSpirals = 4 margin = {top:50,bottom:50,left:50,right:50}; var theta = function(r) { return numSpirals * Math.PI * r; }; // used to assign nodes color by group var color = d3.scaleOrdinal() // D3 Version 4 .domain(1,20,300,4000,50000) .range(["#B8DE29","#238A8D","#55C667", '#A7226E','#440154']);; var r = d3.min([width, height]) / 2-20 ; var radius = d3.scaleLinear() .domain([start, end]) .range([30, r]); var svg = d3.select("#chart").append("svg") .attr("width", width + margin.right + margin.left) .attr("height", height + margin.left + margin.right) .append("g") .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); var points = d3.range(start, end + 0.001, (end - start) / 1000); var spiral = d3.radialLine() .curve(d3.curveCardinal) .angle(theta) .radius(radius); var path = svg.append("path") .datum(points) .attr("id", "spiral") .attr("d", spiral) .style("fill", "none") .style("stroke", "lightgrey") .style("stroke-dasharray", ("6, 5")) .style("opacity",0.2); var spiralLength = path.node().getTotalLength(), N = 274, barWidth = (spiralLength / N) - 1; var formatNum=d3.format(".2s") //define data d3.csv('data_leak.csv', function(error, someData) { if (error) throw error; // format the data someData.forEach(function(d) { d.idx = +d.idx; d.num_records_stolen = +d.num_records_stolen; d.group=d.group; d.data_sensitivity=+d.data_sensitivity; d.method_of_leak=d.method_of_leak }); var timeScale = d3.scaleLinear() .domain(d3.extent(someData, function(d){ return d.idx; })) .range([0, spiralLength]); // yScale for the bar height var yScale = d3.scalePow().exponent(0.5) .domain(d3.extent(someData, d=> d.num_records_stolen)) .range([0, (r / numSpirals)-10 ]); svg.selectAll("circle") .data(someData) .enter() .append("circle") .attr("cx", function(d,i){ var linePer = timeScale(d.idx), posOnLine = path.node().getPointAtLength(linePer), angleOnLine = path.node().getPointAtLength(linePer - barWidth); d.linePer = linePer; // % distance are on the spiral d.cx = posOnLine.x; // x postion on the spiral d.cy = posOnLine.y; // y position on the spiral d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 180 / Math.PI) - 90; //angle at the spiral position return d.cx; }) .attr("cy", function(d){ return d.cy; }) .attr("r", d=>yScale(d.num_records_stolen)) .attr("opacity", 0.85) .style("fill", d=>color(d.data_sensitivity)) .style("stroke", d=>d.method_of_leak=='hacking'?"black":"none") .style("stroke-dasharray", ("1, 2")) .style("stroke-width", 1.2); // add date labels svg.selectAll("text") .data(someData) .enter() .append("text") .attr("dy", 10) .style("text-anchor", "start") .style("font", "10px arial") .append("textPath") // only add for the first of each month .filter(d=>d.first==1) .text(d=>d.year) // place text along spiral .attr("xlink:href", "#spiral") .style("fill", "grey") .attr("startOffset", function(d){ return ((d.linePer / spiralLength) * 100) + "%"; }) //add select company label svg.selectAll(null) .data(someData) .enter() .append("text") .attr("x", function(d,i){ var linePer = timeScale(d.idx), posOnLine = path.node().getPointAtLength(linePer), angleOnLine = path.node().getPointAtLength(linePer - barWidth); d.linePer = linePer; // % distance are on the spiral d.x = posOnLine.x; // x postion on the spiral d.y = posOnLine.y; // y position on the spiral d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 180 / Math.PI) - 90; //angle at the spiral position return d.x-yScale(d.num_records_stolen)*0.6; }) .attr("y", function(d){ return d.y+6; }) .text(d=>(d.entity==='Fb'&&d.year==2014)||(d.num_records_stolen>200000000 && (d.data_sensitivity==20||d.data_sensitivity==300||d.data_sensitivity==4000|| d.data_sensitivity==50000))?d.entity:'') .attr('fill','white') .style('font-size', function(d) { return Math.min(2 * yScale(d.num_records_stolen), (2 * yScale(d.num_records_stolen) - 40) / this.getComputedTextLength() * 20) + "px"; }) //tooltip var tooltip = d3.select("#chart") .append('div') .attr('class', 'tooltip'); tooltip.append('div') .attr('class', 'date'); tooltip.append('div') .attr('class', 'value'); svg.selectAll("circle") .on('mouseover', function(d) { tooltip .style('position', 'absolute') .style('left', `${d3.event.pageX + 10}px`) .style('top', `${d3.event.pageY + 20}px`) .style('display', 'inline-block') .style('opacity', '0.9') .html(` ${d.year}
${d.entity}
lost ${formatNum(d.records_lost)} records due to ${d.method_of_leak}

${d.story}
`); }) .on('mouseout', function(d) { d3.selectAll("rect") .style("fill", function(d){return color(d.group);}) .style("stroke", "none") tooltip.style('display', 'none'); tooltip.style('opacity',0); }); })