Skip to content

Commit 955708a

Browse files
committed
facet filter option
1 parent b3cdc4a commit 955708a

10 files changed

+11816
-79
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,16 @@ 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
703+
704+
The facet filter option can be one of:
705+
* *eq* (default) - the data points shown in each facet are those that exactly match the facet value
706+
* *lte* - the data points shown in each facet are those that are lower than or equal to the facet value
707+
* *gte* - the data points shown in each facet are those that are greater than or equal to the facet value
708+
* *lt* - the data points shown in each facet are those that are lower than the facet value
709+
* *gt* - the data points shown in each facet are those that are greater than the facet value
710+
* a function which takes as input the value of the data point and the facet value, and returns whether the data point is present in the facet
711+
702712

703713
```js
704714
Plot.plot({

src/facet.js

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {cross, groups, InternMap} from "d3";
2+
import {isObject, range, keyword} from "./options.js";
3+
4+
// facet filter, by mark
5+
export function filterFacets({xFilter, yFilter}, facetChannels) {
6+
if (xFilter && yFilter) {
7+
const {
8+
fx: {value: vx},
9+
fy: {value: vy}
10+
} = facetChannels;
11+
const I = range(vx);
12+
const sy = new Set(vy);
13+
return Array.from(new Set(vx), (key) => {
14+
const J = xFilter(I, vx, key);
15+
return Array.from(sy, (key) => yFilter(J, vy, key));
16+
});
17+
}
18+
if (xFilter) {
19+
const {value} = facetChannels.fx;
20+
const I = range(value);
21+
return Array.from(new Set(value), (key) => xFilter(I, value, key));
22+
}
23+
if (yFilter) {
24+
const {value} = facetChannels.fy;
25+
const I = range(value);
26+
return Array.from(new Set(value), (key) => yFilter(I, value, key));
27+
}
28+
}
29+
30+
export function maybeFacet(facet) {
31+
if (facet == null || facet === false) return null;
32+
if (facet === true) return "include";
33+
if (typeof facet === "string") return keyword(facet, "facet", ["auto", "include", "exclude"]);
34+
// local facets can be defined as facet: {x: accessor, xFilter: "lte"}
35+
if (!isObject(facet)) throw new Error(`Unsupported facet ${facet}`);
36+
const {xFilter, yFilter} = facet;
37+
return {
38+
...(xFilter !== undefined && {xFilter: maybeFacetFilter(xFilter, "x")}),
39+
...(yFilter !== undefined && {yFilter: maybeFacetFilter(yFilter, "y")})
40+
};
41+
}
42+
43+
function maybeFacetFilter(filter = "eq", x /* string */) {
44+
if (typeof filter === "function") return facetFunction(filter);
45+
switch (`${filter}`.toLowerCase()) {
46+
case "lt":
47+
return facetLt;
48+
case "lte":
49+
return facetLte;
50+
case "gt":
51+
return facetGt;
52+
case "gte":
53+
return facetGte;
54+
case "eq":
55+
return facetEq;
56+
}
57+
throw new Error(`invalid ${x} filter: ${filter}`);
58+
}
59+
60+
function facetFunction(f) {
61+
return (I, T, facet) => {
62+
return I.filter((i) => f(T[i], facet));
63+
};
64+
}
65+
66+
function facetLt(I, T, facet) {
67+
return I.filter((i) => T[i] < facet);
68+
}
69+
70+
function facetLte(I, T, facet) {
71+
return I.filter((i) => T[i] <= facet);
72+
}
73+
74+
function facetGt(I, T, facet) {
75+
return I.filter((i) => T[i] > facet);
76+
}
77+
78+
function facetGte(I, T, facet) {
79+
return I.filter((i) => T[i] >= facet);
80+
}
81+
82+
function facetEq(I, T, facet) {
83+
return I.filter((i) => T[i] === facet);
84+
}
85+
86+
// Unlike facetGroups, which returns groups in order of input data, this returns
87+
// keys in order of the associated scale’s domains.
88+
export function facetKeys({fx, fy}) {
89+
return fx && fy ? cross(fx.domain(), fy.domain()) : fx ? fx.domain() : fy.domain();
90+
}
91+
92+
// Returns an array of [[key1, index1], [key2, index2], …] representing the data
93+
// indexes associated with each facet. For two-dimensional faceting, each key
94+
// is a two-element array; see also facetMap.
95+
export function facetGroups(index, {fx, fy}) {
96+
return fx && fy ? facetGroup2(index, fx, fy) : fx ? facetGroup1(index, fx) : facetGroup1(index, fy);
97+
}
98+
99+
function facetGroup1(index, {value: F}) {
100+
return groups(index, (i) => F[i]);
101+
}
102+
103+
function facetGroup2(index, {value: FX}, {value: FY}) {
104+
return groups(
105+
index,
106+
(i) => FX[i],
107+
(i) => FY[i]
108+
).flatMap(([x, xgroup]) => xgroup.map(([y, ygroup]) => [[x, y], ygroup]));
109+
}
110+
111+
// This must match the key structure returned by facetGroups.
112+
export function facetTranslate(fx, fy) {
113+
return fx && fy
114+
? ([kx, ky]) => `translate(${fx(kx)},${fy(ky)})`
115+
: fx
116+
? (kx) => `translate(${fx(kx)},0)`
117+
: (ky) => `translate(0,${fy(ky)})`;
118+
}
119+
120+
export function facetMap({fx, fy}) {
121+
return new (fx && fy ? FacetMap2 : FacetMap)();
122+
}
123+
124+
class FacetMap {
125+
constructor() {
126+
this._ = new InternMap();
127+
}
128+
has(key) {
129+
return this._.has(key);
130+
}
131+
get(key) {
132+
return this._.get(key);
133+
}
134+
set(key, value) {
135+
return this._.set(key, value), this;
136+
}
137+
}
138+
139+
// A Map-like interface that supports paired keys.
140+
class FacetMap2 extends FacetMap {
141+
has([key1, key2]) {
142+
const map = super.get(key1);
143+
return map ? map.has(key2) : false;
144+
}
145+
get([key1, key2]) {
146+
const map = super.get(key1);
147+
return map && map.get(key2);
148+
}
149+
set([key1, key2], value) {
150+
const map = super.get(key1);
151+
if (map) map.set(key2, value);
152+
else super.set(key1, new InternMap([[key2, value]]));
153+
return this;
154+
}
155+
}

src/plot.js

+7-79
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import {cross, difference, groups, InternMap, select} from "d3";
1+
import {difference, select} from "d3";
22
import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js";
33
import {Channel, Channels, channelDomain, valueObject} from "./channel.js";
44
import {Context, create} from "./context.js";
55
import {defined} from "./defined.js";
66
import {Dimensions} from "./dimensions.js";
7+
import {facetGroups, facetKeys, facetMap, facetTranslate, filterFacets, maybeFacet} from "./facet.js";
78
import {Legends, exposeLegends} from "./legends.js";
89
import {
910
arrayify,
1011
isDomainSort,
1112
isScaleOptions,
1213
isTypedArray,
13-
keyword,
1414
map,
1515
maybeNamed,
1616
range,
@@ -433,7 +433,7 @@ export function plot(options = {}) {
433433
for (const mark of marks) {
434434
if (stateByMark.has(mark)) throw new Error("duplicate mark; each mark must be unique");
435435
const markFacets =
436-
facetsIndex === undefined
436+
facetsIndex === undefined || mark.facet === null
437437
? undefined
438438
: mark.facet === "auto"
439439
? mark.data === facet.data
@@ -443,7 +443,8 @@ export function plot(options = {}) {
443443
? facetsIndex
444444
: mark.facet === "exclude"
445445
? facetsExclude || (facetsExclude = facetsIndex.map((f) => Uint32Array.from(difference(facetIndex, f))))
446-
: undefined;
446+
: filterFacets(mark.facet, facetChannels);
447+
447448
const {data, facets, channels} = mark.initialize(markFacets, facetChannels);
448449
applyScaleTransforms(channels, options);
449450
stateByMark.set(mark, {data, facets, channels});
@@ -668,10 +669,7 @@ export class Mark {
668669
this.sort = isDomainSort(sort) ? sort : null;
669670
this.initializer = initializer(options).initializer;
670671
this.transform = this.initializer ? options.transform : basic(options).transform;
671-
this.facet =
672-
facet == null || facet === false
673-
? null
674-
: keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]);
672+
this.facet = maybeFacet(facet, data);
675673
channels = maybeNamed(channels);
676674
if (extraChannels !== undefined) channels = {...maybeNamed(extraChannels), ...channels};
677675
if (defaults !== undefined) channels = {...styles(this, options, defaults), ...channels};
@@ -693,6 +691,7 @@ export class Mark {
693691

694692
if (this.transform != null) {
695693
// If the mark has a transform, reindex facets that overlap
694+
// TODO optimize
696695
const overlap = new Set();
697696
const reindex = new Map();
698697
let j = data.length;
@@ -834,77 +833,6 @@ function nolabel(axis) {
834833
: Object.assign(Object.create(axis), {label: undefined});
835834
}
836835

837-
// Unlike facetGroups, which returns groups in order of input data, this returns
838-
// keys in order of the associated scale’s domains.
839-
function facetKeys({fx, fy}) {
840-
return fx && fy ? cross(fx.domain(), fy.domain()) : fx ? fx.domain() : fy.domain();
841-
}
842-
843-
// Returns an array of [[key1, index1], [key2, index2], …] representing the data
844-
// indexes associated with each facet. For two-dimensional faceting, each key
845-
// is a two-element array; see also facetMap.
846-
function facetGroups(index, {fx, fy}) {
847-
return fx && fy ? facetGroup2(index, fx, fy) : fx ? facetGroup1(index, fx) : facetGroup1(index, fy);
848-
}
849-
850-
function facetGroup1(index, {value: F}) {
851-
return groups(index, (i) => F[i]);
852-
}
853-
854-
function facetGroup2(index, {value: FX}, {value: FY}) {
855-
return groups(
856-
index,
857-
(i) => FX[i],
858-
(i) => FY[i]
859-
).flatMap(([x, xgroup]) => xgroup.map(([y, ygroup]) => [[x, y], ygroup]));
860-
}
861-
862-
// This must match the key structure returned by facetGroups.
863-
function facetTranslate(fx, fy) {
864-
return fx && fy
865-
? ([kx, ky]) => `translate(${fx(kx)},${fy(ky)})`
866-
: fx
867-
? (kx) => `translate(${fx(kx)},0)`
868-
: (ky) => `translate(0,${fy(ky)})`;
869-
}
870-
871-
function facetMap({fx, fy}) {
872-
return new (fx && fy ? FacetMap2 : FacetMap)();
873-
}
874-
875-
class FacetMap {
876-
constructor() {
877-
this._ = new InternMap();
878-
}
879-
has(key) {
880-
return this._.has(key);
881-
}
882-
get(key) {
883-
return this._.get(key);
884-
}
885-
set(key, value) {
886-
return this._.set(key, value), this;
887-
}
888-
}
889-
890-
// A Map-like interface that supports paired keys.
891-
class FacetMap2 extends FacetMap {
892-
has([key1, key2]) {
893-
const map = super.get(key1);
894-
return map ? map.has(key2) : false;
895-
}
896-
get([key1, key2]) {
897-
const map = super.get(key1);
898-
return map && map.get(key2);
899-
}
900-
set([key1, key2], value) {
901-
const map = super.get(key1);
902-
if (map) map.set(key2, value);
903-
else super.set(key1, new InternMap([[key2, value]]));
904-
return this;
905-
}
906-
}
907-
908836
// expands an array or typed array to make room for n values
909837
function expandArray(values, n) {
910838
if (isTypedArray(values)) {

test/data/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ https://www.census.gov/retail/mrts/historic_releases.html
139139
U.S. Census Bureau (?)
140140
https://observablehq.com/@d3/diverging-bar-chart
141141

142+
## walmart
143+
Thomas J. Holmes, University of Minnesota, Federal Reserve Bank of Minneapolis, and NBER
144+
https://users.econ.umn.edu/~holmes/data/WalMart/
145+
142146
## wealth-britain.csv
143147
U.K. Office for National Statistics
144148
A recreation of “Who owns Britain?” by Richard Speigal; proportion plot chart type by Stephanie Evergreen

0 commit comments

Comments
 (0)