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