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,
aspectRatio,
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 = "transparent",
fillOpacity = 1,
stroke = "currentColor",
strokeWidth = 1.5,
strokeOpacity = 1,
halo = "#fff",
haloWidth = 3 // padding around the labels
} = {}) {
// Compute values.
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]));
// Compute default domains.
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined) yDomain = d3.extent(Y);
// Construct scales.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
// Compute dimensions.
// Adapted from Observable Plot.
// Released under the ISC license.
// https://github.com/observablehq/plot/blob/main/src/dimensions.js
if (aspectRatio != null) {
aspectRatio = +aspectRatio;
if (!(isFinite(aspectRatio) && aspectRatio > 0)) throw new Error(`invalid aspectRatio: ${aspectRatio}`);
const ratio = aspectRatioLength(yScale) / (aspectRatioLength(xScale) * aspectRatio);
const w = width - marginLeft - marginRight - insetLeft - insetRight;
height = ratio * w + insetTop + insetBottom + marginTop + marginBottom;
xScale.range([marginLeft + insetLeft, width - marginRight - insetRight]);
yScale.range([height - marginBottom - insetBottom, marginTop + insetTop]);
}
// Create canvas.
const ctx = DOM.context2d(width, height);
// x-axis
const xFormatter = xScale.tickFormat(width / 80, xFormat);
const xTicks = xScale.ticks(width / 80);
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.strokeStyle = 'black';
xTicks.forEach(d => {
// tick
ctx.globalAlpha = 1.0;
ctx.beginPath();
ctx.moveTo(xScale(d), height - marginBottom);
ctx.lineTo(xScale(d), height - marginBottom + 6);
ctx.stroke();
// grid lines
ctx.globalAlpha = 0.1;
ctx.beginPath();
ctx.moveTo(xScale(d), height - marginBottom);
ctx.lineTo(xScale(d), marginTop);
ctx.stroke();
// label
ctx.globalAlpha = 1.0;
ctx.fillText(xFormatter(d), xScale(d), height - marginBottom + 9);
});
if (xLabel) {
ctx.globalAlpha = 1.0;
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText(xLabel, width, height - 1);
}
// y-axis
const yFormatter = yScale.tickFormat(height / 50, yFormat);
const yTicks = yScale.ticks(height / 50);
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.strokeStyle = 'black';
yTicks.forEach(d => {
// tick
ctx.globalAlpha = 1.0;
ctx.beginPath();
ctx.moveTo(marginLeft, yScale(d));
ctx.lineTo(marginLeft - 6, yScale(d));
ctx.stroke();
// grid lines
ctx.globalAlpha = 0.1;
ctx.beginPath();
ctx.moveTo(marginLeft, yScale(d));
ctx.lineTo(width - marginRight, yScale(d));
ctx.stroke();
// label
ctx.globalAlpha = 1.0;
ctx.fillText(yFormatter(d), marginLeft - 9, yScale(d));
});
if (yLabel) {
ctx.globalAlpha = 1.0;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(yLabel, 0, 1);
}
// text
if (T) {
ctx.globalAlpha = 1;
ctx.font = '10px sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = halo;
ctx.lineWidth = haloWidth;
I.forEach(i => {
ctx.strokeText(T[i], xScale(X[i]) + 7, yScale(Y[i]));
ctx.fillText(T[i], xScale(X[i]) + 7, yScale(Y[i]));
});
}
// dots
ctx.strokeStyle = stroke;
ctx.fillStyle = fill;
ctx.lineWidth = strokeWidth;
I.forEach(i => {
ctx.beginPath();
ctx.arc(xScale(X[i]), yScale(Y[i]), r, 0, 2 * Math.PI);
ctx.globalAlpha = strokeOpacity;
ctx.stroke();
ctx.globalAlpha = fillOpacity;
ctx.fill();
});
return ctx.canvas;
}