@kahwee/sf-map-svg 2.0.0 → 2.2.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/CHANGELOG.md +14 -0
- package/README.md +72 -1
- package/SOURCES.md +34 -0
- package/dist/data/candidates/2016-11-08.json +1 -0
- package/dist/data/candidates/2018-11-06.json +1 -0
- package/dist/data/candidates/2020-11-03.json +1 -0
- package/dist/data/candidates/2022-11-08.json +1 -0
- package/dist/data/candidates/2024-11-05.json +1 -0
- package/dist/data/candidates/2026-06-02.json +1 -0
- package/dist/data/candidates/README.md +43 -0
- package/dist/data/candidates/catalog.json +40 -0
- package/dist/src/configuration.js +1 -0
- package/dist/src/controller-types.d.ts +23 -2
- package/dist/src/explorer-core.js +230 -4
- package/dist/src/layers.d.ts +2 -0
- package/dist/src/layers.js +2 -2
- package/dist/src/map-core.d.ts +33 -0
- package/dist/src/map-core.js +90 -28
- package/dist/src/map.d.ts +1 -1
- package/dist/src/map.js +8 -0
- package/dist/src/static.d.ts +35 -2
- package/dist/src/static.js +5 -1
- package/dist/src/types.d.ts +19 -0
- package/dist/src/validation.js +3 -0
- package/docs/EXAMPLES.md +8 -1
- package/package.json +5 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"electionDate":"2026-06-02","electionName":"2026-06-02 San Francisco candidate results","districtYear":2022,"certifiedDate":"2026-06-25","downloadedDate":"2026-09-26","source":"https://sfelections.org/results/20260602/data/20260625/dsov.xlsx","sourceSha256":"1103791ffe31599fe3a7a4c40d4a7c3492189dc3bd5ce2f992b5e1307e6286fe","geography":"Votes cast in San Francisco, reported by supervisorial district. Congressional and state legislative contests include only eligible portions of each district.","method":"Candidate vote share divides a candidate count by all valid votes in that contest and area. Write-ins are included when reported. District eligibility uses registered-voter counts in the official workbook compared with the election’s citywide presidential or gubernatorial contest. Missing supervisorial rows indicate no eligible voters, not zero support. County cumulative votes, when present, are recorded separately as unassigned.","contests":[{"id":"governor","title":"GOVERNOR","sourceSheet":"Sheet2","candidates":[{"name":"MARGARET TROWE","party":"NPP","sourceColumn":7},{"name":"TONY K. THURMOND","party":"DEM","sourceColumn":9},{"name":"THUNDER PARLEY","party":"DEM","sourceColumn":11},{"name":"KATIE PORTER","party":"DEM","sourceColumn":14},{"name":"TOM WOODARD","party":"LIB","sourceColumn":16},{"name":"LEO SAMUEL ZACKY","party":"REP","sourceColumn":19},{"name":"ERIN \"ZEZ\" ZEZULAK","party":"DEM","sourceColumn":21},{"name":"DAVID ZICKEFOOSE","party":"REP","sourceColumn":23},{"name":"AMANDA MARTIN","party":"NPP","sourceColumn":25},{"name":"MATT MAHAN","party":"DEM","sourceColumn":27},{"name":"BRENT MAUPIN","party":"NPP","sourceColumn":29},{"name":"DANIEL MERCURI","party":"NPP","sourceColumn":31},{"name":"BETTY T. YEE","party":"DEM","sourceColumn":33},{"name":"NANCY D. YOUNG","party":"NPP","sourceColumn":35},{"name":"ALICIA OLIVIA LAPP","party":"REP","sourceColumn":37},{"name":"MATTHEW CHASE LEVY","party":"DEM","sourceColumn":39},{"name":"DUANE TERRENCE LOYNES JR.","party":"NPP","sourceColumn":41},{"name":"RANDEEP S. DHILLON","party":"REP","sourceColumn":43},{"name":"LIVINGFORGOD ANDCOUNTRY DEMOTT","party":"NPP","sourceColumn":45},{"name":"PATRICIA DE LUCA BASUALDO","party":"REP","sourceColumn":47},{"name":"LOUIS A. DE BARRAICUA","party":"DEM","sourceColumn":49},{"name":"RAJI RAB","party":"DEM","sourceColumn":51},{"name":"SATISH RAO","party":"DEM","sourceColumn":53},{"name":"RAMSEY ROBINSON","party":"PF","sourceColumn":55},{"name":"LEWIS HERMS","party":"NPP","sourceColumn":57},{"name":"RAFAEL M. HERNANDEZ","party":"REP","sourceColumn":59},{"name":"JON HENDERSON","party":"NPP","sourceColumn":61},{"name":"STEVE HILTON","party":"REP","sourceColumn":63},{"name":"NAOMI BAR-LEV","party":"NPP","sourceColumn":65},{"name":"XAVIER BECERRA","party":"DEM","sourceColumn":67},{"name":"CAROLINA BUHLER","party":"DEM","sourceColumn":69},{"name":"CHAD BIANCO","party":"REP","sourceColumn":71},{"name":"JOSEPH CABRERA","party":"NPP","sourceColumn":73},{"name":"ELAINE CULOTTI","party":"NPP","sourceColumn":75},{"name":"JOEL E. JACOB","party":"DEM","sourceColumn":77},{"name":"MAX FOMIN","party":"NPP","sourceColumn":79},{"name":"LUKASZ ADAM FILINSKI","party":"NPP","sourceColumn":81},{"name":"SERGE FIANKAN","party":"NPP","sourceColumn":83},{"name":"LEO NARANJO IV","party":"REP","sourceColumn":85},{"name":"TIM NELSON","party":"REP","sourceColumn":87},{"name":"TOM STEYER","party":"DEM","sourceColumn":89},{"name":"ERIC SWALWELL","party":"DEM","sourceColumn":91},{"name":"SCOTT P SHIELDS","party":"DEM","sourceColumn":93},{"name":"FREDERIC C. SCHULTZ","party":"NPP","sourceColumn":95},{"name":"CHRISTINE R. SARMIENTO","party":"NPP","sourceColumn":97},{"name":"REZA SAFARNEJAD","party":"NPP","sourceColumn":99},{"name":"SAM SANDAK","party":"NPP","sourceColumn":101},{"name":"GRETHA SOLORZANO","party":"REP","sourceColumn":103},{"name":"JAMES ATHANS JR.","party":"REP","sourceColumn":105},{"name":"LARRY AZEVEDO","party":"DEM","sourceColumn":107},{"name":"MOHAMMAD ARIF","party":"DEM","sourceColumn":109},{"name":"AKINYEMI AGBEDE","party":"DEM","sourceColumn":111},{"name":"DEREK GRASTY","party":"DEM","sourceColumn":113},{"name":"DON J. GRUNDMANN","party":"NPP","sourceColumn":115},{"name":"DAWIT KELLEL","party":"NPP","sourceColumn":117},{"name":"ANNE KOMAROVSK","party":"NPP","sourceColumn":119},{"name":"GARY HOWARD KIDGELL","party":"DEM","sourceColumn":121},{"name":"SOPHIA EDUM-A-SAM","party":"DEM","sourceColumn":123},{"name":"MAURO ALBERTO OROZCO","party":"NPP","sourceColumn":125},{"name":"BARACK D. OBAMA SHAW","party":"DEM","sourceColumn":127},{"name":"ANTONIO VILLARAIGOSA","party":"DEM","sourceColumn":129},{"name":"Write-in","party":null,"sourceColumn":131}],"citywide":{"votes":[33,1694,47,22195,243,78,86,100,84,28109,33,51,2136,307,29,237,125,630,134,212,417,181,190,2035,185,128,110,24700,65,67287,256,4286,261,103,121,56,24,18,135,451,104239,926,156,135,79,31,31,182,133,40,198,61,93,38,80,18,28,37,20,280,888,812],"totalVotes":266077,"registeredVoters":533546,"undervotes":4107,"overvotes":1302,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[3,124,2,2266,24,12,8,18,5,2683,1,3,201,38,1,16,2,29,15,30,38,13,21,209,23,14,21,2788,4,6233,17,440,19,8,11,8,3,0,17,51,10710,78,13,9,7,3,2,13,4,2,19,5,8,8,9,3,3,3,1,20,72,62],"totalVotes":26473,"registeredVoters":48250,"undervotes":419,"overvotes":78,"sourceRow":83,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[3,123,3,2167,28,3,6,3,0,4086,7,3,60,10,1,21,2,14,5,12,32,8,12,92,18,4,7,3141,5,6476,16,327,15,5,10,0,2,0,7,24,9989,45,9,5,2,0,1,14,8,1,12,4,6,2,3,0,1,1,1,13,69,47],"totalVotes":26991,"registeredVoters":50721,"undervotes":290,"overvotes":98,"sourceRow":87,"partiallyEligible":false},{"district":3,"eligible":true,"votes":[4,112,3,1755,25,2,9,5,8,2327,6,5,214,21,2,22,22,61,10,12,34,20,14,154,10,9,6,2047,1,5088,19,295,29,17,11,4,1,2,10,30,7556,118,16,8,2,4,3,20,18,5,17,3,10,4,8,0,3,4,0,21,75,55],"totalVotes":20376,"registeredVoters":44896,"undervotes":451,"overvotes":115,"sourceRow":91,"partiallyEligible":false},{"district":4,"eligible":true,"votes":[3,139,13,1972,26,12,5,26,4,2665,3,6,405,61,4,28,5,64,37,56,55,15,21,177,25,23,18,4126,9,6030,26,670,19,10,26,7,3,3,20,67,8963,136,21,18,11,5,2,34,4,1,22,2,8,7,7,2,2,3,5,14,58,89],"totalVotes":26298,"registeredVoters":50790,"undervotes":704,"overvotes":105,"sourceRow":95,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[3,218,5,2021,23,5,14,7,10,2186,4,10,130,13,3,18,27,76,13,16,48,25,25,221,17,7,7,1092,10,5006,28,231,29,13,7,8,3,2,13,38,9719,116,16,12,13,2,6,20,11,4,22,9,14,2,14,1,5,3,0,35,75,75],"totalVotes":21806,"registeredVoters":46287,"undervotes":348,"overvotes":162,"sourceRow":99,"partiallyEligible":false},{"district":6,"eligible":true,"votes":[4,139,7,1325,13,3,10,3,7,2053,3,2,125,22,1,29,17,55,6,9,28,16,25,142,8,13,10,1320,8,4162,28,240,22,9,6,5,3,1,4,37,5639,79,9,14,6,5,4,11,16,2,19,6,8,3,2,1,2,4,1,28,57,45],"totalVotes":15881,"registeredVoters":38903,"undervotes":277,"overvotes":105,"sourceRow":103,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[1,148,3,2527,24,12,9,6,8,2898,2,3,196,21,2,25,3,33,17,17,37,10,11,156,25,11,12,3410,4,8221,16,541,21,8,8,8,3,2,9,44,10607,64,8,9,10,5,3,25,5,3,15,8,12,2,4,3,3,1,1,21,95,69],"totalVotes":29485,"registeredVoters":52295,"undervotes":365,"overvotes":100,"sourceRow":107,"partiallyEligible":false},{"district":8,"eligible":true,"votes":[2,196,2,3756,16,2,6,3,6,3809,1,2,72,2,1,21,5,13,3,3,37,7,12,220,17,5,4,1604,7,10453,17,224,23,9,1,3,2,0,5,15,17181,61,5,9,4,1,1,5,10,5,14,10,2,1,8,0,0,1,2,19,92,116],"totalVotes":38133,"registeredVoters":60213,"undervotes":338,"overvotes":101,"sourceRow":111,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[2,118,0,1991,19,5,7,6,9,1857,4,5,161,25,3,18,12,78,8,15,32,12,20,282,13,13,4,1308,7,6152,27,338,30,8,7,5,0,0,7,38,11606,58,8,6,5,0,3,10,17,5,17,4,9,3,2,3,2,6,2,17,128,117],"totalVotes":24674,"registeredVoters":49268,"undervotes":300,"overvotes":145,"sourceRow":115,"partiallyEligible":false},{"district":10,"eligible":true,"votes":[6,250,2,1262,17,5,6,4,9,1800,0,7,255,57,5,23,22,114,7,8,27,24,12,172,13,12,12,1493,4,4413,40,396,39,8,12,2,2,4,13,36,6026,89,23,24,10,2,4,12,23,6,18,3,9,3,10,1,3,4,1,52,78,64],"totalVotes":17058,"registeredVoters":45189,"undervotes":294,"overvotes":157,"sourceRow":119,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[2,127,7,1153,28,17,6,19,18,1745,2,5,317,37,6,16,8,93,13,34,49,31,17,210,16,17,9,2371,6,5053,22,584,15,8,22,6,2,4,30,71,6243,82,28,21,9,4,2,18,17,6,23,7,7,3,13,4,4,7,6,40,89,73],"totalVotes":18902,"registeredVoters":46734,"undervotes":321,"overvotes":136,"sourceRow":123,"partiallyEligible":false}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},{"id":"lieutenant-governor","title":"LIEUTENANT GOVERNOR","sourceSheet":"Sheet3","candidates":[{"name":"MICHAEL TUBBS","party":"DEM","sourceColumn":7},{"name":"TIM MYERS","party":"DEM","sourceColumn":9},{"name":"FIONA MA","party":"DEM","sourceColumn":11},{"name":"OLIVER MA","party":"DEM","sourceColumn":14},{"name":"EBIE LYNCH","party":"REP","sourceColumn":16},{"name":"JEYSON LOPEZ","party":"DEM","sourceColumn":19},{"name":"GLORIA ROMERO","party":"REP","sourceColumn":21},{"name":"RAKESH CHRISTIAN","party":"NPP","sourceColumn":23},{"name":"DAVID COLLENBERG","party":"REP","sourceColumn":25},{"name":"SEAN COLLINSON","party":"NPP","sourceColumn":27},{"name":"JOSH FRYDAY","party":"DEM","sourceColumn":29},{"name":"DAVID FENNELL","party":"REP","sourceColumn":31},{"name":"ALICE STEK","party":"PF","sourceColumn":33},{"name":"SKIP SHELTON","party":"REP","sourceColumn":35},{"name":"ABDUR RAHMAN SIKDER","party":"DEM","sourceColumn":37},{"name":"JANELLE KELLMAN","party":"DEM","sourceColumn":39},{"name":"Write-in","party":null,"sourceColumn":41}],"citywide":{"votes":[48309,1945,70164,36244,1958,555,13236,258,3546,429,54089,5133,3325,3492,668,5105,0],"totalVotes":248456,"registeredVoters":533546,"undervotes":22633,"overvotes":397,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[4044,132,7640,3779,230,19,1392,27,401,46,5299,528,315,392,43,496,0],"totalVotes":24783,"registeredVoters":48250,"undervotes":2151,"overvotes":36,"sourceRow":83,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[4382,146,6385,2353,159,14,1764,17,308,31,7497,565,221,356,44,627,0],"totalVotes":24869,"registeredVoters":50721,"undervotes":2481,"overvotes":29,"sourceRow":87,"partiallyEligible":false},{"district":3,"eligible":true,"votes":[3515,178,5145,2315,123,35,1058,18,270,39,4485,454,292,289,54,433,0],"totalVotes":18703,"registeredVoters":44896,"undervotes":2205,"overvotes":34,"sourceRow":91,"partiallyEligible":false},{"district":4,"eligible":true,"votes":[3061,134,9089,3193,373,28,2038,27,563,70,4160,834,314,516,61,360,0],"totalVotes":24821,"registeredVoters":50790,"undervotes":2246,"overvotes":40,"sourceRow":95,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[4786,224,4571,3869,89,46,649,17,217,28,4361,236,359,193,88,490,0],"totalVotes":20223,"registeredVoters":46287,"undervotes":2036,"overvotes":57,"sourceRow":99,"partiallyEligible":false},{"district":6,"eligible":true,"votes":[3044,183,3729,1875,115,25,769,22,205,31,3581,301,213,202,82,337,0],"totalVotes":14714,"registeredVoters":38903,"undervotes":1520,"overvotes":29,"sourceRow":103,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[4957,157,8986,3242,213,44,1729,23,441,57,5825,742,319,440,57,474,0],"totalVotes":27706,"registeredVoters":52295,"undervotes":2223,"overvotes":21,"sourceRow":107,"partiallyEligible":false},{"district":8,"eligible":true,"votes":[8940,208,8811,5493,101,33,851,17,198,27,8815,334,402,183,64,846,0],"totalVotes":35323,"registeredVoters":60213,"undervotes":3221,"overvotes":28,"sourceRow":111,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[5401,214,5131,5594,142,133,806,25,241,24,4048,274,413,227,63,441,0],"totalVotes":23177,"registeredVoters":49268,"undervotes":1898,"overvotes":44,"sourceRow":115,"partiallyEligible":false},{"district":10,"eligible":true,"votes":[3600,178,4681,1948,161,73,859,39,275,41,3066,352,222,249,56,304,0],"totalVotes":16104,"registeredVoters":45189,"undervotes":1369,"overvotes":36,"sourceRow":119,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[2579,191,5996,2583,252,105,1321,26,427,35,2952,513,255,445,56,297,0],"totalVotes":18033,"registeredVoters":46734,"undervotes":1283,"overvotes":43,"sourceRow":123,"partiallyEligible":false}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},{"id":"secretary-of-state","title":"SECRETARY OF STATE","sourceSheet":"Sheet4","candidates":[{"name":"DONALD P. (DON) WAGNER","party":"REP","sourceColumn":7},{"name":"SHIRLEY N. WEBER","party":"DEM","sourceColumn":9},{"name":"GARY N. BLENNER","party":"GRN","sourceColumn":11},{"name":"MICHAEL FEINSTEIN","party":"GRN","sourceColumn":14},{"name":"Write-in","party":null,"sourceColumn":16}],"citywide":{"votes":[30445,200250,6306,6698,0],"totalVotes":243699,"registeredVoters":533546,"undervotes":27724,"overvotes":63,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[3266,19459,696,667,0],"totalVotes":24088,"registeredVoters":48250,"undervotes":2877,"overvotes":5,"sourceRow":83,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[3594,19907,457,547,0],"totalVotes":24505,"registeredVoters":50721,"undervotes":2872,"overvotes":2,"sourceRow":87,"partiallyEligible":false},{"district":3,"eligible":true,"votes":[2501,14980,444,501,0],"totalVotes":18426,"registeredVoters":44896,"undervotes":2505,"overvotes":11,"sourceRow":91,"partiallyEligible":false},{"district":4,"eligible":true,"votes":[4878,17279,767,756,0],"totalVotes":23680,"registeredVoters":50790,"undervotes":3424,"overvotes":3,"sourceRow":95,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[1486,17362,551,653,0],"totalVotes":20052,"registeredVoters":46287,"undervotes":2254,"overvotes":10,"sourceRow":99,"partiallyEligible":false},{"district":6,"eligible":true,"votes":[1748,11914,412,455,0],"totalVotes":14529,"registeredVoters":38903,"undervotes":1726,"overvotes":8,"sourceRow":103,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[4023,21693,575,693,0],"totalVotes":26984,"registeredVoters":52295,"undervotes":2963,"overvotes":3,"sourceRow":107,"partiallyEligible":false},{"district":8,"eligible":true,"votes":[1901,31982,662,681,0],"totalVotes":35226,"registeredVoters":60213,"undervotes":3341,"overvotes":5,"sourceRow":111,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[1779,19944,651,632,0],"totalVotes":23006,"registeredVoters":49268,"undervotes":2107,"overvotes":6,"sourceRow":115,"partiallyEligible":false},{"district":10,"eligible":true,"votes":[2091,12710,433,479,0],"totalVotes":15713,"registeredVoters":45189,"undervotes":1790,"overvotes":6,"sourceRow":119,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[3178,13020,658,634,0],"totalVotes":17490,"registeredVoters":46734,"undervotes":1865,"overvotes":4,"sourceRow":123,"partiallyEligible":false}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0,0]}},{"id":"controller","title":"CONTROLLER","sourceSheet":"Sheet5","candidates":[{"name":"HERB W MORGAN","party":"REP","sourceColumn":7},{"name":"MALIA M. COHEN","party":"DEM","sourceColumn":9},{"name":"MEGHANN ADAMS","party":"PF","sourceColumn":11},{"name":"Write-in","party":null,"sourceColumn":14}],"citywide":{"votes":[33461,175509,32710,0],"totalVotes":241680,"registeredVoters":533546,"undervotes":29766,"overvotes":40,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[3715,17014,3320,0],"totalVotes":24049,"registeredVoters":48250,"undervotes":2918,"overvotes":3,"sourceRow":83,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[4214,18162,1940,0],"totalVotes":24316,"registeredVoters":50721,"undervotes":3062,"overvotes":1,"sourceRow":87,"partiallyEligible":false},{"district":3,"eligible":true,"votes":[2667,13362,2130,0],"totalVotes":18159,"registeredVoters":44896,"undervotes":2776,"overvotes":7,"sourceRow":91,"partiallyEligible":false},{"district":4,"eligible":true,"votes":[5323,15683,2790,0],"totalVotes":23796,"registeredVoters":50790,"undervotes":3308,"overvotes":3,"sourceRow":95,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[1632,14464,3645,0],"totalVotes":19741,"registeredVoters":46287,"undervotes":2570,"overvotes":5,"sourceRow":99,"partiallyEligible":false},{"district":6,"eligible":true,"votes":[1896,10739,1732,0],"totalVotes":14367,"registeredVoters":38903,"undervotes":1889,"overvotes":7,"sourceRow":103,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[4363,19748,2783,0],"totalVotes":26894,"registeredVoters":52295,"undervotes":3054,"overvotes":2,"sourceRow":107,"partiallyEligible":false},{"district":8,"eligible":true,"votes":[2238,27400,5051,0],"totalVotes":34689,"registeredVoters":60213,"undervotes":3881,"overvotes":2,"sourceRow":111,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[1875,15541,5180,0],"totalVotes":22596,"registeredVoters":49268,"undervotes":2518,"overvotes":5,"sourceRow":115,"partiallyEligible":false},{"district":10,"eligible":true,"votes":[2167,11614,1853,0],"totalVotes":15634,"registeredVoters":45189,"undervotes":1873,"overvotes":2,"sourceRow":119,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[3371,11782,2286,0],"totalVotes":17439,"registeredVoters":46734,"undervotes":1917,"overvotes":3,"sourceRow":123,"partiallyEligible":false}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0]}},{"id":"treasurer","title":"TREASURER","sourceSheet":"Sheet6","candidates":[{"name":"GLENN TURNER","party":"GRN","sourceColumn":7},{"name":"JENNIFER HAWKS","party":"REP","sourceColumn":9},{"name":"ANNA M. CABALLERO","party":"DEM","sourceColumn":11},{"name":"DAVID SERPA","party":"REP","sourceColumn":14},{"name":"ELENI KOUNALAKIS","party":"DEM","sourceColumn":16},{"name":"TONY VAZQUEZ","party":"DEM","sourceColumn":19},{"name":"Write-in","party":null,"sourceColumn":21}],"citywide":{"votes":[8031,18940,32754,10272,150941,11468,0],"totalVotes":232406,"registeredVoters":533546,"undervotes":38910,"overvotes":170,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[821,2041,3391,1122,14603,1037,0],"totalVotes":23015,"registeredVoters":48250,"undervotes":3947,"overvotes":8,"sourceRow":83,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[499,2243,2900,1001,16463,730,0],"totalVotes":23836,"registeredVoters":50721,"undervotes":3534,"overvotes":9,"sourceRow":87,"partiallyEligible":false},{"district":3,"eligible":true,"votes":[609,1494,2214,787,11737,878,0],"totalVotes":17719,"registeredVoters":44896,"undervotes":3200,"overvotes":23,"sourceRow":91,"partiallyEligible":false},{"district":4,"eligible":true,"votes":[770,3219,3183,1618,12926,1113,0],"totalVotes":22829,"registeredVoters":50790,"undervotes":4267,"overvotes":11,"sourceRow":95,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[835,931,3132,549,12283,1016,0],"totalVotes":18746,"registeredVoters":46287,"undervotes":3547,"overvotes":23,"sourceRow":99,"partiallyEligible":false},{"district":6,"eligible":true,"votes":[506,1039,1984,612,9088,772,0],"totalVotes":14001,"registeredVoters":38903,"undervotes":2249,"overvotes":13,"sourceRow":103,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[728,2466,3180,1264,17387,1066,0],"totalVotes":26091,"registeredVoters":52295,"undervotes":3849,"overvotes":10,"sourceRow":107,"partiallyEligible":false},{"district":8,"eligible":true,"votes":[1082,1223,4250,540,24785,1267,0],"totalVotes":33147,"registeredVoters":60213,"undervotes":5414,"overvotes":11,"sourceRow":111,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[1059,1046,3584,702,13204,1362,0],"totalVotes":20957,"registeredVoters":49268,"undervotes":4143,"overvotes":19,"sourceRow":115,"partiallyEligible":false},{"district":10,"eligible":true,"votes":[522,1268,2162,828,9427,960,0],"totalVotes":15167,"registeredVoters":45189,"undervotes":2320,"overvotes":22,"sourceRow":119,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[600,1970,2774,1249,9038,1267,0],"totalVotes":16898,"registeredVoters":46734,"undervotes":2440,"overvotes":21,"sourceRow":123,"partiallyEligible":false}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0,0,0,0]}},{"id":"attorney-general","title":"ATTORNEY GENERAL","sourceSheet":"Sheet7","candidates":[{"name":"MARJORIE MIKELS","party":"GRN","sourceColumn":7},{"name":"ROB BONTA","party":"DEM","sourceColumn":9},{"name":"MICHAEL E. GATES","party":"REP","sourceColumn":11},{"name":"Write-in","party":null,"sourceColumn":14}],"citywide":{"votes":[17337,189269,35004,0],"totalVotes":241610,"registeredVoters":533546,"undervotes":29834,"overvotes":42,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[1850,18346,3704,0],"totalVotes":23900,"registeredVoters":48250,"undervotes":3064,"overvotes":6,"sourceRow":83,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[1246,19422,4129,0],"totalVotes":24797,"registeredVoters":50721,"undervotes":2581,"overvotes":1,"sourceRow":87,"partiallyEligible":false},{"district":3,"eligible":true,"votes":[1338,14232,2800,0],"totalVotes":18370,"registeredVoters":44896,"undervotes":2566,"overvotes":6,"sourceRow":91,"partiallyEligible":false},{"district":4,"eligible":true,"votes":[1851,16388,5500,0],"totalVotes":23739,"registeredVoters":50790,"undervotes":3367,"overvotes":1,"sourceRow":95,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[1756,15944,1881,0],"totalVotes":19581,"registeredVoters":46287,"undervotes":2732,"overvotes":3,"sourceRow":99,"partiallyEligible":false},{"district":6,"eligible":true,"votes":[1065,11379,2090,0],"totalVotes":14534,"registeredVoters":38903,"undervotes":1723,"overvotes":6,"sourceRow":103,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[1632,20922,4526,0],"totalVotes":27080,"registeredVoters":52295,"undervotes":2866,"overvotes":4,"sourceRow":107,"partiallyEligible":false},{"district":8,"eligible":true,"votes":[2083,30416,2215,0],"totalVotes":34714,"registeredVoters":60213,"undervotes":3853,"overvotes":5,"sourceRow":111,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[2005,17970,2055,0],"totalVotes":22030,"registeredVoters":49268,"undervotes":3084,"overvotes":5,"sourceRow":115,"partiallyEligible":false},{"district":10,"eligible":true,"votes":[1143,11945,2489,0],"totalVotes":15577,"registeredVoters":45189,"undervotes":1929,"overvotes":3,"sourceRow":119,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[1368,12305,3615,0],"totalVotes":17288,"registeredVoters":46734,"undervotes":2069,"overvotes":2,"sourceRow":123,"partiallyEligible":false}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0]}},{"id":"insurance-commissioner","title":"INSURANCE COMMISSIONER","sourceSheet":"Sheet8","candidates":[{"name":"PATRICK WOLFF","party":"DEM","sourceColumn":7},{"name":"SEAN LEE","party":"REP","sourceColumn":9},{"name":"KEITH W. DAVIS","party":"AI","sourceColumn":11},{"name":"ROBERT P HOWELL","party":"REP","sourceColumn":14},{"name":"STEVEN CRAIG BRADFORD","party":"DEM","sourceColumn":16},{"name":"MERRITT FARREN","party":"REP","sourceColumn":19},{"name":"BEN ALLEN","party":"DEM","sourceColumn":21},{"name":"ERIC THOR AARNIO","party":"REP","sourceColumn":23},{"name":"STACY A. KORSGADEN","party":"REP","sourceColumn":25},{"name":"JANE KIM","party":"DEM","sourceColumn":27},{"name":"EDUARDO \"LALO\" VARGAS","party":"PF","sourceColumn":29},{"name":"Write-in","party":null,"sourceColumn":31}],"citywide":{"votes":[54304,5589,1365,5609,4472,7417,42995,1778,9996,100173,5669,0],"totalVotes":239367,"registeredVoters":533546,"undervotes":31749,"overvotes":370,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[5330,692,128,609,371,615,3779,219,1134,10358,516,0],"totalVotes":23751,"registeredVoters":48250,"undervotes":3185,"overvotes":34,"sourceRow":83,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[7700,448,111,719,420,597,4587,204,1250,7340,313,0],"totalVotes":23689,"registeredVoters":50721,"undervotes":3659,"overvotes":31,"sourceRow":87,"partiallyEligible":false},{"district":3,"eligible":true,"votes":[4346,424,117,491,311,688,3429,91,743,7037,427,0],"totalVotes":18104,"registeredVoters":44896,"undervotes":2798,"overvotes":40,"sourceRow":91,"partiallyEligible":false},{"district":4,"eligible":true,"votes":[4109,1140,169,796,372,879,3435,413,1741,10077,479,0],"totalVotes":23610,"registeredVoters":50790,"undervotes":3467,"overvotes":30,"sourceRow":95,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[4118,218,136,300,424,482,3430,95,487,9368,546,0],"totalVotes":19604,"registeredVoters":46287,"undervotes":2674,"overvotes":38,"sourceRow":99,"partiallyEligible":false},{"district":6,"eligible":true,"votes":[3480,259,105,355,317,571,2779,62,504,5392,413,0],"totalVotes":14237,"registeredVoters":38903,"undervotes":1998,"overvotes":28,"sourceRow":103,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[6212,710,124,770,392,844,5035,244,1344,10347,526,0],"totalVotes":26548,"registeredVoters":52295,"undervotes":3368,"overvotes":34,"sourceRow":107,"partiallyEligible":false},{"district":8,"eligible":true,"votes":[9286,254,86,367,478,562,6711,77,610,15118,611,0],"totalVotes":34160,"registeredVoters":60213,"undervotes":4378,"overvotes":34,"sourceRow":111,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[4067,262,79,317,399,630,3802,84,558,11685,763,0],"totalVotes":22646,"registeredVoters":49268,"undervotes":2444,"overvotes":29,"sourceRow":115,"partiallyEligible":false},{"district":10,"eligible":true,"votes":[3137,446,138,356,537,697,2995,90,586,6136,471,0],"totalVotes":15589,"registeredVoters":45189,"undervotes":1888,"overvotes":32,"sourceRow":119,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[2519,736,172,529,451,852,3013,199,1039,7315,604,0],"totalVotes":17429,"registeredVoters":46734,"undervotes":1890,"overvotes":40,"sourceRow":123,"partiallyEligible":false}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0,0,0,0,0,0,0,0,0]}},{"id":"board-of-equalization-member-district-2","title":"BOARD OF EQUALIZATION MEMBER, DISTRICT 2","sourceSheet":"Sheet9","candidates":[{"name":"JOHN PIMENTEL","party":"DEM","sourceColumn":7},{"name":"JOHN W. ZARUKA","party":"REP","sourceColumn":9},{"name":"MARK MCCOMAS","party":"REP","sourceColumn":11},{"name":"J BRETT MARYMEE","party":"REP","sourceColumn":14},{"name":"SALLY J. LIEBER","party":"DEM","sourceColumn":16},{"name":"BILL SHIREMAN","party":"REP","sourceColumn":19},{"name":"Write-in","party":null,"sourceColumn":21}],"citywide":{"votes":[32335,3574,5075,9216,162680,14370,0],"totalVotes":227250,"registeredVoters":533546,"undervotes":43953,"overvotes":252,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[3034,271,464,1096,16132,1566,0],"totalVotes":22563,"registeredVoters":48250,"undervotes":4389,"overvotes":21,"sourceRow":83,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[3355,250,508,1146,15707,1566,0],"totalVotes":22532,"registeredVoters":50721,"undervotes":4817,"overvotes":16,"sourceRow":87,"partiallyEligible":false},{"district":3,"eligible":true,"votes":[2356,382,454,645,12165,1071,0],"totalVotes":17073,"registeredVoters":44896,"undervotes":3836,"overvotes":26,"sourceRow":91,"partiallyEligible":false},{"district":4,"eligible":true,"votes":[2766,323,657,1647,14305,2415,0],"totalVotes":22113,"registeredVoters":50790,"undervotes":5004,"overvotes":20,"sourceRow":95,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[2496,293,355,464,14356,719,0],"totalVotes":18683,"registeredVoters":46287,"undervotes":3599,"overvotes":33,"sourceRow":99,"partiallyEligible":false},{"district":6,"eligible":true,"votes":[2058,294,321,448,9698,821,0],"totalVotes":13640,"registeredVoters":38903,"undervotes":2601,"overvotes":18,"sourceRow":103,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[3900,377,592,1162,17217,1851,0],"totalVotes":25099,"registeredVoters":52295,"undervotes":4826,"overvotes":16,"sourceRow":107,"partiallyEligible":false},{"district":8,"eligible":true,"votes":[4827,247,375,534,25469,877,0],"totalVotes":32329,"registeredVoters":60213,"undervotes":6211,"overvotes":19,"sourceRow":111,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[3044,336,366,462,16459,885,0],"totalVotes":21552,"registeredVoters":49268,"undervotes":3553,"overvotes":17,"sourceRow":115,"partiallyEligible":false},{"district":10,"eligible":true,"votes":[2102,391,463,591,10357,1019,0],"totalVotes":14923,"registeredVoters":45189,"undervotes":2550,"overvotes":36,"sourceRow":119,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[2397,410,520,1021,10815,1580,0],"totalVotes":16743,"registeredVoters":46734,"undervotes":2567,"overvotes":30,"sourceRow":123,"partiallyEligible":false}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0,0,0,0]}},{"id":"united-states-representative-district-11","title":"UNITED STATES REPRESENTATIVE, DISTRICT 11","sourceSheet":"Sheet10","candidates":[{"name":"SCOTT WIENER","party":"DEM","sourceColumn":7},{"name":"NATHAN DEER","party":"NPP","sourceColumn":9},{"name":"OMED HAMID","party":"DEM","sourceColumn":11},{"name":"GREGORY M HAYNES","party":"DEM","sourceColumn":14},{"name":"MARIE HURABIELL","party":"DEM","sourceColumn":16},{"name":"JOHN \"GUS\" BUFFLER","party":"DEM","sourceColumn":19},{"name":"CONNIE CHAN","party":"DEM","sourceColumn":21},{"name":"SAIKAT CHAKRABARTI","party":"DEM","sourceColumn":23},{"name":"JINGCHAO XIONG","party":"REP","sourceColumn":25},{"name":"KEITH FREEDMAN","party":"DEM","sourceColumn":27},{"name":"DAVID GANEZER","party":"REP","sourceColumn":29},{"name":"Write-in","party":null,"sourceColumn":31}],"citywide":{"votes":[95816,649,401,2025,9410,942,69899,42060,4238,591,9299,7],"totalVotes":235337,"registeredVoters":470299,"undervotes":10659,"overvotes":428,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[9039,56,15,166,1361,86,8973,4432,537,81,1132,1],"totalVotes":25879,"registeredVoters":48250,"undervotes":1068,"overvotes":26,"sourceRow":79,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[12246,56,35,161,1940,126,5913,3906,367,69,1237,0],"totalVotes":26056,"registeredVoters":50721,"undervotes":1274,"overvotes":35,"sourceRow":83,"partiallyEligible":false},{"district":3,"eligible":true,"votes":[7962,78,59,177,714,87,5661,3733,369,55,927,1],"totalVotes":19823,"registeredVoters":44896,"undervotes":1074,"overvotes":38,"sourceRow":87,"partiallyEligible":false},{"district":4,"eligible":true,"votes":[7391,75,14,206,1426,112,9591,3996,1025,72,1715,1],"totalVotes":25624,"registeredVoters":50790,"undervotes":1477,"overvotes":36,"sourceRow":91,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[8595,64,59,316,496,89,6232,4627,265,59,547,1],"totalVotes":21350,"registeredVoters":46287,"undervotes":893,"overvotes":72,"sourceRow":95,"partiallyEligible":false},{"district":6,"eligible":true,"votes":[6852,64,56,184,478,90,3448,3272,320,46,575,0],"totalVotes":15385,"registeredVoters":38903,"undervotes":824,"overvotes":50,"sourceRow":99,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[11225,86,30,213,1579,136,8571,4335,593,71,1425,0],"totalVotes":28264,"registeredVoters":51639,"undervotes":1330,"overvotes":38,"sourceRow":103,"partiallyEligible":true},{"district":8,"eligible":true,"votes":[19140,56,39,152,726,87,9737,6433,199,50,644,1],"totalVotes":37264,"registeredVoters":60213,"undervotes":1242,"overvotes":53,"sourceRow":107,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[7619,47,45,202,334,76,7543,4518,202,37,499,2],"totalVotes":21124,"registeredVoters":40969,"undervotes":747,"overvotes":36,"sourceRow":111,"partiallyEligible":true},{"district":10,"eligible":true,"votes":[5332,63,47,221,318,51,3734,2597,286,47,516,0],"totalVotes":13212,"registeredVoters":34328,"undervotes":665,"overvotes":43,"sourceRow":115,"partiallyEligible":true},{"district":11,"eligible":true,"votes":[415,4,2,27,38,2,496,211,75,4,82,0],"totalVotes":1356,"registeredVoters":3303,"undervotes":65,"overvotes":1,"sourceRow":119,"partiallyEligible":true}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0,0,0,0,0,0,0,0,0]}},{"id":"united-states-representative-district-15","title":"UNITED STATES REPRESENTATIVE, DISTRICT 15","sourceSheet":"Sheet11","candidates":[{"name":"KEVIN MULLIN","party":"DEM","sourceColumn":7},{"name":"ANTHONY VAN DANG","party":"DEM","sourceColumn":9},{"name":"CHARLES HOELTER","party":"REP","sourceColumn":11},{"name":"JIM GARRITY","party":"NPP","sourceColumn":14},{"name":"MANTOSH KUMAR","party":"DEM","sourceColumn":16},{"name":"Write-in","party":null,"sourceColumn":19}],"citywide":{"votes":[13691,2036,3530,947,973,0],"totalVotes":21177,"registeredVoters":63247,"undervotes":3829,"overvotes":25,"sourceRow":9},"districts":[{"district":1,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":2,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":3,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":4,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":5,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":6,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[153,24,56,5,14,0],"totalVotes":252,"registeredVoters":656,"undervotes":57,"overvotes":0,"sourceRow":63,"partiallyEligible":true},{"district":8,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":9,"eligible":true,"votes":[1718,232,491,150,111,0],"totalVotes":2702,"registeredVoters":8299,"undervotes":508,"overvotes":5,"sourceRow":67,"partiallyEligible":true},{"district":10,"eligible":true,"votes":[1909,285,560,140,140,0],"totalVotes":3034,"registeredVoters":10861,"undervotes":549,"overvotes":6,"sourceRow":71,"partiallyEligible":true},{"district":11,"eligible":true,"votes":[9911,1495,2423,652,708,0],"totalVotes":15189,"registeredVoters":43431,"undervotes":2715,"overvotes":14,"sourceRow":75,"partiallyEligible":true}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0,0,0,0]}},{"id":"state-assembly-member-district-17","title":"STATE ASSEMBLY MEMBER, DISTRICT 17","sourceSheet":"Sheet14","candidates":[{"name":"MATT HANEY","party":"DEM","sourceColumn":7},{"name":"Write-in","party":null,"sourceColumn":9}],"citywide":{"votes":[106260,612],"totalVotes":106872,"registeredVoters":302434,"undervotes":41186,"overvotes":2,"sourceRow":9},"districts":[{"district":1,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[2267,21],"totalVotes":2288,"registeredVoters":5465,"undervotes":801,"overvotes":0,"sourceRow":63,"partiallyEligible":true},{"district":3,"eligible":true,"votes":[12112,102],"totalVotes":12214,"registeredVoters":38266,"undervotes":5303,"overvotes":0,"sourceRow":67,"partiallyEligible":true},{"district":4,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[11207,62],"totalVotes":11269,"registeredVoters":34898,"undervotes":4430,"overvotes":0,"sourceRow":71,"partiallyEligible":true},{"district":6,"eligible":true,"votes":[11860,65],"totalVotes":11925,"registeredVoters":38903,"undervotes":4334,"overvotes":0,"sourceRow":75,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[7673,86],"totalVotes":7759,"registeredVoters":17955,"undervotes":3429,"overvotes":0,"sourceRow":79,"partiallyEligible":true},{"district":8,"eligible":true,"votes":[24694,100],"totalVotes":24794,"registeredVoters":51977,"undervotes":8461,"overvotes":1,"sourceRow":83,"partiallyEligible":true},{"district":9,"eligible":true,"votes":[17597,87],"totalVotes":17684,"registeredVoters":49268,"undervotes":7438,"overvotes":0,"sourceRow":87,"partiallyEligible":false},{"district":10,"eligible":true,"votes":[12733,59],"totalVotes":12792,"registeredVoters":45189,"undervotes":4716,"overvotes":1,"sourceRow":91,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[6117,30],"totalVotes":6147,"registeredVoters":20513,"undervotes":2274,"overvotes":0,"sourceRow":95,"partiallyEligible":true}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0]}},{"id":"state-assembly-member-district-19","title":"STATE ASSEMBLY MEMBER, DISTRICT 19","sourceSheet":"Sheet16","candidates":[{"name":"PHILIP LOUIS WING","party":"REP","sourceColumn":7},{"name":"CATHERINE STEFANI","party":"DEM","sourceColumn":9},{"name":"Write-in","party":null,"sourceColumn":11}],"citywide":{"votes":[15946,88138,0],"totalVotes":104084,"registeredVoters":231112,"undervotes":19304,"overvotes":7,"sourceRow":9},"districts":[{"district":1,"eligible":true,"votes":[3209,19347,0],"totalVotes":22556,"registeredVoters":48250,"undervotes":4416,"overvotes":1,"sourceRow":63,"partiallyEligible":false},{"district":2,"eligible":true,"votes":[2738,18165,0],"totalVotes":20903,"registeredVoters":45256,"undervotes":3372,"overvotes":1,"sourceRow":67,"partiallyEligible":true},{"district":3,"eligible":true,"votes":[308,2540,0],"totalVotes":2848,"registeredVoters":6630,"undervotes":570,"overvotes":0,"sourceRow":71,"partiallyEligible":true},{"district":4,"eligible":true,"votes":[5049,17563,0],"totalVotes":22612,"registeredVoters":50790,"undervotes":4523,"overvotes":2,"sourceRow":75,"partiallyEligible":false},{"district":5,"eligible":true,"votes":[265,5203,0],"totalVotes":5468,"registeredVoters":11389,"undervotes":1147,"overvotes":1,"sourceRow":79,"partiallyEligible":true},{"district":6,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":7,"eligible":true,"votes":[2231,13653,0],"totalVotes":15884,"registeredVoters":34340,"undervotes":2869,"overvotes":0,"sourceRow":83,"partiallyEligible":true},{"district":8,"eligible":true,"votes":[218,4240,0],"totalVotes":4458,"registeredVoters":8236,"undervotes":844,"overvotes":1,"sourceRow":87,"partiallyEligible":true},{"district":9,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":10,"eligible":false,"votes":null,"totalVotes":null,"undervotes":null,"overvotes":null,"registeredVoters":null,"sourceRow":null,"partiallyEligible":false},{"district":11,"eligible":true,"votes":[1928,7427,0],"totalVotes":9355,"registeredVoters":26221,"undervotes":1563,"overvotes":1,"sourceRow":91,"partiallyEligible":true}],"unassigned":{"totalVotes":0,"undervotes":0,"overvotes":0,"votes":[0,0,0]}}]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# San Francisco candidate results
|
|
2
|
+
|
|
3
|
+
`catalog.json` lists six certified election snapshots. Each election JSON is
|
|
4
|
+
separate so the Pages explorer fetches only the selected year. Local builds
|
|
5
|
+
ship these files as optional JSON subpaths. For example:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import results from '@kahwee/sf-map-svg/data/candidates/2024-11-05.json' with { type: 'json' };
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
No candidate results are imported by the default map renderer. These are
|
|
12
|
+
**San Francisco votes**, not statewide totals or votes by individual people.
|
|
13
|
+
The `districtYear` is the supervisorial map vintage, not a congressional
|
|
14
|
+
boundary. For House and state legislative contests, an `eligible: false` row
|
|
15
|
+
means that the workbook had no voters from that supervisorial district for
|
|
16
|
+
the contest. Do not convert it to zero support. Districts can be partly inside
|
|
17
|
+
a congressional district; the JSON does not provide congressional polygons.
|
|
18
|
+
`partiallyEligible` compares a contest's district registration with the same
|
|
19
|
+
election's citywide presidential or gubernatorial registration, so a client
|
|
20
|
+
can mark districts that straddle a contest boundary.
|
|
21
|
+
|
|
22
|
+
`contests` hold an election-scoped ID, official workbook title, source sheet,
|
|
23
|
+
candidate names and party labels, a citywide vote vector, and eleven
|
|
24
|
+
supervisorial district rows. Candidate positions in every `votes` array match
|
|
25
|
+
the `candidates` array. `totalVotes` includes reported write-ins. A candidate
|
|
26
|
+
share is `votes[index] / totalVotes` when the total is positive. The
|
|
27
|
+
`unassigned` object records rare county cumulative votes that the official
|
|
28
|
+
workbook does not assign to a supervisorial district; the 2024 Treasurer
|
|
29
|
+
contest has one such vote and two undervotes. These remain in citywide totals.
|
|
30
|
+
|
|
31
|
+
`scripts/import-candidate-results.py` reads the official final Excel workbooks,
|
|
32
|
+
records their SHA-256 digests, and checks candidate and district sums. Download
|
|
33
|
+
the six workbook URLs in `SOURCES.md` as `2016.xlsx`, `2018.xlsx`,
|
|
34
|
+
`2020.xlsx`, `2022.xlsx`, `2024.xlsx`, and `2026.xlsx` into a local folder, then run:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
uv run --with openpyxl python3 scripts/import-candidate-results.py /path/to/folder
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This archive includes federal, statewide, and state legislative candidate
|
|
41
|
+
contests with SF votes in these six elections. It is not an exhaustive
|
|
42
|
+
archive of every local office or election. Sources and limitations are in
|
|
43
|
+
`SOURCES.md`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"elections": [
|
|
3
|
+
{
|
|
4
|
+
"date": "2016-11-08",
|
|
5
|
+
"label": "2016 November general",
|
|
6
|
+
"file": "2016-11-08.json",
|
|
7
|
+
"contestCount": 4
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"date": "2018-11-06",
|
|
11
|
+
"label": "2018 November general",
|
|
12
|
+
"file": "2018-11-06.json",
|
|
13
|
+
"contestCount": 11
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"date": "2020-11-03",
|
|
17
|
+
"label": "2020 November general",
|
|
18
|
+
"file": "2020-11-03.json",
|
|
19
|
+
"contestCount": 6
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"date": "2022-11-08",
|
|
23
|
+
"label": "2022 November general",
|
|
24
|
+
"file": "2022-11-08.json",
|
|
25
|
+
"contestCount": 14
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"date": "2024-11-05",
|
|
29
|
+
"label": "2024 November general",
|
|
30
|
+
"file": "2024-11-05.json",
|
|
31
|
+
"contestCount": 9
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"date": "2026-06-02",
|
|
35
|
+
"label": "2026 June primary",
|
|
36
|
+
"file": "2026-06-02.json",
|
|
37
|
+
"contestCount": 12
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Geometry, NeighborhoodSource } from '../data/types.js';
|
|
2
|
-
import type { CameraOptions, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodExplorerOptions, NeighborhoodSelection } from './types.js';
|
|
3
|
-
export type MapAppearance = Pick<NeighborhoodExplorerOptions, 'theme' | 'colors' | 'labelStyle' | 'areaStyle' | 'labelSize' | 'style' | 'markerRadius' | 'markerHitSize' | 'markerColor' | 'selectedMarkerColor'>;
|
|
2
|
+
import type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodExplorerOptions, NeighborhoodSelection } from './types.js';
|
|
3
|
+
export type MapAppearance = Pick<NeighborhoodExplorerOptions, 'theme' | 'colors' | 'labelStyle' | 'areaStyle' | 'districtStyle' | 'labelSize' | 'style' | 'markerRadius' | 'markerHitSize' | 'markerColor' | 'selectedMarkerColor'>;
|
|
4
4
|
export type MapControls = NonNullable<NeighborhoodExplorerOptions['controls']>;
|
|
5
5
|
/** Omitted groups/keys retain their values; undefined groups reset all keys in that group. */
|
|
6
6
|
export interface MapConfiguration {
|
|
@@ -19,6 +19,21 @@ export interface MapConfigurationSnapshot {
|
|
|
19
19
|
controls: MapControls;
|
|
20
20
|
}
|
|
21
21
|
export interface MapEvents {
|
|
22
|
+
districtchange: DistrictSelection | {
|
|
23
|
+
id: null;
|
|
24
|
+
year: DistrictYear;
|
|
25
|
+
district: null;
|
|
26
|
+
};
|
|
27
|
+
districthover: DistrictSelection | {
|
|
28
|
+
id: null;
|
|
29
|
+
year: DistrictYear;
|
|
30
|
+
district: null;
|
|
31
|
+
};
|
|
32
|
+
districtactivate: DistrictSelection;
|
|
33
|
+
districtyearchange: {
|
|
34
|
+
year: DistrictYear;
|
|
35
|
+
previousYear: DistrictYear;
|
|
36
|
+
};
|
|
22
37
|
markerchange: {
|
|
23
38
|
id: string | null;
|
|
24
39
|
marker: MapMarker | null;
|
|
@@ -70,6 +85,12 @@ export interface MapController {
|
|
|
70
85
|
fit?: boolean;
|
|
71
86
|
}): boolean;
|
|
72
87
|
getSelectedNeighborhood(): NeighborhoodSelection | null;
|
|
88
|
+
selectDistrict(id: number | null, options?: CameraOptions & {
|
|
89
|
+
fit?: boolean;
|
|
90
|
+
}): boolean;
|
|
91
|
+
getSelectedDistrict(): DistrictSelection | null;
|
|
92
|
+
setDistrictYear(year: DistrictYear, options?: CameraOptions): void;
|
|
93
|
+
setDistrictStyle(style: ((district: DistrictSelection['district']) => DistrictStyle) | undefined): void;
|
|
73
94
|
setSource(source: NeighborhoodSource): void;
|
|
74
95
|
setMode(mode: NonNullable<MapOptions['mode']>): void;
|
|
75
96
|
setLabels(visible: boolean): void;
|
|
@@ -3,7 +3,7 @@ import { clusterPoints } from './clusters.js';
|
|
|
3
3
|
import { fitBounds, interiorAnchor, layoutLabels, projectedBounds } from './explorer-layout.js';
|
|
4
4
|
import { controlKeys, layerKeys, normalizeFeatures, validateSwitchPatch } from './features.js';
|
|
5
5
|
import { geometryPath } from './geometry.js';
|
|
6
|
-
import { createSFMapWithData, districtColors } from './map-core.js';
|
|
6
|
+
import { createSFMapWithData, districtColors, getLayerPathsWithData } from './map-core.js';
|
|
7
7
|
import { attachNavigation } from './navigation.js';
|
|
8
8
|
import { validateExplorerOptions, validateMarkers, validateOverlays } from './validation.js';
|
|
9
9
|
import { fitViewport, validateViewport } from './viewport.js';
|
|
@@ -38,7 +38,7 @@ let explorerCount = 0;
|
|
|
38
38
|
/** Create an offline, browser-only neighborhood explorer. Call destroy() before disposal. */
|
|
39
39
|
export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
40
40
|
validateExplorerOptions(options);
|
|
41
|
-
let { source = 'realtor', mode = 'neighborhoods', labels = true, neighborhood, year = 2022, theme = 'transit', colors = {}, labelStyle = {}, areaStyle = {}, motion = false, markerEntrance = false, selectedMarkerRing, clustering = false, legend: legendOptions = {}, attribution: attributionMode = 'full', northArrow = false, scaleBar = false, interface: chrome = 'explorer', layers = {}, selectableNeighborhoods = true, labelSize = {}, fitPadding = 24, markers: initialMarkers = [], markerRadius = 6, markerHitSize = 44, markerColor = colors.marker ?? '#245b61', selectedMarkerColor = colors.selected ?? '#f04f32', onMarkerActivate, overlays: initialOverlays = [], onOverlayActivate, style: styleOptions = {}, strings = {}, controls = {}, } = options;
|
|
41
|
+
let { source = 'realtor', mode = 'neighborhoods', labels = true, neighborhood, year = 2022, theme = 'transit', colors = {}, labelStyle = {}, areaStyle = {}, districtStyle: initialDistrictStyle, motion = false, markerEntrance = false, selectedMarkerRing, clustering = false, legend: legendOptions = {}, attribution: attributionMode = 'full', northArrow = false, scaleBar = false, interface: chrome = 'explorer', layers = {}, selectableNeighborhoods = true, labelSize = {}, fitPadding = 24, markers: initialMarkers = [], markerRadius = 6, markerHitSize = 44, markerColor = colors.marker ?? '#245b61', selectedMarkerColor = colors.selected ?? '#f04f32', onMarkerActivate, overlays: initialOverlays = [], onOverlayActivate, style: styleOptions = {}, strings = {}, controls = {}, } = options;
|
|
42
42
|
if (!['neighborhoods', 'districts', 'basemap'].includes(mode))
|
|
43
43
|
throw new RangeError(`Unknown map mode: ${mode}`);
|
|
44
44
|
if (chrome !== 'explorer' && chrome !== 'map')
|
|
@@ -314,6 +314,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
314
314
|
districtLabels: false,
|
|
315
315
|
districtLines: true,
|
|
316
316
|
districtFills: true,
|
|
317
|
+
districtStyle: initialDistrictStyle,
|
|
317
318
|
highways: true,
|
|
318
319
|
keyRoads: true,
|
|
319
320
|
roadLabels: false,
|
|
@@ -365,7 +366,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
365
366
|
if (color)
|
|
366
367
|
path.setAttribute('fill', color);
|
|
367
368
|
}
|
|
368
|
-
|
|
369
|
+
let districtItems = (data.districts?.[year]?.features ?? []).flatMap((feature) => feature.properties.labelPoints.map((point) => ({
|
|
369
370
|
point: map.project(point),
|
|
370
371
|
name: String(feature.properties.district),
|
|
371
372
|
kind: 'district',
|
|
@@ -382,6 +383,171 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
382
383
|
geography.append(areas, stations, labelLayer, overlayLayer, markerLayer);
|
|
383
384
|
let view = [0, 0, 800];
|
|
384
385
|
let selected;
|
|
386
|
+
let selectedDistrict = null;
|
|
387
|
+
let hoveredDistrict = null;
|
|
388
|
+
let districtStyle = initialDistrictStyle;
|
|
389
|
+
let districtTransition;
|
|
390
|
+
root.dataset.year = String(year);
|
|
391
|
+
function districtRow(id) {
|
|
392
|
+
return data.map.districts?.[year]?.find((district) => district.id === id);
|
|
393
|
+
}
|
|
394
|
+
function districtSelection(id) {
|
|
395
|
+
const district = id === null ? undefined : districtRow(id);
|
|
396
|
+
return district
|
|
397
|
+
? { id: district.id, year, district: structuredClone(district) }
|
|
398
|
+
: { id: null, year, district: null };
|
|
399
|
+
}
|
|
400
|
+
function getSelectedDistrict() {
|
|
401
|
+
return selectedDistrict === null
|
|
402
|
+
? null
|
|
403
|
+
: districtSelection(selectedDistrict);
|
|
404
|
+
}
|
|
405
|
+
function updateDistrictAppearance() {
|
|
406
|
+
const rows = data.map.districts?.[year] ?? [];
|
|
407
|
+
for (const row of rows) {
|
|
408
|
+
const styled = districtStyle?.(row);
|
|
409
|
+
const active = selectedDistrict === row.id;
|
|
410
|
+
const hovered = hoveredDistrict === row.id;
|
|
411
|
+
const fill = svg.querySelector(`[data-layer="district-fills"] [data-district="${row.id}"]`);
|
|
412
|
+
const line = svg.querySelector(`[data-layer="district-lines"] [data-district="${row.id}"]`);
|
|
413
|
+
if (fill) {
|
|
414
|
+
fill.setAttribute('fill', styled?.fill ??
|
|
415
|
+
(theme === 'transit' ? (colors.land ?? '#fcfcf8') : districtColors[row.id - 1]));
|
|
416
|
+
if (styled?.opacity === undefined)
|
|
417
|
+
fill.removeAttribute('fill-opacity');
|
|
418
|
+
else
|
|
419
|
+
fill.setAttribute('fill-opacity', String(styled.opacity));
|
|
420
|
+
fill.setAttribute('role', 'button');
|
|
421
|
+
fill.setAttribute('aria-label', `District ${row.id}, ${year}`);
|
|
422
|
+
fill.setAttribute('aria-pressed', String(active));
|
|
423
|
+
fill.setAttribute('tabindex', row.id === 1 ? '0' : '-1');
|
|
424
|
+
fill.style.cursor = 'pointer';
|
|
425
|
+
}
|
|
426
|
+
if (line) {
|
|
427
|
+
line.setAttribute('stroke', active
|
|
428
|
+
? (colors.selected ?? '#f04f32')
|
|
429
|
+
: hovered
|
|
430
|
+
? '#163e56'
|
|
431
|
+
: (styled?.stroke ?? colors.district ?? '#a7b8c0'));
|
|
432
|
+
line.setAttribute('stroke-width', active ? '3' : hovered ? '2.2' : '1.1');
|
|
433
|
+
const lineInteractive = !enabled('districtFills') && enabled('districtLines');
|
|
434
|
+
line.style.pointerEvents = lineInteractive ? 'stroke' : 'none';
|
|
435
|
+
if (lineInteractive) {
|
|
436
|
+
line.setAttribute('role', 'button');
|
|
437
|
+
line.setAttribute('aria-label', `District ${row.id}, ${year}`);
|
|
438
|
+
line.setAttribute('aria-pressed', String(active));
|
|
439
|
+
line.setAttribute('tabindex', row.id === 1 ? '0' : '-1');
|
|
440
|
+
line.style.cursor = 'pointer';
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
line.removeAttribute('role');
|
|
444
|
+
line.removeAttribute('aria-label');
|
|
445
|
+
line.removeAttribute('aria-pressed');
|
|
446
|
+
line.removeAttribute('tabindex');
|
|
447
|
+
}
|
|
448
|
+
if (styled?.opacity === undefined)
|
|
449
|
+
line.removeAttribute('stroke-opacity');
|
|
450
|
+
else
|
|
451
|
+
line.setAttribute('stroke-opacity', String(styled.opacity));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function selectDistrict(id, options = {}) {
|
|
456
|
+
if (destroyed)
|
|
457
|
+
return false;
|
|
458
|
+
validateCameraOptions(options);
|
|
459
|
+
if (id !== null && (!Number.isInteger(id) || !districtRow(id)))
|
|
460
|
+
return false;
|
|
461
|
+
const changed = selectedDistrict !== id;
|
|
462
|
+
selectedDistrict = id;
|
|
463
|
+
if (id === null)
|
|
464
|
+
delete root.dataset.selectedDistrict;
|
|
465
|
+
else
|
|
466
|
+
root.dataset.selectedDistrict = String(id);
|
|
467
|
+
updateDistrictAppearance();
|
|
468
|
+
const selectedRow = id === null ? undefined : districtRow(id);
|
|
469
|
+
if (selectedRow && options.fit)
|
|
470
|
+
fitGeometry(selectedRow.geometry, undefined, options);
|
|
471
|
+
status.textContent =
|
|
472
|
+
id === null ? 'District selection cleared.' : `District ${id} selected, ${year} boundaries.`;
|
|
473
|
+
if (changed)
|
|
474
|
+
root.dispatchEvent(new CustomEvent('districtchange', { bubbles: true, detail: districtSelection(id) }));
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
function setDistrictStyle(style) {
|
|
478
|
+
if (destroyed)
|
|
479
|
+
return;
|
|
480
|
+
if (style !== undefined && typeof style !== 'function')
|
|
481
|
+
throw new TypeError('districtStyle must be a function.');
|
|
482
|
+
createSFMapWithData({ year, districtStyle: style, districtLabels: false }, data.map);
|
|
483
|
+
districtStyle = style;
|
|
484
|
+
updateDistrictAppearance();
|
|
485
|
+
}
|
|
486
|
+
function setDistrictYear(next, options = {}) {
|
|
487
|
+
if (destroyed)
|
|
488
|
+
return;
|
|
489
|
+
validateCameraOptions(options);
|
|
490
|
+
if (![2002, 2012, 2022].includes(next))
|
|
491
|
+
throw new RangeError('District year must be 2002, 2012, or 2022.');
|
|
492
|
+
if (!data.map.districts?.[next] || !data.districts?.[next])
|
|
493
|
+
throw new RangeError(`No ${next} district dataset was supplied.`);
|
|
494
|
+
if (next === year)
|
|
495
|
+
return;
|
|
496
|
+
const geometry = getLayerPathsWithData({ year: next }, data.map);
|
|
497
|
+
createSFMapWithData({ year: next, districtStyle, districtLabels: false }, data.map);
|
|
498
|
+
const previousYear = year;
|
|
499
|
+
districtTransition?.cancel();
|
|
500
|
+
for (const layer of districtLayers) {
|
|
501
|
+
const old = options.animate && !reducedMotion.matches ? layer.cloneNode(true) : null;
|
|
502
|
+
if (old) {
|
|
503
|
+
old.setAttribute('aria-hidden', 'true');
|
|
504
|
+
old.style.pointerEvents = 'none';
|
|
505
|
+
for (const path of old.querySelectorAll('[data-district]')) {
|
|
506
|
+
path.removeAttribute('tabindex');
|
|
507
|
+
path.removeAttribute('role');
|
|
508
|
+
path.removeAttribute('aria-label');
|
|
509
|
+
path.removeAttribute('aria-pressed');
|
|
510
|
+
}
|
|
511
|
+
layer.after(old);
|
|
512
|
+
districtTransition = old.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
513
|
+
duration: options.duration ?? 280,
|
|
514
|
+
easing: 'ease-out',
|
|
515
|
+
});
|
|
516
|
+
districtTransition.finished.then(() => old.remove()).catch(() => old.remove());
|
|
517
|
+
}
|
|
518
|
+
layer.replaceChildren(...geometry.districts.map((district) => svgElement('path', {
|
|
519
|
+
'data-district': district.id,
|
|
520
|
+
d: district.path,
|
|
521
|
+
...(layer.dataset.layer === 'district-fills'
|
|
522
|
+
? { 'fill-rule': 'evenodd' }
|
|
523
|
+
: { fill: 'none', 'stroke-width': 1.1 }),
|
|
524
|
+
})));
|
|
525
|
+
}
|
|
526
|
+
year = next;
|
|
527
|
+
hoveredDistrict = null;
|
|
528
|
+
root.dataset.year = String(year);
|
|
529
|
+
svg.dataset.year = String(year);
|
|
530
|
+
districtItems = (data.districts[next]?.features ?? []).flatMap((feature) => feature.properties.labelPoints.map((point) => ({
|
|
531
|
+
point: map.project(point),
|
|
532
|
+
name: String(feature.properties.district),
|
|
533
|
+
kind: 'district',
|
|
534
|
+
})));
|
|
535
|
+
if (selectedDistrict !== null && !districtRow(selectedDistrict))
|
|
536
|
+
selectedDistrict = null;
|
|
537
|
+
if (selectedDistrict === null)
|
|
538
|
+
delete root.dataset.selectedDistrict;
|
|
539
|
+
updateDistrictAppearance();
|
|
540
|
+
updateComposition();
|
|
541
|
+
scheduleLabels();
|
|
542
|
+
status.textContent = `${year} supervisorial districts.`;
|
|
543
|
+
root.dispatchEvent(new CustomEvent('districtyearchange', { bubbles: true, detail: { year, previousYear } }));
|
|
544
|
+
if (selectedDistrict !== null)
|
|
545
|
+
root.dispatchEvent(new CustomEvent('districtchange', {
|
|
546
|
+
bubbles: true,
|
|
547
|
+
detail: districtSelection(selectedDistrict),
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
updateDistrictAppearance();
|
|
385
551
|
let items = [];
|
|
386
552
|
let destroyed = false;
|
|
387
553
|
let initialized = false;
|
|
@@ -1128,6 +1294,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1128
1294
|
const key = layer.dataset.layer === 'district-fills' ? 'districtFills' : 'districtLines';
|
|
1129
1295
|
layer.style.display = enabled(key) ? '' : 'none';
|
|
1130
1296
|
}
|
|
1297
|
+
updateDistrictAppearance();
|
|
1131
1298
|
const districtsVisible = enabled('districtFills') || enabled('districtLines') || (labels && enabled('districtLabels'));
|
|
1132
1299
|
const descriptions = ['San Francisco map.'];
|
|
1133
1300
|
if (districtsVisible)
|
|
@@ -1151,7 +1318,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1151
1318
|
const title = svg.querySelector('title');
|
|
1152
1319
|
if (title)
|
|
1153
1320
|
title.textContent = 'San Francisco map';
|
|
1154
|
-
svg.setAttribute('aria-label', `${description} Arrow keys pan; plus and minus zoom; Home resets.
|
|
1321
|
+
svg.setAttribute('aria-label', `${description} Arrow keys pan; plus and minus zoom; Home resets. Districts and neighborhoods: brackets move focus; Enter selects. The neighborhood and marker menus include every supplied item.`);
|
|
1155
1322
|
attribution.textContent = description;
|
|
1156
1323
|
scheduleLabels();
|
|
1157
1324
|
}
|
|
@@ -1409,6 +1576,31 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1409
1576
|
listen(markerSelect, 'change', () => selectMarker(markerSelect.value || null));
|
|
1410
1577
|
listen(svg, 'keydown', (event) => {
|
|
1411
1578
|
const target = event.target instanceof SVGElement ? event.target : null;
|
|
1579
|
+
const districtId = Number(target?.dataset.district);
|
|
1580
|
+
if (target?.closest('[data-layer="district-fills"], [data-layer="district-lines"]') &&
|
|
1581
|
+
districtRow(districtId)) {
|
|
1582
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1583
|
+
event.preventDefault();
|
|
1584
|
+
selectDistrict(districtId);
|
|
1585
|
+
root.dispatchEvent(new CustomEvent('districtactivate', {
|
|
1586
|
+
bubbles: true,
|
|
1587
|
+
detail: districtSelection(districtId),
|
|
1588
|
+
}));
|
|
1589
|
+
target.focus();
|
|
1590
|
+
}
|
|
1591
|
+
else if (event.key === '[' || event.key === ']') {
|
|
1592
|
+
event.preventDefault();
|
|
1593
|
+
const rows = data.map.districts?.[year] ?? [];
|
|
1594
|
+
const index = rows.findIndex((row) => row.id === districtId);
|
|
1595
|
+
const next = rows[(index + (event.key === ']' ? 1 : rows.length - 1)) % rows.length];
|
|
1596
|
+
const activeLayer = enabled('districtFills') ? 'district-fills' : 'district-lines';
|
|
1597
|
+
const nextNode = svg.querySelector(`[data-layer="${activeLayer}"] [data-district="${next.id}"]`);
|
|
1598
|
+
target.setAttribute('tabindex', '-1');
|
|
1599
|
+
nextNode?.setAttribute('tabindex', '0');
|
|
1600
|
+
nextNode?.focus();
|
|
1601
|
+
}
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1412
1604
|
const id = target?.dataset.neighborhoodId;
|
|
1413
1605
|
if (!id || !selectableNeighborhoods)
|
|
1414
1606
|
return;
|
|
@@ -1441,12 +1633,42 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1441
1633
|
}
|
|
1442
1634
|
});
|
|
1443
1635
|
listen(svg, 'click', (event) => {
|
|
1636
|
+
const district = event.target instanceof Element
|
|
1637
|
+
? event.target.closest('[data-layer="district-fills"] [data-district], [data-layer="district-lines"] [data-district]')
|
|
1638
|
+
: null;
|
|
1639
|
+
if (district?.dataset.district) {
|
|
1640
|
+
const id = Number(district.dataset.district);
|
|
1641
|
+
selectDistrict(id);
|
|
1642
|
+
root.dispatchEvent(new CustomEvent('districtactivate', { bubbles: true, detail: districtSelection(id) }));
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1444
1645
|
const node = event.target instanceof Element
|
|
1445
1646
|
? event.target.closest('[data-neighborhood-id]')
|
|
1446
1647
|
: null;
|
|
1447
1648
|
if (selectableNeighborhoods && node?.dataset.neighborhoodId)
|
|
1448
1649
|
selectNeighborhood(node.dataset.neighborhoodId);
|
|
1449
1650
|
});
|
|
1651
|
+
listen(svg, 'pointerover', (event) => {
|
|
1652
|
+
const district = event.target instanceof Element
|
|
1653
|
+
? event.target.closest('[data-layer="district-fills"] [data-district], [data-layer="district-lines"] [data-district]')
|
|
1654
|
+
: null;
|
|
1655
|
+
const id = district?.dataset.district ? Number(district.dataset.district) : null;
|
|
1656
|
+
if (id === hoveredDistrict)
|
|
1657
|
+
return;
|
|
1658
|
+
hoveredDistrict = id;
|
|
1659
|
+
updateDistrictAppearance();
|
|
1660
|
+
root.dispatchEvent(new CustomEvent('districthover', { bubbles: true, detail: districtSelection(id) }));
|
|
1661
|
+
});
|
|
1662
|
+
listen(svg, 'pointerout', (event) => {
|
|
1663
|
+
if (event.relatedTarget instanceof Element &&
|
|
1664
|
+
event.relatedTarget.closest('[data-layer="district-fills"] [data-district], [data-layer="district-lines"] [data-district]'))
|
|
1665
|
+
return;
|
|
1666
|
+
if (hoveredDistrict === null)
|
|
1667
|
+
return;
|
|
1668
|
+
hoveredDistrict = null;
|
|
1669
|
+
updateDistrictAppearance();
|
|
1670
|
+
root.dispatchEvent(new CustomEvent('districthover', { bubbles: true, detail: districtSelection(null) }));
|
|
1671
|
+
});
|
|
1450
1672
|
listen(svg, 'pointerdown', stopAnimation);
|
|
1451
1673
|
let observer;
|
|
1452
1674
|
const onResize = () => {
|
|
@@ -1457,6 +1679,10 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1457
1679
|
const explorer = Object.assign(root, {
|
|
1458
1680
|
selectNeighborhood,
|
|
1459
1681
|
getSelection,
|
|
1682
|
+
selectDistrict,
|
|
1683
|
+
getSelectedDistrict,
|
|
1684
|
+
setDistrictYear,
|
|
1685
|
+
setDistrictStyle,
|
|
1460
1686
|
getViewport,
|
|
1461
1687
|
setViewport: moveView,
|
|
1462
1688
|
stopAnimation,
|
package/dist/src/layers.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ interface DistrictPath {
|
|
|
15
15
|
id: number;
|
|
16
16
|
path: string;
|
|
17
17
|
color: string;
|
|
18
|
+
stroke?: string;
|
|
19
|
+
opacity?: number;
|
|
18
20
|
}
|
|
19
21
|
export declare function districtFills(districts: readonly DistrictPath[], { idPrefix }: LayerContext): string;
|
|
20
22
|
export declare function districtLines(districts: readonly DistrictPath[], { idPrefix, colors }: LayerContext): string;
|
package/dist/src/layers.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { escapeXml, number, overlayLabel, stroke } from './svg.js';
|
|
2
2
|
export function districtFills(districts, { idPrefix }) {
|
|
3
|
-
return `<g data-layer="district-fills" clip-path="url(#${idPrefix}-coast)">${districts.map((d) => `<path data-district="${d.id}" d="${d.path}" fill="${escapeXml(d.color)}" fill-rule="evenodd"/>`).join('')}</g>`;
|
|
3
|
+
return `<g data-layer="district-fills" clip-path="url(#${idPrefix}-coast)">${districts.map((d) => `<path data-district="${d.id}" d="${d.path}" fill="${escapeXml(d.color)}"${d.opacity === undefined ? '' : ` fill-opacity="${d.opacity}"`} fill-rule="evenodd"/>`).join('')}</g>`;
|
|
4
4
|
}
|
|
5
5
|
export function districtLines(districts, { idPrefix, colors }) {
|
|
6
|
-
return `<g data-layer="district-lines" clip-path="url(#${idPrefix}-coast)">${districts.map((d) => `<path data-district="${d.id}" d="${d.path}" ${stroke(colors.district, 1.1)}/>`).join('')}</g>`;
|
|
6
|
+
return `<g data-layer="district-lines" clip-path="url(#${idPrefix}-coast)">${districts.map((d) => `<path data-district="${d.id}" d="${d.path}" ${stroke(d.stroke ?? colors.district, 1.1)}${d.opacity === undefined ? '' : ` stroke-opacity="${d.opacity}"`}/>`).join('')}</g>`;
|
|
7
7
|
}
|
|
8
8
|
export function landmarks(items, { idPrefix, path, colors }) {
|
|
9
9
|
return `<g data-layer="landmarks" clip-path="url(#${idPrefix}-coast)">${items.map((item) => `<path data-landmark="${escapeXml(item.id)}" d="${path(item.geometry)}" fill="${escapeXml(colors.park)}" fill-rule="evenodd"><title>${escapeXml(item.name)}</title></path>`).join('')}</g>`;
|