@dra2020/district-analytics 1.0.3 → 1.0.6

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/src/report.ts ADDED
@@ -0,0 +1,997 @@
1
+ //
2
+ // GENERATE REPORTS
3
+ // - A test log: a simple enumeration of all analytics & validations w/ raw results
4
+ // - A scorecard: a structured subset of analytics & validations w/ normalized
5
+ // results, cateories, and an overall score
6
+ //
7
+
8
+ import * as T from './types'
9
+ import * as U from './utils';
10
+ import * as S from './settings';
11
+
12
+ import * as D from './_data'
13
+ import { AnalyticsSession } from './_api';
14
+ import { doDeriveSecondaryTests } from './analyze';
15
+
16
+
17
+ // TEST META-DATA
18
+
19
+ enum TestType {
20
+ PassFail,
21
+ Percentage,
22
+ Number
23
+ }
24
+
25
+ const completeDefn: T.Dict = {
26
+ ID: T.Test.Complete,
27
+ name: "Complete",
28
+ normalize: false,
29
+ externalType: TestType.PassFail,
30
+ detailsFn: doPrepareCompleteDetails,
31
+ suites: [T.Suite.Legal]
32
+ };
33
+
34
+ const contiguousDefn: T.Dict = {
35
+ ID: T.Test.Contiguous,
36
+ name: "Contiguous",
37
+ normalize: false,
38
+ externalType: TestType.PassFail,
39
+ detailsFn: doPrepareContiguousDetails,
40
+ suites: [T.Suite.Legal]
41
+ };
42
+
43
+ const freeOfHolesDefn: T.Dict = {
44
+ ID: T.Test.FreeOfHoles,
45
+ name: "Free of Holes",
46
+ normalize: false,
47
+ externalType: TestType.PassFail,
48
+ detailsFn: doPrepareFreeOfHolesDetails,
49
+ suites: [T.Suite.Legal]
50
+ };
51
+
52
+ const equalPopulationDefn: T.Dict = {
53
+ ID: T.Test.EqualPopulation,
54
+ name: "Equal Population",
55
+ normalize: false,
56
+ externalType: TestType.PassFail,
57
+ detailsFn: doPrepareEqualPopulationDetails,
58
+ suites: [T.Suite.Legal]
59
+ };
60
+
61
+ const populationDeviationDefn: T.Dict = {
62
+ ID: T.Test.PopulationDeviation,
63
+ name: "Population Deviation",
64
+ normalize: true,
65
+ externalType: TestType.Percentage,
66
+ detailsFn: doPreparePopulationDeviationDetails,
67
+ suites: [T.Suite.Legal, T.Suite.Best] // Both so EqualPopulation can be assessed
68
+ };
69
+
70
+ const reockDefn: T.Dict = {
71
+ ID: T.Test.Reock,
72
+ name: "Reock",
73
+ normalize: true,
74
+ externalType: TestType.Number,
75
+ detailsFn: doPrepareReockDetails,
76
+ suites: [T.Suite.Best]
77
+ };
78
+
79
+ const polsbyPopperDefn: T.Dict = {
80
+ ID: T.Test.PolsbyPopper,
81
+ name: "Polsby-Popper",
82
+ normalize: true,
83
+ externalType: TestType.Number,
84
+ detailsFn: doPreparePolsbyPopperDetails,
85
+ suites: [T.Suite.Best]
86
+ };
87
+
88
+ const countySplitsDefn: T.Dict = {
89
+ ID: T.Test.CountySplits,
90
+ name: "County splits",
91
+ trailer: "of the population had their county split unexpectedly.",
92
+ normalize: true, // TODO - Normalize this for real
93
+ externalType: TestType.Percentage,
94
+ detailsFn: doPrepareCountySplitDetails,
95
+ suites: [T.Suite.Best]
96
+ };
97
+
98
+ const efficiencyGapDefn: T.Dict = {
99
+ ID: T.Test.EfficiencyGap,
100
+ name: "Efficiency Gap",
101
+ normalize: false, // TODO - Normalize this!
102
+ externalType: TestType.Percentage,
103
+ detailsFn: doPrepareEfficiencyGapDetails,
104
+ suites: [T.Suite.Fair]
105
+ };
106
+
107
+ // A structure to hold test definitions
108
+ type TestDefns = {
109
+ [category: number]: any;
110
+ }
111
+
112
+ // All the tests that have been defined (can be reported on)
113
+ const testDefns = {
114
+ [T.Test.Complete]: completeDefn,
115
+ [T.Test.Contiguous]: contiguousDefn,
116
+ [T.Test.FreeOfHoles]: freeOfHolesDefn,
117
+ [T.Test.EqualPopulation]: equalPopulationDefn,
118
+ [T.Test.PopulationDeviation]: populationDeviationDefn,
119
+ [T.Test.Reock]: reockDefn,
120
+ [T.Test.PolsbyPopper]: polsbyPopperDefn,
121
+ [T.Test.CountySplits]: countySplitsDefn,
122
+ [T.Test.EfficiencyGap]: efficiencyGapDefn
123
+
124
+ /* TODO - More tests ... */
125
+ } as TestDefns;
126
+
127
+
128
+ // SCORECARD
129
+
130
+ // The notion of a scorecard is:
131
+ // - A subset of analytics & validations
132
+ // - Normalized, if numeric
133
+ // - Organized into several dimensions/categories
134
+ // - With combined category scores, and
135
+ // - Those combined into an overall score
136
+
137
+ // A scorecard
138
+ export type Scorecard = {
139
+ [category: number]: ScorecardCategoryEntry;
140
+ }
141
+
142
+ type ScorecardCategoryEntry = {
143
+ catName: string;
144
+ catScore?: number | boolean;
145
+ catTests: T.TestEntries;
146
+ }
147
+
148
+ // A scorecard definition
149
+ type ScorecardDefn = {
150
+ [category: number]: any;
151
+ }
152
+
153
+ // Scorecard category definitions
154
+
155
+ const validCategory = {
156
+ catName: "Valid",
157
+ catTests: [
158
+ { testID: T.Test.Complete },
159
+ { testID: T.Test.Contiguous },
160
+ { testID: T.Test.FreeOfHoles },
161
+ { testID: T.Test.EqualPopulation }
162
+ ],
163
+ catNumeric: false,
164
+ catWeight: undefined,
165
+ catPrepareFn: doPrepareValidSection
166
+ };
167
+
168
+ const fairCategory = {
169
+ catName: "Fair",
170
+ catTests: [
171
+ {
172
+ testID: T.Test.EfficiencyGap,
173
+ testWeight: 100
174
+ }
175
+ ],
176
+ catNumeric: true,
177
+ catWeight: undefined,
178
+ catPrepareFn: doPrepareFairSection
179
+ };
180
+
181
+ // TODO - Decide on the relative weights of these tests!
182
+ // NOTE: 'testWeights' are simply relative, i.e., each normalized score is
183
+ // multiplied by the associated 'testWeight', and the sum of those is divided
184
+ // by the total weight. Weights don't have to add to 100.
185
+ const bestCategory = {
186
+ catName: "Best",
187
+ catTests: [
188
+ {
189
+ testID: T.Test.PopulationDeviation,
190
+ testWeight: 10
191
+ },
192
+ {
193
+ testID: T.Test.Reock,
194
+ testWeight: 25
195
+ },
196
+ {
197
+ testID: T.Test.PolsbyPopper,
198
+ testWeight: 25
199
+ },
200
+ {
201
+ testID: T.Test.CountySplits,
202
+ testWeight: 50
203
+ }
204
+ ],
205
+ catNumeric: true,
206
+ catWeight: undefined,
207
+ catPrepareFn: doPrepareBestSection
208
+ };
209
+
210
+
211
+ // The overall scorecard definition
212
+
213
+ const scorecardDefn = {
214
+ [T.Suite.Legal]: validCategory,
215
+ // TODO - NIY
216
+ // [T.Suite.Fair]: fairCategory,
217
+ [T.Suite.Best]: bestCategory
218
+ } as ScorecardDefn;
219
+
220
+
221
+ // NORMALIZE RAW ANALYTICS
222
+ // Raw numeric analytics, such as population deviation, compactness, etc. are
223
+ // normalized as part of creating a scorecard, so the code to normalize results
224
+ // is encapsulated here.
225
+
226
+ // Configure scale parameters for normalizing each raw test result
227
+ // This needs to be separate from the scorecard configuration info above,
228
+ // because some scales need access to the analytics session object.
229
+ export function doConfigureScales(s: AnalyticsSession): void {
230
+ // Scale defn for PopulationDeviation
231
+ const CDLimit = 0.75 / 100; // Deviation threshold for CD's
232
+ const LDLimit = 10.00 / 100; // Deviation threshold for LD's
233
+
234
+ const CDGoodEnough = 0.20 / 100;
235
+ const LDGoodEnough = (CDGoodEnough / CDLimit) * LDLimit;
236
+ const scale = (s.legislativeDistricts) ? [1.0 - LDLimit, 1.0 - LDGoodEnough] : [1.0 - CDLimit, 1.0 - CDGoodEnough];
237
+ // const scale = [1.0 - CDLimit, 1.0 - CDGoodEnough];
238
+
239
+ s.testScales[T.Test.PopulationDeviation] = { testScale: scale, testInvertp: true };
240
+
241
+ s.testScales[T.Test.Reock] = { testScale: [0.25, 0.50], testInvertp: false };
242
+ s.testScales[T.Test.PolsbyPopper] = { testScale: [0.10, 0.50], testInvertp: false };
243
+
244
+ const SPLITLimit = 0.1; // TODO - Just a placeholder default maximum (10%)
245
+ s.testScales[T.Test.CountySplits] = { testScale: [1.0 - SPLITLimit, 1.0], testInvertp: true };
246
+
247
+ // TODO - More analytics ...
248
+ }
249
+
250
+ // Postprocess analytics - Normalize numeric results and derive secondary tests.
251
+ // Do this after analytics have been run and before preparing a test log or scorecard.
252
+ export function doAnalyzePostProcessing(s: AnalyticsSession): void {
253
+ // Normalize the raw scores for all the numerics tests
254
+ let testResults = U.getNumericObjectKeys(testDefns);
255
+
256
+ for (let testID of testResults) {
257
+ if (testDefns[testID]['normalize']) {
258
+ let testResult = s.getTest(testID) as T.TestEntry;
259
+
260
+ let rawScore = testResult['score'] as number;
261
+ let normalizedScore: number;
262
+ let { testScale, testInvertp } = s.testScales[testID];
263
+
264
+ normalizedScore = U.normalize(rawScore, testScale, testInvertp);
265
+ testResult['normalizedScore'] = normalizedScore;
266
+
267
+ // Add the scale used to normalize the raw score to the details
268
+ testResult['details']['scale'] = testScale;
269
+ }
270
+ }
271
+
272
+ // Derive secondary tests
273
+ doDeriveSecondaryTests(s);
274
+
275
+ // Toggle the semaphore, so postprocessing isn't for both the testlog & scorecard
276
+ s.bPostProcessingDone = true;
277
+ }
278
+
279
+ // Prepare a structured but unformatted scorecard, from the test results
280
+ function doGenerateScorecard(s: AnalyticsSession): Scorecard {
281
+ if (!(s.bPostProcessingDone)) {
282
+ doAnalyzePostProcessing(s);
283
+ }
284
+
285
+ // Create a new scorecard
286
+ let scorecard = {} as Scorecard;
287
+
288
+ // Filter the defined scorecard categories by the requested test suites
289
+ let categories = U.getNumericObjectKeys(scorecardDefn);
290
+ let suitesRequested = s.config['suites'];
291
+ categories = categories.filter(x => suitesRequested.includes(x));
292
+
293
+ // ... and initialize each one in the new scorecard
294
+ for (let c of categories) {
295
+ scorecard[c] = {} as ScorecardCategoryEntry;
296
+ scorecard[c]['catName'] = scorecardDefn[c]['catName'];
297
+ scorecard[c]['catTests'] = {} as T.TestEntries;
298
+ // scorecard[c]['catScore'] = undefined;
299
+ }
300
+
301
+ // For each scorecard category
302
+ for (let c of categories) {
303
+ // Grab the scorecard category definition
304
+ let { catName, catTests, catNumeric } = scorecardDefn[c];
305
+
306
+ let numericCategoryScore = 0;
307
+ let totalWeight = 0;
308
+ let booleanCategoryScore = true;
309
+
310
+ // Process the results for each test result in the category
311
+ for (let testDefn of catTests) {
312
+ // Get the config info for the test
313
+ let testID = testDefn['testID'];
314
+ // ... and the actual test result
315
+ let testResult = s.getTest(testID) as T.TestEntry;
316
+
317
+ // Create a new test entry for the scorecard
318
+ let testReport = U.deepCopy(testResult);
319
+ // Add the name
320
+ testReport['name'] = testDefns[testID]['name'];
321
+
322
+ if (catNumeric) {
323
+ // Normalize raw numeric scores ... moved to FIRST PASS above
324
+
325
+ // Accumulate a category score
326
+ let normalizedScore = testReport['normalizedScore'];
327
+ numericCategoryScore += normalizedScore * testDefn['testWeight'];
328
+ totalWeight += testDefn['testWeight'];
329
+ }
330
+ else {
331
+ // AND together pass/fail tests into a category score
332
+ if (!testReport['score']) {
333
+ booleanCategoryScore = false;
334
+ }
335
+ }
336
+ scorecard[c]['catTests'][testID] = testReport;
337
+ }
338
+
339
+ // Set the category score
340
+ if (catNumeric) {
341
+ scorecard[c]['catScore'] = Math.round(numericCategoryScore / totalWeight);
342
+ }
343
+ else {
344
+ scorecard[c]['catScore'] = booleanCategoryScore;
345
+ }
346
+ }
347
+ // TODO - Compute an overall score from the category weights
348
+
349
+ return scorecard;
350
+ }
351
+
352
+ // Prepare a formatted scorecard suitable for rendering
353
+ export function doPrepareScorecard(s: AnalyticsSession): any {
354
+ // Initialize the output format
355
+ let text: any = { data: [] };
356
+ let blocks: any = text.data;
357
+
358
+ // If the plan as already been analyzed, prepare a scorecard
359
+ if (s.bPlanAnalyzed) {
360
+ // Create and cache a new, unformatted scorecard
361
+ s.scorecard = doGenerateScorecard(s);
362
+
363
+ // Create a scorecard header
364
+ blocks.push({ variant: 'h4', text: `Analysis` });
365
+
366
+ // Report district statistics
367
+ blocks.push({ variant: 'h5', text: `Individual Districts` });
368
+ let districtStatisticsText = doPrepareDistrictStatistics(s);
369
+ blocks.push(...districtStatisticsText);
370
+
371
+ // Prepare each scorecard category
372
+ blocks.push({ variant: 'h5', text: `Overall Plan` });
373
+ let categories = U.getNumericObjectKeys(s.scorecard);
374
+ for (let c of categories) {
375
+ let sectionPrepareFn = scorecardDefn[c]['catPrepareFn'];
376
+ let sectionText = sectionPrepareFn(s, c);
377
+ blocks.push(...sectionText);
378
+ }
379
+
380
+ // Report what datasets were used
381
+ let c = s.config['datasets'][D.Dataset.CENSUS];
382
+ let v = s.config['datasets'][D.Dataset.VAP];
383
+ let e = s.config['datasets'][D.Dataset.ELECTION];
384
+
385
+ blocks.push({ variant: 'body1', text: `Using datasets:` });
386
+ blocks.push({ variant: 'body1', text: `* ${c}: ${D.DatasetDescriptions[c]}` });
387
+ blocks.push({ variant: 'body1', text: `* ${v}: ${D.DatasetDescriptions[v]}` });
388
+ blocks.push({ variant: 'body1', text: `* ${e}: ${D.DatasetDescriptions[e]}` });
389
+ }
390
+ // Otherwise, return a blank scorecard
391
+
392
+ // TODO - What dra-client returns from renderAnalyzeCore()
393
+ // return <STV.StaticTextView text={ text } />;
394
+ return text;
395
+ }
396
+
397
+ function doPrepareDistrictStatistics(s: AnalyticsSession): any {
398
+ let text: any = { data: [] };
399
+ let blocks: any = text.data;
400
+
401
+ blocks.push({ variant: 'beginTable' });
402
+ blocks.push({ variant: 'row', cells: ['ID', 'Total', 'Δ%', 'OK?', '*', 'Dem', 'Rep', 'White', 'Minority', 'Black', 'Hispanic', 'Pacific', 'Asian', 'Native'] });
403
+
404
+ for (let d = 0; d < s.districts.numberOfRows(); d++) {
405
+ let tot = s.districts.statistics[D.DistrictField.TotalPop][d];
406
+ if (tot == 0)
407
+ blocks.push({ variant: 'row', cells: [String(d), '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-'] });
408
+ else {
409
+ tot = Math.round(tot);
410
+ let dev = fractionToPercentage(s.districts.statistics[D.DistrictField.PopDevPct][d]);
411
+ let bEq = true; // TODO - Set based on population threshold
412
+ let bC = s.districts.statistics[D.DistrictField.bContiguous][d]
413
+ && s.districts.statistics[D.DistrictField.bNotEmbedded][d];
414
+ let dPct = fractionToPercentage(s.districts.statistics[D.DistrictField.DemPct][d]);
415
+ let rPct = fractionToPercentage(s.districts.statistics[D.DistrictField.RepPct][d]);
416
+ let wPct = fractionToPercentage(s.districts.statistics[D.DistrictField.WhitePct][d]);
417
+ let mPct = fractionToPercentage(s.districts.statistics[D.DistrictField.MinorityPct][d]);
418
+ let bPct = fractionToPercentage(s.districts.statistics[D.DistrictField.BlackPct][d]);
419
+ let hPct = fractionToPercentage(s.districts.statistics[D.DistrictField.HispanicPct][d]);
420
+ let pPct = fractionToPercentage(s.districts.statistics[D.DistrictField.PacificPct][d]);
421
+ let aPct = fractionToPercentage(s.districts.statistics[D.DistrictField.AsianPct][d]);
422
+ let nPct = fractionToPercentage(s.districts.statistics[D.DistrictField.NativePct][d]);
423
+
424
+ let id: string;
425
+ if (d == 0) id = "??";
426
+ else if (d == (s.districts.numberOfRows() - 1)) id = " ";
427
+ else id = String(d);
428
+
429
+ blocks.push({
430
+ variant: 'row',
431
+ cells: [
432
+ `${id}`,
433
+ `${formatInteger(tot)}`,
434
+ `${formatPercentage(dev)}%`,
435
+ `${pfBoolToString(bEq)}`,
436
+ `${pfBoolToString(bC)}`,
437
+ `${formatPercentage(dPct)}%`,
438
+ `${formatPercentage(rPct)}%`,
439
+ `${formatPercentage(wPct)}%`,
440
+ `${formatPercentage(mPct)}%`,
441
+ `${formatPercentage(bPct)}%`,
442
+ `${formatPercentage(hPct)}%`,
443
+ `${formatPercentage(pPct)}%`,
444
+ `${formatPercentage(aPct)}%`,
445
+ `${formatPercentage(nPct)}%`]
446
+ });
447
+ }
448
+ }
449
+ blocks.push({ variant: 'endTable' });
450
+
451
+ return blocks;
452
+ }
453
+
454
+ // TEST LOG
455
+
456
+ // Prepare formatted test results for rendering
457
+ export function doPrepareTestLog(s: AnalyticsSession): any {
458
+ // Initialize the output format
459
+ let text: any = { data: [] };
460
+ let blocks: any = text.data;
461
+
462
+ // If the plan as already been analyzed, prepare a test log
463
+ if (s.bPlanAnalyzed) {
464
+ if (!(s.bPostProcessingDone)) {
465
+ doAnalyzePostProcessing(s);
466
+ }
467
+
468
+ // Create a test log header
469
+ blocks.push({ variant: 'h4', text: `Test Log` });
470
+
471
+ let testResults = U.getNumericObjectKeys(testDefns);
472
+ let suitesRequested = new Set(s.config['suites']);
473
+
474
+ for (let testID of testResults) {
475
+ // Filter the defined tests by the requested test suites
476
+ let inSuites: number[] = testDefns[testID]['suites'];
477
+ if (!(U.isArrayEmpty(inSuites.filter(x => suitesRequested.has(x))))) {
478
+ // Get the test result
479
+ let testResult = s.getTest(testID) as T.TestEntry;
480
+ // Prepare the text for it, and append it to the output
481
+ let testText = prepareTestEntry(testID, testResult);
482
+ blocks.push(...testText);
483
+ }
484
+ }
485
+ }
486
+ // Otherwise, return a blank test log
487
+
488
+ // TODO - What dra-client returns from renderAnalyzeCore()
489
+ // return <STV.StaticTextView text={ text } />;
490
+ return text;
491
+ }
492
+
493
+ function prepareTestEntry(testID: number, testResult: T.TestEntry): any {
494
+ let text: any = { data: [] };
495
+ let blocks: any = text.data;
496
+
497
+ let testName = testDefns[testID]['name'];
498
+ let testNameTrailer: string = "";
499
+ if (U.keyExists('trailer', testDefns[testID])) {
500
+ testNameTrailer = testDefns[testID]['trailer'];
501
+ }
502
+ let testType = testDefns[testID]['externalType'];
503
+ let bNormalize = testDefns[testID]['normalize'];
504
+ let detailsFn = testDefns[testID]['detailsFn'];
505
+ let detailsText = detailsFn(testResult);
506
+ let score: number | boolean | undefined; // NOTE - Won't be undefined here
507
+ let normalizedScore: number;
508
+ let scoreText: string;
509
+
510
+ // Get the score ...
511
+ score = testResult['score'];
512
+
513
+ // ... and format it for rendering
514
+ switch (testType) {
515
+ case TestType.PassFail: {
516
+ scoreText = pfBoolToString(score as boolean);
517
+ blocks.push({ variant: 'body1', text: `${testName}: ${scoreText} ${testNameTrailer}` });
518
+ break;
519
+ }
520
+ case TestType.Percentage: {
521
+ score = fractionToPercentage(score as number);
522
+ if (bNormalize) {
523
+ normalizedScore = testResult['normalizedScore'] as number;
524
+ blocks.push({ variant: 'body1', text: `${testName}: ${normalizedScore} / 100 : ${formatPercentage(score)}% ${testNameTrailer}` });
525
+ }
526
+ else {
527
+ blocks.push({ variant: 'body1', text: `${testName}: ${formatPercentage(score)}% ${testNameTrailer}` });
528
+ }
529
+ break;
530
+ }
531
+ case TestType.Number: {
532
+ if (bNormalize) {
533
+ normalizedScore = testResult['normalizedScore'] as number;
534
+ blocks.push({ variant: 'body1', text: `${testName}: ${normalizedScore} / 100 : ${formatNumber(score as number)} ${testNameTrailer}` });
535
+ }
536
+ else {
537
+ blocks.push({ variant: 'body1', text: `${testName}: ${formatNumber(score as number)} ${testNameTrailer}` });
538
+ }
539
+ break;
540
+ }
541
+ default: {
542
+ // Unknown test type
543
+ throw new RangeError();
544
+ }
545
+ }
546
+
547
+ // Add the details text
548
+ blocks.push(...detailsText);
549
+
550
+ return blocks;
551
+ }
552
+
553
+
554
+ // FORMATTERS FOR TEST DETAILS
555
+
556
+ function doPrepareCompleteDetails(testResult: T.TestEntry): any {
557
+ let text: any = { data: [] };
558
+ let blocks: any = text.data;
559
+
560
+ if (!U.isObjectEmpty(testResult['details'])) {
561
+ let unassignedText = "";
562
+ let emptyText = "";
563
+ let missingText = "";
564
+
565
+ if (U.keyExists('unassignedFeatures', testResult['details'])) {
566
+ let unassignedFeatures = testResult['details']['unassignedFeatures'];
567
+
568
+ let unassignedList = prepareListItems(unassignedFeatures);
569
+ let unassignedTextTemplates: string[] = [
570
+ `GEOID ${unassignedList} is not assigned to a district.`,
571
+ `GEOIDs ${unassignedList} are not assigned to districts.`,
572
+ `Several GEOIDs are not assigned to districts, including ${unassignedList}.`
573
+ ];
574
+ unassignedText = prepareListText(unassignedFeatures, unassignedTextTemplates);
575
+ }
576
+
577
+ if (U.keyExists('emptyDistricts', testResult['details'])) {
578
+ let emptyDistricts = testResult['details']['emptyDistricts'];
579
+
580
+ let emptyList = prepareListItems(emptyDistricts);
581
+ let emptyTextTemplates: string[] = [
582
+ `District ${emptyList} is empty.`,
583
+ `Districts ${emptyList} are empty.`,
584
+ `Several districts are empty, including ${emptyList}.`
585
+ ];
586
+ emptyText = prepareListText(emptyDistricts, emptyTextTemplates);
587
+ }
588
+
589
+ if (U.keyExists('missingDistricts', testResult['details'])) {
590
+ missingText = `Not enough districts have been defined. `;
591
+ }
592
+
593
+ let detailsText = " " + unassignedText + emptyText + missingText;
594
+ blocks.push({ variant: 'body1', text: detailsText });
595
+ }
596
+
597
+ return blocks;
598
+ }
599
+
600
+ function doPrepareContiguousDetails(testResult: T.TestEntry): any {
601
+ let text: any = { data: [] };
602
+ let blocks: any = text.data;
603
+
604
+ if (!U.isObjectEmpty(testResult['details'])) {
605
+ let discontiguousDistricts = testResult['details']['discontiguousDistricts'];
606
+
607
+ let discontiguousList = prepareListItems(discontiguousDistricts);
608
+ let discontiguousTextTemplates: string[] = [
609
+ `District ${discontiguousList} is not contiguous.`,
610
+ `Districts ${discontiguousList} are not contiguous.`,
611
+ `Several districts are not contiguous, including ${discontiguousList}.`
612
+ ];
613
+ let detailsText = prepareListText(discontiguousDistricts, discontiguousTextTemplates);
614
+
615
+ blocks.push({ variant: 'body1', text: detailsText });
616
+ }
617
+
618
+ return blocks;
619
+ }
620
+
621
+ function doPrepareFreeOfHolesDetails(testResult: T.TestEntry): any {
622
+ let text: any = { data: [] };
623
+ let blocks: any = text.data;
624
+
625
+ if (!U.isObjectEmpty(testResult['details'])) {
626
+ let embeddedDistricts = testResult['details']['embeddedDistricts'];
627
+
628
+ let embeddedList = prepareListItems(embeddedDistricts);
629
+ let embeddedTextTemplates: string[] = [
630
+ `District ${embeddedList} is fully embedded within another district.`,
631
+ `Both districts ${embeddedList} are fully embedded within other districts.`,
632
+ `Several districts are fully embedded within other districts, including ${embeddedList}.`
633
+ ];
634
+ let detailsText = prepareListText(embeddedDistricts, embeddedTextTemplates);
635
+
636
+ blocks.push({ variant: 'body1', text: detailsText });
637
+ }
638
+
639
+ return blocks;
640
+ }
641
+
642
+ function doPrepareEqualPopulationDetails(testResult: T.TestEntry): any {
643
+ let text: any = { data: [] };
644
+ let blocks: any = text.data;
645
+
646
+ if (!U.isObjectEmpty(testResult['details'])) {
647
+ let popDevPct = fractionToPercentage(testResult['details']['deviation']);
648
+ let thresholdPct = fractionToPercentage(1.0 - testResult['details']['thresholds'][0]);
649
+
650
+ let detailsText = '';
651
+ if (!(testResult['score'])) {
652
+ detailsText = `The ${formatPercentage(popDevPct)}% population deviation is greater than the ${formatPercentage(thresholdPct)}% threshold tolerated by courts.`;
653
+ }
654
+ blocks.push({ variant: 'body1', text: detailsText });
655
+ }
656
+
657
+ return blocks;
658
+ }
659
+
660
+ function doPreparePopulationDeviationDetails(testResult: T.TestEntry): any {
661
+ let text: any = { data: [] };
662
+ let blocks: any = text.data;
663
+
664
+ let n = Math.round(testResult['details']['maxDeviation']);
665
+ let term: string = "people";
666
+ if (n == 1) {
667
+ term = "person";
668
+ }
669
+ blocks.push({ variant: 'body1', text: `The maximum population deviation between districts is ${formatInteger(n)} ${term}.` });
670
+
671
+ return blocks;
672
+ }
673
+
674
+ function doPrepareReockDetails(testResult: T.TestEntry): any {
675
+ let text: any = { data: [] };
676
+ let blocks: any = text.data;
677
+
678
+ // TODO - No details implemented yet
679
+
680
+ return blocks;
681
+ }
682
+
683
+ function doPreparePolsbyPopperDetails(testResult: T.TestEntry): any {
684
+ let text: any = { data: [] };
685
+ let blocks: any = text.data;
686
+
687
+ // TODO - No details implemented yet
688
+
689
+ return blocks;
690
+ }
691
+
692
+ function doPrepareEfficiencyGapDetails(testResult: T.TestEntry): any {
693
+ let text: any = { data: [] };
694
+ let blocks: any = text.data;
695
+
696
+ // TODO - Not yet implemented
697
+
698
+ return blocks;
699
+ }
700
+
701
+ function doPrepareCountySplitDetails(testResult: T.TestEntry): any {
702
+ let text: any = { data: [] };
703
+ let blocks: any = text.data;
704
+
705
+ let unexpectedAffected = fractionToPercentage(testResult['details']['unexpectedAffected']);
706
+ let affectedText = `affecting ${formatPercentage(unexpectedAffected)}% of the total population.`;
707
+
708
+ let countiesSplitUnexpectedly = testResult['details']['countiesSplitUnexpectedly'];
709
+ let nCountiesSplitUnexpectedly = countiesSplitUnexpectedly.length;
710
+ let nUnexpectedSplits = testResult['details']['unexpectedSplits'];
711
+ let splitList = prepareListItems(countiesSplitUnexpectedly);
712
+ let splitTextTemplates: string[] = [
713
+ `${splitList} county is split unexpectedly, `,
714
+ `${splitList} counties are split unexpectedly, `,
715
+ // `These ${formatInteger(nCountiesSplitUnexpectedly)} counties are split unexpectedly--${splitList}--`
716
+ `${splitList} counties are split unexpectedly ${formatInteger(nUnexpectedSplits)} times, `
717
+
718
+ ];
719
+ let detailsText = prepareListText(countiesSplitUnexpectedly, splitTextTemplates) + affectedText;
720
+
721
+ blocks.push({ variant: 'body1', text: detailsText });
722
+
723
+ return blocks;
724
+ }
725
+
726
+
727
+ // FORMATTERS FOR CATEGORIES
728
+
729
+ // This function parses & formats the 'Valid' section of a scorecard.
730
+ function doPrepareValidSection(s: AnalyticsSession, c: number): any {
731
+ // Get the category meta data
732
+ let { catName, catScore, catTests } = s.scorecard[c];
733
+ let testReport: T.TestEntry;
734
+
735
+ // Initialize the section text
736
+ let text: any = { data: [] };
737
+ let blocks: any = text.data;
738
+ let testText: any;
739
+
740
+ // Section header
741
+ let stringScore = pfBoolToString(catScore as boolean);
742
+ blocks.push({ variant: 'h6', text: `${catName}: ${stringScore}` });
743
+
744
+ // Complete
745
+ testReport = catTests[T.Test.Complete];
746
+ testText = prepareTestEntry(T.Test.Complete, testReport);
747
+ blocks.push(...testText);
748
+
749
+ // Contiguous
750
+ testReport = catTests[T.Test.Contiguous];
751
+ testText = prepareTestEntry(T.Test.Contiguous, testReport);
752
+ blocks.push(...testText);
753
+
754
+ // Free of holes (no embedded districts)
755
+ testReport = catTests[T.Test.FreeOfHoles];
756
+ testText = prepareTestEntry(T.Test.FreeOfHoles, testReport);
757
+ blocks.push(...testText);
758
+
759
+ // Equal population (w/in the appropriate legal threshold)
760
+ testReport = catTests[T.Test.EqualPopulation];
761
+ testText = prepareTestEntry(T.Test.EqualPopulation, testReport);
762
+ blocks.push(...testText);
763
+
764
+ return blocks;
765
+ }
766
+
767
+ // TODO - NIY
768
+ // This function parses & formats the 'Fair' section of a scorecard.
769
+ function doPrepareFairSection(s: AnalyticsSession, c: number): any {
770
+ // Get the category meta data
771
+ let { catName, catScore, catTests } = s.scorecard[c];
772
+ let testReport: T.TestEntry;
773
+
774
+ // Initialize the section text
775
+ let text: any = { data: [] };
776
+ let blocks: any = text.data;
777
+ let testText: any;
778
+
779
+ // Section header
780
+ blocks.push({ variant: 'h6', text: `${catName}: ${catScore}` });
781
+
782
+ // TODO - Flesh this out
783
+ // There's only one test: Population Deviation.
784
+ // testReport = catTests[T.Test.PopulationDeviation];
785
+ // testText = prepareTestEntry(T.Test.PopulationDeviation, testReport);
786
+ // blocks.push(...testText);
787
+
788
+ return blocks;
789
+ }
790
+
791
+ // This function parses & formats the 'Best' section of a scorecard.
792
+ function doPrepareBestSection(s: AnalyticsSession, c: number): any {
793
+ // Get the category meta data
794
+ let { catName, catScore, catTests } = s.scorecard[c];
795
+ let testReport: T.TestEntry;
796
+
797
+ // Initialize the section text
798
+ let text: any = { data: [] };
799
+ let blocks: any = text.data;
800
+ let testText: any;
801
+
802
+ // Section header
803
+ blocks.push({ variant: 'h6', text: `${catName}: ${catScore}` });
804
+
805
+ // Population deviation
806
+ testReport = catTests[T.Test.PopulationDeviation];
807
+ testText = prepareTestEntry(T.Test.PopulationDeviation, testReport);
808
+ blocks.push(...testText);
809
+
810
+ // Compactness
811
+ testReport = catTests[T.Test.Reock];
812
+ let normalizedReock = testReport['normalizedScore'] as number;
813
+ let reockTestText = prepareTestEntry(T.Test.Reock, testReport);
814
+
815
+ testReport = catTests[T.Test.PolsbyPopper];
816
+ let normalizedPolsbyPopper = testReport['normalizedScore'] as number;
817
+ let polsbyPopperTestText = prepareTestEntry(T.Test.PolsbyPopper, testReport);
818
+
819
+ let compactnessScore = (normalizedReock + normalizedPolsbyPopper) / 2;
820
+ blocks.push({ variant: 'body1', text: `Compactness: ${compactnessScore} / 100` });
821
+
822
+ blocks.push(...reockTestText);
823
+ blocks.push(...polsbyPopperTestText);
824
+
825
+ // County splits
826
+ testReport = catTests[T.Test.CountySplits];
827
+ testText = prepareTestEntry(T.Test.CountySplits, testReport);
828
+ blocks.push(...testText);
829
+
830
+ return blocks;
831
+ }
832
+
833
+ // FORMATTING HELPERS
834
+
835
+ // Convert a boolean representing Pass/Fail to a string
836
+ function pfBoolToString(score: boolean): string {
837
+ if (score) {
838
+ return "Yes";
839
+ }
840
+ else {
841
+ return "No";
842
+ }
843
+ }
844
+
845
+ function fractionToPercentage(f: number): number {
846
+ return f * 100;
847
+ }
848
+
849
+ function formatNumber(n: number): string {
850
+ let p = S.PRECISION;
851
+
852
+ return n.toLocaleString('en-US', { minimumFractionDigits: p, maximumFractionDigits: p });
853
+ }
854
+
855
+ function formatPercentage(n: number): string {
856
+ let p = (S.PRECISION / 2);
857
+
858
+ return n.toLocaleString('en-US', { minimumFractionDigits: p, maximumFractionDigits: p });
859
+ }
860
+
861
+ function formatInteger(i: number): string {
862
+ return new Intl.NumberFormat().format(i);
863
+ }
864
+
865
+ // Prepare the items in a list for rendering
866
+ function prepareListItems(list: any[]): string {
867
+ let nItems = list.length;
868
+ let listStr: string;
869
+
870
+ switch (nItems) {
871
+ case 1: {
872
+ listStr = list[0];
873
+ break;
874
+ }
875
+ case 2: {
876
+ listStr = list[0] + " and " + list[1];
877
+ break;
878
+ }
879
+ default: {
880
+ let listWithCommas = list.join(', ');
881
+ let lastCommaIndex = listWithCommas.length - ((list[list.length - 1].length) + 1);
882
+ let beforeAnd = listWithCommas.substr(0, lastCommaIndex);
883
+ let afterAnd = listWithCommas.substr(lastCommaIndex + 1);
884
+ listStr = beforeAnd + " and " + afterAnd;
885
+ break;
886
+ }
887
+ }
888
+ return listStr;
889
+ }
890
+
891
+ // Pick the rendering text for the appropriate list length
892
+ function prepareListText(list: any[], listTemplates: string[]): string {
893
+ let nItems = list.length;
894
+ switch (nItems) {
895
+ case 1: {
896
+ return listTemplates[0];
897
+ break;
898
+ }
899
+ case 2: {
900
+ return listTemplates[1];
901
+ break;
902
+ }
903
+ default: {
904
+ return listTemplates[2];
905
+ break;
906
+ }
907
+ }
908
+ }
909
+
910
+
911
+ /* COMMAND-LINE TESTS
912
+
913
+ node main.js scorecard -v -x NC -n 13 -p ~/src/district-analytics/data/SAMPLE-BG-map.csv -d ~/src/district-analytics/data/SAMPLE-BG-data2.json -s ~/src/district-analytics/data/SAMPLE-BG-shapes.geojson -g ~/src/district-analytics/data/SAMPLE-BG-graph.json -c ~/src/district-analytics/data/SAMPLE-COUNTY.geojson
914
+ node --inspect --inspect-brk main.js scorecard -v -x NC -n 13 -p ~/src/district-analytics/data/SAMPLE-BG-map.csv -d ~/src/district-analytics/data/SAMPLE-BG-data2.json -s ~/src/district-analytics/data/SAMPLE-BG-shapes.geojson -g ~/src/district-analytics/data/SAMPLE-BG-graph.json -c ~/src/district-analytics/data/SAMPLE-COUNTY.geojson
915
+
916
+ -or-
917
+
918
+ ./main.js scorecard -v -x NC -n 13 -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
919
+ ./main.js --inspect --inspect-brk scorecard -v -x NC -n 13 -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
920
+
921
+ These calls work at the project directory, using samples in the data/ directory:
922
+
923
+ ./main.js testlog -v -x NC -n 13 -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
924
+
925
+ TODO - Fix this
926
+ ./main.js testlog -v -x NC -n 13 -p ../data/SAMPLE-BG-map-missing.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
927
+ ./main.js scorecard -v -x NC-n 13 -p ../data/SAMPLE-BG-map-missing.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
928
+
929
+ ./main.js testlog -v -x NC -n 13 -p ../data/SAMPLE-BG-map-unassigned.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
930
+ ./main.js scorecard -v -x NC -n 13 -p ../data/SAMPLE-BG-map-unassigned.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson
931
+
932
+ TODO - HERE
933
+
934
+ ./main.js -v -x NC testlog -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson --empty
935
+ ./main.js -v -x NC scorecard -p ../data/SAMPLE-BG-map.csv -d ../data/SAMPLE-BG-data2.json -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -c ../data/SAMPLE-COUNTY.geojson --empty
936
+
937
+ TODO
938
+
939
+ ./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-discontiguous.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
940
+ ./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-discontiguous.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
941
+
942
+ ./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-hole.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
943
+ ./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-hole.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
944
+
945
+
946
+ node --inspect --inspect-brk main.js scorecard -v -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
947
+ node main.js scorecard -v -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
948
+
949
+
950
+ These calls test NC block-level data stored in my ~/data/redistricting-data/2010/compact/sample directory:
951
+
952
+ node --inspect --inspect-brk main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-shapes.geojson -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
953
+ node --inspect --inspect-brk main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/tl_2018_37_tabblock10.json -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
954
+
955
+ node main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-shapes.geojson -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
956
+ node main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/tl_2018_37_tabblock10.json -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
957
+
958
+
959
+
960
+ */
961
+
962
+ /* TODO - DELETE
963
+
964
+ These calls work at the project directory, using samples in the data/ directory:
965
+
966
+ ./main.js testlog -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
967
+ ./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
968
+
969
+ ./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-missing.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
970
+ ./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-missing.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
971
+
972
+ ./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-unassigned.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
973
+ ./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-unassigned.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
974
+
975
+ ./main.js testlog -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv --empty
976
+ ./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv --empty
977
+
978
+ ./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-discontiguous.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
979
+ ./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-discontiguous.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
980
+
981
+ ./main.js testlog -n 13 -p ../data/SAMPLE-BG-map-hole.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
982
+ ./main.js scorecard -n 13 -p ../data/SAMPLE-BG-map-hole.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
983
+
984
+
985
+ node --inspect --inspect-brk main.js scorecard -v -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
986
+ node main.js scorecard -v -n 13 -p ../data/SAMPLE-BG-map.csv -c ../data/SAMPLE-BG-census.csv -s ../data/SAMPLE-BG-shapes.geojson -g ../data/SAMPLE-BG-graph.json -e ../data/SAMPLE-BG-election.csv -f ../data/SAMPLE-FIPS-county-name-map.csv
987
+
988
+
989
+ These calls test NC block-level data stored in my ~/data/redistricting-data/2010/compact/sample directory:
990
+
991
+ node --inspect --inspect-brk main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-shapes.geojson -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
992
+ node --inspect --inspect-brk main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/tl_2018_37_tabblock10.json -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
993
+
994
+ node main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-shapes.geojson -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
995
+ node main.js scorecard -v -n 13 -p ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-map.csv -c ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-census.csv -s ~/data/redistricting-data/2010/compact/sample/tl_2018_37_tabblock10.json -g ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-graph.json -e ~/data/redistricting-data/2010/compact/sample/SAMPLE-BLOCK-election.csv -f ~/data/redistricting-data/2010/compact/sample/SAMPLE-FIPS-county-name-map.csv
996
+
997
+ */