Skip to content

Commit d9bd8f8

Browse files
committed
Create referendum-arrows-2026.html
1 parent 7fc8c4c commit d9bd8f8

1 file changed

Lines changed: 368 additions & 0 deletions

File tree

Stage/referendum-arrows-2026.html

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
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

Comments
 (0)