@dra2020/district-analytics 15.6.0 → 16.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,80 +459,87 @@ class Districts {
440
459
  bNotEmpty = true;
441
460
  // ... loop over the geoIDs creating district-by-district statistics
442
461
  geoIDs.forEach(function (geoID) {
443
- // Skip water-only features
444
- if (!(U.isWaterOnly(geoID))) {
445
- // Map from geoID to feature index
446
- let featureID = outerThis._session.features.featureID(geoID);
447
- let f = outerThis._session.features.featureByIndex(featureID);
448
- if (f == undefined) {
449
- if (bLog)
450
- console.log("Statistics: Skipping undefined feature in district statistics: GEOID =", geoID, "Feature ID =", featureID);
462
+ // 01-04-22 -- Removed water-only guard
463
+ // // Skip water-only features
464
+ // if (!(U.isWaterOnly(geoID)))
465
+ // {
466
+ // Map from geoID to feature index
467
+ let featureID = outerThis._session.features.featureID(geoID);
468
+ let f = outerThis._session.features.featureByIndex(featureID);
469
+ if (f == undefined) {
470
+ if (bLog)
471
+ console.log("Statistics: Skipping undefined feature in district statistics: GEOID =", geoID, "Feature ID =", featureID);
472
+ }
473
+ else {
474
+ // ACCUMULATE VALUES
475
+ // Total population of each feature
476
+ // NOTE - This result is used more than once
477
+ // 03-27-21
478
+ const dkCENSUS = outerThis._session.features._keys["CENSUS" /* CENSUS */];
479
+ featurePop = fieldForFeature(f, dkCENSUS, 0 /* TotalPop */);
480
+ // featurePop = outerThis._session.features.fieldForFeature(f, T.Dataset.CENSUS, T.FeatureField.TotalPop);
481
+ // Total district population
482
+ totalPop += featurePop;
483
+ // Ignore features when the county is unrecognized
484
+ const countyFIPS = U.parseGeoID(geoID)['county'];
485
+ if (U.keyExists(countyFIPS, outerThis._session.counties.index)) {
486
+ // Total population by counties w/in a district,
487
+ // except the dummy unassigned district 0
488
+ if (i > 0)
489
+ countySplits[outerThis.getCountyIndex(geoID)] += featurePop;
451
490
  }
452
491
  else {
453
- // ACCUMULATE VALUES
454
- // Total population of each feature
455
- // NOTE - This result is used more than once
456
- // 03-27-21
457
- const dkCENSUS = outerThis._session.features._keys["CENSUS" /* CENSUS */];
458
- featurePop = fieldForFeature(f, dkCENSUS, 0 /* TotalPop */);
459
- // featurePop = outerThis._session.features.fieldForFeature(f, T.Dataset.CENSUS, T.FeatureField.TotalPop);
460
- // Total district population
461
- totalPop += featurePop;
462
- // Ignore features when the county is unrecognized
463
- const countyFIPS = U.parseGeoID(geoID)['county'];
464
- if (U.keyExists(countyFIPS, outerThis._session.counties.index)) {
465
- // Total population by counties w/in a district,
466
- // except the dummy unassigned district 0
467
- if (i > 0)
468
- countySplits[outerThis.getCountyIndex(geoID)] += featurePop;
469
- }
470
- else {
471
- if (bLog)
472
- console.log("Statistics: County not recognized:", geoID);
473
- }
474
- // Democratic and Republican vote totals
475
- // 10-22-2020 - Added Other votes
476
- // 10-24-2020 - Added guard against inconsistent election data
477
- // 03-27-2021
478
- const dkELECTION = outerThis._session.features._keys["ELECTION" /* ELECTION */];
479
- const featureDem = fieldForFeature(f, dkELECTION, 7 /* DemVotes */);
480
- // const featureDem = outerThis._session.features.fieldForFeature(f, T.Dataset.ELECTION, T.FeatureField.DemVotes);
481
- const featureRep = fieldForFeature(f, dkELECTION, 8 /* RepVotes */);
482
- const featureTot = fieldForFeature(f, dkELECTION, 9 /* TotalVotes */);
483
- demVotes += featureDem;
484
- repVotes += featureRep;
485
- totVotes += featureTot;
486
- // NOTE: Unless you grab the values above before accumulating them,
487
- // you can't accumulate othVotes for districts. You must calculate
488
- // them by implication later.
489
- if (bLog) {
490
- const bBadElection = (featureDem + featureRep > featureTot) ? true : false;
491
- if (bBadElection)
492
- console.log("Statistics: Inconsistent election data for precinct:", geoID, featureDem, featureRep, featureTot);
493
- }
494
- // Voting-age demographic breakdowns (or citizen voting-age)
495
- // 03-27-21
496
- const dkVAP = outerThis._session.features._keys["VAP" /* VAP */];
497
- totalVAP += fieldForFeature(f, dkVAP, 0 /* TotalPop */);
498
- // totalVAP += outerThis._session.features.fieldForFeature(f, T.Dataset.VAP, T.FeatureField.TotalPop);
499
- whitePop += fieldForFeature(f, dkVAP, 1 /* WhitePop */);
500
- blackPop += fieldForFeature(f, dkVAP, 2 /* BlackPop */);
501
- hispanicPop += fieldForFeature(f, dkVAP, 3 /* HispanicPop */);
502
- pacificPop += fieldForFeature(f, dkVAP, 5 /* PacificPop */);
503
- asianPop += fieldForFeature(f, dkVAP, 4 /* AsianPop */);
504
- nativePop += fieldForFeature(f, dkVAP, 6 /* NativePop */);
492
+ if (bLog)
493
+ console.log("Statistics: County not recognized:", geoID);
505
494
  }
495
+ // Democratic and Republican vote totals
496
+ // 10-22-2020 - Added Other votes
497
+ // 10-24-2020 - Added guard against inconsistent election data
498
+ // 03-27-2021
499
+ const dkELECTION = outerThis._session.features._keys["ELECTION" /* ELECTION */];
500
+ const featureDem = fieldForFeature(f, dkELECTION, 7 /* DemVotes */);
501
+ // const featureDem = outerThis._session.features.fieldForFeature(f, T.Dataset.ELECTION, T.FeatureField.DemVotes);
502
+ const featureRep = fieldForFeature(f, dkELECTION, 8 /* RepVotes */);
503
+ const featureTot = fieldForFeature(f, dkELECTION, 9 /* TotalVotes */);
504
+ demVotes += featureDem;
505
+ repVotes += featureRep;
506
+ totVotes += featureTot;
507
+ // NOTE: Unless you grab the values above before accumulating them,
508
+ // you can't accumulate othVotes for districts. You must calculate
509
+ // them by implication later.
510
+ if (bLog) {
511
+ const bBadElection = (featureDem + featureRep > featureTot) ? true : false;
512
+ if (bBadElection)
513
+ console.log("Statistics: Inconsistent election data for precinct:", geoID, featureDem, featureRep, featureTot);
514
+ }
515
+ // Voting-age demographic breakdowns (or citizen voting-age)
516
+ // 03-27-21
517
+ const dkVAP = outerThis._session.features._keys["VAP" /* VAP */];
518
+ totalVAP += fieldForFeature(f, dkVAP, 0 /* TotalPop */);
519
+ // totalVAP += outerThis._session.features.fieldForFeature(f, T.Dataset.VAP, T.FeatureField.TotalPop);
520
+ whitePop += fieldForFeature(f, dkVAP, 1 /* WhitePop */);
521
+ blackPop += fieldForFeature(f, dkVAP, 2 /* BlackPop */);
522
+ hispanicPop += fieldForFeature(f, dkVAP, 3 /* HispanicPop */);
523
+ pacificPop += fieldForFeature(f, dkVAP, 5 /* PacificPop */);
524
+ asianPop += fieldForFeature(f, dkVAP, 4 /* AsianPop */);
525
+ nativePop += fieldForFeature(f, dkVAP, 6 /* NativePop */);
506
526
  }
507
- else {
508
- if (bLog)
509
- console.log("Statistics: Skipping water-only feature in district statistics:", geoID);
510
- }
527
+ // }
528
+ // else
529
+ // {
530
+ // if (bLog) console.log("Statistics: Skipping water-only feature in district statistics:", geoID);
531
+ // }
511
532
  });
512
533
  // COMPUTE DERIVED VALUES
534
+ // MMD - Generalized the per-district population deviations's for MMD's with variable #'s of reps per district.
535
+ // - The real districts are indexed 1–N.
536
+ // - But the # reps per district are indexed from zero.
513
537
  // Population deviation % and equal population (boolean) by district.
514
538
  if (i > 0) {
515
539
  if (totalPop > 0) {
516
- popDevPct = (totalPop - targetSize) / targetSize;
540
+ const n = (repsByDistrictt) ? repsByDistrictt[i - 1] : 1; // MMD - # of reps for the district
541
+ popDevPct = (totalPop - (n * targetSize)) / (n * targetSize); // MMD
542
+ // popDevPct = (totalPop - targetSize) / targetSize;
517
543
  bEqualPop = (Math.abs(popDevPct) <= deviationThreshold);
518
544
  }
519
545
  }
@@ -998,47 +1024,75 @@ exports.fieldForFeature = fieldForFeature;
998
1024
  // f is a direct GeoJSON feature
999
1025
  // p is a geoID
1000
1026
  function _getFeatures(f, datasetKey, p) {
1001
- // Shim to load sample data2.json from disk for command-line scaffolding
1002
- if (f.properties && f.properties['datasets']) {
1003
- if (!f.properties['datasets'][datasetKey]) {
1004
- // Feature is missing the dataset
1005
- nMissingDataset += 1;
1006
- // console.log(`${nMissingDataset}: Data ${datasetKey} missing for feature ${f} Returning zero.`);
1007
- return 0;
1027
+ if (!f.properties || !f.properties.datasets)
1028
+ return 0;
1029
+ if (datasetKey && !f.properties.datasets[datasetKey])
1030
+ return 0;
1031
+ let n = datasetKey ? f.properties.datasets[datasetKey][p] : f.properties[p];
1032
+ return !n || isNaN(n) ? 0 : n;
1033
+ }
1034
+ /* 01-04-22 -- Replaced with the above
1035
+ function _getFeatures(f: any, datasetKey: string, p: string): any
1036
+ {
1037
+ // Shim to load sample data2.json from disk for command-line scaffolding
1038
+ if (f.properties && f.properties['datasets'])
1039
+ {
1040
+ if (!f.properties['datasets'][datasetKey])
1041
+ {
1042
+ // Feature is missing the dataset
1043
+ nMissingDataset += 1;
1044
+ // console.log(`${nMissingDataset}: Data ${datasetKey} missing for feature ${f} Returning zero.`);
1045
+
1046
+ return 0;
1047
+ }
1048
+
1049
+ return f.properties['datasets'][datasetKey][p];
1050
+ }
1051
+
1052
+ // NOTE - The fGetW() code from dra-client below here ...
1053
+
1054
+ // Direct property?
1055
+ if (f.properties && f.properties[p] !== undefined)
1056
+ {
1057
+ return f.properties[p];
1058
+ }
1059
+
1060
+ // Joined property?
1061
+ let a: any[] = _fGetJoined(f);
1062
+ if (a)
1063
+ {
1064
+ for (let i: number = 0; i < a.length; i++)
1065
+ {
1066
+ let o: any = a[i];
1067
+ if (!datasetKey)
1068
+ {
1069
+ if (o[p] !== undefined)
1070
+ {
1071
+ return o[p];
1008
1072
  }
1009
- return f.properties['datasets'][datasetKey][p];
1010
- }
1011
- // NOTE - The fGetW() code from dra-client below here ...
1012
- // Direct property?
1013
- if (f.properties && f.properties[p] !== undefined) {
1014
- return f.properties[p];
1015
- }
1016
- // Joined property?
1017
- let a = _fGetJoined(f);
1018
- if (a) {
1019
- for (let i = 0; i < a.length; i++) {
1020
- let o = a[i];
1021
- if (!datasetKey) {
1022
- if (o[p] !== undefined) {
1023
- return o[p];
1024
- }
1025
- }
1026
- else {
1027
- if (o['datasets'] && o['datasets'][datasetKey]) {
1028
- let v = (o['datasets'][datasetKey][p]);
1029
- if ((!(v == null)) && (!(v == undefined))) {
1030
- return o['datasets'][datasetKey][p];
1031
- }
1032
- }
1033
- }
1073
+ }
1074
+ else
1075
+ {
1076
+ if (o['datasets'] && o['datasets'][datasetKey])
1077
+ {
1078
+ let v = (o['datasets'][datasetKey][p]);
1079
+ if ((!(v == null)) && (!(v == undefined)))
1080
+ {
1081
+ return o['datasets'][datasetKey][p];
1082
+ }
1034
1083
  }
1084
+ }
1035
1085
  }
1036
- // Feature is missing the property
1037
- nMissingProperty += 1;
1038
- // console.log(`${nMissingProperty}: ${p} value undefined for ${f.properties['GEOID10']}. Returning zero.`);
1039
- return 0;
1040
- // return undefined;
1086
+ }
1087
+
1088
+ // Feature is missing the property
1089
+ nMissingProperty += 1;
1090
+ // console.log(`${nMissingProperty}: ${p} value undefined for ${f.properties['GEOID10']}. Returning zero.`);
1091
+
1092
+ return 0;
1093
+ // return undefined;
1041
1094
  }
1095
+ */
1042
1096
  function _fGetJoined(f) {
1043
1097
  return (f.properties && f.properties.joined) ? f.properties.joined : undefined;
1044
1098
  }
@@ -1065,9 +1119,11 @@ class Counties {
1065
1119
  }
1066
1120
  exports.Counties = Counties;
1067
1121
  // CLASSES TO ORGANIZE AND/OR ABSTRACT OTHER DATA
1122
+ // MMD - Extended this for # of reps
1068
1123
  class State {
1069
1124
  constructor(s, xx, n) {
1070
1125
  this.totalPop = 0;
1126
+ this.targetSize = 0; // HACK - Will get set by doPreprocessCensus()
1071
1127
  this.tooBigFIPS = [];
1072
1128
  this.tooBigName = [];
1073
1129
  this.singleCountyDistrictMax = 0;
@@ -1076,6 +1132,7 @@ class State {
1076
1132
  this._session = s;
1077
1133
  this.xx = xx;
1078
1134
  this.nDistricts = n;
1135
+ this.nReps = (s.repsByDistrictt) ? s.repsByDistrictt.reduce((a, b) => a + b, 0) : this.nDistricts; // MMD
1079
1136
  }
1080
1137
  }
1081
1138
  exports.State = State;
@@ -1556,6 +1613,7 @@ function isAShape(poly) {
1556
1613
  //
1557
1614
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1558
1615
  exports.doHasEqualPopulations = void 0;
1616
+ // MMD - This generalizes for variable #'s of reps per district.
1559
1617
  // NOTE - This validity check is *derived* and depends on population deviation %
1560
1618
  // being computed (above) and normalized in test log & scorecard generation.
1561
1619
  function doHasEqualPopulations(s, bLog = false) {
@@ -1563,7 +1621,7 @@ function doHasEqualPopulations(s, bLog = false) {
1563
1621
  // Get the normalized population deviation %
1564
1622
  let popDevTest = s.getTest(4 /* PopulationDeviation */);
1565
1623
  const popDevPct = popDevTest['score'];
1566
- const popDevNormalized = popDevTest['normalizedScore'];
1624
+ // const popDevNormalized: number = popDevTest['normalizedScore'] as number;
1567
1625
  test['details']['deviation'] = popDevPct;
1568
1626
  test['details']['thresholds'] = popDevTest['details']['scale'];
1569
1627
  // Populate the N+1 summary "district" in district.statistics
@@ -1820,9 +1878,13 @@ function doPreprocessCensus(s, bLog = false) {
1820
1878
  countyTotals[i] = totalByCounty[fipsCode];
1821
1879
  }
1822
1880
  s.counties.totalPopulation = countyTotals;
1881
+ // MMD - Rationalized calculation of target size *per rep*
1882
+ s.state.targetSize = s.state.totalPop / s.state.nReps;
1823
1883
  // ANALYZE THE COUNTIES
1884
+ // MMD - This "these counties must be split" analysis does NOT generalize to heterogenous MMD.
1824
1885
  // 'target_size': 733499, # calc as total / districts
1825
- let targetSize = Math.round(s.state.totalPop / s.state.nDistricts);
1886
+ const targetSize = s.state.targetSize;
1887
+ // let targetSize = Math.round(s.state.totalPop / s.state.nDistricts);
1826
1888
  // Find counties that are bigger than the target district size.
1827
1889
  // 'too_big' = The counties that are bigger than a district, e.g., ['Mecklenburg', 'Wake']
1828
1890
  // 'too_big_fips' = Their FIPS codes, e.g., ['119', '183']
@@ -2100,12 +2162,15 @@ const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
2100
2162
  const dra_analytics_1 = __webpack_require__(/*! @dra2020/dra-analytics */ "@dra2020/dra-analytics");
2101
2163
  // PROFILE A PLAN
2102
2164
  const KEEP_DECIMALS = 6;
2165
+ // MMD - Added # reps per district to the profile
2103
2166
  function profilePlan(s, bLog = false) {
2104
2167
  const state = s.state.xx;
2105
2168
  const planName = s.title;
2106
2169
  const nDistricts = s.state.nDistricts;
2170
+ const nReps = s.state.nReps; // MMD
2107
2171
  const nCounties = s.counties.nCounties;
2108
- const targetSize = Math.round(s.state.totalPop / nDistricts);
2172
+ const targetSize = s.state.targetSize; // MMD
2173
+ // const targetSize: number = Math.round(s.state.totalPop / nDistricts);
2109
2174
  const popByDistrict = U.deepCopy(s.districts.table.totalPop.slice(1, -1));
2110
2175
  const geoPropsByDistrict = makeArrayOfGeoProps(s, bLog);
2111
2176
  const splits = makeNakedCxD(s);
@@ -2126,10 +2191,12 @@ function profilePlan(s, bLog = false) {
2126
2191
  }
2127
2192
  const statewideDemographics = getStatewideDemographics(s);
2128
2193
  const demographicsByDistrict = getDemographicsByDistrict(s);
2194
+ // MMD - Extended the profile for # reps per district
2129
2195
  const profile = {
2130
2196
  state: state,
2131
2197
  name: planName,
2132
2198
  nDistricts: nDistricts,
2199
+ repsByDistrict: s.repsByDistrictt,
2133
2200
  nCounties: nCounties,
2134
2201
  bStateLeg: s.legislativeDistricts,
2135
2202
  population: {
@@ -2227,10 +2294,12 @@ function computeMetrics(p, districtShapes, bLog = false) {
2227
2294
  // Calculate county-district splitting metrics ...
2228
2295
  const CxD = p.counties;
2229
2296
  const _sS = dra_analytics_1.Splitting.makeSplittingScorecard(CxD, bLog);
2297
+ // MMD - Add # reps per district to call
2230
2298
  // Calculate population deviation-related metrics ...
2231
2299
  const totPopByDistrict = p.population.byDistrict;
2232
2300
  const targetSize = p.population.targetSize;
2233
- const _pdS = dra_analytics_1.Equal.makePopulationScorecard(totPopByDistrict, targetSize, bLegislative, bLog);
2301
+ const repsByDistrict = p.repsByDistrict;
2302
+ const _pdS = dra_analytics_1.Equal.makePopulationScorecard(totPopByDistrict, targetSize, bLegislative, repsByDistrict, bLog);
2234
2303
  const details = {};
2235
2304
  // Assemble the pieces into new scorecard
2236
2305
  const scorecard = {