Skip to content

Commit

Permalink
[Feat] Custom color scale for categorical/ordinal field (#2880)
Browse files Browse the repository at this point in the history
- [feat] Implement custom color scale for categorical field
- [fix] ColorMap is not cleared when field or scale change
- [fix] Fix crash when custom ordinal scale does not have colorMap
- [fix] fix custom stroke color initiation

Signed-off-by: Ihor Dykhta <[email protected]>
  • Loading branch information
igorDykhta authored Jan 3, 2025
1 parent 23f6034 commit 4bcf55b
Show file tree
Hide file tree
Showing 19 changed files with 948 additions and 116 deletions.
4 changes: 2 additions & 2 deletions src/actions/src/vis-state-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function applyLayerConfig(

export type LayerConfigChangeUpdaterAction = {
oldLayer: Layer;
newConfig: Partial<LayerBaseConfig>;
newConfig: Partial<Layer['config']>;
};
/**
* Update layer base config: dataId, label, column, isVisible
Expand Down Expand Up @@ -184,7 +184,7 @@ export function layerTypeChange(
}
export type LayerVisualChannelConfigChangeUpdaterAction = {
oldLayer: Layer;
newConfig: Partial<LayerBaseConfig>;
newConfig: Partial<Layer['config']>;
channel: string;
newVisConfig?: Partial<LayerVisConfig>;
};
Expand Down
35 changes: 25 additions & 10 deletions src/components/src/common/color-legend.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import {ColorRange} from '@kepler.gl/types';
import React, {useCallback, useMemo} from 'react';
import styled, {css} from 'styled-components';

import {SCALE_TYPES} from '@kepler.gl/constants';
import {Layer} from '@kepler.gl/layers';
import {HexColor, MapState} from '@kepler.gl/types';
import {ColorRange, HexColor, MapState} from '@kepler.gl/types';
import {
getLayerColorScale,
getLegendOfScale,
getVisualChannelScaleByZoom,
colorMapToCategoricalColorBreaks,
isObject
} from '@kepler.gl/utils';
import React, {useCallback, useMemo} from 'react';
import styled, {css} from 'styled-components';

import {Reset} from './icons';
import {InlineInput} from './styled-components';

Expand Down Expand Up @@ -203,7 +206,7 @@ type OverrideByCustomLegendOptions = {
/**
* Original Legends
*/
currentLegends: ReturnType<typeof getLegendOfScale>;
currentLegends?: ReturnType<typeof getLegendOfScale>;
};

/**
Expand Down Expand Up @@ -243,10 +246,22 @@ export function useLayerColorLegends(
[scale, layer, mapState]
);

const currentLegends = useMemo(
() => getLegendOfScale({scale: scaleByZoom, scaleType, labelFormat, fieldType}),
[scaleByZoom, scaleType, labelFormat, fieldType]
);
const currentLegends = useMemo(() => {
if (scaleType === SCALE_TYPES.customOrdinal && range?.colorMap) {
const colorBreaks = colorMapToCategoricalColorBreaks(range.colorMap);
return colorBreaks?.map(cb => {
return {
data: cb.data,
label: Array.isArray(cb.label)
? cb.label.length > 5
? `${cb.label.length} selected`
: cb.label
: cb.label || ''
};
});
}
return getLegendOfScale({scale: scaleByZoom, scaleType, labelFormat, fieldType});
}, [range, scaleByZoom, scaleType, labelFormat, fieldType]);

const LegendsWithCustomLegends = useMemo(
() =>
Expand All @@ -257,7 +272,7 @@ export function useLayerColorLegends(
[range?.colorLegends, currentLegends]
);

return LegendsWithCustomLegends;
return LegendsWithCustomLegends || [];
}

export type ColorLegendProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface ChickletedInputProps {
error?: boolean;
placeholder?: string;
inputTheme?: string;
CustomChickletComponent?: ElementType;
CustomChickletComponent?: ElementType | null;
className?: string;
reorderItems?: (newOrder: any) => void;
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/src/common/styled-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ export const SidePanelDivider = styled.div.attrs({
height: ${props => props.theme.sidepanelDividerHeight}px;
`;

export const Tooltip = styled(ReactTooltip)`
type TooltipProps = {interactive?: boolean};
export const Tooltip = styled(ReactTooltip)<TooltipProps>`
&.__react_component_tooltip {
font-size: ${props => props.theme.tooltipFontSize};
font-weight: 400;
Expand Down Expand Up @@ -199,6 +200,7 @@ export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
disabled?: boolean;
width?: string;
inactive?: boolean;
size?: string;
};

// this needs to be an actual button to be able to set disabled attribute correctly
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import React from 'react';
import React, {useCallback} from 'react';

import {CHANNEL_SCALE_SUPPORTED_FIELDS} from '@kepler.gl/constants';
import {Layer, VisualChannel} from '@kepler.gl/layers';
import {KeplerTable} from '@kepler.gl/table';
import {ColorUI, Field, LayerVisConfig} from '@kepler.gl/types';
import {ColorUI, Field, LayerVisConfig, NestedPartial} from '@kepler.gl/types';

import DimensionScaleSelectorFactory from './dimension-scale-selector';
import VisConfigByFieldSelectorFactory from './vis-config-by-field-selector';
Expand All @@ -22,12 +22,7 @@ export type ChannelByValueSelectorProps = {
fields: Field[];
dataset: KeplerTable | undefined;
description?: string;
setColorUI: (
prop: string,
newConfig: {
[key in keyof ColorUI]: ColorUI[keyof ColorUI];
}
) => void;
setColorUI: (prop: string, newConfig: NestedPartial<ColorUI>) => void;
updateLayerVisConfig: (newConfig: Partial<LayerVisConfig>) => void;
disabled?: boolean;
};
Expand Down Expand Up @@ -58,6 +53,12 @@ export function ChannelByValueSelectorFactory(
const supportedFields = fields.filter(({type}) => channelSupportedFieldTypes.includes(type));
const showScale = !layer.isAggregated && layer.config[scale] && layer.config[field];
const defaultDescription = 'layerConfiguration.defaultDescription';
const updateField = useCallback(
val => {
onChange({[field]: val}, key);
},
[onChange, field, key]
);

return (
<div className="channel-by-value-selector">
Expand All @@ -70,7 +71,7 @@ export function ChannelByValueSelectorFactory(
disabled={disabled}
placeholder={defaultMeasure || 'placeholder.selectField'}
selectedField={layer.config[field]}
updateField={val => onChange({[field]: val}, key)}
updateField={updateField}
/>
{showScale && !disabled ? (
<DimensionScaleSelector
Expand Down
55 changes: 47 additions & 8 deletions src/components/src/side-panel/layer-panel/color-breaks-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import {toArray} from '@kepler.gl/common-utils';
import {SCALE_TYPES} from '@kepler.gl/constants';
import {KeplerTable} from '@kepler.gl/table';
import {Bin, ColorUI, Field} from '@kepler.gl/types';
import {Bin, ColorMap, ColorUI, Field} from '@kepler.gl/types';
import {
ColorBreak,
ColorBreakOrdinal,
colorBreaksToColorMap,
colorMapToColorBreaks,
isNumericColorBreaks
isNumericColorBreaks as notOrdinalColorBreaks
} from '@kepler.gl/utils';
import React, {useCallback, useMemo} from 'react';
import styled from 'styled-components';
Expand All @@ -20,6 +21,7 @@ import CustomPaletteFactory, {
ColorPaletteItem,
ColorSwatch,
EditableColorRange,
CategoricalSelector,
SetColorUIFunc
} from './custom-palette';

Expand Down Expand Up @@ -52,7 +54,7 @@ export type ColorBreaksDisplayProps = {
};

export const ColorBreaksDisplay: React.FC<ColorBreaksDisplayProps> = ({currentBreaks, onEdit}) => {
if (!isNumericColorBreaks(currentBreaks)) {
if (!notOrdinalColorBreaks(currentBreaks)) {
// don't display color breaks for ordinal breaks, user can change it in custom breaks
return null;
}
Expand All @@ -76,9 +78,35 @@ export const ColorBreaksDisplay: React.FC<ColorBreaksDisplayProps> = ({currentBr
);
};

/**
* ColorBreaksPanelProps
*/
export type CategoricalColorDisplayProps = {
colorMap?: ColorMap;
onEdit: (() => void) | null;
};

export const CategoricalColorDisplay: React.FC<CategoricalColorDisplayProps> = ({
colorMap,
onEdit
}: CategoricalColorDisplayProps) => {
return (
<StyledColorBreaksDisplay>
{onEdit ? <EditButton onClickEdit={onEdit} /> : null}
{colorMap?.map((cm, index) => (
<ColorPaletteItem className="disabled" key={index}>
<div className="custom-palette-input__left">
<ColorSwatch color={cm[1]} />
<CategoricalSelector
index={index}
selectedValues={toArray(cm[0])}
allValues={[]}
editable={false}
/>
</div>
</ColorPaletteItem>
))}
</StyledColorBreaksDisplay>
);
};

export type ColorBreaksPanelProps = {
colorBreaks: ColorBreak[] | ColorBreakOrdinal[] | null;
colorUIConfig: ColorUI;
Expand All @@ -89,6 +117,7 @@ export type ColorBreaksPanelProps = {
filteredBins: Bin[];
isFiltered: boolean;
histogramDomain: number[];
ordinalDomain: number[] | string[];
setColorUI: SetColorUIFunc;
onScaleChange: (v: string, visConfg?: Record<string, any>) => void;
onApply: (e: React.MouseEvent) => void;
Expand All @@ -101,6 +130,7 @@ function ColorBreaksPanelFactory(
CustomPalette: ReturnType<typeof CustomPaletteFactory>,
ColumnStatsChart: ReturnType<typeof ColumnStatsChartFactory>
): React.FC<ColorBreaksPanelProps> {
// eslint-disable-next-line complexity
const ColorBreaksPanel: React.FC<ColorBreaksPanelProps> = ({
colorBreaks,
colorUIConfig,
Expand All @@ -111,6 +141,7 @@ function ColorBreaksPanelFactory(
filteredBins,
isFiltered,
histogramDomain,
ordinalDomain,
setColorUI,
onScaleChange,
onApply,
Expand Down Expand Up @@ -173,7 +204,7 @@ function ColorBreaksPanelFactory(

return (
<ColorBreaksPanelWrapper>
{dataset && allBins.length > 1 ? (
{dataset && allBins.length > 1 && notOrdinalColorBreaks(colorBreaks) ? (
<ColumnStatsChart
colorField={colorField}
dataset={dataset}
Expand All @@ -188,17 +219,25 @@ function ColorBreaksPanelFactory(
<StyledColorBreaksPanel>
{isEditingCustomBreaks ? (
<CustomPalette
ordinalDomain={ordinalDomain}
customPalette={customPalette}
setColorPaletteUI={setColorUI}
showSketcher={showSketcher}
onApply={onApply}
onCancel={onCilckCancel}
/>
) : currentBreaks && allBins.length > 1 ? (
) : currentBreaks && allBins.length > 1 && notOrdinalColorBreaks(colorBreaks) ? (
<ColorBreaksDisplay
currentBreaks={currentBreaks}
onEdit={isCustomBreaks ? onClickEditCustomBreaks : null}
/>
) : customPalette.colorMap &&
customPalette.type === 'customOrdinal' &&
customPalette.name?.endsWith(colorField.name) ? (
<CategoricalColorDisplay
colorMap={customPalette.colorMap}
onEdit={isCustomBreaks ? onClickEditCustomBreaks : null}
/>
) : null}
</StyledColorBreaksPanel>
</ColorBreaksPanelWrapper>
Expand Down
Loading

0 comments on commit 4bcf55b

Please sign in to comment.