X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=3867b290d06939922947f6c15d5937b719bd8b48;hb=6a4457b403f78ba559550f97330ac25ee4d9629f;hp=fde3c5a9c01f6b8196e95ade6dc3f1493b72f310;hpb=6b8e33dda6c353db62aa58ae0fb31c0d9e705e65;p=dygraphs.git
diff --git a/dygraph.js b/dygraph.js
index fde3c5a..3867b29 100644
--- a/dygraph.js
+++ b/dygraph.js
@@ -1,842 +1,2628 @@
-// Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
-// All Rights Reserved.
+/**
+ * @license
+ * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
/**
* @fileoverview Creates an interactive, zoomable graph based on a CSV file or
- * string. DateGraph can handle multiple series with or without error bars. The
- * date/value ranges will be automatically set. DateGraph uses the
+ * string. Dygraph can handle multiple series with or without error bars. The
+ * date/value ranges will be automatically set. Dygraph uses the
* <canvas> tag, so it only works in FF1.5+.
* @author danvdk@gmail.com (Dan Vanderkam)
Usage:
The CSV file is of the form
+ Date,SeriesA,SeriesB,SeriesC
YYYYMMDD,A1,B1,C1
YYYYMMDD,A2,B2,C2
- If null is passed as the third parameter (series names), then the first line
- of the CSV file is assumed to contain names for each series.
-
If the 'errorBars' option is set in the constructor, the input should be of
the form
-
+ Date,SeriesA,SeriesB,...
YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
If the 'fractions' option is set, the input should be of the form:
+ Date,SeriesA,SeriesB,...
YYYYMMDD,A1/B1,A2/B2,...
YYYYMMDD,A1/B1,A2/B2,...
And error bars will be calculated automatically using a binomial distribution.
- For further documentation and examples, see http://www/~danvk/dg/
+ For further documentation and examples, see http://dygraphs.com/
*/
+/*jshint globalstrict: true */
+/*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager:false */
+"use strict";
+
/**
- * An interactive, zoomable graph
- * @param {String | Function} file A file containing CSV data or a function that
- * returns this data. The expected format for each line is
- * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
- * YYYYMMDD,val1,stddev1,val2,stddev2,...
- * @param {Array.} labels Labels for the data series
+ * Creates an interactive, zoomable chart.
+ *
+ * @constructor
+ * @param {div | String} div A div or the id of a div into which to construct
+ * the chart.
+ * @param {String | Function} file A file containing CSV data or a function
+ * that returns this data. The most basic expected format for each line is
+ * "YYYY/MM/DD,val1,val2,...". For more information, see
+ * http://dygraphs.com/data.html.
* @param {Object} attrs Various other attributes, e.g. errorBars determines
- * whether the input data contains error ranges.
+ * whether the input data contains error ranges. For a complete list of
+ * options, see http://dygraphs.com/options.html.
*/
-DateGraph = function(div, file, labels, attrs) {
- if (arguments.length > 0)
- this.__init__(div, file, labels, attrs);
+var Dygraph = function(div, data, opts, opt_fourth_param) {
+ if (opt_fourth_param !== undefined) {
+ // Old versions of dygraphs took in the series labels as a constructor
+ // parameter. This doesn't make sense anymore, but it's easy to continue
+ // to support this usage.
+ this.warn("Using deprecated four-argument dygraph constructor");
+ this.__old_init__(div, data, opts, opt_fourth_param);
+ } else {
+ this.__init__(div, data, opts);
+ }
};
-DateGraph.NAME = "DateGraph";
-DateGraph.VERSION = "1.1";
-DateGraph.__repr__ = function() {
+Dygraph.NAME = "Dygraph";
+Dygraph.VERSION = "1.2";
+Dygraph.__repr__ = function() {
return "[" + this.NAME + " " + this.VERSION + "]";
};
-DateGraph.toString = function() {
+
+/**
+ * Returns information about the Dygraph class.
+ */
+Dygraph.toString = function() {
return this.__repr__();
};
// Various default values
-DateGraph.DEFAULT_ROLL_PERIOD = 1;
-DateGraph.DEFAULT_WIDTH = 480;
-DateGraph.DEFAULT_HEIGHT = 320;
-DateGraph.DEFAULT_STROKE_WIDTH = 1.0;
-DateGraph.AXIS_LINE_WIDTH = 0.3;
+Dygraph.DEFAULT_ROLL_PERIOD = 1;
+Dygraph.DEFAULT_WIDTH = 480;
+Dygraph.DEFAULT_HEIGHT = 320;
+Dygraph.ANIMATION_STEPS = 10;
+Dygraph.ANIMATION_DURATION = 200;
+
+// These are defined before DEFAULT_ATTRS so that it can refer to them.
/**
- * Initializes the DateGraph. This creates a new DIV and constructs the PlotKit
- * and interaction <canvas> inside of it. See the constructor for details
- * on the parameters.
- * @param {String | Function} file Source data
- * @param {Array.} labels Names of the data series
- * @param {Object} attrs Miscellaneous other options
* @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
+ * @param {String} name The name of the point's data series
+ * @param {Dygraph} g The dygraph object
*/
-DateGraph.prototype.__init__ = function(div, file, labels, attrs) {
- // Copy the important bits into the object
- this.maindiv_ = div;
- this.labels_ = labels;
- this.file_ = file;
- this.rollPeriod_ = attrs.rollPeriod || DateGraph.DEFAULT_ROLL_PERIOD;
- this.previousVerticalX_ = -1;
- this.width_ = parseInt(div.style.width, 10);
- this.height_ = parseInt(div.style.height, 10);
- this.errorBars_ = attrs.errorBars || false;
- this.fractions_ = attrs.fractions || false;
- this.strokeWidth_ = attrs.strokeWidth || DateGraph.DEFAULT_STROKE_WIDTH;
- this.dateWindow_ = attrs.dateWindow || null;
- this.valueRange_ = attrs.valueRange || null;
- this.labelsSeparateLines = attrs.labelsSeparateLines || false;
- this.labelsDiv_ = attrs.labelsDiv || null;
- this.labelsKMB_ = attrs.labelsKMB || false;
- this.minTickSize_ = attrs.minTickSize || 0;
- this.xValueParser_ = attrs.xValueParser || DateGraph.prototype.dateParser;
- this.xValueFormatter_ = attrs.xValueFormatter ||
- DateGraph.prototype.dateString_;
- this.xTicker_ = attrs.xTicker || DateGraph.prototype.dateTicker;
- this.sigma_ = attrs.sigma || 2.0;
- this.wilsonInterval_ = attrs.wilsonInterval || true;
- this.customBars_ = attrs.customBars || false;
- this.attrs_ = attrs;
-
- // Make a note of whether labels will be pulled from the CSV file.
- this.labelsFromCSV_ = (this.labels_ == null);
- if (this.labels_ == null)
- this.labels_ = [];
-
- // Prototype of the callback is "void clickCallback(event, date)"
- this.clickCallback_ = attrs.clickCallback || null;
-
- // Prototype of zoom callback is "void dragCallback(minDate, maxDate)"
- this.zoomCallback_ = attrs.zoomCallback || null;
-
- // Create the containing DIV and other interactive elements
- this.createInterface_();
-
- // Create the PlotKit grapher
- this.layoutOptions_ = { 'errorBars': (this.errorBars_ || this.customBars_),
- 'xOriginIsZero': false };
- MochiKit.Base.update(this.layoutOptions_, attrs);
- this.setColors_(attrs);
+Dygraph.numberValueFormatter = function(x, opts, pt, g) {
+ var sigFigs = opts('sigFigs');
- this.layout_ = new DateGraphLayout(this.layoutOptions_);
-
- this.renderOptions_ = { colorScheme: this.colors_,
- strokeColor: null,
- strokeWidth: this.strokeWidth_,
- axisLabelFontSize: 14,
- axisLineWidth: DateGraph.AXIS_LINE_WIDTH };
- MochiKit.Base.update(this.renderOptions_, attrs);
- this.plotter_ = new DateGraphCanvasRenderer(this.hidden_, this.layout_,
- this.renderOptions_);
+ if (sigFigs !== null) {
+ // User has opted for a fixed number of significant figures.
+ return Dygraph.floatFormat(x, sigFigs);
+ }
- this.createStatusMessage_();
- this.createRollInterface_();
- this.createDragInterface_();
+ var digits = opts('digitsAfterDecimal');
+ var maxNumberWidth = opts('maxNumberWidth');
- // connect(window, 'onload', this, function(e) { this.start_(); });
- this.start_();
+ // 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))) {
+ return x.toExponential(digits);
+ } else {
+ return '' + Dygraph.round_(x, digits);
+ }
};
/**
- * Returns the current rolling period, as set by the user or an option.
- * @return {Number} The number of days in the rolling window
+ * variant for use as an axisLabelFormatter.
+ * @private
*/
-DateGraph.prototype.rollPeriod = function() {
- return this.rollPeriod_;
-}
+Dygraph.numberAxisLabelFormatter = function(x, granularity, opts, g) {
+ return Dygraph.numberValueFormatter(x, opts, g);
+};
/**
- * Generates interface elements for the DateGraph: a containing div, a div to
- * display the current point, and a textbox to adjust the rolling average
- * period.
+ * Convert a JS date (millis since epoch) to YYYY/MM/DD
+ * @param {Number} date The JavaScript date (ms since epoch)
+ * @return {String} A date of the form "YYYY/MM/DD"
* @private
*/
-DateGraph.prototype.createInterface_ = function() {
- // Create the all-enclosing graph div
- var enclosing = this.maindiv_;
-
- this.graphDiv = MochiKit.DOM.DIV( { style: { 'width': this.width_ + "px",
- 'height': this.height_ + "px"
- }});
- appendChildNodes(enclosing, this.graphDiv);
+Dygraph.dateString_ = function(date) {
+ var zeropad = Dygraph.zeropad;
+ var d = new Date(date);
- // Create the canvas to store
- var canvas = MochiKit.DOM.CANVAS;
- this.canvas_ = canvas( { style: { 'position': 'absolute' },
- width: this.width_,
- height: this.height_});
- appendChildNodes(this.graphDiv, this.canvas_);
+ // Get the year:
+ var year = "" + d.getFullYear();
+ // Get a 0 padded month string
+ var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
+ // Get a 0 padded day string
+ var day = zeropad(d.getDate());
- this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
- connect(this.hidden_, 'onmousemove', this, function(e) { this.mouseMove_(e) });
- connect(this.hidden_, 'onmouseout', this, function(e) { this.mouseOut_(e) });
-}
+ var ret = "";
+ var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
+ if (frac) ret = " " + Dygraph.hmsString_(date);
-/**
- * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
- * this particular canvas. All DateGraph work is done on this.canvas_.
- * @param {Object} canvas The DateGraph canvas to over which to overlay the plot
- * @return {Object} The newly-created canvas
- * @private
- */
-DateGraph.prototype.createPlotKitCanvas_ = function(canvas) {
- var h = document.createElement("canvas");
- h.style.position = "absolute";
- h.style.top = canvas.style.top;
- h.style.left = canvas.style.left;
- h.width = this.width_;
- h.height = this.height_;
- MochiKit.DOM.appendChildNodes(this.graphDiv, h);
- return h;
+ return year + "/" + month + "/" + day + ret;
};
/**
- * Generate a set of distinct colors for the data series. This is done with a
- * color wheel. Saturation/Value are customizable, and the hue is
- * equally-spaced around the color wheel. If a custom set of colors is
- * specified, that is used instead.
- * @param {Object} attrs Various attributes, e.g. saturation and value
+ * Convert a JS date to a string appropriate to display on an axis that
+ * is displaying values at the stated granularity.
+ * @param {Date} date The date to format
+ * @param {Number} granularity One of the Dygraph granularity constants
+ * @return {String} The formatted date
* @private
*/
-DateGraph.prototype.setColors_ = function(attrs) {
- var num = this.labels_.length;
- this.colors_ = [];
- if (!attrs.colors) {
- var sat = attrs.colorSaturation || 1.0;
- var val = attrs.colorValue || 0.5;
- for (var i = 1; i <= num; i++) {
- var hue = (1.0*i/(1+num));
- this.colors_.push( MochiKit.Color.Color.fromHSV(hue, sat, val) );
- }
+Dygraph.dateAxisFormatter = function(date, granularity) {
+ if (granularity >= Dygraph.DECADAL) {
+ return date.strftime('%Y');
+ } else if (granularity >= Dygraph.MONTHLY) {
+ return date.strftime('%b %y');
} else {
- for (var i = 0; i < num; i++) {
- var colorStr = attrs.colors[i % attrs.colors.length];
- this.colors_.push( MochiKit.Color.Color.fromString(colorStr) );
+ var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
+ if (frac === 0 || granularity >= Dygraph.DAILY) {
+ return new Date(date.getTime() + 3600*1000).strftime('%d%b');
+ } else {
+ return Dygraph.hmsString_(date.getTime());
}
}
-}
+};
-/**
- * Create the div that contains information on the selected point(s)
- * This goes in the top right of the canvas, unless an external div has already
- * been specified.
- * @private
- */
-DateGraph.prototype.createStatusMessage_ = function(){
- if (!this.labelsDiv_) {
- var divWidth = 250;
- var messagestyle = { "style": {
- "position": "absolute",
- "fontSize": "14px",
- "zIndex": 10,
- "width": divWidth + "px",
- "top": "0px",
- "left": this.width_ - divWidth + "px",
- "background": "white",
- "textAlign": "left",
- "overflow": "hidden"}};
- this.labelsDiv_ = MochiKit.DOM.DIV(messagestyle);
- MochiKit.DOM.appendChildNodes(this.graphDiv, this.labelsDiv_);
+
+// Default attribute values.
+Dygraph.DEFAULT_ATTRS = {
+ highlightCircleSize: 3,
+ highlightSeriesOpts: null,
+ highlightSeriesBackgroundAlpha: 0.5,
+
+ labelsDivWidth: 250,
+ labelsDivStyles: {
+ // TODO(danvk): move defaults from createStatusMessage_ here.
+ },
+ labelsSeparateLines: false,
+ labelsShowZeroValues: true,
+ labelsKMB: false,
+ labelsKMG2: false,
+ showLabelsOnHighlight: true,
+
+ digitsAfterDecimal: 2,
+ maxNumberWidth: 6,
+ sigFigs: null,
+
+ strokeWidth: 1.0,
+ strokeBorderWidth: 0,
+ strokeBorderColor: "white",
+
+ axisTickSize: 3,
+ axisLabelFontSize: 14,
+ xAxisLabelWidth: 50,
+ yAxisLabelWidth: 50,
+ rightGap: 5,
+
+ showRoller: false,
+ xValueParser: Dygraph.dateParser,
+
+ delimiter: ',',
+
+ sigma: 2.0,
+ errorBars: false,
+ fractions: false,
+ wilsonInterval: true, // only relevant if fractions is true
+ customBars: false,
+ fillGraph: false,
+ fillAlpha: 0.15,
+ connectSeparatedPoints: false,
+
+ stackedGraph: false,
+ hideOverlayOnMouseOut: true,
+
+ // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
+ legend: 'onmouseover', // the only relevant value at the moment is 'always'.
+
+ stepPlot: false,
+ avoidMinZero: false,
+
+ // Sizes of the various chart labels.
+ titleHeight: 28,
+ xLabelHeight: 18,
+ yLabelWidth: 18,
+
+ drawXAxis: true,
+ drawYAxis: true,
+ axisLineColor: "black",
+ axisLineWidth: 0.3,
+ gridLineWidth: 0.3,
+ axisLabelColor: "black",
+ axisLabelFont: "Arial", // TODO(danvk): is this implemented?
+ axisLabelWidth: 50,
+ drawYGrid: true,
+ drawXGrid: true,
+ gridLineColor: "rgb(128,128,128)",
+
+ interactionModel: null, // will be set to Dygraph.Interaction.defaultModel
+ animatedZooms: false, // (for now)
+
+ // Range selector options
+ showRangeSelector: false,
+ rangeSelectorHeight: 40,
+ rangeSelectorPlotStrokeColor: "#808FAB",
+ rangeSelectorPlotFillColor: "#A7B1C4",
+
+ // per-axis options
+ axes: {
+ x: {
+ pixelsPerLabel: 60,
+ axisLabelFormatter: Dygraph.dateAxisFormatter,
+ valueFormatter: Dygraph.dateString_,
+ ticker: null // will be set in dygraph-tickers.js
+ },
+ y: {
+ pixelsPerLabel: 30,
+ valueFormatter: Dygraph.numberValueFormatter,
+ axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
+ ticker: null // will be set in dygraph-tickers.js
+ },
+ y2: {
+ pixelsPerLabel: 30,
+ valueFormatter: Dygraph.numberValueFormatter,
+ axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
+ ticker: null // will be set in dygraph-tickers.js
+ }
}
};
-/**
- * Create the text box to adjust the averaging period
- * @return {Object} The newly-created text box
- * @private
- */
-DateGraph.prototype.createRollInterface_ = function() {
- var padding = this.plotter_.options.padding;
- if (typeof this.attrs_.showRoller == 'undefined') {
- this.attrs_.showRoller = false;
- }
- var display = this.attrs_.showRoller ? "block" : "none";
- var textAttr = { "type": "text",
- "size": "2",
- "value": this.rollPeriod_,
- "style": { "position": "absolute",
- "zIndex": 10,
- "top": (this.height_ - 25 - padding.bottom) + "px",
- "left": (padding.left+1) + "px",
- "display": display }
- };
- var roller = MochiKit.DOM.INPUT(textAttr);
- var pa = this.graphDiv;
- MochiKit.DOM.appendChildNodes(pa, roller);
- connect(roller, 'onchange', this,
- function() { this.adjustRoll(roller.value); });
- return roller;
-}
+// Directions for panning and zooming. Use bit operations when combined
+// values are possible.
+Dygraph.HORIZONTAL = 1;
+Dygraph.VERTICAL = 2;
+
+// Installed plugins, in order of precedence (most-general to most-specific).
+// Plugins are installed after they are defined, in plugins/install.js.
+Dygraph.PLUGINS = [
+];
+
+// Used for initializing annotation CSS rules only once.
+Dygraph.addedAnnotationCSS = false;
+
+Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
+ // Labels is no longer a constructor parameter, since it's typically set
+ // directly from the data source. It also conains a name for the x-axis,
+ // which the previous constructor form did not.
+ if (labels !== null) {
+ var new_labels = ["Date"];
+ for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
+ Dygraph.update(attrs, { 'labels': new_labels });
+ }
+ this.__init__(div, file, attrs);
+};
/**
- * Set up all the mouse handlers needed to capture dragging behavior for zoom
- * events. Uses MochiKit.Signal to attach all the event handlers.
+ * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
+ * and context <canvas> inside of it. See the constructor for details.
+ * on the parameters.
+ * @param {Element} div the Element to render the graph into.
+ * @param {String | Function} file Source data
+ * @param {Object} attrs Miscellaneous other options
* @private
*/
-DateGraph.prototype.createDragInterface_ = function() {
- var self = this;
+Dygraph.prototype.__init__ = function(div, file, attrs) {
+ // Hack for IE: if we're using excanvas and the document hasn't finished
+ // loading yet (and hence may not have initialized whatever it needs to
+ // initialize), then keep calling this routine periodically until it has.
+ if (/MSIE/.test(navigator.userAgent) && !window.opera &&
+ typeof(G_vmlCanvasManager) != 'undefined' &&
+ document.readyState != 'complete') {
+ var self = this;
+ setTimeout(function() { self.__init__(div, file, attrs); }, 100);
+ return;
+ }
- // Tracks whether the mouse is down right now
- var mouseDown = false;
- var dragStartX = null;
- var dragStartY = null;
- var dragEndX = null;
- var dragEndY = null;
- var prevEndX = null;
-
- // Utility function to convert page-wide coordinates to canvas coords
- var px = 0;
- var py = 0;
- var getX = function(e) { return e.mouse().page.x - px };
- var getY = function(e) { return e.mouse().page.y - py };
-
- // Draw zoom rectangles when the mouse is down and the user moves around
- connect(this.hidden_, 'onmousemove', function(event) {
- if (mouseDown) {
- dragEndX = getX(event);
- dragEndY = getY(event);
-
- self.drawZoomRect_(dragStartX, dragEndX, prevEndX);
- prevEndX = dragEndX;
- }
- });
+ // Support two-argument constructor
+ if (attrs === null || attrs === undefined) { attrs = {}; }
- // Track the beginning of drag events
- connect(this.hidden_, 'onmousedown', function(event) {
- mouseDown = true;
- px = PlotKit.Base.findPosX(self.canvas_);
- py = PlotKit.Base.findPosY(self.canvas_);
- dragStartX = getX(event);
- dragStartY = getY(event);
- });
+ attrs = Dygraph.mapLegacyOptions_(attrs);
- // If the user releases the mouse button during a drag, but not over the
- // canvas, then it doesn't count as a zooming action.
- connect(document, 'onmouseup', this, function(event) {
- if (mouseDown) {
- mouseDown = false;
- dragStartX = null;
- dragStartY = null;
+ if (!div) {
+ Dygraph.error("Constructing dygraph with a non-existent div!");
+ return;
+ }
+
+ this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != 'undefined';
+
+ // Copy the important bits into the object
+ // TODO(danvk): most of these should just stay in the attrs_ dictionary.
+ this.maindiv_ = div;
+ this.file_ = file;
+ this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
+ this.previousVerticalX_ = -1;
+ this.fractions_ = attrs.fractions || false;
+ this.dateWindow_ = attrs.dateWindow || null;
+
+ this.is_initial_draw_ = true;
+ this.annotations_ = [];
+
+ // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
+ this.zoomed_x_ = false;
+ this.zoomed_y_ = false;
+
+ // Clear the div. This ensure that, if multiple dygraphs are passed the same
+ // div, then only one will be drawn.
+ div.innerHTML = "";
+
+ // For historical reasons, the 'width' and 'height' options trump all CSS
+ // rules _except_ for an explicit 'width' or 'height' on the div.
+ // As an added convenience, if the div has zero height (like does
+ // without any styles), then we use a default height/width.
+ if (div.style.width === '' && attrs.width) {
+ div.style.width = attrs.width + "px";
+ }
+ if (div.style.height === '' && attrs.height) {
+ div.style.height = attrs.height + "px";
+ }
+ if (div.style.height === '' && div.clientHeight === 0) {
+ div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
+ if (div.style.width === '') {
+ div.style.width = Dygraph.DEFAULT_WIDTH + "px";
}
- });
+ }
+ // these will be zero if the dygraph's div is hidden.
+ this.width_ = div.clientWidth;
+ this.height_ = div.clientHeight;
+
+ // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
+ if (attrs.stackedGraph) {
+ attrs.fillGraph = true;
+ // TODO(nikhilk): Add any other stackedGraph checks here.
+ }
- // Temporarily cancel the dragging event when the mouse leaves the graph
- connect(this.hidden_, 'onmouseout', this, function(event) {
- if (mouseDown) {
- dragEndX = null;
- dragEndY = null;
+ // Dygraphs has many options, some of which interact with one another.
+ // To keep track of everything, we maintain two sets of options:
+ //
+ // this.user_attrs_ only options explicitly set by the user.
+ // this.attrs_ defaults, options derived from user_attrs_, data.
+ //
+ // Options are then accessed this.attr_('attr'), which first looks at
+ // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
+ // defaults without overriding behavior that the user specifically asks for.
+ this.user_attrs_ = {};
+ Dygraph.update(this.user_attrs_, attrs);
+
+ // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
+ this.attrs_ = {};
+ Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS);
+
+ this.boundaryIds_ = [];
+ this.setIndexByName_ = {};
+ this.datasetIndex_ = [];
+
+ // Create the containing DIV and other interactive elements
+ this.createInterface_();
+
+ // Activate plugins.
+ this.plugins_ = [];
+ for (var i = 0; i < Dygraph.PLUGINS.length; i++) {
+ var plugin = Dygraph.PLUGINS[i];
+ var pluginInstance = new plugin();
+ var pluginDict = {
+ plugin: pluginInstance,
+ events: {},
+ options: {},
+ pluginOptions: {}
+ };
+
+ var handlers = pluginInstance.activate(this);
+ for (var eventName in handlers) {
+ pluginDict.events[eventName] = handlers[eventName];
}
- });
- // If the mouse is released on the canvas during a drag event, then it's a
- // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
- connect(this.hidden_, 'onmouseup', this, function(event) {
- if (mouseDown) {
- mouseDown = false;
- dragEndX = getX(event);
- dragEndY = getY(event);
- var regionWidth = Math.abs(dragEndX - dragStartX);
- var regionHeight = Math.abs(dragEndY - dragStartY);
-
- if (regionWidth < 2 && regionHeight < 2 &&
- self.clickCallback_ != null &&
- self.lastx_ != undefined) {
- self.clickCallback_(event, new Date(self.lastx_));
- }
+ this.plugins_.push(pluginDict);
+ }
- if (regionWidth >= 10) {
- self.doZoom_(Math.min(dragStartX, dragEndX),
- Math.max(dragStartX, dragEndX));
+ // At this point, plugins can no longer register event handlers.
+ // Construct a map from event -> ordered list of [callback, plugin].
+ this.eventListeners_ = {};
+ for (var i = 0; i < this.plugins_.length; i++) {
+ var plugin_dict = this.plugins_[i];
+ for (var eventName in plugin_dict.events) {
+ if (!plugin_dict.events.hasOwnProperty(eventName)) continue;
+ var callback = plugin_dict.events[eventName];
+
+ var pair = [plugin_dict.plugin, callback];
+ if (!(eventName in this.eventListeners_)) {
+ this.eventListeners_[eventName] = [pair];
} else {
- self.canvas_.getContext("2d").clearRect(0, 0,
- self.canvas_.width,
- self.canvas_.height);
+ this.eventListeners_[eventName].push(pair);
}
-
- dragStartX = null;
- dragStartY = null;
}
- });
+ }
- // Double-clicking zooms back out
- connect(this.hidden_, 'ondblclick', this, function(event) {
- self.dateWindow_ = null;
- self.drawGraph_(self.rawData_);
- var minDate = self.rawData_[0][0];
- var maxDate = self.rawData_[self.rawData_.length - 1][0];
- if (self.zoomCallback_) {
- self.zoomCallback_(minDate, maxDate);
- }
- });
+ this.start_();
};
/**
- * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
- * up any previous zoom rectangles that were drawn. This could be optimized to
- * avoid extra redrawing, but it's tricky to avoid interactions with the status
- * dots.
- * @param {Number} startX The X position where the drag started, in canvas
- * coordinates.
- * @param {Number} endX The current X position of the drag, in canvas coords.
- * @param {Number} prevEndX The value of endX on the previous call to this
- * function. Used to avoid excess redrawing
+ * Triggers a cascade of events to the various plugins which are interested in them.
+ * Returns true if the "default behavior" should be performed, i.e. if none of
+ * the event listeners called event.preventDefault().
* @private
*/
-DateGraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
- var ctx = this.canvas_.getContext("2d");
-
- // Clean up from the previous rect if necessary
- if (prevEndX) {
- ctx.clearRect(Math.min(startX, prevEndX), 0,
- Math.abs(startX - prevEndX), this.height_);
- }
-
- // Draw a light-grey rectangle to show the new viewing area
- if (endX && startX) {
- ctx.fillStyle = "rgba(128,128,128,0.33)";
- ctx.fillRect(Math.min(startX, endX), 0,
- Math.abs(endX - startX), this.height_);
+Dygraph.prototype.cascadeEvents_ = function(name, extra_props) {
+ if (!name in this.eventListeners_) return true;
+
+ // QUESTION: can we use objects & prototypes to speed this up?
+ var e = {
+ dygraph: this,
+ cancelable: false,
+ defaultPrevented: false,
+ preventDefault: function() {
+ if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event.";
+ e.defaultPrevented = true;
+ },
+ propagationStopped: false,
+ stopPropagation: function() {
+ e.propagationStopped = true;
+ }
+ };
+ Dygraph.update(e, extra_props);
+
+ var callback_plugin_pairs = this.eventListeners_[name];
+ if (callback_plugin_pairs) {
+ for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) {
+ var plugin = callback_plugin_pairs[i][0];
+ var callback = callback_plugin_pairs[i][1];
+ callback.call(plugin, e);
+ if (e.propagationStopped) break;
+ }
}
+ return e.defaultPrevented;
};
/**
- * Zoom to something containing [lowX, highX]. These are pixel coordinates
- * in the canvas. The exact zoom window may be slightly larger if there are no
- * data points near lowX or highX. This function redraws the graph.
- * @param {Number} lowX The leftmost pixel value that should be visible.
- * @param {Number} highX The rightmost pixel value that should be visible.
- * @private
+ * Returns the zoomed status of the chart for one or both axes.
+ *
+ * Axis is an optional parameter. Can be set to 'x' or 'y'.
+ *
+ * The zoomed status for an axis is set whenever a user zooms using the mouse
+ * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom
+ * option is also specified).
*/
-DateGraph.prototype.doZoom_ = function(lowX, highX) {
- // Find the earliest and latest dates contained in this canvasx range.
- var points = this.layout_.points;
- var minDate = null;
- var maxDate = null;
- // Find the nearest [minDate, maxDate] that contains [lowX, highX]
- for (var i = 0; i < points.length; i++) {
- var cx = points[i].canvasx;
- var x = points[i].xval;
- if (cx < lowX && (minDate == null || x > minDate)) minDate = x;
- if (cx > highX && (maxDate == null || x < maxDate)) maxDate = x;
- }
- // Use the extremes if either is missing
- if (minDate == null) minDate = points[0].xval;
- if (maxDate == null) maxDate = points[points.length-1].xval;
+Dygraph.prototype.isZoomed = function(axis) {
+ if (axis == null) return this.zoomed_x_ || this.zoomed_y_;
+ if (axis === 'x') return this.zoomed_x_;
+ if (axis === 'y') return this.zoomed_y_;
+ throw "axis parameter is [" + axis + "] must be null, 'x' or 'y'.";
+};
- this.dateWindow_ = [minDate, maxDate];
- this.drawGraph_(this.rawData_);
- if (this.zoomCallback_) {
- this.zoomCallback_(minDate, maxDate);
- }
+/**
+ * Returns information about the Dygraph object, including its containing ID.
+ */
+Dygraph.prototype.toString = function() {
+ var maindiv = this.maindiv_;
+ var id = (maindiv && maindiv.id) ? maindiv.id : maindiv;
+ return "[Dygraph " + id + "]";
};
/**
- * When the mouse moves in the canvas, display information about a nearby data
- * point and draw dots over those points in the data series. This function
- * takes care of cleanup of previously-drawn dots.
- * @param {Object} event The mousemove event from the browser.
* @private
+ * Returns the value of an option. This may be set by the user (either in the
+ * constructor or by calling updateOptions) or by dygraphs, and may be set to a
+ * per-series value.
+ * @param { String } name The name of the option, e.g. 'rollPeriod'.
+ * @param { String } [seriesName] The name of the series to which the option
+ * will be applied. If no per-series value of this option is available, then
+ * the global value is returned. This is optional.
+ * @return { ... } The value of the option.
*/
-DateGraph.prototype.mouseMove_ = function(event) {
- var canvasx = event.mouse().page.x - PlotKit.Base.findPosX(this.hidden_);
- var points = this.layout_.points;
-
- var lastx = -1;
- var lasty = -1;
-
- // Loop through all the points and find the date nearest to our current
- // location.
- var minDist = 1e+100;
- var idx = -1;
- for (var i = 0; i < points.length; i++) {
- var dist = Math.abs(points[i].canvasx - canvasx);
- if (dist > minDist) break;
- minDist = dist;
- idx = i;
+Dygraph.prototype.attr_ = function(name, seriesName) {
+//
+ if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
+ this.error('Must include options reference JS for testing');
+ } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
+ this.error('Dygraphs is using property ' + name + ', which has no entry ' +
+ 'in the Dygraphs.OPTIONS_REFERENCE listing.');
+ // Only log this error once.
+ Dygraph.OPTIONS_REFERENCE[name] = true;
}
- if (idx >= 0) lastx = points[idx].xval;
- // Check that you can really highlight the last day's data
- if (canvasx > points[points.length-1].canvasx)
- lastx = points[points.length-1].xval;
-
- // Extract the points we've selected
- var selPoints = [];
- for (var i = 0; i < points.length; i++) {
- if (points[i].xval == lastx) {
- selPoints.push(points[i]);
+//
+
+ var sources = [];
+ sources.push(this.attrs_);
+ if (this.user_attrs_) {
+ sources.push(this.user_attrs_);
+ if (seriesName) {
+ if (this.user_attrs_.hasOwnProperty(seriesName)) {
+ sources.push(this.user_attrs_[seriesName]);
+ }
+ if (seriesName === this.highlightSet_ &&
+ this.user_attrs_.hasOwnProperty('highlightSeriesOpts')) {
+ sources.push(this.user_attrs_['highlightSeriesOpts']);
+ }
}
}
- // Clear the previously drawn vertical, if there is one
- var circleSize = 3;
- var ctx = this.canvas_.getContext("2d");
- if (this.previousVerticalX_ >= 0) {
- var px = this.previousVerticalX_;
- ctx.clearRect(px - circleSize - 1, 0, 2 * circleSize + 2, this.height_);
+ var ret = null;
+ for (var i = sources.length - 1; i >= 0; --i) {
+ var source = sources[i];
+ if (source.hasOwnProperty(name)) {
+ ret = source[name];
+ break;
+ }
}
+ return ret;
+};
- if (selPoints.length > 0) {
- var canvasx = selPoints[0].canvasx;
+/**
+ * Returns the current value for an option, as set in the constructor or via
+ * updateOptions. You may pass in an (optional) series name to get per-series
+ * values for the option.
+ *
+ * All values returned by this method should be considered immutable. If you
+ * modify them, there is no guarantee that the changes will be honored or that
+ * dygraphs will remain in a consistent state. If you want to modify an option,
+ * use updateOptions() instead.
+ *
+ * @param { String } name The name of the option (e.g. 'strokeWidth')
+ * @param { String } [opt_seriesName] Series name to get per-series values.
+ * @return { ... } The value of the option.
+ */
+Dygraph.prototype.getOption = function(name, opt_seriesName) {
+ return this.attr_(name, opt_seriesName);
+};
- // Set the status message to indicate the selected point(s)
- var replace = this.xValueFormatter_(lastx) + ":";
- var clen = this.colors_.length;
- for (var i = 0; i < selPoints.length; i++) {
- if (this.labelsSeparateLines) {
- replace += " ";
- }
- var point = selPoints[i];
- replace += " "
- + point.name + ":"
- + this.round_(point.yval, 2);
+/**
+ * @private
+ * @param String} axis The name of the axis (i.e. 'x', 'y' or 'y2')
+ * @return { ... } A function mapping string -> option value
+ */
+Dygraph.prototype.optionsViewForAxis_ = function(axis) {
+ var self = this;
+ return function(opt) {
+ var axis_opts = self.user_attrs_.axes;
+ if (axis_opts && axis_opts[axis] && axis_opts[axis][opt]) {
+ return axis_opts[axis][opt];
+ }
+ // user-specified attributes always trump defaults, even if they're less
+ // specific.
+ if (typeof(self.user_attrs_[opt]) != 'undefined') {
+ return self.user_attrs_[opt];
}
- this.labelsDiv_.innerHTML = replace;
-
- // Save last x position for callbacks.
- this.lastx_ = lastx;
- // Draw colored circles over the center of each selected point
- ctx.save()
- for (var i = 0; i < selPoints.length; i++) {
- ctx.beginPath();
- ctx.fillStyle = this.colors_[i%clen].toRGBString();
- ctx.arc(canvasx, selPoints[i%clen].canvasy, circleSize, 0, 360, false);
- ctx.fill();
+ axis_opts = self.attrs_.axes;
+ if (axis_opts && axis_opts[axis] && axis_opts[axis][opt]) {
+ return axis_opts[axis][opt];
}
- ctx.restore();
+ // check old-style axis options
+ // TODO(danvk): add a deprecation warning if either of these match.
+ if (axis == 'y' && self.axes_[0].hasOwnProperty(opt)) {
+ return self.axes_[0][opt];
+ } else if (axis == 'y2' && self.axes_[1].hasOwnProperty(opt)) {
+ return self.axes_[1][opt];
+ }
+ return self.attr_(opt);
+ };
+};
- this.previousVerticalX_ = canvasx;
- }
+/**
+ * Returns the current rolling period, as set by the user or an option.
+ * @return {Number} The number of points in the rolling window
+ */
+Dygraph.prototype.rollPeriod = function() {
+ return this.rollPeriod_;
};
/**
- * The mouse has left the canvas. Clear out whatever artifacts remain
- * @param {Object} event the mouseout event from the browser.
- * @private
+ * Returns the currently-visible x-range. This can be affected by zooming,
+ * panning or a call to updateOptions.
+ * Returns a two-element array: [left, right].
+ * If the Dygraph has dates on the x-axis, these will be millis since epoch.
*/
-DateGraph.prototype.mouseOut_ = function(event) {
- // Get rid of the overlay data
- var ctx = this.canvas_.getContext("2d");
- ctx.clearRect(0, 0, this.width_, this.height_);
- this.labelsDiv_.innerHTML = "";
+Dygraph.prototype.xAxisRange = function() {
+ return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
};
/**
- * Return a string version of the hours, minutes and seconds portion of a date.
- * @param {Number} date The JavaScript date (ms since epoch)
- * @return {String} A time of the form "HH:MM:SS"
- * @private
+ * Returns the lower- and upper-bound x-axis values of the
+ * data set.
*/
-DateGraph.prototype.hmsString_ = function(date) {
- var zeropad = function(x) {
- if (x < 10) return "0" + x; else return "" + x;
- };
- var d = new Date(date);
- if (d.getSeconds()) {
- return zeropad(d.getHours()) + ":" +
- zeropad(d.getMinutes()) + ":" +
- zeropad(d.getSeconds());
- } else if (d.getMinutes()) {
- return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
- } else {
- return zeropad(d.getHours());
+Dygraph.prototype.xAxisExtremes = function() {
+ var left = this.rawData_[0][0];
+ var right = this.rawData_[this.rawData_.length - 1][0];
+ return [left, right];
+};
+
+/**
+ * Returns the currently-visible y-range for an axis. This can be affected by
+ * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
+ * called with no arguments, returns the range of the first axis.
+ * Returns a two-element array: [bottom, top].
+ */
+Dygraph.prototype.yAxisRange = function(idx) {
+ if (typeof(idx) == "undefined") idx = 0;
+ if (idx < 0 || idx >= this.axes_.length) {
+ return null;
}
-}
+ var axis = this.axes_[idx];
+ return [ axis.computedValueRange[0], axis.computedValueRange[1] ];
+};
/**
- * Convert a JS date (millis since epoch) to YYYY/MM/DD
- * @param {Number} date The JavaScript date (ms since epoch)
- * @return {String} A date of the form "YYYY/MM/DD"
- * @private
+ * Returns the currently-visible y-ranges for each axis. This can be affected by
+ * zooming, panning, calls to updateOptions, etc.
+ * Returns an array of [bottom, top] pairs, one for each y-axis.
*/
-DateGraph.prototype.dateString_ = function(date) {
- var zeropad = function(x) {
- if (x < 10) return "0" + x; else return "" + x;
- };
- var d = new Date(date);
+Dygraph.prototype.yAxisRanges = function() {
+ var ret = [];
+ for (var i = 0; i < this.axes_.length; i++) {
+ ret.push(this.yAxisRange(i));
+ }
+ return ret;
+};
- // Get the year:
- var year = "" + d.getFullYear();
- // Get a 0 padded month string
- var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
- // Get a 0 padded day string
- var day = zeropad(d.getDate());
+// TODO(danvk): use these functions throughout dygraphs.
+/**
+ * Convert from data coordinates to canvas/div X/Y coordinates.
+ * If specified, do this conversion for the coordinate system of a particular
+ * axis. Uses the first axis by default.
+ * Returns a two-element array: [X, Y]
+ *
+ * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
+ * instead of toDomCoords(null, y, axis).
+ */
+Dygraph.prototype.toDomCoords = function(x, y, axis) {
+ return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
+};
- var ret = "";
- var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
- if (frac) ret = " " + this.hmsString_(date);
+/**
+ * Convert from data x coordinates to canvas/div X coordinate.
+ * If specified, do this conversion for the coordinate system of a particular
+ * axis.
+ * Returns a single value or null if x is null.
+ */
+Dygraph.prototype.toDomXCoord = function(x) {
+ if (x === null) {
+ return null;
+ }
- return year + "/" + month + "/" + day + ret;
+ var area = this.plotter_.area;
+ var xRange = this.xAxisRange();
+ return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
};
/**
- * Round a number to the specified number of digits past the decimal point.
- * @param {Number} num The number to round
- * @param {Number} places The number of decimals to which to round
- * @return {Number} The rounded number
- * @private
+ * Convert from data x coordinates to canvas/div Y coordinate and optional
+ * axis. Uses the first axis by default.
+ *
+ * returns a single value or null if y is null.
*/
-DateGraph.prototype.round_ = function(num, places) {
- var shift = Math.pow(10, places);
- return Math.round(num * shift)/shift;
+Dygraph.prototype.toDomYCoord = function(y, axis) {
+ var pct = this.toPercentYCoord(y, axis);
+
+ if (pct === null) {
+ return null;
+ }
+ var area = this.plotter_.area;
+ return area.y + pct * area.h;
};
/**
- * Fires when there's data available to be graphed.
- * @param {String} data Raw CSV data to be plotted
- * @private
+ * Convert from canvas/div coords to data coordinates.
+ * If specified, do this conversion for the coordinate system of a particular
+ * axis. Uses the first axis by default.
+ * Returns a two-element array: [X, Y].
+ *
+ * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
+ * instead of toDataCoords(null, y, axis).
*/
-DateGraph.prototype.loadedEvent_ = function(data) {
- this.rawData_ = this.parseCSV_(data);
- this.drawGraph_(this.rawData_);
+Dygraph.prototype.toDataCoords = function(x, y, axis) {
+ return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
};
-DateGraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
- "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
-DateGraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
+/**
+ * Convert from canvas/div x coordinate to data coordinate.
+ *
+ * If x is null, this returns null.
+ */
+Dygraph.prototype.toDataXCoord = function(x) {
+ if (x === null) {
+ return null;
+ }
+
+ var area = this.plotter_.area;
+ var xRange = this.xAxisRange();
+ return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
+};
/**
- * Add ticks on the x-axis representing years, months, quarters, weeks, or days
- * @private
+ * Convert from canvas/div y coord to value.
+ *
+ * If y is null, this returns null.
+ * if axis is null, this uses the first axis.
*/
-DateGraph.prototype.addXTicks_ = function() {
- // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
- var startDate, endDate;
- if (this.dateWindow_) {
- startDate = this.dateWindow_[0];
- endDate = this.dateWindow_[1];
- } else {
- startDate = this.rawData_[0][0];
- endDate = this.rawData_[this.rawData_.length - 1][0];
- }
-
- var xTicks = this.xTicker_(startDate, endDate);
- this.layout_.updateOptions({xTicks: xTicks});
-}
-
-/**
- * Add ticks to the x-axis based on a date range.
- * @param {Number} startDate Start of the date window (millis since epoch)
- * @param {Number} endDate End of the date window (millis since epoch)
- * @return {Array.