Skip to content

차트 유형 전환 — swapSeries

캔들 · 바 · 라인 · 에어리어를 버튼으로 오간다. 데이터는 그대로 OHLC고 갈리는 것은 그리는 법뿐이라, 얹힌 MA(20)도 뷰포트도 유형을 갈아탄 뒤 그대로 산다. 라인으로 갔다 캔들로 돌아와도 고가·저가가 온전하다 — swapSeries가 데시메이션·접근자까지 새 시리즈 것으로 갈아 끼우기 때문이다.

요점

  • swapSeries는 표현만 간다. 데이터·뷰포트·얹힌 지표는 그대로다 — 재등록도, 상태 복사도 없다. 유형 토글이 상태를 잃지 않는 이유다.
  • 접근자가 유형을 따라간다. 라인·에어리어에 OHLC를 먹이려면 OHLCAccessor를 줘야 종가를 읽는다. 캔들·바는 자기 접근자가 기본이다.
  • 데시메이션도 따라간다. 라인(M4)으로 갔다 캔들(OHLC 집계)로 돌아와도 고가·저가가 지워지지 않는다 — 스왑이 그릴 점 사슬까지 새 시리즈 것으로 갈아 끼운다.

소스

apps/examples/src/cases/chart-types.ts — CI가 타입체크하는 실물이다.

ts
import { PlotBuilder, browserDeps } from "@finchart/dom";
import type { OHLC, Series, SeriesHandle } from "@finchart/core";
import {
  AreaSeries,
  barSeries,
  candleSeries,
  crosshair,
  LineSeries,
  OHLCAccessor,
  priceFormat,
  timeTicks,
} from "@finchart/core";
import { attachMovingAverage } from "@finchart/indicators";
import { fixtureCandles } from "./fixture";
import { chartHost } from "./stage";

export const title = "차트 유형 전환 — swapSeries는 표현만 간다";
export const description =
  "캔들 · 바 · 라인 · 에어리어를 버튼으로 오간다. 데이터는 그대로 OHLC고 갈리는 것은 그리는 법뿐이라, 얹힌 MA(20)도 뷰포트도 유형을 갈아탄 뒤 그대로 산다. " +
  "라인으로 갔다 캔들로 돌아와도 고가·저가가 온전하다 — swapSeries가 데시메이션·접근자까지 새 시리즈 것으로 갈아 끼우기 때문이다.";

/** 눌린 버튼의 배경 — 셸 CSS 없이도 케이스 혼자 보인다(senior-review-2026-08-13 D3). */
function paintPressed(el: HTMLButtonElement, pressed: boolean): void {
  el.style.background = pressed ? "#3b82f6" : "";
  el.style.color = pressed ? "#fff" : "";
}

type ChartType = "candle" | "bar" | "line" | "area";
const CHART_TYPES: readonly ChartType[] = ["candle", "bar", "line", "area"];
const LABELS: Record<ChartType, string> = {
  candle: "캔들",
  bar: "바",
  line: "라인",
  area: "에어리어",
};

export function mount(container: HTMLElement): () => void {
  const toolbar = document.createElement("div");
  toolbar.setAttribute("role", "group");
  toolbar.setAttribute("aria-label", "차트 유형");
  toolbar.style.cssText = "display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap";
  container.append(toolbar);
  const host = chartHost(container, 480);

  const plot = PlotBuilder.create<OHLC>(browserDeps({ autoSize: true }))
    .setSize(container.clientWidth || 900, 480)
    .setAxis({
      x: { ticks: timeTicks({ timeZone: "UTC", locale: "ko" }) },
      y: { position: "right", format: priceFormat({ compact: true, locale: "ko" }) },
    })
    .build(host);

  /**
   * 유형 목록 — 넷 다 **같은 OHLC 데이터**를 그린다. 라인·에어리어는
   * 기본 접근자(`value`를 읽는다)가 아니라 `OHLCAccessor`를 받아야
   * 종가를 읽는다. 캔들·바는 자기 접근자가 기본이라 팩토리면 된다.
   */
  const chartTypes: Record<ChartType, Series<OHLC>> = {
    candle: candleSeries(),
    bar: barSeries(),
    line: new LineSeries<OHLC>({ coordinates: new OHLCAccessor() }),
    area: new AreaSeries<OHLC>({ coordinates: new OHLCAccessor() }),
  };

  const price: SeriesHandle<OHLC> = plot.mainPane.addSeries({
    series: chartTypes.candle,
    data: fixtureCandles(),
    name: "가격",
  });

  // 표현만 가는 것의 증인 — 유형을 갈아도 이 MA는 재등록 없이 그대로다.
  plot.mainPane.use(attachMovingAverage({ source: price, period: 20, color: "#f59e0b" }));

  plot.use(crosshair({ magnet: true }));

  const buttons = new Map<ChartType, HTMLButtonElement>();
  const select = (next: ChartType): void => {
    // swapSeries는 데이터·뷰포트·파생을 두고 그리는 법만 바꾼다.
    // 데시메이션·접근자도 새 시리즈 것으로 따라간다 — 라인(M4)에서
    // 캔들(OHLC 집계)로 돌아올 때 고가·저가가 지워지지 않는 이유다.
    price.swapSeries(chartTypes[next]);
    for (const [type, el] of buttons) {
      el.setAttribute("aria-pressed", String(type === next));
      paintPressed(el, type === next);
    }
  };

  for (const type of CHART_TYPES) {
    const el = document.createElement("button");
    el.textContent = LABELS[type];
    el.setAttribute("aria-pressed", String(type === "candle"));
    paintPressed(el, type === "candle");
    el.addEventListener("click", () => select(type));
    buttons.set(type, el);
    toolbar.append(el);
  }

  return Object.assign(
    () => {
      plot.destroy();
      toolbar.remove();
      host.remove();
    },
    { requestRender: () => plot.requestRender() },
  );
}