Carpincho/src/gui_common/libs/RGraph/RGraph.bar.js
2026-09-27 23:33:38 -03:00

5043 lines
182 KiB
JavaScript

'version:2026-08-11 (7.21)';
//
// o---------------------------------------------------------------------------o
// | This file is part of the RGraph package - you can learn more at: |
// | |
// | https://www.rgraph.net |
// | |
// | RGraph is dual-licensed under the Open Source GPL license. This means |
// | that it's free to use for any purpose. The GPL license does have |
// | consequences on the license of the software that you include it in, |
// | however. If this is not desirable, then there's an inexpensive commercial |
// | license option available. See the RGraph website for more details. |
// o---------------------------------------------------------------------------o
RGraph = window.RGraph || {isrgraph:true,isRGraph: true,rgraph:true};
//
// The bar chart constructor
//
RGraph.Bar = function (conf)
{
var id = conf.id,
canvas = document.getElementById(id),
data = conf.data;
// Get the canvas and context objects
this.id = id;
this.canvas = canvas;
this.context = this.canvas.getContext('2d');
this.canvas.__object__ = this;
this.type = 'bar';
this.max = 0;
this.stackedOrGrouped = false;
this.isRGraph = true;
this.isrgraph = true;
this.rgraph = true;
this.uid = RGraph.createUID();
this.canvas.uid = this.canvas.uid ? this.canvas.uid : RGraph.createUID();
this.colorsParsed = false;
this.original_colors = [];
this.cachedBackgroundCanvas = null;
this.firstDraw = true; // After the first draw this will be false
this.stopAnimationRequested = false;// Used to control the animations
// Various config type stuff
this.properties =
{
backgroundBarsCount: null,
backgroundBarsColor1: 'transparent',
backgroundBarsColor2: 'transparent',
backgroundGrid: true,
backgroundGridColor: '#eee',
backgroundGridLinewidth: 1,
backgroundGridHsize: 20,
backgroundGridVsize: 20,
backgroundGridVlines: false,
backgroundGridHlines: true,
backgroundGridBorder: false,
backgroundGridAutofit: true,
backgroundGridAutofitAlign: true,
backgroundGridHlinesCount: 5,
backgroundGridDashed: false,
backgroundGridDotted: false,
backgroundGridDashArray: null,
backgroundGridThreedYaxis: true,
backgroundImage: null,
backgroundImageStretch: true,
backgroundImageX: null,
backgroundImageY: null,
backgroundImageW: null,
backgroundImageH: null,
backgroundImageAlign: null,
backgroundColor: null,
backgroundHbars: null,
backgroundBorder: false,
backgroundBorderLinewidth: 1,
backgroundBorderColor: '#aaa',
backgroundBorderDashed: false,
backgroundBorderDotted: false,
backgroundBorderDashArray: null,
marginTop: 35,
marginBottom: 35,
marginLeft: 35,
marginRight: 35,
marginInner: 5,
marginInnerGrouped: 1,
labelsIngraph: null,
labelsIngraphFont: null,
labelsIngraphSize: null,
labelsIngraphColor: null,
labelsIngraphBold: null,
labelsIngraphItalic: null,
labelsIngraphOffsetx: 0,
labelsIngraphOffsety: 0,
labelsAbove: false,
labelsAboveDecimals: 0,
labelsAboveSize: null,
labelsAboveColor: null,
labelsAboveBold: null,
labelsAboveItalic: null,
labelsAboveFont: null,
labelsAbovePoint: '.',
labelsAboveThousand: ',',
labelsAboveBackground:'transparent',
labelsAboveAngle: null,
labelsAboveOffset: null,
labelsAboveOffsetx: 0,
labelsAboveOffsety: 0,
labelsAboveUnitsPre: '',
labelsAboveUnitsPost:'',
labelsAboveFormatter:null,
yaxis: false,
yaxisLinewidth: 1,
yaxisColor: 'black',
yaxisTickmarks: true,
yaxisTickmarksCount: null,
yaxisTickmarksLastTop: null,
yaxisTickmarksLastBottom: null,
yaxisTickmarksLength: 3,
yaxisScale: true,
yaxisScaleMin: 0,
yaxisScaleMax: null,
yaxisScaleUnitsPre: '',
yaxisScaleUnitsPost: '',
yaxisScaleDecimals: 0,
yaxisScalePoint: '.',
yaxisScaleThousand: ',',
yaxisScaleRound: false,
yaxisScaleFormatter: null,
yaxisLabelsSpecific: null,
yaxisLabelsCount: 5,
yaxisLabelsOffsetx: 0,
yaxisLabelsOffsety: 0,
yaxisLabelsHalign: null,
yaxisLabelsValign: null,
yaxisLabelsFont: null,
yaxisLabelsSize: null,
yaxisLabelsColor: null,
yaxisLabelsBold: null,
yaxisLabelsItalic: null,
yaxisLabelsPosition: 'edge',
yaxisPosition: 'left',
yaxisTitle: '',
yaxisTitleAccessible: null,
yaxisTitleBold: null,
yaxisTitleSize: null,
yaxisTitleFont: null,
yaxisTitleColor: null,
yaxisTitleItalic: null,
yaxisTitlePos: null,
yaxisTitleX: null,
yaxisTitleY: null,
yaxisTitleOffsetx: 0,
yaxisTitleOffsety: 0,
yaxisTitleHalign: null,
yaxisTitleValign: null,
yaxisTitleAccessible: null,
xaxis: false,
xaxisPosition: 'bottom',
xaxisLinewidth: 1,
xaxisColor: 'black',
xaxisTickmarks: true,
xaxisTickmarksLength: 3,
xaxisTickmarksLastLeft: null,
xaxisTickmarksLastRight: null,
xaxisTickmarksCount: null,
xaxisLabels: null,
xaxisLabelsSize: null,
xaxisLabelsFont: null,
xaxisLabelsItalic: null,
xaxisLabelsBold: null,
xaxisLabelsColor: null,
xaxisLabelsFormattedDecimals: 0,
xaxisLabelsFormattedPoint: '.',
xaxisLabelsFormattedThousand: ',',
xaxisLabelsFormattedUnitsPre: '',
xaxisLabelsFormattedUnitsPost: '',
xaxisLabelsOffsetx: 0,
xaxisLabelsOffsety: 0,
xaxisLabelsHalign: null,
xaxisLabelsValign: null,
xaxisLabelsPosition: 'section',
xaxisLabelsSpecificAlign: 'left',
xaxisPosition: 'bottom',
xaxisLabelsAngle: 0,
xaxisTitle: '',
xaxisTitleBold: null,
xaxisTitleSize: null,
xaxisTitleFont: null,
xaxisTitleColor: null,
xaxisTitleItalic: null,
xaxisTitlePos: null,
xaxisTitleOffsetx: 0,
xaxisTitleOffsety: 0,
xaxisTitleX: null,
xaxisTitleY: null,
xaxisTitleHalign: null,
xaxisTitleValign: null,
textItalic: false,
textBold: false,
textColor: 'black',
textSize: 12,
textFont: 'Arial, Verdana, sans-serif',
textAccessible: false,
textAccessibleOverflow: 'visible',
textAccessiblePointerevents: false,
text: null,
title: '',
titleX: null,
titleY: null,
titleHalign: null,
titleValign: null,
titleFont: null,
titleSize: null,
titleColor: null,
titleBold: true,
titleItalic: null,
titleOffsetx: 0,
titleOffsety: 0,
titleSubtitle: '',
titleSubtitleSize: null,
titleSubtitleColor: '#aaa',
titleSubtitleFont: null,
titleSubtitleBold: null,
titleSubtitleItalic: null,
titleSubtitleOffsetx: 0,
titleSubtitleOffsety: 0,
colorsStroke: 'transparent',
colors: RGraph.getColors(),
colorsSequential: false,
colorsReverse: false,
grouping: 'grouped',
variant: 'bar',
variantSketchVerticals: true,
variantThreedXaxis: true,
variantThreedYaxis: true,
variantThreedAngle: 0.1,
variantThreedOffsetx: 10,
variantThreedOffsety: 5,
variantThreedXaxisColor: '#ddd',
variantThreedYaxisColor: '#ddd',
variantDumbbellLinewidth: 8,
variantDumbbellEndTop: true,
variantDumbbellEndBottom: true,
variantDumbbellEndRadius: 13,
variantDumbbellEndTopRadius: null,
variantDumbbellEndBottomRadius: null,
shadow: false,
shadowColor: '#aaa',
shadowOffsetx: 0,
shadowOffsety: 0,
shadowBlur: 15,
tooltips: null,
tooltipsEffect: 'slide',
tooltipsCssClass: 'RGraph_tooltip',
tooltipsCss: null,
tooltipsEvent: 'onclick',
tooltipsHighlight: true,
tooltipsPersistent: false,
tooltipsHotspotXonly: false,
tooltipsHotspotIgnore: null,
tooltipsFormattedThousand: ',',
tooltipsFormattedPoint: '.',
tooltipsFormattedDecimals: 0,
tooltipsFormattedUnitsPre: '',
tooltipsFormattedUnitsPost: '',
tooltipsFormattedKeyColors: null,
tooltipsFormattedKeyColorsShape: 'square',
tooltipsFormattedKeyLabels: [],
tooltipsFormattedListType: 'ul',
tooltipsFormattedListItems: null,
tooltipsFormattedTableHeaders: null,
tooltipsFormattedTableData: null,
tooltipsPointer: true,
tooltipsPointerOffsetx: 0,
tooltipsPointerOffsety: 0,
tooltipsPositionStatic: true,
highlightStroke: 'transparent',
highlightFill: 'rgba(255,255,255,0.7)',
highlightFade: true,
key: null,
keyBackground: 'white',
keyPosition: 'graph',
keyShadow: false,
keyShadowColor: '#666',
keyShadowBlur: 3,
keyShadowOffsetx: 2,
keyShadowOffsety: 2,
keyPositionMarginBoxed:false,
keyPositionMarginHSpace: 0,
keyPositionX: null,
keyPositionY: null,
keyInteractive: false,
keyInteractiveHighlightChartLinewidth: 2,
keyInteractiveHighlightChartStroke: 'black',
keyInteractiveHighlightChartFill: 'rgba(255,255,255,0.7)',
keyInteractiveHighlightLabel: 'rgba(255,0,0,0.2)',
keyHalign: 'right',
keyColorShape: 'square',
keyRounded: true,
keyLinewidth: 1,
keyColors: null,
keyLabelsColor: null,
keyLabelsSize: null,
keyLabelsFont: null,
keyLabelsBold: null,
keyLabelsItalic: null,
keyLabelsOffsetx: 0,
keyLabelsOffsety: 0,
keyFormattedDecimals: 0,
keyFormattedPoint: '.',
keyFormattedThousand: ',',
keyFormattedUnitsPre: '',
keyFormattedUnitsPost: '',
keyFormattedValueSpecific: null,
keyFormattedItemsCount: null,
contextmenu: null,
crosshairs: false,
crosshairsColor: '#333',
crosshairsLinewidth: 1,
crosshairsHline: true,
crosshairsVline: true,
crosshairsSnapToScale: false,
linewidth: 1,
annotatable: false,
annotatableLinewidth: 1,
annotatableColor: 'black',
adjustable: false,
adjustableOnly: null,
beveled: false,
errorbars: false,
errorbarsColor: 'black',
errorbarsCapped: true,
errorbarsCappedWidth: 14,
errorbarsLinewidth: 1,
combinedEffect: null,
combinedEffectOptions: null,
combinedEffectCallback: null,
corners: 'round',
cornersRoundRadius: 5,
cornersRoundLeft: true,
cornersRoundRight: true,
cornersRoundLeftRadius: null,
cornersRoundRightRadius: null,
responsive: null,
clearto: 'transparent',
style: null,
events: {},
clip: null,
scale: true,
scaleFactor: 2,
antialiasTranslate: false,
style: []
};
//
// These are the properties that get scaled up if the
// scale option is enabled.
//
this.properties_scale = [
'backgroundGridLinewidth',
'backgroundGridDashArray',
'backgroundGridHsize',
'backgroundGridVsize',
'backgroundImageX',
'backgroundImageY',
'backgroundImageW',
'backgroundImageH',
'backgroundBorderLinewidth',
'backgroundBorderDashArray',
'marginTop',
'marginBottom',
'marginLeft',
'marginRight',
'marginInner',
'marginInnerGrouped',
'labelsIngraphSize',
'labelsIngraphOffsetx',
'labelsIngraphOffsety',
'labelsAboveSize',
'labelsAboveOffset',
'labelsAboveOffsetx',
'labelsAboveOffsety',
'yaxisLinewidth',
'yaxisTickmarksLength',
'yaxisLabelsOffsetx',
'yaxisLabelsOffsety',
'yaxisLabelsSize',
'yaxisTitleSize',
'yaxisTitleX',
'yaxisTitleY',
'yaxisTitleOffsetx',
'yaxisTitleOffsety',
'xaxisLinewidth',
'xaxisTickmarksLength',
'xaxisLabelsSize',
'xaxisLabelsOffsetx',
'xaxisLabelsOffsety',
'xaxisTitleSize',
'xaxisTitleOffsetx',
'xaxisTitleOffsety',
'xaxisTitleX',
'xaxisTitleY',
'textSize',
'titleX',
'titleY',
'titleSize',
'titleOffsetx',
'titleOffsety',
'titleSubtitleSize',
'titleSubtitleOffsetx',
'titleSubtitleOffsety',
'variantThreedOffsetx',
'variantThreedOffsety',
'variantDumbbellLinewidth',
'variantDumbbellEndRadius',
'variantDumbbellEndTopRadius',
'variantDumbbellEndBottomRadius',
'shadowOffsetx',
'shadowOffsety',
'shadowBlur',
'tooltipsPointerOffsetx',
'tooltipsPointerOffsety',
'keyShadowBlur',
'keyShadowOffsetx',
'keyShadowOffsety',
'keyPositionX',
'keyPositionY',
'keyInteractiveHighlightChartLinewidth',
'keyLinewidth',
'keyLabelsSize',
'keyLabelsOffsetx',
'keyLabelsOffsety',
'crosshairsLinewidth',
'linewidth',
'annotatableLinewidth',
'errorbarsCappedWidth',
'errorbarsLinewidth',
'cornersRoundRadius',
'cornersRoundLeftRadius',
'cornersRoundRightRadius',
'variantThreedOffsetx',
'variantThreedOffsety'
];
//
// Add the reverse look-up table for property names
// so that property names can be specified in any case.
//
this.properties_lowercase_map = [];
for (var i in this.properties) {
if (typeof i === 'string') {
this.properties_lowercase_map[i.toLowerCase()] = i;
}
}
// Check for support
if (!this.canvas) {
alert('[BAR] No canvas support');
return;
}
//
// Convert strings into numbers. Also converts undefined
// elements to null
//
data = RGraph.stringsToNumbers(data);
//
// Determine whether the chart will contain stacked or
// grouped bars
//
for (var i=0; i<data.length; ++i) {
if (typeof data[i] === 'object' && !RGraph.isNullish(data[i])) {
this.stackedOrGrouped = true;
}
}
//
// Create the dollar objects so that functions can be
// added to them
//
var linear_data = RGraph.arrayLinearize(data);
for (var i=0; i<linear_data.length; ++i) {
this['$' + i] = {};
}
// Store the data and set the orignal_data to it
this.data = data;
this.original_data = RGraph.arrayClone(data);
// Used to store the coords of the bars
this.coords = [];
this.coords2 = [];
this.coordsText = [];
//
// This linearises the data. Doing so can make it easier to pull
// out the appropriate data from tooltips
//
this.data_arr = RGraph.arrayLinearize(this.data);
// Easy access to properties and the path function
var properties = this.properties;
this.path = RGraph.pathObjectFunction;
//
// "Decorate" the object with the generic effects if the effects library has been included
//
if (RGraph.Effects && typeof RGraph.Effects.decorate === 'function') {
RGraph.Effects.decorate(this);
}
// Add the responsive method. This method resides in the common file.
this.responsive = RGraph.responsive;
//
// A setter
//
// @param name string The name of the property to set
// @param value mixed The value of the property
//
this.set = function (name)
{
var value = typeof arguments[1] === 'undefined' ? null : arguments[1];
// Go through all of the properties and make sure
// that they're using the correct capitalisation
if (typeof name === 'string') {
name = this.properties_lowercase_map[name.toLowerCase()] || name;
}
// Some BC for the bevelled property
if (name === 'bevelled') {
name = 'beveled';
}
// Set the colorsParsed flag to false if the colors
// property is being set
if (
name === 'colors'
|| name === 'keyColors'
|| name === 'crosshairsColor'
|| name === 'highlightStroke'
|| name === 'highlightFill'
|| name === 'textColor'
|| name === 'backgroundBarsColor1'
|| name === 'backgroundBarsColor2'
|| name === 'backgroundGridColor'
|| name === 'backgroundColor'
|| name === 'colorsStroke'
|| name === 'axesColor'
) {
this.colorsParsed = false;
}
// the number of arguments is only one and it's an
// object - parse it for configuration data and return.
if (arguments.length === 1 && typeof arguments[0] === 'object') {
for (i in arguments[0]) {
if (typeof i === 'string') {
this.set(i, arguments[0][i]);
}
}
return this;
}
properties[name] = value;
return this;
};
//
// A getter
//
// @param name string The name of the property to get
//
this.get = function (name)
{
// Go through all of the properties and make sure
// that they're using the correct capitalisation
name = this.properties_lowercase_map[name.toLowerCase()] || name;
return properties[name];
};
//
// The function you call to draw the bar chart
//
this.draw = function ()
{
// MUST be the first thing that's done - but only
// once!!
RGraph.runOnce(`scale-up-the-canvas-once-in-the-draw-function-${this.id}-${this.uid}`, () =>
{
// Note that were in an arrow function so the
// 'this' variable is OK to be used and refers
// to the RGraph Line chart object.
RGraph.scale(this);
});
// MUST be the second thing that's done!!
//
// Fire the onbeforedraw event
//
RGraph.fireCustomEvent(this, 'onbeforedraw');
//
// Add any CSS that has been specified to the document.
// This is general CSS and does not necessarily have to
// pertain to the canvas tag. It only gets added once
// to the document no matter how many times this draw
// function is called.
//
// Add the CSS to a <style> block in the <head>.
RGraph.addConfigurationBasedCSS(this);
// Translate half a pixel for antialiasing purposes - but
// only if it hasn't been done already
//
// The old style antialias fix
//
if ( !this.properties.scale
&& this.properties.antialiasTranslate
&& !this.canvas.__rgraph_aa_translated__) {
this.context.translate(0.5,0.5);
this.canvas.__rgraph_aa_translated__ = true;
}
// MUST be the THIRD thing done!
if (typeof properties.backgroundImage === 'string' && properties.backgroundImage) {
//
// Install clipping for the background image. This
// is not the main installation of clipping - it's
// just for the background image.
//
// The this.scale2 check means that clipping is only
// installed if that variable is present - ie this
// is most likely a redraw and the necessary things
// have been calculated so clipping can be
// installed correctly.
//
if (!RGraph.isNullish(this.properties.clip) && this.scale2) {
RGraph.clipTo.start(this, this.properties.clip);
}
RGraph.drawBackgroundImage(this);
if (!RGraph.isNullish(this.properties.clip) && this.scale2) {
RGraph.clipTo.end();
}
}
// If 3D is enabled then turn off rounded corners
// because it doesn't look correct.
if (this.properties.variant === '3d' && this.properties.corners === 'round') {
this.properties.corners = 'square'
}
//
// If the X axis is at the top then all the
// data-points should be negative
//
if (properties.xaxisPosition === 'top') {
for (var i=0; i<this.data.length; ++i) {
if (typeof this.data[i] === 'object' && properties.grouping === 'grouped') {
for (var j=0;j<this.data[i].length; ++j) {
this.data[i][j] = Math.abs(this.data[i][j]) * -1;
}
} else if (typeof this.data[i] === 'number') {
this.data[i] = Math.abs(this.data[i]) * -1;
}
}
}
//
// If the chart is 3d then angle it
//
if (properties.variant === '3d') {
if (properties.textAccessible) {
// Nada
} else {
this.context.setTransform(1,properties.variantThreedAngle,0,1,0.5,0.5);
}
}
//
// Parse the colors. This allows for simple gradient
// syntax
//
if (!this.colorsParsed) {
this.parseColors();
// Don't want to do this again
this.colorsParsed = true;
}
//
// Make the margins easy ro access
//
this.marginLeft = properties.marginLeft;
this.marginRight = properties.marginRight;
this.marginTop = properties.marginTop;
this.marginBottom = properties.marginBottom;
//
// Check for tooltips and alert the user that they're
// not supported with pyramid charts
//
if ( (properties.variant == 'pyramid' || properties.variant == 'dot')
&& typeof properties.tooltips == 'object'
&& properties.tooltips
&& properties.tooltips.length > 0) {
alert('[BAR] (' + this.id + ') Sorry, tooltips are not supported with dot or pyramid charts');
}
//
// Stop the coords arrays from growing uncontrollably
//
this.coords = [];
this.coords2 = [];
this.coordsText = [];
//
// Work out a few things. They need to be here because
// they depend on things you can change before you
// call Draw() but after you instantiate the object
//
this.max = 0;
this.grapharea = this.canvas.height - this.marginTop - this.marginBottom;
this.halfgrapharea = this.grapharea / 2;
this.halfTextHeight = properties.textSize / 2;
// If a dumbbell chart is being drawn then a few
// properties need to be massaged
if (!this.properties.colorsDumbbell && this.properties.variant === 'dumbbell') {
this.set({
colorsDumbbell: this.get('colors')
});
this.set({
colors: ['#0000']
});
};
//
// Work out the max value
//
if (this.properties.yaxisScaleMax) {
this.scale2 = RGraph.getScale({object: this, options: {
'scale.max': properties.yaxisScaleMax,
'scale.strict': properties.yaxisScaleRound ? false : true,
'scale.min': properties.yaxisScaleMin,
'scale.thousand': properties.yaxisScaleThousand,
'scale.point': properties.yaxisScalePoint,
'scale.decimals': properties.yaxisScaleDecimals,
'scale.labels.count':properties.yaxisLabelsCount,
'scale.round': properties.yaxisScaleRound,
'scale.units.pre': properties.yaxisScaleUnitsPre,
'scale.units.post': properties.yaxisScaleUnitsPost,
'scale.formatter': properties.yaxisScaleFormatter
}});
} else {
//
// If errorbars are given as a number then convert the nuumber to an
// array.
//
var errorbars = properties.errorbars;
if (typeof errorbars === 'number') {
var value = errorbars;
properties.errorbars = [];
for (var i=0; i<this.data.length; ++i) {
if (typeof this.data[i] === 'number') {
properties.errorbars.push([value, null]);
} else if (typeof this.data[i] === 'object' && !RGraph.isNullish(this.data[i])) {
for (var j=0; j<this.data[i].length; ++j) {
properties.errorbars.push([value, null]);
}
}
}
errorbars = properties.errorbars;
}
for (i=0; i<this.data.length; ++i) {
if (typeof this.data[i] == 'object') {
var value = properties.grouping === 'grouped' ? Number(RGraph.arrayMax(this.data[i], true)) : Number(RGraph.arraySum(this.data[i]));
} else {
var value = Number(this.data[i]);
}
this.max = Math.max(Math.abs(this.max), Math.abs(value) +
Number(
(
typeof properties.errorbars === 'object'
&& typeof properties.errorbars[i] === 'object'
&& !RGraph.isNullish(properties.errorbars[i])
&& typeof properties.errorbars[i][0] === 'number'
) ? properties.errorbars[i][0] : 0
)
);
}
this.scale2 = RGraph.getScale({object: this, options: {
'scale.max': this.max,
'scale.min': properties.yaxisScaleMin,
'scale.thousand': properties.yaxisScaleThousand,
'scale.point': properties.yaxisScalePoint,
'scale.decimals': properties.yaxisScaleDecimals,
'scale.labels.count':properties.yaxisLabelsCount,
'scale.round': properties.yaxisScaleRound,
'scale.units.pre': properties.yaxisScaleUnitsPre,
'scale.units.post': properties.yaxisScaleUnitsPost,
'scale.formatter': properties.yaxisScaleFormatter
}});
this.max = this.scale2.max;
}
//
// Install clipping
//
// MUST be the first thing that's done after the
// beforedraw event
//
if (!RGraph.isNullish(this.properties.clip)) {
RGraph.clipTo.start(this, this.properties.clip);
}
// Now draw the background on to the main canvas
RGraph.Background.draw(this);
//If it's a sketch chart variant, draw the axes first
//if (properties.variant == 'sketch') {
// this.drawAxes();
// this.drawbars();
//} else {
this.drawbars();
this.drawAxes();
//}
//
// Draw the dumbbell bars variant Draw this here
// so that the dumbells are drawn over the axes.
//
if (this.properties.variant === 'dumbbell') {
this.drawdumbbell();
}
this.drawLabels();
//
// Draw the bevel if required
//
if (properties.beveled || properties.beveled) {
this.drawBevel();
}
// Draw the key if necessary
if (properties.key && properties.key.length) {
RGraph.drawKey(
this,
properties.key,
properties.colors
);
}
//
// Setup the context menu if required
//
if (properties.contextmenu) {
RGraph.showContext(this);
}
//
// Draw errorbars
//
if (properties.errorbars) {
this.drawErrorbars();
}
//
// Draw "in graph" labels
//
if (properties.labelsIngraph) {
RGraph.drawInGraphLabels(this);
}
//
// Add custom text thats specified
//
RGraph.addCustomText(this);
// Draw any custom lines that have been defined
RGraph.drawHorizontalLines(this);
//
// This installs the event listeners
//
RGraph.installEventListeners(this);
//
// End clipping
//
if (!RGraph.isNullish(this.properties.clip)) {
RGraph.clipTo.end();
}
//
// Fire the onfirstdraw event
//
if (this.firstDraw) {
this.firstDraw = false;
RGraph.fireCustomEvent(this, 'onfirstdraw');
this.firstDrawFunc();
}
//
// Fire the RGraph draw event
//
RGraph.fireCustomEvent(this, 'ondraw');
//
// Install any inline responsive configuration. This
// should be last in the draw function - even after
// the draw events.
//
RGraph.installInlineResponsive(this);
return this;
};
//
// Used in chaining. Runs a function there and then - not waiting for
// the events to fire (eg the onbeforedraw event)
//
// @param function func The function to execute
//
this.exec = function (func)
{
func(this);
return this;
};
//
// Draws the charts axes
//
this.drawAxes = function ()
{
if (RGraph.ISSAFARI == -1) {
this.context.lineCap = 'square';
}
//
// If the xaxisLabels option is a string then turn it
// into an array.
//
if (properties.xaxisLabels && properties.xaxisLabels.length) {
if (typeof properties.xaxisLabels === 'string') {
properties.xaxisLabels = RGraph.arrayPad(
[],
this.data.length,
properties.xaxisLabels
);
}
// Label substitution
//
for (var i=0; i<properties.xaxisLabels.length; ++i) {
if (RGraph.isString(properties.xaxisLabels[i])) {
properties.xaxisLabels[i] = RGraph.labelSubstitution({
object: this,
text: properties.xaxisLabels[i],
index: i,
value: this.data[i],
decimals: properties.xaxisLabelsFormattedDecimals || 0,
unitsPre: properties.xaxisLabelsFormattedUnitsPre || '',
unitsPost: properties.xaxisLabelsFormattedUnitsPost || '',
thousand: properties.xaxisLabelsFormattedThousand || ',',
point: properties.xaxisLabelsFormattedPoint || '.'
});
}
}
}
//
// The new common X axis drawing function
RGraph.drawXAxis(this);
//
// The new common Y axis drawing function
RGraph.drawYAxis(this);
};
//
// Draws the bars
//
this.drawbars = function ()
{
this.context.lineWidth = properties.linewidth;
this.context.strokeStyle = properties.colorsStroke;
this.context.fillStyle = properties.colors[0];
var prevX = 0,
prevY = 0,
decimals = properties.yaxisScaleDecimals;
//
// if the chart is adjustable fix the scale so that it doesn't change.
//
if (properties.adjustable && !properties.yaxisScaleMax) {
this.set('yaxisScaleMax', this.scale2.max);
}
//
// Draw horizontal bars here
//
if (properties.backgroundHbars && properties.backgroundHbars.length > 0) {
RGraph.drawBars(this);
}
var variant = this.properties.variant;
//
// Draw the 3D axes is necessary
//
if (variant === '3d') {
RGraph.draw3DAxes(this);
}
//
// Get the variant once, and draw the bars, be they regular, stacked or grouped
//
// Get these variables outside of the loop
var xaxispos = properties.xaxisPosition,
width = (this.canvas.width - this.marginLeft - this.marginRight ) / this.data.length,
orig_height = height,
hmargin = properties.marginInner,
shadow = properties.shadow,
shadowColor = properties.shadowColor,
shadowBlur = properties.shadowBlur,
shadowOffsetX = properties.shadowOffsetx,
shadowOffsetY = properties.shadowOffsety,
strokeStyle = properties.colorsStroke,
colors = properties.colors,
sequentialColorIndex = 0
var height;
//
// Loop through the data
//
for (i=0,len=this.data.length; i<len; i+=1) {
// Work out the height
//The width is up outside the loop
if (RGraph.arraySum(this.data[i]) < 0) {
var height = (RGraph.arraySum(this.data[i]) + this.scale2.min) / (this.scale2.max - this.scale2.min);
} else {
var height = (RGraph.arraySum(this.data[i]) - this.scale2.min) / (this.scale2.max - this.scale2.min);
}
height *= Math.abs(this.getYCoord(this.scale2.max) - this.getYCoord(this.scale2.min));
var x = (i * width) + this.marginLeft;
var y = xaxispos == 'center' ? ((this.canvas.height - this.marginTop - this.marginBottom) / 2) + this.marginTop - height
: this.canvas.height - height - this.marginBottom;
// xaxispos is top
if (xaxispos == 'top') {
y = this.marginTop;
}
// Account for negative lengths - Some browsers don't like a negative value
if (height < 0) {
y += height;
height = Math.abs(height);
}
//
// Turn on the shadow if need be
//
if (shadow) {
this.context.shadowColor = shadowColor;
this.context.shadowBlur = shadowBlur;
this.context.shadowOffsetX = shadowOffsetX;
this.context.shadowOffsetY = shadowOffsetY;
}
//
// Draw the bar
//
this.context.beginPath();
if (typeof this.data[i] == 'number') {
// If the Y axis is offset change the bar start (the top of the bar)
if (xaxispos === 'bottom' && properties.yaxisScaleMin < 0) {
if (this.data[i] >= 0) {
height = Math.abs(this.getYCoord(0) - this.getYCoord(this.data[i]));
} else {
y = this.getYCoord(0);
height = Math.abs(this.getYCoord(0) - this.getYCoord(this.data[i]));
}
}
var barWidth = width - (2 * hmargin);
//
// Check for a negative bar width
//
if (barWidth < 0) {
alert('[RGRAPH] Warning: you have a negative bar width. This may be caused by the marginInner being too high or the width of the canvas not being sufficient.');
}
// Set the fill color
this.context.strokeStyle = strokeStyle;
this.context.fillStyle = colors[0];
//
// Sequential colors
//
if (properties.colorsSequential) {
this.context.fillStyle = colors[i];
}
if (variant === 'sketch') {
this.context.lineCap = 'round';
var sketchOffset = 3;
this.context.beginPath();
this.context.strokeStyle = colors[0];
//
// Sequential colors
//
if (properties.colorsSequential) {
this.context.strokeStyle = colors[i];
}
// Left side
this.context.moveTo(x + hmargin + 2, y + height - 2);
this.context.lineTo(x + hmargin - 1, y - 4);
// The top
this.context.moveTo(x + hmargin - 3, y + -2 + (this.data[i] < 0 ? height : 0));
this.context.quadraticCurveTo(
x + hmargin + ((width - hmargin - hmargin) / 4),
y + 0 + (this.data[i] < 0 ? height : 0) + (this.data[i] > 0 ? 10 : -10),
x + hmargin + width + -1 - hmargin - hmargin,
y + 0 + (this.data[i] < 0 ? height : 0)
);
// The right side
this.context.moveTo(x + hmargin + width - 5 - hmargin - hmargin, y - 5);
this.context.lineTo(x + hmargin + width - 3 - hmargin - hmargin, y + height - 3);
// Draw the inner-bar verticals
if (properties.variantSketchVerticals) {
for (var r=0.2; r<=0.8; r+=0.2) {
this.context.moveTo(
x + hmargin + ((width - hmargin - hmargin) * r),
y - 1
);
this.context.lineTo(
x + hmargin + ((width - hmargin - hmargin) * r),
y + height + (r == 0.2 ? 1 : -2)
);
}
}
this.context.stroke();
// Regular bar
} else if (variant === 'bar' || variant === '3d' || variant === 'glass' || variant === 'bevel' || variant === 'dumbbell') {
if (variant == 'glass') {
RGraph.roundedRect(
this.context,
x + hmargin,
y,
barWidth,
height,
RGraph.isNumber(properties.cornersRoundRadius) ? properties.cornersRoundRadius : 5,
this.data[i] > 0,
this.data[i] > 0,
this.data[i] < 0,
this.data[i] < 0
);
this.context.stroke();
this.context.fill();
} else {
// On 9th April 2013 these two were swapped around so that the stroke happens SECOND so that any
// shadow that is cast by the fill does not overwrite the stroke
// Why is this here?
//this.path(
// 'b r % % % % f',
// x + hmargin, y, barWidth, height
//);
// Turn the shadow off so that the stroke doesn't cast any "extra" shadow
// that would show inside the bar
//
// 31/07/21 Removed as regular bar charts weren't showing shadows
//
//RGraph.noShadow(this);
if (properties.corners === 'round') {
this.context.beginPath();
this.context.lineCap = 'miter';
this.context.lineJoin = 'square';
(this.data[i] < 0 || properties.xaxisPosition === 'top')
? this.roundedCornersRectNegative(x + hmargin,y,barWidth,height)
: this.roundedCornersRect(x + hmargin,y,barWidth,height);
this.context.stroke();
this.context.fill();
} else {
if (properties.xaxisPosition === 'top') {
y += height;
}
this.context.beginPath();
this.context.lineJoin = 'miter';
this.context.lineCap = 'square';
this.context.rect(x + hmargin,y,barWidth,height);
this.context.stroke();
this.context.fill();
}
}
// 3D effect
if (variant == '3d') {
var prevStrokeStyle = this.context.strokeStyle;
var prevFillStyle = this.context.fillStyle;
// Draw the top (if the value is positive - otherwise there's no point)
if (this.data[i] >= 0) {
this.context.beginPath();
this.context.moveTo(x + hmargin, y);
this.context.lineTo(x + hmargin + properties.variantThreedOffsetx, y - properties.variantThreedOffsety);
this.context.lineTo(x + hmargin + properties.variantThreedOffsetx + barWidth, y - properties.variantThreedOffsety);
this.context.lineTo(x + hmargin + barWidth, y);
this.context.closePath();
this.context.stroke();
this.context.fill();
}
// Draw the right hand side
this.context.beginPath();
this.context.moveTo(x + hmargin + barWidth, y);
this.context.lineTo(
x + hmargin + barWidth + properties.variantThreedOffsetx,
this.data[i] < 0 && xaxispos === 'bottom' ?
this.getYCoord(0) : (
this.data[i] < 0 && (y - properties.variantThreedOffsety)
< (this.marginTop + this.halfgrapharea)
?
(this.marginTop + this.halfgrapharea)
: (y - properties.variantThreedOffsety))
);
this.context.lineTo(
x + hmargin + barWidth + properties.variantThreedOffsetx,
this.data[i] < 0 && (y - properties.variantThreedOffsety + height) < (this.marginTop + this.getYCoord(0))
? this.getYCoord(this.data[i]) - properties.variantThreedOffsety
: (this.data[i] > 0 ?
y - properties.variantThreedOffsety + height :
Math.min(y - properties.variantThreedOffsety + height, this.canvas.height - this.marginBottom)
)
);
this.context.lineTo(x + hmargin + barWidth, y + height);
this.context.closePath();
this.context.stroke();
this.context.fill();
// Draw the lighter top section
if (this.data[i] > 0) {
this.context.beginPath();
this.context.fillStyle = 'rgba(255,255,255,0.5)';
this.context.moveTo(x + hmargin, y);
this.context.lineTo(x + hmargin + properties.variantThreedOffsetx, y - properties.variantThreedOffsety);
this.context.lineTo(x + hmargin + properties.variantThreedOffsetx + barWidth, y - properties.variantThreedOffsety);
this.context.lineTo(x + hmargin + barWidth, y);
this.context.lineTo(x + hmargin, y);
this.context.closePath();
this.context.stroke();
this.context.fill();
}
// Draw the darker right side section
this.context.beginPath();
this.context.fillStyle = 'rgba(0,0,0,0.4)';
// TL
this.context.moveTo(x + hmargin + barWidth, y);
// TR
this.context.lineTo(
x + hmargin + barWidth + properties.variantThreedOffsetx,
this.data[i] < 0 && xaxispos === 'bottom' ? this.getYCoord(0) : (this.data[i] < 0 && (y - properties.variantThreedOffsety) < (this.marginTop + this.halfgrapharea) ? (this.marginTop + this.halfgrapharea) : y - properties.variantThreedOffsety)
);
// BR
this.context.lineTo(
x + hmargin + barWidth + properties.variantThreedOffsetx,
this.data[i] < 0 && (y - properties.variantThreedOffsety + height) < this.getYCoord(0)
? this.getYCoord(0)
: this.data[i] > 0 ? y - properties.variantThreedOffsety + height : Math.min(y - properties.variantThreedOffsety + height, this.canvas.height - this.marginBottom)
);
// BL
this.context.lineTo(x + hmargin + barWidth, y + height);
this.context.lineTo(x + hmargin + barWidth, y);
this.context.closePath();
this.context.stroke();
this.context.fill();
this.context.strokeStyle = prevStrokeStyle;
this.context.fillStyle = prevFillStyle;
// Glass variant
} else if (variant == 'glass') {
var grad = this.context.createLinearGradient(x + hmargin,y,x + hmargin + (barWidth / 2),y);
grad.addColorStop(0, 'rgba(255,255,255,0.9)');
grad.addColorStop(1, 'rgba(255,255,255,0.5)');
this.context.beginPath();
this.context.fillStyle = grad;
this.context.roundRect(
x + hmargin + 3,
y + (this.data[i] > 0 ? 3 : 0),
(barWidth / 2) - 2,
height - 2,
this.data[i] > 0 ? ([RGraph.isNumber(properties.cornersRoundRadius) ? properties.cornersRoundRadius - 3 : 5, 0,0,0]) : [0,0,0,RGraph.isNumber(properties.cornersRoundRadius) ? properties.cornersRoundRadius : 5]
);
this.context.fill();
}
// Dot chart
} else if (variant === 'dot') {
var scaleFactor = RGraph.getScaleFactor(this);
this.context.beginPath();
this.context.strokeStyle = this.properties.colors[0];
this.context.moveTo(x + (width / 2), y);
this.context.lineTo(x + (width / 2), y + height);
this.context.stroke();
this.context.beginPath();
this.context.fillStyle = this.properties.colors[i];
this.context.arc(
x + (width / 2),
y + (this.data[i] > 0 ? 0 : height),
2 * scaleFactor,
0,
6.28,
0
);
// Set the colour for the dots
this.context.fillStyle = properties.colors[0];
//
// Sequential colors
//
if (properties.colorsSequential) {
this.context.fillStyle = colors[i];
}
this.context.stroke();
this.context.fill();
// Unknown variant type
} else {
alert('[BAR] Warning! Unknown variant: ' + variant);
}
this.coords.push([x + hmargin, y, width - (2 * hmargin), height]);
if (typeof this.coords2[i] == 'undefined') {
this.coords2[i] = [];
}
this.coords2[i].push([x + hmargin, y, width - (2 * hmargin), height]);
//
// Stacked bar
//
} else if (this.data[i] && typeof this.data[i] == 'object' && properties.grouping == 'stacked') {
if (this.scale2.min) {
alert("[ERROR] Stacked Bar charts with a Y min are not supported");
}
var barWidth = width - (2 * hmargin);
var redrawCoords = [];// Necessary to draw if the shadow is enabled
var startY = 0;
var dataset = this.data[i];
//
// Check for a negative bar width
//
if (barWidth < 0) {
alert('[RGRAPH] Warning: you have a negative bar width. This may be caused by the marginInner being too high or the width of the canvas not being sufficient.');
}
for (j=0; j<dataset.length; ++j) {
// Stacked bar chart and X axis pos in the middle - poitless since negative values are not permitted
if (xaxispos == 'center') {
alert("[BAR] It's pointless having the X axis position at the center on a stacked bar chart.");
return;
}
// Negative values not permitted for the stacked chart
if (this.data[i][j] < 0) {
alert('[BAR] Negative values are not permitted with a stacked bar chart. Try a grouped one instead.');
return;
}
//
// Set the fill and stroke colors
//
this.context.strokeStyle = strokeStyle
this.context.fillStyle = colors[j];
if (properties.colorsReverse) {
this.context.fillStyle = colors[this.data[i].length - j - 1];
}
if (properties.colorsSequential && colors[sequentialColorIndex]) {
this.context.fillStyle = colors[sequentialColorIndex++];
} else if (properties.colorsSequential) {
this.context.fillStyle = colors[sequentialColorIndex - 1];
}
var height = (dataset[j] / this.scale2.max) * (this.canvas.height - this.marginTop - this.marginBottom );
// If the X axis pos is in the center, we need to half the height
if (xaxispos == 'center') {
height /= 2;
}
var totalHeight = (RGraph.arraySum(dataset) / this.scale2.max) * (this.canvas.height - hmargin - this.marginTop - this.marginBottom);
//
// Store the coords for tooltips
//
this.coords.push([x + hmargin, y, width - (2 * hmargin), height]);
if (typeof this.coords2[i] == 'undefined') {
this.coords2[i] = [];
}
this.coords2[i].push([x + hmargin, y, width - (2 * hmargin), height]);
if (height > 0) {
// TODO Handle xaxisPosition=top here
if (j === 0 && properties.corners === 'round' && properties.xaxisPosition === 'bottom') {
this.context.beginPath();
this.context.lineCap = 'miter';
this.context.lineJoin = 'square';
this.roundedCornersRect(x + hmargin, y, width - (2 * hmargin), height);
this.context.stroke();
this.context.fill();
} else if (j === (dataset.length - 1) && properties.corners === 'round' && properties.xaxisPosition === 'top') {
this.context.beginPath();
this.context.lineCap = 'miter';
this.context.lineJoin = 'square';
this.roundedCornersRectNegative(x + hmargin, y, width - (2 * hmargin), height);
this.context.stroke();
this.context.fill();
} else {
this.path(
'b lj % lc % r % % % % s % f %',
'miter','square',
x + hmargin, y, width - (2 * hmargin), height,
this.context.strokeStyle,
this.context.fillStyle
);
}
}
if (j == 0) {
var startY = y;
var startX = x;
}
//
// Store the redraw coords if the shadow is enabled
//
if (shadow) {
redrawCoords.push([x + hmargin, y, width - (2 * hmargin), height, this.context.fillStyle]);
}
//
// Stacked 3D effect
//
if (variant == '3d') {
var prevFillStyle = this.context.fillStyle;
var prevStrokeStyle = this.context.strokeStyle;
// Draw the top side
if (j == 0) {
this.context.beginPath();
this.context.moveTo(startX + hmargin, y);
this.context.lineTo(startX + properties.variantThreedOffsetx + hmargin, y - properties.variantThreedOffsety);
this.context.lineTo(startX + properties.variantThreedOffsetx + barWidth + hmargin, y - properties.variantThreedOffsety);
this.context.lineTo(startX + barWidth + hmargin, y);
this.context.closePath();
this.context.fill();
this.context.stroke();
}
// Draw the side section
this.context.beginPath();
this.context.moveTo(startX + barWidth + hmargin, y);
this.context.lineTo(startX + barWidth + hmargin + properties.variantThreedOffsetx, y - properties.variantThreedOffsety);
this.context.lineTo(startX + barWidth + hmargin + properties.variantThreedOffsetx, y - properties.variantThreedOffsety + height);
this.context.lineTo(startX + barWidth + hmargin , y + height);
this.context.closePath();
this.context.fill();
this.context.stroke();
// Draw the lighter top side
if (j == 0) {
this.context.fillStyle = 'rgba(255,255,255,0.5)';
this.context.beginPath();
this.context.moveTo(startX + hmargin, y);
this.context.lineTo(startX + properties.variantThreedOffsetx + hmargin, y - properties.variantThreedOffsety);
this.context.lineTo(startX + properties.variantThreedOffsetx + barWidth + hmargin, y - properties.variantThreedOffsety);
this.context.lineTo(startX + barWidth + hmargin, y);
this.context.closePath();
this.context.fill();
this.context.stroke();
}
// Draw the darker side section
this.context.fillStyle = 'rgba(0,0,0,0.4)';
this.context.beginPath();
this.context.moveTo(startX + barWidth + hmargin, y);
this.context.lineTo(startX + barWidth + hmargin + properties.variantThreedOffsetx, y - properties.variantThreedOffsety);
this.context.lineTo(startX + barWidth + hmargin + properties.variantThreedOffsetx, y - properties.variantThreedOffsety + height);
this.context.lineTo(startX + barWidth + hmargin , y + height);
this.context.closePath();
this.context.fill();
this.context.stroke();
this.context.strokeStyle = prevStrokeStyle;
this.context.fillStyle = prevFillStyle;
}
y += height;
}
//
// Redraw the bars if the shadow is enabled due to hem being drawn from the bottom up, and the
// shadow spilling over to higher up bars
//
if (shadow) {
RGraph.noShadow(this);
for (k=0; k<redrawCoords.length; ++k) {
this.context.strokeStyle = strokeStyle;
this.context.fillStyle = redrawCoords[k][4];
this.context.strokeRect(redrawCoords[k][0], redrawCoords[k][1], redrawCoords[k][2], redrawCoords[k][3]);
this.context.fillRect(redrawCoords[k][0], redrawCoords[k][1], redrawCoords[k][2], redrawCoords[k][3]);
this.context.stroke();
this.context.fill();
}
// Reset the redraw coords to be empty
redrawCoords = [];
}
//
// Grouped bar
//
} else if (this.data[i] && typeof this.data[i] == 'object' && properties.grouping == 'grouped') {
var redrawCoords = [];
this.context.lineWidth = properties.linewidth;
for (j=0; j<this.data[i].length; ++j) {
// Set the fill and stroke colors
this.context.strokeStyle = strokeStyle;
this.context.fillStyle = colors[j];
//
// Sequential colors
//
if (properties.colorsSequential && colors[sequentialColorIndex]) {
this.context.fillStyle = colors[sequentialColorIndex++];
} else if (properties.colorsSequential) {
this.context.fillStyle = colors[sequentialColorIndex - 1];
}
var individualBarWidth = (width - (2 * hmargin)) / this.data[i].length;
var height = ((this.data[i][j] + (this.data[i][j] < 0 ? this.scale2.min : (-1 * this.scale2.min) )) / (this.scale2.max - this.scale2.min) ) * (this.canvas.height - this.marginTop - this.marginBottom );
var groupedMargin = properties.marginInnerGrouped;
var startX = x + hmargin + (j * individualBarWidth);
//
// Check for a negative bar width
//
if (individualBarWidth < 0) {
alert('[RGRAPH] Warning: you have a negative bar width. This may be caused by the marginInner being too high or the width of the canvas not being sufficient.');
}
// If the X axis pos is in the center, we need to half the height
if (xaxispos == 'center') {
height /= 2;
}
//
// Determine the start positioning for the bar
//
if (xaxispos == 'top') {
var startY = this.marginTop;
var height = Math.abs(height);
} else if (xaxispos == 'center') {
var startY = this.marginTop + (this.grapharea / 2) - height;
} else {
var startY = this.getYCoord(0);//this.canvas.height - this.marginBottom - height;
var height = Math.abs(Math.abs(this.getYCoord(this.data[i][j])) - this.getYCoord(0));
if (this.data[i][j] >= 0) {
startY -= height;
}
}
if (properties.corners === 'round') {
this.context.beginPath();
this.context.lineCap = 'miter';
this.context.lineJoin = 'square';
(this.data[i][j] < 0)
? this.roundedCornersRectNegative(startX + groupedMargin, startY, individualBarWidth - (2 * groupedMargin), height)
: this.roundedCornersRect(startX + groupedMargin, startY, individualBarWidth - (2 * groupedMargin), height);
this.context.stroke();
this.context.fill();
} else {
this.context.beginPath();
this.context.lineJoin = 'miter';
this.context.lineCap = 'square';
this.context.rect(startX + groupedMargin, startY, individualBarWidth - (2 * groupedMargin), height);
this.context.stroke();
this.context.fill();
}
y += height;
//
// Grouped 3D effect
//
if (variant == '3d') {
var prevFillStyle = this.context.fillStyle;
var prevStrokeStyle = this.context.strokeStyle;
var hmarginGrouped = properties.marginInnerGrouped;
// Draw the top side
if (this.data[i][j] >= 0) {
this.context.beginPath();
this.context.moveTo(startX + hmarginGrouped, startY);
this.context.lineTo(startX + hmarginGrouped + properties.variantThreedOffsetx, startY - properties.variantThreedOffsety);
this.context.lineTo(startX + properties.variantThreedOffsetx + individualBarWidth - hmarginGrouped, startY - properties.variantThreedOffsety);
this.context.lineTo(startX + individualBarWidth - hmarginGrouped, startY);
this.context.closePath();
this.context.fill();
this.context.stroke();
}
// Draw the side section
this.context.beginPath();
this.context.moveTo(
startX + individualBarWidth - hmarginGrouped - 1,
startY
);
this.context.lineTo(
startX + individualBarWidth - hmarginGrouped + properties.variantThreedOffsetx,
this.data[i][j] < 0
? (this.getYCoord(0) + Math.abs(height) - properties.variantThreedOffsety) - (properties.xaxisPosition === 'center'
? 0
: Math.abs(height) - this.properties.variantThreedOffsety)
: this.getYCoord(0) - height - properties.variantThreedOffsety
);
this.context.lineTo(
startX + individualBarWidth - hmarginGrouped + properties.variantThreedOffsetx,
this.data[i][j] < 0 && (startY + height - properties.variantThreedOffsety) < this.getYCoord(0)
? (this.getYCoord(0))
: (startY + height - properties.variantThreedOffsety)
);
this.context.lineTo(startX + individualBarWidth - hmarginGrouped - 1, startY + height);
this.context.closePath();
this.context.fill();
this.context.stroke();
// Draw the lighter top side - but only if the current value is positive
if (this.data[i][j] >= 0) {
this.context.fillStyle = 'rgba(255,255,255,0.5)';
this.context.beginPath();
// BL
this.context.moveTo(startX + hmarginGrouped, startY);
// BR
this.context.lineTo(startX + hmarginGrouped + properties.variantThreedOffsetx, startY - properties.variantThreedOffsety);
// TR
this.context.lineTo(startX + properties.variantThreedOffsetx + individualBarWidth - hmarginGrouped, startY - properties.variantThreedOffsety);
// TL
this.context.lineTo(startX + individualBarWidth - hmarginGrouped, startY);
this.context.closePath();
this.context.fill();
this.context.stroke();
}
// Draw the darker side section
this.context.fillStyle = 'rgba(0,0,0,0.4)';
this.context.beginPath();
this.context.moveTo(
startX + individualBarWidth - hmarginGrouped,
startY
);
this.context.lineTo(
startX + individualBarWidth + properties.variantThreedOffsetx - hmarginGrouped,
this.data[i][j] < 0
? (this.getYCoord(0) + Math.abs(height) - properties.variantThreedOffsety) - (properties.xaxisPosition === 'center' ? 0 : Math.abs(height) - this.properties.variantThreedOffsety)
: this.getYCoord(0) - height - properties.variantThreedOffsety
);
this.context.lineTo(
startX + individualBarWidth + properties.variantThreedOffsetx - hmarginGrouped,
+this.data[i][j] < 0 && (startY + height - 5) < this.getYCoord(0)
? ((height > this.properties.variantThreedOffsety) ? this.getYCoord(0) + height - this.properties.variantThreedOffsety : this.getYCoord(0))
: (startY + height - properties.variantThreedOffsety)
);
// TL corner
this.context.lineTo(startX + individualBarWidth - hmarginGrouped, startY + height);
this.context.closePath();
this.context.fill();
this.context.stroke();
this.context.strokeStyle = prevStrokeStyle;
this.context.fillStyle = prevFillStyle;
}
if (height < 0) {
height = Math.abs(height);
startY = startY - height;
}
this.coords.push([startX + groupedMargin, startY, individualBarWidth - (2 * groupedMargin), height]);
if (typeof this.coords2[i] == 'undefined') {
this.coords2[i] = [];
}
this.coords2[i].push([startX + groupedMargin, startY, individualBarWidth - (2 * groupedMargin), height]);
// Facilitate shadows going to the left
if (properties.shadow) {
redrawCoords.push([
startX + groupedMargin,
startY,
individualBarWidth - (2 * groupedMargin),
height,
this.context.fillStyle
]);
}
}
//
// Redraw the bar if shadows are going to the left
//
if (redrawCoords.length) {
RGraph.noShadow(this);
this.context.lineWidth = properties.linewidth;
this.context.beginPath();
for (var j=0; j<redrawCoords.length; ++j) {
this.context.fillStyle = redrawCoords[j][4];
this.context.strokeStyle = properties.colorsStroke;
this.context.fillRect(redrawCoords[j][0], redrawCoords[j][1], redrawCoords[j][2], redrawCoords[j][3]);
this.context.strokeRect(redrawCoords[j][0], redrawCoords[j][1], redrawCoords[j][2], redrawCoords[j][3]);
}
this.context.fill();
this.context.stroke();
redrawCoords = [];
}
} else {
this.coords.push([]);
}
this.context.closePath();
}
// If 3D, redraw the right hand Y axis
if (properties.variant === '3d' && properties.yaxisPosition === 'right' && properties.yaxis) {
RGraph.draw3DYAxis(this);
}
//
// Turn off any shadow
//
RGraph.noShadow(this);
};
//
// Draws the labels for the graph. As of version 5.2 this
// no longer draws the X axis labels
//
this.drawLabels = function ()
{
//
// Draw above labels - nothing else todo here now that there
// are common drawXAxis() and drawYAxis() functions.
//
this.drawAboveLabels();
};
//
// Not used by the class during creating the graph, but is used by event handlers
// to get the coordinates (if any) of the selected bar
//
// @param object e The event object
// @param object OPTIONAL You can pass in the bar object instead of the
// function using "this"
//
this.getShape = function (e)
{
// This facilitates you being able to pass in the bar object as a parameter instead of
// the function getting it from itself
var obj = arguments[1] ? arguments[1] : this;
var mouseXY = RGraph.getMouseXY(e),
mouseX = mouseXY[0],
mouseY = mouseXY[1],
canvas = obj.canvas,
context = obj.context,
coords = obj.coords
for (var i=0,len=coords.length; i<len; i+=1) {
if (this.coords[i].length == 0) {
continue;
}
if (RGraph.tooltipsHotspotIgnore(this, i)) {
continue;
}
var left = coords[i][0],
top = coords[i][1],
width = coords[i][2],
height = coords[i][3];
// Old way of testing
//if (mouseX >= left && mouseX <= (left + width) && mouseY >= top && mouseY <= (top + height)) {
// Recreate the path/rectangle so that it can be tested
// ** DO NOT STROKE OR FILL IT **
if (properties.tooltipsHotspotXonly) {
this.path(
'b r % % % %',
left, this.marginTop, width, this.canvas.height - this.marginBottom
);
} else {
var indexes = RGraph.sequentialIndexToGrouped(i, this.data);
// Use the rounded rect function if the chart is stacked and the index is 0
if (properties.grouping === 'stacked' && properties.corners === 'round' && indexes[1] === 0) {
this.context.beginPath();
this.roundedCornersRect(left, top, width, height);
} else {
this.path(
'b r % % % %',
left, top, width, height
);
}
}
if (
this.context.isPointInPath(mouseX, mouseY)
&& (this.properties.clip ? RGraph.clipTo.test(this, mouseX, mouseY) : true)
) {
if (properties.tooltips && RGraph.parseTooltipText) {
var tooltip = RGraph.parseTooltipText(properties.tooltips, i);
}
// Work out the dataset
var dataset = 0,
idx = i
while (idx >= (typeof obj.data[dataset] === 'object' && obj.data[dataset] ? obj.data[dataset].length : 1)) {
if (typeof obj.data[dataset] === 'number') {
idx -= 1;
} else if (obj.data[dataset]) { // Accounts for null being an object
idx -= obj.data[dataset].length;
} else {
idx -= 1;
}
dataset++;
}
if (typeof obj.data[dataset] == 'number') {
idx = 0;
}
return {
object: this,
x: left,
y: top,
width: width,
height: height,
tooltip: typeof tooltip === 'string' ? tooltip : null,
label: properties.xaxisLabels && typeof properties.xaxisLabels[dataset] === 'string' ? properties.xaxisLabels[dataset] : null,
dataset: dataset,
index: idx,
sequentialIndex: i
};
}
}
return null;
};
//
// This retrives the bar based on the X coordinate only.
//
// @param object e The event object
// @param object OPTIONAL You can pass in the bar object instead of the
// function using "this"
//
this.getShapeByX = function (e)
{
var canvas = e.target;
var mouseCoords = RGraph.getMouseXY(e);
// This facilitates you being able to pass in the bar object as a parameter instead of
// the function getting it from itself
var obj = arguments[1] ? arguments[1] : this;
//
// Loop through the bars determining if the mouse is over a bar
//
for (var i=0,len=obj.coords.length; i<len; i++) {
if (obj.coords[i].length == 0) {
continue;
}
var mouseX = mouseCoords[0];
var mouseY = mouseCoords[1];
var left = obj.coords[i][0];
var top = obj.coords[i][1];
var width = obj.coords[i][2];
var height = obj.coords[i][3];
if (mouseX >= left && mouseX <= (left + width)) {
if (properties.tooltips) {
var tooltip = RGraph.parseTooltipText ? RGraph.parseTooltipText(properties.tooltips, i) : properties.tooltips[i];
}
var indexes = RGraph.sequentialIndexToGrouped(i, this.data);
return {
object: obj,
x: left,
y: top,
width: width,
height: height,
dataset: indexes[0],
index: indexes[1],
sequentialIndex: i,
tooltip: typeof tooltip === 'string' ? tooltip : null
};
}
}
return null;
};
//
// When you click on the chart, this method can return the Y value at that point. It works for any point on the
// chart (that is inside the margins) - not just points within the Bars.
//
// EITHER:
//
// @param object arg The event object
//
// OR:
//
// @param object arg A two element array containing the X and Y coordinates
//
this.getValue = function (arg)
{
if (arg.length == 2) {
var mouseX = arg[0];
var mouseY = arg[1];
} else {
var mouseCoords = RGraph.getMouseXY(arg);
var mouseX = mouseCoords[0];
var mouseY = mouseCoords[1];
}
if ( mouseY < properties.marginTop
|| mouseY > (this.canvas.height - properties.marginBottom)
|| mouseX < properties.marginLeft
|| mouseX > (this.canvas.width - properties.marginRight)
) {
return null;
}
if (properties.xaxisPosition == 'center') {
var value = (((this.grapharea / 2) - (mouseY - properties.marginTop)) / this.grapharea) * (this.scale2.max - this.scale2.min)
value *= 2;
if (value >= 0) {
value += this.scale2.min;
} else {
value -= this.scale2.min;
}
} else if (properties.xaxisPosition == 'top') {
var value = ((this.grapharea - (mouseY - properties.marginTop)) / this.grapharea) * (this.scale2.max - this.scale2.min)
value = this.scale2.max - value;
value = Math.abs(value) * -1;
} else {
var value = ((this.grapharea - (mouseY - properties.marginTop)) / this.grapharea) * (this.scale2.max - this.scale2.min)
value += this.scale2.min;
}
return value;
};
//
// This function can be used when the canvas is clicked on (or similar - depending on the event)
// to retrieve the relevant Y coordinate for a particular value.
//
// @param int value The value to get the Y coordinate for
//
this.getYCoord = function (value)
{
if (value > this.scale2.max) {
return null;
}
var y, xaxispos = properties.xaxisPosition;
if (xaxispos == 'top') {
// Account for negative numbers
if (value < 0) {
value = Math.abs(value);
}
y = ((value - this.scale2.min) / (this.scale2.max - this.scale2.min)) * this.grapharea;
y = y + this.marginTop
} else if (xaxispos == 'center') {
y = ((value - this.scale2.min) / (this.scale2.max - this.scale2.min)) * (this.grapharea / 2);
y = (this.grapharea / 2) - y;
y += this.marginTop;
} else {
if (value < this.scale2.min) {
value = this.scale2.min;
}
y = ((value - this.scale2.min) / (this.scale2.max - this.scale2.min));
y *= (this.canvas.height - this.marginTop - this.marginBottom);
y = this.canvas.height - this.marginBottom - y;
}
return y;
};
//
// Each object type has its own Highlight() function which
// highlights the appropriate shape.
//
// @param object shape The shape to highlight.
//
this.highlight = function (shape)
{
// If the highlightFade property is not set then call
// the highlight function multiple times when the
// globalAlpha is set at varying levels of transparency
// to fade the highlight in. If the variable is set,
// however, then this call is probably a part of said
// highlighting process so don't call ourselves
// again - just highlight the chart.
if (this.properties.highlightFade && !this.highlight.highlighting) {
// Add a variable to the highlight function (this
// function) to indicate that we're currently in
// the process of highlighting.
this.highlight.highlighting = true;
var obj = this;
setTimeout(function() {obj.context.globalAlpha = 0.2; obj.highlight(shape); obj.context.globalAlpha = 1;}, 20);
setTimeout(function() {obj.context.globalAlpha = 0.2; obj.highlight(shape); obj.context.globalAlpha = 1;}, 40);
setTimeout(function() {obj.context.globalAlpha = 0.2; obj.highlight(shape); obj.context.globalAlpha = 1;}, 60);
setTimeout(function() {obj.context.globalAlpha = 0.2; obj.highlight(shape); obj.context.globalAlpha = 1;}, 80);
setTimeout(function() {obj.context.globalAlpha = 0.2; obj.highlight(shape); obj.context.globalAlpha = 1;}, 100);
setTimeout(function() {obj.context.globalAlpha = 0.2; obj.highlight(shape); obj.context.globalAlpha = 1;}, 120);
setTimeout(function() {obj.context.globalAlpha = 0.2; obj.highlight(shape); obj.context.globalAlpha = 1;}, 140);
setTimeout(function() {obj.highlight.highlighting = false;}, 400);
return;
}
RGraph.clipTo.callback(this, function (obj)
{
if (typeof obj.properties.highlightStyle === 'function') {
(obj.properties.highlightStyle)(shape);
// Highlight all of the rects except this one - essentially
// an inverted highlight
} else if (typeof obj.properties.highlightStyle === 'string' && obj.properties.highlightStyle === 'invert') {
for (var i=0; i<obj.coords.length; ++i) {
if (i !== shape.sequentialIndex) {
obj.path(
'b r % % % % s % f %',
obj.coords[i][0],obj.coords[i][1],obj.coords[i][2],obj.coords[i][3],
obj.properties.highlightStroke,
obj.properties.highlightFill
);
}
}
// Redraw the X axis so the highlight doesn't
// appear over the X axis. But not the X axis
// labels or the title. This is new in
// September 2024.
if (obj.properties.variant !== 'dumbbell') {
RGraph.drawXAxis(obj, {
labels: false,
title: false
});
}
} else {
if (properties.grouping === 'stacked' && shape.index === 0 && obj.properties.xaxisPosition === 'bottom') {
obj.context.beginPath();
obj.context.strokeStyle = obj.properties.highlightStroke;
obj.context.fillStyle = obj.properties.highlightFill;
if (obj.properties.corners === 'round') {
obj.roundedCornersRect(shape.x,shape.y,shape.width,shape.height);
} else {
obj.context.rect(shape.x, shape.y, shape.width, shape.height);
}
obj.context.stroke();
obj.context.fill();
} else {
// Add the new highlight
RGraph.Highlight.rect(obj, shape);
// Redraw the X axis so the highlight doesn't
// appear over the X axis. But not the X axis
// labels or the title. This is new in
// September 2024.
if (obj.properties.variant !== 'dumbbell') {
RGraph.drawXAxis(obj, {
labels: false,
title: false
});
}
}
}
}); // End of clipping callback function.
};
//
// The getObjectByXY() worker method
//
this.getObjectByXY = function (e)
{
var mouseXY = RGraph.getMouseXY(e);
var shape = this.getShape(e);
// Adjust the mouse Y coordinate for when the bar chart is
// a 3D variant if the textAccessible wrapper DIV is not here
if (properties.variant === '3d' && !properties.textAccessible) {
var adjustment = properties.variantThreedAngle * mouseXY[0];
mouseXY[1] -= adjustment;
}
var dbEndTopradius = 0;
var dbEndBottomradius = 0;
if (this.properties.variant === 'dumbbell') {
dbEndTopradius = this.properties.variantDumbbellEndTopRadius || this.properties.variantDumbbellEndRadius;
dbEndBottomradius = this.properties.variantDumbbellEndBottomRadius || this.properties.variantDumbbellEndRadius;
}
if (
mouseXY[0] >= this.properties.marginLeft
&& mouseXY[0] <= (this.canvas.width - this.properties.marginRight)
&& mouseXY[1] >= this.properties.marginTop - dbEndTopradius
&& mouseXY[1] <= (this.canvas.height - this.properties.marginBottom + dbEndBottomradius)
) {
return this;
}
};
//
// This method handles the adjusting calculation for when the mouse is moved
//
// @param object e The event object
//
this.adjusting_mousemove = function (e)
{
//
// Handle adjusting for the Bar
//
if (properties.adjustable && RGraph.Registry.get('adjusting') && RGraph.Registry.get('adjusting').uid == this.uid) {
// Rounding the value to the given number of decimals make the chart step
var value = Number(this.getValue(e));
var shape = RGraph.Registry.get('adjusting.shape')
if (shape) {
RGraph.Registry.set('adjusting.shape', shape);
if (this.stackedOrGrouped && properties.grouping == 'grouped') {
var indexes = RGraph.sequentialIndexToGrouped(shape.sequentialIndex, this.data);
if (typeof this.data[indexes[0]] == 'number') {
this.data[indexes[0]] = Number(value);
} else if (!RGraph.isNullish(this.data[indexes[0]])) {
this.data[indexes[0]][indexes[1]] = Number(value);
}
} else if (typeof this.data[shape.sequentialIndex] == 'number') {
this.data[shape.sequentialIndex] = Number(value);
}
RGraph.redrawCanvas(e.target);
RGraph.fireCustomEvent(this, 'onadjust');
}
}
};
//
// This allows for easy specification of gradients
//
this.parseColors = function ()
{
// Save the original colors so that they can be restored when the canvas is reset
if (this.original_colors.length === 0) {
this.original_colors.colors = RGraph.arrayClone(properties.colors);
this.original_colors.keyColors = RGraph.arrayClone(properties.keyColors);
this.original_colors.crosshairsColor = properties.crosshairsColor;
this.original_colors.highlightStroke = properties.highlightStroke;
this.original_colors.highlightFill = properties.highlightFill;
this.original_colors.textColor = properties.textColor;
this.original_colors.backgroundBarsColor1 = properties.backgroundBarsColor1;
this.original_colors.backgroundBarsColor2 = properties.backgroundBarsColor2;
this.original_colors.backgroundGridColor = properties.backgroundGridColor;
this.original_colors.backgroundColor = properties.backgroundColor;
this.original_colors.colorsStroke = properties.colorsStroke;
this.original_colors.axesColor = properties.axesColor;
}
// colors
var colors = properties.colors;
if (colors) {
for (var i=0; i<colors.length; ++i) {
colors[i] = this.parseSingleColorForGradient(colors[i]);
}
}
// keyColors
var colors = properties.keyColors;
if (colors) {
for (var i=0; i<colors.length; ++i) {
colors[i] = this.parseSingleColorForGradient(colors[i]);
}
}
properties.crosshairsColor = this.parseSingleColorForGradient(properties.crosshairsColor);
properties.highlightStroke = this.parseSingleColorForGradient(properties.highlightStroke);
properties.highlightFill = this.parseSingleColorForGradient(properties.highlightFill);
properties.textColor = this.parseSingleColorForGradient(properties.textColor);
properties.backgroundBarsColor1 = this.parseSingleColorForGradient(properties.backgroundBarsColor1);
properties.backgroundBarsColor2 = this.parseSingleColorForGradient(properties.backgroundBarsColor2);
properties.backgroundGridColor = this.parseSingleColorForGradient(properties.backgroundGridColor);
properties.backgroundColor = this.parseSingleColorForGradient(properties.backgroundColor);
properties.colorStroke = this.parseSingleColorForGradient(properties.colorStroke);
properties.axesColor = this.parseSingleColorForGradient(properties.axesColor);
};
//
// Use this function to reset the object to the post-constructor state. Eg reset colors if
// need be etc
//
this.reset = function ()
{
};
//
// This parses a single color value. This method can also parse the new
// JSON gradient syntax.
//
// @param string The color to parse
//
this.parseSingleColorForGradient = function (color)
{
if (!color || typeof color != 'string') {
return color;
}
if (color.match(/^gradient\((.*)\)$/i)) {
// Allow for JSON gradients
if (color.match(/^gradient\(({.*})\)$/i)) {
return RGraph.parseJSONGradient({
object: this,
def: RegExp.$1
});
}
var parts = RegExp.$1.split(':');
// Create the gradient
var grad = this.context.createLinearGradient(0,this.canvas.height - properties.marginBottom, 0, properties.marginTop);
var diff = 1 / (parts.length - 1);
grad.addColorStop(0, RGraph.trim(parts[0]));
for (var j=1,len=parts.length; j<len; ++j) {
grad.addColorStop(j * diff, RGraph.trim(parts[j]));
}
}
return grad ? grad : color;
};
this.drawBevel = function ()
{
var coords = this.coords,
coords2 = this.coords2;
if (properties.grouping == 'stacked') {
for (var i=0; i<coords2.length; ++i) {
if (coords2[i] && coords2[i][0] && coords2[i][0][0]) {
var x = coords2[i][0][0];
var y = coords2[i][0][1];
var w = coords2[i][0][2];
var arr = [];
for (var j=0; j<coords2[i].length; ++j) {
arr.push(coords2[i][j][3]);
}
var h = RGraph.arraySum(arr);
this.context.save();
this.context.strokeStyle = 'black';
// Clip to the rect
this.context.beginPath();
this.context.rect(x, y, w, h);
this.context.clip();
// Add the shadow
this.context.shadowColor = 'black';
this.context.shadowOffsetX = 0;
this.context.shadowOffsetY = 0;
this.context.shadowBlur = 20;
this.context.beginPath();
this.context.rect(x - 3, y - 3, w + 6, h + 100);
this.context.lineWidth = 5;
this.context.stroke();
this.context.restore();
}
}
} else {
for (var i=0; i<coords.length; ++i) {
if (coords[i]) {
var x = coords[i][0];
var y = coords[i][1];
var w = coords[i][2];
var h = coords[i][3];
var xaxispos = properties.xaxisPosition;
var xaxis_ycoord = ((this.canvas.height - this.marginTop - this.marginBottom) / 2) + this.marginTop;
this.context.save();
this.context.strokeStyle = 'black';
// Clip to the rect
this.context.beginPath();
this.context.rect(x, y, w, h);
this.context.clip();
// Add the shadow
this.context.shadowColor = 'black';
this.context.shadowOffsetX = 0;
this.context.shadowOffsetY = 0;
this.context.shadowBlur = 20;
if (xaxispos == 'top' || (xaxispos == 'center' && (y + h) > xaxis_ycoord)) {
y = y - 100;
h = h + 100;
} else {
y = y;
h = h + 100;
}
this.context.beginPath();
this.context.rect(x - 3, y - 3, w + 6, h + 6);
this.context.lineWidth = 5;
this.context.stroke();
this.context.restore();
}
}
}
};
//
// This function handles highlighting an entire data-series for the interactive
// key
//
// @param int index The index of the data series to be highlighted
//
this.interactiveKeyHighlight = function (index)
{
var obj = this;
this.coords2.forEach(function (value, idx, arr)
{
if (typeof value[index] == 'object' && value[index]) {
var x = value[index][0] - 0.5,
y = value[index][1] - 0.5,
w = value[index][2] + 1,
h = value[index][3] + 1;
obj.context.fillStyle = properties.keyInteractiveHighlightChartFill;
obj.context.strokeStyle = properties.keyInteractiveHighlightChartStroke;
obj.context.lineWidth = properties.keyInteractiveHighlightChartLinewidth;
obj.context.strokeRect(x, y, w, h);
obj.context.fillRect(x, y, w, h);
}
});
};
//
// Using a function to add events makes it easier to facilitate method chaining
//
// @param string type The type of even to add
// @param function func
//
this.on = function (type, func)
{
if (type.substr(0,2) !== 'on') {
type = 'on' + type;
}
if (typeof this[type] !== 'function') {
this[type] = func;
} else {
RGraph.addCustomEventListener(this, type, func);
}
return this;
};
// Draws the above labels
this.drawLabelsAbove =
this.drawAboveLabels = function ()
{
var labels = properties.labelsAbove,
specific = properties.labelsAboveSpecific,
bold = typeof properties.labelsAboveBold === 'boolean' ? properties.labelsAboveBold : properties.textBold,
italic = typeof properties.labelsAboveItalic === 'boolean' ? properties.labelsAboveItalic : properties.textItalic,
color = properties.labelsAboveColor || properties.textColor,
font = properties.labelsAboveFont || properties.textFont,
size = typeof properties.labelsAboveSize === 'number' ? properties.labelsAboveSize : properties.textSize,
background = properties.labelsAboveBackground,
decimals = properties.labelsAboveDecimals,
angle = -1 * properties.labelsAboveAngle,
unitsPre = properties.labelsAboveUnitsPre,
unitsPost = properties.labelsAboveUnitsPost,
point = properties.labelsAbovePoint,
thousand = properties.labelsAboveThousand,
formatter = properties.labelsAboveFormatter,
coords = this.coords,
coords2 = this.coords2,
data = this.data,
ldata = RGraph.arrayLinearize(this.data),
offsetx = properties.labelsAboveOffsetx,
offsety = properties.labelsAboveOffsety,
text_italic = properties.textItalic,
text_bold = properties.textBold,
text_color = properties.textColor,
text_font = properties.textFont,
text_size = properties.textSize,
grouping = properties.grouping;
// BC
if (typeof properties.labelsAboveOffset === 'number') {
offsety = properties.labelsAboveOffset;
}
var textConf = RGraph.getTextConf({
object: this,
prefix: 'labelsAbove'
});
// Turn off any shadow
RGraph.noShadow(this);
// Color
this.context.fillStyle = textConf.color;
// This bit draws the text labels that appear above the bars if requested
if (labels && grouping === 'grouped') {
for (var i=0,len=data.length,sequentialIndex=0; i<len; i+=1) {
// Alignment for regular, positive bars
if (typeof data[i] === 'number' && data[i] >= 0) {
var angle = angle;
var halign = (angle ? 'left' : 'center');
var valign = angle !== 0 ? 'center' : 'bottom';
RGraph.text({
object: this,
font: textConf.font,
size: textConf.size,
color: textConf.color,
bold: textConf.bold,
italic: textConf.italic,
x: coords2[i][0][0] + (coords2[i][0][2] / 2) + offsetx,
y: coords2[i][0][1] - offsety - 3,
text: specific ? (specific[sequentialIndex] || '') : RGraph.numberFormat({
object: this,
number: Number(typeof data[i] === 'object' ? data[i][0] : data[i]).toFixed(decimals),
value: Number(typeof data[i] === 'object' ? data[i][0] : data[i]).toFixed(decimals),
unitspre: unitsPre,
unitspost: unitsPost,
point: point,
thousand: thousand,
formatter: formatter,
dataset: 0,
index: i
}),
halign: halign,
valign: valign,
angle: angle,
marker: false,
bounding: true,
'bounding.fill': background,
'bounding.stroke': 'rgba(0,0,0,0)',
tag: 'labels.above'
});
sequentialIndex++;
// Alignment for regular, negative bars
} else if (typeof data[i] === 'number' && data[i] < 0) {
var angle = angle;
var halign = angle ? 'right' : 'center';
var valign = angle !== 0 ? 'center' : 'top';
RGraph.text({
object: this,
font: textConf.font,
size: textConf.size,
color: textConf.color,
bold: textConf.bold,
italic: textConf.italic,
x: coords2[i][0][0] + (coords2[i][0][2] / 2) + offsetx,
y: coords2[i][0][1] + coords2[i][0][3] + offsety + 5,
text: specific ? (specific[sequentialIndex] || '') : RGraph.numberFormat({
object: this,
number: Number(typeof data[i] === 'object' ? data[i][0] : data[i]).toFixed(decimals),
value: Number(typeof data[i] === 'object' ? data[i][0] : data[i]).toFixed(decimals),
unitspre: unitsPre,
unitspost: unitsPost,
point: point,
thousand: thousand,
formatter: formatter,
dataset: 0,
index: i,
}),
halign: halign,
valign: valign,
angle: angle,
bounding: true,
'bounding.fill':background,
'bounding.stroke':'rgba(0,0,0,0)',
marker: false,
tag: 'labels.above'
});
sequentialIndex++;
// Alignment for grouped bars
} else if (typeof data[i] === 'object') {
for (var j=0,len2=data[i].length; j<len2; j+=1) {
var angle = angle;
var halign = data[i][j] < 0 ? 'right' : 'left';
halign = angle === 0 ? 'center' : halign;
var valign = data[i][j] < 0 ? 'top' : 'bottom';
valign = angle != 0 ? 'center' : valign;
RGraph.text({
object: this,
font: textConf.font,
size: textConf.size,
color: textConf.color,
bold: textConf.bold,
italic: textConf.italic,
x: coords2[i][j][0] + (coords2[i][j][2] / 2) + offsetx,
y: properties.xaxisPosition === 'top' ? coords2[i][j][1] + coords2[i][j][3] + 5 : coords2[i][j][1] + (data[i][j] < 0 ? coords2[i][j][3] + offsety + 5: -offsety),
text: specific ? (specific[sequentialIndex] || '') : RGraph.numberFormat({
object: this,
number: Number(data[i][j]).toFixed(decimals),
value: Number(data[i][j]).toFixed(decimals),
unitspre: unitsPre,
unitspost: unitsPost,
point: point,
thousand: thousand,
formatter: formatter,
dataset: i,
index: j
}),
halign: halign,
valign: properties.xaxisPosition === 'top' ? 'top' : valign,
angle: angle,
bounding: true,
'bounding.fill': background,
'bounding.stroke': 'rgba(0,0,0,0)',
marker: false,
tag: 'labels.above'
});
sequentialIndex++;
}
}
}
//
// STACKED bars
//
} else if (labels && grouping === 'stacked') {
for (var i=0,len=data.length,sequentialIndex=0; i<len; i+=1) {
if (typeof data[i] === 'object') {
var angle = angle;
var halign = angle != 0 ? 'left' : 'center';
var valign = angle != 0 ? 'center' : 'bottom';
RGraph.text({
object: this,
font: textConf.font,
size: textConf.size,
color: textConf.color,
bold: textConf.bold,
italic: textConf.italic,
x: coords2[i][0][0] + (coords2[i][0][2] / 2) + offsetx,
y: coords2[i][0][1] + (data[i][0] < 0 ? coords2[i][0][3] : 0) + (properties.xaxisPosition === 'top' ? (coords2[i].reduce( (acc, curr) => acc += curr[3], 0) + 3 + offsety) : (-3 - offsety) ),
text: specific ? (specific[sequentialIndex] || '') : RGraph.numberFormat({
object: this,
number: Number(RGraph.arraySum(data[i])).toFixed(decimals),
value: Number(RGraph.arraySum(data[i])).toFixed(decimals),
unitspre: unitsPre,
unitspost: unitsPost,
point: point,
thousand: thousand,
formatter: formatter,
dataset: i
}),
halign: halign,
valign: properties.xaxisPosition === 'top' ? 'top' : valign,
angle: angle,
bounding: true,
'bounding.fill':background,
'bounding.stroke': 'rgba(0,0,0,0)',
marker: false,
tag: 'labels.above'
});
sequentialIndex += data[i].length;
//
// Regular numbers but in a stacked grouping
//
} else {
var angle = angle;
var halign = angle != 0 ? 'left' : 'center';
var valign = angle != 0 ? 'center' : 'bottom';
RGraph.text({
object: this,
font: textConf.font,
size: textConf.size,
color: textConf.color,
bold: textConf.bold,
italic: textConf.italic,
x: coords2[i][0][0] + (coords2[i][0][2] / 2) + offsetx,
y: coords2[i][0][1] + (data[i][0] < 0 ? coords2[i][0][3] : 0) + (properties.xaxisPosition === 'top' ? coords2[i][0][3] + offsety + 3 : 0 - offsety - 3),
text: specific ? (specific[sequentialIndex] || '') : RGraph.numberFormat({
object: this,
number: Number(data[i]).toFixed(decimals),
value: Number(data[i]).toFixed(decimals),
unitspre: unitsPre,
unitspost: unitsPost,
point: point,
thousand: thousand,
formatter: formatter,
dataset: i
}),
halign: halign,
valign: properties.xaxisPosition === 'top' ? 'top' : valign,
angle: angle,
bounding: true,
'bounding.fill': background,
'bounding.stroke': 'rgba(0,0,0,0)',
marker: false,
tag: 'labels.above'
});
sequentialIndex++;
}
}
}
};
//
// This function runs once only
//
this.firstDrawFunc = function ()
{
};
//
// (new) Bar chart Wave effect. This is a rewrite that should be smoother
// because it just uses a single loop and not setTimeout
//
// @param object OPTIONAL An object map of options. You specify 'frames' here to give the number of frames in the effect
// @param function OPTIONAL A function that will be called when the effect is complete
//
this.wave = function ()
{
// Cancel any stop request if one is pending
this.cancelStopAnimation();
// Reset the data to the original
this.data = RGraph.arrayClone(this.original_data);
// If there's only one bar call the grow function instead
if (this.data.length === 1) {
return this.grow(arguments[0], arguments[1]);
}
var obj = this,
opt = arguments[0] || {},
labelsAbove = this.get('labelsAbove');
opt.frames = opt.frames || 60;
opt.startFrames = [];
opt.counters = [];
var framesperbar = opt.frames / 3,
frame = -1,
callback = arguments[1] || function () {},
original = RGraph.arrayClone(this.original_data);
//
// turn off the labelsAbove option whilst animating
//
this.set('labelsAbove', false);
for (var i=0,len=obj.data.length; i<len; i+=1) {
opt.startFrames[i] = ((opt.frames / 2) / (obj.data.length - 1)) * i;
if (typeof obj.data[i] === 'object' && obj.data[i]) {
opt.counters[i] = [];
for (var j=0; j<obj.data[i].length; j++) {
opt.counters[i][j] = 0;
}
} else {
opt.counters[i] = 0;
}
}
//
// This stops the chart from jumping
//
obj.draw();
obj.set('yaxisScaleMax', obj.scale2.max);
RGraph.clear(obj.canvas);
function iterator ()
{
if (obj.stopAnimationRequested) {
// Reset the flag
obj.stopAnimationRequested = false;
// Reset the data
obj.data = RGraph.arrayClone(obj.original_data);
return;
}
++frame;
for (let i=0,len=obj.data.length; i<len; i+=1) {
if (frame > opt.startFrames[i]) {
if (typeof obj.data[i] === 'number') {
obj.data[i] = Math.min(
Math.abs(original[i]),
Math.abs(original[i] * ( (opt.counters[i]++) / framesperbar))
);
// Make the number negative if the original was
if (original[i] < 0) {
obj.data[i] *= -1;
}
} else if (!RGraph.isNullish(obj.data[i])) {
for (let j=0,len2=obj.data[i].length; j<len2; j+=1) {
obj.data[i][j] = Math.min(
Math.abs(original[i][j]),
Math.abs(original[i][j] * ( (opt.counters[i][j]++) / framesperbar))
);
// Make the number negative if the original was
if (original[i][j] < 0) {
obj.data[i][j] *= -1;
}
}
}
} else {
obj.data[i] = typeof obj.data[i] === 'object' && obj.data[i] ? RGraph.arrayPad([], obj.data[i].length, 0) : (RGraph.isNullish(obj.data[i]) ? null : 0);
}
}
if (frame >= opt.frames) {
if (labelsAbove) {
obj.set('labelsAbove', true);
RGraph.redraw();
}
callback(obj);
} else {
RGraph.redrawCanvas(obj.canvas);
RGraph.Effects.updateCanvas(iterator);
}
}
iterator();
return this;
};
//
// Color Wave effect. This fades in color sequentially like the wave effect
// makes the bars grow.
//
// @param object OPTIONAL An object map of options. You specify 'frames'
// here to give the number of frames in the effect
// @param function OPTIONAL A function that will be called when the effect
// is complete
//
this.colorwave =
this.colorWave = function ()
{
// Cancel any stop request if one is pending
this.cancelStopAnimation();
// Reset the data to the original
this.data = RGraph.arrayClone(this.original_data);
var obj = this,
opt = arguments[0] || {};
opt.frames = opt.frames || 60;
opt.startFrames = [];
opt.counters = [];
colors = obj.properties.colors;
// If just one color is specified and colorsSequential is not, then
// pad the colors array out
if (colors.length <= obj.data.length) {
obj.set('colorsSequential', true);
colors = RGraph.arrayPad(colors, obj.data.length, colors[colors.length - 1]);
}
var framesperbar = opt.frames / 2,
frame = -1,
callback = arguments[1] || function () {},
originalColors = RGraph.arrayClone(obj.properties.colors);
for (var i=0,len=originalColors.length; i<len; i+=1) {
opt.startFrames[i] = ((opt.frames / 2) / (originalColors.length - 1)) * i;
opt.counters[i] = 0;
}
function iterator ()
{
++frame;
for (var i=0,len=colors.length; i<len; i+=1) {
if (frame > opt.startFrames[i] && colors[i].match(/^rgba?\(([0-9 ]+),([0-9 ]+),([0-9 ]+)(,([ 0-9.]+)?)\)/)) {
// DO NOT USE SPACES!
colors[i] = 'rgba({1},{2},{3},{4})'.format(
RegExp.$1,
RegExp.$2,
RegExp.$3,
(frame - opt.startFrames[i]) / framesperbar
);
} else {
colors[i] = colors[i].replace(/,[0-9. ]+\)/, ',0)');
}
}
if (frame >= opt.frames) {
callback(obj);
} else {
RGraph.redrawCanvas(obj.canvas);
RGraph.Effects.updateCanvas(iterator);
}
}
iterator();
return this;
};
//
// Grow
//
// The Bar chart Grow effect gradually increases the values of the bars
//
// @param object An object of options - eg: {frames: 30}
// @param function A function to call when the effect is complete
//
this.grow = function ()
{
// Cancel any stop request if one is pending
this.cancelStopAnimation();
// Reset the data to the original
this.data = RGraph.arrayClone(this.original_data);
// Callback
var opt = arguments[0] || {},
frames = opt.frames || 60,
frame = 0,
callback = arguments[1] || function () {},
obj = this,
labelsAbove = this.get('labelsAbove');
// Go through the data and change string arguments of the format +/-[0-9]
// to absolute numbers
if (RGraph.isArray(opt.data)) {
var ymax = 0;
for (var i=0; i<opt.data.length; ++i) {
if (typeof opt.data[i] === 'object') {
for (var j=0; j<opt.data[i].length; ++j) {
if (typeof opt.data[i][j] === 'string'&& opt.data[i][j].match(/(\+|\-)([0-9]+)/)) {
if (RegExp.$1 === '+') {
opt.data[i][j] = this.original_data[i][j] + parseInt(RegExp.$2);
} else {
opt.data[i][j] = this.original_data[i][j] - parseInt(RegExp.$2);
}
}
ymax = Math.max(ymax, opt.data[i][j]);
}
} else if (typeof opt.data[i] === 'string' && opt.data[i].match(/(\+|\-)([0-9]+)/)) {
if (RegExp.$1 === '+') {
opt.data[i] = this.original_data[i] + parseInt(RegExp.$2);
} else {
opt.data[i] = this.original_data[i] - parseInt(RegExp.$2);
}
ymax = Math.max(ymax, opt.data[i]);
} else {
ymax = Math.max(ymax, opt.data[i]);
}
}
var scale = RGraph.getScale({object: this, options: {'scale.max':ymax}});
if (typeof properties.yaxisScaleMax !== 'number') {
this.set('yaxisScaleMax', scale.max);
}
}
//
// turn off the labelsAbove option whilst animating
//
this.set('labelsAbove', false);
// Stop the scale from changing by setting yaxisScaleMax (if it's not already set)
if (properties.yaxisScaleMax == null) {
var ymax = 0;
for (var i=0; i<this.data.length; ++i) {
if (RGraph.isArray(this.data[i]) && properties.grouping === 'stacked') {
ymax = Math.max(ymax, Math.abs(RGraph.arraySum(this.data[i])));
} else if (RGraph.isArray(this.data[i]) && properties.grouping === 'grouped') {
for (var j=0,group=[]; j<this.data[i].length; j++) {
group.push(Math.abs(this.data[i][j]));
}
ymax = Math.max(ymax, Math.abs(RGraph.arrayMax(group)));
} else {
ymax = Math.max(ymax, Math.abs(this.data[i]));
}
}
var scale = RGraph.getScale({object: this, options: {'scale.max':ymax}});
this.set('yaxisScaleMax', scale.max);
}
// You can give a ymax to the grow function
if (typeof opt.ymax === 'number') {
this.set('yaxisScaleMax', opt.ymax);
}
var iterator = function ()
{
if (obj.stopAnimationRequested) {
// Reset the flag
obj.stopAnimationRequested = false;
// Reset the data
obj.data = RGraph.arrayClone(obj.original_data);
return;
}
var easingMultiplier = RGraph.Effects.getEasingMultiplier(frames, frame);
// Alter the Bar chart data depending on the frame
for (var j=0,len=obj.original_data.length; j<len; ++j) {
if (typeof obj.data[j] === 'object' && !RGraph.isNullish(obj.data[j])) {
for (var k=0,len2=obj.data[j].length; k<len2; ++k) {
if (obj.firstDraw || !opt.data) {
obj.data[j][k] = easingMultiplier * obj.original_data[j][k];
} else if (opt.data && opt.data.length === obj.original_data.length) {
var diff = opt.data[j][k] - obj.original_data[j][k];
obj.data[j][k] = (easingMultiplier * diff) + obj.original_data[j][k];
}
}
} else {
if (obj.firstDraw || !opt.data) {
obj.data[j] = easingMultiplier * obj.original_data[j];
} else if (opt.data && opt.data.length === obj.original_data.length) {
var diff = opt.data[j] - obj.original_data[j];
obj.data[j] = (easingMultiplier * diff) + obj.original_data[j];
}
}
}
//RGraph.clear(obj.canvas);
RGraph.redrawCanvas(obj.canvas);
if (frame < frames) {
frame += 1;
RGraph.Effects.updateCanvas(iterator);
// Call the callback function
} else {
// Do some housekeeping if new data was specified thats done in
// the constructor - but needs to be redone because new data
// has been specified
if (RGraph.isArray(opt.data)) {
var linear_data = RGraph.arrayLinearize(data);
for (var i=0; i<linear_data.length; ++i) {
if (!obj['$' + i]) {
obj['$' + i] = {};
}
}
}
obj.data = data;
obj.original_data = RGraph.arrayClone(data);
if (labelsAbove) {
obj.set('labelsAbove', true);
RGraph.redraw();
}
callback(obj);
}
};
iterator();
return this;
};
//
// Couple of functions that allow you to control the
// Bipolar animation effects
//
this.stopAnimation = function ()
{
this.stopAnimationRequested = true;
};
this.cancelStopAnimation = function ()
{
this.stopAnimationRequested = false;
};
//
// Draws error-bars for the Bar and Line charts
//
this.drawErrorbars = function ()
{
var coords = this.coords,
color = properties.errorbarsColor || 'black',
default_halfwidth = Math.min(properties.errorbarsCappedWidth, coords[0][2]) / 2,
x = 0,
errorbars = properties.errorbars,
length = 0;
// If not capped set the width of the cqap to zero
if (!properties.errorbarsCapped) {
properties.errorbarsCappedWidth = 0;
halfwidth = 0;
}
// Set the linewidth
this.context.lineWidth = properties.errorbarsLinewidth;
for (var i=0; i<coords.length; ++i) {
var barX = coords[i][0],
barY = coords[i][1],
barW = coords[i][2],
barH = coords[i][3];
// Get the grouped version of the index
var groupedIndexes = RGraph.sequentialIndexToGrouped(i, this.data);
// Determine if this is
if (typeof this.data[groupedIndexes[0]] === 'object' && !RGraph.isNullish(this.data[groupedIndexes[0]])) {
var isGrouped = true,
group = groupedIndexes[0],
subgroup = groupedIndexes[1];
}
// Default to black
color = properties.errorbarsColor || 'black';
// Set the perbar linewidth if the fourth option in the array
// is specified
if (errorbars[i] && typeof errorbars[i][3] === 'number') {
this.context.lineWidth = errorbars[i][3];
}
// Set the halfwidth
var halfwidth = (errorbars[i]&& typeof errorbars[i][4] === 'number') ? errorbars[i][4] / 2 : default_halfwidth;
if (!properties.errorbarsCapped) {
halfwidth = 0;
}
// Calulate the pixel size
if (typeof errorbars[i] === 'number') {
length = Math.abs(this.getYCoord(errorbars[i]) - this.getYCoord(0));
if (length) {
this.path(
'b % % l % % l % % l % % s %',
barX + (barW / 2),
(typeof this.data[i] === 'number' && this.data[i] < 0 || (isGrouped && this.data[group][subgroup] < 0) ) ? barY + barH : barY,
barX + (barW / 2),
(typeof this.data[i] === 'number' && this.data[i] < 0 || (isGrouped && this.data[group][subgroup] < 0)) ? barY + barH + length : barY - length,
barX + (barW / 2) - halfwidth,
(typeof this.data[i] === 'number' && this.data[i] < 0 || (isGrouped && this.data[group][subgroup] < 0)) ? Math.round(barY + barH + length) : Math.round(barY - length),
barX + (barW / 2) + halfwidth,
(typeof this.data[i] === 'number' && this.data[i] < 0 || (isGrouped && this.data[group][subgroup] < 0)) ? Math.round(barY + barH + length) : Math.round(barY - length),
color
);
}
} else if (typeof errorbars[i] === 'object' && !RGraph.isNullish(errorbars[i])) {
var positiveLength = Math.abs(this.getYCoord(errorbars[i][0]) - this.getYCoord(0));
// Color
if (typeof errorbars[i][1] === 'string') {
color = errorbars[i][1];
} else if (typeof errorbars[i][2] === 'string') {
color = errorbars[i][2];
}
// Cap width
halfwidth = typeof errorbars[i][4] === 'number' ? errorbars[i][4] / 2 : default_halfwidth;
if (!properties.errorbarsCapped) {
halfwidth = 0;
}
if (!RGraph.isNullish(errorbars[i][0])) {
this.path(
'b m % % l % % l % % l % % s %',
barX + (barW / 2),
barY + (this.data[i] < 0 ? barH : 0) + ((isGrouped && this.data[group][subgroup] < 0) ? barH : 0),
barX + (barW / 2),
barY - positiveLength + (this.data[i] < 0 ? barH : 0)+ ((isGrouped && this.data[group][subgroup] < 0) ? barH : 0),
barX + (barW / 2) - halfwidth,
Math.round(barY - positiveLength) + (this.data[i] < 0 ? barH : 0) + ((isGrouped && this.data[group][subgroup] < 0) ? barH : 0),
barX + (barW / 2) + halfwidth,
Math.round(barY - positiveLength) + (this.data[i] < 0 ? barH : 0) + ((isGrouped && this.data[group][subgroup] < 0) ? barH : 0),
color
);
}
if (typeof errorbars[i][1] === 'number') {
var negativeLength = Math.abs(this.getYCoord(errorbars[i][1]) - this.getYCoord(0));
this.path(
'b m % % l % % l % % l % % s %',
barX + (barW / 2),
barY + (this.data[i] < 0 ? barH : 0)+ ((isGrouped && this.data[group][subgroup] < 0) ? barH : 0),
barX + (barW / 2),
barY + negativeLength + (this.data[i] < 0 ? barH : 0)+ ((isGrouped && this.data[group][subgroup] < 0) ? barH : 0),
barX + (barW / 2) - halfwidth,
Math.round(coords[i][1] + negativeLength) + (this.data[i] < 0 ? barH : 0)+ ((isGrouped && this.data[group][subgroup] < 0) ? barH : 0),
barX + (barW / 2) + halfwidth,
Math.round(barY + negativeLength) + (this.data[i] < 0 ? barH : 0)+ ((isGrouped && this.data[group][subgroup] < 0) ? barH : 0),
color
);
}
}
// Reset the perbar linewidth to the default if the fourth option
// in the array was specified specified
if (errorbars[i] && typeof errorbars[i][3] === 'number') {
this.context.lineWidth = properties.errorbarsLinewidth;
}
}
};
//
// A per-object to test whether a particular bar is adjustable or not
//
// @param shape The shape object
//
this.isAdjustable = function (shape)
{
if (RGraph.isNullish(properties.adjustableOnly) || !RGraph.isArray(properties.adjustableOnly)) {
return true;
}
if (RGraph.isArray(properties.adjustableOnly) && properties.adjustableOnly[shape.sequentialIndex]) {
return true;
}
return false;
};
//
// A worker function that handles Bar chart specific tooltip substitutions
//
this.tooltipSubstitutions = function (opt)
{
var indexes = RGraph.sequentialIndexToGrouped(opt.index, this.data);
var values = this.data[indexes[0]];
// This makes dumbbell charts work correctly
//
// Match the current index with the correct group/index
// values by looking through the coords2 array using the
// current mouse position.
if (this.properties.variant === 'dumbbell') {
var mouseXY = RGraph.getMouseXY(window.event);
outer:
for (var i=0; i<this.coords2.length; ++i) {
for (var j=0; j<this.coords2[i].length; ++j) {
if (
mouseXY[0] > this.coords2[i][j][0]
&& mouseXY[0] < (this.coords2[i][j][0] + this.coords2[i][j][2])
&& mouseXY[1] > this.coords2[i][j][1]
&& mouseXY[1] < (this.coords2[i][j][1] + this.coords2[i][j][3])
) {
indexes = [i, j];
break outer;
}
}
}
}
if (typeof values === 'number') {
values = [values];
}
return {
index: indexes[1],
dataset: indexes[0],
sequentialIndex: opt.index,
value: this.data_arr[opt.index],
values: values
};
};
//
// A worker function that returns the correct color/label/value
//
// @param object specific The indexes that are applicable
// @param number index The appropriate index
//
this.tooltipsFormattedCustom = function (specific, index)
{
var label;
if (this.stackedOrGrouped) {
label = (!RGraph.isNullish(properties.tooltipsFormattedKeyLabels) && typeof properties.tooltipsFormattedKeyLabels === 'object' && properties.tooltipsFormattedKeyLabels[index])
? properties.tooltipsFormattedKeyLabels[index]
: '';
} else {
label = ( !RGraph.isNullish(properties.tooltipsFormattedKeyLabels)
&& typeof properties.tooltipsFormattedKeyLabels === 'object'
&& properties.tooltipsFormattedKeyLabels[specific.index])
? properties.tooltipsFormattedKeyLabels[specific.index]
: '';
}
return {
label: label
};
};
//
// This allows for static tooltip positioning
//
this.positionTooltipStatic = function (args)
{
var obj = args.object,
e = args.event,
tooltip = args.tooltip,
tooltipWidth = tooltip.offsetWidth,
index = args.index,
canvasXY = RGraph.getCanvasXY(obj.canvas)
coords = RGraph.arrayClone(this.coords[args.index]),
barWidth = coords[2],
barHeight = coords[3];
// SCALING
//
// Account for the (7.00) scaling
//
if (this.properties.scale) {
coords[0] /= 2;
coords[1] /= 2;
barWidth /= 2;
barHeight /= 2;
}
// Position the tooltip in the X direction
args.tooltip.style.left = (
canvasXY[0] // The X coordinate of the canvas
+ coords[0] // The X coordinate of the bar on the chart
- (tooltipWidth / 2) // Subtract half of the tooltip width
+ (barWidth / 2) // Add half of the bar width
+ obj.properties.tooltipsOffsetx // Add any user defined offset
) + 'px';
args.tooltip.style.top = (
canvasXY[1] // The Y coordinate of the canvas
+ coords[1] // The Y coordinate of the bar on the chart
- tooltip.offsetHeight // The height of the tooltip
- 10 // An arbitrary amount
+ obj.properties.tooltipsOffsety // Add any user defined offset
) + 'px';
// If the chart is a 3D version the tooltip Y position needs this
// adjustment
if (properties.variant === '3d') {
var left = coords[0];
var top = coords[1];
var angle = properties.variantThreedAngle;
var adjustment = Math.tan(angle) * left;
args.tooltip.style.top = parseInt(args.tooltip.style.top) + adjustment - 5 + 'px';
}
// If the bar is a negative one, add half the height to the Y coord
if (this.data_arr[index] < 0) {
args.tooltip.style.top =
parseFloat(args.tooltip.style.top)
+ (barHeight / 2)
+ 'px';
}
// If the top of the tooltip is off the top of the page
// then move the tooltip down
//if(parseFloat(args.tooltip.style.top) < 0) {
//args.tooltip.style.top = 5 + 'px';
//}
};
//
// This adds a roundedRect(x, y, width, height, radius) function to the drawing context.
// The radius argument dictates by how much the corners are rounded.
//
// @param number x The X coordinate
// @param number y The Y coordinate
// @param number width The width of the rectangle
// @param number height The height of the rectangle
// @param number radius The radius of the corners. Bigger values mean more rounded corners
//
this.roundedCornersRect = function (x, y, width, height)
{
var radiusLeft = null;
var radiusRight = null;
// LHS radius
if (RGraph.isNumber(properties.cornersRoundLeftRadius)) {
radiusLeft = properties.cornersRoundLeftRadius;
} else {
radiusLeft = Math.min(width / 2, height / 2, properties.cornersRoundRadius);
}
// RHS radius
if (RGraph.isNumber(properties.cornersRoundRightRadius)) {
radiusRight = properties.cornersRoundRightRadius;
} else {
radiusRight = Math.min(width / 2, height / 2, properties.cornersRoundRadius);
}
if ( (radiusLeft + radiusRight) > width) {
// Calculate the left and right radiuses and assign
// to temporary variables
var a = width / (radiusLeft + radiusRight) * radiusLeft;
var b = width / (radiusLeft + radiusRight) * radiusRight;
// Reassign the values to the correct variables
radiusLeft = a;
radiusRight = b;
}
// Save the existing state of the canvas so that it can be restored later
this.context.save();
// Translate to the given X/Y coordinates
this.context.translate(x, y);
// Move to the center of the top horizontal line
this.context.moveTo(width / 2,0);
// Draw the rounded corners. The connecting lines in between them are drawn automatically
this.context.arcTo(width,0,width,height,Math.min(height / 2, properties.cornersRoundRight ? radiusRight : 0));
this.context.arcTo(width, height, 0, height, 0);
this.context.arcTo(0, height, 0, 0, 0);
this.context.arcTo(0, 0, radiusLeft, 0, Math.min(height / 2, this.properties.cornersRoundLeft ? radiusLeft : 0));
// Draw a line back to the start coordinates
this.context.lineTo(width / 2,0);
// Restore the state of the canvas to as it was before the save()
this.context.restore();
};
//
// This adds a roundedRectNegative(x, y, width, height, radius) function to the drawing context.
// The radius argument dictates by how much the corners are rounded.
// This function handles negative bars whereas the above
// function handles positive ones.
//
// @param number x The X coordinate
// @param number y The Y coordinate
// @param number width The width of the rectangle
// @param number height The height of the rectangle
// @param number radius The radius of the corners. Bigger values mean more rounded corners
//
this.roundedCornersRectNegative = function (x, y, width, height)
{
if (height < 0) {
height = Math.abs(height);
y -= height;
}
var radiusLeft = null;
var radiusRight = null;
// LHS radius
if (RGraph.isNumber(properties.cornersRoundLeftRadius)) {
radiusLeft = properties.cornersRoundLeftRadius;
} else {
radiusLeft = Math.min(width / 2, height / 2, properties.cornersRoundRadius);;
}
// RHS radius
if (RGraph.isNumber(properties.cornersRoundRightRadius)) {
radiusRight = properties.cornersRoundRightRadius;
} else {
radiusRight = Math.min(width / 2, height / 2, properties.cornersRoundRadius);;
}
if ( (radiusLeft + radiusRight) > width) {
// Calculate the left and right radiuses and assign
// to temporary variables
var a = width / (radiusLeft + radiusRight) * radiusLeft;
var b = width / (radiusLeft + radiusRight) * radiusRight;
// Reassign the values to the correct variables
radiusLeft = a;
radiusRight = b;
}
// Save the existing state of the canvas so that it can be restored later
this.context.save();
// Translate to the given X/Y coordinates
this.context.translate(x, y);
// Move to the center of the top horizontal line
this.context.moveTo(width / 2,0);
// Draw the rounded corners. The connecting lines in
// between them are drawn automatically
this.context.arcTo(width,0,width,height, 0);
this.context.arcTo(width, height, 0, height, Math.min(height / 2, properties.cornersRoundRight ? radiusRight : 0));
this.context.arcTo(0, height, 0, 0, Math.min(height / 2, properties.cornersRoundLeft ? radiusLeft : 0));
this.context.arcTo(0, 0, width, 0, 0);
// Draw a line back to the start coordinates
this.context.lineTo(width / 2,0);
// Restore the state of the canvas to as it was before the save()
this.context.restore();
};
//
// This function is NOT currently used - it installs an
// appropriate clipping region for the lower half of 3D
// Bar charts when the X axis is in the middle. It's
// used like this:
//
// this.context.save();
// this.context.install3DAxisClip();
// ...
// this.context.restore();
//
this.install3DAxisNegativeClip = function ()
{
this.path(
'b m % % l % % l % % l % % l % % l % % c cl',
this.marginLeft, this.getYCoord(0),
this.marginLeft, this.canvas.height - this.marginBottom,
this.canvas.width - this.marginRight, this.canvas.height - this.marginBottom,
this.canvas.width - this.marginRight + this.properties.variantThreedOffsetx, this.canvas.height - this.marginBottom - this.properties.variantThreedOffsety,
this.canvas.width - this.marginRight + this.properties.variantThreedOffsetx, this.getYCoord(0) - this.properties.variantThreedOffsety,
this.canvas.width - this.marginRight, this.getYCoord(0)
);
};
//
// This returns the relevant value for the formatted key
// macro %{value}. THIS VALUE SHOULD NOT BE FORMATTED.
//
// @param number index The index in the dataset to get
// the value for
//
this.getKeyValue = function (index)
{
if (RGraph.isArray(this.properties.keyFormattedValueSpecific) && RGraph.isNumber(this.properties.keyFormattedValueSpecific[index])) {
return this.properties.keyFormattedValueSpecific[index];
} else {
var total = 0;
for (let i=0; i<this.data.length; ++i) {
if (RGraph.isArray(this.data[i])) {
total += this.data[i][index];
}
}
return total;
}
};
//
// Returns how many data-points there should be when a string
// based key property has been specified. For example, this:
//
// key: '%{property:_labels[%{index}]} %{value_formatted}'
//
// ...depending on how many bits of data ther is might get
// turned into this:
//
// key: [
// '%{property:_labels[%{index}]} %{value_formatted}',
// '%{property:_labels[%{index}]} %{value_formatted}',
// '%{property:_labels[%{index}]} %{value_formatted}',
// '%{property:_labels[%{index}]} %{value_formatted}',
// '%{property:_labels[%{index}]} %{value_formatted}',
// ]
//
// ... ie in that case there would be 4 data-points so the
// template is repeated 4 times.
//
this.getKeyNumDatapoints = function ()
{
var num = 0;
for (let i=0; i<this.data.length; ++i) {
if (RGraph.isArray(this.data[i])) {
num = Math.max(
num,
this.data[i].length
);
}
}
return num;
};
//
// This function handles clipping to scale values. Because
// each chart handles scales differently, a worker function
// is needed instead of it all being done centrally in the
// RGraph.clipTo.start() function.
//
// @param string clip The clip string as supplied by the
// user in the chart configuration
//
this.clipToScaleWorker = function (clip)
{
// The Regular expression is actually done by the
// calling RGraph.clipTo.start() function in the core
// library
if (RegExp.$1 === 'min') from = this.scale2.min; else from = Number(RegExp.$1);
if (RegExp.$2 === 'max') to = this.scale2.max; else to = Number(RegExp.$2);
var width = this.canvas.width,
y1 = this.getYCoord(from),
y2 = this.getYCoord(to),
height = Math.abs(y2 - y1),
x = 0,
y = Math.min(y1, y2);
// Increase the height if the maximum value is "max"
if (RegExp.$2 === 'max') {
y = 0;
height += this.properties.marginTop;
}
// Increase the height if the minimum value is "min"
if (RegExp.$1 === 'min') {
height += this.properties.marginBottom;
}
this.path(
'sa b r % % % % cl',
x, y, width, height
);
};
//
// This function handles TESTING clipping to scale values.
// Because each chart handles scales differently, a worker
// function is needed instead of it all being done centrally
// in the RGraph.clipTo.start() function.
//
// @param string clip The clip string as supplied by the
// user in the chart configuration
//
this.clipToScaleTestWorker = function (clip)
{
// The Regular expression is actually done by the
// calling RGraph.clipTo.start() function in the core
// library
if (RegExp.$1 === 'min') from = this.scale2.min; else from = Number(RegExp.$1);
if (RegExp.$2 === 'max') to = this.scale2.max; else to = Number(RegExp.$2);
var width = this.canvas.width,
y1 = this.getYCoord(from),
y2 = this.getYCoord(to),
height = Math.abs(y2 - y1),
x = 0,
y = Math.min(y1, y2);
// Increase the height if the maximum value is "max"
if (RegExp.$2 === 'max') {
y = 0;
height += this.properties.marginTop;
}
// Increase the height if the minimum value is "min"
if (RegExp.$1 === 'min') {
height += this.properties.marginBottom;
}
this.path(
'b r % % % %',
x, y, width, height
);
};
//
// Scale worker function that increases the size of
// properties as required. Called by the RGraph.scale()
// function.
//
// @param string name The name of the property
// @param mixed value The value of the property
//
this.scalePropertiesWorker = function (name, value)
{
var scaleFactor = RGraph.getScaleFactor(this);
if (name === 'backgroundGridDashArray') {
value[0] *= scaleFactor;
value[1] *= scaleFactor;
} else if (name === 'titleY') {
value = String(parseFloat(value) * scaleFactor);
} else if (name === 'titleX') {
value = String(parseFloat(value) * scaleFactor);
}
return value;
};
//
// Draw a dumbbell chart
//
this.drawdumbbell = function ()
{
var dumbbellCoords = [];
var obj = this;
var seq = 0;
//var y = obj.getYCoord(this.data[0]);
this.coords = [];
this.coords2 = [];
this.data.forEach(function (v, k, arr)
{
var section = ((obj.canvas.width - obj.properties.marginLeft - obj.properties.marginRight) / obj.data.length);
var coordX = (section * k) + obj.properties.marginLeft + (section / 2);
var coordW = obj.properties.variantDumbbellEndRadius;
if (obj.properties.shadow) {
RGraph.setShadow(
obj,
obj.properties.shadowColor,
obj.properties.shadowOffsetx,
obj.properties.shadowOffsety,
obj.properties.shadowBlur
);
}
//
// TODO Need to convert this if condition
//
if (v.length > 2) {
obj.coords2[k] = [];
for (var j=1; j<v.length; ++j) {
var coordY = obj.getYCoord(v[j]);
var coordH = obj.getYCoord(v[j - 1]) - obj.getYCoord(v[j]);
obj.path('b r % % % %',
coordX,
coordY,
coordW,
coordH//(coordH * 2) + (2 * obj.properties.shadowOffsety)
);
obj.pathDumbbell(
coordX,
coordY,
coordW,
coordH,
j === (v.length - 1) && (RGraph.isNull(obj.properties.variantDumbbellEndTop) || obj.properties.variantDumbbellEndTop),
j === 1 && (RGraph.isNull(obj.properties.variantDumbbellEndBottom) || obj.properties.variantDumbbellEndBottom)
);
// Add the rect to the canvas
if (obj.properties.colorsSequential) {
obj.path('f ' + obj.properties.colorsDumbbell[seq]);
} else {
obj.path('f ' + obj.properties.colorsDumbbell[j - 1]);
}
// Determine the height of the
// hotspot
var radius = Math.max(
(obj.properties.variantDumbbellEndTopRadius || obj.properties.variantDumbbellEndTopRadius || 0),
(obj.properties.variantDumbbellEndBottomRadius || obj.properties.variantDumbbellEndBottomRadius || 0)
);
radius = Math.max(coordW, radius);
//
// Calculate the shadowOffset so that the
// highlight covers the shadow as well.
//
var shadowOffset = Math.max(
Math.abs(obj.properties.shadowOffsetx),
Math.abs(obj.properties.shadowOffsety)
);
var coords = [
coordX - radius,
coordY - (j === (v.length - 1) ? radius : 0),
coordW + (2 * radius),
coordH + (j === 1 || (j === v.length - 1) ? radius : 0),
];
// Modify the coords so that the
// shadowOffset is taken into account when
// the highlight is added.
/*
// First segment
if (j === 1) {
// Horizontal offset manglement
if (obj.properties.shadowOffsetx < 0) {
coords[0] -= shadowOffset;
coords[2] += shadowOffset;
} else {
coords[2] += shadowOffset;
}
// Vertical offset manglement
if (obj.properties.shadowOffsety > 0) {
coords[3] += shadowOffset;
}
// Last segment
} else if (j === (v.length - 1) ) {
// Horizontal offset manglement
if (obj.properties.shadowOffsetx > 0) {
coords[2] += shadowOffset;
} else {
coords[0] -= shadowOffset;
coords[2] += shadowOffset;
}
// Vertical offset manglement
if (obj.properties.shadowOffsety > 0) {
//coords[3] += shadowOffset;
} else if (obj.properties.shadowOffsety < 0) {
coords[1] -= shadowOffset;
coords[3] += shadowOffset;
}
// Middle segments
} else {
// Vertical offset manglement
if (obj.properties.shadowOffsetx > 0) {
coords[2] += shadowOffset;
} else {
coords[0] -= shadowOffset;
coords[2] += shadowOffset;
}
}
*/
obj.coords.push(coords);
obj.coords2[k].push(coords);
seq++;
}
// //
// Need this so that the colors are
// used in the coorect order
//
seq--;
// Only TWO Y values - so a minimum and a
// maximum.
} else {
obj.coords2[k] = [];
var coordH = obj.getYCoord(v[0]) - obj.getYCoord(v[1]);
var coordY = obj.getYCoord(v[0]) - coordH;
obj.pathDumbbell(
coordX,
coordY,
coordW,
coordH
);
if (obj.properties.colorsSequential) {
obj.path('f ' + obj.properties.colorsDumbbell[k]);
} else {
obj.path('f ' + obj.properties.colorsDumbbell[0]);
}
// Store the dumbbell coordinates
var coords = [
coordX - obj.properties.variantDumbbellEndRadius - obj.properties.shadowOffsetx,
coordY - obj.properties.shadowOffsety - obj.properties.variantDumbbellEndRadius,
coordW + (2 * obj.properties.variantDumbbellEndRadius) + (2 * obj.properties.shadowOffsetx),
coordH + (2 * obj.properties.variantDumbbellEndRadius) + (2 * obj.properties.shadowOffsety)
];
obj.coords.push(coords);
obj.coords2[k].push(coords);
}
seq++;
RGraph.noShadow(obj);
});
};
//
// Paths a dumbbell bar
//
this.pathDumbbell = function (x, y, w, h, topEnd = true, bottomEnd = true)
{
this.path(
'b r % % % %',
x + (this.properties.variantDumbbellLinewidth / 2) - 2,
y,
this.properties.variantDumbbellLinewidth,
h
);
if (this.properties.variantDumbbellEndTop && topEnd) {
this.path(
'm % % a % % % 0 6.29 false',
x + (w/2), y,
x + (w/2), y, (RGraph.isNumber(this.properties.variantDumbbellEndTopRadius) ? this.properties.variantDumbbellEndTopRadius : this.properties.variantDumbbellEndRadius)
);
}
if (this.properties.variantDumbbellEndBottom && bottomEnd) {
this.path(
'm % % a % % % 0 6.29 false',
x + (w/2), y+h,
x + (w/2), y+h, (RGraph.isNumber(this.properties.variantDumbbellEndBottomRadius) ? this.properties.variantDumbbellEndBottomRadius : this.properties.variantDumbbellEndRadius)
);
}
};
//
// Register the object
//
RGraph.register(this);
//
// This is the 'end' of the constructor so if the first argument
// contains configuration data - handle that.
//
RGraph.parseObjectStyleConfig(this, conf.options);
};
//
// This is the combined bar and Line class which makes creating bar/line combo charts a little bit easier
//
RGraph.CombinedChart = function ()
{
//
// Create a default empty array for the objects
//
this.objects = [];
var objects = [];
if (RGraph.isArray(arguments[0])) {
objects = arguments[0];
} else {
for (var i=0; i<arguments.length; i+=1) {
objects[i] = arguments[i];
}
}
for (var i=0; i<objects.length; ++i) {
this.objects[i] = objects[i];
//
// Set the Line chart margins to match the Bar chart margins
//
this.objects[i].set({
marginLeft: this.objects[0].get('marginLeft'), // Needs to use the dot form to skirt an IE9 bug
marginRight: this.objects[0].get('marginRight'), // Needs to use the dot form to skirt an IE9 bug
marginTop: this.objects[0].get('marginTop'), // Needs to use the dot form to skirt an IE9 bug
marginBottom: this.objects[0].get('marginBottom') // Needs to use the dot form to skirt an IE9 bug
});
if (this.objects[i].type == 'line') {
var obj = this.objects[i];
//
// Set the line chart marginInner
//
obj.set('marginInner', ((this.objects[0].canvas.width - this.objects[0].get('marginRight') - this.objects[0].get('marginLeft')) / this.objects[0].data.length) / 2 );
//
// No labels, axes or grid on the Line chart
//
obj.set('yaxis', false);
obj.set('xaxis', false);
obj.set('backgroundGrid', false);
obj.set('yaxisScale', false);
}
//
// Resizing
//
if (this.objects[i].get('resizable')) {
var resizable_object = obj;
}
}
//
// Resizing
//
if (resizable_object) {
//
// This recalculates the Line chart marginInner when the chart is resized
//
function myOnresizebeforedraw (obj)
{
var marginLeft = obj.get('marginLeft');
var marginRight = obj.get('marginRight');
obj.set('marginInner', (obj.canvas.width - marginLeft - marginRight) / (obj.original_data[0].length * 2));
}
RGraph.addCustomEventListener(
resizable_object,
'onresizebeforedraw',
myOnresizebeforedraw
);
}
return this;
};
//
// The Add method can be used to add methods to the CombinedChart object.
//
RGraph.CombinedChart.prototype.add = function (obj)
{
this.objects.push(obj);
return this;
};
//
// The Draw method goes through all of the objects drawing them (sequentially)
//
RGraph.CombinedChart.prototype.draw = function ()
{
if (RGraph.isArray(this.objects)) {
for (var i=0; i<this.objects.length; ++i) {
if (this.objects[i].properties['combinedEffect']) {
// The options must be given as a string because of the
// RGraph configuration system
var options = this.objects[i].properties['combinedEffectOptions'] ? eval('(' + this.objects[i].properties['combinedEffectOptions'] + ')') : null,
callback = this.objects[i].properties['combinedEffectCallback'],
func = this.objects[i].properties['combinedEffect'];
(this.objects[i][func])(options, callback);
} else {
this.objects[i].draw();
}
}
}
return this;
};
//
// Provides an easy way to get a segmented Bar chart.
// See the Bar chart documentation page for more details.
//
RGraph.SegmentedBar = function (conf)
{
this.config = conf;
this.properties = {};
// Process the data
this.config.data.forEach(function (v, k, arr)
{
arr[k] = RGraph.arrayPad([], v, 1);
});
// Set the options on the segmentedBar object
for (i in this.config.options) {
if (typeof i === 'string') {
this.properties[i] = this.config.options[i];
}
}
// Prevent any 'this' madness
var seg = this;
this.background = new RGraph.Bar({
id: conf.id[0],
data: RGraph.arrayPad([], conf.data.length),
options: {
backgroundGridHlinesCount: seg.properties.segmentsCount,
colors: ['rgba(0,0,0,0)'],
xaxis: false,
yaxis: false,
yaxisLabels: false,
xaxis: false,
yaxis: false,
yaxisScale: false
}
});
this.foreground = new RGraph.Bar({
id: conf.id[1],
data: conf.data,
options: {
grouping: 'stacked',
xaxis: false,
yaxis: false,
backgroundGrid: false,
yaxisLabelsOffsetx: -3,
yaxisLabelsOffsety: 20
}
}).on('draw', function (obj)
{
var lw = seg.properties.segmentsLinewidth;
var count = seg.properties.segmentsCount;
for (var i=0; i<=count; ++i) {
obj.path(
'cr 0 % 1000 %',
obj.getYCoord(i / count * seg.foreground.scale2.max) - (lw / 2), lw);
}
});
// Set the Y label Offset
this.foreground.set({
yaxisLabelsOffsety:
(this.foreground.canvas.height - this.foreground.properties.marginTop - this.foreground.properties.marginBottom) / seg.properties.segmentsCount / 2
});
// Draw the Bar chart
this.draw = function ()
{
this.background.draw();
this.foreground.draw();
return this;
}
// Animate the Bar chart (only the foreground chart though)
this.grow = function ()
{
this.background.draw();
this.foreground.grow(arguments[0], arguments[1]);
return this;
}
// Animate the Bar chart (only the foreground chart though)
this.wave = function ()
{
this.background.draw();
this.foreground.wave(arguments[0], arguments[1]);
return this;
}
// This allows you to add responsive configuration to the chart by
// passing the configuration throough to the underlying objects.
this.responsive = function (conf)
{
this.foreground.responsive(conf);
this.background.responsive(conf);
return this;
}
}