UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

1,034 lines 55.6 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { Rect } from '@antv/g'; import { deepMix, upperFirst, isArray } from '@antv/util'; import { group, groups } from 'd3-array'; import { format } from 'd3-format'; import { mapObject } from '../utils/array'; import { ChartEvent } from '../utils/event'; import { isStrictObject, appendTransform, compose, copyAttributes, defined, error, maybeSubObject, subObject, useMemo, } from '../utils/helper'; import { select, Selection } from '../utils/selection'; import { groupComponents, inferComponent, normalizeComponents, renderComponent, } from './component'; import { AREA_CLASS_NAME, COMPONENT_CLASS_NAME, ELEMENT_CLASS_NAME, LABEL_CLASS_NAME, LABEL_LAYER_CLASS_NAME, MAIN_LAYER_CLASS_NAME, PLOT_CLASS_NAME, VIEW_CLASS_NAME, } from './constant'; import { coordinate2Transform, createCoordinate } from './coordinate'; import { computeLayout, computeRoughPlotSize, placeComponents, processAxisZ, } from './layout'; import { documentOf, useLibrary } from './library'; import { initializeMark } from './mark'; import { applyScale, assignScale, collectScales, inferScale, syncFacetsScales, useRelationScale, } from './scale'; import { applyDataTransform } from './transform'; export function plot(options, selection, context) { var _a; return __awaiter(this, void 0, void 0, function* () { const { library } = context; const [useComposition] = useLibrary('composition', library); const [useInteraction] = useLibrary('interaction', library); // Some helper functions. const marks = new Set(Object.keys(library) .map((d) => { var _a; return (_a = /mark\.(.*)/.exec(d)) === null || _a === void 0 ? void 0 : _a[1]; }) .filter(defined)); const staticMarks = new Set(Object.keys(library) .map((d) => { var _a; return (_a = /component\.(.*)/.exec(d)) === null || _a === void 0 ? void 0 : _a[1]; }) .filter(defined)); const typeOf = (node) => { const { type } = node; if (typeof type === 'function') { // @ts-ignore const { props = {} } = type; const { composite = true } = props; if (composite) return 'mark'; } if (typeof type !== 'string') return type; if (marks.has(type) || staticMarks.has(type)) return 'mark'; return type; }; const isMark = (node) => typeOf(node) === 'mark'; const isStandardView = (node) => typeOf(node) === 'standardView'; const isStaticMark = (node) => { const { type } = node; if (typeof type !== 'string') return false; if (staticMarks.has(type)) return true; return false; }; const transform = (node) => { if (isStandardView(node)) return [node]; const type = typeOf(node); const composition = useComposition({ type, static: isStaticMark(node) }); return composition(node); }; // Some temporary variables help parse the view tree. const views = []; const viewNode = new Map(); const nodeState = new Map(); const discovered = [options]; const nodeGenerators = []; while (discovered.length) { const node = discovered.shift(); if (isStandardView(node)) { // Initialize view to get data to be visualized. If the marks // of the view have already been initialized (facet view), // initialize the view based on the initialized mark states, // otherwise initialize it from beginning. const state = nodeState.get(node); const [view, children] = state ? initializeState(state, node, library) : yield initializeView(node, context); viewNode.set(view, node); views.push(view); // Transform children, they will be transformed into // standardView if they are mark or view node. const transformedNodes = children .flatMap(transform) .map((d) => coordinate2Transform(d, library)); discovered.push(...transformedNodes); // Only StandardView can be treated as facet and it // should sync position scales among facets normally. if (transformedNodes.every(isStandardView)) { const states = yield Promise.all(transformedNodes.map((d) => initializeMarks(d, context))); // Note!!! // This will mutate scales for marks. syncFacetsScales(states); for (let i = 0; i < transformedNodes.length; i++) { const nodeT = transformedNodes[i]; const state = states[i]; nodeState.set(nodeT, state); } } } else { // Apply transform to get data in advance for non-mark composition // node, which makes sure that composition node can preprocess the // data to produce more nodes based on it. const n = isMark(node) ? node : yield applyTransform(node, context); const N = transform(n); if (Array.isArray(N)) discovered.push(...N); else if (typeof N === 'function') nodeGenerators.push(N()); } } context.emitter.emit(ChartEvent.BEFORE_PAINT); // Plot chart. const enterContainer = new Map(); const updateContainer = new Map(); const transitions = []; selection .selectAll(className(VIEW_CLASS_NAME)) .data(views, (d) => d.key) .join((enter) => enter .append('g') .attr('className', VIEW_CLASS_NAME) .attr('id', (view) => view.key) .call(applyTranslate) .each(function (view, i, element) { plotView(view, select(element), transitions, context); enterContainer.set(view, element); }), (update) => update.call(applyTranslate).each(function (view, i, element) { plotView(view, select(element), transitions, context); updateContainer.set(view, element); }), (exit) => exit .each(function (d, i, element) { // Remove existed interactions. const interactions = element['nameInteraction'].values(); for (const interaction of interactions) { interaction.destroy(); } }) .remove()); // Apply interactions. const viewInstanceof = (viewContainer, updateInteractions, oldStore) => { return Array.from(viewContainer.entries()).map(([view, container]) => { // Index state by component or interaction name, // such as legend, scrollbar, brushFilter. // Each state transform options to another options. const store = oldStore || new Map(); const setState = (key, reducer = (x) => x) => store.set(key, reducer); const options = viewNode.get(view); const update = createUpdateView(select(container), options, context); return { view, container, options, setState, update: (from, updateTypes) => __awaiter(this, void 0, void 0, function* () { // Apply all state functions to get new options. const reducer = compose(Array.from(store.values())); const newOptions = reducer(options); return yield update(newOptions, from, () => { if (isArray(updateTypes)) { updateInteractions(viewContainer, updateTypes, store); } }); }), }; }); }; const updateInteractions = (container = updateContainer, updateType, oldStore) => { var _a; // Interactions for update views. const updateViewInstances = viewInstanceof(container, updateInteractions, oldStore); for (const target of updateViewInstances) { const { options, container } = target; const nameInteraction = container['nameInteraction']; let typeOptions = inferInteraction(options); if (updateType) { typeOptions = typeOptions.filter((v) => updateType.includes(v[0])); } for (const typeOption of typeOptions) { const [type, option] = typeOption; // Remove interaction for existed views. const prevInteraction = nameInteraction.get(type); if (prevInteraction) (_a = prevInteraction.destroy) === null || _a === void 0 ? void 0 : _a.call(prevInteraction); // Apply new interaction. if (option) { const interaction = useThemeInteraction(target.view, type, option, useInteraction); const destroy = interaction(target, updateViewInstances, context.emitter); nameInteraction.set(type, { destroy }); } } } }; // Interactions for enter views. const enterViewInstances = viewInstanceof(enterContainer, updateInteractions); for (const target of enterViewInstances) { const { options } = target; // A Map index interaction by interaction name. const nameInteraction = new Map(); target.container['nameInteraction'] = nameInteraction; // Apply interactions. for (const typeOption of inferInteraction(options)) { const [type, option] = typeOption; if (option) { const interaction = useThemeInteraction(target.view, type, option, useInteraction); const destroy = interaction(target, enterViewInstances, context.emitter); nameInteraction.set(type, { destroy }); } } } updateInteractions(); // Author animations. const { width, height } = options; const keyframes = []; for (const nodeGenerator of nodeGenerators) { // Delay the rendering of animation keyframe. Different animation // created by different nodeGenerator will play in the same time. // eslint-disable-next-line no-async-promise-executor const keyframe = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { for (const node of nodeGenerator) { const sizedNode = Object.assign({ width, height }, node); yield plot(sizedNode, selection, context); } resolve(); })); keyframes.push(keyframe); } context.views = views; // Clear and update animation. (_a = context.animations) === null || _a === void 0 ? void 0 : _a.forEach((animation) => animation === null || animation === void 0 ? void 0 : animation.cancel()); context.animations = transitions; context.emitter.emit(ChartEvent.AFTER_PAINT); // Note!!! // The returned promise will never resolved if one of nodeGenerator // never stop to yield node, which may created by a keyframe composition // with iteration count set to infinite. const finished = transitions .filter(defined) .map(cancel) .map((d) => d.finished); return Promise.all([...finished, ...keyframes]); }); } function applyTranslate(selection) { selection.style('transform', (d) => `translate(${d.layout.x}, ${d.layout.y})`); } function definedInteraction(library) { const [, createInteraction] = useLibrary('interaction', library); return (d) => { const [name, options] = d; try { return [name, createInteraction(name)]; } catch (_a) { return [name, options.type]; } }; } function createUpdateView(selection, options, context) { const { library } = context; const createDefinedInteraction = definedInteraction(library); const filter = (d) => d[1] && d[1].props && d[1].props.reapplyWhenUpdate; const interactions = inferInteraction(options); const updates = interactions .map(createDefinedInteraction) .filter(filter) .map((d) => d[0]); return (newOptions, source, callback) => __awaiter(this, void 0, void 0, function* () { const transitions = []; const [newView, newChildren] = yield initializeView(newOptions, context); plotView(newView, selection, transitions, context); // Update interaction need to reapply when update. for (const name of updates.filter((d) => d !== source)) { updateInteraction(name, selection, newOptions, newView, context); } for (const child of newChildren) { plot(child, selection, context); } callback(); return { options: newOptions, view: newView }; }); } function updateInteraction(name, selection, options, view, context) { var _a; const { library } = context; const [useInteraction] = useLibrary('interaction', library); // Instances for interaction. const container = selection.node(); const nameInteraction = container['nameInteraction']; const interactionOptions = inferInteraction(options).find(([d]) => d === name); // Destroy older interaction. const interaction = nameInteraction.get(name); if (!interaction) return; (_a = interaction.destroy) === null || _a === void 0 ? void 0 : _a.call(interaction); if (!interactionOptions[1]) return; // Apply new interaction. const applyInteraction = useThemeInteraction(view, name, interactionOptions[1], useInteraction); const target = { options, view, container: selection.node(), update: (options) => Promise.resolve(options), }; const destroy = applyInteraction(target, [], context.emitter); nameInteraction.set(name, { destroy }); } function initializeView(options, context) { return __awaiter(this, void 0, void 0, function* () { const { library } = context; const flattenOptions = yield transformMarks(options, context); const mergedOptions = bubbleOptions(flattenOptions); // @todo Remove this. // !!! NOTE: Mute original view options. // Update interaction and coordinate for this view. options.interaction = mergedOptions.interaction; options.coordinate = mergedOptions.coordinate; // @ts-ignore options.marks = [...mergedOptions.marks, ...mergedOptions.components]; const transformedOptions = coordinate2Transform(mergedOptions, library); const state = yield initializeMarks(transformedOptions, context); return initializeState(state, transformedOptions, library); }); } function bubbleOptions(options) { const { coordinate: viewCoordinate = {}, interaction: viewInteraction = {}, style: viewStyle = {}, marks } = options, rest = __rest(options, ["coordinate", "interaction", "style", "marks"]); const markCoordinates = marks.map((d) => d.coordinate || {}); const markInteractions = marks.map((d) => d.interaction || {}); const markViewStyles = marks.map((d) => d.viewStyle || {}); const newCoordinate = [...markCoordinates, viewCoordinate].reduceRight((prev, cur) => deepMix(prev, cur), {}); const newInteraction = [viewInteraction, ...markInteractions].reduce((prev, cur) => deepMix(prev, cur), {}); const newStyle = [...markViewStyles, viewStyle].reduce((prev, cur) => deepMix(prev, cur), {}); return Object.assign(Object.assign({}, rest), { marks, coordinate: newCoordinate, interaction: newInteraction, style: newStyle }); } function transformMarks(options, context) { return __awaiter(this, void 0, void 0, function* () { const { library } = context; const [useMark, createMark] = useLibrary('mark', library); const staticMarks = new Set(Object.keys(library) .map((d) => { var _a; return (_a = /component\.(.*)/.exec(d)) === null || _a === void 0 ? void 0 : _a[1]; }) .filter(defined)); const { marks } = options; const flattenMarks = []; const components = []; const discovered = [...marks]; const { width, height } = computeRoughPlotSize(options); const markOptions = { options, width, height }; // Pre order traversal. while (discovered.length) { const [node] = discovered.splice(0, 1); // Apply data transform to get data. const mark = (yield applyTransform(node, context)); const { type = error('G2Mark type is required.'), key } = mark; // For components. if (staticMarks.has(type)) components.push(mark); else { const { props = {} } = createMark(type); const { composite = true } = props; if (!composite) flattenMarks.push(mark); else { // Unwrap data from { value: data } to data, // then the composite mark can process the normalized data. const { data } = mark; const newMark = Object.assign(Object.assign({}, mark), { data: data ? (Array.isArray(data) ? data : data.value) : data }); // Convert composite mark to marks. const marks = yield useMark(newMark, markOptions); const M = Array.isArray(marks) ? marks : [marks]; discovered.unshift(...M.map((d, i) => (Object.assign(Object.assign({}, d), { key: `${key}-${i}` })))); } } } return Object.assign(Object.assign({}, options), { marks: flattenMarks, components }); }); } function initializeMarks(options, context) { return __awaiter(this, void 0, void 0, function* () { const { library } = context; const [useTheme] = useLibrary('theme', library); const [, createMark] = useLibrary('mark', library); const { theme: partialTheme, marks: partialMarks, coordinates = [], } = options; const theme = useTheme(inferTheme(partialTheme)); const markState = new Map(); // Initialize channels for marks. for (const markOptions of partialMarks) { const { type } = markOptions; const { props = {} } = createMark(type); const markAndState = yield initializeMark(markOptions, props, context); if (markAndState) { const [initializedMark, state] = markAndState; markState.set(initializedMark, state); } } // Group channels by scale key, each group has scale. const scaleChannels = group(Array.from(markState.values()).flatMap((d) => d.channels), ({ scaleKey }) => scaleKey); // Infer scale for each channel groups. for (const channels of scaleChannels.values()) { // Merge scale options for these channels. const scaleOptions = channels.reduce((total, { scale }) => deepMix(total, scale), {}); const { scaleKey } = channels[0]; // Use the fields of the first channel as the title. const { values: FV } = channels[0]; const fields = Array.from(new Set(FV.map((d) => d.field).filter(defined))); const options = deepMix({ guide: { title: fields.length === 0 ? undefined : fields }, field: fields[0], }, scaleOptions); // Use the name of the first channel as the scale name. const { name } = channels[0]; const values = channels.flatMap(({ values }) => values.map((d) => d.value)); const scale = Object.assign(Object.assign({}, inferScale(name, values, options, coordinates, theme, library)), { key: scaleKey }); channels.forEach((channel) => (channel.scale = scale)); } return markState; }); } function useThemeInteraction(view, type, option, useInteraction) { const theme = view.theme; const defaults = typeof type === 'string' ? theme[type] || {} : {}; const interaction = useInteraction(deepMix(defaults, Object.assign({ type }, option))); return interaction; } function initializeState(markState, options, library) { const [useMark] = useLibrary('mark', library); const [useTheme] = useLibrary('theme', library); const [useLabelTransform] = useLibrary('labelTransform', library); const { key, frame = false, theme: partialTheme, clip, style = {}, labelTransform = [], } = options; const theme = useTheme(inferTheme(partialTheme)); // Infer components and compute layout. const states = Array.from(markState.values()); const scales = collectScales(states, options); const components = normalizeComponents(inferComponent(inferComponentScales(Array.from(scales), states, markState), options, library)); const layout = computeLayout(components, options, theme, library); const coordinate = createCoordinate(layout, options, library); const framedStyle = frame ? deepMix({ mainLineWidth: 1, mainStroke: '#000' }, style) : style; // Place components and mutate their bbox. placeComponents(groupComponents(components), coordinate, layout); // AxisZ need a copy of axisX and axisY to show grids in X-Z & Y-Z planes. processAxisZ(components); // Scale from marks and components. const scaleInstance = {}; // Initialize scale from components. for (const component of components) { const { scales: scaleDescriptors = [] } = component; const scales = []; for (const descriptor of scaleDescriptors) { const { name } = descriptor; const scale = useRelationScale(descriptor, library); scales.push(scale); // Delivery the scale of axisX to the AxisY, // in order to calculate the angle of axisY component when rendering radar chart. if (name === 'y') { scale.update(Object.assign(Object.assign({}, scale.getOptions()), { xScale: scaleInstance.x })); } assignScale(scaleInstance, { [name]: scale }); } component.scaleInstances = scales; } // Calc data to be rendered for each mark. // @todo More readable APIs for Container which stays // the same style with JS standard and lodash APIs. // @todo More proper way to index scale for different marks. const children = []; for (const [mark, state] of markState.entries()) { const { // scale, // Callback to create children options based on this mark. children: createChildren, // The total count of data (both show and hide)for this facet. // This is for unit visualization to sync data domain. dataDomain, modifier, key: markKey, } = mark; const { index, channels, tooltip } = state; const scale = Object.fromEntries(channels.map(({ name, scale }) => [name, scale])); // Transform abstract value to visual value by scales. const markScaleInstance = mapObject(scale, (options) => { return useRelationScale(options, library); }); assignScale(scaleInstance, markScaleInstance); const value = applyScale(channels, markScaleInstance); // Calc points and transformation for each data, // and then transform visual value to visual data. const calcPoints = useMark(mark); const [I, P, S] = filterValid(calcPoints(index, markScaleInstance, value, coordinate)); const count = dataDomain || I.length; const T = modifier ? modifier(P, count, layout) : []; const titleOf = (i) => { var _a, _b; return (_b = (_a = tooltip.title) === null || _a === void 0 ? void 0 : _a[i]) === null || _b === void 0 ? void 0 : _b.value; }; const itemsOf = (i) => tooltip.items.map((V) => V[i]); const visualData = I.map((d, i) => { const datum = Object.assign({ points: P[i], transform: T[i], index: d, markKey, viewKey: key }, (tooltip && { title: titleOf(d), items: itemsOf(d), })); for (const [k, V] of Object.entries(value)) { datum[k] = V[d]; if (S) datum[`series${upperFirst(k)}`] = S[i].map((i) => V[i]); } if (S) datum['seriesIndex'] = S[i]; if (S && tooltip) { datum['seriesItems'] = S[i].map((si) => itemsOf(si)); datum['seriesTitle'] = S[i].map((si) => titleOf(si)); } return datum; }); state.data = visualData; state.index = I; // Create children options by children callback, // and then propagate data to each child. const markChildren = createChildren === null || createChildren === void 0 ? void 0 : createChildren(visualData, markScaleInstance, layout); children.push(...(markChildren || [])); } const view = { layout, theme, coordinate, markState, key, clip, scale: scaleInstance, style: framedStyle, components, labelTransform: compose(labelTransform.map(useLabelTransform)), }; return [view, children]; } function plotView(view, selection, transitions, context) { return __awaiter(this, void 0, void 0, function* () { const { library } = context; const { components, theme, layout, markState, coordinate, key, style, clip, scale, } = view; // Render background for the different areas. const { x, y, width, height } = layout, rest = __rest(layout, ["x", "y", "width", "height"]); const areaKeys = ['view', 'plot', 'main', 'content']; const I = areaKeys.map((_, i) => i); const sizeKeys = ['a', 'margin', 'padding', 'inset']; const areaStyles = areaKeys.map((d) => maybeSubObject(Object.assign({}, theme.view, style), d)); const areaSizes = sizeKeys.map((d) => subObject(rest, d)); const styleArea = (selection) => selection .style('x', (i) => areaLayouts[i].x) .style('y', (i) => areaLayouts[i].y) .style('width', (i) => areaLayouts[i].width) .style('height', (i) => areaLayouts[i].height) .each(function (i, d, element) { applyStyle(select(element), areaStyles[i]); }); let px = 0; let py = 0; let pw = width; let ph = height; const areaLayouts = I.map((i) => { const size = areaSizes[i]; const { left = 0, top = 0, bottom = 0, right = 0 } = size; px += left; py += top; pw -= left + right; ph -= top + bottom; return { x: px, y: py, width: pw, height: ph, }; }); selection .selectAll(className(AREA_CLASS_NAME)) .data( // Only render area with defined style. I.filter((i) => defined(areaStyles[i])), (i) => areaKeys[i]) .join((enter) => enter .append('rect') .attr('className', AREA_CLASS_NAME) .style('zIndex', -2) .call(styleArea), (update) => update.call(styleArea), (exit) => exit.remove()); const animationExtent = computeAnimationExtent(markState); const componentAnimateOptions = animationExtent ? { duration: animationExtent[1] } : false; // Render components. // @todo renderComponent return ctor and options. // Key for each type of component. // Index them grouped by position. for (const [, C] of groups(components, (d) => `${d.type}-${d.position}`)) { C.forEach((d, i) => (d.index = i)); } const componentsTransitions = selection .selectAll(className(COMPONENT_CLASS_NAME)) .data(components, (d) => `${d.type}-${d.position}-${d.index}`) .join((enter) => enter .append('g') .style('zIndex', ({ zIndex }) => zIndex || -1) .attr('className', COMPONENT_CLASS_NAME) .append((options) => renderComponent(deepMix({ animate: componentAnimateOptions, scale }, options), coordinate, theme, library, markState)), (update) => update.transition(function (options, i, element) { const { preserve = false } = options; if (preserve) return; const newComponent = renderComponent(deepMix({ animate: componentAnimateOptions, scale }, options), coordinate, theme, library, markState); const { attributes } = newComponent; const [node] = element.childNodes; return node.update(attributes, false); })) .transitions(); transitions.push(...componentsTransitions.flat().filter(defined)); // Main layer is for showing the main visual representation such as marks. There // may be multiple main layers for a view, each main layer correspond to one of marks. // @todo Test DOM structure. const T = selection .selectAll(className(PLOT_CLASS_NAME)) .data([layout], () => key) .join((enter) => enter // Make this layer interactive, such as click and mousemove events. .append('rect') .style('zIndex', 0) .style('fill', 'transparent') .attr('className', PLOT_CLASS_NAME) .call(updateBBox) .call(updateLayers, Array.from(markState.keys())) .call(applyClip, clip), (update) => update .call(updateLayers, Array.from(markState.keys())) .call((selection) => { return animationExtent ? animateBBox(selection, animationExtent) : updateBBox(selection); }) .call(applyClip, clip)) .transitions(); transitions.push(...T.flat()); // Render marks with corresponding data. for (const [mark, state] of markState.entries()) { const { data } = state; const { key, class: cls, type } = mark; const viewNode = selection.select(`#${key}`); const shapeFunction = createMarkShapeFunction(mark, state, view, context); const enterFunction = createEnterFunction(mark, state, view, library); const updateFunction = createUpdateFunction(mark, state, view, library); const exitFunction = createExitFunction(mark, state, view, library); const facetElements = selectFacetElements(selection, viewNode, cls, 'element'); const T = viewNode .selectAll(className(ELEMENT_CLASS_NAME)) .selectFacetAll(facetElements) .data(data, (d) => d.key, (d) => d.groupKey) .join((enter) => enter .append(shapeFunction) // Note!!! Only one className can be set. // Using attribute as alternative for other classNames. .attr('className', ELEMENT_CLASS_NAME) .attr('markType', type) .transition(function (data, i, element) { return enterFunction(data, [element]); }), (update) => update.call((selection) => { const parent = selection.parent(); const origin = useMemo((node) => { const [x, y] = node.getBounds().min; return [x, y]; }); selection .transition(function (data, index, element) { maybeFacetElement(element, parent, origin); const node = shapeFunction(data, index); const animation = updateFunction(data, [element], [node]); if (animation !== null) return animation; if (element.nodeName === node.nodeName && node.nodeName !== 'g') { copyAttributes(element, node); } else { element.parentNode.replaceChild(node, element); node.className = ELEMENT_CLASS_NAME; // @ts-ignore node.markType = type; // @ts-ignore node.__data__ = element.__data__; } return animation; }) .attr('markType', type) .attr('className', ELEMENT_CLASS_NAME); }), (exit) => { return exit .each(function (d, i, element) { element.__removed__ = true; }) .transition(function (data, i, element) { return exitFunction(data, [element]); }) .remove(); }, (merge) => merge // Append elements to be merged. .append(shapeFunction) .attr('className', ELEMENT_CLASS_NAME) .attr('markType', type) .transition(function (data, i, element) { // Remove merged elements after animation finishing. const { __fromElements__: fromElements } = element; const transition = updateFunction(data, fromElements, [element]); const exit = new Selection(fromElements, null, element.parentNode); exit.transition(transition).remove(); return transition; }), (split) => split .transition(function (data, i, element) { // Append splitted shapes. const enter = new Selection([], element.__toData__, element.parentNode); const toElements = enter .append(shapeFunction) .attr('className', ELEMENT_CLASS_NAME) .attr('markType', type) .nodes(); return updateFunction(data, [element], toElements); }) // Remove elements to be splitted after animation finishing. .remove()) .transitions(); transitions.push(...T.flat()); } // Plot label for this view. plotLabel(view, selection, transitions, library, context); }); } /** * Auto hide labels be specify label layout. */ function plotLabel(view, selection, transitions, library, context) { const [useLabelTransform] = useLibrary('labelTransform', library); const { markState, labelTransform } = view; const labelLayer = selection.select(className(LABEL_LAYER_CLASS_NAME)).node(); // A Map index shapeFunction by label. const labelShapeFunction = new Map(); // A Map index options by label. const labelDescriptor = new Map(); // Get all labels for this view. const labels = Array.from(markState.entries()).flatMap(([mark, state]) => { const { labels: labelOptions = [], key } = mark; const shapeFunction = createLabelShapeFunction(mark, state, view, library, context); const elements = selection .select(`#${key}`) .selectAll(className(ELEMENT_CLASS_NAME)) .nodes() // Only select the valid element. .filter((n) => !n.__removed__); return labelOptions.flatMap((labelOption, i) => { const { transform = [] } = labelOption, options = __rest(labelOption, ["transform"]); return elements.flatMap((e) => { const L = getLabels(options, i, e); L.forEach((l) => { labelShapeFunction.set(l, shapeFunction); labelDescriptor.set(l, labelOption); }); return L; }); }); }); // Render all labels. const labelShapes = select(labelLayer) .selectAll(className(LABEL_CLASS_NAME)) .data(labels, (d) => d.key) .join((enter) => enter .append((d) => labelShapeFunction.get(d)(d)) .attr('className', LABEL_CLASS_NAME), (update) => update.each(function (d, i, element) { // @todo Handle Label with different type. const shapeFunction = labelShapeFunction.get(d); const node = shapeFunction(d); copyAttributes(element, node); }), (exit) => exit.remove()) .nodes(); // Apply group-level transforms. const labelGroups = group(labelShapes, (d) => labelDescriptor.get(d.__data__)); const { coordinate } = view; const labelTransformContext = { canvas: context.canvas, coordinate, }; for (const [label, shapes] of labelGroups) { const { transform = [] } = label; const transformFunction = compose(transform.map(useLabelTransform)); transformFunction(shapes, labelTransformContext); } // Apply view-level transform. if (labelTransform) { labelTransform(labelShapes, labelTransformContext); } } function getLabels(label, labelIndex, element) { const { seriesIndex: SI, seriesKey, points, key, index } = element.__data__; const bounds = getLocalBounds(element); if (!SI) { return [ Object.assign(Object.assign({}, label), { key: `${key}-${labelIndex}`, bounds, index, points, dependentElement: element }), ]; } const selector = normalizeLabelSelector(label); const F = SI.map((index, i) => (Object.assign(Object.assign({}, label), { key: `${seriesKey[i]}-${labelIndex}`, bounds: [points[i]], index, points, dependentElement: element }))); return selector ? selector(F) : F; } function filterValid([I, P, S]) { if (S) return [I, P, S]; const definedIndex = []; const definedPoints = []; for (let i = 0; i < I.length; i++) { const d = I[i]; const p = P[i]; if (p.every(([x, y]) => defined(x) && defined(y))) { definedIndex.push(d); definedPoints.push(p); } } return [definedIndex, definedPoints]; } function normalizeLabelSelector(label) { const { selector } = label; if (!selector) return null; if (typeof selector === 'function') return selector; if (selector === 'first') return (I) => [I[0]]; if (selector === 'last') return (I) => [I[I.length - 1]]; throw new Error(`Unknown selector: ${selector}`); } /** * Avoid getting error bounds caused by element animations. * @todo Remove this temporary handle method, if runtime supports * correct process: drawElement, do label layout and then do * transitions together. */ function getLocalBounds(element) { const cloneElement = element.cloneNode(); const animations = element.getAnimations(); cloneElement.style.visibility = 'hidden'; animations.forEach((animation) => { const keyframes = animation.effect.getKeyframes(); cloneElement.attr(keyframes[keyframes.length - 1]); }); element.parentNode.appendChild(cloneElement); const bounds = cloneElement.getLocalBounds(); cloneElement.destroy(); const { min, max } = bounds; return [min, max]; } function createLabelShapeFunction(mark, state, view, library, context) { const [useShape] = useLibrary('shape', library); const { data: abstractData, encode } = mark; const { data: visualData, defaultLabelShape } = state; const point2d = visualData.map((d) => d.points); const channel = mapObject(encode, (d) => d.value); // Assemble Context. const { theme, coordinate } = view; const shapeContext = Object.assign(Object.assign({}, context), { document: documentOf(context), theme, coordinate }); return (options) => { // Computed values from data and styles. const { index, points } = options; const datum = abstractData[index]; const { formatter = (d) => `${d}`, transform, style: abstractStyle, render } = options, abstractOptions = __rest(options, ["formatter", "transform", "style", "render"]); const visualOptions = mapObject(Object.assign(Object.assign({}, abstractOptions), abstractStyle), (d) => valueOf(d, datum, index, abstractData, { channel })); const { shape = defaultLabelShape, text } = visualOptions, style = __rest(visualOptions, ["shape", "text"]); const f = typeof formatter === 'string' ? format(formatter) : formatter; const value = Object.assign(Object.assign({}, style), { text: f(text, datum, index, abstractData), datum }); // Params for create shape. const shapeOptions = Object.assign({ type: `label.${shape}`, render }, style); const shapeFunction = useShape(shapeOptions, shapeContext); const defaults = getDefaultsStyle(theme, 'label', shape, 'label'); return shapeFunction(points, value, defaults, point2d); }; } function valueOf(value, datum, i, data, options) { if (typeof value === 'function') return value(datum, i, data, options); if (typeof value !== 'string') return value; if (isStrictObject(datum) && datum[value] !== undefined) return datum[value]; return value; } /** * Compute max duration for this frame. */ function computeAnimationExtent(markState) { let maxDuration = -Infinity; let minDelay = Infinity; for (const [mark, state] of markState) { const { animate = {} } = mark; const { data } = state; const { enter = {}, update = {}, exit = {} } = animate; const { type: defaultUpdateType, duration: defaultUpdateDuration = 300, delay: defaultUpdateDelay = 0, } = update; const { type: defaultEnterType, duration: defaultEnterDuration = 300, delay: defaultEnterDelay = 0, } = enter; const { type: defaultExitType, duration: defaultExitDuration = 300, delay: defaultExitDelay = 0, } = exit; for (const d of data) { const { updateType = defaultUpdateType, updateDuration = defaultUpdateDuration, updateDelay = defaultUpdateDelay, enterType = defaultEnterType, enterDuration = defaultEnterDuration, enterDelay = defaultEnterDelay, exitDuration = defaultExitDuration, exitDelay = defaultExitDelay, exitType = defaultExitType, } = d; if (updateType === undefined || updateType) { maxDuration = Math.max(maxDuration, updateDuration + updateDelay); minDelay = Math.min(minDelay, updateDelay); } if (exitType === undefined || exitType) { maxDuration = Math.max(maxDuration, exitDuration + exitDelay); minDelay = Math.min(minDelay, exitDelay); } if (enterType === undefined || enterType) { maxDuration = Math.max(maxDuration, enterDuration + enterDelay); minDelay = Math.min(minDelay, enterDelay); } } } if (maxDuration === -Infinity) return null; return [minDelay, maxDuration - minDelay]; } function selectFacetElements(selection, current, facetClassName, elementClassName) { const group = selection.node().parentElement; return group .findAll((node) => node.style.facet !== undefined && node.style.facet === facetClassName && node !== current.node()) .flatMap((node) => node.getElementsByClassName(elementClassName)); } /** * Update the parent of element and apply transform to make it * stay in original position. */ function maybeFacetElement(element, parent, originOf) { if (!element.__facet__) return; // element -> g#main -> rect#plot const prePlot = element.parentNode.parentNode; // g#main -> rect#plot const newPlot = parent.parentNode; const [px, py] = originOf(prePlot); const [x, y] = originOf(newPlot); const translate = `translate(${px - x}, ${py - y})`; appendTransform(element, translate); parent.append(element); } function createMarkShapeFunction(mark, state, view, context) { const { library } = context; const [useShape] = useLibrary('shape', library); const { data: abstractData, encode } = mark; const { defaultShape, data, shape: shapeLibrary } = state; const channel = mapObject(encode, (d) => d.value); const point2d = data.map((d) => d.points); const { theme, coordinate } = view; const { type: markType, style = {} } = mark; const shapeContext = Object.assign(Object.assign({}, context), { document: documentOf(context), coordinate, theme }); return (data) => { const { shape: styleShape = defaultShape } = style; const { shape = styleShape, points, seriesIndex, index: i } = data, v = __rest(data, ["shape", "points", "seriesIndex", "index"]); const value = Object.assign(Object.assign({}, v), { index: i }); // Get data-driven style. // If it is a series shape, such as area and line, // provides the series of abstract data and indices // for this shape, otherwise the single datum and // index. const abstractDatum = seriesIndex ? seriesIndex.map((i) => abstractData[i]) : abstractData[i]; const I = seriesIndex ? seriesIndex : i; const visualStyle = mapObject(style, (d) => valueOf(d, abstractDatum, I, abstractData, { channel })); // Try get shape from mark first, then from library. const shapeFunction = shapeLibrary[shape] ? shapeLibrary[shape](visualStyle, shapeContext) : useShape(Object.assign(Object.assign({}, visualStyle), { type: shapeName(mark, shape) }), shapeContext); const defaults = getDefaultsStyle(theme, markType, shape, defaultShape); return shapeFunction(points, value, defaults, point2d); }; } function getDefaultsStyle(theme, mark, shape, defaultShape) { if (typeof mark !== 'string') return; const { color } = theme; const markTheme = theme[mark] || {}; const shapeTheme = markTheme[shape] || markTheme[defaultShape]; return Object.assign({ color }, shapeTheme); } function createAnimationFunction(type, mark, state, view, library) { var _a, _b; const [, createShape] = useLibrary('shape', library); const [useAnimation] = useLibrary('animation', library); const { defaultShape, shape: shapeLibrary } = state; const { theme, coordinate } = view; const upperType = upperFirst(type); const key = `default${upperType}Animation`; // Get shape from mark first, then from library. const { [key]: defaultAnimation } = ((_a = shapeLibrary[defaultShape]) === null || _a === void 0 ? void 0 : _a.props) || createShape(shapeName(mark, defaultShape)).props; const { [type]: defaultEffectTiming = {} } = theme; const animate = ((_b = mark.animate) === null || _b === void 0 ? void 0 : _b[type]) || {}; const context = { coordinate }; return (data, from, to) => { const { [`${type}Type`]: animation, [`${type}Delay`]: delay, [`${type}Duration`]: duration, [`${type}Easing`]: easing, } = data; const options = Object.assign({ type: animation || defaultAnimation }, animate); if (!options.type) return null; const animateFunction = useAnimation(options, context); const value = { delay, duration, easing }; const A = animateFunction(from, to, deepMix(defaultEffectTiming, value)); if (!Array.isArray(A)) return [A]; return A; }; } function createEnterFunction(mark, state, view, library) { return createAnimationFunction('enter', mark, state, view, library); } /** * Animation will not cancel automatically,