plot.js 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043
  1. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  2. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  3. return new (P || (P = Promise))(function (resolve, reject) {
  4. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  5. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  6. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  7. step((generator = generator.apply(thisArg, _arguments || [])).next());
  8. });
  9. };
  10. var __rest = (this && this.__rest) || function (s, e) {
  11. var t = {};
  12. for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
  13. t[p] = s[p];
  14. if (s != null && typeof Object.getOwnPropertySymbols === "function")
  15. for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
  16. if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
  17. t[p[i]] = s[p[i]];
  18. }
  19. return t;
  20. };
  21. import { Rect } from '@antv/g';
  22. import { deepMix, upperFirst } from '@antv/util';
  23. import { group } from 'd3-array';
  24. import { format } from 'd3-format';
  25. import { mapObject } from '../utils/array';
  26. import { ChartEvent } from '../utils/event';
  27. import { appendTransform, copyAttributes, defined, error, maybeSubObject, subObject, useMemo, } from '../utils/helper';
  28. import { select, Selection } from '../utils/selection';
  29. import { inferComponent, renderComponent } from './component';
  30. 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';
  31. import { coordinate2Transform, createCoordinate } from './coordinate';
  32. import { computeLayout, placeComponents } from './layout';
  33. import { useLibrary } from './library';
  34. import { initializeMark } from './mark';
  35. import { applyScale, inferScale, syncFacetsScales, useRelationScale, } from './scale';
  36. import { applyDataTransform } from './transform';
  37. export function plot(options, selection, library, context) {
  38. var _a, _b;
  39. return __awaiter(this, void 0, void 0, function* () {
  40. const [useComposition] = useLibrary('composition', library);
  41. const [useInteraction] = useLibrary('interaction', library);
  42. // Some helper functions.
  43. const marks = new Set(Object.keys(library)
  44. .map((d) => { var _a; return (_a = /mark\.(.*)/.exec(d)) === null || _a === void 0 ? void 0 : _a[1]; })
  45. .filter(defined));
  46. const typeOf = (node) => {
  47. const { type } = node;
  48. if (typeof type === 'function') {
  49. // @ts-ignore
  50. const { props = {} } = type;
  51. const { composite = true } = props;
  52. if (composite)
  53. return 'mark';
  54. }
  55. return typeof type === 'string' && marks.has(type) ? 'mark' : type;
  56. };
  57. const isMark = (node) => typeOf(node) === 'mark';
  58. const isStandardView = (node) => typeOf(node) === 'standardView';
  59. const transform = (node) => {
  60. if (isStandardView(node))
  61. return [node];
  62. const type = typeOf(node);
  63. const composition = useComposition({ type });
  64. return composition(node);
  65. };
  66. // Some temporary variables help parse the view tree.
  67. const views = [];
  68. const viewNode = new Map();
  69. const nodeState = new Map();
  70. const discovered = [options];
  71. const nodeGenerators = [];
  72. while (discovered.length) {
  73. const node = discovered.shift();
  74. if (isStandardView(node)) {
  75. // Initialize view to get data to be visualized. If the marks
  76. // of the view have already been initialized (facet view),
  77. // initialize the view based on the initialized mark states,
  78. // otherwise initialize it from beginning.
  79. const state = nodeState.get(node);
  80. const [view, children] = state
  81. ? initializeState(state, node, library)
  82. : yield initializeView(node, library);
  83. viewNode.set(view, node);
  84. views.push(view);
  85. // Transform children, they will be transformed into
  86. // standardView if they are mark or view node.
  87. const transformedNodes = children
  88. .flatMap(transform)
  89. .map((d) => coordinate2Transform(d, library));
  90. discovered.push(...transformedNodes);
  91. // Only StandardView can be treated as facet and it
  92. // should sync position scales among facets normally.
  93. if (transformedNodes.every(isStandardView)) {
  94. const states = yield Promise.all(transformedNodes.map((d) => initializeMarks(d, library)));
  95. // Note!!!
  96. // This will mutate scales for marks.
  97. syncFacetsScales(states);
  98. for (let i = 0; i < transformedNodes.length; i++) {
  99. const nodeT = transformedNodes[i];
  100. const state = states[i];
  101. nodeState.set(nodeT, state);
  102. }
  103. }
  104. }
  105. else {
  106. // Apply transform to get data in advance for non-mark composition
  107. // node, which makes sure that composition node can preprocess the
  108. // data to produce more nodes based on it.
  109. const n = isMark(node) ? node : yield applyTransform(node, library);
  110. const N = transform(n);
  111. if (Array.isArray(N))
  112. discovered.push(...N);
  113. else if (typeof N === 'function')
  114. nodeGenerators.push(N());
  115. }
  116. }
  117. context.emitter.emit(ChartEvent.BEFORE_PAINT);
  118. // Plot chart.
  119. const enterContainer = new Map();
  120. const updateContainer = new Map();
  121. const transitions = [];
  122. selection
  123. .selectAll(className(VIEW_CLASS_NAME))
  124. .data(views, (d) => d.key)
  125. .join((enter) => enter
  126. .append('g')
  127. .attr('className', VIEW_CLASS_NAME)
  128. .attr('id', (view) => view.key)
  129. .call(applyTranslate)
  130. .each(function (view) {
  131. plotView(view, select(this), transitions, library, context);
  132. enterContainer.set(view, this);
  133. }), (update) => update.call(applyTranslate).each(function (view) {
  134. plotView(view, select(this), transitions, library, context);
  135. updateContainer.set(view, this);
  136. }), (exit) => exit
  137. .each(function () {
  138. // Remove existed interactions.
  139. const interactions = this['nameInteraction'].values();
  140. for (const interaction of interactions) {
  141. interaction.destroy();
  142. }
  143. })
  144. .remove());
  145. // Apply interactions.
  146. const viewInstanceof = (viewContainer) => {
  147. return Array.from(viewContainer.entries()).map(([view, container]) => ({
  148. view,
  149. container,
  150. options: viewNode.get(view),
  151. update: createUpdateView(select(container), library, context),
  152. }));
  153. };
  154. // Interactions for enter views.
  155. const enterViewInstances = viewInstanceof(enterContainer);
  156. for (const target of enterViewInstances) {
  157. const { options } = target;
  158. // A Map index interaction by interaction name.
  159. const nameInteraction = new Map();
  160. target.container['nameInteraction'] = nameInteraction;
  161. // Apply interactions.
  162. for (const typeOption of inferInteraction(options)) {
  163. const [type, option] = typeOption;
  164. if (option) {
  165. const interaction = useInteraction(Object.assign({ type }, option));
  166. const destroy = interaction(target, enterViewInstances, context.emitter);
  167. nameInteraction.set(type, { destroy });
  168. }
  169. }
  170. }
  171. // Interactions for update views.
  172. const updateViewInstances = viewInstanceof(updateContainer);
  173. for (const target of updateViewInstances) {
  174. const { options, container } = target;
  175. const nameInteraction = container['nameInteraction'];
  176. for (const typeOption of inferInteraction(options)) {
  177. const [type, option] = typeOption;
  178. // Remove interaction for existed views.
  179. const prevInteraction = nameInteraction.get(type);
  180. if (prevInteraction)
  181. (_a = prevInteraction.destroy) === null || _a === void 0 ? void 0 : _a.call(prevInteraction);
  182. // Apply new interaction.
  183. if (option) {
  184. const interaction = useInteraction(Object.assign({ type }, option));
  185. const destroy = interaction(target, updateViewInstances, context.emitter);
  186. nameInteraction.set(type, { destroy });
  187. }
  188. }
  189. }
  190. // Author animations.
  191. const { width, height } = options;
  192. const keyframes = [];
  193. for (const nodeGenerator of nodeGenerators) {
  194. // Delay the rendering of animation keyframe. Different animation
  195. // created by different nodeGenerator will play in the same time.
  196. // eslint-disable-next-line no-async-promise-executor
  197. const keyframe = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
  198. for (const node of nodeGenerator) {
  199. const sizedNode = Object.assign({ width, height }, node);
  200. yield plot(sizedNode, selection, library, context);
  201. }
  202. resolve();
  203. }));
  204. keyframes.push(keyframe);
  205. }
  206. context.views = views;
  207. // Clear and update animation.
  208. (_b = context.animations) === null || _b === void 0 ? void 0 : _b.forEach((animation) => animation === null || animation === void 0 ? void 0 : animation.cancel());
  209. context.animations = transitions;
  210. context.emitter.emit(ChartEvent.AFTER_PAINT);
  211. // Note!!!
  212. // The returned promise will never resolved if one of nodeGenerator
  213. // never stop to yield node, which may created by a keyframe composition
  214. // with iteration count set to infinite.
  215. const finished = transitions
  216. .filter(defined)
  217. .map(cancel)
  218. .map((d) => d.finished);
  219. return Promise.all([...finished, ...keyframes]);
  220. });
  221. }
  222. function applyTranslate(selection) {
  223. selection.style('transform', (d) => `translate(${d.layout.x}, ${d.layout.y})`);
  224. }
  225. function createUpdateView(selection, library, context) {
  226. return (newOptions) => __awaiter(this, void 0, void 0, function* () {
  227. const transitions = [];
  228. const [newView, newChildren] = yield initializeView(newOptions, library);
  229. plotView(newView, selection, transitions, library, context);
  230. updateTooltip(selection, newOptions, newView, library, context);
  231. for (const child of newChildren) {
  232. plot(child, selection, library, context);
  233. }
  234. return { options: newOptions, view: newView };
  235. });
  236. }
  237. function updateTooltip(selection, options, view, library, context) {
  238. var _a;
  239. const [useInteraction] = useLibrary('interaction', library);
  240. // Instances for tooltip.
  241. const container = selection.node();
  242. const nameInteraction = container['nameInteraction'];
  243. const tooltipOptions = inferInteraction(options).find(([d]) => d === 'tooltip');
  244. // Destroy older tooltip.
  245. const tooltip = nameInteraction.get('tooltip');
  246. if (!tooltip)
  247. return;
  248. (_a = tooltip.destroy) === null || _a === void 0 ? void 0 : _a.call(tooltip);
  249. if (!tooltipOptions[1])
  250. return;
  251. // Apply new tooltip interaction.
  252. const applyTooltip = useInteraction(Object.assign({ type: 'tooltip' }, tooltipOptions[1]));
  253. const target = {
  254. options,
  255. view,
  256. container: selection.node(),
  257. update: (options) => Promise.resolve(options),
  258. };
  259. applyTooltip(target, [], context.emitter);
  260. }
  261. function initializeView(options, library) {
  262. return __awaiter(this, void 0, void 0, function* () {
  263. const flattenOptions = yield transformMarks(options, library);
  264. const mergedOptions = bubbleOptions(flattenOptions);
  265. // @todo Remove this.
  266. // !!! NOTE: Mute original view options.
  267. // Update interaction and coordinate for this view.
  268. options.interaction = mergedOptions.interaction;
  269. options.coordinate = mergedOptions.coordinate;
  270. const transformedOptions = coordinate2Transform(mergedOptions, library);
  271. const state = yield initializeMarks(transformedOptions, library);
  272. return initializeState(state, transformedOptions, library);
  273. });
  274. }
  275. function bubbleOptions(options) {
  276. const { coordinate: viewCoordinate = {}, interaction: viewInteraction = {}, style: viewStyle = {}, marks } = options, rest = __rest(options, ["coordinate", "interaction", "style", "marks"]);
  277. const markCoordinates = marks.map((d) => d.coordinate || {});
  278. const markInteractions = marks.map((d) => d.interaction || {});
  279. const markViewStyles = marks.map((d) => d.viewStyle || {});
  280. const newCoordinate = [...markCoordinates, viewCoordinate].reduceRight((prev, cur) => deepMix(prev, cur), {});
  281. const newInteraction = [viewInteraction, ...markInteractions].reduce((prev, cur) => deepMix(prev, cur), {});
  282. const newStyle = [...markViewStyles, viewStyle].reduce((prev, cur) => deepMix(prev, cur), {});
  283. return Object.assign(Object.assign({}, rest), { marks, coordinate: newCoordinate, interaction: newInteraction, style: newStyle });
  284. }
  285. function transformMarks(options, library) {
  286. return __awaiter(this, void 0, void 0, function* () {
  287. const [useMark, createMark] = useLibrary('mark', library);
  288. const { marks } = options;
  289. const flattenMarks = [];
  290. const discovered = [...marks];
  291. // Pre order traversal.
  292. while (discovered.length) {
  293. const [node] = discovered.splice(0, 1);
  294. // Apply data transform to get data.
  295. const mark = (yield applyTransform(node, library));
  296. const { type = error('G2Mark type is required.'), key } = mark;
  297. const { props = {} } = createMark(type);
  298. const { composite = true } = props;
  299. if (!composite)
  300. flattenMarks.push(mark);
  301. else {
  302. // Convert composite mark to marks.
  303. const marks = yield useMark(mark)(options);
  304. const M = Array.isArray(marks) ? marks : [marks];
  305. discovered.unshift(...M.map((d, i) => (Object.assign(Object.assign({}, d), { key: `${key}-${i}` }))));
  306. }
  307. }
  308. return Object.assign(Object.assign({}, options), { marks: flattenMarks });
  309. });
  310. }
  311. function initializeMarks(options, library) {
  312. return __awaiter(this, void 0, void 0, function* () {
  313. const [useTheme] = useLibrary('theme', library);
  314. const [, createMark] = useLibrary('mark', library);
  315. const { theme: partialTheme, marks: partialMarks, coordinates = [], } = options;
  316. const theme = useTheme(inferTheme(partialTheme));
  317. const markState = new Map();
  318. // Initialize channels for marks.
  319. for (const markOptions of partialMarks) {
  320. const { type } = markOptions;
  321. const { props = {} } = createMark(type);
  322. const markAndState = yield initializeMark(markOptions, props, library);
  323. if (markAndState) {
  324. const [initializedMark, state] = markAndState;
  325. markState.set(initializedMark, state);
  326. }
  327. }
  328. // Group channels by scale key, each group has scale.
  329. const scaleChannels = group(Array.from(markState.values()).flatMap((d) => d.channels), ({ scaleKey }) => scaleKey);
  330. // Infer scale for each channel groups.
  331. for (const channels of scaleChannels.values()) {
  332. // Merge scale options for these channels.
  333. const scaleOptions = channels.reduce((total, { scale }) => deepMix(total, scale), {});
  334. // Use the fields of the first channel as the title.
  335. const { values: FV } = channels[0];
  336. const fields = Array.from(new Set(FV.map((d) => d.field).filter(defined)));
  337. const options = deepMix({
  338. guide: { title: fields.length === 0 ? undefined : fields },
  339. field: fields[0],
  340. }, scaleOptions);
  341. // Use the name of the first channel as the scale name.
  342. const { name } = channels[0];
  343. const values = channels.flatMap(({ values }) => values.map((d) => d.value));
  344. const scale = inferScale(name, values, options, coordinates, theme, library);
  345. channels.forEach((channel) => (channel.scale = scale));
  346. }
  347. return markState;
  348. });
  349. }
  350. function initializeState(markState, options, library) {
  351. const [useMark] = useLibrary('mark', library);
  352. const [useTheme] = useLibrary('theme', library);
  353. const [useLabelTransform] = useLibrary('labelTransform', library);
  354. const { key, frame = false, theme: partialTheme, clip, style = {}, labelTransform = [], } = options;
  355. const theme = useTheme(inferTheme(partialTheme));
  356. // Infer components and compute layout.
  357. const states = Array.from(markState.values());
  358. const scales = Array.from(new Set(states.flatMap((d) => d.channels.map((d) => d.scale))));
  359. const components = inferComponent(inferComponentScales(Array.from(scales), states, markState), options, library);
  360. const layout = computeLayout(components, options, theme, library);
  361. const coordinate = createCoordinate(layout, options, library);
  362. const framedStyle = frame
  363. ? deepMix({ mainLineWidth: 1, mainStroke: '#000' }, style)
  364. : style;
  365. // Place components and mutate their bbox.
  366. placeComponents(components, coordinate, layout);
  367. // Calc data to be rendered for each mark.
  368. // @todo More readable APIs for Container which stays
  369. // the same style with JS standard and lodash APIs.
  370. // @todo More proper way to index scale for different marks.
  371. const scaleInstance = {};
  372. const children = [];
  373. for (const [mark, state] of markState.entries()) {
  374. const {
  375. // scale,
  376. // Callback to create children options based on this mark.
  377. children: createChildren,
  378. // The total count of data (both show and hide)for this facet.
  379. // This is for unit visualization to sync data domain.
  380. dataDomain, modifier, key: markKey, } = mark;
  381. const { index, channels, tooltip } = state;
  382. const scale = Object.fromEntries(channels.map(({ name, scale }) => [name, scale]));
  383. // Transform abstract value to visual value by scales.
  384. const markScaleInstance = mapObject(scale, (options) => {
  385. return useRelationScale(options, library);
  386. });
  387. Object.assign(scaleInstance, markScaleInstance);
  388. const value = applyScale(channels, markScaleInstance);
  389. // Calc points and transformation for each data,
  390. // and then transform visual value to visual data.
  391. const calcPoints = useMark(mark);
  392. const [I, P, S] = filterValid(calcPoints(index, markScaleInstance, value, coordinate));
  393. const count = dataDomain || I.length;
  394. const T = modifier ? modifier(P, count, layout) : [];
  395. 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; };
  396. const itemsOf = (i) => tooltip.items.map((V) => V[i]);
  397. const visualData = I.map((d, i) => {
  398. const datum = Object.assign({ points: P[i], transform: T[i], index: d, markKey, viewKey: key }, (tooltip && {
  399. title: titleOf(d),
  400. items: itemsOf(d),
  401. }));
  402. for (const [k, V] of Object.entries(value)) {
  403. datum[k] = V[d];
  404. if (S)
  405. datum[`series${upperFirst(k)}`] = S[i].map((i) => V[i]);
  406. }
  407. if (S)
  408. datum['seriesIndex'] = S[i];
  409. if (S && tooltip) {
  410. datum['seriesItems'] = S[i].map((si) => itemsOf(si));
  411. datum['seriesTitle'] = S[i].map((si) => titleOf(si));
  412. }
  413. return datum;
  414. });
  415. state.data = visualData;
  416. state.index = I;
  417. // Create children options by children callback,
  418. // and then propagate data to each child.
  419. const markChildren = createChildren === null || createChildren === void 0 ? void 0 : createChildren(visualData, markScaleInstance, layout);
  420. children.push(...(markChildren || []));
  421. }
  422. const view = {
  423. layout,
  424. theme,
  425. coordinate,
  426. components,
  427. markState,
  428. key,
  429. clip,
  430. scale: scaleInstance,
  431. style: framedStyle,
  432. labelTransform: composeLabelTransform(labelTransform.map(useLabelTransform)),
  433. };
  434. return [view, children];
  435. }
  436. function plotView(view, selection, transitions, library, context) {
  437. return __awaiter(this, void 0, void 0, function* () {
  438. const { components, theme, layout, markState, coordinate, key, style, clip } = view;
  439. // Render background for the different areas.
  440. const { x, y, width, height } = layout, rest = __rest(layout, ["x", "y", "width", "height"]);
  441. const areaKeys = ['view', 'plot', 'main', 'content'];
  442. const I = areaKeys.map((_, i) => i);
  443. const sizeKeys = ['a', 'margin', 'padding', 'inset'];
  444. const areaStyles = areaKeys.map((d) => maybeSubObject(Object.assign({}, theme, style), d));
  445. const areaSizes = sizeKeys.map((d) => subObject(rest, d));
  446. const styleArea = (selection) => selection
  447. .style('x', (i) => areaLayouts[i].x)
  448. .style('y', (i) => areaLayouts[i].y)
  449. .style('width', (i) => areaLayouts[i].width)
  450. .style('height', (i) => areaLayouts[i].height)
  451. .each(function (i) {
  452. applyStyle(select(this), areaStyles[i]);
  453. });
  454. let px = 0;
  455. let py = 0;
  456. let pw = width;
  457. let ph = height;
  458. const areaLayouts = I.map((i) => {
  459. const size = areaSizes[i];
  460. const { left = 0, top = 0, bottom = 0, right = 0 } = size;
  461. px += left;
  462. py += top;
  463. pw -= left + right;
  464. ph -= top + bottom;
  465. return {
  466. x: px,
  467. y: py,
  468. width: pw,
  469. height: ph,
  470. };
  471. });
  472. selection
  473. .selectAll(className(AREA_CLASS_NAME))
  474. .data(
  475. // Only render area with defined style.
  476. I.filter((i) => defined(areaStyles[i])), (i) => areaKeys[i])
  477. .join((enter) => enter
  478. .append('rect')
  479. .attr('className', AREA_CLASS_NAME)
  480. .style('zIndex', -2)
  481. .call(styleArea), (update) => update.call(styleArea), (exit) => exit.remove());
  482. const animationExtent = computeAnimationExtent(markState);
  483. const componentAnimateOptions = animationExtent
  484. ? { duration: animationExtent[1] }
  485. : false;
  486. // Render components.
  487. // @todo renderComponent return ctor and options.
  488. const componentsTransitions = selection
  489. .selectAll(className(COMPONENT_CLASS_NAME))
  490. .data(components, (d, i) => `${d.type}-${i}`)
  491. .join((enter) => enter
  492. .append('g')
  493. .style('zIndex', ({ zIndex }) => zIndex || -1)
  494. .attr('className', COMPONENT_CLASS_NAME)
  495. .append((options) => renderComponent(deepMix({ animate: componentAnimateOptions }, options), coordinate, theme, library, markState)), (update) => update.transition(function (options) {
  496. const { preserve = false } = options;
  497. if (preserve)
  498. return;
  499. const newComponent = renderComponent(deepMix({ animate: componentAnimateOptions }, options), coordinate, theme, library, markState);
  500. const { attributes } = newComponent;
  501. const [node] = this.childNodes;
  502. return node.update(attributes);
  503. }))
  504. .transitions();
  505. transitions.push(...componentsTransitions.flat().filter(defined));
  506. // Main layer is for showing the main visual representation such as marks. There
  507. // may be multiple main layers for a view, each main layer correspond to one of marks.
  508. // @todo Test DOM structure.
  509. const T = selection
  510. .selectAll(className(PLOT_CLASS_NAME))
  511. .data([layout], () => key)
  512. .join((enter) => enter
  513. // Make this layer interactive, such as click and mousemove events.
  514. .append('rect')
  515. .style('zIndex', 0)
  516. .style('fill', 'transparent')
  517. .attr('className', PLOT_CLASS_NAME)
  518. .call(updateBBox)
  519. .call(updateLayers, Array.from(markState.keys()))
  520. .call(applyClip, clip), (update) => update
  521. .call(updateLayers, Array.from(markState.keys()))
  522. .call((selection) => {
  523. return animationExtent
  524. ? animateBBox(selection, animationExtent)
  525. : updateBBox(selection);
  526. })
  527. .call(applyClip, clip))
  528. .transitions();
  529. transitions.push(...T.flat());
  530. // Render marks with corresponding data.
  531. for (const [mark, state] of markState.entries()) {
  532. const { data } = state;
  533. const { key, class: cls, type } = mark;
  534. const viewNode = selection.select(`#${key}`);
  535. const shapeFunction = createMarkShapeFunction(mark, state, view, library, context);
  536. const enterFunction = createEnterFunction(mark, state, view, library);
  537. const updateFunction = createUpdateFunction(mark, state, view, library);
  538. const exitFunction = createExitFunction(mark, state, view, library);
  539. const facetElements = selectFacetElements(selection, viewNode, cls, 'element');
  540. const T = viewNode
  541. .selectAll(className(ELEMENT_CLASS_NAME))
  542. .selectFacetAll(facetElements)
  543. .data(data, (d) => d.key, (d) => d.groupKey)
  544. .join((enter) => enter
  545. .append(shapeFunction)
  546. // Note!!! Only one className can be set.
  547. // Using attribute as alternative for other classNames.
  548. .attr('className', ELEMENT_CLASS_NAME)
  549. .attr('markType', type)
  550. .transition(function (data) {
  551. return enterFunction(data, [this]);
  552. }), (update) => update.call((selection) => {
  553. const parent = selection.parent();
  554. const origin = useMemo((node) => {
  555. const [x, y] = node.getBounds().min;
  556. return [x, y];
  557. });
  558. update.transition(function (data, index) {
  559. maybeFacetElement(this, parent, origin);
  560. const node = shapeFunction(data, index);
  561. const animation = updateFunction(data, [this], [node]);
  562. if (animation === null) {
  563. if (this.nodeName === node.nodeName)
  564. copyAttributes(this, node);
  565. else
  566. this.parentNode.replaceChild(node, this);
  567. }
  568. return animation;
  569. });
  570. }), (exit) => {
  571. return exit
  572. .each(function () {
  573. this.__removed__ = true;
  574. })
  575. .transition(function (data) {
  576. return exitFunction(data, [this]);
  577. })
  578. .remove();
  579. }, (merge) => merge
  580. // Append elements to be merged.
  581. .append(shapeFunction)
  582. .attr('className', ELEMENT_CLASS_NAME)
  583. .attr('markType', type)
  584. .transition(function (data) {
  585. // Remove merged elements after animation finishing.
  586. const { __fromElements__: fromElements } = this;
  587. const transition = updateFunction(data, fromElements, [this]);
  588. const exit = new Selection(fromElements, null, this.parentNode);
  589. exit.transition(transition).remove();
  590. return transition;
  591. }), (split) => split
  592. .transition(function (data) {
  593. // Append splitted shapes.
  594. const enter = new Selection([], this.__toData__, this.parentNode);
  595. const toElements = enter
  596. .append(shapeFunction)
  597. .attr('className', ELEMENT_CLASS_NAME)
  598. .attr('markType', type)
  599. .nodes();
  600. return updateFunction(data, [this], toElements);
  601. })
  602. // Remove elements to be splitted after animation finishing.
  603. .remove())
  604. .transitions();
  605. transitions.push(...T.flat());
  606. }
  607. // Plot label for this view.
  608. plotLabel(view, selection, transitions, library);
  609. });
  610. }
  611. /**
  612. * Auto hide labels be specify label layout.
  613. */
  614. function plotLabel(view, selection, transitions, library) {
  615. const [useLabelTransform] = useLibrary('labelTransform', library);
  616. const { markState, labelTransform } = view;
  617. const labelLayer = selection.select(className(LABEL_LAYER_CLASS_NAME)).node();
  618. // A Map index shapeFunction by label.
  619. const labelShapeFunction = new Map();
  620. // A Map index options by label.
  621. const labelDescriptor = new Map();
  622. // Get all labels for this view.
  623. const labels = Array.from(markState.entries()).flatMap(([mark, state]) => {
  624. const { labels: labelOptions = [], key } = mark;
  625. const shapeFunction = createLabelShapeFunction(mark, state, view, library);
  626. const elements = selection
  627. .select(`#${key}`)
  628. .selectAll(className(ELEMENT_CLASS_NAME))
  629. .nodes()
  630. // Only select the valid element.
  631. .filter((n) => !n.__removed__);
  632. return labelOptions.flatMap((labelOption, i) => {
  633. const { transform = [] } = labelOption, options = __rest(labelOption, ["transform"]);
  634. return elements.flatMap((e) => {
  635. const L = getLabels(options, i, e);
  636. L.forEach((l) => {
  637. labelShapeFunction.set(l, shapeFunction);
  638. labelDescriptor.set(l, labelOption);
  639. });
  640. return L;
  641. });
  642. });
  643. });
  644. // Render all labels.
  645. const labelShapes = select(labelLayer)
  646. .selectAll(className(LABEL_CLASS_NAME))
  647. .data(labels, (d) => d.key)
  648. .join((enter) => enter
  649. .append((d) => labelShapeFunction.get(d)(d))
  650. .attr('className', LABEL_CLASS_NAME), (update) => update.each(function (d) {
  651. // @todo Handle Label with different type.
  652. const shapeFunction = labelShapeFunction.get(d);
  653. const node = shapeFunction(d);
  654. copyAttributes(this, node);
  655. }), (exit) => exit.remove())
  656. .nodes();
  657. // Apply group-level transforms.
  658. const labelGroups = group(labelShapes, (d) => labelDescriptor.get(d.__data__));
  659. const { coordinate } = view;
  660. for (const [label, shapes] of labelGroups) {
  661. const { transform = [] } = label;
  662. const transformFunction = composeLabelTransform(transform.map(useLabelTransform));
  663. transformFunction(shapes, coordinate);
  664. }
  665. // Apply view-level transform.
  666. if (labelTransform) {
  667. labelTransform(labelShapes, coordinate);
  668. }
  669. }
  670. function composeLabelTransform(transform) {
  671. return (labels, coordinate) => {
  672. for (const t of transform) {
  673. labels = t(labels, coordinate);
  674. }
  675. return labels;
  676. };
  677. }
  678. function getLabels(label, labelIndex, element) {
  679. const { seriesIndex: SI, seriesKey, points, key, index } = element.__data__;
  680. const bounds = getLocalBounds(element);
  681. if (!SI) {
  682. return [
  683. Object.assign(Object.assign({}, label), { key: `${key}-${labelIndex}`, bounds,
  684. index,
  685. points, dependentElement: element }),
  686. ];
  687. }
  688. const selector = normalizeLabelSelector(label);
  689. 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,
  690. points, dependentElement: element })));
  691. return selector ? selector(F) : F;
  692. }
  693. function filterValid([I, P, S]) {
  694. if (S)
  695. return [I, P, S];
  696. const definedIndex = [];
  697. const definedPoints = [];
  698. for (let i = 0; i < I.length; i++) {
  699. const d = I[i];
  700. const p = P[i];
  701. if (p.every(([x, y]) => defined(x) && defined(y))) {
  702. definedIndex.push(d);
  703. definedPoints.push(p);
  704. }
  705. }
  706. return [definedIndex, definedPoints];
  707. }
  708. function normalizeLabelSelector(label) {
  709. const { selector } = label;
  710. if (!selector)
  711. return null;
  712. if (typeof selector === 'function')
  713. return selector;
  714. if (selector === 'first')
  715. return (I) => [I[0]];
  716. if (selector === 'last')
  717. return (I) => [I[I.length - 1]];
  718. throw new Error(`Unknown selector: ${selector}`);
  719. }
  720. /**
  721. * Avoid getting error bounds caused by element animations.
  722. * @todo Remove this temporary handle method, if runtime supports
  723. * correct process: drawElement, do label layout and then do
  724. * transitions together.
  725. */
  726. function getLocalBounds(element) {
  727. const cloneElement = element.cloneNode();
  728. const animations = element.getAnimations();
  729. cloneElement.style.visibility = 'hidden';
  730. animations.forEach((animation) => {
  731. const keyframes = animation.effect.getKeyframes();
  732. cloneElement.attr(keyframes[keyframes.length - 1]);
  733. });
  734. element.parentNode.appendChild(cloneElement);
  735. const bounds = cloneElement.getLocalBounds();
  736. cloneElement.destroy();
  737. const { min, max } = bounds;
  738. return [min, max];
  739. }
  740. function createLabelShapeFunction(mark, state, view, library) {
  741. const [useShape] = useLibrary('shape', library);
  742. const { data: abstractData } = mark;
  743. const { data: visualData, defaultLabelShape } = state;
  744. const point2d = visualData.map((d) => d.points);
  745. const { theme, coordinate } = view;
  746. return (options) => {
  747. const { index, points } = options;
  748. const datum = abstractData[index];
  749. const { formatter = (d) => `${d}`, transform, style: abstractStyle } = options, abstractOptions = __rest(options, ["formatter", "transform", "style"]);
  750. const visualOptions = mapObject(Object.assign(Object.assign({}, abstractOptions), abstractStyle), (d) => valueOf(d, datum, index, abstractData));
  751. const { shape = defaultLabelShape, text } = visualOptions, style = __rest(visualOptions, ["shape", "text"]);
  752. const f = typeof formatter === 'string' ? format(formatter) : formatter;
  753. const value = Object.assign(Object.assign({}, style), { text: f(text, datum, index, abstractData) });
  754. const shapeFunction = useShape(Object.assign({ type: `label.${shape}` }, style));
  755. return shapeFunction(points, value, coordinate, theme, point2d);
  756. };
  757. }
  758. function valueOf(value, datum, i, data) {
  759. if (typeof value === 'function')
  760. return value(datum, i, data);
  761. if (typeof value !== 'string')
  762. return value;
  763. if (datum[value] !== undefined)
  764. return datum[value];
  765. return value;
  766. }
  767. /**
  768. * Compute max duration for this frame.
  769. */
  770. function computeAnimationExtent(markState) {
  771. let maxDuration = -Infinity;
  772. let minDelay = Infinity;
  773. for (const [mark, state] of markState) {
  774. const { animate = {} } = mark;
  775. const { data } = state;
  776. const { enter = {}, update = {}, exit = {} } = animate;
  777. const { type: defaultUpdateType, duration: defaultUpdateDuration = 300, delay: defaultUpdateDelay = 0, } = update;
  778. const { type: defaultEnterType, duration: defaultEnterDuration = 300, delay: defaultEnterDelay = 0, } = enter;
  779. const { type: defaultExitType, duration: defaultExitDuration = 300, delay: defaultExitDelay = 0, } = exit;
  780. for (const d of data) {
  781. const { updateType = defaultUpdateType, updateDuration = defaultUpdateDuration, updateDelay = defaultUpdateDelay, enterType = defaultEnterType, enterDuration = defaultEnterDuration, enterDelay = defaultEnterDelay, exitDuration = defaultExitDuration, exitDelay = defaultExitDelay, exitType = defaultExitType, } = d;
  782. if (updateType === undefined || updateType) {
  783. maxDuration = Math.max(maxDuration, updateDuration + updateDelay);
  784. minDelay = Math.min(minDelay, updateDelay);
  785. }
  786. if (exitType === undefined || exitType) {
  787. maxDuration = Math.max(maxDuration, exitDuration + exitDelay);
  788. minDelay = Math.min(minDelay, exitDelay);
  789. }
  790. if (enterType === undefined || enterType) {
  791. maxDuration = Math.max(maxDuration, enterDuration + enterDelay);
  792. minDelay = Math.min(minDelay, enterDelay);
  793. }
  794. }
  795. }
  796. if (maxDuration === -Infinity)
  797. return null;
  798. return [minDelay, maxDuration - minDelay];
  799. }
  800. function selectFacetElements(selection, current, facetClassName, elementClassName) {
  801. const group = selection.node().parentElement;
  802. return group
  803. .findAll((node) => node.style.facet !== undefined &&
  804. node.style.facet === facetClassName &&
  805. node !== current.node())
  806. .flatMap((node) => node.getElementsByClassName(elementClassName));
  807. }
  808. /**
  809. * Update the parent of element and apply transform to make it
  810. * stay in original position.
  811. */
  812. function maybeFacetElement(element, parent, originOf) {
  813. if (!element.__facet__)
  814. return;
  815. // element -> g#main -> rect#plot
  816. const prePlot = element.parentNode.parentNode;
  817. // g#main -> rect#plot
  818. const newPlot = parent.parentNode;
  819. const [px, py] = originOf(prePlot);
  820. const [x, y] = originOf(newPlot);
  821. const translate = `translate(${px - x}, ${py - y})`;
  822. appendTransform(element, translate);
  823. parent.append(element);
  824. }
  825. function createMarkShapeFunction(mark, state, view, library, context) {
  826. const [useShape] = useLibrary('shape', library);
  827. const { data: abstractData } = mark;
  828. const { defaultShape, data } = state;
  829. const point2d = data.map((d) => d.points);
  830. const { theme, coordinate } = view;
  831. const { type: markType, style = {} } = mark;
  832. return (data) => {
  833. const { shape: styleShape = defaultShape } = style;
  834. const { shape = styleShape, points, seriesIndex, index: i } = data, v = __rest(data, ["shape", "points", "seriesIndex", "index"]);
  835. const value = Object.assign(Object.assign({}, v), { shape, mark: markType, defaultShape, index: i });
  836. // Get data-driven style.
  837. // If it is a series shape, such as area and line,
  838. // provides the series of abstract data and indices
  839. // for this shape, otherwise the single datum and
  840. // index.
  841. const abstractDatum = seriesIndex
  842. ? seriesIndex.map((i) => abstractData[i])
  843. : abstractData[i];
  844. const I = seriesIndex ? seriesIndex : i;
  845. const visualStyle = mapObject(style, (d) => valueOf(d, abstractDatum, I, abstractData));
  846. const shapeFunction = useShape(Object.assign(Object.assign({}, visualStyle), { type: shapeName(mark, shape) }));
  847. return shapeFunction(points, value, coordinate, theme, point2d, context);
  848. };
  849. }
  850. function createAnimationFunction(type, mark, state, view, library) {
  851. var _a;
  852. const [, createShape] = useLibrary('shape', library);
  853. const [useAnimation] = useLibrary('animation', library);
  854. const { defaultShape } = state;
  855. const { theme, coordinate } = view;
  856. const upperType = upperFirst(type);
  857. const key = `default${upperType}Animation`;
  858. const { [key]: defaultAnimation } = createShape(shapeName(mark, defaultShape)).props;
  859. const { [type]: defaultEffectTiming = {} } = theme;
  860. const animate = ((_a = mark.animate) === null || _a === void 0 ? void 0 : _a[type]) || {};
  861. return (data, from, to) => {
  862. const { [`${type}Type`]: animation, [`${type}Delay`]: delay, [`${type}Duration`]: duration, [`${type}Easing`]: easing, } = data;
  863. const options = Object.assign({ type: animation || defaultAnimation }, animate);
  864. if (!options.type)
  865. return null;
  866. const animateFunction = useAnimation(options);
  867. const value = { delay, duration, easing };
  868. const A = animateFunction(from, to, value, coordinate, defaultEffectTiming);
  869. if (!Array.isArray(A))
  870. return [A];
  871. return A;
  872. };
  873. }
  874. function createEnterFunction(mark, state, view, library) {
  875. return createAnimationFunction('enter', mark, state, view, library);
  876. }
  877. /**
  878. * Animation will not cancel automatically, it should be canceled
  879. * manually. This is very important for performance.
  880. */
  881. function cancel(animation) {
  882. animation.finished.then(() => {
  883. animation.cancel();
  884. });
  885. return animation;
  886. }
  887. function createUpdateFunction(mark, state, view, library) {
  888. return createAnimationFunction('update', mark, state, view, library);
  889. }
  890. function createExitFunction(mark, state, view, library) {
  891. return createAnimationFunction('exit', mark, state, view, library);
  892. }
  893. function inferTheme(theme = {}) {
  894. if (typeof theme === 'string')
  895. return { type: theme };
  896. const { type = 'classic' } = theme, rest = __rest(theme, ["type"]);
  897. return Object.assign(Object.assign({}, rest), { type });
  898. }
  899. /**
  900. * @todo Infer builtin tooltips.
  901. */
  902. function inferInteraction(view) {
  903. const defaults = {
  904. event: true,
  905. tooltip: true,
  906. // @todo Inferred by slider self.
  907. sliderFilter: true,
  908. legendFilter: true,
  909. scrollbarFilter: true,
  910. };
  911. const { interaction = {} } = view;
  912. return Object.entries(deepMix(defaults, interaction));
  913. }
  914. function applyTransform(node, library) {
  915. return __awaiter(this, void 0, void 0, function* () {
  916. const context = { library };
  917. const { data } = node, rest = __rest(node, ["data"]);
  918. if (data == undefined)
  919. return node;
  920. const [, { data: newData }] = yield applyDataTransform([], { data }, context);
  921. return Object.assign({ data: newData }, rest);
  922. });
  923. }
  924. function updateBBox(selection) {
  925. selection
  926. .style('x', (d) => d.paddingLeft)
  927. .style('y', (d) => d.paddingTop)
  928. .style('width', (d) => d.innerWidth)
  929. .style('height', (d) => d.innerHeight);
  930. }
  931. function animateBBox(selection, extent) {
  932. const [delay, duration] = extent;
  933. selection.transition(function (data) {
  934. const { x, y, width, height } = this.style;
  935. const { paddingLeft, paddingTop, innerWidth, innerHeight } = data;
  936. const keyframes = [
  937. {
  938. x,
  939. y,
  940. width,
  941. height,
  942. },
  943. {
  944. x: paddingLeft,
  945. y: paddingTop,
  946. width: innerWidth,
  947. height: innerHeight,
  948. },
  949. ];
  950. return this.animate(keyframes, { delay, duration, fill: 'both' });
  951. });
  952. }
  953. function shapeName(mark, name) {
  954. const { type } = mark;
  955. if (typeof name === 'string')
  956. return `${type}.${name}`;
  957. return name;
  958. }
  959. /**
  960. * Create and update layer for each mark.
  961. * All the layers created here are treated as main layers.
  962. */
  963. function updateLayers(selection, marks) {
  964. const facet = (d) => (d.class !== undefined ? `${d.class}` : '');
  965. // Skip for empty selection, it can't append nodes.
  966. const nodes = selection.nodes();
  967. if (nodes.length === 0)
  968. return;
  969. selection
  970. .selectAll(className(MAIN_LAYER_CLASS_NAME))
  971. .data(marks, (d) => d.key)
  972. .join((enter) => enter
  973. .append('g')
  974. .attr('className', MAIN_LAYER_CLASS_NAME)
  975. .attr('id', (d) => d.key)
  976. .style('facet', facet)
  977. .style('fill', 'transparent')
  978. .style('zIndex', (d) => { var _a; return (_a = d.zIndex) !== null && _a !== void 0 ? _a : 0; }), (update) => update
  979. .style('facet', facet)
  980. .style('fill', 'transparent')
  981. .style('zIndex', (d) => { var _a; return (_a = d.zIndex) !== null && _a !== void 0 ? _a : 0; }), (exit) => exit.remove());
  982. const labelLayer = selection.select(className(LABEL_LAYER_CLASS_NAME)).node();
  983. if (labelLayer)
  984. return;
  985. selection
  986. .append('g')
  987. .attr('className', LABEL_LAYER_CLASS_NAME)
  988. .style('zIndex', 0);
  989. }
  990. function className(...names) {
  991. return names.map((d) => `.${d}`).join('');
  992. }
  993. function applyClip(selection, clip) {
  994. if (!selection.node())
  995. return;
  996. selection.style('clipPath', (data) => {
  997. if (!clip)
  998. return null;
  999. const { paddingTop: y, paddingLeft: x, innerWidth: width, innerHeight: height, } = data;
  1000. return new Rect({ style: { x, y, width, height } });
  1001. });
  1002. }
  1003. function inferComponentScales(scales, states, markState) {
  1004. // add shape scale to state.
  1005. var _a;
  1006. // for cell, omit shape scale.
  1007. // @todo support shape scale for cell.
  1008. for (const [key] of markState.entries()) {
  1009. if (key.type === 'cell') {
  1010. return scales.filter((scale) => scale.name !== 'shape');
  1011. }
  1012. }
  1013. // can't infer shape scale if there are multiple states.
  1014. if (states.length !== 1 || scales.some((scale) => scale.name === 'shape')) {
  1015. return scales;
  1016. }
  1017. const { defaultShape: shape } = states[0];
  1018. const acceptMarkTypes = ['point', 'line', 'rect', 'hollow'];
  1019. if (!acceptMarkTypes.includes(shape))
  1020. return scales;
  1021. const shapeMap = {
  1022. point: 'point',
  1023. line: 'hyphen',
  1024. rect: 'square',
  1025. hollow: 'hollow',
  1026. };
  1027. // create shape scale
  1028. const field = ((_a = scales.find((scale) => scale.name === 'color')) === null || _a === void 0 ? void 0 : _a.field) || null;
  1029. const shapeScale = {
  1030. field,
  1031. name: 'shape',
  1032. type: 'constant',
  1033. domain: [],
  1034. range: [shapeMap[shape]],
  1035. };
  1036. return [...scales, shapeScale];
  1037. }
  1038. export function applyStyle(selection, style) {
  1039. for (const [key, value] of Object.entries(style)) {
  1040. selection.style(key, value);
  1041. }
  1042. }
  1043. //# sourceMappingURL=plot.js.map