|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="it"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8"> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | + <title>Referendum 22 marzo 2026 – Vincitore e voti in più per comune</title> |
| 7 | + <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@master/ixmaps.js"></script> |
| 8 | + <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script> |
| 9 | + <!-- USER chart dependencies (same as Marche map) --> |
| 10 | + <script src="https://d3js.org/d3.v3.min.js"></script> |
| 11 | + <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@master/usercharts/d3/chart.js"></script> |
| 12 | + <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@master/usercharts/d3/arrow_chart.js"></script> |
| 13 | + <style> |
| 14 | + html, body { width: 100%; height: 100%; overflow: hidden; margin: 0; padding: 0; } |
| 15 | + body { font-family: 'Segoe UI', Arial, sans-serif; background: #e8e8e8; } |
| 16 | + #map { width: 100%; height: 100%; } |
| 17 | + |
| 18 | + #header { |
| 19 | + position: absolute; |
| 20 | + top: 0; left: 0; right: 0; |
| 21 | + z-index: 1000; |
| 22 | + background: linear-gradient(135deg, rgba(20,20,20,0.88) 0%, rgba(30,30,30,0.88) 100%); |
| 23 | + backdrop-filter: blur(8px); |
| 24 | + padding: 10px 16px; |
| 25 | + display: flex; |
| 26 | + align-items: center; |
| 27 | + gap: 12px; |
| 28 | + border-bottom: 1px solid rgba(255,255,255,0.08); |
| 29 | + height: 56px; |
| 30 | + box-sizing: border-box; |
| 31 | + } |
| 32 | + #header h1 { color: #f0f0f0; font-size: 1.05rem; font-weight: 600; white-space: nowrap; margin: 0; } |
| 33 | + #header p { color: rgba(220,220,220,0.55); font-size: 0.72rem; margin: 2px 0 0; } |
| 34 | + |
| 35 | + #app-loading { |
| 36 | + position: absolute; |
| 37 | + top: 50%; left: 50%; |
| 38 | + transform: translate(-50%, -50%); |
| 39 | + z-index: 2000; |
| 40 | + background: rgba(255,255,255,0.97); |
| 41 | + border-radius: 12px; |
| 42 | + padding: 24px 36px; |
| 43 | + text-align: center; |
| 44 | + box-shadow: 0 8px 32px rgba(0,0,0,0.25); |
| 45 | + } |
| 46 | + .spinner { |
| 47 | + width: 36px; height: 36px; |
| 48 | + border: 3px solid #d0eaea; |
| 49 | + border-top-color: #00838f; |
| 50 | + border-radius: 50%; |
| 51 | + animation: spin 0.8s linear infinite; |
| 52 | + margin: 0 auto 10px; |
| 53 | + } |
| 54 | + #app-loading p { color: #555; font-size: 0.88rem; margin: 0; } |
| 55 | + @keyframes spin { to { transform: rotate(360deg); } } |
| 56 | + |
| 57 | + #legend { |
| 58 | + position: absolute; |
| 59 | + bottom: 30px; left: 14px; |
| 60 | + z-index: 1000; |
| 61 | + background: rgba(20,20,20,0.88); |
| 62 | + border-radius: 10px; |
| 63 | + padding: 11px 14px; |
| 64 | + color: #e8e8e8; |
| 65 | + font-size: 0.72rem; |
| 66 | + box-shadow: 0 2px 14px rgba(0,0,0,0.35); |
| 67 | + border: 1px solid rgba(255,255,255,0.08); |
| 68 | + display: none; |
| 69 | + min-width: 205px; |
| 70 | + line-height: 1.7; |
| 71 | + } |
| 72 | + #legend b { display: block; font-size: 0.8rem; margin-bottom: 6px; color: #fff; } |
| 73 | + .leg-row { display: flex; align-items: center; gap: 8px; } |
| 74 | + .leg-bar { height: 6px; border-radius: 3px; flex: 1; } |
| 75 | + .leg-sep { border: none; border-top: 1px solid rgba(255,255,255,0.1); margin: 6px 0; } |
| 76 | + |
| 77 | + #stats { |
| 78 | + position: absolute; |
| 79 | + bottom: 30px; right: 14px; |
| 80 | + z-index: 1000; |
| 81 | + background: rgba(20,20,20,0.88); |
| 82 | + border-radius: 10px; |
| 83 | + padding: 10px 14px; |
| 84 | + font-size: 0.75rem; |
| 85 | + box-shadow: 0 2px 14px rgba(0,0,0,0.35); |
| 86 | + border: 1px solid rgba(255,255,255,0.08); |
| 87 | + line-height: 1.9; |
| 88 | + min-width: 185px; |
| 89 | + display: none; |
| 90 | + } |
| 91 | + #stats b { display: block; font-size: 0.8rem; color: #fff; margin-bottom: 4px; } |
| 92 | + .stat-row { display: flex; justify-content: space-between; gap: 12px; color: #bbb; } |
| 93 | + .si-val { color: #5ac8c8; font-weight: 700; } |
| 94 | + .no-val { color: #e07070; font-weight: 700; } |
| 95 | + .neu-val { color: #ccc; font-weight: 700; } |
| 96 | + .stat-sep { border: none; border-top: 1px solid rgba(255,255,255,0.08); margin: 5px 0; } |
| 97 | + </style> |
| 98 | +</head> |
| 99 | +<body> |
| 100 | + |
| 101 | + |
| 102 | +<div id="app-loading"> |
| 103 | + <div class="spinner"></div> |
| 104 | + <p id="loading-msg">Caricamento scrutini…</p> |
| 105 | +</div> |
| 106 | + |
| 107 | +<div id="map" style="max-width: 1024px; margin:auto"></div> |
| 108 | + |
| 109 | +<div id="legend"> |
| 110 | + <b>📖 Legenda</b> |
| 111 | + <div class="leg-row"> |
| 112 | + <div class="leg-bar" style="background:linear-gradient(to right,#8b1a1a,#f0a8a8)"></div> |
| 113 | + <span>← No vince (freccia sinistra)</span> |
| 114 | + </div> |
| 115 | + <div class="leg-row"> |
| 116 | + <div class="leg-bar" style="background:linear-gradient(to right,#a0d8d8,#005858)"></div> |
| 117 | + <span>Sì vince (freccia destra) →</span> |
| 118 | + </div> |
| 119 | + <hr class="leg-sep"> |
| 120 | + <span style="color:rgba(200,200,200,0.5);font-size:0.65rem">Dimensione freccia ∝ √(voti in più)</span> |
| 121 | +</div> |
| 122 | + |
| 123 | +<script> |
| 124 | +const TOPO_URL = "https://raw.githubusercontent.com/gjrichter/geo/main/italy/boundaries/italy_istat_municipalities_4326_500m.topojson"; |
| 125 | +const DATA_URL = "https://raw.githubusercontent.com/ondata/referendum-download/main/data/20260322/scrutini_flat.csv"; |
| 126 | +const CENTROIDS_URL = "https://raw.githubusercontent.com/aborruso/centroidiurbanfabric/master/output/ElencoUnitaAmministrative2011.geojson"; |
| 127 | + |
| 128 | +// 4 rossi + 4 teal, taglio netto a 50% |
| 129 | +const BG_COLORS = [ |
| 130 | + "#8b1a1a","#c04040","#e07070","#f0a8a8", |
| 131 | + "#a0d8d8","#5ac8c8","#1a9696","#005858" |
| 132 | +]; |
| 133 | + |
| 134 | +let DATA = []; |
| 135 | + |
| 136 | +function parsePct(s) { |
| 137 | + return parseFloat((s || "0").replace(",", ".")) || 0; |
| 138 | +} |
| 139 | + |
| 140 | +async function loadData() { |
| 141 | + // Solo CSV — i centroidi li carica ixmaps come layer |
| 142 | + var csvText = await fetch(DATA_URL).then(function(r) { |
| 143 | + if (!r.ok) throw new Error("scrutini HTTP " + r.status); |
| 144 | + return r.text(); |
| 145 | + }); |
| 146 | + |
| 147 | + var rows = Papa.parse(csvText, { header: true, skipEmptyLines: true }).data; |
| 148 | + |
| 149 | + DATA = rows |
| 150 | + .filter(function(r) { return r.livello === "comune" && r.cod_istat; }) |
| 151 | + .map(function(r) { |
| 152 | + var si = parsePct(r.perc_si); |
| 153 | + var no = parsePct(r.perc_no); |
| 154 | + var aff = parsePct(r.perc_vot); |
| 155 | + var vsi = parseInt(r.voti_si) || 0; |
| 156 | + var vno = parseInt(r.voti_no) || 0; |
| 157 | + var vot = parseInt(r.votanti_t) || 0; |
| 158 | + var ele = parseInt(r.elettori_t)|| 0; |
| 159 | + var istat = r.cod_istat.trim().padStart(6, "0"); |
| 160 | + return { |
| 161 | + cod_istat: istat, |
| 162 | + pro_com: parseInt(istat), // join con PRO_COM (intero) di Borruso |
| 163 | + perc_si: si, |
| 164 | + perc_no: no, |
| 165 | + perc_vot: aff, |
| 166 | + voti_si: vsi, |
| 167 | + voti_no: vno, |
| 168 | + winner: vsi >= vno ? "si" : "no", |
| 169 | + desc_com: r.desc_com || "", |
| 170 | + desc_prov: r.desc_prov || "", |
| 171 | + desc_reg: r.desc_reg || "", |
| 172 | + si_f: si.toFixed(1), |
| 173 | + no_f: no.toFixed(1), |
| 174 | + aff_f: aff.toFixed(1), |
| 175 | + voti_si_f: vsi.toLocaleString("it-IT"), |
| 176 | + voti_no_f: vno.toLocaleString("it-IT"), |
| 177 | + margin_f: Math.abs(vsi - vno).toLocaleString("it-IT"), |
| 178 | + votanti_f: vot.toLocaleString("it-IT"), |
| 179 | + elettori_f: ele.toLocaleString("it-IT") |
| 180 | + }; |
| 181 | + }); |
| 182 | +} |
| 183 | + |
| 184 | +var TOOLTIP = [ |
| 185 | + '<b style="font-size:1.05em">{{desc_com}}</b>', |
| 186 | + '<br><span style="color:#999;font-size:0.88em">{{desc_prov}} · {{desc_reg}}</span>', |
| 187 | + '<hr style="border:none;border-top:1px solid #ddd;margin:5px 0">', |
| 188 | + '<table>', |
| 189 | + '<tr><td style="color:#888;padding:2px 4px">Sì</td>', |
| 190 | + ' <td style="font-weight:600;padding:2px 4px;color:#009999">{{si_f}}%</td>', |
| 191 | + ' <td style="color:#aaa;font-size:0.85em;padding:2px 4px">({{voti_si_f}} voti)</td></tr>', |
| 192 | + '<tr><td style="color:#888;padding:2px 4px">No</td>', |
| 193 | + ' <td style="font-weight:600;padding:2px 4px;color:#c03030">{{no_f}}%</td>', |
| 194 | + ' <td style="color:#aaa;font-size:0.85em;padding:2px 4px">({{voti_no_f}} voti)</td></tr>', |
| 195 | + '<tr><td style="color:#888;padding:2px 4px">Margine</td>', |
| 196 | + ' <td style="font-weight:600;padding:2px 4px" colspan="2">{{margin_f}} voti</td></tr>', |
| 197 | + '<tr><td style="color:#888;padding:2px 4px">Affluenza</td>', |
| 198 | + ' <td style="font-weight:600;padding:2px 4px">{{aff_f}}%</td>', |
| 199 | + ' <td style="color:#aaa;font-size:0.85em;padding:2px 4px">({{votanti_f}}/{{elettori_f}})</td></tr>', |
| 200 | + '</table>' |
| 201 | +].join(""); |
| 202 | + |
| 203 | +function buildMap() { |
| 204 | + var myMap = ixmaps.Map("map", { |
| 205 | + mapType: "VT_TONER_LITE", |
| 206 | + mode: "info", |
| 207 | + legend: "open", |
| 208 | + tools: true |
| 209 | + }) |
| 210 | + .view({ center: { lat: 42.5, lng: 12.5 }, zoom: 6 }) |
| 211 | + .options({ |
| 212 | + objectscaling: "dynamic", |
| 213 | + normalSizeScale: "10000000", |
| 214 | + basemapopacity: 0, |
| 215 | + flushChartDraw: 1000000, |
| 216 | + flushPaintShape: 1000000 |
| 217 | + }); |
| 218 | + |
| 219 | + // ── Layer 1: contorni comunali (polygon) ────────────────────────────── |
| 220 | + myMap.layer("comuni") |
| 221 | + .data({ url: TOPO_URL, type: "topojson" }) |
| 222 | + .binding({ geo: "geometry", id: "com_istat_code", title: "name" }) |
| 223 | + .type("FEATURE") |
| 224 | + .style({ |
| 225 | + colorscheme: ["none"], |
| 226 | + fillopacity: 0, |
| 227 | + linecolor: "#888888", |
| 228 | + linewidth: 0.2, |
| 229 | + showdata: "true" |
| 230 | + }) |
| 231 | + .define(); |
| 232 | + |
| 233 | + // ── Layer 2: sfondo divergente % Sì ─────────────────────────────────── |
| 234 | + myMap.layer("comuni") |
| 235 | + .data({ obj: DATA, type: "json" }) |
| 236 | + .binding({ lookup: "cod_istat", value: "perc_si", title: "desc_com" }) |
| 237 | + .type("CHOROPLETH|EQUIDISTANT") |
| 238 | + .style({ |
| 239 | + colorscheme: BG_COLORS, |
| 240 | + rangecentervalue: "50", |
| 241 | + fillopacity: 0.1, |
| 242 | + linecolor: "#888888", |
| 243 | + linewidth: 0.15, |
| 244 | + showdata: "true" |
| 245 | + }) |
| 246 | + .meta({ name: "si_bg", tooltip: TOOLTIP, description: "Sfondo: % Sì (teal) vs % No (rosso) · Frecce: voti in più del vincitore · Dati: ondata (CC BY) · Geometrie: ISTAT" }) |
| 247 | + .title("<h2 style='margin:0 0 0.5em 0'>Referendum 22 marzo 2026 — Vincitore e voti in più per comune</h2>") |
| 248 | + .define(); |
| 249 | + |
| 250 | + // ── Layer 3: centroidi urbani Borruso (posizionamento frecce) ───────── |
| 251 | + myMap.layer("centroids") |
| 252 | + .data({ url: CENTROIDS_URL, type: "geojson" }) |
| 253 | + .binding({ geo: "geometry", id: "PRO_COM", title: "PRO_COM" }) |
| 254 | + .type("FEATURE|NOLEGEND") |
| 255 | + .style({ |
| 256 | + colorscheme: ["none"], |
| 257 | + scale: 0, |
| 258 | + fillopacity: 0, |
| 259 | + linecolor: "none", |
| 260 | + linewidth: 0, |
| 261 | + showdata: "true" |
| 262 | + }) |
| 263 | + .define(); |
| 264 | + |
| 265 | + // ── Layer 4: frecce Sì (→ destra, rotation 35) ──────────────────────── |
| 266 | + // value: "voti_no|voti_si" + NONEGATIVE → solo dove Sì vince |
| 267 | + myMap.layer("comuni") |
| 268 | + .data({ obj: DATA, type: "json" }) |
| 269 | + .binding({ lookup: "cod_istat", value: "voti_no|voti_si", title: "desc_com" }) |
| 270 | + .type("CHART|USER|3D|DIFFERENCE|AGGREGATE|RECT|RELOCATE|SUM|VALUES|NONEGATIVE|BOX|BOTTOMTITLE|NOLEGEND") |
| 271 | + .style({ |
| 272 | + name: "chart_si", |
| 273 | + userdraw: "arrowChart", |
| 274 | + colorscheme: ["#1a8844"], |
| 275 | + label: ["Sì"], |
| 276 | + fillopacity: 1, |
| 277 | + sizepow: 2, |
| 278 | + normalsizevalue: 100000, |
| 279 | + rotation: 35, |
| 280 | + rangescale: 1, |
| 281 | + shadow: "false", |
| 282 | + units: "", |
| 283 | + boxcolor: "none", |
| 284 | + bordercolor: "none", |
| 285 | + textscale: "0.5", |
| 286 | + boxupper: "1:250000", |
| 287 | + textcolor: "black", |
| 288 | + valuesupper: "1:1000000", |
| 289 | + valuedecimals: "0", |
| 290 | + minvaluesize: "8", |
| 291 | + aggregationfield:"desc_com", |
| 292 | + titlefield: "desc_com", |
| 293 | + showdata: "true", |
| 294 | + datafields: ["desc_com","desc_prov","voti_si_f","voti_no_f","margin_f"] |
| 295 | + }) |
| 296 | + .meta({ |
| 297 | + name: "chart_si", |
| 298 | + tooltip: "<h2 style='margin:0 0 0.5em 0'>{{desc_com}}</h2>{{theme.item.chart}}<br>voti in più Sì: {{margin_f}}" |
| 299 | + }) |
| 300 | + .title("Voti in più — Sì vince") |
| 301 | + .define(); |
| 302 | + |
| 303 | + // ── Layer 5: frecce No (← sinistra, rotation 325) ──────────────────── |
| 304 | + // value: "voti_no|voti_si" + NONEGATIVE → solo dove No vince |
| 305 | + myMap.layer("comuni") |
| 306 | + .data({ obj: DATA, type: "json" }) |
| 307 | + .binding({ lookup: "cod_istat", value: "voti_si|voti_no", title: "desc_com" }) |
| 308 | + .type("CHART|USER|3D|DIFFERENCE|AGGREGATE|RECT|RELOCATE|SUM|VALUES|NONEGATIVE|NOLEGEND") |
| 309 | + .style({ |
| 310 | + name: "chart_no", |
| 311 | + userdraw: "arrowChart", |
| 312 | + colorscheme: ["#c04040"], |
| 313 | + label: ["No"], |
| 314 | + fillopacity: 1, |
| 315 | + sizepow: 2, |
| 316 | + normalsizevalue: 100000, |
| 317 | + rotation: 325, |
| 318 | + rangescale: 1, |
| 319 | + shadow: "false", |
| 320 | + units: "", |
| 321 | + boxcolor: "none", |
| 322 | + bordercolor: "none", |
| 323 | + textscale: "0.5", |
| 324 | + boxupper: "1:250000", |
| 325 | + textcolor: "black", |
| 326 | + valuesupper: "1:1000000", |
| 327 | + valuedecimals: "0", |
| 328 | + minvaluesize: "8", |
| 329 | + aggregationfield:"desc_com", |
| 330 | + titlefield: "desc_com", |
| 331 | + showdata: "true", |
| 332 | + datafields: ["desc_com","desc_prov","voti_si_f","voti_no_f","margin_f"] |
| 333 | + }) |
| 334 | + .meta({ |
| 335 | + name: "chart_no", |
| 336 | + tooltip: "<h2 style='margin:0 0 0.5em 0'>{{desc_com}}</h2>{{theme.item.chart}}<br>voti in più No: {{margin_f}}" |
| 337 | + }) |
| 338 | + .title("Voti in più — No vince") |
| 339 | + .define(); |
| 340 | + |
| 341 | + // ── Zoom dynamics (come nel Marche map) ─────────────────────────────── |
| 342 | + ixmaps.htmlgui_onZoomAndPan = function() { |
| 343 | + var z = ixmaps.getZoom(); |
| 344 | + var opacity = Math.max(0, Math.min(0.8, (z - 9) / 3)); |
| 345 | + var minSize = z > 10 ? 1 : z > 9 ? 6 : z > 8 ? 7 : z > 7 ? 8 : 18; |
| 346 | + |
| 347 | + myMap.then(function(m) { |
| 348 | + m.setBasemapOpacity(opacity, "absolute"); |
| 349 | + m.changeThemeStyle("chart_si", "minvaluesize:" + minSize); |
| 350 | + m.changeThemeStyle("chart_no", "minvaluesize:" + minSize); |
| 351 | + }); |
| 352 | + }; |
| 353 | +} |
| 354 | + |
| 355 | +(async function() { |
| 356 | + try { |
| 357 | + await loadData(); |
| 358 | + document.getElementById("app-loading").style.display = "none"; |
| 359 | + buildMap(); |
| 360 | + } catch(err) { |
| 361 | + document.getElementById("app-loading").innerHTML = |
| 362 | + '<p style="color:#c0392b;padding:12px">⚠️ Errore: ' + err.message + '</p>'; |
| 363 | + console.error(err); |
| 364 | + } |
| 365 | +})(); |
| 366 | +</script> |
| 367 | +</body> |
| 368 | +</html> |
0 commit comments