placesMap = {
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);
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
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>`);
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();
}