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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 29 additions & 71 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
115 changes: 55 additions & 60 deletions src/app/map/[id]/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string, string>;
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<string, string>;
customColor?: string;
viewConfig: MapViewConfig;
}): CategoricColorScheme | NumericColorScheme | null => {
if (!areaStats || !areaStats.stats.length) {
return null;
Expand All @@ -179,7 +182,8 @@ const getColorScheme = ({
const colorMap: Record<string, string> = {};
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,
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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<string, string>;
colorScaleType?: ColorScaleType;
steppedColorSteps?: SteppedColorStep[];
customColor?: string;
}): DataDrivenPropertyValueSpecification<string> => {
// useMemo to cache calculated fillColor
return useMemo(() => {
Expand All @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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<string> => {
// Sort steps by start value to ensure correct order
const sortedSteps = [...steps].sort((a, b) => a.start - b.start);
Expand All @@ -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)
Expand All @@ -365,16 +359,17 @@ 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
// For each step after the first, use its start value as the threshold
// 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;
Expand Down
Loading