pattern-fill.src.js 19 KB

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