BaseAxisPointer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. var zrUtil = require("zrender/lib/core/util");
  2. var clazzUtil = require("../../util/clazz");
  3. var graphic = require("../../util/graphic");
  4. var axisPointerModelHelper = require("./modelHelper");
  5. var eventTool = require("zrender/lib/core/event");
  6. var throttleUtil = require("../../util/throttle");
  7. var modelUtil = require("../../util/model");
  8. var get = modelUtil.makeGetter();
  9. var clone = zrUtil.clone;
  10. var bind = zrUtil.bind;
  11. /**
  12. * Base axis pointer class in 2D.
  13. * Implemenents {module:echarts/component/axis/IAxisPointer}.
  14. */
  15. function BaseAxisPointer() {}
  16. BaseAxisPointer.prototype = {
  17. /**
  18. * @private
  19. */
  20. _group: null,
  21. /**
  22. * @private
  23. */
  24. _lastGraphicKey: null,
  25. /**
  26. * @private
  27. */
  28. _handle: null,
  29. /**
  30. * @private
  31. */
  32. _dragging: false,
  33. /**
  34. * @private
  35. */
  36. _lastValue: null,
  37. /**
  38. * @private
  39. */
  40. _lastStatus: null,
  41. /**
  42. * @private
  43. */
  44. _payloadInfo: null,
  45. /**
  46. * In px, arbitrary value. Do not set too small,
  47. * no animation is ok for most cases.
  48. * @protected
  49. */
  50. animationThreshold: 15,
  51. /**
  52. * @implement
  53. */
  54. render: function (axisModel, axisPointerModel, api, forceRender) {
  55. var value = axisPointerModel.get('value');
  56. var status = axisPointerModel.get('status'); // Bind them to `this`, not in closure, otherwise they will not
  57. // be replaced when user calling setOption in not merge mode.
  58. this._axisModel = axisModel;
  59. this._axisPointerModel = axisPointerModel;
  60. this._api = api; // Optimize: `render` will be called repeatly during mouse move.
  61. // So it is power consuming if performing `render` each time,
  62. // especially on mobile device.
  63. if (!forceRender && this._lastValue === value && this._lastStatus === status) {
  64. return;
  65. }
  66. this._lastValue = value;
  67. this._lastStatus = status;
  68. var group = this._group;
  69. var handle = this._handle;
  70. if (!status || status === 'hide') {
  71. // Do not clear here, for animation better.
  72. group && group.hide();
  73. handle && handle.hide();
  74. return;
  75. }
  76. group && group.show();
  77. handle && handle.show(); // Otherwise status is 'show'
  78. var elOption = {};
  79. this.makeElOption(elOption, value, axisModel, axisPointerModel, api); // Enable change axis pointer type.
  80. var graphicKey = elOption.graphicKey;
  81. if (graphicKey !== this._lastGraphicKey) {
  82. this.clear(api);
  83. }
  84. this._lastGraphicKey = graphicKey;
  85. var moveAnimation = this._moveAnimation = this.determineAnimation(axisModel, axisPointerModel);
  86. if (!group) {
  87. group = this._group = new graphic.Group();
  88. this.createPointerEl(group, elOption, axisModel, axisPointerModel);
  89. this.createLabelEl(group, elOption, axisModel, axisPointerModel);
  90. api.getZr().add(group);
  91. } else {
  92. var doUpdateProps = zrUtil.curry(updateProps, axisPointerModel, moveAnimation);
  93. this.updatePointerEl(group, elOption, doUpdateProps, axisPointerModel);
  94. this.updateLabelEl(group, elOption, doUpdateProps, axisPointerModel);
  95. }
  96. updateMandatoryProps(group, axisPointerModel, true);
  97. this._renderHandle(value);
  98. },
  99. /**
  100. * @implement
  101. */
  102. remove: function (api) {
  103. this.clear(api);
  104. },
  105. /**
  106. * @implement
  107. */
  108. dispose: function (api) {
  109. this.clear(api);
  110. },
  111. /**
  112. * @protected
  113. */
  114. determineAnimation: function (axisModel, axisPointerModel) {
  115. var animation = axisPointerModel.get('animation');
  116. var axis = axisModel.axis;
  117. var isCategoryAxis = axis.type === 'category';
  118. var useSnap = axisPointerModel.get('snap'); // Value axis without snap always do not snap.
  119. if (!useSnap && !isCategoryAxis) {
  120. return false;
  121. }
  122. if (animation === 'auto' || animation == null) {
  123. var animationThreshold = this.animationThreshold;
  124. if (isCategoryAxis && axis.getBandWidth() > animationThreshold) {
  125. return true;
  126. } // It is important to auto animation when snap used. Consider if there is
  127. // a dataZoom, animation will be disabled when too many points exist, while
  128. // it will be enabled for better visual effect when little points exist.
  129. if (useSnap) {
  130. var seriesDataCount = axisPointerModelHelper.getAxisInfo(axisModel).seriesDataCount;
  131. var axisExtent = axis.getExtent(); // Approximate band width
  132. return Math.abs(axisExtent[0] - axisExtent[1]) / seriesDataCount > animationThreshold;
  133. }
  134. return false;
  135. }
  136. return animation === true;
  137. },
  138. /**
  139. * add {pointer, label, graphicKey} to elOption
  140. * @protected
  141. */
  142. makeElOption: function (elOption, value, axisModel, axisPointerModel, api) {// Shoule be implemenented by sub-class.
  143. },
  144. /**
  145. * @protected
  146. */
  147. createPointerEl: function (group, elOption, axisModel, axisPointerModel) {
  148. var pointerOption = elOption.pointer;
  149. if (pointerOption) {
  150. var pointerEl = get(group).pointerEl = new graphic[pointerOption.type](clone(elOption.pointer));
  151. group.add(pointerEl);
  152. }
  153. },
  154. /**
  155. * @protected
  156. */
  157. createLabelEl: function (group, elOption, axisModel, axisPointerModel) {
  158. if (elOption.label) {
  159. var labelEl = get(group).labelEl = new graphic.Rect(clone(elOption.label));
  160. group.add(labelEl);
  161. updateLabelShowHide(labelEl, axisPointerModel);
  162. }
  163. },
  164. /**
  165. * @protected
  166. */
  167. updatePointerEl: function (group, elOption, updateProps) {
  168. var pointerEl = get(group).pointerEl;
  169. if (pointerEl) {
  170. pointerEl.setStyle(elOption.pointer.style);
  171. updateProps(pointerEl, {
  172. shape: elOption.pointer.shape
  173. });
  174. }
  175. },
  176. /**
  177. * @protected
  178. */
  179. updateLabelEl: function (group, elOption, updateProps, axisPointerModel) {
  180. var labelEl = get(group).labelEl;
  181. if (labelEl) {
  182. labelEl.setStyle(elOption.label.style);
  183. updateProps(labelEl, {
  184. // Consider text length change in vertical axis, animation should
  185. // be used on shape, otherwise the effect will be weird.
  186. shape: elOption.label.shape,
  187. position: elOption.label.position
  188. });
  189. updateLabelShowHide(labelEl, axisPointerModel);
  190. }
  191. },
  192. /**
  193. * @private
  194. */
  195. _renderHandle: function (value) {
  196. if (this._dragging || !this.updateHandleTransform) {
  197. return;
  198. }
  199. var axisPointerModel = this._axisPointerModel;
  200. var zr = this._api.getZr();
  201. var handle = this._handle;
  202. var handleModel = axisPointerModel.getModel('handle');
  203. var status = axisPointerModel.get('status');
  204. if (!handleModel.get('show') || !status || status === 'hide') {
  205. handle && zr.remove(handle);
  206. this._handle = null;
  207. return;
  208. }
  209. var isInit;
  210. if (!this._handle) {
  211. isInit = true;
  212. handle = this._handle = graphic.createIcon(handleModel.get('icon'), {
  213. cursor: 'move',
  214. draggable: true,
  215. onmousemove: function (e) {
  216. // Fot mobile devicem, prevent screen slider on the button.
  217. eventTool.stop(e.event);
  218. },
  219. onmousedown: bind(this._onHandleDragMove, this, 0, 0),
  220. drift: bind(this._onHandleDragMove, this),
  221. ondragend: bind(this._onHandleDragEnd, this)
  222. });
  223. zr.add(handle);
  224. }
  225. updateMandatoryProps(handle, axisPointerModel, false); // update style
  226. var includeStyles = ['color', 'borderColor', 'borderWidth', 'opacity', 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY'];
  227. handle.setStyle(handleModel.getItemStyle(null, includeStyles)); // update position
  228. var handleSize = handleModel.get('size');
  229. if (!zrUtil.isArray(handleSize)) {
  230. handleSize = [handleSize, handleSize];
  231. }
  232. handle.attr('scale', [handleSize[0] / 2, handleSize[1] / 2]);
  233. throttleUtil.createOrUpdate(this, '_doDispatchAxisPointer', handleModel.get('throttle') || 0, 'fixRate');
  234. this._moveHandleToValue(value, isInit);
  235. },
  236. /**
  237. * @private
  238. */
  239. _moveHandleToValue: function (value, isInit) {
  240. updateProps(this._axisPointerModel, !isInit && this._moveAnimation, this._handle, getHandleTransProps(this.getHandleTransform(value, this._axisModel, this._axisPointerModel)));
  241. },
  242. /**
  243. * @private
  244. */
  245. _onHandleDragMove: function (dx, dy) {
  246. var handle = this._handle;
  247. if (!handle) {
  248. return;
  249. }
  250. this._dragging = true; // Persistent for throttle.
  251. var trans = this.updateHandleTransform(getHandleTransProps(handle), [dx, dy], this._axisModel, this._axisPointerModel);
  252. this._payloadInfo = trans;
  253. handle.stopAnimation();
  254. handle.attr(getHandleTransProps(trans));
  255. get(handle).lastProp = null;
  256. this._doDispatchAxisPointer();
  257. },
  258. /**
  259. * Throttled method.
  260. * @private
  261. */
  262. _doDispatchAxisPointer: function () {
  263. var handle = this._handle;
  264. if (!handle) {
  265. return;
  266. }
  267. var payloadInfo = this._payloadInfo;
  268. var axisModel = this._axisModel;
  269. this._api.dispatchAction({
  270. type: 'updateAxisPointer',
  271. x: payloadInfo.cursorPoint[0],
  272. y: payloadInfo.cursorPoint[1],
  273. tooltipOption: payloadInfo.tooltipOption,
  274. axesInfo: [{
  275. axisDim: axisModel.axis.dim,
  276. axisIndex: axisModel.componentIndex
  277. }]
  278. });
  279. },
  280. /**
  281. * @private
  282. */
  283. _onHandleDragEnd: function (moveAnimation) {
  284. this._dragging = false;
  285. var handle = this._handle;
  286. if (!handle) {
  287. return;
  288. }
  289. var value = this._axisPointerModel.get('value'); // Consider snap or categroy axis, handle may be not consistent with
  290. // axisPointer. So move handle to align the exact value position when
  291. // drag ended.
  292. this._moveHandleToValue(value); // For the effect: tooltip will be shown when finger holding on handle
  293. // button, and will be hidden after finger left handle button.
  294. this._api.dispatchAction({
  295. type: 'hideTip'
  296. });
  297. },
  298. /**
  299. * Should be implemenented by sub-class if support `handle`.
  300. * @protected
  301. * @param {number} value
  302. * @param {module:echarts/model/Model} axisModel
  303. * @param {module:echarts/model/Model} axisPointerModel
  304. * @return {Object} {position: [x, y], rotation: 0}
  305. */
  306. getHandleTransform: null,
  307. /**
  308. * * Should be implemenented by sub-class if support `handle`.
  309. * @protected
  310. * @param {Object} transform {position, rotation}
  311. * @param {Array.<number>} delta [dx, dy]
  312. * @param {module:echarts/model/Model} axisModel
  313. * @param {module:echarts/model/Model} axisPointerModel
  314. * @return {Object} {position: [x, y], rotation: 0, cursorPoint: [x, y]}
  315. */
  316. updateHandleTransform: null,
  317. /**
  318. * @private
  319. */
  320. clear: function (api) {
  321. this._lastValue = null;
  322. this._lastStatus = null;
  323. var zr = api.getZr();
  324. var group = this._group;
  325. var handle = this._handle;
  326. if (zr && group) {
  327. this._lastGraphicKey = null;
  328. group && zr.remove(group);
  329. handle && zr.remove(handle);
  330. this._group = null;
  331. this._handle = null;
  332. this._payloadInfo = null;
  333. }
  334. },
  335. /**
  336. * @protected
  337. */
  338. doClear: function () {// Implemented by sub-class if necessary.
  339. },
  340. /**
  341. * @protected
  342. * @param {Array.<number>} xy
  343. * @param {Array.<number>} wh
  344. * @param {number} [xDimIndex=0] or 1
  345. */
  346. buildLabel: function (xy, wh, xDimIndex) {
  347. xDimIndex = xDimIndex || 0;
  348. return {
  349. x: xy[xDimIndex],
  350. y: xy[1 - xDimIndex],
  351. width: wh[xDimIndex],
  352. height: wh[1 - xDimIndex]
  353. };
  354. }
  355. };
  356. BaseAxisPointer.prototype.constructor = BaseAxisPointer;
  357. function updateProps(animationModel, moveAnimation, el, props) {
  358. // Animation optimize.
  359. if (!propsEqual(get(el).lastProp, props)) {
  360. get(el).lastProp = props;
  361. moveAnimation ? graphic.updateProps(el, props, animationModel) : (el.stopAnimation(), el.attr(props));
  362. }
  363. }
  364. function propsEqual(lastProps, newProps) {
  365. if (zrUtil.isObject(lastProps) && zrUtil.isObject(newProps)) {
  366. var equals = true;
  367. zrUtil.each(newProps, function (item, key) {
  368. equals = equals && propsEqual(lastProps[key], item);
  369. });
  370. return !!equals;
  371. } else {
  372. return lastProps === newProps;
  373. }
  374. }
  375. function updateLabelShowHide(labelEl, axisPointerModel) {
  376. labelEl[axisPointerModel.get('label.show') ? 'show' : 'hide']();
  377. }
  378. function getHandleTransProps(trans) {
  379. return {
  380. position: trans.position.slice(),
  381. rotation: trans.rotation || 0
  382. };
  383. }
  384. function updateMandatoryProps(group, axisPointerModel, silent) {
  385. var z = axisPointerModel.get('z');
  386. var zlevel = axisPointerModel.get('zlevel');
  387. group && group.traverse(function (el) {
  388. if (el.type !== 'group') {
  389. z != null && (el.z = z);
  390. zlevel != null && (el.zlevel = zlevel);
  391. el.silent = silent;
  392. }
  393. });
  394. }
  395. clazzUtil.enableClassExtend(BaseAxisPointer);
  396. var _default = BaseAxisPointer;
  397. module.exports = _default;