map = {
const circle = d3.geoCircle(),
svg = d3.create("svg").attr("width", width).attr("height", height);
const path = d3.geoPath().projection(projection);
const topg = svg.append("g").attr("class", "topg");
topg
.append("rect")
.on("click", (d) =>
svg.transition().duration(1500).call(zoom.transform, d3.zoomIdentity)
)
.attr("width", width)
.attr("height", height)
.attr("fill", "#CFDCDE");
topg
.selectAll("path")
.data(borders.features)
.enter()
.append("path")
.on("click", (d) => {
const [[x0, y0], [x1, y1]] = path.bounds(d);
svg
.transition()
.duration(1500)
.call(
zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(
Math.min(8, 0.9 / Math.max((x1 - x0) / width, (y1 - y0) / height))
)
.translate(-(x0 + x1) / 2, -(y0 + y1) / 2)
// d3.clientPoint(event, svg.node())
);
})
.attr("d", path)
.attr("fill", "white")
// .attr("cursor", "pointer")
.attr("stroke", "#999")
.attr("stroke-width", "0.5px");
// country borders
topg
.selectAll("path")
.data(borders.features)
.enter()
.append("path")
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "#B8B8B8")
.attr("stroke-width", "0.5px");
let hexes;
let colorPicker = (rtt) => {
if (rtt <= min_rtt) return color(min_rtt); // minn_rtt
if (rtt >= max_rtt) return color(max_rtt);
return color(rtt);
};
const zoom = d3
.zoom()
.scaleExtent([1, 32])
.on("zoom", () => {
// called on onzoom event
const { transform } = d3.event;
// let bounds = topg.node().getBoundingClientRect();
// map lines also get thinner as we zoom in
topg.selectAll("path").attr("stroke-width", 0.5 / transform.k);
topg.attr("transform", transform);
let hexbin = d3
.hexbin()
.extent([
[0, 0],
[width, height]
])
.radius(2 / transform.k)
.x((d) => d.latlon[0])
.y((d) => d.latlon[1]);
let aggregateFunction = aggregateBy.f || d3.median;
let min_rtts_with_geo_hex = Object.assign(
hexbin(min_rtts_with_geo)
.map((d) => ((d.min_rtt = aggregateFunction(d, (d) => d.min_rtt)), d))
.sort((a, b) => b.prb_id - a.prb_id),
{ title: "RTT" }
);
let p95 = d3.quantile(
min_rtts_with_geo_hex.map((hex) => hex.length),
1.0
);
hexes.selectAll("path").remove();
hexes
.selectAll("path")
.data(
min_rtts_with_geo_hex.filter((hex) => {
let [x, y] = projection([hex.y, hex.x]);
return true; //x > (-bounds.left + bounds.width / 2) / transform.k;
})
)
.join("path")
.attr("transform", (d) => {
let [x, y] = projection([d.y, d.x]);
return `translate(${x},${y})`;
})
.attr("d", (d) =>
hexbin.hexagon(
(d.length <= p95
? probeScale(p95)(d.length)
: probeScale(p95)(p95)) / transform.k
)
) // 5
.attr("fill", (d) => colorPicker(d.min_rtt))
.attr("stroke", (d) => d3.lab(colorPicker(d.min_rtt)).darker())
.attr("stroke-width", 1.5 / transform.k)
.attr("cursor", (h) => {
// link to atlas.ripe.net if hexbin contains only 1 probe
return h.length === 1 ? "pointer" : "";
})
.on("click", ([first, ...otherThings]) => {
// let url = `https://atlas.ripe.net/probes/${first.prb_id}`
let url = `https://observablehq.com/@ripencc/atlas-probe-neighbourhood?probe_id=${first.prb_id}&af=${af.af}`;
window.open(url, "_blank");
})
.on("mouseover", (d) => {
mutable current_hexbin = d;
mutable hover = d.min_rtt;
})
.on("mouseout", () => {
mutable hover = null;
})
.append("title")
// hexbin on:hover tooltip
.text((d) => {
return `In this hexbin \n\nprobes: \t\t\t ${d.length.toLocaleString()} \nlatency (${aggregateBy.label.toLowerCase()}): \t ${d.min_rtt.toFixed(
2
)}ms`;
});
});
hexes = topg.append("g");
svg.call(zoom);
// by this point we've got all of our drawing
// definitions inside the 'zoom' callback, so
// we need to fake a 0-factor zoom event to
// get things painted for the first time
// Also, give a 2x zoom factor if we're on
// fullscreen
svg.transition().call(zoom.scaleBy, document.fullscreenElement ? 2 : 0);
return Object.assign(svg.node(), {
zoomIn: () => svg.transition().call(zoom.scaleBy, 2),
zoomOut: () => svg.transition().call(zoom.scaleBy, 0.5)
// zoomRandom: random,
// zoomReset: reset
});
}