From 21c9bc941d4cc001a9102aa7d16d34194b47abce Mon Sep 17 00:00:00 2001 From: Joaquim d'Souza Date: Thu, 5 Feb 2026 14:02:15 +0100 Subject: [PATCH 1/2] fix: minor bugs for mapped for funders demo --- .../[id]/components/ColumnRoleFields.tsx | 4 +- src/app/map/[id]/colors.ts | 9 +- .../BoundaryHoverInfo/AreasList.tsx | 24 +- .../components/BoundaryHoverInfo/utils.ts | 36 - .../map/[id]/components/Choropleth/index.tsx | 1 + .../Choropleth/useChoroplethColors.ts | 6 +- .../VisualisationPanel/VisualisationPanel.tsx | 622 +++++++++--------- .../inspector/BoundaryDataPanel.tsx | 12 +- src/app/map/[id]/utils/stats.ts | 33 + 9 files changed, 383 insertions(+), 364 deletions(-) create mode 100644 src/app/map/[id]/utils/stats.ts diff --git a/src/app/(private)/data-sources/[id]/components/ColumnRoleFields.tsx b/src/app/(private)/data-sources/[id]/components/ColumnRoleFields.tsx index 5bec236a..aa0a4989 100644 --- a/src/app/(private)/data-sources/[id]/components/ColumnRoleFields.tsx +++ b/src/app/(private)/data-sources/[id]/components/ColumnRoleFields.tsx @@ -39,7 +39,7 @@ export function ColumnRoleFields({ /> ({ @@ -50,7 +50,7 @@ export function ColumnRoleFields({ /> => { // useMemo to cache calculated fillColor - return useMemo(() => { + const fillColor = useMemo(() => { if (areaStats?.secondary) { return getBivariateFillColor(areaStats, selectedBivariateBucket); } @@ -340,6 +340,13 @@ export const useFillColor = ({ ...interpolateColorStops, ]; }, [areaStats, viewConfig, selectedBivariateBucket]); + + return [ + "case", + ["!=", ["feature-state", "value"], null], + fillColor, + DEFAULT_FILL_COLOR, + ]; }; const getSteppedFillColor = ( diff --git a/src/app/map/[id]/components/BoundaryHoverInfo/AreasList.tsx b/src/app/map/[id]/components/BoundaryHoverInfo/AreasList.tsx index d0bbba5b..67e725fa 100644 --- a/src/app/map/[id]/components/BoundaryHoverInfo/AreasList.tsx +++ b/src/app/map/[id]/components/BoundaryHoverInfo/AreasList.tsx @@ -7,7 +7,7 @@ import { TableRow, } from "@/shadcn/ui/table"; -import { getDisplayValue } from "./utils"; +import { getDisplayValue } from "../../utils/stats"; import type { ColumnType } from "@/server/models/DataSource"; import type { CalculationType } from "@/server/models/MapView"; @@ -113,18 +113,20 @@ export function AreasList({ : null; const primaryValue = areaStats - ? getDisplayValue( - areaStats.calculationType, - areaStats.primary, - areaStat?.primary, - ) + ? getDisplayValue(areaStat?.primary, { + calculationType: areaStats.calculationType, + columnType: areaStats.primary?.columnType, + minValue: areaStats.primary?.minValue, + maxValue: areaStats.primary?.maxValue, + }) : null; const secondaryValue = areaStats - ? getDisplayValue( - areaStats.calculationType, - areaStats.secondary, - areaStat?.secondary, - ) + ? getDisplayValue(areaStat?.secondary, { + calculationType: areaStats.calculationType, + columnType: areaStats.primary?.columnType, + minValue: areaStats.secondary?.minValue, + maxValue: areaStats.secondary?.maxValue, + }) : null; return ( diff --git a/src/app/map/[id]/components/BoundaryHoverInfo/utils.ts b/src/app/map/[id]/components/BoundaryHoverInfo/utils.ts index 91cba351..5e36a310 100644 --- a/src/app/map/[id]/components/BoundaryHoverInfo/utils.ts +++ b/src/app/map/[id]/components/BoundaryHoverInfo/utils.ts @@ -1,39 +1,3 @@ -import { ColumnType } from "@/server/models/DataSource"; -import { CalculationType } from "@/server/models/MapView"; -import { formatNumber } from "@/utils/text"; - -export const getDisplayValue = ( - calculationType: CalculationType | null | undefined, - areaStats: - | { - columnType: ColumnType; - minValue: number; - maxValue: number; - } - | undefined - | null, - areaStatValue: unknown, -): string => { - if ( - areaStatValue === undefined || - areaStatValue === null || - areaStatValue === "" - ) { - return calculationType === CalculationType.Count ? "0" : "-"; - } - if (areaStats?.columnType !== ColumnType.Number) { - return String(areaStatValue); - } - const value = Number(areaStatValue); - if (isNaN(value)) { - return "-"; - } - if (areaStats?.minValue >= 0 && areaStats?.maxValue <= 1) { - return `${Math.round(value * 1000) / 10}%`; - } - return formatNumber(value); -}; - export const toRGBA = (expressionResult: unknown) => { if ( !expressionResult || diff --git a/src/app/map/[id]/components/Choropleth/index.tsx b/src/app/map/[id]/components/Choropleth/index.tsx index 11e663c0..5672c34d 100644 --- a/src/app/map/[id]/components/Choropleth/index.tsx +++ b/src/app/map/[id]/components/Choropleth/index.tsx @@ -20,6 +20,7 @@ export default function Choropleth() { // Custom hooks for effects const fillColor = useChoroplethFillColor(); + const opacity = (viewConfig.choroplethOpacityPct ?? 80) / 100; const hoverSourceId = `${sourceId}-hover`; diff --git a/src/app/map/[id]/components/Choropleth/useChoroplethColors.ts b/src/app/map/[id]/components/Choropleth/useChoroplethColors.ts index 831c3f8b..46cb56c1 100644 --- a/src/app/map/[id]/components/Choropleth/useChoroplethColors.ts +++ b/src/app/map/[id]/components/Choropleth/useChoroplethColors.ts @@ -42,6 +42,7 @@ export function useChoroplethFeatureStatesEffect() { useEffect(() => { const map = mapRef?.current; if (!areaStats || !map) { + prevAreaStatValues.current = new Map(); return; } @@ -107,10 +108,5 @@ export function useChoroplethFeatureStatesEffect() { areaCodesToClean.current = nextAreaCodesToClean; prevAreaStatValues.current = nextStatValues; - - return () => { - areaCodesToClean.current = {}; - prevAreaStatValues.current = new Map(); - }; }, [areaStats, lastLoadedSourceId, layerId, mapRef, sourceId]); } diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx index 845ca535..4faf51c2 100644 --- a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx +++ b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx @@ -167,6 +167,7 @@ export default function VisualisationPanel({ ); const showEmptyZeroSwitch = !isCount && columnOneIsNumber; + const showStyle = !viewConfig.areaDataSecondaryColumn; const canSelectColorScale = isCount || columnOneIsNumber; const canSelectColorScheme = canSelectColorScale && !isCategorical; const canSetCategoryColors = isCategorical; @@ -500,251 +501,275 @@ export default function VisualisationPanel({ )} -
- {/* Color Scheme Selection */} -

