// o---------------------------------------------------------------------------------o
    // | This file is part of the RGraph package - you can learn more at:                |
    // |                                                                                 |
    // |                       https://www.rgraph.net/license.html                       |
    // |                                                                                 |
    // | RGraph is dual-licensed under the Open Source GPL license. That means that it's |
    // | free to use and there are no restrictions on what you can use RGraph for!       |
    // | If the GPL license does not suit you however, then there's an inexpensive       |
    // | commercial license option available. See the URL above for more details.        |
    // o---------------------------------------------------------------------------------o

    window.RGraph = window.RGraph || {isrgraph:true,isRGraph: true,rgraph:true};

// Module pattern
(function (win, doc, undefined)
{
    // A short name variable
    var ua  = navigator.userAgent;

    //
    // Initialise the various objects
    //
    RGraph.Highlight      = {};
    RGraph.Registry       = {};
    RGraph.Registry.store = [];
    RGraph.Registry.store['event.handlers']       = [];
    RGraph.Registry.store['__rgraph_event_listeners__'] = []; // Used in the new system for tooltips
    RGraph.Registry.store['rgraph-runonce-functions']   = []; // Used in the runonce system
    RGraph.Background     = {};
    RGraph.background     = {};
    RGraph.objects        = [];
    RGraph.Resizing       = {};
    RGraph.events         = [];
    RGraph.cursor         = [];
    RGraph.Effects        = RGraph.Effects || {};
    RGraph.cache          = [];
    RGraph.GET            = {};
    RGraph.GET.__parts__  = null;

    RGraph.ObjectRegistry                    = {};
    RGraph.ObjectRegistry.objects            = {};
    RGraph.ObjectRegistry.objects.byUID      = [];
    RGraph.ObjectRegistry.objects.byCanvasID = [];
    RGraph.OR                                = RGraph.ObjectRegistry;




    //
    // Some "constants". The ua variable is navigator.userAgent (definedabove)
    //
    RGraph.PI       = Math.PI;
    RGraph.HALFPI   = RGraph.PI / 2;
    RGraph.TWOPI    = RGraph.PI * 2;

    RGraph.ISFF     = ua.indexOf('Firefox') != -1;
    RGraph.ISOPERA  = ua.indexOf('Opera') != -1;
    RGraph.ISCHROME = ua.indexOf('Chrome') != -1;
    RGraph.ISSAFARI = ua.indexOf('Safari') != -1 && !RGraph.ISCHROME;
    RGraph.ISWEBKIT = ua.indexOf('WebKit') != -1;

    RGraph.ISIE   = ua.indexOf('Trident') > 0 || navigator.userAgent.indexOf('MSIE') > 0;
    RGraph.ISIE6  = ua.indexOf('MSIE 6') > 0;
    RGraph.ISIE7  = ua.indexOf('MSIE 7') > 0;
    RGraph.ISIE8  = ua.indexOf('MSIE 8') > 0;
    RGraph.ISIE9  = ua.indexOf('MSIE 9') > 0;
    RGraph.ISIE10 = ua.indexOf('MSIE 10') > 0;
    RGraph.ISOLD  = RGraph.ISIE6 || RGraph.ISIE7 || RGraph.ISIE8; // MUST be here
    
    RGraph.ISIE11UP = ua.indexOf('MSIE') == -1 && ua.indexOf('Trident') > 0;
    RGraph.ISIE10UP = RGraph.ISIE10 || RGraph.ISIE11UP;
    RGraph.ISIE9UP  = RGraph.ISIE9 || RGraph.ISIE10UP;

    // Some commonly used bits of info
    RGraph.MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    RGraph.MONTHS_LONG  = ['January','February','March','April','May','June','July','August','September','October','November','December'];
    RGraph.DAYS_SHORT   = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
    RGraph.DAYS_LONG    = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'];
    RGraph.HOURS24      = ['00:00','01:00','02:00','03:00','04:00','05:00','06:00','07:00','08:00','09:00','10:00','11:00','12:00','13:00','14:00','15:00','16:00','17:00','18:00','19:00','20:00','21:00','22:00','23:00'];
    RGraph.HOURS12      = ['00:00','01:00','02:00','03:00','04:00','05:00','06:00','07:00','08:00','09:00','10:00','11:00'];



    //
    // Returns five values which are used as a nice scale
    // 
    // 11/12/2018
    // ==========
    // This funtction doesn't appear to be being used
    // any more - could remove it.
    // 
    // @param  max int    The maximum value of the graph
    // @param  obj object The graph object
    // @return     array   An appropriate scale
    //
//    RGraph.getScale = function (max, obj)
//    {
//        var prefix = obj.type === 'hbar' ? 'xaxis' : 'yaxis';
//
//        //
//        // Special case for 0
//        //
//        if (max == 0) {
//            return ['0.2', '0.4', '0.6', '0.8', '1.0'];
//        }
//
//        var original_max = max;
//
//        //
//        // Manually do decimals
//        //
//        if (max <= 1) {
//            if (max > 0.5) {
//                return [0.2,0.4,0.6,0.8, Number(1).toFixed(1)];
//
//            } else if (max >= 0.1) {
//                return obj.get(prefix + 'ScaleRound') ? [0.2,0.4,0.6,0.8,1] : [0.1,0.2,0.3,0.4,0.5];
//
//            } else {
//
//                var tmp = max;
//                var exp = 0;
//
//                while (tmp < 1.01) {
//                    exp += 1;
//                    tmp *= 10;
//                }
//
//                var ret = ['2e-' + exp, '4e-' + exp, '6e-' + exp, '8e-' + exp, '10e-' + exp];
//
//
//                if (max <= ('5e-' + exp)) {
//                    ret = ['1e-' + exp, '2e-' + exp, '3e-' + exp, '4e-' + exp, '5e-' + exp];
//                }
//
//                return ret;
//            }
//        }
//
//        // Take off any decimals
//        if (String(max).indexOf('.') > 0) {
//            max = String(max).replace(/\.\d+$/, '');
//        }
//
//        var interval = Math.pow(10, Number(String(Number(max)).length - 1));
//        var topValue = interval;
//
//        while (topValue < max) {
//            topValue += (interval / 2);
//        }
//
//        // Handles cases where the max is (for example) 50.5
//        if (Number(original_max) > Number(topValue)) {
//            topValue += (interval / 2);
//        }
//
//        // Custom if the max is greater than 5 and less than 10
//        if (max < 10) {
//            topValue = (Number(original_max) <= 5 ? 5 : 10);
//        }
//        
//        //
//        // Added 02/11/2010 to create "nicer" scales
//        //
//        if (obj && typeof obj.get(prefix + 'ScaleRound') == 'boolean' && obj.get(prefix + 'ScaleRound')) {
//            topValue = 10 * interval;
//        }
//
//        return [topValue * 0.2, topValue * 0.4, topValue * 0.6, topValue * 0.8, topValue];
//    };








    //
    // This function allows both object based arguments to functions
    // and also regular arguments as well.
    //
    // You can call it from inside a function like this:
    //
    // args = RGraph.getArgs(arguments, 'object,id,foo,bar');
    //
    // So you're passing it the arguments object and a comma seperated list of names
    // for the arguments.
    //
    // @param array args   The arguments object that you get when inside a function
    // @param string names A comma seperated list of desired names for the arguments
    //                     eg: 'object,color,size'
    //
    RGraph.getArgs = function (args, names)
    {
        var ret   = {};
        var count = 0;
        names     = names.trim().split(/ *, */);

        if (   args
            && args[0]
            && args.length === 1
            && typeof args[0][names[0]] !== 'undefined') {
            
            for (var i=0; i<names.length; ++i) {
                if (typeof args[0][names[i]] === 'undefined') {
                    args[0][names[i]] = null;
                }
            }

            return args[0];
        } else {
            for (var i in names) {
                if (RGraph.isString(i)) {
                    ret[names[i]] = typeof args[count] === 'undefined' ? null : args[count];
                    
                    count += 1;
                }
            }
        }

        return ret;
    };








    //
    // Returns an appropriate scale. The return value is actualy an object consisting of:
    //  scale.max
    //  scale.min
    //  scale.scale
    //
    // @param  args object An object consisting of:
    //                     object  - The chart object
    //                     options - Options for the function
    //
    // @return     object  An object containing scale information
    //
    RGraph.getScale  = function (args)
    {
        var properties   = args.object.properties,
            numlabels    = typeof args.options['scale.labels.count'] == 'number' ? args.options['scale.labels.count'] : 5,
            units_pre    = typeof args.options['scale.units.pre'] == 'string' ? args.options['scale.units.pre'] : '',
            units_post   = typeof args.options['scale.units.post'] == 'string' ? args.options['scale.units.post'] : '',
            max          = Number(args.options['scale.max']),
            min          = typeof args.options['scale.min'] == 'number' ? args.options['scale.min'] : 0,
            strict       = args.options['scale.strict'],
            decimals     = Number(args.options['scale.decimals']), // Sometimes the default is null
            point        = args.options['scale.point'], // Default is a string in all chart libraries so no need to cast it
            thousand     = args.options['scale.thousand'], // Default is a string in all chart libraries so no need to cast it
            original_max = max,
            round        = args.options['scale.round'],
            scale        = {max:1,labels:[],values:[]},
            formatter    = args.options['scale.formatter'];

            // Determine any prefix to use
            prefix = args.object.type === 'hbar' ? 'xaxis' : 'yaxis';
            prefix = args.object.type === 'odo' ? '' : prefix;





        //
        // Special case for 0
        // 
        // ** Must be first **
        //
        if (!max) {

            var max   = 1;

            for (var i=0; i<numlabels; ++i) {

                var label = ((((max - min) / numlabels) + min) * (i + 1)).toFixed(decimals);

                scale.labels.push(units_pre + label + units_post);
                scale.values.push(parseFloat(label))
            }

        //
        // Manually do decimals
        //
        } else if (max <= 1 && !strict) {

            var arr = [
                1,0.5,
                0.10,0.05,
                0.010,0.005,
                0.0010,0.0005,
                0.00010,0.00005,
                0.000010,0.000005,
                0.0000010,0.0000005,
                0.00000010,0.00000005,
                0.000000010,0.000000005,
                0.0000000010,0.0000000005,
                0.00000000010,0.00000000005,
                0.000000000010,0.000000000005,
                0.0000000000010,0.0000000000005
            ], vals = [];



            for (var i=0; i<arr.length; ++i) {
                if (max > arr[i]) {
                    i--;
                    break;
                }
            }


            scale.max    = arr[i];
            scale.labels = [];
            scale.values = [];
        
            for (var j=0; j<numlabels; ++j) {

                var value = ((((arr[i] - min) / numlabels) * (j + 1)) + min).toFixed(decimals);

                scale.values.push(value);
                scale.labels.push(RGraph.numberFormat({
                    object:    args.object,
                    number:    value,
                    unitspre:  units_pre,
                    unitspost: units_post,
                    thousand:  thousand,
                    point:     point,
                    formatter: formatter,
                    decimals:  decimals
                }));
            }




        } else if (!strict) {

            //
            // Now comes the scale handling for integer values
            //

            // This accommodates decimals by rounding the max up to the next integer
            max = Math.ceil(max);

            var interval = Math.pow(10, Math.max(1, Number(String(Number(max) - Number(min)).length - 1)) );

            var topValue = interval;

            while (topValue < max) {
                topValue += (interval / 2);
            }

            // Handles cases where the max is (for example) 50.5
            if (Number(original_max) > Number(topValue)) {
                topValue += (interval / 2);
            }

            // Custom if the max is greater than 5 and less than 10
            if (max <= 10) {
                topValue = (Number(original_max) <= 5 ? 5 : 10);
            }
    
    
            // Added 02/11/2010 to create "nicer" scales
            if (args.object && typeof round == 'boolean' && round) {
                topValue = 10 * interval;
            }

            scale.max = topValue;

            // Now generate the scale. Temporarily set the objects scaleDecimal and scalePoint to those
            // that we've been given as the number_format functuion looks at those instead of using
            // arguments.
            var tmp_point    = properties[prefix + 'ScalePoint'];
            var tmp_thousand = properties[prefix + 'ScaleThousand'];

            args.object.set(prefix + 'scaleThousand', thousand);
            args.object.set(prefix + 'scalePoint', point);


            for (var i=0; i<numlabels; ++i) {
                scale.labels.push(RGraph.numberFormat({
                    object:    args.object,
                    number:    ((((i+1) / numlabels) * (topValue - min)) + min).toFixed(decimals),
                    unitspre:  units_pre,
                    unitspost: units_post,
                    point:     point,
                    thousand:  thousand,
                    formatter: formatter
                }) );
                scale.values.push(((((i+1) / numlabels) * (topValue - min)) + min).toFixed(decimals));
            }

            args.object.set(prefix + 'scaleThousand', tmp_thousand);
            args.object.set(prefix + 'scalePoint', tmp_point);

        } else if (typeof max == 'number' && strict) {

            //
            // ymax is set and also strict
            //
            for (var i=0; i<numlabels; ++i) {
                scale.labels.push(RGraph.numberFormat({
                    object:    args.object,
                    number:    ((((i+1) / numlabels) * (max - min)) + min).toFixed(decimals),
                    unitspre:  units_pre,
                    unitspost: units_post,
                    thousand:  thousand,
                    point:     point,
                    formatter: formatter
                }));

                scale.values.push(
                    ((((i+1) / numlabels) * (max - min)) + min).toFixed(decimals)
                );
            }

            // ???
            scale.max = max;
        }


        scale.units_pre  = units_pre;
        scale.units_post = units_post;
        scale.point      = point;
        scale.decimals   = decimals;
        scale.thousand   = thousand;
        scale.numlabels  = numlabels;
        scale.round      = Boolean(round);
        scale.min        = min;
        scale.formatter  = formatter;

        //
        // Convert all of the scale values to numbers
        //
        for (var i=0; i<scale.values.length; ++i) {
            scale.values[i] = parseFloat(scale.values[i]);
        }

        return scale;
    };








    //
    // Parse a gradient thats in JSON format like this:
    //
    // Gradient({colors: ["red","white"],x1:0,y1:25,x2:0,y2:275})
    //
    // @param  args object An object consisting of:
    //                      o object
    //                      o Th gradient definition
    //
    RGraph.parseJSONGradient = function (args)
    {
        var obj      = args.object,
            def      = args.def, // The gradient definition
            context  = args.object.context;

        // Evaluate the JSON
        def = eval("(" + def + ")");





        // Create a radial gradient
        if (typeof def.r1 === 'number' && typeof def.r2 === 'number') {
            // Create the gradient
            var grad = context.createRadialGradient(
                def.x1, def.y1, def.r1,
                def.x2, def.y2, def.r2
            );
        // Create a linear gradient
        } else {
            var grad = context.createLinearGradient(
                def.x1, def.y1,
                def.x2, def.y2
            );
        }




        // Add the parts to the gradient
        var diff = 1 / (def.colors.length - 1);
        
        grad.addColorStop(0, RGraph.trim(def.colors[0]));
        
        for (var j=1,len=def.colors.length; j<len; ++j) {
            grad.addColorStop(j * diff, RGraph.trim(def.colors[j]));
        }

        return grad;
    };







    //
    // A convenient way to get the last element in the array:
    //
    // foo = [8,6,6,7,4,2,3,8];
    // RGraph.arrayLast(foo); // 8
    //
    // @param array array The array to get the last element from
    //
    RGraph.arrayLast = function (array)
    {
        return array[array.length - 1];
    };








    //
    // Converts an the truthy values to falsey values and vice-versa
    //
    // @param  args object An object consisting of:
    //                      o array
    //
    RGraph.arrayInvert = function ()
    {
        var args = RGraph.getArgs(arguments, 'array');

        for (var i=0,len=args.array.length; i<len; ++i) {
            args.array[i] = !args.array[i];
        }

        return args.array;
    };








    //
    // An arrayTrim function that removes the empty elements off
    // both ends
    //
    // @param  args object An object consisting of:
    //                      o array
    // OR
    //
    //@param        array The array to trim
    //
    RGraph.arrayTrim = function ()
    {
        var args = RGraph.getArgs(arguments, 'array');
        var out = [], content = false;

        // Trim the start
        for (var i=0; i<args.array.length; i++) {
        
            if (args.array[i]) {
                content = true;
            }
        
            if (content) {
                out.push(args.array[i]);
            }
        }
        
        // Reverse the array and trim the start again
        out = RGraph.arrayReverse(out);

        var out2 = [], content = false ;
        for (var i=0; i<out.length; i++) {
        
            if (out[i]) {
                content = true;
            }
        
            if (content) {
                out2.push(out[i]);
            }
        }
        
        // Now reverse the array and return it
        out2 = RGraph.arrayReverse(out2);

        return out2;
    };








    //
    //
    // *** OLD VERSION OF THE CLONE FUNCTION ***
    //
    // Makes a clone of an ARRAY
    //
    // @param args object An object containg the array to clone
    // 
    // OR
    //
    // @param args mixed The object to clone
    //
    //RGraph.arrayClone = function ()
    //{
    //    var args = RGraph.getArgs(arguments, 'array,options');
    //    var c;
    //    var structuredClone = args.options && args.options.structuredClone;
    //
    //    if (window.structuredClone && structuredClone) {
    //        c = window.structuredClone(args.array);
    //    } else {
    //        if(args.array === null || typeof args.array !== 'object') {
    //            return args.array;
    //        }
    //
    //        c = JSON.parse(JSON.stringify(args.array));
    //     }
    //
    //    return c;
    //};









    //
    // An updated clone function that works better
    //
    // @param array mixed The variable to clone and
    //                    return a copy of. Doesn't
    //                    clone objects.
    // @param objects boolean Whether to clone objects or not,
    //                        default is no to
    //
    RGraph.arrayClone = function (array, objects = false, maxdepth = 5)
    {
        // This is here to limit recurusion
        if (maxdepth < 0) {
            return;
        }

        var ret = null;

        // Account for undefined values
        if (typeof array === 'undefined') {
            return undefined;
        }

        // Account for null values
        if (typeof array === 'object' && !array) {
            return null;
        }


        switch(typeof array)
        {
            case 'string':
                ret = (function (str){return String(str)})(array);
                break;
            
            case 'number':
                ret = (function (num){return Number(num)})(array);
                break;

            case 'boolean':
                ret = array;
                break;

            case 'object':
                if (array.constructor.toString().indexOf('Array') >= 0) {
                    ret = new Array();
                    for (var i=0; i<array.length; ++i) {

                        ret[i] = RGraph.arrayClone(array[i], objects, maxdepth - 1);
                    }
                } else if (array.constructor.toString().indexOf('Object') > 0 && objects) {
                    ret = new Object();
                    for (var i in array) {
                        ret[i] = RGraph.arrayClone(array[i], true, maxdepth - 1);
                    }
                }
                break;
            
            case 'function':
                ret = array;
                break;
        }

        return ret;
    };








    //
    // Returns the maximum numeric value which is in an array. This function IS NOT
    // recursive
    // 
    // @param object args An object consisting of an array property which is the array to get
    //                    the max value of.
    //
    // OR
    //
    // @param  array arr The array (can also be a number, in which case it's returned as-is)
    // @param  int       Whether to ignore signs (ie negative/positive)
    // @return int       The maximum value in the array
    //
    RGraph.arrayMax = function ()
    {
        var args = RGraph.getArgs(arguments, 'array,ignore');
        var max = null;
        
        if (typeof args.array === 'number') {
            return args.array;
        }
        
        if (RGraph.isNullish(args.array)) {
            return 0;
        }

        for (var i=0,len=args.array.length; i<len; ++i) {
            if (typeof args.array[i] === 'number' && !isNaN(args.array[i])) {

                var val = args.ignore ? Math.abs(args.array[i]) : args.array[i];
                
                if (typeof max === 'number') {
                    max = Math.max(max, val);
                } else {
                    max = val;
                }
            }
        }

        return max;
    };








    //
    // Returns the minimum numeric value which is in an array
    // 
    // @param  object args An object consisting of the array to find the min of
    //
    // OR
    //
    // @param  array arr The array (can also be a number, in which case it's returned as-is)
    // @param  int       Whether to ignore signs (ie negative/positive)
    // @return int       The minimum value in the array
    //
    RGraph.arrayMin = function (args)
    {
        var args = RGraph.getArgs(arguments, 'array,ignore');

        var max = null,
            min = null,
            ma  = Math;
        
        if (typeof args.array === 'number') {
            return args.array;
        }
        
        if (RGraph.isNullish(args.array)) {
            return 0;
        }

        for (var i=0,len=args.array.length; i<len; ++i) {
            if (typeof args.array[i] === 'number') {

                var val = args.ignore ? Math.abs(args.array[i]) : args.array[i];
                
                if (typeof min === 'number') {
                    min = Math.min(min, val);
                } else {
                    min = val;
                }
            }
        }

        return min;
    };








    //
    // Returns the maximum value which is in an array
    // 
    // @param object args An object which consists of the arguments
    //                    to the function. Keys should be: array, length
    //
    // OR
    //
    // @param  array arr The array
    // @param  int   len The length to pad the array to
    // @param  mixed     The value to use to pad the array (optional)
    //
    RGraph.arrayFill =
    RGraph.arrayPad  = function ()
    {
        var args = RGraph.getArgs(arguments, 'array,length,value');

        if (args.array.length < args.length) {            
            for (var i=args.array.length; i<args.length; i+=1) {
                args.array[i] = args.value;
            }
        }
        
        return args.array;
    };








    //
    // An array sum function
    // 
    // @param object args An object consisting of the argumments to the
    //                    function
    //
    // OR
    //
    // @param  array arr The  array to calculate the total of
    // @return int       The summed total of the arrays elements
    //
    RGraph.arraySum = function ()
    {
        var args = RGraph.getArgs(arguments, 'array');

        // Allow integers
        if (typeof args.array === 'number') {
            return args.array;
        }
        
        // Account for null
        if (RGraph.isNullish(args.array)) {
            return 0;
        }

        var i, sum, len = args.array.length;

        for(i=0,sum=0;i<len;sum+=(args.array[i++]||0));

        return sum;
    };








    //
    // Use this method to remove null vlues from your objects
    // or arrays
    //
    // @param mixed  src An array or an object
    // @param object opt An options object. There's only two
    //                   options currently: By default only arrays
    //                   are handled - ie objects are left untouched.
    //                   Set the options parameter to true if you
    //                   want objects and their properties to be
    //                   checked. For example:
    //
    //                   newobj = RGraph.arrayRemoveNull(
    //                       myArray,
    //                       {objects: true}
    //                   );
    //
    //                   And you can also set a 'value' option to
    //                   the value to replace null values with
    //                   instead of removing them.
    //
    //
    RGraph.arrayRemoveNull = function ()
    {
        var args = RGraph.getArgs(arguments, 'source,options');

        if (!args.options) {
            args.options = {};
        }

        //
        // Remove null values from arrays
        //
        if (RGraph.isArray(args.source)) {
            
            var arr = [];

            for (let i in args.source) {

                if (RGraph.isNullish(args.source[i])) {
                    if (!RGraph.isUndefined(args.options.value)) {
                        arr.push(args.options.value);
                    }
                    continue;
                
                } else if (RGraph.isArray(args.source[i])) {
                    arr.push(RGraph.arrayRemoveNull(args.source[i], args.options));

                } else if (RGraph.isObject(args.source[i]) && args.options.objects) {
                    arr.push(RGraph.arrayRemoveNull(args.source[i], args.options));
                
                } else {                
                    arr.push(args.source[i]);
                }
            }
            
        //
        // Remove null values from objects
        //
        } else if (RGraph.isObject(args.source) && args.options.objects) {

            var arr = {};

            for (let i in args.source) {
                
                if (RGraph.isArray(args.source[i]) || RGraph.isObject(args.source[i])) {
                    arr[i] = RGraph.arrayRemoveNull(args.source[i], args.options);

                } else if (!RGraph.isNullish(args.source[i])) {
                    Object.defineProperty(arr, i, {
                        value: args.source[i]
                    });
                
                } else if (RGraph.isNullish(args.source[i])) {
                    if (!RGraph.isUndefined(args.options.value)) {
                        arr[i] = args.options.value;
                    }
                }
            }
        
        // 
        // Don't remove nulls from objects
        //
        } else if (RGraph.isObject(args.source) && !args.options.objects) {

            var arr = args.source;
        
        // Other types - just return them
        } else {
            var arr = args.source;
        }

        return arr;
    };








    //
    // Takes any number of arguments and adds them to one big linear array
    // which is then returned
    // 
    // @param ... mixed The data to linearise. You can strings, booleans, numbers or arrays
    //
    RGraph.arrayLinearize = function ()
    {
        var arr  = [],
            args = arguments

        for (var i=0,len=args.length; i<len; ++i) {

            if (typeof args[i] === 'object' && args[i]) {
                for (var j=0,len2=args[i].length; j<len2; ++j) {
                    var sub = RGraph.arrayLinearize(args[i][j]);
                    
                    for (var k=0,len3=sub.length; k<len3; ++k) {
                        arr.push(sub[k]);
                    }
                }
            } else {
                arr.push(args[i]);
            }
        }

        return arr;
    };








    //
    // Takes one off the front of the given array and returns the new array.
    //
    // @param object args An object consisting of the array to linearise. 
    //
    // OR
    //
    // @param  array arr The array from which to take one off the front of array
    // @return array     The new array
    //
    RGraph.arrayShift = function()
    {
        var args = RGraph.getArgs(arguments, 'array');
        var ret  = [];
        
        for(var i=1,len=args.array.length; i<len; ++i) {
            ret.push(args.array[i]);
        }
        
        return ret;
    };








    //
    // Reverses the order of an array
    //
    // @param  args object An object consisting of:
    //                      o array
    //
    // OR
    //
    // @param array  arr The array to reverse
    //
    RGraph.arrayReverse = function ()
    {
        var args = RGraph.getArgs(arguments, 'array');

        if (!args.array) {
            return;
        }

        var newarr=[];

        for(var i=args.array.length - 1; i>=0; i-=1) {
            newarr.push(args.array[i]);
        }
        
        return newarr;
    };








    //
    // Returns the absolute value of a number. You can also pass in an
    // array and it will run the abs() function on each element. It
    // operates recursively so sub-arrays are also traversed.
    // 
    // @param  args object An object consisting of:
    //                      o value
    // OR
    //
    // @param array arr The number or array to work on
    //
    RGraph.abs = function ()
    {
        var args = RGraph.getArgs(arguments, 'value');

        if (typeof args.value === 'string') {
            args.value = parseFloat(args.value) || 0;
        }

        if (typeof args.value === 'number') {
            return Math.abs(args.value);
        }

        if (typeof args.value === 'object') {
            for (i in args.value) {
                if (   typeof args.value[i] === 'string'
                    || typeof args.value[i] === 'number'
                    || typeof args.value[i] === 'object') {

                    args.value[i] = RGraph.abs(args.value[i]);
                }
            }
            
            return args.value;
        }
        
        return 0;
    };








    //
    // Clears the canvas by setting the width. You can specify a colour if you wish.
    //
    // @param  args object An object consisting of:
    //                      o canvas
    //                      o color
    // OR
    //
    //
    // @param object canvas The canvas to clear
    // @param mixed         Usually a color string to use to clear the canvas
    //                      with - could also be a gradient object
    //
    RGraph.clear =
    RGraph.Clear = function (args)
    {
        var args    = RGraph.getArgs(arguments, 'canvas,color');
        var obj     = args.canvas.__object__;
        var context = args.canvas.getContext('2d');
        var color   = args.color || (obj && obj.get('clearto'));

        if (!args.canvas) {
            return;
        }
        
        RGraph.fireCustomEvent(obj, 'onbeforeclear');

        //
        // Set the CSS display: to none for DOM text
        //
        if (RGraph.text.domNodeCache && RGraph.text.domNodeCache[args.canvas.id]) {
            for (var i in RGraph.text.domNodeCache[args.canvas.id]) {
                
                var el = RGraph.text.domNodeCache[args.canvas.id][i];
    
                if (el && el.style) {
                    el.style.display = 'none';
                }
            }
        }

        //
        // Can now clear the canvas back to fully transparent
        //
        if (   !color
            || (color && color === 'rgba(0,0,0,0)' || color === 'transparent')
            ) {

            context.clearRect(
                -100,
                -100,
                args.canvas.width + 200,
                args.canvas.height + 200
            );

            // Reset the globalCompositeOperation
            context.globalCompositeOperation = 'source-over';

        } else if (color) {

            obj.path(
                'fs % fr -100 -100 % %',
                color,
                args.canvas.width + 200,
                args.canvas.height + 200
            );
        
        } else {
            obj.path(
                'fs % fr -100 -100 % %',
                obj.get('clearto'),
                args.canvas.width + 200,
                args.canvas.height + 200
            );
        }
        
        //if (RGraph.clearAnnotations) {
            //RGraph.clearAnnotations(canvas.id);
        //}
        
        //
        // This removes any background image that may be present
        //
        if (RGraph.Registry.get('background.image.' + args.canvas.id)) {
            var img            = RGraph.Registry.get('background.image.' + args.canvas.id);
            img.style.position = 'absolute';
            img.style.left     = '-10000px';
            img.style.top      = '-10000px';
        }
        
        //
        // This hides the tooltip that is showing IF it has the same canvas ID as
        // that which is being cleared
        //
        if (RGraph.Registry.get('tooltip') && obj && !obj.get('tooltipsNohideonclear')) {
            RGraph.hideTooltip(args.canvas);
        }

        //
        // Set the cursor to default
        //
        args.canvas.style.cursor = 'default';

        RGraph.fireCustomEvent(obj, 'onclear');
    };








    //
    // Draws the title of the graph
    // 
    // @param object  args   An object consisting of the arguments to the function
    //                        o object
    //
    // OR
    //
    // @param object  canvas The canvas object
    //
    RGraph.drawTitle = function ()
    {
        var args     = RGraph.getArgs(arguments, 'object'),
            obj      = args.object,
            halign   = 'center',
            valign   = 'center',
            x        = ((obj.canvas.width - obj.properties.marginLeft - obj.properties.marginRight) / 2) + obj.properties.marginLeft,
            y        = null,
            textConf = RGraph.getTextConf({
                object: args.object,
                prefix: 'title'
            });


        obj.context.beginPath();
        obj.context.fillStyle = textConf.color;
        
        //
        // If not set then set the size of the text 4 pt higher than
        // the textSize setting. Also set the title to be bold if the
        //bold property isn't set
        //
        if (!RGraph.isNumber(obj.properties.titleSize)) {
            textConf.size += 4;
        }
        if (!RGraph.isBoolean(obj.properties.titleBold)) {
            textConf.bold = true;
        }






        // Determine the Y coordinate
        y = obj.properties.marginTop - textConf.size - 5;

        if (obj.properties.xaxisPosition === 'top') {
            y = obj.canvas.height  - obj.properties.marginBottom + textConf.size + 5;
        }



        //
        // Vertically center the text if the key is not present
        // or if it's not positioned in the margin
        //
        if (obj.properties.key && obj.properties.key.length && obj.properties.keyPosition && obj.properties.keyPosition !== 'margin') {
            var valign = 'center';

        } else if (obj.properties.key && obj.properties.key.length && obj.properties.keyPosition && obj.properties.keyPosition === 'margin') {
            var valign = 'bottom';
            
            // Measure the size of the key text
            var keyTextDim = RGraph.measureText({
                bold:   obj.properties.keyLabelsBold,
                italic: obj.properties.keyLabelsItalic,
                size:   obj.properties.keyLabelsSize,
                font:   obj.properties.keyLabelsFont,
                text:   'Mg'
            });
            
            y -= keyTextDim[1];

        } else {
            var valign = 'center';
        }


        //
        // Now, the titleX and titleY settings override (if set) the above
        if (RGraph.isNumber(obj.properties.titleX)) x = obj.properties.titleX;
        if (RGraph.isNumber(obj.properties.titleY)) y = obj.properties.titleY;

        // the titleX and titleY properties can be strings - in
        // which case the added to the calculated coordinate
        if (RGraph.isString(obj.properties.titleX)) x += parseFloat(obj.properties.titleX);
        if (RGraph.isString(obj.properties.titleY)) y += parseFloat(obj.properties.titleY);

        // Similar to the above - the titleOffsetx and titleOffsety
        // are more explicit properties for moving the title
        if (RGraph.isNumber(obj.properties.titleOffsetx)) x += obj.properties.titleOffsetx;
        if (RGraph.isNumber(obj.properties.titleOffsety)) y += obj.properties.titleOffsety;


        // Set the default vertical alignment for the title
        if (RGraph.isString(obj.properties.titleSubtitle) && obj.properties.titleSubtitle.length > 0) {
            valign = 'bottom';
        } else {
            valign = 'center';
        }



        //
        // Allow the user to override the horizontal alignment
        //
        if (RGraph.isString(obj.properties.titleHalign)) {
            halign = obj.properties.titleHalign;
        }
        
        //
        // Allow the user to override the vertical alignment
        //
        if (RGraph.isString(obj.properties.titleValign)) {
            valign = obj.properties.titleValign;
        }




        
        // Set the color            
        var oldColor = obj.context.fillStyle;
        obj.context.fillStyle = textConf.color ? textConf.color : 'black';





        // Draw the title
        var ret = RGraph.text({
            object:       obj,
            font:         textConf.font,
            size:         textConf.size,
            color:        textConf.color,
            bold:         textConf.bold,
            italic:       textConf.italic,
            x:            x,
            y:            y,
            text:         obj.properties.title,
            valign:       valign,
            halign:       halign,
            tag:          'title',
            marker:       false
        });












        // Draw the subtitle        
        if (typeof obj.properties.titleSubtitle === 'string' && obj.properties.titleSubtitle.length) {

            // Get the size of the title
            // Necessary any more ? var titleSize = textConf.size;
            
            // Set the default subtitle size if it's null
            if (RGraph.isNullish(obj.properties.titleSubtitleSize)) {
                obj.properties.titleSubtitleSize = textConf.size - 4;
            }
        
            var textConf = RGraph.getTextConf({
                object: obj,
                prefix: 'titleSubtitle'
            });

            // Draw the subtitle
            var ret = RGraph.text({
                object:  obj,
                font:    textConf.font,
                size:    textConf.size,
                color:   textConf.color,
                bold:    textConf.bold,
                italic:  textConf.italic,
                x:       x + obj.properties.titleSubtitleOffsetx,
                y:       y + obj.properties.titleSubtitleOffsety,
                text:    obj.properties.titleSubtitle,
                valign:  'top',
                halign:  halign,
                tag:     'subtitle',
                marker:  false
            });
        }






        // Reset the fill colour
        obj.context.fillStyle = oldColor;
    };








    //
    // Gets the mouse X/Y coordinates relative to the canvas
    // 
    // @param  args object An object consisting of:
    //                      o event
    // OR
    //
    // @param object e The event object. As such this method should be used in an event listener.
    //
    RGraph.getMouseXY = function ()
    {
        // If e.offsetX and e.offsetY are available just return
        // them
        if (   typeof window.event.offsetX === 'number'
            && typeof window.event.offsetY === 'number'
           ) {
           return [window.event.offsetX, window.event.offsetY];
        }

        var args = RGraph.getArgs(arguments, 'event');

        // This is necessary for IE9
        if (!args.event.target) {
            return;
        }

        var el      = args.event.target,
            canvas  = el,
            caStyle = canvas.style,
            offsetX = 0,
            offsetY = 0,
            x,
            y,
            borderLeft  = parseInt(caStyle.borderLeftWidth) || 0,
            borderTop   = parseInt(caStyle.borderTopWidth) || 0,
            paddingLeft = parseInt(caStyle.paddingLeft) || 0,
            paddingTop  = parseInt(caStyle.paddingTop) || 0,
            additionalX = borderLeft + paddingLeft,
            additionalY = borderTop + paddingTop;

        if (typeof args.event.offsetX === 'number' && typeof args.event.offsetY === 'number') {

            if (!RGraph.ISIE && !RGraph.ISOPERA) {
                x = args.event.offsetX - borderLeft - paddingLeft;
                y = args.event.offsetY - borderTop - paddingTop;
            
            } else if (RGraph.ISIE) {
                x = args.event.offsetX - paddingLeft;
                y = args.event.offsetY - paddingTop;
            
            } else {
                x = args.event.offsetX;
                y = args.event.offsetY;
            }   

        } else {

            if (typeof el.offsetParent !== 'undefined') {
                do {
                    offsetX += el.offsetLeft;
                    offsetY += el.offsetTop;
                } while ((el = el.offsetParent));
            }

            x = args.event.pageX - offsetX - additionalX;
            y = args.event.pageY - offsetY - additionalY;

            x -= (2 * (parseInt(document.body.style.borderLeftWidth) || 0));
            y -= (2 * (parseInt(document.body.style.borderTopWidth) || 0));

            //x += (parseInt(caStyle.borderLeftWidth) || 0);
            //y += (parseInt(caStyle.borderTopWidth) || 0);
        }

        // We return a javascript array with x and y defined
        return [x, y];
    };








    //
    // This function returns a two element array of the canvas x/y position in
    // relation to the page
    // 
    // @param  args object An object consisting of:
    //                      o canvas
    // OR
    //
    // @param object canvas
    //
    RGraph.getCanvasXY = function ()
    {
        var args = RGraph.getArgs(arguments, 'canvas');




        // If the getBoundingClientRect function is available - use that
        //
        if (args.canvas.getBoundingClientRect) {
            
            var rect = args.canvas.getBoundingClientRect();

            // Add the the current scrollTop and scrollLeft becuase the getBoundingClientRect()
            // method is relative to the viewport - not the document
            var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
                scrollTop  = window.pageYOffset || document.documentElement.scrollTop;
            
            return [rect.x + scrollLeft, rect.y + scrollTop];
        }


















        var x  = 0;
        var y  = 0;
        var el = args.canvas; // !!!

        do {

            x += el.offsetLeft;
            y += el.offsetTop;
            
            // ACCOUNT FOR TABLES IN wEBkIT
            if (el.tagName.toLowerCase() == 'table' && (RGraph.ISCHROME || RGraph.ISSAFARI)) {
                x += parseInt(el.border) || 0;
                y += parseInt(el.border) || 0;
            }

            el = el.offsetParent;

        } while (el && el.tagName.toLowerCase() != 'body');


        var paddingLeft = args.canvas.style.paddingLeft ? parseInt(args.canvas.style.paddingLeft) : 0;
        var paddingTop  = args.canvas.style.paddingTop ? parseInt(args.canvas.style.paddingTop) : 0;
        var borderLeft  = args.canvas.style.borderLeftWidth ? parseInt(args.canvas.style.borderLeftWidth) : 0;
        var borderTop   = args.canvas.style.borderTopWidth  ? parseInt(args.canvas.style.borderTopWidth) : 0;

        if (navigator.userAgent.indexOf('Firefox') > 0) {
            x += parseInt(document.body.style.borderLeftWidth) || 0;
            y += parseInt(document.body.style.borderTopWidth) || 0;
        }

        return [x + paddingLeft + borderLeft, y + paddingTop + borderTop];
    };








    //
    // This function determines whther a canvas is fixed (CSS positioning) or not. If not it returns
    // false. If it is then the element that is fixed is returned (it may be a parent of the canvas).
    // 
    // @param  args object An object consisting of:
    //                      o canvas
    // OR
    //
    // @return Either false or the fixed positioned element
    //
    RGraph.isFixed = function ()
    {
        var args = RGraph.getArgs(arguments, 'canvas');
        var i    = 0;

        while (args.canvas && args.canvas.tagName.toLowerCase() != 'body' && i < 99) {

            if (args.canvas.style.position == 'fixed') {
                return args.canvas;
            }
            
            args.canvas = args.canvas.offsetParent;
        }

        return false;
    };








    //
    // Registers a graph object (used when the canvas is redrawn)
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object obj The object to be registered
    //
    RGraph.register = function ()
    {
        var args = RGraph.getArgs(arguments, 'object');

        // Allow the registration of functions
        if (typeof args.object === 'function') {
            
            var func = args.object;
            
            // Register a shell object
            var temp = function ()
            {
                this.id            = null;
                this.isFunction    = true;
                this.canvas        = {id: null};
                this.getObjectByXY = function (){return false;};
                this.get           = function (){};
                this.set           = function (){};
                this.draw          = function (){func();};
            };
            
            args.object = new temp();
        }




        // Checking this property ensures the object is only
        // registered once
        if (!args.object.get('noregister') && args.object.get('register') !== false) {
            RGraph.ObjectRegistry.add(args.object);
            args.object.set('register', false);
        }
    };








    //
    // Causes all registered objects to be redrawn
    // 
    // @param  args object An object consisting of:
    //                      o color
    // OR
    //
    // @param string An optional color to use to clear the canvas
    //
    RGraph.redraw = function ()
    {
        var args = RGraph.getArgs(arguments, 'color');
        var objectRegistry = RGraph.ObjectRegistry.objects.byCanvasID;





        // if the argument is a canvas object (ie not a color string) then
        // call .redrawCanvas instead
        if (    typeof args.color === 'object'
            && args.color
            && typeof args.color.toString === 'function'
            && typeof args.color.toString().indexOf === 'function'
            && args.color.toString().indexOf('HTMLCanvasElement') > -1) {
            
            var opt = {canvas: args.color};
            
            // Has a color been given as well?
            if (arguments[1]) {
                opt.color = arguments[1];
            }
            
            return RGraph.redrawCanvas(opt);
        }





        // Get all of the canvas tags on the page
        var tags = document.getElementsByTagName('canvas');

        for (var i=0,len=tags.length; i<len; ++i) {
            if (tags[i] && tags[i].__object__ && tags[i].__object__.isrgraph) {
                
                // Only clear the canvas if it's not Trace'ing - this applies to the Line/Scatter Trace effects
                if (!tags[i].noclear) {
                    RGraph.clear(tags[i], args.color ? args.color : null);
                }
            }
        }

        // Go through the object registry and redraw *all* of the canvas'es that have been registered
        for (var i=0,len=objectRegistry.length; i<len; ++i) {
            if (objectRegistry[i]) {
                var id = objectRegistry[i][0];
                objectRegistry[i][1].draw();
            }
        }
    };








    //
    // Causes all registered objects ON THE GIVEN CANVAS to be redrawn
    // 
    // @param  args object An object consisting of:
    //                      o canvas
    //                      o clear
    //                      o color
    // OR
    //
    // @param canvas object The canvas object to redraw
    // @param        bool   Optional boolean which defaults to true and determines whether to clear the canvas
    //
    RGraph.redrawCanvas = function ()
    {
        var args = RGraph.getArgs(arguments, 'canvas,clear,color');
        var objects = RGraph.ObjectRegistry.getObjectsByCanvasID(args.canvas.id);

        //
        // First clear the canvas
        //
        if (RGraph.isNullish(args.clear) || (typeof args.clear === 'boolean' && args.clear !== false) ) {
            var color = args.color || args.canvas.__object__.get('clearto') || 'transparent';
            RGraph.clear(args.canvas, args.color);
        }
 
        //
        // Now redraw all the charts associated with that canvas
        //
        for (var i=0,len=objects.length; i<len; ++i) {
            if (objects[i]) {
                if (objects[i] && objects[i].isrgraph) { // Is it an RGraph object ??
                    objects[i].draw();
                }
            }
        }
    };








    //
    // This function draws the background for the bar chart, line chart and scatter chart.
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param  object obj The graph object
    //
    RGraph.Background.draw = function ()
    {
        var args         = RGraph.getArgs(arguments, 'object');
        var properties   = args.object.properties,
            height       = 0,
            marginLeft   = args.object.marginLeft,
            marginRight  = args.object.marginRight,
            marginTop    = args.object.marginTop,
            marginBottom = args.object.marginBottom,
            variant      = properties.variant

            RGraph.fireCustomEvent(args.object, 'beforebackground');

            args.object.context.fillStyle = properties.textColor;

            // If it's a bar and 3D variant, translate
            if (variant == '3d') {
                args.object.context.save();
                args.object.context.translate(properties.variantThreedOffsetx, -1 * properties.variantThreedOffsety);
            }
    
            // X axis title (not for the Bar or waterfall charts now - that's done
            // in the newer drawXAxis() function)
            if (
                   args.object.type !== 'bar'
                && args.object.type !== 'waterfall'
                && args.object.type !== 'hbar'
                && args.object.type !== 'line'
                && args.object.type !== 'gantt'
                && args.object.type !== 'scatter'
                && typeof properties.xaxisTitle === 'string'
                && properties.xaxisTitle.length
               ) {
            
                var size   = properties.textSize + 2;
                //var font   = properties.textFont;
                //var bold   = properties.xaxisTitleBold;
                //var italic = properties.xaxisTitleItalic;

                //if (typeof properties.xaxisTitleSize === 'number') {
                //    size = properties.xaxisTitleSize;
                //}
    
                //if (typeof properties.xaxisTitleFont === 'string') {
                //    font = properties.xaxisTitleFont;
                //}
                
                var hpos = ((args.object.canvas.width - marginLeft - marginRight) / 2) + marginLeft;
                var vpos = args.object.canvas.height - marginBottom + 25;
                
                if (typeof properties.xaxisTitlePos === 'number') {
                    vpos = args.object.canvas.height - (marginBottom * properties.xaxisTitlePos);
                }
    
    
    
    
                // Specifically specified X/Y positions
                if (typeof properties.xaxisTitleX === 'number') {
                    hpos = properties.xaxisTitleX;
                }
    
                if (typeof properties.xaxisTitleY === 'number') {
                    vpos = properties.xaxisTitleY;
                }
                
                // Get the text configuration
                var textConf = RGraph.getTextConf({
                    object: args.object,
                    prefix: 'xaxisTitle'
                });
    

                RGraph.text({
                
                  object: args.object,

                    font:   textConf.font,
                    size:   textConf.size,
                    color:  textConf.color,
                    bold:   textConf.bold,
                    italic: textConf.italic,

                    x:      hpos,
                    y:      vpos,
                    text:   properties.xaxisTitle,
                    halign: 'center',
                    valign: 'top',
                    tag:    'xaxis.title'
                });
            }

            // Y axis title
            if (
                   args.object.type !== 'bar'
                && args.object.type !== 'waterfall'
                && args.object.type !== 'hbar'
                && args.object.type !== 'line'
                && args.object.type !== 'gantt'
                && args.object.type !== 'scatter'
                && typeof properties.yaxisTitle === 'string'
                && properties.yaxisTitle.length) {

                var size   = properties.textSize + 2;
                var font   = properties.textFont;
                var italic = properties.textItalic;
                var angle  = 270;
                var bold   = properties.yaxisTitleBold;
                var color  = properties.yaxisTitleColor;

                if (typeof properties.yaxisTitlePos == 'number') {
                    var yaxis_title_pos = properties.yaxisTitlePos * marginLeft;
                } else if (args.object.type === 'hbar' && RGraph.isNullish(properties.yaxisTitlePos) ) {
                    var yaxis_title_pos = properties.marginLeft - args.object.yaxisLabelsSize;
                } else {

                    if (args.object && args.object.scale2) {

                        var yaxisTitleDimensions = RGraph.measureText({
                            text: args.object.scale2.labels[args.object.scale2.labels.length - 1],
                            bold: typeof properties.yaxisScaleBold === 'boolean' ? properties.yaxisScaleBold : properties.textBold,
                            font: properties.yaxisScaleFont || properties.textFont,
                            size: typeof properties.yaxisScaleSize === 'number' ? properties.yaxisScaleSize : properties.textSize
                        });

                    } else {
                        // This is here to allow for the drawing API background
                        // object
                        yaxisTitleDimensions = [0,0];
                    }

                    var yaxis_title_pos = properties.marginLeft - yaxisTitleDimensions[0] - 7;
                }
                if (typeof properties.yaxisTitleSize === 'number') {
                    size = properties.yaxisTitleSize;
                }
    
                if (typeof properties.yaxisTitleItalic === 'boolean') {
                    italic = properties.yaxisTitleItalic;
                }
    
                if (typeof properties.yaxisTitleFont === 'string') {
                    font = properties.yaxisTitleFont;
                }
                
                

                if (   properties.yaxisTitleAlign == 'right'
                    || properties.yaxisTitlePosition == 'right'
                    || (args.object.type === 'hbar' && properties.yaxisPosition === 'right' && typeof properties.yaxisTitleAlign === 'undefined' && typeof properties.yaxisTitlePosition === 'undefined')
                   ) {

                    angle = 90;
                    yaxis_title_pos = typeof properties.yaxisTitlePos === 'number'
                                          ? (args.object.canvas.width - marginRight) + (properties.yaxisTitlePos * marginRight)
                                          : args.object.canvas.width - marginRight + (properties.yaxisLabelsSize || properties.textSize) + 10;

                }

                var y = ((args.object.canvas.height - marginTop - marginBottom) / 2) + marginTop;

                // Specifically specified X/Y positions
                if (typeof properties.yaxisTitleX === 'number') {
                    yaxis_title_pos = properties.yaxisTitleX;
                }
    
                if (typeof properties.yaxisTitleY === 'number') {
                    y = properties.yaxisTitleY;
                }

                args.object.context.fillStyle = color;

                // Get the text configuration
                var textConf = RGraph.getTextConf({
                    object: args.object,
                    prefix: 'yaxisTitle'
                });


                RGraph.text({
                
                  object: args.object,
                
                    font:   textConf.font,
                    size:   textConf.size,
                    color:  textConf.color,
                    bold:   textConf.bold,
                    italic: textConf.italic,

                    x:          yaxis_title_pos,
                    y:          y,
                    valign:     'bottom',
                    halign:     'center',
                    angle:      angle,
                    text:       properties.yaxisTitle,
                    tag:        'yaxis.title',
                    accessible: false
                });
            }
    
            //
            // If the background color is spec ified - draw that. It's a rectangle that fills the
            // entire area within the margins
            //
            var bgcolor = properties.backgroundColor;

            if (bgcolor) {
                args.object.context.fillStyle = bgcolor;
                args.object.context.fillRect(marginLeft + 0.5, marginTop + 0.5, args.object.canvas.width - marginLeft - marginRight, args.object.canvas.height - marginTop - marginBottom);
            }







            //
            // Draw horizontal background bars
            //
            var numbars   = (properties.yaxisLabelsCount || 5);

            // If the backbgroundBarcount property is set use that
            if (typeof properties.backgroundBarsCount === 'number') {
                numbars = properties.backgroundBarsCount;
            }

            // Calculate the height of the bars
            var barHeight = (args.object.canvas.height - marginBottom - marginTop) / numbars;

            args.object.context.beginPath();
                args.object.context.fillStyle   = properties.backgroundBarsColor1;
                args.object.context.strokeStyle = args.object.context.fillStyle;
                height = (args.object.canvas.height - marginBottom);

                for (var i=0; i<numbars; i+=2) {
                    args.object.context.rect(marginLeft,
                        (i * barHeight) + marginTop,
                        args.object.canvas.width - marginLeft - marginRight,
                        barHeight
                    );
                }
            args.object.context.fill();



            args.object.context.beginPath();
                args.object.context.fillStyle   = properties.backgroundBarsColor2;
                args.object.context.strokeStyle = args.object.context.fillStyle;
        
                for (var i=1; i<numbars; i+=2) {
                    args.object.context.rect(
                        marginLeft,
                        (i * barHeight) + marginTop,
                        args.object.canvas.width - marginLeft - marginRight,
                        barHeight
                    );
                }
            
            args.object.context.fill();
            
            // Close any errantly open path
            args.object.context.beginPath();













            //
            // The background grid is cached
            //
            var func = function (obj, cacheCanvas, cacheContext)
            {
                // Draw the background grid
                if (properties.backgroundGrid) {
                
                    properties.backgroundGridHlinesCount += 0.0001;

                    // If autofit is specified, use the .numhlines and .numvlines along with the width to work
                    // out the hsize and vsize
                    if (properties.backgroundGridAutofit) {

                        //
                        // Align the grid to the tickmarks
                        //
                        if (properties.backgroundGridAutofitAlign) {

                            // Align the horizontal lines
                            if (obj.type === 'hbar') {
                                obj.set('backgroundGridHlinesCount', obj.data.length);
                            }

                            // Align the vertical lines for the line
                            if (obj.type === 'line') {

                                if (typeof properties.backgroundGridVlinesCount === 'number') {
                                    // Nada
                                } else if (properties.xaxisLabels && properties.xaxisLabels.length) {
                                    obj.set('backgroundGridVlinesCount', properties.xaxisLabels.length - 1);
                                } else {
                                
                                    obj.set(
                                        'backgroundGridVlinesCount',
                                        obj.data[0].length > 0 ? obj.data[0].length - 1 : 0
                                    );
                                }
                            } else if (obj.type === 'waterfall') {
                                obj.set(
                                    'backgroundGridVlinesCount',
                                    obj.data.length + (properties.total ? 1 : 0)
                                );

                            // Align the vertical lines for the bar
                            } else if (obj.type === 'bar') {
                                
                                // 13/12/2018
                                //
                                // Updated to be the same as the number of data points
                                //
                                obj.set('backgroundGridVlinesCount', obj.data.length);
                            
                            // Align the vertical lines for the Scatter
                            } else if (obj.type === 'scatter') {
                                if (typeof properties.backgroundGridVlinesCount !== 'number') {
                                    
                                    // Set the number of grid lines to the same
                                    // as the number of labels
                                    if (RGraph.isArray(properties.xaxisLabels) && properties.xaxisLabels.length) {
                                        obj.set('backgroundGridVlinesCount', properties.xaxisLabels.length);
                                    
                                    // No labels - set the number of grid lines
                                    // to 10
                                    } else {
                                        obj.set('backgroundGridVlinesCount', 10);
                                    }
                                }

    
                            // Gantt
                            } else if (obj.type === 'gantt') {

                                if (typeof obj.get('backgroundGridVlinesCount') === 'number') {
                                    // Nothing to do here
                                } else {
                                    obj.set('backgroundGridVlinesCount', properties.xaxisScaleMax);
                                }
    
                                obj.set('backgroundGridHlinesCount', obj.data.length);
                            
                            // HBar
                            } else if (obj.type === 'hbar' && RGraph.isNullish(properties.backgroundGridHlinesCount) ) {
                                obj.set('backgroundGridHlinesCount', obj.data.length);
                            }
                        }
    
                        var vsize = ((cacheCanvas.width - marginLeft - marginRight)) / properties.backgroundGridVlinesCount;
                        var hsize = (cacheCanvas.height - marginTop - marginBottom) / properties.backgroundGridHlinesCount;

                        obj.set('backgroundGridVsize', vsize);
                        obj.set('backgroundGridHsize', hsize);
                    }

                    obj.context.beginPath();
                    cacheContext.lineWidth   = properties.backgroundGridLinewidth ? properties.backgroundGridLinewidth : 1;
                    cacheContext.strokeStyle = properties.backgroundGridColor;

                    // Dashed background grid
                    if (properties.backgroundGridDashed && typeof cacheContext.setLineDash == 'function') {
                        cacheContext.setLineDash([3,5]);
                    }
                    
                    // Dotted background grid
                    if (properties.backgroundGridDotted && typeof cacheContext.setLineDash == 'function') {
                        cacheContext.setLineDash([1,3]);
                    }
                    
                    // Custom linedash
                    if (RGraph.isArray(properties.backgroundGridDashArray)) {
                        cacheContext.setLineDash(properties.backgroundGridDashArray);
                    }
                    
                    obj.context.beginPath();
        

                    // Draw the horizontal lines
                    if (properties.backgroundGridHlines) {
                        height = (cacheCanvas.height - marginBottom)
                        var hsize = properties.backgroundGridHsize;
                        
                        for (y=marginTop; y<=(height+1); y+=hsize) {

                            cacheContext.moveTo(marginLeft, Math.round(y));
                            cacheContext.lineTo(args.object.canvas.width - marginRight, Math.round(y));

                            if (   args.object.properties.variant === '3d'
                                && args.object.type === 'bar'
                                && args.object.properties.backgroundGridThreedYaxis) {

                                cacheContext.moveTo(marginLeft, Math.round(y));
                                cacheContext.lineTo(
                                    marginLeft - args.object.properties.variantThreedOffsetx,
                                    Math.round(y) + args.object.properties.variantThreedOffsety
                                );
                            }
                        }
                    }
        
                    // Draw the vertical lines

                    if (properties.backgroundGridVlines) {

                        var width = (cacheCanvas.width - marginRight);
                        var vsize = properties.backgroundGridVsize;

                        for (var x=marginLeft; Math.round(x)<=width; x+=vsize) {
                            cacheContext.moveTo(Math.round(x), marginTop);
                            cacheContext.lineTo(Math.round(x), obj.canvas.height - marginBottom);
                        }
                    }
        
                    if (properties.backgroundGridBorder) {
                        // Make sure a rectangle, the same colour as the grid goes around the graph
                        cacheContext.strokeStyle = properties.backgroundGridColor;    
                        cacheContext.strokeRect(Math.round(marginLeft), Math.round(marginTop), obj.canvas.width - marginLeft - marginRight, obj.canvas.height - marginTop - marginBottom);
                    }
                }
    
                cacheContext.stroke();
    
    
    
                // Ensure the grid is drawn before continuing
                cacheContext.beginPath();
                cacheContext.closePath();
            }

            // Now a cached draw in newer browsers
            RGraph.cachedDraw(
                args.object,
                args.object.uid + '_background',
                func
            );







            // If it's a bar and 3D variant, translate
            if (variant == '3d') {
                args.object.context.restore();
            }

            // Reset the line dash
            if (typeof args.object.context.setLineDash == 'function') {
                //args.object.context.setLineDash([1, 0]); // Old
                args.object.context.setLineDash([]); // New - should be faster
            }
    
            args.object.context.stroke();
            
            
            //
            // Draw the backgroundBorder if requested
            //
            if (properties.backgroundBorder) {
                
                var color     = RGraph.isString(properties.backgroundBorderColor) ? properties.backgroundBorderColor : '#aaa';
                var linewidth = RGraph.isNumber(properties.backgroundBorderLinewidth) ? properties.backgroundBorderLinewidth : 1;
                
                // Dashed background border
                if (properties.backgroundBorderDashed && typeof args.object.context.setLineDash == 'function') {
                    args.object.context.setLineDash([3,5]);
                }
                
                // Dotted background grid
                if (properties.backgroundBorderDotted && typeof args.object.context.setLineDash == 'function') {
                    args.object.context.setLineDash([1,3]);
                }
                
                // Custom linedash
                if (RGraph.isArray(properties.backgroundBorderDashArray)) {
                    args.object.context.setLineDash(properties.backgroundBorderDashArray);
                }

                args.object.path(
                    'b lc square lw % r % % % % s %',
                    linewidth,
                    args.object.properties.marginLeft,
                    args.object.properties.marginTop,
                    args.object.canvas.width  - args.object.properties.marginLeft - args.object.properties.marginRight,
                    args.object.canvas.height - args.object.properties.marginTop  - args.object.properties.marginBottom,
                    color
                );
                
                // Reset the linedash
                //args.object.context.setLineDash([1,0]);  // Old
                args.object.context.setLineDash([]);// New - should be faster
            }



        // Draw the title if one is set
        if ( typeof args.object.properties.title === 'string') {
            RGraph.drawTitle(args.object);
        }
        
        // Fire the background event
        RGraph.fireCustomEvent(args.object, 'background');
    };








    //
    // Formats a number with thousand separators so it's easier to read
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o number
    //                      o unitspre
    //                      o unitspost
    //                      o point
    //                      o thousand
    //                      o formatter
    // OR
    //
    // THESE ARE OLDER ARGS:
    // 
    // @param  object obj The chart object
    // @param  integer num The number to format
    // @param  string      The (optional) string to prepend to the string
    // @param  string      The (optional) string to append to the string
    // @return string      The formatted number
    //
    RGraph.numberFormat = function (args)
    {
        var i;
        var prepend = args.unitspre ? String(args.unitspre) : '';
        var append  = args.unitspost ? String(args.unitspost) : '';
        var output  = '';
        var decimal = '';
        var decimal_seperator  = typeof args.point    === 'string' ? args.point    : '.';
        var thousand_seperator = typeof args.thousand === 'string' ? args.thousand : ',';
        RegExp.$1   = '';
        var i,j;

        if (typeof args.formatter === 'function') {
            return (args.formatter)(args);
        }

        // Ignore the preformatted version of "1e-2"
        if (String(args.number).indexOf('e') > 0) {
            return String(prepend + String(args.number) + append);
        }

        // We need then number as a string
        args.number = String(args.number);

        // Take off the decimal part - we re-append it later
        if (args.number.indexOf('.') > 0) {
            var tmp    = args.number;
            args.number = args.number.replace(/\.(.*)/, ''); // The front part of the number
            decimal    = tmp.replace(/(.*)\.(.*)/, '$2'); // The decimal part of the number
        }

        // Thousand separator
        //var separator = arguments[1] ? String(arguments[1]) : ',';
        var seperator = thousand_seperator;

        // Work backwards adding the thousand separators
        //
        // ** i is a local variable at this poin **
        var foundPoint;
        for (i=(args.number.length - 1),j=0; i>=0; j++,i--) {
            var character = args.number.charAt(i);
            
            if ( j % 3 == 0 && j != 0) {
                output += seperator;
            }
            
            //
            // Build the output
            //
            output += character;
        }

        //
        // Now need to reverse the string
        //
        var rev = output;
        output = '';
        for (i=(rev.length - 1); i>=0; i--) {
            output += rev.charAt(i);
        }

        // Tidy up
        //output = output.replace(/^-,/, '-');
        if (output.indexOf('-' + args.thousand) == 0) {
            output = '-' + output.substr(('-' + args.thousand).length);
        }

        // Reappend the decimal
        if (decimal.length) {
            output =  output + decimal_seperator + decimal;
            decimal = '';
            RegExp.$1 = '';
        }

        // Minor bugette
        if (output.charAt(0) == '-') {
            output = output.replace(/-/, '');
            prepend = '-' + prepend;
        }
        
        // Get rid of leading commas
        output = output.replace(/^,+/,'');

        return prepend + output + append;
    };








    //
    // Draws horizontal coloured bars on something like the bar, line or scatter
    // 
    // @param  args object An object consisting of:
    //                      o object
    //
    RGraph.drawBars = function ()
    {
        var args       = RGraph.getArgs(arguments, 'object'),
            properties = args.object.properties,
            hbars      = properties.backgroundHbars;

        if (hbars === null) {
            return;
        }

        //
        // Draws a horizontal bar
        //
        args.object.context.beginPath();

        for (var i=0,len=hbars.length; i<len; ++i) {
        
            var start  = hbars[i][0];
            var length = hbars[i][1];
            var color  = hbars[i][2];
            

            // Perform some bounds checking
            if(RGraph.isNullish(start))start = args.object.scale2.max
            if (start > args.object.scale2.max) start = args.object.scale2.max;
            if (RGraph.isNullish(length)) length = args.object.scale2.max - start;
            if (start + length > args.object.scale2.max) length = args.object.scale2.max - start;
            if (start + length < (-1 * args.object.scale2.max) ) length = (-1 * args.object.scale2.max) - start;

            if (properties.xaxisPosition == 'center' && start == args.object.scale2.max && length < (args.object.scale2.max * -2)) {
                length = args.object.scale2.max * -2;
            }


            //
            // Draw the bar
            //
            var x = properties.marginLeft;
            var y = args.object.getYCoord(start);
            var w = args.object.canvas.width - properties.marginLeft - properties.marginRight;
            var h = args.object.getYCoord(start + length) - y;

            // Accommodate Opera :-/
            if (RGraph.ISOPERA != -1 && properties.xaxisPosition == 'center' && h < 0) {
                h *= -1;
                y = y - h;
            }

            //
            // Account for X axis at the top
            //
            if (properties.xaxisPosition == 'top') {
                y  = args.object.canvas.height - y;
                h *= -1;
            }

            args.object.context.fillStyle = color;
            args.object.context.fillRect(x, y, w, h);
        }




//
//            // If the X axis is at the bottom, and a negative max is given, warn the user
//            if (args.object.get('xaxisPosition') == 'bottom' && (hbars[i][0] < 0 || (hbars[i][1] + hbars[i][1] < 0)) ) {
//                alert('[' + args.object.type.toUpperCase() + ' (ID: ' + args.object.id + ') BACKGROUND HBARS] You have a negative value in one of your background hbars values, whilst the X axis is in the center');
//            }
//
//            var ystart = (args.object.grapharea - (((hbars[i][0] - args.object.scale2.min) / (args.object.scale2.max - args.object.scale2.min)) * args.object.grapharea));
//            //var height = (Math.min(hbars[i][1], args.object.max - hbars[i][0]) / (args.object.scale2.max - args.object.scale2.min)) * args.object.grapharea;
//            var height = args.object.getYCoord(hbars[i][0]) - args.object.getYCoord(hbars[i][1]);
//
//           // Account for the X axis being in the center
//            if (args.object.get('xaxisPosition') == 'center') {
//                ystart /= 2;
//                //height /= 2;
//            }
//            
//            ystart += args.object.get('marginTop')
//
//            var x = args.object.get('marginLeft');
//            var y = ystart - height;
//            var w = args.object.canvas.width - args.object.get('marginLeft') - args.object.get('marginRight');
//            var h = height;
//
//            // Accommodate Opera :-/
//            if (navigator.userAgent.indexOf('Opera') != -1 && args.object.get('xaxisPosition') == 'center' && h < 0) {
//                h *= -1;
//                y = y - h;
//            }
//            
//            //
//            // Account for X axis at the top
//            //
//            //if (args.object.get('xaxisPosition') == 'top') {
//            //    y  = args.object.canvas.height - y;
//            //    h *= -1;
//            //}
//
//            //args.object.context.fillStyle = hbars[i][2];
//            //args.object.context.fillRect(x, y, w, h);
//        //}
    };








    //
    // Draws in-graph labels.
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object obj The graph object
    //
    RGraph.drawInGraphLabels = function ()
    {
        var args             = RGraph.getArgs(arguments, 'object');
        var properties       = args.object.properties,
            labels           = properties.labelsIngraph,
            labels_processed = [];

        // Defaults
        var fgcolor   = 'black',
            bgcolor   = 'white',
            direction = 1;

        if (!labels) {
            return;
        }

        // Get the text configuration
        var textConf = RGraph.getTextConf({
            object: args.object,
            prefix: 'labelsIngraph'
        });

        //
        // Preprocess the labels array. Numbers are expanded
        //
        for (var i=0,len=labels.length; i<len; i+=1) {
            if (typeof labels[i] === 'number') {
                for (var j=0; j<labels[i]; ++j) {
                    labels_processed.push(null);
                }
            } else if (typeof labels[i] === 'string' || typeof labels[i] === 'object') {
                labels_processed.push(labels[i]);

            } else {
                labels_processed.push('');
            }
        }








        // Turn off any shadow
        RGraph.noShadow(args.object);








        if (labels_processed && labels_processed.length > 0) {

            for (var i=0,len=labels_processed.length; i<len; i+=1) {
                if (labels_processed[i]) {
                    var coords = args.object.coords[i];
                    
                    if (coords && coords.length > 0) {
                        var x      = ((args.object.type == 'bar' ? coords[0] + (coords[2] / 2) : coords[0])) + (properties.labelsIngraphOffsetx || 0);
                        var y      = (args.object.type == 'bar' ? coords[1] + (coords[3] / 2) : coords[1]) + (properties.labelsIngraphOffsety || 0);
                        var length = typeof labels_processed[i][4] === 'number' ? labels_processed[i][4] : 25;
    
                        args.object.context.beginPath();
                        args.object.context.fillStyle   = 'black';
                        args.object.context.strokeStyle = 'black';
                        

                        if (args.object.type === 'bar') {
                        
                            //
                            // X axis at the top
                            //
                            if (args.object.get('xaxisPosition') == 'top') {
                                length *= -1;
                            }
    
                            if (properties.variant == 'dot') {
                                args.object.context.moveTo(Math.round(x), args.object.coords[i][1] - 5);
                                args.object.context.lineTo(Math.round(x), args.object.coords[i][1] - 5 - length);
                                
                                var text_x = Math.round(x);
                                var text_y = args.object.coords[i][1] - 5 - length;
                            
                            } else if (properties.variant == 'arrow') {
                                args.object.context.moveTo(Math.round(x), args.object.coords[i][1] - 5);
                                args.object.context.lineTo(Math.round(x), args.object.coords[i][1] - 5 - length);
                                
                                var text_x = Math.round(x);
                                var text_y = args.object.coords[i][1] - 5 - length;
                            
                            } else {
    
                                args.object.context.arc(Math.round(x), y, 2.5, 0, 6.28, 0);
                                args.object.context.moveTo(Math.round(x), y);
                                args.object.context.lineTo(Math.round(x), y - length);

                                var text_x = Math.round(x);
                                var text_y = y - length;
                            }

                            args.object.context.stroke();
                            args.object.context.fill();
                            
    
                        } else {

                            if (
                                typeof labels_processed[i] == 'object' &&
                                typeof labels_processed[i][3] == 'number' &&
                                labels_processed[i][3] == -1
                               ) {

                                // Draw an up arrow
                                drawUpArrow(x, y)
                                var valign = 'top';
                                
                                var text_x = x;
                                var text_y = y + 5 + length;
                            
                            } else {

                                var text_x = x;
                                var text_y = y - 5 - length;

                                if (text_y < 5 && (typeof labels_processed[i] === 'string' || typeof labels_processed[i][3] === 'undefined')) {
                                    text_y = y + 5 + length;
                                    var valign = 'top';
                                }

                                if (valign === 'top') {
                                    /// Draw an down arrow
                                    drawUpArrow(x, y);
                                } else {
                                    /// Draw an up arrow
                                    drawDownArrow(x, y);
                                }
                            }
                        
                            args.object.context.fill();
                        }

                        args.object.context.beginPath();

                            // Foreground color
                            if ((typeof labels_processed[i] === 'object' && typeof labels_processed[i][1] === 'string')) {
                                args.object.context.fillStyle = labels_processed[i][1];
                            } else {
                                args.object.context.fillStyle = properties.labelsIngraphColor;
                            }

                            RGraph.text({
                            
                              object:            args.object,

                                font:            textConf.font,
                                size:            textConf.size,
                                color:           args.object.context.fillStyle || textConf.color,
                                bold:            textConf.bold,
                                italic:          textConf.italic,

                                x:               text_x,
                                y:               text_y + (args.object.properties.textAccessible ? 2 : 0),

                                text:            (typeof labels_processed[i] === 'object' && typeof labels_processed[i][0] === 'string') ? labels_processed[i][0] : labels_processed[i],
                                valign:          valign || 'bottom',
                                halign:          'center',
                                bounding:        true,
                                'bounding.fill': (typeof labels_processed[i] === 'object' && typeof labels_processed[i][2] === 'string') ? labels_processed[i][2] : 'white',
                                tag:             'labels ingraph'
                            });
                        args.object.context.fill();
                    }




                    // Draws a down arrow
                    function drawUpArrow (x, y)
                    {
                        args.object.context.moveTo(Math.round(x), y + 5);
                        args.object.context.lineTo(Math.round(x), y + 5 + length);
                        
                        args.object.context.stroke();
                        args.object.context.beginPath();                                
                        
                        // This draws the arrow
                        args.object.context.moveTo(Math.round(x), y + 5);
                        args.object.context.lineTo(Math.round(x) - 3, y + 10);
                        args.object.context.lineTo(Math.round(x) + 3, y + 10);
                        args.object.context.closePath();
                    }




                    // Draw an up arrow
                    function drawDownArrow (x, y)
                    {
                        args.object.context.moveTo(Math.round(x), y - 5);
                        args.object.context.lineTo(Math.round(x), y - 5 - length);
                        
                        args.object.context.stroke();
                        args.object.context.beginPath();
                        
                        // This draws the arrow
                        args.object.context.moveTo(Math.round(x), y - 5);
                        args.object.context.lineTo(Math.round(x) - 3, y - 10);
                        args.object.context.lineTo(Math.round(x) + 3, y - 10);
                        args.object.context.closePath();
                    }
                    
                    valign = undefined;
                }
            }
        }
    };








    //
    // This function hides the crosshairs coordinates. This function
    // has no arguments
    //
    RGraph.hideCrosshairCoords = function ()
    {
        var div = RGraph.Registry.get('coordinates.coords.div');

        if (   div
            && div.style.opacity == 1
            && div.__object__.get('crosshairsCoordsFadeout')
           ) {
            
            var style = RGraph.Registry.get('coordinates.coords.div').style;

            setTimeout(function() {style.opacity = 0.9;}, 25);
            setTimeout(function() {style.opacity = 0.8;}, 50);
            setTimeout(function() {style.opacity = 0.7;}, 75);
            setTimeout(function() {style.opacity = 0.6;}, 100);
            setTimeout(function() {style.opacity = 0.5;}, 125);
            setTimeout(function() {style.opacity = 0.4;}, 150);
            setTimeout(function() {style.opacity = 0.3;}, 175);
            setTimeout(function() {style.opacity = 0.2;}, 200);
            setTimeout(function() {style.opacity = 0.1;}, 225);
            setTimeout(function() {style.opacity = 0;}, 250);
            setTimeout(function() {style.display = 'none';}, 275);
        }
    };









    //
    // Draws the3D axes/background
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object obj The chart object
    //
    RGraph.draw3DAxes = function ()
    {
        var args       = RGraph.getArgs(arguments, 'object');
        var properties = args.object.properties;

        var marginLeft    = args.object.marginLeft,
            marginRight   = args.object.marginRight,
            marginTop     = args.object.marginTop,
            marginBottom  = args.object.marginBottom,
            xaxispos      = properties.xaxisPosition,
            graphArea     = args.object.canvas.height - marginTop - marginBottom,
            halfGraphArea = graphArea / 2,
            offsetx       = properties.variantThreedOffsetx,
            offsety       = properties.variantThreedOffsety,
            xaxis         = properties.variantThreedXaxis,
            yaxis         = properties.variantThreedYaxis


        //
        // Draw the 3D Y axis
        //
        if (yaxis) {
            RGraph.draw3DYAxis(args.object);
        }
        
        
        
        // X axis
        if (xaxis) {
            if (xaxispos === 'center') {
                args.object.path(
                    'b m % % l % % l % % l % % c s #aaa f %',
                    marginLeft,marginTop + halfGraphArea,
                    marginLeft + offsetx,marginTop + halfGraphArea - offsety,
                    args.object.canvas.width - marginRight + offsetx,marginTop + halfGraphArea - offsety,
                    args.object.canvas.width - marginRight,marginTop + halfGraphArea,
                    properties.variantThreedXaxisColor
                );

            } else {

                if (args.object.type === 'hbar') {
                    var xaxisYCoord = args.object.canvas.height - args.object.properties.marginBottom;
                } else {
                    var xaxisYCoord = args.object.getYCoord(0);
                }

                args.object.path(
                    'm % % l % % l % % l % % c s #aaa f %',
                    marginLeft,xaxisYCoord,
                    marginLeft + offsetx,xaxisYCoord - offsety,
                    args.object.canvas.width - marginRight + offsetx,xaxisYCoord - offsety,
                    args.object.canvas.width - marginRight,xaxisYCoord,
                    properties.variantThreedXaxisColor
                );
            }
        }
    };








    //
    // Draws the3D Y axis/background
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object obj The chart object
    //
    RGraph.draw3DYAxis = function (args)
    {
        var args       = RGraph.getArgs(arguments, 'object');
        var properties = args.object.properties;

        var marginLeft    = args.object.marginLeft,
            marginRight   = args.object.marginRight,
            marginTop     = args.object.marginTop,
            marginBottom  = args.object.marginBottom,
            xaxispos      = properties.xaxisPosition,
            graphArea     = args.object.canvas.height - marginTop - marginBottom,
            halfGraphArea = graphArea / 2,
            offsetx       = properties.variantThreedOffsetx,
            offsety       = properties.variantThreedOffsety,
            yaxisFill     = properties.variantThreedYaxisColor || '#ddd';

        
        
        // Y axis
        // Commented out the if condition because of drawing oddities
        //if (!properties.noaxes && !properties.noyaxis) {

            if ( (args.object.type === 'hbar' || args.object.type === 'bar') && properties.yaxisPosition === 'center') {
                var x = ((args.object.canvas.width - marginLeft - marginRight) / 2) + marginLeft;
            } else if ((args.object.type === 'hbar' || args.object.type === 'bar') && properties.yaxisPosition === 'right') {
                var x = args.object.canvas.width - marginRight;
            } else {
                var x = marginLeft;
            }

            args.object.path(
                'b m % % l % % l % % l % % s #aaa f % b', // Don't know the b at the end is needed
                x,marginTop,
                x + offsetx, marginTop - offsety,
                x + offsetx, args.object.canvas.height - marginBottom - offsety,
                x,args.object.canvas.height - marginBottom,
                properties.variantThreedYaxisColor
            );
        //}
    };








    //
    // Draws a filled rectangle with curvy corners
    // 
    // @param  args object An object consisting of:
    //                      o context
    //                      o x
    //                      o y
    //                      o w
    //                      o h
    //                      o roundtl
    //                      o roundtr
    //                      o roundbl
    //                      o roundbr
    // OR
    //
    // @param context object The context
    // @param x       number The X coordinate (top left of the square)
    // @param y       number The Y coordinate (top left of the square)
    // @param w       number The width of the rectangle
    // @param h       number The height of the rectangle
    // @param         number The radius of the curved corners
    // @param         boolean Whether the top left corner is curvy
    // @param         boolean Whether the top right corner is curvy
    // @param         boolean Whether the bottom right corner is curvy
    // @param         boolean Whether the bottom left corner is curvy
    //
    RGraph.roundedRect = function ()
    {
        var args = RGraph.getArgs(arguments, 'context,x,y,width,height,radius,roundtl,roundtr,roundbl,roundbr');

        // The corner radius
        var r = args.radius ? args.radius : 3;

        // Change the radius based on the smallest width or height
        r = Math.min(
            Math.min(args.width, args.height) / 2,
            args.radius
        );

        // The corners
        var corner_tl = (args.roundtl === false) ? false : true,
            corner_tr = (args.roundtr === false) ? false : true,
            corner_bl = (args.roundbl === false) ? false : true,
            corner_br = (args.roundbr === false) ? false : true;

        args.context.beginPath();
            
            args.context.moveTo(args.x, args.y + r);

            // Top left corner
            if (corner_tl) {
                args.context.arc(args.x + r, args.y + r, r, RGraph.PI, RGraph.PI + RGraph.HALFPI, false);
            } else {
                args.context.lineTo(args.x, args.y);
                args.context.lineTo(args.x + r, args.y);
            }

            // Top right corner
            if (corner_tr) {
                args.context.arc(args.x + args.width - r, args.y + r, r, RGraph.PI + RGraph.HALFPI, 0, false);
            } else {
                args.context.lineTo(args.x + args.width, args.y);
                args.context.lineTo(args.x + args.width, args.y + r);
            }



            // Bottom right corner
            if (corner_br) {
                args.context.arc(args.x + args.width - r, args.y + args.height - r, r, 0, RGraph.HALFPI, false);
            } else {
                args.context.lineTo(args.x + args.width, args.y + args.height);
                args.context.lineTo(args.x + args.width - r, args.y + args.height);
            }

            // Bottom left corner
            if (corner_bl) {
                args.context.arc(args.x + r, args.y - r + args.height, r, RGraph.HALFPI, RGraph.PI, false);
            } else {
                args.context.lineTo(args.x, args.y + args.height);
                args.context.lineTo(args.x, args.y + args.height - r);
            }
            
            args.context.closePath();
    };








    //
    // Hides the zoomed canvas.
    //
    // UPDATE 14th Oct 2019
    // Zoom has been removed for some time now so this is now commented out
    //
    //RGraph.hideZoomedCanvas =
    //RGraph.HideZoomedCanvas = function ()
    //{
    //    var interval = 10;
    //    var frames   = 15;
    //
    //    if (typeof RGraph.zoom_image === 'object') {
    //        var obj        = RGraph.zoom_image.obj;
    //        var properties = obj.properties;
    //    } else {
    //        return;
    //    }
    //
    //    if (properties.zoomFadeOut) {
    //        for (var i=frames,j=1; i>=0; --i, ++j) {
    //            if (typeof RGraph.zoom_image === 'object') {
    //                setTimeout("RGraph.zoom_image.style.opacity = " + String(i / 10), j * interval);
    //            }
    //        }
    //
    //        if (typeof RGraph.zoom_background === 'object') {
    //            setTimeout("RGraph.zoom_background.style.opacity = " + String(i / frames), j * interval);
    //        }
    //    }
    //
    //    if (typeof RGraph.zoom_image === 'object') {
    //        setTimeout("RGraph.zoom_image.style.display = 'none'", properties.zoomFadeOut ? (frames * interval) + 10 : 0);
    //    }
    //
    //    if (typeof RGraph.zoom_background === 'object') {
    //        setTimeout("RGraph.zoom_background.style.display = 'none'", properties.zoomFadeOut ? (frames * interval) + 10 : 0);
    //    }
    //};








    //
    // Adds an event handler
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o name
    //                      o func
    // OR
    //
    // @param object obj   The graph object
    // @param string event The name of the event, eg ontooltip
    // @param object func  The callback function
    //
    RGraph.addCustomEventListener = function ()
    {
        var args = RGraph.getArgs(arguments, 'object,name,func');

        // Initialise the events array if necessary
        if (typeof RGraph.events[args.object.uid] === 'undefined') {
            RGraph.events[args.object.uid] = [];
        }

        // Prepend "on" if necessary
        if (args.name.substr(0, 2) !== 'on') {
            args.name = 'on' + args.name;
        }

        RGraph.events[args.object.uid].push([
            args.object,
            args.name,
            args.func
        ]);

        return RGraph.events[args.object.uid].length - 1;
    };








    //
    // Used to fire one of the RGraph custom events
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o name
    // OR
    //
    // @param object obj   The graph object that fires the event
    // @param string event The name of the event to fire
    //
    RGraph.fireCustomEvent = function ()
    {
        var args = RGraph.getArgs(arguments, 'object,name');

        // Prepend the name with "on" if necessary
        if (args.name.substr(0,2) !== 'on') {
            args.name = 'on' + args.name;
        }

        if (args.object && args.object.isrgraph) {

            // This allows the eventsMouseout property to work
            // (for some reason...)
            //
            // 25/10/19 - Taken out
            //
            //if (args.name.match(/(on)?mouseout/) && typeof args.object.properties.eventsMouseout === 'function') {
            //    (args.object.properties.eventsMouseout)(args.object);
            //}
            // DOM1 style of adding custom events
            if (args.object[args.name]) {
                (args.object[args.name])(args.object);
            }

            var uid = args.object.uid;

            if (   typeof uid === 'string'
                && typeof RGraph.events === 'object'
                && typeof RGraph.events[uid] === 'object'
                && RGraph.events[uid].length > 0) {

                for(var j=0; j<RGraph.events[uid].length; ++j) {
                    if (RGraph.events[uid][j] && RGraph.events[uid][j][1] === args.name) {
                        RGraph.events[uid][j][2](args.object);
                    }
                }
            }
        }
    };








    //
    // Clears all the custom event listeners that have been registered
    // 
    // @param  args object An object consisting of:
    //                      o uid
    // OR
    //
    // @param    string Limits the clearing to this object UID
    //
    RGraph.removeAllCustomEventListeners = function ()
    {
        var args = RGraph.getArgs(arguments, 'uid');

        if (args.uid && RGraph.events[args.uid]) {
            RGraph.events[args.uid] = [];
        } else {
            RGraph.events = [];
            
            // Use the ObjectRegistry to iterate through all registered objects
            // and remove the DOM1 listeners
            RGraph.ObjectRegistry.iterate(function (obj)
            {
                if (obj.onclick)     obj.onclick     = null;
                if (obj.onmousemove) obj.onmousemove = null;
                if (obj.onmouseover) obj.onmouseover = null;
                if (obj.onmouseout)  obj.onmouseout  = null;
            });
        }
    };








    //
    // Clears a particular custom event listener
    // 
    // @param object obj The graph object
    // @param number i   This is the index that is return by .addCustomEventListener()
    //
    RGraph.removeCustomEventListener = function ()
    {
        var args = RGraph.getArgs(arguments, 'object,index');

        if (   typeof RGraph.events === 'object'
            && typeof RGraph.events[args.object.uid] === 'object'
            && typeof RGraph.events[args.object.uid][args.index] === 'object') {

            RGraph.events[args.object.uid][args.index] = null;
        }
    };








    //
    // This draws the background
    // @param  args object An object consisting of:
    //                      o object
    // OR
    // 
    // @param object obj The graph object
    //
    RGraph.drawBackgroundImage = function (args)
    {
        var args       = RGraph.getArgs(arguments, 'object');
        var properties = args.object.properties;

        if (typeof properties.backgroundImage === 'string') {
            if (typeof args.object.canvas.__rgraph_background_image__ === 'undefined') {
                var img = new Image();
                img.__object__  = args.object;
                img.__canvas__  = args.object.canvas;
                img.__context__ = args.object.context;
                img.src         = args.object.get('backgroundImage');
                
                args.object.canvas.__rgraph_background_image__ = img;
            } else {
                img = args.object.canvas.__rgraph_background_image__;
            }

            // When the image has loaded - redraw the canvas
            img.onload = function ()
            {
                args.object.__rgraph_background_image_loaded__ = true;
                RGraph.clear(args.object.canvas);
                RGraph.redrawCanvas(args.object.canvas);
            }
                
            var marginLeft   = args.object.marginLeft;
            var marginRight  = args.object.marginRight;
            var marginTop    = args.object.marginTop;
            var marginBottom = args.object.marginBottom;
            var stretch      = properties.backgroundImageStretch;
            var align        = properties.backgroundImageAlign;
    
            // Handle backgroundImage.align
            if (typeof align === 'string') {
                if (align.indexOf('right') != -1) {
                    var x = args.object.canvas.width - (properties.backgroundImageW || img.width) - marginRight;
                } else {
                    var x = marginLeft;
                }
    
                if (align.indexOf('bottom') != -1) {
                    var y = args.object.canvas.height - (properties.backgroundImageH || img.height) - marginBottom;
                } else {
                    var y = marginTop;
                }
            } else {
                var x = marginLeft || 25;
                var y = marginTop || 25;
            }

            // X/Y coords take precedence over the align
            var x = typeof properties.backgroundImageX === 'number' ? properties.backgroundImageX : x;
            var y = typeof properties.backgroundImageY === 'number' ? properties.backgroundImageY : y;
            var w = stretch ? args.object.canvas.width - marginLeft - marginRight : img.width;
            var h = stretch ? args.object.canvas.height - marginTop - marginBottom : img.height;
            
            //
            // You can now specify the width and height of the image
            //
            if (typeof properties.backgroundImageW === 'number') w  = properties.backgroundImageW;
            if (typeof properties.backgroundImageH === 'number') h = properties.backgroundImageH;

            var oldAlpha = args.object.context.globalAlpha;
                args.object.context.globalAlpha = properties.backgroundImageAlpha;
                args.object.context.drawImage(img,x,y,w, h);
            args.object.context.globalAlpha = oldAlpha;
        }
    };








    //
    // This function determines wshether an object has tooltips or not
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object obj The chart object
    //
    RGraph.hasTooltips = function ()
    {
        var args       = RGraph.getArgs(arguments, 'object');
        var properties = args.object.properties;

        if (typeof properties.tooltips == 'object' && properties.tooltips) {
            for (var i=0,len=properties.tooltips.length; i<len; ++i) {
                if (!RGraph.isNullish(args.object.get('tooltips')[i])) {
                    return true;
                }
            }

        } else if (typeof properties.tooltips === 'string') {
            return true;

        } else if (typeof properties.tooltips === 'function') {
            return true;
        }
        
        return false;
    };








    //
    // This function creates a (G)UID which can be used to identify objects.
    // 
    // @return string The (G)UID
    //
    RGraph.createUID = function ()
    {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c)
        {
            var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
            return v.toString(16);
        });
    };








    //
    // This is the object registry, used to facilitate multiple objects per canvas.
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object obj The object to register
    //
    RGraph.ObjectRegistry.add = function ()
    {
        var args = RGraph.getArgs(arguments, 'object');
        var uid  = args.object.uid;
        var id   = args.object.canvas.id;

        //
        // Index the objects by UID
        //
        //RGraph.ObjectRegistry.objects.byUID.push([uid, obj]);
        RGraph.ObjectRegistry.objects.byUID[RGraph.ObjectRegistry.objects.byUID.length] = [uid, args.object];
        
        //
        // Index the objects by the canvas that they're drawn on
        //
        RGraph.ObjectRegistry.objects.byCanvasID.push([
            id,
            args.object
        ]);
    };








    //
    // Remove an object from the object registry
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object obj The object to remove.
    //
    RGraph.ObjectRegistry.remove = function ()
    {
        var args = RGraph.getArgs(arguments, 'object');
        var id  = args.object.id;
        var uid = args.object.uid;

        for (var i=0; i<RGraph.ObjectRegistry.objects.byUID.length; ++i) {
            if (RGraph.ObjectRegistry.objects.byUID[i] && RGraph.ObjectRegistry.objects.byUID[i][1].uid == uid) {
                RGraph.ObjectRegistry.objects.byUID[i] = null;
            }
        }


        for (var i=0; i<RGraph.ObjectRegistry.objects.byCanvasID.length; ++i) {
            if (   RGraph.ObjectRegistry.objects.byCanvasID[i]
                && RGraph.ObjectRegistry.objects.byCanvasID[i][1]
                && RGraph.ObjectRegistry.objects.byCanvasID[i][1].uid == uid) {
                
                RGraph.ObjectRegistry.objects.byCanvasID[i] = null;
            }
        }
    };








    //
    // Removes all objects from the ObjectRegistry. If either the ID of a canvas is supplied,
    // or the canvas itself, then only objects pertaining to that canvas are cleared.
    // 
    // @param  args object An object consisting of:
    //                      o object or ID or canvas
    // OR
    //
    // @param mixed   Either a canvas object (as returned by document.getElementById()
    //                or the ID of a canvas (ie a string)
    //
    RGraph.ObjectRegistry.clear = function ()
    {
        if (   typeof arguments[0] === 'object'
            && !RGraph.isNullish(arguments)
            && (typeof arguments[0].canvas === 'object' || typeof arguments[0].id === 'string')
           ) {

            var args = arguments[0];

        } else {
        
            // A string has been given
            if (typeof arguments[0] === 'string') {
                var args = {
                    id: arguments[0]
                };
            
            // A canvas has been given
            } else if (typeof arguments[0] === 'object' && arguments[0].getContext) {
                var args = {
                    canvas: arguments[0]
                };

            // No args given - so cear the entire registry
            } else {

                RGraph.ObjectRegistry.objects            = {};
                RGraph.ObjectRegistry.objects.byUID      = [];
                RGraph.ObjectRegistry.objects.byCanvasID = [];
                
                return;
            }
        }

        // If an ID is supplied restrict the learing to that
        if (args.id || args.canvas) { // A string

            // Get the ID of the canvas if a canvas tag has been given
            if (args.canvas) {
                args.id = args.canvas.id;
            }

            var objects = RGraph.ObjectRegistry.getObjectsByCanvasID(args.id);

            for (var i=0,len=objects.length; i<len; ++i) {
                RGraph.ObjectRegistry.remove(objects[i]);
            }

        // Clear the entire ObjectRegistry by resetting
        // the object stores.
        } else {

            RGraph.ObjectRegistry.objects            = {};
            RGraph.ObjectRegistry.objects.byUID      = [];
            RGraph.ObjectRegistry.objects.byCanvasID = [];
        }
    };








    //
    // Lists all objects in the ObjectRegistry
    // 
    // @param  args object An object consisting of:
    //                      o alert
    // OR
    //
    // @param boolean alert Whether to return the list or alert() it
    //
    RGraph.ObjectRegistry.list = function ()
    {
        var args = RGraph.getArgs(arguments, 'alert');
        var list = [];

        for (var i=0,len=RGraph.ObjectRegistry.objects.byUID.length; i<len; ++i) {
            if (RGraph.ObjectRegistry.objects.byUID[i]) {
                list.push(RGraph.ObjectRegistry.objects.byUID[i][1].type);
            }
        }

        if (args.alert) {
            $p(list);
        } else {
            return list;
        }
    };








    //
    // Clears the ObjectRegistry of objects that are of a certain given type
    // 
    // @param  args object An object consisting of:
    //                      o type
    // OR
    //
    // @param type string The type to clear
    //
    RGraph.ObjectRegistry.clearByType = function ()
    {
        var args = RGraph.getArgs(arguments, 'type');
        var objects = RGraph.ObjectRegistry.objects.byUID;

        for (var i=0,len=objects.length; i<len; ++i) {
            if (objects[i]) {
                
                var uid = objects[i][0],
                    obj = objects[i][1];
                
                if (obj && obj.type === args.type) {
                    RGraph.ObjectRegistry.remove(obj);
                }
            }
        }
    };








    //
    // This function provides an easy way to go through all of the objects that are held in the
    // Registry
    // 
    // @param  args object An object consisting of:
    //                      o type
    // OR
    //
    // @param func function This function is run for every object. Its passed the object as an argument
    // @param string type Optionally, you can pass a type of object to look for
    //
    RGraph.ObjectRegistry.iterate = function ()
    {
        var args    = RGraph.getArgs(arguments, 'func,type');
        var objects = RGraph.ObjectRegistry.objects.byUID;

        for (var i=0,len=objects.length; i<len; ++i) {
        
            if (typeof args.type === 'string') {
                
                var types = args.type.split(/,/);

                for (var j=0,len2=types.length; j<len2; ++j) {
                    if (types[j] == objects[i][1].type) {
                        args.func(objects[i][1]);
                    }
                }
            } else {
                args.func(objects[i][1]);
            }
        }
    };








    //
    // Retrieves all objects for a given canvas id
    // 
    // @param  args object An object consisting of:
    //                      o id
    // OR
    //
    // @patarm id string The canvas ID to get objects for.
    //
    RGraph.ObjectRegistry.getObjectsByCanvasID = function ()
    {
        var args  = RGraph.getArgs(arguments, 'id');
        var store = RGraph.ObjectRegistry.objects.byCanvasID;
        var ret   = [];

        // Loop through all of the objects and return the appropriate ones
        for (var i=0,len=store.length; i<len; ++i) {
            if (store[i] && store[i][0] == args.id) {
                ret.push(store[i][1]);
            }
        }

        return ret;
    };








    //
    // Retrieves the relevant object based on the X/Y position.
    // 
    // @param  args object An object consisting of:
    //                      o event
    // OR
    //
    // @param  object e The event object
    // @return object   The applicable (if any) object
    //
    RGraph.ObjectRegistry.firstbyxy =
    RGraph.ObjectRegistry.getObjectByXY =
    RGraph.ObjectRegistry.getFirstObjectByXY = function ()
    {
        var args    = RGraph.getArgs(arguments, 'event');
        var canvas  = args.event.target;
        var ret     = null;
        var objects = RGraph.ObjectRegistry.getObjectsByCanvasID(canvas.id);

        for (var i=(objects.length - 1); i>=0; --i) {

            var obj = objects[i].getObjectByXY(args.event);

            if (obj) {
                return obj;
            }
        }
    };








    //
    // Retrieves the relevant objects based on the X/Y position.
    // NOTE This function returns an array of objects
    // 
    // @param  args object An object consisting of:
    //                      o event
    // OR
    //
    // @param  object e The event object
    // @return          An array of pertinent objects. Note the there may be only one object
    //
    RGraph.ObjectRegistry.getObjectsByXY = function ()
    {
        var args    = RGraph.getArgs(arguments, 'event');
        var canvas  = args.event.target,
            ret     = [],
            objects = RGraph.ObjectRegistry.getObjectsByCanvasID(canvas.id);

        // Retrieve objects "front to back"
        for (var i=(objects.length - 1); i>=0; --i) {

            var obj = objects[i].getObjectByXY(args.event);

            if (obj) {
                ret.push(obj);
            }
        }
        
        return ret;
    };








    //
    // Retrieves the object with the corresponding UID
    // 
    // @param  args object An object consisting of:
    //                      o uid
    // OR
    //
    // @param string uid The UID to get the relevant object for
    //
    RGraph.ObjectRegistry.get =
    RGraph.ObjectRegistry.getObjectByUID = function ()
    {
        var args    = RGraph.getArgs(arguments, 'uid');
        var objects = RGraph.ObjectRegistry.objects.byUID;

        for (var i=0,len=objects.length; i<len; ++i) {
            if (objects[i] && objects[i][1].uid == args.uid) {
                return objects[i][1];
            }
        }
    };








    //
    // Brings a chart to the front of the ObjectRegistry by
    // removing it and then readding it at the end and then
    // redrawing the canvas
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o redraw
    // OR
    //
    // @param object  obj    The object to bring to the front
    // @param boolean redraw Whether to redraw the canvas after the 
    //                       object has been moved
    //
    RGraph.ObjectRegistry.bringToFront = function ()
    {
        var args = RGraph.getArgs(arguments, 'object,redraw');
        var redraw = args.redraw ? true : args.redraw;

        RGraph.ObjectRegistry.remove(args.object);
        RGraph.ObjectRegistry.add(args.object);
        
        if (redraw) {
            RGraph.redrawCanvas(args.object.canvas);
        }
    };








    //
    // Retrieves the objects that are the given type
    // 
    // @param  args object An object consisting of:
    //                      o type
    // OR
    //
    // @param  mixed canvas  The canvas to check. It can either be the canvas object itself or just the ID
    // @param  string type   The type to look for
    // @return array         An array of one or more objects
    //
    RGraph.ObjectRegistry.type =
    RGraph.ObjectRegistry.getObjectsByType = function ()
    {
        var args = RGraph.getArgs(arguments, 'type');

        var objects = RGraph.ObjectRegistry.objects.byUID;
        var ret     = [];

        for (i in objects) {
            if (objects[i] && objects[i][1] && objects[i][1].type && objects[i][1].type && objects[i][1].type == args.type) {
                ret.push(objects[i][1]);
            }
        }

        return ret;
    };








    //
    // Retrieves the FIRST object that matches the given type
    //
    // @param  args object An object consisting of:
    //                      o type
    // OR
    //
    // @param  string type   The type of object to look for
    // @return object        The FIRST object that matches the given type
    //
    RGraph.ObjectRegistry.first =
    RGraph.ObjectRegistry.getFirstObjectByType = function ()
    {
        var args    = RGraph.getArgs(arguments, 'type');
        var objects = RGraph.ObjectRegistry.objects.byUID;

        for (var i in objects) {
            if (objects[i] && objects[i][1] && objects[i][1].type == args.type) {
                return objects[i][1];
            }
        }
        
        return null;
    };








    //
    // This takes centerx, centery, x and y coordinates and returns the
    // appropriate angle relative to the canvas angle system. Remember
    // that the canvas angle system starts at the EAST axis
    // 
    // @param  args object An object consisting of:
    //                      o cx
    //                      o cy
    //                      o x
    //                      o y
    // OR
    //
    // @param  number cx  The centerx coordinate
    // @param  number cy  The centery coordinate
    // @param  number x   The X coordinate (eg the mouseX if coming from a click)
    // @param  number y   The Y coordinate (eg the mouseY if coming from a click)
    // @return number     The relevant angle (measured in in RADIANS)
    //
    RGraph.getAngleByXY = function ()
    {
        var args = RGraph.getArgs(arguments, 'cx,cy,x,y');

        var angle = Math.atan((args.y - args.cy) / (args.x - args.cx));
            angle = Math.abs(angle)

        if (args.x >= args.cx && args.y >= args.cy) {
            angle += RGraph.TWOPI;

        } else if (args.x >= args.cx && args.y < args.cy) {
            angle = (RGraph.HALFPI - angle) + (RGraph.PI + RGraph.HALFPI);

        } else if (args.x < args.cx && args.y < args.cy) {
            angle += RGraph.PI;

        } else {
            angle = RGraph.PI - angle;
        }

        //
        // Upper and lower limit checking
        //
        if (angle > RGraph.TWOPI) {
            angle -= RGraph.TWOPI;
        }

        return angle;
    };








    //
    // This function returns the distance between two points. In effect the
    // radius of an imaginary circle that is centered on x1 and y1. The name
    // of this function is derived from the word "Hypoteneuse", which in
    // trigonmetry is the longest side of a triangle
    // 
    // @param  args object An object consisting of:
    //                      o x1
    //                      o y1
    //                      o x2
    //                      o y2
    // OR
    //
    // @param number x1 The original X coordinate
    // @param number y1 The original Y coordinate
    // @param number x2 The target X coordinate
    // @param number y2 The target Y  coordinate
    //
    RGraph.getHypLength = function ()
    {
        var args = RGraph.getArgs(arguments, 'x1,y1,x2,y2');

        return Math.sqrt(((args.x2 - args.x1) * (args.x2 - args.x1)) + ((args.y2 - args.y1) * (args.y2 - args.y1)));
    };








    //
    // This function gets the end point (X/Y coordinates) of a given radius.
    // You pass it the center X/Y, the angle and the radius and this function
    // will return the endpoint X/Y coordinates.
    // 
    // @param  args object An object consisting of:
    //                      o cx
    //                      o cy
    //                      o angle
    //                      o radius
    // OR
    //
    // @param number cx     The center X coord
    // @param number cy     The center Y coord
    // @param number angle  The angle that the "line" is at from the cx/cy coords
    // @param number radius The length of the radius
    //
    //
    RGraph.getRadiusEndPoint = function ()
    {
        var args = RGraph.getArgs(arguments, 'cx,cy,angle,radius');

        var x    = args.cx + (Math.cos(args.angle) * args.radius);
        var y    = args.cy + (Math.sin(args.angle) * args.radius);

        return [x, y];
    };








    //
    // This installs all of the event listeners
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object object The chart object
    //
    RGraph.installEventListeners = function ()
    {
        var args       = RGraph.getArgs(arguments, 'object');
        var properties = args.object.properties;

        //
        // If this function exists, then the dynamic file has been included.
        //
        if (RGraph.installCanvasClickListener) {

            RGraph.installWindowMousedownListener(args.object);
            RGraph.installWindowMouseupListener(args.object);
            RGraph.installCanvasMousemoveListener(args.object);
            RGraph.installCanvasMouseupListener(args.object);
            RGraph.installCanvasMousedownListener(args.object);
            RGraph.installCanvasClickListener(args.object);
        
        } else if (   RGraph.hasTooltips(args.object)
                   || properties.adjustable
                   || properties.annotatable
                   || properties.contextmenu
                   || properties.keyInteractive
                   || typeof args.object.onclick     === 'function'
                   || typeof args.object.onmousemove === 'function'
                   || typeof args.object.onmouseout  === 'function'
                   || typeof args.object.onmouseover === 'function'
                  ) {

            alert('[RGRAPH] You appear to have used dynamic features but not included the file: RGraph.common.dynamic.js');
        }
    };








    //
    // Loosly mimicks the PHP function print_r();
    //
    // @param  args object An object consisting of:
    //                      o object - You only really need to give this argument
    //                      o alert
    //                      o indent
    //                      o count
    //
    RGraph.pr = function (obj)
    {
        var args = RGraph.getArgs(arguments, 'object,alert,indent,counter');

        var indent = (args.indent ? args.indent : '    ');
        var str    = '';

        var counter = typeof args.counter == 'number' ? args.counter : 0;
        
        if (counter >= 3) {
            return '';
        }

        switch (typeof obj) {
            
            case 'string':    str += args.object + ' (' + (typeof args.object) + ', ' + args.object.length + ')'; break;
            case 'number':    str += args.object + ' (' + (typeof args.object) + ')'; break;
            case 'boolean':   str += args.object + ' (' + (typeof args.object) + ')'; break;
            case 'function':  str += 'function () {}'; break;
            case 'undefined': str += 'undefined'; break;
            case 'null':      str += 'null'; break;
            
            case 'object':
                // In case of null
                if (RGraph.isNullish(args.object)) {
                    str += 'null';
                } else {
                    str += 'Object {' + '\n'
                    for (var j in obj) {
                        str += indent + '    ' + j + ' => ' + RGraph.pr(args.object[j], true, indent + '    ', counter + 1) + '\n';
                    }
                    str += indent + '}';
                }
                break;
            
            
            default:
                str += 'Unknown type: ' + typeof args.object + '';
                break;
        }


        //
        // Finished, now either return if we're in a recursed call, or alert()
        // if we're not.
        //
        if (!args.alert) {
            alert(str);
        }
        
        return str;
    };








    //
    // Produces a dashed line
    // 
    // @param  args object An object consisting of:
    //                      o context
    //                      o x1
    //                      o y1
    //                      o x2
    //                      o y2
    //                      o size
    // OR
    //
    // @param object context The 2D context
    // @param number x1 The start X coordinate
    // @param number y1 The start Y coordinate
    // @param number x2 The end X coordinate
    // @param number y2 The end Y coordinate
    // @param number    The size of the dashes
    //
    RGraph.dashedLine = function()
    {
        var args  = RGraph.getArgs(arguments, 'context,x1,y1,x2,y2,size'),
            dx    = args.x2 - args.x1,
            dy    = args.y2 - args.y1,
            num   = Math.floor(Math.sqrt((dx * dx) + (dy * dy)) / (args.size || 3)),
            xLen  = dx / num,
            yLen  = dy / num,
            count = 0;

        do {
            if (count % 2 == 0 && count > 0) {
                args.context.lineTo(args.x1, args.y1);
            } else {
                args.context.moveTo(args.x1, args.y1);
            }

            args.x1 += xLen;
            args.y1 += yLen;
            
            count++;
            
        } while(count <= num);
    };








    //
    // Makes an AJAX call. It calls the given callback (a function) when ready
    // 
    // @param  args object An object consisting of:
    //                      o url
    //                      o callback
    // OR
    //
    // @param string   url      The URL to retrieve
    // @param function callback A function that is called when the response is ready,
    //                          there's an example below called "myCallback".
    //
    RGraph.AJAX = function ()
    {
        var args = RGraph.getArgs(arguments, 'url,callback');

        // Mozilla, Safari, ...
        if (window.XMLHttpRequest) {
            var httpRequest = new XMLHttpRequest();
            

        // MSIE
        } else if (window.ActiveXObject) {
            var httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
        }

        httpRequest.onreadystatechange = function ()
        {
            if (this.readyState == 4 && this.status == 200) {
                this.__user_callback__ = args.callback;
                this.__user_callback__(this.responseText);
            }
        }

        httpRequest.open('GET', args.url, true);
        
        // Set a Cache-Control header
        if (httpRequest && httpRequest.setRequestHeader) {
            httpRequest.setRequestHeader('Cache-Control', 'no-cache');
        }
        
        httpRequest.send();
    };








    //
    // Makes an AJAX POST request. It calls the given callback (a function) when ready
    // 
    // @param  args object An object consisting of:
    //                      o url
    //                      o data
    //                      o callback
    // OR
    //
    // @param string   url      The URL to retrieve
    // @param object   data     The POST data
    // @param function callback A function that is called when the response is ready, there's an example below
    //                          called "myCallback".
    //
    // DO NOT REMOVE THIS ALIAS!
    RGraph.AJAX.post =
    RGraph.AJAX.POST = function ()
    {
        var args = RGraph.getArgs(arguments, 'url,data,callback');

        // Used when building the POST string
        var crumbs = [];

        // Mozilla, Safari, ...
        if (window.XMLHttpRequest) {
            var httpRequest = new XMLHttpRequest();

        // MSIE
        } else if (window.ActiveXObject) {
            var httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
        }

        httpRequest.onreadystatechange = function ()
        {
            if (this.readyState == 4 && this.status == 200) {
                this.__user_callback__ = args.callback;
                this.__user_callback__(this.responseText);
            }
        }

        httpRequest.open('POST', args.url, true);
        httpRequest.setRequestHeader("Content-type","application/x-www-form-urlencoded");
        
        for (i in args.data) {
            if (typeof i == 'string') {
                crumbs.push(i + '=' + encodeURIComponent(args.data[i]));
            }
        }

        httpRequest.send(crumbs.join('&'));
    };








    //
    // Uses the above function but calls the call back passing a number as its argument
    // 
    // @param  args object An object consisting of:
    //                      o url
    //                      o callback
    // OR
    //
    // @param url string The URL to fetch
    // @param callback function Your callback function (which is passed the number as an argument)
    //
    RGraph.AJAX.number =
    RGraph.AJAX.getNumber = function ()
    {
        var args = RGraph.getArgs(arguments, 'url,callback');

        RGraph.AJAX(args.url, function ()
        {
            var num = parseFloat(this.responseText);

            args.callback(num);
        });
    };








    //
    // Uses the above function but calls the call back passing a string as its argument
    // 
    // @param  args object An object consisting of:
    //                      o url
    //                      o callback
    // OR
    //
    // @param url string The URL to fetch
    // @param callback function Your callback function (which is passed the string as an argument)
    //
    RGraph.AJAX.string =
    RGraph.AJAX.getString = function ()
    {
        var args = RGraph.getArgs(arguments, 'url,callback');

        RGraph.AJAX(args.url, function ()
        {
            var str = String(this.responseText);

            args.callback(str);
        });
    };








    //
    // Uses the above function but calls the call back passing JSON (ie a JavaScript object ) as its argument
    // 
    // @param  args object An object consisting of:
    //                      o url
    //                      o callback
    // OR
    //
    // @param url string The URL to fetch
    // @param callback function Your callback function (which is passed the JSON object as an argument)
    //
    RGraph.AJAX.json =
    RGraph.AJAX.JSON =
    RGraph.AJAX.getJSON = function ()
    {
        var args = RGraph.getArgs(arguments, 'url,callback');

        RGraph.AJAX(args.url, function ()
        {
            var json = eval('(' + this.responseText + ')');

            args.callback(json);
        });
    };








    //
    // Uses the above RGraph.AJAX function but calls the call back passing an array as its argument.
    // Useful if you're retrieving CSV data
    // 
    // @param  args object An object consisting of:
    //                      o url
    //                      o callback
    //                      o sparator (optional)
    // OR
    //
    // @param url      string   The URL to fetch
    // @param callback function Your callback function (which is passed the CSV/array as an argument)
    // @param          string   An OPTIONAL separator character
    //
    RGraph.AJAX.csv =
    RGraph.AJAX.CSV =
    RGraph.AJAX.getCSV = function ()
    {
        var args      = RGraph.getArgs(arguments, 'url,callback,separator');
        var separator = args.separator ? args.separator : ',';

        RGraph.AJAX(args.url, function ()
        {
            var regexp = new RegExp(separator);
            var arr = this.responseText.split(regexp);
            
            // Convert the strings to numbers
            for (var i=0,len=arr.length;i<len;++i) {
                arr[i] = parseFloat(arr[i]);
            }

            args.callback(arr);
        });
    };








    //
    // Rotates the canvas
    // 
    // @param  args object An object consisting of:
    //                      o canvas
    //                      o x
    //                      o y
    //                      o angle (measured in radians)
    // OR
    //
    // @param object canvas The canvas to rotate
    // @param  int   x      The X coordinate about which to rotate the canvas
    // @param  int   y      The Y coordinate about which to rotate the canvas
    // @param  int   angle  The angle(in RADIANS) to rotate the canvas by
    //
    RGraph.rotateCanvas = function ()
    {
        var args    = RGraph.getArgs(arguments, 'canvas,x,y,angle');
        var context = args.canvas.getContext('2d');

        context.translate(args.x,args.y);
        context.rotate(args.angle);
        context.translate(0 - args.x, 0 - args.y);    
    };








    //
    // Measures text by creating a DIV in the document and adding the relevant text to it.
    // Then checking the .offsetWidth and .offsetHeight.
    // 
    // @param  args object An object consisting of:
    //                      o text
    //                      o bold
    //                      o font
    //                      o size
    // OR
    //
    // @param  string text   The text to measure
    // @param  bool   bold   Whether the text is bold or not
    // @param  string font   The font to use
    // @param  size   number The size of the text (in pts)
    // @return array         A two element array of the width and height of the text
    //
    RGraph.measureText = function ()
    {
        var args = RGraph.getArgs(arguments, 'text,bold,font,size');

        // Add the sizes to the cache as adding DOM elements is costly and causes slow downs
        if (typeof RGraph.measuretext_cache === 'undefined') {
            RGraph.measuretext_cache = [];
        }

        var str = args.text + ':' + args.bold + ':' + args.font + ':' + args.size;

        if (typeof RGraph.measuretext_cache == 'object' && RGraph.measuretext_cache[str]) {
            return RGraph.measuretext_cache[str];
        }
        
        if (!RGraph.measuretext_cache['text-div']) {
            var div = document.createElement('DIV');
                div.style.position = 'absolute';
                div.style.top = '-100px';
                div.style.left = '-100px';
                div.style.lineHeight = (args.size || 12) * 1.5 + 'px';
            document.body.appendChild(div);

            // Now store the newly created DIV
            RGraph.measuretext_cache['text-div'] = div;

        } else if (RGraph.measuretext_cache['text-div']) {
            var div = RGraph.measuretext_cache['text-div'];
        
            // Clear the contents of the DIV tag
            while(div.firstChild){
                div.removeChild(div.firstChild);
            }
        }

        div.insertAdjacentHTML('afterbegin', String(args.text).replace(/\r?\n/g, '<br />'));
        div.style.fontFamily = args.font;
        div.style.fontWeight = args.bold ? 'bold' : 'normal';
        div.style.fontSize   = (args.size || 12) + 'pt';

        //document.body.removeChild(div);
        RGraph.measuretext_cache[str] = [div.offsetWidth, div.offsetHeight];
        
        return [div.offsetWidth, div.offsetHeight];
    };








    //
    // New text function. Accepts two arguments:
    //  o opt - An object/hash/map of properties. This can consist of:
    //          object           The RGraph chart object (THIS OR BELOW OPTION IS REQUIRED)
    //          context          The canvases context. This can be given in
    //                           place of the above      (THIS OR ABOVE OPTION IS REQUIRED)
    //          x                The X coordinate        (REQUIRED)
    //          y                The Y coordinate        (REQUIRED)
    //          text             The text to show        (REQUIRED)
    //          font             The font to use
    //          size             The size of the text (in pt)
    //          italic           Whether the text should be italic or not
    //          bold             Whether the text should be bold or not
    //          marker           Whether to show a marker that indicates the X/Y coordinates
    //          valign           The vertical alignment
    //          halign           The horizontal alignment
    //          bounding         Whether to draw a bounding box for the text
    //          boundingStroke   The strokeStyle of the bounding box
    //          boundingFill     The fillStyle of the bounding box
    //          accessible       If false this will cause the text to be
    //                           rendered as native canvas text. DOM text otherwise
    //
    RGraph.text = function (args)
    {

        // Allow for the use of a single argument or two
        // 1. First handle two arguments
        if (arguments[0] && arguments[1] && (typeof arguments[1].text === 'string' || typeof arguments[1].text === 'number' ) ) {
            var obj  = arguments[0],
                args = arguments[1];

        // 2. The alternative is a single option
       } else {

            var obj = args.object;

            //
            // The text arg must be a string or a number
            //
            if (typeof args.text === 'number') {
                args.text = String(args.text);
            }
        }
        
        // Get the defaults for the text function from RGraph.text.defaults object
        for (var i in RGraph.text.defaults) {
            if (typeof i === 'string' && typeof args[i] === 'undefined') {
                args[i] = RGraph.text.defaults[i];
            }
        }

        // Use DOM nodes to get better quality text. This option is BETA quality
        // code and most likely and will not work if you use 3D or if you use
        // your own transformations.
        function domtext ()
        {
            //
            // Check the font property to see if it contains the italic keyword,
            // and if it does then take it out and set the italic property
            //
            if (String(args.size).toLowerCase().indexOf('italic') !== -1) {
                args.size = args.size.replace(/ *italic +/, '');
                args.italic = true;
            }



            // Used for caching the DOM node
            var cacheKey = Math.abs(parseInt(args.x)) + '_' + Math.abs(parseInt(args.y)) + '_' + String(args.text).replace(/[^a-zA-Z0-9]+/g, '_') + '_' + obj.canvas.id;



            // Wrap the canvas in a DIV
            if (!obj.canvas.rgraph_domtext_wrapper) {

                var wrapper = document.createElement('div');
                    wrapper.id        = obj.canvas.id + '_rgraph_domtext_wrapper';
                    wrapper.className = 'rgraph_domtext_wrapper';

                    // The wrapper can be configured to hide or show the
                    // overflow with the textAccessibleOverflow option
                    wrapper.style.overflow = obj.properties.textAccessibleOverflow != false && obj.properties.textAccessibleOverflow != 'hidden' ? 'visible' : 'hidden';
                    
                    wrapper.style.width    = obj.canvas.offsetWidth + 'px';
                    wrapper.style.height   = obj.canvas.offsetHeight + 'px';

                    wrapper.style.cssFloat   = obj.canvas.style.cssFloat;
                    wrapper.style.display    = obj.canvas.style.display || 'inline-block';
                    wrapper.style.position   = obj.canvas.style.position || 'relative';
                    wrapper.style.left       = obj.canvas.style.left;
                    wrapper.style.right      = obj.canvas.style.right;
                    wrapper.style.top        = obj.canvas.style.top;
                    wrapper.style.bottom     = obj.canvas.style.bottom;
                    wrapper.style.width      = obj.canvas.width + 'px';
                    wrapper.style.height     = obj.canvas.height + 'px';
                    wrapper.style.lineHeight = 'initial';

                    obj.canvas.style.position      = 'absolute';
                    obj.canvas.style.left          = 0;
                    obj.canvas.style.top           = 0;
                    obj.canvas.style.display       = 'inline';
                    obj.canvas.style.cssFloat      = 'none';

                    // This now (10/12/2022) skews ro the same
                    // angle as the variantThreedAngle property
                    if ((obj.type === 'bar' || obj.type === 'bipolar' || obj.type === 'hbar') && obj.properties.variant === '3d') {
                        wrapper.style.transform = 'skewY(' + obj.properties.variantThreedAngle + 'rad)';
                    }

                obj.canvas.parentNode.insertBefore(wrapper, obj.canvas);
                
                // Remove the canvas from the DOM and put it in the wrapper
                obj.canvas.parentNode.removeChild(obj.canvas);
                wrapper.appendChild(obj.canvas);
                
                obj.canvas.rgraph_domtext_wrapper = wrapper;
                
                // TODO Add a subwrapper here

            } else {
                wrapper = obj.canvas.rgraph_domtext_wrapper;
            }



            var defaults = {
                size:   12,
                font:   'Arial',
                italic: 'normal',
                bold:   'normal',
                valign: 'bottom',
                halign: 'left',
                marker: true,
                color:  context.fillStyle,
                bounding: {
                    enabled:   false,
                    fill:      'rgba(255,255,255,0.7)',
                    stroke:    '#666',
                    linewidth: 1
                }
            }

            
            // Transform \n to the string [[RETURN]] which is then replaced
            // further down
            args.text = String(args.text).replace(/\r?\n/g, '[[RETURN]]');


            // Create the node cache array that nodes
            // already created are stored in
            if (typeof RGraph.text.domNodeCache === 'undefined') {
                RGraph.text.domNodeCache = new Array();
            }
            
            if (typeof RGraph.text.domNodeCache[obj.id] === 'undefined') {
                RGraph.text.domNodeCache[obj.id] = new Array();
            }

            // Create the dimension cache array that node
            // dimensions are stored in
            if (typeof RGraph.text.domNodeDimensionCache === 'undefined') {
                RGraph.text.domNodeDimensionCache = new Array();
            }
            
            if (typeof RGraph.text.domNodeDimensionCache[obj.id] === 'undefined') {
                RGraph.text.domNodeDimensionCache[obj.id] = new Array();
            }



            // Create the DOM node
            if (!RGraph.text.domNodeCache[obj.id] || !RGraph.text.domNodeCache[obj.id][cacheKey]) {

                var span = document.createElement('span');
                    span.style.position      = 'absolute';
                    span.style.display       = 'inline';
                    
                    span.className        =   ' rgraph_accessible_text'
                                            + ' rgraph_accessible_text_' + obj.id
                                            + ' rgraph_accessible_text_' + (args.tag || '').replace(/\./, '_')
                                            + ' rgraph_accessible_text_' + obj.type
                                            + ' ' + (args.cssClass || '');

                    // This is here to accommodate 3D charts
                    //
                    span.style.left       = (args.x * (parseInt(obj.canvas.offsetWidth) / parseInt(obj.canvas.width))) + 'px';
                    span.style.top        = (args.y * (parseInt(obj.canvas.offsetHeight) / parseInt(obj.canvas.height)))  + 'px';
                    
                    // This could be used for none-3d charts
                    //
                    //span.style.left       = args.x + 'px';
                    //span.style.top        = args.y  + 'px';
                    
                    span.style.color      = args.color || defaults.color;
                    span.style.fontFamily = args.font || defaults.font;
                    span.style.fontWeight = args.bold ? 'bold' : defaults.bold;
                    span.style.fontStyle  = args.italic ? 'italic' : defaults.italic;
                    span.style.fontSize   = (args.size || defaults.size) + 'pt'; // Also see line-height setting a few lines down
                    span.style.whiteSpace = 'nowrap';
                    span.style.lineHeight = RGraph.ISIE ? 'normal' : 'initial'; // Also see font-size setting a few lines up
                    span.tag              = args.tag;
                    span.setAttribute('data-tag', args.tag);


                    // CSS angled text. This should be conasidered BETA quality code at the moment,
                    // but it seems to be OK. You may need to use the labelsOffsety when using this
                    // option.
                    if (typeof args.angle === 'number' && args.angle !== 0) {
                    
                        var coords = RGraph.measureText(
                            args.text,
                            args.bold,
                            args.font,
                            args.size
                        );
                    
                        //span.style.left = parseFloat(span.style.left) - coords[0] + 'px';
                        var hOrigin, vOrigin;
                        
                        if (args.halign === 'center') {hOrigin = '50%';}
                        else if (args.halign === 'right') {hOrigin = '100%';}
                        else {hOrigin = '0%';}
                        
                        if (args.valign === 'center') {vOrigin = '50%';}
                        else if (args.valign === 'top') {vOrigin = '0%';}
                        else {vOrigin = '100%';}
                        
                        span.style.transformOrigin = '{1} {2}'.format(
                            hOrigin,
                            vOrigin
                        );
                        
                        span.style.transform       = 'rotate(' + args.angle + 'deg)';
                    }




                    // Shadow
                    span.style.textShadow = '{1}px {2}px {3}px {4}'.format(
                        context.shadowOffsetX,
                        context.shadowOffsetY,
                        context.shadowBlur,
                        context.shadowColor
                    );


                    if (args.bounding) {
                        span.style.border          = '1px solid ' + (args['bounding.stroke'] || defaults.bounding.stroke);
                        span.style.backgroundColor = args['bounding.fill'] || defaults.bounding.fill;
                        span.style.borderWidth     = typeof args['bounding.linewidth'] === 'number' ? args['bounding.linewidth'] : defaults.bounding.linewidth;
                    }
                    // Pointer events
                    if (
                           (typeof obj.properties.textAccessiblePointerevents === 'undefined' || obj.properties.textAccessiblePointerevents)
                        && obj.properties.textAccessiblePointerevents !== 'none'
                       ) {
                        
                        span.style.pointerEvents =  'auto';
                    } else {
                        span.style.pointerEvents =  'none';
                    }

                    span.style.padding = args.bounding ? '2px' : null; // Changed to 2px on 16th January 2019
                    span.__text__      = args.text
                    
                span.insertAdjacentHTML(
                    'afterbegin',
                    args.text.replace('&', '&amp;')
                             .replace('<', '&lt;')
                             .replace('>', '&gt;')
                             .replace(/\[\[RETURN\]\]/g, '<br />')
                );
                //span.innerHTML     = args.text.replace('&', '&amp;')
                //                             .replace('<', '&lt;')
                //                             .replace('>', '&gt;');
                
                // Now replace the string [[RETURN]] with a <br />
                //span.innerHTML = span.innerHTML.replace(/\[\[RETURN\]\]/g, '<br />');

                wrapper.appendChild(span);

                // Alignment defaults
                args.halign = args.halign || 'left';
                args.valign = args.valign || 'bottom';
                
                // Horizontal alignment
                if (args.halign === 'right') {
                    span.style.left      = parseFloat(span.style.left) - span.offsetWidth + 'px';
                    span.style.textAlign = 'right';
                } else if (args.halign === 'center') {
                    span.style.left      = parseFloat(span.style.left) - (span.offsetWidth  / 2) + 'px';
                    span.style.textAlign = 'center';
                }
                
                // Vertical alignment
                if (args.valign === 'top') {
                    // Nothing to do here
                } else if (args.valign === 'center') {
                    span.style.top = parseFloat(span.style.top) - (span.offsetHeight / 2) + 'px';
                } else {
                    span.style.top = parseFloat(span.style.top) - span.offsetHeight + 'px';
                }
                        
                
                var offsetWidth  = parseFloat(span.offsetWidth),
                    offsetHeight = parseFloat(span.offsetHeight),
                    top          = parseFloat(span.style.top),
                    left         = parseFloat(span.style.left);

                RGraph.text.domNodeCache[obj.id][cacheKey] = span;
                RGraph.text.domNodeDimensionCache[obj.id][cacheKey] = {
                      left: left,
                       top: top,
                     width: offsetWidth,
                    height: offsetHeight
                };
                span.id = cacheKey;


            
            } else {
                span = RGraph.text.domNodeCache[obj.id][cacheKey];
                span.style.display = 'inline';
                
                var offsetWidth  = RGraph.text.domNodeDimensionCache[obj.id][cacheKey].width,
                    offsetHeight = RGraph.text.domNodeDimensionCache[obj.id][cacheKey].height,
                    top          = RGraph.text.domNodeDimensionCache[obj.id][cacheKey].top,
                    left         = RGraph.text.domNodeDimensionCache[obj.id][cacheKey].left;
            }


            

            
            
            // If requested, draw a marker to indicate the coords
            if (args.marker) {
                obj.path(
                    'b m % % l % % m % % l % % s',
                    args.x - 5, args.y,
                    args.x + 5, args.y,
                    args.x, args.y - 5,
                    args.x, args.y + 5
                );
            }
            
            //
            // If its a drawing API text object then allow
            // for events and tooltips
            //
            if (obj.type === 'drawing.text') {

                // Mousemove
                //if (obj.properties.eventsMousemove) {
                //    span.addEventListener('mousemove', function (e) {(obj.properties.eventsMousemove)(e, obj);}, false);
                //}
                
                // Click
                //if (obj.properties.eventsClick) {
                //    span.addEventListener('click', function (e) {(obj.properties.eventsClick)(e, obj);}, false);
                //}
                
                // Tooltips
                if (obj.properties.tooltips) {
                    span.addEventListener(
                        obj.properties.tooltipsEvent.indexOf('mousemove') !== -1 ? 'mousemove' : 'click',
                        function (e)
                        {
                            if (   !RGraph.Registry.get('tooltip')
                                || RGraph.Registry.get('tooltip').__index__ !== 0
                                || RGraph.Registry.get('tooltip').__object__.uid != obj.uid
                               ) {
                               
                                RGraph.hideTooltip();
                                RGraph.redraw();
                                RGraph.tooltip(obj, obj.properties.tooltips[0], args.x, args.y, 0, e);
                            }
                        },
                        false
                    );
                }
            }

            // Build the return value
            var ret        = {};
                ret.x      = left;
                ret.y      = top;
                ret.width  = offsetWidth;
                ret.height = offsetHeight;
                ret.object = obj;
                ret.text   = args.text;
                ret.tag    = args.tag;

            
            // The reset() function clears the domNodeCache
            ////
            // @param object OPTIONAL You can pass in the canvas to limit the
            //                        clearing to that canvas.
            RGraph.text.domNodeCache.reset = function ()
            {
                // Limit the clearing to a single canvas tag
                if (arguments[0]) {
                    
                    if (typeof arguments[0] === 'string') {
                        var canvas = document.getElementById(arguments[0])
                    } else {
                        var canvas = arguments[0];
                    }

                    var nodes = RGraph.text.domNodeCache[canvas.id];

                    for (j in nodes) {
                        
                        var node = RGraph.text.domNodeCache[canvas.id][j];
                        
                        if (node && node.parentNode) {
                            node.parentNode.removeChild(node);
                        }
                    }
                    
                    RGraph.text.domNodeCache[canvas.id]          = [];
                    RGraph.text.domNodeDimensionCache[canvas.id] = [];

                // Clear all DOM text from all tags
                } else {
                    for (i in RGraph.text.domNodeCache) {
                        for (j in RGraph.text.domNodeCache[i]) {
                            if (RGraph.text.domNodeCache[i][j] && RGraph.text.domNodeCache[i][j].parentNode) {
                                RGraph.text.domNodeCache[i][j].parentNode.removeChild(RGraph.text.domNodeCache[i][j]);
                            }
                        }
                    }

                    RGraph.text.domNodeCache          = [];
                    RGraph.text.domNodeDimensionCache = [];
                }
            };




            //
            // Helps you get hold of the SPAN tag nodes that hold the text on the chart
            //
            RGraph.text.find = function (args)
            {
                var span, nodes = [];
                
                if (args.object && args.object.isrgraph) {
                    var id = args.object.id;
                } else if (args.id) {
                    var id     = typeof args.id === 'string' ? args.id : args.object.id;
                    args.object = document.getElementById(id).__object__;
                } else {
                    alert('[RGRAPH] You Must give either an object or an ID to the RGraph.text.find() function');
                    return false;
                }

                for (i in RGraph.text.domNodeCache[id]) {
                
                    span = RGraph.text.domNodeCache[id][i];

                    // A full tag is given
                    if (typeof args.tag === 'string' && args.tag === span.tag) {
                        nodes.push(span);
                        continue;
                    }



                    // A regex is given as the tag
                    if (typeof args.tag === 'object' && args.tag.constructor.toString().indexOf('RegExp')) {

                        var regexp = new RegExp(args.tag);

                        if (regexp.test(span.tag)) {
                            nodes.push(span);
                            continue;
                        }
                    }



                    // A full text is given
                    if (typeof args.text === 'string' && args.text === span.__text__) {
                        nodes.push(span);
                        continue;
                    }



                    // Regex for the text is given
                    // A regex is given as the tag
                    if (typeof args.text === 'object' && args.text.constructor.toString().indexOf('RegExp')) {

                        var regexp = new RegExp(args.text);

                        if (regexp.test(span.__text__)) {
                            nodes.push(span);
                            
                        continue;
                        }
                    }
                }
                
                // If a callback has been specified then call it whilst
                // passing it the text
                if (typeof args.callback === 'function') {
                    (args.callback)({nodes: nodes, object:args.object});
                }

                return nodes;
            };




            //
            // Add the SPAN tag to the return value
            //
            ret.node = span;


            //
            // Save and then return the details of the text (but oly
            // if it's an RGraph object that was given)
            //
            if (obj && obj.isrgraph && obj.coordsText) {
                obj.coordsText.push(ret);
            }


            return ret;
        }




        //
        // An RGraph object can be given, or a string or the 2D rendering context
        // The coords are placed on the obj.coordsText variable ONLY if it's an RGraph object. The function
        // still returns the cooords though in all cases.
        //

        if (obj && obj.isrgraph) {
            var obj     = obj;
            var canvas  = obj.canvas;
            var context = obj.context;
        
        } else if (typeof obj == 'string') {
            var canvas  = document.getElementById(obj);
            var context = canvas.getContext('2d');
            var obj     = canvas.__object__;
        
        } else if (typeof obj.getContext === 'function') {
            var canvas  = obj;
            var context = canvas.getContext('2d');
            var obj     = canvas.__object__;
        
        } else if (obj.toString().indexOf('CanvasRenderingContext2D') != -1 || RGraph.ISIE8 && obj.moveTo) {
            var context  = obj;
            var canvas   = obj.canvas;
            var obj      = canvas.__object__;

        // IE7/8
        } else if (RGraph.ISOLD && obj.fillText) {
            var context  = obj;
            var canvas   = obj.canvas;
            var obj      = canvas.__object__;
        }


        //
        // Changed the name of boundingFill/boundingStroke - this allows you to still use those names
        //

        if (typeof args.boundingFill      === 'string') args['bounding.fill']   = args.boundingFill;
        if (typeof args.boundingStroke    === 'string') args['bounding.stroke'] = args.boundingStroke;
        if (typeof args.boundingLinewidth === 'number') args['bounding.linewidth'] = args.boundingLinewidth;



        //
        // If textConfPrefix is set then get the style configuration
        //
        if (typeof args.textConfPrefix === 'string' && args.textConfPrefix.length) {
            var textConf = RGraph.getTextConf({
                object: obj,
                prefix: args.textConfPrefix
            });
            
            args.font   = textConf.font;
            args.size   = textConf.size;
            args.color  = textConf.color;
            args.bold   = textConf.bold;
            args.italic = textConf.italic;
        }


        if (typeof args.accessible === 'undefined') {
            if (obj && obj.properties.textAccessible) {
                return domtext();
            }
        } else if (typeof args.accessible === 'boolean' && args.accessible) {
            return domtext();
        }




        var x              = args.x,
            y              = args.y,
            originalX      = x,
            originalY      = y,
            text           = args.text,
            text_multiline = typeof text === 'string' ? text.split(/\r?\n/g) : '',
            numlines       = text_multiline.length,
            font           = args.font ? args.font : 'Arial',
            size           = args.size ? args.size : 10,
            size_pixels    = size * 1.5,
            bold           = args.bold,
            italic         = args.italic,
            halign         = args.halign ? args.halign : 'left',
            valign         = args.valign ? args.valign : 'bottom',
            tag            = typeof args.tag == 'string' && args.tag.length > 0 ? args.tag : '',
            marker         = args.marker,
            angle          = args.angle || 0;


        //
        // The text arg must be a string or a number
        //
        if (typeof text == 'number') {
            text = String(text);
        }



        var bounding                = args.bounding,
            bounding_stroke         = args['bounding.stroke'] ? args['bounding.stroke'] : 'black',
            bounding_fill           = args['bounding.fill'] ? args['bounding.fill'] : 'rgba(255,255,255,0.7)',
            bounding_shadow         = args['bounding.shadow'],
            bounding_shadow_color   = args['bounding.shadow.color'] || '#ccc',
            bounding_shadow_blur    = args['bounding.shadow.blur'] || 3,
            bounding_shadow_offsetx = args['bounding.shadow.offsetx'] || 3,
            bounding_shadow_offsety = args['bounding.shadow.offsety'] || 3,
            bounding_linewidth      = typeof args['bounding.linewidth'] === 'number' ? args['bounding.linewidth'] : 1;



        //
        // Initialize the return value to an empty object
        //
        var ret = {};
        
        //
        // Color
        //
        if (typeof args.color === 'string') {
            var orig_fillstyle = context.fillStyle;
            context.fillStyle = args.color;
        }




        if (typeof text !== 'string') {
            return;
        }
        
        
        
        //
        // This facilitates vertical text
        //
        if (angle != 0) {
            context.save();
            context.translate(x, y);
            context.rotate((Math.PI / 180) * angle)
            x = 0;
            y = 0;
        }


        
        //
        // Set the font
        //
        context.font = (args.italic ? 'italic ' : '') + (args.bold ? 'bold ' : '') + size + 'pt ' + font;



        //
        // Measure the width/height. This must be done AFTER the font has been set
        //
        var width=0;
        for (var i=0; i<numlines; ++i) {
            width = Math.max(width, context.measureText(text_multiline[i]).width);
        }
        var height = size_pixels * numlines;




        //
        // Accommodate old MSIE 7/8
        //
        //if (document.all && RGraph.ISOLD) {
            //y += 2;
        //}



        //
        // If marker is specified draw a marker at the X/Y coordinates
        //
        if (args.marker) {
            
            var marker_size = 5;
            var strokestyle = context.strokeStyle;
            
            context.beginPath();
                context.strokeStyle = 'red';
                context.lineWidth = 1;
                context.moveTo(x, y - marker_size);
                context.lineTo(x, y + marker_size);
                context.moveTo(x - marker_size, y);
                context.lineTo(x + marker_size, y);
            context.stroke();
            context.strokeStyle = strokestyle;
        }



        //
        // Set the horizontal alignment
        //
        if (halign == 'center') {
            context.textAlign = 'center';
            var boundingX = x - 2 - (width / 2);
        } else if (halign == 'right') {
            context.textAlign = 'right';
            var boundingX = x - 2 - width;
        } else {
            context.textAlign = 'left';
            var boundingX = x - 2;
        }


        //
        // Set the vertical alignment
        //
        if (valign == 'center') {

            context.textBaseline = 'middle';
            // Move the text slightly
            y -= 1;
            
            y -= ((numlines - 1) / 2) * size_pixels;
            var boundingY = y - (size_pixels / 2) - 2;
        
        } else if (valign == 'top') {
            context.textBaseline = 'top';

            var boundingY = y - 2;
        
        } else if (valign == 'alphabetic') {
            context.textBaseline = 'alphabetic';

            var boundingY = y - size_pixels + 5;

        } else {

            context.textBaseline = 'bottom';

            // Move the Y coord if multiline text
            if (numlines > 1) {
                y -= ((numlines - 1) * size_pixels);
            }

            var boundingY = y - size_pixels - 2;
        }
        
        var boundingW = width + 4;
        var boundingH = height + 2;



        //
        // Draw a bounding box if required
        //
        if (bounding) {

            var pre_bounding_linewidth     = context.lineWidth,
                pre_bounding_strokestyle   = context.strokeStyle,
                pre_bounding_fillstyle     = context.fillStyle,
                pre_bounding_shadowcolor   = context.shadowColor,
                pre_bounding_shadowblur    = context.shadowBlur,
                pre_bounding_shadowoffsetx = context.shadowOffsetX,
                pre_bounding_shadowoffsety = context.shadowOffsetY;

            context.lineWidth   = bounding_linewidth ? bounding_linewidth : 0.001;
            context.strokeStyle = bounding_stroke;
            context.fillStyle   = bounding_fill;

            if (bounding_shadow) {
                context.shadowColor   = bounding_shadow_color;
                context.shadowBlur    = bounding_shadow_blur;
                context.shadowOffsetX = bounding_shadow_offsetx;
                context.shadowOffsetY = bounding_shadow_offsety;
            }

            //obj.context.strokeRect(boundingX, boundingY, width + 6, (size_pixels * numlines) + 4);
            //obj.context.fillRect(boundingX, boundingY, width + 6, (size_pixels * numlines) + 4);
            context.fillRect(
                boundingX,
                boundingY,
                boundingW,
                boundingH
            );
            
            context.strokeRect(
                boundingX,
                boundingY,
                boundingW,
                boundingH
            );

            // Reset the linewidth,colors and shadow to it's original setting
            context.lineWidth     = pre_bounding_linewidth;
            context.strokeStyle   = pre_bounding_strokestyle;
            context.fillStyle     = pre_bounding_fillstyle;
            context.shadowColor   = pre_bounding_shadowcolor
            context.shadowBlur    = pre_bounding_shadowblur
            context.shadowOffsetX = pre_bounding_shadowoffsetx
            context.shadowOffsetY = pre_bounding_shadowoffsety
        }

        
        
        //
        // Draw the text
        //
        if (numlines > 1) {
            for (var i=0; i<numlines; ++i) {
                context.fillText(text_multiline[i], x, y + (size_pixels * i));
            }
        } else {
            context.fillText(text, x + 0.5, y + 0.5);
        }
        
        
        
        //
        // If the text is at 90 degrees restore() the canvas - getting rid of the rotation
        // and the translate that we did
        //
        if (angle != 0) {
            if (angle == 90) {
                if (halign == 'left') {
                    if (valign == 'bottom') {boundingX = originalX - 2; boundingY = originalY - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'center') {boundingX = originalX - (height / 2) - 2; boundingY = originalY - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'top')    {boundingX = originalX - height - 2; boundingY = originalY - 2; boundingW = height + 4; boundingH = width + 4;}
                
                } else if (halign == 'center') {
                    if (valign == 'bottom') {boundingX = originalX - 2; boundingY = originalY - (width / 2) - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'center') {boundingX = originalX - (height / 2) -  2; boundingY = originalY - (width / 2) - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'top')    {boundingX = originalX - height -  2; boundingY = originalY - (width / 2) - 2; boundingW = height + 4; boundingH = width + 4;}
                
                } else if (halign == 'right') {
                    if (valign == 'bottom') {boundingX = originalX - 2; boundingY = originalY - width - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'center') {boundingX = originalX - (height / 2) - 2; boundingY = originalY - width - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'top')    {boundingX = originalX - height - 2; boundingY = originalY - width - 2; boundingW = height + 4; boundingH = width + 4;}
                }

            } else if (angle == 180) {

                if (halign == 'left') {
                    if (valign == 'bottom') {boundingX = originalX - width - 2; boundingY = originalY - 2; boundingW = width + 4; boundingH = height + 4;}
                    if (valign == 'center') {boundingX = originalX - width - 2; boundingY = originalY - (height / 2) - 2; boundingW = width + 4; boundingH = height + 4;}
                    if (valign == 'top')    {boundingX = originalX - width - 2; boundingY = originalY - height - 2; boundingW = width + 4; boundingH = height + 4;}
                
                } else if (halign == 'center') {
                    if (valign == 'bottom') {boundingX = originalX - (width / 2) - 2; boundingY = originalY - 2; boundingW = width + 4; boundingH = height + 4;}
                    if (valign == 'center') {boundingX = originalX - (width / 2) - 2; boundingY = originalY - (height / 2) - 2; boundingW = width + 4; boundingH = height + 4;}
                    if (valign == 'top')    {boundingX = originalX - (width / 2) - 2; boundingY = originalY - height - 2; boundingW = width + 4; boundingH = height + 4;}
                
                } else if (halign == 'right') {
                    if (valign == 'bottom') {boundingX = originalX - 2; boundingY = originalY - 2; boundingW = width + 4; boundingH = height + 4;}
                    if (valign == 'center') {boundingX = originalX - 2; boundingY = originalY - (height / 2) - 2; boundingW = width + 4; boundingH = height + 4;}
                    if (valign == 'top')    {boundingX = originalX - 2; boundingY = originalY - height - 2; boundingW = width + 4; boundingH = height + 4;}
                }
            
            } else if (angle == 270) {

                if (halign == 'left') {
                    if (valign == 'bottom') {boundingX = originalX - height - 2; boundingY = originalY - width - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'center') {boundingX = originalX - (height / 2) - 4; boundingY = originalY - width - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'top')    {boundingX = originalX - 2; boundingY = originalY - width - 2; boundingW = height + 4; boundingH = width + 4;}
                
                } else if (halign == 'center') {
                    if (valign == 'bottom') {boundingX = originalX - height - 2; boundingY = originalY - (width/2) - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'center') {boundingX = originalX - (height/2) - 4; boundingY = originalY - (width/2) - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'top')    {boundingX = originalX - 2; boundingY = originalY - (width/2) - 2; boundingW = height + 4; boundingH = width + 4;}
                
                } else if (halign == 'right') {
                    if (valign == 'bottom') {boundingX = originalX - height - 2; boundingY = originalY - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'center') {boundingX = originalX - (height/2) - 2; boundingY = originalY - 2; boundingW = height + 4; boundingH = width + 4;}
                    if (valign == 'top')    {boundingX = originalX - 2; boundingY = originalY - 2; boundingW = height + 4; boundingH = width + 4;}
                }
            }

            context.restore();
        }




        //
        // Reset the text alignment so that text rendered after this text function is not affected
        //
        context.textBaseline = 'alphabetic';
        context.textAlign    = 'left';





        //
        // Fill the ret variable with details of the text
        //
        ret.x      = boundingX;
        ret.y      = boundingY;
        ret.width  = boundingW;
        ret.height = boundingH
        ret.object = obj;
        ret.text   = text;
        ret.tag    = tag;



        //
        // Save and then return the details of the text (but oly
        // if it's an RGraph object that was given)
        //
        if (obj && obj.isrgraph && obj.coordsText) {
            obj.coordsText.push(ret);
        }
        
        //
        // Restore the original fillstyle
        //
        if (typeof orig_fillstyle === 'string') {
            context.fillStyle = orig_fillstyle;
        }

        return ret;
    };
    
    // Create the DEFAULTS object
    RGraph.text.defaults = {};








    //
    //
    // Adds custom text to the chart based on whats
    // in the objects text: property.
    //
    //@param object obj The chart object
    //
    RGraph.addCustomText = function (obj)
    {
        if (RGraph.isArray(obj.properties.text) && obj.properties.text.length) {
            for (var i=0; i<obj.properties.text.length; ++i) {
                var conf = obj.properties.text[i];
                
                // Add the object to the config
                conf.object = obj;
                
                // Set the color to black if it's not set
                if (typeof conf.color !== 'string' || !conf.color.length) {
                    conf.color = 'black';
                }
                
                RGraph.text(conf);
            }
        }
    };








    //
    // Takes a sequential index abd returns the group/index variation of it. Eg if you have a
    // sequential index from a grouped bar chart this function can be used to convert that into
    // an appropriate group/index combination
    // 
    // @param  args object An object consisting of:
    //                      o index
    //                      o data
    // OR
    //
    // @param index number The sequential index
    // @param data  array  The original data (which is grouped)
    // @return             The group/index information
    //
    RGraph.sequentialIndexToGrouped = function ()
    {
        var args          = RGraph.getArgs(arguments, 'index,data');
        var group         = 0;
        var grouped_index = 0;

        while (--args.index >= 0) {

            if (RGraph.isNullish(args.data[group])) {
                group++;
                grouped_index = 0;
                continue;
            }

            // Allow for numbers as well as arrays in the dataset
            if (typeof args.data[group] == 'number') {
                group++
                grouped_index = 0;
                continue;
            }
            

            grouped_index++;
            
            if (grouped_index >= args.data[group].length) {
                group++;
                grouped_index = 0;
            }
        }
        
        return [group, grouped_index];
    };








    //
    // This is the reverse of the above function - converting
    // group/index to a sequential index
    //
    // @return number The sequential index
    //
    RGraph.groupedIndexToSequential = function ()
    {
        var args = RGraph.getArgs(arguments, 'object,dataset,index');

        for (var i=0,seq=0; i<=args.dataset; ++i) {
            for (var j=0; j<args.object.data[args.dataset].length; ++j) {
                
                if (i === args.dataset && j === args.index) {
                    return seq;
                }
                seq++;
            }
        }
        
        return seq;
    };








    //
    // This function highlights a rectangle
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o shape
    // OR
    //
    // @param object obj    The chart object
    // @param number shape  The coordinates of the rect to highlight
    //
    RGraph.Highlight.rect = function ()
    {
        var args       = RGraph.getArgs(arguments, 'object,shape'),
            properties = args.object.properties;

        if (properties.tooltipsHighlight) {
            
        
            // Safari seems to need this
            args.object.context.lineWidth = 1;


            //
            // Draw a rectangle on the canvas to highlight the appropriate area
            //
            args.object.context.beginPath();

                args.object.context.strokeStyle = properties.highlightStroke;
                args.object.context.fillStyle   = properties.highlightFill;

                args.object.context.rect(
                    args.shape.x - 0.5,
                    args.shape.y - 0.5,
                    args.shape.width + 1,
                    args.shape.height + 1
                );
                //obj.context.fillRect(shape.x,shape.y,shape.width,shape.height);
            args.object.context.stroke();
            args.object.context.fill();
        }
    };








    //
    // This function highlights a point
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o shape
    // OR
    //
    // @param object obj    The chart object
    // @param number shape  The coordinates of the rect to highlight
    //
    RGraph.Highlight.point = function ()
    {
        var args        = RGraph.getArgs(arguments, 'object,shape');
        var properties  = args.object.properties;

        if (properties.tooltipsHighlight) {
    
            //
            // Draw a rectangle on the canvas to highlight the
            // appropriate area
            //
            args.object.context.beginPath();
                args.object.context.strokeStyle = properties.highlightStroke;
                args.object.context.fillStyle   = properties.highlightFill;

                var radius = properties.highlightPointRadius || 2;

                args.object.context.arc(
                    args.shape.x,
                    args.shape.y,
                    radius,
                    0,
                    RGraph.TWOPI,
                    0
                );
            args.object.context.stroke();
            args.object.context.fill();
        }
    };








    //
    // A better, more flexible, date parsing function that
    // was taken from the SVG libraries.
    //
    // @param  args object An object consisting of:
    //                      o str
    // OR
    //
    // @param  string str The string to parse
    // @return number     A number, as returned by Date.parse()
    //
    RGraph.parseDate = function ()
    {
        var args = RGraph.getArgs(arguments, 'str');

        // First off - remove the T from the format: YYYY-MM-DDTHH:MM:SS
        if (args.str.match(/^\d\d\d\d-\d\d-\d\d(t|T)\d\d:\d\d(:\d\d)?$/)) {
            args.str = args.str.toUpperCase().replace(/T/, ' ');
        }


        var d = new Date();

        // Initialise the default values
        var defaults = {
            seconds: '00',
            minutes: '00',
              hours: '00',
               date: d.getDate(),
              month: d.getMonth() + 1,
               year: d.getFullYear()
        };

        // Create the months array for turning textual months back to numbers
        var months       = ['january','february','march','april','may','june','july','august','september','october','november','december'],
            months_regex = months.join('|');

        for (var i=0; i<months.length; ++i) {
            months[months[i]] = i;
            months[months[i].substring(0,3)] = i;
            months_regex = months_regex + '|' + months[i].substring(0,3);
        }

        // These are the separators allowable for d/m/y and y/m/d dates
        // (Its part of a regexp so the position of the square brackets
        //  is crucial)
        var sep = '[-./_=+~#:;,]+';


        // Tokenise the string
        var tokens = args.str.split(/ +/);

        // Loop through each token checking what it is
        for (var i=0,len=tokens.length; i<len; ++i) {
            if (tokens[i]) {
                
                // Year
                if (tokens[i].match(/^\d\d\d\d$/)) {
                    defaults.year = tokens[i];
                }

                // Month
                var res = isMonth(tokens[i]);
                if (typeof res === 'number') {
                    defaults.month = res + 1; // Months are zero indexed
                }

                // Date
                if (tokens[i].match(/^\d?\d(?:st|nd|rd|th)?$/)) {
                    defaults.date = parseInt(tokens[i]);
                }

                // Time
                if (tokens[i].match(/^(\d\d):(\d\d):?(?:(\d\d))?$/)) {
                    defaults.hours   = parseInt(RegExp.$1);
                    defaults.minutes = parseInt(RegExp.$2);
                    
                    if (RegExp.$3) {
                        defaults.seconds = parseInt(RegExp.$3);
                    }
                }

                // Dateformat: XXXX-XX-XX
                if (tokens[i].match(new RegExp('^(\\d\\d\\d\\d)' + sep + '(\\d\\d)' + sep + '(\\d\\d)$', 'i'))) {
                    defaults.date  = parseInt(RegExp.$3);
                    defaults.month = parseInt(RegExp.$2);
                    defaults.year  = parseInt(RegExp.$1);

                }

                // Dateformat: XX-XX-XXXX
                if (tokens[i].match(new RegExp('^(\\d\\d)' + sep + '(\\d\\d)' + sep + '(\\d\\d\\d\\d)$','i') )) {
                    defaults.date  = parseInt(RegExp.$1);
                    defaults.month = parseInt(RegExp.$2);
                    defaults.year  = parseInt(RegExp.$3);
                }
            }
        }

        // Now put the defaults into a format thats recognised by Date.parse()
        args.str = '{1}/{2}/{3} {4}:{5}:{6}'.format(
            defaults.year,
            String(defaults.month).length     === 1 ? '0' + (defaults.month) : defaults.month,
            String(defaults.date).length      === 1 ? '0' + (defaults.date)      : defaults.date,
            String(defaults.hours).length     === 1 ? '0' + (defaults.hours)     : defaults.hours,
            String(defaults.minutes).length   === 1 ? '0' + (defaults.minutes)   : defaults.minutes,
            String(defaults.seconds).length   === 1 ? '0' + (defaults.seconds)   : defaults.seconds
        );

        return Date.parse(args.str);

        //
        // Support functions
        //
        function isMonth(str)
        {
            var res = str.toLowerCase().match(months_regex);

            return res ? months[res[0]] : false;
        }
    };








    //
    // This is the same as Date.parse - though a little more flexible.
    // 
    // @param  args object An object consisting of:
    //                      o str
    // OR
    //
    // @param string str The date string to parse
    // @return Returns the same thing as Date.parse
    //
    RGraph.parseDateOld = function ()
    {
        var args = RGraph.getArgs(arguments, 'str');

        args.str = RGraph.trim(args.str);

        // Allow for: now (just the word "now")
        if (args.str === 'now') {
            args.str = (new Date()).toString();
        }


        // Allow for: 22-11-2013
        // Allow for: 22/11/2013
        // Allow for: 22-11-2013 12:09:09
        // Allow for: 22/11/2013 12:09:09
        if (args.str.match(/^(\d\d)(?:-|\/)(\d\d)(?:-|\/)(\d\d\d\d)(.*)$/)) {
            args.str = '{1}/{2}/{3}{4}'.format(
                RegExp.$3,
                RegExp.$2,
                RegExp.$1,
                RegExp.$4
            );
        }

        // Allow for: 2013-11-22 12:12:12 or  2013/11/22 12:12:12
        if (args.str.match(/^(\d\d\d\d)(-|\/)(\d\d)(-|\/)(\d\d)( |T)(\d\d):(\d\d):(\d\d)$/)) {
            args.str = RegExp.$1 + '-' + RegExp.$3 + '-' + RegExp.$5 + 'T' + RegExp.$7 + ':' + RegExp.$8 + ':' + RegExp.$9;
        }

        // Allow for: 2013-11-22
        if (args.str.match(/^\d\d\d\d-\d\d-\d\d$/)) {
            args.str = args.str.replace(/-/g, '/');
        }


        // Allow for: 12:09:44 (time only using todays date)
        if (args.str.match(/^\d\d:\d\d:\d\d$/)) {
        
            var dateObj  = new Date();
            var date     = dateObj.getDate();
            var month    = dateObj.getMonth() + 1;
            var year     = dateObj.getFullYear();
            
            // Pad the date/month with a zero if it's not two characters
            if (String(month).length === 1) month = '0' + month;
            if (String(date).length === 1) date = '0' + date;

            args.str = (year + '/' + month + '/' + date) + ' ' + args.str;
        }

        return Date.parse(args.str);
    };








    //
    // Reset all of the color values to their original values
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object The chart object
    //
    RGraph.resetColorsToOriginalValues = function ()
    {
        var args = RGraph.getArgs(arguments, 'object');

        if (args.object.original_colors) {
            // Reset the colors to their original values
            for (var j in args.object.original_colors) {
                if (typeof j === 'string') {// TAKEN OUT 1st AUGUST && j.substr(0,6) === 'chart.'
                    args.object.properties[j] = RGraph.arrayClone(args.object.original_colors[j]);
                }
            }
        }



        //
        // If the function is present on the object to reset
        // specific colors - use that
        //
        if (typeof args.object.resetColorsToOriginalValues === 'function') {
            args.object.resetColorsToOriginalValues();
        }

        // Reset the colorsParsed flag so that they're parsed for gradients again
        args.object.colorsParsed = false;
    };








    //
    // Creates a Linear gradient
    // 
    // @param object object An object that can contain these properties
    //                       o object  The RGraph object
    //                       o x1      The starting X coordinate
    //                       o y1      The starting Y coordinate
    //                       o x2      The ending X coordinate
    //                       o y2      The ending Y coordinate
    //                       o colors  An array of colors for the gradient
    RGraph.linearGradient = function ()
    {
        var args = RGraph.getArgs(arguments,'object,x1,y1,x2,y2,colors');

        var gradient = args.object.context.createLinearGradient(
            args.x1,
            args.y1,
            args.x2,
            args.y2
        );
        var numColors = args.colors.length;

        for (var i=0; i<args.colors.length; ++i) {
            
            var color = args.colors[i];
            var stop = i / (numColors - 1);

            gradient.addColorStop(stop, color);
        }

        return gradient;
    };








    //
    // Creates a Radial gradient
    // 
    // @param object object An object that can contain these properties
    //                       o object  The RGraph object
    //                       o x1      The start X coordinate
    //                       o y1      The start Y coordinate
    //                       o r1      The start radius
    //                       o x2      The end X coordinate
    //                       o y2      The end Y coordinate
    //                       o r2      The end radius
    //                       o colors  An array of colors for the gradient
    RGraph.radialGradient = function()
    {
        var args = arguments[0];

        var gradient  = args.object.context.createRadialGradient(
            args.x1,
            args.y1,
            args.r1,
            args.x2,
            args.y2,
            args.r2
        );

        var numColors = args.colors.length;
        
        for(var i=0; i<args.colors.length; ++i) {
            gradient.addColorStop(
                i / (numColors-1),
                args.colors[i]
            );
        }
        
        return gradient;
    };








    //
    // Adds an event listener to RGraphs internal array so that RGraph can track them.
    // This DOESN'T add the event listener to the canvas/window.
    // 
    // 05/01/2014 TODO Used in the tooltips file, but is it necessary any more?
    // 15/10/2019 Commented out
    //
    //RGraph.addEventListener =
    //RGraph.AddEventListener = function (id, e, func)
    //{
    //    var type = arguments[3] ? arguments[3] : 'unknown';
    //    
    //    RGraph.Registry.get('event.handlers').push([id,e,func,type]);
    //};








    //
    // Clears event listeners that have been installed by RGraph
    // 
    // @param  args object An object consisting of:
    //                      o id
    // OR
    //
    // @param string id The ID of the canvas to clear event listeners for - or 'window' to clear
    //                  the event listeners attached to the window
    //
    RGraph.clearEventListeners = function ()
    {
        var args = RGraph.getArgs(arguments, 'id');

        if (args.id && args.id === 'window') {

            window.removeEventListener('mousedown', RGraph.window_mousedown_event_listener, false);
            window.removeEventListener('mouseup', RGraph.window_mouseup_event_listener, false);

        } else {
            
            var canvas = document.getElementById(args.id);

            canvas.removeEventListener('mouseup', canvas.rgraph_mouseup_event_listener, false);
            canvas.removeEventListener('mousemove', canvas.rgraph_mousemove_event_listener, false);
            canvas.removeEventListener('mousedown', canvas.rgraph_mousedown_event_listener, false);
            canvas.removeEventListener('click', canvas.rgraph_click_event_listener, false);
        }
    };







    //
    // Hides the annotating palette. It's here because it can be called
    // from code other than the annotating code.
    //
    // No arguments
    //
    RGraph.hidePalette = function ()
    {
        var div = RGraph.Registry.get('palette');
        
        if(typeof div == 'object' && div) {
            
            div.style.visibility = 'hidden';
            div.style.display = 'none';
            
            RGraph.Registry.set('palette', null);
        }
    };








    //
    // Generates a random number between the minimum and maximum
    // 
    // @param  args object An object consisting of:
    //                      o min
    //                      o max
    //                      o decimals
    // OR
    //
    // @param number min The minimum value
    // @param number max The maximum value
    // @param number     OPTIONAL Number of decimal places
    //
    RGraph.random = function ()
    {
        var args = RGraph.getArgs(arguments, 'min,max,decimals');

        var dp = args.decimals ? args.decimals : 0;
        var r  = Math.random();
        
        return Number((((args.max - args.min) * r) + args.min).toFixed(dp));
    };








    //
    // Returns an array of random values
    //
    // @param  args object An object consisting of:
    //                      o count
    //                      o min
    //                      o max
    //                      o decimals
    // OR
    //
    //
    RGraph.arrayRandom = function ()
    {
        var args = RGraph.getArgs(arguments, 'count,min,max,decimals');

        for(var i=0,arr=[]; i<args.count; i+=1) {
            arr.push(
                RGraph.random(
                    args.min,
                    args.max,
                    args.decimals
                )
            );
        }
        
        return arr;
    };








    //
    // Turns off shadow by setting blur to zero, the offsets to zero and the color to transparent black.
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param object obj The chart object
    //
    RGraph.noShadow = function ()
    {
        var args = RGraph.getArgs(arguments, 'object');

        args.object.context.shadowColor   = 'rgba(0,0,0,0)';
        args.object.context.shadowBlur    = 0;
        args.object.context.shadowOffsetx = 0;
        args.object.context.shadowOffsety = 0;
    };








    //
    // Sets the various shadow properties
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o prefix
    // OR
    // 
    // @param object obj     The chart object (only supplying the RGraph chart
    //                       object turns the shadow off)
    // OR
    //
    // @param object obj     The chart object
    // @param string color   The color of the shadow
    // @param number offsetx The offsetX value for the shadow
    // @param number offsety The offsetY value for the shadow
    // @param number blur    The blurring value for the shadow
    //
    RGraph.setShadow = function ()
    {
        // 1 Argument
        if (   typeof arguments[0] === 'object'
            && typeof arguments[0].object === 'object'
            && typeof arguments[0].object.isrgraph
            && typeof arguments[0].prefix === 'string'
           ) {
           
           var args = arguments[0];

            args.object.context.shadowColor   = args.object.properties[args.prefix + 'Color'];
            args.object.context.shadowOffsetX = args.object.properties[args.prefix + 'Offsetx'];
            args.object.context.shadowOffsetY = args.object.properties[args.prefix + 'Offsety'];
            args.object.context.shadowBlur    = args.object.properties[args.prefix + 'Blur'];

        // Turn Off the shadow
        } else if (   arguments.length === 1
                   && typeof arguments[0] === 'object'
                   && typeof arguments[0].isrgraph) {
            
            var obj = arguments[0];

            obj.context.shadowColor   = 'rgba(0,0,0,0)';
            obj.context.shadowOffsetX = 0;
            obj.context.shadowOffsetY = 0;
            obj.context.shadowBlur    = 0;

        // Separate arguments
        } else {

            var obj = arguments[0];
    
            obj.context.shadowColor   = arguments[1];
            obj.context.shadowOffsetX = arguments[2];
            obj.context.shadowOffsetY = arguments[3];
            obj.context.shadowBlur    = arguments[4];
        }
    };








    //
    // Sets an object in the RGraph registry
    // 
    // @param  args object An object consisting of:
    //                      o name
    //                      o value
    // OR
    //
    // @param string name The name of the value to set
    //
    RGraph.Registry.set = function ()
    {
        var args = RGraph.getArgs(arguments, 'name,value');

        // Convert uppercase letters to dot+lower case letter
        args.name = args.name.replace(/([A-Z])/g, function (str)
        {
            return '.' + String(RegExp.$1).toLowerCase();
        });

        RGraph.Registry.store[args.name] = args.value;
        
        return args.value;
    };








    //
    // Gets an object from the RGraph registry
    // 
    // @param  args object An object consisting of:
    //                      o name
    // OR
    //
    // @param string name The name of the value to fetch
    //
    RGraph.Registry.get = function ()
    {
        var args = RGraph.getArgs(arguments, 'name');

        // Convert uppercase letters to dot+lower case letter
        args.name = args.name.replace(/([A-Z])/g, function (str)
        {
            return '.' + String(RegExp.$1).toLowerCase();
        });


        return RGraph.Registry.store[args.name];
    };








    //
    // Converts the given number of degrees to radians. Angles in canvas are
    // measured in radians. There are a .toDegrees() function and a toRadians()
    // function too.
    // 
    // @param  args object An object consisting of:
    //                      o degrees
    // OR
    //
    // @param number degrees The value to convert
    //
    RGraph.toRadians = function ()
    {
        var args = RGraph.getArgs(arguments, 'degrees');

        return args.degrees * (RGraph.PI / 180);
    };

    // Usage: RGraph.toDegrees(3.14) // 180ish
    //
    // @param  args object An object consisting of:
    //                      o radians
    // OR
    //
    // @param number radians The angle in radians to convert to degrees
    //
    RGraph.toDegrees = function ()
    {
        var args = RGraph.getArgs(arguments, 'radians');

        return args.radians * (180 / Math.PI);
    };








    //
    // Generates logs for... log charts
    // 
    // @param  args object An object consisting of:
    //                      o number
    //                      o base
    // OR
    //
    // @param number n    The number to generate the log for
    // @param number base The base to use
    //
    RGraph.log = function ()
    {
        var args = RGraph.getArgs(arguments, 'number,base');

        return Math.log(args.number) / (args.base ? Math.log(args.base) : 1);
    };








    //
    // Determines if the given object is an array or not
    // 
    // @param  args object An object consisting of:
    //                      o object
    // OR
    //
    // @param mixed obj The variable to test
    //
    RGraph.isArray = function (o)
    {
        if (o && o.constructor) {
            var pos = o.constructor.toString().indexOf('Array');
        } else {
            return false;
        }

        return o != null &&
               typeof pos === 'number' &&
               pos > 0 &&
               pos < 20;
    };








    //
    // Removes white-space from the start aqnd end of a string
    // 
    // @param  args object An object consisting of:
    //                      o str
    // OR
    //
    // @param string str The string to trim
    //
    RGraph.trim = function ()
    {
        var args = RGraph.getArgs(arguments, 'str');

        return RGraph.ltrim(RGraph.rtrim(args.str));
    };








    //
    // Trims the white-space from the start of a string
    // 
    // @param  args object An object consisting of:
    //                      o str
    // OR
    //
    // @param string str The string to trim
    //
    RGraph.ltrim = function ()
    {
        var args = RGraph.getArgs(arguments, 'str');

        return args.str.replace(/^(\s|\0)+/, '');
    };








    //
    // Trims the white-space off of the end of a string
    // 
    // @param  args object An object consisting of:
    //                      o str
    // OR
    //
    // @param string str The string to trim
    //
    RGraph.rtrim = function ()
    {
        var args = RGraph.getArgs(arguments, 'str');

        return args.str.replace(/(\s|\0)+$/, '');
    };








    //
    // Returns true/false as to whether the given variable is
    // null or not.
    // 
    // @param mixed obj The argument to check
    //
    RGraph.isNull = function (obj)
    {
        if (typeof obj === 'object' && !obj) {
            return true;
        }
    
        return false;
    };








    //
    // Returns true/false as to whether the given variable is
    // null or not. This function also returns true if the
    // variable is undefined
    // 
    // @param mixed obj The argument to check
    //
    RGraph.isNullish = function (obj)
    {
        // Check for undefined
        if (RGraph.isUndefined(obj)) {
            return true;
        }

        // Check for null
        if (RGraph.isNull(obj)) {
            return true;
        }
    
        return false;
    };








    //
    // This function facilitates a very limited way of making your charts
    // whilst letting the rest of page continue - using  the setTimeout function
    // 
    // @param  args object An object consisting of:
    //                      o func
    //                      o delay
    // OR
    //
    // @param function func The function to run that creates the chart
    //
    RGraph.async = function ()
    {
        var args = RGraph.getArgs(arguments, 'func,delay');

        return setTimeout(args.func, args.delay ? args.delay : 1);
    };








    //
    // Resets (more than just clears) the canvas and clears any
    // pertinent objects from the ObjectRegistry.
    // 
    // @param  args object An object consisting of:
    //                      o canvas
    // OR
    //
    // @param object canvas The canvas object (as returned by document.getElementById() ).
    //
    RGraph.reset = function ()
    {
        var args = RGraph.getArgs(arguments, 'canvas');

        // If a string has been given then treat it as the ID
        // of the canvas
        if (typeof args.canvas === 'string') {
            args.canvas = document.getElementById(args.canvas);
        }

        args.canvas.width = args.canvas.width;
        
        // Clear the ObjectRegistry
        RGraph.ObjectRegistry.clear(args.canvas);
         
        
        // Get rid of references from the canvas that are added by
        // various RGraph dynamic features
        //
        // Do the back image first
        if (args.canvas.__rgraph_background_image__) {
            delete args.canvas.__rgraph_background_image__.__object__;
            delete args.canvas.__rgraph_background_image__.__canvas__;
            delete args.canvas.__rgraph_background_image__.__context__;
        }

        for (v of ['__object__', '__rgraph_background_image__']) {
            delete args.canvas[v];
        }






        if (RGraph.text.domNodeCache && RGraph.text.domNodeCache.reset) {
            RGraph.text.domNodeCache.reset(args.canvas);
        }

        // Create the node and dimension caches if they don't
        // already exist
        if (!RGraph.text.domNodeCache)          { RGraph.text.domNodeCache          = []; }
        if (!RGraph.text.domNodeDimensionCache) { RGraph.text.domNodeDimensionCache = []; }

        // Create/reset the specific canvas arrays in the caches
        RGraph.text.domNodeCache[args.canvas.id]          = [];
        RGraph.text.domNodeDimensionCache[args.canvas.id] = [];
    };








    //
    // This function is due to be removed.
    //
    // 19/10/2019 Commented out
    // 
    // @param string id The ID of what can be either the canvas tag or a DIV tag
    //
    //RGraph.getCanvasTag = function ()
    //{
    //    var args = RGraph.getArgs(arguments, 'id');
    //
    //    var id = typeof args.id === 'object' ? args.id.id : args.id;
    //    
    //    var canvas = document.getElementById(id);
    //
    //    return [id, canvas];
    //};








    //
    // A wrapper function that encapsulate requestAnimationFrame
    // 
    // @param  args object An object consisting of:
    //                      o func
    // OR
    //
    // @param function func The animation function
    //
    RGraph.Effects.updateCanvas = function ()
    {
        var args = RGraph.getArgs(arguments, 'func');

        window.requestAnimationFrame =    
                window.requestAnimationFrame
            || window.webkitRequestAnimationFrame
            || window.msRequestAnimationFrame
            || window.mozRequestAnimationFrame
            || (function (func){setTimeout(func, 16.666);});
        
        window.requestAnimationFrame(args.func);
    };








    //
    // Checks to see if the user has requested to stop the
    // animation
    //
    // @param obj object The chart object
    //
    RGraph.Effects.userRequestedStop = function (obj)
    {
        var ret = false;

        if (obj.stopAnimationRequested) {

            ret = true;

            // Reset the flag now that its been checked
            obj.stopAnimationRequested = false;
        }

        return ret;
    };








    //
    // This function returns an easing multiplier for effects so they eas out towards the
    // end of the effect.
    // 
    // @param  args object An object consisting of:
    //                      o frames
    //                      o frame
    // OR
    //
    // @param number frames The total number of frames
    // @param number frame  The frame number
    //
    RGraph.Effects.getEasingMultiplier = function ()
    {
        var args = RGraph.getArgs(arguments, 'frames,frame');

        return Math.pow(Math.sin((args.frame / args.frames) * RGraph.HALFPI), 3);
    };








    //
    // This function converts an array of strings to an array of numbers. Its used by the meter/gauge
    // style charts so that if you want you can pass in a string. It supports various formats:
    // 
    // '45.2'
    // '-45.2'
    // ['45.2']
    // ['-45.2']
    // '45.2,45.2,45.2' // A CSV style string
    // 
    // @param  args object An object consisting of:
    //                      o string
    //                      o separator (optional)
    // OR
    //
    // @param number frames    The string or array to parse
    // @param string separator Optional Use this instead of the default comma
    //
    RGraph.stringsToNumbers = function ()
    {
        var args = RGraph.getArgs(arguments, 'string,separator');

        // An optional separator to use intead of a comma
        var sep = args.separator || ',';



        // Remove preceding square brackets
        if (typeof args.string === 'string' && args.string.trim().match(/^\[ *\d+$/)) {
            args.string = args.string.replace('[', '');
        }


        // If it's already a number just return it
        if (typeof args.string === 'number') {
            return args.string;
        }





        if (typeof args.string === 'string') {
            if (args.string.indexOf(sep) != -1) {
                args.string = args.string.split(sep);
            } else {
                args.string = parseFloat(args.string);

                if (isNaN(args.string)) {
                    args.string = null;
                }

            }
        }





        if (typeof args.string === 'object' && !RGraph.isNullish(args.string)) {
            for (var i=0,len=args.string.length; i<len; i+=1) {
                args.string[i] = RGraph.stringsToNumbers(
                    args.string[i],
                    args.separator
                );
            }
        }

        return args.string;
    };








    //
    // Drawing cache function. This function creates an off-screen canvas and draws [wwhatever] to it
    // and then subsequent calls use that  instead of repeatedly drawing the same thing.
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o id
    //                      o function
    // OR
    //
    // @param object   obj  The graph object
    // @param string   id   An ID string used to identify the relevant entry in the cache
    // @param function func The drawing function. This will be called to do the draw.
    //
    RGraph.cachedDraw = function ()
    {
        var args = RGraph.getArgs(arguments, 'object,id,function');




        /////////////////////////////////////////
        //
        // This bypasses caching entirely:
        //
        // func(obj, obj.canvas, obj.context);
        // return;
        //
        /////////////////////////////////////////





        //If the cache entry exists - just copy it across to the main canvas
        if (!RGraph.cache[args.id]) {

            RGraph.cache[args.id] = {};

            RGraph.cache[args.id].object = args.object;
            RGraph.cache[args.id].canvas = document.createElement('canvas');

            RGraph.cache[args.id].canvas.setAttribute('width', args.object.canvas.width);
            RGraph.cache[args.id].canvas.setAttribute('height', args.object.canvas.height);
            RGraph.cache[args.id].canvas.setAttribute('id', 'background_cached_canvas' + args.object.canvas.id);

            RGraph.cache[args.id].canvas.__object__ = args.object;
            RGraph.cache[args.id].context = RGraph.cache[args.id].canvas.getContext('2d');
            
            // Antialiasing on the cache canvas
            RGraph.cache[args.id].context.translate(0.5,0.5);

            // Call the function
            args.function(
                args.object,
                RGraph.cache[args.id].canvas,
                RGraph.cache[args.id].context
            );
        }

        // Now copy the contents of the cached canvas over to the main one.
        // The coordinates are -0.5 because of the anti-aliasing effect in
        // use on the main canvas
        args.object.context.drawImage(RGraph.cache[args.id].canvas,-0.5,-0.5);
    };








    //
    // The function that runs through the supplied configuration and
    // converts it to the RGraph style.
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o config
    // OR
    //
    // @param object conf The config
    // @param object      The settings for the object
    //
    RGraph.parseObjectStyleConfig = function ()
    {
        var args = RGraph.getArgs(arguments, 'object,config');

        for (var i in args.config) {
            if (typeof i === 'string') {
                args.object.set(i, args.config[i]);
            }
        }
    };








    //
    // This function is a short-cut for the canvas path syntax (which can be rather
    // verbose). You can read a description of it (which details all of the
    // various options) on the RGraph blog (www.rgraph.net/blog). The function is
    // added to the CanvasRenderingContext2D object so it becomes a context function.
    // 
    // So you can use it like these examples show:
    // 
    // 1. RGraph.path(context, 'b r 0 0 50 50 f red');
    // 2. RGraph.path(context, 'b a 50 50 50 0 3.14 false f red');
    // 3. RGraph.path(context, 'b m 5 100 bc 5 0 100 0 100 100 s red');
    // 4. RGraph.path(context, 'b m 5 100 at 50 0 95 100 50 s red');
    // 5. RGraph.path(context, 'sa b r 0 0 50 50 c b r 5 5 590 240 f red rs');
    // 6. RGraph.path(context, 'ld [2,6] ldo 4 b r 5 5 590 240 f red');
    // 7. RGraph.path(context, 'ga 0.25 b r 5 5 590 240 f red');
    //
    // You can also call it like this - as a member function of an RGraph object:
    //
    // 8 obj.path('b r % % % % f red s black', x, y, width, height);
    // 
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o path
    //                      o args
    // OR
    // 
    // @param  args object An object consisting of:
    //                      o context
    //                      o path
    //                      o args
    // OR
    //
    // @param  args object The chart object
    // @param  args string The path to draw
    // @param  args array  An array of arguments for the path
    //
    // OR
    //
    // @param args context The canvas tags context
    // @param args string  The path to draw
    // @param args array   An array of arguments to the path
    //
    RGraph.path = function (args)
    {
        var arguments = Array.prototype.slice.call(arguments);

        // Allow a single arg to be passed as well as multiple

        // Object is passed
        if (arguments.length === 1 && args.object && args.path) {
            var context = args.object.context;
            var p       = args.path;
            var args    = args.args;
        
        // Context is passed
        } else if (arguments.length === 1 && args.context && args.path) {
            var context  = args.context;
            var p        = args.path;
            var args     = args.args;
        
        // Multiple args, object given
        } else if (arguments.length >= 2 && arguments[0].isrgraph && arguments[0].context) {
            var context = arguments[0].context;
            var p       = arguments[1];
            var args    = arguments.length > 2 ? arguments.slice(2) : [];
        
        // Multiple args, context given
        } else if (arguments.length >= 2 && arguments[0].toString().indexOf('Context')) {
            var context   = arguments[0];
            var p         = arguments[1];
            var args      = arguments.length > 2 ? arguments.slice(2) : [];
        }

        
        // If the path was a string - split it then collapse quoted bits together
        if (typeof p === 'string') {
            p = splitstring(p);
        }

        // Store the last path on the RGraph object
        RGraph.path.last = RGraph.arrayClone(p);

        // Go through the path information.
        for (var i=0,len=p.length; i<len; i+=1) {

            switch (p[i]) {
                case 'b':context.beginPath();break;
                case 'c':context.closePath();break;
                case 'm':context.moveTo(parseFloat(p[i+1]),parseFloat(p[i+2]));i+=2;break;
                case 'l':context.lineTo(parseFloat(p[i+1]),parseFloat(p[i+2]));i+=2;break;
                case 's':if(p[i+1]&&p[i+1]!=='null')context.strokeStyle=p[i+1];context.stroke();i++;break;
                case 'f':if(p[i+1]&&p[i+1]!=='null'){context.fillStyle=p[i+1];}context.fill();i++;break;
                case 'qc':context.quadraticCurveTo(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]));i+=4;break;
                case 'bc':context.bezierCurveTo(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]),parseFloat(p[i+5]),parseFloat(p[i+6]));i+=6;break;
                case 'r':context.rect(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]));i+=4;break;
                case 'rr':RGraph.roundedRect(context,parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]),parseFloat(p[i+5]));i+=5;break;
                case 'a':context.arc(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]),parseFloat(p[i+5]),p[i+6]==='true'||p[i+6]===true||p[i+6]===1||p[i+6]==='1'?true:false);i+=6;break;
                case 'at':context.arcTo(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]),parseFloat(p[i+5]));i+=5;break;
                case 'lw':context.lineWidth=parseFloat(p[i+1]);i++;break;
                case 'e':context.ellipse(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]),parseFloat(p[i+5]),parseFloat(p[i+6]),parseFloat(p[i+7]),p[i+8] === 'true' ? true : false);i+=8;break;
                case 'lj':context.lineJoin=p[i+1];i++;break;
                case 'lc':context.lineCap=p[i+1];i++;break;
                case 'sc':context.shadowColor=p[i+1];i++;break;
                case 'sb':context.shadowBlur=parseFloat(p[i+1]);i++;break;
                case 'sx':context.shadowOffsetX=parseFloat(p[i+1]);i++;break;
                case 'sy':context.shadowOffsetY=parseFloat(p[i+1]);i++;break;
                case 'fs':context.fillStyle=p[i+1];i++;break;
                case 'ss':context.strokeStyle=p[i+1];i++;break;
                case 'fr':context.fillRect(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]));i+=4;break;
                case 'sr':context.strokeRect(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]));i+=4;break;
                case 'cl':context.clip();break;
                case 'sa':context.save();break;
                case 'rs':context.restore();break;
                case 'tr':context.translate(parseFloat(p[i+1]),parseFloat(p[i+2]));i+=2;break;
                case 'sl':context.scale(parseFloat(p[i+1]), parseFloat(p[i+2]));i+=2;break;
                case 'ro':context.rotate(parseFloat(p[i+1]));i++;break;
                case 'tf':context.transform(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]),parseFloat(p[i+5]),parseFloat(p[i+6]));i+=6;break;
                case 'stf':context.setTransform(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]),parseFloat(p[i+5]),parseFloat(p[i+6]));i+=6;break;
                case 'cr':context.clearRect(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),parseFloat(p[i+4]));i+=4;break;
                case 'ld':var parts = eval(p[i+1]);context.setLineDash(parts);i+=1;break;
                case 'ldo':context.lineDashOffset=p[i+1];i++;break;
                case 'fo':context.font=p[i+1];i++;break;
                case 'ft':context.fillText(p[i+1], parseFloat(p[i+2]), parseFloat(p[i+3]));i+=3;break;
                case 'st':context.strokeText(p[i+1], parseFloat(p[i+2]), parseFloat(p[i+3]));i+=3;break;
                case 'ta':context.textAlign=p[i+1];i++;break;
                case 'tbl':context.textBaseline=p[i+1];i++;break;
                case 'ga':context.globalAlpha=parseFloat(p[i+1]);i++;break;
                case 'gco':context.globalCompositeOperation=p[i+1];i++;break;
                case 'fu':(p[i+1])(context.canvas.__object__);i++;break;
                case 'ci':context.arc(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),0,6.29,false);i+=3;break;
                case 'do':context.arc(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+3]),0,6.29,false);context.arc(parseFloat(p[i+1]),parseFloat(p[i+2]),parseFloat(p[i+4]),6.29,0,true);i+=4;break;
                
                // Empty option - ignore it
                case '':break;
                
                // Unknown option
                default: alert('[ERROR] Unknown option: ' + p[i]);
            }
        }
        
        function splitstring (p)
        {
            var ret = [], buffer = '', inquote = false, quote = '', substitutionIndex = 0;

            // p is a string - not an array
            for (var i=0; i<p.length; i+=1) {
                
                var chr = p[i],
                    isWS = chr.match(/ /);

                if (isWS) {
                    if (!inquote) {

                        // Get rid of any enclosing quotes
                        if (buffer[0] === '"' || buffer[0] === "'") {
                            buffer = buffer.substr(1, buffer.length - 2);
                        }


                        // String substitution
                        if (buffer.trim() === '%' && typeof args[substitutionIndex] !== 'undefined') {
                            buffer = args[substitutionIndex++];
                        }

                        ret.push(buffer);
                        buffer = '';
                    } else {
                        buffer += chr;
                    }
                } else {
                    if (chr === "'" || chr === '"') {
                        inquote = !inquote;
                    }

                    buffer += chr;
                }
            }

            // Do the last bit (including substitution)
            if (buffer.trim() === '%' && args[substitutionIndex]) {
                buffer = args[substitutionIndex++];
            }

            ret.push(buffer);

            return ret;
        }
    };








    //
    // This function gets the text properties when given a relevant prefix.
    // So if you give it 'text' as the prefix you'll get the:
    //
    //  o textFont
    //  o textSize
    //  o textColor
    //  o textBold
    //  o textItalic
    //
    // ...properties. On the other hand if you give it 'yaxisScaleLabels'
    // as the prefix you'll get:
    //
    //  o yaxisScaleLabelsFont
    //  o yaxisScaleLabelsSize
    //  o yaxisScaleLabelsColor
    //  o yaxisScaleLabelsBold
    //  o yaxisScaleLabelsItalic
    // 
    // @param  args object An object consisting of:
    //                      o object
    //                      o prefix
    //
    RGraph.getTextConf = function (args)
    {
        var obj        = args.object,
            properties = obj.properties,
            prefix     = args.prefix;

        // Has to be a seperate var statement
        var font   = typeof properties[prefix + 'Font']   === 'string'  ? properties[prefix + 'Font']   : properties.textFont,
            size   = typeof properties[prefix + 'Size']   === 'number'  ? properties[prefix + 'Size']   : properties.textSize,
            color  = typeof properties[prefix + 'Color']  === 'string'  ? properties[prefix + 'Color']  : properties.textColor,
            bold   = !RGraph.isNullish(properties[prefix + 'Bold'])        ? properties[prefix + 'Bold']   : properties.textBold,
            italic = !RGraph.isNullish(properties[prefix + 'Italic'])      ? properties[prefix + 'Italic'] : properties.textItalic;

        return {
            font:   font,
            size:   size,
            color:  color,
            bold:   bold,
            italic: italic
        };
    };








    //
    // The responsive function. This installs the rules as stipulated
    // in the rules array.
    //
    // @param object conf An object map of properties/arguments for the function.
    //                    This should consist of:
    //                     o maxWidth
    //                     o width
    //                     o height
    //                     o options
    //                     o css
    //                     o parentCss
    //                     o callback
    //
    RGraph.responsive = function (conf)
    {
        var obj = this;
        
        //
        // Sort the configuration so that it descends in order of biggest screen
        // to smallest
        //
        conf.sort(function (a, b)
        {
            var aNull = RGraph.isNullish(a.maxWidth);
            var bNull = RGraph.isNullish(b.maxWidth);
            
            if (aNull && bNull) return 0;
            if (aNull && !bNull) return -1;
            if (!aNull && bNull) return 1;

            return b.maxWidth - a.maxWidth;
        });

        //
        // Preparse the configuration adding any missing minWidth values to the configuration
        //
        for (var i=0; i<conf.length; ++i) {
            if (conf[i+1] && typeof conf[i+1].maxWidth === 'number') {
                conf[i].minWidth = conf[i+1].maxWidth;
            } else if (!conf[i+1]) {
                conf[i].minWidth = 0;
            }
        }


        //
        // Loop through the configurations
        //
        for (var i=0; i<conf.length; ++i) {
        
            // Set the minimum and maximum
            conf[i].minWidth = RGraph.isNullish(conf[i].minWidth) ?      0 : conf[i].minWidth;
            conf[i].maxWidth = RGraph.isNullish(conf[i].maxWidth) ? 100000 : conf[i].maxWidth;
            
            // Create the media query string
            var str = 'screen and (min-width: %1px) and (max-width: %2px)'.format(
                conf[i].minWidth,
                conf[i].maxWidth
            );
        
            var mediaQuery = window.matchMedia(str);
            (function (index)
            {
                mediaQuery.addListener(function (e)
                {
                    if (e.matches) {
                        matchFunction(conf[index]);
                    }
                });
            })(i);
            
            // An Initial test
            if (   document.documentElement.clientWidth >= conf[i].minWidth
                && document.documentElement.clientWidth < conf[i].maxWidth) {
                matchFunction(conf[i]);
            }
        }
        
        //
        // If a rule matches - this is the function that runs
        //
        function matchFunction (rule)
        {
            // If a width is defined for this rule set it
            if (typeof rule.width === 'number') {
                if (obj.get('textAccessible')) {
                    obj.canvas.parentNode.style.width  = rule.width + 'px';
                }
                
                obj.canvas.width = rule.width;
                obj.canvas.__rgraph_aa_translated__ = false;
            }

            //
            // If a height is defined for this rule set it
            //
            if (typeof rule.height === 'number') {
                if (obj.get('textAccessible')) {
                    obj.canvas.parentNode.style.height = rule.height + 'px';
                }

                obj.canvas.height = rule.height;
                obj.canvas.__rgraph_aa_translated__ = false;
            }

            //
            // Are there any options to be set?
            //
            if (typeof rule.options === 'object') {
                for (var j in rule.options) {
                    if (typeof j === 'string') {
                        obj.set(j, rule.options[j]);
                        
                        // Set the original colors to the new colors
                        // if necessary
                        if (j === 'colors' && obj.original_colors) {
                            obj.original_colors = RGraph.arrayClone(rule.options[j]);
                        }
                    }
                }
            }





            //
            // This function simply sets a CSS property on the object.
            // It accommodates certain name changes
            //
            var setCSS = function (el, name, value)
            {
                var replacements = [
                    ['float', 'cssFloat']
                ];
                
                // Replace the name if necessary
                for (var i=0; i<replacements.length; ++i) {
                    if (name === replacements[i][0]) {
                        name = replacements[i][1];
                    }
                }

                el.style[name] = value;
            };




            //
            // Are there any CSS properties to set on the canvas tag?
            //
            if (typeof rule.css === 'object') {
                for (var j in rule.css) {
                    if (typeof j === 'string') {
                        if (obj.get('textAccessible')) {
                            setCSS(obj.canvas.parentNode, j, rule.css[j]);
                        } else {
                            setCSS(obj.canvas, j, rule.css[j]);
                        }
                    }
                }
            }

            //
            // Are there any CSS properties to set on the canvas tahs PARENT?
            //
            if (typeof rule.parentCss === 'object') {
                for (var j in rule.parentCss) {
                    if (typeof j === 'string') {
                        if (obj.get('textAccessible')) {
                            setCSS(obj.canvas.parentNode.parentNode, j, rule.parentCss[j]);
                        } else {
                            setCSS(obj.canvas.parentNode, j, rule.parentCss[j])
                        }
                    }
                }
            }





            RGraph.cache = [];
            RGraph.resetColorsToOriginalValues(obj);
            if (obj.get('textAccessible') && RGraph.text.domNodeCache && RGraph.text.domNodeCache.reset) {
                RGraph.text.domNodeCache.reset(obj.canvas);
            }

            RGraph.redraw(obj.canvas);

            // Run the callback function if it's defined
            if (typeof rule.callback === 'function') {
                (rule.callback)(obj);
            }
        }
        
        // Returning the object facilitates chaining
        return obj;
    };








    //
    // You can now specify your reponsive configuration inline,
    // with the rest of your charts configuration.
    //
    // @param object obj The chart object
    //
    RGraph.installInlineResponsive = function (obj)
    {
        if (RGraph.isArray(obj.properties.responsive)) {
            RGraph.runOnce('install-responsive-configuration-' + obj.uid, function ()
            {
                    obj.responsive(obj.properties.responsive);
            });
        }
    };








    //
    // This function can be used to resize the canvas when the screen size changes. You
    // specify various rules and they're then checked.
    //
    // @param object conf An object map of properties/arguments for the function.
    //                    This should consist of:
    //                     o maxWidth
    //                     o width
    //                     o height
    //                     o options
    //                     o css
    //                     o parentCss
    //                     o callback
    // @param object       Optionally you can give a second object map to the function
    //                     of options. There's only on option at the moment:
    //                      o delay
    //
    // RGraph.responsiveOld
    RGraph.responsive_old = function (conf)
    {
        var args = arguments[1] || {},
            
            // This function is added to each object in their constructors so the this
            // variable is the chart object.
            obj   = this,
            
            // The func variable becomes the function that is fired by the resize event
            func  = null,
            
            // This is the timer reference
            timer = null;
        
        // The resizie function will run This many milliseconds after the
        // resize has "finished"
        args.delay = typeof args.delay === 'number' ? args.delay : 200;

        // [TODO] Store defaults that are used if there's no match
        var func = function ()
        {
            // This is set to true if a rule matches
            var matched = false;

            // Loop through all of the rules
            for (var i=0; i<conf.length; ++i) {

                //
                // If a maxWidth is stipulated test that
                //
                if (!matched && (document.documentElement.clientWidth <= conf[i].maxWidth || RGraph.isNullish(conf[i].maxWidth))) {

                    matched = true;

                    // If a width is defined for this rule set it
                    if (typeof conf[i].width === 'number') {
                        if (obj.get('textAccessible')) {
                            obj.canvas.parentNode.style.width  = conf[i].width + 'px';
                        }
                        
                        obj.canvas.width = conf[i].width;
                        obj.canvas.__rgraph_aa_translated__ = false;
                    }




                    //
                    // If a height is defined for this rule set it
                    //
                    if (typeof conf[i].height === 'number') {
                        if (obj.get('textAccessible')) {
                            obj.canvas.parentNode.style.height = conf[i].height + 'px';
                        }

                        obj.canvas.height = conf[i].height;
                        obj.canvas.__rgraph_aa_translated__ = false;
                    }




                    //
                    // Are there any options to be set?
                    //
                    if (typeof conf[i].options === 'object' && typeof conf[i].options === 'object') {
                        for (var j in conf[i].options) {
                            if (typeof j === 'string') {
                                obj.set(j, conf[i].options[j]);
                            }
                        }
                    }


                    //
                    // This function simply sets a CSS property on the object.
                    // It accommodates certain name changes
                    //
                    var setCSS = function (el, name, value)
                    {
                        var replacements = [
                            ['float', 'cssFloat']
                        ];
                        
                        // Replace the name if necessary
                        for (var i=0; i<replacements.length; ++i) {
                            if (name === replacements[i][0]) {
                                name = replacements[i][1];
                            }
                        }

                        el.style[name] = value;
                    };




                    //
                    // Are there any CSS properties to set on the canvas tag?
                    //
                    if (typeof conf[i].css === 'object') {
                        for (var j in conf[i].css) {
                            if (typeof j === 'string') {
                                if (obj.get('textAccessible')) {
                                    setCSS(obj.canvas.parentNode, j, conf[i].css[j]);
                                } else {
                                    setCSS(obj.canvas, j, conf[i].css[j]);
                                }
                            }
                        }
                    }

                    //
                    // Are there any CSS properties to set on the canvas tahs PARENT?
                    //
                    if (typeof conf[i].parentCss === 'object') {
                        for (var j in conf[i].parentCss) {
                            if (typeof j === 'string') {
                                if (obj.get('textAccessible')) {
                                    setCSS(obj.canvas.parentNode.parentNode, j, conf[i].parentCss[j]);
                                } else {
                                    setCSS(obj.canvas.parentNode, j, conf[i].parentCss[j])
                                }
                            }
                        }
                    }


                    // Redraw the chart
                    RGraph.cache = [];
                    RGraph.resetColorsToOriginalValues(obj);
                    if (obj.get('textAccessible')) {
                        RGraph.text.domNodeCache.reset(obj.canvas);
                    }

                    RGraph.redraw();


                    // Run the callback function if it's defined
                    if (typeof conf[i].callback === 'function') {
                        (conf[i].callback)(obj);
                    }
                }
            }
        }






        // Install the resize event listener
        RGraph.responsive.window_resize_event_listener = function ()
        {
            // Set a new timer in order to fire the func() function
            if (args.delay > 0) {
                // Clear the timeout
                clearTimeout(timer);
                
                // Start a new timer going
                timer = setTimeout(func, args.delay);
            
            // If you don't want a delay before the resizing occurs
            // then set the delay to zero and it will be fired immediately
            } else {
                func();
            }
        };
        window.addEventListener(
            'resize',
            RGraph.responsive.window_resize_event_listener,
            false
        );

        
        // Call the function initially otherwise it may never run
        func();
        
        // This facilitates chaining
        return obj;
    };








    //
    // A shortcut function for the RGraph.path() function. Saves
    // approximately 40 characters, In each objects constructor
    // it is added to the object so you can call it like this:
    //
    // myBar.path({
    //     path: 'lw 10 b r % % % % s black f red'
    //     args: [5,5,50,50]
    // });
    //
    // Or like this (whichever you prefer):
    //
    // myBar.path(
    //     'lw 10 b r % % % % s black f red',
    //     5, 5, 50, 50
    // );
    //
    RGraph.pathObjectFunction = function ()
    {
        // Siongle object argument
        if (arguments.length === 1 && typeof arguments[0] === 'object') {
            
            var args = RGraph.getArgs(arguments, 'path,args');

            RGraph.path({
                object: this,
                  path: args.path,
                  args: args.args
            });
        
        // First arg is a string                
        } else {
        
            var args = Array.prototype.slice.call(arguments, 1);

            RGraph.path({
                object: this,
                  path: arguments[0],
                  args: args
            });
        }
    };








    //
    // A common X axis drawing function that can be used by  the
    // Bar, HBar, Line, Scatter functions. A long time coming - but
    // this will eventually be joined by a common Y axis drawing
    // function.
    //
    // @param object obj The chart object. All the properties are
    //                   retrieved from this.
    // @param object opt Options to the function
    //
    RGraph.drawXAxis = function (obj, opt = {})
    {
        var properties      = obj.properties,
            context         = obj.context,
            tickmarksLength = (typeof properties.xaxisTickmarksLength === 'number' ? properties.xaxisTickmarksLength : 3),
            isSketch        = (obj.type === 'bar' && properties.variant === 'sketch');






        









        //
        // If the xaxisLabels property is defined then go through it converting
        // null and undefined values to empty strings.
        //
        if (   typeof properties.xaxisLabels === 'object'
            && !RGraph.isNullish(properties.xaxisLabels)
            && properties.xaxisLabels.length) {
            
            for (var i=0; i<properties.xaxisLabels.length; ++i) {
                if (typeof properties.xaxisLabels[i] === 'undefined' || properties.xaxisLabels[i] === null) {
                    properties.xaxisLabels[i] = '';
                }
            }
        }

        //
        // Calculate the Y coordinate for the X axis
        if ( (obj.type === 'hbar' || obj.type === 'gantt') && properties.xaxisPosition === 'bottom') {
            var y = obj.canvas.height - properties.marginBottom;
        } else if ( (obj.type === 'hbar' || obj.type === 'gantt') && properties.xaxisPosition === 'top') {
            var y = properties.marginTop;
        //} else if (obj.type === 'scatter' && properties.yaxisScaleInvert) {
        //    var y = obj.getYCoord(properties.yaxisScaleMin > 0 ? properties.yaxisScaleMin : 0);
        } else {
            var y = obj.getYCoord(properties.yaxisScaleMin > 0 ? properties.yaxisScaleMin : 0);
        }

        // Special case for a Line chart with an inverted scale
        if (obj.type === 'line' && properties.yaxisScaleInvert && properties.yaxisScaleMin === 0) {
            y = obj.getYCoord(obj.scale2.max);
        }

        // Special case for a Scatter chart with an inverted scale
        if (obj.type === 'scatter' && properties.yaxisScaleInvert) {
            if (properties.yaxisScaleMin >= 0) {
                y = obj.getYCoord(obj.scale2.max);
            }
        }

        // Special case for positioning an X axis Drawing API object
        if (obj.type === 'drawing.xaxis') {
            if (properties.xaxisPosition === 'center') {
                y = ((obj.canvas.height - properties.marginTop - properties.marginBottom) / 2) + properties.marginTop;
            } else {
                y = obj.y;
            }
        }

        //
        // Draw the axis
        //
        if (properties.xaxis) {

            // Draw the axis
            obj.path(
                'lc square lw % b m % % l % % s %',
                properties.xaxisLinewidth,
                properties.marginLeft - (isSketch ? 5 : 0),
                
                y - (isSketch ? 2 : 0),
                
                obj.canvas.width - properties.marginRight + (isSketch ? 7 : 0), y + (isSketch ? 2 : 0),
                properties.xaxisColor
            );

                // Draw the tickmarks if necessary
                if (!isSketch) {

                    if (properties.xaxisTickmarks) {

                        if (typeof properties.xaxisTickmarksCount === 'number') {
                            var xaxisTickmarksCount = properties.xaxisTickmarksCount;

                        // Bar - get number of tickmarks from the number of data points
                        } else if (obj.type === 'bar') {
                            var xaxisTickmarksCount = obj.data.length || 10;
                            
                        // HBar - get number of tickmarks from the xaxisLabelsCount property and default to 5
                        } else if (obj.type === 'hbar') {
                            var xaxisTickmarksCount = (properties.xaxisLabelsCount || 5);
                        
                        // Line - get the number of tickmarks from the number of datapoints
                        } else if (obj.type === 'line') {
                            var xaxisTickmarksCount = obj.data[0].length > 0 ? obj.data[0].length - 1 : 10;
                        
                        // Scatter - with a scale - get the number of tickmarks from the number of scale labels
                        } else if (obj.type === 'scatter' && properties.scale) {
                            var xaxisTickmarksCount = 5;

                            
                        // Scatter - with labels - get the number of tickmarks from the number of labels
                        } else if (obj.type === 'scatter' && properties.xaxisLabels) {
                            var xaxisTickmarksCount = properties.xaxisLabels.length;

                        // Scatter - with no labels and no scale
                        } else if (obj.type === 'scatter') {
                            var xaxisTickmarksCount = 5;

                        // Waterfall - get the number of tickmarks from the number of datapoints
                        } else if (obj.type === 'waterfall') {
                            var xaxisTickmarksCount = obj.data.length + (properties.total ? 1 : 0);
                            
                        // Drawing API X axis
                        } else if (obj.type === 'drawing.xaxis') {

                            if (properties.scale) {
                                var xaxisTickmarksCount = properties.xaxisScaleLabelsCount;
                            } else if (properties.xaxisLabels && properties.xaxisLabels.length) {
                                var xaxisTickmarksCount = properties.xaxisLabels.length;
                            } else {
                                var xaxisTickmarksCount = 5;
                            }

                        //  Default to 5 tickmarks
                        } else {
                            xaxisTickmarksCount = 5;
                        }
                    } else {
                        properties.xaxisTickmarksCount = 0;
                    }









                // Determine the Y start coordinate for the tickmarks
                if (properties.xaxisPosition === 'center' && properties.yaxisScaleMin >= 0) {
                    if (properties.yaxisScaleInvert) {
                        var tickmarksYStart = obj.getYCoord(properties.yaxisScaleMax) - tickmarksLength;
                        var tickmarksYEnd   = obj.getYCoord(properties.yaxisScaleMax) + tickmarksLength;
                    } else {
                        var tickmarksYStart = obj.getYCoord(properties.yaxisScaleMin) - tickmarksLength;
                        var tickmarksYEnd   = obj.getYCoord(properties.yaxisScaleMin) + tickmarksLength;
                    }

                } else if (properties.xaxisPosition === 'center' || properties.yaxisScaleMin < 0) {
                    if (properties.yaxisScaleInvert) {
                        var tickmarksYStart = obj.getYCoord(obj.scale2.max) - tickmarksLength;
                        var tickmarksYEnd   = obj.getYCoord(obj.scale2.max) + tickmarksLength;
                    } else {
                        var tickmarksYStart = obj.getYCoord(0) - tickmarksLength;
                        var tickmarksYEnd   = obj.getYCoord(0) + tickmarksLength;
                    }

                    // Account for offset axes
                    if (properties.yaxisScaleMin < 0 && properties.yaxisScaleMax > 0) {
                        var tickmarksYStart = obj.getYCoord(0) - tickmarksLength;
                        var tickmarksYEnd   = obj.getYCoord(0) + tickmarksLength;
                    }

                } else if (properties.xaxisPosition === 'top') {
                    if (obj.getYCoord) {
                        var tickmarksYStart = obj.getYCoord(0) - tickmarksLength;
                        var tickmarksYEnd   = obj.getYCoord(0);
                    } else {
                        var tickmarksYStart = properties.marginTop - tickmarksLength;
                        var tickmarksYEnd   = properties.marginTop;
                    }

                } else {

                    if (obj.getYCoord) {
                        if (obj.type === 'line' && properties.yaxisScaleInvert) {
                            var tickmarksYStart = obj.getYCoord(obj.scale2.max);
                            var tickmarksYEnd   = obj.getYCoord(obj.scale2.max) + tickmarksLength;
                        
                        } else if (obj.type === 'scatter' && properties.yaxisScaleInvert) {
                            var tickmarksYStart = obj.getYCoord(obj.scale2.max);
                            var tickmarksYEnd   = obj.getYCoord(obj.scale2.max) + tickmarksLength;

                        } else if (obj.type === 'drawing.xaxis') {
                            tickmarksYStart = obj.y;
                            tickmarksYEnd   = obj.y + tickmarksLength;

                        } else {
                            var tickmarksYStart = obj.getYCoord(properties.yaxisScaleMin);
                            var tickmarksYEnd   = obj.getYCoord(properties.yaxisScaleMin) + tickmarksLength;
                        }

                    } else {

                        var tickmarksYStart = obj.canvas.height - properties.marginBottom;
                        var tickmarksYEnd   = obj.canvas.height - properties.marginBottom + tickmarksLength;
                    }
                }







                //if (!xaxisTickmarksCount) {
                    //xaxisTickmarksCount = 10;
                //}
    
                for (var i=0; i<=xaxisTickmarksCount; ++i) {

                    // Don't draw the LEFT tickmark if there's a Y axis on the left or if specifically
                    // told not to
                    if (RGraph.isNullish(properties.xaxisTickmarksLastLeft)) {

                        if ( i === 0 && properties.yaxis && properties.yaxisPosition === 'left' && !(properties.xaxisScaleMax > 0 && properties.xaxisScaleMin < 0) ) {
                            continue;
                        }

                    } else if (i === 0 && !properties.xaxisTickmarksLastLeft) {
                        continue;
                    }
                    
                    // Don't draw the RIGHT tickmark if there's a Y axis on the right or if specifically
                    // told not to
                    if (RGraph.isNullish(properties.xaxisTickmarksLastRight)) {
                        if (   i === xaxisTickmarksCount && properties.yaxis && properties.yaxisPosition === 'right') {
                            continue;
                        }
                    } else if ( i === xaxisTickmarksCount && !properties.xaxisTickmarksLastRight ) {
                        continue;
                    }
    
                    var x = (((obj.canvas.width - properties.marginLeft - properties.marginRight) / xaxisTickmarksCount) * i) + properties.marginLeft;

                    // If the chart is an HBar and the Y axis is in the center
                    // then don't draw a tickmark at the same position as the
                    // Y axis.
                    if (obj.type === 'hbar' ) {
                        if (properties.yaxisPosition === 'center' && x > obj.getXCoord(0) - 2 && x < obj.getXCoord(0) + 2) {
                            continue;
                        } else if (properties.yaxisPosition === 'left' && properties.xaxisScaleMin < 0 && properties.xaxisScaleMax > 0 && x > obj.getXCoord(0) - 2 && x < obj.getXCoord(0) + 2) {
                            continue;
                        }
                    }
    
                    obj.path(
                        'b m % % l % % s %',
                        x, tickmarksYStart,
                        x, tickmarksYEnd,
                        properties.xaxisColor
                    );
                } // END loop thru xaxisTickmarksCount
            } // END if (isSketch)
        }












        
        //
        // Draw the X axis labels if they're specified
        //

        if (opt.labels !== false) {
            //
            // Text angle
            //
            if (properties.xaxisLabelsAngle != 0) {
                
                var valign =  'center';
                var halign =  'right';
                var angle  = 0 - properties.xaxisLabelsAngle;
                
                if (properties.xaxisPosition === 'top') {
                    var angle  = properties.xaxisLabelsAngle;
                }
                
            } else {
                var valign =  'top';
                var halign =  'center';
                var angle  = 0;
            }
    
            //
            // Draw an X axis scale if requested. The HBar uses an X axis scale and the
            // Scatter chart can (optionally) too
            //
            if (properties.xaxisScale) {
    
                var scale = obj.scale2;

                //
                // Get the scale for a Scatter chart X axis
                //
                if (obj.type === 'scatter') {
                    scale = obj.xscale2;

                // Get the scale for a drawing API X axis
                } else if (obj.type === 'drawing.xaxis') {
                    if (properties.xaxisScale) {
    
                        scale = RGraph.getScale({object: this, options: {
                            'scale.max':          properties.xaxisScaleMax,
                            'scale.min':          properties.xaxisScaleMin,
                            'scale.decimals':     Number(properties.xaxisScaleDecimals),
                            'scale.point':        properties.xaxisScalePoint,
                            'scale.thousand':     properties.xaxisScaleThousand,
                            'scale.round':        properties.xaxisScaleRound,
                            'scale.units.pre':    properties.xaxisScaleUnitsPre,
                            'scale.units.post':   properties.xaxisScaleUnitsPost,
                            'scale.labels.count': properties.xaxisScaleLabelsCount,
                            'scale.strict':       true
                         }});
    
                        for (var i=0; i<=properties.xaxisScaleLabelsCount; ++i) {
                        
                            var original = (((properties.xaxisScaleMax - properties.xaxisScaleMin) / properties.xaxisScaleLabelsCount) * i) + properties.xaxisScaleMin;
                            var hmargin  = properties.marginInner;
                        
                            if (typeof properties.xaxisScaleFormatter === 'function') {
                                var text =  (properties.xaxisScaleFormatter)(this, original)
                            } else {
                        
                                text = RGraph.numberFormat({
                                    object:    obj,
                                    number:    original.toFixed(original === 0 ? 0 : properties.xaxisScaleDecimals),
                                    unitspre:  properties.xaxisScaleUnitsPre,
                                    unitspost: properties.xaxisScaleUnitsPost,
                                    point:     properties.xaxisScalePoint,
                                    thousand:  properties.xaxisScaleThousand
                                });
                            }
                        }
                    }
                }
    
                for (var i=0; i<scale.labels.length; ++i) {

                    var section = (obj.canvas.width - properties.marginLeft - properties.marginRight) / scale.labels.length;
                    
                    if (properties.yaxisPosition === 'right') {
                        var x = properties.marginLeft + (section * i);
                    } else if (properties.yaxisPosition === 'center') {
                        section /= 2;
                        var x = obj.getXCoord(0) + section + (section * i);
                    } else {
                        var x = properties.marginLeft + (section * i) + section;
                    }
                    
                    var y = properties.xaxisPosition === 'top' ? obj.marginTop - (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety - 5 : (obj.canvas.height - properties.marginBottom) + (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety + 5;
                    
                    if (obj.type === 'drawing.xaxis') {
                        if (properties.xaxisPosition === 'top') {
                            y = obj.y - (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety;
                        } else {
                            y = obj.y + (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety + 5;
                        }
                    }
    
                    RGraph.text({
                      object: obj,
              textConfPrefix: 'xaxisLabels',
                        x:      x + properties.xaxisLabelsOffsetx,
                        y:      y,
                        text:   typeof properties.xaxisScaleFormatter === 'function' ? (properties.xaxisScaleFormatter)({object: obj, number: scale.values[i]}) : (properties.yaxisPosition === 'right' ? String(scale.labels[scale.labels.length - 1 - i]) : String(scale.labels[i])),
                        valign: typeof properties.xaxisLabelsValign === 'string' ? properties.xaxisLabelsValign : (properties.xaxisPosition === 'top' ? 'bottom' : valign),
                        halign: typeof properties.xaxisLabelsHalign === 'string' ? properties.xaxisLabelsHalign : halign,
                        marker: false,
                        angle:  angle,
                        tag:    'xaxis.labels'
                    });
    
                    //
                    // If the chart is a HBar and the X axis is in the center then
                    // draw the negative side of the labels
                    //
                    if (obj.type === 'hbar' && properties.yaxisPosition === 'center') {
    
                            x = obj.getXCoord(0) - section - (section * i);
                    
                            RGraph.text({
                              object: obj,
                      textConfPrefix: 'xaxisLabels',
                                x:      x + properties.xaxisLabelsOffsetx,
                                y:      properties.xaxisPosition === 'top' ? obj.marginTop - (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety - 5 : (obj.canvas.height - properties.marginBottom) + (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety + 5,
                                text:   typeof properties.xaxisScaleFormatter === 'function' ? (properties.xaxisScaleFormatter)({object: obj, number: scale.values[i]}) : '-' + String(scale.labels[i]),
                                valign: typeof properties.xaxisLabelsValign === 'string' ? properties.xaxisLabelsValign : (properties.xaxisPosition === 'top' ? 'bottom' : valign),
                                halign: typeof properties.xaxisLabelsHalign === 'string' ? properties.xaxisLabelsHalign : halign,
                                marker: false,
                                angle:  angle,
                                tag:    'xaxis.labels'
                            });
                    }
                }
    
    
    
    
                //
                // Draw the minimum label
                //
                var str = ((properties.xaxisScaleUnitsPre || '') + (properties.xaxisScaleMin || 0).toFixed(properties.xaxisScaleDecimals).replace(/\./, properties.xaxisScalePoint) + (properties.xaxisScaleUnitsPost || ''));
                str     = str.replace(/^(.+)-(\d)/, '-$1$2');
    
                RGraph.text({
                  object:   obj,
          textConfPrefix:   'xaxisLabels',
                    x:      properties.yaxisPosition === 'right' ? obj.canvas.width - properties.marginRight + properties.xaxisLabelsOffsetx : (properties.yaxisPosition === 'center' ? obj.getXCoord(0) + properties.xaxisLabelsOffsetx : properties.marginLeft + properties.xaxisLabelsOffsetx),
                    y:      y,
                    text:   typeof properties.xaxisScaleFormatter === 'function' ? (properties.xaxisScaleFormatter)({object: obj, number: obj.properties.xaxisScaleMin || 0}) : str,
                    valign: typeof properties.xaxisLabelsValign === 'string' ? properties.xaxisLabelsValign : (typeof properties.xaxisLabelsValign === 'string' ? properties.xaxisLabelsValign : (properties.xaxisPosition === 'top' ? 'bottom' : valign)),
                    halign: typeof properties.xaxisLabelsHalign === 'string' ? properties.xaxisLabelsHalign : halign,
                    marker: false,
                    angle:  angle,
                    tag:    'xaxis.labels'
                });
    
            } else if (properties.xaxisLabels && properties.xaxisLabels.length && (properties.xaxisLabelsPosition === 'section' || properties.xaxisLabelsPosition === 'edge') ) {
    
                if (properties.xaxisLabelsPosition === 'edge') {
                    var section = (obj.canvas.width - properties.marginLeft - properties.marginRight - (properties.marginInner || 0) - (properties.marginInner || 0) ) / (properties.xaxisLabels.length - 1);
                } else {
                    var section = (obj.canvas.width - properties.marginLeft - properties.marginRight) / properties.xaxisLabels.length;
                }
    
    
                for (var i=0; i<properties.xaxisLabels.length; ++i) {
    
    
                    if (properties.xaxisLabelsPosition === 'edge') {
                        var x = properties.marginLeft + (properties.marginInner || 0) + (section * i);
                    } else {
                        var x = properties.marginLeft + (section * i) + (section / 2) ;
                    }
    
    
    
    
    
    
    
    
    
    
    
                    // Allow for the Scatter chart labels to be at specific points
                    // along the X scale
                    if (typeof properties.xaxisLabels[i] === 'object' && obj.type === 'scatter') {
    
                        var rightEdge = 0;
                        var align     = properties.xaxisLabelsSpecificAlign === 'left' ? 'left' : 'center';
                        var halign    = 'center'
                        
    
                        if (properties.xaxisLabels[i+1] && typeof properties.xaxisLabels[i+1][1] === 'number') {
                            rightEdge = properties.xaxisLabels[i+1][1];
    
                        } else {
                            rightEdge = properties.xaxisScaleMax;
                        }
    
                        var leftEdge = properties.xaxisLabels[i][1];
                        var x        = ((obj.getXCoord(rightEdge) - obj.getXCoord(leftEdge)) / 2) + obj.getXCoord(leftEdge);
                        
                        if (align === 'left') {
                            x      = obj.getXCoord(leftEdge);
                            halign = 'left';
                        }
    
                        if (RGraph.isNullish(x)) {
                            continue;
                        }
    
                        if (obj.type === 'drawing.xaxis') {
                            if (properties.xaxisPosition === 'top') {
                                y = obj.y - (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety;
                            } else {
                                y = obj.y + (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety + 5;
                            }
                        }
    
                        var ret = RGraph.text({
                       object:      obj,
               textConfPrefix:      'xaxisLabels',
                            x:      x + 5 + properties.xaxisLabelsOffsetx,
                            y:      y + 5 + properties.xaxisLabelsOffsety,
                            valign: valign,
                            halign: angle != 0 ? 'right' : halign,
                            text:   String(RGraph.isNullish(properties.xaxisLabels[i][0]) ? '' : properties.xaxisLabels[i][0]),
                            angle:  angle,
                            marker: false,
                            tag:    'labels.specific',
                       cssClass:    RGraph.getLabelsCSSClassName({
                                        object: obj,
                                          name: 'xaxisLabelsClass',
                                         index: i
                                    })
                        });
    
    
    
    
    
    
    
    
    
    
    
    
    
    
                        //
                        // Draw the gray indicator line
                        //
                        obj.path('b m % % l % % s #bbb',
                            Math.round(properties.marginLeft + (((properties.xaxisLabels[i][1] - properties.xaxisScaleMin) / (properties.xaxisScaleMax - properties.xaxisScaleMin )) * (obj.canvas.width - properties.marginLeft - properties.marginRight) )), obj.canvas.height - properties.marginBottom,
                            Math.round(properties.marginLeft + (((properties.xaxisLabels[i][1] - properties.xaxisScaleMin) / (properties.xaxisScaleMax - properties.xaxisScaleMin))) * (obj.canvas.width - properties.marginLeft - properties.marginRight) ), obj.canvas.height - properties.marginBottom + 20
                        );
                        
                        // Draw the final indicator line if we're on the final label
                        if (i === properties.xaxisLabels.length - 1) {
                            obj.path('b m % % l % % s #bbb',
                                obj.canvas.width - properties.marginRight, obj.canvas.height - properties.marginBottom,
                                obj.canvas.width - properties.marginRight, obj.canvas.height - properties.marginBottom + 20
                            );
                        }
    
                    // A regular label
                    } else {
    
                        var y = properties.xaxisPosition === 'top' ? properties.marginTop + properties.xaxisLabelsOffsety - 5 : (obj.canvas.height - properties.marginBottom) + properties.xaxisLabelsOffsety + 5;
    
                        if (obj.type === 'drawing.xaxis') {
                            //y = obj.y + (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety + 5;
                            if (obj.type === 'drawing.xaxis') {
                                if (properties.xaxisPosition === 'top') {
                                    y = obj.y - (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety;
                                } else {
                                    y = obj.y + (properties.xaxisTickmarksLength || 0) + properties.xaxisLabelsOffsety + 5;
                                }
                            }
                        }
    
                        var ret = RGraph.text({
                          object:   obj,
                  
                  textConfPrefix:   'xaxisLabels',
    
                            x:      x + properties.xaxisLabelsOffsetx,
                            y:      y + properties.xaxisLabelsOffsety,
                            
                            text:   String(RGraph.isNullish(properties.xaxisLabels[i]) ? '' : properties.xaxisLabels[i]),
                            
                            valign: typeof properties.xaxisLabelsValign === 'string' ? properties.xaxisLabelsValign : (properties.xaxisPosition === 'top' ? 'bottom' : valign),
                            halign: typeof properties.xaxisLabelsHalign === 'string' ? properties.xaxisLabelsHalign : halign,
                            
                            marker: false,
                            angle:  angle,
                            tag:    'xaxis.labels',
                            
                       cssClass:    RGraph.getLabelsCSSClassName({
                                        object: obj,
                                          name: 'xaxisLabelsClass',
                                         index: i
                                    })
                        });
                    }
                }
            }
        }
        
        
        
        
        
        
        
        
        
        












        //
        // Draw the title if necessary
        //
        if (opt.title !== false) {
            if (properties.xaxisTitle) {
    
                var x = properties.marginLeft + ((obj.canvas.width - properties.marginLeft - properties.marginRight) / 2) + properties.xaxisTitleOffsetx;
                var y = properties.xaxisPosition === 'top' ? properties.marginTop  - 7 + properties.xaxisTitleOffsety - properties.xaxisTickmarksLength : obj.canvas.height - properties.marginBottom + 7 + properties.xaxisTitleOffsety + properties.xaxisTickmarksLength;
    
                if (obj.type === 'drawing.xaxis') {
                    y = obj.y  + 7 + properties.xaxisTitleOffsety + properties.xaxisTickmarksLength;
                }
    
                
                // Get the size of the X axis labels
                if (properties.xaxisScale || (properties.xaxisLabels && properties.xaxisLabels.length) ) {
                    var textConf = RGraph.getTextConf({
                        object: obj,
                        prefix: 'xaxisLabels'
                    });
    
                    if (properties.xaxisPosition === 'top') {
                        y -= textConf.size * 1.5;
                    } else {
                        y += textConf.size * 1.5;
                    }
                }
    
                // The xaxisTitlePos property
                if (typeof properties.xaxisTitlePos === 'number') {
                    if (properties.xaxisPosition === 'top') {
                        y = properties.marginTop * properties.xaxisTitlePos;
                    } else {
                        y = obj.canvas.height - properties.marginBottom + (properties.marginBottom * properties.xaxisTitlePos);
                    }
                }
    
                // Specific X and Y coordinates for the title
                if (typeof properties.xaxisTitleX === 'number') x = properties.xaxisTitleX;
                if (typeof properties.xaxisTitleY === 'number') y = properties.xaxisTitleY;
    
    
                RGraph.text({
                  object: obj,
                  textConfPrefix: 'xaxisTitle',
                    x:      x,
                    y:      y,
                    text:   properties.xaxisTitle.toString(),
                    valign: properties.xaxisPosition === 'top' ? (properties.xaxisTitleValign || 'bottom') : (properties.xaxisTitleValign || 'top'),
                    halign: properties.xaxisTitleHalign || 'center',
                    marker: false,
                    tag:    'xaxis.title'
                });
            }
        }
    };








    //
    // A common X axis drawing function that can be used by  the
    // Bar, HBar, Line, Scatter functions. A long time coming - but
    // this will eventually be joined by a common Y axis drawing
    // function.
    //
    // @param object obj The chart object. All the properties are
    //                   retrieved from this.
    // @param object opt A few options to the function
    //
    RGraph.drawYAxis = function (obj, opt = {})
    {
        var properties      = obj.properties,
            context         = obj.context,
            isSketch        = obj.type === 'bar' && properties.variant === 'sketch';
            tickmarksLength = typeof properties.yaxisTickmarksLength === 'number' ? properties.yaxisTickmarksLength : 3;








        









        // If drawing a HBar or a Gantt chart then set the yaxisLabelsSpecific option
        if (obj.type === 'hbar') {
            properties.yaxisLabelsSpecific = properties.yaxisLabels;
        }
    
        // Calculate the X coordinate for the Y axis
        if ( (obj.type === 'hbar' || obj.type === 'gantt') && properties.yaxisPosition === 'left') {
            var x = obj.getXCoord(0);
            
            if (obj.type === 'hbar' && properties.xaxisScaleMin > 0 && properties.xaxisScaleMax > properties.xaxisScaleMin) {
                var x = obj.getXCoord(properties.xaxisScaleMin);
            }
            

        } else if ( (obj.type === 'hbar' || obj.type === 'gantt') && properties.yaxisPosition === 'right') {
            var x = obj.canvas.width - properties.marginRight;
        
        } else if (obj.type === 'drawing.yaxis') {
            var x = obj.x;
        
        } else {
            var x = properties.yaxisPosition === 'right' ? obj.canvas.width - properties.marginRight : (obj.type === 'hbar' ? obj.getXCoord(0) : properties.marginLeft);
        }

        //
        // Draw the Y axis
        //
        if (properties.yaxis) {

            // Draw the axis
            obj.path(
                'lc square lw % b m % % l % % s %',
                properties.yaxisLinewidth,
                x - (isSketch ? 5 : 0),     properties.marginTop - (isSketch ? 2 : 0),                
                x + (isSketch ? 7 : 0),     obj.canvas.height - properties.marginBottom  + (isSketch ? 2 : 0),
                properties.yaxisColor
            );




            // Draw the tickmarks for the Y axis if necessary
            if (!isSketch) {
                if (properties.yaxisTickmarks) {
    
                    if (typeof properties.yaxisTickmarksCount === 'number') {
                        var yaxisTickmarksCount = properties.yaxisTickmarksCount;

                    // Bar - get number of tickmarks from the number of data points
                    } else if (obj.type === 'bar') {
                        var yaxisTickmarksCount = properties.yaxisLabelsSpecific ? properties.yaxisLabelsSpecific.length - 1 : properties.yaxisLabelsCount;

                    // HBar - get number of tickmarks from the xaxisLabelsCount property and default to 5
                    } else if (obj.type === 'hbar') {
                        var yaxisTickmarksCount = obj.data.length || 5;
                    
                    // Line - get the number of tickmarks from the number of datapoints
                    } else if (obj.type === 'line') {
                        var yaxisTickmarksCount = properties.yaxisLabelsSpecific ? properties.yaxisLabelsSpecific.length - 1  : properties.yaxisLabelsCount;
                    
                    // Scatter - with a scale - get the number of tickmarks from the number of scale labels
                    } else if (obj.type === 'scatter') {
                        var yaxisTickmarksCount = properties.yaxisLabelsCount;
                    
                    // Waterfall - get the number of tickmarks from the number of datapoints
                    } else if (obj.type === 'waterfall') {
                        var yaxisTickmarksCount = properties.yaxisLabelsCount;
                        
                    //  Default to 5 tickmarks
                    } else {
                        yaxisTickmarksCount = 5;
                    }
                } else {
                    properties.yaxisTickmarksCount = 0;
                }

    
    
    

    
    
                // Determine the X start/end coordinates for the tickmarks
                if (properties.yaxisPosition === 'right') {
                    var tickmarksXStart = x;
                    var tickmarksXEnd   = x + tickmarksLength;
    
                //} else if (properties.yaxisPosition === 'center') {
                //    var tickmarksXStart = x - tickmarksLength;
                //    var tickmarksXEnd   = x + tickmarksLength;

                } else {
                    var tickmarksXStart = x;
                    var tickmarksXEnd   = x - tickmarksLength;
                }
    
                // Account for HBar offset axes
                if (obj.type === 'hbar' && properties.xaxisScaleMin < 0 && properties.xaxisScaleMax > 0) {
                    var tickmarksXStart = obj.getXCoord(0) - tickmarksLength;
                    var tickmarksXEnd   = obj.getXCoord(0) + tickmarksLength;
                }
    
    
                //
                // Now draw the tickmarks
                //
                for (var i=0; i<=yaxisTickmarksCount; ++i) {

                    // Don't draw the TOP tickmark if there's an X axis at the top or if specifically
                    // told not to
                    if (RGraph.isNullish(properties.yaxisTickmarksLastTop)) {
                        if (i === 0 && properties.xaxis && properties.xaxisPosition === 'top') {
                            continue;
                        }
                    } else if (i === 0 && !properties.yaxisTickmarksLastTop) {
                        continue;
                    }
                    
                    // Don't draw the BOTTOM tickmark if there's an X axis at the bottom or if specifically
                    // told not to
                    if (RGraph.isNullish(properties.yaxisTickmarksLastBottom)) {

                        if (i === yaxisTickmarksCount && properties.xaxis && properties.xaxisPosition === 'bottom') {
                            continue;
                        }
                    } else if (i === yaxisTickmarksCount && !properties.yaxisTickmarksLastBottom) {
                        continue;
                    }

                    var y           = (((obj.canvas.height - properties.marginTop - properties.marginBottom) / yaxisTickmarksCount) * i) + properties.marginTop;
                    
                    if (obj.getYCoord) {
                        var xaxisYCoord = obj.getYCoord(0);
                    } else if (obj.getXCoord) {
                        var xaxisYCoord = obj.getXCoord(0);
                    } else {
                        var xaxisYCoord = obj.marginTop;
                    }

                    if (    properties.xaxis
                        && (properties.xaxisPosition === 'center' || (properties.xaxisPosition === 'bottom' && properties.yaxisScaleMin < 0 && properties.yaxisScaleMax > 0))
                        && y > (xaxisYCoord - 1)
                        && y < (xaxisYCoord + 1)) {
                        continue;
                    }

                    obj.path(
                        'b m % % l % % s %',
                        tickmarksXStart, y,
                        tickmarksXEnd,   y,
                        properties.yaxisColor
                    );
                    
                    // if the X axis is offset (eg -10,0,10,20,30,40) draw an extra
                    // tickmark at the bottom of the axes
                    if (properties.yaxisScaleMin < 0 && properties.yaxisScaleMax > 0) {
                        obj.path(
                            'b m % % l % % s %',
                            tickmarksXStart, obj.canvas.height - properties.marginBottom,
                            tickmarksXEnd,   obj.canvas.height - properties.marginBottom,
                            properties.yaxisColor
                        );
                    }

                    // if the chart is:
                    // o A Scatter chart
                    // o X axis is in the center
                    // o The scale is inverted
                    if (obj.type === 'scatter' && properties.xaxisPosition === 'center' && properties.yaxisScaleInvert) {
                        obj.path(
                            'b m % % l % % s %',
                            tickmarksXStart,   properties.marginTop,
                            tickmarksXEnd,     properties.marginTop,
                            properties.yaxisColor
                        );
                    }
                } // END for loop thru yaxisTickmarksCount
            } // END if (!isSketch)
        }







        //
        // Whether to draw the labels
        //
        if (opt.labels !== false) {
            //
            // The text angle - this does not apply to the Y axis so these
            // are just the alignments
            //
            var valign =  'center',
                halign =  'right',
                angle  = 0;
            
            
            
    
    
            //
            // Draw a Y axis scale.
            //
            if (properties.yaxisScale && !properties.yaxisLabelsSpecific) {
                if (obj.type === 'drawing.yaxis') {
                    if (properties.yaxisScale) {
    
                        obj.scale2 = RGraph.getScale({object: obj, options: {
                            'scale.max':          properties.yaxisScaleMax,
                            'scale.min':          properties.yaxisScaleMin,
                            'scale.decimals':     Number(properties.yaxisScaleDecimals),
                            'scale.point':        properties.yaxisScalePoint,
                            'scale.thousand':     properties.yaxisScaleThousand,
                            'scale.round':        properties.yaxisScaleRound,
                            'scale.units.pre':    properties.yaxisScaleUnitsPre,
                            'scale.units.post':   properties.yaxisScaleUnitsPost,
                            'scale.labels.count': properties.yaxisScaleLabelsCount,
                            'scale.strict':       true
                         }});
    
                        for (var i=0; i<=properties.yaxisScaleLabelsCount; ++i) {
                        
                            var original = (((properties.yaxisScaleMax - properties.yaxisScaleMin) / properties.yaxisScaleLabelsCount) * i) + properties.yaxisScaleMin;
                            var hmargin  = properties.marginInner;
                        
                            if (typeof properties.yaxisScaleFormatter === 'function') {
                                var text =  (properties.yaxisScaleFormatter)(this, original)
                            } else {
                        
                                text = RGraph.numberFormat({
                                    object:    obj,
                                    number:    original.toFixed(original === 0 ? 0 : properties.yaxisScaleDecimals),
                                    unitspre:  properties.yaxisScaleUnitsPre,
                                    unitspost: properties.yaxisScaleUnitsPost,
                                    point:     properties.yaxisScalePoint,
                                    thousand:  properties.yaxisScaleThousand
                                });
                            }
                        }
                    }
                }
    
                var scale           = obj.scale2;
                obj.maxLabelLength = Math.max(
                    obj.maxLabelLength,
                    obj.context.measureText(obj.scale2.labels[4]).width// * 2 // Don't know why this was doubled...?
                );
    
    
                //
                // X axis position in the center
                //
                if (properties.xaxisPosition === 'center') {
                
                    var halfHeight = ((obj.canvas.height - properties.marginTop - properties.marginBottom) / 2);
                    
                    // Draw the top halves labels
                    for (var i=0; i<scale.labels.length; ++i) {
    
                        var section = (obj.canvas.height - properties.marginTop - properties.marginBottom) / (scale.labels.length * 2);
                        var y       = properties.marginTop + (section * i);
    
                        RGraph.text({
                          object:   obj,
                  textConfPrefix:   'yaxisLabels',
                            x:      properties.yaxisPosition === 'right' ? x + (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx + 5 : x - (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx - 5,
                            y:      properties.yaxisScaleInvert ? halfHeight + properties.marginTop - (section * i) + properties.yaxisLabelsOffsety : properties.marginTop + (section * i) + properties.yaxisLabelsOffsety,
                            text:   String(scale.labels[scale.labels.length - 1 - i]),
                            valign: typeof properties.yaxisLabelsValign === 'string' ? properties.yaxisLabelsValign : valign,
                            halign: typeof properties.yaxisLabelsHalign === 'string' ? properties.yaxisLabelsHalign : (properties.yaxisPosition === 'right' ? 'left' : halign),
                            marker: false,
                            angle:  angle,
                            tag:    'yaxis.labels'
                        });
                    }
    
                    // Draw the bottom half
                    for (var i=0; i<scale.labels.length; ++i) {
                    
                        if (i === 0 && properties.yaxisScaleInvert) continue;
    
                        var section = (obj.canvas.height - properties.marginTop - properties.marginBottom) / (scale.labels.length * 2);
                        var y       = properties.marginTop + ((obj.canvas.height - properties.marginTop - properties.marginBottom) / 2) + (section * i) + section;
        
                        RGraph.text({
                          object:   obj,
                  textConfPrefix:   'yaxisLabels',
                            x:      properties.yaxisPosition === 'right' ? x + (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx + 5 : x - (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx - 5,
                            y:      properties.yaxisScaleInvert ? halfHeight + properties.marginTop + (section * i) : y + properties.yaxisLabelsOffsety,
                            text:   '-' + (properties.yaxisScaleInvert ? String(scale.labels[scale.labels.length - i - 1]) : String(scale.labels[i])),
                            valign: typeof properties.yaxisLabelsValign === 'string' ? properties.yaxisLabelsValign : valign,
                            halign: typeof properties.yaxisLabelsHalign === 'string' ? properties.yaxisLabelsHalign : (properties.yaxisPosition === 'right' ? 'left' : halign),
                            marker: false,
                            angle:  angle,
                            tag:    'yaxis.labels'
                        });
                    }
    
    
                    //
                    // Draw the zero label
                    //
                    RGraph.text({
                      object:   obj,
              textConfPrefix:   'yaxisLabels',
                        x:      properties.yaxisPosition === 'right' ? x + 5 + (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx : x - 5 - (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx,
                        y:      properties.yaxisScaleInvert ? properties.marginTop + properties.yaxisLabelsOffsety : properties.marginTop + ((obj.canvas.height - properties.marginBottom - properties.marginTop) / 2) + properties.yaxisLabelsOffsety,
                        text:   typeof properties.yaxisScaleFormatter === 'function' ? (properties.yaxisScaleFormatter)({object: this,number: 0,unitspre: properties.yaxisScaleUnitsPre,unitspost: properties.yaxisScaleUnitsPost,point: properties.yaxisScalePoint,thousand: properties.yaxisScaleThousand,formatter: properties.yaxisScaleFormatter}) : (properties.yaxisScaleUnitsPre || '') + properties.yaxisScaleMin.toFixed(properties.yaxisScaleDecimals).replace(/\./, properties.yaxisScalePoint) + (properties.yaxisScaleUnitsPost || ''),
                        valign: typeof properties.yaxisLabelsValign === 'string' ? properties.yaxisLabelsValign : valign,
                        halign: typeof properties.yaxisLabelsHalign === 'string' ? properties.yaxisLabelsHalign : (properties.yaxisPosition === 'right' ? 'left' : halign),
                        marker: false,
                        angle:  angle,
                        tag:    'yaxis.labels'
                    });
                    
    
                    //
                    // Draw the zero label for the bottom half if the scale is inverted
                    //
                    if (properties.yaxisScaleInvert) {
                        RGraph.text({
                          object:   obj,
                  textConfPrefix:   'yaxisLabels',
                            x:      properties.yaxisPosition === 'right' ? x + 5 + (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx : x - 5 - (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx,
                            y:      obj.canvas.height - properties.marginBottom + properties.yaxisLabelsOffsety,
                            text:   typeof properties.yaxisScaleFormatter === 'function' ? (properties.yaxisScaleFormatter)({object: this,number: 0,unitspre: properties.yaxisScaleUnitsPre,unitspost: properties.yaxisScaleUnitsPost,point: properties.yaxisScalePoint,thousand: properties.yaxisScaleThousand,formatter: properties.yaxisScaleFormatter}) : (properties.yaxisScaleUnitsPre || '') + properties.yaxisScaleMin.toFixed(properties.yaxisScaleDecimals).replace(/\./, properties.yaxisScalePoint) + (properties.yaxisScaleUnitsPost || ''),
                            valign: typeof properties.yaxisLabelsValign === 'string' ? properties.yaxisLabelsValign : valign,
                            halign: typeof properties.yaxisLabelsHalign === 'string' ? properties.yaxisLabelsHalign : (properties.yaxisPosition === 'right' ? 'left' : halign),
                            marker: false,
                            angle:  angle,
                            tag:    'yaxis.labels'
                        });
                    }
    
                //
                // X axis at the top
                //
                } else if (properties.xaxisPosition === 'top') {
                    for (var i=0; i<scale.labels.length; ++i) {
                
                        var section = (obj.canvas.height - properties.marginTop - properties.marginBottom) / scale.labels.length;
                        
                        // Account for inverting the scale
                        if (properties.yaxisScaleInvert) {
                            var y = obj.canvas.height - properties.marginBottom - (section * i) - section - section;
                        } else {
                            var y = properties.marginTop + (section * i);
                        }
                
                        RGraph.text({
                          object:   obj,
                  textConfPrefix:   'yaxisLabels',
                            x:      properties.yaxisPosition === 'right' ? x + (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx + 5 : x - (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx - 5,
                            y:      y + section + properties.yaxisLabelsOffsety,
                            text:   '-' + String(scale.labels[i]),
                            valign: typeof properties.yaxisLabelsValign === 'string' ? properties.yaxisLabelsValign : valign,
                            halign: typeof properties.yaxisLabelsHalign === 'string' ? properties.yaxisLabelsHalign : (properties.yaxisPosition === 'right' ? 'left' : halign),
                            marker: false,
                            angle:  angle,
                            tag:    'yaxis.labels'
                        });
                    }
                        
                        
                
                    //
                    // Draw the zero label
                    //
    
                    RGraph.text({
                      object:   obj,
                textConfPrefix:   'yaxisLabels',
                        x:      properties.yaxisPosition === 'right' ? x + 5 + (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx : x - 5 - (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx,
                        y:      properties.yaxisScaleInvert ? obj.canvas.height - properties.marginBottom + properties.yaxisLabelsOffsety : properties.marginTop + properties.yaxisLabelsOffsety,
                        text:   typeof properties.yaxisScaleFormatter === 'function' ? (properties.yaxisScaleFormatter)({object: this,number: 0,unitspre: properties.yaxisScaleUnitsPre,unitspost: properties.yaxisScaleUnitsPost,point: properties.yaxisScalePoint,thousand: properties.yaxisScaleThousand,formatter: properties.yaxisScaleFormatter}) : (properties.yaxisScaleUnitsPre || '') + properties.yaxisScaleMin.toFixed(properties.yaxisScaleDecimals).replace(/\./, properties.yaxisScalePoint) + (properties.yaxisScaleUnitsPost || ''),
                        valign: typeof properties.yaxisLabelsValign === 'string' ? properties.yaxisLabelsValign : valign,
                        halign: typeof properties.yaxisLabelsHalign === 'string' ? properties.yaxisLabelsHalign : (properties.yaxisPosition === 'right' ? 'left' : halign),
                        marker: false,
                        angle:  angle,
                        tag:    'yaxis.labels'
                    });
                //
                // X axis position at the bottom
                //
                } else {
                    for (var i=0; i<scale.labels.length; ++i) {
    
                        var section = (obj.canvas.height - properties.marginTop - properties.marginBottom) / scale.labels.length;
                        var y       = properties.marginTop + (section * (i + (properties.yaxisScaleInvert ? 1 : 0)) );
        
                        RGraph.text({
                          object:   obj,
                  textConfPrefix:   'yaxisLabels',
                            x:      properties.yaxisPosition === 'right' ? x + (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx + 5 : x - (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx - 5,
                            y:      y + properties.yaxisLabelsOffsety,
                            text:   String(properties.yaxisScaleInvert ? scale.labels[i] : scale.labels[scale.labels.length - 1 - i]),
                            valign: typeof properties.yaxisLabelsValign === 'string' ? properties.yaxisLabelsValign : valign,
                            halign: typeof properties.yaxisLabelsHalign === 'string' ? properties.yaxisLabelsHalign : (properties.yaxisPosition === 'right' ? 'left' : halign),
                            marker: false,
                            angle:  angle,
                            tag:    'yaxis.labels'
                        });
                    }
                        
                        
    
                    var zerolabel = RGraph.numberFormat({
                        object:    obj,
                        number:    properties.yaxisScaleMin.toFixed(properties.yaxisScaleDecimals),
                        unitspre:  properties.yaxisScaleUnitsPre,
                        unitspost: properties.yaxisScaleUnitsPost,
                        point:     properties.yaxisScalePoint,
                        thousand:  properties.yaxisScaleThousand
                    });
    
                    //
                    // Draw the zero label
                    //
                    RGraph.text({
                      object:   obj,
              textConfPrefix:   'yaxisLabels',
                        x:      properties.yaxisPosition === 'right' ? x + 5 + (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx : x - 5 - (properties.yaxisTickmarksLength || 0) + properties.yaxisLabelsOffsetx,
                        y:      properties.yaxisScaleInvert ? properties.marginTop : obj.canvas.height - properties.marginBottom + properties.yaxisLabelsOffsety,
                        text:   typeof properties.yaxisScaleFormatter === 'function'
                                    ? (properties.yaxisScaleFormatter)({object: this,number: 0,unitspre: properties.yaxisScaleUnitsPre,unitspost: properties.yaxisScaleUnitsPost,point: properties.yaxisScalePoint,thousand: properties.yaxisScaleThousand,formatter: properties.yaxisScaleFormatter})
                                    : zerolabel,
                        valign: typeof properties.yaxisLabelsValign === 'string' ? properties.yaxisLabelsValign : valign,
                        halign: typeof properties.yaxisLabelsHalign === 'string' ? properties.yaxisLabelsHalign : (properties.yaxisPosition === 'right' ? 'left' : halign),
                        marker: false,
                        angle:  angle,
                        tag:    'yaxis.labels'
                    });
                }
            
            //
            // Draw labels instead of a scale
            //
            } else if (properties.yaxisLabelsSpecific && properties.yaxisLabelsSpecific.length && (properties.yaxisLabelsPosition === 'section' || properties.yaxisLabelsPosition === 'edge') ) {
    
                var section        = (obj.canvas.height - properties.marginTop - properties.marginBottom) / (properties.yaxisLabelsSpecific.length - (properties.yaxisLabelsPosition === 'section' ? 0 : 1));
                obj.maxLabelLength = 0;
    
                for (var i=0; i<properties.yaxisLabelsSpecific.length; ++i) {
    
                    var y = properties.marginTop  + (section * i) + (properties.yaxisLabelsPosition === 'section' ? section / 2 : 0);
    
                    var ret = RGraph.text({
                      object:   obj,
              textConfPrefix:   'yaxisLabels',
                        x:      obj.type === 'drawing.yaxis'
                                    ? (properties.yaxisPosition === 'right' ? obj.x + 7 + properties.yaxisLabelsOffsetx : obj.x - 5 + properties.yaxisLabelsOffsetx)
                                    : (properties.yaxisPosition === 'right' ? x + properties.yaxisLabelsOffsetx + 5 : properties.marginLeft - 5 + properties.yaxisLabelsOffsetx),
                        y:      y + properties.yaxisLabelsOffsety,
                        text:   String(properties.yaxisLabelsSpecific[i] || ''),
                        valign: typeof properties.yaxisLabelsValign === 'string' ? properties.yaxisLabelsValign : 'center',
                        halign: typeof properties.yaxisLabelsHalign === 'string' ? properties.yaxisLabelsHalign : (properties.yaxisPosition === 'right' ? 'left' : 'right'),
                        marker: false,
                        tag:    'yaxis.labels',
                   cssClass:    RGraph.getLabelsCSSClassName({
                                    object: obj,
                                      name: 'yaxisLabelsClass',
                                     index: i
                                  })
                    });
    
                    obj.maxLabelLength = Math.max(
                        obj.maxLabelLength,
                        obj.context.measureText(String(properties.yaxisLabelsSpecific[i])).width * 2
                    );
                }
            }
        }



















        //
        // Draw the title
        //
        if (opt.title) {
            if (properties.yaxisTitle) {
    
                //
                // Get the text width of the labels so that the position of the title
                // can be adjusted
                //
                if (obj.type === 'gantt') {
                    for (var i=0, maxLabelLength=0; i<properties.yaxisLabels.length;++i) {
                    
                        var textConf = RGraph.getTextConf({
                            object: obj,
                            prefix: 'yaxisLabels'
                        });
    
                        maxLabelLength = Math.max(maxLabelLength, RGraph.measureText(
                            properties.yaxisLabels[i],
                            textConf.bold,
                            textConf.font,
                            textConf.size
                        )[0]);
                    }
                } else if (obj.scale2 && obj.scale2.labels) {
    
                    var textConf = RGraph.getTextConf({
                        object: obj,
                        prefix: 'yaxisLabels'
                    });
    
                    var maxLabelLength = RGraph.measureText(
                        obj.scale2.labels[obj.scale2.labels.length - 1],
                        textConf.bold,
                        textConf.font,
                        textConf.size
                    )[0];
                }
    
    
                // If the chart is an HBar chart then the maximum length of the labels
                // needs to be calculated so that the title doesn't overlap them
                if (
                        (obj.type === 'hbar' && properties.yaxisLabels && properties.yaxisLabels.length)
                     || (obj.type === 'drawing.yaxis' && properties.yaxisLabelsSpecific && properties.yaxisLabelsSpecific.length)
                   ) {
                    maxLabelLength = (function (labels)
                    {
                        var textConf = RGraph.getTextConf({
                            object: obj,
                            prefix: 'yaxisLabels'
                        });
    
                        for (var i=0,max=0; i<labels.length; ++i) {
                            var dim = RGraph.measureText(
                                labels[i],
                                textConf.bold,
                                textConf.font,
                                textConf.size
                            );
                            max = Math.max(max, dim[0]);
                        }
    
                        return max;
                    })(obj.type === 'drawing.yaxis' ? properties.yaxisLabelsSpecific : properties.yaxisLabels);
                }
    
                var x = properties.yaxisPosition === 'right' ? (obj.canvas.width - properties.marginRight) + 5 + maxLabelLength + 10 : properties.marginLeft - 5 - maxLabelLength - 10;
                var y = ((obj.canvas.height - properties.marginTop - properties.marginBottom) / 2) + properties.marginTop;
                
                if (obj.type === 'drawing.yaxis') {
                    var x = properties.yaxisPosition === 'right'
                                ? obj.x + 5 + maxLabelLength + 10
                                : obj.x - 5 - maxLabelLength - 10;
                }
    
    
                // The yaxisTitlePos property
                if (typeof properties.yaxisTitlePos === 'number') {
                    if (properties.yaxisPosition === 'right') {
                        x = obj.canvas.width - (properties.marginRight * properties.yaxisTitlePos);
                    } else {
                        x = properties.marginLeft * properties.yaxisTitlePos;
                    }
                }
    
                // Specific X and Y coordinates for the title
                if (typeof properties.yaxisTitleOffsetx === 'number') x += properties.yaxisTitleOffsetx;
                if (typeof properties.yaxisTitleOffsety === 'number') y += properties.yaxisTitleOffsety;
    
                // Specific X and Y coordinates for the title
                if (typeof properties.yaxisTitleX === 'number') x = properties.yaxisTitleX;
                if (typeof properties.yaxisTitleY === 'number') y = properties.yaxisTitleY;
    
    
    
                RGraph.text({
                  object:       obj,
                textConfPrefix: 'yaxisTitle',
                    x:          x,
                    y:          y,
                    text:       properties.yaxisTitle.toString(),
                    valign:     properties.yaxisTitleValign || 'bottom',
                    halign:     properties.yaxisTitleHalign || 'center',
                    marker:     false,
                    accessible: typeof properties.yaxisTitleAccessible === 'boolean' ? properties.yaxisTitleAccessible : undefined,
                    angle:      -45,
                    angle:      properties.yaxisPosition === 'right' ? 90 : -90,
                    tag:        'yaxis.title'
                });
            }
        }
    };








    //
    // Returns the CSS className for labels
    //
    // @param object object The RGraph object
    // @param string name   The name of the property you wish to set
    //
    RGraph.getLabelsCSSClassName = function ()
    {
        var args       = RGraph.getArgs(arguments, 'object,name,index');
        var properties = args.object.properties;
        var value      = '';

        if (typeof properties[args.name] === 'string') {
            value = properties[args.name];
        } else {
            if (typeof properties[args.name] === 'object' && typeof properties[args.name][args.index] === 'string') {
                value = properties[args.name][args.index];
            }
        }

        return value;
    };








    //
    // This function sets CSS styles on a DOM element
    //
    // @param element    mixed  This can either be a string or a DOM
    //                          object
    // @param properties object This should be an object map of
    //                          the CSS properties to set.
    //                          JavaScript property names should
    //                          be used.
    //
    RGraph.setCSS = function (element, properties)
    {
        if (typeof element === 'string') {
            element = document.getElementById(element);
        }

        for (i in properties) {
            if (typeof i === 'string') {
                element.style[i] = properties[i];
            }
        }
    };








    //
    // Label substitution. This allows you to use dynamic
    // labels if you want like this:
    //
    // ...
    // names: ['Richard','Jerry','Lucy'],
    // xaxisLabels: '%{names:[%{index}]}: %{value_formatted}'
    // ...
    //
    //@param object args This can be an object which contains the
    //                   following things:
    //                           args.text      The text on which to perform the substitution on
    //                                          (ie the original label)
    //                           args.object    The chart object
    //                           args.index     The index of the label
    //                           args.value     The value of the data point
    //                           args.decimals  The number of decimals
    //                           args.point     The decimal character
    //                           args.thousand  The thousand separator
    //                           args.unitsPre  The units that are prepended to the number
    //                           args.unitsPost The units that are appended to the number
    //                          
    //
    RGraph.labelSubstitution = function (args)
    {
        //////////////////////
        // Must be a string //
        //////////////////////
        if (RGraph.isNullish(args.text)) {
            return args.text;
        }
        var text = String(args.text);

          /////////////////////////////////////////////////////////////////
         // If there's no template tokens in the string simply reurn it //
        /////////////////////////////////////////////////////////////////
        if (!text.match(/%{.*?}/)) {
            return text;
        }

          //////////////////////////////////////////
         // This allows for escaping the percent //
        //////////////////////////////////////////
        var text = text.replace(/%%/g, '___--PERCENT--___');





          ////////////////////////////////////
         // Replace the index of the label //
        ////////////////////////////////////
        text = text.replace(/%{index}/g, args.index);



        ///////////////////////////////////////////////////////
        // Do property substitution when there's an index to //
        // the property                                      //
        ///////////////////////////////////////////////////////
        var reg = /%{prop(?:erty)?:([_a-z0-9]+)\[([0-9]+)\]}/i;

        while (text.match(reg)) {

            var property = RegExp.$1,
                index    = parseInt(RegExp.$2);

            if (args.object.properties[property]) {
                text = text.replace(
                    reg,
                    args.object.properties[property][index] || ''
                );

            // Get rid of the text if there was nothing to replace the template bit with
            } else {
                text = text.replace(reg,'');
            }
                
            RegExp.lastIndex = null;
        }


          ////////////////////////////////////
         // Replace this: %{property:xxx}% //
        ////////////////////////////////////
        while (text.match(/%{property:([_a-z0-9]+)}/i)) {
            var str = '%{property:' + RegExp.$1 + '}';
            text    = text.replace(str, args.object.properties[RegExp.$1]);
        }



         ////////////////////////////////
        // Replace this: %{prop:xxx}% //
        ///////////////////////////////
        while (text.match(/%{prop:([_a-z0-9]+)}/i)) {
            var str = '%{prop:' + RegExp.$1 + '}';
            text    = text.replace(str, args.object.properties[RegExp.$1]);
        }



          /////////////////////////////////////////////////////////
         // Replace this: %{value} and this: %{value_formatted} //
        ////////////////////////////////////////////////////////
        while (text.match(/%{value(?:_formatted)?}/i)) {
            
            var value = args.value;

            if (text.match(/%{value_formatted}/i)) {
                text = text.replace(
                    '%{value_formatted}',
                    typeof value === 'number' ? RGraph.numberFormat({
                        object:    args.object,
                        number:    value.toFixed(args.decimals),
                        thousand:  args.thousand  || ',',
                        point:     args.point     || '.',
                        unitspre:  args.unitsPre  || '',
                        unitspost: args.unitsPost || ''
                    }) : null
                );
            } else {
                text = text.replace('%{value}', value);
            }
        }
















          ////////////////////////////////////////////////////////////////
         // Do global substitution when there's an index to the global //
        ////////////////////////////////////////////////////////////////
        var reg = /%{global:([_a-z0-9.]+)\[([0-9]+)\]}/i;

        while (text.match(reg)) {

            var name  = RegExp.$1,
                index = parseInt(RegExp.$2);

            if (eval(name)[index]) {
                text = text.replace(
                    reg,
                    eval(name)[index] || ''
                );

            // Get rid of the text if there was nothing to replace the template bit with
            } else {
                text = text.replace(reg,'');
            }
                
            RegExp.lastIndex = null;
        }
















          //////////////////////////////////////////////////
         // Do global substitution when there's no index //
        //////////////////////////////////////////////////
        var reg = /%{global:([_a-z0-9.]+)}/i;

        while (text.match(reg)) {

            var name = RegExp.$1;

            if (eval(name)) {
                text = text.replace(
                    reg,
                    eval(name) || ''
                );

            // Get rid of the text if there was nothing to replace the template bit with
            } else {
                text = text.replace(reg,'');
            }
                
            RegExp.lastIndex = null;
        }
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        



        ///////////////////////////////////
        // And lastly - call any functions
        // MUST be last
        //////////////////////////////////
        var regexp = /%{function:([_A-Za-z0-9]+)\((.*?)\)}/;
        
        // Temporarily replace carriage returns and line feeds with CR and LF
        // so the the s option is not needed
        text = text.replace(/\r/,'|CR|');
        text = text.replace(/\n/,'|LF|');

        while (text.match(regexp)) {

            var str  = RegExp.$1 + '(' + RegExp.$2 + ')';
            
            for (var i=0,len=str.length; i<len; ++i) {
                str  = str.replace(/\r?\n/, "\\n");
            }
            
            RGraph.Registry.set('label-templates-function-object', args.object);
            RGraph.Registry.set('key-label-templates-function-object', args.object);

            var func = new Function ('return ' + str);
            var ret  = func();

            text = text.replace(regexp, ret)
        }







        // Replace line returns with br tags
        text = text.replace(/\r?\n/g, '<br />');
        text = text.replace(/___--PERCENT--___/g, '%')

        // Replace CR and LF with the relevant character
        text = text.replace(/\|CR\|/, '\r');
        text = text.replace(/\|LF\|/, '\n');


        return text.toString();
    };








    //
    // This splits a basic comma separated value string
    // into JSON.
    //
    // @param  string str The string to split
    // @return object     The resulting JSON object
    //
    RGraph.splitString = function (str)
    {
        var re = new RegExp('(["\'a-z0-9]+) *= *', 'ig');
        str = str.replace(re, '"$1":');

        str = str.replace(/""/g, '"');
        str = str.replace(/''/g, '"');
        str = str.replace(/:'/g, ':"');
        str = str.replace(/' *,/g, '",');
        str = str.trim().replace(/,$/, '');

        var ret = JSON.parse('{' + str + '}');

        return ret;
    };








    //
    // A set of functions which help you get data from the GET
    // string (the query string).
    //
    RGraph.GET.raw = function ()
    {
        return location.search;
    };








    RGraph.GET.parse = function ()
    {
        if (!RGraph.isNullish(RGraph.GET.__parts__)) {
            return RGraph.GET.__parts__;
        }

        var raw   = RGraph.GET.raw().replace(/^\?/, '');
        var parts = raw.split(/\&/);
        
        // Loop thru each part splitting it
        for (var i=0; i<parts.length; ++i) {
            var tmp = parts[i].split('=');

            parts[tmp[0]] = decodeURI(tmp[1]);
        }
        
        // Store the parsed query-string
        RGraph.GET.__parts__ = parts;
        
        return parts;
    };








    //
    // Get a string of text from the query string. No special
    // processing is done here.
    //
    // @param string key The part to get
    //
    RGraph.GET.text =
    RGraph.GET.string = function (key)
    {
        var parts = RGraph.GET.parse();
        
        if (!parts[key]) {
            return null;
        }

        return String(parts[key]);
    };








    //
    //  This fetches a number from the query string. It
    // trims leading zeros and reurns a number (not a
    // string).
    //
    // @param string key The part to get 
    //
    RGraph.GET.number = function (key)
    {
        var parts = RGraph.GET.parse();
        
        if (!parts[key]) {
            return null;
        }

        return Number(parts[key]);
    };








    //
    // Fetches a JSON object from the query string. It must be
    // valid JSON and is an easy way to pass multiple values
    //using the query string. For example:
    //
    // /foo.html?json={"data":[4,8,6],"labels":["John","Luis","Bob"]}
    // 
    // @param string key The part to get
    //
    RGraph.GET.json =
    RGraph.GET.object = function (key)
    {
        var parts = RGraph.GET.parse();
        
        if (!parts[key]) {
            return null;
        }

        return JSON.parse(parts[key]);
    };








    //
    // This allows you to easily pass a  list of numbers over the
    // query string. For example:
    //
    // /test.html?data=5,8,6,3,5,4,6
    //
    // @param string key      The part to get
    // @param string OPTIONAL The seperator to use (defaults to a
    //                        comma)
    //
    RGraph.GET.list  =
    RGraph.GET.array = function (key)
    {
        var parts = RGraph.GET.parse();
        
        if (!parts[key]) {
            return null;
        }
        
        
        if (!arguments[1]) {
            var sep = ',';
        } else {
            var sep = arguments[1];
        }
        
        var arr = parts[key].split(sep);
        
        // Remove any starting or trailing square brackets
        arr[0] = arr[0].replace(/^\[/, '');
        arr[arr.length - 1] = arr[arr.length - 1].replace(/\]$/, '');

        // Convert strings to numbers
        for (var i=0; i<arr.length; ++i) {
            
            // Get rid of surrounding quotes
            arr[i] = arr[i].replace(/^('|")/,'');
            arr[i] = arr[i].replace(/('|")$/,'');

            if (Number(arr[i])) {
                arr[i] = Number(arr[i]);
            }
        }

        return arr;
    };








    //
    // Start clipping the canvas. You can give various
    // different arguments to the function in order to
    // clip the canvas to suit your needs See the API
    // documentation for examples of all possibilities:
    //
    //      https://www.rgraph.net/canvas/api.html
    //
    // @param object obj        The chart object
    // @param mixed  dimensions The dimensions to clip to
    //
    RGraph.clipTo = {};
    RGraph.clipTo.start = function ()
    {
        var args = RGraph.getArgs(arguments, 'object,dimensions');
        RGraph.clipTo.object = args.object;

        // Record the state of the antialiasing flag so that it can
        // be reset to this when the RGraph.clipTo.end() function
        // is called.
        RGraph.clipTo.__rgraph_aa_translated__ = args.object.canvas.__rgraph_aa_translated__ ;

        if (RGraph.isString(args.dimensions)) {

            if (args.dimensions === 'lefthalf') {

                var graphWidth = (args.object.canvas.width - args.object.properties.marginLeft - args.object.properties.marginRight);

                args.object.path(
                    'sa b r % % % % cl',
                    0,
                    0,
                    args.object.properties.marginLeft + (graphWidth / 2),
                    args.object.canvas.height
                );
            
            } else if (args.dimensions === 'righthalf') {

                var graphWidth = (args.object.canvas.width - args.object.properties.marginLeft - args.object.properties.marginRight);

                args.object.path(
                    'sa b r % % % % cl',
                    (graphWidth / 2) + args.object.properties.marginLeft,
                    0,
                    (graphWidth / 2) + args.object.properties.marginRight,
                    args.object.canvas.height
                );



            //
            // Clip to the top part of the chart whilst taking
            // into account the margins. So you can have different
            // margin sizes and still clip to the top part of
            // the chart
            //
            } else if (args.dimensions === 'tophalf') {

                var graphHeight = args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom;

                args.object.path(
                    'sa b r % % % % cl',
                    0,
                    0,
                    args.object.canvas.width,
                    args.object.properties.marginTop + (graphHeight / 2)
                );





            //
            // Clip to the bottom part of the chart whilst taking
            // into account the margins. So you have different
            // margin sizes and still clip to the bottom part of
            // the chart
            //
            } else if (args.dimensions === 'bottomhalf') {

                var graphHeight = args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom;

                args.object.path(
                    'sa b r % % % % cl',
                    0,
                    (graphHeight / 2) + args.object.properties.marginTop,
                    args.object.canvas.width,
                    (graphHeight / 2) + args.object.properties.marginBottom
                );



            // Clip to horizontal percentages
            } else if (args.dimensions.match(/^x:([-.0-9minax]+)%?-([.0-9minax]+)%?$/i)) {


                // Accommodate the min/max keywords
                var from = RegExp.$1,
                    to   = RegExp.$2;

                from = from.replace(/min/, '-200');
                from = from.replace(/max/, '200');
                to   = to.replace(/min/, '-200');
                to   = to.replace(/max/, '200');

                from   = Number(from);
                to     = Number(to);

                var width = ((to - from)  / 100) * (args.object.canvas.width - args.object.properties.marginLeft - args.object.properties.marginRight),
                    height = args.object.canvas.height,
                    x      = (from  / 100) * (args.object.canvas.width - args.object.properties.marginLeft - args.object.properties.marginRight) + args.object.properties.marginLeft,
                    y      = 0;

                    args.object.path(
                        'sa b r % % % % cl',
                        x,y,width,height
                    );
            
            
            // Clip to vertical percentages
            } else if (args.dimensions.match(/^y:([-.0-9minax]+)%?-([.0-9minax]+)%?/i)) {
    
                // Accommodate the min/max keywords
                var from = RegExp.$1,
                    to   = RegExp.$2;

                from = from.replace(/min/, '-200');
                from = from.replace(/max/, '200');
                to   = to.replace(/min/, '-200');
                to   = to.replace(/max/, '200');

                from   = Number(from);
                to     = Number(to);

                var height = args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom,
                    x      = 0,
                    y      = (from / 100) * height + args.object.properties.marginTop,
                    width  = args.object.canvas.width,
                    //y1     = ((to - from) / 100) * (args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom),
                    y2     = (to / 100) * (args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom),
                    y      = args.object.canvas.height - args.object.properties.marginBottom - y2,
                    height = (args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom) * ( (to - from) / 100);
                
                args.object.path(
                    'sa ' + 'b r % % % %' + ' cl',
                    x,y,width,height
                );




            // Clip to radial percentages. This only works with
            // charts that have the centerx and centery properties
            } else if (args.dimensions.match(/^r(?:adius)?:([-.0-9minax]+)%?-([.0-9minax]+)%?/i)) {

                // Accommodate the min/max keywords
                var from = RegExp.$1,
                    to   = RegExp.$2;

                from = from.replace(/min/, '0');
                from = from.replace(/max/, '2000');
                to   = to.replace(/min/, '0');
                to   = to.replace(/max/, '2000');

                from   = Number(from);
                to     = Number(to);

                // Get the radius, centerx and centery from the
                // object
                if (!RGraph.isNumber(args.object.centerx) || !RGraph.isNumber(args.object.centery) || !RGraph.isNumber(args.object.radius)) {
                    alert('[RGRAPH CLIPPING] To use the r: syntax the object (Type: {1}, ID: {2}, UID: {3}) must support the centerx, centery and radius properties.'.format(
                        args.object.type,
                        args.object.id,
                        args.object.uid
                    ));
                }

                var centerx = args.object.centerx,
                    centery = args.object.centery,
                    r1      = (from / 100) * args.object.radius,
                    r2      = (to / 100) * args.object.radius;

                args.object.path(
                    'sa ' + 'b    a % % % 0 6.29 false    a % % % 6.29 0 true    cl',
                    centerx, centery, r1,
                    centerx, centery, r2
                );
















            // Clip to a SEGMENT
            } else if (args.dimensions.match(/^(?:segment|arc): *([-.0-9degrad]+) *, *([-.0-9degrad]+) *, *([-.0-9degrad]+) *, *([-.0-9degrad]+) *, *([-.0-9degrad]+)$/i)) {
                
                var centerx = RegExp.$1,
                    centery = RegExp.$2,
                    radius  = RegExp.$3,
                    start   = RegExp.$4,
                    end     = RegExp.$5;

                // If radians has been stipulated then get rid of it
                start = start.replace(/rad$/, '');
                end   = end.replace(/rad$/, '');
                
                // Convert degrees to radians for the start angle
                if (start.match(/deg$/)) {
                    start = parseFloat(start);
                    start = start * (RGraph.PI / 180);
                }
                
                // Convert degrees to radians for the end angle
                if (end.match(/deg$/)) {
                    end = parseFloat(end);
                    end = end * (RGraph.PI / 180);
                }

                args.object.path(
                    'sa b    m % %    a % % % % % false  c  cl',
                    centerx, centery,
                    centerx, centery, radius, start - RGraph.HALFPI, end - RGraph.HALFPI
                );

















            // Clip to a CIRCLE
            } else if (args.dimensions.match(/^circle: *([-.0-9]+) *, *([-.0-9]+) *, *([-.0-9]+)$/i)) {
                
                var centerx = RegExp.$1,
                    centery = RegExp.$2,
                    radius  = RegExp.$3;

                args.object.path(
                    `sa b    a % % % 0 ${RGraph.TWOPI} false  s #ddd    cl`,
                    centerx, centery, radius,
                );















            //
            // Clip to scale values - since all of the
            // charts handle scales differently this is
            // handled by worker functions on each object
            //
            // IF YOU UPDATE THIS REGEXP THEN IT NEEDS TO BE
            // UPDATED FURTHER DOWN IN THE CODE TOO
            //
            } else if (args.dimensions.match(/^(?:scale:) *([-.0-9min]+) *- *([-.0-9max]+) *$/)) {

                if (args.object.clipToScaleWorker) {
                    args.object.clipToScaleWorker(args.object.properties.clip);
                } else {
                    console.log('The scale: clipping option isn\'t implemented for this chart type (' + args.object.type + ')');
                }


            // Clip to an RGraph path
            } else {
                args.object.path('sa');
                args.object.path(args.dimensions);
                args.object.path('cl');
            }
        
        // Clip to an array of coordinates eg:
        // [[0,0],[0,250],[600,250],[600,0]]
        } else if (RGraph.isArray(args.dimensions) && RGraph.isArray(args.dimensions[0])) {

            for (var i=0,path=[]; i<args.dimensions.length; ++i) {
                path.push('%1 %2 %3'.format(
                        i === 0 ? 'm' : 'l',
                        args.dimensions[i][0],
                        args.dimensions[i][1]
                    )
                );
            }

            // Save the path so it doesn't have to be rebuilt 
            // if/when it comes to testing it
            RGraph.clipTo.path = 'b ' + path.join(' ');

            // Build the string path
            path = 'sa ' + RGraph.clipTo.path + ' cl';

            // Run the path
            args.object.path(path);

        // Clip to a single-dimension array of x/y/width/height
        // (x/y/w/h) A rectangle in other words.
        } else if (RGraph.isArray(args.dimensions)) {
            args.object.path(
                'sa b r % % % % cl',
                args.dimensions[0],
                args.dimensions[1],
                args.dimensions[2],
                args.dimensions[3]
            );
        }
    };








    //
    // Ends clipping that has been started by the
    // RGraph.clipTo.start() function
    //
    RGraph.clipTo.end = function ()
    {
        if (RGraph.clipTo.object){
            // Reset the antialiasing flag to what it was when
            // we started
            RGraph.clipTo.object.canvas.__rgraph_aa_translated__ = RGraph.clipTo.__rgraph_aa_translated__;
    
            RGraph.path(RGraph.clipTo.object, 'rs');
        }
    };








    //
    // Test whether a point is within the clipped area or not
    //
    // @param  object  obj The chart object
    // @param  integer x   The X coordinate to test
    // @param  integer y   The Y coordinate to test
    // @return boolean     true/false whether the point is
    //                     within the path
    //
    RGraph.clipTo.test = function ()
    {
        var args = RGraph.getArgs(arguments, 'object,x,y');




        if (args.object.properties.clip) {
        
            var clip = args.object.properties.clip;

            // Test that the cursor is over the left half
            if (clip === 'lefthalf') {
                if (   args.x > 0
                    && args.x < (args.object.canvas.width / 2)
                    && args.y > 0
                    && args.y < args.object.canvas.height
                   ) {
                    return true;
                }




            // Test that the cursor is over the right half
            } else if (clip === 'righthalf') {
                if (   args.x > (args.object.canvas.width / 2)
                    && args.x < args.object.canvas.width
                    && args.y > 0
                    && args.y < args.object.canvas.height
                   ) {
                    return true;
                }




            // Test that the cursor is over the top half
            } else if (clip === 'tophalf') {
                if (   args.x > 0
                    && args.x < args.object.canvas.width
                    && args.y > 0
                    && args.y < (args.object.canvas.height / 2)
                   ) {
                    return true;
                }




            // Test that the cursor is over the bottom half
            } else if (clip === 'bottomhalf') {
                if (   args.x > 0
                    && args.x < args.object.canvas.width
                    && args.y > (args.object.canvas.height / 2)
                    && args.y < args.object.canvas.height
                   ) {
                    return true;
                }




            // Test that the cursor is within the clipped rect
            } else if (
                          RGraph.isArray(clip)
                       && RGraph.isNumber(clip[0])
                       && RGraph.isNumber(clip[1])
                       && RGraph.isNumber(clip[2])
                       && RGraph.isNumber(clip[3])
                      ) {

                if (   args.x > clip[0]
                    && args.x < (clip[0] + clip[2])
                    && args.y > clip[1]
                    && args.y < (clip[1] + clip[3])
                   ) {
                    return true;
                }









            // Test that the cursor is within the clipped path
            } else if (RGraph.isArray(clip) && RGraph.isArray(clip[0])) {

                args.object.path(RGraph.clipTo.path);
                return args.object.context.isPointInPath(args.x, args.y);




            // Test that the cursor is within the given X percentages range
            } else if (clip.match(/^x:([-.0-9minax]+)%?-([.0-9minax]+)%?$/i)) {
            
                // Accommodate the min/max keywords
                var from = RegExp.$1,
                    to   = RegExp.$2;
            
                from = from.replace(/min/, '-200');
                from = from.replace(/max/, '200');
                to   = to.replace(/min/, '-200');
                to   = to.replace(/max/, '200');
            
                from   = Number(from);
                to     = Number(to);
                
                var width  = ((to - from)  / 100) * (args.object.canvas.width - args.object.properties.marginLeft - args.object.properties.marginRight),
                    height = args.object.canvas.height,
                    x      = (from  / 100) * (args.object.canvas.width - args.object.properties.marginLeft - args.object.properties.marginRight) + args.object.properties.marginLeft,
                    y      = 0;
            
                args.object.path(
                    'b r % % % %',
                    x,y,width,height
                );
            
                return args.object.context.isPointInPath(args.x, args.y);


            // Test that the cursor is within the given Y percentages range
            } else if (clip.match(/^y:([-.0-9minax]+)%?-([.0-9minax]+)%?/i)) {

                // Accommodate the min/max keywords
                var from = RegExp.$1,
                    to   = RegExp.$2;
            
                from = from.replace(/min/, '-200');
                from = from.replace(/max/, '200');
                to   = to.replace(/min/, '-200');
                to   = to.replace(/max/, '200');
            
                from   = Number(from);
                to     = Number(to);
            
                var height = args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom,
                    x      = 0,
                    y      = (from / 100) * height + args.object.properties.marginTop,
                    width  = args.object.canvas.width,
                    //y1     = ((to - from) / 100) * (args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom),
                    y2     = (to / 100) * (args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom),
                    y      = args.object.canvas.height - args.object.properties.marginBottom - y2,
                    height = (args.object.canvas.height - args.object.properties.marginTop - args.object.properties.marginBottom) * ( (to - from) / 100);
                
                    args.object.path(
                        'b r % % % %',
                        x,y,width,height
                    );

                return args.object.context.isPointInPath(args.x, args.y);



















            // Test that the cursor is within the given radius
            // percentages range
            } else if (clip.match(/^r:([-.0-9minax]+)%?-([.0-9minax]+)%?$/i)) {
            
                // Accommodate the min/max keywords
                var from = RegExp.$1,
                    to   = RegExp.$2;
            
                from = from.replace(/min/, '0');
                from = from.replace(/max/, '2000');
                to   = to.replace(/min/, '0');
                to   = to.replace(/max/, '2000');

                from = Number(from);
                to   = Number(to);

                // Get the radius, centerx and centery from the
                // object
                if (!RGraph.isNumber(args.object.centerx) || !RGraph.isNumber(args.object.centery) || !RGraph.isNumber(args.object.radius)) {
                    alert('[RGRAPH CLIPPING] To use the r: syntax the object (Type: {1}, ID: {2}, UID: {3}) must support the centerx, centery and radius properties.'.format(
                        args.object.type,
                        args.object.id,
                        args.object.uid
                    ));
                }
                
                var centerx = args.object.centerx,
                    centery = args.object.centery,
                    r1      = (from / 100) * args.object.radius,
                    r2      = (to / 100) * args.object.radius;

                args.object.path(
                    'sa ' + 'b    a % % % 0 6.29 false    a % % % 6.29 0 true    cl',
                    centerx, centery, r1,
                    centerx, centery, r2
                );

                return args.object.context.isPointInPath(args.x, args.y);












            // Test that the cursor is within the clipped scale
            //
            // IF YOU UPDATE THIS REGEXP THEN IT NEEDS TO BE
            // UPDATED FURTHER UP IN THE CODE TOO
            //
            } else if (clip.match(/^(?:scale:) *([-.0-9min]+) *- *([-.0-9max]+) *$/)) {
                if (args.object.clipToScaleTestWorker) {
                    args.object.clipToScaleTestWorker(clip);
                    
                    return args.object.context.isPointInPath(args.x, args.y);
                } else {
                    console.log('The scale: clipping option isn\'t implemented for this chart type (' + args.object.type + ')');
                }
            }
        }
    };







    //
    // Parses a string such as this:
    //
    // foo=bar,foo2=bar2,foo3="bar 3"
    //
    // @paramstr string The string to parse
    // @return          An object of the results
    //
    RGraph.parseConfigString = function ()
    {
        var args = RGraph.getArgs(arguments, 'string,separator');

        // Default to a comma
        args.separator ??= ',';

        var insideQuote = false,
            entries     = [],
            entry       = [],
            parts       = {};
        
        args.string.split('').forEach(function (character)
        {
            if(character === '"' || character === "'") {
                insideQuote = !insideQuote;
            } else {

                if(character === args.separator && !insideQuote) {
                    entries.push(entry.join(''));
                    entry = [];
                } else {
                    entry.push(character);
                }
            }
        });

        entries.push(entry.join(''));                                        

        for (let i=0; i<entries.length; ++i) {
            var index = entries[i].indexOf('=');
            var name  = entries[i].substr(0, index);
            var value = entries[i].substr(index + 1);

            // Convert the value to a number type if it looks
            // like a number
            if (RGraph.isNumeric(value)) {                
                value = Number(value);
            }
            

            parts[name] = value;
        }
        
        return parts;
    };







    //
    // This function allows the drawing of custom lines
    //
    RGraph.drawHorizontalLines = function (obj)
    {
        var lines = obj.properties.horizontalLines,
            avg,x,y,label,halign,valign,hmargin = 10,vmargin = 5,
            position,textFont,textSize,textColor,textBold,textItalic,
            data,linewidth;

        if (lines) {

            //
            // Set some defaults for the configuration of
            // each line
            //
            var defaults = {
                dotted:        false,
                dashed:        true,
                color:         '#666', // Same as labelColor property below
                linewidth:     1,
                label:         'Average (%{value})',
                labelPosition: 'top right',
                labelColor:    '#666', // Same as color property above
                labelValueDecimals: 2,
                labelOffsetx:       0,
                labelOffsety:       0
            };
        
        
            // Loop through each line to be drawn
            for (let i=0; i<obj.properties.horizontalLines.length; ++i) {

                var conf       = lines[i],
                    textFont   = conf.labelFont  || obj.properties.textFont,
                    textColor  = conf.labelColor || defaults.labelColor,
                    textSize   = conf.labelSize  || obj.properties.textSize - 4,
                    textBold   = typeof conf.labelBold   === 'boolean' ? conf.labelBold   : obj.properties.textBold,
                    textItalic = typeof conf.labelItalic === 'boolean' ? conf.labelItalic : obj.properties.textItalic;








                switch (obj.type) {
                    case 'line':
                        // Calculate the Y coord if we've been
                        // given a numeric value
                        if (typeof conf.value === 'number') {
                            y = obj.getYCoord(conf.value);
                        
                        } else if(conf.value === 'average') {
                            avg        = RGraph.arraySum(obj.original_data[0]) /  obj.original_data[0].length;
                            y          = obj.getYCoord(avg);
                        }
                        break;
                        
                    
                    
                    case 'bar':
                        // Calculate the Y coord if we've been
                        // given a numeric value
                        if (typeof conf.value === 'number') {
                            y = obj.getYCoord(conf.value);

                        } else if (conf.value === 'average') {

                            // Calculate the average value of all
                            // of the
                            // values. Grouped charts are treated
                            // slightly differently to stacked
                            // charts.
                            var total = 0;
                            obj.data.map(v => {

                                if (RGraph.isNumber(v)) {
                                    total += v;
                                } else if (RGraph.isArray(v)) {
                                    total += RGraph.arraySum(v);
                                }
                            });


                            var num = 0;
                            for (let i=0; i<obj.data.length; ++i) {
                                if (obj.properties.grouping === 'grouped') {
                                    num += RGraph.isArray(obj.data[i]) ? obj.data[i].length : 1 ;
                                } else if (obj.properties.grouping === 'stacked') {
                                    ++num;
                                }
                            }

                            avg        = total / num;
                            y          = obj.getYCoord(avg);


                        }
                        break;






                    case 'scatter':
                        // Calculate the Y coord if we've been
                        // given a numeric value
                        if (typeof conf.value === 'number') {
                            y = obj.getYCoord(conf.value);
                        } else if (conf.value === 'average') {
                            // Use the map() function to get an
                            // average value from the first dataset
                            var sum = 0;
                            obj.data[0].map(v => sum += parseFloat(v[1]));

                            avg = sum / obj.data[0].length;
                            y   = obj.getYCoord(avg);
                        }
                        break;
                }










                //
                // Dotted or dashed lines
                //
                linedash = '[1,1]';

                if (conf.dotted === true) {
                    linedash = '[1,3]';
                }
                
                if (conf.dashed === true || (typeof conf.dashed === 'undefined' && typeof conf.dotted === 'undefined') ) {
                    linedash = '[6,6]';
                }










                //
                // Draw the line
                //
                obj.path(
                    'lw % ld % b m % % l % % s %',
                    typeof conf.linewidth === 'number' ? conf.linewidth : defaults.linewidth,
                    linedash,
                    obj.properties.marginLeft,y,
                    obj.canvas.width - obj.properties.marginRight,y,
                    conf.color || defaults.color
                );





                //
                // Draw the label
                //




                // These chart types only
                if (['bar','line','scatter'].includes(obj.type)) {
                    
                    
                    // Default pos for the label
                    if (!conf.labelPosition) {
                        conf.labelPosition = defaults.labelPosition;
                    }



                    if (typeof conf.labelPosition === 'string' && conf.labelPosition.indexOf('left') >= 0) {
                        textX  = obj.marginLeft + hmargin;
                        halign = 'left';
                    } else if (typeof conf.labelPosition === 'string' && conf.labelPosition.indexOf('center') >= 0) {
                        textX  = ((obj.canvas.width - obj.properties.marginLeft - obj.properties.marginRight) / 2) + obj.properties.marginLeft;
                        halign = 'center';
                    } else if (typeof conf.labelPosition === 'string' && conf.labelPosition === 'right-margin') {
                        textX  = obj.canvas.width - obj.properties.marginRight + hmargin;
                        halign = 'left';
                    } else if (typeof conf.labelPosition === 'string' && conf.labelPosition ==='left-margin') {
                        textX  = obj.properties.marginLeft - hmargin;
                        halign = 'right';
                    } else {
                        textX  = obj.canvas.width - obj.marginRight - hmargin;
                        halign = 'right';
                    }


                    if (typeof conf.labelPosition === 'string' && conf.labelPosition.indexOf('bottom') >= 0) {
                        textY  = y + vmargin;
                        valign = 'top';
                    } else if (typeof conf.labelPosition === 'string' && conf.labelPosition === 'right-margin') {
                        textY  = y;
                        valign = 'center';
                    } else if (typeof conf.labelPosition === 'string' && conf.labelPosition === 'left-margin') {
                        textY  = y;
                        valign = 'center';
                    } else {
                        textY  = y - vmargin;
                        valign = 'bottom';
                    }
                }
                
                // Account for linewidth
                linewidth = typeof conf.linewidth === 'number' ? conf.linewidth : defaults.linewidth;

                var num = RGraph.numberFormat({
                    object:    obj,
                    number:    (typeof conf.value === 'number' ? conf.value : avg).toFixed(typeof conf.labelValueDecimals === 'number' ? conf.labelValueDecimals : defaults.labelValueDecimals),
                    unitspre:  conf.labelValueUnitsPre,
                    unitspost: conf.labelValueUnitsPost,
                    thousand:  conf.labelValueThousand,
                    point:     conf.labelValuePoint,
                    formatter: conf.labelValueFormatter,
                    decimals:  conf.labelValueDecimals
                });




                //
                // Draw the label
                //

                RGraph.text({
                    object:     obj,
                    text:       (typeof conf.label === 'string' ? conf.label : defaults.label).replace('%{value}', num),
                    x:          textX + parseFloat(conf.labelOffsetx || defaults.labelOffsetx),
                    y:          textY - (linewidth / 2) + parseFloat((conf.labelPosition.indexOf('top') !== -1 ? (-1 * conf.labelOffsety) : conf.labelOffsety) || defaults.labelOffsety),
                    valign:     valign,
                    halign:     halign,
                    size:       textSize,
                    font:       textFont,
                    color:      textColor,
                    italic:     textItalic,
                    bold:       textBold,
                    bounding:   true,
                    boundingFill: 'rgba(255,255,255,0.75)',
                    boundingStroke: 'transparent'
                });
            }
        }
    };








    //
    // This function allows you to run a function once (immediately)
    // Future calls using the same identifier are not run.
    //
    // @param obj  object   The chart object
    // @param id   string   A unique identifier used to identifier
    //                      this function
    // @param func function The function to call
    // @return              The return value of the function
    //
    RGraph.runOnce = function (id, func)
    {
        if (RGraph.Registry.get('rgraph-runonce-functions')[id]) {
            return;
        }

        RGraph.Registry.get('rgraph-runonce-functions')[id] = func;
        
        return func();
    };








    //
    // This function implements the logic for whether to ignore
    // a particular hotspot or not
    //
    // @param object obj   The chart object
    // @param number index The Index of the hotspot to check for
    //
    RGraph.tooltipsHotspotIgnore = function (obj, index)
    {
        if (   (RGraph.isBoolean(obj.properties.tooltipsHotspotIgnore) && obj.properties.tooltipsHotspotIgnore)
            || (RGraph.isNumber(obj.properties.tooltipsHotspotIgnore)  && obj.properties.tooltipsHotspotIgnore === index)
            || (RGraph.isArray(obj.properties.tooltipsHotspotIgnore)   && obj.properties.tooltipsHotspotIgnore.includes(index))
            || (RGraph.isArray(obj.properties.tooltipsHotspotIgnore)   && obj.properties.tooltipsHotspotIgnore[index] === true)) {
            
            return true;
        }

        return false;
    };








    // Formatted version of a popular md5 implementation
    // Thanks to Paul Johnston & Greg Holt.
    //
    // @param string str The string to hash
    //
    RGraph.md5 = function (str)
    {
        var hc="0123456789abcdef";
        function rh(n) {var j,s="";for(j=0;j<=3;j++) s+=hc.charAt((n>>(j*8+4))&0x0F)+hc.charAt((n>>(j*8))&0x0F);return s;}
        function ad(x,y) {var l=(x&0xFFFF)+(y&0xFFFF);var m=(x>>16)+(y>>16)+(l>>16);return (m<<16)|(l&0xFFFF);}
        function rl(n,c)            {return (n<<c)|(n>>>(32-c));}
        function cm(q,a,b,x,s,t)    {return ad(rl(ad(ad(a,q),ad(x,t)),s),b);}
        function ff(a,b,c,d,x,s,t)  {return cm((b&c)|((~b)&d),a,b,x,s,t);}
        function gg(a,b,c,d,x,s,t)  {return cm((b&d)|(c&(~d)),a,b,x,s,t);}
        function hh(a,b,c,d,x,s,t)  {return cm(b^c^d,a,b,x,s,t);}
        function ii(a,b,c,d,x,s,t)  {return cm(c^(b|(~d)),a,b,x,s,t);}
        function sb(x) {
            var i;var nblk=((x.length+8)>>6)+1;var blks=new Array(nblk*16);for(i=0;i<nblk*16;i++) blks[i]=0;
            for(i=0;i<x.length;i++) blks[i>>2]|=x.charCodeAt(i)<<((i%4)*8);
            blks[i>>2]|=0x80<<((i%4)*8);blks[nblk*16-2]=x.length*8;return blks;
        }
        var i,x=sb(""+str),a=1732584193,b=-271733879,c=-1732584194,d=271733878,olda,oldb,oldc,oldd;
        for(i=0;i<x.length;i+=16) {olda=a;oldb=b;oldc=c;oldd=d;
            a=ff(a,b,c,d,x[i+ 0], 7, -680876936);d=ff(d,a,b,c,x[i+ 1],12, -389564586);c=ff(c,d,a,b,x[i+ 2],17,  606105819);
            b=ff(b,c,d,a,x[i+ 3],22,-1044525330);a=ff(a,b,c,d,x[i+ 4], 7, -176418897);d=ff(d,a,b,c,x[i+ 5],12, 1200080426);
            c=ff(c,d,a,b,x[i+ 6],17,-1473231341);b=ff(b,c,d,a,x[i+ 7],22,  -45705983);a=ff(a,b,c,d,x[i+ 8], 7, 1770035416);
            d=ff(d,a,b,c,x[i+ 9],12,-1958414417);c=ff(c,d,a,b,x[i+10],17,     -42063);b=ff(b,c,d,a,x[i+11],22,-1990404162);
            a=ff(a,b,c,d,x[i+12], 7, 1804603682);d=ff(d,a,b,c,x[i+13],12,  -40341101);c=ff(c,d,a,b,x[i+14],17,-1502002290);
            b=ff(b,c,d,a,x[i+15],22, 1236535329);a=gg(a,b,c,d,x[i+ 1], 5, -165796510);d=gg(d,a,b,c,x[i+ 6], 9,-1069501632);
            c=gg(c,d,a,b,x[i+11],14,  643717713);b=gg(b,c,d,a,x[i+ 0],20, -373897302);a=gg(a,b,c,d,x[i+ 5], 5, -701558691);
            d=gg(d,a,b,c,x[i+10], 9,   38016083);c=gg(c,d,a,b,x[i+15],14, -660478335);b=gg(b,c,d,a,x[i+ 4],20, -405537848);
            a=gg(a,b,c,d,x[i+ 9], 5,  568446438);d=gg(d,a,b,c,x[i+14], 9,-1019803690);c=gg(c,d,a,b,x[i+ 3],14, -187363961);
            b=gg(b,c,d,a,x[i+ 8],20, 1163531501);a=gg(a,b,c,d,x[i+13], 5,-1444681467);d=gg(d,a,b,c,x[i+ 2], 9,  -51403784);
            c=gg(c,d,a,b,x[i+ 7],14, 1735328473);b=gg(b,c,d,a,x[i+12],20,-1926607734);a=hh(a,b,c,d,x[i+ 5], 4,    -378558);
            d=hh(d,a,b,c,x[i+ 8],11,-2022574463);c=hh(c,d,a,b,x[i+11],16, 1839030562);b=hh(b,c,d,a,x[i+14],23,  -35309556);
            a=hh(a,b,c,d,x[i+ 1], 4,-1530992060);d=hh(d,a,b,c,x[i+ 4],11, 1272893353);c=hh(c,d,a,b,x[i+ 7],16, -155497632);
            b=hh(b,c,d,a,x[i+10],23,-1094730640);a=hh(a,b,c,d,x[i+13], 4,  681279174);d=hh(d,a,b,c,x[i+ 0],11, -358537222);
            c=hh(c,d,a,b,x[i+ 3],16, -722521979);b=hh(b,c,d,a,x[i+ 6],23,   76029189);a=hh(a,b,c,d,x[i+ 9], 4, -640364487);
            d=hh(d,a,b,c,x[i+12],11, -421815835);c=hh(c,d,a,b,x[i+15],16,  530742520);b=hh(b,c,d,a,x[i+ 2],23, -995338651);
            a=ii(a,b,c,d,x[i+ 0], 6, -198630844);d=ii(d,a,b,c,x[i+ 7],10, 1126891415);c=ii(c,d,a,b,x[i+14],15,-1416354905);
            b=ii(b,c,d,a,x[i+ 5],21,  -57434055);a=ii(a,b,c,d,x[i+12], 6, 1700485571);d=ii(d,a,b,c,x[i+ 3],10,-1894986606);
            c=ii(c,d,a,b,x[i+10],15,   -1051523);b=ii(b,c,d,a,x[i+ 1],21,-2054922799);a=ii(a,b,c,d,x[i+ 8], 6, 1873313359);
            d=ii(d,a,b,c,x[i+15],10,  -30611744);c=ii(c,d,a,b,x[i+ 6],15,-1560198380);b=ii(b,c,d,a,x[i+13],21, 1309151649);
            a=ii(a,b,c,d,x[i+ 4], 6, -145523070);d=ii(d,a,b,c,x[i+11],10,-1120210379);c=ii(c,d,a,b,x[i+ 2],15,  718787259);
            b=ii(b,c,d,a,x[i+ 9],21, -343485551);a=ad(a,olda);b=ad(b,oldb);c=ad(c,oldc);d=ad(d,oldd);
        }
        return rh(a)+rh(b)+rh(c)+rh(d);
    };








    //
    // This is an easy shortcut function that paths a line in
    // the form of an array of coordinate pairs. The first
    // coordinate is moved to and then the rest are lined to.
    // No beginpath or stroking/filling is done.
    //
    // @param object  context The context
    // @param array   coords  An array of coordinate pairs
    // @param boolean moveto  Whether to moveTo the first
    //                        point or lineTo
    //
    RGraph.pathLine = function ()
    {
        var args = RGraph.getArgs(arguments, 'context,coords,moveto,reverse');

        if (args.reverse) {
            args.coords = RGraph.arrayReverse(args.coords);
        }

        // Path the line

        for (var i=0; i<args.coords.length; ++i) {
            if (i === 0 && args.moveto !== false) {
                args.context.moveTo(args.coords[i][0], args.coords[i][1]);
            } else {
                args.context.lineTo(args.coords[i][0], args.coords[i][1]);
            }
        }
    };








    //
    // This is an easy shortcut function that draws a line in
    // on to the canvas - not just pathing a line like the
    // above function.
    //
    // @param object  context The context
    // @param array   coords  An array of coordinate pairs
    // @param boolean moveto  Whether to moveTo the first
    //                        point or lineTo
    // @param boolean stroke  Whether to stroke the line or not
    // @param boolean fill    Whether to fill the line or not
    //
    RGraph.drawLine = function ()
    {
        var args = RGraph.getArgs(arguments, 'context,coords,moveto,stroke');

        // Start
        args.context.beginPath();
        
            // Path the line on to the canvas
            RGraph.pathLine({
                context: args.context,
                coords:  args.coords,
                moveto:  args.moveto
            });
            
            // Linewidth
            if (args.linewidth) {
                args.context.lineWidth = args.linewidth;
            }
            
            // Fill the line
            if (args.fill) {
                args.context.fillStyle = args.fill;
                args.context.fill();
            }
            
            // Stroke the line
            if (args.stroke) {
                args.context.strokeStyle = args.stroke;
                args.context.stroke();
            }
            
    };







    // Some utility functions that help identify the type of an object
    //
    // Note that isUndefined() should be used like this or you'll get an
    // error (with the window. prefix):
    //
    //        RGraph.isUndefined(window.foo)
    //
    RGraph.isString    = function(obj){return typeof obj === 'string';};
    RGraph.isNumber    = function(obj){return typeof obj === 'number';};
    RGraph.isTextual   = function(obj){return (typeof obj === 'string' || typeof obj === 'number');};
    RGraph.isNumeric   = function(value){value=String(value);return Boolean(value.match(/^[-.0-9]+$/))||Boolean(value.match(/^[-.0-9]+e[-0-9]+$/))||(value==='Infinity')||(value==='-Infinity')||Boolean(value.match(/^[-.0-9]+x[0-9a-f]+$/i));};
    RGraph.isBool      =
    RGraph.isBoolean   = function(obj){return typeof obj === 'boolean';};
    //RGraph.isArray Defined above
    RGraph.isObject    = function(obj){return (obj && typeof obj === 'object' && obj.constructor.toString().toLowerCase().indexOf('object') > 0) ? true : false;};
    //RGraph.isNull  Defined above
    RGraph.isFunction  = function (obj){return typeof obj === 'function';};
    RGraph.isUndefined = function (obj){return typeof obj === 'undefined';};








// End module pattern
})(window, document);








    //
    // Uses the alert() function to show the structure of the given variable
    // 
    // @param mixed v The variable to print/alert the structure of
    //
    window.$p = function (v)
    {
        RGraph.pr(arguments[0], arguments[1], arguments[3]);
    };








    //
    // A shorthand for the default alert() function
    //
    // @param mixed v The variable to alert
    //
    window.$a = function (v)
    {
        if (arguments.length > 1) {
            var args = [];
            
            for (let i=0; i<arguments.length; ++i) {
                args.push(arguments[i]);
                args.push('    ');
            }
            
            // Get rid of the last element - which is spaces
            args.pop();
            
            alert(args);
        } else {
            alert(v);
        }
    };








    //
    // Short-hand for console.log
    // 
    // @param mixed v The variable to log to the console
    //
    window.$c  =
    window.$cl = function (v)
    {
        return console.log(v);
    };








    //
    // A debug function that takes the message that you give and if a textarea output
    // window doesn't yet exists creates one and prepends the msg to it.
    // 
    // @param mixed v The variable to log to the window
    //
    window.$d = function (m)
    {
        var width  = 600;
        var height = 600;

        // Create the debug window if necessary
        if (!this.rgraph_debug_textarea) {
                this.rgraph_debug_textarea = document.createElement('textarea');
                this.rgraph_debug_textarea.style.position = 'fixed';
                this.rgraph_debug_textarea.style.left     = '10px';
                this.rgraph_debug_textarea.style.top      = '10px';
                this.rgraph_debug_textarea.style.width    = width + 'px';
                this.rgraph_debug_textarea.style.height   = height + 'px';
                this.rgraph_debug_textarea.style.opacity  = '0.25';
                this.rgraph_debug_textarea.style.zIndex   = '99999';
                this.rgraph_debug_textarea.style.border   = '2px solid black';
                this.rgraph_debug_textarea.style.backgroundColor = 'yellow'
                this.rgraph_debug_textarea.style.color = 'black';
                this.rgraph_debug_textarea.style.fontSize = '16pt';
                this.rgraph_debug_textarea.style.fontWeight = 'bold';
                this.rgraph_debug_textarea.wrap = 'off';
            document.body.appendChild(this.rgraph_debug_textarea);
            
            // Restore the W/H coords from localstorage
            if  (window.localStorage.rgraph_debug_textarea_w) this.rgraph_debug_textarea.style.width   = window.localStorage.rgraph_debug_textarea_w;
            if  (window.localStorage.rgraph_debug_textarea_h) this.rgraph_debug_textarea.style.height  = window.localStorage.rgraph_debug_textarea_h;

            this.rgraph_debug_textarea.onmouseover   = function (e) {e.target.style.opacity = 1;};
            this.rgraph_debug_textarea.onmousedown   = function (e) {if (e.ctrlKey) {e.preventDefault();e.stopPropagation();this.mousedown = true;this.pickupX = e.offsetX;this.pickupY = e.offsetY; return false;}};
            this.rgraph_debug_textarea.onmouseout    = function (e) {this.style.opacity = 0.25;};
            this.rgraph_debug_textarea.ondblclick = function (e)
            {
                if (confirm('Clear the log?')) {
                    this.value = '';
                }
            };
            
            window.onmouseup = function (e) {this.rgraph_debug_textarea.mousedown = false;};
            window.addEventListener('mousemove', function (e)
            {
                if (this.rgraph_debug_textarea.mousedown) {
                    window.localStorage.rgraph_debug_textarea_w = this.rgraph_debug_textarea.offsetWidth + 'px';
                    window.localStorage.rgraph_debug_textarea_h = this.rgraph_debug_textarea.offsetHeight + 'px';
                }
            }, false);
        }
        
        
        // Create a timestamp and log it to the textarea
        var date = new Date();
        var hour = date.getHours();
        var min  = date.getMinutes();
            min  = String(min).length === 1 ? '0' + min : min;
        var sec  = date.getSeconds();
            sec  = String(sec).length === 1 ? '0' + sec : sec;


        //
        // Handles circular references
        //
        function  getCircularReplacer()
        {
            var seen = [];
            
            return function (key, value)
            {
                if (typeof value === 'object' && value !== null) {
                    
                    // Loop thru the seen array and check that the object is not in there already
                    for (var i=0; i<seen.length; ++i) {
                        if (seen[i] === value) {
                            return;
                        }
                    }

                    seen.push(value);
                }

                return value;
            };
        };

        // Add the message to the debug window
        this.rgraph_debug_textarea.value = "[%1:%2:%3] %4\r\n%5".format(
            hour,
            min,
            sec,
            JSON.stringify(m, getCircularReplacer()),
            this.rgraph_debug_textarea.value
        );
    };







    //
    // Polyfill for the String.protfotype.substr() method which
    // may not be included on some devices
    //
    // @param  number start  The start index. Zero-indexed and can also be negtive - in which case
    //                       the counting starts from the end of the string
    // @param  number length The length of the string to extract
    // @return string        The new string
    //
    if (typeof ''.substr !== 'function') {
        String.prototype.substr = function (start, length)
        {
            start = start >=0 ? start : this.length + start;
            return this.substring(start, start + length);
 
        };
    }








    //
    // A basic string formatting function. Use it like this:
    // 
    // var str = '{0} {1} {2}'.format('a', 'b', 'c');
    // 
    // Outputs: a b c
    //
    // @param ... Replacements to use in the string
    //
    String.prototype.format = function()
    {
        //
        // Allow for this type of formatting: {myVar}
        //
        if (arguments.length === 0) {
        
            var s = this;
        
            // Allow for {myVar} style
            if (s.match(/{[a-z0-9]+?}/i)) {
                var s = this.replace(/{[a-z0-9]+?}/gi, function(str, idx)
                {
                    str = str.substr(1)
                    str = str.substr(0, str.length - 1)
    
                    return window[str];
                });
            }

            return s;
        }
        
        
        //
        // If called with just a single array, then handle that
        //
        //eg '%1 %2 %3'.format(['A','B','C']);
        //
        if (arguments.length === 1 && RGraph.isArray(arguments[0])) {
            return this.format.apply(this, arguments[0]);
        }




        var args = arguments;
        
        for (var i in args) {
            if (RGraph.isNullish(args[i])) {
                args[i] = 'null';
            }
        }

        var s = this.replace(/{(\d+)}/g, function(str, idx)
        {
          return typeof args[idx - 1] !== 'undefined' ? args[idx - 1] : str;
        });

        
        // Save percentage signs that are escaped with either
        // another percent or a backslash
        s = s.replace(/(?:%|\\)%(\d)/g,'__PPEERRCCEENNTT__$1');

        s = s.replace(/%(\d+)/g, function(str, idx)
        {
          return typeof args[idx - 1] !== 'undefined' ? args[idx - 1] : str;
        });

        // Now replace those saved percentage signs with a percentage sign
        return s.replace('__PPEERRCCEENNTT__', '%');
    };