chartIndex.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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 { Line, Text } from '@antv/g';
  22. import { deepMix, throttle } from '@antv/util';
  23. import { max, min, rollup, sort, bisectCenter, bisector, group, } from 'd3-array';
  24. import { subObject } from '../utils/helper';
  25. import { ELEMENT_CLASS_NAME, LABEL_CLASS_NAME, } from '../runtime';
  26. import { selectPlotArea, mousePosition } from './utils';
  27. function maybeTransform(options) {
  28. const { transform = [] } = options;
  29. const normalizeY = transform.find((d) => d.type === 'normalizeY');
  30. if (normalizeY)
  31. return normalizeY;
  32. const newNormalizeY = { type: 'normalizeY' };
  33. transform.push(newNormalizeY);
  34. options.transform = transform;
  35. return newNormalizeY;
  36. }
  37. function markValue(markState, markName, channels) {
  38. const [value] = Array.from(markState.entries())
  39. .filter(([mark]) => mark.type === markName)
  40. .map(([mark]) => {
  41. const { encode } = mark;
  42. const channel = (name) => {
  43. const channel = encode[name];
  44. return [name, channel ? channel.value : undefined];
  45. };
  46. return Object.fromEntries(channels.map(channel));
  47. });
  48. return value;
  49. }
  50. /**
  51. * @todo Perf
  52. */
  53. export function ChartIndex(_a) {
  54. var { wait = 20, leading, trailing = false, labelFormatter = (date) => `${date}` } = _a, style = __rest(_a, ["wait", "leading", "trailing", "labelFormatter"]);
  55. return (context) => {
  56. const { options, view, container, update } = context;
  57. const { markState, scale, coordinate } = view;
  58. // Get line mark value, exit if it is not existed.
  59. const value = markValue(markState, 'line', ['x', 'y', 'series']);
  60. if (!value)
  61. return;
  62. // Prepare channel value.
  63. const { y: Y, x: X, series: S = [] } = value;
  64. const I = Y.map((_, i) => i);
  65. const sortedX = sort(I.map((i) => X[i]));
  66. // Clone options and get line mark.
  67. const clonedOptions = deepMix({}, options);
  68. const lineMark = clonedOptions.marks.find((d) => d.type === 'line');
  69. // Update domain of y scale for the line mark.
  70. const r = (I) => max(I, (i) => +Y[i]) / min(I, (i) => +Y[i]);
  71. const k = max(rollup(I, r, (i) => S[i]).values());
  72. const domainY = [1 / k, k];
  73. deepMix(lineMark, {
  74. scale: { y: { domain: domainY } },
  75. });
  76. // Prepare shapes.
  77. const plotArea = selectPlotArea(container);
  78. const lines = container.getElementsByClassName(ELEMENT_CLASS_NAME);
  79. const labels = container.getElementsByClassName(LABEL_CLASS_NAME);
  80. // The format of label key: `${elementKey}-index`,
  81. // group labels by elementKey.
  82. const keyofLabel = (d) => d.__data__.key.split('-')[0];
  83. const keyLabels = group(labels, keyofLabel);
  84. const rule = new Line({
  85. style: Object.assign({ x1: 0, y1: 0, x2: 0, y2: plotArea.getAttribute('height'), stroke: 'black', lineWidth: 1 }, subObject(style, 'rule')),
  86. });
  87. const text = new Text({
  88. style: Object.assign({ x: 0, y: plotArea.getAttribute('height'), text: '', fontSize: 10 }, subObject(style, 'label')),
  89. });
  90. rule.append(text);
  91. plotArea.appendChild(rule);
  92. // Get the closet date to the rule.
  93. const dateByFocus = (coordinate, scaleX, focus) => {
  94. const [normalizedX] = coordinate.invert(focus);
  95. const date = scaleX.invert(normalizedX);
  96. return sortedX[bisectCenter(sortedX, date)];
  97. };
  98. // Update rule and label content.
  99. const updateRule = (focus, date) => {
  100. rule.setAttribute('x1', focus[0]);
  101. rule.setAttribute('x2', focus[0]);
  102. text.setAttribute('text', labelFormatter(date));
  103. };
  104. // Store the new inner state alter rerender the view.
  105. let newView;
  106. // Rerender the view to update basis for each line.
  107. const updateBasisByRerender = (focus) => __awaiter(this, void 0, void 0, function* () {
  108. // Find the closetDate to the rule.
  109. const { x: scaleX } = scale;
  110. const date = dateByFocus(coordinate, scaleX, focus);
  111. updateRule(focus, date);
  112. // Update normalize options.
  113. const normalizeY = maybeTransform(lineMark);
  114. normalizeY.groupBy = 'color';
  115. normalizeY.basis = (I, Y) => {
  116. const i = I[bisector((i) => X[+i]).center(I, date)];
  117. return Y[i];
  118. };
  119. // Disable animation.
  120. for (const mark of clonedOptions.marks)
  121. mark.animate = false;
  122. const newState = yield update(clonedOptions);
  123. newView = newState.view;
  124. });
  125. // Only apply translate to update basis for each line.
  126. // If performance is ok, there is no need to use this
  127. // strategy to update basis.
  128. const updateBasisByTranslate = (focus) => {
  129. // Find the closetDate to the rule.
  130. const { scale, coordinate } = newView;
  131. const { x: scaleX, y: scaleY } = scale;
  132. const date = dateByFocus(coordinate, scaleX, focus);
  133. updateRule(focus, date);
  134. // Translate mark and label for better performance.
  135. for (const line of lines) {
  136. // Compute transform in y direction.
  137. const { seriesIndex: SI, key } = line.__data__;
  138. const i = SI[bisector((i) => X[+i]).center(SI, date)];
  139. const p0 = [0, scaleY.map(1)]; // basis point
  140. const p1 = [0, scaleY.map(Y[i] / Y[SI[0]])];
  141. const [, y0] = coordinate.map(p0);
  142. const [, y1] = coordinate.map(p1);
  143. const dy = y0 - y1;
  144. line.setAttribute('transform', `translate(0, ${dy})`);
  145. // Update line and related label.
  146. const labels = keyLabels.get(key) || [];
  147. for (const label of labels) {
  148. // @todo Replace with style.transform.
  149. // It now has unexpected behavior.
  150. label.setAttribute('dy', dy);
  151. }
  152. }
  153. };
  154. const updateBasis = throttle((event) => {
  155. const focus = mousePosition(plotArea, event);
  156. if (!focus)
  157. return;
  158. updateBasisByTranslate(focus);
  159. }, wait, { leading, trailing });
  160. updateBasisByRerender([0, 0]);
  161. plotArea.addEventListener('pointerenter', updateBasis);
  162. plotArea.addEventListener('pointermove', updateBasis);
  163. plotArea.addEventListener('pointerleave', updateBasis);
  164. return () => {
  165. rule.remove();
  166. plotArea.removeEventListener('pointerenter', updateBasis);
  167. plotArea.removeEventListener('pointermove', updateBasis);
  168. plotArea.removeEventListener('pointerleave', updateBasis);
  169. };
  170. };
  171. }
  172. //# sourceMappingURL=chartIndex.js.map