Skip to content

Commit 84c2696

Browse files
committed
flatten the mark facet/facet filter API and simplify the code
1 parent eebd122 commit 84c2696

File tree

8 files changed

+49
-51
lines changed

8 files changed

+49
-51
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -699,9 +699,10 @@ Faceting can be explicitly enabled or disabled on a mark with the *facet* option
699699
* *include* (or true) - draw the subset of the mark’s data in the current facet
700700
* *exclude* - draw the subset of the mark’s data *not* in the current facet
701701
* null (or false) - repeat this mark’s data across all facets (i.e., no faceting)
702-
* an object with a xFilter or yFilter option
703702

704-
The facet filter option can be one of:
703+
By default, the data points shown in each facet are those that exactly match the facet value. Use the **xFilter** and **yFilter** options to change that behavior for, respectively, the *x* and *y* facets:
704+
705+
The xFilter or yFilter option can be one of:
705706
* *eq* (default) - the data points shown in each facet are those that exactly match the facet value
706707
* *lte* - the data points shown in each facet are those that are lower than or equal to the facet value
707708
* *gte* - the data points shown in each facet are those that are greater than or equal to the facet value
@@ -725,15 +726,16 @@ Plot.plot({
725726

726727
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.)
727728

728-
Alternatively, facets can be defined from each mark, by specifying the facet option as an object with an x or y channel option.
729+
Alternatively, facets can be defined from each mark by specifying the channel options **fx** or **fy**.
729730

730731
```js
731732
Plot.plot({
732733
marks: [
733734
Plot.dot(penguins, {
734735
x: "culmen_length_mm",
735736
y: "culmen_depth_mm",
736-
facet: {x: "sex", y: "island"}
737+
fx: "sex",
738+
fy: "island"
737739
})
738740
]
739741
})

src/facet.js

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,31 @@
1-
import {keyword, isObject, isTypedArray, labelof, range, slice, valueof} from "./options.js";
1+
import {keyword, isTypedArray, range, slice} from "./options.js";
22
import {warn} from "./warnings.js";
33

44
// facet filter, by mark
5-
export function filterFacets(facetCells, {x, xFilter, y, yFilter}, facetChannels) {
6-
const vx = x != null ? x : facetChannels?.fx?.value;
7-
const vy = y != null ? y : facetChannels?.fy?.value;
5+
export function filterFacets(facetCells, {xFilter, yFilter}, {fx, fy}, facetChannels) {
6+
const vx = fx != null ? fx.value : facetChannels?.fx?.value;
7+
const vy = fy != null ? fy.value : facetChannels?.fy?.value;
8+
if (!vx && !vy) return; // ignore facet filter without facets
89
const I = range(vx || vy);
910
return facetCells.map(([x, y]) => {
1011
let index = I;
11-
if (xFilter && vx) index = xFilter(index, vx, x);
12-
if (yFilter && vy) index = yFilter(index, vy, y);
12+
if (vx) index = facetFilter(xFilter, "x")(index, vx, x);
13+
if (vy) index = facetFilter(yFilter, "y")(index, vy, y);
1314
return index;
1415
});
1516
}
1617

17-
export function maybeFacet(facet, data) {
18-
if (facet == null || facet === false) return null;
18+
export function maybeFacet(options) {
19+
const {fx, xFilter, fy, yFilter, facet = "auto"} = options;
20+
if (fx !== undefined || fy !== undefined || xFilter !== undefined || yFilter !== undefined)
21+
return {x: fx, xFilter, y: fy, yFilter};
22+
if (facet === null || facet === false) return null;
1923
if (facet === true) return "include";
2024
if (typeof facet === "string") return keyword(facet, "facet", ["auto", "include", "exclude"]);
21-
// local facets can be defined as facet: {x: accessor, xFilter: "lte"}
22-
if (!isObject(facet)) throw new Error(`Unsupported facet ${facet}`);
23-
const {x, xFilter = "eq", y, yFilter = "eq"} = facet;
24-
let xv, yv;
25-
if (x !== undefined) {
26-
xv = valueof(data, x);
27-
if (xv != null) xv.label = labelof(x);
28-
}
29-
if (y !== undefined) {
30-
yv = valueof(data, y);
31-
if (yv != null) yv.label = labelof(y);
32-
}
33-
return {
34-
...(xv && {x: xv}),
35-
...(yv && {y: yv}),
36-
xFilter: maybeFacetFilter(xFilter, "x"),
37-
yFilter: maybeFacetFilter(yFilter, "y")
38-
};
25+
if (facet) throw new Error(`Unsupported facet ${facet}`);
3926
}
4027

41-
function maybeFacetFilter(filter = "eq", x /* string */) {
28+
function facetFilter(filter = "eq", x) {
4229
if (typeof filter === "function") return facetFunction(filter);
4330
switch (`${filter}`.toLowerCase()) {
4431
case "lt":

src/plot.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,16 @@ export function plot(options = {}) {
420420
if (mark.facet.x !== undefined) {
421421
const channels = channelsByScale.get("fx") || [];
422422
channelsByScale.set("fx", channels);
423-
channels.push(Channel(mark.data, {value: mark.facet.x, scale: "fx"}));
423+
const channel = Channel(mark.data, {value: mark.facet.x, scale: "fx"});
424+
stateByMark.get(mark).fx = channel;
425+
channels.push(channel);
424426
}
425427
if (mark.facet.y !== undefined) {
426428
const channels = channelsByScale.get("fy") || [];
427429
channelsByScale.set("fy", channels);
428-
channels.push(Channel(mark.data, {value: mark.facet.y, scale: "fy"}));
430+
const channel = Channel(mark.data, {value: mark.facet.y, scale: "fy"});
431+
stateByMark.get(mark).fy = channel;
432+
channels.push(channel);
429433
}
430434
}
431435
}
@@ -449,9 +453,10 @@ export function plot(options = {}) {
449453
let facetIndex; // index over the facet data, e.g. [0, 1, 2, 3, …]
450454
let facetsExclude; // lazily-constructed opposite of facetsIndex
451455
for (const mark of marks) {
452-
if (mark.facet === null) {
453-
continue;
454-
}
456+
if (mark.facet === null) continue;
457+
const state = stateByMark.get(mark);
458+
459+
// top-level facet with no filter: optimize by computing facetIndex once
455460
if (typeof mark.facet === "string") {
456461
if (!facet || (mark.facet === "auto" && mark.data !== facet.data)) continue;
457462
if (!facetsIndex) {
@@ -461,10 +466,12 @@ export function plot(options = {}) {
461466
facetIndex.filter((i) => (!fx || fx.value[i] === kx) && (!fy || fy.value[i] === ky))
462467
);
463468
}
464-
stateByMark.get(mark).facetsIndex = facetsIndex;
469+
state.facetsIndex = facetsIndex;
465470
continue;
466471
}
467-
stateByMark.get(mark).facetsIndex = filterFacets(facetCells, mark.facet, facetChannels);
472+
473+
// Otherwise, compute specific indexes for that mark
474+
if (facetCells) state.facetsIndex = filterFacets(facetCells, mark.facet, state, facetChannels);
468475
}
469476

470477
// A facet is empty if none of the faceted index has contents for any mark
@@ -495,7 +502,7 @@ export function plot(options = {}) {
495502
// Warn for the common pitfall of wanting to facet mapped data.
496503
if (
497504
facet &&
498-
facetCells.length > 1 && // non-trivial faceting
505+
facetCells?.length > 1 && // non-trivial faceting
499506
mark.facet === "auto" && // no explicit mark facet option
500507
mark.data !== facet.data && // mark not implicitly faceted (different data)
501508
arrayify(mark.data)?.length === facetChannels.size // mark data seems parallel to facet data
@@ -718,12 +725,12 @@ export function plot(options = {}) {
718725

719726
export class Mark {
720727
constructor(data, channels = {}, options = {}, defaults) {
721-
const {facet = "auto", sort, dx, dy, clip, channels: extraChannels} = options;
728+
const {sort, dx, dy, clip, channels: extraChannels} = options;
722729
this.data = data;
723730
this.sort = isDomainSort(sort) ? sort : null;
724731
this.initializer = initializer(options).initializer;
725732
this.transform = this.initializer ? options.transform : basic(options).transform;
726-
this.facet = maybeFacet(facet, data);
733+
this.facet = maybeFacet(options, data);
727734
channels = maybeNamed(channels);
728735
if (extraChannels !== undefined) channels = {...maybeNamed(extraChannels), ...channels};
729736
if (defaults !== undefined) channels = {...styles(this, options, defaults), ...channels};

test/plots/multiplication-table.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,20 @@ export default async function () {
99
color: {type: "ordinal", scheme: "tableau10"},
1010
fx: {axis: "top"},
1111
marks: [
12-
Plot.rect(nums, {facet: {y: nums}, fill: nums}),
12+
Plot.rect(nums, {fy: nums, fill: nums}),
1313
Plot.dot(nums, {
1414
frameAnchor: "middle",
1515
r: 19,
16-
fill: (d) => d,
16+
fill: nums,
1717
stroke: "white",
18-
facet: {x: (d) => d}
18+
fx: nums
1919
}),
2020
Plot.text(d3.cross(nums, nums), {
2121
frameAnchor: "middle",
2222
text: ([a, b]) => a * b,
2323
fill: "white",
24-
facet: {x: (d) => d[1], y: (d) => d[0]}
24+
fx: (d) => d[1],
25+
fy: (d) => d[0]
2526
})
2627
]
2728
});

test/plots/penguins-facet-annotated-x.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ export default async function () {
1515
{
1616
y: "species",
1717
fill: "sex",
18-
facet: {x: "island"}
18+
fx: "island"
1919
}
2020
)
2121
),
2222
Plot.text(["Torgersen Island\nonly has\nAdelie\npenguins!"], {
2323
frameAnchor: "right",
2424
dx: -10,
25-
facet: {x: ["Torgersen"]}
25+
fx: ["Torgersen"]
2626
})
2727
]
2828
});

test/plots/penguins-facet-annotated.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ export default async function () {
1717
{
1818
y: "species",
1919
fill: "sex",
20-
facet: {y: "island"}
20+
fy: "island"
2121
}
2222
)
2323
),
2424
Plot.text(["↞ Torgersen Island only has Adelie penguins!"], {
2525
frameAnchor: "top-right",
2626
dx: -10,
27-
facet: {y: ["Torgersen"]}
27+
fy: ["Torgersen"]
2828
})
2929
]
3030
});

test/plots/walmart-expansion-bar.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export default async function () {
2020
{x: "count"},
2121
{
2222
fill: "year",
23-
facet: {y: "year", yFilter: "lte"},
23+
fy: "year",
24+
yFilter: "lte",
2425
stroke: "white",
2526
offset: "expand"
2627
}

test/plots/walmart-expansion.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default async function () {
2424
y: "1",
2525
fill: "#ccc",
2626
r: 2,
27-
facet: {yFilter: "lt"}
27+
yFilter: "lt"
2828
}),
2929
Plot.dot(walmart, {
3030
x: "0",

0 commit comments

Comments
 (0)