Skip to content

Commit 8aafa7c

Browse files
authored
explicit facet option in a mark (#450)
* make the data.slice() technique secondary in README * computes the facet exclusion index only once.
1 parent 8bece3d commit 8aafa7c

File tree

6 files changed

+29
-700
lines changed

6 files changed

+29
-700
lines changed

README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,12 @@ The following *facet* constant options are also supported:
376376
* facet.**marginLeft** - the left margin
377377
* facet.**grid** - if true, draw grid lines for each facet
378378

379-
Marks whose data is strictly equal to (`===`) the facet data will be filtered within each facet to show the current facet’s subset, whereas other marks will be repeated across facets. You can disable faceting for an individual mark by giving it a shallow copy of the data, say using [*array*.slice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice).
379+
Faceting can be explicitly enabled or disabled on a mark with the *facet* option, which accepts the following values:
380+
381+
* *auto* (default) - marks whose data is strictly equal to (`===`) the facet data will be filtered within each facet to show the current facet’s subset (see *include*), whereas other marks will be repeated across facets.
382+
* *include* - enable faceting for this mark (shorthand: *true*)
383+
* *exclude* - enable exclusion faceting for this mark (each facet receives all the data except the facet’s subset)
384+
* null - (or false) disable faceting for this mark
380385

381386
```js
382387
Plot.plot({
@@ -386,12 +391,14 @@ Plot.plot({
386391
},
387392
marks: {
388393
Plot.frame(), // draws an outline around each facet
389-
Plot.dot(penguins.slice(), {x: "culmen_length_mm", y: "culmen_depth_mm", fill: "#eee"}), // draws all penguins on each facet
394+
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", fill: "#eee", facet: "exclude"}), // draws excluded penguins on each facet
390395
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm"}) // draws only the current facet’s subset
391396
}
392397
})
393398
```
394399

400+
The strict equality check means that an individual mark that receives a shallow copy of the data, say using [*array*.slice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) or [*array*.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) has faceting disabled by default.
401+
395402
## Marks
396403

397404
[Marks](https://observablehq.com/@data-workflows/plot-marks) visualize data as geometric shapes such as bars, dots, and lines. An single mark can generate multiple shapes: for example, passing a [Plot.barY](#plotbarydata-options) to [Plot.plot](#plotplotoptions) will produce a bar for each element in the associated data. Multiple marks can be layered into [plots](#plotplotoptions).

src/facet.js

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {cross, groups, InternMap} from "d3";
1+
import {cross, difference, groups, InternMap} from "d3";
22
import {create} from "d3";
33
import {Mark, values, first, second} from "./mark.js";
44

@@ -36,26 +36,30 @@ class Facet extends Mark {
3636
for (const facetKey of facetsKeys) {
3737
marksIndexByFacet.set(facetKey, new Array(this.marks.length));
3838
}
39+
let facetsExclude;
3940
for (let i = 0; i < this.marks.length; ++i) {
4041
const mark = this.marks[i];
41-
const markFacets = mark.data === this.data ? facetsIndex : undefined;
42-
const {index, channels} = mark.initialize(markFacets);
42+
const {facet} = mark;
43+
const markFacets = facet === "auto" ? mark.data === this.data ? facetsIndex : undefined
44+
: facet === "include" ? facetsIndex
45+
: facet === "exclude" ? facetsExclude || (facetsExclude = facetsIndex.map(f => Uint32Array.from(difference(index, f))))
46+
: undefined;
47+
const {index: I, channels} = mark.initialize(markFacets);
4348
// If an index is returned by mark.initialize, its structure depends on
4449
// whether or not faceting has been applied: it is a flat index ([0, 1, 2,
4550
// …]) when not faceted, and a nested index ([[0, 1, …], [2, 3, …], …])
46-
// when faceted. Faceting is only applied if the mark data is the same as
47-
// the facet’s data.
48-
if (index !== undefined) {
51+
// when faceted.
52+
if (I !== undefined) {
4953
if (markFacets) {
5054
for (let j = 0; j < facetsKeys.length; ++j) {
51-
marksIndexByFacet.get(facetsKeys[j])[i] = index[j];
55+
marksIndexByFacet.get(facetsKeys[j])[i] = I[j];
5256
}
5357
marksIndex[i] = []; // implicit empty index for sparse facets
5458
} else {
5559
for (let j = 0; j < facetsKeys.length; ++j) {
56-
marksIndexByFacet.get(facetsKeys[j])[i] = index;
60+
marksIndexByFacet.get(facetsKeys[j])[i] = I;
5761
}
58-
marksIndex[i] = index;
62+
marksIndex[i] = I;
5963
}
6064
}
6165
for (const [, channel] of channels) {

src/mark.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ const TypedArray = Object.getPrototypeOf(Uint8Array);
77
const objectToString = Object.prototype.toString;
88

99
export class Mark {
10-
constructor(data, channels = [], options = {}) {
10+
constructor(data, channels = [], {facet = "auto", ...options} = {}) {
1111
const names = new Set();
1212
this.data = data;
13+
this.facet = facet ? keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]) : null;
1314
const {transform} = maybeTransform(options);
1415
this.transform = transform;
1516
this.channels = channels.filter(channel => {

0 commit comments

Comments
 (0)