facetRect.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import { deepMix } from '@antv/util';
  2. import { extent, group, max } from 'd3-array';
  3. import {
  4. CompositionComponent as CC,
  5. G2MarkChildrenCallback,
  6. G2ViewTree,
  7. Node,
  8. } from '../runtime';
  9. import { FacetRectComposition } from '../spec';
  10. import { calcBBox } from '../utils/vector';
  11. import { Container } from '../utils/container';
  12. import { indexOf } from '../utils/array';
  13. import { useDefaultAdaptor, useOverrideAdaptor } from './utils';
  14. export type SubLayout = (data?: any) => number[];
  15. const setScale = useDefaultAdaptor<G2ViewTree>((options) => {
  16. const { encode, data, scale, shareSize = false } = options;
  17. const { x, y } = encode;
  18. const flexDomain = (encode: string, channel: string) => {
  19. if (encode === undefined || !shareSize) return {};
  20. const groups = group(data, (d) => d[encode]);
  21. const domain = scale?.[channel]?.domain || Array.from(groups.keys());
  22. const flex = domain.map((key) => {
  23. if (!groups.has(key)) return 1;
  24. return groups.get(key).length;
  25. });
  26. return { domain, flex };
  27. };
  28. return {
  29. scale: {
  30. x: {
  31. paddingOuter: 0,
  32. paddingInner: 0.1,
  33. guide: x === undefined ? null : { position: 'top' },
  34. ...(x === undefined && { paddingInner: 0 }),
  35. ...flexDomain(x, 'x'),
  36. },
  37. y: {
  38. range: [0, 1],
  39. paddingOuter: 0,
  40. paddingInner: 0.1,
  41. guide: y === undefined ? null : { position: 'right' },
  42. ...(y === undefined && { paddingInner: 0 }),
  43. ...flexDomain(y, 'y'),
  44. },
  45. },
  46. };
  47. });
  48. /**
  49. * BFS view tree and using the last discovered color encode
  50. * as the top-level encode for this plot. This is useful when
  51. * color encode and color scale is specified in mark node.
  52. * It makes sense because the whole facet should shared the same
  53. * color encoding, but it also can be override with explicity
  54. * encode and scale specification.
  55. */
  56. export const inferColor = useDefaultAdaptor<G2ViewTree>(
  57. (options: G2ViewTree) => {
  58. const { data, scale } = options;
  59. const discovered = [options];
  60. let encodeColor;
  61. let scaleColor;
  62. let legendColor;
  63. while (discovered.length) {
  64. const node = discovered.shift();
  65. const { children, encode = {}, scale = {}, legend = {} } = node;
  66. const { color: c } = encode;
  67. const { color: cs } = scale;
  68. const { color: cl } = legend;
  69. if (c !== undefined) encodeColor = c;
  70. if (cs !== undefined) scaleColor = cs;
  71. if (cl !== undefined) legendColor = cl;
  72. if (Array.isArray(children)) {
  73. discovered.push(...children);
  74. }
  75. }
  76. const domainColor = () => {
  77. const domain = scale?.color?.domain;
  78. if (domain !== undefined) return [domain];
  79. if (encodeColor === undefined) return [undefined];
  80. const color =
  81. typeof encodeColor === 'function' ? encodeColor : (d) => d[encodeColor];
  82. const values = data.map(color);
  83. if (values.some((d) => typeof d === 'number')) return [extent(values)];
  84. return [Array.from(new Set(values)), 'ordinal'];
  85. };
  86. const title = typeof encodeColor === 'string' ? encodeColor : '';
  87. const [domain, type] = domainColor();
  88. return {
  89. encode: { color: encodeColor },
  90. scale: { color: deepMix({}, scaleColor, { domain, type }) },
  91. legend: { color: deepMix({ title }, legendColor) },
  92. };
  93. },
  94. );
  95. export const setAnimation = useDefaultAdaptor<G2ViewTree>(() => ({
  96. animate: {
  97. enterType: 'fadeIn',
  98. },
  99. }));
  100. export const setStyle = useOverrideAdaptor<G2ViewTree>(() => ({
  101. frame: false,
  102. encode: {
  103. shape: 'hollow',
  104. },
  105. style: {
  106. lineWidth: 0,
  107. },
  108. }));
  109. export const toCell = useOverrideAdaptor<G2ViewTree>(() => ({
  110. type: 'cell',
  111. }));
  112. /**
  113. * Do not set cell data directly, the children will get wrong do if do
  114. * so. Use transform to set new data.
  115. **/
  116. export const setData = useOverrideAdaptor<G2ViewTree>((options) => {
  117. const { data } = options;
  118. const connector = {
  119. type: 'custom',
  120. callback: () => {
  121. const { data, encode } = options;
  122. const { x, y } = encode;
  123. const X = x ? Array.from(new Set(data.map((d) => d[x]))) : [];
  124. const Y = y ? Array.from(new Set(data.map((d) => d[y]))) : [];
  125. const cellData = () => {
  126. if (X.length && Y.length) {
  127. const cellData = [];
  128. for (const vx of X) {
  129. for (const vy of Y) {
  130. cellData.push({ [x]: vx, [y]: vy });
  131. }
  132. }
  133. return cellData;
  134. }
  135. if (X.length) return X.map((d) => ({ [x]: d }));
  136. if (Y.length) return Y.map((d) => ({ [y]: d }));
  137. };
  138. return cellData();
  139. },
  140. };
  141. return {
  142. data: { type: 'inline', value: data, transform: [connector] },
  143. };
  144. });
  145. /**
  146. * @todo Move some options assignment to runtime.
  147. */
  148. export const setChildren = useOverrideAdaptor<G2ViewTree>(
  149. (
  150. options,
  151. subLayout: SubLayout = subLayoutRect,
  152. createGuideX = createGuideXRect,
  153. createGuideY = createGuideYRect,
  154. childOptions = {},
  155. ) => {
  156. const {
  157. data: dataValue,
  158. encode,
  159. children,
  160. scale: facetScale,
  161. x: originX = 0,
  162. y: originY = 0,
  163. shareData = false,
  164. key: viewKey,
  165. } = options;
  166. const { value: data } = dataValue;
  167. // Only support field encode now.
  168. const { x: encodeX, y: encodeY } = encode;
  169. const { color: facetScaleColor } = facetScale;
  170. const { domain: facetDomainColor } = facetScaleColor;
  171. const createChildren: G2MarkChildrenCallback = (
  172. visualData,
  173. scale,
  174. layout,
  175. ) => {
  176. const { x: scaleX, y: scaleY } = scale;
  177. const { paddingLeft, paddingTop } = layout;
  178. const { domain: domainX } = scaleX.getOptions();
  179. const { domain: domainY } = scaleY.getOptions();
  180. const index = indexOf(visualData);
  181. const bboxs = visualData.map(subLayout);
  182. const values = visualData.map(({ x, y }) => [
  183. scaleX.invert(x),
  184. scaleY.invert(y),
  185. ]);
  186. const filters = values.map(([fx, fy]) => (d) => {
  187. const { [encodeX]: x, [encodeY]: y } = d;
  188. const inX = encodeX !== undefined ? x === fx : true;
  189. const inY = encodeY !== undefined ? y === fy : true;
  190. return inX && inY;
  191. });
  192. const facetData2d = filters.map((f) => data.filter(f));
  193. const maxDataDomain = shareData
  194. ? max(facetData2d, (data) => data.length)
  195. : undefined;
  196. const facets = values.map(([fx, fy]) => ({
  197. columnField: encodeX,
  198. columnIndex: domainX.indexOf(fx),
  199. columnValue: fx,
  200. columnValuesLength: domainX.length,
  201. rowField: encodeY,
  202. rowIndex: domainY.indexOf(fy),
  203. rowValue: fy,
  204. rowValuesLength: domainY.length,
  205. }));
  206. const normalizedChildren: Node[][] = facets.map((facet) => {
  207. if (Array.isArray(children)) return children;
  208. return [children(facet)].flat(1);
  209. });
  210. return index.flatMap((i) => {
  211. const [left, top, width, height] = bboxs[i];
  212. const facet = facets[i];
  213. const facetData = facetData2d[i];
  214. const children = normalizedChildren[i];
  215. return children.map(
  216. ({
  217. scale,
  218. key,
  219. facet: isFacet = true,
  220. axis = {},
  221. legend = {},
  222. ...rest
  223. }) => {
  224. const guideY = scale?.y?.guide || axis.y;
  225. const guideX = scale?.x?.guide || axis.x;
  226. const defaultScale = {
  227. x: { tickCount: encodeX ? 5 : undefined },
  228. y: { tickCount: encodeY ? 5 : undefined },
  229. };
  230. const newData = isFacet
  231. ? facetData
  232. : facetData.length === 0
  233. ? []
  234. : data;
  235. const newScale = {
  236. color: { domain: facetDomainColor },
  237. };
  238. const newAxis = {
  239. x: createGuide(guideX, createGuideX)(facet, newData),
  240. y: createGuide(guideY, createGuideY)(facet, newData),
  241. };
  242. return {
  243. key: `${key}-${i}`,
  244. data: newData,
  245. x: left + paddingLeft + originX,
  246. y: top + paddingTop + originY,
  247. parentKey: viewKey,
  248. width,
  249. height,
  250. paddingLeft: 0,
  251. paddingRight: 0,
  252. paddingTop: 0,
  253. paddingBottom: 0,
  254. frame: newData.length ? true : false,
  255. dataDomain: maxDataDomain,
  256. scale: deepMix(defaultScale, scale, newScale),
  257. axis: deepMix({}, axis, newAxis),
  258. // Hide all legends for child mark by default,
  259. // they are displayed in the top-level.
  260. legend: false,
  261. ...rest,
  262. ...childOptions,
  263. };
  264. },
  265. );
  266. });
  267. };
  268. return {
  269. children: createChildren,
  270. };
  271. },
  272. );
  273. function subLayoutRect(data) {
  274. const { points } = data;
  275. return calcBBox(points);
  276. }
  277. /**
  278. * Inner guide not show title, tickLine, label and subTickLine,
  279. * if data is empty, do not show guide.
  280. */
  281. export function createInnerGuide(guide, data) {
  282. return data.length
  283. ? deepMix(
  284. {
  285. title: false,
  286. tick: null,
  287. label: null,
  288. },
  289. guide,
  290. )
  291. : deepMix(
  292. {
  293. title: false,
  294. tick: null,
  295. label: null,
  296. grid: null,
  297. },
  298. guide,
  299. );
  300. }
  301. function createGuideXRect(guide) {
  302. return (facet, data) => {
  303. const { rowIndex, rowValuesLength, columnIndex, columnValuesLength } =
  304. facet;
  305. // Only the bottom-most facet show axisX.
  306. if (rowIndex !== rowValuesLength - 1) return createInnerGuide(guide, data);
  307. // Only the bottom-left facet show title.
  308. const title = columnIndex !== columnValuesLength - 1 ? false : undefined;
  309. // If data is empty, do not show cell.
  310. const grid = data.length ? undefined : null;
  311. return deepMix({ title, grid }, guide);
  312. };
  313. }
  314. function createGuideYRect(guide) {
  315. return (facet, data) => {
  316. const { rowIndex, columnIndex } = facet;
  317. // Only the left-most facet show axisY.
  318. if (columnIndex !== 0) return createInnerGuide(guide, data);
  319. // Only the left-top facet show title.
  320. const title = rowIndex !== 0 ? false : undefined;
  321. // If data is empty, do not show cell.
  322. const grid = data.length ? undefined : null;
  323. return deepMix({ title, grid }, guide);
  324. };
  325. }
  326. function createGuide(guide, factory) {
  327. if (typeof guide === 'function') return guide;
  328. if (guide === null) return () => null;
  329. return factory(guide);
  330. }
  331. export type FacetRectOptions = Omit<FacetRectComposition, 'type'>;
  332. export const FacetRect: CC<FacetRectOptions> = () => {
  333. return (options) => {
  334. const newOptions = Container.of<G2ViewTree>(options)
  335. .call(toCell)
  336. .call(inferColor)
  337. .call(setAnimation)
  338. .call(setScale)
  339. .call(setStyle)
  340. .call(setData)
  341. .call(setChildren)
  342. .value();
  343. return [newOptions];
  344. };
  345. };
  346. FacetRect.props = {};