/* steam.js — Steam Advisory 대시보드 (라이브 uPlot + 백테스트 ECharts) */
paneInit.steam = stInit;

let stTimer = null;
let stLiveData = [];       // {ts, recOp, actOp} history for uPlot
const ST_MAX_POINTS = 360; // 30min at 5s intervals
let stUplot = null;

/* ── Initialization ── */
async function stInit() {
  try {
    if (stTimer) { clearInterval(stTimer); stTimer = null; }

    // 탭 전환
    document.querySelectorAll('.steam-tab').forEach(tab => {
      tab.onclick = () => {
        document.querySelectorAll('.steam-tab').forEach(t => t.classList.remove('active'));
        document.querySelectorAll('.st-pane').forEach(p => p.classList.remove('active'));
        tab.classList.add('active');
        const pane = document.getElementById('st-' + tab.dataset.stTab);
        if (pane) pane.classList.add('active');
        if (tab.dataset.stTab === 'backtest') stBtInit();
        else if (tab.dataset.stTab === 'temp') stTempInit();
      };
    });

    // 컬럼 선택 로드
    await stLoadColumns();

    // 라이브 — 조회 버튼으로 시작
    document.getElementById('st-col').onchange = () => { stLiveData = []; if (stTimer) { clearInterval(stTimer); stTimer = null; } };
    document.getElementById('st-live-load').onclick = stLiveStart;

    // 백테스트 서브탭
    document.querySelectorAll('.st-bt-tab').forEach(tab => {
      tab.onclick = () => {
        document.querySelectorAll('.st-bt-tab').forEach(t => t.classList.remove('active'));
        document.querySelectorAll('.st-bt-pane').forEach(p => p.classList.remove('active'));
        tab.classList.add('active');
        const name = tab.dataset.stBt;
        const pane = document.getElementById('st-bt-' + name);
        if (pane) pane.classList.add('active');
        if (stBtData && !stRendered.has(name)) {
          stRendered.add(name);
          if (name === 'startup') stRenderStartup(stBtData);
          else if (name === 'shadow') stRenderShadow(stBtData, 'shadow');
          else if (name === 'advisory') stRenderShadow(stBtData, 'advisory');
        }
      };
    });
    document.getElementById('st-bt-load').onclick = () => stBtLoad();
  } catch (e) {
    console.warn('[steam] init fail:', e);
    const msg = document.getElementById('st-live-msg');
    if (msg) msg.textContent = '초기화 오류: ' + e.message;
  }
}

/* ── Column list ── */
async function stLoadColumns() {
  try {
    const d = await api('GET', '/api/steam/models');
    const sel1 = document.getElementById('st-col');
    const sel2 = document.getElementById('st-bt-col');
    const cols = d.configured || d.columns || [];
    const defaultCol = d.defaultColumn || 'C-6111';
    [sel1, sel2].forEach(sel => {
      sel.innerHTML = cols.map(c => `<option value="${c}" ${c===defaultCol?'selected':''}>${c}</option>`).join('');
    });
  } catch (e) {
    const msg = document.getElementById('st-live-msg');
    if (msg) msg.textContent = '⚠ 컬럼 목록 로드 실패: ' + e.message;
    console.warn('[steam] stLoadColumns fail:', e);
  }
}

/* ── 온도 프로파일 이격 모니터 ── */
let stTempTimer = null;
const ST_TEMP_COLS = [['C-6111','6-1차'],['C-6211','6-2차'],['C-8111','8차'],['C-9111','9-1차'],['C-9211','9-2차'],['C-10111','10-1차'],['C-10211','10-2차']];
const ST_STAGE_LABEL = { reb_temp:'reb-A(보텀)', T_B:'T_B', T_C:'T_C(민감단)', T_D:'T_D(탑)' };
const stFmt = v => (v === null || v === undefined || Number.isNaN(v)) ? '—' : (+v).toFixed(1);

function stTempInit() {
  const sel = document.getElementById('st-temp-col');
  if (sel && !sel.options.length)
    sel.innerHTML = ST_TEMP_COLS.map(([v, l]) => `<option value="${v}">${l}</option>`).join('');
  document.getElementById('st-temp-load').onclick = stTempLoad;
}

async function stTempLoad() {
  if (stTempTimer) { clearInterval(stTempTimer); stTempTimer = null; }
  await stTempTick();
  stTempTimer = setInterval(stTempTick, 5000);
}

