@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.
@@ -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