*/
/*jshint globalstrict: true */
-/*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager:false */
+/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false */
"use strict";
/**
* whether the input data contains error ranges. For a complete list of
* options, see http://dygraphs.com/options.html.
*/
-var Dygraph = function(div, data, opts) {
- if (arguments.length > 0) {
- if (arguments.length == 4) {
- // 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, arguments[2], arguments[3]);
- } else {
- this.__init__(div, data, opts);
- }
+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);
}
};
Dygraph.DEFAULT_WIDTH = 480;
Dygraph.DEFAULT_HEIGHT = 320;
-Dygraph.ANIMATION_STEPS = 10;
+// For max 60 Hz. animation:
+Dygraph.ANIMATION_STEPS = 12;
Dygraph.ANIMATION_DURATION = 200;
// These are defined before DEFAULT_ATTRS so that it can refer to them.
}
};
+/**
+ * Standard plotters. These may be used by clients.
+ * Available plotters are:
+ * - Dygraph.Plotters.linePlotter: draws central lines (most common)
+ * - Dygraph.Plotters.errorPlotter: draws error bars
+ * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph)
+ *
+ * By default, the plotter is [fillPlotter, errorPlotter, linePlotter].
+ * This causes all the lines to be drawn over all the fills/error bars.
+ */
+Dygraph.Plotters = DygraphCanvasRenderer._Plotters;
+
// Default attribute values.
Dygraph.DEFAULT_ATTRS = {
stepPlot: false,
avoidMinZero: false,
+ drawAxesAtZero: false,
// Sizes of the various chart labels.
titleHeight: 28,
rangeSelectorPlotStrokeColor: "#808FAB",
rangeSelectorPlotFillColor: "#A7B1C4",
+ // The ordering here ensures that central lines always appear above any
+ // fill bars/error bars.
+ plotter: [
+ Dygraph.Plotters.fillPlotter,
+ Dygraph.Plotters.errorPlotter,
+ Dygraph.Plotters.linePlotter
+ ],
+
+ plugins: [ ],
+
// per-axis options
axes: {
x: {
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;
attrs = Dygraph.mapLegacyOptions_(attrs);
+ if (typeof(div) == 'string') {
+ div = document.getElementById(div);
+ }
+
if (!div) {
Dygraph.error("Constructing dygraph with a non-existent div!");
return;
// TODO(nikhilk): Add any other stackedGraph checks here.
}
+ // DEPRECATION WARNING: All option processing should be moved from
+ // attrs_ and user_attrs_ to options_, which holds all this information.
+ //
// Dygraphs has many options, some of which interact with one another.
// To keep track of everything, we maintain two sets of options:
//
this.setIndexByName_ = {};
this.datasetIndex_ = [];
+ this.registeredEvents_ = [];
+ this.eventListeners_ = {};
+
+ this.attributes_ = new DygraphOptions(this);
+
// Create the containing DIV and other interactive elements
this.createInterface_();
+ // Activate plugins.
+ this.plugins_ = [];
+ var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins'));
+ for (var i = 0; i < plugins.length; i++) {
+ var Plugin = plugins[i];
+ var pluginInstance = new Plugin();
+ var pluginDict = {
+ plugin: pluginInstance,
+ events: {},
+ options: {},
+ pluginOptions: {}
+ };
+
+ var handlers = pluginInstance.activate(this);
+ for (var eventName in handlers) {
+ // TODO(danvk): validate eventName.
+ pluginDict.events[eventName] = handlers[eventName];
+ }
+
+ this.plugins_.push(pluginDict);
+ }
+
+ // At this point, plugins can no longer register event handlers.
+ // Construct a map from event -> ordered list of [callback, plugin].
+ 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 {
+ this.eventListeners_[eventName].push(pair);
+ }
+ }
+ }
+
+ this.createDragInterface_();
+
this.start_();
};
/**
+ * 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
+ */
+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;
+};
+
+/**
* 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).
+ * or when the dateWindow or valueRange are updated (unless the
+ * isZoomedIgnoreProgrammaticZoom option is also specified).
*/
Dygraph.prototype.isZoomed = function(axis) {
- if (axis == null) return this.zoomed_x_ || this.zoomed_y_;
+ if (axis === null || axis === undefined) {
+ 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'.";
Dygraph.OPTIONS_REFERENCE[name] = true;
}
// </REMOVE_FOR_COMBINED>
+ return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name);
+};
- 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']);
- }
- }
- }
+/**
+ * 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);
+};
- 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;
+Dygraph.prototype.getOptionForAxis = function(name, axis) {
+ return this.attributes_.getForAxis(name, axis);
};
/**
var yRange = this.yAxisRange(axis);
var pct;
- if (!this.axes_[axis].logscale) {
+ var logscale = this.attributes_.getForAxis("logscale", axis);
+ if (!logscale) {
// yRange[1] - y is unit distance from the bottom.
// yRange[1] - yRange[0] is the scale of the range.
// (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
this.graphDiv = document.createElement("div");
this.graphDiv.style.width = this.width_ + "px";
this.graphDiv.style.height = this.height_ + "px";
+ // TODO(danvk): any other styles that are useful to set here?
+ this.graphDiv.style.textAlign = 'left'; // This is a CSS "reset"
enclosing.appendChild(this.graphDiv);
// Create the canvas for interactive parts of the chart.
this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
- if (this.attr_('showRangeSelector')) {
- // The range selector must be created here so that its canvases and contexts get created here.
- // For some reason, if the canvases and contexts don't get created here, things don't work in IE.
- // The range selector also sets xAxisHeight in order to reserve space.
- this.rangeSelector_ = new DygraphRangeSelector(this);
- }
-
// The interactive parts of the graph are drawn on top of the chart.
this.graphDiv.appendChild(this.hidden_);
this.graphDiv.appendChild(this.canvas_);
// Create the grapher
this.layout_ = new DygraphLayout(this);
- if (this.rangeSelector_) {
- // This needs to happen after the graph canvases are added to the div and the layout object is created.
- this.rangeSelector_.addToGraph(this.graphDiv, this.layout_);
- }
-
var dygraph = this;
-
- this.mouseMoveHandler = function(e) {
- dygraph.mouseMove_(e);
+
+ this.mouseMoveHandler_ = function(e) {
+ dygraph.mouseMove_(e);
};
- Dygraph.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler);
-
- this.mouseOutHandler = function(e) {
- dygraph.mouseOut_(e);
+
+ this.mouseOutHandler_ = function(e) {
+ // The mouse has left the chart if:
+ // 1. e.target is inside the chart
+ // 2. e.relatedTarget is outside the chart
+ var target = e.target || e.fromElement;
+ var relatedTarget = e.relatedTarget || e.toElement;
+ if (Dygraph.isElementContainedBy(target, dygraph.graphDiv) &&
+ !Dygraph.isElementContainedBy(relatedTarget, dygraph.graphDiv)) {
+ dygraph.mouseOut_(e);
+ }
};
- Dygraph.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler);
- this.createStatusMessage_();
- this.createDragInterface_();
+ this.addEvent(window, 'mouseout', this.mouseOutHandler_);
+ this.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
- this.resizeHandler = function(e) {
- dygraph.resize();
- };
+ // Don't recreate and register the resize handler on subsequent calls.
+ // This happens when the graph is resized.
+ if (!this.resizeHandler_) {
+ this.resizeHandler_ = function(e) {
+ dygraph.resize();
+ };
- // Update when the window is resized.
- // TODO(danvk): drop frames depending on complexity of the chart.
- Dygraph.addEvent(window, 'resize', this.resizeHandler);
+ // Update when the window is resized.
+ // TODO(danvk): drop frames depending on complexity of the chart.
+ this.addEvent(window, 'resize', this.resizeHandler_);
+ }
};
/**
node.removeChild(node.firstChild);
}
};
-
- // remove mouse event handlers
- Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler);
- Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler);
+
+ 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_ = [];
+
+ // remove mouse event handlers (This may not be necessary anymore)
+ Dygraph.removeEvent(window, 'mouseout', this.mouseOutHandler_);
+ Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
+ Dygraph.removeEvent(this.mouseEventElement_, 'mouseup', this.mouseUpHandler_);
+
+ // remove window handlers
+ Dygraph.removeEvent(window,'resize',this.resizeHandler_);
+ this.resizeHandler_ = null;
+
removeRecursive(this.maindiv_);
var nullOut = function(obj) {
}
}
};
- // remove event handlers
- Dygraph.removeEvent(window,'resize',this.resizeHandler);
- this.resizeHandler = null;
// These may not all be necessary, but it can't hurt...
nullOut(this.layout_);
nullOut(this.plotter_);
* @private
*/
Dygraph.prototype.setColors_ = function() {
- var num = this.attr_("labels").length - 1;
+ var labels = this.getLabels();
+ var num = labels.length - 1;
this.colors_ = [];
+ this.colorsMap_ = {};
var colors = this.attr_('colors');
var i;
if (!colors) {
// alternate colors for high contrast.
var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
var hue = (1.0 * idx/ (1 + num));
- this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
+ var colorStr = Dygraph.hsvToRGB(hue, sat, val);
+ this.colors_.push(colorStr);
+ this.colorsMap_[labels[i]] = colorStr;
}
} else {
for (i = 0; i < num; i++) {
if (!this.visibility()[i]) continue;
var colorStr = colors[i % colors.length];
this.colors_.push(colorStr);
+ this.colorsMap_[labels[1 + i]] = colorStr;
}
}
-
- this.plotter_.setColors(this.colors_);
};
/**
* Return the list of colors. This is either the list of colors passed in the
* attributes or the autogenerated list of rgb(r,g,b) strings.
+ * This does not return colors for invisible series.
* @return {Array<string>} The list of colors.
*/
Dygraph.prototype.getColors = function() {
};
/**
- * 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
+ * Returns a few attributes of a series, i.e. its color, its visibility, which
+ * axis it's assigned to, and its column in the original data.
+ * Returns null if the series does not exist.
+ * Otherwise, returns an object with column, visibility, color and axis properties.
+ * The "axis" property will be set to 1 for y1 and 2 for y2.
+ * The "column" property can be fed back into getValue(row, column) to get
+ * values for this series.
*/
-Dygraph.prototype.createStatusMessage_ = function() {
- var userLabelsDiv = this.user_attrs_.labelsDiv;
- if (userLabelsDiv && null !== userLabelsDiv &&
- (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
- this.user_attrs_.labelsDiv = document.getElementById(userLabelsDiv);
- }
- if (!this.attr_("labelsDiv")) {
- var divWidth = this.attr_('labelsDivWidth');
- var messagestyle = {
- "position": "absolute",
- "fontSize": "14px",
- "zIndex": 10,
- "width": divWidth + "px",
- "top": "0px",
- "left": (this.width_ - divWidth - 2) + "px",
- "background": "white",
- "textAlign": "left",
- "overflow": "hidden"};
- Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
- var div = document.createElement("div");
- div.className = "dygraph-legend";
- for (var name in messagestyle) {
- if (messagestyle.hasOwnProperty(name)) {
- try {
- div.style[name] = messagestyle[name];
- } catch (e) {
- this.warn("You are using unsupported css properties for your browser in labelsDivStyles");
- }
- }
+Dygraph.prototype.getPropertiesForSeries = function(series_name) {
+ var idx = -1;
+ var labels = this.getLabels();
+ for (var i = 1; i < labels.length; i++) {
+ if (labels[i] == series_name) {
+ idx = i;
+ break;
}
- this.graphDiv.appendChild(div);
- this.attrs_.labelsDiv = div;
}
-};
+ if (idx == -1) return null;
-/**
- * Position the labels div so that:
- * - its right edge is flush with the right edge of the charting area
- * - its top edge is flush with the top edge of the charting area
- * @private
- */
-Dygraph.prototype.positionLabelsDiv_ = function() {
- // Don't touch a user-specified labelsDiv.
- if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
-
- var area = this.plotter_.area;
- var div = this.attr_("labelsDiv");
- div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
- div.style.top = area.y + "px";
+ return {
+ name: series_name,
+ column: idx,
+ visible: this.visibility()[idx - 1],
+ color: this.colorsMap_[series_name],
+ axis: 1 + this.attributes_.axisForSeries(series_name)
+ };
};
/**
prevEndX: null, // pixel coordinates
prevEndY: null, // pixel coordinates
prevDragDirection: null,
+ cancelNextDblclick: false, // see comment in dygraph-interaction-model.js
// The value on the left side of the graph when a pan operation starts.
initialLeftmostDate: null,
boundedDates: null, // [minDate, maxDate]
boundedValues: null, // [[minValue, maxValue] ...]
- initializeMouseDown: function(event, g, context) {
+ // We cover iframes during mouse interactions. See comments in
+ // dygraph-utils.js for more info on why this is a good idea.
+ tarp: new Dygraph.IFrameTarp(),
+
+ // contextB is the same thing as this context object but renamed.
+ initializeMouseDown: function(event, g, contextB) {
// prevents mouse drags from selecting page text.
if (event.preventDefault) {
event.preventDefault(); // Firefox, Chrome, etc.
event.cancelBubble = true;
}
- context.px = Dygraph.findPosX(g.canvas_);
- context.py = Dygraph.findPosY(g.canvas_);
- context.dragStartX = g.dragGetX_(event, context);
- context.dragStartY = g.dragGetY_(event, context);
+ contextB.px = Dygraph.findPosX(g.canvas_);
+ contextB.py = Dygraph.findPosY(g.canvas_);
+ contextB.dragStartX = g.dragGetX_(event, contextB);
+ contextB.dragStartY = g.dragGetY_(event, contextB);
+ contextB.cancelNextDblclick = false;
+ contextB.tarp.cover();
}
};
for (var eventName in interactionModel) {
if (!interactionModel.hasOwnProperty(eventName)) continue;
- Dygraph.addEvent(this.mouseEventElement_, eventName,
+ this.addEvent(this.mouseEventElement_, eventName,
bindHandler(interactionModel[eventName]));
}
+ // unregister the handler on subsequent calls.
+ // This happens when the graph is resized.
+ if (this.mouseUpHandler_) {
+ Dygraph.removeEvent(document, 'mouseup', this.mouseUpHandler_);
+ }
+
// If the user releases the mouse button during a drag, but not over the
// canvas, then it doesn't count as a zooming action.
- Dygraph.addEvent(document, 'mouseup', function(event) {
+ this.mouseUpHandler_ = function(event) {
if (context.isZooming || context.isPanning) {
context.isZooming = false;
context.dragStartX = null;
delete self.axes_[i].dragValueRange;
}
}
- });
+
+ context.tarp.uncover();
+ };
+
+ this.addEvent(document, 'mouseup', this.mouseUpHandler_);
};
/**
/**
* Reset the zoom to the original view coordinates. This is the same as
* double-clicking on the graph.
- *
- * @private
*/
-Dygraph.prototype.doUnzoom_ = function() {
+Dygraph.prototype.resetZoom = function() {
var dirty = false, dirtyX = false, dirtyY = false;
if (this.dateWindow_ !== null) {
dirty = true;
newValueRanges = [];
for (i = 0; i < this.axes_.length; i++) {
var axis = this.axes_[i];
- newValueRanges.push(axis.valueRange != null ? axis.valueRange : axis.extremeRange);
+ newValueRanges.push((axis.valueRange !== null &&
+ axis.valueRange !== undefined) ?
+ axis.valueRange : axis.extremeRange);
}
}
* @private
*/
Dygraph.prototype.findClosestRow = function(domX) {
- var minDistX = null;
- var idx = -1;
- var points = this.layout_.points;
- var l = points.length;
- for (var i = 0; i < l; i++) {
- var point = points[i];
- if (point === null) continue;
- var dist = Math.abs(point.canvasx - domX);
- if (minDistX === null || dist < minDistX) {
- minDistX = dist;
- idx = i;
+ var minDistX = Infinity;
+ var pointIdx = -1, setIdx = -1;
+ var sets = this.layout_.points;
+ for (var i = 0; i < sets.length; i++) {
+ var points = sets[i];
+ var len = points.length;
+ for (var j = 0; j < len; j++) {
+ var point = points[j];
+ if (!Dygraph.isValidPoint(point, true)) continue;
+ var dist = Math.abs(point.canvasx - domX);
+ if (dist < minDistX) {
+ minDistX = dist;
+ setIdx = i;
+ pointIdx = j;
+ }
}
}
- return this.idxToRow_(idx);
+
+ // TODO(danvk): remove this function; it's trivial and has only one use.
+ return this.idxToRow_(setIdx, pointIdx);
};
/**
* @private
*/
Dygraph.prototype.findClosestPoint = function(domX, domY) {
- var minDist = null;
+ var minDist = Infinity;
var idx = -1;
- var points = this.layout_.points;
var dist, dx, dy, point, closestPoint, closestSeries;
- for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
- var first = this.layout_.setPointsOffsets[setIdx];
- var len = this.layout_.setPointsLengths[setIdx];
- for (var i = 0; i < len; ++i) {
- var point = points[first + i];
- if (point === null) continue;
+ for ( var setIdx = this.layout_.datasets.length - 1 ; setIdx >= 0 ; --setIdx ) {
+ var points = this.layout_.points[setIdx];
+ for (var i = 0; i < points.length; ++i) {
+ var point = points[i];
+ if (!Dygraph.isValidPoint(point)) continue;
dx = point.canvasx - domX;
dy = point.canvasy - domY;
dist = dx * dx + dy * dy;
- if (minDist === null || dist < minDist) {
+ if (dist < minDist) {
minDist = dist;
closestPoint = point;
closestSeries = setIdx;
}
var name = this.layout_.setNames[closestSeries];
return {
- row: idx,
+ row: idx + this.getLeftBoundary_(),
seriesName: name,
point: closestPoint
};
*/
Dygraph.prototype.findStackedPoint = function(domX, domY) {
var row = this.findClosestRow(domX);
- var points = this.layout_.points;
+ var boundary = this.getLeftBoundary_();
+ var rowIdx = row - boundary;
var closestPoint, closestSeries;
for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
- var first = this.layout_.setPointsOffsets[setIdx];
- var len = this.layout_.setPointsLengths[setIdx];
- if (row >= len) continue;
- var p1 = points[first + row];
+ var points = this.layout_.points[setIdx];
+ if (rowIdx >= points.length) continue;
+ var p1 = points[rowIdx];
+ if (!Dygraph.isValidPoint(p1)) continue;
var py = p1.canvasy;
- if (domX > p1.canvasx && row + 1 < len) {
+ if (domX > p1.canvasx && rowIdx + 1 < points.length) {
// interpolate series Y value using next point
- var p2 = points[first + row + 1];
- var dx = p2.canvasx - p1.canvasx;
- if (dx > 0) {
- var r = (domX - p1.canvasx) / dx;
- py += r * (p2.canvasy - p1.canvasy);
+ var p2 = points[rowIdx + 1];
+ if (Dygraph.isValidPoint(p2)) {
+ var dx = p2.canvasx - p1.canvasx;
+ if (dx > 0) {
+ var r = (domX - p1.canvasx) / dx;
+ py += r * (p2.canvasy - p1.canvasy);
+ }
}
- } else if (domX < p1.canvasx && row > 0) {
+ } else if (domX < p1.canvasx && rowIdx > 0) {
// interpolate series Y value using previous point
- var p0 = points[first + row - 1];
- var dx = p1.canvasx - p0.canvasx;
- if (dx > 0) {
- var r = (p1.canvasx - domX) / dx;
- py += r * (p0.canvasy - p1.canvasy);
+ var p0 = points[rowIdx - 1];
+ if (Dygraph.isValidPoint(p0)) {
+ var dx = p1.canvasx - p0.canvasx;
+ if (dx > 0) {
+ var r = (p1.canvasx - domX) / dx;
+ py += r * (p0.canvasy - p1.canvasy);
+ }
}
}
// Stop if the point (domX, py) is above this series' upper edge
- if (setIdx == 0 || py < domY) {
+ if (setIdx === 0 || py < domY) {
closestPoint = p1;
closestSeries = setIdx;
}
Dygraph.prototype.mouseMove_ = function(event) {
// This prevents JS errors when mousing over the canvas before data loads.
var points = this.layout_.points;
- if (points === undefined) return;
+ if (points === undefined || points === null) return;
var canvasCoords = this.eventToDomCoords(event);
var canvasx = canvasCoords[0];
var highlightSeriesOpts = this.attr_("highlightSeriesOpts");
var selectionChanged = false;
- if (highlightSeriesOpts) {
+ if (highlightSeriesOpts && !this.isSeriesLocked()) {
var closest;
if (this.attr_("stackedGraph")) {
closest = this.findStackedPoint(canvasx, canvasy);
};
/**
- * Transforms layout_.points index into data row number.
- * @param int layout_.points index
- * @return int row number, or -1 if none could be found.
+ * Fetch left offset from first defined boundaryIds record (see bug #236).
* @private
*/
-Dygraph.prototype.idxToRow_ = function(idx) {
- if (idx < 0) return -1;
-
- // make sure that you get the boundaryIds record which is also defined (see bug #236)
- var boundaryIdx = -1;
+Dygraph.prototype.getLeftBoundary_ = function() {
for (var i = 0; i < this.boundaryIds_.length; i++) {
if (this.boundaryIds_[i] !== undefined) {
- boundaryIdx = i;
- break;
- }
- }
- if (boundaryIdx < 0) return -1;
- for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
- var set = this.layout_.datasets[setIdx];
- if (idx < set.length) {
- return this.boundaryIds_[boundaryIdx][0] + idx;
- }
- idx -= set.length;
- }
- return -1;
-};
-
-/**
- * @private
- * Generates legend html dash for any stroke pattern. It will try to scale the
- * pattern to fit in 1em width. Or if small enough repeat the partern for 1em
- * width.
- * @param strokePattern The pattern
- * @param color The color of the series.
- * @param oneEmWidth The width in pixels of 1em in the legend.
- */
-Dygraph.prototype.generateLegendDashHTML_ = function(strokePattern, color, oneEmWidth) {
- var dash = "";
- var i, j, paddingLeft, marginRight;
- var strokePixelLength = 0, segmentLoop = 0;
- var normalizedPattern = [];
- var loop;
- // IE 7,8 fail at these divs, so they get boring legend, have not tested 9.
- var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
- if(isIE) {
- return "—";
- }
- if (!strokePattern || strokePattern.length <= 1) {
- // Solid line
- dash = "<div style=\"display: inline-block; position: relative; " +
- "bottom: .5ex; padding-left: 1em; height: 1px; " +
- "border-bottom: 2px solid " + color + ";\"></div>";
- } else {
- // Compute the length of the pixels including the first segment twice,
- // since we repeat it.
- for (i = 0; i <= strokePattern.length; i++) {
- strokePixelLength += strokePattern[i%strokePattern.length];
- }
-
- // See if we can loop the pattern by itself at least twice.
- loop = Math.floor(oneEmWidth/(strokePixelLength-strokePattern[0]));
- if (loop > 1) {
- // This pattern fits at least two times, no scaling just convert to em;
- for (i = 0; i < strokePattern.length; i++) {
- normalizedPattern[i] = strokePattern[i]/oneEmWidth;
- }
- // Since we are repeating the pattern, we don't worry about repeating the
- // first segment in one draw.
- segmentLoop = normalizedPattern.length;
- } else {
- // If the pattern doesn't fit in the legend we scale it to fit.
- loop = 1;
- for (i = 0; i < strokePattern.length; i++) {
- normalizedPattern[i] = strokePattern[i]/strokePixelLength;
- }
- // For the scaled patterns we do redraw the first segment.
- segmentLoop = normalizedPattern.length+1;
- }
- // Now make the pattern.
- for (j = 0; j < loop; j++) {
- for (i = 0; i < segmentLoop; i+=2) {
- // The padding is the drawn segment.
- paddingLeft = normalizedPattern[i%normalizedPattern.length];
- if (i < strokePattern.length) {
- // The margin is the space segment.
- marginRight = normalizedPattern[(i+1)%normalizedPattern.length];
- } else {
- // The repeated first segment has no right margin.
- marginRight = 0;
- }
- dash += "<div style=\"display: inline-block; position: relative; " +
- "bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " +
- paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color +
- ";\"></div>";
- }
+ return this.boundaryIds_[i][0];
}
}
- return dash;
+ return 0;
};
/**
+ * Transforms layout_.points index into data row number.
+ * @param int layout_.points index
+ * @return int row number, or -1 if none could be found.
* @private
- * Generates HTML for the legend which is displayed when hovering over the
- * chart. If no selected points are specified, a default legend is returned
- * (this may just be the empty string).
- * @param { Number } [x] The x-value of the selected points.
- * @param { [Object] } [sel_points] List of selected points for the given
- * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
- * @param { Number } [oneEmWidth] The pixel width for 1em in the legend.
- */
-Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) {
- // If no points are selected, we display a default legend. Traditionally,
- // this has been blank. But a better default would be a conventional legend,
- // which provides essential information for a non-interactive chart.
- var html, sepLines, i, c, dash, strokePattern;
- if (typeof(x) === 'undefined') {
- if (this.attr_('legend') != 'always') return '';
-
- sepLines = this.attr_('labelsSeparateLines');
- var labels = this.attr_('labels');
- html = '';
- for (i = 1; i < labels.length; i++) {
- if (!this.visibility()[i - 1]) continue;
- c = this.plotter_.colors[labels[i]];
- if (html !== '') html += (sepLines ? '<br/>' : ' ');
- strokePattern = this.attr_("strokePattern", labels[i]);
- dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth);
- html += "<span style='font-weight: bold; color: " + c + ";'>" + dash +
- " " + labels[i] + "</span>";
- }
- return html;
- }
-
- var xOptView = this.optionsViewForAxis_('x');
- var xvf = xOptView('valueFormatter');
- html = xvf(x, xOptView, this.attr_('labels')[0], this) + ":";
-
- var yOptViews = [];
- var num_axes = this.numAxes();
- for (i = 0; i < num_axes; i++) {
- yOptViews[i] = this.optionsViewForAxis_('y' + (i ? 1 + i : ''));
- }
- var showZeros = this.attr_("labelsShowZeroValues");
- sepLines = this.attr_("labelsSeparateLines");
- for (i = 0; i < this.selPoints_.length; i++) {
- var pt = this.selPoints_[i];
- if (pt.yval === 0 && !showZeros) continue;
- if (!Dygraph.isOK(pt.canvasy)) continue;
- if (sepLines) html += "<br/>";
-
- var yOptView = yOptViews[this.seriesToAxisMap_[pt.name]];
- var fmtFunc = yOptView('valueFormatter');
- c = this.plotter_.colors[pt.name];
- var yval = fmtFunc(pt.yval, yOptView, pt.name, this);
-
- var cls = (pt.name == this.highlightSet_) ? " class='highlight'" : "";
- // TODO(danvk): use a template string here and make it an attribute.
- html += "<span" + cls + ">" + " <b><span style='color: " + c + ";'>" + pt.name +
- "</span></b>:" + yval + "</span>";
- }
- return html;
-};
+ */
+Dygraph.prototype.idxToRow_ = function(setIdx, rowIdx) {
+ if (rowIdx < 0) return -1;
-/**
- * @private
- * Displays information about the selected points in the legend. If there is no
- * selection, the legend will be cleared.
- * @param { Number } [x] The x-value of the selected points.
- * @param { [Object] } [sel_points] List of selected points for the given
- * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
- */
-Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
- var labelsDiv = this.attr_("labelsDiv");
- var sizeSpan = document.createElement('span');
- // Calculates the width of 1em in pixels for the legend.
- sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
- labelsDiv.appendChild(sizeSpan);
- var oneEmWidth=sizeSpan.offsetWidth;
-
- var html = this.generateLegendHTML_(x, sel_points, oneEmWidth);
- if (labelsDiv !== null) {
- labelsDiv.innerHTML = html;
- } else {
- if (typeof(this.shown_legend_error_) == 'undefined') {
- this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
- this.shown_legend_error_ = true;
- }
- }
+ var boundary = this.getLeftBoundary_();
+ return boundary + rowIdx;
+ // for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+ // var set = this.layout_.datasets[setIdx];
+ // if (idx < set.length) {
+ // return boundary + idx;
+ // }
+ // idx -= set.length;
+ // }
+ // return -1;
};
Dygraph.prototype.animateSelection_ = function(direction) {
var totalSteps = 10;
var millis = 30;
- if (this.fadeLevel === undefined) {
- this.fadeLevel = 0;
- this.animateId = 0;
- }
+ if (this.fadeLevel === undefined) this.fadeLevel = 0;
+ if (this.animateId === undefined) this.animateId = 0;
var start = this.fadeLevel;
var steps = direction < 0 ? start : totalSteps - start;
if (steps <= 0) {
* @private
*/
Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
+ /*var defaultPrevented = */
+ this.cascadeEvents_('select', {
+ selectedX: this.lastx_,
+ selectedPoints: this.selPoints_
+ });
+ // TODO(danvk): use defaultPrevented here?
+
// Clear the previously drawn vertical, if there is one
var i;
var ctx = this.canvas_ctx_;
ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')';
ctx.fillRect(0, 0, this.width_, this.height_);
}
- var setIdx = this.datasetIndexFromSetName_(this.highlightSet_);
- this.plotter_._drawLine(ctx, setIdx);
+
+ // Redraw only the highlighted series in the interactive canvas (not the
+ // static plot canvas, which is where series are usually drawn).
+ this.plotter_._renderLineChart(this.highlightSet_, ctx);
} else if (this.previousVerticalX_ >= 0) {
// Determine the maximum highlight circle size.
var maxCircleSize = 0;
}
if (this.selPoints_.length > 0) {
- // Set the status message to indicate the selected point(s)
- if (this.attr_('showLabelsOnHighlight')) {
- this.setLegendHTML_(this.lastx_, this.selPoints_);
- }
-
// Draw colored circles over the center of each selected point
var canvasx = this.selPoints_[0].canvasx;
ctx.save();
* hover dots on the chart). Set to false to clear any selection.
* @param { seriesName } optional series name to highlight that series with the
* the highlightSeriesOpts setting.
+ * @param { locked } optional If true, keep seriesName selected when mousing
+ * over the graph, disabling closest-series highlighting. Call clearSelection()
+ * to unlock it.
*/
-Dygraph.prototype.setSelection = function(row, opt_seriesName) {
+Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) {
// Extract the points we've selected
this.selPoints_ = [];
- var pos = 0;
if (row !== false) {
- for (var i = 0; i < this.boundaryIds_.length; i++) {
- if (this.boundaryIds_[i] !== undefined) {
- row -= this.boundaryIds_[i][0];
- break;
- }
- }
+ row -= this.getLeftBoundary_();
}
var changed = false;
for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
var set = this.layout_.datasets[setIdx];
if (row < set.length) {
- var point = this.layout_.points[pos+row];
+ var point = this.layout_.points[setIdx][row];
if (this.attr_("stackedGraph")) {
- point = this.layout_.unstackPointAtIndex(pos+row);
+ point = this.layout_.unstackPointAtIndex(setIdx, row);
}
- this.selPoints_.push(point);
+ if (point.yval !== null) this.selPoints_.push(point);
}
- pos += set.length;
}
} else {
if (this.lastRow_ >= 0) changed = true;
this.highlightSet_ = opt_seriesName;
}
+ if (opt_locked !== undefined) {
+ this.lockedSet_ = opt_locked;
+ }
+
if (changed) {
this.updateSelection_(undefined);
}
this.attr_("unhighlightCallback")(event);
}
- if (this.attr_("hideOverlayOnMouseOut")) {
+ if (this.attr_("hideOverlayOnMouseOut") && !this.lockedSet_) {
this.clearSelection();
}
};
* the mouse over the chart).
*/
Dygraph.prototype.clearSelection = function() {
+ this.cascadeEvents_('deselect', {});
+
+ this.lockedSet_ = false;
// Get rid of the overlay data
if (this.fadeLevel) {
this.animateSelection_(-1);
}
this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
this.fadeLevel = 0;
- this.setLegendHTML_();
this.selPoints_ = [];
this.lastx_ = -1;
this.lastRow_ = -1;
return -1;
}
- for (var row=0; row<this.layout_.points.length; row++ ) {
- if (this.layout_.points[row].x == this.selPoints_[0].x) {
- return row + this.boundaryIds_[0][0];
+ for (var setIdx = 0; setIdx < this.layout_.points.length; setIdx++) {
+ var points = this.layout_.points[setIdx];
+ for (var row = 0; row < points.length; row++) {
+ if (points[row].x == this.selPoints_[0].x) {
+ return row + this.getLeftBoundary_();
+ }
}
}
return -1;
};
+/**
+ * Returns the name of the currently-highlighted series.
+ * Only available when the highlightSeriesOpts option is in use.
+ */
Dygraph.prototype.getHighlightSeries = function() {
return this.highlightSet_;
};
/**
+ * Returns true if the currently-highlighted series was locked
+ * via setSelection(..., seriesName, true).
+ */
+Dygraph.prototype.isSeriesLocked = function() {
+ return this.lockedSet_;
+};
+
+/**
* Fires when there's data available to be graphed.
* @param {String} data Raw CSV data to be plotted
* @private
// With custom bars, maxY is the max of the high values.
for (j = 0; j < series.length; j++) {
y = series[j][1][0];
- if (!y) continue;
+ if (y === null || isNaN(y)) continue;
var low = y - series[j][1][1];
var high = y + series[j][1][2];
if (low > y) low = y; // this can happen with custom bars,
Dygraph.prototype.predraw_ = function() {
var start = new Date();
+ this.layout_.computePlotArea();
+
// TODO(danvk): move more computations out of drawGraph_ and into here.
this.computeYAxes_();
// Create a new plotter.
- if (this.plotter_) this.plotter_.clear();
+ if (this.plotter_) {
+ this.cascadeEvents_('clearChart');
+ this.plotter_.clear();
+ }
this.plotter_ = new DygraphCanvasRenderer(this,
this.hidden_,
this.hidden_ctx_,
// this will be until the options are available, so it's positioned here.
this.createRollInterface_();
- // Same thing applies for the labelsDiv. It's right edge should be flush with
- // the right edge of the charting area (which may not be the same as the right
- // edge of the div, if we have two y-axes.
- this.positionLabelsDiv_();
-
- if (this.rangeSelector_) {
- this.rangeSelector_.renderStaticLayer();
- }
+ this.cascadeEvents_('predraw');
// Convert the raw data (a 2D array) into the internal format and compute
// rolling averages.
this.rolledSeries_ = [null]; // x-axis is the first series and it's special
for (var i = 1; i < this.numColumns(); i++) {
- var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
- var logScale = this.attr_('logscale', i);
- var series = this.extractSeries_(this.rawData_, i, logScale, connectSeparatedPoints);
+ // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too.
+ var logScale = this.attr_('logscale');
+ var series = this.extractSeries_(this.rawData_, i, logScale);
series = this.rollingAverage(series, this.rollPeriod_);
this.rolledSeries_.push(series);
}
for (i = num_series; i >= 1; i--) {
if (!this.visibility()[i - 1]) continue;
- // TODO(danvk): is this copy really necessary?
+ // Note: this copy _is_ necessary at the moment.
+ // If you remove it, it breaks zooming with error bars on.
+ // TODO(danvk): investigate further & write a test for this.
var series = [];
for (j = 0; j < rolledSeries[i].length; j++) {
series.push(rolledSeries[i][j]);
}
actual_y = series[j][1];
+ if (actual_y === null) {
+ series[j] = [x, null];
+ continue;
+ }
+
cumulative_y[x] += actual_y;
series[j] = [x, cumulative_y[x]];
datasets[i] = series;
}
+ // For stacked graphs, a NaN value for any point in the sum should create a
+ // clean gap in the graph. Back-propagate NaNs to all points at this X value.
+ if (this.attr_("stackedGraph")) {
+ for (k = datasets.length - 1; k >= 0; --k) {
+ // Use the first nonempty dataset to get X values.
+ if (!datasets[k]) continue;
+ for (j = 0; j < datasets[k].length; j++) {
+ var x = datasets[k][j][0];
+ if (isNaN(cumulative_y[x])) {
+ // Set all Y values to NaN at that X value.
+ for (i = datasets.length - 1; i >= 0; i--) {
+ if (!datasets[i]) continue;
+ datasets[i][j][1] = NaN;
+ }
+ }
+ }
+ break;
+ }
+ }
+
return [ datasets, extremes, boundaryIds ];
};
* has changed. If the underlying data or options have changed, predraw_ will
* be called before drawGraph_ is called.
*
- * clearSelection, when undefined or true, causes this.clearSelection to be
- * called at the end of the draw operation. This should rarely be defined,
- * and never true (that is it should be undefined most of the time, and
- * rarely false.)
- *
* @private
*/
-Dygraph.prototype.drawGraph_ = function(clearSelection) {
+Dygraph.prototype.drawGraph_ = function() {
var start = new Date();
- if (typeof(clearSelection) === 'undefined') {
- clearSelection = true;
- }
-
// This is used to set the second parameter to drawCallback, below.
var is_initial_draw = this.is_initial_draw_;
this.is_initial_draw_ = false;
this.layout_.setDateWindow(this.dateWindow_);
this.zoomed_x_ = tmp_zoomed_x;
this.layout_.evaluateWithError();
- this.renderGraph_(is_initial_draw, false);
+ this.renderGraph_(is_initial_draw);
if (this.attr_("timingName")) {
var end = new Date();
- if (console) {
- console.log(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms");
- }
+ Dygraph.info(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms");
}
};
-Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) {
+/**
+ * This does the work of drawing the chart. It assumes that the layout and axis
+ * scales have already been set (e.g. by predraw_).
+ *
+ * @private
+ */
+Dygraph.prototype.renderGraph_ = function(is_initial_draw) {
+ this.cascadeEvents_('clearChart');
this.plotter_.clear();
- this.plotter_.render();
- this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
- this.canvas_.height);
- // Generate a static legend before any particular point is selected.
- this.setLegendHTML_();
-
- if (!is_initial_draw) {
- if (clearSelection) {
- if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
- // We should select the point nearest the page x/y here, but it's easier
- // to just clear the selection. This prevents erroneous hover dots from
- // being displayed.
- this.clearSelection();
- } else {
- this.clearSelection();
- }
- }
+ if (this.attr_('underlayCallback')) {
+ // NOTE: we pass the dygraph object to this callback twice to avoid breaking
+ // users who expect a deprecated form of this callback.
+ this.attr_('underlayCallback')(
+ this.hidden_ctx_, this.layout_.getPlotArea(), this, this);
}
- if (this.rangeSelector_) {
- this.rangeSelector_.renderInteractiveLayer();
- }
+ var e = {
+ canvas: this.hidden_,
+ drawingContext: this.hidden_ctx_
+ };
+ this.cascadeEvents_('willDrawChart', e);
+ this.plotter_.render();
+ this.cascadeEvents_('didDrawChart', e);
+
+ // TODO(danvk): is this a performance bottleneck when panning?
+ // The interaction canvas should already be empty in that situation.
+ this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
+ this.canvas_.height);
if (this.attr_("drawCallback") !== null) {
this.attr_("drawCallback")(this, is_initial_draw);
* currently being displayed. This includes things like the number of axes and
* the style of the axes. It does not include the range of each axis and its
* tick marks.
- * This fills in this.axes_ and this.seriesToAxisMap_.
+ * This fills in this.axes_.
* axes_ = [ { options } ]
- * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
* indices are into the axes_ array.
*/
Dygraph.prototype.computeYAxes_ = function() {
// Preserve valueWindow settings if they exist, and if the user hasn't
// specified a new valueRange.
- var i, valueWindows, seriesName, axis, index, opts, v;
+ var valueWindows, axis, index, opts, v;
if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) {
valueWindows = [];
for (index = 0; index < this.axes_.length; index++) {
}
}
- this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis.
- this.seriesToAxisMap_ = {};
+ // this.axes_ doesn't match this.attributes_.axes_.options. It's used for
+ // data computation as well as options storage.
+ // Go through once and add all the axes.
+ this.axes_ = [];
- // Get a list of series names.
- var labels = this.attr_("labels");
- var series = {};
- for (i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
-
- // all options which could be applied per-axis:
- var axisOptions = [
- 'includeZero',
- 'valueRange',
- 'labelsKMB',
- 'labelsKMG2',
- 'pixelsPerYLabel',
- 'yAxisLabelWidth',
- 'axisLabelFontSize',
- 'axisTickSize',
- 'logscale'
- ];
-
- // Copy global axis options over to the first axis.
- for (i = 0; i < axisOptions.length; i++) {
- var k = axisOptions[i];
- v = this.attr_(k);
- if (v) this.axes_[0][k] = v;
+ for (axis = 0; axis < this.attributes_.numAxes(); axis++) {
+ // Add a new axis, making a copy of its per-axis options.
+ opts = { g : this };
+ Dygraph.update(opts, this.attributes_.axisOptions(axis));
+ this.axes_[axis] = opts;
}
- // Go through once and add all the axes.
- for (seriesName in series) {
- if (!series.hasOwnProperty(seriesName)) continue;
- axis = this.attr_("axis", seriesName);
- if (axis === null) {
- this.seriesToAxisMap_[seriesName] = 0;
- continue;
- }
- if (typeof(axis) == 'object') {
- // Add a new axis, making a copy of its per-axis options.
- opts = {};
- Dygraph.update(opts, this.axes_[0]);
- Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this.
- var yAxisId = this.axes_.length;
- opts.yAxisId = yAxisId;
- opts.g = this;
- Dygraph.update(opts, axis);
- this.axes_.push(opts);
- this.seriesToAxisMap_[seriesName] = yAxisId;
- }
- }
-
- // Go through one more time and assign series to an axis defined by another
- // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
- for (seriesName in series) {
- if (!series.hasOwnProperty(seriesName)) continue;
- axis = this.attr_("axis", seriesName);
- if (typeof(axis) == 'string') {
- if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
- this.error("Series " + seriesName + " wants to share a y-axis with " +
- "series " + axis + ", which does not define its own axis.");
- return null;
- }
- var idx = this.seriesToAxisMap_[axis];
- this.seriesToAxisMap_[seriesName] = idx;
- }
- }
+
+ // Copy global valueRange option over to the first axis.
+ // NOTE(konigsberg): Are these two statements necessary?
+ // I tried removing it. The automated tests pass, and manually
+ // messing with tests/zoom.html showed no trouble.
+ v = this.attr_('valueRange');
+ if (v) this.axes_[0].valueRange = v;
if (valueWindows !== undefined) {
// Restore valueWindow settings.
}
}
- // New axes options
for (axis = 0; axis < this.axes_.length; axis++) {
if (axis === 0) {
opts = this.optionsViewForAxis_('y' + (axis ? '2' : ''));
}
}
}
-
};
/**
* @return {Number} the number of axes.
*/
Dygraph.prototype.numAxes = function() {
- var last_axis = 0;
- for (var series in this.seriesToAxisMap_) {
- if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
- var idx = this.seriesToAxisMap_[series];
- if (idx > last_axis) last_axis = idx;
- }
- return 1 + last_axis;
+ return this.attributes_.numAxes();
};
/**
*/
Dygraph.prototype.axisPropertiesForSeries = function(series) {
// TODO(danvk): handle errors.
- return this.axes_[this.seriesToAxisMap_[series]];
+ return this.axes_[this.attributes_.axisForSeries(series)];
};
/**
* This fills in the valueRange and ticks fields in each entry of this.axes_.
*/
Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
- // Build a map from axis number -> [list of series names]
- var seriesForAxis = [], series;
- for (series in this.seriesToAxisMap_) {
- if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
- var idx = this.seriesToAxisMap_[series];
- while (seriesForAxis.length <= idx) seriesForAxis.push([]);
- seriesForAxis[idx].push(series);
- }
+
+ var isNullUndefinedOrNaN = function(num) {
+ return isNaN(parseFloat(num));
+ };
+ var series;
+ var numAxes = this.attributes_.numAxes();
// Compute extreme values, a span and tick marks for each axis.
- for (var i = 0; i < this.axes_.length; i++) {
+ for (var i = 0; i < numAxes; i++) {
var axis = this.axes_[i];
+ var logscale = this.attributes_.getForAxis("logscale", i);
+ var includeZero = this.attributes_.getForAxis("includeZero", i);
+ series = this.attributes_.seriesForAxis(i);
- if (!seriesForAxis[i]) {
+ if (series.length === 0) {
// If no series are defined or visible then use a reasonable default
axis.extremeRange = [0, 1];
} else {
// Calculate the extremes of extremes.
- series = seriesForAxis[i];
var minY = Infinity; // extremes[series[0]][0];
var maxY = -Infinity; // extremes[series[0]][1];
var extremeMinY, extremeMaxY;
maxY = Math.max(extremeMaxY, maxY);
}
}
- if (axis.includeZero && minY > 0) minY = 0;
+ if (includeZero && minY > 0) minY = 0;
// Ensure we have a valid scale, otherwise default to [0, 1] for safety.
if (minY == Infinity) minY = 0;
if (span === 0) { span = maxY; }
var maxAxisY, minAxisY;
- if (axis.logscale) {
+ if (logscale) {
maxAxisY = maxY + 0.1 * span;
minAxisY = minY;
} else {
axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
} else if (axis.valueRange) {
// This is a user-set value range for this axis.
- axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
+ axis.computedValueRange = [
+ isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0],
+ isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1]
+ ];
} else {
axis.computedValueRange = axis.extremeRange;
}
*
* This is where undesirable points (i.e. negative values on log scales and
* missing values through which we wish to connect lines) are dropped.
- *
+ * TODO(danvk): the "missing values" bit above doesn't seem right.
+ *
* @private
*/
-Dygraph.prototype.extractSeries_ = function(rawData, i, logScale, connectSeparatedPoints) {
+Dygraph.prototype.extractSeries_ = function(rawData, i, logScale) {
+ // TODO(danvk): pre-allocate series here.
var series = [];
for (var j = 0; j < rawData.length; j++) {
var x = rawData[j][0];
var point = rawData[j][i];
if (logScale) {
// On the log scale, points less than zero do not exist.
- // This will create a gap in the chart. Note that this ignores
- // connectSeparatedPoints.
+ // This will create a gap in the chart.
if (point <= 0) {
point = null;
}
- series.push([x, point]);
- } else {
- if (point !== null || !connectSeparatedPoints) {
- series.push([x, point]);
- }
}
+ series.push([x, point]);
}
return series;
};
* data
*/
Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
- if (originalData.length < 2)
- return originalData;
rollPeriod = Math.min(rollPeriod, originalData.length);
var rollingData = [];
var sigma = this.attr_("sigma");
isDate = true;
}
+ this.setXAxisOptions_(isDate);
+};
+
+Dygraph.prototype.setXAxisOptions_ = function(isDate) {
if (isDate) {
this.attrs_.xValueParser = Dygraph.dateParser;
this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
// TODO(danvk): use Dygraph.numberValueFormatter here?
/** @private (shut up, jsdoc!) */
this.attrs_.axes.x.valueFormatter = function(x) { return x; };
- this.attrs_.axes.x.ticker = Dygraph.numericTicks;
+ this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks;
this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
}
};
*/
Dygraph.prototype.parseCSV_ = function(data) {
var ret = [];
- var lines = data.split("\n");
+ var line_delimiter = Dygraph.detectLineDelimiter(data);
+ var lines = data.split(line_delimiter || "\n");
var vals, j;
// Use the default delimiter or fall back to a tab if that makes sense.
// User hasn't explicitly set labels, so they're (presumably) in the CSV.
start = 1;
this.attrs_.labels = lines[0].split(delim); // NOTE: _not_ user_attrs_.
+ this.attributes_.reparseSeries();
}
var line_no = 0;
"in the options parameter");
this.attrs_.labels = [ "X" ];
for (i = 1; i < data[0].length; i++) {
- this.attrs_.labels.push("Y" + i);
+ this.attrs_.labels.push("Y" + i); // Not user_attrs_.
+ }
+ this.attributes_.reparseSeries();
+ } else {
+ var num_labels = this.attr_("labels");
+ if (num_labels.length != data[0].length) {
+ this.error("Mismatch between number of labels (" + num_labels +
+ ") and number of columns in array (" + data[0].length + ")");
+ return null;
}
}
if (Dygraph.isDateLike(data[0][0])) {
// Some intelligent defaults for a date x-axis.
this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
- this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+ this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
// Assume they're all dates.
var parsedData = Dygraph.clone(data);
// Some intelligent defaults for a numeric x-axis.
/** @private (shut up, jsdoc!) */
this.attrs_.axes.x.valueFormatter = function(x) { return x; };
+ this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks;
this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter;
- this.attrs_.axes.x.ticker = Dygraph.numericTicks;
return data;
}
};
num = Math.floor((num - 1) / 26);
}
return shortText;
- }
+ };
var cols = data.getNumberOfColumns();
var rows = data.getNumberOfRows();
} else if (indepType == 'number') {
this.attrs_.xValueParser = function(x) { return parseFloat(x); };
this.attrs_.axes.x.valueFormatter = function(x) { return x; };
- this.attrs_.axes.x.ticker = Dygraph.numericTicks;
+ this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks;
this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
} else {
this.error("only 'date', 'datetime' and 'number' types are supported for " +
if (annotations.length > 0) {
this.setAnnotations(annotations, true);
}
+ this.attributes_.reparseSeries();
};
/**
this.predraw_();
} else if (typeof data == 'string') {
// Heuristic: a newline means it's CSV data. Otherwise it's an URL.
- if (data.indexOf('\n') >= 0) {
+ var line_delimiter = Dygraph.detectLineDelimiter(data);
+ if (line_delimiter) {
this.loadedEvent_(data);
} else {
var req = new XMLHttpRequest();
Dygraph.updateDeep(this.user_attrs_, attrs);
+ this.attributes_.reparseSeries();
+
if (file) {
this.file_ = file;
if (!block_redraw) this.start_();
if (requiresNewPoints) {
this.predraw_();
} else {
- this.renderGraph_(false, false);
+ this.renderGraph_(false);
}
}
}
};
var map = function(opt, axis, new_opt) {
if (typeof(attrs[opt]) != 'undefined') {
+ Dygraph.warn("Option " + opt + " is deprecated. Use the " +
+ new_opt + " option for the " + axis + " axis instead. " +
+ "(e.g. { axes : { " + axis + " : { " + new_opt + " : ... } } } " +
+ "(see http://dygraphs.com/per-axis.html for more information.");
set(axis, new_opt, attrs[opt]);
delete my_attrs[opt];
}
// createInterface_ reset the layout, so we need to do this.
this.layout_.setAnnotations(this.annotations_);
}
+ this.createDragInterface_();
this.predraw_();
}
/**
* Get the list of label names for this graph. The first column is the
* x-axis, so the data series names start at index 1.
+ *
+ * Returns null when labels have not yet been defined.
*/
-Dygraph.prototype.getLabels = function(name) {
- return this.attr_("labels").slice();
+Dygraph.prototype.getLabels = function() {
+ var labels = this.attr_("labels");
+ return labels ? labels.slice() : null;
};
/**
* called once -- all calls after the first will return immediately.
*/
Dygraph.addAnnotationRule = function() {
+ // TODO(danvk): move this function into plugins/annotations.js?
if (Dygraph.addedAnnotationCSS) return;
var rule = "border: 1px solid black; " +