| | |
| | | /** |
| | | * @license |
| | | * Highcharts funnel module, Beta |
| | | * |
| | | * (c) 2010-2012 Torstein Hønsi |
| | | * |
| | | * License: www.highcharts.com/license |
| | | */ |
| | | |
| | | /*global Highcharts */ |
| | | (function (Highcharts) { |
| | | |
| | | 'use strict'; |
| | | |
| | | // create shortcuts |
| | | var defaultOptions = Highcharts.getOptions(), |
| | | defaultPlotOptions = defaultOptions.plotOptions, |
| | | seriesTypes = Highcharts.seriesTypes, |
| | | merge = Highcharts.merge, |
| | | noop = function () {}, |
| | | each = Highcharts.each; |
| | | |
| | | // set default options |
| | | defaultPlotOptions.funnel = merge(defaultPlotOptions.pie, { |
| | | center: ['50%', '50%'], |
| | | width: '90%', |
| | | neckWidth: '30%', |
| | | height: '100%', |
| | | neckHeight: '25%', |
| | | |
| | | dataLabels: { |
| | | //position: 'right', |
| | | connectorWidth: 1, |
| | | connectorColor: '#606060' |
| | | }, |
| | | size: true, // to avoid adapting to data label size in Pie.drawDataLabels |
| | | states: { |
| | | select: { |
| | | color: '#C0C0C0', |
| | | borderColor: '#000000', |
| | | shadow: false |
| | | } |
| | | } |
| | | }); |
| | | |
| | | |
| | | seriesTypes.funnel = Highcharts.extendClass(seriesTypes.pie, { |
| | | |
| | | type: 'funnel', |
| | | animate: noop, |
| | | |
| | | /** |
| | | * Overrides the pie translate method |
| | | */ |
| | | translate: function () { |
| | | |
| | | var |
| | | // Get positions - either an integer or a percentage string must be given |
| | | getLength = function (length, relativeTo) { |
| | | return (/%$/).test(length) ? |
| | | relativeTo * parseInt(length, 10) / 100 : |
| | | parseInt(length, 10); |
| | | }, |
| | | |
| | | sum = 0, |
| | | series = this, |
| | | chart = series.chart, |
| | | plotWidth = chart.plotWidth, |
| | | plotHeight = chart.plotHeight, |
| | | cumulative = 0, // start at top |
| | | options = series.options, |
| | | center = options.center, |
| | | centerX = getLength(center[0], plotWidth), |
| | | centerY = getLength(center[0], plotHeight), |
| | | width = getLength(options.width, plotWidth), |
| | | tempWidth, |
| | | getWidthAt, |
| | | height = getLength(options.height, plotHeight), |
| | | neckWidth = getLength(options.neckWidth, plotWidth), |
| | | neckHeight = getLength(options.neckHeight, plotHeight), |
| | | neckY = height - neckHeight, |
| | | data = series.data, |
| | | path, |
| | | fraction, |
| | | half = options.dataLabels.position === 'left' ? 1 : 0, |
| | | |
| | | x1, |
| | | y1, |
| | | x2, |
| | | x3, |
| | | y3, |
| | | x4, |
| | | y5; |
| | | |
| | | // Return the width at a specific y coordinate |
| | | series.getWidthAt = getWidthAt = function (y) { |
| | | return y > height - neckHeight || height === neckHeight ? |
| | | neckWidth : |
| | | neckWidth + (width - neckWidth) * ((height - neckHeight - y) / (height - neckHeight)); |
| | | }; |
| | | series.getX = function (y, half) { |
| | | return centerX + (half ? -1 : 1) * ((getWidthAt(y) / 2) + options.dataLabels.distance); |
| | | }; |
| | | |
| | | // Expose |
| | | series.center = [centerX, centerY, height]; |
| | | series.centerX = centerX; |
| | | |
| | | /* |
| | | * Individual point coordinate naming: |
| | | * |
| | | * x1,y1 _________________ x2,y1 |
| | | * \ / |
| | | * \ / |
| | | * \ / |
| | | * \ / |
| | | * \ / |
| | | * x3,y3 _________ x4,y3 |
| | | * |
| | | * Additional for the base of the neck: |
| | | * |
| | | * | | |
| | | * | | |
| | | * | | |
| | | * x3,y5 _________ x4,y5 |
| | | */ |
| | | |
| | | |
| | | |
| | | |
| | | // get the total sum |
| | | each(data, function (point) { |
| | | sum += point.y; |
| | | }); |
| | | |
| | | each(data, function (point) { |
| | | // set start and end positions |
| | | y5 = null; |
| | | fraction = sum ? point.y / sum : 0; |
| | | y1 = centerY - height / 2 + cumulative * height; |
| | | y3 = y1 + fraction * height; |
| | | //tempWidth = neckWidth + (width - neckWidth) * ((height - neckHeight - y1) / (height - neckHeight)); |
| | | tempWidth = getWidthAt(y1); |
| | | x1 = centerX - tempWidth / 2; |
| | | x2 = x1 + tempWidth; |
| | | tempWidth = getWidthAt(y3); |
| | | x3 = centerX - tempWidth / 2; |
| | | x4 = x3 + tempWidth; |
| | | |
| | | // the entire point is within the neck |
| | | if (y1 > neckY) { |
| | | x1 = x3 = centerX - neckWidth / 2; |
| | | x2 = x4 = centerX + neckWidth / 2; |
| | | |
| | | // the base of the neck |
| | | } else if (y3 > neckY) { |
| | | y5 = y3; |
| | | |
| | | tempWidth = getWidthAt(neckY); |
| | | x3 = centerX - tempWidth / 2; |
| | | x4 = x3 + tempWidth; |
| | | |
| | | y3 = neckY; |
| | | } |
| | | |
| | | // save the path |
| | | path = [ |
| | | 'M', |
| | | x1, y1, |
| | | 'L', |
| | | x2, y1, |
| | | x4, y3 |
| | | ]; |
| | | if (y5) { |
| | | path.push(x4, y5, x3, y5); |
| | | } |
| | | path.push(x3, y3, 'Z'); |
| | | |
| | | // prepare for using shared dr |
| | | point.shapeType = 'path'; |
| | | point.shapeArgs = { d: path }; |
| | | |
| | | |
| | | // for tooltips and data labels |
| | | point.percentage = fraction * 100; |
| | | point.plotX = centerX; |
| | | point.plotY = (y1 + (y5 || y3)) / 2; |
| | | |
| | | // Placement of tooltips and data labels |
| | | point.tooltipPos = [ |
| | | centerX, |
| | | point.plotY |
| | | ]; |
| | | |
| | | // Slice is a noop on funnel points |
| | | point.slice = noop; |
| | | |
| | | // Mimicking pie data label placement logic |
| | | point.half = half; |
| | | |
| | | cumulative += fraction; |
| | | }); |
| | | |
| | | |
| | | series.setTooltipPoints(); |
| | | }, |
| | | /** |
| | | * Draw a single point (wedge) |
| | | * @param {Object} point The point object |
| | | * @param {Object} color The color of the point |
| | | * @param {Number} brightness The brightness relative to the color |
| | | */ |
| | | drawPoints: function () { |
| | | var series = this, |
| | | options = series.options, |
| | | chart = series.chart, |
| | | renderer = chart.renderer; |
| | | |
| | | each(series.data, function (point) { |
| | | |
| | | var graphic = point.graphic, |
| | | shapeArgs = point.shapeArgs; |
| | | |
| | | if (!graphic) { // Create the shapes |
| | | point.graphic = renderer.path(shapeArgs). |
| | | attr({ |
| | | fill: point.color, |
| | | stroke: options.borderColor, |
| | | 'stroke-width': options.borderWidth |
| | | }). |
| | | add(series.group); |
| | | |
| | | } else { // Update the shapes |
| | | graphic.animate(shapeArgs); |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | /** |
| | | * Funnel items don't have angles (#2289) |
| | | */ |
| | | sortByAngle: noop, |
| | | |
| | | /** |
| | | * Extend the pie data label method |
| | | */ |
| | | drawDataLabels: function () { |
| | | var data = this.data, |
| | | labelDistance = this.options.dataLabels.distance, |
| | | leftSide, |
| | | sign, |
| | | point, |
| | | i = data.length, |
| | | x, |
| | | y; |
| | | |
| | | // In the original pie label anticollision logic, the slots are distributed |
| | | // from one labelDistance above to one labelDistance below the pie. In funnels |
| | | // we don't want this. |
| | | this.center[2] -= 2 * labelDistance; |
| | | |
| | | // Set the label position array for each point. |
| | | while (i--) { |
| | | point = data[i]; |
| | | leftSide = point.half; |
| | | sign = leftSide ? 1 : -1; |
| | | y = point.plotY; |
| | | x = this.getX(y, leftSide); |
| | | |
| | | // set the anchor point for data labels |
| | | point.labelPos = [ |
| | | 0, // first break of connector |
| | | y, // a/a |
| | | x + (labelDistance - 5) * sign, // second break, right outside point shape |
| | | y, // a/a |
| | | x + labelDistance * sign, // landing point for connector |
| | | y, // a/a |
| | | leftSide ? 'right' : 'left', // alignment |
| | | 0 // center angle |
| | | ]; |
| | | } |
| | | |
| | | seriesTypes.pie.prototype.drawDataLabels.call(this); |
| | | } |
| | | |
| | | }); |
| | | |
| | | |
| | | }(Highcharts)); |
| | | /**
|
| | | * @license |
| | | * Highcharts funnel module, Beta
|
| | | *
|
| | | * (c) 2010-2012 Torstein Hønsi
|
| | | *
|
| | | * License: www.highcharts.com/license
|
| | | */
|
| | |
|
| | | /*global Highcharts */
|
| | | (function (Highcharts) {
|
| | | |
| | | 'use strict';
|
| | |
|
| | | // create shortcuts
|
| | | var defaultOptions = Highcharts.getOptions(),
|
| | | defaultPlotOptions = defaultOptions.plotOptions,
|
| | | seriesTypes = Highcharts.seriesTypes,
|
| | | merge = Highcharts.merge,
|
| | | noop = function () {},
|
| | | each = Highcharts.each;
|
| | |
|
| | | // set default options
|
| | | defaultPlotOptions.funnel = merge(defaultPlotOptions.pie, {
|
| | | center: ['50%', '50%'],
|
| | | width: '90%',
|
| | | neckWidth: '30%',
|
| | | height: '100%',
|
| | | neckHeight: '25%',
|
| | |
|
| | | dataLabels: {
|
| | | //position: 'right',
|
| | | connectorWidth: 1,
|
| | | connectorColor: '#606060'
|
| | | },
|
| | | size: true, // to avoid adapting to data label size in Pie.drawDataLabels
|
| | | states: {
|
| | | select: {
|
| | | color: '#C0C0C0',
|
| | | borderColor: '#000000',
|
| | | shadow: false
|
| | | }
|
| | | } |
| | | });
|
| | |
|
| | |
|
| | | seriesTypes.funnel = Highcharts.extendClass(seriesTypes.pie, {
|
| | | |
| | | type: 'funnel',
|
| | | animate: noop,
|
| | |
|
| | | /**
|
| | | * Overrides the pie translate method
|
| | | */
|
| | | translate: function () {
|
| | | |
| | | var |
| | | // Get positions - either an integer or a percentage string must be given
|
| | | getLength = function (length, relativeTo) {
|
| | | return (/%$/).test(length) ?
|
| | | relativeTo * parseInt(length, 10) / 100 :
|
| | | parseInt(length, 10);
|
| | | },
|
| | | |
| | | sum = 0,
|
| | | series = this,
|
| | | chart = series.chart,
|
| | | plotWidth = chart.plotWidth,
|
| | | plotHeight = chart.plotHeight,
|
| | | cumulative = 0, // start at top
|
| | | options = series.options,
|
| | | center = options.center,
|
| | | centerX = getLength(center[0], plotWidth),
|
| | | centerY = getLength(center[0], plotHeight),
|
| | | width = getLength(options.width, plotWidth),
|
| | | tempWidth,
|
| | | getWidthAt,
|
| | | height = getLength(options.height, plotHeight),
|
| | | neckWidth = getLength(options.neckWidth, plotWidth),
|
| | | neckHeight = getLength(options.neckHeight, plotHeight),
|
| | | neckY = height - neckHeight,
|
| | | data = series.data,
|
| | | path,
|
| | | fraction,
|
| | | half = options.dataLabels.position === 'left' ? 1 : 0,
|
| | |
|
| | | x1, |
| | | y1, |
| | | x2, |
| | | x3, |
| | | y3, |
| | | x4, |
| | | y5;
|
| | |
|
| | | // Return the width at a specific y coordinate
|
| | | series.getWidthAt = getWidthAt = function (y) {
|
| | | return y > height - neckHeight || height === neckHeight ?
|
| | | neckWidth :
|
| | | neckWidth + (width - neckWidth) * ((height - neckHeight - y) / (height - neckHeight));
|
| | | };
|
| | | series.getX = function (y, half) {
|
| | | return centerX + (half ? -1 : 1) * ((getWidthAt(y) / 2) + options.dataLabels.distance);
|
| | | };
|
| | |
|
| | | // Expose
|
| | | series.center = [centerX, centerY, height];
|
| | | series.centerX = centerX;
|
| | |
|
| | | /*
|
| | | * Individual point coordinate naming:
|
| | | *
|
| | | * x1,y1 _________________ x2,y1
|
| | | * \ /
|
| | | * \ /
|
| | | * \ /
|
| | | * \ /
|
| | | * \ /
|
| | | * x3,y3 _________ x4,y3
|
| | | *
|
| | | * Additional for the base of the neck:
|
| | | *
|
| | | * | |
|
| | | * | |
|
| | | * | |
|
| | | * x3,y5 _________ x4,y5
|
| | | */
|
| | |
|
| | |
|
| | |
|
| | |
|
| | | // get the total sum
|
| | | each(data, function (point) {
|
| | | sum += point.y;
|
| | | });
|
| | |
|
| | | each(data, function (point) {
|
| | | // set start and end positions
|
| | | y5 = null;
|
| | | fraction = sum ? point.y / sum : 0;
|
| | | y1 = centerY - height / 2 + cumulative * height;
|
| | | y3 = y1 + fraction * height;
|
| | | //tempWidth = neckWidth + (width - neckWidth) * ((height - neckHeight - y1) / (height - neckHeight));
|
| | | tempWidth = getWidthAt(y1);
|
| | | x1 = centerX - tempWidth / 2;
|
| | | x2 = x1 + tempWidth;
|
| | | tempWidth = getWidthAt(y3);
|
| | | x3 = centerX - tempWidth / 2;
|
| | | x4 = x3 + tempWidth;
|
| | |
|
| | | // the entire point is within the neck
|
| | | if (y1 > neckY) {
|
| | | x1 = x3 = centerX - neckWidth / 2;
|
| | | x2 = x4 = centerX + neckWidth / 2;
|
| | | |
| | | // the base of the neck
|
| | | } else if (y3 > neckY) {
|
| | | y5 = y3;
|
| | |
|
| | | tempWidth = getWidthAt(neckY);
|
| | | x3 = centerX - tempWidth / 2;
|
| | | x4 = x3 + tempWidth;
|
| | |
|
| | | y3 = neckY;
|
| | | }
|
| | |
|
| | | // save the path
|
| | | path = [
|
| | | 'M',
|
| | | x1, y1,
|
| | | 'L',
|
| | | x2, y1,
|
| | | x4, y3
|
| | | ];
|
| | | if (y5) {
|
| | | path.push(x4, y5, x3, y5);
|
| | | }
|
| | | path.push(x3, y3, 'Z');
|
| | |
|
| | | // prepare for using shared dr
|
| | | point.shapeType = 'path';
|
| | | point.shapeArgs = { d: path };
|
| | |
|
| | |
|
| | | // for tooltips and data labels
|
| | | point.percentage = fraction * 100;
|
| | | point.plotX = centerX;
|
| | | point.plotY = (y1 + (y5 || y3)) / 2;
|
| | |
|
| | | // Placement of tooltips and data labels
|
| | | point.tooltipPos = [
|
| | | centerX,
|
| | | point.plotY
|
| | | ];
|
| | |
|
| | | // Slice is a noop on funnel points
|
| | | point.slice = noop;
|
| | | |
| | | // Mimicking pie data label placement logic
|
| | | point.half = half;
|
| | |
|
| | | cumulative += fraction;
|
| | | });
|
| | |
|
| | |
|
| | | series.setTooltipPoints();
|
| | | },
|
| | | /**
|
| | | * Draw a single point (wedge)
|
| | | * @param {Object} point The point object
|
| | | * @param {Object} color The color of the point
|
| | | * @param {Number} brightness The brightness relative to the color
|
| | | */
|
| | | drawPoints: function () {
|
| | | var series = this,
|
| | | options = series.options,
|
| | | chart = series.chart,
|
| | | renderer = chart.renderer;
|
| | |
|
| | | each(series.data, function (point) {
|
| | | |
| | | var graphic = point.graphic,
|
| | | shapeArgs = point.shapeArgs;
|
| | |
|
| | | if (!graphic) { // Create the shapes
|
| | | point.graphic = renderer.path(shapeArgs).
|
| | | attr({
|
| | | fill: point.color,
|
| | | stroke: options.borderColor,
|
| | | 'stroke-width': options.borderWidth
|
| | | }).
|
| | | add(series.group);
|
| | | |
| | | } else { // Update the shapes
|
| | | graphic.animate(shapeArgs);
|
| | | }
|
| | | });
|
| | | },
|
| | |
|
| | | /**
|
| | | * Funnel items don't have angles (#2289)
|
| | | */
|
| | | sortByAngle: noop,
|
| | | |
| | | /**
|
| | | * Extend the pie data label method
|
| | | */
|
| | | drawDataLabels: function () {
|
| | | var data = this.data,
|
| | | labelDistance = this.options.dataLabels.distance,
|
| | | leftSide,
|
| | | sign,
|
| | | point,
|
| | | i = data.length,
|
| | | x,
|
| | | y;
|
| | | |
| | | // In the original pie label anticollision logic, the slots are distributed
|
| | | // from one labelDistance above to one labelDistance below the pie. In funnels
|
| | | // we don't want this.
|
| | | this.center[2] -= 2 * labelDistance;
|
| | | |
| | | // Set the label position array for each point.
|
| | | while (i--) {
|
| | | point = data[i];
|
| | | leftSide = point.half;
|
| | | sign = leftSide ? 1 : -1;
|
| | | y = point.plotY;
|
| | | x = this.getX(y, leftSide);
|
| | | |
| | | // set the anchor point for data labels
|
| | | point.labelPos = [
|
| | | 0, // first break of connector
|
| | | y, // a/a
|
| | | x + (labelDistance - 5) * sign, // second break, right outside point shape
|
| | | y, // a/a
|
| | | x + labelDistance * sign, // landing point for connector
|
| | | y, // a/a
|
| | | leftSide ? 'right' : 'left', // alignment
|
| | | 0 // center angle
|
| | | ];
|
| | | }
|
| | | |
| | | seriesTypes.pie.prototype.drawDataLabels.call(this);
|
| | | }
|
| | |
|
| | | });
|
| | |
|
| | |
|
| | | }(Highcharts));
|