forked from tomshanley's block: Spiral heatmap
Built with blockbuilder.org
forked from tomshanley's block: Spiral heatmap (with mouseover)
forked from tomshanley's block: Spiral heatmap #2 (with mouseover)
xxxxxxxxxx
<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 1959 to 2017.</p>
<p>Data source: <a href="https://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 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);
let labelRadius = chartRadius + 5;
return x(labelAngle, labelRadius);
})
.attr("y", function (d, i) {
let labelAngle = (i * segmentAngle) + (segmentAngle / 2);
let labelRadius = chartRadius + 5;
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>
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-scale-chromatic.v0.3.min.js