Built with blockbuilder.org, and D3 Annotation + D3 Legend (ie standing on the shoulder's of Shirley Wu's excellent work!)
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-annotation/1.17.0/d3-annotation.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Source+Serif+Pro" rel="stylesheet">
<style>
body {
font-family: 'Source Serif Pro', serif;
margin: 20;
background: Ivory;
top: 20;
right: 20;
bottom: 20;
left: 20;
}
h2 {
margin-top: 0;
}
.box-label {
font-size: 11px;
}
:root {
--annotation-color: #8c8c8c;
}
.annotation path {
stroke: var(--annotation-color);
fill: none;
}
.annotation text,
.annotation .annotation-connector .connector-dot,
.annotation path.connector-arrow,
.annotation.callout.rect path.subject {
fill: var(--annotation-color);
font-size: 13px;
}
.annotation-note-title {
font-weight: bold;
}
.annotation-note-bg {
fill: rgba(0, 0, 0, 0);
}
.annotation.callout.rect path.subject {
fill-opacity: 0;
}
.annotation.badge path.subject-pointer,
.annotation.badge path.subject {
fill: var(--annotation-color);
stroke-width: 3px;
stroke-linecap: round;
}
.annotation.badge path.subject-ring {
fill: white;
stroke-width: 3px;
}
.annotation.badge .badge-text {
fill: white;
font-size: .7em;
}
/* Handling edit mode styles */
.editable .annotation-subject,
.editable .annotation-note {
cursor: move;
}
circle.handle {
stroke-dasharray: 5;
stroke: var(--annotation-color);
fill: rgba(255, 255, 255, .2);
cursor: move;
stroke-opacity: .4;
}
.legendTitle {
text-anchor: start
}
</style>
</head>
<body>
<div id="charts"></div>
<script>
const dateParser = d3.timeParse("%d/%m/%Y");
const maxBoxSize = 45;
const width = maxBoxSize * 13;
const height = maxBoxSize * 9;
const margin = { "top": 50, "bottom": 150, "left": 50, "right": 150 };
const radius = 4;
const years = ["2008", "2009", "2010", "2011", "2012", "2013", "2014", "2015", "2016"];
const months = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"];
var xScale = d3.scaleLinear()
.domain([1, 13])
.range([0, width])
var yScale = d3.scaleLinear()
.domain([2008, 2017])
.range([0, height]);
var size = d3.scaleSqrt()
.range([0, maxBoxSize]);
var colour = d3.scaleSequential(d3.interpolateRdYlGn)
d3.csv("data2.csv", convertTextToNumbers, function (error, allData) {
if (error) { throw error; };
var nestedData = d3.nest()
.key(function (d) { return d.car_type; })
.entries(allData);
nestedData.forEach(function (chartData) {
let div = d3.select("#charts").append("div");
let header = div.append("div")
.attr("class", "header");
header.append("div")
.attr("class", "header")
.append("h2")
.text(chartData.key + " production");
const legendWidth = 3 * maxBoxSize + 10;
const legendHeight = maxBoxSize;
const legendPadding = 30;
let legend = header.append("div")
.attr("class", "legend")
.append("svg")
.attr("width", (legendWidth * 2) + (legendPadding * 4))
.attr("height", legendHeight +(legendPadding * 3));
let diffExtent = d3.extent(chartData.values, function (d) { return d.diff_to_avg; });
let maxExtent = Math.abs(diffExtent[0]) > Math.abs(diffExtent[1])
? Math.abs(diffExtent[0])
: Math.abs(diffExtent[1]);
//colour.domain([-(maxExtent), 0, maxExtent]);
colour.domain([-(maxExtent), maxExtent]);
size.domain([0, d3.max(chartData.values, function (d) { return d.value; })]);
let svg = div.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
let g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
let boxes = g.selectAll("rect")
.data(chartData.values)
.enter()
.append("g")
.attr("transform", function(d){
let x = xScale(d.month_number);
let y = yScale(d.year);
return "translate(" + x +"," + y + ")" ;
});
boxes.append("rect")
.attr("x", function (d){ return (maxBoxSize - size(d.value)) / 2; })
.attr("y", function (d) { return (maxBoxSize - size(d.value)) / 2; })
.attr("width", function (d) { return size(d.value); })
.attr("height", function (d) { return size(d.value); })
.style("fill", function (d) { return colour(d.diff_to_avg); });
boxes.append("text")
.text(function(d){ return formatPercentage(d.diff_to_avg); })
.attr("class", "box-label")
.attr("x", maxBoxSize/2)
.attr("y", 27)
.style("text-anchor", "middle")
let yearLabels = g.selectAll(".year-label")
.data(years)
.enter()
.append("g")
.attr("class", "year-label")
.attr("transform", function (d) { return "translate(0, " + yScale(+d) + ")" });
yearLabels.append("text")
.text(function (d) { return d; })
.attr("x", -5)
.attr("y", 27)
.style("text-anchor", "end");
let monthLabels = g.selectAll(".month-label")
.data(months)
.enter()
.append("g")
.attr("class", "month-label")
.attr("transform", function (d, i) { return "translate(" + xScale(i + 1) + ",0)" });
monthLabels.append("text")
.text(function (d) { return d; })
.attr("x", maxBoxSize / 2)
.attr("y", -5)
.style("text-anchor", "middle");
//MAKE ANNOTATIONS ON THE CAR CHART
if (chartData.key == "Passenger cars") {
const type = d3.annotationCalloutRect;
const annotations = [{
note: {
label: "Below average production (more red)",
title: "Global financial crisis"
},
x: 0,
y: maxBoxSize,
dy: 10,
dx: width + 10,
subject: {
width: width,
height: maxBoxSize
}
},{
note: {
label: "August and December has lower output compared to rest of year (smaller boxes)",
title: "Seasonal downturn"
},
x: width * (7/12),
y: 0,
dy: height + 20,
dx: 10,
subject: {
width: maxBoxSize,
height: height
}
}
];
var makeAnnotations = d3.annotation()
.editMode(false)
.type(type)
.annotations(annotations)
g.append("g")
.attr("class", "annotation-group")
.call(makeAnnotations)
};
if (chartData.key == "Trucks") {
const type = d3.annotationCalloutRect;
const annotations = [{
note: {
label: "Below average production lasted longer for trucks",
title: "Global financial crisis"
},
x: 0,
y: maxBoxSize,
dy: 10,
dx: width + 10,
subject: {
width: width,
height: maxBoxSize * 2
}
},{
note: {
label: "Last three years have been consistently above average",
title: "Recent boom"
},
x: 0,
y: maxBoxSize * 6,
dy: 10,
dx: width + 10,
subject: {
width: width,
height: maxBoxSize * 3
}
}
];
var makeAnnotations = d3.annotation()
.editMode(false)
.type(type)
.annotations(annotations)
g.append("g")
.attr("class", "annotation-group")
.call(makeAnnotations)
};
//drawLegend();
let padding = 5;
let offset = 10;
let boxSize = 23;
let legendSize = d3.legendSize()
.scale(size)
.title("Production p/month")
.shape('rect')
.shapePadding(padding)
.cells(d3.extent(chartData.values, function(d){ return d.value }))
.labelOffset(offset)
.labelFormat(function(d){ return Math.round(d/10000) + "k"; })
//.orient('horizontal');
let legendSizeG = legend.append("g")
.attr("transform", "translate(" + legendPadding + "," + legendPadding + ")")
.call(legendSize);
legendSizeG.selectAll("rect")
.style("fill", "lightgrey")
//.style("stroke", "black")
let legendColour = d3.legendColor()
.scale(colour)
.title("% diff to month avg")
.cells(3)
.shape('rect')
.shapePadding(padding)
.labelOffset(offset)
.shapeWidth(boxSize)
.shapeHeight(boxSize)
.labelFormat(function(d){ return formatPercentage(d); })
//.orient('horizontal');
let colourSizeG = legend.append("g")
.attr("transform", "translate(" + (legendWidth + (legendPadding * 3)) + "," + legendPadding + ")").call(legendColour);
});
});
function drawLegend() {
};
function formatPercentage(n) {
let roundedNumber = Math.round(n*100);
return roundedNumber + "%";
};
function convertTextToNumbers(d) {
d.value = +d.value;
d.month_number = +d.month_number;
d.date = dateParser(d.date);
d.year = +d.year;
d.diff_to_avg = +d.diff_to_avg - 1;
return d;
};
</script>
</body>
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3-annotation/1.17.0/d3-annotation.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js