diff --git a/builder/index.html b/builder/index.html
index 189ddad..943245c 100644
--- a/builder/index.html
+++ b/builder/index.html
@@ -70,6 +70,8 @@
var js = "";
+ document.getElementById('css-code').style.display = chart.options.tooltip ? 'block' : 'none';
+
if (chart.options.timestampFormatter) {
chartOptions.timestampFormatter = "SmoothieChart.timeFormatter";
}
@@ -399,6 +401,7 @@
bindRange({target: canvas, name: 'Chart width', propertyName: 'width', min: 20, max: 1000});
bindRange({target: chart, name: 'Delay', propertyName: 'delay', min: 0, max: 2000});
bindCheckBox({target: chart.options, name: 'Scroll Backwards', propertyName: 'scrollBackwards'});
+ bindCheckBox({target: chart.options, name: 'Show Tooltip', propertyName: 'tooltip'});
// Series
startControlSection('Series');
@@ -548,6 +551,16 @@
border: 1px solid black;
box-sizing: border-box;
}
+
+ div.smoothie-chart-tooltip {
+ background: #444;
+ padding: 1em;
+ margin-top: 20px;
+ font-family: consolas;
+ color: white;
+ font-size: 10px;
+ pointer-events: none;
+ }
@@ -569,6 +582,19 @@ Code
+
+
+
+
diff --git a/smoothie.d.ts b/smoothie.d.ts
index e63c50d..4c7efba 100644
--- a/smoothie.d.ts
+++ b/smoothie.d.ts
@@ -129,6 +129,10 @@ export interface IChartOptions {
labels?: ILabelOptions;
+ tooltip?: boolean;
+ tooltipLine?: { lineWidth: number, strokeStyle: string };
+ tooltipFormatter?: (timestamp: number, data: {series: TimeSeries, index: number, value: number}[]) => string;
+
/** Allows the chart to stretch according to its containers and layout settings. Default is false
, for backwards compatibility. */
responsive?: boolean;
}
diff --git a/smoothie.js b/smoothie.js
index d7c628d..bf097b9 100644
--- a/smoothie.js
+++ b/smoothie.js
@@ -77,6 +77,7 @@
* v1.29: Support responsive sizing, by @drewnoakes
* v1.29.1: Include types in package, and make property optional, by @TrentHouliston
* v1.30: Fix inverted logic in devicePixelRatio support, by @scanlime
+ * v1.31: Support tooltips, by @Sly1024 and @drewnoakes
*/
;(function(exports) {
@@ -103,6 +104,18 @@
}
}
return arguments[0];
+ },
+ binarySearch: function(data, value) {
+ var low = 0,
+ high = data.length;
+ while (low < high) {
+ var mid = (low + high) >> 1;
+ if (value < data[mid][0])
+ high = mid;
+ else
+ low = mid + 1;
+ }
+ return low;
}
};
@@ -264,7 +277,13 @@
* fontSize: 15,
* fontFamily: 'sans-serif',
* precision: 2
- * }
+ * },
+ * tooltip: false // show tooltip when mouse is over the chart
+ * tooltipLine: { // properties for a vertical line at the cursor position
+ * lineWidth: 1,
+ * strokeStyle: '#BBBBBB'
+ * },
+ * tooltipFormatter: SmoothieChart.tooltipFormatter // formatter function for tooltip text
* }
*
*
@@ -276,8 +295,24 @@
this.currentValueRange = 1;
this.currentVisMinValue = 0;
this.lastRenderTimeMillis = 0;
+
+ this.mousemove = this.mousemove.bind(this);
+ this.mouseout = this.mouseout.bind(this);
}
+ /** Formats the HTML string content of the tooltip. */
+ SmoothieChart.tooltipFormatter = function (timestamp, data) {
+ var timestampFormatter = this.options.timestampFormatter || SmoothieChart.timeFormatter,
+ lines = [timestampFormatter(new Date(timestamp))];
+
+ for (var i = 0; i < data.length; ++i) {
+ lines.push('' +
+ this.options.yMaxFormatter(data[i].value, this.options.labels.precision) + '');
+ }
+
+ return lines.join('
');
+ };
+
SmoothieChart.defaultChartOptions = {
millisPerPixel: 20,
enableDpiScaling: true,
@@ -310,6 +345,12 @@
precision: 2
},
horizontalLines: [],
+ tooltip: false,
+ tooltipLine: {
+ lineWidth: 1,
+ strokeStyle: '#BBBBBB'
+ },
+ tooltipFormatter: SmoothieChart.tooltipFormatter,
responsive: false
};
@@ -437,6 +478,78 @@
this.start();
};
+ SmoothieChart.prototype.getTooltipEl = function () {
+ // Use a single tooltip element across all chart instances
+ var el = SmoothieChart.tooltipEl;
+ if (!el) {
+ el = SmoothieChart.tooltipEl = document.createElement('div');
+ el.className = 'smoothie-chart-tooltip';
+ el.style.position = 'absolute';
+ el.style.display = 'none';
+ document.body.appendChild(el);
+ }
+ return el;
+ };
+
+ SmoothieChart.prototype.updateTooltip = function () {
+ var el = this.getTooltipEl();
+
+ if (!this.mouseover || !this.options.tooltip) {
+ el.style.display = 'none';
+ return;
+ }
+
+ var time = this.lastRenderTimeMillis - (this.delay || 0);
+
+ // Round time down to pixel granularity, so motion appears smoother.
+ time -= time % this.options.millisPerPixel;
+
+ // x pixel to time
+ var t = this.options.scrollBackwards
+ ? time - this.mouseX * this.options.millisPerPixel
+ : time - (this.canvas.offsetWidth - this.mouseX) * this.options.millisPerPixel;
+
+ var data = [];
+
+ // For each data set...
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ var timeSeries = this.seriesSet[d].timeSeries,
+ // find datapoint closest to time 't'
+ closeIdx = Util.binarySearch(timeSeries.data, t);
+
+ if (closeIdx > 0 && closeIdx < timeSeries.data.length) {
+ data.push({ series: this.seriesSet[d], index: closeIdx, value: timeSeries.data[closeIdx][1] });
+ }
+ }
+
+ if (data.length) {
+ el.innerHTML = this.options.tooltipFormatter.call(this, t, data);
+ el.style.display = 'block';
+ } else {
+ el.style.display = 'none';
+ }
+ };
+
+ SmoothieChart.prototype.mousemove = function (evt) {
+ this.mouseover = true;
+ this.mouseX = evt.offsetX;
+ this.mouseY = evt.offsetY;
+ this.mousePageX = evt.pageX;
+ this.mousePageY = evt.pageY;
+
+ var el = this.getTooltipEl();
+ el.style.top = Math.round(this.mousePageY) + 'px';
+ el.style.left = Math.round(this.mousePageX) + 'px';
+ this.updateTooltip();
+ };
+
+ SmoothieChart.prototype.mouseout = function () {
+ this.mouseover = false;
+ this.mouseX = this.mouseY = -1;
+ if (SmoothieChart.tooltipEl)
+ SmoothieChart.tooltipEl.style.display = 'none';
+ };
+
/**
* Make sure the canvas has the optimal resolution for the device's pixel ratio.
*/
@@ -488,6 +601,9 @@
return;
}
+ this.canvas.addEventListener('mousemove', this.mousemove);
+ this.canvas.addEventListener('mouseout', this.mouseout);
+
// Renders a frame, and queues the next frame for later rendering
var animate = function() {
this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() {
@@ -506,6 +622,8 @@
if (this.frame) {
SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame);
delete this.frame;
+ this.canvas.removeEventListener('mousemove', this.mousemove);
+ this.canvas.removeEventListener('mouseout', this.mouseout);
}
};
@@ -577,6 +695,7 @@
}
this.resize();
+ this.updateTooltip();
this.lastRenderTimeMillis = nowMillis;
@@ -767,6 +886,18 @@
context.restore();
}
+ if (chartOptions.tooltip && this.mouseX >= 0) {
+ // Draw vertical bar to show tooltip position
+ context.lineWidth = chartOptions.tooltipLine.lineWidth;
+ context.strokeStyle = chartOptions.tooltipLine.strokeStyle;
+ context.beginPath();
+ context.moveTo(this.mouseX, 0);
+ context.lineTo(this.mouseX, dimensions.height);
+ context.closePath();
+ context.stroke();
+ this.updateTooltip();
+ }
+
// Draw the axis values on the chart.
if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) {
var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision),
@@ -822,4 +953,3 @@
exports.SmoothieChart = SmoothieChart;
})(typeof exports === 'undefined' ? this : exports);
-