1 'use strict';
  2 
  3 /**
  4  * @license
  5  * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
  6  * MIT-licenced: https://opensource.org/licenses/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