← back to Ken

kalshi-dash/src/components/MiniCandlestick.jsx

118 lines

/**
 * MiniCandlestick — tiny SVG candlestick chart
 *
 * Props:
 *   data      — array of { o, h, l, c } (open, high, low, close)
 *   width     — SVG width in px (default 80)
 *   height    — SVG height in px (default 32)
 *   bullColor — color for bullish candles (c > o)
 *   bearColor — color for bearish candles (c <= o)
 *
 * Usage:
 *   <MiniCandlestick data={[{o:45,h:52,l:42,c:50}, ...]} />
 *   <MiniCandlestick data={genCandles(seed, 9)} width={80} height={32} />
 */

/**
 * Generate fake OHLC data that trends in a given direction.
 * @param {number} seed  — deterministic seed
 * @param {number} count — number of candles
 * @param {'up'|'down'|'flat'} trend
 */
export function genCandles(seed, count = 9, trend = 'flat') {
  let s = seed;
  const rng = () => {
    s = (s * 1664525 + 1013904223) & 0xffffffff;
    return (Math.abs(s) % 1000) / 1000; // 0..1
  };

  const trendDelta = trend === 'up' ? 0.8 : trend === 'down' ? -0.8 : 0;
  let price = 45 + rng() * 10;
  const candles = [];

  for (let i = 0; i < count; i++) {
    const move = (rng() - 0.45 + trendDelta * 0.1) * 6;
    const open = price;
    const close = Math.max(5, Math.min(95, price + move));
    const highExtra = rng() * 3;
    const lowExtra = rng() * 3;
    const high = Math.max(open, close) + highExtra;
    const low = Math.min(open, close) - lowExtra;
    candles.push({ o: open, h: high, l: low, c: close });
    price = close;
  }
  return candles;
}

export default function MiniCandlestick({
  data,
  width = 80,
  height = 32,
  bullColor = 'var(--green)',
  bearColor = 'var(--red)',
}) {
  if (!data || data.length === 0) return null;

  const allLows  = data.map(d => d.l);
  const allHighs = data.map(d => d.h);
  const minVal   = Math.min(...allLows);
  const maxVal   = Math.max(...allHighs);
  const range    = maxVal - minVal || 1;

  const pad    = 2;
  const drawH  = height - pad * 2;
  const drawW  = width - pad * 2;

  // Normalize a value to SVG y coordinate (inverted — higher = lower y)
  const yOf = (v) => pad + drawH - ((v - minVal) / range) * drawH;

  const n       = data.length;
  const slotW   = drawW / n;
  const bodyW   = Math.max(2, slotW * 0.55);
  const wickW   = 1;

  return (
    <svg
      width={width}
      height={height}
      viewBox={`0 0 ${width} ${height}`}
      style={{ display: 'block', flexShrink: 0 }}
      aria-hidden="true"
    >
      {data.map((candle, i) => {
        const isBull  = candle.c >= candle.o;
        const color   = isBull ? bullColor : bearColor;
        const cx      = pad + i * slotW + slotW / 2;
        const bodyTop = yOf(Math.max(candle.o, candle.c));
        const bodyBot = yOf(Math.min(candle.o, candle.c));
        const bodyHt  = Math.max(1, bodyBot - bodyTop);

        return (
          <g key={i}>
            {/* Wick */}
            <line
              x1={cx}
              y1={yOf(candle.h)}
              x2={cx}
              y2={yOf(candle.l)}
              stroke={color}
              strokeWidth={wickW}
              strokeOpacity={0.7}
            />
            {/* Body */}
            <rect
              x={cx - bodyW / 2}
              y={bodyTop}
              width={bodyW}
              height={bodyHt}
              fill={color}
              fillOpacity={isBull ? 0.85 : 0.75}
              rx={0.5}
            />
          </g>
        );
      })}
    </svg>
  );
}