UNPKG

kepler.gl

Version:

kepler.gl is a webgl based application to visualize large scale location data in the browser

243 lines (210 loc) 6.79 kB
// Copyright (c) 2018 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import {createSelector} from 'reselect'; import {CHANNEL_SCALES, SCALE_FUNC, ALL_FIELD_TYPES} from 'constants/default-settings'; import {hexToRgb} from 'utils/color-utils'; import {geojsonFromPoints} from '../mapbox-utils'; import MapboxGLLayer from '../mapboxgl-layer'; import HeatmapLayerIcon from './heatmap-layer-icon'; const MAX_ZOOM_LEVEL = 18; export const heatmapVisConfigs = { opacity: 'opacity', colorRange: 'colorRange', radius: 'heatmapRadius' }; /** * * @param {Object} colorRange * @return {Array} [ * 0, "rgba(33,102,172,0)", * 0.2, "rgb(103,169,207)", * 0.4, "rgb(209,229,240)", * 0.6, "rgb(253,219,199)", * 0.8, "rgb(239,138,98)", * 1, "rgb(178,24,43)" * ] */ const heatmapDensity = (colorRange) => { const scaleFunction = SCALE_FUNC.quantize; const scale = scaleFunction() .domain([0, 1]) .range(colorRange.colors); return scale.range().reduce((bands, level) => { const invert = scale.invertExtent(level); return [ ...bands, invert[0], // first value in the range `rgb(${hexToRgb(level).join(',')})` // color ] }, []); }; const shouldRebuild = (sameData, sameConfig) => !(sameData && sameConfig); class HeatmapLayer extends MapboxGLLayer { constructor(props) { super(props); this.registerVisConfig(heatmapVisConfigs); } get type() { return 'heatmap'; } get visualChannels() { return { weight: { property: 'weight', field: 'weightField', scale: 'weightScale', domain: 'weightDomain', key: 'weight', // supportedFieldTypes can be determined by channelScaleType // or specified here defaultMeasure: 'density', supportedFieldTypes: [ALL_FIELD_TYPES.real, ALL_FIELD_TYPES.integer], channelScaleType: CHANNEL_SCALES.size } }; } get layerIcon() { return HeatmapLayerIcon; } getVisualChannelDescription(channel) { return channel === 'color' ? { label: 'color', measure: 'Density' } : { label: 'weight', measure: this.config.weightField ? this.config.weightField.name : 'Density' } } getDefaultLayerConfig(props = {}) { // mapbox heatmap layer color is always based on density // no need to set colorField, colorDomain and colorScale /* eslint-disable no-unused-vars */ const {colorField, colorDomain, colorScale, ...layerConfig} = { ...super.getDefaultLayerConfig(props), weightField: null, weightDomain: [0, 1], weightScale: 'linear' }; /* eslint-enable no-unused-vars */ return layerConfig; } isSameData = ({allData, filteredIndex, oldLayerData, opt = {}}, config) => { return Boolean(oldLayerData && oldLayerData.columns === config.columns && opt.sameData ); }; isSameConfig = ({oldLayerData, config}) => { // columns must use the same filedIdx // this is a fast way to compare columns object const { columns, weightField } = config; if (!oldLayerData) { return false; } const sameColumns = columns === oldLayerData.columns; const sameWeightField = weightField === oldLayerData.weightField; return sameColumns && sameWeightField; }; datasetSelector = config => config.dataId; isVisibleSelector = config => config.isVisible; visConfigSelector = config => config.visConfig; weightFieldSelector = config => config.weightField ? config.weightField.name : null; weightDomainSelector = config => config.weightDomain; computeHeatmapConfiguration = createSelector( this.datasetSelector, this.isVisibleSelector, this.visConfigSelector, this.weightFieldSelector, this.weightDomainSelector, (datasetId, isVisible, visConfig, weightField, weightDomain) => { const layer = { type: 'heatmap', id: this.id, source: datasetId, layout: { visibility: isVisible ? 'visible' : 'none' }, maxzoom: MAX_ZOOM_LEVEL, paint: { 'heatmap-weight': weightField ? [ 'interpolate', ['linear'], ['get', weightField], weightDomain[0], 0, weightDomain[1], 1 ] : 1, 'heatmap-intensity': [ 'interpolate', ['linear'], ['zoom'], 0, 1, MAX_ZOOM_LEVEL, 3 ], 'heatmap-color': [ 'interpolate', ['linear'], ['heatmap-density'], ...heatmapDensity(visConfig.colorRange) ], 'heatmap-radius': [ 'interpolate', ['linear'], ['zoom'], 0, 2, MAX_ZOOM_LEVEL, visConfig.radius // radius ], 'heatmap-opacity': visConfig.opacity } }; return layer; } ); formatLayerData(_, allData, filteredIndex, oldLayerData, opt = {}) { const options = { allData, filteredIndex, oldLayerData, opt, config: this.config }; const {weightField} = this.config; const isSameData = this.isSameData(options, this.config); const isSameConfig = this.isSameConfig(options); const data = !shouldRebuild(isSameData, isSameConfig) ? null : geojsonFromPoints( allData, filteredIndex, this.config.columns, weightField ? [weightField] : [] ); const newConfig = this.computeHeatmapConfiguration(this.config); newConfig.id = this.id; return { columns: this.config.columns, config: newConfig, data, weightField }; } } export default HeatmapLayer;