705 lines
27 KiB
PHP
705 lines
27 KiB
PHP
<?php // phpcs:ignoreFile
|
|
|
|
require __DIR__ . '/../../session.php';
|
|
require __DIR__ . '/../../userAccess.php';
|
|
|
|
$pageTitle = 'Single Chart Trending';
|
|
$pageSubtitle = 'Monitor tags in a 60 minute window';
|
|
$pageDescription = 'Launch the single chart beta to track critical process data in real time.';
|
|
$assetBasePath = '../../';
|
|
$layoutWithoutSidebar = true;
|
|
$layoutReturnUrl = null;
|
|
$layoutCloseWindowLabel = 'Close chart';
|
|
|
|
if (!function_exists('trendsSanitizePresetTag')) {
|
|
/**
|
|
* Normalize preset tag values supplied through the query string.
|
|
*
|
|
* @param mixed $value Raw query parameter value.
|
|
*
|
|
* @return string
|
|
*/
|
|
function trendsSanitizePresetTag($value): string
|
|
{
|
|
if ($value === null) {
|
|
return '';
|
|
}
|
|
|
|
$tag = trim((string) $value);
|
|
if ($tag === '') {
|
|
return '';
|
|
}
|
|
|
|
return substr($tag, 0, 255);
|
|
}
|
|
}
|
|
|
|
$presetPrimaryTag = trendsSanitizePresetTag($_GET['primary'] ?? null);
|
|
$presetSecondaryTag = trendsSanitizePresetTag($_GET['secondary'] ?? null);
|
|
$presetPrimaryLabel = trendsSanitizePresetTag($_GET['primary_label'] ?? null);
|
|
$presetSecondaryLabel = trendsSanitizePresetTag($_GET['secondary_label'] ?? null);
|
|
|
|
$presetAutoStart = false;
|
|
if (isset($_GET['autostart'])) {
|
|
$autoStartValue = strtolower((string) $_GET['autostart']);
|
|
$presetAutoStart = in_array($autoStartValue, ['1', 'true', 'yes', 'on'], true);
|
|
}
|
|
|
|
$allowedIntervals = [1000, 5000, 10000, 30000];
|
|
$presetUpdateInterval = null;
|
|
if (isset($_GET['interval'])) {
|
|
$candidateInterval = (int) $_GET['interval'];
|
|
if (in_array($candidateInterval, $allowedIntervals, true)) {
|
|
$presetUpdateInterval = $candidateInterval;
|
|
}
|
|
}
|
|
|
|
$presetConfig = [
|
|
'primary' => $presetPrimaryTag,
|
|
'secondary' => $presetSecondaryTag,
|
|
'autoStart' => $presetAutoStart,
|
|
'updateInterval' => $presetUpdateInterval,
|
|
'primaryLabel' => $presetPrimaryLabel,
|
|
'secondaryLabel' => $presetSecondaryLabel,
|
|
];
|
|
|
|
require __DIR__ . '/../../includes/layout/header.php';
|
|
?>
|
|
|
|
<div class="app-content">
|
|
<section class="data-panel trend-panel">
|
|
<header class="trend-panel__header">
|
|
<div>
|
|
<h2 id="chartTitle">Real-time process trend</h2>
|
|
<p>This chart automatically trends the tag supplied via the link—adjust the refresh rate below and hit Start when you're ready.</p>
|
|
</div>
|
|
<span class="trend-status trend-status--stopped" id="chartStatus">STOPPED</span>
|
|
</header>
|
|
|
|
<div class="trend-control-grid">
|
|
<div class="trend-control">
|
|
<label>Primary tag</label>
|
|
<div id="primaryTagDisplay">--</div>
|
|
</div>
|
|
<div class="trend-control" id="secondaryTagContainer" style="display: none;">
|
|
<label>Secondary tag</label>
|
|
<div id="secondaryTagDisplay">--</div>
|
|
</div>
|
|
<div class="trend-control">
|
|
<label for="updateInterval">Update interval</label>
|
|
<select id="updateInterval">
|
|
<option value="1000">Every 1 second</option>
|
|
<option value="5000" selected>Every 5 seconds</option>
|
|
<option value="10000">Every 10 seconds</option>
|
|
<option value="30000">Every 30 seconds</option>
|
|
</select>
|
|
</div>
|
|
<div class="trend-control">
|
|
<label>Time window</label>
|
|
<div class="time-window-display">Last 60 minutes (sliding)</div>
|
|
</div>
|
|
<div class="trend-control">
|
|
<label for="startBtn">Controls</label>
|
|
<div class="trend-control__actions">
|
|
<button type="button" class="button button--success" id="startBtn" onclick="startRealTime()">Start</button>
|
|
<button type="button" class="button button--danger" id="stopBtn" onclick="stopRealTime()" disabled>Stop</button>
|
|
<button type="button" class="button button--ghost" onclick="clearChart()">Clear</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="status" class="trend-connection trend-connection--disconnected">
|
|
Disconnected — Click Start to begin trending the configured tag
|
|
</div>
|
|
|
|
<div class="trend-stat-grid">
|
|
<div class="trend-stat">
|
|
<span id="currentValue1" class="trend-stat__value">--</span>
|
|
<span class="trend-stat__label" id="tag1Label">Primary current</span>
|
|
</div>
|
|
<div class="trend-stat">
|
|
<span id="minValue1" class="trend-stat__value">--</span>
|
|
<span class="trend-stat__label">Primary min</span>
|
|
</div>
|
|
<div class="trend-stat">
|
|
<span id="maxValue1" class="trend-stat__value">--</span>
|
|
<span class="trend-stat__label">Primary max</span>
|
|
</div>
|
|
<div class="trend-stat">
|
|
<span id="dataPoints" class="trend-stat__value">0</span>
|
|
<span class="trend-stat__label">Total points</span>
|
|
</div>
|
|
<div class="trend-stat">
|
|
<span id="lastUpdate" class="trend-stat__value">--</span>
|
|
<span class="trend-stat__label">Last update</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="trend-chart">
|
|
<canvas id="realtimeChart"></canvas>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@1.0.1/dist/chartjs-adapter-moment.min.js"></script>
|
|
|
|
<script>
|
|
const presetConfig = <?php echo json_encode($presetConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
|
|
const LATENCY_PREFIX = 'latency:';
|
|
let chart = null;
|
|
let updateTimer = null;
|
|
let isRunning = false;
|
|
let lastTimestamp1 = null;
|
|
let lastTimestamp2 = null;
|
|
let chartData1 = [];
|
|
let chartData2 = [];
|
|
let timeWindowMinutes = 60;
|
|
let primaryTag = '';
|
|
let secondaryTag = '';
|
|
let primaryLabel = '';
|
|
let secondaryLabel = '';
|
|
let presetApplied = false;
|
|
|
|
function applyPresetConfiguration() {
|
|
if (presetApplied) {
|
|
return;
|
|
}
|
|
|
|
primaryTag = presetConfig.primary || '';
|
|
secondaryTag = presetConfig.secondary || '';
|
|
primaryLabel = presetConfig.primaryLabel || primaryTag;
|
|
secondaryLabel = presetConfig.secondaryLabel || secondaryTag;
|
|
|
|
const primaryDisplay = document.getElementById('primaryTagDisplay');
|
|
const secondaryDisplay = document.getElementById('secondaryTagDisplay');
|
|
const secondaryContainer = document.getElementById('secondaryTagContainer');
|
|
const intervalSelect = document.getElementById('updateInterval');
|
|
const startButton = document.getElementById('startBtn');
|
|
|
|
if (primaryDisplay) {
|
|
primaryDisplay.textContent = primaryLabel || primaryTag || '--';
|
|
}
|
|
|
|
if (secondaryDisplay && secondaryContainer) {
|
|
if (secondaryTag) {
|
|
secondaryDisplay.textContent = secondaryLabel || secondaryTag;
|
|
secondaryContainer.style.display = '';
|
|
} else {
|
|
secondaryDisplay.textContent = '--';
|
|
secondaryContainer.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
if (intervalSelect && presetConfig.updateInterval) {
|
|
intervalSelect.value = String(presetConfig.updateInterval);
|
|
}
|
|
|
|
if (startButton) {
|
|
startButton.disabled = !primaryTag;
|
|
}
|
|
|
|
updateChartTitle();
|
|
|
|
if (!primaryTag) {
|
|
updateStatus('disconnected', 'No primary tag supplied. Append ?primary=TAG to the link.');
|
|
}
|
|
|
|
if (presetConfig.autoStart && primaryTag) {
|
|
setTimeout(() => {
|
|
if (!isRunning) {
|
|
startRealTime();
|
|
}
|
|
}, 200);
|
|
}
|
|
|
|
presetApplied = true;
|
|
}
|
|
|
|
// Initialize Chart.js with dual datasets
|
|
function initChart() {
|
|
const ctx = document.getElementById('realtimeChart').getContext('2d');
|
|
|
|
chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
datasets: [{
|
|
label: 'Primary Tag',
|
|
data: [],
|
|
borderColor: '#3498db',
|
|
backgroundColor: 'rgba(52, 152, 219, 0.1)',
|
|
borderWidth: 2,
|
|
fill: false,
|
|
tension: 0.1,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 0,
|
|
hitRadius: 0,
|
|
yAxisID: 'y1'
|
|
}, {
|
|
label: 'Secondary Tag',
|
|
data: [],
|
|
borderColor: '#27ae60',
|
|
backgroundColor: 'rgba(39, 174, 96, 0.1)',
|
|
borderWidth: 2,
|
|
fill: false,
|
|
tension: 0.1,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 0,
|
|
hitRadius: 0,
|
|
yAxisID: 'y2',
|
|
hidden: true
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: {
|
|
duration: 300
|
|
},
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'index'
|
|
},
|
|
scales: {
|
|
x: {
|
|
type: 'time',
|
|
time: {
|
|
unit: 'minute',
|
|
displayFormats: {
|
|
second: 'HH:mm:ss',
|
|
minute: 'HH:mm',
|
|
hour: 'HH:mm'
|
|
}
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: 'Time (Last 60 Minutes)',
|
|
font: { size: 12, weight: 'bold' }
|
|
},
|
|
min: function() {
|
|
return moment().subtract(timeWindowMinutes, 'minutes').toDate();
|
|
},
|
|
max: function() {
|
|
return moment().toDate();
|
|
},
|
|
grid: {
|
|
color: 'rgba(0,0,0,0.1)'
|
|
}
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'left',
|
|
title: {
|
|
display: true,
|
|
text: 'Primary Tag Value',
|
|
color: '#3498db',
|
|
font: { size: 12, weight: 'bold' }
|
|
},
|
|
grid: {
|
|
drawOnChartArea: true,
|
|
color: 'rgba(52, 152, 219, 0.1)'
|
|
},
|
|
ticks: {
|
|
color: '#3498db',
|
|
font: { weight: 'bold' }
|
|
}
|
|
},
|
|
y2: {
|
|
type: 'linear',
|
|
display: false,
|
|
position: 'right',
|
|
title: {
|
|
display: true,
|
|
text: 'Secondary Tag Value',
|
|
color: '#27ae60',
|
|
font: { size: 12, weight: 'bold' }
|
|
},
|
|
grid: {
|
|
drawOnChartArea: false,
|
|
},
|
|
ticks: {
|
|
color: '#27ae60',
|
|
font: { weight: 'bold' }
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
font: { size: 12, weight: 'bold' },
|
|
usePointStyle: true,
|
|
padding: 20
|
|
},
|
|
onClick: function(e, legendItem, legend) {
|
|
const index = legendItem.datasetIndex;
|
|
const ci = legend.chart;
|
|
const dataset = ci.data.datasets[index];
|
|
|
|
dataset.hidden = !dataset.hidden;
|
|
|
|
if (index === 1) {
|
|
ci.options.scales.y2.display = !dataset.hidden;
|
|
}
|
|
|
|
ci.update();
|
|
}
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(44, 62, 80, 0.9)',
|
|
titleColor: '#3498db',
|
|
bodyColor: '#ecf0f1',
|
|
borderColor: '#3498db',
|
|
borderWidth: 1,
|
|
titleFont: { size: 13, weight: 'bold' },
|
|
bodyFont: { size: 12 },
|
|
callbacks: {
|
|
title: function(context) {
|
|
return moment(context[0].parsed.x).format('YYYY-MM-DD HH:mm:ss');
|
|
},
|
|
afterTitle: function(context) {
|
|
const now = moment();
|
|
const pointTime = moment(context[0].parsed.x);
|
|
const secondsAgo = now.diff(pointTime, 'seconds');
|
|
return `(${secondsAgo} seconds ago)`;
|
|
},
|
|
label: function(context) {
|
|
const datasetLabel = context.dataset.label;
|
|
const rawValue = context.parsed.y;
|
|
if (rawValue === null || Number.isNaN(rawValue)) {
|
|
return `${datasetLabel}: --`;
|
|
}
|
|
const value = Number(rawValue).toFixed(2);
|
|
return `${datasetLabel}: ${value}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}); // This closing brace and parenthesis were missing!
|
|
}
|
|
|
|
// Fetch data for one or both tags
|
|
async function fetchTagData(tagName, datasetIndex) {
|
|
if (!tagName) return null;
|
|
|
|
try {
|
|
if (tagName.toLowerCase().startsWith(LATENCY_PREFIX)) {
|
|
return await fetchLatencyData(tagName);
|
|
}
|
|
|
|
const endTime = moment().format('YYYY-MM-DD HH:mm:ss');
|
|
const startTime = moment().subtract(timeWindowMinutes, 'minutes').format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
let url = `realtime_data.php?tag=${encodeURIComponent(tagName)}&limit=1500&start_time=${encodeURIComponent(startTime)}&end_time=${encodeURIComponent(endTime)}`;
|
|
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.data && data.data.length > 0) {
|
|
const formattedData = data.data.map(point => ({
|
|
x: new Date(point.x),
|
|
y: point.y
|
|
}));
|
|
|
|
const cutoffTime = moment().subtract(timeWindowMinutes, 'minutes').toDate();
|
|
return formattedData.filter(point => point.x >= cutoffTime);
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error(`Error fetching data for ${tagName}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function fetchLatencyData(tagToken) {
|
|
const deviceId = tagToken.substring(LATENCY_PREFIX.length);
|
|
if (!deviceId) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
device_id: deviceId,
|
|
minutes: String(timeWindowMinutes),
|
|
limit: '2000'
|
|
});
|
|
|
|
const response = await fetch(`../../monitoring/latency_data.php?${params.toString()}`, {
|
|
cache: 'no-store'
|
|
});
|
|
const payload = await response.json();
|
|
|
|
if (payload.success && Array.isArray(payload.data)) {
|
|
return payload.data.map(point => ({
|
|
x: new Date(point.x),
|
|
y: point.y !== null ? Number(point.y) : null,
|
|
status: point.status || null
|
|
}));
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error('Latency data fetch error:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Modified fetchData for dual tags
|
|
async function fetchData() {
|
|
const tagName1 = primaryTag;
|
|
const tagName2 = secondaryTag;
|
|
|
|
if (!tagName1) {
|
|
updateStatus('disconnected', 'No primary tag supplied. Append ?primary=TAG to the link.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data1 = await fetchTagData(tagName1, 0);
|
|
|
|
if (data1 !== null) {
|
|
chartData1 = data1;
|
|
chart.data.datasets[0].data = chartData1;
|
|
chart.data.datasets[0].label = primaryLabel || tagName1;
|
|
}
|
|
|
|
if (tagName2 && tagName2 !== tagName1) {
|
|
const data2 = await fetchTagData(tagName2, 1);
|
|
|
|
if (data2 !== null) {
|
|
chartData2 = data2;
|
|
chart.data.datasets[1].data = chartData2;
|
|
chart.data.datasets[1].label = secondaryLabel || tagName2;
|
|
chart.data.datasets[1].hidden = false;
|
|
chart.options.scales.y2.display = true;
|
|
}
|
|
} else {
|
|
chartData2 = [];
|
|
chart.data.datasets[1].data = [];
|
|
chart.data.datasets[1].hidden = true;
|
|
chart.options.scales.y2.display = false;
|
|
}
|
|
|
|
chart.options.scales.x.min = moment().subtract(timeWindowMinutes, 'minutes').toDate();
|
|
chart.options.scales.x.max = moment().toDate();
|
|
|
|
const isLatencyPrimary = tagName1.toLowerCase().startsWith(LATENCY_PREFIX);
|
|
chart.options.scales.y1.title.text = isLatencyPrimary ? 'Latency (ms)' : 'Primary Tag Value';
|
|
|
|
chart.update('active');
|
|
|
|
updateStatus('connected', `Connected - Last update: ${new Date().toLocaleTimeString()}`);
|
|
updateStatistics();
|
|
updateChartTitle();
|
|
|
|
} catch (error) {
|
|
console.error('Fetch error:', error);
|
|
updateStatus('disconnected', `Connection error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Update chart title
|
|
function updateChartTitle() {
|
|
let title = 'Real-Time Process Trend';
|
|
if (primaryTag) {
|
|
title = primaryLabel || primaryTag;
|
|
if (secondaryTag) {
|
|
const secondaryDisplayLabel = secondaryLabel || secondaryTag;
|
|
title += ` & ${secondaryDisplayLabel}`;
|
|
}
|
|
}
|
|
|
|
document.getElementById('chartTitle').textContent = title;
|
|
}
|
|
|
|
// Enhanced statistics for dual tags
|
|
function updateStatistics() {
|
|
const tag1LabelEl = document.getElementById('tag1Label');
|
|
if (tag1LabelEl) {
|
|
const label = primaryLabel || primaryTag;
|
|
tag1LabelEl.textContent = label ? `${label} Current` : 'Primary Current';
|
|
}
|
|
|
|
const currentValue1El = document.getElementById('currentValue1');
|
|
const minValue1El = document.getElementById('minValue1');
|
|
const maxValue1El = document.getElementById('maxValue1');
|
|
|
|
const numericValues1 = chartData1
|
|
.map(point => point.y)
|
|
.filter(value => typeof value === 'number' && Number.isFinite(value));
|
|
const lastPoint1 = chartData1.length > 0 ? chartData1[chartData1.length - 1] : null;
|
|
|
|
if (currentValue1El) {
|
|
if (lastPoint1) {
|
|
if (lastPoint1.status && lastPoint1.status !== 'up') {
|
|
currentValue1El.textContent = lastPoint1.status.toUpperCase();
|
|
} else if (typeof lastPoint1.y === 'number' && Number.isFinite(lastPoint1.y)) {
|
|
currentValue1El.textContent = lastPoint1.y.toFixed(2);
|
|
} else {
|
|
currentValue1El.textContent = '--';
|
|
}
|
|
} else {
|
|
currentValue1El.textContent = '--';
|
|
}
|
|
}
|
|
|
|
if (minValue1El) {
|
|
minValue1El.textContent = numericValues1.length > 0
|
|
? Math.min(...numericValues1).toFixed(2)
|
|
: '--';
|
|
}
|
|
|
|
if (maxValue1El) {
|
|
maxValue1El.textContent = numericValues1.length > 0
|
|
? Math.max(...numericValues1).toFixed(2)
|
|
: '--';
|
|
}
|
|
|
|
const totalPoints = chartData1.length + chartData2.length;
|
|
const dataPointsEl = document.getElementById('dataPoints');
|
|
if (dataPointsEl) {
|
|
dataPointsEl.textContent = totalPoints;
|
|
}
|
|
|
|
const lastUpdateEl = document.getElementById('lastUpdate');
|
|
if (lastUpdateEl) {
|
|
lastUpdateEl.textContent = moment().format('HH:mm:ss');
|
|
}
|
|
}
|
|
|
|
// Update status display
|
|
function updateStatus(type, message) {
|
|
const status = document.getElementById('status');
|
|
const chartStatus = document.getElementById('chartStatus');
|
|
const stateClass = type === 'connected' ? 'connected' : 'disconnected';
|
|
|
|
status.className = `trend-connection trend-connection--${stateClass}`;
|
|
status.textContent = message;
|
|
|
|
if (stateClass === 'connected') {
|
|
chartStatus.className = 'trend-status trend-status--running';
|
|
chartStatus.textContent = 'RUNNING';
|
|
} else {
|
|
chartStatus.className = 'trend-status trend-status--stopped';
|
|
chartStatus.textContent = 'STOPPED';
|
|
}
|
|
}
|
|
|
|
// Start real-time updates
|
|
function startRealTime() {
|
|
if (!primaryTag) {
|
|
alert('No primary tag configured for this chart.');
|
|
return;
|
|
}
|
|
|
|
isRunning = true;
|
|
lastTimestamp1 = null;
|
|
lastTimestamp2 = null;
|
|
|
|
const intervalSelect = document.getElementById('updateInterval');
|
|
const interval = intervalSelect ? parseInt(intervalSelect.value, 10) : 5000;
|
|
|
|
document.getElementById('startBtn').disabled = true;
|
|
document.getElementById('stopBtn').disabled = false;
|
|
|
|
fetchData();
|
|
|
|
updateTimer = setInterval(() => {
|
|
fetchData();
|
|
}, interval);
|
|
|
|
const primaryDisplayName = primaryLabel || primaryTag || 'Primary';
|
|
const secondaryDisplayName = secondaryLabel || secondaryTag;
|
|
|
|
const statusMessage = secondaryTag ?
|
|
`Trending ${primaryDisplayName} & ${secondaryDisplayName} - Update every ${interval/1000}s` :
|
|
`Trending ${primaryDisplayName} - Update every ${interval/1000}s`;
|
|
|
|
updateStatus('connected', statusMessage);
|
|
}
|
|
|
|
// Stop real-time updates
|
|
function stopRealTime() {
|
|
isRunning = false;
|
|
|
|
if (updateTimer) {
|
|
clearInterval(updateTimer);
|
|
updateTimer = null;
|
|
}
|
|
|
|
document.getElementById('startBtn').disabled = !primaryTag;
|
|
document.getElementById('stopBtn').disabled = true;
|
|
|
|
updateStatus('disconnected', 'Real-time updates stopped - Click Start to resume');
|
|
}
|
|
|
|
// Clear chart data
|
|
function clearChart() {
|
|
chartData1 = [];
|
|
chartData2 = [];
|
|
lastTimestamp1 = null;
|
|
lastTimestamp2 = null;
|
|
|
|
if (chart) {
|
|
chart.data.datasets[0].data = [];
|
|
chart.data.datasets[1].data = [];
|
|
chart.data.datasets[1].hidden = true;
|
|
chart.options.scales.y2.display = false;
|
|
chart.update();
|
|
}
|
|
|
|
// Reset all statistics
|
|
const currentValue1El = document.getElementById('currentValue1');
|
|
const minValue1El = document.getElementById('minValue1');
|
|
const maxValue1El = document.getElementById('maxValue1');
|
|
const dataPointsEl = document.getElementById('dataPoints');
|
|
const lastUpdateEl = document.getElementById('lastUpdate');
|
|
|
|
if (currentValue1El) currentValue1El.textContent = '--';
|
|
if (minValue1El) minValue1El.textContent = '--';
|
|
if (maxValue1El) maxValue1El.textContent = '--';
|
|
if (dataPointsEl) dataPointsEl.textContent = '0';
|
|
if (lastUpdateEl) lastUpdateEl.textContent = '--';
|
|
|
|
// Reset title
|
|
document.getElementById('chartTitle').textContent = primaryTag || 'Real-Time Process Trend';
|
|
}
|
|
|
|
// Handle interval changes
|
|
function setupIntervalListener() {
|
|
const intervalSelect = document.getElementById('updateInterval');
|
|
if (!intervalSelect) {
|
|
return;
|
|
}
|
|
|
|
intervalSelect.addEventListener('change', function() {
|
|
if (isRunning) {
|
|
stopRealTime();
|
|
setTimeout(startRealTime, 100);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initChart();
|
|
setupIntervalListener();
|
|
applyPresetConfiguration();
|
|
});
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', function() {
|
|
if (updateTimer) {
|
|
clearInterval(updateTimer);
|
|
}
|
|
});
|
|
|
|
</script>
|
|
|
|
<?php require __DIR__ . '/../../includes/layout/footer.php'; ?>
|