| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import _ from 'lodash'; |
| import moment from 'moment'; |
| |
| /** |
| * @typedef {{x: number, y: {[key: string]: number}}} IgniteChartDataPoint |
| */ |
| |
| const RANGE_RATE_PRESET = [ |
| {label: '1 min', value: 1}, |
| {label: '5 min', value: 5}, |
| {label: '10 min', value: 10}, |
| {label: '15 min', value: 15}, |
| {label: '30 min', value: 30} |
| ]; |
| |
| /** |
| * Determines what label format was chosen by determineLabelFormat function |
| * in Chart.js streaming plugin. |
| * |
| * @param {string} label |
| */ |
| const inferLabelFormat = (label) => { |
| if (label.match(/\.\d{3} (am|pm)$/)) return 'MMM D, YYYY h:mm:ss.SSS a'; |
| if (label.match(/:\d{1,2} (am|pm)$/)) return 'MMM D, YYYY h:mm:ss a'; |
| if (label.match(/ \d{4}$/)) return 'MMM D, YYYY'; |
| }; |
| |
| export class IgniteChartController { |
| /** @type {import('chart.js').ChartConfiguration} */ |
| chartOptions; |
| /** @type {string} */ |
| chartTitle; |
| /** @type {IgniteChartDataPoint} */ |
| chartDataPoint; |
| /** @type {Array<IgniteChartDataPoint>} */ |
| chartHistory; |
| newPoints = []; |
| |
| static $inject = ['$element', 'IgniteChartColors', '$filter']; |
| |
| /** |
| * @param {JQLite} $element |
| * @param {Array<string>} IgniteChartColors |
| * @param {ng.IFilterService} $filter |
| */ |
| constructor($element, IgniteChartColors, $filter) { |
| this.$element = $element; |
| this.IgniteChartColors = IgniteChartColors; |
| |
| this.datePipe = $filter('date'); |
| this.ranges = RANGE_RATE_PRESET; |
| this.currentRange = this.ranges[0]; |
| this.maxRangeInMilliseconds = RANGE_RATE_PRESET[RANGE_RATE_PRESET.length - 1].value * 60 * 1000; |
| this.ctx = this.$element.find('canvas')[0].getContext('2d'); |
| |
| this.localHistory = []; |
| this.updateIsBusy = false; |
| } |
| |
| $onDestroy() { |
| if (this.chart) |
| this.chart.destroy(); |
| |
| this.$element = this.ctx = this.chart = null; |
| } |
| |
| $onInit() { |
| this.chartColors = _.get(this.chartOptions, 'chartColors', this.IgniteChartColors); |
| } |
| |
| _refresh() { |
| this.onRefresh(); |
| this.rerenderChart(); |
| } |
| |
| /** |
| * @param {{chartOptions: ng.IChangesObject<import('chart.js').ChartConfiguration>, chartTitle: ng.IChangesObject<string>, chartDataPoint: ng.IChangesObject<IgniteChartDataPoint>, chartHistory: ng.IChangesObject<Array<IgniteChartDataPoint>>}} changes |
| */ |
| async $onChanges(changes) { |
| if (this.chart && _.get(changes, 'refreshRate.currentValue')) |
| this.onRefreshRateChanged(_.get(changes, 'refreshRate.currentValue')); |
| |
| if ((changes.chartDataPoint && _.isNil(changes.chartDataPoint.currentValue)) || |
| (changes.chartHistory && _.isEmpty(changes.chartHistory.currentValue))) { |
| this.clearDatasets(); |
| this.localHistory = []; |
| |
| return; |
| } |
| |
| if (changes.chartHistory && changes.chartHistory.currentValue && changes.chartHistory.currentValue.length !== changes.chartHistory.previousValue.length) { |
| if (!this.chart) |
| await this.initChart(); |
| |
| this.clearDatasets(); |
| this.localHistory = [...changes.chartHistory.currentValue]; |
| |
| this.newPoints.splice(0, this.newPoints.length, ...changes.chartHistory.currentValue); |
| |
| this._refresh(); |
| |
| return; |
| } |
| |
| if (this.chartDataPoint && changes.chartDataPoint) { |
| if (!this.chart) |
| this.initChart(); |
| |
| this.newPoints.push(this.chartDataPoint); |
| this.localHistory.push(this.chartDataPoint); |
| |
| this._refresh(); |
| } |
| } |
| |
| async initChart() { |
| /** @type {import('chart.js').ChartConfiguration} */ |
| this.config = { |
| type: 'LineWithVerticalCursor', |
| data: { |
| datasets: [] |
| }, |
| options: { |
| elements: { |
| line: { |
| tension: 0 |
| }, |
| point: { |
| radius: 2, |
| pointStyle: 'rectRounded' |
| } |
| }, |
| animation: { |
| duration: 0 // general animation time |
| }, |
| hover: { |
| animationDuration: 0 // duration of animations when hovering an item |
| }, |
| responsiveAnimationDuration: 0, // animation duration after a resize |
| maintainAspectRatio: false, |
| responsive: true, |
| legend: { |
| display: false |
| }, |
| scales: { |
| xAxes: [{ |
| type: 'realtime', |
| display: true, |
| time: { |
| displayFormats: { |
| second: 'HH:mm:ss', |
| minute: 'HH:mm:ss', |
| hour: 'HH:mm:ss' |
| } |
| }, |
| ticks: { |
| maxRotation: 0, |
| minRotation: 0 |
| } |
| }], |
| yAxes: [{ |
| type: 'linear', |
| display: true, |
| ticks: { |
| min: 0, |
| beginAtZero: true, |
| maxTicksLimit: 4, |
| callback: (value, index, labels) => { |
| if (value === 0) |
| return 0; |
| |
| if (_.max(labels) <= 4000 && value <= 4000) |
| return value; |
| |
| if (_.max(labels) <= 1000000 && value <= 1000000) |
| return `${value / 1000}K`; |
| |
| if ((_.max(labels) <= 4000000 && value >= 500000) || (_.max(labels) > 4000000)) |
| return `${value / 1000000}M`; |
| |
| return value; |
| } |
| } |
| }] |
| }, |
| tooltips: { |
| mode: 'index', |
| position: 'yCenter', |
| intersect: false, |
| yAlign: 'center', |
| xPadding: 20, |
| yPadding: 20, |
| bodyFontSize: 13, |
| callbacks: { |
| title: (tooltipItem) => { |
| return tooltipItem[0].xLabel = moment(tooltipItem[0].xLabel, inferLabelFormat(tooltipItem[0].xLabel)).format('HH:mm:ss'); |
| }, |
| label: (tooltipItem, data) => { |
| const label = data.datasets[tooltipItem.datasetIndex].label || ''; |
| |
| return `${_.startCase(label)}: ${tooltipItem.yLabel} per sec`; |
| }, |
| labelColor: (tooltipItem) => { |
| return { |
| borderColor: 'rgba(255,255,255,0.5)', |
| borderWidth: 0, |
| boxShadow: 'none', |
| backgroundColor: this.chartColors[tooltipItem.datasetIndex] |
| }; |
| } |
| } |
| }, |
| plugins: { |
| streaming: { |
| duration: this.currentRange.value * 1000 * 60, |
| frameRate: 1000 / this.refreshRate || 1 / 3, |
| refresh: this.refreshRate || 3000, |
| // Temporary workaround before https://github.com/nagix/chartjs-plugin-streaming/issues/53 resolved. |
| // ttl: this.maxRangeInMilliseconds, |
| onRefresh: () => { |
| this.onRefresh(); |
| } |
| } |
| } |
| } |
| }; |
| |
| this.config = _.merge(this.config, this.chartOptions); |
| |
| const chartModule = await import('chart.js'); |
| const Chart = chartModule.default; |
| |
| Chart.Tooltip.positioners.yCenter = (elements) => { |
| const chartHeight = elements[0]._chart.height; |
| const tooltipHeight = 60; |
| |
| return {x: elements[0].getCenterPoint().x, y: Math.floor(chartHeight / 2) - Math.floor(tooltipHeight / 2) }; |
| }; |
| |
| |
| // Drawing vertical cursor |
| Chart.defaults.LineWithVerticalCursor = Chart.defaults.line; |
| Chart.controllers.LineWithVerticalCursor = Chart.controllers.line.extend({ |
| draw(ease) { |
| Chart.controllers.line.prototype.draw.call(this, ease); |
| |
| if (this.chart.tooltip._active && this.chart.tooltip._active.length) { |
| const activePoint = this.chart.tooltip._active[0]; |
| const ctx = this.chart.ctx; |
| const x = activePoint.tooltipPosition().x; |
| const topY = this.chart.scales['y-axis-0'].top; |
| const bottomY = this.chart.scales['y-axis-0'].bottom; |
| |
| // draw line |
| ctx.save(); |
| ctx.beginPath(); |
| ctx.moveTo(x, topY); |
| ctx.lineTo(x, bottomY); |
| ctx.lineWidth = 0.5; |
| ctx.strokeStyle = '#0080ff'; |
| ctx.stroke(); |
| ctx.restore(); |
| } |
| } |
| }); |
| |
| await import('chartjs-plugin-streaming'); |
| |
| this.chart = new Chart(this.ctx, this.config); |
| this.changeXRange(this.currentRange); |
| } |
| |
| onRefresh() { |
| this.newPoints.forEach((point) => { |
| this.appendChartPoint(point); |
| }); |
| |
| this.newPoints.splice(0, this.newPoints.length); |
| } |
| |
| /** |
| * @param {IgniteChartDataPoint} dataPoint |
| */ |
| appendChartPoint(dataPoint) { |
| Object.keys(dataPoint.y).forEach((key) => { |
| if (this.checkDatasetCanBeAdded(key)) { |
| let datasetIndex = this.findDatasetIndex(key); |
| |
| if (datasetIndex < 0) { |
| datasetIndex = this.config.data.datasets.length; |
| this.addDataset(key); |
| } |
| |
| this.config.data.datasets[datasetIndex].data.push({x: dataPoint.x, y: dataPoint.y[key]}); |
| this.config.data.datasets[datasetIndex].borderColor = this.chartColors[datasetIndex]; |
| this.config.data.datasets[datasetIndex].borderWidth = 2; |
| this.config.data.datasets[datasetIndex].fill = false; |
| } |
| }); |
| |
| // Temporary workaround before https://github.com/nagix/chartjs-plugin-streaming/issues/53 resolved. |
| this.pruneHistory(); |
| } |
| |
| // Temporary workaround before https://github.com/nagix/chartjs-plugin-streaming/issues/53 resolved. |
| pruneHistory() { |
| if (!this.xRangeUpdateInProgress) { |
| const currenTime = Date.now(); |
| |
| while (currenTime - this.localHistory[0].x > this.maxRangeInMilliseconds) |
| this.localHistory.shift(); |
| |
| this.config.data.datasets.forEach((dataset) => { |
| while (currenTime - dataset.data[0].x > this.maxRangeInMilliseconds) |
| dataset.data.shift(); |
| }); |
| } |
| } |
| |
| /** |
| * Checks if a key of dataset can be added to chart or should be ignored. |
| * @param dataPointKey {String} |
| * @return {Boolean} |
| */ |
| checkDatasetCanBeAdded(dataPointKey) { |
| // If datasetLegendMapping is empty all keys are allowed. |
| if (!this.config.datasetLegendMapping) |
| return true; |
| |
| return Object.keys(this.config.datasetLegendMapping).includes(dataPointKey); |
| } |
| |
| clearDatasets() { |
| if (!_.isNil(this.config)) |
| this.config.data.datasets.forEach((dataset) => dataset.data = []); |
| } |
| |
| addDataset(datasetName) { |
| if (this.findDatasetIndex(datasetName) >= 0) |
| throw new Error(`Dataset with name ${datasetName} is already in chart`); |
| else { |
| const datasetIsHidden = _.isNil(this.config.datasetLegendMapping[datasetName].hidden) |
| ? false |
| : this.config.datasetLegendMapping[datasetName].hidden; |
| |
| this.config.data.datasets.push({ label: datasetName, data: [], hidden: datasetIsHidden }); |
| } |
| } |
| |
| findDatasetIndex(searchedDatasetLabel) { |
| return this.config.data.datasets.findIndex((dataset) => dataset.label === searchedDatasetLabel); |
| } |
| |
| changeXRange(range) { |
| if (this.chart) { |
| this.xRangeUpdateInProgress = true; |
| |
| this.chart.config.options.plugins.streaming.duration = range.value * 60 * 1000; |
| |
| this.clearDatasets(); |
| this.newPoints.splice(0, this.newPoints.length, ...this.localHistory); |
| |
| this.onRefresh(); |
| this.rerenderChart(); |
| |
| this.xRangeUpdateInProgress = false; |
| } |
| } |
| |
| onRefreshRateChanged(refreshRate) { |
| this.chart.config.options.plugins.streaming.frameRate = 1000 / refreshRate; |
| this.chart.config.options.plugins.streaming.refresh = refreshRate; |
| this.rerenderChart(); |
| } |
| |
| rerenderChart() { |
| if (this.chart) |
| this.chart.update(); |
| } |
| } |