Skip to content

arenevier/activities-heatmap

Repository files navigation

activities-heatmap

Description

This library generates a heatmap based on activity data, highlighting routes and paths. The more frequently a path is used, the brighter it appears on the map. The library can be used to implement a tile layer server that renders heatmap tiles efficiently.

Background

There are already tools and libraries to draw a heatmap of activities, for example strava-local-heatmap-tool. But the rendering is done on the client side, which can be inefficient for large dataset. As of November 2024, I have about 1800 activities representing 1.5 million vertices. Downloading and parsing the data on the client side would be prohibitive. That is why I developed that library for use on the server side with a Node.js application.

Installation

npm install activities-heatmap

Usage

The following code renders the heatmap for the tile x, y, z:

      import { HeatmapProducer } from 'activities-heatmap';

      const producer = new HeatmapProducer(datasource);
      const bitmap = await producer.BitmapForTile({x: x, y: y, z: z});

z is the zoom level, and x and y are tile coordinates, according to XYZ standard implemented in OpenLayers, Leaflet, Mapbox, etc.

Providing the data

The dataSource argument of HeatmapForTile represents an object that provides access to activity data, and can retrieve paths within a given bounding box. Two activities sources are provided:

  • StravaLocalZip: reads activities from a local zip bulk export from strava.
  • PostgisDB: reads activities from a postgis database.

You can implement your own data source by implementing the ActivitiesSource interface.

Filtering

data sources can optionally support filtering, to display the heatmap for a subset of the activities only. By providing an activitiesFilter argument to HeatmapForTile, it will pass than filter to the data source. Filter options will be specific to each data source.

Both StravaLocalZip and PostgisDB support filtering by date, and by sport type. If you implement your own datasource, you can decide which filters to implement.

      const bitmap = await producer.BitmapForTile({
                                          x: x, y: y, z: z,
                                          activitiesFilter: {startDate: '2020-01-01', endDate: '2020-12-31'}
                                         });

Rendering options

You can customize the heatmap appeearance by passing a renderingOptions argument to HeatmapForTile. The rendering options are:

  • valueForMaxColor: Specifies the threshold for the brightest color. If a path has been taken more than this number of times, it is rendered with the brightest color. (Default is 25).
  • lineWidth: Defines the width of the lines in the heatmap. (Default is 2).
  • gradientColors: An array of colors to use as linear gradients when picking a color for the path.
    • Default is [ [0x4B, 0x00, 0x82, 130], [0xB2, 0x22, 0x22, 155], [0xFF, 0x00, 0x00, 180], [0xff, 0x45, 0x00, 205], [0xFF, 0x69, 0x00, 230], [0xFF, 0xFF, 0xE0, 255] ].
    • For example, if the path has been taken more than valueForMaxColor, it will be rendered with the color [0xFF, 0xFF, 0xE0, 255] (aka rgba(255, 255, 224, 1)). If it has been taken between 80% and 100% of that value, it will be rendered with a color between [0xFF, 0x69, 0x00, 230] and [0xFF, 0xFF, 0xE0, 255] (aka rgba(255, 105, 0, 0.9) and rgba(255, 255, 224, 1)). And so on.
    • By passing a different array of colors, you can change the rendering colors of the heatmap.
      const bitmap = await producer.BitmapForTile({
                                          x: x, y: y, z: z,
                                          renderingOptions: {valueForMaxColor: 50, lineWidth: 3}
                                         });

Implementation

Pixel drawing

The heatmap is created by drawing activity paths on a bitmap. Each time a path goes trough a pixel, the count is incremented for that pixel. After all paths have been processed for, each pixel is colored according to the count.

I use anti-aliasing to avoid the heatmap paths from being blurry. It means that I cannot use existing libraries like node-canvas or skia-canvas for line rendering, and had to implement the line drawing algorithm inside the activities-heatmap. With anti-aliasing and a configurable linewidth, it requires to draw a polygon for each path. Fortunately, there are good resources to implement that algorithm.

Indeed, each color channel contains only 256 values. So if I want to use 16 values for antialiasing, I would be able to draw only 16 (256/16) paths before reaching the maximum value. With the default value, the maximum color is applied for 25 paths. So in theory, I could have used 8 (256 / 32) levels of antialiasing. But I want to provide the ability to specify a value of more than 32.

Web workers

Rendering paths is computationally intensive, and is done on the main thread. Unfortunately, moving the computation to a web worker would not improve the situation, since the serialization/deserialization of the paths would take longer than the computation itself.

Demo

The website at https://heatmap.renevier.net/ is a simple application that uses this library. It displays a heatmap of the author's list of Strava activities from 2018 to 2024. The code source is available at https://github.com/arenevier/heatmap-demo

For reference:

strava blog about the implementation of the global heatmap: https://medium.com/strava-engineering/the-global-heatmap-now-6x-hotter-23fc01d301de

About

Create heatmap of GPS activities (typescript library).

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published