Dygraph.WARNING = 3;
Dygraph.ERROR = 3;
+// 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,
this.previousVerticalX_ = -1;
this.fractions_ = attrs.fractions || false;
this.dateWindow_ = attrs.dateWindow || null;
+ // valueRange and valueWindow are similar, but not the same. valueRange is a
+ // locally-stored copy of the attribute. valueWindow starts off the same as
+ // valueRange but is impacted by zoom or pan effects. valueRange is kept
+ // around to restore the original value back to valueRange.
+ // TODO(konigsberg): There are no vertical pan effects yet, but valueWindow
+ // would change accordingly.
this.valueRange_ = attrs.valueRange || null;
+ this.valueWindow_ = this.valueRange_;
+
this.wilsonInterval_ = attrs.wilsonInterval || true;
this.is_initial_draw_ = true;
+ this.annotations_ = [];
// Clear the div. This ensure that, if multiple dygraphs are passed the same
// div, then only one will be drawn.
// Make a note of whether labels will be pulled from the CSV file.
this.labelsFromCSV_ = (this.attr_("labels") == null);
+ Dygraph.addAnnotationRule();
+
// Create the containing DIV and other interactive elements
this.createInterface_();
return ret;
};
+/**
+ * Returns the number of columns (including the independent variable).
+ */
+Dygraph.prototype.numColumns = function() {
+ return this.rawData_[0].length;
+};
+
+/**
+ * Returns the number of rows (excluding any header/label row).
+ */
+Dygraph.prototype.numRows = function() {
+ return this.rawData_.length;
+};
+
+/**
+ * Returns the value in the given row and column. If the row and column exceed
+ * the bounds on the data, returns null. Also returns null if the value is
+ * missing.
+ */
+Dygraph.prototype.getValue = function(row, col) {
+ if (row < 0 || row > this.rawData_.length) return null;
+ if (col < 0 || col > this.rawData_[row].length) return null;
+
+ return this.rawData_[row][col];
+};
+
Dygraph.addEvent = function(el, evt, fn) {
var normed_fn = function(e) {
if (!e) var e = window.event;
var dragEndX = null;
var dragEndY = null;
var prevEndX = null;
+ var prevEndY = null;
+ var prevDragDirection = null;
var draggingDate = null;
var dateRange = null;
var px = 0;
var py = 0;
var getX = function(e) { return Dygraph.pageX(e) - px };
- var getY = function(e) { return Dygraph.pageX(e) - py };
+ var getY = function(e) { return Dygraph.pageY(e) - py };
// Draw zoom rectangles when the mouse is down and the user moves around
Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
dragEndX = getX(event);
dragEndY = getY(event);
- self.drawZoomRect_(dragStartX, dragEndX, prevEndX);
+ var xDelta = Math.abs(dragStartX - dragEndX);
+ var yDelta = Math.abs(dragStartY - dragEndY);
+ var dragDirection = (xDelta < yDelta) ? "V" : "H";
+
+ self.drawZoomRect_(dragDirection, dragStartX, dragEndX, dragStartY, dragEndY,
+ prevDragDirection, prevEndX, prevEndY);
+
prevEndX = dragEndX;
+ prevEndY = dragEndY;
+ prevDragDirection = dragDirection;
} else if (isPanning) {
dragEndX = getX(event);
dragEndY = getY(event);
dragStartY = getY(event);
if (event.altKey || event.shiftKey) {
+ // TODO(konigsberg): Support vertical panning.
if (!self.dateWindow_) return; // have to be zoomed in to pan.
isPanning = true;
dateRange = self.dateWindow_[1] - self.dateWindow_[0];
var regionHeight = Math.abs(dragEndY - dragStartY);
if (regionWidth < 2 && regionHeight < 2 &&
- self.attr_('clickCallback') != null &&
- self.lastx_ != undefined) {
- // TODO(danvk): pass along more info about the points.
- self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
+ self.lastx_ != undefined && self.lastx_ != -1) {
+ // TODO(danvk): pass along more info about the points, e.g. 'x'
+ if (self.attr_('clickCallback') != null) {
+ self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
+ }
+ if (self.attr_('pointClickCallback')) {
+ // check if the click was on a particular point.
+ var closestIdx = -1;
+ var closestDistance = 0;
+ for (var i = 0; i < self.selPoints_.length; i++) {
+ var p = self.selPoints_[i];
+ var distance = Math.pow(p.canvasx - dragEndX, 2) +
+ Math.pow(p.canvasy - dragEndY, 2);
+ if (closestIdx == -1 || distance < closestDistance) {
+ closestDistance = distance;
+ closestIdx = i;
+ }
+ }
+
+ // Allow any click within two pixels of the dot.
+ var radius = self.attr_('highlightCircleSize') + 2;
+ if (closestDistance <= 5 * 5) {
+ self.attr_('pointClickCallback')(event, self.selPoints_[closestIdx]);
+ }
+ }
}
- if (regionWidth >= 10) {
- self.doZoom_(Math.min(dragStartX, dragEndX),
+ if (regionWidth >= 10 && regionWidth > regionHeight) {
+ self.doZoomX_(Math.min(dragStartX, dragEndX),
Math.max(dragStartX, dragEndX));
+ } else if (regionHeight >= 10 && regionHeight > regionWidth){
+ self.doZoomY_(Math.min(dragStartY, dragEndY),
+ Math.max(dragStartY, dragEndY));
} else {
self.canvas_.getContext("2d").clearRect(0, 0,
self.canvas_.width,
// Double-clicking zooms back out
Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
- if (self.dateWindow_ == null) return;
- self.dateWindow_ = null;
- self.drawGraph_(self.rawData_);
- var minDate = self.rawData_[0][0];
- var maxDate = self.rawData_[self.rawData_.length - 1][0];
- if (self.attr_("zoomCallback")) {
- self.attr_("zoomCallback")(minDate, maxDate);
- }
+ // Disable zooming out if panning.
+ if (event.altKey || event.shiftKey) return;
+
+ self.doUnzoom_();
});
};
* 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 {String} direction the direction of the zoom rectangle. "H" and "V"
+ * for Horizontal and Vertical.
* @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} startY The Y position where the drag started, in canvas
+ * coordinates.
+ * @param {Number} endY The current Y position of the drag, in canvas coords.
+ * @param {String} prevDirection the value of direction on the previous call to
+ * this function. Used to avoid excess redrawing
* @param {Number} prevEndX The value of endX on the previous call to this
* function. Used to avoid excess redrawing
+ * @param {Number} prevEndY The value of endY on the previous call to this
+ * function. Used to avoid excess redrawing
* @private
*/
-Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
+Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY,
+ prevDirection, prevEndX, prevEndY) {
var ctx = this.canvas_.getContext("2d");
// Clean up from the previous rect if necessary
- if (prevEndX) {
+ if (prevDirection == "H") {
ctx.clearRect(Math.min(startX, prevEndX), 0,
Math.abs(startX - prevEndX), this.height_);
+ } else if (prevDirection == "V"){
+ ctx.clearRect(0, Math.min(startY, prevEndY),
+ this.width_, Math.abs(startY - prevEndY));
}
// 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_);
+ if (direction == "H") {
+ if (endX && startX) {
+ ctx.fillStyle = "rgba(128,128,128,0.33)";
+ ctx.fillRect(Math.min(startX, endX), 0,
+ Math.abs(endX - startX), this.height_);
+ }
+ }
+ if (direction == "V") {
+ if (endY && startY) {
+ ctx.fillStyle = "rgba(128,128,128,0.33)";
+ ctx.fillRect(0, Math.min(startY, endY),
+ this.width_, Math.abs(endY - startY));
+ }
}
};
/**
- * 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.
+ * 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. Don't confuse this function with doZoomXDates,
+ * which accepts dates that match the raw data. 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
*/
-Dygraph.prototype.doZoom_ = function(lowX, highX) {
+Dygraph.prototype.doZoomX_ = function(lowX, highX) {
// Find the earliest and latest dates contained in this canvasx range.
+ // Convert the call to date ranges of the raw data.
var r = this.toDataCoords(lowX, null);
var minDate = r[0];
r = this.toDataCoords(highX, null);
var maxDate = r[0];
+ this.doZoomXDates_(minDate, maxDate);
+};
+/**
+ * Zoom to something containing [minDate, maxDate] values. Don't confuse this
+ * method with doZoomX which accepts pixel coordinates. This function redraws
+ * the graph.
+ *
+ * @param {Number} minDate The minimum date that should be visible.
+ * @param {Number} maxDate The maximum date that should be visible.
+ * @private
+ */
+Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
this.dateWindow_ = [minDate, maxDate];
this.drawGraph_(this.rawData_);
if (this.attr_("zoomCallback")) {
- this.attr_("zoomCallback")(minDate, maxDate);
+ var yRange = this.yAxisRange();
+ this.attr_("zoomCallback")(minDate, maxDate, yRange[0], yRange[1]);
+ }
+};
+
+/**
+ * Zoom to something containing [lowY, highY]. These are pixel coordinates in
+ * the canvas. The exact zoom window may be slightly larger if there are no
+ * data points near lowY or highY. Don't confuse this function with
+ * doZoomYValues, which accepts parameters that match the raw data. This
+ * function redraws the graph.
+ *
+ * @param {Number} lowY The topmost pixel value that should be visible.
+ * @param {Number} highY The lowest pixel value that should be visible.
+ * @private
+ */
+Dygraph.prototype.doZoomY_ = function(lowY, highY) {
+ // Find the highest and lowest values in pixel range.
+ var r = this.toDataCoords(null, lowY);
+ var minValue = r[1];
+ r = this.toDataCoords(null, highY);
+ var maxValue = r[1];
+
+ this.doZoomYValues_(minValue, maxValue);
+};
+
+/**
+ * Zoom to something containing [minValue, maxValue] values. Don't confuse this
+ * method with doZoomY which accepts pixel coordinates. This function redraws
+ * the graph.
+ *
+ * @param {Number} minValue The minimum Value that should be visible.
+ * @param {Number} maxValue The maximum value that should be visible.
+ * @private
+ */
+Dygraph.prototype.doZoomYValues_ = function(minValue, maxValue) {
+ this.valueWindow_ = [maxValue, minValue];
+ this.drawGraph_(this.rawData_);
+ if (this.attr_("zoomCallback")) {
+ var xRange = this.xAxisRange();
+ this.attr_("zoomCallback")(xRange[0], xRange[1], minValue, maxValue);
+ }
+};
+
+/**
+ * Reset the zoom to the original view coordinates. This is the same as
+ * double-clicking on the graph.
+ *
+ * @private
+ */
+Dygraph.prototype.doUnzoom_ = function() {
+ var dirty = null;
+ if (this.dateWindow_ != null) {
+ dirty = 1;
+ this.dateWindow_ = null;
+ }
+ if (this.valueWindow_ != null) {
+ dirty = 1;
+ this.valueWindow_ = this.valueRange_;
+ }
+
+ if (dirty) {
+ if (this.attr_("zoomCallback")) {
+ var minDate = this.rawData_[0][0];
+ var maxDate = this.rawData_[this.rawData_.length - 1][0];
+ var minValue = this.xAxisRange()[0];
+ var maxValue = this.xAxisRange()[1];
+ this.attr_("zoomCallback")(minDate, maxDate, minValue, maxValue);
+ }
+ this.drawGraph_(this.rawData_);
}
};
}
// Use some heuristics to come up with a good maxY value, unless it's been
- // set explicitly by the user.
- if (this.valueRange_ != null) {
- this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
- this.displayedYRange_ = this.valueRange_;
+ // set explicitly by the developer or end-user (via drag)
+ if (this.valueWindow_ != null) {
+ this.addYTicks_(this.valueWindow_[0], this.valueWindow_[1]);
+ this.displayedYRange_ = this.valueWindow_;
} else {
// This affects the calculation of span, below.
if (this.attr_("includeZero") && minY > 0) {
// Parse the x as a float or return null if it's not a number.
var parseFloatOrNull = function(x) {
- if (x.length == 0) return null;
- return parseFloat(x);
+ var val = parseFloat(x);
+ return isNaN(val) ? null : val;
};
var xParser;
var parsedData = Dygraph.clone(data);
for (var i = 0; i < data.length; i++) {
if (parsedData[i].length == 0) {
- this.error("Row " << (1 + i) << " of data is empty");
+ this.error("Row " + (1 + i) + " of data is empty");
return null;
}
if (parsedData[i][0] == null
* The data is expected to have a first column that is either a date or a
* number. All subsequent columns must be numbers. If there is a clear mismatch
* between this.xValueParser_ and the type of the first column, it will be
- * fixed. Returned value is in the same format as return value of parseCSV_.
+ * fixed. Fills out rawData_.
* @param {Array.<Object>} data See above.
* @private
*/
var cols = data.getNumberOfColumns();
var rows = data.getNumberOfRows();
- // Read column labels
- var labels = [];
- for (var i = 0; i < cols; i++) {
- labels.push(data.getColumnLabel(i));
- if (i != 0 && this.attr_("errorBars")) i += 1;
- }
- this.attrs_.labels = labels;
- cols = labels.length;
-
var indepType = data.getColumnType(0);
if (indepType == 'date' || indepType == 'datetime') {
this.attrs_.xValueFormatter = Dygraph.dateString_;
return null;
}
+ // Array of the column indices which contain data (and not annotations).
+ var colIdx = [];
+ var annotationCols = {}; // data index -> [annotation cols]
+ var hasAnnotations = false;
+ for (var i = 1; i < cols; i++) {
+ var type = data.getColumnType(i);
+ if (type == 'number') {
+ colIdx.push(i);
+ } else if (type == 'string' && this.attr_('displayAnnotations')) {
+ // This is OK -- it's an annotation column.
+ var dataIdx = colIdx[colIdx.length - 1];
+ if (!annotationCols.hasOwnProperty(dataIdx)) {
+ annotationCols[dataIdx] = [i];
+ } else {
+ annotationCols[dataIdx].push(i);
+ }
+ hasAnnotations = true;
+ } else {
+ this.error("Only 'number' is supported as a dependent type with Gviz." +
+ " 'string' is only supported if displayAnnotations is true");
+ }
+ }
+
+ // Read column labels
+ // TODO(danvk): add support back for errorBars
+ var labels = [data.getColumnLabel(0)];
+ for (var i = 0; i < colIdx.length; i++) {
+ labels.push(data.getColumnLabel(colIdx[i]));
+ }
+ this.attrs_.labels = labels;
+ cols = labels.length;
+
var ret = [];
var outOfOrder = false;
+ var annotations = [];
for (var i = 0; i < rows; i++) {
var row = [];
if (typeof(data.getValue(i, 0)) === 'undefined' ||
row.push(data.getValue(i, 0));
}
if (!this.attr_("errorBars")) {
- for (var j = 1; j < cols; j++) {
- row.push(data.getValue(i, j));
+ for (var j = 0; j < colIdx.length; j++) {
+ var col = colIdx[j];
+ row.push(data.getValue(i, col));
+ if (hasAnnotations &&
+ annotationCols.hasOwnProperty(col) &&
+ data.getValue(i, annotationCols[col][0]) != null) {
+ var ann = {};
+ ann.series = data.getColumnLabel(col);
+ ann.xval = row[0];
+ ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
+ ann.text = '';
+ for (var k = 0; k < annotationCols[col].length; k++) {
+ if (k) ann.text += "\n";
+ ann.text += data.getValue(i, annotationCols[col][k]);
+ }
+ annotations.push(ann);
+ }
}
} else {
for (var j = 0; j < cols - 1; j++) {
this.warn("DataTable is out of order; order it correctly to speed loading.");
ret.sort(function(a,b) { return a[0] - b[0] });
}
- return ret;
+ this.rawData_ = ret;
+
+ if (annotations.length > 0) {
+ this.setAnnotations(annotations, true);
+ }
}
// These functions are all based on MochiKit.
} else if (typeof this.file_ == 'object' &&
typeof this.file_.getColumnRange == 'function') {
// must be a DataTable from gviz.
- this.rawData_ = this.parseDataTable_(this.file_);
+ this.parseDataTable_(this.file_);
this.drawGraph_(this.rawData_);
} else if (typeof this.file_ == 'string') {
// Heuristic: a newline means it's CSV data. Otherwise it's an URL.
};
/**
+ * Update the list of annotations and redraw the chart.
+ */
+Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
+ this.annotations_ = ann;
+ this.layout_.setAnnotations(this.annotations_);
+ if (!suppressDraw) {
+ this.drawGraph_(this.rawData_);
+ }
+};
+
+/**
+ * Return the list of annotations.
+ */
+Dygraph.prototype.annotations = function() {
+ return this.annotations_;
+};
+
+Dygraph.addAnnotationRule = function() {
+ if (Dygraph.addedAnnotationCSS) return;
+
+ var mysheet;
+ if (document.styleSheets.length > 0) {
+ mysheet = document.styleSheets[0];
+ } else {
+ var styleSheetElement = document.createElement("style");
+ styleSheetElement.type = "text/css";
+ document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
+ for(i = 0; i < document.styleSheets.length; i++) {
+ if (document.styleSheets[i].disabled) continue;
+ mysheet = document.styleSheets[i];
+ }
+ }
+
+ var rule = "border: 1px solid black; " +
+ "background-color: white; " +
+ "text-align: center;";
+ if (mysheet.insertRule) { // Firefox
+ mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", 0);
+ } else if (mysheet.addRule) { // IE
+ mysheet.addRule(".dygraphDefaultAnnotation", rule);
+ }
+
+ Dygraph.addedAnnotationCSS = true;
+}
+
+/**
* Create a new canvas element. This is more complex than a simple
* document.createElement("canvas") because of IE and excanvas.
*/