feat: 5-year forecast area chart + regional adoption heatmap

Dashboard now uses /api/hype-cycle/enriched for forecast data.
Forecast chart: SVG area chart with adoption % per technology over 5 years.
Regional heatmap: table with market share intensity per region per tech.
Both render below the existing hype cycle table.
This commit is contained in:
Rene Fichtmueller 2026-03-30 20:57:08 +02:00
parent 9d9d9ed8ae
commit b238815cb5

View File

@ -164,6 +164,11 @@
box-shadow: var(--shadow-card);
transition: box-shadow 0.2s, border-color 0.2s;
}
.card-header {
font-size: 1rem; font-weight: 700; color: var(--text-bright);
margin-bottom: 1rem; padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.card:hover {
box-shadow: var(--shadow-hover);
}
@ -771,6 +776,36 @@
</table>
</div>
</div>
<div class="card mt" style="border-left:3px solid var(--cyan)">
<div class="panel-section" style="margin-top:0">Methodology: How the Hype Cycle is Calculated</div>
<div style="font-size:0.82rem;color:var(--text-dim);line-height:1.65">
<p style="margin:0.5rem 0">This Hype Cycle uses the <strong>Norton-Bass Multigenerational Diffusion Model</strong> to calculate adoption curves for optical transceiver technologies. The model extends the classic Bass diffusion model to handle multiple generations of technology that compete and cannibalize each other.</p>
<p style="margin:0.5rem 0"><strong>Key Parameters:</strong></p>
<ul style="margin:0.3rem 0;padding-left:1.2rem">
<li><strong>p (innovation coefficient)</strong> &mdash; Rate of adoption driven by external influence (marketing, industry events). Typically 0.01&ndash;0.05 for B2B networking equipment.</li>
<li><strong>q (imitation coefficient)</strong> &mdash; Rate of adoption driven by word-of-mouth and peer influence. Typically 0.3&ndash;0.5 for optical networking.</li>
<li><strong>m (market potential)</strong> &mdash; Total addressable market size, estimated from port shipment forecasts (LightCounting, Dell&rsquo;Oro).</li>
<li><strong>τ (introduction time)</strong> &mdash; Year when the technology became commercially available.</li>
</ul>
<p style="margin:0.5rem 0"><strong>Phase Classification:</strong> Technologies are classified into five phases based on their position on the adoption curve: <span class="b" style="background:#FF810018;color:#FF8100;border:1px solid #FF810033">Innovation Trigger</span> (0&ndash;16%), <span class="b" style="background:#FFa03018;color:#FFa030;border:1px solid #FFa03033">Peak of Inflated Expectations</span> (16&ndash;35%), <span class="b" style="background:#c1121f18;color:#c1121f;border:1px solid #c1121f33">Trough of Disillusionment</span> (35&ndash;55%), <span class="b" style="background:#55555518;color:#555;border:1px solid #55555533">Slope of Enlightenment</span> (55&ndash;80%), and <span class="b" style="background:#00000018;color:#000;border:1px solid #00000033">Plateau of Productivity</span> (80&ndash;100%).</p>
<p style="margin:0.5rem 0"><strong>Composite Score:</strong> A weighted blend of adoption velocity (40%), market momentum from pricing data (30%), and ecosystem maturity from compatibility entries (30%). Score ranges from 0&ndash;100.</p>
<p style="margin:0.5rem 0"><strong>Data Sources:</strong> Adoption timing derived from IEEE/MSA standard ratification dates, market data from aggregated pricing across 60+ vendors, and compatibility data from 29,000+ verified switch-transceiver pairs.</p>
</div>
</div>
</div>
<!-- 5-Year Forecast Chart -->
<div class="card mt">
<div class="card-header">5-Year Adoption Forecast</div>
<div id="forecast-chart" style="width:100%;overflow-x:auto"></div>
</div>
<!-- Regional Adoption Heatmap -->
<div class="card mt">
<div class="card-header">Regional Adoption Heatmap</div>
<div id="regional-heatmap" style="width:100%;overflow-x:auto"></div>
</div>
</div>
<!-- TRANSCEIVERS -->
@ -1532,8 +1567,115 @@ async function openHypeDetail(name) {
} catch(e) { el('panel-content').textContent = 'Error: ' + e.message; }
}
// ── Forecast Area Chart (SVG) ──────────────────────────────────────
var TECH_COLORS = ['#FF8100','#4deaff','#a78bfa','#34d399','#fbbf24','#f87171','#818cf8','#fb923c','#22d3ee','#e879f9','#94a3b8'];
function renderForecastChart(techs) {
var withFc = techs.filter(function(t) { return t.fiveYearForecast && t.fiveYearForecast.length > 0; });
if (!withFc.length) return '<div class="dim" style="padding:1rem;text-align:center">No forecast data — use enriched API endpoint</div>';
var W = 900, H = 320, P = 50, PR = 30, PT = 20, PB = 40;
var cw = W - P - PR, ch = H - PT - PB;
var years = withFc[0].fiveYearForecast.map(function(f) { return f.year; });
var numYears = years.length;
var svg = '<svg viewBox="0 0 ' + W + ' ' + H + '" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto">';
// Grid + Y axis
for (var y = 0; y <= 100; y += 25) {
var gy = PT + ch - (y / 100) * ch;
svg += '<line x1="' + P + '" y1="' + gy + '" x2="' + (P + cw) + '" y2="' + gy + '" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>';
svg += '<text x="' + (P - 8) + '" y="' + (gy + 4) + '" text-anchor="end" fill="rgba(255,255,255,0.4)" font-size="10" font-family="JetBrains Mono,monospace">' + y + '%</text>';
}
// X axis labels
for (var xi = 0; xi < numYears; xi++) {
var xx = P + (xi / (numYears - 1)) * cw;
svg += '<text x="' + xx + '" y="' + (H - 8) + '" text-anchor="middle" fill="rgba(255,255,255,0.5)" font-size="11" font-family="DM Sans,sans-serif">' + years[xi] + '</text>';
}
// Lines per technology
for (var ti = 0; ti < withFc.length; ti++) {
var t = withFc[ti];
var color = TECH_COLORS[ti % TECH_COLORS.length];
var pts = [];
for (var fi = 0; fi < t.fiveYearForecast.length; fi++) {
var f = t.fiveYearForecast[fi];
var fx = P + (fi / (numYears - 1)) * cw;
var fy = PT + ch - (Math.min(f.adoptionPct, 100) / 100) * ch;
pts.push(fx + ',' + fy);
}
// Area fill
svg += '<polygon points="' + pts.join(' ') + ' ' + (P + cw) + ',' + (PT + ch) + ' ' + P + ',' + (PT + ch) + '" fill="' + color + '" opacity="0.08"/>';
// Line
svg += '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + color + '" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>';
// Dots
for (var di = 0; di < pts.length; di++) {
var dp = pts[di].split(',');
svg += '<circle cx="' + dp[0] + '" cy="' + dp[1] + '" r="3" fill="' + color + '" stroke="#0a0b10" stroke-width="1.5"/>';
}
// Label at end
var lastPt = pts[pts.length - 1].split(',');
svg += '<text x="' + (parseFloat(lastPt[0]) + 6) + '" y="' + (parseFloat(lastPt[1]) + 3) + '" fill="' + color + '" font-size="9" font-weight="600" font-family="DM Sans,sans-serif">' + esc(t.technology) + '</text>';
}
svg += '</svg>';
return svg;
}
// ── Regional Adoption Heatmap ──────────────────────────────────────
function renderRegionalHeatmap(techs) {
var regions = ['North America (Hyperscale)', 'China (BAT/Hyperscale)', 'APAC (ex-China)', 'Europe', 'Rest of World'];
var shortRegions = ['NA', 'China', 'APAC', 'Europe', 'RoW'];
// We need regional data — fetch per tech
var h = '<table style="width:100%;border-collapse:collapse;font-size:0.8rem">';
h += '<thead><tr><th style="text-align:left;padding:6px 8px;color:var(--text-dim);font-weight:600">Technology</th>';
for (var ri = 0; ri < shortRegions.length; ri++) {
h += '<th style="padding:6px 8px;color:var(--text-dim);font-weight:600;text-align:center">' + shortRegions[ri] + '</th>';
}
h += '<th style="padding:6px 8px;color:var(--text-dim);font-weight:600;text-align:center">Peak Year</th></tr></thead><tbody id="heatmap-body"></tbody></table>';
return h;
}
async function loadRegionalData(techs) {
var body = document.getElementById('heatmap-body');
if (!body) return;
var html = '';
for (var ti = 0; ti < techs.length; ti++) {
var t = techs[ti];
var color = PC[t.phase] || '#8888a4';
try {
var rd = await api('/api/hype-cycle/regional/' + encodeURIComponent(t.technology));
var regions = rd.regions || [];
html += '<tr>';
html += '<td style="padding:6px 8px;font-weight:600;color:' + color + '">' + esc(t.technology) + '</td>';
for (var ri = 0; ri < 5; ri++) {
var r = regions[ri];
if (r) {
var share = r.marketSharePct || 0;
var opacity = Math.max(0.15, share / 45);
html += '<td style="padding:6px 8px;text-align:center;background:rgba(255,129,0,' + opacity.toFixed(2) + ');border-radius:4px"><span class="mono" style="font-size:0.75rem">' + share + '%</span><br><span style="font-size:0.65rem;color:var(--text-dim)">' + esc(r.adoptionPhase) + '</span></td>';
} else {
html += '<td style="padding:6px 8px;text-align:center;color:var(--text-dim)"></td>';
}
}
html += '<td style="padding:6px 8px;text-align:center" class="mono">' + (rd.globalPeakYear || '—') + '</td>';
html += '</tr>';
} catch(e) {
html += '<tr><td style="padding:6px 8px;color:' + color + '">' + esc(t.technology) + '</td><td colspan="6" style="padding:6px 8px;color:var(--text-dim)"></td></tr>';
}
}
buildDOM(body, html);
}
async function loadHypeCycle() {
var data = await api('/api/hype-cycle');
// Use enriched endpoint for forecast data
var data;
try {
data = await api('/api/hype-cycle/enriched');
} catch(e) {
data = await api('/api/hype-cycle');
}
var techs = data.technologies || [];
el('hype-year').textContent = data.year;
@ -1560,6 +1702,17 @@ async function loadHypeCycle() {
el('hype-table').querySelectorAll('tr.clickable').forEach(function(row) {
row.addEventListener('click', function() { openHypeDetail(this.getAttribute('data-tech')); });
});
// Render forecast chart (only if enriched data has forecasts)
var fcEl = document.getElementById('forecast-chart');
if (fcEl) buildDOM(fcEl, renderForecastChart(techs));
// Render regional heatmap shell, then load data async
var hmEl = document.getElementById('regional-heatmap');
if (hmEl) {
buildDOM(hmEl, renderRegionalHeatmap(techs));
loadRegionalData(techs);
}
}
// TRANSCEIVERS