Skip to content

Commit

Permalink
[FEAT] support domain.domainStops in layer color, render color legend…
Browse files Browse the repository at this point in the history
… based on zoom (#2815)

* use layer.getColorScale in color legend
* add zoom to attribute accessors
* layer color legend by zoom and improve legend number format

Signed-off-by: Ihor Dykhta <[email protected]>
Co-authored-by: Ilya Boyandin <[email protected]>
  • Loading branch information
igorDykhta and ilyabo authored Dec 10, 2024
1 parent 3a4feac commit c4d1cff
Show file tree
Hide file tree
Showing 15 changed files with 579 additions and 151 deletions.
28 changes: 17 additions & 11 deletions src/components/src/common/color-legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {Reset} from './icons';
import {InlineInput} from './styled-components';

const ROW_H = 15;
const GAP = 4;
const GAP = 2;
const RECT_W = 20;

const stopClickPropagation = e => e.stopPropagation();
Expand Down Expand Up @@ -224,15 +224,15 @@ function overrideByCustomLegend({colorLegends, currentLegends}: OverrideByCustom
}

export function useLayerColorLegends(
layer,
scaleType,
domain,
range,
isFixed,
fieldType,
labelFormat,
mapState
) {
layer: ColorLegendProps['layer'],
scaleType: ColorLegendProps['scaleType'],
domain: ColorLegendProps['domain'],
range: ColorLegendProps['range'],
isFixed: ColorLegendProps['isFixed'],
fieldType: ColorLegendProps['fieldType'],
labelFormat: ColorLegendProps['labelFormat'],
mapState: ColorLegendProps['mapState']
): Legend[] {
const scale = useMemo(
() => getLayerColorScale({range, domain, scaleType, isFixed, layer}),
[range, domain, scaleType, isFixed, layer]
Expand Down Expand Up @@ -260,7 +260,7 @@ export function useLayerColorLegends(
return LegendsWithCustomLegends;
}

type ColorLegendProps = {
export type ColorLegendProps = {
layer: Layer;
scaleType: string;
domain: number[] | string[];
Expand All @@ -274,6 +274,12 @@ type ColorLegendProps = {
onUpdateColorLegend?: (colorLegends: {[key: HexColor]: string}) => void;
};

export type Legend = {
data: string;
label: string;
override?: boolean;
};

ColorLegendFactory.deps = [LegendRowFactory];
function ColorLegendFactory(LegendRow: ReturnType<typeof LegendRowFactory>) {
const ColorLegend: React.FC<ColorLegendProps> = ({
Expand Down
52 changes: 37 additions & 15 deletions src/components/src/map/layer-hover-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@

import React, {useMemo} from 'react';
import styled from 'styled-components';
import {TooltipField} from '@kepler.gl/types';
import {CompareType, Field, Merge, TooltipField} from '@kepler.gl/types';
import {CenterFlexbox} from '../common/styled-components';
import {Layers} from '../common/icons';
import PropTypes from 'prop-types';
import {notNullorUndefined} from '@kepler.gl/common-utils';
import {DataRow} from '@kepler.gl/utils';
import {Layer} from '@kepler.gl/layers';
import {
AggregationLayerHoverData,
LayerHoverProp,
getTooltipDisplayDeltaValue,
getTooltipDisplayValue
} from '@kepler.gl/reducers';
import {useIntl} from 'react-intl';
import {VisState} from '@kepler.gl/schemas';
import {capitalizeFirstLetter} from '@kepler.gl/utils';

export const StyledLayerName = styled(CenterFlexbox)`
Expand Down Expand Up @@ -99,36 +102,55 @@ const Row: React.FC<RowProps> = ({name, value, deltaValue, url}) => {
);
};

const EntryInfo = ({fieldsToShow, fields, data, primaryData, compareType}) => (
export type EntryInfoProps = Merge<LayerHoverProp, {fieldsToShow: TooltipField[]}>;

const EntryInfo: React.FC<EntryInfoProps> = ({fieldsToShow, ...props}) => (
<tbody>
{fieldsToShow.map(item => (
<EntryInfoRow
key={item.name}
item={item}
fields={fields}
data={data}
primaryData={primaryData}
compareType={compareType}
/>
<EntryInfoRow key={item.name} item={item} {...props} />
))}
</tbody>
);

const EntryInfoRow = ({item, fields, data, primaryData, compareType}) => {
export type EntryInfoRowProps = {
data: LayerHoverProp['data'];
fields: Field[];
layer: Layer;
primaryData?: LayerHoverProp['primaryData'];
compareType?: CompareType;
currentTime?: VisState['animationConfig']['currentTime'];
item: TooltipField;
};

const EntryInfoRow: React.FC<EntryInfoRowProps> = ({
layer,
item,
fields,
data,
primaryData,
compareType,
currentTime
}) => {
const fieldIdx = fields.findIndex(f => f.name === item.name);
if (fieldIdx < 0) {
return null;
}
const field = fields[fieldIdx];
const value = data.valueAt(fieldIdx);
const fieldValueAccessor = layer.accessVSFieldValue(field, currentTime);
const value = fieldValueAccessor(field, data instanceof DataRow ? {index: data._rowIndex} : data);
const primaryValue = primaryData
? fieldValueAccessor(
field,
primaryData instanceof DataRow ? {index: primaryData._rowIndex} : primaryData
)
: null;
const displayValue = getTooltipDisplayValue({item, field, value});

const displayDeltaValue = primaryData
? getTooltipDisplayDeltaValue({
field,
data,
fieldIdx,
primaryData,
value,
primaryValue,
compareType
})
: null;
Expand Down
11 changes: 9 additions & 2 deletions src/components/src/map/map-legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export type LayerColorLegendProps = {
onLayerVisConfigChange?: (oldLayer: Layer, newVisConfig: Partial<LayerVisConfig>) => void;
layer: Layer;
disableEdit?: boolean;
mapState?: MapState;
};

LayerColorLegendFactory.deps = [ColorLegendFactory, SingleColorLegendFactory];
Expand All @@ -131,11 +132,14 @@ export function LayerColorLegendFactory(
layer,
colorChannel,
disableEdit,
onLayerVisConfigChange
onLayerVisConfigChange,
mapState
}) => {
const enableColorBy = description.measure;
const {scale, field, domain, range, property} = colorChannel;
const {scale, field, domain, range, property, fixed} = colorChannel;
const [colorScale, colorField, colorDomain] = [scale, field, domain].map(k => config[k]);
const isFixed = fixed && config.visConfig[fixed];

const colorRange = config.visConfig[range];
const onUpdateColorLegend = useCallback(
colorLegends => {
Expand Down Expand Up @@ -166,6 +170,8 @@ export function LayerColorLegendFactory(
range={colorRange}
onUpdateColorLegend={onUpdateColorLegend}
disableEdit={disableEdit}
isFixed={isFixed}
mapState={mapState}
/>
) : (
<SingleColorLegend
Expand Down Expand Up @@ -304,6 +310,7 @@ export function LayerLegendContentFactory(
description={layer.getVisualChannelDescription(colorChannel.key)}
layer={layer}
disableEdit={disableEdit}
mapState={mapState}
onLayerVisConfigChange={onLayerVisConfigChange}
/>
))}
Expand Down
91 changes: 78 additions & 13 deletions src/layers/src/base-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ import {
hasColorMap,
hexToRgb,
isPlainObject,
reverseColorRange
reverseColorRange,
isDomainStops
} from '@kepler.gl/utils';
import {generateHashId, toArray, notNullorUndefined} from '@kepler.gl/common-utils';
import {Datasets, GpuFilter, KeplerTable} from '@kepler.gl/table';
Expand All @@ -68,6 +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';

export type VisualChannelDomain = number[] | string[];
export type VisualChannelField = Field | null;
Expand Down Expand Up @@ -229,6 +232,11 @@ export type BaseLayerConstructorProps = {
id?: string;
} & LayerBaseConfigPartial;

export type GetVisChannelScaleReturnType = {
(z: number): any;
byZoom?: boolean;
} | null;

class Layer {
id: string;
meta: Record<string, any>;
Expand Down Expand Up @@ -1049,7 +1057,11 @@ class Layer {
);
}

getColorScale(colorScale: string, colorDomain: VisualChannelDomain, colorRange: ColorRange) {
getColorScale(
colorScale: string,
colorDomain: VisualChannelDomain,
colorRange: ColorRange
): GetVisChannelScaleReturnType {
if (hasColorMap(colorRange) && colorScale === SCALE_TYPES.custom) {
const cMap = new Map();
colorRange.colorMap?.forEach(([k, v]) => {
Expand All @@ -1061,11 +1073,14 @@ class Layer {
const scale = getScaleFunction(scaleType, cMap.values(), cMap.keys(), false);
scale.unknown(cMap.get(UNKNOWN_COLOR_KEY) || NO_VALUE_COLOR);

return scale;
return scale as () => any;
}
return this.getVisChannelScale(colorScale, colorDomain, colorRange.colors.map(hexToRgb));
}

accessVSFieldValue(field, indexKey) {
return defaultGetFieldValue;
}
/**
* Mapping from visual channels to deck.gl accesors
* @param {Object} param Parameters
Expand All @@ -1075,10 +1090,12 @@ class Layer {
*/
getAttributeAccessors({
dataAccessor = defaultDataAccessor,
dataContainer
dataContainer,
indexKey
}: {
dataAccessor?: typeof defaultDataAccessor;
dataContainer: DataContainerInterface;
indexKey?: number;
}) {
const attributeAccessors: {[key: string]: any} = {};

Expand Down Expand Up @@ -1116,14 +1133,35 @@ class Layer {
isFixed
);

attributeAccessors[accessor] = d =>
this.getEncodedChannelValue(
// @ts-ignore
scaleFunction,
dataAccessor(dataContainer)(d),
this.config[field],
nullValue
);
const getFieldValue = this.accessVSFieldValue(this.config[field], indexKey);

if (scaleFunction) {
attributeAccessors[accessor] = scaleFunction.byZoom
? memoize(z => {
const scaleFunc = scaleFunction(z);
return d =>
this.getEncodedChannelValue(
scaleFunc,
dataAccessor(dataContainer)(d),
this.config[field],
nullValue,
getFieldValue
);
})
: d =>
this.getEncodedChannelValue(
scaleFunction,
dataAccessor(dataContainer)(d),
this.config[field],
nullValue,
getFieldValue
);

// set getFillColorByZoom to true
if (scaleFunction.byZoom) {
attributeAccessors[`${accessor}ByZoom`] = true;
}
}
} else if (typeof getAttributeValue === 'function') {
attributeAccessors[accessor] = getAttributeValue(this.config);
} else {
Expand All @@ -1145,7 +1183,34 @@ class Layer {
domain: VisualChannelDomain,
range: any,
fixed?: boolean
): () => any | null {
): GetVisChannelScaleReturnType {
if (isDomainStops(domain)) {
// color is based on zoom
const zSteps = domain.z;
// get scale function by z
// {
// z: [z, z, z],
// stops: [[min, max], [min, max]],
// interpolation: 'interpolate'
// }

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];
}

return SCALE_FUNC[fixed ? 'linear' : scale]()
.domain(scaleDomain)
.range(fixed ? scaleDomain : range);
};
getScale.byZoom = true;
return getScale;
}

return SCALE_FUNC[fixed ? 'linear' : scale]()
.domain(domain)
.range(fixed ? domain : range);
Expand Down
Loading

0 comments on commit c4d1cff

Please sign in to comment.