@carto/kepler.gl 0.0.65-alpha.labels → 0.0.65-alpha.labels.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.
@@ -0,0 +1,174 @@
1
+ import {CompositeLayer} from '@deck.gl/core';
2
+ import {TextLayer} from '@deck.gl/layers';
3
+
4
+ // TODO remove prior to release
5
+ const SHOW_TEXT_BORDER = false;
6
+
7
+ const [LEFT, TOP, RIGHT, BOTTOM] = [0, 1, 2, 3];
8
+ class EnhancedTextLayer extends TextLayer {
9
+ filterSubLayer({layer, renderPass}) {
10
+ const background = layer.id.includes('primary-background');
11
+ if (renderPass === 'collision') {
12
+ return background; // Only draw primary background layer in collision pass
13
+ } else {
14
+ return !background || (SHOW_TEXT_BORDER && this.props.getBorderWidth); // Do not draw background layer in other passes
15
+ }
16
+ }
17
+ }
18
+
19
+ // TODO fix in deck.gl
20
+ TextLayer.defaultProps.fontSettings = {type: 'object', value: {}, optional: true, compare: 2};
21
+
22
+ const defaultProps = {
23
+ ...TextLayer.defaultProps,
24
+ getRadius: {type: 'accessor', value: 1},
25
+ radiusScale: {type: 'number', min: 0, value: 1}
26
+ };
27
+
28
+ export class PointLabelLayer extends CompositeLayer {
29
+ static defaultProps = defaultProps;
30
+
31
+ calculatePixelOffset(secondary) {
32
+ const {
33
+ getTextAnchor: anchor,
34
+ getAlignmentBaseline: alignment,
35
+ getRadius,
36
+ radiusScale,
37
+ sizeScale
38
+ } = this.props;
39
+ const xMult = anchor === 'middle' ? 0 : anchor === 'start' ? 1 : -1;
40
+ const yMult = alignment === 'center' ? 0 : alignment === 'bottom' ? 1 : -1;
41
+
42
+ // Place secondary label under main label (spacing 1 / 3 main label font size)
43
+ const yOffset = secondary ? (1 + 1 / 3) * sizeScale : 0;
44
+ const xPadding = sizeScale / 4; // Padding based on font size (font size / 4)
45
+ const yPadding = sizeScale;
46
+ const radiusPadding = 1 + 1 / 4; // Padding based on point radius (radius/ 4)
47
+
48
+ return typeof getRadius === 'function'
49
+ ? d => {
50
+ const r = (d ? getRadius(d) : 1) * radiusScale * radiusPadding;
51
+ return [xMult * (r + xPadding), yMult * (r + yPadding) + yOffset];
52
+ }
53
+ : [
54
+ xMult * (getRadius * radiusScale * radiusPadding + xPadding),
55
+ yMult * (getRadius * radiusScale * radiusPadding + yPadding) + yOffset
56
+ ];
57
+ }
58
+
59
+ calculateBackgroundPadding(getPixelOffset) {
60
+ // Alignment based only on first label
61
+ const {alignment, anchor, sizeScale} = this.props;
62
+
63
+ // Heuristics to avoid label overlap (should we base this on the text length?)
64
+ const paddingX = 12 * sizeScale;
65
+ const paddingY = 3 * sizeScale;
66
+ const backgroundPadding = [0, 0, 0, 0];
67
+ if (alignment === 'top') {
68
+ backgroundPadding[TOP] = paddingY;
69
+ } else if (alignment === 'bottom') {
70
+ backgroundPadding[BOTTOM] = paddingY;
71
+ } else {
72
+ backgroundPadding[TOP] = 0.5 * paddingY;
73
+ backgroundPadding[BOTTOM] = 0.5 * paddingY;
74
+ }
75
+ if (anchor === 'start') {
76
+ backgroundPadding[LEFT] = paddingX;
77
+ } else if (anchor === 'end') {
78
+ backgroundPadding[RIGHT] = paddingX;
79
+ } else {
80
+ backgroundPadding[LEFT] = 0.5 * paddingX;
81
+ backgroundPadding[RIGHT] = 0.5 * paddingX;
82
+ }
83
+
84
+ // For getRadius function, invoke without data, as there is no backgroundPadding accessor
85
+ const pixelOffset = Array.isArray(getPixelOffset) ? getPixelOffset : getPixelOffset();
86
+ backgroundPadding[LEFT] += pixelOffset[0];
87
+ backgroundPadding[RIGHT] -= pixelOffset[0];
88
+ backgroundPadding[TOP] += pixelOffset[1];
89
+ backgroundPadding[BOTTOM] -= pixelOffset[1];
90
+
91
+ return backgroundPadding;
92
+ }
93
+
94
+ renderTextLayer(id, props) {
95
+ const {
96
+ data,
97
+
98
+ fontFamily,
99
+ fontSettings,
100
+ fontWeight,
101
+ outlineColor,
102
+ outlineWidth,
103
+ sizeScale,
104
+
105
+ getAlignmentBaseline,
106
+ getColor,
107
+ getPosition,
108
+ getTextAnchor,
109
+
110
+ updateTriggers
111
+ } = this.props;
112
+
113
+ return new EnhancedTextLayer(
114
+ this.getSubLayerProps({
115
+ id,
116
+ data: data,
117
+
118
+ fontFamily,
119
+ fontSettings,
120
+ fontWeight,
121
+ outlineColor,
122
+ outlineWidth,
123
+ sizeScale,
124
+
125
+ getAlignmentBaseline,
126
+ getColor,
127
+ getPosition,
128
+ getTextAnchor,
129
+
130
+ updateTriggers
131
+ }),
132
+ {
133
+ getSize: 1
134
+ },
135
+ // DEBUG
136
+ SHOW_TEXT_BORDER
137
+ ? {getBackgroundColor: [0, 0, 0, 0], getBorderColor: [255, 0, 0, 80], getBorderWidth: 1}
138
+ : {},
139
+ props
140
+ );
141
+ }
142
+
143
+ renderLayers() {
144
+ const {
145
+ getText,
146
+ getSecondaryColor,
147
+ getSecondaryText,
148
+ secondaryOutlineColor,
149
+ secondarySizeScale
150
+ } = this.props;
151
+ const getPixelOffset = this.calculatePixelOffset();
152
+ const backgroundPadding = this.calculateBackgroundPadding(getPixelOffset);
153
+ const out = [
154
+ this.renderTextLayer('primary', {
155
+ backgroundPadding,
156
+ getText,
157
+ getPixelOffset,
158
+ background: true // Only use background for primary label for faster collisions
159
+ }),
160
+ getSecondaryText &&
161
+ this.renderTextLayer('secondary', {
162
+ getText: getSecondaryText,
163
+ getPixelOffset: this.calculatePixelOffset(true),
164
+
165
+ // Optional overrides
166
+ ...(getSecondaryColor && {getColor: getSecondaryColor}),
167
+ ...(secondarySizeScale && {sizeScale: secondarySizeScale}),
168
+ ...(secondaryOutlineColor && {outlineColor: secondaryOutlineColor})
169
+ })
170
+ ];
171
+
172
+ return out;
173
+ }
174
+ }
package/src/index.js CHANGED
@@ -39,6 +39,9 @@ export * from './components';
39
39
  // Layers
40
40
  export * from './layers';
41
41
 
42
+ // label layer (move to deck.gl/carto?)
43
+ export {PointLabelLayer} from './deckgl-layers/point-label-layer/point-label-layer';
44
+
42
45
  // Styles
43
46
  export * from './styles';
44
47
 
@@ -22,22 +22,7 @@ import {console as Console} from 'global/window';
22
22
  import keymirror from 'keymirror';
23
23
  import {CollisionFilterExtension, DataFilterExtension} from '@deck.gl/extensions';
24
24
  import {COORDINATE_SYSTEM, Layer as DeckLayer} from '@deck.gl/core';
25
- import {TextLayer} from '@deck.gl/layers';
26
-
27
- // TODO remove prior to release
28
- const SHOW_TEXT_BORDER = false;
29
-
30
- const [LEFT, TOP, RIGHT, BOTTOM] = [0, 1, 2, 3];
31
- class EnhancedTextLayer extends TextLayer {
32
- filterSubLayer({layer, renderPass}) {
33
- const background = layer.id.includes('background');
34
- if (renderPass === 'collision') {
35
- return background; // Only draw background layer in collision pass
36
- } else {
37
- return !background || (SHOW_TEXT_BORDER && this.props.getBorderWidth); // Do not draw background layer in other passes
38
- }
39
- }
40
- }
25
+ import {PointLabelLayer} from '../deckgl-layers/point-label-layer/point-label-layer';
41
26
 
42
27
  // Workaround until https://github.com/visgl/deck.gl/pull/7513 is released
43
28
  DeckLayer.defaultProps.getFilterValue = {type: 'accessor', value: 0};
@@ -1357,110 +1342,87 @@ class Layer {
1357
1342
  };
1358
1343
  }
1359
1344
 
