ControllableLabel.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. 'use strict';
  2. import H from './../../parts/Globals.js';
  3. import U from './../../parts/Utilities.js';
  4. var extend = U.extend,
  5. isNumber = U.isNumber,
  6. pick = U.pick;
  7. import './../../parts/SvgRenderer.js';
  8. import controllableMixin from './controllableMixin.js';
  9. import MockPoint from './../MockPoint.js';
  10. /**
  11. * @private
  12. * @interface Highcharts.AnnotationAnchorObject
  13. *//**
  14. * Relative to the plot area position
  15. * @name Highcharts.AnnotationAnchorObject#relativePosition
  16. * @type {Highcharts.AnnotationAnchorPositionObject}
  17. *//**
  18. * Absolute position
  19. * @name Highcharts.AnnotationAnchorObject#absolutePosition
  20. * @type {Highcharts.AnnotationAnchorPositionObject}
  21. */
  22. /**
  23. * An object which denotes an anchor position
  24. *
  25. * @private
  26. * @interface Highcharts.AnnotationAnchorPositionObject
  27. *//**
  28. * @name Highcharts.AnnotationAnchorPositionObject#x
  29. * @property {number}
  30. *//**
  31. * @name Highcharts.AnnotationAnchorPositionObject#y
  32. * @property {number}
  33. *//**
  34. * @name Highcharts.AnnotationAnchorPositionObject#height
  35. * @property {number}
  36. *//**
  37. * @name Highcharts.AnnotationAnchorPositionObject#width
  38. * @property {number}
  39. */
  40. /**
  41. * A controllable label class.
  42. *
  43. * @private
  44. * @class
  45. * @name Annotation.ControllableLabel
  46. *
  47. * @mixes Annotation.controllableMixin
  48. *
  49. * @param {Highcharts.Annotation} annotation an annotation instance
  50. * @param {object} options a label's options
  51. * @param {number} index of the label
  52. **/
  53. function ControllableLabel(annotation, options, index) {
  54. this.init(annotation, options, index);
  55. this.collection = 'labels';
  56. }
  57. /**
  58. * Shapes which do not have background - the object is used for proper
  59. * setting of the contrast color.
  60. *
  61. * @type {Array<string>}
  62. */
  63. ControllableLabel.shapesWithoutBackground = ['connector'];
  64. /**
  65. * Returns new aligned position based alignment options and box to align to.
  66. * It is almost a one-to-one copy from SVGElement.prototype.align
  67. * except it does not use and mutate an element
  68. *
  69. * @param {Object} alignOptions
  70. * @param {Object} box
  71. * @return {Annotation.controllableMixin.Position} aligned position
  72. */
  73. ControllableLabel.alignedPosition = function (alignOptions, box) {
  74. var align = alignOptions.align,
  75. vAlign = alignOptions.verticalAlign,
  76. x = (box.x || 0) + (alignOptions.x || 0),
  77. y = (box.y || 0) + (alignOptions.y || 0),
  78. alignFactor,
  79. vAlignFactor;
  80. if (align === 'right') {
  81. alignFactor = 1;
  82. } else if (align === 'center') {
  83. alignFactor = 2;
  84. }
  85. if (alignFactor) {
  86. x += (box.width - (alignOptions.width || 0)) / alignFactor;
  87. }
  88. if (vAlign === 'bottom') {
  89. vAlignFactor = 1;
  90. } else if (vAlign === 'middle') {
  91. vAlignFactor = 2;
  92. }
  93. if (vAlignFactor) {
  94. y += (box.height - (alignOptions.height || 0)) / vAlignFactor;
  95. }
  96. return {
  97. x: Math.round(x),
  98. y: Math.round(y)
  99. };
  100. };
  101. /**
  102. * Returns new alignment options for a label if the label is outside the
  103. * plot area. It is almost a one-to-one copy from
  104. * Series.prototype.justifyDataLabel except it does not mutate the label and
  105. * it works with absolute instead of relative position.
  106. *
  107. * @param {Object} label
  108. * @param {Object} alignOptions
  109. * @param {Object} alignAttr
  110. * @return {Object} justified options
  111. **/
  112. ControllableLabel.justifiedOptions = function (
  113. chart,
  114. label,
  115. alignOptions,
  116. alignAttr
  117. ) {
  118. var align = alignOptions.align,
  119. verticalAlign = alignOptions.verticalAlign,
  120. padding = label.box ? 0 : (label.padding || 0),
  121. bBox = label.getBBox(),
  122. off,
  123. options = {
  124. align: align,
  125. verticalAlign: verticalAlign,
  126. x: alignOptions.x,
  127. y: alignOptions.y,
  128. width: label.width,
  129. height: label.height
  130. },
  131. x = alignAttr.x - chart.plotLeft,
  132. y = alignAttr.y - chart.plotTop;
  133. // Off left
  134. off = x + padding;
  135. if (off < 0) {
  136. if (align === 'right') {
  137. options.align = 'left';
  138. } else {
  139. options.x = -off;
  140. }
  141. }
  142. // Off right
  143. off = x + bBox.width - padding;
  144. if (off > chart.plotWidth) {
  145. if (align === 'left') {
  146. options.align = 'right';
  147. } else {
  148. options.x = chart.plotWidth - off;
  149. }
  150. }
  151. // Off top
  152. off = y + padding;
  153. if (off < 0) {
  154. if (verticalAlign === 'bottom') {
  155. options.verticalAlign = 'top';
  156. } else {
  157. options.y = -off;
  158. }
  159. }
  160. // Off bottom
  161. off = y + bBox.height - padding;
  162. if (off > chart.plotHeight) {
  163. if (verticalAlign === 'top') {
  164. options.verticalAlign = 'bottom';
  165. } else {
  166. options.y = chart.plotHeight - off;
  167. }
  168. }
  169. return options;
  170. };
  171. /**
  172. * @typedef {Object} Annotation.ControllableLabel.AttrsMap
  173. * @property {string} backgroundColor=fill
  174. * @property {string} borderColor=stroke
  175. * @property {string} borderWidth=stroke-width
  176. * @property {string} zIndex=zIndex
  177. * @property {string} borderRadius=r
  178. * @property {string} padding=padding
  179. */
  180. /**
  181. * A map object which allows to map options attributes to element attributes
  182. *
  183. * @type {Annotation.ControllableLabel.AttrsMap}
  184. */
  185. ControllableLabel.attrsMap = {
  186. backgroundColor: 'fill',
  187. borderColor: 'stroke',
  188. borderWidth: 'stroke-width',
  189. zIndex: 'zIndex',
  190. borderRadius: 'r',
  191. padding: 'padding'
  192. };
  193. H.merge(
  194. true,
  195. ControllableLabel.prototype,
  196. controllableMixin, /** @lends Annotation.ControllableLabel# */ {
  197. /**
  198. * Translate the point of the label by deltaX and deltaY translations.
  199. * The point is the label's anchor.
  200. *
  201. * @param {number} dx translation for x coordinate
  202. * @param {number} dy translation for y coordinate
  203. **/
  204. translatePoint: function (dx, dy) {
  205. controllableMixin.translatePoint.call(this, dx, dy, 0);
  206. },
  207. /**
  208. * Translate x and y position relative to the label's anchor.
  209. *
  210. * @param {number} dx translation for x coordinate
  211. * @param {number} dy translation for y coordinate
  212. **/
  213. translate: function (dx, dy) {
  214. var chart = this.annotation.chart,
  215. // Annotation.options
  216. labelOptions = this.annotation.userOptions,
  217. // Chart.options.annotations
  218. annotationIndex = chart.annotations.indexOf(this.annotation),
  219. chartAnnotations = chart.options.annotations,
  220. chartOptions = chartAnnotations[annotationIndex],
  221. temp;
  222. if (chart.inverted) {
  223. temp = dx;
  224. dx = dy;
  225. dy = temp;
  226. }
  227. // Local options:
  228. this.options.x += dx;
  229. this.options.y += dy;
  230. // Options stored in chart:
  231. chartOptions[this.collection][this.index].x = this.options.x;
  232. chartOptions[this.collection][this.index].y = this.options.y;
  233. labelOptions[this.collection][this.index].x = this.options.x;
  234. labelOptions[this.collection][this.index].y = this.options.y;
  235. },
  236. render: function (parent) {
  237. var options = this.options,
  238. attrs = this.attrsFromOptions(options),
  239. style = options.style;
  240. this.graphic = this.annotation.chart.renderer
  241. .label(
  242. '',
  243. 0,
  244. -9999, // #10055
  245. options.shape,
  246. null,
  247. null,
  248. options.useHTML,
  249. null,
  250. 'annotation-label'
  251. )
  252. .attr(attrs)
  253. .add(parent);
  254. if (!this.annotation.chart.styledMode) {
  255. if (style.color === 'contrast') {
  256. style.color = this.annotation.chart.renderer.getContrast(
  257. ControllableLabel.shapesWithoutBackground.indexOf(
  258. options.shape
  259. ) > -1 ? '#FFFFFF' : options.backgroundColor
  260. );
  261. }
  262. this.graphic
  263. .css(options.style)
  264. .shadow(options.shadow);
  265. }
  266. if (options.className) {
  267. this.graphic.addClass(options.className);
  268. }
  269. this.graphic.labelrank = options.labelrank;
  270. controllableMixin.render.call(this);
  271. },
  272. redraw: function (animation) {
  273. var options = this.options,
  274. text = this.text || options.format || options.text,
  275. label = this.graphic,
  276. point = this.points[0],
  277. show = false,
  278. anchor,
  279. attrs;
  280. label.attr({
  281. text: text ?
  282. H.format(
  283. text,
  284. point.getLabelConfig(),
  285. this.annotation.chart
  286. ) :
  287. options.formatter.call(point, this)
  288. });
  289. anchor = this.anchor(point);
  290. attrs = this.position(anchor);
  291. show = attrs;
  292. if (show) {
  293. label.alignAttr = attrs;
  294. attrs.anchorX = anchor.absolutePosition.x;
  295. attrs.anchorY = anchor.absolutePosition.y;
  296. label[animation ? 'animate' : 'attr'](attrs);
  297. } else {
  298. label.attr({
  299. x: 0,
  300. y: -9999 // #10055
  301. });
  302. }
  303. label.placed = Boolean(show);
  304. controllableMixin.redraw.call(this, animation);
  305. },
  306. /**
  307. * All basic shapes don't support alignTo() method except label.
  308. * For a controllable label, we need to subtract translation from
  309. * options.
  310. */
  311. anchor: function () {
  312. var anchor = controllableMixin.anchor.apply(this, arguments),
  313. x = this.options.x || 0,
  314. y = this.options.y || 0;
  315. anchor.absolutePosition.x -= x;
  316. anchor.absolutePosition.y -= y;
  317. anchor.relativePosition.x -= x;
  318. anchor.relativePosition.y -= y;
  319. return anchor;
  320. },
  321. /**
  322. * Returns the label position relative to its anchor.
  323. *
  324. * @param {Highcharts.AnnotationAnchorObject} anchor
  325. *
  326. * @return {Highcharts.AnnotationAnchorPositionObject|null} position
  327. */
  328. position: function (anchor) {
  329. var item = this.graphic,
  330. chart = this.annotation.chart,
  331. point = this.points[0],
  332. itemOptions = this.options,
  333. anchorAbsolutePosition = anchor.absolutePosition,
  334. anchorRelativePosition = anchor.relativePosition,
  335. itemPosition,
  336. alignTo,
  337. itemPosRelativeX,
  338. itemPosRelativeY,
  339. showItem =
  340. point.series.visible &&
  341. MockPoint.prototype.isInsidePane.call(point);
  342. if (showItem) {
  343. if (itemOptions.distance) {
  344. itemPosition = H.Tooltip.prototype.getPosition.call(
  345. {
  346. chart: chart,
  347. distance: pick(itemOptions.distance, 16)
  348. },
  349. item.width,
  350. item.height,
  351. {
  352. plotX: anchorRelativePosition.x,
  353. plotY: anchorRelativePosition.y,
  354. negative: point.negative,
  355. ttBelow: point.ttBelow,
  356. h: anchorRelativePosition.height ||
  357. anchorRelativePosition.width
  358. }
  359. );
  360. } else if (itemOptions.positioner) {
  361. itemPosition = itemOptions.positioner.call(this);
  362. } else {
  363. alignTo = {
  364. x: anchorAbsolutePosition.x,
  365. y: anchorAbsolutePosition.y,
  366. width: 0,
  367. height: 0
  368. };
  369. itemPosition = ControllableLabel.alignedPosition(
  370. extend(itemOptions, {
  371. width: item.width,
  372. height: item.height
  373. }),
  374. alignTo
  375. );
  376. if (this.options.overflow === 'justify') {
  377. itemPosition = ControllableLabel.alignedPosition(
  378. ControllableLabel.justifiedOptions(
  379. chart,
  380. item,
  381. itemOptions,
  382. itemPosition
  383. ),
  384. alignTo
  385. );
  386. }
  387. }
  388. if (itemOptions.crop) {
  389. itemPosRelativeX = itemPosition.x - chart.plotLeft;
  390. itemPosRelativeY = itemPosition.y - chart.plotTop;
  391. showItem =
  392. chart.isInsidePlot(
  393. itemPosRelativeX,
  394. itemPosRelativeY
  395. ) &&
  396. chart.isInsidePlot(
  397. itemPosRelativeX + item.width,
  398. itemPosRelativeY + item.height
  399. );
  400. }
  401. }
  402. return showItem ? itemPosition : null;
  403. }
  404. }
  405. );
  406. /* ********************************************************************** */
  407. /**
  408. * General symbol definition for labels with connector
  409. * @private
  410. */
  411. H.SVGRenderer.prototype.symbols.connector = function (x, y, w, h, options) {
  412. var anchorX = options && options.anchorX,
  413. anchorY = options && options.anchorY,
  414. path,
  415. yOffset,
  416. lateral = w / 2;
  417. if (isNumber(anchorX) && isNumber(anchorY)) {
  418. path = ['M', anchorX, anchorY];
  419. // Prefer 45 deg connectors
  420. yOffset = y - anchorY;
  421. if (yOffset < 0) {
  422. yOffset = -h - yOffset;
  423. }
  424. if (yOffset < w) {
  425. lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset;
  426. }
  427. // Anchor below label
  428. if (anchorY > y + h) {
  429. path.push('L', x + lateral, y + h);
  430. // Anchor above label
  431. } else if (anchorY < y) {
  432. path.push('L', x + lateral, y);
  433. // Anchor left of label
  434. } else if (anchorX < x) {
  435. path.push('L', x, y + h / 2);
  436. // Anchor right of label
  437. } else if (anchorX > x + w) {
  438. path.push('L', x + w, y + h / 2);
  439. }
  440. }
  441. return path || [];
  442. };
  443. export default ControllableLabel;