차트 유형 전환 — 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() },
);
}