@dra2020/district-analytics 3.2.0 → 4.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/dist/cli.js CHANGED
@@ -89264,7 +89264,15 @@ module.exports = g;
89264
89264
  //
89265
89265
  // THE NODE PACKAGE API
89266
89266
  //
89267
+ var __importStar = (this && this.__importStar) || function (mod) {
89268
+ if (mod && mod.__esModule) return mod;
89269
+ var result = {};
89270
+ if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
89271
+ result["default"] = mod;
89272
+ return result;
89273
+ };
89267
89274
  Object.defineProperty(exports, "__esModule", { value: true });
89275
+ const C = __importStar(__webpack_require__(/*! ./config */ "./src/config.ts"));
89268
89276
  const score_1 = __webpack_require__(/*! ./score */ "./src/score.ts");
89269
89277
  class Scorer {
89270
89278
  constructor() {
@@ -89272,6 +89280,9 @@ class Scorer {
89272
89280
  score(profile, overridesJSON) {
89273
89281
  return score_1.scorePlan(profile, overridesJSON);
89274
89282
  }
89283
+ populationDeviationThreshold(bLegislative) {
89284
+ return C.popdevThreshold(bLegislative);
89285
+ }
89275
89286
  }
89276
89287
  exports.Scorer = Scorer;
89277
89288
 
@@ -89300,27 +89311,33 @@ var __importStar = (this && this.__importStar) || function (mod) {
89300
89311
  Object.defineProperty(exports, "__esModule", { value: true });
89301
89312
  const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
89302
89313
  const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
89314
+ const C = __importStar(__webpack_require__(/*! ./config */ "./src/config.ts"));
89303
89315
  const normalize_1 = __webpack_require__(/*! ./normalize */ "./src/normalize.ts");
89304
89316
  function scoreCountySplitting(rawValue, nCounties, nDistricts) {
89305
89317
  const _normalizer = new normalize_1.Normalizer(rawValue);
89306
- const stateCountySplitThreshold = countySplitThreshold(nCounties, nDistricts);
89307
- _normalizer.clip(S.COUNTY_BEST, stateCountySplitThreshold);
89308
- _normalizer.unitize(S.COUNTY_BEST, stateCountySplitThreshold);
89318
+ const best = C.countySplittingRange()[C.BEG];
89319
+ const worst = countySplitThreshold(nCounties, nDistricts);
89320
+ _normalizer.clip(best, worst);
89321
+ _normalizer.unitize(best, worst);
89309
89322
  _normalizer.invert();
89310
89323
  _normalizer.rescale();
89311
89324
  return _normalizer.normalizedNum;
89312
89325
  }
89313
89326
  exports.scoreCountySplitting = scoreCountySplitting;
89314
89327
  function countySplitThreshold(nCounties, nDistricts) {
89315
- const nAllowableSplits = Math.min(S.ALLOWABLE_SPLITS_MULTIPLIER * (nDistricts - 1));
89316
- const threshold = ((nAllowableSplits * S.COUNTY_WORST) + ((nCounties - nAllowableSplits) * 1.0)) / nCounties;
89328
+ const m = C.allowableSplitsMultiplier();
89329
+ const worst = C.countySplittingRange()[1];
89330
+ const nAllowableSplits = Math.min(m * (nDistricts - 1));
89331
+ const threshold = ((nAllowableSplits * worst) + ((nCounties - nAllowableSplits) * 1.0)) / nCounties;
89317
89332
  return threshold;
89318
89333
  }
89319
89334
  exports.countySplitThreshold = countySplitThreshold;
89320
89335
  function scoreDistrictSplitting(rawValue) {
89321
89336
  const _normalizer = new normalize_1.Normalizer(rawValue);
89322
- _normalizer.clip(S.DISTRICT_BEST, S.DISTRICT_WORST);
89323
- _normalizer.unitize(S.DISTRICT_BEST, S.DISTRICT_WORST);
89337
+ const best = C.districtSplittingRange()[C.BEG];
89338
+ const worst = C.districtSplittingRange()[C.END];
89339
+ _normalizer.clip(best, worst);
89340
+ _normalizer.unitize(best, worst);
89324
89341
  _normalizer.invert();
89325
89342
  _normalizer.rescale();
89326
89343
  return _normalizer.normalizedNum;
@@ -89415,7 +89432,7 @@ function reduceCSplits(CxD, districtTotals) {
89415
89432
  for (let i = 1; i < nD; i++) {
89416
89433
  let split_total = CxDreducedC[i][j];
89417
89434
  if (split_total > 0) {
89418
- if (areRoughlyEqual(split_total, districtTotals[i - 1])) {
89435
+ if (U.areRoughlyEqual(split_total, districtTotals[i - 1], S.EQUAL_TOLERANCE)) {
89419
89436
  CxDreducedC[0][j] += split_total;
89420
89437
  CxDreducedC[i][j] = 0;
89421
89438
  }
@@ -89438,7 +89455,7 @@ function reduceDSplits(CxD, countyTotals) {
89438
89455
  for (let j = 1; j < nC; j++) {
89439
89456
  let split_total = CxDreducedD[i][j];
89440
89457
  if (split_total > 0) {
89441
- if (areRoughlyEqual(split_total, countyTotals[j - 1])) {
89458
+ if (U.areRoughlyEqual(split_total, countyTotals[j - 1], S.EQUAL_TOLERANCE)) {
89442
89459
  CxDreducedD[i][0] += split_total;
89443
89460
  CxDreducedD[i][j] = 0;
89444
89461
  }
@@ -89502,13 +89519,6 @@ function calcDistrictFractions(CxD, districtTotals) {
89502
89519
  return g;
89503
89520
  }
89504
89521
  exports.calcDistrictFractions = calcDistrictFractions;
89505
- // Deal with decimal census "counts" due to disagg/re-agg
89506
- function areRoughlyEqual(x, y) {
89507
- let delta = Math.abs(x - y);
89508
- let result = (delta < S.EQUAL_TOLERANCE) ? true : false;
89509
- return result;
89510
- }
89511
- exports.areRoughlyEqual = areRoughlyEqual;
89512
89522
  function splitScore(splits) {
89513
89523
  let e;
89514
89524
  if (splits.length > 0) {
@@ -89593,7 +89603,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
89593
89603
  };
89594
89604
  Object.defineProperty(exports, "__esModule", { value: true });
89595
89605
  const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
89596
- const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
89606
+ const C = __importStar(__webpack_require__(/*! ./config */ "./src/config.ts"));
89597
89607
  const normalize_1 = __webpack_require__(/*! ./normalize */ "./src/normalize.ts");
89598
89608
  // Migrated from district-analytics:
89599
89609
  // Measures of compactness compare district shapes to various ideally compact
@@ -89715,8 +89725,10 @@ function calcReock(area, diameter) {
89715
89725
  exports.calcReock = calcReock;
89716
89726
  function scoreReock(rawValue) {
89717
89727
  const _normalizer = new normalize_1.Normalizer(rawValue);
89718
- _normalizer.clip(S.REOCK_WORST, S.REOCK_BEST);
89719
- _normalizer.unitize(S.REOCK_WORST, S.REOCK_BEST);
89728
+ const worst = C.reockRange()[C.BEG];
89729
+ const best = C.reockRange()[C.END];
89730
+ _normalizer.clip(worst, best);
89731
+ _normalizer.unitize(worst, best);
89720
89732
  _normalizer.rescale();
89721
89733
  return _normalizer.normalizedNum;
89722
89734
  }
@@ -89734,12 +89746,12 @@ function doPolsbyPopper(g, bLog = false) {
89734
89746
  if (districtProps) {
89735
89747
  let a = districtProps[0 /* Area */];
89736
89748
  let p = districtProps[1 /* Perimeter */];
89737
- let polsbyPopper = calcPolsbyPopper(a, p);
89749
+ let polsby = calcPolsbyPopper(a, p);
89738
89750
  // Save each district score
89739
- scores.push(polsbyPopper);
89751
+ scores.push(polsby);
89740
89752
  // Echo the results by district
89741
89753
  if (bLog)
89742
- console.log("Polsby-Popper for district", districtID, "=", polsbyPopper);
89754
+ console.log("Polsby-Popper for district", districtID, "=", polsby);
89743
89755
  }
89744
89756
  else {
89745
89757
  console.log("No shape or no geopropertes in Polsby-Popper ...");
@@ -89762,14 +89774,233 @@ function calcPolsbyPopper(area, perimeter) {
89762
89774
  exports.calcPolsbyPopper = calcPolsbyPopper;
89763
89775
  function scorePolsbyPopper(rawValue) {
89764
89776
  const _normalizer = new normalize_1.Normalizer(rawValue);
89765
- _normalizer.clip(S.POLSBY_WORST, S.POLSBY_BEST);
89766
- _normalizer.unitize(S.POLSBY_WORST, S.POLSBY_BEST);
89777
+ const worst = C.polsbyRange()[C.BEG];
89778
+ const best = C.polsbyRange()[C.END];
89779
+ _normalizer.clip(worst, best);
89780
+ _normalizer.unitize(worst, best);
89767
89781
  _normalizer.rescale();
89768
89782
  return _normalizer.normalizedNum;
89769
89783
  }
89770
89784
  exports.scorePolsbyPopper = scorePolsbyPopper;
89771
89785
 
89772
89786
 
89787
+ /***/ }),
89788
+
89789
+ /***/ "./src/config.json":
89790
+ /*!*************************!*\
89791
+ !*** ./src/config.json ***!
89792
+ \*************************/
89793
+ /*! exports provided: partisan, minority, traditionalPrinciples, default */
89794
+ /***/ (function(module) {
89795
+
89796
+ module.exports = JSON.parse("{\"partisan\":{\"bias\":{\"range\":[0,0.2],\"weight\":[50,80]},\"impact\":{\"weight\":[50,0],\"threshold\":4},\"competitiveness\":{\"overall\":{\"range\":[0,0.67],\"weight\":25},\"marginal\":{\"range\":[0,0.85],\"weight\":75},\"range\":[0.45,0.55],\"distribution\":[0.25,0.75],\"weight\":[0,20]},\"bonus\":2,\"weight\":[100,80]},\"minority\":{\"range\":[0.37,0.5],\"distribution\":[0.25,0.75],\"shift\":0.12,\"bonus\":20},\"traditionalPrinciples\":{\"compactness\":{\"reock\":{\"range\":[0.25,0.5],\"weight\":50},\"polsby\":{\"range\":[0.1,0.5],\"weight\":50},\"weight\":[0,50]},\"splitting\":{\"county\":{\"range\":[1,1.71],\"allowableSplitsMultiplier\":1.5,\"weight\":50},\"district\":{\"range\":[1,1.5],\"weight\":50},\"weight\":[0,50]},\"popdev\":{\"range\":[[0.0075,0.002],[0.1,-1]],\"weight\":[0,0]},\"weight\":[0,20]}}");
89797
+
89798
+ /***/ }),
89799
+
89800
+ /***/ "./src/config.ts":
89801
+ /*!***********************!*\
89802
+ !*** ./src/config.ts ***!
89803
+ \***********************/
89804
+ /*! no static exports found */
89805
+ /***/ (function(module, exports, __webpack_require__) {
89806
+
89807
+ "use strict";
89808
+
89809
+ //
89810
+ // THRESHOLDS, SCALES, AND WEIGHTS FOR SCORING MODEL
89811
+ //
89812
+ // * A layer of functions over config.json, so that they can be overridden dynamically (TODO)
89813
+ //
89814
+ var __importDefault = (this && this.__importDefault) || function (mod) {
89815
+ return (mod && mod.__esModule) ? mod : { "default": mod };
89816
+ };
89817
+ Object.defineProperty(exports, "__esModule", { value: true });
89818
+ // Defaults
89819
+ const config_json_1 = __importDefault(__webpack_require__(/*! ./config.json */ "./src/config.json"));
89820
+ exports.BEG = 0;
89821
+ exports.END = 1;
89822
+ // PARTISAN
89823
+ function biasWeight(context, overridesJSON) {
89824
+ const bW = config_json_1.default.partisan.bias.weight[context];
89825
+ return bW;
89826
+ }
89827
+ exports.biasWeight = biasWeight;
89828
+ function biasRange(overridesJSON) {
89829
+ const range = config_json_1.default.partisan.bias.range;
89830
+ return range;
89831
+ }
89832
+ exports.biasRange = biasRange;
89833
+ function impactWeight(context, overridesJSON) {
89834
+ const iW = config_json_1.default.partisan.impact.weight[context];
89835
+ return iW;
89836
+ }
89837
+ exports.impactWeight = impactWeight;
89838
+ function winnerBonus(overridesJSON) {
89839
+ const bonus = config_json_1.default.partisan.bonus;
89840
+ return bonus;
89841
+ }
89842
+ exports.winnerBonus = winnerBonus;
89843
+ // The maximum # of unearned seats that scores positively
89844
+ function unearnedThreshold(overridesJSON) {
89845
+ const threshold = config_json_1.default.partisan.impact.threshold;
89846
+ return threshold;
89847
+ }
89848
+ exports.unearnedThreshold = unearnedThreshold;
89849
+ function competitivenessWeight(context, overridesJSON) {
89850
+ const cW = config_json_1.default.partisan.competitiveness.weight[context];
89851
+ return cW;
89852
+ }
89853
+ exports.competitivenessWeight = competitivenessWeight;
89854
+ function competitiveRange(overridesJSON) {
89855
+ const range = config_json_1.default.partisan.competitiveness.range;
89856
+ return range;
89857
+ }
89858
+ exports.competitiveRange = competitiveRange;
89859
+ function competitiveDistribution(overridesJSON) {
89860
+ const dist = config_json_1.default.partisan.competitiveness.distribution;
89861
+ return dist;
89862
+ }
89863
+ exports.competitiveDistribution = competitiveDistribution;
89864
+ function overallCompetitivenessRange(overridesJSON) {
89865
+ const range = config_json_1.default.partisan.competitiveness.overall.range;
89866
+ return range;
89867
+ }
89868
+ exports.overallCompetitivenessRange = overallCompetitivenessRange;
89869
+ function marginalCompetitivenessRange(overridesJSON) {
89870
+ const range = config_json_1.default.partisan.competitiveness.marginal.range;
89871
+ return range;
89872
+ }
89873
+ exports.marginalCompetitivenessRange = marginalCompetitivenessRange;
89874
+ function marginalCompetitivenessWeight(overridesJSON) {
89875
+ const mcW = config_json_1.default.partisan.competitiveness.marginal.weight;
89876
+ return mcW;
89877
+ }
89878
+ exports.marginalCompetitivenessWeight = marginalCompetitivenessWeight;
89879
+ function overallCompetitivenessWeight(overridesJSON) {
89880
+ const ocW = config_json_1.default.partisan.competitiveness.overall.weight;
89881
+ return ocW;
89882
+ }
89883
+ exports.overallCompetitivenessWeight = overallCompetitivenessWeight;
89884
+ // MINORITY
89885
+ function minorityOpportunityRange(overridesJSON) {
89886
+ const range = config_json_1.default.minority.range;
89887
+ return range;
89888
+ }
89889
+ exports.minorityOpportunityRange = minorityOpportunityRange;
89890
+ function minorityOpportunityDistribution(overridesJSON) {
89891
+ const dist = config_json_1.default.minority.distribution;
89892
+ return dist;
89893
+ }
89894
+ exports.minorityOpportunityDistribution = minorityOpportunityDistribution;
89895
+ function minorityShift(overridesJSON) {
89896
+ const shift = config_json_1.default.minority.shift;
89897
+ return shift;
89898
+ }
89899
+ exports.minorityShift = minorityShift;
89900
+ function opportunityDistrictBonus(overridesJSON) {
89901
+ const bonus = config_json_1.default.minority.bonus;
89902
+ return bonus;
89903
+ }
89904
+ exports.opportunityDistrictBonus = opportunityDistrictBonus;
89905
+ function minorityBonus(overridesJSON) {
89906
+ const bonus = config_json_1.default.minority.bonus;
89907
+ return bonus;
89908
+ }
89909
+ exports.minorityBonus = minorityBonus;
89910
+ // TRADITIONAL DISTRICTING PRINCIPLES
89911
+ function compactnessWeight(context, overridesJSON) {
89912
+ const cW = config_json_1.default.traditionalPrinciples.compactness.weight[context];
89913
+ return cW;
89914
+ }
89915
+ exports.compactnessWeight = compactnessWeight;
89916
+ function reockWeight(overridesJSON) {
89917
+ const rW = config_json_1.default.traditionalPrinciples.compactness.reock.weight;
89918
+ return rW;
89919
+ }
89920
+ exports.reockWeight = reockWeight;
89921
+ function reockRange(overridesJSON) {
89922
+ const range = config_json_1.default.traditionalPrinciples.compactness.reock.range;
89923
+ return range;
89924
+ }
89925
+ exports.reockRange = reockRange;
89926
+ function polsbyWeight(overridesJSON) {
89927
+ const ppW = config_json_1.default.traditionalPrinciples.compactness.polsby.weight;
89928
+ return ppW;
89929
+ }
89930
+ exports.polsbyWeight = polsbyWeight;
89931
+ function polsbyRange(overridesJSON) {
89932
+ const range = config_json_1.default.traditionalPrinciples.compactness.polsby.range;
89933
+ return range;
89934
+ }
89935
+ exports.polsbyRange = polsbyRange;
89936
+ function splittingWeight(context, overridesJSON) {
89937
+ const sW = config_json_1.default.traditionalPrinciples.splitting.weight[context];
89938
+ return sW;
89939
+ }
89940
+ exports.splittingWeight = splittingWeight;
89941
+ function countySplittingWeight(overridesJSON) {
89942
+ const csW = config_json_1.default.traditionalPrinciples.splitting.county.weight;
89943
+ return csW;
89944
+ }
89945
+ exports.countySplittingWeight = countySplittingWeight;
89946
+ function countySplittingRange(overridesJSON) {
89947
+ const range = config_json_1.default.traditionalPrinciples.splitting.county.range;
89948
+ return range;
89949
+ }
89950
+ exports.countySplittingRange = countySplittingRange;
89951
+ function allowableSplitsMultiplier(overridesJSON) {
89952
+ const m = config_json_1.default.traditionalPrinciples.splitting.county.allowableSplitsMultiplier;
89953
+ return m;
89954
+ }
89955
+ exports.allowableSplitsMultiplier = allowableSplitsMultiplier;
89956
+ function districtSplittingWeight(overridesJSON) {
89957
+ const dsW = config_json_1.default.traditionalPrinciples.splitting.district.weight;
89958
+ return dsW;
89959
+ }
89960
+ exports.districtSplittingWeight = districtSplittingWeight;
89961
+ function districtSplittingRange(overridesJSON) {
89962
+ const range = config_json_1.default.traditionalPrinciples.splitting.district.range;
89963
+ return range;
89964
+ }
89965
+ exports.districtSplittingRange = districtSplittingRange;
89966
+ function popdevWeight(context, overridesJSON) {
89967
+ const pdW = config_json_1.default.traditionalPrinciples.popdev.weight[context];
89968
+ return pdW;
89969
+ }
89970
+ exports.popdevWeight = popdevWeight;
89971
+ // NOTE - Raw ranges, not inverted (i.e., smaller is better)
89972
+ // NOTE - This could be optimized to not calc LD values for CD's (or do it once)
89973
+ function popdevRange(bLegislative, overridesJSON) {
89974
+ const cdRange = config_json_1.default.traditionalPrinciples.popdev.range[0 /* Congressional */];
89975
+ const worstCD = cdRange[exports.BEG];
89976
+ const bestCD = cdRange[exports.END];
89977
+ const ldRange = config_json_1.default.traditionalPrinciples.popdev.range[1 /* StateLegislative */];
89978
+ const iRange = bLegislative ? ldRange : cdRange;
89979
+ const worst = iRange[exports.BEG];
89980
+ const best = bLegislative ? (bestCD / worstCD) * iRange[exports.BEG] : iRange[exports.END];
89981
+ // Invert the range, so bigger is better.
89982
+ return [worst, best];
89983
+ }
89984
+ exports.popdevRange = popdevRange;
89985
+ // NOTE - Raw threshold, not inverted (i.e., smaller is better)
89986
+ function popdevThreshold(bLegislative, overridesJSON) {
89987
+ const threshold = popdevRange(bLegislative)[exports.BEG];
89988
+ return threshold;
89989
+ }
89990
+ exports.popdevThreshold = popdevThreshold;
89991
+ // OVERALL SCORE
89992
+ function partisanWeight(context, overridesJSON) {
89993
+ const pW = config_json_1.default.partisan.weight[context];
89994
+ return pW;
89995
+ }
89996
+ exports.partisanWeight = partisanWeight;
89997
+ function traditionalPrinciplesWeight(context, overridesJSON) {
89998
+ const tpW = config_json_1.default.traditionalPrinciples.weight[context];
89999
+ return tpW;
90000
+ }
90001
+ exports.traditionalPrinciplesWeight = traditionalPrinciplesWeight;
90002
+
90003
+
89773
90004
  /***/ }),
89774
90005
 
89775
90006
  /***/ "./src/equal.ts":
@@ -89793,7 +90024,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
89793
90024
  };
89794
90025
  Object.defineProperty(exports, "__esModule", { value: true });
89795
90026
  const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
89796
- const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
90027
+ const C = __importStar(__webpack_require__(/*! ./config */ "./src/config.ts"));
89797
90028
  const normalize_1 = __webpack_require__(/*! ./normalize */ "./src/normalize.ts");
89798
90029
  // Migrated from district-analytics
89799
90030
  // Expect a full dict of district IDs & values (which might be zero = empty)
@@ -89814,7 +90045,7 @@ function doPopulationDeviation(e, targetSize, bLegislative, bLog = false) {
89814
90045
  // Round the raw value to the desired level of precision
89815
90046
  const popDev = U.trim((max - min) / targetSize);
89816
90047
  const score = scorePopulationDeviation(popDev, bLegislative);
89817
- const threshold = bLegislative ? S.POPDEV_WORST_LD : S.POPDEV_WORST;
90048
+ const threshold = C.popdevThreshold(bLegislative);
89818
90049
  const notes = {
89819
90050
  'maxDeviation': max - min,
89820
90051
  'threshold': threshold
@@ -89830,11 +90061,11 @@ function doPopulationDeviation(e, targetSize, bLegislative, bLog = false) {
89830
90061
  exports.doPopulationDeviation = doPopulationDeviation;
89831
90062
  function scorePopulationDeviation(rawValue, bLegislative) {
89832
90063
  const _normalizer = new normalize_1.Normalizer(rawValue);
89833
- const rangeMin = bLegislative ? S.POPEQ_MIN_LD : S.POPEQ_MIN;
89834
- const rangeMax = bLegislative ? S.POPEQ_MAX_LD : S.POPEQ_MAX;
90064
+ // Raw range in not inverted (i.e., smaller is better)
90065
+ const range = C.popdevRange(bLegislative);
89835
90066
  _normalizer.invert();
89836
- _normalizer.clip(rangeMin, rangeMax);
89837
- _normalizer.unitize(rangeMin, rangeMax);
90067
+ _normalizer.clip(1.0 - range[C.BEG], 1.0 - range[C.END]);
90068
+ _normalizer.unitize(1.0 - range[C.BEG], 1.0 - range[C.END]);
89838
90069
  _normalizer.rescale();
89839
90070
  return _normalizer.normalizedNum;
89840
90071
  }
@@ -89843,10 +90074,263 @@ exports.scorePopulationDeviation = scorePopulationDeviation;
89843
90074
 
89844
90075
  /***/ }),
89845
90076
 
89846
- /***/ "./src/fair.ts":
89847
- /*!*********************!*\
89848
- !*** ./src/fair.ts ***!
89849
- \*********************/
90077
+ /***/ "./src/index.ts":
90078
+ /*!**********************!*\
90079
+ !*** ./src/index.ts ***!
90080
+ \**********************/
90081
+ /*! no static exports found */
90082
+ /***/ (function(module, exports, __webpack_require__) {
90083
+
90084
+ "use strict";
90085
+
90086
+ //
90087
+ // SCORE PACKAGE API
90088
+ //
90089
+ function __export(m) {
90090
+ for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
90091
+ }
90092
+ Object.defineProperty(exports, "__esModule", { value: true });
90093
+ __export(__webpack_require__(/*! ./_api */ "./src/_api.ts"));
90094
+ var types_1 = __webpack_require__(/*! ./types */ "./src/types.ts");
90095
+ exports.sampleScorecard = types_1.sampleScorecard;
90096
+ exports.sampleProfile = types_1.sampleProfile;
90097
+
90098
+
90099
+ /***/ }),
90100
+
90101
+ /***/ "./src/minority.ts":
90102
+ /*!*************************!*\
90103
+ !*** ./src/minority.ts ***!
90104
+ \*************************/
90105
+ /*! no static exports found */
90106
+ /***/ (function(module, exports, __webpack_require__) {
90107
+
90108
+ "use strict";
90109
+
90110
+ //
90111
+ // SCORING FOR MINORITY OPPORTUNITY
90112
+ //
90113
+ var __importStar = (this && this.__importStar) || function (mod) {
90114
+ if (mod && mod.__esModule) return mod;
90115
+ var result = {};
90116
+ if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
90117
+ result["default"] = mod;
90118
+ return result;
90119
+ };
90120
+ Object.defineProperty(exports, "__esModule", { value: true });
90121
+ const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
90122
+ const C = __importStar(__webpack_require__(/*! ./config */ "./src/config.ts"));
90123
+ const normalize_1 = __webpack_require__(/*! ./normalize */ "./src/normalize.ts");
90124
+ const partisan_1 = __webpack_require__(/*! ./partisan */ "./src/partisan.ts");
90125
+ function evalMinorityOpportunity(p, bLog = false) {
90126
+ // Calculate average Democratic win share
90127
+ const VfArray = p.partisanProfile.vfArray;
90128
+ const DWins = VfArray.filter(x => x > 0.5);
90129
+ const averageDVf = (DWins.length > 0) ? U.avgArray(DWins) : undefined;
90130
+ // Determine proportional minority districts by demographic (ignore 'White')
90131
+ const districtsByDemo = calcDistrictsByDemo(p.demographicProfile.stateMfArray.slice(1), p.nDistricts);
90132
+ // Initialize arrays for results
90133
+ let offset = 1; // Don't process 'White'
90134
+ let nDemos = 6 /* Native */ + 1 - offset; // Ditto
90135
+ let nBuckets = 2; // 37–50% and > 50%
90136
+ const demosByDistrict = p.demographicProfile.mfArrayByDistrict;
90137
+ let bucketsByDemo = new Array(nDemos);
90138
+ for (let j = 0; j < nDemos; j++) {
90139
+ bucketsByDemo[j] = [0, 0];
90140
+ }
90141
+ let opptyByDemo = U.initArray(nDemos, 0.0);
90142
+ // For each district
90143
+ for (let i = 0; i < p.nDistricts; i++) {
90144
+ // Find the opportunities for minority representation
90145
+ for (let j = 0; j < nDemos; j++) {
90146
+ const proportion = U.deepCopy(demosByDistrict[i][j + offset]);
90147
+ if (exceedsMinimumThreshold(proportion)) {
90148
+ // Bucket opportunity districts
90149
+ const bucket = (exceedsMaximumThreshold(proportion)) ? 1 : 0;
90150
+ bucketsByDemo[j][bucket] += 1;
90151
+ // Accumulate seat probabilities
90152
+ opptyByDemo[j] += estMinorityOpportunity(proportion);
90153
+ }
90154
+ }
90155
+ }
90156
+ // Sum the # of level 1 (37–50%) and level 2 (> 50%) opportunity districts - ignore total minority
90157
+ // let l1: number = 0;
90158
+ // let l2: number = 0;
90159
+ // bucketsByDemo.slice(1).forEach(function (buckets: number[]): void
90160
+ // {
90161
+ // l1 += buckets[T.OpportunityBucket.Level1];
90162
+ // l2 += buckets[T.OpportunityBucket.Level2]
90163
+ // });
90164
+ // Sum the # of opportunity districts - ignore total minority
90165
+ const oD = U.sumArray(opptyByDemo.slice(1));
90166
+ // Sum the # of proportion districts - ignore total minority
90167
+ const pD = U.sumArray(districtsByDemo.slice(1));
90168
+ // Score opportunity
90169
+ const score = scoreMinority(oD, pD);
90170
+ let mS = {
90171
+ report: {
90172
+ averageDVf: averageDVf,
90173
+ bucketsByDemographic: bucketsByDemo
90174
+ },
90175
+ // nOpportunity1: l1,
90176
+ // nOpportunity2: l2,
90177
+ nProportional: pD,
90178
+ opportunityDistricts: oD,
90179
+ score: score
90180
+ };
90181
+ return mS;
90182
+ }
90183
+ exports.evalMinorityOpportunity = evalMinorityOpportunity;
90184
+ function scoreMinority(oD, pD) {
90185
+ const bonus = C.minorityBonus();
90186
+ const score = (pD > 0) ? Math.round((Math.min(oD, pD) / pD) * bonus) : 0;
90187
+ return score;
90188
+ }
90189
+ exports.scoreMinority = scoreMinority;
90190
+ function calcDistrictsByDemo(MfArray, N) {
90191
+ const districtsByDemo = MfArray.map(v => calcProportionalDistricts(v, N));
90192
+ return districtsByDemo;
90193
+ }
90194
+ exports.calcDistrictsByDemo = calcDistrictsByDemo;
90195
+ // NOTE - Shift minority proportions up, so 37% minority scores like 49% share,
90196
+ // but use the uncompressed seat probability distribution. This makes a 37%
90197
+ // district have a 40% chance of winning, a 42% district have an 84% chance,
90198
+ // and a 50% district have a 99% chance. Below 37% has no chance.
90199
+ //
90200
+ function estMinorityOpportunity(Mf) {
90201
+ // NOTE - Switch to compress the probability distribution
90202
+ const bCompress = false;
90203
+ const dist = bCompress ? C.minorityOpportunityDistribution() : [0.0, 1.0];
90204
+ const range = C.minorityOpportunityRange();
90205
+ const _normalizer = new normalize_1.Normalizer(Mf);
90206
+ const shift = C.minorityShift();
90207
+ _normalizer.wipNum += shift;
90208
+ _normalizer.clip(dist[C.BEG], dist[C.END]);
90209
+ _normalizer.unitize(dist[C.BEG], dist[C.END]);
90210
+ const oppty = (Mf < range[C.BEG]) ? 0.0 : partisan_1.estSeatProbability(_normalizer.wipNum, dist);
90211
+ return oppty;
90212
+ }
90213
+ exports.estMinorityOpportunity = estMinorityOpportunity;
90214
+ // HELPERS
90215
+ function exceedsMinimumThreshold(Mf) {
90216
+ const threshold = C.minorityOpportunityRange()[C.BEG];
90217
+ return Mf >= threshold;
90218
+ }
90219
+ function exceedsMaximumThreshold(Mf) {
90220
+ const threshold = C.minorityOpportunityRange()[C.END];
90221
+ return Mf > threshold;
90222
+ }
90223
+ function calcProportionalDistricts(proportion, nDistricts) {
90224
+ // TODO - Bump up to get a statewide proportion on the bubble to rate one district?
90225
+ const roundUp = 0.0;
90226
+ const fractional = proportion * nDistricts;
90227
+ const integral = Math.round(fractional + roundUp);
90228
+ return integral;
90229
+ }
90230
+ exports.calcProportionalDistricts = calcProportionalDistricts;
90231
+
90232
+
90233
+ /***/ }),
90234
+
90235
+ /***/ "./src/normalize.ts":
90236
+ /*!**************************!*\
90237
+ !*** ./src/normalize.ts ***!
90238
+ \**************************/
90239
+ /*! no static exports found */
90240
+ /***/ (function(module, exports, __webpack_require__) {
90241
+
90242
+ "use strict";
90243
+
90244
+ //
90245
+ // NORMALIZATION UTILITIES
90246
+ //
90247
+ var __importStar = (this && this.__importStar) || function (mod) {
90248
+ if (mod && mod.__esModule) return mod;
90249
+ var result = {};
90250
+ if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
90251
+ result["default"] = mod;
90252
+ return result;
90253
+ };
90254
+ Object.defineProperty(exports, "__esModule", { value: true });
90255
+ const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
90256
+ class Normalizer {
90257
+ constructor(rawValue) {
90258
+ this.rawNum = rawValue;
90259
+ this.wipNum = this.rawNum;
90260
+ }
90261
+ // *Don't* transform the input.
90262
+ identity() {
90263
+ return this.wipNum;
90264
+ }
90265
+ positive() {
90266
+ this.wipNum = Math.abs(this.wipNum);
90267
+ return this.wipNum;
90268
+ }
90269
+ // Invert a value in the unit range [0.0–1.0] (so that bigger is better).
90270
+ invert() {
90271
+ // TODO - DAVID: Can we make this throw an Error and expect that in unit test?
90272
+ console.assert(((this.wipNum >= 0.0) && (this.wipNum <= 1.0)), "Inverting: " + S.OUT_OF_RANGE_MSG, this.wipNum);
90273
+ this.wipNum = 1.0 - this.wipNum;
90274
+ return this.wipNum;
90275
+ }
90276
+ // Constrain the value to be within a specified range.
90277
+ clip(begin_range, end_range) {
90278
+ // Handle the ends of the range being given either order
90279
+ const min_range = Math.min(begin_range, end_range);
90280
+ const max_range = Math.max(begin_range, end_range);
90281
+ this.wipNum = Math.max(Math.min(this.wipNum, max_range), min_range);
90282
+ return this.wipNum;
90283
+ }
90284
+ // Recast the value as the delta from a baseline value. NOOP if it is zero.
90285
+ // NOTE - Values can be + or -.
90286
+ rebase(base) {
90287
+ this.wipNum = this.wipNum - base;
90288
+ return this.wipNum;
90289
+ }
90290
+ // Re-scale a value into the [0.0 – 1.0] range, using a given range.
90291
+ // NOTE - This assumes that values have alrady been clipped into the range.
90292
+ unitize(begin_range, end_range) {
90293
+ const min_range = Math.min(begin_range, end_range);
90294
+ const max_range = Math.max(begin_range, end_range);
90295
+ // TODO - DAVID: Can we make this throw an Error and expect that in unit test?
90296
+ console.assert(((this.wipNum >= min_range) && (this.wipNum <= max_range)), "Unitizing: " + S.OUT_OF_RANGE_MSG, this.wipNum);
90297
+ const ranged = this.wipNum - min_range;
90298
+ this.wipNum = Math.abs(ranged / (end_range - begin_range));
90299
+ return this.wipNum;
90300
+ }
90301
+ // Decay a value in the unit range [0.0–1.0] by its distance from zero.
90302
+ // NOTE - If the range is already such that "bigger is better," then the closer
90303
+ // the value is to 1.0 (the best) the *less* it will decay.
90304
+ decay(fn = 0 /* Gravity */) {
90305
+ // TODO - DAVID: Can we make this throw an Error and expect that in unit test?
90306
+ console.assert(((this.wipNum >= 0.0) && (this.wipNum <= 1.0)), "Decaying: " + S.OUT_OF_RANGE_MSG, this.wipNum);
90307
+ switch (fn) {
90308
+ case 0 /* Gravity */: {
90309
+ this.wipNum = Math.pow(this.wipNum, S.DISTANCE_WEIGHT);
90310
+ return this.wipNum;
90311
+ }
90312
+ default: {
90313
+ throw new Error("Decay function not recognized.");
90314
+ }
90315
+ }
90316
+ }
90317
+ // Translate a value in the unit range to the user-friendly range [0 – 100].
90318
+ rescale() {
90319
+ // TODO - DAVID: Can we make this throw an Error and expect that in unit test?
90320
+ console.assert(((this.wipNum >= 0.0) && (this.wipNum <= 1.0)), "Rescaling: " + S.OUT_OF_RANGE_MSG, this.wipNum);
90321
+ this.normalizedNum = Math.round(this.wipNum * S.NORMALIZED_RANGE);
90322
+ return this.normalizedNum;
90323
+ }
90324
+ }
90325
+ exports.Normalizer = Normalizer;
90326
+
90327
+
90328
+ /***/ }),
90329
+
90330
+ /***/ "./src/partisan.ts":
90331
+ /*!*************************!*\
90332
+ !*** ./src/partisan.ts ***!
90333
+ \*************************/
89850
90334
  /*! no static exports found */
89851
90335
  /***/ (function(module, exports, __webpack_require__) {
89852
90336
 
@@ -89865,117 +90349,218 @@ var __importStar = (this && this.__importStar) || function (mod) {
89865
90349
  Object.defineProperty(exports, "__esModule", { value: true });
89866
90350
  const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
89867
90351
  const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
90352
+ const C = __importStar(__webpack_require__(/*! ./config */ "./src/config.ts"));
89868
90353
  const normalize_1 = __webpack_require__(/*! ./normalize */ "./src/normalize.ts");
89869
90354
  // NOTE - I'm passing T.VfArray's into everything. District indices = array indices.
89870
90355
  // NOTE - I do not (cannot) assume that the values are sorted.
89871
90356
  // SCORE BIAS & COMPETITIVENESS
89872
90357
  /* SCORECARD FIELDS:
89873
90358
 
89874
- * ^S# [BestS] = the Democratic seats closest to proportional
89875
- * ^S% [BestSf] = the corresponding Democratic seat share
89876
- * F# [FptpS] = the estimated number of Democratic seats using first past the post
89877
- * S# [ProbableS] = the estimated Democratic seats, using seat probabilities
89878
- * S% [ProbableSf] = the estimated Democratic seat share fraction, calculated as S# / N
89879
- * B% [Bias]= the bias calculated as S% – ^S%
89880
- * B$ [BiasScore] = the bias score normalized [0–100]
89881
-
89882
- * R# [R] = the responsiveness at V%
89883
- * Rd [Rd] = the estimated # of responsive districts (using probabilities)
89884
- * Rd% [Rdf] = the estimated # of responsive districts, as a fraction of N
89885
- * Cd [Cd] = the estimated # of competitive districts, using probabilities & a narrower [0.25–0.75] range
89886
- * Cd% [Cdf] = the estimated # of competitive districts, as a fraction of N
89887
- * Md% [Mdf] = the probability that the marginal seats will flip, so S% => ^S%
89888
- * C$ [CompetitiveScore] = the competitiveness score normalized [0–100]
89889
-
89890
- * $$$ = the combined partisan score normalize [0–100]
90359
+ * ^S# [propS] = the Democratic seats closest to proportional
90360
+ * ^S% [propSf] = the corresponding Democratic seat share
90361
+ * S! [fptpS] = the estimated number of Democratic seats using first past the post
90362
+ * S# [estS] = the estimated Democratic seats, using seat probabilities
90363
+ * S% [estSf] = the estimated Democratic seat share fraction, calculated as S# / N
90364
+
90365
+ * Bs50 [s50V] = Seat bias as a fraction of N
90366
+ * Bv50 [v50S] = Votes bias as a fraction
90367
+ * decl [decl] = Declination
90368
+ * gSym [gSym] = Global symmetry
90369
+
90370
+ * EG [EG] = Efficiency gap as a fraction
90371
+ * sVg [BsGf] = Seats bias @ <V> (geometric)
90372
+ * prop [prop] = Disproportionality
90373
+ * MM [mMs] = Mean median difference using statewide Vf
90374
+ * TO [tOf] = Turnout bias
90375
+ * MM' [mMd] = Mean median difference using average district v
90376
+ * LO [LO] = Lopsided outcomes
90377
+
90378
+ * B% [bias]= the bias calculated as S% – ^S%
90379
+ * B$ [bias.score] = the bias score normalized [0–100]
90380
+
90381
+ * UE# [unearnedS] = the number of unearned seats (R = positive; D = negative)
90382
+ * I$ [impact.score] -- the unearned seats normalized [0–100] as impact
90383
+
90384
+ * R [bigR] = Overall responsiveness
90385
+ * r [littleR] = The point responsiveness at V%
90386
+ * MIR [MIR] = Minimal inverse responsiveness
90387
+ * rD [rD] = the estimated # of responsive districts (using probabilities)
90388
+ * rD% [rDf] = the estimated # of responsive districts, as a fraction of N
90389
+
90390
+ * C [c] = the number of districts that fall into the range [45–55%]
90391
+ * cD [cD] = the estimated # of competitive districts, using probabilities & a narrower [0.25–0.75] range
90392
+ * cD% [cDf] = the estimated # of competitive districts, as a fraction of N
90393
+ * beg/end [mRange] = the 1–N indices for the first and last district that separate the likely result and the proportional result
90394
+ * Md [mD] = the competitiveness of the marginal districts
90395
+ * Md% [mDf] = the probability that the marginal seats will flip, so S% => ^S%
90396
+ * C$ [competitiveness.score] = the competitiveness score normalized [0–100]
90397
+
90398
+ * <P$ [score] = the combined partisan score used to compare plans *across* states
90399
+ * >P$ [score2] = the combined partisan score used to compare plans *within* a state, along with traditional districting principles
89891
90400
 
89892
90401
  */
89893
- function scorePartisan(Vf, VfArray, all = false) {
90402
+ function scorePartisan(Vf, VfArray, options) {
90403
+ const bAlternateMetrics = options.alternates;
90404
+ const bConstrained = options.constrained;
90405
+ const shift = options.shift;
89894
90406
  const N = VfArray.length;
89895
- const BestS = bestSeats(N, Vf);
89896
- const BestSf = bestSeatShare(BestS, N);
89897
- const FptpS = all ? estFPTPSeats(VfArray) : undefined;
89898
- const ProbableS = estProbableSeats(VfArray);
89899
- const ProbableSf = estProbableSeatShare(ProbableS, N);
89900
- const Bias = estBias(ProbableSf, BestSf);
89901
- const normalizedBias = scoreBias(Bias, Vf, ProbableSf, N);
89902
- const inferredSVpoints = all ? inferSVpoints(Vf, VfArray) : undefined;
89903
- const R = all ? estResponsiveness(Vf, inferredSVpoints) : undefined;
89904
- const Rd = all ? estResponsiveDistricts(VfArray) : undefined;
89905
- const Rdf = all ? estResponsiveDistrictsShare(Rd, N) : undefined;
89906
- const Cd = estCompetitiveDistricts(VfArray);
89907
- const Cdf = estCompetitiveDistrictsShare(Cd, N);
89908
- const Mdf = estMarginalCompetitiveShare(Vf, VfArray, N);
89909
- const normalizedCompetitiveness = scoreCompetitiveness(Mdf, Cdf);
89910
- const biasScoring = {
89911
- BestS: BestS,
89912
- BestSf: BestSf,
89913
- FptpS: FptpS,
89914
- ProbableS: ProbableS,
89915
- ProbableSf: ProbableSf,
89916
- Bias: Bias,
89917
- normalized: normalizedBias
90407
+ const propS = propSeats(N, Vf);
90408
+ const propSf = propSeatShare(propS, N);
90409
+ const fptpS = bAlternateMetrics ? estFPTPSeats(VfArray) : undefined;
90410
+ const range = bConstrained ? C.competitiveDistribution() : undefined;
90411
+ const estS = estSeats(VfArray, range);
90412
+ const estSf = estSeatShare(estS, N);
90413
+ const bias = estBias(estSf, propSf);
90414
+ const biasScore = scorebias(bias, Vf, estSf);
90415
+ const unearnedS = estUnearnedSeats(propS, estS);
90416
+ const impactScore = scoreImpact(unearnedS, Vf, N);
90417
+ // Calculate additional alternate metrics for reference
90418
+ // NOTE - Use the uncompressed seat probability function
90419
+ const inferredSVpoints = bAlternateMetrics ? inferSVpoints(Vf, VfArray, shift) : undefined;
90420
+ const TOf = bAlternateMetrics ? calcTurnoutBias(Vf, VfArray) : undefined;
90421
+ const Bs50 = bAlternateMetrics ? estPartisanBias(inferredSVpoints, N) : undefined;
90422
+ const Bs50f = (!(Bs50 === undefined)) ? U.trim(Bs50 / N) : undefined;
90423
+ const Bv50f = bAlternateMetrics ? estVotesBias(inferredSVpoints, N) : undefined;
90424
+ const decl = bAlternateMetrics ? calcDeclination(VfArray) : undefined;
90425
+ const gSym = bAlternateMetrics ? calcGlobalSymmetry(inferredSVpoints, Bs50f) : undefined;
90426
+ const EG = bAlternateMetrics ? calcEfficiencyGap(Vf, estSf) : undefined;
90427
+ const BsGf = bAlternateMetrics ? estGeometricSeatsBias(Vf, inferredSVpoints) : undefined;
90428
+ const prop = bAlternateMetrics ? calcDisproportionality(Vf, estSf) : undefined;
90429
+ const mMs = bAlternateMetrics ? estMeanMedianDifference(VfArray, Vf) : undefined;
90430
+ const mMd = bAlternateMetrics ? estMeanMedianDifference(VfArray) : undefined;
90431
+ const LO = bAlternateMetrics ? calcLopsidedOutcomes(VfArray) : undefined;
90432
+ // Calculate alternate responsiveness metrics for reference
90433
+ const bigR = bAlternateMetrics ? calcBigR(Vf, estSf) : undefined;
90434
+ const littleR = bAlternateMetrics ? estResponsiveness(Vf, inferredSVpoints) : undefined;
90435
+ const MIR = bAlternateMetrics ? calcMinimalInverseResponsiveness(Vf, littleR) : undefined;
90436
+ const rD = (!bConstrained || bAlternateMetrics) ? estResponsiveDistricts(VfArray) : undefined;
90437
+ const rDf = bAlternateMetrics ? estResponsiveDistrictsShare(rD, N) : undefined;
90438
+ const Cn = countCompetitiveDistricts(VfArray);
90439
+ const cD = bConstrained ? estCompetitiveDistricts(VfArray) : rD;
90440
+ const cDf = estCompetitiveDistrictsShare(cD, N);
90441
+ const Mrange = findMarginalDistricts(Vf, VfArray, N);
90442
+ const Md = estMarginalCompetitiveDistricts(Mrange, VfArray);
90443
+ const Mdf = estMarginalCompetitiveShare(Md, Mrange);
90444
+ const competitivenessScore = scoreCompetitiveness(Mdf, cDf);
90445
+ let biasScoring = {
90446
+ propS: propS,
90447
+ propSf: propSf,
90448
+ estS: estS,
90449
+ estSf: estSf,
90450
+ bias: bias,
90451
+ score: biasScore
89918
90452
  };
89919
- let competitiveScoring;
89920
- if (all) {
89921
- competitiveScoring = {
89922
- R: R,
89923
- Rd: Rd,
89924
- Rdf: Rdf,
89925
- Cd: Cd,
89926
- Cdf: Cdf,
89927
- Mdf: Mdf,
89928
- normalized: normalizedCompetitiveness
89929
- };
89930
- }
89931
- else {
89932
- competitiveScoring = {
89933
- Cd: Cd,
89934
- Cdf: Cdf,
89935
- Mdf: Mdf,
89936
- normalized: normalizedCompetitiveness
89937
- };
89938
- }
90453
+ const impactScoring = {
90454
+ unearnedS: unearnedS,
90455
+ score: impactScore
90456
+ };
90457
+ let competitiveScoring = {
90458
+ c: Cn,
90459
+ cD: cD,
90460
+ cDf: cDf,
90461
+ mRange: Mrange,
90462
+ mD: Md,
90463
+ mDf: Mdf,
90464
+ score: competitivenessScore
90465
+ };
90466
+ if (bAlternateMetrics) {
90467
+ biasScoring.tOf = TOf;
90468
+ biasScoring.fptpS = fptpS;
90469
+ biasScoring.s50V = Bs50f;
90470
+ biasScoring.v50S = Bv50f;
90471
+ biasScoring.decl = decl;
90472
+ biasScoring.gSym = gSym;
90473
+ biasScoring.eG = EG;
90474
+ biasScoring.sVg = BsGf;
90475
+ biasScoring.prop = prop;
90476
+ biasScoring.mMs = mMs;
90477
+ biasScoring.mMd = mMd;
90478
+ biasScoring.lO = LO;
90479
+ competitiveScoring.bigR = bigR;
90480
+ competitiveScoring.littleR = littleR;
90481
+ competitiveScoring.mIR = MIR;
90482
+ competitiveScoring.rD = rD;
90483
+ competitiveScoring.rDf = rDf;
90484
+ }
90485
+ // Weight bias, impact, and competitiveness into partisan scores to compare
90486
+ // plans across states and within a state.
90487
+ const bS = biasScoring.score;
90488
+ const iS = impactScoring.score;
90489
+ const cS = competitiveScoring.score;
90490
+ const acrossStatesPartisanScore = weightPartisan(bS, iS, cS, 0 /* AcrossStates */);
90491
+ const withinStatesPartisanScore = weightPartisan(bS, iS, cS, 1 /* WithinAState */);
89939
90492
  const s = {
89940
90493
  bias: biasScoring,
89941
- competitiveness: competitiveScoring
90494
+ impact: impactScoring,
90495
+ competitiveness: competitiveScoring,
90496
+ score: acrossStatesPartisanScore,
90497
+ score2: withinStatesPartisanScore
89942
90498
  };
89943
90499
  return s;
89944
90500
  }
89945
90501
  exports.scorePartisan = scorePartisan;
89946
- function weightPartisan(b, c) {
89947
- return Math.round(((S.BIAS_WEIGHT * b) + (S.COMPETITIVE_WEIGHT * c)) / (S.BIAS_WEIGHT + S.COMPETITIVE_WEIGHT));
90502
+ function weightPartisan(bS, iS, cS, context) {
90503
+ const bW = C.biasWeight(context);
90504
+ const iW = C.impactWeight(context);
90505
+ const cW = C.competitivenessWeight(context);
90506
+ const score = Math.round(((bS * bW) + (iS * iW) + (cS * cW)) / (bW + iW + cW));
90507
+ return score;
89948
90508
  }
89949
90509
  exports.weightPartisan = weightPartisan;
89950
- function printFairScorecardHeader() {
89951
- console.log('XX, Name, N, V%, ^S#, ^S%, F#, S#, S%, B%, B$, R#, Rd, Rd%, Cd, Cd%, Md%, C$, $$$');
89952
- }
89953
- exports.printFairScorecardHeader = printFairScorecardHeader;
89954
- function printFairScorecard(xx, name, N, Vf, s) {
89955
- console.log('%s, %s, %i, %f, %i, %f, %i, %f, %f, %f, %i, %f, %f, %f, %f, %f, %f, %f, %i', xx, name, N, Vf, s.bias.BestS, s.bias.BestSf, s.bias.FptpS, s.bias.ProbableS, s.bias.ProbableSf, s.bias.Bias, s.bias.normalized, s.competitiveness.R, s.competitiveness.Rd, s.competitiveness.Rdf, s.competitiveness.Cd, s.competitiveness.Cdf, s.competitiveness.Mdf, s.competitiveness.normalized, s.score);
90510
+ function extraBonus(Vf) {
90511
+ const okExtra = (0.5 - Vf) * (C.winnerBonus() - 1.0);
90512
+ return U.trim(okExtra);
89956
90513
  }
89957
- exports.printFairScorecard = printFairScorecard;
89958
- function scoreBias(rawBias, Vf, Sf, N) {
90514
+ exports.extraBonus = extraBonus;
90515
+ function scorebias(rawBias, Vf, Sf) {
89959
90516
  if (isAntimajoritarian(Vf, Sf)) {
89960
90517
  return 0;
89961
90518
  }
89962
90519
  else {
89963
- const _normalizer = new normalize_1.Normalizer(rawBias);
89964
- const oneSeatFraction = 1 / N;
89965
- const worst = Math.min(oneSeatFraction, S.BIAS_WORST);
89966
- const best = S.BIAS_BEST;
90520
+ // Adjust bias to incorporate an acceptable winner's bonus based on Vf
90521
+ const extra = extraBonus(Vf);
90522
+ const adjusted = adjustBias(Vf, rawBias, extra);
90523
+ // Then normalize
90524
+ const _normalizer = new normalize_1.Normalizer(adjusted);
90525
+ const worst = C.biasRange()[C.BEG];
90526
+ const best = C.biasRange()[C.END];
90527
+ _normalizer.positive();
89967
90528
  _normalizer.clip(worst, best);
89968
90529
  _normalizer.unitize(worst, best);
89969
90530
  _normalizer.invert();
89970
- // TODO - SCORE: Would also decaying raw values (e.g., by squaring them) be too
89971
- // much with a small range (like 10% or one seat)?
89972
- // _normalizer.decay();
89973
90531
  _normalizer.rescale();
89974
90532
  const score = _normalizer.normalizedNum;
89975
90533
  return score;
89976
90534
  }
89977
90535
  }
89978
- exports.scoreBias = scoreBias;
90536
+ exports.scorebias = scorebias;
90537
+ // Adjust bias to account for a winner's bonus
90538
+ function adjustBias(Vf, bias, extra) {
90539
+ if (Vf > 0.5)
90540
+ return Math.min(bias - extra, 0);
90541
+ else
90542
+ return Math.max(bias - extra, 0);
90543
+ }
90544
+ exports.adjustBias = adjustBias;
90545
+ // Normalize unearned seats
90546
+ function scoreImpact(rawUE, Vf, N) {
90547
+ // Adjust impact to incorporate an acceptable winner's bonus based on Vf
90548
+ const extra = extraBonus(Vf);
90549
+ const adjustedBias = adjustBias(Vf, rawUE / N, extra);
90550
+ const adjustedImpact = adjustedBias * N;
90551
+ // Then normalize
90552
+ const _normalizer = new normalize_1.Normalizer(adjustedImpact);
90553
+ const worst = C.unearnedThreshold();
90554
+ const best = 0.0;
90555
+ _normalizer.positive();
90556
+ _normalizer.clip(worst, best);
90557
+ _normalizer.unitize(worst, best);
90558
+ _normalizer.invert();
90559
+ _normalizer.rescale();
90560
+ const score = _normalizer.normalizedNum;
90561
+ return score;
90562
+ }
90563
+ exports.scoreImpact = scoreImpact;
89979
90564
  function isAntimajoritarian(Vf, Sf) {
89980
90565
  const bDem = ((Vf < 0.5) && (Sf > 0.5)) ? true : false;
89981
90566
  const bRep = (((1 - Vf) < 0.5) && ((1 - Sf) > 0.5)) ? true : false;
@@ -89987,16 +90572,24 @@ function scoreCompetitiveness(rawMarginal, rawOverall) {
89987
90572
  // But the practical max is more like 2/3's, so unitize that range to [0.0–1.0].
89988
90573
  // Then scale the values to [0–100].
89989
90574
  const _overall = new normalize_1.Normalizer(rawOverall);
89990
- _overall.clip(S.COMPETITIVE_WORST, S.COMPETITIVE_BEST);
89991
- _overall.unitize(S.COMPETITIVE_WORST, S.COMPETITIVE_BEST);
90575
+ let worst = C.overallCompetitivenessRange()[C.BEG];
90576
+ let best = C.overallCompetitivenessRange()[C.END];
90577
+ _overall.clip(worst, best);
90578
+ _overall.unitize(worst, best);
89992
90579
  _overall.rescale();
89993
- const overall = _overall.normalizedNum;
90580
+ const ocS = _overall.normalizedNum;
89994
90581
  // Normalize marginal competitiveness
89995
90582
  const _marginal = new normalize_1.Normalizer(rawMarginal);
90583
+ worst = C.marginalCompetitivenessRange()[C.BEG];
90584
+ best = C.marginalCompetitivenessRange()[C.END];
90585
+ _marginal.clip(worst, best);
90586
+ _marginal.unitize(worst, best);
89996
90587
  _marginal.rescale();
89997
- const marginal = _marginal.normalizedNum;
90588
+ const mcS = _marginal.normalizedNum;
90589
+ const mcW = C.marginalCompetitivenessWeight();
90590
+ const ocW = C.overallCompetitivenessWeight();
89998
90591
  // Then combine the results
89999
- const score = Math.round(((S.MARGINAL_WEIGHT * marginal) + (S.OVERALL_WEIGHT * overall)) / (S.MARGINAL_WEIGHT + S.OVERALL_WEIGHT));
90592
+ const score = Math.round(((mcW * mcS) + (ocW * ocS)) / (mcW + ocW));
90000
90593
  return score;
90001
90594
  }
90002
90595
  exports.scoreCompetitiveness = scoreCompetitiveness;
@@ -90006,30 +90599,56 @@ const { erf } = __webpack_require__(/*! mathjs */ "./node_modules/mathjs/main/es
90006
90599
  // console.log("erf(-0.5) =", erf(-0.5)); // returns -0.5204998778130465
90007
90600
  // console.log("erf(4) =", erf(4)); // returns 0.9999999845827421
90008
90601
  // Estimate the probability of a seat win for district, given a Vf
90009
- function estSeatProbability(Vf) {
90602
+ function estSeatProbability(Vf, range) {
90603
+ if (range) {
90604
+ // If a range is provided, it defines end points of a compressed probability
90605
+ // distribution. These *aren't* the points where races start or stop being
90606
+ // contested, just the end points of a distribution that yields the desired
90607
+ // probabilities in the typical competitive range [45-55%].
90608
+ const _normalizer = new normalize_1.Normalizer(Vf);
90609
+ const distBeg = range[C.BEG];
90610
+ const distEnd = range[C.END];
90611
+ _normalizer.clip(distBeg, distEnd);
90612
+ _normalizer.unitize(distBeg, distEnd);
90613
+ return seatProbabilityFn(_normalizer.wipNum);
90614
+ }
90615
+ else {
90616
+ // Otherwise, use the full probability distribution.
90617
+ return seatProbabilityFn(Vf);
90618
+ }
90619
+ }
90620
+ exports.estSeatProbability = estSeatProbability;
90621
+ function seatProbabilityFn(Vf) {
90010
90622
  // Python: 0.5 * (1 + erf((vpi - 0.50) / (0.02 * sqrt(8))))
90011
90623
  return U.trim(0.5 * (1.0 + erf((Vf - 0.50) / (0.02 * Math.sqrt(8)))));
90012
90624
  }
90013
- exports.estSeatProbability = estSeatProbability;
90014
90625
  // Estimate the number of responsive districts [R(d)], given a set of Vf's
90015
90626
  function estDistrictResponsiveness(Vf) {
90016
90627
  // Python: 1 - 4 * (est_seat_probability(vpi) - 0.5)**2
90017
90628
  return U.trim(1.0 - 4.0 * Math.pow((estSeatProbability(Vf) - 0.5), 2));
90018
90629
  }
90019
90630
  exports.estDistrictResponsiveness = estDistrictResponsiveness;
90020
- function inferSVpoints(Vf, VfArray) {
90631
+ function inferSVpoints(Vf, VfArray, shift, range) {
90021
90632
  const nDistricts = VfArray.length;
90022
90633
  let SVpoints = [];
90023
90634
  for (let shiftedVf of shiftRange()) {
90024
- const shiftedVPI = shiftDistricts(Vf, VfArray, shiftedVf);
90025
- const shiftedSf = estProbableSeats(shiftedVPI) / nDistricts;
90635
+ const shiftedVPI = shiftDistricts(Vf, VfArray, shiftedVf, shift);
90636
+ const shiftedSf = estSeats(shiftedVPI, range) / nDistricts;
90026
90637
  SVpoints.push({ v: shiftedVf, s: shiftedSf });
90638
+ // TODO - Why can't I trim these? Why does that only break the Hypotheticals?!?
90639
+ // SVpoints.push({v: U.trim(Number(shiftedVf)), s: shiftedSf});
90027
90640
  }
90028
90641
  return SVpoints;
90029
90642
  }
90030
90643
  exports.inferSVpoints = inferSVpoints;
90031
- // Shift districts *proportionally*
90032
- function shiftDistricts(Vf, VfArray, shiftedVf) {
90644
+ function shiftDistricts(Vf, VfArray, shiftedVf, shift) {
90645
+ if (shift == 0 /* Proportional */)
90646
+ return shiftProportionally(Vf, VfArray, shiftedVf);
90647
+ else
90648
+ return shiftUniformly(Vf, VfArray, shiftedVf);
90649
+ }
90650
+ // Shift districts proportionally
90651
+ function shiftProportionally(Vf, VfArray, shiftedVf) {
90033
90652
  let shiftedVfArray;
90034
90653
  if (shiftedVf < Vf) {
90035
90654
  // Shift down: D's to R's
@@ -90047,39 +90666,48 @@ function shiftDistricts(Vf, VfArray, shiftedVf) {
90047
90666
  }
90048
90667
  return shiftedVfArray;
90049
90668
  }
90050
- // Generate v's from 0.25 to 0.75 in 0.005 (1/2%) increments
90669
+ // Shift districts uniformly
90670
+ function shiftUniformly(Vf, VfArray, shiftedVf) {
90671
+ const shift = shiftedVf - Vf;
90672
+ const shiftedVfArray = VfArray.map((v => v + shift));
90673
+ return shiftedVfArray;
90674
+ }
90675
+ // Generate a range of v's in 1/2% increments
90051
90676
  function shiftRange() {
90052
- const range = [];
90053
- const lower = 25 / 100;
90054
- const upper = 75 / 100;
90677
+ const range = [0.25, 0.75];
90678
+ const axisRange = [];
90055
90679
  const step = (1 / 100) / 2;
90056
- for (let v = lower; v <= upper + S.EPSILON; v += step) {
90057
- range.push(v);
90680
+ for (let v = range[0]; v <= range[1] + S.EPSILON; v += step) {
90681
+ axisRange.push(v);
90058
90682
  }
90059
- return range;
90683
+ return axisRange;
90060
90684
  }
90061
- // ESTIMATE BIAS ("FAIR")
90685
+ // ESTIMATE BIAS
90686
+ //
90687
+ // NOTE: By convention, '+' = R bias; '-' = D bias.
90688
+ //
90062
90689
  // ^S# - The # of Democratic seats closest to proportional @ statewide Vf
90063
- function bestSeats(N, Vf) {
90064
- return Math.round(N * Vf);
90690
+ // The "expected number of seats" from http://bit.ly/2Fcuf4q
90691
+ function propSeats(N, Vf) {
90692
+ return Math.round((N * Vf) - S.EPSILON);
90065
90693
  }
90066
- exports.bestSeats = bestSeats;
90694
+ exports.propSeats = propSeats;
90067
90695
  // ^S% - The corresponding Democratic seat share
90068
- function bestSeatShare(bestS, N) {
90696
+ function propSeatShare(bestS, N) {
90069
90697
  return U.trim(bestS / N);
90070
90698
  }
90071
- exports.bestSeatShare = bestSeatShare;
90072
- // S# - The estimated # of Democratic seats @ statewide Vf, using seat probabilities
90073
- function estProbableSeats(VfArray) {
90699
+ exports.propSeatShare = propSeatShare;
90700
+ // S# - The estimated # of Democratic seats, using seat probabilities
90701
+ function estSeats(VfArray, range) {
90074
90702
  // Python: sum([est_seat_probability(vpi) for vpi in vpi_by_district])
90075
- return U.trim(U.sumArray(VfArray.map(v => estSeatProbability(v))));
90703
+ return U.trim(U.sumArray(VfArray.map(v => estSeatProbability(v, range))));
90076
90704
  }
90077
- exports.estProbableSeats = estProbableSeats;
90078
- // S% - The estimated Democratic seat share fraction @ statewide Vf
90079
- function estProbableSeatShare(probableS, N) {
90080
- return U.trim(probableS / N);
90705
+ exports.estSeats = estSeats;
90706
+ // S% - The estimated Democratic seat share fraction
90707
+ function estSeatShare(estS, N) {
90708
+ return U.trim(estS / N);
90081
90709
  }
90082
- exports.estProbableSeatShare = estProbableSeatShare;
90710
+ exports.estSeatShare = estSeatShare;
90083
90711
  // F# - The estimated number of Democratic seats using first past the post
90084
90712
  function estFPTPSeats(VfArray) {
90085
90713
  // Python: sum([1.0 for vpi in vpi_by_district if (vpi > 0.5)])
@@ -90093,27 +90721,33 @@ function estFPTPSeats(VfArray) {
90093
90721
  }));
90094
90722
  }
90095
90723
  exports.estFPTPSeats = estFPTPSeats;
90096
- // B% - Tthe bias calculated as S% ^S%
90097
- function estBias(estSeatShare, bestSeatShare) {
90098
- return U.trim(Math.abs(estSeatShare - bestSeatShare));
90724
+ // B% - The bias calculated as ^S% S%
90725
+ function estBias(estSf, propSf) {
90726
+ return U.trim(propSf - estSf);
90099
90727
  }
90100
90728
  exports.estBias = estBias;
90729
+ // UE# - The estimated # of unearned seats
90730
+ // UE_# from http://bit.ly/2Fcuf4q
90731
+ function estUnearnedSeats(proportional, probable) {
90732
+ return U.trim(proportional - probable);
90733
+ }
90734
+ exports.estUnearnedSeats = estUnearnedSeats;
90101
90735
  // ESTIMATE RESPONSIVENESS ("COMPETITIVE")
90102
90736
  // R# - Estimate responsiveness at the statewide vote share
90103
90737
  function estResponsiveness(Vf, inferredSVpoints) {
90104
- // Python:
90105
- // V1, S1 = lower_bracket(sv_curve_pts, statewide_vote_share, VOTE_SHARE)
90106
- // V2, S2 = upper_bracket(sv_curve_pts, statewide_vote_share, VOTE_SHARE)
90107
- // R = ((S2 - S1) / total_seats) / (V2 - V1)
90108
- // Note - Seat values are already fractions [0.0–1.0] here.
90109
- const lowerPt = findLowerBracket(Vf, inferredSVpoints);
90110
- const upperPt = findUpperBracket(Vf, inferredSVpoints);
90111
- const R = ((upperPt.s - lowerPt.s) / (upperPt.v - lowerPt.v));
90112
- return U.trim(R);
90738
+ let R = undefined;
90739
+ // NOTE - Seat values are already fractions [0.0–1.0] here.
90740
+ const lowerPt = findBracketingLowerVf(Vf, inferredSVpoints);
90741
+ const upperPt = findBracketingUpperVf(Vf, inferredSVpoints);
90742
+ if (!(U.areRoughlyEqual((upperPt.v - lowerPt.v), 0, S.EPSILON))) {
90743
+ R = ((upperPt.s - lowerPt.s) / (upperPt.v - lowerPt.v));
90744
+ R = U.trim(R);
90745
+ }
90746
+ return R;
90113
90747
  }
90114
90748
  exports.estResponsiveness = estResponsiveness;
90115
90749
  // Find the S(V) point that brackets a Vf value on the lower end
90116
- function findLowerBracket(Vf, inferredSVpoints) {
90750
+ function findBracketingLowerVf(Vf, inferredSVpoints) {
90117
90751
  let lowerPt = inferredSVpoints[0];
90118
90752
  let smallerPoints = [];
90119
90753
  for (let pt of inferredSVpoints) {
@@ -90129,7 +90763,7 @@ function findLowerBracket(Vf, inferredSVpoints) {
90129
90763
  return lowerPt;
90130
90764
  }
90131
90765
  // Find the S(V) point that brackets a Vf value on the upper end
90132
- function findUpperBracket(Vf, inferredSVpoints) {
90766
+ function findBracketingUpperVf(Vf, inferredSVpoints) {
90133
90767
  let upperPt = inferredSVpoints[-1];
90134
90768
  for (let pt of inferredSVpoints) {
90135
90769
  if (pt.v >= Vf) {
@@ -90140,19 +90774,56 @@ function findUpperBracket(Vf, inferredSVpoints) {
90140
90774
  }
90141
90775
  return upperPt;
90142
90776
  }
90143
- // Rd - Estimate the number of responsive districts, given a set of Vf's
90777
+ // The corresponding functions via the Sf y-axis (vs. Vf x-axis)
90778
+ // Find the S(V) point that brackets a Sf value on the lower end
90779
+ function findBracketingLowerSf(Sf, inferredSVpoints) {
90780
+ let lowerPt = inferredSVpoints[0];
90781
+ let smallerPoints = [];
90782
+ for (let pt of inferredSVpoints) {
90783
+ if (pt.s <= Sf) {
90784
+ smallerPoints.push(pt);
90785
+ }
90786
+ else {
90787
+ break;
90788
+ }
90789
+ }
90790
+ // The last smaller point
90791
+ lowerPt = smallerPoints.slice(-1)[0];
90792
+ return lowerPt;
90793
+ }
90794
+ // Find the S(V) point that brackets a Sf value on the upper end
90795
+ function findBracketingUpperSf(Sf, inferredSVpoints) {
90796
+ let upperPt = inferredSVpoints[-1];
90797
+ for (let pt of inferredSVpoints) {
90798
+ if (pt.s >= Sf) {
90799
+ // The first bigger point
90800
+ upperPt = { v: pt.v, s: pt.s };
90801
+ break;
90802
+ }
90803
+ }
90804
+ return upperPt;
90805
+ }
90806
+ // rD - Estimate the number of responsive districts, given a set of Vf's
90144
90807
  function estResponsiveDistricts(VfArray) {
90145
90808
  // Python: sum([est_district_responsiveness(vpi) for vpi in vpi_by_district])
90146
90809
  return U.trim(U.sumArray(VfArray.map(v => estDistrictResponsiveness(v))));
90147
90810
  }
90148
90811
  exports.estResponsiveDistricts = estResponsiveDistricts;
90149
- // Rd% - The estimated # of responsive districts, as a fraction of N
90150
- function estResponsiveDistrictsShare(Rd, N) {
90151
- return U.trim(Rd / N);
90812
+ // rD% - The estimated # of responsive districts, as a fraction of N
90813
+ function estResponsiveDistrictsShare(rD, N) {
90814
+ return U.trim(rD / N);
90152
90815
  }
90153
90816
  exports.estResponsiveDistrictsShare = estResponsiveDistrictsShare;
90154
90817
  // ESTIMATE COMPETITIVENESS (ENHANCED)
90155
- // Cd - The estimated # of competitive districts
90818
+ // C - Count the # of competitive districts, defined as v in [45–55%]
90819
+ function countCompetitiveDistricts(VfArray) {
90820
+ return U.trim(U.sumArray(VfArray.map(v => isCompetitive(v))));
90821
+ }
90822
+ exports.countCompetitiveDistricts = countCompetitiveDistricts;
90823
+ function isCompetitive(v) {
90824
+ return ((v >= C.competitiveRange()[C.BEG]) && (v <= C.competitiveRange()[C.END])) ? 1 : 0;
90825
+ }
90826
+ // cD - The estimated # of competitive districts
90156
90827
  function estCompetitiveDistricts(VfArray) {
90157
90828
  return U.trim(U.sumArray(VfArray.map(v => estDistrictCompetitiveness(v))));
90158
90829
  }
@@ -90160,21 +90831,48 @@ exports.estCompetitiveDistricts = estCompetitiveDistricts;
90160
90831
  // Re-scale a Democratic vote share to the competitive range (e.g., 45–55%).
90161
90832
  function estDistrictCompetitiveness(Vf) {
90162
90833
  const _normalizer = new normalize_1.Normalizer(Vf);
90163
- _normalizer.clip(S.MIN_CONTESTED, S.MAX_CONTESTED);
90164
- _normalizer.unitize(S.MIN_CONTESTED, S.MAX_CONTESTED);
90834
+ // The end points of a compressed probability distribution
90835
+ // NOTE - These aren't the points where races start or stop being contested,
90836
+ // just the end points of a distribution that yields the desired behavior
90837
+ // in the typical competitive range [45-55%].
90838
+ const distBeg = C.competitiveDistribution()[C.BEG];
90839
+ const distEnd = C.competitiveDistribution()[C.END];
90840
+ _normalizer.clip(distBeg, distEnd);
90841
+ _normalizer.unitize(distBeg, distEnd);
90165
90842
  const dC = estDistrictResponsiveness(_normalizer.wipNum);
90166
90843
  return dC;
90167
90844
  }
90168
90845
  exports.estDistrictCompetitiveness = estDistrictCompetitiveness;
90169
- // Cd% - The estimated # of competitive districts, as a fraction of N
90170
- function estCompetitiveDistrictsShare(Cd, N) {
90171
- return U.trim(Cd / N);
90846
+ // cD% - The estimated # of competitive districts, as a fraction of N
90847
+ function estCompetitiveDistrictsShare(cD, N) {
90848
+ return U.trim(cD / N);
90172
90849
  }
90173
90850
  exports.estCompetitiveDistrictsShare = estCompetitiveDistrictsShare;
90851
+ // Md - The estimated # of "marginal" districts in and around the likely FPTP
90852
+ // seats & the best seat split that are competitive.
90853
+ function estMarginalCompetitiveDistricts(Mrange, VfArray) {
90854
+ const minId = Mrange[C.BEG];
90855
+ const maxId = Mrange[C.END];
90856
+ // Sort the array values, and subset it to those districts
90857
+ // NOTE - I'm *not* keeping track of the district indexes right now
90858
+ let subsetVfArray = VfArray.sort().slice(minId - 1, maxId);
90859
+ // Est. competitive districts on that array
90860
+ const Md = U.sumArray(subsetVfArray.map(v => estDistrictCompetitiveness(v)));
90861
+ return U.trim(Md);
90862
+ }
90863
+ exports.estMarginalCompetitiveDistricts = estMarginalCompetitiveDistricts;
90174
90864
  // Md% - The estimated competitiveness of the "marginal" districts in and around
90175
90865
  // the likely FPTP seats & the best seat split as a fraction
90176
- function estMarginalCompetitiveShare(Vf, VfArray, N) {
90177
- const bestS = bestSeats(N, Vf);
90866
+ function estMarginalCompetitiveShare(Md, Mrange) {
90867
+ const minId = Mrange[C.BEG];
90868
+ const maxId = Mrange[C.END];
90869
+ // Est. competitive district share on that result
90870
+ const MdShare = U.trim(estCompetitiveDistrictsShare(Md, maxId - minId + 1));
90871
+ return U.trim(MdShare);
90872
+ }
90873
+ exports.estMarginalCompetitiveShare = estMarginalCompetitiveShare;
90874
+ function findMarginalDistricts(Vf, VfArray, N) {
90875
+ const bestS = propSeats(N, Vf);
90178
90876
  const fptpS = estFPTPSeats(VfArray);
90179
90877
  // Find the marginal districts IDs (indexed 1–N)
90180
90878
  let minId;
@@ -90192,137 +90890,331 @@ function estMarginalCompetitiveShare(Vf, VfArray, N) {
90192
90890
  minId = Math.max((N - bestS) - 1, 1);
90193
90891
  maxId = Math.min((N - bestS) + 1, N);
90194
90892
  }
90195
- const deltaS = maxId - minId + 1;
90196
- // Sort the array values, and subset it to those districts
90197
- // NOTE - I'm *not* keeping track of the district indexes right now
90198
- let subsetVfArray = VfArray.sort().slice(minId - 1, maxId);
90199
- // Est. competitive districts on that array
90200
- const Md = U.sumArray(subsetVfArray.map(v => estDistrictCompetitiveness(v)));
90201
- // Est. competitive district share on that result
90202
- const MdShare = U.trim(estCompetitiveDistrictsShare(Md, deltaS));
90203
- return U.trim(MdShare);
90893
+ return [minId, maxId];
90204
90894
  }
90205
- exports.estMarginalCompetitiveShare = estMarginalCompetitiveShare;
90206
-
90207
-
90208
- /***/ }),
90209
-
90210
- /***/ "./src/index.ts":
90211
- /*!**********************!*\
90212
- !*** ./src/index.ts ***!
90213
- \**********************/
90214
- /*! no static exports found */
90215
- /***/ (function(module, exports, __webpack_require__) {
90216
-
90217
- "use strict";
90218
-
90219
- //
90220
- // SCORE PACKAGE API
90221
- //
90222
- function __export(m) {
90223
- for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
90895
+ exports.findMarginalDistricts = findMarginalDistricts;
90896
+ // ADVANCED/ALTERNATE METRICS FOR REFERENCE
90897
+ function calcTurnoutBias(statewideVf, VfArray) {
90898
+ const districtAvg = U.avgArray(VfArray);
90899
+ const turnoutBias = statewideVf - districtAvg;
90900
+ return U.trim(turnoutBias);
90224
90901
  }
90225
- Object.defineProperty(exports, "__esModule", { value: true });
90226
- __export(__webpack_require__(/*! ./_api */ "./src/_api.ts"));
90227
- var types_1 = __webpack_require__(/*! ./types */ "./src/types.ts");
90228
- exports.sampleProfile = types_1.sampleProfile;
90229
- exports.sampleScorecard = types_1.sampleScorecard;
90230
-
90231
-
90232
- /***/ }),
90233
-
90234
- /***/ "./src/normalize.ts":
90235
- /*!**************************!*\
90236
- !*** ./src/normalize.ts ***!
90237
- \**************************/
90238
- /*! no static exports found */
90239
- /***/ (function(module, exports, __webpack_require__) {
90240
-
90241
- "use strict";
90242
-
90243
- //
90244
- // NORMALIZATION UTILITIES
90902
+ exports.calcTurnoutBias = calcTurnoutBias;
90903
+ // PARTISAN BIAS - I'm using John Nagle's simple seat bias below, which is what
90904
+ // PlanScore is doing:
90245
90905
  //
90246
- var __importStar = (this && this.__importStar) || function (mod) {
90247
- if (mod && mod.__esModule) return mod;
90248
- var result = {};
90249
- if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
90250
- result["default"] = mod;
90251
- return result;
90252
- };
90253
- Object.defineProperty(exports, "__esModule", { value: true });
90254
- const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
90255
- class Normalizer {
90256
- constructor(rawValue) {
90257
- this.rawNum = rawValue;
90258
- this.wipNum = this.rawNum;
90259
- }
90260
- // *Don't* transform the input.
90261
- identity() {
90262
- return this.wipNum;
90263
- }
90264
- // TODO - Do I need this? If so, add TEST cases.
90265
- positive() {
90266
- this.wipNum = Math.abs(this.wipNum);
90267
- return this.wipNum;
90268
- }
90269
- // Invert a value in the unit range [0.0–1.0] (so that bigger is better).
90270
- invert() {
90271
- // TODO - DAVID: Can we make this throw an Error and expect that in unit test?
90272
- console.assert(((this.wipNum >= 0.0) && (this.wipNum <= 1.0)), S.OUT_OF_RANGE_MSG);
90273
- this.wipNum = 1.0 - this.wipNum;
90274
- return this.wipNum;
90906
+ // "Partisan bias is the difference between each party’s seat share and 50 %
90907
+ // in a hypothetical, perfectly tied election.For example, if a party would
90908
+ // win 55 % of a plan’s districts if it received 50 % of the statewide vote,
90909
+ // then the plan would have a bias of 5 % in this party’s favor.To calculate
90910
+ // partisan bias, the observed vote share in each district is shifted by the
90911
+ // amount necessary to simulate a tied statewide election.Each party’s seat
90912
+ // share in this hypothetical election is then determined. The difference
90913
+ // between each party’s seat share and 50 % is partisan bias."
90914
+ //
90915
+ // This is *not* King's & others' geometric partisan bias metric per se.
90916
+ // That is below.
90917
+ function estPartisanBias(inferredSVpoints, nDistricts) {
90918
+ return estSeatBias(inferredSVpoints, nDistricts);
90919
+ }
90920
+ exports.estPartisanBias = estPartisanBias;
90921
+ // SEATS BIAS -- John Nagle's simple seat bias @ 50% (alpha), a fractional # of seats.
90922
+ function estSeatBias(inferredSVpoints, nDistricts) {
90923
+ const half = 0.5;
90924
+ const tolerance = 0.001;
90925
+ let dSeats = 0;
90926
+ for (let pt of inferredSVpoints) {
90927
+ if (U.areRoughlyEqual(pt.v, half, tolerance)) {
90928
+ dSeats = pt.s * nDistricts;
90929
+ break;
90930
+ }
90275
90931
  }
90276
- // Constrain the value to be within a specified range.
90277
- clip(begin_range, end_range) {
90278
- // Handle the ends of the range being given either order
90279
- const min_range = Math.min(begin_range, end_range);
90280
- const max_range = Math.max(begin_range, end_range);
90281
- this.wipNum = Math.max(Math.min(this.wipNum, max_range), min_range);
90282
- return this.wipNum;
90932
+ const rSeats = nDistricts - dSeats;
90933
+ const Bs = (rSeats - dSeats) / 2.0;
90934
+ // NOTE - That is the same as (N/2) - S(0.5).
90935
+ // const BsAlt = (nDistricts / 2.0) - dSeats;
90936
+ return U.trim(Bs);
90937
+ }
90938
+ exports.estSeatBias = estSeatBias;
90939
+ // VOTES BIAS -- John Nagle's simple vote bias @ 50% (alpha2), a percentage.
90940
+ function estVotesBias(inferredSVpoints, nDistricts) {
90941
+ let extraVf = 0.0;
90942
+ // Interpolate the extra Vf required @ Sf = 0.5
90943
+ const lowerPt = findBracketingLowerSf(0.5, inferredSVpoints);
90944
+ const upperPt = findBracketingUpperSf(0.5, inferredSVpoints);
90945
+ if ((upperPt.s - lowerPt.s) != 0) {
90946
+ const ratio = (upperPt.v - lowerPt.v) / (upperPt.s - lowerPt.s);
90947
+ const deltaS = 0.5 - lowerPt.s;
90948
+ extraVf = lowerPt.v + (ratio * deltaS) - 0.5;
90949
+ }
90950
+ extraVf = U.trim(extraVf);
90951
+ return extraVf;
90952
+ }
90953
+ exports.estVotesBias = estVotesBias;
90954
+ // GEOMETRIC SEATS BIAS (@ V = statewide vote share)
90955
+ function estGeometricSeatsBias(Vf, inferredSVpoints) {
90956
+ const bgsSVpoints = inferGeometricSeatsBiasPoints(inferredSVpoints);
90957
+ // Interpolate the seat fraction @ Vf
90958
+ const lowerPt = findBracketingLowerVf(Vf, bgsSVpoints);
90959
+ const upperPt = findBracketingUpperVf(Vf, bgsSVpoints);
90960
+ const ratio = (upperPt.s - lowerPt.s) / (upperPt.v - lowerPt.v);
90961
+ const deltaV = Vf - lowerPt.v;
90962
+ const deltaS = ratio * deltaV;
90963
+ const BsGf = lowerPt.s + deltaS;
90964
+ return U.trim(BsGf);
90965
+ }
90966
+ exports.estGeometricSeatsBias = estGeometricSeatsBias;
90967
+ function inferGeometricSeatsBiasPoints(inferredSVpoints) {
90968
+ const nPoints = inferredSVpoints.length;
90969
+ const inverseSVpoints = invertSVPoints(inferredSVpoints);
90970
+ let bgsSVpoints = [];
90971
+ for (let i = 0; i < nPoints; i++) {
90972
+ const Vf = inferredSVpoints[i].v;
90973
+ const sD = inferredSVpoints[i].s;
90974
+ const sR = inverseSVpoints[i].s;
90975
+ const BsGf = 0.5 * (sR - sD);
90976
+ bgsSVpoints.push({ v: Vf, s: BsGf });
90977
+ }
90978
+ return bgsSVpoints;
90979
+ }
90980
+ exports.inferGeometricSeatsBiasPoints = inferGeometricSeatsBiasPoints;
90981
+ function invertSVPoints(inferredSVpoints) {
90982
+ let invertedSVpoints = [];
90983
+ for (let pt of inferredSVpoints) {
90984
+ const Vd = pt.v;
90985
+ const Sd = pt.s;
90986
+ const Vr = U.trim(1.0 - Vd);
90987
+ const Sr = 1.0 - Sd;
90988
+ invertedSVpoints.push({ v: Vr, s: Sr });
90989
+ }
90990
+ invertedSVpoints.sort(function (a, b) {
90991
+ return a.v - b.v;
90992
+ });
90993
+ return invertedSVpoints;
90994
+ }
90995
+ exports.invertSVPoints = invertSVPoints;
90996
+ // EFFICIENCY GAP -- note the formulation used. Also, to accommodate turnout bias,
90997
+ // we would need to have D & R votes, not just shares.
90998
+ function calcEfficiencyGap(Vf, Sf, shareType = 0 /* Democratic */) {
90999
+ let efficiencyGap;
91000
+ if (shareType == 1 /* Republican */) {
91001
+ // NOTE - This is the common formulation:
91002
+ //
91003
+ // EG = (Sf – 0.5) – (2 × (Vf – 0.5))
91004
+ //
91005
+ // in which it is implied that '-' = R bias; '+' = D bias.
91006
+ efficiencyGap = U.trim((Sf - 0.5) - (2.0 * (Vf - 0.5)));
90283
91007
  }
90284
- // Recast the value as the delta from a baseline value. NOOP if it is zero.
90285
- // NOTE - Values can be + or -.
90286
- rebase(base) {
90287
- this.wipNum = this.wipNum - base;
90288
- return this.wipNum;
91008
+ else {
91009
+ // NOTE - This is the alternate formulation in which '+' = R bias; '-' = D bias,
91010
+ // which is consistent with all our other metrics.
91011
+ efficiencyGap = U.trim((2.0 * (Vf - 0.5)) - (Sf - 0.5));
90289
91012
  }
90290
- // Re-scale a value into the [0.0 – 1.0] range, using a given range.
90291
- // NOTE - This assumes that values have alrady been clipped into the range.
90292
- unitize(begin_range, end_range) {
90293
- const min_range = Math.min(begin_range, end_range);
90294
- const max_range = Math.max(begin_range, end_range);
90295
- // TODO - DAVID: Can we make this throw an Error and expect that in unit test?
90296
- console.assert(((this.wipNum >= min_range) && (this.wipNum <= max_range)), S.OUT_OF_RANGE_MSG);
90297
- const ranged = this.wipNum - min_range;
90298
- this.wipNum = Math.abs(ranged / (end_range - begin_range));
90299
- return this.wipNum;
91013
+ return U.trim(efficiencyGap);
91014
+ }
91015
+ exports.calcEfficiencyGap = calcEfficiencyGap;
91016
+ // MEAN–MEDIAN DIFFERENCE
91017
+ //
91018
+ // From PlanScore.org: "The mean-median difference is a party’s median vote share
91019
+ // minus its mean vote share, across all of a plan’s districts. For example, if
91020
+ // a party has a median vote share of 45 % and a mean vote share of 50 %, then
91021
+ // the plan has a mean - median difference of 5 % against this party. When the
91022
+ // mean and the median diverge significantly, the district distribution is skewed
91023
+ // in favor of one party and against its opponent. Conversely, when the mean and
91024
+ // the median are close, the district distribution is more symmetric."
91025
+ //
91026
+ // From Princeton Gerrymandering Project: "The mean-median difference is calculated
91027
+ // by subtracting the average vote share of either party across all districts from
91028
+ // the median vote share of the same party across all districts. A negative mean -
91029
+ // median difference indicates that the examined party has an advantage; a positive
91030
+ // difference indicates that the examined party is disadvantaged."
91031
+ //
91032
+ // So:
91033
+ // * With D VPI, '+' = R bias; '-' = D bias <<< We're using this convention.
91034
+ // * With R VPI, '-' = R bias; '+' = D bias.
91035
+ function estMeanMedianDifference(VfArray, Vf) {
91036
+ const meanVf = Vf ? Vf : U.avgArray(VfArray);
91037
+ const medianVf = U.medianArray(VfArray);
91038
+ // NOTE - Switched order to get the signs correct
91039
+ const difference = meanVf - medianVf;
91040
+ // const difference: number = medianVf - meanVf;
91041
+ return U.trim(difference);
91042
+ }
91043
+ exports.estMeanMedianDifference = estMeanMedianDifference;
91044
+ // HELPERS FOR DECLINATION & LOPSIDED OUTCOMES
91045
+ // Key r(v) points, defined in Fig. 19:
91046
+ // * VfArray are Democratic seat shares (by convention).
91047
+ // * But the x-axis of r(v) graphs us Republican seat share.
91048
+ // * So, you have to invert the D/R axis for Vb; and
91049
+ // * Invert the D/R probabilities for Va.
91050
+ function keyRVpoints(VfArray) {
91051
+ const nDistricts = VfArray.length;
91052
+ const estS = estSeats(VfArray);
91053
+ const Sb = estSeatShare(estS, nDistricts);
91054
+ // TODO - Understand why the corresponding V to Sb is always @ 0.5.
91055
+ // John Nagle: "This is the dividing vote for party A vs party B wins defined
91056
+ // by Warrington.My modification just puts fractions of districts to the each side."
91057
+ const Rb = Sb / 2;
91058
+ const Ra = (1 + Sb) / 2;
91059
+ const Vb = 1.0 - (U.sumArray(VfArray.map(v => estSeatProbability(v) * v))) / estS;
91060
+ const Va = (U.sumArray(VfArray.map(v => estSeatProbability(1 - v) * (1 - v)))) / (nDistricts - estS);
91061
+ const keyPoints = {
91062
+ Sb: Sb,
91063
+ Ra: Ra,
91064
+ Rb: Rb,
91065
+ Va: Va,
91066
+ Vb: Vb
91067
+ };
91068
+ return keyPoints;
91069
+ }
91070
+ exports.keyRVpoints = keyRVpoints;
91071
+ function isASweep(Sf, nDistricts) {
91072
+ const oneDistrict = 1 / nDistricts;
91073
+ const bSweep = ((Sf > (1 - oneDistrict)) || (Sf < oneDistrict)) ? true : false;
91074
+ return bSweep;
91075
+ }
91076
+ exports.isASweep = isASweep;
91077
+ function radiansToDegrees(radians) {
91078
+ const degrees = radians * (180 / Math.PI);
91079
+ return degrees;
91080
+ }
91081
+ exports.radiansToDegrees = radiansToDegrees;
91082
+ // DECLINATION
91083
+ //
91084
+ // Declination is calculated using the key r(v) points, defined in Fig. 19.
91085
+ // Note that district vote shares are D shares, so party A = Rep & B = Dem.
91086
+ function calcDeclination(VfArray) {
91087
+ const { Sb, Ra, Rb, Va, Vb } = keyRVpoints(VfArray);
91088
+ const bSweep = isASweep(Sb, VfArray.length);
91089
+ const bTooFewDistricts = (VfArray.length < 5) ? true : false;
91090
+ const bVaAt50 = (U.areRoughlyEqual((Va - 0.5), 0.0, S.EPSILON)) ? true : false;
91091
+ const bVbAt50 = (U.areRoughlyEqual((0.5 - Vb), 0.0, S.EPSILON)) ? true : false;
91092
+ let decl;
91093
+ if (bSweep || bTooFewDistricts || bVaAt50 || bVbAt50) {
91094
+ decl = undefined;
90300
91095
  }
90301
- // Decay a value in the unit range [0.0–1.0] by its distance from zero.
90302
- // NOTE - If the range is already such that "bigger is better," then the closer
90303
- // the value is to 1.0 (the best) the *less* it will decay.
90304
- decay(fn = 0 /* Gravity */) {
90305
- // TODO - DAVID: Can we make this throw an Error and expect that in unit test?
90306
- console.assert(((this.wipNum >= 0.0) && (this.wipNum <= 1.0)), S.OUT_OF_RANGE_MSG);
90307
- switch (fn) {
90308
- case 0 /* Gravity */: {
90309
- this.wipNum = Math.pow(this.wipNum, S.DISTANCE_WEIGHT);
90310
- return this.wipNum;
90311
- }
90312
- default: {
90313
- throw new Error("Decay function not recognized.");
90314
- }
90315
- }
91096
+ else {
91097
+ const lTan = (Sb - Rb) / (0.5 - Vb);
91098
+ const rTan = (Ra - Sb) / (Va - 0.5);
91099
+ const lAngle = radiansToDegrees(Math.atan(lTan));
91100
+ const rAngle = radiansToDegrees(Math.atan(rTan));
91101
+ decl = rAngle - lAngle;
91102
+ decl = U.trim(decl);
90316
91103
  }
90317
- // Translate a value in the unit range to the user-friendly range [0 – 100].
90318
- rescale() {
90319
- // TODO - DAVID: Can we make this throw an Error and expect that in unit test?
90320
- console.assert(((this.wipNum >= 0.0) && (this.wipNum <= 1.0)), S.OUT_OF_RANGE_MSG);
90321
- this.normalizedNum = Math.round(this.wipNum * S.NORMALIZED_RANGE);
90322
- return this.normalizedNum;
91104
+ return decl;
91105
+ }
91106
+ exports.calcDeclination = calcDeclination;
91107
+ // LOPSIDED OUTCOMES
91108
+ //
91109
+ // This is a measure of packing bias is:
91110
+ //
91111
+ // LO = (1⁄2 - vB) - (vA – 1⁄2) Eq. 5.4.1 on P. 26
91112
+ //
91113
+ // "The ideal for this measure is that the excess vote share for districts
91114
+ // won by party A averaged over those districts equals the excess vote share
91115
+ // for districts won by party B averaged over those districts.
91116
+ // A positive value of LO indicates greater packing of party B voters and,
91117
+ // therefore, indicates a bias in favor of party A."
91118
+ function calcLopsidedOutcomes(VfArray) {
91119
+ const { Sb, Ra, Rb, Va, Vb } = keyRVpoints(VfArray);
91120
+ const bSweep = isASweep(Sb, VfArray.length);
91121
+ let LO;
91122
+ if (bSweep) {
91123
+ LO = undefined;
90323
91124
  }
91125
+ else {
91126
+ LO = (0.5 - Vb) - (Va - 0.5);
91127
+ LO = U.trim(LO);
91128
+ }
91129
+ return LO;
91130
+ }
91131
+ exports.calcLopsidedOutcomes = calcLopsidedOutcomes;
91132
+ // TODO - Add unit tests <<< Depends on S(V)
91133
+ // GLOBAL SYMMETRY - Fig. 17 in Section 5.1
91134
+ function calcGlobalSymmetry(inferredSVpoints, S50V) {
91135
+ const invertedSVpoints = invertSVPoints(inferredSVpoints);
91136
+ let gSym = 0.0;
91137
+ for (let i in inferredSVpoints) {
91138
+ gSym += Math.abs(inferredSVpoints[i].s - invertedSVpoints[i].s) / 2;
91139
+ }
91140
+ const sign = (S50V < 0) ? -1 : 1;
91141
+ gSym *= sign;
91142
+ return U.trim(gSym);
91143
+ }
91144
+ exports.calcGlobalSymmetry = calcGlobalSymmetry;
91145
+ // RAW DISPROPORTIONALITY
91146
+ //
91147
+ // gamma = Sf – Vf : Eq.C.1.1 on P. 42
91148
+ function calcDisproportionality(Vf, Sf) {
91149
+ const gamma = Vf - Sf;
91150
+ // const gamma = Sf - Vf;
91151
+ return U.trim(gamma);
91152
+ }
91153
+ exports.calcDisproportionality = calcDisproportionality;
91154
+ // TODO - Add unit tests <<< Depends on S(V)
91155
+ // BIG 'R': Defined in Footnote 22 on P. 10
91156
+ function calcBigR(Vf, Sf) {
91157
+ let bigR = undefined;
91158
+ if (!(U.areRoughlyEqual(Vf, 0.5, S.EPSILON))) {
91159
+ bigR = (Sf - 0.5) / (Vf - 0.5);
91160
+ bigR = U.trim(bigR);
91161
+ }
91162
+ return bigR;
91163
+ }
91164
+ exports.calcBigR = calcBigR;
91165
+ // TODO - Add unit tests <<< Depends on S(V)
91166
+ // MINIMAL INVERSE RESPONSIVENESS
91167
+ //
91168
+ // zeta = (1 / r) - (1 / r_sub_max) : Eq. 5.2.1
91169
+ //
91170
+ // where r_sub_max = 10 or 20 for balanced and unbalanced states, respectively.
91171
+ function calcMinimalInverseResponsiveness(Vf, r) {
91172
+ let MIR = undefined;
91173
+ if (!(U.areRoughlyEqual(r, 0, S.EPSILON))) {
91174
+ const bBalanced = isBalanced(Vf);
91175
+ const ideal = bBalanced ? 0.1 : 0.2;
91176
+ MIR = (1 / r) - ideal;
91177
+ MIR = U.trim(MIR);
91178
+ }
91179
+ return MIR;
91180
+ }
91181
+ exports.calcMinimalInverseResponsiveness = calcMinimalInverseResponsiveness;
91182
+ function isBalanced(Vf) {
91183
+ const [lower, upper] = C.competitiveRange();
91184
+ const bBalanced = ((Vf > upper) || (Vf < lower)) ? false : true;
91185
+ return bBalanced;
90324
91186
  }
90325
- exports.Normalizer = Normalizer;
91187
+ // HELPERS
91188
+ function printPartisanScorecardHeader() {
91189
+ console.log('XX, Name, N, V%, ^S#, S#, B%, B$, UE#, I$, C#, Cd, Md, C$, <P$');
91190
+ }
91191
+ exports.printPartisanScorecardHeader = printPartisanScorecardHeader;
91192
+ function printPartisanScorecardRow(xx, name, N, Vf, s) {
91193
+ console.log('%s, %s, %i, %f, %i, %f, %f, %i, %f, %i, %i, %f, %f, %i, %i', xx, // 1
91194
+ name, // 2
91195
+ N, // 3
91196
+ Vf, // 4
91197
+ s.bias.propS, // 5
91198
+ s.bias.estS, // 6
91199
+ s.bias.bias, // 7
91200
+ s.bias.score, s.impact.unearnedS, // 9
91201
+ s.impact.score, s.competitiveness.c, // 11
91202
+ s.competitiveness.cD, s.competitiveness.mD, s.competitiveness.score, s.score // 15
91203
+ );
91204
+ }
91205
+ exports.printPartisanScorecardRow = printPartisanScorecardRow;
91206
+ // Generate partisan details (Table 1)
91207
+ function printPartisanDetailsHeader() {
91208
+ console.log('XX, <V>, S(<V>), S50V, V50S, Decl, B_G, EG, Beta, l-gamma, mM, TO, mM\', LO, R, r, Zeta');
91209
+ }
91210
+ exports.printPartisanDetailsHeader = printPartisanDetailsHeader;
91211
+ function printPartisanDetailsRow(xx, name, N, Vf, s) {
91212
+ console.log('%s, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f', xx, Vf, s.bias.estSf, s.bias.s50V, s.bias.v50S, s.bias.decl, s.bias.gSym, s.bias.eG, s.bias.sVg, // Beta
91213
+ s.bias.prop, // Lower-gamma
91214
+ s.bias.mMs, s.bias.tOf, s.bias.mMd, s.bias.lO, s.competitiveness.bigR, s.competitiveness.littleR, s.competitiveness.mIR // Zeta
91215
+ );
91216
+ }
91217
+ exports.printPartisanDetailsRow = printPartisanDetailsRow;
90326
91218
 
90327
91219
 
90328
91220
  /***/ }),
@@ -90348,73 +91240,129 @@ var __importStar = (this && this.__importStar) || function (mod) {
90348
91240
  };
90349
91241
  Object.defineProperty(exports, "__esModule", { value: true });
90350
91242
  const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
91243
+ const C = __importStar(__webpack_require__(/*! ./config */ "./src/config.ts"));
90351
91244
  const compact_1 = __webpack_require__(/*! ./compact */ "./src/compact.ts");
90352
91245
  const cohesive_1 = __webpack_require__(/*! ./cohesive */ "./src/cohesive.ts");
90353
91246
  const equal_1 = __webpack_require__(/*! ./equal */ "./src/equal.ts");
90354
- const fair_1 = __webpack_require__(/*! ./fair */ "./src/fair.ts");
90355
- // TODO - Score opportunity for minority representation
91247
+ const partisan_1 = __webpack_require__(/*! ./partisan */ "./src/partisan.ts");
91248
+ const minority_1 = __webpack_require__(/*! ./minority */ "./src/minority.ts");
90356
91249
  function scorePlan(p, overridesJSON) {
90357
- // BEST subcategories - compactness, splitting, population deviation
91250
+ // TRADITIONAL DISTRICTING PRINCIPLES ("best") subcategories - compactness, splitting, population deviation
90358
91251
  // Compactness
90359
- const reockM = compact_1.doReock(p.compactnessProfile.GeometryByDistrict);
90360
- const polsbyM = compact_1.doPolsbyPopper(p.compactnessProfile.GeometryByDistrict);
90361
- const compactnessScore = Math.round(((S.REOCK_WEIGHT * reockM.normalized) + (S.POLSBY_WEIGHT * polsbyM.normalized)) / (S.REOCK_WEIGHT + S.POLSBY_WEIGHT));
91252
+ const reockM = compact_1.doReock(p.compactnessProfile.geometryByDistrict);
91253
+ const polsbyM = compact_1.doPolsbyPopper(p.compactnessProfile.geometryByDistrict);
91254
+ const compactnessScore = weightCompactness(reockM.normalized, polsbyM.normalized);
90362
91255
  const cS = {
90363
91256
  score: compactnessScore,
90364
91257
  reock: reockM,
90365
91258
  polsby: polsbyM
90366
91259
  };
90367
91260
  // Splitting
90368
- const CxD = p.splittingProfile.CountyPopByDistrict;
91261
+ const CxD = p.splittingProfile.countyPopByDistrict;
90369
91262
  const dT = cohesive_1.totalDistricts(CxD);
90370
91263
  const cT = cohesive_1.totalCounties(CxD);
90371
91264
  const countyM = cohesive_1.doCountySplittingReduced(CxD, dT, cT);
90372
91265
  const districtM = cohesive_1.doDistrictSplittingReduced(CxD, dT, cT);
90373
- const splittingScore = Math.round(((S.COUNTY_WEIGHT * countyM.normalized) + (S.DISTANCE_WEIGHT * districtM.normalized)) / (S.COUNTY_WEIGHT + S.DISTANCE_WEIGHT));
91266
+ const splittingScore = weightSplitting(countyM.normalized, districtM.normalized);
90374
91267
  const sS = {
90375
91268
  score: splittingScore,
90376
91269
  county: countyM,
90377
91270
  district: districtM
90378
91271
  };
90379
91272
  // Population deviation
90380
- const eS = equal_1.doPopulationDeviation(p.populationProfile.TotalPopByDistrict, p.populationProfile.targetSize, p.legislativeDistricts);
90381
- // Combine compactness, splitting, & population deviation into a "best" score
90382
- const bestScore = Math.round(((S.COMPACTNESS_WEIGHT * cS.score) + (S.SPLITTING_WEIGHT * sS.score) + (S.POPDEV_WEIGHT * eS.normalized)) / (S.COMPACTNESS_WEIGHT + S.SPLITTING_WEIGHT + S.POPDEV_WEIGHT));
90383
- // Populate the "best" scorecard
90384
- const bS = {
90385
- BestScore: bestScore,
91273
+ const pdS = equal_1.doPopulationDeviation(p.populationProfile.totalPopByDistrict, p.populationProfile.targetSize, p.legislativeDistricts);
91274
+ // Combine traditional principles into a score to compare plans w/in a state
91275
+ const tpScore = weightTradtionalPrinciples(cS.score, sS.score, pdS.normalized, 1 /* WithinAState */);
91276
+ // Populate the "best" traditional principles scorecard
91277
+ const tpS = {
91278
+ score: tpScore,
90386
91279
  compactness: cS,
90387
91280
  splitting: sS,
90388
- populationDeviation: eS
91281
+ populationDeviation: pdS
91282
+ };
91283
+ // PARTISAN ("fair") subcategories - bias, impact, & competitiveness (plus lots of supporting measures)
91284
+ const options = {
91285
+ alternates: true,
91286
+ constrained: true,
91287
+ shift: 0 /* Proportional */
90389
91288
  };
90390
- // FAIR subcategories - bias & competitiveness
90391
- // NOTE - This calculates a # of measures of bias & competitiveness.
90392
- const pS = fair_1.scorePartisan(p.partisanProfile.statewideVf, p.partisanProfile.VfArray);
90393
- pS.score = fair_1.weightPartisan(pS.bias.normalized, pS.competitiveness.normalized);
90394
- // Combine "best" and "fair" scores into an overall score for the plan
90395
- const score = Math.round(((S.FAIR_WEIGHT * pS.score) + (S.BEST_WEIGHT * bS.BestScore)) / (S.FAIR_WEIGHT + S.BEST_WEIGHT));
91289
+ const pS = partisan_1.scorePartisan(p.partisanProfile.statewideVf, p.partisanProfile.vfArray, options);
91290
+ // Combine the partisan/partisan & traditional principles/best scores into an overall
91291
+ // score for comparing plans w/in a state
91292
+ let score = weightOverall(pS.score2, tpS.score, 1 /* WithinAState */);
91293
+ const mS = minority_1.evalMinorityOpportunity(p);
91294
+ // Add minority bonus, keeping score it to the range [0–100]
91295
+ score = mixinMinorityBonus(score, mS.score);
90396
91296
  // Roll up an overall scorecard
90397
91297
  const scorecard = {
90398
- score: score,
90399
- best: bS,
90400
- fair: pS
91298
+ partisan: pS,
91299
+ minority: mS,
91300
+ traditionalPrinciples: tpS,
91301
+ score: score
90401
91302
  };
90402
91303
  return scorecard;
90403
91304
  }
90404
91305
  exports.scorePlan = scorePlan;
91306
+ // HELPERS
91307
+ function weightCompactness(rS, ppS) {
91308
+ const rW = C.reockWeight();
91309
+ const ppW = C.polsbyWeight();
91310
+ const score = Math.round(((rS * rW) + (ppS * ppW)) / (rW + ppW));
91311
+ return score;
91312
+ }
91313
+ exports.weightCompactness = weightCompactness;
91314
+ function weightSplitting(csS, dsS) {
91315
+ const csW = C.countySplittingWeight();
91316
+ const dsW = C.districtSplittingWeight();
91317
+ const score = Math.round(((csS * csW) + (dsS * dsW)) / (csW + dsW));
91318
+ return score;
91319
+ }
91320
+ exports.weightSplitting = weightSplitting;
91321
+ function weightTradtionalPrinciples(cS, sS, pdS, context) {
91322
+ const cW = C.compactnessWeight(context);
91323
+ const sW = C.splittingWeight(context);
91324
+ const pdW = C.popdevWeight(context);
91325
+ const score = Math.round(((cS * cW) + (sS * sW) + (pdS * pdW)) / (cW + sW + pdW));
91326
+ return score;
91327
+ }
91328
+ exports.weightTradtionalPrinciples = weightTradtionalPrinciples;
91329
+ function weightOverall(pS, tpS, context) {
91330
+ const pW = C.partisanWeight(context);
91331
+ const tpW = C.traditionalPrinciplesWeight(context);
91332
+ const score = Math.round(((pS * pW) + (tpS * tpW)) / (pW + tpW));
91333
+ return score;
91334
+ }
91335
+ exports.weightOverall = weightOverall;
91336
+ function mixinMinorityBonus(score, minorityBonus) {
91337
+ const modifiedScore = Math.min(score + minorityBonus, S.NORMALIZED_RANGE);
91338
+ return modifiedScore;
91339
+ }
91340
+ exports.mixinMinorityBonus = mixinMinorityBonus;
91341
+ function printScorecardHeader() {
91342
+ console.log('XX, Name, N, V%, ^S#, S#, B%, B$, UE#, I$, C#, Cd, Md, C$, >P$, Rc, Rc$, Pc, Pc$, G$, Cs, Cs$, Ds, Ds$, S$, Eq, Eq$, T$, Od, M$, $$$');
91343
+ }
91344
+ exports.printScorecardHeader = printScorecardHeader;
91345
+ function printScorecardRow(xx, name, N, Vf, s) {
91346
+ console.log('%s, %s, %i, %f, %i, %f, %f, %i, %f, %i, %i, %f, %f, %i, %i, %f, %i, %f, %i, %i, %f, %i, %f, %i, %i, %f, %i, %i, %f, %i, %i', xx, // 1
91347
+ name, // 2
91348
+ N, // 3
91349
+ Vf, // 4
91350
+ s.partisan.bias.propS, // 5
91351
+ s.partisan.bias.estS, // 6
91352
+ s.partisan.bias.bias, // 7
91353
+ s.partisan.bias.score, s.partisan.impact.unearnedS, // 9
91354
+ s.partisan.impact.score, s.partisan.competitiveness.c, // 11
91355
+ s.partisan.competitiveness.cD, s.partisan.competitiveness.mD, s.partisan.competitiveness.score, s.partisan.score2, // 15
91356
+ s.traditionalPrinciples.compactness.reock.raw, s.traditionalPrinciples.compactness.reock.normalized, s.traditionalPrinciples.compactness.polsby.raw, s.traditionalPrinciples.compactness.polsby.normalized, s.traditionalPrinciples.compactness.score, s.traditionalPrinciples.splitting.county.raw, s.traditionalPrinciples.splitting.county.normalized, s.traditionalPrinciples.splitting.district.raw, s.traditionalPrinciples.splitting.district.normalized, s.traditionalPrinciples.splitting.score, s.traditionalPrinciples.populationDeviation.raw, s.traditionalPrinciples.populationDeviation.normalized, s.traditionalPrinciples.score,
91357
+ // s.minority.nOpportunity1,
91358
+ // s.minority.nOpportunity2,
91359
+ s.minority.opportunityDistricts,
91360
+ // s.minority.nProportional,
91361
+ s.minority.score, s.score);
91362
+ }
91363
+ exports.printScorecardRow = printScorecardRow;
90405
91364
 
90406
91365
 
90407
- /***/ }),
90408
-
90409
- /***/ "./src/settings.json":
90410
- /*!***************************!*\
90411
- !*** ./src/settings.json ***!
90412
- \***************************/
90413
- /*! exports provided: fair, best, default */
90414
- /***/ (function(module) {
90415
-
90416
- module.exports = JSON.parse("{\"fair\":{\"unbiased\":{\"worst\":0.1,\"best\":0,\"weight\":80},\"competitive\":{\"worst\":0,\"best\":0.67,\"weight\":20,\"min\":0.25,\"max\":0.75,\"marginal\":75,\"overall\":25},\"weight\":80},\"best\":{\"compact\":{\"reock\":{\"worst\":0.25,\"best\":0.5,\"weight\":50},\"polsby\":{\"worst\":0.1,\"best\":0.5,\"weight\":50},\"weight\":50},\"cohesive\":{\"county\":{\"best\":1,\"worst\":1.71,\"allowableSplitsMultiplier\":1.5,\"weight\":50},\"district\":{\"best\":1,\"worst\":1.5,\"weight\":50},\"weight\":50},\"equal\":{\"worst\":0.0075,\"best\":0.002,\"stateLeg\":{\"worst\":0.1},\"weight\":0},\"weight\":20}}");
90417
-
90418
91366
  /***/ }),
90419
91367
 
90420
91368
  /***/ "./src/settings.ts":
@@ -90429,16 +91377,13 @@ module.exports = JSON.parse("{\"fair\":{\"unbiased\":{\"worst\":0.1,\"best\":0,\
90429
91377
  //
90430
91378
  // GLOBAL CONSTANTS
90431
91379
  //
90432
- var __importDefault = (this && this.__importDefault) || function (mod) {
90433
- return (mod && mod.__esModule) ? mod : { "default": mod };
90434
- };
90435
91380
  Object.defineProperty(exports, "__esModule", { value: true });
90436
91381
  // Normalized scores [0-100]
90437
91382
  exports.NORMALIZED_RANGE = 100;
90438
91383
  // Square deviations from the ideal
90439
91384
  exports.DISTANCE_WEIGHT = 2;
90440
91385
  // Out of range message
90441
- exports.OUT_OF_RANGE_MSG = "# out of range";
91386
+ exports.OUT_OF_RANGE_MSG = "%f out of range";
90442
91387
  // A small delta to use when testing ranges of values
90443
91388
  exports.EPSILON = 1 / Math.pow(10, 6);
90444
91389
  // Keep 4 decimals for fractions [0.0–1.0], i.e., 2 for %'s [0–100]
@@ -90446,62 +91391,6 @@ exports.PRECISION = 4;
90446
91391
  // "Roughly equal" = average census block size / 2
90447
91392
  exports.AVERAGE_BLOCK_SIZE = 30;
90448
91393
  exports.EQUAL_TOLERANCE = exports.AVERAGE_BLOCK_SIZE / 2;
90449
- // SCORING PARAMETERS
90450
- const settings_json_1 = __importDefault(__webpack_require__(/*! ./settings.json */ "./src/settings.json"));
90451
- // TODO - DELETE
90452
- function readOverrides() {
90453
- return undefined;
90454
- }
90455
- const overrides = readOverrides();
90456
- // SETTINGS FOR PARTISAN SCORING
90457
- exports.MIN_CONTESTED = overrides ? overrides.fair.competitive.min : settings_json_1.default.fair.competitive.min;
90458
- exports.MAX_CONTESTED = overrides ? overrides.fair.competitive.max : settings_json_1.default.fair.competitive.max;
90459
- // SCALES FOR NORMALIZING RAW VALUES
90460
- // Compactness scales
90461
- exports.REOCK_WORST = overrides ? overrides.best.compact.reock.worst : settings_json_1.default.best.compact.reock.worst;
90462
- exports.REOCK_BEST = overrides ? overrides.best.compact.reock.best : settings_json_1.default.best.compact.reock.best;
90463
- exports.POLSBY_WORST = overrides ? overrides.best.compact.polsby.worst : settings_json_1.default.best.compact.polsby.worst;
90464
- exports.POLSBY_BEST = overrides ? overrides.best.compact.polsby.best : settings_json_1.default.best.compact.polsby.best;
90465
- // County-District splitting scales (not inverted)
90466
- exports.COUNTY_BEST = overrides ? overrides.best.cohesive.county.best : settings_json_1.default.best.cohesive.county.best;
90467
- exports.COUNTY_WORST = overrides ? overrides.best.cohesive.county.worst : settings_json_1.default.best.cohesive.county.worst;
90468
- exports.ALLOWABLE_SPLITS_MULTIPLIER = overrides ? overrides.best.cohesive.county.allowableSplitsMultiplier : settings_json_1.default.best.cohesive.county.allowableSplitsMultiplier;
90469
- exports.DISTRICT_BEST = overrides ? overrides.best.cohesive.district.best : settings_json_1.default.best.cohesive.district.best;
90470
- exports.DISTRICT_WORST = overrides ? overrides.best.cohesive.district.worst : settings_json_1.default.best.cohesive.district.worst;
90471
- // Population deviation thresholds & scales (inverted)
90472
- exports.POPDEV_WORST = overrides ? overrides.best.equal.worst : settings_json_1.default.best.equal.worst;
90473
- exports.POPDEV_BEST = overrides ? overrides.best.equal.best : settings_json_1.default.best.equal.best;
90474
- exports.POPEQ_MIN = 1.0 - exports.POPDEV_WORST;
90475
- exports.POPEQ_MAX = 1.0 - exports.POPDEV_BEST;
90476
- exports.POPDEV_WORST_LD = overrides ? overrides.best.equal.stateLeg.worst : settings_json_1.default.best.equal.stateLeg.worst;
90477
- exports.POPDEV_BEST_LD = ((exports.POPDEV_BEST / exports.POPDEV_WORST) * exports.POPDEV_WORST_LD);
90478
- exports.POPEQ_MIN_LD = 1.0 - exports.POPDEV_WORST_LD;
90479
- exports.POPEQ_MAX_LD = 1.0 - exports.POPDEV_BEST_LD;
90480
- // Bias & competitiveness scales
90481
- // TODO - SCORE:
90482
- // * How wide a range do we want for bias? 10%? 20%?
90483
- // * Do we want it to be state-specific? E.g., one seat?
90484
- exports.BIAS_WORST = overrides ? overrides.fair.unbiased.worst : settings_json_1.default.fair.unbiased.worst;
90485
- exports.BIAS_BEST = overrides ? overrides.fair.unbiased.best : settings_json_1.default.fair.unbiased.best;
90486
- // The range for raw overall competitiveness values
90487
- exports.COMPETITIVE_WORST = overrides ? overrides.fair.competitive.worst : settings_json_1.default.fair.competitive.worst;
90488
- exports.COMPETITIVE_BEST = overrides ? overrides.fair.competitive.best : settings_json_1.default.fair.competitive.best;
90489
- // WEIGHTS FOR COMBINING NORMALIZED VALUES
90490
- exports.REOCK_WEIGHT = overrides ? overrides.best.compact.reock.weight : settings_json_1.default.best.compact.reock.weight;
90491
- exports.POLSBY_WEIGHT = overrides ? overrides.best.compact.polsby.weight : settings_json_1.default.best.compact.polsby.weight;
90492
- exports.COUNTY_WEIGHT = overrides ? overrides.best.cohesive.county.weight : settings_json_1.default.best.cohesive.county.weight;
90493
- exports.DISTRICT_WEIGHT = overrides ? overrides.best.cohesive.district.weight : settings_json_1.default.best.cohesive.district.weight;
90494
- exports.COMPACTNESS_WEIGHT = overrides ? overrides.best.compact.weight : settings_json_1.default.best.compact.weight;
90495
- exports.SPLITTING_WEIGHT = overrides ? overrides.best.cohesive.weight : settings_json_1.default.best.cohesive.weight;
90496
- exports.POPDEV_WEIGHT = overrides ? overrides.best.equal.weight : settings_json_1.default.best.equal.weight;
90497
- // TODO - SCORE: What kind of relative weighting do we want between marginal and
90498
- // overall competitiveness?
90499
- exports.MARGINAL_WEIGHT = overrides ? overrides.fair.competitive.marginal : settings_json_1.default.fair.competitive.marginal;
90500
- exports.OVERALL_WEIGHT = overrides ? overrides.fair.competitive.overall : settings_json_1.default.fair.competitive.overall;
90501
- exports.BIAS_WEIGHT = overrides ? overrides.fair.unbiased.weight : settings_json_1.default.fair.unbiased.weight;
90502
- exports.COMPETITIVE_WEIGHT = overrides ? overrides.fair.competitive.weight : settings_json_1.default.fair.competitive.weight;
90503
- exports.FAIR_WEIGHT = overrides ? overrides.fair.weight : settings_json_1.default.fair.weight;
90504
- exports.BEST_WEIGHT = overrides ? overrides.best.weight : settings_json_1.default.best.weight;
90505
91394
 
90506
91395
 
90507
91396
  /***/ }),
@@ -90522,10 +91411,70 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
90522
91411
  return (mod && mod.__esModule) ? mod : { "default": mod };
90523
91412
  };
90524
91413
  Object.defineProperty(exports, "__esModule", { value: true });
90525
- const sample_profile_json_1 = __importDefault(__webpack_require__(/*! ../testdata/profiles/sample-profile.json */ "./testdata/profiles/sample-profile.json"));
91414
+ const sample_profile_json_1 = __importDefault(__webpack_require__(/*! ../testdata/samples/sample-profile.json */ "./testdata/samples/sample-profile.json"));
90526
91415
  exports.sampleProfile = sample_profile_json_1.default;
90527
- const sample_scorecard_json_1 = __importDefault(__webpack_require__(/*! ../testdata/profiles/sample-scorecard.json */ "./testdata/profiles/sample-scorecard.json"));
91416
+ const sample_scorecard_json_1 = __importDefault(__webpack_require__(/*! ../testdata/samples/sample-scorecard.json */ "./testdata/samples/sample-scorecard.json"));
90528
91417
  exports.sampleScorecard = sample_scorecard_json_1.default;
91418
+ exports.Metrics = {
91419
+ // PARTISAN
91420
+ // Bias metrics
91421
+ Vf: { label: "Statewide D vote share", abbr: "V" },
91422
+ estS: { label: "Probable D seats", abbr: "S_V" },
91423
+ estSf: { label: "Probable D seat share", units: 1 /* Percentage */, abbr: "S%" },
91424
+ propS: { label: "D seats closest to proportional", abbr: "^S" },
91425
+ propSf: { label: "D seat share closest to proportional", units: 1 /* Percentage */, abbr: "^S%" },
91426
+ fptpS: { label: "Probable FPTP D seats", abbr: "S!" },
91427
+ bias: { label: "Effective bias", units: 1 /* Percentage */, abbr: "B%", description: "Calculated as S% – ^S%" },
91428
+ biasScore: { label: "Bias score", abbr: "B$", description: "Bias score normalized [0–100]" },
91429
+ // Additional bias metrics
91430
+ s50v: { label: "Seats bias", units: 1 /* Percentage */, abbr: "BS_50", symbol: "\u{03B1}" },
91431
+ v50s: { label: "Votes bias", units: 1 /* Percentage */, abbr: "BV_50", symbol: "\u{03B1}2" },
91432
+ decl: { label: "Declination", units: 2 /* Degrees */, abbr: "decl", symbol: "\u{03B4}" },
91433
+ gSym: { label: "Global symmetry", abbr: "GS", symbol: "\u{0393}" },
91434
+ EG: { label: "Efficiency gap", units: 1 /* Percentage */, abbr: "EG", symbol: "\u{03B3}2" },
91435
+ BsGf: { label: "Partisan bias", units: 1 /* Percentage */, abbr: "BS_V", symbol: "\u{03D0}" },
91436
+ prop: { label: "Disproprtionality", units: 1 /* Percentage */, abbr: "prop", symbol: "\u{03B3}" },
91437
+ MM: { label: "Mean–median", abbr: "MM" },
91438
+ LO: { label: "Lopsided outcomes", abbr: "LO" },
91439
+ // Impact metrics
91440
+ unearnedS: { label: "Unearned seats", abbr: "UE" },
91441
+ impactScore: { label: "Impact score", abbr: "I$", description: "Impact score normalized to [0–100]" },
91442
+ // Competitiveness/responsiveness metrics
91443
+ c: { label: "Simple competitive districts", abbr: "C", description: "Count of districts in the range [45–55%]" },
91444
+ cD: { label: "Probable competitive districts", abbr: "Cd", description: "Probable competitive districts, using probabilities" },
91445
+ cDf: { label: "Probable competitive districts share", units: 1 /* Percentage */, abbr: "Cdf" },
91446
+ mD: { label: "Competitive marginal districts", abbr: "Md", description: "Probable competitive marginal districts, using probabilities" },
91447
+ mDf: { label: "Competitive marginal districts share", units: 1 /* Percentage */, abbr: "Mdf" },
91448
+ competitivenessScore: { label: "Competitiveness score", abbr: "C$", description: "Competitiveness score normalized to [0–100]" },
91449
+ // Additional competitiveness/responsiveness metrics
91450
+ rD: { label: "Responsive districts", abbr: "Rd" },
91451
+ rDf: { label: "Responsive districts %", abbr: "Rd%" },
91452
+ bigR: { label: "Overall responsiveness", abbr: "R" /*, symbol: "\u{}" */ },
91453
+ littleR: { label: "Point responsiveness", abbr: "r", symbol: "\u{03C1}" },
91454
+ MIR: { label: "Minimal inverse responsiveness", abbr: "MIR", symbol: "\u{03B6}" },
91455
+ partisanScore: { label: "Partisan score", abbr: "P$" },
91456
+ // * <P$ [score] = the combined partisan score used to compare plans *across* states
91457
+ // * >P$ [score2] = the combined partisan score used to compare plans *within* a state, along with traditional districting principles
91458
+ // MINORITY
91459
+ oD: { label: "Opportunity districts", abbr: "Od" },
91460
+ minorityBonus: { label: "Minority bonus", abbr: "M$" },
91461
+ // TRADITIONAL DISTRICTING PRINCIPLES
91462
+ reockRaw: { label: "Reock", abbr: "Rc" },
91463
+ reockNormalized: { label: "Reock", abbr: "Rc$", description: "Reock normalized to [0–100]" },
91464
+ polsbyRaw: { label: "Polsby-Popper", abbr: "Pc" },
91465
+ polsbyNormalized: { label: "Polsby-Popper", abbr: "Pc$", description: "Polsby-Popper normalized to [0–100]" },
91466
+ compactnessScore: { label: "Compactness score", abbr: "G$" },
91467
+ countySplittingRaw: { label: "County splitting", abbr: "Cs" },
91468
+ countySplittingNormalized: { label: "County splitting", abbr: "Cs$", description: "County splitting normalized to [0–100]" },
91469
+ districtSplittingRaw: { label: "District splitting", abbr: "Ds" },
91470
+ districtSplittingNormalized: { label: "District splitting", abbr: "Ds$", description: "District splitting normalized to [0–100]" },
91471
+ splittingScore: { label: "Splitting score", abbr: "S$" },
91472
+ populationDeviationRaw: { label: "Population deviation", abbr: "Eq" },
91473
+ populationDeviationNormalized: { label: "County splitting", abbr: "Eq$", description: "Population deviation normalized to [0–100]" },
91474
+ traditionalPrinciplesScore: { label: "Traditional districting principles score", abbr: "T$", description: "Traditional principles score normalized to [0–100]" },
91475
+ // OVERALL SCORE
91476
+ score: { label: "Overall score", abbr: "$$$" }
91477
+ };
90529
91478
 
90530
91479
 
90531
91480
  /***/ }),
@@ -90568,11 +91517,31 @@ function maxArray(arr) {
90568
91517
  return Math.max(...arr);
90569
91518
  }
90570
91519
  exports.maxArray = maxArray;
91520
+ // Modified from https://jsfiddle.net/Lucky500/3sy5au0c/
91521
+ function medianArray(arr) {
91522
+ if (arr.length === 0)
91523
+ return 0;
91524
+ arr.sort(function (a, b) {
91525
+ return a - b;
91526
+ });
91527
+ var half = Math.floor(arr.length / 2);
91528
+ if (arr.length % 2)
91529
+ return arr[half];
91530
+ return (arr[half - 1] + arr[half]) / 2.0;
91531
+ }
91532
+ exports.medianArray = medianArray;
90571
91533
  function initArray(n, value) {
90572
91534
  return Array.from(Array(n), () => value);
90573
91535
  }
90574
91536
  exports.initArray = initArray;
90575
91537
  // MISCELLANEOUS
91538
+ // Deal with decimal census "counts" due to disagg/re-agg
91539
+ function areRoughlyEqual(x, y, tolerance) {
91540
+ let delta = Math.abs(x - y);
91541
+ let result = (delta < tolerance) ? true : false;
91542
+ return result;
91543
+ }
91544
+ exports.areRoughlyEqual = areRoughlyEqual;
90576
91545
  // Round a fractional number [0-1] to the desired level of PRECISION.
90577
91546
  function trim(fullFraction, digits = undefined) {
90578
91547
  if (digits == 0) {
@@ -90606,25 +91575,25 @@ exports.deepCopy = deepCopy;
90606
91575
 
90607
91576
  /***/ }),
90608
91577
 
90609
- /***/ "./testdata/profiles/sample-profile.json":
90610
- /*!***********************************************!*\
90611
- !*** ./testdata/profiles/sample-profile.json ***!
90612
- \***********************************************/
91578
+ /***/ "./testdata/samples/sample-profile.json":
91579
+ /*!**********************************************!*\
91580
+ !*** ./testdata/samples/sample-profile.json ***!
91581
+ \**********************************************/
90613
91582
  /*! exports provided: state, planName, nDistricts, nCounties, legislativeDistricts, populationProfile, compactnessProfile, splittingProfile, partisanProfile, demographicProfile, default */
90614
91583
  /***/ (function(module) {
90615
91584
 
90616
- module.exports = JSON.parse("{\"state\":\"NC\",\"planName\":\"Sample profile\",\"nDistricts\":13,\"nCounties\":100,\"legislativeDistricts\":false,\"populationProfile\":{\"TotalPopByDistrict\":[725807,721754,732627,733218,730051,729580,728476,729721,731507,724085,733447,730845,729710],\"targetSize\":729294},\"compactnessProfile\":{\"GeometryByDistrict\":[[1.5773977911324972,10.187637000340999,2.6574300583946115],[0.7058475358440041,7.6543777040386445,1.7632772181381422],[2.920140664739499,9.983421957097065,2.733997011824872],[0.19139679136850038,3.5473804553379114,0.9224411598627837],[1.0389865962844946,6.505473449741405,2.1061393959539023],[1.0334553158144995,6.472595447414713,1.6394147493749138],[1.6241801669130118,8.040301088673953,1.952917034583555],[0.7706765524270006,7.015983900119356,2.1886348192462464],[0.9976965229710011,8.405282685068,2.5604335810665138],[0.676265120610002,5.866361656441843,1.7264712385837822],[1.719226531340005,10.410671767954964,3.1586859214584764],[0.11329460131899982,2.3038812649606695,0.5379072920877442],[0.48373856130249987,5.267369326010034,1.45173753753012]]},\"splittingProfile\":{\"CountyPopByDistrict\":[[0,0,0,0,0,0,0,0,21282,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,240299,56552,0,0,0,12197,0,59916,0,0,54691,0,0,0,24669,0,0,0,0,0,0,0,0,0,0,0,0,24505,0,0,0,0,0,0,22099,0,0,0,0,0,0,0,74235,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,45422,0,20972,13228,0,0,0,55740,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,60619,0,0,0,0,0,0,0,114678,0,0,0,0,0,0,0,109245,0,0,0,0,0,0,0,0,0,0,0,0,95840,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,324879,0,0,0,0,0,16493,0,0],[0,0,0,0,0,0,0,47759,0,0,0,0,0,0,0,9980,66469,0,0,0,0,14793,0,0,0,103505,0,23547,33920,0,0,0,0,0,0,0,0,0,0,0,21362,0,0,0,0,0,0,0,5810,0,0,0,10153,0,59495,0,0,0,0,0,0,0,0,0,0,0,0,177772,0,13144,40661,0,13453,0,86397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4407,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27288,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,133801,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,572129,0,0,0,0,0,0,0,0],[0,0,37198,11155,0,27281,17797,0,0,0,0,0,0,0,0,0,0,0,6051,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,350670,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,47401,73673,0,0,0,0,0,0,0,0,51079,0,69340,0,38406,0],[0,151131,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23719,0,63505,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,158500,0,0,0,0,0,0,0,0,0,0,0,57866,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,39464,0,0,141752,0,0,93643,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,8316,107431,0,0,0,0,0,0,0,0,0,0,0,0,0,58098,0,0,0,0,0,0,58505,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,55188,0,0,0,0,0,0,0,0,0,0,0,0,0,202667,0,0,0,0,0,52217,0,0,0,0,0,0,0,0,0,0,63431,0,0,0,0,0,0,0,0,0,0,0,0,0,122623,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,178011,0,0,0,0,0,0,0,0,0,0,0,0,242290,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,46952,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27798,88247,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85838,0,0,0,60585,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26948,0,0,0,0,25045,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75524,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,185734,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,46639,134168,0,0,0,0,36157,0,0,0,0,0,0,201292,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,108857,0,0,0,0,0,0,144479,0,0,0,0,98078,0,0,0,0,0,0,0,0,0,0,0,0,206086,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78265,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20510,0,0,0,0,0,67810,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,126417,90912,0,83029,0,0,0,0,0,27444,0,10587,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8861,0,0,0,0,0,59036,106740,0,0,0,0,40271,0,0,0,0,0,44996,33922,20764,0,0,15579,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13981,33090,0,0,0,0,0,0,0,0,0,0,0,17818],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,730845,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,162878,41240,0,0,0,0,0,0,0,0,0,0,325932,0,0,0,0,0,0,0,150509,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,49151,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]},\"partisanProfile\":{\"statewideVf\":0.515036,\"VfArray\":[0.4235,0.428588,0.43388,0.443866,0.454505,0.456985,0.458005,0.458134,0.463947,0.473144,0.718314,0.73662,0.775817]},\"demographicProfile\":{\"DemographicsByDistrict\":[[0.462,0.538,0.0659,0.4445,0.0219,0.0009,0.013],[0.7063,0.2937,0.0714,0.1968,0.0203,0.001,0.0113],[0.7125,0.2875,0.0537,0.2118,0.0163,0.0021,0.0104],[0.6162,0.3838,0.0869,0.2239,0.0715,0.0012,0.0101],[0.7767,0.2233,0.0669,0.1402,0.013,0.0008,0.0077],[0.7112,0.2888,0.0715,0.1985,0.0136,0.0008,0.0102],[0.7086,0.2914,0.0695,0.2023,0.0097,0.0011,0.0142],[0.6698,0.3302,0.0725,0.224,0.0229,0.0029,0.0188],[0.6406,0.3594,0.0594,0.1962,0.0222,0.001,0.0862],[0.8193,0.1807,0.0451,0.1158,0.0141,0.0007,0.008],[0.8956,0.1044,0.0433,0.0327,0.0103,0.0012,0.0197],[0.4657,0.5343,0.1215,0.3618,0.0507,0.0017,0.011],[0.698,0.302,0.0572,0.2117,0.0286,0.0009,0.0095]]}}");
91585
+ module.exports = JSON.parse("{\"state\":\"NC\",\"planName\":\"NC 116th Congressional\",\"nDistricts\":13,\"nCounties\":100,\"legislativeDistricts\":false,\"populationProfile\":{\"totalPopByDistrict\":[733499,733499,733498,733499,733499,733498,733499,733499,733498,733499,733499,733498,733499],\"targetSize\":733499},\"compactnessProfile\":{\"geometryByDistrict\":[[1.577393519870778,10.159603194222512,2.6574239327900613],[0.7058443860242223,7.806818205304456,1.7632772181381422],[2.9200833695730037,9.972779463090355,2.733997011824872],[0.1913918462294299,3.5460738146162076,0.9224411598627837],[1.039017192939366,6.4920493759205495,2.1060599512212406],[1.0334601456545682,6.466840626614781,1.6394147493749138],[1.6241884494008865,7.988593581066711,1.9529170345835551],[0.7706777049448708,6.980189925133433,2.188634395661598],[0.9976766364327585,8.350880983965949,2.5604199958307654],[0.6762837622380331,5.8543721706585545,1.7264608756890525],[1.719230725216499,10.37139670690812,3.1586859214584764],[0.11329839503948308,2.2954988975477066,0.5379072920877442],[0.48371245467321944,5.3384017624136435,1.4517375375301198]]},\"splittingProfile\":{\"countyPopByDistrict\":[[0,0,0,0,0,0,0,21282,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,240299,56552,0,0,0,12197,0,59916,0,0,54691,0,0,0,24669,0,0,0,0,0,0,0,0,0,0,0,0,24505,0,0,0,0,0,0,22099,0,0,0,0,0,0,0,80880,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,45422,0,20972,13228,0,0,0,56787,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,60619,0,0,0,0,0,0,0,114678,0,0,0,0,0,0,0,109332,0,0,0,0,0,0,0,0,0,0,0,0,95840,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,328583,0,0,0,0,0,24447,0,0],[0,0,0,0,0,0,47759,0,0,0,0,0,0,0,9980,66469,0,0,0,0,14793,0,0,0,103505,0,23547,33920,0,0,0,0,0,0,0,0,0,0,0,21362,0,0,0,0,0,0,0,5810,0,0,0,10153,0,59495,0,0,0,0,0,0,0,0,0,0,0,0,177772,0,13144,40661,0,13453,0,87268,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4407,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27288,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,133801,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,572410,0,0,0,0,0,0,0,0],[0,37198,11155,0,27281,17797,0,0,0,0,0,0,0,0,0,0,0,9499,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,350670,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,47401,73673,0,0,0,0,0,0,0,0,51079,0,69340,0,38406,0],[151131,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23719,0,63505,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,162418,0,0,0,0,0,0,0,0,0,0,0,57866,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,39464,0,0,141752,0,0,93643,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,8981,107431,0,0,0,0,0,0,0,0,0,0,0,0,0,58098,0,0,0,0,0,0,58505,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,59546,0,0,0,0,0,0,0,0,0,0,0,0,0,202667,0,0,0,0,0,52217,0,0,0,0,0,0,0,0,0,0,63431,0,0,0,0,0,0,0,0,0,0,0,0,0,122623,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,178011,0,0,0,0,0,0,0,0,0,0,0,0,243476,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,46952,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27798,88247,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,88430,0,0,0,60585,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,26948,0,0,0,0,26209,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75955,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,186130,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,46639,134168,0,0,0,0,36157,0,0,0,0,0,0,201292,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,111849,0,0,0,0,0,0,144859,0,0,0,0,98078,0,0,0,0,0,0,0,0,0,0,0,0,206086,0,0,0,0,0,0,0,0,0,0,0,0,6042,0,0,0,0,0,78265,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20510,0,0,0,0,0,67810,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,126469,90912,0,83029,0,0,0,0,0,27444,0,10587,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8861,0,0,0,0,0,59036,106740,0,0,0,0,40271,0,0,0,0,0,44996,33922,20764,0,0,15579,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13981,33090,0,0,0,0,0,0,0,0,0,0,0,17818],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,733498,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,162878,41240,0,0,0,0,0,0,0,0,0,0,325988,0,0,0,0,0,0,0,153395,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,49998,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]},\"partisanProfile\":{\"statewideVf\":0.488799,\"vfArray\":[0.38133475250631976,0.3849820112175976,0.3932778243643098,0.4232536822067312,0.42479562842958446,0.43309446651496813,0.43760621455968074,0.43980028165892326,0.4545677480810613,0.4642133701735491,0.6853270858231137,0.6938881738049054,0.6953207231271951]},\"demographicProfile\":{\"stateMfArray\":[0.684371246819619,0.31562875318038097,0.21168281993226215,0.06787156278984616,0.0012540930000187486,0.024136155044881008,0.017483272326632705],\"mfArrayByDistrict\":[[0.46201994876162167,0.5379800512383783,0.44446912914151915,0.065885219684988,0.0009221497550043815,0.0218600624522836,0.01298238746863721],[0.7062964810448031,0.2937035189551968,0.19676408791341676,0.07140508797387993,0.0009787472035794184,0.02027027027027027,0.011283934941653062],[0.7125415968218078,0.2874584031781921,0.21181863608495496,0.05374539732717163,0.002106863519897368,0.01628256424250371,0.010404964330393058],[0.6162003899079305,0.3837996100920695,0.22386087117385056,0.08687191382180923,0.0012060108434249284,0.07153885560599377,0.010091002234143982],[0.7766842620090415,0.22331573799095852,0.14015703622760559,0.06690518783542039,0.0007790124871119258,0.013001753659331847,0.007680851626320752],[0.7111584046318987,0.2888415953681012,0.19851813634865356,0.07148666466027488,0.0008061395588804333,0.013621967123837368,0.010186021181764765],[0.7086112059348308,0.2913887940651691,0.20229594619799895,0.06953648210647081,0.001149028868250555,0.009700759801937688,0.014166663733974303],[0.6697822118227867,0.33021778817721326,0.2240088599271958,0.07249565635824667,0.002909229825482943,0.022877125445843145,0.01884938491094157],[0.6406346877484486,0.35936531225155144,0.19622106825202063,0.059419121439330605,0.0010389455633486816,0.02217720538538736,0.08616917683114156],[0.8192767260107778,0.18072327398922214,0.11582505713398979,0.045060025957170666,0.0006806703721468273,0.014117738340433936,0.008000521964844963],[0.8956213645578734,0.1043786354421266,0.032662784268536554,0.04329373425036473,0.0012436075643032956,0.010267513422177209,0.019651410943402076],[0.46574079494234033,0.5342592050576597,0.36183998712169996,0.12151883451384417,0.001650032195750161,0.050651598079962536,0.010997775566352515],[0.6979595109954735,0.3020404890045265,0.21171009017357523,0.0572352710553516,0.0008767865416830024,0.028566846063370996,0.009525252165235058]]}}");
90617
91586
 
90618
91587
  /***/ }),
90619
91588
 
90620
- /***/ "./testdata/profiles/sample-scorecard.json":
90621
- /*!*************************************************!*\
90622
- !*** ./testdata/profiles/sample-scorecard.json ***!
90623
- \*************************************************/
90624
- /*! exports provided: score, best, fair, default */
91589
+ /***/ "./testdata/samples/sample-scorecard.json":
91590
+ /*!************************************************!*\
91591
+ !*** ./testdata/samples/sample-scorecard.json ***!
91592
+ \************************************************/
91593
+ /*! exports provided: score, partisan, minority, traditionalPrinciples, default */
90625
91594
  /***/ (function(module) {
90626
91595
 
90627
- module.exports = JSON.parse("{\"score\":5,\"best\":{\"BestScore\":18,\"compactness\":{\"score\":35,\"reock\":{\"raw\":0.3373,\"normalized\":35,\"notes\":{}},\"polsby\":{\"raw\":0.2418,\"normalized\":35,\"notes\":{}}},\"splitting\":{\"score\":0,\"county\":{\"raw\":1.1474,\"normalized\":0,\"notes\":{}},\"district\":{\"raw\":1.4839,\"normalized\":3,\"notes\":{}}},\"populationDeviation\":{\"raw\":0.016,\"normalized\":0,\"notes\":{\"maxDeviation\":11693}}},\"fair\":{\"bias\":{\"BestS\":7,\"BestSf\":0.5385,\"ProbableS\":4.1925,\"ProbableSf\":0.3225,\"Bias\":0.216,\"normalized\":0,\"notes\":{}},\"competitiveness\":{\"Cd\":0.7266,\"Cdf\":0.0559,\"Mdf\":0.1019,\"normalized\":10,\"notes\":{}},\"score\":2,\"notes\":{}}}");
91596
+ module.exports = JSON.parse("{\"score\":5,\"partisan\":{\"bias\":{\"propS\":7,\"propSf\":0.5385,\"estS\":4.1925,\"estSf\":0.3225,\"bias\":0.216,\"tOf\":-0.0023,\"fptpS\":3,\"s50V\":0.2172,\"v50S\":0.045,\"decl\":36.5164,\"gSym\":6.6602,\"eG\":0.2846,\"sVg\":0.2098,\"prop\":0.2695,\"mMd\":0.0593,\"mMs\":0.057,\"lO\":0.1106,\"score\":0},\"impact\":{\"unearnedS\":2.8075,\"score\":0},\"competitiveness\":{\"bigR\":-16.926,\"littleR\":4.3523,\"mIR\":0.1298,\"rD\":4.0207,\"rDf\":0.3093,\"c\":6,\"cD\":0.7266,\"cDf\":0.0559,\"mRange\":[5,11],\"mD\":0.7134,\"mDf\":0.1019,\"score\":10},\"score\":3,\"score2\":2},\"minority\":{\"report\":{\"bucketsByDemographic\":[[10,14],[3,0],[1,9],[0,0],[0,0],[0,0]],\"averageDVf\":0.6737588682747838},\"nProportional\":18,\"opportunityDistricts\":12.9424,\"score\":14},\"traditionalPrinciples\":{\"score\":18,\"compactness\":{\"score\":35,\"reock\":{\"raw\":0.3373,\"normalized\":35,\"notes\":{}},\"polsby\":{\"raw\":0.2418,\"normalized\":35,\"notes\":{}}},\"splitting\":{\"score\":0,\"county\":{\"raw\":1.1474,\"normalized\":0,\"notes\":{}},\"district\":{\"raw\":1.4839,\"normalized\":3,\"notes\":{}}},\"populationDeviation\":{\"raw\":0.016,\"normalized\":0,\"notes\":{\"maxDeviation\":11693}}}}");
90628
91597
 
90629
91598
  /***/ })
90630
91599
 
@@ -100410,6 +101379,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
100410
101379
  return result;
100411
101380
  };
100412
101381
  Object.defineProperty(exports, "__esModule", { value: true });
101382
+ const Score = __importStar(__webpack_require__(/*! @dra2020/dra-score */ "./node_modules/@dra2020/dra-score/dist/dra-score.bundle.js"));
100413
101383
  const preprocess_1 = __webpack_require__(/*! ./preprocess */ "./src/preprocess.ts");
100414
101384
  const analyze_1 = __webpack_require__(/*! ./analyze */ "./src/analyze.ts");
100415
101385
  const score_1 = __webpack_require__(/*! ./score */ "./src/score.ts");
@@ -100603,9 +101573,13 @@ class AnalyticsSession {
100603
101573
  return 1 - this.testScales[4 /* PopulationDeviation */]['scale'][0];
100604
101574
  }
100605
101575
  else {
101576
+ // NOTE - This assumes the plan has been profiled
101577
+ const scorer = new Score.Scorer();
101578
+ const popdev = scorer.populationDeviationThreshold(this.legislativeDistricts);
101579
+ return popdev;
100606
101580
  // TODO - SCORE: Temporary HACK. Query dra-score for threshold.
100607
101581
  // NOTE - The plan may not have been scored yet, i.e., no scorecard yet.
100608
- return 1 - this.testScales[4 /* PopulationDeviation */]['scale'][0];
101582
+ // return 1 - this.testScales[T.Test.PopulationDeviation]['scale'][0];
100609
101583
  }
100610
101584
  }
100611
101585
  }
@@ -102815,7 +103789,9 @@ function doAnalyzePostProcessing(s, bLog = false) {
102815
103789
  // Just populate the normalized population deviation score in the test
102816
103790
  const scorecard = s._scorecard;
102817
103791
  let test = s.getTest(4 /* PopulationDeviation */);
102818
- test['normalizedScore'] = scorecard.best.populationDeviation.normalized;
103792
+ test['normalizedScore'] = scorecard.traditionalPrinciples.populationDeviation.normalized;
103793
+ // TODO - DELETE
103794
+ // test['normalizedScore'] = scorecard.best.populationDeviation.normalized;
102819
103795
  }
102820
103796
  // Derive secondary tests
102821
103797
  analyze_1.doDeriveSecondaryTests(s, bLog);
@@ -102861,9 +103837,10 @@ function profilePlan(s, bLog = false) {
102861
103837
  const geoPropsByDistrict = makeArrayOfGeoProps(s, bLog);
102862
103838
  const splits = makeNakedCxD(s);
102863
103839
  const summaryRow = s.districts.numberOfRows() - 1;
102864
- const statewideVf = s.districts.statistics[D.DistrictField.DemPct][summaryRow];
103840
+ const statewideVf = U.trim(s.districts.statistics[D.DistrictField.DemPct][summaryRow], 6);
102865
103841
  const vpiArray = U.deepCopy(s.districts.statistics[D.DistrictField.DemPct].slice(1, -1));
102866
- const demographicsByDistrict = makeArrayOfDemographics(s);
103842
+ const statewideDemographics = getStatewideDemographics(s);
103843
+ const demographicsByDistrict = getDemographicsByDistrict(s);
102867
103844
  const profile = {
102868
103845
  state: state,
102869
103846
  planName: planName,
@@ -102871,21 +103848,22 @@ function profilePlan(s, bLog = false) {
102871
103848
  nCounties: nCounties,
102872
103849
  legislativeDistricts: s.legislativeDistricts,
102873
103850
  populationProfile: {
102874
- TotalPopByDistrict: popByDistrict,
103851
+ totalPopByDistrict: popByDistrict,
102875
103852
  targetSize: targetSize
102876
103853
  },
102877
103854
  compactnessProfile: {
102878
- GeometryByDistrict: geoPropsByDistrict
103855
+ geometryByDistrict: geoPropsByDistrict
102879
103856
  },
102880
103857
  splittingProfile: {
102881
- CountyPopByDistrict: splits
103858
+ countyPopByDistrict: splits
102882
103859
  },
102883
103860
  partisanProfile: {
102884
103861
  statewideVf: statewideVf,
102885
- VfArray: vpiArray
103862
+ vfArray: vpiArray
102886
103863
  },
102887
103864
  demographicProfile: {
102888
- DemographicsByDistrict: demographicsByDistrict
103865
+ stateMfArray: statewideDemographics,
103866
+ mfArrayByDistrict: demographicsByDistrict
102889
103867
  }
102890
103868
  };
102891
103869
  return profile;
@@ -102919,7 +103897,7 @@ function makeArrayOfGeoProps(s, bLog = false) {
102919
103897
  }
102920
103898
  return geometryByDistrict;
102921
103899
  }
102922
- function makeArrayOfDemographics(s, bLog = false) {
103900
+ function getDemographicsByDistrict(s, bLog = false) {
102923
103901
  let demographicsArray = [];
102924
103902
  // Remove the unassigned & total dummy "districts"
102925
103903
  for (let districtID = 1; districtID <= s.state.nDistricts; districtID++) {
@@ -102936,6 +103914,19 @@ function makeArrayOfDemographics(s, bLog = false) {
102936
103914
  }
102937
103915
  return demographicsArray;
102938
103916
  }
103917
+ function getStatewideDemographics(s, bLog = false) {
103918
+ const summaryRow = s.districts.numberOfRows() - 1;
103919
+ const demographicsArray = [
103920
+ U.deepCopy(s.districts.statistics[D.DistrictField.WhitePct][summaryRow]),
103921
+ U.deepCopy(s.districts.statistics[D.DistrictField.MinorityPct][summaryRow]),
103922
+ U.deepCopy(s.districts.statistics[D.DistrictField.BlackPct][summaryRow]),
103923
+ U.deepCopy(s.districts.statistics[D.DistrictField.HispanicPct][summaryRow]),
103924
+ U.deepCopy(s.districts.statistics[D.DistrictField.PacificPct][summaryRow]),
103925
+ U.deepCopy(s.districts.statistics[D.DistrictField.AsianPct][summaryRow]),
103926
+ U.deepCopy(s.districts.statistics[D.DistrictField.NativePct][summaryRow])
103927
+ ];
103928
+ return demographicsArray;
103929
+ }
102939
103930
  // SCORE A PLAN
102940
103931
  function scorePlan(s, p, bLog = false, overridesJSON) {
102941
103932
  let scorer = new Score.Scorer();
@@ -102945,11 +103936,14 @@ function scorePlan(s, p, bLog = false, overridesJSON) {
102945
103936
  // calling sequence.
102946
103937
  let test = s.getTest(4 /* PopulationDeviation */);
102947
103938
  // TODO - SCORE: U.trim(popDev)???
103939
+ // const popDev = scorecard.best.populationDeviation.raw;
102948
103940
  // Get the raw population deviation
102949
- const popDev = scorecard.best.populationDeviation.raw;
103941
+ const popDev = scorecard.traditionalPrinciples.populationDeviation.raw;
102950
103942
  // Populate the test entry
102951
103943
  test['score'] = popDev;
102952
- test['details'] = { 'maxDeviation': scorecard.best.populationDeviation.notes['maxDeviation'] };
103944
+ test['details'] = { 'maxDeviation': scorecard.traditionalPrinciples.populationDeviation.notes['maxDeviation'] };
103945
+ // TODO - DELETE
103946
+ // test['details'] = { 'maxDeviation': scorecard.best.populationDeviation.notes['maxDeviation'] };
102953
103947
  // Populate the N+1 summary "district" in district.statistics
102954
103948
  let totalPop = s.districts.statistics[D.DistrictField.TotalPop];
102955
103949
  let popDevPct = s.districts.statistics[D.DistrictField.PopDevPct];