Public
Edited
Feb 27, 2023
3 forks
Importers
32 stars
Insert cell
Insert cell
Insert cell
ScatterplotMatrix(penguins, {
dimensions: [
"culmen_length_mm",
"culmen_depth_mm",
"flipper_length_mm",
"body_mass_g",
"island"
],
z: "species",
transform: normalizeMaxY
})
Insert cell
ScatterplotMatrix = function (
data,
{
dimensions = Array.from(d3.intersection(...data.map(Object.keys)).keys()), // Dimensions to display
reversed = [], // Maybe some dimensions need to be reversed
z = undefined // Dimension used for color
} = {}
) {
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
});
}
Insert cell
function normalizeMaxY({ y, maxHeight = 1, ...options }) {
const [H1, setH1] = Plot.column(y);
const [H2, setH2] = Plot.column(y);
options = Plot.transform(options, function (data, facets) {
const Y1 = options.y1.transform();
const Y2 = options.y2.transform();
const H1 = new Array(data.length);
const H2 = new Array(data.length);
setH1(H1);
setH2(H2);
for (const index of facets) {
const M = maxHeight / (d3.max(index, (i) => Y2[i]) || 1);
for (const i of index) {
H1[i] = Y1[i] * M;
H2[i] = Y2[i] * M;
}
}
return { data, facets };
});
return { ...options, y1: H1, y2: H2 };
}
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more