---
code: "TIM-14"
id: "c-cohort"
tab: "timeline"
kind: "core"
title: "Cohort Heatmap · 코호트 잔존"
library: "ECharts"
deps: ["echarts@5"]
load: "low"
pasteKind: "echarts-mount"
family: "cohort"
dataShape: "retention by start-cohort and age period"
tasks: ["show trend", "show seasonality", "show sequence", "inspect shape in 3D", "show retention decay"]
tags: ["time-series", "trend", "timeline", "3d", "spatial", "cohort", "retention", "heatmap", "코호트", "잔존"]
gallery: "https://graph.heal7.com/#TIM-14"
js: "/sku/TIM-14.js"
---

# [TIM-14] Cohort Heatmap · 코호트 잔존

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

## 언제 가져가나
행=가입 코호트, 열=경과 월, 색=잔존율 — 달력·시간대 히트맵과 다른 읽기 구조.

**응용** 구독 잔존, 온보딩 완료율, 기수별 학습 유지, 캠페인 코호트.

## 데이터 모양
- family: `cohort`
- dataShape: retention by start-cohort and age period
- tasks: show trend, show seasonality, show sequence, inspect shape in 3D, show retention decay
- tags: time-series, trend, timeline, 3d, spatial, cohort, retention, heatmap, 코호트, 잔존


## 의존
echarts@5 · load `low` · library `ECharts`

## 붙여넣는 법
1. echarts 5.x 로드 (GL이면 echarts-gl도)
2. 컨테이너 id를 원본 id와 맞추거나 `document.getElementById` 한 줄만 바꾼다
3. `T`, `reg`, `cssv` 가 필요하면 갤러리 테마 스텁을 함께 가져간다

```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 cssv = (n, fb) => (getComputedStyle(document.documentElement).getPropertyValue(n).trim() || fb);
const isDark = () => document.documentElement.getAttribute('data-theme') !== 'light';
const charts = [];
const reg = (id, opt) => {
  const dom = document.getElementById(id); if (!dom) return;
  const c = echarts.init(dom, null, {renderer:'canvas'});
  c.setOption(opt);
  charts.push(c);
  return c;
};
const factory = function(){
  const cohorts = ['1월','2월','3월','4월','5월','6월'];
  const ages = ['M0','M1','M2','M3','M4','M5'];
  const grid = [
    [100, 64, 51, 44, 40, 38],
    [100, 61, 47, 41, 37, null],
    [100, 68, 54, 46, null, null],
    [100, 59, 45, null, null, null],
    [100, 66, null, null, null, null],
    [100, null, null, null, null, null]
  ];
  const data=[];
  grid.forEach((row,y)=> row.forEach((v,x)=> { if(v!=null) data.push([x,y,v]); }));
  reg('c-cohort', {
    backgroundColor:T.bg,
    tooltip:{formatter:p=>`${cohorts[p.value[1]]} 코호트 · ${ages[p.value[0]]}<br/>잔존 <b>${p.value[2]}%</b>`},
    grid:{top:18, right:18, bottom:28, left:48},
    xAxis:{type:'category', data:ages, axisLabel:{color:T.sub,fontSize:11}, axisLine:{lineStyle:{color:T.grid}}, splitArea:{show:true}},
    yAxis:{type:'category', data:cohorts, inverse:true, axisLabel:{color:T.fg,fontSize:12,fontWeight:600}, splitArea:{show:true}},
    visualMap:{min:30, max:100, show:false, inRange:{color:['#1f1640','#3a5dd0','#6b8cff','#e8c069']}},
    series:[{type:'heatmap', data,
      label:{show:true, color:'#fff', fontSize:10, fontWeight:700, formatter:p=>p.value[2]},
      itemStyle:{borderRadius:4, borderColor:'#0e0a1a', borderWidth:1}}]
  });
};
factory();
```

## 원본 factory (갤러리 정본 발췌)
- file: `frontend/js/graph-gallery/main.js`
- lines: 3235-3259
- gallery: https://graph.heal7.com/#TIM-14
- raw js: https://graph.heal7.com/sku/TIM-14.js

```js
function(){
  const cohorts = ['1월','2월','3월','4월','5월','6월'];
  const ages = ['M0','M1','M2','M3','M4','M5'];
  const grid = [
    [100, 64, 51, 44, 40, 38],
    [100, 61, 47, 41, 37, null],
    [100, 68, 54, 46, null, null],
    [100, 59, 45, null, null, null],
    [100, 66, null, null, null, null],
    [100, null, null, null, null, null]
  ];
  const data=[];
  grid.forEach((row,y)=> row.forEach((v,x)=> { if(v!=null) data.push([x,y,v]); }));
  reg('c-cohort', {
    backgroundColor:T.bg,
    tooltip:{formatter:p=>`${cohorts[p.value[1]]} 코호트 · ${ages[p.value[0]]}<br/>잔존 <b>${p.value[2]}%</b>`},
    grid:{top:18, right:18, bottom:28, left:48},
    xAxis:{type:'category', data:ages, axisLabel:{color:T.sub,fontSize:11}, axisLine:{lineStyle:{color:T.grid}}, splitArea:{show:true}},
    yAxis:{type:'category', data:cohorts, inverse:true, axisLabel:{color:T.fg,fontSize:12,fontWeight:600}, splitArea:{show:true}},
    visualMap:{min:30, max:100, show:false, inRange:{color:['#1f1640','#3a5dd0','#6b8cff','#e8c069']}},
    series:[{type:'heatmap', data,
      label:{show:true, color:'#fff', fontSize:10, fontWeight:700, formatter:p=>p.value[2]},
      itemStyle:{borderRadius:4, borderColor:'#0e0a1a', borderWidth:1}}]
  });
}
```

