3 * Copyright 2013 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
6 * Note: This plugin requires jQuery and jQuery UI Draggable.
8 * See high-level documentation at
9 * https://docs.google.com/document/d/1OHNE8BNNmMtFlRQ969DACIYIJ9VVJ7w3dSPRJDEeIew/edit#
12 /*global Dygraph:false */
14 Dygraph
.Plugins
.SuperAnnotations
= (function() {
19 * These are just the basic requirements -- annotations can have whatever other
20 * properties the code that displays them wants them to have.
23 * xval: number, // x-value (i.e. millis or a raw number)
24 * series: string, // series name
25 * yFrac: ?number, // y-positioning. Default is a few px above the point.
26 * lineDiv: !Element // vertical div connecting point to info div.
27 * infoDiv: !Element // div containing info about the annotation.
31 var annotations
= function(opt_options
) {
32 /* @type {!Array.<!Annotation>} */
33 this.annotations_
= [];
34 // Used to detect resizes (which require the divs to be repositioned).
39 opt_options
= opt_options
|| {};
40 this.defaultAnnotationProperties_
= $.extend({
42 }, opt_options
['defaultAnnotationProperties']);
45 annotations
.prototype.toString
= function() {
46 return "SuperAnnotations Plugin";
49 annotations
.prototype.activate
= function(g
) {
51 this.annotations_
= [];
54 didDrawChart
: this.didDrawChart
,
55 pointClick
: this.pointClick
// TODO(danvk): implement in dygraphs
59 annotations
.prototype.detachLabels
= function() {
60 for (var i
= 0; i
< this.annotations_
.length
; i
++) {
61 var a
= this.annotations_
[i
];
62 $(a
.lineDiv
).remove();
63 $(a
.infoDiv
).remove();
64 this.annotations_
[i
] = null;
66 this.annotations_
= [];
69 annotations
.prototype.annotationWasDragged
= function(a
, event
, ui
) {
70 var g
= this.dygraph_
;
71 var area
= g
.getArea();
72 var oldYFrac
= a
.yFrac
;
74 var infoDiv
= a
.infoDiv
;
75 var newYFrac
= ((infoDiv
.offsetTop
+ infoDiv
.offsetHeight
) - area
.y
) / area
.h
;
76 if (newYFrac
== oldYFrac
) return;
80 this.moveAnnotationToTop(a
);
81 this.updateAnnotationDivPositions();
82 this.updateAnnotationInfo();
83 $(this).triggerHandler('annotationMoved', {
88 $(this).triggerHandler('annotationsChanged', {});
91 annotations
.prototype.makeAnnotationEditable
= function(a
) {
92 if (a
.editable
== true) return;
93 this.moveAnnotationToTop(a
);
95 // Note: we have to fill out the HTML ourselves because
96 // updateAnnotationInfo() won't touch editable annotations.
98 var editableTemplateDiv
= $('#annotation-editable-template').get(0);
99 a
.infoDiv
.innerHTML
= this.getTemplateHTML(editableTemplateDiv
, a
);
100 $(a
.infoDiv
).toggleClass('editable', !!a
.editable
);
101 $(this).triggerHandler('beganEditAnnotation', a
);
104 // This creates the hairline object and returns it.
105 // It does not position it and does not attach it to the chart.
106 annotations
.prototype.createAnnotation
= function(a
) {
109 var color
= this.getColorForSeries_(a
.series
);
111 var $lineDiv
= $('<div/>').css({
114 'background': 'black',
116 'position': 'absolute',
117 // TODO(danvk): use border-color here for consistency?
118 'background-color': color
,
120 }).addClass('dygraph-annotation-line');
122 var $infoDiv
= $('#annotation-template').clone().removeAttr('id').css({
123 'position': 'absolute',
124 'border-color': color
,
130 lineDiv
: $lineDiv
.get(0),
131 infoDiv
: $infoDiv
.get(0)
137 'start': function(event
, ui
) {
138 $(this).css({'bottom': ''});
141 'drag': function(event
, ui
) {
142 self
.annotationWasDragged(a
, event
, ui
);
144 'stop': function(event
, ui
) {
145 $(this).css({'top': ''});
146 a
.isDragging
= false;
147 self
.updateAnnotationDivPositions();
150 'containment': 'parent'
153 // TODO(danvk): use 'on' instead of delegate/dblclick
154 $infoDiv
.on('click', '.annotation-kill-button', function() {
155 that
.removeAnnotation(a
);
156 $(that
).triggerHandler('annotationDeleted', a
);
157 $(that
).triggerHandler('annotationsChanged', {});
160 $infoDiv
.on('dblclick', function() {
161 that
.makeAnnotationEditable(a
);
163 $infoDiv
.on('click', '.annotation-update', function() {
164 self
.extractUpdatedProperties_($infoDiv
.get(0), a
);
166 self
.updateAnnotationInfo();
167 $(that
).triggerHandler('annotationEdited', a
);
168 $(that
).triggerHandler('annotationsChanged', {});
170 $infoDiv
.on('click', '.annotation-cancel', function() {
172 self
.updateAnnotationInfo();
173 $(that
).triggerHandler('cancelEditAnnotation', a
);
179 // Find the index of a point in a series.
180 // Returns a 2-element array, [row, col], which can be used with
181 // dygraph.getValue() to get the value at this point.
182 // Returns null if there's no match.
183 annotations
.prototype.findPointIndex_
= function(series
, xval
) {
184 var col
= this.dygraph_
.getLabels().indexOf(series
);
185 if (col
== -1) return null;
187 var lowIdx
= 0, highIdx
= this.dygraph_
.numRows() - 1;
188 while (lowIdx
<= highIdx
) {
189 var idx
= Math
.floor((lowIdx
+ highIdx
) / 2);
190 var xAtIdx
= this.dygraph_
.getValue(idx
, 0);
191 if (xAtIdx
== xval
) {
193 } else if (xAtIdx
< xval
) {
202 annotations
.prototype.getColorForSeries_
= function(series
) {
203 var colors
= this.dygraph_
.getColors();
204 var col
= this.dygraph_
.getLabels().indexOf(series
);
205 if (col
== -1) return null;
207 return colors
[(col
- 1) % colors
.length
];
210 // Moves a hairline's divs to the top of the z-ordering.
211 annotations
.prototype.moveAnnotationToTop
= function(a
) {
212 var div
= this.dygraph_
.graphDiv
;
213 $(a
.infoDiv
).appendTo(div
);
214 $(a
.lineDiv
).appendTo(div
);
216 var idx
= this.annotations_
.indexOf(a
);
217 this.annotations_
.splice(idx
, 1);
218 this.annotations_
.push(a
);
221 // Positions existing hairline divs.
222 annotations
.prototype.updateAnnotationDivPositions
= function() {
223 var layout
= this.dygraph_
.getArea();
224 var chartLeft
= layout
.x
, chartRight
= layout
.x
+ layout
.w
;
225 var chartTop
= layout
.y
, chartBottom
= layout
.y
+ layout
.h
;
226 var div
= this.dygraph_
.graphDiv
;
227 var pos
= Dygraph
.findPos(div
);
228 var box
= [layout
.x
+ pos
.x
, layout
.y
+ pos
.y
];
229 box
.push(box
[0] + layout
.w
);
230 box
.push(box
[1] + layout
.h
);
232 var g
= this.dygraph_
;
235 $.each(this.annotations_
, function(idx
, a
) {
236 var row_col
= that
.findPointIndex_(a
.series
, a
.xval
);
237 if (row_col
== null) {
238 $([a
.lineDiv
, a
.infoDiv
]).hide();
241 // TODO(danvk): only do this if they're invisible?
242 $([a
.lineDiv
, a
.infoDiv
]).show();
244 var xy
= g
.toDomCoords(a
.xval
, g
.getValue(row_col
[0], row_col
[1]));
245 var x
= xy
[0], pointY
= xy
[1];
247 var lineHeight
= 6; // TODO(danvk): option?
250 if (a
.yFrac
!== undefined
) {
251 y
= layout
.y
+ layout
.h
* a
.yFrac
;
256 var lineHeight
= y
< pointY
? (pointY
- y
) : (y
- pointY
- a
.infoDiv
.offsetHeight
);
259 'top': Math
.min(y
, pointY
) + 'px',
260 'height': lineHeight
+ 'px'
266 // jQuery UI draggable likes to set 'top', whereas superannotations sets
267 // 'bottom'. Setting both will make the annotation grow and contract as
268 // the user drags it, which looks bad.
270 'bottom': (div
.offsetHeight
- y
) + 'px'
271 }) //.draggable("option", "containment", box);
273 var visible
= (x
>= chartLeft
&& x
<= chartRight
) &&
274 (pointY
>= chartTop
&& pointY
<= chartBottom
);
275 $([a
.infoDiv
, a
.lineDiv
]).toggle(visible
);
280 // Fills out the info div based on current coordinates.
281 annotations
.prototype.updateAnnotationInfo
= function() {
282 var g
= this.dygraph_
;
285 var templateDiv
= $('#annotation-template').get(0);
286 $.each(this.annotations_
, function(idx
, a
) {
287 // We should never update an editable div -- doing so may kill unsaved
288 // edits to an annotation.
289 $(a
.infoDiv
).toggleClass('editable', !!a
.editable
);
290 if (a
.editable
) return;
291 a
.infoDiv
.innerHTML
= that
.getTemplateHTML(templateDiv
, a
);
296 * @param {!Annotation} a Internal annotation
297 * @return {!PublicAnnotation} a view of the annotation for the public API.
299 annotations
.prototype.createPublicAnnotation_
= function(a
, opt_props
) {
300 var displayAnnotation
= $.extend({}, a
, opt_props
);
301 delete displayAnnotation
['infoDiv'];
302 delete displayAnnotation
['lineDiv'];
303 delete displayAnnotation
['isDragging'];
304 delete displayAnnotation
['editable'];
305 return displayAnnotation
;
308 // Fill out a div using the values in the annotation object.
309 // The div's html is expected to have text of the form "{{key}}"
310 annotations
.prototype.getTemplateHTML
= function(div
, a
) {
311 var g
= this.dygraph_
;
312 var row_col
= this.findPointIndex_(a
.series
, a
.xval
);
313 if (row_col
== null) return; // perhaps it's no longer a real point?
314 var row
= row_col
[0];
315 var col
= row_col
[1];
317 var yOptView
= g
.optionsViewForAxis_('y1'); // TODO: support secondary, too
318 var xvf
= g
.getOptionForAxis('valueFormatter', 'x');
321 var y
= g
.getOption('valueFormatter', a
.series
)(
322 g
.getValue(row
, col
), yOptView
);
324 var displayAnnotation
= this.createPublicAnnotation_(a
, {x
:x
, y
:y
});
325 var html
= div
.innerHTML
;
326 for (var k
in displayAnnotation
) {
327 var v
= displayAnnotation
[k
];
328 if (typeof(v
) == 'object') continue; // e.g. infoDiv or lineDiv
329 html
= html
.replace(new RegExp('\{\{' + k
+ '\}\}', 'g'), v
);
334 // Update the annotation object by looking for elements with a 'dg-ann-field'
335 // attribute. For example, <input type='text' dg-ann-field='text' /> will have
336 // its value placed in the 'text' attribute of the annotation.
337 annotations
.prototype.extractUpdatedProperties_
= function(div
, a
) {
338 $(div
).find('[dg-ann-field]').each(function(idx
, el
) {
339 var k
= $(el
).attr('dg-ann-field');
345 // After a resize, the hairline divs can get dettached from the chart.
346 // This reattaches them.
347 annotations
.prototype.attachAnnotationsToChart_
= function() {
348 var div
= this.dygraph_
.graphDiv
;
349 $.each(this.annotations_
, function(idx
, a
) {
350 // Re-attaching an editable div to the DOM can clear its focus.
351 // This makes typing really difficult!
352 if (a
.editable
) return;
354 $([a
.lineDiv
, a
.infoDiv
]).appendTo(div
);
358 // Deletes a hairline and removes it from the chart.
359 annotations
.prototype.removeAnnotation
= function(a
) {
360 var idx
= this.annotations_
.indexOf(a
);
362 this.annotations_
.splice(idx
, 1);
363 $([a
.lineDiv
, a
.infoDiv
]).remove();
365 Dygraph
.warn('Tried to remove non-existent annotation.');
369 annotations
.prototype.didDrawChart
= function(e
) {
372 // Early out in the (common) case of zero annotations.
373 if (this.annotations_
.length
=== 0) return;
375 this.updateAnnotationDivPositions();
376 this.attachAnnotationsToChart_();
377 this.updateAnnotationInfo();
380 annotations
.prototype.pointClick
= function(e
) {
381 // Prevent any other behavior based on this click, e.g. creation of a hairline.
384 var a
= $.extend({}, this.defaultAnnotationProperties_
, {
385 series
: e
.point
.name
,
388 this.annotations_
.push(this.createAnnotation(a
));
390 this.updateAnnotationDivPositions();
391 this.updateAnnotationInfo();
392 this.attachAnnotationsToChart_();
394 $(this).triggerHandler('annotationCreated', a
);
395 $(this).triggerHandler('annotationsChanged', {});
397 // Annotations should begin life editable.
398 this.makeAnnotationEditable(a
);
401 annotations
.prototype.destroy
= function() {
409 * This is a restricted view of this.annotations_ which doesn't expose
410 * implementation details like the line / info divs.
413 * xval: number, // x-value (i.e. millis or a raw number)
414 * series: string, // series name
419 * @return {!Array.<!PublicAnnotation>} The current set of annotations, ordered
420 * from back to front.
422 annotations
.prototype.get
= function() {
424 for (var i
= 0; i
< this.annotations_
.length
; i
++) {
425 result
.push(this.createPublicAnnotation_(this.annotations_
[i
]));
431 * Calling this will result in an annotationsChanged event being triggered, no
432 * matter whether it consists of additions, deletions, moves or no changes at
435 * @param {!Array.<!PublicAnnotation>} annotations The new set of annotations,
436 * ordered from back to front.
438 annotations
.prototype.set
= function(annotations
) {
439 // Re-use divs from the old annotations array so far as we can.
440 // They're already correctly z-ordered.
441 var anyCreated
= false;
442 for (var i
= 0; i
< annotations
.length
; i
++) {
443 var a
= annotations
[i
];
445 if (this.annotations_
.length
> i
) {
446 // Only the divs need to be preserved.
447 var oldA
= this.annotations_
[i
];
448 this.annotations_
[i
] = $.extend({
449 infoDiv
: oldA
.infoDiv
,
450 lineDiv
: oldA
.lineDiv
453 this.annotations_
.push(this.createAnnotation(a
));
458 // If there are any remaining annotations, destroy them.
459 while (annotations
.length
< this.annotations_
.length
) {
460 this.removeAnnotation(this.annotations_
[annotations
.length
]);
463 this.updateAnnotationDivPositions();
464 this.updateAnnotationInfo();
466 this.attachAnnotationsToChart_();
469 $(this).triggerHandler('annotationsChanged', {});