@carto/kepler.gl 0.0.46 → 0.0.47-alpha.custom-markers.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/index.js +10 -1
- package/dist/components/kepler-gl.js +2 -1
- package/dist/components/side-panel/layer-panel/custom-markers-config-group.js +40 -0
- package/dist/components/side-panel/layer-panel/layer-configurator.js +14 -6
- package/dist/constants/default-settings.js +6 -5
- package/dist/layers/base-layer.js +79 -4
- package/dist/layers/layer-factory.js +23 -1
- package/dist/layers/point-layer/point-layer.js +101 -22
- package/dist/localization/en.js +8 -3
- package/dist/utils/layer-utils.js +5 -3
- package/package.json +8 -5
- package/src/components/index.js +1 -0
- package/src/components/kepler-gl.js +1 -0
- package/src/components/side-panel/layer-panel/custom-markers-config-group.js +30 -0
- package/src/components/side-panel/layer-panel/layer-configurator.js +19 -5
- package/src/constants/default-settings.js +7 -3
- package/src/layers/base-layer.js +62 -1
- package/src/layers/layer-factory.js +22 -0
- package/src/layers/point-layer/point-layer.js +121 -26
- package/src/localization/en.js +7 -2
- package/src/utils/layer-utils.js +4 -2
- package/umd/keplergl.min.js +0 -508
package/src/layers/base-layer.js
CHANGED
|
@@ -56,6 +56,7 @@ import {getSampleData} from 'utils/table-utils/data-container-utils';
|
|
|
56
56
|
|
|
57
57
|
import {hexToRgb, getColorGroupByName, reverseColorRange} from 'utils/color-utils';
|
|
58
58
|
import {getMaskExtensionProps} from 'utils/layer-utils';
|
|
59
|
+
import {scaleOrdinal} from 'd3-scale';
|
|
59
60
|
|
|
60
61
|
/** @typedef {import('./index').Layer} LayerClass} */
|
|
61
62
|
|
|
@@ -65,6 +66,10 @@ import {getMaskExtensionProps} from 'utils/layer-utils';
|
|
|
65
66
|
*/
|
|
66
67
|
export const LAYER_ID_LENGTH = 6;
|
|
67
68
|
|
|
69
|
+
// Note, this is is SVG of solid circle encoded as data-uri
|
|
70
|
+
const FALLBACK_ICON =
|
|
71
|
+
'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNTAiLz4NCjwvc3ZnPg==';
|
|
72
|
+
|
|
68
73
|
const MAX_SAMPLE_SIZE = 5000;
|
|
69
74
|
const defaultDomain = [0, 1];
|
|
70
75
|
const dataFilterExtension = new DataFilterExtension({filterSize: MAX_GPU_FILTERS});
|
|
@@ -808,6 +813,30 @@ class Layer {
|
|
|
808
813
|
return this.getVisChannelScale(colorScale, colorDomain, colorRange.colors.map(hexToRgb));
|
|
809
814
|
}
|
|
810
815
|
|
|
816
|
+
getCustomMarkersScale(range) {
|
|
817
|
+
let unknownValue = FALLBACK_ICON;
|
|
818
|
+
if (!range) {
|
|
819
|
+
() => unknownValue;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const mapping = new Map();
|
|
823
|
+
|
|
824
|
+
for (const {value, markerUrl} of range) {
|
|
825
|
+
if (value === null) {
|
|
826
|
+
unknownValue = markerUrl;
|
|
827
|
+
} else if (markerUrl) {
|
|
828
|
+
mapping.set(value, markerUrl);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// @ts-ignore d3 scale
|
|
833
|
+
const scale = scaleOrdinal()
|
|
834
|
+
.domain(Array.from(mapping.keys()))
|
|
835
|
+
.range(Array.from(mapping.values()))
|
|
836
|
+
.unknown(unknownValue);
|
|
837
|
+
return scale;
|
|
838
|
+
}
|
|
839
|
+
|
|
811
840
|
/**
|
|
812
841
|
* Mapping from visual channels to deck.gl accesors
|
|
813
842
|
* @param {Object} param Parameters
|
|
@@ -833,7 +862,7 @@ class Layer {
|
|
|
833
862
|
aggregation
|
|
834
863
|
} = this.visualChannels[channel];
|
|
835
864
|
|
|
836
|
-
const shouldGetScale = this.config[field];
|
|
865
|
+
const shouldGetScale = Boolean(scale && this.config[field]);
|
|
837
866
|
|
|
838
867
|
if (shouldGetScale) {
|
|
839
868
|
const args = [this.config[scale], this.config[domain], this.config.visConfig[range]];
|
|
@@ -846,6 +875,8 @@ class Layer {
|
|
|
846
875
|
const scaleFunction =
|
|
847
876
|
channelScaleType === CHANNEL_SCALES.color
|
|
848
877
|
? this.getColorScale(...args)
|
|
878
|
+
: channelScaleType === CHANNEL_SCALES.customMarker
|
|
879
|
+
? this.getCustomMarkersScale(this.config.visConfig[range])
|
|
849
880
|
: this.getVisChannelScale(...args, isFixed);
|
|
850
881
|
|
|
851
882
|
attributeAccessors[accessor] = d =>
|
|
@@ -1191,6 +1222,12 @@ class Layer {
|
|
|
1191
1222
|
return fixed ? 1 : (this.config[field] ? 1 : radius);
|
|
1192
1223
|
}
|
|
1193
1224
|
|
|
1225
|
+
getMaxMarkerSize() {
|
|
1226
|
+
const {visConfig} = this.config;
|
|
1227
|
+
const {sizeField, radiusRange, radius} = visConfig;
|
|
1228
|
+
return Math.ceil((sizeField ? radiusRange[1] : radius));
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1194
1231
|
getRadiusUnit(fixedRadius) {
|
|
1195
1232
|
const radiusChannel = Object.values(this.visualChannels).find(vc => vc.property === 'radius');
|
|
1196
1233
|
|
|
@@ -1300,6 +1337,30 @@ class Layer {
|
|
|
1300
1337
|
}, []);
|
|
1301
1338
|
}
|
|
1302
1339
|
|
|
1340
|
+
/**
|
|
1341
|
+
* Create deck.gl `IconLayer` "autoPack" mode accessor for custom markers.
|
|
1342
|
+
*
|
|
1343
|
+
* @param {*} customMarkersStatus Record<url, UrlStatus> provided by builder, UrlStatus `ok` means URL is ok and ready to render
|
|
1344
|
+
* @param {*} iconUrlAccessor deck.gl accessor (or value) that resolves to URL of icon for given row
|
|
1345
|
+
* @returns deck.gl accessor that returns UnpackedIcon ready to consume by IconLayer
|
|
1346
|
+
*/
|
|
1347
|
+
getCustomMarkerIconAccessor(customMarkersStatus, iconUrlAccessor) {
|
|
1348
|
+
const maxIconSize = this.getMaxMarkerSize();
|
|
1349
|
+
|
|
1350
|
+
return d => {
|
|
1351
|
+
const wantedUrl =
|
|
1352
|
+
(typeof iconUrlAccessor === 'function') ? iconUrlAccessor(d) : iconUrlAccessor;
|
|
1353
|
+
const url = customMarkersStatus?.[wantedUrl] === 'ok' ? wantedUrl : FALLBACK_ICON;
|
|
1354
|
+
return {
|
|
1355
|
+
id: `${url}@@${maxIconSize}`,
|
|
1356
|
+
url,
|
|
1357
|
+
width: maxIconSize,
|
|
1358
|
+
height: maxIconSize,
|
|
1359
|
+
mask: true
|
|
1360
|
+
};
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1303
1364
|
calculateDataAttribute(keplerTable, getPosition) {
|
|
1304
1365
|
// implemented in subclasses
|
|
1305
1366
|
return [];
|
|
@@ -26,6 +26,7 @@ import {DEFAULT_COLOR_RANGE} from 'constants/color-ranges';
|
|
|
26
26
|
export const PROPERTY_GROUPS = keyMirror({
|
|
27
27
|
color: null,
|
|
28
28
|
stroke: null,
|
|
29
|
+
customMarkers: null,
|
|
29
30
|
radius: null,
|
|
30
31
|
height: null,
|
|
31
32
|
angle: null,
|
|
@@ -429,6 +430,27 @@ export const LAYER_VIS_CONFIGS = {
|
|
|
429
430
|
step: 0.1,
|
|
430
431
|
group: PROPERTY_GROUPS.cell,
|
|
431
432
|
property: 'radius'
|
|
433
|
+
},
|
|
434
|
+
customMarkers: {
|
|
435
|
+
type: 'boolean',
|
|
436
|
+
defaultValue: false,
|
|
437
|
+
label: 'layerVisConfigs.customMarkers',
|
|
438
|
+
group: PROPERTY_GROUPS.customMarkers,
|
|
439
|
+
property: 'customMarkers'
|
|
440
|
+
},
|
|
441
|
+
customMarkersUrl: {
|
|
442
|
+
type: 'text',
|
|
443
|
+
defaultValue: '',
|
|
444
|
+
label: 'layerVisConfigs.addUrl',
|
|
445
|
+
group: PROPERTY_GROUPS.customMarkers,
|
|
446
|
+
property: 'customMarkersUrl'
|
|
447
|
+
},
|
|
448
|
+
customMarkersRange: {
|
|
449
|
+
type: 'markers-range-select',
|
|
450
|
+
defaultValue: undefined,
|
|
451
|
+
label: 'layerVisConfigs.colorRange',
|
|
452
|
+
group: PROPERTY_GROUPS.customMarkers,
|
|
453
|
+
property: 'customMarkersRange'
|
|
432
454
|
}
|
|
433
455
|
};
|
|
434
456
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import {BrushingExtension} from '@deck.gl/extensions';
|
|
22
22
|
import {ScatterplotLayer} from '@deck.gl/layers';
|
|
23
|
+
import {IconLayer} from '@deck.gl/layers';
|
|
23
24
|
|
|
24
25
|
import Layer from '../base-layer';
|
|
25
26
|
import {hexToRgb} from 'utils/color-utils';
|
|
@@ -43,6 +44,9 @@ const brushingExtension = new BrushingExtension();
|
|
|
43
44
|
export const pointVisConfigs = {
|
|
44
45
|
radius: 'radius',
|
|
45
46
|
fixedRadius: 'fixedRadius',
|
|
47
|
+
customMarkers: 'customMarkers',
|
|
48
|
+
customMarkersUrl: 'customMarkersUrl',
|
|
49
|
+
customMarkersRange: 'customMarkersRange',
|
|
46
50
|
opacity: 'opacity',
|
|
47
51
|
outline: 'outline',
|
|
48
52
|
thickness: 'thickness',
|
|
@@ -122,6 +126,18 @@ export default class PointLayer extends Layer {
|
|
|
122
126
|
channelScaleType: 'radius',
|
|
123
127
|
accessor: 'getRadius',
|
|
124
128
|
defaultValue: 1
|
|
129
|
+
},
|
|
130
|
+
customMarkers: {
|
|
131
|
+
property: 'customMarkers',
|
|
132
|
+
key: 'customMarkers',
|
|
133
|
+
field: 'customMarkersField',
|
|
134
|
+
scale: 'customMarkersScale',
|
|
135
|
+
domain: 'customMarkersDomain',
|
|
136
|
+
range: 'customMarkersRange',
|
|
137
|
+
channelScaleType: CHANNEL_SCALES.customMarker,
|
|
138
|
+
accessor: 'getIconUrl',
|
|
139
|
+
supportedFieldTypes: ['string'],
|
|
140
|
+
getAttributeValue: config => config.visConfig.customMarkersUrl
|
|
125
141
|
}
|
|
126
142
|
};
|
|
127
143
|
}
|
|
@@ -181,7 +197,10 @@ export default class PointLayer extends Layer {
|
|
|
181
197
|
// add stroke color visual channel
|
|
182
198
|
strokeColorField: null,
|
|
183
199
|
strokeColorDomain: [0, 1],
|
|
184
|
-
strokeColorScale: 'quantile'
|
|
200
|
+
strokeColorScale: 'quantile',
|
|
201
|
+
|
|
202
|
+
customMarkersField: null,
|
|
203
|
+
customMarkersScale: 'ordinal'
|
|
185
204
|
};
|
|
186
205
|
}
|
|
187
206
|
|
|
@@ -237,22 +256,21 @@ export default class PointLayer extends Layer {
|
|
|
237
256
|
this.updateMeta({bounds});
|
|
238
257
|
}
|
|
239
258
|
|
|
259
|
+
isLayerHovered(objectInfo) {
|
|
260
|
+
return (
|
|
261
|
+
(objectInfo?.picked && objectInfo?.layer?.props?.id === this.id) ||
|
|
262
|
+
objectInfo?.layer?.props?.id === `${this.id}-icon`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
240
266
|
renderLayer(opts) {
|
|
241
|
-
const {data, gpuFilter, objectHovered, mapState, interactionConfig} = opts;
|
|
267
|
+
const {data, gpuFilter, objectHovered, mapState, interactionConfig, customMarkersStatus} = opts;
|
|
242
268
|
|
|
243
269
|
// if no field size is defined we need to pass fixed radius = false
|
|
244
270
|
const fixedRadius = this.config.visConfig.fixedRadius && Boolean(this.config.sizeField);
|
|
245
271
|
const radiusScale = this.getRadiusScale(fixedRadius);
|
|
246
272
|
const radiusUnits = this.getRadiusUnit(fixedRadius);
|
|
247
273
|
|
|
248
|
-
const layerProps = {
|
|
249
|
-
stroked: this.config.visConfig.outline,
|
|
250
|
-
filled: this.config.visConfig.filled,
|
|
251
|
-
lineWidthScale: this.config.visConfig.thickness,
|
|
252
|
-
radiusScale,
|
|
253
|
-
...(this.config.visConfig.fixedRadius ? {} : {radiusMaxPixels: 500})
|
|
254
|
-
};
|
|
255
|
-
|
|
256
274
|
const updateTriggers = {
|
|
257
275
|
getPosition: this.config.columns,
|
|
258
276
|
getFilterValue: gpuFilter.filterValueUpdateTriggers,
|
|
@@ -278,16 +296,101 @@ export default class PointLayer extends Layer {
|
|
|
278
296
|
};
|
|
279
297
|
const hoveredObject = this.hasHoveredObject(objectHovered);
|
|
280
298
|
|
|
299
|
+
const textLabelLayer = this.renderTextLabelLayer(
|
|
300
|
+
{
|
|
301
|
+
getPosition: data.getPosition,
|
|
302
|
+
sharedProps,
|
|
303
|
+
getPixelOffset,
|
|
304
|
+
updateTriggers
|
|
305
|
+
},
|
|
306
|
+
opts
|
|
307
|
+
);
|
|
308
|
+
const commonLayerProps = {
|
|
309
|
+
...defaultLayerProps,
|
|
310
|
+
parameters: {
|
|
311
|
+
// circles will be flat on the map when the altitude column is not used
|
|
312
|
+
depthTest: this.config.columns.altitude?.fieldIdx > -1
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const customMarkersField = this.config.customMarkersField;
|
|
317
|
+
const {customMarkers, customMarkersUrl, customMarkersRange} = this.config.visConfig;
|
|
318
|
+
const customMarkersConfigured =
|
|
319
|
+
customMarkers && Boolean(customMarkersUrl || (customMarkersField && customMarkersRange));
|
|
320
|
+
const customMarkersMisConfigured = customMarkers && !customMarkersConfigured;
|
|
321
|
+
|
|
322
|
+
if (customMarkersConfigured) {
|
|
323
|
+
const maxIconSize = this.getMaxMarkerSize();
|
|
324
|
+
const getIcon = this.getCustomMarkerIconAccessor(customMarkersStatus, data.getIconUrl);
|
|
325
|
+
const iconLayerProps = {
|
|
326
|
+
getIcon,
|
|
327
|
+
getPosition: data.getPosition,
|
|
328
|
+
getSize: data.getRadius,
|
|
329
|
+
sizeScale: radiusScale,
|
|
330
|
+
sizeUnits: 'pixels',
|
|
331
|
+
loadOptions: {
|
|
332
|
+
image: {
|
|
333
|
+
type: 'imagebitmap'
|
|
334
|
+
},
|
|
335
|
+
imagebitmap: {
|
|
336
|
+
resizeWidth: maxIconSize,
|
|
337
|
+
resizeHeight: maxIconSize,
|
|
338
|
+
resizeQuality: 'high'
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return [
|
|
344
|
+
new IconLayer({
|
|
345
|
+
...commonLayerProps,
|
|
346
|
+
...iconLayerProps,
|
|
347
|
+
id: `${defaultLayerProps.id}-icon`,
|
|
348
|
+
data: data.data,
|
|
349
|
+
getColor: data.getFillColor,
|
|
350
|
+
getFilterValue: data.getFilterValue,
|
|
351
|
+
updateTriggers: {
|
|
352
|
+
getFilterValue: updateTriggers.getFilterValue,
|
|
353
|
+
getColor: updateTriggers.getFillColor,
|
|
354
|
+
getIcon: [
|
|
355
|
+
customMarkersUrl,
|
|
356
|
+
maxIconSize,
|
|
357
|
+
customMarkersStatus,
|
|
358
|
+
customMarkersField,
|
|
359
|
+
customMarkersRange
|
|
360
|
+
],
|
|
361
|
+
getSize: updateTriggers.getRadius
|
|
362
|
+
}
|
|
363
|
+
}),
|
|
364
|
+
...(hoveredObject
|
|
365
|
+
? [
|
|
366
|
+
new IconLayer({
|
|
367
|
+
...iconLayerProps,
|
|
368
|
+
...this.getDefaultHoverLayerProps(),
|
|
369
|
+
id: `${defaultLayerProps.id}-icon-hovered`,
|
|
370
|
+
data: [hoveredObject],
|
|
371
|
+
getColor: this.config.highlightColor
|
|
372
|
+
})
|
|
373
|
+
]
|
|
374
|
+
: []),
|
|
375
|
+
...textLabelLayer
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const scatterPlotLayerProps = {
|
|
380
|
+
stroked: this.config.visConfig.outline,
|
|
381
|
+
filled: this.config.visConfig.filled,
|
|
382
|
+
lineWidthScale: this.config.visConfig.thickness,
|
|
383
|
+
// TODO: invent better way to render something when customMarkers are not yet configured
|
|
384
|
+
radiusScale: customMarkersMisConfigured ? radiusScale / 2 : radiusScale,
|
|
385
|
+
...(this.config.visConfig.fixedRadius ? {} : {radiusMaxPixels: 500})
|
|
386
|
+
};
|
|
387
|
+
|
|
281
388
|
return [
|
|
282
389
|
new ScatterplotLayer({
|
|
283
|
-
...defaultLayerProps,
|
|
284
390
|
...brushingProps,
|
|
285
|
-
...layerProps,
|
|
286
391
|
...data,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
depthTest: this.config.columns.altitude?.fieldIdx > -1
|
|
290
|
-
},
|
|
392
|
+
...commonLayerProps,
|
|
393
|
+
...scatterPlotLayerProps,
|
|
291
394
|
radiusUnits,
|
|
292
395
|
lineWidthUnits: 'pixels',
|
|
293
396
|
updateTriggers,
|
|
@@ -298,7 +401,7 @@ export default class PointLayer extends Layer {
|
|
|
298
401
|
? [
|
|
299
402
|
new ScatterplotLayer({
|
|
300
403
|
...this.getDefaultHoverLayerProps(),
|
|
301
|
-
...
|
|
404
|
+
...scatterPlotLayerProps,
|
|
302
405
|
data: [hoveredObject],
|
|
303
406
|
getLineColor: this.config.highlightColor,
|
|
304
407
|
getFillColor: this.config.highlightColor,
|
|
@@ -308,15 +411,7 @@ export default class PointLayer extends Layer {
|
|
|
308
411
|
]
|
|
309
412
|
: []),
|
|
310
413
|
// text label layer
|
|
311
|
-
...
|
|
312
|
-
{
|
|
313
|
-
getPosition: data.getPosition,
|
|
314
|
-
sharedProps,
|
|
315
|
-
getPixelOffset,
|
|
316
|
-
updateTriggers
|
|
317
|
-
},
|
|
318
|
-
opts
|
|
319
|
-
)
|
|
414
|
+
...textLabelLayer
|
|
320
415
|
];
|
|
321
416
|
}
|
|
322
417
|
}
|
package/src/localization/en.js
CHANGED
|
@@ -34,7 +34,8 @@ export default {
|
|
|
34
34
|
density: 'density',
|
|
35
35
|
height: 'height',
|
|
36
36
|
sum: 'sum',
|
|
37
|
-
pointCount: 'Point Count'
|
|
37
|
+
pointCount: 'Point Count',
|
|
38
|
+
customMarkers: 'Markers'
|
|
38
39
|
},
|
|
39
40
|
placeholder: {
|
|
40
41
|
search: 'Search',
|
|
@@ -85,6 +86,7 @@ export default {
|
|
|
85
86
|
layer: {
|
|
86
87
|
required: 'Required*',
|
|
87
88
|
radius: 'Radius',
|
|
89
|
+
size: 'Size',
|
|
88
90
|
color: 'Color',
|
|
89
91
|
fillColor: 'Fill Color',
|
|
90
92
|
outline: 'Outline',
|
|
@@ -101,6 +103,7 @@ export default {
|
|
|
101
103
|
elevationByDescription: 'When off, height is based on count of points',
|
|
102
104
|
colorByDescription: 'When off, color is based on count of points',
|
|
103
105
|
aggregateBy: 'Aggregate {field} by',
|
|
106
|
+
addUrl: 'Add URL',
|
|
104
107
|
'3DModel': '3D Model',
|
|
105
108
|
'3DModelOptions': '3D Model Options',
|
|
106
109
|
type: {
|
|
@@ -161,7 +164,9 @@ export default {
|
|
|
161
164
|
weightIntensity: 'Weight Intensity',
|
|
162
165
|
zoomScale: 'Zoom Scale',
|
|
163
166
|
heightRange: 'Height Range',
|
|
164
|
-
heightMultiplier: 'Height Multiplier'
|
|
167
|
+
heightMultiplier: 'Height Multiplier',
|
|
168
|
+
customMarkers: 'Custom markers',
|
|
169
|
+
url: 'Add URL'
|
|
165
170
|
},
|
|
166
171
|
layerVisibility: {
|
|
167
172
|
title: 'Visibility by zoom level'
|
package/src/utils/layer-utils.js
CHANGED
|
@@ -126,7 +126,8 @@ export function renderDeckGlLayer(props, layerCallbacks, idx) {
|
|
|
126
126
|
interactionConfig,
|
|
127
127
|
animationConfig,
|
|
128
128
|
mapLayers,
|
|
129
|
-
maskPolygon
|
|
129
|
+
maskPolygon,
|
|
130
|
+
customMarkersStatus
|
|
130
131
|
} = props;
|
|
131
132
|
const layer = layers[idx];
|
|
132
133
|
const data = layerData[idx];
|
|
@@ -144,7 +145,8 @@ export function renderDeckGlLayer(props, layerCallbacks, idx) {
|
|
|
144
145
|
animationConfig,
|
|
145
146
|
objectHovered,
|
|
146
147
|
visible,
|
|
147
|
-
maskPolygon
|
|
148
|
+
maskPolygon,
|
|
149
|
+
customMarkersStatus
|
|
148
150
|
});
|
|
149
151
|
}
|
|
150
152
|
|