@dra2020/district-analytics 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/district-analytics.js +2937 -0
- package/package.json +58 -0
|
@@ -0,0 +1,2937 @@
|
|
|
1
|
+
(function webpackUniversalModuleDefinition(root, factory) {
|
|
2
|
+
if(typeof exports === 'object' && typeof module === 'object')
|
|
3
|
+
module.exports = factory();
|
|
4
|
+
else if(typeof define === 'function' && define.amd)
|
|
5
|
+
define([], factory);
|
|
6
|
+
else if(typeof exports === 'object')
|
|
7
|
+
exports["district-analytics"] = factory();
|
|
8
|
+
else
|
|
9
|
+
root["district-analytics"] = factory();
|
|
10
|
+
})(global, function() {
|
|
11
|
+
return /******/ (function(modules) { // webpackBootstrap
|
|
12
|
+
/******/ // The module cache
|
|
13
|
+
/******/ var installedModules = {};
|
|
14
|
+
/******/
|
|
15
|
+
/******/ // The require function
|
|
16
|
+
/******/ function __webpack_require__(moduleId) {
|
|
17
|
+
/******/
|
|
18
|
+
/******/ // Check if module is in cache
|
|
19
|
+
/******/ if(installedModules[moduleId]) {
|
|
20
|
+
/******/ return installedModules[moduleId].exports;
|
|
21
|
+
/******/ }
|
|
22
|
+
/******/ // Create a new module (and put it into the cache)
|
|
23
|
+
/******/ var module = installedModules[moduleId] = {
|
|
24
|
+
/******/ i: moduleId,
|
|
25
|
+
/******/ l: false,
|
|
26
|
+
/******/ exports: {}
|
|
27
|
+
/******/ };
|
|
28
|
+
/******/
|
|
29
|
+
/******/ // Execute the module function
|
|
30
|
+
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
31
|
+
/******/
|
|
32
|
+
/******/ // Flag the module as loaded
|
|
33
|
+
/******/ module.l = true;
|
|
34
|
+
/******/
|
|
35
|
+
/******/ // Return the exports of the module
|
|
36
|
+
/******/ return module.exports;
|
|
37
|
+
/******/ }
|
|
38
|
+
/******/
|
|
39
|
+
/******/
|
|
40
|
+
/******/ // expose the modules object (__webpack_modules__)
|
|
41
|
+
/******/ __webpack_require__.m = modules;
|
|
42
|
+
/******/
|
|
43
|
+
/******/ // expose the module cache
|
|
44
|
+
/******/ __webpack_require__.c = installedModules;
|
|
45
|
+
/******/
|
|
46
|
+
/******/ // define getter function for harmony exports
|
|
47
|
+
/******/ __webpack_require__.d = function(exports, name, getter) {
|
|
48
|
+
/******/ if(!__webpack_require__.o(exports, name)) {
|
|
49
|
+
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
|
50
|
+
/******/ }
|
|
51
|
+
/******/ };
|
|
52
|
+
/******/
|
|
53
|
+
/******/ // define __esModule on exports
|
|
54
|
+
/******/ __webpack_require__.r = function(exports) {
|
|
55
|
+
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
|
56
|
+
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
57
|
+
/******/ }
|
|
58
|
+
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
|
59
|
+
/******/ };
|
|
60
|
+
/******/
|
|
61
|
+
/******/ // create a fake namespace object
|
|
62
|
+
/******/ // mode & 1: value is a module id, require it
|
|
63
|
+
/******/ // mode & 2: merge all properties of value into the ns
|
|
64
|
+
/******/ // mode & 4: return value when already ns object
|
|
65
|
+
/******/ // mode & 8|1: behave like require
|
|
66
|
+
/******/ __webpack_require__.t = function(value, mode) {
|
|
67
|
+
/******/ if(mode & 1) value = __webpack_require__(value);
|
|
68
|
+
/******/ if(mode & 8) return value;
|
|
69
|
+
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
|
70
|
+
/******/ var ns = Object.create(null);
|
|
71
|
+
/******/ __webpack_require__.r(ns);
|
|
72
|
+
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
|
73
|
+
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
|
74
|
+
/******/ return ns;
|
|
75
|
+
/******/ };
|
|
76
|
+
/******/
|
|
77
|
+
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
78
|
+
/******/ __webpack_require__.n = function(module) {
|
|
79
|
+
/******/ var getter = module && module.__esModule ?
|
|
80
|
+
/******/ function getDefault() { return module['default']; } :
|
|
81
|
+
/******/ function getModuleExports() { return module; };
|
|
82
|
+
/******/ __webpack_require__.d(getter, 'a', getter);
|
|
83
|
+
/******/ return getter;
|
|
84
|
+
/******/ };
|
|
85
|
+
/******/
|
|
86
|
+
/******/ // Object.prototype.hasOwnProperty.call
|
|
87
|
+
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
|
88
|
+
/******/
|
|
89
|
+
/******/ // __webpack_public_path__
|
|
90
|
+
/******/ __webpack_require__.p = "";
|
|
91
|
+
/******/
|
|
92
|
+
/******/
|
|
93
|
+
/******/ // Load entry module and return exports
|
|
94
|
+
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.ts");
|
|
95
|
+
/******/ })
|
|
96
|
+
/************************************************************************/
|
|
97
|
+
/******/ ({
|
|
98
|
+
|
|
99
|
+
/***/ "./src/_api.ts":
|
|
100
|
+
/*!*********************!*\
|
|
101
|
+
!*** ./src/_api.ts ***!
|
|
102
|
+
\*********************/
|
|
103
|
+
/*! no static exports found */
|
|
104
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
105
|
+
|
|
106
|
+
"use strict";
|
|
107
|
+
|
|
108
|
+
//
|
|
109
|
+
// THE NODE PACKAGE API
|
|
110
|
+
//
|
|
111
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
112
|
+
const preprocess_1 = __webpack_require__(/*! ./preprocess */ "./src/preprocess.ts");
|
|
113
|
+
const analyze_1 = __webpack_require__(/*! ./analyze */ "./src/analyze.ts");
|
|
114
|
+
const report_1 = __webpack_require__(/*! ./report */ "./src/report.ts");
|
|
115
|
+
const D = __webpack_require__(/*! ./_data */ "./src/_data.ts");
|
|
116
|
+
const U = __webpack_require__(/*! ./utils */ "./src/utils.ts");
|
|
117
|
+
class AnalyticsSession {
|
|
118
|
+
constructor(SessionRequest) {
|
|
119
|
+
this.config = {};
|
|
120
|
+
this.bOneTimeProcessingDone = false;
|
|
121
|
+
this.bPlanAnalyzed = false;
|
|
122
|
+
this.bPostProcessingDone = false;
|
|
123
|
+
this.testScales = {};
|
|
124
|
+
this.tests = {};
|
|
125
|
+
this.scorecard = {};
|
|
126
|
+
this.title = SessionRequest['title'];
|
|
127
|
+
this.legislativeDistricts = SessionRequest['legislativeDistricts'];
|
|
128
|
+
this.config = this.processConfig(SessionRequest['config']);
|
|
129
|
+
this.state = new D.State(this, SessionRequest['stateXX'], SessionRequest['nDistricts']);
|
|
130
|
+
this.counties = new D.Counties(this, SessionRequest['counties']);
|
|
131
|
+
this.graph = new D.Graph(this, SessionRequest['graph']);
|
|
132
|
+
this.features = new D.Features(this, SessionRequest['data'], this.config['datasets']);
|
|
133
|
+
this.plan = new D.Plan(this, SessionRequest['plan']);
|
|
134
|
+
this.districts = new D.Districts(this, SessionRequest['districtShapes']);
|
|
135
|
+
// NOTE: I've pulled these out of the individual analytics to here. Eventually,
|
|
136
|
+
// we could want them to passed into an analytics session as data, along with
|
|
137
|
+
// everything else. For now, this keeps branching out of the main code.
|
|
138
|
+
report_1.doConfigureScales(this);
|
|
139
|
+
}
|
|
140
|
+
processConfig(config) {
|
|
141
|
+
// NOTE - Session settings are required:
|
|
142
|
+
// - Analytics suites can be defaulted to all with [], but
|
|
143
|
+
// - Dataset keys must be explicitly specified with 'dataset'
|
|
144
|
+
let defaultSuites = [0 /* Legal */, 1 /* Fair */, 2 /* Best */];
|
|
145
|
+
// If the config passed in has no suites = [], use the default suites
|
|
146
|
+
if (U.isArrayEmpty(config['suites'])) {
|
|
147
|
+
config['suites'] = defaultSuites;
|
|
148
|
+
}
|
|
149
|
+
// Default the Census & redistricting cycle to 2010
|
|
150
|
+
if (!(U.keyExists('cycle', config)))
|
|
151
|
+
config['cycle'] = 2010;
|
|
152
|
+
return config;
|
|
153
|
+
}
|
|
154
|
+
// Using the the data in the analytics session, calculate all the
|
|
155
|
+
// analytics & validations, saving/updating the individual test results.
|
|
156
|
+
analyzePlan(bLog = false) {
|
|
157
|
+
try {
|
|
158
|
+
preprocess_1.doPreprocessData(this);
|
|
159
|
+
analyze_1.doAnalyzeDistricts(this, bLog);
|
|
160
|
+
analyze_1.doAnalyzePlan(this, bLog);
|
|
161
|
+
report_1.doAnalyzePostProcessing(this);
|
|
162
|
+
}
|
|
163
|
+
catch (_a) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
// Get an individual test, so you can drive UI with the results.
|
|
169
|
+
getTest(testID) {
|
|
170
|
+
// Get the existing test entries
|
|
171
|
+
// T.Test is a numeric enum, so convert the string keys to numbers
|
|
172
|
+
let testValues = U.getNumericObjectKeys(this.tests);
|
|
173
|
+
// If there's no entry for this test, create & initialize one
|
|
174
|
+
if (!(testValues.includes(testID))) {
|
|
175
|
+
this.tests[testID] = {};
|
|
176
|
+
this.tests[testID]['score'] = undefined;
|
|
177
|
+
this.tests[testID]['details'] = {};
|
|
178
|
+
}
|
|
179
|
+
// Return a pointer to the the test entry for this test
|
|
180
|
+
return this.tests[testID];
|
|
181
|
+
}
|
|
182
|
+
// Prepare a scorecard for rendering
|
|
183
|
+
// NOTE - This assumes that analyzePlan() has been run!
|
|
184
|
+
prepareScorecard() {
|
|
185
|
+
return report_1.doPrepareScorecard(this);
|
|
186
|
+
}
|
|
187
|
+
// Prepare test results for rendering
|
|
188
|
+
// NOTE - This assumes that analyzePlan() has been run!
|
|
189
|
+
prepareTestLog() {
|
|
190
|
+
return report_1.doPrepareTestLog(this);
|
|
191
|
+
}
|
|
192
|
+
populationDeviationThreshold() {
|
|
193
|
+
return 1 - this.testScales[4 /* PopulationDeviation */]['testScale'][0];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
exports.AnalyticsSession = AnalyticsSession;
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
/***/ }),
|
|
200
|
+
|
|
201
|
+
/***/ "./src/_data.ts":
|
|
202
|
+
/*!**********************!*\
|
|
203
|
+
!*** ./src/_data.ts ***!
|
|
204
|
+
\**********************/
|
|
205
|
+
/*! no static exports found */
|
|
206
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
207
|
+
|
|
208
|
+
"use strict";
|
|
209
|
+
|
|
210
|
+
//
|
|
211
|
+
// DATA ABSTRACTION LAYER
|
|
212
|
+
//
|
|
213
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
214
|
+
const U = __webpack_require__(/*! ./utils */ "./src/utils.ts");
|
|
215
|
+
const valid_1 = __webpack_require__(/*! ./valid */ "./src/valid.ts");
|
|
216
|
+
const compact_1 = __webpack_require__(/*! ./compact */ "./src/compact.ts");
|
|
217
|
+
const political_1 = __webpack_require__(/*! ./political */ "./src/political.ts");
|
|
218
|
+
// DISTRICT STATISTICS
|
|
219
|
+
// Indexes for statistics fields in Districts
|
|
220
|
+
// NOTE - Not a const, so the number can be determined dynamically
|
|
221
|
+
var DistrictField;
|
|
222
|
+
(function (DistrictField) {
|
|
223
|
+
DistrictField[DistrictField["TotalPop"] = 0] = "TotalPop";
|
|
224
|
+
DistrictField[DistrictField["PopDevPct"] = 1] = "PopDevPct";
|
|
225
|
+
DistrictField[DistrictField["bEqualPop"] = 2] = "bEqualPop";
|
|
226
|
+
DistrictField[DistrictField["bNotEmpty"] = 3] = "bNotEmpty";
|
|
227
|
+
DistrictField[DistrictField["bContiguous"] = 4] = "bContiguous";
|
|
228
|
+
DistrictField[DistrictField["bNotEmbedded"] = 5] = "bNotEmbedded";
|
|
229
|
+
DistrictField[DistrictField["CountySplits"] = 6] = "CountySplits";
|
|
230
|
+
DistrictField[DistrictField["DemVotes"] = 7] = "DemVotes";
|
|
231
|
+
DistrictField[DistrictField["RepVotes"] = 8] = "RepVotes";
|
|
232
|
+
DistrictField[DistrictField["TwoPartyVote"] = 9] = "TwoPartyVote";
|
|
233
|
+
DistrictField[DistrictField["DemPct"] = 10] = "DemPct";
|
|
234
|
+
DistrictField[DistrictField["RepPct"] = 11] = "RepPct";
|
|
235
|
+
DistrictField[DistrictField["DemSeat"] = 12] = "DemSeat";
|
|
236
|
+
DistrictField[DistrictField["TotalVAP"] = 13] = "TotalVAP";
|
|
237
|
+
DistrictField[DistrictField["MinorityPop"] = 14] = "MinorityPop";
|
|
238
|
+
DistrictField[DistrictField["WhitePop"] = 15] = "WhitePop";
|
|
239
|
+
DistrictField[DistrictField["BlackPop"] = 16] = "BlackPop";
|
|
240
|
+
DistrictField[DistrictField["HispanicPop"] = 17] = "HispanicPop";
|
|
241
|
+
DistrictField[DistrictField["PacificPop"] = 18] = "PacificPop";
|
|
242
|
+
DistrictField[DistrictField["AsianPop"] = 19] = "AsianPop";
|
|
243
|
+
DistrictField[DistrictField["NativePop"] = 20] = "NativePop";
|
|
244
|
+
DistrictField[DistrictField["WhitePct"] = 21] = "WhitePct";
|
|
245
|
+
DistrictField[DistrictField["MinorityPct"] = 22] = "MinorityPct";
|
|
246
|
+
DistrictField[DistrictField["BlackPct"] = 23] = "BlackPct";
|
|
247
|
+
DistrictField[DistrictField["HispanicPct"] = 24] = "HispanicPct";
|
|
248
|
+
DistrictField[DistrictField["PacificPct"] = 25] = "PacificPct";
|
|
249
|
+
DistrictField[DistrictField["AsianPct"] = 26] = "AsianPct";
|
|
250
|
+
DistrictField[DistrictField["NativePct"] = 27] = "NativePct"; // Display
|
|
251
|
+
// 1 - MORE ...
|
|
252
|
+
})(DistrictField = exports.DistrictField || (exports.DistrictField = {}));
|
|
253
|
+
// The fields to display in a District Statistics pane
|
|
254
|
+
exports.DisplayFields = [
|
|
255
|
+
DistrictField.TotalPop,
|
|
256
|
+
DistrictField.PopDevPct,
|
|
257
|
+
DistrictField.bEqualPop,
|
|
258
|
+
DistrictField.bNotEmpty,
|
|
259
|
+
DistrictField.bContiguous,
|
|
260
|
+
DistrictField.bNotEmbedded,
|
|
261
|
+
DistrictField.DemPct,
|
|
262
|
+
DistrictField.WhitePct,
|
|
263
|
+
DistrictField.MinorityPct,
|
|
264
|
+
DistrictField.BlackPct,
|
|
265
|
+
DistrictField.HispanicPct,
|
|
266
|
+
DistrictField.PacificPct,
|
|
267
|
+
DistrictField.AsianPct,
|
|
268
|
+
DistrictField.NativePct
|
|
269
|
+
];
|
|
270
|
+
class Districts {
|
|
271
|
+
constructor(s, ds) {
|
|
272
|
+
this._geoProperties = {};
|
|
273
|
+
this._session = s;
|
|
274
|
+
this._shapes = ds;
|
|
275
|
+
this.statistics = this.initStatistics();
|
|
276
|
+
}
|
|
277
|
+
getShape(i) { return this._shapes.features[i]; }
|
|
278
|
+
getGeoProperties(i) { return this._geoProperties[i]; }
|
|
279
|
+
setGeoProperties(i, p) { this._geoProperties[i] = p; }
|
|
280
|
+
numberOfColumns() { return U.countEnumValues(DistrictField); }
|
|
281
|
+
// +1 for dummy unassigned 0 "district" and +1 for N+1 summary "district" for
|
|
282
|
+
// state-level values. Real districts are 1–N.
|
|
283
|
+
numberOfRows() { return this._session.state.nDistricts + 2; }
|
|
284
|
+
numberOfWorkingDistricts() { return this._session.state.nDistricts + 1; }
|
|
285
|
+
// This is the core statistics 2D matrix:
|
|
286
|
+
// Fields on the outside, districts on the inside
|
|
287
|
+
initStatistics() {
|
|
288
|
+
let nRows = this.numberOfRows();
|
|
289
|
+
let nCols = this.numberOfColumns();
|
|
290
|
+
let outer = U.initArray(nCols, undefined);
|
|
291
|
+
for (let i = 0; i < nCols; i++) {
|
|
292
|
+
outer[i] = U.initArray(nRows, null);
|
|
293
|
+
}
|
|
294
|
+
return outer;
|
|
295
|
+
}
|
|
296
|
+
// This is the workhorse computational routine!
|
|
297
|
+
//
|
|
298
|
+
// TODO - Optimize for getting multiple properties from the same feature?
|
|
299
|
+
// TODO - Optimize by only re-calc'ing districts that have changed?
|
|
300
|
+
// In this case, special attention to getting county-splits right.
|
|
301
|
+
// TODO - Optimize by asyn'ing this?
|
|
302
|
+
// TODO - Is there a way to do this programmatically off data? Does it matter?
|
|
303
|
+
recalcStatistics(bLog = false) {
|
|
304
|
+
// Compute these once per recalc cycle
|
|
305
|
+
let targetSize = this._session.state.totalPop / this._session.state.nDistricts;
|
|
306
|
+
let deviationThreshold = this._session.populationDeviationThreshold();
|
|
307
|
+
let planByDistrict = this._session.plan.byDistrictID();
|
|
308
|
+
let plan = this._session.plan;
|
|
309
|
+
let graph = this._session.graph;
|
|
310
|
+
// INITIALIZE STATE VALUES THAT WILL BE ACCUMULATED
|
|
311
|
+
let stateTPVote = 0;
|
|
312
|
+
let stateDemVote = 0;
|
|
313
|
+
let stateRepVote = 0;
|
|
314
|
+
let stateVAPPop = 0;
|
|
315
|
+
let stateWhitePop = 0;
|
|
316
|
+
let stateMinorityPop = 0;
|
|
317
|
+
let stateBlackPop = 0;
|
|
318
|
+
let stateHispanicPop = 0;
|
|
319
|
+
let statePacificPop = 0;
|
|
320
|
+
let stateAsianPop = 0;
|
|
321
|
+
let stateNativePop = 0;
|
|
322
|
+
// NOTE - These plan-level booleans are set in their respective analytics:
|
|
323
|
+
// - Equal population (bEqualPop)
|
|
324
|
+
// - Complete (bNotEmpty)
|
|
325
|
+
// - Contiguos (bContiguous)
|
|
326
|
+
// - Free of holes (bNotEmbedded)
|
|
327
|
+
// 2 - MORE ...
|
|
328
|
+
// Loop over the districts (including the dummy unassigned one)
|
|
329
|
+
for (let i = 0; i < this.numberOfWorkingDistricts(); i++) {
|
|
330
|
+
// INITIALIZE DISTRICT VALUES THAT WILL BE ACCUMULATED (VS. DERIVED)
|
|
331
|
+
let featurePop;
|
|
332
|
+
let totalPop = 0;
|
|
333
|
+
let countySplits = U.initArray(this._session.counties.nCounties, 0);
|
|
334
|
+
let demVotes = 0;
|
|
335
|
+
let repVotes = 0;
|
|
336
|
+
let totalVAP = 0;
|
|
337
|
+
let whitePop = 0;
|
|
338
|
+
let blackPop = 0;
|
|
339
|
+
let hispanicPop = 0;
|
|
340
|
+
let pacificPop = 0;
|
|
341
|
+
let asianPop = 0;
|
|
342
|
+
let nativePop = 0;
|
|
343
|
+
// 3 - MORE ...
|
|
344
|
+
// HACK - Because "this" gets ghosted inside the forEach loop below
|
|
345
|
+
let outerThis = this;
|
|
346
|
+
// Get the geoIDs assigned to it ...
|
|
347
|
+
let geoIDs = this._session.plan.geoIDsForDistrictID(i);
|
|
348
|
+
// ... loop over them creating district-by-district statistics
|
|
349
|
+
geoIDs.forEach(function (geoID) {
|
|
350
|
+
// Map from geoID to feature index
|
|
351
|
+
let featureID = outerThis._session.features.featureID(geoID);
|
|
352
|
+
let f = outerThis._session.features.featureByIndex(featureID);
|
|
353
|
+
// ACCUMULATE VALUES
|
|
354
|
+
// Total population of each feature (used more than once)
|
|
355
|
+
featurePop = outerThis._session.features.fieldForFeature(f, "CENSUS" /* CENSUS */, "Tot" /* TotalPop */);
|
|
356
|
+
// Total district population
|
|
357
|
+
totalPop += featurePop;
|
|
358
|
+
// Total population by counties w/in a district
|
|
359
|
+
countySplits[outerThis.getCountyIndex(geoID)] += featurePop;
|
|
360
|
+
// Democratic and Republican vote totals
|
|
361
|
+
demVotes += outerThis._session.features.fieldForFeature(f, "ELECTION" /* ELECTION */, "D" /* DemVotes */);
|
|
362
|
+
repVotes += outerThis._session.features.fieldForFeature(f, "ELECTION" /* ELECTION */, "R" /* RepVotes */);
|
|
363
|
+
// Voting-age demographic breakdowns (or citizen voting-age)
|
|
364
|
+
totalVAP += outerThis._session.features.fieldForFeature(f, "VAP" /* VAP */, "Tot" /* TotalPop */);
|
|
365
|
+
whitePop += outerThis._session.features.fieldForFeature(f, "VAP" /* VAP */, "Wh" /* WhitePop */);
|
|
366
|
+
blackPop += outerThis._session.features.fieldForFeature(f, "VAP" /* VAP */, "BlC" /* BlackPop */);
|
|
367
|
+
hispanicPop += outerThis._session.features.fieldForFeature(f, "VAP" /* VAP */, "His" /* HispanicPop */);
|
|
368
|
+
pacificPop += outerThis._session.features.fieldForFeature(f, "VAP" /* VAP */, "PacC" /* PacificPop */);
|
|
369
|
+
asianPop += outerThis._session.features.fieldForFeature(f, "VAP" /* VAP */, "AsnC" /* AsianPop */);
|
|
370
|
+
nativePop += outerThis._session.features.fieldForFeature(f, "VAP" /* VAP */, "NatC" /* NativePop */);
|
|
371
|
+
// 4 - MORE ...
|
|
372
|
+
});
|
|
373
|
+
// COMPUTE DERIVED VALUES
|
|
374
|
+
// Population deviation % and equal population (boolean) by district.
|
|
375
|
+
// Leave the values null for the dummy unassigned district.
|
|
376
|
+
let popDevPct = null;
|
|
377
|
+
let bEqualPop = null;
|
|
378
|
+
if (i > 0) {
|
|
379
|
+
popDevPct = (totalPop - targetSize) / targetSize;
|
|
380
|
+
bEqualPop = (popDevPct <= deviationThreshold);
|
|
381
|
+
}
|
|
382
|
+
// Total two-party (not total total!) votes, Democratic and Republican vote
|
|
383
|
+
// shares, and Democratic first-past-the-post win (= 1) or loss (= 0).
|
|
384
|
+
let totVotes;
|
|
385
|
+
let demPct = null;
|
|
386
|
+
let repPct = null;
|
|
387
|
+
let DemSeat = null;
|
|
388
|
+
totVotes = demVotes + repVotes;
|
|
389
|
+
if (totVotes > 0) {
|
|
390
|
+
demPct = demVotes / totVotes;
|
|
391
|
+
repPct = repVotes / totVotes;
|
|
392
|
+
DemSeat = political_1.fptpWin(demPct);
|
|
393
|
+
}
|
|
394
|
+
// Total minority VAP
|
|
395
|
+
let minorityPop = totalVAP - whitePop;
|
|
396
|
+
// Voting-age demographic proportions (or citizen voting-age)
|
|
397
|
+
let whitePct = null;
|
|
398
|
+
let minorityPct = null;
|
|
399
|
+
let blackPct = null;
|
|
400
|
+
let hispanicPct = null;
|
|
401
|
+
let pacificPct = null;
|
|
402
|
+
let asianPct = null;
|
|
403
|
+
let nativePct = null;
|
|
404
|
+
if (totalVAP > 0) {
|
|
405
|
+
whitePct = whitePop / totalVAP;
|
|
406
|
+
minorityPct = minorityPop / totalVAP;
|
|
407
|
+
blackPct = blackPop / totalVAP;
|
|
408
|
+
hispanicPct = hispanicPop / totalVAP;
|
|
409
|
+
pacificPct = pacificPop / totalVAP;
|
|
410
|
+
asianPct = asianPop / totalVAP;
|
|
411
|
+
nativePct = nativePop / totalVAP;
|
|
412
|
+
}
|
|
413
|
+
// 5 - MORE ...
|
|
414
|
+
// COMPUTE DISTRICT-LEVEL VALUES
|
|
415
|
+
// Validations
|
|
416
|
+
let bNotEmpty = (!U.isSetEmpty(geoIDs));
|
|
417
|
+
let bContiguous = null;
|
|
418
|
+
let bNotEmbedded = null;
|
|
419
|
+
// Leave the values null for the dummy unassigned district,
|
|
420
|
+
// and districts that are empty.
|
|
421
|
+
if (i > 0) {
|
|
422
|
+
if (bNotEmpty) {
|
|
423
|
+
bContiguous = valid_1.isConnected(geoIDs, graph);
|
|
424
|
+
bNotEmbedded = (!valid_1.isEmbedded(i, planByDistrict[i], plan, graph));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// 6 - MORE ...
|
|
428
|
+
// UPDATE THE DISTRICT STATISTICS
|
|
429
|
+
this.statistics[DistrictField.bNotEmpty][i] = bNotEmpty;
|
|
430
|
+
this.statistics[DistrictField.bContiguous][i] = bContiguous;
|
|
431
|
+
this.statistics[DistrictField.bNotEmbedded][i] = bNotEmbedded;
|
|
432
|
+
this.statistics[DistrictField.TotalPop][i] = totalPop;
|
|
433
|
+
this.statistics[DistrictField.PopDevPct][i] = popDevPct;
|
|
434
|
+
this.statistics[DistrictField.bEqualPop][i] = bEqualPop;
|
|
435
|
+
this.statistics[DistrictField.CountySplits][i] = countySplits;
|
|
436
|
+
this.statistics[DistrictField.DemVotes][i] = demVotes;
|
|
437
|
+
this.statistics[DistrictField.RepVotes][i] = repVotes;
|
|
438
|
+
this.statistics[DistrictField.TwoPartyVote][i] = totVotes;
|
|
439
|
+
this.statistics[DistrictField.DemPct][i] = demPct;
|
|
440
|
+
this.statistics[DistrictField.RepPct][i] = repPct;
|
|
441
|
+
this.statistics[DistrictField.DemSeat][i] = DemSeat;
|
|
442
|
+
this.statistics[DistrictField.WhitePop][i] = whitePop;
|
|
443
|
+
this.statistics[DistrictField.MinorityPop][i] = minorityPop;
|
|
444
|
+
this.statistics[DistrictField.BlackPop][i] = blackPop;
|
|
445
|
+
this.statistics[DistrictField.HispanicPop][i] = hispanicPop;
|
|
446
|
+
this.statistics[DistrictField.PacificPop][i] = pacificPop;
|
|
447
|
+
this.statistics[DistrictField.AsianPop][i] = asianPop;
|
|
448
|
+
this.statistics[DistrictField.NativePop][i] = nativePop;
|
|
449
|
+
this.statistics[DistrictField.TotalVAP][i] = totalVAP;
|
|
450
|
+
this.statistics[DistrictField.WhitePct][i] = whitePct;
|
|
451
|
+
this.statistics[DistrictField.MinorityPct][i] = minorityPct;
|
|
452
|
+
this.statistics[DistrictField.BlackPct][i] = blackPct;
|
|
453
|
+
this.statistics[DistrictField.HispanicPct][i] = hispanicPct;
|
|
454
|
+
this.statistics[DistrictField.PacificPct][i] = pacificPct;
|
|
455
|
+
this.statistics[DistrictField.AsianPct][i] = asianPct;
|
|
456
|
+
this.statistics[DistrictField.NativePct][i] = nativePct;
|
|
457
|
+
// 7 - MORE ...
|
|
458
|
+
// ACCUMULATE STATE STATISTICS FROM DISTRICT TOTALS
|
|
459
|
+
stateTPVote += totVotes;
|
|
460
|
+
stateDemVote += demVotes;
|
|
461
|
+
stateRepVote += repVotes;
|
|
462
|
+
stateVAPPop += totalVAP;
|
|
463
|
+
stateWhitePop += whitePop;
|
|
464
|
+
stateMinorityPop += minorityPop;
|
|
465
|
+
stateBlackPop += blackPop;
|
|
466
|
+
stateHispanicPop += hispanicPop;
|
|
467
|
+
statePacificPop += pacificPop;
|
|
468
|
+
stateAsianPop += asianPop;
|
|
469
|
+
stateNativePop += nativePop;
|
|
470
|
+
}
|
|
471
|
+
// UPDATE STATE STATISTICS
|
|
472
|
+
let summaryRow = this.numberOfRows() - 1;
|
|
473
|
+
if (stateTPVote > 0) {
|
|
474
|
+
this.statistics[DistrictField.DemPct][summaryRow] = stateDemVote / stateTPVote;
|
|
475
|
+
this.statistics[DistrictField.RepPct][summaryRow] = stateRepVote / stateTPVote;
|
|
476
|
+
}
|
|
477
|
+
if (stateVAPPop > 0) {
|
|
478
|
+
this.statistics[DistrictField.WhitePct][summaryRow] = stateWhitePop / stateVAPPop;
|
|
479
|
+
this.statistics[DistrictField.MinorityPct][summaryRow] = stateMinorityPop / stateVAPPop;
|
|
480
|
+
this.statistics[DistrictField.BlackPct][summaryRow] = stateBlackPop / stateVAPPop;
|
|
481
|
+
this.statistics[DistrictField.HispanicPct][summaryRow] = stateHispanicPop / stateVAPPop;
|
|
482
|
+
this.statistics[DistrictField.PacificPct][summaryRow] = statePacificPop / stateVAPPop;
|
|
483
|
+
this.statistics[DistrictField.AsianPct][summaryRow] = stateAsianPop / stateVAPPop;
|
|
484
|
+
this.statistics[DistrictField.NativePct][summaryRow] = stateNativePop / stateVAPPop;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// NOTE - I did not roll these into district statistics, because creating the
|
|
488
|
+
// district shapes themselves is the big district-by-district activity, these
|
|
489
|
+
// calc's already work, and I'm not going to expose these values. Wrapping
|
|
490
|
+
// the underlying function and exposing it here to illustrate the parallelism
|
|
491
|
+
// with recalcStatistics(). These are called in tandem by doAnalyzeDistricts().
|
|
492
|
+
extractDistrictShapeProperties(bLog = false) {
|
|
493
|
+
compact_1.extractDistrictProperties(this._session, bLog);
|
|
494
|
+
}
|
|
495
|
+
getCountyIndex(geoID) {
|
|
496
|
+
let countyGeoID = U.parseGeoID(geoID)['county'];
|
|
497
|
+
let countyFIPS = U.getFIPSFromCountyGeoID(countyGeoID);
|
|
498
|
+
let countyIndex = this._session.counties.indexFromFIPS(countyFIPS);
|
|
499
|
+
return countyIndex;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
exports.Districts = Districts;
|
|
503
|
+
exports.DatasetDescriptions = {
|
|
504
|
+
D16F: "2016 ACS Total Population",
|
|
505
|
+
D16T: "2016 ACS Voting Age Population",
|
|
506
|
+
E16GPR: "2016 Presidential Election"
|
|
507
|
+
// TODO - What other potential datasets?
|
|
508
|
+
};
|
|
509
|
+
// Wrap data by feature, to abstract the specifics of the internal structure
|
|
510
|
+
class Features {
|
|
511
|
+
constructor(s, data, keys) {
|
|
512
|
+
this._featureIDs = {};
|
|
513
|
+
this._session = s;
|
|
514
|
+
this._data = data;
|
|
515
|
+
this._keys = keys;
|
|
516
|
+
}
|
|
517
|
+
nFeatures() { return this._data.features.length; }
|
|
518
|
+
featureByIndex(i) { return this._data.features[i]; }
|
|
519
|
+
geoIDForFeature(f) { return f.properties['GEOID']; }
|
|
520
|
+
fieldForFeature(f, dt, fk) {
|
|
521
|
+
let dk = this._keys[dt];
|
|
522
|
+
return _getFeatures(f, dk, fk);
|
|
523
|
+
}
|
|
524
|
+
resetDataset(d, k) {
|
|
525
|
+
this._keys[d] = k;
|
|
526
|
+
// TODO - Does anything need to be recalc'd now when a dataset is changed?
|
|
527
|
+
}
|
|
528
|
+
mapGeoIDsToFeatureIDs() {
|
|
529
|
+
for (let i = 0; i < this._session.features.nFeatures(); i++) {
|
|
530
|
+
let f = this._session.features.featureByIndex(i);
|
|
531
|
+
let geoID = this._session.features.geoIDForFeature(f);
|
|
532
|
+
this._featureIDs[geoID] = i;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
featureID(i) { return this._featureIDs[i]; }
|
|
536
|
+
}
|
|
537
|
+
exports.Features = Features;
|
|
538
|
+
// NOTE - This accessor is cloned from fGetW() in dra-client/restrict.ts
|
|
539
|
+
// f is a direct GeoJSON feature
|
|
540
|
+
// p is a geoID
|
|
541
|
+
function _getFeatures(f, datasetKey, p) {
|
|
542
|
+
// Shim to load sample data2.json from disk for command-line scaffolding
|
|
543
|
+
if (f.properties && f.properties['datasets']) {
|
|
544
|
+
return f.properties['datasets'][datasetKey][p];
|
|
545
|
+
}
|
|
546
|
+
// NOTE - The fGetW() code from dra-client below here ...
|
|
547
|
+
// Direct property?
|
|
548
|
+
if (f.properties && f.properties[p] !== undefined)
|
|
549
|
+
return f.properties[p];
|
|
550
|
+
// Joined property?
|
|
551
|
+
let a = _fGetJoined(f);
|
|
552
|
+
if (a) {
|
|
553
|
+
for (let i = 0; i < a.length; i++) {
|
|
554
|
+
let o = a[i];
|
|
555
|
+
if (!datasetKey) {
|
|
556
|
+
if (o[p] !== undefined)
|
|
557
|
+
return o[p];
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
if (o['datasets'] && o['datasets'][datasetKey])
|
|
561
|
+
if (o['datasets'][datasetKey][p])
|
|
562
|
+
return o['datasets'][datasetKey][p];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
function _fGetJoined(f) {
|
|
569
|
+
return (f.properties && f.properties.joined) ? f.properties.joined : undefined;
|
|
570
|
+
}
|
|
571
|
+
// Wrap data by county, to abstract the specifics of the internal structure
|
|
572
|
+
class Counties {
|
|
573
|
+
constructor(s, data) {
|
|
574
|
+
this.index = {};
|
|
575
|
+
this._session = s;
|
|
576
|
+
this._data = data;
|
|
577
|
+
this.nCounties = this._data.features.length;
|
|
578
|
+
this._countyNameLookup = {};
|
|
579
|
+
}
|
|
580
|
+
// nCounties(): number { return this._data.features.length; }
|
|
581
|
+
countyByIndex(i) { return this._data.features[i]; }
|
|
582
|
+
propertyForCounty(f, pk) { return f.properties[pk]; }
|
|
583
|
+
mapFIPSToName(fips, name) { this._countyNameLookup[fips] = name; }
|
|
584
|
+
nameFromFIPS(fips) { return this._countyNameLookup[fips]; }
|
|
585
|
+
indexFromFIPS(fips) { return this.index[fips]; }
|
|
586
|
+
}
|
|
587
|
+
exports.Counties = Counties;
|
|
588
|
+
// CLASSES TO ORGANIZE AND/OR ABSTRACT OTHER DATA
|
|
589
|
+
class State {
|
|
590
|
+
constructor(s, xx, n) {
|
|
591
|
+
this.totalPop = 0;
|
|
592
|
+
this.tooBigFIPS = [];
|
|
593
|
+
this.tooBigName = [];
|
|
594
|
+
this.expectedSplits = 0;
|
|
595
|
+
this.expectedAffected = 0;
|
|
596
|
+
this._session = s;
|
|
597
|
+
this.xx = xx;
|
|
598
|
+
this.nDistricts = n;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
exports.State = State;
|
|
602
|
+
class Plan {
|
|
603
|
+
constructor(s, p) {
|
|
604
|
+
this._session = s;
|
|
605
|
+
this._planByGeoID = p;
|
|
606
|
+
this._planByDistrictID = {};
|
|
607
|
+
this.districtIDs = []; // Set when the plan in inverted
|
|
608
|
+
}
|
|
609
|
+
invertPlan() {
|
|
610
|
+
this._planByDistrictID = U.invertPlan(this._planByGeoID);
|
|
611
|
+
this.districtIDs = U.getNumericObjectKeys(this._planByDistrictID);
|
|
612
|
+
}
|
|
613
|
+
initializeDistrict(i) { this._planByDistrictID[i] = new Set(); }
|
|
614
|
+
byGeoID() { return this._planByGeoID; }
|
|
615
|
+
byDistrictID() { return this._planByDistrictID; }
|
|
616
|
+
districtForGeoID(i) { return this._planByGeoID[i]; }
|
|
617
|
+
geoIDsForDistrictID(i) { return this._planByDistrictID[i]; }
|
|
618
|
+
}
|
|
619
|
+
exports.Plan = Plan;
|
|
620
|
+
class Graph {
|
|
621
|
+
constructor(s, graph) {
|
|
622
|
+
this._session = s;
|
|
623
|
+
this._graph = graph;
|
|
624
|
+
}
|
|
625
|
+
// TODO - Rework this, when we support MIXED MAPS.
|
|
626
|
+
peerNeighbors(node) {
|
|
627
|
+
// Get the neighboring geoIDs connected to a geoID
|
|
628
|
+
// Ignore the lengths of the shared borders (the values), for now
|
|
629
|
+
return U.getObjectKeys(this._graph[node]);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
exports.Graph = Graph;
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
/***/ }),
|
|
636
|
+
|
|
637
|
+
/***/ "./src/analyze.ts":
|
|
638
|
+
/*!************************!*\
|
|
639
|
+
!*** ./src/analyze.ts ***!
|
|
640
|
+
\************************/
|
|
641
|
+
/*! no static exports found */
|
|
642
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
643
|
+
|
|
644
|
+
"use strict";
|
|
645
|
+
|
|
646
|
+
//
|
|
647
|
+
// ANALYZE A PLAN
|
|
648
|
+
//
|
|
649
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
650
|
+
const valid_1 = __webpack_require__(/*! ./valid */ "./src/valid.ts");
|
|
651
|
+
const equal_1 = __webpack_require__(/*! ./equal */ "./src/equal.ts");
|
|
652
|
+
const compact_1 = __webpack_require__(/*! ./compact */ "./src/compact.ts");
|
|
653
|
+
const cohesive_1 = __webpack_require__(/*! ./cohesive */ "./src/cohesive.ts");
|
|
654
|
+
const political_1 = __webpack_require__(/*! ./political */ "./src/political.ts");
|
|
655
|
+
const minority_1 = __webpack_require__(/*! ./minority */ "./src/minority.ts");
|
|
656
|
+
// Compile district-level info for plan/map-level analytics
|
|
657
|
+
function doAnalyzeDistricts(s, bLog = false) {
|
|
658
|
+
s.districts.recalcStatistics(bLog);
|
|
659
|
+
s.districts.extractDistrictShapeProperties(bLog);
|
|
660
|
+
}
|
|
661
|
+
exports.doAnalyzeDistricts = doAnalyzeDistricts;
|
|
662
|
+
// TODO - I could make this table-driven, but I'm thinking that the explicit
|
|
663
|
+
// calls might make chunking for aync easier.
|
|
664
|
+
// Calculate the analytics & validations and cache the results
|
|
665
|
+
// NOTE - doAnalyzePlan() depends on doAnalyzeDistricts() having run first.
|
|
666
|
+
function doAnalyzePlan(s, bLog = false) {
|
|
667
|
+
// Get the requested suites, and only execute those tests
|
|
668
|
+
let requestedSuites = s.config['suites'];
|
|
669
|
+
// Tests in the "Legal" suite, i.e., pass/ fail constraints
|
|
670
|
+
if (requestedSuites.includes(0 /* Legal */)) {
|
|
671
|
+
s.tests[0 /* Complete */] = valid_1.doIsComplete(s);
|
|
672
|
+
s.tests[1 /* Contiguous */] = valid_1.doIsContiguous(s);
|
|
673
|
+
s.tests[2 /* FreeOfHoles */] = valid_1.doIsFreeOfHoles(s);
|
|
674
|
+
s.tests[4 /* PopulationDeviation */] = equal_1.doPopulationDeviation(s);
|
|
675
|
+
// NOTE - I can't check whether a population deviation is legal or not, until
|
|
676
|
+
// the raw % is normalized. A zero (0) would mean "too much" / "not legal," for
|
|
677
|
+
// the given type of district (CD vs. LD). The EqualPopulation test is derived
|
|
678
|
+
// from PopulationDeviation, as part of scorecard or test log preparation.
|
|
679
|
+
// Create an empty test entry here though ...
|
|
680
|
+
s.tests[3 /* EqualPopulation */] = s.getTest(3 /* EqualPopulation */);
|
|
681
|
+
}
|
|
682
|
+
// Tests in the "Fair" suite
|
|
683
|
+
if (requestedSuites.includes(1 /* Fair */)) {
|
|
684
|
+
s.tests[9 /* SeatsBias */] = political_1.doSeatsBias(s);
|
|
685
|
+
s.tests[10 /* VotesBias */] = political_1.doVotesBias(s);
|
|
686
|
+
s.tests[11 /* Responsiveness */] = political_1.doResponsiveness(s);
|
|
687
|
+
s.tests[12 /* ResponsiveDistricts */] = political_1.doResponsiveDistricts(s);
|
|
688
|
+
s.tests[13 /* EfficiencyGap */] = political_1.doEfficiencyGap(s);
|
|
689
|
+
s.tests[14 /* MajorityMinorityDistricts */] = minority_1.doMajorityMinorityDistricts(s);
|
|
690
|
+
}
|
|
691
|
+
// Tests in the "Best" suite, i.e., criteria for better/worse
|
|
692
|
+
if (requestedSuites.includes(2 /* Best */)) {
|
|
693
|
+
s.tests[5 /* Reock */] = compact_1.doReock(s, bLog);
|
|
694
|
+
s.tests[6 /* PolsbyPopper */] = compact_1.doPolsbyPopper(s, bLog);
|
|
695
|
+
s.tests[7 /* CountySplits */] = cohesive_1.doCountySplits(s);
|
|
696
|
+
s.tests[8 /* Complexity */] = cohesive_1.doPlanComplexity(s);
|
|
697
|
+
}
|
|
698
|
+
// Enable a Test Log and Scorecard to be generated
|
|
699
|
+
s.bPlanAnalyzed = true;
|
|
700
|
+
s.bPostProcessingDone = false;
|
|
701
|
+
}
|
|
702
|
+
exports.doAnalyzePlan = doAnalyzePlan;
|
|
703
|
+
// Derive secondary analytics that are based on primary tests.
|
|
704
|
+
// This concept allows Population Deviation to be a primary numeric test and
|
|
705
|
+
// Equal Population to be secondary pass/fail validation.
|
|
706
|
+
//
|
|
707
|
+
// NOTE - Should this be conditionalized on the test suites requested?
|
|
708
|
+
// Those are encapsulated in reports.ts right now, so not doing that.
|
|
709
|
+
function doDeriveSecondaryTests(s) {
|
|
710
|
+
s.tests[3 /* EqualPopulation */] = equal_1.doHasEqualPopulations(s);
|
|
711
|
+
}
|
|
712
|
+
exports.doDeriveSecondaryTests = doDeriveSecondaryTests;
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
/***/ }),
|
|
716
|
+
|
|
717
|
+
/***/ "./src/cohesive.ts":
|
|
718
|
+
/*!*************************!*\
|
|
719
|
+
!*** ./src/cohesive.ts ***!
|
|
720
|
+
\*************************/
|
|
721
|
+
/*! no static exports found */
|
|
722
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
723
|
+
|
|
724
|
+
"use strict";
|
|
725
|
+
|
|
726
|
+
//
|
|
727
|
+
// "COHESIVE" - We're naming this category which is about county splitting.
|
|
728
|
+
//
|
|
729
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
730
|
+
const U = __webpack_require__(/*! ./utils */ "./src/utils.ts");
|
|
731
|
+
const D = __webpack_require__(/*! ./_data */ "./src/_data.ts");
|
|
732
|
+
function doCountySplits(s) {
|
|
733
|
+
let test = s.getTest(7 /* CountySplits */);
|
|
734
|
+
// THE THREE VALUES TO DETERMINE FOR A PLAN
|
|
735
|
+
let unexpectedSplits = 0;
|
|
736
|
+
let unexpectedAffected = 0;
|
|
737
|
+
let countiesSplitUnexpectedly = [];
|
|
738
|
+
// FIRST, ANALYZE THE COUNTY SPLITING FOR THE PLAN
|
|
739
|
+
// Pivot census totals into county-district "splits"
|
|
740
|
+
let countiesByDistrict = s.districts.statistics[D.DistrictField.CountySplits];
|
|
741
|
+
// countiesByDistrict = countiesByDistrict.slice(1, -1);
|
|
742
|
+
// Find the single-county districts, i.e., districts NOT split across counties.
|
|
743
|
+
// Ignore the dummy unassigned 0 and N+1 summary "districts."
|
|
744
|
+
let singleCountyDistricts = [];
|
|
745
|
+
for (let d = 1; d <= s.state.nDistricts; d++) {
|
|
746
|
+
// See if there's only one county partition
|
|
747
|
+
let nCountiesInDistrict = 0;
|
|
748
|
+
for (let c = 0; c < s.counties.nCounties; c++) {
|
|
749
|
+
if (countiesByDistrict[d][c] > 0) {
|
|
750
|
+
nCountiesInDistrict += 1;
|
|
751
|
+
if (nCountiesInDistrict > 1) {
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// If so, save the district
|
|
757
|
+
if (nCountiesInDistrict == 1) {
|
|
758
|
+
singleCountyDistricts.push(d);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Process the splits/partitions in the plan:
|
|
762
|
+
// - Count the total # of partitions,
|
|
763
|
+
// - Find the counties split across districts, and
|
|
764
|
+
// - Accumulate the number people affected (except when single-county districts)
|
|
765
|
+
let nPartitionsOverall = 0;
|
|
766
|
+
let splitCounties = new Set(); // The counties that are split across districts
|
|
767
|
+
let totalAffected = 0; // The total population affected by those splits
|
|
768
|
+
for (let c = 0; c < s.counties.nCounties; c++) {
|
|
769
|
+
let nCountyParts = 0;
|
|
770
|
+
let subtotal = 0;
|
|
771
|
+
for (let d = 1; d <= s.state.nDistricts; d++) {
|
|
772
|
+
if (countiesByDistrict[d][c] > 0) {
|
|
773
|
+
nPartitionsOverall += 1;
|
|
774
|
+
nCountyParts += 1;
|
|
775
|
+
if (!(U.arrayContains(singleCountyDistricts, d))) {
|
|
776
|
+
subtotal += countiesByDistrict[d][c];
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (nCountyParts > 1) {
|
|
781
|
+
splitCounties.add(c);
|
|
782
|
+
totalAffected += subtotal;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// Convert county ordinals to FIPS codes
|
|
786
|
+
let splitCountiesFIPS = U.getSelectObjectKeys(s.counties.index, [...splitCounties]);
|
|
787
|
+
// THEN TAKE ACCOUNT OF THE COUNTY SPLITTING THAT IS EXPECTED (REQUIRED)
|
|
788
|
+
// Compute the total number of splits this way, in case any counties are split
|
|
789
|
+
// more than once. I.e., it's not just len(all_counties_split).
|
|
790
|
+
let nSplits = nPartitionsOverall - s.counties.nCounties;
|
|
791
|
+
// Determine the number of *unexpected* splits. NOTE: Prevent negative numbers,
|
|
792
|
+
// in case you have a plan the *doesn't* split counties that would have to be
|
|
793
|
+
// split due to their size.
|
|
794
|
+
unexpectedSplits = Math.max(0, nSplits - s.state.expectedSplits);
|
|
795
|
+
// Subtract off the total population that *has* to be affected by splits,
|
|
796
|
+
// because their counties are too big. NOTE: Again, prevent negative numbers,
|
|
797
|
+
// in case you have a plan the *doesn't* split counties that would have to be
|
|
798
|
+
// split due to their size.
|
|
799
|
+
unexpectedAffected = U.trim(Math.max(0, totalAffected - s.state.expectedAffected) / s.state.totalPop);
|
|
800
|
+
// Find the counties that are split *unexpectedly*. From all the counties that
|
|
801
|
+
// are split, remove those that *have* to be split, because they are bigger than
|
|
802
|
+
// a district.
|
|
803
|
+
let countiesSplitUnexpectedlyFIPS = [];
|
|
804
|
+
for (let fips of splitCountiesFIPS) {
|
|
805
|
+
if (!(U.arrayContains(s.state.tooBigFIPS, fips))) {
|
|
806
|
+
countiesSplitUnexpectedlyFIPS.push(fips);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// ... and convert the FIPS codes to county names.
|
|
810
|
+
for (let fips of countiesSplitUnexpectedlyFIPS) {
|
|
811
|
+
countiesSplitUnexpectedly.push(s.counties.nameFromFIPS(fips));
|
|
812
|
+
}
|
|
813
|
+
countiesSplitUnexpectedly = countiesSplitUnexpectedly.sort();
|
|
814
|
+
// Cache the results in the test
|
|
815
|
+
test['score'] = unexpectedAffected; // TODO - Use Moon's complexity metric here
|
|
816
|
+
test['details'] = {
|
|
817
|
+
'unexpectedSplits': unexpectedSplits,
|
|
818
|
+
'unexpectedAffected': unexpectedAffected,
|
|
819
|
+
'countiesSplitUnexpectedly': countiesSplitUnexpectedly
|
|
820
|
+
};
|
|
821
|
+
return test;
|
|
822
|
+
}
|
|
823
|
+
exports.doCountySplits = doCountySplits;
|
|
824
|
+
// 2 - THE COMPLEXITY ANALYTIC NEEDS THE FOLLOWING DATA:
|
|
825
|
+
//
|
|
826
|
+
// If a map is already in simplified (mixed) form, the complexity analytic needs
|
|
827
|
+
// two pieces of data:
|
|
828
|
+
// - The counts of features by summary level--i.e., the numbers of counties, tracts,
|
|
829
|
+
// block groups, and blocks in a state; and
|
|
830
|
+
// - The map -- So it can count the features by summary level in the map,
|
|
831
|
+
// as well as the number of BG’s that are split.
|
|
832
|
+
//
|
|
833
|
+
// TODO - Where would the state counts come from? Preprocessed and passed in, or
|
|
834
|
+
// done in a one-time initialization call (which would require a full set of
|
|
835
|
+
// block geo_id's for the state).
|
|
836
|
+
//
|
|
837
|
+
// However, if a map is not yet (fully) simplified, then determining the
|
|
838
|
+
// complexity of a map also requires a preprocessed summary level hierarchy, so
|
|
839
|
+
// you can get the child features (e.g., tracts) of a parent feature (e.g.,
|
|
840
|
+
// a county).
|
|
841
|
+
//
|
|
842
|
+
// NOTE - I have script for producing this hierarchy which we could repurpose.
|
|
843
|
+
//
|
|
844
|
+
// TODO - For mixed map processing--specfically to find the neighbors of a feature
|
|
845
|
+
// that are actually in the map (as opposed to just neighbors at the same
|
|
846
|
+
// summary level in the static graph)--you need a special hierarchy that
|
|
847
|
+
// distinguishes between the 'interior' and 'edge children of a feature.
|
|
848
|
+
//
|
|
849
|
+
// NOTE - The script noted above does this.
|
|
850
|
+
function doPlanComplexity(s) {
|
|
851
|
+
let test = s.getTest(8 /* Complexity */);
|
|
852
|
+
console.log("TODO - Calculating plan complexity ...");
|
|
853
|
+
return test;
|
|
854
|
+
}
|
|
855
|
+
exports.doPlanComplexity = doPlanComplexity;
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
/***/ }),
|
|
859
|
+
|
|
860
|
+
/***/ "./src/compact.ts":
|
|
861
|
+
/*!************************!*\
|
|
862
|
+
!*** ./src/compact.ts ***!
|
|
863
|
+
\************************/
|
|
864
|
+
/*! no static exports found */
|
|
865
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
866
|
+
|
|
867
|
+
"use strict";
|
|
868
|
+
|
|
869
|
+
//
|
|
870
|
+
// COMPACT
|
|
871
|
+
//
|
|
872
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
873
|
+
const geofeature_1 = __webpack_require__(/*! ./geofeature */ "./src/geofeature.ts");
|
|
874
|
+
const U = __webpack_require__(/*! ./utils */ "./src/utils.ts");
|
|
875
|
+
// Measures of compactness compare district shapes to various ideally compact
|
|
876
|
+
// benchmarks, such as circles. All else equal, more compact districts are better.
|
|
877
|
+
//
|
|
878
|
+
// There are four popular measures of compactness. They either focus on how
|
|
879
|
+
// dispersed or how indented a shapes are.
|
|
880
|
+
//
|
|
881
|
+
// These first two measures are the most important:
|
|
882
|
+
//
|
|
883
|
+
// Reock is the primary measure of the dispersion of district shapes, calculated
|
|
884
|
+
// as “the area of the district to the area of the minimum spanning circle that
|
|
885
|
+
// can enclose the district.”
|
|
886
|
+
//
|
|
887
|
+
// R = A / A(Minimum Bounding Circle)
|
|
888
|
+
// R = A / (π * (D / 2)^2)
|
|
889
|
+
//
|
|
890
|
+
// R = 4A / πD^2
|
|
891
|
+
//
|
|
892
|
+
// where A is the area of the district and D is the diameter of the minimum
|
|
893
|
+
// bounding circle.
|
|
894
|
+
//
|
|
895
|
+
// Polsby-Popper is the primary measure of the indendentation of district shapes,
|
|
896
|
+
// calculated as the “the ratio of the area of the district to the area of a circle
|
|
897
|
+
// whose circumference is equal to the perimeter of the district.”
|
|
898
|
+
//
|
|
899
|
+
// PP = A / A(C)
|
|
900
|
+
//
|
|
901
|
+
// where C is that circle. In other words:
|
|
902
|
+
//
|
|
903
|
+
// P = 2πRc and A(C) = π(P / 2π)^2
|
|
904
|
+
//
|
|
905
|
+
// where P is the perimeter of the district and Rc is the radius of the circle.
|
|
906
|
+
//
|
|
907
|
+
// Hence, the measure simplifies to:
|
|
908
|
+
//
|
|
909
|
+
// PP = 4π * (A / P^2)
|
|
910
|
+
//
|
|
911
|
+
// I propose that we use these two, normalize them, and weight equally to determine
|
|
912
|
+
// our compactness value.
|
|
913
|
+
//
|
|
914
|
+
// These second two measures may be used to complement the primary ones above:
|
|
915
|
+
//
|
|
916
|
+
// Convex Hull is a secondary measure of the dispersion of district shapes, calculated
|
|
917
|
+
// as “the ratio of the district area to the area of the minimum convex bounding
|
|
918
|
+
// polygon (also known as a convex hull) enclosing the district.”
|
|
919
|
+
//
|
|
920
|
+
// CH = A / A(Convex Hull)
|
|
921
|
+
//
|
|
922
|
+
// where a convex hull is the minimum perimeter that encloses all points in a shape,
|
|
923
|
+
// basically the shortest unstretched rubber band that fits around the shape.
|
|
924
|
+
//
|
|
925
|
+
// Schwartzberg is a secondary measure of the degree of indentation of district
|
|
926
|
+
// shapes, calculated as “the ratio of the perimeter of the district to the circumference
|
|
927
|
+
// of a circle whose area is equal to the area of the district.”
|
|
928
|
+
//
|
|
929
|
+
// S = 1 / (P / C)
|
|
930
|
+
//
|
|
931
|
+
// where P is the perimeter of the district and C is the circumference of the circle.
|
|
932
|
+
// The radius of the circle is:
|
|
933
|
+
//
|
|
934
|
+
// Rc = SQRT(A / π)
|
|
935
|
+
//
|
|
936
|
+
// So, the circumference of the circle is:
|
|
937
|
+
//
|
|
938
|
+
// C = 2πRc or C = 2π * SQRT(A / π)
|
|
939
|
+
//
|
|
940
|
+
// Hence:
|
|
941
|
+
//
|
|
942
|
+
// S = 1 (P / 2π * SQRT(A / π))
|
|
943
|
+
//
|
|
944
|
+
// S = (2π * SQRT(A / π)) / P
|
|
945
|
+
//
|
|
946
|
+
// All these measures produce values between 0 and 1, with 0 being the least compact
|
|
947
|
+
// and 1 being the most compact. Sometimes these values are multiplied by 100 to
|
|
948
|
+
// give values between 0 and 100.
|
|
949
|
+
//
|
|
950
|
+
// For each measure, the compactness of a set of Congressional districts is the
|
|
951
|
+
// average of that measure for all the districts.
|
|
952
|
+
//
|
|
953
|
+
// Calculate Reock compactness:
|
|
954
|
+
// reock = (4 * a) / (math.pi * d**2)
|
|
955
|
+
// NOTE - Depends on extractDistrictProperties running first
|
|
956
|
+
function doReock(s, bLog = false) {
|
|
957
|
+
let test = s.getTest(5 /* Reock */);
|
|
958
|
+
// Calculate Reock scores by district
|
|
959
|
+
let scores = [];
|
|
960
|
+
for (let districtID = 1; districtID <= s.state.nDistricts; districtID++) {
|
|
961
|
+
let districtProps = s.districts.getGeoProperties(districtID);
|
|
962
|
+
let a = districtProps[0 /* Area */];
|
|
963
|
+
let d = districtProps[1 /* Diameter */];
|
|
964
|
+
let reock = (4 * a) / (Math.PI * Math.pow(d, 2));
|
|
965
|
+
// Save each district score
|
|
966
|
+
scores.push(reock);
|
|
967
|
+
// Echo the results by district
|
|
968
|
+
if (bLog)
|
|
969
|
+
console.log("Reock for district", districtID, "=", reock);
|
|
970
|
+
}
|
|
971
|
+
// Populate the test entry
|
|
972
|
+
let averageReock = U.avgArray(scores);
|
|
973
|
+
test['score'] = U.trim(averageReock);
|
|
974
|
+
test['details'] = {}; // TODO - Any details?
|
|
975
|
+
return test;
|
|
976
|
+
}
|
|
977
|
+
exports.doReock = doReock;
|
|
978
|
+
// Calculate Polsby-Popper compactness measures:
|
|
979
|
+
// polsby_popper = (4 * math.pi) * (a / p**2)
|
|
980
|
+
// NOTE - Depends on extractDistrictProperties running first
|
|
981
|
+
function doPolsbyPopper(s, bLog = false) {
|
|
982
|
+
let test = s.getTest(6 /* PolsbyPopper */);
|
|
983
|
+
// Calculate Polsby-Popper scores by district
|
|
984
|
+
let scores = [];
|
|
985
|
+
for (let districtID = 1; districtID <= s.state.nDistricts; districtID++) {
|
|
986
|
+
let districtProps = s.districts.getGeoProperties(districtID);
|
|
987
|
+
let a = districtProps[0 /* Area */];
|
|
988
|
+
let p = districtProps[2 /* Perimeter */];
|
|
989
|
+
let polsbyPopper = (4 * Math.PI) * (a / Math.pow(p, 2));
|
|
990
|
+
// Save each district score
|
|
991
|
+
scores.push(polsbyPopper);
|
|
992
|
+
// Echo the results by district
|
|
993
|
+
if (bLog)
|
|
994
|
+
console.log("Polsby-Popper for district", districtID, "=", polsbyPopper);
|
|
995
|
+
}
|
|
996
|
+
// Populate the test entry
|
|
997
|
+
let averagePolsbyPopper = U.avgArray(scores);
|
|
998
|
+
test['score'] = U.trim(averagePolsbyPopper);
|
|
999
|
+
test['details'] = {}; // TODO - Any details?
|
|
1000
|
+
return test;
|
|
1001
|
+
}
|
|
1002
|
+
exports.doPolsbyPopper = doPolsbyPopper;
|
|
1003
|
+
// HELPER TO EXTRACT PROPERTIES OF DISTRICT SHAPES
|
|
1004
|
+
function extractDistrictProperties(s, bLog = false) {
|
|
1005
|
+
for (let i = 1; i <= s.state.nDistricts; i++) {
|
|
1006
|
+
let j = i - 1; // TODO - Terry: How do you get away w/o this?!?
|
|
1007
|
+
let poly = s.districts.getShape(j);
|
|
1008
|
+
// TODO - Bundle these calls?
|
|
1009
|
+
let area = geofeature_1.gfArea(poly);
|
|
1010
|
+
let perimeter = geofeature_1.gfPerimeter(poly);
|
|
1011
|
+
let diameter = geofeature_1.gfDiameter(poly);
|
|
1012
|
+
let props = [0, 0, 0]; // TODO - Terry?!?
|
|
1013
|
+
props[0 /* Area */] = area;
|
|
1014
|
+
props[1 /* Diameter */] = diameter;
|
|
1015
|
+
props[2 /* Perimeter */] = perimeter;
|
|
1016
|
+
s.districts.setGeoProperties(i, props);
|
|
1017
|
+
if (bLog)
|
|
1018
|
+
console.log("District", i, "A =", area, "P =", perimeter, "D =", diameter);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
exports.extractDistrictProperties = extractDistrictProperties;
|
|
1022
|
+
// SAVE THESE NOTES, IN CASE WE NEED TO REWORK HOW WE PERFORM THESE CALCS.
|
|
1023
|
+
// THEY REFLECT HOW I DID THEM IN PYTHON.
|
|
1024
|
+
//
|
|
1025
|
+
// THE COMPACTNESS ANALYTICS NEED THE FOLLOWING DATA,
|
|
1026
|
+
// IN ADDITION TO THE MAP (IDEALLY, GEO_IDS INDEXED BY DISTRICT_ID)
|
|
1027
|
+
//
|
|
1028
|
+
// Shapes by geo_id
|
|
1029
|
+
//
|
|
1030
|
+
// If we need/want to speed compactness calculations up, we'll need
|
|
1031
|
+
// to calculate the perimeters and diameters (and areas) of districts implicitly,
|
|
1032
|
+
// which will require identifying district boundaries initially and updating
|
|
1033
|
+
// them incrementally as districts are (re)assigned.
|
|
1034
|
+
//
|
|
1035
|
+
// A district's boundary info is the set/list of features that constitute the
|
|
1036
|
+
// district's border along with each boundary feature's neighbors. Hence, this
|
|
1037
|
+
// requires a contiguity graph with the lengths of shared edges between features
|
|
1038
|
+
// precomputed.
|
|
1039
|
+
//
|
|
1040
|
+
// NOTE - I can write up (if not implement) the logic for determining what shapes
|
|
1041
|
+
// constitute a district's boundary. There are a few nuances.
|
|
1042
|
+
//
|
|
1043
|
+
// If we have to optimize like this when we generalize to mixed maps, the
|
|
1044
|
+
// determination of "neighbors in the map" and the length of shared borders
|
|
1045
|
+
// (for determining a district perimeters) becomes more complicated and dynamic.
|
|
1046
|
+
//
|
|
1047
|
+
// NOTE - Again, I can write up (if not implement) the logic for both of these.
|
|
1048
|
+
// They are a bit tricky and require special preprocessing of the summary level
|
|
1049
|
+
// hierarchy (which I also have a script for that we can repurpose).
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
/***/ }),
|
|
1053
|
+
|
|
1054
|
+
/***/ "./src/equal.ts":
|
|
1055
|
+
/*!**********************!*\
|
|
1056
|
+
!*** ./src/equal.ts ***!
|
|
1057
|
+
\**********************/
|
|
1058
|
+
/*! no static exports found */
|
|
1059
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
1060
|
+
|
|
1061
|
+
"use strict";
|
|
1062
|
+
|
|
1063
|
+
//
|
|
1064
|
+
// EQUAL POPULATION
|
|
1065
|
+
//
|
|
1066
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1067
|
+
const U = __webpack_require__(/*! ./utils */ "./src/utils.ts");
|
|
1068
|
+
const D = __webpack_require__(/*! ./_data */ "./src/_data.ts");
|
|
1069
|
+
function doPopulationDeviation(s) {
|
|
1070
|
+
let test = s.getTest(4 /* PopulationDeviation */);
|
|
1071
|
+
// Compute the min, max, and average district populations,
|
|
1072
|
+
// excluding the dummy 'unassigned' 0 and N+1 summary "districts."
|
|
1073
|
+
let totPopByDistrict = s.districts.statistics[D.DistrictField.TotalPop];
|
|
1074
|
+
totPopByDistrict = totPopByDistrict.slice(1, -1);
|
|
1075
|
+
let min = U.minArray(totPopByDistrict);
|
|
1076
|
+
let max = U.maxArray(totPopByDistrict);
|
|
1077
|
+
let total = U.sumArray(totPopByDistrict);
|
|
1078
|
+
// Calculate the raw population deviation.
|
|
1079
|
+
// The target size is the average population.
|
|
1080
|
+
let avg = total / s.state.nDistricts;
|
|
1081
|
+
let popDev = (max - min) / avg;
|
|
1082
|
+
// Round the raw value to the desired level of precision
|
|
1083
|
+
popDev = U.trim(popDev);
|
|
1084
|
+
// Populate the test entry
|
|
1085
|
+
test['score'] = popDev;
|
|
1086
|
+
test['details'] = { 'maxDeviation': max - min };
|
|
1087
|
+
// Populate the N+1 summary "district" in district.statistics
|
|
1088
|
+
let totalPop = s.districts.statistics[D.DistrictField.TotalPop];
|
|
1089
|
+
let popDevPct = s.districts.statistics[D.DistrictField.PopDevPct];
|
|
1090
|
+
let summaryRow = s.districts.numberOfRows() - 1;
|
|
1091
|
+
totalPop[summaryRow] = avg; // aka "target size"
|
|
1092
|
+
popDevPct[summaryRow] = popDev;
|
|
1093
|
+
return test;
|
|
1094
|
+
}
|
|
1095
|
+
exports.doPopulationDeviation = doPopulationDeviation;
|
|
1096
|
+
// NOTE - This validity check is *derived* and depends on population deviation %
|
|
1097
|
+
// being computed (above) and normalized in test log & scorecard generation.
|
|
1098
|
+
function doHasEqualPopulations(s) {
|
|
1099
|
+
let test = s.getTest(3 /* EqualPopulation */);
|
|
1100
|
+
// Get the normalized population deviation %
|
|
1101
|
+
let popDevTest = s.getTest(4 /* PopulationDeviation */);
|
|
1102
|
+
let popDevPct = popDevTest['score'];
|
|
1103
|
+
let popDevNormalized = popDevTest['normalizedScore'];
|
|
1104
|
+
// Populate the test entry
|
|
1105
|
+
if (popDevNormalized > 0) {
|
|
1106
|
+
test['score'] = true;
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
test['score'] = false;
|
|
1110
|
+
}
|
|
1111
|
+
test['details']['deviation'] = popDevPct;
|
|
1112
|
+
test['details']['thresholds'] = popDevTest['details']['scale'];
|
|
1113
|
+
// Populate the N+1 summary "district" in district.statistics
|
|
1114
|
+
let bEqualPop = s.districts.statistics[D.DistrictField.bEqualPop];
|
|
1115
|
+
let summaryRow = s.districts.numberOfRows() - 1;
|
|
1116
|
+
bEqualPop[summaryRow] = test['score'];
|
|
1117
|
+
return test;
|
|
1118
|
+
}
|
|
1119
|
+
exports.doHasEqualPopulations = doHasEqualPopulations;
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
/***/ }),
|
|
1123
|
+
|
|
1124
|
+
/***/ "./src/geofeature.ts":
|
|
1125
|
+
/*!***************************!*\
|
|
1126
|
+
!*** ./src/geofeature.ts ***!
|
|
1127
|
+
\***************************/
|
|
1128
|
+
/*! no static exports found */
|
|
1129
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
1130
|
+
|
|
1131
|
+
"use strict";
|
|
1132
|
+
|
|
1133
|
+
//
|
|
1134
|
+
// GEO-FEATURES UTILITIES
|
|
1135
|
+
//
|
|
1136
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1137
|
+
const Poly = __webpack_require__(/*! @dra2020/poly */ "@dra2020/poly");
|
|
1138
|
+
// CARTESIAN SHIMS OVER 'POLY' FUNCTIONS
|
|
1139
|
+
// TODO - Terry: Confirm Cartesian calculations
|
|
1140
|
+
function gfArea(poly) {
|
|
1141
|
+
let area = _polygonArea(poly);
|
|
1142
|
+
// let polyOptions = { noLatitudeCorrection: false } DELETE
|
|
1143
|
+
// let area: number = Poly.polyArea(poly, polyOptions);
|
|
1144
|
+
return area;
|
|
1145
|
+
}
|
|
1146
|
+
exports.gfArea = gfArea;
|
|
1147
|
+
// Algorithm for the area of a simple/single planar polygon:
|
|
1148
|
+
// https://algorithmtutor.com/Computational-Geometry/Area-of-a-polygon-given-a-set-of-points/
|
|
1149
|
+
// https://mathopenref.com/coordpolygonarea2.html
|
|
1150
|
+
//
|
|
1151
|
+
// function polygonArea(X, Y, numPoints)
|
|
1152
|
+
// {
|
|
1153
|
+
// area = 0; // Accumulates area in the loop
|
|
1154
|
+
// j = numPoints-1; // The last vertex is the 'previous' one to the first
|
|
1155
|
+
// for (i=0; i<numPoints; i++)
|
|
1156
|
+
// { area = area + (X[j]+X[i]) * (Y[j]-Y[i]);
|
|
1157
|
+
// j = i; //j is previous vertex to i
|
|
1158
|
+
// }
|
|
1159
|
+
// return area/2;
|
|
1160
|
+
// }
|
|
1161
|
+
// Reimplemented to use polygons vs. X & Y vectors
|
|
1162
|
+
function _polygonSimpleArea(p) {
|
|
1163
|
+
let area = 0; // Accumulates area in the loop
|
|
1164
|
+
let n = p.length;
|
|
1165
|
+
let j = n - 1; // The last vertex is the 'previous' one to the first
|
|
1166
|
+
for (let i = 0; i < n; i++) {
|
|
1167
|
+
area = area + (p[j][0] + p[i][0]) * (p[j][1] - p[i][1]);
|
|
1168
|
+
// area = area + (X[j] + X[i]) * (Y[j] - Y[i]);
|
|
1169
|
+
j = i; // j is previous vertex to i
|
|
1170
|
+
}
|
|
1171
|
+
return Math.abs(area / 2);
|
|
1172
|
+
}
|
|
1173
|
+
// Generalizes the above for MultiPolygons -- cloned from polyArea() in 'poly'
|
|
1174
|
+
function _polygonArea(poly) {
|
|
1175
|
+
let polyOptions = { noLatitudeCorrection: true }; // NO-OP?
|
|
1176
|
+
poly = Poly.polyNormalize(poly, polyOptions); // TODO - Discuss w/ Terry
|
|
1177
|
+
let a = 0;
|
|
1178
|
+
// A MultiPolygon is a set of polygons
|
|
1179
|
+
for (let i = 0; poly && i < poly.length; i++) {
|
|
1180
|
+
// A single polygon is an exterior ring with interior holes. Holes are subtracted.
|
|
1181
|
+
let p = poly[i];
|
|
1182
|
+
for (let j = 0; j < p.length; j++) {
|
|
1183
|
+
let sp = p[j];
|
|
1184
|
+
a += _polygonSimpleArea(sp) * (j == 0 ? 1 : -1);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return a;
|
|
1188
|
+
}
|
|
1189
|
+
// TODO - Terry: Confirm Cartesian calculations
|
|
1190
|
+
// The perimeter calculation already just computes cartesian distance if you
|
|
1191
|
+
// pass in the noLatitudeCorrection flag. You would need to divide by
|
|
1192
|
+
// Poly.EARTH_RADIUS to go from the returned units of meters to Lat/Lon “units”.
|
|
1193
|
+
function gfPerimeter(poly) {
|
|
1194
|
+
let perimeter = _polygonPerimeter(poly);
|
|
1195
|
+
// let polyOptions = { noLatitudeCorrection: true } // Cartesian distance
|
|
1196
|
+
// let perimeter: number = Poly.polyPerimeter(poly, polyOptions); DELETE
|
|
1197
|
+
return perimeter;
|
|
1198
|
+
// return perimeter / Poly.EARTH_RADIUS; DELETE
|
|
1199
|
+
}
|
|
1200
|
+
exports.gfPerimeter = gfPerimeter;
|
|
1201
|
+
// TODO - Terry: Confirm Cartesian calculations
|
|
1202
|
+
// Cloned from polyPerimeter() in 'poly' and revised to use Cartesian distance
|
|
1203
|
+
// NOTE: No conversion of degrees to radians!
|
|
1204
|
+
function _polygonPerimeter(poly) {
|
|
1205
|
+
let polyOptions = { noLatitudeCorrection: true }; // Cartesian distance
|
|
1206
|
+
poly = Poly.polyNormalize(poly, polyOptions);
|
|
1207
|
+
let perimeter = 0;
|
|
1208
|
+
for (let i = 0; poly && i < poly.length; i++) {
|
|
1209
|
+
// Ignore holes so only look at first polyline
|
|
1210
|
+
let p = poly[i][0];
|
|
1211
|
+
for (let j = 0; j < p.length - 1; j++)
|
|
1212
|
+
perimeter += _distance(p[j][0], p[j][1], p[j + 1][0], p[j + 1][1]);
|
|
1213
|
+
// perimeter += haversine(p[j][0], p[j][1], p[j + 1][0], p[j + 1][1], options); DELETE
|
|
1214
|
+
if (p.length > 2 && (p[0][0] != p[p.length - 1][0] || p[0][1] != p[p.length - 1][1]))
|
|
1215
|
+
perimeter += _distance(p[0][0], p[0][1], p[p.length - 1][0], p[p.length - 1][1]);
|
|
1216
|
+
// perimeter += haversine(p[0][0], p[0][1], p[p.length - 1][0], p[p.length - 1][1], options); DELETE
|
|
1217
|
+
}
|
|
1218
|
+
return perimeter;
|
|
1219
|
+
}
|
|
1220
|
+
function _distance(x1, y1, x2, y2) {
|
|
1221
|
+
let dLat = y2 - y1;
|
|
1222
|
+
let dLon = x2 - x1;
|
|
1223
|
+
let d;
|
|
1224
|
+
d = Math.sqrt((dLat * dLat) + (dLon * dLon));
|
|
1225
|
+
return d;
|
|
1226
|
+
}
|
|
1227
|
+
// TODO - Terry: Confirm Cartesian calculations
|
|
1228
|
+
// As I mentioned, the polyCircle code was already just treating the coordinate
|
|
1229
|
+
// system as Cartesian. I then did polyFromCircle to convert it to a polygon that
|
|
1230
|
+
// then could be passed to polyArea in order to take into account the projection.
|
|
1231
|
+
// If instead, you compute area directly from the circle as PI R squared, then
|
|
1232
|
+
// you should have your cartesian circular area.
|
|
1233
|
+
function gfDiameter(poly) {
|
|
1234
|
+
let polyOptions = { noLatitudeCorrection: true }; // NO-OP
|
|
1235
|
+
let circle = Poly.polyToCircle(poly, polyOptions);
|
|
1236
|
+
// let circleArea: number = Poly.polyArea(Poly.polyFromCircle(circle, undefined, polyOptions), polyOptions);
|
|
1237
|
+
// let circleRadius: number = Math.sqrt(circleArea / Math.PI);
|
|
1238
|
+
// let diameter: number = circleRadius * 2; DELETE
|
|
1239
|
+
let diameter = circle.r * 2;
|
|
1240
|
+
return diameter;
|
|
1241
|
+
}
|
|
1242
|
+
exports.gfDiameter = gfDiameter;
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
/***/ }),
|
|
1246
|
+
|
|
1247
|
+
/***/ "./src/index.ts":
|
|
1248
|
+
/*!**********************!*\
|
|
1249
|
+
!*** ./src/index.ts ***!
|
|
1250
|
+
\**********************/
|
|
1251
|
+
/*! no static exports found */
|
|
1252
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
1253
|
+
|
|
1254
|
+
"use strict";
|
|
1255
|
+
|
|
1256
|
+
//
|
|
1257
|
+
// THE DISTRICT-ANALYTICS NODE PACKAGE API
|
|
1258
|
+
//
|
|
1259
|
+
function __export(m) {
|
|
1260
|
+
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
|
1261
|
+
}
|
|
1262
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1263
|
+
__export(__webpack_require__(/*! ./_api */ "./src/_api.ts"));
|
|
1264
|
+
__export(__webpack_require__(/*! ./types */ "./src/types.ts"));
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
/***/ }),
|
|
1268
|
+
|
|
1269
|
+
/***/ "./src/minority.ts":
|
|
1270
|
+
/*!*************************!*\
|
|
1271
|
+
!*** ./src/minority.ts ***!
|
|
1272
|
+
\*************************/
|
|
1273
|
+
/*! no static exports found */
|
|
1274
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
1275
|
+
|
|
1276
|
+
"use strict";
|
|
1277
|
+
|
|
1278
|
+
//
|
|
1279
|
+
// PROTECTS MINORITIES
|
|
1280
|
+
//
|
|
1281
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1282
|
+
// TODO - This definition is wrong. Need to fix it.
|
|
1283
|
+
//
|
|
1284
|
+
// MINORITY-PROTECTION ANALYTICS NEED THE FOLLOWING DATA:
|
|
1285
|
+
//
|
|
1286
|
+
// The TOTAL, WHITE, BLACK, and HISPANIC counts from the Census, aggregated by
|
|
1287
|
+
// district. We *might* also ultimately need TOTAL18, WHITE18, BLACK18, and
|
|
1288
|
+
// HISPANIC18 counts by feature for these analytics.
|
|
1289
|
+
//
|
|
1290
|
+
// The minority population of a feature will probably be calculated as everyone
|
|
1291
|
+
// exceot non-Hispanic Whites:
|
|
1292
|
+
//
|
|
1293
|
+
// MINORITY = TOTAL - (WHITE - HISPANIC)
|
|
1294
|
+
//
|
|
1295
|
+
// That could be calculated as part of preprocessing the Census data, or it
|
|
1296
|
+
// could be computed on the fly. Since it's derived data and the formula might
|
|
1297
|
+
// change, it's probably best to compute it on the fly.
|
|
1298
|
+
//
|
|
1299
|
+
// In addition to the Census extract, these analytics need:
|
|
1300
|
+
// - The # of districts in the map, for determining minority proportionality
|
|
1301
|
+
// - Minorities as a % of the total population; possibly the voting age share
|
|
1302
|
+
//
|
|
1303
|
+
// TODO - Is the # of districts passed as a parameter or inferred from the # in
|
|
1304
|
+
// the map?
|
|
1305
|
+
// TODO - Is minority share preprocessed once and passed as a parameter or
|
|
1306
|
+
// computed in a initialization routine?
|
|
1307
|
+
function doMajorityMinorityDistricts(s) {
|
|
1308
|
+
let test = s.getTest(14 /* MajorityMinorityDistricts */);
|
|
1309
|
+
console.log("TODO - Calculating # of majority-minority districts ...");
|
|
1310
|
+
return test;
|
|
1311
|
+
}
|
|
1312
|
+
exports.doMajorityMinorityDistricts = doMajorityMinorityDistricts;
|
|
1313
|
+
// SAVE THESE NOTES, IN CASE WE NEED TO REWORK HOW WE DO PERFORM THESE CALCS.
|
|
1314
|
+
// THEY REFLECT HOW I/ALEC DID THESE IN PYTHON.
|
|
1315
|
+
//
|
|
1316
|
+
// MINORITY-PROTECTION ANALYTICS WILL NEED THE FOLLOWING DATA,
|
|
1317
|
+
// IN ADDITION TO THE MAP (IDEALLY, GEO_IDS INDEXED BY DISTRICT_ID)
|
|
1318
|
+
//
|
|
1319
|
+
// Census data by geo_id - { total population | white | black | hispanic }
|
|
1320
|
+
//
|
|
1321
|
+
// The minority population of a feature will probably be calculated as the # of
|
|
1322
|
+
// non-White Hispanics:
|
|
1323
|
+
//
|
|
1324
|
+
// MINORITY = TOTAL - (WHITE - HISPANIC)
|
|
1325
|
+
//
|
|
1326
|
+
// That could be calculated as part of preprocessing the Census data, or it
|
|
1327
|
+
// could be computed on the fly.
|
|
1328
|
+
//
|
|
1329
|
+
// And probably:
|
|
1330
|
+
// 'districts' - The # of districts for determining minority proportionality.
|
|
1331
|
+
// 'minority_share' - Minorities as a % of the total population
|
|
1332
|
+
//
|
|
1333
|
+
// TODO - Is the # of districts passed as a parameter or inferred from the # in
|
|
1334
|
+
// the map?
|
|
1335
|
+
// TODO - Is minority share preprocessed once and passed as a parameter or
|
|
1336
|
+
// computed in a initialization routine?
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
/***/ }),
|
|
1340
|
+
|
|
1341
|
+
/***/ "./src/political.ts":
|
|
1342
|
+
/*!**************************!*\
|
|
1343
|
+
!*** ./src/political.ts ***!
|
|
1344
|
+
\**************************/
|
|
1345
|
+
/*! no static exports found */
|
|
1346
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
1347
|
+
|
|
1348
|
+
"use strict";
|
|
1349
|
+
|
|
1350
|
+
//
|
|
1351
|
+
// FAIR/PROPORTIONAL
|
|
1352
|
+
//
|
|
1353
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1354
|
+
const assert_1 = __webpack_require__(/*! assert */ "assert");
|
|
1355
|
+
const U = __webpack_require__(/*! ./utils */ "./src/utils.ts");
|
|
1356
|
+
const D = __webpack_require__(/*! ./_data */ "./src/_data.ts");
|
|
1357
|
+
// Partisan analytics need the following data:
|
|
1358
|
+
//
|
|
1359
|
+
// An "election model" by geo_id, where each item has 4 pieces of data:
|
|
1360
|
+
//
|
|
1361
|
+
// { geo_id, Democratic votes, Republican votes, Total votes }
|
|
1362
|
+
//
|
|
1363
|
+
// NOTE: D + R <= Total, because there could be third-party or write-in votes.
|
|
1364
|
+
//
|
|
1365
|
+
// An election model can simply represent one election, e.g., President 2012,
|
|
1366
|
+
// or combine multiple elections in some fashion. An election model is used to
|
|
1367
|
+
// computer a single index of the likely partisan weight / lean / preference
|
|
1368
|
+
// for the districts in a plan. An election model and the associated index go
|
|
1369
|
+
// hand in hand. So much so that the index frequently stands in as the name for
|
|
1370
|
+
// both, e.g., Cook's PVI is one example, Nagle's 7s, Hofeller's Formula, NDRC's
|
|
1371
|
+
// DPI.
|
|
1372
|
+
//
|
|
1373
|
+
// I'm labelling this general concept a "Voter Preference Index (VPI)," a
|
|
1374
|
+
// conscious +1 letter play on Cook's "PVI" acronymn.
|
|
1375
|
+
// MEASURING BIAS & RESPONSIVENESS (NAGLE'S METHOD)
|
|
1376
|
+
function doSeatsBias(s) {
|
|
1377
|
+
let test = s.getTest(9 /* SeatsBias */);
|
|
1378
|
+
console.log("TODO - Calculating seats bias ...");
|
|
1379
|
+
return test;
|
|
1380
|
+
}
|
|
1381
|
+
exports.doSeatsBias = doSeatsBias;
|
|
1382
|
+
function doVotesBias(s) {
|
|
1383
|
+
let test = s.getTest(10 /* VotesBias */);
|
|
1384
|
+
console.log("TODO - Calculating votes bias ...");
|
|
1385
|
+
return test;
|
|
1386
|
+
}
|
|
1387
|
+
exports.doVotesBias = doVotesBias;
|
|
1388
|
+
function doResponsiveness(s) {
|
|
1389
|
+
let test = s.getTest(11 /* Responsiveness */);
|
|
1390
|
+
console.log("TODO - Calculating responsiveness ...");
|
|
1391
|
+
return test;
|
|
1392
|
+
}
|
|
1393
|
+
exports.doResponsiveness = doResponsiveness;
|
|
1394
|
+
function doResponsiveDistricts(s) {
|
|
1395
|
+
let test = s.getTest(12 /* ResponsiveDistricts */);
|
|
1396
|
+
console.log("TODO - Calculating # of responsive districts ...");
|
|
1397
|
+
return test;
|
|
1398
|
+
}
|
|
1399
|
+
exports.doResponsiveDistricts = doResponsiveDistricts;
|
|
1400
|
+
// OTHER MEASURES OF PARTISAN BIAS
|
|
1401
|
+
// TODO - This formula might need to be inverted for D vs. R +/-
|
|
1402
|
+
// TODO - Normalize the results.
|
|
1403
|
+
function doEfficiencyGap(s) {
|
|
1404
|
+
console.log("TODO - Calculating the efficiency gap ...");
|
|
1405
|
+
let test = s.getTest(13 /* EfficiencyGap */);
|
|
1406
|
+
// Get partisan statistics by districts.
|
|
1407
|
+
// Use Democratic votes, seats, and shares by convention.
|
|
1408
|
+
let DVotes = s.districts.statistics[D.DistrictField.DemVotes];
|
|
1409
|
+
let DSeats = s.districts.statistics[D.DistrictField.DemSeat];
|
|
1410
|
+
let TPVotes = s.districts.statistics[D.DistrictField.TwoPartyVote];
|
|
1411
|
+
// Exclude the dummy unassigned '0' and N+1 summary "districts"
|
|
1412
|
+
DVotes = DVotes.slice(1, -1);
|
|
1413
|
+
DSeats = DSeats.slice(1, -1);
|
|
1414
|
+
TPVotes = TPVotes.slice(1, -1);
|
|
1415
|
+
// Calculate D vote share & D seat share
|
|
1416
|
+
let DVoteShare = U.sumArray(DVotes) / U.sumArray(TPVotes);
|
|
1417
|
+
let DSeatShare = U.sumArray(DSeats) / s.state.nDistricts;
|
|
1418
|
+
// Finally, calculate the Efficiency Gap
|
|
1419
|
+
let efficiencyGap = (DSeatShare - 0.5) - (2.0 * (DVoteShare - 0.5));
|
|
1420
|
+
// Round the raw value to the desired level of precision
|
|
1421
|
+
efficiencyGap = U.trim(efficiencyGap);
|
|
1422
|
+
// Populate the test entry
|
|
1423
|
+
test['score'] = efficiencyGap;
|
|
1424
|
+
// test['normalizedScore'] = 0; /* TODO - Normalize the raw score */
|
|
1425
|
+
test['details'] = {}; /* TODO - Add details, if any */
|
|
1426
|
+
return test;
|
|
1427
|
+
}
|
|
1428
|
+
exports.doEfficiencyGap = doEfficiencyGap;
|
|
1429
|
+
// HELPERS
|
|
1430
|
+
function fptpWin(demPct) {
|
|
1431
|
+
// Vote shares should be fractions in the range [0.0 – 1.0]
|
|
1432
|
+
assert_1.strict((demPct <= 1.0) && (demPct >= 0.));
|
|
1433
|
+
return ((demPct > 0.5) ? 1 : 0);
|
|
1434
|
+
}
|
|
1435
|
+
exports.fptpWin = fptpWin;
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
/***/ }),
|
|
1439
|
+
|
|
1440
|
+
/***/ "./src/preprocess.ts":
|
|
1441
|
+
/*!***************************!*\
|
|
1442
|
+
!*** ./src/preprocess.ts ***!
|
|
1443
|
+
\***************************/
|
|
1444
|
+
/*! no static exports found */
|
|
1445
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
1446
|
+
|
|
1447
|
+
"use strict";
|
|
1448
|
+
|
|
1449
|
+
//
|
|
1450
|
+
// PREPROCESS DATA
|
|
1451
|
+
//
|
|
1452
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1453
|
+
const U = __webpack_require__(/*! ./utils */ "./src/utils.ts");
|
|
1454
|
+
// NOTE - Do preprocessing separately, so the constructor returns quickly.
|
|
1455
|
+
function doPreprocessData(s) {
|
|
1456
|
+
// If necessary, do one-time preprocessing
|
|
1457
|
+
if (!s.bOneTimeProcessingDone) {
|
|
1458
|
+
doPreprocessCountyFeatures(s);
|
|
1459
|
+
doPreprocessCensus(s);
|
|
1460
|
+
doPreprocessElection(s);
|
|
1461
|
+
s.bOneTimeProcessingDone = true;
|
|
1462
|
+
}
|
|
1463
|
+
// Invert the plan by district ID
|
|
1464
|
+
s.plan.invertPlan();
|
|
1465
|
+
// Create a map of geoIDs to feature IDs
|
|
1466
|
+
s.features.mapGeoIDsToFeatureIDs();
|
|
1467
|
+
}
|
|
1468
|
+
exports.doPreprocessData = doPreprocessData;
|
|
1469
|
+
// CREATE A FIPS CODE TO COUNTY NAME LOOKUP
|
|
1470
|
+
function doPreprocessCountyFeatures(s) {
|
|
1471
|
+
for (let i = 0; i < s.counties.nCounties; i++) {
|
|
1472
|
+
let county = s.counties.countyByIndex(i);
|
|
1473
|
+
let fips = s.counties.propertyForCounty(county, 'COUNTYFP');
|
|
1474
|
+
let name = s.counties.propertyForCounty(county, 'NAME');
|
|
1475
|
+
s.counties.mapFIPSToName(fips, name);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
// ANALYZE THE CENSUS BY COUNTY
|
|
1479
|
+
function doPreprocessCensus(s) {
|
|
1480
|
+
// The county-splitting analytic needs the following info, using NC as an example:
|
|
1481
|
+
// '_stateTotal' = The total state population, e.g., 9,535,483 for NC's 2010 Census
|
|
1482
|
+
// 'totalByCounty' = The total population by county FIPS code
|
|
1483
|
+
let totalByCounty = {};
|
|
1484
|
+
// NOTE - This works w/o GEOIDs, because you're looping over all features.
|
|
1485
|
+
for (let i = 0; i < s.features.nFeatures(); i++) {
|
|
1486
|
+
let f = s.features.featureByIndex(i);
|
|
1487
|
+
let geoID = s.features.geoIDForFeature(f);
|
|
1488
|
+
let value = s.features.fieldForFeature(f, "CENSUS" /* CENSUS */, "Tot" /* TotalPop */);
|
|
1489
|
+
// Sum total population across the state
|
|
1490
|
+
s.state.totalPop += value;
|
|
1491
|
+
// Get the county FIPS code for the feature
|
|
1492
|
+
let county = U.parseGeoID(geoID)['county'];
|
|
1493
|
+
let countyFIPS = U.getFIPSFromCountyGeoID(county);
|
|
1494
|
+
// If a subtotal for the county doesn't exist, initialize one
|
|
1495
|
+
if (!(U.keyExists(countyFIPS, totalByCounty))) {
|
|
1496
|
+
totalByCounty[countyFIPS] = 0;
|
|
1497
|
+
}
|
|
1498
|
+
// Sum total population by county
|
|
1499
|
+
totalByCounty[countyFIPS] += value;
|
|
1500
|
+
}
|
|
1501
|
+
// NOTE - The above could be replaced, if I got totals on county.geojson.
|
|
1502
|
+
// 'target_size': 733499, # calc as total / districts
|
|
1503
|
+
let targetSize = Math.round(s.state.totalPop / s.state.nDistricts);
|
|
1504
|
+
// Find counties that are bigger than the target district size.
|
|
1505
|
+
// 'too_big' = The counties that are bigger than a district, e.g., ['Mecklenburg', 'Wake']
|
|
1506
|
+
// 'too_big_fips' = Their FIPS codes, e.g., ['119', '183']
|
|
1507
|
+
// 'expected_splits' = The # of counties that are bigger than a single district, e.g., 2
|
|
1508
|
+
// 'expected_affected' = The # of people whose district must be split, e.g., 353,623
|
|
1509
|
+
let tooBigFIPS = [];
|
|
1510
|
+
let tooBigName = [];
|
|
1511
|
+
let expectedSplits = 0;
|
|
1512
|
+
let expectedAffected = 0;
|
|
1513
|
+
// Create a FIPS code-ordinal map
|
|
1514
|
+
// Get the county FIPS codes
|
|
1515
|
+
let fipsCodes = U.getObjectKeys(totalByCounty);
|
|
1516
|
+
// Sort the results
|
|
1517
|
+
fipsCodes = fipsCodes.sort();
|
|
1518
|
+
// Create the ID-ordinal map
|
|
1519
|
+
for (let i in fipsCodes) {
|
|
1520
|
+
s.counties.index[fipsCodes[i]] = Number(i);
|
|
1521
|
+
}
|
|
1522
|
+
// Loop over the counties
|
|
1523
|
+
for (let county in fipsCodes) {
|
|
1524
|
+
let fipsCode = fipsCodes[county];
|
|
1525
|
+
let countyAffected = 0;
|
|
1526
|
+
// Find the number of required splits, assuming target district size.
|
|
1527
|
+
let rawQuotient = totalByCounty[fipsCode] / (targetSize + 1);
|
|
1528
|
+
let remainder = rawQuotient % 1;
|
|
1529
|
+
let quotient = rawQuotient - remainder;
|
|
1530
|
+
let countySplits = quotient;
|
|
1531
|
+
if (countySplits > 0) {
|
|
1532
|
+
countyAffected = totalByCounty[fipsCode] % targetSize;
|
|
1533
|
+
tooBigFIPS.push(fipsCode);
|
|
1534
|
+
tooBigName.push(s.counties.nameFromFIPS(fipsCode));
|
|
1535
|
+
}
|
|
1536
|
+
expectedSplits += countySplits;
|
|
1537
|
+
expectedAffected += countyAffected;
|
|
1538
|
+
}
|
|
1539
|
+
s.state.tooBigFIPS = tooBigFIPS;
|
|
1540
|
+
s.state.tooBigName = tooBigName;
|
|
1541
|
+
s.state.expectedSplits = expectedSplits;
|
|
1542
|
+
s.state.expectedAffected = expectedAffected;
|
|
1543
|
+
}
|
|
1544
|
+
// PREPROCESS ELECTION RESULTS
|
|
1545
|
+
function doPreprocessElection(s) {
|
|
1546
|
+
console.log("TODO - Preprocessing election data ...");
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
/***/ }),
|
|
1551
|
+
|
|
1552
|
+
/***/ "./src/report.ts":
|
|
1553
|
+
/*!***********************!*\
|
|
1554
|
+
!*** ./src/report.ts ***!
|
|
1555
|
+
\***********************/
|
|
1556
|
+
/*! no static exports found */
|
|
1557
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
1558
|
+
|
|
1559
|
+
"use strict";
|
|
1560
|
+
|
|
1561
|
+
//
|
|
1562
|
+
// GENERATE REPORTS
|
|
1563
|
+
// - A test log: a simple enumeration of all analytics & validations w/ raw results
|
|
1564
|
+
// - A scorecard: a structured subset of analytics & validations w/ normalized
|
|
1565
|
+
// results, cateories, and an overall score
|
|
1566
|
+
//
|
|
1567
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1568
|
+
const U = __webpack_require__(/*! ./utils */ "./src/utils.ts");
|
|
1569
|
+
const S = __webpack_require__(/*! ./settings */ "./src/settings.ts");
|
|
1570
|
+
const D = __webpack_require__(/*! ./_data */ "./src/_data.ts");
|
|
1571
|
+
const analyze_1 = __webpack_require__(/*! ./analyze */ "./src/analyze.ts");
|
|
1572
|
+
// TEST META-DATA
|
|
1573
|
+
var TestType;
|
|
1574
|
+
(function (TestType) {
|
|
1575
|
+
TestType[TestType["PassFail"] = 0] = "PassFail";
|
|
1576
|
+
TestType[TestType["Percentage"] = 1] = "Percentage";
|
|
1577
|
+
TestType[TestType["Number"] = 2] = "Number";
|
|
1578
|
+
})(TestType || (TestType = {}));
|
|
1579
|
+
const completeDefn = {
|
|
1580
|
+
ID: 0 /* Complete */,
|
|
1581
|
+
name: "Complete",
|
|
1582
|
+
normalize: false,
|
|
1583
|
+
externalType: TestType.PassFail,
|
|
1584
|
+
detailsFn: doPrepareCompleteDetails,
|
|
1585
|
+
suites: [0 /* Legal */]
|
|
1586
|
+
};
|
|
1587
|
+
const contiguousDefn = {
|
|
1588
|
+
ID: 1 /* Contiguous */,
|
|
1589
|
+
name: "Contiguous",
|
|
1590
|
+
normalize: false,
|
|
1591
|
+
externalType: TestType.PassFail,
|
|
1592
|
+
detailsFn: doPrepareContiguousDetails,
|
|
1593
|
+
suites: [0 /* Legal */]
|
|
1594
|
+
};
|
|
1595
|
+
const freeOfHolesDefn = {
|
|
1596
|
+
ID: 2 /* FreeOfHoles */,
|
|
1597
|
+
name: "Free of Holes",
|
|
1598
|
+
normalize: false,
|
|
1599
|
+
externalType: TestType.PassFail,
|
|
1600
|
+
detailsFn: doPrepareFreeOfHolesDetails,
|
|
1601
|
+
suites: [0 /* Legal */]
|
|
1602
|
+
};
|
|
1603
|
+
const equalPopulationDefn = {
|
|
1604
|
+
ID: 3 /* EqualPopulation */,
|
|
1605
|
+
name: "Equal Population",
|
|
1606
|
+
normalize: false,
|
|
1607
|
+
externalType: TestType.PassFail,
|
|
1608
|
+
detailsFn: doPrepareEqualPopulationDetails,
|
|
1609
|
+
suites: [0 /* Legal */]
|
|
1610
|
+
};
|
|
1611
|
+
const populationDeviationDefn = {
|
|
1612
|
+
ID: 4 /* PopulationDeviation */,
|
|
1613
|
+
name: "Population Deviation",
|
|
1614
|
+
normalize: true,
|
|
1615
|
+
externalType: TestType.Percentage,
|
|
1616
|
+
detailsFn: doPreparePopulationDeviationDetails,
|
|
1617
|
+
suites: [0 /* Legal */, 2 /* Best */] // Both so EqualPopulation can be assessed
|
|
1618
|
+
};
|
|
1619
|
+
const reockDefn = {
|
|
1620
|
+
ID: 5 /* Reock */,
|
|
1621
|
+
name: "Reock",
|
|
1622
|
+
normalize: true,
|
|
1623
|
+
externalType: TestType.Number,
|
|
1624
|
+
detailsFn: doPrepareReockDetails,
|
|
1625
|
+
suites: [2 /* Best */]
|
|
1626
|
+
};
|
|
1627
|
+
const polsbyPopperDefn = {
|
|
1628
|
+
ID: 6 /* PolsbyPopper */,
|
|
1629
|
+
name: "Polsby-Popper",
|
|
1630
|
+
normalize: true,
|
|
1631
|
+
externalType: TestType.Number,
|
|
1632
|
+
detailsFn: doPreparePolsbyPopperDetails,
|
|
1633
|
+
suites: [2 /* Best */]
|
|
1634
|
+
};
|
|
1635
|
+
const countySplitsDefn = {
|
|
1636
|
+
ID: 7 /* CountySplits */,
|
|
1637
|
+
name: "County splits",
|
|
1638
|
+
trailer: "of the population had their county split unexpectedly.",
|
|
1639
|
+
normalize: true,
|
|
1640
|
+
externalType: TestType.Percentage,
|
|
1641
|
+
detailsFn: doPrepareCountySplitDetails,
|
|
1642
|
+
suites: [2 /* Best */]
|
|
1643
|
+
};
|
|
1644
|
+
const efficiencyGapDefn = {
|
|
1645
|
+
ID: 13 /* EfficiencyGap */,
|
|
1646
|
+
name: "Efficiency Gap",
|
|
1647
|
+
normalize: false,
|
|
1648
|
+
externalType: TestType.Percentage,
|
|
1649
|
+
detailsFn: doPrepareEfficiencyGapDetails,
|
|
1650
|
+
suites: [1 /* Fair */]
|
|
1651
|
+
};
|
|
1652
|
+
// All the tests that have been defined (can be reported on)
|
|
1653
|
+
const testDefns = {
|
|
1654
|
+
[0 /* Complete */]: completeDefn,
|
|
1655
|
+
[1 /* Contiguous */]: contiguousDefn,
|
|
1656
|
+
[2 /* FreeOfHoles */]: freeOfHolesDefn,
|
|
1657
|
+
[3 /* EqualPopulation */]: equalPopulationDefn,
|
|
1658
|
+
[4 /* PopulationDeviation */]: populationDeviationDefn,
|
|
1659
|
+
[5 /* Reock */]: reockDefn,
|
|
1660
|
+
[6 /* PolsbyPopper */]: polsbyPopperDefn,
|
|
1661
|
+
[7 /* CountySplits */]: countySplitsDefn,
|
|
1662
|
+
[13 /* EfficiencyGap */]: efficiencyGapDefn
|
|
1663
|
+
/* TODO - More tests ... */
|
|
1664
|
+
};
|
|
1665
|
+
// Scorecard category definitions
|
|
1666
|
+
const validCategory = {
|
|
1667
|
+
catName: "Valid",
|
|
1668
|
+
catTests: [
|
|
1669
|
+
{ testID: 0 /* Complete */ },
|
|
1670
|
+
{ testID: 1 /* Contiguous */ },
|
|
1671
|
+
{ testID: 2 /* FreeOfHoles */ },
|
|
1672
|
+
{ testID: 3 /* EqualPopulation */ }
|
|
1673
|
+
],
|
|
1674
|
+
catNumeric: false,
|
|
1675
|
+
catWeight: undefined,
|
|
1676
|
+
catPrepareFn: doPrepareValidSection
|
|
1677
|
+
};
|
|
1678
|
+
const fairCategory = {
|
|
1679
|
+
catName: "Fair",
|
|
1680
|
+
catTests: [
|
|
1681
|
+
{
|
|
1682
|
+
testID: 13 /* EfficiencyGap */,
|
|
1683
|
+
testWeight: 100
|
|
1684
|
+
}
|
|
1685
|
+
],
|
|
1686
|
+
catNumeric: true,
|
|
1687
|
+
catWeight: undefined,
|
|
1688
|
+
catPrepareFn: doPrepareFairSection
|
|
1689
|
+
};
|
|
1690
|
+
// TODO - Decide on the relative weights of these tests!
|
|
1691
|
+
// NOTE: 'testWeights' are simply relative, i.e., each normalized score is
|
|
1692
|
+
// multiplied by the associated 'testWeight', and the sum of those is divided
|
|
1693
|
+
// by the total weight. Weights don't have to add to 100.
|
|
1694
|
+
const bestCategory = {
|
|
1695
|
+
catName: "Best",
|
|
1696
|
+
catTests: [
|
|
1697
|
+
{
|
|
1698
|
+
testID: 4 /* PopulationDeviation */,
|
|
1699
|
+
testWeight: 10
|
|
1700
|
+
},
|
|
1701
|
+
{
|
|
1702
|
+
testID: 5 /* Reock */,
|
|
1703
|
+
testWeight: 25
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
testID: 6 /* PolsbyPopper */,
|
|
1707
|
+
testWeight: 25
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
testID: 7 /* CountySplits */,
|
|
1711
|
+
testWeight: 50
|
|
1712
|
+
}
|
|
1713
|
+
],
|
|
1714
|
+
catNumeric: true,
|
|
1715
|
+
catWeight: undefined,
|
|
1716
|
+
catPrepareFn: doPrepareBestSection
|
|
1717
|
+
};
|
|
1718
|
+
// The overall scorecard definition
|
|
1719
|
+
const scorecardDefn = {
|
|
1720
|
+
[0 /* Legal */]: validCategory,
|
|
1721
|
+
// TODO - NIY
|
|
1722
|
+
// [T.Suite.Fair]: fairCategory,
|
|
1723
|
+
[2 /* Best */]: bestCategory
|
|
1724
|
+
};
|
|
1725
|
+
// NORMALIZE RAW ANALYTICS
|
|
1726
|
+
// Raw numeric analytics, such as population deviation, compactness, etc. are
|
|
1727
|
+
// normalized as part of creating a scorecard, so the code to normalize results
|
|
1728
|
+
// is encapsulated here.
|
|
1729
|
+
// Configure scale parameters for normalizing each raw test result
|
|
1730
|
+
// This needs to be separate from the scorecard configuration info above,
|
|
1731
|
+
// because some scales need access to the analytics session object.
|
|
1732
|
+
function doConfigureScales(s) {
|
|
1733
|
+
// Scale defn for PopulationDeviation
|
|
1734
|
+
const CDLimit = 0.75 / 100; // Deviation threshold for CD's
|
|
1735
|
+
const LDLimit = 10.00 / 100; // Deviation threshold for LD's
|
|
1736
|
+
const CDGoodEnough = 0.20 / 100;
|
|
1737
|
+
const LDGoodEnough = (CDGoodEnough / CDLimit) * LDLimit;
|
|
1738
|
+
const scale = (s.legislativeDistricts) ? [1.0 - LDLimit, 1.0 - LDGoodEnough] : [1.0 - CDLimit, 1.0 - CDGoodEnough];
|
|
1739
|
+
// const scale = [1.0 - CDLimit, 1.0 - CDGoodEnough];
|
|
1740
|
+
s.testScales[4 /* PopulationDeviation */] = { testScale: scale, testInvertp: true };
|
|
1741
|
+
s.testScales[5 /* Reock */] = { testScale: [0.25, 0.50], testInvertp: false };
|
|
1742
|
+
s.testScales[6 /* PolsbyPopper */] = { testScale: [0.10, 0.50], testInvertp: false };
|
|
1743
|
+
const SPLITLimit = 0.1; // TODO - Just a placeholder default maximum (10%)
|
|
1744
|
+
s.testScales[7 /* CountySplits */] = { testScale: [1.0 - SPLITLimit, 1.0], testInvertp: true };
|
|
1745
|
+
// TODO - More analytics ...
|
|
1746
|
+
}
|
|
1747
|
+
exports.doConfigureScales = doConfigureScales;
|
|
1748
|
+
// Postprocess analytics - Normalize numeric results and derive secondary tests.
|
|
1749
|
+
// Do this after analytics have been run and before preparing a test log or scorecard.
|
|
1750
|
+
function doAnalyzePostProcessing(s) {
|
|
1751
|
+
// Normalize the raw scores for all the numerics tests
|
|
1752
|
+
let testResults = U.getNumericObjectKeys(testDefns);
|
|
1753
|
+
for (let testID of testResults) {
|
|
1754
|
+
if (testDefns[testID]['normalize']) {
|
|
1755
|
+
let testResult = s.getTest(testID);
|
|
1756
|
+
let rawScore = testResult['score'];
|
|
1757
|
+
let normalizedScore;
|
|
1758
|
+
let { testScale, testInvertp } = s.testScales[testID];
|
|
1759
|
+
normalizedScore = U.normalize(rawScore, testScale, testInvertp);
|
|
1760
|
+
testResult['normalizedScore'] = normalizedScore;
|
|
1761
|
+
// Add the scale used to normalize the raw score to the details
|
|
1762
|
+
testResult['details']['scale'] = testScale;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
// Derive secondary tests
|
|
1766
|
+
analyze_1.doDeriveSecondaryTests(s);
|
|
1767
|
+
// Toggle the semaphore, so postprocessing isn't for both the testlog & scorecard
|
|
1768
|
+
s.bPostProcessingDone = true;
|
|
1769
|
+
}
|
|
1770
|
+
exports.doAnalyzePostProcessing = doAnalyzePostProcessing;
|
|
1771
|
+
// Prepare a structured but unformatted scorecard, from the test results
|
|
1772
|
+
function doGenerateScorecard(s) {
|
|
1773
|
+
if (!(s.bPostProcessingDone)) {
|
|
1774
|
+
doAnalyzePostProcessing(s);
|
|
1775
|
+
}
|
|
1776
|
+
// Create a new scorecard
|
|
1777
|
+
let scorecard = {};
|
|
1778
|
+
// Filter the defined scorecard categories by the requested test suites
|
|
1779
|
+
let categories = U.getNumericObjectKeys(scorecardDefn);
|
|
1780
|
+
let suitesRequested = s.config['suites'];
|
|
1781
|
+
categories = categories.filter(x => suitesRequested.includes(x));
|
|
1782
|
+
// ... and initialize each one in the new scorecard
|
|
1783
|
+
for (let c of categories) {
|
|
1784
|
+
scorecard[c] = {};
|
|
1785
|
+
scorecard[c]['catName'] = scorecardDefn[c]['catName'];
|
|
1786
|
+
scorecard[c]['catTests'] = {};
|
|
1787
|
+
// scorecard[c]['catScore'] = undefined;
|
|
1788
|
+
}
|
|
1789
|
+
// For each scorecard category
|
|
1790
|
+
for (let c of categories) {
|
|
1791
|
+
// Grab the scorecard category definition
|
|
1792
|
+
let { catName, catTests, catNumeric } = scorecardDefn[c];
|
|
1793
|
+
let numericCategoryScore = 0;
|
|
1794
|
+
let totalWeight = 0;
|
|
1795
|
+
let booleanCategoryScore = true;
|
|
1796
|
+
// Process the results for each test result in the category
|
|
1797
|
+
for (let testDefn of catTests) {
|
|
1798
|
+
// Get the config info for the test
|
|
1799
|
+
let testID = testDefn['testID'];
|
|
1800
|
+
// ... and the actual test result
|
|
1801
|
+
let testResult = s.getTest(testID);
|
|
1802
|
+
// Create a new test entry for the scorecard
|
|
1803
|
+
let testReport = U.deepCopy(testResult);
|
|
1804
|
+
// Add the name
|
|
1805
|
+
testReport['name'] = testDefns[testID]['name'];
|
|
1806
|
+
if (catNumeric) {
|
|
1807
|
+
// Normalize raw numeric scores ... moved to FIRST PASS above
|
|
1808
|
+
// Accumulate a category score
|
|
1809
|
+
let normalizedScore = testReport['normalizedScore'];
|
|
1810
|
+
numericCategoryScore += normalizedScore * testDefn['testWeight'];
|
|
1811
|
+
totalWeight += testDefn['testWeight'];
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
// AND together pass/fail tests into a category score
|
|
1815
|
+
if (!testReport['score']) {
|
|
1816
|
+
booleanCategoryScore = false;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
scorecard[c]['catTests'][testID] = testReport;
|
|
1820
|
+
}
|
|
1821
|
+
// Set the category score
|
|
1822
|
+
if (catNumeric) {
|
|
1823
|
+
scorecard[c]['catScore'] = Math.round(numericCategoryScore / totalWeight);
|
|
1824
|
+
}
|
|
1825
|
+
else {
|
|
1826
|
+
scorecard[c]['catScore'] = booleanCategoryScore;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// TODO - Compute an overall score from the category weights
|
|
1830
|
+
return scorecard;
|
|
1831
|
+
}
|
|
1832
|
+
// Prepare a formatted scorecard suitable for rendering
|
|
1833
|
+
function doPrepareScorecard(s) {
|
|
1834
|
+
// Initialize the output format
|
|
1835
|
+
let text = { data: [] };
|
|
1836
|
+
let blocks = text.data;
|
|
1837
|
+
// If the plan as already been analyzed, prepare a scorecard
|
|
1838
|
+
if (s.bPlanAnalyzed) {
|
|
1839
|
+
// Create and cache a new, unformatted scorecard
|
|
1840
|
+
s.scorecard = doGenerateScorecard(s);
|
|
1841
|
+
// Create a scorecard header
|
|
1842
|
+
blocks.push({ variant: 'h4', text: `Analysis` });
|
|
1843
|
+
// Report district statistics
|
|
1844
|
+
blocks.push({ variant: 'h5', text: `Individual Districts` });
|
|
1845
|
+
let districtStatisticsText = doPrepareDistrictStatistics(s);
|
|
1846
|
+
blocks.push(...districtStatisticsText);
|
|
1847
|
+
// Prepare each scorecard category
|
|
1848
|
+
blocks.push({ variant: 'h5', text: `Overall Plan` });
|
|
1849
|
+
let categories = U.getNumericObjectKeys(s.scorecard);
|
|
1850
|
+
for (let c of categories) {
|
|
1851
|
+
let sectionPrepareFn = scorecardDefn[c]['catPrepareFn'];
|
|
1852
|
+
let sectionText = sectionPrepareFn(s, c);
|
|
1853
|
+
blocks.push(...sectionText);
|
|
1854
|
+
}
|
|
1855
|
+
// Report what datasets were used
|
|
1856
|
+
let c = s.config['datasets']["CENSUS" /* CENSUS */];
|
|
1857
|
+
let v = s.config['datasets']["VAP" /* VAP */];
|
|
1858
|
+
let e = s.config['datasets']["ELECTION" /* ELECTION */];
|
|
1859
|
+
blocks.push({ variant: 'body1', text: `Using datasets:` });
|
|
1860
|
+
blocks.push({ variant: 'body1', text: `* ${c}: ${D.DatasetDescriptions[c]}` });
|
|
1861
|
+
blocks.push({ variant: 'body1', text: `* ${v}: ${D.DatasetDescriptions[v]}` });
|
|
1862
|
+
blocks.push({ variant: 'body1', text: `* ${e}: ${D.DatasetDescriptions[e]}` });
|
|
1863
|
+
}
|
|
1864
|
+
// Otherwise, return a blank scorecard
|
|
1865
|
+
// TODO - What dra-client returns from renderAnalyzeCore()
|
|
1866
|
+
// return <STV.StaticTextView text={ text } />;
|
|
1867
|
+
return text;
|
|
1868
|
+
}
|
|
1869
|
+
exports.doPrepareScorecard = doPrepareScorecard;
|
|
1870
|
+
function doPrepareDistrictStatistics(s) {
|
|
1871
|
+
let text = { data: [] };
|
|
1872
|
+
let blocks = text.data;
|
|
1873
|
+
blocks.push({ variant: 'beginTable' });
|
|
1874
|
+
blocks.push({ variant: 'row', cells: ['ID', 'Total', 'Δ%', 'OK?', '*', 'Dem', 'Rep', 'White', 'Minority', 'Black', 'Hispanic', 'Pacific', 'Asian', 'Native'] });
|
|
1875
|
+
for (let d = 0; d < s.districts.numberOfRows(); d++) {
|
|
1876
|
+
let tot = s.districts.statistics[D.DistrictField.TotalPop][d];
|
|
1877
|
+
if (tot == 0)
|
|
1878
|
+
blocks.push({ variant: 'row', cells: [String(d), '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-'] });
|
|
1879
|
+
else {
|
|
1880
|
+
tot = Math.round(tot);
|
|
1881
|
+
let dev = fractionToPercentage(s.districts.statistics[D.DistrictField.PopDevPct][d]);
|
|
1882
|
+
let bEq = true; // TODO - Set based on population threshold
|
|
1883
|
+
let bC = s.districts.statistics[D.DistrictField.bContiguous][d]
|
|
1884
|
+
&& s.districts.statistics[D.DistrictField.bNotEmbedded][d];
|
|
1885
|
+
let dPct = fractionToPercentage(s.districts.statistics[D.DistrictField.DemPct][d]);
|
|
1886
|
+
let rPct = fractionToPercentage(s.districts.statistics[D.DistrictField.RepPct][d]);
|
|
1887
|
+
let wPct = fractionToPercentage(s.districts.statistics[D.DistrictField.WhitePct][d]);
|
|
1888
|
+
let mPct = fractionToPercentage(s.districts.statistics[D.DistrictField.MinorityPct][d]);
|
|
1889
|
+
let bPct = fractionToPercentage(s.districts.statistics[D.DistrictField.BlackPct][d]);
|
|
1890
|
+
let hPct = fractionToPercentage(s.districts.statistics[D.DistrictField.HispanicPct][d]);
|
|
1891
|
+
let pPct = fractionToPercentage(s.districts.statistics[D.DistrictField.PacificPct][d]);
|
|
1892
|
+
let aPct = fractionToPercentage(s.districts.statistics[D.DistrictField.AsianPct][d]);
|
|
1893
|
+
let nPct = fractionToPercentage(s.districts.statistics[D.DistrictField.NativePct][d]);
|
|
1894
|
+
let id;
|
|
1895
|
+
if (d == 0)
|
|
1896
|
+
id = "??";
|
|
1897
|
+
else if (d == (s.districts.numberOfRows() - 1))
|
|
1898
|
+
id = " ";
|
|
1899
|
+
else
|
|
1900
|
+
id = String(d);
|
|
1901
|
+
blocks.push({
|
|
1902
|
+
variant: 'row',
|
|
1903
|
+
cells: [
|
|
1904
|
+
`${id}`,
|
|
1905
|
+
`${formatInteger(tot)}`,
|
|
1906
|
+
`${formatPercentage(dev)}%`,
|
|
1907
|
+
`${pfBoolToString(bEq)}`,
|
|
1908
|
+
`${pfBoolToString(bC)}`,
|
|
1909
|
+
`${formatPercentage(dPct)}%`,
|
|
1910
|
+
`${formatPercentage(rPct)}%`,
|
|
1911
|
+
`${formatPercentage(wPct)}%`,
|
|
1912
|
+
`${formatPercentage(mPct)}%`,
|
|
1913
|
+
`${formatPercentage(bPct)}%`,
|
|
1914
|
+
`${formatPercentage(hPct)}%`,
|
|
1915
|
+
`${formatPercentage(pPct)}%`,
|
|
1916
|
+
`${formatPercentage(aPct)}%`,
|
|
1917
|
+
`${formatPercentage(nPct)}%`
|
|
1918
|
+
]
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
blocks.push({ variant: 'endTable' });
|
|
1923
|
+
return blocks;
|
|
1924
|
+
}
|
|
1925
|
+
// TEST LOG
|
|
1926
|
+
// Prepare formatted test results for rendering
|
|
1927
|
+
function doPrepareTestLog(s) {
|
|
1928
|
+
// Initialize the output format
|
|
1929
|
+
let text = { data: [] };
|
|
1930
|
+
let blocks = text.data;
|
|
1931
|
+
// If the plan as already been analyzed, prepare a test log
|
|
1932
|
+
if (s.bPlanAnalyzed) {
|
|
1933
|
+
if (!(s.bPostProcessingDone)) {
|
|
1934
|
+
doAnalyzePostProcessing(s);
|
|
1935
|
+
}
|
|
1936
|
+
// Create a test log header
|
|
1937
|
+
blocks.push({ variant: 'h4', text: `Test Log` });
|
|
1938
|
+
let testResults = U.getNumericObjectKeys(testDefns);
|
|
1939
|
+
let suitesRequested = new Set(s.config['suites']);
|
|
1940
|
+
for (let testID of testResults) {
|
|
1941
|
+
// Filter the defined tests by the requested test suites
|
|
1942
|
+
let inSuites = testDefns[testID]['suites'];
|
|
1943
|
+
if (!(U.isArrayEmpty(inSuites.filter(x => suitesRequested.has(x))))) {
|
|
1944
|
+
// Get the test result
|
|
1945
|
+
let testResult = s.getTest(testID);
|
|
1946
|
+
// Prepare the text for it, and append it to the output
|
|
1947
|
+
let testText = prepareTestEntry(testID, testResult);
|
|
1948
|
+
blocks.push(...testText);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
// Otherwise, return a blank test log
|
|
1953
|
+
// TODO - What dra-client returns from renderAnalyzeCore()
|
|
1954
|
+
// return <STV.StaticTextView text={ text } />;
|
|
1955
|
+
return text;
|
|
1956
|
+
}
|
|
1957
|
+
exports.doPrepareTestLog = doPrepareTestLog;
|
|
1958
|
+
function prepareTestEntry(testID, testResult) {
|
|
1959
|
+
let text = { data: [] };
|
|
1960
|
+
let blocks = text.data;
|
|
1961
|
+
let testName = testDefns[testID]['name'];
|
|
1962
|
+
let testNameTrailer = "";
|
|
1963
|
+
if (U.keyExists('trailer', testDefns[testID])) {
|
|
1964
|
+
testNameTrailer = testDefns[testID]['trailer'];
|
|
1965
|
+
}
|
|
1966
|
+
let testType = testDefns[testID]['externalType'];
|
|
1967
|
+
let bNormalize = testDefns[testID]['normalize'];
|
|
1968
|
+
let detailsFn = testDefns[testID]['detailsFn'];
|
|
1969
|
+
let detailsText = detailsFn(testResult);
|
|
1970
|
+
let score; // NOTE - Won't be undefined here
|
|
1971
|
+
let normalizedScore;
|
|
1972
|
+
let scoreText;
|
|
1973
|
+
// Get the score ...
|
|
1974
|
+
score = testResult['score'];
|
|
1975
|
+
// ... and format it for rendering
|
|
1976
|
+
switch (testType) {
|
|
1977
|
+
case TestType.PassFail: {
|
|
1978
|
+
scoreText = pfBoolToString(score);
|
|
1979
|
+
blocks.push({ variant: 'body1', text: `${testName}: ${scoreText} ${testNameTrailer}` });
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1982
|
+
case TestType.Percentage: {
|
|
1983
|
+
score = fractionToPercentage(score);
|
|
1984
|
+
if (bNormalize) {
|
|
1985
|
+
normalizedScore = testResult['normalizedScore'];
|
|
1986
|
+
blocks.push({ variant: 'body1', text: `${testName}: ${normalizedScore} / 100 : ${formatPercentage(score)}% ${testNameTrailer}` });
|
|
1987
|
+
}
|
|
1988
|
+
else {
|
|
1989
|
+
blocks.push({ variant: 'body1', text: `${testName}: ${formatPercentage(score)}% ${testNameTrailer}` });
|
|
1990
|
+
}
|
|
1991
|
+
break;
|
|
1992
|
+
}
|
|
1993
|
+
case TestType.Number: {
|
|
1994
|
+
if (bNormalize) {
|
|
1995
|
+
normalizedScore = testResult['normalizedScore'];
|
|
1996
|
+
blocks.push({ variant: 'body1', text: `${testName}: ${normalizedScore} / 100 : ${formatNumber(score)} ${testNameTrailer}` });
|
|
1997
|
+
}
|
|
1998
|
+
else {
|
|
1999
|
+
blocks.push({ variant: 'body1', text: `${testName}: ${formatNumber(score)} ${testNameTrailer}` });
|
|
2000
|
+
}
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
default: {
|
|
2004
|
+
// Unknown test type
|
|
2005
|
+
throw new RangeError();
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
// Add the details text
|
|
2009
|
+
blocks.push(...detailsText);
|
|
2010
|
+
return blocks;
|
|
2011
|
+
}
|
|
2012
|
+
// FORMATTERS FOR TEST DETAILS
|
|
2013
|
+
function doPrepareCompleteDetails(testResult) {
|
|
2014
|
+
let text = { data: [] };
|
|
2015
|
+
let blocks = text.data;
|
|
2016
|
+
if (!U.isObjectEmpty(testResult['details'])) {
|
|
2017
|
+
let unassignedText = "";
|
|
2018
|
+
let emptyText = "";
|
|
2019
|
+
let missingText = "";
|
|
2020
|
+
if (U.keyExists('unassignedFeatures', testResult['details'])) {
|
|
2021
|
+
let unassignedFeatures = testResult['details']['unassignedFeatures'];
|
|
2022
|
+
let unassignedList = prepareListItems(unassignedFeatures);
|
|
2023
|
+
let unassignedTextTemplates = [
|
|
2024
|
+
`GEOID ${unassignedList} is not assigned to a district.`,
|
|
2025
|
+
`GEOIDs ${unassignedList} are not assigned to districts.`,
|
|
2026
|
+
`Several GEOIDs are not assigned to districts, including ${unassignedList}.`
|
|
2027
|
+
];
|
|
2028
|
+
unassignedText = prepareListText(unassignedFeatures, unassignedTextTemplates);
|
|
2029
|
+
}
|
|
2030
|
+
if (U.keyExists('emptyDistricts', testResult['details'])) {
|
|
2031
|
+
let emptyDistricts = testResult['details']['emptyDistricts'];
|
|
2032
|
+
let emptyList = prepareListItems(emptyDistricts);
|
|
2033
|
+
let emptyTextTemplates = [
|
|
2034
|
+
`District ${emptyList} is empty.`,
|
|
2035
|
+
`Districts ${emptyList} are empty.`,
|
|
2036
|
+
`Several districts are empty, including ${emptyList}.`
|
|
2037
|
+
];
|
|
2038
|
+
emptyText = prepareListText(emptyDistricts, emptyTextTemplates);
|
|
2039
|
+
}
|
|
2040
|
+
if (U.keyExists('missingDistricts', testResult['details'])) {
|
|
2041
|
+
missingText = `Not enough districts have been defined. `;
|
|
2042
|
+
}
|
|
2043
|
+
let detailsText = " " + unassignedText + emptyText + missingText;
|
|
2044
|
+
blocks.push({ variant: 'body1', text: detailsText });
|
|
2045
|
+
}
|
|
2046
|
+
return blocks;
|
|
2047
|
+
}
|
|
2048
|
+
function doPrepareContiguousDetails(testResult) {
|
|
2049
|
+
let text = { data: [] };
|
|
2050
|
+
let blocks = text.data;
|
|
2051
|
+
if (!U.isObjectEmpty(testResult['details'])) {
|
|
2052
|
+
let discontiguousDistricts = testResult['details']['discontiguousDistricts'];
|
|
2053
|
+
let discontiguousList = prepareListItems(discontiguousDistricts);
|
|
2054
|
+
let discontiguousTextTemplates = [
|
|
2055
|
+
`District ${discontiguousList} is not contiguous.`,
|
|
2056
|
+
`Districts ${discontiguousList} are not contiguous.`,
|
|
2057
|
+
`Several districts are not contiguous, including ${discontiguousList}.`
|
|
2058
|
+
];
|
|
2059
|
+
let detailsText = prepareListText(discontiguousDistricts, discontiguousTextTemplates);
|
|
2060
|
+
blocks.push({ variant: 'body1', text: detailsText });
|
|
2061
|
+
}
|
|
2062
|
+
return blocks;
|
|
2063
|
+
}
|
|
2064
|
+
function doPrepareFreeOfHolesDetails(testResult) {
|
|
2065
|
+
let text = { data: [] };
|
|
2066
|
+
let blocks = text.data;
|
|
2067
|
+
if (!U.isObjectEmpty(testResult['details'])) {
|
|
2068
|
+
let embeddedDistricts = testResult['details']['embeddedDistricts'];
|
|
2069
|
+
let embeddedList = prepareListItems(embeddedDistricts);
|
|
2070
|
+
let embeddedTextTemplates = [
|
|
2071
|
+
`District ${embeddedList} is fully embedded within another district.`,
|
|
2072
|
+
`Both districts ${embeddedList} are fully embedded within other districts.`,
|
|
2073
|
+
`Several districts are fully embedded within other districts, including ${embeddedList}.`
|
|
2074
|
+
];
|
|
2075
|
+
let detailsText = prepareListText(embeddedDistricts, embeddedTextTemplates);
|
|
2076
|
+
blocks.push({ variant: 'body1', text: detailsText });
|
|
2077
|
+
}
|
|
2078
|
+
return blocks;
|
|
2079
|
+
}
|
|
2080
|
+
function doPrepareEqualPopulationDetails(testResult) {
|
|
2081
|
+
let text = { data: [] };
|
|
2082
|
+
let blocks = text.data;
|
|
2083
|
+
if (!U.isObjectEmpty(testResult['details'])) {
|
|
2084
|
+
let popDevPct = fractionToPercentage(testResult['details']['deviation']);
|
|
2085
|
+
let thresholdPct = fractionToPercentage(1.0 - testResult['details']['thresholds'][0]);
|
|
2086
|
+
let detailsText = '';
|
|
2087
|
+
if (!(testResult['score'])) {
|
|
2088
|
+
detailsText = `The ${formatPercentage(popDevPct)}% population deviation is greater than the ${formatPercentage(thresholdPct)}% threshold tolerated by courts.`;
|
|
2089
|
+
}
|
|
2090
|
+
blocks.push({ variant: 'body1', text: detailsText });
|
|
2091
|
+
}
|
|
2092
|
+
return blocks;
|
|
2093
|
+
}
|
|
2094
|
+
function doPreparePopulationDeviationDetails(testResult) {
|
|
2095
|
+
let text = { data: [] };
|
|
2096
|
+
let blocks = text.data;
|
|
2097
|
+
let n = Math.round(testResult['details']['maxDeviation']);
|
|
2098
|
+
let term = "people";
|
|
2099
|
+
if (n == 1) {
|
|
2100
|
+
term = "person";
|
|
2101
|
+
}
|
|
2102
|
+
blocks.push({ variant: 'body1', text: `The maximum population deviation between districts is ${formatInteger(n)} ${term}.` });
|
|
2103
|
+
return blocks;
|
|
2104
|
+
}
|
|
2105
|
+
function doPrepareReockDetails(testResult) {
|
|
2106
|
+
let text = { data: [] };
|
|
2107
|
+
let blocks = text.data;
|
|
2108
|
+
// TODO - No details implemented yet
|
|
2109
|
+
return blocks;
|
|
2110
|
+
}
|
|
2111
|
+
function doPreparePolsbyPopperDetails(testResult) {
|
|
2112
|
+
let text = { data: [] };
|
|
2113
|
+
let blocks = text.data;
|
|
2114
|
+
// TODO - No details implemented yet
|
|
2115
|
+
return blocks;
|
|
2116
|
+
}
|
|
2117
|
+
function doPrepareEfficiencyGapDetails(testResult) {
|
|
2118
|
+
let text = { data: [] };
|
|
2119
|
+
let blocks = text.data;
|
|
2120
|
+
// TODO - Not yet implemented
|
|
2121
|
+
return blocks;
|
|
2122
|
+
}
|
|
2123
|
+
function doPrepareCountySplitDetails(testResult) {
|
|
2124
|
+
let text = { data: [] };
|
|
2125
|
+
let blocks = text.data;
|
|
2126
|
+
let unexpectedAffected = fractionToPercentage(testResult['details']['unexpectedAffected']);
|
|
2127
|
+
let affectedText = `affecting ${formatPercentage(unexpectedAffected)}% of the total population.`;
|
|
2128
|
+
let countiesSplitUnexpectedly = testResult['details']['countiesSplitUnexpectedly'];
|
|
2129
|
+
let nCountiesSplitUnexpectedly = countiesSplitUnexpectedly.length;
|
|
2130
|
+
let nUnexpectedSplits = testResult['details']['unexpectedSplits'];
|
|
2131
|
+
let splitList = prepareListItems(countiesSplitUnexpectedly);
|
|
2132
|
+
let splitTextTemplates = [
|
|
2133
|
+
`${splitList} county is split unexpectedly, `,
|
|
2134
|
+
`${splitList} counties are split unexpectedly, `,
|
|
2135
|
+
// `These ${formatInteger(nCountiesSplitUnexpectedly)} counties are split unexpectedly--${splitList}--`
|
|
2136
|
+
`${splitList} counties are split unexpectedly ${formatInteger(nUnexpectedSplits)} times, `
|
|
2137
|
+
];
|
|
2138
|
+
let detailsText = prepareListText(countiesSplitUnexpectedly, splitTextTemplates) + affectedText;
|
|
2139
|
+
blocks.push({ variant: 'body1', text: detailsText });
|
|
2140
|
+
return blocks;
|
|
2141
|
+
}
|
|
2142
|
+
// FORMATTERS FOR CATEGORIES
|
|
2143
|
+
// This function parses & formats the 'Valid' section of a scorecard.
|
|
2144
|
+
function doPrepareValidSection(s, c) {
|
|
2145
|
+
// Get the category meta data
|
|
2146
|
+
let { catName, catScore, catTests } = s.scorecard[c];
|
|
2147
|
+
let testReport;
|
|
2148
|
+
// Initialize the section text
|
|
2149
|
+
let text = { data: [] };
|
|
2150
|
+
let blocks = text.data;
|
|
2151
|
+
let testText;
|
|
2152
|
+
// Section header
|
|
2153
|
+
let stringScore = pfBoolToString(catScore);
|
|
2154
|
+
blocks.push({ variant: 'h6', text: `${catName}: ${stringScore}` });
|
|
2155
|
+
// Complete
|
|
2156
|
+
testReport = catTests[0 /* Complete */];
|
|
2157
|
+
testText = prepareTestEntry(0 /* Complete */, testReport);
|
|
2158
|
+
blocks.push(...testText);
|
|
2159
|
+
// Contiguous
|
|
2160
|
+
testReport = catTests[1 /* Contiguous */];
|
|
2161
|
+
testText = prepareTestEntry(1 /* Contiguous */, testReport);
|
|
2162
|
+
blocks.push(...testText);
|
|
2163
|
+
// Free of holes (no embedded districts)
|
|
2164
|
+
testReport = catTests[2 /* FreeOfHoles */];
|
|
2165
|
+
testText = prepareTestEntry(2 /* FreeOfHoles */, testReport);
|
|
2166
|
+
blocks.push(...testText);
|
|
2167
|
+
// Equal population (w/in the appropriate legal threshold)
|
|
2168
|
+
testReport = catTests[3 /* EqualPopulation */];
|
|
2169
|
+
testText = prepareTestEntry(3 /* EqualPopulation */, testReport);
|
|
2170
|
+
blocks.push(...testText);
|
|
2171
|
+
return blocks;
|
|
2172
|
+
}
|
|
2173
|
+
// TODO - NIY
|
|
2174
|
+
// This function parses & formats the 'Fair' section of a scorecard.
|
|
2175
|
+
function doPrepareFairSection(s, c) {
|
|
2176
|
+
// Get the category meta data
|
|
2177
|
+
let { catName, catScore, catTests } = s.scorecard[c];
|
|
2178
|
+
let testReport;
|
|
2179
|
+
// Initialize the section text
|
|
2180
|
+
let text = { data: [] };
|
|
2181
|
+
let blocks = text.data;
|
|
2182
|
+
let testText;
|
|
2183
|
+
// Section header
|
|
2184
|
+
blocks.push({ variant: 'h6', text: `${catName}: ${catScore}` });
|
|
2185
|
+
// TODO - Flesh this out
|
|
2186
|
+
// There's only one test: Population Deviation.
|
|
2187
|
+
// testReport = catTests[T.Test.PopulationDeviation];
|
|
2188
|
+
// testText = prepareTestEntry(T.Test.PopulationDeviation, testReport);
|
|
2189
|
+
// blocks.push(...testText);
|
|
2190
|
+
return blocks;
|
|
2191
|
+
}
|
|
2192
|
+
// This function parses & formats the 'Best' section of a scorecard.
|
|
2193
|
+
function doPrepareBestSection(s, c) {
|
|
2194
|
+
// Get the category meta data
|
|
2195
|
+
let { catName, catScore, catTests } = s.scorecard[c];
|
|
2196
|
+
let testReport;
|
|
2197
|
+
// Initialize the section text
|
|
2198
|
+
let text = { data: [] };
|
|
2199
|
+
let blocks = text.data;
|
|
2200
|
+
let testText;
|
|
2201
|
+
// Section header
|
|
2202
|
+
blocks.push({ variant: 'h6', text: `${catName}: ${catScore}` });
|
|
2203
|
+
// Population deviation
|
|
2204
|
+
testReport = catTests[4 /* PopulationDeviation */];
|
|
2205
|
+
testText = prepareTestEntry(4 /* PopulationDeviation */, testReport);
|
|
2206
|
+
blocks.push(...testText);
|
|
2207
|
+
// Compactness
|
|
2208
|
+
testReport = catTests[5 /* Reock */];
|
|
2209
|
+
let normalizedReock = testReport['normalizedScore'];
|
|
2210
|
+
let reockTestText = prepareTestEntry(5 /* Reock */, testReport);
|
|
2211
|
+
testReport = catTests[6 /* PolsbyPopper */];
|
|
2212
|
+
let normalizedPolsbyPopper = testReport['normalizedScore'];
|
|
2213
|
+
let polsbyPopperTestText = prepareTestEntry(6 /* PolsbyPopper */, testReport);
|
|
2214
|
+
let compactnessScore = (normalizedReock + normalizedPolsbyPopper) / 2;
|
|
2215
|
+
blocks.push({ variant: 'body1', text: `Compactness: ${compactnessScore} / 100` });
|
|
2216
|
+
blocks.push(...reockTestText);
|
|
2217
|
+
blocks.push(...polsbyPopperTestText);
|
|
2218
|
+
// County splits
|
|
2219
|
+
testReport = catTests[7 /* CountySplits */];
|
|
2220
|
+
testText = prepareTestEntry(7 /* CountySplits */, testReport);
|
|
2221
|
+
blocks.push(...testText);
|
|
2222
|
+
return blocks;
|
|
2223
|
+
}
|
|
2224
|
+
// FORMATTING HELPERS
|
|
2225
|
+
// Convert a boolean representing Pass/Fail to a string
|
|
2226
|
+
function pfBoolToString(score) {
|
|
2227
|
+
if (score) {
|
|
2228
|
+
return "Yes";
|
|
2229
|
+
}
|
|
2230
|
+
else {
|
|
2231
|
+
return "No";
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
function fractionToPercentage(f) {
|
|
2235
|
+
return f * 100;
|
|
2236
|
+
}
|
|
2237
|
+
function formatNumber(n) {
|
|
2238
|
+
let p = S.PRECISION;
|
|
2239
|
+
return n.toLocaleString('en-US', { minimumFractionDigits: p, maximumFractionDigits: p });
|
|
2240
|
+
}
|
|
2241
|
+
function formatPercentage(n) {
|
|
2242
|
+
let p = (S.PRECISION / 2);
|
|
2243
|
+
return n.toLocaleString('en-US', { minimumFractionDigits: p, maximumFractionDigits: p });
|
|
2244
|
+
}
|
|
2245
|
+
function formatInteger(i) {
|
|
2246
|
+
return new Intl.NumberFormat().format(i);
|
|
2247
|
+
}
|
|
2248
|
+
// Prepare the items in a list for rendering
|
|
2249
|
+
function prepareListItems(list) {
|
|
2250
|
+
let nItems = list.length;
|
|
2251
|
+
let listStr;
|
|
2252
|
+
switch (nItems) {
|
|
2253
|
+
case 1: {
|
|
2254
|
+
listStr = list[0];
|
|
2255
|
+
break;
|
|
2256
|
+
}
|
|
2257
|
+
case 2: {
|
|
2258
|
+
listStr = list[0] + " and " + list[1];
|
|
2259
|
+
break;
|
|
2260
|
+
}
|
|
2261
|
+
default: {
|
|
2262
|
+
let listWithCommas = list.join(', ');
|
|
2263
|
+
let lastCommaIndex = listWithCommas.length - ((list[list.length - 1].length) + 1);
|
|
2264
|
+
let beforeAnd = listWithCommas.substr(0, lastCommaIndex);
|
|
2265
|
+
let afterAnd = listWithCommas.substr(lastCommaIndex + 1);
|
|
2266
|
+
listStr = beforeAnd + " and " + afterAnd;
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
return listStr;
|
|
2271
|
+
}
|
|
2272
|
+
// Pick the rendering text for the appropriate list length
|
|
2273
|
+
function prepareListText(list, listTemplates) {
|
|
2274
|
+
let nItems = list.length;
|
|
2275
|
+
switch (nItems) {
|
|
2276
|
+
case 1: {
|
|
2277
|
+
return listTemplates[0];
|
|
2278
|
+
break;
|
|
2279
|
+
}
|
|
2280
|
+
case 2: {
|
|
2281
|
+
return listTemplates[1];
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2284
|
+
default: {
|
|
2285
|
+
return listTemplates[2];
|
|
2286
|
+
break;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
/* COMMAND-LINE TESTS
|
|
2291
|
+
|
|
2292
|
+
node main.js scorecard -v -x NC -n 13 -p ~/src/district-analytics/data/SAMPLE-BG-map.csv -d ~/src/district-analytics/data/SAMPLE-BG-data2.json -s ~/src/district-analytics/data/SAMPLE-BG-shapes.geojson -g ~/src/district-analytics/data/SAMPLE-BG-graph.json -c ~/src/district-analytics/data/SAMPLE-COUNTY.geojson
|
|
2293
|
+
node --inspect --inspect-brk main.js scorecard -v -x NC -n 13 -p ~/src/district-analytics/data/SAMPLE-BG-map.csv -d ~/src/district-analytics/data/SAMPLE-BG-data2.json -s ~/src/district-analytics/data/SAMPLE-BG-shapes.geojson -g ~/src/district-analytics/data/SAMPLE-BG-graph.json -c ~/src/district-analytics/data/SAMPLE-COUNTY.geojson
|
|
2294
|
+
|
|
2295
|
+
-or-
|
|
2296
|
+
|
|
2297
|
+
./main.js scorecard -v -x NC -n 13 -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
|
|
2298
|
+
./main.js --inspect --inspect-brk scorecard -v -x NC -n 13 -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
|
|
2299
|
+
|
|
2300
|
+
These calls work at the project directory, using samples in the data/ directory:
|
|
2301
|
+
|
|
2302
|
+
./main.js testlog -v -x NC -n 13 -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
|
|
2303
|
+
|
|
2304
|
+
TODO - Fix this
|
|
2305
|
+
./main.js testlog -v -x NC -n 13 -p ../data/SAMPLE-BG-map-missing.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
|
|
2306
|
+
./main.js scorecard -v -x NC-n 13 -p ../data/SAMPLE-BG-map-missing.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2307
|
+
|
|
2308
|
+
./main.js testlog -v -x NC -n 13 -p ../data/SAMPLE-BG-map-unassigned.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
|
|
2309
|
+
./main.js scorecard -v -x NC -n 13 -p ../data/SAMPLE-BG-map-unassigned.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
|
|
2310
|
+
|
|
2311
|
+
TODO - HERE
|
|
2312
|
+
|
|
2313
|
+
./main.js -v -x NC testlog -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson --empty
|
|
2314
|
+
./main.js -v -x NC scorecard -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson --empty
|
|
2315
|
+
|
|
2316
|
+
TODO
|
|
2317
|
+
|
|
2318
|
+
./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-discontiguous.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2319
|
+
./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-discontiguous.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2320
|
+
|
|
2321
|
+
./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-hole.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2322
|
+
./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-hole.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2323
|
+
|
|
2324
|
+
|
|
2325
|
+
node --inspect --inspect-brk main.js scorecard -v -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2326
|
+
node main.js scorecard -v -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2327
|
+
|
|
2328
|
+
|
|
2329
|
+
These calls test NC block-level data stored in my ~/data/redistricting-data/2010/compact/sample directory:
|
|
2330
|
+
|
|
2331
|
+
node --inspect --inspect-brk main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-shapes.geojson -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
|
|
2332
|
+
node --inspect --inspect-brk main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/tl_2018_37_tabblock10.json -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
|
|
2333
|
+
|
|
2334
|
+
node main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-shapes.geojson -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
|
|
2335
|
+
node main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/tl_2018_37_tabblock10.json -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
|
|
2336
|
+
|
|
2337
|
+
|
|
2338
|
+
|
|
2339
|
+
*/
|
|
2340
|
+
/* TODO - DELETE
|
|
2341
|
+
|
|
2342
|
+
These calls work at the project directory, using samples in the data/ directory:
|
|
2343
|
+
|
|
2344
|
+
./main.js testlog -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2345
|
+
./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2346
|
+
|
|
2347
|
+
./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-missing.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2348
|
+
./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-missing.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2349
|
+
|
|
2350
|
+
./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-unassigned.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2351
|
+
./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-unassigned.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2352
|
+
|
|
2353
|
+
./main.js testlog -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv --empty
|
|
2354
|
+
./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv --empty
|
|
2355
|
+
|
|
2356
|
+
./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-discontiguous.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2357
|
+
./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-discontiguous.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2358
|
+
|
|
2359
|
+
./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-hole.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2360
|
+
./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-hole.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
node --inspect --inspect-brk main.js scorecard -v -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2364
|
+
node main.js scorecard -v -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
|
|
2365
|
+
|
|
2366
|
+
|
|
2367
|
+
These calls test NC block-level data stored in my ~/data/redistricting-data/2010/compact/sample directory:
|
|
2368
|
+
|
|
2369
|
+
node --inspect --inspect-brk main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-shapes.geojson -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
|
|
2370
|
+
node --inspect --inspect-brk main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/tl_2018_37_tabblock10.json -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
|
|
2371
|
+
|
|
2372
|
+
node main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-shapes.geojson -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
|
|
2373
|
+
node main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/tl_2018_37_tabblock10.json -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
|
|
2374
|
+
|
|
2375
|
+
*/
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
/***/ }),
|
|
2379
|
+
|
|
2380
|
+
/***/ "./src/settings.ts":
|
|
2381
|
+
/*!*************************!*\
|
|
2382
|
+
!*** ./src/settings.ts ***!
|
|
2383
|
+
\*************************/
|
|
2384
|
+
/*! no static exports found */
|
|
2385
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
2386
|
+
|
|
2387
|
+
"use strict";
|
|
2388
|
+
|
|
2389
|
+
//
|
|
2390
|
+
// GLOBAL CONSTANTS
|
|
2391
|
+
//
|
|
2392
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2393
|
+
// Keep four decimal places for fractions [0–1], i.e.,
|
|
2394
|
+
// keep two decimal places for %'s [0–100].
|
|
2395
|
+
exports.PRECISION = 4;
|
|
2396
|
+
// Normalized scores [0-100]
|
|
2397
|
+
exports.NORMALIZED_RANGE = 100;
|
|
2398
|
+
// The dummy district ID for features not assigned districts yet
|
|
2399
|
+
exports.NOT_ASSIGNED = 0;
|
|
2400
|
+
// TODO - Discuss w/ Dave & Terry
|
|
2401
|
+
// # of items to report as problematic (e.g., features, districts, etc.)
|
|
2402
|
+
exports.NUMBER_OF_ITEMS_TO_REPORT = 10;
|
|
2403
|
+
// The virtual geoID for "neighbors" in other states
|
|
2404
|
+
exports.OUT_OF_STATE = "OUT_OF_STATE";
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
/***/ }),
|
|
2408
|
+
|
|
2409
|
+
/***/ "./src/types.ts":
|
|
2410
|
+
/*!**********************!*\
|
|
2411
|
+
!*** ./src/types.ts ***!
|
|
2412
|
+
\**********************/
|
|
2413
|
+
/*! no static exports found */
|
|
2414
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
2415
|
+
|
|
2416
|
+
"use strict";
|
|
2417
|
+
|
|
2418
|
+
//
|
|
2419
|
+
// TYPE DEFINITIONS
|
|
2420
|
+
//
|
|
2421
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2422
|
+
// END
|
|
2423
|
+
|
|
2424
|
+
|
|
2425
|
+
/***/ }),
|
|
2426
|
+
|
|
2427
|
+
/***/ "./src/utils.ts":
|
|
2428
|
+
/*!**********************!*\
|
|
2429
|
+
!*** ./src/utils.ts ***!
|
|
2430
|
+
\**********************/
|
|
2431
|
+
/*! no static exports found */
|
|
2432
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
2433
|
+
|
|
2434
|
+
"use strict";
|
|
2435
|
+
|
|
2436
|
+
//
|
|
2437
|
+
// UTILITIES
|
|
2438
|
+
//
|
|
2439
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2440
|
+
const S = __webpack_require__(/*! ./settings */ "./src/settings.ts");
|
|
2441
|
+
// PLAN HELPERS
|
|
2442
|
+
// Is a "neighbor" in state?
|
|
2443
|
+
function isInState(geoID) {
|
|
2444
|
+
return geoID != S.OUT_OF_STATE;
|
|
2445
|
+
}
|
|
2446
|
+
exports.isInState = isInState;
|
|
2447
|
+
// Is a "neighbor" out of state?
|
|
2448
|
+
function isOutOfState(geoID) {
|
|
2449
|
+
return geoID == S.OUT_OF_STATE;
|
|
2450
|
+
}
|
|
2451
|
+
exports.isOutOfState = isOutOfState;
|
|
2452
|
+
// Get the districtID to which a geoID is assigned
|
|
2453
|
+
function getDistrict(plan, geoID) {
|
|
2454
|
+
// All geoIDs in a state *should be* assigned to a district (including the
|
|
2455
|
+
// dummy "unassigned" district), but "water-only" features are sometimes missing
|
|
2456
|
+
// from a map. This is also a guard against a bug in which a geoID has no district.
|
|
2457
|
+
if (keyExists(geoID, plan)) {
|
|
2458
|
+
return plan[geoID];
|
|
2459
|
+
}
|
|
2460
|
+
else {
|
|
2461
|
+
return undefined;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
exports.getDistrict = getDistrict;
|
|
2465
|
+
// Invert a feature assignment structure to sets of ids by district
|
|
2466
|
+
// NOTE - This is here vs. _data.ts, so it can also be used in cli.ts
|
|
2467
|
+
function invertPlan(plan) {
|
|
2468
|
+
let invertedPlan = {};
|
|
2469
|
+
// Add a dummy 'unassigned' district
|
|
2470
|
+
invertedPlan[S.NOT_ASSIGNED] = new Set();
|
|
2471
|
+
for (let geoID in plan) {
|
|
2472
|
+
let districtID = plan[geoID];
|
|
2473
|
+
// Make sure the set for the districtID exists
|
|
2474
|
+
if (!(objectContains(invertedPlan, districtID))) {
|
|
2475
|
+
invertedPlan[districtID] = new Set();
|
|
2476
|
+
}
|
|
2477
|
+
// Add the geoID to the districtID's set
|
|
2478
|
+
invertedPlan[districtID].add(geoID);
|
|
2479
|
+
}
|
|
2480
|
+
return invertedPlan;
|
|
2481
|
+
}
|
|
2482
|
+
exports.invertPlan = invertPlan;
|
|
2483
|
+
// WORKING WITH GEOIDS
|
|
2484
|
+
function parseGeoID(geoID) {
|
|
2485
|
+
let parts = {};
|
|
2486
|
+
parts['state'] = geoID.substring(0, 2);
|
|
2487
|
+
parts['county'] = geoID.substring(0, 5);
|
|
2488
|
+
let l = geoID.length;
|
|
2489
|
+
if (l >= 11) {
|
|
2490
|
+
parts['tract'] = geoID.substring(0, 11);
|
|
2491
|
+
}
|
|
2492
|
+
if (l >= 12) {
|
|
2493
|
+
parts['bg'] = geoID.substring(0, 12);
|
|
2494
|
+
}
|
|
2495
|
+
if (l == 15) {
|
|
2496
|
+
parts['block'] = geoID;
|
|
2497
|
+
}
|
|
2498
|
+
return parts;
|
|
2499
|
+
}
|
|
2500
|
+
exports.parseGeoID = parseGeoID;
|
|
2501
|
+
function getFIPSFromCountyGeoID(geoID) {
|
|
2502
|
+
return geoID.substring(2, 5);
|
|
2503
|
+
}
|
|
2504
|
+
exports.getFIPSFromCountyGeoID = getFIPSFromCountyGeoID;
|
|
2505
|
+
// NORMALIZING RESULTS
|
|
2506
|
+
// Convert a raw score [0-1] into a normalized score [0-100]
|
|
2507
|
+
function normalize(rawScore, scale, invertp = false) {
|
|
2508
|
+
// Invert the axis if necessary to make bigger = better
|
|
2509
|
+
if (invertp) {
|
|
2510
|
+
rawScore = 1.0 - rawScore;
|
|
2511
|
+
}
|
|
2512
|
+
// Coerce the value to be w/in the given range
|
|
2513
|
+
let rangeMin = scale[0];
|
|
2514
|
+
let rangeMax = scale[1];
|
|
2515
|
+
let coercedValue = Math.min(Math.max(rawScore, rangeMin), rangeMax);
|
|
2516
|
+
// Scale the bounded value w/in the range [0 - (rangeMax - rangeMin)]
|
|
2517
|
+
let scaledValue = (coercedValue - rangeMin) / (rangeMax - rangeMin);
|
|
2518
|
+
// Finally, make the range [0-100]
|
|
2519
|
+
return Math.round(scaledValue * S.NORMALIZED_RANGE);
|
|
2520
|
+
}
|
|
2521
|
+
exports.normalize = normalize;
|
|
2522
|
+
// Round a fractional number [0-1] to the desired level of PRECISION.
|
|
2523
|
+
function trim(fullFraction) {
|
|
2524
|
+
let shiftPlaces = Math.pow(10, S.PRECISION);
|
|
2525
|
+
return Math.round(fullFraction * shiftPlaces) / shiftPlaces;
|
|
2526
|
+
}
|
|
2527
|
+
exports.trim = trim;
|
|
2528
|
+
// ARRAY HELPERS
|
|
2529
|
+
function sumArray(arr) {
|
|
2530
|
+
return arr.reduce((a, b) => a + b, 0);
|
|
2531
|
+
}
|
|
2532
|
+
exports.sumArray = sumArray;
|
|
2533
|
+
function avgArray(arr) {
|
|
2534
|
+
return (arr.reduce((a, b) => a + b, 0)) / arr.length;
|
|
2535
|
+
}
|
|
2536
|
+
exports.avgArray = avgArray;
|
|
2537
|
+
function minArray(arr) {
|
|
2538
|
+
return Math.min(...arr);
|
|
2539
|
+
}
|
|
2540
|
+
exports.minArray = minArray;
|
|
2541
|
+
function maxArray(arr) {
|
|
2542
|
+
return Math.max(...arr);
|
|
2543
|
+
}
|
|
2544
|
+
exports.maxArray = maxArray;
|
|
2545
|
+
function initArray(n, value) {
|
|
2546
|
+
return Array.from(Array(n), () => value);
|
|
2547
|
+
}
|
|
2548
|
+
exports.initArray = initArray;
|
|
2549
|
+
function andArray(arr) {
|
|
2550
|
+
return arr.reduce(function (a, b) { return a && b; }, true);
|
|
2551
|
+
}
|
|
2552
|
+
exports.andArray = andArray;
|
|
2553
|
+
// WORKING WITH OBJECT KEYS/PROPERTIES
|
|
2554
|
+
// TODO - Terry, is this copesetic?
|
|
2555
|
+
// Does an object have a key/property?
|
|
2556
|
+
function keyExists(k, o) {
|
|
2557
|
+
return k in o;
|
|
2558
|
+
}
|
|
2559
|
+
exports.keyExists = keyExists;
|
|
2560
|
+
// TODO - Terry, can these three be combined into a generic isEmpty() check?
|
|
2561
|
+
// Does an object (dict) have any keys/properties?
|
|
2562
|
+
function isObjectEmpty(o) {
|
|
2563
|
+
return Object.keys(o).length === 0;
|
|
2564
|
+
}
|
|
2565
|
+
exports.isObjectEmpty = isObjectEmpty;
|
|
2566
|
+
// Does a Set have any members?
|
|
2567
|
+
function isSetEmpty(s) {
|
|
2568
|
+
return s.size === 0;
|
|
2569
|
+
}
|
|
2570
|
+
exports.isSetEmpty = isSetEmpty;
|
|
2571
|
+
// Does an array hold any items?
|
|
2572
|
+
function isArrayEmpty(a) {
|
|
2573
|
+
if (a === undefined || a.length == 0) {
|
|
2574
|
+
// array empty or does not exist
|
|
2575
|
+
return true;
|
|
2576
|
+
}
|
|
2577
|
+
else {
|
|
2578
|
+
return false;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
exports.isArrayEmpty = isArrayEmpty;
|
|
2582
|
+
// Get the keys for an object
|
|
2583
|
+
function getObjectKeys(o) {
|
|
2584
|
+
return Object.keys(o);
|
|
2585
|
+
}
|
|
2586
|
+
exports.getObjectKeys = getObjectKeys;
|
|
2587
|
+
// TODO - Convert getNumericObjectKeys() idiom to for..of where possible
|
|
2588
|
+
function getNumericObjectKeys(o) {
|
|
2589
|
+
return Object.keys(o).map(Number);
|
|
2590
|
+
}
|
|
2591
|
+
exports.getNumericObjectKeys = getNumericObjectKeys;
|
|
2592
|
+
function getSelectObjectKeys(o, v) {
|
|
2593
|
+
let selectKeys = [];
|
|
2594
|
+
Object.keys(o).forEach(key => {
|
|
2595
|
+
if (arrayContains(v, o[key])) {
|
|
2596
|
+
selectKeys.push(key);
|
|
2597
|
+
}
|
|
2598
|
+
});
|
|
2599
|
+
return selectKeys;
|
|
2600
|
+
}
|
|
2601
|
+
exports.getSelectObjectKeys = getSelectObjectKeys;
|
|
2602
|
+
function arrayContains(a, item) {
|
|
2603
|
+
return a.some(x => x === item);
|
|
2604
|
+
}
|
|
2605
|
+
exports.arrayContains = arrayContains;
|
|
2606
|
+
function objectContains(o, key) {
|
|
2607
|
+
return (key in o);
|
|
2608
|
+
}
|
|
2609
|
+
exports.objectContains = objectContains;
|
|
2610
|
+
// ENUM HELPERS
|
|
2611
|
+
// Source: https://stackoverflow.com/questions/38034673/determine-the-number-of-enum-elements-typescript
|
|
2612
|
+
function countEnumValues(enumName) {
|
|
2613
|
+
let count = 0;
|
|
2614
|
+
for (let item in enumName) {
|
|
2615
|
+
if (isNaN(Number(item)))
|
|
2616
|
+
count++;
|
|
2617
|
+
}
|
|
2618
|
+
return count;
|
|
2619
|
+
}
|
|
2620
|
+
exports.countEnumValues = countEnumValues;
|
|
2621
|
+
// COPYING - Copied from dra-client/util.ts
|
|
2622
|
+
function shallowCopy(src) {
|
|
2623
|
+
if (Array.isArray(src))
|
|
2624
|
+
return src.slice();
|
|
2625
|
+
else if (typeof src === 'object') {
|
|
2626
|
+
let dst = {};
|
|
2627
|
+
for (var p in src)
|
|
2628
|
+
if (src.hasOwnProperty(p))
|
|
2629
|
+
dst[p] = src[p];
|
|
2630
|
+
return dst;
|
|
2631
|
+
}
|
|
2632
|
+
else
|
|
2633
|
+
return src;
|
|
2634
|
+
}
|
|
2635
|
+
exports.shallowCopy = shallowCopy;
|
|
2636
|
+
function deepCopy(src) {
|
|
2637
|
+
if (Array.isArray(src)) {
|
|
2638
|
+
let dst = [];
|
|
2639
|
+
for (let i = 0; i < src.length; i++)
|
|
2640
|
+
dst.push(deepCopy(src[i]));
|
|
2641
|
+
return dst;
|
|
2642
|
+
}
|
|
2643
|
+
else if (typeof src === 'object') {
|
|
2644
|
+
let dst = {};
|
|
2645
|
+
for (var p in src)
|
|
2646
|
+
if (src.hasOwnProperty(p))
|
|
2647
|
+
dst[p] = deepCopy(src[p]);
|
|
2648
|
+
return dst;
|
|
2649
|
+
}
|
|
2650
|
+
else
|
|
2651
|
+
return src;
|
|
2652
|
+
}
|
|
2653
|
+
exports.deepCopy = deepCopy;
|
|
2654
|
+
// TODO - Terry: What is this the simple explanation of what this thing is doing?
|
|
2655
|
+
var util_1 = __webpack_require__(/*! @dra2020/util */ "@dra2020/util");
|
|
2656
|
+
exports.depthof = util_1.depthof;
|
|
2657
|
+
|
|
2658
|
+
|
|
2659
|
+
/***/ }),
|
|
2660
|
+
|
|
2661
|
+
/***/ "./src/valid.ts":
|
|
2662
|
+
/*!**********************!*\
|
|
2663
|
+
!*** ./src/valid.ts ***!
|
|
2664
|
+
\**********************/
|
|
2665
|
+
/*! no static exports found */
|
|
2666
|
+
/***/ (function(module, exports, __webpack_require__) {
|
|
2667
|
+
|
|
2668
|
+
"use strict";
|
|
2669
|
+
|
|
2670
|
+
//
|
|
2671
|
+
// MAP/PLAN VALIDATIONS
|
|
2672
|
+
//
|
|
2673
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2674
|
+
const U = __webpack_require__(/*! ./utils */ "./src/utils.ts");
|
|
2675
|
+
const S = __webpack_require__(/*! ./settings */ "./src/settings.ts");
|
|
2676
|
+
const D = __webpack_require__(/*! ./_data */ "./src/_data.ts");
|
|
2677
|
+
//
|
|
2678
|
+
// COMPLETE - Are all geo's assigned to a district, and do all districts have
|
|
2679
|
+
// at least one geo assigned to them?
|
|
2680
|
+
//
|
|
2681
|
+
function doIsComplete(s) {
|
|
2682
|
+
let test = s.getTest(0 /* Complete */);
|
|
2683
|
+
let bAllAssigned = true;
|
|
2684
|
+
let bNoneEmpty = true;
|
|
2685
|
+
let unassignedFeatures = [];
|
|
2686
|
+
let emptyDistricts = [];
|
|
2687
|
+
// Get the by district results, including the dummy unassigned district,
|
|
2688
|
+
// but ignoring the N+1 summary district
|
|
2689
|
+
let bNotEmptyByDistrict = s.districts.statistics[D.DistrictField.bNotEmpty];
|
|
2690
|
+
bNotEmptyByDistrict = bNotEmptyByDistrict.slice(0, -1);
|
|
2691
|
+
// Are all features assigned to districts?
|
|
2692
|
+
// Check the dummy district that holds any unassigned features.
|
|
2693
|
+
bAllAssigned = (!bNotEmptyByDistrict[S.NOT_ASSIGNED]);
|
|
2694
|
+
if (!bAllAssigned) {
|
|
2695
|
+
let unassignedDistrict = s.plan.geoIDsForDistrictID(S.NOT_ASSIGNED);
|
|
2696
|
+
unassignedFeatures = Array.from(unassignedDistrict);
|
|
2697
|
+
unassignedFeatures = unassignedFeatures.slice(0, S.NUMBER_OF_ITEMS_TO_REPORT);
|
|
2698
|
+
}
|
|
2699
|
+
// Do all real districts have at least one feature assigned to them?
|
|
2700
|
+
bNoneEmpty = U.andArray(bNotEmptyByDistrict.slice(1));
|
|
2701
|
+
// Case 1 - One or more districts are missing:
|
|
2702
|
+
// The # of enumerated districts minus the dummy NOT_ASSIGNED one should
|
|
2703
|
+
// equal the number apportioned districts. This guards against a district
|
|
2704
|
+
// not being included in a map that is imported.
|
|
2705
|
+
//
|
|
2706
|
+
// TODO - I'm no longer checking for this, but DRA should!
|
|
2707
|
+
// Case 2 - Or a district is explicitly named but empty:
|
|
2708
|
+
// Note, this can happen if a district is created, and then all features
|
|
2709
|
+
// are removed from it (in DRA).
|
|
2710
|
+
// Populate the test entry
|
|
2711
|
+
test['score'] = bAllAssigned && bNoneEmpty;
|
|
2712
|
+
if (!bAllAssigned) {
|
|
2713
|
+
test['details']['unassignedFeatures'] = unassignedFeatures;
|
|
2714
|
+
}
|
|
2715
|
+
if (!bNoneEmpty) {
|
|
2716
|
+
test['details']['emptyDistricts'] = emptyDistricts;
|
|
2717
|
+
}
|
|
2718
|
+
// Populate the N+1 summary "district" in district.statistics
|
|
2719
|
+
let bNotEmpty = s.districts.statistics[D.DistrictField.bNotEmpty];
|
|
2720
|
+
let summaryRow = s.districts.numberOfRows() - 1;
|
|
2721
|
+
bNotEmpty[summaryRow] = test['score'];
|
|
2722
|
+
return test;
|
|
2723
|
+
}
|
|
2724
|
+
exports.doIsComplete = doIsComplete;
|
|
2725
|
+
//
|
|
2726
|
+
// CONTIGUOUS - Is each district in a plan fully connected?
|
|
2727
|
+
//
|
|
2728
|
+
// NOTE - To check "operational contiguity," we need to use a graph, i.e.,
|
|
2729
|
+
// we can't rely on just the geometric contiguity of shapes in a shapefile.
|
|
2730
|
+
//
|
|
2731
|
+
// To test this, load the NC 2010 map 'SAMPLE-BG-map-discontiguous.csv'.
|
|
2732
|
+
//
|
|
2733
|
+
function doIsContiguous(s) {
|
|
2734
|
+
let test = s.getTest(1 /* Contiguous */);
|
|
2735
|
+
// Get the contiguity of each district. Ignore dummy unassigned district
|
|
2736
|
+
// and the N+1 summary district.
|
|
2737
|
+
let bContiguousByDistrict = s.districts.statistics[D.DistrictField.bContiguous];
|
|
2738
|
+
bContiguousByDistrict = bContiguousByDistrict.slice(1, -1);
|
|
2739
|
+
// If any real districts aren't contiguous, mark the plan as not contiguous
|
|
2740
|
+
let bMapContiguous = U.andArray(bContiguousByDistrict);
|
|
2741
|
+
// If the map is not contiguous, log the offending districts.
|
|
2742
|
+
let discontiguousDistricts = [];
|
|
2743
|
+
let districtID = 1;
|
|
2744
|
+
bContiguousByDistrict.forEach(function (bDistrictContiguous) {
|
|
2745
|
+
if (!bDistrictContiguous)
|
|
2746
|
+
discontiguousDistricts.push(districtID);
|
|
2747
|
+
districtID += 1;
|
|
2748
|
+
});
|
|
2749
|
+
// Populate the test entry
|
|
2750
|
+
test['score'] = bMapContiguous;
|
|
2751
|
+
if (!bMapContiguous) {
|
|
2752
|
+
test['details'] = { 'discontiguousDistricts': discontiguousDistricts };
|
|
2753
|
+
}
|
|
2754
|
+
// Populate the N+1 summary "district" in district.statistics
|
|
2755
|
+
let bContiguous = s.districts.statistics[D.DistrictField.bContiguous];
|
|
2756
|
+
let summaryRow = s.districts.numberOfRows() - 1;
|
|
2757
|
+
bContiguous[summaryRow] = test['score'];
|
|
2758
|
+
return test;
|
|
2759
|
+
}
|
|
2760
|
+
exports.doIsContiguous = doIsContiguous;
|
|
2761
|
+
// Are the features in a district fully connected?
|
|
2762
|
+
function isConnected(districtGeos, graph) {
|
|
2763
|
+
// export function isConnected(districtGeos: Set<string>, graph: T.ContiguityGraph): boolean {
|
|
2764
|
+
// TODO - Terry, why does this constructor need a <T> type specification?
|
|
2765
|
+
let visited = new Set();
|
|
2766
|
+
let toProcess = [];
|
|
2767
|
+
// Start processing with the first geoID in the district
|
|
2768
|
+
let iter = districtGeos.values();
|
|
2769
|
+
toProcess.push(iter.next().value);
|
|
2770
|
+
// While there are geoIDs in the district that haven't been processed
|
|
2771
|
+
while (toProcess.length > 0) {
|
|
2772
|
+
// Grab a geoID and process it
|
|
2773
|
+
let node = toProcess.pop();
|
|
2774
|
+
visited.add(node);
|
|
2775
|
+
// Get its actual, in-state neighbors
|
|
2776
|
+
let actualNeighbors = graph.peerNeighbors(node).filter(x => U.isInState(x));
|
|
2777
|
+
// Add neighbors to visit, if they're in the same district Y haven't already been visited
|
|
2778
|
+
let neighborsToVisit = actualNeighbors.filter(x => districtGeos.has(x) && (!visited.has(x)));
|
|
2779
|
+
// TODO - Terry, is this the quickest/best way to do this?
|
|
2780
|
+
toProcess.push(...neighborsToVisit);
|
|
2781
|
+
}
|
|
2782
|
+
// Stop when you've visited all the geoIDs in the district
|
|
2783
|
+
return visited.size == districtGeos.size;
|
|
2784
|
+
}
|
|
2785
|
+
exports.isConnected = isConnected;
|
|
2786
|
+
//
|
|
2787
|
+
// FREE OF HOLES - Are any districts fully embedded w/in another district?
|
|
2788
|
+
//
|
|
2789
|
+
// A district is NOT a "donut hole" district:
|
|
2790
|
+
// - If any neighbor is 'OUT_OF_STATE'; or
|
|
2791
|
+
// - If there are 2 or more neighboring districts.
|
|
2792
|
+
//
|
|
2793
|
+
// To test this, load the NC 2010 map 'SAMPLE-BG-map-hole.csv'. District 1,
|
|
2794
|
+
// Buncombe County (37021), is a donut hole w/in District 3.
|
|
2795
|
+
//
|
|
2796
|
+
// TODO - Optimize this to take advantage of district boundary info, if/when
|
|
2797
|
+
// we cache one to optimize compactness.
|
|
2798
|
+
//
|
|
2799
|
+
function doIsFreeOfHoles(s) {
|
|
2800
|
+
let test = s.getTest(2 /* FreeOfHoles */);
|
|
2801
|
+
// Initialize values
|
|
2802
|
+
let bFreeOfHoles = true;
|
|
2803
|
+
let embeddedDistricts = [];
|
|
2804
|
+
// Get the embeddedness of each district. Ignore dummy unassigned district
|
|
2805
|
+
// and the N+1 summary district.
|
|
2806
|
+
let bNotEmbeddedByDistrict = s.districts.statistics[D.DistrictField.bNotEmbedded];
|
|
2807
|
+
bNotEmbeddedByDistrict = bNotEmbeddedByDistrict.slice(1, -1);
|
|
2808
|
+
let districtID = 1;
|
|
2809
|
+
bNotEmbeddedByDistrict.forEach(function (bDistrictNotEmbedded) {
|
|
2810
|
+
if (!bDistrictNotEmbedded) {
|
|
2811
|
+
embeddedDistricts.push(districtID);
|
|
2812
|
+
bFreeOfHoles = false;
|
|
2813
|
+
}
|
|
2814
|
+
districtID += 1;
|
|
2815
|
+
});
|
|
2816
|
+
// Populate the test entry
|
|
2817
|
+
test['score'] = bFreeOfHoles;
|
|
2818
|
+
if (!bFreeOfHoles) {
|
|
2819
|
+
test['details'] = { 'embeddedDistricts': embeddedDistricts };
|
|
2820
|
+
}
|
|
2821
|
+
// Populate the N+1 summary "district" in district.statistics
|
|
2822
|
+
let bNotEmbedded = s.districts.statistics[D.DistrictField.bNotEmbedded];
|
|
2823
|
+
let summaryRow = s.districts.numberOfRows() - 1;
|
|
2824
|
+
bNotEmbedded[summaryRow] = test['score'];
|
|
2825
|
+
return test;
|
|
2826
|
+
}
|
|
2827
|
+
exports.doIsFreeOfHoles = doIsFreeOfHoles;
|
|
2828
|
+
// Test whether one district is embedded w/in any other.
|
|
2829
|
+
function isEmbedded(districtID, geoIDs, plan, graph) {
|
|
2830
|
+
// TODO - Make "features" = "geoIDs." These aren't "features" proper, just
|
|
2831
|
+
// identifier strings.
|
|
2832
|
+
let features = geoIDs;
|
|
2833
|
+
let planByGeo = plan.byGeoID();
|
|
2834
|
+
// Assume the district is embedded
|
|
2835
|
+
let bEmbedded = true;
|
|
2836
|
+
// Keep track of the neighoring districts
|
|
2837
|
+
let neighboringDistricts = new Set();
|
|
2838
|
+
// TODO - Use just the boundary features, when available
|
|
2839
|
+
// Get the features for the real district
|
|
2840
|
+
let featuresToCheck = Array.from(features);
|
|
2841
|
+
// If the district has features, check whether it is embedded
|
|
2842
|
+
if (!(U.isArrayEmpty(featuresToCheck))) {
|
|
2843
|
+
// For each feature that needs to be checked (see above)
|
|
2844
|
+
for (let feature of featuresToCheck) {
|
|
2845
|
+
// Get its neighbors (including the virtual "out of state" ones)
|
|
2846
|
+
let neighbors = graph.peerNeighbors(feature);
|
|
2847
|
+
for (let neighbor of neighbors) {
|
|
2848
|
+
if (U.isOutOfState(neighbor)) {
|
|
2849
|
+
bEmbedded = false;
|
|
2850
|
+
// No need to check any more neighbors
|
|
2851
|
+
break;
|
|
2852
|
+
}
|
|
2853
|
+
else {
|
|
2854
|
+
let neighboringDistrict = U.getDistrict(planByGeo, neighbor);
|
|
2855
|
+
// Assume that a missing district assignment (= None) means that the
|
|
2856
|
+
// feature is "water-only" AND part of the state border (vs. internal)
|
|
2857
|
+
// and, therefore, not in the plan/map.
|
|
2858
|
+
if (neighboringDistrict == undefined) {
|
|
2859
|
+
bEmbedded = false;
|
|
2860
|
+
// No need to check any more neighbors
|
|
2861
|
+
break;
|
|
2862
|
+
}
|
|
2863
|
+
else {
|
|
2864
|
+
// TODO - Since we're checking *all* features in a district right
|
|
2865
|
+
// now, not just boundary features and neighbors in other districts,
|
|
2866
|
+
// prune out the current district. If/when we optimize compactness
|
|
2867
|
+
// to cache district boundaries (as before in my Python implementation),
|
|
2868
|
+
// we won't have to guard adding "neighboring" districts in this way.
|
|
2869
|
+
if (neighboringDistrict != districtID) {
|
|
2870
|
+
neighboringDistricts.add(neighboringDistrict);
|
|
2871
|
+
}
|
|
2872
|
+
if (neighboringDistricts.size > 1) {
|
|
2873
|
+
bEmbedded = false;
|
|
2874
|
+
// No need to check any more neighbors
|
|
2875
|
+
break;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
// If a district is not embedded, there's no need to check anymore
|
|
2881
|
+
// border geos.
|
|
2882
|
+
if (!bEmbedded) {
|
|
2883
|
+
break;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
return bEmbedded;
|
|
2888
|
+
}
|
|
2889
|
+
exports.isEmbedded = isEmbedded;
|
|
2890
|
+
// TODO - MIXED MAPS
|
|
2891
|
+
// - When we generalize to mixed maps, the determination of "neighbors in
|
|
2892
|
+
// the map" -- which is the core function in determining connectedness -- becomes
|
|
2893
|
+
// *much* more complicated and dynamic.
|
|
2894
|
+
// - I can write up (if not implement) the logic for this. It is a bit tricky
|
|
2895
|
+
// and requires special preprocessing of the summary level hierarchy (which I
|
|
2896
|
+
// also have a script for that we can repurpose) to distinguish between 'interior'
|
|
2897
|
+
// and 'edge' children.
|
|
2898
|
+
|
|
2899
|
+
|
|
2900
|
+
/***/ }),
|
|
2901
|
+
|
|
2902
|
+
/***/ "@dra2020/poly":
|
|
2903
|
+
/*!********************************!*\
|
|
2904
|
+
!*** external "@dra2020/poly" ***!
|
|
2905
|
+
\********************************/
|
|
2906
|
+
/*! no static exports found */
|
|
2907
|
+
/***/ (function(module, exports) {
|
|
2908
|
+
|
|
2909
|
+
module.exports = require("@dra2020/poly");
|
|
2910
|
+
|
|
2911
|
+
/***/ }),
|
|
2912
|
+
|
|
2913
|
+
/***/ "@dra2020/util":
|
|
2914
|
+
/*!********************************!*\
|
|
2915
|
+
!*** external "@dra2020/util" ***!
|
|
2916
|
+
\********************************/
|
|
2917
|
+
/*! no static exports found */
|
|
2918
|
+
/***/ (function(module, exports) {
|
|
2919
|
+
|
|
2920
|
+
module.exports = require("@dra2020/util");
|
|
2921
|
+
|
|
2922
|
+
/***/ }),
|
|
2923
|
+
|
|
2924
|
+
/***/ "assert":
|
|
2925
|
+
/*!*************************!*\
|
|
2926
|
+
!*** external "assert" ***!
|
|
2927
|
+
\*************************/
|
|
2928
|
+
/*! no static exports found */
|
|
2929
|
+
/***/ (function(module, exports) {
|
|
2930
|
+
|
|
2931
|
+
module.exports = require("assert");
|
|
2932
|
+
|
|
2933
|
+
/***/ })
|
|
2934
|
+
|
|
2935
|
+
/******/ });
|
|
2936
|
+
});
|
|
2937
|
+
//# sourceMappingURL=district-analytics.js.map
|