anycoder-f705e2df / index.html
Yasharsin's picture
Upload folder using huggingface_hub
6ca7a22 verified
<!DOCTYPE html>
<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 !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
@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,