캔들스틱 차트

Candlestick chart

한 구간의 시가·고가·저가·종가 네 값을 막대(몸통)와 선(꼬리) 하나로 압축해 보여주는 금융 차트.

다른 이름: OHLC chart봉 차트
···
html
<div class="viz">
  <svg id="svg"></svg>
</div>
css
body{display:block}
.viz{position:relative;width:100%;height:100%}
svg{display:block;width:100%;height:100%;overflow:visible}
.axis{stroke:var(--line);stroke-width:1}
.tick{fill:var(--muted);font-weight:600}
.minor{display:none}
@media (min-width:460px){.minor{display:inline}}
.wick{stroke-width:1.2}
.up{fill:var(--surface);stroke:var(--accent);stroke-width:1.6}
.down{fill:var(--accent-2);stroke:var(--accent-2)}
js
var svg = document.getElementById('svg');
var NS = 'http://www.w3.org/2000/svg';
function se(t){ return document.createElementNS(NS,t); }
function sa(e,o){ for (var k in o) e.setAttribute(k,o[k]); }
function R(a,b){ return a + Math.random()*(b-a); }

var N = 12;
function genSeries(){
  var out = [], price = R(40,60);
  for (var i=0;i<N;i++){
    var open = price;
    var close = Math.max(6, Math.min(94, open + R(-9,9)));
    var high = Math.max(open,close) + R(0,4);
    var low = Math.min(open,close) - R(0,4);
    out.push({ o:open, h:Math.min(99,high), l:Math.max(1,low), c:close });
    price = close;
  }
  return out;
}
var cur = genSeries();
var W=300, H=200;

function measure(){
  var r = svg.getBoundingClientRect();
  W = Math.max(r.width,10); H = Math.max(r.height,10);
  sa(svg,{ viewBox: '0 0 ' + W + ' ' + H });
}

function draw(){
  svg.innerHTML = '';
  var padL=24, padR=10, padT=12, padB=14;
  var pw=W-padL-padR, ph=H-padT-padB;
  function Y(v){ return padT+ph-(v/100)*ph; }
  var slot = pw/N;
  var bw = Math.min(16, slot*0.55);

  [0,100].forEach(function(v){
    var g = se('line');
    sa(g,{x1:padL,y1:Y(v),x2:W-padR,y2:Y(v),class:'axis' + (v===100?' minor':'')});
    svg.appendChild(g);
  });

  cur.forEach(function(k,i){
    var cx = padL+slot*i+slot/2;
    var up = k.c >= k.o;
    var wick = se('line');
    sa(wick,{x1:cx,y1:Y(k.h),x2:cx,y2:Y(k.l),class:'wick',stroke:up?'var(--accent)':'var(--accent-2)'});
    svg.appendChild(wick);
    var top = Y(Math.max(k.o,k.c)), bot = Y(Math.min(k.o,k.c));
    var body = se('rect');
    sa(body,{x:cx-bw/2,y:top,width:bw,height:Math.max(bot-top,1.4),rx:1.4,class:up?'up':'down'});
    svg.appendChild(body);
  });
}

function animate(target){
  var from = cur;
  var t0 = performance.now();
  function step(t){
    var p = Math.min(1,(t-t0)/750);
    var e = 1-Math.pow(1-p,3);
    cur = from.map(function(k,i){
      var to = target[i];
      return { o:k.o+(to.o-k.o)*e, h:k.h+(to.h-k.h)*e, l:k.l+(to.l-k.l)*e, c:k.c+(to.c-k.c)*e };
    });
    draw();
    if (p<1) requestAnimationFrame(step); else cur = target;
  }
  requestAnimationFrame(step);
}

new ResizeObserver(function(){ measure(); draw(); }).observe(svg);
measure();
draw();
setInterval(function(){ animate(genSeries()); }, 3800);

몸통(굵은 부분)은 시가와 종가 사이, 꼬리(가는 선)는 그 구간의 최고~최저가입니다. 선 차트라면 종가 하나만 보이지만, 캔들스틱은 "그 안에서 얼마나 출렁였는지"까지 한 번에 보여줍니다.

색은 흔히 상승(초록/빨강 등)으로 관례가 굳어 있지만, 색만으로 상승·하락을 구분하면 색각 이상이 있는 사람에게는 정보가 사라집니다. 그래서 이 데모는 색과 함께 모양으로도 구분합니다 — 상승은 테두리만 있는 속이 빈 몸통, 하락은 꽉 찬 몸통을 씁니다.

봉이 너무 많아지면(몇 백 개) 화면 폭에 다 못 넣고 뭉개집니다 — 그럴 땐 기간을 넓혀 봉 하나가 더 긴 구간(일봉→주봉)을 대표하게 하거나, 확대/스크롤 인터랙션을 붙입니다.

언제 쓰나

가격처럼 구간마다 시가·고가·저가·종가가 모두 의미 있는 금융/시계열 데이터에 씁니다.