diff --git a/README.md b/README.md index fa57cbe..4ad9669 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ chartjs-plugin-streaming can be used with ES6 modules, plain JavaScript and modu chartjs-plugin-streaming requires [Moment.js](https://momentjs.com/) and [Chart.js](https://www.chartjs.org). -Version 1.6 supports the [line](https://www.chartjs.org/docs/latest/charts/line.html) and [bar](https://www.chartjs.org/docs/latest/charts/bar.html) chart types with both [Number data](https://www.chartjs.org/docs/latest/charts/line.html#number) and [Point data](https://www.chartjs.org/docs/latest/charts/line.html#point) (each data point is specified an array of objects containing x and y properties) as well as the [bubble](https://www.chartjs.org/docs/latest/charts/bubble.html) and [scatter](https://www.chartjs.org/docs/latest/charts/scatter.html) chart types with Point data. In case of Point data, either x or y must be in any of the [date formats](https://momentjs.com/docs/#/parsing/) that Moment.js accepts, and the corresponding axis must have a 'realtime' scale that has the same options as [time](https://www.chartjs.org/docs/latest/axes/cartesian/time.html) scale. Once the realtime scale is specified, the chart will auto-scroll along with that axis. Old data will be automatically deleted after the time specified by the `ttl` option, or as it disappears off the chart. +Version 1.7 supports the [line](https://www.chartjs.org/docs/latest/charts/line.html) and [bar](https://www.chartjs.org/docs/latest/charts/bar.html) chart types with both [Number data](https://www.chartjs.org/docs/latest/charts/line.html#number) and [Point data](https://www.chartjs.org/docs/latest/charts/line.html#point) (each data point is specified an array of objects containing x and y properties) as well as the [bubble](https://www.chartjs.org/docs/latest/charts/bubble.html) and [scatter](https://www.chartjs.org/docs/latest/charts/scatter.html) chart types with Point data. In case of Point data, either x or y must be in any of the [date formats](https://momentjs.com/docs/#/parsing/) that Moment.js accepts, and the corresponding axis must have a 'realtime' scale that has the same options as [time](https://www.chartjs.org/docs/latest/axes/cartesian/time.html) scale. Once the realtime scale is specified, the chart will auto-scroll along with that axis. Old data will be automatically deleted after the time specified by the `ttl` option, or as it disappears off the chart. ## Tutorial and Samples @@ -36,20 +36,44 @@ You can find a tutorial and samples at [nagix.github.io/chartjs-plugin-streaming ## Configuration -To configure this plugin, you can simply add the following entries to your chart options. [This example](https://nagix.github.io/chartjs-plugin-streaming/samples/interactions.html) shows how each option affects the appearance of a chart. +The plugin options can be changed at 3 different levels and with the following priority: + +- per axis: `options.scales.xAxes[].realtime.*` or `options.scales.yAxes[].realtime.*` +- per chart: `options.plugins.streaming.*` +- globally: `Chart.defaults.global.plugins.streaming.*` + +All available options are listed below. [This example](https://nagix.github.io/chartjs-plugin-streaming/samples/interactions.html) shows how each option affects the appearance of a chart. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- -| `plugins.streaming` | `Object` or `Boolean` | `true` | The streaming options (see `plugins.streaming.*` options). Also accepts a boolean, in which case if `true`, the chart will auto-scroll using the **global options**, else if `false`, the chart will not auto-scroll. -| `plugins.streaming.duration` | `Number` | `10000` | Duration of the chart in milliseconds (how much time of data it will show). -| `plugins.streaming.refresh` | `Number` | `1000` | Refresh interval of data in milliseconds. `onRefresh` callback function will be called at this interval. -| `plugins.streaming.delay` | `Number` | `0` | Delay added to the chart in milliseconds so that upcoming values are known before lines are plotted. This makes the chart look like a continual stream rather than very jumpy on the right hand side. Specify the maximum expected delay. -| `plugins.streaming.frameRate` | `Number` | `30` | Frequency at which the chart is drawn on a display (frames per second). Decrease this value to save CPU power. [more...](#lowering-cpu-usage) -| `plugins.streaming.pause` | `Boolean` | `false` | If set to `true`, scrolling stops. Note that `onRefresh` callback is called even when this is set to `true`. -| `plugins.streaming.onRefresh` | `Function` | `null` | Callback function that will be called at a regular interval. The callback takes one argument, a reference to the chart object. You can update your datasets here. The chart will be automatically updated after returning. -| `plugins.streaming.ttl` | `Number` | | Duration of the data to be kept in milliseconds. If not set, old data will be automatically deleted as it disappears off the chart. +| `duration` | `Number` | `10000` | Duration of the chart in milliseconds (how much time of data it will show). +| `ttl` | `Number` | | Duration of the data to be kept in milliseconds. If not set, old data will be automatically deleted as it disappears off the chart. +| `delay` | `Number` | `0` | Delay added to the chart in milliseconds so that upcoming values are known before lines are plotted. This makes the chart look like a continual stream rather than very jumpy on the right hand side. Specify the maximum expected delay. +| `refresh` | `Number` | `1000` | Refresh interval of data in milliseconds. `onRefresh` callback function will be called at this interval. +| `onRefresh` | `Function` | `null` | Callback function that will be called at a regular interval. The callback takes one argument, a reference to the chart object. You can update your datasets here. The chart will be automatically updated after returning. +| `frameRate` | `Number` | `30` | Frequency at which the chart is drawn on a display (frames per second). This option can be set at chart level but not at axis level. Decrease this value to save CPU power. [more...](#lowering-cpu-usage) +| `pause` | `Boolean` | `false` | If set to `true`, scrolling stops. Note that `onRefresh` callback is called even when this is set to `true`. + +Due to historical reasons, a chart with the 'time' scale will also auto-scroll if this plugin is enabled. If you want to stop scrolling a particular chart, set `options.plugins.streaming` to `false`. + +Note that the following axis options are ignored for the 'realtime' scale. + +- `bounds` +- `distribution` (always `'linear'`) +- `offset` (always `false`) +- `ticks.major.enabled` (always `true`) +- `time.max` +- `time.min` + +## Data Feed Models + +This plugin supports both pull and push based data feed. -> **Global options** can be change through `Chart.defaults.global.plugins.streaming`, which by default enable auto-scroll of the charts that have a time scale. +### Pull Model (Polling Based) + +In the pull model, the user code needs to asks for new data and pull it from a data source. To enable this, the plugin provides two options: `onRefresh` which is the callback function that is called at a regular interval to check the data source and `refresh` which specifies the interval. In this callback function, you can add data into the existing data array as usual, but you don't need to call the `update` function as it is called internally. + +This model is suitable for data sources such as web servers, Kafka (REST Proxy), Kinesis (Data Streams API) and other time series databases with REST API support including Elasticsearch, OpenTSDB and Graphite. For example: @@ -64,39 +88,95 @@ For example: options: { scales: { xAxes: [{ - type: 'realtime' // x axis will auto-scroll from right to left + type: 'realtime', // x axis will auto-scroll from right to left + realtime: {. // per-axis options + duration: 20000, // data in the past 20000 ms will be displayed + refresh: 1000, // onRefresh callback will be called every 1000 ms + delay: 1000, // delay of 1000 ms, so upcoming values are known before plotting a line + pause: false, // chart is not paused + ttl: undefined, // data will be automatically deleted as it disappears off the chart + + // a callback to update datasets + onRefresh: function(chart) { + + // query your data source and get the array of {x: timestamp, y: value} objects + var data = getLatestData(); + + // append the new data array to the existing chart data + Array.prototype.push.apply(chart.data.datasets[0].data, data); + } + } }] }, plugins: { - streaming: { // enabled by default - duration: 20000, // data in the past 20000 ms will be displayed - refresh: 1000, // onRefresh callback will be called every 1000 ms - delay: 1000, // delay of 1000 ms, so upcoming values are known before plotting a line - frameRate: 30, // chart is drawn 30 times every second - pause: false, // chart is not paused - ttl: undefined, // data will be automatically deleted as it disappears off the chart - - // a callback to update datasets - onRefresh: function(chart) { - chart.data.datasets[0].data.push({ - x: Date.now(), - y: Math.random() * 100 - }); + streaming: { // per-chart option + frameRate: 30 // chart is drawn 30 times every second + } + } + } +} +``` + +### Push Model (Listening Based) + +In the push model, the user code registers a listener that waits for new data, and data can be picked up immediately after it arrives. Usually, data source connector libraries that supports the push model provide a listener callback function in which you can add data into the existing data array. The `update` function needs to be called after adding new data. + +A problem with calling the `update` function for stream data feeds is that it can disrupt smooth transition because an `update` call interrupts the current animation and initiates a new one. To avoid this, this plugin added the `preservation` config property for the `update` function. If it is set to `true`, the current animation won't be interrupted and new data can be added without initiating a new animation. + +This model is suitable for data sources such as WebSocket, MQTT, Kinesis (Client Library) and other realtime messaging services including Socket.IO, Pusher and Firebase. + +For example: + +```javascript +{ + type: 'line', // 'line', 'bar', 'bubble' and 'scatter' types are supported + data: { + datasets: [{ + data: [] // empty at the beginning + }] + }, + options: { + scales: { + xAxes: [{ + type: 'realtime', // x axis will auto-scroll from right to left + realtime: { // per-axis options + duration: 20000, // data in the past 20000 ms will be displayed + delay: 1000, // delay of 1000 ms, so upcoming values are known before plotting a line + pause: false, // chart is not paused + ttl: undefined // data will be automatically deleted as it disappears off the chart } + }] + }, + plugins: { + streaming: { // per-chart option + frameRate: 30 // chart is drawn 30 times every second } } } } ``` -Note that the following options are ignored for the 'realtime' scale. +Here is an example of a listener function: -- `bounds` -- `distribution` (always `'linear'`) -- `offset` (always `false`) -- `ticks.major.enabled` (always `true`) -- `time.max` -- `time.min` +```javascript +// save the chart instance to a variable +var myChart = new Chart(ctx, config); + +// your event listener code - assuming the event object has the timestamp and value properties +function onReceive(event) { + + // append the new data to the existing chart data + myChart.data.datasets[0].data.push({ + x: event.timestamp, + y: event.value + }); + + // update chart datasets keeping the current animation + myChart.update({ + preservation: true + }); +} +``` ## Support for Zooming and panning diff --git a/docs/index.html b/docs/index.html index 416e439..4f14169 100644 --- a/docs/index.html +++ b/docs/index.html @@ -15,7 +15,7 @@ - +
...
options: {
-...
-plugins: {
-streaming: {
-onRefresh: function(chart) {
-chart.data.datasets.forEach(function(dataset) {
-dataset.data.push({
-x: Date.now(),
-y: Math.random()
+scales: {
+xAxes: [{
+...
+realtime: {
+onRefresh: function(chart) {
+chart.data.datasets.forEach(function(dataset) {
+dataset.data.push({
+x: Date.now(),
+y: Math.random()
+});
});
-});
+}
}
-}
-}
-...
+...
@@ -137,13 +137,13 @@
...
options: {
-...
-plugins: {
-streaming: {
+scales: {
+xAxes: [{
...
-delay: 2000
-}
-...
+realtime: {
+...
+delay: 2000
+...
@@ -317,19 +317,21 @@
label: 'Dataset 2'
}];
options: any = {
-...
-plugins: {
-streaming: {
-onRefresh: function(chart: any) {
-chart.data.datasets.forEach(function(dataset: any) {
-dataset.data.push({
-x: Date.now(),
-y: Math.random()
+scales: {
+xAxes: [{
+...
+realtime: {
+onRefresh: function(chart: any) {
+chart.data.datasets.forEach(function(dataset: any) {
+dataset.data.push({
+x: Date.now(),
+y: Math.random()
+});
});
-});
-},
-delay: 2000
-}
+},
+delay: 2000
+}
+}]
}
};
}
@@ -437,19 +439,21 @@}]
}}
options={{
-...
-plugins: {
-streaming: {
-onRefresh: function(chart) {
-chart.data.datasets.forEach(function(dataset) {
-dataset.data.push({
-x: Date.now(),
-y: Math.random()
+scales: {
+xAxes: [{
+...
+realtime: {
+onRefresh: function(chart) {
+chart.data.datasets.forEach(function(dataset) {
+dataset.data.push({
+x: Date.now(),
+y: Math.random()
+});
});
-});
-},
-delay: 2000
-}
+},
+delay: 2000
+}
+}]
}
}}
/>
@@ -576,19 +580,21 @@backgroundColor: 'rgba(54, 162, 235, 0.5)'
}]
}, {
-...
-plugins: {
-streaming: {
-onRefresh: function(chart) {
-chart.data.datasets.forEach(function(dataset) {
-dataset.data.push({
-x: Date.now(),
-y: Math.random()
+scales: {
+xAxes: [{
+...
+realtime: {
+onRefresh: function(chart) {
+chart.data.datasets.forEach(function(dataset) {
+dataset.data.push({
+x: Date.now(),
+y: Math.random()
+});
});
-});
-},
-delay: 2000
-}
+},
+delay: 2000
+}
+}]
}
});
...
@@ -628,20 +634,20 @@...
options: {
-...
-plugins: {
-streaming: {
-onRefresh: function(chart) {
-chart.data.datasets.forEach(function(dataset) {
-dataset.data.push({
-x: Date.now(),
-y: Math.random()
+scales: {
+xAxes: [{
+...
+realtime: {
+onRefresh: function(chart) {
+chart.data.datasets.forEach(function(dataset) {
+dataset.data.push({
+x: Date.now(),
+y: Math.random()
+});
});
-});
+}
}
-}
-}
-...
+...
@@ -137,13 +137,13 @@
...
options: {
-...
-plugins: {
-streaming: {
+scales: {
+xAxes: [{
...
-delay: 2000
-}
-...
+realtime: {
+...
+delay: 2000
+...
@@ -317,19 +317,21 @@
label: 'Dataset 2'
}];
options: any = {
-...
-plugins: {
-streaming: {
-onRefresh: function(chart: any) {
-chart.data.datasets.forEach(function(dataset: any) {
-dataset.data.push({
-x: Date.now(),
-y: Math.random()
+scales: {
+xAxes: [{
+...
+realtime: {
+onRefresh: function(chart: any) {
+chart.data.datasets.forEach(function(dataset: any) {
+dataset.data.push({
+x: Date.now(),
+y: Math.random()
+});
});
-});
-},
-delay: 2000
-}
+},
+delay: 2000
+}
+}]
}
};
}
@@ -437,19 +439,21 @@}]
}}
options={{
-...
-plugins: {
-streaming: {
-onRefresh: function(chart) {
-chart.data.datasets.forEach(function(dataset) {
-dataset.data.push({
-x: Date.now(),
-y: Math.random()
+scales: {
+xAxes: [{
+...
+realtime: {
+onRefresh: function(chart) {
+chart.data.datasets.forEach(function(dataset) {
+dataset.data.push({
+x: Date.now(),
+y: Math.random()
+});
});
-});
-},
-delay: 2000
-}
+},
+delay: 2000
+}
+}]
}
}}
/>
@@ -576,19 +580,21 @@backgroundColor: 'rgba(54, 162, 235, 0.5)'
}]
}, {
-...
-plugins: {
-streaming: {
-onRefresh: function(chart) {
-chart.data.datasets.forEach(function(dataset) {
-dataset.data.push({
-x: Date.now(),
-y: Math.random()
+scales: {
+xAxes: [{
+...
+realtime: {
+onRefresh: function(chart) {
+chart.data.datasets.forEach(function(dataset) {
+dataset.data.push({
+x: Date.now(),
+y: Math.random()
+});
});
-});
-},
-delay: 2000
-}
+},
+delay: 2000
+}
+}]
}
});
...
@@ -628,20 +634,20 @@+ + + + +
+var chartColors = { + red: 'rgb(255, 99, 132)', + orange: 'rgb(255, 159, 64)', + yellow: 'rgb(255, 205, 86)', + green: 'rgb(75, 192, 192)', + blue: 'rgb(54, 162, 235)', + purple: 'rgb(153, 102, 255)', + grey: 'rgb(201, 203, 207)' +}; + +function randomScalingFactor() { + return (Math.random() > 0.5 ? 1.0 : -1.0) * Math.round(Math.random() * 100); +} + +function onReceive(event) { + window.myChart.config.data.datasets[event.index].data.push({ + x: event.timestamp, + y: event.value + }); + window.myChart.update({ + preservation: true + }); +} + +var timeoutIDs = []; + +function startFeed(index) { + var receive = function() { + onReceive({ + index: index, + timestamp: Date.now(), + value: randomScalingFactor() + }); + timeoutIDs[index] = setTimeout(receive, Math.random() * 1000 + 500); + } + timeoutIDs[index] = setTimeout(receive, Math.random() * 1000 + 500); +} + +function stopFeed(index) { + clearTimeout(timeoutIDs[index]); +} + +var color = Chart.helpers.color; +var config = { + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1 (linear interpolation)', + backgroundColor: color(chartColors.red).alpha(0.5).rgbString(), + borderColor: chartColors.red, + fill: false, + lineTension: 0, + borderDash: [8, 4], + data: [] + }, { + label: 'Dataset 2 (cubic interpolation)', + backgroundColor: color(chartColors.blue).alpha(0.5).rgbString(), + borderColor: chartColors.blue, + fill: false, + cubicInterpolationMode: 'monotone', + data: [] + }] + }, + options: { + title: { + display: true, + text: 'Push data feed sample' + }, + scales: { + xAxes: [{ + type: 'realtime', + realtime: { + duration: 20000, + delay: 2000, + } + }], + yAxes: [{ + scaleLabel: { + display: true, + labelString: 'value' + } + }] + }, + tooltips: { + mode: 'nearest', + intersect: false + }, + hover: { + mode: 'nearest', + intersect: false + } + } +}; + +window.onload = function() { + var ctx = document.getElementById('myChart').getContext('2d'); + window.myChart = new Chart(ctx, config); + startFeed(0); + startFeed(1); +}; + +document.getElementById('randomizeData').addEventListener('click', function() { + config.data.datasets.forEach(function(dataset) { + dataset.data.forEach(function(dataObj) { + dataObj.y = randomScalingFactor(); + }); + }); + window.myChart.update(); +}); + +var colorNames = Object.keys(chartColors); +document.getElementById('addDataset').addEventListener('click', function() { + var colorName = colorNames[config.data.datasets.length % colorNames.length]; + var newColor = chartColors[colorName]; + var newDataset = { + label: 'Dataset ' + (config.data.datasets.length + 1), + backgroundColor: color(newColor).alpha(0.5).rgbString(), + borderColor: newColor, + fill: false, + lineTension: 0, + data: [] + }; + + config.data.datasets.push(newDataset); + window.myChart.update(); + startFeed(config.data.datasets.length - 1); +}); + +document.getElementById('removeDataset').addEventListener('click', function() { + stopFeed(config.data.datasets.length - 1); + config.data.datasets.pop(); + window.myChart.update(); +}); + +document.getElementById('addData').addEventListener('click', function() { + config.data.datasets.forEach(function(dataset) { + dataset.data.push({ + x: Date.now(), + y: randomScalingFactor() + }); + }); + window.myChart.update(); +});+
<head> + <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js"></script> + <script src="https://github.com/nagix/chartjs-plugin-streaming/releases/download/v1.7.0/chartjs-plugin-streaming.min.js"></script> +</head> +<body> + <div> + <canvas id="myChart"></canvas> + </div> + <p> + <button id="randomizeData">Randomize Data</button> + <button id="addDataset">Add Dataset</button> + <button id="removeDataset">Remove Dataset</button> + <button id="addData">Add Data</button> + </p> +</body>+