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
10 changes: 6 additions & 4 deletions src/app/(private)/(dashboards)/data-sources/new/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
zetkinConfigSchema,
} from "@/models/DataSource";

export const newCSVConfigSchema = csvConfigSchema.extend({
file: z.instanceof(File),
filename: z.string().min(1, "Filename is required"),
}).omit({ url: true });
export const newCSVConfigSchema = csvConfigSchema
.extend({
file: z.instanceof(File),
filename: z.string().min(1, "Filename is required"),
})
.omit({ url: true });

export type NewCSVConfig = z.infer<typeof newCSVConfigSchema>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function AreasList({
>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 rounded shrink-0"
className="w-4 h-4 rounded shrink-0 border border-neutral-200"
style={{ backgroundColor: area.backgroundColor }}
/>
<span>{area.name}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ export default function InspectorDataTab({
);

const isBoundary = type === LayerType.Boundary;
const hasChoroplethVisualisation = Boolean(
choroplethDataSource?.id && viewConfig.areaDataColumn,
);
const bivariateBucket =
areaStats?.secondary &&
typeof areaStat?.primary === "number" &&
Expand Down Expand Up @@ -146,7 +149,7 @@ export default function InspectorDataTab({

return (
<div className="flex flex-col gap-4">
{isBoundary ? (
{isBoundary && hasChoroplethVisualisation ? (
<div className="rounded border border-neutral-200 bg-neutral-50 px-2.5 py-2 flex flex-col gap-3">
<div className="min-w-0 flex items-center justify-between gap-2">
<h3 className="text-[11px] uppercase font-mono tracking-wide text-muted-foreground inline-flex items-center gap-1">
Expand Down
244 changes: 187 additions & 57 deletions src/app/(private)/map/[id]/components/Legend/Legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/shadcn/ui/dialog";
import { Popover, PopoverContent, PopoverTrigger } from "@/shadcn/ui/popover";
import {
Select,
SelectContent,
Expand All @@ -35,6 +36,10 @@ import {
import { cn } from "@/shadcn/utils";
import { useColorScheme } from "../../colors";
import { useAreaStats } from "../../data";
import {
DEFAULT_SECONDARY_BOUNDARY_STROKE_COLOR,
mapColorPalette,
} from "../../styles";
import BivariateLegend from "../BivariateLagend";
import {
dataRecordsWillAggregate,
Expand Down Expand Up @@ -65,6 +70,20 @@ export default function Legend({
null,
);
const [bivariatePickerOpen, setBivariatePickerOpen] = useState(false);
const [secondaryBoundariesOpen, setSecondaryBoundariesOpen] = useState(
Boolean(viewConfig.secondaryAreaSetCode),
);

useEffect(() => {
if (viewConfig.secondaryAreaSetCode) {
setSecondaryBoundariesOpen(true);
}
}, [viewConfig.secondaryAreaSetCode]);

const secondaryBoundaryStrokeColor =
viewConfig.secondaryBoundaryStrokeColor ||
DEFAULT_SECONDARY_BOUNDARY_STROKE_COLOR;
const secondaryBoundarySwatches = mapColorPalette.map((c) => c.color);

const areaStatsQuery = useAreaStats();
const areaStats = areaStatsQuery?.data;
Expand Down Expand Up @@ -335,17 +354,6 @@ export default function Legend({
)}
</div>
)}
{canSelectSecondaryColumn && (
<button
type="button"
className="text-xs text-muted-foreground underline cursor-pointer text-left pl-6 hover:text-foreground transition-colors"
onClick={toggleBivariatePicker}
>
{viewConfig.areaDataSecondaryColumn || bivariatePickerOpen
? "Remove second column"
: "Add another column"}
</button>
)}
</div>
)}
</div>
Expand All @@ -370,15 +378,32 @@ export default function Legend({
</div>
) : null}

{hasDataSource && onClearRequest && (
<div className="border-b border-neutral-100 px-3 pb-2">
<button
type="button"
className="text-xs text-muted-foreground underline cursor-pointer text-left hover:text-foreground transition-colors"
onClick={onClearRequest}
>
Clear visualisation
</button>
{(canSelectSecondaryColumn || (hasDataSource && onClearRequest)) && (
<div className=" px-3 pb-2">
<div className="flex items-center justify-between gap-3">
{hasDataSource && onClearRequest ? (
<button
type="button"
className="text-xs text-muted-foreground underline cursor-pointer text-left hover:text-foreground transition-colors"
onClick={onClearRequest}
>
Clear
</button>
) : (
<span />
)}
{canSelectSecondaryColumn && (
<button
type="button"
className="text-xs text-muted-foreground underline cursor-pointer text-left hover:text-foreground transition-colors"
onClick={toggleBivariatePicker}
>
{viewConfig.areaDataSecondaryColumn || bivariatePickerOpen
? "Remove second column"
: "Add another column"}
</button>
)}
</div>
</div>
)}

Expand Down Expand Up @@ -455,7 +480,7 @@ export default function Legend({

{/* Aggregation */}
{canSelectAggregation && (
<div className="border-t border-neutral-100 px-3 py-3">
<div className="border-b border-neutral-100 px-3 pb-3">
<p className="text-xs text-muted-foreground font-mono font-medium uppercase mb-1">
Aggregation
</p>
Expand Down Expand Up @@ -484,46 +509,151 @@ export default function Legend({

{/* Secondary boundaries */}
{viewConfig.mapType !== MapType.Hex && (
<div className="border-t border-neutral-100 px-3 py-3">
<p className="text-xs text-muted-foreground font-mono font-medium uppercase mb-1">
Secondary boundaries
</p>
<Select
value={viewConfig.secondaryAreaSetCode || NULL_UUID}
onValueChange={(value) => {
updateViewConfig({
secondaryAreaSetCode:
value === NULL_UUID ? null : (value as AreaSetCode),
});
}}
>
<SelectTrigger
size="sm"
className="h-8 w-full text-xs font-normal shadow-xs hover:border-action-hover"
<div className="px-3 pb-3">
{!secondaryBoundariesOpen && !viewConfig.secondaryAreaSetCode ? (
<button
type="button"
className="text-xs text-muted-foreground underline cursor-pointer text-left hover:text-foreground transition-colors"
onClick={() => setSecondaryBoundariesOpen(true)}
>
<SelectValue placeholder="Secondary boundaries…">
{viewConfig.secondaryAreaSetCode
? AreaSetCodeLabels[viewConfig.secondaryAreaSetCode]
: "No secondary boundaries"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={NULL_UUID}>No secondary boundaries</SelectItem>
{CHOROPLETH_AREA_SET_CODES.map((code) => (
<SelectItem key={code} value={code}>
<div className="flex flex-col">
<span>{AreaSetCodeLabels[code]}</span>
Add secondary boundaries
</button>
) : (
<div className="flex items-center justify-between gap-2 mb-1 mt-2">
<p className="text-xs text-muted-foreground font-mono font-medium uppercase">
Secondary boundaries
</p>
<button
type="button"
className="text-xs text-muted-foreground underline cursor-pointer text-left hover:text-foreground transition-colors"
onClick={() => {
updateViewConfig({ secondaryAreaSetCode: null });
setSecondaryBoundariesOpen(false);
}}
>
Remove
</button>
</div>
)}
{(secondaryBoundariesOpen || viewConfig.secondaryAreaSetCode) && (
<div className="flex items-center gap-2">
<Select
value={viewConfig.secondaryAreaSetCode || NULL_UUID}
onValueChange={(value) => {
const nextValue =
value === NULL_UUID ? null : (value as AreaSetCode);
updateViewConfig({ secondaryAreaSetCode: nextValue });
if (!nextValue) {
setSecondaryBoundariesOpen(false);
}
}}
>
<SelectTrigger
size="sm"
className="h-8 w-full text-xs font-normal shadow-xs hover:border-action-hover"
>
<SelectValue placeholder="Secondary boundaries…">
{viewConfig.secondaryAreaSetCode
? AreaSetCodeLabels[viewConfig.secondaryAreaSetCode]
: "No secondary boundaries"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={NULL_UUID}>
No secondary boundaries
</SelectItem>
{CHOROPLETH_AREA_SET_CODES.map((code) => (
<SelectItem key={code} value={code}>
<div className="flex flex-col">
<span>{AreaSetCodeLabels[code]}</span>
<span
className="text-xs text-muted-foreground"
dangerouslySetInnerHTML={{
__html: AreaSetCodeYears[code],
}}
/>
</div>
</SelectItem>
))}
</SelectContent>
</Select>

<Popover>
<PopoverTrigger asChild>
<button
type="button"
className={cn(
"h-8 w-8 rounded-lg border border-input shadow-xs flex items-center justify-center",
"hover:border-action-hover transition-colors",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
)}
aria-label="Secondary boundary stroke colour"
title="Secondary boundary stroke colour"
>
<span
className="text-xs text-muted-foreground"
dangerouslySetInnerHTML={{
__html: AreaSetCodeYears[code],
}}
className="h-4 w-4 rounded-full border border-black/15"
style={{ backgroundColor: secondaryBoundaryStrokeColor }}
/>
</button>
</PopoverTrigger>
<PopoverContent align="end" className="w-56 p-3">
<p className="text-xs font-medium mb-2">Stroke colour</p>
<div className="flex flex-wrap gap-2">
{secondaryBoundarySwatches.map((c) => (
<button
key={c}
type="button"
className={cn(
"h-7 w-7 rounded-full border border-black/10",
"hover:scale-[1.02] transition-transform",
c === secondaryBoundaryStrokeColor &&
"ring-2 ring-ring ring-offset-2 ring-offset-background",
)}
style={{ backgroundColor: c }}
onClick={() =>
updateViewConfig({ secondaryBoundaryStrokeColor: c })
}
aria-label={`Set stroke colour to ${c}`}
/>
))}
<label className="h-7 w-7 rounded-full border border-black/10 relative overflow-hidden cursor-pointer">
<input
type="color"
className="absolute inset-0 h-full w-full opacity-0 cursor-pointer"
value={secondaryBoundaryStrokeColor}
onChange={(e) =>
updateViewConfig({
secondaryBoundaryStrokeColor: e.target.value,
})
}
aria-label="Custom stroke colour"
/>
<span
className="absolute inset-0"
style={{
backgroundColor: secondaryBoundaryStrokeColor,
}}
aria-hidden
/>
</label>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{viewConfig.secondaryBoundaryStrokeColor && (
<button
type="button"
className="mt-3 text-xs text-muted-foreground underline hover:text-foreground transition-colors"
onClick={() =>
updateViewConfig({
secondaryBoundaryStrokeColor: null,
})
}
>
Reset to default
</button>
)}
</PopoverContent>
</Popover>
</div>
)}
</div>
)}

Expand Down
10 changes: 8 additions & 2 deletions src/app/(private)/map/[id]/components/SecondaryBoundaries.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Layer, Source } from "react-map-gl/mapbox";
import { useMapViews } from "../hooks/useMapViews";
import { useSecondaryAreaSetConfig } from "../hooks/useSecondaryAreaSet";
import { DEFAULT_SECONDARY_BOUNDARY_STROKE_COLOR } from "../styles";
import type { ChoroplethLayerConfig } from "./Choropleth/configs";

const SECONDARY_TOP_LAYER_ID = "secondary-top";
Expand Down Expand Up @@ -32,10 +34,14 @@ export default function SecondaryBoundaries() {
}

function Boundaries({ config }: { config: ChoroplethLayerConfig }) {
const { viewConfig } = useMapViews();
const {
mapbox: { sourceId, layerId, featureCodeProperty },
} = config;
const secondarySourceId = `${sourceId}-secondary`;
const strokeColor =
viewConfig.secondaryBoundaryStrokeColor ||
DEFAULT_SECONDARY_BOUNDARY_STROKE_COLOR;
return (
<Source
id={secondarySourceId}
Expand All @@ -52,8 +58,8 @@ function Boundaries({ config }: { config: ChoroplethLayerConfig }) {
source-layer={layerId}
type="line"
paint={{
"line-color": "#555",
"line-width": 3,
"line-color": strokeColor,
"line-width": 1.5,
"line-opacity": 1,
}}
layout={{
Expand Down
Loading
Loading