| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- import {InternMap, cross, rollup, sum} from "d3";
- import {keyof, map, range} from "./options.js";
- import {createScales} from "./scales.js";
- // Returns an array of {x?, y?, i} objects representing the facet domain.
- export function createFacets(channelsByScale, options) {
- const {fx, fy} = createScales(channelsByScale, options);
- const fxDomain = fx?.scale.domain();
- const fyDomain = fy?.scale.domain();
- return fxDomain && fyDomain
- ? cross(fxDomain, fyDomain).map(([x, y], i) => ({x, y, i}))
- : fxDomain
- ? fxDomain.map((x, i) => ({x, i}))
- : fyDomain
- ? fyDomain.map((y, i) => ({y, i}))
- : undefined;
- }
- export function recreateFacets(facets, {x: X, y: Y}) {
- X &&= facetIndex(X);
- Y &&= facetIndex(Y);
- return facets
- .filter(
- X && Y // remove any facets no longer present in the domain
- ? (f) => X.has(f.x) && Y.has(f.y)
- : X
- ? (f) => X.has(f.x)
- : (f) => Y.has(f.y)
- )
- .sort(
- X && Y // reorder facets to match the new scale domains
- ? (a, b) => X.get(a.x) - X.get(b.x) || Y.get(a.y) - Y.get(b.y)
- : X
- ? (a, b) => X.get(a.x) - X.get(b.x)
- : (a, b) => Y.get(a.y) - Y.get(b.y)
- );
- }
- // Returns a (possibly nested) Map of [[key1, index1], [key2, index2], …]
- // representing the data indexes associated with each facet.
- export function facetGroups(data, {fx, fy}) {
- const I = range(data);
- const FX = fx?.value;
- const FY = fy?.value;
- return fx && fy
- ? rollup(
- I,
- (G) => ((G.fx = FX[G[0]]), (G.fy = FY[G[0]]), G),
- (i) => FX[i],
- (i) => FY[i]
- )
- : fx
- ? rollup(
- I,
- (G) => ((G.fx = FX[G[0]]), G),
- (i) => FX[i]
- )
- : rollup(
- I,
- (G) => ((G.fy = FY[G[0]]), G),
- (i) => FY[i]
- );
- }
- export function facetTranslator(fx, fy, {marginTop, marginLeft}) {
- const x = fx ? ({x}) => fx(x) - marginLeft : () => 0;
- const y = fy ? ({y}) => fy(y) - marginTop : () => 0;
- return function (d) {
- if (this.tagName === "svg") {
- this.setAttribute("x", x(d));
- this.setAttribute("y", y(d));
- } else {
- this.setAttribute("transform", `translate(${x(d)},${y(d)})`);
- }
- };
- }
- // Returns an index that for each facet lists all the elements present in other
- // facets in the original index. TODO Memoize to avoid repeated work?
- export function facetExclude(index) {
- const ex = [];
- const e = new Uint32Array(sum(index, (d) => d.length));
- for (const i of index) {
- let n = 0;
- for (const j of index) {
- if (i === j) continue;
- e.set(j, n);
- n += j.length;
- }
- ex.push(e.slice(0, n));
- }
- return ex;
- }
- const facetAnchors = new Map([
- ["top", facetAnchorTop],
- ["right", facetAnchorRight],
- ["bottom", facetAnchorBottom],
- ["left", facetAnchorLeft],
- ["top-left", and(facetAnchorTop, facetAnchorLeft)],
- ["top-right", and(facetAnchorTop, facetAnchorRight)],
- ["bottom-left", and(facetAnchorBottom, facetAnchorLeft)],
- ["bottom-right", and(facetAnchorBottom, facetAnchorRight)],
- ["top-empty", facetAnchorTopEmpty],
- ["right-empty", facetAnchorRightEmpty],
- ["bottom-empty", facetAnchorBottomEmpty],
- ["left-empty", facetAnchorLeftEmpty],
- ["empty", facetAnchorEmpty]
- ]);
- export function maybeFacetAnchor(facetAnchor) {
- if (facetAnchor == null) return null;
- const anchor = facetAnchors.get(`${facetAnchor}`.toLowerCase());
- if (anchor) return anchor;
- throw new Error(`invalid facet anchor: ${facetAnchor}`);
- }
- const indexCache = new WeakMap();
- function facetIndex(V) {
- let I = indexCache.get(V);
- if (!I) indexCache.set(V, (I = new InternMap(map(V, (v, i) => [v, i]))));
- return I;
- }
- // Like V.indexOf(v), but with the same semantics as InternMap.
- function facetIndexOf(V, v) {
- return facetIndex(V).get(v);
- }
- // Like facets.find, but with the same semantics as InternMap.
- function facetFind(facets, x, y) {
- x = keyof(x);
- y = keyof(y);
- return facets.find((f) => Object.is(keyof(f.x), x) && Object.is(keyof(f.y), y));
- }
- function facetEmpty(facets, x, y) {
- return facetFind(facets, x, y)?.empty;
- }
- function facetAnchorTop(facets, {y: Y}, {y}) {
- return Y ? facetIndexOf(Y, y) === 0 : true;
- }
- function facetAnchorBottom(facets, {y: Y}, {y}) {
- return Y ? facetIndexOf(Y, y) === Y.length - 1 : true;
- }
- function facetAnchorLeft(facets, {x: X}, {x}) {
- return X ? facetIndexOf(X, x) === 0 : true;
- }
- function facetAnchorRight(facets, {x: X}, {x}) {
- return X ? facetIndexOf(X, x) === X.length - 1 : true;
- }
- function facetAnchorTopEmpty(facets, {y: Y}, {x, y, empty}) {
- if (empty) return false;
- if (!Y) return;
- const i = facetIndexOf(Y, y);
- if (i > 0) return facetEmpty(facets, x, Y[i - 1]);
- }
- function facetAnchorBottomEmpty(facets, {y: Y}, {x, y, empty}) {
- if (empty) return false;
- if (!Y) return;
- const i = facetIndexOf(Y, y);
- if (i < Y.length - 1) return facetEmpty(facets, x, Y[i + 1]);
- }
- function facetAnchorLeftEmpty(facets, {x: X}, {x, y, empty}) {
- if (empty) return false;
- if (!X) return;
- const i = facetIndexOf(X, x);
- if (i > 0) return facetEmpty(facets, X[i - 1], y);
- }
- function facetAnchorRightEmpty(facets, {x: X}, {x, y, empty}) {
- if (empty) return false;
- if (!X) return;
- const i = facetIndexOf(X, x);
- if (i < X.length - 1) return facetEmpty(facets, X[i + 1], y);
- }
- function facetAnchorEmpty(facets, channels, {empty}) {
- return empty;
- }
- function and(a, b) {
- return function () {
- return a.apply(null, arguments) && b.apply(null, arguments);
- };
- }
- // Facet filter, by mark; for now only the "eq" filter is provided.
- export function facetFilter(facets, {channels: {fx, fy}, groups}) {
- return fx && fy
- ? facets.map(({x, y}) => groups.get(x)?.get(y) ?? [])
- : fx
- ? facets.map(({x}) => groups.get(x) ?? [])
- : facets.map(({y}) => groups.get(y) ?? []);
- }
|