Skip to content

Commit 7055da1

Browse files
Filmbostock
andauthored
mark-level facets (#1085)
* define facets from each mark * allow facet: "exclude" in mark faceting this changes one unit test as it does not maintain the data order in the exclude facet. but that order was kinda arbitrary anyway. Use an explicit sort transform if necessary. * a bit more documentation * unnecessarily polish multiplication table test * stricter/looser facet validation * add TODO * remove and simplify the weird parts (j, empty facet determination) add a test plot with mark-level facets and exclude clarify comments simplify proper facet look-up for grid lines * clean up filterFacets * fix comments * regroup all facet handling, simplify * inline maybeFacet; improve backwards compatibility * minimize diff * comments * clean * fix tests * minimize diff * keep top-level facet state separate * avoid re-initializing fx and fy channels * TODO re. applyScaleTransforms * prettier * adopt InternMap; remove sorting * nullish, not undefined * style * remove TODO * fx and fy aren’t in mark.channels * remove todo * fix comment * use groups * tighten state * sparse facetsIndex * simpler facetsIndex initialization * small consolidation * compute exclude facet index earlier * simpler facet channels collection Co-authored-by: Mike Bostock <[email protected]>
1 parent b9cb875 commit 7055da1

13 files changed

+5196
-1213
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,21 @@ Plot.plot({
569569

570570
When the *include* or *exclude* facet mode is chosen, the mark data must be parallel to the facet data: the mark data must have the same length and order as the facet data. If the data are not parallel, then the wrong data may be shown in each facet. The default *auto* therefore requires strict equality (`===`) for safety, and using the facet data as mark data is recommended when using the *exclude* facet mode. (To construct parallel data safely, consider using [*array*.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) on the facet data.)
571571

572+
Alternatively, facets can be defined for each individual mark by specifying the channel options **fx** or **fy**. In that case, the **facet** option only considers the mark data, and the default *auto* setting is equivalent to *include*. Other values of the *facet* option are unchanged: null or false disable faceting, and *exclude* draws the subset of the mark’s data *not* in the current facet.
573+
574+
```js
575+
Plot.plot({
576+
marks: [
577+
Plot.dot(penguins, {
578+
x: "culmen_length_mm",
579+
y: "culmen_depth_mm",
580+
fx: "sex",
581+
fy: "island"
582+
})
583+
]
584+
})
585+
```
586+
572587
## Legends
573588

574589
Plot can generate legends for *color*, *opacity*, and *symbol* [scales](#scale-options). (An opacity scale is treated as a color scale with varying transparency.) For an inline legend, use the *scale*.**legend** option:

src/channel.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ export function Channel(data, {scale, type, value, filter, hint}) {
1616
}
1717

1818
export function Channels(descriptors, data) {
19-
return Object.fromEntries(
20-
Object.entries(descriptors).map(([name, channel]) => {
21-
return [name, Channel(data, channel)];
22-
})
23-
);
19+
return Object.fromEntries(Object.entries(descriptors).map(([name, channel]) => [name, Channel(data, channel)]));
2420
}
2521

2622
// TODO Use Float64Array for scales with numeric ranges, e.g. position?

src/plot.js

Lines changed: 233 additions & 141 deletions
Large diffs are not rendered by default.

test/output/multiplicationTable.svg

Lines changed: 644 additions & 0 deletions
Loading

test/output/penguinCulmen.svg

Lines changed: 1067 additions & 1067 deletions
Loading

test/output/penguinCulmenMarkFacet.svg

Lines changed: 2905 additions & 0 deletions
Loading

test/output/penguinFacetAnnotated.svg

Lines changed: 112 additions & 0 deletions
Loading
Lines changed: 100 additions & 0 deletions
Loading

test/plots/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,15 @@ export {default as morleyBoxplot} from "./morley-boxplot.js";
139139
export {default as moviesProfitByGenre} from "./movies-profit-by-genre.js";
140140
export {default as moviesRatingByGenre} from "./movies-rating-by-genre.js";
141141
export {default as musicRevenue} from "./music-revenue.js";
142+
export {default as multiplicationTable} from "./multiplication-table.js";
142143
export {default as ordinalBar} from "./ordinal-bar.js";
143144
export {default as penguinAnnotated} from "./penguin-annotated.js";
144145
export {default as penguinCulmen} from "./penguin-culmen.js";
145146
export {default as penguinCulmenArray} from "./penguin-culmen-array.js";
146147
export {default as penguinCulmenDelaunay} from "./penguin-culmen-delaunay.js";
147148
export {default as penguinCulmenDelaunayMesh} from "./penguin-culmen-delaunay-mesh.js";
148149
export {default as penguinCulmenDelaunaySpecies} from "./penguin-culmen-delaunay-species.js";
150+
export {default as penguinCulmenMarkFacet} from "./penguin-culmen-mark-facet.js";
149151
export {default as penguinCulmenVoronoi} from "./penguin-culmen-voronoi.js";
150152
export {default as penguinVoronoi1D} from "./penguin-voronoi-1d.js";
151153
export {default as penguinDensity} from "./penguin-density.js";
@@ -154,6 +156,8 @@ export {default as penguinDensityZ} from "./penguin-density-z.js";
154156
export {default as penguinDodge} from "./penguin-dodge.js";
155157
export {default as penguinDodgeHexbin} from "./penguin-dodge-hexbin.js";
156158
export {default as penguinDodgeVoronoi} from "./penguin-dodge-voronoi.js";
159+
export {default as penguinFacetAnnotated} from "./penguins-facet-annotated.js";
160+
export {default as penguinFacetAnnotatedX} from "./penguins-facet-annotated-x.js";
157161
export {default as penguinFacetDodge} from "./penguin-facet-dodge.js";
158162
export {default as penguinFacetDodgeIdentity} from "./penguin-facet-dodge-identity.js";
159163
export {default as penguinFacetDodgeIsland} from "./penguin-facet-dodge-island.js";

test/plots/multiplication-table.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export default async function () {
5+
const numbers = d3.range(2, 10);
6+
return Plot.plot({
7+
height: 450,
8+
width: 450,
9+
padding: 0,
10+
color: {type: "categorical"},
11+
fx: {axis: "top", tickSize: 6},
12+
fy: {tickSize: 6},
13+
marks: [
14+
// This rect is faceted by y and repeated across x, and hence all rects in
15+
// a row have the same fill. With rect, the default definitions of x1, x2,
16+
// y1, and y2 will fill the entire frame, similar to Plot.frame.
17+
Plot.rect(numbers, {
18+
fy: numbers,
19+
fill: numbers,
20+
inset: 1
21+
}),
22+
// This dot is faceted by x and repeated across y, and hence all dots in a
23+
// column have the same fill. With dot, the default definitions of x and y
24+
// would assume that the data is a tuple [x, y], so we set the frameAnchor
25+
// to middle to draw one dot in the center of each frame.
26+
Plot.dot(numbers, {
27+
frameAnchor: "middle",
28+
r: 19,
29+
fx: numbers,
30+
fill: numbers,
31+
stroke: "white"
32+
}),
33+
// This text is faceted by x and y, and hence we need the cross product of
34+
// the numbers. Again there is just one text mark per facet.
35+
Plot.text(d3.cross(numbers, numbers), {
36+
frameAnchor: "middle",
37+
text: ([x, y]) => x * y,
38+
fill: "white",
39+
fx: ([x]) => x,
40+
fy: ([, y]) => y
41+
})
42+
]
43+
});
44+
}

0 commit comments

Comments
 (0)