<commit_before>'use strict';

app.factory('plotting', function(d3Service, panelState, frameCache, $timeout) {

    var INITIAL_HEIGHT = 400;

    function cleanNumber(v) {
        v = v.replace(/\.0+(\D+)/, '$1');
        v = v.replace(/(\.\d)0+(\D+)/, '$1$2');
        return v;
    }

    return {

        computePeaks: function(json, dimensions, xPoints, xAxisScale, yAxisScale) {
            var peakSpacing = dimensions[0] / 20;
            var minPixelHeight = dimensions[1] * .995;
            var xPeakValues = [];
            var sortedPoints = d3.zip(xPoints, json.points).sort(function(a, b) { return b[1] - a[1] });
            for (var i = 0; i < sortedPoints.length / 2; i++) {
                var p = sortedPoints[i]
                var xPixel = xAxisScale(p[0]);
                var yPixel = yAxisScale(p[1]);
                if (yPixel >= minPixelHeight) {
                    break;
                }
                var found = false;
                for (var j = 0; j < xPeakValues.length; j++) {
                    if (Math.abs(xPixel - xPeakValues[j][2]) < peakSpacing) {
                        found = true;
                        break;
                    }
                }
                if (! found)
                    xPeakValues.push([p[0], p[1], xPixel]);
            }
            //console.log('local maxes: ', xPeakValues.length);
            return xPeakValues;
        },

        createAxis: createAxis,

        createExponentialAxis: function(scale, orient) {
            return createAxis(scale, orient)
            // this causes a 'number of fractional digits' error in MSIE
            //.tickFormat(d3.format('e'))
                .tickFormat(function (value) {
                    if (value)
                        return cleanNumber(value.toExponential(2));
                    return value;
                });
        },

        // Returns a function, that, as long as it continues to be invoked, will not
        // be triggered. The function will be called after it stops being called for
        // N milliseconds. If `immediate` is passed, trigger the function on the
        // leading edge, instead of the trailing.
        // taken from http://davidwalsh.name/javascript-debounce-function
        debounce: function(func, wait) {
            var timeout;
            return function() {
                var context = this, args = arguments;
                var later = function() {
                    timeout = null;
                    func.apply(context, args);
                };
                clearTimeout(timeout);
                timeout = setTimeout(later, wait);
            };
        },

        extractUnits: function(scope, axis, label) {
            var match = label.match(/\[(.*?)\]/);
            if (match) {
                scope[axis + 'units'] = match[1];
                label = label.replace(/\[.*?\]/, '');
            }
            return label;
        },

        fixFormat: function(scope, axis) {
            var format = d3.format('.3s');
            // amounts near zero may appear as NNNz, change them to 0
            return function(n) {
                var v = format(n);
                if ((v && v.indexOf('z') > 0) || v == '0.00')
                    return '0';
                v = cleanNumber(v);
                var units = scope[axis + 'units'] || '';
                return v + units;
            }
        },

        initialHeight: function(scope) {
            return scope.isAnimation ? 1 : INITIAL_HEIGHT;
        },

        linkPlot: function(scope, element) {
            d3Service.d3().then(function(d3) {
                scope.element = element[0];
                scope.isAnimation = scope.modelName.indexOf('Animation') >= 0;
                var requestData;

                if (scope.isAnimation) {
                    scope.prevFrameIndex = -1;
                    scope.isPlaying = false;
                    requestData = function() {
                        if (! scope.hasFrames())
                            return;
                        var index = frameCache.getCurrentFrame(scope.modelName);
                        if (frameCache.getCurrentFrame(scope.modelName) == scope.prevFrameIndex)
                            return;
                        scope.prevFrameIndex = index;
                        frameCache.getFrame(scope.modelName, index, scope.isPlaying, function(index, data) {
                            if (scope.element)
                                scope.load(data);
                            if (scope.isPlaying)
                                scope.advanceFrame(1);
                        });
                    }
                    scope.advanceFrame = function(increment) {
                        var next = frameCache.getCurrentFrame(scope.modelName) + increment;
                        if (next < 0 || next > frameCache.frameCount - 1) {
                            scope.isPlaying = false;
                            return;
                        }
                        frameCache.setCurrentFrame(scope.modelName, next);
                        requestData();
                    };
                    scope.firstFrame = function() {
                        scope.isPlaying = false;
                        frameCache.setCurrentFrame(scope.modelName, 0);
                        if (scope.modelChanged)
                            scope.modelChanged();
                        requestData();
                    };
                    scope.hasFrames = function() {
                        return frameCache.isLoaded() && frameCache.frameCount > 0;
                    };
                    scope.isFirstFrame = function() {
                        return frameCache.getCurrentFrame(scope.modelName) == 0;
                    };
                    scope.isLastFrame = function() {
                        return frameCache.getCurrentFrame(scope.modelName) == frameCache.frameCount - 1;
                    };
                    scope.lastFrame = function() {
                        scope.isPlaying = false;
                        frameCache.setCurrentFrame(scope.modelName, frameCache.frameCount - 1);
                        requestData();
                    };
                    scope.togglePlay = function() {
                        scope.isPlaying = ! scope.isPlaying;
                        if (scope.isPlaying)
                            scope.advanceFrame(1);
                    };
                    if (scope.clearData)
                        scope.$on('framesCleared', scope.clearData);
                    scope.$on('modelsLoaded', requestData);
                    scope.$on('framesLoaded', function(event, oldFrameCount) {
                        if (scope.prevFrameIndex < 0)
                            scope.firstFrame();
                        else if (oldFrameCount == 0)
                            scope.lastFrame();
                        else if (scope.prevFrameIndex > frameCache.frameCount)
                            scope.firstFrame();
                        // go to the next last frame, if the current frame was the previous last frame
                        else if (frameCache.getCurrentFrame(scope.modelName) == oldFrameCount - 1)
                            scope.lastFrame();
                    });
                }
                else if (scope.isClientOnly) {
                    requestData = function() {};
                }
                else {
                    requestData = function() {
                        //TODO(pjm): timeout is a hack to give time for invalid reports to be destroyed
                        $timeout(function() {
                            if (! scope.element)
                                return;
                            panelState.requestData(scope.modelName, function(data) {
                                if (scope.element)
                                    scope.load(data);
                            });
                        }, 50);
                    }
                }
                scope.$on(
                    scope.modelName + '.changed',
                    function() {
                        scope.prevFrameIndex = -1;
                        if (scope.modelChanged)
                            scope.modelChanged();
                        panelState.clear(scope.modelName);
                        requestData();
                    });
                scope.isLoading = function() {
                    if (scope.isAnimation)
                        return false;
                    return panelState.isLoading(scope.modelName);
                };
                scope.init();
                requestData();
            });
        },

        linspace: function(start, stop, nsteps) {
            var delta = (stop - start) / (nsteps - 1);
            return d3.range(start, stop, delta).slice(0, nsteps);
        },

        ticks: function(axis, width, isHorizontalAxis) {
            var spacing = isHorizontalAxis ? 60 : 40;
            var n = Math.max(Math.round(width / spacing), 2);
            axis.ticks(n);
        },
    };

    function createAxis(scale, orient) {
        return d3.svg.axis()
            .scale(scale)
            .orient(orient);
    }
});

