import { useEffect, useMemo, useState } from 'preact/hooks'; import { explainPanelQuestion, getCreditCycle } from '../api.js'; import { useECharts, useThemeVersion } from '../charts/useECharts.js'; import { colors, chartText, gridLine, inkMuted, surface, baselineColor } from '../palette.js'; import { fmtPct, runDate } from '../format.js'; import Panel from './Panel.jsx'; const prefersReducedMotion = () => typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; // Fixed series -> hue SLOT assignment (never cycled): identity is stable // even if a series is missing from the payload. Hues are resolved live in // buildOption so light/dark steps apply. const SERIES_SPEC = [ { key: 'observed', name: 'Observed default rate', slot: 'orange', dash: 'solid', width: 1.5 }, { key: 'ttc', name: 'TTC PD', slot: 'gray', dash: 'dashed', width: 2 }, { key: 'pit', name: 'PIT PD', slot: 'accent', dash: 'solid', width: 2 }, { key: 'hybrid', name: 'Hybrid PD (α=0.5)', slot: 'purple', dash: 'solid', width: 2 }, ]; function buildOption(data, reducedMotion) { const c = colors(); const text = chartText(); const grid = gridLine(); const muted = inkMuted(); const base = baselineColor(); const surf = surface(); const present = SERIES_SPEC.filter((s) => Array.isArray(data[s.key])).map( (s) => ({ ...s, color: c[s.slot] }), ); return { textStyle: text, backgroundColor: 'transparent', legend: { top: 0, icon: 'roundRect', itemWidth: 14, itemHeight: 3, textStyle: { color: muted, fontSize: 12, fontFamily: text.fontFamily }, }, grid: { left: 56, right: 16, top: 36, bottom: 36, containLabel: true }, tooltip: { trigger: 'axis', axisPointer: { type: 'line', lineStyle: { color: base } }, backgroundColor: surf, borderColor: base, borderWidth: 1, textStyle: { color: text.color }, valueFormatter: (v) => fmtPct(v), }, xAxis: { type: 'category', data: data.calendar, axisLabel: { color: muted, fontSize: 12, fontFamily: text.fontFamily }, axisLine: { lineStyle: { color: base } }, axisTick: { show: false }, }, yAxis: { type: 'value', name: 'Quarterly PD', nameTextStyle: { color: muted, fontFamily: text.fontFamily }, axisLabel: { color: muted, formatter: (v) => fmtPct(v, 1), fontFamily: text.fontFamily }, axisLine: { show: false }, axisTick: { show: false }, splitLine: { lineStyle: { color: grid, type: 'solid' } }, }, series: present.map((s) => ({ name: s.name, type: 'line', data: data[s.key], showSymbol: false, lineStyle: { color: s.color, width: s.width, type: s.dash, cap: 'round', join: 'round' }, itemStyle: { color: s.color }, emphasis: { focus: 'series' }, })), animationDuration: reducedMotion ? 0 : 300, }; } // Adapt the contract's {points:[{calendar, z, observed_dr, ttc_pd, pit_pd}]} // into parallel arrays for the line-chart option builder (chart-shape-only // reshaping, no business arithmetic). function adapt(payload) { return { calendar: payload.points.map((p) => p.calendar), ttc: payload.points.map((p) => p.ttc_pd), pit: payload.points.map((p) => p.pit_pd), observed: payload.points.map((p) => p.observed_dr), rho: payload.rho, }; } function CreditCycleTable({ data }) { return (
| Quarter | Observed default rate (%) | TTC PD (%) | PIT PD (%) |
|---|---|---|---|
| {cal} | {fmtPct(data.observed[i])} | {fmtPct(data.ttc[i])} | {fmtPct(data.pit[i])} |