X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph-layout.js;h=4b600f773b8893a617b081a10245493d2b0c8fca;hb=b99970f8bc1a0cbec2c00f4eeafc8d97f0883edb;hp=15638176b320776b9af1af380f016b04a51ec8d7;hpb=2996a18eb00729b2624d8d75e8d24b056c5732bc;p=dygraphs.git diff --git a/dygraph-layout.js b/dygraph-layout.js index 1563817..4b600f7 100644 --- a/dygraph-layout.js +++ b/dygraph-layout.js @@ -35,6 +35,7 @@ var DygraphLayout = function(dygraph) { this.setNames = []; this.annotations = []; this.yAxes_ = null; + this.points = null; // TODO(danvk): it's odd that xTicks_ and yTicks_ are inputs, but xticks and // yticks are outputs. Clean this up. @@ -51,67 +52,81 @@ DygraphLayout.prototype.addDataset = function(setname, set_xy) { this.setNames.push(setname); }; +/** + * Returns the box which the chart should be drawn in. This is the canvas's + * box, less space needed for the axis and chart labels. + * + * @return {{x: number, y: number, w: number, h: number}} + */ DygraphLayout.prototype.getPlotArea = function() { - return this.computePlotArea_(); + return this.area_; }; // Compute the box which the chart should be drawn in. This is the canvas's // box, less space needed for axis and chart labels. -DygraphLayout.prototype.computePlotArea_ = function() { +// NOTE: This should only be called by Dygraph.predraw_(). +DygraphLayout.prototype.computePlotArea = function() { var area = { // TODO(danvk): per-axis setting. x: 0, y: 0 }; - if (this.attr_('drawYAxis')) { - area.x = this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize'); - } area.w = this.dygraph_.width_ - area.x - this.attr_('rightGap'); area.h = this.dygraph_.height_; - if (this.attr_('drawXAxis')) { - if (this.attr_('xAxisHeight')) { - area.h -= this.attr_('xAxisHeight'); - } else { - area.h -= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize'); - } - } - // Shrink the drawing area to accomodate additional y-axes. - if (this.dygraph_.numAxes() == 2) { - // TODO(danvk): per-axis setting. - area.w -= (this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize')); - } else if (this.dygraph_.numAxes() > 2) { - this.dygraph_.error("Only two y-axes are supported at this time. (Trying " + - "to use " + this.dygraph_.numAxes() + ")"); - } - - // Add space for chart labels: title, xlabel and ylabel. - if (this.attr_('title')) { - area.h -= this.attr_('titleHeight'); - area.y += this.attr_('titleHeight'); - } - if (this.attr_('xlabel')) { - area.h -= this.attr_('xLabelHeight'); - } - if (this.attr_('ylabel')) { - // It would make sense to shift the chart here to make room for the y-axis - // label, but the default yAxisLabelWidth is large enough that this results - // in overly-padded charts. The y-axis label should fit fine. If it - // doesn't, the yAxisLabelWidth option can be increased. - } - - if (this.attr_('y2label')) { - // same logic applies here as for ylabel. - // TODO(danvk): make yAxisLabelWidth a per-axis property - } - - // Add space for range selector, if needed. - if (this.attr_('showRangeSelector')) { - area.h -= this.attr_('rangeSelectorHeight') + 4; - } + // Let plugins reserve space. + var e = { + chart_div: this.dygraph_.graphDiv, + reserveSpaceLeft: function(px) { + var r = { + x: area.x, + y: area.y, + w: px, + h: area.h + }; + area.x += px; + area.w -= px; + return r; + }, + reserveSpaceRight: function(px) { + var r = { + x: area.x + area.w - px, + y: area.y, + w: px, + h: area.h + }; + area.w -= px; + return r; + }, + reserveSpaceTop: function(px) { + var r = { + x: area.x, + y: area.y, + w: area.w, + h: px + }; + area.y += px; + area.h -= px; + return r; + }, + reserveSpaceBottom: function(px) { + var r = { + x: area.x, + y: area.y + area.h - px, + w: area.w, + h: px + }; + area.h -= px; + return r; + }, + chartRect: function() { + return {x:area.x, y:area.y, w:area.w, h:area.h}; + } + }; + this.dygraph_.cascadeEvents_('layout', e); - return area; + this.area_ = area; }; DygraphLayout.prototype.setAnnotations = function(ann) { @@ -121,7 +136,7 @@ DygraphLayout.prototype.setAnnotations = function(ann) { var parse = this.attr_('xValueParser') || function(x) { return x; }; for (var i = 0; i < ann.length; i++) { var a = {}; - if (!ann[i].xval && !ann[i].x) { + if (!ann[i].xval && ann[i].x === undefined) { this.dygraph_.error("Annotations must have an 'x' property"); return; } @@ -159,24 +174,11 @@ DygraphLayout.prototype.evaluate = function() { }; DygraphLayout.prototype._evaluateLimits = function() { - this.minxval = this.maxxval = null; - if (this.dateWindow_) { - this.minxval = this.dateWindow_[0]; - this.maxxval = this.dateWindow_[1]; - } else { - for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) { - var series = this.datasets[setIdx]; - if (series.length > 1) { - var x1 = series[0][0]; - if (!this.minxval || x1 < this.minxval) this.minxval = x1; - - var x2 = series[series.length - 1][0]; - if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2; - } - } - } - this.xrange = this.maxxval - this.minxval; - this.xscale = (this.xrange !== 0 ? 1/this.xrange : 1.0); + var xlimits = this.dygraph_.xAxisRange(); + this.minxval = xlimits[0]; + this.maxxval = xlimits[1]; + var xrange = xlimits[1] - xlimits[0]; + this.xscale = (xrange !== 0 ? 1 / xrange : 1.0); for (var i = 0; i < this.yAxes_.length; i++) { var axis = this.yAxes_[i]; @@ -197,8 +199,8 @@ DygraphLayout.prototype._evaluateLimits = function() { } }; -DygraphLayout._calcYNormal = function(axis, value) { - if (axis.logscale) { +DygraphLayout._calcYNormal = function(axis, value, logscale) { + if (logscale) { return 1.0 - ((Dygraph.log10(value) - Dygraph.log10(axis.minyval)) * axis.ylogscale); } else { return 1.0 - ((value - axis.minyval) * axis.yscale); @@ -206,46 +208,68 @@ DygraphLayout._calcYNormal = function(axis, value) { }; DygraphLayout.prototype._evaluateLineCharts = function() { - // add all the rects - this.points = []; - // An array to keep track of how many points will be drawn for each set. - // This will allow for the canvas renderer to not have to check every point - // for every data set since the points are added in order of the sets in - // datasets. - this.setPointsLengths = []; - this.setPointsOffsets = []; + var connectSeparated = this.attr_('connectSeparatedPoints'); - for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) { + // series index -> point index in series -> |point| structure + this.points = new Array(this.datasets.length); + + // TODO(bhs): these loops are a hot-spot for high-point-count charts. In fact, + // on chrome+linux, they are 6 times more expensive than iterating through the + // points and drawing the lines. The brunt of the cost comes from allocating + // the |point| structures. + var boundaryIdStart = this.dygraph_.getLeftBoundary_(); + for (var setIdx = 0; setIdx < this.datasets.length; setIdx++) { var dataset = this.datasets[setIdx]; var setName = this.setNames[setIdx]; var axis = this.dygraph_.axisPropertiesForSeries(setName); + // TODO (konigsberg): use optionsForAxis instead. + var logscale = this.dygraph_.attributes_.getForSeries("logscale", setName); - this.setPointsOffsets.push(this.points.length); - var setPointsLength = 0; + // Preallocating the size of points reduces reallocations, and therefore, + // calls to collect garbage. + var seriesPoints = new Array(dataset.length); for (var j = 0; j < dataset.length; j++) { var item = dataset[j]; - var xValue = parseFloat(item[0]); - var yValue = parseFloat(item[1]); + var xValue = DygraphLayout.parseFloat_(item[0]); + var yValue = DygraphLayout.parseFloat_(item[1]); // Range from 0-1 where 0 represents left and 1 represents right. var xNormal = (xValue - this.minxval) * this.xscale; // Range from 0-1 where 0 represents top and 1 represents bottom - var yNormal = DygraphLayout._calcYNormal(axis, yValue); + var yNormal = DygraphLayout._calcYNormal(axis, yValue, logscale); - var point = { - // TODO(danvk): here + // TODO(danvk): drop the point in this case, don't null it. + // The nulls create complexity in DygraphCanvasRenderer._drawSeries. + if (connectSeparated && item[1] === null) { + yValue = null; + } + seriesPoints[j] = { x: xNormal, y: yNormal, xval: xValue, yval: yValue, - name: setName + name: setName, // TODO(danvk): is this really necessary? + idx: j + boundaryIdStart }; - this.points.push(point); - setPointsLength += 1; } - this.setPointsLengths.push(setPointsLength); + + this.points[setIdx] = seriesPoints; + } +}; + +/** + * Optimized replacement for parseFloat, which was way too slow when almost + * all values were type number, with few edge cases, none of which were strings. + */ +DygraphLayout.parseFloat_ = function(val) { + // parseFloat(null) is NaN + if (val === null) { + return NaN; } + + // Assume it's a number or NaN. If it's something else, I'll be shocked. + return val; }; DygraphLayout.prototype._evaluateLineTicks = function() { @@ -286,24 +310,28 @@ DygraphLayout.prototype.evaluateWithError = function() { // Copy over the error terms var i = 0; // index in this.points for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) { + var points = this.points[setIdx]; var j = 0; var dataset = this.datasets[setIdx]; var setName = this.setNames[setIdx]; var axis = this.dygraph_.axisPropertiesForSeries(setName); + // TODO (konigsberg): use optionsForAxis instead. + var logscale = this.dygraph_.attributes_.getForSeries("logscale", setName); + for (j = 0; j < dataset.length; j++, i++) { var item = dataset[j]; - var xv = parseFloat(item[0]); - var yv = parseFloat(item[1]); + var xv = DygraphLayout.parseFloat_(item[0]); + var yv = DygraphLayout.parseFloat_(item[1]); - if (xv == this.points[i].xval && - yv == this.points[i].yval) { - var errorMinus = parseFloat(item[2]); - var errorPlus = parseFloat(item[3]); + if (xv == points[j].xval && + yv == points[j].yval) { + var errorMinus = DygraphLayout.parseFloat_(item[2]); + var errorPlus = DygraphLayout.parseFloat_(item[3]); var yv_minus = yv - errorMinus; var yv_plus = yv + errorPlus; - this.points[i].y_top = DygraphLayout._calcYNormal(axis, yv_minus); - this.points[i].y_bottom = DygraphLayout._calcYNormal(axis, yv_plus); + points[j].y_top = DygraphLayout._calcYNormal(axis, yv_minus, logscale); + points[j].y_bottom = DygraphLayout._calcYNormal(axis, yv_plus, logscale); } } } @@ -327,12 +355,15 @@ DygraphLayout.prototype._evaluateAnnotations = function() { } // TODO(antrob): loop through annotations not points. - for (i = 0; i < this.points.length; i++) { - var p = this.points[i]; - var k = p.xval + "," + p.name; - if (k in annotations) { - p.annotation = annotations[k]; - this.annotated_points.push(p); + for (var setIdx = 0; setIdx < this.points.length; setIdx++) { + var points = this.points[setIdx]; + for (i = 0; i < points.length; i++) { + var p = points[i]; + var k = p.xval + "," + p.name; + if (k in annotations) { + p.annotation = annotations[k]; + this.annotated_points.push(p); + } } } }; @@ -355,8 +386,12 @@ DygraphLayout.prototype.removeAllDatasets = function() { * Return a copy of the point at the indicated index, with its yval unstacked. * @param int index of point in layout_.points */ -DygraphLayout.prototype.unstackPointAtIndex = function(idx) { - var point = this.points[idx]; +DygraphLayout.prototype.unstackPointAtIndex = function(setIdx, row) { + var point = this.points[setIdx][row]; + // If the point is missing, no unstacking is necessary + if (!Dygraph.isValidPoint(point)) { + return point; + } // Clone the point since we modify it var unstackedPoint = {}; @@ -370,10 +405,14 @@ DygraphLayout.prototype.unstackPointAtIndex = function(idx) { // The unstacked yval is equal to the current yval minus the yval of the // next point at the same xval. - for (var i = idx+1; i < this.points.length; i++) { - if (this.points[i].xval == point.xval) { - unstackedPoint.yval -= this.points[i].yval; - break; + // We need to iterate over setIdx just in case some series have invalid values + // at current row + for(setIdx++; setIdx < this.points.length; setIdx++) { + var nextPoint = this.points[setIdx][row]; + if (nextPoint.xval == point.xval && // should always be true? + Dygraph.isValidPoint(nextPoint)) { + unstackedPoint.yval -= nextPoint.yval; + break; // stop at first valid point } }