Day 4 - Hexagons

November 4, 2024 ยท 3 min read

Hexbin map of Boulder County crashes in 2023

I started with looking for some open datasets that included point values within the state of Colorado and found Colorado's Department of Transportation has statewide crash data for every year. I downloaded the 2023 dataset only to realize it was a bit larger than I expected (52.5 MB) and an excel file. Filtering the data down to Boulder county alone and exporting it as a csv resulted in a 4.2 MB file. Turns out there were a lot of columns I didn't need for this challenge since we'd be converting the points into hexbins anyways so I filtered those out and then removed all rows that were missing latitude and longitude data. This stripped down data was now just a 99 KB csv.

Now we need to convert this into a geojson and the simplest way I could find was using the GDAL ogr2ogr tool. This one line command converted my csv file into a geojson!

ogr2ogr -f "GeoJSON" crashes.geojson boulder_crashes_simple.csv -oo X_POSSIBLE_NAMES=Longitude -oo Y_POSSIBLE_NAMES=Latitude -oo KEEP_GEOM_COLUMNS=NO

Now let's remove whitespace and newlines.

jq -c . crashes.geojson > crashes_flat.geojson

The geojson representing all crashes in Boulder County that included lat/long position was only 465 KB, not bad. I plugged the resulting file into geojson.io and there is definitely data outside of Boulder county but there's no time to fix that now as the baby just went to bed and the clock is ticking. We need to convert this point data into hexagons and for that I used @turf/turf. Here's a little node script that takes a set of point data and converts it to hexagons with a crashes property equal to the number of points in that hexagon. Near the end of the script we filter out any hexagons that have no data before writing it to a new file.

import fs from 'fs';
import * as turf from '@turf/turf';

const points = JSON.parse(fs.readFileSync('crashes_flat.geojson'));
const bbox = turf.bbox(points);
const hexSize = 0.5;
const hexGrid = turf.hexGrid(bbox, hexSize, { units: 'kilometers' });

hexGrid.features.forEach((hex) => {
  const pointsInHex = turf.pointsWithinPolygon(points, hex);

  let crashes = 0;
  pointsInHex.features.forEach(() => {
    crashes += 1;
  });

  hex.properties.crashes = crashes;
});

hexGrid.features = hexGrid.features.filter((d) => d.properties.crashes !== 0);
fs.writeFileSync('hexagons.json', JSON.stringify(hexGrid));

With the hexagons.json file (now 255KB), we can now load it into our app as a source and add a fill layer referring to that source.

const crashColors = [
  [0, '#ffe5e5'],
  [10, '#ff9999'],
  [20, '#ff6666'],
  [30, '#ff3333'],
  [40, '#cc0000'],
  [50, '#800000'],
];

map.addLayer({
  id: 'hexagonCrashes',
  type: 'fill',
  source: 'hexGrid',
  layout: {},
  paint: {
    'fill-color': [
      'interpolate',
      ['linear'],
      ['get', 'crashes'],
      ...crashColors.flat(),
    ],
    'fill-opacity': 0.6,
  },
});

Here's the finished product for today. Having lived in Boulder County for over 8 years now, the data is not too surprising, the really dark red spots are pretty much exactly where I've seen accidents happen myself. As a father now I'm happy to see that my 500m hexagon on the map is accident free. ๐Ÿ˜„

Based on the MapBoxGL HexBin Map tutorial by Chris Henrick.