TooltipView.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. /**
  20. * AUTO-GENERATED FILE. DO NOT MODIFY.
  21. */
  22. import { __extends } from "tslib";
  23. /*
  24. * Licensed to the Apache Software Foundation (ASF) under one
  25. * or more contributor license agreements. See the NOTICE file
  26. * distributed with this work for additional information
  27. * regarding copyright ownership. The ASF licenses this file
  28. * to you under the Apache License, Version 2.0 (the
  29. * "License"); you may not use this file except in compliance
  30. * with the License. You may obtain a copy of the License at
  31. *
  32. * http://www.apache.org/licenses/LICENSE-2.0
  33. *
  34. * Unless required by applicable law or agreed to in writing,
  35. * software distributed under the License is distributed on an
  36. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  37. * KIND, either express or implied. See the License for the
  38. * specific language governing permissions and limitations
  39. * under the License.
  40. */
  41. import { bind, each, clone, trim, isString, isFunction, isArray, isObject, extend } from 'zrender/lib/core/util.js';
  42. import env from 'zrender/lib/core/env.js';
  43. import TooltipHTMLContent from './TooltipHTMLContent.js';
  44. import TooltipRichContent from './TooltipRichContent.js';
  45. import { convertToColorString, encodeHTML, formatTpl } from '../../util/format.js';
  46. import { parsePercent } from '../../util/number.js';
  47. import { Rect } from '../../util/graphic.js';
  48. import findPointFromSeries from '../axisPointer/findPointFromSeries.js';
  49. import { getLayoutRect } from '../../util/layout.js';
  50. import Model from '../../model/Model.js';
  51. import * as globalListener from '../axisPointer/globalListener.js';
  52. import * as axisHelper from '../../coord/axisHelper.js';
  53. import * as axisPointerViewHelper from '../axisPointer/viewHelper.js';
  54. import { getTooltipRenderMode, preParseFinder, queryReferringComponents } from '../../util/model.js';
  55. import ComponentView from '../../view/Component.js';
  56. import { format as timeFormat } from '../../util/time.js';
  57. import { getECData } from '../../util/innerStore.js';
  58. import { shouldTooltipConfine } from './helper.js';
  59. import { normalizeTooltipFormatResult } from '../../model/mixin/dataFormat.js';
  60. import { createTooltipMarkup, buildTooltipMarkup, TooltipMarkupStyleCreator } from './tooltipMarkup.js';
  61. import { findEventDispatcher } from '../../util/event.js';
  62. import { clear, createOrUpdate } from '../../util/throttle.js';
  63. var proxyRect = new Rect({
  64. shape: {
  65. x: -1,
  66. y: -1,
  67. width: 2,
  68. height: 2
  69. }
  70. });
  71. var TooltipView = /** @class */function (_super) {
  72. __extends(TooltipView, _super);
  73. function TooltipView() {
  74. var _this = _super !== null && _super.apply(this, arguments) || this;
  75. _this.type = TooltipView.type;
  76. return _this;
  77. }
  78. TooltipView.prototype.init = function (ecModel, api) {
  79. if (env.node || !api.getDom()) {
  80. return;
  81. }
  82. var tooltipModel = ecModel.getComponent('tooltip');
  83. var renderMode = this._renderMode = getTooltipRenderMode(tooltipModel.get('renderMode'));
  84. this._tooltipContent = renderMode === 'richText' ? new TooltipRichContent(api) : new TooltipHTMLContent(api, {
  85. appendTo: tooltipModel.get('appendToBody', true) ? 'body' : tooltipModel.get('appendTo', true)
  86. });
  87. };
  88. TooltipView.prototype.render = function (tooltipModel, ecModel, api) {
  89. if (env.node || !api.getDom()) {
  90. return;
  91. }
  92. // Reset
  93. this.group.removeAll();
  94. this._tooltipModel = tooltipModel;
  95. this._ecModel = ecModel;
  96. this._api = api;
  97. var tooltipContent = this._tooltipContent;
  98. tooltipContent.update(tooltipModel);
  99. tooltipContent.setEnterable(tooltipModel.get('enterable'));
  100. this._initGlobalListener();
  101. this._keepShow();
  102. // PENDING
  103. // `mousemove` event will be triggered very frequently when the mouse moves fast,
  104. // which causes that the `updatePosition` function was also called frequently.
  105. // In Chrome with devtools open and Firefox, tooltip looks laggy and shakes. See #14695 #16101
  106. // To avoid frequent triggering,
  107. // consider throttling it in 50ms when transition is enabled
  108. if (this._renderMode !== 'richText' && tooltipModel.get('transitionDuration')) {
  109. createOrUpdate(this, '_updatePosition', 50, 'fixRate');
  110. } else {
  111. clear(this, '_updatePosition');
  112. }
  113. };
  114. TooltipView.prototype._initGlobalListener = function () {
  115. var tooltipModel = this._tooltipModel;
  116. var triggerOn = tooltipModel.get('triggerOn');
  117. globalListener.register('itemTooltip', this._api, bind(function (currTrigger, e, dispatchAction) {
  118. // If 'none', it is not controlled by mouse totally.
  119. if (triggerOn !== 'none') {
  120. if (triggerOn.indexOf(currTrigger) >= 0) {
  121. this._tryShow(e, dispatchAction);
  122. } else if (currTrigger === 'leave') {
  123. this._hide(dispatchAction);
  124. }
  125. }
  126. }, this));
  127. };
  128. TooltipView.prototype._keepShow = function () {
  129. var tooltipModel = this._tooltipModel;
  130. var ecModel = this._ecModel;
  131. var api = this._api;
  132. var triggerOn = tooltipModel.get('triggerOn');
  133. // Try to keep the tooltip show when refreshing
  134. if (this._lastX != null && this._lastY != null
  135. // When user is willing to control tooltip totally using API,
  136. // self.manuallyShowTip({x, y}) might cause tooltip hide,
  137. // which is not expected.
  138. && triggerOn !== 'none' && triggerOn !== 'click') {
  139. var self_1 = this;
  140. clearTimeout(this._refreshUpdateTimeout);
  141. this._refreshUpdateTimeout = setTimeout(function () {
  142. // Show tip next tick after other charts are rendered
  143. // In case highlight action has wrong result
  144. // FIXME
  145. !api.isDisposed() && self_1.manuallyShowTip(tooltipModel, ecModel, api, {
  146. x: self_1._lastX,
  147. y: self_1._lastY,
  148. dataByCoordSys: self_1._lastDataByCoordSys
  149. });
  150. });
  151. }
  152. };
  153. /**
  154. * Show tip manually by
  155. * dispatchAction({
  156. * type: 'showTip',
  157. * x: 10,
  158. * y: 10
  159. * });
  160. * Or
  161. * dispatchAction({
  162. * type: 'showTip',
  163. * seriesIndex: 0,
  164. * dataIndex or dataIndexInside or name
  165. * });
  166. *
  167. * TODO Batch
  168. */
  169. TooltipView.prototype.manuallyShowTip = function (tooltipModel, ecModel, api, payload) {
  170. if (payload.from === this.uid || env.node || !api.getDom()) {
  171. return;
  172. }
  173. var dispatchAction = makeDispatchAction(payload, api);
  174. // Reset ticket
  175. this._ticket = '';
  176. // When triggered from axisPointer.
  177. var dataByCoordSys = payload.dataByCoordSys;
  178. var cmptRef = findComponentReference(payload, ecModel, api);
  179. if (cmptRef) {
  180. var rect = cmptRef.el.getBoundingRect().clone();
  181. rect.applyTransform(cmptRef.el.transform);
  182. this._tryShow({
  183. offsetX: rect.x + rect.width / 2,
  184. offsetY: rect.y + rect.height / 2,
  185. target: cmptRef.el,
  186. position: payload.position,
  187. // When manully trigger, the mouse is not on the el, so we'd better to
  188. // position tooltip on the bottom of the el and display arrow is possible.
  189. positionDefault: 'bottom'
  190. }, dispatchAction);
  191. } else if (payload.tooltip && payload.x != null && payload.y != null) {
  192. var el = proxyRect;
  193. el.x = payload.x;
  194. el.y = payload.y;
  195. el.update();
  196. getECData(el).tooltipConfig = {
  197. name: null,
  198. option: payload.tooltip
  199. };
  200. // Manually show tooltip while view is not using zrender elements.
  201. this._tryShow({
  202. offsetX: payload.x,
  203. offsetY: payload.y,
  204. target: el
  205. }, dispatchAction);
  206. } else if (dataByCoordSys) {
  207. this._tryShow({
  208. offsetX: payload.x,
  209. offsetY: payload.y,
  210. position: payload.position,
  211. dataByCoordSys: dataByCoordSys,
  212. tooltipOption: payload.tooltipOption
  213. }, dispatchAction);
  214. } else if (payload.seriesIndex != null) {
  215. if (this._manuallyAxisShowTip(tooltipModel, ecModel, api, payload)) {
  216. return;
  217. }
  218. var pointInfo = findPointFromSeries(payload, ecModel);
  219. var cx = pointInfo.point[0];
  220. var cy = pointInfo.point[1];
  221. if (cx != null && cy != null) {
  222. this._tryShow({
  223. offsetX: cx,
  224. offsetY: cy,
  225. target: pointInfo.el,
  226. position: payload.position,
  227. // When manully trigger, the mouse is not on the el, so we'd better to
  228. // position tooltip on the bottom of the el and display arrow is possible.
  229. positionDefault: 'bottom'
  230. }, dispatchAction);
  231. }
  232. } else if (payload.x != null && payload.y != null) {
  233. // FIXME
  234. // should wrap dispatchAction like `axisPointer/globalListener` ?
  235. api.dispatchAction({
  236. type: 'updateAxisPointer',
  237. x: payload.x,
  238. y: payload.y
  239. });
  240. this._tryShow({
  241. offsetX: payload.x,
  242. offsetY: payload.y,
  243. position: payload.position,
  244. target: api.getZr().findHover(payload.x, payload.y).target
  245. }, dispatchAction);
  246. }
  247. };
  248. TooltipView.prototype.manuallyHideTip = function (tooltipModel, ecModel, api, payload) {
  249. var tooltipContent = this._tooltipContent;
  250. if (this._tooltipModel) {
  251. tooltipContent.hideLater(this._tooltipModel.get('hideDelay'));
  252. }
  253. this._lastX = this._lastY = this._lastDataByCoordSys = null;
  254. if (payload.from !== this.uid) {
  255. this._hide(makeDispatchAction(payload, api));
  256. }
  257. };
  258. // Be compatible with previous design, that is, when tooltip.type is 'axis' and
  259. // dispatchAction 'showTip' with seriesIndex and dataIndex will trigger axis pointer
  260. // and tooltip.
  261. TooltipView.prototype._manuallyAxisShowTip = function (tooltipModel, ecModel, api, payload) {
  262. var seriesIndex = payload.seriesIndex;
  263. var dataIndex = payload.dataIndex;
  264. // @ts-ignore
  265. var coordSysAxesInfo = ecModel.getComponent('axisPointer').coordSysAxesInfo;
  266. if (seriesIndex == null || dataIndex == null || coordSysAxesInfo == null) {
  267. return;
  268. }
  269. var seriesModel = ecModel.getSeriesByIndex(seriesIndex);
  270. if (!seriesModel) {
  271. return;
  272. }
  273. var data = seriesModel.getData();
  274. var tooltipCascadedModel = buildTooltipModel([data.getItemModel(dataIndex), seriesModel, (seriesModel.coordinateSystem || {}).model], this._tooltipModel);
  275. if (tooltipCascadedModel.get('trigger') !== 'axis') {
  276. return;
  277. }
  278. api.dispatchAction({
  279. type: 'updateAxisPointer',
  280. seriesIndex: seriesIndex,
  281. dataIndex: dataIndex,
  282. position: payload.position
  283. });
  284. return true;
  285. };
  286. TooltipView.prototype._tryShow = function (e, dispatchAction) {
  287. var el = e.target;
  288. var tooltipModel = this._tooltipModel;
  289. if (!tooltipModel) {
  290. return;
  291. }
  292. // Save mouse x, mouse y. So we can try to keep showing the tip if chart is refreshed
  293. this._lastX = e.offsetX;
  294. this._lastY = e.offsetY;
  295. var dataByCoordSys = e.dataByCoordSys;
  296. if (dataByCoordSys && dataByCoordSys.length) {
  297. this._showAxisTooltip(dataByCoordSys, e);
  298. } else if (el) {
  299. var ecData = getECData(el);
  300. if (ecData.ssrType === 'legend') {
  301. // Don't trigger tooltip for legend tooltip item
  302. return;
  303. }
  304. this._lastDataByCoordSys = null;
  305. var seriesDispatcher_1;
  306. var cmptDispatcher_1;
  307. findEventDispatcher(el, function (target) {
  308. if (target.tooltipDisabled) {
  309. seriesDispatcher_1 = cmptDispatcher_1 = null;
  310. return true;
  311. }
  312. if (seriesDispatcher_1 || cmptDispatcher_1) {
  313. return;
  314. }
  315. // Always show item tooltip if mouse is on the element with dataIndex
  316. if (getECData(target).dataIndex != null) {
  317. seriesDispatcher_1 = target;
  318. }
  319. // Tooltip provided directly. Like legend.
  320. else if (getECData(target).tooltipConfig != null) {
  321. cmptDispatcher_1 = target;
  322. }
  323. }, true);
  324. if (seriesDispatcher_1) {
  325. this._showSeriesItemTooltip(e, seriesDispatcher_1, dispatchAction);
  326. } else if (cmptDispatcher_1) {
  327. this._showComponentItemTooltip(e, cmptDispatcher_1, dispatchAction);
  328. } else {
  329. this._hide(dispatchAction);
  330. }
  331. } else {
  332. this._lastDataByCoordSys = null;
  333. this._hide(dispatchAction);
  334. }
  335. };
  336. TooltipView.prototype._showOrMove = function (tooltipModel, cb) {
  337. // showDelay is used in this case: tooltip.enterable is set
  338. // as true. User intent to move mouse into tooltip and click
  339. // something. `showDelay` makes it easier to enter the content
  340. // but tooltip do not move immediately.
  341. var delay = tooltipModel.get('showDelay');
  342. cb = bind(cb, this);
  343. clearTimeout(this._showTimout);
  344. delay > 0 ? this._showTimout = setTimeout(cb, delay) : cb();
  345. };
  346. TooltipView.prototype._showAxisTooltip = function (dataByCoordSys, e) {
  347. var ecModel = this._ecModel;
  348. var globalTooltipModel = this._tooltipModel;
  349. var point = [e.offsetX, e.offsetY];
  350. var singleTooltipModel = buildTooltipModel([e.tooltipOption], globalTooltipModel);
  351. var renderMode = this._renderMode;
  352. var cbParamsList = [];
  353. var articleMarkup = createTooltipMarkup('section', {
  354. blocks: [],
  355. noHeader: true
  356. });
  357. // Only for legacy: `Serise['formatTooltip']` returns a string.
  358. var markupTextArrLegacy = [];
  359. var markupStyleCreator = new TooltipMarkupStyleCreator();
  360. each(dataByCoordSys, function (itemCoordSys) {
  361. each(itemCoordSys.dataByAxis, function (axisItem) {
  362. var axisModel = ecModel.getComponent(axisItem.axisDim + 'Axis', axisItem.axisIndex);
  363. var axisValue = axisItem.value;
  364. if (!axisModel || axisValue == null) {
  365. return;
  366. }
  367. // FIXME: when using `tooltip.trigger: 'axis'`, the precision of the axis value displayed in tooltip
  368. // should match the original series values rather than using the default stretegy in Interval.ts
  369. // (getPrecision(interval) + 2); otherwise it may cuase confusion.
  370. var axisValueLabel = axisPointerViewHelper.getValueLabel(axisValue, axisModel.axis, ecModel, axisItem.seriesDataIndices, axisItem.valueLabelOpt);
  371. var axisSectionMarkup = createTooltipMarkup('section', {
  372. header: axisValueLabel,
  373. noHeader: !trim(axisValueLabel),
  374. sortBlocks: true,
  375. blocks: []
  376. });
  377. articleMarkup.blocks.push(axisSectionMarkup);
  378. each(axisItem.seriesDataIndices, function (idxItem) {
  379. var series = ecModel.getSeriesByIndex(idxItem.seriesIndex);
  380. var dataIndex = idxItem.dataIndexInside;
  381. var cbParams = series.getDataParams(dataIndex);
  382. // Can't find data.
  383. if (cbParams.dataIndex < 0) {
  384. return;
  385. }
  386. cbParams.axisDim = axisItem.axisDim;
  387. cbParams.axisIndex = axisItem.axisIndex;
  388. cbParams.axisType = axisItem.axisType;
  389. cbParams.axisId = axisItem.axisId;
  390. cbParams.axisValue = axisHelper.getAxisRawValue(axisModel.axis, {
  391. value: axisValue
  392. });
  393. cbParams.axisValueLabel = axisValueLabel;
  394. // Pre-create marker style for makers. Users can assemble richText
  395. // text in `formatter` callback and use those markers style.
  396. cbParams.marker = markupStyleCreator.makeTooltipMarker('item', convertToColorString(cbParams.color), renderMode);
  397. var seriesTooltipResult = normalizeTooltipFormatResult(series.formatTooltip(dataIndex, true, null));
  398. var frag = seriesTooltipResult.frag;
  399. if (frag) {
  400. var valueFormatter = buildTooltipModel([series], globalTooltipModel).get('valueFormatter');
  401. axisSectionMarkup.blocks.push(valueFormatter ? extend({
  402. valueFormatter: valueFormatter
  403. }, frag) : frag);
  404. }
  405. if (seriesTooltipResult.text) {
  406. markupTextArrLegacy.push(seriesTooltipResult.text);
  407. }
  408. cbParamsList.push(cbParams);
  409. });
  410. });
  411. });
  412. // In most cases, the second axis is displays upper on the first one.
  413. // So we reverse it to look better.
  414. articleMarkup.blocks.reverse();
  415. markupTextArrLegacy.reverse();
  416. var positionExpr = e.position;
  417. var orderMode = singleTooltipModel.get('order');
  418. var builtMarkupText = buildTooltipMarkup(articleMarkup, markupStyleCreator, renderMode, orderMode, ecModel.get('useUTC'), singleTooltipModel.get('textStyle'));
  419. builtMarkupText && markupTextArrLegacy.unshift(builtMarkupText);
  420. var blockBreak = renderMode === 'richText' ? '\n\n' : '<br/>';
  421. var allMarkupText = markupTextArrLegacy.join(blockBreak);
  422. this._showOrMove(singleTooltipModel, function () {
  423. if (this._updateContentNotChangedOnAxis(dataByCoordSys, cbParamsList)) {
  424. this._updatePosition(singleTooltipModel, positionExpr, point[0], point[1], this._tooltipContent, cbParamsList);
  425. } else {
  426. this._showTooltipContent(singleTooltipModel, allMarkupText, cbParamsList, Math.random() + '', point[0], point[1], positionExpr, null, markupStyleCreator);
  427. }
  428. });
  429. // Do not trigger events here, because this branch only be entered
  430. // from dispatchAction.
  431. };
  432. TooltipView.prototype._showSeriesItemTooltip = function (e, dispatcher, dispatchAction) {
  433. var ecModel = this._ecModel;
  434. var ecData = getECData(dispatcher);
  435. // Use dataModel in element if possible
  436. // Used when mouseover on a element like markPoint or edge
  437. // In which case, the data is not main data in series.
  438. var seriesIndex = ecData.seriesIndex;
  439. var seriesModel = ecModel.getSeriesByIndex(seriesIndex);
  440. // For example, graph link.
  441. var dataModel = ecData.dataModel || seriesModel;
  442. var dataIndex = ecData.dataIndex;
  443. var dataType = ecData.dataType;
  444. var data = dataModel.getData(dataType);
  445. var renderMode = this._renderMode;
  446. var positionDefault = e.positionDefault;
  447. var tooltipModel = buildTooltipModel([data.getItemModel(dataIndex), dataModel, seriesModel && (seriesModel.coordinateSystem || {}).model], this._tooltipModel, positionDefault ? {
  448. position: positionDefault
  449. } : null);
  450. var tooltipTrigger = tooltipModel.get('trigger');
  451. if (tooltipTrigger != null && tooltipTrigger !== 'item') {
  452. return;
  453. }
  454. var params = dataModel.getDataParams(dataIndex, dataType);
  455. var markupStyleCreator = new TooltipMarkupStyleCreator();
  456. // Pre-create marker style for makers. Users can assemble richText
  457. // text in `formatter` callback and use those markers style.
  458. params.marker = markupStyleCreator.makeTooltipMarker('item', convertToColorString(params.color), renderMode);
  459. var seriesTooltipResult = normalizeTooltipFormatResult(dataModel.formatTooltip(dataIndex, false, dataType));
  460. var orderMode = tooltipModel.get('order');
  461. var valueFormatter = tooltipModel.get('valueFormatter');
  462. var frag = seriesTooltipResult.frag;
  463. var markupText = frag ? buildTooltipMarkup(valueFormatter ? extend({
  464. valueFormatter: valueFormatter
  465. }, frag) : frag, markupStyleCreator, renderMode, orderMode, ecModel.get('useUTC'), tooltipModel.get('textStyle')) : seriesTooltipResult.text;
  466. var asyncTicket = 'item_' + dataModel.name + '_' + dataIndex;
  467. this._showOrMove(tooltipModel, function () {
  468. this._showTooltipContent(tooltipModel, markupText, params, asyncTicket, e.offsetX, e.offsetY, e.position, e.target, markupStyleCreator);
  469. });
  470. // FIXME
  471. // duplicated showtip if manuallyShowTip is called from dispatchAction.
  472. dispatchAction({
  473. type: 'showTip',
  474. dataIndexInside: dataIndex,
  475. dataIndex: data.getRawIndex(dataIndex),
  476. seriesIndex: seriesIndex,
  477. from: this.uid
  478. });
  479. };
  480. TooltipView.prototype._showComponentItemTooltip = function (e, el, dispatchAction) {
  481. var isHTMLRenderMode = this._renderMode === 'html';
  482. var ecData = getECData(el);
  483. var tooltipConfig = ecData.tooltipConfig;
  484. var tooltipOpt = tooltipConfig.option || {};
  485. var encodeHTMLContent = tooltipOpt.encodeHTMLContent;
  486. if (isString(tooltipOpt)) {
  487. var content = tooltipOpt;
  488. tooltipOpt = {
  489. content: content,
  490. // Fixed formatter
  491. formatter: content
  492. };
  493. // when `tooltipConfig.option` is a string rather than an object,
  494. // we can't know if the content needs to be encoded
  495. // for the sake of security, encode it by default.
  496. encodeHTMLContent = true;
  497. }
  498. if (encodeHTMLContent && isHTMLRenderMode && tooltipOpt.content) {
  499. // clone might be unnecessary?
  500. tooltipOpt = clone(tooltipOpt);
  501. tooltipOpt.content = encodeHTML(tooltipOpt.content);
  502. }
  503. var tooltipModelCascade = [tooltipOpt];
  504. var cmpt = this._ecModel.getComponent(ecData.componentMainType, ecData.componentIndex);
  505. if (cmpt) {
  506. tooltipModelCascade.push(cmpt);
  507. }
  508. // In most cases, component tooltip formatter has different params with series tooltip formatter,
  509. // so that they cannot share the same formatter. Since the global tooltip formatter is used for series
  510. // by convention, we do not use it as the default formatter for component.
  511. tooltipModelCascade.push({
  512. formatter: tooltipOpt.content
  513. });
  514. var positionDefault = e.positionDefault;
  515. var subTooltipModel = buildTooltipModel(tooltipModelCascade, this._tooltipModel, positionDefault ? {
  516. position: positionDefault
  517. } : null);
  518. var defaultHtml = subTooltipModel.get('content');
  519. var asyncTicket = Math.random() + '';
  520. // PENDING: this case do not support richText style yet.
  521. var markupStyleCreator = new TooltipMarkupStyleCreator();
  522. // Do not check whether `trigger` is 'none' here, because `trigger`
  523. // only works on coordinate system. In fact, we have not found case
  524. // that requires setting `trigger` nothing on component yet.
  525. this._showOrMove(subTooltipModel, function () {
  526. // Use formatterParams from element defined in component
  527. // Avoid users modify it.
  528. var formatterParams = clone(subTooltipModel.get('formatterParams') || {});
  529. this._showTooltipContent(subTooltipModel, defaultHtml, formatterParams, asyncTicket, e.offsetX, e.offsetY, e.position, el, markupStyleCreator);
  530. });
  531. // If not dispatch showTip, tip may be hide triggered by axis.
  532. dispatchAction({
  533. type: 'showTip',
  534. from: this.uid
  535. });
  536. };
  537. TooltipView.prototype._showTooltipContent = function (
  538. // Use Model<TooltipOption> insteadof TooltipModel because this model may be from series or other options.
  539. // Instead of top level tooltip.
  540. tooltipModel, defaultHtml, params, asyncTicket, x, y, positionExpr, el, markupStyleCreator) {
  541. // Reset ticket
  542. this._ticket = '';
  543. if (!tooltipModel.get('showContent') || !tooltipModel.get('show')) {
  544. return;
  545. }
  546. var tooltipContent = this._tooltipContent;
  547. tooltipContent.setEnterable(tooltipModel.get('enterable'));
  548. var formatter = tooltipModel.get('formatter');
  549. positionExpr = positionExpr || tooltipModel.get('position');
  550. var html = defaultHtml;
  551. var nearPoint = this._getNearestPoint([x, y], params, tooltipModel.get('trigger'), tooltipModel.get('borderColor'), tooltipModel.get('defaultBorderColor', true));
  552. var nearPointColor = nearPoint.color;
  553. if (formatter) {
  554. if (isString(formatter)) {
  555. var useUTC = tooltipModel.ecModel.get('useUTC');
  556. var params0 = isArray(params) ? params[0] : params;
  557. var isTimeAxis = params0 && params0.axisType && params0.axisType.indexOf('time') >= 0;
  558. html = formatter;
  559. if (isTimeAxis) {
  560. html = timeFormat(params0.axisValue, html, useUTC);
  561. }
  562. html = formatTpl(html, params, true);
  563. } else if (isFunction(formatter)) {
  564. var callback = bind(function (cbTicket, html) {
  565. if (cbTicket === this._ticket) {
  566. tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPointColor, positionExpr);
  567. this._updatePosition(tooltipModel, positionExpr, x, y, tooltipContent, params, el);
  568. }
  569. }, this);
  570. this._ticket = asyncTicket;
  571. html = formatter(params, asyncTicket, callback);
  572. } else {
  573. html = formatter;
  574. }
  575. }
  576. tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPointColor, positionExpr);
  577. tooltipContent.show(tooltipModel, nearPointColor);
  578. this._updatePosition(tooltipModel, positionExpr, x, y, tooltipContent, params, el);
  579. };
  580. TooltipView.prototype._getNearestPoint = function (point, tooltipDataParams, trigger, borderColor, defaultBorderColor) {
  581. if (trigger === 'axis' || isArray(tooltipDataParams)) {
  582. return {
  583. color: borderColor || defaultBorderColor
  584. };
  585. }
  586. if (!isArray(tooltipDataParams)) {
  587. return {
  588. color: borderColor || tooltipDataParams.color || tooltipDataParams.borderColor
  589. };
  590. }
  591. };
  592. TooltipView.prototype._updatePosition = function (tooltipModel, positionExpr, x,
  593. // Mouse x
  594. y,
  595. // Mouse y
  596. content, params, el) {
  597. var viewWidth = this._api.getWidth();
  598. var viewHeight = this._api.getHeight();
  599. positionExpr = positionExpr || tooltipModel.get('position');
  600. var contentSize = content.getSize();
  601. var align = tooltipModel.get('align');
  602. var vAlign = tooltipModel.get('verticalAlign');
  603. var rect = el && el.getBoundingRect().clone();
  604. el && rect.applyTransform(el.transform);
  605. if (isFunction(positionExpr)) {
  606. // Callback of position can be an array or a string specify the position
  607. positionExpr = positionExpr([x, y], params, content.el, rect, {
  608. viewSize: [viewWidth, viewHeight],
  609. contentSize: contentSize.slice()
  610. });
  611. }
  612. if (isArray(positionExpr)) {
  613. x = parsePercent(positionExpr[0], viewWidth);
  614. y = parsePercent(positionExpr[1], viewHeight);
  615. } else if (isObject(positionExpr)) {
  616. var boxLayoutPosition = positionExpr;
  617. boxLayoutPosition.width = contentSize[0];
  618. boxLayoutPosition.height = contentSize[1];
  619. var layoutRect = getLayoutRect(boxLayoutPosition, {
  620. width: viewWidth,
  621. height: viewHeight
  622. });
  623. x = layoutRect.x;
  624. y = layoutRect.y;
  625. align = null;
  626. // When positionExpr is left/top/right/bottom,
  627. // align and verticalAlign will not work.
  628. vAlign = null;
  629. }
  630. // Specify tooltip position by string 'top' 'bottom' 'left' 'right' around graphic element
  631. else if (isString(positionExpr) && el) {
  632. var pos = calcTooltipPosition(positionExpr, rect, contentSize, tooltipModel.get('borderWidth'));
  633. x = pos[0];
  634. y = pos[1];
  635. } else {
  636. var pos = refixTooltipPosition(x, y, content, viewWidth, viewHeight, align ? null : 20, vAlign ? null : 20);
  637. x = pos[0];
  638. y = pos[1];
  639. }
  640. align && (x -= isCenterAlign(align) ? contentSize[0] / 2 : align === 'right' ? contentSize[0] : 0);
  641. vAlign && (y -= isCenterAlign(vAlign) ? contentSize[1] / 2 : vAlign === 'bottom' ? contentSize[1] : 0);
  642. if (shouldTooltipConfine(tooltipModel)) {
  643. var pos = confineTooltipPosition(x, y, content, viewWidth, viewHeight);
  644. x = pos[0];
  645. y = pos[1];
  646. }
  647. content.moveTo(x, y);
  648. };
  649. // FIXME
  650. // Should we remove this but leave this to user?
  651. TooltipView.prototype._updateContentNotChangedOnAxis = function (dataByCoordSys, cbParamsList) {
  652. var lastCoordSys = this._lastDataByCoordSys;
  653. var lastCbParamsList = this._cbParamsList;
  654. var contentNotChanged = !!lastCoordSys && lastCoordSys.length === dataByCoordSys.length;
  655. contentNotChanged && each(lastCoordSys, function (lastItemCoordSys, indexCoordSys) {
  656. var lastDataByAxis = lastItemCoordSys.dataByAxis || [];
  657. var thisItemCoordSys = dataByCoordSys[indexCoordSys] || {};
  658. var thisDataByAxis = thisItemCoordSys.dataByAxis || [];
  659. contentNotChanged = contentNotChanged && lastDataByAxis.length === thisDataByAxis.length;
  660. contentNotChanged && each(lastDataByAxis, function (lastItem, indexAxis) {
  661. var thisItem = thisDataByAxis[indexAxis] || {};
  662. var lastIndices = lastItem.seriesDataIndices || [];
  663. var newIndices = thisItem.seriesDataIndices || [];
  664. contentNotChanged = contentNotChanged && lastItem.value === thisItem.value && lastItem.axisType === thisItem.axisType && lastItem.axisId === thisItem.axisId && lastIndices.length === newIndices.length;
  665. contentNotChanged && each(lastIndices, function (lastIdxItem, j) {
  666. var newIdxItem = newIndices[j];
  667. contentNotChanged = contentNotChanged && lastIdxItem.seriesIndex === newIdxItem.seriesIndex && lastIdxItem.dataIndex === newIdxItem.dataIndex;
  668. });
  669. // check is cbParams data value changed
  670. lastCbParamsList && each(lastItem.seriesDataIndices, function (idxItem) {
  671. var seriesIdx = idxItem.seriesIndex;
  672. var cbParams = cbParamsList[seriesIdx];
  673. var lastCbParams = lastCbParamsList[seriesIdx];
  674. if (cbParams && lastCbParams && lastCbParams.data !== cbParams.data) {
  675. contentNotChanged = false;
  676. }
  677. });
  678. });
  679. });
  680. this._lastDataByCoordSys = dataByCoordSys;
  681. this._cbParamsList = cbParamsList;
  682. return !!contentNotChanged;
  683. };
  684. TooltipView.prototype._hide = function (dispatchAction) {
  685. // Do not directly hideLater here, because this behavior may be prevented
  686. // in dispatchAction when showTip is dispatched.
  687. // FIXME
  688. // duplicated hideTip if manuallyHideTip is called from dispatchAction.
  689. this._lastDataByCoordSys = null;
  690. dispatchAction({
  691. type: 'hideTip',
  692. from: this.uid
  693. });
  694. };
  695. TooltipView.prototype.dispose = function (ecModel, api) {
  696. if (env.node || !api.getDom()) {
  697. return;
  698. }
  699. clear(this, '_updatePosition');
  700. this._tooltipContent.dispose();
  701. globalListener.unregister('itemTooltip', api);
  702. };
  703. TooltipView.type = 'tooltip';
  704. return TooltipView;
  705. }(ComponentView);
  706. /**
  707. * From top to bottom. (the last one should be globalTooltipModel);
  708. */
  709. function buildTooltipModel(modelCascade, globalTooltipModel, defaultTooltipOption) {
  710. // Last is always tooltip model.
  711. var ecModel = globalTooltipModel.ecModel;
  712. var resultModel;
  713. if (defaultTooltipOption) {
  714. resultModel = new Model(defaultTooltipOption, ecModel, ecModel);
  715. resultModel = new Model(globalTooltipModel.option, resultModel, ecModel);
  716. } else {
  717. resultModel = globalTooltipModel;
  718. }
  719. for (var i = modelCascade.length - 1; i >= 0; i--) {
  720. var tooltipOpt = modelCascade[i];
  721. if (tooltipOpt) {
  722. if (tooltipOpt instanceof Model) {
  723. tooltipOpt = tooltipOpt.get('tooltip', true);
  724. }
  725. // In each data item tooltip can be simply write:
  726. // {
  727. // value: 10,
  728. // tooltip: 'Something you need to know'
  729. // }
  730. if (isString(tooltipOpt)) {
  731. tooltipOpt = {
  732. formatter: tooltipOpt
  733. };
  734. }
  735. if (tooltipOpt) {
  736. resultModel = new Model(tooltipOpt, resultModel, ecModel);
  737. }
  738. }
  739. }
  740. return resultModel;
  741. }
  742. function makeDispatchAction(payload, api) {
  743. return payload.dispatchAction || bind(api.dispatchAction, api);
  744. }
  745. function refixTooltipPosition(x, y, content, viewWidth, viewHeight, gapH, gapV) {
  746. var size = content.getSize();
  747. var width = size[0];
  748. var height = size[1];
  749. if (gapH != null) {
  750. // Add extra 2 pixels for this case:
  751. // At present the "values" in default tooltip are using CSS `float: right`.
  752. // When the right edge of the tooltip box is on the right side of the
  753. // viewport, the `float` layout might push the "values" to the second line.
  754. if (x + width + gapH + 2 > viewWidth) {
  755. x -= width + gapH;
  756. } else {
  757. x += gapH;
  758. }
  759. }
  760. if (gapV != null) {
  761. if (y + height + gapV > viewHeight) {
  762. y -= height + gapV;
  763. } else {
  764. y += gapV;
  765. }
  766. }
  767. return [x, y];
  768. }
  769. function confineTooltipPosition(x, y, content, viewWidth, viewHeight) {
  770. var size = content.getSize();
  771. var width = size[0];
  772. var height = size[1];
  773. x = Math.min(x + width, viewWidth) - width;
  774. y = Math.min(y + height, viewHeight) - height;
  775. x = Math.max(x, 0);
  776. y = Math.max(y, 0);
  777. return [x, y];
  778. }
  779. function calcTooltipPosition(position, rect, contentSize, borderWidth) {
  780. var domWidth = contentSize[0];
  781. var domHeight = contentSize[1];
  782. var offset = Math.ceil(Math.SQRT2 * borderWidth) + 8;
  783. var x = 0;
  784. var y = 0;
  785. var rectWidth = rect.width;
  786. var rectHeight = rect.height;
  787. switch (position) {
  788. case 'inside':
  789. x = rect.x + rectWidth / 2 - domWidth / 2;
  790. y = rect.y + rectHeight / 2 - domHeight / 2;
  791. break;
  792. case 'top':
  793. x = rect.x + rectWidth / 2 - domWidth / 2;
  794. y = rect.y - domHeight - offset;
  795. break;
  796. case 'bottom':
  797. x = rect.x + rectWidth / 2 - domWidth / 2;
  798. y = rect.y + rectHeight + offset;
  799. break;
  800. case 'left':
  801. x = rect.x - domWidth - offset;
  802. y = rect.y + rectHeight / 2 - domHeight / 2;
  803. break;
  804. case 'right':
  805. x = rect.x + rectWidth + offset;
  806. y = rect.y + rectHeight / 2 - domHeight / 2;
  807. }
  808. return [x, y];
  809. }
  810. function isCenterAlign(align) {
  811. return align === 'center' || align === 'middle';
  812. }
  813. /**
  814. * Find target component by payload like:
  815. * ```js
  816. * { legendId: 'some_id', name: 'xxx' }
  817. * { toolboxIndex: 1, name: 'xxx' }
  818. * { geoName: 'some_name', name: 'xxx' }
  819. * ```
  820. * PENDING: at present only
  821. *
  822. * If not found, return null/undefined.
  823. */
  824. function findComponentReference(payload, ecModel, api) {
  825. var queryOptionMap = preParseFinder(payload).queryOptionMap;
  826. var componentMainType = queryOptionMap.keys()[0];
  827. if (!componentMainType || componentMainType === 'series') {
  828. return;
  829. }
  830. var queryResult = queryReferringComponents(ecModel, componentMainType, queryOptionMap.get(componentMainType), {
  831. useDefault: false,
  832. enableAll: false,
  833. enableNone: false
  834. });
  835. var model = queryResult.models[0];
  836. if (!model) {
  837. return;
  838. }
  839. var view = api.getViewOfComponentModel(model);
  840. var el;
  841. view.group.traverse(function (subEl) {
  842. var tooltipConfig = getECData(subEl).tooltipConfig;
  843. if (tooltipConfig && tooltipConfig.name === payload.name) {
  844. el = subEl;
  845. return true; // stop
  846. }
  847. });
  848. if (el) {
  849. return {
  850. componentMainType: componentMainType,
  851. componentIndex: model.componentIndex,
  852. el: el
  853. };
  854. }
  855. }
  856. export default TooltipView;