hack, hack
[dygraphs.git] / dygraph-canvas.js
CommitLineData
6a1aa64f
DV
1// Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2// All Rights Reserved.
3
4/**
5 * @fileoverview Subclasses various parts of PlotKit to meet the additional
285a6bda 6 * needs of Dygraph: grid overlays and error bars
6a1aa64f
DV
7 */
8
9// Subclass PlotKit.Layout to add:
10// 1. Sigma/errorBars properties
11// 2. Copy error terms for PlotKit.CanvasRenderer._renderLineChart
12
13/**
285a6bda 14 * Creates a new DygraphLayout object. Options are the same as those allowed
6a1aa64f
DV
15 * by the PlotKit.Layout constructor.
16 * @param {Object} options Options for PlotKit.Layout
285a6bda 17 * @return {Object} The DygraphLayout object
6a1aa64f 18 */
285a6bda 19DygraphLayout = function(options) {
6a1aa64f
DV
20 PlotKit.Layout.call(this, "line", options);
21};
285a6bda 22DygraphLayout.prototype = new PlotKit.Layout();
6a1aa64f
DV
23
24/**
25 * Behaves the same way as PlotKit.Layout, but also copies the errors
26 * @private
27 */
285a6bda 28DygraphLayout.prototype.evaluateWithError = function() {
6a1aa64f
DV
29 this.evaluate();
30 if (!this.options.errorBars) return;
31
32 // Copy over the error terms
33 var i = 0; // index in this.points
34 for (var setName in this.datasets) {
35 var j = 0;
36 var dataset = this.datasets[setName];
37 if (PlotKit.Base.isFuncLike(dataset)) continue;
38 for (var j = 0; j < dataset.length; j++, i++) {
39 var item = dataset[j];
40 var xv = parseFloat(item[0]);
41 var yv = parseFloat(item[1]);
42
43 if (xv == this.points[i].xval &&
44 yv == this.points[i].yval) {
45 this.points[i].errorMinus = parseFloat(item[2]);
46 this.points[i].errorPlus = parseFloat(item[3]);
47 }
48 }
49 }
50};
51
52/**
53 * Convenience function to remove all the data sets from a graph
54 */
285a6bda 55DygraphLayout.prototype.removeAllDatasets = function() {
6a1aa64f
DV
56 delete this.datasets;
57 this.datasets = new Array();
58};
59
60/**
61 * Change the values of various layout options
62 * @param {Object} new_options an associative array of new properties
63 */
285a6bda 64DygraphLayout.prototype.updateOptions = function(new_options) {
6a1aa64f
DV
65 MochiKit.Base.update(this.options, new_options ? new_options : {});
66};
67
68// Subclass PlotKit.CanvasRenderer to add:
69// 1. X/Y grid overlay
70// 2. Ability to draw error bars (if required)
71
72/**
73 * Sets some PlotKit.CanvasRenderer options
74 * @param {Object} element The canvas to attach to
285a6bda 75 * @param {Layout} layout The DygraphLayout object for this graph.
6a1aa64f
DV
76 * @param {Object} options Options to pass on to CanvasRenderer
77 */
285a6bda 78DygraphCanvasRenderer = function(element, layout, options) {
6a1aa64f
DV
79 PlotKit.CanvasRenderer.call(this, element, layout, options);
80 this.options.shouldFill = false;
81 this.options.shouldStroke = true;
82 this.options.drawYGrid = true;
83 this.options.drawXGrid = true;
84 this.options.gridLineColor = MochiKit.Color.Color.grayColor();
85 MochiKit.Base.update(this.options, options);
86
87 // TODO(danvk) This shouldn't be necessary: effects should be overlaid
88 this.options.drawBackground = false;
89};
285a6bda 90DygraphCanvasRenderer.prototype = new PlotKit.CanvasRenderer();
6a1aa64f
DV
91
92/**
93 * Draw an X/Y grid on top of the existing plot
94 */
285a6bda 95DygraphCanvasRenderer.prototype.render = function() {
6a1aa64f
DV
96 // Draw the new X/Y grid
97 var ctx = this.element.getContext("2d");
98 if (this.options.drawYGrid) {
99 var ticks = this.layout.yticks;
100 ctx.save();
101 ctx.strokeStyle = this.options.gridLineColor.toRGBString();
102 ctx.lineWidth = this.options.axisLineWidth;
103 for (var i = 0; i < ticks.length; i++) {
104 var x = this.area.x;
105 var y = this.area.y + ticks[i][0] * this.area.h;
106 ctx.beginPath();
107 ctx.moveTo(x, y);
108 ctx.lineTo(x + this.area.w, y);
109 ctx.closePath();
110 ctx.stroke();
111 }
112 }
113
114 if (this.options.drawXGrid) {
115 var ticks = this.layout.xticks;
116 ctx.save();
117 ctx.strokeStyle = this.options.gridLineColor.toRGBString();
118 ctx.lineWidth = this.options.axisLineWidth;
119 for (var i=0; i<ticks.length; i++) {
120 var x = this.area.x + ticks[i][0] * this.area.w;
121 var y = this.area.y + this.area.h;
122 ctx.beginPath();
123 ctx.moveTo(x, y);
124 ctx.lineTo(x, this.area.y);
125 ctx.closePath();
126 ctx.stroke();
127 }
128 }
2ce09b19
DV
129
130 // Do the ordinary rendering, as before
131 // TODO(danvk) Call super.render()
132 this._renderLineChart();
133 this._renderLineAxis();
6a1aa64f
DV
134};
135
136/**
137 * Overrides the CanvasRenderer method to draw error bars
138 */
285a6bda 139DygraphCanvasRenderer.prototype._renderLineChart = function() {
6a1aa64f
DV
140 var context = this.element.getContext("2d");
141 var colorCount = this.options.colorScheme.length;
142 var colorScheme = this.options.colorScheme;
143 var setNames = MochiKit.Base.keys(this.layout.datasets);
144 var errorBars = this.layout.options.errorBars;
145 var setCount = setNames.length;
146 var bind = MochiKit.Base.bind;
147 var partial = MochiKit.Base.partial;
148
149 //Update Points
150 var updatePoint = function(point) {
151 point.canvasx = this.area.w * point.x + this.area.x;
152 point.canvasy = this.area.h * point.y + this.area.y;
153 }
154 MochiKit.Iter.forEach(this.layout.points, updatePoint, this);
155
156 // create paths
157 var makePath = function(ctx) {
158 for (var i = 0; i < setCount; i++) {
159 var setName = setNames[i];
160 var color = colorScheme[i%colorCount];
161 var strokeX = this.options.strokeColorTransform;
162
163 // setup graphics context
164 context.save();
165 context.strokeStyle = color.toRGBString();
166 context.lineWidth = this.options.strokeWidth;
167 ctx.beginPath();
168 var point = this.layout.points[0];
169 var first_point = true;
170 var addPoint = function(ctx_, point) {
171 if (point.name == setName) {
584ceeaa 172 if (!point.canvasy || isNaN(point.canvasy)) {
9a730910
DV
173 // this will make us move to the next point, not draw a line to it.
174 first_point = true;
175 } else {
176 if (first_point) {
177 ctx_.moveTo(point.canvasx, point.canvasy);
178 first_point = false;
179 } else {
180 ctx_.lineTo(point.canvasx, point.canvasy);
181 }
182 }
6a1aa64f
DV
183 }
184 };
185 MochiKit.Iter.forEach(this.layout.points, partial(addPoint, ctx), this);
186 ctx.stroke();
187 }
188 };
189
190 var makeErrorBars = function(ctx) {
191 for (var i = 0; i < setCount; i++) {
192 var setName = setNames[i];
193 var color = colorScheme[i % colorCount];
194 var strokeX = this.options.strokeColorTransform;
195
196 // setup graphics context
197 context.save();
198 context.strokeStyle = color.toRGBString();
199 context.lineWidth = this.options.strokeWidth;
200 var prevX = -1;
201 var prevYs = [-1, -1];
202 var count = 0;
203 var yscale = this.layout.yscale;
204 var errorTrapezoid = function(ctx_,point) {
205 count++;
206 if (point.name == setName) {
5011e7a1
DV
207 if (!point.y || isNaN(point.y)) {
208 prevX = -1;
209 return;
210 }
6a1aa64f
DV
211 var newYs = [ point.y - point.errorPlus * yscale,
212 point.y + point.errorMinus * yscale ];
213 newYs[0] = this.area.h * newYs[0] + this.area.y;
214 newYs[1] = this.area.h * newYs[1] + this.area.y;
215 if (prevX >= 0) {
216 ctx_.moveTo(prevX, prevYs[0]);
217 ctx_.lineTo(point.canvasx, newYs[0]);
218 ctx_.lineTo(point.canvasx, newYs[1]);
219 ctx_.lineTo(prevX, prevYs[1]);
220 ctx_.closePath();
221 }
222 prevYs[0] = newYs[0];
223 prevYs[1] = newYs[1];
224 prevX = point.canvasx;
225 }
226 };
227 // should be same color as the lines
228 var err_color = color.colorWithAlpha(0.15);
229 ctx.fillStyle = err_color.toRGBString();
230 ctx.beginPath();
231 MochiKit.Iter.forEach(this.layout.points, partial(errorTrapezoid, ctx), this);
232 ctx.fill();
233 }
234 };
235
236 if (errorBars)
237 bind(makeErrorBars, this)(context);
238 bind(makePath, this)(context);
239 context.restore();
240};