mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(panels): Hormuz — remove summary text, per-chart colors, interactive tooltip (#2231)
* fix(panels): Hormuz — remove summary text, per-chart colors, interactive tooltip
- Remove summary text blob (user-facing text from WTO page)
- Per-chart colors: crude=#e67e22, LNG=#1abc9c, fertilizer=#9b59b6, agriculture=#27ae60
- Zero-value bars in red to signal disruption
- Interactive bar hover: fixed-position tooltip shows date + value + unit
- Event delegation on stable this.element so setContent debounce does not break listeners
* fix(panels): label-based unit detection, deduplicate bar data attrs, clamp tooltip
- Revert unit from index-based (idx===0) to label-based (chart.label.includes('crude_oil'))
so unit stays correct if seed CHART_CONFIGS order ever changes
- Remove redundant data attrs from visible rects; only hit rects need them
- Clamp tooltip top to Math.max(8, ...) to prevent viewport overflow on top charts
This commit is contained in:
@@ -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 `<div style="height:${height}px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);font-size:10px">No data</div>`;
|
||||
function barChart(series: HormuzSeries[], color: string, unit: string, width = 280, height = 52): string {
|
||||
if (!series.length) return `<div style="height:${height}px;display:flex;align-items:center;color:var(--text-dim);font-size:10px">No data</div>`;
|
||||
|
||||
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 `<rect x="0" y="${height - h}" width="${barW}" height="${h}" fill="${color}" rx="1"/>`;
|
||||
});
|
||||
const barW = Math.max(2, Math.floor((width - series.length) / series.length));
|
||||
|
||||
let x = 0;
|
||||
const positioned = bars.map(b => {
|
||||
const rect = `<g transform="translate(${x},0)">${b}</g>`;
|
||||
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 = `<rect x="${x}" y="${height - h}" width="${barW}" height="${h}" fill="${fill}" rx="1"/>`;
|
||||
x += barW + 1;
|
||||
return rect;
|
||||
});
|
||||
|
||||
return `<svg width="${width}" height="${height}" style="display:block;overflow:visible">
|
||||
${positioned.join('')}
|
||||
</svg>`;
|
||||
x = 0;
|
||||
const hits = series.map(p => {
|
||||
const hit = `<rect class="hbar" x="${x}" y="0" width="${barW}" height="${height}" fill="transparent" data-date="${escapeHtml(p.date)}" data-val="${p.value}" data-unit="${escapeHtml(unit)}" style="cursor:crosshair"/>`;
|
||||
x += barW + 1;
|
||||
return hit;
|
||||
});
|
||||
|
||||
return `<svg class="hz-svg" width="${width}" height="${height}" style="display:block;overflow:visible">${rects.join('')}${hits.join('')}</svg>`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div style="margin-bottom:10px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:3px">
|
||||
<div class="hz-chart" style="margin-bottom:12px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px">
|
||||
<span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.04em">${escapeHtml(chart.title)}</span>
|
||||
<span style="font-size:11px;font-weight:600;color:var(--text)">${escapeHtml(lastVal)} <span style="font-size:9px;color:var(--text-dim)">${unit} · ${escapeHtml(lastDate)}</span></span>
|
||||
<span style="font-size:11px;font-weight:600;color:${color}">${escapeHtml(lastVal)} <span style="font-size:9px;color:var(--text-dim)">${unit} · ${escapeHtml(lastDate)}</span></span>
|
||||
</div>
|
||||
${barChart(chart.series)}
|
||||
<div style="position:relative">${barChart(chart.series, color, unit)}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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<HTMLElement>('.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<HTMLElement>('.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('')
|
||||
: '<div style="color:var(--text-dim);font-size:11px;padding:8px 0">Chart data unavailable</div>';
|
||||
|
||||
const dateStr = d.updatedDate ? `<span style="font-size:10px;color:var(--text-dim)">${escapeHtml(d.updatedDate)}</span>` : '';
|
||||
const summary = d.summary ? `<div style="font-size:11px;color:var(--text-dim);margin:6px 0;line-height:1.4">${escapeHtml(d.summary)}</div>` : '';
|
||||
|
||||
const html = `
|
||||
<div style="padding:12px 14px">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<span style="background:${color};color:#fff;font-size:9px;font-weight:700;padding:2px 6px;border-radius:3px;letter-spacing:0.08em">${statusLabel(d.status)}</span>
|
||||
<div style="padding:12px 14px;position:relative">
|
||||
<div class="hz-tip" style="position:fixed;pointer-events:none;background:rgba(15,17,26,0.95);border:1px solid rgba(255,255,255,0.15);border-radius:4px;padding:3px 8px;font-size:10px;color:#fff;white-space:nowrap;z-index:9999;opacity:0;transition:opacity 0.08s;letter-spacing:0.02em"></div>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
|
||||
<span style="background:${sColor};color:#fff;font-size:9px;font-weight:700;padding:2px 6px;border-radius:3px;letter-spacing:0.08em">${d.status.toUpperCase()}</span>
|
||||
${dateStr}
|
||||
</div>
|
||||
${summary}
|
||||
<div style="border-top:1px solid rgba(255,255,255,0.07);padding-top:8px;margin-top:4px">
|
||||
${charts}
|
||||
</div>
|
||||
<div style="margin-top:6px;font-size:9px;color:var(--text-dim)">
|
||||
<div>${charts}</div>
|
||||
<div style="margin-top:4px;font-size:9px;color:var(--text-dim)">
|
||||
Source: <a href="${escapeHtml(d.attribution.url)}" target="_blank" rel="noopener" style="color:var(--text-dim);text-decoration:underline">${escapeHtml(d.attribution.source)}</a>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
Reference in New Issue
Block a user