Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deck.gl how to show popup onClick

Setup:

Basic react app using react-map-gl to show a map with a deck.gl ScatterplotLayer over the top to visualise the data

Goal:

1) To show points on a map as circles of a given radius and colour.
2) When a user clicks on a circle, a tooltip/popup should show with more data about it (included in the data provided) until the user clicks away (essentially the same as this graph but for click instead of hover, http://uber.github.io/deck.gl/#/documentation/layer-catalog/scatterplot-layer. FYI I looked at the code for this and the hover logic has been removed, I assume for simplicity).

Issue:

I have completed point 1 but I cannot get point 2 to work. The furthest I have gotten to prove the data is there is to log to the console.

To note:

I'm not married to react-tooltip - I don't mind taking it out entirely if there's a better way of doing this. I only need to keep mapbox and deck.gl.

Data: https://gist.github.com/NikkiChristofi/bf79ca37028b29b50cffb215360db999

deckgl-overlay.js

import React, {Component} from 'react';
import ReactTooltip from 'react-tooltip';
import DeckGL, {ScatterplotLayer} from 'deck.gl';

export default class DeckGLOverlay extends Component {

  static get defaultViewport() {
    return {
      longitude: 0,
      latitude: 0,
      zoom: 2,
      maxZoom: 16,
      pitch: 0,
      bearing: 0
    };
  }

# in this method I want to update the variable tooltipText with
# whatever object data has been clicked. 
# The console log successfully logs the right data (i.e. the third 
# element in the array), but the tooltip doesn't even show 

  onClickHandler = (info) => {
    let dataToShow = info ? info.object[2] : "not found";
    this.tooltipText = dataToShow;
    console.log(dataToShow);
  }

  render() {
    const {viewport, lowPerformerColor, highPerformerColor, data, radius, smallRadius, largeRadius} = this.props;

    if (!data) {
      return null;
    }

    const layer = new ScatterplotLayer({
      id: 'scatter-plot',
      data,
      radiusScale: radius,
      radiusMinPixels: 0.25,
      getPosition: d => [d[1], d[0], 0],
      getColor: d => d[2] > 50 ? lowPerformerColor : highPerformerColor,
      getRadius: d => d[2] < 25 || d[2] > 75 ? smallRadius : largeRadius,
      updateTriggers: {
        getColor: [lowPerformerColor, highPerformerColor]
      },
      pickable: true, 
      onClick: info => this.onClickHandler(info),
      opacity: 0.3   
    });

    return (
      <DeckGL {...viewport} layers={ [layer] } data-tip={this.tooltipText}>
        <ReactTooltip />
      </DeckGL>
    );
  }
}

app.js

import React, {Component} from 'react';
import {render} from 'react-dom';
import MapGL from 'react-map-gl';
import DeckGLOverlay from './deckgl-overlay.js';
import {json as requestJson} from 'd3-request';

const MAPBOX_TOKEN = process.env.MAPBOX_TOKEN; // eslint-disable-line
const lowPerformerColor = [204, 0, 0];
const highPerformerColor = [0, 255, 0];
const smallRadius = 500;
const largeRadius = 1000;

const DATA_URL = 'https://gist.github.com/NikkiChristofi/bf79ca37028b29b50cffb215360db999';
export default class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      viewport: {
        ...DeckGLOverlay.defaultViewport,
        width: 500,
        height: 500
      },
      data: null
    };

    requestJson(DATA_URL, (error, response) => {
      if (!error) {
        console.log(response);
        this.setState({data: response});
      }
      else{
          console.log(error);
      }
    });
  }

  componentDidMount() {
    window.addEventListener('resize', this._resize.bind(this));
    this._resize();
  }

  _resize() {
    this._onViewportChange({
      width: window.innerWidth,
      height: window.innerHeight
    });
  }

  _onViewportChange(viewport) {
    this.setState({
      viewport: {...this.state.viewport, ...viewport}
    });
  }

  render() {
    const {viewport, data} = this.state;

    return (
      <MapGL
        {...viewport}
        onViewportChange={this._onViewportChange.bind(this)}
        mapboxApiAccessToken={MAPBOX_TOKEN}
        mapStyle='mapbox://styles/mapbox/dark-v9'>
        <DeckGLOverlay viewport={viewport}
          data={data}
          lowPerformerColor={lowPerformerColor}
          highPerformerColor={highPerformerColor}
          smallRadius={smallRadius}
          largeRadius={largeRadius}
          radius={300}
          />
      </MapGL>
    );
  }
}
like image 411
Naadof Avatar asked Mar 09 '23 00:03

