Built with blockbuilder.org
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>
<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>
<div id="title">
<h1>Northern hemisphere sea ice extent: 1978 to 2017</h1>
<p>An example of a resuable spiral heatmap showing the extent of the sea ice in the northern hemisphere.</p>
<p>Low sea ice is starting earlier in July, and lasting longer into October and November.</p>
</div>
<div id="legend"></div>
<div id="chart"></div>
<div id="source">
<p>Source: https://nsidc.org/data/nsidc-0081.html and https://nsidc.org/data/nsidc-0051.html</p>
</div>
<script>
//SVG dimensions
const radians = 0.0174532925
const chartWidth = 750
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 dateFormat = d3.timeFormat("%d%m");// returns date string ddmm, eg 2902
let dayOfYear = d3.timeFormat("%j");
let startAngle = 0;
let arcLabels = [
{ "month": "Jan", "start": 0, "days": 31 },
{ "month": "Feb", "start": 31, "days": 28 },
{ "month": "Mar", "start": 59, "days": 31 },
{ "month": "Apr", "start": 90, "days": 30 },
{ "month": "May", "start": 120, "days": 31 },
{ "month": "Jun", "start": 151, "days": 30 },
{ "month": "Jul", "start": 181, "days": 31 },
{ "month": "Aug", "start": 212, "days": 31 },
{ "month": "Sep", "start": 243, "days": 30 },
{ "month": "Oct", "start": 273, "days": 31 },
{ "month": "Nov", "start": 304, "days": 30 },
{ "month": "Dec", "start": 334, "days": 31 }
]
//Colour scale
var colour = d3.scaleSequential(d3.interpolateSpectral);
//Load the data, nest, sort and draw charts
d3.csv("sea-ice-extent.csv", convertTextToNumbers, function (error, data) {
if (error) { throw error; };
colour.domain(d3.extent(data, function (d) { return d.extent; }));
let chartData = createDataPerDay(data);
startAngle = ((data[0].dayOfYear / 365) * 360) - 1;
//set the options for the sprial heatmap
let heatmap = spiralHeatmap()
.radius(chartRadius)
.holeRadiusProportion(0.1)
.arcsPerCoil(365)
.startAngle(startAngle)
.coilPadding(0.1)
//.coilLabel("year")
//CREATE SVG AND A G PLACED IN THE CENTRE OF THE SVG
const div = d3.select("#chart").append("div")
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)
.call(heatmap);
g.selectAll(".arc").selectAll("path")
.style("fill", function (d) { return colour(d.extent); })
d3.selectAll(".arc-label").remove();
var arcLabelsG = g.selectAll('.arc-label')
.data(arcLabels)
.enter()
.append('g')
.attr('class', 'arc-label')
let dayAngle = 360 / 365;
arcLabelsG
.append('text')
.text(function (d) {
return d.month
})
.attr('x', function (d) {
let monthAngle = 360 * (d.days / 365);
let labelAngle = (d.start * dayAngle) + (monthAngle / 2)
let labelRadius = chartRadius + 20;
return labelX(labelAngle, labelRadius)
})
.attr('y', function (d) {
let monthAngle = 360 * (d.days / 365);
let labelAngle = (d.start * dayAngle) + (monthAngle / 2)
let labelRadius = chartRadius + 20;
return labelY(labelAngle, labelRadius)
})
.style('text-anchor', function (d, i) {
return i < arcLabels.length / 2 ? 'start' : 'end'
})
arcLabelsG
.append('line')
.attr('x2', function (d, i) {
let lineAngle = d.start * dayAngle
let lineRadius = chartRadius + 40
return labelX(lineAngle, lineRadius)
})
.attr('y2', function (d, i) {
let lineAngle = d.start * dayAngle
let lineRadius = chartRadius + 40
return labelY(lineAngle, lineRadius)
})
//DRAW LEGEND
const legendWidth = chartRadius;
const legendHeight = 20;
const legendPadding = 40;
const units = "million square km"
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 = colour.domain()[1] - colour.domain()[0];
let stepSize = dataRange / noOfSamples;
for (i = 0; i < noOfSamples; i++) {
legendGradient.append("stop")
.attr("offset", (i / (noOfSamples - 1)))
.attr("stop-color", colour(colour.domain()[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("Low (" + colour.domain()[0] + " " + units + ")")
.attr("x", 0)
.attr("y", legendHeight - 35)
.style("font-size", "12px");
legendG.append("text")
.text("High (" + colour.domain()[1] + " " + units + ")")
.attr("x", legendWidth)
.attr("y", legendHeight - 35)
.style("text-anchor", "end")
.style("font-size", "12px");
});
//Returns a new array, which a data point per day, except for 29th Feb.
//Where a day is missing in the original data, use the average from the preceding and next day.
function createDataPerDay(data) {
let day = data[0].date;
let lastDay = data[data.length - 1].date;
let newData = []
let i = 0;
for (day; day <= lastDay; day.setDate(day.getDate() + 1)) {
//skip 29th Feb in new dataset
if (dateFormat(day) != "2902") {
let datum = {};
datum.date = new Date(day);
let compareDate = new Date(data[i].date);
if (compareDate.getTime() === day.getTime()) {
datum.extent = data[i].extent;
datum.source = "original";
//move to the next day in the original data, unless it is 29th Feb
if (i < (data.length - 1)) {
i = (data[i + 1].month == 2 && data[i + 1].day == 29) ? i + 2 : i + 1;
}
} else {
datum.extent = (data[i - 1].extent + data[i + 1].extent) / 2;
datum.source = "avg";
};
newData.push(datum);
}
}
return newData;
};
function labelX(angle, radius) {
// change to clockwise
let a = 360 - angle
// start from 12 o'clock
a = a + 180;
return radius * Math.sin(a * radians)
}
function labelY(angle, radius) {
// change to clockwise
let a = 360 - angle
// start from 12 o'clock
a = a + 180;
return radius * Math.cos(a * radians)
}
function convertTextToNumbers(d) {
let dateString = d.day + "/" + d.month + "/" + d.year
d.date = dateParse(dateString);
d.extent = +d.extent;
d.year = +d.year;
d.month = +d.month;
d.day = +d.day;
d.dayOfYear = +dayOfYear(d.date);
return d;
};
</script>
</body>
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-scale-chromatic.v0.3.min.js