1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
5 * @fileoverview Based on PlotKit, but modified to meet the needs of dygraphs.
6 * In particular, support for:
9 * - dygraphs attribute system
11 * High level overview of classes:
14 * This contains all the data to be charted.
15 * It uses data coordinates, but also records the chart range (in data
16 * coordinates) and hence is able to calculate percentage positions ('In
17 * this view, Point A lies 25% down the x-axis.')
18 * Two things that it does not do are:
19 * 1. Record pixel coordinates for anything.
20 * 2. (oddly) determine anything about the layout of chart elements.
21 * The naming is a vestige of Dygraph's original PlotKit roots.
23 * - DygraphCanvasRenderer
24 * This class determines the charting area (in pixel coordinates), maps the
25 * percentage coordinates in the DygraphLayout to pixels and draws them.
26 * It's also responsible for creating chart DOM elements, i.e. annotations,
27 * tick mark labels, the title and the x/y-axis labels.
31 * Creates a new DygraphLayout object.
32 * @param {Object} options Options for PlotKit.Layout
33 * @return {Object} The DygraphLayout object
35 DygraphLayout
= function(dygraph
, options
) {
36 this.dygraph_
= dygraph
;
37 this.options
= {}; // TODO(danvk): remove, use attr_ instead.
38 Dygraph
.update(this.options
, options
? options
: {});
39 this.datasets
= new Array();
40 this.annotations
= new Array();
43 DygraphLayout
.prototype.attr_
= function(name
) {
44 return this.dygraph_
.attr_(name
);
47 DygraphLayout
.prototype.addDataset
= function(setname
, set_xy
) {
48 this.datasets
[setname
] = set_xy
;
51 DygraphLayout
.prototype.setAnnotations
= function(ann
) {
52 // The Dygraph object's annotations aren't parsed. We parse them here and
53 // save a copy. If there is no parser, then the user must be using raw format.
54 this.annotations
= [];
55 var parse
= this.attr_('xValueParser') || function(x
) { return x
; };
56 for (var i
= 0; i
< ann
.length
; i
++) {
58 if (!ann
[i
].xval
&& !ann
[i
].x
) {
59 this.dygraph_
.error("Annotations must have an 'x' property");
63 !(ann
[i
].hasOwnProperty('width') &&
64 ann
[i
].hasOwnProperty('height'))) {
65 this.dygraph_
.error("Must set width and height when setting " +
66 "annotation.icon property");
69 Dygraph
.update(a
, ann
[i
]);
70 if (!a
.xval
) a
.xval
= parse(a
.x
);
71 this.annotations
.push(a
);
75 DygraphLayout
.prototype.evaluate
= function() {
76 this._evaluateLimits();
77 this._evaluateLineCharts();
78 this._evaluateLineTicks();
79 this._evaluateAnnotations();
82 DygraphLayout
.prototype._evaluateLimits
= function() {
83 this.minxval
= this.maxxval
= null;
84 if (this.options
.dateWindow
) {
85 this.minxval
= this.options
.dateWindow
[0];
86 this.maxxval
= this.options
.dateWindow
[1];
88 for (var name
in this.datasets
) {
89 if (!this.datasets
.hasOwnProperty(name
)) continue;
90 var series
= this.datasets
[name
];
91 if (series
.length
> 1) {
92 var x1
= series
[0][0];
93 if (!this.minxval
|| x1
< this.minxval
) this.minxval
= x1
;
95 var x2
= series
[series
.length
- 1][0];
96 if (!this.maxxval
|| x2
> this.maxxval
) this.maxxval
= x2
;
100 this.xrange
= this.maxxval
- this.minxval
;
101 this.xscale
= (this.xrange
!= 0 ? 1/this.xrange
: 1.0);
103 for (var i
= 0; i
< this.options
.yAxes
.length
; i
++) {
104 var axis
= this.options
.yAxes
[i
];
105 axis
.minyval
= axis
.computedValueRange
[0];
106 axis
.maxyval
= axis
.computedValueRange
[1];
107 axis
.yrange
= axis
.maxyval
- axis
.minyval
;
108 axis
.yscale
= (axis
.yrange
!= 0 ? 1.0 / axis
.yrange
: 1.0);
110 if (axis
.g
.attr_("logscale")) {
111 axis
.ylogrange
= Dygraph
.log10(axis
.maxyval
) - Dygraph
.log10(axis
.minyval
);
112 axis
.ylogscale
= (axis
.ylogrange
!= 0 ? 1.0 / axis
.ylogrange
: 1.0);
113 if (!isFinite(axis
.ylogrange
) || isNaN(axis
.ylogrange
)) {
114 axis
.g
.error('axis ' + i
+ ' of graph at ' + axis
.g
+
115 ' can\'t be displayed in log scale for range [' +
116 axis
.minyval
+ ' - ' + axis
.maxyval
+ ']');
122 DygraphLayout
.prototype._evaluateLineCharts
= function() {
124 this.points
= new Array();
125 for (var setName
in this.datasets
) {
126 if (!this.datasets
.hasOwnProperty(setName
)) continue;
128 var dataset
= this.datasets
[setName
];
129 var axis
= this.options
.yAxes
[this.options
.seriesToAxisMap
[setName
]];
131 for (var j
= 0; j
< dataset
.length
; j
++) {
132 var item
= dataset
[j
];
136 yval
= 1.0 - ((Dygraph
.log10(parseFloat(item
[1])) - Dygraph
.log10(axis
.minyval
)) * axis
.ylogscale
); // really should just be yscale.
138 yval
= 1.0 - ((parseFloat(item
[1]) - axis
.minyval
) * axis
.yscale
);
142 x
: ((parseFloat(item
[0]) - this.minxval
) * this.xscale
),
144 xval
: parseFloat(item
[0]),
145 yval
: parseFloat(item
[1]),
149 this.points
.push(point
);
154 DygraphLayout
.prototype._evaluateLineTicks
= function() {
155 this.xticks
= new Array();
156 for (var i
= 0; i
< this.options
.xTicks
.length
; i
++) {
157 var tick
= this.options
.xTicks
[i
];
158 var label
= tick
.label
;
159 var pos
= this.xscale
* (tick
.v
- this.minxval
);
160 if ((pos
>= 0.0) && (pos
<= 1.0)) {
161 this.xticks
.push([pos
, label
]);
165 this.yticks
= new Array();
166 for (var i
= 0; i
< this.options
.yAxes
.length
; i
++ ) {
167 var axis
= this.options
.yAxes
[i
];
168 for (var j
= 0; j
< axis
.ticks
.length
; j
++) {
169 var tick
= axis
.ticks
[j
];
170 var label
= tick
.label
;
171 var pos
= this.dygraph_
.toPercentYCoord(tick
.v
, i
);
172 if ((pos
>= 0.0) && (pos
<= 1.0)) {
173 this.yticks
.push([i
, pos
, label
]);
181 * Behaves the same way as PlotKit.Layout, but also copies the errors
184 DygraphLayout
.prototype.evaluateWithError
= function() {
186 if (!this.options
.errorBars
) return;
188 // Copy over the error terms
189 var i
= 0; // index in this.points
190 for (var setName
in this.datasets
) {
191 if (!this.datasets
.hasOwnProperty(setName
)) continue;
193 var dataset
= this.datasets
[setName
];
194 for (var j
= 0; j
< dataset
.length
; j
++, i
++) {
195 var item
= dataset
[j
];
196 var xv
= parseFloat(item
[0]);
197 var yv
= parseFloat(item
[1]);
199 if (xv
== this.points
[i
].xval
&&
200 yv
== this.points
[i
].yval
) {
201 this.points
[i
].errorMinus
= parseFloat(item
[2]);
202 this.points
[i
].errorPlus
= parseFloat(item
[3]);
208 DygraphLayout
.prototype._evaluateAnnotations
= function() {
209 // Add the annotations to the point to which they belong.
210 // Make a map from (setName, xval) to annotation for quick lookups.
211 var annotations
= {};
212 for (var i
= 0; i
< this.annotations
.length
; i
++) {
213 var a
= this.annotations
[i
];
214 annotations
[a
.xval
+ "," + a
.series
] = a
;
217 this.annotated_points
= [];
218 for (var i
= 0; i
< this.points
.length
; i
++) {
219 var p
= this.points
[i
];
220 var k
= p
.xval
+ "," + p
.name
;
221 if (k
in annotations
) {
222 p
.annotation
= annotations
[k
];
223 this.annotated_points
.push(p
);
229 * Convenience function to remove all the data sets from a graph
231 DygraphLayout
.prototype.removeAllDatasets
= function() {
232 delete this.datasets
;
233 this.datasets
= new Array();
237 * Change the values of various layout options
238 * @param {Object} new_options an associative array of new properties
240 DygraphLayout
.prototype.updateOptions
= function(new_options
) {
241 Dygraph
.update(this.options
, new_options
? new_options
: {});
245 * Return a copy of the point at the indicated index, with its yval unstacked.
246 * @param int index of point in layout_.points
248 DygraphLayout
.prototype.unstackPointAtIndex
= function(idx
) {
249 var point
= this.points
[idx
];
251 // Clone the point since we modify it
252 var unstackedPoint
= {};
253 for (var i
in point
) {
254 unstackedPoint
[i
] = point
[i
];
257 if (!this.attr_("stackedGraph")) {
258 return unstackedPoint
;
261 // The unstacked yval is equal to the current yval minus the yval of the
262 // next point at the same xval.
263 for (var i
= idx
+1; i
< this.points
.length
; i
++) {
264 if (this.points
[i
].xval
== point
.xval
) {
265 unstackedPoint
.yval
-= this.points
[i
].yval
;
270 return unstackedPoint
;
273 // Subclass PlotKit.CanvasRenderer to add:
274 // 1. X/Y grid overlay
275 // 2. Ability to draw error bars (if required)
278 * Sets some PlotKit.CanvasRenderer options
279 * @param {Object} element The canvas to attach to
280 * @param {Object} elementContext The 2d context of the canvas (injected so it
281 * can be mocked for testing.)
282 * @param {Layout} layout The DygraphLayout object for this graph.
283 * @param {Object} options Options to pass on to CanvasRenderer
285 DygraphCanvasRenderer
= function(dygraph
, element
, elementContext
, layout
,
287 // TODO(danvk): remove options, just use dygraph.attr_.
288 this.dygraph_
= dygraph
;
295 "axisLineColor": "black",
296 "axisLineWidth": 0.5,
298 "axisLabelColor": "black",
299 "axisLabelFont": "Arial",
300 "axisLabelFontSize": 9,
301 "axisLabelWidth": 50,
304 "gridLineColor": "rgb(128,128,128)",
306 "underlayCallback": null
308 Dygraph
.update(this.options
, options
);
310 this.layout
= layout
;
311 this.element
= element
;
312 this.elementContext
= elementContext
;
313 this.container
= this.element
.parentNode
;
315 this.height
= this.element
.height
;
316 this.width
= this.element
.width
;
318 // --- check whether everything is ok before we return
319 if (!this.isIE
&& !(DygraphCanvasRenderer
.isSupported(this.element
)))
320 throw "Canvas is not supported.";
323 this.xlabels
= new Array();
324 this.ylabels
= new Array();
325 this.annotations
= new Array();
326 this.chartLabels
= {};
328 // TODO(danvk): consider all axes in this computation.
330 // TODO(danvk): per-axis setting.
331 x
: this.options
.yAxisLabelWidth
+ 2 * this.options
.axisTickSize
,
334 this.area
.w
= this.width
- this.area
.x
- this.options
.rightGap
;
335 this.area
.h
= this.height
- this.options
.axisLabelFontSize
-
336 2 * this.options
.axisTickSize
;
338 // Shrink the drawing area to accomodate additional y-axes.
339 if (this.dygraph_
.numAxes() == 2) {
340 // TODO(danvk): per-axis setting.
341 this.area
.w
-= (this.options
.yAxisLabelWidth
+ 2 * this.options
.axisTickSize
);
342 } else if (this.dygraph_
.numAxes() > 2) {
343 this.dygraph_
.error("Only two y-axes are supported at this time. (Trying " +
344 "to use " + this.dygraph_
.numAxes() + ")");
347 // Add space for chart labels: title, xlabel and ylabel.
348 if (this.attr_('title')) {
349 this.area
.h
-= this.attr_('titleHeight');
350 this.area
.y
+= this.attr_('titleHeight');
352 if (this.attr_('xlabel')) {
353 this.area
.h
-= this.attr_('xLabelHeight');
355 if (this.attr_('ylabel')) {
356 // It would make sense to shift the chart here to make room for the y-axis
357 // label, but the default yAxisLabelWidth is large enough that this results
358 // in overly-padded charts. The y-axis label should fit fine. If it
359 // doesn't, the yAxisLabelWidth option can be increased.
362 this.container
.style
.position
= "relative";
363 this.container
.style
.width
= this.width
+ "px";
365 // Set up a clipping area for the canvas (and the interaction canvas).
366 // This ensures that we don't overdraw.
367 var ctx
= this.dygraph_
.canvas_ctx_
;
369 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
372 ctx
= this.dygraph_
.hidden_ctx_
;
374 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
378 DygraphCanvasRenderer
.prototype.attr_
= function(x
) {
379 return this.dygraph_
.attr_(x
);
382 DygraphCanvasRenderer
.prototype.clear
= function() {
384 // VML takes a while to start up, so we just poll every this.IEDelay
386 if (this.clearDelay
) {
387 this.clearDelay
.cancel();
388 this.clearDelay
= null;
390 var context
= this.elementContext
;
393 // TODO(danvk): this is broken, since MochiKit.Async is gone.
394 this.clearDelay
= MochiKit
.Async
.wait(this.IEDelay
);
395 this.clearDelay
.addCallback(bind(this.clear
, this));
400 var context
= this.elementContext
;
401 context
.clearRect(0, 0, this.width
, this.height
);
403 for (var i
= 0; i
< this.xlabels
.length
; i
++) {
404 var el
= this.xlabels
[i
];
405 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
407 for (var i
= 0; i
< this.ylabels
.length
; i
++) {
408 var el
= this.ylabels
[i
];
409 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
411 for (var i
= 0; i
< this.annotations
.length
; i
++) {
412 var el
= this.annotations
[i
];
413 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
415 for (var k
in this.chartLabels
) {
416 if (!this.chartLabels
.hasOwnProperty(k
)) continue;
417 var el
= this.chartLabels
[k
];
418 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
420 this.xlabels
= new Array();
421 this.ylabels
= new Array();
422 this.annotations
= new Array();
423 this.chartLabels
= {};
427 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
430 if (typeof(canvasName
) == 'undefined' || canvasName
== null)
431 canvas
= document
.createElement("canvas");
434 var context
= canvas
.getContext("2d");
437 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
438 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
439 if ((!ie
) || (ie
[1] < 6) || (opera
))
447 * Draw an X/Y grid on top of the existing plot
449 DygraphCanvasRenderer
.prototype.render
= function() {
450 // Draw the new X/Y grid
. Lines appear crisper when pixels are rounded to
451 // half-integers. This prevents them from drawing in two rows/cols.
452 var ctx
= this.elementContext
;
453 function halfUp(x
){return Math
.round(x
)+0.5};
454 function halfDown(y
){return Math
.round(y
)-0.5};
456 if (this.options
.underlayCallback
) {
457 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
458 // users who expect a deprecated form of this callback.
459 this.options
.underlayCallback(ctx
, this.area
, this.dygraph_
, this.dygraph_
);
462 if (this.options
.drawYGrid
) {
463 var ticks
= this.layout
.yticks
;
465 ctx
.strokeStyle
= this.options
.gridLineColor
;
466 ctx
.lineWidth
= this.options
.axisLineWidth
;
467 for (var i
= 0; i
< ticks
.length
; i
++) {
468 // TODO(danvk): allow secondary axes to draw a grid, too.
469 if (ticks
[i
][0] != 0) continue;
470 var x
= halfUp(this.area
.x
);
471 var y
= halfDown(this.area
.y
+ ticks
[i
][1] * this.area
.h
);
474 ctx
.lineTo(x
+ this.area
.w
, y
);
480 if (this.options
.drawXGrid
) {
481 var ticks
= this.layout
.xticks
;
483 ctx
.strokeStyle
= this.options
.gridLineColor
;
484 ctx
.lineWidth
= this.options
.axisLineWidth
;
485 for (var i
=0; i
<ticks
.length
; i
++) {
486 var x
= halfUp(this.area
.x
+ ticks
[i
][0] * this.area
.w
);
487 var y
= halfDown(this.area
.y
+ this.area
.h
);
490 ctx
.lineTo(x
, this.area
.y
);
496 // Do the ordinary rendering, as before
497 this._renderLineChart();
499 this._renderChartLabels();
500 this._renderAnnotations();
504 DygraphCanvasRenderer
.prototype._renderAxis
= function() {
505 if (!this.options
.drawXAxis
&& !this.options
.drawYAxis
)
508 // Round pixels to half-integer boundaries for crisper drawing.
509 function halfUp(x
){return Math
.round(x
)+0.5};
510 function halfDown(y
){return Math
.round(y
)-0.5};
512 var context
= this.elementContext
;
515 "position": "absolute",
516 "fontSize": this.options
.axisLabelFontSize
+ "px",
518 "color": this.options
.axisLabelColor
,
519 "width": this.options
.axisLabelWidth
+ "px",
522 var makeDiv
= function(txt
) {
523 var div
= document
.createElement("div");
524 for (var name
in labelStyle
) {
525 if (labelStyle
.hasOwnProperty(name
)) {
526 div
.style
[name
] = labelStyle
[name
];
529 div
.appendChild(document
.createTextNode(txt
));
535 context
.strokeStyle
= this.options
.axisLineColor
;
536 context
.lineWidth
= this.options
.axisLineWidth
;
538 if (this.options
.drawYAxis
) {
539 if (this.layout
.yticks
&& this.layout
.yticks
.length
> 0) {
540 for (var i
= 0; i
< this.layout
.yticks
.length
; i
++) {
541 var tick
= this.layout
.yticks
[i
];
542 if (typeof(tick
) == "function") return;
545 if (tick
[0] == 1) { // right-side y-axis
546 x
= this.area
.x
+ this.area
.w
;
549 var y
= this.area
.y
+ tick
[1] * this.area
.h
;
551 context
.moveTo(halfUp(x
), halfDown(y
));
552 context
.lineTo(halfUp(x
- sgn
* this.options
.axisTickSize
), halfDown(y
));
556 var label
= makeDiv(tick
[2]);
557 var top
= (y
- this.options
.axisLabelFontSize
/ 2);
558 if (top
< 0) top
= 0;
560 if (top
+ this.options
.axisLabelFontSize
+ 3 > this.height
) {
561 label
.style
.bottom
= "0px";
563 label
.style
.top
= top
+ "px";
566 label
.style
.left
= (this.area
.x
- this.options
.yAxisLabelWidth
- this.options
.axisTickSize
) + "px";
567 label
.style
.textAlign
= "right";
568 } else if (tick
[0] == 1) {
569 label
.style
.left
= (this.area
.x
+ this.area
.w
+
570 this.options
.axisTickSize
) + "px";
571 label
.style
.textAlign
= "left";
573 label
.style
.width
= this.options
.yAxisLabelWidth
+ "px";
574 this.container
.appendChild(label
);
575 this.ylabels
.push(label
);
578 // The lowest tick on the y-axis often overlaps with the leftmost
579 // tick on the x-axis. Shift the bottom tick up a little bit to
580 // compensate if necessary.
581 var bottomTick
= this.ylabels
[0];
582 var fontSize
= this.options
.axisLabelFontSize
;
583 var bottom
= parseInt(bottomTick
.style
.top
) + fontSize
;
584 if (bottom
> this.height
- fontSize
) {
585 bottomTick
.style
.top
= (parseInt(bottomTick
.style
.top
) -
586 fontSize
/ 2) + "px";
590 // draw a vertical line on the left to separate the chart from the labels.
592 context
.moveTo(halfUp(this.area
.x
), halfDown(this.area
.y
));
593 context
.lineTo(halfUp(this.area
.x
), halfDown(this.area
.y
+ this.area
.h
));
597 // if there's a secondary y-axis, draw a vertical line for that, too.
598 if (this.dygraph_
.numAxes() == 2) {
600 context
.moveTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
));
601 context
.lineTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
+ this.area
.h
));
607 if (this.options
.drawXAxis
) {
608 if (this.layout
.xticks
) {
609 for (var i
= 0; i
< this.layout
.xticks
.length
; i
++) {
610 var tick
= this.layout
.xticks
[i
];
611 if (typeof(dataset
) == "function") return;
613 var x
= this.area
.x
+ tick
[0] * this.area
.w
;
614 var y
= this.area
.y
+ this.area
.h
;
616 context
.moveTo(halfUp(x
), halfDown(y
));
617 context
.lineTo(halfUp(x
), halfDown(y
+ this.options
.axisTickSize
));
621 var label
= makeDiv(tick
[1]);
622 label
.style
.textAlign
= "center";
623 label
.style
.top
= (y
+ this.options
.axisTickSize
) + 'px';
625 var left
= (x
- this.options
.axisLabelWidth
/2);
626 if (left
+ this.options
.axisLabelWidth
> this.width
) {
627 left
= this.width
- this.options
.xAxisLabelWidth
;
628 label
.style
.textAlign
= "right";
632 label
.style
.textAlign
= "left";
635 label
.style
.left
= left
+ "px";
636 label
.style
.width
= this.options
.xAxisLabelWidth
+ "px";
637 this.container
.appendChild(label
);
638 this.xlabels
.push(label
);
643 context
.moveTo(halfUp(this.area
.x
), halfDown(this.area
.y
+ this.area
.h
));
644 context
.lineTo(halfUp(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
+ this.area
.h
));
653 DygraphCanvasRenderer
.prototype._renderChartLabels
= function() {
654 // Generate divs for the chart title, xlabel and ylabel.
655 // Space for these divs has already been taken away from the charting area in
656 // the DygraphCanvasRenderer constructor.
657 if (this.attr_('title')) {
658 var div
= document
.createElement("div");
659 div
.style
.position
= 'absolute';
660 div
.style
.top
= '0px';
661 div
.style
.left
= this.area
.x
+ 'px';
662 div
.style
.width
= this.area
.w
+ 'px';
663 div
.style
.height
= this.attr_('titleHeight') + 'px';
664 div
.style
.textAlign
= 'center';
665 div
.style
.fontSize
= (this.attr_('titleHeight') - 8) + 'px';
666 div
.style
.fontWeight
= 'bold';
667 var class_div
= document
.createElement("div");
668 class_div
.className
= 'dygraph-label dygraph-title';
669 class_div
.innerHTML
= this.attr_('title');
670 div
.appendChild(class_div
);
671 this.container
.appendChild(div
);
672 this.chartLabels
.title
= div
;
675 if (this.attr_('xlabel')) {
676 var div
= document
.createElement("div");
677 div
.style
.position
= 'absolute';
678 div
.style
.bottom
= 0; // TODO(danvk): this is lazy. Calculate style.top.
679 div
.style
.left
= this.area
.x
+ 'px';
680 div
.style
.width
= this.area
.w
+ 'px';
681 div
.style
.height
= this.attr_('xLabelHeight') + 'px';
682 div
.style
.textAlign
= 'center';
683 div
.style
.fontSize
= (this.attr_('xLabelHeight') - 2) + 'px';
685 var class_div
= document
.createElement("div");
686 class_div
.className
= 'dygraph-label dygraph-xlabel';
687 class_div
.innerHTML
= this.attr_('xlabel');
688 div
.appendChild(class_div
);
689 this.container
.appendChild(div
);
690 this.chartLabels
.xlabel
= div
;
693 if (this.attr_('ylabel')) {
697 width
: this.attr_('yLabelWidth'),
700 // TODO(danvk): is this outer div actually necessary?
701 var div
= document
.createElement("div");
702 div
.style
.position
= 'absolute';
703 div
.style
.left
= box
.left
;
704 div
.style
.top
= box
.top
+ 'px';
705 div
.style
.width
= box
.width
+ 'px';
706 div
.style
.height
= box
.height
+ 'px';
707 div
.style
.fontSize
= (this.attr_('yLabelWidth') - 2) + 'px';
709 var inner_div
= document
.createElement("div");
710 inner_div
.style
.position
= 'absolute';
711 inner_div
.style
.width
= box
.height
+ 'px';
712 inner_div
.style
.height
= box
.width
+ 'px';
713 inner_div
.style
.top
= (box
.height
/ 2 - box.width / 2) + 'px';
714 inner_div
.style
.left
= (box
.width
/ 2 - box.height / 2) + 'px';
715 inner_div
.style
.textAlign
= 'center';
717 // CSS rotation is an HTML5 feature which is not standardized. Hence every
718 // browser has its own name for the CSS style.
719 inner_div
.style
.transform
= 'rotate(-90deg)'; // HTML5
720 inner_div
.style
.WebkitTransform
= 'rotate(-90deg)'; // Safari/Chrome
721 inner_div
.style
.MozTransform
= 'rotate(-90deg)'; // Firefox
722 inner_div
.style
.OTransform
= 'rotate(-90deg)'; // Opera
723 inner_div
.style
.msTransform
= 'rotate(-90deg)'; // IE9
725 if (typeof(document
.documentMode
) !== 'undefined' &&
726 document
.documentMode
< 9) {
727 // We're dealing w/ an old version of IE
, so we have to rotate the text
728 // using a BasicImage transform. This uses a different origin of rotation
729 // than HTML5 rotation (top left of div vs. its center).
730 inner_div
.style
.filter
=
731 'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)';
732 inner_div
.style
.left
= '0px';
733 inner_div
.style
.top
= '0px';
736 var class_div
= document
.createElement("div");
737 class_div
.className
= 'dygraph-label dygraph-ylabel';
738 class_div
.innerHTML
= this.attr_('ylabel');
740 inner_div
.appendChild(class_div
);
741 div
.appendChild(inner_div
);
742 this.container
.appendChild(div
);
743 this.chartLabels
.ylabel
= div
;
748 DygraphCanvasRenderer
.prototype._renderAnnotations
= function() {
749 var annotationStyle
= {
750 "position": "absolute",
751 "fontSize": this.options
.axisLabelFontSize
+ "px",
756 var bindEvt
= function(eventName
, classEventName
, p
, self
) {
758 var a
= p
.annotation
;
759 if (a
.hasOwnProperty(eventName
)) {
760 a
[eventName
](a
, p
, self
.dygraph_
, e
);
761 } else if (self
.dygraph_
.attr_(classEventName
)) {
762 self
.dygraph_
.attr_(classEventName
)(a
, p
, self
.dygraph_
,e
);
767 // Get a list of point with annotations.
768 var points
= this.layout
.annotated_points
;
769 for (var i
= 0; i
< points
.length
; i
++) {
771 if (p
.canvasx
< this.area
.x
|| p
.canvasx
> this.area
.x
+ this.area
.w
) {
775 var a
= p
.annotation
;
777 if (a
.hasOwnProperty("tickHeight")) {
778 tick_height
= a
.tickHeight
;
781 var div
= document
.createElement("div");
782 for (var name
in annotationStyle
) {
783 if (annotationStyle
.hasOwnProperty(name
)) {
784 div
.style
[name
] = annotationStyle
[name
];
787 if (!a
.hasOwnProperty('icon')) {
788 div
.className
= "dygraphDefaultAnnotation";
790 if (a
.hasOwnProperty('cssClass')) {
791 div
.className
+= " " + a
.cssClass
;
794 var width
= a
.hasOwnProperty('width') ? a
.width
: 16;
795 var height
= a
.hasOwnProperty('height') ? a
.height
: 16;
796 if (a
.hasOwnProperty('icon')) {
797 var img
= document
.createElement("img");
801 div
.appendChild(img
);
802 } else if (p
.annotation
.hasOwnProperty('shortText')) {
803 div
.appendChild(document
.createTextNode(p
.annotation
.shortText
));
805 div
.style
.left
= (p
.canvasx
- width
/ 2) + "px";
806 if (a
.attachAtBottom
) {
807 div
.style
.top
= (this.area
.h
- height
- tick_height
) + "px";
809 div
.style
.top
= (p
.canvasy
- height
- tick_height
) + "px";
811 div
.style
.width
= width
+ "px";
812 div
.style
.height
= height
+ "px";
813 div
.title
= p
.annotation
.text
;
814 div
.style
.color
= this.colors
[p
.name
];
815 div
.style
.borderColor
= this.colors
[p
.name
];
818 Dygraph
.addEvent(div
, 'click',
819 bindEvt('clickHandler', 'annotationClickHandler', p
, this));
820 Dygraph
.addEvent(div
, 'mouseover',
821 bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p
, this));
822 Dygraph
.addEvent(div
, 'mouseout',
823 bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p
, this));
824 Dygraph
.addEvent(div
, 'dblclick',
825 bindEvt('dblClickHandler', 'annotationDblClickHandler', p
, this));
827 this.container
.appendChild(div
);
828 this.annotations
.push(div
);
830 var ctx
= this.elementContext
;
831 ctx
.strokeStyle
= this.colors
[p
.name
];
833 if (!a
.attachAtBottom
) {
834 ctx
.moveTo(p
.canvasx
, p
.canvasy
);
835 ctx
.lineTo(p
.canvasx
, p
.canvasy
- 2 - tick_height
);
837 ctx
.moveTo(p
.canvasx
, this.area
.h
);
838 ctx
.lineTo(p
.canvasx
, this.area
.h
- 2 - tick_height
);
847 * Overrides the CanvasRenderer method to draw error bars
849 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
850 // TODO(danvk): use this.attr_ for many of these.
851 var context
= this.elementContext
;
852 var colorCount
= this.options
.colorScheme
.length
;
853 var colorScheme
= this.options
.colorScheme
;
854 var fillAlpha
= this.options
.fillAlpha
;
855 var errorBars
= this.layout
.options
.errorBars
;
856 var fillGraph
= this.attr_("fillGraph");
857 var stackedGraph
= this.layout
.options
.stackedGraph
;
858 var stepPlot
= this.layout
.options
.stepPlot
;
861 for (var name
in this.layout
.datasets
) {
862 if (this.layout
.datasets
.hasOwnProperty(name
)) {
866 var setCount
= setNames
.length
;
869 for (var i
= 0; i
< setCount
; i
++) {
870 this.colors
[setNames
[i
]] = colorScheme
[i
% colorCount
];
875 for (var i
= 0; i
< this.layout
.points
.length
; i
++) {
876 var point
= this.layout
.points
[i
];
877 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
878 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
885 this.dygraph_
.warn("Can't use fillGraph option with error bars");
888 for (var i
= 0; i
< setCount
; i
++) {
889 var setName
= setNames
[i
];
890 var axis
= this.layout
.options
.yAxes
[
891 this.layout
.options
.seriesToAxisMap
[setName
]];
892 var color
= this.colors
[setName
];
894 // setup graphics context
898 var prevYs
= [-1, -1];
899 var yscale
= axis
.yscale
;
900 // should be same color as the lines but only 15% opaque.
901 var rgb
= new RGBColor(color
);
902 var err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
904 ctx
.fillStyle
= err_color
;
906 for (var j
= 0; j
< this.layout
.points
.length
; j
++) {
907 var point
= this.layout
.points
[j
];
908 if (point
.name
== setName
) {
909 if (!Dygraph
.isOK(point
.y
)) {
916 var newYs
= [ prevY
- point
.errorPlus
* yscale
,
917 prevY
+ point
.errorMinus
* yscale
];
920 var newYs
= [ point
.y
- point
.errorPlus
* yscale
,
921 point
.y
+ point
.errorMinus
* yscale
];
923 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
924 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
927 ctx
.moveTo(prevX
, newYs
[0]);
929 ctx
.moveTo(prevX
, prevYs
[0]);
931 ctx
.lineTo(point
.canvasx
, newYs
[0]);
932 ctx
.lineTo(point
.canvasx
, newYs
[1]);
934 ctx
.lineTo(prevX
, newYs
[1]);
936 ctx
.lineTo(prevX
, prevYs
[1]);
941 prevX
= point
.canvasx
;
946 } else if (fillGraph
) {
947 var baseline
= [] // for stacked graphs: baseline for filling
949 // process sets in reverse order (needed for stacked graphs)
950 for (var i
= setCount
- 1; i
>= 0; i
--) {
951 var setName
= setNames
[i
];
952 var color
= this.colors
[setName
];
953 var axis
= this.layout
.options
.yAxes
[
954 this.layout
.options
.seriesToAxisMap
[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 for (var i
= 0; i
< setCount
; i
++) {
1007 var setName
= setNames
[i
];
1008 var color
= this.colors
[setName
];
1009 var strokeWidth
= this.dygraph_
.attr_("strokeWidth", setName
);
1011 // setup graphics context
1013 var point
= this.layout
.points
[0];
1014 var pointSize
= this.dygraph_
.attr_("pointSize", setName
);
1015 var prevX
= null, prevY
= null;
1016 var drawPoints
= this.dygraph_
.attr_("drawPoints", setName
);
1017 var points
= this.layout
.points
;
1018 for (var j
= 0; j
< points
.length
; j
++) {
1019 var point
= points
[j
];
1020 if (point
.name
== setName
) {
1021 if (!Dygraph
.isOK(point
.canvasy
)) {
1022 if (stepPlot
&& prevX
!= null) {
1023 // Draw a horizontal line to the start of the missing data
1025 ctx
.strokeStyle
= color
;
1026 ctx
.lineWidth
= this.options
.strokeWidth
;
1027 ctx
.moveTo(prevX
, prevY
);
1028 ctx
.lineTo(point
.canvasx
, prevY
);
1031 // this will make us move to the next point, not draw a line to it.
1032 prevX
= prevY
= null;
1034 // A point is "isolated" if it is non-null but both the previous
1035 // and next points are null.
1036 var isIsolated
= (!prevX
&& (j
== points
.length
- 1 ||
1037 !Dygraph
.isOK(points
[j
+1].canvasy
)));
1040 prevX
= point
.canvasx
;
1041 prevY
= point
.canvasy
;
1043 // TODO(danvk): figure out why this conditional is necessary.
1046 ctx
.strokeStyle
= color
;
1047 ctx
.lineWidth
= strokeWidth
;
1048 ctx
.moveTo(prevX
, prevY
);
1050 ctx
.lineTo(point
.canvasx
, prevY
);
1052 prevX
= point
.canvasx
;
1053 prevY
= point
.canvasy
;
1054 ctx
.lineTo(prevX
, prevY
);
1059 if (drawPoints
|| isIsolated
) {
1061 ctx
.fillStyle
= color
;
1062 ctx
.arc(point
.canvasx
, point
.canvasy
, pointSize
,
1063 0, 2 * Math
.PI
, false);