This example demonstrates how to use a heatmap to show time series data. The actual heatmap visualization is not difficult to generate---more time in this example is spent on preparing the data using d3.nest()
and on creating the color legend.
This data is not adjusted for changes in population, and this spans enough time that what we could really be seeing is a change in population and not a change in crime.
We might be able to fix that using these numbers:
But, this is not yet fixed in this example.
You can access the dataset for this example directly at:
https://data.sfgov.org/Public-Safety/Monthly-Property-Crime-2005-to-2015/k5vw-3yuz
The dataset is derived from the SFPD Incidents - from 1 January 2003 dataset available at data.sfgov.org.
xxxxxxxxxx
<head>
<meta charset="utf-8">
<link href="style.css" rel="stylesheet" type="text/css">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="heatmap.js"></script>
</head>
<body>
<script>
/* helper method to make translating easier */
var translate = function(x, y) {
return "translate(" + x + "," + y + ")";
};
// container for configuration parameters
var config = {};
config.svg = {
width: 960,
height: 500
};
config.margin = {
top: 50,
right: 10,
bottom: 20,
left: 40
};
config.plot = {
width: config.svg.width - config.margin.right - config.margin.left,
height: config.svg.height - config.margin.top - config.margin.bottom
};
config.legend = {
width: 200,
height: 15
};
// create svg based on config
var svg = d3.select("body").append("svg")
.attr("width", config.svg.width)
.attr("height", config.svg.height);
// create plot area based on config
var plot = svg.append("g")
.attr("id", "plot")
.attr("transform", translate(config.margin.left, config.margin.top));
// create the scales and set ranges
// https:github.com/d3/d3-scale/blob/master/README.md#scaleBand
// we only need the plot dimensions for the range!
// we will set the domain later when we load the data
var xScale = d3.scaleBand()
.range([0, config.plot.width]);
var yScale = d3.scaleBand()
.range([config.plot.height, 0], 0, 0);
// create color scale for each cell
// https://github.com/d3/d3-scale/blob/master/README.md#scaleSequential
var colorScale = d3.scaleSequential(d3.interpolateViridis);
// formatter to parse dates
// https://github.com/mbostock/d3/wiki/Time-Formatting
// must match format used by csv
// converts string to date
var dateParse = d3.timeParse("%x %I:%M:%S %p");
// used to nest data and output values nicely
// converts date to string
var yearFormat = d3.timeFormat("%Y");
var monthFormat = d3.timeFormat("%b");
// placeholder for data
var data = [];
var file = "Monthly_Property_Crime_2005_to_2015.csv";
d3.csv(file, rowAccessor, dataCallback);
function rowAccessor(d) {
var row = {};
row.date = dateParse(d["Date"]);
row.year = yearFormat(row.date);
row.month = monthFormat(row.date);
row.type = d["Category"];
row.count = +d["IncidntNum"];
return row;
}
function dataCallback(error, rows) { // callback
if (error) {
console.warn(error);
}
// we can use fancy d3 operations to group our data by year and month
// https://github.com/d3/d3-collection/blob/master/README.md#nest
data = d3.nest()
.key(function(d) {
return d.year;
})
// group by year first
.key(function(d) {
return d.month;
})
// group by month second
.rollup(function(values) {
// sum together all of the categories
return d3.sum(values, function(d) {
return d.count;
})
})
.map(rows, d3.map); // return as nested d3.maps
console.log("rows", rows);
console.log("nest", data);
// pull out years and months
// use to set domain of x and y scales
var years = data.keys();
var months = data.get(years[0]).keys();
xScale.domain(months);
yScale.domain(years);
/*
* how will we map our values to those colors?
* our first step is to calculate the min and max
* the issue now is our sums are nested
*/
// un-nesting these values is a little strange looking
// debug each step in the console so you know what is going on
// this returns all the values in our map
// in this case, our values are themselves maps
var values = data.values();
// now we have an array of maps
// grab the map values for each inner map
values = values.map(function(d) {
return d.values();
});
// now we have an array of arrays
// merge arrays into a single array
values = d3.merge(values);
// now we can calculate the extent our counts
var extent = d3.extent(values);
/*
* is this efficient?
* goodness no! but if you cared, why are you doing
* data processing in javascript!
*/
// use these values as domain for our color scale
colorScale.domain(extent);
// finally, draw stuff
drawBackground();
drawAxes();
drawHeatmap();
drawTitle();
drawLegend();
}
</script>
</body>
https://d3js.org/d3.v4.min.js