async function stTempTick() {
  const col = document.getElementById('st-temp-col').value;
  const st = document.getElementById('st-temp-status');
  try {
    const d = await api('GET', `/api/steam/tempprofile/${col}`);
    stRenderTemp(d);
    st.textContent = '갱신: ' + new Date().toLocaleTimeString();
  } catch (e) {
    st.textContent = '오류: ' + e.message;
  }
}

function stRenderTemp(d) {
  // 제품/진공 배지
  const pb = document.getElementById('st-temp-product');
  pb.textContent = '제품 ' + (d.matchedProduct || '—');
  const vb = document.getElementById('st-temp-vac');
  vb.textContent = `진공 ${stFmt(d.vacuum.current)} (기준 ${stFmt(d.vacuum.refMedian)})` + (d.vacuum.deviated ? ' ⚠이격' : '');
  vb.style.background = d.vacuum.deviated ? '#3a1a1a' : '#1a1a3a';
  vb.style.color = d.vacuum.deviated ? '#f66' : '#66f';

  const cats = d.stages.map(s => ST_STAGE_LABEL[s.stage] || s.stage);
  const lo = d.stages.map(s => (s.refMedian != null && s.refStd != null) ? +(s.refMedian - 2 * s.refStd).toFixed(2) : null);
  const band = d.stages.map(s => s.refStd != null ? +(4 * s.refStd).toFixed(2) : null);
  const med = d.stages.map(s => s.refMedian);
  const cur = d.stages.map(s => ({ value: s.current, itemStyle: { color: s.deviated ? '#f66' : '#6cf' } }));

  const el = document.getElementById('st-chart-temp');
  const chart = echarts.getInstanceByDom(el) || echarts.init(el);
  chart.setOption({
    backgroundColor: 'transparent',
    title: { text: `${d.column} 단면 온도 프로파일`, subtext: `매칭제품 ${d.matchedProduct || '—'} · 진공 ${stFmt(d.vacuum.current)} (기준 ${stFmt(d.vacuum.refMedian)})${d.vacuum.deviated ? ' ⚠' : ''}`,
      textStyle: { color: '#ccc', fontSize: 13 }, subtextStyle: { color: d.vacuum.deviated ? '#f66' : '#888', fontSize: 11 } },
    tooltip: { trigger: 'axis' },
    legend: { data: ['기준밴드', '기준', '현재'], textStyle: { color: '#888' }, top: 6, right: 10 },
    grid: { top: 64, left: 48, right: 20, bottom: 28 },
    xAxis: { type: 'category', data: cats, axisLabel: { color: '#aaa' } },
    yAxis: { type: 'value', name: '℃', scale: true, axisLabel: { color: '#aaa' }, splitLine: { lineStyle: { color: '#1a2a3a' } } },
    series: [
      { name: '_lo', type: 'line', data: lo, lineStyle: { opacity: 0 }, stack: 'band', symbol: 'none', silent: true },
      { name: '기준밴드', type: 'line', data: band, stack: 'band', lineStyle: { opacity: 0 }, areaStyle: { color: 'rgba(80,140,200,0.18)' }, symbol: 'none', silent: true },
      { name: '기준', type: 'line', data: med, lineStyle: { type: 'dashed', color: '#688' }, symbol: 'none' },
      { name: '현재', type: 'line', data: cur, lineStyle: { color: '#6cf', width: 2 }, symbolSize: 9 },
    ],
  });
  chart.resize();

  // 상세 메타
  const meta = document.getElementById('st-temp-meta');
  meta.style.display = 'block';
  const lines = d.stages.map(s =>
    `${(ST_STAGE_LABEL[s.stage] || s.stage).padEnd(12)} ${stFmt(s.current)}℃  기준 ${stFmt(s.refMedian)}±${stFmt(s.refStd)}  z=${s.z != null ? s.z.toFixed(1) : '—'}${s.deviated ? '  ⚠이격' : ''}`);
  lines.push(`ΔT(A-D)      ${stFmt(d.spanAD)}℃  기준 ${stFmt(d.spanRef)}`);
  lines.push(`진공         ${stFmt(d.vacuum.current)}  기준 ${stFmt(d.vacuum.refMedian)}±${stFmt(d.vacuum.refStd)}  z=${d.vacuum.z != null ? d.vacuum.z.toFixed(1) : '—'}${d.vacuum.deviated ? '  ⚠이격' : ''}`);
  meta.textContent = lines.join('\n') + `\n\n기간 ${d.period} · 제품 ${d.nProducts}종`;
}

