@carto/kepler.gl 0.0.71 → 0.0.72-alpha.1

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 (44) hide show
  1. package/dist/components/common/item-selector/accessor.js +2 -2
  2. package/dist/components/common/item-selector/dropdown-list.js +12 -9
  3. package/dist/components/common/item-selector/item-selector.js +2 -2
  4. package/dist/components/side-panel/layer-panel/color-range-selector.js +28 -7
  5. package/dist/components/side-panel/layer-panel/color-selector.js +4 -1
  6. package/dist/components/side-panel/layer-panel/hex-column-config.js +93 -0
  7. package/dist/components/side-panel/layer-panel/layer-configurator.js +39 -11
  8. package/dist/constants/color-ranges.js +12 -2
  9. package/dist/constants/default-settings.js +7 -4
  10. package/dist/layers/base-layer.js +197 -49
  11. package/dist/layers/point-layer/point-layer.js +2 -3
  12. package/dist/localization/en.js +5 -2
  13. package/dist/reducers/vis-state-merger.js +16 -2
  14. package/dist/schemas/vis-state-schema.js +69 -34
  15. package/dist/utils/color-utils.js +17 -21
  16. package/dist/utils/custom-scales.js +28 -0
  17. package/dist/utils/data-utils.js +31 -1
  18. package/dist/utils/index.js +7 -1
  19. package/dist/utils/layer-utils.js +2 -1
  20. package/dist/utils/table-utils/kepler-table.js +20 -1
  21. package/package.json +2 -1
  22. package/src/components/common/item-selector/accessor.js +1 -1
  23. package/src/components/common/item-selector/dropdown-list.js +23 -15
  24. package/src/components/common/item-selector/item-selector.js +1 -1
  25. package/src/components/side-panel/layer-panel/color-range-selector.js +30 -7
  26. package/src/components/side-panel/layer-panel/color-selector.js +3 -0
  27. package/src/components/side-panel/layer-panel/hex-column-config.js +44 -0
  28. package/src/components/side-panel/layer-panel/layer-configurator.js +47 -7
  29. package/src/constants/color-ranges.js +9 -0
  30. package/src/constants/default-settings.js +5 -2
  31. package/src/layers/base-layer.js +138 -30
  32. package/src/layers/index.d.ts +3 -1
  33. package/src/layers/layer-factory.d.ts +1 -0
  34. package/src/layers/point-layer/point-layer.js +1 -1
  35. package/src/localization/en.js +4 -1
  36. package/src/reducers/vis-state-merger.js +13 -1
  37. package/src/schemas/vis-state-schema.js +24 -1
  38. package/src/utils/color-utils.js +9 -0
  39. package/src/utils/custom-scales.js +20 -0
  40. package/src/utils/data-utils.d.ts +1 -0
  41. package/src/utils/data-utils.js +19 -0
  42. package/src/utils/index.js +1 -1
  43. package/src/utils/layer-utils.js +1 -0
  44. package/src/utils/table-utils/kepler-table.js +22 -0
@@ -25,6 +25,7 @@ import {COORDINATE_SYSTEM, Layer as DeckLayer} from '@deck.gl/core';
25
25
  import {_PointLabelLayer as PointLabelLayer} from '@deck.gl/carto';
26
26
  import DefaultLayerIcon from './default-layer-icon';
27
27
  import {diffUpdateTriggers} from './layer-update';
28
+ import {rgb} from 'd3-color';
28
29
 
