Skip to content

Commit 4eafab6

Browse files
committed
xxx
1 parent 3e1184a commit 4eafab6

9 files changed

Lines changed: 3016 additions & 0 deletions

Stage/readme.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Stage
2+
3+
Work-in-progress visualizations.
4+
5+
## Files
6+
7+
- [referendum-arrows-2026-grid.html](referendum-arrows-2026-grid.html)
8+
- [referendum-arrows-2026.html](referendum-arrows-2026.html)
9+
- [referendum-peaks-2026-reg-grid-no-animate-zoom.html](referendum-peaks-2026-reg-grid-no-animate-zoom.html)
10+
- [referendum-peaks-2026-reg-grid.html](referendum-peaks-2026-reg-grid.html)
11+
- [referendum-peaks-2026-reg-percent.html](referendum-peaks-2026-reg-percent.html)
12+
- [referendum-peaks-2026-reg.html](referendum-peaks-2026-reg.html)
13+
- [referendum-peaks-2026.html](referendum-peaks-2026.html)
14+
- [referendum_si_no_divergent_slider.html](referendum_si_no_divergent_slider.html)
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
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+
#stats {
58+
position: absolute;
59+
bottom: 30px; left: 14px;
60+
z-index: 1000;
61+
background: rgba(255,255,255,0.93);
62+
border-radius: 10px;
63+
padding: 10px 14px;
64+
font-size: 0.75rem;
65+
box-shadow: 0 2px 14px rgba(0,0,0,0.15);
66+
border: 1px solid rgba(0,0,0,0.08);
67+
line-height: 1.9;
68+
min-width: 185px;
69+
display: none;
70+
}
71+
#stats b { display: block; font-size: 0.8rem; color: #111; margin-bottom: 4px; }
72+
.stat-row { display: flex; justify-content: space-between; gap: 12px; color: #555; }
73+
.si-val { color: #007a7a; font-weight: 700; }
74+
.no-val { color: #b03030; font-weight: 700; }
75+
.neu-val { color: #444; font-weight: 700; }
76+
.stat-sep { border: none; border-top: 1px solid rgba(0,0,0,0.08); margin: 5px 0; }
77+
</style>
78+
</head>
79+
<body>
80+
81+
82+
<div id="app-loading">
83+
<div class="spinner"></div>
84+
<p id="loading-msg">Caricamento scrutini…</p>
85+
</div>
86+
87+
<div id="map" style="max-width: 1024px; margin:auto"></div>
88+
89+
<div id="stats">
90+
<b>Risultato nazionale</b>
91+
<div class="stat-row"><span>Comuni Sì</span><span class="si-val" id="s-nsi"></span></div>
92+
<div class="stat-row"><span>Voti in più (Sì)</span><span class="si-val" id="s-msi"></span></div>
93+
<hr class="stat-sep">
94+
<div class="stat-row"><span>Comuni No</span><span class="no-val" id="s-nno"></span></div>
95+
<div class="stat-row"><span>Voti in più (No)</span><span class="no-val" id="s-mno"></span></div>
96+
<hr class="stat-sep">
97+
<div class="stat-row"><span>Margine finale</span><span id="s-fin"></span></div>
98+
</div>
99+
100+
101+
<script>
102+
const TOPO_URL = "https://raw.githubusercontent.com/gjrichter/geo/main/italy/boundaries/italy_istat_municipalities_4326_500m.topojson";
103+
const DATA_URL = "https://raw.githubusercontent.com/ondata/referendum-download/main/data/20260322/scrutini_flat.csv";
104+
const CENTROIDS_URL = "https://raw.githubusercontent.com/aborruso/centroidiurbanfabric/master/output/ElencoUnitaAmministrative2011.geojson";
105+
106+
// 4 rossi + 4 teal, taglio netto a 50%
107+
const BG_COLORS = [
108+
"#8b1a1a","#c04040","#e07070","#f0a8a8",
109+
"#a0d8d8","#5ac8c8","#1a9696","#005858"
110+
];
111+
112+
let DATA = [];
113+
114+
function parsePct(s) {
115+
return parseFloat((s || "0").replace(",", ".")) || 0;
116+
}
117+
118+
async function loadData() {
119+
// Solo CSV — i centroidi li carica ixmaps come layer
120+
var csvText = await fetch(DATA_URL).then(function(r) {
121+
if (!r.ok) throw new Error("scrutini HTTP " + r.status);
122+
return r.text();
123+
});
124+
125+
var rows = Papa.parse(csvText, { header: true, skipEmptyLines: true }).data;
126+
127+
DATA = rows
128+
.filter(function(r) { return r.livello === "comune" && r.cod_istat; })
129+
.map(function(r) {
130+
var si = parsePct(r.perc_si);
131+
var no = parsePct(r.perc_no);
132+
var aff = parsePct(r.perc_vot);
133+
var vsi = parseInt(r.voti_si) || 0;
134+
var vno = parseInt(r.voti_no) || 0;
135+
var vot = parseInt(r.votanti_t) || 0;
136+
var ele = parseInt(r.elettori_t)|| 0;
137+
var istat = r.cod_istat.trim().padStart(6, "0");
138+
return {
139+
cod_istat: istat,
140+
pro_com: parseInt(istat), // join con PRO_COM (intero) di Borruso
141+
perc_si: si,
142+
perc_no: no,
143+
perc_vot: aff,
144+
voti_si: vsi,
145+
voti_no: vno,
146+
winner: vsi >= vno ? "si" : "no",
147+
desc_com: r.desc_com || "",
148+
desc_prov: r.desc_prov || "",
149+
desc_reg: r.desc_reg || "",
150+
si_f: si.toFixed(1),
151+
no_f: no.toFixed(1),
152+
aff_f: aff.toFixed(1),
153+
voti_si_f: vsi.toLocaleString("it-IT"),
154+
voti_no_f: vno.toLocaleString("it-IT"),
155+
margin_f: Math.abs(vsi - vno).toLocaleString("it-IT"),
156+
votanti_f: vot.toLocaleString("it-IT"),
157+
elettori_f: ele.toLocaleString("it-IT")
158+
};
159+
});
160+
}
161+
162+
var TOOLTIP = [
163+
'<b style="font-size:1.05em">{{desc_com}}</b>',
164+
'<br><span style="color:#999;font-size:0.88em">{{desc_prov}} · {{desc_reg}}</span>',
165+
'<hr style="border:none;border-top:1px solid #ddd;margin:5px 0">',
166+
'<table>',
167+
'<tr><td style="color:#888;padding:2px 4px">Sì</td>',
168+
' <td style="font-weight:600;padding:2px 4px;color:#009999">{{si_f}}%</td>',
169+
' <td style="color:#aaa;font-size:0.85em;padding:2px 4px">({{voti_si_f}} voti)</td></tr>',
170+
'<tr><td style="color:#888;padding:2px 4px">No</td>',
171+
' <td style="font-weight:600;padding:2px 4px;color:#c03030">{{no_f}}%</td>',
172+
' <td style="color:#aaa;font-size:0.85em;padding:2px 4px">({{voti_no_f}} voti)</td></tr>',
173+
'<tr><td style="color:#888;padding:2px 4px">Margine</td>',
174+
' <td style="font-weight:600;padding:2px 4px" colspan="2">{{margin_f}} voti</td></tr>',
175+
'<tr><td style="color:#888;padding:2px 4px">Affluenza</td>',
176+
' <td style="font-weight:600;padding:2px 4px">{{aff_f}}%</td>',
177+
' <td style="color:#aaa;font-size:0.85em;padding:2px 4px">({{votanti_f}}/{{elettori_f}})</td></tr>',
178+
'</table>'
179+
].join("");
180+
181+
function buildStats() {
182+
var nSi = 0, nNo = 0, sumMSi = 0, sumMNo = 0;
183+
DATA.forEach(function(r) {
184+
var m = Math.abs(r.voti_si - r.voti_no);
185+
if (r.winner === "si") { nSi++; sumMSi += m; }
186+
else { nNo++; sumMNo += m; }
187+
});
188+
var finale = sumMSi - sumMNo; // = Σvoti_si - Σvoti_no
189+
var fmt = function(n) { return n.toLocaleString("it-IT"); };
190+
var finStr = (finale >= 0 ? "Sì +" : "No +") + fmt(Math.abs(finale));
191+
var finColor = finale >= 0 ? "#5ac8c8" : "#e07070";
192+
193+
document.getElementById("s-nsi").textContent = fmt(nSi);
194+
document.getElementById("s-msi").textContent = fmt(sumMSi);
195+
document.getElementById("s-nno").textContent = fmt(nNo);
196+
document.getElementById("s-mno").textContent = fmt(sumMNo);
197+
var fin = document.getElementById("s-fin");
198+
fin.textContent = finStr;
199+
fin.style.color = finColor;
200+
fin.style.fontWeight = "700";
201+
202+
document.getElementById("stats").style.display = "block";
203+
}
204+
205+
function buildMap() {
206+
var myMap = ixmaps.Map("map", {
207+
mapType: "VT_TONER_LITE",
208+
mode: "map",
209+
legend: "open",
210+
tools: true
211+
})
212+
.view({ center: { lat: 42.5, lng: 12.5 }, zoom: 6 })
213+
.options({
214+
objectscaling: "dynamic",
215+
normalSizeScale: "10000000",
216+
dynamicScalePow: "2.5",
217+
basemapopacity: 0,
218+
flushChartDraw: 1000000,
219+
flushPaintShape: 1000000
220+
});
221+
222+
// ── Layer 1: contorni comunali (polygon) ──────────────────────────────
223+
myMap.layer("comuni")
224+
.data({ url: TOPO_URL, type: "topojson" })
225+
.binding({ geo: "geometry", id: "com_istat_code", title: "name" })
226+
.type("FEATURE")
227+
.style({
228+
colorscheme: ["none"],
229+
fillopacity: 0,
230+
linecolor: "#888888",
231+
linewidth: 0.2,
232+
showdata: "true"
233+
})
234+
.define();
235+
236+
// ── Layer 2: sfondo divergente % Sì ───────────────────────────────────
237+
myMap.layer("comuni")
238+
.data({ obj: DATA, type: "json" })
239+
.binding({ lookup: "cod_istat", value: "perc_si", title: "desc_com" })
240+
.type("CHOROPLETH|EQUIDISTANT")
241+
.style({
242+
colorscheme: BG_COLORS,
243+
rangecentervalue: "50",
244+
fillopacity: 0.1,
245+
linecolor: "#888888",
246+
linewidth: 0.15,
247+
showdata: "true"
248+
})
249+
.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" })
250+
.title("<h2 style='margin:0 0 0.5em 0'>Referendum 22 marzo 2026 — Vincitore e voti in più per comune</h2>")
251+
.define();
252+
253+
// ── Layer 3: centroidi urbani Borruso (posizionamento frecce) ─────────
254+
myMap.layer("centroids")
255+
.data({ url: CENTROIDS_URL, type: "geojson" })
256+
.binding({ geo: "geometry", id: "PRO_COM", title: "PRO_COM" })
257+
.type("FEATURE|NOLEGEND")
258+
.style({
259+
colorscheme: ["none"],
260+
scale: 0,
261+
fillopacity: 0,
262+
linecolor: "none",
263+
linewidth: 0,
264+
showdata: "true"
265+
})
266+
.define();
267+
268+
// ── Layer 4: frecce Sì (→ destra, rotation 35) ────────────────────────
269+
// value: "voti_no|voti_si" + NONEGATIVE → solo dove Sì vince
270+
myMap.layer("comuni")
271+
.data({ obj: DATA, type: "json" })
272+
.binding({ lookup: "cod_istat", value: "voti_no|voti_si", title: "desc_com" })
273+
.type("CHART|USER|3D|DIFFERENCE|AGGREGATE|RECT|RELOCATE|SUM|VALUES|NONEGATIVE|BOX|BOTTOMTITLE|NOLEGEND")
274+
.style({
275+
name: "chart_si",
276+
userdraw: "arrowChart",
277+
colorscheme: ["#1a8844"],
278+
label: ["Sì"],
279+
fillopacity: 1,
280+
sizepow: 2,
281+
normalsizevalue: 100000,
282+
rotation: 15,
283+
scale: 0.6,
284+
rangescale: 1,
285+
shadow: "false",
286+
units: "",
287+
boxcolor: "none",
288+
bordercolor: "none",
289+
textscale: "0.5",
290+
boxupper: "1:250000",
291+
textcolor: "black",
292+
valuesupper: "1:1000000",
293+
valuedecimals: "0",
294+
minvaluesize: "8",
295+
aggregationscale: [
296+
"1:1","1px",
297+
"1:1500000","desc_prov",
298+
],
299+
titlefield: "desc_com",
300+
showdata: "true"
301+
})
302+
.define();
303+
304+
// ── Layer 5: frecce No (← sinistra, rotation 325) ────────────────────
305+
// value: "voti_no|voti_si" + NONEGATIVE → solo dove No vince
306+
myMap.layer("comuni")
307+
.data({ obj: DATA, type: "json" })
308+
.binding({ lookup: "cod_istat", value: "voti_si|voti_no", title: "desc_com" })
309+
.type("CHART|USER|3D|DIFFERENCE|AGGREGATE|RECT|RELOCATE|SUM|VALUES|NONEGATIVE|NOLEGEND")
310+
.style({
311+
name: "chart_no",
312+
userdraw: "arrowChart",
313+
colorscheme: ["#c04040"],
314+
label: ["No"],
315+
fillopacity: 1,
316+
sizepow: 2,
317+
normalsizevalue: 100000,
318+
rotation: 345,
319+
scale: 0.6,
320+
rangescale: 1,
321+
shadow: "false",
322+
units: "",
323+
boxcolor: "none",
324+
bordercolor: "none",
325+
textscale: "0.5",
326+
boxupper: "1:250000",
327+
textcolor: "black",
328+
valuesupper: "1:1000000",
329+
valuedecimals: "0",
330+
minvaluesize: "8",
331+
aggregationscale: [
332+
"1:1","1px",
333+
"1:1500000","desc_prov",
334+
],
335+
titlefield: "desc_com",
336+
showdata: "true",
337+
datafields: ["desc_com","desc_prov","voti_si_f","voti_no_f","margin_f"]
338+
})
339+
.meta({
340+
name: "chart_no",
341+
tooltip: "<h2 style='margin:0 0 0.5em 0'>{{desc_com}}</h2>{{theme.item.chart}}<br>voti in più No: {{raw.margin_f}}"
342+
})
343+
.title("Voti in più — No vince")
344+
.define();
345+
346+
// ── Zoom dynamics (come nel Marche map) ───────────────────────────────
347+
ixmaps.htmlgui_onZoomAndPan = function() {
348+
var z = ixmaps.getZoom();
349+
var opacity = Math.max(0, Math.min(0.8, (z - 9) / 3));
350+
var minSize = z > 10 ? 1 : z > 9 ? 6 : z > 8 ? 7 : z > 7 ? 8 : 18;
351+
352+
myMap.then(function(m) {
353+
m.setBasemapOpacity(opacity, "absolute");
354+
m.changeThemeStyle("chart_si", "minvaluesize:" + minSize);
355+
m.changeThemeStyle("chart_no", "minvaluesize:" + minSize);
356+
});
357+
};
358+
}
359+
360+
(async function() {
361+
try {
362+
await loadData();
363+
document.getElementById("app-loading").style.display = "none";
364+
buildStats();
365+
buildMap();
366+
} catch(err) {
367+
document.getElementById("app-loading").innerHTML =
368+
'<p style="color:#c0392b;padding:12px">⚠️ Errore: ' + err.message + '</p>';
369+
console.error(err);
370+
}
371+
})();
372+
</script>
373+
</body>
374+
</html>

0 commit comments

Comments
 (0)