Skip to content

Commit

Permalink
Add data filter example to the website (visgl#3348)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress authored Jul 15, 2019
1 parent f357a6d commit 9e40523
Show file tree
Hide file tree
Showing 15 changed files with 542 additions and 161 deletions.
29 changes: 29 additions & 0 deletions examples/website/data-filter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
This is a minimal standalone version of the DataFilterExtension example
on [deck.gl](http://deck.gl) website.

### Usage

Copy the content of this folder to your project.

To see the base map, you need a [Mapbox access token](https://docs.mapbox.com/help/how-mapbox-works/access-tokens/). You can either set an environment variable:

```bash
export MapboxAccessToken=<mapbox_access_token>
```

Or set `MAPBOX_TOKEN` directly in `app.js`.

Other options can be found at [using with Mapbox GL](../../../docs/get-started/using-with-mapbox-gl.md).

```bash
# install dependencies
npm install
# or
yarn
# bundle and serve the app with webpack
npm start
```

### Data format
Sample data is stored in [kepler.gl Example Data](https://raw.githubusercontent.com/uber-web/kepler.gl-data/master/earthquakes/). To use your own data, checkout
the [documentation of DataFilterExtension](../../../docs/extensions/data-filter-extension.md).
181 changes: 181 additions & 0 deletions examples/website/data-filter/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, {Component, Fragment} from 'react';
import {render} from 'react-dom';
import {StaticMap} from 'react-map-gl';
import DeckGL from '@deck.gl/react';
import {ScatterplotLayer} from '@deck.gl/layers';
import {DataFilterExtension} from '@deck.gl/extensions';

import RangeInput from './range-input';

// Set your mapbox token here
const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line

// Source data GeoJSON
const DATA_URL =
'https://raw.githubusercontent.com/uber-web/kepler.gl-data/master/earthquakes/data.csv'; // eslint-disable-line

const INITIAL_VIEW_STATE = {
latitude: 36.5,
longitude: -120,
zoom: 5.5,
pitch: 0,
bearing: 0
};

const MS_PER_DAY = 8.64e7; // milliseconds in a day

const dataFilter = new DataFilterExtension({filterSize: 1});

export class App extends Component {
constructor(props) {
super(props);

const timeRange = this._getTimeRange(props.data);

this.state = {
timeRange,
filterValue: timeRange,
hoveredObject: null
};
this._onHover = this._onHover.bind(this);
this._renderTooltip = this._renderTooltip.bind(this);
}

componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
const timeRange = this._getTimeRange(nextProps.data);
this.setState({timeRange, filterValue: timeRange});
}
}

_getTimeRange(data) {
if (!data) {
return null;
}
return data.reduce(
(range, d) => {
const t = d.timestamp / MS_PER_DAY;
range[0] = Math.min(range[0], t);
range[1] = Math.max(range[1], t);
return range;
},
[Infinity, -Infinity]
);
}

_onHover({x, y, object}) {
this.setState({x, y, hoveredObject: object});
}

_renderLayers() {
const {data} = this.props;
const {filterValue} = this.state;

return [
data &&
new ScatterplotLayer({
id: 'earthquakes',
data,
opacity: 0.8,
radiusScale: 100,
radiusMinPixels: 1,
wrapLongitude: true,

getPosition: d => [d.longitude, d.latitude, -d.depth * 1000],
getRadius: d => Math.pow(2, d.magnitude),
getFillColor: d => {
const r = Math.sqrt(Math.max(d.depth, 0));
return [255 - r * 15, r * 5, r * 10];
},

getFilterValue: d => d.timestamp / MS_PER_DAY, // in days
filterRange: [filterValue[0], filterValue[1]],
filterSoftRange: [
filterValue[0] * 0.9 + filterValue[1] * 0.1,
filterValue[0] * 0.1 + filterValue[1] * 0.9
],
extensions: [dataFilter],

pickable: true,
onHover: this._onHover
})
];
}

_renderTooltip() {
const {x, y, hoveredObject} = this.state;
return (
hoveredObject && (
<div className="tooltip" style={{top: y, left: x}}>
<div>
<b>Time: </b>
<span>{new Date(hoveredObject.timestamp).toUTCString()}</span>
</div>
<div>
<b>Magnitude: </b>
<span>{hoveredObject.magnitude}</span>
</div>
<div>
<b>Depth: </b>
<span>{hoveredObject.depth} km</span>
</div>
</div>
)
);
}

_formatLabel(t) {
const date = new Date(t * MS_PER_DAY);
return `${date.getUTCFullYear()}/${date.getUTCMonth() + 1}`;
}

render() {
const {mapStyle = 'mapbox://styles/mapbox/light-v9'} = this.props;
const {timeRange, filterValue} = this.state;

return (
<Fragment>
<DeckGL
layers={this._renderLayers()}
initialViewState={INITIAL_VIEW_STATE}
controller={true}
>
<StaticMap
reuseMaps
mapStyle={mapStyle}
preventStyleDiffing={true}
mapboxApiAccessToken={MAPBOX_TOKEN}
/>

{this._renderTooltip}
</DeckGL>

{timeRange && (
<RangeInput
min={timeRange[0]}
max={timeRange[1]}
value={filterValue}
formatLabel={this._formatLabel}
onChange={({value}) => this.setState({filterValue: value})}
/>
)}
</Fragment>
);
}
}

export function renderToDOM(container) {
render(<App />, container);
require('d3-request').csv(DATA_URL, (error, response) => {
if (!error) {
const data = response.map(row => ({
timestamp: new Date(`${row.DateTime} UTC`).getTime(),
latitude: Number(row.Latitude),
longitude: Number(row.Longitude),
depth: Number(row.Depth),
magnitude: Number(row.Magnitude)
}));
render(<App data={data} />, container);
}
});
}
19 changes: 19 additions & 0 deletions examples/website/data-filter/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>deck.gl Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {margin: 0; font-family: sans-serif; width: 100vw; height: 100vh; overflow: hidden;}
.tooltip {pointer-events: none; position: absolute; z-index: 9; font-size: 12px; padding: 8px; background: #000; color: #fff; min-width: 160px; max-height: 240px; overflow-y: hidden;}
</style>
</head>
<body>
<div id="app"></div>
</body>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript">
App.renderToDOM(document.getElementById('app'));
</script>
</html>
27 changes: 27 additions & 0 deletions examples/website/data-filter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "geojson",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"start-local": "webpack-dev-server --env.local --progress --hot --open",
"start": "webpack-dev-server --progress --hot --open"
},
"dependencies": {
"baseui": "^8.5.1",
"d3-request": "^1.0.5",
"deck.gl": "^7.2.0-alpha.4",
"react": "^16.3.0",
"react-dom": "^16.3.0",
"react-map-gl": "^5.0.0",
"styletron-engine-atomic": "^1.4.0",
"styletron-react": "^5.2.0"
},
"devDependencies": {
"@babel/core": "^7.4.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.5",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.1"
}
}
110 changes: 110 additions & 0 deletions examples/website/data-filter/range-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* global requestAnimationFrame, cancelAnimationFrame */
import React, {PureComponent} from 'react';
import {Client as Styletron} from 'styletron-engine-atomic';
import {Provider as StyletronProvider} from 'styletron-react';
import {LightTheme, BaseProvider, styled} from 'baseui';
import {Slider} from 'baseui/slider';
import {Button, SHAPE, SIZE} from 'baseui/button';
import Start from 'baseui/icon/chevron-right';
import Stop from 'baseui/icon/delete';

const engine = new Styletron();

const Container = styled('div', {
position: 'absolute',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
bottom: '20px',
width: '100%'
});

const ThumbValue = styled('div', {
position: 'absolute',
top: '-2em'
});

const TickBar = styled('div', {
width: '480px',
height: '24px',
maxWidth: '80vw'
});

const ANIMATION_SPEED = 30;

export default class RangeInput extends PureComponent {
constructor(props) {
super(props);

this.state = {
isPlaying: false
};

this._renderThumbValue = this._renderThumbValue.bind(this);
this._animate = this._animate.bind(this);
this._toggle = this._toggle.bind(this);
this._animationFrame = null;
}

componentWillUnmount() {
cancelAnimationFrame(this._animationFrame);
}

_toggle() {
cancelAnimationFrame(this._animationFrame);
const {isPlaying} = this.state;
if (!isPlaying) {
this._animate();
}
this.setState({isPlaying: !isPlaying});
}

_animate() {
const {min, max, value} = this.props;
const span = value[1] - value[0];
let newValueMin = value[0] + ANIMATION_SPEED;
if (newValueMin + span >= max) {
newValueMin = min;
}
this.props.onChange({
value: [newValueMin, newValueMin + span]
});

this._animationFrame = requestAnimationFrame(this._animate);
}

_renderThumbValue({$thumbIndex, $value}) {
const value = $value[$thumbIndex];
return <ThumbValue>{this.props.formatLabel(value)}</ThumbValue>;
}

render() {
const {value, min, max} = this.props;
const isButtonEnabled = value[0] > min || value[1] < max;

return (
<StyletronProvider value={engine}>
<BaseProvider theme={LightTheme}>
<Container>
<Button
shape={SHAPE.round}
size={SIZE.compact}
disabled={!isButtonEnabled}
onClick={this._toggle}
>
{this.state.isPlaying ? <Stop title="Stop" /> : <Start title="Animate" />}
</Button>
<Slider
{...this.props}
overrides={{
ThumbValue: this._renderThumbValue,
TickBar: () => <TickBar />
}}
/>
</Container>
</BaseProvider>
</StyletronProvider>
);
}
}
Loading

0 comments on commit 9e40523

Please sign in to comment.