funnel.src.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. /* *
  2. * Highcharts funnel module
  3. *
  4. * (c) 2010-2019 Torstein Honsi
  5. *
  6. * License: www.highcharts.com/license
  7. */
  8. /* eslint indent: 0 */
  9. 'use strict';
  10. import Highcharts from '../parts/Globals.js';
  11. import '../parts/Utilities.js';
  12. import '../parts/Options.js';
  13. import '../parts/Series.js';
  14. // create shortcuts
  15. var seriesType = Highcharts.seriesType,
  16. seriesTypes = Highcharts.seriesTypes,
  17. noop = Highcharts.noop,
  18. pick = Highcharts.pick;
  19. /**
  20. * @private
  21. * @class
  22. * @name Highcharts.seriesTypes.funnel
  23. *
  24. * @augments Highcharts.Series
  25. */
  26. seriesType('funnel', 'pie',
  27. /**
  28. * Funnel charts are a type of chart often used to visualize stages in a sales
  29. * project, where the top are the initial stages with the most clients.
  30. * It requires that the modules/funnel.js file is loaded.
  31. *
  32. * @sample highcharts/demo/funnel/
  33. * Funnel demo
  34. *
  35. * @extends plotOptions.pie
  36. * @excluding size
  37. * @product highcharts
  38. * @optionparent plotOptions.funnel
  39. */
  40. {
  41. /**
  42. * Initial animation is by default disabled for the funnel chart.
  43. */
  44. animation: false,
  45. /**
  46. * The center of the series. By default, it is centered in the middle
  47. * of the plot area, so it fills the plot area height.
  48. *
  49. * @type {Array<number|string>}
  50. * @default ["50%", "50%"]
  51. * @since 3.0
  52. */
  53. center: ['50%', '50%'],
  54. /**
  55. * The width of the funnel compared to the width of the plot area,
  56. * or the pixel width if it is a number.
  57. *
  58. * @type {number|string}
  59. * @since 3.0
  60. */
  61. width: '90%',
  62. /**
  63. * The width of the neck, the lower part of the funnel. A number defines
  64. * pixel width, a percentage string defines a percentage of the plot
  65. * area width.
  66. *
  67. * @sample {highcharts} highcharts/demo/funnel/
  68. * Funnel demo
  69. *
  70. * @type {number|string}
  71. * @since 3.0
  72. */
  73. neckWidth: '30%',
  74. /**
  75. * The height of the funnel or pyramid. If it is a number it defines
  76. * the pixel height, if it is a percentage string it is the percentage
  77. * of the plot area height.
  78. *
  79. * @sample {highcharts} highcharts/demo/funnel/
  80. * Funnel demo
  81. *
  82. * @type {number|string}
  83. * @since 3.0
  84. */
  85. height: '100%',
  86. /**
  87. * The height of the neck, the lower part of the funnel. A number defines
  88. * pixel width, a percentage string defines a percentage of the plot
  89. * area height.
  90. *
  91. * @type {number|string}
  92. */
  93. neckHeight: '25%',
  94. /**
  95. * A reversed funnel has the widest area down. A reversed funnel with
  96. * no neck width and neck height is a pyramid.
  97. *
  98. * @since 3.0.10
  99. */
  100. reversed: false,
  101. /** @ignore */
  102. size: true, // to avoid adapting to data label size in Pie.drawDataLabels
  103. dataLabels: {
  104. connectorWidth: 1
  105. },
  106. /**
  107. * Options for the series states.
  108. */
  109. states: {
  110. /**
  111. * @excluding halo, marker, lineWidth, lineWidthPlus
  112. * @apioption plotOptions.funnel.states.hover
  113. */
  114. /**
  115. * Options for a selected funnel item.
  116. *
  117. * @excluding halo, marker, lineWidth, lineWidthPlus
  118. */
  119. select: {
  120. /**
  121. * A specific color for the selected point.
  122. *
  123. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  124. */
  125. color: '#cccccc',
  126. /**
  127. * A specific border color for the selected point.
  128. *
  129. * @type {Highcharts.ColorString}
  130. */
  131. borderColor: '#000000'
  132. }
  133. }
  134. },
  135. // Properties
  136. {
  137. animate: noop,
  138. // Overrides the pie translate method
  139. translate: function () {
  140. var
  141. // Get positions - either an integer or a percentage string
  142. // must be given
  143. getLength = function (length, relativeTo) {
  144. return (/%$/).test(length) ?
  145. relativeTo * parseInt(length, 10) / 100 :
  146. parseInt(length, 10);
  147. },
  148. sum = 0,
  149. series = this,
  150. chart = series.chart,
  151. options = series.options,
  152. reversed = options.reversed,
  153. ignoreHiddenPoint = options.ignoreHiddenPoint,
  154. plotWidth = chart.plotWidth,
  155. plotHeight = chart.plotHeight,
  156. cumulative = 0, // start at top
  157. center = options.center,
  158. centerX = getLength(center[0], plotWidth),
  159. centerY = getLength(center[1], plotHeight),
  160. width = getLength(options.width, plotWidth),
  161. tempWidth,
  162. getWidthAt,
  163. height = getLength(options.height, plotHeight),
  164. neckWidth = getLength(options.neckWidth, plotWidth),
  165. neckHeight = getLength(options.neckHeight, plotHeight),
  166. neckY = (centerY - height / 2) + height - neckHeight,
  167. data = series.data,
  168. path,
  169. fraction,
  170. half = options.dataLabels.position === 'left' ? 1 : 0,
  171. x1,
  172. y1,
  173. x2,
  174. x3,
  175. y3,
  176. x4,
  177. y5;
  178. // Return the width at a specific y coordinate
  179. series.getWidthAt = getWidthAt = function (y) {
  180. var top = (centerY - height / 2);
  181. return (y > neckY || height === neckHeight) ?
  182. neckWidth :
  183. neckWidth + (width - neckWidth) *
  184. (1 - (y - top) / (height - neckHeight));
  185. };
  186. series.getX = function (y, half, point) {
  187. return centerX + (half ? -1 : 1) *
  188. ((getWidthAt(reversed ? 2 * centerY - y : y) / 2) +
  189. point.labelDistance);
  190. };
  191. // Expose
  192. series.center = [centerX, centerY, height];
  193. series.centerX = centerX;
  194. /*
  195. Individual point coordinate naming:
  196. x1,y1 _________________ x2,y1
  197. \ /
  198. \ /
  199. \ /
  200. \ /
  201. \ /
  202. x3,y3 _________ x4,y3
  203. Additional for the base of the neck:
  204. | |
  205. | |
  206. | |
  207. x3,y5 _________ x4,y5
  208. */
  209. // get the total sum
  210. data.forEach(function (point) {
  211. if (!ignoreHiddenPoint || point.visible !== false) {
  212. sum += point.y;
  213. }
  214. });
  215. data.forEach(function (point) {
  216. // set start and end positions
  217. y5 = null;
  218. fraction = sum ? point.y / sum : 0;
  219. y1 = centerY - height / 2 + cumulative * height;
  220. y3 = y1 + fraction * height;
  221. tempWidth = getWidthAt(y1);
  222. x1 = centerX - tempWidth / 2;
  223. x2 = x1 + tempWidth;
  224. tempWidth = getWidthAt(y3);
  225. x3 = centerX - tempWidth / 2;
  226. x4 = x3 + tempWidth;
  227. // the entire point is within the neck
  228. if (y1 > neckY) {
  229. x1 = x3 = centerX - neckWidth / 2;
  230. x2 = x4 = centerX + neckWidth / 2;
  231. // the base of the neck
  232. } else if (y3 > neckY) {
  233. y5 = y3;
  234. tempWidth = getWidthAt(neckY);
  235. x3 = centerX - tempWidth / 2;
  236. x4 = x3 + tempWidth;
  237. y3 = neckY;
  238. }
  239. if (reversed) {
  240. y1 = 2 * centerY - y1;
  241. y3 = 2 * centerY - y3;
  242. if (y5 !== null) {
  243. y5 = 2 * centerY - y5;
  244. }
  245. }
  246. // save the path
  247. path = [
  248. 'M',
  249. x1, y1,
  250. 'L',
  251. x2, y1,
  252. x4, y3
  253. ];
  254. if (y5 !== null) {
  255. path.push(x4, y5, x3, y5);
  256. }
  257. path.push(x3, y3, 'Z');
  258. // prepare for using shared dr
  259. point.shapeType = 'path';
  260. point.shapeArgs = { d: path };
  261. // for tooltips and data labels
  262. point.percentage = fraction * 100;
  263. point.plotX = centerX;
  264. point.plotY = (y1 + (y5 || y3)) / 2;
  265. // Placement of tooltips and data labels
  266. point.tooltipPos = [
  267. centerX,
  268. point.plotY
  269. ];
  270. // Slice is a noop on funnel points
  271. point.slice = noop;
  272. // Mimicking pie data label placement logic
  273. point.half = half;
  274. if (!ignoreHiddenPoint || point.visible !== false) {
  275. cumulative += fraction;
  276. }
  277. });
  278. },
  279. // Funnel items don't have angles (#2289)
  280. sortByAngle: function (points) {
  281. points.sort(function (a, b) {
  282. return a.plotY - b.plotY;
  283. });
  284. },
  285. // Extend the pie data label method
  286. drawDataLabels: function () {
  287. var series = this,
  288. data = series.data,
  289. labelDistance = series.options.dataLabels.distance,
  290. leftSide,
  291. sign,
  292. point,
  293. i = data.length,
  294. x,
  295. y;
  296. // In the original pie label anticollision logic, the slots are
  297. // distributed from one labelDistance above to one labelDistance below
  298. // the pie. In funnels we don't want this.
  299. series.center[2] -= 2 * labelDistance;
  300. // Set the label position array for each point.
  301. while (i--) {
  302. point = data[i];
  303. leftSide = point.half;
  304. sign = leftSide ? 1 : -1;
  305. y = point.plotY;
  306. point.labelDistance = pick(
  307. point.options.dataLabels && point.options.dataLabels.distance,
  308. labelDistance
  309. );
  310. series.maxLabelDistance = Math.max(
  311. point.labelDistance,
  312. series.maxLabelDistance || 0
  313. );
  314. x = series.getX(y, leftSide, point);
  315. // set the anchor point for data labels
  316. point.labelPosition = {
  317. // initial position of the data label - it's utilized for
  318. // finding the final position for the label
  319. natural: {
  320. x: 0,
  321. y: y
  322. },
  323. 'final': {
  324. // used for generating connector path -
  325. // initialized later in drawDataLabels function
  326. // x: undefined,
  327. // y: undefined
  328. },
  329. // left - funnel on the left side of the data label
  330. // right - funnel on the right side of the data label
  331. alignment: leftSide ? 'right' : 'left',
  332. connectorPosition: {
  333. breakAt: { // used in connectorShapes.fixedOffset
  334. x: x + (point.labelDistance - 5) * sign,
  335. y: y
  336. },
  337. touchingSliceAt: {
  338. x: x + point.labelDistance * sign,
  339. y: y
  340. }
  341. }
  342. };
  343. }
  344. seriesTypes.pie.prototype.drawDataLabels.call(this);
  345. }
  346. });
  347. /**
  348. * A `funnel` series. If the [type](#series.funnel.type) option is
  349. * not specified, it is inherited from [chart.type](#chart.type).
  350. *
  351. * @extends series,plotOptions.funnel
  352. * @excluding dataParser, dataURL, stack, xAxis, yAxis
  353. * @product highcharts
  354. * @apioption series.funnel
  355. */
  356. /**
  357. * An array of data points for the series. For the `funnel` series type,
  358. * points can be given in the following ways:
  359. *
  360. * 1. An array of numerical values. In this case, the numerical values
  361. * will be interpreted as `y` options. Example:
  362. *
  363. * ```js
  364. * data: [0, 5, 3, 5]
  365. * ```
  366. *
  367. * 2. An array of objects with named values. The following snippet shows only a
  368. * few settings, see the complete options set below. If the total number of data
  369. * points exceeds the series' [turboThreshold](#series.funnel.turboThreshold),
  370. * this option is not available.
  371. *
  372. * ```js
  373. * data: [{
  374. * y: 3,
  375. * name: "Point2",
  376. * color: "#00FF00"
  377. * }, {
  378. * y: 1,
  379. * name: "Point1",
  380. * color: "#FF00FF"
  381. * }]
  382. * ```
  383. *
  384. * @sample {highcharts} highcharts/chart/reflow-true/
  385. * Numerical values
  386. * @sample {highcharts} highcharts/series/data-array-of-arrays/
  387. * Arrays of numeric x and y
  388. * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
  389. * Arrays of datetime x and y
  390. * @sample {highcharts} highcharts/series/data-array-of-name-value/
  391. * Arrays of point.name and y
  392. * @sample {highcharts} highcharts/series/data-array-of-objects/
  393. * Config objects
  394. *
  395. * @type {Array<number|*>}
  396. * @extends series.pie.data
  397. * @excluding sliced
  398. * @product highcharts
  399. * @apioption series.funnel.data
  400. */
  401. /**
  402. * Pyramid series type.
  403. *
  404. * @private
  405. * @class
  406. * @name Highcharts.seriesTypes.pyramid
  407. *
  408. * @augments Highcharts.Series
  409. */
  410. seriesType('pyramid', 'funnel',
  411. /**
  412. * A pyramid series is a special type of funnel, without neck and reversed by
  413. * default. Requires the funnel module.
  414. *
  415. * @sample highcharts/demo/pyramid/
  416. * Pyramid chart
  417. *
  418. * @extends plotOptions.funnel
  419. * @product highcharts
  420. * @optionparent plotOptions.pyramid
  421. */
  422. {
  423. /**
  424. * The pyramid neck width is zero by default, as opposed to the funnel,
  425. * which shares the same layout logic.
  426. *
  427. * @since 3.0.10
  428. */
  429. neckWidth: '0%',
  430. /**
  431. * The pyramid neck width is zero by default, as opposed to the funnel,
  432. * which shares the same layout logic.
  433. *
  434. * @since 3.0.10
  435. */
  436. neckHeight: '0%',
  437. /**
  438. * The pyramid is reversed by default, as opposed to the funnel, which
  439. * shares the layout engine, and is not reversed.
  440. *
  441. * @since 3.0.10
  442. */
  443. reversed: true
  444. });
  445. /**
  446. * A `pyramid` series. If the [type](#series.pyramid.type) option is
  447. * not specified, it is inherited from [chart.type](#chart.type).
  448. *
  449. * @extends series,plotOptions.pyramid
  450. * @excluding dataParser, dataURL, stack, xAxis, yAxis
  451. * @product highcharts
  452. * @apioption series.pyramid
  453. */
  454. /**
  455. * An array of data points for the series. For the `pyramid` series
  456. * type, points can be given in the following ways:
  457. *
  458. * 1. An array of numerical values. In this case, the numerical values will be
  459. * interpreted as `y` options. Example:
  460. * ```js
  461. * data: [0, 5, 3, 5]
  462. * ```
  463. *
  464. * 2. An array of objects with named values. The following snippet shows only a
  465. * few settings, see the complete options set below. If the total number of
  466. * data points exceeds the series'
  467. * [turboThreshold](#series.pyramid.turboThreshold), this option is not
  468. * available.
  469. * ```js
  470. * data: [{
  471. * y: 9,
  472. * name: "Point2",
  473. * color: "#00FF00"
  474. * }, {
  475. * y: 6,
  476. * name: "Point1",
  477. * color: "#FF00FF"
  478. * }]
  479. * ```
  480. *
  481. * @sample {highcharts} highcharts/chart/reflow-true/
  482. * Numerical values
  483. * @sample {highcharts} highcharts/series/data-array-of-objects/
  484. * Config objects
  485. *
  486. * @type {Array<number|*>}
  487. * @extends series.pie.data
  488. * @excluding sliced
  489. * @product highcharts
  490. * @apioption series.pyramid.data
  491. */