From a12a78ae06c196aa0d3211ab57b584367504b9a0 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 20 Jul 2012 13:58:53 -0400 Subject: [PATCH 1/1] first stab at keying DygraphLayout.points on setIdx --- dygraph-canvas.js | 21 ++++++------- dygraph-layout.js | 76 ++++++++++++++++++++++------------------------- dygraph.js | 89 +++++++++++++++++++++++++++++-------------------------- 3 files changed, 94 insertions(+), 92 deletions(-) diff --git a/dygraph-canvas.js b/dygraph-canvas.js index 5b7c973..9263250 100644 --- a/dygraph-canvas.js +++ b/dygraph-canvas.js @@ -247,19 +247,17 @@ DygraphCanvasRenderer._predicateThatSkipsEmptyPoints = * @private */ DygraphCanvasRenderer.prototype._drawStyledLine = function( - ctx, i, setName, color, strokeWidth, strokePattern, drawPoints, + ctx, setIdx, setName, color, strokeWidth, strokePattern, drawPoints, drawPointCallback, pointSize) { // TODO(konigsberg): Compute attributes outside this method call. var stepPlot = this.attr_("stepPlot"); - var firstIndexInSet = this.layout.setPointsOffsets[i]; - var setLength = this.layout.setPointsLengths[i]; - var points = this.layout.points; if (!Dygraph.isArrayLike(strokePattern)) { strokePattern = null; } var drawGapPoints = this.dygraph_.attr_('drawGapEdgePoints', setName); - var iter = Dygraph.createIterator(points, firstIndexInSet, setLength, + var points = this.layout.points[setIdx]; + var iter = Dygraph.createIterator(points, 0, points.length, DygraphCanvasRenderer._getIteratorPredicate( this.attr_("connectSeparatedPoints"))); @@ -428,11 +426,14 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { // unaffected. An alternative is to reduce the stroke width in the // transformed coordinate space, but you can't specify different values for // each dimension (as you can with .scale()). The speedup here is ~12%. - var points = this.layout.points; - for (i = points.length; i--;) { - var point = points[i]; - point.canvasx = this.area.w * point.x + this.area.x; - point.canvasy = this.area.h * point.y + this.area.y; + var sets = this.layout.points; + for (i = sets.length; i--;) { + var points = sets[i]; + for (var j = points.length; j--;) { + var point = points[j]; + point.canvasx = this.area.w * point.x + this.area.x; + point.canvasy = this.area.h * point.y + this.area.y; + } } // Draw any "fills", i.e. error bars or the filled area under a series. diff --git a/dygraph-layout.js b/dygraph-layout.js index a5e6908..e25afba 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. @@ -218,35 +219,24 @@ DygraphLayout._calcYNormal = function(axis, value) { }; DygraphLayout.prototype._evaluateLineCharts = function() { - // 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'); + + // 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 i = 0; - var setIdx; - - // Preallocating the size of points reduces reallocations, and therefore, - // calls to collect garbage. - var totalPoints = 0; - for (setIdx = 0; setIdx < this.datasets.length; ++setIdx) { - totalPoints += this.datasets[setIdx].length; - } - this.points = new Array(totalPoints); - - for (setIdx = 0; setIdx < this.datasets.length; ++setIdx) { - this.setPointsOffsets.push(i); + 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); + // 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 = DygraphLayout.parseFloat_(item[0]); @@ -257,20 +247,21 @@ DygraphLayout.prototype._evaluateLineCharts = function() { // Range from 0-1 where 0 represents top and 1 represents bottom var yNormal = DygraphLayout._calcYNormal(axis, yValue); + // 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; } - this.points[i] = { - // TODO(danvk): here + seriesPoints[j] = { x: xNormal, y: yNormal, xval: xValue, yval: yValue, - name: setName + name: setName // TODO(danvk): is this really necessary? }; - i++; } - this.setPointsLengths.push(i - this.setPointsOffsets[setIdx]); + + this.points[setIdx] = seriesPoints; } }; @@ -326,6 +317,7 @@ 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]; @@ -335,15 +327,15 @@ DygraphLayout.prototype.evaluateWithError = function() { var xv = DygraphLayout.parseFloat_(item[0]); var yv = DygraphLayout.parseFloat_(item[1]); - if (xv == this.points[i].xval && - yv == this.points[i].yval) { + 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); + points[j].y_bottom = DygraphLayout._calcYNormal(axis, yv_plus); } } } @@ -367,12 +359,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); + } } } }; @@ -395,8 +390,8 @@ 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 (!point.yval) { return point; @@ -414,9 +409,10 @@ 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) && this.points[i].yval) { - unstackedPoint.yval -= this.points[i].yval; + var points = this.points[setIdx]; + for (var i = row + 1; i < points.length; i++) { + if ((points[i].xval == point.xval) && points[i].yval) { + unstackedPoint.yval -= points[i].yval; break; } } diff --git a/dygraph.js b/dygraph.js index 5e10d06..f97494a 100644 --- a/dygraph.js +++ b/dygraph.js @@ -1597,19 +1597,25 @@ Dygraph.prototype.eventToDomCoords = function(event) { */ Dygraph.prototype.findClosestRow = function(domX) { var minDistX = Infinity; - var idx = -1; - var points = this.layout_.points; - var l = points.length; - for (var i = 0; i < l; i++) { - var point = points[i]; - if (!Dygraph.isValidPoint(point, true)) continue; - var dist = Math.abs(point.canvasx - domX); - if (dist < minDistX) { - minDistX = dist; - idx = i; + var pointIdx = -1, setIdx = -1; + var sets = this.layout_.points; + for (var i = 0; i < sets.length; i++) { + var points = sets[i]; + var len = points.length; + for (var j = 0; j < len; j++) { + var point = points[j]; + if (!Dygraph.isValidPoint(point, true)) continue; + var dist = Math.abs(point.canvasx - domX); + if (dist < minDistX) { + minDistX = dist; + setIdx = i; + pointIdx = j; + } } } - return this.idxToRow_(idx); + + // TODO(danvk): remove this function; it's trivial and has only one use. + return this.idxToRow_(setIdx, pointIdx); }; /** @@ -1627,13 +1633,11 @@ Dygraph.prototype.findClosestRow = function(domX) { Dygraph.prototype.findClosestPoint = function(domX, domY) { var minDist = Infinity; var idx = -1; - var points = this.layout_.points; var dist, dx, dy, point, closestPoint, closestSeries; for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - var first = this.layout_.setPointsOffsets[setIdx]; - var len = this.layout_.setPointsLengths[setIdx]; - for (var i = 0; i < len; ++i) { - var point = points[first + i]; + var points = this.layout_.points[setIdx]; + for (var i = 0; i < points.length; ++i) { + var point = points[i]; if (!Dygraph.isValidPoint(point)) continue; dx = point.canvasx - domX; dy = point.canvasy - domY; @@ -1670,18 +1674,17 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { var row = this.findClosestRow(domX); var boundary = this.getLeftBoundary_(); var rowIdx = row - boundary; - var points = this.layout_.points; + var sets = this.layout_.points; var closestPoint, closestSeries; for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - var first = this.layout_.setPointsOffsets[setIdx]; - var len = this.layout_.setPointsLengths[setIdx]; - if (rowIdx >= len) continue; - var p1 = points[first + rowIdx]; + var points = this.layout_.points[setIdx]; + if (rowIdx >= points.length) continue; + var p1 = points[rowIdx]; if (!Dygraph.isValidPoint(p1)) continue; var py = p1.canvasy; - if (domX > p1.canvasx && rowIdx + 1 < len) { + if (domX > p1.canvasx && rowIdx + 1 < points.length) { // interpolate series Y value using next point - var p2 = points[first + rowIdx + 1]; + var p2 = points[rowIdx + 1]; if (Dygraph.isValidPoint(p2)) { var dx = p2.canvasx - p1.canvasx; if (dx > 0) { @@ -1691,7 +1694,7 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { } } else if (domX < p1.canvasx && rowIdx > 0) { // interpolate series Y value using previous point - var p0 = points[first + rowIdx - 1]; + var p0 = points[rowIdx - 1]; if (Dygraph.isValidPoint(p0)) { var dx = p1.canvasx - p0.canvasx; if (dx > 0) { @@ -1724,7 +1727,7 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { Dygraph.prototype.mouseMove_ = function(event) { // This prevents JS errors when mousing over the canvas before data loads. var points = this.layout_.points; - if (points === undefined) return; + if (points === undefined || points === null) return; var canvasCoords = this.eventToDomCoords(event); var canvasx = canvasCoords[0]; @@ -1770,18 +1773,19 @@ Dygraph.prototype.getLeftBoundary_ = function() { * @return int row number, or -1 if none could be found. * @private */ -Dygraph.prototype.idxToRow_ = function(idx) { - if (idx < 0) return -1; +Dygraph.prototype.idxToRow_ = function(setIdx, rowIdx) { + if (rowIdx < 0) return -1; var boundary = this.getLeftBoundary_(); - for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - var set = this.layout_.datasets[setIdx]; - if (idx < set.length) { - return boundary + idx; - } - idx -= set.length; - } - return -1; + return boundary + rowIdx; + // for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { + // var set = this.layout_.datasets[setIdx]; + // if (idx < set.length) { + // return boundary + idx; + // } + // idx -= set.length; + // } + // return -1; }; Dygraph.prototype.animateSelection_ = function(direction) { @@ -1906,7 +1910,6 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { Dygraph.prototype.setSelection = function(row, opt_seriesName) { // Extract the points we've selected this.selPoints_ = []; - var pos = 0; if (row !== false) { row -= this.getLeftBoundary_(); @@ -1919,15 +1922,14 @@ Dygraph.prototype.setSelection = function(row, opt_seriesName) { for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { var set = this.layout_.datasets[setIdx]; if (row < set.length) { - var point = this.layout_.points[pos+row]; + var point = this.layout_.points[setIdx][row]; if (this.attr_("stackedGraph")) { - point = this.layout_.unstackPointAtIndex(pos+row); + point = this.layout_.unstackPointAtIndex(setIdx, row); } if (!(point.yval === null)) this.selPoints_.push(point); } - pos += set.length; } } else { if (this.lastRow_ >= 0) changed = true; @@ -1996,9 +1998,12 @@ Dygraph.prototype.getSelection = function() { return -1; } - for (var row=0; row