X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph-canvas.js;h=7ce34bf39b4483547f44697922ca339acbe71171;hb=ea4942ed6644e9ae7ce1e955ecbfb67666f051dc;hp=53eac5d884aefea0a70b96783d7be6ab09091ea3;hpb=bbf72e4acf7a2bf30d414039a37b1cc30853597d;p=dygraphs.git diff --git a/dygraph-canvas.js b/dygraph-canvas.js index 53eac5d..7ce34bf 100644 --- a/dygraph-canvas.js +++ b/dygraph-canvas.js @@ -19,6 +19,7 @@ DygraphLayout = function(dygraph, options) { this.options = {}; // TODO(danvk): remove, use attr_ instead. Dygraph.update(this.options, options ? options : {}); this.datasets = new Array(); + this.annotations = new Array() }; DygraphLayout.prototype.attr_ = function(name) { @@ -29,10 +30,34 @@ DygraphLayout.prototype.addDataset = function(setname, set_xy) { this.datasets[setname] = set_xy; }; +DygraphLayout.prototype.setAnnotations = function(ann) { + // The Dygraph object's annotations aren't parsed. We parse them here and + // save a copy. + var parse = this.attr_('xValueParser'); + for (var i = 0; i < ann.length; i++) { + var a = {}; + if (!ann[i].xval && !ann[i].x) { + this.dygraph_.error("Annotations must have an 'x' property"); + return; + } + if (ann[i].icon && + !(ann[i].hasOwnProperty('width') && + ann[i].hasOwnProperty('height'))) { + this.dygraph_.error("Must set width and height when setting " + + "annotation.icon property"); + return; + } + Dygraph.update(a, ann[i]); + if (!a.xval) a.xval = parse(a.x); + this.annotations.push(a); + } +}; + DygraphLayout.prototype.evaluate = function() { this._evaluateLimits(); this._evaluateLineCharts(); this._evaluateLineTicks(); + this._evaluateAnnotations(); }; DygraphLayout.prototype._evaluateLimits = function() { @@ -54,10 +79,13 @@ DygraphLayout.prototype._evaluateLimits = function() { this.xrange = this.maxxval - this.minxval; this.xscale = (this.xrange != 0 ? 1/this.xrange : 1.0); - this.minyval = this.options.yAxis[0]; - this.maxyval = this.options.yAxis[1]; - this.yrange = this.maxyval - this.minyval; - this.yscale = (this.yrange != 0 ? 1/this.yrange : 1.0); + for (var i = 0; i < this.options.yAxes.length; i++) { + var axis = this.options.yAxes[i]; + axis.minyval = axis.valueRange[0]; + axis.maxyval = axis.valueRange[1]; + axis.yrange = axis.maxyval - axis.minyval; + axis.yscale = (axis.yrange != 0 ? 1.0 / axis.yrange : 1.0); + } }; DygraphLayout.prototype._evaluateLineCharts = function() { @@ -67,12 +95,14 @@ DygraphLayout.prototype._evaluateLineCharts = function() { if (!this.datasets.hasOwnProperty(setName)) continue; var dataset = this.datasets[setName]; + var axis = this.options.yAxes[this.options.seriesToAxisMap[setName]]; + for (var j = 0; j < dataset.length; j++) { var item = dataset[j]; var point = { // TODO(danvk): here x: ((parseFloat(item[0]) - this.minxval) * this.xscale), - y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale), + y: 1.0 - ((parseFloat(item[1]) - axis.minyval) * axis.yscale), xval: parseFloat(item[0]), yval: parseFloat(item[1]), name: setName @@ -85,9 +115,7 @@ DygraphLayout.prototype._evaluateLineCharts = function() { if (point.y >= 1.0) { point.y = 1.0; } - if ((point.x >= 0.0) && (point.x <= 1.0)) { - this.points.push(point); - } + this.points.push(point); } } }; @@ -143,6 +171,26 @@ DygraphLayout.prototype.evaluateWithError = function() { } }; +DygraphLayout.prototype._evaluateAnnotations = function() { + // Add the annotations to the point to which they belong. + // Make a map from (setName, xval) to annotation for quick lookups. + var annotations = {}; + for (var i = 0; i < this.annotations.length; i++) { + var a = this.annotations[i]; + annotations[a.xval + "," + a.series] = a; + } + + this.annotated_points = []; + for (var 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); + } + } +}; + /** * Convenience function to remove all the data sets from a graph */ @@ -188,7 +236,8 @@ DygraphCanvasRenderer = function(dygraph, element, layout, options) { "drawYGrid": true, "drawXGrid": true, "gridLineColor": "rgb(128,128,128)", - "fillAlpha": 0.15 + "fillAlpha": 0.15, + "underlayCallback": null }; Dygraph.update(this.options, options); @@ -206,7 +255,9 @@ DygraphCanvasRenderer = function(dygraph, element, layout, options) { // internal state this.xlabels = new Array(); this.ylabels = new Array(); + this.annotations = new Array(); + // TODO(danvk): consider all axes in this computation. this.area = { x: this.options.yAxisLabelWidth + 2 * this.options.axisTickSize, y: 0 @@ -248,8 +299,13 @@ DygraphCanvasRenderer.prototype.clear = function() { var el = this.ylabels[i]; el.parentNode.removeChild(el); } + for (var i = 0; i < this.annotations.length; i++) { + var el = this.annotations[i]; + el.parentNode.removeChild(el); + } this.xlabels = new Array(); this.ylabels = new Array(); + this.annotations = new Array(); }; @@ -278,6 +334,11 @@ DygraphCanvasRenderer.isSupported = function(canvasName) { DygraphCanvasRenderer.prototype.render = function() { // Draw the new X/Y grid var ctx = this.element.getContext("2d"); + + if (this.options.underlayCallback) { + this.options.underlayCallback(ctx, this.area, this.layout, this.dygraph_); + } + if (this.options.drawYGrid) { var ticks = this.layout.yticks; ctx.save(); @@ -313,6 +374,7 @@ DygraphCanvasRenderer.prototype.render = function() { // Do the ordinary rendering, as before this._renderLineChart(); this._renderAxis(); + this._renderAnnotations(); }; @@ -440,6 +502,104 @@ DygraphCanvasRenderer.prototype._renderAxis = function() { }; +DygraphCanvasRenderer.prototype._renderAnnotations = function() { + var annotationStyle = { + "position": "absolute", + "fontSize": this.options.axisLabelFontSize + "px", + "zIndex": 10, + "overflow": "hidden" + }; + + var bindEvt = function(eventName, classEventName, p, self) { + return function(e) { + var a = p.annotation; + if (a.hasOwnProperty(eventName)) { + a[eventName](a, p, self.dygraph_, e); + } else if (self.dygraph_.attr_(classEventName)) { + self.dygraph_.attr_(classEventName)(a, p, self.dygraph_,e ); + } + }; + } + + // Get a list of point with annotations. + var points = this.layout.annotated_points; + for (var i = 0; i < points.length; i++) { + var p = points[i]; + if (p.canvasx < this.area.x || p.canvasx > this.area.x + this.area.w) { + continue; + } + + var a = p.annotation; + var tick_height = 6; + if (a.hasOwnProperty("tickHeight")) { + tick_height = a.tickHeight; + } + + var div = document.createElement("div"); + for (var name in annotationStyle) { + if (annotationStyle.hasOwnProperty(name)) { + div.style[name] = annotationStyle[name]; + } + } + if (!a.hasOwnProperty('icon')) { + div.className = "dygraphDefaultAnnotation"; + } + if (a.hasOwnProperty('cssClass')) { + div.className += " " + a.cssClass; + } + + var width = a.hasOwnProperty('width') ? a.width : 16; + var height = a.hasOwnProperty('height') ? a.height : 16; + if (a.hasOwnProperty('icon')) { + var img = document.createElement("img"); + img.src = a.icon; + img.width = width; + img.height = height; + div.appendChild(img); + } else if (p.annotation.hasOwnProperty('shortText')) { + div.appendChild(document.createTextNode(p.annotation.shortText)); + } + div.style.left = (p.canvasx - width / 2) + "px"; + if (a.attachAtBottom) { + div.style.top = (this.area.h - height - tick_height) + "px"; + } else { + div.style.top = (p.canvasy - height - tick_height) + "px"; + } + div.style.width = width + "px"; + div.style.height = height + "px"; + div.title = p.annotation.text; + div.style.color = this.colors[p.name]; + div.style.borderColor = this.colors[p.name]; + a.div = div; + + Dygraph.addEvent(div, 'click', + bindEvt('clickHandler', 'annotationClickHandler', p, this)); + Dygraph.addEvent(div, 'mouseover', + bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p, this)); + Dygraph.addEvent(div, 'mouseout', + bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p, this)); + Dygraph.addEvent(div, 'dblclick', + bindEvt('dblClickHandler', 'annotationDblClickHandler', p, this)); + + this.container.appendChild(div); + this.annotations.push(div); + + var ctx = this.element.getContext("2d"); + ctx.strokeStyle = this.colors[p.name]; + ctx.beginPath(); + if (!a.attachAtBottom) { + ctx.moveTo(p.canvasx, p.canvasy); + ctx.lineTo(p.canvasx, p.canvasy - 2 - tick_height); + } else { + ctx.moveTo(p.canvasx, this.area.h); + ctx.lineTo(p.canvasx, this.area.h - 2 - tick_height); + } + ctx.closePath(); + ctx.stroke(); + } +}; + + /** * Overrides the CanvasRenderer method to draw error bars */ @@ -450,6 +610,8 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { var fillAlpha = this.options.fillAlpha; var errorBars = this.layout.options.errorBars; var fillGraph = this.layout.options.fillGraph; + var stackedGraph = this.layout.options.stackedGraph; + var stepPlot = this.layout.options.stepPlot; var setNames = []; for (var name in this.layout.datasets) { @@ -459,6 +621,11 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { } var setCount = setNames.length; + this.colors = {} + for (var i = 0; i < setCount; i++) { + this.colors[setNames[i]] = colorScheme[i % colorCount]; + } + // Update Points // TODO(danvk): here for (var i = 0; i < this.layout.points.length; i++) { @@ -478,16 +645,16 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { for (var i = 0; i < setCount; i++) { var setName = setNames[i]; - var color = colorScheme[i % colorCount]; + var axis = this.layout.options.yAxes[ + this.layout.options.seriesToAxisMap[setName]]; + var color = this.colors[setName]; // setup graphics context ctx.save(); - ctx.strokeStyle = color; - ctx.lineWidth = this.options.strokeWidth; - var prevX = -1; + var prevX = NaN; + var prevY = NaN; var prevYs = [-1, -1]; - var count = 0; - var yscale = this.layout.yscale; + var yscale = axis.yscale; // should be same color as the lines but only 15% opaque. var rgb = new RGBColor(color); var err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + @@ -496,47 +663,63 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { ctx.beginPath(); for (var j = 0; j < this.layout.points.length; j++) { var point = this.layout.points[j]; - count++; if (point.name == setName) { if (!isOK(point.y)) { - prevX = -1; + prevX = NaN; continue; } + // TODO(danvk): here - var newYs = [ point.y - point.errorPlus * yscale, - point.y + point.errorMinus * yscale ]; + if (stepPlot) { + var newYs = [ prevY - point.errorPlus * yscale, + prevY + point.errorMinus * yscale ]; + prevY = point.y; + } else { + var newYs = [ point.y - point.errorPlus * yscale, + point.y + point.errorMinus * yscale ]; + } newYs[0] = this.area.h * newYs[0] + this.area.y; newYs[1] = this.area.h * newYs[1] + this.area.y; - if (prevX >= 0) { - ctx.moveTo(prevX, prevYs[0]); + if (!isNaN(prevX)) { + if (stepPlot) { + ctx.moveTo(prevX, newYs[0]); + } else { + ctx.moveTo(prevX, prevYs[0]); + } ctx.lineTo(point.canvasx, newYs[0]); ctx.lineTo(point.canvasx, newYs[1]); - ctx.lineTo(prevX, prevYs[1]); + if (stepPlot) { + ctx.lineTo(prevX, newYs[1]); + } else { + ctx.lineTo(prevX, prevYs[1]); + } ctx.closePath(); } - prevYs[0] = newYs[0]; - prevYs[1] = newYs[1]; + prevYs = newYs; prevX = point.canvasx; } } ctx.fill(); } } else if (fillGraph) { - // TODO(danvk): merge this code with the logic above; they're very similar. - for (var i = 0; i < setCount; i++) { + var baseline = [] // for stacked graphs: baseline for filling + + // process sets in reverse order (needed for stacked graphs) + for (var i = setCount - 1; i >= 0; i--) { var setName = setNames[i]; - var setNameLast; - if (i>0) setNameLast = setNames[i-1]; - var color = colorScheme[i % colorCount]; + var color = this.colors[setName]; + var axis = this.layout.options.yAxes[ + this.layout.options.seriesToAxisMap[setName]]; + var axisY = 1.0 + axis.minyval * axis.yscale; + if (axisY < 0.0) axisY = 0.0; + else if (axisY > 1.0) axisY = 1.0; + axisY = this.area.h * axisY + this.area.y; // setup graphics context ctx.save(); - ctx.strokeStyle = color; - ctx.lineWidth = this.options.strokeWidth; - var prevX = -1; + var prevX = NaN; var prevYs = [-1, -1]; - var count = 0; - var yscale = this.layout.yscale; + var yscale = axis.yscale; // should be same color as the lines but only 15% opaque. var rgb = new RGBColor(color); var err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + @@ -545,27 +728,32 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { ctx.beginPath(); for (var j = 0; j < this.layout.points.length; j++) { var point = this.layout.points[j]; - count++; if (point.name == setName) { if (!isOK(point.y)) { - prevX = -1; + prevX = NaN; continue; } - var pX = 1.0 + this.layout.minyval * this.layout.yscale; - if (pX < 0.0) pX = 0.0; - else if (pX > 1.0) pX = 1.0; - var newYs = [ point.y, pX ]; - newYs[0] = this.area.h * newYs[0] + this.area.y; - newYs[1] = this.area.h * newYs[1] + this.area.y; - if (prevX >= 0) { + var newYs; + if (stackedGraph) { + lastY = baseline[point.canvasx]; + if (lastY === undefined) lastY = axisY; + baseline[point.canvasx] = point.canvasy; + newYs = [ point.canvasy, lastY ]; + } else { + newYs = [ point.canvasy, axisY ]; + } + if (!isNaN(prevX)) { ctx.moveTo(prevX, prevYs[0]); - ctx.lineTo(point.canvasx, newYs[0]); + if (stepPlot) { + ctx.lineTo(point.canvasx, prevYs[0]); + } else { + ctx.lineTo(point.canvasx, newYs[0]); + } ctx.lineTo(point.canvasx, newYs[1]); ctx.lineTo(prevX, prevYs[1]); ctx.closePath(); } - prevYs[0] = newYs[0]; - prevYs[1] = newYs[1]; + prevYs = newYs; prevX = point.canvasx; } } @@ -575,14 +763,15 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { for (var i = 0; i < setCount; i++) { var setName = setNames[i]; - var color = colorScheme[i%colorCount]; + var color = this.colors[setName]; + var strokeWidth = this.dygraph_.attr_("strokeWidth", setName); // setup graphics context context.save(); var point = this.layout.points[0]; - var pointSize = this.dygraph_.attr_("pointSize"); + var pointSize = this.dygraph_.attr_("pointSize", setName); var prevX = null, prevY = null; - var drawPoints = this.dygraph_.attr_("drawPoints"); + var drawPoints = this.dygraph_.attr_("drawPoints", setName); var points = this.layout.points; for (var j = 0; j < points.length; j++) { var point = points[j]; @@ -600,14 +789,20 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { prevX = point.canvasx; prevY = point.canvasy; } else { - ctx.beginPath(); - ctx.strokeStyle = color; - ctx.lineWidth = this.options.strokeWidth; - ctx.moveTo(prevX, prevY); - prevX = point.canvasx; - prevY = point.canvasy; - ctx.lineTo(prevX, prevY); - ctx.stroke(); + // TODO(danvk): figure out why this conditional is necessary. + if (strokeWidth) { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = strokeWidth; + ctx.moveTo(prevX, prevY); + if (stepPlot) { + ctx.lineTo(point.canvasx, prevY); + } + prevX = point.canvasx; + prevY = point.canvasy; + ctx.lineTo(prevX, prevY); + ctx.stroke(); + } } if (drawPoints || isIsolated) {