TooltipView.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  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. // Always show item tooltip if mouse is on the element with dataIndex
  309. if (getECData(target).dataIndex != null) {
  310. seriesDispatcher_1 = target;
  311. return true;
  312. }
  313. // Tooltip provided directly. Like legend.
  314. if (getECData(target).tooltipConfig != null) {
  315. cmptDispatcher_1 = target;
  316. return true;
  317. }
  318. }, true);
  319. if (seriesDispatcher_1) {
  320. this._showSeriesItemTooltip(e, seriesDispatcher_1, dispatchAction);
  321. } else if (cmptDispatcher_1) {
  322. this._showComponentItemTooltip(e, cmptDispatcher_1, dispatchAction);
  323. } else {
  324. this._hide(dispatchAction);
  325. }
  326. } else {
  327. this._lastDataByCoordSys = null;
  328. this._hide(dispatchAction);
  329. }
  330. };
  331. TooltipView.prototype._showOrMove = function (tooltipModel, cb) {
  332. // showDelay is used in this case: tooltip.enterable is set
  333. // as true. User intent to move mouse into tooltip and click
  334. // something. `showDelay` makes it easier to enter the content
  335. // but tooltip do not move immediately.
  336. var delay = tooltipModel.get('showDelay');
  337. cb = bind(cb, this);
  338. clearTimeout(this._showTimout);
  339. delay > 0 ? this._showTimout = setTimeout(cb, delay) : cb();
  340. };
  341. TooltipView.prototype._showAxisTooltip = function (dataByCoordSys, e) {
  342. var ecModel = this._ecModel;
  343. var globalTooltipModel = this._tooltipModel;
  344. var point = [e.offsetX, e.offsetY];
  345. var singleTooltipModel = buildTooltipModel([e.tooltipOption], globalTooltipModel);
  346. var renderMode = this._renderMode;
  347. var cbParamsList = [];
  348. var articleMarkup = createTooltipMarkup('section', {
  349. blocks: [],
  350. noHeader: true
  351. });
  352. // Only for legacy: `Serise['formatTooltip']` returns a string.
  353. var markupTextArrLegacy = [];
  354. var markupStyleCreator = new TooltipMarkupStyleCreator();
  355. each(dataByCoordSys, function (itemCoordSys) {
  356. each(itemCoordSys.dataByAxis, function (axisItem) {
  357. var axisModel = ecModel.getComponent(axisItem.axisDim + 'Axis', axisItem.axisIndex);
  358. var axisValue = axisItem.value;
  359. if (!axisModel || axisValue == null) {
  360. return;
  361. }
  362. var axisValueLabel = axisPointerViewHelper.getValueLabel(axisValue, axisModel.axis, ecModel, axisItem.seriesDataIndices, axisItem.valueLabelOpt);
  363. var axisSectionMarkup = createTooltipMarkup('section', {
  364. header: axisValueLabel,
  365. noHeader: !trim(axisValueLabel),
  366. sortBlocks: true,
  367. blocks: []
  368. });
  369. articleMarkup.blocks.push(axisSectionMarkup);
  370. each(axisItem.seriesDataIndices, function (idxItem) {
  371. var series = ecModel.getSeriesByIndex(idxItem.seriesIndex);
  372. var dataIndex = idxItem.dataIndexInside;
  373. var cbParams = series.getDataParams(dataIndex);
  374. // Can't find data.
  375. if (cbParams.dataIndex < 0) {
  376. return;
  377. }
  378. cbParams.axisDim = axisItem.axisDim;
  379. cbParams.axisIndex = axisItem.axisIndex;
  380. cbParams.axisType = axisItem.axisType;
  381. cbParams.axisId = axisItem.axisId;
  382. cbParams.axisValue = axisHelper.getAxisRawValue(axisModel.axis, {
  383. value: axisValue
  384. });
  385. cbParams.axisValueLabel = axisValueLabel;
  386. // Pre-create marker style for makers. Users can assemble richText
  387. // text in `formatter` callback and use those markers style.
  388. cbParams.marker = markupStyleCreator.makeTooltipMarker('item', convertToColorString(cbParams.color), renderMode);
  389. var seriesTooltipResult = normalizeTooltipFormatResult(series.formatTooltip(dataIndex, true, null));
  390. var frag = seriesTooltipResult.frag;
  391. if (frag) {
  392. var valueFormatter = buildTooltipModel([series], globalTooltipModel).get('valueFormatter');
  393. axisSectionMarkup.blocks.push(valueFormatter ? extend({
  394. valueFormatter: valueFormatter
  395. }, frag) : frag);
  396. }
  397. if (seriesTooltipResult.text) {
  398. markupTextArrLegacy.push(seriesTooltipResult.text);
  399. }
  400. cbParamsList.push(cbParams);
  401. });
  402. });
  403. });
  404. // In most cases, the second axis is displays upper on the first one.
  405. // So we reverse it to look better.
  406. articleMarkup.blocks.reverse();
  407. markupTextArrLegacy.reverse();
  408. var positionExpr = e.position;
  409. var orderMode = singleTooltipModel.get('order');
  410. var builtMarkupText = buildTooltipMarkup(articleMarkup, markupStyleCreator, renderMode, orderMode, ecModel.get('useUTC'), singleTooltipModel.get('textStyle'));
  411. builtMarkupText && markupTextArrLegacy.unshift(builtMarkupText);
  412. var blockBreak = renderMode === 'richText' ? '\n\n' : '<br/>';
  413. var allMarkupText = markupTextArrLegacy.join(blockBreak);
  414. this._showOrMove(singleTooltipModel, function () {
  415. if (this._updateContentNotChangedOnAxis(dataByCoordSys, cbParamsList)) {
  416. this._updatePosition(singleTooltipModel, positionExpr, point[0], point[1], this._tooltipContent, cbParamsList);
  417. } else {
  418. this._showTooltipContent(singleTooltipModel, allMarkupText, cbParamsList, Math.random() + '', point[0], point[1], positionExpr, null, markupStyleCreator);
  419. }
  420. });
  421. // Do not trigger events here, because this branch only be entered
  422. // from dispatchAction.
  423. };
  424. TooltipView.prototype._showSeriesItemTooltip = function (e, dispatcher, dispatchAction) {
  425. var ecModel = this._ecModel;
  426. var ecData = getECData(dispatcher);
  427. // Use dataModel in element if possible
  428. // Used when mouseover on a element like markPoint or edge
  429. // In which case, the data is not main data in series.
  430. var seriesIndex = ecData.seriesIndex;
  431. var seriesModel = ecModel.getSeriesByIndex(seriesIndex);
  432. // For example, graph link.
  433. var dataModel = ecData.dataModel || seriesModel;
  434. var dataIndex = ecData.dataIndex;
  435. var dataType = ecData.dataType;
  436. var data = dataModel.getData(dataType);
  437. var renderMode = this._renderMode;
  438. var positionDefault = e.positionDefault;
  439. var tooltipModel = buildTooltipModel([data.getItemModel(dataIndex), dataModel, seriesModel && (seriesModel.coordinateSystem || {}).model], this._tooltipModel, positionDefault ? {
  440. position: positionDefault
  441. } : null);
  442. var tooltipTrigger = tooltipModel.get('trigger');
  443. if (tooltipTrigger != null && tooltipTrigger !== 'item') {
  444. return;
  445. }
  446. var params = dataModel.getDataParams(dataIndex, dataType);
  447. var markupStyleCreator = new TooltipMarkupStyleCreator();
  448. // Pre-create marker style for makers. Users can assemble richText
  449. // text in `formatter` callback and use those markers style.
  450. params.marker = markupStyleCreator.makeTooltipMarker('item', convertToColorString(params.color), renderMode);
  451. var seriesTooltipResult = normalizeTooltipFormatResult(dataModel.formatTooltip(dataIndex, false, dataType));
  452. var orderMode = tooltipModel.get('order');
  453. var valueFormatter = tooltipModel.get('valueFormatter');
  454. var frag = seriesTooltipResult.frag;
  455. var markupText = frag ? buildTooltipMarkup(valueFormatter ? extend({
  456. valueFormatter: valueFormatter
  457. }, frag) : frag, markupStyleCreator, renderMode, orderMode, ecModel.get('useUTC'), tooltipModel.get('textStyle')) : seriesTooltipResult.text;
  458. var asyncTicket = 'item_' + dataModel.name + '_' + dataIndex;
  459. this._showOrMove(tooltipModel, function () {
  460. this._showTooltipContent(tooltipModel, markupText, params, asyncTicket, e.offsetX, e.offsetY, e.position, e.target, markupStyleCreator);
  461. });
  462. // FIXME
  463. // duplicated showtip if manuallyShowTip is called from dispatchAction.
  464. dispatchAction({
  465. type: 'showTip',
  466. dataIndexInside: dataIndex,
  467. dataIndex: data.getRawIndex(dataIndex),
  468. seriesIndex: seriesIndex,
  469. from: this.uid
  470. });
  471. };
  472. TooltipView.prototype._showComponentItemTooltip = function (e, el, dispatchAction) {
  473. var isHTMLRenderMode = this._renderMode === 'html';
  474. var ecData = getECData(el);
  475. var tooltipConfig = ecData.tooltipConfig;
  476. var tooltipOpt = tooltipConfig.option || {};
  477. var encodeHTMLContent = tooltipOpt.encodeHTMLContent;
  478. if (isString(tooltipOpt)) {
  479. var content = tooltipOpt;
  480. tooltipOpt = {
  481. content: content,
  482. // Fixed formatter
  483. formatter: content
  484. };
  485. // when `tooltipConfig.option` is a string rather than an object,
  486. // we can't know if the content needs to be encoded
  487. // for the sake of security, encode it by default.
  488. encodeHTMLContent = true;
  489. }
  490. if (encodeHTMLContent && isHTMLRenderMode && tooltipOpt.content) {
  491. // clone might be unnecessary?
  492. tooltipOpt = clone(tooltipOpt);
  493. tooltipOpt.content = encodeHTML(tooltipOpt.content);
  494. }
  495. var tooltipModelCascade = [tooltipOpt];
  496. var cmpt = this._ecModel.getComponent(ecData.componentMainType, ecData.componentIndex);
  497. if (cmpt) {
  498. tooltipModelCascade.push(cmpt);
  499. }
  500. // In most cases, component tooltip formatter has different params with series tooltip formatter,
  501. // so that they cannot share the same formatter. Since the global tooltip formatter is used for series
  502. // by convention, we do not use it as the default formatter for component.
  503. tooltipModelCascade.push({
  504. formatter: tooltipOpt.content
  505. });
  506. var positionDefault = e.positionDefault;
  507. var subTooltipModel = buildTooltipModel(tooltipModelCascade, this._tooltipModel, positionDefault ? {
  508. position: positionDefault
  509. } : null);
  510. var defaultHtml = subTooltipModel.get('content');
  511. var asyncTicket = Math.random() + '';
  512. // PENDING: this case do not support richText style yet.
  513. var markupStyleCreator = new TooltipMarkupStyleCreator();
  514. // Do not check whether `trigger` is 'none' here, because `trigger`
  515. // only works on coordinate system. In fact, we have not found case
  516. // that requires setting `trigger` nothing on component yet.
  517. this._showOrMove(subTooltipModel, function () {
  518. // Use formatterParams from element defined in component
  519. // Avoid users modify it.
  520. var formatterParams = clone(subTooltipModel.get('formatterParams') || {});
  521. this._showTooltipContent(subTooltipModel, defaultHtml, formatterParams, asyncTicket, e.offsetX, e.offsetY, e.position, el, markupStyleCreator);
  522. });
  523. // If not dispatch showTip, tip may be hide triggered by axis.
  524. dispatchAction({
  525. type: 'showTip',
  526. from: this.uid
  527. });
  528. };
  529. TooltipView.prototype._showTooltipContent = function (
  530. // Use Model<TooltipOption> insteadof TooltipModel because this model may be from series or other options.
  531. // Instead of top level tooltip.
  532. tooltipModel, defaultHtml, params, asyncTicket, x, y, positionExpr, el, markupStyleCreator) {
  533. // Reset ticket
  534. this._ticket = '';
  535. if (!tooltipModel.get('showContent') || !tooltipModel.get('show')) {
  536. return;
  537. }
  538. var tooltipContent = this._tooltipContent;
  539. tooltipContent.setEnterable(tooltipModel.get('enterable'));
  540. var formatter = tooltipModel.get('formatter');
  541. positionExpr = positionExpr || tooltipModel.get('position');
  542. var html = defaultHtml;
  543. var nearPoint = this._getNearestPoint([x, y], params, tooltipModel.get('trigger'), tooltipModel.get('borderColor'));
  544. var nearPointColor = nearPoint.color;
  545. if (formatter) {
  546. if (isString(formatter)) {
  547. var useUTC = tooltipModel.ecModel.get('useUTC');
  548. var params0 = isArray(params) ? params[0] : params;
  549. var isTimeAxis = params0 && params0.axisType && params0.axisType.indexOf('time') >= 0;
  550. html = formatter;
  551. if (isTimeAxis) {
  552. html = timeFormat(params0.axisValue, html, useUTC);
  553. }
  554. html = formatTpl(html, params, true);
  555. } else if (isFunction(formatter)) {
  556. var callback = bind(function (cbTicket, html) {
  557. if (cbTicket === this._ticket) {
  558. tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPointColor, positionExpr);
  559. this._updatePosition(tooltipModel, positionExpr, x, y, tooltipContent, params, el);
  560. }
  561. }, this);
  562. this._ticket = asyncTicket;
  563. html = formatter(params, asyncTicket, callback);
  564. } else {
  565. html = formatter;
  566. }
  567. }
  568. tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPointColor, positionExpr);
  569. tooltipContent.show(tooltipModel, nearPointColor);
  570. this._updatePosition(tooltipModel, positionExpr, x, y, tooltipContent, params, el);
  571. };
  572. TooltipView.prototype._getNearestPoint = function (point, tooltipDataParams, trigger, borderColor) {
  573. if (trigger === 'axis' || isArray(tooltipDataParams)) {
  574. return {
  575. color: borderColor || (this._renderMode === 'html' ? '#fff' : 'none')
  576. };
  577. }
  578. if (!isArray(tooltipDataParams)) {
  579. return {
  580. color: borderColor || tooltipDataParams.color || tooltipDataParams.borderColor
  581. };
  582. }
  583. };
  584. TooltipView.prototype._updatePosition = function (tooltipModel, positionExpr, x,
  585. // Mouse x
  586. y,
  587. // Mouse y
  588. content, params, el) {
  589. var viewWidth = this._api.getWidth();
  590. var viewHeight = this._api.getHeight();
  591. positionExpr = positionExpr || tooltipModel.get('position');
  592. var contentSize = content.getSize();
  593. var align = tooltipModel.get('align');
  594. var vAlign = tooltipModel.get('verticalAlign');
  595. var rect = el && el.getBoundingRect().clone();
  596. el && rect.applyTransform(el.transform);
  597. if (isFunction(positionExpr)) {
  598. // Callback of position can be an array or a string specify the position
  599. positionExpr = positionExpr([x, y], params, content.el, rect, {
  600. viewSize: [viewWidth, viewHeight],
  601. contentSize: contentSize.slice()
  602. });
  603. }
  604. if (isArray(positionExpr)) {
  605. x = parsePercent(positionExpr[0], viewWidth);
  606. y = parsePercent(positionExpr[1], viewHeight);
  607. } else if (isObject(positionExpr)) {
  608. var boxLayoutPosition = positionExpr;
  609. boxLayoutPosition.width = contentSize[0];
  610. boxLayoutPosition.height = contentSize[1];
  611. var layoutRect = getLayoutRect(boxLayoutPosition, {
  612. width: viewWidth,
  613. height: viewHeight
  614. });
  615. x = layoutRect.x;
  616. y = layoutRect.y;
  617. align = null;
  618. // When positionExpr is left/top/right/bottom,
  619. // align and verticalAlign will not work.
  620. vAlign = null;
  621. }
  622. // Specify tooltip position by string 'top' 'bottom' 'left' 'right' around graphic element
  623. else if (isString(positionExpr) && el) {
  624. var pos = calcTooltipPosition(positionExpr, rect, contentSize, tooltipModel.get('borderWidth'));
  625. x = pos[0];
  626. y = pos[1];
  627. } else {
  628. var pos = refixTooltipPosition(x, y, content, viewWidth, viewHeight, align ? null : 20, vAlign ? null : 20);
  629. x = pos[0];
  630. y = pos[1];
  631. }
  632. align && (x -= isCenterAlign(align) ? contentSize[0] / 2 : align === 'right' ? contentSize[0] : 0);
  633. vAlign && (y -= isCenterAlign(vAlign) ? contentSize[1] / 2 : vAlign === 'bottom' ? contentSize[1] : 0);
  634. if (shouldTooltipConfine(tooltipModel)) {
  635. var pos = confineTooltipPosition(x, y, content, viewWidth, viewHeight);
  636. x = pos[0];
  637. y = pos[1];
  638. }
  639. content.moveTo(x, y);
  640. };
  641. // FIXME
  642. // Should we remove this but leave this to user?
  643. TooltipView.prototype._updateContentNotChangedOnAxis = function (dataByCoordSys, cbParamsList) {
  644. var lastCoordSys = this._lastDataByCoordSys;
  645. var lastCbParamsList = this._cbParamsList;
  646. var contentNotChanged = !!lastCoordSys && lastCoordSys.length === dataByCoordSys.length;
  647. contentNotChanged && each(lastCoordSys, function (lastItemCoordSys, indexCoordSys) {
  648. var lastDataByAxis = lastItemCoordSys.dataByAxis || [];
  649. var thisItemCoordSys = dataByCoordSys[indexCoordSys] || {};
  650. var thisDataByAxis = thisItemCoordSys.dataByAxis || [];
  651. contentNotChanged = contentNotChanged && lastDataByAxis.length === thisDataByAxis.length;
  652. contentNotChanged && each(lastDataByAxis, function (lastItem, indexAxis) {
  653. var thisItem = thisDataByAxis[indexAxis] || {};
  654. var lastIndices = lastItem.seriesDataIndices || [];
  655. var newIndices = thisItem.seriesDataIndices || [];
  656. contentNotChanged = contentNotChanged && lastItem.value === thisItem.value && lastItem.axisType === thisItem.axisType && lastItem.axisId === thisItem.axisId && lastIndices.length === newIndices.length;
  657. contentNotChanged && each(lastIndices, function (lastIdxItem, j) {
  658. var newIdxItem = newIndices[j];
  659. contentNotChanged = contentNotChanged && lastIdxItem.seriesIndex === newIdxItem.seriesIndex && lastIdxItem.dataIndex === newIdxItem.dataIndex;
  660. });
  661. // check is cbParams data value changed
  662. lastCbParamsList && each(lastItem.seriesDataIndices, function (idxItem) {
  663. var seriesIdx = idxItem.seriesIndex;
  664. var cbParams = cbParamsList[seriesIdx];
  665. var lastCbParams = lastCbParamsList[seriesIdx];
  666. if (cbParams && lastCbParams && lastCbParams.data !== cbParams.data) {
  667. contentNotChanged = false;
  668. }
  669. });
  670. });
  671. });
  672. this._lastDataByCoordSys = dataByCoordSys;
  673. this._cbParamsList = cbParamsList;
  674. return !!contentNotChanged;
  675. };
  676. TooltipView.prototype._hide = function (dispatchAction) {
  677. // Do not directly hideLater here, because this behavior may be prevented
  678. // in dispatchAction when showTip is dispatched.
  679. // FIXME
  680. // duplicated hideTip if manuallyHideTip is called from dispatchAction.
  681. this._lastDataByCoordSys = null;
  682. dispatchAction({
  683. type: 'hideTip',
  684. from: this.uid
  685. });
  686. };
  687. TooltipView.prototype.dispose = function (ecModel, api) {
  688. if (env.node || !api.getDom()) {
  689. return;
  690. }
  691. clear(this, '_updatePosition');
  692. this._tooltipContent.dispose();
  693. globalListener.unregister('itemTooltip', api);
  694. };
  695. TooltipView.type = 'tooltip';
  696. return TooltipView;
  697. }(ComponentView);
  698. /**
  699. * From top to bottom. (the last one should be globalTooltipModel);
  700. */
  701. function buildTooltipModel(modelCascade, globalTooltipModel, defaultTooltipOption) {
  702. // Last is always tooltip model.
  703. var ecModel = globalTooltipModel.ecModel;
  704. var resultModel;
  705. if (defaultTooltipOption) {
  706. resultModel = new Model(defaultTooltipOption, ecModel, ecModel);
  707. resultModel = new Model(globalTooltipModel.option, resultModel, ecModel);
  708. } else {
  709. resultModel = globalTooltipModel;
  710. }
  711. for (var i = modelCascade.length - 1; i >= 0; i--) {
  712. var tooltipOpt = modelCascade[i];
  713. if (tooltipOpt) {
  714. if (tooltipOpt instanceof Model) {
  715. tooltipOpt = tooltipOpt.get('tooltip', true);
  716. }
  717. // In each data item tooltip can be simply write:
  718. // {
  719. // value: 10,
  720. // tooltip: 'Something you need to know'
  721. // }
  722. if (isString(tooltipOpt)) {
  723. tooltipOpt = {
  724. formatter: tooltipOpt
  725. };
  726. }
  727. if (tooltipOpt) {
  728. resultModel = new Model(tooltipOpt, resultModel, ecModel);
  729. }
  730. }
  731. }
  732. return resultModel;
  733. }
  734. function makeDispatchAction(payload, api) {
  735. return payload.dispatchAction || bind(api.dispatchAction, api);
  736. }
  737. function refixTooltipPosition(x, y, content, viewWidth, viewHeight, gapH, gapV) {
  738. var size = content.getSize();
  739. var width = size[0];
  740. var height = size[1];
  741. if (gapH != null) {
  742. // Add extra 2 pixels for this case:
  743. // At present the "values" in default tooltip are using CSS `float: right`.
  744. // When the right edge of the tooltip box is on the right side of the
  745. // viewport, the `float` layout might push the "values" to the second line.
  746. if (x + width + gapH + 2 > viewWidth) {
  747. x -= width + gapH;
  748. } else {
  749. x += gapH;
  750. }
  751. }
  752. if (gapV != null) {
  753. if (y + height + gapV > viewHeight) {
  754. y -= height + gapV;
  755. } else {
  756. y += gapV;
  757. }
  758. }
  759. return [x, y];
  760. }
  761. function confineTooltipPosition(x, y, content, viewWidth, viewHeight) {
  762. var size = content.getSize();
  763. var width = size[0];
  764. var height = size[1];
  765. x = Math.min(x + width, viewWidth) - width;
  766. y = Math.min(y + height, viewHeight) - height;
  767. x = Math.max(x, 0);
  768. y = Math.max(y, 0);
  769. return [x, y];
  770. }
  771. function calcTooltipPosition(position, rect, contentSize, borderWidth) {
  772. var domWidth = contentSize[0];
  773. var domHeight = contentSize[1];
  774. var offset = Math.ceil(Math.SQRT2 * borderWidth) + 8;
  775. var x = 0;
  776. var y = 0;
  777. var rectWidth = rect.width;
  778. var rectHeight = rect.height;
  779. switch (position) {
  780. case 'inside':
  781. x = rect.x + rectWidth / 2 - domWidth / 2;
  782. y = rect.y + rectHeight / 2 - domHeight / 2;
  783. break;
  784. case 'top':
  785. x = rect.x + rectWidth / 2 - domWidth / 2;
  786. y = rect.y - domHeight - offset;
  787. break;
  788. case 'bottom':
  789. x = rect.x + rectWidth / 2 - domWidth / 2;
  790. y = rect.y + rectHeight + offset;
  791. break;
  792. case 'left':
  793. x = rect.x - domWidth - offset;
  794. y = rect.y + rectHeight / 2 - domHeight / 2;
  795. break;
  796. case 'right':
  797. x = rect.x + rectWidth + offset;
  798. y = rect.y + rectHeight / 2 - domHeight / 2;
  799. }
  800. return [x, y];
  801. }
  802. function isCenterAlign(align) {
  803. return align === 'center' || align === 'middle';
  804. }
  805. /**
  806. * Find target component by payload like:
  807. * ```js
  808. * { legendId: 'some_id', name: 'xxx' }
  809. * { toolboxIndex: 1, name: 'xxx' }
  810. * { geoName: 'some_name', name: 'xxx' }
  811. * ```
  812. * PENDING: at present only
  813. *
  814. * If not found, return null/undefined.
  815. */
  816. function findComponentReference(payload, ecModel, api) {
  817. var queryOptionMap = preParseFinder(payload).queryOptionMap;
  818. var componentMainType = queryOptionMap.keys()[0];
  819. if (!componentMainType || componentMainType === 'series') {
  820. return;
  821. }
  822. var queryResult = queryReferringComponents(ecModel, componentMainType, queryOptionMap.get(componentMainType), {
  823. useDefault: false,
  824. enableAll: false,
  825. enableNone: false
  826. });
  827. var model = queryResult.models[0];
  828. if (!model) {
  829. return;
  830. }
  831. var view = api.getViewOfComponentModel(model);
  832. var el;
  833. view.group.traverse(function (subEl) {
  834. var tooltipConfig = getECData(subEl).tooltipConfig;
  835. if (tooltipConfig && tooltipConfig.name === payload.name) {
  836. el = subEl;
  837. return true; // stop
  838. }
  839. });
  840. if (el) {
  841. return {
  842. componentMainType: componentMainType,
  843. componentIndex: model.componentIndex,
  844. el: el
  845. };
  846. }
  847. }
  848. export default TooltipView;