5043 lines
182 KiB
JavaScript
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;
|
|
}
|
|
} |