| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- import { deepMix } from '@antv/util';
- import { extent, group, max } from 'd3-array';
- import {
- CompositionComponent as CC,
- G2MarkChildrenCallback,
- G2ViewTree,
- Node,
- } from '../runtime';
- import { FacetRectComposition } from '../spec';
- import { calcBBox } from '../utils/vector';
- import { Container } from '../utils/container';
- import { indexOf } from '../utils/array';
- import { useDefaultAdaptor, useOverrideAdaptor } from './utils';
- export type SubLayout = (data?: any) => number[];
- const setScale = useDefaultAdaptor<G2ViewTree>((options) => {
- const { encode, data, scale, shareSize = false } = options;
- const { x, y } = encode;
- const flexDomain = (encode: string, channel: string) => {
- if (encode === undefined || !shareSize) return {};
- const groups = group(data, (d) => d[encode]);
- const domain = scale?.[channel]?.domain || Array.from(groups.keys());
- const flex = domain.map((key) => {
- if (!groups.has(key)) return 1;
- return groups.get(key).length;
- });
- return { domain, flex };
- };
- return {
- scale: {
- x: {
- paddingOuter: 0,
- paddingInner: 0.1,
- guide: x === undefined ? null : { position: 'top' },
- ...(x === undefined && { paddingInner: 0 }),
- ...flexDomain(x, 'x'),
- },
- y: {
- range: [0, 1],
- paddingOuter: 0,
- paddingInner: 0.1,
- guide: y === undefined ? null : { position: 'right' },
- ...(y === undefined && { paddingInner: 0 }),
- ...flexDomain(y, 'y'),
- },
- },
- };
- });
- /**
- * BFS view tree and using the last discovered color encode
- * as the top-level encode for this plot. This is useful when
- * color encode and color scale is specified in mark node.
- * It makes sense because the whole facet should shared the same
- * color encoding, but it also can be override with explicity
- * encode and scale specification.
- */
- export const inferColor = useDefaultAdaptor<G2ViewTree>(
- (options: G2ViewTree) => {
- const { data, scale } = options;
- const discovered = [options];
- let encodeColor;
- let scaleColor;
- let legendColor;
- while (discovered.length) {
- const node = discovered.shift();
- const { children, encode = {}, scale = {}, legend = {} } = node;
- const { color: c } = encode;
- const { color: cs } = scale;
- const { color: cl } = legend;
- if (c !== undefined) encodeColor = c;
- if (cs !== undefined) scaleColor = cs;
- if (cl !== undefined) legendColor = cl;
- if (Array.isArray(children)) {
- discovered.push(...children);
- }
- }
- const domainColor = () => {
- const domain = scale?.color?.domain;
- if (domain !== undefined) return [domain];
- if (encodeColor === undefined) return [undefined];
- const color =
- typeof encodeColor === 'function' ? encodeColor : (d) => d[encodeColor];
- const values = data.map(color);
- if (values.some((d) => typeof d === 'number')) return [extent(values)];
- return [Array.from(new Set(values)), 'ordinal'];
- };
- const title = typeof encodeColor === 'string' ? encodeColor : '';
- const [domain, type] = domainColor();
- return {
- encode: { color: encodeColor },
- scale: { color: deepMix({}, scaleColor, { domain, type }) },
- legend: { color: deepMix({ title }, legendColor) },
- };
- },
- );
- export const setAnimation = useDefaultAdaptor<G2ViewTree>(() => ({
- animate: {
- enterType: 'fadeIn',
- },
- }));
- export const setStyle = useOverrideAdaptor<G2ViewTree>(() => ({
- frame: false,
- encode: {
- shape: 'hollow',
- },
- style: {
- lineWidth: 0,
- },
- }));
- export const toCell = useOverrideAdaptor<G2ViewTree>(() => ({
- type: 'cell',
- }));
- /**
- * Do not set cell data directly, the children will get wrong do if do
- * so. Use transform to set new data.
- **/
- export const setData = useOverrideAdaptor<G2ViewTree>((options) => {
- const { data } = options;
- const connector = {
- type: 'custom',
- callback: () => {
- const { data, encode } = options;
- const { x, y } = encode;
- const X = x ? Array.from(new Set(data.map((d) => d[x]))) : [];
- const Y = y ? Array.from(new Set(data.map((d) => d[y]))) : [];
- const cellData = () => {
- if (X.length && Y.length) {
- const cellData = [];
- for (const vx of X) {
- for (const vy of Y) {
- cellData.push({ [x]: vx, [y]: vy });
- }
- }
- return cellData;
- }
- if (X.length) return X.map((d) => ({ [x]: d }));
- if (Y.length) return Y.map((d) => ({ [y]: d }));
- };
- return cellData();
- },
- };
- return {
- data: { type: 'inline', value: data, transform: [connector] },
- };
- });
- /**
- * @todo Move some options assignment to runtime.
- */
- export const setChildren = useOverrideAdaptor<G2ViewTree>(
- (
- options,
- subLayout: SubLayout = subLayoutRect,
- createGuideX = createGuideXRect,
- createGuideY = createGuideYRect,
- childOptions = {},
- ) => {
- const {
- data: dataValue,
- encode,
- children,
- scale: facetScale,
- x: originX = 0,
- y: originY = 0,
- shareData = false,
- key: viewKey,
- } = options;
- const { value: data } = dataValue;
- // Only support field encode now.
- const { x: encodeX, y: encodeY } = encode;
- const { color: facetScaleColor } = facetScale;
- const { domain: facetDomainColor } = facetScaleColor;
- const createChildren: G2MarkChildrenCallback = (
- visualData,
- scale,
- layout,
- ) => {
- const { x: scaleX, y: scaleY } = scale;
- const { paddingLeft, paddingTop } = layout;
- const { domain: domainX } = scaleX.getOptions();
- const { domain: domainY } = scaleY.getOptions();
- const index = indexOf(visualData);
- const bboxs = visualData.map(subLayout);
- const values = visualData.map(({ x, y }) => [
- scaleX.invert(x),
- scaleY.invert(y),
- ]);
- const filters = values.map(([fx, fy]) => (d) => {
- const { [encodeX]: x, [encodeY]: y } = d;
- const inX = encodeX !== undefined ? x === fx : true;
- const inY = encodeY !== undefined ? y === fy : true;
- return inX && inY;
- });
- const facetData2d = filters.map((f) => data.filter(f));
- const maxDataDomain = shareData
- ? max(facetData2d, (data) => data.length)
- : undefined;
- const facets = values.map(([fx, fy]) => ({
- columnField: encodeX,
- columnIndex: domainX.indexOf(fx),
- columnValue: fx,
- columnValuesLength: domainX.length,
- rowField: encodeY,
- rowIndex: domainY.indexOf(fy),
- rowValue: fy,
- rowValuesLength: domainY.length,
- }));
- const normalizedChildren: Node[][] = facets.map((facet) => {
- if (Array.isArray(children)) return children;
- return [children(facet)].flat(1);
- });
- return index.flatMap((i) => {
- const [left, top, width, height] = bboxs[i];
- const facet = facets[i];
- const facetData = facetData2d[i];
- const children = normalizedChildren[i];
- return children.map(
- ({
- scale,
- key,
- facet: isFacet = true,
- axis = {},
- legend = {},
- ...rest
- }) => {
- const guideY = scale?.y?.guide || axis.y;
- const guideX = scale?.x?.guide || axis.x;
- const defaultScale = {
- x: { tickCount: encodeX ? 5 : undefined },
- y: { tickCount: encodeY ? 5 : undefined },
- };
- const newData = isFacet
- ? facetData
- : facetData.length === 0
- ? []
- : data;
- const newScale = {
- color: { domain: facetDomainColor },
- };
- const newAxis = {
- x: createGuide(guideX, createGuideX)(facet, newData),
- y: createGuide(guideY, createGuideY)(facet, newData),
- };
- return {
- key: `${key}-${i}`,
- data: newData,
- x: left + paddingLeft + originX,
- y: top + paddingTop + originY,
- parentKey: viewKey,
- width,
- height,
- paddingLeft: 0,
- paddingRight: 0,
- paddingTop: 0,
- paddingBottom: 0,
- frame: newData.length ? true : false,
- dataDomain: maxDataDomain,
- scale: deepMix(defaultScale, scale, newScale),
- axis: deepMix({}, axis, newAxis),
- // Hide all legends for child mark by default,
- // they are displayed in the top-level.
- legend: false,
- ...rest,
- ...childOptions,
- };
- },
- );
- });
- };
- return {
- children: createChildren,
- };
- },
- );
- function subLayoutRect(data) {
- const { points } = data;
- return calcBBox(points);
- }
- /**
- * Inner guide not show title, tickLine, label and subTickLine,
- * if data is empty, do not show guide.
- */
- export function createInnerGuide(guide, data) {
- return data.length
- ? deepMix(
- {
- title: false,
- tick: null,
- label: null,
- },
- guide,
- )
- : deepMix(
- {
- title: false,
- tick: null,
- label: null,
- grid: null,
- },
- guide,
- );
- }
- function createGuideXRect(guide) {
- return (facet, data) => {
- const { rowIndex, rowValuesLength, columnIndex, columnValuesLength } =
- facet;
- // Only the bottom-most facet show axisX.
- if (rowIndex !== rowValuesLength - 1) return createInnerGuide(guide, data);
- // Only the bottom-left facet show title.
- const title = columnIndex !== columnValuesLength - 1 ? false : undefined;
- // If data is empty, do not show cell.
- const grid = data.length ? undefined : null;
- return deepMix({ title, grid }, guide);
- };
- }
- function createGuideYRect(guide) {
- return (facet, data) => {
- const { rowIndex, columnIndex } = facet;
- // Only the left-most facet show axisY.
- if (columnIndex !== 0) return createInnerGuide(guide, data);
- // Only the left-top facet show title.
- const title = rowIndex !== 0 ? false : undefined;
- // If data is empty, do not show cell.
- const grid = data.length ? undefined : null;
- return deepMix({ title, grid }, guide);
- };
- }
- function createGuide(guide, factory) {
- if (typeof guide === 'function') return guide;
- if (guide === null) return () => null;
- return factory(guide);
- }
- export type FacetRectOptions = Omit<FacetRectComposition, 'type'>;
- export const FacetRect: CC<FacetRectOptions> = () => {
- return (options) => {
- const newOptions = Container.of<G2ViewTree>(options)
- .call(toCell)
- .call(inferColor)
- .call(setAnimation)
- .call(setScale)
- .call(setStyle)
- .call(setData)
- .call(setChildren)
- .value();
- return [newOptions];
- };
- };
- FacetRect.props = {};
|