@dra2020/district-analytics 17.0.2 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/dist/_api.d.ts +5 -29
- package/dist/_data.d.ts +2 -67
- package/dist/analyze.d.ts +1 -3
- package/dist/cohesive.d.ts +2 -3
- package/dist/compact.d.ts +0 -2
- package/dist/data.d.ts +60 -16
- package/dist/district-analytics.js +1368 -1979
- package/dist/district-analytics.js.map +1 -1
- package/dist/equal.d.ts +0 -3
- package/dist/index.d.ts +2 -2
- package/dist/legacy-types.d.ts +1 -3
- package/dist/minority.d.ts +0 -6
- package/dist/preprocess.d.ts +1 -1
- package/dist/requirements.d.ts +12 -0
- package/dist/results.d.ts +0 -50
- package/dist/rpv.d.ts +2 -2
- package/dist/score.d.ts +3 -4
- package/dist/settings.d.ts +0 -1
- package/dist/src/_api.d.ts +5 -29
- package/dist/src/_data.d.ts +2 -67
- package/dist/src/analyze.d.ts +1 -3
- package/dist/src/cohesive.d.ts +2 -3
- package/dist/src/compact.d.ts +0 -2
- package/dist/src/data.d.ts +60 -16
- package/dist/src/equal.d.ts +0 -3
- package/dist/src/index.d.ts +2 -2
- package/dist/src/legacy-types.d.ts +1 -3
- package/dist/src/minority.d.ts +0 -6
- package/dist/src/preprocess.d.ts +1 -1
- package/dist/src/requirements.d.ts +12 -0
- package/dist/src/results.d.ts +0 -50
- package/dist/src/rpv.d.ts +2 -2
- package/dist/src/score.d.ts +3 -4
- package/dist/src/settings.d.ts +0 -1
- package/dist/src/types.d.ts +86 -42
- package/dist/src/utils.d.ts +8 -5
- package/dist/src/valid.d.ts +0 -5
- package/dist/types.d.ts +86 -42
- package/dist/utils.d.ts +8 -5
- package/dist/valid.d.ts +0 -5
- package/package.json +3 -3
|
@@ -20,7 +20,7 @@ return /******/ (() => { // webpackBootstrap
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
//
|
|
23
|
-
//
|
|
23
|
+
// ANALYTICS INTERFACE
|
|
24
24
|
//
|
|
25
25
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
26
26
|
if (k2 === undefined) k2 = k;
|
|
@@ -57,121 +57,23 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
57
57
|
})();
|
|
58
58
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
59
59
|
exports.AnalyticsSession = void 0;
|
|
60
|
-
const baseclient_1 = __webpack_require__(/*! @dra2020/baseclient */ "@dra2020/baseclient");
|
|
61
|
-
const dra_analytics_1 = __webpack_require__(/*! @dra2020/dra-analytics */ "@dra2020/dra-analytics");
|
|
62
|
-
const preprocess_1 = __webpack_require__(/*! ./preprocess */ "./src/preprocess.ts");
|
|
63
60
|
const analyze_1 = __webpack_require__(/*! ./analyze */ "./src/analyze.ts");
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const results_2 = __webpack_require__(/*! ./results */ "./src/results.ts");
|
|
67
|
-
const minority_1 = __webpack_require__(/*! ./minority */ "./src/minority.ts");
|
|
68
|
-
const D = __importStar(__webpack_require__(/*! ./_data */ "./src/_data.ts"));
|
|
69
|
-
const M = __importStar(__webpack_require__(/*! ./minority */ "./src/minority.ts"));
|
|
61
|
+
const rpv_1 = __webpack_require__(/*! ./rpv */ "./src/rpv.ts");
|
|
62
|
+
const D = __importStar(__webpack_require__(/*! ./data */ "./src/data.ts"));
|
|
70
63
|
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
71
|
-
// MMD - Extended for optional # reps per district
|
|
72
64
|
class AnalyticsSession {
|
|
73
65
|
constructor(SessionRequest) {
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
76
|
-
this.bOneTimeProcessingDone = false;
|
|
77
|
-
this.bPlanAnalyzed = false;
|
|
78
|
-
this.bPostProcessingDone = false;
|
|
79
|
-
this.tests = {};
|
|
80
|
-
this.title = SessionRequest['title'];
|
|
81
|
-
this.config = this.processConfig(SessionRequest['config']);
|
|
82
|
-
// 2020 - Set the CD / LD toggle:
|
|
83
|
-
// * If 2020, use the PlanType, if given.
|
|
84
|
-
// * If 2010, use the legacy LD toggle, if given.
|
|
85
|
-
// * Otherwise assume CD's.
|
|
86
|
-
// * Use the LD population deviation threshold for districts that are not CD's.
|
|
87
|
-
if ((this.config.cycle == 2020) && (SessionRequest['planType'] !== undefined))
|
|
88
|
-
this.legislativeDistricts = (SessionRequest['planType'] != 'congress');
|
|
89
|
-
else if ((this.config.cycle == 2010) && (SessionRequest['legislativeDistricts'] !== undefined))
|
|
90
|
-
this.legislativeDistricts = SessionRequest['legislativeDistricts'];
|
|
91
|
-
// MMD - Validate & handle optional # reps per district
|
|
92
|
-
const repsByDistrict = SessionRequest['repsByDistrict'];
|
|
93
|
-
const nDistricts = SessionRequest['nDistricts'];
|
|
94
|
-
if (repsByDistrict !== undefined) {
|
|
95
|
-
if (repsByDistrict.length != nDistricts)
|
|
96
|
-
throw new Error("Mismatched #'s of districts passed to AnalyticsSession constructor!");
|
|
97
|
-
if (repsByDistrict.includes(0))
|
|
98
|
-
throw new Error("Zero reps for a district passed to AnalyticsSession constructor!");
|
|
99
|
-
// Assume a positive integer # of reps per district
|
|
100
|
-
this.repsByDistrict = repsByDistrict;
|
|
101
|
-
}
|
|
102
|
-
this.state = new D.State(this, SessionRequest['stateXX'], nDistricts);
|
|
103
|
-
this.counties = new D.Counties(this, SessionRequest['counties']);
|
|
104
|
-
this.graph = new D.GraphClass(this, SessionRequest['graph']);
|
|
105
|
-
this.features = new D.Features(this, SessionRequest['data'], this.config['datasets']);
|
|
106
|
-
this.plan = new D.Plan(this, SessionRequest['plan']);
|
|
107
|
-
this.districts = new D.Districts(this, SessionRequest['districtShapes']);
|
|
108
|
-
this.aggregates = SessionRequest['aggregates'];
|
|
109
|
-
this.datasetsMeta = SessionRequest['datasetsMeta'];
|
|
110
|
-
}
|
|
111
|
-
processConfig(config) {
|
|
112
|
-
// Default the Census & redistricting cycle to 2010
|
|
113
|
-
if (!(U.keyExists('cycle', config))) {
|
|
114
|
-
config['cycle'] = 2010;
|
|
115
|
-
// DEBUG
|
|
116
|
-
console.log("Cycle was not set explicitly on the session request.");
|
|
117
|
-
}
|
|
118
|
-
// DEBUG - Turned off 03/05/21
|
|
119
|
-
// console.log("Cycle = ", config['cycle']);
|
|
120
|
-
// console.log("Shapes = ", config['datasets'][T.Dataset.SHAPES], ": ", config['descriptions']['SHAPES']);
|
|
121
|
-
return config;
|
|
66
|
+
this.bLog = false;
|
|
67
|
+
this.data = new D.SessionData(this, SessionRequest);
|
|
122
68
|
}
|
|
123
|
-
// Using the the data in the analytics session, calculate all the
|
|
124
|
-
// analytics & validations, saving/updating the individual test results.
|
|
125
69
|
analyzePlan(bLog = false, overridesJSON) {
|
|
126
70
|
// Return values:
|
|
127
71
|
// * true = everything good
|
|
128
72
|
// * false = should not have been called with empty plan
|
|
129
73
|
// * exception = exception caught and logged on the console
|
|
130
74
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return false;
|
|
134
|
-
(0, preprocess_1.doPreprocessData)(this, bLog);
|
|
135
|
-
(0, analyze_1.doAnalyzeDistricts)(this, bLog);
|
|
136
|
-
// This does a little stuff that didn't get factored out into dra-score and then dra-analytics
|
|
137
|
-
(0, analyze_1.doAnalyzePlan)(this, bLog);
|
|
138
|
-
// THE MAIN ANALYTICS
|
|
139
|
-
// Even though we don't save the profile (as I thought we would),
|
|
140
|
-
// we allow it to be exported, so keep generating it, and use it
|
|
141
|
-
// to gather the inputs to new analytics (as well as the legacy).
|
|
142
|
-
this._profile = (0, score_1.profilePlan)(this, bLog);
|
|
143
|
-
let legacyScorecardAlt = {};
|
|
144
|
-
let newScorecard = {};
|
|
145
|
-
// Construct a new/alternate scorecard
|
|
146
|
-
newScorecard = (0, score_1.computeMetrics)(this._profile, this.getGoodShapes(), bLog); // w/o ratings
|
|
147
|
-
newScorecard = (0, score_1.rateKeyDimensions)(newScorecard, this._profile, bLog); // w/ ratings
|
|
148
|
-
legacyScorecardAlt = (0, score_1.thunkScorecard)(newScorecard, bLog);
|
|
149
|
-
// Add minority notes
|
|
150
|
-
legacyScorecardAlt.minority.details['majorityMinority'] = M.getMajorityMinority(this);
|
|
151
|
-
legacyScorecardAlt.minority.details['vraPreclearance'] = M.getVRASection5(this);
|
|
152
|
-
this._scorecard = legacyScorecardAlt;
|
|
153
|
-
// Before returning, create a dummy population deviation test, for
|
|
154
|
-
// doHasEqualPopulations() to use later.This is preserving the old calling sequence.
|
|
155
|
-
let test = this.getTest(4 /* T.Test.PopulationDeviation */);
|
|
156
|
-
// Get the raw population deviation
|
|
157
|
-
const popDev = this._scorecard.populationDeviation.raw;
|
|
158
|
-
// Populate the test entry
|
|
159
|
-
test['score'] = popDev;
|
|
160
|
-
test['details'] = { 'maxDeviation': this._scorecard.populationDeviation.notes['maxDeviation'] };
|
|
161
|
-
// Populate the N+1 summary "district" in district.statistics
|
|
162
|
-
let totalPop = this.districts.table.totalPop;
|
|
163
|
-
let popDevPct = this.districts.table.popDevPct;
|
|
164
|
-
let totalVAP = this.districts.table.totalVAP;
|
|
165
|
-
const summaryRow = this.districts.numberOfRows() - 1;
|
|
166
|
-
totalPop[summaryRow] = this._profile.population.targetSize; // MMD - This is generalized when the profile is created.
|
|
167
|
-
popDevPct[summaryRow] = popDev;
|
|
168
|
-
totalVAP[summaryRow] = Math.round(totalVAP[summaryRow] / this._profile.nDistricts);
|
|
169
|
-
// Added w/ new scorecard
|
|
170
|
-
// Use 'roughly' equal population from dra-analytics
|
|
171
|
-
let test2 = this.getTest(3 /* T.Test.EqualPopulation */);
|
|
172
|
-
test2['score'] = newScorecard.populationDeviation.roughlyEqual;
|
|
173
|
-
// END main analytics
|
|
174
|
-
(0, results_1.doAnalyzePostProcessing)(this, bLog);
|
|
75
|
+
this.bLog = bLog;
|
|
76
|
+
(0, analyze_1.newAnalyzePlan)(this);
|
|
175
77
|
}
|
|
176
78
|
catch (e) {
|
|
177
79
|
console.log("Exception caught by analyzePlan().");
|
|
@@ -180,108 +82,223 @@ class AnalyticsSession {
|
|
|
180
82
|
}
|
|
181
83
|
return true;
|
|
182
84
|
}
|
|
183
|
-
getGoodShapes() {
|
|
184
|
-
const rawShapes = this.districts.getDistrictShapes();
|
|
185
|
-
// Filter the real shapes & throw everything else away
|
|
186
|
-
let goodShapes = {};
|
|
187
|
-
goodShapes['type'] = "FeatureCollection";
|
|
188
|
-
goodShapes['features'] = [];
|
|
189
|
-
for (let i = 0; i < rawShapes.features.length; i++) {
|
|
190
|
-
const shape = rawShapes.features[i];
|
|
191
|
-
if (isAShape(shape)) {
|
|
192
|
-
const d = baseclient_1.Poly.polyDescribe(shape);
|
|
193
|
-
let f = {
|
|
194
|
-
type: 'Feature',
|
|
195
|
-
properties: { districtID: `${i + 1}` },
|
|
196
|
-
geometry: {
|
|
197
|
-
type: (d.npoly > 1) ? 'MultiPolygon' : 'Polygon',
|
|
198
|
-
coordinates: shape.geometry.coordinates
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
goodShapes.features.push(f);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return goodShapes;
|
|
205
|
-
}
|
|
206
|
-
// 11-03-2020 - Added for racially polarized voting analysis
|
|
207
|
-
// NOTE - This assumes that analyzePlan() has been run!
|
|
208
85
|
analyzeRacialPolarization(districtID, groups, bLog = false) {
|
|
209
|
-
return (0,
|
|
210
|
-
}
|
|
211
|
-
// NOTE - This assumes that analyzePlan() has been run!
|
|
212
|
-
getDistrictStatistics(bLog = false) {
|
|
213
|
-
return (0, results_2.prepareDistrictStatistics)(this, bLog);
|
|
86
|
+
return (0, rpv_1.doRPV)(this.data, districtID, groups, this.bLog);
|
|
214
87
|
}
|
|
88
|
+
// NOTE - These methods assume that analyzePlan() has been run.
|
|
215
89
|
getPlanProfile(bLog = false) {
|
|
216
|
-
return this.
|
|
90
|
+
return U.deepCopy(this.data.profile);
|
|
217
91
|
}
|
|
218
92
|
getPlanScorecard(bLog = false) {
|
|
219
|
-
return this.
|
|
93
|
+
return U.deepCopy(this.data.legacyScorecardAlt);
|
|
220
94
|
}
|
|
221
95
|
getRatings(bLog = false) {
|
|
222
|
-
|
|
223
|
-
const r = {
|
|
224
|
-
proportionality: scorecard.partisan.bias.score,
|
|
225
|
-
competitiveness: scorecard.partisan.responsiveness.score,
|
|
226
|
-
minorityRights: scorecard.minority.score,
|
|
227
|
-
compactness: scorecard.compactness.score,
|
|
228
|
-
splitting: scorecard.splitting.score
|
|
229
|
-
};
|
|
230
|
-
return r;
|
|
96
|
+
return U.deepCopy(this.data.ratings);
|
|
231
97
|
}
|
|
232
|
-
// NOTE - This assumes that analyzePlan() has been run!
|
|
233
98
|
getRequirementsChecklist(bLog = false) {
|
|
234
|
-
return
|
|
235
|
-
}
|
|
236
|
-
// NOTE - This assumes that analyzePlan() has been run!
|
|
237
|
-
getDiscontiguousDistrictFeatures(bLog = false) {
|
|
238
|
-
// Get the (possibly empty) list of discontiguous district IDs
|
|
239
|
-
const contiguousTest = this.getTest(1 /* T.Test.Contiguous */);
|
|
240
|
-
const discontiguousDistrictIDs = contiguousTest['details']['discontiguousDistricts'] || [];
|
|
241
|
-
// Convert them into a (possibly empty) list of features
|
|
242
|
-
let discontiguousDistrictFeatures = { type: 'FeatureCollection', features: [] };
|
|
243
|
-
if (!(U.isArrayEmpty(discontiguousDistrictIDs))) {
|
|
244
|
-
for (let id of discontiguousDistrictIDs) {
|
|
245
|
-
let poly = this.districts.getDistrictShapeByID(id);
|
|
246
|
-
if (poly) {
|
|
247
|
-
// If a district has a shape & it is not contiguous, by definition,
|
|
248
|
-
// it will be a Multipolygon, i.e., it will have multiple pieces, some
|
|
249
|
-
// possibly embedded w/in other districts. Get & add all the pieces.
|
|
250
|
-
const districtParts = baseclient_1.Poly.polyParts(poly);
|
|
251
|
-
discontiguousDistrictFeatures.features.push(...districtParts.features);
|
|
252
|
-
// discontiguousDistrictFeatures.features.push(poly);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return discontiguousDistrictFeatures;
|
|
257
|
-
}
|
|
258
|
-
// Comments clipped from dra-client geodistrict.ts.
|
|
259
|
-
// Discontiguous polygons are:
|
|
260
|
-
// 1. All polygons in a multi-polygon; and
|
|
261
|
-
// 2. All holes in a otherwise cohesive polygon.
|
|
262
|
-
// Note that all non-cohesive features are always simple polygons.
|
|
263
|
-
// HELPERS USED INTERNALLY
|
|
264
|
-
// Get an individual test, so you can drive UI with the results.
|
|
265
|
-
getTest(testID) {
|
|
266
|
-
// Get the existing test entries
|
|
267
|
-
// T.Test is a numeric enum, so convert the string keys to numbers
|
|
268
|
-
let testValues = U.getNumericObjectKeys(this.tests);
|
|
269
|
-
// If there's no entry for this test, create & initialize one
|
|
270
|
-
if (!(testValues.includes(testID))) {
|
|
271
|
-
this.tests[testID] = {};
|
|
272
|
-
this.tests[testID]['score'] = undefined;
|
|
273
|
-
this.tests[testID]['details'] = {};
|
|
274
|
-
}
|
|
275
|
-
// Return a pointer to the the test entry for this test
|
|
276
|
-
return this.tests[testID];
|
|
99
|
+
return U.deepCopy(this.data.checklist);
|
|
277
100
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const threshold = dra_analytics_1.Rate.popdevThreshold(this.legislativeDistricts);
|
|
281
|
-
return threshold;
|
|
101
|
+
getDistrictStatistics(bLog = false) {
|
|
102
|
+
return U.deepCopy(this.data.statistics);
|
|
282
103
|
}
|
|
283
104
|
}
|
|
284
105
|
exports.AnalyticsSession = AnalyticsSession;
|
|
106
|
+
// END
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
/***/ }),
|
|
110
|
+
|
|
111
|
+
/***/ "./src/analyze.ts":
|
|
112
|
+
/*!************************!*\
|
|
113
|
+
!*** ./src/analyze.ts ***!
|
|
114
|
+
\************************/
|
|
115
|
+
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
//
|
|
119
|
+
// TOP-LEVEL ANALYTICS FUNCTIONS
|
|
120
|
+
// re-factored here to keep _api.ts small
|
|
121
|
+
//
|
|
122
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
123
|
+
if (k2 === undefined) k2 = k;
|
|
124
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
125
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
126
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
127
|
+
}
|
|
128
|
+
Object.defineProperty(o, k2, desc);
|
|
129
|
+
}) : (function(o, m, k, k2) {
|
|
130
|
+
if (k2 === undefined) k2 = k;
|
|
131
|
+
o[k2] = m[k];
|
|
132
|
+
}));
|
|
133
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
134
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
135
|
+
}) : function(o, v) {
|
|
136
|
+
o["default"] = v;
|
|
137
|
+
});
|
|
138
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
139
|
+
var ownKeys = function(o) {
|
|
140
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
141
|
+
var ar = [];
|
|
142
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
143
|
+
return ar;
|
|
144
|
+
};
|
|
145
|
+
return ownKeys(o);
|
|
146
|
+
};
|
|
147
|
+
return function (mod) {
|
|
148
|
+
if (mod && mod.__esModule) return mod;
|
|
149
|
+
var result = {};
|
|
150
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
151
|
+
__setModuleDefault(result, mod);
|
|
152
|
+
return result;
|
|
153
|
+
};
|
|
154
|
+
})();
|
|
155
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
156
|
+
exports.newAnalyzePlan = newAnalyzePlan;
|
|
157
|
+
const baseclient_1 = __webpack_require__(/*! @dra2020/baseclient */ "@dra2020/baseclient");
|
|
158
|
+
const dra_analytics_1 = __webpack_require__(/*! @dra2020/dra-analytics */ "@dra2020/dra-analytics");
|
|
159
|
+
const preprocess_1 = __webpack_require__(/*! ./preprocess */ "./src/preprocess.ts");
|
|
160
|
+
const score_1 = __webpack_require__(/*! ./score */ "./src/score.ts");
|
|
161
|
+
const requirements_1 = __webpack_require__(/*! ./requirements */ "./src/requirements.ts");
|
|
162
|
+
const cohesive_1 = __webpack_require__(/*! ./cohesive */ "./src/cohesive.ts");
|
|
163
|
+
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
164
|
+
function newAnalyzePlan(s) {
|
|
165
|
+
if (U.isObjectEmpty(s.data.plan._planByGeoID))
|
|
166
|
+
return false;
|
|
167
|
+
(0, preprocess_1.initialize)(s);
|
|
168
|
+
s.data.aggregates.process();
|
|
169
|
+
s.data.profile = (0, score_1.makeProfile)(s.data);
|
|
170
|
+
s.data.newScorecard = (0, score_1.computeMetrics)(s.data.profile, getGoodShapes(s.data.districts.getShapes()), false /* s.bLog */);
|
|
171
|
+
s.data.newScorecard = (0, score_1.rateKeyDimensions)(s.data.newScorecard, s.data.profile, false /* s.bLog */);
|
|
172
|
+
s.data.legacyScorecardAlt = (0, score_1.thunkScorecard)(s.data.newScorecard, false /* s.bLog */);
|
|
173
|
+
s.data.legacyScorecardAlt.minority.details['majorityMinority'] = {};
|
|
174
|
+
s.data.legacyScorecardAlt.minority.details['vraPreclearance'] = "";
|
|
175
|
+
_add_details(s);
|
|
176
|
+
s.data.ratings = _extractRatings(s);
|
|
177
|
+
s.data.checklist = _makeRequirementsChecklist(s);
|
|
178
|
+
s.data.statistics = _makeDistrictStatistics(s);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
function _add_details(s) {
|
|
182
|
+
const scorecard = s.data.legacyScorecardAlt;
|
|
183
|
+
const datasets = {
|
|
184
|
+
shapes: U.deepCopy(s.data.config['descriptions']['SHAPES']),
|
|
185
|
+
census: U.deepCopy(s.data.config['descriptions']['CENSUS']),
|
|
186
|
+
vap: U.deepCopy(s.data.config['descriptions']['VAP']),
|
|
187
|
+
election: U.deepCopy(s.data.config['descriptions']['ELECTION'])
|
|
188
|
+
};
|
|
189
|
+
scorecard.partisan.details['election'] = datasets.election;
|
|
190
|
+
scorecard.minority.details['vap'] = datasets.vap;
|
|
191
|
+
scorecard.details['shapes'] = datasets.shapes;
|
|
192
|
+
scorecard.details['census'] = datasets.census;
|
|
193
|
+
const simpleSplits = (0, cohesive_1.findCountiesSplitUnexpectedly)(s.data, false /* s.bLog */);
|
|
194
|
+
scorecard.splitting.details['unexpectedAffected'] = simpleSplits['score'];
|
|
195
|
+
scorecard.splitting.details['nSplits'] = simpleSplits['details']['nSplits'];
|
|
196
|
+
scorecard.splitting.details['countiesSplitUnexpectedly'] = U.deepCopy(simpleSplits['details']['countiesSplitUnexpectedly']);
|
|
197
|
+
scorecard.splitting.details['nTooBigCounties'] = simpleSplits['details']['expectedSplits'];
|
|
198
|
+
scorecard.splitting.details['tooBigName'] = simpleSplits['details']['tooBigName'];
|
|
199
|
+
scorecard.splitting.details['nCountiesSplit'] = simpleSplits['details']['nCountiesSplit'];
|
|
200
|
+
scorecard.splitting.details['nSingleCountyDistricts'] = simpleSplits['details']['nSingleCountyDistricts'];
|
|
201
|
+
scorecard.splitting.details['singleCountyDistrictMax'] = simpleSplits['details']['singleCountyDistrictMax'];
|
|
202
|
+
scorecard.splitting.details['countiesWithSplits'] = simpleSplits['details']['countiesWithSplits'];
|
|
203
|
+
}
|
|
204
|
+
function _makeRequirementsChecklist(s) {
|
|
205
|
+
var _a;
|
|
206
|
+
const popByDistricts = s.data.aggregates.columns.totalPop;
|
|
207
|
+
const completeness = (0, requirements_1.isComplete)(popByDistricts);
|
|
208
|
+
const bContiguousByDistrict = s.data.aggregates.columns.bContiguous;
|
|
209
|
+
const discontiguousDistrictsList = U.falseIndexes(bContiguousByDistrict);
|
|
210
|
+
const contiguity = bContiguousByDistrict.at(-1);
|
|
211
|
+
const bNotEmbeddedByDistrict = s.data.aggregates.columns.bNotEmbedded;
|
|
212
|
+
const embeddedDistrictsList = U.falseIndexes(bNotEmbeddedByDistrict);
|
|
213
|
+
const freeOfHoles = bNotEmbeddedByDistrict.at(-1);
|
|
214
|
+
const roughlyEqual = (_a = s.data.newScorecard) === null || _a === void 0 ? void 0 : _a.populationDeviation.roughlyEqual;
|
|
215
|
+
const reqScore = completeness && contiguity && freeOfHoles && roughlyEqual;
|
|
216
|
+
// Get values to support details/notes entries in dra-client
|
|
217
|
+
const unassignedFeaturesDetail = (0, requirements_1.getUnassignedFeatures)(s.data);
|
|
218
|
+
const emptyDistrictsDetail = (0, requirements_1.emptyDistricts)(popByDistricts);
|
|
219
|
+
const discontiguousDistrictsDetail = discontiguousDistrictsList;
|
|
220
|
+
const embeddedDistrictsDetail = embeddedDistrictsList;
|
|
221
|
+
const newScorecard = s.data.newScorecard;
|
|
222
|
+
const populationDeviationDetail = newScorecard.populationDeviation.deviation;
|
|
223
|
+
const deviationThresholdDetail = U.trim(dra_analytics_1.Rate.popdevThreshold(s.data.legislativeDistricts));
|
|
224
|
+
const census = U.deepCopy(s.data.config['descriptions']['CENSUS']);
|
|
225
|
+
// Populate the checklist
|
|
226
|
+
let paRequirements = {
|
|
227
|
+
score: reqScore,
|
|
228
|
+
metrics: {
|
|
229
|
+
complete: completeness,
|
|
230
|
+
contiguous: contiguity,
|
|
231
|
+
freeOfHoles: freeOfHoles,
|
|
232
|
+
equalPopulation: roughlyEqual
|
|
233
|
+
},
|
|
234
|
+
details: {
|
|
235
|
+
unassignedFeatures: unassignedFeaturesDetail,
|
|
236
|
+
emptyDistricts: emptyDistrictsDetail,
|
|
237
|
+
discontiguousDistricts: discontiguousDistrictsDetail,
|
|
238
|
+
embeddedDistricts: embeddedDistrictsDetail,
|
|
239
|
+
populationDeviation: populationDeviationDetail,
|
|
240
|
+
deviationThreshold: deviationThresholdDetail
|
|
241
|
+
},
|
|
242
|
+
datasets: {
|
|
243
|
+
census: census,
|
|
244
|
+
},
|
|
245
|
+
resources: {
|
|
246
|
+
// stateReqs: reqLinkToStateReqs
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
return paRequirements;
|
|
250
|
+
}
|
|
251
|
+
function _extractRatings(s) {
|
|
252
|
+
const scorecard = s.data.legacyScorecardAlt;
|
|
253
|
+
const r = {
|
|
254
|
+
proportionality: scorecard.partisan.bias.score,
|
|
255
|
+
competitiveness: scorecard.partisan.responsiveness.score,
|
|
256
|
+
minorityRights: scorecard.minority.score,
|
|
257
|
+
compactness: scorecard.compactness.score,
|
|
258
|
+
splitting: scorecard.splitting.score
|
|
259
|
+
};
|
|
260
|
+
s.data.ratings = r;
|
|
261
|
+
return r;
|
|
262
|
+
}
|
|
263
|
+
function _makeDistrictStatistics(s) {
|
|
264
|
+
const table = s.data.aggregates.transpose();
|
|
265
|
+
const datasets = {
|
|
266
|
+
shapes: U.deepCopy(s.data.config['descriptions']['SHAPES']),
|
|
267
|
+
census: U.deepCopy(s.data.config['descriptions']['CENSUS']),
|
|
268
|
+
vap: U.deepCopy(s.data.config['descriptions']['VAP']),
|
|
269
|
+
election: U.deepCopy(s.data.config['descriptions']['ELECTION'])
|
|
270
|
+
};
|
|
271
|
+
const statistics = {
|
|
272
|
+
table: table,
|
|
273
|
+
details: {}, // None at this time
|
|
274
|
+
datasets: datasets,
|
|
275
|
+
resources: {} // None at this time
|
|
276
|
+
};
|
|
277
|
+
return statistics;
|
|
278
|
+
}
|
|
279
|
+
// HELPERS
|
|
280
|
+
function getGoodShapes(rawShapes) {
|
|
281
|
+
// Filter the real shapes & throw everything else away
|
|
282
|
+
let goodShapes = {};
|
|
283
|
+
goodShapes['type'] = "FeatureCollection";
|
|
284
|
+
goodShapes['features'] = [];
|
|
285
|
+
for (let i = 0; i < rawShapes.features.length; i++) {
|
|
286
|
+
const shape = rawShapes.features[i];
|
|
287
|
+
if (isAShape(shape)) {
|
|
288
|
+
const d = baseclient_1.Poly.polyDescribe(shape);
|
|
289
|
+
let f = {
|
|
290
|
+
type: 'Feature',
|
|
291
|
+
properties: { districtID: `${i + 1}` },
|
|
292
|
+
geometry: {
|
|
293
|
+
type: (d.npoly > 1) ? 'MultiPolygon' : 'Polygon',
|
|
294
|
+
coordinates: shape.geometry.coordinates
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
goodShapes.features.push(f);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return goodShapes;
|
|
301
|
+
}
|
|
285
302
|
function isAShape(poly) {
|
|
286
303
|
if (poly == null)
|
|
287
304
|
return false;
|
|
@@ -289,19 +306,21 @@ function isAShape(poly) {
|
|
|
289
306
|
return false;
|
|
290
307
|
return poly.geometry && poly.geometry.coordinates && !U.isArrayEmpty(poly.geometry.coordinates);
|
|
291
308
|
}
|
|
309
|
+
// END
|
|
292
310
|
|
|
293
311
|
|
|
294
312
|
/***/ }),
|
|
295
313
|
|
|
296
|
-
/***/ "./src/
|
|
297
|
-
|
|
298
|
-
!*** ./src/
|
|
299
|
-
|
|
314
|
+
/***/ "./src/cohesive.ts":
|
|
315
|
+
/*!*************************!*\
|
|
316
|
+
!*** ./src/cohesive.ts ***!
|
|
317
|
+
\*************************/
|
|
300
318
|
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
301
319
|
|
|
302
320
|
|
|
303
321
|
//
|
|
304
|
-
//
|
|
322
|
+
// ANALYZE SIMPLE COUNTY SPLITTING
|
|
323
|
+
// The main county-district splitting code is in the dra-analytics package.
|
|
305
324
|
//
|
|
306
325
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
307
326
|
if (k2 === undefined) k2 = k;
|
|
@@ -337,652 +356,487 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
337
356
|
};
|
|
338
357
|
})();
|
|
339
358
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
340
|
-
exports.
|
|
341
|
-
exports.inferSelectedMinority = inferSelectedMinority;
|
|
342
|
-
exports.geoIDForFeature = geoIDForFeature;
|
|
343
|
-
exports.fieldForFeature = fieldForFeature;
|
|
344
|
-
exports.invertPlan = invertPlan;
|
|
345
|
-
const dra_analytics_1 = __webpack_require__(/*! @dra2020/dra-analytics */ "@dra2020/dra-analytics");
|
|
346
|
-
const T = __importStar(__webpack_require__(/*! ./types */ "./src/types.ts"));
|
|
359
|
+
exports.findCountiesSplitUnexpectedly = findCountiesSplitUnexpectedly;
|
|
347
360
|
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
nativePop: U.initArray(nRows, null),
|
|
412
|
-
whitePct: U.initArray(nRows, null),
|
|
413
|
-
minorityPct: U.initArray(nRows, null),
|
|
414
|
-
blackPct: U.initArray(nRows, null),
|
|
415
|
-
hispanicPct: U.initArray(nRows, null),
|
|
416
|
-
pacificPct: U.initArray(nRows, null),
|
|
417
|
-
asianPct: U.initArray(nRows, null),
|
|
418
|
-
nativePct: U.initArray(nRows, null)
|
|
419
|
-
};
|
|
420
|
-
return t;
|
|
421
|
-
}
|
|
422
|
-
// MMD - Generalized district statistics for MMD's
|
|
423
|
-
// This is the workhorse computational routine!
|
|
424
|
-
recalcStatistics(bLog = false) {
|
|
425
|
-
// Initialize debug counters
|
|
426
|
-
nMissingDataset = 0;
|
|
427
|
-
nMissingProperty = 0;
|
|
428
|
-
// MMD - Generalized targetSize
|
|
429
|
-
// Compute these once per recalc cycle
|
|
430
|
-
const nDistricts = this._session.state.nDistricts;
|
|
431
|
-
const repsByDistrict = this._session.repsByDistrict;
|
|
432
|
-
const nReps = this._session.state.nReps;
|
|
433
|
-
const stateTotal = this._session.state.totalPop;
|
|
434
|
-
const targetSize = this._session.state.targetSize;
|
|
435
|
-
// const targetSize = Math.round(this._session.state.totalPop / this._session.state.nDistricts);
|
|
436
|
-
const deviationThreshold = this._session.populationDeviationThreshold();
|
|
437
|
-
// let planByDistrict = this._session.plan.byDistrictID();
|
|
438
|
-
const plan = this._session.plan;
|
|
439
|
-
const graph = this._session.graph;
|
|
440
|
-
// Add an extra 0th virtual county bucket for county-district splitting analysis
|
|
441
|
-
let nCountyBuckets = this._session.counties.nCounties + 1;
|
|
442
|
-
// INITIALIZE STATE VALUES THAT WILL BE ACCUMULATED
|
|
443
|
-
// 10-22-2020 - Added Other votes
|
|
444
|
-
let stateDemVote = 0;
|
|
445
|
-
let stateRepVote = 0;
|
|
446
|
-
let stateOthVote = 0;
|
|
447
|
-
let stateTotVote = 0;
|
|
448
|
-
// let stateTPVote = 0;
|
|
449
|
-
let stateVAPPop = 0;
|
|
450
|
-
let stateWhitePop = 0;
|
|
451
|
-
let stateMinorityPop = 0;
|
|
452
|
-
let stateBlackPop = 0;
|
|
453
|
-
let stateHispanicPop = 0;
|
|
454
|
-
let statePacificPop = 0;
|
|
455
|
-
let stateAsianPop = 0;
|
|
456
|
-
let stateNativePop = 0;
|
|
457
|
-
// NOTE - These plan-level booleans are set in their respective analytics:
|
|
458
|
-
// - Equal population (bEqualPop)
|
|
459
|
-
// - Complete (bNotEmpty)
|
|
460
|
-
// - Contiguous (bContiguous)
|
|
461
|
-
// - Free of holes (bNotEmbedded)
|
|
462
|
-
// Loop over the districts (including the dummy unassigned one)
|
|
463
|
-
for (let i = 0; i < this.numberOfWorkingDistricts(); i++) {
|
|
464
|
-
// INITIALIZE DISTRICT VALUES THAT WILL BE ACCUMULATED (VS. DERIVED)
|
|
465
|
-
let featurePop;
|
|
466
|
-
let totalPop = 0;
|
|
467
|
-
let countySplits = U.initArray(nCountyBuckets, 0);
|
|
468
|
-
// 10-22-2020 - Added Other votes
|
|
469
|
-
let demVotes = 0;
|
|
470
|
-
let repVotes = 0;
|
|
471
|
-
let othVotes = 0;
|
|
472
|
-
let totVotes = 0;
|
|
473
|
-
let totalVAP = 0;
|
|
474
|
-
let whitePop = 0;
|
|
475
|
-
let blackPop = 0;
|
|
476
|
-
let hispanicPop = 0;
|
|
477
|
-
let pacificPop = 0;
|
|
478
|
-
let asianPop = 0;
|
|
479
|
-
let nativePop = 0;
|
|
480
|
-
// NOTE - Only report explicitly found validity issues
|
|
481
|
-
let bNotEmpty = false;
|
|
482
|
-
let bContiguous = true;
|
|
483
|
-
let bNotEmbedded = true;
|
|
484
|
-
let bEqualPop = true;
|
|
485
|
-
// HACK - Because "this" gets ghosted inside the forEach loop below
|
|
486
|
-
let outerThis = this;
|
|
487
|
-
// Default the pop dev % for the dummy Unassigned district to 0%.
|
|
488
|
-
// Default the pop dev % for real (1–N) but empty districts to 100%.
|
|
489
|
-
let popDevPct = (i > 0) ? (targetSize / targetSize) : 0 / targetSize;
|
|
490
|
-
// Get the geoIDs assigned to the district
|
|
491
|
-
// Guard against empty districts
|
|
492
|
-
let geoIDs = this._session.plan.geoIDsForDistrictID(i);
|
|
493
|
-
if (geoIDs && (geoIDs.size > 0)) {
|
|
494
|
-
bNotEmpty = true;
|
|
495
|
-
// ... loop over the geoIDs creating district-by-district statistics
|
|
496
|
-
geoIDs.forEach(function (geoID) {
|
|
497
|
-
// 01-04-22 -- Removed water-only guard
|
|
498
|
-
// // Skip water-only features
|
|
499
|
-
// if (!(U.isWaterOnly(geoID)))
|
|
500
|
-
// {
|
|
501
|
-
// Map from geoID to feature index
|
|
502
|
-
let featureID = outerThis._session.features.featureID(geoID);
|
|
503
|
-
let f = outerThis._session.features.featureByIndex(featureID);
|
|
504
|
-
if (f == undefined) {
|
|
505
|
-
if (bLog)
|
|
506
|
-
console.log("Statistics: Skipping undefined feature in district statistics: GEOID =", geoID, "Feature ID =", featureID);
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
// ACCUMULATE VALUES
|
|
510
|
-
// Total population of each feature
|
|
511
|
-
// NOTE - This result is used more than once
|
|
512
|
-
// 03-27-21
|
|
513
|
-
const dkCENSUS = outerThis._session.features._keys["CENSUS" /* T.Dataset.CENSUS */];
|
|
514
|
-
featurePop = fieldForFeature(f, dkCENSUS, 0 /* T.FeatureField.TotalPop */);
|
|
515
|
-
// featurePop = outerThis._session.features.fieldForFeature(f, T.Dataset.CENSUS, T.FeatureField.TotalPop);
|
|
516
|
-
// Total district population
|
|
517
|
-
totalPop += featurePop;
|
|
518
|
-
// Ignore features when the county is unrecognized
|
|
519
|
-
const countyFIPS = U.parseGeoID(geoID)['county'];
|
|
520
|
-
if (U.keyExists(countyFIPS, outerThis._session.counties.index)) {
|
|
521
|
-
// Total population by counties w/in a district,
|
|
522
|
-
// except the dummy unassigned district 0
|
|
523
|
-
if (i > 0)
|
|
524
|
-
countySplits[outerThis.getCountyIndex(geoID)] += featurePop;
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
if (bLog)
|
|
528
|
-
console.log("Statistics: County not recognized:", geoID);
|
|
529
|
-
}
|
|
530
|
-
// Democratic and Republican vote totals
|
|
531
|
-
// 10-22-2020 - Added Other votes
|
|
532
|
-
// 10-24-2020 - Added guard against inconsistent election data
|
|
533
|
-
// 03-27-2021
|
|
534
|
-
const dkELECTION = outerThis._session.features._keys["ELECTION" /* T.Dataset.ELECTION */];
|
|
535
|
-
const featureDem = fieldForFeature(f, dkELECTION, 7 /* T.FeatureField.DemVotes */);
|
|
536
|
-
// const featureDem = outerThis._session.features.fieldForFeature(f, T.Dataset.ELECTION, T.FeatureField.DemVotes);
|
|
537
|
-
const featureRep = fieldForFeature(f, dkELECTION, 8 /* T.FeatureField.RepVotes */);
|
|
538
|
-
const featureTot = fieldForFeature(f, dkELECTION, 9 /* T.FeatureField.TotalVotes */);
|
|
539
|
-
demVotes += featureDem;
|
|
540
|
-
repVotes += featureRep;
|
|
541
|
-
totVotes += featureTot;
|
|
542
|
-
// NOTE: Unless you grab the values above before accumulating them,
|
|
543
|
-
// you can't accumulate othVotes for districts. You must calculate
|
|
544
|
-
// them by implication later.
|
|
545
|
-
if (bLog) {
|
|
546
|
-
const bBadElection = (featureDem + featureRep > featureTot) ? true : false;
|
|
547
|
-
if (bBadElection)
|
|
548
|
-
console.log("Statistics: Inconsistent election data for precinct:", geoID, featureDem, featureRep, featureTot);
|
|
549
|
-
}
|
|
550
|
-
// Voting-age demographic breakdowns (or citizen voting-age)
|
|
551
|
-
// 03-27-21
|
|
552
|
-
const dkVAP = outerThis._session.features._keys["VAP" /* T.Dataset.VAP */];
|
|
553
|
-
totalVAP += fieldForFeature(f, dkVAP, 0 /* T.FeatureField.TotalPop */);
|
|
554
|
-
// totalVAP += outerThis._session.features.fieldForFeature(f, T.Dataset.VAP, T.FeatureField.TotalPop);
|
|
555
|
-
whitePop += fieldForFeature(f, dkVAP, 1 /* T.FeatureField.WhitePop */);
|
|
556
|
-
blackPop += fieldForFeature(f, dkVAP, 2 /* T.FeatureField.BlackPop */);
|
|
557
|
-
hispanicPop += fieldForFeature(f, dkVAP, 3 /* T.FeatureField.HispanicPop */);
|
|
558
|
-
pacificPop += fieldForFeature(f, dkVAP, 5 /* T.FeatureField.PacificPop */);
|
|
559
|
-
asianPop += fieldForFeature(f, dkVAP, 4 /* T.FeatureField.AsianPop */);
|
|
560
|
-
nativePop += fieldForFeature(f, dkVAP, 6 /* T.FeatureField.NativePop */);
|
|
561
|
-
}
|
|
562
|
-
// }
|
|
563
|
-
// else
|
|
564
|
-
// {
|
|
565
|
-
// if (bLog) console.log("Statistics: Skipping water-only feature in district statistics:", geoID);
|
|
566
|
-
// }
|
|
567
|
-
});
|
|
568
|
-
//console.log(`totalVAP: ${totalVAP}, blackPop: ${blackPop}`);
|
|
569
|
-
// COMPUTE DERIVED VALUES
|
|
570
|
-
// MMD - Generalized the per-district population deviations's for MMD's with variable #'s of reps per district.
|
|
571
|
-
// - The real districts are indexed 1–N.
|
|
572
|
-
// - But the # reps per district are indexed from zero.
|
|
573
|
-
// Population deviation % and equal population (boolean) by district.
|
|
574
|
-
if (i > 0) {
|
|
575
|
-
if (totalPop > 0) {
|
|
576
|
-
const n = (repsByDistrict) ? repsByDistrict[i - 1] : 1; // MMD - # of reps for the district
|
|
577
|
-
popDevPct = (totalPop - (n * targetSize)) / (n * targetSize); // MMD
|
|
578
|
-
// popDevPct = (totalPop - targetSize) / targetSize;
|
|
579
|
-
bEqualPop = (Math.abs(popDevPct) <= deviationThreshold);
|
|
361
|
+
/*
|
|
362
|
+
|
|
363
|
+
Sample results for NC 2016 congressional plan
|
|
364
|
+
________________________________________________________________________________
|
|
365
|
+
|
|
366
|
+
State: NC
|
|
367
|
+
Census: 2010
|
|
368
|
+
Total population: 9,535,483
|
|
369
|
+
Number of districts: 13
|
|
370
|
+
Target district size: 733,499
|
|
371
|
+
Number of counties: 100
|
|
372
|
+
|
|
373
|
+
Equal Population: 11.24% deviation
|
|
374
|
+
Compactness: None
|
|
375
|
+
Proportionality: 27.67% gap
|
|
376
|
+
Cohesiveness: 11 unexpected splits, affecting 27.14% of the total population
|
|
377
|
+
|
|
378
|
+
These counties are split unexpectedly (meaning that they're smaller than a district):
|
|
379
|
+
|
|
380
|
+
• Bladen
|
|
381
|
+
• Buncombe
|
|
382
|
+
• Catawba
|
|
383
|
+
• Cumberland
|
|
384
|
+
• Durham
|
|
385
|
+
• Guilford
|
|
386
|
+
• Iredell
|
|
387
|
+
• Johnston
|
|
388
|
+
• Pitt
|
|
389
|
+
• Rowan
|
|
390
|
+
• Wilson
|
|
391
|
+
|
|
392
|
+
*/
|
|
393
|
+
// This analysis *assumes* that a map is complete (or nearly so).
|
|
394
|
+
// - We do the analysis regardless, but
|
|
395
|
+
// - Protect against weird results in the client
|
|
396
|
+
function findCountiesSplitUnexpectedly(container, bLog = false) {
|
|
397
|
+
// Simulate an old school test entry, to preserve this and downstream code
|
|
398
|
+
let test = {};
|
|
399
|
+
test['score'] = undefined;
|
|
400
|
+
test['details'] = {};
|
|
401
|
+
// THE THREE VALUES TO DETERMINE FOR A PLAN
|
|
402
|
+
let unexpectedSplits = 0;
|
|
403
|
+
let unexpectedAffected = 0;
|
|
404
|
+
let countiesSplitUnexpectedly = [];
|
|
405
|
+
// FIRST, ANALYZE THE COUNTY SPLITTING FOR THE PLAN
|
|
406
|
+
// Get the county-district pivot ("splits")
|
|
407
|
+
let CxD = container.CxD;
|
|
408
|
+
// NOTE - Unlike the legacy code that uses the district statistics table with extra virtual districts & counties,
|
|
409
|
+
// this CxD matrix just has the right number that are zero-indexed.
|
|
410
|
+
// Find the single-county districts, i.e., districts NOT split across counties.
|
|
411
|
+
// Ignore the dummy unassigned 0 and N+1 summary "districts."
|
|
412
|
+
let singleCountyDistricts = [];
|
|
413
|
+
for (let d = 0; d < container.state.nDistricts; d++) {
|
|
414
|
+
// See if there's only one county partition
|
|
415
|
+
// Ignore the dummy unassigned 0 "county."
|
|
416
|
+
let nCountiesInDistrict = 0;
|
|
417
|
+
for (let c = 0; c < container.counties.nCounties; c++) {
|
|
418
|
+
// Guard against empty district
|
|
419
|
+
if (CxD[d]) {
|
|
420
|
+
if (CxD[d][c] > 0) {
|
|
421
|
+
nCountiesInDistrict += 1;
|
|
422
|
+
if (nCountiesInDistrict > 1) {
|
|
423
|
+
break;
|
|
580
424
|
}
|
|
581
425
|
}
|
|
582
|
-
// 10-22-2020 - Added Other votes. Revised from two-party to include Other.
|
|
583
|
-
// let totVotes: number; <<< Now being accumulated by district
|
|
584
|
-
let demPct = 0;
|
|
585
|
-
let repPct = 0;
|
|
586
|
-
let othPct = 0;
|
|
587
|
-
let DemSeat = 0;
|
|
588
|
-
// totVotes now being accumulated by district
|
|
589
|
-
if (totVotes > 0) {
|
|
590
|
-
// 10-24-2020 - Added guard against inconsistent election data.
|
|
591
|
-
const bBadElection = (demVotes + repVotes > totVotes) ? true : false;
|
|
592
|
-
if (bBadElection && bLog)
|
|
593
|
-
console.log("Statistics: Inconsistent election data for district:", i, demVotes, repVotes, totVotes);
|
|
594
|
-
if (bBadElection)
|
|
595
|
-
totVotes = demVotes + repVotes; // HACK to guard against bad election data!
|
|
596
|
-
othVotes = totVotes - demVotes - repVotes;
|
|
597
|
-
demPct = demVotes / totVotes;
|
|
598
|
-
repPct = repVotes / totVotes;
|
|
599
|
-
othPct = othVotes / totVotes;
|
|
600
|
-
DemSeat = dra_analytics_2.Partisan.fptpWin(demPct);
|
|
601
|
-
}
|
|
602
|
-
// Total minority VAP
|
|
603
|
-
let minorityPop = totalVAP - whitePop;
|
|
604
|
-
// Voting-age demographic proportions (or citizen voting-age)
|
|
605
|
-
let whitePct = 0;
|
|
606
|
-
let minorityPct = 0;
|
|
607
|
-
let blackPct = 0;
|
|
608
|
-
let hispanicPct = 0;
|
|
609
|
-
let pacificPct = 0;
|
|
610
|
-
let asianPct = 0;
|
|
611
|
-
let nativePct = 0;
|
|
612
|
-
if (totalVAP > 0) {
|
|
613
|
-
whitePct = whitePop / totalVAP;
|
|
614
|
-
minorityPct = minorityPop / totalVAP;
|
|
615
|
-
blackPct = blackPop / totalVAP;
|
|
616
|
-
hispanicPct = hispanicPop / totalVAP;
|
|
617
|
-
pacificPct = pacificPop / totalVAP;
|
|
618
|
-
asianPct = asianPop / totalVAP;
|
|
619
|
-
nativePct = nativePop / totalVAP;
|
|
620
|
-
}
|
|
621
|
-
// COMPUTE DISTRICT-LEVEL VALUES
|
|
622
|
-
// Validations
|
|
623
|
-
// Leave the default values for the dummy unassigned district,
|
|
624
|
-
// and districts that are empty.
|
|
625
|
-
if ((i > 0) && bNotEmpty) {
|
|
626
|
-
// GRAPH - 10/05/2020 - Using dra-graph instead.
|
|
627
|
-
// bContiguous = isConnected(geoIDs, graph);
|
|
628
|
-
// bNotEmbedded = (!isEmbedded(i, planByDistrict[i], plan, graph));
|
|
629
|
-
// GRAPH - 10/05/2020 - Using dra-graph.
|
|
630
|
-
const features = geoIDs;
|
|
631
|
-
const nakedGraph = graph._graph;
|
|
632
|
-
const nakedPlan = plan._planByGeoID;
|
|
633
|
-
bContiguous = dra_analytics_1.Graph.isConnected(features, nakedGraph, bLog);
|
|
634
|
-
bNotEmbedded = (!dra_analytics_1.Graph.isEmbedded(i, features, nakedPlan, nakedGraph, bLog));
|
|
635
|
-
}
|
|
636
|
-
{ // UPDATE THE DISTRICT STATISTICS
|
|
637
|
-
// NOTE - These are set below for both non-empty & empty districts:
|
|
638
|
-
// * bNotEmpty;
|
|
639
|
-
// * bContiguous;
|
|
640
|
-
// * bNotEmbedded;
|
|
641
|
-
// * totalPop;
|
|
642
|
-
// * bEqualPop;
|
|
643
|
-
this.table.popDevPct[i] = popDevPct;
|
|
644
|
-
this.table.demVotes[i] = demVotes;
|
|
645
|
-
this.table.repVotes[i] = repVotes;
|
|
646
|
-
this.table.otherVotes[i] = othVotes;
|
|
647
|
-
this.table.demPct[i] = demPct;
|
|
648
|
-
this.table.repPct[i] = repPct;
|
|
649
|
-
this.table.otherPct[i] = othPct;
|
|
650
|
-
this.table.demSeat[i] = DemSeat;
|
|
651
|
-
this.table.whitePop[i] = whitePop;
|
|
652
|
-
this.table.minorityPop[i] = minorityPop;
|
|
653
|
-
this.table.blackPop[i] = blackPop;
|
|
654
|
-
this.table.hispanicPop[i] = hispanicPop;
|
|
655
|
-
this.table.pacificPop[i] = pacificPop;
|
|
656
|
-
this.table.asianPop[i] = asianPop;
|
|
657
|
-
this.table.nativePop[i] = nativePop;
|
|
658
|
-
this.table.totalVAP[i] = totalVAP;
|
|
659
|
-
this.table.whitePct[i] = whitePct;
|
|
660
|
-
this.table.minorityPct[i] = minorityPct;
|
|
661
|
-
this.table.blackPct[i] = blackPct;
|
|
662
|
-
this.table.hispanicPct[i] = hispanicPct;
|
|
663
|
-
this.table.pacificPct[i] = pacificPct;
|
|
664
|
-
this.table.asianPct[i] = asianPct;
|
|
665
|
-
this.table.nativePct[i] = nativePct;
|
|
666
|
-
}
|
|
667
|
-
{ // ACCUMULATE STATE STATISTICS FROM DISTRICT TOTALS
|
|
668
|
-
// 10-22-2020 - Added Other votes
|
|
669
|
-
stateTotVote += totVotes;
|
|
670
|
-
// stateTPVote += totVotes;
|
|
671
|
-
stateDemVote += demVotes;
|
|
672
|
-
stateRepVote += repVotes;
|
|
673
|
-
stateOthVote += othVotes;
|
|
674
|
-
stateVAPPop += totalVAP;
|
|
675
|
-
stateWhitePop += whitePop;
|
|
676
|
-
stateMinorityPop += minorityPop;
|
|
677
|
-
stateBlackPop += blackPop;
|
|
678
|
-
stateHispanicPop += hispanicPop;
|
|
679
|
-
statePacificPop += pacificPop;
|
|
680
|
-
stateAsianPop += asianPop;
|
|
681
|
-
stateNativePop += nativePop;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
else { // If a district is empty, zero these results (vs. null)
|
|
685
|
-
this.table.popDevPct[i] = popDevPct;
|
|
686
|
-
this.table.demVotes[i] = 0;
|
|
687
|
-
this.table.repVotes[i] = 0;
|
|
688
|
-
this.table.otherVotes[i] = 0;
|
|
689
|
-
this.table.demPct[i] = 0;
|
|
690
|
-
this.table.repPct[i] = 0;
|
|
691
|
-
this.table.otherPct[i] = 0;
|
|
692
|
-
this.table.demSeat[i] = 0;
|
|
693
|
-
this.table.whitePop[i] = 0;
|
|
694
|
-
this.table.minorityPop[i] = 0;
|
|
695
|
-
this.table.blackPop[i] = 0;
|
|
696
|
-
this.table.hispanicPop[i] = 0;
|
|
697
|
-
this.table.pacificPop[i] = 0;
|
|
698
|
-
this.table.asianPop[i] = 0;
|
|
699
|
-
this.table.nativePop[i] = 0;
|
|
700
|
-
this.table.totalVAP[i] = 0;
|
|
701
|
-
this.table.whitePct[i] = 0;
|
|
702
|
-
this.table.minorityPct[i] = 0;
|
|
703
|
-
this.table.blackPct[i] = 0;
|
|
704
|
-
this.table.hispanicPct[i] = 0;
|
|
705
|
-
this.table.pacificPct[i] = 0;
|
|
706
|
-
this.table.asianPct[i] = 0;
|
|
707
|
-
this.table.nativePct[i] = 0;
|
|
708
|
-
}
|
|
709
|
-
{ // UPDATE THESE DISTRICT STATISTICS, EVEN WHEN THEY ARE EMPTY
|
|
710
|
-
this.table.totalPop[i] = totalPop;
|
|
711
|
-
this.table.bNotEmpty[i] = bNotEmpty;
|
|
712
|
-
this.table.bContiguous[i] = bContiguous;
|
|
713
|
-
this.table.bNotEmbedded[i] = bNotEmbedded;
|
|
714
|
-
this.table.bEqualPop[i] = bEqualPop;
|
|
715
|
-
this.table.countySplits[i] = countySplits;
|
|
716
426
|
}
|
|
717
427
|
}
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (stateTotVote > 0) {
|
|
722
|
-
this.table.demVotes[summaryRow] = stateDemVote;
|
|
723
|
-
this.table.repVotes[summaryRow] = stateRepVote;
|
|
724
|
-
this.table.demPct[summaryRow] = stateDemVote / stateTotVote;
|
|
725
|
-
this.table.repPct[summaryRow] = stateRepVote / stateTotVote;
|
|
726
|
-
this.table.otherPct[summaryRow] = stateOthVote / stateTotVote;
|
|
727
|
-
}
|
|
728
|
-
if (stateVAPPop > 0) {
|
|
729
|
-
this.table.totalVAP[summaryRow] = stateVAPPop;
|
|
730
|
-
this.table.whitePct[summaryRow] = stateWhitePop / stateVAPPop;
|
|
731
|
-
this.table.minorityPct[summaryRow] = stateMinorityPop / stateVAPPop;
|
|
732
|
-
this.table.blackPct[summaryRow] = stateBlackPop / stateVAPPop;
|
|
733
|
-
this.table.hispanicPct[summaryRow] = stateHispanicPop / stateVAPPop;
|
|
734
|
-
this.table.pacificPct[summaryRow] = statePacificPop / stateVAPPop;
|
|
735
|
-
this.table.asianPct[summaryRow] = stateAsianPop / stateVAPPop;
|
|
736
|
-
this.table.nativePct[summaryRow] = stateNativePop / stateVAPPop;
|
|
737
|
-
}
|
|
738
|
-
if (bLog) {
|
|
739
|
-
console.log(`Statistics: ${nMissingDataset} features with missing datasets.`);
|
|
740
|
-
console.log(`Statistics: ${nMissingProperty} features with missing properties.`);
|
|
428
|
+
// If so, save the district
|
|
429
|
+
if (nCountiesInDistrict == 1) {
|
|
430
|
+
singleCountyDistricts.push(d + 1); // Districts are 1-indexed
|
|
741
431
|
}
|
|
742
432
|
}
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
let
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
let nativePts = [];
|
|
765
|
-
let minorityPts = [];
|
|
766
|
-
// let dataDump: any[] = []; // For testing
|
|
767
|
-
// Gather [demographic %, D %] points for the selected district ID
|
|
768
|
-
let i = districtID;
|
|
769
|
-
// HACK - For comparing a minority group to everyone else (vs. just White),
|
|
770
|
-
// infer the *single* minority group that has been selected.
|
|
771
|
-
const selectedMinority = inferSelectedMinority(groups);
|
|
772
|
-
// HACK - Because "this" gets ghosted inside the forEach loop below
|
|
773
|
-
let outerThis = this;
|
|
774
|
-
// Get the geoIDs assigned to the district
|
|
775
|
-
// Guard against empty districts
|
|
776
|
-
let geoIDs = this._session.plan.geoIDsForDistrictID(i);
|
|
777
|
-
if (geoIDs && (geoIDs.size > 0)) {
|
|
778
|
-
// ... loop over the geoIDs collecting the points for ecological regression
|
|
779
|
-
geoIDs.forEach(function (geoID) {
|
|
780
|
-
// Skip water-only features
|
|
781
|
-
if (!(U.isWaterOnly(geoID))) {
|
|
782
|
-
// Map from geoID to feature index
|
|
783
|
-
let featureID = outerThis._session.features.featureID(geoID);
|
|
784
|
-
let f = outerThis._session.features.featureByIndex(featureID);
|
|
785
|
-
if (!(f == undefined)) {
|
|
786
|
-
// Calculate the Dem two-party vote
|
|
787
|
-
// 03-27-21
|
|
788
|
-
const dkELECTION = outerThis._session.features._keys["ELECTION" /* T.Dataset.ELECTION */];
|
|
789
|
-
const featureDem = fieldForFeature(f, dkELECTION, 7 /* T.FeatureField.DemVotes */);
|
|
790
|
-
const featureRep = fieldForFeature(f, dkELECTION, 8 /* T.FeatureField.RepVotes */);
|
|
791
|
-
// const featureRep = outerThis._session.features.fieldForFeature(f, T.Dataset.ELECTION, T.FeatureField.RepVotes);
|
|
792
|
-
if ((featureDem + featureRep) > 0) {
|
|
793
|
-
const pctDem = featureDem / (featureDem + featureRep);
|
|
794
|
-
// Calculate the VAP/CVAP percentages by demographic
|
|
795
|
-
// 03-27-21
|
|
796
|
-
const dkVAP = outerThis._session.features._keys["VAP" /* T.Dataset.VAP */];
|
|
797
|
-
const totalVAP = fieldForFeature(f, dkVAP, 0 /* T.FeatureField.TotalPop */);
|
|
798
|
-
// const totalVAP = outerThis._session.features.fieldForFeature(f, T.Dataset.VAP, T.FeatureField.TotalPop);
|
|
799
|
-
if (totalVAP > 0) {
|
|
800
|
-
// Gather all points, for debugging purposes ...
|
|
801
|
-
const hispanicVAP = fieldForFeature(f, dkVAP, 3 /* T.FeatureField.HispanicPop */);
|
|
802
|
-
const blackVAP = fieldForFeature(f, dkVAP, 2 /* T.FeatureField.BlackPop */);
|
|
803
|
-
const pacificVAP = fieldForFeature(f, dkVAP, 5 /* T.FeatureField.PacificPop */);
|
|
804
|
-
const asianVAP = fieldForFeature(f, dkVAP, 4 /* T.FeatureField.AsianPop */);
|
|
805
|
-
const nativeVAP = fieldForFeature(f, dkVAP, 6 /* T.FeatureField.NativePop */);
|
|
806
|
-
const pctHispanic = hispanicVAP / totalVAP;
|
|
807
|
-
const pctBlack = blackVAP / totalVAP;
|
|
808
|
-
const pctPacific = pacificVAP / totalVAP;
|
|
809
|
-
const pctAsian = asianVAP / totalVAP;
|
|
810
|
-
const pctNative = nativeVAP / totalVAP;
|
|
811
|
-
const whiteVAP = fieldForFeature(f, dkVAP, 1 /* T.FeatureField.WhitePop */);
|
|
812
|
-
const minorityVAP = totalVAP - whiteVAP;
|
|
813
|
-
const pctMinority = minorityVAP / totalVAP;
|
|
814
|
-
let pctComparison = whiteVAP / totalVAP;
|
|
815
|
-
if (groups.invertSelection) {
|
|
816
|
-
switch (selectedMinority) {
|
|
817
|
-
case 'Hispanic': {
|
|
818
|
-
pctComparison = (totalVAP - hispanicVAP) / totalVAP;
|
|
819
|
-
break;
|
|
820
|
-
}
|
|
821
|
-
case 'Black': {
|
|
822
|
-
pctComparison = (totalVAP - blackVAP) / totalVAP;
|
|
823
|
-
break;
|
|
824
|
-
}
|
|
825
|
-
case 'Pacific': {
|
|
826
|
-
pctComparison = (totalVAP - pacificVAP) / totalVAP;
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
case 'Asian': {
|
|
830
|
-
pctComparison = (totalVAP - asianVAP) / totalVAP;
|
|
831
|
-
break;
|
|
832
|
-
}
|
|
833
|
-
case 'Native': {
|
|
834
|
-
pctComparison = (totalVAP - nativeVAP) / totalVAP;
|
|
835
|
-
break;
|
|
836
|
-
}
|
|
837
|
-
case 'Minority': {
|
|
838
|
-
pctComparison = (totalVAP - minorityVAP) / totalVAP;
|
|
839
|
-
break;
|
|
840
|
-
}
|
|
841
|
-
default: {
|
|
842
|
-
console.log("Selected minority not recognized!");
|
|
843
|
-
break;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
ids.push(geoID);
|
|
848
|
-
comparisonPts.push({ x: pctComparison, y: pctDem }); // White -or- (1 – <selected minority>)
|
|
849
|
-
hispanicPts.push({ x: pctHispanic, y: pctDem });
|
|
850
|
-
blackPts.push({ x: pctBlack, y: pctDem });
|
|
851
|
-
pacificPts.push({ x: pctPacific, y: pctDem });
|
|
852
|
-
asianPts.push({ x: pctAsian, y: pctDem });
|
|
853
|
-
nativePts.push({ x: pctNative, y: pctDem });
|
|
854
|
-
minorityPts.push({ x: pctMinority, y: pctDem });
|
|
855
|
-
/* Dump test data based on config switch
|
|
856
|
-
const s: AnalyticsSession = outerThis._session;
|
|
857
|
-
const bDump = (('dump' in s.config) && s.config.dump) ? true : false;
|
|
858
|
-
if (bDump)
|
|
859
|
-
{
|
|
860
|
-
const row: number[] = [pctWhite, pctMinority, pctBlack, pctHispanic, pctPacific, pctAsian, pctNative, pctDem];
|
|
861
|
-
dataDump.push(row);
|
|
862
|
-
}
|
|
863
|
-
*/
|
|
864
|
-
}
|
|
865
|
-
else {
|
|
866
|
-
if (bLog)
|
|
867
|
-
console.log("RPV: Total VAP not > 0.");
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
else {
|
|
871
|
-
if (bLog)
|
|
872
|
-
console.log("RPV: D + R not > 0.");
|
|
873
|
-
}
|
|
433
|
+
// Process the splits/partitions in the plan:
|
|
434
|
+
// - Count the total # of partitions,
|
|
435
|
+
// - Find the counties split across districts, and
|
|
436
|
+
// - Accumulate the number people affected (except when single-county districts)
|
|
437
|
+
let nPartitionsOverall = 0;
|
|
438
|
+
let splitCounties = new Set(); // The counties that are split across districts
|
|
439
|
+
let totalAffected = 0; // The total population affected by those splits
|
|
440
|
+
// For enumerating # of times each split county is split
|
|
441
|
+
let countyIndexesWithSplits = [];
|
|
442
|
+
for (let c = 0; c < container.counties.nCounties; c++) {
|
|
443
|
+
let nCountyParts = 0;
|
|
444
|
+
let subtotal = 0;
|
|
445
|
+
for (let d = 0; d < container.state.nDistricts; d++) {
|
|
446
|
+
// Guard against empty district
|
|
447
|
+
if (CxD[d]) {
|
|
448
|
+
if (CxD[d][c] > 0) {
|
|
449
|
+
nPartitionsOverall += 1;
|
|
450
|
+
nCountyParts += 1;
|
|
451
|
+
if (!(U.arrayContains(singleCountyDistricts, d + 1))) // Districts are 1-indexed
|
|
452
|
+
{
|
|
453
|
+
subtotal += CxD[d][c];
|
|
874
454
|
}
|
|
875
455
|
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
hispanic: groups.hispanic ? hispanicPts : [],
|
|
885
|
-
pacific: groups.pacific ? pacificPts : [],
|
|
886
|
-
asian: groups.asian ? asianPts : [],
|
|
887
|
-
native: groups.native ? nativePts : []
|
|
888
|
-
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (nCountyParts > 1) {
|
|
459
|
+
// Counties are 1-indexed (because of the virtual county bucket)
|
|
460
|
+
splitCounties.add(c + 1);
|
|
461
|
+
totalAffected += subtotal;
|
|
462
|
+
// Enumerate # of times each split county is split
|
|
463
|
+
countyIndexesWithSplits.push([c + 1, nCountyParts - 1]);
|
|
889
464
|
}
|
|
890
|
-
// If a district is empty
|
|
891
|
-
return undefined;
|
|
892
465
|
}
|
|
466
|
+
// Enumerate # of times each split county is split
|
|
467
|
+
let splitCountiesWithSplits = [];
|
|
468
|
+
countyIndexesWithSplits.forEach(pair => {
|
|
469
|
+
const index = pair[0];
|
|
470
|
+
const splits = pair[1];
|
|
471
|
+
// Convert 1–N indices to FIPS codes
|
|
472
|
+
const fips = container.counties.fips[index];
|
|
473
|
+
if (fips) {
|
|
474
|
+
// Convert FIPS codes to names
|
|
475
|
+
const name = container.counties.nameFromFIPS(fips);
|
|
476
|
+
// Combine # of splits with names
|
|
477
|
+
if (name) {
|
|
478
|
+
const text = `${name} (${splits})`;
|
|
479
|
+
splitCountiesWithSplits.push(text);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
// Sort by name
|
|
484
|
+
splitCountiesWithSplits.sort();
|
|
485
|
+
// Convert county ordinals to FIPS codes
|
|
486
|
+
let splitCountiesFIPS = U.getSelectObjectKeys(container.counties.index, [...splitCounties]);
|
|
487
|
+
// THEN TAKE ACCOUNT OF THE COUNTY SPLITTING THAT IS EXPECTED (REQUIRED)
|
|
488
|
+
// Compute the total number of splits this way, in case any counties are split
|
|
489
|
+
// more than once. I.e., it's not just len(all_counties_split).
|
|
490
|
+
let nSplits = nPartitionsOverall - container.counties.nCounties;
|
|
491
|
+
// Determine the number of *unexpected* splits. NOTE: Prevent negative numbers,
|
|
492
|
+
// in case you have a plan the *doesn't* split counties that would have to be
|
|
493
|
+
// split due to their size.
|
|
494
|
+
unexpectedSplits = Math.max(0, nSplits - container.state.expectedSplits);
|
|
495
|
+
// Subtract off the total population that *has* to be affected by splits,
|
|
496
|
+
// because their counties are too big. NOTE: Again, prevent negative numbers,
|
|
497
|
+
// in case you have a plan the *doesn't* split counties that would have to be
|
|
498
|
+
// split due to their size.
|
|
499
|
+
unexpectedAffected = U.trim(Math.max(0, totalAffected - container.state.expectedAffected) / container.state.totalPop);
|
|
500
|
+
// Find the counties that are split *unexpectedly*. From all the counties that
|
|
501
|
+
// are split, remove those that *have* to be split, because they are bigger than
|
|
502
|
+
// a district.
|
|
503
|
+
let countiesSplitUnexpectedlyFIPS = [];
|
|
504
|
+
for (let fips of splitCountiesFIPS) {
|
|
505
|
+
if (!(U.arrayContains(container.state.tooBigFIPS, fips))) {
|
|
506
|
+
countiesSplitUnexpectedlyFIPS.push(fips);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// ... and convert the FIPS codes to county names.
|
|
510
|
+
for (let fips of countiesSplitUnexpectedlyFIPS) {
|
|
511
|
+
const name = container.counties.nameFromFIPS(fips);
|
|
512
|
+
// 07-06-20 - Guard in case the FIPS code isn't in the county name lookup
|
|
513
|
+
if (name)
|
|
514
|
+
countiesSplitUnexpectedly.push(name);
|
|
515
|
+
else {
|
|
516
|
+
if (bLog)
|
|
517
|
+
console.log("County is not in the FIPS-to-name lookup table: ", fips);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
countiesSplitUnexpectedly = countiesSplitUnexpectedly.sort();
|
|
521
|
+
test['score'] = U.trim(unexpectedAffected);
|
|
522
|
+
test['details']['nSplits'] = nSplits;
|
|
523
|
+
test['details']['unexpectedSplits'] = unexpectedSplits;
|
|
524
|
+
test['details']['countiesSplitUnexpectedly'] = countiesSplitUnexpectedly;
|
|
525
|
+
test['details']['expectedSplits'] = container.state.expectedSplits;
|
|
526
|
+
test['details']['tooBigName'] = container.state.tooBigName;
|
|
527
|
+
test['details']['singleCountyDistrictMax'] = container.state.singleCountyDistrictMax;
|
|
528
|
+
test['details']['nCountiesSplit'] = splitCountiesFIPS.length;
|
|
529
|
+
test['details']['nSingleCountyDistricts'] = singleCountyDistricts.length;
|
|
530
|
+
test['details']['countiesWithSplits'] = splitCountiesWithSplits;
|
|
531
|
+
return test;
|
|
893
532
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
533
|
+
// END
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
/***/ }),
|
|
537
|
+
|
|
538
|
+
/***/ "./src/data.ts":
|
|
539
|
+
/*!*********************!*\
|
|
540
|
+
!*** ./src/data.ts ***!
|
|
541
|
+
\*********************/
|
|
542
|
+
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
//
|
|
546
|
+
// DATA ABSTRACTION LAYER
|
|
547
|
+
//
|
|
548
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
549
|
+
if (k2 === undefined) k2 = k;
|
|
550
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
551
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
552
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
553
|
+
}
|
|
554
|
+
Object.defineProperty(o, k2, desc);
|
|
555
|
+
}) : (function(o, m, k, k2) {
|
|
556
|
+
if (k2 === undefined) k2 = k;
|
|
557
|
+
o[k2] = m[k];
|
|
558
|
+
}));
|
|
559
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
560
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
561
|
+
}) : function(o, v) {
|
|
562
|
+
o["default"] = v;
|
|
563
|
+
});
|
|
564
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
565
|
+
var ownKeys = function(o) {
|
|
566
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
567
|
+
var ar = [];
|
|
568
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
569
|
+
return ar;
|
|
570
|
+
};
|
|
571
|
+
return ownKeys(o);
|
|
572
|
+
};
|
|
573
|
+
return function (mod) {
|
|
574
|
+
if (mod && mod.__esModule) return mod;
|
|
575
|
+
var result = {};
|
|
576
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
577
|
+
__setModuleDefault(result, mod);
|
|
578
|
+
return result;
|
|
579
|
+
};
|
|
580
|
+
})();
|
|
581
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
582
|
+
exports.Plan = exports.State = exports.Counties = exports.Features = exports.Districts = exports.Aggregates = exports.SessionData = void 0;
|
|
583
|
+
exports.geoIDForFeature = geoIDForFeature;
|
|
584
|
+
exports.fieldForFeature = fieldForFeature;
|
|
585
|
+
exports.invertPlan = invertPlan;
|
|
586
|
+
exports.processConfig = processConfig;
|
|
587
|
+
const dra_types_1 = __webpack_require__(/*! @dra2020/dra-types */ "@dra2020/dra-types");
|
|
588
|
+
const T = __importStar(__webpack_require__(/*! ./types */ "./src/types.ts"));
|
|
589
|
+
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
590
|
+
const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
|
|
591
|
+
const requirements_1 = __webpack_require__(/*! ./requirements */ "./src/requirements.ts");
|
|
592
|
+
class SessionData {
|
|
593
|
+
constructor(s, SessionRequest) {
|
|
594
|
+
this.bOneTimeProcessingDone = false;
|
|
595
|
+
this.bPlanAnalyzed = false;
|
|
596
|
+
this.legislativeDistricts = false;
|
|
597
|
+
this.config = {};
|
|
598
|
+
this.CxD = []; // Derived later
|
|
599
|
+
this._session = s;
|
|
600
|
+
this.title = SessionRequest['title'];
|
|
601
|
+
this.config = processConfig(SessionRequest['config']);
|
|
602
|
+
// 2020 - Set the CD / LD toggle:
|
|
603
|
+
// * If 2020+, use the PlanType, if given.
|
|
604
|
+
// * If 2010, use the legacy LD toggle, if given.
|
|
605
|
+
// * Otherwise assume CD's.
|
|
606
|
+
// * Use the LD population deviation threshold for districts that are not CD's.
|
|
607
|
+
if ((this.config.cycle > 2010) && (SessionRequest['planType'] !== undefined))
|
|
608
|
+
this.legislativeDistricts = (SessionRequest['planType'] != 'congress');
|
|
609
|
+
else if ((this.config.cycle == 2010) && (SessionRequest['legislativeDistricts'] !== undefined))
|
|
610
|
+
this.legislativeDistricts = SessionRequest['legislativeDistricts'];
|
|
611
|
+
const repsByDistrict = SessionRequest['repsByDistrict'];
|
|
612
|
+
const nDistricts = SessionRequest['nDistricts'];
|
|
613
|
+
if (repsByDistrict !== undefined) {
|
|
614
|
+
if (repsByDistrict.length != nDistricts)
|
|
615
|
+
throw new Error("Mismatched #'s of districts passed to AnalyticsSession constructor!");
|
|
616
|
+
if (repsByDistrict.includes(0))
|
|
617
|
+
throw new Error("Zero reps for a district passed to AnalyticsSession constructor!");
|
|
618
|
+
// Assume a positive integer # of reps per district
|
|
619
|
+
this.repsByDistrict = repsByDistrict;
|
|
620
|
+
}
|
|
621
|
+
this.state = new State(this, SessionRequest['stateXX'], SessionRequest['nDistricts']);
|
|
622
|
+
this.counties = new Counties(this, SessionRequest['counties']);
|
|
623
|
+
this.graph = SessionRequest['graph'];
|
|
624
|
+
this.features = new Features(this, SessionRequest['data'], this.config['datasets']);
|
|
625
|
+
this.plan = new Plan(this, SessionRequest['plan']);
|
|
626
|
+
this.districts = new Districts(this, SessionRequest['districtShapes']);
|
|
627
|
+
this.aggregates = new Aggregates(this, SessionRequest['aggregates']);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
exports.SessionData = SessionData;
|
|
631
|
+
// CLASSES FOR DISTRICTS & DISTRICT AGGREGATES
|
|
632
|
+
class Aggregates {
|
|
633
|
+
constructor(container, aggregates) {
|
|
634
|
+
this._container = container;
|
|
635
|
+
this._aggregates = aggregates;
|
|
636
|
+
this.unpacked = {};
|
|
637
|
+
this.columns = {};
|
|
638
|
+
}
|
|
639
|
+
process() {
|
|
640
|
+
this._unpack();
|
|
641
|
+
this._columnize();
|
|
642
|
+
this._checkReqs();
|
|
643
|
+
this._convert();
|
|
644
|
+
}
|
|
645
|
+
transpose() {
|
|
646
|
+
const nRows = this._container.state.nDistricts + 2; // +1 for unassigned, +1 for statewide
|
|
647
|
+
let table = [];
|
|
648
|
+
for (let i = 0; i < nRows; i++) {
|
|
649
|
+
const row = {
|
|
650
|
+
districtID: this.columns['districtID'][i],
|
|
651
|
+
totalPop: this.columns['totalPop'][i],
|
|
652
|
+
popDevPct: this.columns['popDevPct'][i],
|
|
653
|
+
bEqualPop: this.columns['bEqualPop'][i],
|
|
654
|
+
bNotEmpty: this.columns['bNotEmpty'][i],
|
|
655
|
+
bContiguous: this.columns['bContiguous'][i],
|
|
656
|
+
bNotEmbedded: this.columns['bNotEmbedded'][i],
|
|
657
|
+
demPct: this.columns['demPct'][i],
|
|
658
|
+
repPct: this.columns['repPct'][i],
|
|
659
|
+
othPct: this.columns['othPct'][i],
|
|
660
|
+
whitePct: this.columns['whitePct'][i],
|
|
661
|
+
totalVAP: this.columns['totalVAP'][i],
|
|
662
|
+
minorityPct: this.columns['minorityPct'][i],
|
|
663
|
+
blackPct: this.columns['blackPct'][i],
|
|
664
|
+
hispanicPct: this.columns['hispanicPct'][i],
|
|
665
|
+
pacificPct: this.columns['pacificPct'][i],
|
|
666
|
+
asianPct: this.columns['asianPct'][i],
|
|
667
|
+
nativePct: this.columns['nativePct'][i]
|
|
668
|
+
};
|
|
669
|
+
table.push(row);
|
|
670
|
+
}
|
|
671
|
+
return table;
|
|
672
|
+
}
|
|
673
|
+
// PRIVATE HELPER METHODS
|
|
674
|
+
_unpack() {
|
|
675
|
+
// NOTE - This maintains the dataset hierarchy, but merges multi-fields into single buckets
|
|
676
|
+
var _a, _b, _c;
|
|
677
|
+
for (const dataset of Object.values(T.Dataset)) {
|
|
678
|
+
if (dataset === T.Dataset.SHAPES)
|
|
679
|
+
continue;
|
|
680
|
+
if (!this.unpacked[dataset]) {
|
|
681
|
+
this.unpacked[dataset] = {};
|
|
682
|
+
}
|
|
683
|
+
const did = this._container.config['datasets'][dataset];
|
|
684
|
+
const fields = packedFields(this._aggregates, did);
|
|
685
|
+
for (const field of fields) {
|
|
686
|
+
const mappedField = (_a = T.fieldMap[dataset]) === null || _a === void 0 ? void 0 : _a[field];
|
|
687
|
+
if (mappedField) {
|
|
688
|
+
const bucket = T.multiFields.includes(field) ? field : mappedField;
|
|
689
|
+
this.unpacked[dataset][bucket] = unpackedFields(this._aggregates, did, field);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
// Post-process potential multiField pairs
|
|
693
|
+
for (const [field1, field2] of T.multiFieldPairs) {
|
|
694
|
+
const has1 = field1 in this.unpacked[dataset];
|
|
695
|
+
const has2 = field2 in this.unpacked[dataset];
|
|
696
|
+
if (has1 && has2) {
|
|
697
|
+
// Both exist - pick the one with max sum
|
|
698
|
+
const sum1 = this.unpacked[dataset][field1].reduce((a, b) => a + b, 0);
|
|
699
|
+
const sum2 = this.unpacked[dataset][field2].reduce((a, b) => a + b, 0);
|
|
700
|
+
const keepField = sum1 >= sum2 ? field1 : field2;
|
|
701
|
+
const removeField = sum1 >= sum2 ? field2 : field1;
|
|
702
|
+
const mappedField = (_b = T.fieldMap[dataset]) === null || _b === void 0 ? void 0 : _b[keepField];
|
|
703
|
+
if (mappedField) {
|
|
704
|
+
this.unpacked[dataset][mappedField] = this.unpacked[dataset][keepField];
|
|
705
|
+
delete this.unpacked[dataset][keepField];
|
|
706
|
+
}
|
|
707
|
+
delete this.unpacked[dataset][removeField];
|
|
708
|
+
}
|
|
709
|
+
else if (has1 || has2) {
|
|
710
|
+
// Only one exists - rename it
|
|
711
|
+
const existingField = has1 ? field1 : field2;
|
|
712
|
+
const mappedField = (_c = T.fieldMap[dataset]) === null || _c === void 0 ? void 0 : _c[existingField];
|
|
713
|
+
if (mappedField) {
|
|
714
|
+
this.unpacked[dataset][mappedField] = this.unpacked[dataset][existingField];
|
|
715
|
+
delete this.unpacked[dataset][existingField];
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
_columnize() {
|
|
722
|
+
// Flatten the unpacked aggregates
|
|
723
|
+
for (const dataset in this.unpacked) {
|
|
724
|
+
Object.assign(this.columns, this.unpacked[dataset]);
|
|
725
|
+
}
|
|
726
|
+
// Add statewide totals
|
|
727
|
+
for (const field in this.columns) {
|
|
728
|
+
const statewide = U.sumArray(this.columns[field]);
|
|
729
|
+
this.columns[field].push(statewide);
|
|
730
|
+
}
|
|
731
|
+
// Add a district id column
|
|
732
|
+
const nRows = this._container.state.nDistricts + 2; // +1 for unassigned, +1 for statewide
|
|
733
|
+
this.columns['districtID'] = [...Array(nRows).keys()];
|
|
734
|
+
}
|
|
735
|
+
// Check district requirements and other other booleans for the district statistics table
|
|
736
|
+
_checkReqs() {
|
|
737
|
+
let contiguous = [true]; // Dummy value for the unassigned district
|
|
738
|
+
let notEmbedded = [true];
|
|
739
|
+
const simpleGraph = this._container.graph; // HACK
|
|
740
|
+
for (let d = 1; d <= this._container.state.nDistricts; d++) // Skip unassigned district
|
|
741
|
+
{
|
|
742
|
+
let geoIDs = this._container.plan.geoIDsForDistrictID(d);
|
|
743
|
+
if (geoIDs && (geoIDs.size > 0)) {
|
|
744
|
+
contiguous.push((0, requirements_1.isContiguous)(geoIDs, simpleGraph));
|
|
745
|
+
notEmbedded.push(!(0, requirements_1.isEmbedded)(d, geoIDs, this._container.plan.byGeoID(), simpleGraph));
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
contiguous.push(true); // Empty districts are trivially contiguous
|
|
749
|
+
notEmbedded.push(true); // Empty districts are trivially non-embedded
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const bPlanContiguous = contiguous.every(c => c);
|
|
753
|
+
contiguous.push(bPlanContiguous);
|
|
754
|
+
const bPlanFreeOfHoles = notEmbedded.every(ne => ne);
|
|
755
|
+
notEmbedded.push(bPlanFreeOfHoles);
|
|
756
|
+
const nRows = this._container.state.nDistricts + 2; // +1 for unassigned, +1 for statewide
|
|
757
|
+
let equalPop = U.initArray(nRows, true);
|
|
758
|
+
this.columns['bContiguous'] = contiguous;
|
|
759
|
+
this.columns['bNotEmbedded'] = notEmbedded;
|
|
760
|
+
this.columns['bEqualPop'] = equalPop;
|
|
761
|
+
this.columns['bNotEmpty'] = this.columns.TotalPop.map((value, index) => index === 0 || index === this.columns.TotalPop.length - 1 || value !== 0);
|
|
762
|
+
}
|
|
763
|
+
// Convert counts to %'s for the plan profile and the district statistics table
|
|
764
|
+
_convert() {
|
|
765
|
+
// Compute population deviations by district
|
|
766
|
+
{
|
|
767
|
+
let popDevPct = [];
|
|
768
|
+
const targetSize = this._container.state.targetSize;
|
|
769
|
+
const repsByDistrict = this._container.repsByDistrict;
|
|
770
|
+
for (let i = 0; i < this.columns.TotalPop.length; i++) {
|
|
771
|
+
if (i === 0) {
|
|
772
|
+
popDevPct.push(0.0); // Dummy value for the unassigned district
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const totalPop = this.columns.TotalPop[i];
|
|
776
|
+
if ((totalPop === 0) || (i === this.columns.TotalPop.length - 1)) {
|
|
777
|
+
popDevPct.push(1.0);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
// NOTE - Generalized for MMD
|
|
781
|
+
const n = (repsByDistrict) ? repsByDistrict[i - 1] : 1;
|
|
782
|
+
const dev = (totalPop - (n * targetSize)) / (n * targetSize);
|
|
783
|
+
popDevPct.push(dev);
|
|
784
|
+
}
|
|
785
|
+
this.columns['popDevPct'] = popDevPct;
|
|
786
|
+
}
|
|
787
|
+
// Convert election data to %'s
|
|
788
|
+
{
|
|
789
|
+
this.columns['demPct'] = U.divideArrays(this.columns['DemVotes'], this.columns['TotalVotes']);
|
|
790
|
+
this.columns['repPct'] = U.divideArrays(this.columns['RepVotes'], this.columns['TotalVotes']);
|
|
791
|
+
const twoPartyTotals = U.addArrays(this.columns['DemVotes'], this.columns['RepVotes']);
|
|
792
|
+
this.columns['othPct'] = U.divideArrays(U.subtractArrays(this.columns['TotalVotes'], twoPartyTotals), this.columns['TotalVotes']);
|
|
793
|
+
this.columns['Vf'] = U.divideArrays(this.columns['DemVotes'], twoPartyTotals);
|
|
794
|
+
}
|
|
795
|
+
// Convert VAP/CVAP data to %'s
|
|
796
|
+
{
|
|
797
|
+
// Derive minority VAP from total and white VAP
|
|
798
|
+
this.columns['MinorityVAP'] = U.subtractArrays(this.columns['TotalVAP'], this.columns['WhiteVAP']);
|
|
799
|
+
// Rename total pop and total VAP fields for district statistics table
|
|
800
|
+
this.columns['totalPop'] = this.columns['TotalPop'];
|
|
801
|
+
delete this.columns['TotalPop'];
|
|
802
|
+
this.columns['totalVAP'] = this.columns['TotalVAP'];
|
|
803
|
+
delete this.columns['TotalVAP'];
|
|
804
|
+
// Compute %'s
|
|
805
|
+
this.columns['whitePct'] = U.divideArrays(this.columns['WhiteVAP'], this.columns['totalVAP']);
|
|
806
|
+
this.columns['minorityPct'] = U.divideArrays(this.columns['MinorityVAP'], this.columns['totalVAP']);
|
|
807
|
+
this.columns['blackPct'] = U.divideArrays(this.columns['BlackVAP'], this.columns['totalVAP']);
|
|
808
|
+
this.columns['hispanicPct'] = U.divideArrays(this.columns['HispanicVAP'], this.columns['totalVAP']);
|
|
809
|
+
this.columns['pacificPct'] = U.divideArrays(this.columns['PacificVAP'], this.columns['totalVAP']);
|
|
810
|
+
this.columns['asianPct'] = U.divideArrays(this.columns['AsianVAP'], this.columns['totalVAP']);
|
|
811
|
+
this.columns['nativePct'] = U.divideArrays(this.columns['NativeVAP'], this.columns['totalVAP']);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
909
814
|
}
|
|
910
|
-
|
|
911
|
-
|
|
815
|
+
exports.Aggregates = Aggregates;
|
|
816
|
+
class Districts {
|
|
817
|
+
constructor(container, ds) {
|
|
818
|
+
this._container = container;
|
|
819
|
+
this._shapes = ds;
|
|
820
|
+
}
|
|
821
|
+
getShapes() {
|
|
822
|
+
return this._shapes;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
exports.Districts = Districts;
|
|
826
|
+
// CLASSES & HELPERS FOR FEATURE & COUNTY DATA
|
|
912
827
|
class Features {
|
|
913
|
-
constructor(
|
|
828
|
+
constructor(container, data, keys) {
|
|
914
829
|
this._featureIDs = {};
|
|
915
|
-
this.
|
|
830
|
+
this._container = container;
|
|
916
831
|
this._data = data;
|
|
917
832
|
this._keys = keys;
|
|
918
833
|
}
|
|
919
834
|
nFeatures() { return this._data.features.length; }
|
|
920
835
|
featureByIndex(i) { return this._data.features[i]; }
|
|
921
|
-
/* 03/27/21 - Moved to top-level & exported:
|
|
922
|
-
geoIDForFeature(f: any): string
|
|
923
|
-
{
|
|
924
|
-
// 12-02-2020 - Switched to using ids vs. geoids per Terry
|
|
925
|
-
if (f.properties && f.properties['id'])
|
|
926
|
-
{
|
|
927
|
-
return f.properties['id'];
|
|
928
|
-
// GEOIDs will be one of these properties
|
|
929
|
-
// const value = f.properties['GEOID10'] || f.properties['GEOID20'] || f.properties['GEOID'];
|
|
930
|
-
|
|
931
|
-
// return value;
|
|
932
|
-
}
|
|
933
|
-
else
|
|
934
|
-
{
|
|
935
|
-
console.log("This feature does not have an id property:", f);
|
|
936
|
-
throw "Feature with no id property.";
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
fieldForFeature(f: any, dt: T.Dataset, fk: string): any
|
|
940
|
-
{
|
|
941
|
-
const dk: string = this._keys[dt];
|
|
942
|
-
|
|
943
|
-
let result = undefined;
|
|
944
|
-
|
|
945
|
-
// 07-31-2020 - Fix to post-process PVI into the expected election composite format.
|
|
946
|
-
if (dk === 'P16GPR')
|
|
947
|
-
{
|
|
948
|
-
// 10-27-2020 - Enabling a 'Tot' result for PVI, now that we're reporting 'Other'
|
|
949
|
-
switch (fk)
|
|
950
|
-
{
|
|
951
|
-
case 'D': {
|
|
952
|
-
result = (_getFeatures(f, dk, 'D12') + _getFeatures(f, dk, 'D16')) / 2;
|
|
953
|
-
break;
|
|
954
|
-
}
|
|
955
|
-
case 'R': {
|
|
956
|
-
result = (_getFeatures(f, dk, 'R12') + _getFeatures(f, dk, 'R16')) / 2;
|
|
957
|
-
break;
|
|
958
|
-
}
|
|
959
|
-
case 'Tot': {
|
|
960
|
-
result = (_getFeatures(f, dk, 'D12') + _getFeatures(f, dk, 'D16') + _getFeatures(f, dk, 'R12') + _getFeatures(f, dk, 'R16')) / 2;
|
|
961
|
-
break;
|
|
962
|
-
}
|
|
963
|
-
default: {
|
|
964
|
-
console.log("Field not recognized.");
|
|
965
|
-
break;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
// result = (_getFeatures(f, dk, (fk === 'D' ? 'D12' : 'R12')) + _getFeatures(f, dk, (fk === 'D' ? 'D16' : 'R16'))) / 2;
|
|
969
|
-
}
|
|
970
|
-
else
|
|
971
|
-
result = _getFeatures(f, dk, fk);
|
|
972
|
-
|
|
973
|
-
return result;
|
|
974
|
-
}
|
|
975
|
-
*/
|
|
976
|
-
resetDataset(d, k) {
|
|
977
|
-
this._keys[d] = k;
|
|
978
|
-
// NOTE - RECALC: Does anything need to be recalc'd now when a dataset is changed?
|
|
979
|
-
}
|
|
980
836
|
mapGeoIDsToFeatureIDs() {
|
|
981
|
-
for (let i = 0; i < this.
|
|
982
|
-
let f = this.
|
|
983
|
-
// 03-27-21
|
|
837
|
+
for (let i = 0; i < this.nFeatures(); i++) {
|
|
838
|
+
let f = this.featureByIndex(i);
|
|
984
839
|
let geoID = geoIDForFeature(f);
|
|
985
|
-
// let geoID: string = this._session.features.geoIDForFeature(f);
|
|
986
840
|
this._featureIDs[geoID] = i;
|
|
987
841
|
}
|
|
988
842
|
}
|
|
@@ -1007,10 +861,10 @@ function fieldForFeature(f, dk /* dt: T.Dataset */, ff) {
|
|
|
1007
861
|
// Take the max (typically one field will be zero, but some datasets contain Bl subset of BlC)
|
|
1008
862
|
return Math.max(...T.fieldsFromFeatureField(dk, ff).map(key => _getFeatures(f, dk, key)));
|
|
1009
863
|
}
|
|
1010
|
-
// NOTE - This accessor is cloned from fGetW() in dra-client/restrict.ts
|
|
1011
|
-
// f is a direct GeoJSON feature
|
|
1012
|
-
// p is a geoID
|
|
1013
864
|
function _getFeatures(f, datasetKey, p) {
|
|
865
|
+
// This accessor is cloned from fGetW() in dra-client/restrict.ts
|
|
866
|
+
// f is a direct GeoJSON feature
|
|
867
|
+
// p is a geoID
|
|
1014
868
|
if (!f.properties)
|
|
1015
869
|
return 0;
|
|
1016
870
|
// This is actual path
|
|
@@ -1023,19 +877,17 @@ function _getFeatures(f, datasetKey, p) {
|
|
|
1023
877
|
return !n || isNaN(n) ? 0 : n;
|
|
1024
878
|
*/
|
|
1025
879
|
}
|
|
1026
|
-
// Wrap data by county, to abstract the specifics of the internal structure
|
|
1027
880
|
class Counties {
|
|
1028
|
-
constructor(
|
|
881
|
+
constructor(container, data) {
|
|
1029
882
|
this._countyNameLookup = {};
|
|
1030
883
|
this.index = {};
|
|
1031
884
|
this.fips = {};
|
|
1032
885
|
this.totalPopulation = [];
|
|
1033
|
-
this.
|
|
886
|
+
this._container = container;
|
|
1034
887
|
this._data = data;
|
|
1035
888
|
this.nCounties = this._data.features.length;
|
|
1036
889
|
}
|
|
1037
890
|
countyByIndex(i) { return this._data.features[i]; }
|
|
1038
|
-
// 10-16-21 - Analyze view crashing because county names were undefined in the lookup table.
|
|
1039
891
|
// Property had changed from 'NAME' to 'name' for *some* states.
|
|
1040
892
|
propertyForCounty(f, pk) {
|
|
1041
893
|
return f.properties[pk] ? f.properties[pk] : f.properties[pk.toLowerCase()];
|
|
@@ -1045,542 +897,94 @@ class Counties {
|
|
|
1045
897
|
indexFromFIPS(fips) { return this.index[fips]; }
|
|
1046
898
|
}
|
|
1047
899
|
exports.Counties = Counties;
|
|
1048
|
-
// CLASSES
|
|
1049
|
-
// MMD - Extended this for # of reps
|
|
900
|
+
// CLASSES & HELPERS FOR STATE & PLAN DATA
|
|
1050
901
|
class State {
|
|
1051
|
-
constructor(
|
|
902
|
+
constructor(container, xx, n) {
|
|
1052
903
|
this.totalPop = 0;
|
|
1053
|
-
this.targetSize = 0; // HACK -
|
|
904
|
+
this.targetSize = 0; // HACK - This gets set later
|
|
1054
905
|
this.tooBigFIPS = [];
|
|
1055
906
|
this.tooBigName = [];
|
|
1056
907
|
this.singleCountyDistrictMax = 0;
|
|
1057
908
|
this.expectedSplits = 0;
|
|
1058
909
|
this.expectedAffected = 0;
|
|
1059
|
-
this.
|
|
910
|
+
this._container = container;
|
|
1060
911
|
this.xx = xx;
|
|
1061
912
|
this.nDistricts = n;
|
|
1062
|
-
this.nReps = (
|
|
913
|
+
this.nReps = (container.repsByDistrict) ? container.repsByDistrict.reduce((a, b) => a + b, 0) : this.nDistricts;
|
|
1063
914
|
}
|
|
1064
915
|
}
|
|
1065
916
|
exports.State = State;
|
|
1066
|
-
class Plan {
|
|
1067
|
-
constructor(
|
|
1068
|
-
this.
|
|
1069
|
-
this._planByGeoID = p;
|
|
1070
|
-
this._planByDistrictID = {};
|
|
1071
|
-
this.districtIDs = []; // Set when the plan in inverted
|
|
1072
|
-
}
|
|
1073
|
-
// NOTE - DON'T remove water-only features from the plan, as they may be required
|
|
1074
|
-
// for contiguity. Just skip them in aggregating district statistics.
|
|
1075
|
-
//
|
|
1076
|
-
// removeWaterOnlyFeatures(plan: T.PlanByGeoID): T.PlanByGeoID {
|
|
1077
|
-
// let newPlan = {} as T.PlanByGeoID;
|
|
1078
|
-
// for (let geoID in plan) {
|
|
1079
|
-
// // Remove water-only features
|
|
1080
|
-
// if (!(U.isWaterOnly(geoID))) {
|
|
1081
|
-
// newPlan[geoID] = plan[geoID];
|
|
1082
|
-
// }
|
|
1083
|
-
// else {
|
|
1084
|
-
// console.log("Removing water-only feature", geoID);
|
|
1085
|
-
// }
|
|
1086
|
-
// }
|
|
1087
|
-
// return newPlan;
|
|
1088
|
-
// }
|
|
1089
|
-
invertPlan(bLog = false) {
|
|
1090
|
-
this._planByDistrictID = invertPlan(this._planByGeoID, this._session, bLog);
|
|
1091
|
-
this.districtIDs = U.getNumericObjectKeys(this._planByDistrictID);
|
|
1092
|
-
}
|
|
1093
|
-
initializeDistrict(i) { this._planByDistrictID[i] = new Set(); }
|
|
1094
|
-
byGeoID() { return this._planByGeoID; }
|
|
1095
|
-
byDistrictID() { return this._planByDistrictID; }
|
|
1096
|
-
districtForGeoID(i) { return this._planByGeoID[i]; }
|
|
1097
|
-
geoIDsForDistrictID(i) { return this._planByDistrictID[i]; }
|
|
1098
|
-
}
|
|
1099
|
-
exports.Plan = Plan;
|
|
1100
|
-
// Invert a feature assignment structure to sets of ids by district
|
|
1101
|
-
function invertPlan(plan, s, bLog = false) {
|
|
1102
|
-
let invertedPlan = {};
|
|
1103
|
-
// Add a dummy 'unassigned' district
|
|
1104
|
-
invertedPlan[S.NOT_ASSIGNED] = new Set();
|
|
1105
|
-
// The feature assignments coming from DRA do not include unassigned ones.
|
|
1106
|
-
// - In the DRA-calling context, there's an analytics session with a reference
|
|
1107
|
-
// to the features. Loop over all the features to find the unassigned ones,
|
|
1108
|
-
// and add them to the dummy unassigned district explicitly.
|
|
1109
|
-
// - In the CLI-calling context, there's no session (yet) but the plan is complete.
|
|
1110
|
-
if (!(s == undefined)) {
|
|
1111
|
-
for (let i = 0; i < s.features.nFeatures(); i++) {
|
|
1112
|
-
let f = s.features.featureByIndex(i);
|
|
1113
|
-
// 03-27-21
|
|
1114
|
-
let geoID = geoIDForFeature(f);
|
|
1115
|
-
// let geoID: string = s.features.geoIDForFeature(f);
|
|
1116
|
-
// If the feature is NOT explicitly assigned to a district, add the geoID
|
|
1117
|
-
// to the dummy unassigned district 0.
|
|
1118
|
-
if (!(U.keyExists(geoID, plan)))
|
|
1119
|
-
invertedPlan[S.NOT_ASSIGNED].add(geoID);
|
|
1120
|
-
// NOTE - NOT skipping WATER-ONLY features here, because we're
|
|
1121
|
-
// not skipping them below when they are explicitly assigned in plans. Should
|
|
1122
|
-
// we skip them in both places?
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
for (let geoID in plan) {
|
|
1126
|
-
let districtID = plan[geoID];
|
|
1127
|
-
// Make sure the set for the districtID exists
|
|
1128
|
-
if (!(U.objectContains(invertedPlan, districtID))) {
|
|
1129
|
-
invertedPlan[districtID] = new Set();
|
|
1130
|
-
}
|
|
1131
|
-
// Add the geoID to the districtID's set
|
|
1132
|
-
invertedPlan[districtID].add(geoID);
|
|
1133
|
-
if (U.isWaterOnly(geoID) && bLog)
|
|
1134
|
-
console.log("Invert plan: Water-only feature still in plan!", geoID);
|
|
1135
|
-
}
|
|
1136
|
-
return invertedPlan;
|
|
1137
|
-
}
|
|
1138
|
-
class GraphClass {
|
|
1139
|
-
constructor(s, graph) {
|
|
1140
|
-
this._session = s;
|
|
1141
|
-
this._graph = graph;
|
|
1142
|
-
}
|
|
1143
|
-
peerNeighbors(node) {
|
|
1144
|
-
// Get the neighboring geoIDs connected to a geoID
|
|
1145
|
-
// Ignore the lengths of the shared borders (the values), for now
|
|
1146
|
-
// Protect against getting a GEOID that's not in the graph
|
|
1147
|
-
if (U.keyExists(node, this._graph)) {
|
|
1148
|
-
// Handle both unweighted & weighted neighbors
|
|
1149
|
-
let n = this._graph[node];
|
|
1150
|
-
let l = (n instanceof Array) ? n : U.getObjectKeys(n);
|
|
1151
|
-
return l;
|
|
1152
|
-
// return U.getObjectKeys(this._graph[node]);
|
|
1153
|
-
}
|
|
1154
|
-
else
|
|
1155
|
-
return [];
|
|
1156
|
-
// return U.getObjectKeys(this._graph[node]);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
exports.GraphClass = GraphClass;
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
/***/ }),
|
|
1163
|
-
|
|
1164
|
-
/***/ "./src/analyze.ts":
|
|
1165
|
-
/*!************************!*\
|
|
1166
|
-
!*** ./src/analyze.ts ***!
|
|
1167
|
-
\************************/
|
|
1168
|
-
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
//
|
|
1172
|
-
// ANALYZE A PLAN
|
|
1173
|
-
//
|
|
1174
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1175
|
-
exports.doAnalyzeDistricts = doAnalyzeDistricts;
|
|
1176
|
-
exports.doAnalyzePlan = doAnalyzePlan;
|
|
1177
|
-
exports.doDeriveSecondaryTests = doDeriveSecondaryTests;
|
|
1178
|
-
const valid_1 = __webpack_require__(/*! ./valid */ "./src/valid.ts");
|
|
1179
|
-
const equal_1 = __webpack_require__(/*! ./equal */ "./src/equal.ts");
|
|
1180
|
-
const cohesive_1 = __webpack_require__(/*! ./cohesive */ "./src/cohesive.ts");
|
|
1181
|
-
// Compile district-level info for plan/map-level analytics
|
|
1182
|
-
function doAnalyzeDistricts(s, bLog = false) {
|
|
1183
|
-
s.districts.recalcStatistics(bLog);
|
|
1184
|
-
s.districts.extractDistrictShapeProperties(bLog);
|
|
1185
|
-
}
|
|
1186
|
-
// Calculate the analytics & validations and cache the results
|
|
1187
|
-
// NOTE - doAnalyzePlan() depends on doAnalyzeDistricts() having run first.
|
|
1188
|
-
// NOTE - I could make this table-driven, but I'm thinking that the explicit
|
|
1189
|
-
// calls might make chunking for aync easier.
|
|
1190
|
-
function doAnalyzePlan(s, bLog = false) {
|
|
1191
|
-
s.tests[0 /* T.Test.Complete */] = (0, valid_1.doIsComplete)(s, bLog);
|
|
1192
|
-
s.tests[1 /* T.Test.Contiguous */] = (0, valid_1.doIsContiguous)(s, bLog);
|
|
1193
|
-
s.tests[2 /* T.Test.FreeOfHoles */] = (0, valid_1.doIsFreeOfHoles)(s, bLog);
|
|
1194
|
-
// NOTE - I can't check whether a population deviation is legal or not, until
|
|
1195
|
-
// the raw % is normalized. A zero (0) would mean "too much" / "not legal," for
|
|
1196
|
-
// the given type of district (CD vs. LD). The EqualPopulation test is derived
|
|
1197
|
-
// from PopulationDeviation, as part of scorecard or test log preparation.
|
|
1198
|
-
// Create an empty test entry here though ...
|
|
1199
|
-
s.tests[3 /* T.Test.EqualPopulation */] = s.getTest(3 /* T.Test.EqualPopulation */);
|
|
1200
|
-
s.tests[5 /* T.Test.UnexpectedCountySplits */] = (0, cohesive_1.doFindCountiesSplitUnexpectedly)(s, bLog);
|
|
1201
|
-
s.tests[6 /* T.Test.VTDSplits */] = (0, cohesive_1.doFindSplitVTDs)(s, bLog);
|
|
1202
|
-
// Enable a Test Log and Scorecard to be generated
|
|
1203
|
-
s.bPlanAnalyzed = true;
|
|
1204
|
-
s.bPostProcessingDone = false;
|
|
1205
|
-
}
|
|
1206
|
-
//
|
|
1207
|
-
// Derive secondary analytics that are based on primary tests.
|
|
1208
|
-
// This concept allows Population Deviation to be a primary numeric test and
|
|
1209
|
-
// Equal Population to be secondary pass/fail validation.
|
|
1210
|
-
//
|
|
1211
|
-
function doDeriveSecondaryTests(s, bLog = false) {
|
|
1212
|
-
s.tests[3 /* T.Test.EqualPopulation */] = (0, equal_1.doHasEqualPopulations)(s, bLog);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
/***/ }),
|
|
1217
|
-
|
|
1218
|
-
/***/ "./src/cohesive.ts":
|
|
1219
|
-
/*!*************************!*\
|
|
1220
|
-
!*** ./src/cohesive.ts ***!
|
|
1221
|
-
\*************************/
|
|
1222
|
-
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
//
|
|
1226
|
-
// SPLITTING
|
|
1227
|
-
//
|
|
1228
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
1229
|
-
if (k2 === undefined) k2 = k;
|
|
1230
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
1231
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
1232
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
1233
|
-
}
|
|
1234
|
-
Object.defineProperty(o, k2, desc);
|
|
1235
|
-
}) : (function(o, m, k, k2) {
|
|
1236
|
-
if (k2 === undefined) k2 = k;
|
|
1237
|
-
o[k2] = m[k];
|
|
1238
|
-
}));
|
|
1239
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
1240
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
1241
|
-
}) : function(o, v) {
|
|
1242
|
-
o["default"] = v;
|
|
1243
|
-
});
|
|
1244
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
1245
|
-
var ownKeys = function(o) {
|
|
1246
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
1247
|
-
var ar = [];
|
|
1248
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
1249
|
-
return ar;
|
|
1250
|
-
};
|
|
1251
|
-
return ownKeys(o);
|
|
1252
|
-
};
|
|
1253
|
-
return function (mod) {
|
|
1254
|
-
if (mod && mod.__esModule) return mod;
|
|
1255
|
-
var result = {};
|
|
1256
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
1257
|
-
__setModuleDefault(result, mod);
|
|
1258
|
-
return result;
|
|
1259
|
-
};
|
|
1260
|
-
})();
|
|
1261
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1262
|
-
exports.doFindCountiesSplitUnexpectedly = doFindCountiesSplitUnexpectedly;
|
|
1263
|
-
exports.doFindSplitVTDs = doFindSplitVTDs;
|
|
1264
|
-
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
1265
|
-
// The main county-district splitting code is in the dra-analytics package.
|
|
1266
|
-
// ANALYZE SIMPLE COUNTY & VTD SPLITTING
|
|
1267
|
-
/*
|
|
1268
|
-
|
|
1269
|
-
Sample results for NC 2016 dongressional plan
|
|
1270
|
-
________________________________________________________________________________
|
|
1271
|
-
|
|
1272
|
-
State: NC
|
|
1273
|
-
Census: 2010
|
|
1274
|
-
Total population: 9,535,483
|
|
1275
|
-
Number of districts: 13
|
|
1276
|
-
Target district size: 733,499
|
|
1277
|
-
Number of counties: 100
|
|
1278
|
-
|
|
1279
|
-
Equal Population: 11.24% deviation
|
|
1280
|
-
Compactness: None
|
|
1281
|
-
Proportionality: 27.67% gap
|
|
1282
|
-
Cohesiveness: 11 unexpected splits, affecting 27.14% of the total population
|
|
1283
|
-
|
|
1284
|
-
These counties are split unexpectedly (meaning that they're smaller than a district):
|
|
1285
|
-
|
|
1286
|
-
• Bladen
|
|
1287
|
-
• Buncombe
|
|
1288
|
-
• Catawba
|
|
1289
|
-
• Cumberland
|
|
1290
|
-
• Durham
|
|
1291
|
-
• Guilford
|
|
1292
|
-
• Iredell
|
|
1293
|
-
• Johnston
|
|
1294
|
-
• Pitt
|
|
1295
|
-
• Rowan
|
|
1296
|
-
• Wilson
|
|
1297
|
-
|
|
1298
|
-
*/
|
|
1299
|
-
// 10-23-21 - This analysis *assumes* that a map is complete (or nearly so).
|
|
1300
|
-
// - We do the analysis regardless, but
|
|
1301
|
-
// - Protect against weird results in the client
|
|
1302
|
-
function doFindCountiesSplitUnexpectedly(s, bLog = false) {
|
|
1303
|
-
let test = s.getTest(5 /* T.Test.UnexpectedCountySplits */);
|
|
1304
|
-
// THE THREE VALUES TO DETERMINE FOR A PLAN
|
|
1305
|
-
let unexpectedSplits = 0;
|
|
1306
|
-
let unexpectedAffected = 0;
|
|
1307
|
-
let countiesSplitUnexpectedly = [];
|
|
1308
|
-
// FIRST, ANALYZE THE COUNTY SPLITTING FOR THE PLAN
|
|
1309
|
-
// Get the county-district pivot ("splits")
|
|
1310
|
-
let countiesByDistrict = s.districts.table.countySplits;
|
|
1311
|
-
// Find the single-county districts, i.e., districts NOT split across counties.
|
|
1312
|
-
// Ignore the dummy unassigned 0 and N+1 summary "districts."
|
|
1313
|
-
let singleCountyDistricts = [];
|
|
1314
|
-
for (let d = 1; d <= s.state.nDistricts; d++) {
|
|
1315
|
-
// See if there's only one county partition
|
|
1316
|
-
// Ignore the dummy unassigned 0 "county."
|
|
1317
|
-
let nCountiesInDistrict = 0;
|
|
1318
|
-
for (let c = 1; c <= s.counties.nCounties; c++) {
|
|
1319
|
-
// Guard against empty district
|
|
1320
|
-
if (countiesByDistrict[d]) {
|
|
1321
|
-
if (countiesByDistrict[d][c] > 0) {
|
|
1322
|
-
nCountiesInDistrict += 1;
|
|
1323
|
-
if (nCountiesInDistrict > 1) {
|
|
1324
|
-
break;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
// If so, save the district
|
|
1330
|
-
if (nCountiesInDistrict == 1) {
|
|
1331
|
-
singleCountyDistricts.push(d);
|
|
1332
|
-
}
|
|
917
|
+
class Plan {
|
|
918
|
+
constructor(container, p) {
|
|
919
|
+
this._container = container;
|
|
920
|
+
this._planByGeoID = p;
|
|
921
|
+
this._planByDistrictID = {};
|
|
922
|
+
this.districtIDs = []; // Set when the plan in inverted
|
|
1333
923
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
// - Accumulate the number people affected (except when single-county districts)
|
|
1338
|
-
let nPartitionsOverall = 0;
|
|
1339
|
-
let splitCounties = new Set(); // The counties that are split across districts
|
|
1340
|
-
let totalAffected = 0; // The total population affected by those splits
|
|
1341
|
-
// 07-17-21 - For enumerating # of times each split county is split
|
|
1342
|
-
let countyIndexesWithSplits = [];
|
|
1343
|
-
// Ignore the dummy unassigned 0 "county."
|
|
1344
|
-
for (let c = 1; c <= s.counties.nCounties; c++) {
|
|
1345
|
-
let nCountyParts = 0;
|
|
1346
|
-
let subtotal = 0;
|
|
1347
|
-
// Ignore the dummy unassigned 0 and N+1 summary "districts."
|
|
1348
|
-
for (let d = 1; d <= s.state.nDistricts; d++) {
|
|
1349
|
-
// Guard against empty district
|
|
1350
|
-
if (countiesByDistrict[d]) {
|
|
1351
|
-
if (countiesByDistrict[d][c] > 0) {
|
|
1352
|
-
nPartitionsOverall += 1;
|
|
1353
|
-
nCountyParts += 1;
|
|
1354
|
-
if (!(U.arrayContains(singleCountyDistricts, d))) {
|
|
1355
|
-
subtotal += countiesByDistrict[d][c];
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
if (nCountyParts > 1) {
|
|
1361
|
-
splitCounties.add(c);
|
|
1362
|
-
totalAffected += subtotal;
|
|
1363
|
-
// 07-17-21 - Enumerate # of times each split county is split
|
|
1364
|
-
countyIndexesWithSplits.push([c, nCountyParts - 1]);
|
|
1365
|
-
}
|
|
924
|
+
invertPlan(bLog = false) {
|
|
925
|
+
this._planByDistrictID = invertPlan(this._planByGeoID, this._container, bLog);
|
|
926
|
+
this.districtIDs = U.getNumericObjectKeys(this._planByDistrictID);
|
|
1366
927
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
// more than once. I.e., it's not just len(all_counties_split).
|
|
1392
|
-
let nSplits = nPartitionsOverall - s.counties.nCounties;
|
|
1393
|
-
// Determine the number of *unexpected* splits. NOTE: Prevent negative numbers,
|
|
1394
|
-
// in case you have a plan the *doesn't* split counties that would have to be
|
|
1395
|
-
// split due to their size.
|
|
1396
|
-
unexpectedSplits = Math.max(0, nSplits - s.state.expectedSplits);
|
|
1397
|
-
// Subtract off the total population that *has* to be affected by splits,
|
|
1398
|
-
// because their counties are too big. NOTE: Again, prevent negative numbers,
|
|
1399
|
-
// in case you have a plan the *doesn't* split counties that would have to be
|
|
1400
|
-
// split due to their size.
|
|
1401
|
-
unexpectedAffected = U.trim(Math.max(0, totalAffected - s.state.expectedAffected) / s.state.totalPop);
|
|
1402
|
-
// Find the counties that are split *unexpectedly*. From all the counties that
|
|
1403
|
-
// are split, remove those that *have* to be split, because they are bigger than
|
|
1404
|
-
// a district.
|
|
1405
|
-
let countiesSplitUnexpectedlyFIPS = [];
|
|
1406
|
-
for (let fips of splitCountiesFIPS) {
|
|
1407
|
-
if (!(U.arrayContains(s.state.tooBigFIPS, fips))) {
|
|
1408
|
-
countiesSplitUnexpectedlyFIPS.push(fips);
|
|
928
|
+
initializeDistrict(i) { this._planByDistrictID[i] = new Set(); }
|
|
929
|
+
byGeoID() { return this._planByGeoID; }
|
|
930
|
+
byDistrictID() { return this._planByDistrictID; }
|
|
931
|
+
districtForGeoID(i) { return this._planByGeoID[i]; }
|
|
932
|
+
geoIDsForDistrictID(i) { return this._planByDistrictID[i]; }
|
|
933
|
+
}
|
|
934
|
+
exports.Plan = Plan;
|
|
935
|
+
function invertPlan(plan, container, bLog = false) {
|
|
936
|
+
let invertedPlan = {};
|
|
937
|
+
// Add a dummy 'unassigned' district
|
|
938
|
+
invertedPlan[S.NOT_ASSIGNED] = new Set();
|
|
939
|
+
// The feature assignments coming from DRA do not include unassigned ones.
|
|
940
|
+
// - In the DRA-calling context, there's an analytics session with a reference
|
|
941
|
+
// to the features. Loop over all the features to find the unassigned ones,
|
|
942
|
+
// and add them to the dummy unassigned district explicitly.
|
|
943
|
+
// - In the CLI-calling context, there's no session (yet) but the plan is complete.
|
|
944
|
+
if (!(container == undefined)) {
|
|
945
|
+
for (let i = 0; i < container.features.nFeatures(); i++) {
|
|
946
|
+
let f = container.features.featureByIndex(i);
|
|
947
|
+
let geoID = geoIDForFeature(f);
|
|
948
|
+
// If the feature is NOT explicitly assigned to a district, add the geoID
|
|
949
|
+
// to the dummy unassigned district 0.
|
|
950
|
+
if (!(U.keyExists(geoID, plan)))
|
|
951
|
+
invertedPlan[S.NOT_ASSIGNED].add(geoID);
|
|
1409
952
|
}
|
|
1410
953
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
countiesSplitUnexpectedly.push(name);
|
|
1417
|
-
else {
|
|
1418
|
-
if (bLog)
|
|
1419
|
-
console.log("County is not in the FIPS-to-name lookup table: ", fips);
|
|
954
|
+
for (let geoID in plan) {
|
|
955
|
+
let districtID = plan[geoID];
|
|
956
|
+
// Make sure the set for the districtID exists
|
|
957
|
+
if (!(U.objectContains(invertedPlan, districtID))) {
|
|
958
|
+
invertedPlan[districtID] = new Set();
|
|
1420
959
|
}
|
|
960
|
+
// Add the geoID to the districtID's set
|
|
961
|
+
invertedPlan[districtID].add(geoID);
|
|
962
|
+
if (U.isWaterOnly(geoID) && bLog)
|
|
963
|
+
console.log("Invert plan: Water-only feature still in plan!", geoID);
|
|
1421
964
|
}
|
|
1422
|
-
|
|
1423
|
-
test['score'] = U.trim(unexpectedAffected);
|
|
1424
|
-
test['details']['nSplits'] = nSplits;
|
|
1425
|
-
test['details']['unexpectedSplits'] = unexpectedSplits;
|
|
1426
|
-
test['details']['countiesSplitUnexpectedly'] = countiesSplitUnexpectedly;
|
|
1427
|
-
// 02-23-21 - Added single-county district details
|
|
1428
|
-
test['details']['expectedSplits'] = s.state.expectedSplits;
|
|
1429
|
-
test['details']['tooBigName'] = s.state.tooBigName;
|
|
1430
|
-
test['details']['singleCountyDistrictMax'] = s.state.singleCountyDistrictMax;
|
|
1431
|
-
test['details']['nCountiesSplit'] = splitCountiesFIPS.length;
|
|
1432
|
-
test['details']['nSingleCountyDistricts'] = singleCountyDistricts.length;
|
|
1433
|
-
// 07-17-21 - Enumerate # of times each split county is split
|
|
1434
|
-
test['details']['countiesWithSplits'] = splitCountiesWithSplits;
|
|
1435
|
-
return test;
|
|
1436
|
-
}
|
|
1437
|
-
// NOTE - This function just creates an empty container that dra-client fills in when generating the UI
|
|
1438
|
-
function doFindSplitVTDs(s, bLog = false) {
|
|
1439
|
-
let test = s.getTest(6 /* T.Test.VTDSplits */);
|
|
1440
|
-
let splitVTDs = [];
|
|
1441
|
-
test['score'] = splitVTDs.length;
|
|
1442
|
-
test['details']['splitVTDs'] = splitVTDs;
|
|
1443
|
-
return test;
|
|
965
|
+
return invertedPlan;
|
|
1444
966
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
!*** ./src/compact.ts ***!
|
|
1452
|
-
\************************/
|
|
1453
|
-
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
//
|
|
1457
|
-
// COMPACT
|
|
1458
|
-
//
|
|
1459
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
1460
|
-
if (k2 === undefined) k2 = k;
|
|
1461
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
1462
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
1463
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
1464
|
-
}
|
|
1465
|
-
Object.defineProperty(o, k2, desc);
|
|
1466
|
-
}) : (function(o, m, k, k2) {
|
|
1467
|
-
if (k2 === undefined) k2 = k;
|
|
1468
|
-
o[k2] = m[k];
|
|
1469
|
-
}));
|
|
1470
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
1471
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
1472
|
-
}) : function(o, v) {
|
|
1473
|
-
o["default"] = v;
|
|
1474
|
-
});
|
|
1475
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
1476
|
-
var ownKeys = function(o) {
|
|
1477
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
1478
|
-
var ar = [];
|
|
1479
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
1480
|
-
return ar;
|
|
1481
|
-
};
|
|
1482
|
-
return ownKeys(o);
|
|
1483
|
-
};
|
|
1484
|
-
return function (mod) {
|
|
1485
|
-
if (mod && mod.__esModule) return mod;
|
|
1486
|
-
var result = {};
|
|
1487
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
1488
|
-
__setModuleDefault(result, mod);
|
|
1489
|
-
return result;
|
|
1490
|
-
};
|
|
1491
|
-
})();
|
|
1492
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1493
|
-
exports.extractDistrictProperties = extractDistrictProperties;
|
|
1494
|
-
const baseclient_1 = __webpack_require__(/*! @dra2020/baseclient */ "@dra2020/baseclient");
|
|
1495
|
-
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
1496
|
-
// The main compactness code is in the dra-analytics package.
|
|
1497
|
-
// HELPER TO EXTRACT PROPERTIES OF DISTRICT SHAPES
|
|
1498
|
-
function extractDistrictProperties(s, bLog = false) {
|
|
1499
|
-
// NOTE - I am assuming that district IDs are integers 1–N
|
|
1500
|
-
for (let i = 1; i <= s.state.nDistricts; i++) {
|
|
1501
|
-
const poly = s.districts.getDistrictShapeByID(i);
|
|
1502
|
-
// Guard against no shape for empty districts AND null shapes
|
|
1503
|
-
if (isAShape(poly)) {
|
|
1504
|
-
const area = baseclient_1.Poly.polyAreaFlat(poly);
|
|
1505
|
-
const perimeter = baseclient_1.Poly.polyPerimeterFlat(poly);
|
|
1506
|
-
const diameter = baseclient_1.Poly.polyDiameterFlat(poly);
|
|
1507
|
-
let props = [0, 0, 0];
|
|
1508
|
-
props[0 /* T.DistrictShapeProperty.Area */] = area;
|
|
1509
|
-
props[1 /* T.DistrictShapeProperty.Diameter */] = diameter;
|
|
1510
|
-
props[2 /* T.DistrictShapeProperty.Perimeter */] = perimeter;
|
|
1511
|
-
s.districts.setGeoProperties(i, props);
|
|
1512
|
-
// if (bLog) console.log("District", i, "A =", area, "P =", perimeter, "D =", diameter);
|
|
1513
|
-
}
|
|
967
|
+
// HELPERS
|
|
968
|
+
function processConfig(config) {
|
|
969
|
+
// Set the default the Census & redistricting cycle to 2010
|
|
970
|
+
if (!(U.keyExists('cycle', config))) {
|
|
971
|
+
config['cycle'] = 2010;
|
|
972
|
+
console.log("Cycle was not set explicitly on the session request.");
|
|
1514
973
|
}
|
|
974
|
+
return config;
|
|
1515
975
|
}
|
|
1516
|
-
function
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
976
|
+
function packedFields(pfs, did) {
|
|
977
|
+
let pf = pfs[0];
|
|
978
|
+
if (!pf || !pf.dsGroup || !pf.dsGroup[did])
|
|
979
|
+
return [];
|
|
980
|
+
let fields = pf.dsGroup[did].fields[did];
|
|
981
|
+
const keys = Object.keys(fields);
|
|
982
|
+
return keys;
|
|
1522
983
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
//
|
|
1526
|
-
// THE COMPACTNESS ANALYTICS NEED THE FOLLOWING DATA,
|
|
1527
|
-
// IN ADDITION TO THE MAP (IDEALLY, GEO_IDS INDEXED BY DISTRICT_ID)
|
|
1528
|
-
//
|
|
1529
|
-
// Shapes by geo_id
|
|
1530
|
-
//
|
|
1531
|
-
// If we need/want to speed compactness calculations up, we'll need
|
|
1532
|
-
// to calculate the perimeters and diameters (and areas) of districts implicitly,
|
|
1533
|
-
// which will require identifying district boundaries initially and updating
|
|
1534
|
-
// them incrementally as districts are (re)assigned.
|
|
1535
|
-
//
|
|
1536
|
-
// A district's boundary info is the set/list of features that constitute the
|
|
1537
|
-
// district's border along with each boundary feature's neighbors. Hence, this
|
|
1538
|
-
// requires a contiguity graph with the lengths of shared edges between features
|
|
1539
|
-
// precomputed.
|
|
1540
|
-
//
|
|
1541
|
-
// NOTE - I can write up (if not implement) the logic for determining what shapes
|
|
1542
|
-
// constitute a district's boundary. There are a few nuances.
|
|
1543
|
-
//
|
|
1544
|
-
// If we have to optimize like this when we generalize to mixed maps, the
|
|
1545
|
-
// determination of "neighbors in the map" and the length of shared borders
|
|
1546
|
-
// (for determining a district perimeters) becomes more complicated and dynamic.
|
|
1547
|
-
//
|
|
1548
|
-
// NOTE - Again, I can write up (if not implement) the logic for both of these.
|
|
1549
|
-
// They are a bit tricky and require special preprocessing of the summary level
|
|
1550
|
-
// hierarchy (which I also have a script for that we can repurpose).
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
/***/ }),
|
|
1554
|
-
|
|
1555
|
-
/***/ "./src/equal.ts":
|
|
1556
|
-
/*!**********************!*\
|
|
1557
|
-
!*** ./src/equal.ts ***!
|
|
1558
|
-
\**********************/
|
|
1559
|
-
/***/ ((__unused_webpack_module, exports) => {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
//
|
|
1563
|
-
// EQUAL POPULATION
|
|
1564
|
-
//
|
|
1565
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1566
|
-
exports.doHasEqualPopulations = doHasEqualPopulations;
|
|
1567
|
-
// MMD - This generalizes for variable #'s of reps per district.
|
|
1568
|
-
// NOTE - This validity check is *derived* and depends on population deviation %
|
|
1569
|
-
// being computed (above) and normalized in test log & scorecard generation.
|
|
1570
|
-
function doHasEqualPopulations(s, bLog = false) {
|
|
1571
|
-
let test = s.getTest(3 /* T.Test.EqualPopulation */);
|
|
1572
|
-
// Get the normalized population deviation %
|
|
1573
|
-
let popDevTest = s.getTest(4 /* T.Test.PopulationDeviation */);
|
|
1574
|
-
const popDevPct = popDevTest['score'];
|
|
1575
|
-
// const popDevNormalized: number = popDevTest['normalizedScore'] as number;
|
|
1576
|
-
test['details']['deviation'] = popDevPct;
|
|
1577
|
-
test['details']['thresholds'] = popDevTest['details']['scale'];
|
|
1578
|
-
// Populate the N+1 summary "district" in district.statistics
|
|
1579
|
-
let bEqualPop = s.districts.table.bEqualPop;
|
|
1580
|
-
let summaryRow = s.districts.numberOfRows() - 1;
|
|
1581
|
-
bEqualPop[summaryRow] = test['score'];
|
|
1582
|
-
return test;
|
|
984
|
+
function unpackedFields(aggs, did, field) {
|
|
985
|
+
return aggs.map(agg => dra_types_1.PF.getPackedField(agg, did, field));
|
|
1583
986
|
}
|
|
987
|
+
// END
|
|
1584
988
|
|
|
1585
989
|
|
|
1586
990
|
/***/ }),
|
|
@@ -1612,11 +1016,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
1612
1016
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1613
1017
|
exports.uncertaintyOfMembership = exports.effectiveSplits = exports.avgSVError = exports.isAntimajoritarian = exports.normalizePartisanBias = exports.ratePartisanBias = exports.estSeatProbability = exports.inferSelectedMinority = exports.fieldForFeature = exports.geoIDForFeature = void 0;
|
|
1614
1018
|
__exportStar(__webpack_require__(/*! ./_api */ "./src/_api.ts"), exports);
|
|
1615
|
-
var
|
|
1616
|
-
Object.defineProperty(exports, "geoIDForFeature", ({ enumerable: true, get: function () { return
|
|
1617
|
-
Object.defineProperty(exports, "fieldForFeature", ({ enumerable: true, get: function () { return
|
|
1618
|
-
|
|
1619
|
-
|
|
1019
|
+
var data_1 = __webpack_require__(/*! ./data */ "./src/data.ts");
|
|
1020
|
+
Object.defineProperty(exports, "geoIDForFeature", ({ enumerable: true, get: function () { return data_1.geoIDForFeature; } }));
|
|
1021
|
+
Object.defineProperty(exports, "fieldForFeature", ({ enumerable: true, get: function () { return data_1.fieldForFeature; } }));
|
|
1022
|
+
var rpv_1 = __webpack_require__(/*! ./rpv */ "./src/rpv.ts");
|
|
1023
|
+
Object.defineProperty(exports, "inferSelectedMinority", ({ enumerable: true, get: function () { return rpv_1.inferSelectedMinority; } }));
|
|
1620
1024
|
__exportStar(__webpack_require__(/*! ./types */ "./src/types.ts"), exports);
|
|
1621
1025
|
__exportStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"), exports);
|
|
1622
1026
|
// Re-export RPV types and COI splitting functions
|
|
@@ -1628,63 +1032,7 @@ exports.isAntimajoritarian = dra_analytics_1.Rate.isAntimajoritarian;
|
|
|
1628
1032
|
exports.avgSVError = dra_analytics_1.Partisan.avgSVError;
|
|
1629
1033
|
exports.effectiveSplits = dra_analytics_1.Splitting.effectiveSplits;
|
|
1630
1034
|
exports.uncertaintyOfMembership = dra_analytics_1.Splitting.uncertaintyOfMembership;
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
/***/ }),
|
|
1634
|
-
|
|
1635
|
-
/***/ "./src/minority.ts":
|
|
1636
|
-
/*!*************************!*\
|
|
1637
|
-
!*** ./src/minority.ts ***!
|
|
1638
|
-
\*************************/
|
|
1639
|
-
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
//
|
|
1643
|
-
// PROTECTS MINORITIES
|
|
1644
|
-
//
|
|
1645
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
1646
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
1647
|
-
};
|
|
1648
|
-
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1649
|
-
exports.getMajorityMinority = getMajorityMinority;
|
|
1650
|
-
exports.getVRASection5 = getVRASection5;
|
|
1651
|
-
exports.doAnalyzeRacialPolarization = doAnalyzeRacialPolarization;
|
|
1652
|
-
const dra_analytics_1 = __webpack_require__(/*! @dra2020/dra-analytics */ "@dra2020/dra-analytics");
|
|
1653
|
-
const majority_minority_json_1 = __importDefault(__webpack_require__(/*! ../static/majority-minority.json */ "./static/majority-minority.json"));
|
|
1654
|
-
const vra5_preclearance_json_1 = __importDefault(__webpack_require__(/*! ../static/vra5-preclearance.json */ "./static/vra5-preclearance.json"));
|
|
1655
|
-
// TODO - Update/revise this, when the update comes out in September:
|
|
1656
|
-
// Sources for majority-minority info:
|
|
1657
|
-
// - https://en.wikipedia.org/wiki/List_of_majority-minority_United_States_congressional_districts
|
|
1658
|
-
// - http://www.ncsl.org/Portals/1/Documents/Redistricting/Redistricting_2010.pdf (PP. 80–84)
|
|
1659
|
-
// - https://www.justice.gov/crt/jurisdictions-previously-covered-section-5
|
|
1660
|
-
function getMajorityMinority(s) {
|
|
1661
|
-
const xx = s.state.xx;
|
|
1662
|
-
const mMDict = majority_minority_json_1.default;
|
|
1663
|
-
const stateMM = mMDict[xx];
|
|
1664
|
-
return stateMM;
|
|
1665
|
-
}
|
|
1666
|
-
function getVRASection5(s) {
|
|
1667
|
-
const xx = s.state.xx;
|
|
1668
|
-
const vraPreDict = vra5_preclearance_json_1.default;
|
|
1669
|
-
const stateVRAPre = vraPreDict[xx];
|
|
1670
|
-
return stateVRAPre;
|
|
1671
|
-
}
|
|
1672
|
-
// RPV - pulled into a separate component 11-17-2020
|
|
1673
|
-
function doAnalyzeRacialPolarization(s, districtID, groups, bLog = false) {
|
|
1674
|
-
// Make sure that a minority is specified
|
|
1675
|
-
if (!(groups.black || groups.hispanic || groups.pacific || groups.asian || groups.native || groups.minority))
|
|
1676
|
-
return undefined;
|
|
1677
|
-
const points = s.districts.extractVotesByDemographic(districtID, groups, bLog);
|
|
1678
|
-
// Make sure the district is not empty & there are enough points
|
|
1679
|
-
if (points === undefined)
|
|
1680
|
-
return undefined;
|
|
1681
|
-
if (points.comparison === undefined)
|
|
1682
|
-
return undefined;
|
|
1683
|
-
if (points.comparison.length <= 10)
|
|
1684
|
-
return undefined;
|
|
1685
|
-
return dra_analytics_1.Minority.analyzeRacialVoting(points, districtID, groups);
|
|
1686
|
-
}
|
|
1687
|
-
// RPV is in the dra-analytics package.
|
|
1035
|
+
// END
|
|
1688
1036
|
|
|
1689
1037
|
|
|
1690
1038
|
/***/ }),
|
|
@@ -1733,72 +1081,67 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
1733
1081
|
};
|
|
1734
1082
|
})();
|
|
1735
1083
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1736
|
-
exports.
|
|
1084
|
+
exports.initialize = initialize;
|
|
1085
|
+
const T = __importStar(__webpack_require__(/*! ./types */ "./src/types.ts"));
|
|
1737
1086
|
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
1738
|
-
const D = __importStar(__webpack_require__(/*! ./
|
|
1739
|
-
|
|
1740
|
-
|
|
1087
|
+
const D = __importStar(__webpack_require__(/*! ./data */ "./src/data.ts"));
|
|
1088
|
+
function initialize(s) {
|
|
1089
|
+
// NOTE - Do preprocessing separately, so the constructor returns quickly.
|
|
1741
1090
|
// If necessary, do one-time preprocessing
|
|
1742
|
-
if (!s.bOneTimeProcessingDone) {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
s.bOneTimeProcessingDone = true;
|
|
1091
|
+
if (!s.data.bOneTimeProcessingDone) {
|
|
1092
|
+
preprocessCountyFeatures(s.data, s.bLog);
|
|
1093
|
+
preprocessCensus(s.data, s.bLog);
|
|
1094
|
+
s.data.features.mapGeoIDsToFeatureIDs();
|
|
1095
|
+
s.data.bOneTimeProcessingDone = true;
|
|
1747
1096
|
}
|
|
1748
|
-
|
|
1749
|
-
s.plan.invertPlan(bLog);
|
|
1750
|
-
// Create a map of geoIDs to feature IDs
|
|
1751
|
-
s.features.mapGeoIDsToFeatureIDs();
|
|
1097
|
+
s.data.plan.invertPlan(s.bLog);
|
|
1752
1098
|
}
|
|
1753
1099
|
// CREATE A FIPS CODE TO COUNTY NAME LOOKUP
|
|
1754
|
-
function
|
|
1100
|
+
function preprocessCountyFeatures(container, bLog = false) {
|
|
1755
1101
|
let fipsCodes = [];
|
|
1756
1102
|
// CREATE A MAP OF FIPS CODES TO NAMES
|
|
1757
|
-
for (let i = 0; i <
|
|
1758
|
-
let county =
|
|
1759
|
-
let fips =
|
|
1103
|
+
for (let i = 0; i < container.counties.nCounties; i++) {
|
|
1104
|
+
let county = container.counties.countyByIndex(i);
|
|
1105
|
+
let fips = container.counties.propertyForCounty(county, 'COUNTYFP');
|
|
1760
1106
|
fipsCodes.push(fips);
|
|
1761
|
-
let name =
|
|
1762
|
-
|
|
1107
|
+
let name = container.counties.propertyForCounty(county, 'NAME');
|
|
1108
|
+
container.counties.mapFIPSToName(fips, name);
|
|
1763
1109
|
}
|
|
1764
1110
|
// CREATE A FIPS CODE-ORDINAL MAP
|
|
1765
1111
|
// Sort the FIPS codes in the county shapes
|
|
1766
1112
|
fipsCodes = fipsCodes.sort();
|
|
1113
|
+
// NOTE - 02/05/25 - I'm not sure why we're adding the virtual county bucket here,
|
|
1114
|
+
// because we strip it off in creating the profile for analytics. To be safe, I'm
|
|
1115
|
+
// Keeping it here and stripping it off later.
|
|
1767
1116
|
// Add a dummy county, for county-district splitting analysis.
|
|
1768
1117
|
fipsCodes.unshift('000');
|
|
1769
|
-
// NOTE - This was added for the legacy SPLITTING implementation.
|
|
1770
1118
|
// Create the ID-ordinal map
|
|
1771
1119
|
for (let i in fipsCodes) {
|
|
1772
|
-
|
|
1120
|
+
container.counties.index[fipsCodes[i]] = Number(i);
|
|
1773
1121
|
// 07-17-21 - For enumerating # of times each split county is split
|
|
1774
|
-
|
|
1122
|
+
container.counties.fips[Number(i)] = fipsCodes[i];
|
|
1775
1123
|
}
|
|
1776
1124
|
}
|
|
1777
1125
|
// ANALYZE THE CENSUS BY COUNTY
|
|
1778
|
-
function
|
|
1779
|
-
// The county-splitting analytic needs the following info, using NC as an example:
|
|
1780
|
-
// '_stateTotal' = The total state population, e.g., 9,535,483 for NC's 2010 Census
|
|
1781
|
-
// 'totalByCounty' = The total population by county FIPS code
|
|
1126
|
+
function preprocessCensus(container, bLog = false) {
|
|
1782
1127
|
// SUM TOTAL POPULATION BY COUNTY
|
|
1783
1128
|
let totalByCounty = {};
|
|
1784
1129
|
// NOTE - This works w/o GEOIDs, because you're looping over all features.
|
|
1785
|
-
for (let i = 0; i <
|
|
1786
|
-
let f =
|
|
1130
|
+
for (let i = 0; i < container.features.nFeatures(); i++) {
|
|
1131
|
+
let f = container.features.featureByIndex(i);
|
|
1787
1132
|
// 03-27-21
|
|
1788
1133
|
let geoID = D.geoIDForFeature(f);
|
|
1789
|
-
// let geoID: string = s.features.geoIDForFeature(f);
|
|
1790
1134
|
// Skip water-only features
|
|
1791
1135
|
if (!(U.isWaterOnly(geoID))) {
|
|
1792
1136
|
// 03-27-21
|
|
1793
|
-
const dk =
|
|
1137
|
+
const dk = container.features._keys[T.Dataset.CENSUS];
|
|
1794
1138
|
let value = D.fieldForFeature(f, dk, 0 /* T.FeatureField.TotalPop */);
|
|
1795
|
-
// let value: number = s.features.fieldForFeature(f, T.Dataset.CENSUS, T.FeatureField.TotalPop);
|
|
1796
1139
|
// Sum total population across the state
|
|
1797
|
-
|
|
1140
|
+
container.state.totalPop += value;
|
|
1798
1141
|
// Get the county FIPS code for the feature
|
|
1799
1142
|
let countyFIPS = U.parseGeoID(geoID)['county'];
|
|
1800
1143
|
// Ignore features when the county is unrecognized
|
|
1801
|
-
if (U.keyExists(countyFIPS,
|
|
1144
|
+
if (U.keyExists(countyFIPS, container.counties.index)) {
|
|
1802
1145
|
// If a subtotal for the county doesn't exist, initialize one
|
|
1803
1146
|
if (!(U.keyExists(countyFIPS, totalByCounty))) {
|
|
1804
1147
|
totalByCounty[countyFIPS] = 0;
|
|
@@ -1816,40 +1159,21 @@ function doPreprocessCensus(s, bLog = false) {
|
|
|
1816
1159
|
console.log("Skipping water-only feature in Census preprocessing:", geoID);
|
|
1817
1160
|
}
|
|
1818
1161
|
}
|
|
1819
|
-
// NOTE - The above could be replaced, if I got totals on county.geojson.
|
|
1820
|
-
/* Moved this to doPreprocessCountyFeatures() - 09-14-2020 to fix VA county mismatch bug
|
|
1821
|
-
// CREATE A FIPS CODE-ORDINAL MAP
|
|
1822
|
-
|
|
1823
|
-
// Get the county FIPS codes
|
|
1824
|
-
let fipsCodes = U.getObjectKeys(totalByCounty);
|
|
1825
|
-
// Sort the results
|
|
1826
|
-
fipsCodes = fipsCodes.sort();
|
|
1827
|
-
|
|
1828
|
-
// NOTE - This was added for the legacy SPLITTING implementation.
|
|
1829
|
-
// Add a dummy county, for county-district splitting analysis.
|
|
1830
|
-
fipsCodes.unshift('000');
|
|
1831
|
-
|
|
1832
|
-
// Create the ID-ordinal map
|
|
1833
|
-
for (let i in fipsCodes)
|
|
1834
|
-
{
|
|
1835
|
-
s.counties.index[fipsCodes[i]] = Number(i);
|
|
1836
|
-
}
|
|
1837
|
-
*/
|
|
1838
1162
|
// MAKE AN ARRAY OF TOTAL POPULATIONS BY COUNTY INDEX
|
|
1839
1163
|
// Add an extra 0th virtual county bucket for county-district splitting analysis
|
|
1840
|
-
let nCountyBuckets =
|
|
1164
|
+
let nCountyBuckets = container.counties.nCounties + 1;
|
|
1841
1165
|
let countyTotals = U.initArray(nCountyBuckets, 0);
|
|
1842
1166
|
for (let fipsCode in totalByCounty) {
|
|
1843
|
-
let i =
|
|
1167
|
+
let i = container.counties.indexFromFIPS(fipsCode);
|
|
1844
1168
|
countyTotals[i] = totalByCounty[fipsCode];
|
|
1845
1169
|
}
|
|
1846
|
-
|
|
1170
|
+
container.counties.totalPopulation = countyTotals;
|
|
1847
1171
|
// MMD - Rationalized calculation of target size *per rep*
|
|
1848
|
-
|
|
1172
|
+
container.state.targetSize = Math.round(container.state.totalPop / container.state.nReps);
|
|
1849
1173
|
// ANALYZE THE COUNTIES
|
|
1850
1174
|
// MMD - This "these counties must be split" analysis does NOT generalize to heterogenous MMD.
|
|
1851
1175
|
// 'target_size': 733499, # calc as total / districts
|
|
1852
|
-
const targetSize =
|
|
1176
|
+
const targetSize = container.state.targetSize;
|
|
1853
1177
|
// let targetSize = Math.round(s.state.totalPop / s.state.nDistricts);
|
|
1854
1178
|
// Find counties that are bigger than the target district size.
|
|
1855
1179
|
// 'too_big' = The counties that are bigger than a district, e.g., ['Mecklenburg', 'Wake']
|
|
@@ -1862,7 +1186,7 @@ function doPreprocessCensus(s, bLog = false) {
|
|
|
1862
1186
|
let expectedSplits = 0;
|
|
1863
1187
|
let expectedAffected = 0;
|
|
1864
1188
|
// Get the county FIPS codes
|
|
1865
|
-
let fipsCodes = U.getObjectKeys(
|
|
1189
|
+
let fipsCodes = U.getObjectKeys(container.counties.index);
|
|
1866
1190
|
// Loop over the counties
|
|
1867
1191
|
for (let county in fipsCodes) {
|
|
1868
1192
|
let fipsCode = fipsCodes[county];
|
|
@@ -1878,37 +1202,166 @@ function doPreprocessCensus(s, bLog = false) {
|
|
|
1878
1202
|
if (countySplits > 0) {
|
|
1879
1203
|
countyAffected = totalByCounty[fipsCode] % targetSize;
|
|
1880
1204
|
tooBigFIPS.push(fipsCode);
|
|
1881
|
-
tooBigName.push(
|
|
1205
|
+
tooBigName.push(container.counties.nameFromFIPS(fipsCode));
|
|
1882
1206
|
singleCountyDistrictMax += countySplits;
|
|
1883
1207
|
}
|
|
1884
1208
|
expectedSplits += countySplits;
|
|
1885
1209
|
expectedAffected += countyAffected;
|
|
1886
1210
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1211
|
+
container.state.tooBigFIPS = tooBigFIPS;
|
|
1212
|
+
container.state.tooBigName = tooBigName;
|
|
1213
|
+
container.state.expectedSplits = expectedSplits;
|
|
1214
|
+
container.state.expectedAffected = Math.round(expectedAffected);
|
|
1215
|
+
container.state.singleCountyDistrictMax = singleCountyDistrictMax;
|
|
1216
|
+
}
|
|
1217
|
+
// END
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
/***/ }),
|
|
1221
|
+
|
|
1222
|
+
/***/ "./src/requirements.ts":
|
|
1223
|
+
/*!*****************************!*\
|
|
1224
|
+
!*** ./src/requirements.ts ***!
|
|
1225
|
+
\*****************************/
|
|
1226
|
+
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
//
|
|
1230
|
+
// MAP/PLAN REQUIREMENTS CHECKS ("CONSTRAINTS")
|
|
1231
|
+
//
|
|
1232
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
1233
|
+
if (k2 === undefined) k2 = k;
|
|
1234
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
1235
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
1236
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
1237
|
+
}
|
|
1238
|
+
Object.defineProperty(o, k2, desc);
|
|
1239
|
+
}) : (function(o, m, k, k2) {
|
|
1240
|
+
if (k2 === undefined) k2 = k;
|
|
1241
|
+
o[k2] = m[k];
|
|
1242
|
+
}));
|
|
1243
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
1244
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
1245
|
+
}) : function(o, v) {
|
|
1246
|
+
o["default"] = v;
|
|
1247
|
+
});
|
|
1248
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
1249
|
+
var ownKeys = function(o) {
|
|
1250
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
1251
|
+
var ar = [];
|
|
1252
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
1253
|
+
return ar;
|
|
1254
|
+
};
|
|
1255
|
+
return ownKeys(o);
|
|
1256
|
+
};
|
|
1257
|
+
return function (mod) {
|
|
1258
|
+
if (mod && mod.__esModule) return mod;
|
|
1259
|
+
var result = {};
|
|
1260
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
1261
|
+
__setModuleDefault(result, mod);
|
|
1262
|
+
return result;
|
|
1263
|
+
};
|
|
1264
|
+
})();
|
|
1265
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1266
|
+
exports.isComplete = isComplete;
|
|
1267
|
+
exports.emptyDistricts = emptyDistricts;
|
|
1268
|
+
exports.getUnassignedFeatures = getUnassignedFeatures;
|
|
1269
|
+
exports.isContiguous = isContiguous;
|
|
1270
|
+
exports.isEmbedded = isEmbedded;
|
|
1271
|
+
const dra_analytics_1 = __webpack_require__(/*! @dra2020/dra-analytics */ "@dra2020/dra-analytics");
|
|
1272
|
+
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
1273
|
+
const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
|
|
1274
|
+
// COMPLETE-NESS
|
|
1275
|
+
/*
|
|
1276
|
+
* A plan is "complete" if all districts have non-zero population, and
|
|
1277
|
+
* all unassigned features have zero population.
|
|
1278
|
+
*
|
|
1279
|
+
* Note: This helper does not verify the correct number of districts:
|
|
1280
|
+
* That is the caller's responsibility.
|
|
1281
|
+
*/
|
|
1282
|
+
function isComplete(popByDistricts) {
|
|
1283
|
+
if (popByDistricts[0] > 0)
|
|
1284
|
+
return false; // Unassigned pop
|
|
1285
|
+
for (let d = 1; d < popByDistricts.length; d++) {
|
|
1286
|
+
if (popByDistricts[d] <= 0)
|
|
1287
|
+
return false; // Empty district
|
|
1288
|
+
}
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
function emptyDistricts(popByDistricts) {
|
|
1292
|
+
let emptyDistricts = [];
|
|
1293
|
+
for (let d = 1; d < popByDistricts.length; d++) {
|
|
1294
|
+
if (popByDistricts[d] <= 0)
|
|
1295
|
+
emptyDistricts.push(d); // Empty district
|
|
1296
|
+
}
|
|
1297
|
+
return emptyDistricts;
|
|
1298
|
+
}
|
|
1299
|
+
function getUnassignedFeatures(container) {
|
|
1300
|
+
// Simulate an old school test entry, to preserve this and downstream code
|
|
1301
|
+
let test = {};
|
|
1302
|
+
const bLog = container._session.bLog;
|
|
1303
|
+
let unassignedFeatures = [];
|
|
1304
|
+
// Is all population assigned to real districts?
|
|
1305
|
+
// If not, collect populated features (not water-only or uninhabited).
|
|
1306
|
+
if (container.aggregates.columns.totalPop[S.NOT_ASSIGNED] > 0) {
|
|
1307
|
+
const geoIDs = Array.from(container.plan.geoIDsForDistrictID(S.NOT_ASSIGNED));
|
|
1308
|
+
geoIDs.forEach(function (geoID) {
|
|
1309
|
+
if (U.isWaterOnly(geoID, container)) {
|
|
1310
|
+
if (bLog)
|
|
1311
|
+
console.log("Unassigned water-only feature ignored in completeness check: ", geoID);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (U.isUninhabited(geoID, container)) {
|
|
1315
|
+
if (bLog)
|
|
1316
|
+
console.log("Uninhabited feature ignored in completeness check: ", geoID);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
// Not water-only and inhabited
|
|
1320
|
+
unassignedFeatures.push(geoID);
|
|
1321
|
+
return;
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
return unassignedFeatures;
|
|
1325
|
+
}
|
|
1326
|
+
// CONTIGUITY
|
|
1327
|
+
/*
|
|
1328
|
+
* A district is contiguous if all geo's in the set are connected
|
|
1329
|
+
* via the contiguity graph.
|
|
1330
|
+
*
|
|
1331
|
+
* Use this helper to test individual districts in dra-client.
|
|
1332
|
+
* AND all the district results together to test plan contiguity.
|
|
1333
|
+
*/
|
|
1334
|
+
function isContiguous(geoids, graph, bLog = false) {
|
|
1335
|
+
return dra_analytics_1.Graph.isConnected(geoids, graph, bLog);
|
|
1336
|
+
}
|
|
1337
|
+
// FREE OF HOLES (NOT EMBEDDED)
|
|
1338
|
+
/*
|
|
1339
|
+
* A district is NOT a "donut hole" district:
|
|
1340
|
+
* - If any neighbor is 'OUT_OF_STATE'; or
|
|
1341
|
+
* - If there are 2 or more neighboring districts.
|
|
1342
|
+
*
|
|
1343
|
+
* Use this helper to test individual districts in dra-client.
|
|
1344
|
+
* AND all the district results together to test whether a plan is free of holes.
|
|
1345
|
+
*/
|
|
1346
|
+
function isEmbedded(districtID, geoids, plan, graph, bLog = false) {
|
|
1347
|
+
return dra_analytics_1.Graph.isEmbedded(districtID, geoids, plan, graph, bLog);
|
|
1892
1348
|
}
|
|
1893
|
-
//
|
|
1894
|
-
// function doPreprocessElection(s: AnalyticsSession, bLog: boolean = false): void
|
|
1895
|
-
// {
|
|
1896
|
-
// if (bLog) console.log("Preprocessing election data ...");
|
|
1897
|
-
// }
|
|
1349
|
+
// END
|
|
1898
1350
|
|
|
1899
1351
|
|
|
1900
1352
|
/***/ }),
|
|
1901
1353
|
|
|
1902
|
-
/***/ "./src/
|
|
1903
|
-
|
|
1904
|
-
!*** ./src/
|
|
1905
|
-
|
|
1354
|
+
/***/ "./src/rpv.ts":
|
|
1355
|
+
/*!********************!*\
|
|
1356
|
+
!*** ./src/rpv.ts ***!
|
|
1357
|
+
\********************/
|
|
1906
1358
|
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
1907
1359
|
|
|
1908
1360
|
|
|
1909
1361
|
//
|
|
1910
|
-
//
|
|
1911
|
-
//
|
|
1362
|
+
// RACIALLY POLARIZED VOTING (RPV) SUPPORT
|
|
1363
|
+
// The main RPV code is in the dra-analytics package.
|
|
1364
|
+
//
|
|
1912
1365
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
1913
1366
|
if (k2 === undefined) k2 = k;
|
|
1914
1367
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -1943,165 +1396,163 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
1943
1396
|
};
|
|
1944
1397
|
})();
|
|
1945
1398
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1946
|
-
exports.
|
|
1947
|
-
exports.
|
|
1948
|
-
|
|
1399
|
+
exports.doRPV = doRPV;
|
|
1400
|
+
exports.inferSelectedMinority = inferSelectedMinority;
|
|
1401
|
+
const dra_analytics_1 = __webpack_require__(/*! @dra2020/dra-analytics */ "@dra2020/dra-analytics");
|
|
1402
|
+
const T = __importStar(__webpack_require__(/*! ./types */ "./src/types.ts"));
|
|
1949
1403
|
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
1950
|
-
const
|
|
1951
|
-
function
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
1404
|
+
const data_1 = __webpack_require__(/*! ./data */ "./src/data.ts");
|
|
1405
|
+
function doRPV(container, districtID, groups, bLog = false) {
|
|
1406
|
+
// Make sure that a minority is specified
|
|
1407
|
+
if (!(groups.black || groups.hispanic || groups.pacific || groups.asian || groups.native || groups.minority))
|
|
1408
|
+
return undefined;
|
|
1409
|
+
const points = extractVotesByDemographic(container, districtID, groups, bLog);
|
|
1410
|
+
// Make sure the district is not empty & there are enough points
|
|
1411
|
+
if (points === undefined)
|
|
1412
|
+
return undefined;
|
|
1413
|
+
if (points.comparison === undefined)
|
|
1414
|
+
return undefined;
|
|
1415
|
+
if (points.comparison.length <= 10)
|
|
1416
|
+
return undefined;
|
|
1417
|
+
return dra_analytics_1.Minority.analyzeRacialVoting(points, districtID, groups);
|
|
1418
|
+
}
|
|
1419
|
+
function extractVotesByDemographic(container, districtID, groups, bLog = false) {
|
|
1420
|
+
let ids = [];
|
|
1421
|
+
let comparisonPts = []; // Either White -or- (1 – <selected minority>) i.e., White++
|
|
1422
|
+
let hispanicPts = [];
|
|
1423
|
+
let blackPts = [];
|
|
1424
|
+
let pacificPts = [];
|
|
1425
|
+
let asianPts = [];
|
|
1426
|
+
let nativePts = [];
|
|
1427
|
+
let minorityPts = [];
|
|
1428
|
+
// Gather [demographic %, D %] points for the selected district ID
|
|
1429
|
+
let i = districtID;
|
|
1430
|
+
// HACK - For comparing a minority group to everyone else (vs. just White),
|
|
1431
|
+
// infer the *single* minority group that has been selected.
|
|
1432
|
+
const selectedMinority = inferSelectedMinority(groups);
|
|
1433
|
+
// Get the geoIDs assigned to the district
|
|
1434
|
+
// Guard against empty districts
|
|
1435
|
+
let geoIDs = container.plan.geoIDsForDistrictID(i);
|
|
1436
|
+
if (geoIDs && (geoIDs.size > 0)) {
|
|
1437
|
+
// ... loop over the geoIDs collecting the points for ecological regression
|
|
1438
|
+
geoIDs.forEach(function (geoID) {
|
|
1439
|
+
// Skip water-only features
|
|
1440
|
+
if (!(U.isWaterOnly(geoID))) {
|
|
1441
|
+
// Map from geoID to feature index
|
|
1442
|
+
let featureID = container.features.featureID(geoID);
|
|
1443
|
+
let f = container.features.featureByIndex(featureID);
|
|
1444
|
+
if (!(f == undefined)) {
|
|
1445
|
+
// Calculate the Dem two-party vote
|
|
1446
|
+
// 03-27-21
|
|
1447
|
+
const dkELECTION = container.features._keys[T.Dataset.ELECTION];
|
|
1448
|
+
const featureDem = (0, data_1.fieldForFeature)(f, dkELECTION, 7 /* T.FeatureField.DemVotes */);
|
|
1449
|
+
const featureRep = (0, data_1.fieldForFeature)(f, dkELECTION, 8 /* T.FeatureField.RepVotes */);
|
|
1450
|
+
if ((featureDem + featureRep) > 0) {
|
|
1451
|
+
const pctDem = featureDem / (featureDem + featureRep);
|
|
1452
|
+
// Calculate the VAP/CVAP percentages by demographic
|
|
1453
|
+
// 03-27-21
|
|
1454
|
+
const dkVAP = container.features._keys[T.Dataset.VAP];
|
|
1455
|
+
const totalVAP = (0, data_1.fieldForFeature)(f, dkVAP, 0 /* T.FeatureField.TotalPop */);
|
|
1456
|
+
if (totalVAP > 0) {
|
|
1457
|
+
// Gather all points, for debugging purposes ...
|
|
1458
|
+
const hispanicVAP = (0, data_1.fieldForFeature)(f, dkVAP, 3 /* T.FeatureField.HispanicPop */);
|
|
1459
|
+
const blackVAP = (0, data_1.fieldForFeature)(f, dkVAP, 2 /* T.FeatureField.BlackPop */);
|
|
1460
|
+
const pacificVAP = (0, data_1.fieldForFeature)(f, dkVAP, 5 /* T.FeatureField.PacificPop */);
|
|
1461
|
+
const asianVAP = (0, data_1.fieldForFeature)(f, dkVAP, 4 /* T.FeatureField.AsianPop */);
|
|
1462
|
+
const nativeVAP = (0, data_1.fieldForFeature)(f, dkVAP, 6 /* T.FeatureField.NativePop */);
|
|
1463
|
+
const pctHispanic = hispanicVAP / totalVAP;
|
|
1464
|
+
const pctBlack = blackVAP / totalVAP;
|
|
1465
|
+
const pctPacific = pacificVAP / totalVAP;
|
|
1466
|
+
const pctAsian = asianVAP / totalVAP;
|
|
1467
|
+
const pctNative = nativeVAP / totalVAP;
|
|
1468
|
+
const whiteVAP = (0, data_1.fieldForFeature)(f, dkVAP, 1 /* T.FeatureField.WhitePop */);
|
|
1469
|
+
const minorityVAP = totalVAP - whiteVAP;
|
|
1470
|
+
const pctMinority = minorityVAP / totalVAP;
|
|
1471
|
+
let pctComparison = whiteVAP / totalVAP;
|
|
1472
|
+
if (groups.invertSelection) {
|
|
1473
|
+
switch (selectedMinority) {
|
|
1474
|
+
case 'Hispanic': {
|
|
1475
|
+
pctComparison = (totalVAP - hispanicVAP) / totalVAP;
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
case 'Black': {
|
|
1479
|
+
pctComparison = (totalVAP - blackVAP) / totalVAP;
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
case 'Pacific': {
|
|
1483
|
+
pctComparison = (totalVAP - pacificVAP) / totalVAP;
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
case 'Asian': {
|
|
1487
|
+
pctComparison = (totalVAP - asianVAP) / totalVAP;
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
case 'Native': {
|
|
1491
|
+
pctComparison = (totalVAP - nativeVAP) / totalVAP;
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
case 'Minority': {
|
|
1495
|
+
pctComparison = (totalVAP - minorityVAP) / totalVAP;
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
default: {
|
|
1499
|
+
console.log("Selected minority not recognized!");
|
|
1500
|
+
break;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
ids.push(geoID);
|
|
1505
|
+
comparisonPts.push({ x: pctComparison, y: pctDem }); // White -or- (1 – <selected minority>)
|
|
1506
|
+
hispanicPts.push({ x: pctHispanic, y: pctDem });
|
|
1507
|
+
blackPts.push({ x: pctBlack, y: pctDem });
|
|
1508
|
+
pacificPts.push({ x: pctPacific, y: pctDem });
|
|
1509
|
+
asianPts.push({ x: pctAsian, y: pctDem });
|
|
1510
|
+
nativePts.push({ x: pctNative, y: pctDem });
|
|
1511
|
+
minorityPts.push({ x: pctMinority, y: pctDem });
|
|
1512
|
+
}
|
|
1513
|
+
else {
|
|
1514
|
+
if (bLog)
|
|
1515
|
+
console.log("RPV: Total VAP not > 0.");
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
if (bLog)
|
|
1520
|
+
console.log("RPV: D + R not > 0.");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
2013
1523
|
}
|
|
1524
|
+
});
|
|
1525
|
+
// ... but only keep points for the demographics that are going to be analyzed
|
|
1526
|
+
return {
|
|
1527
|
+
ids: ids,
|
|
1528
|
+
comparison: comparisonPts,
|
|
1529
|
+
minority: groups.minority ? minorityPts : [],
|
|
1530
|
+
black: groups.black ? blackPts : [],
|
|
1531
|
+
hispanic: groups.hispanic ? hispanicPts : [],
|
|
1532
|
+
pacific: groups.pacific ? pacificPts : [],
|
|
1533
|
+
asian: groups.asian ? asianPts : [],
|
|
1534
|
+
native: groups.native ? nativePts : []
|
|
2014
1535
|
};
|
|
2015
1536
|
}
|
|
2016
|
-
|
|
1537
|
+
// If a district is empty
|
|
1538
|
+
return undefined;
|
|
2017
1539
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
bContiguous: s.districts.table.bContiguous[i],
|
|
2033
|
-
bNotEmbedded: s.districts.table.bNotEmbedded[i],
|
|
2034
|
-
demPct: s.districts.table.demPct[i],
|
|
2035
|
-
repPct: s.districts.table.repPct[i],
|
|
2036
|
-
othPct: s.districts.table.otherPct[i],
|
|
2037
|
-
whitePct: s.districts.table.whitePct[i],
|
|
2038
|
-
totalVAP: s.districts.table.totalVAP[i],
|
|
2039
|
-
minorityPct: s.districts.table.minorityPct[i],
|
|
2040
|
-
blackPct: s.districts.table.blackPct[i],
|
|
2041
|
-
hispanicPct: s.districts.table.hispanicPct[i],
|
|
2042
|
-
pacificPct: s.districts.table.pacificPct[i],
|
|
2043
|
-
asianPct: s.districts.table.asianPct[i],
|
|
2044
|
-
nativePct: s.districts.table.nativePct[i]
|
|
2045
|
-
};
|
|
2046
|
-
const readyRow = U.deepCopy(rawRow);
|
|
2047
|
-
dsTable.push(readyRow);
|
|
2048
|
-
}
|
|
2049
|
-
const dsDetails = {
|
|
2050
|
-
// None at this time
|
|
2051
|
-
};
|
|
2052
|
-
const dsDatasets = {
|
|
2053
|
-
shapes: U.deepCopy(s.config['descriptions']['SHAPES']), // 2020
|
|
2054
|
-
census: U.deepCopy(s.config['descriptions']['CENSUS']),
|
|
2055
|
-
vap: U.deepCopy(s.config['descriptions']['VAP']),
|
|
2056
|
-
election: U.deepCopy(s.config['descriptions']['ELECTION'])
|
|
2057
|
-
};
|
|
2058
|
-
const dsResources = {
|
|
2059
|
-
// None at this time
|
|
2060
|
-
};
|
|
2061
|
-
const ds = {
|
|
2062
|
-
table: dsTable,
|
|
2063
|
-
details: dsDetails,
|
|
2064
|
-
datasets: dsDatasets,
|
|
2065
|
-
resources: dsResources
|
|
2066
|
-
};
|
|
2067
|
-
return ds;
|
|
2068
|
-
}
|
|
2069
|
-
// Postprocess analytics - Normalize numeric results and derive secondary tests.
|
|
2070
|
-
// Do this after analytics have been run and before preparing a test log or scorecard.
|
|
2071
|
-
function doAnalyzePostProcessing(s, bLog = false) {
|
|
2072
|
-
// Just populate the normalized population deviation score in the test
|
|
2073
|
-
const scorecard = s._scorecard;
|
|
2074
|
-
let popDev = s.getTest(4 /* T.Test.PopulationDeviation */);
|
|
2075
|
-
popDev['normalizedScore'] = scorecard.populationDeviation.normalized;
|
|
2076
|
-
const datasets = {
|
|
2077
|
-
shapes: U.deepCopy(s.config['descriptions']['SHAPES']), // 2020
|
|
2078
|
-
census: U.deepCopy(s.config['descriptions']['CENSUS']),
|
|
2079
|
-
vap: U.deepCopy(s.config['descriptions']['VAP']),
|
|
2080
|
-
election: U.deepCopy(s.config['descriptions']['ELECTION'])
|
|
2081
|
-
};
|
|
2082
|
-
scorecard.partisan.details['election'] = datasets.election;
|
|
2083
|
-
scorecard.minority.details['vap'] = datasets.vap;
|
|
2084
|
-
scorecard.details['shapes'] = datasets.shapes;
|
|
2085
|
-
scorecard.details['census'] = datasets.census;
|
|
2086
|
-
const simpleSplits = s.getTest(5 /* T.Test.UnexpectedCountySplits */);
|
|
2087
|
-
scorecard.splitting.details['unexpectedAffected'] = simpleSplits['score'];
|
|
2088
|
-
scorecard.splitting.details['nSplits'] = simpleSplits['details']['nSplits'];
|
|
2089
|
-
scorecard.splitting.details['countiesSplitUnexpectedly'] = U.deepCopy(simpleSplits['details']['countiesSplitUnexpectedly']);
|
|
2090
|
-
// 02-23-21 - Added single-county district details
|
|
2091
|
-
scorecard.splitting.details['nTooBigCounties'] = simpleSplits['details']['expectedSplits'];
|
|
2092
|
-
scorecard.splitting.details['tooBigName'] = simpleSplits['details']['tooBigName'];
|
|
2093
|
-
scorecard.splitting.details['nCountiesSplit'] = simpleSplits['details']['nCountiesSplit'];
|
|
2094
|
-
scorecard.splitting.details['nSingleCountyDistricts'] = simpleSplits['details']['nSingleCountyDistricts'];
|
|
2095
|
-
scorecard.splitting.details['singleCountyDistrictMax'] = simpleSplits['details']['singleCountyDistrictMax'];
|
|
2096
|
-
// 07-17-21 - Enumerate # of times each split county is split
|
|
2097
|
-
scorecard.splitting.details['countiesWithSplits'] = simpleSplits['details']['countiesWithSplits'];
|
|
2098
|
-
// NOTE - Add split precincts in dra-client directly
|
|
2099
|
-
// Derive secondary tests
|
|
2100
|
-
// Note - The only secondary test is 'roughly equal population (true/false)
|
|
2101
|
-
(0, analyze_1.doDeriveSecondaryTests)(s, bLog);
|
|
2102
|
-
// Toggle the semaphore, so postprocessing isn't for both the testlog & scorecard
|
|
2103
|
-
s.bPostProcessingDone = true;
|
|
1540
|
+
function inferSelectedMinority(groups) {
|
|
1541
|
+
if (groups.hispanic)
|
|
1542
|
+
return 'Hispanic';
|
|
1543
|
+
if (groups.black)
|
|
1544
|
+
return 'Black';
|
|
1545
|
+
if (groups.pacific)
|
|
1546
|
+
return 'Pacific';
|
|
1547
|
+
if (groups.asian)
|
|
1548
|
+
return 'Asian';
|
|
1549
|
+
if (groups.native)
|
|
1550
|
+
return 'Native';
|
|
1551
|
+
if (groups.minority)
|
|
1552
|
+
return 'Minority';
|
|
1553
|
+
return 'No minority selected!';
|
|
2104
1554
|
}
|
|
1555
|
+
// END
|
|
2105
1556
|
|
|
2106
1557
|
|
|
2107
1558
|
/***/ }),
|
|
@@ -2150,57 +1601,52 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
2150
1601
|
};
|
|
2151
1602
|
})();
|
|
2152
1603
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2153
|
-
exports.
|
|
2154
|
-
exports.getStatewideDemographics = getStatewideDemographics;
|
|
1604
|
+
exports.makeProfile = makeProfile;
|
|
2155
1605
|
exports.computeMetrics = computeMetrics;
|
|
2156
1606
|
exports.rateKeyDimensions = rateKeyDimensions;
|
|
2157
1607
|
exports.thunkScorecard = thunkScorecard;
|
|
2158
|
-
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
2159
1608
|
const dra_analytics_1 = __webpack_require__(/*! @dra2020/dra-analytics */ "@dra2020/dra-analytics");
|
|
1609
|
+
const T = __importStar(__webpack_require__(/*! ./types */ "./src/types.ts"));
|
|
1610
|
+
const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
|
|
1611
|
+
const D = __importStar(__webpack_require__(/*! ./data */ "./src/data.ts"));
|
|
2160
1612
|
// PROFILE A PLAN
|
|
2161
1613
|
const KEEP_DECIMALS = 6;
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
const
|
|
2165
|
-
const
|
|
2166
|
-
const
|
|
2167
|
-
const
|
|
2168
|
-
const
|
|
2169
|
-
const
|
|
2170
|
-
// const
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2175
|
-
// 10-22-2020 - Converted Dem + Rep + Other = Total to two-party vote shares for analytics.
|
|
2176
|
-
const demVote = s.districts.table.demVotes[summaryRow];
|
|
2177
|
-
const repVote = s.districts.table.repVotes[summaryRow];
|
|
1614
|
+
function makeProfile(container) {
|
|
1615
|
+
const state = container.state.xx;
|
|
1616
|
+
const planName = container.title;
|
|
1617
|
+
const nDistricts = container.state.nDistricts;
|
|
1618
|
+
const nReps = container.state.nReps;
|
|
1619
|
+
const nCounties = container.counties.nCounties;
|
|
1620
|
+
const targetSize = container.state.targetSize;
|
|
1621
|
+
const popByDistrict = U.deepCopy(container.aggregates.columns.totalPop.slice(1, -1));
|
|
1622
|
+
// const geoPropsByDistrict: L.GeoProperties[] = makeArrayOfGeoProps(s, bLog); // No longer used
|
|
1623
|
+
const splits = _makeCxD(container, container._session.bLog);
|
|
1624
|
+
container.CxD = splits; // Squirrel CxD away for later "unexpected splits" analysis
|
|
1625
|
+
const demVote = container.aggregates.columns.DemVotes.at(-1);
|
|
1626
|
+
const repVote = container.aggregates.columns.RepVotes.at(-1);
|
|
2178
1627
|
const statewideVf = U.trim(demVote / (demVote + repVote), KEEP_DECIMALS);
|
|
2179
1628
|
let vpiArray = [];
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
const D = demVotes[districtID - 1];
|
|
2184
|
-
const R = repVotes[districtID - 1];
|
|
1629
|
+
for (let d = 1; d <= nDistricts; d++) {
|
|
1630
|
+
const D = container.aggregates.columns.DemVotes[d];
|
|
1631
|
+
const R = container.aggregates.columns.RepVotes[d];
|
|
2185
1632
|
const T = D + R;
|
|
2186
1633
|
const v = (T > 0) ? U.trim(D / T, KEEP_DECIMALS) : 0;
|
|
2187
1634
|
vpiArray.push(v);
|
|
2188
1635
|
}
|
|
2189
|
-
const statewideDemographics =
|
|
2190
|
-
const demographicsByDistrict =
|
|
2191
|
-
// MMD - Extended the profile for # reps per district
|
|
1636
|
+
const statewideDemographics = _makeStatewideDemographics(container, container._session.bLog);
|
|
1637
|
+
const demographicsByDistrict = _makeDemographicsByDistrict(container, container._session.bLog);
|
|
2192
1638
|
const profile = {
|
|
2193
1639
|
state: state,
|
|
2194
1640
|
name: planName,
|
|
2195
1641
|
nDistricts: nDistricts,
|
|
2196
|
-
repsByDistrict:
|
|
1642
|
+
repsByDistrict: container.repsByDistrict,
|
|
2197
1643
|
nCounties: nCounties,
|
|
2198
|
-
bStateLeg:
|
|
1644
|
+
bStateLeg: container.legislativeDistricts,
|
|
2199
1645
|
population: {
|
|
2200
1646
|
byDistrict: popByDistrict,
|
|
2201
1647
|
targetSize: targetSize
|
|
2202
1648
|
},
|
|
2203
|
-
shapes: geoPropsByDistrict
|
|
1649
|
+
shapes: [] /*geoPropsByDistrict*/,
|
|
2204
1650
|
counties: splits,
|
|
2205
1651
|
partisanship: {
|
|
2206
1652
|
statewide: statewideVf,
|
|
@@ -2213,64 +1659,71 @@ function profilePlan(s, bLog = false) {
|
|
|
2213
1659
|
};
|
|
2214
1660
|
return profile;
|
|
2215
1661
|
}
|
|
2216
|
-
|
|
2217
|
-
// unassigned precincts & state summary and an extra 0 county. But dra-score takes
|
|
2218
|
-
// a simple 1–D x 1–C splits array (zero-based, of course).
|
|
2219
|
-
function makeNakedCxD(s, bLog = false) {
|
|
2220
|
-
const adornedCxD = s.districts.table.countySplits;
|
|
2221
|
-
let CxD = [];
|
|
2222
|
-
// Remove the unassigned & total dummy "districts"
|
|
2223
|
-
for (let districtID = 1; districtID <= s.state.nDistricts; districtID++) {
|
|
2224
|
-
const splits = U.deepCopy(adornedCxD[districtID].slice(1));
|
|
2225
|
-
CxD.push(splits);
|
|
2226
|
-
}
|
|
2227
|
-
return CxD;
|
|
2228
|
-
}
|
|
2229
|
-
function makeArrayOfGeoProps(s, bLog = false) {
|
|
2230
|
-
let geometryByDistrict = [];
|
|
2231
|
-
for (let districtID = 1; districtID <= s.state.nDistricts; districtID++) {
|
|
2232
|
-
let districtProps = s.districts.getGeoProperties(districtID);
|
|
2233
|
-
// Guard against no shape and no properties
|
|
2234
|
-
if (districtProps) {
|
|
2235
|
-
let a = U.trim(districtProps[0 /* T.DistrictShapeProperty.Area */], KEEP_DECIMALS);
|
|
2236
|
-
let p = U.trim(districtProps[2 /* T.DistrictShapeProperty.Perimeter */], KEEP_DECIMALS);
|
|
2237
|
-
let d = U.trim(districtProps[1 /* T.DistrictShapeProperty.Diameter */], KEEP_DECIMALS);
|
|
2238
|
-
// Save each triple
|
|
2239
|
-
geometryByDistrict.push({ area: a, perimeter: p, diameter: d });
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
return geometryByDistrict;
|
|
2243
|
-
}
|
|
2244
|
-
function getDemographicsByDistrict(s, bLog = false) {
|
|
1662
|
+
function _makeDemographicsByDistrict(container, bLog = false) {
|
|
2245
1663
|
let demographicsArray = [];
|
|
2246
|
-
//
|
|
2247
|
-
for (let
|
|
1664
|
+
// Ignorethe unassigned & total dummy "districts"
|
|
1665
|
+
for (let d = 1; d <= container.state.nDistricts; d++) {
|
|
2248
1666
|
const districtDemographics = {
|
|
2249
|
-
white: U.trim(
|
|
2250
|
-
minority: U.trim(
|
|
2251
|
-
black: U.trim(
|
|
2252
|
-
hispanic: U.trim(
|
|
2253
|
-
pacific: U.trim(
|
|
2254
|
-
asian: U.trim(
|
|
2255
|
-
native: U.trim(
|
|
1667
|
+
white: U.trim(container.aggregates.columns.whitePct[d], KEEP_DECIMALS),
|
|
1668
|
+
minority: U.trim(container.aggregates.columns.minorityPct[d], KEEP_DECIMALS),
|
|
1669
|
+
black: U.trim(container.aggregates.columns.blackPct[d], KEEP_DECIMALS),
|
|
1670
|
+
hispanic: U.trim(container.aggregates.columns.hispanicPct[d], KEEP_DECIMALS),
|
|
1671
|
+
pacific: U.trim(container.aggregates.columns.pacificPct[d], KEEP_DECIMALS),
|
|
1672
|
+
asian: U.trim(container.aggregates.columns.asianPct[d], KEEP_DECIMALS),
|
|
1673
|
+
native: U.trim(container.aggregates.columns.nativePct[d], KEEP_DECIMALS)
|
|
2256
1674
|
};
|
|
2257
1675
|
demographicsArray.push(districtDemographics);
|
|
2258
1676
|
}
|
|
2259
1677
|
return demographicsArray;
|
|
2260
1678
|
}
|
|
2261
|
-
function
|
|
2262
|
-
const summaryRow =
|
|
1679
|
+
function _makeStatewideDemographics(container, bLog = false) {
|
|
1680
|
+
const summaryRow = container.state.nDistricts + 2 - 1; // Account for unassigned & total dummy "districts";
|
|
2263
1681
|
const demographics = {
|
|
2264
|
-
white: U.trim(
|
|
2265
|
-
minority: U.trim(
|
|
2266
|
-
black: U.trim(
|
|
2267
|
-
hispanic: U.trim(
|
|
2268
|
-
pacific: U.trim(
|
|
2269
|
-
asian: U.trim(
|
|
2270
|
-
native: U.trim(
|
|
1682
|
+
white: U.trim(container.aggregates.columns.whitePct[summaryRow], KEEP_DECIMALS),
|
|
1683
|
+
minority: U.trim(container.aggregates.columns.minorityPct[summaryRow], KEEP_DECIMALS),
|
|
1684
|
+
black: U.trim(container.aggregates.columns.blackPct[summaryRow], KEEP_DECIMALS),
|
|
1685
|
+
hispanic: U.trim(container.aggregates.columns.hispanicPct[summaryRow], KEEP_DECIMALS),
|
|
1686
|
+
pacific: U.trim(container.aggregates.columns.pacificPct[summaryRow], KEEP_DECIMALS),
|
|
1687
|
+
asian: U.trim(container.aggregates.columns.asianPct[summaryRow], KEEP_DECIMALS),
|
|
1688
|
+
native: U.trim(container.aggregates.columns.nativePct[summaryRow], KEEP_DECIMALS)
|
|
2271
1689
|
};
|
|
2272
1690
|
return demographics;
|
|
2273
1691
|
}
|
|
1692
|
+
function _makeCxD(container, bLog = false) {
|
|
1693
|
+
let CxD = [];
|
|
1694
|
+
let nCountyBuckets = container.counties.nCounties;
|
|
1695
|
+
for (let d = 1; d <= container.state.nDistricts; d++) {
|
|
1696
|
+
let countySplits = U.initArray(nCountyBuckets, 0);
|
|
1697
|
+
let geoIDs = container.plan.geoIDsForDistrictID(d);
|
|
1698
|
+
if (geoIDs && (geoIDs.size > 0)) {
|
|
1699
|
+
geoIDs.forEach(function (geoID) {
|
|
1700
|
+
let featureID = container.features.featureID(geoID);
|
|
1701
|
+
let f = container.features.featureByIndex(featureID);
|
|
1702
|
+
if (f == undefined) {
|
|
1703
|
+
if (bLog)
|
|
1704
|
+
console.log("Skipping undefined feature in making CxD matrix: GEOID =", geoID, "Feature ID =", featureID);
|
|
1705
|
+
}
|
|
1706
|
+
else {
|
|
1707
|
+
const dkCENSUS = container.features._keys[T.Dataset.CENSUS];
|
|
1708
|
+
const featurePop = D.fieldForFeature(f, dkCENSUS, 0 /* T.FeatureField.TotalPop */);
|
|
1709
|
+
// Ignore features when the county is unrecognized
|
|
1710
|
+
const countyFIPS = U.parseGeoID(geoID)['county'];
|
|
1711
|
+
if (U.keyExists(countyFIPS, container.counties.index)) {
|
|
1712
|
+
// Total population by counties w/in a district
|
|
1713
|
+
let i = container.counties.indexFromFIPS(countyFIPS) - 1; // Remove the virtual county bucket
|
|
1714
|
+
countySplits[i] += featurePop;
|
|
1715
|
+
}
|
|
1716
|
+
else {
|
|
1717
|
+
if (bLog)
|
|
1718
|
+
console.log("Statistics: County not recognized:", geoID);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
CxD.push(countySplits);
|
|
1724
|
+
}
|
|
1725
|
+
return CxD;
|
|
1726
|
+
}
|
|
2274
1727
|
// SCORE A PLAN using dra-analytics
|
|
2275
1728
|
function computeMetrics(p, districtShapes, bLog = false) {
|
|
2276
1729
|
if (bLog)
|
|
@@ -2357,13 +1810,12 @@ function thunkScorecard(newScorecard, bLog = false) {
|
|
|
2357
1810
|
const scratchpad = newScorecard.scratchpad;
|
|
2358
1811
|
// Partisan scorecard
|
|
2359
1812
|
const pS = U.deepCopy(newScorecard.partisan);
|
|
1813
|
+
// NOTE - 02/05/26 - Stopped return experimental metrics in the scorecard.
|
|
2360
1814
|
// EXPERIMENTAL
|
|
2361
|
-
const lPropAlt = newScorecard.partisan.experimental.lProp;
|
|
2362
|
-
if (lPropAlt)
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
if (lUE)
|
|
2366
|
-
pS.details['lUE'] = lUE;
|
|
1815
|
+
// const lPropAlt = newScorecard.partisan.experimental.lProp;
|
|
1816
|
+
// if (lPropAlt) pS.details['lProp'] = lPropAlt;
|
|
1817
|
+
// const lUE = newScorecard.partisan.experimental.lUE;
|
|
1818
|
+
// if (lUE) pS.details['lUE'] = lUE;
|
|
2367
1819
|
// Minority scorecard
|
|
2368
1820
|
const mS = U.deepCopy(newScorecard.minority);
|
|
2369
1821
|
// Compactness scorecard
|
|
@@ -2421,6 +1873,7 @@ function thunkScorecard(newScorecard, bLog = false) {
|
|
|
2421
1873
|
};
|
|
2422
1874
|
return scorecard;
|
|
2423
1875
|
}
|
|
1876
|
+
// END
|
|
2424
1877
|
|
|
2425
1878
|
|
|
2426
1879
|
/***/ }),
|
|
@@ -2436,14 +1889,12 @@ function thunkScorecard(newScorecard, bLog = false) {
|
|
|
2436
1889
|
// GLOBAL CONSTANTS
|
|
2437
1890
|
//
|
|
2438
1891
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2439
|
-
exports.OUT_OF_STATE = exports.
|
|
1892
|
+
exports.OUT_OF_STATE = exports.NOT_ASSIGNED = exports.PRECISION = void 0;
|
|
2440
1893
|
// Keep four decimal places for fractions [0–1], i.e.,
|
|
2441
1894
|
// keep two decimal places for %'s [0–100].
|
|
2442
1895
|
exports.PRECISION = 4;
|
|
2443
1896
|
// The dummy district ID for features not assigned districts yet
|
|
2444
1897
|
exports.NOT_ASSIGNED = 0;
|
|
2445
|
-
// # of items to report as problematic (e.g., features, districts, etc.)
|
|
2446
|
-
exports.NUMBER_OF_ITEMS_TO_REPORT = 10;
|
|
2447
1898
|
// The virtual geoID for "neighbors" in other states
|
|
2448
1899
|
exports.OUT_OF_STATE = "OUT_OF_STATE";
|
|
2449
1900
|
|
|
@@ -2461,7 +1912,48 @@ exports.OUT_OF_STATE = "OUT_OF_STATE";
|
|
|
2461
1912
|
// TYPE DEFINITIONS
|
|
2462
1913
|
//
|
|
2463
1914
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1915
|
+
exports.StatisticsFields = exports.multiFields = exports.multiFieldPairs = exports.fieldMap = exports.Dataset = void 0;
|
|
2464
1916
|
exports.fieldsFromFeatureField = fieldsFromFeatureField;
|
|
1917
|
+
var Dataset;
|
|
1918
|
+
(function (Dataset) {
|
|
1919
|
+
Dataset["SHAPES"] = "SHAPES";
|
|
1920
|
+
Dataset["CENSUS"] = "CENSUS";
|
|
1921
|
+
Dataset["VAP"] = "VAP";
|
|
1922
|
+
Dataset["ELECTION"] = "ELECTION";
|
|
1923
|
+
})(Dataset || (exports.Dataset = Dataset = {}));
|
|
1924
|
+
// MAPPING BETWEEN FIELDS OF VARIOUS TYPES
|
|
1925
|
+
// Map between packed field keys and buckets
|
|
1926
|
+
exports.fieldMap = {
|
|
1927
|
+
"CENSUS": {
|
|
1928
|
+
"Tot": "TotalPop"
|
|
1929
|
+
},
|
|
1930
|
+
"VAP": {
|
|
1931
|
+
"Tot": "TotalVAP",
|
|
1932
|
+
"Wh": "WhiteVAP",
|
|
1933
|
+
"Bl": "BlackVAP",
|
|
1934
|
+
"BlC": "BlackVAP",
|
|
1935
|
+
"His": "HispanicVAP",
|
|
1936
|
+
"Asn": "AsianVAP",
|
|
1937
|
+
"AsnC": "AsianVAP",
|
|
1938
|
+
"Pac": "PacificVAP",
|
|
1939
|
+
"PacC": "PacificVAP",
|
|
1940
|
+
"Nat": "NativeVAP",
|
|
1941
|
+
"NatC": "NativeVAP"
|
|
1942
|
+
},
|
|
1943
|
+
"ELECTION": {
|
|
1944
|
+
"Tot": "TotalVotes",
|
|
1945
|
+
"D": "DemVotes",
|
|
1946
|
+
"R": "RepVotes"
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
// Pairs of packed/source fields that can correspond to the same bucket
|
|
1950
|
+
exports.multiFieldPairs = [
|
|
1951
|
+
["Bl", "BlC"],
|
|
1952
|
+
["Asn", "AsnC"],
|
|
1953
|
+
["Pac", "PacC"],
|
|
1954
|
+
["Nat", "NatC"]
|
|
1955
|
+
];
|
|
1956
|
+
exports.multiFields = exports.multiFieldPairs.flat();
|
|
2465
1957
|
const FieldsByFeatureField = [
|
|
2466
1958
|
["Tot"],
|
|
2467
1959
|
["Wh"],
|
|
@@ -2477,6 +1969,29 @@ const FieldsByFeatureField = [
|
|
|
2477
1969
|
function fieldsFromFeatureField(ds, ff) {
|
|
2478
1970
|
return FieldsByFeatureField[ff];
|
|
2479
1971
|
}
|
|
1972
|
+
var StatisticsFields;
|
|
1973
|
+
(function (StatisticsFields) {
|
|
1974
|
+
StatisticsFields["districtID"] = "districtID";
|
|
1975
|
+
StatisticsFields["totalPop"] = "totalPop";
|
|
1976
|
+
StatisticsFields["popDevPct"] = "popDevPct";
|
|
1977
|
+
StatisticsFields["bEqualPop"] = "bEqualPop";
|
|
1978
|
+
StatisticsFields["bNotEmpty"] = "bNotEmpty";
|
|
1979
|
+
StatisticsFields["bContiguous"] = "bContiguous";
|
|
1980
|
+
StatisticsFields["bNotEmbedded"] = "bNotEmbedded";
|
|
1981
|
+
StatisticsFields["demPct"] = "demPct";
|
|
1982
|
+
StatisticsFields["repPct"] = "repPct";
|
|
1983
|
+
StatisticsFields["othPct"] = "othPct";
|
|
1984
|
+
StatisticsFields["totalVAP"] = "totalVAP";
|
|
1985
|
+
StatisticsFields["whitePct"] = "whitePct";
|
|
1986
|
+
StatisticsFields["minorityPct"] = "minorityPct";
|
|
1987
|
+
StatisticsFields["blackPct"] = "blackPct";
|
|
1988
|
+
StatisticsFields["hispanicPct"] = "hispanicPct";
|
|
1989
|
+
StatisticsFields["pacificPct"] = "pacificPct";
|
|
1990
|
+
StatisticsFields["asianPct"] = "asianPct";
|
|
1991
|
+
StatisticsFields["nativePct"] = "nativePct";
|
|
1992
|
+
})(StatisticsFields || (exports.StatisticsFields = StatisticsFields = {}));
|
|
1993
|
+
;
|
|
1994
|
+
// END
|
|
2480
1995
|
|
|
2481
1996
|
|
|
2482
1997
|
/***/ }),
|
|
@@ -2525,7 +2040,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
2525
2040
|
};
|
|
2526
2041
|
})();
|
|
2527
2042
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2528
|
-
exports.getDistrict = getDistrict;
|
|
2529
2043
|
exports.parseGeoID = parseGeoID;
|
|
2530
2044
|
exports.isWaterOnly = isWaterOnly;
|
|
2531
2045
|
exports.isUninhabited = isUninhabited;
|
|
@@ -2536,6 +2050,10 @@ exports.minArray = minArray;
|
|
|
2536
2050
|
exports.maxArray = maxArray;
|
|
2537
2051
|
exports.initArray = initArray;
|
|
2538
2052
|
exports.andArray = andArray;
|
|
2053
|
+
exports.addArrays = addArrays;
|
|
2054
|
+
exports.subtractArrays = subtractArrays;
|
|
2055
|
+
exports.divideArrays = divideArrays;
|
|
2056
|
+
exports.falseIndexes = falseIndexes;
|
|
2539
2057
|
exports.keyExists = keyExists;
|
|
2540
2058
|
exports.isObjectEmpty = isObjectEmpty;
|
|
2541
2059
|
exports.isSetEmpty = isSetEmpty;
|
|
@@ -2545,26 +2063,14 @@ exports.getNumericObjectKeys = getNumericObjectKeys;
|
|
|
2545
2063
|
exports.getSelectObjectKeys = getSelectObjectKeys;
|
|
2546
2064
|
exports.arrayContains = arrayContains;
|
|
2547
2065
|
exports.objectContains = objectContains;
|
|
2548
|
-
exports.countEnumValues = countEnumValues;
|
|
2549
2066
|
exports.shallowCopy = shallowCopy;
|
|
2550
2067
|
exports.deepCopy = deepCopy;
|
|
2551
2068
|
exports.depthof = depthof;
|
|
2069
|
+
exports.deepEqual = deepEqual;
|
|
2552
2070
|
const dra_types_1 = __webpack_require__(/*! @dra2020/dra-types */ "@dra2020/dra-types");
|
|
2553
|
-
const
|
|
2071
|
+
const D = __importStar(__webpack_require__(/*! ./data */ "./src/data.ts"));
|
|
2072
|
+
const T = __importStar(__webpack_require__(/*! ./types */ "./src/types.ts"));
|
|
2554
2073
|
const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
|
|
2555
|
-
// PLAN HELPERS
|
|
2556
|
-
// Get the districtID to which a geoID is assigned
|
|
2557
|
-
function getDistrict(plan, geoID) {
|
|
2558
|
-
// All geoIDs in a state *should be* assigned to a district (including the
|
|
2559
|
-
// dummy "unassigned" district), but "water-only" features are sometimes missing
|
|
2560
|
-
// from a map. This is also a guard against a bug in which a geoID has no district.
|
|
2561
|
-
if (keyExists(geoID, plan)) {
|
|
2562
|
-
return plan[geoID];
|
|
2563
|
-
}
|
|
2564
|
-
else {
|
|
2565
|
-
return undefined;
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
2074
|
// WORKING WITH GEOIDS
|
|
2569
2075
|
function parseGeoID(geoID) {
|
|
2570
2076
|
let bVfeature = false;
|
|
@@ -2582,15 +2088,14 @@ function parseGeoID(geoID) {
|
|
|
2582
2088
|
};
|
|
2583
2089
|
return parts;
|
|
2584
2090
|
}
|
|
2585
|
-
|
|
2586
|
-
function isWaterOnly(geoID, s) {
|
|
2091
|
+
function isWaterOnly(geoID, container) {
|
|
2587
2092
|
const waterOnlySignature = 'ZZZZZZ';
|
|
2588
2093
|
if (geoID.indexOf(waterOnlySignature) >= 0)
|
|
2589
2094
|
return true;
|
|
2590
|
-
if (
|
|
2095
|
+
if (container) {
|
|
2591
2096
|
// If called with a session, get the feature ...
|
|
2592
|
-
const featureID =
|
|
2593
|
-
const f /* T.GeoFeature */ =
|
|
2097
|
+
const featureID = container.features.featureID(geoID);
|
|
2098
|
+
const f /* T.GeoFeature */ = container.features.featureByIndex(featureID);
|
|
2594
2099
|
// ... and also check the 'ALAND' property
|
|
2595
2100
|
const bNoLand = ((f.properties['ALAND10'] !== undefined) && (f.properties['ALAND10'] == 0)) ? true : false;
|
|
2596
2101
|
return bNoLand;
|
|
@@ -2598,12 +2103,12 @@ function isWaterOnly(geoID, s) {
|
|
|
2598
2103
|
else
|
|
2599
2104
|
return false;
|
|
2600
2105
|
}
|
|
2601
|
-
function isUninhabited(geoID,
|
|
2602
|
-
const featureID =
|
|
2603
|
-
const f /* T.GeoFeature */ =
|
|
2106
|
+
function isUninhabited(geoID, container) {
|
|
2107
|
+
const featureID = container.features.featureID(geoID);
|
|
2108
|
+
const f /* T.GeoFeature */ = container.features.featureByIndex(featureID);
|
|
2604
2109
|
// 03-27-21
|
|
2605
|
-
const dk =
|
|
2606
|
-
const totalPop =
|
|
2110
|
+
const dk = container.features._keys[T.Dataset.CENSUS];
|
|
2111
|
+
const totalPop = D.fieldForFeature(f, dk, 0 /* T.FeatureField.TotalPop */);
|
|
2607
2112
|
// const totalPop = s.features.fieldForFeature(f, T.Dataset.CENSUS, T.FeatureField.TotalPop);
|
|
2608
2113
|
let bUninhabited = (totalPop > 0) ? false : true;
|
|
2609
2114
|
// HACK for Kentucky's atypical data, so the official map shows as complete
|
|
@@ -2644,20 +2149,44 @@ function initArray(n, value) {
|
|
|
2644
2149
|
function andArray(arr) {
|
|
2645
2150
|
return arr.reduce(function (a, b) { return a && b; }, true);
|
|
2646
2151
|
}
|
|
2152
|
+
function addArrays(a, b) {
|
|
2153
|
+
if (a.length !== b.length) {
|
|
2154
|
+
throw new Error(`Array lengths must match: ${a.length} !== ${b.length}`);
|
|
2155
|
+
}
|
|
2156
|
+
return a.map((val, i) => val + b[i]);
|
|
2157
|
+
}
|
|
2158
|
+
function subtractArrays(a, b) {
|
|
2159
|
+
if (a.length !== b.length) {
|
|
2160
|
+
throw new Error(`Array lengths must match: ${a.length} !== ${b.length}`);
|
|
2161
|
+
}
|
|
2162
|
+
return a.map((val, i) => val - b[i]);
|
|
2163
|
+
}
|
|
2164
|
+
function divideArrays(numerator, denominator) {
|
|
2165
|
+
if (numerator.length !== denominator.length) {
|
|
2166
|
+
throw new Error(`Array lengths must match: ${numerator.length} !== ${denominator.length}`);
|
|
2167
|
+
}
|
|
2168
|
+
return numerator.map((val, i) => denominator[i] <= 0 ? 0.0 : val / denominator[i]);
|
|
2169
|
+
}
|
|
2170
|
+
// NOTE - This helper excludes the first and last items in the array,
|
|
2171
|
+
// which are dummy unassigned and summary values, respectively.
|
|
2172
|
+
function falseIndexes(booleans) {
|
|
2173
|
+
return booleans.reduce((indexes, value, index) => {
|
|
2174
|
+
if (index > 0 && index < booleans.length - 1 && value === false) {
|
|
2175
|
+
indexes.push(index);
|
|
2176
|
+
}
|
|
2177
|
+
return indexes;
|
|
2178
|
+
}, []);
|
|
2179
|
+
}
|
|
2647
2180
|
// WORKING WITH OBJECT KEYS/PROPERTIES
|
|
2648
|
-
// Does an object have a key/property?
|
|
2649
2181
|
function keyExists(k, o) {
|
|
2650
2182
|
return k in o;
|
|
2651
2183
|
}
|
|
2652
|
-
// Does an object (dict) have any keys/properties?
|
|
2653
2184
|
function isObjectEmpty(o) {
|
|
2654
2185
|
return Object.keys(o).length === 0;
|
|
2655
2186
|
}
|
|
2656
|
-
// Does a Set have any members?
|
|
2657
2187
|
function isSetEmpty(s) {
|
|
2658
2188
|
return s.size === 0;
|
|
2659
2189
|
}
|
|
2660
|
-
// Does an array hold any items?
|
|
2661
2190
|
function isArrayEmpty(a) {
|
|
2662
2191
|
if (a === undefined || a.length == 0) {
|
|
2663
2192
|
// array empty or does not exist
|
|
@@ -2667,7 +2196,6 @@ function isArrayEmpty(a) {
|
|
|
2667
2196
|
return false;
|
|
2668
2197
|
}
|
|
2669
2198
|
}
|
|
2670
|
-
// Get the keys for an object
|
|
2671
2199
|
function getObjectKeys(o) {
|
|
2672
2200
|
return Object.keys(o);
|
|
2673
2201
|
}
|
|
@@ -2689,16 +2217,6 @@ function arrayContains(a, item) {
|
|
|
2689
2217
|
function objectContains(o, key) {
|
|
2690
2218
|
return (key in o);
|
|
2691
2219
|
}
|
|
2692
|
-
// ENUM HELPERS
|
|
2693
|
-
// Source: https://stackoverflow.com/questions/38034673/determine-the-number-of-enum-elements-typescript
|
|
2694
|
-
function countEnumValues(enumName) {
|
|
2695
|
-
let count = 0;
|
|
2696
|
-
for (let item in enumName) {
|
|
2697
|
-
if (isNaN(Number(item)))
|
|
2698
|
-
count++;
|
|
2699
|
-
}
|
|
2700
|
-
return count;
|
|
2701
|
-
}
|
|
2702
2220
|
// COPYING - Copied from dra-client/util.ts
|
|
2703
2221
|
function shallowCopy(src) {
|
|
2704
2222
|
if (Array.isArray(src))
|
|
@@ -2756,233 +2274,104 @@ function depthof(a) {
|
|
|
2756
2274
|
}
|
|
2757
2275
|
}
|
|
2758
2276
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
//
|
|
2773
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
2774
|
-
if (k2 === undefined) k2 = k;
|
|
2775
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
2776
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
2777
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
2277
|
+
// EQUALITY HELPERS
|
|
2278
|
+
function approxEqual(x, y, places = 2) {
|
|
2279
|
+
const tolerance = Math.pow(10, -places) * 0.5;
|
|
2280
|
+
return Math.abs(x - y) <= tolerance;
|
|
2281
|
+
}
|
|
2282
|
+
function deepEqual(obj1, obj2, path = '') {
|
|
2283
|
+
// Same reference
|
|
2284
|
+
if (obj1 === obj2)
|
|
2285
|
+
return true;
|
|
2286
|
+
// Type checking
|
|
2287
|
+
if (typeof obj1 !== typeof obj2) {
|
|
2288
|
+
console.log(`${path || 'root'}: type mismatch - ${typeof obj1} vs ${typeof obj2}`);
|
|
2289
|
+
return false;
|
|
2778
2290
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
});
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
// Are any of the unassigned features not water-only -or- inhabited?
|
|
2833
|
-
// Check the official congressional maps for CT, KY, IL, and MI.
|
|
2834
|
-
bAllNonWaterOnlyAssigned = !unassignedFeatures.some(function (geoID) {
|
|
2835
|
-
if (U.isWaterOnly(geoID, s)) {
|
|
2836
|
-
if (bLog)
|
|
2837
|
-
console.log("Unassigned water-only feature ignored in completeness check: ", geoID);
|
|
2838
|
-
return false;
|
|
2839
|
-
}
|
|
2840
|
-
if (U.isUninhabited(geoID, s)) {
|
|
2841
|
-
if (bLog)
|
|
2842
|
-
console.log("Uninhabited feature ignored in completeness check: ", geoID);
|
|
2843
|
-
return false;
|
|
2291
|
+
// Handle null
|
|
2292
|
+
if (obj1 === null || obj2 === null) {
|
|
2293
|
+
console.log(`${path || 'root'}: ${obj1} vs ${obj2}`);
|
|
2294
|
+
return false;
|
|
2295
|
+
}
|
|
2296
|
+
// Atomic type comparisons
|
|
2297
|
+
if (typeof obj1 === 'string') {
|
|
2298
|
+
// Strings must be exactly equal
|
|
2299
|
+
if (obj1 !== obj2) {
|
|
2300
|
+
console.log(`${path || 'root'}: "${obj1}" vs "${obj2}"`);
|
|
2301
|
+
return false;
|
|
2302
|
+
}
|
|
2303
|
+
return true;
|
|
2304
|
+
}
|
|
2305
|
+
if (typeof obj1 === 'number') {
|
|
2306
|
+
// Numbers use approximate equality (2 decimal places)
|
|
2307
|
+
if (!approxEqual(obj1, obj2, 2)) {
|
|
2308
|
+
console.log(`${path || 'root'}: ${obj1} vs ${obj2} (difference: ${Math.abs(obj1 - obj2)})`);
|
|
2309
|
+
return false;
|
|
2310
|
+
}
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
2313
|
+
if (typeof obj1 === 'boolean') {
|
|
2314
|
+
// Booleans must be exactly equal
|
|
2315
|
+
if (obj1 !== obj2) {
|
|
2316
|
+
console.log(`${path || 'root'}: ${obj1} vs ${obj2}`);
|
|
2317
|
+
return false;
|
|
2318
|
+
}
|
|
2319
|
+
return true;
|
|
2320
|
+
}
|
|
2321
|
+
// If not an object type, fall back to strict equality
|
|
2322
|
+
if (typeof obj1 !== 'object') {
|
|
2323
|
+
if (obj1 !== obj2) {
|
|
2324
|
+
console.log(`${path || 'root'}: ${obj1} vs ${obj2}`);
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
return true;
|
|
2328
|
+
}
|
|
2329
|
+
// Array handling
|
|
2330
|
+
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
|
|
2331
|
+
console.log(`${path || 'root'}: array vs non-array`);
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
2334
|
+
if (Array.isArray(obj1)) {
|
|
2335
|
+
if (obj1.length !== obj2.length) {
|
|
2336
|
+
console.log(`${path || 'root'}: array length ${obj1.length} vs ${obj2.length}`);
|
|
2337
|
+
return false;
|
|
2338
|
+
}
|
|
2339
|
+
let isEqual = true;
|
|
2340
|
+
obj1.forEach((val, idx) => {
|
|
2341
|
+
const itemPath = `${path}[${idx}]`;
|
|
2342
|
+
if (!deepEqual(val, obj2[idx], itemPath)) {
|
|
2343
|
+
isEqual = false;
|
|
2844
2344
|
}
|
|
2845
|
-
// Not water-only and inhabited
|
|
2846
|
-
return true;
|
|
2847
2345
|
});
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2346
|
+
return isEqual;
|
|
2347
|
+
}
|
|
2348
|
+
// Object handling
|
|
2349
|
+
const keys1 = Object.keys(obj1);
|
|
2350
|
+
const keys2 = Object.keys(obj2);
|
|
2351
|
+
// Check for missing keys
|
|
2352
|
+
const allKeys = new Set([...keys1, ...keys2]);
|
|
2353
|
+
let isEqual = true;
|
|
2354
|
+
for (const key of allKeys) {
|
|
2355
|
+
const keyPath = path ? `${path}.${key}` : key;
|
|
2356
|
+
if (!(key in obj1)) {
|
|
2357
|
+
console.log(`${keyPath}: missing in first object`);
|
|
2358
|
+
isEqual = false;
|
|
2858
2359
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
// The # of enumerated districts minus the dummy NOT_ASSIGNED one should
|
|
2863
|
-
// equal the number apportioned districts. This guards against a district
|
|
2864
|
-
// not being included in a map that is imported.
|
|
2865
|
-
//
|
|
2866
|
-
// NOTE - I'm no longer checking for this, but DRA should!
|
|
2867
|
-
// Case 2 - Or a district is explicitly named but empty:
|
|
2868
|
-
// Note, this can happen if a district is created, and then all features
|
|
2869
|
-
// are removed from it (in DRA).
|
|
2870
|
-
// Populate the test entry
|
|
2871
|
-
// 08-13-2020 - Revised completeness check:
|
|
2872
|
-
// A plan is complete if:
|
|
2873
|
-
// * All inhabited, not water-only districts are assigned to districts.
|
|
2874
|
-
// * No districts are empty, i.e., each has one or more geos assigned to them.
|
|
2875
|
-
// Note: We're not checking (in _data.ts) whether those geos are water-only
|
|
2876
|
-
// or have any population, but that's a real edge case.
|
|
2877
|
-
test['score'] = bAllNonWaterOnlyAssigned && bNoneEmpty;
|
|
2878
|
-
// test['score'] = bAllAssigned && bNoneEmpty;
|
|
2879
|
-
if (!bAllAssigned) {
|
|
2880
|
-
test['details']['unassignedFeatures'] = unassignedFeatures;
|
|
2881
|
-
}
|
|
2882
|
-
if (!bNoneEmpty) {
|
|
2883
|
-
test['details']['emptyDistricts'] = emptyDistricts;
|
|
2884
|
-
}
|
|
2885
|
-
// Populate the N+1 summary "district" in district.statistics
|
|
2886
|
-
let bNotEmpty = s.districts.table.bNotEmpty;
|
|
2887
|
-
let summaryRow = s.districts.numberOfRows() - 1;
|
|
2888
|
-
bNotEmpty[summaryRow] = test['score'];
|
|
2889
|
-
return test;
|
|
2890
|
-
}
|
|
2891
|
-
//
|
|
2892
|
-
// CONTIGUOUS - Is each district in a plan fully connected?
|
|
2893
|
-
//
|
|
2894
|
-
// NOTE - To check "operational contiguity," we need to use a graph, i.e.,
|
|
2895
|
-
// we can't rely on just the geometric contiguity of shapes in a shapefile.
|
|
2896
|
-
//
|
|
2897
|
-
// To test this, load the NC 2010 map 'SAMPLE-BG-map-discontiguous.csv'.
|
|
2898
|
-
//
|
|
2899
|
-
function doIsContiguous(s, bLog = false) {
|
|
2900
|
-
let test = s.getTest(1 /* T.Test.Contiguous */);
|
|
2901
|
-
// Get the contiguity of each district. Ignore dummy unassigned district
|
|
2902
|
-
// and the N+1 summary district.
|
|
2903
|
-
let bContiguousByDistrict = s.districts.table.bContiguous;
|
|
2904
|
-
bContiguousByDistrict = bContiguousByDistrict.slice(1, -1);
|
|
2905
|
-
// If any real districts aren't contiguous, mark the plan as not contiguous
|
|
2906
|
-
let bMapContiguous = U.andArray(bContiguousByDistrict);
|
|
2907
|
-
// If the map is not contiguous, log the offending districts.
|
|
2908
|
-
let discontiguousDistricts = [];
|
|
2909
|
-
let districtID = 1;
|
|
2910
|
-
bContiguousByDistrict.forEach(function (bDistrictContiguous) {
|
|
2911
|
-
if (!bDistrictContiguous)
|
|
2912
|
-
discontiguousDistricts.push(districtID);
|
|
2913
|
-
districtID += 1;
|
|
2914
|
-
});
|
|
2915
|
-
// Populate the test entry
|
|
2916
|
-
test['score'] = bMapContiguous;
|
|
2917
|
-
if (!bMapContiguous) {
|
|
2918
|
-
test['details'] = { 'discontiguousDistricts': discontiguousDistricts };
|
|
2919
|
-
}
|
|
2920
|
-
// Populate the N+1 summary "district" in district.statistics
|
|
2921
|
-
let bContiguous = s.districts.table.bContiguous;
|
|
2922
|
-
let summaryRow = s.districts.numberOfRows() - 1;
|
|
2923
|
-
bContiguous[summaryRow] = test['score'];
|
|
2924
|
-
return test;
|
|
2925
|
-
}
|
|
2926
|
-
//
|
|
2927
|
-
// FREE OF HOLES - Are any districts fully embedded w/in another district?
|
|
2928
|
-
//
|
|
2929
|
-
// A district is NOT a "donut hole" district:
|
|
2930
|
-
// - If any neighbor is 'OUT_OF_STATE'; or
|
|
2931
|
-
// - If there are 2 or more neighboring districts.
|
|
2932
|
-
//
|
|
2933
|
-
// To test this, load the NC 2010 map 'SAMPLE-BG-map-hole.csv'. District 1,
|
|
2934
|
-
// Buncombe County (37021), is a donut hole w/in District 3.
|
|
2935
|
-
//
|
|
2936
|
-
function doIsFreeOfHoles(s, bLog = false) {
|
|
2937
|
-
let test = s.getTest(2 /* T.Test.FreeOfHoles */);
|
|
2938
|
-
// Initialize values
|
|
2939
|
-
let bFreeOfHoles = true;
|
|
2940
|
-
let embeddedDistricts = [];
|
|
2941
|
-
// Get the embeddedness of each district. Ignore dummy unassigned district
|
|
2942
|
-
// and the N+1 summary district.
|
|
2943
|
-
let bNotEmbeddedByDistrict = s.districts.table.bNotEmbedded;
|
|
2944
|
-
bNotEmbeddedByDistrict = bNotEmbeddedByDistrict.slice(1, -1);
|
|
2945
|
-
let districtID = 1;
|
|
2946
|
-
bNotEmbeddedByDistrict.forEach(function (bDistrictNotEmbedded) {
|
|
2947
|
-
if (!bDistrictNotEmbedded) {
|
|
2948
|
-
embeddedDistricts.push(districtID);
|
|
2949
|
-
bFreeOfHoles = false;
|
|
2360
|
+
else if (!(key in obj2)) {
|
|
2361
|
+
console.log(`${keyPath}: missing in second object`);
|
|
2362
|
+
isEqual = false;
|
|
2950
2363
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
// Populate the N+1 summary "district" in district.statistics
|
|
2959
|
-
let bNotEmbedded = s.districts.table.bNotEmbedded;
|
|
2960
|
-
let summaryRow = s.districts.numberOfRows() - 1;
|
|
2961
|
-
bNotEmbedded[summaryRow] = test['score'];
|
|
2962
|
-
return test;
|
|
2364
|
+
else {
|
|
2365
|
+
if (!deepEqual(obj1[key], obj2[key], keyPath)) {
|
|
2366
|
+
isEqual = false;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
return isEqual;
|
|
2963
2371
|
}
|
|
2372
|
+
// END
|
|
2964
2373
|
|
|
2965
2374
|
|
|
2966
|
-
/***/ }),
|
|
2967
|
-
|
|
2968
|
-
/***/ "./static/majority-minority.json":
|
|
2969
|
-
/*!***************************************!*\
|
|
2970
|
-
!*** ./static/majority-minority.json ***!
|
|
2971
|
-
\***************************************/
|
|
2972
|
-
/***/ ((module) => {
|
|
2973
|
-
|
|
2974
|
-
module.exports = /*#__PURE__*/JSON.parse('{"AL":{"Black":[7],"Hispanic":[],"Pacific":[]},"AK":{"Black":[],"Hispanic":[],"Pacific":[]},"AZ":{"Black":[],"Hispanic":[2,4,7],"Pacific":[]},"AR":{"Black":[],"Hispanic":[],"Pacific":[]},"CA":{"Black":[13,37,43],"Hispanic":[17,18,20,21,28,31,32,34,35,38,39,43,45,47,51],"Pacific":[12,13,14,15,17,18,19,27,34,39,45,47,52]},"CO":{"Black":[],"Hispanic":[],"Pacific":[]},"CT":{"Black":[],"Hispanic":[],"Pacific":[]},"DC":{"Black":[],"Hispanic":[],"Pacific":[]},"DE":{"Black":[],"Hispanic":[],"Pacific":[]},"FL":{"Black":[5,20,24],"Hispanic":[18,21,25],"Pacific":[]},"GA":{"Black":[2,4,5,13],"Hispanic":[],"Pacific":[]},"HI":{"Black":[],"Hispanic":[],"Pacific":[2]},"ID":{"Black":[],"Hispanic":[1],"Pacific":[]},"IL":{"Black":[1,2,7],"Hispanic":[4],"Pacific":[]},"IN":{"Black":[7],"Hispanic":[],"Pacific":[]},"IA":{"Black":[],"Hispanic":[],"Pacific":[]},"KS":{"Black":[],"Hispanic":[],"Pacific":[]},"KY":{"Black":[],"Hispanic":[],"Pacific":[]},"LA":{"Black":[2],"Hispanic":[],"Pacific":[]},"ME":{"Black":[],"Hispanic":[],"Pacific":[]},"MD":{"Black":[4,7],"Hispanic":[],"Pacific":[]},"MA":{"Black":[],"Hispanic":[],"Pacific":[]},"MI":{"Black":[13,14],"Hispanic":[],"Pacific":[]},"MN":{"Black":[],"Hispanic":[],"Pacific":[]},"MS":{"Black":[2],"Hispanic":[],"Pacific":[]},"MO":{"Black":[1,5],"Hispanic":[],"Pacific":[]},"MT":{"Black":[],"Hispanic":[],"Pacific":[]},"NE":{"Black":[],"Hispanic":[],"Pacific":[]},"NV":{"Black":[],"Hispanic":[],"Pacific":[]},"NH":{"Black":[],"Hispanic":[],"Pacific":[]},"NJ":{"Black":[10,12],"Hispanic":[13],"Pacific":[6]},"NM":{"Black":[],"Hispanic":[2,13],"Pacific":[]},"NY":{"Black":[5,8,9,13],"Hispanic":[12,16],"Pacific":[6,7,10]},"NC":{"Black":[1,12],"Hispanic":[],"Pacific":[]},"ND":{"Black":[],"Hispanic":[],"Pacific":[]},"OH":{"Black":[3,11],"Hispanic":[],"Pacific":[]},"OK":{"Black":[],"Hispanic":[],"Pacific":[]},"OR":{"Black":[],"Hispanic":[],"Pacific":[]},"PA":{"Black":[2],"Hispanic":[],"Pacific":[]},"PR":{"Black":[],"Hispanic":[],"Pacific":[]},"RI":{"Black":[],"Hispanic":[],"Pacific":[]},"SC":{"Black":[6],"Hispanic":[],"Pacific":[]},"SD":{"Black":[],"Hispanic":[],"Pacific":[]},"TN":{"Black":[9],"Hispanic":[],"Pacific":[]},"TX":{"Black":[9,18,33],"Hispanic":[15,16,19,20,23,27,28,29,32],"Pacific":[]},"UT":{"Black":[],"Hispanic":[],"Pacific":[]},"VT":{"Black":[],"Hispanic":[],"Pacific":[]},"VA":{"Black":[3],"Hispanic":[],"Pacific":[]},"WA":{"Black":[],"Hispanic":[3],"Pacific":[9]},"WV":{"Black":[],"Hispanic":[],"Pacific":[]},"WI":{"Black":[4],"Hispanic":[],"Pacific":[]},"WY":{"Black":[],"Hispanic":[],"Pacific":[]}}');
|
|
2975
|
-
|
|
2976
|
-
/***/ }),
|
|
2977
|
-
|
|
2978
|
-
/***/ "./static/vra5-preclearance.json":
|
|
2979
|
-
/*!***************************************!*\
|
|
2980
|
-
!*** ./static/vra5-preclearance.json ***!
|
|
2981
|
-
\***************************************/
|
|
2982
|
-
/***/ ((module) => {
|
|
2983
|
-
|
|
2984
|
-
module.exports = /*#__PURE__*/JSON.parse('{"AL":"all","AK":"all","AZ":"all","CA":"parts","FL":"parts","GA":"all","LA":"all","MI":"parts","MS":"all","NY":"parts","NC":"parts","SC":"all","SD":"parts","TX":"all","VA":"all"}');
|
|
2985
|
-
|
|
2986
2375
|
/***/ }),
|
|
2987
2376
|
|
|
2988
2377
|
/***/ "@dra2020/baseclient":
|