      1 'use strict';
      2
      3 /**
      4  * @license
      5  * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
      6  * MIT-licenced: https://opensource.org/license/MIT
      7  */
      8
      9 /**
     10  * @fileoverview Based on PlotKitLayout, but modified to meet the needs of
     11  * dygraphs.
     12  */
     13
     14 /*global Dygraph:false */
     15
     16 import * as utils from './dygraph-utils';
     17
     18 /**
     19  * Creates a new DygraphLayout object.
     20  *
     21  * This class contains all the data to be charted.
     22  * It uses data coordinates, but also records the chart range (in data
     23  * coordinates) and hence is able to calculate percentage positions ('In this
     24  * view, Point A lies 25% down the x-axis.')
     25  *
     26  * Two things that it does not do are:
     27  * 1. Record pixel coordinates for anything.
     28  * 2. (oddly) determine anything about the layout of chart elements.
     29  *
     30  * The naming is a vestige of Dygraph's original PlotKit roots.
     31  *
     32  * @constructor
     33  */
     34 var DygraphLayout = function(dygraph) {
     35   this.dygraph_ = dygraph;
     36   /**
     37    * Array of points for each series.
     38    *
     39    * [series index][row index in series] = |Point| structure,
     40    * where series index refers to visible series only, and the
     41    * point index is for the reduced set of points for the current
     42    * zoom region (including one point just outside the window).
     43    * All points in the same row index share the same X value.
     44    *
     45    * @type {Array.<Array.<Dygraph.PointType>>}
     46    */
     47   this.points = [];
     48   this.setNames = [];
     49   this.annotations = [];
     50   this.yAxes_ = null;
     51
     52   // TODO(danvk): it's odd that xTicks_ and yTicks_ are inputs,
     53   // but xticks and yticks are outputs. Clean this up.
     54   this.xTicks_ = null;
     55   this.yTicks_ = null;
     56 };
     57
     58 /**
     59  * Add points for a single series.
     60  *
     61  * @param {string} setname Name of the series.
     62  * @param {Array.<Dygraph.PointType>} set_xy Points for the series.
     63  */
     64 DygraphLayout.prototype.addDataset = function(setname, set_xy) {
     65   this.points.push(set_xy);
     66   this.setNames.push(setname);
     67 };
     68
     69 /**
     70  * Returns the box which the chart should be drawn in. This is the canvas's
     71  * box, less space needed for the axis and chart labels.
     72  *
     73  * @return {{x: number, y: number, w: number, h: number}}
     74  */
     75 DygraphLayout.prototype.getPlotArea = function() {
     76   return this.area_;
     77 };
     78
     79 // Compute the box which the chart should be drawn in. This is the canvas's
     80 // box, less space needed for axis, chart labels, and other plug-ins.
     81 // NOTE: This should only be called by Dygraph.predraw_().
     82 DygraphLayout.prototype.computePlotArea = function() {
     83   var area = {
     84     // TODO(danvk): per-axis setting.
     85     x: 0,
     86     y: 0
     87   };
     88
     89   area.w = this.dygraph_.width_ - area.x - this.dygraph_.getOption('rightGap');
     90   area.h = this.dygraph_.height_;
     91
     92   // Let plugins reserve space.
     93   var e = {
     94     chart_div: this.dygraph_.graphDiv,
     95     reserveSpaceLeft: function(px) {
     96       var r = {
     97         x: area.x,
     98         y: area.y,
     99         w: px,
    100         h: area.h
    101       };
    102       area.x += px;
    103       area.w -= px;
    104       return r;
    105     },
    106     reserveSpaceRight: function(px) {
    107       var r = {
    108         x: area.x + area.w - px,
    109         y: area.y,
    110         w: px,
    111         h: area.h
    112       };
    113       area.w -= px;
    114       return r;
    115     },
    116     reserveSpaceTop: function(px) {
    117       var r = {
    118         x: area.x,
    119         y: area.y,
    120         w: area.w,
    121         h: px
    122       };
    123       area.y += px;
    124       area.h -= px;
    125       return r;
    126     },
    127     reserveSpaceBottom: function(px) {
    128       var r = {
    129         x: area.x,
    130         y: area.y + area.h - px,
    131         w: area.w,
    132         h: px
    133       };
    134       area.h -= px;
    135       return r;
    136     },
    137     chartRect: function() {
    138       return {x:area.x, y:area.y, w:area.w, h:area.h};
    139     }
    140   };
    141   this.dygraph_.cascadeEvents_('layout', e);
    142
    143   this.area_ = area;
    144 };
    145
    146 DygraphLayout.prototype.setAnnotations = function(ann) {
    147   // The Dygraph object's annotations aren't parsed. We parse them here and
    148   // save a copy. If there is no parser, then the user must be using raw format.
    149   this.annotations = [];
    150   var parse = this.dygraph_.getOption('xValueParser') || function(x) { return x; };
    151   for (var i = 0; i < ann.length; i++) {
    152     var a = {};
    153     if (!ann[i].xval && ann[i].x === undefined) {
    154       console.error("Annotations must have an 'x' property");
    155       return;
    156     }
    157     if (ann[i].icon &&
    158         !(ann[i].hasOwnProperty('width') &&
    159           ann[i].hasOwnProperty('height'))) {
    160       console.error("Must set width and height when setting " +
    161                     "annotation.icon property");
    162       return;
    163     }
    164     utils.update(a, ann[i]);
    165     if (!a.xval) a.xval = parse(a.x);
    166     this.annotations.push(a);
    167   }
    168 };
    169
    170 DygraphLayout.prototype.setXTicks = function(xTicks) {
    171   this.xTicks_ = xTicks;
    172 };
    173
    174 // TODO(danvk): add this to the Dygraph object's API or move it into Layout.
    175 DygraphLayout.prototype.setYAxes = function (yAxes) {
    176   this.yAxes_ = yAxes;
    177 };
    178
    179 DygraphLayout.prototype.evaluate = function() {
    180   this._xAxis = {};
    181   this._evaluateLimits();
    182   this._evaluateLineCharts();
    183   this._evaluateLineTicks();
    184   this._evaluateAnnotations();
    185 };
    186
    187 DygraphLayout.prototype._evaluateLimits = function() {
    188   var xlimits = this.dygraph_.xAxisRange();
    189   this._xAxis.minval = xlimits[0];
    190   this._xAxis.maxval = xlimits[1];
    191   var xrange = xlimits[1] - xlimits[0];
    192   this._xAxis.scale = (xrange !== 0 ? 1 / xrange : 1.0);
    193
    194   if (this.dygraph_.getOptionForAxis("logscale", 'x')) {
    195     this._xAxis.xlogrange = utils.log10(this._xAxis.maxval) - utils.log10(this._xAxis.minval);
    196     this._xAxis.xlogscale = (this._xAxis.xlogrange !== 0 ? 1.0 / this._xAxis.xlogrange : 1.0);
    197   }
    198   for (var i = 0; i < this.yAxes_.length; i++) {
    199     var axis = this.yAxes_[i];
    200     axis.minyval = axis.computedValueRange[0];
    201     axis.maxyval = axis.computedValueRange[1];
    202     axis.yrange = axis.maxyval - axis.minyval;
    203     axis.yscale = (axis.yrange !== 0 ? 1.0 / axis.yrange : 1.0);
    204
    205     if (this.dygraph_.getOption("logscale") || axis.logscale) {
    206       axis.ylogrange = utils.log10(axis.maxyval) - utils.log10(axis.minyval);
    207       axis.ylogscale = (axis.ylogrange !== 0 ? 1.0 / axis.ylogrange : 1.0);
    208       if (!isFinite(axis.ylogrange) || isNaN(axis.ylogrange)) {
    209         console.error('axis ' + i + ' of graph at ' + axis.g +
    210                       ' can\'t be displayed in log scale for range [' +
    211                       axis.minyval + ' - ' + axis.maxyval + ']');
    212       }
    213     }
    214   }
    215 };
    216
    217 DygraphLayout.calcXNormal_ = function(value, xAxis, logscale) {
    218   if (logscale) {
    219     return ((utils.log10(value) - utils.log10(xAxis.minval)) * xAxis.xlogscale);
    220   } else {
    221     return (value - xAxis.minval) * xAxis.scale;
    222   }
    223 };
    224
    225 /**
    226  * @param {DygraphAxisType} axis
    227  * @param {number} value
    228  * @param {boolean} logscale
    229  * @return {number}
    230  */
    231 DygraphLayout.calcYNormal_ = function(axis, value, logscale) {
    232   if (logscale) {
    233     var x = 1.0 - ((utils.log10(value) - utils.log10(axis.minyval)) * axis.ylogscale);
    234     return isFinite(x) ? x : NaN;  // shim for v8 issue; see pull request 276
    235   } else {
    236     return 1.0 - ((value - axis.minyval) * axis.yscale);
    237   }
    238 };
    239
    240 DygraphLayout.prototype._evaluateLineCharts = function() {
    241   var isStacked = this.dygraph_.getOption("stackedGraph");
    242   var isLogscaleForX = this.dygraph_.getOptionForAxis("logscale", 'x');
    243
    244   for (var setIdx = 0; setIdx < this.points.length; setIdx++) {
    245     var points = this.points[setIdx];
    246     var setName = this.setNames[setIdx];
    247     var connectSeparated = this.dygraph_.getOption('connectSeparatedPoints', setName);
    248     var axis = this.dygraph_.axisPropertiesForSeries(setName);
    249     // TODO (konigsberg): use optionsForAxis instead.
    250     var logscale = this.dygraph_.attributes_.getForSeries("logscale", setName);
    251     var outOfXBounds = 0, outOfYBounds = 0;
    252
    253     for (var j = 0; j < points.length; j++) {
    254       var point = points[j];
    255
    256       // Range from 0-1 where 0 represents left and 1 represents right.
    257       point.x = DygraphLayout.calcXNormal_(point.xval, this._xAxis, isLogscaleForX);
    258       outOfXBounds += (point.x < 0) || (point.x > 1);
    259       // Range from 0-1 where 0 represents top and 1 represents bottom
    260       var yval = point.yval;
    261       if (isStacked) {
    262         point.y_stacked = DygraphLayout.calcYNormal_(
    263             axis, point.yval_stacked, logscale);
    264         if (yval !== null && !isNaN(yval)) {
    265           yval = point.yval_stacked;
    266         }
    267       }
    268       if (yval === null) {
    269         yval = NaN;
    270         if (!connectSeparated) {
    271           point.yval = NaN;
    272         }
    273       }
    274       point.y = DygraphLayout.calcYNormal_(axis, yval, logscale);
    275       outOfYBounds += (point.y < 0) || (point.y > 1);
    276     }
    277
    278     if (outOfXBounds > 2) {
    279       console.warn(outOfXBounds + ' points out of X bounds:' + this._xAxis.minval + ' - ' + this._xAxis.maxval);
    280     }
    281     if (outOfYBounds > 0) {
    282       console.warn(outOfYBounds + ' points out of Y bounds:' + axis.minyval + ' - ' + axis.maxyval);
    283     }
    284
    285     this.dygraph_.dataHandler_.onLineEvaluated(points, axis, logscale);
    286   }
    287 };
    288
    289 DygraphLayout.prototype._evaluateLineTicks = function() {
    290   var i, tick, label, pos, v, has_tick;
    291   this.xticks = [];
    292   for (i = 0; i < this.xTicks_.length; i++) {
    293     tick = this.xTicks_[i];
    294     label = tick.label;
    295     has_tick = !('label_v' in tick);
    296     v = has_tick ? tick.v : tick.label_v;
    297     pos = this.dygraph_.toPercentXCoord(v);
    298     if ((pos >= 0.0) && (pos < 1.0)) {
    299       this.xticks.push({pos, label, has_tick});
    300     }
    301   }
    302
    303   this.yticks = [];
    304   for (i = 0; i < this.yAxes_.length; i++ ) {
    305     var axis = this.yAxes_[i];
    306     for (var j = 0; j < axis.ticks.length; j++) {
    307       tick = axis.ticks[j];
    308       label = tick.label;
    309       has_tick = !('label_v' in tick);
    310       v = has_tick ? tick.v : tick.label_v;
    311       pos = this.dygraph_.toPercentYCoord(v, i);
    312       if ((pos > 0.0) && (pos <= 1.0)) {
    313         this.yticks.push({axis: i, pos, label, has_tick});
    314       }
    315     }
    316   }
    317 };
    318
    319 DygraphLayout.prototype._evaluateAnnotations = function() {
    320   // Add the annotations to the point to which they belong.
    321   // Make a map from (setName, xval) to annotation for quick lookups.
    322   var i;
    323   var annotations = {};
    324   for (i = 0; i < this.annotations.length; i++) {
    325     var a = this.annotations[i];
    326     annotations[a.xval + "," + a.series] = a;
    327   }
    328
    329   this.annotated_points = [];
    330
    331   // Exit the function early if there are no annotations.
    332   if (!this.annotations || !this.annotations.length) {
    333     return;
    334   }
    335
    336   // TODO(antrob): loop through annotations not points.
    337   for (var setIdx = 0; setIdx < this.points.length; setIdx++) {
    338     var points = this.points[setIdx];
    339     for (i = 0; i < points.length; i++) {
    340       var p = points[i];
    341       var k = p.xval + "," + p.name;
    342       if (k in annotations) {
    343         p.annotation = annotations[k];
    344         this.annotated_points.push(p);
    345         //if there are multiple same x-valued points, the annotation would be rendered multiple times
    346         //remove already rendered annotation
    347         delete annotations[k];
    348       }
    349     }
    350   }
    351 };
    352
    353 /**
    354  * Convenience function to remove all the data sets from a graph
    355  */
    356 DygraphLayout.prototype.removeAllDatasets = function() {
    357   delete this.points;
    358   delete this.setNames;
    359   delete this.setPointsLengths;
    360   delete this.setPointsOffsets;
    361   this.points = [];
    362   this.setNames = [];
    363   this.setPointsLengths = [];
    364   this.setPointsOffsets = [];
    365 };
    366
    367 export default DygraphLayout;
    368