/* ============================================================
   ui.jsx — 图标 / 基础控件 / 图表
   ============================================================ */
const { useEffect: useEffectUI, useState: useStateUI, Fragment: FragUI } = React;

/* ------------------------------ 图标 ------------------------------ */
const ICONS = {
  grid: ['M4 4h6.5v6.5H4z', 'M13.5 4H20v6.5h-6.5z', 'M4 13.5h6.5V20H4z', 'M13.5 13.5H20V20h-6.5z'],
  plug: ['M9.5 14.5l5-5', 'M12.5 7.5l1.6-1.6a4 4 0 015.9 5.9L18.4 13.4', 'M11.5 16.5l-1.6 1.6a4 4 0 01-5.9-5.9L5.6 10.6', 'M4.5 19.5l1.2-1.2', 'M19.5 4.5l-1.2 1.2'],
  box: ['M20.5 7.8L12 3 3.5 7.8 12 12.6l8.5-4.8z', 'M3.5 7.8v8.4L12 21l8.5-4.8V7.8', 'M12 12.6V21'],
  sigma: ['M17.5 4.5H6.5l6 7.5-6 7.5h11'],
  send: ['M21.5 2.5L11 13', 'M21.5 2.5l-6.8 19-3.7-8.5-8.5-3.7 19-6.8z'],
  form: ['M14 2.5H6.5a2 2 0 00-2 2v15a2 2 0 002 2h11a2 2 0 002-2V8', 'M14 2.5V8h5.5', 'M8.5 13h7', 'M8.5 17h4.5'],
  check: ['M20 6.5L9.5 17 4.5 12'],
  calc: ['M5.5 3h13v18h-13z', 'M8.5 7h7', 'M8.5 11.5h2', 'M13.5 11.5h2', 'M8.5 16h2', 'M13.5 16h2'],
  clock: ['M12 21.5a9.5 9.5 0 100-19 9.5 9.5 0 000 19z', 'M12 6.8V12l3.4 2'],
  right: ['M9 18l6-6-6-6'],
  left: ['M15 18l-6-6 6-6'],
  down: ['M6 9.5l6 6 6-6'],
  up: ['M6 14.5l6-6 6 6'],
  x: ['M18 6L6 18', 'M6 6l12 12'],
  search: ['M10.5 18a7.5 7.5 0 100-15 7.5 7.5 0 000 15z', 'M16 16l5 5'],
  refresh: ['M20.5 12a8.5 8.5 0 11-2.6-6.1', 'M20.5 3.5v5h-5'],
  undo: ['M3.5 8.5v5.5h5.5', 'M3.8 14a9 9 0 1115.4 2.6'],
  alert: ['M12 3.2L21 19H3l9-15.8z', 'M12 9.5v4.2', 'M12 16.8h.01'],
  info: ['M12 21.5a9.5 9.5 0 100-19 9.5 9.5 0 000 19z', 'M12 11v5.5', 'M12 7.6h.01'],
  checkc: ['M12 21.5a9.5 9.5 0 100-19 9.5 9.5 0 000 19z', 'M8.2 12.2l2.6 2.6 5-5.2'],
  xc: ['M12 21.5a9.5 9.5 0 100-19 9.5 9.5 0 000 19z', 'M14.8 9.2l-5.6 5.6', 'M9.2 9.2l5.6 5.6'],
  plus: ['M12 5v14', 'M5 12h14'],
  filter: ['M3.5 5h17l-6.5 7.5V20l-4-2.5v-5L3.5 5z'],
  download: ['M12 3.5v11', 'M7.5 10l4.5 4.5 4.5-4.5', 'M4.5 19.5h15'],
  eye: ['M2.5 12S6 6 12 6s9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6z', 'M12 15a3 3 0 100-6 3 3 0 000 6z'],
  user: ['M12 12a4 4 0 100-8 4 4 0 000 8z', 'M4.5 20.5a7.5 7.5 0 0115 0'],
  arrowUp: ['M12 19V5', 'M6 11l6-6 6 6'],
  arrowDown: ['M12 5v14', 'M6 13l6 6 6-6'],
  arrowRight: ['M5 12h14', 'M13 6l6 6-6 6'],
  layers: ['M12 2.5l9 5-9 5-9-5 9-5z', 'M3 12l9 5 9-5', 'M3 16.8l9 5 9-5'],
  link: ['M10 13.5a4 4 0 005.7 0l2.6-2.6a4 4 0 10-5.7-5.7l-1.3 1.3', 'M14 10.5a4 4 0 00-5.7 0L5.7 13a4 4 0 105.7 5.7l1.3-1.3'],
  calendar: ['M4.5 5.5h15v15h-15z', 'M4.5 10h15', 'M8.5 3v4.5', 'M15.5 3v4.5'],
  lock: ['M6 10.5h12v10H6z', 'M8.5 10.5V7.8a3.5 3.5 0 017 0v2.7'],
  drag: ['M9 6h.01', 'M15 6h.01', 'M9 12h.01', 'M15 12h.01', 'M9 18h.01', 'M15 18h.01'],
  bell: ['M18 8.5a6 6 0 10-12 0c0 6-2.5 7.5-2.5 7.5h17S18 14.5 18 8.5z', 'M13.7 20a2 2 0 01-3.4 0'],
  sliders: ['M4 8h10', 'M18 8h2', 'M4 16h4', 'M12 16h8', 'M16 5.5v5', 'M10 13.5v5'],
  book: ['M4 4.5A2 2 0 016 2.5h13v19H6a2 2 0 01-2-2z', 'M19 16.5H6a2 2 0 00-2 2'],
  building: ['M4 21V5.5l8-3 8 3V21', 'M9 21v-5h6v5', 'M8.5 9h1.5', 'M14 9h1.5', 'M8.5 12.5h1.5', 'M14 12.5h1.5'],
  swap: ['M4 8h13', 'M14 5l3 3-3 3', 'M20 16H7', 'M10 13l-3 3 3 3'],
  minus: ['M5 12h14'],
  equal: ['M5 9h14', 'M5 15h14'],
  file: ['M13.5 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8.5', 'M13.5 3L19 8.5h-5.5z'],
  settings: ['M12 15.2a3.2 3.2 0 100-6.4 3.2 3.2 0 000 6.4z', 'M19.4 15a1.7 1.7 0 00.3 1.9l.1.1a2 2 0 11-2.9 2.9l-.1-.1a1.7 1.7 0 00-2.9 1.2v.2a2 2 0 11-4 0v-.1A1.7 1.7 0 007 19.4l-.1.1a2 2 0 11-2.9-2.9l.1-.1A1.7 1.7 0 003 13.6H2.8a2 2 0 110-4h.1A1.7 1.7 0 004.6 7L4.5 6.9a2 2 0 112.9-2.9l.1.1A1.7 1.7 0 0010.4 3V2.8a2 2 0 114 0v.1A1.7 1.7 0 0017 4.6l.1-.1a2 2 0 112.9 2.9l-.1.1a1.7 1.7 0 001.2 2.9h.2a2 2 0 110 4h-.1a1.7 1.7 0 00-1.8 1.2z'],
  history: ['M3.5 8.5v5.5h5.5', 'M3.8 14a9 9 0 1115.4 2.6', 'M12 8v4.5l3 2'],
  target: ['M12 21a9 9 0 100-18 9 9 0 000 18z', 'M12 17a5 5 0 100-10 5 5 0 000 10z', 'M12 13.4a1.4 1.4 0 100-2.8 1.4 1.4 0 000 2.8z'],
  chart: ['M4.5 20V10', 'M10 20V4.5', 'M15.5 20v-7', 'M21 20V7.5'],
  chip: ['M7.5 7.5h9v9h-9z', 'M5 10h-2', 'M5 14h-2', 'M21 10h-2', 'M21 14h-2', 'M10 5V3', 'M14 5V3', 'M10 21v-2', 'M14 21v-2'],
};

