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
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,16 @@ 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).
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, whereas other marks will be repeated across facets.

Faceting can be explicitly enabled or disabled on a mark with the *facet* option, which accepts the following values:

* *null* (default) - facet this mark if its data is strictly equal to the facet data
* *false* - disable faceting for this mark
* *true* - enable faceting for this mark
* *exclude* - enable exclusion faceting for this mark (each facet receives all the data except the facet’s subset)

If faceting is enabled, the mark’s data must have the same cardinality as the facet data (and should match its order).

```js
Plot.plot({
Expand All @@ -382,12 +391,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
25 changes: 15 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 @@ -38,24 +38,29 @@ class Facet extends Mark {
}
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 markFacets = mark.facet == null ? mark.data === this.data ? facetsIndex : undefined
: mark.facet === "exclude" ? facetsIndex.map(facet => Uint32Array.from(difference(index, facet)))
: mark.facet === true ? facetsIndex
: undefined;
if (mark.facet && 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
3 changes: 2 additions & 1 deletion src/mark.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ const TypedArray = Object.getPrototypeOf(Uint8Array);
const objectToString = Object.prototype.toString;

export class Mark {
constructor(data, channels = [], options = {}) {
constructor(data, channels = [], {facet, ...options} = {}) {
const names = new Set();
this.data = data;
this.facet = facet;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do some validation and coercion here.

I also think we can unify the types a little more. Currently:

  • "exclude" is a string,
  • null and undefined are equivalent, and
  • true and false are booleans.

If we want to be more analogous to the scale.axis option, perhaps:

  • "exclude"
  • "include", and true is shorthand for "include"
  • undefined is "include" if data is strict equal, and otherwise null to disable faceting
  • null or any other falsey value is mapped to null to disable faceting

Any other truthy value, after being coerced to a string, would throw an error. We’d use the keyword helper:

export function keyword(input, name, allowed) {

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