时序数据库 (TSDB) 存储高密度传感器数据组件 (模拟)
概述
这是一个交互式的 Web 组件,用于模拟从时序数据库 (Time Series Database, TSDB) 查询和展示发电厂高密度温度传感器历史数据的场景。用户可以选择不同的温度测点,定义一个时间范围,然后模拟执行查询,并查看结果的统计摘要、数据样本以及一个预留的趋势图区域。
请注意:这是一个前端模拟演示,它不连接任何真实的数据库,所有数据和查询延迟都是在浏览器端模拟生成的。
主要功能
- 模拟 TSDB 查询: 模拟针对高密度传感器数据的历史查询过程。
- 传感器选择: 提供多个预定义的发电厂温度测点供用户选择。
- 时间范围定义: 允许用户通过日期时间选择器指定查询的开始和结束时间。
- 查询执行与状态反馈:
- 点击按钮触发模拟查询。
- 显示查询状态(查询中、完成、无数据、失败)和加载指示器。
- 模拟网络和数据库处理的延迟。
- 结果展示:
- 统计摘要: 显示查询时间范围内数据的最小值、最大值、平均值和数据点总数。
- 数据样本: 以表格形式展示少量(首尾)查询到的数据点(时间戳和数值)。
- 趋势图占位符: 预留了用于展示温度变化趋势图的区域(当前未实现具体图表绘制)。
- 模拟数据归档: 右上角计数器持续增加,模拟传感器数据不断被写入 TSDB。
- 界面风格: 采用苹果科技风格,布局简洁,响应式设计。
如何使用
- 打开页面: 在浏览器中打开
index.html
。 - 查看默认状态: 页面加载后,会显示默认选中的测点和过去一小时的时间范围。
- 选择测点: 从"选择温度测点"下拉菜单中选择你感兴趣的传感器。
- 定义时间范围: 使用日期时间选择器设置查询的"开始时间"和"结束时间"。确保开始时间早于结束时间,否则"查询"按钮将不可用。
- 执行查询: 点击"查询历史数据"按钮。
- 观察结果:
- 查询状态会更新,并显示加载动画。
- 模拟延迟后,结果区域会显示所选测点的统计摘要和数据样本。
- 如果时间范围过短或模拟逻辑未生成数据,会提示"无数据"。
模拟细节
- 数据生成: 查询时,脚本会根据所选测点的预设参数(基础值、波动幅度、噪声)和时间范围,通过叠加正弦波(模拟日周期和小时周期)和随机噪声来动态生成模拟温度数据。每个测点的参数不同,以产生不同的数据模式。
- 归档计数: 右上角的"已归档数据点"是一个纯粹的视觉模拟,表示数据在持续写入,其增长速度与实际查询生成的数据量无关。
- 查询延迟: 查询按钮按下后,会有一个 0.5 到 2 秒的随机延迟,用以模拟真实查询所需的时间。
- 数据密度: 模拟生成数据时,尝试模拟约每 5 秒一个数据点,但查询返回的总点数上限为 5000 个,以避免浏览器性能问题。
文件结构
数据与物联网组件/tsdb-power-plant-archive/
├── index.html # 组件的 HTML 结构
├── styles.css # 组件的 CSS 样式
├── script.js # 组件的 JavaScript 逻辑(模拟查询、数据生成、交互)
└── README.md # 本说明文件
技术栈
- HTML5
- CSS3 (使用了 CSS 变量, Flexbox/Grid 布局)
- JavaScript (原生 JS, 无外部库依赖)
效果展示
源码
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TSDB - 发电厂历史温度归档</title>
<link rel="stylesheet" href="styles.css">
<!-- Placeholder for potential charting library -->
</head>
<body>
<div class="tsdb-container">
<header class="main-header">
<h1>时序数据库 - 发电厂温度数据归档模拟</h1>
<div class="header-info">
<span title="Simulated number of data points archived since page load">模拟已归档点数: <span id="archivedPointsCounter">0</span></span>
<span>时间: <span id="currentTime">--:--:--</span></span>
</div>
</header>
<main class="main-content">
<!-- Column 1: Sensor Selection & Query Params -->
<section class="query-params-section">
<h2><i class="icon icon-tag"></i> 查询参数</h2>
<div class="param-group">
<label for="sensorSelect"><i class="icon icon-sensor"></i> 选择温度测点:</label>
<select id="sensorSelect">
<option value="" disabled selected>-- 请选择测点 --</option>
<!-- Options populated by JS -->
</select>
</div>
<div class="param-group">
<label><i class="icon icon-time"></i> 选择时间范围:</label>
<div class="time-inputs">
<div>
<label for="startTime">开始时间:</label>
<input type="datetime-local" id="startTime" name="startTime">
</div>
<div>
<label for="endTime">结束时间:</label>
<input type="datetime-local" id="endTime" name="endTime">
</div>
</div>
</div>
<div class="param-group action-group">
<button id="queryButton" class="action-button" disabled><i class="icon icon-query"></i> 查询历史数据</button>
<div class="query-status">
状态: <span id="queryStatus" class="status-idle">空闲</span>
<span id="querySpinner" class="spinner" style="display: none;"></span>
</div>
</div>
</section>
<!-- Column 2: Query Results -->
<section class="results-section">
<h2><i class="icon icon-results"></i> 查询结果 (<span id="selectedSensorDisplay">未选择测点</span>)</h2>
<div id="resultsPlaceholder" class="placeholder-results">
请选择测点和时间范围,然后点击查询。
</div>
<div id="resultsContent" style="display: none;">
<div class="summary-stats">
<h3><i class="icon icon-stats"></i> 统计摘要</h3>
<div class="stats-grid">
<div class="stat-item"><span>最低温度 (°C):</span> <strong id="statMinTemp">--</strong></div>
<div class="stat-item"><span>最高温度 (°C):</span> <strong id="statMaxTemp">--</strong></div>
<div class="stat-item"><span>平均温度 (°C):</span> <strong id="statAvgTemp">--</strong></div>
<div class="stat-item"><span>模拟点数:</span> <strong id="statPointCount">--</strong></div>
</div>
</div>
<div class="sample-data">
<h3><i class="icon icon-table"></i> 数据抽样 (模拟)</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>时间戳</th>
<th>温度值 (°C)</th>
</tr>
</thead>
<tbody id="sampleDataTableBody">
<!-- Sample rows populated by JS -->
<tr><td colspan="2" class="placeholder">暂无数据</td></tr>
</tbody>
</table>
</div>
</div>
<div class="trend-chart">
<h3><i class="icon icon-chart"></i> 温度趋势 (模拟)</h3>
<div class="chart-placeholder" id="tempTrendChart">
<p>温度变化趋势图</p>
<div class="fake-chart tsdb-trend"></div>
</div>
</div>
</div>
</section>
</main>
<footer class="main-footer">
<p>© 2024 模拟 TSDB 查询系统. 概念演示.</p>
</footer>
</div>
<script src="script.js"></script>
</body>
</html>
styles.css
:root {
--background-color: #f0f2f5;
--container-bg: #ffffff;
--header-bg: #6c757d; /* Neutral grey header */
--header-text: #ffffff;
--section-border: #dee2e6;
--input-bg: #ffffff;
--input-border: #ced4da;
--input-focus-border: #86b7fe;
--input-focus-shadow: rgba(13, 110, 253, 0.25);
--button-bg: #0d6efd;
--button-hover-bg: #0b5ed7;
--button-disabled-bg: #adb5bd;
--text-primary: #212529;
--text-secondary: #6c757d;
--text-label: #495057;
--value-color: #000000;
--status-idle: var(--text-secondary);
--status-querying: #ffc107; /* Yellow */
--status-done: #198754; /* Green */
--status-error: #dc3545; /* Red */
--placeholder-bg: #f8f9fa;
--table-header-bg: #f1f3f5;
--table-row-hover-bg: #e9ecef;
--chart-placeholder-bg: #f8f9fa;
--spinner-color: var(--button-bg);
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--border-radius: 6px;
--box-shadow: 0 1px 3px rgba(0, 0, 0, 0.07);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
background-color: var(--background-color);
color: var(--text-primary);
line-height: 1.5;
overflow-x: hidden;
}
.tsdb-container {
max-width: 1400px;
margin: 1rem auto;
background-color: var(--container-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
overflow: hidden;
display: flex;
flex-direction: column;
min-height: calc(100vh - 2rem);
max-height: calc(100vh - 2rem);
height: 750px; /* Adjust height as needed */
}
/* Header */
.main-header {
background-color: var(--header-bg);
color: var(--header-text);
padding: 0.7rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.main-header h1 {
font-size: 1.25rem;
font-weight: 500;
}
.header-info span {
margin-left: 1.5rem;
font-size: 0.85rem;
opacity: 0.9;
}
/* Main Content Layout */
.main-content {
flex-grow: 1;
padding: 1rem;
overflow: hidden;
display: grid;
grid-template-columns: 400px 1fr; /* Fixed width for params, flexible results */
gap: 1rem;
}
/* Query Parameters Section */
.query-params-section {
background-color: #fdfdfd;
border: 1px solid var(--section-border);
border-radius: var(--border-radius);
padding: 1.2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
overflow-y: auto;
}
.query-params-section h2,
.results-section h2 {
font-size: 1.15rem;
font-weight: 500;
margin-bottom: 0.5rem;
padding-bottom: 0.6rem;
border-bottom: 1px solid var(--section-border);
display: flex;
align-items: center;
color: var(--text-primary);
flex-shrink: 0;
}
.query-params-section h2 .icon,
.results-section h2 .icon {
margin-right: 0.6rem;
font-size: 1.05em;
}
.param-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.param-group > label:first-child {
font-weight: 500;
color: var(--text-label);
font-size: 0.9rem;
display: flex;
align-items: center;
}
.param-group > label:first-child .icon {
margin-right: 0.4rem;
}
select,
input[type="datetime-local"] {
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
border: 1px solid var(--input-border);
border-radius: var(--border-radius);
background-color: var(--input-bg);
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
select:focus,
input[type="datetime-local"]:focus {
border-color: var(--input-focus-border);
outline: 0;
box-shadow: 0 0 0 0.2rem var(--input-focus-shadow);
}
.time-inputs {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding-left: 1.5rem; /* Indent time inputs */
}
.time-inputs div {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.time-inputs label {
font-size: 0.8rem;
color: var(--text-secondary);
}
.action-group {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px dashed var(--section-border);
align-items: center;
flex-direction: row;
justify-content: space-between;
}
.action-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
background-color: var(--button-bg);
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: var(--border-radius);
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
transition: background-color 0.2s ease;
flex-shrink: 0;
}
.action-button:hover:not(:disabled) {
background-color: var(--button-hover-bg);
}
.action-button:disabled {
background-color: var(--button-disabled-bg);
cursor: not-allowed;
}
.query-status {
font-size: 0.85rem;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 0.5rem;
}
#queryStatus {
font-weight: 500;
}
#queryStatus.status-idle { color: var(--status-idle); }
#queryStatus.status-querying { color: var(--status-querying); }
#queryStatus.status-done { color: var(--status-done); }
#queryStatus.status-error { color: var(--status-error); }
/* Spinner */
.spinner {
border: 3px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--spinner-color);
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Results Section */
.results-section {
background-color: #fdfdfd;
border: 1px solid var(--section-border);
border-radius: var(--border-radius);
padding: 1.2rem;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.results-section h2 #selectedSensorDisplay {
font-weight: normal;
color: var(--text-secondary);
font-size: 0.9em;
margin-left: 0.3rem;
}
.placeholder-results {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-style: italic;
text-align: center;
background-color: var(--placeholder-bg);
border-radius: var(--border-radius);
}
#resultsContent {
display: flex; /* Changed from none initially */
flex-direction: column;
gap: 1.5rem;
/* flex-grow: 1; */
}
.results-section h3 {
font-size: 1rem;
font-weight: 500;
margin-bottom: 0.8rem;
display: flex;
align-items: center;
color: var(--text-label);
}
.results-section h3 .icon {
margin-right: 0.5rem;
font-size: 1em;
}
/* Summary Stats */
.summary-stats {
background-color: var(--widget-bg);
padding: 1rem;
border-radius: var(--border-radius);
border: 1px solid var(--widget-border);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.5rem 1rem;
}
.stat-item {
font-size: 0.9rem;
}
.stat-item span {
color: var(--text-secondary);
margin-right: 0.4rem;
}
.stat-item strong {
font-weight: 600;
color: var(--value-color);
}
/* Sample Data Table */
.sample-data {
background-color: var(--widget-bg);
padding: 1rem;
border-radius: var(--border-radius);
border: 1px solid var(--widget-border);
}
.table-container {
max-height: 200px; /* Limit table height */
overflow-y: auto;
border: 1px solid var(--section-border);
border-radius: var(--border-radius);
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
background-color: var(--table-header-bg);
padding: 0.6rem 0.8rem;
text-align: left;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-secondary);
border-bottom: 1px solid var(--section-border);
position: sticky; /* Keep header visible */
top: 0;
}
tbody td {
padding: 0.5rem 0.8rem;
font-size: 0.85rem;
border-bottom: 1px solid var(--section-border);
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background-color: var(--table-row-hover-bg);
}
/* Trend Chart */
.trend-chart {
background-color: var(--widget-bg);
padding: 1rem;
border-radius: var(--border-radius);
border: 1px solid var(--widget-border);
}
.chart-placeholder {
background-color: var(--chart-placeholder-bg);
border-radius: var(--border-radius);
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid var(--widget-border);
min-height: 150px;
position: relative;
}
.chart-placeholder p {
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
font-weight: 500;
}
.fake-chart.tsdb-trend {
width: 95%;
height: 70%;
border: 1px dashed var(--text-secondary);
position: relative;
overflow: hidden;
}
.fake-chart.tsdb-trend::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to right, transparent, rgba(220, 53, 69, 0.1), rgba(255, 193, 7, 0.15), rgba(13, 110, 253, 0.1), transparent);
animation: fakeTrendPulse 8s ease-in-out infinite;
}
@keyframes fakeTrendPulse {
0% { transform: scaleY(0.5) translateY(30%); opacity: 0.4; }
50% { transform: scaleY(1) translateY(0); opacity: 0.7; }
100% { transform: scaleY(0.5) translateY(30%); opacity: 0.4; }
}
/* Footer */
.main-footer {
background-color: #f8f8f8;
padding: 0.5rem 1.5rem;
text-align: center;
font-size: 0.8rem;
color: var(--text-secondary);
border-top: 1px solid var(--section-border);
flex-shrink: 0;
}
/* Responsive Design */
@media (max-width: 992px) {
.main-content {
grid-template-columns: 1fr; /* Stack columns */
grid-template-rows: auto 1fr;
}
.query-params-section { grid-row: 1; }
.results-section { grid-row: 2; }
.tsdb-container {
max-height: none;
height: auto;
margin: 0.5rem;
}
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
}
}
@media (max-width: 768px) {
.main-content {
padding: 0.8rem;
gap: 0.8rem;
}
.query-params-section, .results-section {
padding: 1rem;
}
.action-group {
flex-direction: column;
align-items: stretch;
gap: 0.8rem;
}
.query-status {
justify-content: center;
margin-top: 0.5rem;
}
.main-header {
padding: 0.6rem 1rem;
}
.main-header h1 {
font-size: 1.1rem;
}
}
/* Basic Icon Placeholders */
.icon::before {
display: inline-block;
font-weight: normal;
font-style: normal;
font-variant: normal;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
margin-right: 0.3em;
}
.icon-tag::before { content: "🏷️"; }
.icon-sensor::before { content: "🌡️"; } /* Thermometer */
.icon-time::before { content: "⏱️"; }
.icon-query::before { content: "🔍"; }
.icon-results::before { content: "📊"; }
.icon-stats::before { content: "🔢"; }
.icon-table::before { content: "📋"; }
.icon-chart::before { content: "📈"; }
script.js
// script.js - TSDB Power Plant Archive Component
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Elements ---
const currentTimeEl = document.getElementById('currentTime');
const archivedPointsCounterEl = document.getElementById('archivedPointsCounter');
const sensorSelect = document.getElementById('sensorSelect');
const startTimeInput = document.getElementById('startTime');
const endTimeInput = document.getElementById('endTime');
const queryButton = document.getElementById('queryButton');
const queryStatusEl = document.getElementById('queryStatus');
const querySpinnerEl = document.getElementById('querySpinner');
const selectedSensorDisplayEl = document.getElementById('selectedSensorDisplay');
const resultsPlaceholderEl = document.getElementById('resultsPlaceholder');
const resultsContentEl = document.getElementById('resultsContent');
const statMinTempEl = document.getElementById('statMinTemp');
const statMaxTempEl = document.getElementById('statMaxTemp');
const statAvgTempEl = document.getElementById('statAvgTemp');
const statPointCountEl = document.getElementById('statPointCount');
const sampleDataTableBody = document.getElementById('sampleDataTableBody');
// Chart placeholder elements are not directly manipulated in this simulation
// --- Simulation State & Parameters ---
let simulationTime = new Date();
let archivedPoints = 0;
let isQuerying = false;
const sensors = {
'Boiler_ZoneA_Temp': { name: '锅炉区域A温度', base: 600, amplitude: 25, noise: 5 },
'Boiler_ZoneB_Temp': { name: '锅炉区域B温度', base: 650, amplitude: 30, noise: 6 },
'Turbine_Inlet_Temp': { name: '汽轮机入口温度', base: 550, amplitude: 15, noise: 3 },
'Condenser_Outlet_Temp': { name: '冷凝器出口温度', base: 45, amplitude: 5, noise: 1 },
'Feedwater_Heater_Temp': { name: '给水加热器温度', base: 180, amplitude: 10, noise: 2 }
};
// Update intervals
const timeUpdateInterval = 1000;
const archiveInterval = 100; // Simulate points archiving quickly
// --- Initialization ---
function initializeTSDBMonitor() {
populateSensorSelect();
setDefaultTimeRange();
setupEventListeners();
updateTime();
setInterval(updateTime, timeUpdateInterval);
// Start simulated archiving
setInterval(() => {
archivedPoints += Math.floor(Math.random() * 5 + 1); // Archive 1-5 points per interval
archivedPointsCounterEl.textContent = archivedPoints.toLocaleString();
}, archiveInterval);
validateInputs(); // Initial validation check
console.log("TSDB Monitor Initialized");
}
function populateSensorSelect() {
for (const tag in sensors) {
const option = document.createElement('option');
option.value = tag;
option.textContent = `${sensors[tag].name} (${tag})`;
sensorSelect.appendChild(option);
}
}
function setDefaultTimeRange() {
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
// Format for datetime-local input (YYYY-MM-DDTHH:mm)
endTimeInput.value = formatDateForInput(now);
startTimeInput.value = formatDateForInput(oneHourAgo);
}
function formatDateForInput(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
// --- Event Handlers ---
function setupEventListeners() {
sensorSelect.addEventListener('change', validateInputs);
startTimeInput.addEventListener('change', validateInputs);
endTimeInput.addEventListener('change', validateInputs);
queryButton.addEventListener('click', handleQuery);
}
function validateInputs() {
const sensorSelected = sensorSelect.value !== '';
const startTimeValid = startTimeInput.value !== '';
const endTimeValid = endTimeInput.value !== '';
let timeRangeValid = false;
if (startTimeValid && endTimeValid) {
const start = new Date(startTimeInput.value);
const end = new Date(endTimeInput.value);
timeRangeValid = start < end;
}
queryButton.disabled = !(sensorSelected && timeRangeValid);
return sensorSelected && timeRangeValid;
}
function handleQuery() {
if (isQuerying || !validateInputs()) {
return;
}
isQuerying = true;
setQueryStatus('querying', '查询中...');
resultsPlaceholderEl.style.display = 'none'; // Hide placeholder immediately
resultsContentEl.style.display = 'none'; // Hide old results
sampleDataTableBody.innerHTML = '<tr><td colspan="2" class="placeholder">生成数据中...</td></tr>'; // Clear table
const selectedTag = sensorSelect.value;
const sensorInfo = sensors[selectedTag];
selectedSensorDisplayEl.textContent = sensorInfo ? sensorInfo.name : '未知测点';
// Simulate network/DB delay
const queryDelay = Math.random() * 1500 + 500; // 0.5s to 2s delay
setTimeout(() => {
try {
const startTime = new Date(startTimeInput.value);
const endTime = new Date(endTimeInput.value);
const simulatedData = generateSimulatedData(sensorInfo, startTime, endTime);
if (simulatedData.length === 0) {
setQueryStatus('error', '无数据');
resultsPlaceholderEl.textContent = `在选定时间范围 (${startTime.toLocaleString()} - ${endTime.toLocaleString()}) 内,${sensorInfo.name} 无模拟数据。`;
resultsPlaceholderEl.style.display = 'block';
resultsContentEl.style.display = 'none';
} else {
processAndRenderResults(simulatedData);
setQueryStatus('done', '完成');
}
} catch (error) {
console.error("Query simulation error:", error);
setQueryStatus('error', '查询失败');
resultsPlaceholderEl.textContent = '模拟查询过程中发生错误。';
resultsPlaceholderEl.style.display = 'block';
resultsContentEl.style.display = 'none';
}
isQuerying = false;
}, queryDelay);
}
// --- Data Simulation & Processing ---
function generateSimulatedData(sensorInfo, startTime, endTime) {
const data = [];
const durationMillis = endTime.getTime() - startTime.getTime();
const pointsToGenerate = Math.min(5000, Math.floor(durationMillis / (1000 * 5))); // Simulate 1 point every 5 seconds, max 5000
if (pointsToGenerate <= 0) return [];
const timeStep = durationMillis / pointsToGenerate;
for (let i = 0; i < pointsToGenerate; i++) {
const timestamp = new Date(startTime.getTime() + i * timeStep);
// Simulate value based on a slow sine wave (daily cycle) + faster sine (hourly cycle) + noise
const timeFractionDay = (timestamp.getHours() * 3600 + timestamp.getMinutes() * 60 + timestamp.getSeconds()) / (24 * 3600);
const timeFractionHour = (timestamp.getMinutes() * 60 + timestamp.getSeconds()) / 3600;
const dailyCycle = Math.sin(timeFractionDay * 2 * Math.PI) * (sensorInfo.amplitude * 0.6);
const hourlyCycle = Math.sin(timeFractionHour * 2 * Math.PI) * (sensorInfo.amplitude * 0.3);
const noise = (Math.random() - 0.5) * sensorInfo.noise;
const value = sensorInfo.base + dailyCycle + hourlyCycle + noise;
data.push({ timestamp, value });
}
return data;
}
function processAndRenderResults(data) {
if (!data || data.length === 0) {
// Should be handled before calling this, but double-check
resultsContentEl.style.display = 'none';
resultsPlaceholderEl.textContent = '无数据显示。';
resultsPlaceholderEl.style.display = 'block';
return;
}
let minTemp = data[0].value;
let maxTemp = data[0].value;
let sumTemp = 0;
data.forEach(point => {
if (point.value < minTemp) minTemp = point.value;
if (point.value > maxTemp) maxTemp = point.value;
sumTemp += point.value;
});
const avgTemp = sumTemp / data.length;
// Update stats
statMinTempEl.textContent = minTemp.toFixed(1);
statMaxTempEl.textContent = maxTemp.toFixed(1);
statAvgTempEl.textContent = avgTemp.toFixed(1);
statPointCountEl.textContent = data.length.toLocaleString();
// Update sample data table (show first/last few points)
sampleDataTableBody.innerHTML = ''; // Clear previous samples
const sampleSize = 10;
const samples = data.slice(0, sampleSize / 2).concat(data.slice(-sampleSize / 2));
samples.forEach(point => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${point.timestamp.toLocaleString('zh-CN')}</td>
<td>${point.value.toFixed(2)}</td>
`;
sampleDataTableBody.appendChild(row);
});
// Show results section
resultsContentEl.style.display = 'flex'; // Use flex for column layout
resultsPlaceholderEl.style.display = 'none';
// In a real scenario, update the chart here
// e.g., updateChart(data);
}
// --- UI Updates ---
function setQueryStatus(statusType, statusText) {
queryStatusEl.textContent = statusText;
queryStatusEl.className = `status-${statusType}`;
querySpinnerEl.style.display = (statusType === 'querying') ? 'inline-block' : 'none';
}
// --- Utility Functions ---
function updateTime() {
simulationTime = new Date();
currentTimeEl.textContent = simulationTime.toLocaleTimeString('zh-CN');
}
// --- Initial Call ---
initializeTSDBMonitor();
});