function Ico({ name, size, style, className }) {
  const paths = ICONS[name] || ICONS.info;
  const s = size || 16;
  return (
    <svg className={className} style={Object.assign({ flex: '0 0 auto', display: 'block' }, style)} width={s} height={s} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      {paths.map((d, i) => <path key={i} d={d} />)}
    </svg>
  );
}

/* ------------------------------ 基础控件 ------------------------------ */
function Btn({ children, kind, size, icon, iconRight, onClick, disabled, style, title, wide }) {
  const cls = ['btn', kind || '', size || '', wide ? 'wide' : ''].join(' ').trim();
  return (
    <button className={cls} onClick={onClick} disabled={disabled} style={style} title={title}>
      {icon && <Ico name={icon} size={size === 'sm' ? 13 : 15} />}
      {children}
      {iconRight && <Ico name={iconRight} size={size === 'sm' ? 13 : 15} />}
    </button>
  );
}

function Tag({ children, kind, dot }) {
  return <span className={'tag ' + (kind || '')}>{dot && <i className="dot" />}{children}</span>;
}

function Chip({ children, on, onClick, icon }) {
  return <button className={'chip' + (on ? ' on' : '')} onClick={onClick}>{icon && <Ico name={icon} size={13} />}{children}</button>;
}

function Card({ children, style, className }) {
  return <div className={'card ' + (className || '')} style={style}>{children}</div>;
}

