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 && (
<>
-
+
{viewConfig.colorScaleType === ColorScaleType.Gradient
? "Gradient"
@@ -635,7 +635,7 @@ export default function VisualisationPanel({
}
>
From c83a14378dd3dfbbf7e2fa01e4c5f0a3d561750e Mon Sep 17 00:00:00 2001
From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com>
Date: Tue, 20 Jan 2026 11:00:16 +0000
Subject: [PATCH 2/9] undoing styling to layer header
---
src/app/map/[id]/components/controls/LayerHeader.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/map/[id]/components/controls/LayerHeader.tsx b/src/app/map/[id]/components/controls/LayerHeader.tsx
index ff34e373..c26a81a3 100644
--- a/src/app/map/[id]/components/controls/LayerHeader.tsx
+++ b/src/app/map/[id]/components/controls/LayerHeader.tsx
@@ -38,7 +38,7 @@ export default function LayerHeader({
return (
From 0ff6359e86000388d8b6c3e86679c462202406fb Mon Sep 17 00:00:00 2001
From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com>
Date: Tue, 20 Jan 2026 14:39:01 +0000
Subject: [PATCH 3/9] lint fix
---
.../VisualisationPanel/SteppedColorEditor.tsx | 32 +++++++++++--------
.../VisualisationPanel/VisualisationPanel.tsx | 7 ++--
2 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
index e194858c..8a4359cd 100644
--- a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
+++ b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
@@ -174,8 +174,7 @@ export default function SteppedColorEditor() {
const currentDataSourceId = areaStats?.dataSourceId;
const columnChanged =
- prevColumnRef.current !== null &&
- prevColumnRef.current !== currentColumn;
+ prevColumnRef.current !== null && prevColumnRef.current !== currentColumn;
const dataSourceChanged =
prevDataSourceIdRef.current !== null &&
prevDataSourceIdRef.current !== currentDataSourceId;
@@ -242,7 +241,7 @@ export default function SteppedColorEditor() {
// 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
@@ -252,7 +251,7 @@ export default function SteppedColorEditor() {
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];
@@ -272,7 +271,7 @@ export default function SteppedColorEditor() {
}
// 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
@@ -312,13 +311,13 @@ export default function SteppedColorEditor() {
}
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) =>
@@ -326,7 +325,7 @@ export default function SteppedColorEditor() {
step.end <= maxValue &&
step.start <= step.end,
);
-
+
const boundariesMatch =
firstStep &&
lastStep &&
@@ -338,15 +337,16 @@ export default function SteppedColorEditor() {
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 &&
+ 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 });
@@ -355,7 +355,13 @@ export default function SteppedColorEditor() {
// No steps in config, set initial defaults
updateViewConfig({ steppedColorSteps: steps });
}
- }, [steps, updateViewConfig, viewConfig.steppedColorSteps, minValue, maxValue]);
+ }, [
+ steps,
+ updateViewConfig,
+ viewConfig.steppedColorSteps,
+ minValue,
+ maxValue,
+ ]);
// Track previous min/max to detect range changes
const prevMinMaxRef = useRef<{ min: number; max: number } | null>(null);
@@ -384,7 +390,7 @@ export default function SteppedColorEditor() {
// 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]);
diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx
index 382a4415..0a6551db 100644
--- a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx
+++ b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx
@@ -485,7 +485,7 @@ export default function VisualisationPanel({
{viewConfig.calculationType !== CalculationType.Count &&
columnOneIsNumber && (
-
+
-
+
{viewConfig.colorScaleType === ColorScaleType.Gradient
? "Gradient"
From d48b01ad18869c7eaaadf0fca235762072d20901 Mon Sep 17 00:00:00 2001
From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com>
Date: Tue, 20 Jan 2026 15:31:45 +0000
Subject: [PATCH 4/9] SteppedColourEditor Agent Refactor
---
.../VisualisationPanel/SteppedColorEditor.tsx | 439 +++++++-----------
1 file changed, 176 insertions(+), 263 deletions(-)
diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
index 8a4359cd..b879cb58 100644
--- a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
+++ b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
@@ -152,6 +152,84 @@ function RangeSlider({
);
}
+// Helper function to calculate color for a step based on its position
+function calculateStepColor(
+ index: number,
+ totalSteps: number,
+ colorScheme: ColorScheme,
+ customColor: string | undefined,
+ isReversed: boolean,
+): string {
+ const interpolator = getInterpolator(colorScheme, customColor);
+ const gradientPosition = totalSteps > 1 ? index / (totalSteps - 1) : 0;
+ const t = isReversed ? 1 - gradientPosition : gradientPosition;
+ const clampedT = Math.max(0, Math.min(1, t));
+ return interpolator(clampedT) || "#cccccc";
+}
+
+// Helper function to create default steps
+function createDefaultSteps(
+ minValue: number,
+ maxValue: number,
+ colorScheme: ColorScheme,
+ customColor: string | undefined,
+ isReversed: boolean,
+): SteppedColorStep[] {
+ const stepSize = (maxValue - minValue) / 3;
+ const ranges = [
+ { start: minValue, end: minValue + stepSize },
+ { start: minValue + stepSize, end: minValue + stepSize * 2 },
+ { start: minValue + stepSize * 2, end: maxValue },
+ ];
+
+ return ranges.map((range, index) => ({
+ start: range.start,
+ end: range.end,
+ color: calculateStepColor(index, ranges.length, colorScheme, customColor, isReversed),
+ }));
+}
+
+// Helper function to validate steps against data range
+function areStepsValid(
+ steps: SteppedColorStep[],
+ minValue: number,
+ maxValue: number,
+): boolean {
+ if (!steps || steps.length === 0) return false;
+
+ const tolerance = 0.00001;
+ const firstStep = steps[0];
+ const lastStep = steps[steps.length - 1];
+
+ const allStepsInRange = steps.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;
+
+ return allStepsInRange && boundariesMatch;
+}
+
+// Helper function to update step colors while preserving ranges
+function updateStepColors(
+ steps: SteppedColorStep[],
+ colorScheme: ColorScheme,
+ customColor: string | undefined,
+ isReversed: boolean,
+): SteppedColorStep[] {
+ return steps.map((step, index) => ({
+ ...step,
+ color: calculateStepColor(index, steps.length, colorScheme, customColor, isReversed),
+ }));
+}
+
export default function SteppedColorEditor() {
const { viewConfig, updateViewConfig } = useMapViews();
const areaStatsQuery = useAreaStats();
@@ -163,260 +241,101 @@ export default function SteppedColorEditor() {
const maxValue = areaStats?.primary?.maxValue ?? 0;
const colorScheme = viewConfig.colorScheme || ColorScheme.RedBlue;
const isReversed = Boolean(viewConfig.reverseColorScheme);
+ const hasValidRange = maxValue > minValue;
- // 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 && !maxValue) {
- return [];
- }
-
- // 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,
- }));
- }
- // 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 [
- { start: minValue, end: minValue + stepSize },
- { start: minValue + stepSize, end: minValue + stepSize * 2 },
- { start: minValue + stepSize * 2, end: maxValue },
- ];
- }, [viewConfig.steppedColorSteps, minValue, maxValue]);
+ // Create a key that changes when data source, column, or range changes
+ const dataKey = useMemo(() => {
+ if (!hasValidRange) return null;
+ return `${areaStats?.dataSourceId}-${areaStats?.primary?.column}-${minValue}-${maxValue}`;
+ }, [areaStats?.dataSourceId, areaStats?.primary?.column, minValue, maxValue, hasValidRange]);
- // Calculate steps with colors from gradient
- const steps = useMemo(() => {
- const interpolator = getInterpolator(colorScheme, viewConfig.customColor);
-
- return stepRanges.map((rangeItem, index) => {
- const numSteps = stepRanges.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",
- };
- });
- }, [stepRanges, colorScheme, isReversed, viewConfig.customColor]);
-
- // Clear invalid steps and set defaults when data range changes
- useEffect(() => {
- if (!steps || steps.length === 0 || (!minValue && !maxValue)) {
- return;
- }
+ // Compute valid steps from config or create defaults
+ const validSteps = useMemo(() => {
+ if (!hasValidRange) 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,
+
+ // If config steps are valid, use them (but update colors if needed)
+ if (configSteps && areStepsValid(configSteps, minValue, maxValue)) {
+ // Check if colors need updating (e.g., color scheme changed)
+ const updatedSteps = updateStepColors(
+ configSteps,
+ colorScheme,
+ viewConfig.customColor,
+ isReversed,
);
-
- 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;
- });
-
+
+ // Only update if colors actually changed
+ const colorsChanged = updatedSteps.some(
+ (step, index) => step.color !== configSteps[index]?.color,
+ );
+
if (colorsChanged) {
- // Update colors while keeping the same ranges
- updateViewConfig({ steppedColorSteps: steps });
+ // Update colors in config (async, won't block render)
+ updateViewConfig({ steppedColorSteps: updatedSteps });
}
- } else {
- // No steps in config, set initial defaults
- updateViewConfig({ steppedColorSteps: steps });
+
+ return updatedSteps;
+ }
+
+ // Create default steps
+ const defaults = createDefaultSteps(
+ minValue,
+ maxValue,
+ colorScheme,
+ viewConfig.customColor,
+ isReversed,
+ );
+
+ // Update config with defaults if we don't have valid steps
+ if (!configSteps || !areStepsValid(configSteps, minValue, maxValue)) {
+ updateViewConfig({ steppedColorSteps: defaults });
}
+
+ return defaults;
}, [
- steps,
- updateViewConfig,
+ hasValidRange,
viewConfig.steppedColorSteps,
minValue,
maxValue,
+ colorScheme,
+ viewConfig.customColor,
+ isReversed,
+ updateViewConfig,
]);
- // Track previous min/max to detect range changes
- const prevMinMaxRef = useRef<{ min: number; max: number } | null>(null);
-
- // Clear localSteps when dialog closes
- useEffect(() => {
- if (!isOpen) {
- setLocalSteps([]);
- prevMinMaxRef.current = null;
- }
- }, [isOpen]);
-
- // Initialize local steps when dialog opens or when data range changes
- // Always use computed steps - they're already validated in stepRanges
+ // Initialize local steps when dialog opens or data key changes
useEffect(() => {
- if (!isOpen) {
+ if (!isOpen || !validSteps.length) {
+ if (!isOpen) {
+ setLocalSteps([]);
+ }
return;
}
- if (!steps || steps.length === 0) {
- setLocalSteps([]);
- prevMinMaxRef.current = null;
- return;
- }
+ setLocalSteps([...validSteps]);
+ }, [isOpen, validSteps]);
- // Always update localSteps to match computed steps
- // Computed steps are already validated in stepRanges to be within [minValue, maxValue]
- setLocalSteps([...steps]);
+ // Update local step colors when color scheme changes (only if dialog is open)
+ useEffect(() => {
+ if (!isOpen || localSteps.length === 0) return;
- // Update ref to track current range
- prevMinMaxRef.current = { min: minValue, max: maxValue };
- }, [isOpen, steps, minValue, maxValue]);
+ const updatedSteps = updateStepColors(
+ localSteps,
+ colorScheme,
+ viewConfig.customColor,
+ isReversed,
+ );
- // Recalculate colors when color scheme changes (but don't auto-apply)
- const localStepsRef = useRef(localSteps);
- useEffect(() => {
- localStepsRef.current = localSteps;
- }, [localSteps]);
+ // Only update if colors changed
+ const colorsChanged = updatedSteps.some(
+ (step, index) => step.color !== localSteps[index]?.color,
+ );
- useEffect(() => {
- if (isOpen && localStepsRef.current.length > 0) {
- const interpolator = getInterpolator(colorScheme, viewConfig.customColor);
- const numSteps = localStepsRef.current.length;
- const updatedSteps = localStepsRef.current.map((step, index) => {
- const gradientPosition = numSteps > 1 ? index / (numSteps - 1) : 0;
- const t = isReversed ? 1 - gradientPosition : gradientPosition;
- const clampedT = Math.max(0, Math.min(1, t));
- return {
- ...step,
- color: interpolator(clampedT) || "#cccccc",
- };
- });
+ if (colorsChanged) {
setLocalSteps(updatedSteps);
}
- }, [colorScheme, isReversed, viewConfig.customColor, isOpen]);
+ }, [isOpen, colorScheme, viewConfig.customColor, isReversed, localSteps]);
if (
!areaStats ||
@@ -448,16 +367,14 @@ export default function SteppedColorEditor() {
}
// Recalculate colors
- const interpolator = getInterpolator(colorScheme, viewConfig.customColor);
- const numSteps = newSteps.length;
- newSteps.forEach((step, i) => {
- const gradientPosition = numSteps > 1 ? i / (numSteps - 1) : 0;
- const t = isReversed ? 1 - gradientPosition : gradientPosition;
- const clampedT = Math.max(0, Math.min(1, t));
- step.color = interpolator(clampedT) || "#cccccc";
- });
-
- setLocalSteps(newSteps);
+ const updatedSteps = updateStepColors(
+ newSteps,
+ colorScheme,
+ viewConfig.customColor,
+ isReversed,
+ );
+
+ setLocalSteps(updatedSteps);
};
const handleAddStep = () => {
@@ -472,21 +389,19 @@ export default function SteppedColorEditor() {
const newStep: SteppedColorStep = {
start: midpoint,
end: maxValue,
- color: "#cccccc",
+ color: "#cccccc", // Will be recalculated
};
newSteps.push(newStep);
// Recalculate colors
- const interpolator = getInterpolator(colorScheme, viewConfig.customColor);
- const numSteps = newSteps.length;
- newSteps.forEach((step, i) => {
- const gradientPosition = numSteps > 1 ? i / (numSteps - 1) : 0;
- const t = isReversed ? 1 - gradientPosition : gradientPosition;
- const clampedT = Math.max(0, Math.min(1, t));
- step.color = interpolator(clampedT) || "#cccccc";
- });
-
- setLocalSteps(newSteps);
+ const updatedSteps = updateStepColors(
+ newSteps,
+ colorScheme,
+ viewConfig.customColor,
+ isReversed,
+ );
+
+ setLocalSteps(updatedSteps);
};
const handleRemoveStep = (index: number) => {
@@ -503,20 +418,18 @@ export default function SteppedColorEditor() {
}
// Recalculate colors
- const interpolator = getInterpolator(colorScheme, viewConfig.customColor);
- const numSteps = newSteps.length;
- newSteps.forEach((step, i) => {
- const gradientPosition = numSteps > 1 ? i / (numSteps - 1) : 0;
- const t = isReversed ? 1 - gradientPosition : gradientPosition;
- const clampedT = Math.max(0, Math.min(1, t));
- step.color = interpolator(clampedT) || "#cccccc";
- });
-
- setLocalSteps(newSteps);
+ const updatedSteps = updateStepColors(
+ newSteps,
+ colorScheme,
+ viewConfig.customColor,
+ isReversed,
+ );
+
+ setLocalSteps(updatedSteps);
};
const handleReset = () => {
- setLocalSteps(steps);
+ setLocalSteps([...validSteps]);
};
const handleApply = () => {
@@ -570,7 +483,7 @@ export default function SteppedColorEditor() {
{localSteps.map((step, index) => {
const isFirst = index === 0;
- const isLast = index === steps.length - 1;
+ const isLast = index === localSteps.length - 1;
return (
Date: Tue, 20 Jan 2026 15:33:17 +0000
Subject: [PATCH 5/9] removed uncessary datakey
---
.../controls/VisualisationPanel/SteppedColorEditor.tsx | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
index b879cb58..6d651b56 100644
--- a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
+++ b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
@@ -244,11 +244,6 @@ export default function SteppedColorEditor() {
const hasValidRange = maxValue > minValue;
// Create a key that changes when data source, column, or range changes
- const dataKey = useMemo(() => {
- if (!hasValidRange) return null;
- return `${areaStats?.dataSourceId}-${areaStats?.primary?.column}-${minValue}-${maxValue}`;
- }, [areaStats?.dataSourceId, areaStats?.primary?.column, minValue, maxValue, hasValidRange]);
-
// Compute valid steps from config or create defaults
const validSteps = useMemo(() => {
if (!hasValidRange) return [];
From 29c5c696d80dda8771930b24f2300a9194fc1d1c Mon Sep 17 00:00:00 2001
From: Joaquim d'Souza
Date: Wed, 21 Jan 2026 13:01:27 +0100
Subject: [PATCH 6/9] refactor: stepped color selector
---
package-lock.json | 23 --
src/app/map/[id]/colors.ts | 115 ++++----
src/app/map/[id]/components/AreaInfo.tsx | 9 +-
src/app/map/[id]/components/AreaPopup.tsx | 5 +-
.../map/[id]/components/Choropleth/index.tsx | 8 +-
.../Choropleth/useChoroplethAreaStats.ts | 27 --
...StatesEffect.ts => useChoroplethColors.ts} | 21 +-
.../map/[id]/components/Choropleth/utils.ts | 23 ++
src/app/map/[id]/components/Legend.tsx | 28 +-
.../useBoundariesControl.tsx | 8 +-
.../CategoryColorEditor.tsx | 6 +-
.../VisualisationPanel/SteppedColorEditor.tsx | 248 +++++-------------
.../VisualisationPanel/VisualisationPanel.tsx | 3 +-
src/app/map/[id]/data.ts | 7 +-
src/app/map/[id]/hooks/useMapViews.ts | 4 +-
src/app/map/[id]/utils/mapView.ts | 4 +-
src/server/models/MapView.ts | 6 +-
17 files changed, 204 insertions(+), 341 deletions(-)
delete mode 100644 src/app/map/[id]/components/Choropleth/useChoroplethAreaStats.ts
rename src/app/map/[id]/components/Choropleth/{useChoroplethFeatureStatesEffect.ts => useChoroplethColors.ts} (83%)
create mode 100644 src/app/map/[id]/components/Choropleth/utils.ts
diff --git a/package-lock.json b/package-lock.json
index fa0e2a75..919cbcb1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14019,29 +14019,6 @@
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
- "node_modules/@tiptap/react/node_modules/@floating-ui/core": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
- "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@floating-ui/utils": "^0.2.10"
- }
- },
- "node_modules/@tiptap/react/node_modules/@floating-ui/dom": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
- "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@floating-ui/core": "^1.7.3",
- "@floating-ui/utils": "^0.2.10"
- }
- },
"node_modules/@tiptap/react/node_modules/@tiptap/extension-floating-menu": {
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.16.0.tgz",
diff --git a/src/app/map/[id]/colors.ts b/src/app/map/[id]/colors.ts
index 83caddd8..31a80674 100644
--- a/src/app/map/[id]/colors.ts
+++ b/src/app/map/[id]/colors.ts
@@ -18,8 +18,10 @@ import {
ColorScheme,
type SteppedColorStep,
} from "@/server/models/MapView";
+import { getChoroplethDataKey } from "./components/Choropleth/utils";
import { DEFAULT_FILL_COLOR, PARTY_COLORS } from "./constants";
import type { CombinedAreaStats } from "./data";
+import type { MapViewConfig } from "@/server/models/MapView";
import type { ScaleOrdinal, ScaleSequential } from "d3-scale";
import type { DataDrivenPropertyValueSpecification } from "mapbox-gl";
@@ -101,8 +103,24 @@ export const CHOROPLETH_COLOR_SCHEMES = [
},
];
+export const calculateStepColor = (
+ index: number,
+ totalSteps: number,
+ viewConfig: MapViewConfig,
+) => {
+ const { colorScheme, customColor, reverseColorScheme } = viewConfig;
+ const interpolator = getInterpolator(
+ colorScheme || ColorScheme.RedBlue,
+ customColor,
+ );
+ const gradientPosition = totalSteps > 1 ? index / (totalSteps - 1) : 0;
+ const t = reverseColorScheme ? 1 - gradientPosition : gradientPosition;
+ const clampedT = Math.max(0, Math.min(1, t));
+ return interpolator(clampedT) || "#cccccc";
+};
+
export const getInterpolator = (
- scheme: ColorScheme | undefined,
+ scheme: ColorScheme | null | undefined,
customColor?: string,
) => {
switch (scheme) {
@@ -130,41 +148,26 @@ export const getInterpolator = (
export const useColorScheme = ({
areaStats,
- scheme,
- isReversed,
- categoryColors,
- customColor,
+ viewConfig,
}: {
areaStats: CombinedAreaStats | null;
- scheme: ColorScheme;
- isReversed: boolean;
- categoryColors?: Record;
- customColor?: string;
+ viewConfig: MapViewConfig;
}): CategoricColorScheme | NumericColorScheme | null => {
// useMemo to cache calculated scales
return useMemo(() => {
return getColorScheme({
areaStats,
- scheme,
- isReversed,
- categoryColors,
- customColor,
+ viewConfig,
});
- }, [areaStats, scheme, isReversed, categoryColors, customColor]);
+ }, [areaStats, viewConfig]);
};
const getColorScheme = ({
areaStats,
- scheme,
- isReversed,
- categoryColors,
- customColor,
+ viewConfig,
}: {
areaStats: CombinedAreaStats | null;
- scheme: ColorScheme;
- isReversed: boolean;
- categoryColors?: Record;
- customColor?: string;
+ viewConfig: MapViewConfig;
}): CategoricColorScheme | NumericColorScheme | null => {
if (!areaStats || !areaStats.stats.length) {
return null;
@@ -179,7 +182,8 @@ const getColorScheme = ({
const colorMap: Record = {};
distinctValues.forEach((v) => {
// Use custom color if provided, otherwise use default
- colorMap[v] = categoryColors?.[v] ?? getCategoricalColor(v, colorScale);
+ colorMap[v] =
+ viewConfig.categoryColors?.[v] ?? getCategoricalColor(v, colorScale);
});
return {
columnType: ColumnType.String,
@@ -193,10 +197,13 @@ const getColorScheme = ({
const minValue = areaStats.primary.minValue;
const maxValue = areaStats.primary.maxValue;
if (minValue === maxValue) {
- const domain = isReversed ? [1, 0] : [0, 1];
+ const domain = viewConfig.reverseColorScheme ? [1, 0] : [0, 1];
// For count records, create a simple color scheme
// Use a small range to ensure valid interpolation
- const interpolator = getInterpolator(scheme, customColor);
+ const interpolator = getInterpolator(
+ viewConfig.colorScheme,
+ viewConfig.customColor,
+ );
const colorScale = scaleSequential()
.domain(domain) // Use 0-1 range for single values
.interpolator(interpolator);
@@ -210,12 +217,14 @@ const getColorScheme = ({
};
}
- const domain = (isReversed ? [maxValue, minValue] : [minValue, maxValue]) as [
- number,
- number,
- ];
+ const domain = (
+ viewConfig.reverseColorScheme ? [maxValue, minValue] : [minValue, maxValue]
+ ) as [number, number];
- const interpolator = getInterpolator(scheme, customColor);
+ const interpolator = getInterpolator(
+ viewConfig.colorScheme,
+ viewConfig.customColor,
+ );
const colorScale = scaleSequential()
.domain(domain)
.interpolator(interpolator);
@@ -237,22 +246,12 @@ const getCategoricalColor = (
export const useFillColor = ({
areaStats,
- scheme,
- isReversed,
+ viewConfig,
selectedBivariateBucket,
- categoryColors,
- colorScaleType,
- steppedColorSteps,
- customColor,
}: {
areaStats: CombinedAreaStats | null;
- scheme: ColorScheme;
- isReversed: boolean;
+ viewConfig: MapViewConfig;
selectedBivariateBucket: string | null;
- categoryColors?: Record;
- colorScaleType?: ColorScaleType;
- steppedColorSteps?: SteppedColorStep[];
- customColor?: string;
}): DataDrivenPropertyValueSpecification => {
// useMemo to cache calculated fillColor
return useMemo(() => {
@@ -263,10 +262,7 @@ export const useFillColor = ({
const isCount = areaStats?.calculationType === CalculationType.Count;
const colorScheme = getColorScheme({
areaStats,
- scheme,
- isReversed,
- categoryColors,
- customColor,
+ viewConfig,
});
if (!colorScheme) {
return DEFAULT_FILL_COLOR;
@@ -284,12 +280,14 @@ export const useFillColor = ({
}
// ColumnType.Number - Check if stepped colors are enabled
+ const steppedColorSteps =
+ viewConfig.steppedColorStepsByKey?.[getChoroplethDataKey(viewConfig)];
if (
- colorScaleType === ColorScaleType.Stepped &&
+ viewConfig.colorScaleType === ColorScaleType.Stepped &&
steppedColorSteps &&
steppedColorSteps.length > 0
) {
- return getSteppedFillColor(steppedColorSteps, isCount);
+ return getSteppedFillColor(steppedColorSteps, isCount, viewConfig);
}
// ColumnType.Number - Gradient (default)
@@ -330,21 +328,13 @@ export const useFillColor = ({
: ["feature-state", "value"],
...interpolateColorStops,
];
- }, [
- areaStats,
- isReversed,
- scheme,
- selectedBivariateBucket,
- categoryColors,
- colorScaleType,
- steppedColorSteps,
- customColor,
- ]);
+ }, [areaStats, viewConfig, selectedBivariateBucket]);
};
const getSteppedFillColor = (
steps: SteppedColorStep[],
isCount: boolean,
+ viewConfig: MapViewConfig,
): DataDrivenPropertyValueSpecification => {
// Sort steps by start value to ensure correct order
const sortedSteps = [...steps].sort((a, b) => a.start - b.start);
@@ -353,6 +343,10 @@ const getSteppedFillColor = (
return DEFAULT_FILL_COLOR;
}
+ if (sortedSteps.length === 1) {
+ return calculateStepColor(0, sortedSteps.length, viewConfig);
+ }
+
// Build a step expression: ["step", input, default, threshold1, color1, threshold2, color2, ...]
// Mapbox step expression: if value < threshold1, use default, else if value < threshold2, use color1, etc.
// For stepped colors, we want: if value < step1.start, use step1.color (or default)
@@ -365,7 +359,7 @@ const getSteppedFillColor = (
isCount
? ["coalesce", ["feature-state", "value"], 0]
: ["feature-state", "value"],
- sortedSteps[0]?.color || DEFAULT_FILL_COLOR, // Default color for values < first threshold
+ calculateStepColor(0, sortedSteps.length, viewConfig),
];
// Add thresholds and colors
@@ -373,8 +367,9 @@ const getSteppedFillColor = (
// The color applies to values >= threshold
for (let i = 1; i < sortedSteps.length; i++) {
const step = sortedSteps[i];
+ const color = calculateStepColor(i, sortedSteps.length, viewConfig);
stepExpression.push(step.start); // Threshold
- stepExpression.push(step.color); // Color for values >= threshold
+ stepExpression.push(color); // Color for values >= threshold
}
return stepExpression;
diff --git a/src/app/map/[id]/components/AreaInfo.tsx b/src/app/map/[id]/components/AreaInfo.tsx
index ab89bab8..a4b6b730 100644
--- a/src/app/map/[id]/components/AreaInfo.tsx
+++ b/src/app/map/[id]/components/AreaInfo.tsx
@@ -5,7 +5,7 @@ import { expression } from "mapbox-gl/dist/style-spec/index.cjs";
import { useMemo, useState } from "react";
import { ColumnType } from "@/server/models/DataSource";
-import { CalculationType, ColorScheme } from "@/server/models/MapView";
+import { CalculationType } from "@/server/models/MapView";
import {
Table,
TableBody,
@@ -84,13 +84,8 @@ export default function AreaInfo() {
const fillColor = useFillColor({
areaStats,
- scheme: viewConfig.colorScheme || ColorScheme.RedBlue,
- isReversed: Boolean(viewConfig.reverseColorScheme),
+ viewConfig,
selectedBivariateBucket: null,
- categoryColors: viewConfig.categoryColors,
- colorScaleType: viewConfig.colorScaleType,
- steppedColorSteps: viewConfig.steppedColorSteps,
- customColor: viewConfig.customColor,
});
// Combine selected areas and hover area, avoiding duplicates
diff --git a/src/app/map/[id]/components/AreaPopup.tsx b/src/app/map/[id]/components/AreaPopup.tsx
index 5acb4759..e51cf204 100644
--- a/src/app/map/[id]/components/AreaPopup.tsx
+++ b/src/app/map/[id]/components/AreaPopup.tsx
@@ -1,7 +1,7 @@
import { expression } from "mapbox-gl/dist/style-spec/index.cjs";
import { Popup } from "react-map-gl/mapbox";
import { ColumnType } from "@/server/models/DataSource";
-import { CalculationType, ColorScheme } from "@/server/models/MapView";
+import { CalculationType } from "@/server/models/MapView";
import { formatNumber } from "@/utils/text";
import { useFillColor } from "../colors";
import { useAreaStats } from "../data";
@@ -56,8 +56,7 @@ function WrappedAreaPopup({
const fillColor = useFillColor({
areaStats,
- scheme: viewConfig.colorScheme || ColorScheme.RedBlue,
- isReversed: Boolean(viewConfig.reverseColorScheme),
+ viewConfig,
selectedBivariateBucket: null,
});
diff --git a/src/app/map/[id]/components/Choropleth/index.tsx b/src/app/map/[id]/components/Choropleth/index.tsx
index 8d82915d..c6aee507 100644
--- a/src/app/map/[id]/components/Choropleth/index.tsx
+++ b/src/app/map/[id]/components/Choropleth/index.tsx
@@ -4,8 +4,10 @@ import { useMapViews } from "@/app/map/[id]/hooks/useMapViews";
import { MapType } from "@/server/models/MapView";
import { mapColors } from "../../styles";
import { getMapStyle } from "../../utils/map";
-import { useChoroplethAreaStats } from "./useChoroplethAreaStats";
-import { useChoroplethFeatureStatesEffect } from "./useChoroplethFeatureStatesEffect";
+import {
+ useChoroplethFeatureStatesEffect,
+ useChoroplethFillColor,
+} from "./useChoroplethColors";
export default function Choropleth() {
const { viewConfig } = useMapViews();
@@ -17,7 +19,7 @@ export default function Choropleth() {
const choroplethTopLayerId = "choropleth-top";
// Custom hooks for effects
- const fillColor = useChoroplethAreaStats();
+ const fillColor = useChoroplethFillColor();
const opacity = (viewConfig.choroplethOpacityPct ?? 80) / 100;
useChoroplethFeatureStatesEffect();
diff --git a/src/app/map/[id]/components/Choropleth/useChoroplethAreaStats.ts b/src/app/map/[id]/components/Choropleth/useChoroplethAreaStats.ts
deleted file mode 100644
index 1aaabaea..00000000
--- a/src/app/map/[id]/components/Choropleth/useChoroplethAreaStats.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useFillColor } from "@/app/map/[id]/colors";
-import { useAreaStats } from "@/app/map/[id]/data";
-import { useChoropleth } from "@/app/map/[id]/hooks/useChoropleth";
-import { useMapViews } from "@/app/map/[id]/hooks/useMapViews";
-import { ColorScheme } from "@/server/models/MapView";
-
-export function useChoroplethAreaStats() {
- const { selectedBivariateBucket } = useChoropleth();
-
- const { viewConfig } = useMapViews();
- const areaStatsQuery = useAreaStats();
- const areaStats = areaStatsQuery.data;
-
- // Get fill color
- const fillColor = useFillColor({
- areaStats,
- scheme: viewConfig.colorScheme || ColorScheme.RedBlue,
- isReversed: Boolean(viewConfig.reverseColorScheme),
- selectedBivariateBucket,
- categoryColors: viewConfig.categoryColors,
- colorScaleType: viewConfig.colorScaleType,
- steppedColorSteps: viewConfig.steppedColorSteps,
- customColor: viewConfig.customColor,
- });
-
- return fillColor;
-}
diff --git a/src/app/map/[id]/components/Choropleth/useChoroplethFeatureStatesEffect.ts b/src/app/map/[id]/components/Choropleth/useChoroplethColors.ts
similarity index 83%
rename from src/app/map/[id]/components/Choropleth/useChoroplethFeatureStatesEffect.ts
rename to src/app/map/[id]/components/Choropleth/useChoroplethColors.ts
index dc7decd5..831c3f8b 100644
--- a/src/app/map/[id]/components/Choropleth/useChoroplethFeatureStatesEffect.ts
+++ b/src/app/map/[id]/components/Choropleth/useChoroplethColors.ts
@@ -1,7 +1,26 @@
import { useEffect, useRef } from "react";
+import { useFillColor } from "@/app/map/[id]/colors";
import { useAreaStats } from "@/app/map/[id]/data";
import { useChoropleth } from "@/app/map/[id]/hooks/useChoropleth";
-import { useMapRef } from "../../hooks/useMapCore";
+import { useMapRef } from "@/app/map/[id]/hooks/useMapCore";
+import { useMapViews } from "@/app/map/[id]/hooks/useMapViews";
+
+export function useChoroplethFillColor() {
+ const { selectedBivariateBucket } = useChoropleth();
+
+ const { viewConfig } = useMapViews();
+ const areaStatsQuery = useAreaStats();
+ const areaStats = areaStatsQuery.data;
+
+ // Get fill color
+ const fillColor = useFillColor({
+ areaStats,
+ viewConfig,
+ selectedBivariateBucket,
+ });
+
+ return fillColor;
+}
export function useChoroplethFeatureStatesEffect() {
const mapRef = useMapRef();
diff --git a/src/app/map/[id]/components/Choropleth/utils.ts b/src/app/map/[id]/components/Choropleth/utils.ts
new file mode 100644
index 00000000..fb83fc05
--- /dev/null
+++ b/src/app/map/[id]/components/Choropleth/utils.ts
@@ -0,0 +1,23 @@
+import {
+ CalculationType,
+ DEFAULT_CALCULATION_TYPE,
+} from "@/server/models/MapView";
+
+export const getChoroplethDataKey = (viewConfig: {
+ calculationType?: CalculationType | null | undefined;
+ areaDataSourceId: string;
+ areaDataColumn: string;
+ areaDataSecondaryColumn?: string | null | undefined;
+}) => {
+ const calculationType =
+ viewConfig.calculationType || DEFAULT_CALCULATION_TYPE;
+ const parts: (string | null | undefined)[] = [
+ viewConfig.areaDataSourceId,
+ calculationType,
+ ];
+ if (viewConfig.calculationType !== CalculationType.Count) {
+ parts.push(viewConfig.areaDataColumn);
+ parts.push(viewConfig.areaDataSecondaryColumn);
+ }
+ return parts.filter(Boolean).join("-");
+};
diff --git a/src/app/map/[id]/components/Legend.tsx b/src/app/map/[id]/components/Legend.tsx
index 7d212911..53693a09 100644
--- a/src/app/map/[id]/components/Legend.tsx
+++ b/src/app/map/[id]/components/Legend.tsx
@@ -4,15 +4,12 @@ import { useChoroplethDataSource } from "@/app/map/[id]/hooks/useDataSources";
import { useMapViews } from "@/app/map/[id]/hooks/useMapViews";
import { MAX_COLUMN_KEY } from "@/constants";
import { ColumnType } from "@/server/models/DataSource";
-import {
- CalculationType,
- ColorScaleType,
- ColorScheme,
-} from "@/server/models/MapView";
+import { CalculationType, ColorScaleType } from "@/server/models/MapView";
import { formatNumber } from "@/utils/text";
-import { useColorScheme } from "../colors";
+import { calculateStepColor, useColorScheme } from "../colors";
import { useAreaStats } from "../data";
import BivariateLegend from "./BivariateLagend";
+import { getChoroplethDataKey } from "./Choropleth/utils";
export default function Legend() {
const { viewConfig, updateViewConfig } = useMapViews();
@@ -25,10 +22,7 @@ export default function Legend() {
const colorScheme = useColorScheme({
areaStats,
- scheme: viewConfig.colorScheme || ColorScheme.RedBlue,
- isReversed: Boolean(viewConfig.reverseColorScheme),
- categoryColors: viewConfig.categoryColors,
- customColor: viewConfig.customColor,
+ viewConfig,
});
const isLayerVisible = viewConfig.showChoropleth !== false;
@@ -72,12 +66,14 @@ export default function Legend() {
if (colorScheme.columnType === ColumnType.Number) {
// Handle stepped colors
+ const steppedColorSteps =
+ viewConfig.steppedColorStepsByKey?.[getChoroplethDataKey(viewConfig)];
if (
viewConfig.colorScaleType === ColorScaleType.Stepped &&
- viewConfig.steppedColorSteps &&
- viewConfig.steppedColorSteps.length > 0
+ steppedColorSteps &&
+ steppedColorSteps.length > 0
) {
- const sortedSteps = [...viewConfig.steppedColorSteps].sort(
+ const sortedSteps = [...steppedColorSteps].sort(
(a, b) => a.start - b.start,
);
const range = colorScheme.maxValue - colorScheme.minValue;
@@ -111,7 +107,11 @@ export default function Legend() {
className="h-full border-r border-neutral-400 last:border-r-0"
style={{
width: `${width}%`,
- backgroundColor: step.color,
+ backgroundColor: calculateStepColor(
+ index,
+ sortedSteps.length,
+ viewConfig,
+ ),
}}
/>
);
diff --git a/src/app/map/[id]/components/controls/BoundariesControl/useBoundariesControl.tsx b/src/app/map/[id]/components/controls/BoundariesControl/useBoundariesControl.tsx
index 0eff8ae4..f0167835 100644
--- a/src/app/map/[id]/components/controls/BoundariesControl/useBoundariesControl.tsx
+++ b/src/app/map/[id]/components/controls/BoundariesControl/useBoundariesControl.tsx
@@ -7,7 +7,11 @@ import { useMapViews } from "@/app/map/[id]/hooks/useMapViews";
import { MAX_COLUMN_KEY } from "@/constants";
import { AreaSetGroupCodeLabels } from "@/labels";
import { AreaSetGroupCode } from "@/server/models/AreaSet";
-import { CalculationType, MapType } from "@/server/models/MapView";
+import {
+ CalculationType,
+ DEFAULT_CALCULATION_TYPE,
+ MapType,
+} from "@/server/models/MapView";
export function useBoundariesControl() {
const { viewConfig, updateViewConfig } = useMapViews();
@@ -107,7 +111,7 @@ export function useBoundariesControl() {
updateViewConfig({
areaDataSourceId: voteShareDataSource.id,
areaDataColumn: column.name,
- calculationType: CalculationType.Avg,
+ calculationType: DEFAULT_CALCULATION_TYPE,
});
},
}))
diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/CategoryColorEditor.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/CategoryColorEditor.tsx
index 923dceae..8f9aea5e 100644
--- a/src/app/map/[id]/components/controls/VisualisationPanel/CategoryColorEditor.tsx
+++ b/src/app/map/[id]/components/controls/VisualisationPanel/CategoryColorEditor.tsx
@@ -4,7 +4,6 @@ import { useColorScheme } from "@/app/map/[id]/colors";
import { useAreaStats } from "@/app/map/[id]/data";
import { useMapViews } from "@/app/map/[id]/hooks/useMapViews";
import { ColumnType } from "@/server/models/DataSource";
-import { ColorScheme } from "@/server/models/MapView";
import { Button } from "@/shadcn/ui/button";
import {
Dialog,
@@ -22,10 +21,7 @@ export default function CategoryColorEditor() {
const colorScheme = useColorScheme({
areaStats,
- scheme: viewConfig.colorScheme || ColorScheme.RedBlue,
- isReversed: Boolean(viewConfig.reverseColorScheme),
- categoryColors: viewConfig.categoryColors,
- customColor: viewConfig.customColor,
+ viewConfig,
});
// Get unique categories from areaStats
diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
index 6d651b56..4af374c5 100644
--- a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
+++ b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
@@ -1,14 +1,10 @@
import { Plus, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { getInterpolator } from "@/app/map/[id]/colors";
+import { calculateStepColor } from "@/app/map/[id]/colors";
import { useAreaStats } from "@/app/map/[id]/data";
import { useMapViews } from "@/app/map/[id]/hooks/useMapViews";
import { ColumnType } from "@/server/models/DataSource";
-import {
- ColorScaleType,
- ColorScheme,
- type SteppedColorStep,
-} from "@/server/models/MapView";
+import { ColorScaleType, type SteppedColorStep } from "@/server/models/MapView";
import { Button } from "@/shadcn/ui/button";
import {
Dialog,
@@ -17,6 +13,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/shadcn/ui/dialog";
+import { getChoroplethDataKey } from "../../Choropleth/utils";
// Custom multi-handle range slider
function RangeSlider({
@@ -153,27 +150,11 @@ function RangeSlider({
}
// Helper function to calculate color for a step based on its position
-function calculateStepColor(
- index: number,
- totalSteps: number,
- colorScheme: ColorScheme,
- customColor: string | undefined,
- isReversed: boolean,
-): string {
- const interpolator = getInterpolator(colorScheme, customColor);
- const gradientPosition = totalSteps > 1 ? index / (totalSteps - 1) : 0;
- const t = isReversed ? 1 - gradientPosition : gradientPosition;
- const clampedT = Math.max(0, Math.min(1, t));
- return interpolator(clampedT) || "#cccccc";
-}
// Helper function to create default steps
function createDefaultSteps(
minValue: number,
maxValue: number,
- colorScheme: ColorScheme,
- customColor: string | undefined,
- isReversed: boolean,
): SteppedColorStep[] {
const stepSize = (maxValue - minValue) / 3;
const ranges = [
@@ -182,156 +163,65 @@ function createDefaultSteps(
{ start: minValue + stepSize * 2, end: maxValue },
];
- return ranges.map((range, index) => ({
+ return ranges.map((range) => ({
start: range.start,
end: range.end,
- color: calculateStepColor(index, ranges.length, colorScheme, customColor, isReversed),
- }));
-}
-
-// Helper function to validate steps against data range
-function areStepsValid(
- steps: SteppedColorStep[],
- minValue: number,
- maxValue: number,
-): boolean {
- if (!steps || steps.length === 0) return false;
-
- const tolerance = 0.00001;
- const firstStep = steps[0];
- const lastStep = steps[steps.length - 1];
-
- const allStepsInRange = steps.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;
-
- return allStepsInRange && boundariesMatch;
-}
-
-// Helper function to update step colors while preserving ranges
-function updateStepColors(
- steps: SteppedColorStep[],
- colorScheme: ColorScheme,
- customColor: string | undefined,
- isReversed: boolean,
-): SteppedColorStep[] {
- return steps.map((step, index) => ({
- ...step,
- color: calculateStepColor(index, steps.length, colorScheme, customColor, isReversed),
}));
}
export default function SteppedColorEditor() {
const { viewConfig, updateViewConfig } = useMapViews();
- const areaStatsQuery = useAreaStats();
- const areaStats = areaStatsQuery?.data;
+ const choroplethDataKey = getChoroplethDataKey(viewConfig);
+ const savedSteppedColorSteps =
+ viewConfig.steppedColorStepsByKey?.[choroplethDataKey];
+ const { data: areaStats } = useAreaStats();
const [isOpen, setIsOpen] = useState(false);
- const [localSteps, setLocalSteps] = useState([]);
+ const areaStatsDataKey = getChoroplethDataKey({
+ calculationType: areaStats?.calculationType,
+ areaDataSourceId: areaStats?.dataSourceId || "",
+ areaDataColumn: areaStats?.primary?.column || "",
+ areaDataSecondaryColumn: areaStats?.secondary?.column,
+ });
const minValue = areaStats?.primary?.minValue ?? 0;
const maxValue = areaStats?.primary?.maxValue ?? 0;
- const colorScheme = viewConfig.colorScheme || ColorScheme.RedBlue;
- const isReversed = Boolean(viewConfig.reverseColorScheme);
const hasValidRange = maxValue > minValue;
- // Create a key that changes when data source, column, or range changes
- // Compute valid steps from config or create defaults
- const validSteps = useMemo(() => {
- if (!hasValidRange) return [];
-
- const configSteps = viewConfig.steppedColorSteps;
-
- // If config steps are valid, use them (but update colors if needed)
- if (configSteps && areStepsValid(configSteps, minValue, maxValue)) {
- // Check if colors need updating (e.g., color scheme changed)
- const updatedSteps = updateStepColors(
- configSteps,
- colorScheme,
- viewConfig.customColor,
- isReversed,
- );
-
- // Only update if colors actually changed
- const colorsChanged = updatedSteps.some(
- (step, index) => step.color !== configSteps[index]?.color,
- );
-
- if (colorsChanged) {
- // Update colors in config (async, won't block render)
- updateViewConfig({ steppedColorSteps: updatedSteps });
- }
-
- return updatedSteps;
+ // Compute default steps
+ const defaultSteps = useMemo(() => {
+ if (!hasValidRange) {
+ return [];
}
+ return createDefaultSteps(minValue, maxValue);
+ }, [hasValidRange, maxValue, minValue]);
- // Create default steps
- const defaults = createDefaultSteps(
- minValue,
- maxValue,
- colorScheme,
- viewConfig.customColor,
- isReversed,
- );
-
- // Update config with defaults if we don't have valid steps
- if (!configSteps || !areStepsValid(configSteps, minValue, maxValue)) {
- updateViewConfig({ steppedColorSteps: defaults });
+ const [localSteps, setLocalSteps] = useState(
+ savedSteppedColorSteps || [],
+ );
+
+ // Initial setup
+ useEffect(() => {
+ if (
+ !savedSteppedColorSteps?.length &&
+ choroplethDataKey === areaStatsDataKey
+ ) {
+ setLocalSteps(defaultSteps);
+ updateViewConfig({
+ steppedColorStepsByKey: {
+ ...viewConfig.steppedColorStepsByKey,
+ [choroplethDataKey]: defaultSteps,
+ },
+ });
}
-
- return defaults;
}, [
- hasValidRange,
- viewConfig.steppedColorSteps,
- minValue,
- maxValue,
- colorScheme,
- viewConfig.customColor,
- isReversed,
+ areaStatsDataKey,
+ choroplethDataKey,
+ defaultSteps,
+ savedSteppedColorSteps?.length,
updateViewConfig,
+ viewConfig.steppedColorStepsByKey,
]);
- // Initialize local steps when dialog opens or data key changes
- useEffect(() => {
- if (!isOpen || !validSteps.length) {
- if (!isOpen) {
- setLocalSteps([]);
- }
- return;
- }
-
- setLocalSteps([...validSteps]);
- }, [isOpen, validSteps]);
-
- // Update local step colors when color scheme changes (only if dialog is open)
- useEffect(() => {
- if (!isOpen || localSteps.length === 0) return;
-
- const updatedSteps = updateStepColors(
- localSteps,
- colorScheme,
- viewConfig.customColor,
- isReversed,
- );
-
- // Only update if colors changed
- const colorsChanged = updatedSteps.some(
- (step, index) => step.color !== localSteps[index]?.color,
- );
-
- if (colorsChanged) {
- setLocalSteps(updatedSteps);
- }
- }, [isOpen, colorScheme, viewConfig.customColor, isReversed, localSteps]);
-
if (
!areaStats ||
areaStats.primary?.columnType !== ColumnType.Number ||
@@ -361,15 +251,7 @@ export default function SteppedColorEditor() {
newSteps[index - 1].end = newSteps[index].start;
}
- // Recalculate colors
- const updatedSteps = updateStepColors(
- newSteps,
- colorScheme,
- viewConfig.customColor,
- isReversed,
- );
-
- setLocalSteps(updatedSteps);
+ setLocalSteps(newSteps);
};
const handleAddStep = () => {
@@ -384,19 +266,10 @@ export default function SteppedColorEditor() {
const newStep: SteppedColorStep = {
start: midpoint,
end: maxValue,
- color: "#cccccc", // Will be recalculated
};
newSteps.push(newStep);
- // Recalculate colors
- const updatedSteps = updateStepColors(
- newSteps,
- colorScheme,
- viewConfig.customColor,
- isReversed,
- );
-
- setLocalSteps(updatedSteps);
+ setLocalSteps(newSteps);
};
const handleRemoveStep = (index: number) => {
@@ -412,19 +285,12 @@ export default function SteppedColorEditor() {
}
}
- // Recalculate colors
- const updatedSteps = updateStepColors(
- newSteps,
- colorScheme,
- viewConfig.customColor,
- isReversed,
- );
-
- setLocalSteps(updatedSteps);
+ setLocalSteps(newSteps);
};
const handleReset = () => {
- setLocalSteps([...validSteps]);
+ const steps = savedSteppedColorSteps || defaultSteps;
+ setLocalSteps([...steps]);
};
const handleApply = () => {
@@ -443,7 +309,10 @@ export default function SteppedColorEditor() {
}
updateViewConfig({
- steppedColorSteps: stepsToApply.length > 0 ? stepsToApply : undefined,
+ steppedColorStepsByKey: {
+ ...viewConfig.steppedColorStepsByKey,
+ [choroplethDataKey]: stepsToApply,
+ },
});
setIsOpen(false);
};
@@ -479,6 +348,11 @@ export default function SteppedColorEditor() {
{localSteps.map((step, index) => {
const isFirst = index === 0;
const isLast = index === localSteps.length - 1;
+ const stepColor = calculateStepColor(
+ index,
+ localSteps.length,
+ viewConfig,
+ );
return (
@@ -531,7 +405,7 @@ export default function SteppedColorEditor() {
}
}}
step={stepSize}
- color={step.color}
+ color={stepColor}
isFirst={isFirst}
isLast={isLast}
/>
@@ -548,7 +422,7 @@ export default function SteppedColorEditor() {
- {viewConfig.steppedColorSteps && (
+ {localSteps && (
diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx
index 0a6551db..8bf9e670 100644
--- a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx
+++ b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx
@@ -21,6 +21,7 @@ import {
CalculationType,
ColorScaleType,
ColorScheme,
+ DEFAULT_CALCULATION_TYPE,
} from "@/server/models/MapView";
import { Button } from "@/shadcn/ui/button";
import { Checkbox } from "@/shadcn/ui/checkbox";
@@ -328,7 +329,7 @@ export default function VisualisationPanel({
calculationType:
value === "counts"
? CalculationType.Count
- : CalculationType.Avg,
+ : DEFAULT_CALCULATION_TYPE,
})
}
>
diff --git a/src/app/map/[id]/data.ts b/src/app/map/[id]/data.ts
index 8db647a8..169b508f 100644
--- a/src/app/map/[id]/data.ts
+++ b/src/app/map/[id]/data.ts
@@ -1,6 +1,9 @@
import { useQuery as useTanstackQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useState } from "react";
-import { CalculationType } from "@/server/models/MapView";
+import {
+ CalculationType,
+ DEFAULT_CALCULATION_TYPE,
+} from "@/server/models/MapView";
import { useTRPC } from "@/services/trpc/react";
import { useChoropleth } from "./hooks/useChoropleth";
import { useMapBounds } from "./hooks/useMapBounds";
@@ -105,7 +108,7 @@ export const useAreaStats = () => {
trpc.area.stats.queryOptions(
{
areaSetCode,
- calculationType: calculationType || CalculationType.Avg,
+ calculationType: calculationType || DEFAULT_CALCULATION_TYPE,
dataSourceId,
column: columnOrCount,
secondaryColumn: secondaryColumn,
diff --git a/src/app/map/[id]/hooks/useMapViews.ts b/src/app/map/[id]/hooks/useMapViews.ts
index b469309b..899422e9 100644
--- a/src/app/map/[id]/hooks/useMapViews.ts
+++ b/src/app/map/[id]/hooks/useMapViews.ts
@@ -6,7 +6,7 @@ import { useCallback, useContext, useMemo } from "react";
import { toast } from "sonner";
import { AreaSetGroupCode } from "@/server/models/AreaSet";
import {
- CalculationType,
+ DEFAULT_CALCULATION_TYPE,
MapType,
type MapViewConfig,
} from "@/server/models/MapView";
@@ -201,7 +201,7 @@ export function useMapViews() {
// Fallback to the default calculation type if a data column has been selected
if (viewConfig.areaDataColumn && !viewConfig.calculationType) {
- viewConfig.calculationType = CalculationType.Avg;
+ viewConfig.calculationType = DEFAULT_CALCULATION_TYPE;
}
// Clear the selected columns when the user changes the data source
diff --git a/src/app/map/[id]/utils/mapView.ts b/src/app/map/[id]/utils/mapView.ts
index b32476ed..3b15201e 100644
--- a/src/app/map/[id]/utils/mapView.ts
+++ b/src/app/map/[id]/utils/mapView.ts
@@ -1,6 +1,6 @@
import {
- CalculationType,
ColorScheme,
+ DEFAULT_CALCULATION_TYPE,
MapStyleName,
} from "@/server/models/MapView";
import type { MapViewConfig } from "@/server/models/MapView";
@@ -17,7 +17,7 @@ export const createNewViewConfig = (): MapViewConfig => {
showMembers: true,
showLocations: true,
showTurf: true,
- calculationType: CalculationType.Avg,
+ calculationType: DEFAULT_CALCULATION_TYPE,
colorScheme: ColorScheme.RedBlue,
reverseColorScheme: false,
};
diff --git a/src/server/models/MapView.ts b/src/server/models/MapView.ts
index f2428465..81127c99 100644
--- a/src/server/models/MapView.ts
+++ b/src/server/models/MapView.ts
@@ -74,6 +74,7 @@ export enum CalculationType {
}
export const calculationTypes = Object.values(CalculationType);
export const calculationType = z.nativeEnum(CalculationType);
+export const DEFAULT_CALCULATION_TYPE = CalculationType.Avg;
export enum ColorScheme {
RedBlue = "RedBlue",
@@ -109,7 +110,6 @@ export const mapStyleNames = Object.values(MapStyleName);
export const steppedColorStepSchema = z.object({
start: z.number(),
end: z.number(),
- color: z.string(),
});
export type SteppedColorStep = z.infer;
@@ -135,7 +135,9 @@ export const mapViewConfigSchema = z.object({
reverseColorScheme: z.boolean().nullish(),
categoryColors: z.record(z.string(), z.string()).optional(),
colorScaleType: z.nativeEnum(ColorScaleType).optional(),
- steppedColorSteps: z.array(steppedColorStepSchema).optional(),
+ steppedColorStepsByKey: z
+ .record(z.string(), z.array(steppedColorStepSchema))
+ .optional(),
customColor: z.string().optional(),
});
From fb38841e37b0deac745cdb7cff640a0ce3b7c531 Mon Sep 17 00:00:00 2001
From: Joaquim d'Souza
Date: Wed, 21 Jan 2026 13:07:12 +0100
Subject: [PATCH 7/9] fix: npm dependencies issue
---
package-lock.json | 77 ++++++++++++++++++-----------------------------
package.json | 2 ++
2 files changed, 31 insertions(+), 48 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 919cbcb1..4fc1f761 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -139,6 +139,8 @@
"vitest": "^3.2.4"
},
"optionalDependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/dom": "^1.7.4",
"@img/sharp-linux-x64": "^0.34.1",
"@ngrok/ngrok-linux-x64-gnu": "^1.5",
"@rollup/rollup-linux-x64-gnu": "^4.44",
@@ -3842,18 +3844,22 @@
}
},
"node_modules/@floating-ui/core": {
- "version": "0.7.3",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
- "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==",
- "license": "MIT"
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
},
"node_modules/@floating-ui/dom": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz",
- "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
- "@floating-ui/core": "^0.7.3"
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react-dom": {
@@ -3869,25 +3875,6 @@
"react-dom": ">=16.8.0"
}
},
- "node_modules/@floating-ui/react-dom/node_modules/@floating-ui/core": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
- "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/utils": "^0.2.10"
- }
- },
- "node_modules/@floating-ui/react-dom/node_modules/@floating-ui/dom": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
- "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/core": "^1.7.3",
- "@floating-ui/utils": "^0.2.10"
- }
- },
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
@@ -5048,6 +5035,21 @@
"mapbox-gl": ">=2.7.0"
}
},
+ "node_modules/@mapbox/search-js-web/node_modules/@floating-ui/core": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
+ "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==",
+ "license": "MIT"
+ },
+ "node_modules/@mapbox/search-js-web/node_modules/@floating-ui/dom": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz",
+ "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^0.7.3"
+ }
+ },
"node_modules/@mapbox/sphericalmercator": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.2.0.tgz",
@@ -13660,27 +13662,6 @@
"@tiptap/pm": "^3.16.0"
}
},
- "node_modules/@tiptap/extension-bubble-menu/node_modules/@floating-ui/core": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
- "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@floating-ui/utils": "^0.2.10"
- }
- },
- "node_modules/@tiptap/extension-bubble-menu/node_modules/@floating-ui/dom": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
- "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@floating-ui/core": "^1.7.3",
- "@floating-ui/utils": "^0.2.10"
- }
- },
"node_modules/@tiptap/extension-bullet-list": {
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.16.0.tgz",
diff --git a/package.json b/package.json
index fbb3294b..a3df55a8 100644
--- a/package.json
+++ b/package.json
@@ -151,6 +151,8 @@
},
"optionalDependencies": {
"@img/sharp-linux-x64": "^0.34.1",
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/dom": "^1.7.4",
"@ngrok/ngrok-linux-x64-gnu": "^1.5",
"@rollup/rollup-linux-x64-gnu": "^4.44",
"lightningcss-linux-x64-gnu": "^1.30.1"
From 4f5c18da3ffcbd555fabb90d2d822313df3f94fe Mon Sep 17 00:00:00 2001
From: joaquimds
Date: Wed, 21 Jan 2026 13:09:41 +0100
Subject: [PATCH 8/9] Update src/app/map/[id]/components/Choropleth/utils.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/app/map/[id]/components/Choropleth/utils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/map/[id]/components/Choropleth/utils.ts b/src/app/map/[id]/components/Choropleth/utils.ts
index fb83fc05..8dd792d6 100644
--- a/src/app/map/[id]/components/Choropleth/utils.ts
+++ b/src/app/map/[id]/components/Choropleth/utils.ts
@@ -15,7 +15,7 @@ export const getChoroplethDataKey = (viewConfig: {
viewConfig.areaDataSourceId,
calculationType,
];
- if (viewConfig.calculationType !== CalculationType.Count) {
+ if (calculationType !== CalculationType.Count) {
parts.push(viewConfig.areaDataColumn);
parts.push(viewConfig.areaDataSecondaryColumn);
}
From 0dfd60e22733b7c022169b5a8a660b7397a6ff8b Mon Sep 17 00:00:00 2001
From: joaquimds
Date: Wed, 21 Jan 2026 13:10:20 +0100
Subject: [PATCH 9/9] Update
src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../controls/VisualisationPanel/SteppedColorEditor.tsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
index 4af374c5..6f939b7a 100644
--- a/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
+++ b/src/app/map/[id]/components/controls/VisualisationPanel/SteppedColorEditor.tsx
@@ -149,8 +149,6 @@ function RangeSlider({
);
}
-// Helper function to calculate color for a step based on its position
-
// Helper function to create default steps
function createDefaultSteps(
minValue: number,