ControllableLabel.js 12 KB

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