forked from aaronpdennis/congress-maps
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathprocess.js
171 lines (149 loc) · 6.38 KB
/
process.js
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
var fs = require('fs'),
fiveColorMap = require('five-color-map'),
turf = require('@turf/turf'),
polylabel = require('@mapbox/polylabel');
// Load the state names, FIPS codes, and USPS abbreviations and make a mapping
// from FIPS codes (found in Census data) to USPS abbreviations (used in our output).
var stateCodes = JSON.parse(fs.readFileSync('states.json', 'utf8'));
var stateFipsCodesMap = { };
stateCodes.forEach(function(item) { stateFipsCodesMap[item.FIPS] = item; })
fs.writeFileSync('./example/states.js', 'var states = ' + JSON.stringify(stateCodes, null, 2));
// turns 1 into '1st', etc.
function ordinal(number) {
var suffixes = ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th'];
if (((number % 100) == 11) || ((number % 100) == 12) || ((number % 100) == 13))
return number + suffixes[0];
return number + suffixes[number % 10];
}
// Load the GeoJSON files.
var census_boundaries = process.argv.slice(2)
.map(fn => {
return JSON.parse(fs.readFileSync(fn, 'utf8'))
.features;
}).flat();
// Re-style GeoJSON features.
census_boundaries = census_boundaries
.filter(function(d) {
// Some states have district 'ZZ' which represents the area of
// a state, usually over water, that is not included in any
// congressional district --- filter these out.
if (d.properties['CD119FP'] == 'ZZ')
return false;
return true;
})
.map(function(item) {
// Get state from FIPS code.
let stateinfo = stateFipsCodesMap[parseInt(item.properties.STATEFP)];
stateinfo.seen = true; // did we get boundaries for every state?
// Get the district number in two-digit form: "00" for at-large
// districts, "01", "02", .... The Census data's CD___FP field
// holds it in this format, except for the island territories
// which have "98", but are just at-large and should be "00".
let district_number = item.properties.CD119FP;
if (district_number == "98") district_number = "00";
if (district_number == "AL") district_number = "00"; // American Redistricting Project
return {
"type": "Feature",
"properties": {
state: stateinfo.USPS,
state_name: stateinfo.Name,
number: district_number,
title_short: stateinfo.USPS + ' ' + (district_number == "00" ? "At Large" : parseInt(district_number)),
title_long: stateinfo.Name + '’s ' + (district_number == "00" ? "At Large" : ordinal(parseInt(district_number))) + ' Congressional District',
},
"geometry": item.geometry
};
});
// Build a new FeatureCollection that we can pass into fiveColorMap.
var districts = {
'type': 'FeatureCollection',
'features': census_boundaries
};
// Use the five-color-map package to assign color numbers to each
// congressional district so that no two touching districts are
// assigned the same color number. fiveColorMap assigns a 'fill'
// property to each feature with a differnet color, but the colors
// aren't what we want. Change these back to indexes.
districts = fiveColorMap(districts);
var fiveColorMapMap = { };
districts.features.forEach(function(feature) {
if (typeof fiveColorMapMap[feature.properties.fill] == "undefined")
fiveColorMapMap[feature.properties.fill] = Object.keys(fiveColorMapMap).length;
feature.properties.color_index = fiveColorMapMap[feature.properties.fill];
delete feature.properties.fill;
});
// Create a new empty FeatureCollection to contain final map data that
// contains both district boundaries and label points.
var mapData = { 'type': 'FeatureCollection', 'features': [] }
districts.features.forEach(function(d) {
// Compute a good location to place a label for this district.
// If the district has multiple parts, use the largest part.
// polylabel doesn't work with a MultiPolygon and also putting
// the label in the largest part probably will look best.
let d_label = d;
if (d_label.geometry.type == "MultiPolygon") {
// Split the MultiPolygon into Polygon features and use
// turf.area to compute each's area. Find the one with
// the largest area.
d_label = null;
d.geometry.coordinates.forEach(geom => {
let polygon = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: geom
}
};
polygon.area = turf.area(polygon);
if (d_label === null || polygon.area > d_label.area)
d_label = polygon;
});
};
var label_coord = polylabel(d_label.geometry.coordinates, 1);
if (Number.isNaN(label_coord[0]))
throw d.properties.title_long;
// Create a turf.point to hold information for rending labels.
var pt = turf.point(label_coord);
// copy district metadata to the label
pt.properties = JSON.parse(JSON.stringify(d.properties)); // copy hack to avoid mutability issues
// add a type property to distinguish between labels and boundaries
pt.properties.group = 'label';
d.properties.group = 'boundary';
// add both the label point and congressional district to the mapData feature collection
mapData.features.push(pt);
mapData.features.push(d);
});
// Write out the mapData. It's too large to use JSON.stringify with indentation,
// so output in a kind of streaming way.
var f = fs.openSync('./data/map.geojson', 'w');
fs.writeSync(f, '{\n"type": "FeatureCollection",\n"features": [\n');
var first = true;
mapData.features.forEach(function(item) {
if (!first) fs.writeSync(f, ",\n"); first = false;
fs.writeSync(f, JSON.stringify(item, null, 2));
});
fs.writeSync(f, "\n]\n}");
fs.closeSync(f);
// Compute bounding boxes for each congressional district and each
// state so that we know how to center and zoom maps.
var districtBboxes = {},
stateBboxes = {};
districts.features.forEach(function(d) {
var bounds = turf.bbox(d);
// for the district
districtBboxes[d.properties.state + d.properties.number] = bounds;
// and for the states
if (stateBboxes[d.properties.state]) {
stateBboxes[d.properties.state].features.push(turf.bboxPolygon(bounds));
} else {
stateBboxes[d.properties.state] = { type: 'FeatureCollection', features: [] };
stateBboxes[d.properties.state].features.push(turf.bboxPolygon(bounds));
}
})
for (var s in stateBboxes) {
stateBboxes[s] = turf.bbox(stateBboxes[s]);
}
var bboxes = {};
for (var b in districtBboxes) { bboxes[b] = districtBboxes[b] };
for (var b in stateBboxes) { bboxes[b] = stateBboxes[b] };
fs.writeFileSync('./data/bboxes.js', 'var bboxes = ' + JSON.stringify(bboxes, null, 2));