@carto/kepler.gl 0.0.65-alpha.labels → 0.0.65-alpha.labels.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.
@@ -0,0 +1,173 @@
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: 1};
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
+ const {getTextAnchor: anchor, getAlignmentBaseline: alignment, sizeScale} = this.props;
61
+
62
+ // Heuristics to avoid label overlap (should we base this on the text length?)
63
+ const paddingX = 12 * sizeScale;
64
+ const paddingY = 3 * sizeScale;
65
+ const backgroundPadding = [0, 0, 0, 0];
66
+ if (alignment === 'top') {
67
+ backgroundPadding[TOP] = paddingY;
68
+ } else if (alignment === 'bottom') {
69
+ backgroundPadding[BOTTOM] = paddingY;
70
+ } else {
71
+ backgroundPadding[TOP] = 0.5 * paddingY;
72
+ backgroundPadding[BOTTOM] = 0.5 * paddingY;
73
+ }
74
+ if (anchor === 'start') {
75
+ backgroundPadding[LEFT] = paddingX;
76
+ } else if (anchor === 'end') {
77
+ backgroundPadding[RIGHT] = paddingX;
78
+ } else {
79
+ backgroundPadding[LEFT] = 0.5 * paddingX;
80
+ backgroundPadding[RIGHT] = 0.5 * paddingX;
81
+ }
82
+
83
+ // For getRadius function, invoke without data, as there is no backgroundPadding accessor
84
+ const pixelOffset = Array.isArray(getPixelOffset) ? getPixelOffset : getPixelOffset();
85
+ backgroundPadding[LEFT] += pixelOffset[0];
86
+ backgroundPadding[RIGHT] -= pixelOffset[0];
87
+ backgroundPadding[TOP] += pixelOffset[1];
88
+ backgroundPadding[BOTTOM] -= pixelOffset[1];
89
+
90
+ return backgroundPadding;
91
+ }
92
+
93
+ renderTextLayer(id, props) {
94
+ const {
95
+ data,
96
+
97
+ fontFamily,
98
+ fontSettings,
99
+ fontWeight,
100
+ outlineColor,
101
+ outlineWidth,
102
+ sizeScale,
103
+
104
+ getAlignmentBaseline,
105
+ getColor,
106
+ getPosition,
107
+ getTextAnchor,
108
+
109
+ updateTriggers
110
+ } = this.props;
111
+
112
+ return new EnhancedTextLayer(
113
+ this.getSubLayerProps({
114
+ id,
115
+ data: data,
116
+
117
+ fontFamily,
118
+ fontSettings,
119
+ fontWeight,
120
+ outlineColor,
121
+ outlineWidth,
122
+ sizeScale,
123
+
124
+ getAlignmentBaseline,
125
+ getColor,
126
+ getPosition,
127
+ getTextAnchor,
128
+
129
+ updateTriggers
130
+ }),
131
+ {
132
+ getSize: 1
133
+ },
134
+ // DEBUG
135
+ SHOW_TEXT_BORDER
136
+ ? {getBackgroundColor: [0, 0, 0, 0], getBorderColor: [255, 0, 0, 80], getBorderWidth: 1}
137
+ : {},
138
+ props
139
+ );
140
+ }
141
+
142
+ renderLayers() {
143
+ const {
144
+ getText,
145
+ getSecondaryColor,
146
+ getSecondaryText,
147
+ secondaryOutlineColor,
148
+ secondarySizeScale
149
+ } = this.props;
150
+ const getPixelOffset = this.calculatePixelOffset(false);
151
+ const backgroundPadding = this.calculateBackgroundPadding(getPixelOffset);
152
+ const out = [
153
+ this.renderTextLayer('primary', {
154
+ backgroundPadding,
155
+ getText,
156
+ getPixelOffset,
157
+ background: true // Only use background for primary label for faster collisions
158
+ }),
159
+ getSecondaryText &&
160
+ this.renderTextLayer('secondary', {
161
+ getText: getSecondaryText,
162
+ getPixelOffset: this.calculatePixelOffset(true),
163
+
164
+ // Optional overrides
165
+ ...(getSecondaryColor && {getColor: getSecondaryColor}),
166
+ ...(secondarySizeScale && {sizeScale: secondarySizeScale}),
167
+ ...(secondaryOutlineColor && {outlineColor: secondaryOutlineColor})
168
+ })
169
+ ];
170
+
171
+ return out;
172
+ }
173
+ }
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,53 +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
- }
41
-
42
- // Workaround until https://github.com/visgl/deck.gl/pull/7513 is released
43
- DeckLayer.defaultProps.getFilterValue = {type: 'accessor', value: 0};
44
-
45
- // Workaround until https://github.com/visgl/deck.gl/pull/7507 is released
46
- function deepEqual(a, b) {
47
- if (a === b) {
48
- return true;
49
- }
50
- if (!a || !b) {
51
- return false;
52
- }
53
- for (const key in a) {
54
- const aValue = a[key];
55
- const bValue = b[key];
56
- const equals = aValue === bValue || deepEqual(aValue, bValue);
57
- if (!equals) {
58
- return false;
59
- }
60
- }
61
- return true;
62
- }
63
-
64
- DeckLayer.defaultProps.loadOptions = {
65
- type: 'object',
66
- value: {},
67
- optional: true,
68
- compare: true,
69
- equal: deepEqual
70
- };
71
-
25
+ import {PointLabelLayer} from '../deckgl-layers/point-label-layer/point-label-layer';
72
26
  import DefaultLayerIcon from './default-layer-icon';
