Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 64 additions & 65 deletions packages/base/src/dialogs/symbology/colorRampUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,87 +3,86 @@ import colorScale from 'colormap/colorScale.js';
import { useEffect } from 'react';

import rawCmocean from '@/src/dialogs/symbology/components/color_ramp/cmocean.json';

export interface IColorMap {
name: ColorRampName;
colors: string[];
}
import { objectKeys } from '@/src/tools';
import { IColorMap, IColorRampDefinition } from '@/src/types';

const { __license__: _, ...cmocean } = rawCmocean;

Object.assign(colorScale, cmocean);

export const COLOR_RAMP_NAMES = [
'jet',
// 'hsv', 11 steps min
'hot',
'cool',
'spring',
'summer',
'autumn',
'winter',
'bone',
'copper',
'greys',
'YiGnBu',
'greens',
'YiOrRd',
'bluered',
'RdBu',
// 'picnic', 11 steps min
'rainbow',
'portland',
'blackbody',
'earth',
'electric',
'viridis',
'inferno',
'magma',
'plasma',
'warm',
// 'rainbow-soft', 11 steps min
'bathymetry',
'cdom',
'chlorophyll',
'density',
'freesurface-blue',
'freesurface-red',
'oxygen',
'par',
'phase',
'salinity',
'temperature',
'turbidity',
'velocity-blue',
'velocity-green',
// 'cubehelix' 16 steps min
'ice',
'oxy',
'matter',
'amp',
'tempo',
'rain',
'topo',
'balance',
'delta',
'curl',
'diff',
'tarn',
] as const;

export const COLOR_RAMP_DEFINITIONS = {
jet: { type: 'Sequential' },
// 'hsv': {type: 'Sequential'}, 11 steps min
hot: { type: 'Sequential' },
cool: { type: 'Sequential' },
spring: { type: 'Sequential' },
summer: { type: 'Sequential' },
autumn: { type: 'Sequential' },
winter: { type: 'Sequential' },
bone: { type: 'Sequential' },
copper: { type: 'Sequential' },
greys: { type: 'Sequential' },
YiGnBu: { type: 'Sequential' },
greens: { type: 'Sequential' },
YiOrRd: { type: 'Sequential' },
bluered: { type: 'Sequential' },
RdBu: { type: 'Sequential' },
// 'picnic': {type: 'Sequential'}, 11 steps min
rainbow: { type: 'Sequential' },
portland: { type: 'Sequential' },
blackbody: { type: 'Sequential' },
earth: { type: 'Sequential' },
electric: { type: 'Sequential' },
viridis: { type: 'Sequential' },
inferno: { type: 'Sequential' },
magma: { type: 'Sequential' },
plasma: { type: 'Sequential' },
warm: { type: 'Sequential' },
// 'rainbow-soft': {type: 'Sequential'}, 11 steps min
bathymetry: { type: 'Sequential' },
cdom: { type: 'Sequential' },
chlorophyll: { type: 'Sequential' },
density: { type: 'Sequential' },
'freesurface-blue': { type: 'Sequential' },
'freesurface-red': { type: 'Sequential' },
oxygen: { type: 'Sequential' },
par: { type: 'Sequential' },
phase: { type: 'Cyclic' },
salinity: { type: 'Sequential' },
temperature: { type: 'Sequential' },
turbidity: { type: 'Sequential' },
'velocity-blue': { type: 'Sequential' },
'velocity-green': { type: 'Sequential' },
// 'cubehelix': {type: 'Sequential'}, 16 steps min
ice: { type: 'Sequential' },
oxy: { type: 'Sequential' },
matter: { type: 'Sequential' },
amp: { type: 'Sequential' },
tempo: { type: 'Sequential' },
rain: { type: 'Sequential' },
topo: { type: 'Sequential' },
balance: { type: 'Divergent', criticalValue: 0.5 },
delta: { type: 'Divergent', criticalValue: 0.5 },
curl: { type: 'Divergent', criticalValue: 0.5 },
diff: { type: 'Divergent', criticalValue: 0.5 },
tarn: { type: 'Divergent', criticalValue: 0.5 },
} as const satisfies { [key: string]: IColorRampDefinition };

