| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043 |
- 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 } from '@antv/util';
- import { group } from 'd3-array';
- import { format } from 'd3-format';
- import { mapObject } from '../utils/array';
- import { ChartEvent } from '../utils/event';
- import { appendTransform, copyAttributes, defined, error, maybeSubObject, subObject, useMemo, } from '../utils/helper';
- import { select, Selection } from '../utils/selection';
- import { inferComponent, 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, placeComponents } from './layout';
- import { useLibrary } from './library';
- import { initializeMark } from './mark';
- import { applyScale, inferScale, syncFacetsScales, useRelationScale, } from './scale';
- import { applyDataTransform } from './transform';
- export function plot(options, selection, library, context) {
- var _a, _b;
- return __awaiter(this, void 0, void 0, function* () {
- 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 typeOf = (node) => {
- const { type } = node;
- if (typeof type === 'function') {
- // @ts-ignore
- const { props = {} } = type;
- const { composite = true } = props;
- if (composite)
- return 'mark';
- }
- return typeof type === 'string' && marks.has(type) ? 'mark' : type;
- };
- const isMark = (node) => typeOf(node) === 'mark';
- const isStandardView = (node) => typeOf(node) === 'standardView';
- const transform = (node) => {
- if (isStandardView(node))
- return [node];
- const type = typeOf(node);
- const composition = useComposition({ type });
- 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, library);
- 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, library)));
- // 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, library);
- 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) {
- plotView(view, select(this), transitions, library, context);
- enterContainer.set(view, this);
- }), (update) => update.call(applyTranslate).each(function (view) {
- plotView(view, select(this), transitions, library, context);
- updateContainer.set(view, this);
- }), (exit) => exit
- .each(function () {
- // Remove existed interactions.
- const interactions = this['nameInteraction'].values();
- for (const interaction of interactions) {
- interaction.destroy();
- }
- })
- .remove());
- // Apply interactions.
- const viewInstanceof = (viewContainer) => {
- return Array.from(viewContainer.entries()).map(([view, container]) => ({
- view,
- container,
- options: viewNode.get(view),
- update: createUpdateView(select(container), library, context),
- }));
- };
- // Interactions for enter views.
- const enterViewInstances = viewInstanceof(enterContainer);
- 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 = useInteraction(Object.assign({ type }, option));
- const destroy = interaction(target, enterViewInstances, context.emitter);
- nameInteraction.set(type, { destroy });
- }
- }
- }
- // Interactions for update views.
- const updateViewInstances = viewInstanceof(updateContainer);
- for (const target of updateViewInstances) {
- const { options, container } = target;
- const nameInteraction = container['nameInteraction'];
- for (const typeOption of inferInteraction(options)) {
- 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 = useInteraction(Object.assign({ type }, option));
- const destroy = interaction(target, updateViewInstances, context.emitter);
- nameInteraction.set(type, { destroy });
- }
- }
- }
- // 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, library, context);
- }
- resolve();
- }));
- keyframes.push(keyframe);
- }
- context.views = views;
- // Clear and update animation.
- (_b = context.animations) === null || _b === void 0 ? void 0 : _b.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 createUpdateView(selection, library, context) {
- return (newOptions) => __awaiter(this, void 0, void 0, function* () {
- const transitions = [];
- const [newView, newChildren] = yield initializeView(newOptions, library);
- plotView(newView, selection, transitions, library, context);
- updateTooltip(selection, newOptions, newView, library, context);
- for (const child of newChildren) {
- plot(child, selection, library, context);
- }
- return { options: newOptions, view: newView };
- });
- }
- function updateTooltip(selection, options, view, library, context) {
- var _a;
- const [useInteraction] = useLibrary('interaction', library);
- // Instances for tooltip.
- const container = selection.node();
- const nameInteraction = container['nameInteraction'];
- const tooltipOptions = inferInteraction(options).find(([d]) => d === 'tooltip');
- // Destroy older tooltip.
- const tooltip = nameInteraction.get('tooltip');
- if (!tooltip)
- return;
- (_a = tooltip.destroy) === null || _a === void 0 ? void 0 : _a.call(tooltip);
- if (!tooltipOptions[1])
- return;
- // Apply new tooltip interaction.
- const applyTooltip = useInteraction(Object.assign({ type: 'tooltip' }, tooltipOptions[1]));
- const target = {
- options,
- view,
- container: selection.node(),
- update: (options) => Promise.resolve(options),
- };
- applyTooltip(target, [], context.emitter);
- }
- function initializeView(options, library) {
- return __awaiter(this, void 0, void 0, function* () {
- const flattenOptions = yield transformMarks(options, library);
- 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;
- const transformedOptions = coordinate2Transform(mergedOptions, library);
- const state = yield initializeMarks(transformedOptions, library);
- 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, library) {
- return __awaiter(this, void 0, void 0, function* () {
- const [useMark, createMark] = useLibrary('mark', library);
- const { marks } = options;
- const flattenMarks = [];
- const discovered = [...marks];
- // Pre order traversal.
- while (discovered.length) {
- const [node] = discovered.splice(0, 1);
- // Apply data transform to get data.
- const mark = (yield applyTransform(node, library));
- const { type = error('G2Mark type is required.'), key } = mark;
- const { props = {} } = createMark(type);
- const { composite = true } = props;
- if (!composite)
- flattenMarks.push(mark);
- else {
- // Convert composite mark to marks.
- const marks = yield useMark(mark)(options);
- 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 });
- });
- }
- function initializeMarks(options, library) {
- return __awaiter(this, void 0, void 0, function* () {
- 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, library);
- 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), {});
- // 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 = inferScale(name, values, options, coordinates, theme, library);
- channels.forEach((channel) => (channel.scale = scale));
- }
- return markState;
- });
- }
- 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 = Array.from(new Set(states.flatMap((d) => d.channels.map((d) => d.scale))));
- const components = 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(components, coordinate, layout);
- // 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 scaleInstance = {};
- 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);
- });
- Object.assign(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,
- components,
- markState,
- key,
- clip,
- scale: scaleInstance,
- style: framedStyle,
- labelTransform: composeLabelTransform(labelTransform.map(useLabelTransform)),
- };
- return [view, children];
- }
- function plotView(view, selection, transitions, library, context) {
- return __awaiter(this, void 0, void 0, function* () {
- const { components, theme, layout, markState, coordinate, key, style, clip } = 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, 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) {
- applyStyle(select(this), 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.
- const componentsTransitions = selection
- .selectAll(className(COMPONENT_CLASS_NAME))
- .data(components, (d, i) => `${d.type}-${i}`)
- .join((enter) => enter
- .append('g')
- .style('zIndex', ({ zIndex }) => zIndex || -1)
- .attr('className', COMPONENT_CLASS_NAME)
- .append((options) => renderComponent(deepMix({ animate: componentAnimateOptions }, options), coordinate, theme, library, markState)), (update) => update.transition(function (options) {
- const { preserve = false } = options;
- if (preserve)
- return;
- const newComponent = renderComponent(deepMix({ animate: componentAnimateOptions }, options), coordinate, theme, library, markState);
- const { attributes } = newComponent;
- const [node] = this.childNodes;
- return node.update(attributes);
- }))
- .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, library, 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) {
- return enterFunction(data, [this]);
- }), (update) => update.call((selection) => {
- const parent = selection.parent();
- const origin = useMemo((node) => {
- const [x, y] = node.getBounds().min;
- return [x, y];
- });
- update.transition(function (data, index) {
- maybeFacetElement(this, parent, origin);
- const node = shapeFunction(data, index);
- const animation = updateFunction(data, [this], [node]);
- if (animation === null) {
- if (this.nodeName === node.nodeName)
- copyAttributes(this, node);
- else
- this.parentNode.replaceChild(node, this);
- }
- return animation;
- });
- }), (exit) => {
- return exit
- .each(function () {
- this.__removed__ = true;
- })
- .transition(function (data) {
- return exitFunction(data, [this]);
- })
- .remove();
- }, (merge) => merge
- // Append elements to be merged.
- .append(shapeFunction)
- .attr('className', ELEMENT_CLASS_NAME)
- .attr('markType', type)
- .transition(function (data) {
- // Remove merged elements after animation finishing.
- const { __fromElements__: fromElements } = this;
- const transition = updateFunction(data, fromElements, [this]);
- const exit = new Selection(fromElements, null, this.parentNode);
- exit.transition(transition).remove();
- return transition;
- }), (split) => split
- .transition(function (data) {
- // Append splitted shapes.
- const enter = new Selection([], this.__toData__, this.parentNode);
- const toElements = enter
- .append(shapeFunction)
- .attr('className', ELEMENT_CLASS_NAME)
- .attr('markType', type)
- .nodes();
- return updateFunction(data, [this], 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);
- });
- }
- /**
- * Auto hide labels be specify label layout.
- */
- function plotLabel(view, selection, transitions, library) {
- 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);
- 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) {
- // @todo Handle Label with different type.
- const shapeFunction = labelShapeFunction.get(d);
- const node = shapeFunction(d);
- copyAttributes(this, node);
- }), (exit) => exit.remove())
- .nodes();
- // Apply group-level transforms.
- const labelGroups = group(labelShapes, (d) => labelDescriptor.get(d.__data__));
- const { coordinate } = view;
- for (const [label, shapes] of labelGroups) {
- const { transform = [] } = label;
- const transformFunction = composeLabelTransform(transform.map(useLabelTransform));
- transformFunction(shapes, coordinate);
- }
- // Apply view-level transform.
- if (labelTransform) {
- labelTransform(labelShapes, coordinate);
- }
- }
- function composeLabelTransform(transform) {
- return (labels, coordinate) => {
- for (const t of transform) {
- labels = t(labels, coordinate);
- }
- return labels;
- };
- }
- 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.filter((_, i) => points[i].every(defined)).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) {
- const [useShape] = useLibrary('shape', library);
- const { data: abstractData } = mark;
- const { data: visualData, defaultLabelShape } = state;
- const point2d = visualData.map((d) => d.points);
- const { theme, coordinate } = view;
- return (options) => {
- const { index, points } = options;
- const datum = abstractData[index];
- const { formatter = (d) => `${d}`, transform, style: abstractStyle } = options, abstractOptions = __rest(options, ["formatter", "transform", "style"]);
- const visualOptions = mapObject(Object.assign(Object.assign({}, abstractOptions), abstractStyle), (d) => valueOf(d, datum, index, abstractData));
- 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) });
- const shapeFunction = useShape(Object.assign({ type: `label.${shape}` }, style));
- return shapeFunction(points, value, coordinate, theme, point2d);
- };
- }
- function valueOf(value, datum, i, data) {
- if (typeof value === 'function')
- return value(datum, i, data);
- if (typeof value !== 'string')
- return value;
- if (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, library, context) {
- const [useShape] = useLibrary('shape', library);
- const { data: abstractData } = mark;
- const { defaultShape, data } = state;
- const point2d = data.map((d) => d.points);
- const { theme, coordinate } = view;
- const { type: markType, style = {} } = mark;
- 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), { shape, mark: markType, defaultShape, 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));
- const shapeFunction = useShape(Object.assign(Object.assign({}, visualStyle), { type: shapeName(mark, shape) }));
- return shapeFunction(points, value, coordinate, theme, point2d, context);
- };
- }
- function createAnimationFunction(type, mark, state, view, library) {
- var _a;
- const [, createShape] = useLibrary('shape', library);
- const [useAnimation] = useLibrary('animation', library);
- const { defaultShape } = state;
- const { theme, coordinate } = view;
- const upperType = upperFirst(type);
- const key = `default${upperType}Animation`;
- const { [key]: defaultAnimation } = createShape(shapeName(mark, defaultShape)).props;
- const { [type]: defaultEffectTiming = {} } = theme;
- const animate = ((_a = mark.animate) === null || _a === void 0 ? void 0 : _a[type]) || {};
- 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);
- const value = { delay, duration, easing };
- const A = animateFunction(from, to, value, coordinate, defaultEffectTiming);
- 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, it should be canceled
- * manually. This is very important for performance.
- */
- function cancel(animation) {
- animation.finished.then(() => {
- animation.cancel();
- });
- return animation;
- }
- function createUpdateFunction(mark, state, view, library) {
- return createAnimationFunction('update', mark, state, view, library);
- }
- function createExitFunction(mark, state, view, library) {
- return createAnimationFunction('exit', mark, state, view, library);
- }
- function inferTheme(theme = {}) {
- if (typeof theme === 'string')
- return { type: theme };
- const { type = 'classic' } = theme, rest = __rest(theme, ["type"]);
- return Object.assign(Object.assign({}, rest), { type });
- }
- /**
- * @todo Infer builtin tooltips.
- */
- function inferInteraction(view) {
- const defaults = {
- event: true,
- tooltip: true,
- // @todo Inferred by slider self.
- sliderFilter: true,
- legendFilter: true,
- scrollbarFilter: true,
- };
- const { interaction = {} } = view;
- return Object.entries(deepMix(defaults, interaction));
- }
- function applyTransform(node, library) {
- return __awaiter(this, void 0, void 0, function* () {
- const context = { library };
- const { data } = node, rest = __rest(node, ["data"]);
- if (data == undefined)
- return node;
- const [, { data: newData }] = yield applyDataTransform([], { data }, context);
- return Object.assign({ data: newData }, rest);
- });
- }
- function updateBBox(selection) {
- selection
- .style('x', (d) => d.paddingLeft)
- .style('y', (d) => d.paddingTop)
- .style('width', (d) => d.innerWidth)
- .style('height', (d) => d.innerHeight);
- }
- function animateBBox(selection, extent) {
- const [delay, duration] = extent;
- selection.transition(function (data) {
- const { x, y, width, height } = this.style;
- const { paddingLeft, paddingTop, innerWidth, innerHeight } = data;
- const keyframes = [
- {
- x,
- y,
- width,
- height,
- },
- {
- x: paddingLeft,
- y: paddingTop,
- width: innerWidth,
- height: innerHeight,
- },
- ];
- return this.animate(keyframes, { delay, duration, fill: 'both' });
- });
- }
- function shapeName(mark, name) {
- const { type } = mark;
- if (typeof name === 'string')
- return `${type}.${name}`;
- return name;
- }
- /**
- * Create and update layer for each mark.
- * All the layers created here are treated as main layers.
- */
- function updateLayers(selection, marks) {
- const facet = (d) => (d.class !== undefined ? `${d.class}` : '');
- // Skip for empty selection, it can't append nodes.
- const nodes = selection.nodes();
- if (nodes.length === 0)
- return;
- selection
- .selectAll(className(MAIN_LAYER_CLASS_NAME))
- .data(marks, (d) => d.key)
- .join((enter) => enter
- .append('g')
- .attr('className', MAIN_LAYER_CLASS_NAME)
- .attr('id', (d) => d.key)
- .style('facet', facet)
- .style('fill', 'transparent')
- .style('zIndex', (d) => { var _a; return (_a = d.zIndex) !== null && _a !== void 0 ? _a : 0; }), (update) => update
- .style('facet', facet)
- .style('fill', 'transparent')
- .style('zIndex', (d) => { var _a; return (_a = d.zIndex) !== null && _a !== void 0 ? _a : 0; }), (exit) => exit.remove());
- const labelLayer = selection.select(className(LABEL_LAYER_CLASS_NAME)).node();
- if (labelLayer)
- return;
- selection
- .append('g')
- .attr('className', LABEL_LAYER_CLASS_NAME)
- .style('zIndex', 0);
- }
- function className(...names) {
- return names.map((d) => `.${d}`).join('');
- }
- function applyClip(selection, clip) {
- if (!selection.node())
- return;
- selection.style('clipPath', (data) => {
- if (!clip)
- return null;
- const { paddingTop: y, paddingLeft: x, innerWidth: width, innerHeight: height, } = data;
- return new Rect({ style: { x, y, width, height } });
- });
- }
- function inferComponentScales(scales, states, markState) {
- // add shape scale to state.
- var _a;
- // for cell, omit shape scale.
- // @todo support shape scale for cell.
- for (const [key] of markState.entries()) {
- if (key.type === 'cell') {
- return scales.filter((scale) => scale.name !== 'shape');
- }
- }
- // can't infer shape scale if there are multiple states.
- if (states.length !== 1 || scales.some((scale) => scale.name === 'shape')) {
- return scales;
- }
- const { defaultShape: shape } = states[0];
- const acceptMarkTypes = ['point', 'line', 'rect', 'hollow'];
- if (!acceptMarkTypes.includes(shape))
- return scales;
- const shapeMap = {
- point: 'point',
- line: 'hyphen',
- rect: 'square',
- hollow: 'hollow',
- };
- // create shape scale
- const field = ((_a = scales.find((scale) => scale.name === 'color')) === null || _a === void 0 ? void 0 : _a.field) || null;
- const shapeScale = {
- field,
- name: 'shape',
- type: 'constant',
- domain: [],
- range: [shapeMap[shape]],
- };
- return [...scales, shapeScale];
- }
- export function applyStyle(selection, style) {
- for (const [key, value] of Object.entries(style)) {
- selection.style(key, value);
- }
- }
- //# sourceMappingURL=plot.js.map
|