24、钢铁厂峰谷电价策略优化分析 - /能源管理组件/steel-plant-tou-optimization

76个工业组件库示例汇总

钢铁厂峰谷电价策略优化分析组件

1. 组件概述

本组件旨在模拟钢铁厂(特别是电弧炉 EAF 等高耗能工序)在峰谷电价机制下的用电成本,并通过模拟调整生产计划(负荷转移)来分析潜在的成本优化空间。

用户可以设定负荷转移策略(转移多少负荷、从哪个高峰时段转移、转移到哪个谷/平时段、持续多久),然后组件会计算并对比优化前后的总用电成本,并在图表中直观展示负荷曲线的变化以及与电价时段的关系。

设计风格遵循苹果科技工业美学,注重数据的清晰呈现和交互的便捷性。

2. 主要功能

  • 电价信息展示: 清晰列出预设的峰、平、谷时段及其对应的电价费率。
  • 基准负荷模拟: 基于设定的工厂基础负荷和高耗能设备(如 EAF)的典型运行时间,生成一个 24 小时的基准负荷曲线。
  • 优化策略配置: 用户可以输入想要模拟的负荷转移量 (MW)、选择负荷转移的起始高峰时段、目标谷/平时段以及转移的持续时长 (小时)。
  • 成本对比分析: 计算并显示模拟的 24 小时内,基准负荷下的总用电成本和执行优化策略后的总用电成本,以及明确的成本节省金额和百分比。
  • 可视化图表: 使用 Chart.js 图表展示:
    • 基准负荷曲线(例如,蓝色实线)。
    • 优化后的负荷曲线(例如,橙色虚线)。
    • 代表峰、平、谷电价时段的背景色块,方便用户直观理解负荷与电价的关系。
  • 交互式分析: 用户调整优化策略参数后,点击按钮即可重新计算并更新成本和图表。
  • 响应式布局: 界面适应不同屏幕宽度。

3. 技术栈

  • HTML5
  • CSS3 (Flexbox, CSS Variables, Media Queries)
  • JavaScript (ES6+)
  • Chart.js (用于绘制图表)
  • Day.js (本次模拟未使用,但已包含在 HTML 中以备将来扩展)

4. 运行与使用

  1. steel-plant-tou-optimization 文件夹放置在 能源管理组件 目录下。
  2. 在支持 HTML5 和 JavaScript 的浏览器中打开 index.html 文件。
  3. 组件加载后会显示预设的电价信息、基准 EAF 运行计划,并进行一次初始成本计算和图表绘制。
  4. 在左侧"优化策略模拟"区域:
    • 修改"转移负荷量"、“从高峰时段”、"转移至谷/平时段"和"持续时长"等参数。
    • 点击"执行优化分析"按钮。
  5. 观察左下侧"成本分析概要"面板中更新的成本数据(优化前、优化后、节省额)。
  6. 观察右侧图表中负荷曲线(橙色虚线)的变化,以及它与电价背景色的对应关系。

5. 模拟逻辑说明

  • 峰谷电价: 在 script.jsconfig.touTariff 中预定义。getTariffForHour(hour) 函数根据小时确定对应的电价类型和费率。
  • 基准负荷: 由固定基础负荷 (config.baseLoadMW) 和在特定高峰时段 (config.eafBaselineRunHours) 叠加的高耗能设备负荷 (config.eafLoadMW) 组成。
  • 成本计算: calculateCost(loadProfile) 函数遍历 24 小时负荷曲线,将每小时的负荷 (MW) 乘以对应时段的电价费率(此处简化假设费率为 元/MWh),累加得到总成本。
  • 负荷转移: generateOptimizedLoadProfile() 函数根据用户输入的参数,从指定的 fromHour 开始,在 duration 小时内,尝试将每小时 shiftAmount MW 的负荷(仅转移超出基础负荷的部分)减少,并增加到对应的 toHour 开始的时段。如果源时段没有足够的可转移负荷,会进行部分转移并在分析说明中提示。
  • 图表可视化: 使用 Chart.js 的组合图表类型。基准和优化后的负荷用 line 类型绘制,电价时段用 bar 类型作为背景色块绘制,通过 order 属性控制绘制顺序。工具提示 (Tooltip) 会同时显示负荷值和当前时段的电价信息。

6. 注意事项

  • 这是一个概念性模拟,用于演示峰谷电价优化策略的基本思路和效果。实际钢铁厂的负荷构成、生产调度约束和电价结构要复杂得多。
  • 负荷转移模型非常简化,未考虑生产工艺的连续性、设备启停限制等实际约束。
  • 成本计算假设电价费率单位为 元/MWh,是为了简化计算(MW * 元/MWh = 元)。如果实际费率是 元/kWh,则需要在计算时乘以 1000。
  • 所有数据均为程序生成

效果展示

在这里插入图片描述

源码

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">
    <!-- Chart.js for visualization -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <!-- Using Day.js for potential time formatting if needed -->
    <script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
</head>
<body>
    <div class="container">
        <header class="header-bar">
            <h1>峰谷电价策略优化分析</h1>
        </header>

        <main class="main-content">
            <section class="config-control-section">
                <div class="panel tou-tariff-panel">
                    <h2>峰谷电价时段与费率 (/kWh)</h2>
                    <ul id="tariffList">
                        <!-- Tariff details loaded by JS -->
                        <li>加载电价信息...</li>
                    </ul>
                </div>

                <div class="panel schedule-panel">
                    <h2>基准生产计划 ()</h2>
                    <p>假设主要高耗能设备 (如电弧炉 EAF) 的运行时间段如下:</p>
                    <div id="baselineScheduleDisplay">
                         <!-- Baseline schedule info loaded by JS -->
                         <p>加载基准计划...</p>
                    </div>
                </div>

                <div class="panel optimization-panel">
                    <h2>优化策略</h2>
                    <div class="control-group">
                        <label for="shiftLoadAmount">转移负荷量 (MW):</label>
                        <input type="number" id="shiftLoadAmount" value="50" min="0" step="5">
                    </div>
                    <div class="control-group">
                         <label for="shiftFromHour">从高峰时段 (小时):</label>
                         <select id="shiftFromHour">
                             <!-- Options populated by JS based on peak hours -->
                         </select>
                    </div>
                     <div class="control-group">
                         <label for="shiftToHour">转移至谷/平时段 (小时):</label>
                         <select id="shiftToHour">
                            <!-- Options populated by JS based on off-peak hours -->
                         </select>
                    </div>
                     <div class="control-group">
                         <label for="shiftDuration">持续时长 (小时):</label>
                         <input type="number" id="shiftDuration" value="3" min="1" max="8" step="1">
                     </div>
                    <button id="optimizeBtn">执行优化分析</button>
                </div>

                <div class="panel cost-summary-panel">
                    <h2>成本分析概要 (24小时)</h2>
                     <div class="summary-item">
                         <span>优化前总成本:</span>
                         <strong id="baselineCost">--</strong>
                     </div>
                     <div class="summary-item">
                         <span>优化后总成本:</span>
                         <strong id="optimizedCost">--</strong>
                     </div>
                     <div class="summary-item savings">
                         <span>节省成本:</span>
                         <strong id="costSavings">--</strong>
                         <span id="savingsPercentage"></span>
                     </div>
                </div>
            </section>

            <section class="visualization-section">
                 <div class="chart-container load-chart-container">
                     <h2>负荷曲线与电价 (24小时)</h2>
                     <canvas id="loadProfileChart"></canvas>
                 </div>
                  <div class="analysis-details-panel panel">
                    <h2>分析说明</h2>
                    <p>此展示了通过将高峰时段的部分高能耗生产(如电弧炉运行)转移到电价较低的谷/平时段,可以实现的潜在成本节约。</p>
                    <p>上方图表显示:</p>
                    <ul>
                        <li>蓝色实线: 基准负荷曲线</li>
                        <li>橙色虚线: 优化后负荷曲线</li>
                        <li>背景色块: 不同电价时段 (//)</li>
                    </ul>
                    <p id="analysisNotes">请调整左侧优化策略参数,点击"执行优化分析"查看结果。</p>
                  </div>
            </section>
        </main>

    </div>

    <script src="script.js"></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-red: #ff3b30;
    --accent-orange: #ff9500; /* For optimized line */
    --tariff-peak-bg: rgba(255, 59, 48, 0.1);
    --tariff-shoulder-bg: rgba(255, 204, 0, 0.1);
    --tariff-offpeak-bg: rgba(52, 199, 89, 0.1);
    --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --border-radius: 8px;
    --container-padding: 20px;
    --panel-padding: 15px;
    --header-height: 50px;
}

* {
    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: 1300px; /* Adjust max width as needed */
    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 */
.header-bar {
    padding: 0 var(--container-padding);
    height: var(--header-height);
    border-bottom: 1px solid var(--border-color);
    display: flex;
    align-items: center;
}

.header-bar h1 {
    font-size: 1.15em;
    font-weight: 600;
}

/* Main Content */
.main-content {
    display: flex;
    flex: 1;
    padding: var(--container-padding);
    gap: var(--container-padding);
    min-height: 500px; /* Ensure reasonable height */
}

.config-control-section {
    flex: 2; /* Left side takes less space */
    display: flex;
    flex-direction: column;
    gap: var(--panel-padding);
}

.visualization-section {
    flex: 3; /* Right side takes more space */
    display: flex;
    flex-direction: column;
    gap: var(--panel-padding);
    flex-grow: 1;
}

/* Panels */
.panel {
    background-color: #ffffff;
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius);
    padding: var(--panel-padding);
    box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}

.panel h2 {
    font-size: 0.95em;
    font-weight: 600;
    margin-bottom: 12px;
    color: var(--text-color-primary);
}

/* Left Panel Specifics */
.tou-tariff-panel ul {
    list-style: none;
    padding: 0;
    font-size: 0.85em;
}

.tou-tariff-panel li {
    padding: 5px 0;
    border-bottom: 1px dashed #eee;
    display: flex;
    justify-content: space-between;
}
.tou-tariff-panel li:last-child {
    border-bottom: none;
}
.tou-tariff-panel .tariff-type {
    font-weight: 500;
}
.tou-tariff-panel .tariff-hours {
    color: var(--text-color-secondary);
    font-size: 0.9em;
}
.tou-tariff-panel .tariff-rate {
    font-weight: 600;
    color: var(--accent-blue);
}

.schedule-panel p {
    font-size: 0.9em;
    margin-bottom: 8px;
    color: var(--text-color-secondary);
}

#baselineScheduleDisplay p {
     font-size: 0.9em;
     color: var(--text-color-primary);
     font-style: italic;
}

.optimization-panel .control-group {
    margin-bottom: 12px;
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 0.9em;
}

.optimization-panel label {
    flex-basis: 120px; /* Align labels */
    text-align: right;
    color: var(--text-color-secondary);
    font-size: 0.95em;
}

.optimization-panel input[type="number"],
.optimization-panel select {
    flex-grow: 1;
    padding: 6px 8px;
    border: 1px solid var(--border-color);
    border-radius: 4px;
    font-size: 0.95em;
    max-width: 150px; /* Limit width of inputs/selects */
}

.optimization-panel button {
    display: block;
    width: 100%;
    padding: 10px 15px;
    margin-top: 10px;
    background-color: var(--accent-blue);
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 1em;
    font-weight: 500;
    transition: background-color 0.2s ease;
}

.optimization-panel button:hover {
    background-color: #0056b3;
}

.cost-summary-panel .summary-item {
    display: flex;
    justify-content: space-between;
    padding: 6px 0;
    font-size: 0.9em;
    border-bottom: 1px dashed #eee;
}
.cost-summary-panel .summary-item:last-child {
    border-bottom: none;
}

.cost-summary-panel .summary-item span {
     color: var(--text-color-secondary);
}
.cost-summary-panel .summary-item strong {
    color: var(--text-color-primary);
    font-weight: 600;
}
.cost-summary-panel .summary-item.savings strong {
    color: var(--accent-green);
}
.cost-summary-panel .summary-item #savingsPercentage {
    font-size: 0.9em;
    margin-left: 5px;
    color: var(--accent-green);
}

/* Right Panel Specifics */
.load-chart-container {
    flex-grow: 3; /* Chart takes more space vertically compared to details */
    padding: var(--panel-padding);
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius);
    box-shadow: 0 1px 2px rgba(0,0,0,0.05);
    display: flex; /* Use flex to help canvas resize */
    flex-direction: column;
    height: 500px; /* Suggest a fixed height */
    max-height: 800px; /* Absolute maximum height */
    min-height: 300px; /* Minimum reasonable height */
}

.load-chart-container canvas {
    max-width: 100%;
    /* Let Chart.js handle canvas resizing within the container */
}

.analysis-details-panel {
    flex-grow: 1; /* Details panel takes less space */
    flex-shrink: 0; /* Prevent shrinking too much */
    font-size: 0.85em;
    line-height: 1.6;
    color: var(--text-color-secondary);
    max-height: 500px; /* Also limit details panel height */
    overflow-y: auto;
}
.analysis-details-panel h2 {
     margin-bottom: 10px;
}
.analysis-details-panel ul {
     margin-left: 20px;
     margin-bottom: 10px;
}
.analysis-details-panel p {
    margin-bottom: 10px;
}
#analysisNotes {
    font-style: italic;
    color: var(--text-color-secondary);
}

/* Responsive Adjustments */
@media (max-width: 1100px) {
    .main-content {
        flex-direction: column;
        min-height: auto;
    }
    .config-control-section,
    .visualization-section {
        flex: none;
        width: 100%;
    }
}

@media (max-width: 768px) {
    body {
        padding: 10px;
    }
    .container {
        border-radius: 0;
        border-left: none;
        border-right: none;
    }
    .main-content {
        padding: var(--panel-padding);
    }
    .config-control-section, .visualization-section {
        gap: var(--panel-padding);
    }
     .optimization-panel .control-group {
         flex-direction: column;
         align-items: stretch;
         gap: 5px;
     }
     .optimization-panel label {
         text-align: left;
         margin-bottom: 2px;
     }
     .optimization-panel input[type="number"],
     .optimization-panel select {
        max-width: none;
     }
}

@media (max-width: 480px) {
     .header-bar h1 {
         font-size: 1em;
     }
     .panel h2 {
         font-size: 0.9em;
     }
     body {
         padding: 5px;
     }
} 

script.js

