---
code: "CMP-11"
id: "c-waterfall"
tab: "compare"
kind: "core"
title: "Waterfall · 증감 브리지"
library: "SVG"
deps: ["svg"]
load: "low"
pasteKind: "custom-mount"
family: "bridge"
dataShape: "ordered plus-minus contributions from start to end"
tasks: ["compare alternatives", "show delta", "benchmark groups", "explain a total"]
tags: ["comparison", "benchmark", "delta", "waterfall", "bridge", "증감", "브리지"]
gallery: "https://graph.heal7.com/#CMP-11"
js: "/sku/CMP-11.js"
---

# [CMP-11] Waterfall · 증감 브리지

> AI 붙여넣기 카드. 갤러리 런타임(`main.js`)이 아니라 이 파일을 가져와라.

## 언제 가져가나
시작값에서 +/− 항목을 순서대로 쌓아 최종값으로 연결 — 덤벨·토네이도와 다른 순차 분해.

**응용** 손익 브리지, 점수 가감, 전환 퍼널 증감, 예산 변동 설명.

## 데이터 모양
- family: `bridge`
- dataShape: ordered plus-minus contributions from start to end
- tasks: compare alternatives, show delta, benchmark groups, explain a total
- tags: comparison, benchmark, delta, waterfall, bridge, 증감, 브리지


## 의존
svg · load `low` · library `SVG`

## 붙여넣는 법
1. 라이브러리 없이 host 엘리먼트에 직접 그린다
2. 함수 전체를 가져간다
3. `getElementById('원본id')` 만 대상 노드로 바꾼다

```js
const T = {
  "bg": "transparent",
  "fg": "#ece8f5",
  "sub": "#a59bbd",
  "grid": "rgba(255,255,255,.08)",
  "gold": "#e8c069",
  "royal": "#6b8cff",
  "rose": "#e07a9f",
  "teal": "#6dd2c4",
  "plum": "#b487e8"
};
const isDark = () => document.documentElement.getAttribute('data-theme') !== 'light';
const host = document.querySelector('#chart');
const factory = function(){
  const steps = [
    {n:'기초 점수', v:72, total:true},
    {n:'경력 가산', v:8},
    {n:'자격 가산', v:5},
    {n:'이직 감점', v:-6},
    {n:'시장 보정', v:-3},
    {n:'최종 점수', v:76, total:true}
  ];
  const host = document.getElementById('c-waterfall'); if(!host) return;
  const yOf = v => 200 - v*1.7;
  const spans=[];
  let acc=0;
  steps.forEach((s,i)=>{
    if(s.total){ spans.push({i, y0:0, y1:s.v, c:T.gold, s}); acc=s.v; }
    else if(s.v>=0){ spans.push({i, y0:acc, y1:acc+s.v, c:T.teal, s}); acc+=s.v; }
    else { spans.push({i, y0:acc, y1:acc+s.v, c:T.rose, s}); acc+=s.v; }
  });
  const bars = spans.map((d,i) => {
    const x = 36 + i*60;
    const y = Math.min(yOf(d.y0), yOf(d.y1));
    const h = Math.max(3, Math.abs(yOf(d.y1)-yOf(d.y0)));
    const label = d.s.total ? String(d.s.v) : ((d.s.v>0?'+':'')+d.s.v);
    const prev = i>0 && !d.s.total ? `<line x1="${36+(i-1)*60+36}" x2="${x}" y1="${yOf(d.y0)}" y2="${yOf(d.y0)}" stroke="${T.sub}" stroke-dasharray="3 3"/>` : '';
    return `${prev}<rect x="${x}" y="${y}" width="36" height="${h}" rx="4" fill="${d.c}"/>
      <text x="${x+18}" y="${y-6}" text-anchor="middle" fill="${T.fg}" font-size="11" font-weight="700">${label}</text>
      <text x="${x+18}" y="228" text-anchor="middle" fill="${T.sub}" font-size="10">${d.s.n}</text>`;
  }).join('');
  host.innerHTML = `<svg viewBox="0 0 400 240" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">${bars}</svg>`;
};
factory();
```

## 원본 factory (갤러리 정본 발췌)
- file: `frontend/js/graph-gallery/main.js`
- lines: 3262-3291
- gallery: https://graph.heal7.com/#CMP-11
- raw js: https://graph.heal7.com/sku/CMP-11.js

```js
function(){
  const steps = [
    {n:'기초 점수', v:72, total:true},
    {n:'경력 가산', v:8},
    {n:'자격 가산', v:5},
    {n:'이직 감점', v:-6},
    {n:'시장 보정', v:-3},
    {n:'최종 점수', v:76, total:true}
  ];
  const host = document.getElementById('c-waterfall'); if(!host) return;
  const yOf = v => 200 - v*1.7;
  const spans=[];
  let acc=0;
  steps.forEach((s,i)=>{
    if(s.total){ spans.push({i, y0:0, y1:s.v, c:T.gold, s}); acc=s.v; }
    else if(s.v>=0){ spans.push({i, y0:acc, y1:acc+s.v, c:T.teal, s}); acc+=s.v; }
    else { spans.push({i, y0:acc, y1:acc+s.v, c:T.rose, s}); acc+=s.v; }
  });
  const bars = spans.map((d,i) => {
    const x = 36 + i*60;
    const y = Math.min(yOf(d.y0), yOf(d.y1));
    const h = Math.max(3, Math.abs(yOf(d.y1)-yOf(d.y0)));
    const label = d.s.total ? String(d.s.v) : ((d.s.v>0?'+':'')+d.s.v);
    const prev = i>0 && !d.s.total ? `<line x1="${36+(i-1)*60+36}" x2="${x}" y1="${yOf(d.y0)}" y2="${yOf(d.y0)}" stroke="${T.sub}" stroke-dasharray="3 3"/>` : '';
    return `${prev}<rect x="${x}" y="${y}" width="36" height="${h}" rx="4" fill="${d.c}"/>
      <text x="${x+18}" y="${y-6}" text-anchor="middle" fill="${T.fg}" font-size="11" font-weight="700">${label}</text>
      <text x="${x+18}" y="228" text-anchor="middle" fill="${T.sub}" font-size="10">${d.s.n}</text>`;
  }).join('');
  host.innerHTML = `<svg viewBox="0 0 400 240" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">${bars}</svg>`;
}
```