/* ── Live panel ── */
async function stLiveStart() {
  try {
    if (stTimer) { clearInterval(stTimer); stTimer = null; }
    stLiveData = [];
    stInitUplot();
    await stLiveTick();
    stTimer = setInterval(stLiveTick, 5000);
    document.getElementById('st-live-msg').textContent = '';
  } catch (e) {
    document.getElementById('st-live-msg').textContent = '오류: ' + e.message;
    console.warn('[steam] live start fail:', e);
  }
}

async function stLiveTick() {
  const col = document.getElementById('st-col').value;
  try {
    const d = await api('GET', `/api/steam/live?col=${col}`);
    if (d.status === 'missing_tags') {
      document.getElementById('st-live-msg').textContent = `⚠ 태그 없음: ${d.missing?.join(', ') || '—'} (${d.message || '게이트웨이 폴링 확인'})`;
      return;
    }
    stUpdateLive(d);
  } catch (e) {
    document.getElementById('st-live-msg').textContent = '⚠ 조회 실패: ' + e.message;
  }
}

function stUpdateLive(d) {
  document.getElementById('st-mode').textContent = d.mode || '—';
  document.getElementById('st-conf').textContent = d.confidence || '—';
  const oodEl = document.getElementById('st-ood');
  if (d.ood) { oodEl.style.display = 'inline-block'; } else { oodEl.style.display = 'none'; }

  document.getElementById('st-op-rec').textContent = d.recOp != null ? d.recOp.toFixed(1) : '—';
  document.getElementById('st-op-act').textContent = d.actualOp != null ? d.actualOp.toFixed(1) : '—';
  const opTag = document.getElementById('st-op-act-tag');
  if (opTag) opTag.textContent = (d.Tags?.steamOp || '') + (d.Descs?.steamOp ? ' · ' + d.Descs.steamOp : '');

  // 게이지
  const fill = document.getElementById('st-gauge-fill');
  if (d.recOp != null) fill.style.width = Math.min(100, Math.max(0, d.recOp)) + '%';

  // 입력값 + 태그명 + 설명
  document.getElementById('st-live-feed').textContent = d.feed != null ? d.feed.toFixed(1) : '—';
  document.getElementById('st-live-feed-tag').textContent = (d.Tags?.feed || '') + (d.Descs?.feed ? ' · ' + d.Descs.feed : '');
  document.getElementById('st-live-product').textContent = d.product != null ? d.product.toFixed(1) : '—';
  document.getElementById('st-live-product-tag').textContent = (d.Tags?.product || '') + (d.Descs?.product ? ' · ' + d.Descs.product : '');
  document.getElementById('st-live-tc').textContent = d.tC != null ? d.tC.toFixed(1) : '—';
  document.getElementById('st-live-tc-tag').textContent = (d.Tags?.tC || '') + (d.Descs?.tC ? ' · ' + d.Descs.tC : '');
  document.getElementById('st-live-sf').textContent = d.actualSteamFlow != null ? d.actualSteamFlow.toFixed(1) : '—';
  document.getElementById('st-live-sf-tag').textContent = (d.Tags?.steamFlow || '') + (d.Descs?.steamFlow ? ' · ' + d.Descs.steamFlow : '');

  document.getElementById('st-live-msg').textContent = d.message || '';

  // uPlot data
  const now = Date.now() / 1000;
  stLiveData.push({ ts: now, recOp: d.recOp, actOp: d.actualOp });
  if (stLiveData.length > ST_MAX_POINTS) stLiveData.splice(0, stLiveData.length - ST_MAX_POINTS);
  stUpdateUplot();
}

/* ── uPlot OP chart ── */
function stInitUplot() {
  const opts = {
    width: document.getElementById('st-op-chart').clientWidth || 600,
    height: 240,
    cursor: { show: true },
    legend: { show: true },
    series: [
      { label: 'time' },
      { label: '권장 OP', stroke: '#4af', width: 2, points: { show: false } },
      { label: '실제 OP', stroke: '#fa3', width: 1.5, points: { show: false }, spanGaps: true },
    ],
    axes: [
      { stroke: '#555', grid: { stroke: '#1a2a3a' } },
      { stroke: '#555', grid: { stroke: '#1a2a3a' }, range: [0, 100] },
    ],
  };
  const data = [[], [], []];
  if (stUplot) try { stUplot.destroy(); } catch(_) {}
  stUplot = new uPlot(opts, data, document.getElementById('st-op-chart'));
}

