Merge branch 'PR723'
authorAdrian Iain Lam <adrianiainlam@users.noreply.github.com>
Tue, 31 Jul 2018 03:55:13 +0000 (11:55 +0800)
committerAdrian Iain Lam <adrianiainlam@users.noreply.github.com>
Tue, 31 Jul 2018 03:55:13 +0000 (11:55 +0800)
This merges changes by @kbaggott in danvk/dygraphs PR #723,
to allow points to be selected (and thus legends to show up)
on touchscreens.

1  2 
src/dygraph-interaction-model.js
src/dygraph-utils.js

   * @author Robert Konigsberg (konigsberg@google.com)
   */
  
 -(function() {
  /*global Dygraph:false */
  "use strict";
  
 +import * as utils from './dygraph-utils';
 +
  /**
   * You can drag this many pixels past the edge of the chart and still have it
   * be considered a zoom. This makes it easier to zoom to the exact edge of the
@@@ -26,7 -25,7 +26,7 @@@ var DRAG_EDGE_MARGIN = 100
   * A collection of functions to facilitate build custom interaction models.
   * @class
   */
 -Dygraph.Interaction = {};
 +var DygraphInteraction = {};
  
  /**
   * Checks whether the beginning & ending of an event were close enough that it
   * @param {Dygraph} g
   * @param {Object} context
   */
 -Dygraph.Interaction.maybeTreatMouseOpAsClick = function(event, g, context) {
 -  context.dragEndX = Dygraph.dragGetX_(event, context);
 -  context.dragEndY = Dygraph.dragGetY_(event, context);
 +DygraphInteraction.maybeTreatMouseOpAsClick = function(event, g, context) {
 +  context.dragEndX = utils.dragGetX_(event, context);
 +  context.dragEndY = utils.dragGetY_(event, context);
    var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
    var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
  
    if (regionWidth < 2 && regionHeight < 2 &&
        g.lastx_ !== undefined && g.lastx_ != -1) {
 -    Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
 +    DygraphInteraction.treatMouseOpAsClick(g, event, context);
    }
  
    context.regionWidth = regionWidth;
   *     dragStartX/dragStartY/etc. properties). This function modifies the
   *     context.
   */
 -Dygraph.Interaction.startPan = function(event, g, context) {
 +DygraphInteraction.startPan = function(event, g, context) {
    var i, axis;
    context.isPanning = true;
    var xRange = g.xAxisRange();
  
    if (g.getOptionForAxis("logscale", "x")) {
 -    context.initialLeftmostDate = Dygraph.log10(xRange[0]);
 -    context.dateRange = Dygraph.log10(xRange[1]) - Dygraph.log10(xRange[0]);
 +    context.initialLeftmostDate = utils.log10(xRange[0]);
 +    context.dateRange = utils.log10(xRange[1]) - utils.log10(xRange[0]);
    } else {
 -    context.initialLeftmostDate = xRange[0];    
 +    context.initialLeftmostDate = xRange[0];
      context.dateRange = xRange[1] - xRange[0];
    }
    context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
    }
  
    // Record the range of each y-axis at the start of the drag.
 -  // If any axis has a valueRange or valueWindow, then we want a 2D pan.
 +  // If any axis has a valueRange, then we want a 2D pan.
    // We can't store data directly in g.axes_, because it does not belong to us
    // and could change out from under us during a pan (say if there's a data
    // update).
      // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
      var logscale = g.attributes_.getForAxis("logscale", i);
      if (logscale) {
 -      axis_data.initialTopValue = Dygraph.log10(yRange[1]);
 -      axis_data.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
 +      axis_data.initialTopValue = utils.log10(yRange[1]);
 +      axis_data.dragValueRange = utils.log10(yRange[1]) - utils.log10(yRange[0]);
      } else {
        axis_data.initialTopValue = yRange[1];
        axis_data.dragValueRange = yRange[1] - yRange[0];
      context.axes.push(axis_data);
  
      // While calculating axes, set 2dpan.
 -    if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
 +    if (axis.valueRange) context.is2DPan = true;
    }
  };
  
   *     dragStartX/dragStartY/etc. properties). This function modifies the
   *     context.
   */
 -Dygraph.Interaction.movePan = function(event, g, context) {
 -  context.dragEndX = Dygraph.dragGetX_(event, context);
 -  context.dragEndY = Dygraph.dragGetY_(event, context);
 +DygraphInteraction.movePan = function(event, g, context) {
 +  context.dragEndX = utils.dragGetX_(event, context);
 +  context.dragEndY = utils.dragGetY_(event, context);
  
    var minDate = context.initialLeftmostDate -
      (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
    }
  
    if (g.getOptionForAxis("logscale", "x")) {
 -    g.dateWindow_ = [ Math.pow(Dygraph.LOG_SCALE, minDate),
 -                      Math.pow(Dygraph.LOG_SCALE, maxDate) ];
 +    g.dateWindow_ = [ Math.pow(utils.LOG_SCALE, minDate),
 +                      Math.pow(utils.LOG_SCALE, maxDate) ];
    } else {
 -    g.dateWindow_ = [minDate, maxDate];    
 +    g.dateWindow_ = [minDate, maxDate];
    }
  
    // y-axis scaling is automatic unless this is a full 2D pan.
          }
        }
        if (g.attributes_.getForAxis("logscale", i)) {
 -        axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
 -                             Math.pow(Dygraph.LOG_SCALE, maxValue) ];
 +        axis.valueRange = [ Math.pow(utils.LOG_SCALE, minValue),
 +                            Math.pow(utils.LOG_SCALE, maxValue) ];
        } else {
 -        axis.valueWindow = [ minValue, maxValue ];
 +        axis.valueRange = [ minValue, maxValue ];
        }
      }
    }
   *     dragStartX/dragStartY/etc. properties). This function modifies the
   *     context.
   */
 -Dygraph.Interaction.endPan = Dygraph.Interaction.maybeTreatMouseOpAsClick;
 +DygraphInteraction.endPan = DygraphInteraction.maybeTreatMouseOpAsClick;
  
  /**
   * Called in response to an interaction model operation that
   *     dragStartX/dragStartY/etc. properties). This function modifies the
   *     context.
   */
 -Dygraph.Interaction.startZoom = function(event, g, context) {
 +DygraphInteraction.startZoom = function(event, g, context) {
    context.isZooming = true;
    context.zoomMoved = false;
  };
   *     dragStartX/dragStartY/etc. properties). This function modifies the
   *     context.
   */
 -Dygraph.Interaction.moveZoom = function(event, g, context) {
 +DygraphInteraction.moveZoom = function(event, g, context) {
    context.zoomMoved = true;
 -  context.dragEndX = Dygraph.dragGetX_(event, context);
 -  context.dragEndY = Dygraph.dragGetY_(event, context);
 +  context.dragEndX = utils.dragGetX_(event, context);
 +  context.dragEndY = utils.dragGetY_(event, context);
  
    var xDelta = Math.abs(context.dragStartX - context.dragEndX);
    var yDelta = Math.abs(context.dragStartY - context.dragEndY);
  
    // drag direction threshold for y axis is twice as large as x axis
 -  context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
 +  context.dragDirection = (xDelta < yDelta / 2) ? utils.VERTICAL : utils.HORIZONTAL;
  
    g.drawZoomRect_(
        context.dragDirection,
   * @param {Event} event
   * @param {Object} context
   */
 -Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
 +DygraphInteraction.treatMouseOpAsClick = function(g, event, context) {
    var clickCallback = g.getFunctionOption('clickCallback');
    var pointClickCallback = g.getFunctionOption('pointClickCallback');
  
   *     dragStartX/dragStartY/etc. properties). This function modifies the
   *     context.
   */
 -Dygraph.Interaction.endZoom = function(event, g, context) {
 +DygraphInteraction.endZoom = function(event, g, context) {
    g.clearZoomRect_();
    context.isZooming = false;
 -  Dygraph.Interaction.maybeTreatMouseOpAsClick(event, g, context);
 +  DygraphInteraction.maybeTreatMouseOpAsClick(event, g, context);
  
    // The zoom rectangle is visibly clipped to the plot area, so its behavior
    // should be as well.
    // See http://code.google.com/p/dygraphs/issues/detail?id=280
    var plotArea = g.getArea();
    if (context.regionWidth >= 10 &&
 -      context.dragDirection == Dygraph.HORIZONTAL) {
 +      context.dragDirection == utils.HORIZONTAL) {
      var left = Math.min(context.dragStartX, context.dragEndX),
          right = Math.max(context.dragStartX, context.dragEndX);
      left = Math.max(left, plotArea.x);
      }
      context.cancelNextDblclick = true;
    } else if (context.regionHeight >= 10 &&
 -             context.dragDirection == Dygraph.VERTICAL) {
 +             context.dragDirection == utils.VERTICAL) {
      var top = Math.min(context.dragStartY, context.dragEndY),
          bottom = Math.max(context.dragStartY, context.dragEndY);
      top = Math.max(top, plotArea.y);
  /**
   * @private
   */
 -Dygraph.Interaction.startTouch = function(event, g, context) {
 +DygraphInteraction.startTouch = function(event, g, context) {
    event.preventDefault();  // touch browsers are all nice.
    if (event.touches.length > 1) {
      // If the user ever puts two fingers down, it's not a double tap.
    context.initialTouches = touches;
  
    if (touches.length == 1) {
-     // This is just a swipe.
+     // This is possbily a touchOVER, save the last touch to check
+     context.lastTouch = event;
+     // or This is just a swipe. 
      context.initialPinchCenter = touches[0];
      context.touchDirections = { x: true, y: true };
    } else if (touches.length >= 2) {
  /**
   * @private
   */
 -Dygraph.Interaction.moveTouch = function(event, g, context) {
 +DygraphInteraction.moveTouch = function(event, g, context) {
    // If the tap moves, then it's definitely not part of a double-tap.
    context.startTimeForDoubleTapMs = null;
+  
+  // clear the last touch if it's doing something else
+   context.lastTouch = null;
  
    var i, touches = [];
    for (i = 0; i < event.touches.length; i++) {
      ];
      didZoom = true;
    }
 -  
 +
    if (context.touchDirections.y) {
      for (i = 0; i < 1  /*g.axes_.length*/; i++) {
        var axis = g.axes_[i];
        if (logscale) {
          // TODO(danvk): implement
        } else {
 -        axis.valueWindow = [
 +        axis.valueRange = [
            c_init.dataY - swipe.dataY + (context.initialRange.y[0] - c_init.dataY) / yScale,
            c_init.dataY - swipe.dataY + (context.initialRange.y[1] - c_init.dataY) / yScale
          ];
  /**
   * @private
   */
 -Dygraph.Interaction.endTouch = function(event, g, context) {
 +DygraphInteraction.endTouch = function(event, g, context) {
    if (event.touches.length !== 0) {
      // this is effectively a "reset"
 -    Dygraph.Interaction.startTouch(event, g, context);
 +    DygraphInteraction.startTouch(event, g, context);
    } else if (event.changedTouches.length == 1) {
      // Could be part of a "double tap"
      // The heuristic here is that it's a double-tap if the two touchend events
          context.doubleTapY && Math.abs(context.doubleTapY - t.screenY) < 50) {
        g.resetZoom();
      } else {
+       
+       if (context.lastTouch !== null){
+         // no double-tap, pan or pinch so it's a touchOVER
+         event.isTouchOver = true;
+         g.mouseMove(event);
+       }
        context.startTimeForDoubleTapMs = now;
        context.doubleTapX = t.screenX;
        context.doubleTapY = t.screenY;
@@@ -604,7 -615,7 +616,7 @@@ var distanceFromInterval = function(x, 
   * edge of the chart. For events in the interior of the chart, this returns zero.
   */
  var distanceFromChart = function(event, g) {
 -  var chartPos = Dygraph.findPos(g.canvas_);
 +  var chartPos = utils.findPos(g.canvas_);
    var box = {
      left: chartPos.x,
      right: chartPos.x + g.canvas_.offsetWidth,
    };
  
    var pt = {
 -    x: Dygraph.pageX(event),
 -    y: Dygraph.pageY(event)
 +    x: utils.pageX(event),
 +    y: utils.pageY(event)
    };
  
    var dx = distanceFromInterval(pt.x, box.left, box.right),
   * this when constructing your own interaction model, e.g.:
   * g.updateOptions( {
   *   interactionModel: {
 - *     mousedown: Dygraph.defaultInteractionModel.mousedown
 + *     mousedown: DygraphInteraction.defaultInteractionModel.mousedown
   *   }
   * } );
   */
 -Dygraph.Interaction.defaultModel = {
 +DygraphInteraction.defaultModel = {
    // Track the beginning of drag events
    mousedown: function(event, g, context) {
      // Right-click should not initiate a zoom.
      context.initializeMouseDown(event, g, context);
  
      if (event.altKey || event.shiftKey) {
 -      Dygraph.startPan(event, g, context);
 +      DygraphInteraction.startPan(event, g, context);
      } else {
 -      Dygraph.startZoom(event, g, context);
 +      DygraphInteraction.startZoom(event, g, context);
      }
  
      // Note: we register mousemove/mouseup on document to allow some leeway for
          // When the mouse moves >200px from the chart edge, cancel the zoom.
          var d = distanceFromChart(event, g);
          if (d < DRAG_EDGE_MARGIN) {
 -          Dygraph.moveZoom(event, g, context);
 +          DygraphInteraction.moveZoom(event, g, context);
          } else {
            if (context.dragEndX !== null) {
              context.dragEndX = null;
            }
          }
        } else if (context.isPanning) {
 -        Dygraph.movePan(event, g, context);
 +        DygraphInteraction.movePan(event, g, context);
        }
      };
      var mouseup = function(event) {
        if (context.isZooming) {
          if (context.dragEndX !== null) {
 -          Dygraph.endZoom(event, g, context);
 +          DygraphInteraction.endZoom(event, g, context);
          } else {
 -          Dygraph.Interaction.maybeTreatMouseOpAsClick(event, g, context);
 +          DygraphInteraction.maybeTreatMouseOpAsClick(event, g, context);
          }
        } else if (context.isPanning) {
 -        Dygraph.endPan(event, g, context);
 +        DygraphInteraction.endPan(event, g, context);
        }
  
 -      Dygraph.removeEvent(document, 'mousemove', mousemove);
 -      Dygraph.removeEvent(document, 'mouseup', mouseup);
 +      utils.removeEvent(document, 'mousemove', mousemove);
 +      utils.removeEvent(document, 'mouseup', mouseup);
        context.destroy();
      };
  
    willDestroyContextMyself: true,
  
    touchstart: function(event, g, context) {
 -    Dygraph.Interaction.startTouch(event, g, context);
 +    DygraphInteraction.startTouch(event, g, context);
    },
    touchmove: function(event, g, context) {
 -    Dygraph.Interaction.moveTouch(event, g, context);
 +    DygraphInteraction.moveTouch(event, g, context);
    },
    touchend: function(event, g, context) {
 -    Dygraph.Interaction.endTouch(event, g, context);
 +    DygraphInteraction.endTouch(event, g, context);
    },
  
    // Disable zooming out if panning.
      // Give plugins a chance to grab this event.
      var e = {
        canvasx: context.dragEndX,
 -      canvasy: context.dragEndY
 +      canvasy: context.dragEndY,
 +      cancelable: true,
      };
      if (g.cascadeEvents_('dblclick', e)) {
        return;
    }
  };
  
 -Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.Interaction.defaultModel;
 +/*
 +Dygraph.DEFAULT_ATTRS.interactionModel = DygraphInteraction.defaultModel;
  
  // old ways of accessing these methods/properties
 -Dygraph.defaultInteractionModel = Dygraph.Interaction.defaultModel;
 -Dygraph.endZoom = Dygraph.Interaction.endZoom;
 -Dygraph.moveZoom = Dygraph.Interaction.moveZoom;
 -Dygraph.startZoom = Dygraph.Interaction.startZoom;
 -Dygraph.endPan = Dygraph.Interaction.endPan;
 -Dygraph.movePan = Dygraph.Interaction.movePan;
 -Dygraph.startPan = Dygraph.Interaction.startPan;
 -
 -Dygraph.Interaction.nonInteractiveModel_ = {
 +Dygraph.defaultInteractionModel = DygraphInteraction.defaultModel;
 +Dygraph.endZoom = DygraphInteraction.endZoom;
 +Dygraph.moveZoom = DygraphInteraction.moveZoom;
 +Dygraph.startZoom = DygraphInteraction.startZoom;
 +Dygraph.endPan = DygraphInteraction.endPan;
 +Dygraph.movePan = DygraphInteraction.movePan;
 +Dygraph.startPan = DygraphInteraction.startPan;
 +*/
 +
 +DygraphInteraction.nonInteractiveModel_ = {
    mousedown: function(event, g, context) {
      context.initializeMouseDown(event, g, context);
    },
 -  mouseup: Dygraph.Interaction.maybeTreatMouseOpAsClick
 +  mouseup: DygraphInteraction.maybeTreatMouseOpAsClick
  };
  
  // Default interaction model when using the range selector.
 -Dygraph.Interaction.dragIsPanInteractionModel = {
 +DygraphInteraction.dragIsPanInteractionModel = {
    mousedown: function(event, g, context) {
      context.initializeMouseDown(event, g, context);
 -    Dygraph.startPan(event, g, context);
 +    DygraphInteraction.startPan(event, g, context);
    },
    mousemove: function(event, g, context) {
      if (context.isPanning) {
 -      Dygraph.movePan(event, g, context);
 +      DygraphInteraction.movePan(event, g, context);
      }
    },
    mouseup: function(event, g, context) {
      if (context.isPanning) {
 -      Dygraph.endPan(event, g, context);
 +      DygraphInteraction.endPan(event, g, context);
      }
    }
  };
  
 -})();
 +export default DygraphInteraction;
diff --combined src/dygraph-utils.js
   * search) and generic DOM-manipulation functions.
   */
  
 -(function() {
 -
  /*global Dygraph:false, Node:false */
  "use strict";
  
 -Dygraph.LOG_SCALE = 10;
 -Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
 +import * as DygraphTickers from './dygraph-tickers';
 +
 +export var LOG_SCALE = 10;
 +export var LN_TEN = Math.log(LOG_SCALE);
  
  /**
   * @private
   * @param {number} x
   * @return {number}
   */
 -Dygraph.log10 = function(x) {
 -  return Math.log(x) / Dygraph.LN_TEN;
 +export var log10 = function(x) {
 +  return Math.log(x) / LN_TEN;
 +};
 +
 +/**
 + * @private
 + * @param {number} r0
 + * @param {number} r1
 + * @param {number} pct
 + * @return {number}
 + */
 +export var logRangeFraction = function(r0, r1, pct) {
 +  // Computing the inverse of toPercentXCoord. The function was arrived at with
 +  // the following steps:
 +  //
 +  // Original calcuation:
 +  // pct = (log(x) - log(xRange[0])) / (log(xRange[1]) - log(xRange[0])));
 +  //
 +  // Multiply both sides by the right-side denominator.
 +  // pct * (log(xRange[1] - log(xRange[0]))) = log(x) - log(xRange[0])
 +  //
 +  // add log(xRange[0]) to both sides
 +  // log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])) = log(x);
 +  //
 +  // Swap both sides of the equation,
 +  // log(x) = log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0]))
 +  //
 +  // Use both sides as the exponent in 10^exp and we're done.
 +  // x = 10 ^ (log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])))
 +
 +  var logr0 = log10(r0);
 +  var logr1 = log10(r1);
 +  var exponent = logr0 + (pct * (logr1 - logr0));
 +  var value = Math.pow(LOG_SCALE, exponent);
 +  return value;
  };
  
  /** A dotted line stroke pattern. */
 -Dygraph.DOTTED_LINE = [2, 2];
 +export var DOTTED_LINE = [2, 2];
  /** A dashed line stroke pattern. */
 -Dygraph.DASHED_LINE = [7, 3];
 +export var DASHED_LINE = [7, 3];
  /** A dot dash stroke pattern. */
 -Dygraph.DOT_DASH_LINE = [7, 2, 2, 2];
 +export var DOT_DASH_LINE = [7, 2, 2, 2];
 +
 +// Directions for panning and zooming. Use bit operations when combined
 +// values are possible.
 +export var HORIZONTAL = 1;
 +export var VERTICAL = 2;
  
  /**
   * Return the 2d context for a dygraph canvas.
   *
   * This method is only exposed for the sake of replacing the function in
 - * automated tests, e.g.
 + * automated tests.
   *
 - * var oldFunc = Dygraph.getContext();
 - * Dygraph.getContext = function(canvas) {
 - *   var realContext = oldFunc(canvas);
 - *   return new Proxy(realContext);
 - * };
   * @param {!HTMLCanvasElement} canvas
   * @return {!CanvasRenderingContext2D}
   * @private
   */
 -Dygraph.getContext = function(canvas) {
 +export var getContext = function(canvas) {
    return /** @type{!CanvasRenderingContext2D}*/(canvas.getContext("2d"));
  };
  
   *     on the event. The function takes one parameter: the event object.
   * @private
   */
 -Dygraph.addEvent = function addEvent(elem, type, fn) {
 +export var addEvent = function addEvent(elem, type, fn) {
    elem.addEventListener(type, fn, false);
  };
  
  /**
 - * Add an event handler. This event handler is kept until the graph is
 - * destroyed with a call to graph.destroy().
 - *
 - * @param {!Node} elem The element to add the event to.
 - * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
 - * @param {function(Event):(boolean|undefined)} fn The function to call
 - *     on the event. The function takes one parameter: the event object.
 - * @private
 - */
 -Dygraph.prototype.addAndTrackEvent = function(elem, type, fn) {
 -  Dygraph.addEvent(elem, type, fn);
 -  this.registeredEvents_.push({ elem : elem, type : type, fn : fn });
 -};
 -
 -/**
   * Remove an event handler.
   * @param {!Node} elem The element to remove the event from.
   * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
   * @param {function(Event):(boolean|undefined)} fn The function to call
   *     on the event. The function takes one parameter: the event object.
 - * @private
   */
 -Dygraph.removeEvent = function(elem, type, fn) {
 +export function removeEvent(elem, type, fn) {
    elem.removeEventListener(type, fn, false);
  };
  
 -Dygraph.prototype.removeTrackedEvents_ = function() {
 -  if (this.registeredEvents_) {
 -    for (var idx = 0; idx < this.registeredEvents_.length; idx++) {
 -      var reg = this.registeredEvents_[idx];
 -      Dygraph.removeEvent(reg.elem, reg.type, reg.fn);
 -    }
 -  }
 -
 -  this.registeredEvents_ = [];
 -};
 -
  /**
   * Cancels further processing of an event. This is useful to prevent default
   * browser actions, e.g. highlighting text on a double-click.
   * @param {!Event} e The event whose normal behavior should be canceled.
   * @private
   */
 -Dygraph.cancelEvent = function(e) {
 +export function cancelEvent(e) {
    e = e ? e : window.event;
    if (e.stopPropagation) {
      e.stopPropagation();
   * @return { string } "rgb(r,g,b)" where r, g and b range from 0-255.
   * @private
   */
 -Dygraph.hsvToRGB = function (hue, saturation, value) {
 +export function hsvToRGB(hue, saturation, value) {
    var red;
    var green;
    var blue;
   * @return {{x:number,y:number}}
   * @private
   */
 -Dygraph.findPos = function(obj) {
 +export function findPos(obj) {
    var p = obj.getBoundingClientRect(),
        w = window,
        d = document.documentElement;
   * @return {number}
   * @private
   */
 -Dygraph.pageX = function(e) {
 +export function pageX(e) {
+   if (e.isTouchOver) return (!e.changedTouches[0] || e.changedTouches[0].pageX < 0) ? 0 : e.changedTouches[0].pageX;
    return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
  };
  
   * @return {number}
   * @private
   */
 -Dygraph.pageY = function(e) {
 +export function pageY(e) {
+   if (e.isTouchOver) return (!e.changedTouches[0] || e.changedTouches[0].pageY < 0) ? 0 : e.changedTouches[0].pageY;
    return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
  };
  
   * @param {!DygraphInteractionContext} context Interaction context object.
   * @return {number} The amount by which the drag has moved to the right.
   */
 -Dygraph.dragGetX_ = function(e, context) {
 -  return Dygraph.pageX(e) - context.px;
 +export function dragGetX_(e, context) {
 +  return pageX(e) - context.px;
  };
  
  /**
   * @param {!DygraphInteractionContext} context Interaction context object.
   * @return {number} The amount by which the drag has moved down.
   */
 -Dygraph.dragGetY_ = function(e, context) {
 -  return Dygraph.pageY(e) - context.py;
 +export function dragGetY_(e, context) {
 +  return pageY(e) - context.py;
  };
  
  /**
   * @return {boolean} Whether the number is zero or NaN.
   * @private
   */
 -Dygraph.isOK = function(x) {
 +export function isOK(x) {
    return !!x && !isNaN(x);
  };
  
   * @return {boolean} Whether the point has numeric x and y.
   * @private
   */
 -Dygraph.isValidPoint = function(p, opt_allowNaNY) {
 +export function isValidPoint(p, opt_allowNaNY) {
    if (!p) return false;  // null or undefined object
    if (p.yval === null) return false;  // missing point
    if (p.x === null || p.x === undefined) return false;
  };
  
  /**
 - * Number formatting function which mimicks the behavior of %g in printf, i.e.
 + * Number formatting function which mimics the behavior of %g in printf, i.e.
   * either exponential or fixed format (without trailing 0s) is used depending on
   * the length of the generated string.  The advantage of this format is that
   * there is a predictable upper bound on the resulting string length,
   * @return {string} A string formatted like %g in printf.  The max generated
   *                  string length should be precision + 6 (e.g 1.123e+300).
   */
 -Dygraph.floatFormat = function(x, opt_precision) {
 +export function floatFormat(x, opt_precision) {
    // Avoid invalid precision values; [1, 21] is the valid range.
    var p = Math.min(Math.max(1, opt_precision || 2), 21);
  
   * @return {string}
   * @private
   */
 -Dygraph.zeropad = function(x) {
 +export function zeropad(x) {
    if (x < 10) return "0" + x; else return "" + x;
  };
  
   * day, hour, minute, second and millisecond) according to local time,
   * and factory method to call the Date constructor with an array of arguments.
   */
 -Dygraph.DateAccessorsLocal = {
 -  getFullYear:     function(d) {return d.getFullYear();},
 -  getMonth:        function(d) {return d.getMonth();},
 -  getDate:         function(d) {return d.getDate();},
 -  getHours:        function(d) {return d.getHours();},
 -  getMinutes:      function(d) {return d.getMinutes();},
 -  getSeconds:      function(d) {return d.getSeconds();},
 -  getMilliseconds: function(d) {return d.getMilliseconds();},
 -  getDay:          function(d) {return d.getDay();},
 +export var DateAccessorsLocal = {
 +  getFullYear:     d => d.getFullYear(),
 +  getMonth:        d => d.getMonth(),
 +  getDate:         d => d.getDate(),
 +  getHours:        d => d.getHours(),
 +  getMinutes:      d => d.getMinutes(),
 +  getSeconds:      d => d.getSeconds(),
 +  getMilliseconds: d => d.getMilliseconds(),
 +  getDay:          d => d.getDay(),
    makeDate:        function(y, m, d, hh, mm, ss, ms) {
      return new Date(y, m, d, hh, mm, ss, ms);
    }
   * day of month, hour, minute, second and millisecond) according to UTC time,
   * and factory method to call the Date constructor with an array of arguments.
   */
 -Dygraph.DateAccessorsUTC = {
 -  getFullYear:     function(d) {return d.getUTCFullYear();},
 -  getMonth:        function(d) {return d.getUTCMonth();},
 -  getDate:         function(d) {return d.getUTCDate();},
 -  getHours:        function(d) {return d.getUTCHours();},
 -  getMinutes:      function(d) {return d.getUTCMinutes();},
 -  getSeconds:      function(d) {return d.getUTCSeconds();},
 -  getMilliseconds: function(d) {return d.getUTCMilliseconds();},
 -  getDay:          function(d) {return d.getUTCDay();},
 +export var DateAccessorsUTC = {
 +  getFullYear:     d => d.getUTCFullYear(),
 +  getMonth:        d => d.getUTCMonth(),
 +  getDate:         d => d.getUTCDate(),
 +  getHours:        d => d.getUTCHours(),
 +  getMinutes:      d => d.getUTCMinutes(),
 +  getSeconds:      d => d.getUTCSeconds(),
 +  getMilliseconds: d => d.getUTCMilliseconds(),
 +  getDay:          d => d.getUTCDay(),
    makeDate:        function(y, m, d, hh, mm, ss, ms) {
      return new Date(Date.UTC(y, m, d, hh, mm, ss, ms));
    }
   * @return {string} A time of the form "HH:MM" or "HH:MM:SS"
   * @private
   */
 -Dygraph.hmsString_ = function(hh, mm, ss) {
 -  var zeropad = Dygraph.zeropad;
 +export function hmsString_(hh, mm, ss, ms) {
    var ret = zeropad(hh) + ":" + zeropad(mm);
    if (ss) {
      ret += ":" + zeropad(ss);
 +    if (ms) {
 +      var str = "" + ms;
 +      ret += "." + ('000'+str).substring(str.length);
 +    }
    }
    return ret;
  };
  /**
   * Convert a JS date (millis since epoch) to a formatted string.
   * @param {number} time The JavaScript time value (ms since epoch)
 - * @param {boolean} utc Wether output UTC or local time
 + * @param {boolean} utc Whether output UTC or local time
   * @return {string} A date of one of these forms:
   *     "YYYY/MM/DD", "YYYY/MM/DD HH:MM" or "YYYY/MM/DD HH:MM:SS"
   * @private
   */
 -Dygraph.dateString_ = function(time, utc) {
 -  var zeropad = Dygraph.zeropad;
 -  var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal;
 +export function dateString_(time, utc) {
 +  var accessors = utc ? DateAccessorsUTC : DateAccessorsLocal;
    var date = new Date(time);
    var y = accessors.getFullYear(date);
    var m = accessors.getMonth(date);
    var hh = accessors.getHours(date);
    var mm = accessors.getMinutes(date);
    var ss = accessors.getSeconds(date);
 +  var ms = accessors.getMilliseconds(date);
    // Get a year string:
    var year = "" + y;
    // Get a 0 padded month string
    var month = zeropad(m + 1);  //months are 0-offset, sigh
    // Get a 0 padded day string
    var day = zeropad(d);
 -  var frac = hh * 3600 + mm * 60 + ss;
 +  var frac = hh * 3600 + mm * 60 + ss + 1e-3 * ms;
    var ret = year + "/" + month + "/" + day;
    if (frac) {
 -    ret += " " + Dygraph.hmsString_(hh, mm, ss);
 +    ret += " " + hmsString_(hh, mm, ss, ms);
    }
    return ret;
  };
   * @return {number} The rounded number
   * @private
   */
 -Dygraph.round_ = function(num, places) {
 +export function round_(num, places) {
    var shift = Math.pow(10, places);
    return Math.round(num * shift)/shift;
  };
   * @return {number} Index of the element, or -1 if it isn't found.
   * @private
   */
 -Dygraph.binarySearch = function(val, arry, abs, low, high) {
 +export function binarySearch(val, arry, abs, low, high) {
    if (low === null || low === undefined ||
        high === null || high === undefined) {
      low = 0;
          return mid;
        }
      }
 -    return Dygraph.binarySearch(val, arry, abs, low, mid - 1);
 +    return binarySearch(val, arry, abs, low, mid - 1);
    } else if (element < val) {
      if (abs < 0) {
        // Accept if element < val, but also if prior element > val.
          return mid;
        }
      }
 -    return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
 +    return binarySearch(val, arry, abs, mid + 1, high);
    }
    return -1;  // can't actually happen, but makes closure compiler happy
  };
   * @return {number} Milliseconds since epoch.
   * @private
   */
 -Dygraph.dateParser = function(dateStr) {
 +export function dateParser(dateStr) {
    var dateStrSlashed;
    var d;
  
    // Issue: http://code.google.com/p/dygraphs/issues/detail?id=255
    if (dateStr.search("-") == -1 ||
        dateStr.search("T") != -1 || dateStr.search("Z") != -1) {
 -    d = Dygraph.dateStrToMillis(dateStr);
 +    d = dateStrToMillis(dateStr);
      if (d && !isNaN(d)) return d;
    }
  
      while (dateStrSlashed.search("-") != -1) {
        dateStrSlashed = dateStrSlashed.replace("-", "/");
      }
 -    d = Dygraph.dateStrToMillis(dateStrSlashed);
 +    d = dateStrToMillis(dateStrSlashed);
    } else if (dateStr.length == 8) {  // e.g. '20090712'
      // TODO(danvk): remove support for this format. It's confusing.
      dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2) + "/" +
          dateStr.substr(6,2);
 -    d = Dygraph.dateStrToMillis(dateStrSlashed);
 +    d = dateStrToMillis(dateStrSlashed);
    } else {
      // Any format that Date.parse will accept, e.g. "2009/07/12" or
      // "2009/07/12 12:34:56"
 -    d = Dygraph.dateStrToMillis(dateStr);
 +    d = dateStrToMillis(dateStr);
    }
  
    if (!d || isNaN(d)) {
   * @return {number} millis since epoch
   * @private
   */
 -Dygraph.dateStrToMillis = function(str) {
 +export function dateStrToMillis(str) {
    return new Date(str).getTime();
  };
  
   * @param {!Object} o
   * @return {!Object}
   */
 -Dygraph.update = function(self, o) {
 +export function update(self, o) {
    if (typeof(o) != 'undefined' && o !== null) {
      for (var k in o) {
        if (o.hasOwnProperty(k)) {
   * @return {!Object}
   * @private
   */
 -Dygraph.updateDeep = function (self, o) {
 +export function updateDeep(self, o) {
    // Taken from http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
    function isNode(o) {
      return (
        if (o.hasOwnProperty(k)) {
          if (o[k] === null) {
            self[k] = null;
 -        } else if (Dygraph.isArrayLike(o[k])) {
 +        } else if (isArrayLike(o[k])) {
            self[k] = o[k].slice();
          } else if (isNode(o[k])) {
            // DOM objects are shallowly-copied.
            if (typeof(self[k]) != 'object' || self[k] === null) {
              self[k] = {};
            }
 -          Dygraph.updateDeep(self[k], o[k]);
 +          updateDeep(self[k], o[k]);
          } else {
            self[k] = o[k];
          }
   * @return {boolean}
   * @private
   */
 -Dygraph.isArrayLike = function(o) {
 +export function isArrayLike(o) {
    var typ = typeof(o);
    if (
        (typ != 'object' && !(typ == 'function' &&
   * @return {boolean}
   * @private
   */
 -Dygraph.isDateLike = function (o) {
 +export function isDateLike(o) {
    if (typeof(o) != "object" || o === null ||
        typeof(o.getTime) != 'function') {
      return false;
   * @return {!Array}
   * @private
   */
 -Dygraph.clone = function(o) {
 +export function clone(o) {
    // TODO(danvk): figure out how MochiKit's version works
    var r = [];
    for (var i = 0; i < o.length; i++) {
 -    if (Dygraph.isArrayLike(o[i])) {
 -      r.push(Dygraph.clone(o[i]));
 +    if (isArrayLike(o[i])) {
 +      r.push(clone(o[i]));
      } else {
        r.push(o[i]);
      }
   * @return {!HTMLCanvasElement}
   * @private
   */
 -Dygraph.createCanvas = function() {
 +export function createCanvas() {
    return document.createElement('canvas');
  };
  
   * @return {number} The ratio of the device pixel ratio and the backing store
   * ratio for the specified context.
   */
 -Dygraph.getContextPixelRatio = function(context) {
 +export function getContextPixelRatio(context) {
    try {
      var devicePixelRatio = window.devicePixelRatio;
      var backingStoreRatio = context.webkitBackingStorePixelRatio ||
  };
  
  /**
 - * Checks whether the user is on an Android browser.
 - * Android does not fully support the <canvas> tag, e.g. w/r/t/ clipping.
 - * @return {boolean}
 - * @private
 - */
 -Dygraph.isAndroid = function() {
 -  return (/Android/).test(navigator.userAgent);
 -};
 -
 -
 -/**
   * TODO(danvk): use @template here when it's better supported for classes.
   * @param {!Array} array
   * @param {number} start
   * @param {function(!Array,?):boolean=} predicate
   * @constructor
   */
 -Dygraph.Iterator = function(array, start, length, predicate) {
 +export function Iterator(array, start, length, predicate) {
    start = start || 0;
    length = length || array.length;
    this.hasNext = true; // Use to identify if there's another element.
  /**
   * @return {Object}
   */
 -Dygraph.Iterator.prototype.next = function() {
 +Iterator.prototype.next = function() {
    if (!this.hasNext) {
      return null;
    }
   *     returned.  If omitted, all elements are accepted.
   * @private
   */
 -Dygraph.createIterator = function(array, start, length, opt_predicate) {
 -  return new Dygraph.Iterator(array, start, length, opt_predicate);
 +export function createIterator(array, start, length, opt_predicate) {
 +  return new Iterator(array, start, length, opt_predicate);
  };
  
  // Shim layer with setTimeout fallback.
  // From: http://paulirish.com/2011/requestanimationframe-for-smart-animating/
  // Should be called with the window context:
  //   Dygraph.requestAnimFrame.call(window, function() {})
 -Dygraph.requestAnimFrame = (function() {
 +export var requestAnimFrame = (function() {
    return window.requestAnimationFrame       ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
   * @param {function()} cleanupFn A function to call after all repeatFn calls.
   * @private
   */
 -Dygraph.repeatAndCleanup = function(repeatFn, maxFrames, framePeriodInMillis,
 +export function repeatAndCleanup(repeatFn, maxFrames, framePeriodInMillis,
      cleanupFn) {
    var frameNumber = 0;
    var previousFrameNumber;
  
    (function loop() {
      if (frameNumber >= maxFrames) return;
 -    Dygraph.requestAnimFrame.call(window, function() {
 +    requestAnimFrame.call(window, function() {
        // Determine which frame to draw based on the delay so far.  Will skip
        // frames if necessary.
        var currentTime = new Date().getTime();
@@@ -826,6 -830,7 +828,6 @@@ var pixelSafeOptions = 
    'annotationDblClickHandler': true,
    'annotationMouseOutHandler': true,
    'annotationMouseOverHandler': true,
 -  'axisLabelColor': true,
    'axisLineColor': true,
    'axisLineWidth': true,
    'clickCallback': true,
    'highlightCallback': true,
    'highlightCircleSize': true,
    'interactionModel': true,
 -  'isZoomedIgnoreProgrammaticZoom': true,
    'labelsDiv': true,
 -  'labelsDivStyles': true,
 -  'labelsDivWidth': true,
    'labelsKMB': true,
    'labelsKMG2': true,
    'labelsSeparateLines': true,
   * @return {boolean} true if the graph needs new points else false.
   * @private
   */
 -Dygraph.isPixelChangingOptionList = function(labels, attrs) {
 +export function isPixelChangingOptionList(labels, attrs) {
    // Assume that we do not require new points.
    // This will change to true if we actually do need new points.
  
    return false;
  };
  
 -Dygraph.Circles = {
 +export var Circles = {
    DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) {
      ctx.beginPath();
      ctx.fillStyle = color;
  };
  
  /**
 - * To create a "drag" interaction, you typically register a mousedown event
 - * handler on the element where the drag begins. In that handler, you register a
 - * mouseup handler on the window to determine when the mouse is released,
 - * wherever that release happens. This works well, except when the user releases
 - * the mouse over an off-domain iframe. In that case, the mouseup event is
 - * handled by the iframe and never bubbles up to the window handler.
 - *
 - * To deal with this issue, we cover iframes with high z-index divs to make sure
 - * they don't capture mouseup.
 - *
 - * Usage:
 - * element.addEventListener('mousedown', function() {
 - *   var tarper = new Dygraph.IFrameTarp();
 - *   tarper.cover();
 - *   var mouseUpHandler = function() {
 - *     ...
 - *     window.removeEventListener(mouseUpHandler);
 - *     tarper.uncover();
 - *   };
 - *   window.addEventListener('mouseup', mouseUpHandler);
 - * };
 - *
 - * @constructor
 - */
 -Dygraph.IFrameTarp = function() {
 -  /** @type {Array.<!HTMLDivElement>} */
 -  this.tarps = [];
 -};
 -
 -/**
 - * Find all the iframes in the document and cover them with high z-index
 - * transparent divs.
 - */
 -Dygraph.IFrameTarp.prototype.cover = function() {
 -  var iframes = document.getElementsByTagName("iframe");
 -  for (var i = 0; i < iframes.length; i++) {
 -    var iframe = iframes[i];
 -    var pos = Dygraph.findPos(iframe),
 -        x = pos.x,
 -        y = pos.y,
 -        width = iframe.offsetWidth,
 -        height = iframe.offsetHeight;
 -
 -    var div = document.createElement("div");
 -    div.style.position = "absolute";
 -    div.style.left = x + 'px';
 -    div.style.top = y + 'px';
 -    div.style.width = width + 'px';
 -    div.style.height = height + 'px';
 -    div.style.zIndex = 999;
 -    document.body.appendChild(div);
 -    this.tarps.push(div);
 -  }
 -};
 -
 -/**
 - * Remove all the iframe covers. You should call this in a mouseup handler.
 - */
 -Dygraph.IFrameTarp.prototype.uncover = function() {
 -  for (var i = 0; i < this.tarps.length; i++) {
 -    this.tarps[i].parentNode.removeChild(this.tarps[i]);
 -  }
 -  this.tarps = [];
 -};
 -
 -/**
   * Determine whether |data| is delimited by CR, CRLF, LF, LFCR.
   * @param {string} data
   * @return {?string} the delimiter that was detected (or null on failure).
   */
 -Dygraph.detectLineDelimiter = function(data) {
 +export function detectLineDelimiter(data) {
    for (var i = 0; i < data.length; i++) {
      var code = data.charAt(i);
      if (code === '\r') {
   * @return {boolean} Whether containee is inside (or equal to) container.
   * @private
   */
 -Dygraph.isNodeContainedBy = function(containee, container) {
 +export function isNodeContainedBy(containee, container) {
    if (container === null || containee === null) {
      return false;
    }
    return (containeeNode === container);
  };
  
 -
  // This masks some numeric issues in older versions of Firefox,
  // where 1.0/Math.pow(10,2) != Math.pow(10,-2).
  /** @type {function(number,number):number} */
 -Dygraph.pow = function(base, exp) {
 +export function pow(base, exp) {
    if (exp < 0) {
      return 1.0 / Math.pow(base, -exp);
    }
  var RGBA_RE = /^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*([01](?:\.\d+)?))?\)$/;
  
  /**
 - * Helper for Dygraph.toRGB_ which parses strings of the form:
 + * Helper for toRGB_ which parses strings of the form:
   * rgb(123, 45, 67)
   * rgba(123, 45, 67, 0.5)
   * @return parsed {r,g,b,a?} tuple or null.
@@@ -1023,7 -1098,7 +1025,7 @@@ function parseRGBA(rgbStr) 
   * @return {{r:number,g:number,b:number,a:number?}} Parsed RGB tuple.
   * @private
   */
 -Dygraph.toRGB_ = function(colorStr) {
 +export function toRGB_(colorStr) {
    // Strategy: First try to parse colorStr directly. This is fast & avoids DOM
    // manipulation.  If that fails (e.g. for named colors like 'red'), then
    // create a hidden DOM element and parse its computed color.
   *     optimization if you have one.
   * @return {boolean} Whether the browser supports canvas.
   */
 -Dygraph.isCanvasSupported = function(opt_canvasElement) {
 +export function isCanvasSupported(opt_canvasElement) {
    try {
      var canvas = opt_canvasElement || document.createElement("canvas");
      canvas.getContext("2d");
   * @param {number=} opt_line_no The line number from which the string comes.
   * @param {string=} opt_line The text of the line from which the string comes.
   */
 -Dygraph.parseFloat_ = function(x, opt_line_no, opt_line) {
 +export function parseFloat_(x, opt_line_no, opt_line) {
    var val = parseFloat(x);
    if (!isNaN(val)) return val;
  
    return null;
  };
  
 -})();
 +
 +// Label constants for the labelsKMB and labelsKMG2 options.
 +// (i.e. '100000' -> '100K')
 +var KMB_LABELS = [ 'K', 'M', 'B', 'T', 'Q' ];
 +var KMG2_BIG_LABELS = [ 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' ];
 +var KMG2_SMALL_LABELS = [ 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y' ];
 +
 +/**
 + * @private
 + * Return a string version of a number. This respects the digitsAfterDecimal
 + * and maxNumberWidth options.
 + * @param {number} x The number to be formatted
 + * @param {Dygraph} opts An options view
 + */
 +export function numberValueFormatter(x, opts) {
 +  var sigFigs = opts('sigFigs');
 +
 +  if (sigFigs !== null) {
 +    // User has opted for a fixed number of significant figures.
 +    return floatFormat(x, sigFigs);
 +  }
 +
 +  var digits = opts('digitsAfterDecimal');
 +  var maxNumberWidth = opts('maxNumberWidth');
 +
 +  var kmb = opts('labelsKMB');
 +  var kmg2 = opts('labelsKMG2');
 +
 +  var label;
 +
 +  // switch to scientific notation if we underflow or overflow fixed display.
 +  if (x !== 0.0 &&
 +      (Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
 +       Math.abs(x) < Math.pow(10, -digits))) {
 +    label = x.toExponential(digits);
 +  } else {
 +    label = '' + round_(x, digits);
 +  }
 +
 +  if (kmb || kmg2) {
 +    var k;
 +    var k_labels = [];
 +    var m_labels = [];
 +    if (kmb) {
 +      k = 1000;
 +      k_labels = KMB_LABELS;
 +    }
 +    if (kmg2) {
 +      if (kmb) console.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
 +      k = 1024;
 +      k_labels = KMG2_BIG_LABELS;
 +      m_labels = KMG2_SMALL_LABELS;
 +    }
 +
 +    var absx = Math.abs(x);
 +    var n = pow(k, k_labels.length);
 +    for (var j = k_labels.length - 1; j >= 0; j--, n /= k) {
 +      if (absx >= n) {
 +        label = round_(x / n, digits) + k_labels[j];
 +        break;
 +      }
 +    }
 +    if (kmg2) {
 +      // TODO(danvk): clean up this logic. Why so different than kmb?
 +      var x_parts = String(x.toExponential()).split('e-');
 +      if (x_parts.length === 2 && x_parts[1] >= 3 && x_parts[1] <= 24) {
 +        if (x_parts[1] % 3 > 0) {
 +          label = round_(x_parts[0] /
 +              pow(10, (x_parts[1] % 3)),
 +              digits);
 +        } else {
 +          label = Number(x_parts[0]).toFixed(2);
 +        }
 +        label += m_labels[Math.floor(x_parts[1] / 3) - 1];
 +      }
 +    }
 +  }
 +
 +  return label;
 +};
 +
 +/**
 + * variant for use as an axisLabelFormatter.
 + * @private
 + */
 +export function numberAxisLabelFormatter(x, granularity, opts) {
 +  return numberValueFormatter.call(this, x, opts);
 +};
 +
 +/**
 + * @type {!Array.<string>}
 + * @private
 + * @constant
 + */
 +var SHORT_MONTH_NAMES_ = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
 +
 +
 +/**
 + * Convert a JS date to a string appropriate to display on an axis that
 + * is displaying values at the stated granularity. This respects the
 + * labelsUTC option.
 + * @param {Date} date The date to format
 + * @param {number} granularity One of the Dygraph granularity constants
 + * @param {Dygraph} opts An options view
 + * @return {string} The date formatted as local time
 + * @private
 + */
 +export function dateAxisLabelFormatter(date, granularity, opts) {
 +  var utc = opts('labelsUTC');
 +  var accessors = utc ? DateAccessorsUTC : DateAccessorsLocal;
 +
 +  var year = accessors.getFullYear(date),
 +      month = accessors.getMonth(date),
 +      day = accessors.getDate(date),
 +      hours = accessors.getHours(date),
 +      mins = accessors.getMinutes(date),
 +      secs = accessors.getSeconds(date),
 +      millis = accessors.getMilliseconds(date);
 +
 +  if (granularity >= DygraphTickers.Granularity.DECADAL) {
 +    return '' + year;
 +  } else if (granularity >= DygraphTickers.Granularity.MONTHLY) {
 +    return SHORT_MONTH_NAMES_[month] + '&#160;' + year;
 +  } else {
 +    var frac = hours * 3600 + mins * 60 + secs + 1e-3 * millis;
 +    if (frac === 0 || granularity >= DygraphTickers.Granularity.DAILY) {
 +      // e.g. '21 Jan' (%d%b)
 +      return zeropad(day) + '&#160;' + SHORT_MONTH_NAMES_[month];
 +    } else if (granularity < DygraphTickers.Granularity.SECONDLY) {
 +      // e.g. 40.310 (meaning 40 seconds and 310 milliseconds)
 +      var str = "" + millis;
 +      return zeropad(secs) + "." + ('000'+str).substring(str.length);
 +    } else if (granularity > DygraphTickers.Granularity.MINUTELY) {
 +      return hmsString_(hours, mins, secs, 0);
 +    } else {
 +      return hmsString_(hours, mins, secs, millis);
 +    }
 +  }
 +};
 +// alias in case anyone is referencing the old method.
 +// Dygraph.dateAxisFormatter = Dygraph.dateAxisLabelFormatter;
 +
 +/**
 + * Return a string version of a JS date for a value label. This respects the
 + * labelsUTC option.
 + * @param {Date} date The date to be formatted
 + * @param {Dygraph} opts An options view
 + * @private
 + */
 +export function dateValueFormatter(d, opts) {
 +  return dateString_(d, opts('labelsUTC'));
 +};