Skip to content

Commit 533c319

Browse files
Map wip - added topojson, visx/geo markup, doesn't work xp
1 parent d151a19 commit 533c319

File tree

310 files changed

+25656
-3
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

310 files changed

+25656
-3
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import React, { useMemo } from "react";
2+
import { CustomProjection, Graticule } from "@visx/geo";
3+
import type { Projection } from "@visx/geo/lib/types";
4+
// import { geoConicConformal } from "d3-geo";
5+
import { geoConicConformal } from "@visx/vendor/d3-geo";
6+
import * as topojson from "topojson-client";
7+
import { Topology } from "topojson-specification";
8+
9+
// --- TYPES ---
10+
interface StatsCanFederalProperties {
11+
PRNAME: string;
12+
PRUID: string;
13+
}
14+
15+
interface StatsCanProvincialProperties {
16+
CDNAME: string;
17+
PRNAME: string;
18+
CDUID: string;
19+
}
20+
21+
interface StatsCanRegionProperties {
22+
PRUID: string;
23+
PRNAME: string;
24+
CDUID: string;
25+
CDNAME: string;
26+
CDTYPE: string;
27+
CSDUID: string;
28+
CSDNAME: string;
29+
CSDTYPE: string;
30+
}
31+
32+
// StatsCan standard properties (based on your mapshaper output)
33+
type StatsCanProperties =
34+
| StatsCanFederalProperties
35+
| StatsCanProvincialProperties
36+
| StatsCanRegionProperties;
37+
38+
interface PolygonGeometry {
39+
type: "Polygon";
40+
coordinates: number[][][]; // [Ring][Point]
41+
}
42+
43+
interface MultiPolygonGeometry {
44+
type: "MultiPolygon";
45+
coordinates: number[][][][];
46+
}
47+
48+
interface FeatureShape {
49+
type: "Feature";
50+
geometry: PolygonGeometry | MultiPolygonGeometry;
51+
properties: StatsCanProperties;
52+
}
53+
54+
interface CanadaMapProps {
55+
width: number;
56+
height: number;
57+
data: Topology; // Generic Topology to accept any generated file
58+
events?: boolean;
59+
// Optional: Allow the parent to color shapes based on data (e.g., sequential palette)
60+
getFill?: (feature: FeatureShape) => string;
61+
onSelect?: (feature: FeatureShape) => void;
62+
}
63+
64+
export const CanadaMap = ({
65+
width,
66+
height,
67+
data,
68+
events = false,
69+
getFill,
70+
onSelect,
71+
}: CanadaMapProps) => {
72+
// 1. CONVERT TO GEOJSON (Dynamic Key)
73+
const world = useMemo<FeatureShape[]>(() => {
74+
if (!data) return [];
75+
76+
// Auto-detect the primary layer key so we don't crash on "PROVINCE" vs "regions"
77+
const keys = Object.keys(data.objects);
78+
if (keys.length === 0) return [];
79+
const primaryLayer = keys[0]; // e.g., "provinces" or "lcsd000a25a_e"
80+
const topodata = topojson.feature(data, data.objects[primaryLayer]);
81+
82+
// @ts-expect-error - topojson types are loose, but we know the structure
83+
const features = topodata.features as FeatureShape[];
84+
return features;
85+
}, [data]);
86+
87+
// 2. DEFINE PROJECTION
88+
// geoConicConformal + fitSize is the "Magic Bullet" here.
89+
// It works for the whole country OR a single province drill-down automatically.
90+
const projection = useMemo(
91+
() =>
92+
geoConicConformal()
93+
.parallels([50, 70])
94+
.rotate([96, 0])
95+
.fitSize([width, height], {
96+
type: "FeatureCollection",
97+
features: world,
98+
}),
99+
[width, height, world],
100+
);
101+
console.dir({ projection });
102+
if (world.length === 0) return null;
103+
104+
return (
105+
<svg width={width} height={height}>
106+
<CustomProjection<FeatureShape> projection={"mercator"} data={world}>
107+
{(customProjection) => {
108+
return (
109+
<g>
110+
{customProjection.features.map((feature, i) => {
111+
const { path, feature: f } = feature;
112+
// Use provided fill logic or default to StatsCan grey
113+
// const fillColor = getFill ? getFill(f) : "#EAEAEA";
114+
const fillColor = "#EAEAEA";
115+
116+
return (
117+
<path
118+
key={`map-feature-${i}`}
119+
d={path || ""}
120+
fill={fillColor}
121+
stroke="#FFFFFF"
122+
strokeWidth={0.5}
123+
// style={{
124+
// transition: "fill 0.2s ease",
125+
// cursor: events ? "pointer" : "default",
126+
// outline: "none", // Prevents focus ring issues on click
127+
// }}
128+
// onClick={() => {
129+
// if (events && onSelect) onSelect(f);
130+
// }}
131+
// onMouseEnter={(e) => {
132+
// if (events && !getFill)
133+
// e.currentTarget.style.fill = "#CFD8DC"; // Simple hover if no data color
134+
// }}
135+
// onMouseLeave={(e) => {
136+
// if (events && !getFill)
137+
// e.currentTarget.style.fill = fillColor;
138+
// }}
139+
/>
140+
);
141+
})}
142+
</g>
143+
);
144+
}}
145+
</CustomProjection>
146+
</svg>
147+
);
148+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Topology } from "topojson-specification";
2+
import * as Federal from "../../_data/maps/canada-provinces.json";
3+
import * as Alberta from "../../_data/maps/provinces/ab.json";
4+
import VisxMercator from "./visx_map";
5+
6+
export default function MapController() {
7+
console.log();
8+
return (
9+
<div className="grid place-items-center">
10+
<VisxMercator width={480} height={480} />
11+
</div>
12+
);
13+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React from "react";
2+
import { scaleQuantize } from "@visx/scale";
3+
import { Mercator, Graticule } from "@visx/geo";
4+
import * as topojson from "topojson-client";
5+
import * as topology from "../../_data/maps/canada-provinces.json";
6+
7+
export const background = "#f9f7e8";
8+
9+
export type GeoMercatorProps = {
10+
width: number;
11+
height: number;
12+
events?: boolean;
13+
};
14+
15+
interface FeatureShape {
16+
type: "Feature";
17+
id: string;
18+
geometry: { coordinates: [number, number][][]; type: "Polygon" };
19+
properties: { name: string };
20+
}
21+
22+
// @ts-expect-error Topojson is untyped
23+
const world = topojson.feature(topology, topology.objects.units) as {
24+
type: "FeatureCollection";
25+
features: FeatureShape[];
26+
};
27+
28+
const color = scaleQuantize({
29+
domain: [
30+
Math.min(...world.features.map((f) => f.geometry.coordinates.length)),
31+
Math.max(...world.features.map((f) => f.geometry.coordinates.length)),
32+
],
33+
range: [
34+
"#ffb01d",
35+
"#ffa020",
36+
"#ff9221",
37+
"#ff8424",
38+
"#ff7425",
39+
"#fc5e2f",
40+
"#f94b3a",
41+
"#f63a48",
42+
],
43+
});
44+
45+
export default function VisxMercator({
46+
width,
47+
height,
48+
events = false,
49+
}: GeoMercatorProps) {
50+
const centerX = width / 2;
51+
const centerY = height / 2;
52+
const scale = (width / 630) * 100;
53+
54+
return width < 10 ? null : (
55+
<svg width={width} height={height}>
56+
<rect
57+
x={0}
58+
y={0}
59+
width={width}
60+
height={height}
61+
fill={background}
62+
rx={14}
63+
/>
64+
<Mercator<FeatureShape>
65+
data={world.features}
66+
scale={scale}
67+
translate={[centerX, centerY + 50]}
68+
>
69+
{(mercator) => (
70+
<g>
71+
<Graticule
72+
graticule={(g) => mercator.path(g) || ""}
73+
stroke="rgba(33,33,33,0.05)"
74+
/>
75+
{mercator.features.map(({ feature, path }, i) => (
76+
<path
77+
key={`map-feature-${i}`}
78+
d={path || ""}
79+
fill={color(feature.geometry.coordinates.length)}
80+
stroke={background}
81+
strokeWidth={0.5}
82+
onClick={() => {
83+
if (events)
84+
alert(
85+
`Clicked: ${feature.properties.name} (${feature.id})`,
86+
);
87+
}}
88+
/>
89+
))}
90+
</g>
91+
)}
92+
</Mercator>
93+
</svg>
94+
);
95+
}

0 commit comments

Comments
 (0)