SVGElement.js 82 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191
  1. /* *
  2. *
  3. * (c) 2010-2020 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * */
  10. 'use strict';
  11. import Color from './Color.js';
  12. import H from './Globals.js';
  13. var deg2rad = H.deg2rad, doc = H.doc, hasTouch = H.hasTouch, isFirefox = H.isFirefox, noop = H.noop, svg = H.svg, SVG_NS = H.SVG_NS, win = H.win;
  14. import U from './Utilities.js';
  15. var animate = U.animate, animObject = U.animObject, attr = U.attr, createElement = U.createElement, css = U.css, defined = U.defined, erase = U.erase, extend = U.extend, fireEvent = U.fireEvent, inArray = U.inArray, isArray = U.isArray, isFunction = U.isFunction, isNumber = U.isNumber, isString = U.isString, merge = U.merge, objectEach = U.objectEach, pick = U.pick, pInt = U.pInt, stop = U.stop, uniqueKey = U.uniqueKey;
  16. /**
  17. * The horizontal alignment of an element.
  18. *
  19. * @typedef {"center"|"left"|"right"} Highcharts.AlignValue
  20. */
  21. /**
  22. * Options to align the element relative to the chart or another box.
  23. *
  24. * @interface Highcharts.AlignObject
  25. */ /**
  26. * Horizontal alignment. Can be one of `left`, `center` and `right`.
  27. *
  28. * @name Highcharts.AlignObject#align
  29. * @type {Highcharts.AlignValue|undefined}
  30. *
  31. * @default left
  32. */ /**
  33. * Vertical alignment. Can be one of `top`, `middle` and `bottom`.
  34. *
  35. * @name Highcharts.AlignObject#verticalAlign
  36. * @type {Highcharts.VerticalAlignValue|undefined}
  37. *
  38. * @default top
  39. */ /**
  40. * Horizontal pixel offset from alignment.
  41. *
  42. * @name Highcharts.AlignObject#x
  43. * @type {number|undefined}
  44. *
  45. * @default 0
  46. */ /**
  47. * Vertical pixel offset from alignment.
  48. *
  49. * @name Highcharts.AlignObject#y
  50. * @type {number|undefined}
  51. *
  52. * @default 0
  53. */ /**
  54. * Use the `transform` attribute with translateX and translateY custom
  55. * attributes to align this elements rather than `x` and `y` attributes.
  56. *
  57. * @name Highcharts.AlignObject#alignByTranslate
  58. * @type {boolean|undefined}
  59. *
  60. * @default false
  61. */
  62. /**
  63. * Bounding box of an element.
  64. *
  65. * @interface Highcharts.BBoxObject
  66. * @extends Highcharts.PositionObject
  67. */ /**
  68. * Height of the bounding box.
  69. *
  70. * @name Highcharts.BBoxObject#height
  71. * @type {number}
  72. */ /**
  73. * Width of the bounding box.
  74. *
  75. * @name Highcharts.BBoxObject#width
  76. * @type {number}
  77. */ /**
  78. * Horizontal position of the bounding box.
  79. *
  80. * @name Highcharts.BBoxObject#x
  81. * @type {number}
  82. */ /**
  83. * Vertical position of the bounding box.
  84. *
  85. * @name Highcharts.BBoxObject#y
  86. * @type {number}
  87. */
  88. /**
  89. * An object of key-value pairs for SVG attributes. Attributes in Highcharts
  90. * elements for the most parts correspond to SVG, but some are specific to
  91. * Highcharts, like `zIndex`, `rotation`, `rotationOriginX`,
  92. * `rotationOriginY`, `translateX`, `translateY`, `scaleX` and `scaleY`. SVG
  93. * attributes containing a hyphen are _not_ camel-cased, they should be
  94. * quoted to preserve the hyphen.
  95. *
  96. * @example
  97. * {
  98. * 'stroke': '#ff0000', // basic
  99. * 'stroke-width': 2, // hyphenated
  100. * 'rotation': 45 // custom
  101. * 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format
  102. * }
  103. *
  104. * @interface Highcharts.SVGAttributes
  105. */ /**
  106. * @name Highcharts.SVGAttributes#[key:string]
  107. * @type {*}
  108. */ /**
  109. * @name Highcharts.SVGAttributes#d
  110. * @type {string|Highcharts.SVGPathArray|undefined}
  111. */ /**
  112. * @name Highcharts.SVGAttributes#fill
  113. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject|undefined}
  114. */ /**
  115. * @name Highcharts.SVGAttributes#inverted
  116. * @type {boolean|undefined}
  117. */ /**
  118. * @name Highcharts.SVGAttributes#matrix
  119. * @type {Array<number>|undefined}
  120. */ /**
  121. * @name Highcharts.SVGAttributes#rotation
  122. * @type {number|undefined}
  123. */ /**
  124. * @name Highcharts.SVGAttributes#rotationOriginX
  125. * @type {number|undefined}
  126. */ /**
  127. * @name Highcharts.SVGAttributes#rotationOriginY
  128. * @type {number|undefined}
  129. */ /**
  130. * @name Highcharts.SVGAttributes#scaleX
  131. * @type {number|undefined}
  132. */ /**
  133. * @name Highcharts.SVGAttributes#scaleY
  134. * @type {number|undefined}
  135. */ /**
  136. * @name Highcharts.SVGAttributes#stroke
  137. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject|undefined}
  138. */ /**
  139. * @name Highcharts.SVGAttributes#style
  140. * @type {string|Highcharts.CSSObject|undefined}
  141. */ /**
  142. * @name Highcharts.SVGAttributes#translateX
  143. * @type {number|undefined}
  144. */ /**
  145. * @name Highcharts.SVGAttributes#translateY
  146. * @type {number|undefined}
  147. */ /**
  148. * @name Highcharts.SVGAttributes#zIndex
  149. * @type {number|undefined}
  150. */
  151. /**
  152. * An SVG DOM element. The type is a reference to the regular SVGElement in the
  153. * global scope.
  154. *
  155. * @typedef {globals.GlobalSVGElement} Highcharts.SVGDOMElement
  156. *
  157. * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGElement
  158. */
  159. /**
  160. * The vertical alignment of an element.
  161. *
  162. * @typedef {"bottom"|"middle"|"top"} Highcharts.VerticalAlignValue
  163. */
  164. ''; // detach doclets above
  165. /* eslint-disable no-invalid-this, valid-jsdoc */
  166. /**
  167. * The SVGElement prototype is a JavaScript wrapper for SVG elements used in the
  168. * rendering layer of Highcharts. Combined with the
  169. * {@link Highcharts.SVGRenderer}
  170. * object, these prototypes allow freeform annotation in the charts or even in
  171. * HTML pages without instanciating a chart. The SVGElement can also wrap HTML
  172. * labels, when `text` or `label` elements are created with the `useHTML`
  173. * parameter.
  174. *
  175. * The SVGElement instances are created through factory functions on the
  176. * {@link Highcharts.SVGRenderer}
  177. * object, like
  178. * {@link Highcharts.SVGRenderer#rect|rect},
  179. * {@link Highcharts.SVGRenderer#path|path},
  180. * {@link Highcharts.SVGRenderer#text|text},
  181. * {@link Highcharts.SVGRenderer#label|label},
  182. * {@link Highcharts.SVGRenderer#g|g}
  183. * and more.
  184. *
  185. * @class
  186. * @name Highcharts.SVGElement
  187. */
  188. var SVGElement = /** @class */ (function () {
  189. function SVGElement() {
  190. /* *
  191. *
  192. * Properties
  193. *
  194. * */
  195. this.element = void 0;
  196. this.height = void 0;
  197. this.opacity = 1; // Default base for animation
  198. this.renderer = void 0;
  199. this.SVG_NS = SVG_NS;
  200. // Custom attributes used for symbols, these should be filtered out when
  201. // setting SVGElement attributes (#9375).
  202. this.symbolCustomAttribs = [
  203. 'x',
  204. 'y',
  205. 'width',
  206. 'height',
  207. 'r',
  208. 'start',
  209. 'end',
  210. 'innerR',
  211. 'anchorX',
  212. 'anchorY',
  213. 'rounded'
  214. ];
  215. this.width = void 0;
  216. }
  217. /* *
  218. *
  219. * Functions
  220. *
  221. * */
  222. /**
  223. * Get the current value of an attribute or pseudo attribute,
  224. * used mainly for animation. Called internally from
  225. * the {@link Highcharts.SVGRenderer#attr} function.
  226. *
  227. * @private
  228. * @function Highcharts.SVGElement#_defaultGetter
  229. *
  230. * @param {string} key
  231. * Property key.
  232. *
  233. * @return {number|string}
  234. * Property value.
  235. */
  236. SVGElement.prototype._defaultGetter = function (key) {
  237. var ret = pick(this[key + 'Value'], // align getter
  238. this[key], this.element ? this.element.getAttribute(key) : null, 0);
  239. if (/^[\-0-9\.]+$/.test(ret)) { // is numerical
  240. ret = parseFloat(ret);
  241. }
  242. return ret;
  243. };
  244. /**
  245. * @private
  246. * @function Highcharts.SVGElement#_defaultSetter
  247. *
  248. * @param {string} value
  249. *
  250. * @param {string} key
  251. *
  252. * @param {Highcharts.SVGDOMElement} element
  253. *
  254. * @return {void}
  255. */
  256. SVGElement.prototype._defaultSetter = function (value, key, element) {
  257. element.setAttribute(key, value);
  258. };
  259. /**
  260. * Add the element to the DOM. All elements must be added this way.
  261. *
  262. * @sample highcharts/members/renderer-g
  263. * Elements added to a group
  264. *
  265. * @function Highcharts.SVGElement#add
  266. *
  267. * @param {Highcharts.SVGElement} [parent]
  268. * The parent item to add it to. If undefined, the element is added
  269. * to the {@link Highcharts.SVGRenderer.box}.
  270. *
  271. * @return {Highcharts.SVGElement}
  272. * Returns the SVGElement for chaining.
  273. */
  274. SVGElement.prototype.add = function (parent) {
  275. var renderer = this.renderer, element = this.element, inserted;
  276. if (parent) {
  277. this.parentGroup = parent;
  278. }
  279. // Mark as inverted
  280. this.parentInverted = parent && parent.inverted;
  281. // Build formatted text
  282. if (typeof this.textStr !== 'undefined' &&
  283. this.element.nodeName === 'text' // Not for SVGLabel instances
  284. ) {
  285. renderer.buildText(this);
  286. }
  287. // Mark as added
  288. this.added = true;
  289. // If we're adding to renderer root, or other elements in the group
  290. // have a z index, we need to handle it
  291. if (!parent || parent.handleZ || this.zIndex) {
  292. inserted = this.zIndexSetter();
  293. }
  294. // If zIndex is not handled, append at the end
  295. if (!inserted) {
  296. (parent ?
  297. parent.element :
  298. renderer.box).appendChild(element);
  299. }
  300. // fire an event for internal hooks
  301. if (this.onAdd) {
  302. this.onAdd();
  303. }
  304. return this;
  305. };
  306. /**
  307. * Add a class name to an element.
  308. *
  309. * @function Highcharts.SVGElement#addClass
  310. *
  311. * @param {string} className
  312. * The new class name to add.
  313. *
  314. * @param {boolean} [replace=false]
  315. * When true, the existing class name(s) will be overwritten with the new
  316. * one. When false, the new one is added.
  317. *
  318. * @return {Highcharts.SVGElement}
  319. * Return the SVG element for chainability.
  320. */
  321. SVGElement.prototype.addClass = function (className, replace) {
  322. var currentClassName = replace ? '' : (this.attr('class') || '');
  323. // Trim the string and remove duplicates
  324. className = (className || '')
  325. .split(/ /g)
  326. .reduce(function (newClassName, name) {
  327. if (currentClassName.indexOf(name) === -1) {
  328. newClassName.push(name);
  329. }
  330. return newClassName;
  331. }, (currentClassName ?
  332. [currentClassName] :
  333. []))
  334. .join(' ');
  335. if (className !== currentClassName) {
  336. this.attr('class', className);
  337. }
  338. return this;
  339. };
  340. /**
  341. * This method is executed in the end of `attr()`, after setting all
  342. * attributes in the hash. In can be used to efficiently consolidate
  343. * multiple attributes in one SVG property -- e.g., translate, rotate and
  344. * scale are merged in one "transform" attribute in the SVG node.
  345. *
  346. * @private
  347. * @function Highcharts.SVGElement#afterSetters
  348. */
  349. SVGElement.prototype.afterSetters = function () {
  350. // Update transform. Do this outside the loop to prevent redundant
  351. // updating for batch setting of attributes.
  352. if (this.doTransform) {
  353. this.updateTransform();
  354. this.doTransform = false;
  355. }
  356. };
  357. /**
  358. * Align the element relative to the chart or another box.
  359. *
  360. * @function Highcharts.SVGElement#align
  361. *
  362. * @param {Highcharts.AlignObject} [alignOptions]
  363. * The alignment options. The function can be called without this
  364. * parameter in order to re-align an element after the box has been
  365. * updated.
  366. *
  367. * @param {boolean} [alignByTranslate]
  368. * Align element by translation.
  369. *
  370. * @param {string|Highcharts.BBoxObject} [box]
  371. * The box to align to, needs a width and height. When the box is a
  372. * string, it refers to an object in the Renderer. For example, when
  373. * box is `spacingBox`, it refers to `Renderer.spacingBox` which
  374. * holds `width`, `height`, `x` and `y` properties.
  375. *
  376. * @return {Highcharts.SVGElement} Returns the SVGElement for chaining.
  377. */
  378. SVGElement.prototype.align = function (alignOptions, alignByTranslate, box) {
  379. var align, vAlign, x, y, attribs = {}, alignTo, renderer = this.renderer, alignedObjects = renderer.alignedObjects, alignFactor, vAlignFactor;
  380. // First call on instanciate
  381. if (alignOptions) {
  382. this.alignOptions = alignOptions;
  383. this.alignByTranslate = alignByTranslate;
  384. if (!box || isString(box)) {
  385. this.alignTo = alignTo = box || 'renderer';
  386. // prevent duplicates, like legendGroup after resize
  387. erase(alignedObjects, this);
  388. alignedObjects.push(this);
  389. box = void 0; // reassign it below
  390. }
  391. // When called on resize, no arguments are supplied
  392. }
  393. else {
  394. alignOptions = this.alignOptions;
  395. alignByTranslate = this.alignByTranslate;
  396. alignTo = this.alignTo;
  397. }
  398. box = pick(box, renderer[alignTo], renderer);
  399. // Assign variables
  400. align = alignOptions.align;
  401. vAlign = alignOptions.verticalAlign;
  402. // default: left align
  403. x = (box.x || 0) + (alignOptions.x || 0);
  404. // default: top align
  405. y = (box.y || 0) + (alignOptions.y || 0);
  406. // Align
  407. if (align === 'right') {
  408. alignFactor = 1;
  409. }
  410. else if (align === 'center') {
  411. alignFactor = 2;
  412. }
  413. if (alignFactor) {
  414. x += (box.width - (alignOptions.width || 0)) /
  415. alignFactor;
  416. }
  417. attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x);
  418. // Vertical align
  419. if (vAlign === 'bottom') {
  420. vAlignFactor = 1;
  421. }
  422. else if (vAlign === 'middle') {
  423. vAlignFactor = 2;
  424. }
  425. if (vAlignFactor) {
  426. y += (box.height - (alignOptions.height || 0)) /
  427. vAlignFactor;
  428. }
  429. attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y);
  430. // Animate only if already placed
  431. this[this.placed ? 'animate' : 'attr'](attribs);
  432. this.placed = true;
  433. this.alignAttr = attribs;
  434. return this;
  435. };
  436. /**
  437. * @private
  438. * @function Highcharts.SVGElement#alignSetter
  439. * @param {"left"|"center"|"right"} value
  440. */
  441. SVGElement.prototype.alignSetter = function (value) {
  442. var convert = {
  443. left: 'start',
  444. center: 'middle',
  445. right: 'end'
  446. };
  447. if (convert[value]) {
  448. this.alignValue = value;
  449. this.element.setAttribute('text-anchor', convert[value]);
  450. }
  451. };
  452. /**
  453. * Animate to given attributes or CSS properties.
  454. *
  455. * @sample highcharts/members/element-on/
  456. * Setting some attributes by animation
  457. *
  458. * @function Highcharts.SVGElement#animate
  459. *
  460. * @param {Highcharts.SVGAttributes} params
  461. * SVG attributes or CSS to animate.
  462. *
  463. * @param {boolean|Highcharts.AnimationOptionsObject} [options]
  464. * Animation options.
  465. *
  466. * @param {Function} [complete]
  467. * Function to perform at the end of animation.
  468. *
  469. * @return {Highcharts.SVGElement}
  470. * Returns the SVGElement for chaining.
  471. */
  472. SVGElement.prototype.animate = function (params, options, complete) {
  473. var animOptions = animObject(pick(options, this.renderer.globalAnimation, true));
  474. // When the page is hidden save resources in the background by not
  475. // running animation at all (#9749).
  476. if (pick(doc.hidden, doc.msHidden, doc.webkitHidden, false)) {
  477. animOptions.duration = 0;
  478. }
  479. if (animOptions.duration !== 0) {
  480. // allows using a callback with the global animation without
  481. // overwriting it
  482. if (complete) {
  483. animOptions.complete = complete;
  484. }
  485. animate(this, params, animOptions);
  486. }
  487. else {
  488. this.attr(params, void 0, complete);
  489. // Call the end step synchronously
  490. objectEach(params, function (val, prop) {
  491. if (animOptions.step) {
  492. animOptions.step.call(this, val, { prop: prop, pos: 1 });
  493. }
  494. }, this);
  495. }
  496. return this;
  497. };
  498. /**
  499. * Apply a text outline through a custom CSS property, by copying the text
  500. * element and apply stroke to the copy. Used internally. Contrast checks at
  501. * [example](https://jsfiddle.net/highcharts/43soe9m1/2/).
  502. *
  503. * @example
  504. * // Specific color
  505. * text.css({
  506. * textOutline: '1px black'
  507. * });
  508. * // Automatic contrast
  509. * text.css({
  510. * color: '#000000', // black text
  511. * textOutline: '1px contrast' // => white outline
  512. * });
  513. *
  514. * @private
  515. * @function Highcharts.SVGElement#applyTextOutline
  516. *
  517. * @param {string} textOutline
  518. * A custom CSS `text-outline` setting, defined by `width color`.
  519. */
  520. SVGElement.prototype.applyTextOutline = function (textOutline) {
  521. var elem = this.element, tspans, hasContrast = textOutline.indexOf('contrast') !== -1, styles = {}, color, strokeWidth, firstRealChild;
  522. // When the text shadow is set to contrast, use dark stroke for light
  523. // text and vice versa.
  524. if (hasContrast) {
  525. styles.textOutline = textOutline = textOutline.replace(/contrast/g, this.renderer.getContrast(elem.style.fill));
  526. }
  527. // Extract the stroke width and color
  528. textOutline = textOutline.split(' ');
  529. color = textOutline[textOutline.length - 1];
  530. strokeWidth = textOutline[0];
  531. if (strokeWidth && strokeWidth !== 'none' && H.svg) {
  532. this.fakeTS = true; // Fake text shadow
  533. tspans = [].slice.call(elem.getElementsByTagName('tspan'));
  534. // In order to get the right y position of the clone,
  535. // copy over the y setter
  536. this.ySetter = this.xSetter;
  537. // Since the stroke is applied on center of the actual outline, we
  538. // need to double it to get the correct stroke-width outside the
  539. // glyphs.
  540. strokeWidth = strokeWidth.replace(/(^[\d\.]+)(.*?)$/g, function (match, digit, unit) {
  541. return (2 * digit) + unit;
  542. });
  543. // Remove shadows from previous runs.
  544. this.removeTextOutline(tspans);
  545. // Check if the element contains RTL characters.
  546. // Comparing against Hebrew and Arabic characters,
  547. // excluding Arabic digits. Source:
  548. // https://www.unicode.org/Public/UNIDATA/extracted/DerivedBidiClass.txt
  549. var isRTL_1 = elem.textContent ?
  550. /^[\u0591-\u065F\u066A-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/
  551. .test(elem.textContent) : false;
  552. // For each of the tspans, create a stroked copy behind it.
  553. firstRealChild = elem.firstChild;
  554. tspans.forEach(function (tspan, y) {
  555. var clone;
  556. // Let the first line start at the correct X position
  557. if (y === 0) {
  558. tspan.setAttribute('x', elem.getAttribute('x'));
  559. y = elem.getAttribute('y');
  560. tspan.setAttribute('y', y || 0);
  561. if (y === null) {
  562. elem.setAttribute('y', 0);
  563. }
  564. }
  565. // Create the clone and apply outline properties.
  566. // For RTL elements apply outline properties for orginal element
  567. // to prevent outline from overlapping the text.
  568. // For RTL in Firefox keep the orginal order (#10162).
  569. clone = tspan.cloneNode(true);
  570. attr((isRTL_1 && !isFirefox) ? tspan : clone, {
  571. 'class': 'highcharts-text-outline',
  572. fill: color,
  573. stroke: color,
  574. 'stroke-width': strokeWidth,
  575. 'stroke-linejoin': 'round'
  576. });
  577. elem.insertBefore(clone, firstRealChild);
  578. });
  579. // Create a whitespace between tspan and clone,
  580. // to fix the display of Arabic characters in Firefox.
  581. if (isRTL_1 && isFirefox && tspans[0]) {
  582. var whitespace = tspans[0].cloneNode(true);
  583. whitespace.textContent = ' ';
  584. elem.insertBefore(whitespace, firstRealChild);
  585. }
  586. }
  587. };
  588. /**
  589. * @function Highcharts.SVGElement#attr
  590. * @param {string} key
  591. * @return {number|string}
  592. */ /**
  593. * Apply native and custom attributes to the SVG elements.
  594. *
  595. * In order to set the rotation center for rotation, set x and y to 0 and
  596. * use `translateX` and `translateY` attributes to position the element
  597. * instead.
  598. *
  599. * Attributes frequently used in Highcharts are `fill`, `stroke`,
  600. * `stroke-width`.
  601. *
  602. * @sample highcharts/members/renderer-rect/
  603. * Setting some attributes
  604. *
  605. * @example
  606. * // Set multiple attributes
  607. * element.attr({
  608. * stroke: 'red',
  609. * fill: 'blue',
  610. * x: 10,
  611. * y: 10
  612. * });
  613. *
  614. * // Set a single attribute
  615. * element.attr('stroke', 'red');
  616. *
  617. * // Get an attribute
  618. * element.attr('stroke'); // => 'red'
  619. *
  620. * @function Highcharts.SVGElement#attr
  621. *
  622. * @param {string|Highcharts.SVGAttributes} [hash]
  623. * The native and custom SVG attributes.
  624. *
  625. * @param {number|string|Highcharts.SVGPathArray} [val]
  626. * If the type of the first argument is `string`, the second can be a
  627. * value, which will serve as a single attribute setter. If the first
  628. * argument is a string and the second is undefined, the function
  629. * serves as a getter and the current value of the property is
  630. * returned.
  631. *
  632. * @param {Function} [complete]
  633. * A callback function to execute after setting the attributes. This
  634. * makes the function compliant and interchangeable with the
  635. * {@link SVGElement#animate} function.
  636. *
  637. * @param {boolean} [continueAnimation=true]
  638. * Used internally when `.attr` is called as part of an animation
  639. * step. Otherwise, calling `.attr` for an attribute will stop
  640. * animation for that attribute.
  641. *
  642. * @return {Highcharts.SVGElement}
  643. * If used as a setter, it returns the current
  644. * {@link Highcharts.SVGElement} so the calls can be chained. If
  645. * used as a getter, the current value of the attribute is returned.
  646. */
  647. SVGElement.prototype.attr = function (hash, val, complete, continueAnimation) {
  648. var key, element = this.element, hasSetSymbolSize, ret = this, skipAttr, setter, symbolCustomAttribs = this.symbolCustomAttribs;
  649. // single key-value pair
  650. if (typeof hash === 'string' && typeof val !== 'undefined') {
  651. key = hash;
  652. hash = {};
  653. hash[key] = val;
  654. }
  655. // used as a getter: first argument is a string, second is undefined
  656. if (typeof hash === 'string') {
  657. ret = (this[hash + 'Getter'] ||
  658. this._defaultGetter).call(this, hash, element);
  659. // setter
  660. }
  661. else {
  662. objectEach(hash, function eachAttribute(val, key) {
  663. skipAttr = false;
  664. // Unless .attr is from the animator update, stop current
  665. // running animation of this property
  666. if (!continueAnimation) {
  667. stop(this, key);
  668. }
  669. // Special handling of symbol attributes
  670. if (this.symbolName &&
  671. symbolCustomAttribs.indexOf(key) !== -1) {
  672. if (!hasSetSymbolSize) {
  673. this.symbolAttr(hash);
  674. hasSetSymbolSize = true;
  675. }
  676. skipAttr = true;
  677. }
  678. if (this.rotation && (key === 'x' || key === 'y')) {
  679. this.doTransform = true;
  680. }
  681. if (!skipAttr) {
  682. setter = (this[key + 'Setter'] ||
  683. this._defaultSetter);
  684. setter.call(this, val, key, element);
  685. // Let the shadow follow the main element
  686. if (!this.styledMode &&
  687. this.shadows &&
  688. /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
  689. this.updateShadows(key, val, setter);
  690. }
  691. }
  692. }, this);
  693. this.afterSetters();
  694. }
  695. // In accordance with animate, run a complete callback
  696. if (complete) {
  697. complete.call(this);
  698. }
  699. return ret;
  700. };
  701. /**
  702. * Apply a clipping rectangle to this element.
  703. *
  704. * @function Highcharts.SVGElement#clip
  705. *
  706. * @param {Highcharts.ClipRectElement} [clipRect]
  707. * The clipping rectangle. If skipped, the current clip is removed.
  708. *
  709. * @return {Highcharts.SVGElement}
  710. * Returns the SVG element to allow chaining.
  711. */
  712. SVGElement.prototype.clip = function (clipRect) {
  713. return this.attr('clip-path', clipRect ?
  714. 'url(' + this.renderer.url + '#' + clipRect.id + ')' :
  715. 'none');
  716. };
  717. /**
  718. * Calculate the coordinates needed for drawing a rectangle crisply and
  719. * return the calculated attributes.
  720. *
  721. * @function Highcharts.SVGElement#crisp
  722. *
  723. * @param {Highcharts.RectangleObject} rect
  724. * Rectangle to crisp.
  725. *
  726. * @param {number} [strokeWidth]
  727. * The stroke width to consider when computing crisp positioning. It can
  728. * also be set directly on the rect parameter.
  729. *
  730. * @return {Highcharts.RectangleObject}
  731. * The modified rectangle arguments.
  732. */
  733. SVGElement.prototype.crisp = function (rect, strokeWidth) {
  734. var wrapper = this, normalizer;
  735. strokeWidth = strokeWidth || rect.strokeWidth || 0;
  736. // Math.round because strokeWidth can sometimes have roundoff errors
  737. normalizer = Math.round(strokeWidth) % 2 / 2;
  738. // normalize for crisp edges
  739. rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer;
  740. rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer;
  741. rect.width = Math.floor((rect.width || wrapper.width || 0) - 2 * normalizer);
  742. rect.height = Math.floor((rect.height || wrapper.height || 0) - 2 * normalizer);
  743. if (defined(rect.strokeWidth)) {
  744. rect.strokeWidth = strokeWidth;
  745. }
  746. return rect;
  747. };
  748. /**
  749. * Build and apply an SVG gradient out of a common JavaScript configuration
  750. * object. This function is called from the attribute setters. An event
  751. * hook is added for supporting other complex color types.
  752. *
  753. * @private
  754. * @function Highcharts.SVGElement#complexColor
  755. *
  756. * @param {Highcharts.GradientColorObject|Highcharts.PatternObject} colorOptions
  757. * The gradient or pattern options structure.
  758. *
  759. * @param {string} prop
  760. * The property to apply, can either be `fill` or `stroke`.
  761. *
  762. * @param {Highcharts.SVGDOMElement} elem
  763. * SVG element to apply the gradient on.
  764. */
  765. SVGElement.prototype.complexColor = function (colorOptions, prop, elem) {
  766. var renderer = this.renderer, colorObject, gradName, gradAttr, radAttr, gradients, stops, stopColor, stopOpacity, radialReference, id, key = [], value;
  767. fireEvent(this.renderer, 'complexColor', {
  768. args: arguments
  769. }, function () {
  770. // Apply linear or radial gradients
  771. if (colorOptions.radialGradient) {
  772. gradName = 'radialGradient';
  773. }
  774. else if (colorOptions.linearGradient) {
  775. gradName = 'linearGradient';
  776. }
  777. if (gradName) {
  778. gradAttr = colorOptions[gradName];
  779. gradients = renderer.gradients;
  780. stops = colorOptions.stops;
  781. radialReference = elem.radialReference;
  782. // Keep < 2.2 kompatibility
  783. if (isArray(gradAttr)) {
  784. colorOptions[gradName] = gradAttr = {
  785. x1: gradAttr[0],
  786. y1: gradAttr[1],
  787. x2: gradAttr[2],
  788. y2: gradAttr[3],
  789. gradientUnits: 'userSpaceOnUse'
  790. };
  791. }
  792. // Correct the radial gradient for the radial reference system
  793. if (gradName === 'radialGradient' &&
  794. radialReference &&
  795. !defined(gradAttr.gradientUnits)) {
  796. // Save the radial attributes for updating
  797. radAttr = gradAttr;
  798. gradAttr = merge(gradAttr, renderer.getRadialAttr(radialReference, radAttr), { gradientUnits: 'userSpaceOnUse' });
  799. }
  800. // Build the unique key to detect whether we need to create a
  801. // new element (#1282)
  802. objectEach(gradAttr, function (val, n) {
  803. if (n !== 'id') {
  804. key.push(n, val);
  805. }
  806. });
  807. objectEach(stops, function (val) {
  808. key.push(val);
  809. });
  810. key = key.join(',');
  811. // Check if a gradient object with the same config object is
  812. // created within this renderer
  813. if (gradients[key]) {
  814. id = gradients[key].attr('id');
  815. }
  816. else {
  817. // Set the id and create the element
  818. gradAttr.id = id = uniqueKey();
  819. var gradientObject_1 = gradients[key] =
  820. renderer.createElement(gradName)
  821. .attr(gradAttr)
  822. .add(renderer.defs);
  823. gradientObject_1.radAttr = radAttr;
  824. // The gradient needs to keep a list of stops to be able to
  825. // destroy them
  826. gradientObject_1.stops = [];
  827. stops.forEach(function (stop) {
  828. var stopObject;
  829. if (stop[1].indexOf('rgba') === 0) {
  830. colorObject = Color.parse(stop[1]);
  831. stopColor = colorObject.get('rgb');
  832. stopOpacity = colorObject.get('a');
  833. }
  834. else {
  835. stopColor = stop[1];
  836. stopOpacity = 1;
  837. }
  838. stopObject = renderer.createElement('stop').attr({
  839. offset: stop[0],
  840. 'stop-color': stopColor,
  841. 'stop-opacity': stopOpacity
  842. }).add(gradientObject_1);
  843. // Add the stop element to the gradient
  844. gradientObject_1.stops.push(stopObject);
  845. });
  846. }
  847. // Set the reference to the gradient object
  848. value = 'url(' + renderer.url + '#' + id + ')';
  849. elem.setAttribute(prop, value);
  850. elem.gradient = key;
  851. // Allow the color to be concatenated into tooltips formatters
  852. // etc. (#2995)
  853. colorOptions.toString = function () {
  854. return value;
  855. };
  856. }
  857. });
  858. };
  859. /**
  860. * Set styles for the element. In addition to CSS styles supported by
  861. * native SVG and HTML elements, there are also some custom made for
  862. * Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text
  863. * elements.
  864. *
  865. * @sample highcharts/members/renderer-text-on-chart/
  866. * Styled text
  867. *
  868. * @function Highcharts.SVGElement#css
  869. *
  870. * @param {Highcharts.CSSObject} styles
  871. * The new CSS styles.
  872. *
  873. * @return {Highcharts.SVGElement}
  874. * Return the SVG element for chaining.
  875. */
  876. SVGElement.prototype.css = function (styles) {
  877. var oldStyles = this.styles, newStyles = {}, elem = this.element, textWidth, serializedCss = '', hyphenate, hasNew = !oldStyles,
  878. // These CSS properties are interpreted internally by the SVG
  879. // renderer, but are not supported by SVG and should not be added to
  880. // the DOM. In styled mode, no CSS should find its way to the DOM
  881. // whatsoever (#6173, #6474).
  882. svgPseudoProps = ['textOutline', 'textOverflow', 'width'];
  883. // convert legacy
  884. if (styles && styles.color) {
  885. styles.fill = styles.color;
  886. }
  887. // Filter out existing styles to increase performance (#2640)
  888. if (oldStyles) {
  889. objectEach(styles, function (style, n) {
  890. if (oldStyles && oldStyles[n] !== style) {
  891. newStyles[n] = style;
  892. hasNew = true;
  893. }
  894. });
  895. }
  896. if (hasNew) {
  897. // Merge the new styles with the old ones
  898. if (oldStyles) {
  899. styles = extend(oldStyles, newStyles);
  900. }
  901. // Get the text width from style
  902. if (styles) {
  903. // Previously set, unset it (#8234)
  904. if (styles.width === null || styles.width === 'auto') {
  905. delete this.textWidth;
  906. // Apply new
  907. }
  908. else if (elem.nodeName.toLowerCase() === 'text' &&
  909. styles.width) {
  910. textWidth = this.textWidth = pInt(styles.width);
  911. }
  912. }
  913. // store object
  914. this.styles = styles;
  915. if (textWidth && (!svg && this.renderer.forExport)) {
  916. delete styles.width;
  917. }
  918. // Serialize and set style attribute
  919. if (elem.namespaceURI === this.SVG_NS) { // #7633
  920. hyphenate = function (a, b) {
  921. return '-' + b.toLowerCase();
  922. };
  923. objectEach(styles, function (style, n) {
  924. if (svgPseudoProps.indexOf(n) === -1) {
  925. serializedCss +=
  926. n.replace(/([A-Z])/g, hyphenate) + ':' +
  927. style + ';';
  928. }
  929. });
  930. if (serializedCss) {
  931. attr(elem, 'style', serializedCss); // #1881
  932. }
  933. }
  934. else {
  935. css(elem, styles);
  936. }
  937. if (this.added) {
  938. // Rebuild text after added. Cache mechanisms in the buildText
  939. // will prevent building if there are no significant changes.
  940. if (this.element.nodeName === 'text') {
  941. this.renderer.buildText(this);
  942. }
  943. // Apply text outline after added
  944. if (styles && styles.textOutline) {
  945. this.applyTextOutline(styles.textOutline);
  946. }
  947. }
  948. }
  949. return this;
  950. };
  951. /**
  952. * @private
  953. * @function Highcharts.SVGElement#dashstyleSetter
  954. * @param {string} value
  955. */
  956. SVGElement.prototype.dashstyleSetter = function (value) {
  957. var i, strokeWidth = this['stroke-width'];
  958. // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new
  959. // strokeWidth function, we should be able to use that instead.
  960. if (strokeWidth === 'inherit') {
  961. strokeWidth = 1;
  962. }
  963. value = value && value.toLowerCase();
  964. if (value) {
  965. var v = value
  966. .replace('shortdashdotdot', '3,1,1,1,1,1,')
  967. .replace('shortdashdot', '3,1,1,1')
  968. .replace('shortdot', '1,1,')
  969. .replace('shortdash', '3,1,')
  970. .replace('longdash', '8,3,')
  971. .replace(/dot/g, '1,3,')
  972. .replace('dash', '4,3,')
  973. .replace(/,$/, '')
  974. .split(','); // ending comma
  975. i = v.length;
  976. while (i--) {
  977. v[i] = '' + (pInt(v[i]) * pick(strokeWidth, NaN));
  978. }
  979. value = v.join(',').replace(/NaN/g, 'none'); // #3226
  980. this.element.setAttribute('stroke-dasharray', value);
  981. }
  982. };
  983. /**
  984. * Destroy the element and element wrapper and clear up the DOM and event
  985. * hooks.
  986. *
  987. * @function Highcharts.SVGElement#destroy
  988. */
  989. SVGElement.prototype.destroy = function () {
  990. var wrapper = this, element = wrapper.element || {}, renderer = wrapper.renderer, parentToClean = (renderer.isSVG &&
  991. element.nodeName === 'SPAN' &&
  992. wrapper.parentGroup ||
  993. void 0), grandParent, ownerSVGElement = element.ownerSVGElement, i;
  994. // remove events
  995. element.onclick = element.onmouseout = element.onmouseover =
  996. element.onmousemove = element.point = null;
  997. stop(wrapper); // stop running animations
  998. if (wrapper.clipPath && ownerSVGElement) {
  999. var clipPath_1 = wrapper.clipPath;
  1000. // Look for existing references to this clipPath and remove them
  1001. // before destroying the element (#6196).
  1002. // The upper case version is for Edge
  1003. [].forEach.call(ownerSVGElement.querySelectorAll('[clip-path],[CLIP-PATH]'), function (el) {
  1004. var clipPathAttr = el.getAttribute('clip-path');
  1005. if (clipPathAttr.indexOf(clipPath_1.element.id) > -1) {
  1006. el.removeAttribute('clip-path');
  1007. }
  1008. });
  1009. wrapper.clipPath = clipPath_1.destroy();
  1010. }
  1011. // Destroy stops in case this is a gradient object @todo old code?
  1012. if (wrapper.stops) {
  1013. for (i = 0; i < wrapper.stops.length; i++) {
  1014. wrapper.stops[i].destroy();
  1015. }
  1016. wrapper.stops.length = 0;
  1017. wrapper.stops = void 0;
  1018. }
  1019. // remove element
  1020. wrapper.safeRemoveChild(element);
  1021. if (!renderer.styledMode) {
  1022. wrapper.destroyShadows();
  1023. }
  1024. // In case of useHTML, clean up empty containers emulating SVG groups
  1025. // (#1960, #2393, #2697).
  1026. while (parentToClean &&
  1027. parentToClean.div &&
  1028. parentToClean.div.childNodes.length === 0) {
  1029. grandParent = parentToClean.parentGroup;
  1030. wrapper.safeRemoveChild(parentToClean.div);
  1031. delete parentToClean.div;
  1032. parentToClean = grandParent;
  1033. }
  1034. // remove from alignObjects
  1035. if (wrapper.alignTo) {
  1036. erase(renderer.alignedObjects, wrapper);
  1037. }
  1038. objectEach(wrapper, function (val, key) {
  1039. // Destroy child elements of a group
  1040. if (wrapper[key] &&
  1041. wrapper[key].parentGroup === wrapper &&
  1042. wrapper[key].destroy) {
  1043. wrapper[key].destroy();
  1044. }
  1045. // Delete all properties
  1046. delete wrapper[key];
  1047. });
  1048. return;
  1049. };
  1050. /**
  1051. * Destroy shadows on the element.
  1052. *
  1053. * @private
  1054. * @function Highcharts.SVGElement#destroyShadows
  1055. *
  1056. * @return {void}
  1057. */
  1058. SVGElement.prototype.destroyShadows = function () {
  1059. (this.shadows || []).forEach(function (shadow) {
  1060. this.safeRemoveChild(shadow);
  1061. }, this);
  1062. this.shadows = void 0;
  1063. };
  1064. /**
  1065. * @private
  1066. */
  1067. SVGElement.prototype.destroyTextPath = function (elem, path) {
  1068. var textElement = elem.getElementsByTagName('text')[0];
  1069. var tspans;
  1070. if (textElement) {
  1071. // Remove textPath attributes
  1072. textElement.removeAttribute('dx');
  1073. textElement.removeAttribute('dy');
  1074. // Remove ID's:
  1075. path.element.setAttribute('id', '');
  1076. // Check if textElement includes textPath,
  1077. if (this.textPathWrapper &&
  1078. textElement.getElementsByTagName('textPath').length) {
  1079. // Move nodes to <text>
  1080. tspans = this.textPathWrapper.element.childNodes;
  1081. // Now move all <tspan>'s to the <textPath> node
  1082. while (tspans.length) {
  1083. textElement.appendChild(tspans[0]);
  1084. }
  1085. // Remove <textPath> from the DOM
  1086. textElement.removeChild(this.textPathWrapper.element);
  1087. }
  1088. }
  1089. else if (elem.getAttribute('dx') || elem.getAttribute('dy')) {
  1090. // Remove textPath attributes from elem
  1091. // to get correct text-outline position
  1092. elem.removeAttribute('dx');
  1093. elem.removeAttribute('dy');
  1094. }
  1095. if (this.textPathWrapper) {
  1096. // Set textPathWrapper to undefined and destroy it
  1097. this.textPathWrapper = this.textPathWrapper.destroy();
  1098. }
  1099. };
  1100. /**
  1101. * @private
  1102. * @function Highcharts.SVGElement#dSettter
  1103. * @param {number|string|Highcharts.SVGPathArray} value
  1104. * @param {string} key
  1105. * @param {Highcharts.SVGDOMElement} element
  1106. */
  1107. SVGElement.prototype.dSetter = function (value, key, element) {
  1108. if (isArray(value)) {
  1109. // Backwards compatibility, convert one-dimensional array into an
  1110. // array of segments
  1111. if (typeof value[0] === 'string') {
  1112. value = this.renderer.pathToSegments(value);
  1113. }
  1114. this.pathArray = value;
  1115. value = value.reduce(function (acc, seg, i) {
  1116. if (!seg || !seg.join) {
  1117. return (seg || '').toString();
  1118. }
  1119. return (i ? acc + ' ' : '') + seg.join(' ');
  1120. }, '');
  1121. }
  1122. if (/(NaN| {2}|^$)/.test(value)) {
  1123. value = 'M 0 0';
  1124. }
  1125. // Check for cache before resetting. Resetting causes disturbance in the
  1126. // DOM, causing flickering in some cases in Edge/IE (#6747). Also
  1127. // possible performance gain.
  1128. if (this[key] !== value) {
  1129. element.setAttribute(key, value);
  1130. this[key] = value;
  1131. }
  1132. };
  1133. /**
  1134. * Fade out an element by animating its opacity down to 0, and hide it on
  1135. * complete. Used internally for the tooltip.
  1136. *
  1137. * @function Highcharts.SVGElement#fadeOut
  1138. *
  1139. * @param {number} [duration=150]
  1140. * The fade duration in milliseconds.
  1141. */
  1142. SVGElement.prototype.fadeOut = function (duration) {
  1143. var elemWrapper = this;
  1144. elemWrapper.animate({
  1145. opacity: 0
  1146. }, {
  1147. duration: pick(duration, 150),
  1148. complete: function () {
  1149. // #3088, assuming we're only using this for tooltips
  1150. elemWrapper.attr({ y: -9999 }).hide();
  1151. }
  1152. });
  1153. };
  1154. /**
  1155. * @private
  1156. * @function Highcharts.SVGElement#fillSetter
  1157. * @param {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} value
  1158. * @param {string} key
  1159. * @param {Highcharts.SVGDOMElement} element
  1160. */
  1161. SVGElement.prototype.fillSetter = function (value, key, element) {
  1162. if (typeof value === 'string') {
  1163. element.setAttribute(key, value);
  1164. }
  1165. else if (value) {
  1166. this.complexColor(value, key, element);
  1167. }
  1168. };
  1169. /**
  1170. * Get the bounding box (width, height, x and y) for the element. Generally
  1171. * used to get rendered text size. Since this is called a lot in charts,
  1172. * the results are cached based on text properties, in order to save DOM
  1173. * traffic. The returned bounding box includes the rotation, so for example
  1174. * a single text line of rotation 90 will report a greater height, and a
  1175. * width corresponding to the line-height.
  1176. *
  1177. * @sample highcharts/members/renderer-on-chart/
  1178. * Draw a rectangle based on a text's bounding box
  1179. *
  1180. * @function Highcharts.SVGElement#getBBox
  1181. *
  1182. * @param {boolean} [reload]
  1183. * Skip the cache and get the updated DOM bouding box.
  1184. *
  1185. * @param {number} [rot]
  1186. * Override the element's rotation. This is internally used on axis
  1187. * labels with a value of 0 to find out what the bounding box would
  1188. * be have been if it were not rotated.
  1189. *
  1190. * @return {Highcharts.BBoxObject}
  1191. * The bounding box with `x`, `y`, `width` and `height` properties.
  1192. */
  1193. SVGElement.prototype.getBBox = function (reload, rot) {
  1194. var wrapper = this, bBox, // = wrapper.bBox,
  1195. renderer = wrapper.renderer, width, height, element = wrapper.element, styles = wrapper.styles, fontSize, textStr = wrapper.textStr, toggleTextShadowShim, cache = renderer.cache, cacheKeys = renderer.cacheKeys, isSVG = element.namespaceURI === wrapper.SVG_NS, cacheKey;
  1196. var rotation = pick(rot, wrapper.rotation, 0);
  1197. fontSize = renderer.styledMode ? (element &&
  1198. SVGElement.prototype.getStyle.call(element, 'font-size')) : (styles && styles.fontSize);
  1199. // Avoid undefined and null (#7316)
  1200. if (defined(textStr)) {
  1201. cacheKey = textStr.toString();
  1202. // Since numbers are monospaced, and numerical labels appear a lot
  1203. // in a chart, we assume that a label of n characters has the same
  1204. // bounding box as others of the same length. Unless there is inner
  1205. // HTML in the label. In that case, leave the numbers as is (#5899).
  1206. if (cacheKey.indexOf('<') === -1) {
  1207. cacheKey = cacheKey.replace(/[0-9]/g, '0');
  1208. }
  1209. // Properties that affect bounding box
  1210. cacheKey += [
  1211. '',
  1212. rotation,
  1213. fontSize,
  1214. wrapper.textWidth,
  1215. styles && styles.textOverflow,
  1216. styles && styles.fontWeight // #12163
  1217. ].join(',');
  1218. }
  1219. if (cacheKey && !reload) {
  1220. bBox = cache[cacheKey];
  1221. }
  1222. // No cache found
  1223. if (!bBox) {
  1224. // SVG elements
  1225. if (isSVG || renderer.forExport) {
  1226. try { // Fails in Firefox if the container has display: none.
  1227. // When the text shadow shim is used, we need to hide the
  1228. // fake shadows to get the correct bounding box (#3872)
  1229. toggleTextShadowShim = this.fakeTS && function (display) {
  1230. [].forEach.call(element.querySelectorAll('.highcharts-text-outline'), function (tspan) {
  1231. tspan.style.display = display;
  1232. });
  1233. };
  1234. // Workaround for #3842, Firefox reporting wrong bounding
  1235. // box for shadows
  1236. if (isFunction(toggleTextShadowShim)) {
  1237. toggleTextShadowShim('none');
  1238. }
  1239. bBox = element.getBBox ?
  1240. // SVG: use extend because IE9 is not allowed to change
  1241. // width and height in case of rotation (below)
  1242. extend({}, element.getBBox()) : {
  1243. // Legacy IE in export mode
  1244. width: element.offsetWidth,
  1245. height: element.offsetHeight
  1246. };
  1247. // #3842
  1248. if (isFunction(toggleTextShadowShim)) {
  1249. toggleTextShadowShim('');
  1250. }
  1251. }
  1252. catch (e) {
  1253. '';
  1254. }
  1255. // If the bBox is not set, the try-catch block above failed. The
  1256. // other condition is for Opera that returns a width of
  1257. // -Infinity on hidden elements.
  1258. if (!bBox || bBox.width < 0) {
  1259. bBox = { width: 0, height: 0 };
  1260. }
  1261. // VML Renderer or useHTML within SVG
  1262. }
  1263. else {
  1264. bBox = wrapper.htmlGetBBox();
  1265. }
  1266. // True SVG elements as well as HTML elements in modern browsers
  1267. // using the .useHTML option need to compensated for rotation
  1268. if (renderer.isSVG) {
  1269. width = bBox.width;
  1270. height = bBox.height;
  1271. // Workaround for wrong bounding box in IE, Edge and Chrome on
  1272. // Windows. With Highcharts' default font, IE and Edge report
  1273. // a box height of 16.899 and Chrome rounds it to 17. If this
  1274. // stands uncorrected, it results in more padding added below
  1275. // the text than above when adding a label border or background.
  1276. // Also vertical positioning is affected.
  1277. // https://jsfiddle.net/highcharts/em37nvuj/
  1278. // (#1101, #1505, #1669, #2568, #6213).
  1279. if (isSVG) {
  1280. bBox.height = height = ({
  1281. '11px,17': 14,
  1282. '13px,20': 16
  1283. }[styles &&
  1284. styles.fontSize + ',' + Math.round(height)] ||
  1285. height);
  1286. }
  1287. // Adjust for rotated text
  1288. if (rotation) {
  1289. var rad = rotation * deg2rad;
  1290. bBox.width = Math.abs(height * Math.sin(rad)) +
  1291. Math.abs(width * Math.cos(rad));
  1292. bBox.height = Math.abs(height * Math.cos(rad)) +
  1293. Math.abs(width * Math.sin(rad));
  1294. }
  1295. }
  1296. // Cache it. When loading a chart in a hidden iframe in Firefox and
  1297. // IE/Edge, the bounding box height is 0, so don't cache it (#5620).
  1298. if (cacheKey && bBox.height > 0) {
  1299. // Rotate (#4681)
  1300. while (cacheKeys.length > 250) {
  1301. delete cache[cacheKeys.shift()];
  1302. }
  1303. if (!cache[cacheKey]) {
  1304. cacheKeys.push(cacheKey);
  1305. }
  1306. cache[cacheKey] = bBox;
  1307. }
  1308. }
  1309. return bBox;
  1310. };
  1311. /**
  1312. * Get the computed style. Only in styled mode.
  1313. *
  1314. * @example
  1315. * chart.series[0].points[0].graphic.getStyle('stroke-width'); // => '1px'
  1316. *
  1317. * @function Highcharts.SVGElement#getStyle
  1318. *
  1319. * @param {string} prop
  1320. * The property name to check for.
  1321. *
  1322. * @return {string}
  1323. * The current computed value.
  1324. */
  1325. SVGElement.prototype.getStyle = function (prop) {
  1326. return win
  1327. .getComputedStyle(this.element || this, '')
  1328. .getPropertyValue(prop);
  1329. };
  1330. /**
  1331. * Check if an element has the given class name.
  1332. *
  1333. * @function Highcharts.SVGElement#hasClass
  1334. *
  1335. * @param {string} className
  1336. * The class name to check for.
  1337. *
  1338. * @return {boolean}
  1339. * Whether the class name is found.
  1340. */
  1341. SVGElement.prototype.hasClass = function (className) {
  1342. return ('' + this.attr('class'))
  1343. .split(' ')
  1344. .indexOf(className) !== -1;
  1345. };
  1346. /**
  1347. * Hide the element, similar to setting the `visibility` attribute to
  1348. * `hidden`.
  1349. *
  1350. * @function Highcharts.SVGElement#hide
  1351. *
  1352. * @param {boolean} [hideByTranslation=false]
  1353. * The flag to determine if element should be hidden by moving out
  1354. * of the viewport. Used for example for dataLabels.
  1355. *
  1356. * @return {Highcharts.SVGElement}
  1357. * Returns the SVGElement for chaining.
  1358. */
  1359. SVGElement.prototype.hide = function (hideByTranslation) {
  1360. if (hideByTranslation) {
  1361. this.attr({ y: -9999 });
  1362. }
  1363. else {
  1364. this.attr({ visibility: 'hidden' });
  1365. }
  1366. return this;
  1367. };
  1368. /**
  1369. * @private
  1370. */
  1371. SVGElement.prototype.htmlGetBBox = function () {
  1372. return { height: 0, width: 0, x: 0, y: 0 };
  1373. };
  1374. /**
  1375. * Initialize the SVG element. This function only exists to make the
  1376. * initialization process overridable. It should not be called directly.
  1377. *
  1378. * @function Highcharts.SVGElement#init
  1379. *
  1380. * @param {Highcharts.SVGRenderer} renderer
  1381. * The SVGRenderer instance to initialize to.
  1382. *
  1383. * @param {string} nodeName
  1384. * The SVG node name.
  1385. */
  1386. SVGElement.prototype.init = function (renderer, nodeName) {
  1387. /**
  1388. * The primary DOM node. Each `SVGElement` instance wraps a main DOM
  1389. * node, but may also represent more nodes.
  1390. *
  1391. * @name Highcharts.SVGElement#element
  1392. * @type {Highcharts.SVGDOMElement|Highcharts.HTMLDOMElement}
  1393. */
  1394. this.element = nodeName === 'span' ?
  1395. createElement(nodeName) :
  1396. doc.createElementNS(this.SVG_NS, nodeName);
  1397. /**
  1398. * The renderer that the SVGElement belongs to.
  1399. *
  1400. * @name Highcharts.SVGElement#renderer
  1401. * @type {Highcharts.SVGRenderer}
  1402. */
  1403. this.renderer = renderer;
  1404. fireEvent(this, 'afterInit');
  1405. };
  1406. /**
  1407. * Invert a group, rotate and flip. This is used internally on inverted
  1408. * charts, where the points and graphs are drawn as if not inverted, then
  1409. * the series group elements are inverted.
  1410. *
  1411. * @function Highcharts.SVGElement#invert
  1412. *
  1413. * @param {boolean} inverted
  1414. * Whether to invert or not. An inverted shape can be un-inverted by
  1415. * setting it to false.
  1416. *
  1417. * @return {Highcharts.SVGElement}
  1418. * Return the SVGElement for chaining.
  1419. */
  1420. SVGElement.prototype.invert = function (inverted) {
  1421. var wrapper = this;
  1422. wrapper.inverted = inverted;
  1423. wrapper.updateTransform();
  1424. return wrapper;
  1425. };
  1426. /**
  1427. * Add an event listener. This is a simple setter that replaces all other
  1428. * events of the same type, opposed to the {@link Highcharts#addEvent}
  1429. * function.
  1430. *
  1431. * @sample highcharts/members/element-on/
  1432. * A clickable rectangle
  1433. *
  1434. * @function Highcharts.SVGElement#on
  1435. *
  1436. * @param {string} eventType
  1437. * The event type. If the type is `click`, Highcharts will internally
  1438. * translate it to a `touchstart` event on touch devices, to prevent the
  1439. * browser from waiting for a click event from firing.
  1440. *
  1441. * @param {Function} handler
  1442. * The handler callback.
  1443. *
  1444. * @return {Highcharts.SVGElement}
  1445. * The SVGElement for chaining.
  1446. */
  1447. SVGElement.prototype.on = function (eventType, handler) {
  1448. var svgElement = this, element = svgElement.element, touchStartPos, touchEventFired;
  1449. // touch
  1450. if (hasTouch && eventType === 'click') {
  1451. element.ontouchstart = function (e) {
  1452. // save touch position for later calculation
  1453. touchStartPos = {
  1454. clientX: e.touches[0].clientX,
  1455. clientY: e.touches[0].clientY
  1456. };
  1457. };
  1458. // Instead of ontouchstart, event handlers should be called
  1459. // on touchend - similar to how current mouseup events are called
  1460. element.ontouchend = function (e) {
  1461. // hasMoved is a boolean variable containing logic if page
  1462. // was scrolled, so if touch position changed more than
  1463. // ~4px (value borrowed from general touch handler)
  1464. var hasMoved = touchStartPos.clientX ? Math.sqrt(Math.pow(touchStartPos.clientX - e.changedTouches[0].clientX, 2) +
  1465. Math.pow(touchStartPos.clientY - e.changedTouches[0].clientY, 2)) >= 4 : false;
  1466. if (!hasMoved) { // only call handlers if page was not scrolled
  1467. handler.call(element, e);
  1468. }
  1469. touchEventFired = true;
  1470. // prevent other events from being fired. #9682
  1471. e.preventDefault();
  1472. };
  1473. element.onclick = function (e) {
  1474. // Do not call onclick handler if touch event was fired already.
  1475. if (!touchEventFired) {
  1476. handler.call(element, e);
  1477. }
  1478. };
  1479. }
  1480. else {
  1481. // simplest possible event model for internal use
  1482. element['on' + eventType] = handler;
  1483. }
  1484. return this;
  1485. };
  1486. /**
  1487. * @private
  1488. * @function Highcharts.SVGElement#opacitySetter
  1489. * @param {string} value
  1490. * @param {string} key
  1491. * @param {Highcharts.SVGDOMElement} element
  1492. */
  1493. SVGElement.prototype.opacitySetter = function (value, key, element) {
  1494. this[key] = value;
  1495. element.setAttribute(key, value);
  1496. };
  1497. /**
  1498. * Remove a class name from the element.
  1499. *
  1500. * @function Highcharts.SVGElement#removeClass
  1501. *
  1502. * @param {string|RegExp} className
  1503. * The class name to remove.
  1504. *
  1505. * @return {Highcharts.SVGElement} Returns the SVG element for chainability.
  1506. */
  1507. SVGElement.prototype.removeClass = function (className) {
  1508. return this.attr('class', ('' + this.attr('class'))
  1509. .replace(isString(className) ?
  1510. new RegExp("(^| )" + className + "( |$)") : // #12064, #13590
  1511. className, ' ')
  1512. .replace(/ +/g, ' ')
  1513. .trim());
  1514. };
  1515. /**
  1516. * @private
  1517. * @param {Array<Highcharts.SVGDOMElement>} tspans
  1518. * Text spans.
  1519. */
  1520. SVGElement.prototype.removeTextOutline = function (tspans) {
  1521. // Iterate from the end to
  1522. // support removing items inside the cycle (#6472).
  1523. var i = tspans.length, tspan;
  1524. while (i--) {
  1525. tspan = tspans[i];
  1526. if (tspan.getAttribute('class') === 'highcharts-text-outline') {
  1527. // Remove then erase
  1528. erase(tspans, this.element.removeChild(tspan));
  1529. }
  1530. }
  1531. };
  1532. /**
  1533. * Removes an element from the DOM.
  1534. *
  1535. * @private
  1536. * @function Highcharts.SVGElement#safeRemoveChild
  1537. *
  1538. * @param {Highcharts.SVGDOMElement|Highcharts.HTMLDOMElement} element
  1539. * The DOM node to remove.
  1540. */
  1541. SVGElement.prototype.safeRemoveChild = function (element) {
  1542. var parentNode = element.parentNode;
  1543. if (parentNode) {
  1544. parentNode.removeChild(element);
  1545. }
  1546. };
  1547. /**
  1548. * Set the coordinates needed to draw a consistent radial gradient across
  1549. * a shape regardless of positioning inside the chart. Used on pie slices
  1550. * to make all the slices have the same radial reference point.
  1551. *
  1552. * @function Highcharts.SVGElement#setRadialReference
  1553. *
  1554. * @param {Array<number>} coordinates
  1555. * The center reference. The format is `[centerX, centerY, diameter]` in
  1556. * pixels.
  1557. *
  1558. * @return {Highcharts.SVGElement}
  1559. * Returns the SVGElement for chaining.
  1560. */
  1561. SVGElement.prototype.setRadialReference = function (coordinates) {
  1562. var existingGradient = (this.element.gradient &&
  1563. this.renderer.gradients[this.element.gradient]);
  1564. this.element.radialReference = coordinates;
  1565. // On redrawing objects with an existing gradient, the gradient needs
  1566. // to be repositioned (#3801)
  1567. if (existingGradient && existingGradient.radAttr) {
  1568. existingGradient.animate(this.renderer.getRadialAttr(coordinates, existingGradient.radAttr));
  1569. }
  1570. return this;
  1571. };
  1572. /**
  1573. * @private
  1574. * @function Highcharts.SVGElement#setTextPath
  1575. * @param {Highcharts.SVGElement} path
  1576. * Path to follow.
  1577. * @param {Highcharts.DataLabelsTextPathOptionsObject} textPathOptions
  1578. * Options.
  1579. * @return {Highcharts.SVGElement}
  1580. * Returns the SVGElement for chaining.
  1581. */
  1582. SVGElement.prototype.setTextPath = function (path, textPathOptions) {
  1583. var elem = this.element, attribsMap = {
  1584. textAnchor: 'text-anchor'
  1585. }, attrs, adder = false, textPathElement, textPathId, textPathWrapper = this.textPathWrapper, tspans, firstTime = !textPathWrapper;
  1586. // Defaults
  1587. textPathOptions = merge(true, {
  1588. enabled: true,
  1589. attributes: {
  1590. dy: -5,
  1591. startOffset: '50%',
  1592. textAnchor: 'middle'
  1593. }
  1594. }, textPathOptions);
  1595. attrs = textPathOptions.attributes;
  1596. if (path && textPathOptions && textPathOptions.enabled) {
  1597. // In case of fixed width for a text, string is rebuilt
  1598. // (e.g. ellipsis is applied), so we need to rebuild textPath too
  1599. if (textPathWrapper &&
  1600. textPathWrapper.element.parentNode === null) {
  1601. // When buildText functionality was triggered again
  1602. // and deletes textPathWrapper parentNode
  1603. firstTime = true;
  1604. textPathWrapper = textPathWrapper.destroy();
  1605. }
  1606. else if (textPathWrapper) {
  1607. // Case after drillup when spans were added into
  1608. // the DOM outside the textPathWrapper parentGroup
  1609. this.removeTextOutline.call(textPathWrapper.parentGroup, [].slice.call(elem.getElementsByTagName('tspan')));
  1610. }
  1611. // label() has padding, text() doesn't
  1612. if (this.options && this.options.padding) {
  1613. attrs.dx = -this.options.padding;
  1614. }
  1615. if (!textPathWrapper) {
  1616. // Create <textPath>, defer the DOM adder
  1617. this.textPathWrapper = textPathWrapper =
  1618. this.renderer.createElement('textPath');
  1619. adder = true;
  1620. }
  1621. textPathElement = textPathWrapper.element;
  1622. // Set ID for the path
  1623. textPathId = path.element.getAttribute('id');
  1624. if (!textPathId) {
  1625. path.element.setAttribute('id', textPathId = uniqueKey());
  1626. }
  1627. // Change DOM structure, by placing <textPath> tag in <text>
  1628. if (firstTime) {
  1629. tspans = elem.getElementsByTagName('tspan');
  1630. // Now move all <tspan>'s to the <textPath> node
  1631. while (tspans.length) {
  1632. // Remove "y" from tspans, as Firefox translates them
  1633. tspans[0].setAttribute('y', 0);
  1634. // Remove "x" from tspans
  1635. if (isNumber(attrs.dx)) {
  1636. tspans[0].setAttribute('x', -attrs.dx);
  1637. }
  1638. textPathElement.appendChild(tspans[0]);
  1639. }
  1640. }
  1641. // Add <textPath> to the DOM
  1642. if (adder &&
  1643. textPathWrapper) {
  1644. textPathWrapper.add({
  1645. // label() is placed in a group, text() is standalone
  1646. element: this.text ? this.text.element : elem
  1647. });
  1648. }
  1649. // Set basic options:
  1650. // Use `setAttributeNS` because Safari needs this..
  1651. textPathElement.setAttributeNS('http://www.w3.org/1999/xlink', 'href', this.renderer.url + '#' + textPathId);
  1652. // Presentation attributes:
  1653. // dx/dy options must by set on <text> (parent),
  1654. // the rest should be set on <textPath>
  1655. if (defined(attrs.dy)) {
  1656. textPathElement.parentNode
  1657. .setAttribute('dy', attrs.dy);
  1658. delete attrs.dy;
  1659. }
  1660. if (defined(attrs.dx)) {
  1661. textPathElement.parentNode
  1662. .setAttribute('dx', attrs.dx);
  1663. delete attrs.dx;
  1664. }
  1665. // Additional attributes
  1666. objectEach(attrs, function (val, key) {
  1667. textPathElement.setAttribute(attribsMap[key] || key, val);
  1668. });
  1669. // Remove translation, text that follows path does not need that
  1670. elem.removeAttribute('transform');
  1671. // Remove shadows and text outlines
  1672. this.removeTextOutline.call(textPathWrapper, [].slice.call(elem.getElementsByTagName('tspan')));
  1673. // Remove background and border for label(), see #10545
  1674. // Alternatively, we can disable setting background rects in
  1675. // series.drawDataLabels()
  1676. if (this.text && !this.renderer.styledMode) {
  1677. this.attr({
  1678. fill: 'none',
  1679. 'stroke-width': 0
  1680. });
  1681. }
  1682. // Disable some functions
  1683. this.updateTransform = noop;
  1684. this.applyTextOutline = noop;
  1685. }
  1686. else if (textPathWrapper) {
  1687. // Reset to prototype
  1688. delete this.updateTransform;
  1689. delete this.applyTextOutline;
  1690. // Restore DOM structure:
  1691. this.destroyTextPath(elem, path);
  1692. // Bring attributes back
  1693. this.updateTransform();
  1694. // Set textOutline back for text()
  1695. if (this.options && this.options.rotation) {
  1696. this.applyTextOutline(this.options.style.textOutline);
  1697. }
  1698. }
  1699. return this;
  1700. };
  1701. /**
  1702. * Add a shadow to the element. Must be called after the element is added to
  1703. * the DOM. In styled mode, this method is not used, instead use `defs` and
  1704. * filters.
  1705. *
  1706. * @example
  1707. * renderer.rect(10, 100, 100, 100)
  1708. * .attr({ fill: 'red' })
  1709. * .shadow(true);
  1710. *
  1711. * @function Highcharts.SVGElement#shadow
  1712. *
  1713. * @param {boolean|Highcharts.ShadowOptionsObject} [shadowOptions]
  1714. * The shadow options. If `true`, the default options are applied. If
  1715. * `false`, the current shadow will be removed.
  1716. *
  1717. * @param {Highcharts.SVGElement} [group]
  1718. * The SVG group element where the shadows will be applied. The
  1719. * default is to add it to the same parent as the current element.
  1720. * Internally, this is ised for pie slices, where all the shadows are
  1721. * added to an element behind all the slices.
  1722. *
  1723. * @param {boolean} [cutOff]
  1724. * Used internally for column shadows.
  1725. *
  1726. * @return {Highcharts.SVGElement}
  1727. * Returns the SVGElement for chaining.
  1728. */
  1729. SVGElement.prototype.shadow = function (shadowOptions, group, cutOff) {
  1730. var shadows = [], i, shadow, element = this.element, strokeWidth, shadowElementOpacity, update = false, oldShadowOptions = this.oldShadowOptions,
  1731. // compensate for inverted plot area
  1732. transform;
  1733. var defaultShadowOptions = {
  1734. color: '#000000',
  1735. offsetX: 1,
  1736. offsetY: 1,
  1737. opacity: 0.15,
  1738. width: 3
  1739. };
  1740. var options;
  1741. if (shadowOptions === true) {
  1742. options = defaultShadowOptions;
  1743. }
  1744. else if (typeof shadowOptions === 'object') {
  1745. options = extend(defaultShadowOptions, shadowOptions);
  1746. }
  1747. // Update shadow when options change (#12091).
  1748. if (options) {
  1749. // Go over each key to look for change
  1750. if (options && oldShadowOptions) {
  1751. objectEach(options, function (value, key) {
  1752. if (value !== oldShadowOptions[key]) {
  1753. update = true;
  1754. }
  1755. });
  1756. }
  1757. if (update) {
  1758. this.destroyShadows();
  1759. }
  1760. this.oldShadowOptions = options;
  1761. }
  1762. if (!options) {
  1763. this.destroyShadows();
  1764. }
  1765. else if (!this.shadows) {
  1766. shadowElementOpacity = options.opacity / options.width;
  1767. transform = this.parentInverted ?
  1768. 'translate(-1,-1)' :
  1769. "translate(" + options.offsetX + ", " + options.offsetY + ")";
  1770. for (i = 1; i <= options.width; i++) {
  1771. shadow = element.cloneNode(false);
  1772. strokeWidth = (options.width * 2) + 1 - (2 * i);
  1773. attr(shadow, {
  1774. stroke: (shadowOptions.color ||
  1775. '#000000'),
  1776. 'stroke-opacity': shadowElementOpacity * i,
  1777. 'stroke-width': strokeWidth,
  1778. transform: transform,
  1779. fill: 'none'
  1780. });
  1781. shadow.setAttribute('class', (shadow.getAttribute('class') || '') + ' highcharts-shadow');
  1782. if (cutOff) {
  1783. attr(shadow, 'height', Math.max(attr(shadow, 'height') - strokeWidth, 0));
  1784. shadow.cutHeight = strokeWidth;
  1785. }
  1786. if (group) {
  1787. group.element.appendChild(shadow);
  1788. }
  1789. else if (element.parentNode) {
  1790. element.parentNode.insertBefore(shadow, element);
  1791. }
  1792. shadows.push(shadow);
  1793. }
  1794. this.shadows = shadows;
  1795. }
  1796. return this;
  1797. };
  1798. /**
  1799. * Show the element after it has been hidden.
  1800. *
  1801. * @function Highcharts.SVGElement#show
  1802. *
  1803. * @param {boolean} [inherit=false]
  1804. * Set the visibility attribute to `inherit` rather than `visible`.
  1805. * The difference is that an element with `visibility="visible"`
  1806. * will be visible even if the parent is hidden.
  1807. *
  1808. * @return {Highcharts.SVGElement}
  1809. * Returns the SVGElement for chaining.
  1810. */
  1811. SVGElement.prototype.show = function (inherit) {
  1812. return this.attr({ visibility: inherit ? 'inherit' : 'visible' });
  1813. };
  1814. /**
  1815. * WebKit and Batik have problems with a stroke-width of zero, so in this
  1816. * case we remove the stroke attribute altogether. #1270, #1369, #3065,
  1817. * #3072.
  1818. *
  1819. * @private
  1820. * @function Highcharts.SVGElement#strokeSetter
  1821. * @param {number|string} value
  1822. * @param {string} key
  1823. * @param {Highcharts.SVGDOMElement} element
  1824. */
  1825. SVGElement.prototype.strokeSetter = function (value, key, element) {
  1826. this[key] = value;
  1827. // Only apply the stroke attribute if the stroke width is defined and
  1828. // larger than 0
  1829. if (this.stroke && this['stroke-width']) {
  1830. // Use prototype as instance may be overridden
  1831. SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element);
  1832. element.setAttribute('stroke-width', this['stroke-width']);
  1833. this.hasStroke = true;
  1834. }
  1835. else if (key === 'stroke-width' && value === 0 && this.hasStroke) {
  1836. element.removeAttribute('stroke');
  1837. this.hasStroke = false;
  1838. }
  1839. else if (this.renderer.styledMode && this['stroke-width']) {
  1840. element.setAttribute('stroke-width', this['stroke-width']);
  1841. this.hasStroke = true;
  1842. }
  1843. };
  1844. /**
  1845. * Get the computed stroke width in pixel values. This is used extensively
  1846. * when drawing shapes to ensure the shapes are rendered crisp and
  1847. * positioned correctly relative to each other. Using
  1848. * `shape-rendering: crispEdges` leaves us less control over positioning,
  1849. * for example when we want to stack columns next to each other, or position
  1850. * things pixel-perfectly within the plot box.
  1851. *
  1852. * The common pattern when placing a shape is:
  1853. * - Create the SVGElement and add it to the DOM. In styled mode, it will
  1854. * now receive a stroke width from the style sheet. In classic mode we
  1855. * will add the `stroke-width` attribute.
  1856. * - Read the computed `elem.strokeWidth()`.
  1857. * - Place it based on the stroke width.
  1858. *
  1859. * @function Highcharts.SVGElement#strokeWidth
  1860. *
  1861. * @return {number}
  1862. * The stroke width in pixels. Even if the given stroke widtch (in CSS or by
  1863. * attributes) is based on `em` or other units, the pixel size is returned.
  1864. */
  1865. SVGElement.prototype.strokeWidth = function () {
  1866. // In non-styled mode, read the stroke width as set by .attr
  1867. if (!this.renderer.styledMode) {
  1868. return this['stroke-width'] || 0;
  1869. }
  1870. // In styled mode, read computed stroke width
  1871. var val = this.getStyle('stroke-width'), ret = 0, dummy;
  1872. // Read pixel values directly
  1873. if (val.indexOf('px') === val.length - 2) {
  1874. ret = pInt(val);
  1875. // Other values like em, pt etc need to be measured
  1876. }
  1877. else if (val !== '') {
  1878. dummy = doc.createElementNS(SVG_NS, 'rect');
  1879. attr(dummy, {
  1880. width: val,
  1881. 'stroke-width': 0
  1882. });
  1883. this.element.parentNode.appendChild(dummy);
  1884. ret = dummy.getBBox().width;
  1885. dummy.parentNode.removeChild(dummy);
  1886. }
  1887. return ret;
  1888. };
  1889. /**
  1890. * If one of the symbol size affecting parameters are changed,
  1891. * check all the others only once for each call to an element's
  1892. * .attr() method
  1893. *
  1894. * @private
  1895. * @function Highcharts.SVGElement#symbolAttr
  1896. *
  1897. * @param {Highcharts.SVGAttributes} hash
  1898. * The attributes to set.
  1899. */
  1900. SVGElement.prototype.symbolAttr = function (hash) {
  1901. var wrapper = this;
  1902. [
  1903. 'x',
  1904. 'y',
  1905. 'r',
  1906. 'start',
  1907. 'end',
  1908. 'width',
  1909. 'height',
  1910. 'innerR',
  1911. 'anchorX',
  1912. 'anchorY',
  1913. 'clockwise'
  1914. ].forEach(function (key) {
  1915. wrapper[key] = pick(hash[key], wrapper[key]);
  1916. });
  1917. wrapper.attr({
  1918. d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.width, wrapper.height, wrapper)
  1919. });
  1920. };
  1921. /**
  1922. * @private
  1923. * @function Highcharts.SVGElement#textSetter
  1924. * @param {string} value
  1925. */
  1926. SVGElement.prototype.textSetter = function (value) {
  1927. if (value !== this.textStr) {
  1928. // Delete size caches when the text changes
  1929. // delete this.bBox; // old code in series-label
  1930. delete this.textPxLength;
  1931. this.textStr = value;
  1932. if (this.added) {
  1933. this.renderer.buildText(this);
  1934. }
  1935. }
  1936. };
  1937. /**
  1938. * @private
  1939. * @function Highcharts.SVGElement#titleSetter
  1940. * @param {string} value
  1941. */
  1942. SVGElement.prototype.titleSetter = function (value) {
  1943. var titleNode = this.element.getElementsByTagName('title')[0];
  1944. if (!titleNode) {
  1945. titleNode = doc.createElementNS(this.SVG_NS, 'title');
  1946. this.element.appendChild(titleNode);
  1947. }
  1948. // Remove text content if it exists
  1949. if (titleNode.firstChild) {
  1950. titleNode.removeChild(titleNode.firstChild);
  1951. }
  1952. titleNode.appendChild(doc.createTextNode(
  1953. // #3276, #3895
  1954. String(pick(value, ''))
  1955. .replace(/<[^>]*>/g, '')
  1956. .replace(/&lt;/g, '<')
  1957. .replace(/&gt;/g, '>')));
  1958. };
  1959. /**
  1960. * Bring the element to the front. Alternatively, a new zIndex can be set.
  1961. *
  1962. * @sample highcharts/members/element-tofront/
  1963. * Click an element to bring it to front
  1964. *
  1965. * @function Highcharts.SVGElement#toFront
  1966. *
  1967. * @return {Highcharts.SVGElement}
  1968. * Returns the SVGElement for chaining.
  1969. */
  1970. SVGElement.prototype.toFront = function () {
  1971. var element = this.element;
  1972. element.parentNode.appendChild(element);
  1973. return this;
  1974. };
  1975. /**
  1976. * Move an object and its children by x and y values.
  1977. *
  1978. * @function Highcharts.SVGElement#translate
  1979. *
  1980. * @param {number} x
  1981. * The x value.
  1982. *
  1983. * @param {number} y
  1984. * The y value.
  1985. *
  1986. * @return {Highcharts.SVGElement}
  1987. */
  1988. SVGElement.prototype.translate = function (x, y) {
  1989. return this.attr({
  1990. translateX: x,
  1991. translateY: y
  1992. });
  1993. };
  1994. /**
  1995. * Update the shadow elements with new attributes.
  1996. *
  1997. * @private
  1998. * @function Highcharts.SVGElement#updateShadows
  1999. *
  2000. * @param {string} key
  2001. * The attribute name.
  2002. *
  2003. * @param {number} value
  2004. * The value of the attribute.
  2005. *
  2006. * @param {Function} setter
  2007. * The setter function, inherited from the parent wrapper.
  2008. */
  2009. SVGElement.prototype.updateShadows = function (key, value, setter) {
  2010. var shadows = this.shadows;
  2011. if (shadows) {
  2012. var i = shadows.length;
  2013. while (i--) {
  2014. setter.call(shadows[i], key === 'height' ?
  2015. Math.max(value - (shadows[i].cutHeight || 0), 0) :
  2016. key === 'd' ? this.d : value, key, shadows[i]);
  2017. }
  2018. }
  2019. };
  2020. /**
  2021. * Update the transform attribute based on internal properties. Deals with
  2022. * the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY`
  2023. * attributes and updates the SVG `transform` attribute.
  2024. *
  2025. * @private
  2026. * @function Highcharts.SVGElement#updateTransform
  2027. */
  2028. SVGElement.prototype.updateTransform = function () {
  2029. var wrapper = this, translateX = wrapper.translateX || 0, translateY = wrapper.translateY || 0, scaleX = wrapper.scaleX, scaleY = wrapper.scaleY, inverted = wrapper.inverted, rotation = wrapper.rotation, matrix = wrapper.matrix, element = wrapper.element, transform;
  2030. // Flipping affects translate as adjustment for flipping around the
  2031. // group's axis
  2032. if (inverted) {
  2033. translateX += wrapper.width;
  2034. translateY += wrapper.height;
  2035. }
  2036. // Apply translate. Nearly all transformed elements have translation,
  2037. // so instead of checking for translate = 0, do it always (#1767,
  2038. // #1846).
  2039. transform = ['translate(' + translateX + ',' + translateY + ')'];
  2040. // apply matrix
  2041. if (defined(matrix)) {
  2042. transform.push('matrix(' + matrix.join(',') + ')');
  2043. }
  2044. // apply rotation
  2045. if (inverted) {
  2046. transform.push('rotate(90) scale(-1,1)');
  2047. }
  2048. else if (rotation) { // text rotation
  2049. transform.push('rotate(' + rotation + ' ' +
  2050. pick(this.rotationOriginX, element.getAttribute('x'), 0) +
  2051. ' ' +
  2052. pick(this.rotationOriginY, element.getAttribute('y') || 0) + ')');
  2053. }
  2054. // apply scale
  2055. if (defined(scaleX) || defined(scaleY)) {
  2056. transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
  2057. }
  2058. if (transform.length) {
  2059. element.setAttribute('transform', transform.join(' '));
  2060. }
  2061. };
  2062. /**
  2063. * @private
  2064. * @function Highcharts.SVGElement#visibilitySetter
  2065. *
  2066. * @param {string} value
  2067. *
  2068. * @param {string} key
  2069. *
  2070. * @param {Highcharts.SVGDOMElement} element
  2071. *
  2072. * @return {void}
  2073. */
  2074. SVGElement.prototype.visibilitySetter = function (value, key, element) {
  2075. // IE9-11 doesn't handle visibilty:inherit well, so we remove the
  2076. // attribute instead (#2881, #3909)
  2077. if (value === 'inherit') {
  2078. element.removeAttribute(key);
  2079. }
  2080. else if (this[key] !== value) { // #6747
  2081. element.setAttribute(key, value);
  2082. }
  2083. this[key] = value;
  2084. };
  2085. /**
  2086. * @private
  2087. * @function Highcharts.SVGElement#xGetter
  2088. *
  2089. * @param {string} key
  2090. *
  2091. * @return {number|string|null}
  2092. */
  2093. SVGElement.prototype.xGetter = function (key) {
  2094. if (this.element.nodeName === 'circle') {
  2095. if (key === 'x') {
  2096. key = 'cx';
  2097. }
  2098. else if (key === 'y') {
  2099. key = 'cy';
  2100. }
  2101. }
  2102. return this._defaultGetter(key);
  2103. };
  2104. /**
  2105. * @private
  2106. * @function Highcharts.SVGElement#zIndexSetter
  2107. * @param {number} [value]
  2108. * @param {string} [key]
  2109. * @return {boolean}
  2110. */
  2111. SVGElement.prototype.zIndexSetter = function (value, key) {
  2112. var renderer = this.renderer, parentGroup = this.parentGroup, parentWrapper = parentGroup || renderer, parentNode = parentWrapper.element || renderer.box, childNodes, otherElement, otherZIndex, element = this.element, inserted = false, undefinedOtherZIndex, svgParent = parentNode === renderer.box, run = this.added, i;
  2113. if (defined(value)) {
  2114. // So we can read it for other elements in the group
  2115. element.setAttribute('data-z-index', value);
  2116. value = +value;
  2117. if (this[key] === value) {
  2118. // Only update when needed (#3865)
  2119. run = false;
  2120. }
  2121. }
  2122. else if (defined(this[key])) {
  2123. element.removeAttribute('data-z-index');
  2124. }
  2125. this[key] = value;
  2126. // Insert according to this and other elements' zIndex. Before .add() is
  2127. // called, nothing is done. Then on add, or by later calls to
  2128. // zIndexSetter, the node is placed on the right place in the DOM.
  2129. if (run) {
  2130. value = this.zIndex;
  2131. if (value && parentGroup) {
  2132. parentGroup.handleZ = true;
  2133. }
  2134. childNodes = parentNode.childNodes;
  2135. for (i = childNodes.length - 1; i >= 0 && !inserted; i--) {
  2136. otherElement = childNodes[i];
  2137. otherZIndex = otherElement.getAttribute('data-z-index');
  2138. undefinedOtherZIndex = !defined(otherZIndex);
  2139. if (otherElement !== element) {
  2140. if (
  2141. // Negative zIndex versus no zIndex:
  2142. // On all levels except the highest. If the parent is
  2143. // <svg>, then we don't want to put items before <desc>
  2144. // or <defs>
  2145. value < 0 &&
  2146. undefinedOtherZIndex &&
  2147. !svgParent &&
  2148. !i) {
  2149. parentNode.insertBefore(element, childNodes[i]);
  2150. inserted = true;
  2151. }
  2152. else if (
  2153. // Insert after the first element with a lower zIndex
  2154. pInt(otherZIndex) <= value ||
  2155. // If negative zIndex, add this before first undefined
  2156. // zIndex element
  2157. (undefinedOtherZIndex &&
  2158. (!defined(value) || value >= 0))) {
  2159. parentNode.insertBefore(element, childNodes[i + 1] || null // null for oldIE export
  2160. );
  2161. inserted = true;
  2162. }
  2163. }
  2164. }
  2165. if (!inserted) {
  2166. parentNode.insertBefore(element, childNodes[svgParent ? 3 : 0] || null // null for oldIE
  2167. );
  2168. inserted = true;
  2169. }
  2170. }
  2171. return inserted;
  2172. };
  2173. return SVGElement;
  2174. }());
  2175. // Some shared setters and getters
  2176. SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter;
  2177. SVGElement.prototype.yGetter = SVGElement.prototype.xGetter;
  2178. SVGElement.prototype.matrixSetter =
  2179. SVGElement.prototype.rotationOriginXSetter =
  2180. SVGElement.prototype.rotationOriginYSetter =
  2181. SVGElement.prototype.rotationSetter =
  2182. SVGElement.prototype.scaleXSetter =
  2183. SVGElement.prototype.scaleYSetter =
  2184. SVGElement.prototype.translateXSetter =
  2185. SVGElement.prototype.translateYSetter =
  2186. SVGElement.prototype.verticalAlignSetter = function (value, key) {
  2187. this[key] = value;
  2188. this.doTransform = true;
  2189. };
  2190. H.SVGElement = SVGElement;
  2191. export default H.SVGElement;