function CardHead({ title, sub, right, plain }) {
  return (
    <div className={'card-head' + (plain ? ' plain' : '')}>
      <h3>{title}</h3>
      {sub && <span className="sub">{sub}</span>}
      {right && <div className="right">{right}</div>}
    </div>
  );
}

function Switch({ on, onChange, label }) {
  return (
    <div className="row" style={{ gap: 8, cursor: 'pointer' }} onClick={() => onChange(!on)}>
      <div className={'switch' + (on ? ' on' : '')} />
      {label && <span style={{ fontSize: 12.5 }}>{label}</span>}
    </div>
  );
}

function Avatar({ name, tint, size }) {
  const s = size || 22;
  return <span className="avatar-sm" style={{ background: tint || '#7e8695', width: s, height: s, fontSize: s * 0.46 }}>{name}</span>;
}

/* ------------------------------ 抽屉 / 弹窗 ------------------------------ */
function Drawer({ title, meta, onClose, children, footer, wide }) {
  useEffectUI(() => {
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [onClose]);
  return (
    <FragUI>
      <div className="scrim" onClick={onClose} />
      <div className={'drawer' + (wide ? ' wide' : '')} role="dialog" aria-label={title}>
        <div className="drawer-head">
          <div style={{ minWidth: 0 }}>
            <h2>{title}</h2>
            {meta && <div className="meta">{meta}</div>}
          </div>
          <div className="spacer" />
          <button className="icon-btn" onClick={onClose} title="关闭"><Ico name="x" size={16} /></button>
        </div>
        <div className="drawer-body">{children}</div>
        {footer && <div className="drawer-foot">{footer}</div>}
      </div>
    </FragUI>
  );
}

function Modal({ title, onClose, children, footer, wide }) {
  useEffectUI(() => {
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [onClose]);
  return (
    <FragUI>
      <div className="scrim" onClick={onClose} />
      <div className={'modal' + (wide ? ' wide' : '')} role="dialog" aria-label={title}>
        <div className="modal-head"><h3>{title}</h3><div className="spacer" /><button className="icon-btn" onClick={onClose}><Ico name="x" size={16} /></button></div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-foot">{footer}</div>}
      </div>
    </FragUI>
  );
}

function Toasts({ items }) {
  if (!items.length) return null;
  return (
    <div className="toasts">
      {items.map((t) => (
        <div key={t.id} className={'toast ' + t.kind}>
          <span className="t-ico"><Ico name={t.kind === 'ok' ? 'checkc' : t.kind === 'err' ? 'xc' : 'info'} size={15} /></span>
          <span>{t.msg}</span>
        </div>
      ))}
    </div>
  );
}

function Empty({ icon, title, desc, action }) {
  return (
    <div className="empty">
      <div className="e-ico"><Ico name={icon || 'info'} size={18} /></div>
      <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--ink-2)' }}>{title}</div>
      {desc && <div className="tiny mt8" style={{ maxWidth: 320, margin: '6px auto 0' }}>{desc}</div>}
      {action && <div className="mt12">{action}</div>}
    </div>
  );
}

/* ------------------------------ 进度 ------------------------------ */
function Bar({ value, kind }) {
  return <div className="bar-thin"><i className={kind || ''} style={{ width: Math.max(0, Math.min(100, value)) + '%' }} /></div>;
}

function Ring({ value, size, stroke, color, label }) {
  const s = size || 64, sw = stroke || 6, r = (s - sw) / 2, c = 2 * Math.PI * r;
  const off = c * (1 - Math.max(0, Math.min(100, value)) / 100);
  const col = color || (value >= 100 ? 'var(--ok)' : value >= 50 ? 'var(--primary)' : 'var(--warn)');
  return (
    <div style={{ position: 'relative', width: s, height: s, flex: '0 0 auto' }}>
      <svg width={s} height={s} style={{ transform: 'rotate(-90deg)' }}>
        <circle cx={s / 2} cy={s / 2} r={r} stroke="var(--surface-3)" strokeWidth={sw} fill="none" />
        <circle cx={s / 2} cy={s / 2} r={r} stroke={col} strokeWidth={sw} fill="none"
          strokeDasharray={c} strokeDashoffset={off} strokeLinecap="round"
          style={{ transition: 'stroke-dashoffset .6s cubic-bezier(.2,.8,.3,1)' }} />
      </svg>
      <div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center' }}>
        <span className="num" style={{ fontSize: s * 0.26, fontWeight: 600, color: 'var(--ink)' }}>{label != null ? label : Math.round(value) + '%'}</span>
      </div>
    </div>
  );
}

/* ------------------------------ 图表 ------------------------------ */
function niceMax(v) {
  if (v <= 0) return 1;
  const p = Math.pow(10, Math.floor(Math.log10(v)));
  const n = v / p;
  const step = n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10;
  return step * p;
}

/* 纵向柱状图 */
function Columns({ data, height, unit, fmt, grid = true, labelEvery = 1 }) {
  const h = height || 200, padL = 46, padR = 10, padT = 18, padB = 30;
  const W = 520, innerW = W - padL - padR, innerH = h - padT - padB;
  const max = niceMax(Math.max.apply(null, data.map((d) => d.value).concat([1])));
  const bw = Math.min(52, (innerW / data.length) * 0.52);
  const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => f * max);
  return (
    <svg className="chart" viewBox={'0 0 ' + W + ' ' + h} style={{ height: h, width: '100%' }} preserveAspectRatio="none">
      {grid && ticks.map((t, i) => {
        const y = padT + innerH - (t / max) * innerH;
        return <line key={i} className="gl" x1={padL} x2={W - padR} y1={y} y2={y} strokeDasharray={i === 0 ? '' : '2 3'} />;
      })}
      {grid && ticks.map((t, i) => (
        <text key={'t' + i} x={padL - 8} y={padT + innerH - (t / max) * innerH + 3.5} textAnchor="end" style={{ fontSize: 10 }}>
          {fmt ? fmt(t) : (t >= 10000 ? (t / 10000) + '万' : Math.round(t))}
        </text>
      ))}
      {data.map((d, i) => {
        const cx = padL + (innerW / data.length) * (i + 0.5);
        const bh = Math.max(2, (d.value / max) * innerH);
        const y = padT + innerH - bh;
        return (
          <g key={i}>
            <rect x={cx - bw / 2} y={y} width={bw} height={bh} rx="2.5" fill={d.color || 'var(--primary)'} opacity={d.dim ? 0.38 : 1} />
            {d.value > 0 && <text className="val" x={cx} y={y - 5} textAnchor="middle">{fmt ? fmt(d.value) : d.value}</text>}
            {i % labelEvery === 0 && <text className="lbl" x={cx} y={h - 11} textAnchor="middle">{d.label}</text>}
          </g>
        );
      })}
      <line className="gl" x1={padL} x2={W - padR} y1={padT + innerH} y2={padT + innerH} />
    </svg>
  );
}

