@antv/g2
Version:
the Grammar of Graphics in Javascript
1,016 lines • 56.8 kB
JavaScript
"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