Spaces:
Running
Running
| /** HPIChart — GCC with diamond markers for selected heat pumps. */ | |
| import Plot from 'react-plotly.js'; | |
| import { useAnalysisStore } from '../../store/analysisStore'; | |
| import { useUIStore } from '../../store/uiStore'; | |
| import ChartHelpButton from '../ui/ChartHelpButton'; | |
| const HP_COLORS = [ | |
| '#00CC96', '#AB63FA', '#FFA15A', '#19D3F3', | |
| '#FF6692', '#B6E880', '#FF97FF', '#FECB52', | |
| ]; | |
| export default function HPIChart() { | |
| const hpiResult = useAnalysisStore((s) => s.hpiResult); | |
| const pinchResult = useAnalysisStore((s) => s.pinchResult); | |
| const selectedHPTypes = useAnalysisStore((s) => s.selectedHPTypes); | |
| const theme = useUIStore((s) => s.theme); | |
| const isDark = theme === 'dark'; | |
| if (!hpiResult || !pinchResult) return null; | |
| const gccH = hpiResult.gcc_data.H; | |
| const gccT = hpiResult.gcc_data.T; | |
| // GCC segment traces | |
| const traces: any[] = []; | |
| const heatCascade = pinchResult.heat_cascade; | |
| for (let i = 0; i < gccH.length - 1; i++) { | |
| let color = 'gray'; | |
| if (i < heatCascade.length) { | |
| const dh = heatCascade[i].deltaH ?? 0; | |
| if (dh > 0) color = 'red'; | |
| else if (dh < 0) color = 'blue'; | |
| } | |
| traces.push({ | |
| x: [gccH[i], gccH[i + 1]], | |
| y: [gccT[i], gccT[i + 1]], | |
| mode: 'lines+markers' as const, | |
| line: { color, width: 2 }, | |
| marker: { size: 5 }, | |
| showlegend: false, | |
| hovertemplate: `T: %{y:.1f}°C<br>H: %{x:.1f} kW<extra></extra>`, | |
| }); | |
| } | |
| // Diamond markers for selected HP types | |
| const feasible = hpiResult.integrations.filter((r) => r.feasible); | |
| const allHPNames = feasible.map((r) => r.hp_type); | |
| const displayed = selectedHPTypes.length > 0 | |
| ? feasible.filter((r) => selectedHPTypes.includes(r.hp_type)) | |
| : feasible.slice(0, 1); // default: first | |
| displayed.forEach((hp) => { | |
| const globalIdx = allHPNames.indexOf(hp.hp_type); | |
| const color = HP_COLORS[globalIdx % HP_COLORS.length]; | |
| const hpName = hp.hp_type; | |
| if (hp.source_points.H.length > 0) { | |
| traces.push({ | |
| x: hp.source_points.H, | |
| y: hp.source_points.T, | |
| mode: 'markers' as const, | |
| marker: { | |
| size: 12, | |
| color, | |
| symbol: 'diamond', | |
| line: { width: 2, color }, | |
| }, | |
| name: `${hpName} - Source`, | |
| legendgroup: hpName, | |
| hovertemplate: `<b>${hpName} - Source</b><br>T: %{y:.1f}°C<br>Q: %{x:.1f} kW<br>COP: ${hp.COP?.toFixed(2)}<extra></extra>`, | |
| }); | |
| } | |
| if (hp.sink_points.H.length > 0) { | |
| traces.push({ | |
| x: hp.sink_points.H, | |
| y: hp.sink_points.T, | |
| mode: 'markers' as const, | |
| marker: { | |
| size: 12, | |
| color, | |
| symbol: 'diamond-open', | |
| line: { width: 2, color }, | |
| }, | |
| name: `${hpName} - Sink`, | |
| legendgroup: hpName, | |
| hovertemplate: `<b>${hpName} - Sink</b><br>T: %{y:.1f}°C<br>Q: %{x:.1f} kW<br>COP: ${hp.COP?.toFixed(2)}<extra></extra>`, | |
| }); | |
| } | |
| }); | |
| const layout: any = { | |
| title: { text: 'Heat Pump Integration - Grand Composite Curve', font: { size: 14, color: isDark ? '#F8FAFC' : '#1A1C1E' } }, | |
| xaxis: { | |
| title: 'Net ΔH (kW)', | |
| gridcolor: isDark ? '#334155' : '#E2E8F0', | |
| tickfont: { color: isDark ? '#94A3B8' : '#5F6368' }, | |
| titlefont: { color: isDark ? '#F8FAFC' : '#1A1C1E' } | |
| }, | |
| yaxis: { | |
| title: 'Shifted Temperature (°C)', | |
| rangemode: 'tozero', | |
| gridcolor: isDark ? '#334155' : '#E2E8F0', | |
| tickfont: { color: isDark ? '#94A3B8' : '#5F6368' }, | |
| titlefont: { color: isDark ? '#F8FAFC' : '#1A1C1E' } | |
| }, | |
| height: 400, | |
| paper_bgcolor: 'rgba(0,0,0,0)', | |
| plot_bgcolor: 'rgba(0,0,0,0)', | |
| margin: { l: 220, r: 20, t: 40, b: 50 }, | |
| hovermode: 'closest' as const, | |
| showlegend: true, | |
| legend: { | |
| x: -0.15, y: 1.0, | |
| xanchor: 'right', yanchor: 'top', | |
| font: { size: 10, color: isDark ? '#F8FAFC' : '#1A1C1E' }, | |
| itemsizing: 'constant' | |
| }, | |
| shapes: [ | |
| { | |
| type: 'line', | |
| x0: 0, x1: 1, xref: 'paper', | |
| y0: pinchResult.pinch_temperature, | |
| y1: pinchResult.pinch_temperature, | |
| line: { dash: 'dash', color: 'gray', width: 1 }, | |
| }, | |
| { | |
| type: 'line', | |
| x0: 0, x1: 0, | |
| y0: 0, y1: 1, yref: 'paper', | |
| line: { color: isDark ? '#94A3B8' : 'black', width: 1 }, | |
| opacity: 0.3, | |
| }, | |
| ], | |
| annotations: [ | |
| { | |
| x: 1, xref: 'paper', xanchor: 'right', | |
| y: pinchResult.pinch_temperature, | |
| text: `Pinch: ${pinchResult.pinch_temperature.toFixed(1)}°C`, | |
| showarrow: false, | |
| font: { size: 11, color: isDark ? '#60A5FA' : 'gray' }, | |
| }, | |
| ], | |
| }; | |
| return ( | |
| <div className="pa-chart" style={{ position: 'relative' }}> | |
| <ChartHelpButton | |
| title="Heat Pump Integration" | |
| description={ | |
| <> | |
| <p>Visualizes how a heat pump bridges the temperature gap to lift heat from <strong>below the pinch</strong> (source) to <strong>above the pinch</strong> (sink).</p> | |
| <p>The solid diamonds represent the heat pump source, and hollow diamonds represent the sink.</p> | |
| <p>Heat pumps reduce both external heating and cooling demands simultaneously.</p> | |
| </> | |
| } | |
| /> | |
| <Plot | |
| data={traces} | |
| layout={layout} | |
| config={{ responsive: true }} | |
| style={{ width: '100%' }} | |
| /> | |
| </div> | |
| ); | |
| } | |