Skip to content

Commit

Permalink
[Feat] Redesign color range to use chormajs and d3 color function (#2835
Browse files Browse the repository at this point in the history
)

- Replace default color ranges with color palette object, with .colors(step), .linear function to auto generate colors / ramps
- Add more d3 color chroma palette
- Add more color blind safe palette
- Save and load color range from old config 

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
Co-authored-by: Ilya Boyandin <iboyandin@foursquare.com>
  • Loading branch information
igorDykhta and ilyabo authored Dec 11, 2024
1 parent bded7af commit 2153836
Show file tree
Hide file tree
Showing 36 changed files with 1,537 additions and 966 deletions.
11 changes: 8 additions & 3 deletions src/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export {LayerBlendingSelector, default as LayerManagerFactory} from './side-pane
export {default as ColorPalette} from './side-panel/layer-panel/color-palette';
export {
ALL_TYPES,
ColorPaletteGroup,
default as ColorRangeSelector,
PaletteConfig
} from './side-panel/layer-panel/color-range-selector';
Expand All @@ -101,8 +100,14 @@ export * from './side-panel/layer-panel/channel-by-value-selector';
export * from './side-panel/layer-panel/color-breaks-panel';
export * from './side-panel/layer-panel/color-range-selector';
export * from './side-panel/layer-panel/color-scale-selector';
export * from './side-panel/layer-panel/custom-palette';
export {default as CustomPalette} from './side-panel/layer-panel/custom-palette';
export {
AddColorStop,
default as CustomPalette,
DeleteColorStop,
ColorPaletteItem,
ColorSwatch,
EditableColorRange
} from './side-panel/layer-panel/custom-palette';
export {default as CustomPicker} from './side-panel/layer-panel/custom-picker';
export {default as DatasetLayerGroupFactory} from './side-panel/layer-panel/dataset-layer-group';
export {default as DatasetLayerSectionFactory} from './side-panel/layer-panel/dataset-layer-section';
Expand Down
6 changes: 1 addition & 5 deletions src/components/src/side-panel/common/dataset-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,7 @@ export default function DatasetTitleFactory(
top={-50}
onClose={_handleClosePicker}
>
<CustomPicker
color={rgbToHex(dataset.color)}
onChange={_handleCustomPicker}
onSwatchClose={_handleClosePicker}
/>
<CustomPicker color={rgbToHex(dataset.color)} onChange={_handleCustomPicker} />
</Portaled>
{showDatasetTable ? (
<CenterFlexbox className="source-data-arrow">
Expand Down
148 changes: 84 additions & 64 deletions src/components/src/side-panel/layer-panel/color-range-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import uniq from 'lodash.uniq';
import React, {MouseEvent, useCallback, useMemo} from 'react';
import styled from 'styled-components';

import {COLOR_RANGES, ColorRange} from '@kepler.gl/constants';
import {KEPLER_COLOR_PALETTES, PALETTE_TYPES, ColorRange, ColorPalette} from '@kepler.gl/constants';
import {FormattedMessage} from '@kepler.gl/localization';
import {ColorUI, NestedPartial} from '@kepler.gl/types';
import {hasColorMap, numberSort, reverseColorRange, updateColorRange} from '@kepler.gl/utils';
import {
hasColorMap,
updateColorRangeBySelectedPalette,
paletteIsSteps,
paletteIsType,
paletteIsColorBlindSafe
} from '@kepler.gl/utils';
import ItemSelector from '../../common/item-selector/item-selector';
import {PanelLabel, Tooltip} from '../../common/styled-components';
import Switch from '../../common/switch';
import ColorPalette from './color-palette';
import ColorPalettePanel from './color-palette';
import CustomPaletteFactory from './custom-palette';

import {capitalizeFirstLetter} from '@kepler.gl/utils';
import {range} from 'd3-array';

type ColorRangeSelectorProps = {
colorRanges?: ColorRange[];
colorPalettes?: ColorPalette[];
colorPaletteUI: ColorUI;
selectedColorRange: ColorRange;
onSelectColorRange: (p: ColorRange, e: MouseEvent) => void;
Expand All @@ -36,20 +43,10 @@ type PaletteConfigProps = {
reason?: string;
};

type ColorPaletteGroupProps = {
reversed?: boolean;
selected: ColorRange;
colorRanges: ColorRange[];
onSelect: (p: ColorRange, e: MouseEvent) => void;
};

export const ALL_TYPES: string[] = uniq(
COLOR_RANGES.map(c => c.type)
.filter(ctype => ctype)
.concat(['all', 'custom']) as string[]
);

export const ALL_STEPS: number[] = uniq(COLOR_RANGES.map(d => d.colors.length)).sort(numberSort);
// @ts-ignore cant concat 'all' to PALETTE_TYPES values
export const ALL_TYPES = Object.values(PALETTE_TYPES).concat(['all']);
const MAX_STEPS = 20;
const ALL_STEPS = range(2, MAX_STEPS + 1, 1);

const StyledColorConfig = styled.div`
padding: 12px 12px 0 12px;
Expand Down Expand Up @@ -96,37 +93,43 @@ const CONFIG_SETTINGS = {
type: 'switch',
options: [true, false]
},
colorBlindSafe: {
type: 'switch',
options: [true, false]
},
custom: {
label: 'customPalette',
type: 'switch',
options: [true, false]
}
};
const displayOption = d => capitalizeFirstLetter(d);
const getOptionValue = d => d;

ColorRangeSelectorFactory.deps = [CustomPaletteFactory];
function ColorRangeSelectorFactory(
CustomPalette: ReturnType<typeof CustomPaletteFactory>
): React.FC<ColorRangeSelectorProps> {
const ColorRangeSelector: React.FC<ColorRangeSelectorProps> = ({
colorRanges,
colorPalettes,
colorPaletteUI,
setColorPaletteUI,
onSelectColorRange,
selectedColorRange
}) => {
const {customPalette, showSketcher, colorRangeConfig} = colorPaletteUI;
const {type, steps} = colorRangeConfig;
const {type, steps, colorBlindSafe, reversed} = colorRangeConfig;

const filteredColorRanges = useMemo(() => {
const filteredColorPalettes = useMemo(() => {
return (
colorRanges?.filter(colorRange => {
const isType = type === 'all' || type === colorRange.type;
const isStep = Number(steps) === colorRange.colors.length;

return isType && isStep;
}) ?? []
colorPalettes?.filter(
palette =>
paletteIsType(palette, type) &&
paletteIsSteps(palette, steps) &&
paletteIsColorBlindSafe(palette, colorBlindSafe)
) ?? []
);
}, [colorRanges, type, steps]);
}, [colorPalettes, colorBlindSafe, steps, type]);

const _updateConfig = useCallback(
({key, value}) => {
Expand All @@ -147,12 +150,16 @@ function ColorRangeSelectorFactory(
[customPalette, onSelectColorRange]
);

const onSelectFromList = useCallback(
(colorRange, e) => {
const newColorRange = updateColorRange(selectedColorRange, colorRange);
const _onSelectPalette = useCallback(
(colorPalette, e) => {
const newColorRange = updateColorRangeBySelectedPalette(selectedColorRange, colorPalette, {
steps,
reversed
});

onSelectColorRange(newColorRange, e);
},
[selectedColorRange, onSelectColorRange]
[selectedColorRange, reversed, steps, onSelectColorRange]
);
return (
<StyledColorRangeSelector>
Expand All @@ -179,7 +186,7 @@ function ColorRangeSelectorFactory(
{colorRangeConfig.custom ? (
<>
<StyledColorPalette>
<ColorPalette colors={customPalette.colors} />
<ColorPalettePanel colors={customPalette.colors} />
</StyledColorPalette>
<CustomPalette
customPalette={customPalette}
Expand All @@ -190,19 +197,25 @@ function ColorRangeSelectorFactory(
/>
</>
) : (
<ColorPaletteGroup
colorRanges={filteredColorRanges}
onSelect={onSelectFromList}
selected={selectedColorRange}
reversed={colorRangeConfig.reversed}
/>
<div className="color-palette__group">
{filteredColorPalettes.map((colorPalette, i) => (
<ColorPaletteItem
key={`${colorPalette.name}-${i}`}
colorPalette={colorPalette}
selectedColorRange={selectedColorRange}
onSelect={_onSelectPalette}
reversed={reversed}
steps={steps}
/>
))}
</div>
)}
</StyledColorRangeSelector>
);
};

ColorRangeSelector.defaultProps = {
colorRanges: COLOR_RANGES,
colorPalettes: KEPLER_COLOR_PALETTES,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onSelectColorRange: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
Expand Down Expand Up @@ -248,6 +261,8 @@ export const PaletteConfig: React.FC<PaletteConfigProps> = ({
onChange={updateSelect}
disabled={disabled}
inputTheme="secondary"
displayOption={displayOption}
getOptionValue={getOptionValue}
/>
)}
{type === 'switch' && (
Expand Down Expand Up @@ -280,28 +295,33 @@ const StyledColorRange = styled.div.attrs({
}
`;

export const ColorPaletteGroup: React.FC<ColorPaletteGroupProps> = ({
reversed,
type ColorPaletteItemProps = {
reversed?: boolean;
selected?: ColorRange;
colorPalette: ColorPalette;
selectedColorRange: ColorRange;
onSelect: (colorPalette: ColorPalette, e: MouseEvent) => void;
steps: number;
};

export const ColorPaletteItem: React.FC<ColorPaletteItemProps> = ({
colorPalette,
steps,
selectedColorRange,
onSelect,
selected,
colorRanges
}) => (
<div className="color-palette__group">
{colorRanges.map((colorRange, i) => (
<StyledColorRange
key={`${colorRange.name}-${i}`}
onClick={e =>
onSelect(reversed ? (reverseColorRange(true, colorRange) as ColorRange) : colorRange, e)
}
>
<ColorPalette
colors={colorRange.colors}
isReversed={reversed}
isSelected={colorRange.name === selected.name && reversed === Boolean(selected.reversed)}
/>
</StyledColorRange>
))}
</div>
);
reversed
}) => {
const colors = useMemo(() => colorPalette.colors(steps), [colorPalette, steps]);
const onClick = useCallback(e => onSelect(colorPalette, e), [colorPalette, onSelect]);
return (
<StyledColorRange onClick={onClick}>
<ColorPalettePanel
colors={colors}
isReversed={reversed}
isSelected={colorPalette.name === selectedColorRange.name}
/>
</StyledColorRange>
);
};

export default ColorRangeSelectorFactory;
18 changes: 6 additions & 12 deletions src/components/src/side-panel/layer-panel/custom-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import styled, {StyledComponent, css} from 'styled-components';
import Portaled from '../../common/portaled';

import {KeyEvent} from '@kepler.gl/constants';
import {ColorUI, HexColor} from '@kepler.gl/types';
import {ColorUI, HexColor, NestedPartial} from '@kepler.gl/types';
import {colorMapToColorBreaks, isNumericColorBreaks} from '@kepler.gl/utils';
import {
addCustomPaletteColor,
Expand All @@ -42,9 +42,7 @@ export type ActionIcons = {
};

export type EditColorMapFunc = (v: number, i: number) => void;
export type SetColorUIFunc = (
newConfig: Partial<{[key in keyof ColorUI]: ColorUI[keyof ColorUI]}>
) => void;
export type SetColorUIFunc = (newConfig: NestedPartial<ColorUI>) => void;

/**
* EditableColorRange
Expand Down Expand Up @@ -526,8 +524,8 @@ function CustomPaletteFactory(): React.FC<CustomPaletteProps> {
if (!customPalette.colorMap) {
return;
}
const newColorMap = customPalette.colorMap.map((cm, i) =>
i === index ? [value, cm[1]] : cm
const newColorMap = customPalette.colorMap.map(
(cm, i) => (i === index ? [value, cm[1]] : cm) as [string, string]
);
setColorPaletteUI({
customPalette: {
Expand Down Expand Up @@ -569,12 +567,8 @@ function CustomPaletteFactory(): React.FC<CustomPaletteProps> {
<DividerLine />
{/* Cancel or Confirm Buttons */}
<BottomAction onCancel={onCancel} onConfirm={onConfirm} />
<Portaled isOpened={showSketcher !== false} left={280} top={-300}>
<CustomPicker
color={colors[Number(showSketcher)]}
onChange={onPickerUpdate}
onSwatchClose={onSwatchClose}
/>
<Portaled isOpened={showSketcher !== false} left={280} top={-300} onClose={onSwatchClose}>
<CustomPicker color={colors[Number(showSketcher)]} onChange={onPickerUpdate} />
</Portaled>
</StyledCustomPalette>
);
Expand Down
10 changes: 2 additions & 8 deletions src/components/src/side-panel/layer-panel/custom-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {SketchPicker, ColorChangeHandler} from 'react-color';

import {HexColor} from '@kepler.gl/types';

import useOnClickOutside from '../../hooks/use-on-click-outside';

// This was put in because 3rd party library react-color doesn't yet cater for customized color of child component <SketchField> which contains HEX/RGB input text box
// Issue raised: https://github.com/casesandberg/react-color/issues/631

Expand Down Expand Up @@ -63,13 +61,9 @@ type CustomPickerProps = {
panelBackground: string;
};
onChange: ColorChangeHandler;
onSwatchClose: () => void;
};

const CustomPicker: React.FC<CustomPickerProps> = props => {
const {color, onChange, theme} = props;
const ref = useOnClickOutside<HTMLDivElement>(props.onSwatchClose);

const CustomPicker: React.FC<CustomPickerProps> = ({color, onChange, theme}: CustomPickerProps) => {
const pickerStyle = useMemo(
() => ({
picker: {
Expand All @@ -83,7 +77,7 @@ const CustomPicker: React.FC<CustomPickerProps> = props => {
);

return (
<StyledPicker ref={ref}>
<StyledPicker>
<SketchPicker
color={color}
onChange={onChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@ const ColorPickerTop = ({setMode, mode}) => (
</StyledColorPickerTop>
);

// eslint-disable-next-line @typescript-eslint/no-empty-function
const nop = () => {};

const SingleColorPalette: React.FC<SingleColorPaletteProps> = ({
selectedColor,
onSelectColor
Expand All @@ -85,9 +82,7 @@ const SingleColorPalette: React.FC<SingleColorPaletteProps> = ({
selectedColor={selectedColor}
/>
) : null}
{mode === MODE.picker ? (
<CustomPicker color={selectedColor} onChange={onSetColor} onSwatchClose={nop} />
) : null}
{mode === MODE.picker ? <CustomPicker color={selectedColor} onChange={onSetColor} /> : null}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,7 @@ function LayerGroupColorPickerFactory() {
top={-50}
onClose={onClosePicker}
>
<CustomPicker
color={rgbToHex(color)}
onChange={onCustomPickerChange}
onSwatchClose={onClosePicker}
/>
<CustomPicker color={rgbToHex(color)} onChange={onCustomPickerChange} />
</Portaled>
</LayerGroupColorPickerWrapper>
);
Expand Down
Loading

0 comments on commit 2153836

Please sign in to comment.