/**
* Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
- * and context <canvas> inside of it. See the constructor for details
+ * 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
this.wilsonInterval_ = attrs.wilsonInterval || true;
this.is_initial_draw_ = true;
this.annotations_ = [];
-
+
// Number of digits to use when labeling the x (if numeric) and y axis
// ticks.
this.numXDigits_ = 2;
this.start_();
};
+Dygraph.prototype.toString = function() {
+ var maindiv = this.maindiv_;
+ var id = (maindiv && maindiv.id) ? maindiv.id : maindiv
+ return "[Dygraph " + id + "]";
+}
+
Dygraph.prototype.attr_ = function(name, seriesName) {
if (seriesName &&
typeof(this.user_attrs_[seriesName]) != 'undefined' &&
var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
var points = this.layout_.points;
+ // This prevents JS errors when mousing over the canvas before data loads.
+ if (points === undefined) return;
+
var lastx = -1;
var lasty = -1;
var ret = formatter(range[0], range[1], this);
var xTicks = [];
+ // Note: numericTicks() returns a {ticks: [...], numDigits: yy} dictionary,
+ // whereas dateTicker and user-defined tickers typically just return a ticks
+ // array.
if (ret.ticks !== undefined) {
- // numericTicks() returns multiple values.
xTicks = ret.ticks;
this.numXDigits_ = ret.numDigits;
} else {
var ticks = [];
if (vals) {
for (var i = 0; i < vals.length; i++) {
- ticks[i].push({v: vals[i]});
+ ticks.push({v: vals[i]});
}
} else {
if (axis_props && attr("logscale")) {
// Add labels to the ticks.
for (var i = 0; i < ticks.length; i++) {
+ if (ticks[i].label !== undefined) continue; // Use current label.
var tickV = ticks[i].v;
var absTickV = Math.abs(tickV);
var label = (formatter !== undefined) ?
// On the log scale, points less than zero do not exist.
// This will create a gap in the chart. Note that this ignores
// connectSeparatedPoints.
- if (point < 0) {
+ if (point <= 0) {
point = null;
}
series.push([date, point]);
this.attrs_.xTicker = Dygraph.dateTicker;
this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
} else {
- this.attrs_.xValueFormatter = this.attrs_.xValueFormatter;
+ this.attrs_.xValueFormatter = this.attrs_.yValueFormatter;
this.attrs_.xValueParser = function(x) { return parseFloat(x); };
this.attrs_.xTicker = Dygraph.numericTicks;
this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
};
/**
+ * Parses the value as a floating point number. This is like the parseFloat()
+ * built-in, but with a few differences:
+ * - the empty string is parsed as null, rather than NaN.
+ * - if the string cannot be parsed at all, an error is logged.
+ * If the string can't be parsed, this method returns null.
+ * @param {String} x The string to be parsed
+ * @param {Number} opt_line_no The line number from which the string comes.
+ * @param {String} opt_line The text of the line from which the string comes.
+ * @private
+ */
+
+// Parse the x as a float or return null if it's not a number.
+Dygraph.prototype.parseFloat_ = function(x, opt_line_no, opt_line) {
+ var val = parseFloat(x);
+ if (!isNaN(val)) return val;
+
+ // Try to figure out what happeend.
+ // If the value is the empty string, parse it as null.
+ if (/^ *$/.test(x)) return null;
+
+ // If it was actually "NaN", return it as NaN.
+ if (/^ *nan *$/i.test(x)) return NaN;
+
+ // Looks like a parsing error.
+ var msg = "Unable to parse '" + x + "' as a number";
+ if (opt_line !== null && opt_line_no !== null) {
+ msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV.";
+ }
+ this.error(msg);
+
+ return null;
+};
+
+/**
* Parses a string in a special csv format. We expect a csv file where each
* line is a date point, and the first field in each line is the date string.
* We also expect that all remaining fields represent series.
start = 1;
this.attrs_.labels = lines[0].split(delim);
}
-
- // Parse the x as a float or return null if it's not a number.
- var parseFloatOrNull = function(x) {
- var val = parseFloat(x);
- // isFinite() returns false for NaN and +/-Infinity.
- return isFinite(val) ? val : null;
- };
+ var line_no = 0;
var xParser;
var defaultParserSet = false; // attempt to auto-detect x value type
var outOfOrder = false;
for (var i = start; i < lines.length; i++) {
var line = lines[i];
+ line_no = i;
if (line.length == 0) continue; // skip blank lines
if (line[0] == '#') continue; // skip comment lines
var inFields = line.split(delim);
for (var j = 1; j < inFields.length; j++) {
// TODO(danvk): figure out an appropriate way to flag parse errors.
var vals = inFields[j].split("/");
- fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
+ if (vals.length != 2) {
+ this.error('Expected fractional "num/den" values in CSV data ' +
+ "but found a value '" + inFields[j] + "' on line " +
+ (1 + i) + " ('" + line + "') which is not of this form.");
+ fields[j] = [0, 0];
+ } else {
+ fields[j] = [this.parseFloat_(vals[0], i, line),
+ this.parseFloat_(vals[1], i, line)];
+ }
}
} else if (this.attr_("errorBars")) {
// If there are error bars, values are (value, stddev) pairs
- for (var j = 1; j < inFields.length; j += 2)
- fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
- parseFloatOrNull(inFields[j + 1])];
+ if (inFields.length % 2 != 1) {
+ this.error('Expected alternating (value, stdev.) pairs in CSV data ' +
+ 'but line ' + (1 + i) + ' has an odd number of values (' +
+ (inFields.length - 1) + "): '" + line + "'");
+ }
+ for (var j = 1; j < inFields.length; j += 2) {
+ fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line),
+ this.parseFloat_(inFields[j + 1], i, line)];
+ }
} else if (this.attr_("customBars")) {
// Bars are a low;center;high tuple
for (var j = 1; j < inFields.length; j++) {
var vals = inFields[j].split(";");
- fields[j] = [ parseFloatOrNull(vals[0]),
- parseFloatOrNull(vals[1]),
- parseFloatOrNull(vals[2]) ];
+ fields[j] = [ this.parseFloat_(vals[0], i, line),
+ this.parseFloat_(vals[1], i, line),
+ this.parseFloat_(vals[2], i, line) ];
}
} else {
// Values are just numbers
for (var j = 1; j < inFields.length; j++) {
- fields[j] = parseFloatOrNull(inFields[j]);
+ fields[j] = this.parseFloat_(inFields[j], i, line);
}
}
if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
outOfOrder = true;
}
- ret.push(fields);
if (fields.length != expectedCols) {
this.error("Number of columns in line " + i + " (" + fields.length +
") does not agree with number of labels (" + expectedCols +
") " + line);
}
+
+ // If the user specified the 'labels' option and none of the cells of the
+ // first row parsed correctly, then they probably double-specified the
+ // labels. We go with the values set in the option, discard this row and
+ // log a warning to the JS console.
+ if (i == 0 && this.attr_('labels')) {
+ var all_null = true;
+ for (var j = 0; all_null && j < fields.length; j++) {
+ if (fields[j]) all_null = false;
+ }
+ if (all_null) {
+ this.warn("The dygraphs 'labels' option is set, but the first row of " +
+ "CSV data ('" + line + "') appears to also contain labels. " +
+ "Will drop the CSV labels and use the option labels.");
+ continue;
+ }
+ }
+ ret.push(fields);
}
if (outOfOrder) {