diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..9d7ee2e --- /dev/null +++ b/bower.json @@ -0,0 +1,14 @@ +{ + "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", + "main": "dist/chartjs-plugin-streaming.js", + "ignore": [ + ".github", + ".codeclimate.yml", + ".gitignore", + "scripts" + ] +} diff --git a/dist/chartjs-plugin-streaming.esm.js b/dist/chartjs-plugin-streaming.esm.js new file mode 100644 index 0000000..0c09d13 --- /dev/null +++ b/dist/chartjs-plugin-streaming.esm.js @@ -0,0 +1,944 @@ +/*! + * chartjs-plugin-streaming v2.0.0 + * https://nagix.github.io/chartjs-plugin-streaming + * (c) 2017-2021 Akihiko Kusanagi + * Released under the MIT license + */ +import { registry, TimeScale, defaults, DatasetController, Chart } from 'chart.js'; +import { valueOrDefault, each, requestAnimFrame, callback, noop, isFinite, clipArea, unclipArea, isNumber, _lookup, 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]); +} +function getAxisMap(element, {x, y}, {xAxisID, yAxisID}) { + const axisMap = {}; + each(x, key => { + axisMap[key] = {axisId: xAxisID}; + }); + each(y, key => { + axisMap[key] = {axisId: yAxisID}; + }); + return axisMap; +} +const cancelAnimFrame = (function() { + if (typeof window === 'undefined') { + return noop; + } + return window.cancelAnimationFrame; +}()); +function startFrameRefreshTimer(context, func) { + if (!context.frameRequestID) { + const refresh = () => { + const nextRefresh = context.nextRefresh || 0; + const now = Date.now(); + if (nextRefresh <= now) { + const newFrameRate = callback(func); + const frameDuration = 1000 / (Math.max(newFrameRate, 0) || 30); + const newNextRefresh = context.nextRefresh + frameDuration || 0; + context.nextRefresh = newNextRefresh > now ? newNextRefresh : now + frameDuration; + } + context.frameRequestID = requestAnimFrame.call(window, refresh); + }; + context.frameRequestID = requestAnimFrame.call(window, refresh); + } +} +function stopFrameRefreshTimer(context) { + const frameRequestID = context.frameRequestID; + if (frameRequestID) { + cancelAnimFrame.call(window, frameRequestID); + delete context.frameRequestID; + } +} +function stopDataRefreshTimer(context) { + const refreshTimerID = context.refreshTimerID; + if (refreshTimerID) { + clearInterval(refreshTimerID); + delete context.refreshTimerID; + delete context.refreshInterval; + } +} +function startDataRefreshTimer(context, func, interval) { + if (!context.refreshTimerID) { + context.refreshTimerID = setInterval(() => { + const newInterval = callback(func); + if (context.refreshInterval !== newInterval && !isNaN(newInterval)) { + stopDataRefreshTimer(context); + startDataRefreshTimer(context, func, newInterval); + } + }, interval || 0); + context.refreshInterval = interval || 0; + } +} + +function scaleValue(scale, value, fallback) { + value = typeof value === 'number' ? value : scale.parse(value); + return isFinite(value) ? + {value: scale.getPixelForValue(value), transitionable: true} : + {value: fallback}; +} +function updateBoxAnnotation(element, chart, options) { + const {scales, chartArea} = chart; + const {xScaleID, yScaleID, xMin, xMax, yMin, yMax} = options; + const xScale = scales[xScaleID]; + const yScale = scales[yScaleID]; + const {top, left, bottom, right} = chartArea; + const streaming = element.$streaming = {}; + if (xScale) { + const min = scaleValue(xScale, xMin, left); + const max = scaleValue(xScale, xMax, right); + const reverse = min.value > max.value; + if (min.transitionable) { + streaming[reverse ? 'x2' : 'x'] = {axisId: xScaleID}; + } + if (max.transitionable) { + streaming[reverse ? 'x' : 'x2'] = {axisId: xScaleID}; + } + if (min.transitionable !== max.transitionable) { + streaming.width = {axisId: xScaleID, reverse: min.transitionable}; + } + } + if (yScale) { + const min = scaleValue(yScale, yMin, top); + const max = scaleValue(yScale, yMax, bottom); + const reverse = min.value > max.value; + if (min.transitionable) { + streaming[reverse ? 'y2' : 'y'] = {axisId: yScaleID}; + } + if (max.transitionable) { + streaming[reverse ? 'y' : 'y2'] = {axisId: yScaleID}; + } + if (min.transitionable !== max.transitionable) { + streaming.height = {axisId: yScaleID, reverse: min.transitionable}; + } + } +} +function updateLineAnnotation(element, chart, options) { + const {scales, chartArea} = chart; + const {scaleID, value} = options; + const scale = scales[scaleID]; + const {top, left, bottom, right} = chartArea; + const streaming = element.$streaming = {}; + if (scale) { + const isHorizontal = scale.isHorizontal(); + const pixel = scaleValue(scale, value); + if (pixel.transitionable) { + streaming[isHorizontal ? 'x' : 'y'] = {axisId: scaleID}; + streaming[isHorizontal ? 'x2' : 'y2'] = {axisId: scaleID}; + } + return isHorizontal ? {top, bottom} : {left, right}; + } + const {xScaleID, yScaleID, xMin, xMax, yMin, yMax} = options; + const xScale = scales[xScaleID]; + const yScale = scales[yScaleID]; + const clip = {}; + if (xScale) { + const min = scaleValue(xScale, xMin); + const max = scaleValue(xScale, xMax); + if (min.transitionable) { + streaming.x = {axisId: xScaleID}; + } else { + clip.left = left; + } + if (max.transitionable) { + streaming.x2 = {axisId: xScaleID}; + } else { + clip.right = right; + } + } + if (yScale) { + const min = scaleValue(yScale, yMin); + const max = scaleValue(yScale, yMax); + if (min.transitionable) { + streaming.y = {axisId: yScaleID}; + } else { + clip.top = top; + } + if (max.transitionable) { + streaming.y2 = {axisId: yScaleID}; + } else { + clip.bottom = bottom; + } + } + return clip; +} +function updatePointAnnotation(element, chart, options) { + const scales = chart.scales; + const {xScaleID, yScaleID, xValue, yValue} = options; + const xScale = scales[xScaleID]; + const yScale = scales[yScaleID]; + const streaming = element.$streaming = {}; + if (xScale) { + const x = scaleValue(xScale, xValue); + if (x.transitionable) { + streaming.x = {axisId: xScaleID}; + } + } + if (yScale) { + const y = scaleValue(yScale, yValue); + if (y.transitionable) { + streaming.y = {axisId: yScaleID}; + } + } +} +function initAnnotationPlugin() { + const BoxAnnotation = registry.getElement('boxAnnotation'); + const LineAnnotation = registry.getElement('lineAnnotation'); + const PointAnnotation = registry.getElement('pointAnnotation'); + const resolveBoxAnnotationProperties = BoxAnnotation.prototype.resolveElementProperties; + const resolveLineAnnotationProperties = LineAnnotation.prototype.resolveElementProperties; + const resolvePointAnnotationProperties = PointAnnotation.prototype.resolveElementProperties; + BoxAnnotation.prototype.resolveElementProperties = function(chart, options) { + updateBoxAnnotation(this, chart, options); + return resolveBoxAnnotationProperties.call(this, chart, options); + }; + LineAnnotation.prototype.resolveElementProperties = function(chart, options) { + const chartArea = chart.chartArea; + chart.chartArea = updateLineAnnotation(this, chart, options); + const properties = resolveLineAnnotationProperties.call(this, chart, options); + chart.chartArea = chartArea; + return properties; + }; + PointAnnotation.prototype.resolveElementProperties = function(chart, options) { + updatePointAnnotation(this, chart, options); + return resolvePointAnnotationProperties.call(this, chart, options); + }; +} +function attachChart$1(plugin, chart) { + const streaming = chart.$streaming; + if (streaming.annotationPlugin !== plugin) { + const afterUpdate = plugin.afterUpdate; + initAnnotationPlugin(); + streaming.annotationPlugin = plugin; + plugin.afterUpdate = (_chart, args, options) => { + const mode = args.mode; + const animationOpts = options.animation; + if (mode === 'quiet') { + options.animation = false; + } + afterUpdate.call(this, _chart, args, options); + if (mode === 'quiet') { + options.animation = animationOpts; + } + }; + } +} +function getElements(chart) { + const plugin = chart.$streaming.annotationPlugin; + if (plugin) { + const state = plugin._getState(chart); + return state && state.elements || []; + } + return []; +} +function detachChart$1(chart) { + delete chart.$streaming.annotationPlugin; +} + +const transitionKeys$1 = {x: ['x', 'caretX'], y: ['y', 'caretY']}; +function update$1(...args) { + const me = this; + const element = me.getActiveElements()[0]; + if (element) { + const meta = me._chart.getDatasetMeta(element.datasetIndex); + me.$streaming = getAxisMap(me, transitionKeys$1, meta); + } else { + me.$streaming = {}; + } + me.constructor.prototype.update.call(me, ...args); +} + +const chartStates = new WeakMap(); +function getState(chart) { + let state = chartStates.get(chart); + if (!state) { + state = {originalScaleOptions: {}}; + chartStates.set(chart, state); + } + return state; +} +function removeState(chart) { + chartStates.delete(chart); +} +function storeOriginalScaleOptions(chart) { + const {originalScaleOptions} = getState(chart); + const scales = chart.scales; + each(scales, scale => { + const id = scale.id; + if (!originalScaleOptions[id]) { + originalScaleOptions[id] = { + duration: resolveOption(scale, 'duration'), + delay: resolveOption(scale, 'delay') + }; + } + }); + each(originalScaleOptions, (opt, key) => { + if (!scales[key]) { + delete originalScaleOptions[key]; + } + }); + return originalScaleOptions; +} +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; + storeOriginalScaleOptions(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)); + storeOriginalScaleOptions(chart); + scale.options.realtime.delay = clamp(newDelay, minDelay, maxDelay); + return true; +} +function resetRealTimeScaleOptions(chart) { + const originalScaleOptions = storeOriginalScaleOptions(chart); + each(chart.scales, scale => { + const realtimeOptions = scale.options.realtime; + if (realtimeOptions) { + const original = originalScaleOptions[scale.id]; + if (original) { + realtimeOptions.duration = original.duration; + realtimeOptions.delay = original.delay; + } else { + delete realtimeOptions.duration; + delete realtimeOptions.delay; + } + } + }); +} +function initZoomPlugin(plugin) { + plugin.zoomFunctions.realtime = zoomRealTimeScale; + plugin.panFunctions.realtime = panRealTimeScale; +} +function attachChart(plugin, chart) { + const streaming = chart.$streaming; + if (streaming.zoomPlugin !== plugin) { + const resetZoom = streaming.resetZoom = chart.resetZoom; + initZoomPlugin(plugin); + chart.resetZoom = transition => { + resetRealTimeScaleOptions(chart); + resetZoom(transition); + }; + streaming.zoomPlugin = plugin; + } +} +function detachChart(chart) { + const streaming = chart.$streaming; + if (streaming.zoomPlugin) { + chart.resetZoom = streaming.resetZoom; + removeState(chart); + delete streaming.resetZoom; + delete streaming.zoomPlugin; + } +} + +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; + } +} +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 clean(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 min = Date.now() - (isNaN(ttl) ? duration + delay : ttl); + let i, start, count, removalRange; + each(chart.data.datasets, (dataset, datasetIndex) => { + const meta = chart.getDatasetMeta(datasetIndex); + const axis = id === meta.xAxisID && 'x' || id === meta.yAxisID && 'y'; + if (axis) { + const controller = meta.controller; + const data = dataset.data; + const length = data.length; + if (pause) { + for (i = 0; i < length; ++i) { + const point = controller.getParsed(i); + if (point && !(point[axis] < max)) { + break; + } + } + start = i + 2; + } else { + start = 0; + } + for (i = start; i < length; ++i) { + const point = controller.getParsed(i); + if (!point || !(point[axis] <= min)) { + break; + } + } + count = i - start; + if (isNaN(ttl)) { + count = Math.max(count - 2, 0); + } + data.splice(start, count); + each(datasetPropertyKeys, 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 + }; + } + each(chart._active, (item, index) => { + if (item.datasetIndex === datasetIndex && item.index >= start) { + if (item.index >= start + count) { + item.index -= count; + } else { + chart._active.splice(index, 1); + } + } + }, null, true); + } + }); + if (removalRange) { + chart.data.labels.splice(removalRange.start, removalRange.count); + } +} +function transition(element, id, translate) { + const animations = element.$animations || {}; + each(element.$streaming, (item, key) => { + if (item.axisId === id) { + const delta = item.reverse ? -translate : translate; + const animation = animations[key]; + if (isFinite(element[key])) { + element[key] -= delta; + } + if (animation) { + animation._from -= delta; + animation._to -= delta; + } + } + }); +} +function scroll(scale) { + const {chart, id, $realtime: realtime} = scale; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const isHorizontal = scale.isHorizontal(); + const length = isHorizontal ? scale.width : scale.height; + const now = Date.now(); + const tooltip = chart.tooltip; + const annotations = getElements(chart); + let offset = length * (now - realtime.head) / duration; + if (isHorizontal === !!scale.options.reverse) { + offset = -offset; + } + each(chart.data.datasets, (dataset, datasetIndex) => { + const meta = chart.getDatasetMeta(datasetIndex); + const {data: elements = [], dataset: element} = meta; + for (let i = 0, ilen = elements.length; i < ilen; ++i) { + transition(elements[i], id, offset); + } + if (element) { + transition(element, id, offset); + delete element._path; + } + }); + for (let i = 0, ilen = annotations.length; i < ilen; ++i) { + transition(annotations[i], id, offset); + } + if (tooltip) { + transition(tooltip, id, 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) { + const me = this; + super.init(scaleOpts, opts); + startDataRefreshTimer(me.$realtime, () => { + const chart = me.chart; + const onRefresh = resolveOption(me, 'onRefresh'); + callback(onRefresh, [chart], me); + clean(me); + chart.update('quiet'); + return resolveOption(me, 'refresh'); + }); + } + update(maxWidth, maxHeight, margins) { + const me = this; + const {$realtime: 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 { + if (!realtime.frameRequestID) { + realtime.head = Date.now(); + } + startFrameRefreshTimer(realtime, () => { + const chart = me.chart; + const streaming = chart.$streaming; + scroll(me); + if (streaming) { + callback(streaming.render, [chart]); + } + return resolveOption(me, 'frameRate'); + }); + } + 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 realtime = this.$realtime; + stopFrameRefreshTimer(realtime); + stopDataRefreshTimer(realtime); + } + _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"; + +defaults.set('transitions', { + quiet: { + animation: { + duration: 0 + } + } +}); +const transitionKeys = {x: ['x', 'cp1x', 'cp2x'], y: ['y', 'cp1y', 'cp2y']}; +function update(mode) { + const me = this; + if (mode === 'quiet') { + each(me.data.datasets, (dataset, datasetIndex) => { + const controller = me.getDatasetMeta(datasetIndex).controller; + controller._setStyle = function(element, index, _mode, active) { + DatasetController.prototype._setStyle.call(this, element, index, 'quiet', active); + }; + }); + } + Chart.prototype.update.call(me, mode); + if (mode === 'quiet') { + each(me.data.datasets, (dataset, datasetIndex) => { + delete me.getDatasetMeta(datasetIndex).controller._setStyle; + }); + } +} +function render(chart) { + const streaming = chart.$streaming; + chart.render(); + if (streaming.lastMouseEvent) { + setTimeout(() => { + const lastMouseEvent = streaming.lastMouseEvent; + if (lastMouseEvent) { + chart._eventHandler(lastMouseEvent); + } + }, 0); + } +} +var StreamingPlugin = { + id: 'streaming', + version, + beforeInit(chart) { + const streaming = chart.$streaming = chart.$streaming || {render}; + 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); + }, + afterInit(chart) { + chart.update = update; + }, + beforeUpdate(chart) { + const {scales, elements} = chart.options; + const tooltip = chart.tooltip; + each(scales, ({type}) => { + if (type === 'realtime') { + elements.line.capBezierPoints = false; + } + }); + if (tooltip) { + tooltip.update = update$1; + } + try { + const plugin = registry.getPlugin('annotation'); + attachChart$1(plugin, chart); + } catch (e) { + detachChart$1(chart); + } + try { + const plugin = registry.getPlugin('zoom'); + attachChart(plugin, chart); + } catch (e) { + detachChart(chart); + } + }, + beforeDatasetUpdate(chart, args) { + const {meta, mode} = args; + if (mode === 'quiet') { + const {controller, $animations} = meta; + if ($animations && $animations.visible && $animations.visible._active) { + controller.updateElement = noop; + controller.updateSharedOptions = noop; + } + } + }, + afterDatasetUpdate(chart, args) { + const {meta, mode} = args; + const {data: elements = [], dataset: element, controller} = meta; + for (let i = 0, ilen = elements.length; i < ilen; ++i) { + elements[i].$streaming = getAxisMap(elements[i], transitionKeys, meta); + } + if (element) { + element.$streaming = getAxisMap(element, transitionKeys, meta); + } + if (mode === 'quiet') { + delete controller.updateElement; + delete controller.updateSharedOptions; + } + }, + 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); + }, + 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; + } + }, + destroy(chart) { + const {scales, $streaming: streaming, tooltip} = chart; + const {canvas, mouseEventListener} = streaming; + delete chart.update; + if (tooltip) { + delete tooltip.update; + } + 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' + } +}; + +const registerables = [StreamingPlugin, RealTimeScale]; + +export default registerables; +export { RealTimeScale, StreamingPlugin }; diff --git a/dist/chartjs-plugin-streaming.js b/dist/chartjs-plugin-streaming.js new file mode 100644 index 0000000..deb3769 --- /dev/null +++ b/dist/chartjs-plugin-streaming.js @@ -0,0 +1,949 @@ +/*! + * chartjs-plugin-streaming v2.0.0 + * 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]); +} +function getAxisMap(element, {x, y}, {xAxisID, yAxisID}) { + const axisMap = {}; + helpers.each(x, key => { + axisMap[key] = {axisId: xAxisID}; + }); + helpers.each(y, key => { + axisMap[key] = {axisId: yAxisID}; + }); + return axisMap; +} +const cancelAnimFrame = (function() { + if (typeof window === 'undefined') { + return helpers.noop; + } + return window.cancelAnimationFrame; +}()); +function startFrameRefreshTimer(context, func) { + if (!context.frameRequestID) { + const refresh = () => { + const nextRefresh = context.nextRefresh || 0; + const now = Date.now(); + if (nextRefresh <= now) { + const newFrameRate = helpers.callback(func); + const frameDuration = 1000 / (Math.max(newFrameRate, 0) || 30); + const newNextRefresh = context.nextRefresh + frameDuration || 0; + context.nextRefresh = newNextRefresh > now ? newNextRefresh : now + frameDuration; + } + context.frameRequestID = helpers.requestAnimFrame.call(window, refresh); + }; + context.frameRequestID = helpers.requestAnimFrame.call(window, refresh); + } +} +function stopFrameRefreshTimer(context) { + const frameRequestID = context.frameRequestID; + if (frameRequestID) { + cancelAnimFrame.call(window, frameRequestID); + delete context.frameRequestID; + } +} +function stopDataRefreshTimer(context) { + const refreshTimerID = context.refreshTimerID; + if (refreshTimerID) { + clearInterval(refreshTimerID); + delete context.refreshTimerID; + delete context.refreshInterval; + } +} +function startDataRefreshTimer(context, func, interval) { + if (!context.refreshTimerID) { + context.refreshTimerID = setInterval(() => { + const newInterval = helpers.callback(func); + if (context.refreshInterval !== newInterval && !isNaN(newInterval)) { + stopDataRefreshTimer(context); + startDataRefreshTimer(context, func, newInterval); + } + }, interval || 0); + context.refreshInterval = interval || 0; + } +} + +function scaleValue(scale, value, fallback) { + value = typeof value === 'number' ? value : scale.parse(value); + return helpers.isFinite(value) ? + {value: scale.getPixelForValue(value), transitionable: true} : + {value: fallback}; +} +function updateBoxAnnotation(element, chart, options) { + const {scales, chartArea} = chart; + const {xScaleID, yScaleID, xMin, xMax, yMin, yMax} = options; + const xScale = scales[xScaleID]; + const yScale = scales[yScaleID]; + const {top, left, bottom, right} = chartArea; + const streaming = element.$streaming = {}; + if (xScale) { + const min = scaleValue(xScale, xMin, left); + const max = scaleValue(xScale, xMax, right); + const reverse = min.value > max.value; + if (min.transitionable) { + streaming[reverse ? 'x2' : 'x'] = {axisId: xScaleID}; + } + if (max.transitionable) { + streaming[reverse ? 'x' : 'x2'] = {axisId: xScaleID}; + } + if (min.transitionable !== max.transitionable) { + streaming.width = {axisId: xScaleID, reverse: min.transitionable}; + } + } + if (yScale) { + const min = scaleValue(yScale, yMin, top); + const max = scaleValue(yScale, yMax, bottom); + const reverse = min.value > max.value; + if (min.transitionable) { + streaming[reverse ? 'y2' : 'y'] = {axisId: yScaleID}; + } + if (max.transitionable) { + streaming[reverse ? 'y' : 'y2'] = {axisId: yScaleID}; + } + if (min.transitionable !== max.transitionable) { + streaming.height = {axisId: yScaleID, reverse: min.transitionable}; + } + } +} +function updateLineAnnotation(element, chart, options) { + const {scales, chartArea} = chart; + const {scaleID, value} = options; + const scale = scales[scaleID]; + const {top, left, bottom, right} = chartArea; + const streaming = element.$streaming = {}; + if (scale) { + const isHorizontal = scale.isHorizontal(); + const pixel = scaleValue(scale, value); + if (pixel.transitionable) { + streaming[isHorizontal ? 'x' : 'y'] = {axisId: scaleID}; + streaming[isHorizontal ? 'x2' : 'y2'] = {axisId: scaleID}; + } + return isHorizontal ? {top, bottom} : {left, right}; + } + const {xScaleID, yScaleID, xMin, xMax, yMin, yMax} = options; + const xScale = scales[xScaleID]; + const yScale = scales[yScaleID]; + const clip = {}; + if (xScale) { + const min = scaleValue(xScale, xMin); + const max = scaleValue(xScale, xMax); + if (min.transitionable) { + streaming.x = {axisId: xScaleID}; + } else { + clip.left = left; + } + if (max.transitionable) { + streaming.x2 = {axisId: xScaleID}; + } else { + clip.right = right; + } + } + if (yScale) { + const min = scaleValue(yScale, yMin); + const max = scaleValue(yScale, yMax); + if (min.transitionable) { + streaming.y = {axisId: yScaleID}; + } else { + clip.top = top; + } + if (max.transitionable) { + streaming.y2 = {axisId: yScaleID}; + } else { + clip.bottom = bottom; + } + } + return clip; +} +function updatePointAnnotation(element, chart, options) { + const scales = chart.scales; + const {xScaleID, yScaleID, xValue, yValue} = options; + const xScale = scales[xScaleID]; + const yScale = scales[yScaleID]; + const streaming = element.$streaming = {}; + if (xScale) { + const x = scaleValue(xScale, xValue); + if (x.transitionable) { + streaming.x = {axisId: xScaleID}; + } + } + if (yScale) { + const y = scaleValue(yScale, yValue); + if (y.transitionable) { + streaming.y = {axisId: yScaleID}; + } + } +} +function initAnnotationPlugin() { + const BoxAnnotation = chart_js.registry.getElement('boxAnnotation'); + const LineAnnotation = chart_js.registry.getElement('lineAnnotation'); + const PointAnnotation = chart_js.registry.getElement('pointAnnotation'); + const resolveBoxAnnotationProperties = BoxAnnotation.prototype.resolveElementProperties; + const resolveLineAnnotationProperties = LineAnnotation.prototype.resolveElementProperties; + const resolvePointAnnotationProperties = PointAnnotation.prototype.resolveElementProperties; + BoxAnnotation.prototype.resolveElementProperties = function(chart, options) { + updateBoxAnnotation(this, chart, options); + return resolveBoxAnnotationProperties.call(this, chart, options); + }; + LineAnnotation.prototype.resolveElementProperties = function(chart, options) { + const chartArea = chart.chartArea; + chart.chartArea = updateLineAnnotation(this, chart, options); + const properties = resolveLineAnnotationProperties.call(this, chart, options); + chart.chartArea = chartArea; + return properties; + }; + PointAnnotation.prototype.resolveElementProperties = function(chart, options) { + updatePointAnnotation(this, chart, options); + return resolvePointAnnotationProperties.call(this, chart, options); + }; +} +function attachChart$1(plugin, chart) { + const streaming = chart.$streaming; + if (streaming.annotationPlugin !== plugin) { + const afterUpdate = plugin.afterUpdate; + initAnnotationPlugin(); + streaming.annotationPlugin = plugin; + plugin.afterUpdate = (_chart, args, options) => { + const mode = args.mode; + const animationOpts = options.animation; + if (mode === 'quiet') { + options.animation = false; + } + afterUpdate.call(this, _chart, args, options); + if (mode === 'quiet') { + options.animation = animationOpts; + } + }; + } +} +function getElements(chart) { + const plugin = chart.$streaming.annotationPlugin; + if (plugin) { + const state = plugin._getState(chart); + return state && state.elements || []; + } + return []; +} +function detachChart$1(chart) { + delete chart.$streaming.annotationPlugin; +} + +const transitionKeys$1 = {x: ['x', 'caretX'], y: ['y', 'caretY']}; +function update$1(...args) { + const me = this; + const element = me.getActiveElements()[0]; + if (element) { + const meta = me._chart.getDatasetMeta(element.datasetIndex); + me.$streaming = getAxisMap(me, transitionKeys$1, meta); + } else { + me.$streaming = {}; + } + me.constructor.prototype.update.call(me, ...args); +} + +const chartStates = new WeakMap(); +function getState(chart) { + let state = chartStates.get(chart); + if (!state) { + state = {originalScaleOptions: {}}; + chartStates.set(chart, state); + } + return state; +} +function removeState(chart) { + chartStates.delete(chart); +} +function storeOriginalScaleOptions(chart) { + const {originalScaleOptions} = getState(chart); + const scales = chart.scales; + helpers.each(scales, scale => { + const id = scale.id; + if (!originalScaleOptions[id]) { + originalScaleOptions[id] = { + duration: resolveOption(scale, 'duration'), + delay: resolveOption(scale, 'delay') + }; + } + }); + helpers.each(originalScaleOptions, (opt, key) => { + if (!scales[key]) { + delete originalScaleOptions[key]; + } + }); + return originalScaleOptions; +} +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; + storeOriginalScaleOptions(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)); + storeOriginalScaleOptions(chart); + scale.options.realtime.delay = clamp(newDelay, minDelay, maxDelay); + return true; +} +function resetRealTimeScaleOptions(chart) { + const originalScaleOptions = storeOriginalScaleOptions(chart); + helpers.each(chart.scales, scale => { + const realtimeOptions = scale.options.realtime; + if (realtimeOptions) { + const original = originalScaleOptions[scale.id]; + if (original) { + realtimeOptions.duration = original.duration; + realtimeOptions.delay = original.delay; + } else { + delete realtimeOptions.duration; + delete realtimeOptions.delay; + } + } + }); +} +function initZoomPlugin(plugin) { + plugin.zoomFunctions.realtime = zoomRealTimeScale; + plugin.panFunctions.realtime = panRealTimeScale; +} +function attachChart(plugin, chart) { + const streaming = chart.$streaming; + if (streaming.zoomPlugin !== plugin) { + const resetZoom = streaming.resetZoom = chart.resetZoom; + initZoomPlugin(plugin); + chart.resetZoom = transition => { + resetRealTimeScaleOptions(chart); + resetZoom(transition); + }; + streaming.zoomPlugin = plugin; + } +} +function detachChart(chart) { + const streaming = chart.$streaming; + if (streaming.zoomPlugin) { + chart.resetZoom = streaming.resetZoom; + removeState(chart); + delete streaming.resetZoom; + delete streaming.zoomPlugin; + } +} + +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; + } +} +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 clean(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 min = Date.now() - (isNaN(ttl) ? duration + delay : ttl); + let i, start, count, removalRange; + helpers.each(chart.data.datasets, (dataset, datasetIndex) => { + const meta = chart.getDatasetMeta(datasetIndex); + const axis = id === meta.xAxisID && 'x' || id === meta.yAxisID && 'y'; + if (axis) { + const controller = meta.controller; + const data = dataset.data; + const length = data.length; + if (pause) { + for (i = 0; i < length; ++i) { + const point = controller.getParsed(i); + if (point && !(point[axis] < max)) { + break; + } + } + start = i + 2; + } else { + start = 0; + } + for (i = start; i < length; ++i) { + const point = controller.getParsed(i); + if (!point || !(point[axis] <= min)) { + break; + } + } + count = i - start; + if (isNaN(ttl)) { + count = Math.max(count - 2, 0); + } + data.splice(start, count); + helpers.each(datasetPropertyKeys, 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 + }; + } + helpers.each(chart._active, (item, index) => { + if (item.datasetIndex === datasetIndex && item.index >= start) { + if (item.index >= start + count) { + item.index -= count; + } else { + chart._active.splice(index, 1); + } + } + }, null, true); + } + }); + if (removalRange) { + chart.data.labels.splice(removalRange.start, removalRange.count); + } +} +function transition(element, id, translate) { + const animations = element.$animations || {}; + helpers.each(element.$streaming, (item, key) => { + if (item.axisId === id) { + const delta = item.reverse ? -translate : translate; + const animation = animations[key]; + if (helpers.isFinite(element[key])) { + element[key] -= delta; + } + if (animation) { + animation._from -= delta; + animation._to -= delta; + } + } + }); +} +function scroll(scale) { + const {chart, id, $realtime: realtime} = scale; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const isHorizontal = scale.isHorizontal(); + const length = isHorizontal ? scale.width : scale.height; + const now = Date.now(); + const tooltip = chart.tooltip; + const annotations = getElements(chart); + let 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); + const {data: elements = [], dataset: element} = meta; + for (let i = 0, ilen = elements.length; i < ilen; ++i) { + transition(elements[i], id, offset); + } + if (element) { + transition(element, id, offset); + delete element._path; + } + }); + for (let i = 0, ilen = annotations.length; i < ilen; ++i) { + transition(annotations[i], id, offset); + } + if (tooltip) { + transition(tooltip, id, 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) { + const me = this; + super.init(scaleOpts, opts); + startDataRefreshTimer(me.$realtime, () => { + const chart = me.chart; + const onRefresh = resolveOption(me, 'onRefresh'); + helpers.callback(onRefresh, [chart], me); + clean(me); + chart.update('quiet'); + return resolveOption(me, 'refresh'); + }); + } + update(maxWidth, maxHeight, margins) { + const me = this; + const {$realtime: 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 { + if (!realtime.frameRequestID) { + realtime.head = Date.now(); + } + startFrameRefreshTimer(realtime, () => { + const chart = me.chart; + const streaming = chart.$streaming; + scroll(me); + if (streaming) { + helpers.callback(streaming.render, [chart]); + } + return resolveOption(me, 'frameRate'); + }); + } + 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 realtime = this.$realtime; + stopFrameRefreshTimer(realtime); + stopDataRefreshTimer(realtime); + } + _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"; + +chart_js.defaults.set('transitions', { + quiet: { + animation: { + duration: 0 + } + } +}); +const transitionKeys = {x: ['x', 'cp1x', 'cp2x'], y: ['y', 'cp1y', 'cp2y']}; +function update(mode) { + const me = this; + if (mode === 'quiet') { + helpers.each(me.data.datasets, (dataset, datasetIndex) => { + const controller = me.getDatasetMeta(datasetIndex).controller; + controller._setStyle = function(element, index, _mode, active) { + chart_js.DatasetController.prototype._setStyle.call(this, element, index, 'quiet', active); + }; + }); + } + chart_js.Chart.prototype.update.call(me, mode); + if (mode === 'quiet') { + helpers.each(me.data.datasets, (dataset, datasetIndex) => { + delete me.getDatasetMeta(datasetIndex).controller._setStyle; + }); + } +} +function render(chart) { + const streaming = chart.$streaming; + chart.render(); + if (streaming.lastMouseEvent) { + setTimeout(() => { + const lastMouseEvent = streaming.lastMouseEvent; + if (lastMouseEvent) { + chart._eventHandler(lastMouseEvent); + } + }, 0); + } +} +var StreamingPlugin = { + id: 'streaming', + version, + beforeInit(chart) { + const streaming = chart.$streaming = chart.$streaming || {render}; + 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); + }, + afterInit(chart) { + chart.update = update; + }, + beforeUpdate(chart) { + const {scales, elements} = chart.options; + const tooltip = chart.tooltip; + helpers.each(scales, ({type}) => { + if (type === 'realtime') { + elements.line.capBezierPoints = false; + } + }); + if (tooltip) { + tooltip.update = update$1; + } + try { + const plugin = chart_js.registry.getPlugin('annotation'); + attachChart$1(plugin, chart); + } catch (e) { + detachChart$1(chart); + } + try { + const plugin = chart_js.registry.getPlugin('zoom'); + attachChart(plugin, chart); + } catch (e) { + detachChart(chart); + } + }, + beforeDatasetUpdate(chart, args) { + const {meta, mode} = args; + if (mode === 'quiet') { + const {controller, $animations} = meta; + if ($animations && $animations.visible && $animations.visible._active) { + controller.updateElement = helpers.noop; + controller.updateSharedOptions = helpers.noop; + } + } + }, + afterDatasetUpdate(chart, args) { + const {meta, mode} = args; + const {data: elements = [], dataset: element, controller} = meta; + for (let i = 0, ilen = elements.length; i < ilen; ++i) { + elements[i].$streaming = getAxisMap(elements[i], transitionKeys, meta); + } + if (element) { + element.$streaming = getAxisMap(element, transitionKeys, meta); + } + if (mode === 'quiet') { + delete controller.updateElement; + delete controller.updateSharedOptions; + } + }, + 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); + }, + 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; + } + }, + destroy(chart) { + const {scales, $streaming: streaming, tooltip} = chart; + const {canvas, mouseEventListener} = streaming; + delete chart.update; + if (tooltip) { + delete tooltip.update; + } + 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' + } +}; + +const registerables = [StreamingPlugin, RealTimeScale]; +chart_js.Chart.register(registerables); + +return registerables; + +}))); diff --git a/dist/chartjs-plugin-streaming.min.js b/dist/chartjs-plugin-streaming.min.js new file mode 100644 index 0000000..b6412c3 --- /dev/null +++ b/dist/chartjs-plugin-streaming.min.js @@ -0,0 +1,7 @@ +/*! + * chartjs-plugin-streaming v2.0.0 + * 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 n(e,o){const n=e.options.realtime,a=e.chart.options.plugins.streaming;return t.valueOrDefault(n[o],a[o])}function a(e,{x:o,y:n},{xAxisID:a,yAxisID:i}){const s={};return t.each(o,(e=>{s[e]={axisId:a}})),t.each(n,(e=>{s[e]={axisId:i}})),s}const i="undefined"==typeof window?t.noop:window.cancelAnimationFrame;function s(e){const t=e.frameRequestID;t&&(i.call(window,t),delete e.frameRequestID)}function r(e){const t=e.refreshTimerID;t&&(clearInterval(t),delete e.refreshTimerID,delete e.refreshInterval)}function l(e,o,n){e.refreshTimerID||(e.refreshTimerID=setInterval((()=>{const n=t.callback(o);e.refreshInterval===n||isNaN(n)||(r(e),l(e,o,n))}),n||0),e.refreshInterval=n||0)}function c(e,o,n){return o="number"==typeof o?o:e.parse(o),t.isFinite(o)?{value:e.getPixelForValue(o),transitionable:!0}:{value:n}}function d(){const t=e.registry.getElement("boxAnnotation"),o=e.registry.getElement("lineAnnotation"),n=e.registry.getElement("pointAnnotation"),a=t.prototype.resolveElementProperties,i=o.prototype.resolveElementProperties,s=n.prototype.resolveElementProperties;t.prototype.resolveElementProperties=function(e,t){return function(e,t,o){const{scales:n,chartArea:a}=t,{xScaleID:i,yScaleID:s,xMin:r,xMax:l,yMin:d,yMax:u}=o,m=n[i],p=n[s],{top:f,left:h,bottom:g,right:y}=a,x=e.$streaming={};if(m){const e=c(m,r,h),t=c(m,l,y),o=e.value>t.value;e.transitionable&&(x[o?"x2":"x"]={axisId:i}),t.transitionable&&(x[o?"x":"x2"]={axisId:i}),e.transitionable!==t.transitionable&&(x.width={axisId:i,reverse:e.transitionable})}if(p){const e=c(p,d,f),t=c(p,u,g),o=e.value>t.value;e.transitionable&&(x[o?"y2":"y"]={axisId:s}),t.transitionable&&(x[o?"y":"y2"]={axisId:s}),e.transitionable!==t.transitionable&&(x.height={axisId:s,reverse:e.transitionable})}}(this,e,t),a.call(this,e,t)},o.prototype.resolveElementProperties=function(e,t){const o=e.chartArea;e.chartArea=function(e,t,o){const{scales:n,chartArea:a}=t,{scaleID:i,value:s}=o,r=n[i],{top:l,left:d,bottom:u,right:m}=a,p=e.$streaming={};if(r){const e=r.isHorizontal();return c(r,s).transitionable&&(p[e?"x":"y"]={axisId:i},p[e?"x2":"y2"]={axisId:i}),e?{top:l,bottom:u}:{left:d,right:m}}const{xScaleID:f,yScaleID:h,xMin:g,xMax:y,yMin:x,yMax:b}=o,v=n[f],I=n[h],D={};if(v){const e=c(v,g),t=c(v,y);e.transitionable?p.x={axisId:f}:D.left=d,t.transitionable?p.x2={axisId:f}:D.right=m}if(I){const e=c(I,x),t=c(I,b);e.transitionable?p.y={axisId:h}:D.top=l,t.transitionable?p.y2={axisId:h}:D.bottom=u}return D}(this,e,t);const n=i.call(this,e,t);return e.chartArea=o,n},n.prototype.resolveElementProperties=function(e,t){return function(e,t,o){const n=t.scales,{xScaleID:a,yScaleID:i,xValue:s,yValue:r}=o,l=n[a],d=n[i],u=e.$streaming={};l&&c(l,s).transitionable&&(u.x={axisId:a});d&&c(d,r).transitionable&&(u.y={axisId:i})}(this,e,t),s.call(this,e,t)}}const u={x:["x","caretX"],y:["y","caretY"]};function m(...e){const t=this,o=t.getActiveElements()[0];if(o){const e=t._chart.getDatasetMeta(o.datasetIndex);t.$streaming=a(0,u,e)}else t.$streaming={};t.constructor.prototype.update.call(t,...e)}const p=new WeakMap;function f(e){const{originalScaleOptions:o}=function(e){let t=p.get(e);return t||(t={originalScaleOptions:{}},p.set(e,t)),t}(e),a=e.scales;return t.each(a,(e=>{const t=e.id;o[t]||(o[t]={duration:n(e,"duration"),delay:n(e,"delay")})})),t.each(o,((e,t)=>{a[t]||delete o[t]})),o}function h(e,t,a,i){const{chart:s,axis:r}=e,{minDuration:l=0,maxDuration:c=1/0,minDelay:d=-1/0,maxDelay:u=1/0}=i&&i[r]||{},m=e.options.realtime,p=n(e,"duration"),h=n(e,"delay"),g=o(p*(2-t),l,c);let y,x;return f(s),y=e.isHorizontal()?(e.right-a.x)/(e.right-e.left):(e.bottom-a.y)/(e.bottom-e.top),x=h+y*(p-g),m.duration=g,m.delay=o(x,d,u),g!==e.max-e.min}function g(e,t,a){const{chart:i,axis:s}=e,{minDelay:r=-1/0,maxDelay:l=1/0}=a&&a[s]||{},c=n(e,"delay")+(e.getValueForPixel(t)-e.getValueForPixel(0));return f(i),e.options.realtime.delay=o(c,r,l),!0}function y(e,o){const n=o.$streaming;if(n.zoomPlugin!==e){const a=n.resetZoom=o.resetZoom;!function(e){e.zoomFunctions.realtime=h,e.panFunctions.realtime=g}(e),o.resetZoom=e=>{!function(e){const o=f(e);t.each(e.scales,(e=>{const t=e.options.realtime;if(t){const n=o[e.id];n?(t.duration=n.duration,t.delay=n.delay):(delete t.duration,delete t.delay)}}))}(o),a(e)},n.zoomPlugin=e}}function x(e){const t=e.$streaming;t.zoomPlugin&&(e.resetZoom=t.resetZoom,function(e){p.delete(e)}(e),delete t.resetZoom,delete t.zoomPlugin)}const b={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}},v=Object.keys(b);function I(e,o,n){if(n){if(n.length){const{lo:a,hi:i}=t._lookup(n,o);e[n[a]>=o?n[a]:n[i]]=!0}}else e[o]=!0}const D=["pointBackgroundColor","pointBorderColor","pointBorderWidth","pointRadius","pointRotation","pointStyle","pointHitRadius","pointHoverBackgroundColor","pointHoverBorderColor","pointHoverBorderWidth","pointHoverRadius","backgroundColor","borderColor","borderSkipped","borderWidth","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth","hoverRadius","hitRadius","radius","rotation"];function k(e,o,n){const a=e.$animations||{};t.each(e.$streaming,((i,s)=>{if(i.axisId===o){const o=i.reverse?-n:n,r=a[s];t.isFinite(e[s])&&(e[s]-=o),r&&(r._from-=o,r._to-=o)}}))}class w extends e.TimeScale{constructor(e){super(e),this.$realtime=this.$realtime||{}}init(e,o){const a=this;super.init(e,o),l(a.$realtime,(()=>{const e=a.chart,o=n(a,"onRefresh");return t.callback(o,[e],a),function(e){const{chart:o,id:a,max:i}=e,s=n(e,"duration"),r=n(e,"delay"),l=n(e,"ttl"),c=n(e,"pause"),d=Date.now()-(isNaN(l)?s+r:l);let u,m,p,f;t.each(o.data.datasets,((e,n)=>{const s=o.getDatasetMeta(n),r=a===s.xAxisID?"x":a===s.yAxisID&&"y";if(r){const a=s.controller,h=e.data,g=h.length;if(c){for(u=0;u{t.isArray(e[o])&&e[o].splice(m,p)})),t.each(e.datalabels,(e=>{t.isArray(e)&&e.splice(m,p)})),"object"!=typeof h[0]&&(f={start:m,count:p}),t.each(o._active,((e,t)=>{e.datasetIndex===n&&e.index>=m&&(e.index>=m+p?e.index-=p:o._active.splice(t,1))}),null,!0)}})),f&&o.data.labels.splice(f.start,f.count)}(a),e.update("quiet"),n(a,"refresh")}))}update(e,o,a){const i=this,{$realtime:r,options:l}=i,{bounds:c,offset:d,ticks:u}=l,{autoSkip:m,source:p,major:f}=u,h=f.enabled;n(i,"pause")?s(r):(r.frameRequestID||(r.head=Date.now()),function(e,o){if(!e.frameRequestID){const n=()=>{const a=e.nextRefresh||0,i=Date.now();if(a<=i){const n=t.callback(o),a=1e3/(Math.max(n,0)||30),s=e.nextRefresh+a||0;e.nextRefresh=s>i?s:i+a}e.frameRequestID=t.requestAnimFrame.call(window,n)};e.frameRequestID=t.requestAnimFrame.call(window,n)}}(r,(()=>{const e=i.chart,o=e.$streaming;return function(e){const{chart:o,id:a,$realtime:i}=e,s=n(e,"duration"),r=n(e,"delay"),l=e.isHorizontal(),c=l?e.width:e.height,d=Date.now(),u=o.tooltip,m=function(e){const t=e.$streaming.annotationPlugin;if(t){const o=t._getState(e);return o&&o.elements||[]}return[]}(o);let p=c*(d-i.head)/s;l===!!e.options.reverse&&(p=-p),t.each(o.data.datasets,((e,t)=>{const n=o.getDatasetMeta(t),{data:i=[],dataset:s}=n;for(let e=0,t=i.length;el.shift(),set:t.noop}),Object.defineProperty(e,"max",{get:()=>r.shift(),set:t.noop});const c=super.buildTicks();return delete e.min,delete e.max,e.min=s,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:n,ctx:a}=o,i=o.isHorizontal()?{left:e.left,top:0,right:e.right,bottom:n.height}:{left:0,top:e.top,right:n.width,bottom:e.bottom};o._gridLineItems=null,o._labelItems=null,t.clipArea(a,i),super.draw(e),t.unclipArea(a)}destroy(){const e=this.$realtime;s(e),r(e)}_generate(){const e=this,o=e._adapter,a=n(e,"duration"),i=n(e,"delay"),s=n(e,"refresh"),r=e.$realtime.head-i,l=r-a,c=e._getLabelCapacity(l),{time:d,ticks:u}=e.options,m=d.unit||function(e,t,o,n){const a=o-t,i=v.length;for(let t=v.indexOf(e);t1e5*f)throw new Error(l+" and "+r+" are too far apart with stepSize of "+f+" "+m);k=R,g&&p&&!y&&!d.round&&(k=+o.startOf(k,p),k=+o.add(k,~~((R-k)/(x.size*f))*f,m));const $="data"===u.source&&e.getDataTimestamps();for(w=0;ke-t)).map((e=>+e))}}w.id="realtime",w.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});e.defaults.set("transitions",{quiet:{animation:{duration:0}}});const R={x:["x","cp1x","cp2x"],y:["y","cp1y","cp2y"]};function $(o){const n=this;"quiet"===o&&t.each(n.data.datasets,((t,o)=>{n.getDatasetMeta(o).controller._setStyle=function(t,o,n,a){e.DatasetController.prototype._setStyle.call(this,t,o,"quiet",a)}})),e.Chart.prototype.update.call(n,o),"quiet"===o&&t.each(n.data.datasets,((e,t)=>{delete n.getDatasetMeta(t).controller._setStyle}))}function E(e){const t=e.$streaming;e.render(),t.lastMouseEvent&&setTimeout((()=>{const o=t.lastMouseEvent;o&&e._eventHandler(o)}),0)}const M=[{id:"streaming",version:"2.0.0",beforeInit(e){const o=e.$streaming=e.$streaming||{render:E},n=o.canvas=e.canvas,a=o.mouseEventListener=n=>{const a=t.getRelativePosition(n,e);o.lastMouseEvent={type:"mousemove",chart:e,native:n,x:a.x,y:a.y}};n.addEventListener("mousedown",a),n.addEventListener("mouseup",a)},afterInit(e){e.update=$},beforeUpdate(o){const{scales:n,elements:a}=o.options,i=o.tooltip;t.each(n,(({type:e})=>{"realtime"===e&&(a.line.capBezierPoints=!1)})),i&&(i.update=m);try{!function(e,t){const o=t.$streaming;if(o.annotationPlugin!==e){const t=e.afterUpdate;d(),o.annotationPlugin=e,e.afterUpdate=(e,o,n)=>{const a=o.mode,i=n.animation;"quiet"===a&&(n.animation=!1),t.call(this,e,o,n),"quiet"===a&&(n.animation=i)}}}(e.registry.getPlugin("annotation"),o)}catch(e){!function(e){delete e.$streaming.annotationPlugin}(o)}try{y(e.registry.getPlugin("zoom"),o)}catch(e){x(o)}},beforeDatasetUpdate(e,o){const{meta:n,mode:a}=o;if("quiet"===a){const{controller:e,$animations:o}=n;o&&o.visible&&o.visible._active&&(e.updateElement=t.noop,e.updateSharedOptions=t.noop)}},afterDatasetUpdate(e,t){const{meta:o,mode:n}=t,{data:i=[],dataset:s,controller:r}=o;for(let e=0,t=i.length;e{e instanceof w&&e.destroy()}))},defaults:{duration:1e4,delay:0,frameRate:30,refresh:1e3,onRefresh:null,pause:!1,ttl:void 0},descriptors:{_scriptable:e=>"onRefresh"!==e}},w];return e.Chart.register(M),M}));