app.directive('plot2d', function(plotting) {
    return {
        restrict: 'A',
        scope: {
            modelName: '@',
        },
        templateUrl: '/static/html/plot2d.html?' + SIREPO_APP_VERSION,
        controller: function($scope) {

            var ASPECT_RATIO = 4.0 / 7;
            $scope.margin = {top: 50, right: 20, bottom: 50, left: 70};
            $scope.width = $scope.height = 0;
            $scope.dataCleared = true;
            var formatter, ordinate_formatter, graphLine, points, xAxis, xAxisGrid, xAxisScale, xPeakValues, xUnits, yAxis, yAxisGrid, yAxisScale, yUnits;

            function mouseMove() {
                if (! points)
                    return;
                var x0 = xAxisScale.invert(d3.mouse(this)[0]);
                var localMax = null;
                for (var i = 0; i < xPeakValues.length; i++) {
                    var v = xPeakValues[i];
                    if (localMax === null || Math.abs(v[0] - x0) < Math.abs(localMax[0] - x0)) {
                        localMax = v;
                    }
                }
                if (localMax) {
                    var xPixel = xAxisScale(localMax[0]);
                    if (xPixel < 0 || xPixel >= select('.plot-viewport').attr('width'))
                        return;
                    var focus = select('.focus');
                    focus.style('display', null);
                    focus.attr('transform', 'translate(' + xPixel + ',' + yAxisScale(localMax[1]) + ')');
                    select('.focus-text').text('[' + formatter(localMax[0]) + ', ' + ordinate_formatter(localMax[1]) + ']');
                }
            };

            function resize() {
                var width = parseInt(select().style('width')) - $scope.margin.left - $scope.margin.right;
                if (! points || isNaN(width))
                    return;
                $scope.width = width;
                $scope.height = ASPECT_RATIO * $scope.width;
                select('svg')
                    .attr('width', $scope.width + $scope.margin.left + $scope.margin.right)
                    .attr('height', $scope.height + $scope.margin.top + $scope.margin.bottom);
                plotting.ticks(xAxis, $scope.width, true);
                plotting.ticks(xAxisGrid, $scope.width, true);
                plotting.ticks(yAxis, $scope.height, false);
                plotting.ticks(yAxisGrid, $scope.height, false);
                xAxisScale.range([-0.5, $scope.width - 0.5]);
                yAxisScale.range([$scope.height - 0.5, 0 - 0.5]).nice();
                xAxisGrid.tickSize(-$scope.height);
                yAxisGrid.tickSize(-$scope.width);
                select('.x.axis').call(xAxis);
                select('.x.axis.grid').call(xAxisGrid); // tickLine == gridline
                select('.y.axis').call(yAxis);
                select('.y.axis.grid').call(yAxisGrid);
                select('.line').attr('d', graphLine);
                return [$scope.width, $scope.height];
            }

            function select(selector) {
                var e = d3.select($scope.element);
                return selector ? e.select(selector) : e;
            }

            function sliderChanged(ev) {
                if (! points)
                    return;
                function computePoint(value) {
                    return Math.round($scope.xRange[0] + (value / 100) * ($scope.xRange[1] - $scope.xRange[0]));
                }
                var xStart = computePoint(ev.value[0]);
                var xEnd = computePoint(ev.value[1]);
                xAxisScale.domain([xStart, xEnd]);

                var yMin, yMax;
                for (var i = 0; i < points.length; i++) {
                    var p = points[i];
                    if (p[0] < xStart)
                        continue;
                    if (p[0] > xEnd)
                        break;
                    if (yMin === undefined || yMin > p[1])
                        yMin = p[1];
                    if (yMax === undefined || yMax < p[1])
                        yMax = p[1];
                }
                yAxisScale.domain([yMin, yMax]);
                resize();
            }

            $scope.clearData = function() {
                $scope.dataCleared = true;
            };

            $scope.init = function() {
                formatter = d3.format('.0f');
                ordinate_formatter = d3.format('.3e');
                select('svg').attr('height', plotting.initialHeight($scope));
                $scope.slider = $(select('.srw-plot2d-slider').node()).slider();
                $scope.slider.on('slide', sliderChanged);
                $(window).resize($scope.windowResize);
                xAxisScale = d3.scale.linear();
                yAxisScale = d3.scale.linear();
                xAxis = plotting.createAxis(xAxisScale, 'bottom');
                xAxis.tickFormat(d3.format('s'));
                xAxisGrid = plotting.createAxis(xAxisScale, 'bottom');
                yAxis = plotting.createExponentialAxis(yAxisScale, 'left');
                yAxisGrid = plotting.createAxis(yAxisScale, 'left');
                graphLine = d3.svg.line()
                    .x(function(d) {return xAxisScale(d[0])})
                    .y(function(d) {return yAxisScale(d[1])});
                var focus = select('.focus');
                var focus_text = select('.focus-text');
                var focus_hint = select('.focus-hint');
                select('.overlay')
                    .on('mouseover', function() {
                        focus.style('display', focus.style('display'));
                        focus_hint.style('display', 'none');
                        focus_hint.text('Double-click to copy the values');
                    })
                    .on('mouseout', function() {
                        focus_hint.style('display', 'none');
                    })
                    .on('click', mouseMove)
                    .on('dblclick', function copyToClipboard() {
                        var $temp = $('<input>');<commit_msg>#137 Implementation of keyboard keys processing for 2D-plots.<commit_after>'use strict';

app.factory('plotting', function(d3Service, panelState, frameCache, $timeout) {

    var INITIAL_HEIGHT = 400;

    function cleanNumber(v) {
        v = v.replace(/\.0+(\D+)/, '$1');
        v = v.replace(/(\.\d)0+(\D+)/, '$1$2');
        return v;
    }

    return {

        computePeaks: function(json, dimensions, xPoints, xAxisScale, yAxisScale) {
            var peakSpacing = dimensions[0] / 20;
            var minPixelHeight = dimensions[1] * .995;
            var xPeakValues = [];
            var sortedPoints = d3.zip(xPoints, json.points).sort(function(a, b) { return b[1] - a[1] });
            for (var i = 0; i < sortedPoints.length / 2; i++) {
                var p = sortedPoints[i]
                var xPixel = xAxisScale(p[0]);
                var yPixel = yAxisScale(p[1]);
                if (yPixel >= minPixelHeight) {
                    break;
                }
                var found = false;
                for (var j = 0; j < xPeakValues.length; j++) {
                    if (Math.abs(xPixel - xPeakValues[j][2]) < peakSpacing) {
                        found = true;
                        break;
                    }
                }
                if (! found)
                    xPeakValues.push([p[0], p[1], xPixel]);
            }
            //console.log('local maxes: ', xPeakValues.length);
            return xPeakValues;
        },

        createAxis: createAxis,

        createExponentialAxis: function(scale, orient) {
            return createAxis(scale, orient)
            // this causes a 'number of fractional digits' error in MSIE
            //.tickFormat(d3.format('e'))
                .tickFormat(function (value) {
                    if (value)
                        return cleanNumber(value.toExponential(2));
                    return value;
                });
        },

        // Returns a function, that, as long as it continues to be invoked, will not
        // be triggered. The function will be called after it stops being called for
        // N milliseconds. If `immediate` is passed, trigger the function on the
        // leading edge, instead of the trailing.
        // taken from http://davidwalsh.name/javascript-debounce-function
        debounce: function(func, wait) {
            var timeout;
            return function() {
                var context = this, args = arguments;
                var later = function() {
                    timeout = null;
                    func.apply(context, args);
                };
                clearTimeout(timeout);
                timeout = setTimeout(later, wait);
            };
        },

        extractUnits: function(scope, axis, label) {
            var match = label.match(/\[(.*?)\]/);
            if (match) {
                scope[axis + 'units'] = match[1];
                label = label.replace(/\[.*?\]/, '');
            }
            return label;
        },

        fixFormat: function(scope, axis) {
            var format = d3.format('.3s');
            // amounts near zero may appear as NNNz, change them to 0
            return function(n) {
                var v = format(n);
                if ((v && v.indexOf('z') > 0) || v == '0.00')
                    return '0';
                v = cleanNumber(v);
                var units = scope[axis + 'units'] || '';
                return v + units;
            }
        },

        initialHeight: function(scope) {
            return scope.isAnimation ? 1 : INITIAL_HEIGHT;
        },

        linkPlot: function(scope, element) {
            d3Service.d3().then(function(d3) {
                scope.element = element[0];
                scope.isAnimation = scope.modelName.indexOf('Animation') >= 0;
                var requestData;

                if (scope.isAnimation) {
                    scope.prevFrameIndex = -1;
                    scope.isPlaying = false;
                    requestData = function() {
                        if (! scope.hasFrames())
                            return;
                        var index = frameCache.getCurrentFrame(scope.modelName);
                        if (frameCache.getCurrentFrame(scope.modelName) == scope.prevFrameIndex)
                            return;
                        scope.prevFrameIndex = index;
                        frameCache.getFrame(scope.modelName, index, scope.isPlaying, function(index, data) {
                            if (scope.element)
                                scope.load(data);
                            if (scope.isPlaying)
                                scope.advanceFrame(1);
                        });
                    }
                    scope.advanceFrame = function(increment) {
                        var next = frameCache.getCurrentFrame(scope.modelName) + increment;
                        if (next < 0 || next > frameCache.frameCount - 1) {
                            scope.isPlaying = false;
                            return;
                        }
                        frameCache.setCurrentFrame(scope.modelName, next);
                        requestData();
                    };
                    scope.firstFrame = function() {
                        scope.isPlaying = false;
                        frameCache.setCurrentFrame(scope.modelName, 0);
                        if (scope.modelChanged)
                            scope.modelChanged();
                        requestData();
                    };
                    scope.hasFrames = function() {
                        return frameCache.isLoaded() && frameCache.frameCount > 0;
                    };
                    scope.isFirstFrame = function() {
                        return frameCache.getCurrentFrame(scope.modelName) == 0;
                    };
                    scope.isLastFrame = function() {
                        return frameCache.getCurrentFrame(scope.modelName) == frameCache.frameCount - 1;
                    };
                    scope.lastFrame = function() {
                        scope.isPlaying = false;
                        frameCache.setCurrentFrame(scope.modelName, frameCache.frameCount - 1);
                        requestData();
                    };
                    scope.togglePlay = function() {
                        scope.isPlaying = ! scope.isPlaying;
                        if (scope.isPlaying)
                            scope.advanceFrame(1);
                    };
                    if (scope.clearData)
                        scope.$on('framesCleared', scope.clearData);
                    scope.$on('modelsLoaded', requestData);
                    scope.$on('framesLoaded', function(event, oldFrameCount) {
                        if (scope.prevFrameIndex < 0)
                            scope.firstFrame();
                        else if (oldFrameCount == 0)
                            scope.lastFrame();
                        else if (scope.prevFrameIndex > frameCache.frameCount)
                            scope.firstFrame();
                        // go to the next last frame, if the current frame was the previous last frame
                        else if (frameCache.getCurrentFrame(scope.modelName) == oldFrameCount - 1)
                            scope.lastFrame();
                    });
                }
                else if (scope.isClientOnly) {
                    requestData = function() {};
                }
                else {
                    requestData = function() {
                        //TODO(pjm): timeout is a hack to give time for invalid reports to be destroyed
                        $timeout(function() {
                            if (! scope.element)
                                return;
                            panelState.requestData(scope.modelName, function(data) {
                                if (scope.element)
                                    scope.load(data);
                            });
                        }, 50);
                    }
                }
                scope.$on(
                    scope.modelName + '.changed',
                    function() {
                        scope.prevFrameIndex = -1;
                        if (scope.modelChanged)
                            scope.modelChanged();
                        panelState.clear(scope.modelName);
                        requestData();
                    });
                scope.isLoading = function() {
                    if (scope.isAnimation)
                        return false;
                    return panelState.isLoading(scope.modelName);
                };
                scope.init();
                requestData();
            });
        },

        linspace: function(start, stop, nsteps) {
            var delta = (stop - start) / (nsteps - 1);
            return d3.range(start, stop, delta).slice(0, nsteps);
        },

        ticks: function(axis, width, isHorizontalAxis) {
            var spacing = isHorizontalAxis ? 60 : 40;
            var n = Math.max(Math.round(width / spacing), 2);
            axis.ticks(n);
        },
    };

    function createAxis(scale, orient) {
        return d3.svg.axis()
            .scale(scale)
            .orient(orient);
    }
});

app.directive('plot2d', function(plotting) {
    return {
        restrict: 'A',
        scope: {
            modelName: '@',
        },
        templateUrl: '/static/html/plot2d.html?' + SIREPO_APP_VERSION,
        controller: function($scope) {

            var ASPECT_RATIO = 4.0 / 7;
            $scope.margin = {top: 50, right: 20, bottom: 50, left: 70};
            $scope.width = $scope.height = 0;
            $scope.dataCleared = true;
            var formatter, ordinate_formatter, graphLine, points, xAxis, xAxisGrid, xAxisScale, xPeakValues, xUnits, yAxis, yAxisGrid, yAxisScale, yUnits;
            var defaultCircleSize = null;
            function mouseMove() {
                if (! points)
                    return;
                var x0 = xAxisScale.invert(d3.mouse(this)[0]);
                var localMax = null;
                for (var i = 0; i < xPeakValues.length; i++) {
                    var v = xPeakValues[i];
                    if (localMax === null || Math.abs(v[0] - x0) < Math.abs(localMax[0] - x0)) {
                        localMax = v;
                    }
                }
                if (localMax) {
                    var xPixel = xAxisScale(localMax[0]);
                    if (xPixel < 0 || xPixel >= select('.plot-viewport').attr('width'))
                        return;
                    var focus = select('.focus');
                    focus.style('display', null);
                    var circle = select('circle');
                    if (defaultCircleSize === null) {defaultCircleSize = circle.attr('r')};
                    circle.attr('r', defaultCircleSize);
                    focus.attr('transform', 'translate(' + xPixel + ',' + yAxisScale(localMax[1]) + ')');
                    select('.focus-text').text('[' + formatter(localMax[0]) + ', ' + ordinate_formatter(localMax[1]) + ']');
                }
            };

            function text2array(text) {
                var arr = text.replace('translate(', '').replace(')', '').split(',')
                for(var i=0; i<arr.length; i++) {
                    arr[i] = +arr[i];
                }
                return arr;
            };

            function findY(data, X) {
                var Y = null;
                for (var i=0; i < data.length - 1; i++) {  // i < data.length - 1 because we don't need to include last element
                    if (X >= data[i][0] && X < data[i+1][0]) {
                        Y = data[i][1];
                        break;
                    }
                };
                return Y
            };

            function moveFocus(focus_arr, step) {
                // 'step' in pixels - can be positive (move to the right) and negative (move to the left).
                var xPixel = focus_arr[0] + step;  // move 'step' pixels left or right from the selected peak
                var xValue = xAxisScale.invert(xPixel);  // real X value (in physical units)
                var yValue = findY(points, xValue);  // find real Y value (in physical units)
                var yPixel = yAxisScale(yValue);  // Y in pixels
                if (xPixel < 0 || xPixel >= select('.plot-viewport').attr('width'))
                    return;
                var focus = select('.focus');
                focus.style('display', null);
                var circle = select('circle');
                circle.attr('r', defaultCircleSize - 2);
                focus.attr('transform', 'translate(' + xPixel + ',' + yPixel + ')');
                select('.focus-text').text('[' + formatter(xValue) + ', ' + ordinate_formatter(yValue) + ']');
            };

            function keyboardMove(keyCode, focusString) {
                if (! points)
                    return;
                var focusArray = text2array(focusString);
                var stepPixels = 0.5;
                if (keyCode == 37) { // left
                    moveFocus(focusArray, -1 * stepPixels);
                } else if (keyCode == 39) { // right
                    moveFocus(focusArray, stepPixels);
                }
            };

            function resize() {
                var width = parseInt(select().style('width')) - $scope.margin.left - $scope.margin.right;
                if (! points || isNaN(width))
                    return;
                $scope.width = width;
                $scope.height = ASPECT_RATIO * $scope.width;
                select('svg')
                    .attr('width', $scope.width + $scope.margin.left + $scope.margin.right)
                    .attr('height', $scope.height + $scope.margin.top + $scope.margin.bottom);
                plotting.ticks(xAxis, $scope.width, true);
                plotting.ticks(xAxisGrid, $scope.width, true);
                plotting.ticks(yAxis, $scope.height, false);
                plotting.ticks(yAxisGrid, $scope.height, false);
                xAxisScale.range([-0.5, $scope.width - 0.5]);
                yAxisScale.range([$scope.height - 0.5, 0 - 0.5]).nice();
                xAxisGrid.tickSize(-$scope.height);
                yAxisGrid.tickSize(-$scope.width);
                select('.x.axis').call(xAxis);
                select('.x.axis.grid').call(xAxisGrid); // tickLine == gridline
                select('.y.axis').call(yAxis);
                select('.y.axis.grid').call(yAxisGrid);
                select('.line').attr('d', graphLine);
                return [$scope.width, $scope.height];
            }

            function select(selector) {
                var e = d3.select($scope.element);
                return selector ? e.select(selector) : e;
            }

            function sliderChanged(ev) {
                if (! points)
                    return;
                function computePoint(value) {
                    return Math.round($scope.xRange[0] + (value / 100) * ($scope.xRange[1] - $scope.xRange[0]));
                }
                var xStart = computePoint(ev.value[0]);
                var xEnd = computePoint(ev.value[1]);
                xAxisScale.domain([xStart, xEnd]);

                var yMin, yMax;
                for (var i = 0; i < points.length; i++) {
                    var p = points[i];
                    if (p[0] < xStart)
                        continue;
                    if (p[0] > xEnd)
                        break;
                    if (yMin === undefined || yMin > p[1])
                        yMin = p[1];
                    if (yMax === undefined || yMax < p[1])
                        yMax = p[1];
                }
                yAxisScale.domain([yMin, yMax]);
                resize();
            }

            $scope.clearData = function() {
                $scope.dataCleared = true;
            };

            $scope.init = function() {
                formatter = d3.format('.0f');
                ordinate_formatter = d3.format('.3e');
                select('svg').attr('height', plotting.initialHeight($scope));
                $scope.slider = $(select('.srw-plot2d-slider').node()).slider();
                $scope.slider.on('slide', sliderChanged);
                $(window).resize($scope.windowResize);
                xAxisScale = d3.scale.linear();
                yAxisScale = d3.scale.linear();
                xAxis = plotting.createAxis(xAxisScale, 'bottom');
                xAxis.tickFormat(d3.format('s'));
                xAxisGrid = plotting.createAxis(xAxisScale, 'bottom');
                yAxis = plotting.createExponentialAxis(yAxisScale, 'left');
                yAxisGrid = plotting.createAxis(yAxisScale, 'left');
                graphLine = d3.svg.line()
                    .x(function(d) {return xAxisScale(d[0])})
                    .y(function(d) {return yAxisScale(d[1])});
                var focus = select('.focus');
                var focus_text = select('.focus-text');
                var focus_hint = select('.focus-hint');
                select('.overlay')
                    .on('mouseover', function() {
                        focus.style('display', focus.style('display'));
                        focus_hint.style('display', 'none');
                        focus_hint.text('Double-click to copy the values');
                        d3.select('body')
                            .on('keydown', function() {
                                var focusString = $($scope.element).find('.focus').attr('transform');
                                var keyCode = d3.event.keyCode;
                                if (focusString !== undefined) {
                                    keyboardMove(keyCode, focusString);
                                }
                            });
                    })
                    .on('mouseout', function() {
                        focus_hint.style('display', 'none');
                        d3.select('body').on('keydown', null);
                    })
                    .on('click', mouseMove)
                    .on('dblclick', function copyToClipboard() {
                        var $temp = $('<input>');
{'fraction_non_alphanumeric': 0.11953750370589979, 'fraction_numerical': 0.008627334716869255, 'mean_word_length': 1.1764743837914569, 'pattern_counts': {'":': 0, '<': 32, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2972995', 'n_tokens_mistral': 8850, 'n_tokens_neox': 8149, 'n_words': 1761}