export const COLOR_RAMP_NAMES = objectKeys(COLOR_RAMP_DEFINITIONS);
export type ColorRampName = (typeof COLOR_RAMP_NAMES)[number];

export const getColorMapList = (): IColorMap[] => {
const colorMapList: IColorMap[] = [];

COLOR_RAMP_NAMES.forEach(name => {
const colorRamp = colormap({
const definition = COLOR_RAMP_DEFINITIONS[name];
const colors = colormap({
colormap: name,
nshades: 255,
format: 'rgbaString',
});

colorMapList.push({ name, colors: colorRamp });
colorMapList.push({ name, colors, definition });
});

return colorMapList;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Button } from '@jupyterlab/ui-components';
import React, { useEffect, useRef, useState } from 'react';

import { useColorMapList } from '@/src/dialogs/symbology/colorRampUtils';
import {
useColorMapList,
COLOR_RAMP_DEFINITIONS,
ColorRampName,
} from '@/src/dialogs/symbology/colorRampUtils';
import ColorRampEntry from './ColorRampEntry';

export interface IColorMap {
name: string;
colors: string[];
type?: string;
}

interface ICanvasSelectComponentProps {
Expand All @@ -22,7 +27,13 @@ const CanvasSelectComponent: React.FC<ICanvasSelectComponentProps> = ({
const [isOpen, setIsOpen] = useState(false);
const [colorMaps, setColorMaps] = useState<IColorMap[]>([]);

useColorMapList(setColorMaps);
useColorMapList((maps: IColorMap[]) => {
const withTypes = maps.map(m => {
const def = COLOR_RAMP_DEFINITIONS[m.name as ColorRampName];
return { ...m, type: def?.type ?? 'Unknown' };
});
setColorMaps(withTypes);
});

useEffect(() => {
if (colorMaps.length > 0) {
Expand Down Expand Up @@ -99,7 +110,12 @@ const CanvasSelectComponent: React.FC<ICanvasSelectComponentProps> = ({
className={`jp-gis-color-ramp-dropdown ${isOpen ? 'jp-gis-open' : ''}`}
>
{colorMaps.map((item, index) => (
<ColorRampEntry index={index} colorMap={item} onClick={selectItem} />
<ColorRampEntry
key={item.name}
index={index}
colorMap={item}
onClick={selectItem}
/>
))}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { IDict } from '@jupytergis/schema';
import { Button } from '@jupyterlab/ui-components';
import React, { useEffect, useState } from 'react';

import {
COLOR_RAMP_DEFINITIONS,
ColorRampName,
} from '@/src/dialogs/symbology/colorRampUtils';
import { LoadingIcon } from '@/src/shared/components/loading';
import CanvasSelectComponent from './CanvasSelectComponent';
import { ColorRampValueControls } from './ColorRampValueControls';
import ModeSelectRow from './ModeSelectRow';

interface IColorRampProps {
modeOptions: string[];
layerParams: IDict;
Expand All @@ -14,15 +18,22 @@ interface IColorRampProps {
numberOfShades: string,
selectedRamp: string,
setIsLoading: (isLoading: boolean) => void,
criticalValue?: number,
minValue?: number,
maxValue?: number,
) => void;
showModeRow: boolean;
showRampSelector: boolean;
renderType?: 'graduated' | 'categorized' | 'heatmap' | 'singleband';
}

export type ColorRampOptions = {
selectedRamp: string;
numberOfShades: string;
selectedMode: string;
minValue?: number;
maxValue?: number;
criticalValue?: number;
};

const ColorRamp: React.FC<IColorRampProps> = ({
Expand All @@ -31,10 +42,13 @@ const ColorRamp: React.FC<IColorRampProps> = ({
classifyFunc,
showModeRow,
showRampSelector,
renderType,
}) => {
const [selectedRamp, setSelectedRamp] = useState('');
const [selectedMode, setSelectedMode] = useState('');
const [numberOfShades, setNumberOfShades] = useState('');
const [minValue, setMinValue] = useState<number | undefined>(-5);
Copy link
Member

@mfisher87 mfisher87 Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not set an arbitrary default. We should either leave it blank or use the min/max from the actual data.

I think we have two options:

  • Leave them blank and offer a button ("Set to data min & max"? "Fill from data"? "Detect range"? "Use actual range"?) which populates the values from the actual data. This will mean the dialog will load faster and the user will control when the data is read.
  • Pre-populate them with the values from the actual data. This will mean the dialog loads more slowly.

The "classify" button should be greyed out unless min & max are populated. Then the classify button will always use the min & max and won't have to calculate them.

What do you think?

Copy link
Contributor Author

@nakul-py nakul-py Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mfisher87 Using both options :)

First we pre-populate min and max values from actual data and if user deletes the min/max values or if undefined a button shows up calles "Use actual range" from which user get actual range back.

What do you think??

const [maxValue, setMaxValue] = useState<number | undefined>(5);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
Expand All @@ -43,19 +57,49 @@ const ColorRamp: React.FC<IColorRampProps> = ({
}
}, [layerParams]);

useEffect(() => {
if (!layerParams.symbologyState) {
layerParams.symbologyState = {};
}

if (renderType !== 'heatmap') {
layerParams.symbologyState.min = minValue;
layerParams.symbologyState.max = maxValue;
layerParams.symbologyState.colorRamp = selectedRamp;
layerParams.symbologyState.nClasses = numberOfShades;
layerParams.symbologyState.mode = selectedMode;

if (rampDef?.type === 'Divergent') {
layerParams.symbologyState.criticalValue = scaledCritical;
}
}
}, [minValue, maxValue, selectedRamp, selectedMode, numberOfShades]);

const populateOptions = () => {
let nClasses, singleBandMode, colorRamp;
let nClasses, singleBandMode, colorRamp, min, max;

if (layerParams.symbologyState) {
nClasses = layerParams.symbologyState.nClasses;
singleBandMode = layerParams.symbologyState.mode;
colorRamp = layerParams.symbologyState.colorRamp;
min = layerParams.symbologyState.min;
max = layerParams.symbologyState.max;
}
setNumberOfShades(nClasses ? nClasses : '9');
setSelectedMode(singleBandMode ? singleBandMode : 'equal interval');
setSelectedRamp(colorRamp ? colorRamp : 'viridis');
setMinValue(min !== undefined ? min : -5);
setMaxValue(max !== undefined ? max : 5);
};

const rampDef = COLOR_RAMP_DEFINITIONS[selectedRamp as ColorRampName];
const normalizedCritical =
rampDef?.type === 'Divergent' ? (rampDef.criticalValue ?? 0.5) : 0.5;
const scaledCritical =
minValue !== undefined && maxValue !== undefined
? minValue + normalizedCritical * (maxValue - minValue)
: undefined;

return (
<div className="jp-gis-color-ramp-container">
{showRampSelector && (
Expand All @@ -76,6 +120,16 @@ const ColorRamp: React.FC<IColorRampProps> = ({
setSelectedMode={setSelectedMode}
/>
)}
{rampDef && (
<ColorRampValueControls
min={minValue}
setMin={setMinValue}
max={maxValue}
setMax={setMaxValue}
rampDef={rampDef}
/>
)}

{isLoading ? (
<LoadingIcon />
) : (
Expand All @@ -87,6 +141,9 @@ const ColorRamp: React.FC<IColorRampProps> = ({
numberOfShades,
selectedRamp,
setIsLoading,
scaledCritical,
minValue,
maxValue,
)
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@ const ColorRampEntry: React.FC<IColorRampEntryProps> = ({
onClick={() => onClick(colorMap.name)}
className="jp-gis-color-ramp-entry"
>
<span className="jp-gis-color-label">{colorMap.name}</span>
<span className="jp-gis-color-label">
<strong>{colorMap.name}</strong>{' '}
<span
style={{ color: 'var(--jp-ui-font-color2)', fontStyle: 'italic' }}
>
({colorMap.type})
</span>
</span>
<canvas
id={`cv-${index}`}
height={canvasHeight}
Expand Down
Loading
Loading