- - Style -

+ {showStyle && ( +
+ {/* Color Scheme Selection */} -
- {canSelectColorScale && ( - <> - +

+ + Style +

- - - {canSelectColorScheme && ( - <> - + Color scale + - + updateViewConfig({ + colorScaleType: value as ColorScaleType, + }) + } + > + - - - - - {CHOROPLETH_COLOR_SCHEMES.map((option, index) => { - const isCustom = option.value === ColorScheme.Custom; - const customColorValue = isCustom - ? viewConfig.customColor || "#3b82f6" - : undefined; - return ( - - {isCustom && customColorValue ? ( -
- ) : ( -
- )} - {option.label} - - ); - })} - - - - {viewConfig.colorScheme === ColorScheme.Custom && ( - <> - + + {viewConfig.colorScaleType || ColorScaleType.Gradient} + + + +
+ Gradient +
+ + +
+
+
+
+
+
+
+ Stepped +
+ + +
+
+
+
+
+
+
+ Categorical +
+ + + + + {canSelectColorScheme && ( + <> + + + + + {viewConfig.colorScheme === ColorScheme.Custom && ( + <> +
- - )} + + )} - - - - updateViewConfig({ reverseColorScheme: v }) - } - /> - - {viewConfig.colorScaleType === ColorScaleType.Stepped && ( - <> - -
- -
- - )} - - )} - - )} - {canSetCategoryColors && ( - <> - -
- + + + + updateViewConfig({ reverseColorScheme: v }) + } + /> + + {viewConfig.colorScaleType === ColorScaleType.Stepped && ( + <> + +
+ +
+ + )} + + )} + + )} + {canSetCategoryColors && ( + <> + +
+ +
+ + )} + + +
+
+ { + const v = Number(e.target.value); + const choroplethOpacityPct = isNaN(v) + ? 80 + : Math.max(0, Math.min(v, 100)); + updateViewConfig({ choroplethOpacityPct }); + }} + className="w-full h-2 bg-neutral-200 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-neutral-500 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:shadow-sm [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-neutral-500 [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:shadow-sm [&::-moz-range-thumb]:appearance-none" + style={{ + background: `linear-gradient(to right, #737373 0%, #737373 ${viewConfig.choroplethOpacityPct ?? 80}%, #e5e7eb ${viewConfig.choroplethOpacityPct ?? 80}%, #e5e7eb 100%)`, + }} + />
- - )} - - -
-
- { const v = Number(e.target.value); @@ -753,57 +778,19 @@ export default function VisualisationPanel({ : Math.max(0, Math.min(v, 100)); updateViewConfig({ choroplethOpacityPct }); }} - className="w-full h-2 bg-neutral-200 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-neutral-500 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:shadow-sm [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-neutral-500 [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:shadow-sm [&::-moz-range-thumb]:appearance-none" - style={{ - background: `linear-gradient(to right, #737373 0%, #737373 ${viewConfig.choroplethOpacityPct ?? 80}%, #e5e7eb ${viewConfig.choroplethOpacityPct ?? 80}%, #e5e7eb 100%)`, - }} + className="w-16" />
- { - const v = Number(e.target.value); - const choroplethOpacityPct = isNaN(v) - ? 80 - : Math.max(0, Math.min(v, 100)); - updateViewConfig({ choroplethOpacityPct }); - }} - className="w-16" - />
-
- {/* Bivariate visualization button at the bottom */} - {canSelectSecondaryColumn && !viewConfig.areaDataSecondaryColumn && ( -
-
{ - // Find the first available numeric column - const dataSource = dataSources?.find( - (ds) => ds.id === viewConfig.areaDataSourceId, - ); - const firstNumericColumn = dataSource?.columnDefs - .filter((col) => col.type === ColumnType.Number) - .find((col) => col.name !== viewConfig.areaDataColumn); - if (firstNumericColumn) { - updateViewConfig({ - areaDataSecondaryColumn: firstNumericColumn.name, - }); - } else { - updateViewConfig({ - areaDataSecondaryColumn: undefined, - }); - } - }} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + {/* Bivariate visualization button at the bottom */} + {canSelectSecondaryColumn && !viewConfig.areaDataSecondaryColumn && ( +
+
{ // Find the first available numeric column const dataSource = dataSources?.find( (ds) => ds.id === viewConfig.areaDataSourceId, @@ -820,50 +807,71 @@ export default function VisualisationPanel({ areaDataSecondaryColumn: undefined, }); } - } - }} - > -
-
- - Create bivariate visualization - - - Using a second column - -
-
-
- Column 1 + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + // Find the first available numeric column + const dataSource = dataSources?.find( + (ds) => ds.id === viewConfig.areaDataSourceId, + ); + const firstNumericColumn = dataSource?.columnDefs + .filter((col) => col.type === ColumnType.Number) + .find((col) => col.name !== viewConfig.areaDataColumn); + if (firstNumericColumn) { + updateViewConfig({ + areaDataSecondaryColumn: firstNumericColumn.name, + }); + } else { + updateViewConfig({ + areaDataSecondaryColumn: undefined, + }); + } + } + }} + > +
+
+ + Create bivariate visualization + + + Using a second column +
-
-
- {[ - ["#e8e8e8", "#ace4e4", "#5ac8c8"], - ["#dfb0d6", "#a5add3", "#5698b9"], - ["#be64ac", "#8c62aa", "#3b4994"], - ] - .reverse() - .map((row, i) => - row.map((color, j) => ( -
- )), - )} -
+
- Column 2 → + Column 1 +
+
+
+ {[ + ["#e8e8e8", "#ace4e4", "#5ac8c8"], + ["#dfb0d6", "#a5add3", "#5698b9"], + ["#be64ac", "#8c62aa", "#3b4994"], + ] + .reverse() + .map((row, i) => + row.map((color, j) => ( +
+ )), + )} +
+
+ Column 2 → +
-
- )} -
+ )} +
+ )} {/* Modal for handling invalid data source / boundary combination */} ) : data?.records.length ? ( @@ -74,6 +77,7 @@ export function BoundaryDataPanel({ @@ -92,21 +96,25 @@ export function BoundaryDataPanel({ function BoundaryDataProperties({ json, columns, + columnDefs, match, }: { json: Record; columns: string[]; + columnDefs?: ColumnDef[]; match: DataRecordMatchType; }) { const filteredProperties = useMemo(() => { const filtered: Record = {}; columns.forEach((columnName) => { if (json[columnName] !== undefined) { - filtered[columnName] = json[columnName]; + filtered[columnName] = getDisplayValue(json[columnName], { + columnType: columnDefs?.find((cd) => cd.name === columnName)?.type, + }); } }); return filtered; - }, [columns, json]); + }, [columnDefs, columns, json]); return (
{match === DataRecordMatchType.Approximate && ( diff --git a/src/app/map/[id]/utils/stats.ts b/src/app/map/[id]/utils/stats.ts new file mode 100644 index 00000000..b9527626 --- /dev/null +++ b/src/app/map/[id]/utils/stats.ts @@ -0,0 +1,33 @@ +import { ColumnType } from "@/server/models/DataSource"; +import { CalculationType } from "@/server/models/MapView"; +import { formatNumber } from "@/utils/text"; + +export const getDisplayValue = ( + value: unknown, + { + calculationType, + columnType, + minValue, + maxValue, + }: { + calculationType?: CalculationType; + columnType?: ColumnType; + minValue?: number; + maxValue?: number; + }, +): string => { + if (value === undefined || value === null || value === "") { + return calculationType === CalculationType.Count ? "0" : "-"; + } + if (columnType !== ColumnType.Number) { + return String(value); + } + const nValue = Number(value); + if (isNaN(nValue)) { + return "-"; + } + if (minValue && minValue >= 0 && maxValue && maxValue <= 1) { + return `${Math.round(nValue * 1000) / 10}%`; + } + return formatNumber(nValue); +}; From 2d9b4639e39895ed5d7858612710978532e9daaf Mon Sep 17 00:00:00 2001 From: joaquimds Date: Thu, 5 Feb 2026 14:13:13 +0100 Subject: [PATCH 2/2] Update src/app/map/[id]/utils/stats.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/map/[id]/utils/stats.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/map/[id]/utils/stats.ts b/src/app/map/[id]/utils/stats.ts index b9527626..b8f21851 100644 --- a/src/app/map/[id]/utils/stats.ts +++ b/src/app/map/[id]/utils/stats.ts @@ -26,7 +26,12 @@ export const getDisplayValue = ( if (isNaN(nValue)) { return "-"; } - if (minValue && minValue >= 0 && maxValue && maxValue <= 1) { + if ( + typeof minValue === "number" && + typeof maxValue === "number" && + minValue >= 0 && + maxValue <= 1 + ) { return `${Math.round(nValue * 1000) / 10}%`; } return formatNumber(nValue);