1360
- renderTextLabelLayer({getPosition, getPixelOffset, updateTriggers, sharedProps}, renderOpts) {
1345
+ renderTextLabelLayer(
1346
+ {getPosition, getRadius, radiusScale, updateTriggers, sharedProps},
1347
+ renderOpts
1348
+ ) {
1361
1349
  const {data, mapState} = renderOpts;
1362
1350
  const {textLabel, visibilityByZoom} = this.config;
1363
1351
  // const extensions = [...sharedProps.extensions];
1364
1352
  const extensions = [...sharedProps.extensions, collisionFilterExtension];
1365
1353
 
1366
- return data.textLabels.reduce((accu, d, i) => {
1367
- if (d.getText) {
1368
- // Alignment based only on first label
1369
- const mainLabel = textLabel[0];
1370
- const {alignment, anchor, size} = mainLabel;
1371
- // Heuristics to avoid label overlap (should we base this on the text length?)
1372
- const paddingX = 12 * size;
1373
- const paddingY = 3 * size;
1374
- const backgroundPadding = [0, 0, 0, 0];
1375
- if (alignment === 'top') {
1376
- backgroundPadding[TOP] = paddingY;
1377
- } else if (alignment === 'bottom') {
1378
- backgroundPadding[BOTTOM] = paddingY;
1379
- } else {
1380
- backgroundPadding[TOP] = 0.5 * paddingY;
1381
- backgroundPadding[BOTTOM] = 0.5 * paddingY;
1382
- }
1383
- if (anchor === 'start') {
1384
- backgroundPadding[LEFT] = paddingX;
1385
- } else if (anchor === 'end') {
1386
- backgroundPadding[RIGHT] = paddingX;
1387
- } else {
1388
- backgroundPadding[LEFT] = 0.5 * paddingX;
1389
- backgroundPadding[RIGHT] = 0.5 * paddingX;
1390
- }
1354
+ if (!data || !data.textLabels || !data.textLabels[0] || !data.textLabels[0].getText) {
1355
+ return null;
1356
+ }
1391
1357
 
1392
- const pixelOffset = getPixelOffset(mainLabel);
1393
- if (i !== 0) {
1394
- // Place secondary label under main label (spacing 1 / 3 main label font size)
1395
- pixelOffset[1] += (1 + 1 / 3) * size;
1396
- }
1358
+ // TODO other labels
1359
+ const i = 0;
1360
+
1361
+ const [mainLabel, secondaryLabel] = textLabel;
1362
+ const {alignment, anchor, color: getColor, outlineColor, size: sizeScale} = mainLabel;
1363
+ const {
1364
+ color: getSecondaryColor,
1365
+ outlineColor: secondaryOutlineColor,
1366
+ size: secondarySizeScale
1367
+ } = secondaryLabel || {};
1368
+
1369
+ return [
1370
+ new PointLabelLayer({
1371
+ ...sharedProps,
1372
+ id: `${this.id}-label`,
1373
+ data: data.data,
1374
+ visibilityByZoom,
1375
+ characterSet: 'auto',
1376
+
1377
+ extensions,
1378
+ collisionEnabled: true,
1379
+ collisionGroup: `${this.id}-labels`,
1380
+
1381
+ getText: data.textLabels[0].getText,
1382
+ getColor,
1383
+ sizeScale,
1384
+ outlineColor,
1385
+
1386
+ ...(secondaryLabel && {
1387
+ getSecondaryText: data.textLabels[1].getText,
1388
+ getSecondaryColor,
1389
+ secondarySizeScale,
1390
+ secondaryOutlineColor
1391
+ }),
1397
1392
 
1398
- // TODO handle function case
1399
- backgroundPadding[LEFT] += pixelOffset[0];
1400
- backgroundPadding[RIGHT] -= pixelOffset[0];
1401
- backgroundPadding[TOP] += pixelOffset[1];
1402
- backgroundPadding[BOTTOM] -= pixelOffset[1];
1403
-
1404
- const {size: sizeScale} = textLabel[i];
1405
-
1406
- accu.push(
1407
- new EnhancedTextLayer({
1408
- ...sharedProps,
1409
- extensions,
1410
- collisionEnabled: true,
1411
- collisionGroup: 'labels',
1412
- id: `${this.id}-label-${textLabel[i].field?.name}`,
1413
- data: data.data,
1414
- visibilityByZoom,
1415
- getText: d.getText,
1416
- getPosition,
1417
- characterSet: d.characterSet,
1418
- getPixelOffset: pixelOffset,
1419
- getSize: 1,
1420
- sizeScale,
1421
- getTextAnchor: anchor,
1422
- getAlignmentBaseline: alignment,
1423
-
1424
- background: true,
1425
- backgroundPadding,
1426
-
1427
- // DEBUG
1428
- ...(SHOW_TEXT_BORDER && {
1429
- getBorderColor: [255, 0, 0, 80],
1430
- getBackgroundColor: [0, 0, 0, 0],
1431
- getBorderWidth: 1
1432
- }),
1433
-
1434
- getColor: textLabel[i].color,
1435
- fontFamily: 'Inter, sans',
1436
- fontSettings: {sdf: true},
1437
- fontWeight: 500,
1438
- outlineColor: textLabel[i].outlineColor,
1439
- outlineWidth: 4, // 1 seems to be thinner than 1px in CSS
1440
- parameters: {
1441
- // text will always show on top of all layers
1442
- depthTest: false
1443
- },
1444
-
1445
- getFilterValue: data.getFilterValue,
1446
- updateTriggers: {
1447
- ...updateTriggers,
1448
- getText: textLabel[i].field?.name,
1449
- getPixelOffset: {
1450
- ...updateTriggers.getPixelOffset,
1451
- ...updateTriggers.getRadius,
1452
- anchor: mainLabel.anchor,
1453
- alignment: mainLabel.alignment
1454
- },
1455
- getTextAnchor: mainLabel.anchor,
1456
- getAlignmentBaseline: mainLabel.alignment,
1457
- getColor: textLabel[i].color
1458
- }
1459
- })
1460
- );
1461
- }
1462
- return accu;
1463
- }, []);
1393
+ // Position
1394
+ getPosition,
1395
+ getRadius,
1396
+ radiusScale,
1397
+ getTextAnchor: anchor,
1398
+ getAlignmentBaseline: alignment,
1399
+
1400
+ // Style
1401
+ fontFamily: 'Inter, sans',
1402
+ fontSettings: {sdf: true},
1403
+ fontWeight: 500,
1404
+ outlineWidth: 4,
1405
+ parameters: {
1406
+ // text will always show on top of all layers
1407
+ depthTest: false
1408
+ },
1409
+
1410
+ getFilterValue: data.getFilterValue,
1411
+ updateTriggers: {
1412
+ ...updateTriggers,
1413
+ getText: textLabel[i].field?.name,
1414
+ getPixelOffset: {
1415
+ ...updateTriggers.getPixelOffset,
1416
+ ...updateTriggers.getRadius,
1417
+ anchor: mainLabel.anchor,
1418
+ alignment: mainLabel.alignment
1419
+ },
1420
+ getTextAnchor: mainLabel.anchor,
1421
+ getAlignmentBaseline: mainLabel.alignment,
1422
+ getColor: textLabel[i].color
1423
+ }
1424
+ })
1425
+ ];
1464
1426
  }
1465
1427
 
1466
1428
  /**
@@ -45,7 +45,7 @@ export {DEFAULT_COLOR_RANGE};
45
45
  export const DEFAULT_TEXT_LABEL = {
46
46
  field: null,
47
47
  color: [44, 48, 50],
48
- outlineColor: [0, 0, 0],
48
+ outlineColor: [255, 255, 255],
49
49
  size: 12,
50
50
  offset: [0, 0],
51
51
  anchor: 'start',
@@ -21,31 +21,25 @@
21
21
  import {notNullorUndefined} from 'utils/data-utils';
22
22
  import uniq from 'lodash.uniq';
23
23
 
24
- export const defaultPadding = 20;
25
-
26
24
  export function getTextOffsetByRadius(radiusScale, getRadius) {
27
25
  return textLabel => {
28
26
  const xMult = textLabel.anchor === 'middle' ? 0 : textLabel.anchor === 'start' ? 1 : -1;
29
27
  const yMult = textLabel.alignment === 'center' ? 0 : textLabel.alignment === 'bottom' ? 1 : -1;
30
28
 
31
- const sizeOffset =
32
- textLabel.alignment === 'center'
33
- ? 0
34
- : textLabel.alignment === 'bottom'
35
- ? textLabel.size
36
- : textLabel.size;
29
+ const sizeOffset = Math.abs(yMult * textLabel.size);
37
30
 
38
- const padding = textLabel.size / 4; // Padding based on font size (font size / 4)
39
- const pointPadding = 1 + 1 / 4; // Padding based on point size (point size / 4)
31
+ const xPadding = textLabel.size / 4; // Padding based on font size (font size / 4)
32
+ const yPadding = textLabel.size;
33
+ const radiusPadding = 1 + 1 / 4; // Padding based on point radius (radius/ 4)
40
34
 
41
35
  return typeof getRadius === 'function'
42
36
  ? d => [
43
- xMult * (getRadius(d) * radiusScale * pointPadding + padding),
44
- yMult * (getRadius(d) * radiusScale * pointPadding + sizeOffset)
37
+ xMult * (getRadius(d) * radiusScale * radiusPadding + xPadding),
38
+ yMult * (getRadius(d) * radiusScale * radiusPadding + yPadding)
45
39
  ]
46
40
  : [
47
- xMult * (getRadius * radiusScale * pointPadding + padding),
48
- yMult * (getRadius * radiusScale * pointPadding + sizeOffset)
41
+ xMult * (getRadius * radiusScale * radiusPadding + xPadding),
42
+ yMult * (getRadius * radiusScale * radiusPadding + yPadding)
49
43
  ];
50
44
  };
51
45
  }
@@ -55,40 +49,9 @@ export const textLabelAccessor = textLabel => dc => d => {
55
49
  return notNullorUndefined(val) ? String(val) : '';
56
50
  };
57
51
 
58
- export const formatTextLabelData = ({
59
- textLabel,
60
- triggerChanged,
61
- oldLayerData,
62
- data,
63
- dataContainer
64
- }) => {
52
+ export const formatTextLabelData = ({textLabel, dataContainer}) => {
65
53
  return textLabel.map((tl, i) => {
66
- if (!tl.field) {
67
- // if no field selected,
68
- return {
69
- getText: null,
70
- characterSet: []
71
- };
72
- }
73
-
74
- const getText = textLabelAccessor(tl)(dataContainer);
75
- let characterSet;
76
-
77
- if (
78
- !triggerChanged[`getLabelCharacterSet-${i}`] &&
79
- oldLayerData &&
80
- oldLayerData.textLabels &&
81
- oldLayerData.textLabels[i]
82
- ) {
83
- characterSet = oldLayerData.textLabels[i].characterSet;
84
- } else {
85
- const allLabels = tl.field ? data.map(getText) : [];
86
- characterSet = uniq(allLabels.join(''));
87
- }
88
-
89
- return {
90
- characterSet,
91
- getText
92
- };
54
+ const getText = tl.field ? textLabelAccessor(tl)(dataContainer) : null;
55
+ return {getText};
93
56
  });
94
57
  };
@@ -29,7 +29,7 @@ import {findDefaultColorField} from 'utils/dataset-utils';
29
29
  import PointLayerIcon from './point-layer-icon';
30
30
  import {DEFAULT_LAYER_COLOR, CHANNEL_SCALES, UNKNOWN_COLOR} from 'constants/default-settings';
31
31
 
32
- import {getTextOffsetByRadius, formatTextLabelData} from '../layer-text-label';
32
+ import {formatTextLabelData} from '../layer-text-label';
33
33
 
34
34
  export const pointPosAccessor = ({lat, lng, altitude}) => dc => d => [
35
35
  dc.valueAt(d.index, lng.fieldIdx),
@@ -243,13 +243,7 @@ export default class PointLayer extends Layer {
243
243
  const getPosition = this.getPositionAccessor(dataContainer);
244
244
 
245
245
  // get all distinct characters in the text labels
246
- const textLabels = formatTextLabelData({
247
- textLabel,
248
- triggerChanged,
249
- oldLayerData,
250
- data,
251
- dataContainer
252
- });
246
+ const textLabels = formatTextLabelData({textLabel, dataContainer});
253
247
 
254
248
  const accessors = this.getAttributeAccessors({dataContainer});
255
249
 
@@ -285,11 +279,6 @@ export default class PointLayer extends Layer {
285
279
 
286
280
  const defaultLayerProps = this.getDefaultDeckLayerProps(opts);
287
281
  const brushingProps = this.getBrushingExtensionProps(interactionConfig);
288
- const getPixelOffset = getTextOffsetByRadius(
289
- // TODO remove this scaling by half here and elsewhere
290
- customMarkers ? radiusScale / 2 : radiusScale,
291
- data.getRadius
292
- );
293
282
  const extensions = [...defaultLayerProps.extensions, brushingExtension];
294
283
 
295
284
  const updateTriggers = {
@@ -311,7 +300,9 @@ export default class PointLayer extends Layer {
311
300
  {
312
301
  getPosition: data.getPosition,
313
302
  sharedProps,
314
- getPixelOffset,
303
+ // TODO remove this scaling by half here and elsewhere
304
+ radiusScale: customMarkers ? radiusScale / 2 : radiusScale,
305
+ getRadius: data.getRadius,
315
306
  updateTriggers
316
307
  },
317
308
  opts