/* 横向排名条 */
function RankBars({ data, fmt, onRow }) {
  const max = Math.max.apply(null, data.map((d) => d.value).concat([1]));
  return (
    <div>
      {data.map((d, i) => (
        <div key={i} onClick={onRow ? () => onRow(d) : undefined}
          style={{ display: 'grid', gridTemplateColumns: '104px 1fr 92px', gap: 12, alignItems: 'center', padding: '9px 0', borderTop: i ? '1px dashed var(--line)' : 'none', cursor: onRow ? 'pointer' : 'default' }}>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 12.5, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{d.label}</div>
            <div className="tiny muted">{d.sub}</div>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 4, height: 18 }}>
            <div style={{ height: 12, borderRadius: 3, background: d.color || 'var(--primary)', width: Math.max(3, (d.value / max) * 100) + '%', opacity: d.dim ? 0.4 : 1, transition: 'width .5s cubic-bezier(.2,.8,.3,1)' }} />
          </div>
          <div className="num" style={{ textAlign: 'right', fontSize: 13, fontWeight: 500 }}>{fmt ? fmt(d.value) : d.value}</div>
        </div>
      ))}
    </div>
  );
}

/* 成本构成堆叠条 + 图例 */
function StackBar({ parts, height }) {
  const total = parts.reduce((s, p) => s + p.value, 0) || 1;
  return (
    <div>
      <div className="stackbar" style={{ height: height || 10 }}>
        {parts.map((p, i) => <i key={i} style={{ width: (p.value / total * 100) + '%', background: p.color }} title={p.label} />)}
      </div>
      <div className="legend mt12">
        {parts.map((p, i) => (
          <span className="li" key={i}>
            <i className="sw" style={{ background: p.color }} />
            {p.label}
            <span className="num" style={{ color: 'var(--ink-3)' }}>{(p.value / total * 100).toFixed(1)}%</span>
          </span>
        ))}
      </div>
    </div>
  );
}

/* 迷你折线（趋势） */
function Spark({ data, color, w, h }) {
  const W = w || 92, H = h || 28;
  const max = Math.max.apply(null, data), min = Math.min.apply(null, data);
  const span = (max - min) || 1;
  const pts = data.map((v, i) => [(i / (data.length - 1)) * (W - 4) + 2, H - 3 - ((v - min) / span) * (H - 8)]);
  const dPath = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');
  const col = color || 'var(--ink-4)';
  return (
    <svg width={W} height={H} style={{ display: 'block', overflow: 'visible' }}>
      <path d={dPath} fill="none" stroke={col} strokeWidth="1.6" strokeLinecap="round" />
      <circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r="2.4" fill={col} />
    </svg>
  );
}

/* 双序列柱（单位成本 vs 上期） */
function ColumnsCompare({ data, height, fmt }) {
  const h = height || 210, padL = 54, padR = 12, padT = 20, padB = 34;
  const W = 620, innerW = W - padL - padR, innerH = h - padT - padB;
  const max = niceMax(Math.max.apply(null, data.flatMap((d) => [d.a, d.b])) * 1.08);
  const slot = innerW / data.length, bw = Math.min(24, slot * 0.24);
  const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => f * max);
  return (
    <svg className="chart" viewBox={'0 0 ' + W + ' ' + h} style={{ height: h, width: '100%' }} preserveAspectRatio="none">
      {ticks.map((t, i) => (
        <line key={i} className="gl" x1={padL} x2={W - padR} y1={padT + innerH - (t / max) * innerH} y2={padT + innerH - (t / max) * innerH} strokeDasharray={i === 0 ? '' : '2 3'} />
      ))}
      {ticks.map((t, i) => (
        <text key={'t' + i} x={padL - 8} y={padT + innerH - (t / max) * innerH + 3.5} textAnchor="end" style={{ fontSize: 10 }}>
          {fmt ? fmt(t) : t >= 10000 ? (t / 10000).toFixed(1) + '万' : Math.round(t)}
        </text>
      ))}
      {data.map((d, i) => {
        const cx = padL + slot * (i + 0.5);
        const ha = Math.max(2, (d.a / max) * innerH), hb = Math.max(2, (d.b / max) * innerH);
        return (
          <g key={i}>
            <rect x={cx - bw - 2} y={padT + innerH - hb} width={bw} height={hb} rx="2" fill="var(--line-strong)" />
            <rect x={cx + 2} y={padT + innerH - ha} width={bw} height={ha} rx="2" fill={d.color || 'var(--primary)'} />
            <text className="lbl" x={cx} y={h - 16} textAnchor="middle">{d.label}</text>
            <text className="tiny" x={cx} y={h - 4} textAnchor="middle" style={{ fontSize: 9.5, fill: 'var(--ink-4)' }}>{d.sub}</text>
          </g>
        );
      })}
      <line className="gl" x1={padL} x2={W - padR} y1={padT + innerH} y2={padT + innerH} />
    </svg>
  );
}

/* 环形占比 */
function Donut({ parts, size, thickness, centerLabel, centerSub }) {
  const s = size || 132, th = thickness || 16, r = (s - th) / 2, c = 2 * Math.PI * r;
  const total = parts.reduce((a, p) => a + p.value, 0) || 1;
  let acc = 0;
  return (
    <div style={{ position: 'relative', width: s, height: s, flex: '0 0 auto' }}>
      <svg width={s} height={s} style={{ transform: 'rotate(-90deg)' }}>
        <circle cx={s / 2} cy={s / 2} r={r} fill="none" stroke="var(--surface-3)" strokeWidth={th} />
        {parts.map((p, i) => {
          const len = (p.value / total) * c;
          const el = <circle key={i} cx={s / 2} cy={s / 2} r={r} fill="none" stroke={p.color} strokeWidth={th}
            strokeDasharray={len + ' ' + (c - len)} strokeDashoffset={-acc} />;
          acc += len;
          return el;
        })}
      </svg>
      {centerLabel && (
        <div style={{ position: 'absolute', inset: 0, display: 'grid', placeContent: 'center', textAlign: 'center' }}>
          <div className="num" style={{ fontSize: 17, fontWeight: 600, letterSpacing: '-0.4px' }}>{centerLabel}</div>
          {centerSub && <div className="tiny muted" style={{ marginTop: 1 }}>{centerSub}</div>}
        </div>
      )}
    </div>
  );
}

/* 折线（多期趋势） */
function Lines({ series, height, labels, fmt }) {
  const h = height || 180, padL = 50, padR = 14, padT = 16, padB = 26;
  const W = 620, iw = W - padL - padR, ih = h - padT - padB;
  const all = series.flatMap((s) => s.data);
  const max = niceMax(Math.max.apply(null, all) * 1.08), min = 0;
  const ticks = [0, 0.5, 1].map((f) => f * max);
  const X = (i, n) => padL + (iw / (n - 1)) * i;
  const Y = (v) => padT + ih - ((v - min) / (max - min)) * ih;
  return (
    <svg className="chart" viewBox={'0 0 ' + W + ' ' + h} style={{ height: h, width: '100%' }} preserveAspectRatio="none">
      {ticks.map((t, i) => <line key={i} className="gl" x1={padL} x2={W - padR} y1={Y(t)} y2={Y(t)} strokeDasharray={i === 0 ? '' : '2 3'} />)}
      {ticks.map((t, i) => <text key={'x' + i} x={padL - 8} y={Y(t) + 3.5} textAnchor="end" style={{ fontSize: 10 }}>{fmt ? fmt(t) : Math.round(t)}</text>)}
      {series.map((s, si) => (
        <g key={si}>
          <path d={s.data.map((v, i) => (i ? 'L' : 'M') + X(i, s.data.length).toFixed(1) + ' ' + Y(v).toFixed(1)).join(' ')}
            fill="none" stroke={s.color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" strokeDasharray={s.dash ? '4 3' : ''} />
          {s.data.map((v, i) => <circle key={i} cx={X(i, s.data.length)} cy={Y(v)} r="2.6" fill="#fff" stroke={s.color} strokeWidth="1.8" />)}
        </g>
      ))}
      {labels && labels.map((l, i) => <text key={i} className="lbl" x={X(i, labels.length)} y={h - 8} textAnchor="middle" style={{ fontSize: 10.5 }}>{l}</text>)}
    </svg>
  );
}

Object.assign(window, { Ico, ICONS, Btn, Tag, Chip, Card, CardHead, Switch, Avatar, Drawer, Modal, Toasts, Empty, Bar, Ring, Columns, RankBars, StackBar, Spark, Donut, Lines, ColumnsCompare, niceMax });
