diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..e975120 --- /dev/null +++ b/bower.json @@ -0,0 +1,15 @@ +{ + "name": "chartjs-plugin-streaming", + "description": "Chart.js plugin for live streaming data", + "homepage": "https://nagix.github.io/chartjs-plugin-streaming", + "license": "MIT", + "version": "2.0.0-beta.2", + "main": "dist/chartjs-plugin-streaming.js", + "ignore": [ + ".codeclimate.yml", + ".gitignore", + ".npmignore", + ".travis.yml", + "scripts" + ] +} diff --git a/dist/chartjs-plugin-streaming.esm.js b/dist/chartjs-plugin-streaming.esm.js new file mode 100644 index 0000000..a1a250a --- /dev/null +++ b/dist/chartjs-plugin-streaming.esm.js @@ -0,0 +1,713 @@ +/*! + * chartjs-plugin-streaming v2.0.0-beta.2 + * https://nagix.github.io/chartjs-plugin-streaming + * (c) 2017-2021 Akihiko Kusanagi + * Released under the MIT license + */ +import { TimeScale, defaults, registry, Chart } from 'chart.js'; +import { valueOrDefault, requestAnimFrame, noop, each, clipArea, unclipArea, isNumber, _lookup, callback, isArray, getRelativePosition } from 'chart.js/helpers'; + +function clamp(value, lower, upper) { + return Math.min(Math.max(value, lower), upper); +} +function resolveOption(scale, key) { + const realtimeOpts = scale.options.realtime; + const streamingOpts = scale.chart.options.plugins.streaming; + return valueOrDefault(realtimeOpts[key], streamingOpts[key]); +} +const cancelAnimFrame = (function() { + if (typeof window === 'undefined') { + return noop; + } + return window.cancelAnimationFrame; +}()); +function startFrameRefreshTimer(context, func) { + if (!context.frameRequestID) { + const frameRefresh = function() { + func(); + context.frameRequestID = requestAnimFrame.call(window, frameRefresh); + }; + context.frameRequestID = requestAnimFrame.call(window, frameRefresh); + } +} +function stopFrameRefreshTimer(context) { + const frameRequestID = context.frameRequestID; + if (frameRequestID) { + cancelAnimFrame.call(window, frameRequestID); + delete context.frameRequestID; + } +} + +const chartStates = new WeakMap(); +function getState(chart) { + let state = chartStates.get(chart); + if (!state) { + state = {originalScaleLimits: {}}; + chartStates.set(chart, state); + } + return state; +} +function storeOriginalScaleLimits(chart) { + const {originalScaleLimits} = getState(chart); + const scales = chart.scales; + each(scales, scale => { + const id = scale.id; + if (!originalScaleLimits[id]) { + originalScaleLimits[id] = { + duration: resolveOption(scale, 'duration'), + delay: resolveOption(scale, 'delay') + }; + } + }); + each(originalScaleLimits, (opt, key) => { + if (!scales[key]) { + delete originalScaleLimits[key]; + } + }); + return originalScaleLimits; +} +function zoomRealTimeScale(scale, zoom, center, limits) { + const {chart, axis} = scale; + const {minDuration = 0, maxDuration = Infinity, minDelay = -Infinity, maxDelay = Infinity} = limits && limits[axis] || {}; + const realtimeOpts = scale.options.realtime; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const newDuration = clamp(duration * (2 - zoom), minDuration, maxDuration); + let maxPercent, newDelay; + storeOriginalScaleLimits(chart); + if (scale.isHorizontal()) { + maxPercent = (scale.right - center.x) / (scale.right - scale.left); + } else { + maxPercent = (scale.bottom - center.y) / (scale.bottom - scale.top); + } + newDelay = delay + maxPercent * (duration - newDuration); + realtimeOpts.duration = newDuration; + realtimeOpts.delay = clamp(newDelay, minDelay, maxDelay); + return newDuration !== scale.max - scale.min; +} +function panRealTimeScale(scale, delta, limits) { + const {chart, axis} = scale; + const {minDelay = -Infinity, maxDelay = Infinity} = limits && limits[axis] || {}; + const delay = resolveOption(scale, 'delay'); + const newDelay = delay + (scale.getValueForPixel(delta) - scale.getValueForPixel(0)); + storeOriginalScaleLimits(chart); + scale.options.realtime.delay = clamp(newDelay, minDelay, maxDelay); + return true; +} +function resetRealTimeOptions(chart) { + const originalScaleLimits = storeOriginalScaleLimits(chart); + each(chart.scales, scale => { + const realtimeOptions = scale.options.realtime; + if (realtimeOptions) { + const original = originalScaleLimits[scale.id]; + if (original) { + realtimeOptions.duration = original.duration; + realtimeOptions.delay = original.delay; + } else { + delete realtimeOptions.duration; + delete realtimeOptions.delay; + } + } + }); +} + +const INTERVALS = { + millisecond: { + common: true, + size: 1, + steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] + }, + second: { + common: true, + size: 1000, + steps: [1, 2, 5, 10, 15, 30] + }, + minute: { + common: true, + size: 60000, + steps: [1, 2, 5, 10, 15, 30] + }, + hour: { + common: true, + size: 3600000, + steps: [1, 2, 3, 6, 12] + }, + day: { + common: true, + size: 86400000, + steps: [1, 2, 5] + }, + week: { + common: false, + size: 604800000, + steps: [1, 2, 3, 4] + }, + month: { + common: true, + size: 2.628e9, + steps: [1, 2, 3] + }, + quarter: { + common: false, + size: 7.884e9, + steps: [1, 2, 3, 4] + }, + year: { + common: true, + size: 3.154e10 + } +}; +const UNITS = Object.keys(INTERVALS); +function determineStepSize(min, max, unit, capacity) { + const range = max - min; + const {size: milliseconds, steps} = INTERVALS[unit]; + let factor; + if (!steps) { + return Math.ceil(range / (capacity * milliseconds)); + } + for (let i = 0, ilen = steps.length; i < ilen; ++i) { + factor = steps[i]; + if (Math.ceil(range / (milliseconds * factor)) <= capacity) { + break; + } + } + return factor; +} +function determineUnitForAutoTicks(minUnit, min, max, capacity) { + const range = max - min; + const ilen = UNITS.length; + for (let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) { + const {common, size, steps} = INTERVALS[UNITS[i]]; + const factor = steps ? steps[steps.length - 1] : Number.MAX_SAFE_INTEGER; + if (common && Math.ceil(range / (factor * size)) <= capacity) { + return UNITS[i]; + } + } + return UNITS[ilen - 1]; +} +function determineMajorUnit(unit) { + for (let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { + if (INTERVALS[UNITS[i]].common) { + return UNITS[i]; + } + } +} +function addTick(ticks, time, timestamps) { + if (!timestamps) { + ticks[time] = true; + } else if (timestamps.length) { + const {lo, hi} = _lookup(timestamps, time); + const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi]; + ticks[timestamp] = true; + } +} +function getAxisKey(meta, id) { + return id === meta.xAxisID && 'x' || id === meta.yAxisID && 'y'; +} +const datasetPropertyKeys = [ + 'pointBackgroundColor', + 'pointBorderColor', + 'pointBorderWidth', + 'pointRadius', + 'pointRotation', + 'pointStyle', + 'pointHitRadius', + 'pointHoverBackgroundColor', + 'pointHoverBorderColor', + 'pointHoverBorderWidth', + 'pointHoverRadius', + 'backgroundColor', + 'borderColor', + 'borderSkipped', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'radius', + 'rotation' +]; +function refreshData(scale) { + const {chart, id, max} = scale; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const ttl = resolveOption(scale, 'ttl'); + const pause = resolveOption(scale, 'pause'); + const onRefresh = resolveOption(scale, 'onRefresh'); + const min = Date.now() - (isNaN(ttl) ? duration + delay : ttl); + let i, start, count, removalRange; + callback(onRefresh, [chart]); + chart.data.datasets.forEach((dataset, datasetIndex) => { + const meta = chart.getDatasetMeta(datasetIndex); + const axis = getAxisKey(meta, id); + if (axis) { + const controller = meta.controller; + const data = dataset.data; + const length = data.length; + if (pause) { + for (i = 0; i < length; ++i) { + if (!(controller.getParsed(i)[axis] < max)) { + break; + } + } + start = i + 2; + } else { + start = 0; + } + for (i = start; i < length; ++i) { + if (!(controller.getParsed(i)[axis] <= min)) { + break; + } + } + count = i - start; + if (isNaN(ttl)) { + count = Math.max(count - 2, 0); + } + data.splice(start, count); + datasetPropertyKeys.forEach(key => { + if (isArray(dataset[key])) { + dataset[key].splice(start, count); + } + }); + each(dataset.datalabels, value => { + if (isArray(value)) { + value.splice(start, count); + } + }); + if (typeof data[0] !== 'object') { + removalRange = { + start: start, + count: count + }; + } + } + }); + if (removalRange) { + chart.data.labels.splice(removalRange.start, removalRange.count); + } + chart.update('quiet'); +} +function stopDataRefreshTimer(scale) { + const realtime = scale.realtime; + const refreshTimerID = realtime.refreshTimerID; + if (refreshTimerID) { + clearInterval(refreshTimerID); + delete realtime.refreshTimerID; + delete realtime.refreshInterval; + } +} +function startDataRefreshTimer(scale) { + const realtime = scale.realtime; + const interval = resolveOption(scale, 'refresh'); + if (realtime.refreshTimerID) { + return; + } + realtime.refreshTimerID = setInterval(() => { + const newInterval = resolveOption(scale, 'refresh'); + refreshData(scale); + if (realtime.refreshInterval !== newInterval && !isNaN(newInterval)) { + stopDataRefreshTimer(scale); + startDataRefreshTimer(scale); + } + }, interval); + realtime.refreshInterval = interval; +} +const transitionKeys = { + x: { + data: ['x', 'controlPointPreviousX', 'controlPointNextX'], + dataset: ['x'], + tooltip: ['x', 'caretX'] + }, + y: { + data: ['y', 'controlPointPreviousY', 'controlPointNextY'], + dataset: ['y'], + tooltip: ['y', 'caretY'] + } +}; +function transition(element, keys, translate) { + const animations = element.$animations; + for (let i = 0, ilen = keys.length; i < ilen; ++i) { + const key = keys[i]; + if (!isNaN(element[key])) { + element[key] -= translate; + } + } + if (animations) { + for (let i = 0, ilen = keys.length; i < ilen; ++i) { + const value = animations[keys[i]]; + if (value) { + value._from -= translate; + value._to -= translate; + } + } + } +} +function scroll(scale) { + const {chart, id, realtime} = scale; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const isHorizontal = scale.isHorizontal(); + const tooltip = chart.tooltip; + const activeTooltip = tooltip._active; + const now = Date.now(); + let length, keys, offset; + if (isHorizontal) { + length = scale.width; + keys = transitionKeys.x; + } else { + length = scale.height; + keys = transitionKeys.y; + } + offset = length * (now - realtime.head) / duration; + if (!!isHorizontal === !!scale.options.reverse) { + offset = -offset; + } + each(chart.data.datasets, (dataset, datasetIndex) => { + const meta = chart.getDatasetMeta(datasetIndex); + if (getAxisKey(meta, id)) { + const {data, dataset: element} = meta; + const elements = data || []; + for (let i = 0, ilen = elements.length; i < ilen; ++i) { + transition(elements[i], keys.data, offset); + } + if (element) { + transition(element, keys.dataset, offset); + } + } + }); + if (activeTooltip && activeTooltip[0]) { + const meta = chart.getDatasetMeta(activeTooltip[0].datasetIndex); + if (getAxisKey(meta, id)) { + transition(tooltip, keys.tooltip, offset); + } + } + scale.max = now - delay; + scale.min = scale.max - duration; + realtime.head = now; +} +class RealTimeScale extends TimeScale { + constructor(props) { + super(props); + this.realtime = this.realtime || {}; + } + init(scaleOpts, opts) { + super.init(scaleOpts, opts); + startDataRefreshTimer(this); + } + update(maxWidth, maxHeight, margins) { + const me = this; + const {realtime, options} = me; + const {bounds, offset, ticks: ticksOpts} = options; + const {autoSkip, source, major: majorTicksOpts} = ticksOpts; + const majorEnabled = majorTicksOpts.enabled; + if (resolveOption(me, 'pause')) { + stopFrameRefreshTimer(realtime); + } else { + startFrameRefreshTimer(realtime, () => { + scroll(me); + }); + realtime.head = Date.now(); + } + options.bounds = undefined; + options.offset = false; + ticksOpts.autoSkip = false; + ticksOpts.source = source === 'auto' ? '' : source; + majorTicksOpts.enabled = true; + super.update(maxWidth, maxHeight, margins); + options.bounds = bounds; + options.offset = offset; + ticksOpts.autoSkip = autoSkip; + ticksOpts.source = source; + majorTicksOpts.enabled = majorEnabled; + } + buildTicks() { + const me = this; + const duration = resolveOption(me, 'duration'); + const delay = resolveOption(me, 'delay'); + const max = me.realtime.head - delay; + const min = max - duration; + const maxArray = [1e15, max]; + const minArray = [-1e15, min]; + Object.defineProperty(me, 'min', { + get: () => minArray.shift(), + set: noop + }); + Object.defineProperty(me, 'max', { + get: () => maxArray.shift(), + set: noop + }); + const ticks = super.buildTicks(); + delete me.min; + delete me.max; + me.min = min; + me.max = max; + return ticks; + } + calculateLabelRotation() { + const ticksOpts = this.options.ticks; + const maxRotation = ticksOpts.maxRotation; + ticksOpts.maxRotation = ticksOpts.minRotation || 0; + super.calculateLabelRotation(); + ticksOpts.maxRotation = maxRotation; + } + fit() { + const me = this; + const options = me.options; + super.fit(); + if (options.ticks.display && options.display && me.isHorizontal()) { + me.paddingLeft = 3; + me.paddingRight = 3; + me._handleMargins(); + } + } + draw(chartArea) { + const me = this; + const {chart, ctx} = me; + const area = me.isHorizontal() ? + { + left: chartArea.left, + top: 0, + right: chartArea.right, + bottom: chart.height + } : { + left: 0, + top: chartArea.top, + right: chart.width, + bottom: chartArea.bottom + }; + me._gridLineItems = null; + me._labelItems = null; + clipArea(ctx, area); + super.draw(chartArea); + unclipArea(ctx); + } + destroy() { + const me = this; + stopFrameRefreshTimer(me.realtime); + stopDataRefreshTimer(me); + } + _generate() { + const me = this; + const adapter = me._adapter; + const duration = resolveOption(me, 'duration'); + const delay = resolveOption(me, 'delay'); + const refresh = resolveOption(me, 'refresh'); + const max = me.realtime.head - delay; + const min = max - duration; + const capacity = me._getLabelCapacity(min); + const {time: timeOpts, ticks: ticksOpts} = me.options; + const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity); + const major = determineMajorUnit(minor); + const stepSize = timeOpts.stepSize || determineStepSize(min, max, minor, capacity); + const weekday = minor === 'week' ? timeOpts.isoWeekday : false; + const majorTicksEnabled = ticksOpts.major.enabled; + const hasWeekday = isNumber(weekday) || weekday === true; + const interval = INTERVALS[minor]; + const ticks = {}; + let first = min; + let time, count; + if (hasWeekday) { + first = +adapter.startOf(first, 'isoWeek', weekday); + } + first = +adapter.startOf(first, hasWeekday ? 'day' : minor); + if (adapter.diff(max, min, minor) > 100000 * stepSize) { + throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor); + } + time = first; + if (majorTicksEnabled && major && !hasWeekday && !timeOpts.round) { + time = +adapter.startOf(time, major); + time = +adapter.add(time, ~~((first - time) / (interval.size * stepSize)) * stepSize, minor); + } + const timestamps = ticksOpts.source === 'data' && me.getDataTimestamps(); + for (count = 0; time < max + refresh; time = +adapter.add(time, stepSize, minor), count++) { + addTick(ticks, time, timestamps); + } + if (time === max + refresh || count === 1) { + addTick(ticks, time, timestamps); + } + return Object.keys(ticks).sort((a, b) => a - b).map(x => +x); + } +} +RealTimeScale.id = 'realtime'; +RealTimeScale.defaults = { + bounds: 'data', + adapters: {}, + time: { + parser: false, + unit: false, + round: false, + isoWeekday: false, + minUnit: 'millisecond', + displayFormats: {} + }, + realtime: {}, + ticks: { + autoSkip: false, + source: 'auto', + major: { + enabled: true + } + } +}; +defaults.describe('scale.realtime', { + _scriptable: name => name !== 'onRefresh' +}); + +var version = "2.0.0-beta.2"; + +function drawChart(chart) { + const streaming = chart.streaming; + const frameRate = chart.options.plugins.streaming.frameRate; + const frameDuration = 1000 / (Math.max(frameRate, 0) || 30); + const next = streaming.lastDrawn + frameDuration || 0; + const now = Date.now(); + if (next <= now) { + chart.render(); + if (streaming.lastMouseEvent) { + setTimeout(() => { + const lastMouseEvent = streaming.lastMouseEvent; + if (lastMouseEvent) { + chart._eventHandler(lastMouseEvent); + } + }, 0); + } + streaming.lastDrawn = (next + frameDuration > now) ? next : now; + } +} +var StreamingPlugin = { + id: 'streaming', + version, + beforeInit(chart) { + const streaming = chart.streaming = chart.streaming || {}; + const canvas = streaming.canvas = chart.canvas; + const mouseEventListener = streaming.mouseEventListener = event => { + const pos = getRelativePosition(event, chart); + streaming.lastMouseEvent = { + type: 'mousemove', + chart: chart, + native: event, + x: pos.x, + y: pos.y + }; + }; + canvas.addEventListener('mousedown', mouseEventListener); + canvas.addEventListener('mouseup', mouseEventListener); + chart.options.transitions.quiet = { + animation: { + duration: 0 + } + }; + }, + afterInit(chart) { + const {update, render, resetZoom} = chart; + chart.update = mode => { + if (mode === 'quiet') { + chart.render = noop; + update.call(chart, mode); + chart.render = render; + } else { + update.call(chart, mode); + } + }; + if (resetZoom) { + const zoomPlugin = registry.getPlugin('zoom'); + zoomPlugin.zoomFunctions.realtime = zoomRealTimeScale; + zoomPlugin.panFunctions.realtime = panRealTimeScale; + chart.resetZoom = transition => { + resetRealTimeOptions(chart); + resetZoom(transition); + }; + } + }, + beforeUpdate(chart) { + const chartOpts = chart.options; + const scalesOpts = chartOpts.scales; + if (scalesOpts) { + Object.keys(scalesOpts).forEach(id => { + const scaleOpts = scalesOpts[id]; + if (scaleOpts.type === 'realtime') { + chartOpts.elements.line.capBezierPoints = false; + } + }); + } + return true; + }, + afterUpdate(chart) { + const {scales, streaming} = chart; + let pause = true; + each(scales, scale => { + if (scale instanceof RealTimeScale) { + pause &= resolveOption(scale, 'pause'); + } + }); + if (pause) { + stopFrameRefreshTimer(streaming); + } else { + startFrameRefreshTimer(streaming, () => { + drawChart(chart); + }); + } + }, + beforeDatasetDraw(chart, args) { + const {ctx, chartArea, width, height} = chart; + const {xAxisID, yAxisID, controller} = args.meta; + const area = { + left: 0, + top: 0, + right: width, + bottom: height + }; + if (xAxisID && controller.getScaleForId(xAxisID) instanceof RealTimeScale) { + area.left = chartArea.left; + area.right = chartArea.right; + } + if (yAxisID && controller.getScaleForId(yAxisID) instanceof RealTimeScale) { + area.top = chartArea.top; + area.bottom = chartArea.bottom; + } + clipArea(ctx, area); + return true; + }, + afterDatasetDraw(chart) { + unclipArea(chart.ctx); + }, + beforeEvent(chart, args) { + const streaming = chart.streaming; + const event = args.event; + if (event.type === 'mousemove') { + streaming.lastMouseEvent = event; + } else if (event.type === 'mouseout') { + delete streaming.lastMouseEvent; + } + return true; + }, + destroy(chart) { + const {scales, streaming} = chart; + const {canvas, mouseEventListener} = streaming; + stopFrameRefreshTimer(streaming); + canvas.removeEventListener('mousedown', mouseEventListener); + canvas.removeEventListener('mouseup', mouseEventListener); + each(scales, scale => { + if (scale instanceof RealTimeScale) { + scale.destroy(); + } + }); + }, + defaults: { + duration: 10000, + delay: 0, + frameRate: 30, + refresh: 1000, + onRefresh: null, + pause: false, + ttl: undefined + }, + descriptors: { + _scriptable: name => name !== 'onRefresh' + } +}; + +Chart.register(StreamingPlugin, RealTimeScale); + +export default StreamingPlugin; diff --git a/dist/chartjs-plugin-streaming.js b/dist/chartjs-plugin-streaming.js new file mode 100644 index 0000000..88c6438 --- /dev/null +++ b/dist/chartjs-plugin-streaming.js @@ -0,0 +1,718 @@ +/*! + * chartjs-plugin-streaming v2.0.0-beta.2 + * https://nagix.github.io/chartjs-plugin-streaming + * (c) 2017-2021 Akihiko Kusanagi + * Released under the MIT license + */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('chart.js'), require('chart.js/helpers')) : +typeof define === 'function' && define.amd ? define(['chart.js', 'chart.js/helpers'], factory) : +(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ChartStreaming = factory(global.Chart, global.Chart.helpers)); +}(this, (function (chart_js, helpers) { 'use strict'; + +function clamp(value, lower, upper) { + return Math.min(Math.max(value, lower), upper); +} +function resolveOption(scale, key) { + const realtimeOpts = scale.options.realtime; + const streamingOpts = scale.chart.options.plugins.streaming; + return helpers.valueOrDefault(realtimeOpts[key], streamingOpts[key]); +} +const cancelAnimFrame = (function() { + if (typeof window === 'undefined') { + return helpers.noop; + } + return window.cancelAnimationFrame; +}()); +function startFrameRefreshTimer(context, func) { + if (!context.frameRequestID) { + const frameRefresh = function() { + func(); + context.frameRequestID = helpers.requestAnimFrame.call(window, frameRefresh); + }; + context.frameRequestID = helpers.requestAnimFrame.call(window, frameRefresh); + } +} +function stopFrameRefreshTimer(context) { + const frameRequestID = context.frameRequestID; + if (frameRequestID) { + cancelAnimFrame.call(window, frameRequestID); + delete context.frameRequestID; + } +} + +const chartStates = new WeakMap(); +function getState(chart) { + let state = chartStates.get(chart); + if (!state) { + state = {originalScaleLimits: {}}; + chartStates.set(chart, state); + } + return state; +} +function storeOriginalScaleLimits(chart) { + const {originalScaleLimits} = getState(chart); + const scales = chart.scales; + helpers.each(scales, scale => { + const id = scale.id; + if (!originalScaleLimits[id]) { + originalScaleLimits[id] = { + duration: resolveOption(scale, 'duration'), + delay: resolveOption(scale, 'delay') + }; + } + }); + helpers.each(originalScaleLimits, (opt, key) => { + if (!scales[key]) { + delete originalScaleLimits[key]; + } + }); + return originalScaleLimits; +} +function zoomRealTimeScale(scale, zoom, center, limits) { + const {chart, axis} = scale; + const {minDuration = 0, maxDuration = Infinity, minDelay = -Infinity, maxDelay = Infinity} = limits && limits[axis] || {}; + const realtimeOpts = scale.options.realtime; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const newDuration = clamp(duration * (2 - zoom), minDuration, maxDuration); + let maxPercent, newDelay; + storeOriginalScaleLimits(chart); + if (scale.isHorizontal()) { + maxPercent = (scale.right - center.x) / (scale.right - scale.left); + } else { + maxPercent = (scale.bottom - center.y) / (scale.bottom - scale.top); + } + newDelay = delay + maxPercent * (duration - newDuration); + realtimeOpts.duration = newDuration; + realtimeOpts.delay = clamp(newDelay, minDelay, maxDelay); + return newDuration !== scale.max - scale.min; +} +function panRealTimeScale(scale, delta, limits) { + const {chart, axis} = scale; + const {minDelay = -Infinity, maxDelay = Infinity} = limits && limits[axis] || {}; + const delay = resolveOption(scale, 'delay'); + const newDelay = delay + (scale.getValueForPixel(delta) - scale.getValueForPixel(0)); + storeOriginalScaleLimits(chart); + scale.options.realtime.delay = clamp(newDelay, minDelay, maxDelay); + return true; +} +function resetRealTimeOptions(chart) { + const originalScaleLimits = storeOriginalScaleLimits(chart); + helpers.each(chart.scales, scale => { + const realtimeOptions = scale.options.realtime; + if (realtimeOptions) { + const original = originalScaleLimits[scale.id]; + if (original) { + realtimeOptions.duration = original.duration; + realtimeOptions.delay = original.delay; + } else { + delete realtimeOptions.duration; + delete realtimeOptions.delay; + } + } + }); +} + +const INTERVALS = { + millisecond: { + common: true, + size: 1, + steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] + }, + second: { + common: true, + size: 1000, + steps: [1, 2, 5, 10, 15, 30] + }, + minute: { + common: true, + size: 60000, + steps: [1, 2, 5, 10, 15, 30] + }, + hour: { + common: true, + size: 3600000, + steps: [1, 2, 3, 6, 12] + }, + day: { + common: true, + size: 86400000, + steps: [1, 2, 5] + }, + week: { + common: false, + size: 604800000, + steps: [1, 2, 3, 4] + }, + month: { + common: true, + size: 2.628e9, + steps: [1, 2, 3] + }, + quarter: { + common: false, + size: 7.884e9, + steps: [1, 2, 3, 4] + }, + year: { + common: true, + size: 3.154e10 + } +}; +const UNITS = Object.keys(INTERVALS); +function determineStepSize(min, max, unit, capacity) { + const range = max - min; + const {size: milliseconds, steps} = INTERVALS[unit]; + let factor; + if (!steps) { + return Math.ceil(range / (capacity * milliseconds)); + } + for (let i = 0, ilen = steps.length; i < ilen; ++i) { + factor = steps[i]; + if (Math.ceil(range / (milliseconds * factor)) <= capacity) { + break; + } + } + return factor; +} +function determineUnitForAutoTicks(minUnit, min, max, capacity) { + const range = max - min; + const ilen = UNITS.length; + for (let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) { + const {common, size, steps} = INTERVALS[UNITS[i]]; + const factor = steps ? steps[steps.length - 1] : Number.MAX_SAFE_INTEGER; + if (common && Math.ceil(range / (factor * size)) <= capacity) { + return UNITS[i]; + } + } + return UNITS[ilen - 1]; +} +function determineMajorUnit(unit) { + for (let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { + if (INTERVALS[UNITS[i]].common) { + return UNITS[i]; + } + } +} +function addTick(ticks, time, timestamps) { + if (!timestamps) { + ticks[time] = true; + } else if (timestamps.length) { + const {lo, hi} = helpers._lookup(timestamps, time); + const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi]; + ticks[timestamp] = true; + } +} +function getAxisKey(meta, id) { + return id === meta.xAxisID && 'x' || id === meta.yAxisID && 'y'; +} +const datasetPropertyKeys = [ + 'pointBackgroundColor', + 'pointBorderColor', + 'pointBorderWidth', + 'pointRadius', + 'pointRotation', + 'pointStyle', + 'pointHitRadius', + 'pointHoverBackgroundColor', + 'pointHoverBorderColor', + 'pointHoverBorderWidth', + 'pointHoverRadius', + 'backgroundColor', + 'borderColor', + 'borderSkipped', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'radius', + 'rotation' +]; +function refreshData(scale) { + const {chart, id, max} = scale; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const ttl = resolveOption(scale, 'ttl'); + const pause = resolveOption(scale, 'pause'); + const onRefresh = resolveOption(scale, 'onRefresh'); + const min = Date.now() - (isNaN(ttl) ? duration + delay : ttl); + let i, start, count, removalRange; + helpers.callback(onRefresh, [chart]); + chart.data.datasets.forEach((dataset, datasetIndex) => { + const meta = chart.getDatasetMeta(datasetIndex); + const axis = getAxisKey(meta, id); + if (axis) { + const controller = meta.controller; + const data = dataset.data; + const length = data.length; + if (pause) { + for (i = 0; i < length; ++i) { + if (!(controller.getParsed(i)[axis] < max)) { + break; + } + } + start = i + 2; + } else { + start = 0; + } + for (i = start; i < length; ++i) { + if (!(controller.getParsed(i)[axis] <= min)) { + break; + } + } + count = i - start; + if (isNaN(ttl)) { + count = Math.max(count - 2, 0); + } + data.splice(start, count); + datasetPropertyKeys.forEach(key => { + if (helpers.isArray(dataset[key])) { + dataset[key].splice(start, count); + } + }); + helpers.each(dataset.datalabels, value => { + if (helpers.isArray(value)) { + value.splice(start, count); + } + }); + if (typeof data[0] !== 'object') { + removalRange = { + start: start, + count: count + }; + } + } + }); + if (removalRange) { + chart.data.labels.splice(removalRange.start, removalRange.count); + } + chart.update('quiet'); +} +function stopDataRefreshTimer(scale) { + const realtime = scale.realtime; + const refreshTimerID = realtime.refreshTimerID; + if (refreshTimerID) { + clearInterval(refreshTimerID); + delete realtime.refreshTimerID; + delete realtime.refreshInterval; + } +} +function startDataRefreshTimer(scale) { + const realtime = scale.realtime; + const interval = resolveOption(scale, 'refresh'); + if (realtime.refreshTimerID) { + return; + } + realtime.refreshTimerID = setInterval(() => { + const newInterval = resolveOption(scale, 'refresh'); + refreshData(scale); + if (realtime.refreshInterval !== newInterval && !isNaN(newInterval)) { + stopDataRefreshTimer(scale); + startDataRefreshTimer(scale); + } + }, interval); + realtime.refreshInterval = interval; +} +const transitionKeys = { + x: { + data: ['x', 'controlPointPreviousX', 'controlPointNextX'], + dataset: ['x'], + tooltip: ['x', 'caretX'] + }, + y: { + data: ['y', 'controlPointPreviousY', 'controlPointNextY'], + dataset: ['y'], + tooltip: ['y', 'caretY'] + } +}; +function transition(element, keys, translate) { + const animations = element.$animations; + for (let i = 0, ilen = keys.length; i < ilen; ++i) { + const key = keys[i]; + if (!isNaN(element[key])) { + element[key] -= translate; + } + } + if (animations) { + for (let i = 0, ilen = keys.length; i < ilen; ++i) { + const value = animations[keys[i]]; + if (value) { + value._from -= translate; + value._to -= translate; + } + } + } +} +function scroll(scale) { + const {chart, id, realtime} = scale; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const isHorizontal = scale.isHorizontal(); + const tooltip = chart.tooltip; + const activeTooltip = tooltip._active; + const now = Date.now(); + let length, keys, offset; + if (isHorizontal) { + length = scale.width; + keys = transitionKeys.x; + } else { + length = scale.height; + keys = transitionKeys.y; + } + offset = length * (now - realtime.head) / duration; + if (!!isHorizontal === !!scale.options.reverse) { + offset = -offset; + } + helpers.each(chart.data.datasets, (dataset, datasetIndex) => { + const meta = chart.getDatasetMeta(datasetIndex); + if (getAxisKey(meta, id)) { + const {data, dataset: element} = meta; + const elements = data || []; + for (let i = 0, ilen = elements.length; i < ilen; ++i) { + transition(elements[i], keys.data, offset); + } + if (element) { + transition(element, keys.dataset, offset); + } + } + }); + if (activeTooltip && activeTooltip[0]) { + const meta = chart.getDatasetMeta(activeTooltip[0].datasetIndex); + if (getAxisKey(meta, id)) { + transition(tooltip, keys.tooltip, offset); + } + } + scale.max = now - delay; + scale.min = scale.max - duration; + realtime.head = now; +} +class RealTimeScale extends chart_js.TimeScale { + constructor(props) { + super(props); + this.realtime = this.realtime || {}; + } + init(scaleOpts, opts) { + super.init(scaleOpts, opts); + startDataRefreshTimer(this); + } + update(maxWidth, maxHeight, margins) { + const me = this; + const {realtime, options} = me; + const {bounds, offset, ticks: ticksOpts} = options; + const {autoSkip, source, major: majorTicksOpts} = ticksOpts; + const majorEnabled = majorTicksOpts.enabled; + if (resolveOption(me, 'pause')) { + stopFrameRefreshTimer(realtime); + } else { + startFrameRefreshTimer(realtime, () => { + scroll(me); + }); + realtime.head = Date.now(); + } + options.bounds = undefined; + options.offset = false; + ticksOpts.autoSkip = false; + ticksOpts.source = source === 'auto' ? '' : source; + majorTicksOpts.enabled = true; + super.update(maxWidth, maxHeight, margins); + options.bounds = bounds; + options.offset = offset; + ticksOpts.autoSkip = autoSkip; + ticksOpts.source = source; + majorTicksOpts.enabled = majorEnabled; + } + buildTicks() { + const me = this; + const duration = resolveOption(me, 'duration'); + const delay = resolveOption(me, 'delay'); + const max = me.realtime.head - delay; + const min = max - duration; + const maxArray = [1e15, max]; + const minArray = [-1e15, min]; + Object.defineProperty(me, 'min', { + get: () => minArray.shift(), + set: helpers.noop + }); + Object.defineProperty(me, 'max', { + get: () => maxArray.shift(), + set: helpers.noop + }); + const ticks = super.buildTicks(); + delete me.min; + delete me.max; + me.min = min; + me.max = max; + return ticks; + } + calculateLabelRotation() { + const ticksOpts = this.options.ticks; + const maxRotation = ticksOpts.maxRotation; + ticksOpts.maxRotation = ticksOpts.minRotation || 0; + super.calculateLabelRotation(); + ticksOpts.maxRotation = maxRotation; + } + fit() { + const me = this; + const options = me.options; + super.fit(); + if (options.ticks.display && options.display && me.isHorizontal()) { + me.paddingLeft = 3; + me.paddingRight = 3; + me._handleMargins(); + } + } + draw(chartArea) { + const me = this; + const {chart, ctx} = me; + const area = me.isHorizontal() ? + { + left: chartArea.left, + top: 0, + right: chartArea.right, + bottom: chart.height + } : { + left: 0, + top: chartArea.top, + right: chart.width, + bottom: chartArea.bottom + }; + me._gridLineItems = null; + me._labelItems = null; + helpers.clipArea(ctx, area); + super.draw(chartArea); + helpers.unclipArea(ctx); + } + destroy() { + const me = this; + stopFrameRefreshTimer(me.realtime); + stopDataRefreshTimer(me); + } + _generate() { + const me = this; + const adapter = me._adapter; + const duration = resolveOption(me, 'duration'); + const delay = resolveOption(me, 'delay'); + const refresh = resolveOption(me, 'refresh'); + const max = me.realtime.head - delay; + const min = max - duration; + const capacity = me._getLabelCapacity(min); + const {time: timeOpts, ticks: ticksOpts} = me.options; + const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity); + const major = determineMajorUnit(minor); + const stepSize = timeOpts.stepSize || determineStepSize(min, max, minor, capacity); + const weekday = minor === 'week' ? timeOpts.isoWeekday : false; + const majorTicksEnabled = ticksOpts.major.enabled; + const hasWeekday = helpers.isNumber(weekday) || weekday === true; + const interval = INTERVALS[minor]; + const ticks = {}; + let first = min; + let time, count; + if (hasWeekday) { + first = +adapter.startOf(first, 'isoWeek', weekday); + } + first = +adapter.startOf(first, hasWeekday ? 'day' : minor); + if (adapter.diff(max, min, minor) > 100000 * stepSize) { + throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor); + } + time = first; + if (majorTicksEnabled && major && !hasWeekday && !timeOpts.round) { + time = +adapter.startOf(time, major); + time = +adapter.add(time, ~~((first - time) / (interval.size * stepSize)) * stepSize, minor); + } + const timestamps = ticksOpts.source === 'data' && me.getDataTimestamps(); + for (count = 0; time < max + refresh; time = +adapter.add(time, stepSize, minor), count++) { + addTick(ticks, time, timestamps); + } + if (time === max + refresh || count === 1) { + addTick(ticks, time, timestamps); + } + return Object.keys(ticks).sort((a, b) => a - b).map(x => +x); + } +} +RealTimeScale.id = 'realtime'; +RealTimeScale.defaults = { + bounds: 'data', + adapters: {}, + time: { + parser: false, + unit: false, + round: false, + isoWeekday: false, + minUnit: 'millisecond', + displayFormats: {} + }, + realtime: {}, + ticks: { + autoSkip: false, + source: 'auto', + major: { + enabled: true + } + } +}; +chart_js.defaults.describe('scale.realtime', { + _scriptable: name => name !== 'onRefresh' +}); + +var version = "2.0.0-beta.2"; + +function drawChart(chart) { + const streaming = chart.streaming; + const frameRate = chart.options.plugins.streaming.frameRate; + const frameDuration = 1000 / (Math.max(frameRate, 0) || 30); + const next = streaming.lastDrawn + frameDuration || 0; + const now = Date.now(); + if (next <= now) { + chart.render(); + if (streaming.lastMouseEvent) { + setTimeout(() => { + const lastMouseEvent = streaming.lastMouseEvent; + if (lastMouseEvent) { + chart._eventHandler(lastMouseEvent); + } + }, 0); + } + streaming.lastDrawn = (next + frameDuration > now) ? next : now; + } +} +var StreamingPlugin = { + id: 'streaming', + version, + beforeInit(chart) { + const streaming = chart.streaming = chart.streaming || {}; + const canvas = streaming.canvas = chart.canvas; + const mouseEventListener = streaming.mouseEventListener = event => { + const pos = helpers.getRelativePosition(event, chart); + streaming.lastMouseEvent = { + type: 'mousemove', + chart: chart, + native: event, + x: pos.x, + y: pos.y + }; + }; + canvas.addEventListener('mousedown', mouseEventListener); + canvas.addEventListener('mouseup', mouseEventListener); + chart.options.transitions.quiet = { + animation: { + duration: 0 + } + }; + }, + afterInit(chart) { + const {update, render, resetZoom} = chart; + chart.update = mode => { + if (mode === 'quiet') { + chart.render = helpers.noop; + update.call(chart, mode); + chart.render = render; + } else { + update.call(chart, mode); + } + }; + if (resetZoom) { + const zoomPlugin = chart_js.registry.getPlugin('zoom'); + zoomPlugin.zoomFunctions.realtime = zoomRealTimeScale; + zoomPlugin.panFunctions.realtime = panRealTimeScale; + chart.resetZoom = transition => { + resetRealTimeOptions(chart); + resetZoom(transition); + }; + } + }, + beforeUpdate(chart) { + const chartOpts = chart.options; + const scalesOpts = chartOpts.scales; + if (scalesOpts) { + Object.keys(scalesOpts).forEach(id => { + const scaleOpts = scalesOpts[id]; + if (scaleOpts.type === 'realtime') { + chartOpts.elements.line.capBezierPoints = false; + } + }); + } + return true; + }, + afterUpdate(chart) { + const {scales, streaming} = chart; + let pause = true; + helpers.each(scales, scale => { + if (scale instanceof RealTimeScale) { + pause &= resolveOption(scale, 'pause'); + } + }); + if (pause) { + stopFrameRefreshTimer(streaming); + } else { + startFrameRefreshTimer(streaming, () => { + drawChart(chart); + }); + } + }, + beforeDatasetDraw(chart, args) { + const {ctx, chartArea, width, height} = chart; + const {xAxisID, yAxisID, controller} = args.meta; + const area = { + left: 0, + top: 0, + right: width, + bottom: height + }; + if (xAxisID && controller.getScaleForId(xAxisID) instanceof RealTimeScale) { + area.left = chartArea.left; + area.right = chartArea.right; + } + if (yAxisID && controller.getScaleForId(yAxisID) instanceof RealTimeScale) { + area.top = chartArea.top; + area.bottom = chartArea.bottom; + } + helpers.clipArea(ctx, area); + return true; + }, + afterDatasetDraw(chart) { + helpers.unclipArea(chart.ctx); + }, + beforeEvent(chart, args) { + const streaming = chart.streaming; + const event = args.event; + if (event.type === 'mousemove') { + streaming.lastMouseEvent = event; + } else if (event.type === 'mouseout') { + delete streaming.lastMouseEvent; + } + return true; + }, + destroy(chart) { + const {scales, streaming} = chart; + const {canvas, mouseEventListener} = streaming; + stopFrameRefreshTimer(streaming); + canvas.removeEventListener('mousedown', mouseEventListener); + canvas.removeEventListener('mouseup', mouseEventListener); + helpers.each(scales, scale => { + if (scale instanceof RealTimeScale) { + scale.destroy(); + } + }); + }, + defaults: { + duration: 10000, + delay: 0, + frameRate: 30, + refresh: 1000, + onRefresh: null, + pause: false, + ttl: undefined + }, + descriptors: { + _scriptable: name => name !== 'onRefresh' + } +}; + +chart_js.Chart.register(StreamingPlugin, RealTimeScale); + +return StreamingPlugin; + +}))); diff --git a/dist/chartjs-plugin-streaming.min.js b/dist/chartjs-plugin-streaming.min.js new file mode 100644 index 0000000..b375ae0 --- /dev/null +++ b/dist/chartjs-plugin-streaming.min.js @@ -0,0 +1,7 @@ +/*! + * chartjs-plugin-streaming v2.0.0-beta.2 + * https://nagix.github.io/chartjs-plugin-streaming + * (c) 2017-2021 Akihiko Kusanagi + * Released under the MIT license + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","chart.js/helpers"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).ChartStreaming=t(e.Chart,e.Chart.helpers)}(this,(function(e,t){"use strict";function o(e,t,o){return Math.min(Math.max(e,t),o)}function a(e,o){const a=e.options.realtime,n=e.chart.options.plugins.streaming;return t.valueOrDefault(a[o],n[o])}const n="undefined"==typeof window?t.noop:window.cancelAnimationFrame;function i(e,o){if(!e.frameRequestID){const a=function(){o(),e.frameRequestID=t.requestAnimFrame.call(window,a)};e.frameRequestID=t.requestAnimFrame.call(window,a)}}function r(e){const t=e.frameRequestID;t&&(n.call(window,t),delete e.frameRequestID)}const s=new WeakMap;function l(e){const{originalScaleLimits:o}=function(e){let t=s.get(e);return t||(t={originalScaleLimits:{}},s.set(e,t)),t}(e),n=e.scales;return t.each(n,(e=>{const t=e.id;o[t]||(o[t]={duration:a(e,"duration"),delay:a(e,"delay")})})),t.each(o,((e,t)=>{n[t]||delete o[t]})),o}function c(e,t,n,i){const{chart:r,axis:s}=e,{minDuration:c=0,maxDuration:d=1/0,minDelay:u=-1/0,maxDelay:m=1/0}=i&&i[s]||{},f=e.options.realtime,p=a(e,"duration"),h=a(e,"delay"),g=o(p*(2-t),c,d);let y,b;return l(r),y=e.isHorizontal()?(e.right-n.x)/(e.right-e.left):(e.bottom-n.y)/(e.bottom-e.top),b=h+y*(p-g),f.duration=g,f.delay=o(b,u,m),g!==e.max-e.min}function d(e,t,n){const{chart:i,axis:r}=e,{minDelay:s=-1/0,maxDelay:c=1/0}=n&&n[r]||{},d=a(e,"delay")+(e.getValueForPixel(t)-e.getValueForPixel(0));return l(i),e.options.realtime.delay=o(d,s,c),!0}const u={millisecond:{common:!0,size:1,steps:[1,2,5,10,20,50,100,250,500]},second:{common:!0,size:1e3,steps:[1,2,5,10,15,30]},minute:{common:!0,size:6e4,steps:[1,2,5,10,15,30]},hour:{common:!0,size:36e5,steps:[1,2,3,6,12]},day:{common:!0,size:864e5,steps:[1,2,5]},week:{common:!1,size:6048e5,steps:[1,2,3,4]},month:{common:!0,size:2628e6,steps:[1,2,3]},quarter:{common:!1,size:7884e6,steps:[1,2,3,4]},year:{common:!0,size:3154e7}},m=Object.keys(u);function f(e,o,a){if(a){if(a.length){const{lo:n,hi:i}=t._lookup(a,o);e[a[n]>=o?a[n]:a[i]]=!0}}else e[o]=!0}function p(e,t){return t===e.xAxisID?"x":t===e.yAxisID&&"y"}const h=["pointBackgroundColor","pointBorderColor","pointBorderWidth","pointRadius","pointRotation","pointStyle","pointHitRadius","pointHoverBackgroundColor","pointHoverBorderColor","pointHoverBorderWidth","pointHoverRadius","backgroundColor","borderColor","borderSkipped","borderWidth","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth","hoverRadius","hitRadius","radius","rotation"];function g(e){const t=e.realtime,o=t.refreshTimerID;o&&(clearInterval(o),delete t.refreshTimerID,delete t.refreshInterval)}function y(e){const o=e.realtime,n=a(e,"refresh");o.refreshTimerID||(o.refreshTimerID=setInterval((()=>{const n=a(e,"refresh");!function(e){const{chart:o,id:n,max:i}=e,r=a(e,"duration"),s=a(e,"delay"),l=a(e,"ttl"),c=a(e,"pause"),d=a(e,"onRefresh"),u=Date.now()-(isNaN(l)?r+s:l);let m,f,g,y;t.callback(d,[o]),o.data.datasets.forEach(((e,a)=>{const r=o.getDatasetMeta(a),s=p(r,n);if(s){const o=r.controller,a=e.data,n=a.length;if(c){for(m=0;m{t.isArray(e[o])&&e[o].splice(f,g)})),t.each(e.datalabels,(e=>{t.isArray(e)&&e.splice(f,g)})),"object"!=typeof a[0]&&(y={start:f,count:g})}})),y&&o.data.labels.splice(y.start,y.count),o.update("quiet")}(e),o.refreshInterval===n||isNaN(n)||(g(e),y(e))}),n),o.refreshInterval=n)}const b={data:["x","controlPointPreviousX","controlPointNextX"],dataset:["x"],tooltip:["x","caretX"]},v={data:["y","controlPointPreviousY","controlPointNextY"],dataset:["y"],tooltip:["y","caretY"]};function x(e,t,o){const a=e.$animations;for(let a=0,n=t.length;a{!function(e){const{chart:o,id:n,realtime:i}=e,r=a(e,"duration"),s=a(e,"delay"),l=e.isHorizontal(),c=o.tooltip,d=c._active,u=Date.now();let m,f,h;l?(m=e.width,f=b):(m=e.height,f=v),h=m*(u-i.head)/r,!!l==!!e.options.reverse&&(h=-h),t.each(o.data.datasets,((e,t)=>{const a=o.getDatasetMeta(t);if(p(a,n)){const{data:e,dataset:t}=a,o=e||[];for(let e=0,t=o.length;el.shift(),set:t.noop}),Object.defineProperty(e,"max",{get:()=>s.shift(),set:t.noop});const c=super.buildTicks();return delete e.min,delete e.max,e.min=r,e.max=i,c}calculateLabelRotation(){const e=this.options.ticks,t=e.maxRotation;e.maxRotation=e.minRotation||0,super.calculateLabelRotation(),e.maxRotation=t}fit(){const e=this,t=e.options;super.fit(),t.ticks.display&&t.display&&e.isHorizontal()&&(e.paddingLeft=3,e.paddingRight=3,e._handleMargins())}draw(e){const o=this,{chart:a,ctx:n}=o,i=o.isHorizontal()?{left:e.left,top:0,right:e.right,bottom:a.height}:{left:0,top:e.top,right:a.width,bottom:e.bottom};o._gridLineItems=null,o._labelItems=null,t.clipArea(n,i),super.draw(e),t.unclipArea(n)}destroy(){r(this.realtime),g(this)}_generate(){const e=this,o=e._adapter,n=a(e,"duration"),i=a(e,"delay"),r=a(e,"refresh"),s=e.realtime.head-i,l=s-n,c=e._getLabelCapacity(l),{time:d,ticks:p}=e.options,h=d.unit||function(e,t,o,a){const n=o-t,i=m.length;for(let t=m.indexOf(e);t1e5*y)throw new Error(l+" and "+s+" are too far apart with stepSize of "+y+" "+h);k=R,v&&g&&!x&&!d.round&&(k=+o.startOf(k,g),k=+o.add(k,~~((R-k)/(D.size*y))*y,h));const z="data"===p.source&&e.getDataTimestamps();for(I=0;ke-t)).map((e=>+e))}}D.id="realtime",D.defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},realtime:{},ticks:{autoSkip:!1,source:"auto",major:{enabled:!0}}},e.defaults.describe("scale.realtime",{_scriptable:e=>"onRefresh"!==e});var w={id:"streaming",version:"2.0.0-beta.2",beforeInit(e){const o=e.streaming=e.streaming||{},a=o.canvas=e.canvas,n=o.mouseEventListener=a=>{const n=t.getRelativePosition(a,e);o.lastMouseEvent={type:"mousemove",chart:e,native:a,x:n.x,y:n.y}};a.addEventListener("mousedown",n),a.addEventListener("mouseup",n),e.options.transitions.quiet={animation:{duration:0}}},afterInit(o){const{update:a,render:n,resetZoom:i}=o;if(o.update=e=>{"quiet"===e?(o.render=t.noop,a.call(o,e),o.render=n):a.call(o,e)},i){const a=e.registry.getPlugin("zoom");a.zoomFunctions.realtime=c,a.panFunctions.realtime=d,o.resetZoom=e=>{!function(e){const o=l(e);t.each(e.scales,(e=>{const t=e.options.realtime;if(t){const a=o[e.id];a?(t.duration=a.duration,t.delay=a.delay):(delete t.duration,delete t.delay)}}))}(o),i(e)}}},beforeUpdate(e){const t=e.options,o=t.scales;return o&&Object.keys(o).forEach((e=>{"realtime"===o[e].type&&(t.elements.line.capBezierPoints=!1)})),!0},afterUpdate(e){const{scales:o,streaming:n}=e;let s=!0;t.each(o,(e=>{e instanceof D&&(s&=a(e,"pause"))})),s?r(n):i(n,(()=>{!function(e){const t=e.streaming,o=e.options.plugins.streaming.frameRate,a=1e3/(Math.max(o,0)||30),n=t.lastDrawn+a||0,i=Date.now();n<=i&&(e.render(),t.lastMouseEvent&&setTimeout((()=>{const o=t.lastMouseEvent;o&&e._eventHandler(o)}),0),t.lastDrawn=n+a>i?n:i)}(e)}))},beforeDatasetDraw(e,o){const{ctx:a,chartArea:n,width:i,height:r}=e,{xAxisID:s,yAxisID:l,controller:c}=o.meta,d={left:0,top:0,right:i,bottom:r};return s&&c.getScaleForId(s)instanceof D&&(d.left=n.left,d.right=n.right),l&&c.getScaleForId(l)instanceof D&&(d.top=n.top,d.bottom=n.bottom),t.clipArea(a,d),!0},afterDatasetDraw(e){t.unclipArea(e.ctx)},beforeEvent(e,t){const o=e.streaming,a=t.event;return"mousemove"===a.type?o.lastMouseEvent=a:"mouseout"===a.type&&delete o.lastMouseEvent,!0},destroy(e){const{scales:o,streaming:a}=e,{canvas:n,mouseEventListener:i}=a;r(a),n.removeEventListener("mousedown",i),n.removeEventListener("mouseup",i),t.each(o,(e=>{e instanceof D&&e.destroy()}))},defaults:{duration:1e4,delay:0,frameRate:30,refresh:1e3,onRefresh:null,pause:!1,ttl:void 0},descriptors:{_scriptable:e=>"onRefresh"!==e}};return e.Chart.register(w,D),w}));