Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LCOE vs WACC Analysis Tool</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-primary: #0a0e17; | |
| --bg-secondary: #111827; | |
| --bg-tertiary: #1a2234; | |
| --bg-card: #151d2e; | |
| --border: #2a3548; | |
| --text-primary: #f1f5f9; | |
| --text-secondary: #94a3b8; | |
| --text-muted: #64748b; | |
| --accent-solar: #10b981; | |
| --accent-solar-glow: rgba(16, 185, 129, 0.3); | |
| --accent-fossil: #f97316; | |
| --accent-fossil-glow: rgba(249, 115, 22, 0.3); | |
| --accent-blue: #3b82f6; | |
| --radius-sm: 6px; | |
| --radius-md: 10px; | |
| --radius-lg: 16px; | |
| --shadow-card: 0 4px 24px rgba(0, 0, 0, 0.4); | |
| --transition-fast: 0.15s ease; | |
| --transition-normal: 0.3s ease; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Space Grotesk', sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| .bg-atmosphere { | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 0; | |
| overflow: hidden; | |
| } | |
| .bg-atmosphere::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: | |
| radial-gradient(ellipse at 20% 20%, rgba(16, 185, 129, 0.08) 0%, transparent 50%), | |
| radial-gradient(ellipse at 80% 80%, rgba(249, 115, 22, 0.06) 0%, transparent 50%), | |
| radial-gradient(ellipse at 50% 50%, rgba(59, 130, 246, 0.04) 0%, transparent 60%); | |
| animation: atmosphereShift 30s ease-in-out infinite; | |
| } | |
| @keyframes atmosphereShift { | |
| 0%, 100% { transform: translate(0, 0) rotate(0deg); } | |
| 33% { transform: translate(2%, 2%) rotate(1deg); } | |
| 66% { transform: translate(-1%, 1%) rotate(-1deg); } | |
| } | |
| .grid-pattern { | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 1; | |
| opacity: 0.03; | |
| background-image: | |
| linear-gradient(var(--text-secondary) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--text-secondary) 1px, transparent 1px); | |
| background-size: 60px 60px; | |
| } | |
| .container { | |
| position: relative; | |
| z-index: 10; | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| padding: 24px; | |
| min-height: 100vh; | |
| } | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 32px; | |
| padding-bottom: 24px; | |
| border-bottom: 1px solid var(--border); | |
| flex-wrap: wrap; | |
| gap: 16px; | |
| } | |
| .header-left h1 { | |
| font-size: clamp(1.5rem, 4vw, 2rem); | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| background: linear-gradient(135deg, var(--text-primary) 0%, var(--text-secondary) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .header-left p { | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| margin-top: 4px; | |
| } | |
| .header-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 6px 12px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-md); | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| } | |
| .badge-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: var(--accent-solar); | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.6; transform: scale(0.9); } | |
| } | |
| .anycoder-link { | |
| color: var(--accent-blue); | |
| text-decoration: none; | |
| font-weight: 500; | |
| transition: var(--transition-fast); | |
| } | |
| .anycoder-link:hover { | |
| text-decoration: underline; | |
| } | |
| .main-grid { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 24px; | |
| } | |
| @media (min-width: 1200px) { | |
| .main-grid { | |
| grid-template-columns: 1fr 420px; | |
| } | |
| } | |
| .chart-section { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 24px; | |
| box-shadow: var(--shadow-card); | |
| } | |
| .chart-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| } | |
| .chart-title { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .chart-legend { | |
| display: flex; | |
| gap: 20px; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| } | |
| .legend-dot { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 3px; | |
| } | |
| .legend-dot.solar { | |
| background: var(--accent-solar); | |
| box-shadow: 0 0 12px var(--accent-solar-glow); | |
| } | |
| .legend-dot.fossil { | |
| background: var(--accent-fossil); | |
| box-shadow: 0 0 12px var(--accent-fossil-glow); | |
| } | |
| .chart-container { | |
| position: relative; | |
| height: 450px; | |
| width: 100%; | |
| } | |
| @media (max-width: 768px) { | |
| .chart-container { | |
| height: 350px; | |
| } | |
| } | |
| .crossover-info { | |
| margin-top: 20px; | |
| padding: 16px; | |
| background: var(--bg-tertiary); | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border); | |
| } | |
| .crossover-title { | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| margin-bottom: 8px; | |
| } | |
| .crossover-value { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 1.4rem; | |
| font-weight: 600; | |
| color: var(--accent-blue); | |
| } | |
| .crossover-note { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| margin-top: 4px; | |
| } | |
| .controls-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .control-panel { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| overflow: hidden; | |
| box-shadow: var(--shadow-card); | |
| } | |
| .panel-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 16px 20px; | |
| background: var(--bg-tertiary); | |
| border-bottom: 1px solid var(--border); | |
| cursor: pointer; | |
| transition: var(--transition-fast); | |
| } | |
| .panel-header:hover { | |
| background: var(--bg-secondary); | |
| } | |
| .panel-icon { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: var(--radius-md); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.1rem; | |
| } | |
| .panel-icon.solar { | |
| background: rgba(16, 185, 129, 0.15); | |
| color: var(--accent-solar); | |
| } | |
| .panel-icon.fossil { | |
| background: rgba(249, 115, 22, 0.15); | |
| color: var(--accent-fossil); | |
| } | |
| .panel-title-group { | |
| flex: 1; | |
| } | |
| .panel-title { | |
| font-weight: 600; | |
| font-size: 0.95rem; | |
| color: var(--text-primary); | |
| } | |
| .panel-subtitle { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| margin-top: 2px; | |
| } | |
| .panel-toggle { | |
| width: 24px; | |
| height: 24px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--text-muted); | |
| transition: var(--transition-normal); | |
| } | |
| .panel-toggle.collapsed { | |
| transform: rotate(-90deg); | |
| } | |
| .panel-content { | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| max-height: 800px; | |
| overflow: hidden; | |
| transition: max-height 0.4s ease, padding 0.4s ease, opacity 0.3s ease; | |
| } | |
| .panel-content.collapsed { | |
| max-height: 0; | |
| padding-top: 0; | |
| padding-bottom: 0; | |
| opacity: 0; | |
| } | |
| .slider-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .slider-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .slider-label { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| } | |
| .slider-value { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.85rem; | |
| color: var(--text-primary); | |
| background: var(--bg-tertiary); | |
| padding: 4px 10px; | |
| border-radius: var(--radius-sm); | |
| min-width: 80px; | |
| text-align: right; | |
| } | |
| .slider-track { | |
| position: relative; | |
| height: 6px; | |
| background: var(--bg-tertiary); | |
| border-radius: 3px; | |
| margin-top: 4px; | |
| } | |
| .slider-fill { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| height: 100%; | |
| border-radius: 3px; | |
| transition: width 0.1s ease; | |
| } | |
| .slider-fill.solar { | |
| background: linear-gradient(90deg, var(--accent-solar), rgba(16, 185, 129, 0.6)); | |
| } | |
| .slider-fill.fossil { | |
| background: linear-gradient(90deg, var(--accent-fossil), rgba(249, 115, 22, 0.6)); | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 100%; | |
| height: 6px; | |
| background: transparent; | |
| cursor: pointer; | |
| position: relative; | |
| z-index: 2; | |
| margin: 0; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: var(--text-primary); | |
| border: 3px solid var(--bg-primary); | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); | |
| cursor: grab; | |
| transition: var(--transition-fast); | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| transform: scale(1.15); | |
| } | |
| input[type="range"]::-webkit-slider-thumb:active { | |
| cursor: grabbing; | |
| transform: scale(1.1); | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: var(--text-primary); | |
| border: 3px solid var(--bg-primary); | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); | |
| cursor: grab; | |
| transition: var(--transition-fast); | |
| } | |
| input[type="range"]:focus { | |
| outline: none; | |
| } | |
| input[type="range"]:focus-visible::-webkit-slider-thumb { | |
| box-shadow: 0 0 0 3px var(--accent-blue); | |
| } | |
| .slider-bounds { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 0.7rem; | |
| color: var(--text-muted); | |
| margin-top: 4px; | |
| } | |
| .opex-section { | |
| background: var(--bg-tertiary); | |
| border-radius: var(--radius-md); | |
| padding: 14px; | |
| margin-top: 4px; | |
| } | |
| .opex-title { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| margin-bottom: 12px; | |
| } | |
| .opex-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 16px; | |
| } | |
| @media (max-width: 400px) { | |
| .opex-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .summary-cards { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| margin-top: 8px; | |
| } | |
| .summary-card { | |
| background: var(--bg-tertiary); | |
| border-radius: var(--radius-md); | |
| padding: 14px; | |
| text-align: center; | |
| } | |
| .summary-card.solar { | |
| border-left: 3px solid var(--accent-solar); | |
| } | |
| .summary-card.fossil { | |
| border-left: 3px solid var(--accent-fossil); | |
| } | |
| .summary-label { | |
| font-size: 0.7rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .summary-value { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| margin-top: 4px; | |
| } | |
| .summary-card.solar .summary-value { | |
| color: var(--accent-solar); | |
| } | |
| .summary-card.fossil .summary-value { | |
| color: var(--accent-fossil); | |
| } | |
| .reset-section { | |
| display: flex; | |
| gap: 12px; | |
| margin-top: 8px; | |
| } | |
| .btn { | |
| flex: 1; | |
| padding: 12px 20px; | |
| border: none; | |
| border-radius: var(--radius-md); | |
| font-family: 'Space Grotesk', sans-serif; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: var(--transition-fast); | |
| } | |
| .btn-reset { | |
| background: var(--bg-tertiary); | |
| color: var(--text-secondary); | |
| border: 1px solid var(--border); | |
| } | |
| .btn-reset:hover { | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| } | |
| .btn-compare { | |
| background: var(--accent-blue); | |
| color: white; | |
| } | |
| .btn-compare:hover { | |
| background: #2563eb; | |
| transform: translateY(-1px); | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .animate-in { | |
| animation: fadeInUp 0.6s ease forwards; | |
| } | |
| .delay-1 { animation-delay: 0.1s; opacity: 0; } | |
| .delay-2 { animation-delay: 0.2s; opacity: 0; } | |
| .delay-3 { animation-delay: 0.3s; opacity: 0; } | |
| .delay-4 { animation-delay: 0.4s; opacity: 0; } | |
| @media (prefers-reduced-motion: reduce) { | |
| *, *::before, *::after { | |
| animation-duration: 0.01ms ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 16px; | |
| } | |
| header { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .chart-section { | |
| padding: 16px; | |
| } | |
| .panel-content { | |
| padding: 16px; | |
| } | |
| } | |
| .comparison-modal { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.8); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: opacity 0.3s ease, visibility 0.3s ease; | |
| } | |
| .comparison-modal.active { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .comparison-content { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 32px; | |
| max-width: 600px; | |
| width: 90%; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| transform: scale(0.9); | |
| transition: transform 0.3s ease; | |
| } | |
| .comparison-modal.active .comparison-content { | |
| transform: scale(1); | |
| } | |
| .comparison-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 24px; | |
| } | |
| .comparison-title { | |
| font-size: 1.3rem; | |
| font-weight: 600; | |
| } | |
| .close-btn { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| width: 36px; | |
| height: 36px; | |
| border-radius: var(--radius-md); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: var(--transition-fast); | |
| } | |
| .close-btn:hover { | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| } | |
| .comparison-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| .comparison-table th, | |
| .comparison-table td { | |
| padding: 12px 16px; | |
| text-align: left; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .comparison-table th { | |
| color: var(--text-muted); | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .comparison-table td { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.9rem; | |
| } | |
| .comparison-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .solar-text { color: var(--accent-solar); } | |
| .fossil-text { color: var(--accent-fossil); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-atmosphere"></div> | |
| <div class="grid-pattern"></div> | |
| <div class="container"> | |
| <header class="animate-in"> | |
| <div class="header-left"> | |
| <h1>LCOE vs WACC Analysis</h1> | |
| <p>Compare levelized cost of energy between Solar PV and Fossil generation</p> | |
| </div> | |
| <div class="header-right"> | |
| <div class="badge"> | |
| <span class="badge-dot"></span> | |
| Live Analysis | |
| </div> | |
| <span>Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" class="anycoder-link" target="_blank">anycoder</a></span> | |
| </div> | |
| </header> | |
| <div class="main-grid"> | |
| <div class="chart-section animate-in delay-1"> | |
| <div class="chart-header"> | |
| <h2 class="chart-title">LCOE Sensitivity to Cost of Capital</h2> | |
| <div class="chart-legend"> | |
| <div class="legend-item"> | |
| <span class="legend-dot solar"></span> | |
| Solar PV | |
| </div> | |
| <div class="legend-item"> | |
| <span class="legend-dot fossil"></span> | |
| Fossil | |
| </div> | |
| </div> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="lcoeChart"></canvas> | |
| </div> | |
| <div class="crossover-info"> | |
| <div class="crossover-title">Crossover Point</div> | |
| <div class="crossover-value" id="crossoverValue">Calculating...</div> | |
| <div class="crossover-note" id="crossoverNote">WACC where Solar PV becomes cheaper than Fossil</div> | |
| </div> | |
| </div> | |
| <div class="controls-section"> | |
| <div class="control-panel animate-in delay-2"> | |
| <div class="panel-header" onclick="togglePanel('solar')"> | |
| <div class="panel-icon solar"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="5" /> | |
| <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" /> | |
| </svg> | |
| </div> | |
| <div class="panel-title-group"> | |
| <div class="panel-title">Solar PV Parameters</div> | |
| <div class="panel-subtitle">Renewable energy source</div> | |
| </div> | |
| <div class="panel-toggle" id="solar-toggle"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polyline points="6 9 12 15 18 9" /> | |
| </svg> | |
| </div> | |
| </div> | |
| <div class="panel-content" id="solar-content"> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">CAPEX</span> | |
| <span class="slider-value" id="solar-capex-value">700 $/kW</span> | |
| </div> | |
| <div class="slider-track"> | |
| <div class="slider-fill solar" id="solar-capex-fill" style="width: 50%"></div> | |
| </div> | |
| <input type="range" id="solar-capex" min="200" max="1200" value="700" step="10" oninput="updateSlider('solar', 'capex', this.value)"> | |
| <div class="slider-bounds"> | |
| <span>200 $/kW</span> | |
| <span>1,200 $/kW</span> | |
| </div> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">Capacity Factor</span> | |
| <span class="slider-value" id="solar-cf-value">22%</span> | |
| </div> | |
| <div class="slider-track"> | |
| <div class="slider-fill solar" id="solar-cf-fill" style="width: 46.7%"></div> | |
| </div> | |
| <input type="range" id="solar-cf" min="15" max="30" value="22" step="0.5" oninput="updateSlider('solar', 'cf', this.value)"> | |
| <div class="slider-bounds"> | |
| <span>15%</span> | |
| <span>30%</span> | |
| </div> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">Lifecycle</span> | |
| <span class="slider-value" id="solar-life-value">25 years</span> | |
| </div> | |
| <div class="slider-track"> | |
| <div class="slider-fill solar" id="solar-life-fill" style="width: 75%"></div> | |
| </div> | |
| <input type="range" id="solar-life" min="10" max="30" value="25" step="1" oninput="updateSlider('solar', 'life', this.value)"> | |
| <div class="slider-bounds"> | |
| <span>10 years</span> | |
| <span>30 years</span> | |
| </div> | |
| </div> | |
| <div class="opex-section"> | |
| <div class="opex-title">Operating Expenses</div> | |
| <div class="opex-grid"> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">Fixed OPEX</span> | |
| <span class="slider-value" id="solar-fom-value">15 $/kW-yr</span> | |
| </div> | |
| <input type="range" id="solar-fom" min="5" max="40" value="15" step="1" oninput="updateSlider('solar', 'fom', this.value)"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">Variable OPEX</span> | |
| <span class="slider-value" id="solar-vom-value">0 $/MWh</span> | |
| </div> | |
| <input type="range" id="solar-vom" min="0" max="10" value="0" step="0.5" oninput="updateSlider('solar', 'vom', this.value)"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="summary-cards"> | |
| <div class="summary-card solar"> | |
| <div class="summary-label">LCOE at 5% WACC</div> | |
| <div class="summary-value" id="solar-lcoe-5">--</div> | |
| </div> | |
| <div class="summary-card solar"> | |
| <div class="summary-label">LCOE at 10% WACC</div> | |
| <div class="summary-value" id="solar-lcoe-10">--</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="control-panel animate-in delay-3"> | |
| <div class="panel-header" onclick="togglePanel('fossil')"> | |
| <div class="panel-icon fossil"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 2c-4 4-6 8-6 12a6 6 0 0012 0c0-4-2-8-6-12z" /> | |
| <path d="M12 8v8" /> | |
| </svg> | |
| </div> | |
| <div class="panel-title-group"> | |
| <div class="panel-title">Fossil Fuel Parameters</div> | |
| <div class="panel-subtitle">Conventional generation</div> | |
| </div> | |
| <div class="panel-toggle" id="fossil-toggle"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polyline points="6 9 12 15 18 9" /> | |
| </svg> | |
| </div> | |
| </div> | |
| <div class="panel-content" id="fossil-content"> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">CAPEX</span> | |
| <span class="slider-value" id="fossil-capex-value">2,000 $/kW</span> | |
| </div> | |
| <div class="slider-track"> | |
| <div class="slider-fill fossil" id="fossil-capex-fill" style="width: 33.3%"></div> | |
| </div> | |
| <input type="range" id="fossil-capex" min="500" max="5000" value="2000" step="50" oninput="updateSlider('fossil', 'capex', this.value)"> | |
| <div class="slider-bounds"> | |
| <span>500 $/kW</span> | |
| <span>5,000 $/kW</span> | |
| </div> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">Capacity Factor</span> | |
| <span class="slider-value" id="fossil-cf-value">70%</span> | |
| </div> | |
| <div class="slider-track"> | |
| <div class="slider-fill fossil" id="fossil-cf-fill" style="width: 75%"></div> | |
| </div> | |
| <input type="range" id="fossil-cf" min="40" max="90" value="70" step="1" oninput="updateSlider('fossil', 'cf', this.value)"> | |
| <div class="slider-bounds"> | |
| <span>40%</span> | |
| <span>90%</span> | |
| </div> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">Lifecycle</span> | |
| <span class="slider-value" id="fossil-life-value">30 years</span> | |
| </div> | |
| <div class="slider-track"> | |
| <div class="slider-fill fossil" id="fossil-life-fill" style="width: 50%"></div> | |
| </div> | |
| <input type="range" id="fossil-life" min="20" max="40" value="30" step="1" oninput="updateSlider('fossil', 'life', this.value)"> | |
| <div class="slider-bounds"> | |
| <span>20 years</span> | |
| <span>40 years</span> | |
| </div> | |
| </div> | |
| <div class="opex-section"> | |
| <div class="opex-title">Operating Expenses</div> | |
| <div class="opex-grid"> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">Fixed OPEX</span> | |
| <span class="slider-value" id="fossil-fom-value">30 $/kW-yr</span> | |
| </div> | |
| <input type="range" id="fossil-fom" min="10" max="80" value="30" step="1" oninput="updateSlider('fossil', 'fom', this.value)"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-header"> | |
| <span class="slider-label">Variable OPEX</span> | |
| <span class="slider-value" id="fossil-vom-value">25 $/MWh</span> | |
| </div> | |
| <input type="range" id="fossil-vom" min="10" max="80" value="25" step="1" oninput="updateSlider('fossil', 'vom', this.value)"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="summary-cards"> | |
| <div class="summary-card fossil"> | |
| <div class="summary-label">LCOE at 5% WACC</div> | |
| <div class="summary-value" id="fossil-lcoe-5">--</div> | |
| </div> | |
| <div class="summary-card fossil"> | |
| <div class="summary-label">LCOE at 10% WACC</div> | |
| <div class="summary-value" id="fossil-lcoe-10">--</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="reset-section animate-in delay-4"> | |
| <button class="btn btn-reset" onclick="resetDefaults()">Reset Defaults</button> | |
| <button class="btn btn-compare" onclick="showComparison()">Compare</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="comparison-modal" id="comparisonModal"> | |
| <div class="comparison-content"> | |
| <div class="comparison-header"> | |
| <h3 class="comparison-title">Side-by-Side Comparison</h3> | |
| <button class="close-btn" onclick="closeComparison()"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <line x1="18" y1="6" x2="6" y2="18" /> | |
| <line x1="6" y1="6" x2="18" y2="18" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <table class="comparison-table"> | |
| <thead> | |
| <tr> | |
| <th>Parameter</th> | |
| <th>Solar PV</th> | |
| <th>Fossil</th> | |
| </tr> | |
| </thead> | |
| <tbody id="comparisonBody"> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <script> | |
| const params = { | |
| solar: { capex: 700, cf: 22, life: 25, fom: 15, vom: 0 }, | |
| fossil: { capex: 2000, cf: 70, life: 30, fom: 30, vom: 25 } | |
| }; | |
| const defaults = JSON.parse(JSON.stringify(params)); | |
| let chart = null; | |
| const waccRange = []; | |
| for (let w = 0.5; w <= 15; w += 0.25) { | |
| waccRange.push(w); | |
| } | |
| function calculateLCOE(capex, cf, life, fom, vom, wacc) { | |
| const r = wacc / 100; | |
| const cfDecimal = cf / 100; | |
| const hoursPerYear = 8760; | |
| if (r === 0) { | |
| const annualEnergy = cfDecimal * hoursPerYear; | |
| const vomKwh = vom / 1000; | |
| return (capex / life + fom) / annualEnergy + vomKwh; | |
| } | |
| const crf = (r * Math.pow(1 + r, life)) / (Math.pow(1 + r, life) - 1); | |
| const annualEnergy = cfDecimal * hoursPerYear; | |
| const vomKwh = vom / 1000; | |
| const lcoe = (capex * crf + fom) / annualEnergy + vomKwh; | |
| return lcoe; | |
| } | |
| function generateLCOECurve(type) { | |
| const p = params[type]; | |
| return waccRange.map(wacc => ({ | |
| x: wacc, | |
| y: calculateLCOE(p.capex, p.cf, p.life, p.fom, p.vom, wacc) | |
| })); | |
| } | |
| function findCrossover() { | |
| const solarData = generateLCOECurve('solar'); | |
| const fossilData = generateLCOECurve('fossil'); | |
| for (let i = 0; i < solarData.length; i++) { | |
| if (solarData[i].y < fossilData[i].y) { | |
| if (i === 0) return { wacc: waccRange[0], solarLCOE: solarData[0].y, fossilLCOE: fossilData[0].y }; | |
| const prevWacc = waccRange[i - 1]; | |
| const currWacc = waccRange[i]; | |
| const prevDiff = solarData[i - 1].y - fossilData[i - 1].y; | |
| const currDiff = solarData[i].y - fossilData[i].y; | |
| const ratio = prevDiff / (prevDiff - currDiff); | |
| const crossoverWacc = prevWacc + ratio * (currWacc - prevWacc); | |
| const crossoverLCOE = solarData[i - 1].y + ratio * (solarData[i].y - solarData[i - 1].y); | |
| return { wacc: crossoverWacc, solarLCOE: crossoverLCOE, fossilLCOE: crossoverLCOE }; | |
| } | |
| } | |
| return null; | |
| } | |
| function initChart() { | |
| const ctx = document.getElementById('lcoeChart').getContext('2d'); | |
| const solarData = generateLCOECurve('solar'); | |
| const fossilData = generateLCOECurve('fossil'); | |
| chart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| datasets: [ | |
| { | |
| label: 'Solar PV', | |
| data: solarData, | |
| borderColor: '#10b981', | |
| backgroundColor: 'rgba(16, 185, 129, 0.1)', | |
| borderWidth: 3, | |
| fill: true, | |
| tension: 0.4, | |
| pointRadius: 0, | |
| pointHoverRadius: 6, | |
| pointHoverBackgroundColor: '#10b981', | |
| pointHoverBorderColor: '#fff', | |
| pointHoverBorderWidth: 2 | |
| }, | |
| { | |
| label: 'Fossil', | |
| data: fossilData, | |
| borderColor: '#f97316', | |
| backgroundColor: 'rgba(249, 115, 22, 0.1)', | |
| borderWidth: 3, | |
| fill: true, | |
| tension: 0.4, | |
| pointRadius: 0, | |
| pointHoverRadius: 6, | |
| pointHoverBackgroundColor: '#f97316', | |
| pointHoverBorderColor: '#fff', | |
| pointHoverBorderWidth: 2 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { | |
| mode: 'index', | |
| intersect: false | |
| }, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| backgroundColor: 'rgba(17, 24, 39, 0.95)', | |
| titleColor: '#f1f5f9', | |
| bodyColor: '#94a3b8', | |
| borderColor: '#2a3548', | |
| borderWidth: 1, | |
| padding: 12, | |
| cornerRadius: 8, | |
| titleFont: { family: "'Space Grotesk', sans-serif", size: 14, weight: 600 }, | |
| bodyFont: { family: "'JetBrains Mono', monospace", size: 12 }, | |
| callbacks: { | |
| title: function(context) { | |
| return `WACC: ${context[0].parsed.x.toFixed(1)}%`; | |
| }, | |
| label: function(context) { | |
| const label = context.dataset.label; | |
| const value = context.parsed.y; | |
| return `${label}: $${value.toFixed(3)}/kWh`; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| type: 'linear', | |
| min: 0.5, | |
| max: 15, | |
| title: { | |
| display: true, | |
| text: 'WACC (%)', | |
| color: '#94a3b8', | |
| font: { family: "'Space Grotesk', sans-serif", size: 13, weight: 500 } | |
| }, | |
| grid: { color: 'rgba(42, 53, 72, 0.5)', lineWidth: 1 }, | |
| ticks: { | |
| color: '#64748b', | |
| font: { family: "'JetBrains Mono', monospace", size: 11 }, | |
| callback: function(value) { return value.toFixed(1) + '%'; } | |
| } | |
| }, | |
| y: { | |
| min: 0.01, | |
| max: 0.20, | |
| title: { | |
| display: true, | |
| text: 'LCOE ($/kWh)', | |
| color: '#94a3b8', | |
| font: { family: "'Space Grotesk', sans-serif", size: 13, weight: 500 } | |
| }, | |
| grid: { color: 'rgba(42, 53, 72, 0.5)', lineWidth: 1 }, | |
| ticks: { | |
| color: '#64748b', | |
| font: { family: "'JetBrains Mono', monospace", size: 11 }, | |
| callback: function(value) { return '$' + value.toFixed(2); } | |
| } | |
| } | |
| }, | |
| animation: { duration: 500, easing: 'easeOutQuart' } | |
| } | |
| }); | |
| updateCrossover(); | |
| updateSummaryCards(); | |
| } | |
| function updateChart() { | |
| if (!chart) return; | |
| const solarData = generateLCOECurve('solar'); | |
| const fossilData = generateLCOECurve('fossil'); | |
| chart.data.datasets[0].data = solarData; | |
| chart.data.datasets[1].data = fossilData; | |
| chart.update('none'); | |
| updateCrossover(); | |
| updateSummaryCards(); | |
| } | |
| function updateCrossover() { | |
| const crossover = findCrossover(); | |
| const crossoverValueEl = document.getElementById('crossoverValue'); | |
| const crossoverNoteEl = document.getElementById('crossoverNote'); | |
| if (crossover) { | |
| crossoverValueEl.textContent = `WACC: ${crossover.wacc.toFixed(2)}%`; | |
| crossoverNoteEl.textContent = `At this point, both technologies cost $${crossover.solarLCOE.toFixed(4)}/kWh`; | |
| } else { | |
| crossoverValueEl.textContent = 'No crossover in range'; | |
| crossoverNoteEl.textContent = 'Solar PV is always more expensive than Fossil in this WACC range'; | |
| } | |
| } | |
| function updateSummaryCards() { | |
| const solarLCOE5 = calculateLCOE(params.solar.capex, params.solar.cf, params.solar.life, params.solar.fom, params.solar.vom, 5); | |
| const solarLCOE10 = calculateLCOE(params.solar.capex, params.solar.cf, params.solar.life, |