From bded7af76bf066d3bb7e8d6184f2dead67c3f6c5 Mon Sep 17 00:00:00 2001 From: Igor Dykhta Date: Wed, 11 Dec 2024 00:55:56 +0200 Subject: [PATCH] [Feat] handle layer color scale by field.domainQuantiles (#2829) * handle layer domain quantile * add threshold scale Signed-off-by: Ihor Dykhta Co-authored-by: Ilya Boyandin --- src/layers/src/base-layer.ts | 36 +++++++++------- src/utils/src/data-scale-utils.ts | 10 ++--- src/utils/src/index.ts | 5 ++- test/node/utils/data-scale-utils-test.js | 53 +++++++++++++++++++++++- 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/src/layers/src/base-layer.ts b/src/layers/src/base-layer.ts index c9b27c583d..dc08118a56 100644 --- a/src/layers/src/base-layer.ts +++ b/src/layers/src/base-layer.ts @@ -69,8 +69,8 @@ import { ValueOf } from '@kepler.gl/types'; import {getScaleFunction, initializeLayerColorMap} from '@kepler.gl/utils'; -import {bisectLeft} from 'd3-array'; import memoize from 'lodash.memoize'; +import {isDomainQuantile, getDomainStepsbyZoom, getThresholdsFromQuantiles} from '@kepler.gl/utils'; export type VisualChannelDomain = number[] | string[]; export type VisualChannelField = Field | null; @@ -198,6 +198,7 @@ const dataFilterExtension = new DataFilterExtension({ // eslint-disable-next-line @typescript-eslint/no-unused-vars const defaultDataAccessor = dc => d => d; +const identity = d => d; // Can't use fiedValueAccesor because need the raw data to render tooltip // SHAN: Revisit here export const defaultGetFieldValue = (field, d) => field.valueAccessor(d); @@ -1184,7 +1185,20 @@ class Layer { range: any, fixed?: boolean ): GetVisChannelScaleReturnType { - if (isDomainStops(domain)) { + // if quantile is provided per zoom + if (isDomainQuantile(domain) && scale === SCALE_TYPES.quantile) { + const zSteps = domain.z; + + const getScale = function getScaleByZoom(z) { + const scaleDomain = getDomainStepsbyZoom(domain.quantiles, zSteps, z); + const thresholds = getThresholdsFromQuantiles(scaleDomain, range.length); + + return getScaleFunction('threshold', range, thresholds, false); + }; + + getScale.byZoom = true; + return getScale; + } else if (isDomainStops(domain)) { // color is based on zoom const zSteps = domain.z; // get scale function by z @@ -1195,18 +1209,11 @@ class Layer { // } const getScale = function getScaleByZoom(z) { - let scaleDomain; - const i = bisectLeft(zSteps, z); - if (i === 0) { - scaleDomain = domain.stops[0]; - } else { - scaleDomain = domain.stops[i - 1]; - } + const scaleDomain = getDomainStepsbyZoom(domain.stops, zSteps, z); - return SCALE_FUNC[fixed ? 'linear' : scale]() - .domain(scaleDomain) - .range(fixed ? scaleDomain : range); + return getScaleFunction(scale, range, scaleDomain, fixed); }; + getScale.byZoom = true; return getScale; } @@ -1218,13 +1225,10 @@ class Layer { /** * Get longitude and latitude bounds of the data. - * @param {import('utils/table-utils/data-container-interface').DataContainerInterface} dataContainer DataContainer to calculate bounds for. - * @param {(d: {index: number}, dc: import('utils/table-utils/data-container-interface').DataContainerInterface) => number[]} getPosition Access kepler.gl layer data from deck.gl layer - * @return {number[]|null} bounds of the data. */ getPointsBounds( dataContainer: DataContainerInterface, - getPosition?: (x: any, dc: DataContainerInterface) => number[] + getPosition: (x: any, dc: DataContainerInterface) => number[] = identity ): number[] | null { // no need to loop through the entire dataset // get a sample of data to calculate bounds diff --git a/src/utils/src/data-scale-utils.ts b/src/utils/src/data-scale-utils.ts index 35cb2420c0..78f9310da1 100644 --- a/src/utils/src/data-scale-utils.ts +++ b/src/utils/src/data-scale-utils.ts @@ -126,20 +126,20 @@ export function getThresholdsFromQuantiles( /** * get the domain at zoom - * @type {typeof import('./data-scale-utils').getDomainStepsbyZoom} */ export function getDomainStepsbyZoom(domain: any[], steps: number[], z: number): any { const i = bisectLeft(steps, z); - if (i === 0) { - return domain[0]; + if (steps[i] === z) { + // If z is an integer value exactly matching a step, return the corresponding domain + return domain[i]; } - return domain[i - 1]; + // Otherwise, return the next coarsest domain + return domain[Math.max(i - 1, 0)]; } /** * Get d3 scale function - * @type {typeof import('./data-scale-utils').getScaleFunction} */ export function getScaleFunction( scale: string, diff --git a/src/utils/src/index.ts b/src/utils/src/index.ts index 5e7db3a2b8..000dfdce37 100644 --- a/src/utils/src/index.ts +++ b/src/utils/src/index.ts @@ -134,7 +134,10 @@ export { getVisualChannelScaleByZoom, initializeLayerColorMap, isNumericColorBreaks, - isDomainStops + isDomainStops, + isDomainQuantile, + getDomainStepsbyZoom, + getThresholdsFromQuantiles } from './data-scale-utils'; export type {ColorBreak, ColorBreakOrdinal, DomainQuantiles, DomainStops} from './data-scale-utils'; diff --git a/test/node/utils/data-scale-utils-test.js b/test/node/utils/data-scale-utils-test.js index 77e217ae22..4894dc0402 100644 --- a/test/node/utils/data-scale-utils-test.js +++ b/test/node/utils/data-scale-utils-test.js @@ -8,7 +8,9 @@ import { getQuantileDomain, getLinearDomain, getLogDomain, - createDataContainer + createDataContainer, + getThresholdsFromQuantiles, + getDomainStepsbyZoom } from '@kepler.gl/utils'; function numberSort(a, b) { @@ -111,3 +113,52 @@ test('DataScaleUtils -> getLogDomain', t => { t.end(); }); + +test('DataScaleUtils -> getThresholdsFromQuantiles', t => { + t.deepEqual( + getThresholdsFromQuantiles([0, 1, 2, 3, 4, 5], 3), + [1.6666666666666665, 3.333333333333333], + 'should get correct thresholds from quantiles' + ); + + t.deepEqual( + getThresholdsFromQuantiles([0, 1, 2, 3, 4, 5], 1), + [], + 'should get correct thresholds from quantiles' + ); + + t.deepEqual( + getThresholdsFromQuantiles([0, 1, 2, 3, 4, 5], undefined), + [0, 5], + 'should get correct thresholds from quantiles' + ); + t.end(); +}); + +test('DataScaleUtils -> getDomainStepsbyZoom', t => { + const domain = [ + [0, 1], + [0, 2], + [0, 3] + ]; + const steps = [0, 2, 4]; + [ + {z: 0, expected: [0, 1]}, + {z: 0.5, expected: [0, 1]}, + {z: 1, expected: [0, 1]}, + {z: 1.2, expected: [0, 1]}, + {z: 2, expected: [0, 2]}, + {z: 3.5, expected: [0, 2]}, + {z: 4, expected: [0, 3]}, + {z: 4.5, expected: [0, 3]}, + {z: 10, expected: [0, 3]} + ].forEach(({z, expected}) => { + t.deepEqual( + getDomainStepsbyZoom(domain, steps, z), + expected, + `should get correct domain from zoom ${z}` + ); + }); + + t.end(); +});