From b58fb4c6f5ed18a6d446d144ea2d6a6e6a838e13 Mon Sep 17 00:00:00 2001 From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:00:02 +0000 Subject: [PATCH 1/9] fixing stepped colour issues --- .../VisualisationPanel/SteppedColorEditor.tsx | 215 +++++++++++++++--- .../VisualisationPanel/VisualisationPanel.tsx | 14 +- 2 files changed, 188 insertions(+), 41 deletions(-) diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx index f1fb7794..e194858c 100644 --- a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx +++ b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx @@ -164,33 +164,117 @@ export default function SteppedColorEditor() { const colorScheme = viewConfig.colorScheme || ColorScheme.RedBlue; const isReversed = Boolean(viewConfig.reverseColorScheme); + // Track previous column and dataSourceId to detect changes + const prevColumnRef = useRef(null); + const prevDataSourceIdRef = useRef(null); + + // Reset stepped color steps when data source or column changes + useEffect(() => { + const currentColumn = areaStats?.primary?.column; + const currentDataSourceId = areaStats?.dataSourceId; + + const columnChanged = + prevColumnRef.current !== null && + prevColumnRef.current !== currentColumn; + const dataSourceChanged = + prevDataSourceIdRef.current !== null && + prevDataSourceIdRef.current !== currentDataSourceId; + + // Reset when column or data source changes, and we have valid data + if ( + (columnChanged || dataSourceChanged) && + areaStats?.primary && + minValue !== undefined && + maxValue !== undefined && + maxValue >= minValue && + maxValue > minValue // Ensure we have a valid range + ) { + // Immediately clear old steps to prevent stepRanges from using invalid data + // Then set new defaults based on current data range + const stepSize = (maxValue - minValue) / 3; + const defaultStepRanges = [ + { start: minValue, end: minValue + stepSize }, + { start: minValue + stepSize, end: minValue + stepSize * 2 }, + { start: minValue + stepSize * 2, end: maxValue }, + ]; + + // Calculate colors for default steps + const interpolator = getInterpolator(colorScheme, viewConfig.customColor); + const defaultSteps = defaultStepRanges.map((rangeItem, index) => { + const numSteps = defaultStepRanges.length; + const gradientPosition = numSteps > 1 ? index / (numSteps - 1) : 0; + const t = isReversed ? 1 - gradientPosition : gradientPosition; + const clampedT = Math.max(0, Math.min(1, t)); + const color = interpolator(clampedT); + + return { + start: rangeItem.start, + end: rangeItem.end, + color: color || "#cccccc", + }; + }); + + // Update config with new steps - this will trigger stepRanges to recalculate + updateViewConfig({ steppedColorSteps: defaultSteps }); + } + + // Update refs for next comparison + prevColumnRef.current = currentColumn ?? null; + prevDataSourceIdRef.current = currentDataSourceId ?? null; + }, [ + areaStats?.primary?.column, + areaStats?.dataSourceId, + areaStats?.primary, + minValue, + maxValue, + colorScheme, + isReversed, + viewConfig.customColor, + updateViewConfig, + ]); + // Get step ranges (without colors) from config or defaults + // Simplified: if steps don't match current data range, always use defaults const stepRanges = useMemo(() => { - if (minValue === 0 && maxValue === 0) { + if (!minValue && !maxValue) { return []; } - if ( - viewConfig.steppedColorSteps && - viewConfig.steppedColorSteps.length > 0 - ) { - const ranges = viewConfig.steppedColorSteps.map((s) => ({ - start: s.start, - end: s.end, - })); - - // Ensure boundaries are connected - for (let i = 0; i < ranges.length - 1; i++) { - ranges[i].end = ranges[i + 1].start; - } - // Ensure first starts at minValue and last ends at maxValue - if (ranges.length > 0) { - ranges[0].start = minValue; - ranges[ranges.length - 1].end = maxValue; + // Check if we have steps that match the current data range + const steps = viewConfig.steppedColorSteps; + + if (steps && steps.length > 0) { + // Strict validation: ALL steps must be completely within [minValue, maxValue] + // No tolerance - if any step is outside, reject all steps + const allStepsInRange = steps.every( + (step) => + step.start >= minValue && + step.end <= maxValue && + step.start <= step.end, + ); + + // Check boundaries match exactly (with tiny tolerance only for floating point precision) + const tolerance = 0.00001; + const firstStep = steps[0]; + const lastStep = steps[steps.length - 1]; + const boundariesMatch = + firstStep && + lastStep && + Math.abs(firstStep.start - minValue) <= tolerance && + Math.abs(lastStep.end - maxValue) <= tolerance; + + // Only use steps if ALL are valid AND boundaries match + if (allStepsInRange && boundariesMatch) { + return steps.map((s) => ({ + start: s.start, + end: s.end, + })); } - - return ranges; + // If validation fails, fall through to defaults below } + + // Default: 3 steps evenly distributed (fall through if no steps or invalid) + // Default: 3 steps evenly distributed const stepSize = (maxValue - minValue) / 3; return [ @@ -221,26 +305,89 @@ export default function SteppedColorEditor() { }); }, [stepRanges, colorScheme, isReversed, viewConfig.customColor]); - // Set initial steps + // Clear invalid steps and set defaults when data range changes useEffect(() => { - if (!viewConfig.steppedColorSteps?.length && steps) { + if (!steps || steps.length === 0 || (!minValue && !maxValue)) { + return; + } + + const configSteps = viewConfig.steppedColorSteps; + + // If we have steps in config, validate them strictly + if (configSteps && configSteps.length > 0) { + const tolerance = 0.00001; + const firstStep = configSteps[0]; + const lastStep = configSteps[configSteps.length - 1]; + + // Strict check: ALL steps must be within [minValue, maxValue] + const allStepsInRange = configSteps.every( + (step) => + step.start >= minValue && + step.end <= maxValue && + step.start <= step.end, + ); + + const boundariesMatch = + firstStep && + lastStep && + Math.abs(firstStep.start - minValue) <= tolerance && + Math.abs(lastStep.end - maxValue) <= tolerance; + + // If steps are invalid, immediately replace with computed defaults + if (!allStepsInRange || !boundariesMatch) { + updateViewConfig({ steppedColorSteps: steps }); + return; + } + + // If steps are valid, check if colors need updating (e.g., reverse or color scheme changed) + // Compare colors to see if they've changed + const colorsChanged = configSteps.length === steps.length && + configSteps.some((step, index) => { + const computedStep = steps[index]; + return computedStep && step.color !== computedStep.color; + }); + + if (colorsChanged) { + // Update colors while keeping the same ranges + updateViewConfig({ steppedColorSteps: steps }); + } + } else { + // No steps in config, set initial defaults updateViewConfig({ steppedColorSteps: steps }); } - }, [steps, updateViewConfig, viewConfig.steppedColorSteps]); + }, [steps, updateViewConfig, viewConfig.steppedColorSteps, minValue, maxValue]); + + // Track previous min/max to detect range changes + const prevMinMaxRef = useRef<{ min: number; max: number } | null>(null); - // Initialize local steps when dialog opens + // Clear localSteps when dialog closes useEffect(() => { - if (isOpen) { - if ( - viewConfig.steppedColorSteps && - viewConfig.steppedColorSteps.length > 0 - ) { - setLocalSteps(viewConfig.steppedColorSteps); - } else { - setLocalSteps(steps); - } + if (!isOpen) { + setLocalSteps([]); + prevMinMaxRef.current = null; } - }, [isOpen, viewConfig.steppedColorSteps, steps]); + }, [isOpen]); + + // Initialize local steps when dialog opens or when data range changes + // Always use computed steps - they're already validated in stepRanges + useEffect(() => { + if (!isOpen) { + return; + } + + if (!steps || steps.length === 0) { + setLocalSteps([]); + prevMinMaxRef.current = null; + return; + } + + // Always update localSteps to match computed steps + // Computed steps are already validated in stepRanges to be within [minValue, maxValue] + setLocalSteps([...steps]); + + // Update ref to track current range + prevMinMaxRef.current = { min: minValue, max: maxValue }; + }, [isOpen, steps, minValue, maxValue]); // Recalculate colors when color scheme changes (but don't auto-apply) const localStepsRef = useRef(localSteps); diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx index c11b6301..382a4415 100644 --- a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx +++ b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx @@ -275,7 +275,7 @@ export default function VisualisationPanel({ Visualisation

-
+
{!viewConfig.areaDataSourceId && ( @@ -553,7 +553,7 @@ export default function VisualisationPanel({ Style

-
+
{!viewConfig.areaDataSecondaryColumn && columnOneIsNumber && ( <>