diff --git a/src/components/HormuzPanel.ts b/src/components/HormuzPanel.ts index 64f0eec63..232a24ad8 100644 --- a/src/components/HormuzPanel.ts +++ b/src/components/HormuzPanel.ts @@ -3,60 +3,63 @@ import { escapeHtml } from '@/utils/sanitize'; import { fetchHormuzTracker } from '@/services/hormuz-tracker'; import type { HormuzTrackerData, HormuzChart, HormuzSeries } from '@/services/hormuz-tracker'; +const CHART_COLORS = ['#e67e22', '#1abc9c', '#9b59b6', '#27ae60']; +const ZERO_COLOR = 'rgba(231,76,60,0.5)'; + function statusColor(status: string): string { switch (status) { - case 'closed': return '#e74c3c'; - case 'disrupted': return '#e67e22'; + case 'closed': return '#e74c3c'; + case 'disrupted': return '#e67e22'; case 'restricted': return '#f39c12'; - default: return '#2ecc71'; + default: return '#2ecc71'; } } -function statusLabel(status: string): string { - return status.toUpperCase(); -} - -function barChart(series: HormuzSeries[], width = 280, height = 48): string { - if (!series.length) return `
No data
`; +function barChart(series: HormuzSeries[], color: string, unit: string, width = 280, height = 52): string { + if (!series.length) return `
No data
`; const max = Math.max(...series.map(p => p.value), 1); - const barW = Math.max(1, Math.floor((width - series.length) / series.length)); - const bars = series.map(p => { - const h = Math.round((p.value / max) * (height - 2)); - const color = p.value === 0 ? '#e74c3c' : '#3498db'; - return ``; - }); + const barW = Math.max(2, Math.floor((width - series.length) / series.length)); let x = 0; - const positioned = bars.map(b => { - const rect = `${b}`; + const rects = series.map(p => { + const h = Math.max(p.value > 0 ? 2 : 1, Math.round((p.value / max) * (height - 2))); + const fill = p.value === 0 ? ZERO_COLOR : color; + const rect = ``; x += barW + 1; return rect; }); - return ` - ${positioned.join('')} - `; + x = 0; + const hits = series.map(p => { + const hit = ``; + x += barW + 1; + return hit; + }); + + return `${rects.join('')}${hits.join('')}`; } -function renderChart(chart: HormuzChart): string { +function renderChart(chart: HormuzChart, idx: number): string { + const color = CHART_COLORS[idx % CHART_COLORS.length] ?? '#3498db'; const last = chart.series[chart.series.length - 1]; const lastVal = last ? last.value.toFixed(0) : 'N/A'; const lastDate = last ? last.date.slice(5) : ''; const unit = chart.label.includes('crude_oil') ? 'kt/day' : 'units'; return ` -
-
+
+
${escapeHtml(chart.title)} - ${escapeHtml(lastVal)} ${unit} · ${escapeHtml(lastDate)} + ${escapeHtml(lastVal)} ${unit} · ${escapeHtml(lastDate)}
- ${barChart(chart.series)} +
${barChart(chart.series, color, unit)}
`; } export class HormuzPanel extends Panel { private data: HormuzTrackerData | null = null; + private tooltipBound = false; constructor() { super({ id: 'hormuz-tracker', title: 'Hormuz Trade Tracker', showCount: false }); @@ -72,6 +75,7 @@ export class HormuzPanel extends Panel { } this.data = data; this.renderPanel(); + this.bindTooltip(); return true; } catch (e) { this.showError(e instanceof Error ? e.message : 'Failed to load', () => void this.fetchData()); @@ -79,29 +83,52 @@ export class HormuzPanel extends Panel { } } + private bindTooltip(): void { + if (this.tooltipBound || !this.element) return; + this.tooltipBound = true; + + this.element.addEventListener('mousemove', (e: Event) => { + const target = e.target as Element; + if (!target.classList?.contains('hbar')) return; + const date = (target.getAttribute('data-date') ?? '').slice(5); + const val = target.getAttribute('data-val') ?? ''; + const unit = target.getAttribute('data-unit') ?? ''; + const tip = this.element?.querySelector('.hz-tip'); + if (!tip) return; + const barRect = (target as SVGRectElement).getBoundingClientRect(); + tip.style.left = `${barRect.left + barRect.width / 2}px`; + tip.style.top = `${Math.max(8, barRect.top - 28)}px`; + tip.style.transform = 'translateX(-50%)'; + tip.style.opacity = '1'; + tip.textContent = `${date} ${val} ${unit}`; + }); + + this.element.addEventListener('mouseleave', () => { + const tip = this.element?.querySelector('.hz-tip'); + if (tip) tip.style.opacity = '0'; + }); + } + private renderPanel(): void { if (!this.data) return; const d = this.data; - const color = statusColor(d.status); + const sColor = statusColor(d.status); const charts = d.charts.length - ? d.charts.map(c => renderChart(c)).join('') + ? d.charts.map((c, i) => renderChart(c, i)).join('') : '
Chart data unavailable
'; const dateStr = d.updatedDate ? `${escapeHtml(d.updatedDate)}` : ''; - const summary = d.summary ? `
${escapeHtml(d.summary)}
` : ''; const html = ` -
-
- ${statusLabel(d.status)} +
+
+
+ ${d.status.toUpperCase()} ${dateStr}
- ${summary} -
- ${charts} -
-
+
${charts}
+
`;