facet.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import {InternMap, cross, rollup, sum} from "d3";
  2. import {keyof, map, range} from "./options.js";
  3. import {createScales} from "./scales.js";
  4. // Returns an array of {x?, y?, i} objects representing the facet domain.
  5. export function createFacets(channelsByScale, options) {
  6. const {fx, fy} = createScales(channelsByScale, options);
  7. const fxDomain = fx?.scale.domain();
  8. const fyDomain = fy?.scale.domain();
  9. return fxDomain && fyDomain
  10. ? cross(fxDomain, fyDomain).map(([x, y], i) => ({x, y, i}))
  11. : fxDomain
  12. ? fxDomain.map((x, i) => ({x, i}))
  13. : fyDomain
  14. ? fyDomain.map((y, i) => ({y, i}))
  15. : undefined;
  16. }
  17. export function recreateFacets(facets, {x: X, y: Y}) {
  18. X &&= facetIndex(X);
  19. Y &&= facetIndex(Y);
  20. return facets
  21. .filter(
  22. X && Y // remove any facets no longer present in the domain
  23. ? (f) => X.has(f.x) && Y.has(f.y)
  24. : X
  25. ? (f) => X.has(f.x)
  26. : (f) => Y.has(f.y)
  27. )
  28. .sort(
  29. X && Y // reorder facets to match the new scale domains
  30. ? (a, b) => X.get(a.x) - X.get(b.x) || Y.get(a.y) - Y.get(b.y)
  31. : X
  32. ? (a, b) => X.get(a.x) - X.get(b.x)
  33. : (a, b) => Y.get(a.y) - Y.get(b.y)
  34. );
  35. }
  36. // Returns a (possibly nested) Map of [[key1, index1], [key2, index2], …]
  37. // representing the data indexes associated with each facet.
  38. export function facetGroups(data, {fx, fy}) {
  39. const I = range(data);
  40. const FX = fx?.value;
  41. const FY = fy?.value;
  42. return fx && fy
  43. ? rollup(
  44. I,
  45. (G) => ((G.fx = FX[G[0]]), (G.fy = FY[G[0]]), G),
  46. (i) => FX[i],
  47. (i) => FY[i]
  48. )
  49. : fx
  50. ? rollup(
  51. I,
  52. (G) => ((G.fx = FX[G[0]]), G),
  53. (i) => FX[i]
  54. )
  55. : rollup(
  56. I,
  57. (G) => ((G.fy = FY[G[0]]), G),
  58. (i) => FY[i]
  59. );
  60. }
  61. export function facetTranslator(fx, fy, {marginTop, marginLeft}) {
  62. const x = fx ? ({x}) => fx(x) - marginLeft : () => 0;
  63. const y = fy ? ({y}) => fy(y) - marginTop : () => 0;
  64. return function (d) {
  65. if (this.tagName === "svg") {
  66. this.setAttribute("x", x(d));
  67. this.setAttribute("y", y(d));
  68. } else {
  69. this.setAttribute("transform", `translate(${x(d)},${y(d)})`);
  70. }
  71. };
  72. }
  73. // Returns an index that for each facet lists all the elements present in other
  74. // facets in the original index. TODO Memoize to avoid repeated work?
  75. export function facetExclude(index) {
  76. const ex = [];
  77. const e = new Uint32Array(sum(index, (d) => d.length));
  78. for (const i of index) {
  79. let n = 0;
  80. for (const j of index) {
  81. if (i === j) continue;
  82. e.set(j, n);
  83. n += j.length;
  84. }
  85. ex.push(e.slice(0, n));
  86. }
  87. return ex;
  88. }
  89. const facetAnchors = new Map([
  90. ["top", facetAnchorTop],
  91. ["right", facetAnchorRight],
  92. ["bottom", facetAnchorBottom],
  93. ["left", facetAnchorLeft],
  94. ["top-left", and(facetAnchorTop, facetAnchorLeft)],
  95. ["top-right", and(facetAnchorTop, facetAnchorRight)],
  96. ["bottom-left", and(facetAnchorBottom, facetAnchorLeft)],
  97. ["bottom-right", and(facetAnchorBottom, facetAnchorRight)],
  98. ["top-empty", facetAnchorTopEmpty],
  99. ["right-empty", facetAnchorRightEmpty],
  100. ["bottom-empty", facetAnchorBottomEmpty],
  101. ["left-empty", facetAnchorLeftEmpty],
  102. ["empty", facetAnchorEmpty]
  103. ]);
  104. export function maybeFacetAnchor(facetAnchor) {
  105. if (facetAnchor == null) return null;
  106. const anchor = facetAnchors.get(`${facetAnchor}`.toLowerCase());
  107. if (anchor) return anchor;
  108. throw new Error(`invalid facet anchor: ${facetAnchor}`);
  109. }
  110. const indexCache = new WeakMap();
  111. function facetIndex(V) {
  112. let I = indexCache.get(V);
  113. if (!I) indexCache.set(V, (I = new InternMap(map(V, (v, i) => [v, i]))));
  114. return I;
  115. }
  116. // Like V.indexOf(v), but with the same semantics as InternMap.
  117. function facetIndexOf(V, v) {
  118. return facetIndex(V).get(v);
  119. }
  120. // Like facets.find, but with the same semantics as InternMap.
  121. function facetFind(facets, x, y) {
  122. x = keyof(x);
  123. y = keyof(y);
  124. return facets.find((f) => Object.is(keyof(f.x), x) && Object.is(keyof(f.y), y));
  125. }
  126. function facetEmpty(facets, x, y) {
  127. return facetFind(facets, x, y)?.empty;
  128. }
  129. function facetAnchorTop(facets, {y: Y}, {y}) {
  130. return Y ? facetIndexOf(Y, y) === 0 : true;
  131. }
  132. function facetAnchorBottom(facets, {y: Y}, {y}) {
  133. return Y ? facetIndexOf(Y, y) === Y.length - 1 : true;
  134. }
  135. function facetAnchorLeft(facets, {x: X}, {x}) {
  136. return X ? facetIndexOf(X, x) === 0 : true;
  137. }
  138. function facetAnchorRight(facets, {x: X}, {x}) {
  139. return X ? facetIndexOf(X, x) === X.length - 1 : true;
  140. }
  141. function facetAnchorTopEmpty(facets, {y: Y}, {x, y, empty}) {
  142. if (empty) return false;
  143. if (!Y) return;
  144. const i = facetIndexOf(Y, y);
  145. if (i > 0) return facetEmpty(facets, x, Y[i - 1]);
  146. }
  147. function facetAnchorBottomEmpty(facets, {y: Y}, {x, y, empty}) {
  148. if (empty) return false;
  149. if (!Y) return;
  150. const i = facetIndexOf(Y, y);
  151. if (i < Y.length - 1) return facetEmpty(facets, x, Y[i + 1]);
  152. }
  153. function facetAnchorLeftEmpty(facets, {x: X}, {x, y, empty}) {
  154. if (empty) return false;
  155. if (!X) return;
  156. const i = facetIndexOf(X, x);
  157. if (i > 0) return facetEmpty(facets, X[i - 1], y);
  158. }
  159. function facetAnchorRightEmpty(facets, {x: X}, {x, y, empty}) {
  160. if (empty) return false;
  161. if (!X) return;
  162. const i = facetIndexOf(X, x);
  163. if (i < X.length - 1) return facetEmpty(facets, X[i + 1], y);
  164. }
  165. function facetAnchorEmpty(facets, channels, {empty}) {
  166. return empty;
  167. }
  168. function and(a, b) {
  169. return function () {
  170. return a.apply(null, arguments) && b.apply(null, arguments);
  171. };
  172. }
  173. // Facet filter, by mark; for now only the "eq" filter is provided.
  174. export function facetFilter(facets, {channels: {fx, fy}, groups}) {
  175. return fx && fy
  176. ? facets.map(({x, y}) => groups.get(x)?.get(y) ?? [])
  177. : fx
  178. ? facets.map(({x}) => groups.get(x) ?? [])
  179. : facets.map(({y}) => groups.get(y) ?? []);
  180. }