Skip to content

Commit 77e7857

Browse files
authored
[feat] Support custom ordinal color scale on string field in layer config (#2868)
- Support custom ordinal color scale on string field in layer config - histogram from ordinal - pass className to StyledLegend - fix for color palette colorWidths undefined Signed-off-by: Ihor Dykhta <[email protected]>
1 parent cccc4be commit 77e7857

File tree

12 files changed

+105
-20
lines changed

12 files changed

+105
-20
lines changed

src/components/src/common/color-legend.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,11 @@ function ColorLegendFactory(LegendRow: ReturnType<typeof LegendRowFactory>) {
336336
);
337337

338338
return (
339-
<StyledLegend disableEdit={disableEdit} isExpanded={isExpanded}>
339+
<StyledLegend
340+
className="styled-color-legend"
341+
disableEdit={disableEdit}
342+
isExpanded={isExpanded}
343+
>
340344
{legends.map((legend, i) => (
341345
<LegendRow
342346
key={`${legend.data}-${i}`}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ const StyledColorBreaksDisplay = styled.div.attrs({
3737

3838
const ColorBreaksPanelWrapper = styled.div``;
3939

40-
export const EditButton = ({onClickEdit}) => (
40+
type EditButtonProps = {onClickEdit: () => void};
41+
42+
export const EditButton: React.FC<EditButtonProps> = ({onClickEdit}) => (
4143
<Button className="editp__button" link onClick={onClickEdit}>
4244
<Edit height="16px" />
4345
Edit
@@ -51,7 +53,7 @@ export type ColorBreaksDisplayProps = {
5153

5254
export const ColorBreaksDisplay: React.FC<ColorBreaksDisplayProps> = ({currentBreaks, onEdit}) => {
5355
if (!isNumericColorBreaks(currentBreaks)) {
54-
// TODO: implement display for ordinal breaks
56+
// don't display color breaks for ordinal breaks, user can change it in custom breaks
5557
return null;
5658
}
5759
return (

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const ColorPalette: React.FC<ColorPaletteProps> = ({
5959
>
6060
<PaletteWrapper style={{height, transform: `scale(${isReversed ? -1 : 1}, 1)`}}>
6161
{colors.map((color: number | string, index: number) =>
62-
colorWidths ? (
62+
colorWidths && colorWidths[index] ? (
6363
<StyledColorBlock
6464
key={`${color}-${index}`}
6565
style={{backgroundColor: String(color), width: colorWidths[index]}}

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getLayerColorScale,
1414
getLegendOfScale,
1515
histogramFromValues,
16+
histogramFromOrdinal,
1617
histogramFromThreshold,
1718
getHistogramDomain,
1819
hasColorMap
@@ -187,8 +188,10 @@ function ColorScaleSelectorFactory(
187188
}
188189
return columnStats?.bins
189190
? columnStats?.bins
191+
: field?.type === ALL_FIELD_TYPES.string
192+
? histogramFromOrdinal(colorScale?.domain() || [], dataset.allIndexes, fieldValueAccessor)
190193
: histogramFromValues(dataset.allIndexes, HISTOGRAM_BINS, fieldValueAccessor);
191-
}, [aggregatedBins, columnStats, dataset, fieldValueAccessor]);
194+
}, [aggregatedBins, columnStats, dataset, fieldValueAccessor, colorScale, field?.type]);
192195

193196
const histogramDomain = useMemo(() => {
194197
return getHistogramDomain({aggregatedBins, columnStats, dataset, fieldValueAccessor});
@@ -218,17 +221,22 @@ function ColorScaleSelectorFactory(
218221
val => {
219222
// highlight selected option
220223
if (getOptionValue(val) === SCALE_TYPES.custom) {
224+
const colorMap = range.colorMap
225+
? range.colorMap
226+
: colorBreaks
227+
? colorBreaksToColorMap(colorBreaks)
228+
: undefined;
229+
const colors =
230+
field?.type === ALL_FIELD_TYPES.string
231+
? range.colors.slice(0, colorMap?.length)
232+
: range.colors;
221233
// update custom breaks
222234
const customPalette: NestedPartial<ColorRange> = {
223235
name: 'color.customPalette',
224236
type: 'custom',
225237
category: 'Custom',
226-
colors: range.colors,
227-
colorMap: range.colorMap
228-
? range.colorMap
229-
: colorBreaks
230-
? colorBreaksToColorMap(colorBreaks)
231-
: undefined
238+
colors,
239+
colorMap
232240
};
233241
setColorUI({
234242
showColorChart: true,

src/components/src/side-panel/layer-panel/custom-palette.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,23 @@ export const DragHandle = SortableHandle<DragHandleProps>(({className, children}
221221
<StyledDragHandle className={className}>{children}</StyledDragHandle>
222222
));
223223

224-
export const ColorPaletteInput = ({value, onChange, id, width, textAlign, editable}) => {
224+
export type ColorPaletteInputProps = {
225+
value: string | number;
226+
onChange: (val: unknown) => void;
227+
id: string;
228+
width: string;
229+
textAlign: string;
230+
editable: boolean;
231+
};
232+
233+
export const ColorPaletteInput = ({
234+
value,
235+
onChange,
236+
id,
237+
width,
238+
textAlign,
239+
editable
240+
}: ColorPaletteInputProps) => {
225241
const [stateValue, setValue] = useState(value);
226242
const inputRef = useRef(null);
227243
useEffect(() => {
@@ -393,7 +409,19 @@ export const CustomPaletteInput: React.FC<CustomPaletteInputProps> = ({
393409
editColorMap={editColorMapValue}
394410
editable
395411
/>
396-
) : null}
412+
) : (
413+
colorBreaks &&
414+
colorBreaks[index] && (
415+
<ColorPaletteInput
416+
value={colorBreaks[index].label}
417+
id={`color-palette-input-${index}-left`}
418+
width="auto"
419+
textAlign="end"
420+
editable={false}
421+
onChange={v => v}
422+
/>
423+
)
424+
)}
397425
</div>
398426
<div className="custom-palette-input__right">
399427
{!disableAppend ? (

src/constants/src/default-settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ export const linearFieldAggrScaleFunctions = {
666666
};
667667

668668
export const ordinalFieldScaleFunctions = {
669-
[CHANNEL_SCALES.color]: [SCALE_TYPES.ordinal],
669+
[CHANNEL_SCALES.color]: [SCALE_TYPES.ordinal, SCALE_TYPES.custom],
670670
[CHANNEL_SCALES.radius]: [SCALE_TYPES.point],
671671
[CHANNEL_SCALES.size]: [SCALE_TYPES.point]
672672
};

src/layers/src/base-layer.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,13 +1027,22 @@ class Layer implements KeplerLayer {
10271027
colorDomain: VisualChannelDomain,
10281028
colorRange: ColorRange
10291029
): GetVisChannelScaleReturnType {
1030-
if (hasColorMap(colorRange) && colorScale === SCALE_TYPES.custom) {
1030+
if (
1031+
hasColorMap(colorRange) &&
1032+
(colorScale === SCALE_TYPES.custom || colorScale === SCALE_TYPES.ordinal)
1033+
) {
10311034
const cMap = new Map();
10321035
colorRange.colorMap?.forEach(([k, v]) => {
10331036
cMap.set(k, typeof v === 'string' ? hexToRgb(v) : v);
10341037
});
10351038

1036-
const scaleType = colorScale === SCALE_TYPES.custom ? colorScale : SCALE_TYPES.ordinal;
1039+
// in kepler, scaleThreshold function will be used for SCALE_TYPES.custom
1040+
// so we adjust scaleType to SCALE_TYPES.custom to ordinal if necessary
1041+
const isOrdinalDomain = (colorDomain as (string | number)[]).every(
1042+
i => typeof i === 'string'
1043+
);
1044+
const scaleType =
1045+
colorScale === SCALE_TYPES.custom && !isOrdinalDomain ? colorScale : SCALE_TYPES.ordinal;
10371046

10381047
const scale = getScaleFunction(scaleType, cMap.values(), cMap.keys(), false);
10391048
scale.unknown(cMap.get(UNKNOWN_COLOR_KEY) || NO_VALUE_COLOR);

src/table/src/kepler-table.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,9 @@ class KeplerTable<F extends Field = Field> {
496496
case SCALE_TYPES.linear:
497497
case SCALE_TYPES.sqrt:
498498
default:
499-
return getLinearDomain(filteredIndexForDomain, indexValueAccessor);
499+
return field.type === ALL_FIELD_TYPES.string
500+
? getOrdinalDomain(dataContainer, valueAccessor)
501+
: getLinearDomain(filteredIndexForDomain, indexValueAccessor);
500502
}
501503
}
502504

src/utils/src/data-scale-utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ export function getLegendOfScale({
295295
if (!scale || scale.byZoom) {
296296
return [];
297297
}
298-
if (scaleType === SCALE_TYPES.ordinal) {
298+
if (scaleType === SCALE_TYPES.ordinal || fieldType === ALL_FIELD_TYPES.string) {
299299
return getOrdinalLegends(scale);
300300
}
301301

@@ -396,6 +396,13 @@ export function colorMapToColorBreaks(colorMap?: ColorMap): ColorBreak[] | null
396396
return null;
397397
}
398398
const colorBreaks = colorMap.map(([value, color], i) => {
399+
if (typeof value === 'string') {
400+
// for ordinal string value
401+
return {
402+
data: color,
403+
label: value
404+
};
405+
}
399406
const range =
400407
i === 0
401408
? // first

src/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export {
3939
histogramFromThreshold,
4040
histogramFromValues,
4141
histogramFromDomain,
42+
histogramFromOrdinal,
4243
runGpuFilterForPlot,
4344
updateTimeFilterPlotType
4445
} from './plot';

src/utils/src/plot.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,30 @@ export function histogramFromValues(
109109
});
110110
}
111111

112+
export function histogramFromOrdinal(
113+
domain: [string],
114+
values: (Millisecond | null | number)[],
115+
valueAccessor?: (d: unknown) => string
116+
): Bin[] {
117+
// @ts-expect-error to typed to expect strings
118+
const getBins = d3Histogram().thresholds(domain);
119+
if (valueAccessor) {
120+
// @ts-expect-error to typed to expect strings
121+
getBins.value(valueAccessor);
122+
}
123+
124+
// @ts-expect-error null values aren't expected
125+
const bins = getBins(values);
126+
127+
// @ts-ignore d3-array types doesn't match
128+
return bins.map(bin => ({
129+
count: bin.length,
130+
indexes: bin,
131+
x0: bin.x0,
132+
x1: bin.x0
133+
}));
134+
}
135+
112136
/**
113137
*
114138
* @param domain

test/browser/components/side-panel/channel-by-value-selctor-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ test('Components -> ChannelByValueSelector -> ColorScaleSelector -> disabled', t
9898
t.equal(wrapper.find(ColorScaleSelector).length, 1, 'Should render 1 ColorScaleSelector');
9999
t.equal(
100100
wrapper.find(ColorScaleSelector).at(0).props().disabled,
101-
true,
102-
'Should disabled color scale select if only 1 option'
101+
false,
102+
'Should enable ordinal and custom color scale select'
103103
);
104104

105105
t.equal(

0 commit comments

Comments
 (0)