All Downloads are FREE. Search and download functionalities are using the official Maven repository.

assets.lib.dygraphs.extras.super-annotations.js Maven / Gradle / Ivy

The newest version!
/**
 * @license
 * Copyright 2013 Dan Vanderkam ([email protected])
 * MIT-licensed (http://opensource.org/licenses/MIT)
 *
 * Note: This plugin requires jQuery and jQuery UI Draggable.
 *
 * See high-level documentation at
 * https://docs.google.com/document/d/1OHNE8BNNmMtFlRQ969DACIYIJ9VVJ7w3dSPRJDEeIew/edit#
 */

/*global Dygraph:false */

Dygraph.Plugins.SuperAnnotations = (function() {

"use strict";

/**
 * These are just the basic requirements -- annotations can have whatever other
 * properties the code that displays them wants them to have.
 *
 * @typedef {
 *   xval:  number,      // x-value (i.e. millis or a raw number)
 *   series: string,     // series name
 *   yFrac: ?number,     // y-positioning. Default is a few px above the point.
 *   lineDiv: !Element   // vertical div connecting point to info div.
 *   infoDiv: !Element   // div containing info about the annotation.
 * } Annotation
 */

var annotations = function(opt_options) {
  /* @type {!Array.} */
  this.annotations_ = [];
  // Used to detect resizes (which require the divs to be repositioned).
  this.lastWidth_ = -1;
  this.lastHeight = -1;
  this.dygraph_ = null;

  opt_options = opt_options || {};
  this.defaultAnnotationProperties_ = $.extend({
    'text': 'Description'
  }, opt_options['defaultAnnotationProperties']);
};

annotations.prototype.toString = function() {
  return "SuperAnnotations Plugin";
};

annotations.prototype.activate = function(g) {
  this.dygraph_ = g;
  this.annotations_ = [];

  return {
    didDrawChart: this.didDrawChart,
    pointClick: this.pointClick  // TODO(danvk): implement in dygraphs
  };
};

annotations.prototype.detachLabels = function() {
  for (var i = 0; i < this.annotations_.length; i++) {
    var a = this.annotations_[i];
    $(a.lineDiv).remove();
    $(a.infoDiv).remove();
    this.annotations_[i] = null;
  }
  this.annotations_ = [];
};

annotations.prototype.annotationWasDragged = function(a, event, ui) {
  var g = this.dygraph_;
  var area = g.getArea();
  var oldYFrac = a.yFrac;

  var infoDiv = a.infoDiv;
  var newYFrac = ((infoDiv.offsetTop + infoDiv.offsetHeight) - area.y) / area.h;
  if (newYFrac == oldYFrac) return;

  a.yFrac = newYFrac;

  this.moveAnnotationToTop(a);
  this.updateAnnotationDivPositions();
  this.updateAnnotationInfo();
  $(this).triggerHandler('annotationMoved', {
    annotation: a,
    oldYFrac: oldYFrac,
    newYFrac: a.yFrac
  });
  $(this).triggerHandler('annotationsChanged', {});
};

annotations.prototype.makeAnnotationEditable = function(a) {
  if (a.editable == true) return;
  this.moveAnnotationToTop(a);

  // Note: we have to fill out the HTML ourselves because
  // updateAnnotationInfo() won't touch editable annotations.
  a.editable = true;
  var editableTemplateDiv = $('#annotation-editable-template').get(0);
  a.infoDiv.innerHTML = this.getTemplateHTML(editableTemplateDiv, a);
  $(a.infoDiv).toggleClass('editable', !!a.editable);
  $(this).triggerHandler('beganEditAnnotation', a);
};

// 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(a) {
  var self = this;

  var color = this.getColorForSeries_(a.series);

  var $lineDiv = $('
').css({ 'width': '1px', 'left': '3px', 'background': 'black', 'height': '100%', 'position': 'absolute', // TODO(danvk): use border-color here for consistency? 'background-color': color, 'z-index': 10 }).addClass('dygraph-annotation-line'); var $infoDiv = $('#annotation-template').clone().removeAttr('id').css({ 'position': 'absolute', 'border-color': color, 'z-index': 10 }) .show(); $.extend(a, { lineDiv: $lineDiv.get(0), infoDiv: $infoDiv.get(0) }); var that = this; $infoDiv.draggable({ 'start': function(event, ui) { $(this).css({'bottom': ''}); a.isDragging = true; }, 'drag': function(event, ui) { self.annotationWasDragged(a, event, ui); }, 'stop': function(event, ui) { $(this).css({'top': ''}); a.isDragging = false; self.updateAnnotationDivPositions(); }, 'axis': 'y', 'containment': 'parent' }); // TODO(danvk): use 'on' instead of delegate/dblclick $infoDiv.on('click', '.annotation-kill-button', function() { that.removeAnnotation(a); $(that).triggerHandler('annotationDeleted', a); $(that).triggerHandler('annotationsChanged', {}); }); $infoDiv.on('dblclick', function() { that.makeAnnotationEditable(a); }); $infoDiv.on('click', '.annotation-update', function() { self.extractUpdatedProperties_($infoDiv.get(0), a); a.editable = false; self.updateAnnotationInfo(); $(that).triggerHandler('annotationEdited', a); $(that).triggerHandler('annotationsChanged', {}); }); $infoDiv.on('click', '.annotation-cancel', function() { a.editable = false; self.updateAnnotationInfo(); $(that).triggerHandler('cancelEditAnnotation', a); }); return a; }; // Find the index of a point in a series. // Returns a 2-element array, [row, col], which can be used with // dygraph.getValue() to get the value at this point. // Returns null if there's no match. annotations.prototype.findPointIndex_ = function(series, xval) { var col = this.dygraph_.getLabels().indexOf(series); if (col == -1) return null; var lowIdx = 0, highIdx = this.dygraph_.numRows() - 1; while (lowIdx <= highIdx) { var idx = Math.floor((lowIdx + highIdx) / 2); var xAtIdx = this.dygraph_.getValue(idx, 0); if (xAtIdx == xval) { return [idx, col]; } else if (xAtIdx < xval) { lowIdx = idx + 1; } else { highIdx = idx - 1; } } return null; }; annotations.prototype.getColorForSeries_ = function(series) { var colors = this.dygraph_.getColors(); var col = this.dygraph_.getLabels().indexOf(series); if (col == -1) return null; return colors[(col - 1) % colors.length]; }; // Moves a hairline's divs to the top of the z-ordering. annotations.prototype.moveAnnotationToTop = function(a) { var div = this.dygraph_.graphDiv; $(a.infoDiv).appendTo(div); $(a.lineDiv).appendTo(div); var idx = this.annotations_.indexOf(a); this.annotations_.splice(idx, 1); this.annotations_.push(a); }; // Positions existing hairline divs. annotations.prototype.updateAnnotationDivPositions = function() { var layout = this.dygraph_.getArea(); var chartLeft = layout.x, chartRight = layout.x + layout.w; var chartTop = layout.y, chartBottom = layout.y + layout.h; var div = this.dygraph_.graphDiv; var pos = Dygraph.findPos(div); var box = [layout.x + pos.x, layout.y + pos.y]; box.push(box[0] + layout.w); box.push(box[1] + layout.h); var g = this.dygraph_; var that = this; $.each(this.annotations_, function(idx, a) { 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], pointY = xy[1]; var lineHeight = 6; // TODO(danvk): option? var y = pointY; if (a.yFrac !== undefined) { y = layout.y + layout.h * a.yFrac; } else { y -= lineHeight; } var lineHeight = y < pointY ? (pointY - y) : (y - pointY - a.infoDiv.offsetHeight); $(a.lineDiv).css({ 'left': x + 'px', 'top': Math.min(y, pointY) + 'px', 'height': lineHeight + 'px' }); $(a.infoDiv).css({ 'left': x + 'px', }); if (!a.isDragging) { // jQuery UI draggable likes to set 'top', whereas superannotations sets // 'bottom'. Setting both will make the annotation grow and contract as // the user drags it, which looks bad. $(a.infoDiv).css({ 'bottom': (div.offsetHeight - y) + 'px' }) //.draggable("option", "containment", box); var visible = (x >= chartLeft && x <= chartRight) && (pointY >= chartTop && pointY <= chartBottom); $([a.infoDiv, a.lineDiv]).toggle(visible); } }); }; // Fills out the info div based on current coordinates. annotations.prototype.updateAnnotationInfo = function() { var g = this.dygraph_; var that = this; var templateDiv = $('#annotation-template').get(0); $.each(this.annotations_, function(idx, a) { // We should never update an editable div -- doing so may kill unsaved // edits to an annotation. $(a.infoDiv).toggleClass('editable', !!a.editable); 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']; delete displayAnnotation['isDragging']; delete displayAnnotation['editable']; 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]; var yOptView = g.optionsViewForAxis_('y1'); // TODO: support secondary, too var xvf = g.getOptionForAxis('valueFormatter', 'x'); var x = xvf.call(g, a.xval); var y = g.getOption('valueFormatter', a.series).call( g, g.getValue(row, col), yOptView); var displayAnnotation = this.createPublicAnnotation_(a, {x:x, y:y}); var html = div.innerHTML; for (var k in displayAnnotation) { var v = displayAnnotation[k]; if (typeof(v) == 'object') continue; // e.g. infoDiv or lineDiv html = html.replace(new RegExp('\{\{' + k + '\}\}', 'g'), v); } return html; }; // Update the annotation object by looking for elements with a 'dg-ann-field' // attribute. For example, will have // its value placed in the 'text' attribute of the annotation. annotations.prototype.extractUpdatedProperties_ = function(div, a) { $(div).find('[dg-ann-field]').each(function(idx, el) { var k = $(el).attr('dg-ann-field'); var v = $(el).val(); a[k] = v; }); }; // After a resize, the hairline divs can get dettached from the chart. // This reattaches them. 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); }); }; // Deletes a hairline and removes it from the chart. annotations.prototype.removeAnnotation = function(a) { var idx = this.annotations_.indexOf(a); if (idx >= 0) { this.annotations_.splice(idx, 1); $([a.lineDiv, a.infoDiv]).remove(); } else { Dygraph.warn('Tried to remove non-existent annotation.'); } }; annotations.prototype.didDrawChart = function(e) { var g = e.dygraph; // Early out in the (common) case of zero annotations. if (this.annotations_.length === 0) return; this.updateAnnotationDivPositions(); this.attachAnnotationsToChart_(); this.updateAnnotationInfo(); }; annotations.prototype.pointClick = function(e) { // Prevent any other behavior based on this click, e.g. creation of a hairline. e.preventDefault(); 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('annotationCreated', a); $(this).triggerHandler('annotationsChanged', {}); // Annotations should begin life editable. this.makeAnnotationEditable(a); }; annotations.prototype.destroy = function() { this.detachLabels(); }; // Public API /** * This is a restricted view of this.annotations_ which doesn't expose * implementation details like the line / info divs. * * @typedef { * xval: number, // x-value (i.e. millis or a raw number) * series: string, // series name * } PublicAnnotation */ /** * @return {!Array.} The current set of annotations, ordered * from back to front. */ annotations.prototype.get = function() { var result = []; for (var i = 0; i < this.annotations_.length; i++) { result.push(this.createPublicAnnotation_(this.annotations_[i])); } return result; }; /** * Calling this will result in an annotationsChanged event being triggered, no * matter whether it consists of additions, deletions, moves or no changes at * all. * * @param {!Array.} annotations The new set of annotations, * ordered from back to front. */ annotations.prototype.set = function(annotations) { // Re-use divs from the old annotations array so far as we can. // They're already correctly z-ordered. var anyCreated = false; for (var i = 0; i < annotations.length; i++) { var a = annotations[i]; if (this.annotations_.length > i) { // Only the divs need to be preserved. var oldA = this.annotations_[i]; this.annotations_[i] = $.extend({ infoDiv: oldA.infoDiv, lineDiv: oldA.lineDiv }, a); } else { this.annotations_.push(this.createAnnotation(a)); anyCreated = true; } } // If there are any remaining annotations, destroy them. while (annotations.length < this.annotations_.length) { this.removeAnnotation(this.annotations_[annotations.length]); } this.updateAnnotationDivPositions(); this.updateAnnotationInfo(); if (anyCreated) { this.attachAnnotationsToChart_(); } $(this).triggerHandler('annotationsChanged', {}); }; return annotations; })();




© 2015 - 2024 Weber Informatics LLC | Privacy Policy