Public
Edited
Nov 24, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
land = fetch("https://unpkg.com/world-atlas@1/world/50m.json")
.then((response) => response.json())
.then((world) => topojson.feature(world, world.objects.land))
Insert cell
Insert cell
Insert cell
placesMap = {
// Specify the map’s dimensions and projection.
const { width, height, zoomValue } = config;
const projCenter = [0.0, 52.0];
const projection = d3
.geoConicConformal()
.rotate([-20.0, 0.0])
.center(projCenter)
.parallels([35.0, 65.0])
.translate([width / 2, height / 2])
.scale(zoomValue || 700)
.precision(0.1);

const nonPlottable = places.filter((d) => !d.place);
const data = places.filter((d) => d.place);

// Create the container SVG.
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
// from https://thenewcode.com/1068/Making-Arrows-in-SVG
svg.html(`<defs>
<marker id="pointer" markerWidth="10" markerHeight="8" refX="9.5" refY="5.1" orient="-159" markerUnits="userSpaceOnUse">
<polyline points="1 1, 9 5, 1 7" fill="none" stroke="#999" />
</marker>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="0" refY="2.625" orient="auto">
<polygon points="0 0, 7.5 2.625, 0 5.25" fill="#333" />
</marker>
</defs>`);

// Draw the world
svg
.append("path")
.datum(d3.geoGraticule10())
.attr("fill", "none")
.attr("stroke", "#ddd")
.attr("stroke-width", 0.75)
.attr("stroke-linejoin", "round")
.attr("d", d3.geoPath(projection));

svg
.append("path")
.datum(land)
.attr("fill", "#ddd")
// .attr("stroke", "#777")
// .attr("stroke-width", 0.5)
// .attr("stroke-linejoin", "round")
.attr("d", d3.geoPath(projection));

const g = svg.append("g").classed("places", true);

const borders900600 = [
[width / 2, height / 2],
[0, 0],
[width, 0],
[0, height],
[width, height]
].map((p) => projection.invert(p));

const bordersCoord = [
[20, 52],
[-51.11588871854332, 56.81539826050682],
[91.11588871854333, 56.81539826050682],
[-13.826184343590496, 22.99502863936456],
[53.8261843435905, 22.99502863936456]
];

const bordersProjected = bordersCoord.map((d) => {
return projection([d[0], d[1]]);
});

const projectedPlaces = data.map((d) => {
const proj = projection([d.longitude, d.latitude]);
const outsideX = proj[0] < 0 || proj[0] > width;
const outsideY = proj[1] < 0 || proj[1] > height;
const outside = outsideX || outsideY;
return { ...d, proj, outside, outsideX, outsideY };
});

let place = g
.selectAll(".place")
.data(projectedPlaces)
.join("g")
.attr("class", "place")
.attr("transform", (d) => `translate(${d.proj[0]}, ${d.proj[1]})`);

place
.append("circle")
.attr("r", 2)
.attr("fill", (d) => (d.outside ? "red" : "#444"))
.attr("fill", "#444")
.attr("display", (d) => (d.outside ? "none" : "block"))
.attr("opacity", 0.75);

place
.append("text")
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", "0.5rem")
.attr("stroke", "white")
.attr("stroke-width", 2)
.attr("paint-order", "stroke")
.attr("y", (d) => (d.outside ? 0 : -4))
.text((d) => d.place);

const outsidePlaces = projectedPlaces.filter((d) => d.outside);
//console.log(outsidePlaces);

let borderMargin = 35;
let rectBorder = svg
.append("path")
.attr("stroke", "#00f")
.attr("stroke-width", 1)
.attr("fill", "none")
.attr(
"d",
`M ${borderMargin} ${borderMargin} L ${
width - borderMargin
} ${borderMargin} L ${width - borderMargin} ${
height - borderMargin
} L ${borderMargin} ${height - borderMargin} Z`
);

let arrow = g
.selectAll(".arrow")
.data(outsidePlaces)
.join("path")
.attr("class", "arrow")
.attr("stroke", "#333")
.attr("stroke-width", 1)
.attr(
"d",
(d) =>
`M ${bordersProjected[0][0]} ${bordersProjected[0][1]} L ${d.proj[0]} ${d.proj[1]}`
);

arrow.each(function (d) {
const _arrow = d3.select(this);
const intersection = getIntersection(_arrow.node(), rectBorder.node());
// move the place symbol to the discovered intersection
place
.raise()
.filter((p) => p === d)
.attr("transform", `translate(${intersection[0]}, ${intersection[1]})`);
// re-draw arrow to start from intersection and continue to old position outside the canvas
_arrow.attr(
"d",
(d) =>
`M ${intersection[0]} ${intersection[1]} L ${d.proj[0]} ${d.proj[1]}`
);
// shortern the arrow
const p = _arrow.node().getPointAtLength(borderMargin - 5);
_arrow
.attr("d", `M ${intersection[0]} ${intersection[1]} L ${p.x} ${p.y}`)
.attr("marker-end", "url(#arrowhead)");
});
// arrow.attr("display", "none");
// rectBorder.attr("display", "none");

// let borders = g
// .selectAll(".border")
// .data(bordersProjected)
// .join("circle")
// .attr("class", "border")
// .attr("r", 2)
// .attr("fill", "blue")
// .attr("opacity", 0.75)
// .attr("cx", (d) => d[0])
// .attr("cy", (d) => d[1]);

if (nonPlottable) {
// const index = data.indexOf(nonPlottable);
// data.splice(index, 1);
console.log(nonPlottable);
svg
.append("text")
.attr("font-family", "sans-serif")
.attr("font-size", "0.5rem")
.attr("y", height - 10)
.attr("x", 10)
.text(`⚠️ Missing coordinates for ${nonPlottable.length} places.`);
}

svg
.append("text")
.attr("font-family", "sans-serif")
.attr("font-weight", "bold")
.attr("font-size", "0.75rem")
.attr("transform", (d, i) => `translate(${borderMargin}, ${borderMargin})`)
.text("Places of Birth and Death of Travellers");

function getIntersection(path1, path2) {
// from https://jsfiddle.net/0wdz6yn0/1/
var start = Date.now(),
path1Length = path1.getTotalLength(),
path2Length = path2.getTotalLength(),
path2Points = [];

for (var j = 0; j < path2Length; j++) {
path2Points.push(path2.getPointAtLength(j));
}

for (var i = 0; i < path1Length; i++) {
var point1 = path1.getPointAtLength(i);

for (var j = 0; j < path2Points.length; j++) {
const intersection = pointIntersect(point1, path2Points[j]);
if (intersection) {
return [point1.x, point1.y];
}
}
}
}

function pointIntersect(p1, p2) {
p1.x = Math.round(p1.x);
p1.y = Math.round(p1.y);
p2.x = Math.round(p2.x);
p2.y = Math.round(p2.y);
return p1.x === p2.x && p1.y === p2.y;
}

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more