start of hairlines work; can drag them around
authorDan Vanderkam <danvdk@gmail.com>
Mon, 1 Apr 2013 03:46:54 +0000 (23:46 -0400)
committerDan Vanderkam <danvdk@gmail.com>
Mon, 1 Apr 2013 03:46:54 +0000 (23:46 -0400)
dygraph.js
extras/hairlines.js [new file with mode: 0644]
tests/hairlines.html [new file with mode: 0644]
tests/plugins.html

index 4316ac3..7e8fdee 100644 (file)
@@ -505,7 +505,15 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins'));
   for (var i = 0; i < plugins.length; i++) {
     var Plugin = plugins[i];
-    var pluginInstance = new Plugin();
+
+    // the plugins option may contain either plugin classes or instances.
+    var pluginInstance;
+    if (typeof(Plugin.activate) !== 'undefined') {
+      pluginInstance = Plugin;
+    } else {
+      pluginInstance = new Plugin();
+    }
+
     var pluginDict = {
       plugin: pluginInstance,
       events: {},
diff --git a/extras/hairlines.js b/extras/hairlines.js
new file mode 100644 (file)
index 0000000..db82d6e
--- /dev/null
@@ -0,0 +1,175 @@
+/**
+ * @license
+ * Copyright 2013 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/*global Dygraph:false */
+
+Dygraph.Plugins.Hairlines = (function() {
+
+"use strict";
+
+/**
+Current bits of jankiness:
+- Uses dygraph.layout_ to get the parsed hairlines.
+- Uses dygraph.plotter_.area
+
+It would be nice if the plugin didn't require so much special support inside
+the core dygraphs classes, but hairlines involve quite a bit of parsing and
+layout.
+
+TODO(danvk): cache DOM elements.
+
+*/
+
+/**
+ * @typedef {
+ *   xFraction: number,   // invariant across resize
+ *   interpolated: bool,  // alternative is to snap to closest
+ *   lineDiv: !Element    // vertical hairline div
+ *   infoDiv: !Element    // div containing info about the nearest points
+ * } Hairline
+ */
+
+var hairlines = function() {
+  /* @type {!Array.<!Hairline>} */
+  this.hairlines_ = [];
+
+  // Used to detect resizes (which require the divs to be repositioned).
+  this.lastWidth_ = -1;
+  this.lastHeight = -1;
+  this.dygraph_ = null;
+};
+
+hairlines.prototype.toString = function() {
+  return "Hairlines Plugin";
+};
+
+hairlines.prototype.activate = function(g) {
+  this.dygraph_ = g;
+  this.hairlines_ = [this.createHairline(0.55)];
+
+  return {
+    didDrawChart: this.didDrawChart
+  };
+};
+
+hairlines.prototype.detachLabels = function() {
+  for (var i = 0; i < this.hairlines_.length; i++) {
+    var h = this.hairlines_[i];
+    $(h.lineDiv).remove();
+    $(h.infoDiv).remove();
+    this.hairlines_[i] = null;
+  }
+  this.hairlines_ = [];
+};
+
+hairlines.prototype.hairlineWasDragged = function(h, event, ui) {
+  var area = this.dygraph_.getArea();
+  h.xFraction = (ui.position.left - area.x) / area.w;
+  this.updateHairlineDivPositions();
+  this.updateHairlineInfo();
+};
+
+hairlines.prototype.createHairline = function(xFraction) {
+  var h;
+  var self = this;
+
+  var $lineDiv = $('<div/>').css({
+    'border-right': '1px solid black',
+    'width': '0px',
+    'position': 'absolute',
+    'z-index': '10'
+  })
+    .addClass('dygraph-hairline')
+    .appendTo(this.dygraph_.graphDiv);
+
+  var $infoDiv = $('<div/>').css({
+    'border': '1px solid black',
+    'display': 'table',  // shrink to fit
+    'z-index': '10',
+    'padding': '3px',
+    'background': 'white',
+    'position': 'absolute'
+  })
+    .addClass('dygraph-hairline-info')
+    .text('Info')
+    .draggable({
+      'axis': 'x',
+      'containment': 'parent',
+      'drag': function(event, ui) {
+        self.hairlineWasDragged(h, event, ui);
+      }
+      // TODO(danvk): set cursor here
+    })
+    .appendTo(this.dygraph_.graphDiv);
+
+  h = {
+    xFraction: xFraction,
+    interpolated: true,
+    lineDiv: $lineDiv.get(0),
+    infoDiv: $infoDiv.get(0)
+  };
+
+  return h;
+};
+
+// Positions existing hairline divs.
+hairlines.prototype.updateHairlineDivPositions = function() {
+  var layout = this.dygraph_.getArea();
+  $.each(this.hairlines_, function(idx, h) {
+    var left = layout.x + h.xFraction * layout.w;
+    $(h.lineDiv).css({
+      'left': left + 'px',
+      'top': layout.y + 'px',
+      'height': layout.h + 'px'
+    });
+    $(h.infoDiv).css({
+      'left': left + 'px',
+      'top': layout.y + 'px',
+    });
+  });
+};
+
+// Fills out the info div based on current coordinates.
+hairlines.prototype.updateHairlineInfo = function() {
+  var g = this.dygraph_;
+  var xRange = g.xAxisRange();
+  $.each(this.hairlines_, function(idx, h) {
+    var xValue = h.xFraction * (xRange[1] - xRange[0]) + xRange[0];
+
+    // TODO(danvk): find appropriate y-values and format them.
+
+    var xOptView = g.optionsViewForAxis_('x');
+    var xvf = xOptView('valueFormatter');
+    var html = xvf(xValue, xOptView, xValue, g);
+    $(h.infoDiv).html(html);
+  });
+};
+
+hairlines.prototype.didDrawChart = function(e) {
+  var g = e.dygraph;
+
+  // Early out in the (common) case of zero hairlines.
+  if (this.hairlines_.length === 0) return;
+
+  var containerDiv = e.canvas.parentNode;
+  var width = containerDiv.offsetWidth;
+  var height = containerDiv.offsetHeight;
+  if (width !== this.lastWidth_ || height !== this.lastHeight_) {
+    this.lastWidth_ = width;
+    this.lastHeight_ = height;
+    this.updateHairlineDivPositions();
+  }
+
+  this.updateHairlineInfo();
+};
+
+hairlines.prototype.destroy = function() {
+  this.detachLabels();
+};
+
+return hairlines;
+
+})();
diff --git a/tests/hairlines.html b/tests/hairlines.html
new file mode 100644 (file)
index 0000000..ae42f80
--- /dev/null
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>Hairlines demo</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../dygraph-dev.js"></script>
+
+    <!-- Include the Javascript for the plug-in -->
+    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
+    <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/jquery-ui.min.js"></script>
+    <link rel='stylesheet' href='http://code.jquery.com/ui/1.10.1/themes/base/jquery-ui.css' />
+
+    <script type="text/javascript" src="../extras/hairlines.js"></script>
+
+    <style>
+      #demodiv {
+        position: absolute;
+        left: 10px;
+        right: 200px;
+        height: 400px;
+        display: inline-block;
+      }
+      #status {
+        position: absolute;
+        right: 10px;
+        width: 180px;
+        height: 400px;
+        display: inline-block;
+      }
+      /*
+      .dygraph-hairline {
+        border-right-style: dotted !important;
+      }
+      */
+    </style>
+  </head>
+  <body>
+    <h2>Hairlines Demo</h2>
+
+    <div id="demodiv"></div>
+    <div id="status"></div>
+
+    <script type="text/javascript">
+      var last_t = 0;
+      var data = [];
+      var fn = function(t) {
+        return Math.sin(Math.PI/180 * t * 4);
+      };
+      for (; last_t < 200; last_t++) {
+        data.push([last_t, fn(last_t)]);
+      }
+
+      g = new Dygraph(
+              document.getElementById("demodiv"),
+              data,
+              {
+                labelsDiv: document.getElementById('status'),
+                labelsSeparateLines: true,
+                legend: 'always',
+                labels: [ 'Time', 'Value' ],
+                title: 'Interesting Shapes',
+
+                // Set the plug-ins in the options.
+                plugins : [
+                  Dygraph.Plugins.Hairlines
+                ]
+              }
+          );
+
+      var update = function() {
+        data.push([last_t, fn(last_t)]);
+        last_t++;
+        data.splice(0, 1);
+        g.updateOptions({file: data});
+      };
+      window.setInterval(update, 1000);
+    </script>
+</body>
+</html>
index 2d084d0..49a11cd 100644 (file)
@@ -9,7 +9,7 @@
     <script type="text/javascript" src="../dygraph-dev.js"></script>
 
     <!-- Include the Javascript for the plug-in -->
-    <script type="text/javascript" src="../plugins/unzoom.js"></script>
+    <script type="text/javascript" src="../extras/unzoom.js"></script>
   </head>
   <body>
     <h2>Plugins Demo</h2>