diff --git a/packages/base/src/dialogs/symbology/classificationModes.ts b/packages/base/src/dialogs/symbology/classificationModes.ts index b4ab85989..015995a7d 100644 --- a/packages/base/src/dialogs/symbology/classificationModes.ts +++ b/packages/base/src/dialogs/symbology/classificationModes.ts @@ -47,26 +47,13 @@ export namespace VectorClassifications { }; export const calculateEqualIntervalBreaks = ( - values: number[], nClasses: number, - ) => { - const minimum = Math.min(...values); - const maximum = Math.max(...values); - - const breaks: number[] = []; - const step = (maximum - minimum) / nClasses; - - let value = minimum; - - for (let i = 0; i < nClasses; i++) { - value += step; - breaks.push(value); - } - - breaks[nClasses - 1] = maximum; - - return breaks; - }; + minimum: number, + maximum: number, + ): number[] => + Array.from({ length: nClasses }, (_, i) => { + return minimum + (i / (nClasses - 1)) * (maximum - minimum); + }); export const calculateJenksBreaks = (values: number[], nClasses: number) => { const maximum = Math.max(...values); diff --git a/packages/base/src/dialogs/symbology/colorRampUtils.ts b/packages/base/src/dialogs/symbology/colorRampUtils.ts index 19b95908e..c3e24ad95 100644 --- a/packages/base/src/dialogs/symbology/colorRampUtils.ts +++ b/packages/base/src/dialogs/symbology/colorRampUtils.ts @@ -2,89 +2,27 @@ import colormap from 'colormap'; import colorScale from 'colormap/colorScale.js'; import { useEffect } from 'react'; +import { COLOR_RAMP_DEFINITIONS } from '@/src/dialogs/symbology/colorRamps'; import rawCmocean from '@/src/dialogs/symbology/components/color_ramp/cmocean.json'; - -export interface IColorMap { - name: ColorRampName; - colors: string[]; -} +import { objectEntries } from '@/src/tools'; +import { IColorMap } 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 type ColorRampName = (typeof COLOR_RAMP_NAMES)[number]; - export const getColorMapList = (): IColorMap[] => { const colorMapList: IColorMap[] = []; - COLOR_RAMP_NAMES.forEach(name => { - const colorRamp = colormap({ + for (const [name, definition] of objectEntries(COLOR_RAMP_DEFINITIONS)) { + const colors = colormap({ colormap: name, nshades: 255, format: 'rgbaString', }); - colorMapList.push({ name, colors: colorRamp }); - }); + colorMapList.push({ name, colors, definition }); + } return colorMapList; }; @@ -92,7 +30,9 @@ export const getColorMapList = (): IColorMap[] => { /** * Hook that loads and sets color maps. */ -export const useColorMapList = (setColorMaps: (maps: IColorMap[]) => void) => { +export const useColorMapList = ( + setColorMaps: React.Dispatch>, +) => { useEffect(() => { setColorMaps(getColorMapList()); }, [setColorMaps]); diff --git a/packages/base/src/dialogs/symbology/colorRamps.ts b/packages/base/src/dialogs/symbology/colorRamps.ts new file mode 100644 index 000000000..bacfb49da --- /dev/null +++ b/packages/base/src/dialogs/symbology/colorRamps.ts @@ -0,0 +1,59 @@ +import { IColorRampDefinition } from '@/src/types'; + +export const COLOR_RAMP_DEFINITIONS = { + // 'rainbow-soft': {type: 'Cyclic'}, 11 steps min + // 'hsv': {type: 'Cyclic'}, 11 steps min + phase: { type: 'Cyclic' }, + jet: { type: 'Sequential' }, + 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' }, + 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' }, + 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' }, + 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' }, + // 'picnic': {type: 'Divergent', criticalValue: 0.5 }, 11 steps min + 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 }; diff --git a/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx index 062c21577..e19c9368f 100644 --- a/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx +++ b/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx @@ -2,21 +2,21 @@ import { Button } from '@jupyterlab/ui-components'; import React, { useEffect, useRef, useState } from 'react'; import { useColorMapList } from '@/src/dialogs/symbology/colorRampUtils'; +import { IColorMap } from '@/src/types'; import ColorRampEntry from './ColorRampEntry'; -export interface IColorMap { - name: string; - colors: string[]; -} - interface ICanvasSelectComponentProps { selectedRamp: string; setSelected: (item: any) => void; + reverse: boolean; + setReverse: React.Dispatch>; } const CanvasSelectComponent: React.FC = ({ selectedRamp, setSelected, + reverse, + setReverse, }) => { const containerRef = useRef(null); const [isOpen, setIsOpen] = useState(false); @@ -28,7 +28,7 @@ const CanvasSelectComponent: React.FC = ({ if (colorMaps.length > 0) { updateCanvas(selectedRamp); } - }, [selectedRamp]); + }, [selectedRamp, colorMaps, reverse]); const toggleDropdown = () => { setIsOpen(!isOpen); @@ -67,7 +67,7 @@ const CanvasSelectComponent: React.FC = ({ for (let i = 0; i <= 255; i++) { ctx.beginPath(); - const color = ramp[0].colors[i]; + const color = reverse ? ramp[0].colors[255 - i] : ramp[0].colors[i]; ctx.fillStyle = color; ctx.fillRect(i * 2, 0, 2, 50); @@ -99,9 +99,25 @@ const CanvasSelectComponent: React.FC = ({ className={`jp-gis-color-ramp-dropdown ${isOpen ? 'jp-gis-open' : ''}`} > {colorMaps.map((item, index) => ( - + ))} + +
+ +
); }; diff --git a/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx index 70af1d808..8d0f17957 100644 --- a/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx +++ b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx @@ -2,8 +2,11 @@ import { IDict } from '@jupytergis/schema'; import { Button } from '@jupyterlab/ui-components'; import React, { useEffect, useState } from 'react'; +import { COLOR_RAMP_DEFINITIONS } from '@/src/dialogs/symbology/colorRamps'; import { LoadingIcon } from '@/src/shared/components/loading'; +import { ColorRampName } from '@/src/types'; import CanvasSelectComponent from './CanvasSelectComponent'; +import { ColorRampValueControls } from './ColorRampValueControls'; import ModeSelectRow from './ModeSelectRow'; interface IColorRampProps { @@ -12,17 +15,32 @@ interface IColorRampProps { classifyFunc: ( selectedMode: string, numberOfShades: string, - selectedRamp: string, + selectedRamp: ColorRampName, + reverseRamp: boolean, setIsLoading: (isLoading: boolean) => void, + minValue: number, + maxValue: number, + criticalValue?: number, ) => void; showModeRow: boolean; showRampSelector: boolean; + renderType: + | 'Graduated' + | 'Categorized' + | 'Heatmap' + | 'Singleband Pseudocolor'; + dataMin?: number; + dataMax?: number; } export type ColorRampOptions = { - selectedRamp: string; + selectedRamp: ColorRampName; + reverseRamp: boolean; numberOfShades: string; selectedMode: string; + minValue: number; + maxValue: number; + criticalValue?: number; }; const ColorRamp: React.FC = ({ @@ -31,31 +49,83 @@ const ColorRamp: React.FC = ({ classifyFunc, showModeRow, showRampSelector, + renderType, + dataMin, + dataMax, }) => { - const [selectedRamp, setSelectedRamp] = useState(''); + const [selectedRamp, setSelectedRamp] = useState('viridis'); + const [reverseRamp, setReverseRamp] = useState(false); const [selectedMode, setSelectedMode] = useState(''); const [numberOfShades, setNumberOfShades] = useState(''); + const [minValue, setMinValue] = useState(dataMin); + const [maxValue, setMaxValue] = useState(dataMax); const [isLoading, setIsLoading] = useState(false); useEffect(() => { - if (selectedRamp === '' && selectedMode === '' && numberOfShades === '') { - populateOptions(); + if (selectedMode === '' && numberOfShades === '') { + initializeState(); } }, [layerParams]); - const populateOptions = () => { - let nClasses, singleBandMode, colorRamp; + useEffect(() => { + setMinValue(layerParams.symbologyState?.min ?? dataMin); + setMaxValue(layerParams.symbologyState?.max ?? dataMax); + }, [dataMin, dataMax]); + + const initializeState = () => { + let nClasses, singleBandMode, colorRamp, reverseRamp; if (layerParams.symbologyState) { nClasses = layerParams.symbologyState.nClasses; singleBandMode = layerParams.symbologyState.mode; colorRamp = layerParams.symbologyState.colorRamp; + reverseRamp = layerParams.symbologyState.reverse; } - setNumberOfShades(nClasses ? nClasses : '9'); - setSelectedMode(singleBandMode ? singleBandMode : 'equal interval'); - setSelectedRamp(colorRamp ? colorRamp : 'viridis'); + setNumberOfShades(nClasses ?? '9'); + setSelectedMode(singleBandMode ?? 'equal interval'); + setSelectedRamp(colorRamp ?? 'viridis'); + setReverseRamp(reverseRamp ?? false); }; + const rampDef = COLOR_RAMP_DEFINITIONS[selectedRamp]; + + const normalizedCritical = + rampDef?.type === 'Divergent' ? (rampDef.criticalValue ?? 0.5) : 0.5; + const scaledCritical = + minValue !== undefined && maxValue !== undefined + ? minValue + normalizedCritical * (maxValue - minValue) + : undefined; + + useEffect(() => { + if (!layerParams.symbologyState) { + layerParams.symbologyState = {}; + } + + if (renderType !== 'Heatmap') { + layerParams.symbologyState.dataMin = dataMin; + layerParams.symbologyState.dataMax = dataMax; + layerParams.symbologyState.min = minValue; + layerParams.symbologyState.max = maxValue; + layerParams.symbologyState.colorRamp = selectedRamp; + layerParams.symbologyState.reverse = reverseRamp; + layerParams.symbologyState.nClasses = numberOfShades; + layerParams.symbologyState.mode = selectedMode; + + if (rampDef?.type === 'Divergent') { + layerParams.symbologyState.criticalValue = rampDef.criticalValue; + } + } + }, [ + minValue, + maxValue, + selectedRamp, + reverseRamp, + selectedMode, + numberOfShades, + dataMin, + dataMax, + ]); + return (
{showRampSelector && ( @@ -64,9 +134,24 @@ const ColorRamp: React.FC = ({
)} + + + {showModeRow && ( = ({ setSelectedMode={setSelectedMode} /> )} + {isLoading ? ( ) : ( diff --git a/packages/base/src/dialogs/symbology/components/color_ramp/ColorRampEntry.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRampEntry.tsx index e6da51cb5..d89cad9d4 100644 --- a/packages/base/src/dialogs/symbology/components/color_ramp/ColorRampEntry.tsx +++ b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRampEntry.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; -import { IColorMap } from './CanvasSelectComponent'; +import { IColorMap } from '@/src/types'; interface IColorRampEntryProps { index: number; @@ -42,7 +42,9 @@ const ColorRampEntry: React.FC = ({ onClick={() => onClick(colorMap.name)} className="jp-gis-color-ramp-entry" > - {colorMap.name} + + {colorMap.name} ({colorMap.definition.type}) + void; + selectedMax: number | undefined; + settedMax: (v: number | undefined) => void; + rampDef: IColorRampDefinition; + renderType: + | 'Categorized' + | 'Graduated' + | 'Heatmap' + | 'Singleband Pseudocolor'; + dataMin?: number; + dataMax?: number; + selectedMode: string; // TODO: should be ClssificationMode (https://github.com/geojupyter/jupytergis/pull/937) +} +export const ColorRampValueControls: React.FC< + IColorRampValueControlsProps +> = props => { + const permittedRenderTypes = ['Graduated', 'Singleband Pseudocolor']; + if (!permittedRenderTypes.includes(props.renderType)) { + return; + } + + const modesSupportingMinMax = ['equal interval', 'continuous']; + const enableMinMax = modesSupportingMinMax.includes(props.selectedMode); + + const formatMode = (mode: string) => + mode.charAt(0).toUpperCase() + mode.slice(1); + + return ( + <> + {props.rampDef.type === 'Divergent' && + props.selectedMode === 'equal interval' && + props.selectedMin !== undefined && + props.selectedMax !== undefined && ( +
+ + + {`${( + props.selectedMin + + props.rampDef.criticalValue * + (props.selectedMax - props.selectedMin) + ).toFixed( + 2, + )} (Colormap diverges at ${props.rampDef.criticalValue * 100}%)`} + +
+ )} + +
+ + + props.settedMin( + e.target.value !== '' ? parseFloat(e.target.value) : undefined, + ) + } + className={'jp-mod-styled'} + placeholder="Enter min value" + disabled={!enableMinMax} + /> +
+ +
+ + + props.settedMax( + e.target.value !== '' ? parseFloat(e.target.value) : undefined, + ) + } + className={'jp-mod-styled'} + placeholder="Enter max value" + disabled={!enableMinMax} + /> +
+ +
+ {!enableMinMax ? ( +
+ ⚠️ Warning: User-specified min/max values are not supported for " + {formatMode(props.selectedMode)}" mode. +
+ ) : ( +
+ )} + + +
+ + ); +}; diff --git a/packages/base/src/dialogs/symbology/hooks/useGetBandInfo.ts b/packages/base/src/dialogs/symbology/hooks/useGetBandInfo.ts index fd1074eb3..30520aa4f 100644 --- a/packages/base/src/dialogs/symbology/hooks/useGetBandInfo.ts +++ b/packages/base/src/dialogs/symbology/hooks/useGetBandInfo.ts @@ -67,12 +67,73 @@ const useGetBandInfo = (model: IJupyterGISModel, layer: IJGISLayer) => { const image = await tiff.getImage(); const numberOfBands = image.getSamplesPerPixel(); + let min = layer.parameters?.symbologyState?.min; + let max = layer.parameters?.symbologyState?.max; + + if (min === undefined || max === undefined) { + // 1. Try metadata first + let dataMin = image.fileDirectory.STATISTICS_MINIMUM; + let dataMax = image.fileDirectory.STATISTICS_MAXIMUM; + + if (dataMin === undefined || dataMax === undefined) { + // 2. Try smallest overview if available + const overviewCount = await tiff.getImageCount(); + const targetImage = + overviewCount > 1 ? await tiff.getImage(overviewCount - 1) : image; + + // 3. Read a downsampled raster (fast) + const rasters = await targetImage.readRasters({ + width: 256, + height: 256, + resampleMethod: 'nearest', + }); + + dataMin = Infinity; + dataMax = -Infinity; + + for (let i = 0; i < rasters.length; i++) { + const bandData = rasters[i] as + | Float32Array + | Uint16Array + | Int16Array; + for (let j = 0; j < bandData.length; j++) { + if (bandData[j] < dataMin) { + dataMin = bandData[j]; + } + if (bandData[j] > dataMax) { + dataMax = bandData[j]; + } + } + } + } + + min = dataMin; + max = dataMax; + + layer.parameters = { + ...layer.parameters, + symbologyState: { + ...(layer.parameters?.symbologyState ?? {}), + min, + max, + }, + }; + + sourceInfo.min = min; + sourceInfo.max = max; + + console.log(`[Symbology Init] Final Min=${min}, Max=${max}`); + } else { + sourceInfo.min = min; + sourceInfo.max = max; + } + for (let i = 0; i < numberOfBands; i++) { bandsArr.push({ band: i, stats: { - minimum: sourceInfo.min ?? 0, - maximum: sourceInfo.max ?? 100, + minimum: min ?? 0, + maximum: max ?? 100, }, }); } diff --git a/packages/base/src/dialogs/symbology/symbologyUtils.ts b/packages/base/src/dialogs/symbology/symbologyUtils.ts index afc3216ae..1b06a640e 100644 --- a/packages/base/src/dialogs/symbology/symbologyUtils.ts +++ b/packages/base/src/dialogs/symbology/symbologyUtils.ts @@ -1,6 +1,8 @@ import { IJGISLayer } from '@jupytergis/schema'; import colormap from 'colormap'; +import { ColorRampName } from '@/src/types'; +import { VectorClassifications } from './classificationModes'; import { IStopRow } from './symbologyDialog'; const COLOR_EXPR_STOPS_START = 3; @@ -101,10 +103,29 @@ export namespace VectorUtils { export namespace Utils { export const getValueColorPairs = ( stops: number[], - selectedRamp: string, + selectedRamp: ColorRampName, nClasses: number, reverse = false, + renderType: + | 'Categorized' + | 'Graduated' + | 'Heatmap' + | 'Singleband Pseudocolor', + minValue: number, + maxValue: number, ) => { + let effectiveStops: number[] = []; + + if (stops && stops.length > 0) { + effectiveStops = stops.map(v => parseFloat(v.toFixed(2))); + } else { + effectiveStops = VectorClassifications.calculateEqualIntervalBreaks( + nClasses, + minValue, + maxValue, + ).map(v => parseFloat(v.toFixed(2))); + } + let colorMap = colormap({ colormap: selectedRamp, nshades: nClasses > 9 ? nClasses : 9, @@ -127,7 +148,7 @@ export namespace Utils { // Get the last n/2 elements from the second array const secondPart = colorMap.slice( - colorMap.length - (stops.length - firstPart.length), + colorMap.length - (effectiveStops.length - firstPart.length), ); // Create the new array by combining the first and last parts @@ -135,7 +156,7 @@ export namespace Utils { } for (let i = 0; i < nClasses; i++) { - valueColorPairs.push({ stop: stops[i], output: colorMap[i] }); + valueColorPairs.push({ stop: effectiveStops[i], output: colorMap[i] }); } return valueColorPairs; diff --git a/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx b/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx index 62036c54b..8d4893269 100644 --- a/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx +++ b/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { IBandRow } from '@/src/dialogs/symbology/hooks/useGetBandInfo'; @@ -28,111 +28,42 @@ const BandRow: React.FC = ({ bandRow, bandRows, setSelectedBand, - setBandRows, isMultibandColor, }) => { - const [minValue, setMinValue] = useState(bandRow?.stats.minimum); - const [maxValue, setMaxValue] = useState(bandRow?.stats.maximum); - - const handleMinValueChange = (event: { - target: { value: string | number }; - }) => { - setMinValue(+event.target.value); - setNewBands(); - }; - - const handleMaxValueChange = (event: { - target: { value: string | number }; - }) => { - setMaxValue(+event.target.value); - setNewBands(); - }; - - const setNewBands = () => { - const newBandRows = [...bandRows]; - newBandRows[index].stats.minimum = minValue; - newBandRows[index].stats.maximum = maxValue; - setBandRows(newBandRows); - }; - return ( - <> -
- -
- -
-
- {isMultibandColor ? null : ( -
-
- - + +
+ + {band.colorInterpretation + ? `Band ${band.band} (${band.colorInterpretation})` + : `Band ${band.band}`} + + ))} + {isMultibandColor ? ( +
-
- )} - + > + Unset + + ) : null} + +
+ ); }; diff --git a/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx index 0cc58f61d..100907c63 100644 --- a/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx +++ b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx @@ -20,6 +20,7 @@ import { Utils } from '@/src/dialogs/symbology/symbologyUtils'; import BandRow from '@/src/dialogs/symbology/tiff_layer/components/BandRow'; import { LoadingOverlay } from '@/src/shared/components/loading'; import { GlobalStateDbManager } from '@/src/store'; +import { ColorRampName } from '@/src/types'; export type InterpolationType = 'discrete' | 'linear' | 'exact'; @@ -46,7 +47,10 @@ const SingleBandPseudoColor: React.FC = ({ const [layerState, setLayerState] = useState(); const [selectedBand, setSelectedBand] = useState(1); + const [stopRows, setStopRows] = useState([]); + const [dataMin, setDataMin] = useState(); + const [dataMax, setDataMax] = useState(); const [selectedFunction, setSelectedFunction] = useState('linear'); const [colorRampOptions, setColorRampOptions] = useState< @@ -85,6 +89,12 @@ const SingleBandPseudoColor: React.FC = ({ selectedBandRef.current = selectedBand; }, [stopRows, selectedFunction, colorRampOptions, selectedBand, layerState]); + useEffect(() => { + if (bandRows.length > 0) { + applyActualRange(selectedBand); + } + }, [selectedBand, bandRows]); + const populateOptions = async () => { const layerState = (await stateDb?.fetch( `jupytergis:${layerId}`, @@ -100,6 +110,19 @@ const SingleBandPseudoColor: React.FC = ({ setSelectedFunction(interpolation); }; + const applyActualRange = (bandIndex: number) => { + const currentBand = bandRows[bandIndex - 1]; + if (!currentBand || !currentBand.stats) { + return; + } + + const min = currentBand.stats.minimum; + const max = currentBand.stats.maximum; + + setDataMin(min); + setDataMax(max); + }; + const buildColorInfo = () => { // This it to parse a color object on the layer if (!layer.parameters?.color || !layerState) { @@ -172,14 +195,6 @@ const SingleBandPseudoColor: React.FC = ({ const isQuantile = colorRampOptionsRef.current?.selectedMode === 'quantile'; - const sourceInfo = source.parameters.urls[0]; - sourceInfo.min = bandRow.stats.minimum; - sourceInfo.max = bandRow.stats.maximum; - - source.parameters.urls[0] = sourceInfo; - - model.sharedModel.updateSource(sourceId, source); - // Update layer if (!layer.parameters) { return; @@ -252,8 +267,11 @@ const SingleBandPseudoColor: React.FC = ({ band: selectedBandRef.current, interpolation: selectedFunctionRef.current, colorRamp: colorRampOptionsRef.current?.selectedRamp, + reverse: colorRampOptionsRef.current?.reverseRamp, nClasses: colorRampOptionsRef.current?.numberOfShades, mode: colorRampOptionsRef.current?.selectedMode, + min: colorRampOptionsRef.current?.minValue, + max: colorRampOptionsRef.current?.maxValue, }; layer.parameters.symbologyState = symbologyState; @@ -284,19 +302,24 @@ const SingleBandPseudoColor: React.FC = ({ const buildColorInfoFromClassification = async ( selectedMode: string, numberOfShades: string, - selectedRamp: string, + selectedRamp: ColorRampName, + reverseRamp: boolean, setIsLoading: (isLoading: boolean) => void, + minValue: number, + maxValue: number, ) => { // Update layer state with selected options setColorRampOptions({ selectedRamp, + reverseRamp, numberOfShades, selectedMode, + minValue, + maxValue, }); let stops: number[] = []; - const currentBand = bandRows[selectedBand - 1]; const source = model.getSource(layer?.parameters?.source); const sourceInfo = source?.parameters?.urls[0]; const nClasses = selectedMode === 'continuous' ? 52 : +numberOfShades; @@ -314,16 +337,16 @@ const SingleBandPseudoColor: React.FC = ({ case 'continuous': stops = GeoTiffClassifications.classifyContinuousBreaks( nClasses, - currentBand.stats.minimum, - currentBand.stats.maximum, + minValue, + maxValue, selectedFunction, ); break; case 'equal interval': stops = GeoTiffClassifications.classifyEqualIntervalBreaks( nClasses, - currentBand.stats.minimum, - currentBand.stats.maximum, + minValue, + maxValue, selectedFunction, ); break; @@ -337,6 +360,10 @@ const SingleBandPseudoColor: React.FC = ({ stops, selectedRamp, nClasses, + reverseRamp, + 'Singleband Pseudocolor', + minValue, + maxValue, ); setStopRows(valueColorPairs); @@ -403,6 +430,7 @@ const SingleBandPseudoColor: React.FC = ({ + {bandRows.length > 0 && ( = ({ classifyFunc={buildColorInfoFromClassification} showModeRow={true} showRampSelector={true} + renderType="Singleband Pseudocolor" + dataMin={dataMin} + dataMax={dataMax} /> )} +
diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx index 4e0e36a52..e9151e7fd 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx @@ -1,9 +1,10 @@ import { IVectorLayer } from '@jupytergis/schema'; -import { ReadonlyJSONObject } from '@lumino/coreutils'; import { ExpressionValue } from 'ol/expr/expression'; import React, { useEffect, useRef, useState } from 'react'; -import ColorRamp from '@/src/dialogs/symbology/components/color_ramp/ColorRamp'; +import ColorRamp, { + ColorRampOptions, +} from '@/src/dialogs/symbology/components/color_ramp/ColorRamp'; import StopContainer from '@/src/dialogs/symbology/components/color_stops/StopContainer'; import { IStopRow, @@ -11,7 +12,7 @@ import { } from '@/src/dialogs/symbology/symbologyDialog'; import { Utils, VectorUtils } from '@/src/dialogs/symbology/symbologyUtils'; import ValueSelect from '@/src/dialogs/symbology/vector_layer/components/ValueSelect'; -import { SymbologyTab } from '@/src/types'; +import { ColorRampName, SymbologyTab } from '@/src/types'; const Categorized: React.FC = ({ model, @@ -24,12 +25,12 @@ const Categorized: React.FC = ({ }) => { const selectedAttributeRef = useRef(); const stopRowsRef = useRef(); - const colorRampOptionsRef = useRef(); + const colorRampOptionsRef = useRef(); const [selectedAttribute, setSelectedAttribute] = useState(''); const [stopRows, setStopRows] = useState([]); const [colorRampOptions, setColorRampOptions] = useState< - ReadonlyJSONObject | undefined + ColorRampOptions | undefined >(); const [manualStyle, setManualStyle] = useState({ fillColor: '#3399CC', @@ -38,14 +39,15 @@ const Categorized: React.FC = ({ radius: 5, }); const manualStyleRef = useRef(manualStyle); - const [reverseRamp, setReverseRamp] = useState(false); + const [dataMin, setDataMin] = useState(); + const [dataMax, setDataMax] = useState(); if (!layerId) { - return; + return null; } const layer = model.getLayer(layerId); if (!layer?.parameters) { - return; + return null; } useEffect(() => { @@ -108,6 +110,15 @@ const Categorized: React.FC = ({ Object.keys(selectableAttributesAndValues)[0]; setSelectedAttribute(attribute); + + const values = Array.from(selectableAttributesAndValues[attribute] ?? []); + if (values.length > 0) { + const min = Math.min(...values); + const max = Math.max(...values); + + setDataMin(min); + setDataMax(max); + } }, [selectableAttributesAndValues]); useEffect(() => { @@ -119,14 +130,19 @@ const Categorized: React.FC = ({ const buildColorInfoFromClassification = ( selectedMode: string, numberOfShades: string, - selectedRamp: string, + selectedRamp: ColorRampName, + reverseRamp: boolean, setIsLoading: (isLoading: boolean) => void, + minValue: number, + maxValue: number, ) => { setColorRampOptions({ - selectedFunction: '', selectedRamp, - numberOfShades: '', - selectedMode: '', + reverseRamp, + numberOfShades, + selectedMode, + minValue, + maxValue, }); const stops = Array.from( @@ -138,6 +154,9 @@ const Categorized: React.FC = ({ selectedRamp, stops.length, reverseRamp, + 'Categorized', + minValue, + maxValue, ); setStopRows(valueColorPairs); @@ -181,10 +200,10 @@ const Categorized: React.FC = ({ renderType: 'Categorized', value: selectedAttributeRef.current, colorRamp: colorRampOptionsRef.current?.selectedRamp, + reverse: colorRampOptionsRef.current?.reverseRamp, nClasses: colorRampOptionsRef.current?.numberOfShades, mode: colorRampOptionsRef.current?.selectedMode, symbologyTab, - reverse: reverseRamp, }; layer.parameters.symbologyState = symbologyState; @@ -316,19 +335,6 @@ const Categorized: React.FC = ({ )}
- {symbologyTab === 'color' && ( -
- -
- )} -
= ({ classifyFunc={buildColorInfoFromClassification} showModeRow={false} showRampSelector={symbologyTab === 'color'} + renderType="Categorized" + dataMin={dataMin} + dataMax={dataMax} /> = ({ model, @@ -35,7 +36,6 @@ const Graduated: React.FC = ({ const symbologyTabRef = useRef(); const colorStopRowsRef = useRef([]); const radiusStopRowsRef = useRef([]); - const colorRampOptionsRef = useRef(); const [selectedAttribute, setSelectedAttribute] = useState(''); const [colorStopRows, setColorStopRows] = useState([]); @@ -51,8 +51,10 @@ const Graduated: React.FC = ({ const [radiusManualStyle, setRadiusManualStyle] = useState({ radius: 5, }); - const [reverseRamp, setReverseRamp] = useState(false); + const [dataMin, setDataMin] = useState(); + const [dataMax, setDataMax] = useState(); + const colorRampOptionsRef = useRef(); const colorManualStyleRef = useRef(colorManualStyle); const radiusManualStyleRef = useRef(radiusManualStyle); @@ -136,6 +138,15 @@ const Graduated: React.FC = ({ Object.keys(selectableAttributesAndValues)[0]; setSelectedAttribute(attribute); + + const values = Array.from(selectableAttributesAndValues[attribute] ?? []); + if (values.length > 0) { + const min = Math.min(...values); + const max = Math.max(...values); + + setDataMin(min); + setDataMax(max); + } }, [selectableAttributesAndValues]); const updateStopRowsBasedOnLayer = () => { @@ -201,9 +212,11 @@ const Graduated: React.FC = ({ value: selectableAttributeRef.current, method: symbologyTabRef.current, colorRamp: colorRampOptionsRef.current?.selectedRamp, + reverse: colorRampOptionsRef.current?.reverseRamp, nClasses: colorRampOptionsRef.current?.numberOfShades, mode: colorRampOptionsRef.current?.selectedMode, - reverse: reverseRamp, + min: colorRampOptionsRef.current?.minValue, + max: colorRampOptionsRef.current?.maxValue, }; if (layer.type === 'HeatmapLayer') { @@ -217,12 +230,21 @@ const Graduated: React.FC = ({ const buildColorInfoFromClassification = ( selectedMode: string, numberOfShades: string, - selectedRamp: string, + selectedRamp: ColorRampName, + reverseRamp: boolean, + setIsLoading: (isLoading: boolean) => void, + minValue: number, + maxValue: number, + criticalValue?: number, ) => { setColorRampOptions({ selectedRamp, + reverseRamp, numberOfShades, selectedMode, + minValue, + maxValue, + criticalValue, }); let stops: number[]; @@ -238,8 +260,9 @@ const Graduated: React.FC = ({ break; case 'equal interval': stops = VectorClassifications.calculateEqualIntervalBreaks( - values, +numberOfShades, + minValue, + maxValue, ); break; case 'jenks': @@ -267,12 +290,24 @@ const Graduated: React.FC = ({ const stopOutputPairs = symbologyTab === 'radius' - ? stops.map(v => ({ stop: v, output: v })) + ? stops.map(v => { + const scaled = + minValue !== undefined && maxValue !== undefined + ? minValue + + ((v - Math.min(...stops)) / + (Math.max(...stops) - Math.min(...stops))) * + (maxValue - minValue) + : v; + return { stop: scaled, output: scaled }; + }) : Utils.getValueColorPairs( stops, selectedRamp, +numberOfShades, reverseRamp, + 'Graduated', + minValue, + maxValue, ); if (symbologyTab === 'radius') { @@ -280,6 +315,8 @@ const Graduated: React.FC = ({ } else { setColorStopRows(stopOutputPairs); } + + setIsLoading(false); }; const handleReset = (method: string) => { @@ -389,25 +426,15 @@ const Graduated: React.FC = ({ )}
- {symbologyTab === 'color' && ( -
- -
- )} - = ({ radius: 8, blur: 15, }); - const reverseRampRef = useRef(false); + const reverseRampRef = useRef(false); // Do we need these refs here? Why not directly use the state? useEffect(() => { populateOptions(); @@ -102,18 +102,10 @@ const Heatmap: React.FC = ({
-
- -
0) { for (let i = 0; i < urls.length; i++) { - const { url, min, max } = urls[i]; + const { url } = urls[i]; if (this._isSubmitted) { const mimeType = getMimeType(url); if (!mimeType || !mimeType.startsWith('image/tiff')) { @@ -110,33 +110,10 @@ export class GeoTiffSourcePropertiesForm extends SourcePropertiesForm { `URL at index ${i} is required and must be a valid string.`, ); } - - if (min === undefined || typeof min !== 'number') { - errors.push( - `Min value at index ${i} is required and must be a number.`, - ); - valid = false; - } - - if (max === undefined || typeof max !== 'number') { - errors.push( - `Max value at index ${i} is required and must be a number.`, - ); - valid = false; - } - - if ( - typeof min === 'number' && - typeof max === 'number' && - max <= min - ) { - errors.push(`Max value at index ${i} must be greater than Min.`); - valid = false; - } } } } else { - errors.push('At least one valid URL with min/max values is required.'); + errors.push('At least one valid URL is required.'); valid = false; } diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index cd69f6eb9..d82c23698 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -96,7 +96,13 @@ import AnnotationFloater from '@/src/annotations/components/AnnotationFloater'; import { CommandIDs } from '@/src/constants'; import { LoadingOverlay } from '@/src/shared/components/loading'; import StatusBar from '@/src/statusbar/StatusBar'; -import { debounce, isLightTheme, loadFile, throttle } from '@/src/tools'; +import { + debounce, + isLightTheme, + loadFile, + objectEntries, + throttle, +} from '@/src/tools'; import CollaboratorPointers, { ClientPointer } from './CollaboratorPointers'; import { FollowIndicator } from './FollowIndicator'; import TemporalSlider from './TemporalSlider'; @@ -280,6 +286,23 @@ export class MainView extends React.Component { this._mainViewModel.dispose(); } + /** + * Identify which layer uses a source. + * + * Relies on the assumption that sources and layers have a 1:1 relationship. + */ + getLayerIdForSourceId(sourceId: string): string { + const layers = this._model.sharedModel.layers; + + for (const [layerId, layer] of objectEntries(layers)) { + if (layer.parameters?.source === sourceId) { + return layerId; + } + } + + throw new Error(`No layer found for source ID: ${sourceId}`); + } + async generateMap(center: number[], zoom: number): Promise { if (this.divRef.current) { this._Map = new OlMap({ @@ -760,6 +783,10 @@ export class MainView extends React.Component { } case 'GeoTiffSource': { const sourceParameters = source.parameters as IGeoTiffSource; + const layerId = this.getLayerIdForSourceId(id); + const layer = layerId + ? this._model.sharedModel.getLayer(layerId) + : undefined; const addNoData = (url: (typeof sourceParameters.urls)[0]) => { return { ...url, nodata: 0 }; @@ -773,9 +800,9 @@ export class MainView extends React.Component { if (isRemote) { return { ...addNoData(sourceInfo), - min: sourceInfo.min, - max: sourceInfo.max, url: sourceInfo.url, + min: layer?.parameters?.symbologyState?.min, + max: layer?.parameters?.symbologyState?.max, }; } else { const geotiff = await loadFile({ @@ -785,15 +812,31 @@ export class MainView extends React.Component { }); return { ...addNoData(sourceInfo), - min: sourceInfo.min, - max: sourceInfo.max, geotiff, url: URL.createObjectURL(geotiff.file), + min: layer?.parameters?.symbologyState?.min, + max: layer?.parameters?.symbologyState?.max, }; } }), ); + if (layer && layer.parameters) { + if (!layer.parameters.symbologyState) { + layer.parameters.symbologyState = { + renderType: 'Singleband Pseudocolor', + }; + } else { + // Backwards compatibility for older projects that have min/max at + // source.parameters.urls[0] (i.e. before + // https://github.com/geojupyter/jupytergis/pull/912) + layer.parameters.symbologyState.min ??= + sourceParameters.urls[0]?.min; + layer.parameters.symbologyState.max ??= + sourceParameters.urls[0]?.max; + } + } + newSource = new GeoTIFFSource({ interpolate: sourceParameters.interpolate, sources, @@ -860,7 +903,7 @@ export class MainView extends React.Component { */ async updateSource(id: string, source: IJGISSource): Promise { // get the layer id associated with this source - const layerId = this._sourceToLayerMap.get(id); + const layerId = this.getLayerIdForSourceId(id); // get the OL layer const mapLayer = this.getLayer(layerId); if (!mapLayer) { @@ -1096,12 +1139,6 @@ export class MainView extends React.Component { // STAC layers don't have source if (newMapLayer instanceof Layer) { - // we need to keep track of which source has which layers - // Only set sourceToLayerMap if 'source' exists on layerParameters - if ('source' in layerParameters) { - this._sourceToLayerMap.set(layerParameters.source, id); - } - this.addProjection(newMapLayer); await this._waitForSourceReady(newMapLayer); } @@ -1353,6 +1390,23 @@ export class MainView extends React.Component { color: layer.parameters.color, }); } + + // Update source when symbologyState.min/max changes + const sourceId = layer.parameters?.source; + if (sourceId && layer?.parameters?.symbologyState) { + const min = layer.parameters.symbologyState.min; + const max = layer.parameters.symbologyState.max; + const oldMin = oldLayer?.parameters?.symbologyState?.min; + const oldMax = oldLayer?.parameters?.symbologyState?.max; + + if (min !== oldMin || max !== oldMax) { + const sourceModel = + this._model.sharedModel.getLayerSource(sourceId); + if (sourceModel) { + this.updateSource(sourceId, sourceModel); + } + } + } break; } case 'HeatmapLayer': { @@ -2343,7 +2397,6 @@ export class MainView extends React.Component { private _mainViewModel: MainViewModel; private _ready = false; private _sources: Record; - private _sourceToLayerMap = new Map(); private _documentPath?: string; private _contextMenu: ContextMenu; private _loadingLayers: Set; diff --git a/packages/base/src/panelview/components/legendItem.tsx b/packages/base/src/panelview/components/legendItem.tsx index 50fa2fbd2..190a8ec2c 100644 --- a/packages/base/src/panelview/components/legendItem.tsx +++ b/packages/base/src/panelview/components/legendItem.tsx @@ -1,6 +1,7 @@ import { IJupyterGISModel } from '@jupytergis/schema'; import React, { useEffect, useState } from 'react'; +import { getColorMapList } from '@/src/dialogs/symbology/colorRampUtils'; import { useGetSymbology } from '@/src/dialogs/symbology/hooks/useGetSymbology'; export const LegendItem: React.FC<{ @@ -121,6 +122,11 @@ export const LegendItem: React.FC<{ return; } + const rampName = symbology.symbologyState?.colorRamp; + const rampDef = rampName + ? getColorMapList().find(c => c.name === rampName) + : undefined; + const segments = stops .map((s, i) => { const pct = (i / (stops.length - 1)) * 100; @@ -129,6 +135,17 @@ export const LegendItem: React.FC<{ .join(', '); const gradient = `linear-gradient(to right, ${segments})`; + const dataMin = symbology.symbologyState.dataMin ?? stops[0].value; + const dataMax = + symbology.symbologyState.dataMax ?? stops[stops.length - 1].value; + + let criticalValue: number | undefined = undefined; + if (rampDef?.definition.type === 'Divergent') { + const relativeCritical = symbology.symbologyState.criticalValue ?? 0.5; + criticalValue = dataMin + relativeCritical * (dataMax - dataMin); + } + const isDivergent = criticalValue !== undefined; + setContent(
{property && ( @@ -147,16 +164,74 @@ export const LegendItem: React.FC<{ marginTop: 10, }} > - {stops.map((s, i) => { - const left = (i / (stops.length - 1)) * 100; - const up = i % 2 === 0; - return ( + {!isDivergent ? ( + stops.map((s, i) => { + const left = (i / (stops.length - 1)) * 100; + const up = i % 2 === 0; + return ( +
+
+
+ {s.value.toFixed(2)} +
+
+ ); + }) + ) : ( + <> + {/* Min */} +
+
+
+ {dataMin.toFixed(2)} +
+
+ + {/* Max */}
+
+ {dataMax.toFixed(2)} +
+
+ + {/* Critical */} + {isDivergent && criticalValue !== undefined && (
= dataMax + ? 100 + : ((criticalValue - dataMin) / + (dataMax - dataMin)) * + 100 + }%`, + transform: 'translateX(-50%)', }} > - {s.value.toFixed(2)} +
+
+ {criticalValue.toFixed(1)} +
-
- ); - })} + )} + + )}
, ); @@ -197,6 +304,17 @@ export const LegendItem: React.FC<{ return; } + const numericCats = cats + .map(c => (typeof c.category === 'number' ? c.category : NaN)) + .filter(v => !isNaN(v)); + + const minValue = numericCats.length + ? Math.min(...numericCats) + : undefined; + const maxValue = numericCats.length + ? Math.max(...numericCats) + : undefined; + setContent(
{property && ( @@ -204,6 +322,7 @@ export const LegendItem: React.FC<{ {property}
)} +
{cats.map((c, i) => ( @@ -227,10 +347,24 @@ export const LegendItem: React.FC<{ borderRadius: 2, }} /> - {String(c.category)} + + {typeof c.category === 'number' + ? c.category.toFixed(2) + : String(c.category)} +
))}
+ + {/* Min/Max */} + {(minValue !== undefined || maxValue !== undefined) && ( +
+ {minValue !== undefined &&
Min: {minValue}
} + {maxValue !== undefined &&
Max: {maxValue}
} +
+ )}
, ); return; diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index 0660b3d00..5e3a76cc7 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -31,6 +31,9 @@ export const debounce = ( }; }; +export const objectKeys = Object.keys as >( + obj: T, +) => Array; export function throttle void>( callback: T, delay = 100, @@ -995,7 +998,7 @@ export async function getGeoJSONDataFromLayerSource( * code when using it. */ export const objectEntries = Object.entries as < - T extends Record, + T extends Record, >( obj: T, ) => Array<{ [K in keyof T]: [K, T[K]] }[keyof T]>; diff --git a/packages/base/src/types.ts b/packages/base/src/types.ts index 6ae72d4b2..7c3ebaa77 100644 --- a/packages/base/src/types.ts +++ b/packages/base/src/types.ts @@ -2,6 +2,8 @@ import { IDict, IJupyterGISWidget } from '@jupytergis/schema'; import { WidgetTracker } from '@jupyterlab/apputils'; import { Map } from 'ol'; +import { COLOR_RAMP_DEFINITIONS } from '@/src/dialogs/symbology/colorRamps'; + export { IDict }; export type ValueOf = T[keyof T]; @@ -40,3 +42,40 @@ declare global { jupytergisMaps: { [name: string]: Map }; } } + +/** + * Color ramp types and definitions + */ +export type ColorRampType = 'Sequential' | 'Divergent' | 'Cyclic'; + +export interface IBaseColorRampDefinition { + type: ColorRampType; +} + +export interface ISequentialColorRampDefinition + extends IBaseColorRampDefinition { + type: 'Sequential'; +} + +export interface IDivergentColorRampDefinition + extends IBaseColorRampDefinition { + type: 'Divergent'; + criticalValue: number; +} + +export interface ICyclicColorRampDefinition extends IBaseColorRampDefinition { + type: 'Cyclic'; +} + +export type IColorRampDefinition = + | ISequentialColorRampDefinition + | IDivergentColorRampDefinition + | ICyclicColorRampDefinition; + +export interface IColorMap { + name: ColorRampName; + colors: string[]; + definition: IColorRampDefinition; +} + +export type ColorRampName = keyof typeof COLOR_RAMP_DEFINITIONS; diff --git a/packages/base/style/base.css b/packages/base/style/base.css index 946f692e2..92c7571b3 100644 --- a/packages/base/style/base.css +++ b/packages/base/style/base.css @@ -65,6 +65,11 @@ position: relative; } +input.jp-mod-styled:disabled { + opacity: 0.5; + pointer-events: none; +} + /*This is being upstreamed. Will remove once upstream's been fixed.*/ button.jp-mod-styled.jp-mod-accept { background-color: var(--jp-brand-color1) !important; diff --git a/packages/base/style/symbologyDialog.css b/packages/base/style/symbologyDialog.css index 557fb416c..49129e7f5 100644 --- a/packages/base/style/symbologyDialog.css +++ b/packages/base/style/symbologyDialog.css @@ -23,7 +23,10 @@ select option { .jp-gis-symbology-row label { font-size: var(--jp-ui-font-size2); - flex: 0 1 20%; + flex: 0 1 auto; + display: inline-flex; + align-items: center; + gap: 0.4rem; } .jp-gis-symbology-row > .jp-select-wrapper, @@ -74,6 +77,8 @@ select option { display: flex; flex-direction: column; gap: 13px; + border-top: 1px solid var(--jp-border-color2); + padding-top: 8px; } .jp-gis-stop-container { @@ -183,6 +188,9 @@ select option { color: white; position: absolute; transition: transform 0.3s ease; + text-shadow: 0px 0px 4px black; + transform: scale(1); + transform-origin: left bottom; } .jp-gis-color-ramp-entry:hover .jp-gis-color-label { diff --git a/packages/schema/src/schema/project/sources/geoTiffSource.json b/packages/schema/src/schema/project/sources/geoTiffSource.json index 51e2389a4..a65190b5b 100644 --- a/packages/schema/src/schema/project/sources/geoTiffSource.json +++ b/packages/schema/src/schema/project/sources/geoTiffSource.json @@ -12,12 +12,6 @@ "properties": { "url": { "type": "string" - }, - "min": { - "type": "number" - }, - "max": { - "type": "number" } } },