ScatterplotMatrix = function (
data,
{
dimensions = Array.from(d3.intersection(...data.map(Object.keys)).keys()),
reversed = [],
z = undefined
} = {}
) {
const ordinal = dimensions.filter(
(dim) => typeof data.find((d) => d[dim] != null)[dim] !== "number"
);
const scales = d3.rollup(
data.flatMap((d) =>
dimensions.map((dimension) => ({ dimension, value: d[dimension] }))
),
(v) =>
(ordinal.includes(v[0].dimension)
? d3
.scalePoint()
.domain([...new Set(v.map((d) => d.value))].sort())
.padding(0.6)
: d3.scaleLinear().domain(d3.extent(v, (d) => d.value))
).range(reversed.includes(v[0].dimension) ? [0.95, 0.05] : [0.05, 0.95]),
(d) => d.dimension
);
const scaled = data.map((d) => ({
...d,
...Object.fromEntries(
dimensions.map((dimension) => [
`_${dimension}`,
scales.get(dimension)(d[dimension])
])
)
}));
const dots = d3.cross(scales.keys(), scales.keys()).flatMap(([dimx, dimy]) =>
scaled.map((d) => ({
...d,
dimx,
dimy,
x: d[`_${dimx}`],
y: d[`_${dimy}`],
type: "dot"
}))
);
const ticksX = d3
.cross(scales.keys(), scales.keys())
.flatMap(([dimx, dimy]) => {
const s = scales.get(dimx);
const ticks = ordinal.includes(dimx) ? s.domain() : s.ticks(5);
return ticks.map((value) => ({
dimx,
dimy,
value,
x: scales.get(dimx)(value),
type: "tick"
}));
});
const ticksY = d3
.cross(scales.keys(), scales.keys())
.flatMap(([dimx, dimy]) => {
const s = scales.get(dimy);
const ticks = ordinal.includes(dimy) ? s.domain() : s.ticks(5);
return ticks.map((value) => ({
dimx,
dimy,
value,
y: scales.get(dimy)(value),
type: "tick"
}));
});
const labels = Array.from(scales.keys(), (dim) => ({
dimx: dim,
dimy: dim,
label: dim,
type: "label"
}));
const elements = [].concat(dots).concat(ticksX).concat(ticksY).concat(labels);
return Plot.plot({
facet: {
data: elements,
x: "dimx",
y: "dimy"
},
fx: {
domain: dimensions,
axis: null
},
fy: {
domain: dimensions,
tick: 20,
axis: null
},
x: {
axis: null,
domain: [0, 1]
},
y: {
axis: null,
domain: [0, 1]
},
marks: [
// grey grid
Plot.tickX(elements, {
x: "x",
filter: (d) => d.type === "tick",
strokeWidth: 0.5,
stroke: "#ccc"
}),
Plot.tickY(elements, {
y: "y",
filter: (d) => d.type === "tick" && d.dimx !== d.dimy,
strokeWidth: 0.5,
stroke: "#ccc"
}),
// black frame
Plot.frame(),
// colored data dots (penguins!)
Plot.dot(elements, {
filter: (d) => d.type === "dot" && d.dimx !== d.dimy,
x: "x",
y: "y",
fill: z ?? "black",
r: 2,
fillOpacity: 0.7
}),
// distributions
Plot.rectY(
elements,
normalizeMaxY(
Plot.stackY(
Plot.binX(
{ y: "count" },
{
filter: (d) => d.type === "dot" && d.dimx === d.dimy,
x: "x",
fill: z ?? "black",
maxHeight: 0.8
}
)
)
)
),
// tick marks, on the X axis
Plot.text(elements, {
filter: (d) =>
d.type === "tick" && d.dimy === dimensions[dimensions.length - 1],
x: "x",
y: () => 0,
text: "value",
dy: 12
}),
// tick marks, on the Y axis
Plot.text(elements, {
filter: (d) => d.type === "tick" && d.dimx === dimensions[0],
y: "y",
x: () => 0,
text: "value",
textAnchor: "end",
dx: -4
}),
// dimension labels // text: label filters implicitely
Plot.text(elements, {
filter: (d) => d.type === "label",
y: () => 0.94,
x: () => 0.03,
text: "label",
textAnchor: "start",
fontWeight: "bold"
})
],
marginLeft: 50,
marginRight: 30,
marginTop: 10,
marginBottom: 30,
width: 780,
height: 700
});
}