gopheracademy/gcon

View on GitHub
assets/admin/global/plugins/highcharts/js/modules/exporting.src.js

Summary

Maintainability
F
1 mo
Test Coverage
/**
 * @license Highcharts JS v4.1.9 (2015-10-07)
 * Exporting module
 *
 * (c) 2010-2014 Torstein Honsi
 *
 * License: www.highcharts.com/license
 */

// JSLint options:
/*global Highcharts, HighchartsAdapter, document, window, Math, setTimeout */

(function (Highcharts) { // encapsulate

// create shortcuts
var Chart = Highcharts.Chart,
    addEvent = Highcharts.addEvent,
    removeEvent = Highcharts.removeEvent,
    fireEvent = HighchartsAdapter.fireEvent,
    createElement = Highcharts.createElement,
    discardElement = Highcharts.discardElement,
    css = Highcharts.css,
    merge = Highcharts.merge,
    each = Highcharts.each,
    extend = Highcharts.extend,
    splat = Highcharts.splat,
    math = Math,
    mathMax = math.max,
    doc = document,
    win = window,
    isTouchDevice = Highcharts.isTouchDevice,
    M = 'M',
    L = 'L',
    DIV = 'div',
    HIDDEN = 'hidden',
    NONE = 'none',
    PREFIX = 'highcharts-',
    ABSOLUTE = 'absolute',
    PX = 'px',
    UNDEFINED,
    symbols = Highcharts.Renderer.prototype.symbols,
    defaultOptions = Highcharts.getOptions(),
    buttonOffset;

    // Add language
    extend(defaultOptions.lang, {
        printChart: 'Print chart',
        downloadPNG: 'Download PNG image',
        downloadJPEG: 'Download JPEG image',
        downloadPDF: 'Download PDF document',
        downloadSVG: 'Download SVG vector image',
        contextButtonTitle: 'Chart context menu'
    });

// Buttons and menus are collected in a separate config option set called 'navigation'.
// This can be extended later to add control buttons like zoom and pan right click menus.
defaultOptions.navigation = {
    menuStyle: {
        border: '1px solid #A0A0A0',
        background: '#FFFFFF',
        padding: '5px 0'
    },
    menuItemStyle: {
        padding: '0 10px',
        background: NONE,
        color: '#303030',
        fontSize: isTouchDevice ? '14px' : '11px'
    },
    menuItemHoverStyle: {
        background: '#4572A5',
        color: '#FFFFFF'
    },

    buttonOptions: {
        symbolFill: '#E0E0E0',
        symbolSize: 14,
        symbolStroke: '#666',
        symbolStrokeWidth: 3,
        symbolX: 12.5,
        symbolY: 10.5,
        align: 'right',
        buttonSpacing: 3,
        height: 22,
        // text: null,
        theme: {
            fill: 'white', // capture hover
            stroke: 'none'
        },
        verticalAlign: 'top',
        width: 24
    }
};



// Add the export related options
defaultOptions.exporting = {
    //enabled: true,
    //filename: 'chart',
    type: 'image/png',
    url: 'http://export.highcharts.com/',
    //width: undefined,
    //scale: 2
    buttons: {
        contextButton: {
            menuClassName: PREFIX + 'contextmenu',
            //x: -10,
            symbol: 'menu',
            _titleKey: 'contextButtonTitle',
            menuItems: [{
                textKey: 'printChart',
                onclick: function () {
                    this.print();
                }
            }, {
                separator: true
            }, {
                textKey: 'downloadPNG',
                onclick: function () {
                    this.exportChart();
                }
            }, {
                textKey: 'downloadJPEG',
                onclick: function () {
                    this.exportChart({
                        type: 'image/jpeg'
                    });
                }
            }, {
                textKey: 'downloadPDF',
                onclick: function () {
                    this.exportChart({
                        type: 'application/pdf'
                    });
                }
            }, {
                textKey: 'downloadSVG',
                onclick: function () {
                    this.exportChart({
                        type: 'image/svg+xml'
                    });
                }
            }
            // Enable this block to add "View SVG" to the dropdown menu
            /*
            ,{

                text: 'View SVG',
                onclick: function () {
                    var svg = this.getSVG()
                        .replace(/</g, '\n&lt;')
                        .replace(/>/g, '&gt;');

                    doc.body.innerHTML = '<pre>' + svg + '</pre>';
                }
            } // */
            ]
        }
    }
};

// Add the Highcharts.post utility
Highcharts.post = function (url, data, formAttributes) {
    var name,
        form;

    // create the form
    form = createElement('form', merge({
        method: 'post',
        action: url,
        enctype: 'multipart/form-data'
    }, formAttributes), {
        display: NONE
    }, doc.body);

    // add the data
    for (name in data) {
        createElement('input', {
            type: HIDDEN,
            name: name,
            value: data[name]
        }, null, form);
    }

    // submit
    form.submit();

    // clean up
    discardElement(form);
};

extend(Chart.prototype, {

    /**
     * A collection of regex fixes on the produces SVG to account for expando properties,
     * browser bugs, VML problems and other. Returns a cleaned SVG.
     */
    sanitizeSVG: function (svg) {
        return svg
            .replace(/zIndex="[^"]+"/g, '')
            .replace(/isShadow="[^"]+"/g, '')
            .replace(/symbolName="[^"]+"/g, '')
            .replace(/jQuery[0-9]+="[^"]+"/g, '')
            .replace(/url\([^#]+#/g, 'url(#')
            .replace(/<svg /, '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ')
            .replace(/ (NS[0-9]+\:)?href=/g, ' xlink:href=') // #3567
            .replace(/\n/, ' ')
            // Any HTML added to the container after the SVG (#894)
            .replace(/<\/svg>.*?$/, '</svg>') 
            // Batik doesn't support rgba fills and strokes (#3095)
            .replace(/(fill|stroke)="rgba\(([ 0-9]+,[ 0-9]+,[ 0-9]+),([ 0-9\.]+)\)"/g, '$1="rgb($2)" $1-opacity="$3"')
            /* This fails in IE < 8
            .replace(/([0-9]+)\.([0-9]+)/g, function(s1, s2, s3) { // round off to save weight
                return s2 +'.'+ s3[0];
            })*/

            // Replace HTML entities, issue #347
            .replace(/&nbsp;/g, '\u00A0') // no-break space
            .replace(/&shy;/g,  '\u00AD') // soft hyphen

            // IE specific
            .replace(/<IMG /g, '<image ')
            .replace(/<(\/?)TITLE>/g, '<$1title>')
            .replace(/height=([^" ]+)/g, 'height="$1"')
            .replace(/width=([^" ]+)/g, 'width="$1"')
            .replace(/hc-svg-href="([^"]+)">/g, 'xlink:href="$1"/>')
            .replace(/ id=([^" >]+)/g, ' id="$1"') // #4003
            .replace(/class=([^" >]+)/g, 'class="$1"')
            .replace(/ transform /g, ' ')
            .replace(/:(path|rect)/g, '$1')
            .replace(/style="([^"]+)"/g, function (s) {
                return s.toLowerCase();
            });
    },

    /**
     * Return innerHTML of chart. Used as hook for plugins.
     */
    getChartHTML: function () {
        return this.container.innerHTML;
    },

    /**
     * Return an SVG representation of the chart
     *
     * @param additionalOptions {Object} Additional chart options for the generated SVG representation
     */
    getSVG: function (additionalOptions) {
        var chart = this,
            chartCopy,
            sandbox,
            svg,
            seriesOptions,
            sourceWidth,
            sourceHeight,
            cssWidth,
            cssHeight,
            html,
            options = merge(chart.options, additionalOptions), // copy the options and add extra options
            allowHTML = options.exporting.allowHTML; // docs: experimental, see #2473
            

        // IE compatibility hack for generating SVG content that it doesn't really understand
        if (!doc.createElementNS) {
            /*jslint unparam: true*//* allow unused parameter ns in function below */
            doc.createElementNS = function (ns, tagName) {
                return doc.createElement(tagName);
            };
            /*jslint unparam: false*/
        }

        // create a sandbox where a new chart will be generated
        sandbox = createElement(DIV, null, {
            position: ABSOLUTE,
            top: '-9999em',
            width: chart.chartWidth + PX,
            height: chart.chartHeight + PX
        }, doc.body);

        // get the source size
        cssWidth = chart.renderTo.style.width;
        cssHeight = chart.renderTo.style.height;
        sourceWidth = options.exporting.sourceWidth ||
            options.chart.width ||
            (/px$/.test(cssWidth) && parseInt(cssWidth, 10)) ||
            600;
        sourceHeight = options.exporting.sourceHeight ||
            options.chart.height ||
            (/px$/.test(cssHeight) && parseInt(cssHeight, 10)) ||
            400;

        // override some options
        extend(options.chart, {
            animation: false,
            renderTo: sandbox,
            forExport: true,
            renderer: 'SVGRenderer',
            width: sourceWidth,
            height: sourceHeight
        });
        options.exporting.enabled = false; // hide buttons in print
        delete options.data; // #3004

        // prepare for replicating the chart
        options.series = [];
        each(chart.series, function (serie) {
            seriesOptions = merge(serie.options, {
                animation: false, // turn off animation
                enableMouseTracking: false,
                showCheckbox: false,
                visible: serie.visible
            });

            if (!seriesOptions.isInternal) { // used for the navigator series that has its own option set
                options.series.push(seriesOptions);
            }
        });

        // Axis options must be merged in one by one, since it may be an array or an object (#2022, #3900)
        if (additionalOptions) {
            each(['xAxis', 'yAxis'], function (axisType) {
                each(splat(additionalOptions[axisType]), function (axisOptions, i) {
                    options[axisType][i] = merge(options[axisType][i], axisOptions);
                });
            });
        }

        // generate the chart copy
        chartCopy = new Highcharts.Chart(options, chart.callback);

        // reflect axis extremes in the export
        each(['xAxis', 'yAxis'], function (axisType) {
            each(chart[axisType], function (axis, i) {
                var axisCopy = chartCopy[axisType][i],
                    extremes = axis.getExtremes(),
                    userMin = extremes.userMin,
                    userMax = extremes.userMax;

                if (axisCopy && (userMin !== UNDEFINED || userMax !== UNDEFINED)) {
                    axisCopy.setExtremes(userMin, userMax, true, false);
                }
            });
        });

        // get the SVG from the container's innerHTML
        svg = chartCopy.getChartHTML();

        // free up memory
        options = null;
        chartCopy.destroy();
        discardElement(sandbox);

        // Move HTML into a foreignObject
        if (allowHTML) {
            html = svg.match(/<\/svg>(.*?$)/);
            if (html) {
                html = '<foreignObject x="0" y="0" width="200" height="200">' +
                    '<body xmlns="http://www.w3.org/1999/xhtml">' +
                    html[1] +
                    '</body>' + 
                    '</foreignObject>';
                svg = svg.replace('</svg>', html + '</svg>');
            }
        }

        // sanitize
        svg = this.sanitizeSVG(svg);

        // IE9 beta bugs with innerHTML. Test again with final IE9.
        svg = svg.replace(/(url\(#highcharts-[0-9]+)&quot;/g, '$1')
            .replace(/&quot;/g, "'");

        return svg;
    },

    getSVGForExport: function (options, chartOptions) {
        var chartExportingOptions = this.options.exporting;

        return this.getSVG(merge(
            { chart: { borderRadius: 0 } },
            chartExportingOptions.chartOptions,
            chartOptions,
            {
                exporting: {
                    sourceWidth: (options && options.sourceWidth) || chartExportingOptions.sourceWidth,
                    sourceHeight: (options && options.sourceHeight) || chartExportingOptions.sourceHeight
                }
            }
        ));
    },

    /**
     * Submit the SVG representation of the chart to the server
     * @param {Object} options Exporting options. Possible members are url, type, width and formAttributes.
     * @param {Object} chartOptions Additional chart options for the SVG representation of the chart
     */
    exportChart: function (options, chartOptions) {
        
        var svg = this.getSVGForExport(options, chartOptions);

        // merge the options
        options = merge(this.options.exporting, options);

        // do the post
        Highcharts.post(options.url, {
            filename: options.filename || 'chart',
            type: options.type,
            width: options.width || 0, // IE8 fails to post undefined correctly, so use 0
            scale: options.scale || 2,
            svg: svg
        }, options.formAttributes);

    },

    /**
     * Print the chart
     */
    print: function () {

        var chart = this,
            container = chart.container,
            origDisplay = [],
            origParent = container.parentNode,
            body = doc.body,
            childNodes = body.childNodes;

        if (chart.isPrinting) { // block the button while in printing mode
            return;
        }

        chart.isPrinting = true;

        fireEvent(chart, 'beforePrint');

        // hide all body content
        each(childNodes, function (node, i) {
            if (node.nodeType === 1) {
                origDisplay[i] = node.style.display;
                node.style.display = NONE;
            }
        });

        // pull out the chart
        body.appendChild(container);

        // print
        win.focus(); // #1510
        win.print();

        // allow the browser to prepare before reverting
        setTimeout(function () {

            // put the chart back in
            origParent.appendChild(container);

            // restore all body content
            each(childNodes, function (node, i) {
                if (node.nodeType === 1) {
                    node.style.display = origDisplay[i];
                }
            });

            chart.isPrinting = false;

            fireEvent(chart, 'afterPrint');

        }, 1000);

    },

    /**
     * Display a popup menu for choosing the export type
     *
     * @param {String} className An identifier for the menu
     * @param {Array} items A collection with text and onclicks for the items
     * @param {Number} x The x position of the opener button
     * @param {Number} y The y position of the opener button
     * @param {Number} width The width of the opener button
     * @param {Number} height The height of the opener button
     */
    contextMenu: function (className, items, x, y, width, height, button) {
        var chart = this,
            navOptions = chart.options.navigation,
            menuItemStyle = navOptions.menuItemStyle,
            chartWidth = chart.chartWidth,
            chartHeight = chart.chartHeight,
            cacheName = 'cache-' + className,
            menu = chart[cacheName],
            menuPadding = mathMax(width, height), // for mouse leave detection
            boxShadow = '3px 3px 10px #888',
            innerMenu,
            hide,
            hideTimer,
            menuStyle,
            docMouseUpHandler = function (e) {
                if (!chart.pointer.inClass(e.target, className)) {
                    hide();
                }
            };

        // create the menu only the first time
        if (!menu) {

            // create a HTML element above the SVG
            chart[cacheName] = menu = createElement(DIV, {
                className: className
            }, {
                position: ABSOLUTE,
                zIndex: 1000,
                padding: menuPadding + PX
            }, chart.container);

            innerMenu = createElement(DIV, null,
                extend({
                    MozBoxShadow: boxShadow,
                    WebkitBoxShadow: boxShadow,
                    boxShadow: boxShadow
                }, navOptions.menuStyle), menu);

            // hide on mouse out
            hide = function () {
                css(menu, { display: NONE });
                if (button) {
                    button.setState(0);
                }
                chart.openMenu = false;
            };

            // Hide the menu some time after mouse leave (#1357)
            addEvent(menu, 'mouseleave', function () {
                hideTimer = setTimeout(hide, 500);
            });
            addEvent(menu, 'mouseenter', function () {
                clearTimeout(hideTimer);
            });


            // Hide it on clicking or touching outside the menu (#2258, #2335, #2407)
            addEvent(document, 'mouseup', docMouseUpHandler);
            addEvent(chart, 'destroy', function () {
                removeEvent(document, 'mouseup', docMouseUpHandler);
            });


            // create the items
            each(items, function (item) {
                if (item) {
                    var element = item.separator ?
                        createElement('hr', null, null, innerMenu) :
                        createElement(DIV, {
                            onmouseover: function () {
                                css(this, navOptions.menuItemHoverStyle);
                            },
                            onmouseout: function () {
                                css(this, menuItemStyle);
                            },
                            onclick: function (e) {
                                e.stopPropagation();
                                hide();
                                if (item.onclick) {
                                    item.onclick.apply(chart, arguments);
                                }
                            },
                            innerHTML: item.text || chart.options.lang[item.textKey]
                        }, extend({
                            cursor: 'pointer'
                        }, menuItemStyle), innerMenu);


                    // Keep references to menu divs to be able to destroy them
                    chart.exportDivElements.push(element);
                }
            });

            // Keep references to menu and innerMenu div to be able to destroy them
            chart.exportDivElements.push(innerMenu, menu);

            chart.exportMenuWidth = menu.offsetWidth;
            chart.exportMenuHeight = menu.offsetHeight;
        }

        menuStyle = { display: 'block' };

        // if outside right, right align it
        if (x + chart.exportMenuWidth > chartWidth) {
            menuStyle.right = (chartWidth - x - width - menuPadding) + PX;
        } else {
            menuStyle.left = (x - menuPadding) + PX;
        }
        // if outside bottom, bottom align it
        if (y + height + chart.exportMenuHeight > chartHeight && button.alignOptions.verticalAlign !== 'top') {
            menuStyle.bottom = (chartHeight - y - menuPadding)  + PX;
        } else {
            menuStyle.top = (y + height - menuPadding) + PX;
        }

        css(menu, menuStyle);
        chart.openMenu = true;
    },

    /**
     * Add the export button to the chart
     */
    addButton: function (options) {
        var chart = this,
            renderer = chart.renderer,
            btnOptions = merge(chart.options.navigation.buttonOptions, options),
            onclick = btnOptions.onclick,
            menuItems = btnOptions.menuItems,
            symbol,
            button,
            symbolAttr = {
                stroke: btnOptions.symbolStroke,
                fill: btnOptions.symbolFill
            },
            symbolSize = btnOptions.symbolSize || 12;
        if (!chart.btnCount) {
            chart.btnCount = 0;
        }

        // Keeps references to the button elements
        if (!chart.exportDivElements) {
            chart.exportDivElements = [];
            chart.exportSVGElements = [];
        }

        if (btnOptions.enabled === false) {
            return;
        }


        var attr = btnOptions.theme,
            states = attr.states,
            hover = states && states.hover,
            select = states && states.select,
            callback;

        delete attr.states;

        if (onclick) {
            callback = function (e) {
                e.stopPropagation();
                onclick.call(chart, e);
            };

        } else if (menuItems) {
            callback = function () {
                chart.contextMenu(
                    button.menuClassName,
                    menuItems,
                    button.translateX,
                    button.translateY,
                    button.width,
                    button.height,
                    button
                );
                button.setState(2);
            };
        }


        if (btnOptions.text && btnOptions.symbol) {
            attr.paddingLeft = Highcharts.pick(attr.paddingLeft, 25);

        } else if (!btnOptions.text) {
            extend(attr, {
                width: btnOptions.width,
                height: btnOptions.height,
                padding: 0
            });
        }

        button = renderer.button(btnOptions.text, 0, 0, callback, attr, hover, select)
            .attr({
                title: chart.options.lang[btnOptions._titleKey],
                'stroke-linecap': 'round'
            });
        button.menuClassName = options.menuClassName || PREFIX + 'menu-' + chart.btnCount++;

        if (btnOptions.symbol) {
            symbol = renderer.symbol(
                    btnOptions.symbol,
                    btnOptions.symbolX - (symbolSize / 2),
                    btnOptions.symbolY - (symbolSize / 2),
                    symbolSize,
                    symbolSize
                )
                .attr(extend(symbolAttr, {
                    'stroke-width': btnOptions.symbolStrokeWidth || 1,
                    zIndex: 1
                })).add(button);
        }

        button.add()
            .align(extend(btnOptions, {
                width: button.width,
                x: Highcharts.pick(btnOptions.x, buttonOffset) // #1654
            }), true, 'spacingBox');

        buttonOffset += (button.width + btnOptions.buttonSpacing) * (btnOptions.align === 'right' ? -1 : 1);

        chart.exportSVGElements.push(button, symbol);

    },

    /**
     * Destroy the buttons.
     */
    destroyExport: function (e) {
        var chart = e.target,
            i,
            elem;

        // Destroy the extra buttons added
        for (i = 0; i < chart.exportSVGElements.length; i++) {
            elem = chart.exportSVGElements[i];

            // Destroy and null the svg/vml elements
            if (elem) { // #1822
                elem.onclick = elem.ontouchstart = null;
                chart.exportSVGElements[i] = elem.destroy();
            }
        }

        // Destroy the divs for the menu
        for (i = 0; i < chart.exportDivElements.length; i++) {
            elem = chart.exportDivElements[i];

            // Remove the event handler
            removeEvent(elem, 'mouseleave');

            // Remove inline events
            chart.exportDivElements[i] = elem.onmouseout = elem.onmouseover = elem.ontouchstart = elem.onclick = null;

            // Destroy the div by moving to garbage bin
            discardElement(elem);
        }
    }
});


symbols.menu = function (x, y, width, height) {
    var arr = [
        M, x, y + 2.5,
        L, x + width, y + 2.5,
        M, x, y + height / 2 + 0.5,
        L, x + width, y + height / 2 + 0.5,
        M, x, y + height - 1.5,
        L, x + width, y + height - 1.5
    ];
    return arr;
};

// Add the buttons on chart load
Chart.prototype.callbacks.push(function (chart) {
    var n,
        exportingOptions = chart.options.exporting,
        buttons = exportingOptions.buttons;

    buttonOffset = 0;

    if (exportingOptions.enabled !== false) {

        for (n in buttons) {
            chart.addButton(buttons[n]);
        }

        // Destroy the export elements at chart destroy
        addEvent(chart, 'destroy', chart.destroyExport);
    }

});


}(Highcharts));