function Scatterplot(data, {
x = ([x]) => x,
y = ([, y]) => y,
r = 3,
title,
marginTop = 20,
marginRight = 30,
marginBottom = 30,
marginLeft = 40,
inset = r * 2,
insetTop = inset,
insetRight = inset,
insetBottom = inset,
insetLeft = inset,
width = 640,
height = 400,
xType = d3.scaleLinear,
xDomain,
xRange = [marginLeft + insetLeft, width - marginRight - insetRight],
yType = d3.scaleLinear,
yDomain,
yRange = [height - marginBottom - insetBottom, marginTop + insetTop],
xLabel,
yLabel,
xFormat,
yFormat,
fill = "none",
stroke = "currentColor",
strokeWidth = 1.5,
halo = "#fff",
haloWidth = 3
} = {}) {
const X = d3.map(data, x);
const Y = d3.map(data, y);
const T = title == null ? null : d3.map(data, title);
const I = d3.range(X.length).filter(i => !isNaN(X[i]) && !isNaN(Y[i]));
// console.log({X, Y, T, I})
// Compute default domains.
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined) yDomain = d3.extent(Y);
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
// const xAxis = d3.axisBottom(xScale).ticks(width / 80, xFormat);
const xAxis = d3.axisBottom(xScale).ticks(d3.timeYear.every(1));
const yAxis = d3.axisLeft(yScale).ticks(height / 50, yFormat);
// Tooltip
const container = d3.select(
html`<div style="position:relative;">${tooltipTemplate}</div>`
);
const tooltipDiv = container.select(".tooltip");
const svg = container.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("y2", marginTop + marginBottom - height)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", width)
.attr("y", marginBottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(xLabel));
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
if (T) svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.selectAll("text")
.data(I)
.join("text")
.attr("dx", 7)
.attr("dy", i => collide(X, T, i))
.attr("x", i => xScale(X[i]))
.attr("y", i => yScale(Y[i]))
.text(i => T[i])
.call(text => text.clone(true))
.attr("fill", "none")
.attr("stroke", halo)
.attr("stroke-width", haloWidth);
svg.append("g")
.attr("fill", fill)
.attr("stroke-width", strokeWidth)
.selectAll("circle")
.data(I)
.join("circle")
.attr("stroke", i => stroke(T[i]))
.attr("cx", i => xScale(X[i]))
.attr("cy", i => yScale(Y[i]))
.attr("r", r)
.call(tooltip, tooltipDiv);
return container.node();
}