@carto/kepler.gl 0.0.47 → 0.0.48-alpha.custom-markers.3

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.
Files changed (31) hide show
  1. package/dist/components/common/field-selector.js +7 -3
  2. package/dist/components/common/item-selector/item-selector.js +6 -1
  3. package/dist/components/common/styled-components.js +2 -2
  4. package/dist/components/index.js +10 -1
  5. package/dist/components/kepler-gl.js +2 -1
  6. package/dist/components/side-panel/layer-panel/custom-markers-config-group.js +40 -0
  7. package/dist/components/side-panel/layer-panel/layer-configurator.js +16 -8
  8. package/dist/components/side-panel/layer-panel/vis-config-by-field-selector.js +19 -3
  9. package/dist/constants/default-settings.js +6 -5
  10. package/dist/layers/base-layer.js +111 -5
  11. package/dist/layers/layer-factory.js +23 -1
  12. package/dist/layers/point-layer/point-layer.js +123 -22
  13. package/dist/localization/en.js +9 -3
  14. package/dist/utils/layer-utils.js +5 -3
  15. package/dist/utils/table-utils/kepler-table.js +2 -3
  16. package/package.json +7 -4
  17. package/src/components/common/field-selector.js +12 -6
  18. package/src/components/common/item-selector/item-selector.js +4 -0
  19. package/src/components/common/styled-components.js +1 -1
  20. package/src/components/index.js +1 -0
  21. package/src/components/kepler-gl.js +1 -0
  22. package/src/components/side-panel/layer-panel/custom-markers-config-group.js +30 -0
  23. package/src/components/side-panel/layer-panel/layer-configurator.js +27 -10
  24. package/src/components/side-panel/layer-panel/vis-config-by-field-selector.js +11 -1
  25. package/src/constants/default-settings.js +7 -3
  26. package/src/layers/base-layer.js +80 -2
  27. package/src/layers/layer-factory.js +22 -0
  28. package/src/layers/point-layer/point-layer.js +132 -27
  29. package/src/localization/en.js +8 -2
  30. package/src/utils/layer-utils.js +4 -2
  31. package/src/utils/table-utils/kepler-table.js +1 -1
@@ -85,7 +85,7 @@ export const SBFlexboxNoMargin = styled.div`
85
85
  export const PanelLabel = styled.label.attrs({
86
86
  className: 'side-panel-panel__label'
87
87
  })`
88
- color: ${props => props.theme.labelColor};
88
+ color: ${props => (props.active ? props.theme.labelColorActive : props.theme.labelColor)};
89
89
  display: inline-block;
90
90
  font-size: 11px;
91
91
  font-weight: 400;
