From f872b6ec250b121f0a483ea7d312db0c25a0a141 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sun, 19 May 2013 20:01:01 -0700 Subject: [PATCH] Annotation persistence working --- extras/super-annotations.js | 116 +++++++++++++++++++++++++++----------------- tests/hairlines.html | 74 +++++++++++++++++++++------- 2 files changed, 129 insertions(+), 61 deletions(-) diff --git a/extras/super-annotations.js b/extras/super-annotations.js index d009576..f7ff85b 100644 --- a/extras/super-annotations.js +++ b/extras/super-annotations.js @@ -67,28 +67,30 @@ annotations.prototype.annotationWasDragged = function(a, event, ui) { var area = g.getArea(); var oldXVal = a.xval; - // TODO(danvk): find closest point - // - area.x ? var row = g.findClosestRow(ui.position.left); + var newXVval = g.getValue(row, 0); + if (newXVval == oldXVal) return; + a.xval = g.getValue(row, 0); g.setSelection(row, a.series); this.moveAnnotationToTop(a); this.updateAnnotationDivPositions(); this.updateAnnotationInfo(); - // $(this).triggerHandler('hairlineMoved', { - // // TODO(danvk): fill in - // }); - // $(this).triggerHandler('annotationsChanged', {}); + $(this).triggerHandler('annotationMoved', { + annotation: a, + oldXVal: oldXVal, + newXVal: a.xval + }); + $(this).triggerHandler('annotationsChanged', {}); }; // This creates the hairline object and returns it. // It does not position it and does not attach it to the chart. -annotations.prototype.createAnnotation = function(series, xval) { - var a; +annotations.prototype.createAnnotation = function(a) { var self = this; - var color = this.getColorForSeries_(series); + var color = this.getColorForSeries_(a.series); var $lineDiv = $('
').css({ 'width': '1px', @@ -106,13 +108,10 @@ annotations.prototype.createAnnotation = function(series, xval) { }) .show(); - a = { - xval: xval, - series: series, + $.extend(a, { lineDiv: $lineDiv.get(0), infoDiv: $infoDiv.get(0) - }; - $.extend(a, this.defaultAnnotationProperties_); + }); var that = this; @@ -129,7 +128,7 @@ annotations.prototype.createAnnotation = function(series, xval) { } }); - // TODO(danvk): use 'on' instead of + // TODO(danvk): use 'on' instead of delegate/dblclick $infoDiv.delegate('.annotation-kill-button', 'click', function() { that.removeAnnotation(a); $(that).triggerHandler('annotationDeleted', a); @@ -138,17 +137,26 @@ annotations.prototype.createAnnotation = function(series, xval) { $infoDiv.dblclick(function() { if (a.editable == true) return; + self.moveAnnotationToTop(a); + + // Note: we have to fill out the HTML ourselves because + // updateAnnotationInfo() won't touch editable annotations. a.editable = true; - self.updateAnnotationInfo(); + var editableTemplateDiv = $('#annotation-editable-template').get(0); + a.infoDiv.innerHTML = self.getTemplateHTML(editableTemplateDiv, a); + $(that).triggerHandler('beganEditAnnotation', a); }); $infoDiv.delegate('.annotation-update', 'click', function() { self.extractUpdatedProperties_($infoDiv.get(0), a); a.editable = false; self.updateAnnotationInfo(); + $(that).triggerHandler('annotationEdited', a); + $(that).triggerHandler('annotationsChanged', {}); }); $infoDiv.delegate('.annotation-cancel', 'click', function() { a.editable = false; self.updateAnnotationInfo(); + $(that).triggerHandler('cancelEditAnnotation', a); }); return a; @@ -209,12 +217,18 @@ annotations.prototype.updateAnnotationDivPositions = function() { var that = this; $.each(this.annotations_, function(idx, a) { - // TODO(danvk): cache this information for each annotation var row_col = that.findPointIndex_(a.series, a.xval); + if (row_col == null) { + $([a.lineDiv, a.infoDiv]).hide(); + return; + } else { + // TODO(danvk): only do this if they're invisible? + $([a.lineDiv, a.infoDiv]).show(); + } var xy = g.toDomCoords(a.xval, g.getValue(row_col[0], row_col[1])); var x = xy[0], y = xy[1]; - var lineHeight = 6; + var lineHeight = 6; // TODO(danvk): option? $(a.lineDiv).css({ 'left': x + 'px', @@ -234,18 +248,31 @@ annotations.prototype.updateAnnotationInfo = function() { var that = this; var templateDiv = $('#annotation-template').get(0); - var editableTemplateDiv = $('#annotation-editable-template').get(0); $.each(this.annotations_, function(idx, a) { - var div = a.editable ? editableTemplateDiv : templateDiv; - a.infoDiv.innerHTML = that.getTemplateHTML(div, a); + // We should never update an editable div -- doing so may kill unsaved + // edits to an annotation. + if (a.editable) return; + a.infoDiv.innerHTML = that.getTemplateHTML(templateDiv, a); }); }; +/** + * @param {!Annotation} a Internal annotation + * @return {!PublicAnnotation} a view of the annotation for the public API. + */ +annotations.prototype.createPublicAnnotation_ = function(a, opt_props) { + var displayAnnotation = $.extend({}, a, opt_props); + delete displayAnnotation['infoDiv']; + delete displayAnnotation['lineDiv']; + return displayAnnotation; +}; + // Fill out a div using the values in the annotation object. // The div's html is expected to have text of the form "{{key}}" annotations.prototype.getTemplateHTML = function(div, a) { var g = this.dygraph_; var row_col = this.findPointIndex_(a.series, a.xval); + if (row_col == null) return; // perhaps it's no longer a real point? var row = row_col[0]; var col = row_col[1]; @@ -255,11 +282,8 @@ annotations.prototype.getTemplateHTML = function(div, a) { var x = xvf(a.xval); var y = g.getOption('valueFormatter', a.series)( g.getValue(row, col), yOptView); - var displayAnnotation = $.extend({}, a, { - x: x, - y: y - }); + var displayAnnotation = this.createPublicAnnotation_(a, {x:x, y:y}); var html = div.innerHTML; for (var k in displayAnnotation) { var v = displayAnnotation[k]; @@ -285,6 +309,10 @@ annotations.prototype.extractUpdatedProperties_ = function(div, a) { annotations.prototype.attachAnnotationsToChart_ = function() { var div = this.dygraph_.graphDiv; $.each(this.annotations_, function(idx, a) { + // Re-attaching an editable div to the DOM can clear its focus. + // This makes typing really difficult! + if (a.editable) return; + $([a.lineDiv, a.infoDiv]).appendTo(div); }); }; @@ -315,16 +343,17 @@ annotations.prototype.pointClick = function(e) { // Prevent any other behavior based on this click, e.g. creation of a hairline. e.preventDefault(); - this.annotations_.push(this.createAnnotation(e.point.name, e.point.xval)); + var a = $.extend({}, this.defaultAnnotationProperties_, { + series: e.point.name, + xval: e.point.xval + }); + this.annotations_.push(this.createAnnotation(a)); this.updateAnnotationDivPositions(); this.updateAnnotationInfo(); this.attachAnnotationsToChart_(); - $(this).triggerHandler('hairlineCreated', { - // TODO - // xFraction: xFraction - }); + $(this).triggerHandler('annotationCreated', a); $(this).triggerHandler('annotationsChanged', {}); }; @@ -337,11 +366,11 @@ annotations.prototype.destroy = function() { /** * This is a restricted view of this.annotations_ which doesn't expose - * implementation details like the handle divs. + * implementation details like the line / info divs. * * @typedef { - * xFraction: number, // invariant across resize - * interpolated: bool // alternative is to snap to closest + * xval: number, // x-value (i.e. millis or a raw number) + * series: string, // series name * } PublicAnnotation */ @@ -352,17 +381,13 @@ annotations.prototype.destroy = function() { annotations.prototype.get = function() { var result = []; for (var i = 0; i < this.annotations_.length; i++) { - var h = this.annotations_[i]; - result.push({ - xFraction: h.xFraction, - interpolated: h.interpolated - }); + result.push(this.createPublicAnnotation_(this.annotations_[i])); } return result; }; /** - * Calling this will result in a annotationsChanged event being triggered, no + * Calling this will result in an annotationsChanged event being triggered, no * matter whether it consists of additions, deletions, moves or no changes at * all. * @@ -374,14 +399,17 @@ annotations.prototype.set = function(annotations) { // They're already correctly z-ordered. var anyCreated = false; for (var i = 0; i < annotations.length; i++) { - var h = annotations[i]; + var a = annotations[i]; if (this.annotations_.length > i) { - this.annotations_[i].xFraction = h.xFraction; - this.annotations_[i].interpolated = h.interpolated; + // Only the divs need to be preserved. + var oldA = this.annotations_[i]; + this.annotations_[i] = $.extend({ + infoDiv: oldA.infoDiv, + lineDiv: oldA.lineDiv + }, a); } else { - // TODO(danvk): pass in |interpolated| value. - this.annotations_.push(this.createAnnotation(h.xFraction)); + this.annotations_.push(this.createAnnotation(a)); anyCreated = true; } } diff --git a/tests/hairlines.html b/tests/hairlines.html index 199cf45..afb7066 100644 --- a/tests/hairlines.html +++ b/tests/hairlines.html @@ -14,7 +14,10 @@ --> + + @@ -170,7 +173,7 @@ } ); - var shouldUpdate = false; // true; + var shouldUpdate = true; var update = function() { if (!shouldUpdate) return; data.push([last_t, fn(last_t)]); @@ -202,38 +205,75 @@ setDefaultHairlines(); }); - // Hairline persistence + // Persistence function loadFromStorage() { hairlines.set(JSON.parse(localStorage.getItem('hairlines'))); - } - function storeHairlines() { - localStorage.setItem('hairlines', JSON.stringify(hairlines.get())); + annotations.set(JSON.parse(localStorage.getItem('annotations'))); } $(hairlines).on('hairlinesChanged', function(e) { - storeHairlines(); + localStorage.setItem('hairlines', JSON.stringify(hairlines.get())); + }); + $(annotations).on('annotationsChanged', function(e) { + localStorage.setItem('annotations', JSON.stringify(annotations.get())); }); - function setDefaultHairlines() { - // triggers 'hairlinesChanged' event, above. + function setDefaultState() { + // triggers 'hairlinesChanged' and 'annotationsChanged' events, above. hairlines.set([{xFraction: 0.55}]); + hairlines.set([{xFraction: 0.55}]); + annotations.set([{ + xval: 67, + series: 'Value', + text: 'Bottom' + }, + { + xval: 137, + series: 'Value', + text: 'Fast Change' + }]); } - if (!localStorage.getItem('hairlines')) { - setDefaultHairlines(); + if (!localStorage.getItem('hairlines') || + !localStorage.getItem('annotations')) { + setDefaultState(); } else { loadFromStorage(); } // Demonstration of how to use various other event listeners - $(hairlines).on('hairlineMoved', function(e, data) { - // console.log('hairline moved from', data.oldXFraction, ' to ', data.newXFraction); - }); - $(hairlines).on('hairlineCreated', function(e, data) { - console.log('hairline created at ', data.xFraction); + $(hairlines).on({ + 'hairlineMoved': function(e, data) { + // console.log('hairline moved from', data.oldXFraction, ' to ', data.newXFraction); + }, + 'hairlineCreated': function(e, data) { + console.log('hairline created at ', data.xFraction); + }, + 'hairlineDeleted': function(e, data) { + console.log('hairline deleted at ', data.xFraction); + } }); - $(hairlines).on('hairlineDeleted', function(e, data) { - console.log('hairline deleted at ', data.xFraction); + $(annotations).on({ + 'annotationCreated': function(e, data) { + console.log('annotation created at ', data.series, data.xval); + }, + 'annotationMoved': function(e, data) { + console.log('annotation moved from ', data.oldXVal, ' to ', data.newXVal); + }, + 'annotationDeleted': function(e, data) { + console.log('annotation deleted at ', data.series, data.xval); + }, + 'beganEditAnnotation': function(e, data) { + console.log('began editing annotation at ', data.series, data.xval); + }, + 'annotationEdited': function(e, data) { + console.log('edited annotation at ', data.series, data.xval); + }, + 'cancelEditAnnotation': function(e, data) { + console.log('edit canceled on annotation at ', data.series, data.xval); + } }); + + // TODO(danvk): demonstrate other annotations API methods.