Condegram Spiral Plot to show data over time on a spiral curve.
forked from git-ashish's block: Condegram Spiral Plot, and updated to append another hidden rect to allow for better hover over UI
| license: MIT |
Condegram Spiral Plot to show data over time on a spiral curve.
forked from git-ashish's block: Condegram Spiral Plot, and updated to append another hidden rect to allow for better hover over UI
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> | |
| <title>Condegram Spiral Plot</title> | |
| <link rel="stylesheet" type="text/css" href="style.css"/> | |
| <script data-require="[email protected]" data-semver="4.0.0" src="https://d3js.org/d3.v4.js"></script> | |
| <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> | |
| </head> | |
| <body> | |
| <div id="chart"></div> | |
| <script type="text/javascript" src="main.js"></script> | |
| </body> | |
| </html> |
| console.clear() | |
| var width = 500, | |
| height = 500, | |
| start = 0, | |
| end = 2.25, | |
| numSpirals = 3 | |
| 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.schemeDark2); | |
| var r = d3.min([width, height]) / 2 - 40; | |
| var radius = d3.scaleLinear() | |
| .domain([start, end]) | |
| .range([40, 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 rectG = svg.append("g") | |
| var pathG = svg.append("g") | |
| var path = pathG.append("path") | |
| .datum(points) | |
| .attr("class", "spiral") | |
| .attr("d", spiral) | |
| .style("fill", "none") | |
| .style("stroke", "grey"); | |
| var spiralLength = path.node().getTotalLength(), | |
| N = 365, | |
| barWidth = (spiralLength / N) - 1; | |
| var someData = []; | |
| for (var i = 0; i < N; i++) { | |
| var currentDate = new Date(); | |
| currentDate.setDate(currentDate.getDate() + i); | |
| someData.push({ | |
| date: currentDate, | |
| value: Math.random(), | |
| group: currentDate.getMonth() | |
| }); | |
| } | |
| var timeScale = d3.scaleTime() | |
| .domain(d3.extent(someData, function(d){ | |
| return d.date; | |
| })) | |
| .range([0, spiralLength]); | |
| someData.forEach(function(d){ | |
| var linePer = timeScale(d.date), | |
| 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 | |
| }) | |
| // yScale for the bar height | |
| var yScale = d3.scaleLinear() | |
| .domain([0, d3.max(someData, function(d){ | |
| return d.value; | |
| })]) | |
| .range([0, (r / numSpirals) - 30]); | |
| var rects = rectG.selectAll("rect") | |
| .data(someData) | |
| .enter() | |
| .append("g") | |
| .attr("id", (d, i) => "rect-" + i ) | |
| .attr("transform", function(d, i){ | |
| return "translate(" + d.x + ", "+ d.y +")" | |
| }) | |
| rects.append("rect") | |
| .attr("class", "hover-rect") | |
| .attr("width", barWidth) | |
| .attr("height", yScale.range()[1]) | |
| .style("fill", "white") | |
| .style("stroke", "none") | |
| .attr("transform", function(d){ | |
| return "rotate(" + d.a + ")"; // rotate the bar | |
| }); | |
| rects.append("rect") | |
| .attr("class", "visible-rect") | |
| .attr("width", barWidth) | |
| .attr("height", function(d){ | |
| return yScale(d.value); | |
| }) | |
| .style("fill", function(d){return color(d.group);}) | |
| .style("stroke", "none") | |
| .attr("transform", function(d){ | |
| return "rotate(" + d.a + ")"; // rotate the bar | |
| }); | |
| // add date labels | |
| var tF = d3.timeFormat("%b %Y"), | |
| firstInMonth = {}; | |
| rectG.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(function(d){ | |
| var sd = tF(d.date); | |
| if (!firstInMonth[sd]){ | |
| firstInMonth[sd] = 1; | |
| return true; | |
| } | |
| return false; | |
| }) | |
| .text(function(d){ | |
| return tF(d.date); | |
| }) | |
| // place text along spiral | |
| .attr("xlink:href", "#spiral") | |
| .style("fill", function(d){ | |
| return color(d.date.getMonth()) | |
| }) | |
| .attr("startOffset", function(d){ | |
| return ((d.linePer / spiralLength) * 100) + "%"; | |
| }) | |
| var tooltip = d3.select("#chart") | |
| .append('div') | |
| .attr('class', 'tooltip'); | |
| tooltip.append('div').attr('class', 'date'); | |
| tooltip.append('div').attr('class', 'value'); | |
| svg.selectAll(".hover-rect") | |
| .on('mouseover', function(d, i) { | |
| tooltip.select('.date').html("Date: <b>" + d.date.toDateString() + "</b>"); | |
| tooltip.select('.value').html("Value: <b>" + Math.round(d.value*100)/100 + "<b>"); | |
| let selectedID = "#rect-" + i | |
| d3.select(selectedID).select(".visible-rect") | |
| .style("fill","#FFFFFF") | |
| .style("stroke","#000000") | |
| .style("stroke-width","1px"); | |
| tooltip.style('display', 'block'); | |
| tooltip.style('opacity',2); | |
| }) | |
| .on('mousemove', function(d) { | |
| tooltip.style('top', (d3.event.layerY + 10) + 'px') | |
| .style('left', (d3.event.layerX - 25) + 'px'); | |
| }) | |
| .on('mouseout', function(d) { | |
| d3.selectAll(".visible-rect") | |
| .style("fill", function(d){return color(d.group);}) | |
| .style("stroke", "none") | |
| tooltip.style('display', 'none'); | |
| tooltip.style('opacity',0); | |
| }); |
| body { | |
| font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; | |
| margin: 40px; | |
| } | |
| path.spiral { | |
| fill: none; | |
| stroke: #ee8d18; | |
| stroke-width: 1px; | |
| } | |
| .visible-rect { | |
| pointer-events: none; | |
| cursor: pointer | |
| } | |
| .hover-rect { | |
| cursor: pointer | |
| } | |
| .tooltip { | |
| background: #eee; | |
| box-shadow: 0 0 5px #999999; | |
| color: #333; | |
| font-size: 12px; | |
| left: 130px; | |
| padding: 10px; | |
| position: absolute; | |
| text-align: center; | |
| top: 95px; | |
| z-index: 10; | |
| display: block; | |
| opacity: 0; | |
| } |