brushAxisHighlight.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. var __rest = (this && this.__rest) || function (s, e) {
  2. var t = {};
  3. for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
  4. t[p] = s[p];
  5. if (s != null && typeof Object.getOwnPropertySymbols === "function")
  6. for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
  7. if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
  8. t[p[i]] = s[p[i]];
  9. }
  10. return t;
  11. };
  12. import { Rect } from '@antv/g';
  13. import { subObject } from '../utils/helper';
  14. import { domainOf } from '../utils/scale';
  15. import { brush } from './brushHighlight';
  16. import { brushXRegion } from './brushXHighlight';
  17. import { brushYRegion } from './brushYHighlight';
  18. import { selectG2Elements, createDatumof, createValueof, useState, selectPlotArea, mergeState, } from './utils';
  19. export const AXIS_CLASS_NAME = 'axis';
  20. export const AXIS_LINE_CLASS_NAME = 'axis-line';
  21. export const AXIS_MAIN_CLASS_NAME = 'axis-main-group';
  22. export const AXIS_HOT_AREA_CLASS_NAME = 'axis-hot-area';
  23. function axesOf(container) {
  24. return container.getElementsByClassName(AXIS_CLASS_NAME);
  25. }
  26. function lineOf(axis) {
  27. return axis.getElementsByClassName(AXIS_LINE_CLASS_NAME)[0];
  28. }
  29. function mainGroupOf(axis) {
  30. return axis.getElementsByClassName(AXIS_MAIN_CLASS_NAME)[0];
  31. }
  32. // Use the bounds of main group of axis as the bounds of axis,
  33. // get rid of grid and title.
  34. function boundsOfAxis(axis) {
  35. return mainGroupOf(axis).getLocalBounds();
  36. }
  37. // Brush for vertical axis.
  38. function verticalBrush(axis, _a) {
  39. var { cross, offsetX, offsetY } = _a, style = __rest(_a, ["cross", "offsetX", "offsetY"]);
  40. const bounds = boundsOfAxis(axis);
  41. const axisLine = lineOf(axis);
  42. const [lineX] = axisLine.getLocalBounds().min;
  43. const [minX, minY] = bounds.min;
  44. const [maxX, maxY] = bounds.max;
  45. const size = (maxX - minX) * 2;
  46. return {
  47. brushRegion: brushYRegion,
  48. hotZone: new Rect({
  49. className: AXIS_HOT_AREA_CLASS_NAME,
  50. style: Object.assign({
  51. // If it is not cross, draw brush in both side of axisLine,
  52. // otherwise the draw brush within bounds area.
  53. x: cross ? minX : lineX - size / 2, width: cross ? size / 2 : size, y: minY, height: maxY - minY }, style),
  54. }),
  55. extent: cross
  56. ? // If it is cross, the x range is ignored.
  57. (x, y, x1, y1) => [-Infinity, y, Infinity, y1]
  58. : (x, y, x1, y1) => [
  59. Math.floor(minX - offsetX),
  60. y,
  61. Math.ceil(maxX - offsetX),
  62. y1,
  63. ],
  64. };
  65. }
  66. // Brush for horizontal axis.
  67. function horizontalBrush(axis, _a) {
  68. var { offsetY, offsetX, cross = false } = _a, style = __rest(_a, ["offsetY", "offsetX", "cross"]);
  69. const bounds = boundsOfAxis(axis);
  70. const axisLine = lineOf(axis);
  71. const [, lineY] = axisLine.getLocalBounds().min;
  72. const [minX, minY] = bounds.min;
  73. const [maxX, maxY] = bounds.max;
  74. const size = maxY - minY;
  75. return {
  76. brushRegion: brushXRegion,
  77. hotZone: new Rect({
  78. className: AXIS_HOT_AREA_CLASS_NAME,
  79. style: Object.assign({ x: minX, width: maxX - minX,
  80. // If it is not cross, draw brush in both side of axisLine,
  81. // otherwise the draw brush within bounds area.
  82. y: cross ? minY : lineY - size, height: cross ? size : size * 2 }, style),
  83. }),
  84. extent: cross
  85. ? // If it is cross, the y range is ignored.
  86. (x, y, x1, y1) => [x, -Infinity, x1, Infinity]
  87. : (x, y, x1, y1) => [
  88. x,
  89. Math.floor(minY - offsetY),
  90. x1,
  91. Math.ceil(maxY - offsetY),
  92. ],
  93. };
  94. }
  95. export function brushAxisHighlight(root, _a) {
  96. var { axes: axesOf, // given root, return axes
  97. elements: elementsOf, // given root, return elements
  98. points: pointsOf, // given shape, return control points
  99. horizontal: isHorizontal, // given axis, return direction
  100. datum, // given shape, return datum
  101. offsetY, // offsetY for shape area
  102. offsetX, // offsetX for shape area
  103. reverse = false, state = {}, emitter, coordinate } = _a, rest = __rest(_a, ["axes", "elements", "points", "horizontal", "datum", "offsetY", "offsetX", "reverse", "state", "emitter", "coordinate"]) // style
  104. ;
  105. const elements = elementsOf(root);
  106. const axes = axesOf(root);
  107. const valueof = createValueof(elements, datum);
  108. const { setState, removeState } = useState(state, valueof);
  109. const axisExtent = new Map();
  110. const brushStyle = subObject(rest, 'mask');
  111. // Only some of shape's points in all mask, it is selected.
  112. const brushed = (points) => Array.from(axisExtent.values()).every(([x, y, x1, y1]) => points.some(([x0, y0]) => {
  113. return x0 >= x && x0 <= x1 && y0 >= y && y0 <= y1;
  114. }));
  115. const scales = axes.map((d) => d.attributes.scale);
  116. const extentOf = (D) => (D.length > 2 ? [D[0], D[D.length - 1]] : D);
  117. const indexDomain = new Map();
  118. const initIndexDomain = () => {
  119. indexDomain.clear();
  120. for (let i = 0; i < axes.length; i++) {
  121. const scale = scales[i];
  122. const { domain } = scale.getOptions();
  123. indexDomain.set(i, extentOf(domain));
  124. }
  125. };
  126. initIndexDomain();
  127. // Update element when brush changed.
  128. const updateElement = (i, emit) => {
  129. const selectedElements = [];
  130. for (const element of elements) {
  131. const points = pointsOf(element);
  132. if (brushed(points)) {
  133. setState(element, 'active');
  134. selectedElements.push(element);
  135. }
  136. else
  137. setState(element, 'inactive');
  138. }
  139. indexDomain.set(i, selectionOf(selectedElements, i));
  140. if (!emit)
  141. return;
  142. // Emit events.
  143. const selection = () => {
  144. if (!cross)
  145. return Array.from(indexDomain.values());
  146. const S = [];
  147. for (const [index, domain] of indexDomain) {
  148. const scale = scales[index];
  149. const { name } = scale.getOptions();
  150. if (name === 'x')
  151. S[0] = domain;
  152. else
  153. S[1] = domain;
  154. }
  155. return S;
  156. };
  157. emitter.emit('brushAxis:highlight', {
  158. nativeEvent: true,
  159. data: {
  160. selection: selection(),
  161. },
  162. });
  163. };
  164. const clearElement = (emit) => {
  165. for (const element of elements)
  166. removeState(element, 'active', 'inactive');
  167. initIndexDomain();
  168. if (!emit)
  169. return;
  170. emitter.emit('brushAxis:remove', { nativeEvent: true });
  171. };
  172. const selectionOf = (selected, i) => {
  173. const scale = scales[i];
  174. const { name } = scale.getOptions();
  175. const domain = selected.map((d) => {
  176. const data = d.__data__;
  177. return scale.invert(data[name]);
  178. });
  179. return extentOf(domainOf(scale, domain));
  180. };
  181. // Distinguish between parallel coordinates and normal charts.
  182. const cross = axes.some(isHorizontal) && axes.some((d) => !isHorizontal(d));
  183. const handlers = [];
  184. for (let i = 0; i < axes.length; i++) {
  185. const axis = axes[i];
  186. const createBrush = isHorizontal(axis) ? horizontalBrush : verticalBrush;
  187. const { hotZone, brushRegion, extent } = createBrush(axis, {
  188. offsetY,
  189. offsetX,
  190. cross,
  191. zIndex: 999,
  192. fill: 'transparent', // Make it interactive.
  193. });
  194. axis.parentNode.appendChild(hotZone);
  195. const brushHandler = brush(hotZone, Object.assign(Object.assign({}, brushStyle), { reverse,
  196. brushRegion,
  197. brushended(emit) {
  198. axisExtent.delete(axis);
  199. if (Array.from(axisExtent.entries()).length === 0)
  200. clearElement(emit);
  201. else
  202. updateElement(i, emit);
  203. },
  204. brushed(x, y, x1, y1, emit) {
  205. axisExtent.set(axis, extent(x, y, x1, y1));
  206. updateElement(i, emit);
  207. } }));
  208. handlers.push(brushHandler);
  209. }
  210. const onRemove = (event = {}) => {
  211. const { nativeEvent } = event;
  212. if (nativeEvent)
  213. return;
  214. handlers.forEach((d) => d.remove());
  215. };
  216. const rangeOf = (domain, scale, axis) => {
  217. const [d0, d1] = domain;
  218. const maybeStep = (scale) => (scale.getStep ? scale.getStep() : 0);
  219. const x = abstractOf(d0, scale, axis);
  220. const x1 = abstractOf(d1, scale, axis) + maybeStep(scale);
  221. if (isHorizontal(axis))
  222. return [x, -Infinity, x1, Infinity];
  223. return [-Infinity, x, Infinity, x1];
  224. };
  225. const abstractOf = (x, scale, axis) => {
  226. const { height, width } = coordinate.getOptions();
  227. const scale1 = scale.clone();
  228. if (isHorizontal(axis))
  229. scale1.update({ range: [0, width] });
  230. else
  231. scale1.update({ range: [height, 0] });
  232. return scale1.map(x);
  233. };
  234. const onHighlight = (event) => {
  235. const { nativeEvent } = event;
  236. if (nativeEvent)
  237. return;
  238. const { selection } = event.data;
  239. for (let i = 0; i < handlers.length; i++) {
  240. const domain = selection[i];
  241. const handler = handlers[i];
  242. const axis = axes[i];
  243. if (domain) {
  244. const scale = scales[i];
  245. handler.move(...rangeOf(domain, scale, axis), false);
  246. }
  247. else {
  248. handler.remove();
  249. }
  250. }
  251. };
  252. emitter.on('brushAxis:remove', onRemove);
  253. emitter.on('brushAxis:highlight', onHighlight);
  254. return () => {
  255. handlers.forEach((d) => d.destroy());
  256. emitter.off('brushAxis:remove', onRemove);
  257. emitter.off('brushAxis:highlight', onHighlight);
  258. };
  259. }
  260. /**
  261. * @todo Support mask size.
  262. */
  263. export function BrushAxisHighlight(options) {
  264. return (target, _, emitter) => {
  265. const { container, view, options: viewOptions } = target;
  266. const plotArea = selectPlotArea(container);
  267. const { x: x0, y: y0 } = plotArea.getBBox();
  268. const { coordinate } = view;
  269. return brushAxisHighlight(container, Object.assign({ elements: selectG2Elements, axes: axesOf, offsetY: y0, offsetX: x0, points: (element) => element.__data__.points, horizontal: (axis) => {
  270. const { startPos: [sx, sy], endPos: [ex, ey], } = axis.attributes;
  271. // attention, non-horizontal does not mean vertical
  272. // it may has a specific degree angle
  273. return sx !== ex && sy === ey;
  274. }, datum: createDatumof(view), state: mergeState(viewOptions, [
  275. 'active',
  276. ['inactive', { opacity: 0.5 }],
  277. ]), coordinate,
  278. emitter }, options));
  279. };
  280. }
  281. //# sourceMappingURL=brushAxisHighlight.js.map