Naadof


2 Answers

Figured out a way to do it.

Solution

I bubbled up the onClick event to the MapGL layer, and used the Popup element to display the data.

so in app.js:
1) import the Popup element from react-map-gl

import MapGL, { Popup } from 'react-map-gl';

2) Set coordinates state and "info" (to show in the popup)

constructor(props) {
    super(props);
    this.state = {
      viewport: {
        ...DeckGLOverlay.defaultViewport,
        width: 500,
        height: 500
      },
      data: null,
      coordinates: [-0.13235092163085938,51.518250335096376],
      info: "Hello"
    };

3) Create callback method that sets the state with the new data (info will just be an element from the data, can be anything you want to display in the popup though)

myCallback = (info) => {
    console.log(info);
    if(info){
      this.setState({coordinates: info.lngLat, info: info.object[2]});
    }
  }

4) Render the popup and reference the callback method in the DeckGL layer

 return (
      <MapGL
        {...viewport}
        {...this.props}
        onViewportChange={this._onViewportChange.bind(this)}
        mapboxApiAccessToken={MAPBOX_TOKEN}
        mapStyle='mapbox://styles/mapbox/dark-v9'>
        <Popup
          longitude={this.state.coordinates[0]}
          latitude={this.state.coordinates[1]}>
          <div style={style}>
            <p>{this.state.info}</p>
          </div>
        </Popup>
        <DeckGLOverlay viewport={viewport}
          data={data}
          lowPerformerColor={lowPerformerColor}
          highPerformerColor={highPerformerColor}
          smallRadius={smallRadius}
          largeRadius={largeRadius}
          radius={300}
          callbackFromParent={this.myCallback}
          />
      </MapGL>
    );

and in deckgl-overlay.js:
1) Feed data information into the parent's (app.js) method

onClick: info => this.props.callbackFromParent(info),

(obviously delete the React-tooltip element and onClick event handler in deckoverlay.js to clean up)

like image 90
Naadof Avatar answered Mar 12 '23 19:03

Naadof


For anyone reading this who wants to use a custom popover or one from a third party library like antd that doesn't support exact position as a prop I got around this problem by just creating a <div style={{ position: 'absolute', left: x, top: y}} /> to act as a child node for the popover to reference. X and Y are initially set to 0:

    const [selectedPoint, setSelectedPoint] = useState({});
    const [x, setX] = useState(0);
    const [y, setY] = useState(0);

and then are set onClick in the GeoJsonLayer:

    const onClick = ({ x, y, object }) => {
        setSelectedPoint(object);
        setX(x);
        setY(y);
    };

    const layer = new GeoJsonLayer({
        id: "geojson-layer",
        data,
        pickable: true,
        stroked: false,
        filled: true,
        extruded: true,
        lineWidthScale: 20,
        lineWidthMinPixels: 2,
        getFillColor: [0, 0, 0, 255],
        getRadius: 50,
        getLineWidth: 1,
        getElevation: 30,
        onClick
    });

The downside to this approach is that the popover won't stay with the point if the map is zoomed/panned because X and Y are viewport coordinates vs lat and long.

like image 41
ancr Avatar answered Mar 12 '23 20:03

ancr