UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

349 lines (326 loc) 10.1 kB
import { Primitive } from '@antv/vendor/d3-array'; import { deepMix, isNumber } from '@antv/util'; import { format } from '@antv/vendor/d3-format'; import { indexOf, mapObject } from '../utils/array'; import { composeAsync, defined, isStrictObject, isUnset, } from '../utils/helper'; import { isFullTooltip } from '../utils/mark'; import { useLibrary } from './library'; import { createColumnOf } from './mark'; import { Data, DataComponent } from './types/data'; import { G2Mark, G2DataOptions, G2Context } from './types/options'; import { isPosition } from './scale'; export const CALLBACK_ITEM_SYMBOL = Symbol('CALLBACK_ITEM'); // @todo Add more defaults. export function applyDefaults( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { encode = {}, scale = {}, transform = [], ...rest } = mark; return [I, { ...rest, encode, scale, transform }]; } export async function applyDataTransform( I: number[], mark: G2Mark, context: G2Context, ): Promise<[number[], G2Mark]> { const { library } = context; const { data } = mark; const [useData] = useLibrary<G2DataOptions, DataComponent, Data>( 'data', library, ); const descriptor = normalizedDataSource(data); const { transform: T = [], ...connector } = descriptor; const transform = [connector, ...T]; const transformFunctions = transform.map((t) => useData(t, context)); const transformedData = await composeAsync(transformFunctions)(data); // Maintain the consistency of shape between input and output data. // If the shape of raw data is like { value: any } // and the returned transformedData is Object, // returns the wrapped data: { value: transformedData }, // otherwise returns the processed tabular data. const newData = data && !Array.isArray(data) && !Array.isArray(transformedData) ? { value: transformedData } : transformedData; return [ Array.isArray(transformedData) ? indexOf(transformedData) : [], { ...mark, data: newData }, ]; } export function flatEncode( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { encode } = mark; if (!encode) return [I, mark]; const flattenEncode = {}; for (const [key, value] of Object.entries(encode)) { if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { const name = `${key}${i === 0 ? '' : i}`; flattenEncode[name] = value[i]; } } else { flattenEncode[key] = value; } } return [I, { ...mark, encode: flattenEncode }]; } export function inferChannelsType( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { encode, data } = mark; if (!encode) return [I, mark]; const typedEncode = mapObject(encode, (channel) => { if (isTypedChannel(channel)) return channel; const type = inferChannelType(data, channel); return { type, value: channel }; }); return [I, { ...mark, encode: typedEncode }]; } export function maybeVisualChannel( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { encode } = mark; if (!encode) return [I, mark]; const newEncode = mapObject(encode, (channel, name) => { const { type } = channel; if (type !== 'constant' || isPosition(name)) return channel; return { ...channel, constant: true }; }); return [I, { ...mark, encode: newEncode }]; } export function extractColumns( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { encode, data } = mark; if (!encode) return [I, mark]; const { library } = context; const columnOf = createColumnOf(library); const valuedEncode = mapObject(encode, (channel) => columnOf(data, channel)); return [I, { ...mark, encode: valuedEncode }]; } /** * Normalize mark.tooltip to {title, items}. */ export function normalizeTooltip( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { tooltip = {} } = mark; if (isUnset(tooltip)) return [I, mark]; if (Array.isArray(tooltip)) { return [I, { ...mark, tooltip: { items: tooltip } }]; } if (isStrictObject(tooltip) && isFullTooltip(tooltip)) { return [I, { ...mark, tooltip }]; } return [I, { ...mark, tooltip: { items: [tooltip] } }]; } export function extractTooltip( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { data, encode, tooltip = {} } = mark; if (isUnset(tooltip)) return [I, mark]; const valueOf = (item) => { if (!item) return item; if (typeof item === 'string') { return I.map((i) => ({ name: item, value: data[i][item] })); } if (isStrictObject(item)) { const { field, channel, color, name = field, valueFormatter = (d) => d, } = item; // Support d3-format. const normalizedValueFormatter = typeof valueFormatter === 'string' ? format(valueFormatter) : valueFormatter; // Field name. const definedChannel = channel && encode[channel]; const channelField = definedChannel && encode[channel].field; const name1 = name || channelField || channel; const values = []; for (const i of I) { const value1 = field ? data[i][field] : definedChannel ? encode[channel].value[i] : null; values[i] = { name: name1, color, value: normalizedValueFormatter(value1), }; } return values; } if (typeof item === 'function') { const values = []; for (const i of I) { const v = item(data[i], i, data, encode); if (isStrictObject(v)) values[i] = { ...v, [CALLBACK_ITEM_SYMBOL]: true }; else values[i] = { value: v }; } return values; } return item; }; const { title, items = [], ...rest } = tooltip; const newTooltip = { title: valueOf(title), items: Array.isArray(items) ? items.map(valueOf) : [], ...rest, }; return [I, { ...mark, tooltip: newTooltip }]; } export function maybeArrayField( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { encode, ...rest } = mark; if (!encode) return [I, mark]; const columns = Object.entries(encode); const arrayColumns = columns .filter(([, channel]) => { const { value: V } = channel; return Array.isArray(V[0]); }) .flatMap(([key, V]) => { const columns = [[key, new Array(I.length).fill(undefined)] as const]; const { value: rows, ...rest } = V; for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (Array.isArray(row)) { for (let j = 0; j < row.length; j++) { const column = columns[j] || [ `${key}${j}`, new Array(I).fill(undefined), ]; column[1][i] = row[j]; columns[j] = column; } } } return columns.map(([key, value]) => [ key, { type: 'column', value, ...rest }, ]); }); const newEncode = Object.fromEntries([...columns, ...arrayColumns]); return [I, { ...rest, encode: newEncode }]; } export function addGuideToScale( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { axis = {}, legend = {}, slider = {}, scrollbar = {} } = mark; const normalize = (guide: boolean | Record<string, any>, channel: string) => { if (typeof guide === 'boolean') return guide ? {} : null; const eachGuide = guide[channel]; return eachGuide === undefined || eachGuide ? eachGuide : null; }; const axisChannels = typeof axis === 'object' ? Array.from(new Set(['x', 'y', 'z', ...Object.keys(axis)])) : ['x', 'y', 'z']; deepMix(mark, { scale: { ...Object.fromEntries( axisChannels.map((channel) => { const scrollbarOptions = normalize(scrollbar, channel); return [ channel, { guide: normalize(axis, channel), slider: normalize(slider, channel), scrollbar: scrollbarOptions, ...(scrollbarOptions && { ratio: scrollbarOptions.ratio === undefined ? 0.5 : scrollbarOptions.ratio, }), }, ]; }), ), color: { guide: normalize(legend, 'color') }, size: { guide: normalize(legend, 'size') }, shape: { guide: normalize(legend, 'shape') }, // fixme: opacity is conflict with DisplayObject.opacity // to be confirm. opacity: { guide: normalize(legend, 'opacity') }, }, }); return [I, mark]; } export function maybeNonAnimate( I: number[], mark: G2Mark, context: G2Context, ): [number[], G2Mark] { const { animate } = mark; if (animate || animate === undefined) return [I, mark]; deepMix(mark, { animate: { enter: { type: null }, exit: { type: null }, update: { type: null }, }, }); return [I, mark]; } function isTypedChannel(channel): boolean { if ( typeof channel !== 'object' || channel instanceof Date || channel === null ) { return false; } const { type } = channel; return defined(type); } function inferChannelType(data: Record<string, Primitive>[], channel): string { if (typeof channel === 'function') return 'transform'; if (typeof channel === 'string' && isField(data, channel)) return 'field'; return 'constant'; } function isField(data: Record<string, Primitive>[], value: string): boolean { if (!Array.isArray(data)) return false; return data.some((d) => d[value] !== undefined); } function normalizedDataSource(data) { // Liquid、Gauge need number data. if (isNumber(data)) return { type: 'inline', value: data }; // Return null as a placeholder. if (!data) return { type: 'inline', value: null }; if (Array.isArray(data)) return { type: 'inline', value: data }; const { type = 'inline', ...rest } = data; return { ...rest, type }; }