1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
5 * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the
8 * In particular, support for:
11 * - dygraphs attribute system
15 * This class determines the charting area (in pixel coordinates), maps the
16 * percentage coordinates in the DygraphLayout to pixels and draws them.
17 * It's also responsible for creating chart DOM elements, i.e. annotations,
18 * tick mark labels, the title and the x/y-axis labels.
19 * This class is based on PlotKit.CanvasRenderer.
21 * Creates a new DygraphLayout object.
22 * @return {Object} The DygraphLayout object
24 DygraphLayout
= function(dygraph
) {
25 this.dygraph_
= dygraph
;
26 this.datasets
= new Array();
27 this.annotations
= new Array();
30 // TODO(danvk): it's odd that xTicks_ and yTicks_ are inputs, but xticks and
31 // yticks are outputs. Clean this up.
36 DygraphLayout
.prototype.attr_
= function(name
) {
37 return this.dygraph_
.attr_(name
);
40 DygraphLayout
.prototype.addDataset
= function(setname
, set_xy
) {
41 this.datasets
[setname
] = set_xy
;
44 DygraphLayout
.prototype.setAnnotations
= function(ann
) {
45 // The Dygraph object's annotations aren't parsed. We parse them here and
46 // save a copy. If there is no parser, then the user must be using raw format.
47 this.annotations
= [];
48 var parse
= this.attr_('xValueParser') || function(x
) { return x
; };
49 for (var i
= 0; i
< ann
.length
; i
++) {
51 if (!ann
[i
].xval
&& !ann
[i
].x
) {
52 this.dygraph_
.error("Annotations must have an 'x' property");
56 !(ann
[i
].hasOwnProperty('width') &&
57 ann
[i
].hasOwnProperty('height'))) {
58 this.dygraph_
.error("Must set width and height when setting " +
59 "annotation.icon property");
62 Dygraph
.update(a
, ann
[i
]);
63 if (!a
.xval
) a
.xval
= parse(a
.x
, this.dygraph_
);
64 this.annotations
.push(a
);
68 DygraphLayout
.prototype.setXTicks
= function(xTicks
) {
69 this.xTicks_
= xTicks
;
72 // TODO(danvk): add this to the Dygraph object's API or move it into Layout.
73 DygraphLayout
.prototype.setYAxes
= function (yAxes
) {
77 DygraphLayout
.prototype.setDateWindow
= function(dateWindow
) {
78 this.dateWindow_
= dateWindow
;
81 DygraphLayout
.prototype.evaluate
= function() {
82 this._evaluateLimits();
83 this._evaluateLineCharts();
84 this._evaluateLineTicks();
85 this._evaluateAnnotations();
88 DygraphLayout
.prototype._evaluateLimits
= function() {
89 this.minxval
= this.maxxval
= null;
90 if (this.dateWindow_
) {
91 this.minxval
= this.dateWindow_
[0];
92 this.maxxval
= this.dateWindow_
[1];
94 for (var name
in this.datasets
) {
95 if (!this.datasets
.hasOwnProperty(name
)) continue;
96 var series
= this.datasets
[name
];
97 if (series
.length
> 1) {
98 var x1
= series
[0][0];
99 if (!this.minxval
|| x1
< this.minxval
) this.minxval
= x1
;
101 var x2
= series
[series
.length
- 1][0];
102 if (!this.maxxval
|| x2
> this.maxxval
) this.maxxval
= x2
;
106 this.xrange
= this.maxxval
- this.minxval
;
107 this.xscale
= (this.xrange
!= 0 ? 1/this.xrange
: 1.0);
109 for (var i
= 0; i
< this.yAxes_
.length
; i
++) {
110 var axis
= this.yAxes_
[i
];
111 axis
.minyval
= axis
.computedValueRange
[0];
112 axis
.maxyval
= axis
.computedValueRange
[1];
113 axis
.yrange
= axis
.maxyval
- axis
.minyval
;
114 axis
.yscale
= (axis
.yrange
!= 0 ? 1.0 / axis
.yrange
: 1.0);
116 if (axis
.g
.attr_("logscale")) {
117 axis
.ylogrange
= Dygraph
.log10(axis
.maxyval
) - Dygraph
.log10(axis
.minyval
);
118 axis
.ylogscale
= (axis
.ylogrange
!= 0 ? 1.0 / axis
.ylogrange
: 1.0);
119 if (!isFinite(axis
.ylogrange
) || isNaN(axis
.ylogrange
)) {
120 axis
.g
.error('axis ' + i
+ ' of graph at ' + axis
.g
+
121 ' can\'t be displayed in log scale for range [' +
122 axis
.minyval
+ ' - ' + axis
.maxyval
+ ']');
128 DygraphLayout
.prototype._evaluateLineCharts
= function() {
130 this.points
= new Array();
131 for (var setName
in this.datasets
) {
132 if (!this.datasets
.hasOwnProperty(setName
)) continue;
134 var dataset
= this.datasets
[setName
];
135 var axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
137 for (var j
= 0; j
< dataset
.length
; j
++) {
138 var item
= dataset
[j
];
142 yval
= 1.0 - ((Dygraph
.log10(parseFloat(item
[1])) - Dygraph
.log10(axis
.minyval
)) * axis
.ylogscale
); // really should just be yscale.
144 yval
= 1.0 - ((parseFloat(item
[1]) - axis
.minyval
) * axis
.yscale
);
148 x
: ((parseFloat(item
[0]) - this.minxval
) * this.xscale
),
150 xval
: parseFloat(item
[0]),
151 yval
: parseFloat(item
[1]),
155 this.points
.push(point
);
160 DygraphLayout
.prototype._evaluateLineTicks
= function() {
161 this.xticks
= new Array();
162 for (var i
= 0; i
< this.xTicks_
.length
; i
++) {
163 var tick
= this.xTicks_
[i
];
164 var label
= tick
.label
;
165 var pos
= this.xscale
* (tick
.v
- this.minxval
);
166 if ((pos
>= 0.0) && (pos
<= 1.0)) {
167 this.xticks
.push([pos
, label
]);
171 this.yticks
= new Array();
172 for (var i
= 0; i
< this.yAxes_
.length
; i
++ ) {
173 var axis
= this.yAxes_
[i
];
174 for (var j
= 0; j
< axis
.ticks
.length
; j
++) {
175 var tick
= axis
.ticks
[j
];
176 var label
= tick
.label
;
177 var pos
= this.dygraph_
.toPercentYCoord(tick
.v
, i
);
178 if ((pos
>= 0.0) && (pos
<= 1.0)) {
179 this.yticks
.push([i
, pos
, label
]);
187 * Behaves the same way as PlotKit.Layout, but also copies the errors
190 DygraphLayout
.prototype.evaluateWithError
= function() {
192 if (!(this.attr_('errorBars') || this.attr_('customBars'))) return;
194 // Copy over the error terms
195 var i
= 0; // index in this.points
196 for (var setName
in this.datasets
) {
197 if (!this.datasets
.hasOwnProperty(setName
)) continue;
199 var dataset
= this.datasets
[setName
];
200 for (var j
= 0; j
< dataset
.length
; j
++, i
++) {
201 var item
= dataset
[j
];
202 var xv
= parseFloat(item
[0]);
203 var yv
= parseFloat(item
[1]);
205 if (xv
== this.points
[i
].xval
&&
206 yv
== this.points
[i
].yval
) {
207 this.points
[i
].errorMinus
= parseFloat(item
[2]);
208 this.points
[i
].errorPlus
= parseFloat(item
[3]);
214 DygraphLayout
.prototype._evaluateAnnotations
= function() {
215 // Add the annotations to the point to which they belong.
216 // Make a map from (setName, xval) to annotation for quick lookups.
217 var annotations
= {};
218 for (var i
= 0; i
< this.annotations
.length
; i
++) {
219 var a
= this.annotations
[i
];
220 annotations
[a
.xval
+ "," + a
.series
] = a
;
223 this.annotated_points
= [];
224 for (var i
= 0; i
< this.points
.length
; i
++) {
225 var p
= this.points
[i
];
226 var k
= p
.xval
+ "," + p
.name
;
227 if (k
in annotations
) {
228 p
.annotation
= annotations
[k
];
229 this.annotated_points
.push(p
);
235 * Convenience function to remove all the data sets from a graph
237 DygraphLayout
.prototype.removeAllDatasets
= function() {
238 delete this.datasets
;
239 this.datasets
= new Array();
243 * Return a copy of the point at the indicated index, with its yval unstacked.
244 * @param int index of point in layout_.points
246 DygraphLayout
.prototype.unstackPointAtIndex
= function(idx
) {
247 var point
= this.points
[idx
];
249 // Clone the point since we modify it
250 var unstackedPoint
= {};
251 for (var i
in point
) {
252 unstackedPoint
[i
] = point
[i
];
255 if (!this.attr_("stackedGraph")) {
256 return unstackedPoint
;
259 // The unstacked yval is equal to the current yval minus the yval of the
260 // next point at the same xval.
261 for (var i
= idx
+1; i
< this.points
.length
; i
++) {
262 if (this.points
[i
].xval
== point
.xval
) {
263 unstackedPoint
.yval
-= this.points
[i
].yval
;
268 return unstackedPoint
;
272 * The DygraphCanvasRenderer class does the actual rendering of the chart onto
273 * a canvas. It's based on PlotKit.CanvasRenderer.
275 * @param {Object} element The canvas to attach to
276 * @param {Object} elementContext The 2d context of the canvas (injected so it
277 * can be mocked for testing.)
278 * @param {Layout} layout The DygraphLayout object for this graph.
281 DygraphCanvasRenderer
= function(dygraph
, element
, elementContext
, layout
) {
282 this.dygraph_
= dygraph
;
284 this.layout
= layout
;
285 this.element
= element
;
286 this.elementContext
= elementContext
;
287 this.container
= this.element
.parentNode
;
289 this.height
= this.element
.height
;
290 this.width
= this.element
.width
;
292 // --- check whether everything is ok before we return
293 if (!this.isIE
&& !(DygraphCanvasRenderer
.isSupported(this.element
)))
294 throw "Canvas is not supported.";
297 this.xlabels
= new Array();
298 this.ylabels
= new Array();
299 this.annotations
= new Array();
300 this.chartLabels
= {};
302 this.area
= this.computeArea_();
303 this.container
.style
.position
= "relative";
304 this.container
.style
.width
= this.width
+ "px";
306 // Set up a clipping area for the canvas (and the interaction canvas).
307 // This ensures that we don't overdraw.
308 var ctx
= this.dygraph_
.canvas_ctx_
;
310 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
313 ctx
= this.dygraph_
.hidden_ctx_
;
315 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
319 DygraphCanvasRenderer
.prototype.attr_
= function(x
) {
320 return this.dygraph_
.attr_(x
);
323 // Compute the box which the chart should be drawn in. This is the canvas's
324 // box, less space needed for axis and chart labels.
325 // TODO(danvk): this belongs in DygraphLayout.
326 DygraphCanvasRenderer
.prototype.computeArea_
= function() {
328 // TODO(danvk): per-axis setting.
332 if (this.attr_('drawYAxis')) {
333 area
.x
= this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize');
336 area
.w
= this.width
- area
.x
- this.attr_('rightGap');
337 area
.h
= this.height
;
338 if (this.attr_('drawXAxis')) {
339 if (this.attr_('xAxisHeight')) {
340 area
.h
-= this.attr_('xAxisHeight');
342 area
.h
-= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
346 // Shrink the drawing area to accomodate additional y-axes.
347 if (this.dygraph_
.numAxes() == 2) {
348 // TODO(danvk): per-axis setting.
349 area
.w
-= (this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize'));
350 } else if (this.dygraph_
.numAxes() > 2) {
351 this.dygraph_
.error("Only two y-axes are supported at this time. (Trying " +
352 "to use " + this.dygraph_
.numAxes() + ")");
355 // Add space for chart labels: title, xlabel and ylabel.
356 if (this.attr_('title')) {
357 area
.h
-= this.attr_('titleHeight');
358 area
.y
+= this.attr_('titleHeight');
360 if (this.attr_('xlabel')) {
361 area
.h
-= this.attr_('xLabelHeight');
363 if (this.attr_('ylabel')) {
364 // It would make sense to shift the chart here to make room for the y-axis
365 // label, but the default yAxisLabelWidth is large enough that this results
366 // in overly-padded charts. The y-axis label should fit fine. If it
367 // doesn't, the yAxisLabelWidth option can be increased.
373 DygraphCanvasRenderer
.prototype.clear
= function() {
375 // VML takes a while to start up, so we just poll every this.IEDelay
377 if (this.clearDelay
) {
378 this.clearDelay
.cancel();
379 this.clearDelay
= null;
381 var context
= this.elementContext
;
384 // TODO(danvk): this is broken, since MochiKit.Async is gone.
385 this.clearDelay
= MochiKit
.Async
.wait(this.IEDelay
);
386 this.clearDelay
.addCallback(bind(this.clear
, this));
391 var context
= this.elementContext
;
392 context
.clearRect(0, 0, this.width
, this.height
);
394 for (var i
= 0; i
< this.xlabels
.length
; i
++) {
395 var el
= this.xlabels
[i
];
396 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
398 for (var i
= 0; i
< this.ylabels
.length
; i
++) {
399 var el
= this.ylabels
[i
];
400 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
402 for (var i
= 0; i
< this.annotations
.length
; i
++) {
403 var el
= this.annotations
[i
];
404 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
406 for (var k
in this.chartLabels
) {
407 if (!this.chartLabels
.hasOwnProperty(k
)) continue;
408 var el
= this.chartLabels
[k
];
409 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
411 this.xlabels
= new Array();
412 this.ylabels
= new Array();
413 this.annotations
= new Array();
414 this.chartLabels
= {};
418 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
421 if (typeof(canvasName
) == 'undefined' || canvasName
== null)
422 canvas
= document
.createElement("canvas");
425 var context
= canvas
.getContext("2d");
428 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
429 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
430 if ((!ie
) || (ie
[1] < 6) || (opera
))
438 * @param { [String] } colors Array of color strings. Should have one entry for
439 * each series to be rendered.
441 DygraphCanvasRenderer
.prototype.setColors
= function(colors
) {
442 this.colorScheme_
= colors
;
446 * Draw an X/Y grid on top of the existing plot
448 DygraphCanvasRenderer
.prototype.render
= function() {
449 // Draw the new X/Y grid
. Lines appear crisper when pixels are rounded to
450 // half-integers. This prevents them from drawing in two rows/cols.
451 var ctx
= this.elementContext
;
452 function halfUp(x
){return Math
.round(x
)+0.5};
453 function halfDown(y
){return Math
.round(y
)-0.5};
455 if (this.attr_('underlayCallback')) {
456 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
457 // users who expect a deprecated form of this callback.
458 this.attr_('underlayCallback')(ctx
, this.area
, this.dygraph_
, this.dygraph_
);
461 if (this.attr_('drawYGrid')) {
462 var ticks
= this.layout
.yticks
;
464 ctx
.strokeStyle
= this.attr_('gridLineColor');
465 ctx
.lineWidth
= this.attr_('gridLineWidth');
466 for (var i
= 0; i
< ticks
.length
; i
++) {
467 // TODO(danvk): allow secondary axes to draw a grid, too.
468 if (ticks
[i
][0] != 0) continue;
469 var x
= halfUp(this.area
.x
);
470 var y
= halfDown(this.area
.y
+ ticks
[i
][1] * this.area
.h
);
473 ctx
.lineTo(x
+ this.area
.w
, y
);
479 if (this.attr_('drawXGrid')) {
480 var ticks
= this.layout
.xticks
;
482 ctx
.strokeStyle
= this.attr_('gridLineColor');
483 ctx
.lineWidth
= this.attr_('gridLineWidth');
484 for (var i
=0; i
<ticks
.length
; i
++) {
485 var x
= halfUp(this.area
.x
+ ticks
[i
][0] * this.area
.w
);
486 var y
= halfDown(this.area
.y
+ this.area
.h
);
489 ctx
.lineTo(x
, this.area
.y
);
495 // Do the ordinary rendering, as before
496 this._renderLineChart();
498 this._renderChartLabels();
499 this._renderAnnotations();
503 DygraphCanvasRenderer
.prototype._renderAxis
= function() {
504 if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) return;
506 // Round pixels to half-integer boundaries for crisper drawing.
507 function halfUp(x
){return Math
.round(x
)+0.5};
508 function halfDown(y
){return Math
.round(y
)-0.5};
510 var context
= this.elementContext
;
513 position
: "absolute",
514 fontSize
: this.attr_('axisLabelFontSize') + "px",
516 color
: this.attr_('axisLabelColor'),
517 width
: this.attr_('axisLabelWidth') + "px",
518 // height: this.attr_('axisLabelFontSize') + 2 + "px",
521 var makeDiv
= function(txt
, axis
) {
522 var div
= document
.createElement("div");
523 for (var name
in labelStyle
) {
524 if (labelStyle
.hasOwnProperty(name
)) {
525 div
.style
[name
] = labelStyle
[name
];
528 var inner_div
= document
.createElement("div");
529 // TODO(danvk): separate class for secondary y-axis
530 inner_div
.className
= 'dygraph-axis-label dygraph-axis-label-' + axis
;
531 inner_div
.appendChild(document
.createTextNode(txt
));
532 div
.appendChild(inner_div
);
538 context
.strokeStyle
= this.attr_('axisLineColor');
539 context
.lineWidth
= this.attr_('axisLineWidth');
541 if (this.attr_('drawYAxis')) {
542 if (this.layout
.yticks
&& this.layout
.yticks
.length
> 0) {
543 for (var i
= 0; i
< this.layout
.yticks
.length
; i
++) {
544 var tick
= this.layout
.yticks
[i
];
545 if (typeof(tick
) == "function") return;
548 if (tick
[0] == 1) { // right-side y-axis
549 x
= this.area
.x
+ this.area
.w
;
552 var y
= this.area
.y
+ tick
[1] * this.area
.h
;
554 context
.moveTo(halfUp(x
), halfDown(y
));
555 context
.lineTo(halfUp(x
- sgn
* this.attr_('axisTickSize')), halfDown(y
));
559 var label
= makeDiv(tick
[2], 'y');
560 var top
= (y
- this.attr_('axisLabelFontSize') / 2);
561 if (top
< 0) top
= 0;
563 if (top
+ this.attr_('axisLabelFontSize') + 3 > this.height
) {
564 label
.style
.bottom
= "0px";
566 label
.style
.top
= top
+ "px";
569 label
.style
.left
= (this.area
.x
- this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
570 label
.style
.textAlign
= "right";
571 } else if (tick
[0] == 1) {
572 label
.style
.left
= (this.area
.x
+ this.area
.w
+
573 this.attr_('axisTickSize')) + "px";
574 label
.style
.textAlign
= "left";
576 label
.style
.width
= this.attr_('yAxisLabelWidth') + "px";
577 this.container
.appendChild(label
);
578 this.ylabels
.push(label
);
581 // The lowest tick on the y-axis often overlaps with the leftmost
582 // tick on the x-axis. Shift the bottom tick up a little bit to
583 // compensate if necessary.
584 var bottomTick
= this.ylabels
[0];
585 var fontSize
= this.attr_('axisLabelFontSize');
586 var bottom
= parseInt(bottomTick
.style
.top
) + fontSize
;
587 if (bottom
> this.height
- fontSize
) {
588 bottomTick
.style
.top
= (parseInt(bottomTick
.style
.top
) -
589 fontSize
/ 2) + "px";
593 // draw a vertical line on the left to separate the chart from the labels.
595 context
.moveTo(halfUp(this.area
.x
), halfDown(this.area
.y
));
596 context
.lineTo(halfUp(this.area
.x
), halfDown(this.area
.y
+ this.area
.h
));
600 // if there's a secondary y-axis, draw a vertical line for that, too.
601 if (this.dygraph_
.numAxes() == 2) {
603 context
.moveTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
));
604 context
.lineTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
+ this.area
.h
));
610 if (this.attr_('drawXAxis')) {
611 if (this.layout
.xticks
) {
612 for (var i
= 0; i
< this.layout
.xticks
.length
; i
++) {
613 var tick
= this.layout
.xticks
[i
];
614 if (typeof(dataset
) == "function") return;
616 var x
= this.area
.x
+ tick
[0] * this.area
.w
;
617 var y
= this.area
.y
+ this.area
.h
;
619 context
.moveTo(halfUp(x
), halfDown(y
));
620 context
.lineTo(halfUp(x
), halfDown(y
+ this.attr_('axisTickSize')));
624 var label
= makeDiv(tick
[1], 'x');
625 label
.style
.textAlign
= "center";
626 label
.style
.top
= (y
+ this.attr_('axisTickSize')) + 'px';
628 var left
= (x
- this.attr_('axisLabelWidth')/2);
629 if (left
+ this.attr_('axisLabelWidth') > this.width
) {
630 left
= this.width
- this.attr_('xAxisLabelWidth');
631 label
.style
.textAlign
= "right";
635 label
.style
.textAlign
= "left";
638 label
.style
.left
= left
+ "px";
639 label
.style
.width
= this.attr_('xAxisLabelWidth') + "px";
640 this.container
.appendChild(label
);
641 this.xlabels
.push(label
);
646 context
.moveTo(halfUp(this.area
.x
), halfDown(this.area
.y
+ this.area
.h
));
647 context
.lineTo(halfUp(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
+ this.area
.h
));
656 DygraphCanvasRenderer
.prototype._renderChartLabels
= function() {
657 // Generate divs for the chart title, xlabel and ylabel.
658 // Space for these divs has already been taken away from the charting area in
659 // the DygraphCanvasRenderer constructor.
660 if (this.attr_('title')) {
661 var div
= document
.createElement("div");
662 div
.style
.position
= 'absolute';
663 div
.style
.top
= '0px';
664 div
.style
.left
= this.area
.x
+ 'px';
665 div
.style
.width
= this.area
.w
+ 'px';
666 div
.style
.height
= this.attr_('titleHeight') + 'px';
667 div
.style
.textAlign
= 'center';
668 div
.style
.fontSize
= (this.attr_('titleHeight') - 8) + 'px';
669 div
.style
.fontWeight
= 'bold';
670 var class_div
= document
.createElement("div");
671 class_div
.className
= 'dygraph-label dygraph-title';
672 class_div
.innerHTML
= this.attr_('title');
673 div
.appendChild(class_div
);
674 this.container
.appendChild(div
);
675 this.chartLabels
.title
= div
;
678 if (this.attr_('xlabel')) {
679 var div
= document
.createElement("div");
680 div
.style
.position
= 'absolute';
681 div
.style
.bottom
= 0; // TODO(danvk): this is lazy. Calculate style.top.
682 div
.style
.left
= this.area
.x
+ 'px';
683 div
.style
.width
= this.area
.w
+ 'px';
684 div
.style
.height
= this.attr_('xLabelHeight') + 'px';
685 div
.style
.textAlign
= 'center';
686 div
.style
.fontSize
= (this.attr_('xLabelHeight') - 2) + 'px';
688 var class_div
= document
.createElement("div");
689 class_div
.className
= 'dygraph-label dygraph-xlabel';
690 class_div
.innerHTML
= this.attr_('xlabel');
691 div
.appendChild(class_div
);
692 this.container
.appendChild(div
);
693 this.chartLabels
.xlabel
= div
;
696 if (this.attr_('ylabel')) {
700 width
: this.attr_('yLabelWidth'),
703 // TODO(danvk): is this outer div actually necessary?
704 var div
= document
.createElement("div");
705 div
.style
.position
= 'absolute';
706 div
.style
.left
= box
.left
;
707 div
.style
.top
= box
.top
+ 'px';
708 div
.style
.width
= box
.width
+ 'px';
709 div
.style
.height
= box
.height
+ 'px';
710 div
.style
.fontSize
= (this.attr_('yLabelWidth') - 2) + 'px';
712 var inner_div
= document
.createElement("div");
713 inner_div
.style
.position
= 'absolute';
714 inner_div
.style
.width
= box
.height
+ 'px';
715 inner_div
.style
.height
= box
.width
+ 'px';
716 inner_div
.style
.top
= (box
.height
/ 2 - box.width / 2) + 'px';
717 inner_div
.style
.left
= (box
.width
/ 2 - box.height / 2) + 'px';
718 inner_div
.style
.textAlign
= 'center';
720 // CSS rotation is an HTML5 feature which is not standardized. Hence every
721 // browser has its own name for the CSS style.
722 inner_div
.style
.transform
= 'rotate(-90deg)'; // HTML5
723 inner_div
.style
.WebkitTransform
= 'rotate(-90deg)'; // Safari/Chrome
724 inner_div
.style
.MozTransform
= 'rotate(-90deg)'; // Firefox
725 inner_div
.style
.OTransform
= 'rotate(-90deg)'; // Opera
726 inner_div
.style
.msTransform
= 'rotate(-90deg)'; // IE9
728 if (typeof(document
.documentMode
) !== 'undefined' &&
729 document
.documentMode
< 9) {
730 // We're dealing w/ an old version of IE
, so we have to rotate the text
731 // using a BasicImage transform. This uses a different origin of rotation
732 // than HTML5 rotation (top left of div vs. its center).
733 inner_div
.style
.filter
=
734 'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)';
735 inner_div
.style
.left
= '0px';
736 inner_div
.style
.top
= '0px';
739 var class_div
= document
.createElement("div");
740 class_div
.className
= 'dygraph-label dygraph-ylabel';
741 class_div
.innerHTML
= this.attr_('ylabel');
743 inner_div
.appendChild(class_div
);
744 div
.appendChild(inner_div
);
745 this.container
.appendChild(div
);
746 this.chartLabels
.ylabel
= div
;
751 DygraphCanvasRenderer
.prototype._renderAnnotations
= function() {
752 var annotationStyle
= {
753 "position": "absolute",
754 "fontSize": this.attr_('axisLabelFontSize') + "px",
759 var bindEvt
= function(eventName
, classEventName
, p
, self
) {
761 var a
= p
.annotation
;
762 if (a
.hasOwnProperty(eventName
)) {
763 a
[eventName
](a
, p
, self
.dygraph_
, e
);
764 } else if (self
.dygraph_
.attr_(classEventName
)) {
765 self
.dygraph_
.attr_(classEventName
)(a
, p
, self
.dygraph_
,e
);
770 // Get a list of point with annotations.
771 var points
= this.layout
.annotated_points
;
772 for (var i
= 0; i
< points
.length
; i
++) {
774 if (p
.canvasx
< this.area
.x
|| p
.canvasx
> this.area
.x
+ this.area
.w
) {
778 var a
= p
.annotation
;
780 if (a
.hasOwnProperty("tickHeight")) {
781 tick_height
= a
.tickHeight
;
784 var div
= document
.createElement("div");
785 for (var name
in annotationStyle
) {
786 if (annotationStyle
.hasOwnProperty(name
)) {
787 div
.style
[name
] = annotationStyle
[name
];
790 if (!a
.hasOwnProperty('icon')) {
791 div
.className
= "dygraphDefaultAnnotation";
793 if (a
.hasOwnProperty('cssClass')) {
794 div
.className
+= " " + a
.cssClass
;
797 var width
= a
.hasOwnProperty('width') ? a
.width
: 16;
798 var height
= a
.hasOwnProperty('height') ? a
.height
: 16;
799 if (a
.hasOwnProperty('icon')) {
800 var img
= document
.createElement("img");
804 div
.appendChild(img
);
805 } else if (p
.annotation
.hasOwnProperty('shortText')) {
806 div
.appendChild(document
.createTextNode(p
.annotation
.shortText
));
808 div
.style
.left
= (p
.canvasx
- width
/ 2) + "px";
809 if (a
.attachAtBottom
) {
810 div
.style
.top
= (this.area
.h
- height
- tick_height
) + "px";
812 div
.style
.top
= (p
.canvasy
- height
- tick_height
) + "px";
814 div
.style
.width
= width
+ "px";
815 div
.style
.height
= height
+ "px";
816 div
.title
= p
.annotation
.text
;
817 div
.style
.color
= this.colors
[p
.name
];
818 div
.style
.borderColor
= this.colors
[p
.name
];
821 Dygraph
.addEvent(div
, 'click',
822 bindEvt('clickHandler', 'annotationClickHandler', p
, this));
823 Dygraph
.addEvent(div
, 'mouseover',
824 bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p
, this));
825 Dygraph
.addEvent(div
, 'mouseout',
826 bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p
, this));
827 Dygraph
.addEvent(div
, 'dblclick',
828 bindEvt('dblClickHandler', 'annotationDblClickHandler', p
, this));
830 this.container
.appendChild(div
);
831 this.annotations
.push(div
);
833 var ctx
= this.elementContext
;
834 ctx
.strokeStyle
= this.colors
[p
.name
];
836 if (!a
.attachAtBottom
) {
837 ctx
.moveTo(p
.canvasx
, p
.canvasy
);
838 ctx
.lineTo(p
.canvasx
, p
.canvasy
- 2 - tick_height
);
840 ctx
.moveTo(p
.canvasx
, this.area
.h
);
841 ctx
.lineTo(p
.canvasx
, this.area
.h
- 2 - tick_height
);
850 * Overrides the CanvasRenderer method to draw error bars
852 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
853 // TODO(danvk): use this.attr_ for many of these.
854 var context
= this.elementContext
;
855 var fillAlpha
= this.attr_('fillAlpha');
856 var errorBars
= this.attr_("errorBars") || this.attr_("customBars");
857 var fillGraph
= this.attr_("fillGraph");
858 var stackedGraph
= this.attr_("stackedGraph");
859 var stepPlot
= this.attr_("stepPlot");
862 for (var name
in this.layout
.datasets
) {
863 if (this.layout
.datasets
.hasOwnProperty(name
)) {
867 var setCount
= setNames
.length
;
869 // TODO(danvk): Move this mapping into Dygraph and get it out of here.
871 for (var i
= 0; i
< setCount
; i
++) {
872 this.colors
[setNames
[i
]] = this.colorScheme_
[i
% this.colorScheme_
.length
];
877 for (var i
= 0; i
< this.layout
.points
.length
; i
++) {
878 var point
= this.layout
.points
[i
];
879 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
880 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
887 this.dygraph_
.warn("Can't use fillGraph option with error bars");
890 for (var i
= 0; i
< setCount
; i
++) {
891 var setName
= setNames
[i
];
892 var axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
893 var color
= this.colors
[setName
];
895 // setup graphics context
899 var prevYs
= [-1, -1];
900 var yscale
= axis
.yscale
;
901 // should be same color as the lines but only 15% opaque.
902 var rgb
= new RGBColor(color
);
903 var err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
905 ctx
.fillStyle
= err_color
;
907 for (var j
= 0; j
< this.layout
.points
.length
; j
++) {
908 var point
= this.layout
.points
[j
];
909 if (point
.name
== setName
) {
910 if (!Dygraph
.isOK(point
.y
)) {
917 var newYs
= [ prevY
- point
.errorPlus
* yscale
,
918 prevY
+ point
.errorMinus
* yscale
];
921 var newYs
= [ point
.y
- point
.errorPlus
* yscale
,
922 point
.y
+ point
.errorMinus
* yscale
];
924 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
925 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
928 ctx
.moveTo(prevX
, newYs
[0]);
930 ctx
.moveTo(prevX
, prevYs
[0]);
932 ctx
.lineTo(point
.canvasx
, newYs
[0]);
933 ctx
.lineTo(point
.canvasx
, newYs
[1]);
935 ctx
.lineTo(prevX
, newYs
[1]);
937 ctx
.lineTo(prevX
, prevYs
[1]);
942 prevX
= point
.canvasx
;
947 } else if (fillGraph
) {
948 var baseline
= [] // for stacked graphs: baseline for filling
950 // process sets in reverse order (needed for stacked graphs)
951 for (var i
= setCount
- 1; i
>= 0; i
--) {
952 var setName
= setNames
[i
];
953 var color
= this.colors
[setName
];
954 var axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
955 var axisY
= 1.0 + axis
.minyval
* axis
.yscale
;
956 if (axisY
< 0.0) axisY
= 0.0;
957 else if (axisY
> 1.0) axisY
= 1.0;
958 axisY
= this.area
.h
* axisY
+ this.area
.y
;
960 // setup graphics context
963 var prevYs
= [-1, -1];
964 var yscale
= axis
.yscale
;
965 // should be same color as the lines but only 15% opaque.
966 var rgb
= new RGBColor(color
);
967 var err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
969 ctx
.fillStyle
= err_color
;
971 for (var j
= 0; j
< this.layout
.points
.length
; j
++) {
972 var point
= this.layout
.points
[j
];
973 if (point
.name
== setName
) {
974 if (!Dygraph
.isOK(point
.y
)) {
980 lastY
= baseline
[point
.canvasx
];
981 if (lastY
=== undefined
) lastY
= axisY
;
982 baseline
[point
.canvasx
] = point
.canvasy
;
983 newYs
= [ point
.canvasy
, lastY
];
985 newYs
= [ point
.canvasy
, axisY
];
988 ctx
.moveTo(prevX
, prevYs
[0]);
990 ctx
.lineTo(point
.canvasx
, prevYs
[0]);
992 ctx
.lineTo(point
.canvasx
, newYs
[0]);
994 ctx
.lineTo(point
.canvasx
, newYs
[1]);
995 ctx
.lineTo(prevX
, prevYs
[1]);
999 prevX
= point
.canvasx
;
1006 var isNullOrNaN
= function(x
) {
1007 return (x
=== null || isNaN(x
));
1010 for (var i
= 0; i
< setCount
; i
++) {
1011 var setName
= setNames
[i
];
1012 var color
= this.colors
[setName
];
1013 var strokeWidth
= this.dygraph_
.attr_("strokeWidth", setName
);
1015 // setup graphics context
1017 var point
= this.layout
.points
[0];
1018 var pointSize
= this.dygraph_
.attr_("pointSize", setName
);
1019 var prevX
= null, prevY
= null;
1020 var drawPoints
= this.dygraph_
.attr_("drawPoints", setName
);
1021 var points
= this.layout
.points
;
1022 for (var j
= 0; j
< points
.length
; j
++) {
1023 var point
= points
[j
];
1024 if (point
.name
== setName
) {
1025 if (isNullOrNaN(point
.canvasy
)) {
1026 if (stepPlot
&& prevX
!= null) {
1027 // Draw a horizontal line to the start of the missing data
1029 ctx
.strokeStyle
= color
;
1030 ctx
.lineWidth
= this.attr_('strokeWidth');
1031 ctx
.moveTo(prevX
, prevY
);
1032 ctx
.lineTo(point
.canvasx
, prevY
);
1035 // this will make us move to the next point, not draw a line to it.
1036 prevX
= prevY
= null;
1038 // A point is "isolated" if it is non-null but both the previous
1039 // and next points are null.
1040 var isIsolated
= (!prevX
&& (j
== points
.length
- 1 ||
1041 isNullOrNaN(points
[j
+1].canvasy
)));
1043 if (prevX
=== null) {
1044 prevX
= point
.canvasx
;
1045 prevY
= point
.canvasy
;
1047 // TODO(danvk): figure out why this conditional is necessary.
1050 ctx
.strokeStyle
= color
;
1051 ctx
.lineWidth
= strokeWidth
;
1052 ctx
.moveTo(prevX
, prevY
);
1054 ctx
.lineTo(point
.canvasx
, prevY
);
1056 prevX
= point
.canvasx
;
1057 prevY
= point
.canvasy
;
1058 ctx
.lineTo(prevX
, prevY
);
1063 if (drawPoints
|| isIsolated
) {
1065 ctx
.fillStyle
= color
;
1066 ctx
.arc(point
.canvasx
, point
.canvasy
, pointSize
,
1067 0, 2 * Math
.PI
, false);