axisTrigger.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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. var zrUtil = require("zrender/lib/core/util");
  20. var _model = require("../../util/model");
  21. var makeInner = _model.makeInner;
  22. var modelHelper = require("./modelHelper");
  23. var findPointFromSeries = require("./findPointFromSeries");
  24. /*
  25. * Licensed to the Apache Software Foundation (ASF) under one
  26. * or more contributor license agreements. See the NOTICE file
  27. * distributed with this work for additional information
  28. * regarding copyright ownership. The ASF licenses this file
  29. * to you under the Apache License, Version 2.0 (the
  30. * "License"); you may not use this file except in compliance
  31. * with the License. You may obtain a copy of the License at
  32. *
  33. * http://www.apache.org/licenses/LICENSE-2.0
  34. *
  35. * Unless required by applicable law or agreed to in writing,
  36. * software distributed under the License is distributed on an
  37. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  38. * KIND, either express or implied. See the License for the
  39. * specific language governing permissions and limitations
  40. * under the License.
  41. */
  42. var each = zrUtil.each;
  43. var curry = zrUtil.curry;
  44. var inner = makeInner();
  45. /**
  46. * Basic logic: check all axis, if they do not demand show/highlight,
  47. * then hide/downplay them.
  48. *
  49. * @param {Object} coordSysAxesInfo
  50. * @param {Object} payload
  51. * @param {string} [payload.currTrigger] 'click' | 'mousemove' | 'leave'
  52. * @param {Array.<number>} [payload.x] x and y, which are mandatory, specify a point to
  53. * trigger axisPointer and tooltip.
  54. * @param {Array.<number>} [payload.y] x and y, which are mandatory, specify a point to
  55. * trigger axisPointer and tooltip.
  56. * @param {Object} [payload.seriesIndex] finder, optional, restrict target axes.
  57. * @param {Object} [payload.dataIndex] finder, restrict target axes.
  58. * @param {Object} [payload.axesInfo] finder, restrict target axes.
  59. * [{
  60. * axisDim: 'x'|'y'|'angle'|...,
  61. * axisIndex: ...,
  62. * value: ...
  63. * }, ...]
  64. * @param {Function} [payload.dispatchAction]
  65. * @param {Object} [payload.tooltipOption]
  66. * @param {Object|Array.<number>|Function} [payload.position] Tooltip position,
  67. * which can be specified in dispatchAction
  68. * @param {module:echarts/model/Global} ecModel
  69. * @param {module:echarts/ExtensionAPI} api
  70. * @return {Object} content of event obj for echarts.connect.
  71. */
  72. function _default(payload, ecModel, api) {
  73. var currTrigger = payload.currTrigger;
  74. var point = [payload.x, payload.y];
  75. var finder = payload;
  76. var dispatchAction = payload.dispatchAction || zrUtil.bind(api.dispatchAction, api);
  77. var coordSysAxesInfo = ecModel.getComponent('axisPointer').coordSysAxesInfo; // Pending
  78. // See #6121. But we are not able to reproduce it yet.
  79. if (!coordSysAxesInfo) {
  80. return;
  81. }
  82. if (illegalPoint(point)) {
  83. // Used in the default behavior of `connection`: use the sample seriesIndex
  84. // and dataIndex. And also used in the tooltipView trigger.
  85. point = findPointFromSeries({
  86. seriesIndex: finder.seriesIndex,
  87. // Do not use dataIndexInside from other ec instance.
  88. // FIXME: auto detect it?
  89. dataIndex: finder.dataIndex
  90. }, ecModel).point;
  91. }
  92. var isIllegalPoint = illegalPoint(point); // Axis and value can be specified when calling dispatchAction({type: 'updateAxisPointer'}).
  93. // Notice: In this case, it is difficult to get the `point` (which is necessary to show
  94. // tooltip, so if point is not given, we just use the point found by sample seriesIndex
  95. // and dataIndex.
  96. var inputAxesInfo = finder.axesInfo;
  97. var axesInfo = coordSysAxesInfo.axesInfo;
  98. var shouldHide = currTrigger === 'leave' || illegalPoint(point);
  99. var outputFinder = {};
  100. var showValueMap = {};
  101. var dataByCoordSys = {
  102. list: [],
  103. map: {}
  104. };
  105. var updaters = {
  106. showPointer: curry(showPointer, showValueMap),
  107. showTooltip: curry(showTooltip, dataByCoordSys)
  108. }; // Process for triggered axes.
  109. each(coordSysAxesInfo.coordSysMap, function (coordSys, coordSysKey) {
  110. // If a point given, it must be contained by the coordinate system.
  111. var coordSysContainsPoint = isIllegalPoint || coordSys.containPoint(point);
  112. each(coordSysAxesInfo.coordSysAxesInfo[coordSysKey], function (axisInfo, key) {
  113. var axis = axisInfo.axis;
  114. var inputAxisInfo = findInputAxisInfo(inputAxesInfo, axisInfo); // If no inputAxesInfo, no axis is restricted.
  115. if (!shouldHide && coordSysContainsPoint && (!inputAxesInfo || inputAxisInfo)) {
  116. var val = inputAxisInfo && inputAxisInfo.value;
  117. if (val == null && !isIllegalPoint) {
  118. val = axis.pointToData(point);
  119. }
  120. val != null && processOnAxis(axisInfo, val, updaters, false, outputFinder);
  121. }
  122. });
  123. }); // Process for linked axes.
  124. var linkTriggers = {};
  125. each(axesInfo, function (tarAxisInfo, tarKey) {
  126. var linkGroup = tarAxisInfo.linkGroup; // If axis has been triggered in the previous stage, it should not be triggered by link.
  127. if (linkGroup && !showValueMap[tarKey]) {
  128. each(linkGroup.axesInfo, function (srcAxisInfo, srcKey) {
  129. var srcValItem = showValueMap[srcKey]; // If srcValItem exist, source axis is triggered, so link to target axis.
  130. if (srcAxisInfo !== tarAxisInfo && srcValItem) {
  131. var val = srcValItem.value;
  132. linkGroup.mapper && (val = tarAxisInfo.axis.scale.parse(linkGroup.mapper(val, makeMapperParam(srcAxisInfo), makeMapperParam(tarAxisInfo))));
  133. linkTriggers[tarAxisInfo.key] = val;
  134. }
  135. });
  136. }
  137. });
  138. each(linkTriggers, function (val, tarKey) {
  139. processOnAxis(axesInfo[tarKey], val, updaters, true, outputFinder);
  140. });
  141. updateModelActually(showValueMap, axesInfo, outputFinder);
  142. dispatchTooltipActually(dataByCoordSys, point, payload, dispatchAction);
  143. dispatchHighDownActually(axesInfo, dispatchAction, api);
  144. return outputFinder;
  145. }
  146. function processOnAxis(axisInfo, newValue, updaters, dontSnap, outputFinder) {
  147. var axis = axisInfo.axis;
  148. if (axis.scale.isBlank() || !axis.containData(newValue)) {
  149. return;
  150. }
  151. if (!axisInfo.involveSeries) {
  152. updaters.showPointer(axisInfo, newValue);
  153. return;
  154. } // Heavy calculation. So put it after axis.containData checking.
  155. var payloadInfo = buildPayloadsBySeries(newValue, axisInfo);
  156. var payloadBatch = payloadInfo.payloadBatch;
  157. var snapToValue = payloadInfo.snapToValue; // Fill content of event obj for echarts.connect.
  158. // By default use the first involved series data as a sample to connect.
  159. if (payloadBatch[0] && outputFinder.seriesIndex == null) {
  160. zrUtil.extend(outputFinder, payloadBatch[0]);
  161. } // If no linkSource input, this process is for collecting link
  162. // target, where snap should not be accepted.
  163. if (!dontSnap && axisInfo.snap) {
  164. if (axis.containData(snapToValue) && snapToValue != null) {
  165. newValue = snapToValue;
  166. }
  167. }
  168. updaters.showPointer(axisInfo, newValue, payloadBatch, outputFinder); // Tooltip should always be snapToValue, otherwise there will be
  169. // incorrect "axis value ~ series value" mapping displayed in tooltip.
  170. updaters.showTooltip(axisInfo, payloadInfo, snapToValue);
  171. }
  172. function buildPayloadsBySeries(value, axisInfo) {
  173. var axis = axisInfo.axis;
  174. var dim = axis.dim;
  175. var snapToValue = value;
  176. var payloadBatch = [];
  177. var minDist = Number.MAX_VALUE;
  178. var minDiff = -1;
  179. each(axisInfo.seriesModels, function (series, idx) {
  180. var dataDim = series.getData().mapDimension(dim, true);
  181. var seriesNestestValue;
  182. var dataIndices;
  183. if (series.getAxisTooltipData) {
  184. var result = series.getAxisTooltipData(dataDim, value, axis);
  185. dataIndices = result.dataIndices;
  186. seriesNestestValue = result.nestestValue;
  187. } else {
  188. dataIndices = series.getData().indicesOfNearest(dataDim[0], value, // Add a threshold to avoid find the wrong dataIndex
  189. // when data length is not same.
  190. // false,
  191. axis.type === 'category' ? 0.5 : null);
  192. if (!dataIndices.length) {
  193. return;
  194. }
  195. seriesNestestValue = series.getData().get(dataDim[0], dataIndices[0]);
  196. }
  197. if (seriesNestestValue == null || !isFinite(seriesNestestValue)) {
  198. return;
  199. }
  200. var diff = value - seriesNestestValue;
  201. var dist = Math.abs(diff); // Consider category case
  202. if (dist <= minDist) {
  203. if (dist < minDist || diff >= 0 && minDiff < 0) {
  204. minDist = dist;
  205. minDiff = diff;
  206. snapToValue = seriesNestestValue;
  207. payloadBatch.length = 0;
  208. }
  209. each(dataIndices, function (dataIndex) {
  210. payloadBatch.push({
  211. seriesIndex: series.seriesIndex,
  212. dataIndexInside: dataIndex,
  213. dataIndex: series.getData().getRawIndex(dataIndex)
  214. });
  215. });
  216. }
  217. });
  218. return {
  219. payloadBatch: payloadBatch,
  220. snapToValue: snapToValue
  221. };
  222. }
  223. function showPointer(showValueMap, axisInfo, value, payloadBatch) {
  224. showValueMap[axisInfo.key] = {
  225. value: value,
  226. payloadBatch: payloadBatch
  227. };
  228. }
  229. function showTooltip(dataByCoordSys, axisInfo, payloadInfo, value) {
  230. var payloadBatch = payloadInfo.payloadBatch;
  231. var axis = axisInfo.axis;
  232. var axisModel = axis.model;
  233. var axisPointerModel = axisInfo.axisPointerModel; // If no data, do not create anything in dataByCoordSys,
  234. // whose length will be used to judge whether dispatch action.
  235. if (!axisInfo.triggerTooltip || !payloadBatch.length) {
  236. return;
  237. }
  238. var coordSysModel = axisInfo.coordSys.model;
  239. var coordSysKey = modelHelper.makeKey(coordSysModel);
  240. var coordSysItem = dataByCoordSys.map[coordSysKey];
  241. if (!coordSysItem) {
  242. coordSysItem = dataByCoordSys.map[coordSysKey] = {
  243. coordSysId: coordSysModel.id,
  244. coordSysIndex: coordSysModel.componentIndex,
  245. coordSysType: coordSysModel.type,
  246. coordSysMainType: coordSysModel.mainType,
  247. dataByAxis: []
  248. };
  249. dataByCoordSys.list.push(coordSysItem);
  250. }
  251. coordSysItem.dataByAxis.push({
  252. axisDim: axis.dim,
  253. axisIndex: axisModel.componentIndex,
  254. axisType: axisModel.type,
  255. axisId: axisModel.id,
  256. value: value,
  257. // Caustion: viewHelper.getValueLabel is actually on "view stage", which
  258. // depends that all models have been updated. So it should not be performed
  259. // here. Considering axisPointerModel used here is volatile, which is hard
  260. // to be retrieve in TooltipView, we prepare parameters here.
  261. valueLabelOpt: {
  262. precision: axisPointerModel.get('label.precision'),
  263. formatter: axisPointerModel.get('label.formatter')
  264. },
  265. seriesDataIndices: payloadBatch.slice()
  266. });
  267. }
  268. function updateModelActually(showValueMap, axesInfo, outputFinder) {
  269. var outputAxesInfo = outputFinder.axesInfo = []; // Basic logic: If no 'show' required, 'hide' this axisPointer.
  270. each(axesInfo, function (axisInfo, key) {
  271. var option = axisInfo.axisPointerModel.option;
  272. var valItem = showValueMap[key];
  273. if (valItem) {
  274. !axisInfo.useHandle && (option.status = 'show');
  275. option.value = valItem.value; // For label formatter param and highlight.
  276. option.seriesDataIndices = (valItem.payloadBatch || []).slice();
  277. } // When always show (e.g., handle used), remain
  278. // original value and status.
  279. else {
  280. // If hide, value still need to be set, consider
  281. // click legend to toggle axis blank.
  282. !axisInfo.useHandle && (option.status = 'hide');
  283. } // If status is 'hide', should be no info in payload.
  284. option.status === 'show' && outputAxesInfo.push({
  285. axisDim: axisInfo.axis.dim,
  286. axisIndex: axisInfo.axis.model.componentIndex,
  287. value: option.value
  288. });
  289. });
  290. }
  291. function dispatchTooltipActually(dataByCoordSys, point, payload, dispatchAction) {
  292. // Basic logic: If no showTip required, hideTip will be dispatched.
  293. if (illegalPoint(point) || !dataByCoordSys.list.length) {
  294. dispatchAction({
  295. type: 'hideTip'
  296. });
  297. return;
  298. } // In most case only one axis (or event one series is used). It is
  299. // convinient to fetch payload.seriesIndex and payload.dataIndex
  300. // dirtectly. So put the first seriesIndex and dataIndex of the first
  301. // axis on the payload.
  302. var sampleItem = ((dataByCoordSys.list[0].dataByAxis[0] || {}).seriesDataIndices || [])[0] || {};
  303. dispatchAction({
  304. type: 'showTip',
  305. escapeConnect: true,
  306. x: point[0],
  307. y: point[1],
  308. tooltipOption: payload.tooltipOption,
  309. position: payload.position,
  310. dataIndexInside: sampleItem.dataIndexInside,
  311. dataIndex: sampleItem.dataIndex,
  312. seriesIndex: sampleItem.seriesIndex,
  313. dataByCoordSys: dataByCoordSys.list
  314. });
  315. }
  316. function dispatchHighDownActually(axesInfo, dispatchAction, api) {
  317. // FIXME
  318. // highlight status modification shoule be a stage of main process?
  319. // (Consider confilct (e.g., legend and axisPointer) and setOption)
  320. var zr = api.getZr();
  321. var highDownKey = 'axisPointerLastHighlights';
  322. var lastHighlights = inner(zr)[highDownKey] || {};
  323. var newHighlights = inner(zr)[highDownKey] = {}; // Update highlight/downplay status according to axisPointer model.
  324. // Build hash map and remove duplicate incidentally.
  325. each(axesInfo, function (axisInfo, key) {
  326. var option = axisInfo.axisPointerModel.option;
  327. option.status === 'show' && each(option.seriesDataIndices, function (batchItem) {
  328. var key = batchItem.seriesIndex + ' | ' + batchItem.dataIndex;
  329. newHighlights[key] = batchItem;
  330. });
  331. }); // Diff.
  332. var toHighlight = [];
  333. var toDownplay = [];
  334. zrUtil.each(lastHighlights, function (batchItem, key) {
  335. !newHighlights[key] && toDownplay.push(batchItem);
  336. });
  337. zrUtil.each(newHighlights, function (batchItem, key) {
  338. !lastHighlights[key] && toHighlight.push(batchItem);
  339. });
  340. toDownplay.length && api.dispatchAction({
  341. type: 'downplay',
  342. escapeConnect: true,
  343. batch: toDownplay
  344. });
  345. toHighlight.length && api.dispatchAction({
  346. type: 'highlight',
  347. escapeConnect: true,
  348. batch: toHighlight
  349. });
  350. }
  351. function findInputAxisInfo(inputAxesInfo, axisInfo) {
  352. for (var i = 0; i < (inputAxesInfo || []).length; i++) {
  353. var inputAxisInfo = inputAxesInfo[i];
  354. if (axisInfo.axis.dim === inputAxisInfo.axisDim && axisInfo.axis.model.componentIndex === inputAxisInfo.axisIndex) {
  355. return inputAxisInfo;
  356. }
  357. }
  358. }
  359. function makeMapperParam(axisInfo) {
  360. var axisModel = axisInfo.axis.model;
  361. var item = {};
  362. var dim = item.axisDim = axisInfo.axis.dim;
  363. item.axisIndex = item[dim + 'AxisIndex'] = axisModel.componentIndex;
  364. item.axisName = item[dim + 'AxisName'] = axisModel.name;
  365. item.axisId = item[dim + 'AxisId'] = axisModel.id;
  366. return item;
  367. }
  368. function illegalPoint(point) {
  369. return !point || point[0] == null || isNaN(point[0]) || point[1] == null || isNaN(point[1]);
  370. }
  371. module.exports = _default;