ngx-hana-nameserver-history-viewer 21.0.0 → 21.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/ngx-hana-nameserver-history-viewer.mjs +217 -1247
- package/fesm2022/ngx-hana-nameserver-history-viewer.mjs.map +1 -1
- package/ngx-hana-nameserver-history-viewer-21.1.0.tgz +0 -0
- package/package.json +5 -4
- package/types/ngx-hana-nameserver-history-viewer.d.ts +13 -2
- package/ngx-hana-nameserver-history-viewer-21.0.0.tgz +0 -0
|
@@ -2,11 +2,11 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { isDevMode, Injectable, EventEmitter, Output, Input, Component, ChangeDetectionStrategy, ElementRef, ViewChild } from '@angular/core';
|
|
3
3
|
import { SelectionModel } from '@angular/cdk/collections';
|
|
4
4
|
import moment from 'moment-timezone';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import { Chart, registerables, TimeScale, LinearScale, CategoryScale, PointElement, LineElement, Title, Tooltip, Legend, Filler, Decimation } from 'chart.js';
|
|
6
|
+
import 'chartjs-adapter-moment';
|
|
7
|
+
import zoomPlugin from 'chartjs-plugin-zoom';
|
|
7
8
|
import { parse } from 'papaparse';
|
|
8
|
-
import
|
|
9
|
-
import moment$1 from 'moment';
|
|
9
|
+
import 'hammerjs';
|
|
10
10
|
import * as i2 from 'nshviewer-angular-datetime-picker';
|
|
11
11
|
import { OwlDateTimeModule, OwlNativeDateTimeModule } from 'nshviewer-angular-datetime-picker';
|
|
12
12
|
import { SelectionTableComponent } from 'ngx-selection-table';
|
|
@@ -1094,8 +1094,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
|
|
|
1094
1094
|
type: Injectable
|
|
1095
1095
|
}] });
|
|
1096
1096
|
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1097
|
+
// Register Chart.js components and plugins
|
|
1098
|
+
Chart.register(...registerables, TimeScale, LinearScale, CategoryScale, PointElement, LineElement, Title, Tooltip, Legend, Filler, Decimation, zoomPlugin);
|
|
1099
|
+
const MAX_POINTS_TO_RENDER = 100;
|
|
1099
1100
|
class ChartService {
|
|
1100
1101
|
/**
|
|
1101
1102
|
* toggle the dataset status of the chart, synchronously
|
|
@@ -1131,21 +1132,13 @@ class ChartService {
|
|
|
1131
1132
|
*/
|
|
1132
1133
|
static _initConfig(chart) {
|
|
1133
1134
|
// init data sets
|
|
1134
|
-
if (chart.data
|
|
1135
|
+
if (chart.data?.datasets) {
|
|
1135
1136
|
chart.data.datasets.length = 0;
|
|
1136
1137
|
chart.data.datasets = void 0;
|
|
1137
1138
|
chart.data = void 0;
|
|
1138
1139
|
}
|
|
1139
1140
|
// init options
|
|
1140
|
-
if (chart.options
|
|
1141
|
-
if (chart.options.scales.xAxes) {
|
|
1142
|
-
chart.options.scales.xAxes.length = 0;
|
|
1143
|
-
chart.options.scales.xAxes = void 0;
|
|
1144
|
-
}
|
|
1145
|
-
if (chart.options.scales.yAxes) {
|
|
1146
|
-
chart.options.scales.yAxes.length = 0;
|
|
1147
|
-
chart.options.scales.yAxes = void 0;
|
|
1148
|
-
}
|
|
1141
|
+
if (chart.options?.scales) {
|
|
1149
1142
|
chart.options.scales = void 0;
|
|
1150
1143
|
chart.options = void 0;
|
|
1151
1144
|
}
|
|
@@ -1161,62 +1154,70 @@ class ChartService {
|
|
|
1161
1154
|
return {
|
|
1162
1155
|
borderColor: color,
|
|
1163
1156
|
backgroundColor: color,
|
|
1164
|
-
borderWidth: 1
|
|
1157
|
+
borderWidth: 1,
|
|
1165
1158
|
spanGaps: false,
|
|
1166
1159
|
label: header[i],
|
|
1167
1160
|
data: data[i],
|
|
1168
1161
|
fill: false,
|
|
1169
1162
|
yAxisID: `y-axis-${i}`,
|
|
1170
|
-
hidden: !defaultItems.includes(header[i])
|
|
1163
|
+
hidden: !defaultItems.includes(header[i]),
|
|
1164
|
+
pointRadius: 0,
|
|
1165
|
+
pointHitRadius: 5,
|
|
1166
|
+
pointHoverRadius: 5,
|
|
1167
|
+
// Enable smooth curves for more natural line appearance
|
|
1168
|
+
tension: 0.2,
|
|
1169
|
+
// Enable decimation for each dataset
|
|
1170
|
+
parsing: false,
|
|
1171
|
+
normalized: true
|
|
1171
1172
|
};
|
|
1172
1173
|
});
|
|
1173
1174
|
}
|
|
1174
|
-
static
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
{
|
|
1175
|
+
static _generateScales(timeArray, data, yScaleArray) {
|
|
1176
|
+
const scales = {
|
|
1177
|
+
x: {
|
|
1178
1178
|
type: 'time',
|
|
1179
1179
|
time: {
|
|
1180
|
-
// unit: 'second',
|
|
1181
|
-
// stepSize: 1,
|
|
1182
1180
|
displayFormats: {
|
|
1183
|
-
'millisecond':
|
|
1184
|
-
'second':
|
|
1185
|
-
'minute':
|
|
1181
|
+
'millisecond': 'MMMDD HH:mm',
|
|
1182
|
+
'second': 'MMMDD HH:mm',
|
|
1183
|
+
'minute': 'MMMDD HH:mm',
|
|
1186
1184
|
'hour': 'MMMDD HH',
|
|
1187
|
-
'day': 'MMMDD
|
|
1188
|
-
'week':
|
|
1189
|
-
'month':
|
|
1190
|
-
'quarter':
|
|
1191
|
-
'year':
|
|
1185
|
+
'day': 'MMMDD',
|
|
1186
|
+
'week': 'MMMDD',
|
|
1187
|
+
'month': 'MMM YYYY',
|
|
1188
|
+
'quarter': 'MMM YYYY',
|
|
1189
|
+
'year': 'MMM YYYY',
|
|
1192
1190
|
},
|
|
1193
|
-
tooltipFormat: 'll HH:mm:ss'
|
|
1194
|
-
min: time[0],
|
|
1195
|
-
max: time[time.length - 1]
|
|
1191
|
+
tooltipFormat: 'll HH:mm:ss'
|
|
1196
1192
|
},
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
// labelString: 'Date'
|
|
1200
|
-
// },
|
|
1193
|
+
min: timeArray[0],
|
|
1194
|
+
max: timeArray[timeArray.length - 1],
|
|
1201
1195
|
ticks: {
|
|
1196
|
+
source: 'auto',
|
|
1202
1197
|
maxRotation: 0,
|
|
1203
|
-
|
|
1204
|
-
|
|
1198
|
+
autoSkip: true,
|
|
1199
|
+
// callback: function(val, index) {
|
|
1200
|
+
// // Hide every 2nd tick label
|
|
1201
|
+
// return index % 2 === 0? this.getLabelForValue(val) : '';
|
|
1202
|
+
// },
|
|
1203
|
+
},
|
|
1204
|
+
reverse: false
|
|
1205
1205
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1206
|
+
};
|
|
1207
|
+
// Add Y axes
|
|
1208
|
+
data.forEach((item, i) => {
|
|
1209
|
+
scales[`y-axis-${i}`] = {
|
|
1210
|
+
type: 'linear',
|
|
1211
|
+
display: false,
|
|
1212
|
+
position: 'left',
|
|
1213
|
+
grid: {
|
|
1214
|
+
drawOnChartArea: i === 0, // only show grid for first axis
|
|
1215
|
+
},
|
|
1216
1216
|
min: 0,
|
|
1217
|
-
max:
|
|
1218
|
-
}
|
|
1219
|
-
})
|
|
1217
|
+
max: yScaleArray[i]
|
|
1218
|
+
};
|
|
1219
|
+
});
|
|
1220
|
+
return scales;
|
|
1220
1221
|
}
|
|
1221
1222
|
/**
|
|
1222
1223
|
* generate configuration for chart
|
|
@@ -1228,70 +1229,117 @@ class ChartService {
|
|
|
1228
1229
|
datasets: ChartService._generateDataSets(data, header, headerKey, defaultItems)
|
|
1229
1230
|
},
|
|
1230
1231
|
options: {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
duration: 0, // general animation time
|
|
1235
|
-
},
|
|
1236
|
-
hover: {
|
|
1237
|
-
animationDuration: 0, // duration of animations when hovering an item
|
|
1238
|
-
onHover: function (e) {
|
|
1239
|
-
const point = this.getElementAtEvent(e);
|
|
1240
|
-
if (point.length) {
|
|
1241
|
-
e.target.style.cursor = 'pointer';
|
|
1242
|
-
}
|
|
1243
|
-
else {
|
|
1244
|
-
e.target.style.cursor = 'default';
|
|
1245
|
-
}
|
|
1232
|
+
layout: {
|
|
1233
|
+
padding: {
|
|
1234
|
+
right: 25 // Add padding to the right side of the chart
|
|
1246
1235
|
}
|
|
1247
1236
|
},
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
e.target.style.cursor = 'pointer';
|
|
1253
|
-
},
|
|
1254
|
-
labels: {
|
|
1255
|
-
filter: (legendItem) => selection.isSelected(tableSource.find(row => legendItem.text === row.KPI))
|
|
1256
|
-
}
|
|
1237
|
+
interaction: {
|
|
1238
|
+
intersect: true, // true means not a range hit
|
|
1239
|
+
axis: 'x',
|
|
1240
|
+
mode: 'nearest'
|
|
1257
1241
|
},
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
minNumPoints: 200,
|
|
1266
|
-
maxNumPointsToDraw: 100
|
|
1242
|
+
responsive: true,
|
|
1243
|
+
animation: false, // must disable it otherwise zoomin will hang
|
|
1244
|
+
onHover: (event, elements) => {
|
|
1245
|
+
const target = event.native?.target || event.target;
|
|
1246
|
+
if (target) {
|
|
1247
|
+
target.style.cursor = elements?.length > 0 ? 'pointer' : 'default';
|
|
1248
|
+
}
|
|
1267
1249
|
},
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1250
|
+
elements: { point: { radius: 0, hitRadius: 5, hoverRadius: 5 } }, // set radius to 0 not to display point
|
|
1251
|
+
parsing: false,
|
|
1252
|
+
normalized: true,
|
|
1253
|
+
datasets: { line: { pointRadius: 0, pointHitRadius: 5, pointHoverRadius: 5 } },
|
|
1254
|
+
plugins: {
|
|
1255
|
+
legend: {
|
|
1256
|
+
position: 'bottom',
|
|
1257
|
+
onHover: (event, legendItem) => {
|
|
1258
|
+
const target = event.native?.target || event.target;
|
|
1259
|
+
if (target) {
|
|
1260
|
+
target.style.cursor = 'pointer';
|
|
1278
1261
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1262
|
+
},
|
|
1263
|
+
labels: {
|
|
1264
|
+
filter: (legendItem) => selection.isSelected(tableSource.find(row => legendItem.text === row.KPI))
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
title: {
|
|
1268
|
+
display: true,
|
|
1269
|
+
text: title
|
|
1270
|
+
},
|
|
1271
|
+
tooltip: {
|
|
1272
|
+
callbacks: {
|
|
1273
|
+
label: function (context) {
|
|
1274
|
+
// format numbers with commas and add unit information
|
|
1275
|
+
const label = context.dataset.label || '';
|
|
1276
|
+
if (label) {
|
|
1277
|
+
// get unit (eg: MB, GB, MB/s, % and so on)
|
|
1278
|
+
const rowItem = tableSource.find(controlTableRow => label === controlTableRow.KPI) || '';
|
|
1279
|
+
const unit = rowItem[Item.unit] && rowItem[Item.unit] !== Unit.PCT ? ` ${rowItem[Item.unit]}` : rowItem[Item.unit] || '';
|
|
1280
|
+
return `${label}: ${getNumberWithCommas(context.parsed.y)}${unit}`;
|
|
1281
|
+
}
|
|
1282
|
+
else {
|
|
1283
|
+
return getNumberWithCommas(context.parsed.y);
|
|
1284
|
+
}
|
|
1281
1285
|
}
|
|
1282
1286
|
}
|
|
1287
|
+
},
|
|
1288
|
+
// downsampling
|
|
1289
|
+
decimation: {
|
|
1290
|
+
enabled: true,
|
|
1291
|
+
algorithm: 'lttb', // Largest-Triangle-Three-Buckets algorithm
|
|
1292
|
+
samples: 500, // Increased samples for better quality
|
|
1293
|
+
threshold: 1 // increase it may hang the ui due to swith of using/not using downsampling, disable it (set it to 0), will slowdown average zoom in.
|
|
1294
|
+
},
|
|
1295
|
+
zoom: {
|
|
1296
|
+
limits: {
|
|
1297
|
+
x: {
|
|
1298
|
+
min: 'original',
|
|
1299
|
+
max: 'original',
|
|
1300
|
+
//minRange: 180000 // 3 min the min range that can be zoomed
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
pan: {
|
|
1304
|
+
enabled: true,
|
|
1305
|
+
mode: 'x',
|
|
1306
|
+
modifierKey: 'ctrl'
|
|
1307
|
+
},
|
|
1308
|
+
zoom: {
|
|
1309
|
+
// wheel: {
|
|
1310
|
+
// enabled: true,
|
|
1311
|
+
// speed: 0.02
|
|
1312
|
+
// },
|
|
1313
|
+
// pinch: {
|
|
1314
|
+
// enabled: true
|
|
1315
|
+
// },
|
|
1316
|
+
drag: {
|
|
1317
|
+
enabled: true,
|
|
1318
|
+
threshold: 10,
|
|
1319
|
+
drawTime: 'beforeDraw'
|
|
1320
|
+
},
|
|
1321
|
+
mode: 'x',
|
|
1322
|
+
onZoom: zoomCallback,
|
|
1323
|
+
onZoomComplete: (context => {
|
|
1324
|
+
const chart = context?.chart;
|
|
1325
|
+
if (!chart?.data?.datasets) {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
// make point visible when current points are less than MAX_POINTS_TO_RENDER
|
|
1329
|
+
const pointRadiusChangeFlag = chart.data.datasets.some(dataset => dataset.data.length <= MAX_POINTS_TO_RENDER);
|
|
1330
|
+
const pointRadius = pointRadiusChangeFlag ? 2 : 0;
|
|
1331
|
+
chart.data.datasets.forEach((dataset) => {
|
|
1332
|
+
dataset.pointRadius = pointRadius;
|
|
1333
|
+
});
|
|
1334
|
+
if (chart.options?.elements?.point) {
|
|
1335
|
+
chart.options.elements.point.radius = pointRadius;
|
|
1336
|
+
}
|
|
1337
|
+
chart.update();
|
|
1338
|
+
})
|
|
1339
|
+
}
|
|
1283
1340
|
}
|
|
1284
1341
|
},
|
|
1285
|
-
scales:
|
|
1286
|
-
xAxes: ChartService._generateXAxes(time),
|
|
1287
|
-
yAxes: ChartService._generateYAxes(data, yScale)
|
|
1288
|
-
},
|
|
1289
|
-
zoom: {
|
|
1290
|
-
enabled: true,
|
|
1291
|
-
drag: true,
|
|
1292
|
-
mode: 'x',
|
|
1293
|
-
onZoom: zoomCallback
|
|
1294
|
-
}
|
|
1342
|
+
scales: ChartService._generateScales(time, data, yScale)
|
|
1295
1343
|
}
|
|
1296
1344
|
};
|
|
1297
1345
|
}
|
|
@@ -1301,12 +1349,65 @@ class ChartService {
|
|
|
1301
1349
|
}
|
|
1302
1350
|
return -1;
|
|
1303
1351
|
}
|
|
1352
|
+
/**
|
|
1353
|
+
* WORKAROUND: Setup mousemove event listener to fix Chart.js tooltip issue
|
|
1354
|
+
* Issue: https://github.com/chartjs/Chart.js/issues/11972
|
|
1355
|
+
* TODO: Remove this entire method when the Chart.js bug is fixed
|
|
1356
|
+
*/
|
|
1357
|
+
_setupTooltipWorkaround(chart) {
|
|
1358
|
+
const canvas = chart.canvas;
|
|
1359
|
+
const mouseMoveHandler = (event) => {
|
|
1360
|
+
const rect = canvas.getBoundingClientRect();
|
|
1361
|
+
const x = event.clientX - rect.left;
|
|
1362
|
+
const y = event.clientY - rect.top;
|
|
1363
|
+
// Check if mouse is within chart area
|
|
1364
|
+
const chartArea = chart.chartArea;
|
|
1365
|
+
if (chartArea) {
|
|
1366
|
+
const isInChartArea = x >= chartArea.left && x <= chartArea.right &&
|
|
1367
|
+
y <= chartArea.bottom && y >= chartArea.top;
|
|
1368
|
+
// Get legend area (legend is positioned at bottom)
|
|
1369
|
+
const legendArea = chart.legend;
|
|
1370
|
+
let isInLegendArea = false;
|
|
1371
|
+
if (legendArea && legendArea.legendHitBoxes) {
|
|
1372
|
+
isInLegendArea = legendArea.legendHitBoxes.some((hitBox) => x >= hitBox.left && x <= hitBox.left + hitBox.width &&
|
|
1373
|
+
y >= hitBox.top && y <= hitBox.top + hitBox.height);
|
|
1374
|
+
}
|
|
1375
|
+
if (!isInChartArea && !isInLegendArea) {
|
|
1376
|
+
// Mouse is in axis area (but not legend), force hide tooltip and active points
|
|
1377
|
+
if (chart.tooltip) {
|
|
1378
|
+
chart.tooltip.setActiveElements([], { x: 0, y: 0 });
|
|
1379
|
+
}
|
|
1380
|
+
// Clear active elements to hide hover points
|
|
1381
|
+
chart.setActiveElements([]);
|
|
1382
|
+
chart.update('none');
|
|
1383
|
+
canvas.style.cursor = 'default';
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
canvas.addEventListener('mousemove', mouseMoveHandler);
|
|
1388
|
+
// Store the handler for cleanup
|
|
1389
|
+
chart._mouseMoveHandler = mouseMoveHandler;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* WORKAROUND: Clean up mousemove event listener for Chart.js tooltip issue
|
|
1393
|
+
* Issue: https://github.com/chartjs/Chart.js/issues/11972
|
|
1394
|
+
* TODO: Remove this entire method when the Chart.js bug is fixed
|
|
1395
|
+
*/
|
|
1396
|
+
_cleanupTooltipWorkaround(chart) {
|
|
1397
|
+
const canvas = chart.canvas;
|
|
1398
|
+
const mouseMoveHandler = chart._mouseMoveHandler;
|
|
1399
|
+
if (canvas && mouseMoveHandler) {
|
|
1400
|
+
canvas.removeEventListener('mousemove', mouseMoveHandler);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1304
1403
|
/**
|
|
1305
1404
|
* destroy the chart, release the resource of the chart
|
|
1306
1405
|
*/
|
|
1307
1406
|
_destroyChart(chart) {
|
|
1308
1407
|
return new Promise((resolve) => {
|
|
1309
1408
|
if (chart) {
|
|
1409
|
+
// WORKAROUND: Clean up tooltip workaround - TODO: Remove when Chart.js bug is fixed
|
|
1410
|
+
this._cleanupTooltipWorkaround(chart);
|
|
1310
1411
|
chart.destroy();
|
|
1311
1412
|
}
|
|
1312
1413
|
resolve();
|
|
@@ -1344,7 +1445,10 @@ class ChartService {
|
|
|
1344
1445
|
}
|
|
1345
1446
|
});
|
|
1346
1447
|
}
|
|
1347
|
-
|
|
1448
|
+
// Use the zoom plugin's resetZoom method
|
|
1449
|
+
if (chart.resetZoom) {
|
|
1450
|
+
chart.resetZoom();
|
|
1451
|
+
}
|
|
1348
1452
|
resolve(datasetStatusBeforeReset);
|
|
1349
1453
|
}
|
|
1350
1454
|
else {
|
|
@@ -1391,8 +1495,9 @@ class ChartService {
|
|
|
1391
1495
|
return new Promise((resolve, reject) => {
|
|
1392
1496
|
const ctx = document.getElementById('chartNameServerHistory')?.getContext('2d');
|
|
1393
1497
|
if (ctx) {
|
|
1394
|
-
|
|
1395
|
-
|
|
1498
|
+
this._chart = new Chart(ctx, ChartService._generateChartConfig(time, data, yScale, header, headerKey, selection, tableSource, title, defaultItems, zoomCB));
|
|
1499
|
+
// WORKAROUND: Setup tooltip workaround - TODO: Remove when Chart.js bug is fixed
|
|
1500
|
+
this._setupTooltipWorkaround(this._chart);
|
|
1396
1501
|
resolve();
|
|
1397
1502
|
}
|
|
1398
1503
|
else {
|
|
@@ -1801,1141 +1906,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
|
|
|
1801
1906
|
type: Injectable
|
|
1802
1907
|
}] });
|
|
1803
1908
|
|
|
1804
|
-
/**
|
|
1805
|
-
* This plugin is adapted from: https://github.com/3dcl/chartjs-plugin-responsive-downsample
|
|
1806
|
-
* for controlling the radius of point.
|
|
1807
|
-
*
|
|
1808
|
-
* MIT License
|
|
1809
|
-
* Copyright (c) 2018 3D Content Logistics
|
|
1810
|
-
*
|
|
1811
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
1812
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
1813
|
-
* in the Software without restriction, including without limitation the rights
|
|
1814
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
1815
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
1816
|
-
* furnished to do so, subject to the following conditions:
|
|
1817
|
-
* The above copyright notice and this permission notice shall be included in all
|
|
1818
|
-
* copies or substantial portions of the Software.
|
|
1819
|
-
*
|
|
1820
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
1821
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
1822
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
1823
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
1824
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
1825
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
1826
|
-
* SOFTWARE.
|
|
1827
|
-
*/
|
|
1828
|
-
/**
|
|
1829
|
-
* Check if a value is null or undefined
|
|
1830
|
-
*/
|
|
1831
|
-
function isNil(value) {
|
|
1832
|
-
return (typeof value === 'undefined') || value === null;
|
|
1833
|
-
}
|
|
1834
|
-
/**
|
|
1835
|
-
* Clamp a number to a range
|
|
1836
|
-
* @param value
|
|
1837
|
-
* @param min
|
|
1838
|
-
* @param max
|
|
1839
|
-
*/
|
|
1840
|
-
function clamp(value, min, max) {
|
|
1841
|
-
return Math.min(Math.max(value, min), max);
|
|
1842
|
-
}
|
|
1843
|
-
/**
|
|
1844
|
-
* Recursively assign default values to an object if object is missing the keys.
|
|
1845
|
-
* @param object The destination object to assign default values to
|
|
1846
|
-
* @param defaults The default values for the object
|
|
1847
|
-
* @return The destination object
|
|
1848
|
-
*/
|
|
1849
|
-
function defaultsDeep(object, defaults) {
|
|
1850
|
-
for (const key in defaults) {
|
|
1851
|
-
if (defaults.hasOwnProperty(key)) {
|
|
1852
|
-
const value = object[key];
|
|
1853
|
-
if (typeof value === 'undefined') {
|
|
1854
|
-
object[key] = defaults[key];
|
|
1855
|
-
}
|
|
1856
|
-
else if (value !== null && typeof value === 'object') {
|
|
1857
|
-
object[key] = defaultsDeep(value, defaults[key]);
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
return object;
|
|
1862
|
-
}
|
|
1863
|
-
/**
|
|
1864
|
-
* Finds the first element in an array for that the comaperator functions returns true
|
|
1865
|
-
*
|
|
1866
|
-
* @export
|
|
1867
|
-
* @param array An array
|
|
1868
|
-
* @param compareFunction Comperator function returning true for the element seeked
|
|
1869
|
-
* @returns The found element or undefined
|
|
1870
|
-
*/
|
|
1871
|
-
function findInArray(array, compareFunction) {
|
|
1872
|
-
if (isNil(array)) {
|
|
1873
|
-
return undefined;
|
|
1874
|
-
}
|
|
1875
|
-
for (let i = 0; i < array.length; i++) {
|
|
1876
|
-
if (compareFunction(array[i]) === true) {
|
|
1877
|
-
return array[i];
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
return undefined;
|
|
1881
|
-
}
|
|
1882
|
-
/**
|
|
1883
|
-
* Finds the first index in an array for that the comaperator function for an element returns true
|
|
1884
|
-
*
|
|
1885
|
-
* @export
|
|
1886
|
-
* @param array An array of elements
|
|
1887
|
-
* @param compareFunction Comperator function returning true for the element seeked
|
|
1888
|
-
* @returns Index of the matched element or -1 if no element was found
|
|
1889
|
-
*/
|
|
1890
|
-
function findIndexInArray(array, compareFunction) {
|
|
1891
|
-
if (isNil(array)) {
|
|
1892
|
-
return undefined;
|
|
1893
|
-
}
|
|
1894
|
-
for (let i = 0; i < array.length; i++) {
|
|
1895
|
-
if (compareFunction(array[i]) === true) {
|
|
1896
|
-
return i;
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
return -1;
|
|
1900
|
-
}
|
|
1901
|
-
/**
|
|
1902
|
-
* If points number is less than minNumPoints, show radius for all points.
|
|
1903
|
-
*/
|
|
1904
|
-
function changePointRadius(chart, options) {
|
|
1905
|
-
if (chart && chart.data && chart.data.datasets && options) {
|
|
1906
|
-
let pointRadiusChangeFlag = false;
|
|
1907
|
-
const maxNumPointsToDraw = options.maxNumPointsToDraw == null ? 100 : options.maxNumPointsToDraw;
|
|
1908
|
-
for (const dataset of chart.data.datasets) {
|
|
1909
|
-
if (dataset.data.length < maxNumPointsToDraw) {
|
|
1910
|
-
pointRadiusChangeFlag = true;
|
|
1911
|
-
break;
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
chart.options.elements.point.radius = pointRadiusChangeFlag ? 2 : 0;
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
/**
|
|
1919
|
-
* A mipmap data structure for line chart data. Uses averages to downsample data.
|
|
1920
|
-
*/
|
|
1921
|
-
class DataMipmap {
|
|
1922
|
-
/**
|
|
1923
|
-
* Create a data mipmap
|
|
1924
|
-
* @param data The orignal line chart data
|
|
1925
|
-
* @param minNumPoints Minimal number of points on lowest mipmap level(limits number of levels)
|
|
1926
|
-
*/
|
|
1927
|
-
constructor(data, minNumPoints = 2) {
|
|
1928
|
-
this.minNumPoints = minNumPoints;
|
|
1929
|
-
this.setData(data);
|
|
1930
|
-
}
|
|
1931
|
-
/**
|
|
1932
|
-
* Set the line chart data and update mipmap level.
|
|
1933
|
-
* @param data The orignal line chart data
|
|
1934
|
-
*/
|
|
1935
|
-
setData(data) {
|
|
1936
|
-
this.originalData = data || [];
|
|
1937
|
-
this.mipMaps = [];
|
|
1938
|
-
this.resolution = this.computeResolution(this.originalData);
|
|
1939
|
-
this.createMipMap();
|
|
1940
|
-
}
|
|
1941
|
-
/**
|
|
1942
|
-
* Set the minimal number of points
|
|
1943
|
-
* @param minNumPoints Minimal number of points on lowest mipmap level(limits number of levels)
|
|
1944
|
-
*/
|
|
1945
|
-
setMinNumPoints(minNumPoints) {
|
|
1946
|
-
this.minNumPoints = minNumPoints;
|
|
1947
|
-
this.mipMaps = [];
|
|
1948
|
-
this.createMipMap();
|
|
1949
|
-
}
|
|
1950
|
-
/**
|
|
1951
|
-
* Get the best fitting mipmap level for a certain scale resolution
|
|
1952
|
-
* @param resolution Desired resolution in ms per pixel
|
|
1953
|
-
*/
|
|
1954
|
-
getMipMapForResolution(resolution) {
|
|
1955
|
-
return this.getMipMapLevel(this.getMipMapIndexForResolution(resolution));
|
|
1956
|
-
}
|
|
1957
|
-
/**
|
|
1958
|
-
* Computes the index of the best fitting mipmap level for a certain scale resolution
|
|
1959
|
-
* @param resolution Desired resolution in ms per pixel
|
|
1960
|
-
*/
|
|
1961
|
-
getMipMapIndexForResolution(resolution) {
|
|
1962
|
-
if (isNil(resolution)) {
|
|
1963
|
-
return 0;
|
|
1964
|
-
}
|
|
1965
|
-
const factor = resolution / this.resolution;
|
|
1966
|
-
return clamp(Math.floor(Math.log(factor) / Math.log(2.0)), 0, this.mipMaps.length - 1);
|
|
1967
|
-
}
|
|
1968
|
-
/**
|
|
1969
|
-
* Get a mipmap level by index
|
|
1970
|
-
* @param level The index of the mipmap level
|
|
1971
|
-
*/
|
|
1972
|
-
getMipMapLevel(level) {
|
|
1973
|
-
return this.mipMaps[level];
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* Get all mipmap level
|
|
1977
|
-
*/
|
|
1978
|
-
getMipMaps() {
|
|
1979
|
-
return this.mipMaps;
|
|
1980
|
-
}
|
|
1981
|
-
/**
|
|
1982
|
-
* Get the number of available mipmap level
|
|
1983
|
-
*/
|
|
1984
|
-
getNumLevel() {
|
|
1985
|
-
return this.mipMaps.length;
|
|
1986
|
-
}
|
|
1987
|
-
computeResolution(data) {
|
|
1988
|
-
let minTimeDistance = Infinity;
|
|
1989
|
-
for (let i = 0, end = this.originalData.length - 1; i < end; ++i) {
|
|
1990
|
-
const current = this.originalData[i];
|
|
1991
|
-
const next = this.originalData[i + 1];
|
|
1992
|
-
minTimeDistance = Math.min(Math.abs(this.getTime(current) - this.getTime(next)), minTimeDistance);
|
|
1993
|
-
}
|
|
1994
|
-
return minTimeDistance;
|
|
1995
|
-
}
|
|
1996
|
-
createMipMap() {
|
|
1997
|
-
let targetResolution = this.resolution;
|
|
1998
|
-
let targetLength = this.originalData.length;
|
|
1999
|
-
this.mipMaps.push(this.originalData);
|
|
2000
|
-
let lastMipMap = this.originalData;
|
|
2001
|
-
while (lastMipMap.length > this.minNumPoints) {
|
|
2002
|
-
targetResolution = targetResolution * 2;
|
|
2003
|
-
targetLength = Math.floor(targetLength * 0.5);
|
|
2004
|
-
lastMipMap = this.downsampleToResolution(lastMipMap, targetResolution, targetLength);
|
|
2005
|
-
this.mipMaps.push(lastMipMap);
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
downsampleToResolution(data, targetResolution, targetLength) {
|
|
2009
|
-
const output = [];
|
|
2010
|
-
let aggregationValues = [];
|
|
2011
|
-
let firstPoint = data[0];
|
|
2012
|
-
aggregationValues.push(firstPoint);
|
|
2013
|
-
for (let i = 1, end = data.length; i < end; ++i) {
|
|
2014
|
-
const currentPoint = data[i];
|
|
2015
|
-
const timeDistance = Math.abs(this.getTime(firstPoint) - this.getTime(currentPoint));
|
|
2016
|
-
if (timeDistance < targetResolution) {
|
|
2017
|
-
aggregationValues.push(currentPoint);
|
|
2018
|
-
}
|
|
2019
|
-
else {
|
|
2020
|
-
// create new sensor value in output
|
|
2021
|
-
const newPoint = this.getAverage(aggregationValues);
|
|
2022
|
-
output.push(newPoint);
|
|
2023
|
-
// reset aggregation data structure
|
|
2024
|
-
firstPoint = currentPoint;
|
|
2025
|
-
aggregationValues = [currentPoint];
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
// insert last point
|
|
2029
|
-
output.push(this.getAverage(aggregationValues));
|
|
2030
|
-
return output;
|
|
2031
|
-
}
|
|
2032
|
-
getAverage(aggregationValues) {
|
|
2033
|
-
const value = aggregationValues
|
|
2034
|
-
.map(point => point.y)
|
|
2035
|
-
.reduce((previous, current) => previous + current)
|
|
2036
|
-
/ aggregationValues.length;
|
|
2037
|
-
return {
|
|
2038
|
-
x: aggregationValues[0].x || aggregationValues[0].t,
|
|
2039
|
-
y: value
|
|
2040
|
-
};
|
|
2041
|
-
}
|
|
2042
|
-
getTime(point) {
|
|
2043
|
-
const x = point.x || point.t;
|
|
2044
|
-
if (typeof x === 'number') {
|
|
2045
|
-
return x;
|
|
2046
|
-
}
|
|
2047
|
-
else if (typeof x === 'string') {
|
|
2048
|
-
return new Date(x).getTime();
|
|
2049
|
-
}
|
|
2050
|
-
else {
|
|
2051
|
-
return x.getTime();
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
/**
|
|
2057
|
-
* A mipmap data structure that uses Largest-Triangle-Three-Buckets algorithm to downsample data
|
|
2058
|
-
*/
|
|
2059
|
-
class LTTBDataMipmap extends DataMipmap {
|
|
2060
|
-
getMipMapIndexForResolution(resolution) {
|
|
2061
|
-
if (isNil(resolution)) {
|
|
2062
|
-
return 0;
|
|
2063
|
-
}
|
|
2064
|
-
let index = findIndexInArray(this.resolutions, (levelResolution) => levelResolution >= resolution);
|
|
2065
|
-
if (index === -1) {
|
|
2066
|
-
// use smallest mipmap as fallback
|
|
2067
|
-
index = this.resolutions.length - 1;
|
|
2068
|
-
}
|
|
2069
|
-
return index;
|
|
2070
|
-
}
|
|
2071
|
-
createMipMap() {
|
|
2072
|
-
super.createMipMap();
|
|
2073
|
-
this.resolutions = this.mipMaps.map((level) => this.computeAverageResolution(level));
|
|
2074
|
-
}
|
|
2075
|
-
/**
|
|
2076
|
-
* This method is adapted from: https://github.com/sveinn-steinarsson/flot-downsample
|
|
2077
|
-
*
|
|
2078
|
-
* The MIT License
|
|
2079
|
-
* Copyright (c) 2013 by Sveinn Steinarsson
|
|
2080
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
2081
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
2082
|
-
* in the Software without restriction, including without limitation the rights
|
|
2083
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
2084
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
2085
|
-
* furnished to do so, subject to the following conditions:
|
|
2086
|
-
* The above copyright notice and this permission notice shall be included in
|
|
2087
|
-
* all copies or substantial portions of the Software.
|
|
2088
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
2089
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
2090
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
2091
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
2092
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
2093
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
2094
|
-
* THE SOFTWARE.
|
|
2095
|
-
*/
|
|
2096
|
-
downsampleToResolution(data, targetResolution, targetLength) {
|
|
2097
|
-
const dataLength = data.length;
|
|
2098
|
-
if (targetLength >= dataLength || targetLength === 0) {
|
|
2099
|
-
return data; // data has target size
|
|
2100
|
-
}
|
|
2101
|
-
const output = [];
|
|
2102
|
-
// bucket size, leave room for start and end data points
|
|
2103
|
-
const bucksetSize = (dataLength - 2) / (targetLength - 2);
|
|
2104
|
-
let a = 0; // initially a is the first point in the triangle
|
|
2105
|
-
let maxAreaPoint;
|
|
2106
|
-
let maxArea;
|
|
2107
|
-
let area;
|
|
2108
|
-
let nextA;
|
|
2109
|
-
// always add the first point
|
|
2110
|
-
output.push(data[a]);
|
|
2111
|
-
for (let i = 0; i < targetLength - 2; ++i) {
|
|
2112
|
-
// Calculate point average for next bucket (containing c)
|
|
2113
|
-
let avgX = 0;
|
|
2114
|
-
let avgY = 0;
|
|
2115
|
-
let avgRangeStart = Math.floor((i + 1) * bucksetSize) + 1;
|
|
2116
|
-
let avgRangeEnd = Math.floor((i + 2) * bucksetSize) + 1;
|
|
2117
|
-
avgRangeEnd = avgRangeEnd < dataLength ? avgRangeEnd : dataLength;
|
|
2118
|
-
const avgRangeLength = avgRangeEnd - avgRangeStart;
|
|
2119
|
-
for (; avgRangeStart < avgRangeEnd; avgRangeStart++) {
|
|
2120
|
-
avgX += this.getTime(data[avgRangeStart]);
|
|
2121
|
-
avgY += data[avgRangeStart].y * 1;
|
|
2122
|
-
}
|
|
2123
|
-
avgX /= avgRangeLength;
|
|
2124
|
-
avgY /= avgRangeLength;
|
|
2125
|
-
// Get the range for this bucket
|
|
2126
|
-
let rangeOffs = Math.floor((i + 0) * bucksetSize) + 1;
|
|
2127
|
-
const rangeTo = Math.floor((i + 1) * bucksetSize) + 1;
|
|
2128
|
-
// Point a
|
|
2129
|
-
const pointA = data[a];
|
|
2130
|
-
const pointAX = this.getTime(pointA);
|
|
2131
|
-
const pointAY = pointA.y * 1;
|
|
2132
|
-
maxArea = area = -1;
|
|
2133
|
-
for (; rangeOffs < rangeTo; rangeOffs++) {
|
|
2134
|
-
// Calculate triangle area over three buckets
|
|
2135
|
-
area = Math.abs((pointAX - avgX) * (data[rangeOffs].y - pointAY) -
|
|
2136
|
-
(pointAX - this.getTime(data[rangeOffs])) * (avgY - pointAY)) * 0.5;
|
|
2137
|
-
if (area > maxArea) {
|
|
2138
|
-
maxArea = area;
|
|
2139
|
-
maxAreaPoint = data[rangeOffs];
|
|
2140
|
-
nextA = rangeOffs; // Next a is this b
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
output.push(maxAreaPoint); // Pick this point from the bucket
|
|
2144
|
-
a = nextA; // This a is the next a (chosen b)
|
|
2145
|
-
}
|
|
2146
|
-
output.push(data[dataLength - 1]); // Always add last
|
|
2147
|
-
return output;
|
|
2148
|
-
}
|
|
2149
|
-
computeAverageResolution(data) {
|
|
2150
|
-
let timeDistances = 0;
|
|
2151
|
-
for (let i = 0, end = this.originalData.length - 1; i < end; ++i) {
|
|
2152
|
-
const current = this.originalData[i];
|
|
2153
|
-
const next = this.originalData[i + 1];
|
|
2154
|
-
timeDistances += Math.abs(this.getTime(current) - this.getTime(next));
|
|
2155
|
-
}
|
|
2156
|
-
return timeDistances / (data.length - 1);
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
function getCompareValue(value) {
|
|
2161
|
-
if (typeof value === 'number') {
|
|
2162
|
-
return value;
|
|
2163
|
-
}
|
|
2164
|
-
else if (typeof value === 'string') {
|
|
2165
|
-
return (new Date(value)).getTime();
|
|
2166
|
-
}
|
|
2167
|
-
else if (value instanceof Date) {
|
|
2168
|
-
return value.getTime();
|
|
2169
|
-
}
|
|
2170
|
-
else {
|
|
2171
|
-
return moment$1(value).toDate().getTime();
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
function rangeIsEqual(previousValue, currentValue) {
|
|
2175
|
-
if (isNil(previousValue) ||
|
|
2176
|
-
isNil(currentValue) ||
|
|
2177
|
-
isNil(previousValue[0]) ||
|
|
2178
|
-
isNil(previousValue[1]) ||
|
|
2179
|
-
isNil(currentValue[0]) ||
|
|
2180
|
-
isNil(currentValue[1])) {
|
|
2181
|
-
return false;
|
|
2182
|
-
}
|
|
2183
|
-
previousValue = [getCompareValue(previousValue[0]), getCompareValue(previousValue[1])];
|
|
2184
|
-
currentValue = [getCompareValue(currentValue[0]), getCompareValue(currentValue[1])];
|
|
2185
|
-
return previousValue[0] === currentValue[0] && previousValue[1] === currentValue[1];
|
|
2186
|
-
}
|
|
2187
|
-
function getScaleRange(scale) {
|
|
2188
|
-
if (isNil(scale)) {
|
|
2189
|
-
return [null, null];
|
|
2190
|
-
}
|
|
2191
|
-
const start = scale.getValueForPixel(scale.left);
|
|
2192
|
-
const end = scale.getValueForPixel(scale.right);
|
|
2193
|
-
return [start, end];
|
|
2194
|
-
}
|
|
2195
|
-
function cullData(data, range) {
|
|
2196
|
-
const startValue = getCompareValue(range[0]);
|
|
2197
|
-
const endValue = getCompareValue(range[1]);
|
|
2198
|
-
let startIndex = 0;
|
|
2199
|
-
let endIndex = data.length;
|
|
2200
|
-
for (let i = 1; i < data.length; ++i) {
|
|
2201
|
-
const point = data[i];
|
|
2202
|
-
const compareValue = getCompareValue(point.x || point.t);
|
|
2203
|
-
if (compareValue <= startValue) {
|
|
2204
|
-
startIndex = i;
|
|
2205
|
-
}
|
|
2206
|
-
if (compareValue >= endValue) {
|
|
2207
|
-
endIndex = i + 1;
|
|
2208
|
-
break;
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
return data.slice(startIndex, endIndex);
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
/**
|
|
2215
|
-
* This plugin is adapted from: https://github.com/3dcl/chartjs-plugin-responsive-downsample
|
|
2216
|
-
* for controlling the radius of point.
|
|
2217
|
-
*
|
|
2218
|
-
* MIT License
|
|
2219
|
-
* Copyright (c) 2018 3D Content Logistics
|
|
2220
|
-
*
|
|
2221
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
2222
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
2223
|
-
* in the Software without restriction, including without limitation the rights
|
|
2224
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
2225
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
2226
|
-
* furnished to do so, subject to the following conditions:
|
|
2227
|
-
* The above copyright notice and this permission notice shall be included in all
|
|
2228
|
-
* copies or substantial portions of the Software.
|
|
2229
|
-
*
|
|
2230
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
2231
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
2232
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
2233
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
2234
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
2235
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
2236
|
-
* SOFTWARE.
|
|
2237
|
-
*/
|
|
2238
|
-
// Using dynamic flag to fix the issue of ng-packagr #696: Lambda not supported
|
|
2239
|
-
// @dynamic
|
|
2240
|
-
/**
|
|
2241
|
-
* Chart js Plugin for downsampling data
|
|
2242
|
-
*/
|
|
2243
|
-
class ResponsiveDownsamplePlugin {
|
|
2244
|
-
static getPluginOptions(chart) {
|
|
2245
|
-
const options = chart.options.responsiveDownsample || {};
|
|
2246
|
-
defaultsDeep(options, {
|
|
2247
|
-
enabled: false,
|
|
2248
|
-
aggregationAlgorithm: 'LTTB',
|
|
2249
|
-
desiredDataPointDistance: 1,
|
|
2250
|
-
minNumPoints: 100,
|
|
2251
|
-
maxNumPointsToDraw: 100,
|
|
2252
|
-
cullData: true
|
|
2253
|
-
});
|
|
2254
|
-
if (options.enabled) {
|
|
2255
|
-
chart.options.responsiveDownsample = options;
|
|
2256
|
-
}
|
|
2257
|
-
return options;
|
|
2258
|
-
}
|
|
2259
|
-
static hasDataChanged(chart) {
|
|
2260
|
-
return !isNil(findInArray(chart.data.datasets, (dataset) => {
|
|
2261
|
-
return isNil(dataset.mipMap);
|
|
2262
|
-
}));
|
|
2263
|
-
}
|
|
2264
|
-
static createDataMipMap(chart, options) {
|
|
2265
|
-
chart.data.datasets.forEach((dataset) => {
|
|
2266
|
-
// @ts-ignore
|
|
2267
|
-
const data = !isNil(dataset.originalData) ? dataset.originalData : dataset.data;
|
|
2268
|
-
const mipMap = (options.aggregationAlgorithm === 'LTTB')
|
|
2269
|
-
? new LTTBDataMipmap(data, options.minNumPoints)
|
|
2270
|
-
: new DataMipmap(data, options.minNumPoints);
|
|
2271
|
-
dataset.originalData = data;
|
|
2272
|
-
dataset.mipMap = mipMap;
|
|
2273
|
-
// @ts-ignore
|
|
2274
|
-
dataset.data = mipMap.getMipMapLevel(mipMap.getNumLevel() - 1); // set last level for first render pass
|
|
2275
|
-
});
|
|
2276
|
-
}
|
|
2277
|
-
static restoreOriginalData(chart) {
|
|
2278
|
-
let updated = false;
|
|
2279
|
-
chart.data.datasets.forEach((dataset) => {
|
|
2280
|
-
// @ts-ignore
|
|
2281
|
-
if (!isNil(dataset.originalData) && dataset.data !== dataset.originalData) {
|
|
2282
|
-
// @ts-ignore
|
|
2283
|
-
dataset.data = dataset.originalData;
|
|
2284
|
-
updated = true;
|
|
2285
|
-
}
|
|
2286
|
-
});
|
|
2287
|
-
return updated;
|
|
2288
|
-
}
|
|
2289
|
-
static getTargetResolution(chart, options) {
|
|
2290
|
-
const xScale = chart.scales['x-axis-0'];
|
|
2291
|
-
if (isNil(xScale)) {
|
|
2292
|
-
return null;
|
|
2293
|
-
}
|
|
2294
|
-
const start = moment$1(xScale.getValueForPixel(xScale.left));
|
|
2295
|
-
const end = moment$1(xScale.getValueForPixel(xScale.left + 1));
|
|
2296
|
-
const targetResolution = end.diff(start);
|
|
2297
|
-
return targetResolution * options.desiredDataPointDistance;
|
|
2298
|
-
}
|
|
2299
|
-
static updateMipMap(chart, options, rangeChanged) {
|
|
2300
|
-
let updated = false;
|
|
2301
|
-
chart.data.datasets.forEach((dataset) => {
|
|
2302
|
-
const mipMap = dataset.mipMap;
|
|
2303
|
-
if (isNil(mipMap)) {
|
|
2304
|
-
return;
|
|
2305
|
-
}
|
|
2306
|
-
const mipMalLevel = mipMap.getMipMapIndexForResolution(options.targetResolution);
|
|
2307
|
-
if (mipMalLevel === dataset.currentMipMapLevel && !rangeChanged) {
|
|
2308
|
-
// skip update if mip map level and data range did not change
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
updated = true;
|
|
2312
|
-
dataset.currentMipMapLevel = mipMalLevel;
|
|
2313
|
-
let newData = mipMap.getMipMapLevel(mipMalLevel);
|
|
2314
|
-
if (options.cullData) {
|
|
2315
|
-
newData = cullData(newData, options.scaleRange);
|
|
2316
|
-
}
|
|
2317
|
-
// @ts-ignore
|
|
2318
|
-
dataset.data = newData;
|
|
2319
|
-
});
|
|
2320
|
-
return updated;
|
|
2321
|
-
}
|
|
2322
|
-
beforeInit(chart) {
|
|
2323
|
-
const options = ResponsiveDownsamplePlugin.getPluginOptions(chart);
|
|
2324
|
-
if (!options.enabled) {
|
|
2325
|
-
return;
|
|
2326
|
-
}
|
|
2327
|
-
ResponsiveDownsamplePlugin.createDataMipMap(chart, options);
|
|
2328
|
-
options.needsUpdate = true;
|
|
2329
|
-
}
|
|
2330
|
-
beforeDatasetsUpdate(chart) {
|
|
2331
|
-
const options = ResponsiveDownsamplePlugin.getPluginOptions(chart);
|
|
2332
|
-
if (!options.enabled) {
|
|
2333
|
-
// restore original data and remove state from options
|
|
2334
|
-
options.needsUpdate = ResponsiveDownsamplePlugin.restoreOriginalData(chart);
|
|
2335
|
-
delete options.targetResolution;
|
|
2336
|
-
delete options.scaleRange;
|
|
2337
|
-
return;
|
|
2338
|
-
}
|
|
2339
|
-
// only update mip map if data set was reloaded externally
|
|
2340
|
-
if (ResponsiveDownsamplePlugin.hasDataChanged(chart)) {
|
|
2341
|
-
ResponsiveDownsamplePlugin.createDataMipMap(chart, options);
|
|
2342
|
-
options.needsUpdate = true;
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
beforeRender(chart) {
|
|
2346
|
-
const options = ResponsiveDownsamplePlugin.getPluginOptions(chart);
|
|
2347
|
-
if (!options.enabled) {
|
|
2348
|
-
// update chart if data was restored from original data
|
|
2349
|
-
if (options.needsUpdate) {
|
|
2350
|
-
options.needsUpdate = false;
|
|
2351
|
-
chart.update(0);
|
|
2352
|
-
return false;
|
|
2353
|
-
}
|
|
2354
|
-
return true;
|
|
2355
|
-
}
|
|
2356
|
-
const targetResolution = ResponsiveDownsamplePlugin.getTargetResolution(chart, options);
|
|
2357
|
-
const xScale = chart.scales['x-axis-0'];
|
|
2358
|
-
const scaleRange = getScaleRange(xScale);
|
|
2359
|
-
const rangeChanged = !rangeIsEqual(options.scaleRange, scaleRange);
|
|
2360
|
-
if (options.needsUpdate ||
|
|
2361
|
-
options.targetResolution !== targetResolution ||
|
|
2362
|
-
rangeChanged) {
|
|
2363
|
-
options.targetResolution = targetResolution;
|
|
2364
|
-
options.scaleRange = scaleRange;
|
|
2365
|
-
options.needsUpdate = false;
|
|
2366
|
-
if (ResponsiveDownsamplePlugin.updateMipMap(chart, options, rangeChanged)) {
|
|
2367
|
-
changePointRadius(chart, options);
|
|
2368
|
-
// update chart and cancel current render
|
|
2369
|
-
chart.update(0);
|
|
2370
|
-
return false;
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
return true;
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
/**
|
|
2378
|
-
* This plugin is adapted from: https://github.com/3dcl/chartjs-plugin-responsive-downsample
|
|
2379
|
-
* for controlling the radius of point.
|
|
2380
|
-
*
|
|
2381
|
-
* MIT License
|
|
2382
|
-
* Copyright (c) 2018 3D Content Logistics
|
|
2383
|
-
*
|
|
2384
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
2385
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
2386
|
-
* in the Software without restriction, including without limitation the rights
|
|
2387
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
2388
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
2389
|
-
* furnished to do so, subject to the following conditions:
|
|
2390
|
-
* The above copyright notice and this permission notice shall be included in all
|
|
2391
|
-
* copies or substantial portions of the Software.
|
|
2392
|
-
*
|
|
2393
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
2394
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
2395
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
2396
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
2397
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
2398
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
2399
|
-
* SOFTWARE.
|
|
2400
|
-
*/
|
|
2401
|
-
const Chart$1 = window && window.Chart ? window.Chart : chartjs.Chart;
|
|
2402
|
-
// comment this because this plugin is only used internally, merge ChartOptions interface is unnecessary
|
|
2403
|
-
// and causes build error: "error TS2665: Invalid module name in augmentation.".
|
|
2404
|
-
// // Extend chart.js options interface
|
|
2405
|
-
// declare module 'chart.js' {
|
|
2406
|
-
// interface ChartOptions {
|
|
2407
|
-
// /**
|
|
2408
|
-
// * Options for responsive downsample plugin
|
|
2409
|
-
// */
|
|
2410
|
-
// responsiveDownsample?: ResponsiveDownsamplePluginOptions;
|
|
2411
|
-
// }
|
|
2412
|
-
// }
|
|
2413
|
-
Chart$1.pluginService.register(new ResponsiveDownsamplePlugin());
|
|
2414
|
-
|
|
2415
|
-
/**
|
|
2416
|
-
* this plugin is adapted from: https://github.com/chartjs/chartjs-plugin-zoom
|
|
2417
|
-
* for following two open issues:
|
|
2418
|
-
* https://github.com/chartjs/chartjs-plugin-zoom/pull/155
|
|
2419
|
-
* https://github.com/chartjs/chartjs-plugin-zoom/pull/150
|
|
2420
|
-
*
|
|
2421
|
-
* The MIT License (MIT) Copyright (c) 2013-2016 Nick Downie
|
|
2422
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
2423
|
-
* software and associated documentation files (the "Software"), to deal in the Software
|
|
2424
|
-
* without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
2425
|
-
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
2426
|
-
* to whom the Software is furnished to do so, subject to the following conditions:
|
|
2427
|
-
*
|
|
2428
|
-
* The above copyright notice and this permission notice shall be included in all copies or
|
|
2429
|
-
* substantial portions of the Software.
|
|
2430
|
-
*
|
|
2431
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
2432
|
-
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
|
|
2433
|
-
* AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
2434
|
-
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
2435
|
-
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
2436
|
-
* THE SOFTWARE.
|
|
2437
|
-
*/
|
|
2438
|
-
/*jslint browser:true, devel:true, white:true, vars:true */
|
|
2439
|
-
/*global require*/
|
|
2440
|
-
// hammer JS for touch support
|
|
2441
|
-
const Hammer = window && window.Hammer ? window.Hammer : hammerjs.Hammer;
|
|
2442
|
-
const Chart = window && window.Chart ? window.Chart : chartjs.Chart;
|
|
2443
|
-
const helpers = Chart.helpers;
|
|
2444
|
-
// Take the zoom namespace of Chart
|
|
2445
|
-
const zoomNS = Chart.Zoom = Chart.Zoom || {};
|
|
2446
|
-
// Where we store functions to handle different scale types
|
|
2447
|
-
const zoomFunctions = zoomNS.zoomFunctions = zoomNS.zoomFunctions || {};
|
|
2448
|
-
const panFunctions = zoomNS.panFunctions = zoomNS.panFunctions || {};
|
|
2449
|
-
// Default options if none are provided
|
|
2450
|
-
const defaultOptions = zoomNS.defaults = {
|
|
2451
|
-
pan: {
|
|
2452
|
-
enabled: true,
|
|
2453
|
-
mode: 'xy',
|
|
2454
|
-
speed: 20,
|
|
2455
|
-
threshold: 10
|
|
2456
|
-
},
|
|
2457
|
-
zoom: {
|
|
2458
|
-
enabled: true,
|
|
2459
|
-
mode: 'xy',
|
|
2460
|
-
sensitivity: 3
|
|
2461
|
-
}
|
|
2462
|
-
};
|
|
2463
|
-
function directionEnabled(mode, dir) {
|
|
2464
|
-
if (mode === undefined) {
|
|
2465
|
-
return true;
|
|
2466
|
-
}
|
|
2467
|
-
else if (typeof mode === 'string') {
|
|
2468
|
-
return mode.indexOf(dir) !== -1;
|
|
2469
|
-
}
|
|
2470
|
-
return false;
|
|
2471
|
-
}
|
|
2472
|
-
function rangeMaxLimiter(zoomPanOptions, newMax) {
|
|
2473
|
-
if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMax &&
|
|
2474
|
-
!helpers.isNullOrUndef(zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes])) {
|
|
2475
|
-
const rangeMax = zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes];
|
|
2476
|
-
if (newMax > rangeMax) {
|
|
2477
|
-
newMax = rangeMax;
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
return newMax;
|
|
2481
|
-
}
|
|
2482
|
-
function rangeMinLimiter(zoomPanOptions, newMin) {
|
|
2483
|
-
if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMin &&
|
|
2484
|
-
!helpers.isNullOrUndef(zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes])) {
|
|
2485
|
-
const rangeMin = zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes];
|
|
2486
|
-
if (newMin < rangeMin) {
|
|
2487
|
-
newMin = rangeMin;
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
return newMin;
|
|
2491
|
-
}
|
|
2492
|
-
function zoomIndexScale(scale, zoom, center, zoomOptions) {
|
|
2493
|
-
const labels = scale.chart.data.labels;
|
|
2494
|
-
let minIndex = scale.minIndex;
|
|
2495
|
-
const lastLabelIndex = labels.length - 1;
|
|
2496
|
-
let maxIndex = scale.maxIndex;
|
|
2497
|
-
const sensitivity = zoomOptions.sensitivity;
|
|
2498
|
-
const chartCenter = scale.isHorizontal() ? scale.left + (scale.width / 2) : scale.top + (scale.height / 2);
|
|
2499
|
-
const centerPointer = scale.isHorizontal() ? center.x : center.y;
|
|
2500
|
-
zoomNS.zoomCumulativeDelta = zoom > 1 ? zoomNS.zoomCumulativeDelta + 1 : zoomNS.zoomCumulativeDelta - 1;
|
|
2501
|
-
if (Math.abs(zoomNS.zoomCumulativeDelta) > sensitivity) {
|
|
2502
|
-
if (zoomNS.zoomCumulativeDelta < 0) {
|
|
2503
|
-
if (centerPointer >= chartCenter) {
|
|
2504
|
-
if (minIndex <= 0) {
|
|
2505
|
-
maxIndex = Math.min(lastLabelIndex, maxIndex + 1);
|
|
2506
|
-
}
|
|
2507
|
-
else {
|
|
2508
|
-
minIndex = Math.max(0, minIndex - 1);
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
else if (centerPointer < chartCenter) {
|
|
2512
|
-
if (maxIndex >= lastLabelIndex) {
|
|
2513
|
-
minIndex = Math.max(0, minIndex - 1);
|
|
2514
|
-
}
|
|
2515
|
-
else {
|
|
2516
|
-
maxIndex = Math.min(lastLabelIndex, maxIndex + 1);
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
zoomNS.zoomCumulativeDelta = 0;
|
|
2520
|
-
}
|
|
2521
|
-
else if (zoomNS.zoomCumulativeDelta > 0) {
|
|
2522
|
-
if (centerPointer >= chartCenter) {
|
|
2523
|
-
minIndex = minIndex < maxIndex ? minIndex = Math.min(maxIndex, minIndex + 1) : minIndex;
|
|
2524
|
-
}
|
|
2525
|
-
else if (centerPointer < chartCenter) {
|
|
2526
|
-
maxIndex = maxIndex > minIndex ? maxIndex = Math.max(minIndex, maxIndex - 1) : maxIndex;
|
|
2527
|
-
}
|
|
2528
|
-
zoomNS.zoomCumulativeDelta = 0;
|
|
2529
|
-
}
|
|
2530
|
-
scale.options.ticks.min = rangeMinLimiter(zoomOptions, labels[minIndex]);
|
|
2531
|
-
scale.options.ticks.max = rangeMaxLimiter(zoomOptions, labels[maxIndex]);
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
|
-
function zoomTimeScale(scale, zoom, center, zoomOptions) {
|
|
2535
|
-
const options = scale.options;
|
|
2536
|
-
// var range;
|
|
2537
|
-
// var min_percent;
|
|
2538
|
-
// if (scale.isHorizontal()) {
|
|
2539
|
-
// range = scale.right - scale.left;
|
|
2540
|
-
// min_percent = (center.x - scale.left) / range;
|
|
2541
|
-
// } else {
|
|
2542
|
-
// range = scale.bottom - scale.top;
|
|
2543
|
-
// min_percent = (center.y - scale.top) / range;
|
|
2544
|
-
// }
|
|
2545
|
-
//
|
|
2546
|
-
// var max_percent = 1 - min_percent;
|
|
2547
|
-
const range = scale.max - scale.min;
|
|
2548
|
-
const newDiff = range * (zoom - 1);
|
|
2549
|
-
const cursorPixel = scale.isHorizontal() ? center.x : center.y;
|
|
2550
|
-
const min_percent = (scale.getValueForPixel(cursorPixel) - scale.min) / range;
|
|
2551
|
-
const max_percent = 1 - min_percent;
|
|
2552
|
-
const minDelta = newDiff * min_percent;
|
|
2553
|
-
const maxDelta = newDiff * max_percent;
|
|
2554
|
-
const newMin = scale.min + minDelta;
|
|
2555
|
-
const newMax = scale.max - maxDelta;
|
|
2556
|
-
const diffMinMax = newMax - newMin;
|
|
2557
|
-
const minLimitExceeded = rangeMinLimiter(zoomOptions, diffMinMax) !== diffMinMax;
|
|
2558
|
-
const maxLimitExceeded = rangeMaxLimiter(zoomOptions, diffMinMax) !== diffMinMax;
|
|
2559
|
-
if (!minLimitExceeded && !maxLimitExceeded) {
|
|
2560
|
-
options.time.min = newMin;
|
|
2561
|
-
options.time.max = newMax;
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
function zoomNumericalScale(scale, zoom, center, zoomOptions) {
|
|
2565
|
-
const range = scale.max - scale.min;
|
|
2566
|
-
const newDiff = range * (zoom - 1);
|
|
2567
|
-
const cursorPixel = scale.isHorizontal() ? center.x : center.y;
|
|
2568
|
-
const min_percent = (scale.getValueForPixel(cursorPixel) - scale.min) / range;
|
|
2569
|
-
const max_percent = 1 - min_percent;
|
|
2570
|
-
const minDelta = newDiff * min_percent;
|
|
2571
|
-
const maxDelta = newDiff * max_percent;
|
|
2572
|
-
scale.options.ticks.min = rangeMinLimiter(zoomOptions, scale.min + minDelta);
|
|
2573
|
-
scale.options.ticks.max = rangeMaxLimiter(zoomOptions, scale.max - maxDelta);
|
|
2574
|
-
}
|
|
2575
|
-
function zoomScale(scale, zoom, center, zoomOptions) {
|
|
2576
|
-
const fn = zoomFunctions[scale.options.type];
|
|
2577
|
-
if (fn) {
|
|
2578
|
-
fn(scale, zoom, center, zoomOptions);
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
function doZoom(chartInstance, zoom, center, whichAxes) {
|
|
2582
|
-
const ca = chartInstance.chartArea;
|
|
2583
|
-
if (!center) {
|
|
2584
|
-
center = {
|
|
2585
|
-
x: (ca.left + ca.right) / 2,
|
|
2586
|
-
y: (ca.top + ca.bottom) / 2,
|
|
2587
|
-
};
|
|
2588
|
-
}
|
|
2589
|
-
const zoomOptions = chartInstance.options.zoom;
|
|
2590
|
-
if (zoomOptions && helpers.getValueOrDefault(zoomOptions.enabled, defaultOptions.zoom.enabled)) {
|
|
2591
|
-
// Do the zoom here
|
|
2592
|
-
const zoomMode = helpers.getValueOrDefault(chartInstance.options.zoom.mode, defaultOptions.zoom.mode);
|
|
2593
|
-
zoomOptions.sensitivity = helpers.getValueOrDefault(chartInstance.options.zoom.sensitivity, defaultOptions.zoom.sensitivity);
|
|
2594
|
-
// Which axe should be modified when figers were used.
|
|
2595
|
-
let _whichAxes;
|
|
2596
|
-
if (zoomMode === 'xy' && whichAxes !== undefined) {
|
|
2597
|
-
// based on fingers positions
|
|
2598
|
-
_whichAxes = whichAxes;
|
|
2599
|
-
}
|
|
2600
|
-
else {
|
|
2601
|
-
// no effect
|
|
2602
|
-
_whichAxes = 'xy';
|
|
2603
|
-
}
|
|
2604
|
-
helpers.each(chartInstance.scales, function (scale, id) {
|
|
2605
|
-
if (scale.isHorizontal() && directionEnabled(zoomMode, 'x') && directionEnabled(_whichAxes, 'x')) {
|
|
2606
|
-
zoomOptions.scaleAxes = 'x';
|
|
2607
|
-
zoomScale(scale, zoom, center, zoomOptions);
|
|
2608
|
-
}
|
|
2609
|
-
else if (!scale.isHorizontal() && directionEnabled(zoomMode, 'y') && directionEnabled(_whichAxes, 'y')) {
|
|
2610
|
-
// Do Y zoom
|
|
2611
|
-
zoomOptions.scaleAxes = 'y';
|
|
2612
|
-
zoomScale(scale, zoom, center, zoomOptions);
|
|
2613
|
-
}
|
|
2614
|
-
});
|
|
2615
|
-
chartInstance.update(0);
|
|
2616
|
-
if (typeof zoomOptions.onZoom === 'function') {
|
|
2617
|
-
zoomOptions.onZoom();
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
function panIndexScale(scale, delta, panOptions) {
|
|
2622
|
-
const labels = scale.chart.data.labels;
|
|
2623
|
-
const lastLabelIndex = labels.length - 1;
|
|
2624
|
-
const offsetAmt = Math.max((scale.ticks.length - ((scale.options.gridLines.offsetGridLines) ? 0 : 1)), 1);
|
|
2625
|
-
const panSpeed = panOptions.speed;
|
|
2626
|
-
let minIndex = scale.minIndex;
|
|
2627
|
-
const step = Math.round(scale.width / (offsetAmt * panSpeed));
|
|
2628
|
-
let maxIndex;
|
|
2629
|
-
zoomNS.panCumulativeDelta += delta;
|
|
2630
|
-
minIndex = zoomNS.panCumulativeDelta > step ? Math.max(0, minIndex - 1) : zoomNS.panCumulativeDelta < -step ? Math.min(lastLabelIndex - offsetAmt + 1, minIndex + 1) : minIndex;
|
|
2631
|
-
zoomNS.panCumulativeDelta = minIndex !== scale.minIndex ? 0 : zoomNS.panCumulativeDelta;
|
|
2632
|
-
maxIndex = Math.min(lastLabelIndex, minIndex + offsetAmt - 1);
|
|
2633
|
-
scale.options.ticks.min = rangeMinLimiter(panOptions, labels[minIndex]);
|
|
2634
|
-
scale.options.ticks.max = rangeMaxLimiter(panOptions, labels[maxIndex]);
|
|
2635
|
-
}
|
|
2636
|
-
function panTimeScale(scale, delta, panOptions) {
|
|
2637
|
-
const options = scale.options;
|
|
2638
|
-
const limitedMax = rangeMaxLimiter(panOptions, scale.getValueForPixel(scale.getPixelForValue(scale.max) - delta));
|
|
2639
|
-
const limitedMin = rangeMinLimiter(panOptions, scale.getValueForPixel(scale.getPixelForValue(scale.min) - delta));
|
|
2640
|
-
const limitedTimeDelta = delta < 0 ? limitedMax - scale.max : limitedMin - scale.min;
|
|
2641
|
-
options.time.max = scale.max + limitedTimeDelta;
|
|
2642
|
-
options.time.min = scale.min + limitedTimeDelta;
|
|
2643
|
-
}
|
|
2644
|
-
function panNumericalScale(scale, delta, panOptions) {
|
|
2645
|
-
const tickOpts = scale.options.ticks;
|
|
2646
|
-
const start = scale.start, end = scale.end;
|
|
2647
|
-
if (tickOpts.reverse) {
|
|
2648
|
-
tickOpts.max = scale.getValueForPixel(scale.getPixelForValue(start) - delta);
|
|
2649
|
-
tickOpts.min = scale.getValueForPixel(scale.getPixelForValue(end) - delta);
|
|
2650
|
-
}
|
|
2651
|
-
else {
|
|
2652
|
-
tickOpts.min = scale.getValueForPixel(scale.getPixelForValue(start) - delta);
|
|
2653
|
-
tickOpts.max = scale.getValueForPixel(scale.getPixelForValue(end) - delta);
|
|
2654
|
-
}
|
|
2655
|
-
tickOpts.min = rangeMinLimiter(panOptions, tickOpts.min);
|
|
2656
|
-
tickOpts.max = rangeMaxLimiter(panOptions, tickOpts.max);
|
|
2657
|
-
}
|
|
2658
|
-
function panScale(scale, delta, panOptions) {
|
|
2659
|
-
const fn = panFunctions[scale.options.type];
|
|
2660
|
-
if (fn) {
|
|
2661
|
-
fn(scale, delta, panOptions);
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
function doPan(chartInstance, deltaX, deltaY) {
|
|
2665
|
-
const panOptions = chartInstance.options.pan;
|
|
2666
|
-
if (panOptions && helpers.getValueOrDefault(panOptions.enabled, defaultOptions.pan.enabled)) {
|
|
2667
|
-
const panMode = helpers.getValueOrDefault(chartInstance.options.pan.mode, defaultOptions.pan.mode);
|
|
2668
|
-
panOptions.speed = helpers.getValueOrDefault(chartInstance.options.pan.speed, defaultOptions.pan.speed);
|
|
2669
|
-
helpers.each(chartInstance.scales, function (scale, id) {
|
|
2670
|
-
if (scale.isHorizontal() && directionEnabled(panMode, 'x') && deltaX !== 0) {
|
|
2671
|
-
panOptions.scaleAxes = 'x';
|
|
2672
|
-
panScale(scale, deltaX, panOptions);
|
|
2673
|
-
}
|
|
2674
|
-
else if (!scale.isHorizontal() && directionEnabled(panMode, 'y') && deltaY !== 0) {
|
|
2675
|
-
panOptions.scaleAxes = 'y';
|
|
2676
|
-
panScale(scale, deltaY, panOptions);
|
|
2677
|
-
}
|
|
2678
|
-
});
|
|
2679
|
-
chartInstance.update(0);
|
|
2680
|
-
if (typeof panOptions.onPan === 'function') {
|
|
2681
|
-
panOptions.onPan();
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
2685
|
-
function positionInChartArea(chartInstance, position) {
|
|
2686
|
-
return (position.x >= chartInstance.chartArea.left && position.x <= chartInstance.chartArea.right) &&
|
|
2687
|
-
(position.y >= chartInstance.chartArea.top && position.y <= chartInstance.chartArea.bottom);
|
|
2688
|
-
}
|
|
2689
|
-
function getYAxis(chartInstance) {
|
|
2690
|
-
const scales = chartInstance.scales;
|
|
2691
|
-
for (const scaleId in scales) {
|
|
2692
|
-
if (scales.hasOwnProperty(scaleId)) {
|
|
2693
|
-
const scale = scales[scaleId];
|
|
2694
|
-
if (!scale.isHorizontal()) {
|
|
2695
|
-
return scale;
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
// Store these for later
|
|
2701
|
-
zoomNS.zoomFunctions.category = zoomIndexScale;
|
|
2702
|
-
zoomNS.zoomFunctions.time = zoomTimeScale;
|
|
2703
|
-
zoomNS.zoomFunctions.linear = zoomNumericalScale;
|
|
2704
|
-
zoomNS.zoomFunctions.logarithmic = zoomNumericalScale;
|
|
2705
|
-
zoomNS.panFunctions.category = panIndexScale;
|
|
2706
|
-
zoomNS.panFunctions.time = panTimeScale;
|
|
2707
|
-
zoomNS.panFunctions.linear = panNumericalScale;
|
|
2708
|
-
zoomNS.panFunctions.logarithmic = panNumericalScale;
|
|
2709
|
-
// Globals for catergory pan and zoom
|
|
2710
|
-
zoomNS.panCumulativeDelta = 0;
|
|
2711
|
-
zoomNS.zoomCumulativeDelta = 0;
|
|
2712
|
-
// Chartjs Zoom Plugin
|
|
2713
|
-
const zoomPlugin = {
|
|
2714
|
-
afterInit: function (chartInstance) {
|
|
2715
|
-
helpers.each(chartInstance.scales, function (scale) {
|
|
2716
|
-
scale.originalOptions = helpers.clone(scale.options);
|
|
2717
|
-
});
|
|
2718
|
-
chartInstance.resetZoom = function () {
|
|
2719
|
-
helpers.each(chartInstance.scales, function (scale, id) {
|
|
2720
|
-
const timeOptions = scale.options.time;
|
|
2721
|
-
const tickOptions = scale.options.ticks;
|
|
2722
|
-
if (timeOptions) {
|
|
2723
|
-
timeOptions.min = scale.originalOptions.time.min;
|
|
2724
|
-
timeOptions.max = scale.originalOptions.time.max;
|
|
2725
|
-
}
|
|
2726
|
-
if (tickOptions) {
|
|
2727
|
-
tickOptions.min = scale.originalOptions.ticks.min;
|
|
2728
|
-
tickOptions.max = scale.originalOptions.ticks.max;
|
|
2729
|
-
}
|
|
2730
|
-
});
|
|
2731
|
-
helpers.each(chartInstance.data.datasets, function (dataset, id) {
|
|
2732
|
-
dataset._meta = null;
|
|
2733
|
-
});
|
|
2734
|
-
chartInstance.update();
|
|
2735
|
-
};
|
|
2736
|
-
},
|
|
2737
|
-
beforeInit: function (chartInstance) {
|
|
2738
|
-
chartInstance.zoom = {};
|
|
2739
|
-
const node = chartInstance.zoom.node = chartInstance.chart.ctx.canvas;
|
|
2740
|
-
const options = chartInstance.options;
|
|
2741
|
-
const panThreshold = helpers.getValueOrDefault(options.pan ? options.pan.threshold : undefined, zoomNS.defaults.pan.threshold);
|
|
2742
|
-
if (!options.zoom || !options.zoom.enabled) {
|
|
2743
|
-
return;
|
|
2744
|
-
}
|
|
2745
|
-
if (options.zoom.drag) {
|
|
2746
|
-
// Only want to zoom horizontal axis
|
|
2747
|
-
options.zoom.mode = 'x';
|
|
2748
|
-
chartInstance.zoom._mouseDownHandler = function (event) {
|
|
2749
|
-
chartInstance.zoom._dragZoomStart = event;
|
|
2750
|
-
};
|
|
2751
|
-
node.addEventListener('mousedown', chartInstance.zoom._mouseDownHandler);
|
|
2752
|
-
chartInstance.zoom._mouseMoveHandler = function (event) {
|
|
2753
|
-
if (chartInstance.zoom._dragZoomStart) {
|
|
2754
|
-
chartInstance.zoom._dragZoomEnd = event;
|
|
2755
|
-
chartInstance.update(0);
|
|
2756
|
-
}
|
|
2757
|
-
};
|
|
2758
|
-
node.addEventListener('mousemove', chartInstance.zoom._mouseMoveHandler);
|
|
2759
|
-
chartInstance.zoom._mouseUpHandler = function (event) {
|
|
2760
|
-
if (chartInstance.zoom._dragZoomStart) {
|
|
2761
|
-
const chartArea = chartInstance.chartArea;
|
|
2762
|
-
const yAxis = getYAxis(chartInstance);
|
|
2763
|
-
const beginPoint = chartInstance.zoom._dragZoomStart;
|
|
2764
|
-
const offsetX = beginPoint.target.getBoundingClientRect().left;
|
|
2765
|
-
const startX = Math.max(Math.min(beginPoint.clientX, event.clientX) - offsetX, chartArea.left);
|
|
2766
|
-
const endX = Math.min(Math.max(beginPoint.clientX, event.clientX) - offsetX, chartArea.right);
|
|
2767
|
-
const dragDistance = endX - startX;
|
|
2768
|
-
const chartDistance = chartArea.right - chartArea.left;
|
|
2769
|
-
const zoom = 1 + (chartDistance - dragDistance) / chartDistance;
|
|
2770
|
-
const centerX = chartArea.left + (startX - chartArea.left) / (zoom - 1);
|
|
2771
|
-
// Remove drag start and end before chart update to stop drawing selected area
|
|
2772
|
-
chartInstance.zoom._dragZoomStart = null;
|
|
2773
|
-
chartInstance.zoom._dragZoomEnd = null;
|
|
2774
|
-
if (dragDistance > 0) {
|
|
2775
|
-
doZoom(chartInstance, zoom, {
|
|
2776
|
-
x: centerX,
|
|
2777
|
-
y: (yAxis.bottom - yAxis.top) / 2,
|
|
2778
|
-
});
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
};
|
|
2782
|
-
// node.addEventListener('mouseup', chartInstance.zoom._mouseUpHandler);
|
|
2783
|
-
node.ownerDocument.addEventListener('mouseup', chartInstance.zoom._mouseUpHandler);
|
|
2784
|
-
}
|
|
2785
|
-
else {
|
|
2786
|
-
chartInstance.zoom._wheelHandler = function (event) {
|
|
2787
|
-
const rect = event.target.getBoundingClientRect();
|
|
2788
|
-
const offsetX = event.clientX - rect.left;
|
|
2789
|
-
const offsetY = event.clientY - rect.top;
|
|
2790
|
-
const center = {
|
|
2791
|
-
x: offsetX,
|
|
2792
|
-
y: offsetY
|
|
2793
|
-
};
|
|
2794
|
-
if (event.deltaY < 0) {
|
|
2795
|
-
doZoom(chartInstance, 1.1, center);
|
|
2796
|
-
}
|
|
2797
|
-
else {
|
|
2798
|
-
doZoom(chartInstance, 0.909, center);
|
|
2799
|
-
}
|
|
2800
|
-
// Prevent the event from triggering the default behavior (eg. Content scrolling).
|
|
2801
|
-
event.preventDefault();
|
|
2802
|
-
};
|
|
2803
|
-
node.addEventListener('wheel', chartInstance.zoom._wheelHandler);
|
|
2804
|
-
}
|
|
2805
|
-
if (Hammer) {
|
|
2806
|
-
const mc = new Hammer.Manager(node);
|
|
2807
|
-
mc.add(new Hammer.Pinch());
|
|
2808
|
-
mc.add(new Hammer.Pan({
|
|
2809
|
-
threshold: panThreshold
|
|
2810
|
-
}));
|
|
2811
|
-
// Hammer reports the total scaling. We need the incremental amount
|
|
2812
|
-
let currentPinchScaling;
|
|
2813
|
-
const handlePinch = function (e) {
|
|
2814
|
-
const diff = 1 / (currentPinchScaling) * e.scale;
|
|
2815
|
-
const rect = e.target.getBoundingClientRect();
|
|
2816
|
-
const offsetX = e.center.x - rect.left;
|
|
2817
|
-
const offsetY = e.center.y - rect.top;
|
|
2818
|
-
const center = {
|
|
2819
|
-
x: offsetX,
|
|
2820
|
-
y: offsetY
|
|
2821
|
-
};
|
|
2822
|
-
// fingers position difference
|
|
2823
|
-
const x = Math.abs(e.pointers[0].clientX - e.pointers[1].clientX);
|
|
2824
|
-
const y = Math.abs(e.pointers[0].clientY - e.pointers[1].clientY);
|
|
2825
|
-
// diagonal fingers will change both (xy) axes
|
|
2826
|
-
const p = x / y;
|
|
2827
|
-
let xy;
|
|
2828
|
-
if (p > 0.3 && p < 1.7) {
|
|
2829
|
-
xy = 'xy';
|
|
2830
|
-
}
|
|
2831
|
-
else if (x > y) {
|
|
2832
|
-
// x axis
|
|
2833
|
-
xy = 'x';
|
|
2834
|
-
}
|
|
2835
|
-
else {
|
|
2836
|
-
// y axis
|
|
2837
|
-
xy = 'y';
|
|
2838
|
-
}
|
|
2839
|
-
doZoom(chartInstance, diff, center, xy);
|
|
2840
|
-
// Keep track of overall scale
|
|
2841
|
-
currentPinchScaling = e.scale;
|
|
2842
|
-
};
|
|
2843
|
-
mc.on('pinchstart', function (e) {
|
|
2844
|
-
currentPinchScaling = 1; // reset tracker
|
|
2845
|
-
});
|
|
2846
|
-
mc.on('pinch', handlePinch);
|
|
2847
|
-
mc.on('pinchend', function (e) {
|
|
2848
|
-
handlePinch(e);
|
|
2849
|
-
currentPinchScaling = null; // reset
|
|
2850
|
-
zoomNS.zoomCumulativeDelta = 0;
|
|
2851
|
-
});
|
|
2852
|
-
let currentDeltaX = null, currentDeltaY = null, panning = false;
|
|
2853
|
-
const handlePan = function (e) {
|
|
2854
|
-
if (currentDeltaX !== null && currentDeltaY !== null) {
|
|
2855
|
-
panning = true;
|
|
2856
|
-
const deltaX = e.deltaX - currentDeltaX;
|
|
2857
|
-
const deltaY = e.deltaY - currentDeltaY;
|
|
2858
|
-
currentDeltaX = e.deltaX;
|
|
2859
|
-
currentDeltaY = e.deltaY;
|
|
2860
|
-
doPan(chartInstance, deltaX, deltaY);
|
|
2861
|
-
}
|
|
2862
|
-
};
|
|
2863
|
-
mc.on('panstart', function (e) {
|
|
2864
|
-
currentDeltaX = 0;
|
|
2865
|
-
currentDeltaY = 0;
|
|
2866
|
-
handlePan(e);
|
|
2867
|
-
});
|
|
2868
|
-
mc.on('panmove', handlePan);
|
|
2869
|
-
mc.on('panend', function (e) {
|
|
2870
|
-
currentDeltaX = null;
|
|
2871
|
-
currentDeltaY = null;
|
|
2872
|
-
zoomNS.panCumulativeDelta = 0;
|
|
2873
|
-
setTimeout(function () { panning = false; }, 500);
|
|
2874
|
-
});
|
|
2875
|
-
chartInstance.zoom._ghostClickHandler = function (e) {
|
|
2876
|
-
if (panning) {
|
|
2877
|
-
e.stopImmediatePropagation();
|
|
2878
|
-
e.preventDefault();
|
|
2879
|
-
}
|
|
2880
|
-
};
|
|
2881
|
-
node.addEventListener('click', chartInstance.zoom._ghostClickHandler);
|
|
2882
|
-
chartInstance._mc = mc;
|
|
2883
|
-
}
|
|
2884
|
-
},
|
|
2885
|
-
beforeDatasetsDraw: function (chartInstance) {
|
|
2886
|
-
const ctx = chartInstance.chart.ctx;
|
|
2887
|
-
const chartArea = chartInstance.chartArea;
|
|
2888
|
-
ctx.save();
|
|
2889
|
-
ctx.beginPath();
|
|
2890
|
-
if (chartInstance.zoom._dragZoomEnd) {
|
|
2891
|
-
const yAxis = getYAxis(chartInstance);
|
|
2892
|
-
const beginPoint = chartInstance.zoom._dragZoomStart;
|
|
2893
|
-
const endPoint = chartInstance.zoom._dragZoomEnd;
|
|
2894
|
-
const offsetX = beginPoint.target.getBoundingClientRect().left;
|
|
2895
|
-
const startX = Math.min(beginPoint.clientX, endPoint.clientX) - offsetX;
|
|
2896
|
-
const endX = Math.max(beginPoint.clientX, endPoint.clientX) - offsetX;
|
|
2897
|
-
const rectWidth = endX - startX;
|
|
2898
|
-
ctx.fillStyle = 'rgba(225,225,225,0.5)';
|
|
2899
|
-
ctx.lineWidth = 5;
|
|
2900
|
-
ctx.fillRect(startX, yAxis.top, rectWidth, yAxis.bottom - yAxis.top);
|
|
2901
|
-
}
|
|
2902
|
-
ctx.rect(chartArea.left, chartArea.top, chartArea.right - chartArea.left, chartArea.bottom - chartArea.top);
|
|
2903
|
-
ctx.clip();
|
|
2904
|
-
},
|
|
2905
|
-
afterDatasetsDraw: function (chartInstance) {
|
|
2906
|
-
chartInstance.chart.ctx.restore();
|
|
2907
|
-
},
|
|
2908
|
-
destroy: function (chartInstance) {
|
|
2909
|
-
if (chartInstance.zoom) {
|
|
2910
|
-
const options = chartInstance.options;
|
|
2911
|
-
const node = chartInstance.zoom.node;
|
|
2912
|
-
if (options.zoom && options.zoom.drag) {
|
|
2913
|
-
node.removeEventListener('mousedown', chartInstance.zoom._mouseDownHandler);
|
|
2914
|
-
node.removeEventListener('mousemove', chartInstance.zoom._mouseMoveHandler);
|
|
2915
|
-
// node.removeEventListener('mouseup', chartInstance.zoom._mouseUpHandler);
|
|
2916
|
-
node.ownerDocument.removeEventListener('mouseup', chartInstance.zoom._mouseUpHandler);
|
|
2917
|
-
}
|
|
2918
|
-
else {
|
|
2919
|
-
node.removeEventListener('wheel', chartInstance.zoom._wheelHandler);
|
|
2920
|
-
}
|
|
2921
|
-
if (Hammer) {
|
|
2922
|
-
node.removeEventListener('click', chartInstance.zoom._ghostClickHandler);
|
|
2923
|
-
}
|
|
2924
|
-
delete chartInstance.zoom;
|
|
2925
|
-
const mc = chartInstance._mc;
|
|
2926
|
-
if (mc) {
|
|
2927
|
-
mc.remove('pinchstart');
|
|
2928
|
-
mc.remove('pinch');
|
|
2929
|
-
mc.remove('pinchend');
|
|
2930
|
-
mc.remove('panstart');
|
|
2931
|
-
mc.remove('pan');
|
|
2932
|
-
mc.remove('panend');
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
};
|
|
2937
|
-
Chart.pluginService.register(zoomPlugin);
|
|
2938
|
-
|
|
2939
1909
|
class FileDropInputComponent {
|
|
2940
1910
|
constructor() {
|
|
2941
1911
|
this.dropAreaText = '';
|