function ScatterplotMatrix(data, {
columns = data.columns,
x = columns,
y = columns,
z = () => 1,
padding = 20,
marginTop = 10,
marginRight = 20,
marginBottom = 30,
marginLeft = 40,
width = 928,
height = width,
xType = d3.scaleLinear,
yType = d3.scaleLinear,
zDomain,
fillOpacity = 0.7,
colors = d3.schemeCategory10,
} = {}) {
const X = d3.map(x, x => d3.map(data, typeof x === "function" ? x : d => d[x]));
const Y = d3.map(y, y => d3.map(data, typeof y === "function" ? y : d => d[y]));
const Z = d3.map(data, z);
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain);
const I = d3.range(Z.length).filter(i => zDomain.has(Z[i]));
const cellWidth = (width - marginLeft - marginRight - (X.length - 1) * padding) / X.length;
const cellHeight = (height - marginTop - marginBottom - (Y.length - 1) * padding) / Y.length;
const xScales = X.map(X => xType(d3.extent(X), [0, cellWidth]));
const yScales = Y.map(Y => yType(d3.extent(Y), [cellHeight, 0]));
const zScale = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisBottom().ticks(cellWidth / 50);
const yAxis = d3.axisLeft().ticks(cellHeight / 35);
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.selectAll("g")
.data(yScales)
.join("g")
.attr("transform", (d, i) => `translate(0,${i * (cellHeight + padding)})`)
.each(function(yScale) { return d3.select(this).call(yAxis.scale(yScale)); })
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1));
svg.append("g")
.selectAll("g")
.data(xScales)
.join("g")
.attr("transform", (d, i) => `translate(${i * (cellWidth + padding)},${height - marginBottom - marginTop})`)
.each(function(xScale) { return d3.select(this).call(xAxis.scale(xScale)); })
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("y2", -height + marginTop + marginBottom)
.attr("stroke-opacity", 0.1))
const cell = svg.append("g")
.selectAll("g")
.data(d3.cross(d3.range(X.length), d3.range(Y.length)))
.join("g")
.attr("fill-opacity", fillOpacity)
.attr("transform", ([i, j]) => `translate(${i * (cellWidth + padding)},${j * (cellHeight + padding)})`);
cell.append("rect")
.attr("fill", "none")
.attr("stroke", "currentColor")
.attr("width", cellWidth)
.attr("height", cellHeight);
cell.each(function([x, y]) {
d3.select(this).selectAll("circle")
.data(I.filter(i => !isNaN(X[x][i]) && !isNaN(Y[y][i])))
.join("circle")
.attr("r", 3.5)
.attr("cx", i => xScales[x](X[x][i]))
.attr("cy", i => yScales[y](Y[y][i]))
.attr("fill", i => zScale(Z[i]));
});
if (x === y) svg.append("g")
.attr("font-size", 10)
.attr("font-family", "sans-serif")
.attr("font-weight", "bold")
.selectAll("text")
.data(x)
.join("text")
.attr("transform", (d, i) => `translate(${i * (cellWidth + padding)},${i * (cellHeight + padding)})`)
.attr("x", padding / 2)
.attr("y", padding / 2)
.attr("dy", ".71em")
.text(d => d);
return Object.assign(svg.node(), {scales: {color: zScale}});
}