function stUpdateUplot() {
  if (!stUplot || stLiveData.length < 2) return;
  const times = stLiveData.map(d => d.ts);
  const recOps = stLiveData.map(d => d.recOp != null ? d.recOp : null);
  const actOps = stLiveData.map(d => d.actOp != null ? d.actOp : null);
  stUplot.setData([times, recOps, actOps]);
}

/* ── Backtest panel ── */
let stBtData = null;
const stRendered = new Set();

async function stBtInit() {
  if (stBtData) return;
  const sel = document.getElementById('st-bt-col');
  if (sel.options.length === 0) await stLoadColumns();
}

async function stBtLoad() {
  const col = document.getElementById('st-bt-col').value;
  const status = document.getElementById('st-bt-status');
  if (!col) return;
  status.textContent = '로딩 중…';
  try {
    const d = await api('GET', `/api/steam/backtest/${col}`);
    stBtData = d;
    stRendered.clear();
    status.textContent = `PROD ${(d.prodmap?.n_prod_rows || 0).toLocaleString()}행`;
    stRenderProdmap(d);
    stRendered.add('prodmap');
    // 첫 서브탭 활성화
    document.querySelector('.st-bt-tab.active')?.click();
  } catch (ex) {
    status.textContent = '로드 실패: ' + ex.message;
  }
}

/* ── 헬퍼: axisPointer 가시화 ── */
const _stTip = {
  trigger: 'axis',
  axisPointer: { type: 'cross', crossStyle: { color: '#888', width: 1, type: 'dashed' } },
};

