function AnimatedScatterplot(data, {
x = ([x]) => x,
y = ([, y]) => y,
r = 3,
width = 640,
height = 400,
xType = d3.scaleLinear,
yType = d3.scaleLinear,
fill = "none",
stroke = "currentColor",
strokeWidth = 1.5,
} = {}) {
const xRange = [20, width - 20];
const yRange = [height - 30, 20];
let xScale = xType(d3.extent(data, x), xRange);
let yScale = yType(d3.extent(data, y), yRange);
const xAxis = d3.axisBottom(xScale).ticks(width / 80);
const yAxis = d3.axisLeft(yScale).ticks(height / 50);
const svg = d3.create("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 - 30})`)
.call(xAxis)
.call(g => g.select(".domain").remove())
svg.append("g")
.attr("transform", `translate(${20},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
svg.append("g")
.attr("fill", fill)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => xScale(x(d)))
.attr("cy", d => yScale(y(d)))
.attr("r", r);
return Object.assign(svg.node(), {
update(data) {
xScale = xType(d3.extent(data, x), xRange);
yScale = yType(d3.extent(data, y), yRange);
let selection = svg.selectAll("circle")
.data(data)
selection.enter()
.append("circle")
.attr("cx", d => xScale(x(d)))
.attr("cy", d => yScale(y(d)))
.attr("r", 0)
.attr("fill", fill)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.transition().duration(750)
.attr("r", r)
selection
.transition()
.delay(750)
.duration(750)
.attr("cx", d => xScale(x(d)))
.attr("cy", d => yScale(y(d)))
selection.exit()
.transition().duration(750)
.attr("r", 0)
.remove()
}
})
return svg.node();
}