shrink Layout more and more
[dygraphs.git] / dygraph-canvas.js
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
6 * needs of Dygraph: grid overlays and error bars
7 */
8
9 // Subclass PlotKit.Layout to add:
10 // 1. Sigma/errorBars properties
11 // 2. Copy error terms for PlotKit.CanvasRenderer._renderLineChart
12
13 /**
14 * Creates a new DygraphLayout object. Options are the same as those allowed
15 * by the PlotKit.Layout constructor.
16 * @param {Object} options Options for PlotKit.Layout
17 * @return {Object} The DygraphLayout object
18 */
19 DygraphLayout = function(options) {
20 PlotKit.Layout.call(this, "line", options);
21 };
22 DygraphLayout.prototype = new PlotKit.Layout();
23
24 /**
25 * Behaves the same way as PlotKit.Layout, but also copies the errors
26 * @private
27 */
28 DygraphLayout.prototype.evaluateWithError = function() {
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 */
55 DygraphLayout.prototype.removeAllDatasets = function() {
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 */
64 DygraphLayout.prototype.updateOptions = function(new_options) {
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
75 * @param {Layout} layout The DygraphLayout object for this graph.
76 * @param {Object} options Options to pass on to CanvasRenderer
77 */
78 DygraphCanvasRenderer = function(dygraph, element, layout, options) {
79 // TODO(danvk): remove options, just use dygraph.attr_.
80 PlotKit.CanvasRenderer.call(this, element, layout, options);
81 this.dygraph_ = dygraph;
82 this.options.shouldFill = false;
83 this.options.shouldStroke = true;
84 this.options.drawYGrid = true;
85 this.options.drawXGrid = true;
86 this.options.gridLineColor = MochiKit.Color.Color.grayColor();
87 MochiKit.Base.update(this.options, options);
88
89 // TODO(danvk) This shouldn't be necessary: effects should be overlaid
90 this.options.drawBackground = false;
91 };
92 DygraphCanvasRenderer.prototype = new PlotKit.CanvasRenderer();
93
94 /**
95 * Draw an X/Y grid on top of the existing plot
96 */
97 DygraphCanvasRenderer.prototype.render = function() {
98 // Draw the new X/Y grid
99 var ctx = this.element.getContext("2d");
100 if (this.options.drawYGrid) {
101 var ticks = this.layout.yticks;
102 ctx.save();
103 ctx.strokeStyle = this.options.gridLineColor.toRGBString();
104 ctx.lineWidth = this.options.axisLineWidth;
105 for (var i = 0; i < ticks.length; i++) {
106 var x = this.area.x;
107 var y = this.area.y + ticks[i][0] * this.area.h;
108 ctx.beginPath();
109 ctx.moveTo(x, y);
110 ctx.lineTo(x + this.area.w, y);
111 ctx.closePath();
112 ctx.stroke();
113 }
114 }
115
116 if (this.options.drawXGrid) {
117 var ticks = this.layout.xticks;
118 ctx.save();
119 ctx.strokeStyle = this.options.gridLineColor.toRGBString();
120 ctx.lineWidth = this.options.axisLineWidth;
121 for (var i=0; i<ticks.length; i++) {
122 var x = this.area.x + ticks[i][0] * this.area.w;
123 var y = this.area.y + this.area.h;
124 ctx.beginPath();
125 ctx.moveTo(x, y);
126 ctx.lineTo(x, this.area.y);
127 ctx.closePath();
128 ctx.stroke();
129 }
130 }
131
132 // Do the ordinary rendering, as before
133 // TODO(danvk) Call super.render()
134 this._renderLineChart();
135 this._renderLineAxis();
136 };
137
138 /**
139 * Overrides the CanvasRenderer method to draw error bars
140 */
141 DygraphCanvasRenderer.prototype._renderLineChart = function() {
142 var context = this.element.getContext("2d");
143 var colorCount = this.options.colorScheme.length;
144 var colorScheme = this.options.colorScheme;
145 var setNames = MochiKit.Base.keys(this.layout.datasets);
146 var errorBars = this.layout.options.errorBars;
147 var setCount = setNames.length;
148 var bind = MochiKit.Base.bind;
149 var partial = MochiKit.Base.partial;
150
151 //Update Points
152 var updatePoint = function(point) {
153 point.canvasx = this.area.w * point.x + this.area.x;
154 point.canvasy = this.area.h * point.y + this.area.y;
155 }
156 MochiKit.Iter.forEach(this.layout.points, updatePoint, this);
157
158 // create paths
159 var isOK = function(x) { return x && !isNaN(x); };
160 var makePath = function(ctx) {
161 for (var i = 0; i < setCount; i++) {
162 var setName = setNames[i];
163 var color = colorScheme[i%colorCount];
164 var strokeX = this.options.strokeColorTransform;
165
166 // setup graphics context
167 context.save();
168 context.strokeStyle = color.toRGBString();
169 context.lineWidth = this.options.strokeWidth;
170 var point = this.layout.points[0];
171 var pointSize = this.dygraph_.attr_("pointSize");
172 var prevX = null, prevY = null;
173 var drawPoints = this.dygraph_.attr_("drawPoints");
174 var points = this.layout.points;
175 for (var j = 0; j < points.length; j++) {
176 var point = points[j];
177 if (point.name == setName) {
178 if (!isOK(point.canvasy)) {
179 // this will make us move to the next point, not draw a line to it.
180 prevX = prevY = null;
181 } else {
182 // A point is "isolated" if it is non-null but both the previous
183 // and next points are null.
184 var isIsolated = (!prevX && (j == points.length - 1 ||
185 !isOK(points[j+1].canvasy)));
186
187 if (!prevX) {
188 prevX = point.canvasx;
189 prevY = point.canvasy;
190 } else {
191 ctx.beginPath();
192 ctx.moveTo(prevX, prevY);
193 prevX = point.canvasx;
194 prevY = point.canvasy;
195 ctx.lineTo(prevX, prevY);
196 ctx.stroke();
197 }
198
199 if (drawPoints || isIsolated) {
200 ctx.beginPath();
201 ctx.fillStyle = color.toRGBString();
202 ctx.arc(point.canvasx, point.canvasy, pointSize, 0, 360, false);
203 ctx.fill();
204 }
205 }
206 }
207 }
208 }
209 };
210
211 var makeErrorBars = function(ctx) {
212 for (var i = 0; i < setCount; i++) {
213 var setName = setNames[i];
214 var color = colorScheme[i % colorCount];
215 var strokeX = this.options.strokeColorTransform;
216
217 // setup graphics context
218 context.save();
219 context.strokeStyle = color.toRGBString();
220 context.lineWidth = this.options.strokeWidth;
221 var prevX = -1;
222 var prevYs = [-1, -1];
223 var count = 0;
224 var yscale = this.layout.yscale;
225 var errorTrapezoid = function(ctx_,point) {
226 count++;
227 if (point.name == setName) {
228 if (!point.y || isNaN(point.y)) {
229 prevX = -1;
230 return;
231 }
232 var newYs = [ point.y - point.errorPlus * yscale,
233 point.y + point.errorMinus * yscale ];
234 newYs[0] = this.area.h * newYs[0] + this.area.y;
235 newYs[1] = this.area.h * newYs[1] + this.area.y;
236 if (prevX >= 0) {
237 ctx_.moveTo(prevX, prevYs[0]);
238 ctx_.lineTo(point.canvasx, newYs[0]);
239 ctx_.lineTo(point.canvasx, newYs[1]);
240 ctx_.lineTo(prevX, prevYs[1]);
241 ctx_.closePath();
242 }
243 prevYs[0] = newYs[0];
244 prevYs[1] = newYs[1];
245 prevX = point.canvasx;
246 }
247 };
248 // should be same color as the lines
249 var err_color = color.colorWithAlpha(0.15);
250 ctx.fillStyle = err_color.toRGBString();
251 ctx.beginPath();
252 MochiKit.Iter.forEach(this.layout.points, partial(errorTrapezoid, ctx), this);
253 ctx.fill();
254 }
255 };
256
257 if (errorBars)
258 bind(makeErrorBars, this)(context);
259 bind(makePath, this)(context);
260 context.restore();
261 };