UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

366 lines (347 loc) 11.1 kB
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, marginLeft, marginTop } = 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, margin: 0, x: left + paddingLeft + originX + marginLeft, y: top + paddingTop + originY + marginTop, 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 || guide === false) 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 = {};