-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deck.gl Column Layer #613
Comments
you are right, column is only used in the aggregation layers. You can, however, implement a custom layer. The docs is not complete yet. But you can take a look at this PR #540 |
I've been able to throw together a few custom layers if you have questions. Still trying to figure out some of the finer details until the docs are done, but I'm getting to a point where I feel like I know the steps. |
@Harrisandwich I would be interested to know your experience of implementing custom layer. I am not at a point where I am happy with the clarity and ease of use of current layer class methods. I am constantly making changes to it, that's why I hesitate to expose it at the moment |
(Bit of a long comment hahaha hopefully you find all this helpful) It hasn't been too hard to figure out. The first custom layer was the hardest, as it was extending the MapboxLayer class you use for heatmaps. The next few have gone better so far. I'd say my biggest roadblocks creating custom layers are:
Mapbox layer specific
Below are a few blocks used to make custom layers happen in our app In order to get custom layer controls to show I had to create my own import LayerConfigurator, { LayerColorSelector }
from 'kepler.gl/dist/components/side-panel/layer-panel/layer-configurator'
LayerConfigurator.prototype._renderTileLayerConfig = ({
layer,
visConfiguratorProps,
layerConfiguratorProps,
}) => (
// ...component stuff
) Also in order to get kepler to use the layer I added it to the layerClasses store like so: import * as Layers from 'kepler.gl/layers'
import * as CustomLayers from '../common-components/custom-kepler-layers'
Object.values(CustomLayers).forEach((cl) => { Layers.LayerClasses[cl._typeName] = cl })
const customizedKeplerGlReducer = keplerGlReducer
.initialState({
visState: { layerClasses: Layers.LayerClasses },
uiState: { currentModal: null },
}) For your consideration, here is the "TileClass" I made for showing MVT layers: /* eslint-disable no-unused-vars */
/* eslint-disable class-methods-use-this */
import Color from 'color'
import { ALL_FIELD_TYPES } from 'kepler.gl/constants'
import MapboxLayerGL from 'kepler.gl/dist/layers/mapboxgl-layer'
import GeojsonLayerIcon from 'kepler.gl/dist/layers/geojson-layer/geojson-layer-icon'
import { createSelector } from 'reselect'
import { getPolygonLayerFillCondition } from '../../helpers/mapbox-map-utils/dynamic-styling'
import { calculateRange } from '../../helpers/utils'
export const tileVisConfigs = {
opacity: 'opacity',
colorRange: 'colorRange',
type: {
type: 'string',
defaultValue: 'demographic',
},
}
class MapboxTileLayer extends MapboxLayerGL {
constructor(props) {
super(props)
this.registerVisConfig(tileVisConfigs)
}
get type() {
return 'tile'
}
get visualChannels() {
return {}
}
get layerIcon() {
return GeojsonLayerIcon
}
get requiredLayerColumns() {
return ['category', 'style']
}
getVisualChannelDescription(channel) {
return channel === 'color' ? {
label: 'color',
measure: 'Density',
} : {}
}
datasetSelector = config => config.dataId
columnsSelector = config => config.columns
dataSelector = config => config.data
isVisibleSelector = config => config.isVisible
visConfigSelector = config => config.visConfig
colorSelector = config => config.color
computeTileConfiguration = createSelector(
this.datasetSelector,
this.columnsSelector,
this.dataSelector,
this.isVisibleSelector,
this.visConfigSelector,
this.colorSelector,
(
datasetId,
columns,
data,
isVisible,
visConfig,
color,
) => {
// Alot of what is happening here is VERY specific to our app. I'll probably try to create a more generic tile layer type at some point.
const { opacity } = visConfig
const category = JSON.parse(data[columns.category.fieldIdx])
const colorObject = Color(color)
let style = JSON.parse(data[columns.style.fieldIdx])
const range = calculateRange(style.length).reverse()
style = style.map((s, i) => ({
color:
Color(`hsl(${colorObject.hsl().color[0]}, 70%, ${range[i]}%)`)
.rgb()
.alpha(Color(s.color.color).alpha()),
data: s.data,
}))
const mapLayerID = category.table || category.slug
const paint = {
'fill-color': [
'case',
...getPolygonLayerFillCondition(style, mapLayerID),
],
'fill-opacity': [
'case',
// if there's no value, don't show feature (transparent)
['==', ['feature-state', mapLayerID], null],
0,
// else if hover, use 1 for opacity
opacity,
],
}
const layer = {
_sourceType: 'vector',
_categoryIndex: columns.category.fieldIdx,
type: 'fill',
id: this.id,
source: category.resolution,
'source-layer': category.resolution,
layout: { visibility: isVisible ? 'visible' : 'none' },
paint,
}
return layer
},
)
formatLayerData(_, allData, filteredIndex, oldLayerData, opt = {}) {
const config = this.computeTileConfiguration({
...this.config,
data: allData,
})
config.id = this.id
return {
columns: this.config.columns,
config,
data: allData,
}
}
}
MapboxTileLayer._typeName = 'tile' // this is for easily appending new custom layers to the layerClasses store (see above)
export default MapboxTileLayer and my changes to the export default function updateMapboxLayers(
map,
newLayers = [],
oldLayers = null,
mapLayers = null,
opt = { force: false },
/*
IMPORTANT
Having force set to true by default caused some problems, as it would remove layers even if
the config didn't change. Lot's of confusion in trying to figure out why my layers were disappearing
*/
) {
if (newLayers.length > 0 && Object.keys(oldLayers).length === 0) {
// all that interaction stuff I mentioned in #623
document
.querySelector('.maps')
.addEventListener('mousemove', e => onMouseHoverEvent(e, map, 'mousemove'))
document
.querySelector('.maps')
.addEventListener('mouseenter', e => onMouseHoverEvent(e, map, 'mouseenter'))
document
.querySelector('.maps')
.addEventListener('mouseleave', e => onMouseHoverEvent(e, map, 'mouseleave'))
document
.querySelector('.maps')
.addEventListener('mousedown', e => onMapMouseDown(e, map))
document
.querySelector('.maps')
.addEventListener('mouseup', e => onMapMouseUp(e, map))
}
// delete non existing layers
if (oldLayers) {
const oldLayersKeys = Object.keys(oldLayers)
if (newLayers.length === 0 && oldLayersKeys.length > 0) {
oldLayersKeys.forEach(layerId => map.removeLayer(layerId))
} else {
// remove layers
const currentLayersIds = newLayers.reduce((final, layer) => ({
...final,
[layer.id]: true,
}), {})
const layersToDelete = oldLayersKeys.reduce((final, layerId) => {
// if layer doesn't exists anymore
if (!currentLayersIds[layerId]) {
return {
...final,
[layerId]: oldLayers[layerId],
}
}
return final
}, [])
Object.keys(layersToDelete).forEach(layerId => map.removeLayer(layerId))
}
}
// insert or update new layer
// TODO: fix complexity
/* eslint-disable complexity */
newLayers.forEach((overlay) => {
const { id: layerId, config, data, datasetId } = overlay
if (!data && !config) {
return
}
const isAvailableAndVisible =
!(mapLayers && mapLayers[layerId]) || mapLayers[layerId].isVisible
// checking if source already exists
if (data && isAvailableAndVisible) {
if (config.type === 'fill' && config._sourceType === 'vector') {
const category = JSON.parse(data[config._categoryIndex])
const source = map.getSource(category.resolution)
if (!source) {
map.addSource(category.resolution, {
type: 'vector',
tiles: [data[0]],
minzoom: 6,
maxzoom: 14,
})
}
} else {
const source = map.getSource(datasetId)
if (!source) {
map.addSource(datasetId, {
type: 'geojson',
data,
})
} else {
source.setData(data)
}
}
}
const oldConfig = oldLayers[layerId]
const mapboxLayer = map.getLayer(layerId)
// compare with previous configs
if (!oldConfig || oldConfig !== config || !mapboxLayer || opt.force) {
// check if layer already is set
// remove it if exists
if (mapboxLayer) {
map.removeLayer(layerId)
}
// add if visible and available
if (isAvailableAndVisible) {
map.addLayer(config)
}
}
})
/* eslint-enable complexity */
} |
@Harrisandwich we're struggling with pretty much the same task in our project, trying to render MVTLayer providing the PBF url to it on top of our Kepler app. It was always easy and straightforward with DeckGL itself: So I was happy to see your PoC, but could you please provide a bit more details / code example of how exactly to use your MapboxTileLayer? We're close to do the same workaround, but it's still not working properly, so maybe after all these years you have some more to share :) Thanks in advance! |
Hello @punov. Unfortunately, I don't work at the company I was at when I made this comment, so I don't have access to the code. It was also quite a while ago, so I can't really remember what exactly I did to make this work. If I remember correctly, the MVTLayer is based off the Kepler heatmap layer (which uses mapbox) to render. You should be able to implement it in a similar way, though getting interaction to work might be tough. Also I mention in a comment in the code blocks above that some of the logic in the layer itself is very specific to what I was working on at the time, so it may need to be adjusted to work for you |
Having access to the deck.gl column layer would nice.
In some cases you don't want to use point aggregation in a specific area but just visualize a column at a certain coordinate with the height based on a certain value.
Afaik this isn't available at the moment?
The text was updated successfully, but these errors were encountered: