--- title: In-N-Out Seismic Challenge (CA) toc: false --- # Quel In-N-Out de Californie est le plus « secoué » ? 🌋🍔 Problématique zinzin : **si tu veux manger un burger sans mini-frisson tectonique, quel In-N-Out faut-il éviter ?** Datasets utilisés : - `docs/data/in-n-out-ca-overpass.json.js` (OpenStreetMap / Overpass, avec ville et adresse) - `docs/data/ca-earthquakes-last365d.json.js` (USGS, séismes M≥2.5 sur 365 jours) ```js const [stores, quakes] = await Promise.all([ FileAttachment("./data/in-n-out-ca-overpass.json").json(), FileAttachment("./data/ca-earthquakes-last365d.json").json(), ]); ``` ```js const [d3, topojson] = await Promise.all([ import("https://cdn.jsdelivr.net/npm/d3@7/+esm"), import("https://cdn.jsdelivr.net/npm/topojson-client@3/+esm"), ]); const usAtlasUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; const us = await fetch(usAtlasUrl).then((r) => r.json()); const states = topojson.feature(us, us.objects.states); const california = states.features.find((s) => Number(s.id) === 6); ``` ```js function haversineKm(lat1, lon1, lat2, lon2) { const toRad = (deg) => (deg * Math.PI) / 180; const R = 6371; const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; return 2 * R * Math.asin(Math.sqrt(a)); } const californiaStores = stores.filter((store) => d3.geoContains(california, [store.longitude, store.latitude]), ); const quakesInCaBox = quakes.filter( (quake) => quake.latitude >= 32 && quake.latitude <= 42.5 && quake.longitude >= -125 && quake.longitude <= -114, ); const radiusKm = 75; const shakeStats = californiaStores.map((store) => { const distances = quakesInCaBox.map((quake) => { const distanceKm = haversineKm( store.latitude, store.longitude, quake.latitude, quake.longitude, ); return { quake, distanceKm }; }); const nearest = d3.least(distances, (d) => d.distanceKm); const nearby = distances.filter((d) => d.distanceKm <= radiusKm); const quakeCount = nearby.length; const maxMagnitude = d3.max(nearby, (d) => d.quake.mag) ?? 0; const avgMagnitude = d3.mean(nearby, (d) => d.quake.mag) ?? 0; const nearestKm = nearest?.distanceKm ?? Infinity; const shakeIndex = quakeCount * 1.5 + maxMagnitude * 12 + Math.max(0, radiusKm - Math.min(nearestKm, radiusKm)) * 0.25; return { ...store, quakeCount, maxMagnitude, avgMagnitude, nearestKm, shakeIndex, }; }); const ranked = d3.sort(shakeStats, (d) => -d.shakeIndex); const top20 = ranked.slice(0, 20); const safest20 = d3.sort(shakeStats, (d) => d.shakeIndex).slice(0, 20); const boss = ranked[0]; ``` ```js display(html`

🏆 In-N-Out le plus « secoué »

${boss?.name ?? "N/A"} — ${boss?.city ?? "Ville inconnue"}

Shake Index: ${boss ? boss.shakeIndex.toFixed(1) : "N/A"}

Séismes à ≤ ${radiusKm} km: ${boss?.quakeCount ?? 0} · Magnitude max: ${boss ? boss.maxMagnitude.toFixed(1) : "0.0"} · Plus proche: ${boss && Number.isFinite(boss.nearestKm) ? boss.nearestKm.toFixed(1) : "∞"} km

`); ``` ```js display( Plot.plot({ width, height: 480, marginLeft: 280, x: { label: "Shake Index", grid: true }, y: { label: null }, marks: [ Plot.barX(top20, { x: "shakeIndex", y: (d) => `${d.name} (${d.city})`, sort: { y: "x", reverse: true }, fill: "currentColor", fillOpacity: 0.75, tip: true, title: (d) => `${d.name} — ${d.city}\nShake Index: ${d.shakeIndex.toFixed(1)}\nSéismes ≤ ${radiusKm} km: ${d.quakeCount}\nMagnitude max: ${d.maxMagnitude.toFixed(2)}\nSéisme le plus proche: ${Number.isFinite(d.nearestKm) ? d.nearestKm.toFixed(1) : "∞"} km`, }), ], }), ); ``` ```js const map = Plot.plot({ width, height: 620, projection: { type: "albers", domain: california }, color: { label: "Shake Index", scheme: "oranges", }, marks: [ Plot.geo(california, { fill: "currentColor", fillOpacity: 0.06, stroke: "currentColor", }), Plot.dot(shakeStats, { x: "longitude", y: "latitude", r: (d) => 2 + Math.min(10, d.quakeCount / 5), fill: "shakeIndex", fillOpacity: 0.85, stroke: "currentColor", strokeOpacity: 0.25, tip: true, title: (d) => `${d.name} — ${d.city}\nShake Index: ${d.shakeIndex.toFixed(1)}\nSéismes ≤ ${radiusKm} km: ${d.quakeCount}\nMagnitude max: ${d.maxMagnitude.toFixed(2)}\nSéisme le plus proche: ${Number.isFinite(d.nearestKm) ? d.nearestKm.toFixed(1) : "∞"} km`, }), ], }); display(map); ``` ```js display(html`

🔥 Top 5 des plus secoués

    ${ranked .slice(0, 5) .map( (d) => html`
  1. ${d.name} (${d.city}) — ${d.shakeIndex.toFixed(1)}
  2. `, )}

🧘 Top 5 des plus chill

    ${safest20 .slice(0, 5) .map( (d) => html`
  1. ${d.name} (${d.city}) — ${d.shakeIndex.toFixed(1)}
  2. `, )}
`); ```