* @license
* Copyright 2013 Dan Vanderkam (danvdk@gmail.com)
* MIT-licensed (http://opensource.org/licenses/MIT)
+ *
+ * Note: This plugin requires jQuery and jQuery UI Draggable.
*/
/*global Dygraph:false */
+var allHairlines = [];
+
Dygraph.Plugins.Hairlines = (function() {
"use strict";
/**
-Current bits of jankiness:
-- Uses dygraph.layout_ to get the parsed hairlines.
-- Uses dygraph.plotter_.area
-
-It would be nice if the plugin didn't require so much special support inside
-the core dygraphs classes, but hairlines involve quite a bit of parsing and
-layout.
-
-TODO(danvk): cache DOM elements.
-
-*/
-
-/**
+ * xFraction is the position of the hairline on the chart, where 0.0=left edge
+ * of the chart area and 1.0=right edge. Unlike 'canvas' coordinates, it does
+ * not include the y-axis labels.
+ *
* @typedef {
* xFraction: number, // invariant across resize
* interpolated: bool, // alternative is to snap to closest
* } Hairline
*/
+// We have to wait a few ms after clicks to give the user a chance to
+// double-click to unzoom. This sets that delay period.
+var CLICK_DELAY_MS = 300;
+
var hairlines = function() {
/* @type {!Array.<!Hairline>} */
this.hairlines_ = [];
this.lastWidth_ = -1;
this.lastHeight = -1;
this.dygraph_ = null;
+
+ this.addTimer_ = null;
};
hairlines.prototype.toString = function() {
this.hairlines_ = [this.createHairline(0.55)];
return {
- didDrawChart: this.didDrawChart
+ didDrawChart: this.didDrawChart,
+ click: this.click,
+ dblclick: this.dblclick
};
};
this.updateHairlineInfo();
};
+// This creates the hairline object and returns it.
+// It does not position it and does not attach it to the chart.
hairlines.prototype.createHairline = function(xFraction) {
var h;
var self = this;
var $lineDiv = $('<div/>').css({
- 'border-right': '1px solid black',
- 'width': '0px',
- 'position': 'absolute',
- 'z-index': '10'
- })
- .addClass('dygraph-hairline')
- .appendTo(this.dygraph_.graphDiv);
-
- var $infoDiv = $('<div/>').css({
- 'border': '1px solid black',
- 'display': 'table', // shrink to fit
- 'z-index': '10',
- 'padding': '3px',
- 'background': 'white',
- 'position': 'absolute'
- })
- .addClass('dygraph-hairline-info')
- .text('Info')
+ 'border-right': '1px solid black',
+ 'width': '0px',
+ 'position': 'absolute',
+ 'z-index': '10'
+ })
+ .addClass('dygraph-hairline');
+
+ var $infoDiv = $('#hairline-template').clone().removeAttr('id').css({
+ 'position': 'absolute'
+ })
+ .show()
.draggable({
'axis': 'x',
'containment': 'parent',
self.hairlineWasDragged(h, event, ui);
}
// TODO(danvk): set cursor here
- })
- .appendTo(this.dygraph_.graphDiv);
+ });
h = {
xFraction: xFraction,
infoDiv: $infoDiv.get(0)
};
+ var that = this;
+ $infoDiv.on('click', '.hairline-kill-button', function() {
+ that.removeHairline(h);
+ });
+
return h;
};
// Fills out the info div based on current coordinates.
hairlines.prototype.updateHairlineInfo = function() {
+ var mode = 'closest';
+
var g = this.dygraph_;
var xRange = g.xAxisRange();
$.each(this.hairlines_, function(idx, h) {
var xValue = h.xFraction * (xRange[1] - xRange[0]) + xRange[0];
- // TODO(danvk): find appropriate y-values and format them.
+ var row = null;
+ if (mode == 'closest') {
+ // TODO(danvk): make this dygraphs method public
+ row = g.findClosestRow(g.toDomXCoord(xValue));
+ } else if (mode == 'interpolate') {
+ // ...
+ }
+
+ // To use generateLegendHTML, we have to synthesize an array of selected
+ // points.
+ var selPoints = [];
+ var labels = g.getLabels();
+ for (var i = 1; i < g.numColumns(); i++) {
+ selPoints.push({
+ canvasx: 1,
+ canvasy: 1,
+ xval: xValue,
+ yval: g.getValue(row, i),
+ name: labels[i]
+ });
+ }
+
+ var html = Dygraph.Plugins.Legend.generateLegendHTML(g, xValue, selPoints, 10);
+ $('.hairline-legend', h.infoDiv).html(html);
+ });
+};
- var xOptView = g.optionsViewForAxis_('x');
- var xvf = xOptView('valueFormatter');
- var html = xvf(xValue, xOptView, xValue, g);
- $(h.infoDiv).html(html);
+// After a resize, the hairline divs can get dettached from the chart.
+// This reattaches them.
+hairlines.prototype.attachHairlinesToChart_ = function() {
+ var div = this.dygraph_.graphDiv;
+ $.each(this.hairlines_, function(idx, h) {
+ $([h.lineDiv, h.infoDiv]).appendTo(div);
});
};
+// Deletes a hairline and removes it from the chart.
+hairlines.prototype.removeHairline = function(h) {
+ var idx = this.hairlines_.indexOf(h);
+ if (idx >= 0) {
+ this.hairlines_.splice(idx, 1);
+ $([h.lineDiv, h.infoDiv]).remove();
+ } else {
+ Dygraph.warn('Tried to remove non-existent hairline.');
+ }
+};
+
hairlines.prototype.didDrawChart = function(e) {
var g = e.dygraph;
+ allHairlines = this.hairlines_;
+
// Early out in the (common) case of zero hairlines.
if (this.hairlines_.length === 0) return;
+ // TODO(danvk): recreate the hairline divs when the chart resizes.
var containerDiv = e.canvas.parentNode;
var width = containerDiv.offsetWidth;
var height = containerDiv.offsetHeight;
this.lastWidth_ = width;
this.lastHeight_ = height;
this.updateHairlineDivPositions();
+ this.attachHairlinesToChart_();
}
this.updateHairlineInfo();
};
+hairlines.prototype.click = function(e) {
+ if (this.addTimer_) {
+ // Another click is in progress; ignore this one.
+ return;
+ }
+
+ var area = e.dygraph.getArea();
+ var xFraction = (e.canvasx - area.x) / area.w;
+
+ var that = this;
+ this.addTimer_ = setTimeout(function() {
+ that.addTimer_ = null;
+ that.hairlines_.push(that.createHairline(xFraction));
+
+ that.updateHairlineDivPositions();
+ that.updateHairlineInfo();
+ that.attachHairlinesToChart_();
+ }, CLICK_DELAY_MS);
+};
+
+hairlines.prototype.dblclick = function(e) {
+ if (this.addTimer_) {
+ clearTimeout(this.addTimer_);
+ this.addTimer_ = null;
+ }
+};
+
hairlines.prototype.destroy = function() {
this.detachLabels();
};
};
// (defined below)
-var generateLegendHTML, generateLegendDashHTML;
+var generateLegendDashHTML;
/**
* This is called during the dygraph constructor, after options have been set
var xValue = e.selectedX;
var points = e.selectedPoints;
- var html = generateLegendHTML(e.dygraph, xValue, points, this.one_em_width_);
+ var html = legend.generateLegendHTML(e.dygraph, xValue, points, this.one_em_width_);
this.legend_div_.innerHTML = html;
};
var oneEmWidth = calculateEmWidthInDiv(this.legend_div_);
this.one_em_width_ = oneEmWidth;
- var html = generateLegendHTML(e.dygraph, undefined, undefined, oneEmWidth);
+ var html = legend.generateLegendHTML(e.dygraph, undefined, undefined, oneEmWidth);
this.legend_div_.innerHTML = html;
};
* relevant when displaying a legend with no selection (i.e. {legend:
* 'always'}) and with dashed lines.
*/
-generateLegendHTML = function(g, x, sel_points, oneEmWidth) {
+legend.generateLegendHTML = function(g, x, sel_points, oneEmWidth) {
// TODO(danvk): deprecate this option in place of {legend: 'never'}
if (g.getOption('showLabelsOnHighlight') !== true) return '';
height: 400px;
display: inline-block;
}
- /*
+ #controls {
+ position: absolute;
+ left: 10px;
+ top: 490px;
+ }
+
+ .hairline-info {
+ border: 1px solid black;
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+
+ display: table; /* shrink to fit */
+ min-width: 100px;
+
+ z-index: 10; /* should appear on top of the chart */
+ padding: 3px;
+ background: white;
+ font-size: 14px;
+ cursor: move;
+ }
+
.dygraph-hairline {
- border-right-style: dotted !important;
+ /* border-right-style: dotted !important; */
}
- */
</style>
</head>
<body>
<h2>Hairlines Demo</h2>
+ <!--
+ The "info box" for each hairline is based on this template.
+ Customize it as you wish. The .hairline-legend element will be populated
+ with data about the current points and the .hairline-kill-button element
+ will remove the hairline when clicked. Everything else will be untouched.
+ -->
+ <div id="hairline-template" class="hairline-info" style="display:none">
+ <button class='hairline-kill-button'>Kill</button>
+ <div class='hairline-legend'></div>
+ </div>
+
<div id="demodiv"></div>
<div id="status"></div>
+ <div id="controls">
+ <input type="checkbox" id="update" checked=true><label for="update"> Update</label>
+ </div>
+
<script type="text/javascript">
var last_t = 0;
var data = [];
labelsSeparateLines: true,
legend: 'always',
labels: [ 'Time', 'Value' ],
- title: 'Interesting Shapes',
+ title: 'Hairlines Demo',
+
+ axes: {
+ x: {
+ valueFormatter: function(val) {
+ return val.toFixed(2);
+ }
+ },
+ y: {
+ pixelsPerLabel: 50
+ }
+ },
// Set the plug-ins in the options.
plugins : [
}
);
+ var shouldUpdate = true;
var update = function() {
+ if (!shouldUpdate) return;
data.push([last_t, fn(last_t)]);
last_t++;
data.splice(0, 1);
g.updateOptions({file: data});
};
window.setInterval(update, 1000);
+
+ $('#update').on('change', function() {
+ shouldUpdate = $(this).is(':checked');
+ });
</script>
</body>
</html>