Skip to content

Instantly share code, notes, and snippets.

@opencoca
Last active October 22, 2025 09:04
Show Gist options
  • Save opencoca/8b9c17194850e881cc7c0ca39a0a145b to your computer and use it in GitHub Desktop.
Save opencoca/8b9c17194850e881cc7c0ca39a0a145b to your computer and use it in GitHub Desktop.
Sage 3D Globe Dashboard News example.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sage 3D News Globe</title>
<link href="https://startr.style/style.css" rel="stylesheet" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* LEFT SIDEBAR */
.logo {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-bottom: 30px;
color: #00d9ff;
}
.nav-item {
padding: 12px 16px;
margin: 4px 0;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.nav-item.active {
background: #00d9ff;
color: #1a1a2e;
}
/* CENTER - GLOBE */
.globe-container {
background: #0a0a0f;
}
#globeViz {
width: 100%;
height: 100%;
}
.globe-controls {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
padding: 15px;
border-radius: 8px;
color: white;
}
.toggle-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
/* RIGHT SIDEBAR */
.detail-panel h2 {
font-size: 24px;
margin-bottom: 20px;
color: #af1aae;
}
.metric {
background: white;
padding: 16px;
margin: 12px 0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.metric-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-value {
font-size: 32px;
font-weight: bold;
color: #00d9ff;
margin-top: 8px;
}
.chart-placeholder {
background: white;
height: 200px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
margin-top: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.empty-state {
color: #999;
text-align: center;
padding: 40px 20px;
}
</style>
</head>
<body style="--d:flex; --jc: space-between; pointer-events: none; ">
<!-- LEFT SIDEBAR -->
<div class="sidebar-left" style="--w:16rem; --p:1em; pointer-events: auto; ">
<div class="logo">Sage.is News</div>
<div class="nav-item active">Dash 🌍</div>
<div class="nav-item">Analytics</div>
<div class="nav-item">Locations</div>
<div class="nav-item">Reports</div>
<div class="nav-item">Settings</div>
</div>
<!-- CENTER - GLOBE -->
<div class="globe-container" style="--pos:fixed; --z:-1; --w:100%; pointer-events: auto; ">
<div id="globeViz"></div>
<div class="globe-controls">
<label class="toggle-label">
<input type="checkbox" id="showConnections">
<span>Hide Connections</span>
</label>
</div>
</div>
<!-- RIGHT SIDEBAR -->
<div class="sidebar-right; pointer-events: auto; ">
<div id="detailPanel" class="empty-state">
Click on a location marker to view details
</div>
</div>
<!-- Globe.GL Library -->
<script src="https://unpkg.com/globe.gl"></script>
<script>
const locations = [
{
id: 1,
name: "Kyiv",
lat: 50.4501,
lng: 30.5234,
metricA: 85,
metricB: 1000000,
description: "Targeted by massive Russian air strikes causing widespread power outages and residential fires."
},
{
id: 2,
name: "Brovary",
lat: 50.6822,
lng: 30.8036,
metricA: 70,
metricB: 20000,
description: "Civilian areas struck, killing 6 including children in fires from debris."
},
{
id: 3,
name: "Odesa",
lat: 46.4825,
lng: 30.7233,
metricA: 75,
metricB: 500000,
description: "Energy infrastructure hit, exacerbating national power crisis."
},
{
id: 4,
name: "Moscow",
lat: 55.7558,
lng: 37.6173,
metricA: 90,
metricB: 0,
description: "Launched over 100 missiles and drones in escalation against Ukraine."
},
{
id: 5,
name: "Paris",
lat: 48.8566,
lng: 2.3522,
metricA: 80,
metricB: 88,
description: "Louvre reopens after daylight heist of crown jewels valued at €88M."
},
{
id: 6,
name: "Washington DC",
lat: 38.9072,
lng: -77.0369,
metricA: 65,
metricB: 1,
description: "Vance heads to Jerusalem; Trump shelves Putin summit amid Ukraine crisis."
},
{
id: 7,
name: "Jerusalem",
lat: 31.7683,
lng: 35.2137,
metricA: 75,
metricB: 2000000,
description: "Netanyahu to discuss Gaza with US VP Vance as aid remains blocked."
},
{
id: 8,
name: "Gaza City",
lat: 31.5017,
lng: 34.4668,
metricA: 95,
metricB: 500,
description: "Ceasefire fails to deliver aid; UN reports critical shortages and ongoing suffering."
},
{
id: 9,
name: "Rabat",
lat: 34.0209,
lng: -6.8416,
metricA: 60,
metricB: 5000,
description: "Gen Z protests demand education and anti-corruption reforms; met with arrests."
},
{
id: 10,
name: "Casablanca",
lat: 33.5731,
lng: -7.5898,
metricA: 55,
metricB: 10000,
description: "Youth demonstrations spread, calling for healthcare improvements and accountability."
},
{
id: 11,
name: "Munich",
lat: 48.1351,
lng: 11.5820,
metricA: 40,
metricB: 6000000,
description: "Oktoberfest concludes with losses from earlier bomb threat shutdown."
},
{
id: 12,
name: "Kigali",
lat: -1.9493,
lng: 30.0589,
metricA: 30,
metricB: 10000,
description: "MWC25 launches, spotlighting Africa's push for digital connectivity and innovation."
},
{
id: 13,
name: "New York",
lat: 40.7128,
lng: -74.0060,
metricA: 70,
metricB: 10000000,
description: "AWS outage disrupts services, highlighting global reliance on cloud infrastructure."
},
{
id: 14,
name: "Seattle",
lat: 47.6062,
lng: -122.3321,
metricA: 85,
metricB: 1000000000,
description: "AWS US-EAST-1 failure triggers worldwide internet disruptions affecting billions."
},
{
id: 15,
name: "London",
lat: 51.5074,
lng: -0.1278,
metricA: 60,
metricB: 5000000,
description: "Europe grapples with outage fallout, impacting businesses and daily communications."
}
];
const connections = [
{ from: 4, to: 1 },
{ from: 4, to: 2 },
{ from: 4, to: 3 },
{ from: 6, to: 4 },
{ from: 6, to: 7 },
{ from: 7, to: 8 },
{ from: 5, to: 11 },
{ from: 5, to: 15 },
{ from: 9, to: 10 },
{ from: 14, to: 13 },
{ from: 14, to: 15 },
{ from: 14, to: 5 },
{ from: 14, to: 7 },
{ from: 11, to: 5 }
];
function getArcsData() {
return connections.map(conn => {
const from = locations.find(l => l.id === conn.from);
const to = locations.find(l => l.id === conn.to);
return {
startLat: from.lat,
startLng: from.lng,
endLat: to.lat,
endLng: to.lng,
color: from.id % 2 === 0 ? 'rgba(0, 217, 255, 0.5)' : 'rgba(255, 71, 200, 0.5)'
};
});
}
// Initialize Globe
const globe = Globe()
(document.getElementById('globeViz'))
.globeImageUrl('https://upload.wikimedia.org/wikipedia/commons/b/ba/The_earth_at_night.jpg')
.bumpImageUrl('https://unpkg.com/three-globe/example/img/earth-topology.png')
.backgroundImageUrl('https://unpkg.com/three-globe/example/img/night-sky.png')
.pointsData(locations)
.pointLat('lat')
.pointLng('lng')
.pointColor(() => '#ff47FFBB')
.pointAltitude(0.02)
.pointRadius(0.6)
.pointLabel(d => `<b>${d.name}</b><br/>Impact: ${d.metricA}<br/>Affected: ${d.metricB}`)
.onPointClick(point => {
showDetails(point);
globe.pointOfView({
lat: point.lat,
lng: point.lng,
altitude: 2
}, 1000);
})
.arcsData(getArcsData())
.arcColor('color')
.arcStroke(0.5)
.arcDashLength(0.4)
.arcDashGap(0.2)
.arcDashAnimateTime(2000);
// Controls
globe.controls().autoRotate = true;
globe.controls().autoRotateSpeed = 0.06;
// Show/hide connections
document.getElementById('showConnections').addEventListener('change', (e) => {
if (!e.target.checked) {
globe.arcsData(getArcsData());
globe.arcDashGap(0.2);
} else {
globe.arcsData([]);
}
});
// Show location details
function showDetails(location) {
const panel = document.getElementById('detailPanel');
panel.innerHTML = `
<div class="detail-panel">
<h2>${location.name}</h2>
<p style="color: #666; margin-bottom: 20px;">${location.description}</p>
<div class="metric">
<div class="metric-label">Impact Score</div>
<div class="metric-value">${location.metricA}</div>
</div>
<div class="metric">
<div class="metric-label">Affected</div>
<div class="metric-value">${location.metricB}</div>
</div>
<div class="chart-placeholder">
📊 News Impact Chart
</div>
</div>
`;
}
// Handle window resize
window.addEventListener('resize', () => {
globe.width(document.getElementById('globeViz').offsetWidth);
globe.height(document.getElementById('globeViz').offsetHeight);
});
</script>
</body>
</html>
<link href="https://startr.style/style.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment