当前位置: 首页 > news >正文

23、电网数据管理与智能分析 - 负载预测模拟 - /能源管理组件/grid-data-smart-analysis

76个工业组件库示例汇总

电网数据管理与智能分析组件

1. 组件概述

本组件旨在模拟一个城市配电网的运行状态,重点关注数据管理、可视化以及基于模拟数据的智能分析,特别是负载预测功能。用户可以通过界面交互式地探索电网拓扑、查看节点状态、控制时间演进,并观察系统生成的负载预测和相关告警。

设计风格遵循苹果科技工业美学,力求界面清晰、交互流畅、信息直观。

2. 主要功能

  • 实时概览指标: 顶部显示当前电网总负载 (MW)、未来24小时预测峰值负载 (MW) 以及一个概念性的电网稳定指数。
  • 电网拓扑可视化: 左侧区域使用简化的图形展示变电站和馈线的连接关系。节点和连接线的颜色会根据实时模拟的负载状态(低、正常、高、过载、离线)动态变化。
  • 时间演进控制: 用户可以播放/暂停模拟时间的流逝,调整模拟速度(1x, 5x, 10x, 30x),或将时间重置到初始状态。
  • 负载预测图表: 右上侧使用 Chart.js 图表展示选中节点(默认为总负载或首个馈线,点击拓扑图节点切换)的负载曲线,包括过去几小时的历史负载和未来24小时的预测负载。
  • 节点详细数据: 点击左侧拓扑图中的节点(变电站或馈线),右侧中间面板会显示该节点的详细信息,如ID、名称、类型、当前负载、电压、容量(如有)、状态和预测峰值。
  • 智能分析与告警: 右下侧面板根据当前的电网状态和预测结果,自动生成告警信息(如节点当前过载)和预警信息(如预测到未来几小时内可能发生过载),以及基于稳定指数的建议。
  • 响应式布局: 界面适应不同宽度的浏览器窗口,在中小型屏幕上会自动调整布局,并控制整体高度防止内容过长。

3. 技术栈

  • HTML5
  • CSS3 (Flexbox, Grid, CSS Variables, Media Queries)
  • JavaScript (ES6+)
  • Chart.js (用于绘制图表)
  • Day.js (用于日期和时间处理)
  • chartjs-adapter-dayjs (Chart.js 的 Day.js 适配器)

4. 运行与使用

  1. grid-data-smart-analysis 文件夹放置在 能源管理组件 目录下。
  2. 在支持 HTML5 和 JavaScript 的浏览器中打开 index.html 文件。
  3. 组件加载后,模拟处于暂停状态,显示初始电网拓扑和数据。
  4. 点击左下角的"播放"按钮 (▶️) 开始模拟时间的演进,观察拓扑图颜色、图表和告警信息的变化。
  5. 使用"暂停" (⏸️)、"速度"下拉框和"重置"按钮控制模拟进程。
  6. 点击左侧拓扑图中的任意节点(圆形代表馈线,方形代表变电站)来查看该节点的详细数据和负载预测曲线。

5. 模拟逻辑说明

  • 电网拓扑: 在 script.js 中定义了一个包含节点(变电站、馈线)和连接关系的简化电网结构。节点位置使用百分比定义,以便在不同尺寸下绘制。
  • 负载模式: 每个"馈线"节点预定义了一个24小时的基础负载曲线 (baseLoad) 和一个周末负载系数 (weekendMultiplier)。模拟器根据当前模拟时间(小时和星期几)来计算基础负载。
  • 负载计算: 节点的实际负载 = 基础负载 * (1 +/- 随机波动%)。
  • 变电站负载: 简单设定为其所连接的所有下游节点(馈线或其他变电站)的负载之和。
  • 负载状态: 根据节点当前负载与其容量 (capacity) 的比例,判定为低、正常、高或过载状态。
  • 电压模拟: 仅模拟小幅度的随机波动,未与负载严格关联。
  • 负载预测: 高度简化。对于馈线,基于其未来的基础负载模式进行预测,并加入微小波动。对于变电站,预测负载为其所连接馈线的预测负载之和。
  • 总负载/峰值预测: 当前总负载为所有馈线负载之和;预测峰值为所有馈线预测负载在未来24小时内的最大总和。
  • 稳定指数: 基于当前过载和高负载节点的数量计算出的概念性分数。
  • 告警/预警: 基于当前节点是否过载,以及预测负载是否会超过节点容量来生成。

6. 注意事项

  • 这是一个高度简化的概念性模拟,其电网拓扑、负载模型、电压模拟和特别是负载预测算法都与实际电力系统工程相去甚远。
  • 主要目的是演示一个集成化的电网数据监控与分析界面的设计思路、交互方式和数据可视化效果。
  • 所有数据均为程序生成,不代表任何真实的电网运行数据。

效果展示

在这里插入图片描述

源码

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>电网数据管理与智能分析</title><link rel="stylesheet" href="styles.css"><!-- Script loading order changed --><script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script> <!-- 1. Day.js Core --><script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-dayjs@1/dist/chartjs-adapter-dayjs.bundle.min.js"></script> <!-- 2. Day.js Adapter --><script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <!-- 3. Chart.js -->
</head>
<body><div class="container"><header class="overview-bar"><h1>城市配电网数据管理与智能分析</h1><div class="metrics"><div class="metric-item"><span class="label">当前总负载</span><span class="value" id="currentTotalLoad">-- MW</span></div><div class="metric-item"><span class="label">预测峰值 (24h)</span><span class="value" id="predictedPeakLoad">-- MW</span></div><div class="metric-item"><span class="label">电网稳定指数</span><span class="value" id="gridStabilityIndex">--</span><span class="tooltip">概念性指标,越高越稳定</span></div></div></header><main class="main-content"><section class="grid-visualization-section"><div class="topology-container"><h2>电网拓扑与状态 (简化)</h2><div class="topology-map" id="topologyMap"><!-- Grid nodes and lines will be generated by JS --><p>正在加载电网拓扑...</p></div><div class="legend"><span>负载状态:</span><span class="legend-item low"></span><span class="legend-item normal"></span> 正常<span class="legend-item high"></span><span class="legend-item overload"></span> 过载<span class="legend-item offline"></span> 离线</div></div><div class="time-control-panel"><h2>时间控制</h2><label for="currentDateTime">当前时间:</label><input type="datetime-local" id="currentDateTime" disabled><button id="playPauseBtn" title="播放/暂停时间演进">▶️</button><label for="timeSpeed">速度:</label><select id="timeSpeed"><option value="1">1x</option><option value="5">5x</option><option value="10">10x</option><option value="30">30x</option></select><button id="resetTimeBtn" title="重置时间">重置</button></div></section><section class="data-analysis-section"><div class="chart-container load-forecast-container"><h2>负载预测 (未来 24 小时)</h2><canvas id="loadForecastChart"></canvas></div><div class="node-data-panel"><h2>节点数据: <span id="selectedNodeName">未选择</span></h2><div id="nodeDetails"><p>请在左侧拓扑图中选择一个节点查看详细数据。</p><!-- Details like Current Load, Voltage, Predicted Peak, Status --></div></div><div class="analysis-alerts-panel"><h2>智能分析与告警</h2><ul id="alertList"><li>系统初始化完成,等待数据...</li><!-- Analysis results and alerts will be added by JS --></ul></div></section></main><footer class="status-bar"><span>模拟时间: <span id="simulationTime">--</span></span><span>模拟状态: <span id="simulationStatus">已暂停</span></span></footer></div><script src="script.js"></script> <!-- 4. Your main script -->
</body>
</html> 

styles.css

:root {--bg-color: #f5f5f7;--panel-bg-color: #ffffff;--border-color: #d2d2d7;--text-color-primary: #1d1d1f;--text-color-secondary: #6e6e73;--accent-blue: #007aff;--accent-green: #34c759;--accent-yellow: #ffcc00;--accent-orange: #ff9500;--accent-red: #ff3b30;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--border-radius: 8px;--container-padding: 20px;--panel-padding: 15px;--header-height: 60px;--footer-height: 30px; /* Smaller footer *//* Grid status colors */--load-low-color: #a1dd70;--load-normal-color: var(--accent-green);--load-high-color: var(--accent-yellow);--load-overload-color: var(--accent-red);--load-offline-color: #a0a0a0;
}* {box-sizing: border-box;margin: 0;padding: 0;
}body {font-family: var(--font-family);background-color: var(--bg-color);color: var(--text-color-primary);line-height: 1.5;display: flex;justify-content: center;align-items: flex-start;min-height: 100vh;padding: 20px;
}.container {width: 100%;max-width: 1500px; /* Slightly wider for grid layout */background-color: var(--panel-bg-color);border-radius: var(--border-radius);border: 1px solid var(--border-color);box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);overflow: hidden;display: flex;flex-direction: column;
}/* Header / Overview Bar */
.overview-bar {display: flex;justify-content: space-between;align-items: center;padding: 0 var(--container-padding);height: var(--header-height);border-bottom: 1px solid var(--border-color);background-color: #ffffff;
}.overview-bar h1 {font-size: 1.2em;font-weight: 600;color: var(--text-color-primary);white-space: nowrap;overflow: hidden;text-overflow: ellipsis;margin-right: 20px;
}.metrics {display: flex;gap: 25px;flex-shrink: 0; /* Prevent metrics from shrinking */
}.metric-item {display: flex;flex-direction: column;align-items: flex-end;position: relative; /* For tooltip */
}.metric-item .label {font-size: 0.8em;color: var(--text-color-secondary);margin-bottom: 2px;
}.metric-item .value {font-size: 1.1em;font-weight: 600;color: var(--text-color-primary);
}.metric-item .tooltip {position: absolute;bottom: 100%; /* Position above the item */left: 50%;transform: translateX(-50%);background-color: rgba(0, 0, 0, 0.7);color: white;padding: 3px 6px;border-radius: 4px;font-size: 0.7em;white-space: nowrap;opacity: 0;visibility: hidden;transition: opacity 0.2s ease, visibility 0.2s ease;margin-bottom: 5px;pointer-events: none; /* Prevent tooltip from blocking clicks */
}.metric-item:hover .tooltip {opacity: 1;visibility: visible;
}/* Main Content Area */
.main-content {display: flex;flex: 1;padding: var(--container-padding);gap: var(--container-padding);min-height: 450px; /* Minimum height for layout */
}.grid-visualization-section {flex: 2; /* Left side takes less space */display: flex;flex-direction: column;gap: var(--container-padding);
}.data-analysis-section {flex: 3; /* Right side takes more space */display: flex;flex-direction: column;gap: var(--container-padding);
}/* Panels within sections */
.topology-container,
.time-control-panel,
.chart-container,
.node-data-panel,
.analysis-alerts-panel {background-color: var(--panel-bg-color);border-radius: var(--border-radius);padding: var(--panel-padding);box-shadow: 0 1px 3px rgba(0,0,0,0.04);display: flex;flex-direction: column; /* Default to column layout */
}.topology-container h2,
.time-control-panel h2,
.chart-container h2,
.node-data-panel h2,
.analysis-alerts-panel h2 {font-size: 0.95em;font-weight: 600;margin-bottom: 15px;color: var(--text-color-primary);flex-shrink: 0; /* Prevent title shrinking */
}/* Left Side Panels */
.topology-container {flex-grow: 1; /* Allow topology to take available space */min-height: 300px; /* Ensure space for map */
}.topology-map {flex-grow: 1;background-color: #e9e9eb;border-radius: 4px;position: relative; /* For positioning nodes/lines */overflow: auto; /* Allow scroll if content exceeds */display: flex; /* Center initial message */justify-content: center;align-items: center;color: var(--text-color-secondary);
}/* Simple placeholder styling for nodes/lines - JS will handle real elements */
.grid-node {position: absolute;width: 30px;height: 30px;border-radius: 50%;background-color: var(--accent-blue);border: 2px solid white;box-shadow: 0 1px 3px rgba(0,0,0,0.2);display: flex;justify-content: center;align-items: center;font-size: 0.7em;font-weight: bold;color: white;cursor: pointer;transition: transform 0.2s ease, background-color 0.3s ease;z-index: 2;
}
.grid-node:hover {transform: scale(1.1);
}
.grid-node.selected {border-color: var(--accent-orange);box-shadow: 0 0 8px var(--accent-orange);
}.grid-line {position: absolute;background-color: var(--text-color-secondary);height: 3px; /* Line thickness */transform-origin: left center;z-index: 1;transition: background-color 0.3s ease;
}/* Node status colors (applied via JS) */
.grid-node.low, .grid-line.low { background-color: var(--load-low-color); }
.grid-node.normal, .grid-line.normal { background-color: var(--load-normal-color); }
.grid-node.high, .grid-line.high { background-color: var(--load-high-color); }
.grid-node.overload, .grid-line.overload { background-color: var(--load-overload-color); }
.grid-node.offline, .grid-line.offline { background-color: var(--load-offline-color); }/* Legend Styling */
.legend {margin-top: 10px;font-size: 0.75em;display: flex;align-items: center;flex-wrap: wrap;gap: 5px 10px;color: var(--text-color-secondary);flex-shrink: 0;
}
.legend-item {display: inline-block;width: 12px;height: 12px;border-radius: 3px;margin-right: 3px;vertical-align: middle;
}
.legend-item.low { background-color: var(--load-low-color); }
.legend-item.normal { background-color: var(--load-normal-color); }
.legend-item.high { background-color: var(--load-high-color); }
.legend-item.overload { background-color: var(--load-overload-color); }
.legend-item.offline { background-color: var(--load-offline-color); }.time-control-panel {flex-shrink: 0; /* Prevent panel from shrinking */
}
.time-control-panel label {font-size: 0.85em;margin-right: 5px;color: var(--text-color-secondary);
}
.time-control-panel input[type="datetime-local"],
.time-control-panel select {font-size: 0.85em;padding: 4px 6px;border: 1px solid var(--border-color);border-radius: 4px;margin-right: 10px;
}
.time-control-panel button {font-size: 1em;background: none;border: none;cursor: pointer;padding: 5px;margin: 0 5px;vertical-align: middle;
}
.time-control-panel button:hover {opacity: 0.7;
}/* Right Side Panels */
.load-forecast-container {flex-grow: 2; /* Chart takes more space */min-height: 250px;
}
.node-data-panel {flex-grow: 1;min-height: 100px;
}
.analysis-alerts-panel {flex-grow: 1;max-height: 180px; /* Limit height */overflow-y: auto;
}.chart-container canvas {max-width: 100%;flex-grow: 1; /* Allow canvas to fill container */
}#nodeDetails p {font-size: 0.9em;color: var(--text-color-secondary);
}
#nodeDetails strong {color: var(--text-color-primary);
}
#nodeDetails span {margin-left: 5px;
}/* Alerts List Styling (similar to previous component) */
#alertList {list-style: none;padding: 0;font-size: 0.85em;flex-grow: 1;overflow-y: auto; /* Scroll within the list */
}#alertList li {padding: 6px 10px;border-bottom: 1px solid #eee;display: flex;align-items: center;gap: 8px;
}#alertList li:last-child {border-bottom: none;
}/* Alert types styling */
.alert-info::before { content: "\2139"; color: var(--accent-blue); font-weight: bold; }
.alert-warning::before { content: "\26A0"; color: var(--accent-yellow); font-weight: bold; }
.alert-critical::before { content: "\2757"; color: var(--accent-red); font-weight: bold; }
.alert-suggestion::before { content: "\1F4A1"; color: var(--accent-green); }/* Footer / Status Bar */
.status-bar {display: flex;justify-content: space-between;align-items: center;padding: 0 var(--container-padding);height: var(--footer-height);border-top: 1px solid var(--border-color);background-color: #fbfbfd;font-size: 0.8em;color: var(--text-color-secondary);
}/* Scrollbar Styling (optional, Webkit) */
::-webkit-scrollbar {width: 6px;height: 6px;
}
::-webkit-scrollbar-track {background: #f1f1f1;border-radius: 3px;
}
::-webkit-scrollbar-thumb {background: #c1c1c1;border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {background: #a8a8a8;
}/* Responsive Adjustments */
@media (max-width: 1200px) {.metrics {gap: 15px;}.metric-item .value {font-size: 1em;}
}@media (max-width: 992px) {.main-content {flex-direction: column;min-height: auto;}.grid-visualization-section,.data-analysis-section {flex: none;width: 100%;}.topology-container {min-height: 250px;}.analysis-alerts-panel {max-height: 150px;}
}@media (max-width: 768px) {body {padding: 10px;}.container {border-radius: 0;border-left: none;border-right: none;}.overview-bar {flex-direction: column;height: auto;padding: 10px var(--panel-padding);align-items: flex-start;}.overview-bar h1 {margin-bottom: 10px;}.metrics {width: 100%;justify-content: space-between;gap: 10px;}.metric-item {align-items: center; /* Center metrics on mobile */}.main-content {padding: var(--panel-padding);}.grid-visualization-section, .data-analysis-section {gap: var(--panel-padding);}.time-control-panel {display: flex;flex-wrap: wrap;gap: 10px;}.time-control-panel input,.time-control-panel select,.time-control-panel button {margin-right: 0;}
}@media (max-width: 480px) {.metrics {flex-wrap: wrap;justify-content: center;}.metric-item {flex-basis: 45%;align-items: center;margin-bottom: 5px;}.overview-bar h1 {font-size: 1.1em;}
} 

script.js

document.addEventListener('DOMContentLoaded', () => {// --- DOM Elements ---const currentTotalLoadSpan = document.getElementById('currentTotalLoad');const predictedPeakLoadSpan = document.getElementById('predictedPeakLoad');const gridStabilityIndexSpan = document.getElementById('gridStabilityIndex');const topologyMapDiv = document.getElementById('topologyMap');const currentDateTimeInput = document.getElementById('currentDateTime');const playPauseBtn = document.getElementById('playPauseBtn');const timeSpeedSelect = document.getElementById('timeSpeed');const resetTimeBtn = document.getElementById('resetTimeBtn');const loadForecastCanvas = document.getElementById('loadForecastChart');const selectedNodeNameSpan = document.getElementById('selectedNodeName');const nodeDetailsDiv = document.getElementById('nodeDetails');const alertListUl = document.getElementById('alertList');const simulationTimeSpan = document.getElementById('simulationTime');const simulationStatusSpan = document.getElementById('simulationStatus');// --- Simulation Configuration ---const config = {startTime: dayjs().startOf('day').toDate(), // Start at beginning of todayupdateIntervalMs: 1000, // Real-time update intervalforecastHorizonHours: 24,historicalHours: 6, // How many hours of history to show on chartnodeClickHighlightDuration: 5000, // ms// Simplified grid structuregrid: {nodes: [{ id: 'S1', name: '主变电站 A', type: 'substation', x: 10, y: 50, capacity: 150 },{ id: 'S2', name: '变电站 B', type: 'substation', x: 40, y: 20, capacity: 100 },{ id: 'S3', name: '变电站 C', type: 'substation', x: 45, y: 80, capacity: 120 },{ id: 'F1', name: '馈线 1 (商业区)', type: 'feeder', x: 70, y: 10, baseLoad: [10, 8, 7, 6, 7, 8, 15, 25, 35, 40, 45, 50, 48, 45, 42, 40, 38, 42, 48, 45, 35, 25, 18, 12], weekendMultiplier: 0.6, capacity: 60 },{ id: 'F2', name: '馈线 2 (工业区)', type: 'feeder', x: 80, y: 45, baseLoad: [15, 12, 10, 10, 12, 15, 20, 30, 45, 55, 60, 60, 55, 50, 48, 45, 40, 35, 30, 25, 20, 18, 16, 15], weekendMultiplier: 0.4, capacity: 70 },{ id: 'F3', name: '馈线 3 (居民区)', type: 'feeder', x: 75, y: 85, baseLoad: [8, 6, 5, 5, 6, 8, 12, 18, 25, 28, 30, 32, 30, 28, 25, 28, 35, 45, 50, 45, 35, 25, 15, 10], weekendMultiplier: 1.1, capacity: 60 },{ id: 'F4', name: '馈线 4 (混合区)', type: 'feeder', x: 90, y: 65, baseLoad: [5, 4, 4, 4, 5, 7, 10, 15, 20, 22, 25, 26, 25, 24, 22, 23, 28, 35, 38, 35, 28, 20, 12, 8], weekendMultiplier: 0.9, capacity: 50 },],// Connections define power flow directionality for simulationconnections: [{ from: 'S1', to: 'S2' },{ from: 'S1', to: 'S3' },{ from: 'S2', to: 'F1' },{ from: 'S2', to: 'F2' },{ from: 'S3', to: 'F3' },{ from: 'S1', to: 'F4' } // Direct feeder from main substation]},loadFluctuationPercent: 5, // +/- 5% random fluctuationvoltageFluctuationPercent: 1, // +/- 1% random fluctuation from nominal 220kV/10kV etc.stabilityThresholds: { // For calculating stability indexoverloadCount: 3, // Max allowed overloaded nodes for high stabilityhighLoadCount: 5, // Max allowed high-load nodes}};// --- Simulation State ---let currentTime = dayjs(config.startTime);let simulationRunning = false;let simulationSpeed = 1;let simulationTimer = null;let gridState = {}; // { nodeId: { load, voltage, status, forecast[...] }, ... }let selectedNodeId = null;let nodeElements = {}; // Store DOM elements for nodeslet lineElements = {}; // Store DOM elements for lines// --- Chart Instance ---let loadForecastChart = null;// --- Utility Functions ---function getRandom(min, max) {return Math.random() * (max - min) + min;}function formatDateTime(date) {return dayjs(date).format('YYYY-MM-DD HH:mm:ss');}function formatLocalDateTimeForInput(date) {// HTML datetime-local input needs YYYY-MM-DDTHH:mmreturn dayjs(date).format('YYYY-MM-DDTHH:mm');}// *** NEW Helper function to aggregate data points ***function aggregateDataPoints(dataArrays) {const aggregatedMap = new Map(); // Map<timestamp_ms, totalLoad>const timePoints = new Set(); // Store unique timestamps in orderdataArrays.forEach(arr => {if (!arr) return; // Skip if array is null or undefinedarr.forEach(point => {if (!point || !point.time) return; // Skip invalid pointsconst timestampMs = point.time.getTime();const currentLoad = aggregatedMap.get(timestampMs) || 0;aggregatedMap.set(timestampMs, currentLoad + point.load);timePoints.add(timestampMs);});});// Sort timestamps and create the final arrayconst sortedTimestamps = Array.from(timePoints).sort((a, b) => a - b);return sortedTimestamps.map(ts => ({time: new Date(ts),load: aggregatedMap.get(ts)}));}// --- Initialization ---function initializeGridState() {gridState = {};config.grid.nodes.forEach(node => {gridState[node.id] = {load: 0,voltage: node.type === 'substation' ? 220 : 10, // Simplified nominal voltage kVstatus: 'normal', // normal, low, high, overload, offlineforecast: [], // Array of { time, load }config: node // Reference to static config};});}function initializeChart() {if (loadForecastChart) loadForecastChart.destroy();const ctx = loadForecastCanvas.getContext('2d');loadForecastChart = new Chart(ctx, {type: 'line',data: {// labels: [], // Handled by time scaledatasets: [{label: '历史负载 (MW)',data: [], // { x: time, y: load }borderColor: 'rgba(0, 122, 255, 0.8)',backgroundColor: 'transparent',borderWidth: 2,pointRadius: 0,tension: 0.1},{label: '预测负载 (MW)',data: [], // { x: time, y: load }borderColor: 'rgba(255, 149, 0, 0.8)', // OrangebackgroundColor: 'transparent',borderDash: [5, 5], // Dashed line for forecastborderWidth: 2,pointRadius: 0,tension: 0.1}]},options: {responsive: true,maintainAspectRatio: false,animation: { duration: 0 }, // Disable animation for performancescales: {x: {type: 'time',time: {unit: 'hour',tooltipFormat: 'YYYY-MM-DD HH:mm', // Format for tooltipsdisplayFormats: { hour: 'HH:mm' }},title: { display: true, text: '时间' },ticks: { source: 'auto', maxRotation: 0, autoSkipPadding: 20 }},y: {beginAtZero: true,title: { display: true, text: '负载 (MW)' }}},plugins: {legend: { position: 'top' },tooltip: { mode: 'index', intersect: false }},interaction: { mode: 'nearest', axis: 'x', intersect: false }}});}function drawGridTopology() {topologyMapDiv.innerHTML = ''; // Clear previousnodeElements = {};lineElements = {};const mapWidth = topologyMapDiv.clientWidth;const mapHeight = topologyMapDiv.clientHeight;// Create lines first (so they are behind nodes)config.grid.connections.forEach((conn, index) => {const fromNode = config.grid.nodes.find(n => n.id === conn.from);const toNode = config.grid.nodes.find(n => n.id === conn.to);if (!fromNode || !toNode) return;const x1 = (fromNode.x / 100) * mapWidth;const y1 = (fromNode.y / 100) * mapHeight;const x2 = (toNode.x / 100) * mapWidth;const y2 = (toNode.y / 100) * mapHeight;const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;const length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));const line = document.createElement('div');line.classList.add('grid-line');line.style.left = `${x1}px`;line.style.top = `${y1}px`;line.style.width = `${length}px`;line.style.transform = `rotate(${angle}deg)`;const lineId = `line-${conn.from}-${conn.to}`;line.id = lineId;lineElements[lineId] = line;topologyMapDiv.appendChild(line);});// Create nodesconfig.grid.nodes.forEach(node => {const nodeEl = document.createElement('div');nodeEl.classList.add('grid-node');nodeEl.dataset.nodeId = node.id;nodeEl.id = `node-${node.id}`;nodeEl.textContent = node.id; // Simple ID displaynodeEl.title = node.name; // TooltipnodeEl.style.left = `calc(${(node.x / 100) * 100}% - 15px)`; // Center the node (width/2)nodeEl.style.top = `calc(${(node.y / 100) * 100}% - 15px)`; // Center the node (height/2)if (node.type === 'substation') {nodeEl.style.borderRadius = '4px'; // Square for substationsnodeEl.style.width = '35px';nodeEl.style.height = '35px';nodeEl.style.left = `calc(${(node.x / 100) * 100}% - 17.5px)`;nodeEl.style.top = `calc(${(node.y / 100) * 100}% - 17.5px)`;}nodeEl.addEventListener('click', () => handleNodeClick(node.id));nodeElements[node.id] = nodeEl;topologyMapDiv.appendChild(nodeEl);});}// --- Simulation Loop ---function simulationStep() {if (!simulationRunning) return;const timeIncrementSeconds = 3600 * simulationSpeed; // Advance by hours based on speedcurrentTime = dayjs(currentTime).add(timeIncrementSeconds, 'second');updateGridState(currentTime);updateUI();// Schedule next stepsimulationTimer = setTimeout(simulationStep, config.updateIntervalMs);}function updateGridState(time) {const hourOfDay = time.hour();const dayOfWeek = time.day(); // 0 = Sunday, 6 = Saturdayconst isWeekend = (dayOfWeek === 0 || dayOfWeek === 6);let totalLoad = 0;let overloadCount = 0;let highLoadCount = 0;let activeNodes = 0;config.grid.nodes.forEach(node => {const state = gridState[node.id];if (state.status === 'offline') return;let currentBaseLoad = 0;if (node.type === 'feeder' && node.baseLoad) {currentBaseLoad = node.baseLoad[hourOfDay] * (isWeekend ? node.weekendMultiplier : 1);} else if (node.type === 'substation') {// Substation load is sum of loads it feeds (simplified)currentBaseLoad = config.grid.connections.filter(c => c.from === node.id).reduce((sum, conn) => sum + (gridState[conn.to]?.load || 0), 0);}// Add fluctuationstate.load = currentBaseLoad * (1 + getRandom(-config.loadFluctuationPercent / 100, config.loadFluctuationPercent / 100));state.load = Math.max(0, state.load); // Cannot be negative// Simulate voltage fluctuation (simple)const baseVoltage = node.type === 'substation' ? 220 : 10;state.voltage = baseVoltage * (1 + getRandom(-config.voltageFluctuationPercent / 100, config.voltageFluctuationPercent / 100));// Determine status based on load vs capacityconst loadRatio = node.capacity ? state.load / node.capacity : 0;if (loadRatio >= 1.0) {state.status = 'overload';overloadCount++;} else if (loadRatio >= 0.8) {state.status = 'high';highLoadCount++;} else if (loadRatio <= 0.3) {state.status = 'low';} else {state.status = 'normal';}if (node.type === 'feeder') { // Only feeders contribute directly to total load in this modeltotalLoad += state.load;}activeNodes++;// Generate simple forecaststate.forecast = generateSimpleForecast(node, time, config.forecastHorizonHours);});gridState.totalLoad = totalLoad;gridState.stabilityIndex = calculateStabilityIndex(overloadCount, highLoadCount, activeNodes);gridState.predictedPeak = calculatePredictedPeak(config.forecastHorizonHours);// Generate Alerts based on current state and forecastgenerateAlerts(time);}function generateSimpleForecast(node, startTime, hours) {const forecast = [];if (node.type !== 'feeder' || !node.baseLoad) {// Simplified: Substations forecast is sum of feeder forecasts// For now, return empty forecast for non-feeders/nodes without baseLoadif (node.type === 'substation') {const connectedFeeders = config.grid.connections.filter(c => c.from === node.id && gridState[c.to]?.config?.type === 'feeder').map(c => c.to);if (connectedFeeders.length > 0) {for (let i = 1; i <= hours; i++) {const forecastTime = dayjs(startTime).add(i, 'hour');let subForecastLoad = 0;connectedFeeders.forEach(feederId => {const feederNode = gridState[feederId].config;const feederForecast = generateSimpleForecast(feederNode, startTime, hours);subForecastLoad += feederForecast[i-1]?.load || 0;});forecast.push({ time: forecastTime.toDate(), load: subForecastLoad });}return forecast;}}return [];}for (let i = 1; i <= hours; i++) {const forecastTime = dayjs(startTime).add(i, 'hour');const hour = forecastTime.hour();const day = forecastTime.day();const weekend = (day === 0 || day === 6);let forecastLoad = node.baseLoad[hour] * (weekend ? node.weekendMultiplier : 1);// Add some basic trend/randomness if needed, keeping it simple hereforecastLoad *= (1 + getRandom(-config.loadFluctuationPercent / 150, config.loadFluctuationPercent / 150)); // Less fluctuation in forecastforecast.push({ time: forecastTime.toDate(), load: Math.max(0, forecastLoad) });}return forecast;}function calculatePredictedPeak(hours) {let peakLoad = 0;let peakTime = null;const forecastEndTime = dayjs(currentTime).add(hours, 'hour');// Aggregate forecasts across all feederslet aggregatedForecast = Array(hours).fill(0);config.grid.nodes.forEach(node => {if (gridState[node.id]?.forecast?.length === hours) {gridState[node.id].forecast.forEach((f, index) => {if (node.type === 'feeder') { // Only sum feeder forecasts for total peakaggregatedForecast[index] += f.load;}});}});aggregatedForecast.forEach((load, index) => {if (load > peakLoad) {peakLoad = load;peakTime = dayjs(currentTime).add(index + 1, 'hour');}});return { load: peakLoad, time: peakTime };}function calculateStabilityIndex(overloadCount, highLoadCount, activeNodes) {// Very simple conceptual index 0-100let score = 100;score -= overloadCount * 30; // Heavy penalty for overloadsscore -= highLoadCount * 10; // Medium penalty for high loadif (activeNodes < config.grid.nodes.length * 0.8) score -= 20; // Penalty for offline nodesreturn Math.max(0, Math.min(100, Math.round(score)));}// --- UI Update ---function updateUI() {currentDateTimeInput.value = formatLocalDateTimeForInput(currentTime);simulationTimeSpan.textContent = formatDateTime(currentTime);simulationStatusSpan.textContent = simulationRunning ? '运行中' : '已暂停';currentTotalLoadSpan.textContent = `${gridState.totalLoad?.toFixed(1) ?? '--'} MW`;predictedPeakLoadSpan.textContent = `${gridState.predictedPeak?.load.toFixed(1) ?? '--'} MW`;gridStabilityIndexSpan.textContent = gridState.stabilityIndex ?? '--';updateTopologyStyles();updateNodeDetailsPanel(); // Update panel if a node is selectedupdateLoadForecastChart(); // Update chart}function updateTopologyStyles() {config.grid.nodes.forEach(node => {const el = nodeElements[node.id];const state = gridState[node.id];if (el && state) {// Remove old status classesel.classList.remove('low', 'normal', 'high', 'overload', 'offline');// Add current status classel.classList.add(state.status);}});config.grid.connections.forEach(conn => {const lineEl = lineElements[`line-${conn.from}-${conn.to}`];const fromState = gridState[conn.from];const toState = gridState[conn.to];if (lineEl && fromState && toState) {lineEl.classList.remove('low', 'normal', 'high', 'overload', 'offline');// Line color based on the node it feeds (the 'to' node), or 'from' if 'to' is offlineconst statusToUse = (toState.status !== 'offline') ? toState.status : fromState.status;lineEl.classList.add(statusToUse);if (fromState.status === 'offline' || toState.status === 'offline') {lineEl.classList.add('offline'); // If either end is offline, line is offline}}});}function updateNodeDetailsPanel() {if (!selectedNodeId || !gridState[selectedNodeId]) {selectedNodeNameSpan.textContent = '未选择';nodeDetailsDiv.innerHTML = '<p>请在左侧拓扑图中选择一个节点查看详细数据。</p>';return;}const state = gridState[selectedNodeId];const nodeConfig = state.config;selectedNodeNameSpan.textContent = nodeConfig.name;let detailsHTML = `<p><strong>ID:</strong> <span>${nodeConfig.id}</span></p><p><strong>类型:</strong> <span>${nodeConfig.type === 'substation' ? '变电站' : '馈线'}</span></p><p><strong>当前负载:</strong> <span class="status-${state.status}">${state.load.toFixed(1)} MW</span></p><p><strong>${nodeConfig.type === 'substation' ? '额定电压:' : '馈线电压:'}</strong> <span>${state.voltage.toFixed(1)} kV</span></p>${nodeConfig.capacity ? `<p><strong>容量:</strong> <span>${nodeConfig.capacity} MW</span></p>` : ''}<p><strong>状态:</strong> <span class="status-${state.status}">${getStatusText(state.status)}</span></p>`;// Add predicted peak for this specific node if availableif (state.forecast && state.forecast.length > 0) {const nodePeak = state.forecast.reduce((max, p) => p.load > max.load ? p : max, { load: 0 });if (nodePeak.load > 0) {detailsHTML += `<p><strong>预测峰值 (节点, 24h):</strong> <span>${nodePeak.load.toFixed(1)} MW at ${dayjs(nodePeak.time).format('HH:mm')}</span></p>`;}}nodeDetailsDiv.innerHTML = detailsHTML;}// *** MODIFIED function to show total load or selected node load ***function updateLoadForecastChart() {if (!loadForecastChart) return; // Chart not initializedlet historicalData = [];let forecastData = [];let chartLabelSuffix = "";if (!selectedNodeId) {// No node selected - Show aggregated data for all feederschartLabelSuffix = " (总计)";const allHistorical = [];const allForecast = [];config.grid.nodes.forEach(node => {if (node.type === 'feeder') {allHistorical.push(getHistoricalData(node.id, config.historicalHours));allForecast.push(gridState[node.id]?.forecast || []);}});historicalData = aggregateDataPoints(allHistorical);forecastData = aggregateDataPoints(allForecast);} else if (gridState[selectedNodeId]) {// Node selected - Show its specific dataconst state = gridState[selectedNodeId];chartLabelSuffix = ` (${state.config.id})`;historicalData = getHistoricalData(selectedNodeId, config.historicalHours);forecastData = state.forecast || [];} else {// Selected node ID exists but no state found (error case?)// Clear the chart}// Update chart datasetsloadForecastChart.data.datasets[0].data = historicalData.map(p => ({ x: p.time, y: p.load }));loadForecastChart.data.datasets[0].label = `历史负载 (MW)${chartLabelSuffix}`;loadForecastChart.data.datasets[1].data = forecastData.map(p => ({ x: p.time, y: p.load }));loadForecastChart.data.datasets[1].label = `预测负载 (MW)${chartLabelSuffix}`;// Adjust time axis only if there is dataif (historicalData.length > 0 || forecastData.length > 0) {const firstTime = historicalData[0]?.time ?? forecastData[0]?.time;const lastTime = forecastData[forecastData.length - 1]?.time ?? historicalData[historicalData.length - 1]?.time;if (firstTime && lastTime) {loadForecastChart.options.scales.x.min = dayjs(firstTime).subtract(30, 'minute').toDate();loadForecastChart.options.scales.x.max = dayjs(lastTime).add(30, 'minute').toDate();} else {// Reset axes if no valid time dataloadForecastChart.options.scales.x.min = null;loadForecastChart.options.scales.x.max = null;}} else {// Reset axes if no data at allloadForecastChart.options.scales.x.min = null;loadForecastChart.options.scales.x.max = null;}loadForecastChart.update('none'); // Use 'none' to avoid potentially jerky updates when switching nodes}function getHistoricalData(nodeId, hoursBack) {// This is simplified - in a real app, this data would come from a backend/database// Here, we just simulate it by recalculating past loads based on the patternconst history = [];const node = gridState[nodeId].config;if (node.type !== 'feeder' || !node.baseLoad) return []; // Only feeders have direct history in this modelfor (let i = hoursBack; i > 0; i--) {const pastTime = dayjs(currentTime).subtract(i, 'hour');const hour = pastTime.hour();const day = pastTime.day();const weekend = (day === 0 || day === 6);let pastLoad = node.baseLoad[hour] * (weekend ? node.weekendMultiplier : 1);pastLoad *= (1 + getRandom(-config.loadFluctuationPercent / 100, config.loadFluctuationPercent / 100)); // Simulate past fluctuationhistory.push({ time: pastTime.toDate(), load: Math.max(0, pastLoad) });}// Add current pointhistory.push({ time: currentTime.toDate(), load: gridState[nodeId].load });return history;}function getStatusText(status) {switch (status) {case 'low': return '低负载';case 'normal': return '正常';case 'high': return '高负载';case 'overload': return '过载';case 'offline': return '离线';default: return '未知';}}function addLog(message, type = 'info') {const li = document.createElement('li');li.classList.add(`alert-${type}`);li.textContent = `[${formatDateTime(currentTime)}] ${message}`;alertListUl.insertBefore(li, alertListUl.firstChild);if (alertListUl.children.length > 20) { // Limit log sizealertListUl.removeChild(alertListUl.lastChild);}}function generateAlerts(time) {// Check for current overloadsconfig.grid.nodes.forEach(node => {const state = gridState[node.id];if (state.status === 'overload') {addLog(`严重警告: 节点 ${node.name} (${node.id}) 当前已过载! 负载: ${state.load.toFixed(1)} MW / ${node.capacity} MW`, 'critical');}});// Check for predicted overloads (within next few hours)const predictionHorizonAlert = 6; // Check for overloads in next 6 hoursconfig.grid.nodes.forEach(node => {const state = gridState[node.id];if (state.forecast && node.capacity) {for(let i=0; i < predictionHorizonAlert && i < state.forecast.length; i++) {const forecastPoint = state.forecast[i];if (forecastPoint.load > node.capacity) {addLog(`预警: 节点 ${node.name} (${node.id}) 预计在 ${dayjs(forecastPoint.time).format('HH:mm')} 过载 (预测 ${forecastPoint.load.toFixed(1)} MW)`, 'warning');break; // Only log first predicted overload for this node}}}});// Stability suggestionif (gridState.stabilityIndex < 60) {addLog(`建议: 电网稳定性 (${gridState.stabilityIndex}) 偏低,请关注高负载和过载节点。`, 'suggestion');}}// --- Event Handlers ---function handleNodeClick(nodeId) {if (selectedNodeId === nodeId) {selectedNodeId = null; // Deselect if clicked againnodeElements[nodeId]?.classList.remove('selected');} else {if (selectedNodeId && nodeElements[selectedNodeId]) {nodeElements[selectedNodeId].classList.remove('selected');}selectedNodeId = nodeId;if (nodeElements[selectedNodeId]) {nodeElements[selectedNodeId].classList.add('selected');// Optional: remove highlight after some timesetTimeout(() => {nodeElements[selectedNodeId]?.classList.remove('selected');if(selectedNodeId === nodeId) { /* Check if still selected */ } // Keep selected logically}, config.nodeClickHighlightDuration);}}updateNodeDetailsPanel();updateLoadForecastChart(); // Update chart for the selected/deselected node}playPauseBtn.addEventListener('click', () => {simulationRunning = !simulationRunning;playPauseBtn.textContent = simulationRunning ? '⏸️' : '▶️';simulationStatusSpan.textContent = simulationRunning ? '运行中' : '已暂停';if (simulationRunning) {clearTimeout(simulationTimer);simulationStep(); // Start the loop immediatelyaddLog("模拟已开始", 'info');} else {clearTimeout(simulationTimer);addLog("模拟已暂停", 'info');}});timeSpeedSelect.addEventListener('change', (e) => {simulationSpeed = parseInt(e.target.value, 10);addLog(`模拟速度设置为 ${simulationSpeed}x`, 'info');});resetTimeBtn.addEventListener('click', () => {simulationRunning = false;clearTimeout(simulationTimer);currentTime = dayjs(config.startTime);playPauseBtn.textContent = '▶️';initializeGridState();selectedNodeId = null; // Deselect nodeupdateGridState(currentTime); // Recalculate initial stateupdateUI();// *** Modify Reset: Clear chart data instead of re-initializing ***if (loadForecastChart) {loadForecastChart.data.datasets[0].data = [];loadForecastChart.data.datasets[1].data = [];// Update chart labels to default (total load)loadForecastChart.data.datasets[0].label = '历史负载 (MW) (总计)';loadForecastChart.data.datasets[1].label = '预测负载 (MW) (总计)';// Recalculate aggregated data for the reset time and updateconst allHistorical = [];const allForecast = [];config.grid.nodes.forEach(node => {if (node.type === 'feeder') {allHistorical.push(getHistoricalData(node.id, config.historicalHours));allForecast.push(gridState[node.id]?.forecast || []);}});const historicalData = aggregateDataPoints(allHistorical);const forecastData = aggregateDataPoints(allForecast);loadForecastChart.data.datasets[0].data = historicalData.map(p => ({ x: p.time, y: p.load }));loadForecastChart.data.datasets[1].data = forecastData.map(p => ({ x: p.time, y: p.load }));// Reset axesloadForecastChart.options.scales.x.min = null;loadForecastChart.options.scales.x.max = null;loadForecastChart.update('none'); // Update immediately without animation} else {// If chart wasn't initialized somehow, initialize it nowinitializeChart();}// *** End chart modification for reset ***alertListUl.innerHTML = '<li>系统已重置</li>'; // Clear logsaddLog("模拟已重置到初始时间", 'info');// updateLoadForecastChart(); // No longer needed here, handled above});// Resize handler for topology redrawlet resizeTimeout;window.addEventListener('resize', () => {clearTimeout(resizeTimeout);resizeTimeout = setTimeout(() => {drawGridTopology(); // Redraw topologyupdateTopologyStyles(); // Reapply stylesif(loadForecastChart) { // Trigger chart redrawloadForecastChart.resize();}}, 250);});// --- Initial Setup ---function initializeApp() {initializeGridState();drawGridTopology();initializeChart();updateGridState(currentTime); // Calculate initial state before first drawupdateUI();addLog("电网分析组件初始化完成", 'info');}initializeApp();
}); 

相关文章:

  • 2025认证杯数学建模第二阶段C题完整论文(代码齐全)化工厂生产流程的预测和控制
  • 光学设计核心
  • SearchClassUtil
  • 在 Ubuntu 20.04 中使用 init.d 或者systemd实现开机自动执行脚本
  • YOLOv3深度解析:多尺度特征融合与实时检测的里程碑
  • 淘宝扭蛋机系统开发前景分析:解锁电商娱乐化新蓝海
  • 执行apt-get update 报错ModuleNotFoundError: No module named ‘apt_pkg‘的解决方案汇总
  • 文件系统交互实现
  • 特斯拉虚拟电厂:能源互联网时代的分布式革命
  • NX二次开发C#---遍历当前工作部件实体并设置颜色
  • 来一个复古的技术FTP
  • 交叉熵损失函数,KL散度, Focal loss
  • PHP:经典编程语言在新时代的持续活力与演进
  • 中exec()函数因$imagePath参数导致的命令注入漏洞
  • 自定义CString类与MFC CString类接口对比
  • 奥运数据可视化:探索数据讲述奥运故事
  • w~深度学习~合集3
  • PyTorch 的 F.scaled_dot_product_attention 返回Nan
  • 三格电子上新了——Modbus转IEC104网关
  • C42-作业练习
  • 101岁陕西省军区原司令员冀廷璧逝世,曾参加百团大战
  • 收到延期付款利息,该缴纳增值税吗?
  • 圆桌丨新能源车超充技术元年,专家呼吁重视电网承载能力可能面临的结构性挑战
  • 浙江省台州市政协原副主席林虹被“双开”
  • 最高人民法院原副院长唐德华逝世,享年89岁
  • 俄方代表团抵达土耳其,俄乌直接谈判有望于当地时间上午重启