You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
7.0 KiB

  1. /**
  2. * @license
  3. * Highcharts funnel module
  4. *
  5. * (c) 2010-2014 Torstein Honsi
  6. *
  7. * License: www.highcharts.com/license
  8. */
  9. /*global Highcharts */
  10. (function (Highcharts) {
  11. 'use strict';
  12. // create shortcuts
  13. var defaultOptions = Highcharts.getOptions(),
  14. defaultPlotOptions = defaultOptions.plotOptions,
  15. seriesTypes = Highcharts.seriesTypes,
  16. merge = Highcharts.merge,
  17. noop = function () {},
  18. each = Highcharts.each;
  19. // set default options
  20. defaultPlotOptions.funnel = merge(defaultPlotOptions.pie, {
  21. center: ['50%', '50%'],
  22. width: '90%',
  23. neckWidth: '30%',
  24. height: '100%',
  25. neckHeight: '25%',
  26. reversed: false,
  27. dataLabels: {
  28. //position: 'right',
  29. connectorWidth: 1,
  30. connectorColor: '#606060'
  31. },
  32. size: true, // to avoid adapting to data label size in Pie.drawDataLabels
  33. states: {
  34. select: {
  35. color: '#C0C0C0',
  36. borderColor: '#000000',
  37. shadow: false
  38. }
  39. }
  40. });
  41. seriesTypes.funnel = Highcharts.extendClass(seriesTypes.pie, {
  42. type: 'funnel',
  43. animate: noop,
  44. singularTooltips: true,
  45. /**
  46. * Overrides the pie translate method
  47. */
  48. translate: function () {
  49. var
  50. // Get positions - either an integer or a percentage string must be given
  51. getLength = function (length, relativeTo) {
  52. return (/%$/).test(length) ?
  53. relativeTo * parseInt(length, 10) / 100 :
  54. parseInt(length, 10);
  55. },
  56. sum = 0,
  57. series = this,
  58. chart = series.chart,
  59. options = series.options,
  60. reversed = options.reversed,
  61. plotWidth = chart.plotWidth,
  62. plotHeight = chart.plotHeight,
  63. cumulative = 0, // start at top
  64. center = options.center,
  65. centerX = getLength(center[0], plotWidth),
  66. centerY = getLength(center[0], plotHeight),
  67. width = getLength(options.width, plotWidth),
  68. tempWidth,
  69. getWidthAt,
  70. height = getLength(options.height, plotHeight),
  71. neckWidth = getLength(options.neckWidth, plotWidth),
  72. neckHeight = getLength(options.neckHeight, plotHeight),
  73. neckY = height - neckHeight,
  74. data = series.data,
  75. path,
  76. fraction,
  77. half = options.dataLabels.position === 'left' ? 1 : 0,
  78. x1,
  79. y1,
  80. x2,
  81. x3,
  82. y3,
  83. x4,
  84. y5;
  85. // Return the width at a specific y coordinate
  86. series.getWidthAt = getWidthAt = function (y) {
  87. return y > height - neckHeight || height === neckHeight ?
  88. neckWidth :
  89. neckWidth + (width - neckWidth) * ((height - neckHeight - y) / (height - neckHeight));
  90. };
  91. series.getX = function (y, half) {
  92. return centerX + (half ? -1 : 1) * ((getWidthAt(reversed ? plotHeight - y : y) / 2) + options.dataLabels.distance);
  93. };
  94. // Expose
  95. series.center = [centerX, centerY, height];
  96. series.centerX = centerX;
  97. /*
  98. * Individual point coordinate naming:
  99. *
  100. * x1,y1 _________________ x2,y1
  101. * \ /
  102. * \ /
  103. * \ /
  104. * \ /
  105. * \ /
  106. * x3,y3 _________ x4,y3
  107. *
  108. * Additional for the base of the neck:
  109. *
  110. * | |
  111. * | |
  112. * | |
  113. * x3,y5 _________ x4,y5
  114. */
  115. // get the total sum
  116. each(data, function (point) {
  117. sum += point.y;
  118. });
  119. each(data, function (point) {
  120. // set start and end positions
  121. y5 = null;
  122. fraction = sum ? point.y / sum : 0;
  123. y1 = centerY - height / 2 + cumulative * height;
  124. y3 = y1 + fraction * height;
  125. //tempWidth = neckWidth + (width - neckWidth) * ((height - neckHeight - y1) / (height - neckHeight));
  126. tempWidth = getWidthAt(y1);
  127. x1 = centerX - tempWidth / 2;
  128. x2 = x1 + tempWidth;
  129. tempWidth = getWidthAt(y3);
  130. x3 = centerX - tempWidth / 2;
  131. x4 = x3 + tempWidth;
  132. // the entire point is within the neck
  133. if (y1 > neckY) {
  134. x1 = x3 = centerX - neckWidth / 2;
  135. x2 = x4 = centerX + neckWidth / 2;
  136. // the base of the neck
  137. } else if (y3 > neckY) {
  138. y5 = y3;
  139. tempWidth = getWidthAt(neckY);
  140. x3 = centerX - tempWidth / 2;
  141. x4 = x3 + tempWidth;
  142. y3 = neckY;
  143. }
  144. if (reversed) {
  145. y1 = height - y1;
  146. y3 = height - y3;
  147. y5 = (y5 ? height - y5 : null);
  148. }
  149. // save the path
  150. path = [
  151. 'M',
  152. x1, y1,
  153. 'L',
  154. x2, y1,
  155. x4, y3
  156. ];
  157. if (y5) {
  158. path.push(x4, y5, x3, y5);
  159. }
  160. path.push(x3, y3, 'Z');
  161. // prepare for using shared dr
  162. point.shapeType = 'path';
  163. point.shapeArgs = { d: path };
  164. // for tooltips and data labels
  165. point.percentage = fraction * 100;
  166. point.plotX = centerX;
  167. point.plotY = (y1 + (y5 || y3)) / 2;
  168. // Placement of tooltips and data labels
  169. point.tooltipPos = [
  170. centerX,
  171. point.plotY
  172. ];
  173. // Slice is a noop on funnel points
  174. point.slice = noop;
  175. // Mimicking pie data label placement logic
  176. point.half = half;
  177. cumulative += fraction;
  178. });
  179. },
  180. /**
  181. * Draw a single point (wedge)
  182. * @param {Object} point The point object
  183. * @param {Object} color The color of the point
  184. * @param {Number} brightness The brightness relative to the color
  185. */
  186. drawPoints: function () {
  187. var series = this,
  188. options = series.options,
  189. chart = series.chart,
  190. renderer = chart.renderer;
  191. each(series.data, function (point) {
  192. var graphic = point.graphic,
  193. shapeArgs = point.shapeArgs;
  194. if (!graphic) { // Create the shapes
  195. point.graphic = renderer.path(shapeArgs).
  196. attr({
  197. fill: point.color,
  198. stroke: options.borderColor,
  199. 'stroke-width': options.borderWidth
  200. }).
  201. add(series.group);
  202. } else { // Update the shapes
  203. graphic.animate(shapeArgs);
  204. }
  205. });
  206. },
  207. /**
  208. * Funnel items don't have angles (#2289)
  209. */
  210. sortByAngle: function (points) {
  211. points.sort(function (a, b) {
  212. return a.plotY - b.plotY;
  213. });
  214. },
  215. /**
  216. * Extend the pie data label method
  217. */
  218. drawDataLabels: function () {
  219. var data = this.data,
  220. labelDistance = this.options.dataLabels.distance,
  221. leftSide,
  222. sign,
  223. point,
  224. i = data.length,
  225. x,
  226. y;
  227. // In the original pie label anticollision logic, the slots are distributed
  228. // from one labelDistance above to one labelDistance below the pie. In funnels
  229. // we don't want this.
  230. this.center[2] -= 2 * labelDistance;
  231. // Set the label position array for each point.
  232. while (i--) {
  233. point = data[i];
  234. leftSide = point.half;
  235. sign = leftSide ? 1 : -1;
  236. y = point.plotY;
  237. x = this.getX(y, leftSide);
  238. // set the anchor point for data labels
  239. point.labelPos = [
  240. 0, // first break of connector
  241. y, // a/a
  242. x + (labelDistance - 5) * sign, // second break, right outside point shape
  243. y, // a/a
  244. x + labelDistance * sign, // landing point for connector
  245. y, // a/a
  246. leftSide ? 'right' : 'left', // alignment
  247. 0 // center angle
  248. ];
  249. }
  250. seriesTypes.pie.prototype.drawDataLabels.call(this);
  251. }
  252. });
  253. /**
  254. * Pyramid series type.
  255. * A pyramid series is a special type of funnel, without neck and reversed by default.
  256. */
  257. defaultOptions.plotOptions.pyramid = Highcharts.merge(defaultOptions.plotOptions.funnel, {
  258. neckWidth: '0%',
  259. neckHeight: '0%',
  260. reversed: true
  261. });
  262. Highcharts.seriesTypes.pyramid = Highcharts.extendClass(Highcharts.seriesTypes.funnel, {
  263. type: 'pyramid'
  264. });
  265. }(Highcharts));