Home | History | Annotate | Download | only in elements
      1 <!DOCTYPE html>
      2 <!--
      3 Copyright 2016 The Chromium Authors. All rights reserved.
      4 Use of this source code is governed by a BSD-style license that can be
      5 found in the LICENSE file.
      6 -->
      7 
      8 <polymer-element name="chart-slider" attributes="testpath startrev endrev">
      9   <template>
     10     <style>
     11       #revisions_container {
     12         height: 60px;
     13         width: 100%;
     14       }
     15     </style>
     16     <div id="revisions_container"></div>
     17   </template>
     18   <script>
     19     'use strict';
     20     Polymer('chart-slider', {
     21       edgeDist: 2,
     22 
     23       /**
     24        * Initializes the element. This is a lifecycle callback method.
     25        */
     26       ready: function() {
     27         this.data = null;
     28         this.dragType = 'none';
     29         this.drawable = true;
     30 
     31         // Use addEventListener instead of polymer 'on-' attributes so that we
     32         // can catch the mouse events before Flot does.
     33         this.$.revisions_container.addEventListener(
     34             'mousemove', this.onMouseMove.bind(this), true);
     35         this.$.revisions_container.addEventListener(
     36             'mousedown', this.onMouseDown.bind(this), true);
     37 
     38         // The mouseup listener is placed on the document instead of the graph
     39         // since the user could drag to outside the bounds of the graph.
     40         document.addEventListener('mouseup', this.onMouseUp.bind(this), true);
     41 
     42         this.chartOptions = {
     43           series: {
     44             lines: {
     45               show: true,
     46               fill: 0.2
     47             }
     48           },
     49           grid: {
     50             backgroundColor: '#F1F1F1',
     51             borderWidth: 1,
     52             borderColor: 'rgba(0, 0, 0, 0.5)'
     53           },
     54           crosshair: {
     55             mode: 'x',
     56             color: 'rgba(34, 34, 34, 0.3)',
     57             lineWidth: 0.3
     58           },
     59           selection: {
     60             mode: 'x',
     61             color: 'green'
     62           },
     63           yaxis: {
     64             show: false,
     65             reserveSpace: true,
     66             labelWidth: 60
     67           },
     68           xaxis: {
     69             show: true,
     70             tickFormatter: this.tickFormatter.bind(this)
     71           },
     72           colors: ['#4d90fe']
     73         };
     74 
     75         this.revisionToIndexMap = {};
     76         this.chart = null;
     77         this.resizeHandler = this.onResize.bind(this);
     78         this.resizeTimer = null;
     79         window.addEventListener('resize', this.resizeHandler);
     80       },
     81 
     82       /**
     83        * Updates the element when it's removed. This is a lifecycle callback.
     84        */
     85       leftView: function() {
     86         this.drawable = false;
     87         window.removeEventListener('resize', this.resizeHandler);
     88       },
     89 
     90       /**
     91        * Requests new data to update the graph when the test path is set.
     92        */
     93       testpathChanged: function() {
     94         var postdata = 'test_path=' + encodeURIComponent(this.testpath);
     95         var request = new XMLHttpRequest();
     96         request.onload = this.onLoadGraph.bind(this, request);
     97         request.open('post', '/graph_revisions', true);
     98         request.setRequestHeader(
     99             'Content-Type', 'application/x-www-form-urlencoded');
    100         request.send(postdata);
    101       },
    102 
    103       /**
    104        * Updates the chart when graph data is received.
    105        * @param {XMLHttpRequest} request The request for data.
    106        */
    107       onLoadGraph: function(request) {
    108         if (!this.drawable) {
    109           return;
    110         }
    111         this.data = JSON.parse(request.responseText);
    112         this.selectionMax = this.data.length - 1;
    113         this.revisionToIndexMap = {};
    114         var chartData = [];
    115         for (var i = 0; i < this.data.length; i++) {
    116           chartData.push([i, this.data[i][1]]);
    117           var rev = this.data[i][0];
    118           this.revisionToIndexMap[rev] = i;
    119         }
    120         this.chart = $.plot(
    121             this.$.revisions_container, [{data: chartData}], this.chartOptions);
    122         this.updateSelection();
    123       },
    124 
    125       /**
    126        * Updates the selection state when |this.startrev| is changed.
    127        */
    128       startrevChanged: function() {
    129         this.updateSelection();
    130       },
    131 
    132       /**
    133        * Updates the selection state when |this.endrev| is changed.
    134        */
    135       endrevChanged: function() {
    136         this.updateSelection();
    137       },
    138 
    139       /**
    140        * Updates the selection state when the startrev attribute is changed.
    141        */
    142       updateSelection: function() {
    143         if (!this.startrev ||
    144             !this.endrev ||
    145             !this.revisionToIndexMap ||
    146             !this.chart) {
    147           return;
    148         }
    149 
    150         var startIndex = null;
    151         var endIndex = null;
    152         if (this.startrev in this.revisionToIndexMap) {
    153           startIndex = this.revisionToIndexMap[this.startrev];
    154         } else {
    155           startIndex = this.getPreviousIndexForRev(this.startrev);
    156         }
    157 
    158         if (this.endrev in this.revisionToIndexMap) {
    159           endIndex = this.revisionToIndexMap[this.endrev];
    160         } else {
    161           endIndex = this.getPreviousIndexForRev(this.endrev);
    162         }
    163 
    164         // If this ever happens, just expand the selector to a single bar.
    165         if (startIndex == endIndex) {
    166           if (endIndex == 0) {
    167             endIndex = 1;
    168           } else {
    169             startIndex -= 1;
    170           }
    171         }
    172         this.chart.setSelection({xaxis: {from: startIndex, to: endIndex}},
    173                                 true);
    174       },
    175 
    176       /**
    177        * Get the previous index for a revision number in data series.
    178        * @param {number} revision An X-value.
    179        * @return {number} An index number.
    180        */
    181       getPreviousIndexForRev: function(revision) {
    182         for (var i = this.data.length - 1; i >= 0; i--) {
    183           if (revision > this.data[i][0]) {
    184             return i;
    185           }
    186         }
    187         return 0;
    188       },
    189 
    190       /**
    191        * Formats the labels on the X-axis.
    192        * @param {string|number} xValue An X-value on the mini-plot.
    193        * @param {Object=} opt_axis Not used.
    194        * @return {string} A string to display at one point a long the X-axis.
    195        */
    196       tickFormatter: function(xValue, opt_axis) {
    197         xValue = Math.max(0, Math.round(xValue));
    198         xValue = Math.min(xValue, this.data.length - 1);
    199         if (this.data[xValue] && this.data[xValue][2]) {
    200           var d = new Date(this.data[xValue][2]);
    201           return d.toISOString().substring(0, 10);  // yyyy-mm-dd.
    202         }
    203         console.warn('No timestamp found in chart-slider data at', xValue);
    204         return String(xValue);
    205       },
    206 
    207 
    208       /**
    209        * Determines what stage of a mouse drag selection action the user is in.
    210        * @param {MouseEvent} event Mouse event object.
    211        * @return {string} One of "start", "move", "end", or "none".
    212        */
    213       getMouseDragType: function(event) {
    214         if (!this.chart) {
    215           return 'none';
    216         }
    217         var pos = this.getGraphPosFromMouseEvent(event);
    218         var selection = this.chart.getSelection();
    219         if (!pos || !selection) {
    220           return 'none';
    221         }
    222         if (pos.startDist && pos.startDist < this.edgeDist) {
    223           return 'start';
    224         }
    225         if (pos.endDist && pos.endDist < this.edgeDist) {
    226           return 'end';
    227         }
    228         if (pos.index > selection.xaxis.from &&
    229             pos.index < selection.xaxis.to) {
    230           return 'move';
    231         }
    232         return 'none';
    233       },
    234 
    235       /**
    236        * Determines what the cursor type should be based on a drag type string.
    237        * @param {string} dragType One of "start", "move", "end", or "none".
    238        * @return {string} One of "move", "col-resize", or "auto".
    239        */
    240       getCursorForDragType: function(dragType) {
    241         switch (dragType) {
    242           case 'move':
    243            return 'move';
    244           case 'start':
    245           case 'end':
    246             return 'col-resize';
    247           default:
    248             return 'auto';
    249         }
    250       },
    251 
    252       /**
    253        * Gets the position of the mouse selection relative to the chart.
    254        * @param {MouseEvent} event Mouse event object.
    255        */
    256       getGraphPosFromMouseEvent: function(event) {
    257         var boundingRect = this.$.revisions_container.getBoundingClientRect();
    258         var plotOffset = this.chart.getPlotOffset();
    259         var posX = event.pageX - boundingRect.left - plotOffset.left;
    260         posX = Math.max(0, posX);
    261         posX = Math.min(posX, this.chart.width());
    262         var axes = this.chart.getAxes();
    263         var indexX = Math.round(axes.xaxis.c2p(posX));
    264         var revisionX = this.data[indexX][0];
    265         var pos = {index: indexX, revision: revisionX};
    266         var selection = this.chart.getSelection();
    267         if (selection) {
    268           pos.startDist = Math.abs(axes.xaxis.p2c(selection.xaxis.from) - posX);
    269           pos.endDist = Math.abs(axes.xaxis.p2c(selection.xaxis.to) - posX);
    270         }
    271         return pos;
    272       },
    273 
    274       /**
    275        * Updates the selected revision range as the user moves the mouse.
    276        * @param {MouseEvent} event Mouse event object.
    277        */
    278       onMouseMove: function(event) {
    279         // Stop Flot from handling the selection.
    280         event.stopPropagation();
    281         if (!this.data || this.data.length == 0) {
    282           return;
    283         }
    284         if (this.dragType == 'none') {
    285           var cursor = this.getCursorForDragType(this.getMouseDragType(event));
    286           this.$.revisions_container.style.cursor = cursor;
    287           return;
    288         }
    289 
    290         var pos = this.getGraphPosFromMouseEvent(event);
    291         var diff = this.selectionStart.index - pos.index;
    292         var startIndex = Math.max(0, this.selectionStart.from - diff);
    293         var endIndex = Math.min(
    294             this.selectionStart.to - diff, this.selectionMax);
    295 
    296         // Note: There used to be a constant that determined the max number of
    297         // selectable points, and this function would return early here if the
    298         // number selected exceeded that number; this could be re-added if we
    299         // want to limit the number of selectable points.
    300 
    301         if (this.dragType == 'move' || this.dragType == 'start') {
    302           this.startrev = this.data[startIndex][0];
    303         }
    304         if (this.dragType == 'move' || this.dragType == 'end') {
    305           this.endrev = this.data[endIndex][0];
    306         }
    307       },
    308 
    309       /**
    310        * Sets the selection start when the user starts to drag.
    311        * @param {MouseEvent} event Mouse event object.
    312        */
    313       onMouseDown: function(event) {
    314         // Stop Flot from handling the selection.
    315         event.stopPropagation();
    316 
    317         this.dragType = this.getMouseDragType(event);
    318         if (this.dragType == 'none') {
    319           return;
    320         }
    321 
    322         var selection = this.chart.getSelection();
    323         var from = Math.max(0, Math.round(selection.xaxis.from));
    324         var to = Math.min(Math.round(selection.xaxis.to), this.data.length - 1);
    325         var pos = this.getGraphPosFromMouseEvent(event);
    326         this.selectionStart = {
    327           index: pos.index,
    328           from: from,
    329           to: to
    330         };
    331         document.body.style.cursor = this.getCursorForDragType(this.dragType);
    332 
    333         // Stop text selection (screws up cursor).
    334         event.preventDefault();
    335       },
    336 
    337       /**
    338        * Fires a "revisionrange" event when the user is finished selecting.
    339        * @param {MouseEvent} event A "mouseup" event.
    340        */
    341       onMouseUp: function(event) {
    342         if (this.dragType == 'none') {
    343           return;
    344         }
    345         this.dragType = 'none';
    346         var selection = this.chart.getSelection().xaxis;
    347         document.body.style.cursor = 'auto';
    348         this.$.revisions_container.style.cursor = 'auto';
    349         if (selection.from == this.selectionStart.from &&
    350             selection.to == this.selectionStart.to) {
    351           return;
    352         }
    353         var detail = {
    354             start_rev: this.startrev,
    355             end_rev: this.endrev
    356         };
    357         this.fire('revisionrange', detail);
    358       },
    359 
    360       /**
    361        * Sets a timer to resize after a certain amount of time.
    362        */
    363       onResize: function(event) {
    364         // Try not to resize graphs until the user has stopped resizing.
    365         clearTimeout(this.resizeTimer);
    366         this.resizeTimer = setTimeout(this.resizeGraph.bind(this), 100);
    367       },
    368 
    369       /**
    370        * Resizes the graph.
    371        */
    372       resizeGraph: function() {
    373         if (!this.chart) {
    374           return;
    375         }
    376         this.chart.resize();
    377         this.chart.setupGrid();
    378         this.chart.draw();
    379         this.updateSelection();
    380       }
    381     });
    382   </script>
    383 </polymer-element>
    384