Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active July 12, 2017 02:16
Show Gist options
  • Save tomshanley/fbf481071aa45a5a01d7893d4da978fe to your computer and use it in GitHub Desktop.
Save tomshanley/fbf481071aa45a5a01d7893d4da978fe to your computer and use it in GitHub Desktop.
Spiral heatmap #2
license: mit
height: 900
year month temperature
1977 1 5.3
1977 2 9
1977 3 11
1977 4 12.2
1977 5 15.7
1977 6 16.6
1977 7 21.3
1977 8 20
1977 9 18.1
1977 10 16.2
1977 11 9.6
1977 12 9
1978 1 5.9
1978 2 5.4
1978 3 11.1
1978 4 10.3
1978 5 16
1978 6 18.9
1978 7 19.4
1978 8 20.2
1978 9 19.3
1978 10 16.3
1978 11 11.7
1978 12 6.7
1979 1 2.5
1979 2 3.5
1979 3 8.4
1979 4 12.1
1979 5 15.9
1979 6 18.9
1979 7 21.8
1979 8 20.4
1979 9 19.3
1979 10 15.5
1979 11 10
1979 12 8.8
1980 1 5.6
1980 2 9.1
1980 3 8.5
1980 4 13.3
1980 5 16.4
1980 6 19.3
1980 7 19.3
1980 8 21.3
1980 9 19.5
1980 10 13.3
1980 11 8.9
1980 12 8.2
1981 1 7.4
1981 2 6.1
1981 3 11
1981 4 11.7
1981 5 15.8
1981 6 18.1
1981 7 20.8
1981 8 21.7
1981 9 19.9
1981 10 12.4
1981 11 10.9
1981 12 3
1982 1 5.6
1982 2 7.9
1982 3 10.5
1982 4 13.4
1982 5 17.3
1982 6 20.2
1982 7 21.8
1982 8 21.3
1982 9 19.9
1982 10 13.6
1982 11 10.6
1982 12 7.2
1983 1 9.5
1983 2 4.5
1983 3 10.2
1983 4 11.6
1983 5 14.8
1983 6 19.4
1983 7 25.8
1983 8 23.4
1983 9 18.2
1983 10 14.7
1983 11 10.4
1983 12 8.3
1984 1 6.9
1984 2 6.2
1984 3 8
1984 4 13.4
1984 5 14
1984 6 19.5
1984 7 22.3
1984 8 23.6
1984 9 17.5
1984 10 15.6
1984 11 11.2
1984 12 8.2
1985 1 3.4
1985 2 4.9
1985 3 8.7
1985 4 12.9
1985 5 15.4
1985 6 17.4
1985 7 21.8
1985 8 20.1
1985 9 20.1
1985 10 15.6
1985 11 7.1
1985 12 9.2
1986 1 6.3
1986 2 0.8
1986 3 9.3
1986 4 9.8
1986 5 16.3
1986 6 20.7
1986 7 21.7
1986 8 19.1
1986 9 17.1
1986 10 15.6
1986 11 11
1986 12 9
1987 1 2.8
1987 2 7.0
1987 3 7.7
1987 4 15.2
1987 5 14.9
1987 6 17.5
1987 7 20.6
1987 8 20.5
1987 9 18.8
1987 10 14.1
1987 11 9
1987 12 8.2
1988 1 8
1988 2 7.9
1988 3 9.7
1988 4 12.5
1988 5 17
1988 6 18.3
1988 7 19.4
1988 8 21.4
1988 9 18.2
1988 10 14.6
1988 11 9.0
1988 12 9.6
1989 1 8.9
1989 2 9.6
1989 3 12.3
1989 4 10.6
1989 5 19.7
1989 6 21.2
1989 7 24.8
1989 8 24.1
1989 9 20.5
1989 10 16.5
1989 11 9.9
1989 12 8.3
1990 1 9.8
1990 2 11.4
1990 3 12.9
1990 4 13.5
1990 5 19.1
1990 6 19.2
1990 7 23.7
1990 8 25.7
1990 9 18.6
1990 10 16.2
1990 11 9.8
1990 12 6.9
1991 1 6.2
1991 2 4.6
1991 3 12
1991 4 12.4
1991 5 14.7
1991 6 16.7
1991 7 22.9
1991 8 23.7
1991 9 20.5
1991 10 14.1
1991 11 9.8
1991 12 7.3
1992 1 6.4
1992 2 9.2
1992 3 11.1
1992 4 13.2
1992 5 19.8
1992 6 21.7
1992 7 21.8
1992 8 21.2
1992 9 18.4
1992 10 11.5
1992 11 10.8
1992 12 6.4
1993 1 9.6
1993 2 6.6
1993 3 10.7
1993 4 13.4
1993 5 17.2
1993 6 20.4
1993 7 20.5
1993 8 20.5
1993 9 16.7
1993 10 12.1
1993 11 7.9
1993 12 8.5
1994 1 8.5
1994 2 6.7
1994 3 11.7
1994 4 12.4
1994 5 15.1
1994 6 20.3
1994 7 25.8
1994 8 22.1
1994 9 16.6
1994 10 13.9
1994 11 12.6
1994 12 9.8
1995 1 7.9
1995 2 9.6
1995 3 9.9
1995 4 13.9
1995 5 17.9
1995 6 18.7
1995 7 25.8
1995 8 25.7
1995 9 18
1995 10 17.6
1995 11 10.9
1995 12 4.3
1996 1 5.8
1996 2 5.4
1996 3 7.2
1996 4 13.7
1996 5 13.9
1996 6 21.5
1996 7 23.2
1996 8 22.6
1996 9 18.4
1996 10 15.9
1996 11 9.3
1996 12 5.1
1997 1 4.7
1997 2 10.5
1997 3 13
1997 4 13.9
1997 5 17.6
1997 6 18.9
1997 7 22.3
1997 8 26.3
1997 9 19.9
1997 10 14.7
1997 11 11.7
1997 12 8.5
1998 1 8.1
1998 2 11.2
1998 3 11.5
1998 4 11.9
1998 5 18.2
1998 6 18.9
1998 7 20.8
1998 8 22.6
1998 9 19.4
1998 10 14
1998 11 8.4
1998 12 8.7
1999 1 9.1
1999 2 8.2
1999 3 11.7
1999 4 14.3
1999 5 18.3
1999 6 19.2
1999 7 24.5
1999 8 21.6
1999 9 21.3
1999 10 14.9
1999 11 10.6
1999 12 7.8
2000 1 8.1
2000 2 9.8
2000 3 11.2
2000 4 12.4
2000 5 16.8
2000 6 20.5
2000 7 20.1
2000 8 23.1
2000 9 20
2000 10 14
2000 11 10.4
2000 12 8.1
2001 1 6.1
2001 2 8.2
2001 3 9
2001 4 12.1
2001 5 18.5
2001 6 19.9
2001 7 23.1
2001 8 22.9
2001 9 17.3
2001 10 17.3
2001 11 10.6
2001 12 6.2
2002 1 8.9
2002 2 10.7
2002 3 12
2002 4 14.7
2002 5 16.6
2002 6 20.1
2002 7 22
2002 8 22.9
2002 9 19.6
2002 10 14.3
2002 11 11.4
2002 12 8.2
2003 1 7
2003 2 7.8
2003 3 12.9
2003 4 15.1
2003 5 17.5
2003 6 22.1
2003 7 23.7
2003 8 25.3
2003 9 21.3
2003 10 14
2003 11 11.7
2003 12 8.1
2004 1 8.1
2004 2 8.1
2004 3 10.5
2004 4 14.2
2004 5 16.9
2004 6 21.2
2004 7 22
2004 8 24
2004 9 20.6
2004 10 15
2004 11 10.5
2004 12 8.4
2005 1 9.2
2005 2 7.1
2005 3 10.7
2005 4 14.2
2005 5 16.6
2005 6 21.8
2005 7 22
2005 8 22.6
2005 9 21.1
2005 10 17.4
2005 11 10.1
2005 12 7.8
2006 1 7
2006 2 7
2006 3 9
2006 4 13.5
2006 5 17.4
2006 6 22.5
2006 7 28.3
2006 8 21.5
2006 9 22.7
2006 10 17.5
2006 11 12.5
2006 12 9.7
2007 1 10.4
2007 2 9.7
2007 3 12.1
2007 4 17.1
2007 5 16.9
2007 6 20.4
2007 7 21.6
2007 8 21.6
2007 9 19.5
2007 10 15.2
2007 11 11.1
2007 12 8.5
2008 1 10.3
2008 2 9.9
2008 3 10.1
2008 4 13.1
2008 5 19
2008 6 19.7
2008 7 22.8
2008 8 22.3
2008 9 18.6
2008 10 14.4
2008 11 10.3
2008 12 6.6
2009 1 5.8
2009 2 7.1
2009 3 11.9
2009 4 15.6
2009 5 18.3
2009 6 20.9
2009 7 22.5
2009 8 23.6
2009 9 20.1
2009 10 15.7
2009 11 12
2009 12 6.2
2010 1 3.9
2010 2 6.1
2010 3 10.9
2010 4 14.9
2010 5 16.5
2010 6 21.6
2010 7 24.6
2010 8 21
2010 9 19
2010 10 14.7
2010 11 8.4
2010 12 2.8
2011 1 6.8
2011 2 9.6
2011 3 11.6
2011 4 18.6
2011 5 18.7
2011 6 20.3
2011 7 21.2
2011 8 21.6
2011 9 21.2
2011 10 17.9
2011 11 13.3
2011 12 9.1
2012 1 9.1
2012 2 7
2012 3 13.3
2012 4 12.2
2012 5 16.9
2012 6 18.5
2012 7 21.2
2012 8 23.5
2012 9 19.3
2012 10 13.5
2012 11 10
2012 12 8.1
2013 1 5.8
2013 2 6
2013 3 6.2
2013 4 13.2
2013 5 15.9
2013 6 19.2
2013 7 25.1
2013 8 23.5
2013 9 19
2013 10 16.2
2013 11 9.6
2013 12 9.7
2014 1 9.2
2014 2 9.8
2014 3 13.4
2014 4 15.9
2014 5 17.7
2014 6 20.8
2014 7 24.4
2014 8 21
2014 9 20.5
2014 10 17
2014 11 11.9
2014 12 8.5
2015 1 8
2015 2 7.4
2015 3 11.1
2015 4 15
2015 5 16.5
2015 6 20.3
2015 7 22.5
2015 8 22.2
2015 9 18.1
2015 10 15.1
2015 11 13.3
2015 12 13.2
2016 1 8.7
2016 2 8.8
2016 3 10
2016 4 12.4
2016 5 17.7
2016 6 19.7
2016 7 23.2
2016 8 23.9
2016 9 21.8
2016 10 15
2016 11 9.6
2016 12 9.4
2017 1 6.7
2017 2 9.3
2017 3 13.4
2017 4 14.5
2017 5 18.6
2017 6 21.9
<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>
<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: lightgray;
}
.arc text {
fill: white;
stroke: none;
pointer-events: none;
}
.text-path {
stroke: none;
fill: none;
}
</style>
</head>
<body>
<h1>Spiral heatmap</h1>
<p>An example of a spiral heatmap, showing monthly max temperature at Cambridge, UK from 1977 to 2017.</p>
<p>Data source: <a href="http://www.metoffice.gov.uk/pub/data/weather/uk/climate/stationdata/cambridgedata.txt">Metservice</a></p>
<div id="chart"></div>
<div id="legend"></div>
<script>
const radians = 0.0174532925;
//CHART CONSTANTS
const chartRadius = 300;
const chartWidth = chartRadius * 2;
const chartHeight = chartRadius * 2;
const labelRadius = chartRadius + 15;
const margin = { "top": 40, "bottom": 40, "left": 40, "right": 40 };
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
//CHART OPTIONS
const holeRadiusProportion = 0.1; //fraction of chartRadius. 0 gives you some pointy arcs in the centre.
const holeRadius = holeRadiusProportion * chartRadius;
const segmentsPerCoil = 12; //number of coils. for this example, I have 12 months per year. But you change to whatever suits your data.
const segmentAngle = 360 / segmentsPerCoil;
var coils; //number of coils, based on data.length / segmentsPerCoil
var coilWidth; //remaining chartRadius (after holeRadius removed), divided by coils + 1. I add 1 as the end of the coil moves out by 1 each time
//SCALES
var colour = d3.scaleSequential(d3.interpolatePlasma);
//CREATE SVG AND A G PLACED IN THE CENTRE OF THE SVG
const svg = d3.select("#chart")
.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) + ")");
//LOAD THE DATA
d3.csv("cambridge_maxtemp.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
data.sort(function (a, b) {
return a.year - b.year || a.month - b.month;
});
//CALCULATE AND STORE THE REMAING
var dataLength = data.length;
coils = Math.ceil(dataLength / segmentsPerCoil);
coilWidth = (chartRadius * (1 - holeRadiusProportion)) / (coils + 1);
//console.log("coilWidth: " + coilWidth);
var dataExtent = d3.extent(data, function (d) { return d.temperature; });
console.log(dataExtent);
colour.domain(dataExtent);
//ADD LABELS AND GRIDS FOR EACH MONTH FIRST
//SO THE GRID LINES APPEAR BEHIND THE SPIRAL
var monthLabels = g.selectAll(".month-label")
.data(months)
.enter()
.append("g")
.attr("class", "month-label");
monthLabels.append("text")
.text(function (d) { return d; })
.attr("x", function (d, i) {
let labelAngle = (i * segmentAngle) + (segmentAngle / 2);
return x(labelAngle, labelRadius);
})
.attr("y", function (d, i) {
let labelAngle = (i * segmentAngle) + (segmentAngle / 2);
return y(labelAngle, labelRadius);
})
.style("text-anchor", function (d, i) {
return i < (months.length / 2) ? "start" : "end";
});
monthLabels.append("line")
.attr("x2", function (d, i) {
let lineAngle = (i * segmentAngle);
let lineRadius = chartRadius + 50;
return x(lineAngle, lineRadius);
})
.attr("y2", function (d, i) {
let lineAngle = (i * segmentAngle);
let lineRadius = chartRadius + 50;
return y(lineAngle, lineRadius);
});
//ASSUMING DATA IS SORTED, CALCULATE EACH DATA POINT'S SEGMENT VERTICES AND CONTROL POINTS
data.forEach(function (d, i) { calculateArc(d, i); });
var arcs = g.selectAll(".arc")
.data(data)
.enter()
.append("g")
.attr("class", "arc");
//STRAIGHT EDGES
/*
arcs.append("path")
.attr("d", function (d) {
let M = "M " + d.x1 + " " + d.y1;
let L1 = "L " + d.x2 + " " + d.y2;
let L2 = "L " + d.x3 + " " + d.y3;
let L3 = "L " + d.x4 + " " + d.y4;
return M + " " + L1 + " " + L2 + " " + L3 + " Z"
})
//.style("fill", function (d) { return colour(d.temperature); })
.style("fill", "white")
.style("stroke", "white")
*/
//CURVED EDGES
arcs.append("path")
.attr("class", "arc-segment")
.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"
})
.style("fill", function (d) { return colour(d.temperature); })
/*.on("mouseover", function (d) {
//HIGHLIGHT ARCS FROM THE SAME MONTH, OR WITHIN A 6 MONTH PERIOD EITHER SIDE
let thisMonth = d.month;
let lowerLimit = d.index - 6;
let upperLimit = d.index + 6;
arcs.selectAll(".arc-segment")
.style("opacity", function (a) {
if (a.month == thisMonth) {
return 1;
} else if (a.index > lowerLimit && a.index < upperLimit) {
return 1;
} else {
return 0.1
};
});
})
.on("mouseout", function (d) {
arcs.selectAll(".arc-segment").style("opacity", 1);
});*/
//ADD LABELS FOR THE YEAR AT THE START OF EACH COIL (IE THE FIRST MONTH)
/*var yearLabels = arcs.filter(function (d) { return d.month == 1; }).raise();
yearLabels.append("path")
.attr("class", "text-path")
.attr("id", function (d) { return "path-" + d.year; })
.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;
});
yearLabels.append("text")
.attr("class", "year-label")
.attr("x", 3)
.attr("dy", -5)
.append("textPath")
.attr("xlink:href", function (d) {
return "#path-" + d.year;
})
.text(function (d) { return d.year; })
*/
//DRAW LEGEND
const legendWidth = chartRadius;
const legendHeight = 20;
const legendPadding = 40;
var legendSVG = d3.select("#legend")
.append("svg")
.attr("width", legendWidth + legendPadding + legendPadding)
.attr("height", legendHeight + legendPadding + legendPadding);
var defs = legendSVG.append("defs");
var legendGradient = defs.append("linearGradient")
.attr("id", "linear-gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "0%");
let noOfSamples = 20;
let dataRange = dataExtent[1] - dataExtent[0];
let stepSize = dataRange / noOfSamples;
for (i = 0; i < noOfSamples; i++) {
legendGradient.append("stop")
.attr("offset", (i / (noOfSamples - 1)))
.attr("stop-color", colour(dataExtent[0] + (i * stepSize)));
}
var legendG = legendSVG.append("g")
.attr("class", "legendLinear")
.attr("transform", "translate(" + legendPadding + "," + legendPadding + ")");
legendG.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "url(#linear-gradient)");
legendG.append("text")
.text("Cooler (-0.1c)")
.attr("x", 0)
.attr("y", legendHeight - 35)
.style("font-size", "12px");
legendG.append("text")
.text("Warmer (28.3c)")
.attr("x", legendWidth)
.attr("y", legendHeight - 35)
.style("text-anchor", "end")
.style("font-size", "12px");
});
//RETURNS THE X COORDINATE GIVEN A CERTAIN RADIUS AND ANGLE
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);
};
//RETURNS THE Y COORDINATE GIVEN A CERTAIN RADIUS AND ANGLE
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);
};
//CALCULATES THE VERTICES AND CONTROL POINTS FOR DRAWING EACH ARC'S PATH
function calculateArc(d, i) {
d.index = i;
let coil = Math.floor(i / segmentsPerCoil);
let position = i - (coil * segmentsPerCoil);
//console.log("positions: " + i + " " + coil + " " + position);
let startAngle = position * segmentAngle;
let endAngle = (position + 1) * segmentAngle;
//console.log("angles: " + startAngle + " " + endAngle);
//console.log(holeRadius + " " + segmentsPerCoil + " " + coilWidth)
let startInnerRadius = holeRadius + ((i / segmentsPerCoil) * coilWidth)
let startOuterRadius = holeRadius + ((i / segmentsPerCoil) * coilWidth) + coilWidth;
let endInnerRadius = holeRadius + (((i + 1) / segmentsPerCoil) * coilWidth)
let endOuterRadius = holeRadius + (((i + 1) / segmentsPerCoil) * coilWidth) + coilWidth;
//console.log("start radi: " + startInnerRadius + " " + startOuterRadius);
//console.log("end radi: " + endInnerRadius + " " + endOuterRadius);
//vertices of each segment
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 + (segmentAngle / 2)
let midInnerRadius = holeRadius + (((i + 0.5) / segmentsPerCoil) * coilWidth)
let midOuterRadius = holeRadius + (((i + 0.5) / segmentsPerCoil) * coilWidth) + coilWidth;
//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);
//FROM https://stackoverflow.com/questions/5634460/quadratic-b%C3%A9zier-curve-calculate-points
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;
//console.log(d);
};
function convertTextToNumbers(d) {
d.year = +d.year;
d.month = +d.month;
d.temperature = +d.temperature;
return d;
};
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment