This workshop will show you how to:

Bike docks location

To complete this workshop you will need:

Resources and Geodata

Languages used:

DeckGL is a WebGL-powered framework for visual exploratory data analysis of large datasets. Based on Luma.GL, DeckGL provides high-performance visualisations of large datasets using GPU computation and a Layer Approach.
In this first section, GeoJsonLayer and TextLayer will be used.

To get started with DeckGL, create a new index.html file. Add an element div with id="map" as child of another element div with id="container", the former will contain the WebGL renderer of DeckGL

<!DOCTYPE html>
<html>
<title>DeckGL, Hello Data!</title>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- DeckGL Library -->
    <script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>

    <style type="text/css">
        body {
            margin: 0;
            padding: 0;
        }

        #container {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
        }

        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }
    </style>
</head>

<body>
    <div id="container">
      <div id="map"></div>
    </div>
</body>
</html>

The next step is to add a JS script to initialise DeckGL with the essential parameters (i.e. name of the HTML container, latitude, longitude and controller). The script can be added as last tag in the body element of the index.html file or as an external file.

<script type="text/JavaScript">

  const deckgl = new deck.DeckGL({
    container: 'map',
    initialViewState: {
        latitude: 51.47,
        longitude: 0.45,
        zoom: 4,
        bearing: 0,
        pitch: 30
    },
    parameters: {
    //Canvas background color, it can be applied as DIV CSS as well
      clearColor: [0, 0.8, 0.8, 0.5] //RGB 0-1+ opacity
    },
    controller: true,
  });
</script>

Empty DeckGL container

The result is an empty, light blue, interactive page... let's add some data. DeckGL uses a Layer Approach to visualise data. It is possible to add a simple base map using a GeoJson dataset. The GeoJsonLayer is one of the Core Layers.

The layers are passed to the parameters of the DeckGL object in form of array:

 const deckgl = new deck.DeckGL({
    container: 'map', // the id of the div element
    initialViewState: {
      latitude: 51.47,
      longitude: 0.45,
      zoom: 4,
      bearing: 0,
      pitch: 0
    },
    parameters: {
    //Canvas background color, it can be applied to DIV CSS as well
      clearColor: [0, 0.8, 0.8, 0.5] //RGB 0-1+ opacity
    },
    controller: true, //activate the mouse control

    layers: [...]
  });

The first dataset we are going to use, ne_50m_admin_0_countries.geojson, contains the world countries in form of polygons; the second dataset, ne_10m_populated_places_simple.geojson, is used to visualise, in form of points, the most populated places on earth:

//base map
const COUNTRIES = 'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_countries.geojson'

const deckgl = new deck.DeckGL({
    container: 'map', // the id of the div element
    initialViewState: {
      latitude: 51.47,
      longitude: 0.45,
      zoom: 4,
      bearing: 0,
      pitch: 0
    },
    parameters: {
    //Canvas background color, it can be applied to DIV CSS as well
      clearColor: [0.38, 0.89, 0.94, 0.5] //RGB 0-1+ opacity
    },
    controller: true, //activate the mouse control

    layers: [
      //First layer
      new deck.GeoJsonLayer({
        id: 'base-map', //every layer needs to have a unique ID
        data: COUNTRIES, //data can be passed as variable or added inline
        // Styles
        stroked: true,
        filled: true,
        lineWidthMinPixels: 1,
        opacity: 0.7,
        getLineColor: [252, 148, 3], //RGB 0 - 255
        getFillColor: [79, 75, 69]
      }),
    ]
    });

The result is a base map of the world that can be easily customised using the properties after the Style comment

A simple GeoJson Base Map

The order of visualisation in DeckGL follows the order of the layers in the array. The last layer of the array is the one on the top.

The next layer is the ne_10m_populated_places_simple.geojson

const COUNTRIES = 'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_countries.geojson'

const POPULATION = 'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_10m_populated_places_simple.geojson'

const deckgl = new deck.DeckGL({
    container: 'map',
    initialViewState: {
      latitude: 51.47,
      longitude: 0.45,
      zoom: 4,
      bearing: 0,
      pitch: 0
    },
    parameters: {
    //Canvas background color, it can be applied to DIV CSS as well
      clearColor: [0.38, 0.89, 0.94, 0.5] //RGB 0-1+ opacity
    },
    controller: true,

    layers: [
      //First layer
      new deck.GeoJsonLayer({
        id: 'base-map',
        data: COUNTRIES,
        // Styles
        stroked: true,
        filled: true,
        lineWidthMinPixels: 1,
        opacity: 0.7,
        getLineColor: [252, 148, 3], //RGB 0 - 255
        getFillColor: [79, 75, 69]
      }),

      //Second layer
      new deck.GeoJsonLayer({
        id: 'population',
        data: POPULATION,
        dataTransform: d => d.features.filter(f => f.properties.featurecla === 'Admin-0 capital'),
        // Styles
        filled: true,
        pointRadiusScale: 10,
        getPointRadius: f => f.properties.pop_max / 1000,
        getFillColor: [200, 0, 80, 190],
      }),
    ]
    });

Filter the Geojson and change the radius

Using Deck.GL it is possible transform the data on-the-fly using the dataTransform. In this example using arrows functions

 dataTransform: d => d.features.filter(f => f.properties.featurecla === 'Admin-0 capital'),

If we are not sure about the result of the dataTransform, we can log it in the console by changing the syntax of the arrow function

 dataTransform: d =>{ console.log(d.features); return d.features.filter(f => f.properties.featurecla === 'Admin-0 capital')},

the features are filtered by countries' capitals, and by using

getPointRadius: f => f.properties.pop_max / 1000,

the radius of the buffer is created according to the population value of the city (divided by 1000 for visualisation reasons).

A common layer is the TextLayer used to add labels to the map features

    layers: [
      //First layer
      new deck.GeoJsonLayer({
          id: 'base-map',
          data: COUNTRIES,
          // Styles
          stroked: true,
          filled: true,
          lineWidthMinPixels: 1,
          opacity: 0.7,
          getLineColor: [252, 148, 3], //RGB 0 - 255
          getFillColor: [79, 75, 69]
      }),

      //Second layer
      new deck.GeoJsonLayer({
          id: 'population',
          data: POPULATION,
          dataTransform: d => d.features.filter(f => f.properties.featurecla === 'Admin-0 capital'),
          // Styles
          filled: true,
          pointRadiusScale: 10,
          getPointRadius: f => f.properties.pop_max / 1000,
          getFillColor: [200, 0, 80, 190],

      }),

      //Third layer
      new deck.TextLayer({
          id: 'text-layer',
          data: POPULATION,
          dataTransform: d => d.features.filter(f => f.properties.featurecla === 'Admin-0 capital'),
          getPosition: f => f.geometry.coordinates,
          getText: f => { return f.properties.pop_max.toString(); },
          getSize: 22,
          getColor: [0, 0, 0, 180],
          getAngle: 0,
          getTextAnchor: 'middle',
          getAlignmentBaseline: 'bottom',
          fontFamily: 'Gill Sans',
          background: true,
          getBackgroundColor: [255, 255, 255,180]
      }),

        ]

Adding the labels

In addition to the TextLayer it is possible to display information using the getTooltip function. getTooltip is used per layer and it is essential to set the parameter pickable: true to each layer we want to interact with

 //Second layer
      new deck.GeoJsonLayer({
          id: 'population',
          data: POPULATION,
          dataTransform: d => d.features.filter(f => f.properties.featurecla === 'Admin-0 capital'),
          // Styles
          filled: true,
          pointRadiusScale: 10,
          getPointRadius: f => f.properties.pop_max / 1000,
          getFillColor: [200, 0, 80, 190],
          //Interactivity
          pickable: true,
      }),

The getTooltip function needs to be added outside the layers' array

getTooltip: ({ object }) => object && { //object is the reference to the hover feature
            html: `<b>${object.properties.name}:</b> ${object.properties.pop_max}`, //html of the tooltip content
            style: {
                backgroundColor: 'steelblue',
                fontSize: '0.8em',
                color: 'white',
            }
        }

Tooltip and interactivity

The simple base map we used is too limited to provide advanced and multi-scalar spatial visualisations. DeckGL accepts multiple map providers to enhance the visualisation outcomes, such as MapLibre, MapboxGL, HERE, CARTO and Google Maps. Each of them provides a different level of detail and features.
Some of them require signing up to obtain an API Token (e.g. HERE and Google Map) others can be used without any authorization system (e.g. MapboxGL using the version 1 of the library or the open project Maplibre).

MapboxGL integrates nicely with DeckGL. It provides various vector base map styles and, through the DeckGLMapbox Layer, it is possible to overlay seamlessly the DeckGL Layers. The MapboxGL library needs a CSS style based on the same version to properly work.

<!-- Mapbox Library -->
<script src='https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.css' rel='stylesheet' />

We need to remove the content of the script tag (or create a new index.html) add both Mapbox library and CSS.

<!-- Mapbox Library -->
  <script src='https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.js'></script>
  <link href='https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.css' rel='stylesheet' />

In the script element, or in an external JavaScript file, we need to add the access token API_TOKEN_Mapbox obtained from Mapbox website to be passed to the mapboxgl instance:

    // Set your API key here
    const API_TOKEN_Mapbox = 'Your_API-Token';
    mapboxgl.accessToken = API_TOKEN_Mapbox;

and the initial status of the map view in a separate variable. As in the previous example, the HTML will contain just two div elements with id container and map and their CSS style.

A way to pass parameters to the map object is to create a JavaScript object

    const INITIAL_VIEW_STATE = {
        longitude: 0.12,
        latitude: 51.5,
        zoom: 10,
        bearing: 0,
        pitch: 0
    };

The Object is added to the Mapbox map function, together with the style of the vector tiles to use

    // MapBox Vector Tile
    const map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/light-v11', //vector tiles require a Mapbox API to access them
        // Note: deck.gl will be in charge of interaction and event handling
        interactive: true,
        center: [INITIAL_VIEW_STATE.longitude, INITIAL_VIEW_STATE.latitude],
        zoom: INITIAL_VIEW_STATE.zoom,
        bearing: INITIAL_VIEW_STATE.bearing,
        pitch: INITIAL_VIEW_STATE.pitch,
        projection: 'mercator' //see https://docs.mapbox.com/mapbox-gl-js/api/map/ for a list of available projections
    });

The result is a Mapbox map centred on London

An Mapbox vector tile interactivity

Mapbox uses OpenStreetMap data. It is possible to extrude the buildings to create a 3D model of urban environment

 map.on('load', () => {
  const firstLabelLayerId = map.getStyle().layers.find(layer => layer.type === 'symbol').id;
      map.addLayer({
        'id': '3d-buildings',
        'source': 'composite',
        'source-layer': 'building',
        'filter': ['==', 'extrude', 'true'],
        'type': 'fill-extrusion',
        'minzoom': 15,
        'paint': {
            'fill-extrusion-color': '#aaa',

            // use an 'interpolate' expression to add a smooth transition effect to the
            // buildings as the user zooms in
            'fill-extrusion-height':["get", "height"],
            'fill-extrusion-base': ["get", "min_height"],
            'fill-extrusion-opacity': 0.8
        }
      },
      firstLabelLayerId
      );
    });

the event load on the map.on function is used to ensure that the 3D layer is added once the map is ready. The firsLabelLayerId forces all the labels of the map to be displayed over the other layers.

3D buildings in Canary Wharf

'fill-extrusion-height':[
      "interpolate", ["linear"], ["zoom"],
          // zoom is 15 (or less) -> buildings height is 0
            15, 0,
          // zoom is 15.05 (or greater) -> buildings height is actual value
            15.05, ["get", "height"]
        ],
'fill-extrusion-base':[
        "interpolate", ["linear"], ["zoom"],
          // zoom is 15 (or less) -> buildings height is 0
            15, 0,
          // zoom is 15.05 (or greater) -> buildings height is actual value
            15.05, ["get", "min_height"]
        ],

DeckGL layers can be added to the MapboxGL vector tiles canvas by using deck.MapboxLayer method inside the map.on('load') function. The following script adds a GeoJsonLayer and an ArcLayer to the base map using the same dataset:

// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz
    const AIR_PORTS =
  'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson';

map.on('load', () => {
   const AirportPointLayer = new deck.MapboxLayer({
        id: 'airports',
        type: deck.GeoJsonLayer,
        data: AIR_PORTS,
            // Styles
        filled: true,
        pointRadiusMinPixels: 2,
        pointRadiusScale: 200,
        getPointRadius: f => (11 - f.properties.scalerank),
        getFillColor: [200, 0, 80, 180],
            // Interactive props
        pickable: true,
        autoHighlight: true,
        onClick: info => info.object && alert(`${info.object.properties.name} (${info.object.properties.abbrev})`)
    });

    const AirportArcLayer = new deck.MapboxLayer({
        id: 'arcs',
        type: deck.ArcLayer,
        data: AIR_PORTS,
        dataTransform: d => d.features.filter(f => f.properties.scalerank < 4),
            // Styles
        getSourcePosition: f => [-0.4531566,51.4709959], // London
        getTargetPosition: f => f.geometry.coordinates,
        getSourceColor: [0, 128, 200],
        getTargetColor: [200, 0, 80],
        getWidth: 1
    });

    map.addLayer(AirportPointLayer);
    map.addLayer(AirportArcLayer);
});

vector tiles with Deck ArcLayer

DeckGL provides also layers that are specifically focused and optimised for 3D visualisation. One of them is the ScenegraphLayer.

The next script is used to visualise a 3D model for each tree in the Camden borough. The data is passed as GeoJSON. The relative path of the 3D model is set in the scenegraph option.

ATTENTION 3D models could introduce performance issues on mobile devices if too complex (e.g. multiple materials, high number of vertices). GLTF file format or GLB file format with DRACO compression are suggested file format, however FBX and OBJ file formats can be used as well.

const CamdenTrees= 'https://opendata.camden.gov.uk/resource/csqp-kdss.geojson?$limit=50000' //limit attribute is needed to get all the data more info on https://dev.socrata.com/docs/queries/limit.html

const treeCamden3DLayer = new deck.MapboxLayer({
    id: 'tree3D',
    type: deck.ScenegraphLayer,
    data:CamdenTrees,
    dataTransform: d => d.features.filter(f => f.geometry !=null), //filter the data in case of null values
    getPosition: d => d.geometry.coordinates,
    getOrientation: d => [0, Math.random() * 180, 90],
    scenegraph: 'resources/tree.glb',
    sizeScale: 3,
    _lighting: 'pbr',
      });
map.addLayer(treeCamden3DLayer);

3D models tree Camden

Another option is to use the IconLayer

const treeCamdenIconLayer = new deck.MapboxLayer({
      id: 'icon-tree',
      type: deck.IconLayer,
      data:CamdenTrees,
      pickable: true,
      // iconAtlas and iconMapping are required
      iconAtlas: 'https://upload.wikimedia.org/wikipedia/commons/b/ba/Icon_Tree_256x256.png',
      iconMapping: {marker: {x: 0, y: 0, width: 256, height: 256, anchorY: 256, mask: false}},
      billboard: true,
      dataTransform: d => d.features.filter(f => f.geometry !=null),
      getPosition: d => d.geometry.coordinates,
      getIcon: d => 'marker',
      sizeScale: 10,
      getSize: d => 5,
      getColor: d => [120, 140, 0],
      pickable: true,
      autoHighlight: true,
      onClick: info => console.log(info.object),
      });

map.addLayer(treeCamdenIconLayer);

map.setLayerZoomRange('icon-tree', 15, 20); //visualise the layer between zoom 15 and 20

The icon used by the IconLayer does not need to be stored locally. In this example we also used a MapBox native function map.setLayerZoomRange(['LAYER_ID'], [MIN_ZOOM], [MAX_ZOOM]) to limit the visibility of a specific layer between different levels of zoom. The icon can be used as a billboard (i.e. facing the camera) or not (i.e. z up).
A way to know the level of zoom of the current visualisation is to use a moveend event on the main map.

map.on("moveend", e => {
  const ViewState = {
    zoom: map.getZoom(),
    };
    console.log(ViewState);
  });

3D models tree Camden

Finally, custom models can be added also using the GeoJsonLayer. Add RussellSquare.geojson to the resources folder of the website. Create a new MapboxLayer of type deck.GeoJsonLayer and point the getElevation function to the value of the elevation of the GeoJSON file (in this case properties._mean)

const russellSqmodel = new deck.MapboxLayer({
    id: 'geojson-layer',
    type: deck.GeoJsonLayer,
      data: './resources/russellSq.geojson',
      pickable: true,
      filled: true,
      extruded: true,
      autoHighlight: true,
      getFillColor: [238,231,215, 255],
      getElevation: d=>d.properties._mean, //User generated from QGis
  });
  map.addLayer(russellSqmodel);

3D models trees and Russell square

DeckGL can be used to effectively visualise real-time data from various sources (REST API, MQTT brokers).

DeckGL and Real Time API

Create a new index.html. In addition to the DeckGL and Mapbox libraries, we are going to use the library D3.js:

<html>
  <head>
  <!-- DeckGL Library -->
    <script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>

  <!--MapBox 2.10.0-->
    <script src='https://api.mapbox.com/mapbox-gl-js/v2.10.0/mapbox-gl.js'></script>
    <link href='https://api.mapbox.com/mapbox-gl-js/v2.10.0/mapbox-gl.css' rel='stylesheet' />

  <!--D3.js-->
    <script src="https://d3js.org/d3.v7.min.js"></script>

A style for the container and map elements

  <style>
    #container {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
    }

    #map {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 100%;
    }
  </style>
</head>

and the container and map elements in the body of the page:

<body>
  <div id="container">
    <div id="map"></div>
  </div>
</body>

Add a Javascript file as part of the index.html or as external file. Any base map provider can be used at this point, in this example we will use the Vector Map from Mapbox, therefore the Mapbox API Key is needed:

const API_TOKEN_Mapbox = "YOUR API KEY FROM MAPBOX";

mapboxgl.accessToken = API_TOKEN_Mapbox;

const INITIAL_VIEW_STATE = {
  longitude: -0.01132798084514334,
  latitude: 51.54256346290661,
  zoom: 15,
  bearing: 30,
  pitch: 45,
};

const map = new mapboxgl.Map({
  container: "map",
  style: "mapbox://styles/mapbox/dark-v10", //vector tiles require a Mapbox API to access them
  // Note: deck.gl will be in charge of interaction and event handling
  interactive: true,
  center: [INITIAL_VIEW_STATE.longitude, INITIAL_VIEW_STATE.latitude],
  zoom: INITIAL_VIEW_STATE.zoom,
  bearing: INITIAL_VIEW_STATE.bearing,
  pitch: INITIAL_VIEW_STATE.pitch,
  projection: "mercator",
});

map.on("load", () => {
  const firstLabelLayerId = map
    .getStyle()
    .layers.find((layer) => layer.type === "symbol").id;
  map.addLayer(
    {
      id: "3d-buildings",
      source: "composite",
      "source-layer": "building",
      filter: ["==", "extrude", "true"],
      type: "fill-extrusion",
      minzoom: 15,
      paint: {
        "fill-extrusion-color": "#aaa",
        "fill-extrusion-height": ["get", "height"],
        "fill-extrusion-base": ["get", "min_height"],
        "fill-extrusion-opacity": 0.8,
      },
    },
    firstLabelLayerId
  );
});

Starting point Real time

Transport for London Unified API provides a series of publicly accessible real-time APIs, in this case, we are going to use the bike sharing API that returns the location of all the Santander Dock Stations in London together with the number of bikes available for each station updated every 5 minutes.

As the result of the API, the request is a JSON response, and not a GeoJSON one, we are going to use ScatterplotLayer instead of the GeoJSONLayer. The JSON response contains various information for each bike dock. For this visualisation the essential values are latitude and longitude, which are part of the root of the JSON file, data.lat and data.lon, and the values of the number of free bikes and empty docks that are part of the array additionaProperties and they are respectively the index 6 and 7. The value of the free bikes is used to define the radius of the point on the map

map.on("load", () => {
  [..]//existing map
  const BikeTFL = new deck.MapboxLayer({
    id: "bike-tfl",
    data: "https://api.tfl.gov.uk/BikePoint",
    type: deck.ScatterplotLayer,
    pickable: true,
    opacity: 0.8,
    stroked: true,
    filled: true,
    radiusScale: 50,
    radiusMinPixels: 10,
    radiusMaxPixels: 150,
    lineWidthMinPixels: 1,
    getPosition: (d) => [d.lon, d.lat],
    getPointRadius: (d) => d.additionalProperties[6].value,
    getFillColor: (d) => [255, 140, 0],
    getLineColor: (d) => [0, 0, 30],
  });

  map.addLayer(BikeTFL);
});

Bike docks location

Using the D3.js function scaleThreshold it is possible to dynamically change the colours of the docks depending on the number of bikes available. At the top of the script create a new const colorScaleFunction

const INITIAL_VIEW_STATE = {
  longitude: -0.01132798084514334,
  latitude: 51.54256346290661,
  zoom: 15,
  bearing: 30,
  pitch: 45,
};

const colorScaleFunction = d3
  .scaleThreshold()
  .domain([0, 5, 10, 15, 20, 25]) //number of bikes
  .range([
    //RGB format
    [255, 13, 13],
    [255, 78, 17],
    [255, 142, 21],
    [250, 183, 51],
    [172, 179, 52],
    [105, 179, 76],
  ]);

in the BikeTFL layer change the getFillColor to getFillColor: (d) => colorScaleFunction(d.additionalProperties[6].value),

To access all the variables of the layer we can add a popup to each point to display the data that are part of the JSON response. The events onHover and onClick can be used for this purpose. In this example the popup is styled using a CSS Grid to an empty div named tooltip using D3.js.

Update the style with the following elements:

#tooltip {
  position: absolute;
  visibility: hidden;
  z-index: 2;
  right: 0;
  top: 0;
  width: 20%;
  height: auto;
  min-height: 100px;

  display: grid;
  grid-template-columns: auto auto auto;
  grid-template-rows: auto;
  grid-template-areas:
    "icon title title"
    "item-a item-a ."
    "item-b item-b .";

  background-color: rgb(255, 216, 145);
  border: 1px solid #888;
}

.title {
  grid-area: title;
  font-weight: bold;
  margin: 4px;
}

.item-a {
  grid-area: item-a;
  margin: 4px;
}

.item-b {
  grid-area: item-b;
  margin: 4px;
}

.icon {
  grid-area: icon;
  margin: 4px;
  max-width: 50%;
  height: auto;
  place-self: center;
}

Then, add a new variable let isHovering = false; just after the const API_TOKEN_Mapbox and, to the layer BikeTFL, the onHover function:

[...]
getLineColor: d => [0, 0, 30],
onHover: ({ object }, info) => {
       (isHovering = Boolean(object));
       if (isHovering == true) {
        console.log(object.additionalProperties[6].value);
        showTooltip(info, object);
        map.getCanvas().style.cursor = 'pointer';
        }
        else { hideTooltip();
            map.getCanvas().style.cursor = 'grab';}
      },
    });

map.addLayer(BikeTFL);
map.__deck.props.getCursor = () => map.getCanvas().style.cursor;

When the user is hovering over one of the bike docks:

The showTooltip function is used to make the tooltip visible and to populate the div element with id tooltip using the D3.js library.

If the mouse leaves the object, a different function hideTooltip is used to change the visibility of the popup to hidden. The two functions can be added anywhere in the script:

function hideTooltip() {
  d3.select("#tooltip").style("visibility", "hidden");
}

function showTooltip(info, object) {
  //varibales to keep the tooltip inside the view of the browser
  let mapSize={
    height: d3.select("#map").node().getBoundingClientRect().height,
    width: d3.select("#map").node().getBoundingClientRect().width
  }
  let tooltipSize={
    height: d3.select("#tooltip").node().getBoundingClientRect().height,
    width: d3.select("#tooltip").node().getBoundingClientRect().width
  }
    d3
      .select("#tooltip")
      .style("top", function(){
        if(info.center.y+tooltipSize.height>mapSize.height)
        { return info.center.y - tooltipSize.height + "px"; } 
        else { return info.center.y + 3 + "px" }
      })
      .style("left", function(){
        if(info.center.x+tooltipSize.width>mapSize.width)
        {return info.center.x - tooltipSize.width + "px";} 
        else{return info.center.x + 3 + "px"}
      }
        )
    .style("visibility", "visible")
    .style("pointer-events", "none")
    .html(`<img src="https://upload.wikimedia.org/wikipedia/commons/d/d4/Cyclist_Icon_Germany_A1.svg" class='icon'/>
             <p class='title'> ${object.commonName}</p>
             <p class='item-a'> Free Bikes: ${object.additionalProperties[6].value}</p>
             <p class='item-b'> Empty Docks: ${object.additionalProperties[7].value}</p>`);
}

In this case, we used D3.js to select the div element, change dynamically its position, its visibility and the style of the pointer, and inject the HTML code and variables from the JSON response.

Bike docks location

We can now access, and further customise, the bike sharing data. In order to update the values in real time, we need to add a final step. At the moment the request is called just once when the map is loaded. In order to update the entire JSON on a predefined interval we need to add a setInterval function inside the map.on('load') function

map.addLayer(BikeTFL);

const timer = setInterval(() => {
  BikeTFL.setProps({ data: "https://api.tfl.gov.uk/BikePoint" });
}, 300000); //API called every 5 min => 300000 ms

Among the various visualisation libraries, ECharts is an Open Source solution that provides a large number of options and customisations with detailed examples. From basic line chart to more complex 3D charts and animations, ECharts is an interesting tool to explore and that can be a good addition to improve the legibility, and user engagement, of a web-map and not only.

Let's create a first standalone chart to familiarise ourselves with the structure of EChart before embedding it to DeckGL.
In a new index.html, add the EChart and D3.js libraries.

<!--D3.js-->
<script charset="utf-8" src="https://d3js.org/d3.v7.min.js"></script>

<!--ECharts-->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script>

A simple stylesheet for two elements named container and main

#container {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

#main {
  width: 1000px;
  height: 600px;
  margin: auto;
  margin-top: 50px;
}

And the elements container and main in the body of the html

<body>
  <div id="container">
    <div id="main"></div>
  </div>
</body>

We can start now to write the Javascript as part of the index.html or as an external file

d3.csv(
  "https://raw.githubusercontent.com/vsigno/publicResources/main/CityData_WUP2018_top20.csv", d3.autoType).then(
  function (CityData) {
    //initialise ECharts in the Div id=main
    var myChartEchart = echarts.init(document.getElementById("main"), {
      width: 1000,
      height: 650
    }); //height is used to avoid cut the text on the Xaxis

The first part of the script is used to parse the dataset using the D3.js function d3.csv and then to initialise the chart in the html element main. Both width and height can be omitted, if not defined the parameter of the CSS style will be used

The chart is then created using a series of options to the initialised object via the function myChartEchart.setOption(option). There are multiple variables and settings that can be used, here we will see some of them but for a complete overview the Option section of the ECharts documentation is a good starting point

 /* An example of 'option' as variable
     //https://echarts.apache.org/en/option.html#series-bar.label
             var labelOption = {
             show: true,
             position: 'inside',
             rotate: 90,
             align: 'left',
             verticalAlign: 'middle',
             fontSize: 12,
             formatter: '{@pop1950} millions',
                 };*/

    // All the settings of the chart are provided in this variable
    var option = {
      title: { //Title and sub-title of the chart
        //ref https://echarts.apache.org/en/option.html#title
        text: "World's Largest Urban Agglomerations 2020",
        textStyle: {
          color: "blue",
          fontSize: 22,
          fontWeight: "bold"
        },
        subtext: "Population data UN World Urbanization Prospects",
        subtextStyle: {
          color: "coral",
          fontWeight: "bold"
        }
      },

      xAxis: { //style of the X
        type: "category",
        axisLabel: {
          interval: 0,
          rotate: 30, //If the label names are too long you can manage this by rotating the label.
          fontSize: 9
        }
      },

      yAxis: { //Style of the Y
        type: "value",
        name: "Population 2020 (millions)",
        nameLocation: "center",
        nameGap: 30,
        nameTextStyle: {
          align: "center"
        },
        splitNumber: 10
      },

      tooltip: { //a tooltip is shown onHover if true
        show: true
      },

      dataset: [ //the actual Dataset
        // ref https://echarts.apache.org/en/option.html#dataset
        {
          source: CityData
        }
      ],
      series: [ //How the dataset is represented
        {
          // ref https://echarts.apache.org/en/option.html#series-bar.type
          type: "bar", //type of chart and style
          showBackground: true,
          itemStyle: {
            color: "#ffcf7d",
            borderColor: "#ffac1f",
            borderWidth: 1.5,
            borderType: "dashed"
          },
          barWidth: "70%",
          //label: labelOption, //option can be coded in external variable, see above. ref https://echarts.apache.org/en/option.html#series-bar.label
          label: { //a label inside the bar
            show: true,
            position: "inside",
            rotate: 90,
            align: "left",
            verticalAlign: "middle",
            fontSize: 12,
            //formatter: '{@pop2035} millions', //pop value as strin
            formatter: function (params) {
              var pop2035_float = parseFloat(params.data.pop2035);

              return pop2035_float.toFixed(2) + ` millions`;
            }
          },
          encode: { //what value is disply on the X, Y and tooltip (based on the header of the CSV file used)
            x: "CityName",
            y: ["pop2020"],
            tooltip: ["pop1950", "pop2020", "pop2035"]
          },

          tooltip: {
            formatter: function (params) {
              //as pop values are strings, we parsed them to Float to better control the number of decimal places
              var pop1950_float = parseFloat(params.data.pop1950);
              var pop2020_float = parseFloat(params.data.pop2020);
              var pop2035_float = parseFloat(params.data.pop2035);

               return (
                `${params.name}<br />
                                  Population 1950 :  ` +
                pop1950_float.toFixed(2) +
                ` millions <br />
                                  Population 2020 :  ` +
                pop2020_float.toFixed(2) +
                ` millions <br />
                                  Population 2035 :  ` +
                pop2035_float.toFixed(2) +
                ` millions`

              );


            }
          },
          animationDuration: 2000,
          animationEasing: "elasticOut" //https://echarts.apache.org/examples/en/editor.html?c=line-easing
        }
      ]
    };

    // All the above is applied to the chart
    myChartEchart.setOption(option);
  }
);

To apply the various options to the chart we use the function setOption. The result is an animated and interactive bar chart that can be embedded in our website.

First Echarts

The use of ECharts in DeckGL is straightforward. The chart can be added in any position of the web-map: it could be on top of it (in this case the div element where the map is created needs to be placed outside the container element of the map) or, as we are going to see, inside the popup triggered by the onHover function.

In the last index.html created for Real Time data with DeckGL, add the ECharts library.
The CSS of the tooltip need to be changed as we need an additional row item-c on the grid-template-areas

grid-template-areas:
  "icon title title"
  "item-a item-a ."
  "item-b item-b ."
  "item-c item-c item-c";

add the a new style for the element item-c

#item-c {
  grid-area: item-c;
  margin: 4px;
  height: 300px;
}

Then, we need to change the onHover function. D3.js will create the div element for the chart. Finally, we call the function that creates ECharts

d3
  .select("#tooltip")
  .style("top", function(){
        if(info.center.y+tooltipSize.height>mapSize.height)
        { return info.center.y - tooltipSize.height + "px"; } 
        else { return info.center.y + 3 + "px" }
      })
      .style("left", function(){
        if(info.center.x+tooltipSize.width>mapSize.width)
        {return info.center.x - tooltipSize.width + "px";} 
        else{return info.center.x + 3 + "px"}
      }
        )
  .style("visibility", "visible")
  .style("pointer-events", "none")
  .html(`<img src="https://upload.wikimedia.org/wikipedia/commons/d/d4/Cyclist_Icon_Germany_A1.svg" class='icon'/>
             <p class='title'> ${object.commonName}</p>
             <p class='item-a'> Free Bikes: ${object.additionalProperties[6].value}</p>
             <p class='item-b'> Empty Docks: ${object.additionalProperties[7].value}</p>
             <div id='item-c'></div>`);
chart(object);

The function chart(object) is passing to EChart the object hovered by the user. As the object is already a JSON structure, we do not need to use D3.js to pass the values to the chart as we did in the previous section. In the chart function the first step is to initialise ECharts on the element with id item-c

function chart(dataset) {
  var myChartEchart = echarts.init(document.getElementById("item-c"));

The JSON response, in this case named dataset, is now accessible by the function chart

    var option = {
      title: {
        text:dataset.commonName,
        textStyle: {
          color: "blue",
          fontSize: 10,
          fontWeight: "bold"
        },
      },

using the respective keys the options can be populated using the JSON response (e.g. in this example the title of the chart uses the commonName of the bike station)

      xAxis: {
        type: "category",
       // data:[dataset.additionalProperties[6].key, dataset.additionalProperties[7].key],
       data:['Free Bikes', 'Empty Docks'],
        axisLabel: {
          interval: 0,
          rotate: 30, //If the label names are too long you can manage this by rotating the label.
          fontSize: 10
        }
      },

      yAxis: {
        type: "value",
        nameLocation: "center",
        nameTextStyle: {
          align: "center"
        },

In this case the keys for the free bikes and empty docks are not immediately intelligible, thus the text for these labels is manually encoded using the data array ([‘Free Bikes', ‘Empty Docks']).

      },
      series: [
        {
            type: 'bar',
            name: '2015',
            //data: [dataset.additionalProperties[6].value,dataset.additionalProperties[7].value]
            data: [
                    {
                      value: dataset.additionalProperties[6].value,
                      itemStyle: {color:'rgb('+colorScaleFunction(dataset.additionalProperties[6].value)+')'}
                    },
                    {
                      value: dataset.additionalProperties[7].value,
                      itemStyle:
                      itemStyle: {color:'rgb('+colorScaleFunction(dataset.additionalProperties[7].value)+')'}
                    }
                  ]
        }
      ]
    }

To further customise the visualisation, it is possible to pass conditional operations to style the single elements of the charts, in this example, the bars change colour from green to red depending on the value of the free bikes or empty docks

The final step is to set the options on the charts

    myChartEchart.setOption(option);
  }

Bike docks location