Interaction.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299
  1. /**
  2. * (c) 2010-2019 Torstein Honsi
  3. *
  4. * License: www.highcharts.com/license
  5. */
  6. 'use strict';
  7. import H from './Globals.js';
  8. import './Utilities.js';
  9. import './Chart.js';
  10. import './Options.js';
  11. import './Legend.js';
  12. import './Point.js';
  13. import './Series.js';
  14. var addEvent = H.addEvent,
  15. Chart = H.Chart,
  16. createElement = H.createElement,
  17. css = H.css,
  18. defaultOptions = H.defaultOptions,
  19. defaultPlotOptions = H.defaultPlotOptions,
  20. extend = H.extend,
  21. fireEvent = H.fireEvent,
  22. hasTouch = H.hasTouch,
  23. isObject = H.isObject,
  24. Legend = H.Legend,
  25. merge = H.merge,
  26. pick = H.pick,
  27. Point = H.Point,
  28. Series = H.Series,
  29. seriesTypes = H.seriesTypes,
  30. svg = H.svg,
  31. TrackerMixin;
  32. /**
  33. * TrackerMixin for points and graphs.
  34. *
  35. * @private
  36. * @mixin Highcharts.TrackerMixin
  37. */
  38. TrackerMixin = H.TrackerMixin = {
  39. /**
  40. * Draw the tracker for a point.
  41. *
  42. * @private
  43. * @function Highcharts.TrackerMixin.drawTrackerPoint
  44. *
  45. * @fires Highcharts.Series#event:afterDrawTracker
  46. */
  47. drawTrackerPoint: function () {
  48. var series = this,
  49. chart = series.chart,
  50. pointer = chart.pointer,
  51. onMouseOver = function (e) {
  52. var point = pointer.getPointFromEvent(e);
  53. // undefined on graph in scatterchart
  54. if (point !== undefined) {
  55. pointer.isDirectTouch = true;
  56. point.onMouseOver(e);
  57. }
  58. };
  59. // Add reference to the point
  60. series.points.forEach(function (point) {
  61. if (point.graphic) {
  62. point.graphic.element.point = point;
  63. }
  64. if (point.dataLabel) {
  65. if (point.dataLabel.div) {
  66. point.dataLabel.div.point = point;
  67. } else {
  68. point.dataLabel.element.point = point;
  69. }
  70. }
  71. });
  72. // Add the event listeners, we need to do this only once
  73. if (!series._hasTracking) {
  74. series.trackerGroups.forEach(function (key) {
  75. if (series[key]) { // we don't always have dataLabelsGroup
  76. series[key]
  77. .addClass('highcharts-tracker')
  78. .on('mouseover', onMouseOver)
  79. .on('mouseout', function (e) {
  80. pointer.onTrackerMouseOut(e);
  81. });
  82. if (hasTouch) {
  83. series[key].on('touchstart', onMouseOver);
  84. }
  85. if (!chart.styledMode && series.options.cursor) {
  86. series[key]
  87. .css(css)
  88. .css({ cursor: series.options.cursor });
  89. }
  90. }
  91. });
  92. series._hasTracking = true;
  93. }
  94. fireEvent(this, 'afterDrawTracker');
  95. },
  96. /**
  97. * Draw the tracker object that sits above all data labels and markers to
  98. * track mouse events on the graph or points. For the line type charts
  99. * the tracker uses the same graphPath, but with a greater stroke width
  100. * for better control.
  101. *
  102. * @private
  103. * @function Highcharts.TrackerMixin.drawTrackerGraph
  104. *
  105. * @fires Highcharts.Series#event:afterDrawTracker
  106. */
  107. drawTrackerGraph: function () {
  108. var series = this,
  109. options = series.options,
  110. trackByArea = options.trackByArea,
  111. trackerPath = [].concat(
  112. trackByArea ? series.areaPath : series.graphPath
  113. ),
  114. trackerPathLength = trackerPath.length,
  115. chart = series.chart,
  116. pointer = chart.pointer,
  117. renderer = chart.renderer,
  118. snap = chart.options.tooltip.snap,
  119. tracker = series.tracker,
  120. i,
  121. onMouseOver = function () {
  122. if (chart.hoverSeries !== series) {
  123. series.onMouseOver();
  124. }
  125. },
  126. /*
  127. * Empirical lowest possible opacities for TRACKER_FILL for an
  128. * element to stay invisible but clickable
  129. * IE6: 0.002
  130. * IE7: 0.002
  131. * IE8: 0.002
  132. * IE9: 0.00000000001 (unlimited)
  133. * IE10: 0.0001 (exporting only)
  134. * FF: 0.00000000001 (unlimited)
  135. * Chrome: 0.000001
  136. * Safari: 0.000001
  137. * Opera: 0.00000000001 (unlimited)
  138. */
  139. TRACKER_FILL = 'rgba(192,192,192,' + (svg ? 0.0001 : 0.002) + ')';
  140. // Extend end points. A better way would be to use round linecaps,
  141. // but those are not clickable in VML.
  142. if (trackerPathLength && !trackByArea) {
  143. i = trackerPathLength + 1;
  144. while (i--) {
  145. if (trackerPath[i] === 'M') { // extend left side
  146. trackerPath.splice(
  147. i + 1, 0,
  148. trackerPath[i + 1] - snap,
  149. trackerPath[i + 2],
  150. 'L'
  151. );
  152. }
  153. if (
  154. (i && trackerPath[i] === 'M') ||
  155. i === trackerPathLength
  156. ) { // extend right side
  157. trackerPath.splice(
  158. i,
  159. 0,
  160. 'L',
  161. trackerPath[i - 2] + snap,
  162. trackerPath[i - 1]
  163. );
  164. }
  165. }
  166. }
  167. // draw the tracker
  168. if (tracker) {
  169. tracker.attr({ d: trackerPath });
  170. } else if (series.graph) { // create
  171. series.tracker = renderer.path(trackerPath)
  172. .attr({
  173. visibility: series.visible ? 'visible' : 'hidden',
  174. zIndex: 2
  175. })
  176. .addClass(
  177. trackByArea ?
  178. 'highcharts-tracker-area' :
  179. 'highcharts-tracker-line'
  180. )
  181. .add(series.group);
  182. if (!chart.styledMode) {
  183. series.tracker.attr({
  184. 'stroke-linejoin': 'round', // #1225
  185. stroke: TRACKER_FILL,
  186. fill: trackByArea ? TRACKER_FILL : 'none',
  187. 'stroke-width': series.graph.strokeWidth() +
  188. (trackByArea ? 0 : 2 * snap)
  189. });
  190. }
  191. // The tracker is added to the series group, which is clipped, but
  192. // is covered by the marker group. So the marker group also needs to
  193. // capture events.
  194. [series.tracker, series.markerGroup].forEach(function (tracker) {
  195. tracker.addClass('highcharts-tracker')
  196. .on('mouseover', onMouseOver)
  197. .on('mouseout', function (e) {
  198. pointer.onTrackerMouseOut(e);
  199. });
  200. if (options.cursor && !chart.styledMode) {
  201. tracker.css({ cursor: options.cursor });
  202. }
  203. if (hasTouch) {
  204. tracker.on('touchstart', onMouseOver);
  205. }
  206. });
  207. }
  208. fireEvent(this, 'afterDrawTracker');
  209. }
  210. };
  211. /* End TrackerMixin */
  212. /*
  213. * Add tracking event listener to the series group, so the point graphics
  214. * themselves act as trackers
  215. */
  216. if (seriesTypes.column) {
  217. /**
  218. * @private
  219. * @borrows Highcharts.TrackerMixin.drawTrackerPoint as Highcharts.seriesTypes.column#drawTracker
  220. */
  221. seriesTypes.column.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
  222. }
  223. if (seriesTypes.pie) {
  224. /**
  225. * @private
  226. * @borrows Highcharts.TrackerMixin.drawTrackerPoint as Highcharts.seriesTypes.pie#drawTracker
  227. */
  228. seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
  229. }
  230. if (seriesTypes.scatter) {
  231. /**
  232. * @private
  233. * @borrows Highcharts.TrackerMixin.drawTrackerPoint as Highcharts.seriesTypes.scatter#drawTracker
  234. */
  235. seriesTypes.scatter.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
  236. }
  237. // Extend Legend for item events.
  238. extend(Legend.prototype, {
  239. /**
  240. * @private
  241. * @function Highcharts.Legend#setItemEvents
  242. *
  243. * @param {Highcharts.Point|Highcharts.Series} item
  244. *
  245. * @param {Highcharts.SVGElement} legendItem
  246. *
  247. * @param {boolean} [useHTML=false]
  248. *
  249. * @fires Highcharts.Point#event:legendItemClick
  250. * @fires Highcharts.Series#event:legendItemClick
  251. */
  252. setItemEvents: function (item, legendItem, useHTML) {
  253. var legend = this,
  254. boxWrapper = legend.chart.renderer.boxWrapper,
  255. activeClass = 'highcharts-legend-' +
  256. (item instanceof Point ? 'point' : 'series') + '-active',
  257. styledMode = legend.chart.styledMode;
  258. // Set the events on the item group, or in case of useHTML, the item
  259. // itself (#1249)
  260. (useHTML ? legendItem : item.legendGroup).on('mouseover', function () {
  261. item.setState('hover');
  262. // A CSS class to dim or hide other than the hovered series
  263. boxWrapper.addClass(activeClass);
  264. if (!styledMode) {
  265. legendItem.css(legend.options.itemHoverStyle);
  266. }
  267. })
  268. .on('mouseout', function () {
  269. if (!legend.styledMode) {
  270. legendItem.css(
  271. merge(
  272. item.visible ?
  273. legend.itemStyle :
  274. legend.itemHiddenStyle
  275. )
  276. );
  277. }
  278. // A CSS class to dim or hide other than the hovered series
  279. boxWrapper.removeClass(activeClass);
  280. item.setState();
  281. })
  282. .on('click', function (event) {
  283. var strLegendItemClick = 'legendItemClick',
  284. fnLegendItemClick = function () {
  285. if (item.setVisible) {
  286. item.setVisible();
  287. }
  288. };
  289. // A CSS class to dim or hide other than the hovered series.
  290. // Event handling in iOS causes the activeClass to be added
  291. // prior to click in some cases (#7418).
  292. boxWrapper.removeClass(activeClass);
  293. // Pass over the click/touch event. #4.
  294. event = {
  295. browserEvent: event
  296. };
  297. // click the name or symbol
  298. if (item.firePointEvent) { // point
  299. item.firePointEvent(
  300. strLegendItemClick,
  301. event,
  302. fnLegendItemClick
  303. );
  304. } else {
  305. fireEvent(
  306. item, strLegendItemClick, event, fnLegendItemClick
  307. );
  308. }
  309. });
  310. },
  311. /**
  312. * @private
  313. * @function Highcharts.Legend#createCheckboxForItem
  314. *
  315. * @param {Highcharts.Point|Highcharts.Series} item
  316. *
  317. * @fires Highcharts.Series#event:checkboxClick
  318. */
  319. createCheckboxForItem: function (item) {
  320. var legend = this;
  321. item.checkbox = createElement('input', {
  322. type: 'checkbox',
  323. className: 'highcharts-legend-checkbox',
  324. checked: item.selected,
  325. defaultChecked: item.selected // required by IE7
  326. }, legend.options.itemCheckboxStyle, legend.chart.container);
  327. addEvent(item.checkbox, 'click', function (event) {
  328. var target = event.target;
  329. fireEvent(
  330. item.series || item,
  331. 'checkboxClick',
  332. { // #3712
  333. checked: target.checked,
  334. item: item
  335. },
  336. function () {
  337. item.select();
  338. }
  339. );
  340. });
  341. }
  342. });
  343. /*
  344. * Extend the Chart object with interaction
  345. */
  346. extend(Chart.prototype, /** @lends Chart.prototype */ {
  347. /**
  348. * Display the zoom button.
  349. *
  350. * @private
  351. * @function Highcharts.Chart#showResetZoom
  352. *
  353. * @fires Highcharts.Chart#event:beforeShowResetZoom
  354. */
  355. showResetZoom: function () {
  356. var chart = this,
  357. lang = defaultOptions.lang,
  358. btnOptions = chart.options.chart.resetZoomButton,
  359. theme = btnOptions.theme,
  360. states = theme.states,
  361. alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
  362. function zoomOut() {
  363. chart.zoomOut();
  364. }
  365. fireEvent(this, 'beforeShowResetZoom', null, function () {
  366. chart.resetZoomButton = chart.renderer.button(
  367. lang.resetZoom,
  368. null,
  369. null,
  370. zoomOut,
  371. theme,
  372. states && states.hover
  373. )
  374. .attr({
  375. align: btnOptions.position.align,
  376. title: lang.resetZoomTitle
  377. })
  378. .addClass('highcharts-reset-zoom')
  379. .add()
  380. .align(btnOptions.position, false, alignTo);
  381. });
  382. },
  383. /**
  384. * Zoom the chart out after a user has zoomed in. See also
  385. * [Axis.setExtremes](/class-reference/Highcharts.Axis#setExtremes).
  386. *
  387. * @function Highcharts.Chart#zoomOut
  388. *
  389. * @fires Highcharts.Chart#event:selection
  390. */
  391. zoomOut: function () {
  392. fireEvent(this, 'selection', { resetSelection: true }, this.zoom);
  393. },
  394. /**
  395. * Zoom into a given portion of the chart given by axis coordinates.
  396. *
  397. * @private
  398. * @function Highcharts.Chart#zoom
  399. *
  400. * @param {Highcharts.SelectEventObject} event
  401. */
  402. zoom: function (event) {
  403. var chart = this,
  404. hasZoomed,
  405. pointer = chart.pointer,
  406. displayButton = false,
  407. resetZoomButton;
  408. // If zoom is called with no arguments, reset the axes
  409. if (!event || event.resetSelection) {
  410. chart.axes.forEach(function (axis) {
  411. hasZoomed = axis.zoom();
  412. });
  413. pointer.initiated = false; // #6804
  414. } else { // else, zoom in on all axes
  415. event.xAxis.concat(event.yAxis).forEach(function (axisData) {
  416. var axis = axisData.axis,
  417. isXAxis = axis.isXAxis;
  418. // don't zoom more than minRange
  419. if (pointer[isXAxis ? 'zoomX' : 'zoomY']) {
  420. hasZoomed = axis.zoom(axisData.min, axisData.max);
  421. if (axis.displayBtn) {
  422. displayButton = true;
  423. }
  424. }
  425. });
  426. }
  427. // Show or hide the Reset zoom button
  428. resetZoomButton = chart.resetZoomButton;
  429. if (displayButton && !resetZoomButton) {
  430. chart.showResetZoom();
  431. } else if (!displayButton && isObject(resetZoomButton)) {
  432. chart.resetZoomButton = resetZoomButton.destroy();
  433. }
  434. // Redraw
  435. if (hasZoomed) {
  436. chart.redraw(
  437. pick(
  438. chart.options.chart.animation,
  439. event && event.animation,
  440. chart.pointCount < 100
  441. )
  442. );
  443. }
  444. },
  445. /**
  446. * Pan the chart by dragging the mouse across the pane. This function is
  447. * called on mouse move, and the distance to pan is computed from chartX
  448. * compared to the first chartX position in the dragging operation.
  449. *
  450. * @private
  451. * @function Highcharts.Chart#pan
  452. *
  453. * @param {Highcharts.PointerEventObject} e
  454. *
  455. * @param {string} panning
  456. */
  457. pan: function (e, panning) {
  458. var chart = this,
  459. hoverPoints = chart.hoverPoints,
  460. doRedraw;
  461. fireEvent(this, 'pan', { originalEvent: e }, function () {
  462. // remove active points for shared tooltip
  463. if (hoverPoints) {
  464. hoverPoints.forEach(function (point) {
  465. point.setState();
  466. });
  467. }
  468. // xy is used in maps
  469. (panning === 'xy' ? [1, 0] : [1]).forEach(function (isX) {
  470. var axis = chart[isX ? 'xAxis' : 'yAxis'][0],
  471. horiz = axis.horiz,
  472. mousePos = e[horiz ? 'chartX' : 'chartY'],
  473. mouseDown = horiz ? 'mouseDownX' : 'mouseDownY',
  474. startPos = chart[mouseDown],
  475. halfPointRange = (axis.pointRange || 0) / 2,
  476. pointRangeDirection =
  477. (axis.reversed && !chart.inverted) ||
  478. (!axis.reversed && chart.inverted) ?
  479. -1 :
  480. 1,
  481. extremes = axis.getExtremes(),
  482. panMin = axis.toValue(startPos - mousePos, true) +
  483. halfPointRange * pointRangeDirection,
  484. panMax =
  485. axis.toValue(
  486. startPos + axis.len - mousePos, true
  487. ) -
  488. halfPointRange * pointRangeDirection,
  489. flipped = panMax < panMin,
  490. newMin = flipped ? panMax : panMin,
  491. newMax = flipped ? panMin : panMax,
  492. paddedMin = Math.min(
  493. extremes.dataMin,
  494. halfPointRange ?
  495. extremes.min :
  496. axis.toValue(
  497. axis.toPixels(extremes.min) -
  498. axis.minPixelPadding
  499. )
  500. ),
  501. paddedMax = Math.max(
  502. extremes.dataMax,
  503. halfPointRange ?
  504. extremes.max :
  505. axis.toValue(
  506. axis.toPixels(extremes.max) +
  507. axis.minPixelPadding
  508. )
  509. ),
  510. spill;
  511. // If the new range spills over, either to the min or max,
  512. // adjust the new range.
  513. spill = paddedMin - newMin;
  514. if (spill > 0) {
  515. newMax += spill;
  516. newMin = paddedMin;
  517. }
  518. spill = newMax - paddedMax;
  519. if (spill > 0) {
  520. newMax = paddedMax;
  521. newMin -= spill;
  522. }
  523. // Set new extremes if they are actually new
  524. if (
  525. axis.series.length &&
  526. newMin !== extremes.min &&
  527. newMax !== extremes.max
  528. ) {
  529. axis.setExtremes(
  530. newMin,
  531. newMax,
  532. false,
  533. false,
  534. { trigger: 'pan' }
  535. );
  536. doRedraw = true;
  537. }
  538. chart[mouseDown] = mousePos; // set new reference for next run
  539. });
  540. if (doRedraw) {
  541. chart.redraw(false);
  542. }
  543. css(chart.container, { cursor: 'move' });
  544. });
  545. }
  546. });
  547. // Extend the Point object with interaction
  548. extend(Point.prototype, /** @lends Highcharts.Point.prototype */ {
  549. /**
  550. * Toggle the selection status of a point.
  551. *
  552. * @see Highcharts.Chart#getSelectedPoints
  553. *
  554. * @sample highcharts/members/point-select/
  555. * Select a point from a button
  556. * @sample highcharts/chart/events-selection-points/
  557. * Select a range of points through a drag selection
  558. * @sample maps/series/data-id/
  559. * Select a point in Highmaps
  560. *
  561. * @function Highcharts.Point#select
  562. *
  563. * @param {boolean} [selected]
  564. * When `true`, the point is selected. When `false`, the point is
  565. * unselected. When `null` or `undefined`, the selection state is
  566. * toggled.
  567. *
  568. * @param {boolean} [accumulate=false]
  569. * When `true`, the selection is added to other selected points.
  570. * When `false`, other selected points are deselected. Internally in
  571. * Highcharts, when
  572. * [allowPointSelect](http://api.highcharts.com/highcharts/plotOptions.series.allowPointSelect)
  573. * is `true`, selected points are accumulated on Control, Shift or
  574. * Cmd clicking the point.
  575. *
  576. * @fires Highcharts.Point#event:select
  577. * @fires Highcharts.Point#event:unselect
  578. */
  579. select: function (selected, accumulate) {
  580. var point = this,
  581. series = point.series,
  582. chart = series.chart;
  583. selected = pick(selected, !point.selected);
  584. // fire the event with the default handler
  585. point.firePointEvent(
  586. selected ? 'select' : 'unselect',
  587. { accumulate: accumulate },
  588. function () {
  589. /**
  590. * Whether the point is selected or not.
  591. *
  592. * @see Point#select
  593. * @see Chart#getSelectedPoints
  594. *
  595. * @name Highcharts.Point#selected
  596. * @type {boolean}
  597. */
  598. point.selected = point.options.selected = selected;
  599. series.options.data[series.data.indexOf(point)] =
  600. point.options;
  601. point.setState(selected && 'select');
  602. // unselect all other points unless Ctrl or Cmd + click
  603. if (!accumulate) {
  604. chart.getSelectedPoints().forEach(function (loopPoint) {
  605. if (loopPoint.selected && loopPoint !== point) {
  606. loopPoint.selected = loopPoint.options.selected =
  607. false;
  608. series.options.data[
  609. series.data.indexOf(loopPoint)
  610. ] = loopPoint.options;
  611. loopPoint.setState('');
  612. loopPoint.firePointEvent('unselect');
  613. }
  614. });
  615. }
  616. }
  617. );
  618. },
  619. /**
  620. * Runs on mouse over the point. Called internally from mouse and touch
  621. * events.
  622. *
  623. * @function Highcharts.Point#onMouseOver
  624. *
  625. * @param {Highcharts.PointerEventObject} e
  626. * The event arguments.
  627. */
  628. onMouseOver: function (e) {
  629. var point = this,
  630. series = point.series,
  631. chart = series.chart,
  632. pointer = chart.pointer;
  633. e = e ?
  634. pointer.normalize(e) :
  635. // In cases where onMouseOver is called directly without an event
  636. pointer.getChartCoordinatesFromPoint(point, chart.inverted);
  637. pointer.runPointActions(e, point);
  638. },
  639. /**
  640. * Runs on mouse out from the point. Called internally from mouse and touch
  641. * events.
  642. *
  643. * @function Highcharts.Point#onMouseOut
  644. *
  645. * @fires Highcharts.Point#event:mouseOut
  646. */
  647. onMouseOut: function () {
  648. var point = this,
  649. chart = point.series.chart;
  650. point.firePointEvent('mouseOut');
  651. (chart.hoverPoints || []).forEach(function (p) {
  652. p.setState();
  653. });
  654. chart.hoverPoints = chart.hoverPoint = null;
  655. },
  656. /**
  657. * Import events from the series' and point's options. Only do it on
  658. * demand, to save processing time on hovering.
  659. *
  660. * @private
  661. * @function Highcharts.Point#importEvents
  662. */
  663. importEvents: function () {
  664. if (!this.hasImportedEvents) {
  665. var point = this,
  666. options = merge(point.series.options.point, point.options),
  667. events = options.events;
  668. point.events = events;
  669. H.objectEach(events, function (event, eventType) {
  670. addEvent(point, eventType, event);
  671. });
  672. this.hasImportedEvents = true;
  673. }
  674. },
  675. /**
  676. * Set the point's state.
  677. *
  678. * @function Highcharts.Point#setState
  679. *
  680. * @param {string} [state]
  681. * The new state, can be one of `''` (an empty string), `hover` or
  682. * `select`.
  683. *
  684. * @param {boolean} [move]
  685. * State for animation.
  686. *
  687. * @fires Highcharts.Point#event:afterSetState
  688. */
  689. setState: function (state, move) {
  690. var point = this,
  691. plotX = Math.floor(point.plotX), // #4586
  692. plotY = point.plotY,
  693. series = point.series,
  694. stateOptions = series.options.states[state || 'normal'] || {},
  695. markerOptions = defaultPlotOptions[series.type].marker &&
  696. series.options.marker,
  697. normalDisabled = markerOptions && markerOptions.enabled === false,
  698. markerStateOptions = (
  699. markerOptions &&
  700. markerOptions.states &&
  701. markerOptions.states[state || 'normal']
  702. ) || {},
  703. stateDisabled = markerStateOptions.enabled === false,
  704. stateMarkerGraphic = series.stateMarkerGraphic,
  705. pointMarker = point.marker || {},
  706. chart = series.chart,
  707. halo = series.halo,
  708. haloOptions,
  709. markerAttribs,
  710. hasMarkers = markerOptions && series.markerAttribs,
  711. newSymbol;
  712. state = state || ''; // empty string
  713. if (
  714. // already has this state
  715. (state === point.state && !move) ||
  716. // selected points don't respond to hover
  717. (point.selected && state !== 'select') ||
  718. // series' state options is disabled
  719. (stateOptions.enabled === false) ||
  720. // general point marker's state options is disabled
  721. (state && (
  722. stateDisabled ||
  723. (normalDisabled && markerStateOptions.enabled === false)
  724. )) ||
  725. // individual point marker's state options is disabled
  726. (
  727. state &&
  728. pointMarker.states &&
  729. pointMarker.states[state] &&
  730. pointMarker.states[state].enabled === false
  731. ) // #1610
  732. ) {
  733. return;
  734. }
  735. if (hasMarkers) {
  736. markerAttribs = series.markerAttribs(point, state);
  737. }
  738. // Apply hover styles to the existing point
  739. if (point.graphic) {
  740. if (point.state) {
  741. point.graphic.removeClass('highcharts-point-' + point.state);
  742. }
  743. if (state) {
  744. point.graphic.addClass('highcharts-point-' + state);
  745. }
  746. if (!chart.styledMode) {
  747. point.graphic.animate(
  748. series.pointAttribs(point, state),
  749. pick(
  750. chart.options.chart.animation,
  751. stateOptions.animation
  752. )
  753. );
  754. }
  755. if (markerAttribs) {
  756. point.graphic.animate(
  757. markerAttribs,
  758. pick(
  759. chart.options.chart.animation, // Turn off globally
  760. markerStateOptions.animation,
  761. markerOptions.animation
  762. )
  763. );
  764. }
  765. // Zooming in from a range with no markers to a range with markers
  766. if (stateMarkerGraphic) {
  767. stateMarkerGraphic.hide();
  768. }
  769. } else {
  770. // if a graphic is not applied to each point in the normal state,
  771. // create a shared graphic for the hover state
  772. if (state && markerStateOptions) {
  773. newSymbol = pointMarker.symbol || series.symbol;
  774. // If the point has another symbol than the previous one, throw
  775. // away the state marker graphic and force a new one (#1459)
  776. if (
  777. stateMarkerGraphic &&
  778. stateMarkerGraphic.currentSymbol !== newSymbol
  779. ) {
  780. stateMarkerGraphic = stateMarkerGraphic.destroy();
  781. }
  782. // Add a new state marker graphic
  783. if (!stateMarkerGraphic) {
  784. if (newSymbol) {
  785. series.stateMarkerGraphic = stateMarkerGraphic =
  786. chart.renderer.symbol(
  787. newSymbol,
  788. markerAttribs.x,
  789. markerAttribs.y,
  790. markerAttribs.width,
  791. markerAttribs.height
  792. )
  793. .add(series.markerGroup);
  794. stateMarkerGraphic.currentSymbol = newSymbol;
  795. }
  796. // Move the existing graphic
  797. } else {
  798. stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
  799. x: markerAttribs.x,
  800. y: markerAttribs.y
  801. });
  802. }
  803. if (!chart.styledMode && stateMarkerGraphic) {
  804. stateMarkerGraphic.attr(series.pointAttribs(point, state));
  805. }
  806. }
  807. if (stateMarkerGraphic) {
  808. stateMarkerGraphic[
  809. state && chart.isInsidePlot(plotX, plotY, chart.inverted) ?
  810. 'show' :
  811. 'hide'
  812. ](); // #2450
  813. stateMarkerGraphic.element.point = point; // #4310
  814. }
  815. }
  816. // Show me your halo
  817. haloOptions = stateOptions.halo;
  818. if (haloOptions && haloOptions.size) {
  819. if (!halo) {
  820. series.halo = halo = chart.renderer.path()
  821. // #5818, #5903, #6705
  822. .add((point.graphic || stateMarkerGraphic).parentGroup);
  823. }
  824. halo.show()[move ? 'animate' : 'attr']({
  825. d: point.haloPath(haloOptions.size)
  826. });
  827. halo.attr({
  828. 'class': 'highcharts-halo highcharts-color-' +
  829. pick(point.colorIndex, series.colorIndex) +
  830. (point.className ? ' ' + point.className : ''),
  831. 'zIndex': -1 // #4929, #8276
  832. });
  833. halo.point = point; // #6055
  834. if (!chart.styledMode) {
  835. halo.attr(extend({
  836. 'fill': point.color || series.color,
  837. 'fill-opacity': haloOptions.opacity
  838. }, haloOptions.attributes));
  839. }
  840. } else if (halo && halo.point && halo.point.haloPath) {
  841. // Animate back to 0 on the current halo point (#6055)
  842. halo.animate(
  843. { d: halo.point.haloPath(0) },
  844. null,
  845. // Hide after unhovering. The `complete` callback runs in the
  846. // halo's context (#7681).
  847. halo.hide
  848. );
  849. }
  850. point.state = state;
  851. fireEvent(point, 'afterSetState');
  852. },
  853. /**
  854. * Get the path definition for the halo, which is usually a shadow-like
  855. * circle around the currently hovered point.
  856. *
  857. * @function Highcharts.Point#haloPath
  858. *
  859. * @param {number} size
  860. * The radius of the circular halo.
  861. *
  862. * @return {Highcharts.SVGPathArray}
  863. * The path definition.
  864. */
  865. haloPath: function (size) {
  866. var series = this.series,
  867. chart = series.chart;
  868. return chart.renderer.symbols.circle(
  869. Math.floor(this.plotX) - size,
  870. this.plotY - size,
  871. size * 2,
  872. size * 2
  873. );
  874. }
  875. });
  876. // Extend the Series object with interaction
  877. extend(Series.prototype, /** @lends Highcharts.Series.prototype */ {
  878. /**
  879. * Runs on mouse over the series graphical items.
  880. *
  881. * @function Highcharts.Series#onMouseOver
  882. *
  883. * @fires Highcharts.Series#event:mouseOver
  884. */
  885. onMouseOver: function () {
  886. var series = this,
  887. chart = series.chart,
  888. hoverSeries = chart.hoverSeries;
  889. // set normal state to previous series
  890. if (hoverSeries && hoverSeries !== series) {
  891. hoverSeries.onMouseOut();
  892. }
  893. // trigger the event, but to save processing time,
  894. // only if defined
  895. if (series.options.events.mouseOver) {
  896. fireEvent(series, 'mouseOver');
  897. }
  898. // hover this
  899. series.setState('hover');
  900. chart.hoverSeries = series;
  901. },
  902. /**
  903. * Runs on mouse out of the series graphical items.
  904. *
  905. * @function Highcharts.Series#onMouseOut
  906. *
  907. * @fires Highcharts.Series#event:mouseOut
  908. */
  909. onMouseOut: function () {
  910. // trigger the event only if listeners exist
  911. var series = this,
  912. options = series.options,
  913. chart = series.chart,
  914. tooltip = chart.tooltip,
  915. hoverPoint = chart.hoverPoint;
  916. // #182, set to null before the mouseOut event fires
  917. chart.hoverSeries = null;
  918. // trigger mouse out on the point, which must be in this series
  919. if (hoverPoint) {
  920. hoverPoint.onMouseOut();
  921. }
  922. // fire the mouse out event
  923. if (series && options.events.mouseOut) {
  924. fireEvent(series, 'mouseOut');
  925. }
  926. // hide the tooltip
  927. if (
  928. tooltip &&
  929. !series.stickyTracking &&
  930. (!tooltip.shared || series.noSharedTooltip)
  931. ) {
  932. tooltip.hide();
  933. }
  934. // set normal state
  935. series.setState();
  936. },
  937. /**
  938. * Set the state of the series. Called internally on mouse interaction
  939. * operations, but it can also be called directly to visually
  940. * highlight a series.
  941. *
  942. * @function Highcharts.Series#setState
  943. *
  944. * @param {string} [state]
  945. * Can be either `hover` or undefined to set to normal state.
  946. */
  947. setState: function (state) {
  948. var series = this,
  949. options = series.options,
  950. graph = series.graph,
  951. stateOptions = options.states,
  952. lineWidth = options.lineWidth,
  953. attribs,
  954. i = 0;
  955. state = state || '';
  956. if (series.state !== state) {
  957. // Toggle class names
  958. [
  959. series.group,
  960. series.markerGroup,
  961. series.dataLabelsGroup
  962. ].forEach(function (group) {
  963. if (group) {
  964. // Old state
  965. if (series.state) {
  966. group.removeClass('highcharts-series-' + series.state);
  967. }
  968. // New state
  969. if (state) {
  970. group.addClass('highcharts-series-' + state);
  971. }
  972. }
  973. });
  974. series.state = state;
  975. if (!series.chart.styledMode) {
  976. if (
  977. stateOptions[state] &&
  978. stateOptions[state].enabled === false
  979. ) {
  980. return;
  981. }
  982. if (state) {
  983. lineWidth = (
  984. stateOptions[state].lineWidth ||
  985. lineWidth + (stateOptions[state].lineWidthPlus || 0)
  986. ); // #4035
  987. }
  988. if (graph && !graph.dashstyle) {
  989. attribs = {
  990. 'stroke-width': lineWidth
  991. };
  992. // Animate the graph stroke-width. By default a quick
  993. // animation to hover, slower to un-hover.
  994. graph.animate(
  995. attribs,
  996. pick(
  997. (
  998. stateOptions[state || 'normal'] &&
  999. stateOptions[state || 'normal'].animation
  1000. ),
  1001. series.chart.options.chart.animation
  1002. )
  1003. );
  1004. while (series['zone-graph-' + i]) {
  1005. series['zone-graph-' + i].attr(attribs);
  1006. i = i + 1;
  1007. }
  1008. }
  1009. }
  1010. }
  1011. },
  1012. /**
  1013. * Show or hide the series.
  1014. *
  1015. * @function Highcharts.Series#setVisible
  1016. *
  1017. * @param {boolean} [visible]
  1018. * True to show the series, false to hide. If undefined, the
  1019. * visibility is toggled.
  1020. *
  1021. * @param {boolean} [redraw=true]
  1022. * Whether to redraw the chart after the series is altered. If doing
  1023. * more operations on the chart, it is a good idea to set redraw to
  1024. * false and call {@link Chart#redraw|chart.redraw()} after.
  1025. *
  1026. * @fires Highcharts.Series#event:hide
  1027. * @fires Highcharts.Series#event:show
  1028. */
  1029. setVisible: function (vis, redraw) {
  1030. var series = this,
  1031. chart = series.chart,
  1032. legendItem = series.legendItem,
  1033. showOrHide,
  1034. ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
  1035. oldVisibility = series.visible;
  1036. // if called without an argument, toggle visibility
  1037. series.visible =
  1038. vis =
  1039. series.options.visible =
  1040. series.userOptions.visible =
  1041. vis === undefined ? !oldVisibility : vis; // #5618
  1042. showOrHide = vis ? 'show' : 'hide';
  1043. // show or hide elements
  1044. [
  1045. 'group',
  1046. 'dataLabelsGroup',
  1047. 'markerGroup',
  1048. 'tracker',
  1049. 'tt'
  1050. ].forEach(function (key) {
  1051. if (series[key]) {
  1052. series[key][showOrHide]();
  1053. }
  1054. });
  1055. // hide tooltip (#1361)
  1056. if (
  1057. chart.hoverSeries === series ||
  1058. (chart.hoverPoint && chart.hoverPoint.series) === series
  1059. ) {
  1060. series.onMouseOut();
  1061. }
  1062. if (legendItem) {
  1063. chart.legend.colorizeItem(series, vis);
  1064. }
  1065. // rescale or adapt to resized chart
  1066. series.isDirty = true;
  1067. // in a stack, all other series are affected
  1068. if (series.options.stacking) {
  1069. chart.series.forEach(function (otherSeries) {
  1070. if (otherSeries.options.stacking && otherSeries.visible) {
  1071. otherSeries.isDirty = true;
  1072. }
  1073. });
  1074. }
  1075. // show or hide linked series
  1076. series.linkedSeries.forEach(function (otherSeries) {
  1077. otherSeries.setVisible(vis, false);
  1078. });
  1079. if (ignoreHiddenSeries) {
  1080. chart.isDirtyBox = true;
  1081. }
  1082. fireEvent(series, showOrHide);
  1083. if (redraw !== false) {
  1084. chart.redraw();
  1085. }
  1086. },
  1087. /**
  1088. * Show the series if hidden.
  1089. *
  1090. * @sample highcharts/members/series-hide/
  1091. * Toggle visibility from a button
  1092. *
  1093. * @function Highcharts.Series#show
  1094. *
  1095. * @fires Highcharts.Series#event:show
  1096. */
  1097. show: function () {
  1098. this.setVisible(true);
  1099. },
  1100. /**
  1101. * Hide the series if visible. If the {@link
  1102. * https://api.highcharts.com/highcharts/chart.ignoreHiddenSeries|
  1103. * chart.ignoreHiddenSeries} option is true, the chart is redrawn without
  1104. * this series.
  1105. *
  1106. * @sample highcharts/members/series-hide/
  1107. * Toggle visibility from a button
  1108. *
  1109. * @function Highcharts.Series#hide
  1110. *
  1111. * @fires Highcharts.Series#event:hide
  1112. */
  1113. hide: function () {
  1114. this.setVisible(false);
  1115. },
  1116. /**
  1117. * Select or unselect the series. This means its
  1118. * {@link Highcharts.Series.selected|selected}
  1119. * property is set, the checkbox in the legend is toggled and when selected,
  1120. * the series is returned by the
  1121. * {@link Highcharts.Chart#getSelectedSeries}
  1122. * function.
  1123. *
  1124. * @sample highcharts/members/series-select/
  1125. * Select a series from a button
  1126. *
  1127. * @function Highcharts.Series#select
  1128. *
  1129. * @param {boolean} [selected]
  1130. * True to select the series, false to unselect. If undefined, the
  1131. * selection state is toggled.
  1132. *
  1133. * @fires Highcharts.Series#event:select
  1134. * @fires Highcharts.Series#event:unselect
  1135. */
  1136. select: function (selected) {
  1137. var series = this;
  1138. series.selected =
  1139. selected =
  1140. this.options.selected = (
  1141. selected === undefined ?
  1142. !series.selected :
  1143. selected
  1144. );
  1145. if (series.checkbox) {
  1146. series.checkbox.checked = selected;
  1147. }
  1148. fireEvent(series, selected ? 'select' : 'unselect');
  1149. },
  1150. /**
  1151. * @private
  1152. * @borrows Highcharts.TrackerMixin.drawTrackerGraph as Highcharts.Series#drawTracker
  1153. */
  1154. drawTracker: TrackerMixin.drawTrackerGraph
  1155. });