document.addEventListener('DOMContentLoaded', () => {
    // --- DOM Elements ---
    const tariffListUl = document.getElementById('tariffList');
    const baselineScheduleDisplay = document.getElementById('baselineScheduleDisplay');
    const shiftLoadAmountInput = document.getElementById('shiftLoadAmount');
    const shiftFromHourSelect = document.getElementById('shiftFromHour');
    const shiftToHourSelect = document.getElementById('shiftToHour');
    const shiftDurationInput = document.getElementById('shiftDuration');
    const optimizeBtn = document.getElementById('optimizeBtn');
    const baselineCostSpan = document.getElementById('baselineCost');
    const optimizedCostSpan = document.getElementById('optimizedCost');
    const costSavingsSpan = document.getElementById('costSavings');
    const savingsPercentageSpan = document.getElementById('savingsPercentage');
    const loadProfileCanvas = document.getElementById('loadProfileChart');
    const analysisNotesP = document.getElementById('analysisNotes');

    // --- Configuration ---
    const config = {
        hoursInDay: 24,
        baseLoadMW: 20, // Base plant load constant throughout the day
        eafLoadMW: 100, // Electric Arc Furnace load when running
        eafBaselineRunHours: [8, 9, 10, 11, 13, 14, 15, 16], // Example peak hours EAF runs
        // Time-of-Use Tariff Structure (Example)
        touTariff: [
            { type: '谷', startHour: 0, endHour: 7, rate: 0.40 },  // 00:00 - 07:59
            { type: '平', startHour: 8, endHour: 11, rate: 0.85 }, // 08:00 - 11:59
            { type: '峰', startHour: 12, endHour: 17, rate: 1.30 }, // 12:00 - 17:59
            { type: '平', startHour: 18, endHour: 21, rate: 0.85 }, // 18:00 - 21:59
            { type: '谷', startHour: 22, endHour: 23, rate: 0.40 }, // 22:00 - 23:59
        ],
        chartColors: {
            baseline: 'rgba(0, 122, 255, 0.8)',
            optimized: 'rgba(255, 149, 0, 0.8)',
            peakBg: 'rgba(255, 59, 48, 0.1)',
            shoulderBg: 'rgba(255, 204, 0, 0.1)',
            offpeakBg: 'rgba(52, 199, 89, 0.1)'
        }
    };

    // --- State ---
    let baselineLoadProfile = []; // Array of 24 hourly load values (MW)
    let optimizedLoadProfile = []; // Array of 24 hourly load values (MW)
    let loadProfileChart = null;

    // --- Helper Functions ---
    function getTariffForHour(hour) {
        for (const period of config.touTariff) {
            // Handle wrap-around for endHour < startHour (e.g.,谷 period spanning midnight)
            if (period.endHour >= period.startHour) {
                if (hour >= period.startHour && hour <= period.endHour) {
                    return period;
                }
            } else { // Period wraps around midnight (e.g., 22:00 - 07:00)
                if (hour >= period.startHour || hour <= period.endHour) {
                    return period;
                }
            }
        }
        return { type: '未知', rate: 1.0 }; // Fallback
    }

    function generateBaselineLoadProfile() {
        const profile = Array(config.hoursInDay).fill(config.baseLoadMW);
        config.eafBaselineRunHours.forEach(hour => {
            if (hour >= 0 && hour < config.hoursInDay) {
                profile[hour] += config.eafLoadMW;
            }
        });
        return profile;
    }

    function calculateCost(loadProfile) {
        let totalCost = 0;
        for (let hour = 0; hour < config.hoursInDay; hour++) {
            const tariff = getTariffForHour(hour);
            const load = loadProfile[hour] || 0;
            // Assuming rate is effectively 元 per MWh for simulation simplicity
            totalCost += load * tariff.rate;
        }
        return totalCost;
    }

    function generateOptimizedLoadProfile(baselineProfile, shiftAmount, fromHour, toHour, duration) {
        const optimized = [...baselineProfile]; // Create a copy
        fromHour = parseInt(fromHour);
        toHour = parseInt(toHour);
        duration = parseInt(duration);
        shiftAmount = parseFloat(shiftAmount);

        if (isNaN(fromHour) || isNaN(toHour) || isNaN(duration) || isNaN(shiftAmount) || duration <= 0 || shiftAmount <= 0) {
            console.error("Invalid optimization parameters");
            analysisNotesP.textContent = "错误:无效的优化参数。";
            return baselineProfile; // Return baseline if params are bad
        }

        analysisNotesP.textContent = `模拟:将 ${fromHour}:00 开始的 ${duration} 小时内,每小时 ${shiftAmount} MW 的负荷转移到 ${toHour}:00 开始的时段。`;

        for (let i = 0; i < duration; i++) {
            const currentFromHour = (fromHour + i) % config.hoursInDay;
            const currentToHour = (toHour + i) % config.hoursInDay;

            // Check if enough load exists at the source hour (above base load)
            const availableLoadToShift = Math.max(0, optimized[currentFromHour] - config.baseLoadMW);
            const actualShift = Math.min(shiftAmount, availableLoadToShift);

            if (actualShift > 0) {
                optimized[currentFromHour] -= actualShift;
                optimized[currentToHour] += actualShift;
            } else {
                 console.warn(`Hour ${currentFromHour}: Not enough shiftable load (${availableLoadToShift.toFixed(1)} MW) to move ${shiftAmount} MW.`);
                  analysisNotesP.textContent += ` (注意:在 ${currentFromHour}:00 时段可转移负荷不足)`;
            }
        }
        return optimized;
    }

    // --- Initialization Functions ---
    function displayTariffInfo() {
        tariffListUl.innerHTML = '';
        config.touTariff.forEach(period => {
            const li = document.createElement('li');
            li.innerHTML = `
                <span class="tariff-type">${period.type}时段</span>
                <span class="tariff-hours">(${String(period.startHour).padStart(2, '0')}:00 - ${String(period.endHour).padStart(2, '0')}:59)</span>
                <span class="tariff-rate">${period.rate.toFixed(2)} 元</span>
            `;
            tariffListUl.appendChild(li);
        });
    }

    function displayBaselineSchedule() {
        baselineScheduleDisplay.innerHTML = `<p>EAF 运行时段: ${config.eafBaselineRunHours.map(h => `${String(h).padStart(2, '0')}:00`).join(', ')}</p>`;
    }

    function populateShiftSelectOptions() {
        shiftFromHourSelect.innerHTML = '';
        shiftToHourSelect.innerHTML = '';
        const peakHours = [];
        const offPeakHours = [];

        for (let hour = 0; hour < config.hoursInDay; hour++) {
            const tariff = getTariffForHour(hour);
            const option = document.createElement('option');
            option.value = hour;
            option.textContent = `${String(hour).padStart(2, '0')}:00`;

            if (tariff.type === '峰') {
                shiftFromHourSelect.appendChild(option);
                peakHours.push(hour);
            } else {
                shiftToHourSelect.appendChild(option.cloneNode(true)); // Clone node for the other select
                offPeakHours.push(hour);
            }
        }
         // Set default selections if possible
         if (shiftFromHourSelect.options.length > 0) shiftFromHourSelect.selectedIndex = 0;
         if (shiftToHourSelect.options.length > 0) shiftToHourSelect.selectedIndex = 0;
    }

    function initializeChart() {
        if (loadProfileChart) {
            loadProfileChart.destroy();
        }
        const ctx = loadProfileCanvas.getContext('2d');
        const labels = Array.from({ length: config.hoursInDay }, (_, i) => `${String(i).padStart(2, '0')}:00`);

        // Prepare background color array based on tariff
        const backgroundColors = labels.map((_, index) => {
            const tariff = getTariffForHour(index);
            switch (tariff.type) {
                case '峰': return config.chartColors.peakBg;
                case '平': return config.chartColors.shoulderBg;
                case '谷': return config.chartColors.offpeakBg;
                default: return 'rgba(0,0,0,0.05)';
            }
        });

        loadProfileChart = new Chart(ctx, {
            type: 'bar', // Use bar chart to better show hourly load and background color
            data: {
                labels: labels,
                datasets: [
                    {
                        label: '基准负荷 (MW)',
                        data: baselineLoadProfile,
                        backgroundColor: config.chartColors.baseline,
                        borderColor: config.chartColors.baseline,
                        borderWidth: 1,
                        type: 'line', // Overlay line for baseline
                        fill: false,
                        tension: 0.1,
                        pointRadius: 1,
                        order: 1 // Draw line on top
                    },
                    {
                        label: '优化负荷 (MW)',
                        data: optimizedLoadProfile,
                         backgroundColor: config.chartColors.optimized,
                         borderColor: config.chartColors.optimized,
                         borderWidth: 1,
                        type: 'line', // Overlay line for optimized
                        fill: false,
                        borderDash: [5, 5],
                        tension: 0.1,
                         pointRadius: 1,
                         order: 0 // Draw optimized line first
                    },
                    {
                        label: '电价时段',
                        data: baselineLoadProfile, // Use any profile data just for positioning bars
                        backgroundColor: backgroundColors,
                        borderColor: 'transparent',
                        borderWidth: 0,
                        type: 'bar', // Background bars
                        order: 2, // Draw bars behind lines
                        barPercentage: 1.0,
                        categoryPercentage: 1.0,
                        grouped: false // Ensure bars occupy the full category width
                    }
                ]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                interaction: { // Optimize hover/tooltip
                    mode: 'index',
                    intersect: false,
                },
                scales: {
                    x: {
                        stacked: true, // Needed for background bars
                        title: { display: true, text: '时间 (小时)' }
                    },
                    y: {
                        beginAtZero: true,
                        stacked: false,
                        title: { display: true, text: '负荷 (MW)' }
                    }
                },
                plugins: {
                    tooltip: {
                        mode: 'index',
                        intersect: false,
                        callbacks: {
                            // Custom tooltip to show load and tariff
                            label: function(context) {
                                let label = context.dataset.label || '';
                                if (label && context.dataset.type === 'line') {
                                     label += `: ${context.parsed.y.toFixed(1)} MW`;
                                }
                                // Add tariff info to tooltip (only once per x-index)
                                if (context.datasetIndex === 0) { // Show only for the first dataset (baseline line)
                                     const hour = context.dataIndex;
                                     const tariff = getTariffForHour(hour);
                                     label += ` (${tariff.type}: ${tariff.rate.toFixed(2)} 元)`;
                                }
                                return label;
                            },
                            // Filter out the background bar dataset from tooltip
                             filter: function(tooltipItem) {
                                 return tooltipItem.dataset.label !== '电价时段';
                             }
                        }
                    },
                    legend: {
                         labels: {
                             // Filter out the background bar dataset from legend
                             filter: function(legendItem, chartData) {
                                 return legendItem.text !== '电价时段';
                             }
                         }
                    }
                }
            }
        });
    }

    // --- Core Calculation and Update ---
    function runOptimizationAnalysis() {
        const shiftAmount = parseFloat(shiftLoadAmountInput.value);
        const fromHour = parseInt(shiftFromHourSelect.value);
        const toHour = parseInt(shiftToHourSelect.value);
        const duration = parseInt(shiftDurationInput.value);

        // Validate inputs
        if (isNaN(shiftAmount) || shiftAmount < 0 || isNaN(fromHour) || isNaN(toHour) || isNaN(duration) || duration <= 0) {
             analysisNotesP.textContent = "错误: 请输入有效的优化参数。";
             baselineCostSpan.textContent = '-- 元';
             optimizedCostSpan.textContent = '-- 元';
             costSavingsSpan.textContent = '-- 元';
             savingsPercentageSpan.textContent = '';
            return;
        }
         // Prevent shifting to the same hour or overlapping significantly (simple check)
         if (fromHour === toHour) {
              analysisNotesP.textContent = "错误: 不能将负荷转移到同一时段。";
              return;
         }

        // 1. Regenerate baseline (in case base config changes in future)
        baselineLoadProfile = generateBaselineLoadProfile();

        // 2. Generate optimized profile
        optimizedLoadProfile = generateOptimizedLoadProfile(baselineLoadProfile, shiftAmount, fromHour, toHour, duration);

        // 3. Calculate costs
        const baselineCost = calculateCost(baselineLoadProfile);
        const optimizedCost = calculateCost(optimizedLoadProfile);
        const savings = baselineCost - optimizedCost;
        const savingsPercent = baselineCost > 0 ? (savings / baselineCost) * 100 : 0;

        // 4. Update UI
        baselineCostSpan.textContent = `${baselineCost.toFixed(0)}`;
        optimizedCostSpan.textContent = `${optimizedCost.toFixed(0)}`;
        costSavingsSpan.textContent = `${savings.toFixed(0)}`;
        savingsPercentageSpan.textContent = savings > 0 ? `(${savingsPercent.toFixed(1)}%)` : '';

        // Update Chart Data
        if (loadProfileChart) {
            loadProfileChart.data.datasets[0].data = baselineLoadProfile;
             loadProfileChart.data.datasets[1].data = optimizedLoadProfile;
             // Update background bar data as well (though it might not change)
             loadProfileChart.data.datasets[2].data = baselineLoadProfile;
            loadProfileChart.update();
        } else {
            initializeChart(); // Initialize if it doesn't exist yet
        }
    }

    // --- Event Listeners ---
    optimizeBtn.addEventListener('click', runOptimizationAnalysis);

    // --- Initial Setup ---
    function initializeApp() {
        displayTariffInfo();
        displayBaselineSchedule();
        populateShiftSelectOptions();
        baselineLoadProfile = generateBaselineLoadProfile();
        optimizedLoadProfile = [...baselineLoadProfile]; // Initially, optimized is same as baseline
        initializeChart();
        runOptimizationAnalysis(); // Run initial analysis to populate costs and chart
         analysisNotesP.textContent = "请调整左侧优化策略参数,点击'执行优化分析'查看结果。"; // Reset notes after initial run

    }

    initializeApp();
}); 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

地上一の鹅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值