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.
2430 lines
59 KiB
2430 lines
59 KiB
// ==ClosureCompiler== |
|
// @compilation_level SIMPLE_OPTIMIZATIONS |
|
|
|
/** |
|
* @license Highcharts JS v3.0.6 (2013-10-04) |
|
* |
|
* (c) 2009-2013 Torstein Hønsi |
|
* |
|
* License: www.highcharts.com/license |
|
*/ |
|
|
|
// JSLint options: |
|
/*global Highcharts, HighchartsAdapter, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console */ |
|
|
|
(function (Highcharts, UNDEFINED) { |
|
var arrayMin = Highcharts.arrayMin, |
|
arrayMax = Highcharts.arrayMax, |
|
each = Highcharts.each, |
|
extend = Highcharts.extend, |
|
merge = Highcharts.merge, |
|
map = Highcharts.map, |
|
pick = Highcharts.pick, |
|
pInt = Highcharts.pInt, |
|
defaultPlotOptions = Highcharts.getOptions().plotOptions, |
|
seriesTypes = Highcharts.seriesTypes, |
|
extendClass = Highcharts.extendClass, |
|
splat = Highcharts.splat, |
|
wrap = Highcharts.wrap, |
|
Axis = Highcharts.Axis, |
|
Tick = Highcharts.Tick, |
|
Series = Highcharts.Series, |
|
colProto = seriesTypes.column.prototype, |
|
math = Math, |
|
mathRound = math.round, |
|
mathFloor = math.floor, |
|
mathMax = math.max, |
|
noop = function () {};/** |
|
* The Pane object allows options that are common to a set of X and Y axes. |
|
* |
|
* In the future, this can be extended to basic Highcharts and Highstock. |
|
*/ |
|
function Pane(options, chart, firstAxis) { |
|
this.init.call(this, options, chart, firstAxis); |
|
} |
|
|
|
// Extend the Pane prototype |
|
extend(Pane.prototype, { |
|
|
|
/** |
|
* Initiate the Pane object |
|
*/ |
|
init: function (options, chart, firstAxis) { |
|
var pane = this, |
|
backgroundOption, |
|
defaultOptions = pane.defaultOptions; |
|
|
|
pane.chart = chart; |
|
|
|
// Set options |
|
if (chart.angular) { // gauges |
|
defaultOptions.background = {}; // gets extended by this.defaultBackgroundOptions |
|
} |
|
pane.options = options = merge(defaultOptions, options); |
|
|
|
backgroundOption = options.background; |
|
|
|
// To avoid having weighty logic to place, update and remove the backgrounds, |
|
// push them to the first axis' plot bands and borrow the existing logic there. |
|
if (backgroundOption) { |
|
each([].concat(splat(backgroundOption)).reverse(), function (config) { |
|
var backgroundColor = config.backgroundColor; // if defined, replace the old one (specific for gradients) |
|
config = merge(pane.defaultBackgroundOptions, config); |
|
if (backgroundColor) { |
|
config.backgroundColor = backgroundColor; |
|
} |
|
config.color = config.backgroundColor; // due to naming in plotBands |
|
firstAxis.options.plotBands.unshift(config); |
|
}); |
|
} |
|
}, |
|
|
|
/** |
|
* The default options object |
|
*/ |
|
defaultOptions: { |
|
// background: {conditional}, |
|
center: ['50%', '50%'], |
|
size: '85%', |
|
startAngle: 0 |
|
//endAngle: startAngle + 360 |
|
}, |
|
|
|
/** |
|
* The default background options |
|
*/ |
|
defaultBackgroundOptions: { |
|
shape: 'circle', |
|
borderWidth: 1, |
|
borderColor: 'silver', |
|
backgroundColor: { |
|
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, |
|
stops: [ |
|
[0, '#FFF'], |
|
[1, '#DDD'] |
|
] |
|
}, |
|
from: Number.MIN_VALUE, // corrected to axis min |
|
innerRadius: 0, |
|
to: Number.MAX_VALUE, // corrected to axis max |
|
outerRadius: '105%' |
|
} |
|
|
|
}); |
|
var axisProto = Axis.prototype, |
|
tickProto = Tick.prototype; |
|
|
|
/** |
|
* Augmented methods for the x axis in order to hide it completely, used for the X axis in gauges |
|
*/ |
|
var hiddenAxisMixin = { |
|
getOffset: noop, |
|
redraw: function () { |
|
this.isDirty = false; // prevent setting Y axis dirty |
|
}, |
|
render: function () { |
|
this.isDirty = false; // prevent setting Y axis dirty |
|
}, |
|
setScale: noop, |
|
setCategories: noop, |
|
setTitle: noop |
|
}; |
|
|
|
/** |
|
* Augmented methods for the value axis |
|
*/ |
|
/*jslint unparam: true*/ |
|
var radialAxisMixin = { |
|
isRadial: true, |
|
|
|
/** |
|
* The default options extend defaultYAxisOptions |
|
*/ |
|
defaultRadialGaugeOptions: { |
|
labels: { |
|
align: 'center', |
|
x: 0, |
|
y: null // auto |
|
}, |
|
minorGridLineWidth: 0, |
|
minorTickInterval: 'auto', |
|
minorTickLength: 10, |
|
minorTickPosition: 'inside', |
|
minorTickWidth: 1, |
|
plotBands: [], |
|
tickLength: 10, |
|
tickPosition: 'inside', |
|
tickWidth: 2, |
|
title: { |
|
rotation: 0 |
|
}, |
|
zIndex: 2 // behind dials, points in the series group |
|
}, |
|
|
|
// Circular axis around the perimeter of a polar chart |
|
defaultRadialXOptions: { |
|
gridLineWidth: 1, // spokes |
|
labels: { |
|
align: null, // auto |
|
distance: 15, |
|
x: 0, |
|
y: null // auto |
|
}, |
|
maxPadding: 0, |
|
minPadding: 0, |
|
plotBands: [], |
|
showLastLabel: false, |
|
tickLength: 0 |
|
}, |
|
|
|
// Radial axis, like a spoke in a polar chart |
|
defaultRadialYOptions: { |
|
gridLineInterpolation: 'circle', |
|
labels: { |
|
align: 'right', |
|
x: -3, |
|
y: -2 |
|
}, |
|
plotBands: [], |
|
showLastLabel: false, |
|
title: { |
|
x: 4, |
|
text: null, |
|
rotation: 90 |
|
} |
|
}, |
|
|
|
/** |
|
* Merge and set options |
|
*/ |
|
setOptions: function (userOptions) { |
|
|
|
this.options = merge( |
|
this.defaultOptions, |
|
this.defaultRadialOptions, |
|
userOptions |
|
); |
|
|
|
}, |
|
|
|
/** |
|
* Wrap the getOffset method to return zero offset for title or labels in a radial |
|
* axis |
|
*/ |
|
getOffset: function () { |
|
// Call the Axis prototype method (the method we're in now is on the instance) |
|
axisProto.getOffset.call(this); |
|
|
|
// Title or label offsets are not counted |
|
this.chart.axisOffset[this.side] = 0; |
|
}, |
|
|
|
|
|
/** |
|
* Get the path for the axis line. This method is also referenced in the getPlotLinePath |
|
* method. |
|
*/ |
|
getLinePath: function (lineWidth, radius) { |
|
var center = this.center; |
|
radius = pick(radius, center[2] / 2 - this.offset); |
|
|
|
return this.chart.renderer.symbols.arc( |
|
this.left + center[0], |
|
this.top + center[1], |
|
radius, |
|
radius, |
|
{ |
|
start: this.startAngleRad, |
|
end: this.endAngleRad, |
|
open: true, |
|
innerR: 0 |
|
} |
|
); |
|
}, |
|
|
|
/** |
|
* Override setAxisTranslation by setting the translation to the difference |
|
* in rotation. This allows the translate method to return angle for |
|
* any given value. |
|
*/ |
|
setAxisTranslation: function () { |
|
|
|
// Call uber method |
|
axisProto.setAxisTranslation.call(this); |
|
|
|
// Set transA and minPixelPadding |
|
if (this.center) { // it's not defined the first time |
|
if (this.isCircular) { |
|
|
|
this.transA = (this.endAngleRad - this.startAngleRad) / |
|
((this.max - this.min) || 1); |
|
|
|
|
|
} else { |
|
this.transA = (this.center[2] / 2) / ((this.max - this.min) || 1); |
|
} |
|
|
|
if (this.isXAxis) { |
|
this.minPixelPadding = this.transA * this.minPointOffset + |
|
(this.reversed ? (this.endAngleRad - this.startAngleRad) / 4 : 0); // ??? |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* In case of auto connect, add one closestPointRange to the max value right before |
|
* tickPositions are computed, so that ticks will extend passed the real max. |
|
*/ |
|
beforeSetTickPositions: function () { |
|
if (this.autoConnect) { |
|
this.max += (this.categories && 1) || this.pointRange || this.closestPointRange || 0; // #1197, #2260 |
|
} |
|
}, |
|
|
|
/** |
|
* Override the setAxisSize method to use the arc's circumference as length. This |
|
* allows tickPixelInterval to apply to pixel lengths along the perimeter |
|
*/ |
|
setAxisSize: function () { |
|
|
|
axisProto.setAxisSize.call(this); |
|
|
|
if (this.isRadial) { |
|
|
|
// Set the center array |
|
this.center = this.pane.center = seriesTypes.pie.prototype.getCenter.call(this.pane); |
|
|
|
this.len = this.width = this.height = this.isCircular ? |
|
this.center[2] * (this.endAngleRad - this.startAngleRad) / 2 : |
|
this.center[2] / 2; |
|
} |
|
}, |
|
|
|
/** |
|
* Returns the x, y coordinate of a point given by a value and a pixel distance |
|
* from center |
|
*/ |
|
getPosition: function (value, length) { |
|
if (!this.isCircular) { |
|
length = this.translate(value); |
|
value = this.min; |
|
} |
|
|
|
return this.postTranslate( |
|
this.translate(value), |
|
pick(length, this.center[2] / 2) - this.offset |
|
); |
|
}, |
|
|
|
/** |
|
* Translate from intermediate plotX (angle), plotY (axis.len - radius) to final chart coordinates. |
|
*/ |
|
postTranslate: function (angle, radius) { |
|
|
|
var chart = this.chart, |
|
center = this.center; |
|
|
|
angle = this.startAngleRad + angle; |
|
|
|
return { |
|
x: chart.plotLeft + center[0] + Math.cos(angle) * radius, |
|
y: chart.plotTop + center[1] + Math.sin(angle) * radius |
|
}; |
|
|
|
}, |
|
|
|
/** |
|
* Find the path for plot bands along the radial axis |
|
*/ |
|
getPlotBandPath: function (from, to, options) { |
|
var center = this.center, |
|
startAngleRad = this.startAngleRad, |
|
fullRadius = center[2] / 2, |
|
radii = [ |
|
pick(options.outerRadius, '100%'), |
|
options.innerRadius, |
|
pick(options.thickness, 10) |
|
], |
|
percentRegex = /%$/, |
|
start, |
|
end, |
|
open, |
|
isCircular = this.isCircular, // X axis in a polar chart |
|
ret; |
|
|
|
// Polygonal plot bands |
|
if (this.options.gridLineInterpolation === 'polygon') { |
|
ret = this.getPlotLinePath(from).concat(this.getPlotLinePath(to, true)); |
|
|
|
// Circular grid bands |
|
} else { |
|
|
|
// Plot bands on Y axis (radial axis) - inner and outer radius depend on to and from |
|
if (!isCircular) { |
|
radii[0] = this.translate(from); |
|
radii[1] = this.translate(to); |
|
} |
|
|
|
// Convert percentages to pixel values |
|
radii = map(radii, function (radius) { |
|
if (percentRegex.test(radius)) { |
|
radius = (pInt(radius, 10) * fullRadius) / 100; |
|
} |
|
return radius; |
|
}); |
|
|
|
// Handle full circle |
|
if (options.shape === 'circle' || !isCircular) { |
|
start = -Math.PI / 2; |
|
end = Math.PI * 1.5; |
|
open = true; |
|
} else { |
|
start = startAngleRad + this.translate(from); |
|
end = startAngleRad + this.translate(to); |
|
} |
|
|
|
|
|
ret = this.chart.renderer.symbols.arc( |
|
this.left + center[0], |
|
this.top + center[1], |
|
radii[0], |
|
radii[0], |
|
{ |
|
start: start, |
|
end: end, |
|
innerR: pick(radii[1], radii[0] - radii[2]), |
|
open: open |
|
} |
|
); |
|
} |
|
|
|
return ret; |
|
}, |
|
|
|
/** |
|
* Find the path for plot lines perpendicular to the radial axis. |
|
*/ |
|
getPlotLinePath: function (value, reverse) { |
|
var axis = this, |
|
center = axis.center, |
|
chart = axis.chart, |
|
end = axis.getPosition(value), |
|
xAxis, |
|
xy, |
|
tickPositions, |
|
ret; |
|
|
|
// Spokes |
|
if (axis.isCircular) { |
|
ret = ['M', center[0] + chart.plotLeft, center[1] + chart.plotTop, 'L', end.x, end.y]; |
|
|
|
// Concentric circles |
|
} else if (axis.options.gridLineInterpolation === 'circle') { |
|
value = axis.translate(value); |
|
if (value) { // a value of 0 is in the center |
|
ret = axis.getLinePath(0, value); |
|
} |
|
// Concentric polygons |
|
} else { |
|
xAxis = chart.xAxis[0]; |
|
ret = []; |
|
value = axis.translate(value); |
|
tickPositions = xAxis.tickPositions; |
|
if (xAxis.autoConnect) { |
|
tickPositions = tickPositions.concat([tickPositions[0]]); |
|
} |
|
// Reverse the positions for concatenation of polygonal plot bands |
|
if (reverse) { |
|
tickPositions = [].concat(tickPositions).reverse(); |
|
} |
|
|
|
each(tickPositions, function (pos, i) { |
|
xy = xAxis.getPosition(pos, value); |
|
ret.push(i ? 'L' : 'M', xy.x, xy.y); |
|
}); |
|
|
|
} |
|
return ret; |
|
}, |
|
|
|
/** |
|
* Find the position for the axis title, by default inside the gauge |
|
*/ |
|
getTitlePosition: function () { |
|
var center = this.center, |
|
chart = this.chart, |
|
titleOptions = this.options.title; |
|
|
|
return { |
|
x: chart.plotLeft + center[0] + (titleOptions.x || 0), |
|
y: chart.plotTop + center[1] - ({ high: 0.5, middle: 0.25, low: 0 }[titleOptions.align] * |
|
center[2]) + (titleOptions.y || 0) |
|
}; |
|
} |
|
|
|
}; |
|
/*jslint unparam: false*/ |
|
|
|
/** |
|
* Override axisProto.init to mix in special axis instance functions and function overrides |
|
*/ |
|
wrap(axisProto, 'init', function (proceed, chart, userOptions) { |
|
var axis = this, |
|
angular = chart.angular, |
|
polar = chart.polar, |
|
isX = userOptions.isX, |
|
isHidden = angular && isX, |
|
isCircular, |
|
startAngleRad, |
|
endAngleRad, |
|
options, |
|
chartOptions = chart.options, |
|
paneIndex = userOptions.pane || 0, |
|
pane, |
|
paneOptions; |
|
|
|
// Before prototype.init |
|
if (angular) { |
|
extend(this, isHidden ? hiddenAxisMixin : radialAxisMixin); |
|
isCircular = !isX; |
|
if (isCircular) { |
|
this.defaultRadialOptions = this.defaultRadialGaugeOptions; |
|
} |
|
|
|
} else if (polar) { |
|
//extend(this, userOptions.isX ? radialAxisMixin : radialAxisMixin); |
|
extend(this, radialAxisMixin); |
|
isCircular = isX; |
|
this.defaultRadialOptions = isX ? this.defaultRadialXOptions : merge(this.defaultYAxisOptions, this.defaultRadialYOptions); |
|
|
|
} |
|
|
|
// Run prototype.init |
|
proceed.call(this, chart, userOptions); |
|
|
|
if (!isHidden && (angular || polar)) { |
|
options = this.options; |
|
|
|
// Create the pane and set the pane options. |
|
if (!chart.panes) { |
|
chart.panes = []; |
|
} |
|
this.pane = pane = chart.panes[paneIndex] = chart.panes[paneIndex] || new Pane( |
|
splat(chartOptions.pane)[paneIndex], |
|
chart, |
|
axis |
|
); |
|
paneOptions = pane.options; |
|
|
|
|
|
// Disable certain features on angular and polar axes |
|
chart.inverted = false; |
|
chartOptions.chart.zoomType = null; |
|
|
|
// Start and end angle options are |
|
// given in degrees relative to top, while internal computations are |
|
// in radians relative to right (like SVG). |
|
this.startAngleRad = startAngleRad = (paneOptions.startAngle - 90) * Math.PI / 180; |
|
this.endAngleRad = endAngleRad = (pick(paneOptions.endAngle, paneOptions.startAngle + 360) - 90) * Math.PI / 180; |
|
this.offset = options.offset || 0; |
|
|
|
this.isCircular = isCircular; |
|
|
|
// Automatically connect grid lines? |
|
if (isCircular && userOptions.max === UNDEFINED && endAngleRad - startAngleRad === 2 * Math.PI) { |
|
this.autoConnect = true; |
|
} |
|
} |
|
|
|
}); |
|
|
|
/** |
|
* Add special cases within the Tick class' methods for radial axes. |
|
*/ |
|
wrap(tickProto, 'getPosition', function (proceed, horiz, pos, tickmarkOffset, old) { |
|
var axis = this.axis; |
|
|
|
return axis.getPosition ? |
|
axis.getPosition(pos) : |
|
proceed.call(this, horiz, pos, tickmarkOffset, old); |
|
}); |
|
|
|
/** |
|
* Wrap the getLabelPosition function to find the center position of the label |
|
* based on the distance option |
|
*/ |
|
wrap(tickProto, 'getLabelPosition', function (proceed, x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { |
|
var axis = this.axis, |
|
optionsY = labelOptions.y, |
|
ret, |
|
align = labelOptions.align, |
|
angle = ((axis.translate(this.pos) + axis.startAngleRad + Math.PI / 2) / Math.PI * 180) % 360; |
|
|
|
if (axis.isRadial) { |
|
ret = axis.getPosition(this.pos, (axis.center[2] / 2) + pick(labelOptions.distance, -25)); |
|
|
|
// Automatically rotated |
|
if (labelOptions.rotation === 'auto') { |
|
label.attr({ |
|
rotation: angle |
|
}); |
|
|
|
// Vertically centered |
|
} else if (optionsY === null) { |
|
optionsY = pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2; |
|
|
|
} |
|
|
|
// Automatic alignment |
|
if (align === null) { |
|
if (axis.isCircular) { |
|
if (angle > 20 && angle < 160) { |
|
align = 'left'; // right hemisphere |
|
} else if (angle > 200 && angle < 340) { |
|
align = 'right'; // left hemisphere |
|
} else { |
|
align = 'center'; // top or bottom |
|
} |
|
} else { |
|
align = 'center'; |
|
} |
|
label.attr({ |
|
align: align |
|
}); |
|
} |
|
|
|
ret.x += labelOptions.x; |
|
ret.y += optionsY; |
|
|
|
} else { |
|
ret = proceed.call(this, x, y, label, horiz, labelOptions, tickmarkOffset, index, step); |
|
} |
|
return ret; |
|
}); |
|
|
|
/** |
|
* Wrap the getMarkPath function to return the path of the radial marker |
|
*/ |
|
wrap(tickProto, 'getMarkPath', function (proceed, x, y, tickLength, tickWidth, horiz, renderer) { |
|
var axis = this.axis, |
|
endPoint, |
|
ret; |
|
|
|
if (axis.isRadial) { |
|
endPoint = axis.getPosition(this.pos, axis.center[2] / 2 + tickLength); |
|
ret = [ |
|
'M', |
|
x, |
|
y, |
|
'L', |
|
endPoint.x, |
|
endPoint.y |
|
]; |
|
} else { |
|
ret = proceed.call(this, x, y, tickLength, tickWidth, horiz, renderer); |
|
} |
|
return ret; |
|
});/* |
|
* The AreaRangeSeries class |
|
* |
|
*/ |
|
|
|
/** |
|
* Extend the default options with map options |
|
*/ |
|
defaultPlotOptions.arearange = merge(defaultPlotOptions.area, { |
|
lineWidth: 1, |
|
marker: null, |
|
threshold: null, |
|
tooltip: { |
|
pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.low}</b> - <b>{point.high}</b><br/>' |
|
}, |
|
trackByArea: true, |
|
dataLabels: { |
|
verticalAlign: null, |
|
xLow: 0, |
|
xHigh: 0, |
|
yLow: 0, |
|
yHigh: 0 |
|
} |
|
}); |
|
|
|
/** |
|
* Add the series type |
|
*/ |
|
seriesTypes.arearange = Highcharts.extendClass(seriesTypes.area, { |
|
type: 'arearange', |
|
pointArrayMap: ['low', 'high'], |
|
toYData: function (point) { |
|
return [point.low, point.high]; |
|
}, |
|
pointValKey: 'low', |
|
|
|
/** |
|
* Extend getSegments to force null points if the higher value is null. #1703. |
|
*/ |
|
getSegments: function () { |
|
var series = this; |
|
|
|
each(series.points, function (point) { |
|
if (!series.options.connectNulls && (point.low === null || point.high === null)) { |
|
point.y = null; |
|
} else if (point.low === null && point.high !== null) { |
|
point.y = point.high; |
|
} |
|
}); |
|
Series.prototype.getSegments.call(this); |
|
}, |
|
|
|
/** |
|
* Translate data points from raw values x and y to plotX and plotY |
|
*/ |
|
translate: function () { |
|
var series = this, |
|
yAxis = series.yAxis; |
|
|
|
seriesTypes.area.prototype.translate.apply(series); |
|
|
|
// Set plotLow and plotHigh |
|
each(series.points, function (point) { |
|
|
|
var low = point.low, |
|
high = point.high, |
|
plotY = point.plotY; |
|
|
|
if (high === null && low === null) { |
|
point.y = null; |
|
} else if (low === null) { |
|
point.plotLow = point.plotY = null; |
|
point.plotHigh = yAxis.translate(high, 0, 1, 0, 1); |
|
} else if (high === null) { |
|
point.plotLow = plotY; |
|
point.plotHigh = null; |
|
} else { |
|
point.plotLow = plotY; |
|
point.plotHigh = yAxis.translate(high, 0, 1, 0, 1); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Extend the line series' getSegmentPath method by applying the segment |
|
* path to both lower and higher values of the range |
|
*/ |
|
getSegmentPath: function (segment) { |
|
|
|
var lowSegment, |
|
highSegment = [], |
|
i = segment.length, |
|
baseGetSegmentPath = Series.prototype.getSegmentPath, |
|
point, |
|
linePath, |
|
lowerPath, |
|
options = this.options, |
|
step = options.step, |
|
higherPath; |
|
|
|
// Remove nulls from low segment |
|
lowSegment = HighchartsAdapter.grep(segment, function (point) { |
|
return point.plotLow !== null; |
|
}); |
|
|
|
// Make a segment with plotX and plotY for the top values |
|
while (i--) { |
|
point = segment[i]; |
|
if (point.plotHigh !== null) { |
|
highSegment.push({ |
|
plotX: point.plotX, |
|
plotY: point.plotHigh |
|
}); |
|
} |
|
} |
|
|
|
// Get the paths |
|
lowerPath = baseGetSegmentPath.call(this, lowSegment); |
|
if (step) { |
|
if (step === true) { |
|
step = 'left'; |
|
} |
|
options.step = { left: 'right', center: 'center', right: 'left' }[step]; // swap for reading in getSegmentPath |
|
} |
|
higherPath = baseGetSegmentPath.call(this, highSegment); |
|
options.step = step; |
|
|
|
// Create a line on both top and bottom of the range |
|
linePath = [].concat(lowerPath, higherPath); |
|
|
|
// For the area path, we need to change the 'move' statement into 'lineTo' or 'curveTo' |
|
higherPath[0] = 'L'; // this probably doesn't work for spline |
|
this.areaPath = this.areaPath.concat(lowerPath, higherPath); |
|
|
|
return linePath; |
|
}, |
|
|
|
/** |
|
* Extend the basic drawDataLabels method by running it for both lower and higher |
|
* values. |
|
*/ |
|
drawDataLabels: function () { |
|
|
|
var data = this.data, |
|
length = data.length, |
|
i, |
|
originalDataLabels = [], |
|
seriesProto = Series.prototype, |
|
dataLabelOptions = this.options.dataLabels, |
|
point, |
|
inverted = this.chart.inverted; |
|
|
|
if (dataLabelOptions.enabled || this._hasPointLabels) { |
|
|
|
// Step 1: set preliminary values for plotY and dataLabel and draw the upper labels |
|
i = length; |
|
while (i--) { |
|
point = data[i]; |
|
|
|
// Set preliminary values |
|
point.y = point.high; |
|
point.plotY = point.plotHigh; |
|
|
|
// Store original data labels and set preliminary label objects to be picked up |
|
// in the uber method |
|
originalDataLabels[i] = point.dataLabel; |
|
point.dataLabel = point.dataLabelUpper; |
|
|
|
// Set the default offset |
|
point.below = false; |
|
if (inverted) { |
|
dataLabelOptions.align = 'left'; |
|
dataLabelOptions.x = dataLabelOptions.xHigh; |
|
} else { |
|
dataLabelOptions.y = dataLabelOptions.yHigh; |
|
} |
|
} |
|
seriesProto.drawDataLabels.apply(this, arguments); // #1209 |
|
|
|
// Step 2: reorganize and handle data labels for the lower values |
|
i = length; |
|
while (i--) { |
|
point = data[i]; |
|
|
|
// Move the generated labels from step 1, and reassign the original data labels |
|
point.dataLabelUpper = point.dataLabel; |
|
point.dataLabel = originalDataLabels[i]; |
|
|
|
// Reset values |
|
point.y = point.low; |
|
point.plotY = point.plotLow; |
|
|
|
// Set the default offset |
|
point.below = true; |
|
if (inverted) { |
|
dataLabelOptions.align = 'right'; |
|
dataLabelOptions.x = dataLabelOptions.xLow; |
|
} else { |
|
dataLabelOptions.y = dataLabelOptions.yLow; |
|
} |
|
} |
|
seriesProto.drawDataLabels.apply(this, arguments); |
|
} |
|
|
|
}, |
|
|
|
alignDataLabel: seriesTypes.column.prototype.alignDataLabel, |
|
|
|
getSymbol: seriesTypes.column.prototype.getSymbol, |
|
|
|
drawPoints: noop |
|
});/** |
|
* The AreaSplineRangeSeries class |
|
*/ |
|
|
|
defaultPlotOptions.areasplinerange = merge(defaultPlotOptions.arearange); |
|
|
|
/** |
|
* AreaSplineRangeSeries object |
|
*/ |
|
seriesTypes.areasplinerange = extendClass(seriesTypes.arearange, { |
|
type: 'areasplinerange', |
|
getPointSpline: seriesTypes.spline.prototype.getPointSpline |
|
});/** |
|
* The ColumnRangeSeries class |
|
*/ |
|
defaultPlotOptions.columnrange = merge(defaultPlotOptions.column, defaultPlotOptions.arearange, { |
|
lineWidth: 1, |
|
pointRange: null |
|
}); |
|
|
|
/** |
|
* ColumnRangeSeries object |
|
*/ |
|
seriesTypes.columnrange = extendClass(seriesTypes.arearange, { |
|
type: 'columnrange', |
|
/** |
|
* Translate data points from raw values x and y to plotX and plotY |
|
*/ |
|
translate: function () { |
|
var series = this, |
|
yAxis = series.yAxis, |
|
plotHigh; |
|
|
|
colProto.translate.apply(series); |
|
|
|
// Set plotLow and plotHigh |
|
each(series.points, function (point) { |
|
var shapeArgs = point.shapeArgs, |
|
minPointLength = series.options.minPointLength, |
|
heightDifference, |
|
height, |
|
y; |
|
|
|
point.plotHigh = plotHigh = yAxis.translate(point.high, 0, 1, 0, 1); |
|
point.plotLow = point.plotY; |
|
|
|
// adjust shape |
|
y = plotHigh; |
|
height = point.plotY - plotHigh; |
|
|
|
if (height < minPointLength) { |
|
heightDifference = (minPointLength - height); |
|
height += heightDifference; |
|
y -= heightDifference / 2; |
|
} |
|
shapeArgs.height = height; |
|
shapeArgs.y = y; |
|
}); |
|
}, |
|
trackerGroups: ['group', 'dataLabels'], |
|
drawGraph: noop, |
|
pointAttrToOptions: colProto.pointAttrToOptions, |
|
drawPoints: colProto.drawPoints, |
|
drawTracker: colProto.drawTracker, |
|
animate: colProto.animate, |
|
getColumnMetrics: colProto.getColumnMetrics |
|
}); |
|
/* |
|
* The GaugeSeries class |
|
*/ |
|
|
|
|
|
|
|
/** |
|
* Extend the default options |
|
*/ |
|
defaultPlotOptions.gauge = merge(defaultPlotOptions.line, { |
|
dataLabels: { |
|
enabled: true, |
|
y: 15, |
|
borderWidth: 1, |
|
borderColor: 'silver', |
|
borderRadius: 3, |
|
style: { |
|
fontWeight: 'bold' |
|
}, |
|
verticalAlign: 'top', |
|
zIndex: 2 |
|
}, |
|
dial: { |
|
// radius: '80%', |
|
// backgroundColor: 'black', |
|
// borderColor: 'silver', |
|
// borderWidth: 0, |
|
// baseWidth: 3, |
|
// topWidth: 1, |
|
// baseLength: '70%' // of radius |
|
// rearLength: '10%' |
|
}, |
|
pivot: { |
|
//radius: 5, |
|
//borderWidth: 0 |
|
//borderColor: 'silver', |
|
//backgroundColor: 'black' |
|
}, |
|
tooltip: { |
|
headerFormat: '' |
|
}, |
|
showInLegend: false |
|
}); |
|
|
|
/** |
|
* Extend the point object |
|
*/ |
|
var GaugePoint = Highcharts.extendClass(Highcharts.Point, { |
|
/** |
|
* Don't do any hover colors or anything |
|
*/ |
|
setState: function (state) { |
|
this.state = state; |
|
} |
|
}); |
|
|
|
|
|
/** |
|
* Add the series type |
|
*/ |
|
var GaugeSeries = { |
|
type: 'gauge', |
|
pointClass: GaugePoint, |
|
|
|
// chart.angular will be set to true when a gauge series is present, and this will |
|
// be used on the axes |
|
angular: true, |
|
drawGraph: noop, |
|
fixedBox: true, |
|
trackerGroups: ['group', 'dataLabels'], |
|
|
|
/** |
|
* Calculate paths etc |
|
*/ |
|
translate: function () { |
|
|
|
var series = this, |
|
yAxis = series.yAxis, |
|
options = series.options, |
|
center = yAxis.center; |
|
|
|
series.generatePoints(); |
|
|
|
each(series.points, function (point) { |
|
|
|
var dialOptions = merge(options.dial, point.dial), |
|
radius = (pInt(pick(dialOptions.radius, 80)) * center[2]) / 200, |
|
baseLength = (pInt(pick(dialOptions.baseLength, 70)) * radius) / 100, |
|
rearLength = (pInt(pick(dialOptions.rearLength, 10)) * radius) / 100, |
|
baseWidth = dialOptions.baseWidth || 3, |
|
topWidth = dialOptions.topWidth || 1, |
|
rotation = yAxis.startAngleRad + yAxis.translate(point.y, null, null, null, true); |
|
|
|
// Handle the wrap option |
|
if (options.wrap === false) { |
|
rotation = Math.max(yAxis.startAngleRad, Math.min(yAxis.endAngleRad, rotation)); |
|
} |
|
rotation = rotation * 180 / Math.PI; |
|
|
|
point.shapeType = 'path'; |
|
point.shapeArgs = { |
|
d: dialOptions.path || [ |
|
'M', |
|
-rearLength, -baseWidth / 2, |
|
'L', |
|
baseLength, -baseWidth / 2, |
|
radius, -topWidth / 2, |
|
radius, topWidth / 2, |
|
baseLength, baseWidth / 2, |
|
-rearLength, baseWidth / 2, |
|
'z' |
|
], |
|
translateX: center[0], |
|
translateY: center[1], |
|
rotation: rotation |
|
}; |
|
|
|
// Positions for data label |
|
point.plotX = center[0]; |
|
point.plotY = center[1]; |
|
}); |
|
}, |
|
|
|
/** |
|
* Draw the points where each point is one needle |
|
*/ |
|
drawPoints: function () { |
|
|
|
var series = this, |
|
center = series.yAxis.center, |
|
pivot = series.pivot, |
|
options = series.options, |
|
pivotOptions = options.pivot, |
|
renderer = series.chart.renderer; |
|
|
|
each(series.points, function (point) { |
|
|
|
var graphic = point.graphic, |
|
shapeArgs = point.shapeArgs, |
|
d = shapeArgs.d, |
|
dialOptions = merge(options.dial, point.dial); // #1233 |
|
|
|
if (graphic) { |
|
graphic.animate(shapeArgs); |
|
shapeArgs.d = d; // animate alters it |
|
} else { |
|
point.graphic = renderer[point.shapeType](shapeArgs) |
|
.attr({ |
|
stroke: dialOptions.borderColor || 'none', |
|
'stroke-width': dialOptions.borderWidth || 0, |
|
fill: dialOptions.backgroundColor || 'black', |
|
rotation: shapeArgs.rotation // required by VML when animation is false |
|
}) |
|
.add(series.group); |
|
} |
|
}); |
|
|
|
// Add or move the pivot |
|
if (pivot) { |
|
pivot.animate({ // #1235 |
|
translateX: center[0], |
|
translateY: center[1] |
|
}); |
|
} else { |
|
series.pivot = renderer.circle(0, 0, pick(pivotOptions.radius, 5)) |
|
.attr({ |
|
'stroke-width': pivotOptions.borderWidth || 0, |
|
stroke: pivotOptions.borderColor || 'silver', |
|
fill: pivotOptions.backgroundColor || 'black' |
|
}) |
|
.translate(center[0], center[1]) |
|
.add(series.group); |
|
} |
|
}, |
|
|
|
/** |
|
* Animate the arrow up from startAngle |
|
*/ |
|
animate: function (init) { |
|
var series = this; |
|
|
|
if (!init) { |
|
each(series.points, function (point) { |
|
var graphic = point.graphic; |
|
|
|
if (graphic) { |
|
// start value |
|
graphic.attr({ |
|
rotation: series.yAxis.startAngleRad * 180 / Math.PI |
|
}); |
|
|
|
// animate |
|
graphic.animate({ |
|
rotation: point.shapeArgs.rotation |
|
}, series.options.animation); |
|
} |
|
}); |
|
|
|
// delete this function to allow it only once |
|
series.animate = null; |
|
} |
|
}, |
|
|
|
render: function () { |
|
this.group = this.plotGroup( |
|
'group', |
|
'series', |
|
this.visible ? 'visible' : 'hidden', |
|
this.options.zIndex, |
|
this.chart.seriesGroup |
|
); |
|
seriesTypes.pie.prototype.render.call(this); |
|
this.group.clip(this.chart.clipRect); |
|
}, |
|
|
|
setData: seriesTypes.pie.prototype.setData, |
|
drawTracker: seriesTypes.column.prototype.drawTracker |
|
}; |
|
seriesTypes.gauge = Highcharts.extendClass(seriesTypes.line, GaugeSeries);/* **************************************************************************** |
|
* Start Box plot series code * |
|
*****************************************************************************/ |
|
|
|
// Set default options |
|
defaultPlotOptions.boxplot = merge(defaultPlotOptions.column, { |
|
fillColor: '#FFFFFF', |
|
lineWidth: 1, |
|
//medianColor: null, |
|
medianWidth: 2, |
|
states: { |
|
hover: { |
|
brightness: -0.3 |
|
} |
|
}, |
|
//stemColor: null, |
|
//stemDashStyle: 'solid' |
|
//stemWidth: null, |
|
threshold: null, |
|
tooltip: { |
|
pointFormat: '<span style="color:{series.color};font-weight:bold">{series.name}</span><br/>' + |
|
'Maximum: {point.high}<br/>' + |
|
'Upper quartile: {point.q3}<br/>' + |
|
'Median: {point.median}<br/>' + |
|
'Lower quartile: {point.q1}<br/>' + |
|
'Minimum: {point.low}<br/>' |
|
|
|
}, |
|
//whiskerColor: null, |
|
whiskerLength: '50%', |
|
whiskerWidth: 2 |
|
}); |
|
|
|
// Create the series object |
|
seriesTypes.boxplot = extendClass(seriesTypes.column, { |
|
type: 'boxplot', |
|
pointArrayMap: ['low', 'q1', 'median', 'q3', 'high'], // array point configs are mapped to this |
|
toYData: function (point) { // return a plain array for speedy calculation |
|
return [point.low, point.q1, point.median, point.q3, point.high]; |
|
}, |
|
pointValKey: 'high', // defines the top of the tracker |
|
|
|
/** |
|
* One-to-one mapping from options to SVG attributes |
|
*/ |
|
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options |
|
fill: 'fillColor', |
|
stroke: 'color', |
|
'stroke-width': 'lineWidth' |
|
}, |
|
|
|
/** |
|
* Disable data labels for box plot |
|
*/ |
|
drawDataLabels: noop, |
|
|
|
/** |
|
* Translate data points from raw values x and y to plotX and plotY |
|
*/ |
|
translate: function () { |
|
var series = this, |
|
yAxis = series.yAxis, |
|
pointArrayMap = series.pointArrayMap; |
|
|
|
seriesTypes.column.prototype.translate.apply(series); |
|
|
|
// do the translation on each point dimension |
|
each(series.points, function (point) { |
|
each(pointArrayMap, function (key) { |
|
if (point[key] !== null) { |
|
point[key + 'Plot'] = yAxis.translate(point[key], 0, 1, 0, 1); |
|
} |
|
}); |
|
}); |
|
}, |
|
|
|
/** |
|
* Draw the data points |
|
*/ |
|
drawPoints: function () { |
|
var series = this, //state = series.state, |
|
points = series.points, |
|
options = series.options, |
|
chart = series.chart, |
|
renderer = chart.renderer, |
|
pointAttr, |
|
q1Plot, |
|
q3Plot, |
|
highPlot, |
|
lowPlot, |
|
medianPlot, |
|
crispCorr, |
|
crispX, |
|
graphic, |
|
stemPath, |
|
stemAttr, |
|
boxPath, |
|
whiskersPath, |
|
whiskersAttr, |
|
medianPath, |
|
medianAttr, |
|
width, |
|
left, |
|
right, |
|
halfWidth, |
|
shapeArgs, |
|
color, |
|
doQuartiles = series.doQuartiles !== false, // error bar inherits this series type but doesn't do quartiles |
|
whiskerLength = parseInt(series.options.whiskerLength, 10) / 100; |
|
|
|
|
|
each(points, function (point) { |
|
|
|
graphic = point.graphic; |
|
shapeArgs = point.shapeArgs; // the box |
|
stemAttr = {}; |
|
whiskersAttr = {}; |
|
medianAttr = {}; |
|
color = point.color || series.color; |
|
|
|
if (point.plotY !== UNDEFINED) { |
|
|
|
pointAttr = point.pointAttr[point.selected ? 'selected' : '']; |
|
|
|
// crisp vector coordinates |
|
width = shapeArgs.width; |
|
left = mathFloor(shapeArgs.x); |
|
right = left + width; |
|
halfWidth = mathRound(width / 2); |
|
//crispX = mathRound(left + halfWidth) + crispCorr; |
|
q1Plot = mathFloor(doQuartiles ? point.q1Plot : point.lowPlot);// + crispCorr; |
|
q3Plot = mathFloor(doQuartiles ? point.q3Plot : point.lowPlot);// + crispCorr; |
|
highPlot = mathFloor(point.highPlot);// + crispCorr; |
|
lowPlot = mathFloor(point.lowPlot);// + crispCorr; |
|
|
|
// Stem attributes |
|
stemAttr.stroke = point.stemColor || options.stemColor || color; |
|
stemAttr['stroke-width'] = pick(point.stemWidth, options.stemWidth, options.lineWidth); |
|
stemAttr.dashstyle = point.stemDashStyle || options.stemDashStyle; |
|
|
|
// Whiskers attributes |
|
whiskersAttr.stroke = point.whiskerColor || options.whiskerColor || color; |
|
whiskersAttr['stroke-width'] = pick(point.whiskerWidth, options.whiskerWidth, options.lineWidth); |
|
|
|
// Median attributes |
|
medianAttr.stroke = point.medianColor || options.medianColor || color; |
|
medianAttr['stroke-width'] = pick(point.medianWidth, options.medianWidth, options.lineWidth); |
|
|
|
|
|
// The stem |
|
crispCorr = (stemAttr['stroke-width'] % 2) / 2; |
|
crispX = left + halfWidth + crispCorr; |
|
stemPath = [ |
|
// stem up |
|
'M', |
|
crispX, q3Plot, |
|
'L', |
|
crispX, highPlot, |
|
|
|
// stem down |
|
'M', |
|
crispX, q1Plot, |
|
'L', |
|
crispX, lowPlot, |
|
'z' |
|
]; |
|
|
|
// The box |
|
if (doQuartiles) { |
|
crispCorr = (pointAttr['stroke-width'] % 2) / 2; |
|
crispX = mathFloor(crispX) + crispCorr; |
|
q1Plot = mathFloor(q1Plot) + crispCorr; |
|
q3Plot = mathFloor(q3Plot) + crispCorr; |
|
left += crispCorr; |
|
right += crispCorr; |
|
boxPath = [ |
|
'M', |
|
left, q3Plot, |
|
'L', |
|
left, q1Plot, |
|
'L', |
|
right, q1Plot, |
|
'L', |
|
right, q3Plot, |
|
'L', |
|
left, q3Plot, |
|
'z' |
|
]; |
|
} |
|
|
|
// The whiskers |
|
if (whiskerLength) { |
|
crispCorr = (whiskersAttr['stroke-width'] % 2) / 2; |
|
highPlot = highPlot + crispCorr; |
|
lowPlot = lowPlot + crispCorr; |
|
whiskersPath = [ |
|
// High whisker |
|
'M', |
|
crispX - halfWidth * whiskerLength, |
|
highPlot, |
|
'L', |
|
crispX + halfWidth * whiskerLength, |
|
highPlot, |
|
|
|
// Low whisker |
|
'M', |
|
crispX - halfWidth * whiskerLength, |
|
lowPlot, |
|
'L', |
|
crispX + halfWidth * whiskerLength, |
|
lowPlot |
|
]; |
|
} |
|
|
|
// The median |
|
crispCorr = (medianAttr['stroke-width'] % 2) / 2; |
|
medianPlot = mathRound(point.medianPlot) + crispCorr; |
|
medianPath = [ |
|
'M', |
|
left, |
|
medianPlot, |
|
'L', |
|
right, |
|
medianPlot, |
|
'z' |
|
]; |
|
|
|
// Create or update the graphics |
|
if (graphic) { // update |
|
|
|
point.stem.animate({ d: stemPath }); |
|
if (whiskerLength) { |
|
point.whiskers.animate({ d: whiskersPath }); |
|
} |
|
if (doQuartiles) { |
|
point.box.animate({ d: boxPath }); |
|
} |
|
point.medianShape.animate({ d: medianPath }); |
|
|
|
} else { // create new |
|
point.graphic = graphic = renderer.g() |
|
.add(series.group); |
|
|
|
point.stem = renderer.path(stemPath) |
|
.attr(stemAttr) |
|
.add(graphic); |
|
|
|
if (whiskerLength) { |
|
point.whiskers = renderer.path(whiskersPath) |
|
.attr(whiskersAttr) |
|
.add(graphic); |
|
} |
|
if (doQuartiles) { |
|
point.box = renderer.path(boxPath) |
|
.attr(pointAttr) |
|
.add(graphic); |
|
} |
|
point.medianShape = renderer.path(medianPath) |
|
.attr(medianAttr) |
|
.add(graphic); |
|
} |
|
} |
|
}); |
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
/* **************************************************************************** |
|
* End Box plot series code * |
|
*****************************************************************************/ |
|
/* **************************************************************************** |
|
* Start error bar series code * |
|
*****************************************************************************/ |
|
|
|
// 1 - set default options |
|
defaultPlotOptions.errorbar = merge(defaultPlotOptions.boxplot, { |
|
color: '#000000', |
|
grouping: false, |
|
linkedTo: ':previous', |
|
tooltip: { |
|
pointFormat: defaultPlotOptions.arearange.tooltip.pointFormat |
|
}, |
|
whiskerWidth: null |
|
}); |
|
|
|
// 2 - Create the series object |
|
seriesTypes.errorbar = extendClass(seriesTypes.boxplot, { |
|
type: 'errorbar', |
|
pointArrayMap: ['low', 'high'], // array point configs are mapped to this |
|
toYData: function (point) { // return a plain array for speedy calculation |
|
return [point.low, point.high]; |
|
}, |
|
pointValKey: 'high', // defines the top of the tracker |
|
doQuartiles: false, |
|
|
|
/** |
|
* Get the width and X offset, either on top of the linked series column |
|
* or standalone |
|
*/ |
|
getColumnMetrics: function () { |
|
return (this.linkedParent && this.linkedParent.columnMetrics) || |
|
seriesTypes.column.prototype.getColumnMetrics.call(this); |
|
} |
|
}); |
|
|
|
/* **************************************************************************** |
|
* End error bar series code * |
|
*****************************************************************************/ |
|
/* **************************************************************************** |
|
* Start Waterfall series code * |
|
*****************************************************************************/ |
|
|
|
// 1 - set default options |
|
defaultPlotOptions.waterfall = merge(defaultPlotOptions.column, { |
|
lineWidth: 1, |
|
lineColor: '#333', |
|
dashStyle: 'dot', |
|
borderColor: '#333' |
|
}); |
|
|
|
|
|
// 2 - Create the series object |
|
seriesTypes.waterfall = extendClass(seriesTypes.column, { |
|
type: 'waterfall', |
|
|
|
upColorProp: 'fill', |
|
|
|
pointArrayMap: ['low', 'y'], |
|
|
|
pointValKey: 'y', |
|
|
|
/** |
|
* Init waterfall series, force stacking |
|
*/ |
|
init: function (chart, options) { |
|
// force stacking |
|
options.stacking = true; |
|
|
|
seriesTypes.column.prototype.init.call(this, chart, options); |
|
}, |
|
|
|
|
|
/** |
|
* Translate data points from raw values |
|
*/ |
|
translate: function () { |
|
var series = this, |
|
options = series.options, |
|
axis = series.yAxis, |
|
len, |
|
i, |
|
points, |
|
point, |
|
shapeArgs, |
|
stack, |
|
y, |
|
previousY, |
|
stackPoint, |
|
threshold = options.threshold, |
|
crispCorr = (options.borderWidth % 2) / 2; |
|
|
|
// run column series translate |
|
seriesTypes.column.prototype.translate.apply(this); |
|
|
|
previousY = threshold; |
|
points = series.points; |
|
|
|
for (i = 0, len = points.length; i < len; i++) { |
|
// cache current point object |
|
point = points[i]; |
|
shapeArgs = point.shapeArgs; |
|
|
|
// get current stack |
|
stack = series.getStack(i); |
|
stackPoint = stack.points[series.index]; |
|
|
|
// override point value for sums |
|
if (isNaN(point.y)) { |
|
point.y = series.yData[i]; |
|
} |
|
|
|
// up points |
|
y = mathMax(previousY, previousY + point.y) + stackPoint[0]; |
|
shapeArgs.y = axis.translate(y, 0, 1); |
|
|
|
|
|
// sum points |
|
if (point.isSum || point.isIntermediateSum) { |
|
shapeArgs.y = axis.translate(stackPoint[1], 0, 1); |
|
shapeArgs.height = axis.translate(stackPoint[0], 0, 1) - shapeArgs.y; |
|
|
|
// if it's not the sum point, update previous stack end position |
|
} else { |
|
previousY += stack.total; |
|
} |
|
|
|
// negative points |
|
if (shapeArgs.height < 0) { |
|
shapeArgs.y += shapeArgs.height; |
|
shapeArgs.height *= -1; |
|
} |
|
|
|
point.plotY = shapeArgs.y = mathRound(shapeArgs.y) - crispCorr; |
|
shapeArgs.height = mathRound(shapeArgs.height); |
|
point.yBottom = shapeArgs.y + shapeArgs.height; |
|
} |
|
}, |
|
|
|
/** |
|
* Call default processData then override yData to reflect waterfall's extremes on yAxis |
|
*/ |
|
processData: function (force) { |
|
var series = this, |
|
options = series.options, |
|
yData = series.yData, |
|
points = series.points, |
|
point, |
|
dataLength = yData.length, |
|
threshold = options.threshold || 0, |
|
subSum, |
|
sum, |
|
dataMin, |
|
dataMax, |
|
y, |
|
i; |
|
|
|
sum = subSum = dataMin = dataMax = threshold; |
|
|
|
for (i = 0; i < dataLength; i++) { |
|
y = yData[i]; |
|
point = points && points[i] ? points[i] : {}; |
|
|
|
if (y === "sum" || point.isSum) { |
|
yData[i] = sum; |
|
} else if (y === "intermediateSum" || point.isIntermediateSum) { |
|
yData[i] = subSum; |
|
subSum = threshold; |
|
} else { |
|
sum += y; |
|
subSum += y; |
|
} |
|
dataMin = Math.min(sum, dataMin); |
|
dataMax = Math.max(sum, dataMax); |
|
} |
|
|
|
Series.prototype.processData.call(this, force); |
|
|
|
// Record extremes |
|
series.dataMin = dataMin; |
|
series.dataMax = dataMax; |
|
}, |
|
|
|
/** |
|
* Return y value or string if point is sum |
|
*/ |
|
toYData: function (pt) { |
|
if (pt.isSum) { |
|
return "sum"; |
|
} else if (pt.isIntermediateSum) { |
|
return "intermediateSum"; |
|
} |
|
|
|
return pt.y; |
|
}, |
|
|
|
/** |
|
* Postprocess mapping between options and SVG attributes |
|
*/ |
|
getAttribs: function () { |
|
seriesTypes.column.prototype.getAttribs.apply(this, arguments); |
|
|
|
var series = this, |
|
options = series.options, |
|
stateOptions = options.states, |
|
upColor = options.upColor || series.color, |
|
hoverColor = Highcharts.Color(upColor).brighten(0.1).get(), |
|
seriesDownPointAttr = merge(series.pointAttr), |
|
upColorProp = series.upColorProp; |
|
|
|
seriesDownPointAttr[''][upColorProp] = upColor; |
|
seriesDownPointAttr.hover[upColorProp] = stateOptions.hover.upColor || hoverColor; |
|
seriesDownPointAttr.select[upColorProp] = stateOptions.select.upColor || upColor; |
|
|
|
each(series.points, function (point) { |
|
if (point.y > 0 && !point.color) { |
|
point.pointAttr = seriesDownPointAttr; |
|
point.color = upColor; |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Draw columns' connector lines |
|
*/ |
|
getGraphPath: function () { |
|
|
|
var data = this.data, |
|
length = data.length, |
|
lineWidth = this.options.lineWidth + this.options.borderWidth, |
|
normalizer = mathRound(lineWidth) % 2 / 2, |
|
path = [], |
|
M = 'M', |
|
L = 'L', |
|
prevArgs, |
|
pointArgs, |
|
i, |
|
d; |
|
|
|
for (i = 1; i < length; i++) { |
|
pointArgs = data[i].shapeArgs; |
|
prevArgs = data[i - 1].shapeArgs; |
|
|
|
d = [ |
|
M, |
|
prevArgs.x + prevArgs.width, prevArgs.y + normalizer, |
|
L, |
|
pointArgs.x, prevArgs.y + normalizer |
|
]; |
|
|
|
if (data[i - 1].y < 0) { |
|
d[2] += prevArgs.height; |
|
d[5] += prevArgs.height; |
|
} |
|
|
|
path = path.concat(d); |
|
} |
|
|
|
return path; |
|
}, |
|
|
|
/** |
|
* Extremes are recorded in processData |
|
*/ |
|
getExtremes: noop, |
|
|
|
/** |
|
* Return stack for given index |
|
*/ |
|
getStack: function (i) { |
|
var axis = this.yAxis, |
|
stacks = axis.stacks, |
|
key = this.stackKey; |
|
|
|
if (this.processedYData[i] < this.options.threshold) { |
|
key = '-' + key; |
|
} |
|
|
|
return stacks[key][i]; |
|
}, |
|
|
|
drawGraph: Series.prototype.drawGraph |
|
}); |
|
|
|
/* **************************************************************************** |
|
* End Waterfall series code * |
|
*****************************************************************************/ |
|
/* **************************************************************************** |
|
* Start Bubble series code * |
|
*****************************************************************************/ |
|
|
|
// 1 - set default options |
|
defaultPlotOptions.bubble = merge(defaultPlotOptions.scatter, { |
|
dataLabels: { |
|
inside: true, |
|
style: { |
|
color: 'white', |
|
textShadow: '0px 0px 3px black' |
|
}, |
|
verticalAlign: 'middle' |
|
}, |
|
// displayNegative: true, |
|
marker: { |
|
// fillOpacity: 0.5, |
|
lineColor: null, // inherit from series.color |
|
lineWidth: 1 |
|
}, |
|
minSize: 8, |
|
maxSize: '20%', |
|
// negativeColor: null, |
|
tooltip: { |
|
pointFormat: '({point.x}, {point.y}), Size: {point.z}' |
|
}, |
|
turboThreshold: 0, |
|
zThreshold: 0 |
|
}); |
|
|
|
// 2 - Create the series object |
|
seriesTypes.bubble = extendClass(seriesTypes.scatter, { |
|
type: 'bubble', |
|
pointArrayMap: ['y', 'z'], |
|
trackerGroups: ['group', 'dataLabelsGroup'], |
|
|
|
/** |
|
* Mapping between SVG attributes and the corresponding options |
|
*/ |
|
pointAttrToOptions: { |
|
stroke: 'lineColor', |
|
'stroke-width': 'lineWidth', |
|
fill: 'fillColor' |
|
}, |
|
|
|
/** |
|
* Apply the fillOpacity to all fill positions |
|
*/ |
|
applyOpacity: function (fill) { |
|
var markerOptions = this.options.marker, |
|
fillOpacity = pick(markerOptions.fillOpacity, 0.5); |
|
|
|
// When called from Legend.colorizeItem, the fill isn't predefined |
|
fill = fill || markerOptions.fillColor || this.color; |
|
|
|
if (fillOpacity !== 1) { |
|
fill = Highcharts.Color(fill).setOpacity(fillOpacity).get('rgba'); |
|
} |
|
return fill; |
|
}, |
|
|
|
/** |
|
* Extend the convertAttribs method by applying opacity to the fill |
|
*/ |
|
convertAttribs: function () { |
|
var obj = Series.prototype.convertAttribs.apply(this, arguments); |
|
|
|
obj.fill = this.applyOpacity(obj.fill); |
|
|
|
return obj; |
|
}, |
|
|
|
/** |
|
* Get the radius for each point based on the minSize, maxSize and each point's Z value. This |
|
* must be done prior to Series.translate because the axis needs to add padding in |
|
* accordance with the point sizes. |
|
*/ |
|
getRadii: function (zMin, zMax, minSize, maxSize) { |
|
var len, |
|
i, |
|
pos, |
|
zData = this.zData, |
|
radii = [], |
|
zRange; |
|
|
|
// Set the shape type and arguments to be picked up in drawPoints |
|
for (i = 0, len = zData.length; i < len; i++) { |
|
zRange = zMax - zMin; |
|
pos = zRange > 0 ? // relative size, a number between 0 and 1 |
|
(zData[i] - zMin) / (zMax - zMin) : |
|
0.5; |
|
radii.push(math.ceil(minSize + pos * (maxSize - minSize)) / 2); |
|
} |
|
this.radii = radii; |
|
}, |
|
|
|
/** |
|
* Perform animation on the bubbles |
|
*/ |
|
animate: function (init) { |
|
var animation = this.options.animation; |
|
|
|
if (!init) { // run the animation |
|
each(this.points, function (point) { |
|
var graphic = point.graphic, |
|
shapeArgs = point.shapeArgs; |
|
|
|
if (graphic && shapeArgs) { |
|
// start values |
|
graphic.attr('r', 1); |
|
|
|
// animate |
|
graphic.animate({ |
|
r: shapeArgs.r |
|
}, animation); |
|
} |
|
}); |
|
|
|
// delete this function to allow it only once |
|
this.animate = null; |
|
} |
|
}, |
|
|
|
/** |
|
* Extend the base translate method to handle bubble size |
|
*/ |
|
translate: function () { |
|
|
|
var i, |
|
data = this.data, |
|
point, |
|
radius, |
|
radii = this.radii; |
|
|
|
// Run the parent method |
|
seriesTypes.scatter.prototype.translate.call(this); |
|
|
|
// Set the shape type and arguments to be picked up in drawPoints |
|
i = data.length; |
|
|
|
while (i--) { |
|
point = data[i]; |
|
radius = radii ? radii[i] : 0; // #1737 |
|
|
|
// Flag for negativeColor to be applied in Series.js |
|
point.negative = point.z < (this.options.zThreshold || 0); |
|
|
|
if (radius >= this.minPxSize / 2) { |
|
// Shape arguments |
|
point.shapeType = 'circle'; |
|
point.shapeArgs = { |
|
x: point.plotX, |
|
y: point.plotY, |
|
r: radius |
|
}; |
|
|
|
// Alignment box for the data label |
|
point.dlBox = { |
|
x: point.plotX - radius, |
|
y: point.plotY - radius, |
|
width: 2 * radius, |
|
height: 2 * radius |
|
}; |
|
} else { // below zThreshold |
|
point.shapeArgs = point.plotY = point.dlBox = UNDEFINED; // #1691 |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Get the series' symbol in the legend |
|
* |
|
* @param {Object} legend The legend object |
|
* @param {Object} item The series (this) or point |
|
*/ |
|
drawLegendSymbol: function (legend, item) { |
|
var radius = pInt(legend.itemStyle.fontSize) / 2; |
|
|
|
item.legendSymbol = this.chart.renderer.circle( |
|
radius, |
|
legend.baseline - radius, |
|
radius |
|
).attr({ |
|
zIndex: 3 |
|
}).add(item.legendGroup); |
|
item.legendSymbol.isMarker = true; |
|
|
|
}, |
|
|
|
drawPoints: seriesTypes.column.prototype.drawPoints, |
|
alignDataLabel: seriesTypes.column.prototype.alignDataLabel |
|
}); |
|
|
|
/** |
|
* Add logic to pad each axis with the amount of pixels |
|
* necessary to avoid the bubbles to overflow. |
|
*/ |
|
Axis.prototype.beforePadding = function () { |
|
var axis = this, |
|
axisLength = this.len, |
|
chart = this.chart, |
|
pxMin = 0, |
|
pxMax = axisLength, |
|
isXAxis = this.isXAxis, |
|
dataKey = isXAxis ? 'xData' : 'yData', |
|
min = this.min, |
|
extremes = {}, |
|
smallestSize = math.min(chart.plotWidth, chart.plotHeight), |
|
zMin = Number.MAX_VALUE, |
|
zMax = -Number.MAX_VALUE, |
|
range = this.max - min, |
|
transA = axisLength / range, |
|
activeSeries = []; |
|
|
|
// Handle padding on the second pass, or on redraw |
|
if (this.tickPositions) { |
|
each(this.series, function (series) { |
|
|
|
var seriesOptions = series.options, |
|
zData; |
|
|
|
if (series.type === 'bubble' && series.visible) { |
|
|
|
// Correction for #1673 |
|
axis.allowZoomOutside = true; |
|
|
|
// Cache it |
|
activeSeries.push(series); |
|
|
|
if (isXAxis) { // because X axis is evaluated first |
|
|
|
// For each series, translate the size extremes to pixel values |
|
each(['minSize', 'maxSize'], function (prop) { |
|
var length = seriesOptions[prop], |
|
isPercent = /%$/.test(length); |
|
|
|
length = pInt(length); |
|
extremes[prop] = isPercent ? |
|
smallestSize * length / 100 : |
|
length; |
|
|
|
}); |
|
series.minPxSize = extremes.minSize; |
|
|
|
// Find the min and max Z |
|
zData = series.zData; |
|
if (zData.length) { // #1735 |
|
zMin = math.min( |
|
zMin, |
|
math.max( |
|
arrayMin(zData), |
|
seriesOptions.displayNegative === false ? seriesOptions.zThreshold : -Number.MAX_VALUE |
|
) |
|
); |
|
zMax = math.max(zMax, arrayMax(zData)); |
|
} |
|
} |
|
} |
|
}); |
|
|
|
each(activeSeries, function (series) { |
|
|
|
var data = series[dataKey], |
|
i = data.length, |
|
radius; |
|
|
|
if (isXAxis) { |
|
series.getRadii(zMin, zMax, extremes.minSize, extremes.maxSize); |
|
} |
|
|
|
if (range > 0) { |
|
while (i--) { |
|
radius = series.radii[i]; |
|
pxMin = Math.min(((data[i] - min) * transA) - radius, pxMin); |
|
pxMax = Math.max(((data[i] - min) * transA) + radius, pxMax); |
|
} |
|
} |
|
}); |
|
|
|
if (activeSeries.length && range > 0 && pick(this.options.min, this.userMin) === UNDEFINED && pick(this.options.max, this.userMax) === UNDEFINED) { |
|
pxMax -= axisLength; |
|
transA *= (axisLength + pxMin - pxMax) / axisLength; |
|
this.min += pxMin / transA; |
|
this.max += pxMax / transA; |
|
} |
|
} |
|
}; |
|
|
|
/* **************************************************************************** |
|
* End Bubble series code * |
|
*****************************************************************************/ |
|
/** |
|
* Extensions for polar charts. Additionally, much of the geometry required for polar charts is |
|
* gathered in RadialAxes.js. |
|
* |
|
*/ |
|
|
|
var seriesProto = Series.prototype, |
|
pointerProto = Highcharts.Pointer.prototype; |
|
|
|
|
|
|
|
/** |
|
* Translate a point's plotX and plotY from the internal angle and radius measures to |
|
* true plotX, plotY coordinates |
|
*/ |
|
seriesProto.toXY = function (point) { |
|
var xy, |
|
chart = this.chart, |
|
plotX = point.plotX, |
|
plotY = point.plotY; |
|
|
|
// Save rectangular plotX, plotY for later computation |
|
point.rectPlotX = plotX; |
|
point.rectPlotY = plotY; |
|
|
|
// Record the angle in degrees for use in tooltip |
|
point.clientX = ((plotX / Math.PI * 180) + this.xAxis.pane.options.startAngle) % 360; |
|
|
|
// Find the polar plotX and plotY |
|
xy = this.xAxis.postTranslate(point.plotX, this.yAxis.len - plotY); |
|
point.plotX = point.polarPlotX = xy.x - chart.plotLeft; |
|
point.plotY = point.polarPlotY = xy.y - chart.plotTop; |
|
}; |
|
|
|
/** |
|
* Order the tooltip points to get the mouse capture ranges correct. #1915. |
|
*/ |
|
seriesProto.orderTooltipPoints = function (points) { |
|
if (this.chart.polar) { |
|
points.sort(function (a, b) { |
|
return a.clientX - b.clientX; |
|
}); |
|
|
|
// Wrap mouse tracking around to capture movement on the segment to the left |
|
// of the north point (#1469, #2093). |
|
if (points[0]) { |
|
points[0].wrappedClientX = points[0].clientX + 360; |
|
points.push(points[0]); |
|
} |
|
} |
|
}; |
|
|
|
|
|
/** |
|
* Add some special init logic to areas and areasplines |
|
*/ |
|
function initArea(proceed, chart, options) { |
|
proceed.call(this, chart, options); |
|
if (this.chart.polar) { |
|
|
|
/** |
|
* Overridden method to close a segment path. While in a cartesian plane the area |
|
* goes down to the threshold, in the polar chart it goes to the center. |
|
*/ |
|
this.closeSegment = function (path) { |
|
var center = this.xAxis.center; |
|
path.push( |
|
'L', |
|
center[0], |
|
center[1] |
|
); |
|
}; |
|
|
|
// Instead of complicated logic to draw an area around the inner area in a stack, |
|
// just draw it behind |
|
this.closedStacks = true; |
|
} |
|
} |
|
wrap(seriesTypes.area.prototype, 'init', initArea); |
|
wrap(seriesTypes.areaspline.prototype, 'init', initArea); |
|
|
|
|
|
/** |
|
* Overridden method for calculating a spline from one point to the next |
|
*/ |
|
wrap(seriesTypes.spline.prototype, 'getPointSpline', function (proceed, segment, point, i) { |
|
|
|
var ret, |
|
smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc; |
|
denom = smoothing + 1, |
|
plotX, |
|
plotY, |
|
lastPoint, |
|
nextPoint, |
|
lastX, |
|
lastY, |
|
nextX, |
|
nextY, |
|
leftContX, |
|
leftContY, |
|
rightContX, |
|
rightContY, |
|
distanceLeftControlPoint, |
|
distanceRightControlPoint, |
|
leftContAngle, |
|
rightContAngle, |
|
jointAngle; |
|
|
|
|
|
if (this.chart.polar) { |
|
|
|
plotX = point.plotX; |
|
plotY = point.plotY; |
|
lastPoint = segment[i - 1]; |
|
nextPoint = segment[i + 1]; |
|
|
|
// Connect ends |
|
if (this.connectEnds) { |
|
if (!lastPoint) { |
|
lastPoint = segment[segment.length - 2]; // not the last but the second last, because the segment is already connected |
|
} |
|
if (!nextPoint) { |
|
nextPoint = segment[1]; |
|
} |
|
} |
|
|
|
// find control points |
|
if (lastPoint && nextPoint) { |
|
|
|
lastX = lastPoint.plotX; |
|
lastY = lastPoint.plotY; |
|
nextX = nextPoint.plotX; |
|
nextY = nextPoint.plotY; |
|
leftContX = (smoothing * plotX + lastX) / denom; |
|
leftContY = (smoothing * plotY + lastY) / denom; |
|
rightContX = (smoothing * plotX + nextX) / denom; |
|
rightContY = (smoothing * plotY + nextY) / denom; |
|
distanceLeftControlPoint = Math.sqrt(Math.pow(leftContX - plotX, 2) + Math.pow(leftContY - plotY, 2)); |
|
distanceRightControlPoint = Math.sqrt(Math.pow(rightContX - plotX, 2) + Math.pow(rightContY - plotY, 2)); |
|
leftContAngle = Math.atan2(leftContY - plotY, leftContX - plotX); |
|
rightContAngle = Math.atan2(rightContY - plotY, rightContX - plotX); |
|
jointAngle = (Math.PI / 2) + ((leftContAngle + rightContAngle) / 2); |
|
|
|
|
|
// Ensure the right direction, jointAngle should be in the same quadrant as leftContAngle |
|
if (Math.abs(leftContAngle - jointAngle) > Math.PI / 2) { |
|
jointAngle -= Math.PI; |
|
} |
|
|
|
// Find the corrected control points for a spline straight through the point |
|
leftContX = plotX + Math.cos(jointAngle) * distanceLeftControlPoint; |
|
leftContY = plotY + Math.sin(jointAngle) * distanceLeftControlPoint; |
|
rightContX = plotX + Math.cos(Math.PI + jointAngle) * distanceRightControlPoint; |
|
rightContY = plotY + Math.sin(Math.PI + jointAngle) * distanceRightControlPoint; |
|
|
|
// Record for drawing in next point |
|
point.rightContX = rightContX; |
|
point.rightContY = rightContY; |
|
|
|
} |
|
|
|
|
|
// moveTo or lineTo |
|
if (!i) { |
|
ret = ['M', plotX, plotY]; |
|
} else { // curve from last point to this |
|
ret = [ |
|
'C', |
|
lastPoint.rightContX || lastPoint.plotX, |
|
lastPoint.rightContY || lastPoint.plotY, |
|
leftContX || plotX, |
|
leftContY || plotY, |
|
plotX, |
|
plotY |
|
]; |
|
lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later |
|
} |
|
|
|
|
|
} else { |
|
ret = proceed.call(this, segment, point, i); |
|
} |
|
return ret; |
|
}); |
|
|
|
/** |
|
* Extend translate. The plotX and plotY values are computed as if the polar chart were a |
|
* cartesian plane, where plotX denotes the angle in radians and (yAxis.len - plotY) is the pixel distance from |
|
* center. |
|
*/ |
|
wrap(seriesProto, 'translate', function (proceed) { |
|
|
|
// Run uber method |
|
proceed.call(this); |
|
|
|
// Postprocess plot coordinates |
|
if (this.chart.polar && !this.preventPostTranslate) { |
|
var points = this.points, |
|
i = points.length; |
|
while (i--) { |
|
// Translate plotX, plotY from angle and radius to true plot coordinates |
|
this.toXY(points[i]); |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* Extend getSegmentPath to allow connecting ends across 0 to provide a closed circle in |
|
* line-like series. |
|
*/ |
|
wrap(seriesProto, 'getSegmentPath', function (proceed, segment) { |
|
|
|
var points = this.points; |
|
|
|
// Connect the path |
|
if (this.chart.polar && this.options.connectEnds !== false && |
|
segment[segment.length - 1] === points[points.length - 1] && points[0].y !== null) { |
|
this.connectEnds = true; // re-used in splines |
|
segment = [].concat(segment, [points[0]]); |
|
} |
|
|
|
// Run uber method |
|
return proceed.call(this, segment); |
|
|
|
}); |
|
|
|
|
|
function polarAnimate(proceed, init) { |
|
var chart = this.chart, |
|
animation = this.options.animation, |
|
group = this.group, |
|
markerGroup = this.markerGroup, |
|
center = this.xAxis.center, |
|
plotLeft = chart.plotLeft, |
|
plotTop = chart.plotTop, |
|
attribs; |
|
|
|
// Specific animation for polar charts |
|
if (chart.polar) { |
|
|
|
// Enable animation on polar charts only in SVG. In VML, the scaling is different, plus animation |
|
// would be so slow it would't matter. |
|
if (chart.renderer.isSVG) { |
|
|
|
if (animation === true) { |
|
animation = {}; |
|
} |
|
|
|
// Initialize the animation |
|
if (init) { |
|
|
|
// Scale down the group and place it in the center |
|
attribs = { |
|
translateX: center[0] + plotLeft, |
|
translateY: center[1] + plotTop, |
|
scaleX: 0.001, // #1499 |
|
scaleY: 0.001 |
|
}; |
|
|
|
group.attr(attribs); |
|
if (markerGroup) { |
|
markerGroup.attrSetters = group.attrSetters; |
|
markerGroup.attr(attribs); |
|
} |
|
|
|
// Run the animation |
|
} else { |
|
attribs = { |
|
translateX: plotLeft, |
|
translateY: plotTop, |
|
scaleX: 1, |
|
scaleY: 1 |
|
}; |
|
group.animate(attribs, animation); |
|
if (markerGroup) { |
|
markerGroup.animate(attribs, animation); |
|
} |
|
|
|
// Delete this function to allow it only once |
|
this.animate = null; |
|
} |
|
} |
|
|
|
// For non-polar charts, revert to the basic animation |
|
} else { |
|
proceed.call(this, init); |
|
} |
|
} |
|
|
|
// Define the animate method for both regular series and column series and their derivatives |
|
wrap(seriesProto, 'animate', polarAnimate); |
|
wrap(colProto, 'animate', polarAnimate); |
|
|
|
|
|
/** |
|
* Throw in a couple of properties to let setTooltipPoints know we're indexing the points |
|
* in degrees (0-360), not plot pixel width. |
|
*/ |
|
wrap(seriesProto, 'setTooltipPoints', function (proceed, renew) { |
|
|
|
if (this.chart.polar) { |
|
extend(this.xAxis, { |
|
tooltipLen: 360 // degrees are the resolution unit of the tooltipPoints array |
|
}); |
|
} |
|
|
|
// Run uber method |
|
return proceed.call(this, renew); |
|
}); |
|
|
|
|
|
/** |
|
* Extend the column prototype's translate method |
|
*/ |
|
wrap(colProto, 'translate', function (proceed) { |
|
|
|
var xAxis = this.xAxis, |
|
len = this.yAxis.len, |
|
center = xAxis.center, |
|
startAngleRad = xAxis.startAngleRad, |
|
renderer = this.chart.renderer, |
|
start, |
|
points, |
|
point, |
|
i; |
|
|
|
this.preventPostTranslate = true; |
|
|
|
// Run uber method |
|
proceed.call(this); |
|
|
|
// Postprocess plot coordinates |
|
if (xAxis.isRadial) { |
|
points = this.points; |
|
i = points.length; |
|
while (i--) { |
|
point = points[i]; |
|
start = point.barX + startAngleRad; |
|
point.shapeType = 'path'; |
|
point.shapeArgs = { |
|
d: renderer.symbols.arc( |
|
center[0], |
|
center[1], |
|
len - point.plotY, |
|
null, |
|
{ |
|
start: start, |
|
end: start + point.pointWidth, |
|
innerR: len - pick(point.yBottom, len) |
|
} |
|
) |
|
}; |
|
this.toXY(point); // provide correct plotX, plotY for tooltip |
|
} |
|
} |
|
}); |
|
|
|
|
|
/** |
|
* Align column data labels outside the columns. #1199. |
|
*/ |
|
wrap(colProto, 'alignDataLabel', function (proceed, point, dataLabel, options, alignTo, isNew) { |
|
|
|
if (this.chart.polar) { |
|
var angle = point.rectPlotX / Math.PI * 180, |
|
align, |
|
verticalAlign; |
|
|
|
// Align nicely outside the perimeter of the columns |
|
if (options.align === null) { |
|
if (angle > 20 && angle < 160) { |
|
align = 'left'; // right hemisphere |
|
} else if (angle > 200 && angle < 340) { |
|
align = 'right'; // left hemisphere |
|
} else { |
|
align = 'center'; // top or bottom |
|
} |
|
options.align = align; |
|
} |
|
if (options.verticalAlign === null) { |
|
if (angle < 45 || angle > 315) { |
|
verticalAlign = 'bottom'; // top part |
|
} else if (angle > 135 && angle < 225) { |
|
verticalAlign = 'top'; // bottom part |
|
} else { |
|
verticalAlign = 'middle'; // left or right |
|
} |
|
options.verticalAlign = verticalAlign; |
|
} |
|
|
|
seriesProto.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); |
|
} else { |
|
proceed.call(this, point, dataLabel, options, alignTo, isNew); |
|
} |
|
|
|
}); |
|
|
|
/** |
|
* Extend the mouse tracker to return the tooltip position index in terms of |
|
* degrees rather than pixels |
|
*/ |
|
wrap(pointerProto, 'getIndex', function (proceed, e) { |
|
var ret, |
|
chart = this.chart, |
|
center, |
|
x, |
|
y; |
|
|
|
if (chart.polar) { |
|
center = chart.xAxis[0].center; |
|
x = e.chartX - center[0] - chart.plotLeft; |
|
y = e.chartY - center[1] - chart.plotTop; |
|
|
|
ret = 180 - Math.round(Math.atan2(x, y) / Math.PI * 180); |
|
|
|
} else { |
|
|
|
// Run uber method |
|
ret = proceed.call(this, e); |
|
} |
|
return ret; |
|
}); |
|
|
|
/** |
|
* Extend getCoordinates to prepare for polar axis values |
|
*/ |
|
wrap(pointerProto, 'getCoordinates', function (proceed, e) { |
|
var chart = this.chart, |
|
ret = { |
|
xAxis: [], |
|
yAxis: [] |
|
}; |
|
|
|
if (chart.polar) { |
|
|
|
each(chart.axes, function (axis) { |
|
var isXAxis = axis.isXAxis, |
|
center = axis.center, |
|
x = e.chartX - center[0] - chart.plotLeft, |
|
y = e.chartY - center[1] - chart.plotTop; |
|
|
|
ret[isXAxis ? 'xAxis' : 'yAxis'].push({ |
|
axis: axis, |
|
value: axis.translate( |
|
isXAxis ? |
|
Math.PI - Math.atan2(x, y) : // angle |
|
Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)), // distance from center |
|
true |
|
) |
|
}); |
|
}); |
|
|
|
} else { |
|
ret = proceed.call(this, e); |
|
} |
|
|
|
return ret; |
|
}); |
|
}(Highcharts));
|
|
|