Skip to content

Instantly share code, notes, and snippets.

@arpitnarechania
Last active June 14, 2023 13:37
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save arpitnarechania/027e163073864ef2ac4ceb5c2c0bf616 to your computer and use it in GitHub Desktop.
Save arpitnarechania/027e163073864ef2ac4ceb5c2c0bf616 to your computer and use it in GitHub Desktop.
Condegram Spiral Plot
license: MIT

Condegram Spiral Plot to show data over time on a spiral curve.

<!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>
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);
});
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