73
27
  import {diffUpdateTriggers} from './layer-update';
74
28
 
@@ -1357,110 +1311,87 @@ class Layer {
1357
1311
  };
1358
1312
  }
1359
1313
 
1360
- renderTextLabelLayer({getPosition, getPixelOffset, updateTriggers, sharedProps}, renderOpts) {
1314
+ renderTextLabelLayer(
1315
+ {getPosition, getRadius, radiusScale, updateTriggers, sharedProps},
1316
+ renderOpts
1317
+ ) {
1361
1318
  const {data, mapState} = renderOpts;
1362
1319
  const {textLabel, visibilityByZoom} = this.config;
1363
1320
  // const extensions = [...sharedProps.extensions];
1364
1321
  const extensions = [...sharedProps.extensions, collisionFilterExtension];
1365
1322
 
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
- }
1323
+ if (!data || !data.textLabels || !data.textLabels[0] || !data.textLabels[0].getText) {
1324
+ return [];
1325
+ }
1391
1326
 
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
- }
1327
+ // TODO other labels
1328
+ const i = 0;
1329
+
1330
+ const [mainLabel, secondaryLabel] = textLabel;
1331
+ const {alignment, anchor, color: getColor, outlineColor, size: sizeScale} = mainLabel;
1332
+ const {
1333
+ color: getSecondaryColor,
1334
+ outlineColor: secondaryOutlineColor,
1335
+ size: secondarySizeScale
1336
+ } = secondaryLabel || {};
1337
+
1338
+ return [
1339
+ new PointLabelLayer({
1340
+ ...sharedProps,
1341
+ id: `${this.id}-label`,
1342
+ data: data.data,
1343
+ visibilityByZoom,
1344
+ extensions,
1345
+
1346
+ characterSet: 'auto',
1347
+ collisionEnabled: true,
1348
+ collisionGroup: `${this.id}-labels`,
1349
+
1350
+ getText: data.textLabels[0].getText,
1351
+ getColor,
1352
+ sizeScale,
1353
+ outlineColor,
1354
+
1355
+ ...(secondaryLabel && {
1356
+ getSecondaryText: data.textLabels[1].getText,
1357
+ getSecondaryColor,
1358
+ secondarySizeScale,
1359
+ secondaryOutlineColor
1360
+ }),
1397
1361
 
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
- }, []);
1362
+ // Position
1363
+ getPosition,
1364
+ getRadius,
1365
+ radiusScale,
1366
+ getTextAnchor: anchor,
1367
+ getAlignmentBaseline: alignment,
1368
+
1369
+ // Style
1370
+ fontFamily: 'Inter, sans',
1371
+ fontSettings: {sdf: true},
1372
+ fontWeight: 500,
1373
+ outlineWidth: 4,
1374
+ parameters: {
1375
+ // text will always show on top of all layers
1376
+ depthTest: false
1377
+ },
1378
+
1379
+ getFilterValue: data.getFilterValue,
1380
+ updateTriggers: {
1381
+ ...updateTriggers,
1382
+ getText: textLabel[i].field?.name,
1383
+ getPixelOffset: {
1384
+ ...updateTriggers.getPixelOffset,
1385
+ ...updateTriggers.getRadius,
1386
+ anchor: mainLabel.anchor,
1387
+ alignment: mainLabel.alignment
1388
+ },
1389
+ getTextAnchor: mainLabel.anchor,
1390
+ getAlignmentBaseline: mainLabel.alignment,
1391
+ getColor: textLabel[i].color
1392
+ }
1393
+ })
1394
+ ];
1464
1395
  }
1465
1396
 
1466
1397
  /**
@@ -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