---
code: "INT-31"
id: "c-pulsebeat"
tab: "dynamic-2"
kind: "interactive"
title: "Pulse Beat · 심박 라인"
library: "Canvas"
deps: ["canvas"]
load: "high"
pasteKind: "custom-mount"
family: "monitoring"
dataShape: "heartbeat-style signal line"
tasks: ["explore data", "filter subsets", "monitor change", "show trend over time", "show health pulse"]
tags: ["interactive", "exploration", "linked view", "time-series", "pulse", "heartbeat", "beat", "심박", "라인"]
gallery: "https://graph.heal7.com/#INT-31"
js: "/sku/INT-31.js"
---

# [INT-31] Pulse Beat · 심박 라인

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

## 언제 가져가나
생체 펄스 리듬을 닮은 라이브 라인 — 일정 주기마다 큰 박동이 좌→우로 흘러감.

**응용** 헬스 데이터 비주얼, 실시간 만족도/NPS, 살아있는 KPI 인디케이터.

## 데이터 모양
- family: `monitoring`
- dataShape: heartbeat-style signal line
- tasks: explore data, filter subsets, monitor change, show trend over time, show health pulse
- tags: interactive, exploration, linked view, time-series, pulse, heartbeat, beat, 심박, 라인


## 의존
canvas · load `high` · library `Canvas`

## 붙여넣는 법
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 dom = document.getElementById('c-pulsebeat'); if(!dom) return;
  const cnv = document.createElement('canvas'); cnv.style.width='100%'; cnv.style.height='100%';
  dom.appendChild(cnv);
  const ctx = cnv.getContext('2d');
  let W=0,H=0,DPR=window.devicePixelRatio||1;
  function frame(t){
    if(W<=0||H<=0) return;
    ctx.clearRect(0,0,W,H);
    const cy=H/2, period=W*0.6, scrollSpeed=0.06;
    const phase=(t||0)*scrollSpeed;
    ctx.strokeStyle='rgba(255,255,255,.06)'; ctx.lineWidth=1;
    for(let gy=cy-60; gy<=cy+60; gy+=20){ ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(W,gy); ctx.stroke(); }
    function yAt(x){
      const u=(x+phase)/period;
      const beatX=((u%1)+1)%1;
      let beat=0;
      if(beatX<.1){ const k=beatX/.1; beat=Math.sin(k*Math.PI*2)*42*Math.exp(-k*3); }
      return cy + Math.sin(u*Math.PI*4)*8 + beat;
    }
    const grad=ctx.createLinearGradient(0,0,W,0);
    grad.addColorStop(0,'rgba(232,192,105,0)'); grad.addColorStop(.3,T.gold);
    grad.addColorStop(.7,T.rose); grad.addColorStop(1,'rgba(224,122,159,0)');
    ctx.strokeStyle=grad; ctx.lineWidth=2.4;
    ctx.beginPath();
    for(let x=0;x<=W;x+=2){ const y=yAt(x); if(x===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }
    ctx.stroke();
    const headX=W*0.85, headY=yAt(headX);
    const glow=ctx.createRadialGradient(headX,headY,0,headX,headY,18);
    glow.addColorStop(0,T.rose); glow.addColorStop(1,'rgba(224,122,159,0)');
    ctx.fillStyle=glow; ctx.beginPath(); ctx.arc(headX,headY,18,0,Math.PI*2); ctx.fill();
    requestAnimationFrame(frame);
  }
  function fit(){ const r=cnv.getBoundingClientRect(); W=r.width; H=r.height; cnv.width=W*DPR; cnv.height=H*DPR; ctx.setTransform(DPR,0,0,DPR,0,0); if(W>0&&H>0) requestAnimationFrame(frame); }
  fit(); window.addEventListener('resize', fit);
  if(window.ResizeObserver){ const ro=new ResizeObserver(fit); ro.observe(dom); }
};
factory();
```

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

```js
function(){
  const dom = document.getElementById('c-pulsebeat'); if(!dom) return;
  const cnv = document.createElement('canvas'); cnv.style.width='100%'; cnv.style.height='100%';
  dom.appendChild(cnv);
  const ctx = cnv.getContext('2d');
  let W=0,H=0,DPR=window.devicePixelRatio||1;
  function frame(t){
    if(W<=0||H<=0) return;
    ctx.clearRect(0,0,W,H);
    const cy=H/2, period=W*0.6, scrollSpeed=0.06;
    const phase=(t||0)*scrollSpeed;
    ctx.strokeStyle='rgba(255,255,255,.06)'; ctx.lineWidth=1;
    for(let gy=cy-60; gy<=cy+60; gy+=20){ ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(W,gy); ctx.stroke(); }
    function yAt(x){
      const u=(x+phase)/period;
      const beatX=((u%1)+1)%1;
      let beat=0;
      if(beatX<.1){ const k=beatX/.1; beat=Math.sin(k*Math.PI*2)*42*Math.exp(-k*3); }
      return cy + Math.sin(u*Math.PI*4)*8 + beat;
    }
    const grad=ctx.createLinearGradient(0,0,W,0);
    grad.addColorStop(0,'rgba(232,192,105,0)'); grad.addColorStop(.3,T.gold);
    grad.addColorStop(.7,T.rose); grad.addColorStop(1,'rgba(224,122,159,0)');
    ctx.strokeStyle=grad; ctx.lineWidth=2.4;
    ctx.beginPath();
    for(let x=0;x<=W;x+=2){ const y=yAt(x); if(x===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }
    ctx.stroke();
    const headX=W*0.85, headY=yAt(headX);
    const glow=ctx.createRadialGradient(headX,headY,0,headX,headY,18);
    glow.addColorStop(0,T.rose); glow.addColorStop(1,'rgba(224,122,159,0)');
    ctx.fillStyle=glow; ctx.beginPath(); ctx.arc(headX,headY,18,0,Math.PI*2); ctx.fill();
    requestAnimationFrame(frame);
  }
  function fit(){ const r=cnv.getBoundingClientRect(); W=r.width; H=r.height; cnv.width=W*DPR; cnv.height=H*DPR; ctx.setTransform(DPR,0,0,DPR,0,0); if(W>0&&H>0) requestAnimationFrame(frame); }
  fit(); window.addEventListener('resize', fit);
  if(window.ResizeObserver){ const ro=new ResizeObserver(fit); ro.observe(dom); }
}
```

