catdad/grandma

View on GitHub
views/plot.html

Summary

Maintainability
Test Coverage
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Grandma Plot</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pure/0.6.0/buttons.css">
    <script src="http://cdnjs.cloudflare.com/ajax/libs/dygraph/1.1.1/dygraph-combined.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.5.0-beta4/html2canvas.js"></script>

    <style>
        html, body {
            font-family: sans-serif;
            margin: 0;
            padding: 0;
            background: #eee;
        }

        #plot-container {
            width: 100%;
            height: 600px;
        }

        #plot-container, #buttons {
            padding: 8px;
            box-sizing: border-box;
        }

        #plot {
            width: 100%;
            height: 100%;
        }

        #buttons button {
            margin: 3px;
            box-sizing: border-box;
        }

        #buttons p {
            margin: 0;
            padding: 0.5em 0;
            font-weight: bold;
        }

        #download, .btn-no-show {
            display: none;
        }

        .button-active {
            background: #1CB841;
            color: white;
        }

        .right {
            float: right;
        }

/*
        .dygraph-title {
            text-align: left !important;
            margin-left: 32px;
        }
*/

        .dygraph-legend {
            width: 50% !important;
            text-align: right !important;
            left: auto !important;
            right: 8px;
            background: none !important;
            top: 10px !important;
            z-index: 9 !important;
        }

        menu {
            display: block;
            padding: 0;
            margin: 0;
            background: #555;
            color: #eee;
            font-weight: bold;
            font-size: 0.9em;
        }

        menuitem {
            display: inline-block;
            padding: .8em 20px;
            margin: 0;
            cursor: pointer;
        }

        menuitem.selected {
            background: #eee;
            color: #555;
        }

        menuitem:hover {
            background: #d6d6d6;
            color: #555;
        }

        section {
            position: absolute;
            display: block;
            width: 100%;
            padding-top: 3em;
        }

        pre {
            margin: 20px;
            padding: 10px;
            background: white;

            border: 1px solid #d6d6d6;
            border-radius: 4px;
        }

        .hide {
            display: none;
        }
    </style>

    <script>
        window.addEventListener('load', function () {
            var container = document.querySelector('#plot-container');
            var plotDiv = document.querySelector('#plot');
            var buttons = document.querySelector('#buttons');
            var link = document.querySelector('#download');
            var img = document.querySelector('#copy-img');

            var yUnits;
            var yDivisor;

            function getMax(graph) {
                return graph.yAxisRange()[1];
            }

            function setYLabel(graph) {
                graph.updateOptions({
                    ylabel: yUnits
                }, true);
            }

            function getDivisor(graph) {
                if (yDivisor !== undefined) {
                    return yDivisor;
                }

                var max = parseInt(getMax(graph));

                if (max > 1000 * 60) {
                    yUnits = 'minutes';
                    yDivisor = 1000 * 60;
                } else if (max > 1000) {
                    yUnits = 'seconds';
                    yDivisor = 1000;
                } else {
                    yUnits = 'milliseconds';
                    yDivisor = 1;
                }

                setYLabel(graph);

                return yDivisor;
            }

            function yAxisFormatter(number, granularity, opts, graph) {
                var r = number / getDivisor(graph);

                // Apply fixed decimals if the number is not an integer
                if (parseInt(r) === r) {
                    return r;
                } else {
                    return r.toFixed(2);
                }
            }

            var dataLabels;
            var data = (function(dirtyData, labels) {
                dataLabels = labels;

                // make sure that all of the data entries have the name
                // number of items as the labels... dygraph does not
                // like when there numbers don't match, because it is
                // apparently too hard to assume they are null
                return dirtyData.map(function(val) {
                    if (val.length < labels.length) {
                        for (var i = val.length, l = labels.length; i < l; i++) {
                            val.push(null);
                        }
                    }

                    return val;
                });
            }(
                JSON.parse('[{{data}}]'),
                JSON.parse('{{labels}}')
            ));

            var plot = new Dygraph(
                plotDiv,
                data,
                {
                    title: '{{testname}}',
                    showRoller: true,
                    rollPeriod: 1,
                    ylabel: 'milliseconds',
                    xlabel: 'seconds from start',
                    legend: 'always',
                    labels: dataLabels,
                    axes: {
                        y: {
                            axisLabelFormatter: yAxisFormatter
                        }
                    }
                }
            );

            // the first series is always "full Err", which should be red
            var colors = ['rgb(191,0,0)'].concat(plot.colors_);
            plot.updateOptions({
                colors: colors
            });

            // get all of the labels
            var labels = plot.getLabels();
            // remove the first label, which is the x axis
            labels.shift();

            labels.forEach(function(label, i) {
                var active = 'button-active';

                var b = document.createElement('button');
                b.className = 'pure-button ' + active;
                b.innerHTML = label;

                b.onclick = function() {
                    if (b.classList.contains(active)) {
                        plot.setVisibility(i, false);
                        b.classList.remove(active);
                    } else {
                        plot.setVisibility(i, true);
                        b.classList.add(active);
                    }
                };

                buttons.appendChild(b);
            });

            (function(download) {
                download.innerHTML = 'Download PNG';
                download.className = 'pure-button pure-button-primary right';

                download.onclick = function() {
                    html2canvas(container).then(function(canvas) {
                        link.href = canvas.toDataURL();
                        link.download = 'test.png';
                        link.click();
                    });
                };

                buttons.appendChild(download);
            })(document.createElement('button'));

            (function(copy, prep) {
                var noShowClass = 'btn-no-show';
                prep.innerHTML = 'Prepare to copy';

                copy.innerHTML = 'Copy PNG';
                copy.className = prep.className = 'pure-button pure-button-primary right';

                copy.classList.add(noShowClass);

                // If copy is not supported, don't show the button
                // TODO: copying images has terrible support in terms of
                // where they can be pasted, so I am disabling this until
                // I can figure out something that works well.
                if (!document.queryCommandSupported('copy') || true) {
                    prep.classList.add(noShowClass);
                }

                var dataUri;

                // Since this is async, we cannot use a one-click copy. Use the first
                // click to prepare the image data, and the second to copy it.
                prep.onclick = function() {
                    html2canvas(container, {
                        onrendered: function(canvas) {
                            dataUri = canvas.toDataURL();

                            prep.classList.toggle(noShowClass);
                            copy.classList.toggle(noShowClass);
                        },
                        timeout: 0
                    });
                };

                // Use this click to copy the image data, and reset the UI.
                copy.onclick = function() {
                    img.src = dataUri;

                    var range = document.createRange();
                    range.selectNode(img);
                    window.getSelection().addRange(range);

                    try {
                        // Now that we've selected the anchor text, execute the copy command
                        var successful = document.execCommand('copy');
                        var msg = successful ? 'successful' : 'unsuccessful';
                    } catch(err) {
                        // TODO: do I need to tell the user that it failed
                    }

                    // Remove the selections
                    window.getSelection().removeAllRanges();

                    img.src = '';

                    prep.classList.toggle(noShowClass);
                    copy.classList.toggle(noShowClass);
                };

                buttons.appendChild(copy);
                buttons.appendChild(prep);

                var menuItems = document.querySelectorAll('menuitem[x-for]');
                var sections = document.querySelectorAll('section[id]');

                if (menuItems.length && sections.length) {
                    sections = [].slice.call(sections);
                    menuItems = [].slice.call(menuItems);

                    menuItems.forEach(function (item) {
                        item.section = document.querySelector('#' + item.getAttribute('x-for'));

                        item.addEventListener('click', function () {
                            menuItems.forEach(function (i) {
                                if (i === item) {
                                    return;
                                }

                                i.classList.remove('selected');
                                i.section.classList.add('hide');
                            });

                            item.classList.add('selected');
                            item.section.classList.remove('hide');
                        });
                    });
                }
            })(document.createElement('button'), document.createElement('button'));
        });
    </script>
</head>
<body>
    <menu type="toolbar" class="">
        <menuitem label="plot" class="selected" x-for="plot-section">Plot</menuitem>
        <menuitem label="stats" x-for="stats-section">Stats</menuitem>
    </menu>

    <section id="plot-section">
        <div id="plot-container">
            <div id="plot"></div>
        </div>
        <div id="buttons">
            <p>Toggle Series</p>
        </div>

        <a href="" id="download"></a>
        <img id="copy-img">
    </section>

    <section id="stats-section" class="hide">
        <pre>
{{textStats}}
        </pre>
    </section>

</body>
</html>