UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

248 lines (220 loc) 7.01 kB
import type { DisplayObject } from '@antv/g'; import { Category } from '@antv/component'; import { last } from '@antv/util'; import { format } from 'd3-format'; import { Identity } from '@antv/scale'; import type { FlexLayout, G2MarkState, GuideComponentComponent as GCC, GuideComponentOrientation as GCO, GuideComponentPosition as GCP, Scale, } from '../runtime'; import { GuideComponentContext } from '../runtime/types/component'; import type { G2Mark } from '../runtime/types/options'; import { useMarker } from '../utils/marker'; import { adaptor, domainOf, LegendCategoryLayout, inferComponentLayout, inferComponentShape, scaleOf, titleContent, } from './utils'; export type LegendCategoryOptions = { dx?: number; dy?: number; labelFormatter?: (d: any) => string; layout?: FlexLayout; orientation?: GCO; position?: GCP; title?: string | string[]; [key: string]: any; }; function inferShape(scales: Scale[], markState: Map<G2Mark, G2MarkState>) { const shapeScale = scaleOf(scales, 'shape'); const colorScale = scaleOf(scales, 'color'); // NOTE!!! // scaleOrdinal.map will mute domain. const shapeScale1 = shapeScale ? shapeScale.clone() : null; // Infer the main shape if multiple marks are used. const shapes: [string, string[]][] = []; for (const [mark, state] of markState) { const namespace = mark.type; const domain = colorScale?.getOptions().domain.length > 0 ? colorScale?.getOptions().domain : state.data; const shape: string[] = domain.map((d, i) => { if (shapeScale1) return shapeScale1.map(d || 'point'); return mark?.style?.shape || state.defaultShape || 'point'; }); if (typeof namespace === 'string') shapes.push([namespace, shape]); } if (shapes.length === 0) return ['point', ['point']]; if (shapes.length === 1) return shapes[0]; if (!shapeScale) return shapes[0]; // Evaluate the maximum likelihood of shape const { range } = shapeScale.getOptions(); return shapes .map(([namespace, shape]) => { let sum = 0; for (let i = 0; i < shapes.length; i++) { const targetShape = range[i % range.length]; if (shape[i] === targetShape) sum++; } return [sum / shape.length, [namespace, shape]] as const; }) .sort((a, b) => b[0] - a[0])[0][1]; } function inferItemMarker( options, context: GuideComponentContext, ): ((datum: any, i: number, data: any) => () => DisplayObject) | undefined { const { scales, library, markState } = context; const [mark, shapes] = inferShape(scales, markState); const { itemMarker, itemMarkerSize: size } = options; const create = (name, d) => { const marker = (library[`mark.${mark}`]?.props?.shape[name]?.props .defaultMarker as string) || last(name.split('.')); const radius = typeof size === 'function' ? size(d) : size; return () => useMarker(marker, { color: d.color })(0, 0, radius); }; const shapeOf = (i) => `${shapes[i]}`; const shapeScale = scaleOf(scales, 'shape'); if (shapeScale && !itemMarker) return (d, i) => create(shapeOf(i), d); if (typeof itemMarker === 'function') { return (d, i) => { // @todo Fix this in GUI. // It should pass primitive value rather object. const node = itemMarker(d.id, i); if (typeof node === 'string') return create(node, d); return node; }; } return (d, i) => create(itemMarker || shapeOf(i), d); } function inferItemMarkerOpacity(scales: Scale[]) { const scale = scaleOf(scales, 'opacity'); if (scale) { const { range } = scale.getOptions(); return (d, i) => range[i]; } return undefined; } function inferItemMarkerSize(scales: Scale[], defaults: number) { const scale = scaleOf(scales, 'size'); if (scale instanceof Identity) return scale.map(NaN) * 2; return defaults; } function inferCategoryStyle(options, context: GuideComponentContext) { const { labelFormatter = (d) => `${d}` } = options; const { scales, theme } = context; const defaultSize = theme.legendCategory.itemMarkerSize; const itemMarkerSize = inferItemMarkerSize(scales, defaultSize); const baseStyle = { itemMarker: inferItemMarker({ ...options, itemMarkerSize }, context), itemMarkerSize: itemMarkerSize, itemMarkerOpacity: inferItemMarkerOpacity(scales), }; const finalLabelFormatter = typeof labelFormatter === 'string' ? format(labelFormatter) : labelFormatter; const colorScale = scaleOf(scales, 'color'); const domain = domainOf(scales); const colorOf = colorScale ? (d) => colorScale.map(d) : () => context.theme.color; return { ...baseStyle, data: domain.map((d) => ({ id: d, label: finalLabelFormatter(d), color: colorOf(d), })), }; } function inferLegendShape( value: Record<string, any>, options: LegendCategoryOptions, component: GCC, ) { const { position } = options; if (position === 'center') { const { bbox } = value; // to be confirm: if position is center, we should use the width and height of user definition. const { width, height } = bbox; return { width, height }; } const { width, height } = inferComponentShape(value, options, component); return { width, height }; } /** * Guide Component for ordinal color scale. */ export const LegendCategory: GCC<LegendCategoryOptions> = (options) => { const { labelFormatter, layout, order, orientation, position, size, title, cols, itemMarker, ...style } = options; const { gridRow } = style; return (context) => { const { value, theme } = context; const { bbox } = value; const { width, height } = inferLegendShape(value, options, LegendCategory); const finalLayout = inferComponentLayout(position, layout); const legendStyle = { orientation: ['right', 'left', 'center'].includes(position) ? 'vertical' : 'horizontal', width, height, layout: cols !== undefined ? 'grid' : 'flex', ...(cols !== undefined && { gridCol: cols }), ...(gridRow !== undefined && { gridRow }), titleText: titleContent(title), ...inferCategoryStyle(options, context), }; const { legendCategory: legendTheme = {} } = theme; const categoryStyle = adaptor( Object.assign({}, legendTheme, legendStyle, style), ); const layoutWrapper = new LegendCategoryLayout({ style: { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height, ...finalLayout, // @ts-ignore subOptions: categoryStyle, }, }); layoutWrapper.appendChild( new Category({ className: 'legend-category', style: categoryStyle, }), ); return layoutWrapper as unknown as DisplayObject; }; }; LegendCategory.props = { defaultPosition: 'top', defaultOrder: 1, defaultSize: 40, defaultCrossPadding: [12, 12], defaultPadding: [12, 12], };