---
code: "INT-26"
id: "c-liquidmulti"
tab: "dynamic-2"
kind: "interactive"
title: "Liquid Multi · 다중 물병"
library: "SVG"
deps: ["svg"]
load: "high"
pasteKind: "custom-mount"
family: "kpi"
dataShape: "headline value, rank race or progress state"
tasks: ["explore data", "filter subsets", "monitor change", "surface headline KPI"]
tags: ["interactive", "exploration", "linked view", "kpi", "progress", "liquid", "multi", "다중", "물병"]
gallery: "https://graph.heal7.com/#INT-26"
js: "/sku/INT-26.js"
---

# [INT-26] Liquid Multi · 다중 물병

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

## 언제 가져가나
3개 SVG 게이지 동시 — 에너지·집중·회복을 액체 레벨로.

**응용** 다축 헬스 메트릭, 자원 잔량 대시보드.

## 데이터 모양
- family: `kpi`
- dataShape: headline value, rank race or progress state
- tasks: explore data, filter subsets, monitor change, surface headline KPI
- tags: interactive, exploration, linked view, kpi, progress, liquid, multi, 다중, 물병


## 의존
svg · load `high` · 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 host = document.getElementById('c-liquidmulti'); if(!host) return;
  const items = [
    {label:'에너지', val:74, c1:'#6b8cff', c2:'#3a2a5e'},
    {label:'집중',   val:58, c1:'#e8c069', c2:'#b487e8'},
    {label:'회복',   val:88, c1:'#6dd2c4', c2:'#3a2a5e'}
  ];
  host.innerHTML = '<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:14px;height:100%;align-items:center;padding:8px 0">' +
    items.map((it,k)=>`<div style="display:flex;flex-direction:column;align-items:center;gap:6px;height:100%">
      <svg viewBox="0 0 160 160" style="width:100%;height:100%;max-height:200px" preserveAspectRatio="xMidYMid meet">
        <defs>
          <clipPath id="lm-clip-${k}"><circle cx="80" cy="80" r="58"/></clipPath>
          <linearGradient id="lm-g-${k}" x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stop-color="${it.c1}"/><stop offset="100%" stop-color="${it.c2}"/>
          </linearGradient>
        </defs>
        <circle cx="80" cy="80" r="62" fill="none" stroke="rgba(232,192,105,.35)" stroke-width="1.5"/>
        <circle cx="80" cy="80" r="58" fill="rgba(255,255,255,.03)"/>
        <g clip-path="url(#lm-clip-${k})"><path id="lm-w-${k}" fill="url(#lm-g-${k})" d=""/></g>
        <text x="80" y="86" text-anchor="middle" fill="#fff" font-size="22" font-weight="700">${it.val}%</text>
      </svg>
      <div style="font-size:11px;color:var(--sub);font-weight:600">${it.label}</div>
    </div>`).join('') + '</div>';
  const ws = items.map((it,k)=>({el:host.querySelector('#lm-w-'+k), val:it.val}));
  let phase = 0;
  function tick(){
    phase += 0.04;
    ws.forEach((w,k)=>{
      const baseY = 145 - (90 * w.val/100);
      let d = `M 0 ${baseY} `;
      for(let x=0; x<=160; x+=5) d += `L ${x} ${baseY + Math.sin((x/22)+phase+k*1.4)*4} `;
      d += `L 160 160 L 0 160 Z`;
      w.el.setAttribute('d', d);
    });
    requestAnimationFrame(tick);
  }
  tick();
};
factory();
```

## 원본 factory (갤러리 정본 발췌)
- file: `frontend/js/graph-gallery/main.js`
- lines: 2772-2809
- gallery: https://graph.heal7.com/#INT-26
- raw js: https://graph.heal7.com/sku/INT-26.js

```js
function(){
  const host = document.getElementById('c-liquidmulti'); if(!host) return;
  const items = [
    {label:'에너지', val:74, c1:'#6b8cff', c2:'#3a2a5e'},
    {label:'집중',   val:58, c1:'#e8c069', c2:'#b487e8'},
    {label:'회복',   val:88, c1:'#6dd2c4', c2:'#3a2a5e'}
  ];
  host.innerHTML = '<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:14px;height:100%;align-items:center;padding:8px 0">' +
    items.map((it,k)=>`<div style="display:flex;flex-direction:column;align-items:center;gap:6px;height:100%">
      <svg viewBox="0 0 160 160" style="width:100%;height:100%;max-height:200px" preserveAspectRatio="xMidYMid meet">
        <defs>
          <clipPath id="lm-clip-${k}"><circle cx="80" cy="80" r="58"/></clipPath>
          <linearGradient id="lm-g-${k}" x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stop-color="${it.c1}"/><stop offset="100%" stop-color="${it.c2}"/>
          </linearGradient>
        </defs>
        <circle cx="80" cy="80" r="62" fill="none" stroke="rgba(232,192,105,.35)" stroke-width="1.5"/>
        <circle cx="80" cy="80" r="58" fill="rgba(255,255,255,.03)"/>
        <g clip-path="url(#lm-clip-${k})"><path id="lm-w-${k}" fill="url(#lm-g-${k})" d=""/></g>
        <text x="80" y="86" text-anchor="middle" fill="#fff" font-size="22" font-weight="700">${it.val}%</text>
      </svg>
      <div style="font-size:11px;color:var(--sub);font-weight:600">${it.label}</div>
    </div>`).join('') + '</div>';
  const ws = items.map((it,k)=>({el:host.querySelector('#lm-w-'+k), val:it.val}));
  let phase = 0;
  function tick(){
    phase += 0.04;
    ws.forEach((w,k)=>{
      const baseY = 145 - (90 * w.val/100);
      let d = `M 0 ${baseY} `;
      for(let x=0; x<=160; x+=5) d += `L ${x} ${baseY + Math.sin((x/22)+phase+k*1.4)*4} `;
      d += `L 160 160 L 0 160 Z`;
      w.el.setAttribute('d', d);
    });
    requestAnimationFrame(tick);
  }
  tick();
}
```

