UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

1,016 lines 56.8 kB
"use strict"; 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; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.applyStyle = exports.plot = void 0; const g_1 = require("@antv/g"); const util_1 = require("@antv/util"); const d3_array_1 = require("d3-array"); const d3_format_1 = require("d3-format"); const array_1 = require("../utils/array"); const event_1 = require("../utils/event"); const helper_1 = require("../utils/helper"); const selection_1 = require("../utils/selection"); const component_1 = require("./component"); const constant_1 = require("./constant"); const coordinate_1 = require("./coordinate"); const layout_1 = require("./layout"); const library_1 = require("./library"); const mark_1 = require("./mark"); const scale_1 = require("./scale"); const transform_1 = require("./transform"); function plot(options, selection, context) { var _a; return __awaiter(this, void 0, void 0, function* () { const { library } = context; const [useComposition] = (0, library_1.useLibrary)('composition', library); const [useInteraction] = (0, library_1.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(helper_1.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(helper_1.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) => (0, coordinate_1.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. (0, scale_1.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(event_1.ChartEvent.BEFORE_PAINT); // Plot chart. const enterContainer = new Map(); const updateContainer = new Map(); const transitions = []; selection .selectAll(className(constant_1.VIEW_CLASS_NAME)) .data(views, (d) => d.key) .join((enter) => enter .append('g') .attr('className', constant_1.VIEW_CLASS_NAME) .attr('id', (view) => view.key) .call(applyTranslate) .each(function (view, i, element) { plotView(view, (0, selection_1.select)(element), transitions, context); enterContainer.set(view, element); }), (update) => update.call(applyTranslate).each(function (view, i, element) { plotView(view, (0, selection_1.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((0, selection_1.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 = (0, helper_1.compose)(Array.from(store.values())); const newOptions = reducer(options); return yield update(newOptions, from, () => { if ((0, util_1.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(event_1.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(helper_1.defined) .map(cancel) .map((d) => d.finished); return Promise.all([...finished, ...keyframes]); }); } exports.plot = plot; function applyTranslate(selection) { selection.style('transform', (d) => `translate(${d.layout.x}, ${d.layout.y})`); } function definedInteraction(library) { const [, createInteraction] = (0, library_1.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] = (0, library_1.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 = (0, coordinate_1.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) => (0, util_1.deepMix)(prev, cur), {}); const newInteraction = [viewInteraction, ...markInteractions].reduce((prev, cur) => (0, util_1.deepMix)(prev, cur), {}); const newStyle = [...markViewStyles, viewStyle].reduce((prev, cur) => (0, util_1.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] = (0, library_1.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(helper_1.defined)); const { marks } = options; const flattenMarks = []; const components = []; const discovered = [...marks]; const { width, height } = (0, layout_1.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 = (0, helper_1.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] = (0, library_1.useLibrary)('theme', library); const [, createMark] = (0, library_1.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 (0, mark_1.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 = (0, d3_array_1.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 }) => (0, util_1.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(helper_1.defined))); const options = (0, util_1.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({}, (0, scale_1.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((0, util_1.deepMix)(defaults, Object.assign({ type }, option))); return interaction; } function initializeState(markState, options, library) { const [useMark] = (0, library_1.useLibrary)('mark', library); const [useTheme] = (0, library_1.useLibrary)('theme', library); const [useLabelTransform] = (0, library_1.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 = (0, scale_1.collectScales)(states, options); const components = (0, component_1.normalizeComponents)((0, component_1.inferComponent)(inferComponentScales(Array.from(scales), states, markState), options, library)); const layout = (0, layout_1.computeLayout)(components, options, theme, library); const coordinate = (0, coordinate_1.createCoordinate)(layout, options, library); const framedStyle = frame ? (0, util_1.deepMix)({ mainLineWidth: 1, mainStroke: '#000' }, style) : style; // Place components and mutate their bbox. (0, layout_1.placeComponents)((0, component_1.groupComponents)(components), coordinate, layout); // AxisZ need a copy of axisX and axisY to show grids in X-Z & Y-Z planes. (0, layout_1.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 = (0, scale_1.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 })); } (0, scale_1.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 = (0, array_1.mapObject)(scale, (options) => { return (0, scale_1.useRelationScale)(options, library); }); (0, scale_1.assignScale)(scaleInstance, markScaleInstance); const value = (0, scale_1.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${(0, util_1.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: (0, helper_1.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) => (0, helper_1.maybeSubObject)(Object.assign({}, theme.view, style), d)); const areaSizes = sizeKeys.map((d) => (0, helper_1.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((0, selection_1.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(constant_1.AREA_CLASS_NAME)) .data( // Only render area with defined style. I.filter((i) => (0, helper_1.defined)(areaStyles[i])), (i) => areaKeys[i]) .join((enter) => enter .append('rect') .attr('className', constant_1.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 (0, d3_array_1.groups)(components, (d) => `${d.type}-${d.position}`)) { C.forEach((d, i) => (d.index = i)); } const componentsTransitions = selection .selectAll(className(constant_1.COMPONENT_CLASS_NAME)) .data(components, (d) => `${d.type}-${d.position}-${d.index}`) .join((enter) => enter .append('g') .style('zIndex', ({ zIndex }) => zIndex || -1) .attr('className', constant_1.COMPONENT_CLASS_NAME) .append((options) => (0, component_1.renderComponent)((0, util_1.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 = (0, component_1.renderComponent)((0, util_1.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(helper_1.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(constant_1.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', constant_1.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(constant_1.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', constant_1.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 = (0, helper_1.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') { (0, helper_1.copyAttributes)(element, node); } else { element.parentNode.replaceChild(node, element); node.className = constant_1.ELEMENT_CLASS_NAME; // @ts-ignore node.markType = type; // @ts-ignore node.__data__ = element.__data__; } return animation; }) .attr('markType', type) .attr('className', constant_1.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', constant_1.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_1.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_1.Selection([], element.__toData__, element.parentNode); const toElements = enter .append(shapeFunction) .attr('className', constant_1.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] = (0, library_1.useLibrary)('labelTransform', library); const { markState, labelTransform } = view; const labelLayer = selection.select(className(constant_1.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(constant_1.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 = (0, selection_1.select)(labelLayer) .selectAll(className(constant_1.LABEL_CLASS_NAME)) .data(labels, (d) => d.key) .join((enter) => enter .append((d) => labelShapeFunction.get(d)(d)) .attr('className', constant_1.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); (0, helper_1.copyAttributes)(element, node); }), (exit) => exit.remove()) .nodes(); // Apply group-level transforms. const labelGroups = (0, d3_array_1.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 = (0, helper_1.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]) => (0, helper_1.defined)(x) && (0, helper_1.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] = (0, library_1.useLibrary)('shape', library); const { data: abstractData, encode } = mark; const { data: visualData, defaultLabelShape } = state; const point2d = visualData.map((d) => d.points); const channel = (0, array_1.mapObject)(encode, (d) => d.value); // Assemble Context. const { theme, coordinate } = view; const shapeContext = Object.assign(Object.assign({}, context), { document: (0, library_1.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 = (0, array_1.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' ? (0, d3_format_1.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 ((0, helper_1.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})`; (0, helper_1.appendTransform)(element, translate); parent.append(element); } function createMarkShapeFunction(mark, state, view, context) { const { library } = context; const [useShape] = (0, library_1.useLibrary)('shape', library); const { data: abstractData, encode } = mark; const { defaultShape, data, shape: shapeLibrary } = state; const channel = (0, array_1.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: (0, library_1.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 = (0, array_1.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] = (0, library_1.useLibrary)('shape', library); const [useAnimation] = (0, library_1.useLibrary)('animation', library); const { defaultShape, shape: shapeLibrary } = state; const { theme, coordinate } = view; const upperType = (0, util_1.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