Skip to content

explicit facet option in a mark #450

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 10, 2021
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,15 @@ The following *facet* constant options are also supported:
* facet.**marginLeft** - the left margin
* facet.**grid** - if true, draw grid lines for each facet

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).
Faceting can be explicitly enabled or disabled on a mark with the *facet* option, which accepts the following values:

* *null* - (or false) disable faceting for this mark
* *include* - enable faceting for this mark (shorthand: *true*)
* *exclude* - enable exclusion faceting for this mark (each facet receives all the data except the facet’s subset)

By default, marks whose data is strictly equal to (`===`) the facet data will be filtered within each facet to show the current facet’s subset (*include*), whereas other marks will be repeated across facets (*null*).

The data in faceted marks must have the same cardinality as the facet data (and should match its order).

```js
Plot.plot({
Expand All @@ -382,12 +390,14 @@ Plot.plot({
},
marks: {
Plot.frame(), // draws an outline around each facet
Plot.dot(penguins.slice(), {x: "culmen_length_mm", y: "culmen_depth_mm", fill: "#eee"}), // draws all penguins on each facet
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", fill: "#eee", facet: "exclude"}), // draws excluded penguins on each facet
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm"}) // draws only the current facet’s subset
}
})
```

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.

## Marks

[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).
Expand Down
27 changes: 17 additions & 10 deletions src/facet.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {cross, groups, InternMap} from "d3";
import {cross, difference, groups, InternMap} from "d3";
import {create} from "d3";
import {Mark, values, first, second} from "./mark.js";
import {Mark, range, values, first, second} from "./mark.js";

export function facets(data, {x, y, ...options}, marks) {
return x === undefined && y === undefined
Expand Down Expand Up @@ -36,26 +36,33 @@ class Facet extends Mark {
for (const facetKey of facetsKeys) {
marksIndexByFacet.set(facetKey, new Array(this.marks.length));
}
let facetsExclude;
for (let i = 0; i < this.marks.length; ++i) {
const mark = this.marks[i];
const markFacets = mark.data === this.data ? facetsIndex : undefined;
const {index, channels} = mark.initialize(markFacets);
const {facet} = mark;
const markFacets = facet === "auto" ? mark.data === this.data ? facetsIndex : undefined
: facet === "include" ? facetsIndex
: facet === "exclude" ? facetsExclude || (facetsExclude = facetsIndex.map(f => Uint32Array.from(difference(index, f))))
: undefined;
if (markFacets && range(mark.data).length !== index.length) {
throw new Error("faceted mark data must match facet data length");
}
const {index: I, channels} = mark.initialize(markFacets);
// If an index is returned by mark.initialize, its structure depends on
// whether or not faceting has been applied: it is a flat index ([0, 1, 2,
// …]) when not faceted, and a nested index ([[0, 1, …], [2, 3, …], …])
// when faceted. Faceting is only applied if the mark data is the same as
// the facet’s data.
if (index !== undefined) {
// when faceted.
if (I !== undefined) {
if (markFacets) {
for (let j = 0; j < facetsKeys.length; ++j) {
marksIndexByFacet.get(facetsKeys[j])[i] = index[j];
marksIndexByFacet.get(facetsKeys[j])[i] = I[j];
}
marksIndex[i] = []; // implicit empty index for sparse facets
} else {
for (let j = 0; j < facetsKeys.length; ++j) {
marksIndexByFacet.get(facetsKeys[j])[i] = index;
marksIndexByFacet.get(facetsKeys[j])[i] = I;
}
marksIndex[i] = index;
marksIndex[i] = I;
}
}
for (const [, channel] of channels) {
Expand Down
4 changes: 3 additions & 1 deletion src/mark.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ const TypedArray = Object.getPrototypeOf(Uint8Array);
const objectToString = Object.prototype.toString;

export class Mark {
constructor(data, channels = [], options = {}) {
constructor(data, channels = [], {facet = "auto", ...options} = {}) {
const names = new Set();
this.data = data;
this.facet = !!facet && keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]);

const {transform} = maybeTransform(options);
this.transform = transform;
this.channels = channels.filter(channel => {
Expand Down
Loading