@dra2020/district-analytics 15.6.1 → 16.0.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.
@@ -54,6 +54,7 @@ const minority_1 = __webpack_require__(/*! ./minority */ "./src/minority.ts");
54
54
  const D = __importStar(__webpack_require__(/*! ./_data */ "./src/_data.ts"));
55
55
  const M = __importStar(__webpack_require__(/*! ./minority */ "./src/minority.ts"));
56
56
  const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
57
+ // MMD - Extended for optional # reps per district
57
58
  class AnalyticsSession {
58
59
  constructor(SessionRequest) {
59
60
  this.legislativeDistricts = false; // 2020
@@ -79,6 +80,17 @@ class AnalyticsSession {
79
80
  this.features = new D.Features(this, SessionRequest['data'], this.config['datasets']);
80
81
  this.plan = new D.Plan(this, SessionRequest['plan']);
81
82
  this.districts = new D.Districts(this, SessionRequest['districtShapes']);
83
+ // MMD - Validate & handle optional # reps per district
84
+ const repsByDistrictt = SessionRequest['repsByDistrictt'];
85
+ const nDistricts = this.state.nDistricts;
86
+ if (repsByDistrictt !== undefined) {
87
+ if (repsByDistrictt.length != nDistricts)
88
+ throw new Error("Mismatched #'s of districts passed to AnalyticsSession constructor!");
89
+ if (repsByDistrictt.includes(0))
90
+ throw new Error("Zero reps for a district passed to AnalyticsSession constructor!");
91
+ // Assume a positive integer # of reps per district
92
+ this.repsByDistrictt = SessionRequest['repsByDistrictt'];
93
+ }
82
94
  }
83
95
  processConfig(config) {
84
96
  // Default the Census & redistricting cycle to 2010
@@ -135,7 +147,7 @@ class AnalyticsSession {
135
147
  let popDevPct = this.districts.table.popDevPct;
136
148
  let totalVAP = this.districts.table.totalVAP;
137
149
  const summaryRow = this.districts.numberOfRows() - 1;
138
- totalPop[summaryRow] = this._profile.population.targetSize;
150
+ totalPop[summaryRow] = this._profile.population.targetSize; // MMD - This is generalized when the profile is created.
139
151
  popDevPct[summaryRow] = popDev;
140
152
  totalVAP[summaryRow] = Math.round(totalVAP[summaryRow] / this._profile.nDistricts);
141
153
  // Added w/ new scorecard
@@ -333,8 +345,8 @@ class Districts {
333
345
  return null;
334
346
  }
335
347
  setGeoProperties(i, p) { this._geoProperties[i] = p; }
336
- // +1 for dummy unassigned 0 "district" and +1 for N+1 summary "district" for
337
- // state-level values. Real districts are 1–N.
348
+ // +1 for dummy unassigned 0 "district" and +1 for N+1 summary "district" for state-level values.
349
+ // Real districts are 1–N.
338
350
  numberOfRows() { return this._session.state.nDistricts + 2; }
339
351
  numberOfWorkingDistricts() { return this._session.state.nDistricts + 1; }
340
352
  initTable() {
@@ -372,17 +384,24 @@ class Districts {
372
384
  };
373
385
  return t;
374
386
  }
387
+ // MMD - Generalized district statistics for MMD's
375
388
  // This is the workhorse computational routine!
376
389
  recalcStatistics(bLog = false) {
377
390
  // Initialize debug counters
378
391
  nMissingDataset = 0;
379
392
  nMissingProperty = 0;
393
+ // MMD - Generalized targetSize
380
394
  // Compute these once per recalc cycle
381
- let targetSize = Math.round(this._session.state.totalPop / this._session.state.nDistricts);
382
- let deviationThreshold = this._session.populationDeviationThreshold();
395
+ const nDistricts = this._session.state.nDistricts;
396
+ const repsByDistrictt = this._session.repsByDistrictt;
397
+ const nReps = this._session.state.nReps;
398
+ const stateTotal = this._session.state.totalPop;
399
+ const targetSize = this._session.state.targetSize;
400
+ // const targetSize = Math.round(this._session.state.totalPop / this._session.state.nDistricts);
401
+ const deviationThreshold = this._session.populationDeviationThreshold();
383
402
  // let planByDistrict = this._session.plan.byDistrictID();
384
- let plan = this._session.plan;
385
- let graph = this._session.graph;
403
+ const plan = this._session.plan;
404
+ const graph = this._session.graph;
386
405
  // Add an extra 0th virtual county bucket for county-district splitting analysis
387
406
  let nCountyBuckets = this._session.counties.nCounties + 1;
388
407
  // INITIALIZE STATE VALUES THAT WILL BE ACCUMULATED
@@ -440,82 +459,85 @@ class Districts {
440
459
  bNotEmpty = true;
441
460
  // ... loop over the geoIDs creating district-by-district statistics
442
461
  geoIDs.forEach(function (geoID) {
443
- // 01-04-22 -- Removed water-only guard
444
- // // Skip water-only features
445
- // if (!(U.isWaterOnly(geoID)))
446
- // {
447
- // Map from geoID to feature index
448
- let featureID = outerThis._session.features.featureID(geoID);
449
- let f = outerThis._session.features.featureByIndex(featureID);
450
- if (f == undefined) {
451
- if (bLog)
452
- console.log("Statistics: Skipping undefined feature in district statistics: GEOID =", geoID, "Feature ID =", featureID);
453
- }
454
- else {
455
- // ACCUMULATE VALUES
456
- // Total population of each feature
457
- // NOTE - This result is used more than once
458
- // 03-27-21
459
- const dkCENSUS = outerThis._session.features._keys["CENSUS" /* CENSUS */];
460
- featurePop = fieldForFeature(f, dkCENSUS, 0 /* TotalPop */);
461
- // featurePop = outerThis._session.features.fieldForFeature(f, T.Dataset.CENSUS, T.FeatureField.TotalPop);
462
- // Total district population
463
- totalPop += featurePop;
464
- // Ignore features when the county is unrecognized
465
- const countyFIPS = U.parseGeoID(geoID)['county'];
466
- if (U.keyExists(countyFIPS, outerThis._session.counties.index)) {
467
- // Total population by counties w/in a district,
468
- // except the dummy unassigned district 0
469
- if (i > 0)
470
- countySplits[outerThis.getCountyIndex(geoID)] += featurePop;
471
- }
472
- else {
462
+ // Skip water-only features
463
+ if (!(U.isWaterOnly(geoID))) {
464
+ // Map from geoID to feature index
465
+ let featureID = outerThis._session.features.featureID(geoID);
466
+ let f = outerThis._session.features.featureByIndex(featureID);
467
+ if (f == undefined) {
473
468
  if (bLog)
474
- console.log("Statistics: County not recognized:", geoID);
469
+ console.log("Statistics: Skipping undefined feature in district statistics: GEOID =", geoID, "Feature ID =", featureID);
475
470
  }
476
- // Democratic and Republican vote totals
477
- // 10-22-2020 - Added Other votes
478
- // 10-24-2020 - Added guard against inconsistent election data
479
- // 03-27-2021
480
- const dkELECTION = outerThis._session.features._keys["ELECTION" /* ELECTION */];
481
- const featureDem = fieldForFeature(f, dkELECTION, 7 /* DemVotes */);
482
- // const featureDem = outerThis._session.features.fieldForFeature(f, T.Dataset.ELECTION, T.FeatureField.DemVotes);
483
- const featureRep = fieldForFeature(f, dkELECTION, 8 /* RepVotes */);
484
- const featureTot = fieldForFeature(f, dkELECTION, 9 /* TotalVotes */);
485
- demVotes += featureDem;
486
- repVotes += featureRep;
487
- totVotes += featureTot;
488
- // NOTE: Unless you grab the values above before accumulating them,
489
- // you can't accumulate othVotes for districts. You must calculate
490
- // them by implication later.
491
- if (bLog) {
492
- const bBadElection = (featureDem + featureRep > featureTot) ? true : false;
493
- if (bBadElection)
494
- console.log("Statistics: Inconsistent election data for precinct:", geoID, featureDem, featureRep, featureTot);
471
+ else {
472
+ // ACCUMULATE VALUES
473
+ // Total population of each feature
474
+ // NOTE - This result is used more than once
475
+ // 03-27-21
476
+ const dkCENSUS = outerThis._session.features._keys["CENSUS" /* CENSUS */];
477
+ featurePop = fieldForFeature(f, dkCENSUS, 0 /* TotalPop */);
478
+ // featurePop = outerThis._session.features.fieldForFeature(f, T.Dataset.CENSUS, T.FeatureField.TotalPop);
479
+ // Total district population
480
+ totalPop += featurePop;
481
+ // Ignore features when the county is unrecognized
482
+ const countyFIPS = U.parseGeoID(geoID)['county'];
483
+ if (U.keyExists(countyFIPS, outerThis._session.counties.index)) {
484
+ // Total population by counties w/in a district,
485
+ // except the dummy unassigned district 0
486
+ if (i > 0)
487
+ countySplits[outerThis.getCountyIndex(geoID)] += featurePop;
488
+ }
489
+ else {
490
+ if (bLog)
491
+ console.log("Statistics: County not recognized:", geoID);
492
+ }
493
+ // Democratic and Republican vote totals
494
+ // 10-22-2020 - Added Other votes
495
+ // 10-24-2020 - Added guard against inconsistent election data
496
+ // 03-27-2021
497
+ const dkELECTION = outerThis._session.features._keys["ELECTION" /* ELECTION */];
498
+ const featureDem = fieldForFeature(f, dkELECTION, 7 /* DemVotes */);
499
+ // const featureDem = outerThis._session.features.fieldForFeature(f, T.Dataset.ELECTION, T.FeatureField.DemVotes);
500
+ const featureRep = fieldForFeature(f, dkELECTION, 8 /* RepVotes */);
501
+ const featureTot = fieldForFeature(f, dkELECTION, 9 /* TotalVotes */);
502
+ demVotes += featureDem;
503
+ repVotes += featureRep;
504
+ totVotes += featureTot;
505
+ // NOTE: Unless you grab the values above before accumulating them,
506
+ // you can't accumulate othVotes for districts. You must calculate
507
+ // them by implication later.
508
+ if (bLog) {
509
+ const bBadElection = (featureDem + featureRep > featureTot) ? true : false;
510
+ if (bBadElection)
511
+ console.log("Statistics: Inconsistent election data for precinct:", geoID, featureDem, featureRep, featureTot);
512
+ }
513
+ // Voting-age demographic breakdowns (or citizen voting-age)
514
+ // 03-27-21
515
+ const dkVAP = outerThis._session.features._keys["VAP" /* VAP */];
516
+ totalVAP += fieldForFeature(f, dkVAP, 0 /* TotalPop */);
517
+ // totalVAP += outerThis._session.features.fieldForFeature(f, T.Dataset.VAP, T.FeatureField.TotalPop);
518
+ whitePop += fieldForFeature(f, dkVAP, 1 /* WhitePop */);
519
+ blackPop += fieldForFeature(f, dkVAP, 2 /* BlackPop */);
520
+ hispanicPop += fieldForFeature(f, dkVAP, 3 /* HispanicPop */);
521
+ pacificPop += fieldForFeature(f, dkVAP, 5 /* PacificPop */);
522
+ asianPop += fieldForFeature(f, dkVAP, 4 /* AsianPop */);
523
+ nativePop += fieldForFeature(f, dkVAP, 6 /* NativePop */);
495
524
  }
496
- // Voting-age demographic breakdowns (or citizen voting-age)
497
- // 03-27-21
498
- const dkVAP = outerThis._session.features._keys["VAP" /* VAP */];
499
- totalVAP += fieldForFeature(f, dkVAP, 0 /* TotalPop */);
500
- // totalVAP += outerThis._session.features.fieldForFeature(f, T.Dataset.VAP, T.FeatureField.TotalPop);
501
- whitePop += fieldForFeature(f, dkVAP, 1 /* WhitePop */);
502
- blackPop += fieldForFeature(f, dkVAP, 2 /* BlackPop */);
503
- hispanicPop += fieldForFeature(f, dkVAP, 3 /* HispanicPop */);
504
- pacificPop += fieldForFeature(f, dkVAP, 5 /* PacificPop */);
505
- asianPop += fieldForFeature(f, dkVAP, 4 /* AsianPop */);
506
- nativePop += fieldForFeature(f, dkVAP, 6 /* NativePop */);
507
525
  }
508
- // }
509
- // else
510
- // {
511
- // if (bLog) console.log("Statistics: Skipping water-only feature in district statistics:", geoID);
512
- // }
526
+ else {
527
+ if (bLog)
528
+ console.log("Statistics: Skipping water-only feature in district statistics:", geoID);
529
+ }
513
530
  });
514
531
  // COMPUTE DERIVED VALUES
532
+ // MMD - Generalized the per-district population deviations's for MMD's with variable #'s of reps per district.
533
+ // - The real districts are indexed 1–N.
534
+ // - But the # reps per district are indexed from zero.
515
535
  // Population deviation % and equal population (boolean) by district.
516
536
  if (i > 0) {
517
537
  if (totalPop > 0) {
518
- popDevPct = (totalPop - targetSize) / targetSize;
538
+ const n = (repsByDistrictt) ? repsByDistrictt[i - 1] : 1; // MMD - # of reps for the district
539
+ popDevPct = (totalPop - (n * targetSize)) / (n * targetSize); // MMD
540
+ // popDevPct = (totalPop - targetSize) / targetSize;
519
541
  bEqualPop = (Math.abs(popDevPct) <= deviationThreshold);
520
542
  }
521
543
  }
@@ -1000,75 +1022,47 @@ exports.fieldForFeature = fieldForFeature;
1000
1022
  // f is a direct GeoJSON feature
1001
1023
  // p is a geoID
1002
1024
  function _getFeatures(f, datasetKey, p) {
1003
- if (!f.properties || !f.properties.datasets)
1004
- return 0;
1005
- if (datasetKey && !f.properties.datasets[datasetKey])
1006
- return 0;
1007
- let n = datasetKey ? f.properties.datasets[datasetKey][p] : f.properties[p];
1008
- return !n || isNaN(n) ? 0 : n;
1009
- }
1010
- /* 01-04-22 -- Replaced with the above
1011
- function _getFeatures(f: any, datasetKey: string, p: string): any
1012
- {
1013
- // Shim to load sample data2.json from disk for command-line scaffolding
1014
- if (f.properties && f.properties['datasets'])
1015
- {
1016
- if (!f.properties['datasets'][datasetKey])
1017
- {
1018
- // Feature is missing the dataset
1019
- nMissingDataset += 1;
1020
- // console.log(`${nMissingDataset}: Data ${datasetKey} missing for feature ${f} Returning zero.`);
1021
-
1022
- return 0;
1023
- }
1024
-
1025
- return f.properties['datasets'][datasetKey][p];
1026
- }
1027
-
1028
- // NOTE - The fGetW() code from dra-client below here ...
1029
-
1030
- // Direct property?
1031
- if (f.properties && f.properties[p] !== undefined)
1032
- {
1033
- return f.properties[p];
1034
- }
1035
-
1036
- // Joined property?
1037
- let a: any[] = _fGetJoined(f);
1038
- if (a)
1039
- {
1040
- for (let i: number = 0; i < a.length; i++)
1041
- {
1042
- let o: any = a[i];
1043
- if (!datasetKey)
1044
- {
1045
- if (o[p] !== undefined)
1046
- {
1047
- return o[p];
1025
+ // Shim to load sample data2.json from disk for command-line scaffolding
1026
+ if (f.properties && f.properties['datasets']) {
1027
+ if (!f.properties['datasets'][datasetKey]) {
1028
+ // Feature is missing the dataset
1029
+ nMissingDataset += 1;
1030
+ // console.log(`${nMissingDataset}: Data ${datasetKey} missing for feature ${f} Returning zero.`);
1031
+ return 0;
1048
1032
  }
1049
- }
1050
- else
1051
- {
1052
- if (o['datasets'] && o['datasets'][datasetKey])
1053
- {
1054
- let v = (o['datasets'][datasetKey][p]);
1055
- if ((!(v == null)) && (!(v == undefined)))
1056
- {
1057
- return o['datasets'][datasetKey][p];
1058
- }
1033
+ return f.properties['datasets'][datasetKey][p];
1034
+ }
1035
+ // NOTE - The fGetW() code from dra-client below here ...
1036
+ // Direct property?
1037
+ if (f.properties && f.properties[p] !== undefined) {
1038
+ return f.properties[p];
1039
+ }
1040
+ // Joined property?
1041
+ let a = _fGetJoined(f);
1042
+ if (a) {
1043
+ for (let i = 0; i < a.length; i++) {
1044
+ let o = a[i];
1045
+ if (!datasetKey) {
1046
+ if (o[p] !== undefined) {
1047
+ return o[p];
1048
+ }
1049
+ }
1050
+ else {
1051
+ if (o['datasets'] && o['datasets'][datasetKey]) {
1052
+ let v = (o['datasets'][datasetKey][p]);
1053
+ if ((!(v == null)) && (!(v == undefined))) {
1054
+ return o['datasets'][datasetKey][p];
1055
+ }
1056
+ }
1057
+ }
1059
1058
  }
1060
- }
1061
1059
  }
1062
- }
1063
-
1064
- // Feature is missing the property
1065
- nMissingProperty += 1;
1066
- // console.log(`${nMissingProperty}: ${p} value undefined for ${f.properties['GEOID10']}. Returning zero.`);
1067
-
1068
- return 0;
1069
- // return undefined;
1060
+ // Feature is missing the property
1061
+ nMissingProperty += 1;
1062
+ // console.log(`${nMissingProperty}: ${p} value undefined for ${f.properties['GEOID10']}. Returning zero.`);
1063
+ return 0;
1064
+ // return undefined;
1070
1065
  }
1071
- */
1072
1066
  function _fGetJoined(f) {
1073
1067
  return (f.properties && f.properties.joined) ? f.properties.joined : undefined;
1074
1068
  }
@@ -1095,9 +1089,11 @@ class Counties {
1095
1089
  }
1096
1090
  exports.Counties = Counties;
1097
1091
  // CLASSES TO ORGANIZE AND/OR ABSTRACT OTHER DATA
1092
+ // MMD - Extended this for # of reps
1098
1093
  class State {
1099
1094
  constructor(s, xx, n) {
1100
1095
  this.totalPop = 0;
1096
+ this.targetSize = 0; // HACK - Will get set by doPreprocessCensus()
1101
1097
  this.tooBigFIPS = [];
1102
1098
  this.tooBigName = [];
1103
1099
  this.singleCountyDistrictMax = 0;
@@ -1106,6 +1102,7 @@ class State {
1106
1102
  this._session = s;
1107
1103
  this.xx = xx;
1108
1104
  this.nDistricts = n;
1105
+ this.nReps = (s.repsByDistrictt) ? s.repsByDistrictt.reduce((a, b) => a + b, 0) : this.nDistricts; // MMD
1109
1106
  }
1110
1107
  }
1111
1108
  exports.State = State;
@@ -1586,6 +1583,7 @@ function isAShape(poly) {
1586
1583
  //
1587
1584
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1588
1585
  exports.doHasEqualPopulations = void 0;
1586
+ // MMD - This generalizes for variable #'s of reps per district.
1589
1587
  // NOTE - This validity check is *derived* and depends on population deviation %
1590
1588
  // being computed (above) and normalized in test log & scorecard generation.
1591
1589
  function doHasEqualPopulations(s, bLog = false) {
@@ -1593,7 +1591,7 @@ function doHasEqualPopulations(s, bLog = false) {
1593
1591
  // Get the normalized population deviation %
1594
1592
  let popDevTest = s.getTest(4 /* PopulationDeviation */);
1595
1593
  const popDevPct = popDevTest['score'];
1596
- const popDevNormalized = popDevTest['normalizedScore'];
1594
+ // const popDevNormalized: number = popDevTest['normalizedScore'] as number;
1597
1595
  test['details']['deviation'] = popDevPct;
1598
1596
  test['details']['thresholds'] = popDevTest['details']['scale'];
1599
1597
  // Populate the N+1 summary "district" in district.statistics
@@ -1850,9 +1848,13 @@ function doPreprocessCensus(s, bLog = false) {
1850
1848
  countyTotals[i] = totalByCounty[fipsCode];
1851
1849
  }
1852
1850
  s.counties.totalPopulation = countyTotals;
1851
+ // MMD - Rationalized calculation of target size *per rep*
1852
+ s.state.targetSize = s.state.totalPop / s.state.nReps;
1853
1853
  // ANALYZE THE COUNTIES
1854
+ // MMD - This "these counties must be split" analysis does NOT generalize to heterogenous MMD.
1854
1855
  // 'target_size': 733499, # calc as total / districts
1855
- let targetSize = Math.round(s.state.totalPop / s.state.nDistricts);
1856
+ const targetSize = s.state.targetSize;
1857
+ // let targetSize = Math.round(s.state.totalPop / s.state.nDistricts);
1856
1858
  // Find counties that are bigger than the target district size.
1857
1859
  // 'too_big' = The counties that are bigger than a district, e.g., ['Mecklenburg', 'Wake']
1858
1860
  // 'too_big_fips' = Their FIPS codes, e.g., ['119', '183']
@@ -2130,12 +2132,15 @@ const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
2130
2132
  const dra_analytics_1 = __webpack_require__(/*! @dra2020/dra-analytics */ "@dra2020/dra-analytics");
2131
2133
  // PROFILE A PLAN
2132
2134
  const KEEP_DECIMALS = 6;
2135
+ // MMD - Added # reps per district to the profile
2133
2136
  function profilePlan(s, bLog = false) {
2134
2137
  const state = s.state.xx;
2135
2138
  const planName = s.title;
2136
2139
  const nDistricts = s.state.nDistricts;
2140
+ const nReps = s.state.nReps; // MMD
2137
2141
  const nCounties = s.counties.nCounties;
2138
- const targetSize = Math.round(s.state.totalPop / nDistricts);
2142
+ const targetSize = s.state.targetSize; // MMD
2143
+ // const targetSize: number = Math.round(s.state.totalPop / nDistricts);
2139
2144
  const popByDistrict = U.deepCopy(s.districts.table.totalPop.slice(1, -1));
2140
2145
  const geoPropsByDistrict = makeArrayOfGeoProps(s, bLog);
2141
2146
  const splits = makeNakedCxD(s);
@@ -2156,10 +2161,12 @@ function profilePlan(s, bLog = false) {
2156
2161
  }
2157
2162
  const statewideDemographics = getStatewideDemographics(s);
2158
2163
  const demographicsByDistrict = getDemographicsByDistrict(s);
2164
+ // MMD - Extended the profile for # reps per district
2159
2165
  const profile = {
2160
2166
  state: state,
2161
2167
  name: planName,
2162
2168
  nDistricts: nDistricts,
2169
+ repsByDistrict: s.repsByDistrictt,
2163
2170
  nCounties: nCounties,
2164
2171
  bStateLeg: s.legislativeDistricts,
2165
2172
  population: {
@@ -2257,10 +2264,12 @@ function computeMetrics(p, districtShapes, bLog = false) {
2257
2264
  // Calculate county-district splitting metrics ...
2258
2265
  const CxD = p.counties;
2259
2266
  const _sS = dra_analytics_1.Splitting.makeSplittingScorecard(CxD, bLog);
2267
+ // MMD - Add # reps per district to call
2260
2268
  // Calculate population deviation-related metrics ...
2261
2269
  const totPopByDistrict = p.population.byDistrict;
2262
2270
  const targetSize = p.population.targetSize;
2263
- const _pdS = dra_analytics_1.Equal.makePopulationScorecard(totPopByDistrict, targetSize, bLegislative, bLog);
2271
+ const repsByDistrict = p.repsByDistrict;
2272
+ const _pdS = dra_analytics_1.Equal.makePopulationScorecard(totPopByDistrict, targetSize, bLegislative, repsByDistrict, bLog);
2264
2273
  const details = {};
2265
2274
  // Assemble the pieces into new scorecard
2266
2275
  const scorecard = {