re-add layout plugin event; title display works
[dygraphs.git] / dygraph-canvas.js
CommitLineData
88e95c46
DV
1/**
2 * @license
3 * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
5 */
6a1aa64f
DV
6
7/**
74a5af31
DV
8 * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the
9 * needs of dygraphs.
10 *
3df0ccf0 11 * In particular, support for:
0abfbd7e 12 * - grid overlays
3df0ccf0
DV
13 * - error bars
14 * - dygraphs attribute system
6a1aa64f
DV
15 */
16
6a1aa64f 17/**
423f5ed3
DV
18 * The DygraphCanvasRenderer class does the actual rendering of the chart onto
19 * a canvas. It's based on PlotKit.CanvasRenderer.
6a1aa64f 20 * @param {Object} element The canvas to attach to
2cf95fff
RK
21 * @param {Object} elementContext The 2d context of the canvas (injected so it
22 * can be mocked for testing.)
285a6bda 23 * @param {Layout} layout The DygraphLayout object for this graph.
74a5af31 24 * @constructor
6a1aa64f 25 */
c0f54d4f 26
758a629f
DV
27/*jshint globalstrict: true */
28/*global Dygraph:false,RGBColor:false */
c0f54d4f
DV
29"use strict";
30
79253bd0 31
8cfe592f
DV
32/**
33 * @constructor
34 *
35 * This gets called when there are "new points" to chart. This is generally the
36 * case when the underlying data being charted has changed. It is _not_ called
37 * in the common case that the user has zoomed or is panning the view.
38 *
39 * The chart canvas has already been created by the Dygraph object. The
40 * renderer simply gets a drawing context.
41 *
42 * @param {Dyraph} dygraph The chart to which this renderer belongs.
43 * @param {Canvas} element The <canvas> DOM element on which to draw.
44 * @param {CanvasRenderingContext2D} elementContext The drawing context.
45 * @param {DygraphLayout} layout The chart's DygraphLayout object.
46 *
47 * TODO(danvk): remove the elementContext property.
48 */
c0f54d4f 49var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
9317362d 50 this.dygraph_ = dygraph;
fbe31dc8 51
fbe31dc8 52 this.layout = layout;
b0c3b730 53 this.element = element;
2cf95fff 54 this.elementContext = elementContext;
fbe31dc8
DV
55 this.container = this.element.parentNode;
56
fbe31dc8
DV
57 this.height = this.element.height;
58 this.width = this.element.width;
59
60 // --- check whether everything is ok before we return
61 if (!this.isIE && !(DygraphCanvasRenderer.isSupported(this.element)))
62 throw "Canvas is not supported.";
63
64 // internal state
758a629f
DV
65 this.xlabels = [];
66 this.ylabels = [];
67 this.annotations = [];
ad1798c2 68 this.chartLabels = {};
fbe31dc8 69
70be5ed1 70 this.area = layout.getPlotArea();
423f5ed3
DV
71 this.container.style.position = "relative";
72 this.container.style.width = this.width + "px";
73
74 // Set up a clipping area for the canvas (and the interaction canvas).
75 // This ensures that we don't overdraw.
920208fb
PF
76 if (this.dygraph_.isUsingExcanvas_) {
77 this._createIEClipArea();
78 } else {
971870e5
DV
79 // on Android 3 and 4, setting a clipping area on a canvas prevents it from
80 // displaying anything.
81 if (!Dygraph.isAndroid()) {
82 var ctx = this.dygraph_.canvas_ctx_;
83 ctx.beginPath();
84 ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h);
85 ctx.clip();
920208fb 86
971870e5
DV
87 ctx = this.dygraph_.hidden_ctx_;
88 ctx.beginPath();
89 ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h);
90 ctx.clip();
91 }
920208fb 92 }
423f5ed3
DV
93};
94
95DygraphCanvasRenderer.prototype.attr_ = function(x) {
96 return this.dygraph_.attr_(x);
97};
98
8cfe592f
DV
99/**
100 * Clears out all chart content and DOM elements.
101 * This is called immediately before render() on every frame, including
102 * during zooms and pans.
103 * @private
104 */
fbe31dc8 105DygraphCanvasRenderer.prototype.clear = function() {
758a629f 106 var context;
fbe31dc8
DV
107 if (this.isIE) {
108 // VML takes a while to start up, so we just poll every this.IEDelay
109 try {
110 if (this.clearDelay) {
111 this.clearDelay.cancel();
112 this.clearDelay = null;
113 }
758a629f 114 context = this.elementContext;
fbe31dc8
DV
115 }
116 catch (e) {
76171648 117 // TODO(danvk): this is broken, since MochiKit.Async is gone.
758a629f
DV
118 // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
119 // this.clearDelay.addCallback(bind(this.clear, this));
fbe31dc8
DV
120 return;
121 }
122 }
123
758a629f 124 context = this.elementContext;
fbe31dc8
DV
125 context.clearRect(0, 0, this.width, this.height);
126
758a629f
DV
127 function removeArray(ary) {
128 for (var i = 0; i < ary.length; i++) {
129 var el = ary[i];
130 if (el.parentNode) el.parentNode.removeChild(el);
131 }
ce49c2fa 132 }
758a629f
DV
133
134 removeArray(this.xlabels);
135 removeArray(this.ylabels);
136 removeArray(this.annotations);
137
ad1798c2
DV
138 for (var k in this.chartLabels) {
139 if (!this.chartLabels.hasOwnProperty(k)) continue;
140 var el = this.chartLabels[k];
141 if (el.parentNode) el.parentNode.removeChild(el);
142 }
758a629f
DV
143 this.xlabels = [];
144 this.ylabels = [];
145 this.annotations = [];
ad1798c2 146 this.chartLabels = {};
fbe31dc8
DV
147};
148
8cfe592f
DV
149/**
150 * Checks whether the browser supports the &lt;canvas&gt; tag.
151 * @private
152 */
fbe31dc8
DV
153DygraphCanvasRenderer.isSupported = function(canvasName) {
154 var canvas = null;
155 try {
758a629f 156 if (typeof(canvasName) == 'undefined' || canvasName === null) {
b0c3b730 157 canvas = document.createElement("canvas");
758a629f 158 } else {
b0c3b730 159 canvas = canvasName;
758a629f
DV
160 }
161 canvas.getContext("2d");
fbe31dc8
DV
162 }
163 catch (e) {
164 var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
165 var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
166 if ((!ie) || (ie[1] < 6) || (opera))
167 return false;
168 return true;
169 }
170 return true;
6a1aa64f 171};
6a1aa64f
DV
172
173/**
600d841a
DV
174 * @param { [String] } colors Array of color strings. Should have one entry for
175 * each series to be rendered.
176 */
177DygraphCanvasRenderer.prototype.setColors = function(colors) {
178 this.colorScheme_ = colors;
179};
180
181/**
8cfe592f
DV
182 * This method is responsible for drawing everything on the chart, including
183 * lines, error bars, fills and axes.
184 * It is called immediately after clear() on every frame, including during pans
185 * and zooms.
186 * @private
6a1aa64f 187 */
285a6bda 188DygraphCanvasRenderer.prototype.render = function() {
528ce7e5
DV
189 // Draw the new X/Y grid. Lines appear crisper when pixels are rounded to
190 // half-integers. This prevents them from drawing in two rows/cols.
2cf95fff 191 var ctx = this.elementContext;
758a629f
DV
192 function halfUp(x) { return Math.round(x) + 0.5; }
193 function halfDown(y){ return Math.round(y) - 0.5; }
e7746234 194
423f5ed3 195 if (this.attr_('underlayCallback')) {
1e41bd2d
DV
196 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
197 // users who expect a deprecated form of this callback.
423f5ed3 198 this.attr_('underlayCallback')(ctx, this.area, this.dygraph_, this.dygraph_);
e7746234
EC
199 }
200
758a629f 201 var x, y, i, ticks;
423f5ed3 202 if (this.attr_('drawYGrid')) {
758a629f 203 ticks = this.layout.yticks;
bbba718a 204 // TODO(konigsberg): I don't think these calls to save() have a corresponding restore().
6a1aa64f 205 ctx.save();
423f5ed3 206 ctx.strokeStyle = this.attr_('gridLineColor');
990d6a35 207 ctx.lineWidth = this.attr_('gridLineWidth');
758a629f 208 for (i = 0; i < ticks.length; i++) {
880a574f 209 // TODO(danvk): allow secondary axes to draw a grid, too.
758a629f
DV
210 if (ticks[i][0] !== 0) continue;
211 x = halfUp(this.area.x);
212 y = halfDown(this.area.y + ticks[i][1] * this.area.h);
6a1aa64f
DV
213 ctx.beginPath();
214 ctx.moveTo(x, y);
215 ctx.lineTo(x + this.area.w, y);
216 ctx.closePath();
217 ctx.stroke();
218 }
6bf4df7f 219 ctx.restore();
6a1aa64f
DV
220 }
221
423f5ed3 222 if (this.attr_('drawXGrid')) {
758a629f 223 ticks = this.layout.xticks;
6a1aa64f 224 ctx.save();
423f5ed3 225 ctx.strokeStyle = this.attr_('gridLineColor');
990d6a35 226 ctx.lineWidth = this.attr_('gridLineWidth');
758a629f
DV
227 for (i=0; i<ticks.length; i++) {
228 x = halfUp(this.area.x + ticks[i][0] * this.area.w);
229 y = halfDown(this.area.y + this.area.h);
6a1aa64f 230 ctx.beginPath();
880a574f 231 ctx.moveTo(x, y);
6a1aa64f
DV
232 ctx.lineTo(x, this.area.y);
233 ctx.closePath();
234 ctx.stroke();
235 }
6bf4df7f 236 ctx.restore();
6a1aa64f 237 }
2ce09b19
DV
238
239 // Do the ordinary rendering, as before
2ce09b19 240 this._renderLineChart();
fbe31dc8 241 this._renderAxis();
6dca682f 242 // this._renderChartLabels();
ce49c2fa 243 this._renderAnnotations();
fbe31dc8
DV
244};
245
920208fb
PF
246DygraphCanvasRenderer.prototype._createIEClipArea = function() {
247 var className = 'dygraph-clip-div';
248 var graphDiv = this.dygraph_.graphDiv;
249
250 // Remove old clip divs.
251 for (var i = graphDiv.childNodes.length-1; i >= 0; i--) {
252 if (graphDiv.childNodes[i].className == className) {
253 graphDiv.removeChild(graphDiv.childNodes[i]);
254 }
255 }
256
257 // Determine background color to give clip divs.
258 var backgroundColor = document.bgColor;
259 var element = this.dygraph_.graphDiv;
260 while (element != document) {
261 var bgcolor = element.currentStyle.backgroundColor;
262 if (bgcolor && bgcolor != 'transparent') {
263 backgroundColor = bgcolor;
264 break;
265 }
266 element = element.parentNode;
267 }
268
269 function createClipDiv(area) {
758a629f 270 if (area.w === 0 || area.h === 0) {
920208fb
PF
271 return;
272 }
273 var elem = document.createElement('div');
274 elem.className = className;
275 elem.style.backgroundColor = backgroundColor;
276 elem.style.position = 'absolute';
277 elem.style.left = area.x + 'px';
278 elem.style.top = area.y + 'px';
279 elem.style.width = area.w + 'px';
280 elem.style.height = area.h + 'px';
281 graphDiv.appendChild(elem);
282 }
283
284 var plotArea = this.area;
285 // Left side
758a629f
DV
286 createClipDiv({
287 x:0, y:0,
288 w:plotArea.x,
289 h:this.height
290 });
291
920208fb 292 // Top
758a629f
DV
293 createClipDiv({
294 x: plotArea.x, y: 0,
295 w: this.width - plotArea.x,
296 h: plotArea.y
297 });
298
920208fb 299 // Right side
758a629f
DV
300 createClipDiv({
301 x: plotArea.x + plotArea.w, y: 0,
302 w: this.width-plotArea.x - plotArea.w,
303 h: this.height
304 });
305
920208fb 306 // Bottom
758a629f
DV
307 createClipDiv({
308 x: plotArea.x,
309 y: plotArea.y + plotArea.h,
310 w: this.width - plotArea.x,
311 h: this.height - plotArea.h - plotArea.y
312 });
313};
fbe31dc8
DV
314
315DygraphCanvasRenderer.prototype._renderAxis = function() {
423f5ed3 316 if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) return;
fbe31dc8 317
528ce7e5 318 // Round pixels to half-integer boundaries for crisper drawing.
758a629f
DV
319 function halfUp(x) { return Math.round(x) + 0.5; }
320 function halfDown(y){ return Math.round(y) - 0.5; }
528ce7e5 321
2cf95fff 322 var context = this.elementContext;
fbe31dc8 323
758a629f
DV
324 var label, x, y, tick, i;
325
34fedff8 326 var labelStyle = {
423f5ed3
DV
327 position: "absolute",
328 fontSize: this.attr_('axisLabelFontSize') + "px",
329 zIndex: 10,
330 color: this.attr_('axisLabelColor'),
331 width: this.attr_('axisLabelWidth') + "px",
74a5af31 332 // height: this.attr_('axisLabelFontSize') + 2 + "px",
758a629f 333 lineHeight: "normal", // Something other than "normal" line-height screws up label positioning.
423f5ed3 334 overflow: "hidden"
34fedff8 335 };
48e614ac 336 var makeDiv = function(txt, axis, prec_axis) {
34fedff8
DV
337 var div = document.createElement("div");
338 for (var name in labelStyle) {
85b99f0b
DV
339 if (labelStyle.hasOwnProperty(name)) {
340 div.style[name] = labelStyle[name];
341 }
fbe31dc8 342 }
ba451526 343 var inner_div = document.createElement("div");
48e614ac
DV
344 inner_div.className = 'dygraph-axis-label' +
345 ' dygraph-axis-label-' + axis +
346 (prec_axis ? ' dygraph-axis-label-' + prec_axis : '');
3bdf7140 347 inner_div.innerHTML=txt;
ba451526 348 div.appendChild(inner_div);
34fedff8 349 return div;
fbe31dc8
DV
350 };
351
352 // axis lines
353 context.save();
423f5ed3
DV
354 context.strokeStyle = this.attr_('axisLineColor');
355 context.lineWidth = this.attr_('axisLineWidth');
fbe31dc8 356
423f5ed3 357 if (this.attr_('drawYAxis')) {
8b7a0cc3 358 if (this.layout.yticks && this.layout.yticks.length > 0) {
48e614ac 359 var num_axes = this.dygraph_.numAxes();
758a629f
DV
360 for (i = 0; i < this.layout.yticks.length; i++) {
361 tick = this.layout.yticks[i];
fbe31dc8 362 if (typeof(tick) == "function") return;
758a629f 363 x = this.area.x;
880a574f 364 var sgn = 1;
48e614ac 365 var prec_axis = 'y1';
880a574f
DV
366 if (tick[0] == 1) { // right-side y-axis
367 x = this.area.x + this.area.w;
368 sgn = -1;
48e614ac 369 prec_axis = 'y2';
9012dd21 370 }
758a629f 371 y = this.area.y + tick[1] * this.area.h;
920208fb
PF
372
373 /* Tick marks are currently clipped, so don't bother drawing them.
fbe31dc8 374 context.beginPath();
528ce7e5 375 context.moveTo(halfUp(x), halfDown(y));
0e23cfc6 376 context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
fbe31dc8
DV
377 context.closePath();
378 context.stroke();
920208fb 379 */
fbe31dc8 380
758a629f 381 label = makeDiv(tick[2], 'y', num_axes == 2 ? prec_axis : null);
423f5ed3 382 var top = (y - this.attr_('axisLabelFontSize') / 2);
fbe31dc8
DV
383 if (top < 0) top = 0;
384
423f5ed3 385 if (top + this.attr_('axisLabelFontSize') + 3 > this.height) {
fbe31dc8
DV
386 label.style.bottom = "0px";
387 } else {
388 label.style.top = top + "px";
389 }
758a629f 390 if (tick[0] === 0) {
423f5ed3 391 label.style.left = (this.area.x - this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
9012dd21
DV
392 label.style.textAlign = "right";
393 } else if (tick[0] == 1) {
394 label.style.left = (this.area.x + this.area.w +
423f5ed3 395 this.attr_('axisTickSize')) + "px";
9012dd21
DV
396 label.style.textAlign = "left";
397 }
423f5ed3 398 label.style.width = this.attr_('yAxisLabelWidth') + "px";
b0c3b730 399 this.container.appendChild(label);
fbe31dc8 400 this.ylabels.push(label);
2160ed4a 401 }
fbe31dc8
DV
402
403 // The lowest tick on the y-axis often overlaps with the leftmost
404 // tick on the x-axis. Shift the bottom tick up a little bit to
405 // compensate if necessary.
406 var bottomTick = this.ylabels[0];
423f5ed3 407 var fontSize = this.attr_('axisLabelFontSize');
758a629f 408 var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
fbe31dc8 409 if (bottom > this.height - fontSize) {
758a629f 410 bottomTick.style.top = (parseInt(bottomTick.style.top, 10) -
fbe31dc8
DV
411 fontSize / 2) + "px";
412 }
413 }
414
528ce7e5 415 // draw a vertical line on the left to separate the chart from the labels.
f4b87da2
KW
416 var axisX;
417 if (this.attr_('drawAxesAtZero')) {
418 var r = this.dygraph_.toPercentXCoord(0);
419 if (r > 1 || r < 0) r = 0;
420 axisX = halfUp(this.area.x + r * this.area.w);
421 } else {
422 axisX = halfUp(this.area.x);
423 }
fbe31dc8 424 context.beginPath();
f4b87da2
KW
425 context.moveTo(axisX, halfDown(this.area.y));
426 context.lineTo(axisX, halfDown(this.area.y + this.area.h));
fbe31dc8
DV
427 context.closePath();
428 context.stroke();
c1dbeb10 429
528ce7e5 430 // if there's a secondary y-axis, draw a vertical line for that, too.
c1dbeb10
DV
431 if (this.dygraph_.numAxes() == 2) {
432 context.beginPath();
528ce7e5
DV
433 context.moveTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y));
434 context.lineTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y + this.area.h));
c1dbeb10
DV
435 context.closePath();
436 context.stroke();
437 }
fbe31dc8
DV
438 }
439
423f5ed3 440 if (this.attr_('drawXAxis')) {
fbe31dc8 441 if (this.layout.xticks) {
758a629f
DV
442 for (i = 0; i < this.layout.xticks.length; i++) {
443 tick = this.layout.xticks[i];
444 x = this.area.x + tick[0] * this.area.w;
445 y = this.area.y + this.area.h;
920208fb
PF
446
447 /* Tick marks are currently clipped, so don't bother drawing them.
fbe31dc8 448 context.beginPath();
528ce7e5 449 context.moveTo(halfUp(x), halfDown(y));
423f5ed3 450 context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
fbe31dc8
DV
451 context.closePath();
452 context.stroke();
920208fb 453 */
fbe31dc8 454
758a629f 455 label = makeDiv(tick[1], 'x');
fbe31dc8 456 label.style.textAlign = "center";
423f5ed3 457 label.style.top = (y + this.attr_('axisTickSize')) + 'px';
fbe31dc8 458
423f5ed3
DV
459 var left = (x - this.attr_('axisLabelWidth')/2);
460 if (left + this.attr_('axisLabelWidth') > this.width) {
461 left = this.width - this.attr_('xAxisLabelWidth');
fbe31dc8
DV
462 label.style.textAlign = "right";
463 }
464 if (left < 0) {
465 left = 0;
466 label.style.textAlign = "left";
467 }
468
469 label.style.left = left + "px";
423f5ed3 470 label.style.width = this.attr_('xAxisLabelWidth') + "px";
b0c3b730 471 this.container.appendChild(label);
fbe31dc8 472 this.xlabels.push(label);
2160ed4a 473 }
fbe31dc8
DV
474 }
475
476 context.beginPath();
f4b87da2
KW
477 var axisY;
478 if (this.attr_('drawAxesAtZero')) {
479 var r = this.dygraph_.toPercentYCoord(0, 0);
480 if (r > 1 || r < 0) r = 1;
481 axisY = halfDown(this.area.y + r * this.area.h);
482 } else {
483 axisY = halfDown(this.area.y + this.area.h);
484 }
485 context.moveTo(halfUp(this.area.x), axisY);
486 context.lineTo(halfUp(this.area.x + this.area.w), axisY);
fbe31dc8
DV
487 context.closePath();
488 context.stroke();
489 }
490
491 context.restore();
6a1aa64f
DV
492};
493
fbe31dc8 494
ad1798c2 495DygraphCanvasRenderer.prototype._renderChartLabels = function() {
758a629f
DV
496 var div, class_div;
497
ad1798c2
DV
498 // Generate divs for the chart title, xlabel and ylabel.
499 // Space for these divs has already been taken away from the charting area in
500 // the DygraphCanvasRenderer constructor.
501 if (this.attr_('title')) {
758a629f 502 div = document.createElement("div");
ad1798c2
DV
503 div.style.position = 'absolute';
504 div.style.top = '0px';
505 div.style.left = this.area.x + 'px';
506 div.style.width = this.area.w + 'px';
507 div.style.height = this.attr_('titleHeight') + 'px';
508 div.style.textAlign = 'center';
b4202b3d 509 div.style.fontSize = (this.attr_('titleHeight') - 8) + 'px';
ad1798c2 510 div.style.fontWeight = 'bold';
758a629f 511 class_div = document.createElement("div");
ca49434a
DV
512 class_div.className = 'dygraph-label dygraph-title';
513 class_div.innerHTML = this.attr_('title');
514 div.appendChild(class_div);
ad1798c2
DV
515 this.container.appendChild(div);
516 this.chartLabels.title = div;
517 }
518
519 if (this.attr_('xlabel')) {
758a629f 520 div = document.createElement("div");
ad1798c2
DV
521 div.style.position = 'absolute';
522 div.style.bottom = 0; // TODO(danvk): this is lazy. Calculate style.top.
523 div.style.left = this.area.x + 'px';
524 div.style.width = this.area.w + 'px';
525 div.style.height = this.attr_('xLabelHeight') + 'px';
526 div.style.textAlign = 'center';
86cce9e8 527 div.style.fontSize = (this.attr_('xLabelHeight') - 2) + 'px';
ca49434a 528
758a629f 529 class_div = document.createElement("div");
ca49434a
DV
530 class_div.className = 'dygraph-label dygraph-xlabel';
531 class_div.innerHTML = this.attr_('xlabel');
532 div.appendChild(class_div);
ad1798c2
DV
533 this.container.appendChild(div);
534 this.chartLabels.xlabel = div;
535 }
536
d0c39108
DV
537 var that = this;
538 function createRotatedDiv(axis, classes, html) {
ad1798c2
DV
539 var box = {
540 left: 0,
d0c39108
DV
541 top: that.area.y,
542 width: that.attr_('yLabelWidth'),
543 height: that.area.h
ad1798c2 544 };
ca49434a 545 // TODO(danvk): is this outer div actually necessary?
758a629f 546 div = document.createElement("div");
ad1798c2 547 div.style.position = 'absolute';
d0c39108
DV
548 if (axis == 1) {
549 div.style.left = box.left;
550 } else {
551 div.style.right = box.left;
552 }
ad1798c2
DV
553 div.style.top = box.top + 'px';
554 div.style.width = box.width + 'px';
555 div.style.height = box.height + 'px';
d0c39108 556 div.style.fontSize = (that.attr_('yLabelWidth') - 2) + 'px';
ad1798c2
DV
557
558 var inner_div = document.createElement("div");
559 inner_div.style.position = 'absolute';
ad1798c2
DV
560 inner_div.style.width = box.height + 'px';
561 inner_div.style.height = box.width + 'px';
562 inner_div.style.top = (box.height / 2 - box.width / 2) + 'px';
563 inner_div.style.left = (box.width / 2 - box.height / 2) + 'px';
564 inner_div.style.textAlign = 'center';
b56b6993
DV
565
566 // CSS rotation is an HTML5 feature which is not standardized. Hence every
567 // browser has its own name for the CSS style.
d0c39108
DV
568 var val = 'rotate(' + (axis == 1 ? '-' : '') + '90deg)';
569 inner_div.style.transform = val; // HTML5
570 inner_div.style.WebkitTransform = val; // Safari/Chrome
571 inner_div.style.MozTransform = val; // Firefox
572 inner_div.style.OTransform = val; // Opera
573 inner_div.style.msTransform = val; // IE9
b56b6993
DV
574
575 if (typeof(document.documentMode) !== 'undefined' &&
576 document.documentMode < 9) {
577 // We're dealing w/ an old version of IE, so we have to rotate the text
578 // using a BasicImage transform. This uses a different origin of rotation
579 // than HTML5 rotation (top left of div vs. its center).
580 inner_div.style.filter =
d0c39108
DV
581 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' +
582 (axis == 1 ? '3' : '1') + ')';
b56b6993
DV
583 inner_div.style.left = '0px';
584 inner_div.style.top = '0px';
585 }
ad1798c2 586
758a629f 587 class_div = document.createElement("div");
d0c39108
DV
588 class_div.className = classes;
589 class_div.innerHTML = html;
ca49434a
DV
590
591 inner_div.appendChild(class_div);
ad1798c2 592 div.appendChild(inner_div);
d0c39108
DV
593 return div;
594 }
595
596 var div;
597 if (this.attr_('ylabel')) {
598 div = createRotatedDiv(1, 'dygraph-label dygraph-ylabel',
599 this.attr_('ylabel'));
ad1798c2
DV
600 this.container.appendChild(div);
601 this.chartLabels.ylabel = div;
602 }
107f9d8e 603 if (this.attr_('y2label') && this.dygraph_.numAxes() == 2) {
d0c39108
DV
604 div = createRotatedDiv(2, 'dygraph-label dygraph-y2label',
605 this.attr_('y2label'));
606 this.container.appendChild(div);
607 this.chartLabels.y2label = div;
608 }
ad1798c2
DV
609};
610
611
ce49c2fa
DV
612DygraphCanvasRenderer.prototype._renderAnnotations = function() {
613 var annotationStyle = {
614 "position": "absolute",
423f5ed3 615 "fontSize": this.attr_('axisLabelFontSize') + "px",
ce49c2fa 616 "zIndex": 10,
3bf2fa91 617 "overflow": "hidden"
ce49c2fa
DV
618 };
619
ab5e5c75
DV
620 var bindEvt = function(eventName, classEventName, p, self) {
621 return function(e) {
622 var a = p.annotation;
623 if (a.hasOwnProperty(eventName)) {
624 a[eventName](a, p, self.dygraph_, e);
625 } else if (self.dygraph_.attr_(classEventName)) {
626 self.dygraph_.attr_(classEventName)(a, p, self.dygraph_,e );
627 }
628 };
758a629f 629 };
ab5e5c75 630
ce49c2fa
DV
631 // Get a list of point with annotations.
632 var points = this.layout.annotated_points;
633 for (var i = 0; i < points.length; i++) {
634 var p = points[i];
4c919f55
DV
635 if (p.canvasx < this.area.x || p.canvasx > this.area.x + this.area.w ||
636 p.canvasy < this.area.y || p.canvasy > this.area.y + this.area.h) {
e6d53148
DV
637 continue;
638 }
639
ce5e8d36
DV
640 var a = p.annotation;
641 var tick_height = 6;
642 if (a.hasOwnProperty("tickHeight")) {
643 tick_height = a.tickHeight;
9a40897e
DV
644 }
645
ce49c2fa
DV
646 var div = document.createElement("div");
647 for (var name in annotationStyle) {
648 if (annotationStyle.hasOwnProperty(name)) {
649 div.style[name] = annotationStyle[name];
650 }
651 }
ce5e8d36
DV
652 if (!a.hasOwnProperty('icon')) {
653 div.className = "dygraphDefaultAnnotation";
654 }
655 if (a.hasOwnProperty('cssClass')) {
656 div.className += " " + a.cssClass;
657 }
658
a5ad69cc
DV
659 var width = a.hasOwnProperty('width') ? a.width : 16;
660 var height = a.hasOwnProperty('height') ? a.height : 16;
ce5e8d36
DV
661 if (a.hasOwnProperty('icon')) {
662 var img = document.createElement("img");
663 img.src = a.icon;
33030f33
DV
664 img.width = width;
665 img.height = height;
ce5e8d36
DV
666 div.appendChild(img);
667 } else if (p.annotation.hasOwnProperty('shortText')) {
668 div.appendChild(document.createTextNode(p.annotation.shortText));
5c528fa2 669 }
ce5e8d36 670 div.style.left = (p.canvasx - width / 2) + "px";
d14b9eed
DV
671 if (a.attachAtBottom) {
672 div.style.top = (this.area.h - height - tick_height) + "px";
673 } else {
674 div.style.top = (p.canvasy - height - tick_height) + "px";
675 }
ce5e8d36
DV
676 div.style.width = width + "px";
677 div.style.height = height + "px";
ce49c2fa
DV
678 div.title = p.annotation.text;
679 div.style.color = this.colors[p.name];
680 div.style.borderColor = this.colors[p.name];
e6d53148 681 a.div = div;
ab5e5c75 682
6a4587ac 683 this.dygraph_.addEvent(div, 'click',
9a40897e 684 bindEvt('clickHandler', 'annotationClickHandler', p, this));
6a4587ac 685 this.dygraph_.addEvent(div, 'mouseover',
9a40897e 686 bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p, this));
6a4587ac 687 this.dygraph_.addEvent(div, 'mouseout',
9a40897e 688 bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p, this));
6a4587ac 689 this.dygraph_.addEvent(div, 'dblclick',
9a40897e 690 bindEvt('dblClickHandler', 'annotationDblClickHandler', p, this));
ab5e5c75 691
ce49c2fa
DV
692 this.container.appendChild(div);
693 this.annotations.push(div);
9a40897e 694
2cf95fff 695 var ctx = this.elementContext;
9a40897e
DV
696 ctx.strokeStyle = this.colors[p.name];
697 ctx.beginPath();
d14b9eed
DV
698 if (!a.attachAtBottom) {
699 ctx.moveTo(p.canvasx, p.canvasy);
700 ctx.lineTo(p.canvasx, p.canvasy - 2 - tick_height);
701 } else {
702 ctx.moveTo(p.canvasx, this.area.h);
703 ctx.lineTo(p.canvasx, this.area.h - 2 - tick_height);
704 }
9a40897e
DV
705 ctx.closePath();
706 ctx.stroke();
ce49c2fa
DV
707 }
708};
709
ccb0001c 710/**
8722284b
RK
711 * Returns a predicate to be used with an iterator, which will
712 * iterate over points appropriately, depending on whether
713 * connectSeparatedPoints is true. When it's false, the predicate will
714 * skip over points with missing yVals.
ccb0001c 715 */
8722284b
RK
716DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) {
717 return connectSeparatedPoints ? DygraphCanvasRenderer._predicateThatSkipsEmptyPoints : null;
718}
719
720DygraphCanvasRenderer._predicateThatSkipsEmptyPoints =
721 function(array, idx) { return array[idx].yval !== null; }
04c104d7 722
857a6931 723DygraphCanvasRenderer.prototype._drawStyledLine = function(
5469113b
KW
724 ctx, i, setName, color, strokeWidth, strokePattern, drawPoints,
725 drawPointCallback, pointSize) {
99a77a04 726 // TODO(konigsberg): Compute attributes outside this method call.
857a6931
KW
727 var stepPlot = this.attr_("stepPlot");
728 var firstIndexInSet = this.layout.setPointsOffsets[i];
729 var setLength = this.layout.setPointsLengths[i];
857a6931 730 var points = this.layout.points;
857a6931
KW
731 if (!Dygraph.isArrayLike(strokePattern)) {
732 strokePattern = null;
733 }
a5a50727 734 var drawGapPoints = this.dygraph_.attr_('drawGapEdgePoints', setName);
857a6931 735
b843b52c 736 ctx.save();
7d1afbb9 737
7d1afbb9 738 var iter = Dygraph.createIterator(points, firstIndexInSet, setLength,
8722284b 739 DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
7d1afbb9 740
31f8e58b
RK
741 var pointsOnLine;
742 var strategy;
743 if (!strokePattern || strokePattern.length <= 1) {
744 strategy = trivialStrategy(ctx, color, strokeWidth);
b843b52c 745 } else {
31f8e58b 746 strategy = nonTrivialStrategy(this, ctx, color, strokeWidth, strokePattern);
b843b52c 747 }
31f8e58b
RK
748 pointsOnLine = this._drawSeries(ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, strategy);
749 this._drawPointsOnLine(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize);
750
b843b52c
RK
751 ctx.restore();
752};
753
31f8e58b
RK
754var nonTrivialStrategy = function(renderer, ctx, color, strokeWidth, strokePattern) {
755 return new function() {
756 this.init = function() { };
757 this.finish = function() { };
758 this.startSegment = function() {
759 ctx.beginPath();
760 ctx.strokeStyle = color;
761 ctx.lineWidth = strokeWidth;
762 };
763 this.endSegment = function() {
764 ctx.stroke(); // should this include closePath?
765 };
766 this.drawLine = function(x1, y1, x2, y2) {
767 renderer._dashedLine(ctx, x1, y1, x2, y2, strokePattern);
768 };
769 this.skipPixel = function(prevX, prevY, curX, curY) {
770 // TODO(konigsberg): optimize with http://jsperf.com/math-round-vs-hack/6 ?
771 return (Math.round(prevX) == Math.round(curX) &&
772 Math.round(prevY) == Math.round(curY));
773 };
774 };
775};
776
777var trivialStrategy = function(ctx, color, strokeWidth) {
778 return new function() {
779 this.init = function() {
780 ctx.beginPath();
781 ctx.strokeStyle = color;
782 ctx.lineWidth = strokeWidth;
783 };
784 this.finish = function() {
785 ctx.stroke(); // should this include closePath?
786 };
787 this.startSegment = function() { };
788 this.endSegment = function() { };
789 this.drawLine = function(x1, y1, x2, y2) {
790 ctx.moveTo(x1, y1);
791 ctx.lineTo(x2, y2);
792 };
793 // don't skip pixels.
794 this.skipPixel = function() {
795 return false;
796 };
797 };
798};
799
a401ca8a
RK
800DygraphCanvasRenderer.prototype._drawPointsOnLine = function(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize) {
801 for (var idx = 0; idx < pointsOnLine.length; idx++) {
802 var cb = pointsOnLine[idx];
803 ctx.save();
804 drawPointCallback(
805 this.dygraph_, setName, ctx, cb[0], cb[1], color, pointSize);
806 ctx.restore();
807 }
808}
809
31f8e58b
RK
810DygraphCanvasRenderer.prototype._drawSeries = function(
811 ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints,
812 stepPlot, strategy) {
813
31f8e58b
RK
814 var prevCanvasX = null;
815 var prevCanvasY = null;
816 var nextCanvasY = null;
817 var isIsolated; // true if this point is isolated (no line segments)
818 var point; // the point being processed in the while loop
b843b52c 819 var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
31f8e58b
RK
820 var first = true; // the first cycle through the while loop
821
822 strategy.init();
823
7d1afbb9
RK
824 while(iter.hasNext()) {
825 point = iter.next();
a02978e2 826 if (point.canvasy === null || point.canvasy != point.canvasy) {
31f8e58b 827 if (stepPlot && prevCanvasX !== null) {
857a6931 828 // Draw a horizontal line to the start of the missing data
31f8e58b
RK
829 strategy.startSegment();
830 strategy.drawLine(prevX, prevY, point.canvasx, prevY);
831 strategy.endSegment();
857a6931 832 }
31f8e58b 833 prevCanvasX = prevCanvasY = null;
857a6931 834 } else {
31f8e58b 835 nextCanvasY = iter.hasNext() ? iter.peek().canvasy : null;
a02978e2
RK
836 // TODO: we calculate isNullOrNaN for this point, and the next, and then, when
837 // we iterate, test for isNullOrNaN again. Why bother?
838 var isNextCanvasYNullOrNaN = nextCanvasY === null || nextCanvasY != nextCanvasY;
839 isIsolated = (!prevCanvasX && isNextCanvasYNullOrNaN);
19b84fe7 840 if (drawGapPoints) {
31f8e58b 841 // Also consider a point to be "isolated" if it's adjacent to a
19b84fe7 842 // null point, excluding the graph edges.
31f8e58b 843 if ((!first && !prevCanvasX) ||
a02978e2 844 (iter.hasNext() && isNextCanvasYNullOrNaN)) {
19b84fe7
KW
845 isIsolated = true;
846 }
847 }
31f8e58b
RK
848 if (prevCanvasX !== null) {
849 if (strategy.skipPixel(prevCanvasX, prevCanvasY, point.canvasx, point.canvasy)) {
857a6931
KW
850 continue;
851 }
857a6931 852 if (strokeWidth) {
31f8e58b 853 strategy.startSegment();
857a6931 854 if (stepPlot) {
31f8e58b
RK
855 strategy.drawLine(prevCanvasX, prevCanvasY, point.canvasx, prevCanvasY);
856 prevCanvasX = point.canvasx;
857a6931 857 }
31f8e58b
RK
858 strategy.drawLine(prevCanvasX, prevCanvasY, point.canvasx, point.canvasy);
859 strategy.endSegment();
b843b52c
RK
860 }
861 }
b843b52c
RK
862 if (drawPoints || isIsolated) {
863 pointsOnLine.push([point.canvasx, point.canvasy]);
864 }
31f8e58b
RK
865 prevCanvasX = point.canvasx;
866 prevCanvasY = point.canvasy;
b843b52c 867 }
7d1afbb9 868 first = false;
b843b52c 869 }
31f8e58b
RK
870 strategy.finish();
871 return pointsOnLine;
857a6931
KW
872};
873
874DygraphCanvasRenderer.prototype._drawLine = function(ctx, i) {
875 var setNames = this.layout.setNames;
876 var setName = setNames[i];
877
878 var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
879 var borderWidth = this.dygraph_.attr_("strokeBorderWidth", setName);
5469113b
KW
880 var drawPointCallback = this.dygraph_.attr_("drawPointCallback", setName) ||
881 Dygraph.Circles.DEFAULT;
99a77a04 882
857a6931 883 if (borderWidth && strokeWidth) {
5469113b 884 this._drawStyledLine(ctx, i, setName,
857a6931
KW
885 this.dygraph_.attr_("strokeBorderColor", setName),
886 strokeWidth + 2 * borderWidth,
887 this.dygraph_.attr_("strokePattern", setName),
888 this.dygraph_.attr_("drawPoints", setName),
5469113b 889 drawPointCallback,
857a6931
KW
890 this.dygraph_.attr_("pointSize", setName));
891 }
892
5469113b 893 this._drawStyledLine(ctx, i, setName,
857a6931
KW
894 this.colors[setName],
895 strokeWidth,
896 this.dygraph_.attr_("strokePattern", setName),
897 this.dygraph_.attr_("drawPoints", setName),
5469113b 898 drawPointCallback,
857a6931
KW
899 this.dygraph_.attr_("pointSize", setName));
900};
ce49c2fa 901
6a1aa64f 902/**
758a629f
DV
903 * Actually draw the lines chart, including error bars.
904 * TODO(danvk): split this into several smaller functions.
905 * @private
6a1aa64f 906 */
285a6bda 907DygraphCanvasRenderer.prototype._renderLineChart = function() {
44c6bc29 908 // TODO(danvk): use this.attr_ for many of these.
857a6931 909 var ctx = this.elementContext;
423f5ed3 910 var fillAlpha = this.attr_('fillAlpha');
e4182459 911 var errorBars = this.attr_("errorBars") || this.attr_("customBars");
44c6bc29 912 var fillGraph = this.attr_("fillGraph");
b2c9222a
DV
913 var stackedGraph = this.attr_("stackedGraph");
914 var stepPlot = this.attr_("stepPlot");
c3e1495b
AR
915 var points = this.layout.points;
916 var pointsLength = points.length;
8722284b 917 var point, i, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis;
21d3323f 918
82c6fe4d 919 var setNames = this.layout.setNames;
21d3323f 920 var setCount = setNames.length;
6a1aa64f 921
0e23cfc6 922 // TODO(danvk): Move this mapping into Dygraph and get it out of here.
758a629f
DV
923 this.colors = {};
924 for (i = 0; i < setCount; i++) {
600d841a 925 this.colors[setNames[i]] = this.colorScheme_[i % this.colorScheme_.length];
f032c51d
AV
926 }
927
ff00d3e2
DV
928 // Update Points
929 // TODO(danvk): here
b843b52c
RK
930 //
931 // TODO(bhs): this loop is a hot-spot for high-point-count charts. These
932 // transformations can be pushed into the canvas via linear transformation
933 // matrices.
758a629f
DV
934 for (i = pointsLength; i--;) {
935 point = points[i];
6a1aa64f
DV
936 point.canvasx = this.area.w * point.x + this.area.x;
937 point.canvasy = this.area.h * point.y + this.area.y;
938 }
6a1aa64f
DV
939
940 // create paths
80aaae18 941 if (errorBars) {
857a6931 942 ctx.save();
6a834bbb
DV
943 if (fillGraph) {
944 this.dygraph_.warn("Can't use fillGraph option with error bars");
945 }
946
758a629f
DV
947 for (i = 0; i < setCount; i++) {
948 setName = setNames[i];
949 axis = this.dygraph_.axisPropertiesForSeries(setName);
950 color = this.colors[setName];
6a1aa64f 951
04c104d7
KW
952 var firstIndexInSet = this.layout.setPointsOffsets[i];
953 var setLength = this.layout.setPointsLengths[i];
04c104d7 954
8722284b
RK
955 var iter = Dygraph.createIterator(points, firstIndexInSet, setLength,
956 DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
04c104d7 957
6a1aa64f 958 // setup graphics context
758a629f
DV
959 prevX = NaN;
960 prevY = NaN;
961 prevYs = [-1, -1];
962 yscale = axis.yscale;
f474c2a3 963 // should be same color as the lines but only 15% opaque.
758a629f
DV
964 rgb = new RGBColor(color);
965 err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
43af96e7 966 fillAlpha + ')';
f474c2a3 967 ctx.fillStyle = err_color;
05c9d0c4 968 ctx.beginPath();
8722284b
RK
969 while (iter.hasNext()) {
970 point = iter.next();
04c104d7 971 if (point.name == setName) { // TODO(klausw): this is always true
e9fe4a2f 972 if (!Dygraph.isOK(point.y)) {
56623f3b 973 prevX = NaN;
ae85914a 974 continue;
5011e7a1 975 }
ce49c2fa 976
3637724f 977 // TODO(danvk): here
afdc483f 978 if (stepPlot) {
758a629f 979 newYs = [ point.y_bottom, point.y_top ];
afdc483f
NN
980 prevY = point.y;
981 } else {
758a629f 982 newYs = [ point.y_bottom, point.y_top ];
afdc483f 983 }
6a1aa64f
DV
984 newYs[0] = this.area.h * newYs[0] + this.area.y;
985 newYs[1] = this.area.h * newYs[1] + this.area.y;
56623f3b 986 if (!isNaN(prevX)) {
afdc483f 987 if (stepPlot) {
47600757 988 ctx.moveTo(prevX, newYs[0]);
afdc483f 989 } else {
47600757 990 ctx.moveTo(prevX, prevYs[0]);
afdc483f 991 }
5954ef32
DV
992 ctx.lineTo(point.canvasx, newYs[0]);
993 ctx.lineTo(point.canvasx, newYs[1]);
afdc483f 994 if (stepPlot) {
47600757 995 ctx.lineTo(prevX, newYs[1]);
afdc483f 996 } else {
47600757 997 ctx.lineTo(prevX, prevYs[1]);
afdc483f 998 }
5954ef32
DV
999 ctx.closePath();
1000 }
354e15ab 1001 prevYs = newYs;
5954ef32
DV
1002 prevX = point.canvasx;
1003 }
1004 }
1005 ctx.fill();
1006 }
857a6931 1007 ctx.restore();
5954ef32 1008 } else if (fillGraph) {
857a6931 1009 ctx.save();
349dd9ba
DW
1010 var baseline = {}; // for stacked graphs: baseline for filling
1011 var currBaseline;
354e15ab
DE
1012
1013 // process sets in reverse order (needed for stacked graphs)
758a629f
DV
1014 for (i = setCount - 1; i >= 0; i--) {
1015 setName = setNames[i];
1016 color = this.colors[setName];
1017 axis = this.dygraph_.axisPropertiesForSeries(setName);
ea4942ed
DV
1018 var axisY = 1.0 + axis.minyval * axis.yscale;
1019 if (axisY < 0.0) axisY = 0.0;
1020 else if (axisY > 1.0) axisY = 1.0;
1021 axisY = this.area.h * axisY + this.area.y;
04c104d7
KW
1022 var firstIndexInSet = this.layout.setPointsOffsets[i];
1023 var setLength = this.layout.setPointsLengths[i];
04c104d7 1024
8722284b
RK
1025 var iter = Dygraph.createIterator(points, firstIndexInSet, setLength,
1026 DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
5954ef32
DV
1027
1028 // setup graphics context
758a629f
DV
1029 prevX = NaN;
1030 prevYs = [-1, -1];
1031 yscale = axis.yscale;
5954ef32 1032 // should be same color as the lines but only 15% opaque.
758a629f
DV
1033 rgb = new RGBColor(color);
1034 err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
43af96e7 1035 fillAlpha + ')';
5954ef32
DV
1036 ctx.fillStyle = err_color;
1037 ctx.beginPath();
8722284b
RK
1038 while(iter.hasNext()) {
1039 point = iter.next();
04c104d7 1040 if (point.name == setName) { // TODO(klausw): this is always true
e9fe4a2f 1041 if (!Dygraph.isOK(point.y)) {
56623f3b 1042 prevX = NaN;
5954ef32
DV
1043 continue;
1044 }
354e15ab 1045 if (stackedGraph) {
349dd9ba
DW
1046 currBaseline = baseline[point.canvasx];
1047 var lastY;
1048 if (currBaseline === undefined) {
1049 lastY = axisY;
1050 } else {
1051 if(stepPlot) {
1052 lastY = currBaseline[0];
1053 } else {
1054 lastY = currBaseline;
1055 }
1056 }
354e15ab 1057 newYs = [ point.canvasy, lastY ];
b843b52c 1058
349dd9ba
DW
1059 if(stepPlot) {
1060 // Step plots must keep track of the top and bottom of
1061 // the baseline at each point.
1062 if(prevYs[0] === -1) {
1063 baseline[point.canvasx] = [ point.canvasy, axisY ];
1064 } else {
1065 baseline[point.canvasx] = [ point.canvasy, prevYs[0] ];
1066 }
1067 } else {
1068 baseline[point.canvasx] = point.canvasy;
1069 }
b843b52c 1070
354e15ab
DE
1071 } else {
1072 newYs = [ point.canvasy, axisY ];
1073 }
56623f3b 1074 if (!isNaN(prevX)) {
05c9d0c4 1075 ctx.moveTo(prevX, prevYs[0]);
b843b52c 1076
afdc483f 1077 if (stepPlot) {
47600757 1078 ctx.lineTo(point.canvasx, prevYs[0]);
349dd9ba
DW
1079 if(currBaseline) {
1080 // Draw to the bottom of the baseline
1081 ctx.lineTo(point.canvasx, currBaseline[1]);
1082 } else {
1083 ctx.lineTo(point.canvasx, newYs[1]);
1084 }
afdc483f 1085 } else {
47600757 1086 ctx.lineTo(point.canvasx, newYs[0]);
349dd9ba 1087 ctx.lineTo(point.canvasx, newYs[1]);
afdc483f 1088 }
b843b52c 1089
05c9d0c4
DV
1090 ctx.lineTo(prevX, prevYs[1]);
1091 ctx.closePath();
6a1aa64f 1092 }
354e15ab 1093 prevYs = newYs;
6a1aa64f
DV
1094 prevX = point.canvasx;
1095 }
05c9d0c4 1096 }
6a1aa64f
DV
1097 ctx.fill();
1098 }
857a6931 1099 ctx.restore();
80aaae18
DV
1100 }
1101
f9414b11 1102 // Drawing the lines.
758a629f 1103 for (i = 0; i < setCount; i += 1) {
857a6931 1104 this._drawLine(ctx, i);
80aaae18 1105 }
6a1aa64f 1106};
79253bd0 1107
1108/**
1109 * This does dashed lines onto a canvas for a given pattern. You must call
1110 * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers
1111 * the state of the line in regards to where we left off on drawing the pattern.
1112 * You can draw a dashed line in several function calls and the pattern will be
1113 * continous as long as you didn't call this function with a different pattern
1114 * in between.
1115 * @param ctx The canvas 2d context to draw on.
1116 * @param x The start of the line's x coordinate.
1117 * @param y The start of the line's y coordinate.
1118 * @param x2 The end of the line's x coordinate.
1119 * @param y2 The end of the line's y coordinate.
1120 * @param pattern The dash pattern to draw, an array of integers where even
1121 * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5
1122 * is drawn, 2 is the space between.). A null pattern, array of length one, or
1123 * empty array will do just a solid line.
1124 * @private
1125 */
1126DygraphCanvasRenderer.prototype._dashedLine = function(ctx, x, y, x2, y2, pattern) {
1127 // Original version http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
1128 // Modified by Russell Valentine to keep line history and continue the pattern
1129 // where it left off.
1130 var dx, dy, len, rot, patternIndex, segment;
1131
1132 // If we don't have a pattern or it is an empty array or of size one just
1133 // do a solid line.
1134 if (!pattern || pattern.length <= 1) {
1135 ctx.moveTo(x, y);
1136 ctx.lineTo(x2, y2);
1137 return;
1138 }
1139
1140 // If we have a different dash pattern than the last time this was called we
1141 // reset our dash history and start the pattern from the begging
1142 // regardless of state of the last pattern.
1143 if (!Dygraph.compareArrays(pattern, this._dashedLineToHistoryPattern)) {
1144 this._dashedLineToHistoryPattern = pattern;
1145 this._dashedLineToHistory = [0, 0];
1146 }
1147 ctx.save();
1148
1149 // Calculate transformation parameters
1150 dx = (x2-x);
1151 dy = (y2-y);
1152 len = Math.sqrt(dx*dx + dy*dy);
1153 rot = Math.atan2(dy, dx);
1154
1155 // Set transformation
1156 ctx.translate(x, y);
1157 ctx.moveTo(0, 0);
1158 ctx.rotate(rot);
1159
1160 // Set last pattern index we used for this pattern.
1161 patternIndex = this._dashedLineToHistory[0];
1162 x = 0;
1163 while (len > x) {
1164 // Get the length of the pattern segment we are dealing with.
1165 segment = pattern[patternIndex];
1166 // If our last draw didn't complete the pattern segment all the way we
1167 // will try to finish it. Otherwise we will try to do the whole segment.
1168 if (this._dashedLineToHistory[1]) {
1169 x += this._dashedLineToHistory[1];
1170 } else {
1171 x += segment;
1172 }
1173 if (x > len) {
1174 // We were unable to complete this pattern index all the way, keep
1175 // where we are the history so our next draw continues where we left off
1176 // in the pattern.
1177 this._dashedLineToHistory = [patternIndex, x-len];
1178 x = len;
1179 } else {
1180 // We completed this patternIndex, we put in the history that we are on
1181 // the beginning of the next segment.
1182 this._dashedLineToHistory = [(patternIndex+1)%pattern.length, 0];
1183 }
1184
1185 // We do a line on a even pattern index and just move on a odd pattern index.
1186 // The move is the empty space in the dash.
1187 if(patternIndex % 2 === 0) {
1188 ctx.lineTo(x, 0);
1189 } else {
1190 ctx.moveTo(x, 0);
1191 }
1192 // If we are not done, next loop process the next pattern segment, or the
1193 // first segment again if we are at the end of the pattern.
1194 patternIndex = (patternIndex+1) % pattern.length;
1195 }
1196 ctx.restore();
1197};