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;
+
// Number of digits to use when labeling the x (if numeric) and y axis
// ticks.
this.numXDigits_ = 2;
this.start_();
};
+/**
+ * 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).
+ */
+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 to Dygraph.isZoomed must be missing, 'x' or 'y'.";
+};
+
Dygraph.prototype.toString = function() {
var maindiv = this.maindiv_;
var id = (maindiv && maindiv.id) ? maindiv.id : maindiv
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
return null;
}
- var area = this.plotter_.area;
var xRange = this.xAxisRange();
-
- var pct;
- // xRange[1] - x is unit distance from the right.
- // xRange[1] - xRange[0] is the scale of the range.
- // (xRange[1] - x / (xRange[1] - xRange[0]) is the % from the right.
- // 1 - (that) is the % distance from the left.
- pct = (xRange[1] - x) / (xRange[1] - xRange[0]);
- // There's a way to optimize that, but I'm copying the y-coord function
- // and am lazy.
- return 1 - pct;
+ return (x - xRange[0]) / (xRange[1] - xRange[0]);
}
/**
context.initialLeftmostDate = xRange[0];
context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
- // TODO(konigsberg): do that clever "undefined" thing.
- if (g.attr_("panFrame")) {
- var maxXPixelsToDraw = g.width_ * g.attr_("panFrame");
+ if (g.attr_("panEdgeFraction")) {
+ var maxXPixelsToDraw = g.width_ * g.attr_("panEdgeFraction");
var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
context.boundedDates = [boundedLeftDate, boundedRightDate];
var boundedValues = [];
- var maxYPixelsToDraw = g.height_ * g.attr_("panFrame");
+ var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction");
for (var i = 0; i < g.axes_.length; i++) {
var axis = g.axes_[i];
var boundedTopValue = g.toDataYCoord(boundedTopY);
var boundedBottomValue = g.toDataYCoord(boundedBottomY);
- console.log(yExtremes[0], yExtremes[1], boundedTopValue, boundedBottomValue);
- // could reverse these, who knows?
boundedValues[i] = [boundedTopValue, boundedBottomValue];
}
context.boundedValues = boundedValues;
if (maxDate > context.boundedDates[1]) {
// Adjust minDate, and recompute maxDate.
minDate = minDate - (maxDate - context.boundedDates[1]);
- var maxDate = minDate + context.dateRange;
+ maxDate = minDate + context.dateRange;
}
}
context.initialLeftmostDate = null;
context.dateRange = null;
context.valueRange = null;
+ context.boundedDates = null;
+ context.boundedValues = null;
}
// Called in response to an interaction model operation that
px: 0,
py: 0,
- // Values for use with panFrame, which limit how far outside the
+ // Values for use with panEdgeFraction, which limit how far outside the
// graph's data boundaries it can be panned.
boundedDates: null, // [minDate, maxDate]
boundedValues: null, // [[minValue, maxValue] ...]
*/
Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
this.dateWindow_ = [minDate, maxDate];
+ this.zoomed_x_ = true;
this.drawGraph_();
if (this.attr_("zoomCallback")) {
this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
valueRanges.push([low, hi]);
}
+ this.zoomed_y_ = true;
this.drawGraph_();
if (this.attr_("zoomCallback")) {
var xRange = this.xAxisRange();
+ var yRange = this.yAxisRange();
this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges());
}
};
if (dirty) {
// Putting the drawing operation before the callback because it resets
// yAxisRange.
+ this.zoomed_x_ = false;
+ this.zoomed_y_ = false;
this.drawGraph_();
if (this.attr_("zoomCallback")) {
var minDate = this.rawData_[0][0];
if (i % year_mod != 0) continue;
for (var j = 0; j < months.length; j++) {
var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
- var t = Date.parse(date_str);
+ var t = Dygraph.dateStrToMillis(date_str);
if (t < start_time || t > end_time) continue;
ticks.push({ v:t, label: formatter(new Date(t), granularity) });
}
this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
}
- this.computeYAxisRanges_(extremes);
- this.layout_.updateOptions( { yAxes: this.axes_,
- seriesToAxisMap: this.seriesToAxisMap_
- } );
-
+ if (datasets.length > 0) {
+ // TODO(danvk): this method doesn't need to return anything.
+ this.computeYAxisRanges_(extremes);
+ this.layout_.updateOptions( { yAxes: this.axes_,
+ seriesToAxisMap: this.seriesToAxisMap_
+ } );
+ }
this.addXTicks_();
+ // Save the X axis zoomed status as the updateOptions call will tend to set it errorneously
+ var tmp_zoomed_x = this.zoomed_x_;
// Tell PlotKit to use this new data and render itself
this.layout_.updateOptions({dateWindow: this.dateWindow_});
+ this.zoomed_x_ = tmp_zoomed_x;
this.layout_.evaluateWithError();
this.plotter_.clear();
this.plotter_.render();
* indices are into the axes_ array.
*/
Dygraph.prototype.computeYAxes_ = function() {
+ var valueWindows;
+ if (this.axes_ != undefined) {
+ // Preserve valueWindow settings.
+ valueWindows = [];
+ for (var index = 0; index < this.axes_.length; index++) {
+ valueWindows.push(this.axes_[index].valueWindow);
+ }
+ }
+
this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis.
this.seriesToAxisMap_ = {};
if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
}
this.seriesToAxisMap_ = seriesToAxisFiltered;
+
+ if (valueWindows != undefined) {
+ // Restore valueWindow settings.
+ for (var index = 0; index < valueWindows.length; index++) {
+ this.axes_[index].valueWindow = valueWindows[index];
+ }
+ }
};
/**
seriesForAxis[idx].push(series);
}
+ // If no series are defined or visible then fill in some reasonable defaults.
+ if (seriesForAxis.length == 0) {
+ var axis = this.axes_[0];
+ axis.computedValueRange = [0, 1];
+ var ret =
+ Dygraph.numericTicks(axis.computedValueRange[0],
+ axis.computedValueRange[1],
+ this,
+ axis);
+ axis.ticks = ret.ticks;
+ this.numYDigits_ = ret.numDigits;
+ return;
+ }
+
// Compute extreme values, a span and tick marks for each axis.
for (var i = 0; i < this.axes_.length; i++) {
var axis = this.axes_[i];
var series = seriesForAxis[i];
var minY = Infinity; // extremes[series[0]][0];
var maxY = -Infinity; // extremes[series[0]][1];
+ var extremeMinY, extremeMaxY;
for (var j = 0; j < series.length; j++) {
- minY = Math.min(extremes[series[j]][0], minY);
- maxY = Math.max(extremes[series[j]][1], maxY);
+ // Only use valid extremes to stop null data series' from corrupting the scale.
+ extremeMinY = extremes[series[j]][0];
+ if (extremeMinY != null) {
+ minY = Math.min(extremeMinY, minY);
+ }
+ extremeMaxY = extremes[series[j]][1];
+ if (extremeMaxY != null) {
+ maxY = Math.max(extremeMaxY, maxY);
+ }
}
if (axis.includeZero && minY > 0) minY = 0;
+ // Ensure we have a valid scale, otherwise defualt to zero for safety.
+ if (minY == Infinity) {
+ minY = 0;
+ }
+
+ if (maxY == -Infinity) {
+ maxY = 0;
+ }
+
// Add some padding and round up to an integer to be human-friendly.
var span = maxY - minY;
// special case: if we have no sense of scale, use +/-10% of the sole value.
while (dateStrSlashed.search("-") != -1) {
dateStrSlashed = dateStrSlashed.replace("-", "/");
}
- d = Date.parse(dateStrSlashed);
+ d = Dygraph.dateStrToMillis(dateStrSlashed);
} else if (dateStr.length == 8) { // e.g. '20090712'
// TODO(danvk): remove support for this format. It's confusing.
dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
+ "/" + dateStr.substr(6,2);
- d = Date.parse(dateStrSlashed);
+ d = Dygraph.dateStrToMillis(dateStrSlashed);
} else {
// Any format that Date.parse will accept, e.g. "2009/07/12" or
// "2009/07/12 12:34:56"
- d = Date.parse(dateStr);
+ d = Dygraph.dateStrToMillis(dateStr);
}
if (!d || isNaN(d)) {
annotations.push(ann);
}
}
+
+ // Strip out infinities, which give dygraphs problems later on.
+ for (var j = 0; j < row.length; j++) {
+ if (!isFinite(row[j])) row[j] = null;
+ }
} else {
for (var j = 0; j < cols - 1; j++) {
row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
outOfOrder = true;
}
-
- // Strip out infinities, which give dygraphs problems later on.
- for (var j = 0; j < row.length; j++) {
- if (!isFinite(row[j])) row[j] = null;
- }
ret.push(row);
}
}
}
+// This is identical to JavaScript's built-in Date.parse() method, except that
+// it doesn't get replaced with an incompatible method by aggressive JS
+// libraries like MooTools or Joomla.
+Dygraph.dateStrToMillis = function(str) {
+ return new Date(str).getTime();
+};
+
// These functions are all based on MochiKit.
Dygraph.update = function (self, o) {
if (typeof(o) != 'undefined' && o !== null) {
* <li>file: changes the source data for the graph</li>
* <li>errorBars: changes whether the data contains stddev</li>
* </ul>
+ *
+ * If the dateWindow or valueRange options are specified, the relevant zoomed_x_
+ * or zoomed_y_ flags are set, unless the isZoomedIgnoreProgrammaticZoom option is also
+ * secified. This allows for the chart to be programmatically zoomed without
+ * altering the zoomed flags.
+ *
* @param {Object} attrs The new properties and values
*/
Dygraph.prototype.updateOptions = function(attrs) {
}
if ('dateWindow' in attrs) {
this.dateWindow_ = attrs.dateWindow;
+ if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) {
+ this.zoomed_x_ = attrs.dateWindow != null;
+ }
+ }
+ if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) {
+ this.zoomed_y_ = attrs.valueRange != null;
}
// TODO(danvk): validate per-series options.
"type": "boolean",
"description": "Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart."
},
- "panFrame": {
+ "panEdgeFraction": {
"default": "null",
- "labels": ["Axis Display?"],
+ "labels": ["Axis Display", "Interactive Elements"],
"type": "float",
"default": "null",
"description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."
+ },
+ "isZoomedIgnoreProgrammaticZoom" : {
+ "default": "false",
+ "labels": ["Zooming"],
+ "type": "boolean",
+ "description" : "When this flag is passed along with either the <code>dateWindow</code> or <code>valueRange</code> options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the <code>isZoomed</code> method to determine this."
}
}
; // </JSON>