map = () => {
const wrapper = d3.create("div")
.style("width", `${width}px`)
.style("height", `${height}px`)
.style("position", "relative");
wrapper.append("style").html(css);
const canvas = wrapper.append("canvas")
.style("position", "absolute");
canvas.node().width = width;
canvas.node().height = height;
const context = canvas.node().getContext("2d");
path.context(context);
points
.forEach((d, i) => {
const { lon, lat } = d;
const color_prop = color(d);
let c = color_prop ? colors[color_prop] : "#fff";
const p = projection([lon, lat]);
const x = p[0] - cellwidth / 2;
const y = p[1] - cellheight * 2;
context.fillStyle = c;
context.strokeStyle = c;
context.fillRect(x, y, cellwidth, cellheight);
context.strokeRect(x, y, cellwidth, cellheight);
});
context.strokeStyle = "white";
context.fillStyle = "white";
canmexGeo.features
.forEach((feature, index) => {
context.beginPath();
path(feature);
context.fill();
context.stroke();
});
oceanGeo.features
.forEach((feature, index) => {
context.beginPath();
path(feature);
context.fill();
context.stroke();
});
// Draw the boundaries on the raster
context.beginPath();
path(usGeoInner);
context.strokeStyle = "#e9e9e9"
context.stroke();
context.beginPath();
path(usGeoOuter);
context.strokeStyle = "#d5d5d5"
context.stroke();
// Vector layor
const svg = wrapper.append("svg")
.style("position", "absolute")
.attr("width", width)
.attr("height", height);
// Add legend explaining range outline
const legendPolygonWidth = 12;
const legendPolygonHeight = 16;
const legendPoints = [
[0, 0],
[0, legendPolygonHeight / 2],
[2, legendPolygonHeight / 2],
[2, legendPolygonHeight],
[legendPolygonWidth, legendPolygonHeight],
[legendPolygonWidth, 0],
[0, 0]
];
const legendG = svg.append("g")
.attr("transform", `translate(${[width * (rangeKeyLeft(width)), height * (rangeKeyTop(width))]})`);
legendG.append("polygon")
.attr("fill", "none")
.attr("stroke", "black")
.attr("points", legendPoints);
legendG.append("text")
.attr("font-family", franklinLight)
.attr("x", legendPolygonWidth + 4)
.attr("y", legendPolygonHeight * 0.5 + 5)
.text("Typical range");
// Highlight typical growing range with subtle inner glow
// See also https://observablehq.com/@veltman/inner-glow
path.context(null)
const defs = svg.append("defs")
defs
.append("filter")
.attr("id", "blur")
.append("feGaussianBlur")
.attr("color-interpolation-filters", "sRGB") // for better results in Safari
.attr("stdDeviation", 2);
defs.append("clipPath")
.datum(range)
.attr("id", "clip-path")
.append("path")
.attr("d", path);
svg.append("path")
.datum(range)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-linejoin", "round")
.attr("stroke-opacity", 0.8)
.attr("stroke-width", 1)
.attr("d", path)
svg.append("path")
.datum(range)
.attr("d", path)
.attr("clip-path", "url(#clip-path)")
.attr("filter", "url(#blur)")
.style("stroke", "black")
.style("opacity", 0.8)
.style("fill", "none");
const citiesG = svg.selectAll(".city")
.data(cities)
.join("g")
.attr("class", d => `city ${d.pos} ${toSlugCase(d.name)}`)
.attr("transform", d => `translate(${projection([d.lon, d.lat])})`);
citiesG.append("circle")
.attr("r", 3);
citiesG.append("text")
.attr("class", "bg bg-0")
.text(d => d.name);
citiesG.append("text")
.attr("class", "bg bg-1")
.text(d => d.name);
citiesG.append("text")
.attr("class", "fg")
.text(d => d.name);
return wrapper.node();
}