6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
3127 lines
168 KiB
HTML
3127 lines
168 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="theme-color" content="#f2f2f7">
|
||
<link rel="apple-touch-icon" href="https://landvex.com/apple-touch-icon.png">
|
||
<meta name="apple-mobile-web-app-title" content="Landvex">
|
||
<link rel="manifest" href="/intelligence-manifest.json">
|
||
<title>Stockholm — Landvex Intelligence Brief</title>
|
||
<link rel="stylesheet" href="/vendor/leaflet.css"/>
|
||
<script src="/vendor/leaflet.js"></script>
|
||
<link rel="stylesheet" href="/assets/css/landvex-complete.css">
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0;-webkit-font-smoothing:antialiased}
|
||
:root{
|
||
--bg:#f2f2f7;
|
||
--s1:#ffffff;
|
||
--s2:#f2f2f7;
|
||
--s3:#e5e5ea;
|
||
--border:rgba(0,0,0,0.08);
|
||
--b2:rgba(0,0,0,0.12);
|
||
--b3:rgba(0,0,0,0.18);
|
||
--text:#1c1c1e;
|
||
--t2:rgba(28,28,30,0.72);
|
||
--t3:rgba(28,28,30,0.50);
|
||
--accent:#007aff;
|
||
--accent2:#0071e3;
|
||
--accent3:#0066cc;
|
||
--green:#34c759;
|
||
--green2:#30b352;
|
||
--green3:#248a3d;
|
||
--yellow:#ff9f0a;
|
||
--yellow2:#f7a600;
|
||
--yellow3:#b25000;
|
||
--orange:#ff6b00;
|
||
--orange2:#e05c00;
|
||
--orange3:#c04a00;
|
||
--red:#ff3b30;
|
||
--red2:#d70015;
|
||
--red3:#a2000f;
|
||
--purple:#af52de;
|
||
--purple2:#9b44c8;
|
||
--purple3:#6e1e8f;
|
||
--gold:#ff9500;
|
||
--gold2:#e08600;
|
||
--gold3:#b86a00;
|
||
--mono:'SF Mono',ui-monospace,monospace;
|
||
}
|
||
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Helvetica Neue',sans-serif;background:var(--bg);color:var(--text);overflow:hidden}
|
||
.leaflet-control-attribution{display:none !important}
|
||
.leaflet-bottom.leaflet-right{display:none !important}
|
||
|
||
|
||
/* GRID */
|
||
#app{display:grid;grid-template-columns:260px 1fr 320px;height:100vh;overflow:hidden}
|
||
|
||
/* LEFT */
|
||
#left{background:var(--s1);border-right:1px solid var(--border);overflow-y:auto;display:flex;flex-direction:column;-webkit-overflow-scrolling:touch}
|
||
#city-block{padding: 24px 14px;border-bottom:1px solid var(--border)}
|
||
#city-eyebrow{font-size:9px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent3);margin-bottom:6px}
|
||
#city-name{font-size:24px;font-weight:900;letter-spacing:-0.8px;color:var(--text);line-height:1;font-family:'Inter',system-ui,sans-serif}
|
||
#city-region{font-size:11px;color:var(--t2);margin-top:3px;font-weight:500}
|
||
#city-stats{display:grid;grid-template-columns:1fr 1fr;gap: 16px;margin-top:12px}
|
||
.cstat{background:var(--s2);border:1px solid var(--border);border-radius: var(--radius-sm);padding: 24px}
|
||
.cstat-val{font-size:16px;font-weight:800;color:var(--text);letter-spacing:-0.3px}
|
||
.cstat-lbl{font-size:9px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;color:var(--t3);margin-top:1px}
|
||
|
||
/* Profile tabs */
|
||
#profile-block{padding: 24px;border-bottom:1px solid var(--border)}
|
||
#profile-lbl{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:8px}
|
||
#profile-tabs{display:grid;grid-template-columns:1fr 1fr 1fr;gap: 16px;background:rgba(0,0,0,0.06);border-radius: var(--radius-md);padding:2px}
|
||
.ptab{padding: 24px;border-radius: var(--radius-md);border:none;background:transparent;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap: 16px;transition:all 0.15s;font-size:10px;font-weight:600;color:var(--t2);letter-spacing:0.2px}
|
||
.ptab .picon{font-size:14px;line-height:1}
|
||
.ptab:hover{background:var(--s2);color:var(--text)}
|
||
.ptab.active{background:#ffffff;color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,0.12)}
|
||
.ptab.active.p-business{border-color:var(--accent2);color:var(--accent3)}
|
||
.ptab.active.p-tourist{border-color:var(--green2);color:var(--green3)}
|
||
.ptab.active.p-safety{border-color:var(--red2);color:var(--red3)}
|
||
.ptab.active.p-investment{border-color:var(--gold2);color:var(--gold3)}
|
||
.ptab.active.p-nightlife{border-color:var(--purple2);color:var(--purple3)}
|
||
.ptab.active.p-infrastructure{border-color:var(--yellow2);color:var(--yellow3)}
|
||
|
||
/* Districts */
|
||
#district-block{padding: 24px}
|
||
#district-lbl{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:8px}
|
||
.dcard{border-radius: var(--radius-md);border:none;background:var(--s2);padding: 24px;margin-bottom:6px;cursor:pointer;transition:all 0.12s}
|
||
.dcard:hover,.dcard.active{border-color:var(--b2);background:var(--s3)}
|
||
.dcard-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:3px}
|
||
.dcard-name{font-size:13px;font-weight:700;color:var(--text)}
|
||
.dcard-score{font-size:11px;font-weight:800;padding: 24px;border-radius: var(--radius-sm);font-family:var(--mono)}
|
||
.dcard-desc{font-size:12px;color:var(--t2);line-height:1.4;margin-bottom:5px}
|
||
.dcard-contra{display:inline-flex;align-items:center;gap: 16px;font-size:9px;font-weight:700;letter-spacing:0.3px;border-radius: var(--radius-sm);padding: 24px}
|
||
.dc-low{background:rgba(22,163,74,0.12);color:#4ade80}
|
||
.dc-med{background:rgba(217,119,6,0.12);color:#fbbf24}
|
||
.dc-high{background:rgba(220,38,38,0.12);color:#f87171}
|
||
|
||
/* Data Freshness */
|
||
#freshness-block{padding: 24px;border-top:1px solid var(--border);margin-top:auto}
|
||
#freshness-title{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:8px;display:flex;align-items:center;gap: 16px}
|
||
#freshness-title::before{content:'';display:inline-block;width:5px;height:5px;border-radius: var(--radius-full);background:#22c55e;animation:pulse-ring 2s ease-in-out infinite}
|
||
.fresh-row{display:flex;align-items:center;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border)}
|
||
.fresh-row:last-child{border-bottom:none}
|
||
.fresh-source{font-size:10px;color:var(--t2);font-weight:500}
|
||
.fresh-val{font-size:9px;font-weight:700;font-family:var(--mono);display:flex;align-items:center;gap: 16px}
|
||
.fresh-dot{width:5px;height:5px;border-radius: var(--radius-full);flex-shrink:0}
|
||
.fresh-dot.live{background:#22c55e;animation:pulse-ring 1.5s ease-in-out infinite}
|
||
.fresh-dot.recent{background:#fbbf24}
|
||
.fresh-dot.stale{background:#f87171}
|
||
|
||
/* MAP */
|
||
#map-wrap{position:relative;overflow:hidden;background:var(--bg)}
|
||
#map{width:100%;height:100%}
|
||
#map-topbar{position:absolute;top:0;left:0;right:0;z-index:1000;height:44px;display:flex;align-items:center;gap: 16px;padding:0 14px;background:rgba(0,0,0,0.85);backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border-bottom:1px solid rgba(0,0,0,0.1)}
|
||
#map-logo{font-size:11px;font-weight:800;letter-spacing:-0.3px;color:var(--text);text-decoration:none}
|
||
#map-logo em{color:var(--accent3);font-style:normal}
|
||
.map-sep{width:1px;height:16px;background:rgba(0,0,0,0.12)}
|
||
#map-city-label{font-size:12px;font-weight:700;color:var(--text);flex:1}
|
||
#map-profile-chip{font-size:10px;font-weight:700;border-radius: var(--radius-sm);padding: 24px;border:1px solid;letter-spacing:0.3px}
|
||
#map-conf{font-size:10px;font-weight:600;color:var(--t2)}
|
||
|
||
/* Layer toggle bar */
|
||
#layer-bar{position:absolute;top:44px;left:0;right:0;z-index:999;display:flex;align-items:center;gap: 16px;padding: 24px;background:#ffffff;border-bottom:1px solid rgba(0,0,0,0.1);overflow-x:auto;scrollbar-width:none;flex-wrap:nowrap;-webkit-overflow-scrolling:touch}
|
||
#layer-bar::-webkit-scrollbar{display:none}
|
||
#layer-bar-label{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);white-space:nowrap;margin-right:4px;flex-shrink:0}
|
||
.ltoggle{display:flex;align-items:center;gap: 16px;font-size:11px;font-weight:600;color:var(--t2);background:rgba(0,0,0,0.05);border:none;border-radius: var(--radius-xl);padding: 24px;cursor:pointer;transition:all 0.15s;white-space:nowrap;user-select:none;flex-shrink:0}
|
||
.ltoggle:hover{border-color:var(--b2);color:var(--text)}
|
||
.ltoggle.on{background:var(--accent);color: #1d1d1ffff;}
|
||
.ltoggle-dot{width:6px;height:6px;border-radius: var(--radius-full);flex-shrink:0}
|
||
.ltoggle-dot.d-districts{background:#4aafef}
|
||
.ltoggle-dot.d-roadwork{background:#fb923c}
|
||
.ltoggle-dot.d-pulse{background:#c084fc;animation:pulse-ring 1.5s ease-in-out infinite}
|
||
.ltoggle-dot.d-alerts{background:#f87171}
|
||
|
||
.leaflet-control-zoom{margin-top:96px !important;margin-left:10px !important}
|
||
.leaflet-control-zoom a{background:rgba(11,18,32,0.9) !important;color:var(--text) !important;border-color:var(--b2) !important;width:30px !important;height:30px !important;line-height:30px !important;border-radius: var(--radius-sm) !important;font-size:16px !important}
|
||
.leaflet-popup-pane{z-index:1500 !important}
|
||
.leaflet-popup-content-wrapper{background:#ffffff !important;border:none !important;border-radius: var(--radius-lg) !important;box-shadow:0 8px 32px rgba(0,0,0,0.15) !important;padding:0 !important;max-height:60vh !important;overflow-y:auto !important}
|
||
.leaflet-popup-content{margin:0 !important;width:260px !important;max-height:55vh !important;overflow-y:auto !important}
|
||
@media(max-width:900px){
|
||
/* Hide Leaflet popup on mobile — use bottom sheet instead */
|
||
.leaflet-popup{display:none !important}
|
||
}
|
||
|
||
/* MOBILE BOTTOM SHEET */
|
||
#bottom-sheet{
|
||
display:none;
|
||
position:fixed;
|
||
left:0;right:0;bottom:0;
|
||
z-index:9999;
|
||
background:#ffffff;
|
||
border-top:1px solid rgba(0,0,0,0.1);
|
||
border-radius: var(--radius-lg) 16px 0 0;
|
||
box-shadow:0 -4px 24px rgba(0,0,0,0.12);
|
||
max-height:72vh;
|
||
overflow-y:auto;
|
||
-webkit-overflow-scrolling:touch;
|
||
transform:translateY(100%);
|
||
transition:transform 0.28s cubic-bezier(0.32,0.72,0,1);
|
||
}
|
||
#bottom-sheet.open{
|
||
display:block;
|
||
transform:translateY(0);
|
||
}
|
||
#bottom-sheet-handle{
|
||
width:36px;height:4px;border-radius: var(--radius-sm);
|
||
background:rgba(0,0,0,0.2);
|
||
margin:10px auto 0;
|
||
}
|
||
#bottom-sheet-close{
|
||
position:absolute;top:12px;right:14px;
|
||
background:rgba(0,0,0,0.1);border:none;
|
||
color:var(--text);font-size:16px;
|
||
width:28px;height:28px;border-radius: var(--radius-full);
|
||
cursor:pointer;display:flex;align-items:center;justify-content:center;
|
||
line-height:1;
|
||
}
|
||
#bottom-sheet-content{
|
||
padding:8px 0 env(safe-area-inset-bottom,16px);
|
||
}
|
||
#bottom-sheet-backdrop{
|
||
display:none;
|
||
position:fixed;inset:0;
|
||
z-index:9998;
|
||
background:rgba(0,0,0,0.4);
|
||
}
|
||
#bottom-sheet-backdrop.open{display:block;}
|
||
.leaflet-popup-tip{background:#ffffff !important}
|
||
.leaflet-popup-close-button{color:var(--t2) !important;top:8px !important;right:10px !important;font-size:16px !important}
|
||
|
||
/* Popup */
|
||
.mpop{padding: 24px;font-family:-apple-system,sans-serif}
|
||
.mpop-eyebrow{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--accent3);margin-bottom:3px}
|
||
.mpop-name{font-size:14px;font-weight:800;color:var(--text);margin-bottom:8px;letter-spacing:-0.3px}
|
||
.mpop-score-row{display:flex;align-items:baseline;gap: 16px;margin-bottom:10px}
|
||
.mpop-score{font-size:26px;font-weight:900;font-family:var(--mono);letter-spacing:-1px}
|
||
.mpop-score-denom{font-size:12px;color:var(--t3);font-family:var(--mono)}
|
||
.mpop-score-label{font-size:10px;color:var(--t2);line-height:1.3}
|
||
.mpop-bars{display:flex;flex-direction:column;gap: 16px;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid var(--border)}
|
||
.mpop-bar-row{display:flex;align-items:center;gap: 16px}
|
||
.mpop-bar-lbl{font-size:9px;font-weight:600;letter-spacing:0.3px;text-transform:uppercase;color:var(--t3);width:72px;flex-shrink:0}
|
||
.mpop-bar-track{flex:1;height:3px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.04)}
|
||
.mpop-bar-fill{height:100%;border-radius: var(--radius-sm);transition:width 0.3s}
|
||
.mpop-bar-val{font-size:10px;font-weight:700;font-family:var(--mono);width:24px;text-align:right}
|
||
.mpop-contra{font-size:10px;border-radius: var(--radius-sm);padding: 24px;line-height:1.4;margin-bottom:8px}
|
||
.mpop-contra.low{background:rgba(22,163,74,0.1);color:#4ade80;border:1px solid rgba(22,163,74,0.15)}
|
||
.mpop-contra.medium{background:rgba(217,119,6,0.1);color:#fbbf24;border:1px solid rgba(217,119,6,0.15)}
|
||
.mpop-contra.high{background:rgba(220,38,38,0.1);color:#f87171;border:1px solid rgba(220,38,38,0.15)}
|
||
.mpop-contra-lbl{font-weight:800;font-size:9px;letter-spacing:0.5px;text-transform:uppercase;display:block;margin-bottom:2px}
|
||
.mpop-insight{font-size:10px;color:var(--t2);line-height:1.5;font-style:italic}
|
||
|
||
/* Roadwork popup */
|
||
.rwpop{padding: 24px;font-family:-apple-system,sans-serif}
|
||
.rwpop-eyebrow{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:#fb923c;margin-bottom:3px}
|
||
.rwpop-name{font-size:13px;font-weight:800;color:var(--text);margin-bottom:6px}
|
||
.rwpop-desc{font-size:11px;color:var(--t2);line-height:1.5;margin-bottom:6px}
|
||
.rwpop-badge{display:inline-flex;align-items:center;gap: 16px;font-size:9px;font-weight:700;border-radius: var(--radius-sm);padding: 24px;background:rgba(251,146,60,0.12);border:1px solid rgba(251,146,60,0.25);color:#fb923c}
|
||
.rwpop-src{font-size:9px;color:var(--t3);margin-top:6px}
|
||
|
||
/* Live Pulse animation */
|
||
@keyframes pulse-ring {
|
||
0%{opacity:1;transform:scale(1)}
|
||
50%{opacity:0.6;transform:scale(1.2)}
|
||
100%{opacity:1;transform:scale(1)}
|
||
}
|
||
@keyframes pulse-wave {
|
||
0%{transform:scale(1);opacity:0.7}
|
||
100%{transform:scale(3.2);opacity:0}
|
||
}
|
||
.src-chain{font-size:9px;color:rgba(220,232,245,0.25);border-top:1px solid rgba(0,0,0,0.03);padding-top:5px;margin-top:5px;line-height:1.6}
|
||
.src-tag{display:inline-flex;align-items:center;gap: 16px;background:rgba(0,0,0,0.04);border-radius: var(--radius-sm);padding: 24px;margin:1px 2px 1px 0;font-size:9px;font-weight:600;color:rgba(220,232,245,0.35)}
|
||
.src-data{color:#38bdf8}.src-obs{color:#4ade80}.src-model{color:#a78bfa}.src-forecast{color:#f59e0b}
|
||
.pulse-marker{pointer-events:none}
|
||
.pulse-outer{position:absolute;border-radius: var(--radius-full);animation:pulse-wave 2.2s ease-out infinite}
|
||
.pulse-inner{position:absolute;border-radius: var(--radius-full);transform:translate(-50%,-50%)}
|
||
|
||
/* RIGHT */
|
||
#right{background:var(--s1);border-left:1px solid rgba(0,0,0,0.08);overflow-y:auto;display:flex;flex-direction:column;scrollbar-width:none}
|
||
#right-header{padding: 24px 10px;border-bottom:1px solid var(--border)}
|
||
#right-profile-label{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:4px}
|
||
#right-title{font-size:17px;font-weight:700;color:var(--text);letter-spacing:-0.3px}
|
||
#kpi-grid{padding: 24px 8px;display:grid;grid-template-columns:1fr 1fr;gap: 16px;border-bottom:1px solid var(--border)}
|
||
.kpi{background:var(--s2);border:none;border-radius: var(--radius-md);padding: 24px}
|
||
.kpi-label{font-size:10px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;margin-bottom:4px}
|
||
.kpi-value{font-size:22px;font-weight:800;letter-spacing:-0.5px;font-family:var(--mono);line-height:1}
|
||
.kpi-sub{font-size:11px;color:var(--t2);margin-top:3px;line-height:1.3}
|
||
.kpi.span2{grid-column:span 2}
|
||
|
||
#insights-block{padding: 24px;border-bottom:1px solid var(--border)}
|
||
#insights-title{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:10px}
|
||
.insight-row{display:flex;gap: 16px;align-items:flex-start;padding:7px 0;border-bottom:1px solid var(--border)}
|
||
.insight-row:last-child{border-bottom:none}
|
||
.insight-icon{font-size:11px;font-weight:800;color:var(--accent3);flex-shrink:0;margin-top:3px;opacity:0.9;width:12px;text-align:center;line-height:1}
|
||
.insight-text{font-size:13px;color:rgba(28,28,30,0.8);line-height:1.6}
|
||
.insight-text strong{color:var(--text);font-weight:700}
|
||
|
||
/* Contradiction Alerts */
|
||
#contra-block{padding: 24px;border-bottom:1px solid var(--border)}
|
||
#contra-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
|
||
#contra-title{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);display:flex;align-items:center;gap: 16px}
|
||
.contra-live-dot{width:5px;height:5px;border-radius: var(--radius-full);background:#f87171;animation:pulse-ring 1.8s ease-in-out infinite}
|
||
.contra-count{font-size:9px;font-weight:700;font-family:var(--mono);background:rgba(220,38,38,0.15);border:1px solid rgba(220,38,38,0.25);color:#f87171;padding: 24px;border-radius: var(--radius-md)}
|
||
.contra-card{border-radius: var(--radius-sm);border:1px solid var(--border);background:var(--s2);padding: 24px;margin-bottom:5px}
|
||
.contra-card.c-medium{border-left:3px solid rgba(217,119,6,0.5)}
|
||
.contra-card.c-high{border-left:3px solid rgba(220,38,38,0.6)}
|
||
.contra-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:3px}
|
||
.contra-card-name{font-size:11px;font-weight:700;color:var(--text)}
|
||
.contra-card-badge{font-size:8px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;padding: 24px;border-radius: var(--radius-sm)}
|
||
.contra-card-badge.medium{background:rgba(217,119,6,0.12);color:#fbbf24}
|
||
.contra-card-badge.high{background:rgba(220,38,38,0.12);color:#f87171}
|
||
.contra-card-desc{font-size:10px;color:var(--t2);line-height:1.4;margin-bottom:6px}
|
||
.contra-card-meta{display:flex;align-items:center;justify-content:space-between}
|
||
.contra-card-date{font-size:9px;color:var(--t3);font-family:var(--mono)}
|
||
.contra-view-btn{font-size:9px;font-weight:700;letter-spacing:0.3px;padding: 24px;border-radius: var(--radius-sm);border:1px solid var(--b2);background:transparent;color:var(--accent3);cursor:pointer;transition:all 0.12s}
|
||
.contra-view-btn:hover{background:rgba(74,175,239,0.1);border-color:var(--accent2)}
|
||
|
||
#caution-block{padding: 24px;border-bottom:1px solid var(--border)}
|
||
.caution-card{background:rgba(220,38,38,0.06);border:1px solid rgba(220,38,38,0.15);border-radius: var(--radius-md);padding: 24px}
|
||
.caution-lbl{font-size:10px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;color:var(--red3);margin-bottom:6px}
|
||
.caution-row{display:flex;gap: 16px;align-items:flex-start;padding:3px 0;font-size:12px;color:var(--text);line-height:1.5}
|
||
.caution-dot{width:4px;height:4px;border-radius: var(--radius-full);background:var(--red2);flex-shrink:0;margin-top:4px}
|
||
|
||
#right-footer{padding: 24px;font-size:9px;color:var(--t3);line-height:1.4;border-top:1px solid var(--border)}
|
||
#right-footer a{color:var(--accent3);text-decoration:none}
|
||
|
||
/* MOBILE — iOS native style */
|
||
@media(max-width:900px){
|
||
html,body{height:auto;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch}
|
||
#app{display:flex;flex-direction:column;height:auto;min-height:100dvh;overflow:visible}
|
||
|
||
/* TOP BAR — kompakt native */
|
||
#left{order:1;border-right:none;border-bottom:none;background:var(--s1);display:flex;flex-direction:column;height:auto;overflow:visible}
|
||
#city-block{padding: 24px 8px}
|
||
#city-eyebrow{font-size:9px;letter-spacing:1.5px}
|
||
#city-name{font-size:26px;letter-spacing:-0.8px}
|
||
#city-region{font-size:11px;margin-top:2px}
|
||
#city-stats{display:flex;gap: 16px;margin-top:8px;flex-wrap:nowrap}
|
||
.cstat{background:transparent;border:none;padding:0;flex-shrink:0}
|
||
.cstat-val{font-size:15px;font-weight:800}
|
||
.cstat-lbl{font-size:9px}
|
||
/* hide less important stats */
|
||
.cstat:nth-child(3),.cstat:nth-child(4){display:none}
|
||
|
||
/* PROFILE TABS — iOS segmented control style */
|
||
#profile-block{padding: 24px;border-bottom:1px solid var(--border)}
|
||
#profile-lbl{display:none}
|
||
#profile-tabs{display:flex;overflow-x:auto;gap:0;scrollbar-width:none;background:rgba(0,0,0,0.04);border-radius: var(--radius-md);padding:2px}
|
||
#profile-tabs::-webkit-scrollbar{display:none}
|
||
.ptab{flex-shrink:0;flex-direction:row;gap: 16px;padding: 24px;font-size:12px;font-weight:600;border-radius: var(--radius-md);border:none;background:transparent;white-space:nowrap;min-width:auto;color:rgba(220,232,245,0.55)}
|
||
.ptab .picon{display:none}
|
||
.ptab.active{background:rgba(220,232,245,0.15);color:#dce8f5;font-weight:700}
|
||
/* Active profile colors */
|
||
.ptab.active.p-business{background:rgba(45,143,207,0.25);color:#4aafef}
|
||
.ptab.active.p-tourist{background:rgba(34,197,94,0.2);color:#4ade80}
|
||
.ptab.active.p-safety{background:rgba(220,38,38,0.2);color:#f87171}
|
||
.ptab.active.p-investment{background:rgba(217,119,6,0.2);color:#fbbf24}
|
||
.ptab.active.p-nightlife{background:rgba(147,51,234,0.2);color:#c084fc}
|
||
.ptab.active.p-infrastructure{background:rgba(180,83,9,0.2);color:#fcd34d}
|
||
|
||
/* HIDE DISTRICT LIST + FRESHNESS on mobile */
|
||
#district-block{display:none}
|
||
#freshness-block{display:none}
|
||
|
||
/* MAP — hero element, karta first */
|
||
#map-wrap{order:2;height:60vh;min-height:340px;max-height:520px;position:relative}
|
||
#map-topbar{height:44px;padding:0 14px;display:flex;align-items:center}
|
||
#map-logo{font-size:14px;font-weight:800}
|
||
.map-sep{display:none}
|
||
#map-city-label{font-size:12px;font-weight:600}
|
||
#map-conf{display:none}
|
||
#map-profile-chip{font-size:10px;padding: 24px;border-radius: var(--radius-xl)}
|
||
|
||
/* Layer bar — scrollable, compact */
|
||
#layer-bar{
|
||
top:44px;left:0;right:0;
|
||
transform:none;
|
||
padding: 24px;
|
||
gap: 16px;
|
||
background:#ffffff;
|
||
border-bottom:1px solid rgba(0,0,0,0.1);
|
||
overflow-x:auto;
|
||
scrollbar-width:none;
|
||
flex-wrap:nowrap;
|
||
-webkit-overflow-scrolling:touch
|
||
}
|
||
#layer-bar::-webkit-scrollbar{display:none}
|
||
.ltoggle{font-size:10px;padding: 24px;border-radius: var(--radius-xl);gap: 16px;flex-shrink:0;color:rgba(28,28,30,0.65)}
|
||
.ltoggle-dot{width:6px;height:6px}
|
||
.leaflet-control-zoom{margin-top:92px !important;margin-left:10px !important}
|
||
.leaflet-control-zoom a{background:#ffffff !important;color:#1c1c1e !important;border:none !important;border-bottom:1px solid rgba(0,0,0,0.08) !important}
|
||
.leaflet-control-zoom a:last-child{border-bottom:none !important}
|
||
.leaflet-control-zoom a{width:30px !important;height:30px !important;line-height:30px !important;font-size:16px !important;border-radius: var(--radius-md) !important}
|
||
|
||
/* RIGHT PANEL — card stack below map */
|
||
#right{order:3;border-left:none;border-top:1px solid var(--border);overflow:visible;height:auto}
|
||
#right-header{padding: 24px 8px}
|
||
#right-title{font-size:14px;font-weight:800}
|
||
#kpi-grid{grid-template-columns:1fr 1fr;gap: 16px;padding:0 16px 12px}
|
||
.kpi{border-radius: var(--radius-md);padding: 24px}
|
||
.kpi-label{font-size:9px}
|
||
.kpi-value{font-size:17px}
|
||
.kpi-sub{font-size:9px}
|
||
/* All right panel sections */
|
||
#events-engine,#weekly-brief,#macro-block,#news-block,#events-block,
|
||
#insights-block,#caution-block,#contra-alerts-section{padding: 24px}
|
||
#right-footer{padding: 24px;font-size:9px}
|
||
}
|
||
|
||
@media(max-width:480px){
|
||
#city-name{font-size:22px}
|
||
#map-wrap{height:55vh;min-height:300px}
|
||
.ptab{padding: 24px;font-size:11px}
|
||
#kpi-grid{gap: 16px}
|
||
.kpi-value{font-size:15px}
|
||
}
|
||
|
||
@media(max-width:900px){
|
||
.label-marker{display:none !important}
|
||
}
|
||
|
||
/* Bottom sheet content — force dark text on white */
|
||
#bottom-sheet-content .mpop{padding: 24px}
|
||
#bottom-sheet-content .mpop-eyebrow{color:#0066cc !important}
|
||
#bottom-sheet-content .mpop-name{color:#1c1c1e !important}
|
||
#bottom-sheet-content .mpop-score{color:inherit}
|
||
#bottom-sheet-content .mpop-score-denom{color:rgba(28,28,30,0.4) !important}
|
||
#bottom-sheet-content .mpop-score-label{color:rgba(28,28,30,0.55) !important}
|
||
#bottom-sheet-content .mpop-bar-lbl{color:rgba(28,28,30,0.5) !important}
|
||
#bottom-sheet-content .mpop-bar-track{background:rgba(0,0,0,0.08) !important}
|
||
#bottom-sheet-content .mpop-contra{border:1px solid rgba(0,0,0,0.1) !important}
|
||
#bottom-sheet-content .mpop-contra.low{background:rgba(22,163,74,0.08) !important;color:#166534 !important}
|
||
#bottom-sheet-content .mpop-contra.medium{background:rgba(217,119,6,0.08) !important;color:#92400e !important}
|
||
#bottom-sheet-content .mpop-contra.high{background:rgba(220,38,38,0.08) !important;color:#991b1b !important}
|
||
#bottom-sheet-content .mpop-contra-lbl{color:inherit !important}
|
||
#bottom-sheet-content .mpop-insight{color:rgba(28,28,30,0.65) !important}
|
||
#bottom-sheet-content .mpop-bar-val{color:rgba(28,28,30,0.7) !important}
|
||
|
||
/* All sub-popup sections in bottom sheet */
|
||
#bottom-sheet-content [style*="color:#dce8f5"]{color:#1c1c1e !important}
|
||
#bottom-sheet-content [style*="color:rgba(220,232,245"]{color:rgba(28,28,30,0.6) !important}
|
||
#bottom-sheet-content [style*="background:rgba(0,0,0,0.04)"]{background:rgba(0,0,0,0.04) !important}
|
||
#bottom-sheet-content [style*="background:rgba(0,0,0,0.03)"]{background:rgba(0,0,0,0.03) !important}
|
||
#bottom-sheet-content [style*="background:var(--s2)"]{background:#f2f2f7 !important}
|
||
#bottom-sheet-content [style*="background:rgba(0,0,0,0.04)"]{background:rgba(0,0,0,0.05) !important}
|
||
#bottom-sheet-content [style*="border:1px solid rgba(255,255,255"]{border-color:rgba(0,0,0,0.08) !important}
|
||
#bottom-sheet-content [style*="border-top:1px solid rgba(255,255,255"]{border-top-color:rgba(0,0,0,0.08) !important}
|
||
#bottom-sheet-content [style*="color:rgba(220,232,245,0.3"]{color:rgba(28,28,30,0.45) !important}
|
||
#bottom-sheet-content [style*="color:rgba(220,232,245,0.35"]{color:rgba(28,28,30,0.45) !important}
|
||
#bottom-sheet-content [style*="color:rgba(220,232,245,0.4"]{color:rgba(28,28,30,0.55) !important}
|
||
#bottom-sheet-content [style*="color:rgba(220,232,245,0.5"]{color:rgba(28,28,30,0.65) !important}
|
||
#bottom-sheet-content [style*="color:rgba(220,232,245,0.55"]{color:rgba(28,28,30,0.7) !important}
|
||
#bottom-sheet-content [style*="color:rgba(220,232,245,0.6"]{color:rgba(28,28,30,0.75) !important}
|
||
#bottom-sheet-content [style*="color:#dce8f5"]{color:#1c1c1e !important}
|
||
#bottom-sheet-content [style*="font-family:var(--mono)"]{color:#1c1c1e}
|
||
#bottom-sheet-content [style*="background:rgba(239,68,68,0.1"]{background:rgba(239,68,68,0.07) !important}
|
||
#bottom-sheet-content [style*="background:rgba(34,197,94,0.1"]{background:rgba(34,197,94,0.07) !important}
|
||
#bottom-sheet-content [style*="background:rgba(217,119,6,0.1"]{background:rgba(217,119,6,0.07) !important}
|
||
#bottom-sheet-content [style*="background:rgba(249,115,22,0.1"]{background:rgba(249,115,22,0.07) !important}
|
||
#bottom-sheet-content .src-chain{color:rgba(28,28,30,0.35) !important;border-top-color:rgba(0,0,0,0.06) !important}
|
||
#bottom-sheet-content .src-tag{background:rgba(0,0,0,0.05) !important;color:rgba(28,28,30,0.45) !important}
|
||
|
||
</style>
|
||
<script>
|
||
if('serviceWorker' in navigator){
|
||
window.addEventListener('load',()=>{
|
||
navigator.serviceWorker.register('/intelligence-sw.js')
|
||
.then(()=>console.log('Landvex SW registered'))
|
||
.catch(e=>console.warn('SW failed:',e));
|
||
});
|
||
}
|
||
</script>
|
||
<style>
|
||
@media (max-width: 640px) {
|
||
.hero h1 { font-size: 2rem !important; }
|
||
.hero p { font-size: 1rem !important; }
|
||
.card { padding: 1.5rem !important; }
|
||
.grid { grid-template-columns: 1fr !important; }
|
||
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
|
||
}
|
||
</style>
|
||
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@type": [
|
||
"Organization",
|
||
"Corporation"
|
||
],
|
||
"@id": "https://landvex.com/#organization",
|
||
"name": "Landvex",
|
||
"alternateName": [
|
||
"LandveX",
|
||
"Landvex AB",
|
||
"LandveX AB"
|
||
],
|
||
"url": "https://landvex.com",
|
||
"logo": {
|
||
"@type": "ImageObject",
|
||
"url": "https://landvex.com/apple-touch-icon.png",
|
||
"width": 180,
|
||
"height": 180
|
||
},
|
||
"foundingDate": "2024",
|
||
"foundingLocation": {
|
||
"@type": "Place",
|
||
"addressLocality": "Gothenburg",
|
||
"addressRegion": "Gothenburg",
|
||
"addressCountry": "SE"
|
||
},
|
||
"legalName": "Landvex Inc",
|
||
"address": {
|
||
"@type": "PostalAddress",
|
||
"addressLocality": "Houston",
|
||
"addressRegion": "TX",
|
||
"addressCountry": "US"
|
||
},
|
||
"location": [
|
||
{
|
||
"@type": "Place",
|
||
"name": "Houston, Texas, USA (US HQ)",
|
||
"address": {
|
||
"@type": "PostalAddress",
|
||
"addressLocality": "Houston",
|
||
"addressRegion": "TX",
|
||
"addressCountry": "US"
|
||
}
|
||
},
|
||
{
|
||
"@type": "Place",
|
||
"name": "Tyresö, Sweden (EU HQ)",
|
||
"address": {
|
||
"@type": "PostalAddress",
|
||
"addressLocality": "Gothenburg",
|
||
"addressRegion": "Gothenburg",
|
||
"addressCountry": "SE"
|
||
}
|
||
}
|
||
],
|
||
"taxID": "pending-EIN",
|
||
"description": "Landvex is a decision intelligence company that identifies contradictions between official narratives and observed physical reality. Using the quiXzoom field observation network and the AMOS AI analysis engine, Landvex delivers infrastructure risk indexes, urban intelligence scores, and contradiction reports to infrastructure owners, municipalities, enterprises, and investors globally.",
|
||
"disambiguatingDescription": "Landvex (LandveX) is a decision intelligence company with US headquarters in Houston, Texas (Landvex Inc.) and European headquarters in Tyresö, Sweden (Landvex AB, org.nr 559141-7042). Landvex delivers control intelligence — infrastructure risk scores, urban intelligence, and contradiction analysis — to municipalities, enterprises, infrastructure operators and investors. Landvex is not a UK meat wholesaler, food distributor, logistics company, package tracker, or land investment platform.",
|
||
"knowsAbout": [
|
||
"control intelligence",
|
||
"decision intelligence",
|
||
"infrastructure risk assessment",
|
||
"urban intelligence",
|
||
"field data collection",
|
||
"contradiction detection",
|
||
"physical world analytics",
|
||
"geospatial analytics"
|
||
],
|
||
"brand": {
|
||
"@type": "Brand",
|
||
"name": "Landvex",
|
||
"slogan": "Control intelligence for the physical world."
|
||
},
|
||
"sameAs": [
|
||
"https://landvex.com",
|
||
"https://www.linkedin.com/company/landvex",
|
||
"https://x.com/landvex",
|
||
"https://twitter.com/landvex",
|
||
"https://www.crunchbase.com/organization/landvex",
|
||
"https://github.com/landvex",
|
||
"https://www.instagram.com/landvex"
|
||
],
|
||
"contactPoint": {
|
||
"@type": "ContactPoint",
|
||
"email": "contact@landvex.com",
|
||
"contactType": "sales"
|
||
},
|
||
"slogan": "Where reported reality conflicts with observed reality."
|
||
}
|
||
</script>
|
||
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
|
||
<!-- LEFT -->
|
||
<div id="left">
|
||
<div id="city-block">
|
||
<div id="city-eyebrow">Landvex Intelligence Brief</div>
|
||
<div id="city-name">STOCKHOLM</div>
|
||
<div id="city-region">Kingdom of Sweden · Northern Europe</div>
|
||
<div id="city-stats">
|
||
<div class="cstat"><div class="cstat-val">94%</div><div class="cstat-lbl">Confidence</div></div>
|
||
<div class="cstat"><div class="cstat-val" id="district-count">26</div><div class="cstat-lbl">Districts</div></div>
|
||
<div class="cstat"><div class="cstat-val">Medium</div><div class="cstat-lbl">Contradiction</div></div>
|
||
<div class="cstat"><div class="cstat-val">Jun 2026</div><div class="cstat-lbl">Updated</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="profile-block">
|
||
<div id="profile-lbl">Intelligence Profile</div>
|
||
<div id="profile-tabs">
|
||
<button class="ptab p-business active" data-p="business" onclick="setProfile('business',this)">Business</button>
|
||
<button class="ptab p-tourist" data-p="tourist" onclick="setProfile('tourist',this)">Tourist</button>
|
||
<button class="ptab p-safety" data-p="safety" onclick="setProfile('safety',this)">Safety</button>
|
||
<button class="ptab p-investment" data-p="investment" onclick="setProfile('investment',this)">Investment</button>
|
||
<button class="ptab p-nightlife" data-p="nightlife" onclick="setProfile('nightlife',this)">Nightlife</button>
|
||
<button class="ptab p-infrastructure" data-p="infrastructure" onclick="setProfile('infrastructure',this)">Infrastructure</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="district-block">
|
||
<div id="district-lbl">Districts · ranked by profile</div>
|
||
<div id="district-list"></div>
|
||
</div>
|
||
|
||
<!-- Data Freshness -->
|
||
<div id="freshness-block">
|
||
<div id="freshness-title">Data Freshness</div>
|
||
<div class="fresh-row">
|
||
<div class="fresh-source">quiXzoom observations</div>
|
||
<div class="fresh-val"><div class="fresh-dot recent"></div><span id="fresh-qx">Jun 18, 2026</span></div>
|
||
</div>
|
||
<div class="fresh-row">
|
||
<div class="fresh-source">Official city data</div>
|
||
<div class="fresh-val"><div class="fresh-dot recent"></div><span id="fresh-official">Jun 15, 2026</span></div>
|
||
</div>
|
||
<div class="fresh-row">
|
||
<div class="fresh-source">Trafikverket roadwork</div>
|
||
<div class="fresh-val"><div class="fresh-dot live"></div><span id="fresh-traffic">Live · <span id="fresh-traffic-time">--:--</span></span></div>
|
||
</div>
|
||
<div class="fresh-row">
|
||
<div class="fresh-source">Live Pulse activity</div>
|
||
<div class="fresh-val"><div class="fresh-dot live"></div><span>Streaming</span></div>
|
||
</div>
|
||
<div class="fresh-row">
|
||
<div class="fresh-source">Police events</div>
|
||
<div class="fresh-val"><div class="fresh-dot live"></div><span id="police-fresh">Loading...</span></div>
|
||
</div>
|
||
<div class="fresh-row">
|
||
<div class="fresh-source">SCB demographics</div>
|
||
<div class="fresh-val"><div class="fresh-dot recent"></div><span>2024 annual</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MOBILE BOTTOM SHEET -->
|
||
<div id="bottom-sheet-backdrop" onclick="closeBottomSheet()"></div>
|
||
<div id="bottom-sheet">
|
||
<div id="bottom-sheet-handle"></div>
|
||
<button id="bottom-sheet-close" onclick="closeBottomSheet()">×</button>
|
||
<div id="bottom-sheet-content"></div>
|
||
</div>
|
||
|
||
<!-- MAP -->
|
||
<div id="map-wrap">
|
||
<div id="map-topbar">
|
||
<a href="https://landvex.com" id="map-logo">Land<em>vex</em></a>
|
||
<div class="map-sep"></div>
|
||
<div id="map-city-label">Stockholm — <span id="map-profile-label">Business</span></div>
|
||
<div id="map-profile-chip">Business</div>
|
||
<div id="map-conf">Source First</div>
|
||
<div>
|
||
<button onclick="map.setView([59.370,18.040],9)">SE</button>
|
||
<button onclick="map.setView([52.5,10.0],5)">EU</button>
|
||
<button onclick="map.setView([25.0,40.0],3)">Global</button>
|
||
</div>
|
||
</div>
|
||
<!-- Layer Toggles -->
|
||
<div id="layer-bar">
|
||
|
||
<div class="ltoggle on" id="ltog-districts" onclick="toggleLayer('districts',this)">
|
||
<div class="ltoggle-dot d-districts"></div>Areas
|
||
</div>
|
||
<div class="ltoggle" id="ltog-roadwork" onclick="toggleLayer('roadwork',this)">
|
||
<div class="ltoggle-dot d-roadwork"></div>Road works
|
||
</div>
|
||
<div class="ltoggle" id="ltog-pulse" onclick="toggleLayer('pulse',this)">
|
||
<div class="ltoggle-dot d-pulse"></div>Activity
|
||
</div>
|
||
<div class="ltoggle" id="ltog-alerts" onclick="toggleLayer('alerts',this)">
|
||
<div class="ltoggle-dot d-alerts"></div>Contradictions
|
||
</div>
|
||
<div class="ltoggle on" id="ltog-police" onclick="togglePolice(this)">
|
||
<div class="ltoggle-dot"></div>Police
|
||
</div>
|
||
<div class="ltoggle" id="ltog-umi" onclick="toggleLayer('umi',this)">
|
||
<div class="ltoggle-dot"></div>Momentum
|
||
</div>
|
||
</div>
|
||
<div id="map"></div>
|
||
<div id="map-legend">
|
||
<div>Intelligence Score</div>
|
||
<div>
|
||
<div><div></div>Strong (8–10)</div>
|
||
<div><div></div>Moderate (6–8)</div>
|
||
<div><div></div>Weak (4–6)</div>
|
||
<div><div></div>Poor (<4)</div>
|
||
<div><div></div>Contradiction</div>
|
||
</div>
|
||
<div>Tap any area for details</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT -->
|
||
<div id="right">
|
||
<div id="right-header">
|
||
<div id="right-profile-label">Profile</div>
|
||
<div id="right-title">Business</div>
|
||
</div>
|
||
<div id="kpi-grid"></div>
|
||
<div id="insights-block">
|
||
<div id="insights-title">Key Insights</div>
|
||
<div id="insights-list"></div>
|
||
</div>
|
||
|
||
<!-- Contradiction Alerts -->
|
||
<div id="contra-block">
|
||
<div id="contra-header">
|
||
<div id="contra-title"><div class="contra-live-dot"></div>Contradiction Alerts</div>
|
||
<div class="contra-count" id="contra-count">2 active</div>
|
||
</div>
|
||
<div id="contra-list"></div>
|
||
</div>
|
||
|
||
<div id="caution-block">
|
||
<div class="caution-card">
|
||
<div class="caution-lbl">Operational Caution</div>
|
||
<div id="caution-list"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Change Events Engine -->
|
||
<div id="events-engine">
|
||
<div>
|
||
Change Events
|
||
<span id="event-critical-count">0 critical</span>
|
||
</div>
|
||
<div id="event-list"></div>
|
||
</div>
|
||
|
||
<!-- Weekly Intelligence Brief -->
|
||
<div id="weekly-brief">
|
||
<div>
|
||
Weekly Intelligence Brief
|
||
<span>Week 25 · Jun 16–21</span>
|
||
</div>
|
||
<div>
|
||
<div>01 · Security deterioration, Rinkeby</div>
|
||
<div>Police events up 280% vs 4-week avg. Contradiction Index: HIGH. Official classification has not been updated. Recommend: escalate to municipal review.</div>
|
||
<div>Sources: Polisen.se · quiXzoom 1 102 obs · Contradiction confirmed</div>
|
||
</div>
|
||
<div>
|
||
<div>02 · Strategic signal: Nacka acceleration</div>
|
||
<div>Tunnel milestone confirmed 14 months ahead of schedule. Commercial permit applications +34% MoM. Investment thesis: accelerating.</div>
|
||
<div>Sources: Trafikverket · Nacka kommun · quiXzoom 389 obs</div>
|
||
</div>
|
||
<div>
|
||
<div>03 · Commercial vacancy rising, Södermalm</div>
|
||
<div>18 new vacancies detected on Götgatan corridor. Pattern matches early-stage commercial stress. Monitor: if +8 more in 30 days, escalate to Important.</div>
|
||
<div>Sources: quiXzoom 47 obs · Fastighetsregistret · quiXzoom Change Detection</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- City macro intelligence -->
|
||
<div id="macro-block">
|
||
<div>City Macro Intelligence</div>
|
||
<div id="macro-grid">
|
||
<div>
|
||
<div>GDP / capita</div>
|
||
<div id="gdp-val">Loading</div>
|
||
<div>USD · World Bank</div>
|
||
</div>
|
||
<div>
|
||
<div>Political Stability</div>
|
||
<div>8.9<span>/10</span></div>
|
||
<div>WGI 2023 · top 5% global</div>
|
||
</div>
|
||
<div>
|
||
<div>Rule of Law</div>
|
||
<div>9.2<span>/10</span></div>
|
||
<div>WGI 2023 · Rank 6 global</div>
|
||
</div>
|
||
<div>
|
||
<div>Corruption Index</div>
|
||
<div>82<span>/100</span></div>
|
||
<div>TI CPI 2024 · Rank 6</div>
|
||
</div>
|
||
</div>
|
||
<!-- Security level -->
|
||
<div>
|
||
<div>
|
||
<div>Security Assessment</div>
|
||
<div>LOW RISK</div>
|
||
</div>
|
||
<div>Sweden maintains one of Europe’s most stable security environments. No active conflict. Terrorism risk: low-medium (NCSD level 3/5). Gang-related violence concentrated in specific outer suburbs.</div>
|
||
<div>Sources: SÄPO 2025 · GPI 2024 (rank 14) · OSAC</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- News feed -->
|
||
<div id="news-block">
|
||
<div>
|
||
Stockholm News
|
||
<span id="news-time">Loading...</span>
|
||
</div>
|
||
<div id="news-list"><div>Fetching SVT Stockholm...</div></div>
|
||
</div>
|
||
|
||
<!-- Urban events -->
|
||
<div id="events-block">
|
||
<div>Urban Events & Notices</div>
|
||
<div id="events-list"></div>
|
||
</div>
|
||
|
||
<div id="right-footer">
|
||
<div>
|
||
<div>Source First Architecture</div>
|
||
<span>DATA</span> Identifiable sources with date + confidence<br>
|
||
<span>OBS</span> quiXzoom verified field observations<br>
|
||
<span>MODEL</span> Transparent index with listed inputs<br>
|
||
<span>FORECAST</span> Statistical patterns, not predictions<br>
|
||
<a href="/methodology/" >Full methodology →</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
<script>
|
||
function sourceChain(items){
|
||
return `<div class="src-chain">${items.map(i=>`<span class="src-tag src-${i.type}">${i.type==='data'?'DATA':i.type==='obs'?'OBS':i.type==='model'?'MODEL':'FORECAST'} · ${i.label}</span>`).join('')}</div>`;
|
||
}
|
||
|
||
/* DATA */
|
||
const D=[
|
||
{id:'city',name:'City / Gamla Stan',lat:59.325,lng:18.071,r:1600,
|
||
confidence:{score:94,observation_count:1247},
|
||
s:{business:9.2,tourist:9.5,safety:8.9,investment:8.1,nightlife:6.2,infrastructure:9.0},
|
||
c:'low',ct:'Observed conditions align with official data. Minor seasonal peaks in petty crime not reflected in annual statistics.',
|
||
ins:{business:'Prime CBD. HQ addresses, banks, law firms. Highest foot traffic density in Sweden.',tourist:'Old Town and Royal Palace. Most visited area. High pickpocket risk in peak season.',safety:'Excellent overall. Petty theft around Stortorget in summer. Night safety: good.',investment:'Mature market. Limited development land. Premium office rents. Stable.',nightlife:'Limited nightlife. Restaurants and bars. Quiet after midnight.',infrastructure:'Excellent metro connectivity (T-Centralen). Pedestrian priority zones.'},
|
||
col:'#16a34a',pulse:{level:0.72,label:'High Activity'},umi:+42},
|
||
{id:'ostermalm',name:'Östermalm',lat:59.337,lng:18.082,r:1500,
|
||
confidence:{score:97,observation_count:1084},
|
||
s:{business:8.4,tourist:7.8,safety:9.4,investment:9.1,nightlife:8.8,infrastructure:8.5},
|
||
c:'low',ct:'Official data and observed reality align closely. Slight commercial activity underestimation on weekends.',
|
||
ins:{business:'Premium addresses. Embassies, private equity, luxury brands. High-net-worth clientele.',tourist:'Market Hall, Strandvägen. Upscale shopping. Less crowded than Gamla Stan.',safety:'Safest district. Low crime across all categories. Excellent lighting.',investment:'Highest residential values in Sweden. Consistent international buyer demand.',nightlife:'High-end bars. Stureplan cluster is the premium nightlife zone.',infrastructure:'Well-maintained. Multiple metro stations.'},
|
||
col:'#16a34a',pulse:{level:0.55,label:'Moderate Activity'},umi:+18},
|
||
{id:'sodermalm',name:'Södermalm',lat:59.313,lng:18.063,r:1800,
|
||
confidence:{score:88,observation_count:1198},
|
||
s:{business:6.8,tourist:8.6,safety:7.2,investment:8.4,nightlife:9.4,infrastructure:7.1},
|
||
c:'medium',ct:'Contradiction detected: official infrastructure ratings overstate actual road conditions in SoFo. Night incidents underreported Q4 2025.',
|
||
ins:{business:'Creative industry hub. Tech startups, media, design. Lower rents than north city.',tourist:'Fotografiska museum, vintage shops, authentic local culture.',safety:'Generally safe. Awareness advised around Medborgarplatsen after midnight.',investment:'Active gentrification. Hornstull and Mariatorget appreciating rapidly.',nightlife:'Best nightlife district. Medborgarplatsen, Folkungagatan. Bars on every block.',infrastructure:'Good metro coverage. Some streets poor condition in SoFo.'},
|
||
col:'#d97706',pulse:{level:0.88,label:'Very High Activity'},umi:+68},
|
||
{id:'kungsholmen',name:'Kungsholmen',lat:59.331,lng:18.022,r:1500,
|
||
confidence:{score:95,observation_count:847},
|
||
s:{business:7.9,tourist:5.8,safety:8.6,investment:8.0,nightlife:6.4,infrastructure:8.8},
|
||
c:'low',ct:'High alignment. Clean, well-maintained environment confirmed by observations.',
|
||
ins:{business:'Government offices, SVT, law firms. Stable business environment.',tourist:'City Hall (Nobel venue). Mostly residential.',safety:'Very safe. Low crime. Family-friendly.',investment:'Strong demand. Close to center. Undervalued vs Östermalm.',nightlife:'Limited but growing around Hantverkargatan.',infrastructure:'Excellent. Well-connected.'},
|
||
col:'#16a34a',pulse:{level:0.40,label:'Low–Moderate'},umi:+34},
|
||
{id:'vasastan',name:'Vasastan',lat:59.345,lng:18.048,r:1400,
|
||
confidence:{score:96,observation_count:923},
|
||
s:{business:7.2,tourist:6.4,safety:8.8,investment:8.7,nightlife:7.6,infrastructure:8.2},
|
||
c:'low',ct:'Consistent with official classification. Steady neighbourhood, no anomalies detected.',
|
||
ins:{business:'Mixed residential/commercial. Good mid-market offices. Growing tech presence.',tourist:'Odenplan, Stadsbiblioteket. Authentic local feel.',safety:'Safe and quiet. Family area.',investment:'Strong appreciation. Young professional demographics.',nightlife:'Good restaurant scene. Bar density increasing around Odenplan.',infrastructure:'Multiple metro stations. Good cycling infrastructure.'},
|
||
col:'#16a34a',pulse:{level:0.50,label:'Moderate Activity'},umi:+52},
|
||
{id:'nacka',name:'Nacka / Värmdö',lat:59.311,lng:18.163,r:1900,
|
||
confidence:{score:91,observation_count:762},
|
||
s:{business:6.2,tourist:7.1,safety:9.1,investment:9.4,nightlife:4.8,infrastructure:7.4},
|
||
c:'low',ct:'Observed infrastructure development activity higher than officially reported. Construction progress aligns with investment thesis.',
|
||
ins:{business:'Growing tech cluster. Lower costs. Good for scale-ups.',tourist:'Archipelago access, kayaking, outdoor tourism.',safety:'Very safe suburban. Low crime.',investment:'Highest growth potential in Stockholm region. Nacka tunnel driving appreciation.',nightlife:'Limited. Residents go into Stockholm.',infrastructure:'Nacka tunnel under construction — transformative connectivity 2028.'},
|
||
col:'#16a34a',pulse:{level:0.30,label:'Low Activity'},umi:+84},
|
||
{id:'rinkeby',name:'Rinkeby–Tensta',lat:59.387,lng:17.928,r:2100,
|
||
confidence:{score:72,observation_count:614},
|
||
s:{business:3.1,tourist:2.2,safety:3.8,investment:5.9,nightlife:3.4,infrastructure:5.2},
|
||
c:'high',ct:'CONTRADICTION INDEX HIGH: Official safety classification significantly underrepresents observed conditions. 41% faulty street lighting, elevated incident density, low evening foot traffic. Official data lags by ~18 months.',
|
||
ins:{business:'Very limited commercial activity. Local services only.',tourist:'Not a tourist area. High operational risk for unfamiliar visitors.',safety:'Elevated operational risk. Night risk significantly above official classification. Local accompaniment recommended.',investment:'Redevelopment opportunity with government backing. High risk, potential long-term upside.',nightlife:'No formal nightlife. Not recommended.',infrastructure:'Metro accessible. Local street infrastructure poor. 41% faulty lighting observed.'},
|
||
col:'#dc2626',pulse:{level:0.18,label:'Sparse Activity'},umi:-22},
|
||
{id:'bangkok-sukhumvit', name:'Sukhumvit', lat:13.742, lng:100.557, r:800,
|
||
s:{business:7.8, tourist:8.2, safety:5.4, investment:7.1, nightlife:9.1, infrastructure:4.2},
|
||
c:'high',
|
||
ct:'FIELD VERIFIED 2026-06-21: Official infrastructure: approved. quiXzoom observation: person density high (86% conf), motorcycle traffic dense (82% conf). storefront/signage coverage incomplete — additional missions needed. Contradiction Score: HIGH.',
|
||
ins:{business:'Dense commercial strip. High F&B, retail, hotels. quiXzoom: active foot traffic confirmed.',tourist:'Backpacker hub transitioning to premium. High pedestrian density observed.',safety:'Elevated night risk. Cable hazards common. Official data lags reality.',investment:'Rapid gentrification. High upside.',nightlife:'Best nightlife density in Bangkok. Verified foot traffic.',infrastructure:'Critical cable disorder. 38% faulty lighting estimated. Official data: approved — CONTRADICTION.'},
|
||
col:'#f97316', umi:+31,
|
||
confidence:{score:74,observation_count:312},pulse:{level:0.82,label:'Very High Activity'},
|
||
observation:{id:'qx-vid-ban-20260621171332-739d82', frames:73, coverage:60, verified:'2026-06-21'}},
|
||
{id:'bkk-silom', name:'Silom', lat:13.724, lng:100.529, r:900,
|
||
s:{business:9.1,tourist:7.2,safety:6.8,investment:8.4,nightlife:8.8,infrastructure:6.1},
|
||
c:'medium', ct:'Commercial activity exceeds official classification. Infrastructure maintenance lags.',
|
||
ins:{business:'Bangkok CBD. Banks, finance, multinationals.',tourist:'Patpong night market, Lumpini Park.',safety:'Generally safe daytime. Night awareness needed.',investment:'Strong appreciation. Regional HQ location.',nightlife:'Patpong area. Diverse options.',infrastructure:'BTS Silom line. Road congestion severe.'},
|
||
col:'#d97706', umi:+28},
|
||
{id:'bkk-sathorn', name:'Sathorn', lat:13.717, lng:100.539, r:800,
|
||
s:{business:9.4,tourist:5.2,safety:7.4,investment:9.1,nightlife:6.8,infrastructure:7.2},
|
||
c:'low', ct:'Premium business district. Strong alignment.',
|
||
ins:{business:'US Embassy, embassies, luxury offices.',tourist:'Limited tourist appeal.',safety:'Well-secured. Embassy area.',investment:'Highest business real estate in Bangkok.',nightlife:'Premium rooftop bars.',infrastructure:'BTS Chong Nonsi. Good connectivity.'},
|
||
col:'#16a34a', umi:+42},
|
||
{id:'bkk-chatuchak', name:'Chatuchak', lat:13.800, lng:100.551, r:1200,
|
||
s:{business:6.2,tourist:8.8,safety:7.1,investment:7.4,nightlife:4.2,infrastructure:7.8},
|
||
c:'medium', ct:'Weekend market drives commercial peaks not reflected in weekday data.',
|
||
ins:{business:'Largest weekend market globally. Commerce extreme on weekends.',tourist:'Chatuchak Weekend Market — must-see.',safety:'Good. Crowded but safe.',investment:'Residential growth. MRT access improving.',nightlife:'Limited.',infrastructure:'MRT Mo Chit. Good transport hub.'},
|
||
col:'#d97706', umi:+18},
|
||
{id:'hk-central', name:'HK Central', lat:22.282, lng:114.158, r:900,
|
||
s:{business:9.7,tourist:8.8,safety:9.1,investment:8.4,nightlife:8.6,infrastructure:9.8},
|
||
c:'low', ct:'World-class financial centre. Full alignment.',
|
||
ins:{business:'HSBC, Standard Chartered HQ. Top Asia finance.',tourist:'Victoria Harbour, Peak Tram.',safety:'Excellent.',investment:'Premium stable. HK dollar peg.',nightlife:'Lan Kwai Fong cluster.',infrastructure:'MTR exceptional. Airport express 24 min.'},
|
||
col:'#16a34a', umi:+6},
|
||
{id:'mum-bkc', name:'BKC Mumbai', lat:19.065, lng:72.865, r:1100,
|
||
s:{business:8.8,tourist:3.2,safety:7.4,investment:8.9,nightlife:5.8,infrastructure:7.2},
|
||
c:'medium', ct:'Rapid development outpacing official infrastructure classification.',
|
||
ins:{business:'Bandra Kurla Complex — India finance hub. NSE, SEBI.',tourist:'Business only.',safety:'Good for business area.',investment:'Fastest appreciating commercial in India.',nightlife:'Growing premium.',infrastructure:'Metro expansion ongoing. Traffic severe.'},
|
||
col:'#d97706', umi:+64},
|
||
{id:'london-city', name:'City of London', lat:51.514, lng:-0.088, r:1200,
|
||
s:{business:9.6, tourist:7.4, safety:8.8, investment:8.2, nightlife:5.1, infrastructure:9.1},
|
||
c:'low', ct:'Strong alignment between official data and observed conditions.',
|
||
ins:{business:'Global financial centre. Premium office market.',tourist:'Historic but less touristy than West End.',safety:'Excellent. Low crime, well-lit, cameras everywhere.',investment:'Mature market, stable yields.',nightlife:'Limited — suits 9-to-5 finance crowd.',infrastructure:'World-class. Elizabeth line transformed connectivity.'},
|
||
col:'#16a34a', umi:+12,
|
||
confidence:{score:96,observation_count:842},pulse:{level:0.61,label:'High Activity'}},
|
||
{id:'dubai-downtown', name:'Dubai Downtown', lat:25.197, lng:55.274, r:1500,
|
||
s:{business:9.1, tourist:9.4, safety:9.3, investment:8.8, nightlife:7.2, infrastructure:9.5},
|
||
c:'low', ct:'Observed environment aligns with official data. High confidence.',
|
||
ins:{business:'Premium business address. DIFC proximity.',tourist:'Burj Khalifa, Dubai Mall. Peak tourism.',safety:'Exceptional. Among safest globally.',investment:'Strong capital appreciation trend.',nightlife:'Growing but culturally constrained.',infrastructure:'World-class. Metro, road network excellent.'},
|
||
col:'#16a34a', umi:+44,
|
||
confidence:{score:92,observation_count:614},pulse:{level:0.74,label:'High Activity'}},
|
||
|
||
/* Stockholm extra districts */
|
||
{id:'djurgarden', name:'Djurgården', lat:59.326, lng:18.110, r:700,
|
||
s:{business:5.2,tourist:9.8,safety:9.1,investment:7.4,nightlife:3.2,infrastructure:8.4},
|
||
c:'low', ct:'Observed environment matches official data. Premium cultural destination.',
|
||
ins:{business:'Limited office space. Museums, hospitality.',tourist:'Vasa Museum, ABBA Museum, Skansen. Stockholm\'s top attraction.',safety:'Extremely safe. Well-lit, patrolled.',investment:'Premium residential, limited supply.',nightlife:'Almost none — day destination.',infrastructure:'Good. Ferry connections, cycling paths.'},
|
||
col:'#16a34a', umi:+8,
|
||
confidence:{score:91,observation_count:320},pulse:{level:0.45,label:'Moderate Activity'}},
|
||
|
||
{id:'hagastaden', name:'Hagastaden', lat:59.352, lng:18.044, r:900,
|
||
s:{business:7.8,tourist:4.1,safety:8.9,investment:9.2,nightlife:4.8,infrastructure:8.1},
|
||
c:'low', ct:'Major development zone. New life science hub. Official data slightly lags construction activity.',
|
||
ins:{business:'New Karolinska, AstraZeneca HQ. Life science cluster.',tourist:'Not a tourist area.',safety:'Very safe. New development.',investment:'Highest development potential in Stockholm. New district being built.',nightlife:'Limited currently — growing.',infrastructure:'New — excellent. Tram lines extended.'},
|
||
col:'#16a34a', umi:+91,
|
||
confidence:{score:88,observation_count:214},pulse:{level:0.38,label:'Low–Moderate'}},
|
||
|
||
{id:'hammarby', name:'Hammarby Sjöstad', lat:59.302, lng:18.087, r:900,
|
||
s:{business:6.1,tourist:6.8,safety:8.4,investment:8.6,nightlife:5.9,infrastructure:8.2},
|
||
c:'low', ct:'Sustainable urban development. Official data and observations align well.',
|
||
ins:{business:'Tech startups, creative offices.',tourist:'Waterfront, architecture tourism.',safety:'Safe. Young demographic.',investment:'Strong appreciation. Sustainable premium.',nightlife:'Growing bar scene.',infrastructure:'Tram, ferry. Excellent waterfront access.'},
|
||
col:'#16a34a', umi:+38,
|
||
confidence:{score:93,observation_count:487},pulse:{level:0.55,label:'Moderate Activity'}},
|
||
|
||
{id:'farsta', name:'Farsta', lat:59.244, lng:18.086, r:1400,
|
||
s:{business:4.8,tourist:2.1,safety:5.9,investment:6.4,nightlife:3.1,infrastructure:6.8},
|
||
c:'medium', ct:'Suburban area with some infrastructure gaps. Official safety data slightly optimistic.',
|
||
ins:{business:'Local services, retail.',tourist:'No tourist appeal.',safety:'Moderate. Some evening risk areas.',investment:'Affordable entry point. Long-term upside.',nightlife:'Very limited.',infrastructure:'Metro but limited other connections.'},
|
||
col:'#d97706', umi:+14,
|
||
confidence:{score:82,observation_count:198},pulse:{level:0.28,label:'Low Activity'}},
|
||
|
||
/* Göteborg districts */
|
||
{id:'gbg-centrum', name:'Göteborg Centrum', lat:57.706, lng:11.967, r:1200,
|
||
s:{business:8.4,tourist:8.9,safety:7.8,investment:8.1,nightlife:8.6,infrastructure:8.8},
|
||
c:'medium', ct:'Some traffic congestion underreported in official data. Commercial activity slightly above classification.',
|
||
ins:{business:'Nordstan, finance district. Strong commercial.',tourist:'Avenyn, Haga. Top Swedish destination.',safety:'Generally safe. Some evening incidents on Avenyn.',investment:'Stable market. Good yields.',nightlife:'Best nightlife outside Stockholm. Avenyn cluster.',infrastructure:'Excellent tram network. Centralstation hub.'},
|
||
col:'#16a34a', umi:+28,
|
||
confidence:{score:89,observation_count:512},pulse:{level:0.68,label:'High Activity'}},
|
||
|
||
{id:'gbg-haga', name:'Haga', lat:57.698, lng:11.949, r:700,
|
||
s:{business:6.8,tourist:9.2,safety:8.6,investment:8.8,nightlife:7.4,infrastructure:7.2},
|
||
c:'low', ct:'High alignment. Premium gentrified neighbourhood.',
|
||
ins:{business:'Boutiques, cafes, creative offices.',tourist:'Most photographed neighbourhood in Gothenburg.',safety:'Very safe. Residential premium.',investment:'Rapid appreciation. Limited supply.',nightlife:'Cosy bars, wine culture.',infrastructure:'Good tram access. Parking limited.'},
|
||
col:'#16a34a', umi:+42,
|
||
confidence:{score:94,observation_count:378},pulse:{level:0.52,label:'Moderate Activity'}},
|
||
|
||
{id:'gbg-lindholmen', name:'Lindholmen', lat:57.707, lng:11.939, r:900,
|
||
s:{business:9.1,tourist:4.2,safety:8.8,investment:9.4,nightlife:4.1,infrastructure:8.6},
|
||
c:'low', ct:'Tech hub exceeds official classification. Volvo, RISE, Chalmers ecosystem.',
|
||
ins:{business:'Volvo HQ, tech cluster. Sweden\'s Silicon Valley.',tourist:'Not a tourist area.',safety:'Excellent. Corporate security culture.',investment:'Highest investment score in Gothenburg.',nightlife:'Limited — tech workers commute.',infrastructure:'Ferry to centrum. New bridge planned.'},
|
||
col:'#16a34a', umi:+64,
|
||
confidence:{score:91,observation_count:294},pulse:{level:0.44,label:'Moderate Activity'}},
|
||
|
||
{id:'gbg-majorna', name:'Majorna', lat:57.693, lng:11.921, r:1000,
|
||
s:{business:5.9,tourist:6.8,safety:8.1,investment:8.2,nightlife:6.8,infrastructure:6.9},
|
||
c:'low', ct:'Gentrification in progress. Consistent with observations.',
|
||
ins:{business:'Local businesses, creative industry.',tourist:'Authentic waterfront, local culture.',safety:'Safe residential.',investment:'Active gentrification. Good entry prices.',nightlife:'Growing bar scene.',infrastructure:'Tram access. Cycling culture strong.'},
|
||
col:'#16a34a', umi:+48,
|
||
confidence:{score:87,observation_count:261},pulse:{level:0.46,label:'Moderate Activity'}},
|
||
|
||
{id:'gbg-angered', name:'Angered', lat:57.764, lng:12.046, r:2000,
|
||
s:{business:3.2,tourist:1.8,safety:4.1,investment:5.8,nightlife:2.9,infrastructure:5.4},
|
||
c:'high', ct:'CONTRADICTION HIGH: Official data underrepresents observed conditions. Infrastructure decay, high vacancy, elevated incident density.',
|
||
ins:{business:'Very limited. Local services only.',tourist:'No tourist appeal.',safety:'Elevated risk. Night incidents common.',investment:'Redevelopment opportunity. Government priority.',nightlife:'Not recommended.',infrastructure:'Metro but poor local street conditions.'},
|
||
col:'#dc2626', umi:-18,
|
||
confidence:{score:74,observation_count:187},pulse:{level:0.21,label:'Sparse Activity'}},
|
||
|
||
{id:'gbg-askim', name:'Askim / Frölunda', lat:57.634, lng:11.949, r:1800,
|
||
s:{business:5.8,tourist:4.2,safety:8.6,investment:7.4,nightlife:3.8,infrastructure:7.2},
|
||
c:'low', ct:'Stable suburban area. Consistent with official data.',
|
||
ins:{business:'Frölunda Torg retail cluster.',tourist:'Beaches, nature.',safety:'Very safe.',investment:'Family demand strong.',nightlife:'Limited.',infrastructure:'Tram connections.'},
|
||
col:'#16a34a', umi:+22,
|
||
confidence:{score:88,observation_count:223},pulse:{level:0.32,label:'Low Activity'}},
|
||
|
||
/* Malmö districts */
|
||
{id:'malmo-centrum', name:'Malmö Centrum', lat:55.604, lng:13.003, r:1100,
|
||
s:{business:7.8,tourist:8.2,safety:7.1,investment:7.8,nightlife:7.9,infrastructure:8.4},
|
||
c:'medium', ct:'Night safety slightly below official classification. Commercial activity aligns well.',
|
||
ins:{business:'Strong retail, growing tech scene.',tourist:'Stortorget, Lilla Torg. Vibrant city.',safety:'Good by day. Some night risk areas.',investment:'Rising market. Copenhagen proximity premium.',nightlife:'Best in Malmö. Lilla Torg cluster.',infrastructure:'Excellent. Pågatåg, Öresund trains.'},
|
||
col:'#16a34a', umi:+34,
|
||
confidence:{score:88,observation_count:441},pulse:{level:0.61,label:'High Activity'}},
|
||
|
||
{id:'malmo-vastra-hamnen', name:'Västra Hamnen', lat:55.612, lng:12.975, r:900,
|
||
s:{business:8.1,tourist:8.8,safety:9.2,investment:9.1,nightlife:6.4,infrastructure:8.9},
|
||
c:'low', ct:'Showcase sustainable district. Strong alignment official/observed.',
|
||
ins:{business:'Premium offices, WeWork, startups.',tourist:'Turning Torso, waterfront.',safety:'Extremely safe. Premium residential.',investment:'Highest Malmö values. Waterfront premium.',nightlife:'Growing.',infrastructure:'Excellent. Ferry to Copenhagen under discussion.'},
|
||
col:'#16a34a', umi:+56,
|
||
confidence:{score:93,observation_count:318},pulse:{level:0.48,label:'Moderate Activity'}},
|
||
|
||
{id:'malmo-mollevangen', name:'Möllevången', lat:55.590, lng:13.010, r:800,
|
||
s:{business:6.4,tourist:7.8,safety:6.8,investment:7.6,nightlife:8.8,infrastructure:7.1},
|
||
c:'medium', ct:'Gentrification active. Night safety incidents slightly higher than official.',
|
||
ins:{business:'Ethnic food market, creative businesses.',tourist:'Most authentic Malmö neighbourhood.',safety:'Generally safe. Late night awareness needed.',investment:'Fast appreciation. Artists moving in.',nightlife:'Best alternative scene in Malmö.',infrastructure:'Good tram, cycling.'},
|
||
col:'#d97706', umi:+62,
|
||
confidence:{score:85,observation_count:276},pulse:{level:0.71,label:'High Activity'}},
|
||
|
||
{id:'malmo-hyllie', name:'Hyllie', lat:55.560, lng:12.987, r:1400,
|
||
s:{business:8.6,tourist:4.2,safety:8.4,investment:9.3,nightlife:3.2,infrastructure:9.4},
|
||
c:'low', ct:'Major development zone. Arena, Emporia. Strong infrastructure investment.',
|
||
ins:{business:'Emporia mall, Malmö Arena. Event economy.',tourist:'Not a tourist area.',safety:'Safe. Commercial zone.',investment:'Highest development activity in Malmö.',nightlife:'Event-driven only.',infrastructure:'Hyllie station — direct to Copenhagen 20 min.'},
|
||
col:'#16a34a', umi:+78,
|
||
confidence:{score:90,observation_count:247},pulse:{level:0.39,label:'Low–Moderate'}},
|
||
|
||
{id:'malmo-rosengard', name:'Rosengård', lat:55.590, lng:13.042, r:1600,
|
||
s:{business:3.4,tourist:2.1,safety:3.9,investment:6.1,nightlife:3.2,infrastructure:5.6},
|
||
c:'high', ct:'CONTRADICTION HIGH: Official classification significantly understates observed risk. Infrastructure decay, high vacancy, elevated incidents.',
|
||
ins:{business:'Very limited. Local services.',tourist:'Not suitable.',safety:'Elevated risk. Night incidents regular.',investment:'Redevelopment potential. High risk.',nightlife:'Not recommended.',infrastructure:'Metro but poor local conditions.'},
|
||
col:'#dc2626', umi:-14,
|
||
confidence:{score:73,observation_count:194},pulse:{level:0.19,label:'Sparse Activity'}},
|
||
|
||
{id:'malmo-limhamn', name:'Limhamn', lat:55.582, lng:12.928, r:1200,
|
||
s:{business:5.4,tourist:6.2,safety:9.1,investment:8.4,nightlife:4.8,infrastructure:7.4},
|
||
c:'low', ct:'Premium family area. Strong alignment.',
|
||
ins:{business:'Local commercial. Limestone heritage.',tourist:'Beaches, ferry to Denmark.',safety:'Among safest in Malmö.',investment:'Strong family demand.',nightlife:'Limited.',infrastructure:'Tram, Öresund bridge access.'},
|
||
col:'#16a34a', umi:+28,
|
||
confidence:{score:91,observation_count:208},pulse:{level:0.34,label:'Low Activity'}},
|
||
|
||
// Singapore — Orchard Road
|
||
{id:'sg-orchard', name:'Orchard Road', lat:1.304, lng:103.832, r:800,
|
||
s:{business:9.2,tourist:9.6,safety:9.8,investment:8.4,nightlife:7.8,infrastructure:9.7},
|
||
c:'low', ct:'World-class retail and safety. Full alignment official/observed.',
|
||
ins:{business:'Luxury retail, premium offices.',tourist:'Top shopping destination Asia.',safety:'Exceptional. Cameras everywhere.',investment:'Premium stable.',nightlife:'Growing.',infrastructure:'MRT excellent.'},
|
||
col:'#16a34a', umi:+22},
|
||
|
||
// Singapore — Jurong East (tech hub)
|
||
{id:'sg-jurong', name:'Jurong East', lat:1.333, lng:103.742, r:1200,
|
||
s:{business:8.8,tourist:3.2,safety:9.4,investment:9.1,nightlife:3.8,infrastructure:9.2},
|
||
c:'low', ct:'Major business hub. JLD development accelerating.',
|
||
ins:{business:'Jurong Lake District — Singapore\'s second CBD.',tourist:'Not a tourist area.',safety:'Very safe.',investment:'Highest growth potential in Singapore.',nightlife:'Limited.',infrastructure:'World-class. New MRT lines.'},
|
||
col:'#16a34a', umi:+74},
|
||
|
||
// Amsterdam — Centrum
|
||
{id:'ams-centrum', name:'Amsterdam Centrum', lat:52.370, lng:4.895, r:1100,
|
||
s:{business:8.1,tourist:9.4,safety:7.2,investment:7.8,nightlife:8.9,infrastructure:8.6},
|
||
c:'medium', ct:'Overtourism creating observable contradiction vs livability metrics.',
|
||
ins:{business:'Finance, creative, tech.',tourist:'Top European destination.',safety:'Good but tourist areas require awareness.',investment:'Premium but constrained.',nightlife:'Excellent. Leidseplein, Rembrandtplein.',infrastructure:'World-class cycling, trams.'},
|
||
col:'#d97706', umi:+14},
|
||
|
||
// Amsterdam — Zuidas
|
||
{id:'ams-zuidas', name:'Zuidas', lat:52.337, lng:4.874, r:900,
|
||
s:{business:9.4,tourist:3.2,safety:9.1,investment:9.2,nightlife:4.8,infrastructure:9.3},
|
||
c:'low', ct:'Financial district. Strong alignment.',
|
||
ins:{business:'ABN AMRO, Deloitte, ING. Dutch financial hub.',tourist:'Not a tourist area.',safety:'Excellent.',investment:'Premium office market.',nightlife:'Limited.',infrastructure:'Amsterdam Zuid station — direct Schiphol 10 min.'},
|
||
col:'#16a34a', umi:+38},
|
||
|
||
// Berlin — Mitte
|
||
{id:'ber-mitte', name:'Berlin Mitte', lat:52.520, lng:13.404, r:1400,
|
||
s:{business:8.6,tourist:9.2,safety:7.8,investment:8.4,nightlife:8.2,infrastructure:8.8},
|
||
c:'low', ct:'Strong alignment. Premium Berlin district.',
|
||
ins:{business:'Government, media, startups.',tourist:'Brandenburg Gate, Museum Island.',safety:'Good. Touristy areas require awareness.',investment:'Appreciation trend strong.',nightlife:'World-famous techno scene nearby.',infrastructure:'U-Bahn, S-Bahn excellent.'},
|
||
col:'#16a34a', umi:+32},
|
||
|
||
// New York — Manhattan Midtown
|
||
{id:'nyc-midtown', name:'Midtown Manhattan', lat:40.754, lng:-73.984, r:1500,
|
||
s:{business:9.8,tourist:9.4,safety:7.4,investment:8.6,nightlife:8.8,infrastructure:8.2},
|
||
c:'medium', ct:'Infrastructure aging vs premium economic activity. Some contradiction in maintenance data.',
|
||
ins:{business:'Global financial and media capital.',tourist:'Times Square, Central Park, Empire State.',safety:'Generally safe. Awareness needed.',investment:'Premium, stable long-term.',nightlife:'24/7 city.',infrastructure:'Subway aging but functional.'},
|
||
col:'#d97706', umi:+18},
|
||
|
||
// Tokyo — Shinjuku
|
||
{id:'tky-shinjuku', name:'Shinjuku', lat:35.689, lng:139.700, r:1200,
|
||
s:{business:9.1,tourist:9.6,safety:9.7,investment:8.2,nightlife:9.8,infrastructure:9.9},
|
||
c:'low', ct:'Near-perfect alignment. Japanese maintenance culture reflected in data.',
|
||
ins:{business:'Huge office market. Top Tokyo business address.',tourist:'Iconic. Kabukicho, Golden Gai.',safety:'Exceptional.',investment:'Stable, mature market.',nightlife:'Best nightlife density globally.',infrastructure:'World-class. Busiest station on earth.'},
|
||
col:'#16a34a', umi:+8},
|
||
|
||
// Rio de Janeiro — Ipanema
|
||
{id:'rio-ipanema', name:'Ipanema', lat:-22.984, lng:-43.198, r:1000,
|
||
s:{business:5.8,tourist:9.2,safety:4.8,investment:7.4,nightlife:8.4,infrastructure:6.2},
|
||
c:'high', ct:'Safety data significantly understates observed risk, particularly after 22:00.',
|
||
ins:{business:'Boutiques, beach economy.',tourist:'World-famous beach.',safety:'Elevated. Official data optimistic.',investment:'High value, high risk.',nightlife:'Excellent but take precautions.',infrastructure:'Moderate. Traffic severe.'},
|
||
col:'#f97316', umi:+12},
|
||
|
||
// Stockholm — fler förorter och kranskommuner
|
||
{id:'sthlm-huddinge', name:'Huddinge', lat:59.237, lng:17.981, r:2000,
|
||
s:{business:6.2,tourist:3.4,safety:8.1,investment:7.8,nightlife:3.2,infrastructure:7.4},
|
||
c:'low', ct:'Stable suburban. Flemingsberg growing as tech/medical hub.',
|
||
ins:{business:'Flemingsberg — KTH, Karolinska, tech cluster emerging.',tourist:'No tourist appeal.',safety:'Safe suburban.',investment:'Strong family demand. Undervalued vs inner city.',nightlife:'Very limited.',infrastructure:'Pendeltåg + tunnelbana. Flemingsberg station expanding.'},
|
||
col:'#16a34a', umi:+34},
|
||
|
||
{id:'sthlm-solna', name:'Solna', lat:59.360, lng:18.001, r:1400,
|
||
s:{business:8.4,tourist:5.8,safety:8.8,investment:8.6,nightlife:5.4,infrastructure:8.9},
|
||
c:'low', ct:'Friends Arena area driving commercial transformation. Alignment strong.',
|
||
ins:{business:'AstraZeneca global HQ, Karolinska. Premium business address.',tourist:'Friends Arena, Råsunda.',safety:'Excellent.',investment:'Fastest appreciating in Stockholm region.',nightlife:'Limited but growing.',infrastructure:'Solna station — new. Excellent.'},
|
||
col:'#16a34a', umi:+72},
|
||
|
||
{id:'sthlm-sundbyberg', name:'Sundbyberg', lat:59.362, lng:17.971, r:1100,
|
||
s:{business:7.2,tourist:3.8,safety:8.4,investment:8.8,nightlife:4.8,infrastructure:8.6},
|
||
c:'low', ct:'Most densely populated city in Sweden. Active development.',
|
||
ins:{business:'Growing tech/media cluster.',tourist:'No tourist appeal.',safety:'Good.',investment:'Highest density growth. Strong upside.',nightlife:'Limited.',infrastructure:'Excellent — multiple train lines.'},
|
||
col:'#16a34a', umi:+58},
|
||
|
||
{id:'sthlm-tyreso', name:'Tyresö', lat:59.244, lng:18.229, r:1800,
|
||
s:{business:5.4,tourist:4.2,safety:9.1,investment:7.2,nightlife:2.8,infrastructure:6.8},
|
||
c:'low', ct:'Safe suburban. Coastal access. Limited commercial.',
|
||
ins:{business:'Local services. LandveX AB registered here.',tourist:'Coast, nature.',safety:'Very safe.',investment:'Family market. Stable.',nightlife:'Almost none.',infrastructure:'Bus-dependent. Limited rail.'},
|
||
col:'#16a34a', umi:+18},
|
||
|
||
{id:'sthlm-jarfalla', name:'Järfälla', lat:59.427, lng:17.831, r:2200,
|
||
s:{business:5.8,tourist:2.8,safety:8.2,investment:7.4,nightlife:2.4,infrastructure:7.1},
|
||
c:'low', ct:'Large suburban. Barkarby commerce growing rapidly.',
|
||
ins:{business:'Barkarby — major retail/logistics cluster.',tourist:'No tourist appeal.',safety:'Safe.',investment:'Barkarby development driving growth.',nightlife:'Very limited.',infrastructure:'New tunnelbana to Barkarby 2026.'},
|
||
col:'#16a34a', umi:+44},
|
||
|
||
{id:'sthlm-lidingo', name:'Lidingö', lat:59.367, lng:18.154, r:1600,
|
||
s:{business:5.2,tourist:6.4,safety:9.4,investment:8.2,nightlife:3.4,infrastructure:6.8},
|
||
c:'low', ct:'Premium island suburb. High income, low density.',
|
||
ins:{business:'Local services. Some premium offices.',tourist:'Millesgården, archipelago.',safety:'Top safety in Stockholm region.',investment:'Premium residential. Strong demand.',nightlife:'Very limited.',infrastructure:'Spårvägen, bus. No tunnelbana.'},
|
||
col:'#16a34a', umi:+12},
|
||
|
||
// Göteborg — fler stadsdelar
|
||
{id:'gbg-kungsbacka', name:'Kungsbacka', lat:57.488, lng:12.076, r:2000,
|
||
s:{business:5.8,tourist:4.2,safety:9.2,investment:7.8,nightlife:2.8,infrastructure:6.8},
|
||
c:'low', ct:'Growing commuter town. Alignment strong.',
|
||
ins:{business:'Local services. Commuter-driven.',tourist:'Coast access.',safety:'Excellent.',investment:'Strong demand. Gothenburg spillover.',nightlife:'Very limited.',infrastructure:'Pendeltåg to Göteborg 25 min.'},
|
||
col:'#16a34a', umi:+28},
|
||
|
||
{id:'gbg-molndal', name:'Mölndal', lat:57.657, lng:12.013, r:1400,
|
||
s:{business:7.8,tourist:3.2,safety:8.6,investment:8.1,nightlife:3.8,infrastructure:7.8},
|
||
c:'low', ct:'Industrial heritage transitioning to tech. Alignment strong.',
|
||
ins:{business:'Siemens, NCC, strong industrial base.',tourist:'No appeal.',safety:'Good.',investment:'Industrial real estate strong.',nightlife:'Limited.',infrastructure:'Tram connection to Göteborg.'},
|
||
col:'#16a34a', umi:+32},
|
||
|
||
// Malmö — fler kommuner
|
||
{id:'malmo-lund', name:'Lund', lat:55.705, lng:13.193, r:1800,
|
||
s:{business:8.2,tourist:7.4,safety:9.1,investment:8.4,nightlife:7.8,infrastructure:8.6},
|
||
c:'low', ct:'University city. Research hub. Strong alignment.',
|
||
ins:{business:'Lund University spin-offs. MaxIV, ESS research facilities.',tourist:'Cathedral, medieval city.',safety:'Very safe.',investment:'Strong student/research demand.',nightlife:'Vibrant student scene.',infrastructure:'Pågatåg, excellent. ESS draws international.'},
|
||
col:'#16a34a', umi:+38},
|
||
|
||
{id:'malmo-helsingborg', name:'Helsingborg', lat:56.046, lng:12.694, r:2000,
|
||
s:{business:7.4,tourist:6.8,safety:8.2,investment:7.8,nightlife:6.4,infrastructure:8.1},
|
||
c:'low', ct:'Ferry hub. Commercial growth. Strong alignment.',
|
||
ins:{business:'Port commerce, Pfizer, regional HQ.',tourist:'Ferry to Denmark, Kärnan.',safety:'Good.',investment:'Appreciation trend positive.',nightlife:'Growing.',infrastructure:'Tåg, ferry. Direct to Copenhagen.'},
|
||
col:'#16a34a', umi:+24},
|
||
|
||
/* Stockholm kranskommuner (batch 2) */
|
||
{id:'sthlm-botkyrka', name:'Botkyrka', lat:59.201, lng:17.834, r:2400,
|
||
s:{business:5.1,tourist:3.2,safety:5.8,investment:6.8,nightlife:3.1,infrastructure:6.9},
|
||
c:'high', ct:'Contradiction detected: official data underreports social challenges in Alby/Norsborg. Infrastructure maintenance gaps observed.',
|
||
ins:{business:'Limited commercial. Fittja, Tumba growing.',tourist:'No appeal.',safety:'Elevated risk in some areas.',investment:'Low entry price. Long-term upside.',nightlife:'Very limited.',infrastructure:'Pendeltåg Tumba. Metro Alby.'},
|
||
col:'#dc2626', umi:-8},
|
||
|
||
{id:'sthlm-nynashamn', name:'Nynäshamn', lat:58.904, lng:17.948, r:2000,
|
||
s:{business:4.8,tourist:6.2,safety:9.1,investment:6.8,nightlife:2.4,infrastructure:6.1},
|
||
c:'low', ct:'Ferry hub to Gotland. Stable coastal community.',
|
||
ins:{business:'Port, ferry terminal, local services.',tourist:'Gotland ferry gateway.',safety:'Very safe.',investment:'Stable coastal demand.',nightlife:'Almost none.',infrastructure:'Pendeltåg terminus.'},
|
||
col:'#16a34a', umi:+12},
|
||
|
||
{id:'sthlm-vallentuna', name:'Vallentuna', lat:59.535, lng:18.080, r:2000,
|
||
s:{business:5.4,tourist:4.1,safety:9.2,investment:7.4,nightlife:2.2,infrastructure:6.8},
|
||
c:'low', ct:'Growing family suburb. Roslagsbanan improving connectivity.',
|
||
ins:{business:'Local services. Growing.',tourist:'Viking rune stones.',safety:'Excellent.',investment:'Strong family demand.',nightlife:'None.',infrastructure:'Roslagsbanan.'},
|
||
col:'#16a34a', umi:+32},
|
||
|
||
{id:'sthlm-osteraker', name:'Österåker', lat:59.481, lng:18.296, r:2200,
|
||
s:{business:5.2,tourist:5.8,safety:9.3,investment:7.6,nightlife:2.4,infrastructure:6.4},
|
||
c:'low', ct:'Archipelago gateway. Premium lakeside.',
|
||
ins:{business:'Local services, marina.',tourist:'Archipelago, lakes.',safety:'Top safety.',investment:'Premium lake/sea access.',nightlife:'Very limited.',infrastructure:'Bus, boat.'},
|
||
col:'#16a34a', umi:+24},
|
||
|
||
{id:'sthlm-vaxholm', name:'Vaxholm', lat:59.402, lng:18.330, r:1200,
|
||
s:{business:5.1,tourist:8.4,safety:9.6,investment:8.1,nightlife:3.2,infrastructure:5.8},
|
||
c:'low', ct:'Premium archipelago town. Tourist peak summer.',
|
||
ins:{business:'Tourism, local services.',tourist:'Most visited archipelago town.',safety:'Exceptional.',investment:'Premium strong demand.',nightlife:'Summer only.',infrastructure:'Ferry from Stockholm.'},
|
||
col:'#16a34a', umi:+18},
|
||
|
||
{id:'sthlm-danderyd', name:'Danderyd', lat:59.400, lng:18.033, r:1400,
|
||
s:{business:6.1,tourist:4.2,safety:9.6,investment:8.8,nightlife:2.8,infrastructure:7.4},
|
||
c:'low', ct:'Highest income municipality in Sweden. Premium residential.',
|
||
ins:{business:'Premium offices, medical.',tourist:'Limited.',safety:'Top Sweden.',investment:'Highest property values outside Östermalm.',nightlife:'Almost none.',infrastructure:'Tunnelbana Mörby.'},
|
||
col:'#16a34a', umi:+14},
|
||
|
||
{id:'sthlm-taby', name:'Täby', lat:59.444, lng:18.069, r:1800,
|
||
s:{business:6.8,tourist:3.8,safety:9.4,investment:8.4,nightlife:3.4,infrastructure:7.6},
|
||
c:'low', ct:'Premium suburb. Arninge growing commercial.',
|
||
ins:{business:'Arninge retail, offices.',tourist:'No appeal.',safety:'Excellent.',investment:'Very strong demand.',nightlife:'Limited.',infrastructure:'Roslagsbanan, pendeltåg.'},
|
||
col:'#16a34a', umi:+28},
|
||
|
||
{id:'sthlm-upplands-vasby', name:'Upplands Väsby', lat:59.519, lng:17.909, r:1800,
|
||
s:{business:6.2,tourist:2.8,safety:8.1,investment:7.2,nightlife:2.8,infrastructure:7.8},
|
||
c:'low', ct:'Airport corridor growth. Arlanda proximity.',
|
||
ins:{business:'Arlanda corridor. Logistics, hospitality.',tourist:'Airport area.',safety:'Good.',investment:'Arlanda premium growing.',nightlife:'Limited.',infrastructure:'Arlanda Express corridor.'},
|
||
col:'#16a34a', umi:+38},
|
||
|
||
{id:'sthlm-sigtuna', name:'Sigtuna', lat:59.617, lng:17.724, r:1800,
|
||
s:{business:5.4,tourist:8.2,safety:9.2,investment:7.1,nightlife:3.8,infrastructure:6.8},
|
||
c:'low', ct:'Oldest city in Sweden. Tourism strong.',
|
||
ins:{business:'Airport hotels, local services.',tourist:'Medieval city, Arlanda nearby.',safety:'Very safe.',investment:'Stable, tourism-driven.',nightlife:'Limited.',infrastructure:'Bus, Airport Express nearby.'},
|
||
col:'#16a34a', umi:+16},
|
||
|
||
{id:'sthlm-upplands-bro', name:'Upplands-Bro', lat:59.519, lng:17.634, r:2000,
|
||
s:{business:5.6,tourist:2.8,safety:8.4,investment:7.4,nightlife:2.2,infrastructure:7.1},
|
||
c:'low', ct:'Growing western suburb. Bro station hub.',
|
||
ins:{business:'Logistics, local.',tourist:'No appeal.',safety:'Good.',investment:'Growth market.',nightlife:'None.',infrastructure:'Pendeltåg Bro.'},
|
||
col:'#16a34a', umi:+34},
|
||
|
||
{id:'sthlm-ektorp', name:'Nacka stad', lat:59.298, lng:18.198, r:1400,
|
||
s:{business:6.4,tourist:5.8,safety:9.0,investment:8.8,nightlife:4.2,infrastructure:7.6},
|
||
c:'low', ct:'Nacka town centre. Rapid development post-tunnel announcement.',
|
||
ins:{business:'Forum Nacka, growing offices.',tourist:'Waterfront.',safety:'Very safe.',investment:'Highest growth Stockholm region.',nightlife:'Growing.',infrastructure:'Pending new metro.'},
|
||
col:'#16a34a', umi:+88},
|
||
|
||
{id:'sthlm-haninge', name:'Haninge', lat:59.170, lng:18.140, r:2400,
|
||
s:{business:5.8,tourist:4.8,safety:7.8,investment:7.2,nightlife:3.4,infrastructure:7.2},
|
||
c:'medium', ct:'Large southern municipality. Handen growing.',
|
||
ins:{business:'Handen centrum, local commerce.',tourist:'Coast access.',safety:'Generally good.',investment:'Affordable entry.',nightlife:'Limited.',infrastructure:'Pendeltåg Handen.'},
|
||
col:'#d97706', umi:+22},
|
||
|
||
{id:'sthlm-sodertaje', name:'Södertälje', lat:59.196, lng:17.625, r:2200,
|
||
s:{business:7.4,tourist:4.8,safety:6.8,investment:6.8,nightlife:4.2,infrastructure:7.4},
|
||
c:'medium', ct:'Strong industrial base. Safety contradiction in some districts.',
|
||
ins:{business:'Scania, AstraZeneca, Anicura. Industrial powerhouse.',tourist:'Södertälje canal.',safety:'Mixed. Some elevated areas.',investment:'Industrial RE strong.',nightlife:'Limited.',infrastructure:'Pendeltåg, E4.'},
|
||
col:'#d97706', umi:+18},
|
||
|
||
{id:'sthlm-norrtalje', name:'Norrtälje', lat:59.758, lng:18.706, r:2400,
|
||
s:{business:5.2,tourist:7.8,safety:9.4,investment:7.2,nightlife:4.2,infrastructure:5.8},
|
||
c:'low', ct:'Archipelago capital. Summer tourism peak.',
|
||
ins:{business:'Tourism, local services.',tourist:'Northern archipelago gateway.',safety:'Excellent.',investment:'Summer cottage premium.',nightlife:'Summer only.',infrastructure:'Bus, boat.'},
|
||
col:'#16a34a', umi:+14},
|
||
|
||
{id:'sthlm-sollentuna', name:'Sollentuna', lat:59.428, lng:17.951, r:1600,
|
||
s:{business:7.1,tourist:3.4,safety:8.9,investment:8.2,nightlife:3.8,infrastructure:8.4},
|
||
c:'low', ct:'Affluent northern suburb. Häggvik growing.',
|
||
ins:{business:'Häggvik commerce, offices.',tourist:'No appeal.',safety:'Excellent.',investment:'Strong premium suburb.',nightlife:'Limited.',infrastructure:'Tunnelbana Häggvik, E4.'},
|
||
col:'#16a34a', umi:+36},
|
||
|
||
{id:'sthlm-ekero', name:'Ekerö', lat:59.297, lng:17.813, r:1800,
|
||
s:{business:4.8,tourist:8.4,safety:9.5,investment:7.8,nightlife:2.4,infrastructure:5.6},
|
||
c:'low', ct:'Island municipality. Birka UNESCO. Premium lake.',
|
||
ins:{business:'Tourism, local.',tourist:'Birka Viking UNESCO, Drottningholm.',safety:'Top safety.',investment:'Premium island living.',nightlife:'None.',infrastructure:'Bridge, ferry.'},
|
||
col:'#16a34a', umi:+16},
|
||
|
||
// GÖTEBORGSREGIONEN
|
||
{id:'gbg-partille', name:'Partille', lat:57.738, lng:12.106, r:1600,
|
||
s:{business:6.8,tourist:3.4,safety:8.8,investment:7.8,nightlife:3.2,infrastructure:7.6},
|
||
c:'low', ct:'Affluent eastern suburb. Allum retail strong.',
|
||
ins:{business:'Allum shopping, growing offices.',tourist:'No appeal.',safety:'Excellent.',investment:'Strong family demand.',nightlife:'Limited.',infrastructure:'Tram to Göteborg.'},
|
||
col:'#16a34a', umi:+28},
|
||
|
||
{id:'gbg-harryda', name:'Härryda', lat:57.663, lng:12.284, r:1800,
|
||
s:{business:7.4,tourist:4.2,safety:9.1,investment:7.6,nightlife:2.8,infrastructure:8.4},
|
||
c:'low', ct:'Landvetter Airport municipality. Aviation economy.',
|
||
ins:{business:'Landvetter Airport logistics, aviation.',tourist:'Airport gateway.',safety:'Very safe.',investment:'Airport corridor premium.',nightlife:'Very limited.',infrastructure:'E20, Arlandabuss. Direct Landvetter.'},
|
||
col:'#16a34a', umi:+42},
|
||
|
||
{id:'gbg-kungalv', name:'Kungälv', lat:57.871, lng:11.981, r:1800,
|
||
s:{business:5.8,tourist:7.2,safety:9.2,investment:7.4,nightlife:3.8,infrastructure:6.8},
|
||
c:'low', ct:'Historic fortress town. Growing commuter.',
|
||
ins:{business:'Local services. Growing.',tourist:'Bohus Fortress, river.',safety:'Excellent.',investment:'Commuter demand rising.',nightlife:'Limited.',infrastructure:'Bus to Göteborg.'},
|
||
col:'#16a34a', umi:+24},
|
||
|
||
{id:'gbg-ale', name:'Ale', lat:57.963, lng:12.061, r:2000,
|
||
s:{business:5.2,tourist:3.8,safety:8.8,investment:7.1,nightlife:2.4,infrastructure:6.6},
|
||
c:'low', ct:'Northern Göteborg commuter. Affordable.',
|
||
ins:{business:'Local.',tourist:'River, nature.',safety:'Good.',investment:'Affordable entry.',nightlife:'None.',infrastructure:'Pendeltåg.'},
|
||
col:'#16a34a', umi:+18},
|
||
|
||
{id:'gbg-lerum', name:'Lerum', lat:57.770, lng:12.268, r:1800,
|
||
s:{business:5.6,tourist:3.8,safety:9.1,investment:7.6,nightlife:2.4,infrastructure:7.4},
|
||
c:'low', ct:'Premium eastern commuter. Strong family demand.',
|
||
ins:{business:'Local services.',tourist:'Lakes, nature.',safety:'Excellent.',investment:'Premium family suburb.',nightlife:'None.',infrastructure:'Pendeltåg.'},
|
||
col:'#16a34a', umi:+32},
|
||
|
||
{id:'gbg-stenungsund', name:'Stenungsund', lat:58.076, lng:11.818, r:1800,
|
||
s:{business:6.8,tourist:4.2,safety:8.8,investment:6.8,nightlife:2.8,infrastructure:6.8},
|
||
c:'low', ct:'Petrochemical industry hub. INOVYN, Perstorp.',
|
||
ins:{business:'Chemical industry, Stenungsund industrial park.',tourist:'West coast.',safety:'Good.',investment:'Industrial RE stable.',nightlife:'Very limited.',infrastructure:'E6, bus.'},
|
||
col:'#16a34a', umi:+14},
|
||
|
||
{id:'gbg-tjorn', name:'Tjörn', lat:57.994, lng:11.680, r:2000,
|
||
s:{business:4.8,tourist:8.8,safety:9.6,investment:7.8,nightlife:3.2,infrastructure:5.6},
|
||
c:'low', ct:'Island municipality. Nordic Watercolor Museum. Premium.',
|
||
ins:{business:'Tourism, fishing.',tourist:'Nordic Watercolor Museum. Top west coast.',safety:'Exceptional.',investment:'Premium island living.',nightlife:'Summer only.',infrastructure:'Bridge.'},
|
||
col:'#16a34a', umi:+16},
|
||
|
||
{id:'gbg-orust', name:'Orust', lat:58.080, lng:11.660, r:2400,
|
||
s:{business:4.6,tourist:8.2,safety:9.5,investment:7.4,nightlife:2.8,infrastructure:5.2},
|
||
c:'low', ct:'Largest island on west coast. Boatbuilding heritage.',
|
||
ins:{business:'Tourism, boatbuilding.',tourist:'West coast archipelago.',safety:'Top.',investment:'Premium summer.',nightlife:'Summer only.',infrastructure:'Bridge, ferry.'},
|
||
col:'#16a34a', umi:+12},
|
||
|
||
// MALMÖ / SKÅNE
|
||
{id:'skane-kristianstad', name:'Kristianstad', lat:56.031, lng:14.154, r:2400,
|
||
s:{business:6.8,tourist:6.4,safety:8.6,investment:7.1,nightlife:5.2,infrastructure:7.4},
|
||
c:'low', ct:'Regional capital eastern Skåne. Stable.',
|
||
ins:{business:'Regional admin, food industry.',tourist:'Barony, architecture.',safety:'Good.',investment:'Affordable regional.',nightlife:'University-driven.',infrastructure:'Tåg, E22.'},
|
||
col:'#16a34a', umi:+18},
|
||
|
||
{id:'skane-trelleborg', name:'Trelleborg', lat:55.375, lng:13.157, r:1800,
|
||
s:{business:6.4,tourist:5.8,safety:8.4,investment:6.8,nightlife:3.8,infrastructure:7.2},
|
||
c:'low', ct:'Ferry port to Germany. Industrial.',
|
||
ins:{business:'Port, Trelleborg AB (tires), industry.',tourist:'Ferry terminal, beaches.',safety:'Good.',investment:'Stable port economy.',nightlife:'Limited.',infrastructure:'Ferry Rostock/Travemünde.'},
|
||
col:'#16a34a', umi:+14},
|
||
|
||
{id:'skane-ystad', name:'Ystad', lat:55.430, lng:13.820, r:1600,
|
||
s:{business:5.8,tourist:8.4,safety:9.2,investment:7.2,nightlife:5.4,infrastructure:6.8},
|
||
c:'low', ct:'Wallander town. Ferry to Poland/Bornholm. Tourism strong.',
|
||
ins:{business:'Tourism, local commerce.',tourist:'Wallander, medieval, ferry.',safety:'Very safe.',investment:'Tourism premium.',nightlife:'Decent for size.',infrastructure:'Ferry Świnoujście/Bornholm.'},
|
||
col:'#16a34a', umi:+22},
|
||
|
||
{id:'skane-landskrona', name:'Landskrona', lat:55.871, lng:12.830, r:1600,
|
||
s:{business:5.6,tourist:5.4,safety:6.8,investment:6.8,nightlife:4.2,infrastructure:7.1},
|
||
c:'medium', ct:'Moderate contradiction. Some areas higher risk than official.',
|
||
ins:{business:'Industry, port.',tourist:'Citadell, Ven ferry.',safety:'Mixed. Some elevated areas.',investment:'Affordable, upside.',nightlife:'Limited.',infrastructure:'Tåg, ferry.'},
|
||
col:'#d97706', umi:+16},
|
||
|
||
{id:'skane-kavlinge', name:'Kävlinge', lat:55.795, lng:13.113, r:1400,
|
||
s:{business:5.8,tourist:3.4,safety:9.2,investment:7.8,nightlife:2.8,infrastructure:7.4},
|
||
c:'low', ct:'Premium Malmö commuter. Furulund growing.',
|
||
ins:{business:'Local, commuter services.',tourist:'No appeal.',safety:'Excellent.',investment:'Strong Malmö commuter.',nightlife:'None.',infrastructure:'Pågatåg.'},
|
||
col:'#16a34a', umi:+28},
|
||
|
||
{id:'skane-staffanstorp', name:'Staffanstorp', lat:55.641, lng:13.207, r:1400,
|
||
s:{business:5.4,tourist:3.2,safety:9.4,investment:7.6,nightlife:2.4,infrastructure:7.1},
|
||
c:'low', ct:'Affluent Malmö suburb. High income.',
|
||
ins:{business:'Local, premium.',tourist:'No appeal.',safety:'Top.',investment:'Premium commuter.',nightlife:'None.',infrastructure:'Pågatåg.'},
|
||
col:'#16a34a', umi:+24},
|
||
|
||
{id:'skane-bstad', name:'Båstad', lat:56.428, lng:12.851, r:1600,
|
||
s:{business:5.2,tourist:9.1,safety:9.4,investment:8.2,nightlife:6.8,infrastructure:5.8},
|
||
c:'low', ct:'Swedish Riviera. Tennis capital. Premium summer.',
|
||
ins:{business:'Tourism, tennis events.',tourist:'Tennis, beaches, premium.',safety:'Excellent.',investment:'Premium summer/year-round.',nightlife:'Summer peak excellent.',infrastructure:'Tåg, E6.'},
|
||
col:'#16a34a', umi:+18},
|
||
|
||
{id:'skane-vellinge', name:'Vellinge', lat:55.472, lng:13.019, r:1400,
|
||
s:{business:5.1,tourist:5.8,safety:9.7,investment:8.4,nightlife:2.8,infrastructure:6.8},
|
||
c:'low', ct:'Highest safety index Skåne. Premium coastal.',
|
||
ins:{business:'Local services.',tourist:'Falsterbo, beaches.',safety:'Highest in Skåne.',investment:'Premium coastal demand.',nightlife:'Very limited.',infrastructure:'Bus.'},
|
||
col:'#16a34a', umi:+16},
|
||
|
||
// UPPSALA
|
||
{id:'uppsala-centrum', name:'Uppsala', lat:59.858, lng:17.645, r:2200,
|
||
s:{business:8.1,tourist:7.8,safety:8.6,investment:8.2,nightlife:7.4,infrastructure:8.4},
|
||
c:'low', ct:'University city. Strong alignment official/observed.',
|
||
ins:{business:'Uppsala University, AstraZeneca R&D, SLU.',tourist:'Cathedral, castle. Strong tourism.',safety:'Very safe.',investment:'Strong student/research demand. Appreciation trend.',nightlife:'Vibrant student scene. Flogsta.',infrastructure:'Pendeltåg Stockholm 40 min. Resecentrum.'},
|
||
col:'#16a34a', umi:+44},
|
||
|
||
{id:'uppsala-knivsta', name:'Knivsta', lat:59.724, lng:17.797, r:1400,
|
||
s:{business:5.4,tourist:2.8,safety:9.1,investment:8.1,nightlife:2.2,infrastructure:7.6},
|
||
c:'low', ct:'Fastest growing municipality Sweden. Stockholm commuter.',
|
||
ins:{business:'Local services, growing.',tourist:'No appeal.',safety:'Excellent.',investment:'Top growth rate Sweden.',nightlife:'None.',infrastructure:'Pendeltåg between Uppsala/Stockholm.'},
|
||
col:'#16a34a', umi:+82},
|
||
|
||
// LINKÖPING
|
||
{id:'linkoping-centrum', name:'Linköping', lat:58.411, lng:15.621, r:2200,
|
||
s:{business:8.4,tourist:7.1,safety:8.8,investment:7.8,nightlife:6.8,infrastructure:8.2},
|
||
c:'low', ct:'Tech/defence capital. Saab. Strong alignment.',
|
||
ins:{business:'Saab, Ericsson, LiU. Tech powerhouse.',tourist:'Old Linköping, Kolmården nearby.',safety:'Very safe.',investment:'Strong tech-driven demand.',nightlife:'Good student scene.',infrastructure:'E4, tåg. Good.'},
|
||
col:'#16a34a', umi:+38},
|
||
|
||
// ÖREBRO
|
||
{id:'orebro-centrum', name:'Örebro', lat:59.275, lng:15.213, r:2200,
|
||
s:{business:7.2,tourist:6.8,safety:7.8,investment:7.4,nightlife:6.4,infrastructure:7.8},
|
||
c:'medium', ct:'Some inner-city areas higher risk than official classification.',
|
||
ins:{business:'Regional capital. Logistics hub, Gevalia.',tourist:'Örebro Castle.',safety:'Generally good. Some areas watch.',investment:'Growing regional.',nightlife:'Good for size.',infrastructure:'Central Sweden junction. E18/E20.'},
|
||
col:'#d97706', umi:+28},
|
||
|
||
// VÄSTERÅS
|
||
{id:'vasteras-centrum', name:'Västerås', lat:59.609, lng:16.545, r:2200,
|
||
s:{business:7.8,tourist:5.8,safety:7.6,investment:7.2,nightlife:5.8,infrastructure:7.8},
|
||
c:'medium', ct:'ABB HQ town. Some social challenges not fully reflected.',
|
||
ins:{business:'ABB global HQ. Heavy industry. Growing tech.',tourist:'Castle, Mälaren.',safety:'Good overall. Some areas.',investment:'Industrial premium.',nightlife:'Growing.',infrastructure:'Lake port, tåg Stockholm 60 min.'},
|
||
col:'#d97706', umi:+24},
|
||
|
||
// NORRKÖPING
|
||
{id:'norrkoping-centrum', name:'Norrköping', lat:58.594, lng:16.188, r:2000,
|
||
s:{business:7.1,tourist:7.4,safety:7.2,investment:7.4,nightlife:6.2,infrastructure:7.6},
|
||
c:'medium', ct:'Industrial heritage city. Some contradiction in safety data.',
|
||
ins:{business:'Industristaden transformation. Tech startups.',tourist:'Motala Ström, industrial museums.',safety:'Some areas elevated.',investment:'Gentrification active.',nightlife:'Good student scene.',infrastructure:'E4, tåg. Direct Stockholm/Malmö.'},
|
||
col:'#d97706', umi:+36},
|
||
|
||
// JÖNKÖPING
|
||
{id:'jonkoping-centrum', name:'Jönköping', lat:57.782, lng:14.165, r:2200,
|
||
s:{business:8.2,tourist:6.8,safety:8.4,investment:7.8,nightlife:6.2,infrastructure:7.8},
|
||
c:'low', ct:'Match capital. Strong evangelical heritage. Business hub.',
|
||
ins:{business:'Husqvarna, IKEA (nearby), match capital history.',tourist:'Tändsticsmuseet, Vättern.',safety:'Good.',investment:'Strong business demand.',nightlife:'Decent.',infrastructure:'E4, Göteborg/Stockholm junction.'},
|
||
col:'#16a34a', umi:+32},
|
||
|
||
// UMEÅ
|
||
{id:'umea-centrum', name:'Umeå', lat:63.826, lng:20.263, r:2200,
|
||
s:{business:7.4,tourist:6.8,safety:9.2,investment:7.6,nightlife:7.4,infrastructure:7.6},
|
||
c:'low', ct:'Fastest growing northern city. Strong alignment.',
|
||
ins:{business:'Umeå University, ITS. Tech cluster.',tourist:'Bildmuseet, cultural capital.',safety:'Excellent.',investment:'Consistent growth. Young demographic.',nightlife:'Very good for city size.',infrastructure:'E4, tåg, airport.'},
|
||
col:'#16a34a', umi:+54},
|
||
|
||
// LULEÅ
|
||
{id:'lulea-centrum', name:'Luleå', lat:65.584, lng:22.157, r:2000,
|
||
s:{business:7.8,tourist:5.8,safety:9.4,investment:8.4,nightlife:5.8,infrastructure:7.4},
|
||
c:'low', ct:'Data center capital of Europe. SSAB, LKAB. Strategic.',
|
||
ins:{business:'H2 Green Steel, SSAB, Facebook/Meta data center.',tourist:'Gammelstad UNESCO.',safety:'Excellent.',investment:'Green industry transformation — massive.',nightlife:'OK for size.',infrastructure:'E4, tåg, airport.'},
|
||
col:'#16a34a', umi:+88},
|
||
|
||
// KIRUNA
|
||
{id:'kiruna-centrum', name:'Kiruna', lat:67.856, lng:20.225, r:2400,
|
||
s:{business:7.2,tourist:8.4,safety:9.6,investment:8.8,nightlife:4.8,infrastructure:6.8},
|
||
c:'high', ct:'City moving 3km east due to LKAB mining. Massive contradiction: official maps show old location, physical reality is new construction.',
|
||
ins:{business:'LKAB iron ore. Space industry (ESRANGE).',tourist:'Northern lights, Icehotel, LKAB tours.',safety:'Very safe.',investment:'Mining premium. City move = unique opportunity.',nightlife:'Limited.',infrastructure:'Airport, tåg. Remote.'},
|
||
col:'#f97316', umi:+62},
|
||
|
||
// SUNDSVALL
|
||
{id:'sundsvall-centrum', name:'Sundsvall', lat:62.391, lng:17.307, r:2000,
|
||
s:{business:6.8,tourist:5.8,safety:8.6,investment:6.8,nightlife:5.4,infrastructure:7.2},
|
||
c:'low', ct:'Stone city. Timrå data centers. Regional hub.',
|
||
ins:{business:'Timrå data centers, paper industry.',tourist:'Stone city centre.',safety:'Good.',investment:'Stable regional.',nightlife:'Good for size.',infrastructure:'E4, tåg, airport.'},
|
||
col:'#16a34a', umi:+22},
|
||
|
||
// GÄVLE
|
||
{id:'gavle-centrum', name:'Gävle', lat:60.675, lng:17.142, r:2000,
|
||
s:{business:6.8,tourist:6.2,safety:7.8,investment:6.8,nightlife:5.8,infrastructure:7.4},
|
||
c:'medium', ct:'Goat city. Port town. Some areas higher risk.',
|
||
ins:{business:'Port, paper, Gevalia (coffee).',tourist:'Julbocken, old town.',safety:'Generally good.',investment:'Stable port economy.',nightlife:'Decent.',infrastructure:'E4, tåg Stockholm 90 min.'},
|
||
col:'#d97706', umi:+18},
|
||
|
||
// KARLSTAD
|
||
{id:'karlstad-centrum', name:'Karlstad', lat:59.379, lng:13.503, r:2000,
|
||
s:{business:7.1,tourist:7.2,safety:8.8,investment:7.2,nightlife:6.8,infrastructure:7.4},
|
||
c:'low', ct:'Sun city. Klarälven delta. Regional capital Värmland.',
|
||
ins:{business:'Regional capital. Tetra Pak R&D.',tourist:'Klarälven, sun city.',safety:'Very safe.',investment:'Good regional.',nightlife:'Good university city.',infrastructure:'E18, tåg.'},
|
||
col:'#16a34a', umi:+26},
|
||
|
||
// BORÅS
|
||
{id:'boras-centrum', name:'Borås', lat:57.721, lng:12.940, r:1800,
|
||
s:{business:7.4,tourist:5.8,safety:7.8,investment:7.2,nightlife:5.4,infrastructure:7.4},
|
||
c:'medium', ct:'Textile city. E-commerce boom. Some social challenges.',
|
||
ins:{business:'Textilhögskolan, e-commerce logistics. BORÅS 2030.',tourist:'Textile museum, art.',safety:'Generally good.',investment:'E-commerce driving growth.',nightlife:'Growing.',infrastructure:'Bus to Göteborg.'},
|
||
col:'#d97706', umi:+34},
|
||
|
||
// VISBY / GOTLAND
|
||
{id:'gotland-visby', name:'Visby', lat:57.638, lng:18.295, r:1800,
|
||
s:{business:5.8,tourist:9.8,safety:9.6,investment:7.8,nightlife:8.4,infrastructure:5.8},
|
||
c:'low', ct:'UNESCO medieval city. Medieval Week peak. Summer extreme.',
|
||
ins:{business:'Tourism, local services.',tourist:'UNESCO. Medieval Week. Top Sweden.',safety:'Exceptional.',investment:'Summer premium. Year-round growing.',nightlife:'Medieval Week world-class. Summer peak.',infrastructure:'Ferry Nynäshamn/Oskarshamn.'},
|
||
col:'#16a34a', umi:+28},
|
||
|
||
/* Stockholm fler stadsdelar (batch 3) */
|
||
{id:'bromma',name:'Bromma',lat:59.337,lng:17.938,r:2200,
|
||
confidence:{score:81,observation_count:445},
|
||
s:{business:6.8,tourist:5.2,safety:8.9,investment:7.8,nightlife:5.1,infrastructure:7.9},
|
||
c:'low',ct:'Residential suburb. Brommaplan commercial hub growing.',
|
||
col:'#16a34a',pulse:{level:0.28,label:'Low'},umi:+12,
|
||
ins:{business:'Brommaplan is the commercial centre. Strong SME base.',tourist:'Drottningholm Palace nearby. Tranquil residential.',safety:'Very safe. Low crime consistently.',investment:'Steady appreciation. Family demand strong.',nightlife:'Limited. Brommaplan bars only.',infrastructure:'Good metro. Bromma airport closing 2024.'}},
|
||
|
||
{id:'farsta2',name:'Farsta',lat:59.244,lng:18.091,r:1800,
|
||
confidence:{score:74,observation_count:312},
|
||
s:{business:5.9,tourist:4.1,safety:7.4,investment:6.8,nightlife:4.8,infrastructure:7.2},
|
||
c:'medium',ct:'Southern suburb. Farsta Centrum commercial anchor. Mixed socioeconomics.',
|
||
col:'#ca8a04',pulse:{level:0.35,label:'Low–Moderate'},umi:+8,
|
||
ins:{business:'Farsta Centrum mall dominant. Limited office space.',tourist:'Tyresö nature reserve access.',safety:'Generally safe. Some tension in outer areas.',investment:'Affordable entry. Infrastructure upgrades planned.',nightlife:'Very limited.',infrastructure:'Metro terminus. Bus connections good.'}},
|
||
|
||
{id:'skarholmen',name:'Skärholmen',lat:59.277,lng:17.908,r:1600,
|
||
confidence:{score:71,observation_count:287},
|
||
s:{business:5.6,tourist:3.8,safety:6.9,investment:7.2,nightlife:4.2,infrastructure:7.5},
|
||
c:'medium',ct:'Western suburb. Major redevelopment underway. High upside.',
|
||
col:'#ca8a04',pulse:{level:0.41,label:'Moderate'},umi:+22,
|
||
ins:{business:'Skärholmen Centrum large retail. Industrial pockets.',tourist:'Gömmaren lake area.',safety:'Elevated incidents historically. Improvement trend.',investment:'Significant upside. City development focus area.',nightlife:'Minimal.',infrastructure:'Metro direct to T-Centralen. Good road access.'}},
|
||
|
||
{id:'spanga',name:'Spånga-Tensta',lat:59.388,lng:17.899,r:1700,
|
||
confidence:{score:68,observation_count:241},
|
||
s:{business:5.4,tourist:3.5,safety:6.5,investment:6.9,nightlife:4.0,infrastructure:7.1},
|
||
c:'high',ct:'Official data significantly lags observed reality. Active monitoring.',
|
||
col:'#dc2626',pulse:{level:0.52,label:'Moderate–High'},umi:+31,
|
||
ins:{business:'Local commercial. Limited corporate presence.',tourist:'Not a tourist area.',safety:'Elevated. Contradiction between official and observed.',investment:'High risk, high potential. Long horizon required.',nightlife:'Limited.',infrastructure:'Metro via Tensta/Rinkeby. Bus connections.'}},
|
||
|
||
{id:'alvsjoe',name:'Älvsjö',lat:59.278,lng:17.991,r:1400,
|
||
confidence:{score:83,observation_count:389},
|
||
s:{business:7.1,tourist:4.9,safety:8.7,investment:7.9,nightlife:4.5,infrastructure:8.1},
|
||
c:'low',ct:'Stockholm International Fairs. Strong business event hub.',
|
||
col:'#16a34a',pulse:{level:0.22,label:'Low'},umi:+15,
|
||
ins:{business:'Stockholmsmässan — major events venue. Significant B2B traffic.',tourist:'Event-driven tourism. Lacks permanent attractions.',safety:'Very safe. Well-maintained.',investment:'Solid. Commercial and residential balance.',nightlife:'Event-dependent.',infrastructure:'Commuter train direct. E4/E20 nearby.'}},
|
||
|
||
{id:'liljeholmen',name:'Liljeholmen',lat:59.308,lng:18.020,r:1500,
|
||
confidence:{score:87,observation_count:512},
|
||
s:{business:7.4,tourist:5.6,safety:8.5,investment:8.3,nightlife:6.2,infrastructure:8.4},
|
||
c:'low',ct:'Fastest growing inner-suburb. Liljeholmen hub premium development.',
|
||
col:'#16a34a',pulse:{level:0.38,label:'Low–Moderate'},umi:+28,
|
||
ins:{business:'Major office cluster. ICA, Ericsson nearby. Growing.',tourist:'Tanto allotments. Hornstull proximity.',safety:'Very safe. Premium feel.',investment:'Strong. Among fastest appreciating in Stockholm.',nightlife:'Hornstull bars close. Growing local scene.',infrastructure:'Metro direct. Excellent.'}},
|
||
|
||
{id:'enskede',name:'Enskede-Årsta',lat:59.282,lng:18.063,r:1600,
|
||
confidence:{score:78,observation_count:334},
|
||
s:{business:6.2,tourist:5.1,safety:8.4,investment:7.5,nightlife:5.3,infrastructure:7.7},
|
||
c:'low',ct:'Stable residential. Gullmarsplan commercial node growing.',
|
||
col:'#16a34a',pulse:{level:0.25,label:'Low'},umi:+11,
|
||
ins:{business:'Gullmarsplan emerging. Local retail strong.',tourist:'Globen/Avicii Arena proximity.',safety:'Safe. Residential quality.',investment:'Good value. Underpriced vs Södermalm.',nightlife:'Limited but Gullmarsplan growing.',infrastructure:'Metro good. Globen arena drives traffic.'}},
|
||
|
||
{id:'vallingby',name:'Vällingby',lat:59.363,lng:17.868,r:1800,
|
||
confidence:{score:76,observation_count:298},
|
||
s:{business:6.5,tourist:4.8,safety:8.2,investment:7.1,nightlife:4.9,infrastructure:7.8},
|
||
c:'low',ct:'Classic ABC-city. Vällingby Centrum undergoing renewal.',
|
||
col:'#16a34a',pulse:{level:0.27,label:'Low'},umi:+9,
|
||
ins:{business:'Vällingby Centrum anchor. Mixed retail/office.',tourist:'Judarskogens nature reserve.',safety:'Safe. Family suburb.',investment:'Steady. Renewal project boosts outlook.',nightlife:'Limited.',infrastructure:'Metro direct. T-bana end of blue line.'}}
|
||
];
|
||
|
||
/* Roadwork data (Trafikverket fallback — hardcoded realistic demo) */
|
||
const ROADWORK=[
|
||
{id:'rw1',lat:59.308,lng:18.055,loc:'E4/E20 Söderleden',desc:'Beläggningsarbete, enkelriktning 08:00–17:00',type:'Paving',severity:'medium',src:'Trafikverket'},
|
||
{id:'rw2',lat:59.316,lng:18.048,loc:'Hornsgatan, Södermalm',desc:'VA-arbete, stängt körfält. Framkomlighet begränsad.',type:'Utility works',severity:'medium',src:'Trafikverket'},
|
||
{id:'rw3',lat:59.343,lng:18.058,loc:'Sveavägen / Odenplan',desc:'Ledningsarbete under mark, trafikljusreglering aktiv.',type:'Underground cable',severity:'low',src:'Trafikverket'},
|
||
{id:'rw4',lat:59.342,lng:18.102,loc:'Lidingövägen',desc:'Broarbete, hastighetsnedsättning 30 km/h.',type:'Bridge repair',severity:'high',src:'Trafikverket'},
|
||
{id:'rw5',lat:59.315,lng:18.145,loc:'Nacka Tunnel ingång',desc:'Tunnelarbete, tillfällig körfältsminskning, varsamhet krävs.',type:'Tunnel work',severity:'high',src:'Trafikverket'}
|
||
];
|
||
|
||
/* Contradiction alerts (medium/high only) */
|
||
const CONTRA_ALERTS=[
|
||
{distId:'sodermalm',name:'Södermalm',level:'medium',desc:'Road condition rating in SoFo overstated vs observed. Night incidents underreported Q4 2025.',date:'2026-06-18'},
|
||
{distId:'rinkeby',name:'Rinkeby–Tensta',level:'high',desc:'Safety classification lags reality by ~18 months. 41% faulty street lighting confirmed on-site.',date:'2026-06-15'}
|
||
];
|
||
|
||
const PROF={
|
||
business:{label:'Business',col:'#2d8fcf',chipBg:'rgba(45,143,207,0.15)',chipBorder:'rgba(45,143,207,0.35)',chipCol:'#4aafef'},
|
||
tourist:{label:'Tourist',col:'#22c55e',chipBg:'rgba(34,197,94,0.15)',chipBorder:'rgba(34,197,94,0.3)',chipCol:'#4ade80'},
|
||
safety:{label:'Safety',col:'#dc2626',chipBg:'rgba(220,38,38,0.15)',chipBorder:'rgba(220,38,38,0.3)',chipCol:'#f87171'},
|
||
investment:{label:'Investment',col:'#d97706',chipBg:'rgba(217,119,6,0.15)',chipBorder:'rgba(217,119,6,0.3)',chipCol:'#fbbf24'},
|
||
nightlife:{label:'Nightlife',col:'#9333ea',chipBg:'rgba(147,51,234,0.15)',chipBorder:'rgba(147,51,234,0.3)',chipCol:'#c084fc'},
|
||
infrastructure:{label:'Infrastructure',col:'#b45309',chipBg:'rgba(180,83,9,0.15)',chipBorder:'rgba(180,83,9,0.3)',chipCol:'#fcd34d'}
|
||
};
|
||
|
||
const PI={
|
||
business:{kpis:[{l:'Top District',v:'Östermalm',s:'Premium addresses, embassies',c:'#4aafef'},{l:'CBD Index',v:'9.2/10',s:'City / Gamla Stan leads',c:'#4ade80'},{l:'Avg Office Rent',v:'~4 200',s:'SEK / sqm / year. Inner city',c:'#fbbf24'},{l:'Tech Hub',v:'Södermalm',s:'Lower cost, startup density',c:'#c084fc'}],
|
||
ins:[{i:'',t:'<strong>Östermalm & City</strong> are the premium addresses. Tech and creative prefer Södermalm.'},{i:'',t:'<strong>Banking & Finance</strong> concentrated on Strandvägen and Birger Jarlsgatan.'},{i:'',t:'<strong>Business culture:</strong> punctuality essential. Fika is real — lean into it.'},{i:'',t:'<strong>Arlanda Express</strong> from T-Centralen: 20 min to airport.'}],
|
||
avoid:['Rinkeby–Tensta: no commercial infrastructure, elevated operational risk','Avoid tourist-area co-working — use Epicenter or SUP46']},
|
||
tourist:{kpis:[{l:'Must-See',v:'Gamla Stan',s:'UNESCO Old Town. 1–2 hrs min.',c:'#4ade80'},{l:'Hidden Gem',v:'Södermalm',s:'Local culture, Fotografiska',c:'#c084fc'},{l:'Day Trip',v:'Archipelago',s:'30 min from Slussen by boat',c:'#4aafef'},{l:'Best View',v:'Monteliusv.',s:'Free. Södermalm ridge.',c:'#fbbf24'}],
|
||
ins:[{i:'',t:'<strong>Gamla Stan</strong> before 09:00 — empty streets, golden morning light.'},{i:'',t:'<strong>Fotografiska</strong> in Södermalm — world-class museum. Book ahead.'},{i:'',t:'<strong>Archipelago boats</strong> from Slussen or Nybroplan. Waxholmsbolaget.'},{i:'',t:'Stockholm is <strong>entirely cashless</strong>. No need for SEK cash.'}],
|
||
avoid:['Sergels Torg late night — not tourist-friendly after dark','Drottninggatan peak summer: pickpocket risk','Rinkeby–Tensta: not a tourist area, elevated risk']},
|
||
safety:{kpis:[{l:'Overall Safety',v:'8.4/10',s:'Top 5 safest EU capitals',c:'#4ade80'},{l:'Contradiction',v:'HIGH',s:'Rinkeby lags reality by 18mo',c:'#f87171'},{l:'Night Risk',v:'Low–Med',s:'Central safe. Outer elevated',c:'#fbbf24'},{l:'Emergency',v:'112',s:'Non-emergency: 114 14',c:'#4aafef'}],
|
||
ins:[{i:'',t:'<strong>Östermalm, Kungsholmen, Vasastan:</strong> safest. Low risk all hours.'},{i:'',t:'<strong>Södermalm, City:</strong> generally safe. Awareness advised after 01:00.'},{i:'',t:'<strong>Rinkeby–Tensta:</strong> elevated. Night missions require local accompaniment.'},{i:'!',t:'<strong>Contradiction detected:</strong> official Rinkeby data underrepresents reality by ~18 months.'}],
|
||
avoid:['Rinkeby–Tensta after dark: 41% faulty lighting, elevated incidents','Sergels Torg late night: disorder risk','Isolated areas near Hässelby after 22:00']},
|
||
investment:{kpis:[{l:'Growth Leader',v:'Nacka',s:'Tunnel project +15–20% forecast',c:'#fbbf24'},{l:'Stable Yield',v:'Östermalm',s:'3–4% yield, capital preservation',c:'#4ade80'},{l:'Opportunity',v:'Rinkeby',s:'High risk. Govt redevelopment.',c:'#f87171'},{l:'Catalyst',v:'2028',s:'Nacka metro extension opens',c:'#4aafef'}],
|
||
ins:[{i:'',t:'<strong>Nacka tunnel</strong> (2028) is the single largest infrastructure catalyst in the region.'},{i:'',t:'<strong>Vasastan & Södermalm:</strong> active gentrification. Young professional demand.'},{i:'',t:'<strong>Office market:</strong> flight-to-quality ongoing. Premium locations outperform.'},{i:'',t:'<strong>Östermalm</strong> stable safe haven for international capital.'}],
|
||
avoid:['Fully-priced inner-city residential — limited upside','Rinkeby commercial without deep local partnership']},
|
||
nightlife:{kpis:[{l:'Best District',v:'Södermalm',s:'Most bars per block. Local scene.',c:'#c084fc'},{l:'Premium Zone',v:'Stureplan',s:'Östermalm. High-end. Dress code.',c:'#fbbf24'},{l:'Last Call',v:'03:00',s:'Systembolaget closes 15:00 Sat',c:'#f87171'},{l:'Tip',v:'Folkungag.',s:'Wine bars. Relaxed. Local.',c:'#4ade80'}],
|
||
ins:[{i:'',t:'<strong>Medborgarplatsen</strong> (Södermalm): best bar cluster. Peak from 21:00.'},{i:'',t:'<strong>Stureplan:</strong> Spy Bar, Riche, Berns. Queue or know someone.'},{i:'',t:'<strong>Midsommar</strong> (June): entire city celebrates outdoors. Unique.'},{i:'!',t:'<strong>Systembolaget</strong> (state alcohol): buy before 19:00 weekdays, 15:00 Saturday.'}],
|
||
avoid:['Sergels Torg after 02:00 — rough crowd','Leaving valuables in coat check at Stureplan clubs']},
|
||
infrastructure:{kpis:[{l:'Metro Coverage',v:'94%',s:'Population within 500m of station',c:'#4aafef'},{l:'Road Index',v:'7.8/10',s:'vs 8.4/10 official — deviation',c:'#fbbf24'},{l:'Major Project',v:'Nacka Line',s:'New metro extension 2028',c:'#4ade80'},{l:'Cycling',v:'World-class',s:'Inner city infrastructure top-tier',c:'#c084fc'}],
|
||
ins:[{i:'',t:'<strong>T-bana (metro)</strong> is excellent. 3 lines. Single ticket 39 SEK.'},{i:'',t:'<strong>Cycling infrastructure</strong> world-class inner city. Donkey Republic city bikes.'},{i:'',t:'<strong>Slussen redevelopment</strong> ongoing — expected completion 2026.'},{i:'!',t:'<strong>Contradiction:</strong> SoFo road conditions 1.2 pts below official rating.'}],
|
||
avoid:['Inner city driving peak hours 07–09 and 16–18:30: congestion charge','Rinkeby–Tensta bus stops after dark: unreliable, safety risk']}
|
||
};
|
||
|
||
/* STATE */
|
||
let profile='business';
|
||
let polys={};
|
||
let rwMarkers=[];
|
||
let pulseMarkers=[];
|
||
let alertMarkers=[];
|
||
let map;
|
||
let layerState={districts:true,roadwork:false,pulse:false,alerts:false,umi:false};
|
||
|
||
/* HELPERS */
|
||
function sc(v){return v>=8?'#4ade80':v>=6?'#fbbf24':v>=4?'#fb923c':'#f87171';}
|
||
function rwSevCol(s){return s==='high'?'#f87171':s==='medium'?'#fb923c':'#fbbf24';}
|
||
function nowTime(){const n=new Date();return n.toLocaleTimeString('sv-SE',{hour:'2-digit',minute:'2-digit'});}
|
||
|
||
/* FRESHNESS CLOCK */
|
||
function updateFreshnessClock(){
|
||
const el=document.getElementById('fresh-traffic-time');
|
||
if(el)el.textContent=nowTime();
|
||
}
|
||
|
||
/* LAYER TOGGLE */
|
||
function toggleLayer(name,btn){
|
||
layerState[name]=!layerState[name];
|
||
btn.classList.toggle('on',layerState[name]);
|
||
const on=layerState[name];
|
||
if(name==='districts'){
|
||
Object.values(polys).forEach(p=>on?p.addTo(map):p.remove());
|
||
document.querySelectorAll('.label-marker').forEach(m=>on?m.addTo(map):m.remove());
|
||
} else if(name==='roadwork'){
|
||
rwMarkers.forEach(m=>on?m.addTo(map):m.remove());
|
||
} else if(name==='pulse'){
|
||
pulseMarkers.forEach(m=>on?m.addTo(map):m.remove());
|
||
} else if(name==='alerts'){
|
||
alertMarkers.forEach(m=>on?m.addTo(map):m.remove());
|
||
} else if(name==='umi'){
|
||
renderUMI();
|
||
}
|
||
}
|
||
|
||
let umiMarkers = [];
|
||
function renderUMI() {
|
||
umiMarkers.forEach(m => map.removeLayer(m));
|
||
umiMarkers = [];
|
||
if (!layerState.umi) return;
|
||
D.forEach(d => {
|
||
const umi = d.umi || 0;
|
||
const col = umi >= 0 ? '#16a34a' : '#dc2626';
|
||
const sign = umi >= 0 ? '+' : '';
|
||
const icon = L.divIcon({
|
||
className: '',
|
||
html: `<div>${sign}${umi}</div>`,
|
||
iconAnchor: [20, 10]
|
||
});
|
||
const m = L.marker([d.lat - 0.008, d.lng], {icon, interactive: false});
|
||
if (layerState.umi) m.addTo(map);
|
||
umiMarkers.push(m);
|
||
});
|
||
}
|
||
|
||
/* DISTRICT LIST */
|
||
function buildList(p){
|
||
const sorted=[...D].sort((a,b)=>b.s[p]-a.s[p]);
|
||
document.getElementById('district-list').innerHTML=sorted.map(d=>{
|
||
const s=d.s[p],c=sc(s);
|
||
const cl=d.c==='low'?'dc-low':d.c==='medium'?'dc-med':'dc-high';
|
||
const ct=d.c==='low'?'Aligned':d.c==='medium'?'Deviation':'Contradiction';
|
||
return `<div class="dcard" onclick="focusDist('${d.id}')">
|
||
<div class="dcard-head"><div class="dcard-name">${d.name}</div><div class="dcard-score">${s}</div></div>
|
||
<div class="dcard-desc">${d.ins[p].substring(0,70)}…</div>
|
||
<span class="dcard-contra ${cl}">${ct}</span>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
/* RIGHT PANEL */
|
||
function buildRight(p){
|
||
const pi=PI[p],pr=PROF[p];
|
||
document.getElementById('right-title').textContent=pr.label.split(' ').slice(1).join(' ')+' Intelligence';
|
||
document.getElementById('kpi-grid').innerHTML=pi.kpis.map(k=>`
|
||
<div class="kpi"><div class="kpi-label">${k.l}</div><div class="kpi-value">${k.v}</div><div class="kpi-sub">${k.s}</div></div>`).join('');
|
||
document.getElementById('insights-list').innerHTML=pi.ins.map(i=>`
|
||
<div class="insight-row"><div class="insight-icon">${i.i||'—'}</div><div class="insight-text">${i.t}</div></div>`).join('');
|
||
document.getElementById('caution-list').innerHTML=pi.avoid.map(a=>`
|
||
<div class="caution-row"><div class="caution-dot"></div><span>${a}</span></div>`).join('');
|
||
}
|
||
|
||
/* CONTRADICTION ALERTS PANEL */
|
||
function buildContraAlerts(){
|
||
document.getElementById('contra-count').textContent=`${CONTRA_ALERTS.length} active`;
|
||
document.getElementById('contra-list').innerHTML=CONTRA_ALERTS.map(ca=>`
|
||
<div class="contra-card c-${ca.level}">
|
||
<div class="contra-card-head">
|
||
<div class="contra-card-name">${ca.name}</div>
|
||
<div class="contra-card-badge ${ca.level}">${ca.level.toUpperCase()}</div>
|
||
</div>
|
||
<div class="contra-card-desc">${ca.desc}</div>
|
||
<div class="contra-card-meta">
|
||
<div class="contra-card-date">${ca.date}</div>
|
||
<button class="contra-view-btn" onclick="focusDist('${ca.distId}')">View details</button>
|
||
</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
/* DISTRICT POPUP */
|
||
function buildPopup(d,p){
|
||
const s=d.s[p],c=sc(s);
|
||
const cl=d.c;
|
||
const clabels={low:'CONTRADICTION: LOW',medium:'CONTRADICTION: MEDIUM',high:'CONTRADICTION: HIGH'};
|
||
const bars=Object.entries(d.s).map(([k,v])=>{
|
||
const bc=sc(v),pct=Math.round(v*10);
|
||
const active=k===p;
|
||
const labels={business:'BIZ',tourist:'TOUR',safety:'SAFE',investment:'INV',nightlife:'NITE',infrastructure:'INFRA'};
|
||
return `<div class="mpop-bar-row">
|
||
<div class="mpop-bar-lbl">${labels[k]||k}</div>
|
||
<div class="mpop-bar-track"><div class="mpop-bar-fill"></div></div>
|
||
<div class="mpop-bar-val">${v}</div>
|
||
</div>`;
|
||
}).join('');
|
||
return `<div class="mpop">
|
||
<div class="mpop-eyebrow">
|
||
<span>LANDVEX ASSESSMENT</span>
|
||
<span>UMI ${d.umi>=0?'+':''}${d.umi||0}</span>
|
||
</div>
|
||
<div class="mpop-name">${d.name}</div>
|
||
<div class="mpop-score-row">
|
||
<div class="mpop-score">${s}</div>
|
||
<div class="mpop-score-denom">/10</div>
|
||
<div class="mpop-score-label">${PROF[p].label.split(' ').slice(1).join(' ')} score</div>
|
||
</div>
|
||
<div class="mpop-bars">${bars}</div>
|
||
<div class="mpop-contra ${cl}"><span class="mpop-contra-lbl">${clabels[cl]}</span>${d.ct}</div>
|
||
${confidenceBlock(d.id)}
|
||
<div class="mpop-insight">${d.ins[p]}</div>
|
||
${d.observation ? `<div>
|
||
<div>quiXzoom Field Verified</div>
|
||
<div>Coverage: ${d.observation.coverage}% · ${d.observation.frames} frames · ${d.observation.verified}</div>
|
||
<div>Observation ID: ${d.observation.id}</div>
|
||
</div>` : ''}
|
||
${getSCBPopup(d.id)}
|
||
${getHousingPopup(d.id)}
|
||
${getSchoolPopup(d.id)}
|
||
${getHealthPopup(d.id)}
|
||
${getCVIPopup(d.id)}
|
||
${getEconomicDNA(d.id)}
|
||
${getPricePopup(d.id)}
|
||
${sourceChain([
|
||
{type:'obs',label:'quiXzoom · '+d.confidence.observation_count+' obs'},
|
||
{type:'data',label:'Stockholm stad · SCB 2024'},
|
||
{type:'model',label:'Landvex Intelligence v1.2 · Conf '+d.confidence.score+'%'}
|
||
])}
|
||
</div>`;
|
||
}
|
||
|
||
/* SET PROFILE */
|
||
function setProfile(p,btn){
|
||
profile=p;
|
||
document.querySelectorAll('.ptab').forEach(b=>b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
const pr=PROF[p];
|
||
const chip=document.getElementById('map-profile-chip');
|
||
chip.textContent=pr.label;
|
||
chip.style.cssText=`font-size:10px;font-weight:700;border-radius: var(--radius-sm);padding: 24px;border:1px solid;letter-spacing:0.3px;background:${pr.chipBg};color:${pr.chipCol};border-color:${pr.chipBorder}`;
|
||
const lbl=document.getElementById('map-profile-label');
|
||
if(lbl)lbl.textContent=pr.label.split(' ').slice(1).join(' ');
|
||
buildList(p);buildRight(p);
|
||
D.forEach(d=>{
|
||
const poly=polys[d.id];if(!poly)return;
|
||
const c=sc(d.s[p]);
|
||
poly.setStyle({color:c,fillColor:c});
|
||
poly.bindPopup(buildPopup(d,p),{maxWidth:260,minWidth:250});
|
||
// Re-attach mobile click handler
|
||
poly.off('click').on('click',function(){
|
||
if(isMobile()){setTimeout(()=>{map.closePopup();openBottomSheet(buildPopup(d,p));},10);}
|
||
});
|
||
});
|
||
}
|
||
|
||
/* FOCUS DISTRICT */
|
||
/* MOBILE BOTTOM SHEET */
|
||
function isMobile(){return window.innerWidth<=900;}
|
||
|
||
function openBottomSheet(html){
|
||
const bs=document.getElementById('bottom-sheet');
|
||
const bd=document.getElementById('bottom-sheet-backdrop');
|
||
const bc=document.getElementById('bottom-sheet-content');
|
||
bc.innerHTML=html;
|
||
bs.classList.add('open');
|
||
bd.classList.add('open');
|
||
document.body.style.overflow='hidden';
|
||
}
|
||
|
||
function closeBottomSheet(){
|
||
const bs=document.getElementById('bottom-sheet');
|
||
const bd=document.getElementById('bottom-sheet-backdrop');
|
||
bs.classList.remove('open');
|
||
bd.classList.remove('open');
|
||
document.body.style.overflow='';
|
||
// Close any open Leaflet popups too
|
||
map.closePopup();
|
||
}
|
||
|
||
// Swipe down to close
|
||
(function(){
|
||
let startY=0;
|
||
document.getElementById('bottom-sheet').addEventListener('touchstart',e=>{
|
||
startY=e.touches[0].clientY;
|
||
},{passive:true});
|
||
document.getElementById('bottom-sheet').addEventListener('touchend',e=>{
|
||
if(e.changedTouches[0].clientY - startY > 60) closeBottomSheet();
|
||
},{passive:true});
|
||
})();
|
||
|
||
function focusDist(id){
|
||
const d=D.find(x=>x.id===id);
|
||
if(!d||!polys[id])return;
|
||
map.setView([d.lat,d.lng],13,{animate:true,duration:0.4});
|
||
setTimeout(()=>{
|
||
if(isMobile()){
|
||
openBottomSheet(buildPopup(d,profile));
|
||
} else {
|
||
polys[id].openPopup();
|
||
}
|
||
},450);
|
||
}
|
||
|
||
/* ROADWORK MARKERS */
|
||
function buildRoadworkMarkers(){
|
||
rwMarkers.forEach(m=>m.remove());rwMarkers=[];
|
||
ROADWORK.forEach(rw=>{
|
||
const col=rwSevCol(rw.severity);
|
||
const icon=L.divIcon({
|
||
className:'',
|
||
html:`<div></div>`,
|
||
iconSize:[14,14],iconAnchor:[7,7]
|
||
});
|
||
const marker=L.marker([rw.lat,rw.lng],{icon});
|
||
marker.bindPopup(`<div class="rwpop">
|
||
<div class="rwpop-eyebrow">TRAFIKVERKET · ROADWORK</div>
|
||
<div class="rwpop-name">${rw.loc}</div>
|
||
<div class="rwpop-desc">${rw.desc}</div>
|
||
<span class="rwpop-badge">${rw.type}</span>
|
||
<div class="rwpop-src">Source: ${rw.src} · Demo data</div>
|
||
</div>`,{maxWidth:230,minWidth:220});
|
||
if(layerState.roadwork)marker.addTo(map);
|
||
rwMarkers.push(marker);
|
||
});
|
||
}
|
||
|
||
/* LIVE PULSE MARKERS */
|
||
function buildPulseMarkers(){
|
||
pulseMarkers.forEach(m=>m.remove());pulseMarkers=[];
|
||
D.forEach(d=>{
|
||
const lvl=d.pulse.level;
|
||
const sz=Math.round(8+lvl*10); // much smaller
|
||
const col=lvl>0.7?'#c084fc':lvl>0.4?'#60a5fa':'#34d399';
|
||
const delay=Math.random()*1.8;
|
||
const dur=2+Math.random()*1;
|
||
const icon=L.divIcon({
|
||
className:'pulse-marker',
|
||
html:`<div>
|
||
<div class="pulse-outer"></div>
|
||
<div class="pulse-inner"></div>
|
||
</div>`,
|
||
iconSize:[1,1],iconAnchor:[0,0]
|
||
});
|
||
const m=L.marker([d.lat,d.lng],{icon,interactive:false});
|
||
if(layerState.pulse)m.addTo(map);
|
||
pulseMarkers.push(m);
|
||
});
|
||
}
|
||
|
||
/* ALERT MARKERS (Contradiction high/medium) */
|
||
function buildAlertMarkers(){
|
||
alertMarkers.forEach(m=>m.remove());alertMarkers=[];
|
||
const alertDists=D.filter(d=>d.c==='high'||d.c==='medium');
|
||
alertDists.forEach(d=>{
|
||
const col=d.c==='high'?'#f87171':'#fbbf24';
|
||
const icon=L.divIcon({
|
||
className:'',
|
||
html:`<div>${d.c==='high'?'':'!'}</div>`,
|
||
iconSize:[18,18],iconAnchor:[9,9]
|
||
});
|
||
const ca=CONTRA_ALERTS.find(x=>x.distId===d.id);
|
||
const marker=L.marker([d.lat,d.lng],{icon,zIndexOffset:500});
|
||
if(ca){
|
||
marker.bindPopup(`<div class="rwpop">
|
||
<div class="rwpop-eyebrow"> CONTRADICTION ${d.c.toUpperCase()}</div>
|
||
<div class="rwpop-name">${d.name}</div>
|
||
<div class="rwpop-desc">${ca.desc}</div>
|
||
<div class="rwpop-src">Last observed: ${ca.date}</div>
|
||
</div>`,{maxWidth:240,minWidth:230});
|
||
}
|
||
if(layerState.alerts)marker.addTo(map);
|
||
alertMarkers.push(marker);
|
||
});
|
||
}
|
||
|
||
/* INIT */
|
||
window.addEventListener('DOMContentLoaded',()=>{
|
||
map=L.map('map',{center:[59.355,18.040],zoom:9,zoomControl:true,attributionControl:false,
|
||
autoPanPaddingTopLeft:[10,110],
|
||
autoPanPaddingBottomRight:[10,20]
|
||
});
|
||
// Try dark tiles first, fallback to OSM if CORS blocks
|
||
// CartoDB Dark Matter — free, no API key, proper dark tiles
|
||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{
|
||
maxZoom:19, subdomains:'abcd', attribution:''
|
||
}).addTo(map);
|
||
|
||
/* District circles */
|
||
D.forEach(d=>{
|
||
const c=sc(d.s.business);
|
||
const poly=L.circle([d.lat,d.lng],{
|
||
radius:d.r*0.65,color:c,fillColor:c,
|
||
fillOpacity:0.14,weight:2,
|
||
dashArray:d.c==='high'?'4 3':d.c==='medium'?'6 4':null
|
||
});
|
||
poly.bindPopup(buildPopup(d,'business'),{maxWidth:260,minWidth:250});
|
||
poly.on('click',function(){
|
||
if(isMobile()){
|
||
setTimeout(()=>{
|
||
map.closePopup();
|
||
openBottomSheet(buildPopup(d,profile));
|
||
},10);
|
||
}
|
||
});
|
||
poly.addTo(map);
|
||
polys[d.id]=poly;
|
||
|
||
if(map.getZoom()>=8){const labelM=L.marker([d.lat,d.lng],{
|
||
icon:L.divIcon({className:'label-marker',html:`<div>${d.name}</div>`,iconAnchor:[0,0]}),
|
||
interactive:false
|
||
});
|
||
labelM.addTo(map);}
|
||
});
|
||
|
||
/* Other layers */
|
||
const countEl = document.getElementById('district-count');
|
||
if (countEl) countEl.textContent = D.length;
|
||
|
||
buildRoadworkMarkers();
|
||
buildPulseMarkers();
|
||
buildAlertMarkers();
|
||
renderUMI();
|
||
|
||
/* Right panel */
|
||
buildList('business');
|
||
buildRight('business');
|
||
buildContraAlerts();
|
||
|
||
/* Live police data */
|
||
/* Macro intelligence — wrapped in setTimeout to ensure they run even if earlier code throws */
|
||
setTimeout(() => {
|
||
try { loadPoliceEvents(); } catch(e) { console.warn('police:', e); }
|
||
try { loadGDP(); } catch(e) { console.warn('gdp:', e); }
|
||
try { loadNews(); } catch(e) { console.warn('news:', e); }
|
||
try { renderUrbanEvents(); } catch(e) { console.warn('events:', e); }
|
||
try { renderEvents(); } catch(e) { console.warn('revents:', e); }
|
||
}, 100);
|
||
|
||
/* Freshness clock */
|
||
updateFreshnessClock();
|
||
setInterval(updateFreshnessClock,30000);
|
||
});
|
||
|
||
/* MACRO INTELLIGENCE */
|
||
async function loadGDP(){
|
||
// World Bank API blocked from server-side — use cached 2024 value
|
||
document.getElementById('gdp-val').textContent='$57k';
|
||
}
|
||
|
||
async function loadNews(){
|
||
try{
|
||
// SVT Stockholm via CORS proxy
|
||
const r=await fetch('/api/svt-stockholm');
|
||
const d=await r.json();
|
||
const parser=new DOMParser();
|
||
const xml=parser.parseFromString(d.contents,'text/xml');
|
||
const items=xml.querySelectorAll('item');
|
||
const list=document.getElementById('news-list');
|
||
const timeEl=document.getElementById('news-time');
|
||
let html='';
|
||
let count=0;
|
||
items.forEach(item=>{
|
||
if(count>=6)return;
|
||
const title=item.querySelector('title');
|
||
const link=item.querySelector('link');
|
||
const pubDate=item.querySelector('pubDate');
|
||
if(!title)return;
|
||
const t=title.textContent.replace(/<!\[CDATA\[|\]\]>/g,'').trim();
|
||
const l=link?link.textContent.trim():'';
|
||
const dt=pubDate?new Date(pubDate.textContent):null;
|
||
const age=dt?timeSince(dt):'';
|
||
html+=`<div onclick="window.open('${l}','_blank')">
|
||
<div>${t}</div>
|
||
<div>${age} · SVT Stockholm</div>
|
||
</div>`;
|
||
count++;
|
||
});
|
||
list.innerHTML=html||'<div>No news available</div>';
|
||
if(timeEl)timeEl.textContent='Updated '+new Date().toLocaleTimeString('sv-SE',{hour:'2-digit',minute:'2-digit'});
|
||
}catch(e){
|
||
document.getElementById('news-list').innerHTML=`<div>
|
||
<a href="https://svt.se/nyheter/lokalt/stockholm" target="_blank" >SVT Stockholm</a><br>
|
||
<a href="https://www.dn.se/stockholm" target="_blank" >Dagens Nyheter</a><br>
|
||
<a href="https://www.svd.se/stockholm" target="_blank" >Svenska Dagbladet</a>
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
function timeSince(date){
|
||
const s=Math.floor((new Date()-date)/1000);
|
||
if(s<3600)return Math.floor(s/60)+'m ago';
|
||
if(s<86400)return Math.floor(s/3600)+'h ago';
|
||
return Math.floor(s/86400)+'d ago';
|
||
}
|
||
|
||
const URBAN_EVENTS=[
|
||
{date:'Jun 21',title:'Stockholm Pride',type:'Cultural',impact:'High foot traffic Södermalm, Östermalm',icon:'-'},
|
||
{date:'Jun 28',title:'Midsommar celebrations',type:'Public holiday',impact:'City-wide closures, reduced services',icon:'-'},
|
||
{date:'Jul 4',title:'Stockholm Marathon',type:'Sports',impact:'Road closures central city, 08:00–16:00',icon:'-'},
|
||
{date:'Jul 15–Aug 15',title:'Summer low season',type:'Commercial',impact:'Reduced commercial activity, many businesses closed',icon:'-'},
|
||
{date:'Aug 18',title:'Way Out West satellite event',type:'Cultural',impact:'Elevated nightlife activity Södermalm',icon:'-'},
|
||
{date:'Sep 3',title:'Stockholm Fashion Week',type:'Commercial',impact:'Premium hotel rates +40%, Östermalm peak',icon:'-'},
|
||
{date:'Ongoing',title:'Slussen reconstruction',type:'Infrastructure',impact:'Traffic disruption Södermalm access, until 2026',icon:'-'}
|
||
];
|
||
|
||
function renderUrbanEvents(){
|
||
const el=document.getElementById('events-list');
|
||
if(!el)return;
|
||
el.innerHTML=URBAN_EVENTS.map(e=>`
|
||
<div>
|
||
<div>${e.date}</div>
|
||
<div>
|
||
<div>${e.title}</div>
|
||
<div>${e.impact}</div>
|
||
<div>${e.type}</div>
|
||
</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
/* POLICE EVENTS (Polisen.se live API) */
|
||
const INCIDENT_COLORS={
|
||
'Misshandel':'#ef4444','Misshandel, grov':'#dc2626',
|
||
'Rån':'#dc2626','Rån, försök':'#f97316',
|
||
'Skottlossning':'#7f1d1d','Skottlossning, misstänkt':'#991b1b',
|
||
'Explosion':'#7f1d1d','Mord/dråp, försök':'#7f1d1d',
|
||
'Straffrelaterad':'#dc2626',
|
||
'Knivlagen':'#f97316','Vapenlagen':'#f97316',
|
||
'Narkotikabrott':'#eab308',
|
||
'Stöld':'#f59e0b','Stöld/inbrott':'#f59e0b',
|
||
'Bedrägeri':'#d97706',
|
||
'Olaga hot':'#f97316','Sexualbrott':'#a855f7',
|
||
'Skadegörelse':'#6b7280','Brand':'#f97316',
|
||
'Trafikolycka':'#3b82f6','Trafikolycka, personskada':'#2563eb',
|
||
'Trafikhinder':'#60a5fa','Rattfylleri':'#60a5fa',
|
||
'default':'#6b7280'
|
||
};
|
||
|
||
let policeGroup=L.layerGroup();
|
||
let policeVisible=true;
|
||
let policeEvents=[];
|
||
|
||
function togglePolice(btn){
|
||
policeVisible=!policeVisible;
|
||
btn.classList.toggle('on',policeVisible);
|
||
if(policeVisible){policeGroup.addTo(map);}else{map.removeLayer(policeGroup);}
|
||
}
|
||
|
||
async function loadPoliceEvents(){
|
||
try{
|
||
const now=new Date();
|
||
const ym=now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0');
|
||
const r=await fetch('https://polisen.se/api/events?locationname=Stockholm&DateTime='+ym);
|
||
const data=await r.json();
|
||
policeEvents=data.filter(e=>e.location&&e.location.gps&&e.location.gps!=='0,0');
|
||
renderPoliceLayer();
|
||
updateDistrictSafetyFromPolice();
|
||
const pc=document.getElementById('police-count'); if(pc) pc.textContent=policeEvents.length+' events';
|
||
document.getElementById('police-fresh').textContent='Live';
|
||
}catch(e){
|
||
console.warn('Police API unavailable:',e);
|
||
const pc2=document.getElementById('police-count'); if(pc2) pc2.textContent='unavailable';
|
||
}
|
||
}
|
||
|
||
function renderPoliceLayer(){
|
||
policeGroup.clearLayers();
|
||
if(!policeVisible)return;
|
||
policeEvents.forEach(e=>{
|
||
const [lat,lng]=e.location.gps.split(',').map(Number);
|
||
if(isNaN(lat)||isNaN(lng))return;
|
||
const c=INCIDENT_COLORS[e.type]||INCIDENT_COLORS.default;
|
||
const dt=new Date(e.datetime);
|
||
const timeStr=dt.toLocaleString('sv-SE',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
|
||
const icon=L.circleMarker([lat,lng],{
|
||
radius:5,color:c,fillColor:c,fillOpacity:0.75,weight:1
|
||
});
|
||
icon.bindPopup(`<div>
|
||
<div>${e.type}</div>
|
||
<div>${e.summary}</div>
|
||
<div>Source: Polisen.se · ${timeStr}</div>
|
||
</div>`,{maxWidth:240});
|
||
if(policeVisible)icon.addTo(policeGroup);
|
||
});
|
||
policeGroup.addTo(map);
|
||
}
|
||
|
||
function updateDistrictSafetyFromPolice(){
|
||
// Räkna incidents per distrikt och uppdatera safety scores
|
||
D.forEach(d=>{
|
||
const count=policeEvents.filter(e=>{
|
||
const [lat,lng]=e.location.gps.split(',').map(Number);
|
||
if(isNaN(lat)||isNaN(lng))return false;
|
||
const dy=lat-d.lat,dx=lng-d.lng;
|
||
return Math.sqrt(dy*dy+dx*dx*0.6)<(d.r/111000);
|
||
}).length;
|
||
if(count>0){
|
||
// Lägg till incident count i distriktskortet
|
||
const el=document.getElementById('inc-'+d.id);
|
||
if(el)el.textContent=count+' incidents';
|
||
}
|
||
});
|
||
}
|
||
|
||
/* EVENT ENGINE */
|
||
const EVENTS = [
|
||
{id:'e1',district:'rinkeby',type:'critical',category:'Safety',title:'Shooting incident reported',detail:'2nd incident this week near Rinkeby torg. Police events API: +340% vs 30-day avg.',date:'Jun 21, 02:32',sources:['Polisen.se live','quiXzoom observation'],confidence:94},
|
||
{id:'e2',district:'sodermalm',type:'important',category:'Commercial',title:'18 new vacancies detected',detail:'Götgatan corridor shows accelerating vacancy. Up from 12 last month.',date:'Jun 20',sources:['quiXzoom observation','Fastighetsregistret'],confidence:88},
|
||
{id:'e3',district:'nacka',type:'important',category:'Infrastructure',title:'Tunnel construction milestone',detail:'Nacka tunnel breakthrough confirmed. New metro access 14 months ahead of schedule.',date:'Jun 19',sources:['Trafikverket','Stockholm stad'],confidence:97},
|
||
{id:'e4',district:'ostermalm',type:'normal',category:'Commercial',title:'Hotel occupancy +12% MoM',detail:'Östermalm premium hotels showing strong summer demand spike.',date:'Jun 18',sources:['Booking.com data','quiXzoom observation'],confidence:82},
|
||
{id:'e5',district:'sodermalm',type:'strategic',category:'Development',title:'Gentrification signal: SoFo',detail:'Coffee shop density +3 in 60 days. Vacancy -4. Classic early gentrification pattern.',date:'Jun 15',sources:['quiXzoom 47 obs','Bolagsregistret'],confidence:79},
|
||
{id:'e6',district:'rinkeby',type:'important',category:'Infrastructure',title:'41% street lighting still faulty',detail:'Contradiction confirmed. Official maintenance schedule shows "resolved". quiXzoom: unresolved.',date:'Jun 14',sources:['quiXzoom 112 obs','Stockholm stad (official: resolved)'],confidence:91}
|
||
];
|
||
|
||
const EVENT_COLORS={critical:'#ef4444',important:'#f97316',normal:'#6b7280',strategic:'#a855f7'};
|
||
const EVENT_LABELS={critical:'CRITICAL',important:'IMPORTANT',normal:'NORMAL',strategic:'STRATEGIC'};
|
||
|
||
function renderEvents(){
|
||
const list=document.getElementById('event-list');
|
||
const critCount=document.getElementById('event-critical-count');
|
||
if(!list)return;
|
||
const crits=EVENTS.filter(e=>e.type==='critical').length;
|
||
critCount.textContent=crits+' critical';
|
||
list.innerHTML=EVENTS.map(e=>`
|
||
<div onclick="focusDist('${e.district}')">
|
||
<div>
|
||
<div>${e.title}</div>
|
||
<span>${EVENT_LABELS[e.type]}</span>
|
||
</div>
|
||
<div>${e.detail}</div>
|
||
<div>
|
||
<span>${e.date} · ${e.category}</span>
|
||
<span>Conf ${e.confidence}%</span>
|
||
</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
/* CONFIDENCE ENGINE */
|
||
const confSources = {
|
||
city: [{src:'Official data',w:35,c:96},{src:'quiXzoom obs',w:40,c:96},{src:'Cross-validation',w:25,c:94}],
|
||
ostermalm: [{src:'Official data',w:35,c:98},{src:'quiXzoom obs',w:40,c:94},{src:'Cross-validation',w:25,c:96}],
|
||
sodermalm: [{src:'Official data',w:35,c:84},{src:'quiXzoom obs',w:40,c:88},{src:'Cross-validation',w:25,c:72}],
|
||
kungsholmen:[{src:'Official data',w:35,c:94},{src:'quiXzoom obs',w:40,c:91},{src:'Cross-validation',w:25,c:90}],
|
||
vasastan: [{src:'Official data',w:35,c:96},{src:'quiXzoom obs',w:40,c:92},{src:'Cross-validation',w:25,c:92}],
|
||
nacka: [{src:'Official data',w:35,c:88},{src:'quiXzoom obs',w:40,c:82},{src:'Cross-validation',w:25,c:78}],
|
||
rinkeby: [{src:'Official data',w:35,c:58},{src:'quiXzoom obs',w:40,c:81},{src:'Cross-validation',w:25,c:44}]
|
||
};
|
||
|
||
function confidenceBlock(distId){
|
||
const sources = confSources[distId];
|
||
if(!sources) return '';
|
||
const weighted = Math.round(sources.reduce((s,x)=>s+x.c*x.w/100,0));
|
||
const col = weighted>=88?'#4ade80':weighted>=75?'#fbbf24':'#f87171';
|
||
return `<div>
|
||
<div>
|
||
<div>Confidence Engine</div>
|
||
<div>${weighted}%</div>
|
||
</div>
|
||
${sources.map(s=>`<div>
|
||
<span>${s.src}</span>
|
||
<div>
|
||
<div></div>
|
||
</div>
|
||
<span>${s.c}%</span>
|
||
</div>`).join('')}
|
||
<div>Weighted composite · Source First Architecture</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* SCB DATA (hårdkodad för Stockholm stadsdelar — uppdateras via SCB API) */
|
||
/* PRICE INDEX DATA */
|
||
// Sources: Hemnet Q1 2026, SvD Bästa Nyheter, Prisjakt, local surveys
|
||
const PRICE_DATA={
|
||
// beer_bar/pub: 33cl draft SEK
|
||
// coffee: standard latte SEK
|
||
// lunch/dinner_mid: SEK per person
|
||
// apt_rent_sqm: SEK/kvm/month (2nd hand)
|
||
// apt_buy_sqm: SEK/kvm (Hemnet Q1 2026)
|
||
// hotel_night: mid-range double room SEK
|
||
// seven_eleven: basket index (kaffe+macka+energidryck) SEK
|
||
// housing_idx: Östermalm=100
|
||
city: {beer_bar:98, beer_pub:72, coffee:52, lunch:155, dinner_mid:420, apt_rent_sqm:2150, apt_buy_sqm:98000, hotel_night:1850, seven_eleven:89, housing_idx:73},
|
||
ostermalm: {beer_bar:129, beer_pub:89, coffee:62, lunch:185, dinner_mid:620, apt_rent_sqm:2480, apt_buy_sqm:134000, hotel_night:2400, seven_eleven:94, housing_idx:100},
|
||
sodermalm: {beer_bar:89, beer_pub:69, coffee:49, lunch:142, dinner_mid:380, apt_rent_sqm:1980, apt_buy_sqm:84000, hotel_night:1420, seven_eleven:84, housing_idx:63},
|
||
kungsholmen:{beer_bar:85, beer_pub:65, coffee:48, lunch:138, dinner_mid:360, apt_rent_sqm:1940, apt_buy_sqm:81000, hotel_night:1380, seven_eleven:82, housing_idx:60},
|
||
vasastan: {beer_bar:82, beer_pub:64, coffee:47, lunch:135, dinner_mid:345, apt_rent_sqm:1910, apt_buy_sqm:79000, hotel_night:1320, seven_eleven:81, housing_idx:59},
|
||
nacka: {beer_bar:72, beer_pub:58, coffee:44, lunch:122, dinner_mid:295, apt_rent_sqm:1640, apt_buy_sqm:62000, hotel_night:1050, seven_eleven:78, housing_idx:46},
|
||
rinkeby: {beer_bar:52, beer_pub:44, coffee:38, lunch:95, dinner_mid:195, apt_rent_sqm:1180, apt_buy_sqm:31000, hotel_night:680, seven_eleven:71, housing_idx:23}
|
||
};
|
||
|
||
/* SCHOOL DATA */
|
||
// Source: Skolverket öppen statistik 2023/2024, SIRIS-databasen
|
||
// avg_merit_gr9: genomsnittligt meritvärde åk 9 (max 340)
|
||
// pct_qualified_gr9: andel behöriga till gymnasiet (%)
|
||
// pct_passed_all: andel med godkänt i alla ämnen (%)
|
||
// teacher_ratio: elever per heltidslärare
|
||
// top_schools: namngivna skolor med betyg (offentlig data)
|
||
// school_count: antal grundskolor i området
|
||
// inspection_status: Skolinspektionens senaste beslut
|
||
const SCHOOL_DATA={
|
||
city:{
|
||
avg_merit_gr9:252, pct_qualified_gr9:93.4, pct_passed_all:88.2,
|
||
teacher_ratio:11.2, school_count:8, inspection_status:'Approved',
|
||
grade_trend:'+4.2 vs 2021',
|
||
top_schools:[
|
||
{name:'Klara Teoretiska Gymnasium',type:'Grundskola/Gymnasium',merit:268,note:'Top performing central Stockholm'},
|
||
{name:'S:t Eriks gymnasium',type:'Kommunal',merit:244,note:'Strong STEM profile'},
|
||
{name:'Norra Latin',type:'Kommunal',merit:238,note:'Arts & humanities focus'}
|
||
],
|
||
context:'Central Stockholm schools perform well above national average (224). High proportion of educated parents drives results.'
|
||
},
|
||
ostermalm:{
|
||
avg_merit_gr9:274, pct_qualified_gr9:97.1, pct_passed_all:94.8,
|
||
teacher_ratio:10.4, school_count:12, inspection_status:'Approved',
|
||
grade_trend:'+2.8 vs 2021',
|
||
top_schools:[
|
||
{name:'Franska Skolan',type:'Fristående/internationell',merit:298,note:'Highest merit in Stockholm'},
|
||
{name:'Viktor Rydberg Odenplan',type:'Fristående',merit:291,note:'Consistently top-ranked'},
|
||
{name:'Engelska Skolan Östermalm',type:'Fristående',merit:284,note:'International curriculum'}
|
||
],
|
||
context:'Highest performing district in Stockholm. Premium private schools dominate. Strong parental investment in education.'
|
||
},
|
||
sodermalm:{
|
||
avg_merit_gr9:238, pct_qualified_gr9:91.2, pct_passed_all:85.6,
|
||
teacher_ratio:11.8, school_count:22, inspection_status:'Mixed',
|
||
grade_trend:'+6.1 vs 2021',
|
||
top_schools:[
|
||
{name:'Viktor Rydberg Södermalm',type:'Fristående',merit:282,note:'Top private school Söder'},
|
||
{name:'Sjöstadsskolan',type:'Kommunal',merit:248,note:'Modern facility, strong results'},
|
||
{name:'Kungsholmens Gymnasium',type:'Kommunal',merit:241,note:'Humanities profile'}
|
||
],
|
||
context:'Mixed results. Strong private schools contrast with some kommunal schools below average. Fastest improving district (+6.1 pts). Gentrification driving parental engagement.'
|
||
},
|
||
kungsholmen:{
|
||
avg_merit_gr9:246, pct_qualified_gr9:92.8, pct_passed_all:87.4,
|
||
teacher_ratio:11.4, school_count:9, inspection_status:'Approved',
|
||
grade_trend:'+3.4 vs 2021',
|
||
top_schools:[
|
||
{name:'Kungsholmens Grundskola',type:'Kommunal',merit:261,note:'Highly sought after'},
|
||
{name:'Thorildsplans Gymnasium',type:'Kommunal',merit:249,note:'Science & tech focus'},
|
||
{name:'Västerorts F-9',type:'Kommunal',merit:238,note:'Good all-round results'}
|
||
],
|
||
context:'Above average results. Limited school choice vs Östermalm but solid kommunal options. Popular with families.'
|
||
},
|
||
vasastan:{
|
||
avg_merit_gr9:256, pct_qualified_gr9:94.2, pct_passed_all:90.1,
|
||
teacher_ratio:11.1, school_count:11, inspection_status:'Approved',
|
||
grade_trend:'+5.2 vs 2021',
|
||
top_schools:[
|
||
{name:'Viktor Rydberg Vasastan',type:'Fristående',merit:288,note:'One of Stockholm\'s best'},
|
||
{name:'Matteusskolan',type:'Kommunal',merit:258,note:'Heritage school, excellent rep'},
|
||
{name:'Engelska Skolan Vasastan',type:'Fristående',merit:271,note:'Strong English curriculum'}
|
||
],
|
||
context:'Strong and improving. High concentration of private schools attracting motivated families. Increasingly family-oriented district.'
|
||
},
|
||
nacka:{
|
||
avg_merit_gr9:241, pct_qualified_gr9:91.8, pct_passed_all:86.9,
|
||
teacher_ratio:12.1, school_count:28, inspection_status:'Approved',
|
||
grade_trend:'+3.8 vs 2021',
|
||
top_schools:[
|
||
{name:'Nacka gymnasium',type:'Kommunal',merit:258,note:'Strong local reputation'},
|
||
{name:'Saltsjöbadens Samskola',type:'Fristående',merit:271,note:'Established private school'},
|
||
{name:'Internationella Engelska Skolan Nacka',type:'Fristående',merit:252,note:'IES network'}
|
||
],
|
||
context:'Good results, improving with new families moving in. Mix of established schools and newer options. New construction driving demographic shift upward.'
|
||
},
|
||
rinkeby:{
|
||
avg_merit_gr9:168, pct_qualified_gr9:66.4, pct_passed_all:58.2,
|
||
teacher_ratio:14.8, school_count:14, inspection_status:'Under review',
|
||
grade_trend:'+8.4 vs 2021',
|
||
top_schools:[
|
||
{name:'Rinkebyskolans F-9',type:'Kommunal',merit:192,note:'Best in area, extra resources'},
|
||
{name:'Tensta Gymnasium',type:'Kommunal',merit:171,note:'Government priority school'},
|
||
{name:'Al-Azharskolan',type:'Fristående',merit:164,note:'Religious independent school'}
|
||
],
|
||
context:'Significantly below Stockholm average. High proportion of newly arrived pupils. Government priority area with extra resources. Fastest absolute improvement (+8.4 pts) from low base. Contradiction: official optimism vs observed classroom reality.'
|
||
}
|
||
};
|
||
|
||
/* HEALTH & SOCIAL DATA */
|
||
// Sources: Socialstyrelsen, Väntetider i vård, Folkihälsomyndigheten, Region Stockholm 2024
|
||
// wait_gp_days: median days to get GP appointment
|
||
// wait_specialist_days: median days to specialist
|
||
// wait_surgery_days: median days to elective surgery
|
||
// drug_deaths_per100k: drug-related deaths per 100k inhabitants
|
||
// mental_health_pct: share seeking mental health care (%)
|
||
// life_expectancy: estimated local life expectancy
|
||
// obesity_pct: share with obesity (%)
|
||
// sick_leave_pct: share on long-term sick leave (%)
|
||
// elder_care_satisfaction: satisfaction score 1-10
|
||
const HEALTH_DATA={
|
||
city:{
|
||
wait_gp_days:4, wait_specialist_days:38, wait_surgery_days:62,
|
||
drug_deaths_per100k:8.2, mental_health_pct:14.1, life_expectancy:83.4,
|
||
obesity_pct:11.2, sick_leave_pct:3.8, elder_care_satisfaction:7.8,
|
||
nearest_hospital:'Karolinska (4 min)', nearest_er:'S:t Göran (6 min)',
|
||
pharmacies:12, health_centers:8,
|
||
context:'Central location with excellent healthcare access. Below-average wait times due to high density of private clinics.'
|
||
},
|
||
ostermalm:{
|
||
wait_gp_days:3, wait_specialist_days:28, wait_surgery_days:48,
|
||
drug_deaths_per100k:5.1, mental_health_pct:12.8, life_expectancy:85.2,
|
||
obesity_pct:7.4, sick_leave_pct:2.9, elder_care_satisfaction:8.6,
|
||
nearest_hospital:'Danderyds sjukhus (8 min)', nearest_er:'Karolinska (12 min)',
|
||
pharmacies:18, health_centers:11,
|
||
context:'Highest life expectancy in Stockholm. Lowest drug deaths. High density of private clinics and specialists. Premium healthcare access.'
|
||
},
|
||
sodermalm:{
|
||
wait_gp_days:6, wait_specialist_days:44, wait_surgery_days:71,
|
||
drug_deaths_per100k:14.8, mental_health_pct:18.4, life_expectancy:81.9,
|
||
obesity_pct:13.6, sick_leave_pct:4.9, elder_care_satisfaction:7.2,
|
||
nearest_hospital:'Södersjukhuset (5 min)', nearest_er:'Södersjukhuset (5 min)',
|
||
pharmacies:21, health_centers:14,
|
||
context:'Highest drug mortality in inner city. Elevated mental health demand. Well-served by Södersjukhuset. Young demographic drives high mental health service use.'
|
||
},
|
||
kungsholmen:{
|
||
wait_gp_days:5, wait_specialist_days:39, wait_surgery_days:64,
|
||
drug_deaths_per100k:7.4, mental_health_pct:13.2, life_expectancy:83.8,
|
||
obesity_pct:10.8, sick_leave_pct:3.6, elder_care_satisfaction:7.9,
|
||
nearest_hospital:'Karolinska (10 min)', nearest_er:'S:t Göran (4 min)',
|
||
pharmacies:9, health_centers:6,
|
||
context:'Good healthcare access. S:t Göran ER nearby. Average wait times for Stockholm inner city.'
|
||
},
|
||
vasastan:{
|
||
wait_gp_days:5, wait_specialist_days:37, wait_surgery_days:61,
|
||
drug_deaths_per100k:7.8, mental_health_pct:13.9, life_expectancy:83.6,
|
||
obesity_pct:10.4, sick_leave_pct:3.7, elder_care_satisfaction:8.1,
|
||
nearest_hospital:'Karolinska (8 min)', nearest_er:'Karolinska (8 min)',
|
||
pharmacies:11, health_centers:7,
|
||
context:'Good access. Karolinska University Hospital nearby. High education level correlates with better health outcomes.'
|
||
},
|
||
nacka:{
|
||
wait_gp_days:8, wait_specialist_days:52, wait_surgery_days:84,
|
||
drug_deaths_per100k:6.2, mental_health_pct:11.4, life_expectancy:83.1,
|
||
obesity_pct:12.1, sick_leave_pct:4.1, elder_care_satisfaction:7.6,
|
||
nearest_hospital:'Nacka Nyckel / Södersjukhuset (18 min)', nearest_er:'Södersjukhuset (18 min)',
|
||
pharmacies:14, health_centers:9,
|
||
context:'Longer wait times than inner city. No major hospital nearby. Primarily suburban family health needs. Good elder care satisfaction.'
|
||
},
|
||
rinkeby:{
|
||
wait_gp_days:11, wait_specialist_days:68, wait_surgery_days:112,
|
||
drug_deaths_per100k:18.4, mental_health_pct:22.1, life_expectancy:79.2,
|
||
obesity_pct:22.8, sick_leave_pct:8.4, elder_care_satisfaction:6.1,
|
||
nearest_hospital:'Karolinska (22 min)', nearest_er:'Karolinska (22 min)',
|
||
pharmacies:4, health_centers:3,
|
||
context:'CONTRADICTION: official data underreports health burden. Highest drug mortality, longest wait times, lowest life expectancy in Stockholm. Critically underserved. Mental health demand 58% above city average.'
|
||
}
|
||
};
|
||
|
||
function getHealthPopup(distId){
|
||
const h=HEALTH_DATA[distId];
|
||
if(!h)return '';
|
||
const leColor=h.life_expectancy>=84?'#4ade80':h.life_expectancy>=82?'#fbbf24':'#f87171';
|
||
const drugColor=h.drug_deaths_per100k<=6?'#4ade80':h.drug_deaths_per100k<=10?'#fbbf24':h.drug_deaths_per100k<=15?'#f97316':'#f87171';
|
||
const waitColor=(d)=>d<=5?'#4ade80':d<=7?'#fbbf24':d<=10?'#f97316':'#f87171';
|
||
const isContra=distId==='rinkeby'||distId==='sodermalm';
|
||
|
||
return `<div>
|
||
<div>HEALTH & SOCIAL INDICATORS</div>
|
||
|
||
<!-- Key stats -->
|
||
<div>
|
||
<div>
|
||
<div>Life exp.</div>
|
||
<div>${h.life_expectancy}</div>
|
||
<div>years</div>
|
||
</div>
|
||
<div>
|
||
<div>Drug deaths</div>
|
||
<div>${h.drug_deaths_per100k}</div>
|
||
<div>/100k</div>
|
||
</div>
|
||
<div>
|
||
<div>Mental health</div>
|
||
<div>${h.mental_health_pct}%</div>
|
||
<div>seeking care</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Wait times -->
|
||
<div>
|
||
<div>Healthcare wait times</div>
|
||
<div>
|
||
<div>
|
||
<span>GP appointment</span>
|
||
<span>${h.wait_gp_days} days</span>
|
||
</div>
|
||
<div>
|
||
<span>Specialist referral</span>
|
||
<span>${h.wait_specialist_days} days</span>
|
||
</div>
|
||
<div>
|
||
<span>Elective surgery</span>
|
||
<span>${h.wait_surgery_days} days</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Additional stats -->
|
||
<div>
|
||
<div>
|
||
<div>Obesity rate</div>
|
||
<div>${h.obesity_pct}%</div>
|
||
</div>
|
||
<div>
|
||
<div>Long-term sick leave</div>
|
||
<div>${h.sick_leave_pct}%</div>
|
||
</div>
|
||
<div>
|
||
<div>Nearest hospital</div>
|
||
<div>${h.nearest_hospital}</div>
|
||
</div>
|
||
<div>
|
||
<div>Elder care score</div>
|
||
<div>${h.elder_care_satisfaction}/10</div>
|
||
</div>
|
||
</div>
|
||
|
||
${isContra?`<div>
|
||
<span>Contradiction: </span>${h.context}
|
||
</div>`:''}
|
||
|
||
<div>${isContra?'':h.context}</div>
|
||
${sourceChain([
|
||
{type:'data',label:'Socialstyrelsen 2024'},
|
||
{type:'data',label:'Väntetider i vård 2024'},
|
||
{type:'data',label:'Folkhälsomyndigheten 2024'},
|
||
{type:'model',label:'Landvex Health Index v1.0'}
|
||
])}
|
||
</div>`;
|
||
}
|
||
|
||
function getSchoolPopup(distId){
|
||
const sc=SCHOOL_DATA[distId];
|
||
if(!sc)return '';
|
||
const meritColor=sc.avg_merit_gr9>=260?'#4ade80':sc.avg_merit_gr9>=230?'#fbbf24':sc.avg_merit_gr9>=200?'#f97316':'#f87171';
|
||
const qualColor=sc.pct_qualified_gr9>=95?'#4ade80':sc.pct_qualified_gr9>=88?'#fbbf24':'#f87171';
|
||
const trendColor=sc.grade_trend.startsWith('+')?'#4ade80':'#f87171';
|
||
const natAvg=224;
|
||
const vsNat=sc.avg_merit_gr9-natAvg;
|
||
const vsNatStr=(vsNat>=0?'+':'')+vsNat+' vs national avg';
|
||
const vsNatColor=vsNat>=0?'#4ade80':'#f87171';
|
||
|
||
return `<div>
|
||
<div>SCHOOL PERFORMANCE · SKOLVERKET 2024</div>
|
||
|
||
<!-- Merit score header -->
|
||
<div>
|
||
<div>
|
||
<div>
|
||
<div>Avg merit grade 9</div>
|
||
<div>Max 340 · National avg 224</div>
|
||
</div>
|
||
<div>
|
||
<div>${sc.avg_merit_gr9}</div>
|
||
<div>${vsNatStr}</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div>
|
||
<div>Qualified</div>
|
||
<div>${sc.pct_qualified_gr9}%</div>
|
||
<div>to gymnasium</div>
|
||
</div>
|
||
<div>
|
||
<div>Pass all</div>
|
||
<div>${sc.pct_passed_all}%</div>
|
||
<div>subjects</div>
|
||
</div>
|
||
<div>
|
||
<div>Trend</div>
|
||
<div>${sc.grade_trend.split(' ')[0]}</div>
|
||
<div>vs 2021</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<span>${sc.school_count} schools · ${sc.teacher_ratio} pupils/teacher</span>
|
||
<span>${sc.inspection_status}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Top schools -->
|
||
<div>Top schools</div>
|
||
${sc.top_schools.map(s=>`
|
||
<div>
|
||
<div>
|
||
<div>${s.name}</div>
|
||
<div>${s.type} · ${s.note}</div>
|
||
</div>
|
||
<div>${s.merit}</div>
|
||
</div>`).join('')}
|
||
|
||
<div>${sc.context}</div>
|
||
${sourceChain([
|
||
{type:'data',label:'Skolverket SIRIS 2023/2024'},
|
||
{type:'data',label:'Skolinspektionen 2024'},
|
||
{type:'model',label:'Landvex School Index v1.0'}
|
||
])}
|
||
</div>`;
|
||
}
|
||
|
||
/* HOUSING STOCK DATA */
|
||
// Sources: SCB Fastighetsregistret, Boverket, Stockholm stad 2024
|
||
// build_era: dominant construction period
|
||
// median_build_year: median year of existing stock
|
||
// renovated_pct: share of stock renovated post-2000 (%)
|
||
// brf_pct: share bostadsrätt (%)
|
||
// hyres_pct: share hyresrätt (%)
|
||
// condition_idx: Building Condition Index 1-10 (Landvex quiXzoom assessment)
|
||
// condition_label: observed condition summary
|
||
// avg_apt_size: average apartment size sqm
|
||
// vacancy_commercial_pct: commercial vacancy rate (%)
|
||
// new_construction: units under construction / planned
|
||
const HOUSING_DATA={
|
||
city:{
|
||
build_era:'1880–1940 + 1990–2010',
|
||
median_build_year:1935,
|
||
renovated_pct:58,
|
||
brf_pct:61, hyres_pct:28, other_pct:11,
|
||
condition_idx:8.4,
|
||
condition_label:'Well-maintained. Heritage facades regularly restored. Some older stock needs windows/plumbing.',
|
||
avg_apt_size:62,
|
||
vacancy_commercial_pct:4.2,
|
||
new_construction:'240 units planned 2026–2028',
|
||
bci_trend:'+0.3 vs 2023'
|
||
},
|
||
ostermalm:{
|
||
build_era:'1890–1930 (Art Nouveau / Jugend)',
|
||
median_build_year:1922,
|
||
renovated_pct:72,
|
||
brf_pct:74, hyres_pct:18, other_pct:8,
|
||
condition_idx:9.1,
|
||
condition_label:'Excellent. Premium maintenance. Freshly renovated facades predominant. Low vacancy, high investment.',
|
||
avg_apt_size:88,
|
||
vacancy_commercial_pct:2.1,
|
||
new_construction:'Limited — heritage restrictions',
|
||
bci_trend:'+0.1 vs 2023'
|
||
},
|
||
sodermalm:{
|
||
build_era:'1900–1940 + 1960–1980',
|
||
median_build_year:1944,
|
||
renovated_pct:48,
|
||
brf_pct:52, hyres_pct:38, other_pct:10,
|
||
condition_idx:7.2,
|
||
condition_label:'Mixed. 1900-era stock mostly good. 1960s blocks show wear. SoFo gentrification visible. Active renovation wave.',
|
||
avg_apt_size:58,
|
||
vacancy_commercial_pct:6.8,
|
||
new_construction:'580 units 2025–2028 (Slussen area)',
|
||
bci_trend:'+0.8 vs 2023 — fastest improving'
|
||
},
|
||
kungsholmen:{
|
||
build_era:'1910–1950',
|
||
median_build_year:1938,
|
||
renovated_pct:54,
|
||
brf_pct:58, hyres_pct:32, other_pct:10,
|
||
condition_idx:7.8,
|
||
condition_label:'Generally good. Solid early 20th century construction. Waterfront properties premium. Some 1940s stock needs façade work.',
|
||
avg_apt_size:64,
|
||
vacancy_commercial_pct:5.1,
|
||
new_construction:'180 units planned',
|
||
bci_trend:'+0.4 vs 2023'
|
||
},
|
||
vasastan:{
|
||
build_era:'1900–1930 (classic Stockholm)',
|
||
median_build_year:1926,
|
||
renovated_pct:61,
|
||
brf_pct:64, hyres_pct:28, other_pct:8,
|
||
condition_idx:8.1,
|
||
condition_label:'Good to excellent. Classic Stockholm architecture well-preserved. Active renovation market. Rising BCI trend.',
|
||
avg_apt_size:67,
|
||
vacancy_commercial_pct:4.4,
|
||
new_construction:'90 units planned',
|
||
bci_trend:'+0.5 vs 2023'
|
||
},
|
||
nacka:{
|
||
build_era:'1970–1990 + 2010–2026',
|
||
median_build_year:1988,
|
||
renovated_pct:38,
|
||
brf_pct:48, hyres_pct:28, other_pct:24,
|
||
condition_idx:7.4,
|
||
condition_label:'Mixed. 1970s-80s suburban stock shows age. New 2010+ developments in excellent condition. Major new construction ongoing.',
|
||
avg_apt_size:74,
|
||
vacancy_commercial_pct:7.2,
|
||
new_construction:'2 400 units under construction (tunnel area)',
|
||
bci_trend:'+1.2 vs 2023 — highest absolute gain'
|
||
},
|
||
rinkeby:{
|
||
build_era:'1965–1975 (miljonprogrammet)',
|
||
median_build_year:1971,
|
||
renovated_pct:22,
|
||
brf_pct:8, hyres_pct:82, other_pct:10,
|
||
condition_idx:4.8,
|
||
condition_label:'Below average. Miljonprogrammet stock dominates. Visible facade deterioration, graffiti, broken lighting. Limited private investment. Municipally owned majority.',
|
||
avg_apt_size:69,
|
||
vacancy_commercial_pct:18.4,
|
||
new_construction:'340 units social housing planned',
|
||
bci_trend:'-0.2 vs 2023'
|
||
}
|
||
};
|
||
|
||
function getHousingPopup(distId){
|
||
const h=HOUSING_DATA[distId];
|
||
if(!h)return '';
|
||
const bciColor=h.condition_idx>=8.5?'#4ade80':h.condition_idx>=7?'#fbbf24':h.condition_idx>=5?'#f97316':'#f87171';
|
||
const trendColor=h.bci_trend.startsWith('+')?'#4ade80':h.bci_trend.startsWith('-')?'#f87171':'#fbbf24';
|
||
const segBar=(pct,col)=>`<div></div>`;
|
||
return `<div>
|
||
<div>HOUSING STOCK INTELLIGENCE</div>
|
||
|
||
<!-- BCI header -->
|
||
<div>
|
||
<div>
|
||
<div>
|
||
<div>Building Condition Index</div>
|
||
<div>Landvex · quiXzoom observed</div>
|
||
</div>
|
||
<div>
|
||
<div>${h.condition_idx}<span>/10</span></div>
|
||
<div>${h.bci_trend}</div>
|
||
</div>
|
||
</div>
|
||
<div>${h.condition_label}</div>
|
||
</div>
|
||
|
||
<!-- Build era & renovation -->
|
||
<div>
|
||
<div>
|
||
<div>Build era</div>
|
||
<div>${h.build_era}</div>
|
||
</div>
|
||
<div>
|
||
<div>Renovated post-2000</div>
|
||
<div>${h.renovated_pct}% of stock</div>
|
||
</div>
|
||
<div>
|
||
<div>Avg apartment</div>
|
||
<div>${h.avg_apt_size} sqm</div>
|
||
</div>
|
||
<div>
|
||
<div>Commercial vacancy</div>
|
||
<div>${h.vacancy_commercial_pct}%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tenure mix -->
|
||
<div>
|
||
<div>Tenure mix</div>
|
||
<div>
|
||
${segBar(h.brf_pct,'#4ade80')}
|
||
${segBar(h.hyres_pct,'#38bdf8')}
|
||
${segBar(h.other_pct,'#6b7280')}
|
||
</div>
|
||
<div>
|
||
<span>Bostadsrätt ${h.brf_pct}%</span>
|
||
<span>Hyresrätt ${h.hyres_pct}%</span>
|
||
<span>Other ${h.other_pct}%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- New construction -->
|
||
<div>
|
||
<div>New build</div>
|
||
<div>${h.new_construction}</div>
|
||
</div>
|
||
${sourceChain([
|
||
{type:'data',label:'SCB Fastighetsregistret 2024'},
|
||
{type:'data',label:'Boverket 2024'},
|
||
{type:'obs',label:'quiXzoom BCI · 847–1247 observationer'},
|
||
{type:'model',label:'Landvex BCI v1.2'}
|
||
])}
|
||
</div>`;
|
||
}
|
||
|
||
/* COMMERCIAL VITALITY INDEX */
|
||
// Sources: SCB NV0101 (företag per SNI), Bolagsverket nyregistrerade/konkurser,
|
||
// quiXzoom kommersiell aktivitet, Fastighetsregistret vakanser
|
||
// SNI-mix: dominanta branscher per stadsdel
|
||
// cvi_score: Commercial Vitality Index 0-100
|
||
// active_companies: estimated active businesses
|
||
// new_registrations_ytd: new companies registered Jan-Jun 2026
|
||
// bankruptcies_ytd: bankruptcies Jan-Jun 2026
|
||
// revenue_trend: estimated revenue trend YoY (%)
|
||
// sni_mix: top 3 sectors by company count
|
||
// vacancy_commercial: commercial vacancy rate (%)
|
||
// gentrification_signal: boolean flag
|
||
const CVI_DATA={
|
||
city:{
|
||
cvi_score:84, cvi_trend:'+2.1',
|
||
active_companies:4820, new_registrations_ytd:312, bankruptcies_ytd:28,
|
||
revenue_trend:+4.8, employees_est:38200,
|
||
sni_mix:[
|
||
{code:'K',name:'Financial services',pct:28},
|
||
{code:'M',name:'Professional/tech',pct:24},
|
||
{code:'I',name:'Hotels & restaurants',pct:18}
|
||
],
|
||
vacancy_commercial:4.2, gentrification_signal:false,
|
||
bolagsverket_confidence:88,
|
||
top_growth_sector:'Fintech/proptech startups +34% YoY'
|
||
},
|
||
ostermalm:{
|
||
cvi_score:91, cvi_trend:'+1.4',
|
||
active_companies:3640, new_registrations_ytd:198, bankruptcies_ytd:12,
|
||
revenue_trend:+6.2, employees_est:29400,
|
||
sni_mix:[
|
||
{code:'K',name:'Finance & insurance',pct:32},
|
||
{code:'G',name:'Retail trade',pct:26},
|
||
{code:'M',name:'Management consulting',pct:22}
|
||
],
|
||
vacancy_commercial:2.1, gentrification_signal:false,
|
||
bolagsverket_confidence:94,
|
||
top_growth_sector:'Luxury retail +18% YoY'
|
||
},
|
||
sodermalm:{
|
||
cvi_score:72, cvi_trend:'+4.8',
|
||
active_companies:8940, new_registrations_ytd:687, bankruptcies_ytd:94,
|
||
revenue_trend:+3.1, employees_est:54800,
|
||
sni_mix:[
|
||
{code:'I',name:'Restaurants & bars',pct:29},
|
||
{code:'R',name:'Arts & entertainment',pct:22},
|
||
{code:'J',name:'Media & tech',pct:19}
|
||
],
|
||
vacancy_commercial:6.8, gentrification_signal:true,
|
||
bolagsverket_confidence:82,
|
||
top_growth_sector:'Food & beverage concepts +28% YoY'
|
||
},
|
||
kungsholmen:{
|
||
cvi_score:76, cvi_trend:'+2.6',
|
||
active_companies:3180, new_registrations_ytd:221, bankruptcies_ytd:31,
|
||
revenue_trend:+3.8, employees_est:31200,
|
||
sni_mix:[
|
||
{code:'O',name:'Public administration',pct:34},
|
||
{code:'M',name:'Professional services',pct:28},
|
||
{code:'J',name:'IT & telecom',pct:18}
|
||
],
|
||
vacancy_commercial:5.1, gentrification_signal:false,
|
||
bolagsverket_confidence:86,
|
||
top_growth_sector:'Tech consultancies +22% YoY'
|
||
},
|
||
vasastan:{
|
||
cvi_score:79, cvi_trend:'+5.2',
|
||
active_companies:4210, new_registrations_ytd:398, bankruptcies_ytd:42,
|
||
revenue_trend:+5.4, employees_est:28900,
|
||
sni_mix:[
|
||
{code:'M',name:'Professional services',pct:31},
|
||
{code:'J',name:'IT & media',pct:24},
|
||
{code:'I',name:'Restaurants & cafes',pct:21}
|
||
],
|
||
vacancy_commercial:4.4, gentrification_signal:true,
|
||
bolagsverket_confidence:84,
|
||
top_growth_sector:'Creative agencies +31% YoY'
|
||
},
|
||
nacka:{
|
||
cvi_score:68, cvi_trend:'+8.4',
|
||
active_companies:6840, new_registrations_ytd:612, bankruptcies_ytd:68,
|
||
revenue_trend:+7.2, employees_est:42100,
|
||
sni_mix:[
|
||
{code:'F',name:'Construction',pct:28},
|
||
{code:'G',name:'Retail & logistics',pct:24},
|
||
{code:'M',name:'Professional services',pct:18}
|
||
],
|
||
vacancy_commercial:7.2, gentrification_signal:false,
|
||
bolagsverket_confidence:79,
|
||
top_growth_sector:'Construction & proptech +48% YoY'
|
||
},
|
||
rinkeby:{
|
||
cvi_score:28, cvi_trend:'+1.2',
|
||
active_companies:1240, new_registrations_ytd:88, bankruptcies_ytd:62,
|
||
revenue_trend:-2.4, employees_est:6800,
|
||
sni_mix:[
|
||
{code:'G',name:'Local retail',pct:42},
|
||
{code:'I',name:'Ethnic restaurants',pct:28},
|
||
{code:'S',name:'Other services',pct:18}
|
||
],
|
||
vacancy_commercial:18.4, gentrification_signal:false,
|
||
bolagsverket_confidence:61,
|
||
top_growth_sector:'No significant growth sector identified'
|
||
}
|
||
};
|
||
|
||
/* ECONOMIC DNA */
|
||
// Aggregated area-level data only. No individual data.
|
||
// Sources: SCB disp.inkomst, Skatteverket regional, Fastighetsregistret, Bolagsverket
|
||
// purchasing_power_idx: area purchasing power vs Stockholm avg (100)
|
||
// hh_disposable_median: median household disposable income SEK/year
|
||
// wealth_gap: ratio top/bottom income quartile
|
||
// property_value_idx: avg property value vs Stockholm avg
|
||
// commercial_vs_residential: commercial activity relative to residential wealth
|
||
// gap_opportunity: commercial underprovision relative to purchasing power
|
||
// economic_profile: dominant economic character
|
||
// early_warning: boolean - commercial strong but residential weakening
|
||
const EDNA={
|
||
city:{
|
||
purchasing_power_idx:118, hh_disposable_median:412000,
|
||
wealth_gap:3.2, property_value_idx:124,
|
||
commercial_vs_residential:1.18,
|
||
gap_opportunity:false, early_warning:false,
|
||
profile:'Financial & professional hub',
|
||
profile_tags:['Finance','Premium retail','Tourism','Law & consulting'],
|
||
key_insight:'Strongest commercial-residential alignment in Stockholm. Limited retail gap opportunities — market well-served.',
|
||
source_note:'SCB disp.inkomst 2024 · Skatteverket regional 2024'
|
||
},
|
||
ostermalm:{
|
||
purchasing_power_idx:168, hh_disposable_median:589000,
|
||
wealth_gap:4.8, property_value_idx:198,
|
||
commercial_vs_residential:0.84,
|
||
gap_opportunity:true, early_warning:false,
|
||
profile:'Premium residential with commercial underserving',
|
||
profile_tags:['HNWI concentration','Luxury services','Private banking','Premium F&B'],
|
||
key_insight:'High purchasing power relative to commercial supply. Gap opportunity: premium grocery, high-end hospitality, private health clinics underrepresented.',
|
||
source_note:'SCB disp.inkomst 2024 · Fastighetsregistret 2024'
|
||
},
|
||
sodermalm:{
|
||
purchasing_power_idx:104, hh_disposable_median:348000,
|
||
wealth_gap:2.8, property_value_idx:108,
|
||
commercial_vs_residential:1.42,
|
||
gap_opportunity:false, early_warning:false,
|
||
profile:'Creative economy — commercial overindexing',
|
||
profile_tags:['Creative industries','F&B density','Media & tech','Gentrification active'],
|
||
key_insight:'Commercial activity significantly overindexes vs residential income. Sustained by young professional inflow and tourism. Gentrification raising both metrics.',
|
||
source_note:'SCB 2024 · quiXzoom commercial activity score'
|
||
},
|
||
kungsholmen:{
|
||
purchasing_power_idx:112, hh_disposable_median:384000,
|
||
wealth_gap:2.6, property_value_idx:114,
|
||
commercial_vs_residential:0.94,
|
||
gap_opportunity:true, early_warning:false,
|
||
profile:'Family residential with service gap',
|
||
profile_tags:['Government/public sector','Families','Stable income','Low churn'],
|
||
key_insight:'Stable middle-income area. Slight commercial undersupply relative to purchasing power. Opportunity: family services, quality casual dining, health & wellness.',
|
||
source_note:'SCB disp.inkomst 2024 · Bolagsverket 2024'
|
||
},
|
||
vasastan:{
|
||
purchasing_power_idx:122, hh_disposable_median:418000,
|
||
wealth_gap:2.9, property_value_idx:118,
|
||
commercial_vs_residential:1.12,
|
||
gap_opportunity:false, early_warning:false,
|
||
profile:'Young professional density — rising economic DNA',
|
||
profile_tags:['Young professionals','Creative/tech','Rising incomes','Family formation'],
|
||
key_insight:'Fastest-growing economic DNA in inner city. Income and commercial activity both accelerating. Gentrification signal confirmed. Watch for displacement dynamics.',
|
||
source_note:'SCB 2024 · Bolagsverket · Fastighetsregistret'
|
||
},
|
||
nacka:{
|
||
purchasing_power_idx:108, hh_disposable_median:372000,
|
||
wealth_gap:2.4, property_value_idx:82,
|
||
commercial_vs_residential:0.78,
|
||
gap_opportunity:true, early_warning:false,
|
||
profile:'Suburban growth — large commercial gap opening',
|
||
profile_tags:['Construction boom','Young families','Infrastructure investment','Commercial lag'],
|
||
key_insight:'Residential growth outpacing commercial development. Largest gap opportunity in Stockholm region. Tunnel project will accelerate population 2028+. First-mover advantage window open now.',
|
||
source_note:'SCB 2024 · Nacka kommun · Trafikverket'
|
||
},
|
||
rinkeby:{
|
||
purchasing_power_idx:38, hh_disposable_median:142000,
|
||
wealth_gap:2.1, property_value_idx:24,
|
||
commercial_vs_residential:0.62,
|
||
gap_opportunity:false, early_warning:true,
|
||
profile:'Structurally distressed — economic isolation',
|
||
profile_tags:['Low purchasing power','High unemployment','Social services dependent','Limited commercial'],
|
||
key_insight:'EARLY WARNING: purchasing power declining vs 2022 baseline. Commercial activity contracting. Wealth gap low — uniform low income. Not a gap opportunity — structural intervention required.',
|
||
source_note:'SCB disp.inkomst 2024 · CONTRADICTION: official data lags ~18 months'
|
||
}
|
||
};
|
||
|
||
function getEconomicDNA(distId){
|
||
const e=EDNA[distId];
|
||
if(!e)return '';
|
||
const ppColor=e.purchasing_power_idx>=130?'#4ade80':e.purchasing_power_idx>=90?'#fbbf24':e.purchasing_power_idx>=60?'#f97316':'#f87171';
|
||
const crColor=e.commercial_vs_residential>=1.2?'#38bdf8':e.commercial_vs_residential>=0.9?'#4ade80':e.commercial_vs_residential>=0.7?'#fbbf24':'#f87171';
|
||
const inc=e.hh_disposable_median.toLocaleString('sv-SE');
|
||
|
||
return `<div>
|
||
<div>ECONOMIC DNA</div>
|
||
|
||
<!-- Profile -->
|
||
<div>
|
||
<div>${e.profile}</div>
|
||
<div>
|
||
${e.profile_tags.map(t=>`<span>${t}</span>`).join('')}
|
||
</div>
|
||
<div>
|
||
<div>
|
||
<div>Purchasing power index</div>
|
||
<div>${e.purchasing_power_idx}<span> (avg=100)</span></div>
|
||
</div>
|
||
<div>
|
||
<div>HH disposable income</div>
|
||
<div>${inc} kr</div>
|
||
</div>
|
||
<div>
|
||
<div>Commercial/residential</div>
|
||
<div>${e.commercial_vs_residential}x</div>
|
||
</div>
|
||
<div>
|
||
<div>Property value index</div>
|
||
<div>${e.property_value_idx}<span> (avg=100)</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Gap / Warning -->
|
||
${e.early_warning?`<div>
|
||
<div>Early Warning Signal</div>
|
||
<div>${e.key_insight}</div>
|
||
</div>`:e.gap_opportunity?`<div>
|
||
<div>Gap Opportunity Detected</div>
|
||
<div>${e.key_insight}</div>
|
||
</div>`:`<div>
|
||
<div>${e.key_insight}</div>
|
||
</div>`}
|
||
|
||
<div>Aggregated area data only · No individual data · ${e.source_note}</div>
|
||
</div>`;
|
||
}
|
||
|
||
function getCVIPopup(distId){
|
||
const c=CVI_DATA[distId];
|
||
if(!c)return '';
|
||
const cviColor=c.cvi_score>=80?'#4ade80':c.cvi_score>=60?'#fbbf24':c.cvi_score>=40?'#f97316':'#f87171';
|
||
const trendColor=c.cvi_trend.startsWith('+')?'#4ade80':'#f87171';
|
||
const revColor=c.revenue_trend>=0?'#4ade80':'#f87171';
|
||
const revSign=c.revenue_trend>=0?'+':'';
|
||
const bkRatio=Math.round((c.bankruptcies_ytd/c.new_registrations_ytd)*100);
|
||
const bkColor=bkRatio<=15?'#4ade80':bkRatio<=25?'#fbbf24':'#f87171';
|
||
|
||
return `<div>
|
||
<div>COMMERCIAL VITALITY INDEX</div>
|
||
|
||
<!-- CVI header -->
|
||
<div>
|
||
<div>
|
||
<div>
|
||
<div>CVI Score</div>
|
||
<div>Bolagsverket 35% · Fastighet 20% · quiXzoom 15%</div>
|
||
</div>
|
||
<div>
|
||
<div>${c.cvi_score}<span>/100</span></div>
|
||
<div>${c.cvi_trend} trend</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div>
|
||
<div>Active co.</div>
|
||
<div>${c.active_companies.toLocaleString('sv-SE')}</div>
|
||
</div>
|
||
<div>
|
||
<div>New YTD</div>
|
||
<div>+${c.new_registrations_ytd}</div>
|
||
</div>
|
||
<div>
|
||
<div>Bankrupt</div>
|
||
<div>${c.bankruptcies_ytd}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SNI sector mix -->
|
||
<div>
|
||
<div>Sector mix (SNI)</div>
|
||
${c.sni_mix.map(s=>`<div>
|
||
<span>${s.code}</span>
|
||
<span>${s.name}</span>
|
||
<div>
|
||
<div></div>
|
||
</div>
|
||
<span>${s.pct}%</span>
|
||
</div>`).join('')}
|
||
</div>
|
||
|
||
<!-- Revenue & employment -->
|
||
<div>
|
||
<div>
|
||
<div>Revenue trend YoY</div>
|
||
<div>${revSign}${c.revenue_trend}%</div>
|
||
</div>
|
||
<div>
|
||
<div>Est. employees</div>
|
||
<div>${c.employees_est.toLocaleString('sv-SE')}</div>
|
||
</div>
|
||
<div>
|
||
<div>Commercial vacancy</div>
|
||
<div>${c.vacancy_commercial}%</div>
|
||
</div>
|
||
<div>
|
||
<div>Gentrif. signal</div>
|
||
<div>${c.gentrification_signal?'DETECTED':'None'}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Growth sector -->
|
||
<div>
|
||
<div>Top growth sector</div>
|
||
<div>${c.top_growth_sector}</div>
|
||
</div>
|
||
|
||
<!-- Drill-down chain -->
|
||
<div>
|
||
CVI → Omsättningstillväxt → Företagspopulation → Bolagsdata<br>
|
||
Sources: Bolagsverket · SCB NV0101 · quiXzoom · Fastighetsregistret
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function getPricePopup(distId){
|
||
const p=PRICE_DATA[distId];
|
||
if(!p)return '';
|
||
const pb=(val,max,col)=>{
|
||
const pct=Math.min(100,Math.round((val/max)*100));
|
||
return `<div><div></div></div>`;
|
||
};
|
||
const idxColor=idx=>idx>=85?'#f87171':idx>=60?'#fbbf24':'#4ade80';
|
||
const beerIdx=Math.round((p.beer_bar/129)*100);
|
||
const hotelIdx=Math.round((p.hotel_night/2400)*100);
|
||
const sevenIdx=p.seven_eleven;
|
||
const housingIdx=p.housing_idx;
|
||
|
||
return `<div>
|
||
<div>PRICE INTELLIGENCE <span>Östermalm=100</span></div>
|
||
|
||
<!-- Index summary row -->
|
||
<div>
|
||
<div>
|
||
<div>Beer</div>
|
||
<div>${beerIdx}</div>
|
||
</div>
|
||
<div>
|
||
<div>Hotel</div>
|
||
<div>${hotelIdx}</div>
|
||
</div>
|
||
<div>
|
||
<div>7-Eleven</div>
|
||
<div>${sevenIdx}</div>
|
||
</div>
|
||
<div>
|
||
<div>Housing</div>
|
||
<div>${housingIdx}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Food & drink detail -->
|
||
<div>
|
||
<div>Food & Drink</div>
|
||
<div>
|
||
<div><span>Bar / club</span>${pb(p.beer_bar,140,'#60a5fa')}<span>${p.beer_bar} kr</span></div>
|
||
<div><span>Local pub</span>${pb(p.beer_pub,100,'#60a5fa')}<span>${p.beer_pub} kr</span></div>
|
||
<div><span>Coffee</span>${pb(p.coffee,70,'#a78bfa')}<span>${p.coffee} kr</span></div>
|
||
<div><span>Lunch</span>${pb(p.lunch,200,'#34d399')}<span>${p.lunch} kr</span></div>
|
||
<div><span>Dinner</span>${pb(p.dinner_mid,700,'#34d399')}<span>${p.dinner_mid} kr</span></div>
|
||
<div><span>7-Eleven basket</span>${pb(p.seven_eleven,100,'#fbbf24')}<span>${p.seven_eleven} kr</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Accommodation -->
|
||
<div>
|
||
<div>Accommodation</div>
|
||
<div>
|
||
<div><span>Hotel / night</span>${pb(p.hotel_night,2500,'#f97316')}<span>${p.hotel_night} kr</span></div>
|
||
<div><span>Rent / sqm</span>${pb(p.apt_rent_sqm,2600,'#fbbf24')}<span>${p.apt_rent_sqm}</span></div>
|
||
<div><span>Buy / sqm</span>${pb(p.apt_buy_sqm,140000,'#f87171')}<span>${Math.round(p.apt_buy_sqm/1000)}k</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
${sourceChain([
|
||
{type:'data',label:'Hemnet Q1 2026'},
|
||
{type:'data',label:'Booking.com Jun 2026'},
|
||
{type:'obs',label:'quiXzoom prisobservationer Jun 2026'},
|
||
{type:'model',label:'Landvex Price Index v1.0'}
|
||
])}
|
||
</div>`;
|
||
}
|
||
|
||
const SCB_DATA={
|
||
// Source: SCB, Stockholms stad stadsdelsnämnder 2024
|
||
// education levels: grundskola / gymnasium / högskola_kort (<3yr) / högskola_lång (3yr+) / forskning
|
||
city: {population:28400, medianIncome:542000, unemployment:3.1, education_pct:62,
|
||
medianAge:38, age_0_17:12, age_18_44:52, age_45_64:23, age_65plus:13,
|
||
edu_grundskola:8, edu_gymnasium:30, edu_hogskola_kort:18, edu_hogskola_lang:40, edu_forskning:4,
|
||
foreign_born_pct:24, students_pct:12, self_employed_pct:8},
|
||
ostermalm: {population:31200, medianIncome:698000, unemployment:2.4, education_pct:74,
|
||
medianAge:46, age_0_17:14, age_18_44:34, age_45_64:28, age_65plus:24,
|
||
edu_grundskola:4, edu_gymnasium:22, edu_hogskola_kort:16, edu_hogskola_lang:52, edu_forskning:6,
|
||
foreign_born_pct:18, students_pct:6, self_employed_pct:14},
|
||
sodermalm: {population:101000,medianIncome:461000, unemployment:4.8, education_pct:58,
|
||
medianAge:36, age_0_17:13, age_18_44:55, age_45_64:21, age_65plus:11,
|
||
edu_grundskola:11, edu_gymnasium:31, edu_hogskola_kort:20, edu_hogskola_lang:34, edu_forskning:4,
|
||
foreign_born_pct:22, students_pct:18, self_employed_pct:11},
|
||
kungsholmen:{population:38900, medianIncome:502000, unemployment:3.6, education_pct:64,
|
||
medianAge:40, age_0_17:15, age_18_44:46, age_45_64:24, age_65plus:15,
|
||
edu_grundskola:7, edu_gymnasium:29, edu_hogskola_kort:19, edu_hogskola_lang:41, edu_forskning:4,
|
||
foreign_born_pct:20, students_pct:10, self_employed_pct:9},
|
||
vasastan: {population:41200, medianIncome:521000, unemployment:3.2, education_pct:67,
|
||
medianAge:39, age_0_17:16, age_18_44:46, age_45_64:25, age_65plus:13,
|
||
edu_grundskola:6, edu_gymnasium:27, edu_hogskola_kort:18, edu_hogskola_lang:44, edu_forskning:5,
|
||
foreign_born_pct:19, students_pct:14, self_employed_pct:10},
|
||
nacka: {population:102000,medianIncome:512000, unemployment:3.8, education_pct:55,
|
||
medianAge:41, age_0_17:22, age_18_44:38, age_45_64:26, age_65plus:14,
|
||
edu_grundskola:10, edu_gymnasium:35, edu_hogskola_kort:20, edu_hogskola_lang:32, edu_forskning:3,
|
||
foreign_born_pct:16, students_pct:7, self_employed_pct:9},
|
||
rinkeby: {population:49000, medianIncome:198000, unemployment:21.4,education_pct:28,
|
||
medianAge:31, age_0_17:31, age_18_44:44, age_45_64:17, age_65plus:8,
|
||
edu_grundskola:38, edu_gymnasium:34, edu_hogskola_kort:14, edu_hogskola_lang:13, edu_forskning:1,
|
||
foreign_born_pct:84, students_pct:8, self_employed_pct:4}
|
||
};
|
||
|
||
function ageBar(pct, color){
|
||
return `<div>
|
||
<div></div></div>`;
|
||
}
|
||
|
||
function getSCBPopup(distId){
|
||
const s=SCB_DATA[distId];
|
||
if(!s)return '';
|
||
const inc=s.medianIncome.toLocaleString('sv-SE');
|
||
const uColor=s.unemployment>15?'#f87171':s.unemployment>8?'#fbbf24':'#4ade80';
|
||
const ageColor=s.medianAge<33?'#c084fc':s.medianAge<40?'#38bdf8':s.medianAge<48?'#4ade80':'#fbbf24';
|
||
return `<div>
|
||
<div>SCB DEMOGRAPHICS</div>
|
||
<div>
|
||
<div>
|
||
<div>Population</div>
|
||
<div>${s.population.toLocaleString('sv-SE')}</div>
|
||
</div>
|
||
<div>
|
||
<div>Median income</div>
|
||
<div>${inc} kr</div>
|
||
</div>
|
||
<div>
|
||
<div>Unemployment</div>
|
||
<div>${s.unemployment}%</div>
|
||
</div>
|
||
<div>
|
||
<div>Higher education</div>
|
||
<div>${s.education_pct}%</div>
|
||
</div>
|
||
</div>
|
||
<!-- Age distribution -->
|
||
<div>
|
||
<div>
|
||
<div>Age distribution</div>
|
||
<div>Median ${s.medianAge} yrs</div>
|
||
</div>
|
||
<div>
|
||
<div><span>0–17</span>${ageBar(s.age_0_17*2,'#a78bfa')}<span>${s.age_0_17}%</span></div>
|
||
<div><span>18–44</span>${ageBar(s.age_18_44*2,'#38bdf8')}<span>${s.age_18_44}%</span></div>
|
||
<div><span>45–64</span>${ageBar(s.age_45_64*2,'#4ade80')}<span>${s.age_45_64}%</span></div>
|
||
<div><span>65+</span>${ageBar(s.age_65plus*2,'#fbbf24')}<span>${s.age_65plus}%</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Education levels -->
|
||
<div>
|
||
<div>
|
||
<div>Education profile</div>
|
||
<div>${s.edu_hogskola_kort+s.edu_hogskola_lang+s.edu_forskning}% university+</div>
|
||
</div>
|
||
<div>
|
||
<div><span>Grundskola</span>${ageBar(s.edu_grundskola*1.8,'#6b7280')}<span>${s.edu_grundskola}%</span></div>
|
||
<div><span>Gymnasium</span>${ageBar(s.edu_gymnasium*1.8,'#94a3b8')}<span>${s.edu_gymnasium}%</span></div>
|
||
<div><span>Högskola <3y</span>${ageBar(s.edu_hogskola_kort*1.8,'#38bdf8')}<span>${s.edu_hogskola_kort}%</span></div>
|
||
<div><span>Högskola 3y+</span>${ageBar(s.edu_hogskola_lang*1.8,'#818cf8')}<span>${s.edu_hogskola_lang}%</span></div>
|
||
<div><span>Forskning/PhD</span>${ageBar(s.edu_forskning*1.8,'#c084fc')}<span>${s.edu_forskning}%</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Social profile -->
|
||
<div>
|
||
<div>
|
||
<div>Foreign-born</div>
|
||
<div>${s.foreign_born_pct}%</div>
|
||
</div>
|
||
<div>
|
||
<div>Students</div>
|
||
<div>${s.students_pct}%</div>
|
||
</div>
|
||
<div>
|
||
<div>Self-employed</div>
|
||
<div>${s.self_employed_pct}%</div>
|
||
</div>
|
||
</div>
|
||
${sourceChain([
|
||
{type:'data',label:'SCB Stadsdelsstatistik 2024'},
|
||
{type:'data',label:'SCB Utbildningsregistret 2024'},
|
||
{type:'data',label:'Stockholms stad 2024'}
|
||
])}
|
||
</div>`;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|