var CallbackTestCase = TestCase("callback");
CallbackTestCase.prototype.setUp = function() {
- document.body.innerHTML = "<div id='graph'></div>";
+ document.body.innerHTML = "<div id='graph'></div><div id='selection'></div>";
+ this.styleSheet = document.createElement("style");
+ this.styleSheet.type = "text/css";
+ document.getElementsByTagName("head")[0].appendChild(this.styleSheet);
};
CallbackTestCase.prototype.tearDown = function() {
width: 100,
height : 100,
visibility: [false, true, true],
- highlightCallback : highlightCallback,
+ highlightCallback : highlightCallback
});
DygraphOps.dispatchMouseMove(g, 13, 10);
//check there are only two points (because first series is hidden)
assertEquals(2, h_pts.length);
};
+
+var runClosestTest = function(isStacked, widthNormal, widthHighlighted) {
+ var h_row;
+ var h_pts;
+ var h_series;
+
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data,
+ {
+ width: 600,
+ height : 400,
+ visibility: [false, true, true],
+ stackedGraph: isStacked,
+ strokeWidth: widthNormal,
+ strokeBorderWidth: 2,
+ highlightCircleSize: widthNormal * 2,
+ highlightSeriesBackgroundFade: 0.7,
+ highlightSeriesAnimate: true,
+
+ highlightSeriesOpts: {
+ strokeWidth: widthHighlighted,
+ highlightCircleSize: widthHighlighted * 2
+ }
+ });
+
+ var highlightCallback = function(e, x, pts, row, set) {
+ h_row = row;
+ h_pts = pts;
+ h_series = set;
+ document.getElementById('selection').innerHTML='row=' + row + ', set=' + set;
+ };
+
+ g.updateOptions({highlightCallback: highlightCallback}, true);
+
+ if (isStacked) {
+ DygraphOps.dispatchMouseMove(g, 11.45, 1.4);
+ assertEquals(1, h_row);
+ assertEquals('c', h_series);
+
+ //now move up in the same row
+ DygraphOps.dispatchMouseMove(g, 11.45, 1.5);
+ assertEquals(1, h_row);
+ assertEquals('b', h_series);
+
+ //and a bit to the right
+ DygraphOps.dispatchMouseMove(g, 11.55, 1.5);
+ assertEquals(2, h_row);
+ assertEquals('c', h_series);
+ } else {
+ DygraphOps.dispatchMouseMove(g, 11, 1.5);
+ assertEquals(1, h_row);
+ assertEquals('c', h_series);
+
+ //now move up in the same row
+ DygraphOps.dispatchMouseMove(g, 11, 2.5);
+ assertEquals(1, h_row);
+ assertEquals('b', h_series);
+ }
+
+ return g;
+};
+
+/**
+ * Test basic closest-point highlighting.
+ */
+CallbackTestCase.prototype.testClosestPointCallback = function() {
+ runClosestTest(false, 1, 3);
+}
+
+/**
+ * Test setSelection() with series name
+ */
+CallbackTestCase.prototype.testSetSelection = function() {
+ var g = runClosestTest(false, 1, 3);
+ assertEquals(1, g.attr_('strokeWidth', 'c'));
+ g.setSelection(false, 'c');
+ assertEquals(3, g.attr_('strokeWidth', 'c'));
+}
+
+/**
+ * Test closest-point highlighting for stacked graph
+ */
+CallbackTestCase.prototype.testClosestPointStackedCallback = function() {
+ runClosestTest(true, 1, 3);
+}
+
+/**
+ * Closest-point highlighting with legend CSS - border around active series.
+ */
+CallbackTestCase.prototype.testClosestPointCallbackCss1 = function() {
+ var css = "div.dygraph-legend > span { display: block; }\n" +
+ "div.dygraph-legend > span.highlight { border: 1px solid grey; }\n";
+ this.styleSheet.innerHTML = css;
+ runClosestTest(false, 2, 4);
+}
+
+/**
+ * Closest-point highlighting with legend CSS - show only closest series.
+ */
+CallbackTestCase.prototype.testClosestPointCallbackCss2 = function() {
+ var css = "div.dygraph-legend > span { display: none; }\n" +
+ "div.dygraph-legend > span.highlight { display: inline; }\n";
+ this.styleSheet.innerHTML = css;
+ runClosestTest(false, 10, 15);
+ // TODO(klausw): verify that the highlighted line is drawn on top?
+}
}
};
+DygraphCanvasRenderer.prototype._drawStyledLine = function(
+ ctx, i, color, strokeWidth, strokePattern, drawPoints, pointSize) {
+ var isNullOrNaN = function(x) {
+ return (x === null || isNaN(x));
+ };
+
+ var stepPlot = this.attr_("stepPlot");
+ var firstIndexInSet = this.layout.setPointsOffsets[i];
+ var setLength = this.layout.setPointsLengths[i];
+ var afterLastIndexInSet = firstIndexInSet + setLength;
+ var points = this.layout.points;
+ var prevX = null;
+ var prevY = null;
+ if (!Dygraph.isArrayLike(strokePattern)) {
+ strokePattern = null;
+ }
+
+ var point;
+ ctx.save();
+ for (var j = firstIndexInSet; j < afterLastIndexInSet; j++) {
+ point = points[j];
+ if (isNullOrNaN(point.canvasy)) {
+ if (stepPlot && prevX !== null) {
+ // Draw a horizontal line to the start of the missing data
+ ctx.beginPath();
+ ctx.strokeStyle = color;
+ ctx.lineWidth = this.attr_('strokeWidth');
+ this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
+ ctx.stroke();
+ }
+ // this will make us move to the next point, not draw a line to it.
+ prevX = prevY = null;
+ } else {
+ // A point is "isolated" if it is non-null but both the previous
+ // and next points are null.
+ var isIsolated = (!prevX && (j == points.length - 1 ||
+ isNullOrNaN(points[j+1].canvasy)));
+ if (prevX === null) {
+ prevX = point.canvasx;
+ prevY = point.canvasy;
+ } else {
+ // Skip over points that will be drawn in the same pixel.
+ if (Math.round(prevX) == Math.round(point.canvasx) &&
+ Math.round(prevY) == Math.round(point.canvasy)) {
+ continue;
+ }
+ // TODO(antrob): skip over points that lie on a line that is already
+ // going to be drawn. There is no need to have more than 2
+ // consecutive points that are collinear.
+ if (strokeWidth) {
+ ctx.beginPath();
+ ctx.strokeStyle = color;
+ ctx.lineWidth = strokeWidth;
+ if (stepPlot) {
+ this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
+ prevX = point.canvasx;
+ }
+ this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern);
+ prevX = point.canvasx;
+ prevY = point.canvasy;
+ ctx.stroke();
+ }
+ }
+
+ if (drawPoints || isIsolated) {
+ ctx.beginPath();
+ ctx.fillStyle = color;
+ ctx.arc(point.canvasx, point.canvasy, pointSize,
+ 0, 2 * Math.PI, false);
+ ctx.fill();
+ }
+ }
+ }
+ ctx.restore();
+};
+
+DygraphCanvasRenderer.prototype._drawLine = function(ctx, i) {
+ var setNames = this.layout.setNames;
+ var setName = setNames[i];
+
+ var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
+ var borderWidth = this.dygraph_.attr_("strokeBorderWidth", setName);
+ if (borderWidth && strokeWidth) {
+ this._drawStyledLine(ctx, i,
+ this.dygraph_.attr_("strokeBorderColor", setName),
+ strokeWidth + 2 * borderWidth,
+ this.dygraph_.attr_("strokePattern", setName),
+ this.dygraph_.attr_("drawPoints", setName),
+ this.dygraph_.attr_("pointSize", setName));
+ }
+
+ this._drawStyledLine(ctx, i,
+ this.colors[setName],
+ strokeWidth,
+ this.dygraph_.attr_("strokePattern", setName),
+ this.dygraph_.attr_("drawPoints", setName),
+ this.dygraph_.attr_("pointSize", setName));
+};
/**
* Actually draw the lines chart, including error bars.
* @private
*/
DygraphCanvasRenderer.prototype._renderLineChart = function() {
- var isNullOrNaN = function(x) {
- return (x === null || isNaN(x));
- };
-
// TODO(danvk): use this.attr_ for many of these.
- var context = this.elementContext;
+ var ctx = this.elementContext;
var fillAlpha = this.attr_('fillAlpha');
var errorBars = this.attr_("errorBars") || this.attr_("customBars");
var fillGraph = this.attr_("fillGraph");
}
// create paths
- var ctx = context;
if (errorBars) {
+ ctx.save();
if (fillGraph) {
this.dygraph_.warn("Can't use fillGraph option with error bars");
}
color = this.colors[setName];
// setup graphics context
- ctx.save();
prevX = NaN;
prevY = NaN;
prevYs = [-1, -1];
}
ctx.fill();
}
+ ctx.restore();
} else if (fillGraph) {
+ ctx.save();
var baseline = []; // for stacked graphs: baseline for filling
// process sets in reverse order (needed for stacked graphs)
axisY = this.area.h * axisY + this.area.y;
// setup graphics context
- ctx.save();
prevX = NaN;
prevYs = [-1, -1];
yscale = axis.yscale;
}
ctx.fill();
}
+ ctx.restore();
}
// Drawing the lines.
var afterLastIndexInSet = 0;
var setLength = 0;
for (i = 0; i < setCount; i += 1) {
- firstIndexInSet = this.layout.setPointsOffsets[i];
- setLength = this.layout.setPointsLengths[i];
- afterLastIndexInSet = firstIndexInSet + setLength;
- setName = setNames[i];
- color = this.colors[setName];
- var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
-
- // setup graphics context
- context.save();
- var pointSize = this.dygraph_.attr_("pointSize", setName);
- prevX = null;
- prevY = null;
- var drawPoints = this.dygraph_.attr_("drawPoints", setName);
- var strokePattern = this.dygraph_.attr_("strokePattern", setName);
- if (!Dygraph.isArrayLike(strokePattern)) {
- strokePattern = null;
- }
- for (j = firstIndexInSet; j < afterLastIndexInSet; j++) {
- point = points[j];
- if (isNullOrNaN(point.canvasy)) {
- if (stepPlot && prevX !== null) {
- // Draw a horizontal line to the start of the missing data
- ctx.beginPath();
- ctx.strokeStyle = color;
- ctx.lineWidth = this.attr_('strokeWidth');
- this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
- ctx.stroke();
- }
- // this will make us move to the next point, not draw a line to it.
- prevX = prevY = null;
- } else {
- // A point is "isolated" if it is non-null but both the previous
- // and next points are null.
- var isIsolated = (!prevX && (j == points.length - 1 ||
- isNullOrNaN(points[j+1].canvasy)));
- if (prevX === null) {
- prevX = point.canvasx;
- prevY = point.canvasy;
- } else {
- // Skip over points that will be drawn in the same pixel.
- if (Math.round(prevX) == Math.round(point.canvasx) &&
- Math.round(prevY) == Math.round(point.canvasy)) {
- continue;
- }
- // TODO(antrob): skip over points that lie on a line that is already
- // going to be drawn. There is no need to have more than 2
- // consecutive points that are collinear.
- if (strokeWidth) {
- ctx.beginPath();
- ctx.strokeStyle = color;
- ctx.lineWidth = strokeWidth;
- if (stepPlot) {
- this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
- prevX = point.canvasx;
- }
- this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern);
- prevX = point.canvasx;
- prevY = point.canvasy;
- ctx.stroke();
- }
- }
-
- if (drawPoints || isIsolated) {
- ctx.beginPath();
- ctx.fillStyle = color;
- ctx.arc(point.canvasx, point.canvasy, pointSize,
- 0, 2 * Math.PI, false);
- ctx.fill();
- }
- }
- }
+ this._drawLine(ctx, i);
}
-
- context.restore();
};
/**
// Default attribute values.
Dygraph.DEFAULT_ATTRS = {
highlightCircleSize: 3,
+ highlightSeriesOpts: null,
+ highlightSeriesBackgroundFade: 0,
+ highlightSeriesAnimated: false,
labelsDivWidth: 250,
labelsDivStyles: {
sigFigs: null,
strokeWidth: 1.0,
+ strokeBorderWidth: 0,
+ strokeBorderColor: "white",
axisTickSize: 3,
axisLabelFontSize: 14,
this.boundaryIds_ = [];
this.setIndexByName_ = {};
+ this.datasetIndex_ = [];
// Create the containing DIV and other interactive elements
this.createInterface_();
Dygraph.OPTIONS_REFERENCE[name] = true;
}
// </REMOVE_FOR_COMBINED>
- if (this.user_attrs_ !== null && seriesName &&
- typeof(this.user_attrs_[seriesName]) != 'undefined' &&
- this.user_attrs_[seriesName] !== null &&
- typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
- return this.user_attrs_[seriesName][name];
- } else if (this.user_attrs_ !== null && typeof(this.user_attrs_[name]) != 'undefined') {
- return this.user_attrs_[name];
- } else if (this.attrs_ !== null && typeof(this.attrs_[name]) != 'undefined') {
- return this.attrs_[name];
- } else {
- return null;
+
+ var sources = [];
+ sources.push(this.attrs_);
+ if (this.user_attrs_) sources.push(this.user_attrs_);
+ if (this.user_attrs_ && 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']);
+ }
}
+
+ 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;
};
/**
};
/**
+ * Get the current graph's area object.
+ *
+ * Returns: {x, y, w, h}
+ */
+Dygraph.prototype.getArea = function() {
+ return this.plotter_.area;
+};
+
+/**
+ * Convert a mouse event to DOM coordinates relative to the graph origin.
+ *
+ * Returns a two-element array: [X, Y].
+ */
+Dygraph.prototype.eventToDomCoords = function(event) {
+ var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
+ var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(this.mouseEventElement_);
+ return [canvasx, canvasy];
+};
+
+/**
+ * Given a canvas X coordinate, find the closest row.
+ * @param {Number} domX graph-relative DOM X coordinate
+ * Returns: row number, integer
+ * @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) continue;
+ minDistX = dist;
+ idx = i;
+ }
+ return this.idxToRow_(idx);
+};
+
+/**
+ * Given canvas X,Y coordinates, find the closest point
+ * @param {Number} domX graph-relative DOM X coordinate
+ * @param {Number} domY graph-relative DOM Y coordinate
+ * Returns: {row, seriesName, point}
+ * @private
+ */
+Dygraph.prototype.findClosestPoint = function(domX, domY) {
+ var minDist = null;
+ 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;
+ dx = point.canvasx - domX;
+ dy = point.canvasy - domY;
+ dist = dx * dx + dy * dy;
+ if (minDist !== null && dist >= minDist) continue;
+ minDist = dist;
+ closestPoint = point;
+ closestSeries = setIdx;
+ idx = i;
+ }
+ }
+ var name = this.layout_.setNames[closestSeries];
+ return {
+ row: idx,
+ seriesName: name,
+ point: closestPoint
+ };
+};
+
+/**
+ * Given canvas X,Y coordinates, find the touched area in a stacked graph.
+ * @param {Number} domX graph-relative DOM X coordinate
+ * @param {Number} domY graph-relative DOM Y coordinate
+ * Returns: {row, seriesName, point}
+ * @private
+ */
+Dygraph.prototype.findStackedPoint = function(domX, domY) {
+ var row = this.findClosestRow(domX);
+ var points = this.layout_.points;
+ 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 py = p1.canvasy;
+ if (domX > p1.canvasx && row + 1 < len) {
+ // 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);
+ }
+ } else if (domX < p1.canvasx && row > 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);
+ }
+ }
+ // Stop if the point (domX, py) is above this series' upper edge
+ if (setIdx > 0 && py >= domY) break;
+ closestPoint = p1;
+ closestSeries = setIdx;
+ }
+ var name = this.layout_.setNames[closestSeries];
+ return {
+ row: row,
+ seriesName: name,
+ point: closestPoint
+ };
+};
+
+/**
* 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.
var points = this.layout_.points;
if (points === undefined) return;
- var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
-
- var lastx = -1;
- var i;
+ var canvasCoords = this.eventToDomCoords(event);
+ var canvasx = canvasCoords[0];
+ var canvasy = canvasCoords[1];
- // Loop through all the points and find the date nearest to our current
- // location.
- var minDist = 1e+100;
- var idx = -1;
- for (i = 0; i < points.length; i++) {
- var point = points[i];
- if (point === null) continue;
- var dist = Math.abs(point.canvasx - canvasx);
- if (dist > minDist) continue;
- minDist = dist;
- idx = i;
+ var mouseoverCallback = this.attr_("mouseoverCallback");
+ if (mouseoverCallback) {
+ var highlightRow = this.idxToRow_(idx);
+ var ret = mouseoverCallback(this, event);
+ if (ret) return;
}
- if (idx >= 0) lastx = points[idx].xval;
- // Extract the points we've selected
- this.selPoints_ = [];
- var l = points.length;
- if (!this.attr_("stackedGraph")) {
- for (i = 0; i < l; i++) {
- if (points[i].xval == lastx) {
- this.selPoints_.push(points[i]);
- }
+ var highlightSeriesOpts = this.attr_("highlightSeriesOpts");
+ var selectionChanged = false;
+ if (highlightSeriesOpts) {
+ var closest;
+ if (this.attr_("stackedGraph")) {
+ closest = this.findStackedPoint(canvasx, canvasy);
+ } else {
+ closest = this.findClosestPoint(canvasx, canvasy);
}
+ selectionChanged = this.setSelection(closest.row, closest.seriesName);
} else {
- // Need to 'unstack' points starting from the bottom
- var cumulative_sum = 0;
- for (i = l - 1; i >= 0; i--) {
- if (points[i].xval == lastx) {
- var p = {}; // Clone the point since we modify it
- for (var k in points[i]) {
- p[k] = points[i][k];
- }
- p.yval -= cumulative_sum;
- cumulative_sum += p.yval;
- this.selPoints_.push(p);
- }
- }
- this.selPoints_.reverse();
+ var idx = this.findClosestRow(canvasx);
+ selectionChanged = this.setSelection(idx);
}
- if (this.attr_("highlightCallback")) {
- var px = this.lastx_;
- if (px !== null && lastx != px) {
- // only fire if the selected point has changed.
- this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
- }
+ var callback = this.attr_("highlightCallback");
+ if (callback && selectionChanged) {
+ callback(event, this.lastx_, this.selPoints_, this.lastRow_, this.highlightSet_);
}
-
- // Save last x position for callbacks.
- this.lastx_ = lastx;
-
- this.updateSelection_();
};
/**
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 +
+ html += "<span style='font-weight: bold; color: " + c + ";'>" + dash +
" " + labels[i] + "</span>";
}
return html;
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 += " <b><span style='color: " + c + ";'>" + pt.name +
- "</span></b>:" + yval;
+ html += "<span" + cls + ">" + " <b><span style='color: " + c + ";'>" + pt.name +
+ "</span></b>:" + yval + "</span>";
}
return html;
};
}
};
+Dygraph.prototype.animateSelection_ = function(direction) {
+ var totalSteps = 10;
+ var millis = 30;
+ if (this.fadeLevel === undefined) {
+ this.fadeLevel = 0;
+ this.animateId = 0;
+ }
+ var start = this.fadeLevel;
+ var steps = direction < 0 ? start : totalSteps - start;
+ if (steps <= 0) {
+ if (this.fadeLevel) {
+ this.updateSelection_(1.0);
+ }
+ return;
+ }
+
+ var thisId = ++this.animateId;
+ var that = this;
+ Dygraph.repeatAndCleanup(function(n) {
+ // ignore simultaneous animations
+ if (that.animateId != thisId) return;
+
+ that.fadeLevel += direction;
+ if (that.fadeLevel === 0) {
+ that.clearSelection();
+ } else {
+ that.updateSelection_(that.fadeLevel / totalSteps);
+ }
+ },
+ steps, millis, function() {});
+};
+
/**
* Draw dots over the selectied points in the data series. This function
* takes care of cleanup of previously-drawn dots.
* @private
*/
-Dygraph.prototype.updateSelection_ = function() {
+Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
// Clear the previously drawn vertical, if there is one
var i;
var ctx = this.canvas_ctx_;
- if (this.previousVerticalX_ >= 0) {
+ if (this.attr_('highlightSeriesOpts')) {
+ ctx.clearRect(0, 0, this.width_, this.height_);
+ var alpha = this.attr_('highlightSeriesBackgroundFade');
+ if (alpha) {
+ if (this.attr_('highlightSeriesAnimate')) {
+ if (opt_animFraction === undefined) {
+ // start a new animation
+ this.animateSelection_(1);
+ return;
+ }
+ alpha *= opt_animFraction;
+ }
+ ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')';
+ ctx.fillRect(0, 0, this.width_, this.height_);
+ }
+ var setIdx = this.datasetIndexFromSetName_(this.highlightSet_);
+ var underlay = this.attr_('highlightUnderlay');
+ if (underlay) underlay(this, ctx, setIdx);
+ this.plotter_._drawLine(ctx, setIdx);
+ } else if (this.previousVerticalX_ >= 0) {
// Determine the maximum highlight circle size.
var maxCircleSize = 0;
var labels = this.attr_('labels');
* using getSelection().
* @param { Integer } row number that should be highlighted (i.e. appear with
* 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.
*/
-Dygraph.prototype.setSelection = function(row) {
+Dygraph.prototype.setSelection = function(row, opt_seriesName) {
// Extract the points we've selected
this.selPoints_ = [];
var pos = 0;
if (row !== false) {
- row = row - this.boundaryIds_[0][0];
+ for (var i = 0; i < this.boundaryIds_.length; i++) {
+ if (this.boundaryIds_[i] !== undefined) {
+ row -= this.boundaryIds_[i][0];
+ break;
+ }
+ }
}
+ var changed = false;
if (row !== false && row >= 0) {
+ if (row != this.lastRow_) changed = true;
+ this.lastRow_ = row;
for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
var set = this.layout_.datasets[setIdx];
if (row < set.length) {
}
pos += set.length;
}
+ } else {
+ if (this.lastRow_ >= 0) changed = true;
+ this.lastRow_ = -1;
}
if (this.selPoints_.length) {
this.lastx_ = this.selPoints_[0].xval;
- this.updateSelection_();
} else {
- this.clearSelection();
+ this.lastx_ = -1;
}
+ if (opt_seriesName !== undefined) {
+ if (this.highlightSet_ !== opt_seriesName) changed = true;
+ this.highlightSet_ = opt_seriesName;
+ }
+
+ if (changed) {
+ this.updateSelection_(undefined);
+ }
+ return changed;
};
/**
*/
Dygraph.prototype.clearSelection = function() {
// Get rid of the overlay data
+ if (this.fadeLevel) {
+ this.animateSelection_(-1);
+ return;
+ }
this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
+ this.fadeLevel = 0;
this.setLegendHTML_();
this.selPoints_ = [];
this.lastx_ = -1;
+ this.lastRow_ = -1;
+ this.highlightSet_ = null;
};
/**
return -1;
};
+Dygraph.prototype.getHighlightSeries = function() {
+ return this.highlightSet_;
+};
+
/**
* Fires when there's data available to be graphed.
* @param {String} data Raw CSV data to be plotted
if (labels.length > 0) {
this.setIndexByName_[labels[0]] = 0;
}
+ var dataIdx = 0;
for (var i = 1; i < datasets.length; i++) {
this.setIndexByName_[labels[i]] = i;
if (!this.visibility()[i - 1]) continue;
this.layout_.addDataset(labels[i], datasets[i]);
+ this.datasetIndex_[i] = dataIdx++;
}
this.computeYAxisRanges_(extremes);
};
/**
+ * Get the internal dataset index given its name. These are numbered starting from 0,
+ * and only count visible sets.
+ * @private
+ */
+Dygraph.prototype.datasetIndexFromSetName_ = function(name) {
+ return this.datasetIndex_[this.indexFromSetName(name)];
+};
+
+/**
* @private
* Adds a default style for the annotation CSS classes to the document. This is
* only executed when annotations are actually used. It is designed to only be