-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
117 lines (103 loc) · 4 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import * as topojson from "topojson-client";
import { Topology } from "topojson-specification";
import { select, BaseType, Selection } from "d3-selection";
import { geoPath, geoMercator, GeoProjection } from "d3-geo";
import "d3-transition";
export function cartogram(
element: SVGElement,
{ width, height, layers = [], projection }: CartogramOptions
): Cartogram {
const el = select(element);
// If width and height are not specified, get them from the closest SVG parent.
const container = el.node()?.closest("svg");
width = width || container?.viewBox?.animVal?.width || container?.width?.animVal?.value || 0;
height = height || container?.viewBox?.animVal?.height || container?.height?.animVal?.value || 0;
const size: [number, number] = [width, height];
// Default to Mercator projection
projection = projection || geoMercator();
// Create a <g id="${layer.id}"> element for each layer. If ID is missing, use `layer-${index}`.
const layerSelection = el
.selectAll("g.layer")
.data(layers)
.join("g")
.classed("layer", true)
.attr("id", (d, i) => d.id || `layer-${i}`)
.each((layer: CartogramLayer) => {
const featureCollectionName = Object.keys(layer.data.objects)[0];
const featureCollection = topojson.feature(
layer.data,
featureCollectionName
) as unknown as GeoJSON.FeatureCollection;
// Filter if required
if (layer.filter) featureCollection.features = featureCollection.features.filter(layer.filter);
// Store converted TopoJSON features in each layer as .features
layer.features = featureCollection.features;
});
// If projection is not defined, fit to all layers with fit=true, or all layers.
if (!projection?.center?.()?.[0]) {
let features = layers
.filter((d) => d.fit)
.map((d) => d.features || [])
.flat();
if (features.length === 0) features = layers.map((d) => d.features || []).flat();
projection = projection.fitSize(size, {
type: "FeatureCollection",
features,
});
}
const path = geoPath();
if (projection) path.projection(projection);
const features: FeatureSelection[] = [];
layerSelection.each(function (layer: CartogramLayer) {
const plugin = plugins[layer.type];
if (!plugin) return;
const selection = select(this)
.selectAll(".feature")
.data(layer.features || []);
// TODO: Allow layer.enter() and layer.exit()
const join = plugin(selection, { path }).classed("feature", true);
if (layer.update) join.call(layer.update);
features.push(join);
});
return {
layers: layerSelection,
features,
};
}
const plugins = {
choropleth: (selection: FeatureSelection, { path }) => selection.join("path").attr("d", path),
cartogram: (selection: FeatureSelection, { path }) =>
selection.join(
(enter) => enter.append("circle").call(moveToCentroid, { path }).attr("r", 5).attr("stroke", "#fff"),
(update) => update.call(moveToCentroid, { path })
),
centroid: (selection: FeatureSelection, { path }) => selection.join("g").call(moveToCentroid, { path }),
};
function moveToCentroid(selection: FeatureSelection, { path }) {
selection.each(function (d: Feature) {
const [x, y] = path.centroid(d);
if (x && y) select(this).attr("transform", `translate(${x},${y})`);
});
}
export interface CartogramOptions {
width?: number;
height?: number;
layers: CartogramLayer[];
projection?: GeoProjection;
}
export interface CartogramLayer {
type: "choropleth" | "cartogram" | "centroid";
data: Topology;
id?: string;
filter?: (feature: Feature) => boolean;
update?: (join: FeatureSelection) => void;
fit?: boolean;
features?: Feature[];
}
export interface Cartogram {
layers: LayerSelection;
features: FeatureSelection[];
}
type Feature = GeoJSON.Feature<GeoJSON.Geometry, GeoJSON.GeoJsonProperties>;
type LayerSelection = Selection<BaseType | SVGElement, CartogramLayer, SVGElement, unknown>;
type FeatureSelection = Selection<any | SVGCircleElement, Feature, BaseType | SVGElement, unknown>;