drawGraticuleLabels = {
const defaultPositions = ["top", "left", "bottom", "right"];
function drawGraticuleTicks({
selection,
projection,
width,
height,
marginTop,
marginRight,
marginBottom,
marginLeft,
layer,
debug
}) {
const {
tickPositions = defaultPositions,
place = "outside",
fontFamily = "inherit",
fontSize = "12px",
fontWeight = "500",
fontFill = "black",
fontStroke = "white",
fontStrokeWidth = 3,
stroke = "black",
strokeWidth = 1,
strokeOpacity = 1,
strokeLinecap = "round",
strokeLinejoin = "round",
tickSize = 4,
tickPadding = 4,
step = [10, 10],
closenessPrecision = 0.001,
precision,
formatLonTick,
formatLatTick
} = layer;
const formatLatLon = d3.format(".2~f");
const formatLongitude =
typeof formatLonTick === "function"
? formatLonTick
: (x) => `${formatLatLon(Math.abs(x))}°${x < 0 ? "W" : "E"}`;
const formatLatitude =
typeof formatLatTick === "function"
? formatLatTick
: (y) => `${formatLatLon(Math.abs(y))}°${y < 0 ? "S" : "N"}`;
const boundsGeo = getBoundsAsFeature({
projection,
width,
height,
marginTop,
marginRight,
marginBottom,
marginLeft,
precision
});
const pos = graticuleLabelPositions(boundsGeo, { step });
const P = pos.map((p) => {
const [x, y] = projection(p);
p.x = x;
p.y = y;
return p;
});
const g = selection.append("g").attr("class", "graticule-labels");
let topTicks = [],
rightTicks = [],
bottomTicks = [],
leftTicks = [];
if (tickPositions.includes("top")) {
const topMost = d3.min(P, (p) => p.y);
let topTicks = pos.filter(
(p) => Math.abs(p.y - topMost) <= closenessPrecision
);
topTicks = topTicks.filter((t) => t.type === "longitude");
const topTickG = g
.append("g")
.attr("class", "bottom-graticules-ticks")
.selectAll(".tick")
.data(topTicks)
.join("g")
.attr("class", "tick")
.attr("transform", (d) => `translate(${d.x},${d.y})`);
topTickG
.append("line")
.attr("class", "tick-line")
.attr("y2", (place === "outside" ? -1 : 1) * tickSize);
topTickG
.append("text")
.attr("dy", (place === "outside" ? -1 : 1) * (tickSize + tickPadding))
.attr("text-anchor", "middle")
.attr("dominant-baseline", place === "outside" ? "auto" : "hanging");
}
if (tickPositions.includes("bottom")) {
const bottomMost = d3.max(P, (p) => p.y);
let bottomTicks = pos.filter(
(p) => Math.abs(p.y - bottomMost) <= closenessPrecision
);
bottomTicks = bottomTicks.filter((t) => t.type === "longitude");
const bottomTickG = g
.append("g")
.attr("class", "bottom-graticules-ticks")
.selectAll(".tick")
.data(bottomTicks)
.join("g")
.attr("class", "tick")
.attr("transform", (d) => `translate(${d.x},${d.y})`);
bottomTickG
.append("line")
.attr("class", "tick-line")
.attr("y2", (place === "outside" ? 1 : -1) * tickSize);
bottomTickG
.append("text")
.attr("dy", (place === "outside" ? 1 : -1) * (tickSize + tickPadding))
.attr("text-anchor", "middle")
.attr("dominant-baseline", place === "outside" ? "hanging" : "auto");
}
if (tickPositions.includes("right")) {
const rightMost = d3.max(P, (p) => p.x);
rightTicks = pos.filter(
(p) => Math.abs(p.x - rightMost) <= closenessPrecision
);
rightTicks = rightTicks.filter((t) => t.type === "latitude");
const rightTickG = g
.append("g")
.attr("class", "left-graticules-ticks")
.selectAll(".tick")
.data(rightTicks)
.join("g")
.attr("class", "tick")
.attr("transform", (d) => `translate(${d.x},${d.y})`);
rightTickG
.append("line")
.attr("class", "tick-line")
.attr("x2", (place === "outside" ? 1 : -1) * tickSize);
rightTickG
.append("text")
.attr("dy", (place === "outside" ? -1 : 1) * (tickSize + tickPadding))
.attr("dominant-baseline", place === "outside" ? "auto" : "hanging")
.attr("text-anchor", "middle")
.attr("transform", "rotate(90)");
}
if (tickPositions.includes("left")) {
const leftMost = d3.min(P, (p) => p.x);
leftTicks = pos.filter(
(p) => Math.abs(p.x - leftMost) <= closenessPrecision
);
leftTicks = leftTicks.filter((t) => t.type === "latitude");
const leftTickG = g
.append("g")
.attr("class", "left-graticules-ticks")
.selectAll(".tick")
.data(leftTicks)
.join("g")
.attr("class", "tick")
.attr("transform", (d) => `translate(${d.x},${d.y})`);
leftTickG
.append("line")
.attr("class", "tick-line")
.attr("x2", (place === "outside" ? -1 : 1) * tickSize);
leftTickG
.append("text")
.attr("dy", (place === "outside" ? -1 : 1) * (tickSize + tickPadding))
.attr("dominant-baseline", place === "outside" ? "auto" : "hanging")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)");
}
// Add tick styles
g.selectAll(".tick-line")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin);
// Add label text
g.selectAll("text")
.attr("font-family", fontFamily)
.attr("font-size", fontSize)
.attr("font-weight", fontWeight)
.attr("fill", fontFill)
.attr("stroke", fontStroke)
.attr("stroke-width", fontStrokeWidth)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("paint-order", "stroke")
.text((d) =>
d.type === "longitude" ? formatLongitude(d[0]) : formatLatitude(d[1])
);
if (debug) {
g.append("path")
.attr("class", "debug-dgt-bounds")
.attr("d", d3.geoPath(projection)(boundsGeo))
.attr("fill", "#0ff")
.attr("fill-opacity", 0.5)
.attr("stroke", "none");
g.selectAll(".debug-dgt-corner")
.data([
[marginLeft, marginTop],
[width - marginRight, marginTop],
[width - marginRight, height - marginBottom],
[marginLeft, height - marginBottom]
])
.join("circle")
.attr("class", "debug-dgt-corner")
.attr("fill", "#0ff")
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("r", 5);
g.append("g")
.attr("class", "graticules-intersection-points")
.selectAll(".graticules-intersection-point")
.data(pos)
.join("circle")
.attr("class", "graticules-intersection-point")
.attr("r", 4)
.attr("fill", "none")
.attr("stroke-width", 2)
.each(function (d) {
const [cx, cy] = projection(d);
d3.select(this)
.attr("cx", cx)
.attr("cy", cy)
.attr("stroke", d.type === "longitude" ? "red" : "orange");
});
}
}
drawGraticuleTicks.margins = {
x: 20,
y: 20
};
drawGraticuleTicks.positions = defaultPositions;
return drawGraticuleTicks;
}