Skip to content

Commit 65d42d0

Browse files
ilyaboigorDykhta
authored andcommitted
[Feat] Add custom color scale for aggregate layers
Signed-off-by: Ihor Dykhta <[email protected]>
1 parent 12b3231 commit 65d42d0

File tree

21 files changed

+324
-127
lines changed

21 files changed

+324
-127
lines changed

src/components/src/common/column-stats-chart.tsx

Lines changed: 28 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
77
import styled from 'styled-components';
88

99
import {KeplerTable} from '@kepler.gl/table';
10-
import {Field} from '@kepler.gl/types';
10+
import {Bin, Field} from '@kepler.gl/types';
1111
import {
1212
ColorBreak,
1313
ColorBreakOrdinal,
14-
histogramFromThreshold,
15-
histogramFromValues,
1614
isNumber,
1715
isNumericColorBreaks,
1816
useDimensions
@@ -29,7 +27,6 @@ const COLOR_CHART_TICK_WRAPPER_HEIGHT = 10;
2927
const COLOR_CHART_TICK_HEIGHT = 8;
3028
const COLOR_CHART_TICK_WIDTH = 4;
3129
const COLOR_CHART_TICK_BORDER_COLOR = '#999999';
32-
const HISTOGRAM_BINS = 30;
3330

3431
const StyledContainer = styled.div.attrs({
3532
className: 'color-chart-loading'
@@ -208,21 +205,29 @@ export type ColumnStatsChartWLoadingProps = {
208205
colorField: Field;
209206
dataset: KeplerTable;
210207
colorBreaks: ColorBreak[] | ColorBreakOrdinal[] | null;
208+
allBins: Bin[];
209+
filteredBins: Bin[];
210+
isFiltered: boolean;
211+
histogramDomain: number[];
211212
onChangedUpdater: (ticks: ColorBreak[]) => void;
212213
};
213214

214215
export type ColumnStatsChartProps = {
215-
field: Field;
216-
dataset: KeplerTable;
216+
allBins: Bin[];
217+
filteredBins: Bin[];
218+
isFiltered: boolean;
219+
histogramDomain: number[];
217220
colorBreaks: ColorBreak[];
218221
onChangedUpdater: (ticks: ColorBreak[]) => void;
219222
};
220223
function ColumnStatsChartFactory(
221224
HistogramPlot: ReturnType<typeof HistogramPlotFactory>
222225
): React.FC<ColumnStatsChartWLoadingProps> {
223226
const ColumnStatsChart: React.FC<ColumnStatsChartProps> = ({
224-
field,
225-
dataset,
227+
allBins,
228+
filteredBins,
229+
isFiltered,
230+
histogramDomain,
226231
colorBreaks,
227232
onChangedUpdater
228233
}) => {
@@ -241,38 +246,6 @@ function ColumnStatsChartFactory(
241246
isTickChangingRef.current = false;
242247
}, [ticks]);
243248

244-
const valueAccessor = useMemo(() => {
245-
return idx => dataset.getValue(field.name, idx);
246-
}, [dataset, field.name]);
247-
248-
const columnStats = field.filterProps?.columnStats;
249-
250-
// get bins with allIndexes
251-
const allBins = useMemo(() => {
252-
if (columnStats?.bins) {
253-
return columnStats?.bins;
254-
}
255-
return histogramFromValues(dataset.allIndexes, HISTOGRAM_BINS, valueAccessor);
256-
}, [columnStats, dataset.allIndexes, valueAccessor]);
257-
258-
const isFiltered = dataset.filteredIndexForDomain.length !== dataset.allIndexes.length;
259-
260-
// get filteredBins
261-
const filteredBins = useMemo(() => {
262-
if (!isFiltered) {
263-
return allBins;
264-
}
265-
// get threholds
266-
const filterEmptyBins = false;
267-
const threholds = allBins.map(b => b.x0);
268-
return histogramFromThreshold(
269-
threholds,
270-
dataset.filteredIndexForDomain,
271-
valueAccessor,
272-
filterEmptyBins
273-
);
274-
}, [dataset, valueAccessor, allBins, isFiltered]);
275-
276249
// histograms used by histogram-plot.js
277250
const histogramsByGroup = useMemo(
278251
() => ({
@@ -282,34 +255,6 @@ function ColumnStatsChartFactory(
282255
[allBins, filteredBins]
283256
);
284257

285-
// get domain (min, max) of histogram
286-
const histogramDomain = useMemo(() => {
287-
if (columnStats && columnStats.quantiles && columnStats.mean) {
288-
// no need to recalcuate min/max/mean if its already in columnStats
289-
return [
290-
columnStats.quantiles[0].value,
291-
columnStats.quantiles[columnStats.quantiles.length - 1].value,
292-
columnStats.mean
293-
];
294-
}
295-
let domainMin = Number.POSITIVE_INFINITY;
296-
let domainMax = Number.NEGATIVE_INFINITY;
297-
let nValid = 0;
298-
let domainSum = 0;
299-
dataset.allIndexes.forEach((x, i) => {
300-
const val = valueAccessor(x);
301-
if (isNumber(val)) {
302-
if (val < domainMin) domainMin = val;
303-
if (val > domainMax) domainMax = val;
304-
domainSum += val;
305-
nValid += 1;
306-
}
307-
});
308-
const histogramMean = nValid > 0 ? domainSum / nValid : 0;
309-
310-
return [domainMin, domainMax, histogramMean];
311-
}, [dataset, valueAccessor, columnStats]);
312-
313258
// get colors from colorBreaks
314259
const domainColors = useMemo(
315260
() => (colorBreaks ? colorBreaks.map(c => c.data) : []),
@@ -417,19 +362,24 @@ function ColumnStatsChartFactory(
417362
colorField,
418363
dataset,
419364
colorBreaks,
365+
allBins,
366+
filteredBins,
367+
isFiltered,
368+
histogramDomain,
420369
onChangedUpdater
421370
}) => {
422-
const fieldName = colorField.name;
423-
const field = useMemo(() => dataset.getColumnField(fieldName), [dataset, fieldName]);
424-
425-
const isLoading = field?.isLoadingStats;
371+
const fieldName = colorField?.name;
372+
const field = useMemo(() => (fieldName ? dataset.getColumnField(fieldName) : null), [
373+
dataset,
374+
fieldName
375+
]);
426376

427-
if (!isNumericColorBreaks(colorBreaks) || !field) {
377+
if (!isNumericColorBreaks(colorBreaks)) {
428378
// TODO: implement display for ordinal breaks
429379
return null;
430380
}
431381

432-
if (isLoading) {
382+
if (field?.isLoadingStats) {
433383
return (
434384
<StyledContainer>
435385
<LoadingSpinner />
@@ -440,8 +390,10 @@ function ColumnStatsChartFactory(
440390
return (
441391
<ColumnStatsChart
442392
colorBreaks={colorBreaks}
443-
field={field}
444-
dataset={dataset}
393+
allBins={allBins}
394+
filteredBins={filteredBins}
395+
isFiltered={isFiltered}
396+
histogramDomain={histogramDomain}
445397
onChangedUpdater={onChangedUpdater}
446398
/>
447399
);

src/components/src/map-container.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import {
3030
updateMapboxLayers,
3131
LayerBaseConfig,
3232
VisualChannelDomain,
33-
EditorLayerUtils
33+
EditorLayerUtils,
34+
AggregatedBin
3435
} from '@kepler.gl/layers';
3536
import {MapState, MapControls, Viewport, SplitMap, SplitMapLayers} from '@kepler.gl/types';
3637
import {
@@ -465,9 +466,13 @@ export default function MapContainerFactory(
465466
this.props.visStateActions.onLayerHover(info, this.props.index);
466467
};
467468

468-
_onLayerSetDomain = (idx: number, colorDomain: {domain: VisualChannelDomain}) => {
469+
_onLayerSetDomain = (
470+
idx: number,
471+
value: {domain: VisualChannelDomain; aggregatedBins: AggregatedBin[]}
472+
) => {
469473
this.props.visStateActions.layerConfigChange(this.props.visState.layers[idx], {
470-
colorDomain: colorDomain.domain
474+
colorDomain: value.domain,
475+
aggregatedBins: value.aggregatedBins
471476
} as Partial<LayerBaseConfig>);
472477
};
473478

src/components/src/side-panel/layer-panel/aggr-scale-selector.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import DimensionScaleSelectorFactory from './dimension-scale-selector';
66

77
AggrScaleSelectorFactory.deps = [DimensionScaleSelectorFactory];
88
export function AggrScaleSelectorFactory(DimensionScaleSelector) {
9-
const AggrScaleSelector = ({channel, layer, onChange, setColorUI, label}) => {
9+
const AggrScaleSelector = ({channel, dataset, layer, onChange, setColorUI, label}) => {
1010
const {key} = channel;
1111
const scaleOptions = layer.getScaleOptions(key);
1212

1313
return Array.isArray(scaleOptions) && scaleOptions.length > 1 ? (
1414
<DimensionScaleSelector
15+
dataset={dataset}
1516
layer={layer}
1617
channel={channel}
1718
label={label || `${key} Scale`}

src/components/src/side-panel/layer-panel/color-breaks-panel.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import {SCALE_TYPES} from '@kepler.gl/constants';
55
import {KeplerTable} from '@kepler.gl/table';
6-
import {ColorUI, Field} from '@kepler.gl/types';
6+
import {Bin, ColorUI, Field} from '@kepler.gl/types';
77
import {
88
ColorBreak,
99
ColorBreakOrdinal,
@@ -83,6 +83,10 @@ export type ColorBreaksPanelProps = {
8383
dataset: KeplerTable | undefined;
8484
colorField: Field;
8585
isCustomBreaks: boolean;
86+
allBins: Bin[];
87+
filteredBins: Bin[];
88+
isFiltered: boolean;
89+
histogramDomain: number[];
8690
setColorUI: SetColorUIFunc;
8791
onScaleChange: (v: string, visConfg?: Record<string, any>) => void;
8892
onApply: (e: React.MouseEvent) => void;
@@ -101,6 +105,10 @@ function ColorBreaksPanelFactory(
101105
dataset,
102106
colorField,
103107
isCustomBreaks,
108+
allBins,
109+
filteredBins,
110+
isFiltered,
111+
histogramDomain,
104112
setColorUI,
105113
onScaleChange,
106114
onApply,
@@ -168,6 +176,10 @@ function ColorBreaksPanelFactory(
168176
colorField={colorField}
169177
dataset={dataset}
170178
colorBreaks={currentBreaks}
179+
allBins={allBins}
180+
filteredBins={filteredBins}
181+
isFiltered={isFiltered}
182+
histogramDomain={histogramDomain}
171183
onChangedUpdater={onColumnStatsChartChanged}
172184
/>
173185
) : null}

src/components/src/side-panel/layer-panel/color-scale-selector.tsx

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
import React, {useCallback, useMemo, useState} from 'react';
55
import styled from 'styled-components';
66

7-
import {SCALE_TYPES} from '@kepler.gl/constants';
8-
import {Layer, VisualChannelDomain} from '@kepler.gl/layers';
7+
import {ALL_FIELD_TYPES, SCALE_TYPES} from '@kepler.gl/constants';
8+
import {AggregatedBin, Layer, VisualChannelDomain} from '@kepler.gl/layers';
99
import {KeplerTable} from '@kepler.gl/table';
1010
import {ColorRange, ColorUI, Field, NestedPartial} from '@kepler.gl/types';
1111
import {
1212
colorBreaksToColorMap,
1313
getLayerColorScale,
1414
getLegendOfScale,
15+
histogramFromValues,
16+
histogramFromThreshold,
17+
getHistogramDomain,
1518
hasColorMap
1619
} from '@kepler.gl/utils';
1720

@@ -25,6 +28,8 @@ import Typeahead from '../../common/item-selector/typeahead';
2528

2629
type TippyInstance = any; // 'tippy-js'
2730

31+
const HISTOGRAM_BINS = 30;
32+
2833
export type ScaleOption = {
2934
label: string;
3035
value: string;
@@ -50,6 +55,7 @@ export type ColorScaleSelectorProps = {
5055
searchable: boolean;
5156
displayOption: string;
5257
getOptionValue: string;
58+
aggregatedBins?: AggregatedBin[];
5359
};
5460

5561
const DropdownPropContext = React.createContext({});
@@ -126,6 +132,7 @@ function ColorScaleSelectorFactory(
126132
onSelect,
127133
scaleType,
128134
domain,
135+
aggregatedBins,
129136
range,
130137
setColorUI,
131138
colorUIConfig,
@@ -151,10 +158,62 @@ function ColorScaleSelectorFactory(
151158

152159
const colorBreaks = useMemo(
153160
() =>
154-
colorScale ? getLegendOfScale({scale: colorScale, scaleType, fieldType: field.type}) : null,
155-
[colorScale, scaleType, field.type]
161+
colorScale
162+
? getLegendOfScale({
163+
scale: colorScale,
164+
scaleType,
165+
fieldType: field?.type ?? ALL_FIELD_TYPES.real
166+
})
167+
: null,
168+
[colorScale, scaleType, field?.type]
156169
);
157170

171+
const columnStats = field?.filterProps?.columnStats;
172+
173+
const fieldValueAccessor = useMemo(() => {
174+
return field
175+
? idx => dataset.getValue(field.name, idx)
176+
: idx => dataset.dataContainer.rowAsArray(idx);
177+
}, [dataset, field]);
178+
179+
// aggregatedBins should be the raw data
180+
const allBins = useMemo(() => {
181+
if (aggregatedBins) {
182+
return histogramFromValues(
183+
Object.values(aggregatedBins).map(bin => bin.i),
184+
HISTOGRAM_BINS,
185+
idx => aggregatedBins[idx].value
186+
);
187+
}
188+
return columnStats?.bins
189+
? columnStats?.bins
190+
: histogramFromValues(dataset.allIndexes, HISTOGRAM_BINS, fieldValueAccessor);
191+
}, [aggregatedBins, columnStats, dataset, fieldValueAccessor]);
192+
193+
const histogramDomain = useMemo(() => {
194+
return getHistogramDomain({aggregatedBins, columnStats, dataset, fieldValueAccessor});
195+
}, [dataset, fieldValueAccessor, aggregatedBins, columnStats]);
196+
197+
const isFiltered = aggregatedBins
198+
? false
199+
: dataset.filteredIndexForDomain.length !== dataset.allIndexes.length;
200+
201+
// get filteredBins (not apply to aggregate layer)
202+
const filteredBins = useMemo(() => {
203+
if (!isFiltered) {
204+
return allBins;
205+
}
206+
// get threholds
207+
const filterEmptyBins = false;
208+
const threholds = allBins.map(b => b.x0);
209+
return histogramFromThreshold(
210+
threholds,
211+
dataset.filteredIndexForDomain,
212+
fieldValueAccessor,
213+
filterEmptyBins
214+
);
215+
}, [dataset, fieldValueAccessor, allBins, isFiltered]);
216+
158217
const onSelectScale = useCallback(
159218
val => {
160219
// highlight selected option
@@ -221,6 +280,10 @@ function ColorScaleSelectorFactory(
221280
colorUIConfig,
222281
colorBreaks,
223282
isCustomBreaks,
283+
allBins,
284+
filteredBins,
285+
isFiltered,
286+
histogramDomain,
224287
onScaleChange: onSelect,
225288
onApply,
226289
onCancel

src/components/src/side-panel/layer-panel/dimension-scale-selector.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ function DimensionScaleSelectorFactory(
5151
value: op
5252
}));
5353
const disabled = scaleOptions.length < 2;
54-
const isColorScale = channelScaleType === CHANNEL_SCALES.color;
54+
const isColorScale =
55+
channelScaleType === CHANNEL_SCALES.color ||
56+
(layer.config.aggregatedBins && channelScaleType === CHANNEL_SCALES.colorAggr);
5557

5658
const onSelect = useCallback(
5759
(val, newRange) => onChange({[scale]: val}, key, newRange ? {[range]: newRange} : undefined),
@@ -87,6 +89,7 @@ function DimensionScaleSelectorFactory(
8789
onSelect={onSelect}
8890
scaleType={scaleType}
8991
domain={layer.config[domain]}
92+
aggregatedBins={layer.config.aggregatedBins}
9093
range={layer.config.visConfig[range]}
9194
setColorUI={_setColorUI}
9295
colorUIConfig={layer.config.colorUI?.[range]}

0 commit comments

Comments
 (0)