Funnel3DComposition.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /* *
  2. *
  3. * Imports
  4. *
  5. * */
  6. import Color from '../../Core/Color/Color.js';
  7. var color = Color.parse;
  8. import H from '../../Core/Globals.js';
  9. var charts = H.charts, _a = H.Renderer.prototype, cuboidPath = _a.cuboidPath, Elements3D = _a.elements3d;
  10. import U from '../../Core/Utilities.js';
  11. var error = U.error, extend = U.extend, merge = U.merge;
  12. import '../../Core/Renderer/SVG/SVGRenderer.js';
  13. /* *
  14. *
  15. * Composition
  16. *
  17. * */
  18. /* eslint-disable valid-jsdoc */
  19. Elements3D.funnel3d = merge(Elements3D.cuboid, {
  20. parts: [
  21. 'top', 'bottom',
  22. 'frontUpper', 'backUpper',
  23. 'frontLower', 'backLower',
  24. 'rightUpper', 'rightLower'
  25. ],
  26. mainParts: ['top', 'bottom'],
  27. sideGroups: [
  28. 'upperGroup', 'lowerGroup'
  29. ],
  30. sideParts: {
  31. upperGroup: ['frontUpper', 'backUpper', 'rightUpper'],
  32. lowerGroup: ['frontLower', 'backLower', 'rightLower']
  33. },
  34. pathType: 'funnel3d',
  35. // override opacity and color setters to control opacity
  36. opacitySetter: function (opacity) {
  37. var funnel3d = this, parts = funnel3d.parts, chart = H.charts[funnel3d.renderer.chartIndex], filterId = 'group-opacity-' + opacity + '-' + chart.index;
  38. // use default for top and bottom
  39. funnel3d.parts = funnel3d.mainParts;
  40. funnel3d.singleSetterForParts('opacity', opacity);
  41. // restore
  42. funnel3d.parts = parts;
  43. if (!chart.renderer.filterId) {
  44. chart.renderer.definition({
  45. tagName: 'filter',
  46. attributes: {
  47. id: filterId
  48. },
  49. children: [{
  50. tagName: 'feComponentTransfer',
  51. children: [{
  52. tagName: 'feFuncA',
  53. attributes: {
  54. type: 'table',
  55. tableValues: '0 ' + opacity
  56. }
  57. }]
  58. }]
  59. });
  60. funnel3d.sideGroups.forEach(function (groupName) {
  61. funnel3d[groupName].attr({
  62. filter: 'url(#' + filterId + ')'
  63. });
  64. });
  65. // styled mode
  66. if (funnel3d.renderer.styledMode) {
  67. chart.renderer.definition({
  68. tagName: 'style',
  69. textContent: '.highcharts-' + filterId +
  70. ' {filter:url(#' + filterId + ')}'
  71. });
  72. funnel3d.sideGroups.forEach(function (group) {
  73. group.addClass('highcharts-' + filterId);
  74. });
  75. }
  76. }
  77. return funnel3d;
  78. },
  79. fillSetter: function (fill) {
  80. // extract alpha channel to use the opacitySetter
  81. var funnel3d = this, fillColor = color(fill), alpha = fillColor.rgba[3], partsWithColor = {
  82. // standard color for top and bottom
  83. top: color(fill).brighten(0.1).get(),
  84. bottom: color(fill).brighten(-0.2).get()
  85. };
  86. if (alpha < 1) {
  87. fillColor.rgba[3] = 1;
  88. fillColor = fillColor.get('rgb');
  89. // set opacity through the opacitySetter
  90. funnel3d.attr({
  91. opacity: alpha
  92. });
  93. }
  94. else {
  95. // use default for full opacity
  96. fillColor = fill;
  97. }
  98. // add gradient for sides
  99. if (!fillColor.linearGradient &&
  100. !fillColor.radialGradient &&
  101. funnel3d.gradientForSides) {
  102. fillColor = {
  103. linearGradient: { x1: 0, x2: 1, y1: 1, y2: 1 },
  104. stops: [
  105. [0, color(fill).brighten(-0.2).get()],
  106. [0.5, fill],
  107. [1, color(fill).brighten(-0.2).get()]
  108. ]
  109. };
  110. }
  111. // gradient support
  112. if (fillColor.linearGradient) {
  113. // color in steps, as each gradient will generate a key
  114. funnel3d.sideGroups.forEach(function (sideGroupName) {
  115. var box = funnel3d[sideGroupName].gradientBox, gradient = fillColor.linearGradient, alteredGradient = merge(fillColor, {
  116. linearGradient: {
  117. x1: box.x + gradient.x1 * box.width,
  118. y1: box.y + gradient.y1 * box.height,
  119. x2: box.x + gradient.x2 * box.width,
  120. y2: box.y + gradient.y2 * box.height
  121. }
  122. });
  123. funnel3d.sideParts[sideGroupName].forEach(function (partName) {
  124. partsWithColor[partName] = alteredGradient;
  125. });
  126. });
  127. }
  128. else {
  129. merge(true, partsWithColor, {
  130. frontUpper: fillColor,
  131. backUpper: fillColor,
  132. rightUpper: fillColor,
  133. frontLower: fillColor,
  134. backLower: fillColor,
  135. rightLower: fillColor
  136. });
  137. if (fillColor.radialGradient) {
  138. funnel3d.sideGroups.forEach(function (sideGroupName) {
  139. var gradBox = funnel3d[sideGroupName].gradientBox, centerX = gradBox.x + gradBox.width / 2, centerY = gradBox.y + gradBox.height / 2, diameter = Math.min(gradBox.width, gradBox.height);
  140. funnel3d.sideParts[sideGroupName].forEach(function (partName) {
  141. funnel3d[partName].setRadialReference([
  142. centerX, centerY, diameter
  143. ]);
  144. });
  145. });
  146. }
  147. }
  148. funnel3d.singleSetterForParts('fill', null, partsWithColor);
  149. // fill for animation getter (#6776)
  150. funnel3d.color = funnel3d.fill = fill;
  151. // change gradientUnits to userSpaceOnUse for linearGradient
  152. if (fillColor.linearGradient) {
  153. [funnel3d.frontLower, funnel3d.frontUpper].forEach(function (part) {
  154. var elem = part.element, grad = elem && funnel3d.renderer.gradients[elem.gradient];
  155. if (grad && grad.attr('gradientUnits') !== 'userSpaceOnUse') {
  156. grad.attr({
  157. gradientUnits: 'userSpaceOnUse'
  158. });
  159. }
  160. });
  161. }
  162. return funnel3d;
  163. },
  164. adjustForGradient: function () {
  165. var funnel3d = this, bbox;
  166. funnel3d.sideGroups.forEach(function (sideGroupName) {
  167. // use common extremes for groups for matching gradients
  168. var topLeftEdge = {
  169. x: Number.MAX_VALUE,
  170. y: Number.MAX_VALUE
  171. }, bottomRightEdge = {
  172. x: -Number.MAX_VALUE,
  173. y: -Number.MAX_VALUE
  174. };
  175. // get extremes
  176. funnel3d.sideParts[sideGroupName].forEach(function (partName) {
  177. var part = funnel3d[partName];
  178. bbox = part.getBBox(true);
  179. topLeftEdge = {
  180. x: Math.min(topLeftEdge.x, bbox.x),
  181. y: Math.min(topLeftEdge.y, bbox.y)
  182. };
  183. bottomRightEdge = {
  184. x: Math.max(bottomRightEdge.x, bbox.x + bbox.width),
  185. y: Math.max(bottomRightEdge.y, bbox.y + bbox.height)
  186. };
  187. });
  188. // store for color fillSetter
  189. funnel3d[sideGroupName].gradientBox = {
  190. x: topLeftEdge.x,
  191. width: bottomRightEdge.x - topLeftEdge.x,
  192. y: topLeftEdge.y,
  193. height: bottomRightEdge.y - topLeftEdge.y
  194. };
  195. });
  196. },
  197. zIndexSetter: function () {
  198. // this.added won't work, because zIndex is set after the prop is set,
  199. // but before the graphic is really added
  200. if (this.finishedOnAdd) {
  201. this.adjustForGradient();
  202. }
  203. // run default
  204. return this.renderer.Element.prototype.zIndexSetter.apply(this, arguments);
  205. },
  206. onAdd: function () {
  207. this.adjustForGradient();
  208. this.finishedOnAdd = true;
  209. }
  210. });
  211. extend(H.Renderer.prototype, {
  212. funnel3d: function (shapeArgs) {
  213. var renderer = this, funnel3d = renderer.element3d('funnel3d', shapeArgs), styledMode = renderer.styledMode,
  214. // hide stroke for Firefox
  215. strokeAttrs = {
  216. 'stroke-width': 1,
  217. stroke: 'none'
  218. };
  219. // create groups for sides for oppacity setter
  220. funnel3d.upperGroup = renderer.g('funnel3d-upper-group').attr({
  221. zIndex: funnel3d.frontUpper.zIndex
  222. }).add(funnel3d);
  223. [
  224. funnel3d.frontUpper,
  225. funnel3d.backUpper,
  226. funnel3d.rightUpper
  227. ].forEach(function (upperElem) {
  228. if (!styledMode) {
  229. upperElem.attr(strokeAttrs);
  230. }
  231. upperElem.add(funnel3d.upperGroup);
  232. });
  233. funnel3d.lowerGroup = renderer.g('funnel3d-lower-group').attr({
  234. zIndex: funnel3d.frontLower.zIndex
  235. }).add(funnel3d);
  236. [
  237. funnel3d.frontLower,
  238. funnel3d.backLower,
  239. funnel3d.rightLower
  240. ].forEach(function (lowerElem) {
  241. if (!styledMode) {
  242. lowerElem.attr(strokeAttrs);
  243. }
  244. lowerElem.add(funnel3d.lowerGroup);
  245. });
  246. funnel3d.gradientForSides = shapeArgs.gradientForSides;
  247. return funnel3d;
  248. },
  249. /**
  250. * Generates paths and zIndexes.
  251. * @private
  252. */
  253. funnel3dPath: function (shapeArgs) {
  254. // Check getCylinderEnd for better error message if
  255. // the cylinder module is missing
  256. if (!this.getCylinderEnd) {
  257. error('A required Highcharts module is missing: cylinder.js', true, charts[this.chartIndex]);
  258. }
  259. var renderer = this, chart = charts[renderer.chartIndex],
  260. // adjust angles for visible edges
  261. // based on alpha, selected through visual tests
  262. alphaCorrection = shapeArgs.alphaCorrection = 90 -
  263. Math.abs((chart.options.chart.options3d.alpha % 180) - 90),
  264. // set zIndexes of parts based on cubiod logic, for consistency
  265. cuboidData = cuboidPath.call(renderer, merge(shapeArgs, {
  266. depth: shapeArgs.width,
  267. width: (shapeArgs.width + shapeArgs.bottom.width) / 2
  268. })), isTopFirst = cuboidData.isTop, isFrontFirst = !cuboidData.isFront, hasMiddle = !!shapeArgs.middle,
  269. //
  270. top = renderer.getCylinderEnd(chart, merge(shapeArgs, {
  271. x: shapeArgs.x - shapeArgs.width / 2,
  272. z: shapeArgs.z - shapeArgs.width / 2,
  273. alphaCorrection: alphaCorrection
  274. })), bottomWidth = shapeArgs.bottom.width, bottomArgs = merge(shapeArgs, {
  275. width: bottomWidth,
  276. x: shapeArgs.x - bottomWidth / 2,
  277. z: shapeArgs.z - bottomWidth / 2,
  278. alphaCorrection: alphaCorrection
  279. }), bottom = renderer.getCylinderEnd(chart, bottomArgs, true),
  280. //
  281. middleWidth = bottomWidth, middleTopArgs = bottomArgs, middleTop = bottom, middleBottom = bottom, ret,
  282. // masking for cylinders or a missing part of a side shape
  283. useAlphaCorrection;
  284. if (hasMiddle) {
  285. middleWidth = shapeArgs.middle.width;
  286. middleTopArgs = merge(shapeArgs, {
  287. y: shapeArgs.y + shapeArgs.middle.fraction * shapeArgs.height,
  288. width: middleWidth,
  289. x: shapeArgs.x - middleWidth / 2,
  290. z: shapeArgs.z - middleWidth / 2
  291. });
  292. middleTop = renderer.getCylinderEnd(chart, middleTopArgs, false);
  293. middleBottom = renderer.getCylinderEnd(chart, middleTopArgs, false);
  294. }
  295. ret = {
  296. top: top,
  297. bottom: bottom,
  298. frontUpper: renderer.getCylinderFront(top, middleTop),
  299. zIndexes: {
  300. group: cuboidData.zIndexes.group,
  301. top: isTopFirst !== 0 ? 0 : 3,
  302. bottom: isTopFirst !== 1 ? 0 : 3,
  303. frontUpper: isFrontFirst ? 2 : 1,
  304. backUpper: isFrontFirst ? 1 : 2,
  305. rightUpper: isFrontFirst ? 2 : 1
  306. }
  307. };
  308. ret.backUpper = renderer.getCylinderBack(top, middleTop);
  309. useAlphaCorrection = (Math.min(middleWidth, shapeArgs.width) /
  310. Math.max(middleWidth, shapeArgs.width)) !== 1;
  311. ret.rightUpper = renderer.getCylinderFront(renderer.getCylinderEnd(chart, merge(shapeArgs, {
  312. x: shapeArgs.x - shapeArgs.width / 2,
  313. z: shapeArgs.z - shapeArgs.width / 2,
  314. alphaCorrection: useAlphaCorrection ? -alphaCorrection : 0
  315. }), false), renderer.getCylinderEnd(chart, merge(middleTopArgs, {
  316. alphaCorrection: useAlphaCorrection ? -alphaCorrection : 0
  317. }), !hasMiddle));
  318. if (hasMiddle) {
  319. useAlphaCorrection = (Math.min(middleWidth, bottomWidth) /
  320. Math.max(middleWidth, bottomWidth)) !== 1;
  321. merge(true, ret, {
  322. frontLower: renderer.getCylinderFront(middleBottom, bottom),
  323. backLower: renderer.getCylinderBack(middleBottom, bottom),
  324. rightLower: renderer.getCylinderFront(renderer.getCylinderEnd(chart, merge(bottomArgs, {
  325. alphaCorrection: useAlphaCorrection ?
  326. -alphaCorrection : 0
  327. }), true), renderer.getCylinderEnd(chart, merge(middleTopArgs, {
  328. alphaCorrection: useAlphaCorrection ?
  329. -alphaCorrection : 0
  330. }), false)),
  331. zIndexes: {
  332. frontLower: isFrontFirst ? 2 : 1,
  333. backLower: isFrontFirst ? 1 : 2,
  334. rightLower: isFrontFirst ? 1 : 2
  335. }
  336. });
  337. }
  338. return ret;
  339. }
  340. });
  341. /* eslint-enable valid-jsdoc */