Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fe1c48e
cp1-context-menu
Arbyhisenaj Jan 14, 2026
cbdfbb5
cp2-consolidating members and markers
Arbyhisenaj Jan 14, 2026
bd36727
cp3-icon-change
Arbyhisenaj Jan 14, 2026
6505021
linear changes
Arbyhisenaj Jan 15, 2026
6746385
better categories data viz
Arbyhisenaj Jan 15, 2026
f756155
centered areainfo
Arbyhisenaj Jan 15, 2026
4ba8773
empty state + delete layer item alert dialogue component
Arbyhisenaj Jan 15, 2026
565ced9
legend upgrade
Arbyhisenaj Jan 15, 2026
d36e62c
lint fixes
Arbyhisenaj Jan 15, 2026
b477574
Update src/app/map/[id]/components/Legend.tsx
joaquimds Jan 15, 2026
fe0f0f2
Update src/components/DeleteConfirmationDialog.tsx
joaquimds Jan 15, 2026
7ba1278
move bivariate button plus other fixes
Arbyhisenaj Jan 15, 2026
0882d15
Merge branch 'layers-panel/jan26-updates-shorterm' of https://github.…
Arbyhisenaj Jan 15, 2026
239e9cd
Merge branch 'main' into feat/layers-panel-one
joaquimds Jan 15, 2026
5f7fa16
Merge branch 'main' into feat/layers-panel-one
joaquimds Jan 15, 2026
275e118
Update src/app/map/[id]/components/controls/VisualisationPanel/Steppe…
joaquimds Jan 15, 2026
9b1e7d8
Update src/app/map/[id]/components/controls/VisualisationPanel/Visual…
joaquimds Jan 15, 2026
f68e8e1
Update src/app/map/[id]/components/controls/VisualisationPanel/Steppe…
joaquimds Jan 15, 2026
d9511a9
Update src/app/map/[id]/components/controls/VisualisationPanel/Visual…
joaquimds Jan 15, 2026
fe25082
stepped colours performance fix and rounded numbers
Arbyhisenaj Jan 15, 2026
c167f7d
Merge branch 'layers-panel/jan26-updates-shorterm' of https://github.…
Arbyhisenaj Jan 15, 2026
c6135c9
column 1 & 2 fix
Arbyhisenaj Jan 15, 2026
0c978e2
Merge branch 'feat/layers-panel-one' into layers-panel/jan26-updates-…
joaquimds Jan 16, 2026
17a647a
fix: various choropleth bugs
joaquimds Jan 16, 2026
47aadac
Merge branch 'layers-panel/jan26-updates-shorterm' of github.com:comm…
joaquimds Jan 16, 2026
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
6 changes: 4 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"jotai": "^2.16.0",
"kysely": "^0.27.6",
"lucide": "^0.544.0",
"lucide-react": "^0.484.0",
"lucide-react": "^0.562.0",
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lucide-react package is being updated from version 0.484.0 to 0.562.0. This is a significant jump that may introduce breaking changes or new icons (like VectorSquareIcon which is being used in this PR). Ensure that all icon imports are compatible with the new version and check the lucide-react changelog for any breaking changes.

