X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;ds=inline;f=dygraph.js;h=79b53a939248263160b32a81f92c6e7f8c4a94d7;hb=9d31b3fe6bb776384da58c37dc9b2ba1304f1fd0;hp=9264961e348e6903900319ad00fb69a30e99ad02;hpb=4a0cb9c4671b5c212da6ebc71e25fb9d1aaec692;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 9264961..79b53a9 100644 --- a/dygraph.js +++ b/dygraph.js @@ -186,6 +186,8 @@ Dygraph.dateAxisFormatter = function(date, granularity) { // Default attribute values. Dygraph.DEFAULT_ATTRS = { highlightCircleSize: 3, + highlightSeriesOpts: null, + highlightSeriesBackgroundAlpha: 0.5, labelsDivWidth: 250, labelsDivStyles: { @@ -202,6 +204,8 @@ Dygraph.DEFAULT_ATTRS = { sigFigs: null, strokeWidth: 1.0, + strokeBorderWidth: 0, + strokeBorderColor: "white", axisTickSize: 3, axisLabelFontSize: 14, @@ -397,6 +401,8 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS); this.boundaryIds_ = []; + this.setIndexByName_ = {}; + this.datasetIndex_ = []; // Create the containing DIV and other interactive elements this.createInterface_(); @@ -451,18 +457,31 @@ Dygraph.prototype.attr_ = function(name, seriesName) { Dygraph.OPTIONS_REFERENCE[name] = true; } // - if (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 (typeof(this.user_attrs_[name]) != 'undefined') { - return this.user_attrs_[name]; - } else if (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 (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; }; /** @@ -830,21 +849,27 @@ Dygraph.prototype.createInterface_ = function() { } var dygraph = this; - Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) { - dygraph.mouseMove_(e); - }); - Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) { - dygraph.mouseOut_(e); - }); + + this.mouseMoveHandler = function(e) { + dygraph.mouseMove_(e); + }; + Dygraph.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); + + this.mouseOutHandler = function(e) { + dygraph.mouseOut_(e); + }; + Dygraph.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); this.createStatusMessage_(); this.createDragInterface_(); + 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', function(e) { - dygraph.resize(); - }); + Dygraph.addEvent(window, 'resize', this.resizeHandler); }; /** @@ -859,6 +884,10 @@ Dygraph.prototype.destroy = function() { node.removeChild(node.firstChild); } }; + + // remove mouse event handlers + Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); + Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); removeRecursive(this.maindiv_); var nullOut = function(obj) { @@ -868,7 +897,9 @@ Dygraph.prototype.destroy = function() { } } }; - + // 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_); @@ -990,7 +1021,11 @@ Dygraph.prototype.createStatusMessage_ = function() { div.className = "dygraph-legend"; for (var name in messagestyle) { if (messagestyle.hasOwnProperty(name)) { - div.style[name] = messagestyle[name]; + try { + div.style[name] = messagestyle[name]; + } catch (e) { + this.warn("You are using unsupported css properties for your browser in labelsDivStyles"); + } } } this.graphDiv.appendChild(div); @@ -1327,7 +1362,7 @@ Dygraph.prototype.doUnzoom_ = function() { } for (var i = 0; i < this.axes_.length; i++) { - if (this.axes_[i].valueWindow !== null) { + if (typeof(this.axes_[i].valueWindow) !== 'undefined' && this.axes_[i].valueWindow !== null) { dirty = true; dirtyY = true; } @@ -1379,7 +1414,8 @@ Dygraph.prototype.doUnzoom_ = function() { newValueRanges = []; for (i = 0; i < this.axes_.length; i++) { - newValueRanges.push(this.axes_[i].extremeRange); + var axis = this.axes_[i]; + newValueRanges.push(axis.valueRange != null ? axis.valueRange : axis.extremeRange); } } @@ -1447,74 +1483,197 @@ Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, ne }; /** - * 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. - * @param {Object} event The mousemove event from the browser. - * @private + * Get the current graph's area object. + * + * Returns: {x, y, w, h} */ -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; +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]; +}; - var lastx = -1; - var i; - - // Loop through all the points and find the date nearest to our current - // location. - var minDist = 1e+100; +/** + * 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 = Infinity; var idx = -1; - for (i = 0; i < points.length; i++) { + 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 - canvasx); - if (dist > minDist) continue; - minDist = dist; - idx = i; + if (!Dygraph.isValidPoint(point, true)) continue; + var dist = Math.abs(point.canvasx - domX); + if (dist < minDistX) { + minDistX = dist; + idx = i; + } } - if (idx >= 0) lastx = points[idx].xval; + return this.idxToRow_(idx); +}; - // 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]); +/** + * Given canvas X,Y coordinates, find the closest point. + * + * This finds the individual data point across all visible series + * that's closest to the supplied DOM coordinates using the standard + * Euclidean X,Y distance. + * + * @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 = 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 (!Dygraph.isValidPoint(point)) continue; + dx = point.canvasx - domX; + dy = point.canvasy - domY; + dist = dx * dx + dy * dy; + if (dist < minDist) { + minDist = dist; + closestPoint = point; + closestSeries = setIdx; + idx = i; } } - } 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]; + } + var name = this.layout_.setNames[closestSeries]; + return { + row: idx + this.getLeftBoundary_(), + seriesName: name, + point: closestPoint + }; +}; + +/** + * Given canvas X,Y coordinates, find the touched area in a stacked graph. + * + * This first finds the X data point closest to the supplied DOM X coordinate, + * then finds the series which puts the Y coordinate on top of its filled area, + * using linear interpolation between adjacent point pairs. + * + * @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 boundary = this.getLeftBoundary_(); + var rowIdx = row - boundary; + 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 (rowIdx >= len) continue; + var p1 = points[first + rowIdx]; + if (!Dygraph.isValidPoint(p1)) continue; + var py = p1.canvasy; + if (domX > p1.canvasx && rowIdx + 1 < len) { + // interpolate series Y value using next point + var p2 = points[first + 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 && rowIdx > 0) { + // interpolate series Y value using previous point + var p0 = points[first + 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); } - p.yval -= cumulative_sum; - cumulative_sum += p.yval; - this.selPoints_.push(p); } } - this.selPoints_.reverse(); + // Stop if the point (domX, py) is above this series' upper edge + if (setIdx == 0 || py < domY) { + closestPoint = p1; + closestSeries = setIdx; + } } + var name = this.layout_.setNames[closestSeries]; + return { + row: row, + seriesName: name, + point: closestPoint + }; +}; - 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)); +/** + * 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. + * @param {Object} event The mousemove event from the browser. + * @private + */ +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; + + var canvasCoords = this.eventToDomCoords(event); + var canvasx = canvasCoords[0]; + var canvasy = canvasCoords[1]; + + 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 { + var idx = this.findClosestRow(canvasx); + selectionChanged = this.setSelection(idx); } - // Save last x position for callbacks. - this.lastx_ = lastx; + var callback = this.attr_("highlightCallback"); + if (callback && selectionChanged) { + callback(event, this.lastx_, this.selPoints_, this.lastRow_, this.highlightSet_); + } +}; - this.updateSelection_(); +/** + * Fetch left offset from first defined boundaryIds record (see bug #236). + */ +Dygraph.prototype.getLeftBoundary_ = function() { + for (var i = 0; i < this.boundaryIds_.length; i++) { + if (this.boundaryIds_[i] !== undefined) { + return this.boundaryIds_[i][0]; + } + } + return 0; }; /** @@ -1526,29 +1685,105 @@ Dygraph.prototype.mouseMove_ = function(event) { Dygraph.prototype.idxToRow_ = function(idx) { if (idx < 0) return -1; - for (var i in this.layout_.datasets) { - if (idx < this.layout_.datasets[i].length) { - return this.boundaryIds_[0][0]+idx; + var boundary = this.getLeftBoundary_(); + for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { + var set = this.layout_.datasets[setIdx]; + if (idx < set.length) { + return boundary + idx; } - idx -= this.layout_.datasets[i].length; + 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 = "
"; + } 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 += ""; + } + } + } + return dash; +}; + +/** + * @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) { +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; + var html, sepLines, i, c, dash, strokePattern; if (typeof(x) === 'undefined') { if (this.attr_('legend') != 'always') return ''; @@ -1559,8 +1794,10 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { if (!this.visibility()[i - 1]) continue; c = this.plotter_.colors[labels[i]]; if (html !== '') html += (sepLines ? '