Skip to content

Commit 56afa0e

Browse files
committed
DensityLayout
DensityLayout
1 parent bd7a1a0 commit 56afa0e

3 files changed

Lines changed: 152 additions & 0 deletions

File tree

assets/data/density-2026-06.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

src/layouts/BaseLayout.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const navItems = [
3232
['platform', 'Platform'],
3333
['coverage', 'Coverage'],
3434
['prefix-lookup', 'Prefix Lookup'],
35+
['prefix-density', 'Density'],
3536
['methodology', 'Methodology'],
3637
['downloads', 'Downloads'],
3738
['docs', 'Documentation'],

src/pages/prefix-density.astro

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
---
2+
import BaseLayout from '../layouts/BaseLayout.astro';
3+
import releases from '../../assets/data/releases.json';
4+
5+
const release = releases.current_release;
6+
7+
const breadcrumb = {
8+
'@context': 'https://schema.org',
9+
'@type': 'BreadcrumbList',
10+
itemListElement: [
11+
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://geoiplocations.com/' },
12+
{ '@type': 'ListItem', position: 2, name: 'Density map', item: 'https://geoiplocations.com/prefix-density.html' },
13+
],
14+
};
15+
---
16+
<BaseLayout
17+
title="IPv4 Density Map — Where IP Space Concentrates | ProdIPData"
18+
description="An interactive hex-bin density map of the published IPv4 /24 space for the current GeoIP Locations release — see where address space concentrates worldwide, rendered in your browser."
19+
ogTitle="IPv4 Density Map"
20+
canonicalPath="/prefix-density.html"
21+
page="prefix-density"
22+
navPage="prefix-density"
23+
>
24+
<script slot="head" type="application/ld+json" is:inline set:html={JSON.stringify(breadcrumb)} />
25+
26+
<main class="container density">
27+
<section class="density-hero">
28+
<h1>Where IPv4 lives</h1>
29+
<p class="density-sub">
30+
A hex-bin density view of the published <code>/24</code> space for the <strong>{release}</strong>
31+
release — brighter cells hold more address blocks. Aggregated and rendered entirely in your
32+
browser; drag the hex size, tilt to 3D, and hover any cell for its count.
33+
</p>
34+
</section>
35+
36+
<div class="dmap-wrap">
37+
<div id="dmap"></div>
38+
<div id="dpanel">
39+
<div class="dp-title">Density</div>
40+
<div class="dp-meta" id="dmeta">loading…</div>
41+
<label>Hex radius: <span id="rOut">70</span> km</label>
42+
<input id="r" type="range" min="20" max="240" value="70" step="5" />
43+
<label class="dp-check"><input type="checkbox" id="ext" /> 3D extrude</label>
44+
<div class="dp-legend-label">sparse &rarr; dense</div>
45+
<div class="dp-legend" id="leg"></div>
46+
</div>
47+
<div id="dtip"></div>
48+
</div>
49+
50+
<p class="density-note muted">
51+
One record per geolocated <code>/24</code>, binned into hexagons by deck.gl. Open-source, no map
52+
tiles, no tracking. Looking for a specific block? Use <a href={`./prefix-lookup.html`}>Prefix Lookup</a>.
53+
</p>
54+
</main>
55+
56+
<div id="dcfg" data-release={release} data-base={import.meta.env.BASE_URL} hidden></div>
57+
58+
<style is:inline>
59+
.density { padding: 2.2rem 0 3.5rem; max-width: 1100px; }
60+
.density-hero h1 { margin: 0 0 .4rem; }
61+
.density-sub { color: var(--muted); line-height: 1.7; margin: 0 0 1.4rem; max-width: 820px; }
62+
.dmap-wrap { position: relative; width: 100%; height: 600px; border-radius: 18px; overflow: hidden;
63+
border: 1px solid var(--line); background: #070d1c; }
64+
#dmap { position: absolute; inset: 0; }
65+
#dpanel { position: absolute; top: 14px; left: 14px; z-index: 2; width: 210px;
66+
background: rgba(11,21,48,.9); border: 1px solid rgba(255,255,255,.12); border-radius: 12px;
67+
padding: 13px 15px; color: var(--text); font-size: 13px; backdrop-filter: blur(6px); }
68+
#dpanel .dp-title { font-size: 14px; font-weight: 500; }
69+
#dpanel .dp-meta { color: var(--muted); font-size: 12px; margin: 1px 0 8px; }
70+
#dpanel label { display: block; margin: 9px 0 3px; color: var(--muted); }
71+
#dpanel input[type=range] { width: 100%; }
72+
.dp-check { display: flex; align-items: center; gap: 7px; margin-top: 8px; color: var(--muted); }
73+
.dp-check input { width: auto; }
74+
.dp-legend-label { color: var(--muted); font-size: 11px; margin-top: 10px; }
75+
.dp-legend { display: flex; height: 10px; border-radius: 5px; overflow: hidden; margin-top: 4px; }
76+
.dp-legend i { flex: 1; }
77+
#dtip { position: absolute; z-index: 3; pointer-events: none; background: rgba(0,0,0,.82); color: #fff;
78+
font-size: 12px; padding: 4px 8px; border-radius: 6px; display: none; }
79+
.density-note { margin-top: 1.4rem; font-size: .9rem; }
80+
</style>
81+
82+
<script is:inline src="https://cdn.jsdelivr.net/npm/deck.gl@9/dist.min.js"></script>
83+
<script is:inline src="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script>
84+
<script is:inline>
85+
(function () {
86+
var cfg = document.getElementById('dcfg').dataset;
87+
var base = cfg.base.endsWith('/') ? cfg.base : cfg.base + '/';
88+
var release = cfg.release;
89+
var RAMP = [[12,44,92],[14,116,144],[6,182,212],[250,204,21],[249,115,22],[239,68,68]];
90+
document.getElementById('leg').innerHTML = RAMP.map(function (c) { return '<i style="background:rgb(' + c + ')"></i>'; }).join('');
91+
var tip = document.getElementById('dtip');
92+
var points = [], world = null;
93+
94+
var deckgl = new deck.DeckGL({
95+
container: 'dmap',
96+
initialViewState: { longitude: 5, latitude: 25, zoom: 1.15, pitch: 0, bearing: 0 },
97+
controller: true,
98+
parameters: { clearColor: [0.027, 0.05, 0.11, 1] }
99+
});
100+
101+
function build() {
102+
var radius = +document.getElementById('r').value * 1000;
103+
var extruded = document.getElementById('ext').checked;
104+
var layers = [];
105+
if (world) layers.push(new deck.GeoJsonLayer({
106+
id: 'world', data: world, stroked: true, filled: true,
107+
getFillColor: [20, 35, 64], getLineColor: [255, 255, 255, 30], lineWidthMinPixels: 0.4
108+
}));
109+
layers.push(new deck.HexagonLayer({
110+
id: 'hex', data: points,
111+
getPosition: function (d) { return [d[0], d[1]]; },
112+
getColorWeight: function (d) { return d[2]; }, getElevationWeight: function (d) { return d[2]; },
113+
colorAggregation: 'SUM', elevationAggregation: 'SUM',
114+
radius: radius, coverage: 0.92, opacity: 0.85,
115+
extruded: extruded, elevationScale: extruded ? 25 : 0,
116+
colorRange: RAMP, pickable: true,
117+
updateTriggers: { getColorWeight: radius },
118+
onHover: function (info) {
119+
if (info.object) {
120+
var n = info.object.colorValue != null ? info.object.colorValue : (info.object.elevationValue || 0);
121+
tip.style.display = 'block';
122+
tip.style.left = (info.x + 12) + 'px';
123+
tip.style.top = (info.y + 12) + 'px';
124+
tip.textContent = n.toLocaleString() + ' /24s in this hex';
125+
} else { tip.style.display = 'none'; }
126+
}
127+
}));
128+
deckgl.setProps({ layers: layers });
129+
}
130+
131+
document.getElementById('r').addEventListener('input', function (e) {
132+
document.getElementById('rOut').textContent = e.target.value; build();
133+
});
134+
document.getElementById('ext').addEventListener('change', build);
135+
136+
fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json')
137+
.then(function (r) { return r.json(); })
138+
.then(function (w) { world = topojson.feature(w, w.objects.countries); if (points.length) build(); });
139+
140+
fetch(base + 'assets/data/density-' + release + '.json')
141+
.then(function (r) { return r.json(); })
142+
.then(function (j) {
143+
points = j.points;
144+
document.getElementById('dmeta').textContent = points.length.toLocaleString() + ' points · ' + j.release;
145+
build();
146+
})
147+
.catch(function () { document.getElementById('dmeta').textContent = 'density data not published yet'; });
148+
})();
149+
</script>
150+
</BaseLayout>

0 commit comments

Comments
 (0)