Condegram Spiral Plot to show data over time on a spiral curve.
Last active
June 14, 2023 13:37
-
-
Save arpitnarechania/027e163073864ef2ac4ceb5c2c0bf616 to your computer and use it in GitHub Desktop.
Condegram Spiral Plot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: MIT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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="d3@4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.js"></script> | |
</head> | |
<body> | |
<div id="chart"></div> | |
<script type="text/javascript" src="main.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.schemeCategory10); | |
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 path = svg.append("path") | |
.datum(points) | |
.attr("id", "spiral") | |
.attr("d", spiral) | |
.style("fill", "none") | |
.style("stroke", "steelblue"); | |
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]); | |
// yScale for the bar height | |
var yScale = d3.scaleLinear() | |
.domain([0, d3.max(someData, function(d){ | |
return d.value; | |
})]) | |
.range([0, (r / numSpirals) - 30]); | |
svg.selectAll("rect") | |
.data(someData) | |
.enter() | |
.append("rect") | |
.attr("x", function(d,i){ | |
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 | |
return d.x; | |
}) | |
.attr("y", function(d){ | |
return d.y; | |
}) | |
.attr("width", function(d){ | |
return 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 + "," + d.x + "," + d.y + ")"; // rotate the bar | |
}); | |
// add date labels | |
var tF = d3.timeFormat("%b %Y"), | |
firstInMonth = {}; | |
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(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", "grey") | |
.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("rect") | |
.on('mouseover', function(d) { | |
tooltip.select('.date').html("Date: <b>" + d.date.toDateString() + "</b>"); | |
tooltip.select('.value').html("Value: <b>" + Math.round(d.value*100)/100 + "<b>"); | |
d3.select(this) | |
.style("fill","#FFFFFF") | |
.style("stroke","#000000") | |
.style("stroke-width","2px"); | |
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("rect") | |
.style("fill", function(d){return color(d.group);}) | |
.style("stroke", "none") | |
tooltip.style('display', 'none'); | |
tooltip.style('opacity',0); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; | |
margin: 40px; | |
} | |
.axis path { | |
fill: none; | |
stroke: #999; | |
stroke-dasharray: 2 3; | |
} | |
.axis text { | |
font-size: 13px; | |
stroke: #222; | |
} | |
text.title { | |
font-size: 24px; | |
} | |
circle.tick { | |
fill: #f3f3f3; | |
stroke: #999; | |
stroke-dasharray: 2 3; | |
} | |
path.spiral { | |
fill: none; | |
stroke: #ee8d18; | |
stroke-width: 3px; | |
} | |
.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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment