Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active January 25, 2022 10:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomshanley/4080b28445785939b3f043b8c5b63e22 to your computer and use it in GitHub Desktop.
Save tomshanley/4080b28445785939b3f043b8c5b63e22 to your computer and use it in GitHub Desktop.
Reusable spiral heatmap
license: mit
// A spiral heatmap
// The following options are available
// radius: radius of the overall plot, not including any labels. Default 250
// holeRadiusProportion: the proportion (0 to 1) of the radius (see above) that is left as a hole. Default 0.
// arcsPerCoil: a 'coil' is one revolution of the spiral. This sets how many arcs (or arcs) you want per coil. Typically this would
// be set according to the periodicity of the data. For example, 12 for months per year, 24 for hours per day, etc
// coilPadding: the proportion (0 to 1) of the coil width that is used for padding between coils. Useful for making the spiral very noticeable
// arcLabel: the field name to use for the labels around the circumference
// coilLabel: the field name to use for the labels at the beginning of each coil
function spiralHeatmap () {
// constants
const radians = 0.0174532925
// All options that are accessible to caller
// Default values
var radius = 250
var holeRadiusProportion = 0.3 // proportion of radius
var arcsPerCoil = 12 // assuming months per year
var coilPadding = 0 // no padding
var arcLabel = '' // no labels
var coilLabel = '' // no labels
function chart (selection) {
selection.each(function (data) {
const arcAngle = 360 / arcsPerCoil
const labelRadius = radius + 20
var arcLabelsArray = []
for (var i = 0; i < arcsPerCoil; i++) {
arcLabelsArray.push(i)
}
// Create/update the x/y coordinates for the vertices and control points for the paths
// Stores the x/y coordinates on the data
updatePathData(data)
let thisSelection = d3
.select(this)
.append('g')
.attr('class', 'spiral-heatmap')
var arcLabelsG = thisSelection
.selectAll('.arc-label')
.data(arcLabelsArray)
.enter()
.append('g')
.attr('class', 'arc-label')
arcLabelsG
.append('text')
.text(function (d) {
return data[d][arcLabel]
})
.attr('x', function (d, i) {
let labelAngle = i * arcAngle + arcAngle / 2
return x(labelAngle, labelRadius)
})
.attr('y', function (d, i) {
let labelAngle = i * arcAngle + arcAngle / 2
return y(labelAngle, labelRadius)
})
.style('text-anchor', function (d, i) {
return i < arcLabelsArray.length / 2 ? 'start' : 'end'
})
arcLabelsG
.append('line')
.attr('x2', function (d, i) {
let lineAngle = i * arcAngle
let lineRadius = chartRadius + 10
return x(lineAngle, lineRadius)
})
.attr('y2', function (d, i) {
let lineAngle = i * arcAngle
let lineRadius = chartRadius + 10
return y(lineAngle, lineRadius)
})
var arcs = thisSelection
.selectAll('.arc')
.data(data)
.enter()
.append('g')
.attr('class', 'arc')
arcs.append('path').attr('d', function (d) {
// start at vertice 1
let start = 'M ' + d.x1 + ' ' + d.y1
// inner curve to vertice 2
let side1 =
' Q ' +
d.controlPoint1x +
' ' +
d.controlPoint1y +
' ' +
d.x2 +
' ' +
d.y2
// straight line to vertice 3
let side2 = 'L ' + d.x3 + ' ' + d.y3
// outer curve vertice 4
let side3 =
' Q ' +
d.controlPoint2x +
' ' +
d.controlPoint2y +
' ' +
d.x4 +
' ' +
d.y4
// combine into string, with closure (Z) to vertice 1
return start + ' ' + side1 + ' ' + side2 + ' ' + side3 + ' Z'
})
// create coil labels on the first arc of each coil
coilLabels = arcs
.filter(function (d) {
return d.arcNumber == 0
})
.raise()
coilLabels
.append('path')
.attr('id', function (d) {
return 'path-' + d[coilLabel]
})
.attr('d', function (d) {
// start at vertice 1
let start = 'M ' + d.x1 + ' ' + d.y1
// inner curve to vertice 2
let side1 =
' Q ' +
d.controlPoint1x +
' ' +
d.controlPoint1y +
' ' +
d.x2 +
' ' +
d.y2
return start + side1
})
.style('opacity', 0)
coilLabels
.append('text')
.attr('class', 'coil-label')
.attr('x', 3)
.attr('dy', -4)
.append('textPath')
.attr('xlink:href', function (d) {
return '#path-' + d[coilLabel]
})
.text(function (d) {
return d[coilLabel]
})
})
function updatePathData (data) {
let holeRadius = radius * holeRadiusProportion
let arcAngle = 360 / arcsPerCoil
let dataLength = data.length
let coils = Math.ceil(dataLength / arcsPerCoil) // number of coils, based on data.length / arcsPerCoil
let coilWidth = chartRadius * (1 - holeRadiusProportion) / (coils + 1) // remaining chartRadius (after holeRadius removed), divided by coils + 1. I add 1 as the end of the coil moves out by 1 each time
data.forEach(function (d, i) {
let coil = Math.floor(i / arcsPerCoil)
let position = i - coil * arcsPerCoil
let startAngle = position * arcAngle
let endAngle = (position + 1) * arcAngle
let startInnerRadius = holeRadius + i / arcsPerCoil * coilWidth
let startOuterRadius =
holeRadius +
i / arcsPerCoil * coilWidth +
coilWidth * (1 - coilPadding)
let endInnerRadius = holeRadius + (i + 1) / arcsPerCoil * coilWidth
let endOuterRadius =
holeRadius +
(i + 1) / arcsPerCoil * coilWidth +
coilWidth * (1 - coilPadding)
// vertices of each arc
d.x1 = x(startAngle, startInnerRadius)
d.y1 = y(startAngle, startInnerRadius)
d.x2 = x(endAngle, endInnerRadius)
d.y2 = y(endAngle, endInnerRadius)
d.x3 = x(endAngle, endOuterRadius)
d.y3 = y(endAngle, endOuterRadius)
d.x4 = x(startAngle, startOuterRadius)
d.y4 = y(startAngle, startOuterRadius)
// CURVE CONTROL POINTS
let midAngle = startAngle + arcAngle / 2
let midInnerRadius =
holeRadius + (i + 0.5) / arcsPerCoil * coilWidth
let midOuterRadius =
holeRadius +
(i + 0.5) / arcsPerCoil * coilWidth +
coilWidth * (1 - coilPadding)
// MID POINTS, WHERE THE CURVE WILL PASS THRU
d.mid1x = x(midAngle, midInnerRadius)
d.mid1y = y(midAngle, midInnerRadius)
d.mid2x = x(midAngle, midOuterRadius)
d.mid2y = y(midAngle, midOuterRadius)
d.controlPoint1x = (d.mid1x - 0.25 * d.x1 - 0.25 * d.x2) / 0.5
d.controlPoint1y = (d.mid1y - 0.25 * d.y1 - 0.25 * d.y2) / 0.5
d.controlPoint2x = (d.mid2x - 0.25 * d.x3 - 0.25 * d.x4) / 0.5
d.controlPoint2y = (d.mid2y - 0.25 * d.y3 - 0.25 * d.y4) / 0.5
d.arcNumber = position
d.coilNumber = coil
})
return data
}
function x (angle, radius) {
// change to clockwise
let a = 360 - angle
// start from 12 o'clock
a = a + 180
return radius * Math.sin(a * radians)
}
function y (angle, radius) {
// change to clockwise
let a = 360 - angle
// start from 12 o'clock
a = a + 180
return radius * Math.cos(a * radians)
}
function chartWH (r) {
return r * 2
}
}
chart.radius = function (value) {
if (!arguments.length) return radius
radius = value
return chart
}
chart.holeRadiusProportion = function (value) {
if (!arguments.length) return holeRadiusProportion
holeRadiusProportion = value
return chart
}
chart.arcsPerCoil = function (value) {
if (!arguments.length) return arcsPerCoil
arcsPerCoil = value
return chart
}
chart.coilPadding = function (value) {
if (!arguments.length) return coilPadding
coilPadding = value
return chart
}
chart.arcLabel = function (value) {
if (!arguments.length) return arcLabel
arcLabel = value
return chart
}
chart.coilLabel = function (value) {
if (!arguments.length) return coilLabel
coilLabel = value
return chart
}
return chart
}
car_type export_production date value
Passenger cars Production 1/01/2008 475197
Passenger cars Production 1/02/2008 531397
Passenger cars Production 1/03/2008 492336
Passenger cars Production 1/04/2008 579869
Passenger cars Production 1/05/2008 444537
Passenger cars Production 1/06/2008 516609
Passenger cars Production 1/07/2008 434801
Passenger cars Production 1/08/2008 340288
Passenger cars Production 1/09/2008 535893
Passenger cars Production 1/10/2008 448565
Passenger cars Production 1/11/2008 452590
Passenger cars Production 1/12/2008 279948
Passenger cars Production 1/01/2009 310113
Passenger cars Production 1/02/2009 297921
Passenger cars Production 1/03/2009 435693
Passenger cars Production 1/04/2009 373308
Passenger cars Production 1/05/2009 427239
Passenger cars Production 1/06/2009 478133
Passenger cars Production 1/07/2009 410321
Passenger cars Production 1/08/2009 325060
Passenger cars Production 1/09/2009 553205
Passenger cars Production 1/10/2009 508450
Passenger cars Production 1/11/2009 493851
Passenger cars Production 1/12/2009 351229
Passenger cars Production 1/01/2010 376937
Passenger cars Production 1/02/2010 450355
Passenger cars Production 1/03/2010 560055
Passenger cars Production 1/04/2010 464591
Passenger cars Production 1/05/2010 470130
Passenger cars Production 1/06/2010 528820
Passenger cars Production 1/07/2010 392553
Passenger cars Production 1/08/2010 335748
Passenger cars Production 1/09/2010 536468
Passenger cars Production 1/10/2010 502507
Passenger cars Production 1/11/2010 519765
Passenger cars Production 1/12/2010 414480
Passenger cars Production 1/01/2011 398078
Passenger cars Production 1/02/2011 519342
Passenger cars Production 1/03/2011 583399
Passenger cars Production 1/04/2011 470463
Passenger cars Production 1/05/2011 563015
Passenger cars Production 1/06/2011 456548
Passenger cars Production 1/07/2011 462005
Passenger cars Production 1/08/2011 396706
Passenger cars Production 1/09/2011 562504
Passenger cars Production 1/10/2011 502897
Passenger cars Production 1/11/2011 546272
Passenger cars Production 1/12/2011 410689
Passenger cars Production 1/01/2012 443510
Passenger cars Production 1/02/2012 508798
Passenger cars Production 1/03/2012 546169
Passenger cars Production 1/04/2012 426321
Passenger cars Production 1/05/2012 448045
Passenger cars Production 1/06/2012 463839
Passenger cars Production 1/07/2012 460830
Passenger cars Production 1/08/2012 363254
Passenger cars Production 1/09/2012 450072
Passenger cars Production 1/10/2012 454716
Passenger cars Production 1/11/2012 482406
Passenger cars Production 1/12/2012 340499
Passenger cars Production 1/01/2013 397634
Passenger cars Production 1/02/2013 459180
Passenger cars Production 1/03/2013 474467
Passenger cars Production 1/04/2013 502411
Passenger cars Production 1/05/2013 428684
Passenger cars Production 1/06/2013 475779
Passenger cars Production 1/07/2013 444960
Passenger cars Production 1/08/2013 394991
Passenger cars Production 1/09/2013 514468
Passenger cars Production 1/10/2013 456669
Passenger cars Production 1/11/2013 537383
Passenger cars Production 1/12/2013 353278
Passenger cars Production 1/01/2014 442430
Passenger cars Production 1/02/2014 513041
Passenger cars Production 1/03/2014 522428
Passenger cars Production 1/04/2014 494416
Passenger cars Production 1/05/2014 482483
Passenger cars Production 1/06/2014 456069
Passenger cars Production 1/07/2014 535001
Passenger cars Production 1/08/2014 272744
Passenger cars Production 1/09/2014 524072
Passenger cars Production 1/10/2014 478999
Passenger cars Production 1/11/2014 515340
Passenger cars Production 1/12/2014 367003
Passenger cars Production 1/01/2015 421510
Passenger cars Production 1/02/2015 501118
Passenger cars Production 1/03/2015 556049
Passenger cars Production 1/04/2015 479631
Passenger cars Production 1/05/2015 447925
Passenger cars Production 1/06/2015 513315
Passenger cars Production 1/07/2015 533264
Passenger cars Production 1/08/2015 338834
Passenger cars Production 1/09/2015 538149
Passenger cars Production 1/10/2015 529856
Passenger cars Production 1/11/2015 518512
Passenger cars Production 1/12/2015 329975
Passenger cars Production 1/01/2016 416983
Passenger cars Production 1/02/2016 529061
Passenger cars Production 1/03/2016 523210
Passenger cars Production 1/04/2016 550158
Passenger cars Production 1/05/2016 445393
Passenger cars Production 1/06/2016 563546
Passenger cars Production 1/07/2016 410630
Passenger cars Production 1/08/2016 410256
Passenger cars Production 1/09/2016 529463
Passenger cars Production 1/10/2016 464434
Passenger cars Production 1/11/2016 535180
Passenger cars Production 1/12/2016 368494
Trucks Production 1/01/2008 25412
Trucks Production 1/02/2008 28381
Trucks Production 1/03/2008 27083
Trucks Production 1/04/2008 32175
Trucks Production 1/05/2008 25918
Trucks Production 1/06/2008 28591
Trucks Production 1/07/2008 25323
Trucks Production 1/08/2008 24331
Trucks Production 1/09/2008 29329
Trucks Production 1/10/2008 24756
Trucks Production 1/11/2008 18562
Trucks Production 1/12/2008 11065
Trucks Production 1/01/2009 12277
Trucks Production 1/02/2009 9334
Trucks Production 1/03/2009 13133
Trucks Production 1/04/2009 10349
Trucks Production 1/05/2009 12879
Trucks Production 1/06/2009 14238
Trucks Production 1/07/2009 12826
Trucks Production 1/08/2009 12165
Trucks Production 1/09/2009 18601
Trucks Production 1/10/2009 20106
Trucks Production 1/11/2009 17875
Trucks Production 1/12/2009 13845
Trucks Production 1/01/2010 14349
Trucks Production 1/02/2010 17655
Trucks Production 1/03/2010 20594
Trucks Production 1/04/2010 19105
Trucks Production 1/05/2010 18591
Trucks Production 1/06/2010 21773
Trucks Production 1/07/2010 19710
Trucks Production 1/08/2010 10444
Trucks Production 1/09/2010 23025
Trucks Production 1/10/2010 20866
Trucks Production 1/11/2010 27080
Trucks Production 1/12/2010 17494
Trucks Production 1/01/2011 19977
Trucks Production 1/02/2011 21555
Trucks Production 1/03/2011 23891
Trucks Production 1/04/2011 21584
Trucks Production 1/05/2011 25242
Trucks Production 1/06/2011 19651
Trucks Production 1/07/2011 20441
Trucks Production 1/08/2011 19684
Trucks Production 1/09/2011 27674
Trucks Production 1/10/2011 24456
Trucks Production 1/11/2011 29416
Trucks Production 1/12/2011 21459
Trucks Production 1/01/2012 21618
Trucks Production 1/02/2012 21703
Trucks Production 1/03/2012 24872
Trucks Production 1/04/2012 17889
Trucks Production 1/05/2012 22248
Trucks Production 1/06/2012 23178
Trucks Production 1/07/2012 20078
Trucks Production 1/08/2012 19069
Trucks Production 1/09/2012 25366
Trucks Production 1/10/2012 24434
Trucks Production 1/11/2012 24850
Trucks Production 1/12/2012 15496
Trucks Production 1/01/2013 19743
Trucks Production 1/02/2013 21021
Trucks Production 1/03/2013 24014
Trucks Production 1/04/2013 25375
Trucks Production 1/05/2013 23212
Trucks Production 1/06/2013 26161
Trucks Production 1/07/2013 20857
Trucks Production 1/08/2013 21898
Trucks Production 1/09/2013 25196
Trucks Production 1/10/2013 23137
Trucks Production 1/11/2013 29256
Trucks Production 1/12/2013 18448
Trucks Production 1/01/2014 25329
Trucks Production 1/02/2014 23337
Trucks Production 1/03/2014 24855
Trucks Production 1/04/2014 27804
Trucks Production 1/05/2014 27494
Trucks Production 1/06/2014 28063
Trucks Production 1/07/2014 25655
Trucks Production 1/08/2014 20743
Trucks Production 1/09/2014 29240
Trucks Production 1/10/2014 26320
Trucks Production 1/11/2014 25247
Trucks Production 1/12/2014 19435
Trucks Production 1/01/2015 27071
Trucks Production 1/02/2015 26000
Trucks Production 1/03/2015 30193
Trucks Production 1/04/2015 27932
Trucks Production 1/05/2015 24309
Trucks Production 1/06/2015 29727
Trucks Production 1/07/2015 22937
Trucks Production 1/08/2015 19059
Trucks Production 1/09/2015 31125
Trucks Production 1/10/2015 32696
Trucks Production 1/11/2015 31496
Trucks Production 1/12/2015 22681
Trucks Production 1/01/2016 24202
Trucks Production 1/02/2016 28822
Trucks Production 1/03/2016 27792
Trucks Production 1/04/2016 28587
Trucks Production 1/05/2016 26259
Trucks Production 1/06/2016 29962
Trucks Production 1/07/2016 23481
Trucks Production 1/08/2016 16152
Trucks Production 1/09/2016 30938
Trucks Production 1/10/2016 27109
Trucks Production 1/11/2016 28935
Trucks Production 1/12/2016 23515
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v0.3.min.js"></script>
<script src="d3-spiral-heatmap.js"></script>
<link href="https://fonts.googleapis.com/css?family=Catamaran" rel="stylesheet">
<style>
body {
font-family: 'Catamaran', sans-serif;
margin: 20px;
top: 20px;
right: 20px;
bottom: 20px;
left: 20px;
}
line {
stroke: #E9E9E9;
}
.coil-label {
fill: #000;
font-size: 12px;
}
.arc path {
stroke: #FFF;
}
</style>
</head>
<body>
<h1>Spiral heatmap</h1>
<p>An example of a resuable spiral heatmap.</p>
<div id="charts"></div>
<div id="legend"></div>
<script>
//SVG dimensions
const chartWidth = 500
const chartHeight = chartWidth
const chartRadius = chartWidth / 2
const margin = { "top": 40, "bottom": 40, "left": 40, "right": 40 }
let dateParse = d3.timeParse("%d/%m/%Y")
let yearFormat = d3.timeFormat("%Y")
let monthFormat = d3.timeFormat("%b");
//Colour scale
var colour = d3.scaleSequential(d3.interpolateRdYlGn)
//Load the data, nest, sort and draw charts
d3.csv("data.csv", convertTextToNumbers, function (error, data) {
if (error) { throw error; };
//ENSURE THE DATA IS SORTED CORRECTLY, IN THIS CASE BY YEAR AND MONTH
//THE SPIRAL WILL START IN THE MIDDLE AND WORK OUTWARDS
var nestedData = d3.nest()
.key(function (d) { return d.car_type; })
.sortValues(function (a, b) { return a.date - b.date; })
.entries(data);
nestedData.forEach(function (chartData) {
colour.domain(d3.extent(chartData.values, function (d) { return d.value; }));
//set the options for the sprial heatmap
let heatmap = spiralHeatmap()
.radius(chartRadius)
.holeRadiusProportion(0.2)
.arcsPerCoil(12)
.coilPadding(0.1)
.arcLabel("month")
.coilLabel("year")
//CREATE SVG AND A G PLACED IN THE CENTRE OF THE SVG
const div = d3.select("#charts").append("div")
div.append("p")
.text(chartData.key)
const svg = div.append("svg")
.attr("width", chartWidth + margin.left + margin.right)
.attr("height", chartHeight + margin.top + margin.bottom);
const g = svg.append("g")
.attr("transform", "translate("
+ (margin.left + chartRadius)
+ ","
+ (margin.top + chartRadius) + ")");
g.datum(chartData.values)
.call(heatmap);
g.selectAll(".arc").selectAll("path")
.style("fill", function (d) { return colour(d.value); })
})
})
function convertTextToNumbers(d) {
d.value = +d.value;
d.date = dateParse(d.date);
d.year = yearFormat(d.date);
d.month = monthFormat(d.date);
return d;
};
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment