diff --git a/docs/features/marks.md b/docs/features/marks.md
index 4d9cb90cd4..7e163907e6 100644
--- a/docs/features/marks.md
+++ b/docs/features/marks.md
@@ -114,7 +114,7 @@ Plot.plot({
```
:::
-Marks may also be a function which returns an SVG element, if you wish to insert arbitrary content. (Here we use [Hypertext Literal](https://github.com/observablehq/htl) to generate an SVG gradient.)
+Marks may also be a function which returns an [SVG element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element), if you wish to insert arbitrary content. (Here we use [Hypertext Literal](https://github.com/observablehq/htl) to generate an SVG gradient.)
:::plot defer https://observablehq.com/@observablehq/plot-gradient-bars
```js
diff --git a/src/facet.js b/src/facet.js
index 5398b344e7..cc3af59ac4 100644
--- a/src/facet.js
+++ b/src/facet.js
@@ -63,11 +63,16 @@ export function facetGroups(data, {fx, fy}) {
}
export function facetTranslator(fx, fy, {marginTop, marginLeft}) {
- return fx && fy
- ? ({x, y}) => `translate(${fx(x) - marginLeft},${fy(y) - marginTop})`
- : fx
- ? ({x}) => `translate(${fx(x) - marginLeft},0)`
- : ({y}) => `translate(0,${fy(y) - marginTop})`;
+ const x = fx ? ({x}) => fx(x) - marginLeft : () => 0;
+ const y = fy ? ({y}) => fy(y) - marginTop : () => 0;
+ return function (d) {
+ if (this.tagName === "svg") {
+ this.setAttribute("x", x(d));
+ this.setAttribute("y", y(d));
+ } else {
+ this.setAttribute("transform", `translate(${x(d)},${y(d)})`);
+ }
+ };
}
// Returns an index that for each facet lists all the elements present in other
diff --git a/src/plot.js b/src/plot.js
index a7383a66c7..829c4624a4 100644
--- a/src/plot.js
+++ b/src/plot.js
@@ -317,7 +317,7 @@ export function plot(options = {}) {
}
}
}
- g?.selectChildren().attr("transform", facetTranslate);
+ g?.selectChildren().each(facetTranslate);
}
}
diff --git a/test/output/nestedFacets.html b/test/output/nestedFacets.html
new file mode 100644
index 0000000000..78280a3cce
--- /dev/null
+++ b/test/output/nestedFacets.html
@@ -0,0 +1,1008 @@
+
\ No newline at end of file
diff --git a/test/plots/index.ts b/test/plots/index.ts
index b13d1ec45b..3176991d75 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -184,6 +184,7 @@ export * from "./movies-profit-by-genre.js";
export * from "./movies-rating-by-genre.js";
export * from "./multiplication-table.js";
export * from "./music-revenue.js";
+export * from "./nested-facets.js";
export * from "./npm-versions.js";
export * from "./opacity.js";
export * from "./ordinal-bar.js";
diff --git a/test/plots/nested-facets.ts b/test/plots/nested-facets.ts
new file mode 100644
index 0000000000..fa952a4464
--- /dev/null
+++ b/test/plots/nested-facets.ts
@@ -0,0 +1,53 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export async function nestedFacets() {
+ const diamonds = await d3.csv("data/diamonds.csv", d3.autoType);
+ return Plot.plot({
+ width: 960,
+ height: 480,
+ fx: {domain: ["D", "E", "F"]},
+ color: {legend: "ramp", domain: ["IF", "SI1", "I1"]},
+ y: {domain: [51, 71.9], insetTop: 20, labelAnchor: "center"},
+ marginLeft: 40,
+ marginBottom: 40,
+ marginTop: 35,
+ marks: [
+ Plot.axisFx({anchor: "top"}),
+ Plot.frame({anchor: "top", strokeOpacity: 1}),
+ Plot.dot(diamonds, {
+ fx: "color", // outer x facet
+ y: "depth", // shared y scale
+ fill: "clarity", // shared color scale
+ render(index, {scales}, _values, {facet, ...dimensions}) {
+ const data = Array.from(index, (i) => this.data[i]); // subplot dataset as a subset of the data
+ return Plot.plot({
+ ...dimensions,
+ marginTop: 60,
+ ...scales, // shared color scale, shared y scale
+ fx: {axis: "bottom", paddingOuter: 0.1, paddingInner: 0.2}, // inner x facet
+ x: {
+ domain: scales.color.domain,
+ axis: "top",
+ labelAnchor: "left",
+ labelOffset: 16,
+ ...(index["fi"] && {label: null}),
+ grid: true,
+ tickSize: 0
+ }, // new x scale with a common domain and additional axis options
+ y: {...scales.y, grid: 4, axis: null}, // shared y scale with additional options
+ marks: [
+ Plot.frame({anchor: "bottom"}),
+ Plot.boxY(data, {
+ fx: "cut",
+ x: "clarity",
+ y: "depth",
+ fill: "clarity"
+ })
+ ]
+ }) as SVGElement;
+ }
+ })
+ ]
+ });
+}