/* ── Prodmap charts ── */
function stRenderProdmap(d) {
  const renderOne = (id, fn) => {
    try {
      const el = document.getElementById(id);
      if (!el) return;
      fn(echarts.init(el));
    } catch (e) { console.warn('[steam] chart fail:', id, e); }
  };

  renderOne('st-chart-valve', chart => {
    const vc = d.prodmap?.valve_char;
    if (!vc) return;
    chart.setOption({
      title: { text: 'Valve Characteristic', textStyle: { color: '#aaa', fontSize: 12 } },
      tooltip: _stTip,
      legend: { textStyle: { color: '#888' }, bottom: 0 },
      xAxis: { type: 'value', name: 'OP %', axisLabel: { color: '#888' } },
      yAxis: { type: 'value', name: 'steam flow', axisLabel: { color: '#888' } },
      grid: { containLabel: true, left: 50, right: 10, top: 30, bottom: 40 },
      series: [
        { name: '평균', type: 'line', data: vc.map(r => [r.op, r.flow_mean]), smooth: true,
          lineStyle: { color: '#4af', width: 2 }, symbol: 'circle', symbolSize: 4 },
        { name: 'OP 상승', type: 'line', data: vc.filter(r => r.flow_up != null).map(r => [r.op, r.flow_up]),
          lineStyle: { color: '#6a6', width: 1, type: 'dashed' }, symbol: 'none' },
        { name: 'OP 하강', type: 'line', data: vc.filter(r => r.flow_dn != null).map(r => [r.op, r.flow_dn]),
          lineStyle: { color: '#c66', width: 1, type: 'dashed' }, symbol: 'none' },
      ],
      backgroundColor: 'transparent',
    });
    chart.resize();
  });

  renderOne('st-chart-op-points', chart => {
    const op = d.prodmap?.operating_points;
    if (!op || !op.feed || !op.steam_flow) return;
    const data = op.feed.map((f, i) => [f, op.steam_flow[i]]).filter(p => p[0] != null && p[1] != null);
    if (!data.length) return;
    chart.setOption({
      title: { text: 'Operating Points', textStyle: { color: '#aaa', fontSize: 12 } },
      tooltip: { trigger: 'axis', axisPointer: { type: 'cross', lineStyle: { color: '#888', width: 1, type: 'dashed' } }, formatter: p => `Feed: ${p[0].value[0].toFixed(0)}<br/>Steam: ${p[0].value[1].toFixed(0)}` },
      xAxis: { type: 'value', name: 'feed', axisLabel: { color: '#888' } },
      yAxis: { type: 'value', name: 'steam flow', axisLabel: { color: '#888' } },
      grid: { containLabel: true, left: 55, right: 10, top: 30, bottom: 20 },
      series: [{ type: 'scatter', data, symbolSize: 6, itemStyle: { color: '#4af' } }],
      backgroundColor: 'transparent',
    });
    chart.resize();
  });

  renderOne('st-chart-pred-vs-act', chart => {
    const pv = d.prodmap?.pred_vs_actual;
    if (!pv || !pv.actual || !pv.predicted) return;
    const data = pv.actual.map((a, i) => [a, pv.predicted[i]]).filter(p => p[0] != null && p[1] != null);
    if (!data.length) return;
    const allVals = data.flat();
    const maxVal = Math.max(...allVals);
    const minVal = Math.min(...allVals);
    chart.setOption({
      title: { text: `Pred vs Actual (R²=${pv.r2})`, textStyle: { color: '#aaa', fontSize: 12 } },
      tooltip: { trigger: 'axis', axisPointer: { type: 'cross', lineStyle: { color: '#888', width: 1, type: 'dashed' } }, formatter: p => `Actual: ${p[0].value[0].toFixed(0)}<br/>Pred: ${p[0].value[1].toFixed(0)}` },
      xAxis: { type: 'value', name: 'actual', axisLabel: { color: '#888' } },
      yAxis: { type: 'value', name: 'predicted', axisLabel: { color: '#888' } },
      grid: { containLabel: true, left: 55, right: 10, top: 30, bottom: 20 },
      series: [
        { type: 'scatter', data, symbolSize: 5, itemStyle: { color: '#4af' } },
        { type: 'line', data: [[minVal, minVal], [maxVal, maxVal]], lineStyle: { color: '#f44', width: 1, type: 'dashed' }, symbol: 'none' },
      ],
      backgroundColor: 'transparent',
    });
    chart.resize();
  });

  renderOne('st-chart-feat-imp', chart => {
    const fi = d.prodmap?.feature_importance;
    if (!fi || !fi.feature || !fi.gbm_importance) return;
    const feats = fi.feature.slice().reverse();
    const vals = fi.gbm_importance.slice().reverse();
    chart.setOption({
      title: { text: 'Feature Importance (GBM)', textStyle: { color: '#aaa', fontSize: 12 } },
      tooltip: _stTip,
      xAxis: { type: 'value', axisLabel: { color: '#888' } },
      yAxis: { type: 'category', data: feats, axisLabel: { color: '#aaa' } },
      grid: { containLabel: true, left: 80, right: 10, top: 30, bottom: 10 },
      series: [{ type: 'bar', data: vals, itemStyle: { color: '#6af' } }],
      backgroundColor: 'transparent',
    });
    chart.resize();
  });
}

/* ── Startup chart ── */
function stRenderStartup(d) {
  try {
    const st = d.startup;
    const ep = st?.episodes;
    const el = document.getElementById('st-chart-startup');
    if (!ep || ep.length === 0) {
      if (el) el.innerHTML = '<div style="padding:30px;text-align:center;color:#555">Startup 데이터 없음</div>';
      return;
    }
    const chart = echarts.init(el);
    const colors = ['#4af', '#fa3', '#6a6', '#c66', '#a6d', '#6cc', '#cc6', '#f6a', '#6fa', '#a6f'];
    const series = [];
    ep.forEach((e, k) => {
      const c = colors[k % colors.length];
      if (!e.rel_min || !e.reb_temp || !e.steam_flow) return;
      series.push(
        { name: `reb_temp ep${k+1}`, type: 'line', data: e.rel_min.map((t, i) => [t, e.reb_temp[i]]).filter(p => p[0] != null && p[1] != null),
          lineStyle: { color: c, width: 1.5 }, symbol: 'none' },
        { name: `steam ep${k+1}`, type: 'line', data: e.rel_min.map((t, i) => [t, e.steam_flow[i]]).filter(p => p[0] != null && p[1] != null),
          lineStyle: { color: c, width: 1, type: 'dashed' }, symbol: 'none' },
      );
    });
    if (!series.length) return;
    chart.setOption({
      title: { text: `Startup Aligned (${ep.length} episodes)`, textStyle: { color: '#aaa', fontSize: 12 } },
      tooltip: _stTip,
      legend: { type: 'scroll', textStyle: { color: '#888', fontSize: 10 }, top: 24 },
      xAxis: { type: 'value', name: 'minutes from cut-in', axisLabel: { color: '#888' } },
      yAxis: { type: 'value', axisLabel: { color: '#888' } },
      grid: { containLabel: true, left: 50, right: 10, top: 50, bottom: 20 },
      series,
      backgroundColor: 'transparent',
    });
    chart.resize();
  } catch (e) { console.warn('[steam] startup chart fail:', e); }
}

/* ── Shadow / Advisory chart ── */
function stRenderShadow(d, key) {
  try {
    const section = d[key];
    if (!section) return;
    const ts = section.time_series;
    const hist = section.error_histogram;
    const chartId = key === 'advisory' ? 'st-chart-advisory' : 'st-chart-shadow';
    const histId = key === 'advisory' ? 'st-chart-advisory-hist' : 'st-chart-shadow-hist';

    const el = document.getElementById(chartId);
    if (!el) return;
    if (!ts || !ts.time || !ts.actual_op || !ts.predicted_op || ts.n < 2) {
      el.innerHTML = '<div style="padding:30px;text-align:center;color:#555">데이터 없음</div>';
      return;
    }

    const time = ts.time.map(t => new Date(t).getTime());
    const chart = echarts.init(el);

    // NaN/null 필터링
    const actData = [];
    const predData = [];
    const oodFlags = ts.ood || [];
    for (let i = 0; i < time.length; i++) {
      const t = time[i];
      const a = ts.actual_op[i];
      const p = ts.predicted_op[i];
      if (a != null && isFinite(a)) actData.push([t, a]);
      if (p != null && isFinite(p)) predData.push([t, p]);
    }

    // OOD 영역
    const oodAreas = [];
    let oodStart = null;
    for (let i = 0; i < oodFlags.length; i++) {
      if (oodFlags[i] && oodStart === null) oodStart = time[i];
      if (!oodFlags[i] && oodStart !== null) {
        oodAreas.push([{ xAxis: oodStart }, { xAxis: time[i] }]);
        oodStart = null;
      }
    }
    if (oodStart !== null) oodAreas.push([{ xAxis: oodStart }, { xAxis: time[time.length - 1] }]);

    chart.setOption({
      title: { text: `${key === 'advisory' ? 'Advisory' : 'Shadow'} OP: actual vs predicted`,
        textStyle: { color: '#aaa', fontSize: 12 } },
      tooltip: _stTip,
      legend: { textStyle: { color: '#888' }, top: 24 },
      xAxis: { type: 'time', axisLabel: { color: '#888' } },
      yAxis: { type: 'value', name: 'OP %', axisLabel: { color: '#888' }, min: 0, max: 100 },
      grid: { containLabel: true, left: 45, right: 10, top: 50, bottom: 20 },
      series: [
        { name: 'actual OP', type: 'line', data: actData,
          lineStyle: { color: '#fa3', width: 1 }, symbol: 'none' },
        { name: 'predicted OP', type: 'line', data: predData,
          lineStyle: { color: '#4af', width: 1.5 }, symbol: 'none' },
        ...oodAreas.map(area => ({
          type: 'line', markArea: { silent: true, data: [area] },
          lineStyle: { color: 'transparent' },
        })),
      ],
      backgroundColor: 'transparent',
    });
    chart.resize();

    // 히스토그램
    if (hist && hist.bin_edges && hist.counts) {
      const hel = document.getElementById(histId);
      if (hel) {
        const hc = echarts.init(hel);
        const bins = hist.bin_edges;
        const cents = bins.slice(0, -1).map((v, i) => (v + bins[i + 1]) / 2);
        hc.setOption({
          title: { text: 'Error distribution', textStyle: { color: '#aaa', fontSize: 11 } },
          tooltip: _stTip,
          xAxis: { type: 'value', name: 'OP error', axisLabel: { color: '#888' } },
          yAxis: { type: 'value', axisLabel: { color: '#888' } },
          grid: { containLabel: true, left: 45, right: 10, top: 30, bottom: 20 },
          series: [{
            type: 'bar', data: cents.map((c, i) => [c, hist.counts[i]]),
            barWidth: '90%', itemStyle: { color: '#4af' },
          }],
          backgroundColor: 'transparent',
        });
        hc.resize();
      }
    }
  } catch (e) { console.warn('[steam]', key, 'chart fail:', e); }
}
