PatternFill.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. /* *
  2. *
  3. * Module for using patterns or images as point fills.
  4. *
  5. * (c) 2010-2021 Highsoft AS
  6. * Author: Torstein Hønsi, Øystein Moseng
  7. *
  8. * License: www.highcharts.com/license
  9. *
  10. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  11. *
  12. * */
  13. 'use strict';
  14. import A from '../Core/Animation/AnimationUtilities.js';
  15. var animObject = A.animObject;
  16. import Chart from '../Core/Chart/Chart.js';
  17. import H from '../Core/Globals.js';
  18. import D from '../Core/DefaultOptions.js';
  19. var getOptions = D.getOptions;
  20. import Point from '../Core/Series/Point.js';
  21. import Series from '../Core/Series/Series.js';
  22. import SVGRenderer from '../Core/Renderer/SVG/SVGRenderer.js';
  23. import U from '../Core/Utilities.js';
  24. var addEvent = U.addEvent, erase = U.erase, merge = U.merge, pick = U.pick, removeEvent = U.removeEvent, wrap = U.wrap;
  25. // Add the predefined patterns
  26. var patterns = H.patterns = (function () {
  27. var patterns = [], colors = getOptions().colors;
  28. [
  29. 'M 0 0 L 10 10 M 9 -1 L 11 1 M -1 9 L 1 11',
  30. 'M 0 10 L 10 0 M -1 1 L 1 -1 M 9 11 L 11 9',
  31. 'M 3 0 L 3 10 M 8 0 L 8 10',
  32. 'M 0 3 L 10 3 M 0 8 L 10 8',
  33. 'M 0 3 L 5 3 L 5 0 M 5 10 L 5 7 L 10 7',
  34. 'M 3 3 L 8 3 L 8 8 L 3 8 Z',
  35. 'M 5 5 m -4 0 a 4 4 0 1 1 8 0 a 4 4 0 1 1 -8 0',
  36. 'M 10 3 L 5 3 L 5 0 M 5 10 L 5 7 L 0 7',
  37. 'M 2 5 L 5 2 L 8 5 L 5 8 Z',
  38. 'M 0 0 L 5 10 L 10 0'
  39. ].forEach(function (pattern, i) {
  40. patterns.push({
  41. path: pattern,
  42. color: colors[i],
  43. width: 10,
  44. height: 10
  45. });
  46. });
  47. return patterns;
  48. })();
  49. /**
  50. * Utility function to compute a hash value from an object. Modified Java
  51. * String.hashCode implementation in JS. Use the preSeed parameter to add an
  52. * additional seeding step.
  53. *
  54. * @private
  55. * @function hashFromObject
  56. *
  57. * @param {object} obj
  58. * The javascript object to compute the hash from.
  59. *
  60. * @param {boolean} [preSeed=false]
  61. * Add an optional preSeed stage.
  62. *
  63. * @return {string}
  64. * The computed hash.
  65. */
  66. function hashFromObject(obj, preSeed) {
  67. var str = JSON.stringify(obj), strLen = str.length || 0, hash = 0, i = 0, char, seedStep;
  68. if (preSeed) {
  69. seedStep = Math.max(Math.floor(strLen / 500), 1);
  70. for (var a = 0; a < strLen; a += seedStep) {
  71. hash += str.charCodeAt(a);
  72. }
  73. hash = hash & hash;
  74. }
  75. for (; i < strLen; ++i) {
  76. char = str.charCodeAt(i);
  77. hash = ((hash << 5) - hash) + char;
  78. hash = hash & hash;
  79. }
  80. return hash.toString(16).replace('-', '1');
  81. }
  82. /**
  83. * Set dimensions on pattern from point. This function will set internal
  84. * pattern._width/_height properties if width and height are not both already
  85. * set. We only do this on image patterns. The _width/_height properties are set
  86. * to the size of the bounding box of the point, optionally taking aspect ratio
  87. * into account. If only one of width or height are supplied as options, the
  88. * undefined option is calculated as above.
  89. *
  90. * @private
  91. * @function Highcharts.Point#calculatePatternDimensions
  92. *
  93. * @param {Highcharts.PatternOptionsObject} pattern
  94. * The pattern to set dimensions on.
  95. *
  96. * @return {void}
  97. *
  98. * @requires modules/pattern-fill
  99. */
  100. Point.prototype.calculatePatternDimensions = function (pattern) {
  101. if (pattern.width && pattern.height) {
  102. return;
  103. }
  104. var bBox = this.graphic && (this.graphic.getBBox &&
  105. this.graphic.getBBox(true) ||
  106. this.graphic.element &&
  107. this.graphic.element.getBBox()) || {}, shapeArgs = this.shapeArgs;
  108. // Prefer using shapeArgs, as it is animation agnostic
  109. if (shapeArgs) {
  110. bBox.width = shapeArgs.width || bBox.width;
  111. bBox.height = shapeArgs.height || bBox.height;
  112. bBox.x = shapeArgs.x || bBox.x;
  113. bBox.y = shapeArgs.y || bBox.y;
  114. }
  115. // For images we stretch to bounding box
  116. if (pattern.image) {
  117. // If we do not have a bounding box at this point, simply add a defer
  118. // key and pick this up in the fillSetter handler, where the bounding
  119. // box should exist.
  120. if (!bBox.width || !bBox.height) {
  121. pattern._width = 'defer';
  122. pattern._height = 'defer';
  123. return;
  124. }
  125. // Handle aspect ratio filling
  126. if (pattern.aspectRatio) {
  127. bBox.aspectRatio = bBox.width / bBox.height;
  128. if (pattern.aspectRatio > bBox.aspectRatio) {
  129. // Height of bBox will determine width
  130. bBox.aspectWidth = bBox.height * pattern.aspectRatio;
  131. }
  132. else {
  133. // Width of bBox will determine height
  134. bBox.aspectHeight = bBox.width / pattern.aspectRatio;
  135. }
  136. }
  137. // We set the width/height on internal properties to differentiate
  138. // between the options set by a user and by this function.
  139. pattern._width = pattern.width ||
  140. Math.ceil(bBox.aspectWidth || bBox.width);
  141. pattern._height = pattern.height ||
  142. Math.ceil(bBox.aspectHeight || bBox.height);
  143. }
  144. // Set x/y accordingly, centering if using aspect ratio, otherwise adjusting
  145. // so bounding box corner is 0,0 of pattern.
  146. if (!pattern.width) {
  147. pattern._x = pattern.x || 0;
  148. pattern._x += bBox.x - Math.round(bBox.aspectWidth ?
  149. Math.abs(bBox.aspectWidth - bBox.width) / 2 :
  150. 0);
  151. }
  152. if (!pattern.height) {
  153. pattern._y = pattern.y || 0;
  154. pattern._y += bBox.y - Math.round(bBox.aspectHeight ?
  155. Math.abs(bBox.aspectHeight - bBox.height) / 2 :
  156. 0);
  157. }
  158. };
  159. /* eslint-disable no-invalid-this */
  160. /**
  161. * Add a pattern to the renderer.
  162. *
  163. * @private
  164. * @function Highcharts.SVGRenderer#addPattern
  165. *
  166. * @param {Highcharts.PatternObject} options
  167. * The pattern options.
  168. *
  169. * @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation]
  170. * The animation options.
  171. *
  172. * @return {Highcharts.SVGElement|undefined}
  173. * The added pattern. Undefined if the pattern already exists.
  174. *
  175. * @requires modules/pattern-fill
  176. */
  177. SVGRenderer.prototype.addPattern = function (options, animation) {
  178. var pattern, animate = pick(animation, true), animationOptions = animObject(animate), path, defaultSize = 32, width = options.width || options._width || defaultSize, height = (options.height || options._height || defaultSize), color = options.color || '#343434', id = options.id, ren = this, rect = function (fill) {
  179. ren.rect(0, 0, width, height)
  180. .attr({ fill: fill })
  181. .add(pattern);
  182. }, attribs;
  183. if (!id) {
  184. this.idCounter = this.idCounter || 0;
  185. id = 'highcharts-pattern-' + this.idCounter + '-' + (this.chartIndex || 0);
  186. ++this.idCounter;
  187. }
  188. if (this.forExport) {
  189. id += '-export';
  190. }
  191. // Do nothing if ID already exists
  192. this.defIds = this.defIds || [];
  193. if (this.defIds.indexOf(id) > -1) {
  194. return;
  195. }
  196. // Store ID in list to avoid duplicates
  197. this.defIds.push(id);
  198. // Calculate pattern element attributes
  199. var attrs = {
  200. id: id,
  201. patternUnits: 'userSpaceOnUse',
  202. patternContentUnits: options.patternContentUnits || 'userSpaceOnUse',
  203. width: width,
  204. height: height,
  205. x: options._x || options.x || 0,
  206. y: options._y || options.y || 0
  207. };
  208. if (options.patternTransform) {
  209. attrs.patternTransform = options.patternTransform;
  210. }
  211. pattern = this.createElement('pattern').attr(attrs).add(this.defs);
  212. // Set id on the SVGRenderer object
  213. pattern.id = id;
  214. // Use an SVG path for the pattern
  215. if (options.path) {
  216. path = U.isObject(options.path) ?
  217. options.path :
  218. { d: options.path };
  219. // The background
  220. if (options.backgroundColor) {
  221. rect(options.backgroundColor);
  222. }
  223. // The pattern
  224. attribs = {
  225. 'd': path.d
  226. };
  227. if (!this.styledMode) {
  228. attribs.stroke = path.stroke || color;
  229. attribs['stroke-width'] = pick(path.strokeWidth, 2);
  230. attribs.fill = path.fill || 'none';
  231. }
  232. if (path.transform) {
  233. attribs.transform = path.transform;
  234. }
  235. this.createElement('path').attr(attribs).add(pattern);
  236. pattern.color = color;
  237. // Image pattern
  238. }
  239. else if (options.image) {
  240. if (animate) {
  241. this.image(options.image, 0, 0, width, height, function () {
  242. // Onload
  243. this.animate({
  244. opacity: pick(options.opacity, 1)
  245. }, animationOptions);
  246. removeEvent(this.element, 'load');
  247. }).attr({ opacity: 0 }).add(pattern);
  248. }
  249. else {
  250. this.image(options.image, 0, 0, width, height).add(pattern);
  251. }
  252. }
  253. // For non-animated patterns, set opacity now
  254. if (!(options.image && animate) && typeof options.opacity !== 'undefined') {
  255. [].forEach.call(pattern.element.childNodes, function (child) {
  256. child.setAttribute('opacity', options.opacity);
  257. });
  258. }
  259. // Store for future reference
  260. this.patternElements = this.patternElements || {};
  261. this.patternElements[id] = pattern;
  262. return pattern;
  263. };
  264. // Make sure we have a series color
  265. wrap(Series.prototype, 'getColor', function (proceed) {
  266. var oldColor = this.options.color;
  267. // Temporarely remove color options to get defaults
  268. if (oldColor &&
  269. oldColor.pattern &&
  270. !oldColor.pattern.color) {
  271. delete this.options.color;
  272. // Get default
  273. proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  274. // Replace with old, but add default color
  275. oldColor.pattern.color =
  276. this.color;
  277. this.color = this.options.color = oldColor;
  278. }
  279. else {
  280. // We have a color, no need to do anything special
  281. proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  282. }
  283. });
  284. // Calculate pattern dimensions on points that have their own pattern.
  285. addEvent(Series, 'render', function () {
  286. var isResizing = this.chart.isResizing;
  287. if (this.isDirtyData || isResizing || !this.chart.hasRendered) {
  288. (this.points || []).forEach(function (point) {
  289. var colorOptions = point.options && point.options.color;
  290. if (colorOptions &&
  291. colorOptions.pattern) {
  292. // For most points we want to recalculate the dimensions on
  293. // render, where we have the shape args and bbox. But if we
  294. // are resizing and don't have the shape args, defer it, since
  295. // the bounding box is still not resized.
  296. if (isResizing &&
  297. !(point.shapeArgs &&
  298. point.shapeArgs.width &&
  299. point.shapeArgs.height)) {
  300. colorOptions.pattern._width =
  301. 'defer';
  302. colorOptions.pattern._height =
  303. 'defer';
  304. }
  305. else {
  306. point.calculatePatternDimensions(colorOptions.pattern);
  307. }
  308. }
  309. });
  310. }
  311. });
  312. // Merge series color options to points
  313. addEvent(Point, 'afterInit', function () {
  314. var point = this, colorOptions = point.options.color;
  315. // Only do this if we have defined a specific color on this point. Otherwise
  316. // we will end up trying to re-add the series color for each point.
  317. if (colorOptions && colorOptions.pattern) {
  318. // Move path definition to object, allows for merge with series path
  319. // definition
  320. if (typeof colorOptions.pattern.path === 'string') {
  321. colorOptions.pattern.path = {
  322. d: colorOptions.pattern.path
  323. };
  324. }
  325. // Merge with series options
  326. point.color = point.options.color = merge(point.series.options.color, colorOptions);
  327. }
  328. });
  329. // Add functionality to SVG renderer to handle patterns as complex colors
  330. addEvent(SVGRenderer, 'complexColor', function (args) {
  331. var color = args.args[0], prop = args.args[1], element = args.args[2], chartIndex = (this.chartIndex || 0);
  332. var pattern = color.pattern, value = '#343434';
  333. // Handle patternIndex
  334. if (typeof color.patternIndex !== 'undefined' && patterns) {
  335. pattern = patterns[color.patternIndex];
  336. }
  337. // Skip and call default if there is no pattern
  338. if (!pattern) {
  339. return true;
  340. }
  341. // We have a pattern.
  342. if (pattern.image ||
  343. typeof pattern.path === 'string' ||
  344. pattern.path && pattern.path.d) {
  345. // Real pattern. Add it and set the color value to be a reference.
  346. // Force Hash-based IDs for legend items, as they are drawn before
  347. // point render, meaning they are drawn before autocalculated image
  348. // width/heights. We don't want them to highjack the width/height for
  349. // this ID if it is defined by users.
  350. var forceHashId = element.parentNode &&
  351. element.parentNode.getAttribute('class');
  352. forceHashId = forceHashId &&
  353. forceHashId.indexOf('highcharts-legend') > -1;
  354. // If we don't have a width/height yet, handle it. Try faking a point
  355. // and running the algorithm again.
  356. if (pattern._width === 'defer' || pattern._height === 'defer') {
  357. Point.prototype.calculatePatternDimensions.call({ graphic: { element: element } }, pattern);
  358. }
  359. // If we don't have an explicit ID, compute a hash from the
  360. // definition and use that as the ID. This ensures that points with
  361. // the same pattern definition reuse existing pattern elements by
  362. // default. We combine two hashes, the second with an additional
  363. // preSeed algorithm, to minimize collision probability.
  364. if (forceHashId || !pattern.id) {
  365. // Make a copy so we don't accidentally edit options when setting ID
  366. pattern = merge({}, pattern);
  367. pattern.id = 'highcharts-pattern-' + chartIndex + '-' +
  368. hashFromObject(pattern) + hashFromObject(pattern, true);
  369. }
  370. // Add it. This function does nothing if an element with this ID
  371. // already exists.
  372. this.addPattern(pattern, !this.forExport && pick(pattern.animation, this.globalAnimation, { duration: 100 }));
  373. value = "url(" + this.url + "#" + (pattern.id + (this.forExport ? '-export' : '')) + ")";
  374. }
  375. else {
  376. // Not a full pattern definition, just add color
  377. value = pattern.color || value;
  378. }
  379. // Set the fill/stroke prop on the element
  380. element.setAttribute(prop, value);
  381. // Allow the color to be concatenated into tooltips formatters etc.
  382. color.toString = function () {
  383. return value;
  384. };
  385. // Skip default handler
  386. return false;
  387. });
  388. // When animation is used, we have to recalculate pattern dimensions after
  389. // resize, as the bounding boxes are not available until then.
  390. addEvent(Chart, 'endResize', function () {
  391. if ((this.renderer && this.renderer.defIds || []).filter(function (id) {
  392. return (id &&
  393. id.indexOf &&
  394. id.indexOf('highcharts-pattern-') === 0);
  395. }).length) {
  396. // We have non-default patterns to fix. Find them by looping through
  397. // all points.
  398. this.series.forEach(function (series) {
  399. series.points.forEach(function (point) {
  400. var colorOptions = point.options && point.options.color;
  401. if (colorOptions &&
  402. colorOptions.pattern) {
  403. colorOptions.pattern._width =
  404. 'defer';
  405. colorOptions.pattern._height =
  406. 'defer';
  407. }
  408. });
  409. });
  410. // Redraw without animation
  411. this.redraw(false);
  412. }
  413. });
  414. // Add a garbage collector to delete old patterns with autogenerated hashes that
  415. // are no longer being referenced.
  416. addEvent(Chart, 'redraw', function () {
  417. var usedIds = {}, renderer = this.renderer,
  418. // Get the autocomputed patterns - these are the ones we might delete
  419. patterns = (renderer.defIds || []).filter(function (pattern) {
  420. return (pattern.indexOf &&
  421. pattern.indexOf('highcharts-pattern-') === 0);
  422. });
  423. if (patterns.length) {
  424. // Look through the DOM for usage of the patterns. This can be points,
  425. // series, tooltips etc.
  426. [].forEach.call(this.renderTo.querySelectorAll('[color^="url("], [fill^="url("], [stroke^="url("]'), function (node) {
  427. var id = node.getAttribute('fill') ||
  428. node.getAttribute('color') ||
  429. node.getAttribute('stroke');
  430. if (id) {
  431. var sanitizedId = id.replace(renderer.url, '').replace('url(#', '').replace(')', '');
  432. usedIds[sanitizedId] = true;
  433. }
  434. });
  435. // Loop through the patterns that exist and see if they are used
  436. patterns.forEach(function (id) {
  437. if (!usedIds[id]) {
  438. // Remove id from used id list
  439. erase(renderer.defIds, id);
  440. // Remove pattern element
  441. if (renderer.patternElements[id]) {
  442. renderer.patternElements[id].destroy();
  443. delete renderer.patternElements[id];
  444. }
  445. }
  446. });
  447. }
  448. });
  449. /* *
  450. *
  451. * API Declarations
  452. *
  453. * */
  454. /**
  455. * Pattern options
  456. *
  457. * @interface Highcharts.PatternOptionsObject
  458. */ /**
  459. * Background color for the pattern if a `path` is set (not images).
  460. * @name Highcharts.PatternOptionsObject#backgroundColor
  461. * @type {Highcharts.ColorString}
  462. */ /**
  463. * URL to an image to use as the pattern.
  464. * @name Highcharts.PatternOptionsObject#image
  465. * @type {string}
  466. */ /**
  467. * Width of the pattern. For images this is automatically set to the width of
  468. * the element bounding box if not supplied. For non-image patterns the default
  469. * is 32px. Note that automatic resizing of image patterns to fill a bounding
  470. * box dynamically is only supported for patterns with an automatically
  471. * calculated ID.
  472. * @name Highcharts.PatternOptionsObject#width
  473. * @type {number}
  474. */ /**
  475. * Analogous to pattern.width.
  476. * @name Highcharts.PatternOptionsObject#height
  477. * @type {number}
  478. */ /**
  479. * For automatically calculated width and height on images, it is possible to
  480. * set an aspect ratio. The image will be zoomed to fill the bounding box,
  481. * maintaining the aspect ratio defined.
  482. * @name Highcharts.PatternOptionsObject#aspectRatio
  483. * @type {number}
  484. */ /**
  485. * Horizontal offset of the pattern. Defaults to 0.
  486. * @name Highcharts.PatternOptionsObject#x
  487. * @type {number|undefined}
  488. */ /**
  489. * Vertical offset of the pattern. Defaults to 0.
  490. * @name Highcharts.PatternOptionsObject#y
  491. * @type {number|undefined}
  492. */ /**
  493. * Either an SVG path as string, or an object. As an object, supply the path
  494. * string in the `path.d` property. Other supported properties are standard SVG
  495. * attributes like `path.stroke` and `path.fill`. If a path is supplied for the
  496. * pattern, the `image` property is ignored.
  497. * @name Highcharts.PatternOptionsObject#path
  498. * @type {string|Highcharts.SVGAttributes}
  499. */ /**
  500. * SVG `patternTransform` to apply to the entire pattern.
  501. * @name Highcharts.PatternOptionsObject#patternTransform
  502. * @type {string}
  503. * @see [patternTransform demo](https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/series/pattern-fill-transform)
  504. */ /**
  505. * Pattern color, used as default path stroke.
  506. * @name Highcharts.PatternOptionsObject#color
  507. * @type {Highcharts.ColorString}
  508. */ /**
  509. * Opacity of the pattern as a float value from 0 to 1.
  510. * @name Highcharts.PatternOptionsObject#opacity
  511. * @type {number}
  512. */ /**
  513. * ID to assign to the pattern. This is automatically computed if not added, and
  514. * identical patterns are reused. To refer to an existing pattern for a
  515. * Highcharts color, use `color: "url(#pattern-id)"`.
  516. * @name Highcharts.PatternOptionsObject#id
  517. * @type {string|undefined}
  518. */
  519. /**
  520. * Holds a pattern definition.
  521. *
  522. * @sample highcharts/series/pattern-fill-area/
  523. * Define a custom path pattern
  524. * @sample highcharts/series/pattern-fill-pie/
  525. * Default patterns and a custom image pattern
  526. * @sample maps/demo/pattern-fill-map/
  527. * Custom images on map
  528. *
  529. * @example
  530. * // Pattern used as a color option
  531. * color: {
  532. * pattern: {
  533. * path: {
  534. * d: 'M 3 3 L 8 3 L 8 8 Z',
  535. * fill: '#102045'
  536. * },
  537. * width: 12,
  538. * height: 12,
  539. * color: '#907000',
  540. * opacity: 0.5
  541. * }
  542. * }
  543. *
  544. * @interface Highcharts.PatternObject
  545. */ /**
  546. * Pattern options
  547. * @name Highcharts.PatternObject#pattern
  548. * @type {Highcharts.PatternOptionsObject}
  549. */ /**
  550. * Animation options for the image pattern loading.
  551. * @name Highcharts.PatternObject#animation
  552. * @type {boolean|Partial<Highcharts.AnimationOptionsObject>|undefined}
  553. */ /**
  554. * Optionally an index referencing which pattern to use. Highcharts adds
  555. * 10 default patterns to the `Highcharts.patterns` array. Additional
  556. * pattern definitions can be pushed to this array if desired. This option
  557. * is an index into this array.
  558. * @name Highcharts.PatternObject#patternIndex
  559. * @type {number|undefined}
  560. */
  561. ''; // keeps doclets above in transpiled file