ControllableLabel.js 13 KB

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