29
30
  import {
30
31
  ALL_FIELD_TYPES,
@@ -38,7 +39,7 @@ import {
38
39
  UNKNOWN_COLOR,
39
40
  ZOOM_LEVEL_LIMITS
40
41
  } from 'constants/default-settings';
41
- import {COLOR_RANGES} from 'constants/color-ranges';
42
+ import {COLOR_RANGES, hexColor} from 'constants/color-ranges';
42
43
  import {DataVizColors} from 'constants/custom-color-ranges';
43
44
  import {
44
45
  LAYER_VIS_CONFIGS,
@@ -53,7 +54,7 @@ import {generateHashId, isPlainObject} from 'utils/utils';
53
54
  import {getLatLngBounds, notNullorUndefined} from 'utils/data-utils';
54
55
  import {getSampleData} from 'utils/table-utils/data-container-utils';
55
56
 
56
- import {hexToRgb, getColorGroupByName, reverseColorRange} from 'utils/color-utils';
57
+ import {hexToRgb, getColorGroupByName, reverseColorRange, checkColorOrFallback} from 'utils/color-utils';
57
58
  import {getMaskExtensionProps} from 'utils/layer-utils';
58
59
  import {scaleOrdinal} from 'd3-scale';
59
60
 
@@ -100,10 +101,12 @@ export const colorMaker = generateColor();
100
101
  class Layer {
101
102
  constructor(props = {}) {
102
103
  this.id = props.id || generateHashId(LAYER_ID_LENGTH);
103
-
104
104
  // meta
105
105
  this.meta = {};
106
106
 
107
+ // legendSettings
108
+ this.legendSettings = {};
109
+
107
110
  // visConfigSettings
108
111
  this.visConfigSettings = {};
109
112
 
@@ -609,8 +612,9 @@ class Layer {
609
612
  };
610
613
 
611
614
  this.updateLayerConfig({colorUI});
612
- // if colorUI[prop] is colorRange
613
- const isColorRange = visConfig[prop] && visConfig[prop].colors;
615
+ // if colorUI[prop] is colorRange. If type is hexColor then it's also a range
616
+ const isColorRange =
617
+ visConfig[prop] && (visConfig[prop].colors || visConfig[prop].type === hexColor);
614
618
 
615
619
  if (isColorRange) {
616
620
  this.updateColorUIByColorRange(newConfig, prop);
@@ -663,8 +667,10 @@ class Layer {
663
667
  colorRangeConfig: {
664
668
  ...colorUI[prop].colorRangeConfig,
665
669
  type: visConfig[prop].type,
666
- steps: visConfig[prop].colors.length,
667
- reversed: Boolean(visConfig[prop].reversed)
670
+ steps: visConfig[prop].colors?.length,
671
+ reversed: Boolean(visConfig[prop].reversed),
672
+ hexColumn: this.getHexColumn(prop),
673
+ isHexColumn: this.isColoredByColorColumn(prop)
668
674
  }
669
675
  }
670
676
  }
@@ -732,6 +738,12 @@ class Layer {
732
738
  updateCategoryColorMap(colorMap, domain, colorRange) {
733
739
  const colors = colorRange.colors;
734
740
  const assignedCategories = [];
741
+ if (colorRange.type === hexColor) {
742
+ return {
743
+ ...colorRange,
744
+ colorMap: []
745
+ };
746
+ }
735
747
  const newColorMap = colorMap.slice(0, colors.length).map((cMap, index) => {
736
748
  assignedCategories.push(cMap[0]);
737
749
  return [cMap[0], colors[index]];
@@ -821,7 +833,12 @@ class Layer {
821
833
  }
822
834
 
823
835
  getColorScale(colorScale, colorDomain, colorRange) {
824
- if (Array.isArray(colorRange.colorMap)) {
836
+ if (colorRange.type === hexColor) {
837
+ return (_, color) => {
838
+ const {r, g, b} = rgb(color);
839
+ return [r, g, b].some(isNaN) ? UNKNOWN_COLOR : [r, g, b];
840
+ };
841
+ } else if (Array.isArray(colorRange.colorMap)) {
825
842
  const cMap = new Map();
826
843
  colorRange.colorMap.forEach(([k, v]) => {
827
844
  cMap.set(k, typeof v === 'string' ? hexToRgb(v) : v);
@@ -914,13 +931,15 @@ class Layer {
914
931
  : this.getVisChannelScale(...args, isFixed);
915
932
 
916
933
  attributeAccessors[accessor] = d =>
917
- this.getEncodedChannelValue(
918
- scaleFunction,
919
- dataAccessor(dataContainer)(d),
920
- fieldConfig,
921
- aggregationValue,
934
+ this.getEncodedChannelValue({
935
+ channel,
936
+ dataContainer,
937
+ scale: scaleFunction,
938
+ data: dataAccessor(dataContainer)(d),
939
+ field: fieldConfig,
940
+ _aggregation: aggregationValue,
922
941
  nullValue
923
- );
942
+ });
924
943
  } else if (typeof getAttributeValue === 'function') {
925
944
  attributeAccessors[accessor] = getAttributeValue(this.config);
926
945
  } else {
@@ -990,17 +1009,35 @@ class Layer {
990
1009
  return triggerChanged;
991
1010
  }
992
1011
 
993
- getEncodedChannelValue(
1012
+ getEncodedChannelValue({
1013
+ channel,
994
1014
  scale,
995
1015
  data,
996
1016
  field,
1017
+ dataContainer,
997
1018
  // In builder we overwrite the getValue function and we use the _aggregation parameter in spatial index layers as the QuadkeyLayer.
998
1019
  _aggregation,
999
1020
  nullValue = NO_VALUE_COLOR,
1000
1021
  getValue = defaultGetFieldValue
1001
- ) {
1022
+ }) {
1023
+ let colorColumnValue;
1002
1024
  const {type} = field;
1003
1025
  const value = getValue(field, data);
1026
+ const channelConfig = this.config.visConfig[this.visualChannels[channel].range];
1027
+ const channelField = this.config[this.visualChannels[channel].field];
1028
+
1029
+ if (channelConfig.type === hexColor) {
1030
+ const columnField = {
1031
+ id: channelField.colorColumn,
1032
+ name: channelField.colorColumn,
1033
+ fieldIdx: channelField.colorColumnIdx,
1034
+ valueAccessor: d => {
1035
+ return dataContainer.valueAt(d.index, channelField.colorColumnIdx);
1036
+ }
1037
+ };
1038
+
1039
+ colorColumnValue = getValue(columnField, data);
1040
+ }
1004
1041
 
1005
1042
  if (!notNullorUndefined(value)) {
1006
1043
  return nullValue;
@@ -1010,9 +1047,9 @@ class Layer {
1010
1047
  if (type === ALL_FIELD_TYPES.timestamp) {
1011
1048
  // shouldn't need to convert here
1012
1049
  // scale Function should take care of it
1013
- attributeValue = scale(new Date(value));
1050
+ attributeValue = scale(new Date(value), colorColumnValue);
1014
1051
  } else {
1015
- attributeValue = scale(value);
1052
+ attributeValue = scale(value, colorColumnValue);
1016
1053
  }
1017
1054
 
1018
1055
  if (!notNullorUndefined(attributeValue)) {
@@ -1026,6 +1063,10 @@ class Layer {
1026
1063
  this.meta = {...this.meta, ...meta};
1027
1064
  }
1028
1065
 
1066
+ updateLayerLegend(settings) {
1067
+ this.legendSettings = {...this.legendSettings, ...settings};
1068
+ }
1069
+
1029
1070
  getDataUpdateTriggers({filteredIndex, id, allData}) {
1030
1071
  const {columns} = this.config;
1031
1072
 
@@ -1046,13 +1087,14 @@ class Layer {
1046
1087
  if (!this.config.dataId) {
1047
1088
  return {};
1048
1089
  }
1049
- const layerDataset = datasets[this.config.dataId];
1050
- const {dataContainer} = layerDataset;
1090
+ const dataset = this.getDataset(datasets);
1091
+ const {dataContainer} = dataset;
1051
1092
 
1052
- this.updateVisualChannelFields(layerDataset);
1093
+ this.updateVisualChannelFields(dataset);
1094
+ this.updateHexColorDomain(dataset);
1053
1095
 
1054
1096
  const getPosition = this.getPositionAccessor(dataContainer);
1055
- const dataUpdateTriggers = this.getDataUpdateTriggers(layerDataset);
1097
+ const dataUpdateTriggers = this.getDataUpdateTriggers(dataset);
1056
1098
  const triggerChanged = this.getChangedTriggers(dataUpdateTriggers);
1057
1099
 
1058
1100
  if (triggerChanged.getMeta) {
@@ -1065,7 +1107,7 @@ class Layer {
1065
1107
  // same data
1066
1108
  data = oldLayerData.data;
1067
1109
  } else {
1068
- data = this.calculateDataAttribute(layerDataset, getPosition);
1110
+ data = this.calculateDataAttribute(dataset, getPosition);
1069
1111
  }
1070
1112
 
1071
1113
  return {data, triggerChanged};
@@ -1077,12 +1119,20 @@ class Layer {
1077
1119
  * @param {Object} dataset
1078
1120
  */
1079
1121
  updateVisualChannelFields(dataset) {
1080
- for (const {field} of Object.values(this.visualChannels)) {
1122
+ for (const {field, range} of Object.values(this.visualChannels)) {
1081
1123
  const oldField = this.config[field];
1124
+ const isUsingHexColorCode = this.config.visConfig[range].hexColor;
1082
1125
  if (!oldField) {
1083
1126
  return;
1084
1127
  }
1085
- const newDataSetField = dataset.fields.find(fd => fd.name === oldField.name) || null;
1128
+ let newDataSetField = dataset.fields.find(fd => fd.name === oldField.name) || null;
1129
+ if (isUsingHexColorCode) {
1130
+ newDataSetField = {
1131
+ ...newDataSetField,
1132
+ colorColumn: newDataSetField.colorColumn || oldField.colorColumn,
1133
+ colorColumnIdx: newDataSetField.colorColumnIdx || oldField.colorColumnIdx
1134
+ };
1135
+ }
1086
1136
  this.updateLayerConfig({[field]: newDataSetField});
1087
1137
  }
1088
1138
  }
@@ -1103,6 +1153,7 @@ class Layer {
1103
1153
  Object.values(this.visualChannels).forEach(channel => {
1104
1154
  const {scale} = channel;
1105
1155
  const scaleType = this.config[scale];
1156
+
1106
1157
  // ordinal domain is based on dataContainer, if only filter changed
1107
1158
  // no need to update ordinal domain
1108
1159
  if (!newFilter || scaleType !== SCALE_TYPES.ordinal) {
@@ -1153,8 +1204,11 @@ class Layer {
1153
1204
  */
1154
1205
  validateScale(channel) {
1155
1206
  const visualChannel = this.visualChannels[channel];
1156
- const {scale} = visualChannel;
1157
- if (!scale) {
1207
+ const {scale, range} = visualChannel;
1208
+ if (
1209
+ !scale ||
1210
+ (this.isColoredByColorColumn(range) && this.config[scale] === SCALE_TYPES.identity)
1211
+ ) {
1158
1212
  // visualChannel doesn't have scale
1159
1213
  return;
1160
1214
  }
@@ -1224,6 +1278,49 @@ class Layer {
1224
1278
  return dataset.getColumnLayerDomain(dataSetField, scaleType) || defaultDomain;
1225
1279
  }
1226
1280
 
1281
+ updateHexColorDomain(dataset) {
1282
+ if (!dataset) {
1283
+ return this;
1284
+ }
1285
+ Object.values(this.visualChannels).forEach(channel => {
1286
+ const channelConfig = this.config.visConfig[channel.range];
1287
+ const field = this.config[channel.field];
1288
+
1289
+ if (channelConfig?.type === hexColor) {
1290
+ const dataSetField = dataset.fields.find(f => f.name === field.colorColumn);
1291
+ const colors = dataset
1292
+ .getColumnLayerDomain(dataSetField, SCALE_TYPES.ordinal)
1293
+ .map(c => checkColorOrFallback(c));
1294
+ this.updateLayerVisConfig({
1295
+ [channel.range]: {...channelConfig, colors}
1296
+ });
1297
+ }
1298
+ });
1299
+
1300
+ return this;
1301
+ }
1302
+
1303
+ calculateHexColumnLegend(datasets) {
1304
+ const dataset = this.getDataset(datasets);
1305
+ if (!datasets) {
1306
+ return;
1307
+ }
1308
+
1309
+ const legendSettings = Object.keys(this.visualChannels).reduce((legends, channel) => {
1310
+ const {range, field: channelField} = this.visualChannels[channel];
1311
+ const channelConfig = this.config.visConfig[range];
1312
+ const field = this.config[channelField];
1313
+
1314
+ if (channelConfig?.type === hexColor) {
1315
+ const dataSetField = dataset.fields.find(f => f.name === field.colorColumn);
1316
+ const legend = dataset.calculateLegend(field, dataSetField);
1317
+ legends[channel] = legend;
1318
+ }
1319
+ return legends;
1320
+ }, {});
1321
+ this.updateLayerLegend(legendSettings);
1322
+ }
1323
+
1227
1324
  hasHoveredObject(objectInfo) {
1228
1325
  return this.isLayerHovered(objectInfo) && objectInfo.object ? objectInfo.object : null;
1229
1326
  }
@@ -1251,9 +1348,9 @@ class Layer {
1251
1348
 
1252
1349
  if (!radiusChannel || this.config[radiusChannel.field]) {
1253
1350
  return 1;
1254
- } else {
1255
- return this.config.visConfig.radius;
1256
1351
  }
1352
+
1353
+ return this.config.visConfig.radius;
1257
1354
  }
1258
1355
 
1259
1356
  getMaxMarkerSize() {
@@ -1315,7 +1412,7 @@ class Layer {
1315
1412
  {getPosition, getRadius, radiusScale, updateTriggers, sharedProps},
1316
1413
  renderOpts
1317
1414
  ) {
1318
- const {data, mapState} = renderOpts;
1415
+ const {data} = renderOpts;
1319
1416
  const {textLabel, visibilityByZoom} = this.config;
1320
1417
  const extensions = [...sharedProps.extensions, collisionFilterExtension];
1321
1418
 
@@ -1434,6 +1531,17 @@ class Layer {
1434
1531
  // implemented in subclasses
1435
1532
  return () => null;
1436
1533
  }
1534
+
1535
+ getHexColumn(range) {
1536
+ const field = Object.values(this.visualChannels)?.find(value => value.range === range)?.field;
1537
+ if (field) {
1538
+ return this.config[field].colorColumn;
1539
+ }
1540
+ }
1541
+
1542
+ isColoredByColorColumn(range) {
1543
+ return this.config.visConfig[range]?.hexColor;
1544
+ }
1437
1545
  }
1438
1546
 
1439
1547
  export default Layer;
@@ -92,6 +92,8 @@ export class Layer {
92
92
  hasInteractivity: boolean;
93
93
  hasAllColumns(): boolean;
94
94
  updateLayerConfig(p: Partial<LayerConfig>): Layer;
95
+ updateLayerMeta(p: any): Layer
96
+ updateLayerLegend(p: any): Layer
95
97
  updateLayerDomain(datasets: Datasets, filter?: Filter): Layer;
96
98
  updateLayerVisualChannel(dataset: KeplerTable, channel: string): Layer;
97
99
  shouldCalculateLayerData(props: string[]): boolean;
@@ -100,7 +102,7 @@ export class Layer {
100
102
  isValidToSave(): boolean;
101
103
  validateVisualChannel(channel: string);
102
104
  getVisualChannelDescription(key: string): {label: string, measure: string};
103
-
105
+ calculateHexColumnLegend(datasets: Datasets): any;
104
106
  static findDefaultLayerProps(dataset: KeplerTable, foundLayers?: any[]);
105
107
  // static findDefaultColumnField(defaultFields, allFields)
106
108
  }
@@ -31,6 +31,7 @@ export type ColorUI = {
31
31
  steps: number;
32
32
  reversed: boolean;
33
33
  custom: boolean;
34
+ hexColumn: boolean;
34
35
  };
35
36
  };
36
37
 
@@ -238,7 +238,7 @@ export default class PointLayer extends Layer {
238
238
  formatLayerData(datasets, oldLayerData) {
239
239
  const {textLabel} = this.config;
240
240
  const {gpuFilter, dataContainer} = datasets[this.config.dataId];
241
- const {data, triggerChanged} = this.updateData(datasets, oldLayerData);
241
+ const {data} = this.updateData(datasets, oldLayerData);
242
242
  const getPosition = this.getPositionAccessor(dataContainer);
243
243
 
244
244
  // get all distinct characters in the text labels
@@ -448,7 +448,10 @@ export default {
448
448
  customPalette: 'Custom Palette',
449
449
  steps: 'steps',
450
450
  type: 'type',
451
- reversed: 'reversed'
451
+ reversed: 'reversed',
452
+ hexColumn: {
453
+ field: 'Color field'
454
+ }
452
455
  },
453
456
  customPalette: {
454
457
  apply: 'Apply',
@@ -423,6 +423,7 @@ export function validateSavedTextLabel(fields, [layerTextLabel], savedTextLabel)
423
423
  export function validateSavedVisualChannels(fields, newLayer, savedLayer) {
424
424
  Object.values(newLayer.visualChannels).forEach(({field, scale, key}) => {
425
425
  let foundField;
426
+ const colorFieldProps = {};
426
427
  if (savedLayer.config) {
427
428
  if (savedLayer.config[field]) {
428
429
  foundField = fields.find(
@@ -430,8 +431,19 @@ export function validateSavedVisualChannels(fields, newLayer, savedLayer) {
430
431
  );
431
432
  }
432
433
 
434
+ if (savedLayer.config[field]?.colorColumn) {
435
+ const colorColumnField = fields.find(
436
+ fd => savedLayer.config && fd.name === savedLayer.config[field].colorColumn
437
+ );
438
+
439
+ if (colorColumnField) {
440
+ colorFieldProps.colorColumn = colorColumnField.name;
441
+ colorFieldProps.colorColumnIdx = colorColumnField.fieldIdx;
442
+ }
443
+ }
444
+
433
445
  const foundChannel = {
434
- ...(foundField ? {[field]: foundField} : {}),
446
+ ...(foundField ? {[field]: {...foundField, ...colorFieldProps}} : {}),
435
447
  ...(savedLayer.config[scale] ? {[scale]: savedLayer.config[scale]} : {})
436
448
  };
437
449
  if (Object.keys(foundChannel).length) {
@@ -340,7 +340,7 @@ class VisualChannelSchemaV1 extends Schema {
340
340
  (accu, key) => ({
341
341
  ...accu,
342
342
  [visualChannels[key].field]: layer.config[visualChannels[key].field]
343
- ? pick(layer.config[visualChannels[key].field], ['name', 'type'])
343
+ ? pick(layer.config[visualChannels[key].field], ['name', 'type', 'colorColumn'])
344
344
  : null,
345
345
  [visualChannels[key].scale]: layer.config[visualChannels[key].scale]
346
346
  }),
@@ -365,6 +365,25 @@ class VisualChannelSchemaV1 extends Schema {
365
365
  };
366
366
  }
367
367
  }
368
+ /**
369
+ * V1: save [channel]: [{ label, color }, ...] for legend
370
+ */
371
+ class LegendSettingsV1 extends Schema {
372
+ save(legendSettings, parents) {
373
+ return {
374
+ [this.key]: Object.keys(legendSettings).reduce((accu, key) => {
375
+ accu[key] = legendSettings[key];
376
+ return accu;
377
+ }, {})
378
+ };
379
+ }
380
+ load(ls, parents, accumulator) {
381
+ return {
382
+ ...accumulator,
383
+ [this.key]: ls
384
+ };
385
+ }
386
+ }
368
387
  const visConfigModificationV1 = {
369
388
  point: (visConfig, parents, accumulated) => {
370
389
  const modified = {};
@@ -459,6 +478,10 @@ export const layerPropsV1 = {
459
478
  })
460
479
  }
461
480
  }),
481
+ legendSettings: new LegendSettingsV1({
482
+ version: VERSIONS.v1,
483
+ key: 'legendSettings'
484
+ }),
462
485
  visualChannels: new VisualChannelSchemaV1({
463
486
  version: VERSIONS.v1,
464
487
  key: 'visualChannels'
@@ -18,6 +18,9 @@
18
18
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  // THE SOFTWARE.
20
20
 
21
+ import {UNKNOWN_COLOR} from 'constants/default-settings';
22
+ import {rgb as d3Rgb} from 'd3-color';
23
+
21
24
  /**
22
25
  * get r g b from hex code
23
26
  *
@@ -104,3 +107,9 @@ export function createLinearGradient(direction, colors) {
104
107
 
105
108
  return `linear-gradient(to ${direction}, ${bands.join(',')})`;
106
109
  }
110
+
111
+ export function checkColorOrFallback(hexColor, fallback = UNKNOWN_COLOR) {
112
+ const color = d3Rgb(hexColor);
113
+ const {r, g, b} = color;
114
+ return [r, g, b].some(isNaN) ? d3Rgb(...fallback).formatHex() : hexColor;
115
+ }
@@ -0,0 +1,20 @@
1
+ export function scaleIdentity() {
2
+ let unknown;
3
+ function scale(x) {
4
+ return x === null ? unknown : x;
5
+ }
6
+
7
+ scale.invert = scale;
8
+
9
+ scale.domain = scale.range = scaleIdentity;
10
+
11
+ scale.unknown = u => {
12
+ if (u) {
13
+ unknown = u;
14
+ }
15
+
16
+ return scale;
17
+ };
18
+
19
+ return scale;
20
+ }
@@ -13,6 +13,7 @@ export function isPlainObject(d: any): boolean;
13
13
  export function numberSort(a: any, b: any): any;
14
14
  export function unique(values: any[]): any[];
15
15
  export function uniqueSortedByFrequency(values: any[]): any[];
16
+ export function uniqueSortedPairByFrequency(values: any[]): any[];
16
17
  export function arrayMove(array: any[], from: number, to: number);
17
18
  export function getSortingFunction(fieldType: string): numberSort | undefined;
18
19
  export function preciseRound(num: number, decimals: number): string;
@@ -426,3 +426,22 @@ export function datetimeFormatter(timezone) {
426
426
  .format(format)
427
427
  : format => ts => moment.utc(ts).format(format);
428
428
  }
429
+
430
+ export function uniqueSortedPairByFrequency(values) {
431
+ const frequencies = values.reduce((res, value) => {
432
+ if (notNullorUndefined(value)) {
433
+ if (!res[value]) {
434
+ res[value] = {
435
+ key: value,
436
+ frequency: 1
437
+ };
438
+ }
439
+ res[value].frequency += 1;
440
+ }
441
+ return res;
442
+ }, {});
443
+
444
+ return Object.values(frequencies)
445
+ .sort((a, b) => b.frequency - a.frequency)
446
+ .map(({key: [left, right]}) => ({left, right}));
447
+ }
@@ -27,7 +27,7 @@ export {
27
27
  FIELD_DISPLAY_FORMAT
28
28
  } from './data-utils';
29
29
  export {createNewDataEntry, datasetColorMaker, findDefaultColorField} from './dataset-utils';
30
- export {hexToRgb} from './color-utils';
30
+ export {hexToRgb, checkColorOrFallback} from './color-utils';
31
31
  export {errorNotification} from './notifications-utils';
32
32
  export {dataURItoBlob, downloadFile} from './export-utils';
33
33
  export {
@@ -70,6 +70,7 @@ export function calculateLayerData(layer, state, oldLayerData) {
70
70
  }
71
71
 
72
72
  const layerData = layer.formatLayerData(state.datasets, oldLayerData);
73
+ layer.calculateHexColumnLegend(state.datasets);
73
74
  return {layerData, layer};
74
75
  }
75
76
 
@@ -45,6 +45,8 @@ import {
45
45
  import {ALL_FIELD_TYPES, SCALE_TYPES} from 'constants/default-settings';
46
46
 
47
47
  import {createDataContainer} from './data-container-utils';
48
+ import {uniqueSortedPairByFrequency} from 'utils/data-utils';
49
+ import {checkColorOrFallback} from 'utils/color-utils';
48
50
 
49
51
  // Unique identifier of each field
50
52
  const FID_KEY = 'name';
@@ -342,6 +344,26 @@ class KeplerTable {
342
344
  }
343
345
  }
344
346
 
347
+ calculateLegend(leftField, rightField) {
348
+ const limitOfNumberOfLegendEntries = 20;
349
+ const leftValues = this.dataContainer.mapIndex(leftField.valueAccessor);
350
+ const rightValues = this.dataContainer.mapIndex(rightField.valueAccessor);
351
+
352
+ const pairing = leftValues.reduce((previous, current, idx) => {
353
+ previous.push([current, rightValues[idx]]);
354
+ return previous;
355
+ }, []);
356
+
357
+ return uniqueSortedPairByFrequency(pairing)
358
+ .map(row => {
359
+ return {
360
+ label: row.left,
361
+ color: checkColorOrFallback(row.right)
362
+ };
363
+ })
364
+ .slice(0, limitOfNumberOfLegendEntries + 1); // Limit to limitOfNumberOfLegendEntries + 1, then limit to limitOfNumberOfLegendEntries in LegendManagers
365
+ }
366
+
345
367
  /**
346
368
  * Get a sample of rows to calculate layer boundaries
347
369
  */