Dygraph.HORIZONTAL = 1;
Dygraph.VERTICAL = 2;
+// Installed plugins, in order of precedence (most-general to most-specific).
+// Plugins are installed after they are defined, in plugins/install.js.
+Dygraph.PLUGINS = [
+];
+
// Used for initializing annotation CSS rules only once.
Dygraph.addedAnnotationCSS = false;
// Create the containing DIV and other interactive elements
this.createInterface_();
+ // Activate plugins.
+ this.plugins_ = [];
+ for (var i = 0; i < Dygraph.PLUGINS.length; i++) {
+ var plugin = Dygraph.PLUGINS[i];
+ var pluginInstance = new plugin();
+ var pluginDict = {
+ plugin: pluginInstance,
+ events: {},
+ options: {},
+ pluginOptions: {}
+ };
+
+ var registerer = (function(pluginDict) {
+ return {
+ addEventListener: function(eventName, callback) {
+ // TODO(danvk): validate eventName.
+ pluginDict.events[eventName] = callback;
+ }
+ };
+ })(pluginDict);
+ pluginInstance.activate(this, registerer);
+ // TODO(danvk): prevent activate() from holding a reference to registerer.
+
+ this.plugins_.push(pluginDict);
+ }
+
+ // At this point, plugins can no longer register event handlers.
+ // Construct a map from event -> ordered list of [callback, plugin].
+ this.eventListeners_ = {};
+ for (var i = 0; i < this.plugins_.length; i++) {
+ var plugin_dict = this.plugins_[i];
+ for (var eventName in plugin_dict.events) {
+ if (!plugin_dict.events.hasOwnProperty(eventName)) continue;
+ var callback = plugin_dict.events[eventName];
+
+ var pair = [plugin_dict.plugin, callback];
+ if (!(eventName in this.eventListeners_)) {
+ this.eventListeners_[eventName] = [pair];
+ } else {
+ this.eventListeners_[eventName].push(pair);
+ }
+ }
+ }
+
this.start_();
};
/**
+ * Triggers a cascade of events to the various plugins which are interested in them.
+ * Returns true if the "default behavior" should be performed, i.e. if none of
+ * the event listeners called event.preventDefault().
+ * @private
+ */
+Dygraph.prototype.cascadeEvents_ = function(name, extra_props) {
+ if (!name in this.eventListeners_) return true;
+
+ // QUESTION: can we use objects & prototypes to speed this up?
+ var e = {
+ dygraph: this,
+ cancelable: false,
+ defaultPrevented: false,
+ preventDefault: function() {
+ if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event.";
+ e.defaultPrevented = true;
+ },
+ propagationStopped: false,
+ stopPropagation: function() {
+ propagationStopped = true;
+ }
+ };
+ Dygraph.update(e, extra_props);
+
+ var callback_plugin_pairs = this.eventListeners_[name];
+ for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) {
+ var plugin = callback_plugin_pairs[i][0];
+ var callback = callback_plugin_pairs[i][1];
+ callback.call(plugin, e);
+ if (e.propagationStopped) break;
+ }
+ return e.defaultPrevented;
+};
+
+/**
* Returns the zoomed status of the chart for one or both axes.
*
* Axis is an optional parameter. Can be set to 'x' or 'y'.
};
/**
+ * Returns the current value for an option, as set in the constructor or via
+ * updateOptions. You may pass in an (optional) series name to get per-series
+ * values for the option.
+ *
+ * All values returned by this method should be considered immutable. If you
+ * modify them, there is no guarantee that the changes will be honored or that
+ * dygraphs will remain in a consistent state. If you want to modify an option,
+ * use updateOptions() instead.
+ *
+ * @param { String } name The name of the option (e.g. 'strokeWidth')
+ * @param { String } [opt_seriesName] Series name to get per-series values.
+ * @return { ... } The value of the option.
+ */
+Dygraph.prototype.getOption = function(name, opt_seriesName) {
+ return this.attr_(name, opt_seriesName);
+};
+
+/**
* @private
* @param String} axis The name of the axis (i.e. 'x', 'y' or 'y2')
* @return { ... } A function mapping string -> option value
};
Dygraph.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler);
- this.createStatusMessage_();
this.createDragInterface_();
this.resizeHandler = function(e) {
/**
* Return the list of colors. This is either the list of colors passed in the
* attributes or the autogenerated list of rgb(r,g,b) strings.
+ * This does not return colors for invisible series.
* @return {Array<string>} The list of colors.
*/
Dygraph.prototype.getColors = function() {
};
/**
- * Create the div that contains information on the selected point(s)
- * This goes in the top right of the canvas, unless an external div has already
- * been specified.
- * @private
+ * Returns a few attributes of a series, i.e. its color, its visibility, which
+ * axis it's assigned to, and its column in the original data.
+ * Returns null if the series does not exist.
+ * Otherwise, returns an object with column, visibility, color and axis properties.
+ * The "axis" property will be set to 1 for y1 and 2 for y2.
+ * The "column" property can be fed back into getValue(row, column) to get
+ * values for this series.
*/
-Dygraph.prototype.createStatusMessage_ = function() {
- var userLabelsDiv = this.user_attrs_.labelsDiv;
- if (userLabelsDiv && null !== userLabelsDiv &&
- (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
- this.user_attrs_.labelsDiv = document.getElementById(userLabelsDiv);
- }
- if (!this.attr_("labelsDiv")) {
- var divWidth = this.attr_('labelsDivWidth');
- var messagestyle = {
- "position": "absolute",
- "fontSize": "14px",
- "zIndex": 10,
- "width": divWidth + "px",
- "top": "0px",
- "left": (this.width_ - divWidth - 2) + "px",
- "background": "white",
- "lineHeight": "normal",
- "textAlign": "left",
- "overflow": "hidden"};
- Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
- var div = document.createElement("div");
- div.className = "dygraph-legend";
- for (var name in messagestyle) {
- if (messagestyle.hasOwnProperty(name)) {
- try {
- div.style[name] = messagestyle[name];
- } catch (e) {
- this.warn("You are using unsupported css properties for your browser in labelsDivStyles");
- }
- }
+Dygraph.prototype.getPropertiesForSeries = function(series_name) {
+ var idx = -1;
+ var labels = this.getLabels();
+ for (var i = 1; i < labels.length; i++) {
+ if (labels[i] == series_name) {
+ idx = i;
+ break;
}
- this.graphDiv.appendChild(div);
- this.attrs_.labelsDiv = div;
}
-};
-
-/**
- * Position the labels div so that:
- * - its right edge is flush with the right edge of the charting area
- * - its top edge is flush with the top edge of the charting area
- * @private
- */
-Dygraph.prototype.positionLabelsDiv_ = function() {
- // Don't touch a user-specified labelsDiv.
- if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
+ if (idx == -1) return null;
- var area = this.plotter_.area;
- var div = this.attr_("labelsDiv");
- div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
- div.style.top = area.y + "px";
+ return {
+ name: series_name,
+ column: idx,
+ visible: this.visibility()[idx - 1],
+ color: this.plotter_.colors[series_name],
+ axis: 1 + this.seriesToAxisMap_[series_name]
+ };
};
/**
/**
* Fetch left offset from first defined boundaryIds record (see bug #236).
+ * @private
*/
Dygraph.prototype.getLeftBoundary_ = function() {
for (var i = 0; i < this.boundaryIds_.length; i++) {
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 = "<div style=\"display: inline-block; position: relative; " +
- "bottom: .5ex; padding-left: 1em; height: 1px; " +
- "border-bottom: 2px solid " + color + ";\"></div>";
- } 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 += "<div style=\"display: inline-block; position: relative; " +
- "bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " +
- paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color +
- ";\"></div>";
- }
- }
- }
- 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, 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, dash, strokePattern;
- if (typeof(x) === 'undefined') {
- if (this.attr_('legend') != 'always') return '';
-
- sepLines = this.attr_('labelsSeparateLines');
- var labels = this.attr_('labels');
- html = '';
- for (i = 1; i < labels.length; i++) {
- if (!this.visibility()[i - 1]) continue;
- c = this.plotter_.colors[labels[i]];
- 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 +
- " " + labels[i] + "</span>";
- }
- return html;
- }
-
- var xOptView = this.optionsViewForAxis_('x');
- var xvf = xOptView('valueFormatter');
- html = xvf(x, xOptView, this.attr_('labels')[0], this) + ":";
-
- var yOptViews = [];
- var num_axes = this.numAxes();
- for (i = 0; i < num_axes; i++) {
- yOptViews[i] = this.optionsViewForAxis_('y' + (i ? 1 + i : ''));
- }
- var showZeros = this.attr_("labelsShowZeroValues");
- sepLines = this.attr_("labelsSeparateLines");
- for (i = 0; i < this.selPoints_.length; i++) {
- var pt = this.selPoints_[i];
- if (pt.yval === 0 && !showZeros) continue;
- if (!Dygraph.isOK(pt.canvasy)) continue;
- if (sepLines) html += "<br/>";
-
- var yOptView = yOptViews[this.seriesToAxisMap_[pt.name]];
- var fmtFunc = yOptView('valueFormatter');
- 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 += "<span" + cls + ">" + " <b><span style='color: " + c + ";'>" + pt.name +
- "</span></b>:" + yval + "</span>";
- }
- return html;
-};
-
-/**
- * @private
- * Displays information about the selected points in the legend. If there is no
- * selection, the legend will be cleared.
- * @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'.
- */
-Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
- var labelsDiv = this.attr_("labelsDiv");
- if (!labelsDiv) return;
-
- var sizeSpan = document.createElement('span');
- // Calculates the width of 1em in pixels for the legend.
- sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
- labelsDiv.appendChild(sizeSpan);
- var oneEmWidth=sizeSpan.offsetWidth;
-
- var html = this.generateLegendHTML_(x, sel_points, oneEmWidth);
- if (labelsDiv !== null) {
- labelsDiv.innerHTML = html;
- } else {
- if (typeof(this.shown_legend_error_) == 'undefined') {
- this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
- this.shown_legend_error_ = true;
- }
- }
-};
-
Dygraph.prototype.animateSelection_ = function(direction) {
var totalSteps = 10;
var millis = 30;
* @private
*/
Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
+ var defaultPrevented = this.cascadeEvents_('select', {
+ selectedX: this.lastx_,
+ selectedPoints: this.selPoints_
+ });
+ // TODO(danvk): use defaultPrevented here?
+
// Clear the previously drawn vertical, if there is one
var i;
var ctx = this.canvas_ctx_;
}
if (this.selPoints_.length > 0) {
- // Set the status message to indicate the selected point(s)
- if (this.attr_('showLabelsOnHighlight')) {
- this.setLegendHTML_(this.lastx_, this.selPoints_);
- }
-
// Draw colored circles over the center of each selected point
var canvasx = this.selPoints_[0].canvasx;
ctx.save();
* the mouse over the chart).
*/
Dygraph.prototype.clearSelection = function() {
+ this.cascadeEvents_('deselect', {});
+
// Get rid of the overlay data
if (this.fadeLevel) {
this.animateSelection_(-1);
}
this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
this.fadeLevel = 0;
- this.setLegendHTML_();
this.selPoints_ = [];
this.lastx_ = -1;
this.lastRow_ = -1;
return -1;
};
+/**
+ * Returns the name of the currently-highlighted series.
+ * Only available when the highlightSeriesOpts option is in use.
+ */
Dygraph.prototype.getHighlightSeries = function() {
return this.highlightSet_;
};
// this will be until the options are available, so it's positioned here.
this.createRollInterface_();
- // Same thing applies for the labelsDiv. It's right edge should be flush with
- // the right edge of the charting area (which may not be the same as the right
- // edge of the div, if we have two y-axes.
- this.positionLabelsDiv_();
+ this.cascadeEvents_('predraw');
if (this.rangeSelector_) {
this.rangeSelector_.renderStaticLayer();
* has changed. If the underlying data or options have changed, predraw_ will
* be called before drawGraph_ is called.
*
- * clearSelection, when undefined or true, causes this.clearSelection to be
- * called at the end of the draw operation. This should rarely be defined,
- * and never true (that is it should be undefined most of the time, and
- * rarely false.)
- *
* @private
*/
-Dygraph.prototype.drawGraph_ = function(clearSelection) {
+Dygraph.prototype.drawGraph_ = function() {
var start = new Date();
- if (typeof(clearSelection) === 'undefined') {
- clearSelection = true;
- }
-
// This is used to set the second parameter to drawCallback, below.
var is_initial_draw = this.is_initial_draw_;
this.is_initial_draw_ = false;
this.layout_.setDateWindow(this.dateWindow_);
this.zoomed_x_ = tmp_zoomed_x;
this.layout_.evaluateWithError();
- this.renderGraph_(is_initial_draw, false);
+ this.renderGraph_(is_initial_draw);
if (this.attr_("timingName")) {
var end = new Date();
}
};
-Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) {
+/**
+ * This does the work of drawing the chart. It assumes that the layout and axis
+ * scales have already been set (e.g. by predraw_).
+ *
+ * @private
+ */
+Dygraph.prototype.renderGraph_ = function(is_initial_draw) {
this.plotter_.clear();
this.plotter_.render();
this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
this.canvas_.height);
// Generate a static legend before any particular point is selected.
- this.setLegendHTML_();
-
- if (!is_initial_draw) {
- if (clearSelection) {
- if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
- // We should select the point nearest the page x/y here, but it's easier
- // to just clear the selection. This prevents erroneous hover dots from
- // being displayed.
- this.clearSelection();
- } else {
- this.clearSelection();
- }
- }
- }
if (this.rangeSelector_) {
this.rangeSelector_.renderInteractiveLayer();
}
+ this.cascadeEvents_('drawChart');
if (this.attr_("drawCallback") !== null) {
this.attr_("drawCallback")(this, is_initial_draw);
}
if (requiresNewPoints) {
this.predraw_();
} else {
- this.renderGraph_(false, false);
+ this.renderGraph_(false);
}
}
}
* Get the list of label names for this graph. The first column is the
* x-axis, so the data series names start at index 1.
*/
-Dygraph.prototype.getLabels = function(name) {
+Dygraph.prototype.getLabels = function() {
return this.attr_("labels").slice();
};
--- /dev/null
+Dygraph.Plugins.Legend = (function() {
+
+/*
+
+Current bits of jankiness:
+- Uses two private APIs:
+ 1. Dygraph.optionsViewForAxis_
+ 2. dygraph.plotter_.area
+- Registers for a "predraw" event, which should be renamed.
+- I call calculateEmWidthInDiv more often than needed.
+- Why can't I call "this.deselect(e)" instead of "legend.deselect.call(this, e)"?
+
+*/
+
+"use strict";
+
+
+/**
+ * Creates the legend, which appears when the user hovers over the chart.
+ * The legend can be either a user-specified or generated div.
+ *
+ * @constructor
+ */
+var legend = function() {
+ this.legend_div_ = null;
+ this.is_generated_div_ = false; // do we own this div, or was it user-specified?
+};
+
+legend.prototype.toString = function() {
+ return "Legend Plugin";
+};
+
+/**
+ * This is called during the dygraph constructor, after options have been set
+ * but before the data is available.
+ *
+ * Proper tasks to do here include:
+ * - Reading your own options
+ * - DOM manipulation
+ * - Registering event listeners
+ */
+legend.prototype.activate = function(g, r) {
+ var div;
+ var divWidth = g.getOption('labelsDivWidth');
+
+ var userLabelsDiv = g.getOption('labelsDiv');
+ if (userLabelsDiv && null !== userLabelsDiv) {
+ if (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String) {
+ div = document.getElementById(userLabelsDiv);
+ } else {
+ div = userLabelsDiv;
+ }
+ } else {
+ // Default legend styles. These can be overridden in CSS by adding
+ // "!important" after your rule, e.g. "left: 30px !important;"
+ var messagestyle = {
+ "position": "absolute",
+ "fontSize": "14px",
+ "zIndex": 10,
+ "width": divWidth + "px",
+ "top": "0px",
+ "right": "2px",
+ "background": "white",
+ "textAlign": "left",
+ "overflow": "hidden"};
+
+ // TODO(danvk): get rid of labelsDivStyles? CSS is better.
+ Dygraph.update(messagestyle, g.getOption('labelsDivStyles'));
+ div = document.createElement("div");
+ div.className = "dygraph-legend";
+ for (var name in messagestyle) {
+ if (!messagestyle.hasOwnProperty(name)) continue;
+
+ try {
+ div.style[name] = messagestyle[name];
+ } catch (e) {
+ this.warn("You are using unsupported css properties for your " +
+ "browser in labelsDivStyles");
+ }
+ }
+
+ // TODO(danvk): come up with a cleaner way to expose this.
+ g.graphDiv.appendChild(div);
+ this.is_generated_div_ = true;
+ }
+
+ this.legend_div_ = div;
+
+ r.addEventListener('select', legend.select);
+ r.addEventListener('deselect', legend.deselect);
+
+ // TODO(danvk): rethink the name "predraw" before we commit to it in any API.
+ r.addEventListener('predraw', legend.predraw);
+ r.addEventListener('drawChart', legend.drawChart);
+};
+
+// Needed for dashed lines.
+var calculateEmWidthInDiv = function(div) {
+ var sizeSpan = document.createElement('span');
+ sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
+ div.appendChild(sizeSpan);
+ var oneEmWidth=sizeSpan.offsetWidth;
+ div.removeChild(sizeSpan);
+ return oneEmWidth;
+};
+
+legend.select = function(e) {
+ var xValue = e.selectedX;
+ var points = e.selectedPoints;
+
+ // Have to do this every time, since styles might have changed.
+ // TODO(danvk): this is not necessary; dashes never used in this case.
+ var oneEmWidth = calculateEmWidthInDiv(this.legend_div_);
+
+ var html = generateLegendHTML(e.dygraph, xValue, points, oneEmWidth);
+ this.legend_div_.innerHTML = html;
+};
+
+legend.deselect = function(e) {
+ var oneEmWidth = calculateEmWidthInDiv(this.legend_div_);
+ var html = generateLegendHTML(e.dygraph, undefined, undefined, oneEmWidth);
+ this.legend_div_.innerHTML = html;
+};
+
+legend.drawChart = function(e) {
+ // TODO(danvk): why doesn't this.deselect(e) work here?
+ legend.deselect.call(this, e);
+}
+
+// Right edge should be flush with the right edge of the charting area (which
+// may not be the same as the right edge of the div, if we have two y-axes.
+// TODO(danvk): is any of this really necessary? Could just set "right" in "activate".
+/**
+ * Position the labels div so that:
+ * - its right edge is flush with the right edge of the charting area
+ * - its top edge is flush with the top edge of the charting area
+ * @private
+ */
+legend.predraw = function(e) {
+ // Don't touch a user-specified labelsDiv.
+ if (!this.is_generated_div_) return;
+
+ // TODO(danvk): only use real APIs for this.
+ var area = e.dygraph.plotter_.area;
+ this.legend_div_.style.left = area.x + area.w - e.dygraph.getOption("labelsDivWidth") - 1 + "px";
+ this.legend_div_.style.top = area.y + "px";
+};
+
+/**
+ * Called when dygraph.destroy() is called.
+ * You should null out any references and detach any DOM elements.
+ */
+legend.prototype.destroy = function() {
+ this.legend_div_ = null;
+};
+
+/**
+ * @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. Only
+ * relevant when displaying a legend with no selection (i.e. {legend:
+ * 'always'}) and with dashed lines.
+ */
+var generateLegendHTML = function(g, x, sel_points, oneEmWidth) {
+ // TODO(danvk): deprecate this option in place of {legend: 'never'}
+ if (g.getOption('showLabelsOnHighlight') !== true) return '';
+
+ // 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, dash, strokePattern;
+ var labels = g.getLabels();
+
+ if (typeof(x) === 'undefined') {
+ if (g.getOption('legend') != 'always') {
+ return '';
+ }
+
+ sepLines = g.getOption('labelsSeparateLines');
+ html = '';
+ for (i = 1; i < labels.length; i++) {
+ var series = g.getPropertiesForSeries(labels[i]);
+ if (!series.visible) continue;
+
+ if (html !== '') html += (sepLines ? '<br/>' : ' ');
+ strokePattern = g.getOption("strokePattern", labels[i]);
+ dash = generateLegendDashHTML(strokePattern, series.color, oneEmWidth);
+ html += "<span style='font-weight: bold; color: " + series.color + ";'>" +
+ dash + " " + labels[i] + "</span>";
+ }
+ return html;
+ }
+
+ // TODO(danvk): remove this use of a private API
+ var xOptView = g.optionsViewForAxis_('x');
+ var xvf = xOptView('valueFormatter');
+ html = xvf(x, xOptView, labels[0], g) + ":";
+
+ var yOptViews = [];
+ var num_axes = g.numAxes();
+ for (i = 0; i < num_axes; i++) {
+ // TODO(danvk): remove this use of a private API
+ yOptViews[i] = g.optionsViewForAxis_('y' + (i ? 1 + i : ''));
+ }
+ var showZeros = g.getOption("labelsShowZeroValues");
+ sepLines = g.getOption("labelsSeparateLines");
+ var highlightSeries = g.getHighlightSeries();
+ for (i = 0; i < sel_points.length; i++) {
+ var pt = sel_points[i];
+ if (pt.yval === 0 && !showZeros) continue;
+ if (!Dygraph.isOK(pt.canvasy)) continue;
+ if (sepLines) html += "<br/>";
+
+ var series = g.getPropertiesForSeries(pt.name);
+ var yOptView = yOptViews[series.axis - 1];
+ var fmtFunc = yOptView('valueFormatter');
+ var yval = fmtFunc(pt.yval, yOptView, pt.name, g);
+
+ var cls = (pt.name == highlightSeries) ? " class='highlight'" : "";
+
+ // TODO(danvk): use a template string here and make it an attribute.
+ html += "<span" + cls + ">" + " <b><span style='color: " + series.color + ";'>" +
+ pt.name + "</span></b>:" + yval + "</span>";
+ }
+ return html;
+};
+
+
+/**
+ * Generates html for the "dash" displayed on the legend when using "legend: always".
+ * In particular, this works for dashed lines with any stroke pattern. It will
+ * try to scale the pattern to fit in 1em width. Or if small enough repeat the
+ * pattern 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.
+ * @private
+ */
+var generateLegendDashHTML = function(strokePattern, color, oneEmWidth) {
+ // 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 "—";
+
+ // Easy, common case: a solid line
+ if (!strokePattern || strokePattern.length <= 1) {
+ return "<div style=\"display: inline-block; position: relative; " +
+ "bottom: .5ex; padding-left: 1em; height: 1px; " +
+ "border-bottom: 2px solid " + color + ";\"></div>";
+ }
+
+ var i, j, paddingLeft, marginRight;
+ var strokePixelLength = 0, segmentLoop = 0;
+ var normalizedPattern = [];
+ var loop;
+
+ // 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.
+ var dash = "";
+ 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 += "<div style=\"display: inline-block; position: relative; " +
+ "bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " +
+ paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color +
+ ";\"></div>";
+ }
+ }
+ return dash;
+};
+
+
+return legend;
+})();