Copilot uses AI. Check for mistakes.
"mapbox-gl": "^3.10.0",
"minio": "^8.0.5",
"next": "^15.5.7",
Expand Down
53 changes: 16 additions & 37 deletions src/app/(private)/data-sources/[id]/DataSourceDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,11 @@ import { toast } from "sonner";
import DataSourceBadge from "@/components/DataSourceBadge";
import DataSourceRecordTypeIcon from "@/components/DataSourceRecordTypeIcon";
import DefinitionList from "@/components/DefinitionList";
import DeleteConfirmationDialog from "@/components/DeleteConfirmationDialog";
import { Link } from "@/components/Link";
import { DataSourceConfigLabels } from "@/labels";
import { JobStatus } from "@/server/models/DataSource";
import { useTRPC } from "@/services/trpc/react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/shadcn/ui/alert-dialog";
import {
Breadcrumb,
BreadcrumbItem,
Expand Down Expand Up @@ -251,6 +241,7 @@ function DeleteDataSourceButton({
}) {
const router = useRouter();
const trpc = useTRPC();
const [open, setOpen] = useState(false);
const { mutate, isPending } = useMutation(
trpc.dataSource.delete.mutationOptions({
onSuccess: () => {
Expand All @@ -264,31 +255,19 @@ function DeleteDataSourceButton({
);

return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">
<Trash2Icon />
Delete data source
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your data
source.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isPending}
onClick={() => mutate({ dataSourceId: dataSource.id })}
>
{isPending ? "Deleting..." : "Continue"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<>
<Button variant="destructive" onClick={() => setOpen(true)}>
<Trash2Icon />
Delete data source
</Button>
<DeleteConfirmationDialog
open={open}
onOpenChange={setOpen}
description="This action cannot be undone. This will permanently delete your data source."
onConfirm={() => mutate({ dataSourceId: dataSource.id })}
isPending={isPending}
confirmButtonText="Continue"
/>
</>
);
}
140 changes: 130 additions & 10 deletions src/app/map/[id]/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,36 @@ import {
schemeCategory10,
} from "d3-scale-chromatic";
import { useMemo } from "react";
import { DEFAULT_CUSTOM_COLOR } from "@/constants";
import { ColumnType } from "@/server/models/DataSource";
import { CalculationType, ColorScheme } from "@/server/models/MapView";
import {
CalculationType,
ColorScaleType,
ColorScheme,
type SteppedColorStep,
} from "@/server/models/MapView";
import { DEFAULT_FILL_COLOR, PARTY_COLORS } from "./constants";
import type { CombinedAreaStats } from "./data";
import type { ScaleOrdinal, ScaleSequential } from "d3-scale";
import type { DataDrivenPropertyValueSpecification } from "mapbox-gl";

// Simple RGB interpolation helper (white to target color)
const interpolateWhiteToColor = (targetColor: string) => {
// Parse hex color to RGB
const hex = targetColor.replace("#", "");
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);

return (t: number) => {
// Interpolate from white (255, 255, 255) to target color
const newR = Math.round(255 + t * (r - 255));
const newG = Math.round(255 + t * (g - 255));
const newB = Math.round(255 + t * (b - 255));
return `rgb(${newR}, ${newG}, ${newB})`;
};
};

export interface CategoricColorScheme {
columnType: ColumnType.String;
colorMap: Record<string, string>;
Expand Down Expand Up @@ -71,9 +94,17 @@ export const CHOROPLETH_COLOR_SCHEMES = [
value: ColorScheme.Diverging,
color: "bg-gradient-to-r from-brown-500 via-yellow-500 to-teal-500",
},
{
label: "Custom",
value: ColorScheme.Custom,
color: "bg-gradient-to-r from-white to-blue-500",
},
];

const getInterpolator = (scheme: ColorScheme | undefined) => {
export const getInterpolator = (
scheme: ColorScheme | undefined,
customColor?: string,
) => {
switch (scheme) {
case ColorScheme.RedBlue:
return interpolateRdBu;
Expand All @@ -88,6 +119,10 @@ const getInterpolator = (scheme: ColorScheme | undefined) => {
return interpolateBrBG;
case ColorScheme.Sequential:
return interpolateBlues;
case ColorScheme.Custom:
// Interpolate from white to custom color
const targetColor = customColor || DEFAULT_CUSTOM_COLOR;
return interpolateWhiteToColor(targetColor);
default:
return interpolateOrRd;
}
Expand All @@ -97,25 +132,39 @@ export const useColorScheme = ({
areaStats,
scheme,
isReversed,
categoryColors,
customColor,
}: {
areaStats: CombinedAreaStats | null;
scheme: ColorScheme;
isReversed: boolean;
categoryColors?: Record<string, string>;
customColor?: string;
}): CategoricColorScheme | NumericColorScheme | null => {
// useMemo to cache calculated scales
return useMemo(() => {
return getColorScheme({ areaStats, scheme, isReversed });
}, [areaStats, scheme, isReversed]);
return getColorScheme({
areaStats,
scheme,
isReversed,
categoryColors,
customColor,
});
}, [areaStats, scheme, isReversed, categoryColors, customColor]);
};

const getColorScheme = ({
areaStats,
scheme,
isReversed,
categoryColors,
customColor,
}: {
areaStats: CombinedAreaStats | null;
scheme: ColorScheme;
isReversed: boolean;
categoryColors?: Record<string, string>;
customColor?: string;
}): CategoricColorScheme | NumericColorScheme | null => {
if (!areaStats || !areaStats.stats.length) {
return null;
Expand All @@ -129,7 +178,8 @@ const getColorScheme = ({
const colorScale = scaleOrdinal(schemeCategory10).domain(distinctValues);
const colorMap: Record<string, string> = {};
distinctValues.forEach((v) => {
colorMap[v] = getCategoricalColor(v, colorScale);
// Use custom color if provided, otherwise use default
colorMap[v] = categoryColors?.[v] ?? getCategoricalColor(v, colorScale);
});
return {
columnType: ColumnType.String,
Expand All @@ -146,7 +196,7 @@ const getColorScheme = ({
const domain = isReversed ? [1, 0] : [0, 1];
// For count records, create a simple color scheme
// Use a small range to ensure valid interpolation
const interpolator = getInterpolator(scheme);
const interpolator = getInterpolator(scheme, customColor);
const colorScale = scaleSequential()
.domain(domain) // Use 0-1 range for single values
.interpolator(interpolator);
Expand All @@ -165,7 +215,7 @@ const getColorScheme = ({
number,
];

const interpolator = getInterpolator(scheme);
const interpolator = getInterpolator(scheme, customColor);
const colorScale = scaleSequential()
.domain(domain)
.interpolator(interpolator);
Expand All @@ -190,11 +240,19 @@ export const useFillColor = ({
scheme,
isReversed,
selectedBivariateBucket,
categoryColors,
colorScaleType,
steppedColorSteps,
customColor,
}: {
areaStats: CombinedAreaStats | null;
scheme: ColorScheme;
isReversed: boolean;
selectedBivariateBucket: string | null;
categoryColors?: Record<string, string>;
colorScaleType?: ColorScaleType;
steppedColorSteps?: SteppedColorStep[];
customColor?: string;
}): DataDrivenPropertyValueSpecification<string> => {
// useMemo to cache calculated fillColor
return useMemo(() => {
Expand All @@ -203,7 +261,13 @@ export const useFillColor = ({
}

const isCount = areaStats?.calculationType === CalculationType.Count;
const colorScheme = getColorScheme({ areaStats, scheme, isReversed });
const colorScheme = getColorScheme({
areaStats,
scheme,
isReversed,
categoryColors,
customColor,
});
if (!colorScheme) {
return DEFAULT_FILL_COLOR;
}
Expand All @@ -219,7 +283,16 @@ export const useFillColor = ({
return ["match", ["feature-state", "value"], ...ordinalColorStops];
}

// ColumnType.Number
// ColumnType.Number - Check if stepped colors are enabled
if (
colorScaleType === ColorScaleType.Stepped &&
steppedColorSteps &&
steppedColorSteps.length > 0
) {
return getSteppedFillColor(steppedColorSteps, isCount);
}

// ColumnType.Number - Gradient (default)
if (colorScheme.isSingleValue) {
// When all values are the same, map the value to our 0-1 range
// This ensures count data is visible even when all counts are equal
Expand Down Expand Up @@ -257,7 +330,54 @@ export const useFillColor = ({
: ["feature-state", "value"],
...interpolateColorStops,
];
}, [areaStats, isReversed, scheme, selectedBivariateBucket]);
}, [
areaStats,
isReversed,
scheme,
selectedBivariateBucket,
categoryColors,
colorScaleType,
steppedColorSteps,
customColor,
]);
};

const getSteppedFillColor = (
steps: SteppedColorStep[],
isCount: boolean,
): DataDrivenPropertyValueSpecification<string> => {
// Sort steps by start value to ensure correct order
const sortedSteps = [...steps].sort((a, b) => a.start - b.start);

if (sortedSteps.length === 0) {
return DEFAULT_FILL_COLOR;
}

// 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)
// if step1.start <= value < step2.start, use step1.color
// if step2.start <= value < step3.start, use step2.color
// etc.

const stepExpression: DataDrivenPropertyValueSpecification<string> = [
"step",
isCount
? ["coalesce", ["feature-state", "value"], 0]
: ["feature-state", "value"],
sortedSteps[0]?.color || DEFAULT_FILL_COLOR, // Default color for values < first threshold
];

// 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];
stepExpression.push(step.start); // Threshold
stepExpression.push(step.color); // Color for values >= threshold
}

return stepExpression;
};

const getBivariateFillColor = (
Expand Down
4 changes: 4 additions & 0 deletions src/app/map/[id]/components/AreaInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export default function AreaInfo() {
scheme: viewConfig.colorScheme || ColorScheme.RedBlue,
isReversed: Boolean(viewConfig.reverseColorScheme),
selectedBivariateBucket: null,
categoryColors: viewConfig.categoryColors,
colorScaleType: viewConfig.colorScaleType,
steppedColorSteps: viewConfig.steppedColorSteps,
customColor: viewConfig.customColor,
});

// Combine selected areas and hover area, avoiding duplicates
Expand Down
2 changes: 1 addition & 1 deletion src/app/map/[id]/components/Choropleth/areas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const getValidAreaSetGroupCodes = (
dataSourceGeocodingConfig: GeocodingConfig | null | undefined,
): AreaSetGroupCode[] => {
if (!dataSourceGeocodingConfig) {
return [];
return Object.values(AreaSetGroupCode);
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no geocoding config is provided, the function now returns all AreaSetGroupCodes instead of an empty array. This is a significant behavior change that could allow users to select area sets that aren't compatible with their data source. This might lead to incorrect visualizations or errors when geocoding is attempted. Verify this change is intentional and that there are proper safeguards elsewhere in the code.

Suggested change
return Object.values(AreaSetGroupCode);
return [];

Copilot uses AI. Check for mistakes.
}

const areaSetCode =
Expand Down
3 changes: 3 additions & 0 deletions src/app/map/[id]/components/Choropleth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MapType } from "@/server/models/MapView";
import { mapColors } from "../../styles";
import { getMapStyle } from "../../utils/map";
import { useChoroplethAreaStats } from "./useChoroplethAreaStats";
import { useChoroplethFeatureStatesEffect } from "./useChoroplethFeatureStatesEffect";

export default function Choropleth() {
const { viewConfig } = useMapViews();
Expand All @@ -19,6 +20,8 @@ export default function Choropleth() {
const fillColor = useChoroplethAreaStats();
const opacity = (viewConfig.choroplethOpacityPct ?? 80) / 100;

useChoroplethFeatureStatesEffect();

return (
<>
{/* Position layer */}
Expand Down
Loading
Loading