re-add layout plugin event; title display works
[dygraphs.git] / dygraph-canvas.js
1 /**
2 * @license
3 * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
5 */
6
7 /**
8 * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the
9 * needs of dygraphs.
10 *
11 * In particular, support for:
12 * - grid overlays
13 * - error bars
14 * - dygraphs attribute system
15 */
16
17 /**
18 * The DygraphCanvasRenderer class does the actual rendering of the chart onto
19 * a canvas. It's based on PlotKit.CanvasRenderer.
20 * @param {Object} element The canvas to attach to
21 * @param {Object} elementContext The 2d context of the canvas (injected so it
22 * can be mocked for testing.)
23 * @param {Layout} layout The DygraphLayout object for this graph.
24 * @constructor
25 */
26
27 /*jshint globalstrict: true */
28 /*global Dygraph:false,RGBColor:false */
29 "use strict";
30
31
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 */
49 var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
50 this.dygraph_ = dygraph;
51
52 this.layout = layout;
53 this.element = element;
54 this.elementContext = elementContext;
55 this.container = this.element.parentNode;
56
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
65 this.xlabels = [];
66 this.ylabels = [];
67 this.annotations = [];
68 this.chartLabels = {};
69
70 this.area = layout.getPlotArea();
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.
76 if (this.dygraph_.isUsingExcanvas_) {
77 this._createIEClipArea();
78 } else {
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();
86
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 }
92 }
93 };
94
95 DygraphCanvasRenderer.prototype.attr_ = function(x) {
96 return this.dygraph_.attr_(x);
97 };
98
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 */
105 DygraphCanvasRenderer.prototype.clear = function() {
106 var context;
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 }
114 context = this.elementContext;
115 }
116 catch (e) {
117 // TODO(danvk): this is broken, since MochiKit.Async is gone.
118 // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
119 // this.clearDelay.addCallback(bind(this.clear, this));
120 return;
121 }
122 }
123
124 context = this.elementContext;
125 context.clearRect(0, 0, this.width, this.height);
126
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 }
132 }
133
134 removeArray(this.xlabels);
135 removeArray(this.ylabels);
136 removeArray(this.annotations);
137
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 }
143 this.xlabels = [];
144 this.ylabels = [];
145 this.annotations = [];
146 this.chartLabels = {};
147 };
148
149 /**
150 * Checks whether the browser supports the &lt;canvas&gt; tag.
151 * @private
152 */
153 DygraphCanvasRenderer.isSupported = function(canvasName) {
154 var canvas = null;
155 try {
156 if (typeof(canvasName) == 'undefined' || canvasName === null) {
157 canvas = document.createElement("canvas");
158 } else {
159 canvas = canvasName;
160 }
161 canvas.getContext("2d");
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;
171 };
172
173 /**
174 * @param { [String] } colors Array of color strings. Should have one entry for
175 * each series to be rendered.
176 */
177 DygraphCanvasRenderer.prototype.setColors = function(colors) {
178 this.colorScheme_ = colors;
179 };
180
181 /**
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
187 */
188 DygraphCanvasRenderer.prototype.render = function() {
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.
191 var ctx = this.elementContext;
192 function halfUp(x) { return Math.round(x) + 0.5; }
193 function halfDown(y){ return Math.round(y) - 0.5; }
194
195 if (this.attr_('underlayCallback')) {
196 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
197 // users who expect a deprecated form of this callback.
198 this.attr_('underlayCallback')(ctx, this.area, this.dygraph_, this.dygraph_);
199 }
200
201 var x, y, i, ticks;
202 if (this.attr_('drawYGrid')) {
203 ticks = this.layout.yticks;
204 // TODO(konigsberg): I don't think these calls to save() have a corresponding restore().
205 ctx.save();
206 ctx.strokeStyle = this.attr_('gridLineColor');
207 ctx.lineWidth = this.attr_('gridLineWidth');
208 for (i = 0; i < ticks.length; i++) {
209 // TODO(danvk): allow secondary axes to draw a grid, too.
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);
213 ctx.beginPath();
214 ctx.moveTo(x, y);
215 ctx.lineTo(x + this.area.w, y);
216 ctx.closePath();
217 ctx.stroke();
218 }
219 ctx.restore();
220 }
221
222 if (this.attr_('drawXGrid')) {
223 ticks = this.layout.xticks;
224 ctx.save();
225 ctx.strokeStyle = this.attr_('gridLineColor');
226 ctx.lineWidth = this.attr_('gridLineWidth');
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);
230 ctx.beginPath();
231 ctx.moveTo(x, y);
232 ctx.lineTo(x, this.area.y);
233 ctx.closePath();
234 ctx.stroke();
235 }
236 ctx.restore();
237 }
238
239 // Do the ordinary rendering, as before
240 this._renderLineChart();
241 this._renderAxis();
242 // this._renderChartLabels();
243 this._renderAnnotations();
244 };
245
246 DygraphCanvasRenderer.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) {
270 if (area.w === 0 || area.h === 0) {
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
286 createClipDiv({
287 x:0, y:0,
288 w:plotArea.x,
289 h:this.height
290 });
291
292 // Top
293 createClipDiv({
294 x: plotArea.x, y: 0,
295 w: this.width - plotArea.x,
296 h: plotArea.y
297 });
298
299 // Right side
300 createClipDiv({
301 x: plotArea.x + plotArea.w, y: 0,
302 w: this.width-plotArea.x - plotArea.w,
303 h: this.height
304 });
305
306 // Bottom
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 };
314
315 DygraphCanvasRenderer.prototype._renderAxis = function() {
316 if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) return;
317
318 // Round pixels to half-integer boundaries for crisper drawing.
319 function halfUp(x) { return Math.round(x) + 0.5; }
320 function halfDown(y){ return Math.round(y) - 0.5; }
321
322 var context = this.elementContext;
323
324 var label, x, y, tick, i;
325
326 var labelStyle = {
327 position: "absolute",
328 fontSize: this.attr_('axisLabelFontSize') + "px",
329 zIndex: 10,
330 color: this.attr_('axisLabelColor'),
331 width: this.attr_('axisLabelWidth') + "px",
332 // height: this.attr_('axisLabelFontSize') + 2 + "px",
333 lineHeight: "normal", // Something other than "normal" line-height screws up label positioning.
334 overflow: "hidden"
335 };
336 var makeDiv = function(txt, axis, prec_axis) {
337 var div = document.createElement("div");
338 for (var name in labelStyle) {
339 if (labelStyle.hasOwnProperty(name)) {
340 div.style[name] = labelStyle[name];
341 }
342 }
343 var inner_div = document.createElement("div");
344 inner_div.className = 'dygraph-axis-label' +
345 ' dygraph-axis-label-' + axis +
346 (prec_axis ? ' dygraph-axis-label-' + prec_axis : '');
347 inner_div.innerHTML=txt;
348 div.appendChild(inner_div);
349 return div;
350 };
351
352 // axis lines
353 context.save();
354 context.strokeStyle = this.attr_('axisLineColor');
355 context.lineWidth = this.attr_('axisLineWidth');
356
357 if (this.attr_('drawYAxis')) {
358 if (this.layout.yticks && this.layout.yticks.length > 0) {
359 var num_axes = this.dygraph_.numAxes();
360 for (i = 0; i < this.layout.yticks.length; i++) {
361 tick = this.layout.yticks[i];
362 if (typeof(tick) == "function") return;
363 x = this.area.x;
364 var sgn = 1;
365 var prec_axis = 'y1';
366 if (tick[0] == 1) { // right-side y-axis
367 x = this.area.x + this.area.w;
368 sgn = -1;
369 prec_axis = 'y2';
370 }
371 y = this.area.y + tick[1] * this.area.h;
372
373 /* Tick marks are currently clipped, so don't bother drawing them.
374 context.beginPath();
375 context.moveTo(halfUp(x), halfDown(y));
376 context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
377 context.closePath();
378 context.stroke();
379 */
380
381 label = makeDiv(tick[2], 'y', num_axes == 2 ? prec_axis : null);
382 var top = (y - this.attr_('axisLabelFontSize') / 2);
383 if (top < 0) top = 0;
384
385 if (top + this.attr_('axisLabelFontSize') + 3 > this.height) {
386 label.style.bottom = "0px";
387 } else {
388 label.style.top = top + "px";
389 }
390 if (tick[0] === 0) {
391 label.style.left = (this.area.x - this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
392 label.style.textAlign = "right";
393 } else if (tick[0] == 1) {
394 label.style.left = (this.area.x + this.area.w +
395 this.attr_('axisTickSize')) + "px";
396 label.style.textAlign = "left";
397 }
398 label.style.width = this.attr_('yAxisLabelWidth') + "px";
399 this.container.appendChild(label);
400 this.ylabels.push(label);
401 }
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];
407 var fontSize = this.attr_('axisLabelFontSize');
408 var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
409 if (bottom > this.height - fontSize) {
410 bottomTick.style.top = (parseInt(bottomTick.style.top, 10) -
411 fontSize / 2) + "px";
412 }
413 }
414
415 // draw a vertical line on the left to separate the chart from the labels.
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 }
424 context.beginPath();
425 context.moveTo(axisX, halfDown(this.area.y));
426 context.lineTo(axisX, halfDown(this.area.y + this.area.h));
427 context.closePath();
428 context.stroke();
429
430 // if there's a secondary y-axis, draw a vertical line for that, too.
431 if (this.dygraph_.numAxes() == 2) {
432 context.beginPath();
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));
435 context.closePath();
436 context.stroke();
437 }
438 }
439
440 if (this.attr_('drawXAxis')) {
441 if (this.layout.xticks) {
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;
446
447 /* Tick marks are currently clipped, so don't bother drawing them.
448 context.beginPath();
449 context.moveTo(halfUp(x), halfDown(y));
450 context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
451 context.closePath();
452 context.stroke();
453 */
454
455 label = makeDiv(tick[1], 'x');
456 label.style.textAlign = "center";
457 label.style.top = (y + this.attr_('axisTickSize')) + 'px';
458
459 var left = (x - this.attr_('axisLabelWidth')/2);
460 if (left + this.attr_('axisLabelWidth') > this.width) {
461 left = this.width - this.attr_('xAxisLabelWidth');
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";
470 label.style.width = this.attr_('xAxisLabelWidth') + "px";
471 this.container.appendChild(label);
472 this.xlabels.push(label);
473 }
474 }
475
476 context.beginPath();
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);
487 context.closePath();
488 context.stroke();
489 }
490
491 context.restore();
492 };
493
494
495 DygraphCanvasRenderer.prototype._renderChartLabels = function() {
496 var div, class_div;
497
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')) {
502 div = document.createElement("div");
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';
509 div.style.fontSize = (this.attr_('titleHeight') - 8) + 'px';
510 div.style.fontWeight = 'bold';
511 class_div = document.createElement("div");
512 class_div.className = 'dygraph-label dygraph-title';
513 class_div.innerHTML = this.attr_('title');
514 div.appendChild(class_div);
515 this.container.appendChild(div);
516 this.chartLabels.title = div;
517 }
518
519 if (this.attr_('xlabel')) {
520 div = document.createElement("div");
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';
527 div.style.fontSize = (this.attr_('xLabelHeight') - 2) + 'px';
528
529 class_div = document.createElement("div");
530 class_div.className = 'dygraph-label dygraph-xlabel';
531 class_div.innerHTML = this.attr_('xlabel');
532 div.appendChild(class_div);
533 this.container.appendChild(div);
534 this.chartLabels.xlabel = div;
535 }
536
537 var that = this;
538 function createRotatedDiv(axis, classes, html) {
539 var box = {
540 left: 0,
541 top: that.area.y,
542 width: that.attr_('yLabelWidth'),
543 height: that.area.h
544 };
545 // TODO(danvk): is this outer div actually necessary?
546 div = document.createElement("div");
547 div.style.position = 'absolute';
548 if (axis == 1) {
549 div.style.left = box.left;
550 } else {
551 div.style.right = box.left;
552 }
553 div.style.top = box.top + 'px';
554 div.style.width = box.width + 'px';
555 div.style.height = box.height + 'px';
556 div.style.fontSize = (that.attr_('yLabelWidth') - 2) + 'px';
557
558 var inner_div = document.createElement("div");
559 inner_div.style.position = 'absolute';
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';
565
566 // CSS rotation is an HTML5 feature which is not standardized. Hence every
567 // browser has its own name for the CSS style.
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
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 =
581 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' +
582 (axis == 1 ? '3' : '1') + ')';
583 inner_div.style.left = '0px';
584 inner_div.style.top = '0px';
585 }
586
587 class_div = document.createElement("div");
588 class_div.className = classes;
589 class_div.innerHTML = html;
590
591 inner_div.appendChild(class_div);
592 div.appendChild(inner_div);
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'));
600 this.container.appendChild(div);
601 this.chartLabels.ylabel = div;
602 }
603 if (this.attr_('y2label') && this.dygraph_.numAxes() == 2) {
604 div = createRotatedDiv(2, 'dygraph-label dygraph-y2label',
605 this.attr_('y2label'));
606 this.container.appendChild(div);
607 this.chartLabels.y2label = div;
608 }
609 };
610
611
612 DygraphCanvasRenderer.prototype._renderAnnotations = function() {
613 var annotationStyle = {
614 "position": "absolute",
615 "fontSize": this.attr_('axisLabelFontSize') + "px",
616 "zIndex": 10,
617 "overflow": "hidden"
618 };
619
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 };
629 };
630
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];
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) {
637 continue;
638 }
639
640 var a = p.annotation;
641 var tick_height = 6;
642 if (a.hasOwnProperty("tickHeight")) {
643 tick_height = a.tickHeight;
644 }
645
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 }
652 if (!a.hasOwnProperty('icon')) {
653 div.className = "dygraphDefaultAnnotation";
654 }
655 if (a.hasOwnProperty('cssClass')) {
656 div.className += " " + a.cssClass;
657 }
658
659 var width = a.hasOwnProperty('width') ? a.width : 16;
660 var height = a.hasOwnProperty('height') ? a.height : 16;
661 if (a.hasOwnProperty('icon')) {
662 var img = document.createElement("img");
663 img.src = a.icon;
664 img.width = width;
665 img.height = height;
666 div.appendChild(img);
667 } else if (p.annotation.hasOwnProperty('shortText')) {
668 div.appendChild(document.createTextNode(p.annotation.shortText));
669 }
670 div.style.left = (p.canvasx - width / 2) + "px";
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 }
676 div.style.width = width + "px";
677 div.style.height = height + "px";
678 div.title = p.annotation.text;
679 div.style.color = this.colors[p.name];
680 div.style.borderColor = this.colors[p.name];
681 a.div = div;
682
683 this.dygraph_.addEvent(div, 'click',
684 bindEvt('clickHandler', 'annotationClickHandler', p, this));
685 this.dygraph_.addEvent(div, 'mouseover',
686 bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p, this));
687 this.dygraph_.addEvent(div, 'mouseout',
688 bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p, this));
689 this.dygraph_.addEvent(div, 'dblclick',
690 bindEvt('dblClickHandler', 'annotationDblClickHandler', p, this));
691
692 this.container.appendChild(div);
693 this.annotations.push(div);
694
695 var ctx = this.elementContext;
696 ctx.strokeStyle = this.colors[p.name];
697 ctx.beginPath();
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 }
705 ctx.closePath();
706 ctx.stroke();
707 }
708 };
709
710 /**
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.
715 */
716 DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) {
717 return connectSeparatedPoints ? DygraphCanvasRenderer._predicateThatSkipsEmptyPoints : null;
718 }
719
720 DygraphCanvasRenderer._predicateThatSkipsEmptyPoints =
721 function(array, idx) { return array[idx].yval !== null; }
722
723 DygraphCanvasRenderer.prototype._drawStyledLine = function(
724 ctx, i, setName, color, strokeWidth, strokePattern, drawPoints,
725 drawPointCallback, pointSize) {
726 // TODO(konigsberg): Compute attributes outside this method call.
727 var stepPlot = this.attr_("stepPlot");
728 var firstIndexInSet = this.layout.setPointsOffsets[i];
729 var setLength = this.layout.setPointsLengths[i];
730 var points = this.layout.points;
731 if (!Dygraph.isArrayLike(strokePattern)) {
732 strokePattern = null;
733 }
734 var drawGapPoints = this.dygraph_.attr_('drawGapEdgePoints', setName);
735
736 ctx.save();
737
738 var iter = Dygraph.createIterator(points, firstIndexInSet, setLength,
739 DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
740
741 var pointsOnLine;
742 var strategy;
743 if (!strokePattern || strokePattern.length <= 1) {
744 strategy = trivialStrategy(ctx, color, strokeWidth);
745 } else {
746 strategy = nonTrivialStrategy(this, ctx, color, strokeWidth, strokePattern);
747 }
748 pointsOnLine = this._drawSeries(ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, strategy);
749 this._drawPointsOnLine(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize);
750
751 ctx.restore();
752 };
753
754 var 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
777 var 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
800 DygraphCanvasRenderer.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
810 DygraphCanvasRenderer.prototype._drawSeries = function(
811 ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints,
812 stepPlot, strategy) {
813
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
819 var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
820 var first = true; // the first cycle through the while loop
821
822 strategy.init();
823
824 while(iter.hasNext()) {
825 point = iter.next();
826 if (point.canvasy === null || point.canvasy != point.canvasy) {
827 if (stepPlot && prevCanvasX !== null) {
828 // Draw a horizontal line to the start of the missing data
829 strategy.startSegment();
830 strategy.drawLine(prevX, prevY, point.canvasx, prevY);
831 strategy.endSegment();
832 }
833 prevCanvasX = prevCanvasY = null;
834 } else {
835 nextCanvasY = iter.hasNext() ? iter.peek().canvasy : null;
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);
840 if (drawGapPoints) {
841 // Also consider a point to be "isolated" if it's adjacent to a
842 // null point, excluding the graph edges.
843 if ((!first && !prevCanvasX) ||
844 (iter.hasNext() && isNextCanvasYNullOrNaN)) {
845 isIsolated = true;
846 }
847 }
848 if (prevCanvasX !== null) {
849 if (strategy.skipPixel(prevCanvasX, prevCanvasY, point.canvasx, point.canvasy)) {
850 continue;
851 }
852 if (strokeWidth) {
853 strategy.startSegment();
854 if (stepPlot) {
855 strategy.drawLine(prevCanvasX, prevCanvasY, point.canvasx, prevCanvasY);
856 prevCanvasX = point.canvasx;
857 }
858 strategy.drawLine(prevCanvasX, prevCanvasY, point.canvasx, point.canvasy);
859 strategy.endSegment();
860 }
861 }
862 if (drawPoints || isIsolated) {
863 pointsOnLine.push([point.canvasx, point.canvasy]);
864 }
865 prevCanvasX = point.canvasx;
866 prevCanvasY = point.canvasy;
867 }
868 first = false;
869 }
870 strategy.finish();
871 return pointsOnLine;
872 };
873
874 DygraphCanvasRenderer.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);
880 var drawPointCallback = this.dygraph_.attr_("drawPointCallback", setName) ||
881 Dygraph.Circles.DEFAULT;
882
883 if (borderWidth && strokeWidth) {
884 this._drawStyledLine(ctx, i, setName,
885 this.dygraph_.attr_("strokeBorderColor", setName),
886 strokeWidth + 2 * borderWidth,
887 this.dygraph_.attr_("strokePattern", setName),
888 this.dygraph_.attr_("drawPoints", setName),
889 drawPointCallback,
890 this.dygraph_.attr_("pointSize", setName));
891 }
892
893 this._drawStyledLine(ctx, i, setName,
894 this.colors[setName],
895 strokeWidth,
896 this.dygraph_.attr_("strokePattern", setName),
897 this.dygraph_.attr_("drawPoints", setName),
898 drawPointCallback,
899 this.dygraph_.attr_("pointSize", setName));
900 };
901
902 /**
903 * Actually draw the lines chart, including error bars.
904 * TODO(danvk): split this into several smaller functions.
905 * @private
906 */
907 DygraphCanvasRenderer.prototype._renderLineChart = function() {
908 // TODO(danvk): use this.attr_ for many of these.
909 var ctx = this.elementContext;
910 var fillAlpha = this.attr_('fillAlpha');
911 var errorBars = this.attr_("errorBars") || this.attr_("customBars");
912 var fillGraph = this.attr_("fillGraph");
913 var stackedGraph = this.attr_("stackedGraph");
914 var stepPlot = this.attr_("stepPlot");
915 var points = this.layout.points;
916 var pointsLength = points.length;
917 var point, i, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis;
918
919 var setNames = this.layout.setNames;
920 var setCount = setNames.length;
921
922 // TODO(danvk): Move this mapping into Dygraph and get it out of here.
923 this.colors = {};
924 for (i = 0; i < setCount; i++) {
925 this.colors[setNames[i]] = this.colorScheme_[i % this.colorScheme_.length];
926 }
927
928 // Update Points
929 // TODO(danvk): here
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.
934 for (i = pointsLength; i--;) {
935 point = points[i];
936 point.canvasx = this.area.w * point.x + this.area.x;
937 point.canvasy = this.area.h * point.y + this.area.y;
938 }
939
940 // create paths
941 if (errorBars) {
942 ctx.save();
943 if (fillGraph) {
944 this.dygraph_.warn("Can't use fillGraph option with error bars");
945 }
946
947 for (i = 0; i < setCount; i++) {
948 setName = setNames[i];
949 axis = this.dygraph_.axisPropertiesForSeries(setName);
950 color = this.colors[setName];
951
952 var firstIndexInSet = this.layout.setPointsOffsets[i];
953 var setLength = this.layout.setPointsLengths[i];
954
955 var iter = Dygraph.createIterator(points, firstIndexInSet, setLength,
956 DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
957
958 // setup graphics context
959 prevX = NaN;
960 prevY = NaN;
961 prevYs = [-1, -1];
962 yscale = axis.yscale;
963 // should be same color as the lines but only 15% opaque.
964 rgb = new RGBColor(color);
965 err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
966 fillAlpha + ')';
967 ctx.fillStyle = err_color;
968 ctx.beginPath();
969 while (iter.hasNext()) {
970 point = iter.next();
971 if (point.name == setName) { // TODO(klausw): this is always true
972 if (!Dygraph.isOK(point.y)) {
973 prevX = NaN;
974 continue;
975 }
976
977 // TODO(danvk): here
978 if (stepPlot) {
979 newYs = [ point.y_bottom, point.y_top ];
980 prevY = point.y;
981 } else {
982 newYs = [ point.y_bottom, point.y_top ];
983 }
984 newYs[0] = this.area.h * newYs[0] + this.area.y;
985 newYs[1] = this.area.h * newYs[1] + this.area.y;
986 if (!isNaN(prevX)) {
987 if (stepPlot) {
988 ctx.moveTo(prevX, newYs[0]);
989 } else {
990 ctx.moveTo(prevX, prevYs[0]);
991 }
992 ctx.lineTo(point.canvasx, newYs[0]);
993 ctx.lineTo(point.canvasx, newYs[1]);
994 if (stepPlot) {
995 ctx.lineTo(prevX, newYs[1]);
996 } else {
997 ctx.lineTo(prevX, prevYs[1]);
998 }
999 ctx.closePath();
1000 }
1001 prevYs = newYs;
1002 prevX = point.canvasx;
1003 }
1004 }
1005 ctx.fill();
1006 }
1007 ctx.restore();
1008 } else if (fillGraph) {
1009 ctx.save();
1010 var baseline = {}; // for stacked graphs: baseline for filling
1011 var currBaseline;
1012
1013 // process sets in reverse order (needed for stacked graphs)
1014 for (i = setCount - 1; i >= 0; i--) {
1015 setName = setNames[i];
1016 color = this.colors[setName];
1017 axis = this.dygraph_.axisPropertiesForSeries(setName);
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;
1022 var firstIndexInSet = this.layout.setPointsOffsets[i];
1023 var setLength = this.layout.setPointsLengths[i];
1024
1025 var iter = Dygraph.createIterator(points, firstIndexInSet, setLength,
1026 DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
1027
1028 // setup graphics context
1029 prevX = NaN;
1030 prevYs = [-1, -1];
1031 yscale = axis.yscale;
1032 // should be same color as the lines but only 15% opaque.
1033 rgb = new RGBColor(color);
1034 err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
1035 fillAlpha + ')';
1036 ctx.fillStyle = err_color;
1037 ctx.beginPath();
1038 while(iter.hasNext()) {
1039 point = iter.next();
1040 if (point.name == setName) { // TODO(klausw): this is always true
1041 if (!Dygraph.isOK(point.y)) {
1042 prevX = NaN;
1043 continue;
1044 }
1045 if (stackedGraph) {
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 }
1057 newYs = [ point.canvasy, lastY ];
1058
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 }
1070
1071 } else {
1072 newYs = [ point.canvasy, axisY ];
1073 }
1074 if (!isNaN(prevX)) {
1075 ctx.moveTo(prevX, prevYs[0]);
1076
1077 if (stepPlot) {
1078 ctx.lineTo(point.canvasx, prevYs[0]);
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 }
1085 } else {
1086 ctx.lineTo(point.canvasx, newYs[0]);
1087 ctx.lineTo(point.canvasx, newYs[1]);
1088 }
1089
1090 ctx.lineTo(prevX, prevYs[1]);
1091 ctx.closePath();
1092 }
1093 prevYs = newYs;
1094 prevX = point.canvasx;
1095 }
1096 }
1097 ctx.fill();
1098 }
1099 ctx.restore();
1100 }
1101
1102 // Drawing the lines.
1103 for (i = 0; i < setCount; i += 1) {
1104 this._drawLine(ctx, i);
1105 }
1106 };
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 */
1126 DygraphCanvasRenderer.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 };