=g-l;if(0>q||q>a||0>e||e>p)l=!1;e=b.speedx&&l?b.speedx:!1;if(b.speedy&&l&&b.speedy||e){var d=Math.max(16,b.steptime);50
',
pointFormat: '\u25CF {series.name}: {point.y}
',
shadow: true,
//shape: 'callout',
//shared: false,
snap: isTouchDevice ? 25 : 10,
style: {
color: '#333333',
cursor: 'default',
fontSize: '12px',
padding: '8px',
whiteSpace: 'nowrap'
}
//xDateFormat: '%A, %b %e, %Y',
//valueDecimals: null,
//valuePrefix: '',
//valueSuffix: ''
},
credits: {
enabled: true,
text: 'Highcharts.com',
href: 'http://www.highcharts.com',
position: {
align: 'right',
x: -10,
verticalAlign: 'bottom',
y: -5
},
style: {
cursor: 'pointer',
color: '#909090',
fontSize: '9px'
}
}
};
// Series defaults
var defaultPlotOptions = defaultOptions.plotOptions,
defaultSeriesOptions = defaultPlotOptions.line;
// set the default time methods
setTimeMethods();
/**
* Set the time methods globally based on the useUTC option. Time method can be either
* local time or UTC (default).
*/
function setTimeMethods() {
var globalOptions = defaultOptions.global,
useUTC = globalOptions.useUTC,
GET = useUTC ? 'getUTC' : 'get',
SET = useUTC ? 'setUTC' : 'set';
Date = globalOptions.Date || window.Date;
timezoneOffset = useUTC && globalOptions.timezoneOffset;
getTimezoneOffset = useUTC && globalOptions.getTimezoneOffset;
makeTime = function (year, month, date, hours, minutes, seconds) {
var d;
if (useUTC) {
d = Date.UTC.apply(0, arguments);
d += getTZOffset(d);
} else {
d = new Date(
year,
month,
pick(date, 1),
pick(hours, 0),
pick(minutes, 0),
pick(seconds, 0)
).getTime();
}
return d;
};
getMinutes = GET + 'Minutes';
getHours = GET + 'Hours';
getDay = GET + 'Day';
getDate = GET + 'Date';
getMonth = GET + 'Month';
getFullYear = GET + 'FullYear';
setMilliseconds = SET + 'Milliseconds';
setSeconds = SET + 'Seconds';
setMinutes = SET + 'Minutes';
setHours = SET + 'Hours';
setDate = SET + 'Date';
setMonth = SET + 'Month';
setFullYear = SET + 'FullYear';
}
/**
* Merge the default options with custom options and return the new options structure
* @param {Object} options The new custom options
*/
function setOptions(options) {
// Copy in the default options
defaultOptions = merge(true, defaultOptions, options);
// Apply UTC
setTimeMethods();
return defaultOptions;
}
/**
* Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules
* wasn't enough because the setOptions method created a new object.
*/
function getOptions() {
return defaultOptions;
}
/**
* Handle color operations. The object methods are chainable.
* @param {String} input The input color in either rbga or hex format
*/
var rgbaRegEx = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/,
hexRegEx = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
rgbRegEx = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/;
var Color = function (input) {
// declare variables
var rgba = [], result, stops;
/**
* Parse the input color to rgba array
* @param {String} input
*/
function init(input) {
// Gradients
if (input && input.stops) {
stops = map(input.stops, function (stop) {
return Color(stop[1]);
});
// Solid colors
} else {
// rgba
result = rgbaRegEx.exec(input);
if (result) {
rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
} else {
// hex
result = hexRegEx.exec(input);
if (result) {
rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
} else {
// rgb
result = rgbRegEx.exec(input);
if (result) {
rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1];
}
}
}
}
}
/**
* Return the color a specified format
* @param {String} format
*/
function get(format) {
var ret;
if (stops) {
ret = merge(input);
ret.stops = [].concat(ret.stops);
each(stops, function (stop, i) {
ret.stops[i] = [ret.stops[i][0], stop.get(format)];
});
// it's NaN if gradient colors on a column chart
} else if (rgba && !isNaN(rgba[0])) {
if (format === 'rgb') {
ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
} else if (format === 'a') {
ret = rgba[3];
} else {
ret = 'rgba(' + rgba.join(',') + ')';
}
} else {
ret = input;
}
return ret;
}
/**
* Brighten the color
* @param {Number} alpha
*/
function brighten(alpha) {
if (stops) {
each(stops, function (stop) {
stop.brighten(alpha);
});
} else if (isNumber(alpha) && alpha !== 0) {
var i;
for (i = 0; i < 3; i++) {
rgba[i] += pInt(alpha * 255);
if (rgba[i] < 0) {
rgba[i] = 0;
}
if (rgba[i] > 255) {
rgba[i] = 255;
}
}
}
return this;
}
/**
* Set the color's opacity to a given alpha value
* @param {Number} alpha
*/
function setOpacity(alpha) {
rgba[3] = alpha;
return this;
}
// initialize: parse the input
init(input);
// public methods
return {
get: get,
brighten: brighten,
rgba: rgba,
setOpacity: setOpacity,
raw: input
};
};
/**
* A wrapper object for SVG elements
*/
function SVGElement() {}
SVGElement.prototype = {
// Default base for animation
opacity: 1,
// For labels, these CSS properties are applied to the
',
pointFormat: 'x: {point.x}
y: {point.y}
'
}
});
/**
* The scatter series class
*/
var ScatterSeries = extendClass(Series, {
type: 'scatter',
sorted: false,
requireSorting: false,
noSharedTooltip: true,
trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
takeOrdinalPosition: false, // #2342
kdDimensions: 2,
drawGraph: function () {
if (this.options.lineWidth) {
Series.prototype.drawGraph.call(this);
}
}
});
seriesTypes.scatter = ScatterSeries;
/**
* Set the default options for pie
*/
defaultPlotOptions.pie = merge(defaultSeriesOptions, {
borderColor: '#FFFFFF',
borderWidth: 1,
center: [null, null],
clip: false,
colorByPoint: true, // always true for pies
dataLabels: {
// align: null,
// connectorWidth: 1,
// connectorColor: point.color,
// connectorPadding: 5,
distance: 30,
enabled: true,
formatter: function () { // #2945
return this.y === null ? undefined : this.point.name;
},
// softConnector: true,
x: 0
// y: 0
},
ignoreHiddenPoint: true,
//innerSize: 0,
legendType: 'point',
marker: null, // point options are specified in the base options
size: null,
showInLegend: false,
slicedOffset: 10,
states: {
hover: {
brightness: 0.1,
shadow: false
}
},
stickyTracking: false,
tooltip: {
followPointer: true
}
});
/**
* Extended point object for pies
*/
var PiePoint = extendClass(Point, {
/**
* Initiate the pie slice
*/
init: function () {
Point.prototype.init.apply(this, arguments);
var point = this,
toggleSlice;
extend(point, {
visible: point.visible !== false,
name: pick(point.name, 'Slice')
});
// add event listener for select
toggleSlice = function (e) {
point.slice(e.type === 'select');
};
addEvent(point, 'select', toggleSlice);
addEvent(point, 'unselect', toggleSlice);
return point;
},
/**
* Toggle the visibility of the pie slice
* @param {Boolean} vis Whether to show the slice or not. If undefined, the
* visibility is toggled
*/
setVisible: function (vis, redraw) {
var point = this,
series = point.series,
chart = series.chart,
ignoreHiddenPoint = series.options.ignoreHiddenPoint;
redraw = pick(redraw, ignoreHiddenPoint);
if (vis !== point.visible) {
// If called without an argument, toggle visibility
point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis;
series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
// Show and hide associated elements. This is performed regardless of redraw or not,
// because chart.redraw only handles full series.
each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) {
if (point[key]) {
point[key][vis ? 'show' : 'hide'](true);
}
});
if (point.legendItem) {
chart.legend.colorizeItem(point, vis);
}
// #4170, hide halo after hiding point
if (!vis && point.state === 'hover') {
point.setState('');
}
// Handle ignore hidden slices
if (ignoreHiddenPoint) {
series.isDirty = true;
}
if (redraw) {
chart.redraw();
}
}
},
/**
* Set or toggle whether the slice is cut out from the pie
* @param {Boolean} sliced When undefined, the slice state is toggled
* @param {Boolean} redraw Whether to redraw the chart. True by default.
*/
slice: function (sliced, redraw, animation) {
var point = this,
series = point.series,
chart = series.chart,
translation;
setAnimation(animation, chart);
// redraw is true by default
redraw = pick(redraw, true);
// if called without an argument, toggle
point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced;
series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
translation = sliced ? point.slicedTranslation : {
translateX: 0,
translateY: 0
};
point.graphic.animate(translation);
if (point.shadowGroup) {
point.shadowGroup.animate(translation);
}
},
haloPath: function (size) {
var shapeArgs = this.shapeArgs,
chart = this.series.chart;
return this.sliced || !this.visible ? [] : this.series.chart.renderer.symbols.arc(chart.plotLeft + shapeArgs.x, chart.plotTop + shapeArgs.y, shapeArgs.r + size, shapeArgs.r + size, {
innerR: this.shapeArgs.r,
start: shapeArgs.start,
end: shapeArgs.end
});
}
});
/**
* The Pie series class
*/
var PieSeries = {
type: 'pie',
isCartesian: false,
pointClass: PiePoint,
requireSorting: false,
directTouch: true,
noSharedTooltip: true,
trackerGroups: ['group', 'dataLabelsGroup'],
axisTypes: [],
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
stroke: 'borderColor',
'stroke-width': 'borderWidth',
fill: 'color'
},
/**
* Animate the pies in
*/
animate: function (init) {
var series = this,
points = series.points,
startAngleRad = series.startAngleRad;
if (!init) {
each(points, function (point) {
var graphic = point.graphic,
args = point.shapeArgs;
if (graphic) {
// start values
graphic.attr({
r: point.startR || (series.center[3] / 2), // animate from inner radius (#779)
start: startAngleRad,
end: startAngleRad
});
// animate
graphic.animate({
r: args.r,
start: args.start,
end: args.end
}, series.options.animation);
}
});
// delete this function to allow it only once
series.animate = null;
}
},
/**
* Extend the basic setData method by running processData and generatePoints immediately,
* in order to access the points from the legend.
*/
setData: function (data, redraw, animation, updatePoints) {
Series.prototype.setData.call(this, data, false, animation, updatePoints);
this.processData();
this.generatePoints();
if (pick(redraw, true)) {
this.chart.redraw(animation);
}
},
/**
* Recompute total chart sum and update percentages of points.
*/
updateTotals: function () {
var i,
total = 0,
points = this.points,
len = points.length,
point,
ignoreHiddenPoint = this.options.ignoreHiddenPoint;
// Get the total sum
for (i = 0; i < len; i++) {
point = points[i];
total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y;
}
this.total = total;
// Set each point's properties
for (i = 0; i < len; i++) {
point = points[i];
point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0;
point.total = total;
}
},
/**
* Extend the generatePoints method by adding total and percentage properties to each point
*/
generatePoints: function () {
Series.prototype.generatePoints.call(this);
this.updateTotals();
},
/**
* Do translation for pie slices
*/
translate: function (positions) {
this.generatePoints();
var series = this,
cumulative = 0,
precision = 1000, // issue #172
options = series.options,
slicedOffset = options.slicedOffset,
connectorOffset = slicedOffset + options.borderWidth,
start,
end,
angle,
startAngle = options.startAngle || 0,
startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90),
endAngleRad = series.endAngleRad = mathPI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90),
circ = endAngleRad - startAngleRad, //2 * mathPI,
points = series.points,
radiusX, // the x component of the radius vector for a given point
radiusY,
labelDistance = options.dataLabels.distance,
ignoreHiddenPoint = options.ignoreHiddenPoint,
i,
len = points.length,
point;
// Get positions - either an integer or a percentage string must be given.
// If positions are passed as a parameter, we're in a recursive loop for adjusting
// space for data labels.
if (!positions) {
series.center = positions = series.getCenter();
}
// utility for getting the x value from a given y, used for anticollision logic in data labels
series.getX = function (y, left) {
angle = math.asin(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1));
return positions[0] +
(left ? -1 : 1) *
(mathCos(angle) * (positions[2] / 2 + labelDistance));
};
// Calculate the geometry for each point
for (i = 0; i < len; i++) {
point = points[i];
// set start and end angle
start = startAngleRad + (cumulative * circ);
if (!ignoreHiddenPoint || point.visible) {
cumulative += point.percentage / 100;
}
end = startAngleRad + (cumulative * circ);
// set the shape
point.shapeType = 'arc';
point.shapeArgs = {
x: positions[0],
y: positions[1],
r: positions[2] / 2,
innerR: positions[3] / 2,
start: mathRound(start * precision) / precision,
end: mathRound(end * precision) / precision
};
// The angle must stay within -90 and 270 (#2645)
angle = (end + start) / 2;
if (angle > 1.5 * mathPI) {
angle -= 2 * mathPI;
} else if (angle < -mathPI / 2) {
angle += 2 * mathPI;
}
// Center for the sliced out slice
point.slicedTranslation = {
translateX: mathRound(mathCos(angle) * slicedOffset),
translateY: mathRound(mathSin(angle) * slicedOffset)
};
// set the anchor point for tooltips
radiusX = mathCos(angle) * positions[2] / 2;
radiusY = mathSin(angle) * positions[2] / 2;
point.tooltipPos = [
positions[0] + radiusX * 0.7,
positions[1] + radiusY * 0.7
];
point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0;
point.angle = angle;
// set the anchor point for data labels
connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678
point.labelPos = [
positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector
positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a
positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie
positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a
positions[0] + radiusX, // landing point for connector
positions[1] + radiusY, // a/a
labelDistance < 0 ? // alignment
'center' :
point.half ? 'right' : 'left', // alignment
angle // center angle
];
}
},
drawGraph: null,
/**
* Draw the data points
*/
drawPoints: function () {
var series = this,
chart = series.chart,
renderer = chart.renderer,
groupTranslation,
//center,
graphic,
//group,
shadow = series.options.shadow,
shadowGroup,
shapeArgs,
attr;
if (shadow && !series.shadowGroup) {
series.shadowGroup = renderer.g('shadow')
.add(series.group);
}
// draw the slices
each(series.points, function (point) {
if (point.y !== null) {
graphic = point.graphic;
shapeArgs = point.shapeArgs;
shadowGroup = point.shadowGroup;
// put the shadow behind all points
if (shadow && !shadowGroup) {
shadowGroup = point.shadowGroup = renderer.g('shadow')
.add(series.shadowGroup);
}
// if the point is sliced, use special translation, else use plot area traslation
groupTranslation = point.sliced ? point.slicedTranslation : {
translateX: 0,
translateY: 0
};
//group.translate(groupTranslation[0], groupTranslation[1]);
if (shadowGroup) {
shadowGroup.attr(groupTranslation);
}
// draw the slice
if (graphic) {
graphic.animate(extend(shapeArgs, groupTranslation));
} else {
attr = { 'stroke-linejoin': 'round' };
if (!point.visible) {
attr.visibility = 'hidden';
}
point.graphic = graphic = renderer[point.shapeType](shapeArgs)
.setRadialReference(series.center)
.attr(
point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]
)
.attr(attr)
.attr(groupTranslation)
.add(series.group)
.shadow(shadow, shadowGroup);
}
}
});
},
searchPoint: noop,
/**
* Utility for sorting data labels
*/
sortByAngle: function (points, sign) {
points.sort(function (a, b) {
return a.angle !== undefined && (b.angle - a.angle) * sign;
});
},
/**
* Use a simple symbol from LegendSymbolMixin
*/
drawLegendSymbol: LegendSymbolMixin.drawRectangle,
/**
* Use the getCenter method from drawLegendSymbol
*/
getCenter: CenteredSeriesMixin.getCenter,
/**
* Pies don't have point marker symbols
*/
getSymbol: noop
};
PieSeries = extendClass(Series, PieSeries);
seriesTypes.pie = PieSeries;
/**
* Draw the data labels
*/
Series.prototype.drawDataLabels = function () {
var series = this,
seriesOptions = series.options,
cursor = seriesOptions.cursor,
options = seriesOptions.dataLabels,
points = series.points,
pointOptions,
generalOptions,
hasRendered = series.hasRendered || 0,
str,
dataLabelsGroup,
renderer = series.chart.renderer;
if (options.enabled || series._hasPointLabels) {
// Process default alignment of data labels for columns
if (series.dlProcessOptions) {
series.dlProcessOptions(options);
}
// Create a separate group for the data labels to avoid rotation
dataLabelsGroup = series.plotGroup(
'dataLabelsGroup',
'data-labels',
options.defer ? HIDDEN : VISIBLE,
options.zIndex || 6
);
if (pick(options.defer, true)) {
dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300
if (!hasRendered) {
addEvent(series, 'afterAnimate', function () {
if (series.visible) { // #3023, #3024
dataLabelsGroup.show();
}
dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 });
});
}
}
// Make the labels for each point
generalOptions = options;
each(points, function (point) {
var enabled,
dataLabel = point.dataLabel,
labelConfig,
attr,
name,
rotation,
connector = point.connector,
isNew = true,
style,
moreStyle = {};
// Determine if each data label is enabled
pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282
// If the point is outside the plot area, destroy it. #678, #820
if (dataLabel && !enabled) {
point.dataLabel = dataLabel.destroy();
// Individual labels are disabled if the are explicitly disabled
// in the point options, or if they fall outside the plot area.
} else if (enabled) {
// Create individual options structure that can be extended without
// affecting others
options = merge(generalOptions, pointOptions);
style = options.style;
rotation = options.rotation;
// Get the string
labelConfig = point.getLabelConfig();
str = options.format ?
format(options.format, labelConfig) :
options.formatter.call(labelConfig, options);
// Determine the color
style.color = pick(options.color, style.color, series.color, 'black');
// update existing label
if (dataLabel) {
if (defined(str)) {
dataLabel
.attr({
text: str
});
isNew = false;
} else { // #1437 - the label is shown conditionally
point.dataLabel = dataLabel = dataLabel.destroy();
if (connector) {
point.connector = connector.destroy();
}
}
// create new label
} else if (defined(str)) {
attr = {
//align: align,
fill: options.backgroundColor,
stroke: options.borderColor,
'stroke-width': options.borderWidth,
r: options.borderRadius || 0,
rotation: rotation,
padding: options.padding,
zIndex: 1
};
// Get automated contrast color
if (style.color === 'contrast') {
moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ?
renderer.getContrast(point.color || series.color) :
'#000000';
}
if (cursor) {
moreStyle.cursor = cursor;
}
// Remove unused attributes (#947)
for (name in attr) {
if (attr[name] === UNDEFINED) {
delete attr[name];
}
}
dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
str,
0,
-999,
options.shape,
null,
null,
options.useHTML
)
.attr(attr)
.css(extend(style, moreStyle))
.add(dataLabelsGroup)
.shadow(options.shadow);
}
if (dataLabel) {
// Now the data label is created and placed at 0,0, so we need to align it
series.alignDataLabel(point, dataLabel, options, null, isNew);
}
}
});
}
};
/**
* Align each individual data label
*/
Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
var chart = this.chart,
inverted = chart.inverted,
plotX = pick(point.plotX, -999),
plotY = pick(point.plotY, -999),
bBox = dataLabel.getBBox(),
baseline = chart.renderer.fontMetrics(options.style.fontSize).b,
rotCorr, // rotation correction
// Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) ||
(alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
alignAttr; // the final position;
if (visible) {
// The alignment box is a singular point
alignTo = extend({
x: inverted ? chart.plotWidth - plotY : plotX,
y: mathRound(inverted ? chart.plotHeight - plotX : plotY),
width: 0,
height: 0
}, alignTo);
// Add the text size for alignment calculation
extend(options, {
width: bBox.width,
height: bBox.height
});
// Allow a hook for changing alignment in the last moment, then do the alignment
if (options.rotation) { // Fancy box alignment isn't supported for rotated text
rotCorr = chart.renderer.rotCorr(baseline, options.rotation); // #3723
dataLabel[isNew ? 'attr' : 'animate']({
x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
y: alignTo.y + options.y + alignTo.height / 2
})
.attr({ // #3003
align: options.align
});
} else {
dataLabel.align(options, null, alignTo);
alignAttr = dataLabel.alignAttr;
// Handle justify or crop
if (pick(options.overflow, 'justify') === 'justify') {
this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
} else if (pick(options.crop, true)) {
// Now check that the data label is within the plot area
visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
}
// When we're using a shape, make it possible with a connector or an arrow pointing to thie point
if (options.shape) {
dataLabel.attr({
anchorX: point.plotX,
anchorY: point.plotY
});
}
}
}
// Show or hide based on the final aligned position
if (!visible) {
dataLabel.attr({ y: -999 });
dataLabel.placed = false; // don't animate back in
}
};
/**
* If data labels fall partly outside the plot area, align them back in, in a way that
* doesn't hide the point.
*/
Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) {
var chart = this.chart,
align = options.align,
verticalAlign = options.verticalAlign,
off,
justified,
padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
// Off left
off = alignAttr.x + padding;
if (off < 0) {
if (align === 'right') {
options.align = 'left';
} else {
options.x = -off;
}
justified = true;
}
// Off right
off = alignAttr.x + bBox.width - padding;
if (off > chart.plotWidth) {
if (align === 'left') {
options.align = 'right';
} else {
options.x = chart.plotWidth - off;
}
justified = true;
}
// Off top
off = alignAttr.y + padding;
if (off < 0) {
if (verticalAlign === 'bottom') {
options.verticalAlign = 'top';
} else {
options.y = -off;
}
justified = true;
}
// Off bottom
off = alignAttr.y + bBox.height - padding;
if (off > chart.plotHeight) {
if (verticalAlign === 'top') {
options.verticalAlign = 'bottom';
} else {
options.y = chart.plotHeight - off;
}
justified = true;
}
if (justified) {
dataLabel.placed = !isNew;
dataLabel.align(options, null, alignTo);
}
};
/**
* Override the base drawDataLabels method by pie specific functionality
*/
if (seriesTypes.pie) {
seriesTypes.pie.prototype.drawDataLabels = function () {
var series = this,
data = series.data,
point,
chart = series.chart,
options = series.options.dataLabels,
connectorPadding = pick(options.connectorPadding, 10),
connectorWidth = pick(options.connectorWidth, 1),
plotWidth = chart.plotWidth,
plotHeight = chart.plotHeight,
connector,
connectorPath,
softConnector = pick(options.softConnector, true),
distanceOption = options.distance,
seriesCenter = series.center,
radius = seriesCenter[2] / 2,
centerY = seriesCenter[1],
outside = distanceOption > 0,
dataLabel,
dataLabelWidth,
labelPos,
labelHeight,
halves = [// divide the points into right and left halves for anti collision
[], // right
[] // left
],
x,
y,
visibility,
rankArr,
i,
j,
overflow = [0, 0, 0, 0], // top, right, bottom, left
sort = function (a, b) {
return b.y - a.y;
};
// get out if not enabled
if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
return;
}
// run parent method
Series.prototype.drawDataLabels.apply(series);
// arrange points for detection collision
each(data, function (point) {
if (point.dataLabel && point.visible) { // #407, #2510
halves[point.half].push(point);
}
});
/* Loop over the points in each half, starting from the top and bottom
* of the pie to detect overlapping labels.
*/
i = 2;
while (i--) {
var slots = [],
slotsLength,
usedSlots = [],
points = halves[i],
pos,
bottom,
length = points.length,
slotIndex;
if (!length) {
continue;
}
// Sort by angle
series.sortByAngle(points, i - 0.5);
// Assume equal label heights on either hemisphere (#2630)
j = labelHeight = 0;
while (!labelHeight && points[j]) { // #1569
labelHeight = points[j] && points[j].dataLabel && (points[j].dataLabel.getBBox().height || 21); // 21 is for #968
j++;
}
// Only do anti-collision when we are outside the pie and have connectors (#856)
if (distanceOption > 0) {
// Build the slots
bottom = mathMin(centerY + radius + distanceOption, chart.plotHeight);
for (pos = mathMax(0, centerY - radius - distanceOption); pos <= bottom; pos += labelHeight) {
slots.push(pos);
}
slotsLength = slots.length;
/* Visualize the slots
if (!series.slotElements) {
series.slotElements = [];
}
if (i === 1) {
series.slotElements.forEach(function (elem) {
elem.destroy();
});
series.slotElements.length = 0;
}
slots.forEach(function (pos, no) {
var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0),
slotY = pos + chart.plotTop;
if (!isNaN(slotX)) {
series.slotElements.push(chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1)
.attr({
'stroke-width': 1,
stroke: 'silver',
fill: 'rgba(0,0,255,0.1)'
})
.add());
series.slotElements.push(chart.renderer.text('Slot '+ no, slotX, slotY + 4)
.attr({
fill: 'silver'
}).add());
}
});
// */
// if there are more values than available slots, remove lowest values
if (length > slotsLength) {
// create an array for sorting and ranking the points within each quarter
rankArr = [].concat(points);
rankArr.sort(sort);
j = length;
while (j--) {
rankArr[j].rank = j;
}
j = length;
while (j--) {
if (points[j].rank >= slotsLength) {
points.splice(j, 1);
}
}
length = points.length;
}
// The label goes to the nearest open slot, but not closer to the edge than
// the label's index.
for (j = 0; j < length; j++) {
point = points[j];
labelPos = point.labelPos;
var closest = 9999,
distance,
slotI;
// find the closest slot index
for (slotI = 0; slotI < slotsLength; slotI++) {
distance = mathAbs(slots[slotI] - labelPos[1]);
if (distance < closest) {
closest = distance;
slotIndex = slotI;
}
}
// if that slot index is closer to the edges of the slots, move it
// to the closest appropriate slot
if (slotIndex < j && slots[j] !== null) { // cluster at the top
slotIndex = j;
} else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom
slotIndex = slotsLength - length + j;
while (slots[slotIndex] === null) { // make sure it is not taken
slotIndex++;
}
} else {
// Slot is taken, find next free slot below. In the next run, the next slice will find the
// slot above these, because it is the closest one
while (slots[slotIndex] === null) { // make sure it is not taken
slotIndex++;
}
}
usedSlots.push({ i: slotIndex, y: slots[slotIndex] });
slots[slotIndex] = null; // mark as taken
}
// sort them in order to fill in from the top
usedSlots.sort(sort);
}
// now the used slots are sorted, fill them up sequentially
for (j = 0; j < length; j++) {
var slot, naturalY;
point = points[j];
labelPos = point.labelPos;
dataLabel = point.dataLabel;
visibility = point.visible === false ? HIDDEN : 'inherit';
naturalY = labelPos[1];
if (distanceOption > 0) {
slot = usedSlots.pop();
slotIndex = slot.i;
// if the slot next to currrent slot is free, the y value is allowed
// to fall back to the natural position
y = slot.y;
if ((naturalY > y && slots[slotIndex + 1] !== null) ||
(naturalY < y && slots[slotIndex - 1] !== null)) {
y = mathMin(mathMax(0, naturalY), chart.plotHeight);
}
} else {
y = naturalY;
}
// get the x - use the natural x position for first and last slot, to prevent the top
// and botton slice connectors from touching each other on either side
x = options.justify ?
seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) :
series.getX(y === centerY - radius - distanceOption || y === centerY + radius + distanceOption ? naturalY : y, i);
// Record the placement and visibility
dataLabel._attr = {
visibility: visibility,
align: labelPos[6]
};
dataLabel._pos = {
x: x + options.x +
({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
y: y + options.y - 10 // 10 is for the baseline (label vs text)
};
dataLabel.connX = x;
dataLabel.connY = y;
// Detect overflowing data labels
if (this.options.size === null) {
dataLabelWidth = dataLabel.width;
// Overflow left
if (x - dataLabelWidth < connectorPadding) {
overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]);
// Overflow right
} else if (x + dataLabelWidth > plotWidth - connectorPadding) {
overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
}
// Overflow top
if (y - labelHeight / 2 < 0) {
overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]);
// Overflow left
} else if (y + labelHeight / 2 > plotHeight) {
overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]);
}
}
} // for each point
} // for each half
// Do not apply the final placement and draw the connectors until we have verified
// that labels are not spilling over.
if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
// Place the labels in the final position
this.placeDataLabels();
// Draw the connectors
if (outside && connectorWidth) {
each(this.points, function (point) {
connector = point.connector;
labelPos = point.labelPos;
dataLabel = point.dataLabel;
if (dataLabel && dataLabel._pos && point.visible) {
visibility = dataLabel._attr.visibility;
x = dataLabel.connX;
y = dataLabel.connY;
connectorPath = softConnector ? [
M,
x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
'C',
x, y, // first break, next to the label
2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
labelPos[2], labelPos[3], // second break
L,
labelPos[4], labelPos[5] // base
] : [
M,
x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
L,
labelPos[2], labelPos[3], // second break
L,
labelPos[4], labelPos[5] // base
];
if (connector) {
connector.animate({ d: connectorPath });
connector.attr('visibility', visibility);
} else {
point.connector = connector = series.chart.renderer.path(connectorPath).attr({
'stroke-width': connectorWidth,
stroke: options.connectorColor || point.color || '#606060',
visibility: visibility
//zIndex: 0 // #2722 (reversed)
})
.add(series.dataLabelsGroup);
}
} else if (connector) {
point.connector = connector.destroy();
}
});
}
}
};
/**
* Perform the final placement of the data labels after we have verified that they
* fall within the plot area.
*/
seriesTypes.pie.prototype.placeDataLabels = function () {
each(this.points, function (point) {
var dataLabel = point.dataLabel,
_pos;
if (dataLabel && point.visible) {
_pos = dataLabel._pos;
if (_pos) {
dataLabel.attr(dataLabel._attr);
dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
dataLabel.moved = true;
} else if (dataLabel) {
dataLabel.attr({ y: -999 });
}
}
});
};
seriesTypes.pie.prototype.alignDataLabel = noop;
/**
* Verify whether the data labels are allowed to draw, or we should run more translation and data
* label positioning to keep them inside the plot area. Returns true when data labels are ready
* to draw.
*/
seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
var center = this.center,
options = this.options,
centerOption = options.center,
minSize = options.minSize || 80,
newSize = minSize,
ret;
// Handle horizontal size and center
if (centerOption[0] !== null) { // Fixed center
newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize);
} else { // Auto center
newSize = mathMax(
center[2] - overflow[1] - overflow[3], // horizontal overflow
minSize
);
center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
}
// Handle vertical size and center
if (centerOption[1] !== null) { // Fixed center
newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize);
} else { // Auto center
newSize = mathMax(
mathMin(
newSize,
center[2] - overflow[0] - overflow[2] // vertical overflow
),
minSize
);
center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
}
// If the size must be decreased, we need to run translate and drawDataLabels again
if (newSize < center[2]) {
center[2] = newSize;
center[3] = relativeLength(options.innerSize || 0, newSize);
this.translate(center);
each(this.points, function (point) {
if (point.dataLabel) {
point.dataLabel._pos = null; // reset
}
});
if (this.drawDataLabels) {
this.drawDataLabels();
}
// Else, return true to indicate that the pie and its labels is within the plot area
} else {
ret = true;
}
return ret;
};
}
if (seriesTypes.column) {
/**
* Override the basic data label alignment by adjusting for the position of the column
*/
seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
var inverted = this.chart.inverted,
series = point.series,
dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series
inside = pick(options.inside, !!this.options.stacking); // draw it inside the box?
// Align to the column itself, or the top of it
if (dlBox) { // Area range uses this method but not alignTo
alignTo = merge(dlBox);
if (inverted) {
alignTo = {
x: series.yAxis.len - alignTo.y - alignTo.height,
y: series.xAxis.len - alignTo.x - alignTo.width,
width: alignTo.height,
height: alignTo.width
};
}
// Compute the alignment box
if (!inside) {
if (inverted) {
alignTo.x += below ? 0 : alignTo.width;
alignTo.width = 0;
} else {
alignTo.y += below ? alignTo.height : 0;
alignTo.height = 0;
}
}
}
// When alignment is undefined (typically columns and bars), display the individual
// point below or above the point depending on the threshold
options.align = pick(
options.align,
!inverted || inside ? 'center' : below ? 'right' : 'left'
);
options.verticalAlign = pick(
options.verticalAlign,
inverted || inside ? 'middle' : below ? 'top' : 'bottom'
);
// Call the parent method
Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
};
}
/**
* Highcharts JS v4.1.8 (2015-08-20)
* Highcharts module to hide overlapping data labels. This module is included by default in Highmaps.
*
* (c) 2010-2014 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/*global Highcharts, HighchartsAdapter */
(function (H) {
var Chart = H.Chart,
each = H.each,
pick = H.pick,
addEvent = HighchartsAdapter.addEvent;
// Collect potensial overlapping data labels. Stack labels probably don't need to be
// considered because they are usually accompanied by data labels that lie inside the columns.
Chart.prototype.callbacks.push(function (chart) {
function collectAndHide() {
var labels = [];
each(chart.series, function (series) {
var dlOptions = series.options.dataLabels,
collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections
if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866
each(collections, function (coll) {
each(series.points, function (point) {
if (point[coll]) {
point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
labels.push(point[coll]);
}
});
});
}
});
chart.hideOverlappingLabels(labels);
}
// Do it now ...
collectAndHide();
// ... and after each chart redraw
addEvent(chart, 'redraw', collectAndHide);
});
/**
* Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
* visual imression.
*/
Chart.prototype.hideOverlappingLabels = function (labels) {
var len = labels.length,
label,
i,
j,
label1,
label2,
isIntersecting,
pos1,
pos2,
padding,
intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) {
return !(
x2 > x1 + w1 ||
x2 + w2 < x1 ||
y2 > y1 + h1 ||
y2 + h2 < y1
);
};
// Mark with initial opacity
for (i = 0; i < len; i++) {
label = labels[i];
if (label) {
label.oldOpacity = label.opacity;
label.newOpacity = 1;
}
}
// Prevent a situation in a gradually rising slope, that each label
// will hide the previous one because the previous one always has
// lower rank.
labels.sort(function (a, b) {
return (b.labelrank || 0) - (a.labelrank || 0);
});
// Detect overlapping labels
for (i = 0; i < len; i++) {
label1 = labels[i];
for (j = i + 1; j < len; ++j) {
label2 = labels[j];
if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) {
pos1 = label1.alignAttr;
pos2 = label2.alignAttr;
padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333)
isIntersecting = intersectRect(
pos1.x,
pos1.y,
label1.width - padding,
label1.height - padding,
pos2.x,
pos2.y,
label2.width - padding,
label2.height - padding
);
if (isIntersecting) {
(label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
}
}
}
}
// Hide or show
each(labels, function (label) {
var complete,
newOpacity;
if (label) {
newOpacity = label.newOpacity;
if (label.oldOpacity !== newOpacity && label.placed) {
// Make sure the label is completely hidden to avoid catching clicks (#4362)
if (newOpacity) {
label.show(true);
} else {
complete = function () {
label.hide();
};
}
// Animate or set the opacity
label.alignAttr.opacity = newOpacity;
label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
}
label.isOld = true;
}
});
};
}(Highcharts4));/**
* TrackerMixin for points and graphs
*/
var TrackerMixin = Highcharts4.TrackerMixin = {
drawTrackerPoint: function () {
var series = this,
chart = series.chart,
pointer = chart.pointer,
cursor = series.options.cursor,
css = cursor && { cursor: cursor },
onMouseOver = function (e) {
var target = e.target,
point;
while (target && !point) {
point = target.point;
target = target.parentNode;
}
if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart
point.onMouseOver(e);
}
};
// Add reference to the point
each(series.points, function (point) {
if (point.graphic) {
point.graphic.element.point = point;
}
if (point.dataLabel) {
point.dataLabel.element.point = point;
}
if (point.graphic2) { //VE Mar 14, 2016, replicating point.graphic behavior for custom column handles in point.graphic2
point.graphic2.element.point = point;
}
});
// Add the event listeners, we need to do this only once
if (!series._hasTracking) {
each(series.trackerGroups, function (key) {
if (series[key]) { // we don't always have dataLabelsGroup
series[key]
.addClass(PREFIX + 'tracker')
.on('mouseover', onMouseOver)
.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
.css(css);
if (hasTouch) {
series[key].on('touchstart', onMouseOver);
}
}
});
series._hasTracking = true;
}
},
/**
* Draw the tracker object that sits above all data labels and markers to
* track mouse events on the graph or points. For the line type charts
* the tracker uses the same graphPath, but with a greater stroke width
* for better control.
*/
drawTrackerGraph: function () {
var series = this,
options = series.options,
trackByArea = options.trackByArea,
trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
trackerPathLength = trackerPath.length,
chart = series.chart,
pointer = chart.pointer,
renderer = chart.renderer,
snap = chart.options.tooltip.snap,
tracker = series.tracker,
cursor = options.cursor,
css = cursor && { cursor: cursor },
singlePoints = series.singlePoints,
singlePoint,
i,
onMouseOver = function () {
if (chart.hoverSeries !== series) {
series.onMouseOver();
}
},
/*
* Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
* IE6: 0.002
* IE7: 0.002
* IE8: 0.002
* IE9: 0.00000000001 (unlimited)
* IE10: 0.0001 (exporting only)
* FF: 0.00000000001 (unlimited)
* Chrome: 0.000001
* Safari: 0.000001
* Opera: 0.00000000001 (unlimited)
*/
TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')';
// Extend end points. A better way would be to use round linecaps,
// but those are not clickable in VML.
if (trackerPathLength && !trackByArea) {
i = trackerPathLength + 1;
while (i--) {
if (trackerPath[i] === M) { // extend left side
trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
}
if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
}
}
}
// handle single points
for (i = 0; i < singlePoints.length; i++) {
singlePoint = singlePoints[i];
trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
L, singlePoint.plotX + snap, singlePoint.plotY);
}
// draw the tracker
if (tracker) {
tracker.attr({ d: trackerPath });
} else { // create
series.tracker = renderer.path(trackerPath)
.attr({
'stroke-linejoin': 'round', // #1225
visibility: series.visible ? VISIBLE : HIDDEN,
stroke: TRACKER_FILL,
fill: trackByArea ? TRACKER_FILL : NONE,
'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap),
zIndex: 2
})
.add(series.group);
// The tracker is added to the series group, which is clipped, but is covered
// by the marker group. So the marker group also needs to capture events.
each([series.tracker, series.markerGroup], function (tracker) {
tracker.addClass(PREFIX + 'tracker')
.on('mouseover', onMouseOver)
.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
.css(css);
if (hasTouch) {
tracker.on('touchstart', onMouseOver);
}
});
}
}
};
/* End TrackerMixin */
/**
* Add tracking event listener to the series group, so the point graphics
* themselves act as trackers
*/
if (seriesTypes.column) {
ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
}
if (seriesTypes.pie) {
seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
}
if (seriesTypes.scatter) {
ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
}
/*
* Extend Legend for item events
*/
extend(Legend.prototype, {
setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) {
var legend = this;
// Set the events on the item group, or in case of useHTML, the item itself (#1249)
(useHTML ? legendItem : item.legendGroup).on('mouseover', function () {
item.setState(HOVER_STATE);
legendItem.css(legend.options.itemHoverStyle);
})
.on('mouseout', function () {
legendItem.css(item.visible ? itemStyle : itemHiddenStyle);
item.setState();
})
.on('click', function (event) {
var strLegendItemClick = 'legendItemClick',
fnLegendItemClick = function () {
item.setVisible();
};
// Pass over the click/touch event. #4.
event = {
browserEvent: event
};
// click the name or symbol
if (item.firePointEvent) { // point
item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
} else {
fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
}
});
},
createCheckboxForItem: function (item) {
var legend = this;
item.checkbox = createElement('input', {
type: 'checkbox',
checked: item.selected,
defaultChecked: item.selected // required by IE7
}, legend.options.itemCheckboxStyle, legend.chart.container);
addEvent(item.checkbox, 'click', function (event) {
var target = event.target;
fireEvent(item.series || item, 'checkboxClick', { // #3712
checked: target.checked,
item: item
},
function () {
item.select();
}
);
});
}
});
/*
* Add pointer cursor to legend itemstyle in defaultOptions
*/
defaultOptions.legend.itemStyle.cursor = 'pointer';
/*
* Extend the Chart object with interaction
*/
extend(Chart.prototype, {
/**
* Display the zoom button
*/
showResetZoom: function () {
var chart = this,
lang = defaultOptions.lang,
btnOptions = chart.options.chart.resetZoomButton,
theme = btnOptions.theme,
states = theme.states,
alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover)
.attr({
align: btnOptions.position.align,
title: lang.resetZoomTitle
})
.add()
.align(btnOptions.position, false, alignTo);
},
/**
* Zoom out to 1:1
*/
zoomOut: function () {
var chart = this;
fireEvent(chart, 'selection', { resetSelection: true }, function () {
chart.zoom();
});
},
/**
* Zoom into a given portion of the chart given by axis coordinates
* @param {Object} event
*/
zoom: function (event) {
var chart = this,
hasZoomed,
pointer = chart.pointer,
displayButton = false,
resetZoomButton;
// If zoom is called with no arguments, reset the axes
if (!event || event.resetSelection) {
each(chart.axes, function (axis) {
hasZoomed = axis.zoom();
});
} else { // else, zoom in on all axes
each(event.xAxis.concat(event.yAxis), function (axisData) {
var axis = axisData.axis,
isXAxis = axis.isXAxis;
// don't zoom more than minRange
if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
hasZoomed = axis.zoom(axisData.min, axisData.max);
if (axis.displayBtn) {
displayButton = true;
}
}
});
}
// Show or hide the Reset zoom button
resetZoomButton = chart.resetZoomButton;
if (displayButton && !resetZoomButton) {
chart.showResetZoom();
} else if (!displayButton && isObject(resetZoomButton)) {
chart.resetZoomButton = resetZoomButton.destroy();
}
// Redraw
if (hasZoomed) {
chart.redraw(
pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
);
}
},
/**
* Pan the chart by dragging the mouse across the pane. This function is called
* on mouse move, and the distance to pan is computed from chartX compared to
* the first chartX position in the dragging operation.
*/
pan: function (e, panning) {
var chart = this,
hoverPoints = chart.hoverPoints,
doRedraw;
// remove active points for shared tooltip
if (hoverPoints) {
each(hoverPoints, function (point) {
point.setState();
});
}
each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps
var mousePos = e[isX ? 'chartX' : 'chartY'],
axis = chart[isX ? 'xAxis' : 'yAxis'][0],
startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'],
halfPointRange = (axis.pointRange || 0) / 2,
extremes = axis.getExtremes(),
newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange,
goingLeft = startPos > mousePos; // #3613
if (axis.series.length &&
(goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) &&
(!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) {
axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' });
doRedraw = true;
}
chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run
});
if (doRedraw) {
chart.redraw(false);
}
css(chart.container, { cursor: 'move' });
}
});
/*
* Extend the Point object with interaction
*/
extend(Point.prototype, {
/**
* Toggle the selection status of a point
* @param {Boolean} selected Whether to select or unselect the point.
* @param {Boolean} accumulate Whether to add to the previous selection. By default,
* this happens if the control key (Cmd on Mac) was pressed during clicking.
*/
select: function (selected, accumulate) {
var point = this,
series = point.series,
chart = series.chart;
selected = pick(selected, !point.selected);
// fire the event with the defalut handler
point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
point.selected = point.options.selected = selected;
series.options.data[inArray(point, series.data)] = point.options;
point.setState(selected && SELECT_STATE);
// unselect all other points unless Ctrl or Cmd + click
if (!accumulate) {
each(chart.getSelectedPoints(), function (loopPoint) {
if (loopPoint.selected && loopPoint !== point) {
loopPoint.selected = loopPoint.options.selected = false;
series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
loopPoint.setState(NORMAL_STATE);
loopPoint.firePointEvent('unselect');
}
});
}
});
},
/**
* Runs on mouse over the point
*
* @param {Object} e The event arguments
* @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to
* actually hovered points. True for other points in shared tooltip.
*/
onMouseOver: function (e, byProximity) {
var point = this,
series = point.series,
chart = series.chart,
tooltip = chart.tooltip,
hoverPoint = chart.hoverPoint;
if (chart.hoverSeries !== series) {
series.onMouseOver();
}
// set normal state to previous series
if (hoverPoint && hoverPoint !== point) {
hoverPoint.onMouseOut();
}
if (point.series) { // It may have been destroyed, #4130
// trigger the event
point.firePointEvent('mouseOver');
// update the tooltip
if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
tooltip.refresh(point, e);
}
// hover this
point.setState(HOVER_STATE);
if (!byProximity) {
chart.hoverPoint = point;
}
}
},
/**
* Runs on mouse out from the point
*/
onMouseOut: function () {
var chart = this.series.chart,
hoverPoints = chart.hoverPoints;
this.firePointEvent('mouseOut');
if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240
this.setState();
chart.hoverPoint = null;
}
},
/**
* Import events from the series' and point's options. Only do it on
* demand, to save processing time on hovering.
*/
importEvents: function () {
if (!this.hasImportedEvents) {
var point = this,
options = merge(point.series.options.point, point.options),
events = options.events,
eventType;
point.events = events;
for (eventType in events) {
addEvent(point, eventType, events[eventType]);
}
this.hasImportedEvents = true;
}
},
/**
* Set the point's state
* @param {String} state
*/
setState: function (state, move) {
var point = this,
plotX = point.plotX,
plotY = point.plotY,
series = point.series,
stateOptions = series.options.states,
markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
normalDisabled = markerOptions && !markerOptions.enabled,
markerStateOptions = markerOptions && markerOptions.states[state],
stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
stateMarkerGraphic = series.stateMarkerGraphic,
pointMarker = point.marker || {},
chart = series.chart,
radius,
halo = series.halo,
haloOptions,
newSymbol,
pointAttr;
state = state || NORMAL_STATE; // empty string
pointAttr = point.pointAttr[state] || series.pointAttr[state];
if (
// already has this state
(state === point.state && !move) ||
// selected points don't respond to hover
(point.selected && state !== SELECT_STATE) ||
// series' state options is disabled
(stateOptions[state] && stateOptions[state].enabled === false) ||
// general point marker's state options is disabled
(state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) ||
// individual point marker's state options is disabled
(state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
) {
return;
}
// apply hover styles to the existing point
if (point.graphic) {
radius = markerOptions && point.graphic.symbolName && pointAttr.r;
point.graphic.attr(merge(
pointAttr,
radius ? { // new symbol attributes (#507, #612)
x: plotX - radius,
y: plotY - radius,
width: 2 * radius,
height: 2 * radius
} : {}
));
// Zooming in from a range with no markers to a range with markers
if (stateMarkerGraphic) {
stateMarkerGraphic.hide();
}
} else {
// if a graphic is not applied to each point in the normal state, create a shared
// graphic for the hover state
if (state && markerStateOptions) {
radius = markerStateOptions.radius;
newSymbol = pointMarker.symbol || series.symbol;
// If the point has another symbol than the previous one, throw away the
// state marker graphic and force a new one (#1459)
if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
stateMarkerGraphic = stateMarkerGraphic.destroy();
}
// Add a new state marker graphic
if (!stateMarkerGraphic) {
if (newSymbol) {
series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
newSymbol,
plotX - radius,
plotY - radius,
2 * radius,
2 * radius
)
.attr(pointAttr)
.add(series.markerGroup);
stateMarkerGraphic.currentSymbol = newSymbol;
}
// Move the existing graphic
} else {
stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
x: plotX - radius,
y: plotY - radius
});
}
}
if (stateMarkerGraphic) {
stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
stateMarkerGraphic.element.point = point; // #4310
}
}
// Show me your halo
haloOptions = stateOptions[state] && stateOptions[state].halo;
if (haloOptions && haloOptions.size) {
if (!halo) {
series.halo = halo = chart.renderer.path()
.add(chart.seriesGroup);
}
halo.attr(extend({
fill: Color(point.color || series.color).setOpacity(haloOptions.opacity).get()
}, haloOptions.attributes))[move ? 'animate' : 'attr']({
d: point.haloPath(haloOptions.size)
});
} else if (halo) {
halo.attr({ d: [] });
}
point.state = state;
},
haloPath: function (size) {
var series = this.series,
chart = series.chart,
plotBox = series.getPlotBox(),
inverted = chart.inverted;
return chart.renderer.symbols.circle(
plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : this.plotX) - size,
plotBox.translateY + (inverted ? series.xAxis.len - this.plotX : this.plotY) - size,
size * 2,
size * 2
);
}
});
/*
* Extend the Series object with interaction
*/
extend(Series.prototype, {
/**
* Series mouse over handler
*/
onMouseOver: function () {
var series = this,
chart = series.chart,
hoverSeries = chart.hoverSeries;
// set normal state to previous series
if (hoverSeries && hoverSeries !== series) {
hoverSeries.onMouseOut();
}
// trigger the event, but to save processing time,
// only if defined
if (series.options.events.mouseOver) {
fireEvent(series, 'mouseOver');
}
// hover this
series.setState(HOVER_STATE);
chart.hoverSeries = series;
},
/**
* Series mouse out handler
*/
onMouseOut: function () {
// trigger the event only if listeners exist
var series = this,
options = series.options,
chart = series.chart,
tooltip = chart.tooltip,
hoverPoint = chart.hoverPoint;
chart.hoverSeries = null; // #182, set to null before the mouseOut event fires
// trigger mouse out on the point, which must be in this series
if (hoverPoint) {
hoverPoint.onMouseOut();
}
// fire the mouse out event
if (series && options.events.mouseOut) {
fireEvent(series, 'mouseOut');
}
// hide the tooltip
if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
tooltip.hide();
}
// set normal state
series.setState();
},
/**
* Set the state of the graph
*/
setState: function (state) {
var series = this,
options = series.options,
graph = series.graph,
stateOptions = options.states,
lineWidth = options.lineWidth,
attribs,
i = 0;
state = state || NORMAL_STATE;
if (series.state !== state) {
series.state = state;
if (stateOptions[state] && stateOptions[state].enabled === false) {
return;
}
if (state) {
lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035
}
if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
attribs = {
'stroke-width': lineWidth
};
// use attr because animate will cause any other animation on the graph to stop
graph.attr(attribs);
while (series['zoneGraph' + i]) {
series['zoneGraph' + i].attr(attribs);
i = i + 1;
}
}
}
},
/**
* Set the visibility of the graph
*
* @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
* the visibility is toggled.
*/
setVisible: function (vis, redraw) {
var series = this,
chart = series.chart,
legendItem = series.legendItem,
showOrHide,
ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
oldVisibility = series.visible;
// if called without an argument, toggle visibility
series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis;
showOrHide = vis ? 'show' : 'hide';
// show or hide elements
each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) {
if (series[key]) {
series[key][showOrHide]();
}
});
// hide tooltip (#1361)
if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) {
series.onMouseOut();
}
if (legendItem) {
chart.legend.colorizeItem(series, vis);
}
// rescale or adapt to resized chart
series.isDirty = true;
// in a stack, all other series are affected
if (series.options.stacking) {
each(chart.series, function (otherSeries) {
if (otherSeries.options.stacking && otherSeries.visible) {
otherSeries.isDirty = true;
}
});
}
// show or hide linked series
each(series.linkedSeries, function (otherSeries) {
otherSeries.setVisible(vis, false);
});
if (ignoreHiddenSeries) {
chart.isDirtyBox = true;
}
if (redraw !== false) {
chart.redraw();
}
fireEvent(series, showOrHide);
},
/**
* Show the graph
*/
show: function () {
this.setVisible(true);
},
/**
* Hide the graph
*/
hide: function () {
this.setVisible(false);
},
/**
* Set the selected state of the graph
*
* @param selected {Boolean} True to select the series, false to unselect. If
* UNDEFINED, the selection state is toggled.
*/
select: function (selected) {
var series = this;
// if called without an argument, toggle
series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
if (series.checkbox) {
series.checkbox.checked = selected;
}
fireEvent(series, selected ? 'select' : 'unselect');
},
drawTracker: TrackerMixin.drawTrackerGraph
});
// Patch to catch up this with v 4.2. highcharts-more looking for
// grep, returning error.
Highcharts4.grep = function (elements, callback) {
return [].filter.call(elements, callback);
};
// global variables
extend(Highcharts4, {
// Constructors
Color: Color,
Point: Point,
Tick: Tick,
Renderer: Renderer,
SVGElement: SVGElement,
SVGRenderer: SVGRenderer,
// Various
arrayMin: arrayMin,
arrayMax: arrayMax,
charts: charts,
dateFormat: dateFormat,
error: error,
format: format,
pathAnim: pathAnim,
getOptions: getOptions,
hasBidiBug: hasBidiBug,
isTouchDevice: isTouchDevice,
setOptions: setOptions,
addEvent: addEvent,
removeEvent: removeEvent,
createElement: createElement,
discardElement: discardElement,
css: css,
each: each,
map: map,
merge: merge,
splat: splat,
extendClass: extendClass,
pInt: pInt,
svg: hasSVG,
canvas: useCanVG,
vml: !hasSVG && !useCanVG,
product: PRODUCT,
version: VERSION
});
}());
;;
// ==ClosureCompiler==
// @compilation_level SIMPLE_OPTIMIZATIONS
/**
* @license Highcharts4 JS v4.2.0 (2105-12-15)
*
* (c) 2009-2014 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/**
* adds additional chart types
*/
(function (factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory;
} else {
factory(Highcharts4);
}
}(function (Highcharts4) {
var arrayMin = Highcharts4.arrayMin,
arrayMax = Highcharts4.arrayMax,
each = Highcharts4.each,
extend = Highcharts4.extend,
merge = Highcharts4.merge,
map = Highcharts4.map,
pick = Highcharts4.pick,
pInt = Highcharts4.pInt,
defaultPlotOptions = Highcharts4.getOptions().plotOptions,
seriesTypes = Highcharts4.seriesTypes,
extendClass = Highcharts4.extendClass,
splat = Highcharts4.splat,
wrap = Highcharts4.wrap,
Axis = Highcharts4.Axis,
Tick = Highcharts4.Tick,
Point = Highcharts4.Point,
Pointer = Highcharts4.Pointer,
CenteredSeriesMixin = Highcharts4.CenteredSeriesMixin,
TrackerMixin = Highcharts4.TrackerMixin,
Series = Highcharts4.Series,
math = Math,
mathRound = math.round,
mathFloor = math.floor,
mathMax = math.max,
Color = Highcharts4.Color,
noop = function () {},
UNDEFINED;/**
* 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 Highcharts4 and Highstock.
*/
function Pane(options, chart, firstAxis) {
this.init(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. Angular charts have a default background (#3318)
pane.options = options = merge(defaultOptions, chart.angular ? { background: {} } : undefined, 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)
axisUserOptions = firstAxis.userOptions;
config = merge(pane.defaultBackgroundOptions, config);
if (backgroundColor) {
config.backgroundColor = backgroundColor;
}
config.color = config.backgroundColor; // due to naming in plotBands
firstAxis.options.plotBands.unshift(config);
axisUserOptions.plotBands = axisUserOptions.plotBands || []; // #3176
if (axisUserOptions.plotBands !== firstAxis.options.plotBands) {
axisUserOptions.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.MAX_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
*/
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,
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,
showLastLabel: false,
tickLength: 0
},
// Radial axis, like a spoke in a polar chart
defaultRadialYOptions: {
gridLineInterpolation: 'circle',
labels: {
align: 'right',
x: -3,
y: -2
},
showLastLabel: false,
title: {
x: 4,
text: null,
rotation: 90
}
},
/**
* Merge and set options
*/
setOptions: function (userOptions) {
var options = this.options = merge(
this.defaultOptions,
this.defaultRadialOptions,
userOptions
);
// Make sure the plotBands array is instanciated for each Axis (#2649)
if (!options.plotBands) {
options.plotBands = [];
}
},
/**
* 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;
// Set the center array
this.center = this.pane.center = CenteredSeriesMixin.getCenter.call(this.pane);
},
/**
* 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;
} else {
// This is a workaround for regression #2593, but categories still don't position correctly.
this.minPixelPadding = 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 = Highcharts4.CenteredSeriesMixin.getCenter.call(this.pane);
// The sector is used in Axis.translate to compute the translation of reversed axis points (#2570)
if (this.isCircular) {
this.sector = this.endAngleRad - this.startAngleRad;
}
// Axis len is used to lay out the ticks
this.len = this.width = this.height = this.center[2] * pick(this.sector, 1) / 2;
}
},
/**
* Returns the x, y coordinate of a point given by a value and a pixel distance
* from center
*/
getPosition: function (value, length) {
return this.postTranslate(
this.isCircular ? this.translate(value) : 0, // #2848
pick(this.isCircular ? length : this.translate(value), 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 {
// Keep within bounds
from = Math.max(from, this.min);
to = Math.min(to, this.max);
// 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: Math.min(start, end), // Math is for reversed yAxis (#3606)
end: Math.max(start, 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 {
// Find the X axis in the same pane
each(chart.xAxis, function (a) {
if (a.pane === axis.pane) {
xAxis = a;
}
});
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)
};
}
};
/**
* 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,
centerSlot = 20, // 20 degrees to each side at the top and bottom
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 = axis.chart.renderer.fontMetrics(label.styles.fontSize).b - label.getBBox().height / 2;
}
// Automatic alignment
if (align === null) {
if (axis.isCircular) {
if (this.label.getBBox().width > axis.len * axis.tickInterval / (axis.max - axis.min)) { // #3506
centerSlot = 0;
}
if (angle > centerSlot && angle < 180 - centerSlot) {
align = 'left'; // right hemisphere
} else if (angle > 180 + centerSlot && angle < 360 - centerSlot) {
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: '\u25CF {series.name}: {point.low} - {point.high}
'
},
trackByArea: true,
dataLabels: {
align: null,
verticalAlign: null,
xLow: 0,
xHigh: 0,
yLow: 0,
yHigh: 0
},
states: {
hover: {
halo: false
}
}
});
/**
* Add the series type
*/
seriesTypes.arearange = extendClass(seriesTypes.area, {
type: 'arearange',
pointArrayMap: ['low', 'high'],
dataLabelCollections: ['dataLabel', 'dataLabelUpper'],
toYData: function (point) {
return [point.low, point.high];
},
pointValKey: 'low',
deferTranslatePolar: true,
/**
* Translate a point's plotHigh from the internal angle and radius measures to
* true plotHigh coordinates. This is an addition of the toXY method found in
* Polar.js, because it runs too early for arearanges to be considered (#3419).
*/
highToXY: function (point) {
// Find the polar plotX and plotY
var chart = this.chart,
xy = this.xAxis.postTranslate(point.rectPlotX, this.yAxis.len - point.plotHigh);
point.plotHighX = xy.x - chart.plotLeft;
point.plotHigh = xy.y - chart.plotTop;
},
/**
* 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);
}
});
// Postprocess plotHigh
if (this.chart.polar) {
each(this.points, function (point) {
series.highToXY(point);
});
}
},
/**
* 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 = Highcharts4.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.plotHighX || point.plotX, // plotHighX is for polar charts
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'
if (!this.chart.polar) {
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,
align = dataLabelOptions.align,
verticalAlign = dataLabelOptions.verticalAlign,
inside = dataLabelOptions.inside,
point,
up,
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];
if (point) {
up = inside ? point.plotHigh < point.plotLow : point.plotHigh > point.plotLow;
// Set preliminary values
point.y = point.high;
point._plotY = point.plotY;
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 = up;
if (inverted) {
if (!align) {
dataLabelOptions.align = up ? 'right' : 'left';
}
} else {
if (!verticalAlign) {
dataLabelOptions.verticalAlign = up ? 'top' : 'bottom';
}
}
dataLabelOptions.x = dataLabelOptions.xHigh;
dataLabelOptions.y = dataLabelOptions.yHigh;
}
}
if (seriesProto.drawDataLabels) {
seriesProto.drawDataLabels.apply(this, arguments); // #1209
}
// Step 2: reorganize and handle data labels for the lower values
i = length;
while (i--) {
point = data[i];
if (point) {
up = inside ? point.plotHigh < point.plotLow : point.plotHigh > point.plotLow;
// 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._plotY;
// Set the default offset
point.below = !up;
if (inverted) {
if (!align) {
dataLabelOptions.align = up ? 'left' : 'right';
}
} else {
if (!verticalAlign) {
dataLabelOptions.verticalAlign = up ? 'bottom' : 'top';
}
}
dataLabelOptions.x = dataLabelOptions.xLow;
dataLabelOptions.y = dataLabelOptions.yLow;
}
}
if (seriesProto.drawDataLabels) {
seriesProto.drawDataLabels.apply(this, arguments);
}
}
dataLabelOptions.align = align;
dataLabelOptions.verticalAlign = verticalAlign;
},
alignDataLabel: function () {
seriesTypes.column.prototype.alignDataLabel.apply(this, arguments);
},
setStackedPoints: noop,
getSymbol: noop,
drawPoints: noop
});
/**
* The AreaSplineRangeSeries class
*/
defaultPlotOptions.areasplinerange = merge(defaultPlotOptions.arearange);
/**
* AreaSplineRangeSeries object
*/
seriesTypes.areasplinerange = extendClass(seriesTypes.arearange, {
type: 'areasplinerange',
getPointSpline: seriesTypes.spline.prototype.getPointSpline
});
(function () {
var colProto = seriesTypes.column.prototype;
/**
* 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,
xAxis = series.xAxis,
chart = series.chart,
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;
// Adjust for minPointLength
if (Math.abs(height) < minPointLength) {
heightDifference = (minPointLength - height);
height += heightDifference;
y -= heightDifference / 2;
// Adjust for negative ranges or reversed Y axis (#1457)
} else if (height < 0) {
height *= -1;
y -= height;
}
shapeArgs.height = height;
shapeArgs.y = y;
point.tooltipPos = chart.inverted ?
[
yAxis.len + yAxis.pos - chart.plotLeft - y - height / 2,
xAxis.len + xAxis.pos - chart.plotTop - shapeArgs.x - shapeArgs.width / 2,
height
] : [
xAxis.left - chart.plotLeft + shapeArgs.x + shapeArgs.width / 2,
yAxis.pos - chart.plotTop + y + height / 2,
height
]; // don't inherit from column tooltip position - #3372
});
},
directTouch: true,
trackerGroups: ['group', 'dataLabelsGroup'],
drawGraph: noop,
crispCol: colProto.crispCol,
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,
defer: false,
y: 15,
borderWidth: 1,
borderColor: 'silver',
borderRadius: 3,
crop: false,
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 = extendClass(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,
forceDL: true,
trackerGroups: ['group', 'dataLabelsGroup'],
/**
* 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,
overshoot = options.overshoot,
rotation = yAxis.startAngleRad + yAxis.translate(point.y, null, null, null, true);
// Handle the wrap and overshoot options
if (overshoot && typeof overshoot === 'number') {
overshoot = overshoot / 180 * Math.PI;
rotation = Math.max(yAxis.startAngleRad - overshoot, Math.min(yAxis.endAngleRad + overshoot, rotation));
} else 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
zIndex: 1
})
.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',
zIndex: 2
})
.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
);
Series.prototype.render.call(this);
this.group.clip(this.chart.clipRect);
},
/**
* Extend the basic setData method by running processData and generatePoints immediately,
* in order to access the points from the legend.
*/
setData: function (data, redraw) {
Series.prototype.setData.call(this, data, false);
this.processData();
this.generatePoints();
if (pick(redraw, true)) {
this.chart.redraw();
}
},
/**
* If the tracking module is loaded, add the point tracker
*/
drawTracker: TrackerMixin && TrackerMixin.drawTrackerPoint
};
seriesTypes.gauge = 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: '\u25CF {series.name}
' + // docs
'Maximum: {point.high}
' +
'Upper quartile: {point.q3}
' +
'Median: {point.median}
' +
'Lower quartile: {point.q1}
' +
'Minimum: {point.low}
'
},
//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
pointWiskerLength,
whiskerLength = series.options.whiskerLength;
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
];
// 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;
pointWiskerLength = (/%$/).test(whiskerLength) ? halfWidth * parseFloat(whiskerLength) / 100 : whiskerLength / 2;
whiskersPath = [
// High whisker
'M',
crispX - pointWiskerLength,
highPlot,
'L',
crispX + pointWiskerLength,
highPlot,
// Low whisker
'M',
crispX - pointWiskerLength,
lowPlot,
'L',
crispX + pointWiskerLength,
lowPlot
];
}
// The median
crispCorr = (medianAttr['stroke-width'] % 2) / 2;
medianPlot = mathRound(point.medianPlot) + crispCorr;
medianPath = [
'M',
left,
medianPlot,
'L',
right,
medianPlot
];
// 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);
}
}
});
},
setStackedPoints: noop // #3890
});
/* ****************************************************************************
* 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: '\u25CF {series.name}: {point.low} - {point.high}
' // docs
},
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,
drawDataLabels: seriesTypes.arearange ? seriesTypes.arearange.prototype.drawDataLabels : noop,
/**
* 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',
dataLabels: {
inside: true
},
states: {
hover: {
lineWidthPlus: 0 // #3126
}
}
});
// 2 - Create the series object
seriesTypes.waterfall = extendClass(seriesTypes.column, {
type: 'waterfall',
upColorProp: 'fill',
pointValKey: 'y',
/**
* Translate data points from raw values
*/
translate: function () {
var series = this,
options = series.options,
yAxis = series.yAxis,
len,
i,
points,
point,
shapeArgs,
stack,
y,
yValue,
previousY,
previousIntermediate,
range,
threshold = options.threshold,
stacking = options.stacking,
tooltipY;
// run column series translate
seriesTypes.column.prototype.translate.apply(this);
previousY = previousIntermediate = threshold;
points = series.points;
for (i = 0, len = points.length; i < len; i++) {
// cache current point object
point = points[i];
yValue = this.processedYData[i];
shapeArgs = point.shapeArgs;
// get current stack
stack = stacking && yAxis.stacks[(series.negStacks && yValue < threshold ? '-' : '') + series.stackKey];
range = stack ?
stack[point.x].points[series.index + ',' + i] :
[0, yValue];
// override point value for sums
// #3710 Update point does not propagate to sum
if (point.isSum) {
point.y = yValue;
} else if (point.isIntermediateSum) {
point.y = yValue - previousIntermediate; // #3840
}
// up points
y = mathMax(previousY, previousY + point.y) + range[0];
shapeArgs.y = yAxis.translate(y, 0, 1);
// sum points
if (point.isSum) {
shapeArgs.y = yAxis.translate(range[1], 0, 1);
shapeArgs.height = Math.min(yAxis.translate(range[0], 0, 1), yAxis.len) - shapeArgs.y; // #4256
} else if (point.isIntermediateSum) {
shapeArgs.y = yAxis.translate(range[1], 0, 1);
shapeArgs.height = Math.min(yAxis.translate(previousIntermediate, 0, 1), yAxis.len) - shapeArgs.y;
previousIntermediate = range[1];
// If it's not the sum point, update previous stack end position and get
// shape height (#3886)
} else {
if (previousY !== 0) { // Not the first point
shapeArgs.height = yValue > 0 ?
yAxis.translate(previousY, 0, 1) - shapeArgs.y :
yAxis.translate(previousY, 0, 1) - yAxis.translate(previousY - yValue, 0, 1);
}
previousY += yValue;
}
// #3952 Negative sum or intermediate sum not rendered correctly
if (shapeArgs.height < 0) {
shapeArgs.y += shapeArgs.height;
shapeArgs.height *= -1;
}
point.plotY = shapeArgs.y = mathRound(shapeArgs.y) - (series.borderWidth % 2) / 2;
shapeArgs.height = mathMax(mathRound(shapeArgs.height), 0.001); // #3151
point.yBottom = shapeArgs.y + shapeArgs.height;
// Correct tooltip placement (#3014)
tooltipY = point.plotY + (point.negative ? shapeArgs.height : 0);
if (series.chart.inverted) {
point.tooltipPos[0] = yAxis.len - tooltipY;
} else {
point.tooltipPos[1] = tooltipY;
}
}
},
/**
* 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.options.data, // #3710 Update point does not propagate to sum
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;
} 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 (pt.x === 0 ? null : 'sum'); //#3245 Error when first element is Sum or Intermediate Sum
}
if (pt.isIntermediateSum) {
return (pt.x === 0 ? null : 'intermediateSum'); //#3245
}
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 = Highcharts4.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.options.color) {
// Up color
if (point.y > 0) {
point.pointAttr = seriesDownPointAttr;
point.color = upColor;
// Down color (#3710, update to negative)
} else {
point.pointAttr = series.pointAttr;
}
}
});
},
/**
* Draw columns' connector lines
*/
getGraphPath: function () {
var data = this.data,
length = data.length,
lineWidth = this.options.lineWidth + this.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,
drawGraph: Series.prototype.drawGraph
});
/* ****************************************************************************
* End Waterfall series code *
*****************************************************************************/
/**
* Set the default options for polygon
*/
defaultPlotOptions.polygon = merge(defaultPlotOptions.scatter, {
marker: {
enabled: false
}
});
/**
* The polygon series class
*/
seriesTypes.polygon = extendClass(seriesTypes.scatter, {
type: 'polygon',
fillGraph: true,
// Close all segments
getSegmentPath: function (segment) {
return Series.prototype.getSegmentPath.call(this, segment).concat('z');
},
drawGraph: Series.prototype.drawGraph,
drawLegendSymbol: Highcharts4.LegendSymbolMixin.drawRectangle
});
/* ****************************************************************************
* Start Bubble series code *
*****************************************************************************/
// 1 - set default options
defaultPlotOptions.bubble = merge(defaultPlotOptions.scatter, {
dataLabels: {
formatter: function () { // #2945
return this.point.z;
},
inside: true,
verticalAlign: 'middle'
},
// displayNegative: true,
marker: {
// fillOpacity: 0.5,
lineColor: null, // inherit from series.color
lineWidth: 1
},
minSize: 8,
maxSize: '20%',
// negativeColor: null,
// sizeBy: 'area'
softThreshold: false,
states: {
hover: {
halo: {
size: 5
}
}
},
tooltip: {
pointFormat: '({point.x}, {point.y}), Size: {point.z}'
},
turboThreshold: 0,
zThreshold: 0,
zoneAxis: 'z'
});
var BubblePoint = extendClass(Point, {
haloPath: function () {
return Point.prototype.haloPath.call(this, this.shapeArgs.r + this.series.options.states.hover.halo.size);
},
ttBelow: false
});
// 2 - Create the series object
seriesTypes.bubble = extendClass(seriesTypes.scatter, {
type: 'bubble',
pointClass: BubblePoint,
pointArrayMap: ['y', 'z'],
parallelArrays: ['x', 'y', 'z'],
trackerGroups: ['group', 'dataLabelsGroup'],
bubblePadding: true,
zoneAxis: 'z',
/**
* 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 = 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 = [],
options = this.options,
sizeByArea = options.sizeBy !== 'width',
zThreshold = options.zThreshold,
zRange = zMax - zMin,
value,
radius;
// Set the shape type and arguments to be picked up in drawPoints
for (i = 0, len = zData.length; i < len; i++) {
value = zData[i];
// When sizing by threshold, the absolute value of z determines the size
// of the bubble.
if (options.sizeByAbsoluteValue && value !== null) {
value = Math.abs(value - zThreshold);
zMax = Math.max(zMax - zThreshold, Math.abs(zMin - zThreshold));
zMin = 0;
}
if (value === null) {
radius = null;
// Issue #4419 - if value is less than zMin, push a radius that's always smaller than the minimum size
} else if (value < zMin) {
radius = minSize / 2 - 1;
} else {
// Relative size, a number between 0 and 1
pos = zRange > 0 ? (value - zMin) / zRange : 0.5;
if (sizeByArea && pos >= 0) {
pos = Math.sqrt(pos);
}
radius = math.ceil(minSize + pos * (maxSize - minSize)) / 2;
}
radii.push(radius);
}
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
if (typeof radius === 'number' && 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 or z = null
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 renderer = this.chart.renderer,
radius = renderer.fontMetrics(legend.itemStyle.fontSize).f / 2;
item.legendSymbol = 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,
buildKDTree: noop,
applyZones: noop
});
/**
* 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
each(this.series, function (series) {
var seriesOptions = series.options,
zData;
if (series.bubblePadding && (series.visible || !chart.options.chart.ignoreHiddenSeries)) {
// 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;
series.maxPxSize = extremes.maxSize;
// Find the min and max Z
zData = series.zData;
if (zData.length) { // #1735
zMin = pick(seriesOptions.zMin, math.min(
zMin,
math.max(
arrayMin(zData),
seriesOptions.displayNegative === false ? seriesOptions.zThreshold : -Number.MAX_VALUE
)
));
zMax = pick(seriesOptions.zMax, math.max(zMax, arrayMax(zData)));
}
}
}
});
each(activeSeries, function (series) {
var data = series[dataKey],
i = data.length,
radius;
if (isXAxis) {
series.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize);
}
if (range > 0) {
while (i--) {
if (typeof data[i] === 'number') {
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 && !this.isLog) {
pxMax -= axisLength;
transA *= (axisLength + pxMin - pxMax) / axisLength;
each([['min', 'userMin', pxMin], ['max', 'userMax', pxMax]], function (keys) {
if (pick(axis.options[keys[0]], axis[keys[1]]) === UNDEFINED) {
axis[keys[0]] += keys[2] / transA;
}
});
}
};
/* ****************************************************************************
* End Bubble series code *
*****************************************************************************/
(function () {
/**
* Extensions for polar charts. Additionally, much of the geometry required for polar charts is
* gathered in RadialAxes.js.
*
*/
var seriesProto = Series.prototype,
pointerProto = Pointer.prototype,
colProto;
/**
* Search a k-d tree by the point angle, used for shared tooltips in polar charts
*/
seriesProto.searchPointByAngle = function (e) {
var series = this,
chart = series.chart,
xAxis = series.xAxis,
center = xAxis.pane.center,
plotX = e.chartX - center[0] - chart.plotLeft,
plotY = e.chartY - center[1] - chart.plotTop;
return this.searchKDTree({
clientX: 180 + (Math.atan2(plotX, plotY) * (-180 / Math.PI))
});
};
/**
* Wrap the buildKDTree function so that it searches by angle (clientX) in case of shared tooltip,
* and by two dimensional distance in case of non-shared.
*/
wrap(seriesProto, 'buildKDTree', function (proceed) {
if (this.chart.polar) {
if (this.kdByAngle) {
this.searchPoint = this.searchPointByAngle;
} else {
this.kdDimensions = 2;
}
}
proceed.apply(this);
});
/**
* 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,
clientX;
// Save rectangular plotX, plotY for later computation
point.rectPlotX = plotX;
point.rectPlotY = plotY;
// 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;
// If shared tooltip, record the angle in degrees in order to align X points. Otherwise,
// use a standard k-d tree to get the nearest point in two dimensions.
if (this.kdByAngle) {
clientX = ((plotX / Math.PI * 180) + this.xAxis.pane.options.startAngle) % 360;
if (clientX < 0) { // #2665
clientX += 360;
}
point.clientX = clientX;
} else {
point.clientX = point.plotX;
}
};
/**
* 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;
}
}
if (seriesTypes.area) {
wrap(seriesTypes.area.prototype, 'init', initArea);
}
if (seriesTypes.areaspline) {
wrap(seriesTypes.areaspline.prototype, 'init', initArea);
}
if (seriesTypes.spline) {
/**
* 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) {
var chart = this.chart,
points,
i;
// Run uber method
proceed.call(this);
// Postprocess plot coordinates
if (chart.polar) {
this.kdByAngle = chart.tooltip && chart.tooltip.shared;
if (!this.preventPostTranslate) {
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 regular series
wrap(seriesProto, 'animate', polarAnimate);
if (seriesTypes.column) {
colProto = seriesTypes.column.prototype;
/**
* Define the animate method for columnseries
*/
wrap(colProto, 'animate', polarAnimate);
/**
* 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)
}
)
};
// Provide correct plotX, plotY for tooltip
this.toXY(point);
point.tooltipPos = [point.plotX, point.plotY];
point.ttBelow = point.plotY > center[1];
}
}
});
/**
* 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 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;
});
}());
}));
;;
// lib/jquery-1.10.2.min.js
// @include lib/underscore.js
// @include lib/jquery-ui-1.10.3.min.js
// @include infrastructure/period.js
/*
* Advanced Options for Precise Driver.
*
* Note that the options that appear in the popup are not necessarily in order from A to E,
* it's currently A, B, E, C, D.
*
* original impl: Henry Chan
*/
tf = window.tf || {};
tf.preciseDriverAdvancedOptionsText = {
optionA: "Direct Value",
optionATooltip: "",
optionB: "% Growth Rate",
optionBTooltip: "Period over period growth rate %",
optionC: "Cum. Increment",
optionCTooltip: "Cumulative increment over prior period in the same units as the forecast chart",
optionD: "Multiplier",
optionDTooltip: "Multiplier to the current forecast value(s)",
optionE: "Increment",
optionETooltip: "Modifies each point by the same increment",
selectBoxCurPeriod: "curPeriod",
selectBoxAll: "all",
selectBoxCurPeriodAndBeyond: "curPeriodAndBeyond"
};
function PreciseDriverAdvancedOptions(draggableChart, dataSeries, chartSeries, editIndex, editPeriod, editUnit, editInitialVal, window) {
var self = this;
var dataPoints = dataSeries.getDataPoints();
this.disableAllOptions = function($el) {
$el.find('.js-preciseDriverAdvancedOptionRadioDiv').each(function () {
$(this).addClass("inactive");
$(this).removeClass("active");
$(this).find("input").prop("disabled", true);
$(this).find("select").prop("disabled", true);
});
$el.find(".js-preciseDriverAdvancedOptionRadio").prop('checked', false);
// enable the radio buttons so you can select other options
$el.find(".js-preciseDriverAdvancedOptionRadio").prop("disabled", false);
}
this.enableOption = function(jQueryElement) {
jQueryElement.addClass("active");
jQueryElement.removeClass("inactive");
jQueryElement.find("input").prop("disabled", false);
jQueryElement.find("select").prop("disabled", false);
jQueryElement.find(".js-preciseDriverAdvancedOptionRadio").prop("checked", true);
}
this.show = function() {
var tpl = _.template($("#preciseDriverAdvancedOptionsTemplate").html());
var templateOptions = {};
jQuery.extend(true, templateOptions, tf.preciseDriverAdvancedOptionsText);
var formattedPeriod = tf.period.format(editPeriod);
templateOptions.period = formattedPeriod;
// fb4105 growthModeEnabled on chart
templateOptions.growthModeEnabled = dataSeries.isGrowthModeEnabled();
$("body").append(tpl(templateOptions))
var $modal = $('#preciseDriverAdvancedOptions');
$modal.on('click','.js-apply',function(){
var options = {};
var selectedRadio = $('input.js-preciseDriverAdvancedOptionRadio:checked');
var activeRow = selectedRadio.closest('tr');
options.optionDescription = tf.preciseDriverAdvancedOptionsText[$(selectedRadio).val()];
options.selectPeriod = activeRow.find(".js-selectPeriod").val();
var number = activeRow.find(".js-inputField").val();
number = number.replace(/[^\d.eE\-+]/g, "");
if (number == "") {
// storing the elements original value on a data attribute
var defaultValue = getDataValue(activeRow);
number = defaultValue != '' ? defaultValue : 0;
}
self.changeUserInputValueWithAdvancedOptions(editIndex, number, options);
$modal.modal('hide')
})
$modal.on('click','.js-cancel',function(){
$modal.modal('hide')
})
// we rebuild the modal template when the user clicks advanced button
$modal.on('hidden.bs.modal', function (e) {
$modal.remove();
})
// select option listener - % Growth Rate
$("#preciseDriverAdvancedOptionsB select").change(function() {
if ($(this).val() == tf.preciseDriverAdvancedOptionsText.selectBoxCurPeriod) {
// if editIndex is not greater than 0, CAGR will be 0
var cagr = self.computeCAGR(dataPoints,editIndex,'current')
var displayCagr = self.formatCAGR(cagr);
setDataValue(displayCagr,$("#preciseDriverAdvancedOptionsB"))
} else if ($(this).val() == tf.preciseDriverAdvancedOptionsText.selectBoxAll) {
var cagr = self.computeCAGR(dataPoints, 0, 'all');
var displayCagr = self.formatCAGR(cagr);
setDataValue(displayCagr,$("#preciseDriverAdvancedOptionsB"))
} else if ($(this).val() == tf.preciseDriverAdvancedOptionsText.selectBoxCurPeriodAndBeyond) {
var cagr = self.computeCAGR(dataPoints, editIndex);
var displayCagr = self.formatCAGR(cagr);
setDataValue(displayCagr,$("#preciseDriverAdvancedOptionsB"))
}
});
this.disableAllOptions($modal);
this.enableOption($("#preciseDriverAdvancedOptionsA"));
setDataValue(editInitialVal,$("#preciseDriverAdvancedOptionsA"))
$modal.find("#preciseDriverAdvancedOptionsA .js-unit").html(editUnit);
$modal.find(".js-selectPeriod").each(function() {
$(this).append("");
$(this).append("");
$(this).append("");
});
$modal.find("#preciseDriverAdvancedOptionsB .js-unit").html("%");
$modal.find("#preciseDriverAdvancedOptionsC .js-unit").html(editUnit);
setDataValue(1,$("#preciseDriverAdvancedOptionsD"));
$modal.find("#preciseDriverAdvancedOptionsE .js-unit").html(editUnit);
$("#preciseDriverAdvancedOptionsB .js-selectPeriod").trigger("change");
// events
// simulates mutually exclusiveness
$modal.on("click", ".js-preciseDriverAdvancedOptionRadioDiv", function() {
var curSelectedDiv = $modal.find('.active');
if (curSelectedDiv && ($(curSelectedDiv).prop("id") == $(this).prop("id"))) {
return; // nothing to do
}
self.disableAllOptions($modal);
// enable selected
self.enableOption($(this));
});
// initialize and show the modal...
$modal.modal({
keyboard: false,
show:true
})
}
// Compound Annual Growth Rate (CAGR)
/*
EXPECTED BEHAVIOR>
The CAGR should be calculated by taking the historical period before the first projected
period as the denominator for the CAGR calculation i.e.
To calculate compound annual growth rate, divide the value of an investment at the end of the
period in question by its value at the beginning of that period, raise the result to the power of one divided by the period length, and subtract one from the subsequent result.
Read more: Compound Annual Growth Rate - CAGR http://www.investopedia.com/terms/c/cagr.asp#ixzz4cYIrJ1UK
Follow us: Investopedia on Facebook
Example 1:
2017: 50.00 (projected)
2018: 53.57 (projected)
2019: 57.39 (projected)2017-2019 CAGR: (57.39/50)^(1/2) -1
2018-2019 CAGR: (57.39/53.57)^(1/1) -1
Example 2 (it's the same as 1 even if 2017 not projected):
2017: 50.00
2018: 53.57 (projected)
2019: 57.39 (projected)
2017-2019 CAGR: (57.39/50)^(1/2) -1
2018-2019 CAGR: (57.39/53.57)^(1/1) -1
Example 3
2016: 40
2017: 50.00
2018: 53.57 (projected)
2019: 57.39 (projected)
2017-2019 CAGR: (57.39/50)^(1/2) -1
2018-2019 CAGR: (57.39/53.57)^(1/1) -1
Note: we don't make any CAGR starting with 2016 available because 2017 is not projected
--------------------
Example 4
2016: 40
2017: 50.00 (projected)
2018: 53.57 (projected)
2019: 57.39 (projected)
All Periods:
2016-2019 CAGR: (57.39/40)^(1/3) -1
Current Period:
2017 ONLY CAGR: (50.00/40)^(1/1) -1
2018 ONLY CAGR: (53.57/50)^(1/1) -1
2019 ONLY CAGR: (57.39/53.57)^(1/1) -1
Current Period & Beyond
2017-2019 CAGR: (57.39/50)^(1/2) -1
2018-2019 CAGR: (57.39/53.57)^(1/1) -1
2019-2019 CAGR: Same as above
*/
this.computeCAGR = function(dataPoints, beginIndex, periodOption) {
var projectedDataPoints = dataPoints.filter(function(dataPoint,index){
return !dataPoint.Historical
}),
endPoint = dataPoints[dataPoints.length-1],
numberOfPoints,
startPoint;
switch(periodOption) {
case "current":
return beginIndex > 0 ? (dataPoints[beginIndex].Value / dataPoints[beginIndex-1].Value) - 1 : 0
break;
case "all":
// when showing All periods, start with the last historical
var firstProjectedIndex = dataPoints.findIndex(function(dataPoint){
return !dataPoint.Historical;
})
startPoint = firstProjectedIndex > 0 ? dataPoints[firstProjectedIndex-1] : dataPoints[0]
break;
default:
// current & beyond
startPoint = dataPoints[beginIndex]
break
}
// sets the ^(1/[numberOfPoints]).
numberOfPoints = (projectedDataPoints.length - beginIndex) > 0 ? (projectedDataPoints.length - beginIndex) : 1;
if(!dataPoints){
return 0;
} else {
return Math.pow( (endPoint.Value/startPoint.Value), (1/numberOfPoints) ) -1;
}
}
this.formatCAGR = function(cagr) {
return (cagr * 100);
}
// FB2441
this.changeUserInputValueWithAdvancedOptions = function(selectedIndex, newValue, options) {
newValue = parseFloat(newValue);
// this will be a function that takes index, chartPoint as arguments, and applies the newValue to that point
// according to the type of change the user has selected
var calcFunc = null;
// optionA -- direct value entry
if (options.optionDescription == tf.preciseDriverAdvancedOptionsText.optionA) {
calcFunc = function(index, chartPoint) {
dataSeries.applyChange(chartPoint, newValue);
}
// optionB -- % growth rate
} else if (options.optionDescription == tf.preciseDriverAdvancedOptionsText.optionB) {
calcFunc = function(index, chartPoint) {
if(index > 0) {
dataSeries.applyChange(chartPoint, (chartSeries.data[index-1].dataPoint.Value * (1 + newValue/100)) * dataSeries.yaxis.getAxisScaleFactor());
}
}
// optionE -- increment
} else if (options.optionDescription == tf.preciseDriverAdvancedOptionsText.optionE) {
calcFunc = function(index, chartPoint) {
dataSeries.applyChange(chartPoint, (chartPoint.dataPoint.Value * dataSeries.yaxis.getAxisScaleFactor()) + newValue);
}
// optionC -- cumulative increment
} else if (options.optionDescription == tf.preciseDriverAdvancedOptionsText.optionC) {
var calcFunc = function(index, chartPoint) {
if(index > 0) {
dataSeries.applyChange(chartPoint, (chartSeries.data[index-1].dataPoint.Value * dataSeries.yaxis.getAxisScaleFactor()) + newValue);
}
}
// optionD -- multiplier
} else if (options.optionDescription == tf.preciseDriverAdvancedOptionsText.optionD) {
var calcFunc = function(index, chartPoint) {
dataSeries.applyChange(chartPoint, chartPoint.dataPoint.Value * newValue * dataSeries.yaxis.getAxisScaleFactor());
}
}
// now apply the calcFunc to all of the data points the user has selected
if (options.selectPeriod == tf.preciseDriverAdvancedOptionsText.selectBoxCurPeriod) {
calcFunc(selectedIndex, chartSeries.data[selectedIndex]);
} else if (options.selectPeriod == tf.preciseDriverAdvancedOptionsText.selectBoxAll) {
for(var i = 0; i < chartSeries.data.length; i++) {
if(chartSeries.data[i].dataPoint.Editable)
calcFunc(i, chartSeries.data[i]);
};
} else if (options.selectPeriod == tf.preciseDriverAdvancedOptionsText.selectBoxCurPeriodAndBeyond) {
for(var i = selectedIndex; i < chartSeries.data.length; i++) {
if(chartSeries.data[i].dataPoint.Editable)
calcFunc(i, chartSeries.data[i]);
};
}
draggableChart.triggerChartSeriesChanged(dataSeries, true)
}
function setDataValue(value, $el){
// toFixed(2) is a new requirement for display, awkward to convert to a number then back to string..
if (typeof value === 'string'){
value = Number(value);
}
dataValueEl($el).data('baseline',value).text(value.toFixed(2));
}
function getDataValue($el){
return dataValueEl($el).data('baseline');
}
function dataValueEl(el){
return $(el).find('.js-baseline');
}
}
;;
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
}
// https://tc39.github.io/ecma262/#sec-array.prototype.find
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
value: function(predicate) {
// 1. Let O be ? ToObject(this value).
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If IsCallable(predicate) is false, throw a TypeError exception.
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
var thisArg = arguments[1];
// 5. Let k be 0.
var k = 0;
// 6. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kValue be ? Get(O, Pk).
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
// d. If testResult is true, return kValue.
var kValue = o[k];
if (predicate.call(thisArg, kValue, k, o)) {
return kValue;
}
// e. Increase k by 1.
k++;
}
// 7. Return undefined.
return undefined;
}
});
}
;;
// lib/jquery-1.10.2.min.js
// @include lib/underscore.js
// @include common.js
/*
Dynamically generated pane for editing things that are usually edited by
something like dragging. Meant to be used in a pop-up.
*/
(function() {
var DirectEdit;
DirectEdit = (function() {
function DirectEdit(options) {
/* defaults */
this.options = {
/* REQUIRED */
/* Callback to receive the updated value */
save: function() {},
/* OPTIONAL */
/* The initial value to preload */
value: "",
/* Labels for the input field */
labelBefore: "",
labelAfter: "",
saveButtonLabel: "OK",
explanation: "",
template: "
";
}
// Text indicating what this is for, use series name for COMPETITION, COMPARABLE
// and model name for the benchmarks with driver name for TREFIS
var forText;
if(type == CHART_TYPE_COMPETITION || type == CHART_TYPE_COMPARABLE) {
forText = seriesTitle;
}
else if(type == CHART_TYPE_BENCHMARK) {
forText = datum.modelName;
}
else {
forText = driverName;
}
forText = TextUtil.truncate(forText, 45);
if (!gridViewMode) {
line += ((type == CHART_TYPE_YOURS && historical) ||
type == CHART_TYPE_TREFIS ||
type == CHART_TYPE_COMPETITION ||
type == CHART_TYPE_COMPARABLE
? defaultSeriesName + ' ' + (historical ? "estimate" : "forecast")
: seriesTitle + " forecast") +
" for " + tf.period.format(year) + " " + forText +
":
" +
"" + (value==null?"n/a":NumberFormatUtil.numToRoundedString(value, 3)) +
(unit.length
? " (" + unit + ")"
: "") +
"";
} else {
if (seriesTitle.length > 18){
seriesTitle = seriesTitle.substring(0, 18);
}
// if the user line hasn't been modified, don't include a line for it in the tooltip
// since it just overlaps the base line
if(datum !== editableDatum || isUserLineModified) {
var periodFormattedText;
if (tf.period.format(year) != 0) { // fixes FB3375
periodFormattedText = tf.period.format(year) + " ";
} else {
periodFormattedText = "";
}
line += "
" +
"
Advanced",
save: function(input) {
if (input !== editInitialVal) {
var number = input.replace(/[^\d.eE\-+]/g, "");
self.changeUserInputValue(editIndex, number);
}
directEdit.dialog().dialog("close");
}
}).generateElement().styledDialog({
tooltipFor: event,
temporary: true
});
// note: binding 'on' ("#advancedOptionsForPreciseDriver" + curEditableDatum.Identifier) won't always work as there is
// a race condition on this element not being create yet. Therefore, bind 'on' the parent
$(".explanation").on('click', "#advancedOptionsForPreciseDriver" + curEditableDatum.Identifier, function() {
new PreciseDriverAdvancedOptions(self, editableDatum, editIndex, editPeriod, editUnit, editInitialVal, window).show();
});
}
// re-enable text selection
setTextSelectionEnabled(1);
// for some reason, these are necessary to make mouse events work again
if ($.isFunction(activePoint.onMouseOver)){
activePoint.onMouseOver();
activePoint.series.onMouseOver();
}
}
}
self.updateLabelPositions = updateLabelPositions;
function updateLabelPositions() {
var label,
thirdToLastPoints = [],
seriesVisible = [],
closestDistance,
isClosestAbove,
thirdToLastPoint,
lastPoint,
chartXAxis = chart.xAxis[0],
categoriesLength = chartXAxis.categories.length,
numPointsBack = Math.max(Math.round(categoriesLength / 3), 1),
lastPointX = chartXAxis.translate(categoriesLength - 1),
thirdToLastPointX = chartXAxis.translate(categoriesLength - numPointsBack),
distance,
dataWithoutAxes,
left,
bottom;
$.each(myData, function(index, datum) {
if(!datum.series.yAxis) {
dataWithoutAxes = 1;
return false;
}
seriesVisible[index] = datum.visible;
thirdToLastPoints[index] = getPointCoords(datum, numPointsBack);
});
if(!dataWithoutAxes) {
$.each(myData, function(index, datum) {
label = datum.label;
if(label) {
closestDistance = Number.MAX_VALUE;
isClosestAbove = 0;
thirdToLastPoint = thirdToLastPoints[index];
$.each(thirdToLastPoints, function(j, p) {
if(index != j && seriesVisible[j] && (distance = Math.abs(p - thirdToLastPoint)) < closestDistance) {
closestDistance = distance;
isClosestAbove = p > thirdToLastPoint;
}
});
left = Math.min(thirdToLastPointX - 5, $("#" + divId).width() - label.width() - 50);
bottom = isClosestAbove
? thirdToLastPoint + 20 - label.height() + (datum == editableDatum ? -4 : 0)
: thirdToLastPoint + 35 + (datum == editableDatum ? 4 : 0);
// IE7 likes to throw errors here, so both check for NaNs and unfortunately that's not enough (not sure
// why) so need to wrap with a try/catch
if(!isNaN(left) && !isNaN(bottom)) {
try {
label.css({
left: left,
bottom: bottom
});
} catch(e) {}
}
}
});
}
}
function getPointCoords(datum, indexFromLast) {
return datum.series.yAxis.translate(datum.array[Math.max(0, datum.array.length - indexFromLast)].Value);
}
/**
* Changes the value of the editable data at a given point.
* If smoothing is on, will also change values at nearby points
* on the editable line.
*
* @param index the index into the editable data array (including
* historical values), indicating the point whose value should be
* changed
* @param newValue the value that the point should have, in local coords
*/
function changeValue(index, newValue, isFinal) {
var yAxis = editableDatum.series.yAxis;
var yearToEditablePoint = createYearToEditablePoint();
var y;
if(smoothing) {
var lastHistoricalIndex = -1;
// for autoscaling, use an exponential function -- tau is the time constant, delta
// is the prefactor
$.each(editableDatum.array, function(index, point) {
if(!point.Historical && !point.Fixed) {
lastHistoricalIndex = index - 1;
return false;
}
});
var originalShapeVal = originalShape[index];
var isOtherPointsToModifyDifferentToOriginalVal = false;
// are all points the same?
$.each(getPointsToModify(index, lastHistoricalIndex), function(key, val) {
var originalShapeValLoop = originalShape[val];
if (originalShapeValLoop != originalShapeVal) {
isOtherPointsToModifyDifferentToOriginalVal = true;
return false; // break
}
});
var delta;
// if all other points are the same, don't use a delta based on exponential
if (isOtherPointsToModifyDifferentToOriginalVal) {
delta = (newValue - yAxis.translate(originalShape[index])) / (1 - Math.exp(-2)),
tau = 2.0 / (index - lastHistoricalIndex);
} else {
delta = newValue - yAxis.translate(originalShape[index]);
}
$.each(getPointsToModify(index, lastHistoricalIndex), function(_, i) {
if(!editableDatum.array[i].Fixed){
var curveFunction;
// if all other points are the same, don't use a curveFunction
if (isOtherPointsToModifyDifferentToOriginalVal) {
curveFunction = (1.0 - Math.exp( - tau * (i - lastHistoricalIndex) ));
} else {
curveFunction = 1;
}
y = range( minAllowedValue * editableDatum.scaleFactor,
yAxis.translate(yAxis.translate(originalShape[i]) + delta * curveFunction, true),
maxAllowedValue * editableDatum.scaleFactor );
if (isIntegerValuesOnly && editableDatum.array.length == 1)
y = Math.round(y);
editableDatum.array[i].Value = y;
var period = editableDatum.array[i].Year;
if (yearToEditablePoint[period]) {
yearToEditablePoint[period].update(y, false);
}
}
});
} else {
y = range( minAllowedValue * editableDatum.scaleFactor,
yAxis.translate(newValue, true),
maxAllowedValue * editableDatum.scaleFactor );
if (isIntegerValuesOnly && editableDatum.array.length == 1)
y = Math.round(y);
var delta = y - editableDatum.array[index].Value;
//update the point data
editableDatum.array[index].Value += delta;
//update non-displayed points to the right
for (var i = index + 1;
i < editableDatum.array.length &&
!yearToEditablePoint[editableDatum.array[i].Year];
i++) {
editableDatum.array[i].Value += delta;
}
//update the visible marker
yearToEditablePoint[editableDatum.array[index].Year].update(y, false);
}
chart.redraw();
updateLabelPositions();
chartChangeDispatchTimer.stop();
if(isFinal)
self.chartChangedListener && self.chartChangedListener(true);
else
chartChangeDispatchTimer.reset(EVENT_DISPATCH_DELAY);
};
/**
* When the chart is in smoothing mode and the user drags a point, other points adjacent to it
* should move too. However if the user drags a non-zero point, we only want to drag other
* non-zero points that are contiguous with the point the user touched. Similarly if the user
* drags a zero point, we only want to drag the other zero points that are contiguous with the
* point the user touched.
*
* This function is a helper for changeValue that determines which other points should be modified
* when the user drags a point while in smoothing mode.
*
* @param {int} index -- index of the point the user is dragging
* @param {int} lastHistoricalIndex -- index of the last historical point in the modifiable line,
* or -1 if there are no historical points
* @return {Array of int} indices of points that should be dragged along with the selected point
* when the user is in smoothing mode
*/
function getPointsToModify(index, lastHistoricalIndex) {
var result = [];
var searchZeroes = originalShape[index] == 0;
var i = index;
// first, start from the user's point and go left to find the beginning of the contiguous
// sequence of zero or nonzero points
while(i > lastHistoricalIndex+1 && (originalShape[i-1] == 0) == searchZeroes) {
i--;
}
// then, go to the right to find the other end of the contiguous sequence
for( ; i < editableDatum.array.length; i++) {
if((originalShape[i] == 0) != searchZeroes)
break;
result.push(i);
}
return result;
}
self.stopDispatchTimer = function() {
chartChangeDispatchTimer.stop();
}
function createYearToEditablePoint(datum) {
if (datum == null) {
datum = editableDatum;
}
var result = {};
$.each(interpolateIfNeeded(datum.series.data), function(index, pointObj) {
result[chart.xAxis[0].categories[pointObj.x]] = pointObj;
});
return result;
}
self.saveOriginalShape = function() {
originalShape = $.map(editableDatum.array, function(point) {
return point.Value;
});
};
self.reflow = function() {
chart.reflow();
};
self.setAxisTitleFontSize = function(newSize) {
axisTitleFontSize = newSize;
};
self.setAxisLabelFontSize = function(newSize) {
axisLabelFontSize = newSize;
};
self.setPeriodSelectionManager = function(newPeriodSelectionManager) {
// notice that we keep the period manager so we can query it when populating the chart,
// but we do NOT add a listener for when the period changes, since that's handled by
// chartWrapper.js
periodSelectionManager = newPeriodSelectionManager;
};
function shadeColor(color, percent) {
var num = parseInt(color.slice(1),16), amt = Math.round(2.55 * percent), R = (num >> 16) + amt, G = (num >> 8 & 0x00FF) + amt, B = (num & 0x0000FF) + amt;
return "#" + (0x1000000 + (R<255?R<1?0:R:255)*0x10000 + (G<255?G<1?0:G:255)*0x100 + (B<255?B<1?0:B:255)).toString(16).slice(1);
}
}; // end DraggableChart function definition.
// if (window.Highcharts)
// window.Highcharts.setSymbolSize(imageUrl,[28,27]);
}
;;
// lib/jquery-1.10.2.min.js
// @include common.js
// @include lib/jquery-ui-1.10.3.min.js
// @include lib/highcharts4/highcharts.js
// @include lib/highcharts4/highcharts-more.js
// @include widgetSpecific/preciseDriverAdvancedOptions.js
// @include infrastructure/numberFormatUtil.js
// @include infrastructure/period.js
// @include infrastructure/unitUtil.js
// @include infrastructure/polyfills.js
// @include widgetSpecific/directEdit.js
// @include infrastructure/draggableChart.js
/**
* trefisChart is core chart widget that is fairly 'oblivious' to the concept of models
* instead its focuses on managing core chart components, series, yaxies, xaxies, etc...
*
* supported chart format:
*
*
*
* (same format as in model.companyData.md.driverData[], same structure indexed by benchmarks)
*
* the code is broken out into a number of components:
*
* $.widget( 'custom.chart') the chart widget it self.
* InnerChart - plain javascript class that serves as a domain/data object backing the chart
* Xaxis/Yaxis - plain javascript class that serves as a domain/data object backing the xaxis and yaxis objects in the chart
* Series - plain javascript class that serves as a domain/data object backing the series objects in the chart
* note : there is a not a separate class for points, they are stored and accessed from Series.data
* DragContext - an object that encapsulates dragging functionality, like listeners timers, etc...
*
* Alot of the code in the widget focuses on transporting and transforming data between the InnerChart/Xaxis/Yaxis/Series data layer
* and the Highcharts json format.
*
*
* How Data is added/manipulated between trefis/model charts:
* modelChart passes data through addSeries, which creates the series/axis classes etc
* then calls prepareSeries/Yaxis/Xaxis which add to HighCharts through
* highcharts addSeries api method
*
* modelChart calls rerender (changed to growthmode, period, chartType)
* rerender does some recalc, then calls updateSeries with an ID, which
* eventually uses
* highcharts update api method.
*
* modelChart calls updateSeries (points have been updated from draggin points on the chart)
* passes some data (and possibly new options). They get merged/set
* then prepare is called
*
* Dragging:
* when you drag, and stop, trefis chart makes a map of data by identifier, which
* is sent to modelChart (listener), recalced, then sent back down through modelchart,
* eventually making its way to updateSeries({data})then sent through the update
* process listed above.
*
*/
$(function() {
if($.widget) {
$.widget( 'custom.chart', {
//widget defaults
options:{
chart: {
//margin: [12, 75, 27, 18], //commented out to give secod yaxis (from benchmark search) more room
backgroundColor: "rgba(0,0,0,0)",
animation: false,
reflow: false,
alignTicks : false
},
plotBands: [],
plotOptions: {
line:{
states:{
hover:{
halo:{
attributes: {
opacity:0.25,
},
size:8,
}
}
}
},
series: {
shadow: false,
animation: false,
stickyTracking: false,
radius:4,
marker: {
symbol: 'circle',
states: {
hover: {
radius: 4
}
}
},
},
},
tooltip: {
position: 'top',
enabled: false,
templateId: '#chartpoint-tooltip-items-template',
dataFormatter: null
},
xAxis: {
categories: [],
labels: {
style: {
color: "#222",
fontSize: '12px',
lineHeight: '12px',
fontWeight: 'bold',
fontFamily: 'GothamBook,Helvetica,Arial,sans-serif',
textOverflow: 'none'
},
rotation:0//-90
},
title:{
text: null
},
color: "#FF0000",
lineColor: "#a1a1a1",
lineWidth: 1,
tickWidth: 0, // turns off ticks
tickmarkPlacement: "on",
tickColor: "#A1A1A1",
},
yAxis: {
offset: 0,
labels: {
style: {
color: "#222",
fontSize: '12px',
fontFamily: 'GothamBook,Helvetica,Arial,sans-serif',
fontWeight: 'bold'
}
},
title: {
text: null,
style: {
color: "#222",
fontSize: '12px',
fontWeight: "bold",
backgroundColor: "#fff",
fontFamily: 'GothamBook,Helvetica,Arial,sans-serif'
}
},
lineColor: "#A1A1A1",
gridLineColor: "#e0e0e0",
gridLineWidth: 0,
startOnTick: false,
endOnTick: false,
opposite: true,
tickPixelInterval: 50,
tickWidth: 0,
},
credits: {
enabled: false
},
legend: {
enabled: false
},
title: {
text: null
},
tf: {
forceBarChartOrigin:false, //if true, will force barcharts to start at 0, (if no negative values are present)
title:{
text:null,
},
titleContainer:function(){return $("
Advanced",
save: function(input) {
if (input !== editInitialVal) {
var newValue = input.replace(/[^\d.eE\-+]/g, "");
chartPoint.dataSeries.applyChange(chartPoint, newValue);
_this.triggerChartSeriesChanged(chartPoint.dataSeries, true)
}
directEdit.dialog().dialog("close");
}
}).generateElement().styledDialog({
tooltipFor: event,
temporary: true
});
// note: binding 'on' ("#advancedOptionsForPreciseDriver" + curEditableDatum.Identifier) won't always work as there is
// a race condition on this element not being create yet. Therefore, bind 'on' the parent
$(".explanation").on('click', "#advancedOptionsForPreciseDriver" + pointIdentifier, function() {
new PreciseDriverAdvancedOptions(_this, chartPoint.dataSeries, chartPoint.series, editIndex, editPeriod, editUnit, editInitialVal, window).show();
});
},
/**
* fire when a mouse over happens on chart point
* @param {boolean} editable whether the point is non-historical, and part of an editable series
*/
chartPointMouseOverListener : function(point, editable){
this.tooltip.tooltipMouseOver(point);
if(editable) {
/* this is the contents of dragCursor.png or dragCursor.cur, base64 encoded for 2 reasons:
- so there's no delay between mousing over and seeing the cursor
- so we don't have to bother with versioning on dragCursor.png or dragCursor.cur
*/
var isIE11 = !!navigator.userAgent.match(/Trident.*rv[ :]*11\./);
if ((navigator.userAgent.indexOf("MSIE") > "-1")||isIE11){
this.setCursor('url(data:image/x-win-bitmap;base64,AAACAAEAICACAAAAAAAwAQAAFgAAACgAAAAgAAAAQAAAAAEAAQAAAAAAgAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////////////////////////////////////////////////////////////////////////////////////////+/////H////g////+/////v////7////+/////v////7////+/////v////g////8f////v////////8=), n-resize');
} else {
this.setCursor('url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAcCAYAAAC3f0UFAAAAAXNSR0IArs4c6QAAAIpJREFUOMtjYEAD9fX1+w0NDR/V19fvZ8AH6uvr9zMxMf1jYGD4z8TE9A+nBmSFMIxVAzaF2DQwrlmzZkN5ebnl////mWCa7927J6KkpPQGxmdkZPzX2dl5HJfz/2MTZGIgAYwqHlU8nBSzMDAwMCgrK79Ey1YMSkpKr7FmK2IzLOlFAcmFDDHFFwB8gWBC/TVG0gAAAABJRU5ErkJggg==) 5 14, n-resize');
}
}
},
/**
* fire when mouse leaves a chart point
*/
chartPointMouseOutListener : function(event, editable){
this.tooltip.tooltipMouseOut(event.point);
this.setCursor('auto');
},
/**
* Sets the mouse cursor the browser should use
* Meant to be called e.g. to change the cursor when user mouses over editable line
* @param {String} cursorCSS css value for the cursor; can be "auto" to revert to normal
*/
setCursor: function(cursorCSS) {
// set style on the body, for IE
document.body.style.cursor = cursorCSS;
// also set on the highcharts container div, because highcharts tries to change cursors
// and we need to override it
this.options.tf.innerContainer().find('.highcharts-container').css({cursor: cursorCSS});
},
/**
* add color options to a charted series
*/
//COLOR RELATED METHODS
colorizeSeries: function(series){
var yaxisid = series.yAxis;
var yAxis = this.chart.get(yaxisid);
var axisColor = yAxis.options.baseColor;
if(series.color) {
// if the consumer already set a specific color for this series, no need to do anything
} else if(axisColor && !series.dataSeries.options.stack) {
var seriesColor=axisColor;
//shade series color based on axis color
//upto 11 shades: 1 base, 5 lighter, 5 darker in 10% steps
for(var step = 0; step<11; step++){
var shade = 10*Math.ceil(step/2);
if(step%2==0)shade=-shade;
var shadedColor = this.getColorShade(axisColor, shade)
if(!this.isColorInUseBySeries(shadedColor)){
series.color=shadedColor;
series.dataSeries.label.css("color", series.color)
return;
}
}
series.color=axisColor; //if all shades are exhausted continue returning axis color
} else {
series.color = this.findNextAutoColor();
}
//add the hover state which is the a brigher shade of the series color
series.marker = series.marker||{}
if(!series.colorByPoint)series.marker.borderColor = series.color;
series.marker.states = series.marker.states||{}
series.marker.states.hover = series.marker.states.hover||{}
series.marker.states.hover.color = this.getColorShade(series.color, 20)
series.dataSeries.label.css("color", series.color);
},
/**
* add color options to a charted yaxis
*/
colorizeAxis : function(color, yaxis){
yaxis.baseColor = color; //not a highcharts property, allows to quickly assertain this axis' basecolor
yaxis.lineColor = color;
yaxis.tickColor = color;
yaxis.labels = yaxis.labels||{}
yaxis.labels.style = yaxis.labels.style||{}
yaxis.labels.style.color=color
yaxis.title = yaxis.title||{}
yaxis.title.style = yaxis.title.style||{}
yaxis.title.style.color=color
},
/**
* check what the next avaialbe color for this chart it, basically go through
* series and yaxis that are already plotted on the highcharts chart
* and see which ones have already been used
*
* allows for the optional addtionalChartPoints array, this incase we have already used
* up a color, but havent added it to the highcharts object, for example when building
* a waterfall chart, we expect each point to have a unique color, however we collect
* all the points first and only at the end add all the whole series to the chart, that means
* while the waterfall points are being built the colors will be 'used-up' but not appear
* as part of highcharts in yet, isntead we manually pass in an addtional arrat of points to check
*
*/
findNextAutoColor : function(additionalChartPoints){
var colors = this.chart.options.colors
var inUse=false;
for(var c in colors){
var color = colors[c]
inUse = this.isColorInUse(color)
if(!inUse && additionalChartPoints){
inUse= this.isColorInUseByPoint(color, additionalChartPoints);
}
if(!inUse)
return color;
}
return this.chart.options.colors[0]; //if all colors have been exhausted, continue returning the first default color
},
/**
* is this color being used by a series or a yaxis
*/
isColorInUse : function (color){
var inUse = this.isColorInUseByAxis(color);
if(!inUse)inUse = this.isColorInUseBySeries(color);
//if(!inUse)inUse = this.isColorInUseByPoint(color);
return inUse;
},
/**
* is this color being used by a yaxis
*/
isColorInUseByAxis : function (color){
for(var i in this.chart.yAxis){
var yAxis = this.chart.yAxis[i];
if(yAxis.options.tickColor == color)return true;
else if (yAxis.options.lineColor == color) return true;
else if (yAxis.options.label && yAxis.options.label.style && yAxis.options.label.style.color == color)return true;
}
return false
},
/**
* is this color being used by a series
*/
isColorInUseBySeries : function (color){
for(var i in this.chart.series){
var series = this.chart.series[i];
if(series.options.color == color)return true;
}
return false;
},
isColorInUseByPoint : function (color, points){
if(points)
for( var p in points){
var point = points[p];
if(point.color== color)return true;
}
return false;
},
/**
* produce a shade of a color, lighted or darker by give percent
*/
getColorShade: function(color, percent){
var num = parseInt(color.slice(1),16), amt = Math.round(2.55 * percent), R = (num >> 16) + amt, G = (num >> 8 & 0x00FF) + amt, B = (num & 0x0000FF) + amt;
return "#" + (0x1000000 + (R<255?R<1?0:R:255)*0x10000 + (G<255?G<1?0:G:255)*0x100 + (B<255?B<1?0:B:255)).toString(16).slice(1);
},
/**
* update the line label positions, so they are still next to line, if a line has shifted, due to being dragged, or if yaxis scale has changed
*/
updateLabelPositions : function() {
if(!this.innerChart.getBaseXaxis() || this.innerChart.getBaseXaxis().series.length==0)return;
var enabled = this.option("tf.label.enabled");
var hasBarChart = false;
for(var index in this.innerChart.series){
var series = this.innerChart.series[index];
if(series.getSeriesType() == 'column') {
hasBarChart = true;
}
var label = series.getLabel();
var seriesLabelEnabled = enabled && series.option("tf.label.enabled", true)
if(label && seriesLabelEnabled) {
label.show();
}else if(label && !seriesLabelEnabled) {
label.hide();
}
}
if(!enabled)return; //if not enabled,
var label,
thirdToLastPoints = [],
seriesVisible = [],
closestDistance,
isClosestAbove,
thirdToLastPoint,
lastPoint,
chartXAxis = this.chart.xAxis[0],
categoriesLength = chartXAxis.categories.length,
numPointsBack = Math.max(Math.round(categoriesLength / 3), 1),
lastPointX = chartXAxis.translate(categoriesLength - 1),
thirdToLastPointX = chartXAxis.translate(categoriesLength - numPointsBack),
distance,
dataWithoutAxes,
left,
bottom,
innerContainer = this.innerContainer,
editableSeries = null;
//$.each(myData, function(index, datum) {
for(var index in this.innerChart.series){
var series = this.innerChart.series[index];
if(series.getDataPoints().length==0)continue;
seriesVisible[index] = true; //datum.visible;
thirdToLastPoints[index] = this.getPointCoords(series, numPointsBack);
}
//$.each(this.innerChart.series, function(index, series) {
for(var index in this.innerChart.series){
var series = this.innerChart.series[index];
var seriesLabelEnabled = series.option("tf.label.enabled", true)
if(series.getDataPoints().length==0 || !seriesLabelEnabled)continue;
label = series.getLabel();
if(label) {
closestDistance = Number.MAX_VALUE;
isClosestAbove = false;
thirdToLastPoint = thirdToLastPoints[index];
if(!thirdToLastPoint)continue;
$.each(thirdToLastPoints, function(j, p) {
if(index != j && seriesVisible[j] && (distance = Math.abs(p - thirdToLastPoint)) < closestDistance) {
closestDistance = distance;
isClosestAbove = p > thirdToLastPoint;
}
});
left = Math.min(thirdToLastPointX - 5, $(innerContainer).width() - label.width() - 50);
// TREF-5058: for bar charts, with only one timepoint, try to center the label horizontally
// with the graphic element representing the bar. This depends on some highcharts internals,
// so wrap in a try/catch and just leave the "left" variable alone at its previous value
// if anything goes wrong
if(series.getSeriesType() == 'column' && categoriesLength == 1) {
try {
var graphic = this.chart.get(series.id).data[categoriesLength - numPointsBack].graphic;
left = graphic.attr('x') + (graphic.attr('width')/2) - (label.width()/2);
} catch(e) {}
}
// TREF-5058: for line charts, we can place the label above or below the line, depending on where there's more space
// but for bar charts, we can only put labels above the bars, so that they don't overlap with the bars
if(isClosestAbove && !hasBarChart) {
// put label below point
bottom = thirdToLastPoint + 10 +label.height() + (series == editableSeries ? -4 : 0)
} else {
// put label above point
bottom = thirdToLastPoint +45 + (series == editableSeries ? 4 : 0);
}
// IE7 likes to throw errors here, so both check for NaNs and unfortunately that's not enough (not sure
// why) so need to wrap with a try/catch
if(!isNaN(left) && !isNaN(bottom)) {
try {
label.css({
left: left,
bottom: bottom
}).show();
} catch(e) {}
}
}
}
},
/**
* return pixel coordianates for a pont in a series, used to align labels
*/
getPointCoords: function(series, indexFromLast) {
var yAxis = this.chart.get(series.yaxis.id)
if(this.chart.get(series.id) == null) return null;
var point = this.chart.get(series.id).data[Math.max(0, this.chart.get(series.id).data.length - indexFromLast)]
if(!point)return null;
return yAxis.translate(point.dataPoint.Value * series.yaxis.getAxisScaleFactor());
},
/***
* helper to access deep options, avoid js errors
* for example to access options.series.marker.size you would need to make
* sure that series is not null and marker is not null, this method elliminates
* all those checks and allows you to specificy a default value for the options
* if it is not found/set
*/
option : function (pathString, defaultValue){
var path = pathString.split(".")
if(path[0]=="options")
path = path.slice(1);
var obj =this.options;
for (var i=0; i
Advanced",
save: function(input) {
if (input !== editInitialVal) {
var number = input.replace(/[^\d.eE\-+]/g, "");
self.changeUserInputValue(editIndex, number);
}
directEdit.dialog().dialog("close");
}
}).generateElement().styledDialog({
tooltipFor: event,
temporary: true
});
// note: binding 'on' ("#advancedOptionsForPreciseDriver" + curEditableDatum.Identifier) won't always work as there is
// a race condition on this element not being create yet. Therefore, bind 'on' the parent
$(".explanation").on('click', "#advancedOptionsForPreciseDriver" + curEditableDatum.Identifier, function() {
new PreciseDriverAdvancedOptions(self, editableDatum, editIndex, editPeriod, editUnit, editInitialVal, window).show();
});
}
// re-enable text selection
setTextSelectionEnabled(1);
// for some reason, these are necessary to make mouse events work again
if ($.isFunction(activePoint.onMouseOver)){
activePoint.onMouseOver();
activePoint.series.onMouseOver();
}
}*/
};
/**
* disable mouse text selection, so text is not inadvertedly selected when user drags
*/
DragContext.prototype.setTextSelectionEnabled = function(enabled) {
if($.browser.mozilla)
$("body").css({"-moz-user-select": enabled ? "text" : "none"});
else
document.onselectstart = enabled ? old_onselectstart : function() { return false; };
};
/**
* Increases/decreases the range of the editable series' axis by an amount equal to
* the (max - min) * power /100.
* .
* This method is meant to be used to 'scroll' the axis range
* by modifying it a little bit at regular intervals, for instance when
* the user drags the line near the top or bottom of the chart.
*
*
* @param tweakMax true if the upper end of the range should be modified,
* false if the lower end of the range should be modified
* @param power a number between -1 and 1 that tells how much and in what
* direction the axis range should
* be changed. Positive values expand the range, negative values contract
* the range, and the magnitude roughly specifies how much the range should
* expand/contract.
*/
DragContext.prototype.tweakAxisRange = function (tweakMax, power) {
var yAxisObject = this.chartSeries.yAxis;
var axisExtremes = yAxisObject.getExtremes();
var newMinimum = axisExtremes.min;
var newMaximum = axisExtremes.max;
var spread = newMaximum - newMinimum;
if(tweakMax){
newMaximum += spread * power * 0.2;
}else{
newMinimum -= spread * power * 0.2;
}
newMaximum = this.range(this.scale(this.dataSeries.option("tf.drag.min", -Number.MAX_VALUE) ), newMaximum, this.scale(this.dataSeries.option("tf.drag.max", Number.MAX_VALUE) ));
newMinimum = this.range(this.scale(this.dataSeries.option("tf.drag.min", -Number.MAX_VALUE) ), newMinimum, this.scale(this.dataSeries.option("tf.drag.max", Number.MAX_VALUE) ));
// if(!isNaN(this.forcedAxisMax) && !isNaN(this.forcedAxisMin)) {
// newMaximum = this.forcedAxisMax;
// newMinimum = this.forcedAxisMin;
// }
this.setAxisRange(yAxisObject, newMinimum, newMaximum);
this.chartSeries.chart.redraw();
};
/**
* scale the value by dragged yaxis scale factor
*/
DragContext.prototype.scale = function(value){
return value * this.dataSeries.yaxis.getAxisScaleFactor()
};
/**
* create a timer, that will check position of mouse, and as the mouse apporaches yaxis min or yaxis max, push
* out that yaxis exterem to give the impression of scrolling
*/
DragContext.prototype.createAxisRangeScrollTimer = function(){
var _this = this;
return $.timer(500, function() {
var div = $(_this.chartSeries.chart.container);
relativeMouseY = _this.mouseY - div.offset().top,
divHeight = div.height();
if(relativeMouseY < 10) {
_this.tweakAxisRange(1, Math.min(1, (10 - relativeMouseY)/60));
//mouseMoveListenerHelper();
} else if(relativeMouseY > divHeight - 30) {
_this.tweakAxisRange(0, Math.min(1, (relativeMouseY - divHeight + 30)/60));
//mouseMoveListenerHelper();
}
});
};
/**
this timer is used to stop the chart from sending very rapid chartChangedListener() calls when the user drags
the chart; instead, those dispatches are rate limited. We basically wait until the user has stopped moving
for 200ms before firing the event.
*/
DragContext.prototype.createChartChangeDispatchTimer = function(){
var _this = this;
EVENT_DISPATCH_DELAY = 200; // in milliseconds
var result = $.timer( EVENT_DISPATCH_DELAY, function() {
_this.chartChangeDispatchTimer.stop();
_this.widget.triggerChartSeriesChanged(_this.dataSeries, false)
//doEditableSeriesTypeChange(); //<--TODO can an external callback update the color of the series currently being dragged??
});
result.stop();
return result;
};
DragContext.prototype.isTouchDevice = function () {
return !!('ontouchstart' in window);
};
/********************************
JS CHART DOMAIN MODEL
data domain model backing the actual charted object
********************************/
function InnerChart(widget){
this.xaxies=[];
this.yaxies=[];
this.series=[];
this.widget=widget;
}
/**
* get series by id
*/
InnerChart.prototype.getSeries = function(id){
for(var i =0; i=0&&y<=1&&A>=0&&A<=1&&(d?n++:n.push({x:x.x,y:x.y,t1:y,t2:A}))}}return n}function bF(a,b){return bG(a,b,1)}function bE(a,b){return bG(a,b)}function bD(a,b,c,d,e,f,g,h){if(!(x(a,c)
" + seriesName + "
" + value + "
" + percentage;
return html;
};
/**
* Methods for slicing the legend labels based on number of series in the legend
*/
legendLabelFormatter = function() {
var numberOfItems;
numberOfItems = this.chart.series.length;
if (numberOfItems > 2) {
return this.name.slice(0, 23);
} else if (numberOfItems > 1) {
return this.name.slice(0, 38);
}
return this.name;
};
/**
* Methods for creating chart toolbar
* Toolbar can be used to change period type and chart type
* @param {string} defaultChartType default selected chart type in toolbar
*/
ChartPopup.prototype.createToolbar = function(defaultChartType) {
var allValue, columnButton, dataPeriodIdx, dataPeriodValue, divButtonElement, existPeriodsSize, hasValue, key, lineButton, periodTypeKey, periodTypeValues, ref, ref1, ref2, selectElement, selectValue, selectValueVal, self, stackBarChartButton, toolbarElement, val;
self = this;
this.options.toolbarElement.addClass("toolbarElement");
toolbarElement = $(this.options.toolbarElement[0]);
periodTypeValues = [];
for (periodTypeKey in tf.period.type) {
periodTypeValues.push(tf.period.type[periodTypeKey]);
}
existPeriodsSize = $.grep(this.options.dataPeriodsMap, function(dataPeriods, i) {
return $.inArray(i - 1, periodTypeValues) > -1 && dataPeriods > 0;
}).length > 0;
if (existPeriodsSize) {
selectElement = $("");
selectElement.addClass("selectElement");
selectValue = {};
allValue = [];
ref = this.options.dataPeriodsMap;
for (dataPeriodIdx in ref) {
dataPeriodValue = ref[dataPeriodIdx];
hasValue = false;
if (this.options.dataPeriodsMap[dataPeriodIdx] > 0) {
ref1 = this.options.periodToId;
for (key in ref1) {
val = ref1[key];
if (tf.period.typeOf(key) == dataPeriodIdx && activeModel.getValue(val)) {
hasValue = true;
break;
}
}
}
if (hasValue) {
selectValue[tf.period.typeNames[dataPeriodIdx]] = dataPeriodIdx;
allValue.push(dataPeriodIdx);
}
}
for (val in selectValue) {
selectValueVal = selectValue[val];
$("", {
value: selectValueVal,
text: val
}).appendTo(selectElement);
}
selectElement.on("change", function(e) {
return self.changePeriodType(parseInt(this.value));
});
selectElement.appendTo(toolbarElement);
/* Ensure the selected value contains the selected period chart type, if not update */
if (allValue && allValue.length > 0 && (ref2 = this.options.selectedPeriodChartType, indexOf.call(allValue, ref2) < 0)) {
selectElement.val(allValue[0]);
this.changePeriodType(parseInt(allValue[0]));
} else {
selectElement.val(this.options.selectedPeriodType);
}
}
divButtonElement = $("");
divButtonElement.addClass("divButtonElement");
lineButton = $(" ");
columnButton = $(" ");
stackBarChartButton = $(" ");
columnButton.click((function(_this) {
return function(e) {
e.preventDefault();
divButtonElement.find("a").removeClass("active");
$(e.target).addClass("active");
return _this.changeType("column");
};
})(this));
lineButton.click((function(_this) {
return function(e) {
e.preventDefault();
divButtonElement.find("a").removeClass("active");
$(e.target).addClass("active");
return _this.changeType("line");
};
})(this));
stackBarChartButton.click((function(_this) {
return function(e) {
e.preventDefault();
divButtonElement.find("a").removeClass("active");
$(e.target).addClass("active");
return _this.changeType("stacked");
};
})(this));
if (this.options.stackedBarChart) {
stackBarChartButton.addClass("active");
} else if (defaultChartType === 'column') {
columnButton.addClass("active");
} else {
lineButton.addClass("active");
}
if (this.options.stackedBarChart) {
divButtonElement.append(stackBarChartButton).append(lineButton);
} else {
divButtonElement.append(columnButton).append(lineButton);
}
divButtonElement.appendTo(toolbarElement);
$("").addClass("toolbarBottom").appendTo(toolbarElement);
return $(this.options.chartElement[0]).before(toolbarElement);
};
/**
* Methods that can be called for changing chart type
* Called from createToolbar
* @param {string} newType chart type
*/
ChartPopup.prototype.changeType = function(newType) {
if (newType === "stacked") {
$(this.options.chartElementOrig[0]).hide();
$(this.options.chartElementStacked[0]).show();
} else {
$(this.options.chartElementOrig[0]).show();
$(this.options.chartElementStacked[0]).hide();
thisChart.options.chart.type = newType;
}
chartType = newType;
return this.updateAllData();
};
/**
* Methods that can be called for changing chart period type
* Called from createToolbar
* @param {string} periodType chart period type
*/
ChartPopup.prototype.changePeriodType = function(periodType) {
this.options.selectedPeriodChartType = periodType;
return this.updateAllData();
};
return ChartPopup;
})();
window.ChartPopup = ChartPopup;
}).call(this);
;;
// lib/jquery-1.10.2.min.js
// @include tags/toolTip.js
// @include lib/underscore.js
// @include lib/jquery-ui-1.10.3.min.js
// @include widgetCommon/styledDialog.js
// @include infrastructure/period.js
// @include infrastructure/unitUtil.js
// @include infrastructure/numberFormatUtil.js
// @include infrastructure/stackedBarChart.js
/**
* Popup containing a chart that cannot be directly modified, but can
* update on model changes.
*
* Popup created using styledDialog contains Highcharts chart
* Can also use StackedBarChart
*
* @constructor
* @param {Object} options contains
* {Object} companyData Encapsulates all the data on the Company model (REQUIRED)
* {ModelManager} modelManager manager containing the various current ForecastModels (REQUIRED)
* {Object} periodToId map from period strings to identifiers of values that should be on the chart (REQUIRED)
* {JQuery Element} chartElement element that will be used for chart
* {JQuery Element} toolbarElement element that will be used for chart toolbar
* {Object} cssClass css classes contains
* {string} container css class for chart container
* {Object} dialogOptions options for popup which is used by styledDialog
* {Object} chartOptions options for chart inside popup which is used by Highcharts
* {Boolean} isEmbedded whether using the chart inside some container (not in the popup) or not
*
* REQUIRED (can also be set after creation but before use)
*
* You can also override creation-time options for the chart and dialog
* box. For example, you can override `dialogOptions.title` to set a title
* on the popup and override `chartOptions.xAxis.labels.formatter` if you
* are not using periods as categories.
*/
(function() {
var ModelChartPopup,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
ModelChartPopup = (function() {
var activeModel, baseModel, chartType, decimalPlaces, defaultStackedLabelsTooltip, defaultXAxisLabelFormatter, getAbsoluteValue, legendLabelFormatter, processLegendVisibility, thisChart, thisOptions;
thisChart = null;
chartType = null;
decimalPlaces = 1;
baseModel = void 0;
activeModel = void 0;
thisOptions = null;
function ModelChartPopup(options) {
this.unregister = bind(this.unregister, this);
this.switchModel = bind(this.switchModel, this);
this.drawPlotLine = bind(this.drawPlotLine, this);
this.findNowIndex = bind(this.findNowIndex, this);
this.getChartColor = bind(this.getChartColor, this);
this.updateAllData = bind(this.updateAllData, this);
this.createStackedBar = bind(this.createStackedBar, this);
this.updateData = bind(this.updateData, this);
var publishedModels;
var chartOptions;
if (options.collectionIdentifier && options.companyData.md.yAxisLabels && options.companyData.md.yAxisLabels[options.collectionIdentifier]) {
this.unit = options.companyData.md.yAxisLabels[options.collectionIdentifier];
} else {
this.unit = options.unit;
}
this.options = {
deepLinkingEnabled: false,
companyData: null,
modelManager: null,
periodToId: {},
plots: null,
stackedBarChart: null,
chartElement: $(_.template($('#bootstrap-modal-template').html())({
includeHeader: true, includeCloseX: true, includeFooter: false
})),
chartElementOrig: $(''),
chartElementStacked: $(''),
toolbarElement: $(''),
cssClass: {
container: 'chartPopup'
},
dialogOptions: {
title: "Chart Popup",
draggable: true,
resizable: true,
modal: false,
width: 700,
height: 530
},
chartOptions: {
title: "",
chart: {
backgroundColor: '#ffffff',
},
tf :{
titleContainer:null,
label:{
enabled:false
},
plotLines: {
nowLine: {enabled:false }
},
forceBarChartOrigin:true,
series:{
name:{
useSeriesName:false,
useModelName:true,
},
label:{
useSeriesName:false,
useModelName:false,
}
},
},
// colors: ['#69b4eb'],
yAxis: {
title: {
text: this.unit,
style: {
color: '#878787',
fontFamily: 'Gotham, Helvetica, Arial, sans-serif',
fontSize: '10pt'
}
},
labels: {
style: {
color: '#999999',
fontFamily: 'Gotham, Helvetica, Arial, sans-serif',
fontSize: '10pt'
},
formatter: (window.oldCharts)?this.createDefaultYAxisFormatter():null,
},
gridLineWidth: 1,
lineWidth:1,
},
credits: {
enabled: false
},
legend: {
enabled: true,
borderColor: '#000'
},
plotOptions: {
series: {
animation: false,
dataLabels: {
formatter: this.createDefaultPlotFormatter(),
enabled: false,
color: '#999999',
style: {
fontFamily: 'Gotham, Helvetica, Arial, sans-serif',
fontSize: '9pt'
}
}
},
column:{
marker: {
borderColor: '#FFFFFF',
borderWidth: 1
},
groupPadding: 0,
dataLabels: {
enabled: false
},
cursor: 'pointer'
}
},
xAxis: {
labels: {
//formatter: defaultXAxisLabelFormatter,
step: 1,
style: {
color: '#999999',
fontFamily: 'Gotham, Helvetica, Arial, sans-serif',
fontSize: '9pt'
}
},
tickLength:10,
tickWidth:1,
},
tooltip: {
formatter: this.createDefaultTooltipFormatter()
},
series: {
lineWidth: 8,
}
},
selectedPeriodType: false,
selectedPeriodChartType: false,
dataPeriodsMap: false,
isEmbedded: false
};
this.extendOptions(options);
thisOptions = this.options;
this.options.chartElement.find('.modal-header').append($('', { 'class': 'no-margin', text: this.options.dialogOptions.title }))
this.options.chartElement.find('.modal-body').append(this.options.chartElementOrig);
this.options.chartElement.find('.modal-body').append(this.options.chartElementStacked);
this.options.chartElement.modal({ show: false })
if (!this.options.isEmbedded) {
this.options.chartElement.addClass(this.options.cssClass.container);
this.open();
}
baseModel = this.options.modelManager.get('trefis');
activeModel = this.options.modelManager.get('active');
publishedModels = this.options.modelManager.get("publishedModels");
chartOptions = {
chart: {
// renderTo: this.options.chartElementOrig[0]
},
plotOptions: {
line: {
lineWidth: 10
}
}
};
chartOptions = $.extend(true, {}, this.options.chartOptions, chartOptions)
if (publishedModels.length > 0) {
chartOptions.plotOptions.series.type = "line";
}
chartType ="column"
if (this.options.stackedBarChart) {
// this.options.stackedBarCview.answer().constituentBreakdowns[0]
// current fields on the a constituentBreakdown
// {
// name:'default', // not sure what this is used for
// components: [], // the constituents...
// displayType:string // '', stackedcolumn, waterfall
// }
chartType = this.options.stackedBarChart.defaultChartType;
// some charts may not have the display type associated with them, default them to stackedcolumn
if (!chartType){
chartType = 'stackedcolumn'
}
}
if (!this.options.isEmbedded) {
this.createToolbar(chartType);
}
//this.chart = new Highcharts.Chart($.extend(true, {}, this.options.chartOptions, chartOptions));
chartOptions.series = chartOptions.series||{}
this.chart = $(this.options.chartElementOrig[0]).modelChart({
deepLinkingEnabled: this.options.deepLinkingEnabled,
shareLinksModel: this.options.shareLinksModel,
divisionIdentifier: this.options.collectionIdentifier,
answerIdentifier: this.options.answerIdentifier,
modelManager : this.options.modelManager,
maxHistoricalPoints: tf.modelPreferences.historicalYearsNumber,
plots: this.options.plots,
chart: chartOptions,
height: 460,
editableUserSeriesOptions:{
tf:{
series:{
editable:false
}
},
}
})
thisChart = this.chart;
var self = this
this.options.chartElement.on('hide.bs.modal', function() {
self.chart.modelChart('destroy')
self.options.chartElement.remove()
})
tf.tags.toolTip();
//this.register();
}
/**
* Can be called externally to set options after creation
* @param {Object} options
*/
ModelChartPopup.prototype.extendOptions = function(options) {
return this.options = $.extend(true, this.options, options || {});
};
/**
* Create and display non-stacked-bar chart for current model
* Created for called by @updateAllData
* @param {ForecastModel} model current model that will be used for creating the chart
* @param {integer} idx current model index. will be used for clearing the chart if idx is 0.
* @param {Array of String} periods X axis categories that should be used in the chart
*/
ModelChartPopup.prototype.updateData = function(model, idx, periods) {
var axis, countOfData, data, j, len, mantissa, ref, seriesColor, seriesName, sumOfData;
sumOfData = 0;
countOfData = 0;
data = $.map(periods, (function(_this) {
return function(period) {
var ref, value;
value = (ref = model.getValue(_this.options.periodToId[period])) != null ? ref : null;
if (value) {
countOfData++;
sumOfData = sumOfData + value;
return parseFloat(value);
} else {
return 0;
}
};
})(this));
mantissa = sumOfData / countOfData;
decimalPlaces = 1;
if (mantissa > 0) {
while (!(mantissa > 1)) {
decimalPlaces++;
mantissa *= 10;
}
}
ref = this.chart.xAxis;
for (j = 0, len = ref.length; j < len; j++) {
axis = ref[j];
axis.setCategories(periods, false);
}
idx = parseInt(idx);
if (idx === 0) {
while (this.chart.series.length > 0) {
this.chart.series[0].remove(false);
}
}
this.chart.counters.color = 0;
this.chart.counters.symbol = 0;
seriesName = model.getTitle();
seriesColor = "";
if (idx > 0) {
if (model.modelId === baseModel.modelId) {
seriesColor = "#666";
} else {
seriesColor = this.getChartColor(idx);
}
}
this.chart.addSeries({
name: seriesName,
data: data,
color: seriesColor
}, false);
this.chart.redraw();
return this.updateChartSize();
};
/**
* This section will create the stacked bar chart for constituents
* It will limit the json data to 32 elements and 12 labels
* needs to be refactored and not to be rebuilt each time?
* @param {ForecastModel} model curent model that will be used for creating the chart
* @param {PeriodType} periodType is used to filter the data
*/
ModelChartPopup.prototype.createStackedBar = function(model, periodType) {
var chartOptions,
constituent,
difference,
differenceData,
includeDifference,
index,
index2,
j,
k,
keys,
legend,
legendHeight,
legendItemStyleWidth,
legendItemWidth,
maxSize,
maxVal,
modelData,
percentageDifference,
ref,
ref1,
ref2,
ref3,
sbcPeriods,
sbcSeries,
sbcSeriesSize,
series,
seriesValue,
stepSize,
total;
constituent = {
"stat": "Constituents"
};
keys = _.keys(this.options.stackedBarChart.data);
sbcPeriods = [];
maxSize = 32;
if (this.options.isEmbedded) {
maxSize = 20;
}
sbcSeries = $.map(keys, (function(_this) {
return function(key) {
var data, keys2, map, series;
series = {};
series.name = _this.options.stackedBarChart.names[key];
series.unit = _this.options.unit;
map = _this.options.stackedBarChart.data[key];
keys2 = _.keys(_this.options.stackedBarChart.data[key]);
keys2 = $.grep(keys2, function(period) {
return tf.period.typeOf(period) === periodType;
});
keys2.sort(tf.period.compare);
keys2 = keys2.slice(0, maxSize);
if (keys2.length > sbcPeriods.length) {
sbcPeriods = keys2;
}
data = $.map(keys2, function(key2) {
var modelValue, ref, ref1, value;
value = (ref = map[key2]) != null ? ref : null;
modelValue = (ref1 = model.getValue(value)) != null ? ref1 : null;
if (isNaN(modelValue)) {
return 0;
} else {
return modelValue;
}
});
series.data = data;
return series;
};
})(this));
modelData = $.map(sbcPeriods, (function(_this) {
return function(period) {
var ref, value;
value = (ref = model.getValue(_this.options.periodToId[period])) != null ? ref : null;
return value;
};
})(this));
maxVal = sbcPeriods.length - 1;
stepSize = 1;
differenceData = [];
includeDifference = false;
for (index = j = 0, ref = maxVal, ref1 = stepSize; ref1 > 0 ? j <= ref : j >= ref; index = j += ref1) {
sbcSeriesSize = sbcSeries.length - 1;
total = 0;
for (index2 = k = 0, ref2 = sbcSeriesSize, ref3 = stepSize; ref3 > 0 ? k <= ref2 : k >= ref2; index2 = k += ref3) {
seriesValue = sbcSeries[index2].data[index];
total = total + seriesValue;
}
difference = Math.round(modelData[index] - total);
differenceData.push(isNaN(difference) ? 0 : difference);
percentageDifference = Math.abs(difference / modelData[index]);
if (percentageDifference > 0.01) {
includeDifference = true;
}
}
if (includeDifference) {
series = {};
series.name = "Other";
series.unit = "$";
series.data = differenceData;
sbcSeries.push(series);
}
if (sbcSeries.length > 4 && this.options.isEmbedded) {
sbcSeries = processLegendVisibility(sbcSeries);
}
constituent.series = sbcSeries;
constituent.bars = sbcPeriods.slice(0, maxSize);
legend = {
align: 'center',
verticalAlign: 'bottom',
layout: 'horizontal',
labelFormatter: legendLabelFormatter,
y: 1,
itemStyle: {
font: '11pt GothamBook,Helvetica,Arial,sans-serif',
textDecoration: 'none',
color: '#FFF',
cursor: 'default',
color: '#3E576F'
},
itemHoverStyle: {
color: '#3E576F'
}
};
if (sbcSeries.length > 3 && !this.options.isEmbedded) {
legendHeight = 42;
legendItemWidth = 223;
legendItemStyleWidth = 220;
if (sbcSeries.length > 12) {
legendHeight = 108;
} else if (sbcSeries.length > 9) {
legendHeight = 84;
} else if (sbcSeries.length > 6) {
legendHeight = 61;
} v
legend = {
width: 680,
height: legendHeight,
itemWidth: legendItemWidth,
align: 'center',
verticalAlign: 'bottom',
layout: 'horizontal',
labelFormatter: legendLabelFormatter,
y: 1,
itemStyle: {
font: '11pt GothamBook,Helvetica,Arial,sans-serif',
textDecoration: 'none',
color: '#FFF',
cursor: 'default',
width: legendItemStyleWidth,
color: '#3E576F',
textAlign: 'center'
},
itemHoverStyle: {
color: '#3E576F'
}
};
}
chartOptions = {
chart: {
backgroundColor: '#ffffff'
},
xAxis: {
labels: {
formatter: defaultXAxisLabelFormatter,
step: 1,
style: {
color: '#999999',
fontFamily: 'Gotham, Helvetica, Arial, sans-serif',
fontSize: '9pt'
}
}
},
yAxis: {
title: {
text: this.options.unit
},
stackLabels: {
style: {
color: '#333'
},
enabled: false
},
labels: {
formatter: this.createDefaultYAxisFormatter()
},
opposite: this.options.isEmbedded
},
legend: legend,
tooltip: {
backgroundColor: '#ffffff',
useHTML: true,
formatter: defaultStackedLabelsTooltip,
style: {
color: '#333'
}
},
point: {
events: {
legendItemClick: (function(_this) {
return function() {
return false;
};
})(this)
}
}
};
chartOptions = $.extend(true, chartOptions, this.options.stackedBarChartOptions || {});
return this.chartStacked = new StackedBarChart($.extend(constituent, {
width: this.options.chartElement.width(),
height: this.options.chartElement.height(),
renderTo: this.options.chartElementStacked[0],
chartOptions: chartOptions
}));
};
/**
* sort series by absolute value
* hide items #5 and lower
*/
processLegendVisibility = function(series) {
var idx;
for (idx in series) {
series[idx].absoluteValue = getAbsoluteValue(series[idx]);
}
series.sort(function(a, b) {
return parseFloat(b.absoluteValue) - parseFloat(a.absoluteValue);
});
idx = 4;
while (idx < series.length) {
series[idx].showInLegend = false;
idx++;
}
return series;
};
/**
* get serie's absolute value
*/
getAbsoluteValue = function(serie) {
var idx, returnValue;
returnValue = 0;
for (idx in serie.data) {
returnValue += serie.data[idx];
}
return Math.abs(returnValue);
};
/**
* Create and display chart for all model
*/
ModelChartPopup.prototype.updateAllData = function() {
var allModels, publishedModel, idx, model, periodType, periods, ref;
periods = _.keys(this.options.periodToId);
this.options.dataPeriodsMap = tf.period.getCountMap(periods, false);
if (this.options.selectedPeriodChartType) {
periodType = tf.period.getCurrentType(periods, this.options.companyData, false, this.options.selectedPeriodChartType);
} else {
periodType = tf.period.getCurrentType(periods, this.options.companyData, false);
}
this.options.selectedPeriodType = periodType;
periods = $.grep(periods, function(period) {
return tf.period.typeOf(period) === periodType;
});
periods.sort(tf.period.compare);
if (chartType === 'stacked') {
this.createStackedBar(activeModel, periodType);
} else {
allModels = [];
if (this.options.modelManager.get('publishedModels')) {
ref = this.options.modelManager.get('publishedModels');
for (idx in ref) {
publishedModel = ref[idx];
if (publishedModel.modelId !== activeModel.modelId) {
allModels.push(publishedModel);
}
}
}
if (activeModel !== baseModel && activeModel.isModified()) {
allModels.push(activeModel);
}
allModels.push(baseModel);
for (idx in allModels) {
model = allModels[idx];
this.updateData(model, idx, periods);
}
}
return this.drawPlotLine();
};
/**
* Get color that will be used by chart
* @param {integer} idx model index
*/
ModelChartPopup.prototype.getChartColor = function(idx) {
var result;
result = ["936", "639", "396", "993"][idx % 4];
return "#" + result;
};
/**
* Get category index minus one for first projected period
* Created to be called from @drawPlotLine
*/
ModelChartPopup.prototype.findNowIndex = function() {
var categories, category, firstProjectedPeriod, idx, result;
result = -1;
categories = thisChart.xAxis[0].categories;
firstProjectedPeriod = this.options.companyData.md.sankeyPeriods.codes[0];
for (idx in categories) {
category = categories[idx];
if (tf.period.periodEquals(category, firstProjectedPeriod)) {
result = idx === 0 ? 0 : idx - 1;
break;
}
}
return result;
};
/**
* Draw plot line in the chart
*/
ModelChartPopup.prototype.drawPlotLine = function() {
var nowIndex, xAxis;
xAxis = thisChart.xAxis[0];
xAxis.removePlotLine("verticalPlotLine");
if (thisChart.options.chart.type === "line") {
nowIndex = this.findNowIndex();
if (nowIndex >= 0) {
return xAxis.addPlotLine({
color: "#888",
id: "verticalPlotLine",
width: 1,
value: nowIndex,
zIndex: 2
});
}
}
};
/**
* Methods for listening for model changes, reacting to them, and no
* longer listening when the popup is closed.
*
* We want to listen for changes in which model is being used, and
* changes in the output of the current model.
*/
ModelChartPopup.prototype.register = function() {
var ref;
if ((ref = this.options.modelManager) != null) {
ref.on('change:active', this.switchModel);
}
return this.switchModel();
};
/**
* Methods for updating current chart to active model
*/
ModelChartPopup.prototype.switchModel = function() {
if (activeModel != null) {
activeModel.removeChangeListener(this.updateAllData);
}
activeModel = this.options.modelManager.get('active');
activeModel.addChangeListener(this.updateAllData);
return this.updateAllData();
};
/**
* Methods for removing all listener
* Called when popup is closed
*/
ModelChartPopup.prototype.unregister = function() {
this.options.modelManager.off('change:active', this.switchModel);
return activeModel != null ? activeModel.removeChangeListener(this.updateAllData) : void 0;
};
/**
* Methods for opening the popup
*/
ModelChartPopup.prototype.open = function() {
var callbackOptions, options;
callbackOptions = {
close: this.unregister,
resize: (function(_this) {
return function() {
return _this.updateChartSize();
};
})(this)
};
options = $.extend({}, this.options.dialogOptions, callbackOptions);
this.options.chartElement.modal('show')
};
/**
* Methods for closing the popup
*/
ModelChartPopup.prototype.close = function() {
return this.options.chartElement.modal("hide").remove();
};
/**
* Methods for updating the chart size
*/
ModelChartPopup.prototype.updateChartSize = function() {
var height, highChart, width;
width = this.options.chartElement.width();
height = this.options.chartElement.height();
this.chart.setSize(width, height, false);
if (chartType === "stacked") {
highChart = this.chartStacked.chart;
return highChart.setSize(width, height, false);
}
};
/**
* Methods for creating default tooltip formatter
* Called bound to a Highcharts datum descriptor object
*/
ModelChartPopup.prototype.createDefaultTooltipFormatter = function() {
var unit;
unit = this.unit;
return function() {
var x, y;
x = tf.period.format(this.x);
y = UnitUtil.getDisplayString(this.y, unit === '%' ? unit : '');
return x + ': ' + y;
};
};
/**
* Methods for creating default plot formatter
* Called bound to a Highcharts datum descriptor object
*/
ModelChartPopup.prototype.createDefaultPlotFormatter = function() {
var unit;
unit = this.unit;
return function() {
var index, length;
length = this.series.xData.length;
if (length > 12) {
index = $.inArray(this.x, this.series.xAxis.categories);
if (length < 25) {
if ((index % 2) !== 0) {
return "";
}
} else {
if ((index % 4) !== 0) {
return "";
}
}
}
return UnitUtil.getDisplayString(this.y.toFixed(decimalPlaces), unit === '%' ? unit : '');
};
};
/**
* Methods for creating default Y Axis formatter
* Called bound to a Highcharts datum descriptor object
*/
ModelChartPopup.prototype.createDefaultYAxisFormatter = function() {
var unit;
unit = this.unit;
return function() {
return UnitUtil.getDisplayString(this.value, unit === '%' ? unit : '');
};
};
/**
* Method for creating default x axis label formatter
* It will limit to 'lower' labels
* Called bound to a Highcharts datum descriptor object
*/
defaultXAxisLabelFormatter = function() {
var index, lower, upper, xYear;
lower = 12;
upper = 25;
if (thisOptions.isEmbedded) {
lower = 8;
upper = 15;
}
if (this.axis.categories.length > lower) {
index = $.inArray(this.value, this.axis.categories);
if (this.axis.categories.length < upper) {
if ((index % 2) !== 0) {
return "";
}
} else {
if ((index % 4) !== 0) {
return "";
}
}
}
xYear = tf.period.format(this.value);
xYear = xYear.substring(2, xYear.length);
if (parseInt(xYear) === new Date().getFullYear()) {
return "NOW";
} else {
return "'" + xYear.replace(" ", "");
}
};
/**
* Method for creating default stacked bar series tooltip formatter
* This is HTML format
* Called bound to a Highcharts datum descriptor object
*/
defaultStackedLabelsTooltip = function() {
var html, percentage, seriesName, value, year;
year = tf.period.format(this.x);
seriesName = this.series.name;
value = this.series.options.unit + UnitUtil.getDisplayString(this.y, "");
percentage = NumberFormatUtil.formatPercent(this.percentage, 100) + " of " + this.series.options.unit + UnitUtil.getDisplayString(this.total, "");
html = "" + year + "
" + seriesName + "
" + value + "
" + percentage;
return html;
};
/**
* Methods for slicing the legend labels based on number of series in the legend
*/
legendLabelFormatter = function() {
var numberOfItems;
numberOfItems = this.chart.series.length;
if (numberOfItems > 2) {
return this.name.slice(0, 23);
} else if (numberOfItems > 1) {
return this.name.slice(0, 38);
}
return this.name;
};
/**
* Methods for creating chart toolbar
* Toolbar can be used to change period type and chart type
* @param {string} defaultChartType default selected chart type in toolbar
*/
ModelChartPopup.prototype.createToolbar = function(defaultChartType) {
var columnButton,
dataPeriodIdx,
dataPeriodValue,
divButtonElement,
existPeriodsSize,
lineButton,
periodTypeKey,
periodTypeValues,
ref,
selectElement,
selectValue,
selectValueVal,
self,
stackBarChartButton,
arearangeChartButton,
toolbarElement,
val;
self = this;
this.options.toolbarElement.addClass("toolbarElement");
toolbarElement = $(this.options.toolbarElement[0]);
periodTypeValues = [];
for (periodTypeKey in tf.period.type) {
periodTypeValues.push(tf.period.type[periodTypeKey]);
}
existPeriodsSize = $.grep(this.options.dataPeriodsMap, function(dataPeriods, i) {
return $.inArray(i - 1, periodTypeValues) > -1 && dataPeriods > 0;
}).length > 0;
//NOT USING THIS TOOLBAR PERIOD SELECTION ANYMORE, USING THE WIDGET'S CHART BUTTON CONTROL INSTEAD
/*
if (existPeriodsSize) {
selectElement = $("");
selectElement.addClass("selectElement");
selectValue = {};
ref = this.options.dataPeriodsMap;
for (dataPeriodIdx in ref) {
dataPeriodValue = ref[dataPeriodIdx];
if (this.options.dataPeriodsMap[dataPeriodIdx] > 0) {
selectValue[tf.period.typeNames[dataPeriodIdx]] = dataPeriodIdx;
}
}
for (val in selectValue) {
selectValueVal = selectValue[val];
if (this.options.dataPeriodsMap[selectValueVal] > 0) {
$("", {
value: selectValueVal,
text: val
}).appendTo(selectElement);
}
}
selectElement.val(this.options.selectedPeriodType);
selectElement.on("change", function(e) {
return self.changePeriodType(parseInt(this.value));
});
selectElement.appendTo(toolbarElement);
}*/
divButtonElement = $("");
divButtonElement.addClass("divButtonElement");
lineButton = $(" ");
columnButton = $(" ");
stackBarChartButton = $(" ");
arearangeChartButton = $("area");
// wire up a wave
// _this.changeType("arearange")
arearangeChartButton.click((function(_this) {
return function(e) {
e.preventDefault();
divButtonElement.find("a").removeClass("active");
$(e.target).addClass("active");
return _this.changeType("arearange");
};
})(this));
columnButton.click((function(_this) {
return function(e) {
e.preventDefault();
divButtonElement.find("a").removeClass("active");
$(e.target).addClass("active");
return _this.changeType("column");
};
})(this));
lineButton.click((function(_this) {
return function(e) {
e.preventDefault();
divButtonElement.find("a").removeClass("active");
$(e.target).addClass("active");
return _this.changeType("line");
};
})(this));
stackBarChartButton.click((function(_this) {
return function(e) {
e.preventDefault();
divButtonElement.find("a").removeClass("active");
$(e.target).addClass("active");
//return _this.changeType("stacked");
return _this.changeType("stackedcolumn");
};
})(this));
if (this.options.stackedBarChart) {
stackBarChartButton.addClass("active");
} else if (defaultChartType === 'column') {
columnButton.addClass("active");
} else if (this.options.arearangeChart){
arearangeChartButton.addClass("active");
} else {
lineButton.addClass("active");
}
/*
if (this.options.stackedBarChart) {
divButtonElement.append(stackBarChartButton).append(lineButton);
} else {
divButtonElement.append(columnButton).append(lineButton);
}
if (this.options.arearangeChart){
divButtonElement.append(arearangeChartButton);
}*/
divButtonElement.appendTo(toolbarElement);
$("").addClass("toolbarBottom").appendTo(toolbarElement);
return $(this.options.chartElement[0]).before(toolbarElement);
};
/**
* Methods that can be called for changing chart type
* Called from createToolbar
* @param {string} newType chart type
*/
ModelChartPopup.prototype.changeType = function(newType) {
$(this.chart).modelChart("updateSeriesType", newType);
/*if (newType === "stacked") {
$(this.options.chartElementOrig[0]).hide();
$(this.options.chartElementStacked[0]).show();
} else {
$(this.options.chartElementOrig[0]).show();
$(this.options.chartElementStacked[0]).hide();
thisChart.options.chart.type = newType;
}
chartType = newType;
return this.updateAllData();*/
};
/**
* Methods that can be called for changing chart period type
* Called from createToolbar
* @param {string} periodType chart period type
*/
ModelChartPopup.prototype.changePeriodType = function(periodType) {
this.options.selectedPeriodChartType = periodType;
return this.updateAllData();
};
return ModelChartPopup;
})();
window.ModelChartPopup = ModelChartPopup;
}).call(this);
;;
// lib/jquery-1.10.2.min.js
// @include tf/admin.js
(function(){
function findFirstNextChecked(el) {
if(el.is(':checked')) {
return el;
} else {
var next = el.closest("tr").next(); // next() returns a collection
if (next.length === 0) {
return null;
} else {
return findFirstNextChecked($('.driverVisibility', next[0]));
}
}
}
function findFirstPrevChecked(el) {
if(el.is(':checked')) {
return el;
} else {
var prev = el.closest("tr").prev(); // prev() returns a collection
if (prev.length === 0) {
return null;
} else {
return findFirstPrevChecked($('.driverVisibility', prev[0]));
}
}
}
// maintains the color of the label of a checkbox
function colorTheLabel(visible, el) {
el.closest('tr').toggleClass('hideFromSankey', !visible);
}
/*
* @param {jQuery} linkDiv -- the jQuery object of the static part of the drop-down
* @param {Function} changeListener -- optional function to be called when the user changes
* the dropdown state; will be called with 1 argument, the new selectedIndex
* @param {Array of String} options -- elements to show in the dropdown
* @param {int} selectedIndex -- which element should be selected by default; if null, defaults to 0
* @param {String} popupCssClass -- classname to be applied to the popup that appears when the user clicks the
* link. If null, we'll default to right-aligning the popup to the link. If not-null, no horizontal positioning,
* is done by default (the only positioning will be that the top of the popup is vertically aligned with the bottom
* of the link)
*
*/
HeaderDropdown = function(linkDiv, changeListener, options, selectedIndex, popupCssClass) {
var css = {};
if(!popupCssClass) {
css = {
right: linkDiv.length > 0 ? linkDiv.parent().width() - linkDiv.position().left - linkDiv.width() - 16 + parseInt(linkDiv.css('padding-left')) : 0
};
}
var self = this,
table = $(""),
popupDiv = $("", {
"class": "cwHeaderDropdown",
css: css
})
.addClass(popupCssClass != undefined ? popupCssClass : "")
.append("")
.append(table)
;
var form = $("").appendTo(popupDiv);
linkDiv
.click(function() {
if (popupDiv.is(':visible')) {
popupDiv.hide();
} else {
popupDiv
.show()
.css({top: linkDiv.position().top + linkDiv.height() + parseInt(linkDiv.css('padding-top')) + parseInt(linkDiv.css('padding-bottom')) + 1});
setTimeout(function() {$("body").one("click", hidePopup)}, 100);
}
return false;
})
.parent().append(popupDiv);
selectedIndex = selectedIndex || 0;
setOptions(options || []);
/**
* @param {Array} newOptions the new value for the array of options
* @param {int} newSelectedIndex index of the initial selection
* @param {Array} newOptionsMetaData meta-data objects for each option.
* newOptionsMetaData[i] contains key-value metadata for
* option[i]. It would be nice to do this by mapping options
* meta-data, but the options are display strings for which
* we sometimes have duplicates (though that's undesirable).
*/
function setOptions(newOptions, newSelectedIndex, newOptionsMetaData) {
options = newOptions;
table.empty();
table.toggleClass("admin", !!(window.tf && tf.isAdmin && tf.isAdmin()));
var editMode = !!(//jQuery requires an actual boolean
window &&
window.tf &&
window.tf.editMode &&
newOptionsMetaData &&
window.tf.urls &&
window.tf.urls.periodVisibleEndpoint);
table.toggleClass("editMode", editMode);
if(editMode) {
var header = $("
");
table.append(header);
header.append($(" "));
header.append($(" Show ").attr("title", "'unchecked' == inconsistent breakdown, hide in view"));
header.append($("Display ").attr("title", "'unchecked' == inconsistent breakdown, hide in view"));
};
$.each(options, function(i, option) {
var tr = $("
by Default");
var a = $("", {
text: option,
href: '#'
});
var td = $(" ", {
"class": 'periodLabel',
click: internalClickListener
});
table.append(tr.append(td.append(a)));
var hidden = !!(
newOptionsMetaData &&
newOptionsMetaData[i] &&
newOptionsMetaData[i].hideFromSankey);
colorTheLabel(!hidden, td);
if(editMode && newOptionsMetaData[i] && newOptionsMetaData[i].code) {
tr.toggleClass("editMode", editMode);
var check = $("");
tr.append($(" ").append(check));
check.attr("checked", !hidden)
.attr("title", "'unchecked' == inconsistent breakdown, hide in view")
.attr("code", newOptionsMetaData[i].code);
check.click(function(event){
var el = $(this);
var code = el.attr("code");
var visible = el.is(':checked');
var nearbyChecked = findFirstNextChecked(el) || findFirstPrevChecked(el);
if(nearbyChecked) {
if(!visible && $(':radio',tr).is(':checked') ) {
// if the user deselects the default period simulate user action picking up another one
$(':radio', nearbyChecked.closest("tr")).click();
}
} else {
colorTheLabel(!visible, el);
return false; // revert checkbox state -- do not allow the user to deselect all periods
};
colorTheLabel(visible, el);
$.ajax({
type: "POST",
url: window.tf.urls.periodVisibleEndpoint+"&periodCode="+encodeURIComponent(code)+"&isVisible="+visible,
success: function(data){
if(data.success==false){
alert(data.message);
//revert checkbox state on failure
el.prop('checked', !visible);
colorTheLabel(!visible, el);
return false;
}
}
});
$(':radio', tr).attr("disabled", !visible);
event.stopPropagation();
});
var displayByDefault = $("");
tr.append($(" ").append(displayByDefault));
displayByDefault.attr("checked", i === newSelectedIndex)
.attr("code", newOptionsMetaData[i].code)
.attr("disabled", !$('.driverVisibility', tr).is(':checked'));
displayByDefault.click(function(event){
var el = $(this)
var code = el.attr("code");
if( $('.driverVisibility', el.closest("tr")).is(':checked') ) {
$.ajax({
type: "POST",
url: window.tf.urls.periodDefaultEndpoint+"&periodCode="+encodeURIComponent(code),
success: function(data){
if(data.success==false){
alert(data.message);
colorTheLabel(!visible, el);
return false; //revert radio button state on failure
}
}
});
} else {
event.stopPropagation();
colorTheLabel(!visible, el);
return false; //revert radio button -- cannot make non-displaying period default
}
event.stopPropagation();
});
}
if(newOptionsMetaData && newOptionsMetaData[i] && newOptionsMetaData[i].code)tr.attr("code", newOptionsMetaData[i].code)
if(newOptionsMetaData && newOptionsMetaData[i] && newOptionsMetaData[i].id)tr.attr("id", newOptionsMetaData[i].id)
});
setSelected(Math.max(0, Math.min(newSelectedIndex === undefined ? selectedIndex : newSelectedIndex, options.length-1)));
}
function setSelected(newSelected, fireEvent) {
selectedIndex = newSelected;
linkDiv.text(options[selectedIndex]);
if(fireEvent)
changeListener(selectedIndex);
}
function getSelected() {
return selectedIndex;
}
function internalClickListener() {
var clickedElement = this;
table.find("td.periodLabel").each(function(i, el) {
if(el == clickedElement) {
setSelected(i, 1)
hidePopup();
}
});
return false;
}
function hidePopup() {
popupDiv.hide();
}
$.extend(self, {
setOptions: setOptions,
setSelected: setSelected,
getSelected: getSelected,
popupDiv: popupDiv
});
}
})();
;;
// @include lib/jquery-ui-1.10.3.min.js
// @include lib/raphael-min.js
// @include widgetCommon/styledDialog.js
// @include common.js
// @include infrastructure/unitUtil.js
// @include infrastructure/seriesUtil.js
// @include tf/ga.js
// @include widgetCommon/chartPopup.js
// @include widgetCommon/modelChartPopup.js
// @include widgetCommon/headerDropdown.js
/*
This file holds classes for client-side sankey rendering using the Raphael library (SVG/VML).
SankeyDiagram is the main class that external files need to interact with. There are also a couple
"private" classes in this file: Node and Column, which are used internally by SankeyDiagram.
*/
// enclose in function for minification
;(function() {
// various states that a node can be in; should match the values in the NodeState.java enum
var NODE_STATE_ROOT = 0;
var NODE_STATE_NORMAL = 1
var NODE_STATE_NORMAL_SUBDIVISION = 2;
var NODE_STATE_SELECTED_SUPERDIVISION = 3;
var NODE_STATE_SELECTED_DIVISION = 4;
var NODE_STATE_REVERSE = 5;
// various states the sankey as a whole can be in; should match the values in the SnakeyState.java enum
var SANKEY_STATE_NORMAL = 0;
var SANKEY_STATE_SUPERDIVISION_SELECTED = 1;
var SANKEY_STATE_DIVISION_SELECTED = 2;
var SANKEY_STATE_SUBDIVISION_SELECTED = 3;
/*
* when rendering two polygons that meet at an edge, if you set the end of
* one equal to the start of the other, you map see a little gap between the polygons
* because of rounding errors and rasterization. To avoid this, have each polygon extend
* a little farther than its nominal dimensions by adding OVERLAP_FUDGE_FACTOR to each dimension
*/
var OVERLAP_FUDGE_FACTOR = 0.5;
var DEFAULT_NODE_RENDER_OPTIONS ={
cash:{}, //cash gets its defaults from positive unless otherwise specified or passed in
positive:{
"stroke-width": 0, //curve outline width
stroke:'transparent', //curve outline color
superFill:"0-#93b793-#61AD61:30", //curve fill for first level arms, like superdivisions
subFill:"#61AD61", //curve fill for 2nd level arms like sub divisions
extenderFill:"0-#61AD61-#61AD61:20-#ededed:21",
minTubeWidth: 1 //minimum tube width
},
negative:{
"stroke-width": 0, //curve outline width
stroke:'transparent', //curve outline color
superFill:"0-#b27575-#A42D2D:30", //curve fill for first level arms, like superdivisions
subFill:"#A42D2D", //curve fill for 2nd level arms like sub division
extenderFill:"0-#A42D2D-#A42D2D:20-#ededed:21",
minTubeWidth: 1 //minimum tube width
},
root:{
"stroke-width": 0, //curve outline width
stroke:'transparent', //curve outline color
positiveFill: "0-#ededed:50-#93b793",
negativeFill: "0-#ededed:50-#b27575",
fill:"#ededed",
arrowThicknessPercent:1 //arrow thickness as percentage of the very left column (root node)
},
reversed: {
"stroke-width": 0, //curve outline width
stroke:'transparent', //curve outline color
superFill:"0-#A42D2D-#ededed", //curve fill for first level arms, like superdivisions
subFill:"#A42D2D", //curve fill for 2nd level arms like sub division
extenderFill:"0-#A42D2D-#A42D2D:20-#ededed:21",
minTubeWidth: 1 //minimum tube width
},
selectedTextBoxMinHeight:35,
animationDuration:200
}
var RENDER_OPTIONS={};
var SANKEY_INITIAL_LOAD_DATA;
// =============================================================
// ======================== SANKEY DIAGRAM =====================
// =============================================================
/**
* Creates a client-side rendered sankey diagram.
*
* @constructor
* @param {String} divId id of the div in which the sankey should be created. The div must already exist in the page.
* @param {Object} params key/value pairs of configuration options for the sankey. can be null to use all defaults. Accepted parameters are:
* {int} rootTubeWidth: thickness in pixels of the arrow at the root end of the sankey
* {int} heightForText: the lineheight in pixels we should expect for division labels
* {int} width: width in pixels of the area available for sankey
* {int} height: height in pixels of the area available for sankey
* {int} widthForRHSColumn: space in pixels to leave for the RHS column of $/% numbers
* {int} minArmSpacing: minimum vertical space in pixels to leave between sankey arms
* {int} maxArmSpacing: maximum vertical space in pixels to leave between sankey arms
* {int} rootVerticalPosition: distance in pixels from the top of the sankey-containing div to the midpoint of the root arrow
* {String} selectedPeriod: period code for the default selected period
* {int} inspectorLeftEdge: the horizontal distance in pixels from the left of the sankey-containing div to the left of the inspector div.
* Used to make sure the selected division of the sankey can extend its arm to be flush with the inspector
* {boolean} renderPartialSums: if true, red/green brackets will be displayed on the RHS to show the sums of various groups of arms
* {boolean} editableDivisionNames: if true, allow the user to edit
* division names
* {function(element, id, options)} nameDecoratorCallback: a
* callback for making names editable, taking
* {jQuery} element: part of the document to make editable
* {String} id: an id that identifies the name on the back-end
* {object} options: we will set options.extraData.type to say
* what type of name we are editing.
* @param {ForecastModel} forecastModel the model that should be visualized by this sankey
* @param {PeriodSelectionManager} periodSelectionManager
*/
_SankeyDiagram = function(elemOrSelector, params, startingForecastModel, periodSelectionManager, initialLoadData, root, containerDimensions) {
var selfSankey = this,
div = $(elemOrSelector),
// Hack: give it the full viewport width/height to get the sankey to render better and then scale it down later
paper = Raphael(div[0], window.innerWidth, window.innerHeight),
nodes = {}, // map from node identifiers to the Node objects themselves
currentDynamicData; // stores the dynamicData object that was last rendered by applyDynamicData
div.hide() // Hide the chart until it's done rendering
window.paper = paper;
SANKEY_INITIAL_LOAD_DATA = initialLoadData;
var svgHeight = div.height();
// this defines the default params, and overrides them with any params passed as an argument to the ctor
params = $.extend({
rootTubeWidth: 55,
heightForText: 16,
// Hack: give it the full viewport width/height to get the sankey to render better and then scale it down later
width: window.innerWidth,
height: window.innerHeight,
widthForRHSColumn: 90,
minArmSpacing: 14,
maxArmSpacing: 35,
rootVerticalPosition: 90,
selectedPeriod: periodSelectionManager ? periodSelectionManager.getSelectedPeriod() : null,
inspectorLeftEdge:800,
renderPartialSums:true
},
params || {});
var forecastModel;
var answerUnit; // will be set in setModel
var scaleFactorForDisplay; // result of a call to UnitUtil.calculateScaleFactorForDisplay indicating how all sankey arms should be scaled; will be set in setModel
// clicking on the root svg/vml elements is buggy in IE8, so instead put a regular div on top that
// can catch clicks on the root
div.append($('', {
css: {
background: '#000000',
position: 'absolute',
top: params.rootVerticalPosition - params.rootTubeWidth/2,
width: 30,
height: params.rootTubeWidth,
opacity: 0,
zIndex: 2,
cursor: 'pointer'
},
click: function() {
selfSankey.clickListener(null, false);
}
}));
RENDER_OPTIONS = $.extend(true, {}, DEFAULT_NODE_RENDER_OPTIONS, params.renderOptions || {});
RENDER_OPTIONS.cash = $.extend(true, {}, RENDER_OPTIONS.positive, DEFAULT_NODE_RENDER_OPTIONS.cash, params.renderOptions && params.renderOptions.cash || {});
params.getWidth = function(estimatedHeight){
return Math.max(div.width(), estimatedHeight || 0)
}
selfSankey.params = params;
var headerDropdown = new HeaderDropdown(
div.find(".headerDropdown"),
function(selectedIndex) {
updateValueText();
});
var rootNode;
// create a text-measuring function, which will be needed by the Nodes
var textMeasurerDiv = $('', {'class': 'textMeasurer'});
div.append(textMeasurerDiv);
var textMeasurer = function(text, fontSize) {
return textMeasurerDiv
.text(text)
.css({fontSize: fontSize + 'px'})
.width();
};
if(periodSelectionManager)
periodSelectionManager.addChangeListener(function() {
params.selectedPeriod = periodSelectionManager.getSelectedPeriod();
setModel(forecastModel);
// if the previously selected id doesn't exist in the new model,
// we reset the sankey to make sure that e.g. the inspector respects
// the new state of the sankey
if(params.selected && !nodes[params.selected]) {
selfSankey.clickListener(null, false);
}
});
setModel(startingForecastModel);
// ==============================
// ========== METHODS ==========
// ==============================
function setModel(newForecastModel) {
if(forecastModel)
forecastModel.removeChangeListener(modelChangeListener);
forecastModel = newForecastModel;
div
.find('.sshDivTblSumRect,.sshDivTblSum')
.remove();
//var tree = tree || forecastModel.companyData.md.treeData;
// per TREF-3106, get the largest value present anywhere in the tree, and use that
// to determine how all units should be scaled
try {
scaleFactorForDisplay = forecastModel.getLargestScaleFactor(periodSelectionManager.getSelectedPeriod());
} catch(e) {
scaleFactorForDisplay = undefined;
}
// fb2788: older models may not have a sankeyDecimalPrecision field set; if not, we calculate it here on the client
// since the default value depends on the value of the sankey root answer
try {
// if sankeyDecimalPrecision was null on page load, set a flag so we know we need to recalculate it for each model
if(!tf.modelPreferences || tf.modelPreferences.sankeyDecimalPrecision === null || tf.modelPreferences.sankeyDecimalPrecision === undefined)
tf.recalculateSankeyDecimalPrecision = true;
if(tf.recalculateSankeyDecimalPrecision && scaleFactorForDisplay) {
// scale the value to how it will be displayed, e.g. a value of 100 for "100mil" instead of 1e8
var scaledLargestValue = scaleFactorForDisplay.realValue * scaleFactorForDisplay.displayNumericScale;
// this logic forces the sankey decimal precision to be exactly enough decimals to match the required general # of significant figures,
// for the largest value
tf.modelPreferences = tf.modelPreferences || {};
tf.modelPreferences.sankeyDecimalPrecision = Math.max(0, (tf.decimalPrecision || 3) - 1 - Math.floor(Math.log(Math.abs(scaledLargestValue))/Math.log(10)));
// this if block should only be called once
tf.recalculateSankeyDecimalPrecision = false;
}
} catch(e) {}
answerUnit = forecastModel.companyData.getSankeyRootAnswer().answerUnit;
if (!answerUnit) answerUnit = "";
var answerUnitOptionDollar = "Dollars ($)";
var answerUnitOptionPercent = "Percent";
if (forecastModel.companyData.il.provider == 'PFIZER') {
answerUnitOptionDollar += " NPV";
answerUnitOptionPercent = "NPV " + answerUnitOptionPercent;
}
headerDropdown.setOptions(
[answerUnit == "$" ? answerUnitOptionDollar : (answerUnit == "" ? "Units" : answerUnit), answerUnitOptionPercent]);
$.each(nodes, function(id, node) {
node.clear();
});
nodes = {};
paper.clear();
if(SeriesUtil.getValueIdentifiers(root)[params.selectedPeriod]) {
rootNode = new Node(
root,
forecastModel,
paper,
div,
null, //parent
nodes,
{click: createNodeClickListener, mouseover: createNodeMouseOverListener, mouseout: createNodeMouseOutListener},
params,
answerUnit,
scaleFactorForDisplay
);
$.each(paper.canvas.childNodes, function(id, node) {
node.style['cursor'] = 'pointer';
});
$('a.sshDivTblA', div).each(function () {
if (params.nameDecoratorCallback && params.editableDivisionNames) {
var collectionId = $(this).data('datum').identifier;
// first, check divisions
var diffChangeType = findSankeyDiffChangeType('Division', collectionId, '*', false);
// if not there, check super divisions
if (_.isEmpty(diffChangeType)) {
diffChangeType = findSankeyDiffChangeType('SuperDivision', collectionId, '*', false);
};
if (diffChangeType.direct) {
$(this).addClass('directChange')
};
if (diffChangeType.indirect) {
$(this).addClass('indirectChange')
};
var divisionDatum = forecastModel.companyData.getDivisionDatum(collectionId);
// If this division is linked 1-1 to a stream, then update one along with the other
// for this you need to link the two models so they get the save name update
var updateLinked = (divisionDatum !== undefined &&
divisionDatum.streams !== undefined &&
divisionDatum.streams.length == 1);
params.nameDecoratorCallback(false, $(this), collectionId, {
type: 'DIVISION',
currentDivision: collectionId,
linkedModel: updateLinked ? divisionDatum.streams[0].identifier : undefined
});
}
});
forecastModel.addChangeListener(modelChangeListener);
// render the initial sankey
applyDynamicData(calculateDynamicData());
updateValueText();
} else {
// for some reason we don't have a sankey, so show an error message
var t = paper.text(150, 70, 'No Breakdown Available')
.attr({'font-size': 20, fill: '#fff'});
}
};
selfSankey.setModel = setModel;
function modelChangeListener(isFinal, driverSetChanged) {
updateValueText();
if(isFinal) {
var dynamicData = calculateDynamicData();
var duration = 500;
$.each(nodes, function(id, node) {
node.bloop(duration, dynamicData, params.inspectorLeftEdge);
});
setTimeout(function() {
applyDynamicData(dynamicData, 500);
}, duration);
}
}
/*
* Methods for applying dynamic data
*/
/**
* Updates the % value text on the RHS of sankey arms.
*/
function updateValueText() {
var selectedPeriod = periodSelectionManager ? periodSelectionManager.getSelectedPeriod() : null;
var denominator = calculateDenominatorForPercentages(selectedPeriod);
var useAbsolute = !headerDropdown.getSelected();
$.each(nodes, function(id, node) {
node.updateValueText(selectedPeriod, useAbsolute, denominator);
});
renderPartialSums(currentDynamicData, useAbsolute)
}
/**
* Updates the state of the sankey to match the given dynamicData.
*
* @param {Object} dynamicData
* @param {int} duration number of ms over which the sankey should animate to the new state. Can
* be 0 or null/undefined if the sankey should change immediately rather than animate.
* @param {Function} callback will be called at the end of the animation, or immediately if there is no animation.
* Can be null.
*/
function applyDynamicData(dynamicData, duration, callback) {
duration = duration || 0;
currentDynamicData = dynamicData;
rootNode.applyDynamicNodeData(dynamicData, duration);
// the white "DIVISION" label at the top needs to be left aligned with the division labels
div
.find('.sshDivTblHeader.sshDivTblLHS')
.animate({left: dynamicData.columns[NODE_STATE_NORMAL].getRightEdge() + 10}, duration);
// the white "PERCENT" / "DOLLARS" dropdown on the top right needs to be right aligned with the number column
div
.find('.sshDivTblHeader.sshDivTblRHS')
.add(headerDropdown.popupDiv)
.animate({right: dynamicData.params.getWidth() - dynamicData.params.textWidth - 12}, duration);
renderPartialSums(dynamicData, !headerDropdown.getSelected(), duration);
if(callback) {
if(duration) {
setTimeout(callback, duration);
} else {
callback();
}
}
paper.setSize(params.width, dynamicData.maxY+30);
if(params.heightChangeEvent)
params.heightChangeEvent();
}
/**
* gives the total height of the sankey visualization
* @return {Number}
*/
selfSankey.height = function() {
return currentDynamicData ? currentDynamicData.maxY : null;
};
/**
* gives the y coordinate (relative to the main sankey div) of any sankey arm that's extended to
* meet the insector. Can return null if no arm is extended.
* @return {Number}
*/
selfSankey.extenderY = function() {
return currentDynamicData ? currentDynamicData.extenderY : null;
};
/**
* Draws the red/green partial sum brackets to the RHS of the sankey values
* @param {Object} dynamicData
* @param {boolean} useAbsolute whether to use absolute or % values
* @param {int} duration duration of the animation, or null if no animation
* and just update immediately
*/
function renderPartialSums(dynamicData, useAbsolute, duration) {
if(dynamicData.params.renderPartialSums==false)return;
duration = duration || 0;
var numbersColumnRightEdge = dynamicData.params.textWidth;
var lineHeight = 16;// TODO textContainer.css('lineHeight');
var selectedPeriod = periodSelectionManager ? periodSelectionManager.getSelectedPeriod() : null;
var denominator = calculateDenominatorForPercentages(selectedPeriod);
// fade out any old partial sums
div
.find('.sshDivTblSumRect,.sshDivTblSum')
.addClass('oldPartialSum') // tag the old partial sums with a class so we can easily remove them without accidentally removing the new partial sums
.fadeOut(duration, function() {
div
.find('.oldPartialSum')
.remove();
});
// create new partial sum divs
$.each(dynamicData.partialSums, function(i, partialSum) {
div
.append($('
')
.append($('', {
'class': 'sshDivTblVal clientSide'
}))
;
} else {
// FB2465
var anchor = $('', {
'class': 'sshDivTblA clientSide',
href: '#',
text: datum.name
}).data('datum', datum);
function isDivisionWithNoDriver(datum){
if (datum.componentBreakdowns && datum.componentBreakdowns.length > 0 && datum.componentBreakdowns[0].components.length > 0) {
return false; // not a division
}
var division = datum.identifier;
var streams = forecastModel.companyData.getStreams(division);
if (stream) {
return false;
}
// has drivers
for(var s in streams){
var stream = streams[s];
if(stream.categories) {
for(var c in stream.categories) {
var category = stream.categories[c];
if(category.driverIds && category.driverIds.length)
return false;
}
}
}
return true;
}
if (isDivisionWithNoDriver(datum)) {
anchor.addClass("greyedOutDivision");
}
var isAnswer = demoWidget.companyData.md.answers.some(function(e) { return e.answerCollection === datum.identifier })
if (isAnswer) {
textContainer = $(document.createDocumentFragment())
}
else {
textContainer = $('"),
communityHeader = $("", {text: "Community", "class": "checkboxDropdownHeader"}),
competitionHeader = $("", {text: "Competition", "class": "checkboxDropdownHeader"}),
benchmarksHeader = $("", {text: "Relevant Benchmarks", "class": "checkboxDropdownHeader"}),
noContentHeader = $("", {text: noContentNotification || "No competition data available", "class": "checkboxDropdownHeader"}),
otherUserCheckbox = $("")
.click(otherUserCheckboxClickListener),
otherUserDropdown = $("", {
change: otherUserDropdownChangeListener
}),
otherUsersLi = $("")
.append(otherUserCheckbox)
.append(otherUserDropdown),
followPeoplePrompt = $("", {html: "You can view people's forecasts here by following them. Find people to follow"}),
otherUserSelectedId;
div.append(list);
function setOptions(options) {
list.empty();
if(showFollowingDropdown)
otherUserDropdown
.empty()
.append($("", {text: "People you follow..."}))
.append($(""));
var inputId,
li,
hasCommunity,
hasCompetition,
hasBenchmarks,
hasOtherUsers;
list.append(communityHeader)
if(showFollowingDropdown)
list .append(otherUsersLi)
.append(followPeoplePrompt);
list .append(competitionHeader)
.append(benchmarksHeader);
$.each(options, function(optionIndex, option) {
if(optionIndex < 7) {
inputId = "widgetCheckbox-" + CheckboxDropdown.guid++;
li = option.type == ChartType.otherUser && showFollowingDropdown
? $("", {
title: option.toolTip,
text: option.text,
value: option.id
})
: $("", {
title: option.toolTip
})
// can't use the slick creation syntax for checkboxes b/o a bug:
// http://bugs.jquery.com/ticket/7264
.append($("")
.attr("id", inputId)
.prop("checked", !!option.selected)
.click(createClickListener(option.id))
)
.append($("", {
"for": inputId,
text: option.text,
css: {
color: option.color || "#333"
}
}));
if(option.type == ChartType.otherUser && showFollowingDropdown) {
otherUserDropdown.append(li);
hasCommunity = hasOtherUsers = 1;
} else if(option.type == ChartType.community
|| option.type == ChartType.comparisonOtherUser
|| option.type == ChartType.otherUser) {
// checkbox belongs in "community" section
communityHeader.after(li);
hasCommunity = 1;
} else if(option.type == ChartType.competition) {
competitionHeader.after(li);
hasCompetition = 1;
} else {
benchmarksHeader.after(li);
hasBenchmarks = 1;
}
}
});
if(!hasCompetition)
competitionHeader.remove();
if(!hasCommunity)
communityHeader.remove();
if(!hasBenchmarks)
benchmarksHeader.remove();
if(showFollowingDropdown) {
if(hasOtherUsers) {
followPeoplePrompt.remove();
otherUserDropdown
.append($(""))
.append($("", {
text: "Find more people..."
}));
} else
otherUsersLi.remove();
} else if(!options.length)
list.append(noContentHeader);
}
function createClickListener(id) {
return function(e) {
clickCallback(id, $(e.target).prop("checked"));
}
}
function otherUserDropdownChangeListener() {
var selectedIndex = otherUserDropdown.find(":selected").index(),
children = otherUserDropdown.children(),
length = children.length;
if(selectedIndex <= 1 || selectedIndex == length - 2) {
children.eq(0).prop("selected", true);
if(otherUserSelectedId) {
clickCallback(otherUserSelectedId, false);
otherUserCheckbox.prop("checked", false);
otherUserSelectedId = null;
}
} else if(selectedIndex == length - 1)
location.href = "people";
else {
otherUserSelectedId = children.eq(selectedIndex).val()
otherUserCheckbox.prop("checked", true);
clickCallback(otherUserSelectedId, true);
}
}
function otherUserCheckboxClickListener() {
if(otherUserSelectedId)
clickCallback(otherUserSelectedId, otherUserCheckbox.prop("checked"));
}
function showLoading() {
list
.empty()
.append($("", {
text: "Loading...",
css: {
textAlign: "center"
}
}));
}
$.extend(self, {
setOptions: setOptions,
div: div,
showLoading: showLoading
});
}
CheckboxDropdown.guid = 0;
;;
// lib/jquery-1.10.2.min.js
// @include lib/timer.js
// @include infrastructure/widgetRPC.js
// @include widgetCommon/checkboxDropdown.js
// @include infrastructure/chartWrapper.js
/**
* Manages fetching competition lines from the server, and registering them with a chart.
*
* @constructor
* @param {ChartWrapper} chartWrapper
* @param {Function} setCompetitionOptions a callback which takes a single Array argument, and is called to add competition options
* to a selection interface (e.g. CheckboxDropdown). The Array argument contains Objects, each of which has the fields: id (String),
* text (String), type (int, see ChartType), color (String), selected (boolean).
* @param {Function} isSelected CompetitionManager will call this to determine whether competition lines should be selected by default.
* Should take a single argument: a String id. Should return a boolean.
* @param {boolean} includeUser whether the logged-id user id should be passed in the RPC for fetching competition; this needs to be true
* if e.g. you want to fetch trendlines of people you're following, but you can leave it false for e.g. most widgets
* @param {boolean} includeBenchmarks if true, series tagged as Relevant Benchmarks will be included on the chart and competition
* dropdowns. Otherwise, only ones tagged as Direct Competition will appear.
* @param {boolean} caWeekly whether the community average we fetch should be the weekly one
* @param {String} caDate if caWeekly is true, this is the date for which the weekly average should be fetched. Should be a date
* formatted however the fetchCompetition RPC expects it
* @param {String} defaultComparisons the comparisons to show immediately by default on the chart. A comma-separated list of comparison identifiers.
* Often passed as a "comp" parameter to widgets. Only applies to the very first chart (i.e. the first call to prepForDriver).
* Can be null, in which case no comparisons are added by default.
*/
function CompetitionManager(chartWrapper, setCompetitionOptions, isSelected, includeUser, includeBenchmarks, caWeekly, caDate, defaultComparisons) {
var self = this,
// this 10000 is meaningless; timer will get stopped and reset anyway
fetchCompetitionTimer = $.timer(10000, fetchCompetition),
// these 3 fields are set in prepForDriver
companyData, // CompanyData
driverToLoad, // String; the driver we loaded competition for
mustHaveCommunity, // tells whether we must create a community line for the current driver
// map from driver id to the competition data we got from the RPC
competitionCache = {};
// these timers start in the running state, so need to stop them
fetchCompetitionTimer.stop();
/**
* Tells the competition manager to prepare for a particular driver by fetching its competition lines.
*
* @param {CompanData} newCompanyData
* @param {String} driver the newly selected driver
* @param {boolean} needImmediately if true, the RPC to fetch competition will be fired off immediately.
* If false, we'll wait 2 seconds to fire the RPC, and if another prepForDriver call comes during that 2s,
* the original RPC will be canceled. The latter is useful to prevent lots of RPCs while scrubbing drivers
* @param {boolean} newMustHaveCommunity if true, we'll ensure that the competition includes a community
* line. If there was no community line returned by the server, we'll create one whose values are identical
* to the Trefis values.
*/
function prepForDriver(newCompanyData, driver, needImmediately, newMustHaveCommunity) {
companyData = newCompanyData;
driverToLoad = driver;
mustHaveCommunity = newMustHaveCommunity;
fetchCompetitionTimer.stop();
if(competitionCache[driver])
doUpdateCompetition();
else if(needImmediately || defaultComparisons || caWeekly)
fetchCompetition();
else
fetchCompetitionTimer.reset(2000);
}
/**
* Fires the RPC to fetch competition from the server. Uses driverToLoad to determine which competition lines to load.
* When the RPC returns, we'll save the data in competitionCache and then call doUpdateCompetition.
*/
function fetchCompetition() {
fetchCompetitionTimer.stop();
var user = getLoggedInUser(),
driver = driverToLoad;
WidgetRPC.rpc({
type: "GET",
url: getHost()+"/servlet/FlexService/loadCompare",
data: {
driver: driver,
userId: includeUser && user ? user.userId : undefined,
caWeekly: caWeekly || undefined,
caDate: caDate
},
success: function(data, status) {
if(data) {
competitionCache[driver] = data.chartData;
if(driver == driverToLoad)
doUpdateCompetition();
}
},
error: function() {
defaultComparisons = null;
}
});
}
/**
* Updates the chart and competition dropdowns.
* Uses driverToLoad and the data in competitionCache[driverToLoad], which must be non-null.
*/
function doUpdateCompetition() {
var nextColorIndex = 0,
options = [],
sawCommunityLine,
firstCommunity = -1,
identifier,
isCommunity,
idToRegister,
color,
selected;
if(defaultComparisons)
defaultComparisons = defaultComparisons.split(",");
$.each(competitionCache[driverToLoad], function(i, competitionDatum) {
// cache some information on this competition datum, for minification
identifier = competitionDatum.identifier;
isCommunity = competitionDatum.type == ChartType.community;
idToRegister = isCommunity
? "community"
: "comp" + i;
selected = (isCommunity && (isSelected(idToRegister) || caWeekly)) || (defaultComparisons && $.inArray(idToRegister, defaultComparisons) >= 0);
// check if we want to include this competition datum
if(isCommunity
|| (includeUser && competitionDatum.type == ChartType.otherUser)
|| competitionDatum.type == ChartType.competition
|| (includeBenchmarks && competitionDatum.type == ChartType.benchmark)) {
color = chartWrapper.registerSeries(
idToRegister,
isCommunity
? (caWeekly ? "Members' Forecast" : "Community")
: competitionDatum.title,
competitionDatum,
nextColorIndex++);
options.push({
id: idToRegister,
text: competitionDatum.title,
color: color,
type: competitionDatum.type,
selected: selected
});
if(selected)
chartWrapper.setSeriesVisible(idToRegister, true);
}
});
// if we must have a community line, and we didn't get one from the server, we create one here
if (!sawCommunityLine && mustHaveCommunity) {
chartWrapper.registerSeries(
"community",
"Community",
// copy the default Trefis data to make the community data
chartWrapper.copyChartData(
companyData.getChartData(driverToLoad),
"Community",
ChartType.community),
nextColorIndex);
}
// set the options in the dropdown
setCompetitionOptions(options);
defaultComparisons = null;
caWeekly = 0;
}
$.extend(self, {
prepForDriver: prepForDriver
});
}
;;
// @include lib/underscore.js
/**
* Utility functions for the benchmark search widget
*/
function getSeriesType(doc) {
var types = [];
if(doc.isSuperDivision) {
types.push('Super Division');
}
if(doc.isDivision) {
types.push('Division');
}
if(doc.isStream) {
types.push('Stream');
}
if(doc.isCategory) {
types.push('Category');
}
if(doc.isDriver) {
types.push('Driver');
}
return types;
}
function getBenchmarkTooltip(doc) {
var seriesType = getSeriesType(doc)
return 'Series: '+ (seriesType.length ? seriesType.join(', ') : 'None') +', '+
'Provider: '+ getBenchmarkProvider(doc)
}
function getBenchmarkProvider(doc) {
return doc.providerString ? doc.providerString.charAt(0).toUpperCase() + doc.providerString.slice(1) : 'None'
}
/* Take the document object and generate a descriptor string used in the popups describing
* in more depth the series information. */
function getSeriesDescription(doc) {
var types = getSeriesType(doc);
if(types.length > 0) {
var typeList = types.join(',');
}
var thumbnailUrl = getDriverThumbnailUrlFromDoc(doc);
var hoverTemplate = $('#benchmark-hover-template');
var hoverElement = $($.parseHTML(_.template(hoverTemplate.html(), {
title: doc.title,
modelName: doc.modelName,
ticker: doc.lead_symbol,
provider: getBenchmarkProvider(doc),
types: types.length > 0?types.join(','):'None',
breadcrumbs: doc.breadcrumbs && doc.breadcrumbs.length>0?doc.breadcrumbs.join('\n'):''
})));
var chartImage = hoverElement.find(".chartImage");
chartImage.css("backgroundImage", "url(" + thumbnailUrl + ")");
var chartTitle = hoverElement.find(".chart-title");
chartTitle.text(getItemDisplayNameNoUnits(doc));
return hoverElement;
}
function getDriverThumbnailUrlFromDoc(doc) {
var driverData = getDriverDataFromDoc(doc);
var streamDriverUtil = new StreamDriverUtil(window.demoWidget.modelManager, window.demoWidget.periodSelectionManager, window.demoWidget.companyData, null);
var thumbnailUrl = streamDriverUtil.driverThumbnailUrl(null, driverData, 110, 85);
// console.log("thumbnailUrl = " + thumbnailUrl);
return thumbnailUrl;
}
function getDriverDataFromDoc(doc){
var trefisData = doc["trefisData"];
//trefisData is json stored as a string in solr, here we parse the string back to json
var itemChartData = $.parseJSON(doc.trefisData).chartData[0];
itemChartData['array'].sort(compareDataUsingPeriod);
var driverData = [];
$.each(itemChartData['array'], function( index, value ) {
driverData.push(value["Value"]);
});
return driverData;
}
function compareDataUsingPeriod(dataA, dataB) {
return tf.period.sortOrder(dataA["Year"]) - tf.period.sortOrder(dataB["Year"]);
}
/**
* Displays what plottable data is in each period for the chart
* @param doc
* @returns {string}
*/
function getItemPeriodsToDisplay(doc){
if (!doc.periodData){
return;
}
var str = '';
doc.periodData.periods.forEach(function(period,i,periods){
if (period.isPlottable){
str+=period.label+' '+period.counts+', ';
}
});
return str.replace(/(^\s*,)|(,\s*$)/g, '');
}
function getItemDisplayName(doc){
var unit = UnitUtil.calculateScaleFactorForDisplay(doc.unit, doc.scaleFactor).unit;
if(unit)unit="("+unit+") ";
var postfix = doc['lead_symbol'];
if(postfix =="")
postfix = doc['modelName'];
return unit+doc.title+" for "+postfix
}
function getItemDisplayNameNoUnits(doc){
var postfix = doc['lead_symbol'];
if(postfix =="")
postfix = doc['modelName'];
return doc.title+" for "+postfix;
}
function getInfo(doc){
var infoElement = getSeriesDescription(doc);
var breadcrumb;
if(doc.isDriver){
breadcrumb = doc.driverBreadcrumb
}else if (doc.isCategory){
breadcrumb = doc.categoryBreadcrumb
}else if (doc.isStream){
breadcrum = doc.streamBreadcrumb
}
if(breadcrumb)
infoElement.append($(" "));
return infoElement;
}
;;
// lib/jquery-1.10.2.min.js
// @include lib/jquery-ui-1.10.3.min.js
// @include infrastructure/unitUtil.js
// @include widgetCommon/benchmarkSearch.js
// @include tf/globalAutocomplete.js
$(function() {
// Renders the list of results from the search
if($.widget && $.custom && $.custom.catcomplete) {
$.widget( 'custom.benchmarksearch', $.custom.catcomplete, {
_resizeMenu: function() {
if (this.options.position && this.options.position.of instanceof jQuery)
this.menu.element.outerWidth(this.options.position.of.width());
else
this._super('_resizeMenu');
},
/**
* It seems that this method only gets called if there are
* results - however occasionally, i have seen it called with an empty array - unsure why.
* If you need to catch 'no results' situations, bind to the autocomplete response
* event.
*
* @param ul
* @param response
* @private
*/
_renderMenu: function( ul, response ) {
var self = this;
// any processing of data
if (this.options.processResults && response.length){
self.searchResults = this.options.processResults.call(this,response)
} else {
self.searchResults = response;
}
// Filter out
if(this.options.filterResults && self.searchResults.length){
self.searchResults = this.options.filterResults.call(this,self.searchResults);
}
// Sort
if (this.options.sortResults && self.searchResults.length){
self.searchResults = this.options.sortResults.call(this,self.searchResults);
}
var currentCategory = '';
$.each( self.searchResults, function( index, item ) {
var prefix = '';
if ( item.category != currentCategory ) {
currentCategory = prefix = item.category;
}
self._renderItem( ul, item, prefix);
});
ul.find( 'ac_odd' ).removeClass( 'ac_odd' )
.end()
.find( 'li.ui-autocomplete-item:odd' ).addClass( 'ac_odd' );
if(this.options.postRenderCallback)
this.options.postRenderCallback.call(self,ul, self);
},
_renderItem: function(ul, item, prefix) {
ul.removeClass().addClass('list-group')
var ok = true;
if(this.options.preRenderItemCallback){
ok = this.options.preRenderItemCallback.call(this,item.doc);
}
if(!ok) return null;
item.widget=this; //for some reason this.options is not scoped in the select method, but ui.item is, so we'll attach the widget to the item for reference in select
var unit = UnitUtil.calculateScaleFactorForDisplay(item.doc.unit, item.doc.scaleFactor).unit;
if(unit)unit="("+unit+") "
// QUICK fix for certain scale factors... (BIL ) broken, but (BIL $) is fine...
unit = unit.replace(' )',')');
var postfix = item.doc['lead_symbol'];
if(postfix =="")
postfix = item.doc['modelName'];
var renderedItem= $('