@@ -73,6 +73,7 @@ export {
73
73
  export {default as LayerPanelFactory} from './side-panel/layer-panel/layer-panel';
74
74
  export {default as LayerPanelHeaderFactory} from './side-panel/layer-panel/layer-panel-header';
75
75
  export {default as LayerConfiguratorFactory} from './side-panel/layer-panel/layer-configurator';
76
+ export {default as CustomMarkersConfigGroupFactory} from './side-panel/layer-panel/custom-markers-config-group';
76
77
  export {default as TextLabelPanelFactory} from './side-panel/layer-panel/text-label-panel';
77
78
  export {LayerConfigGroupLabelFactory} from './side-panel/layer-panel/layer-config-group';
78
79
  export {default as LayerVisibilityByZoomFactory} from './side-panel/layer-panel/layer-visibility-by-zoom';
@@ -130,6 +130,7 @@ export const mapFieldsSelector = props => ({
130
130
  clicked: props.visState.clicked,
131
131
  mousePos: props.visState.mousePos,
132
132
  animationConfig: props.visState.animationConfig,
133
+ customMarkersStatus: props.visState.customMarkersStatus,
133
134
 
134
135
  // uiState
135
136
  activeSidePanel: props.uiState.activeSidePanel,
@@ -0,0 +1,30 @@
1
+ // Copyright (c) 2022 Uber Technologies, Inc.
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in
11
+ // all copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ // THE SOFTWARE.
20
+
21
+ import React from 'react';
22
+
23
+ export default function CustomMarkersConfigGroupFactory() {
24
+ const CustomMarkersConfigGroup = () => {
25
+ return <div>To be overriden</div>;
26
+ };
27
+ return CustomMarkersConfigGroup;
28
+ }
29
+
30
+ CustomMarkersConfigGroupFactory.deps = [];
@@ -36,6 +36,7 @@ import SourceDataSelectorFactory from 'components/side-panel/common/source-data-
36
36
  import VisConfigSwitchFactory from './vis-config-switch';
37
37
  import VisConfigSliderFactory from './vis-config-slider';
38
38
  import LayerConfigGroupFactory, {ConfigGroupCollapsibleContent} from './layer-config-group';
39
+ import CustomMarkersConfigGroupFactory from './custom-markers-config-group';
39
40
  import TextLabelPanelFactory from './text-label-panel';
40
41
 
41
42
  import {capitalizeFirstLetter} from 'utils/utils';
@@ -102,7 +103,8 @@ LayerConfiguratorFactory.deps = [
102
103
  LayerVisibilityByZoomFactory,
103
104
  LayerColorSelectorFactory,
104
105
  LayerColorRangeSelectorFactory,
105
- ArcLayerColorSelectorFactory
106
+ ArcLayerColorSelectorFactory,
107
+ CustomMarkersConfigGroupFactory
106
108
  ];
107
109
 
108
110
  export default function LayerConfiguratorFactory(
@@ -118,7 +120,8 @@ export default function LayerConfiguratorFactory(
118
120
  LayerVisibilityByZoom,
119
121
  LayerColorSelector,
120
122
  LayerColorRangeSelector,
121
- ArcLayerColorSelector
123
+ ArcLayerColorSelector,
124
+ CustomMarkersConfigGroup
122
125
  ) {
123
126
  class LayerConfigurator extends Component {
124
127
  static propTypes = {
@@ -199,8 +202,19 @@ export default function LayerConfiguratorFactory(
199
202
  </LayerConfigGroup>
200
203
  ) : null}
201
204
 
205
+ {layer.type === LAYER_TYPES.point ? (
206
+ <CustomMarkersConfigGroup
207
+ layer={layer}
208
+ visConfiguratorProps={visConfiguratorProps}
209
+ layerChannelConfigProps={layerChannelConfigProps}
210
+ />
211
+ ) : null}
212
+
202
213
  {/* Radius */}
203
- <LayerConfigGroup label={'layer.radius'} collapsible>
214
+ <LayerConfigGroup
215
+ label={layer.config.visConfig.customMarkers ? 'layer.size' : 'layer.radius'}
216
+ collapsible
217
+ >
204
218
  {!layer.config.sizeField ? (
205
219
  <VisConfigSlider
206
220
  {...layer.visConfigSettings.radius}
@@ -225,6 +239,7 @@ export default function LayerConfiguratorFactory(
225
239
  <VisConfigSwitch
226
240
  {...layer.visConfigSettings.fixedRadius}
227
241
  {...visConfiguratorProps}
242
+ disabled={layer.config.visConfig.customMarkers}
228
243
  />
229
244
  ) : null}
230
245
  </ConfigGroupCollapsibleContent>
@@ -1031,7 +1046,6 @@ export function LayerColorSelectorFactory(ColorSelector) {
1031
1046
  class LayerColorSelector extends Component {
1032
1047
  render() {
1033
1048
  const {layer, onChange, selectedColor, property = 'color', setColorUI} = this.props;
1034
-
1035
1049
  return (
1036
1050
  <SidePanelSection>
1037
1051
  <PanelLabel>
@@ -1060,7 +1074,6 @@ export function ArcLayerColorSelectorFactory(ColorSelector) {
1060
1074
  class ArcLayerColorSelector extends Component {
1061
1075
  render() {
1062
1076
  const {layer, onChangeConfig, onChangeVisConfig, property = 'color', setColorUI} = this.props;
1063
-
1064
1077
  return (
1065
1078
  <SidePanelSection>
1066
1079
  <PanelLabel>
@@ -1153,14 +1166,17 @@ export function ChannelByValueSelectorFactory(VisConfigByFieldSelector) {
1153
1166
  const scaleOptions = layer.getScaleOptions(channel.key);
1154
1167
  const showScaleCustomizable =
1155
1168
  channelScaleType === CHANNEL_SCALES.color &&
1169
+ scale &&
1156
1170
  layer.config[scale] &&
1157
1171
  Boolean(layer.config[field]) &&
1158
- scaleOptions.length > 0;
1159
- const showScale =
1172
+ scaleOptions?.length > 0;
1173
+ const showScale = Boolean(
1160
1174
  !showScaleCustomizable &&
1161
- !layer.isAggregated &&
1162
- layer.config[scale] &&
1163
- scaleOptions.length > 1;
1175
+ scale &&
1176
+ !layer.isAggregated &&
1177
+ layer.config[scale] &&
1178
+ scaleOptions?.length > 1
1179
+ );
1164
1180
  const defaultDescription = 'layerConfiguration.defaultDescription';
1165
1181
  const visRange = layer.config.visConfig[range];
1166
1182
  const aggregator = layer.config.visConfig[aggregation];
@@ -1290,4 +1306,5 @@ export const AggregationTypeSelector = ({layer, channel, onChange}) => {
1290
1306
  </SidePanelSection>
1291
1307
  );
1292
1308
  };
1309
+
1293
1310
  /* eslint-enable max-params */
@@ -62,6 +62,14 @@ function VisConfigByFieldSelectorFactory(InfoHelper, FieldSelector, ColorDimensi
62
62
  this.props.updateField(val);
63
63
  };
64
64
 
65
+ _onFieldSelectorFocused = () => {
66
+ this.setState({fieldSelectorActive: true});
67
+ };
68
+
69
+ _onFieldSelectorBlur = () => {
70
+ this.setState({fieldSelectorActive: false});
71
+ };
72
+
65
73
  render() {
66
74
  const {
67
75
  property,
@@ -80,7 +88,7 @@ function VisConfigByFieldSelectorFactory(InfoHelper, FieldSelector, ColorDimensi
80
88
  <SidePanelSection>
81
89
  <SidePanelSection>
82
90
  <PanelLabelWrapper>
83
- <PanelLabel>
91
+ <PanelLabel active={this.state?.fieldSelectorActive}>
84
92
  {(label && <FormattedMessage id={label} />) || (
85
93
  <FormattedMessage
86
94
  id="layer.propertyBasedOn"
@@ -110,6 +118,8 @@ function VisConfigByFieldSelectorFactory(InfoHelper, FieldSelector, ColorDimensi
110
118
  isLoading={this.props.isLoading}
111
119
  withAggregator={this.props.fieldsAggregated}
112
120
  aggregator={this.props.fieldsAggregated && this.props.aggregator}
121
+ onFocus={this._onFieldSelectorFocused}
122
+ onBlur={this._onFieldSelectorBlur}
113
123
  />
114
124
  </SidePanelSection>
115
125
  <div>
@@ -431,6 +431,7 @@ export const CHANNEL_SCALES = keyMirror({
431
431
  color: null,
432
432
  radius: null,
433
433
  size: null,
434
+ customMarker: null,
434
435
  colorAggr: null,
435
436
  sizeAggr: null
436
437
  });
@@ -505,7 +506,8 @@ export const FIELD_AGGREGATORS = [
505
506
  export const linearFieldScaleFunctions = {
506
507
  [CHANNEL_SCALES.color]: [SCALE_TYPES.quantize, SCALE_TYPES.quantile, SCALE_TYPES.custom],
507
508
  [CHANNEL_SCALES.radius]: [SCALE_TYPES.sqrt],
508
- [CHANNEL_SCALES.size]: [SCALE_TYPES.linear, SCALE_TYPES.sqrt, SCALE_TYPES.log]
509
+ [CHANNEL_SCALES.size]: [SCALE_TYPES.linear, SCALE_TYPES.sqrt, SCALE_TYPES.log],
510
+ [CHANNEL_SCALES.customMarker]: [SCALE_TYPES.ordinal]
509
511
  };
510
512
 
511
513
  export const linearFieldAggrScaleFunctions = {
@@ -533,7 +535,8 @@ export const linearFieldAggrScaleFunctions = {
533
535
  export const ordinalFieldScaleFunctions = {
534
536
  [CHANNEL_SCALES.color]: [SCALE_TYPES.ordinal],
535
537
  [CHANNEL_SCALES.radius]: [SCALE_TYPES.point],
536
- [CHANNEL_SCALES.size]: [SCALE_TYPES.point]
538
+ [CHANNEL_SCALES.size]: [SCALE_TYPES.point],
539
+ [CHANNEL_SCALES.customMarker]: [SCALE_TYPES.ordinal]
537
540
  };
538
541
 
539
542
  export const ordinalFieldAggrScaleFunctions = {
@@ -554,7 +557,8 @@ export const ordinalFieldAggrScaleFunctions = {
554
557
  export const notSupportedScaleOpts = {
555
558
  [CHANNEL_SCALES.color]: [],
556
559
  [CHANNEL_SCALES.radius]: [],
557
- [CHANNEL_SCALES.size]: []
560
+ [CHANNEL_SCALES.size]: [],
561
+ [CHANNEL_SCALES.customMarker]: []
558
562
  };
559
563
 
560
564
  export const notSupportAggrOpts = {
@@ -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,39 @@ 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
+ return () => unknownValue;
820
+ }
821
+
822
+ const mapping = new Map();
823
+
824
+ if (Array.isArray(range)) {
825
+ for (const {value, markerUrl} of range) {
826
+ if (value === null) {
827
+ unknownValue = markerUrl;
828
+ } else if (markerUrl) {
829
+ mapping.set(value, markerUrl);
830
+ }
831
+ }
832
+ } else {
833
+ unknownValue = range.othersMarker;
834
+ for (const {value, markerUrl} of range.markerMap) {
835
+ if (markerUrl) {
836
+ mapping.set(value, markerUrl);
837
+ }
838
+ }
839
+ }
840
+
841
+ // @ts-ignore d3 scale
842
+ const scale = scaleOrdinal()
843
+ .domain(Array.from(mapping.keys()))
844
+ .range(Array.from(mapping.values()))
845
+ .unknown(unknownValue);
846
+ return scale;
847
+ }
848
+
811
849
  /**
812
850
  * Mapping from visual channels to deck.gl accesors
813
851
  * @param {Object} param Parameters
@@ -818,6 +856,7 @@ class Layer {
818
856
  getAttributeAccessors({dataAccessor = defaultDataAccessor, dataContainer}) {
819
857
  const attributeAccessors = {};
820
858
 
859
+ // eslint-disable-next-line complexity
821
860
  Object.keys(this.visualChannels).forEach(channel => {
822
861
  const {
823
862
  field,
@@ -833,8 +872,13 @@ class Layer {
833
872
  aggregation
834
873
  } = this.visualChannels[channel];
835
874
 
836
- const shouldGetScale = this.config[field];
875
+ const shouldGetScale = Boolean(
876
+ (scale || channelScaleType === 'identity') && this.config[field]
877
+ );
837
878
 
879
+ if (field === 'rotationField') {
880
+ console.log('recreating rotationField accessor', this.config[field]);
881
+ }
838
882
  if (shouldGetScale) {
839
883
  const args = [this.config[scale], this.config[domain], this.config.visConfig[range]];
840
884
  const isFixed = fixed && this.config.visConfig[fixed];
@@ -844,8 +888,12 @@ class Layer {
844
888
  const aggregationValue = this.config.visConfig[aggregation];
845
889
 
846
890
  const scaleFunction =
847
- channelScaleType === CHANNEL_SCALES.color
891
+ channelScaleType === 'identity'
892
+ ? v => v
893
+ : channelScaleType === CHANNEL_SCALES.color
848
894
  ? this.getColorScale(...args)
895
+ : channelScaleType === CHANNEL_SCALES.customMarker
896
+ ? this.getCustomMarkersScale(this.config.visConfig[range])
849
897
  : this.getVisChannelScale(...args, isFixed);
850
898
 
851
899
  attributeAccessors[accessor] = d =>
@@ -1191,6 +1239,12 @@ class Layer {
1191
1239
  return fixed ? 1 : (this.config[field] ? 1 : radius);
1192
1240
  }
1193
1241
 
1242
+ getMaxMarkerSize() {
1243
+ const {sizeField, visConfig} = this.config;
1244
+ const {radiusRange, radius} = visConfig;
1245
+ return Math.ceil((sizeField ? radiusRange[1] : radius));
1246
+ }
1247
+
1194
1248
  getRadiusUnit(fixedRadius) {
1195
1249
  const radiusChannel = Object.values(this.visualChannels).find(vc => vc.property === 'radius');
1196
1250
 
@@ -1300,6 +1354,30 @@ class Layer {
1300
1354
  }, []);
1301
1355
  }
1302
1356
 
1357
+ /**
1358
+ * Create deck.gl `IconLayer` "autoPack" mode accessor for custom markers.
1359
+ *
1360
+ * @param {*} customMarkersStatus Record<url, UrlStatus> provided by builder, UrlStatus `ok` means URL is ok and ready to render
1361
+ * @param {*} iconUrlAccessor deck.gl accessor (or value) that resolves to URL of icon for given row
1362
+ * @returns deck.gl accessor that returns UnpackedIcon ready to consume by IconLayer
1363
+ */
1364
+ getCustomMarkerIconAccessor(customMarkersStatus, iconUrlAccessor) {
1365
+ const maxIconSize = this.getMaxMarkerSize();
1366
+
1367
+ return d => {
1368
+ const wantedUrl =
1369
+ (typeof iconUrlAccessor === 'function') ? iconUrlAccessor(d) : iconUrlAccessor;
1370
+ const url = customMarkersStatus?.[wantedUrl] === 'ok' ? wantedUrl : FALLBACK_ICON;
1371
+ return {
1372
+ id: `${url}@@${maxIconSize}`,
1373
+ url,
1374
+ width: maxIconSize,
1375
+ height: maxIconSize,
1376
+ mask: true
1377
+ };
1378
+ };
1379
+ }
1380
+
1303
1381
  calculateDataAttribute(keplerTable, getPosition) {
1304
1382
  // implemented in subclasses
1305
1383
  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,12 +20,13 @@
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';
26
27
  import {findDefaultColorField} from 'utils/dataset-utils';
27
28
  import PointLayerIcon from './point-layer-icon';
28
- import {DEFAULT_LAYER_COLOR, CHANNEL_SCALES} from 'constants/default-settings';
29
+ import {DEFAULT_LAYER_COLOR, CHANNEL_SCALES, UNKNOWN_COLOR} from 'constants/default-settings';
29
30
 
30
31
  import {getTextOffsetByRadius, formatTextLabelData} from '../layer-text-label';
31
32
 
@@ -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,31 @@ 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
+ fixed: 'customMarkersUrl',
138
+ channelScaleType: CHANNEL_SCALES.customMarker,
139
+ accessor: 'getIconUrl',
140
+ supportedFieldTypes: ['string'],
141
+ defaultValue: config => config.visConfig.customMarkersUrl,
142
+ condition: config => config.visConfig.customMarkers
143
+ },
144
+ rotation: {
145
+ property: 'rotation',
146
+ key: 'rotation',
147
+ field: 'rotationField',
148
+ channelScaleType: 'identity',
149
+ accessor: 'getAngle',
150
+ supportedFieldTypes: ['float', 'integer', 'real', 'number'],
151
+ defaultValue: () => 0,
152
+ nullValue: 0,
153
+ condition: config => config.rotationField
125
154
  }
126
155
  };
127
156
  }
@@ -181,7 +210,10 @@ export default class PointLayer extends Layer {
181
210
  // add stroke color visual channel
182
211
  strokeColorField: null,
183
212
  strokeColorDomain: [0, 1],
184
- strokeColorScale: 'quantile'
213
+ strokeColorScale: 'quantile',
214
+
215
+ customMarkersField: null,
216
+ customMarkersScale: 'ordinal'
185
217
  };
186
218
  }
187
219
 
@@ -237,22 +269,22 @@ export default class PointLayer extends Layer {
237
269
  this.updateMeta({bounds});
238
270
  }
239
271
 
272
+ isLayerHovered(objectInfo) {
273
+ return (
274
+ (objectInfo?.picked && objectInfo?.layer?.props?.id === this.id) ||
275
+ objectInfo?.layer?.props?.id === `${this.id}-icon`
276
+ );
277
+ }
278
+
279
+ // eslint-disable-next-line complexity
240
280
  renderLayer(opts) {
241
- const {data, gpuFilter, objectHovered, mapState, interactionConfig} = opts;
281
+ const {data, gpuFilter, objectHovered, mapState, interactionConfig, customMarkersStatus} = opts;
242
282
 
243
283
  // if no field size is defined we need to pass fixed radius = false
244
284
  const fixedRadius = this.config.visConfig.fixedRadius && Boolean(this.config.sizeField);
245
285
  const radiusScale = this.getRadiusScale(fixedRadius);
246
286
  const radiusUnits = this.getRadiusUnit(fixedRadius);
247
287
 
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
288
  const updateTriggers = {
257
289
  getPosition: this.config.columns,
258
290
  getFilterValue: gpuFilter.filterValueUpdateTriggers,
@@ -278,16 +310,97 @@ export default class PointLayer extends Layer {
278
310
  };
279
311
  const hoveredObject = this.hasHoveredObject(objectHovered);
280
312
 
313
+ const textLabelLayer = this.renderTextLabelLayer(
314
+ {
315
+ getPosition: data.getPosition,
316
+ sharedProps,
317
+ getPixelOffset,
318
+ updateTriggers
319
+ },
320
+ opts
321
+ );
322
+ const commonLayerProps = {
323
+ ...defaultLayerProps,
324
+ parameters: {
325
+ // circles will be flat on the map when the altitude column is not used
326
+ depthTest: this.config.columns.altitude?.fieldIdx > -1
327
+ }
328
+ };
329
+
330
+ const customMarkersField = this.config.customMarkersField;
331
+ const {customMarkers, customMarkersUrl, customMarkersRange} = this.config.visConfig;
332
+ const customMarkersConfigured =
333
+ customMarkers && Boolean(customMarkersUrl || (customMarkersField && customMarkersRange));
334
+ const customMarkersMisConfigured = customMarkers && !customMarkersConfigured;
335
+
336
+ if (customMarkersConfigured) {
337
+ const maxIconSize = this.getMaxMarkerSize();
338
+ const getIcon = this.getCustomMarkerIconAccessor(customMarkersStatus, data.getIconUrl);
339
+ const iconLayerProps = {
340
+ getIcon,
341
+ getPosition: data.getPosition,
342
+ getSize: data.getRadius,
343
+ getAngle: data.getAngle,
344
+ sizeScale: radiusScale,
345
+ sizeUnits: 'pixels',
346
+ loadOptions: {
347
+ image: {
348
+ type: 'imagebitmap'
349
+ },
350
+ imagebitmap: {
351
+ resizeWidth: maxIconSize,
352
+ resizeHeight: maxIconSize,
353
+ resizeQuality: 'high'
354
+ }
355
+ }
356
+ };
357
+
358
+ return [
359
+ new IconLayer({
360
+ ...commonLayerProps,
361
+ ...iconLayerProps,
362
+ id: `${defaultLayerProps.id}-icon`,
363
+ data: data.data,
364
+ getColor: this.config.visConfig.filled ? data.getFillColor : UNKNOWN_COLOR,
365
+ getFilterValue: data.getFilterValue,
366
+ updateTriggers: {
367
+ getFilterValue: updateTriggers.getFilterValue,
368
+ getColor: [updateTriggers.getFillColor, this.config.visConfig.filled],
369
+ getIcon: [updateTriggers.getIconUrl, maxIconSize, customMarkersStatus],
370
+ getSize: updateTriggers.getRadius,
371
+ getAngle: updateTriggers.getAngle
372
+ }
373
+ }),
374
+ ...(hoveredObject
375
+ ? [
376
+ new IconLayer({
377
+ ...iconLayerProps,
378
+ ...this.getDefaultHoverLayerProps(),
379
+ id: `${defaultLayerProps.id}-icon-hovered`,
380
+ data: [hoveredObject],
381
+ getColor: this.config.highlightColor
382
+ })
383
+ ]
384
+ : []),
385
+ ...textLabelLayer
386
+ ];
387
+ }
388
+
389
+ const scatterPlotLayerProps = {
390
+ stroked: this.config.visConfig.outline,
391
+ filled: this.config.visConfig.filled,
392
+ lineWidthScale: this.config.visConfig.thickness,
393
+ // TODO: invent better way to render something when customMarkers are not yet configured
394
+ radiusScale: customMarkersMisConfigured ? radiusScale / 2 : radiusScale,
395
+ ...(this.config.visConfig.fixedRadius ? {} : {radiusMaxPixels: 500})
396
+ };
397
+
281
398
  return [
282
399
  new ScatterplotLayer({
283
- ...defaultLayerProps,
284
400
  ...brushingProps,
285
- ...layerProps,
286
401
  ...data,
287
- parameters: {
288
- // circles will be flat on the map when the altitude column is not used
289
- depthTest: this.config.columns.altitude?.fieldIdx > -1
290
- },
402
+ ...commonLayerProps,
403
+ ...scatterPlotLayerProps,
291
404
  radiusUnits,
292
405
  lineWidthUnits: 'pixels',
293
406
  updateTriggers,
@@ -298,7 +411,7 @@ export default class PointLayer extends Layer {
298
411
  ? [
299
412
  new ScatterplotLayer({
300
413
  ...this.getDefaultHoverLayerProps(),
301
- ...layerProps,
414
+ ...scatterPlotLayerProps,
302
415
  data: [hoveredObject],
303
416
  getLineColor: this.config.highlightColor,
304
417
  getFillColor: this.config.highlightColor,
@@ -308,15 +421,7 @@ export default class PointLayer extends Layer {
308
421
  ]
309
422
  : []),
310
423
  // text label layer
311
- ...this.renderTextLabelLayer(
312
- {
313
- getPosition: data.getPosition,
314
- sharedProps,
315
- getPixelOffset,
316
- updateTriggers
317
- },
318
- opts
319
- )
424
+ ...textLabelLayer
320
425
  ];
321
426
  }
322
427
  }
@@ -34,7 +34,9 @@ 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',
39
+ rotation: 'Rotation'
38
40
  },
39
41
  placeholder: {
40
42
  search: 'Search',
@@ -85,6 +87,7 @@ export default {
85
87
  layer: {
86
88
  required: 'Required*',
87
89
  radius: 'Radius',
90
+ size: 'Size',
88
91
  color: 'Color',
89
92
  fillColor: 'Fill Color',
90
93
  outline: 'Outline',
@@ -101,6 +104,7 @@ export default {
101
104
  elevationByDescription: 'When off, height is based on count of points',
102
105
  colorByDescription: 'When off, color is based on count of points',
103
106
  aggregateBy: 'Aggregate {field} by',
107
+ addUrl: 'Add URL',
104
108
  '3DModel': '3D Model',
105
109
  '3DModelOptions': '3D Model Options',
106
110
  type: {
@@ -161,7 +165,9 @@ export default {
161
165
  weightIntensity: 'Weight Intensity',
162
166
  zoomScale: 'Zoom Scale',
163
167
  heightRange: 'Height Range',
164
- heightMultiplier: 'Height Multiplier'
168
+ heightMultiplier: 'Height Multiplier',
169
+ customMarkers: 'Custom markers',
170
+ url: 'Add URL'
165
171
  },
166
172
  layerVisibility: {
167
173
  title: 'Visibility by zoom level'
@@ -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