---
name: data-viz-gen
description: |
  트리거: "차트 만들어줘", "데이터 시각화", "그래프", "차트 코드", "시각화해줘",
  "plotly", "matplotlib", "seaborn", "d3", "python 시각화", "대시보드 차트"
  수행: 데이터셋 구조 및 프레임워크 분석 → 최적 차트 유형 선택 → Chart.js / Recharts(React) /
  Plotly / Matplotlib+Seaborn(Python) 코드 생성
  출력: 차트 유형 선택 근거 + 완전한 컴포넌트/스크립트 코드 + 반응형/접근성 설정
---
# Data Visualization Generator

## 목적
데이터셋의 구조와 목적을 분석해 최적의 차트 유형을 선택하고,
프레임워크(React/Python/바닐라)에 맞는 완전한 시각화 코드를 생성한다.

---

## 실행 절차

### 1단계: 환경 및 데이터 파악

| 확인 항목 | 선택지 |
|---------|--------|
| 환경/프레임워크 | React(Recharts) / 바닐라 JS(Chart.js) / Python(Plotly/Matplotlib/Seaborn) |
| 데이터 구조 | 시계열, 카테고리, 비율, 분포, 상관관계, 계층형 |
| 시각화 목적 | 트렌드, 비교, 분포, 비율, 관계 파악 |
| 인터랙션 필요 | 정적 이미지 vs 인터랙티브(hover/zoom/click) |
| 반응형 필요 | 고정 크기 vs 반응형 |

### 2단계: 차트 유형 선택 가이드

| 목적 | 권장 차트 | 피해야 할 차트 |
|------|-----------|--------------|
| 시간에 따른 트렌드 | Line, Area | Pie, Bar(수직) |
| 카테고리 비교 | Bar(수직/수평), Lollipop | Pie(5개 이상) |
| 비율/구성 | Pie, Donut, Treemap | Pie(5개 초과) |
| 두 변수 상관관계 | Scatter, Bubble | Line, Bar |
| 데이터 분포 | Histogram, Box Plot, Violin | Pie, Line |
| 다차원 비교 | Radar, Parallel Coordinates | Pie |
| 누적 트렌드 | Stacked Area, Stacked Bar | Line(누적) |
| 지리 데이터 | Choropleth, Bubble Map | Bar, Line |
| 계층 구조 | Treemap, Sunburst, Sankey | Pie |

**선택 금지 패턴:**
- 카테고리 7개 이상 → Pie/Donut 대신 Bar 사용
- 음수값 포함 → Pie/Donut 사용 불가
- 데이터 포인트 500개 이상 → 집계 후 시각화
- 3D 차트 → 일반적으로 가독성 저하, 사용 지양

---

### 3단계: Recharts 코드 생성 (React)

**기본 Line Chart:**
```tsx
'use client';
import {
  LineChart, Line, XAxis, YAxis, CartesianGrid,
  Tooltip, Legend, ResponsiveContainer,
} from 'recharts';

interface DataPoint { month: string; revenue: number; cost: number; }

const CustomTooltip = ({ active, payload, label }: any) => {
  if (!active || !payload?.length) return null;
  return (
    <div className="bg-white border border-gray-200 rounded-lg p-3 shadow-md">
      <p className="font-semibold text-gray-700 mb-1">{label}</p>
      {payload.map((entry: any) => (
        <p key={entry.dataKey} style={{ color: entry.color }}>
          {entry.name}: {entry.value.toLocaleString()}원
        </p>
      ))}
    </div>
  );
};

export function SalesLineChart({ data, height = 400 }: { data: DataPoint[]; height?: number }) {
  return (
    <div role="img" aria-label="월별 매출 및 비용 추이 차트">
      <ResponsiveContainer width="100%" height={height}>
        <LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
          <XAxis dataKey="month" tick={{ fontSize: 12 }} tickLine={false} />
          <YAxis
            tick={{ fontSize: 12 }}
            tickFormatter={(v) => `${(v / 1000).toFixed(0)}K`}
            tickLine={false} axisLine={false}
          />
          <Tooltip content={<CustomTooltip />} />
          <Legend />
          <Line type="monotone" dataKey="revenue" name="매출"
            stroke="#6366f1" strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} />
          <Line type="monotone" dataKey="cost" name="비용"
            stroke="#f43f5e" strokeWidth={2} dot={{ r: 4 }} strokeDasharray="5 5" />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}
```

**Pie / Donut Chart:**
```tsx
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts';

const COLORS = ['#6366f1', '#8b5cf6', '#a78bfa', '#c4b5fd', '#ddd6fe'];
const RADIAN = Math.PI / 180;

const renderLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent }: any) => {
  if (percent < 0.05) return null;
  const r = innerRadius + (outerRadius - innerRadius) * 0.5;
  return (
    <text x={cx + r * Math.cos(-midAngle * RADIAN)}
          y={cy + r * Math.sin(-midAngle * RADIAN)}
          fill="white" textAnchor="middle" dominantBaseline="central" fontSize={12}>
      {`${(percent * 100).toFixed(1)}%`}
    </text>
  );
};

export function CategoryDonutChart({ data }: { data: Array<{ name: string; value: number }> }) {
  return (
    <ResponsiveContainer width="100%" height={350}>
      <PieChart>
        <Pie data={data} cx="50%" cy="50%" innerRadius="55%" outerRadius="80%"
          labelLine={false} label={renderLabel} dataKey="value">
          {data.map((_, i) => <Cell key={i} fill={COLORS[i % COLORS.length]} />)}
        </Pie>
        <Tooltip formatter={(v: number) => v.toLocaleString()} />
        <Legend />
      </PieChart>
    </ResponsiveContainer>
  );
}
```

---

### 4단계: Chart.js 코드 생성 (바닐라 JS / CDN)

```javascript
import { Chart, CategoryScale, LinearScale, PointElement, LineElement,
  BarElement, ArcElement, Title, Tooltip, Legend, Filler } from 'chart.js';

Chart.register(CategoryScale, LinearScale, PointElement, LineElement,
  BarElement, ArcElement, Title, Tooltip, Legend, Filler);

function createLineChart(canvasId, data) {
  const ctx = document.getElementById(canvasId).getContext('2d');
  return new Chart(ctx, {
    type: 'line',
    data: {
      labels: data.map(d => d.month),
      datasets: [{
        label: '매출',
        data: data.map(d => d.revenue),
        borderColor: '#6366f1',
        backgroundColor: 'rgba(99,102,241,0.1)',
        fill: true, tension: 0.4,
      }],
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      interaction: { mode: 'index', intersect: false },
      plugins: {
        tooltip: {
          callbacks: { label: (ctx) => `${ctx.dataset.label}: ${ctx.parsed.y.toLocaleString()}원` },
        },
      },
      scales: {
        y: { beginAtZero: true, ticks: { callback: (v) => `${(v/1000).toFixed(0)}K` } },
        x: { grid: { display: false } },
      },
    },
  });
}
```

---

### 5단계: Python — Plotly (인터랙티브)

Plotly는 인터랙티브 차트가 필요하거나 Jupyter / Dash / Streamlit 환경에 적합하다.

```python
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd

# Line Chart
def create_sales_line_chart(df: pd.DataFrame) -> go.Figure:
    """월별 매출 트렌드 인터랙티브 차트."""
    fig = px.line(
        df, x='month', y=['revenue', 'cost'],
        title='월별 매출 및 비용 추이',
        labels={'value': '금액 (원)', 'month': '월', 'variable': '항목'},
        color_discrete_map={'revenue': '#6366f1', 'cost': '#f43f5e'},
    )
    fig.update_traces(mode='lines+markers')
    fig.update_layout(
        hovermode='x unified',
        legend=dict(orientation='h', yanchor='bottom', y=1.02),
        plot_bgcolor='white',
        xaxis=dict(showgrid=False),
        yaxis=dict(gridcolor='#f0f0f0', tickformat=',.0f'),
    )
    return fig

# Bar + Line 복합 차트
def create_combo_chart(df: pd.DataFrame) -> go.Figure:
    """판매량(Bar) + 성장률(Line) 복합 차트."""
    fig = make_subplots(specs=[[{"secondary_y": True}]])
    fig.add_trace(
        go.Bar(x=df['month'], y=df['sales'], name='판매량', marker_color='#6366f1'),
        secondary_y=False,
    )
    fig.add_trace(
        go.Scatter(x=df['month'], y=df['growth_rate'], name='성장률(%)',
                   line=dict(color='#f43f5e', width=2)),
        secondary_y=True,
    )
    fig.update_layout(title='판매량 및 성장률', hovermode='x unified')
    fig.update_yaxes(title_text='판매량', secondary_y=False)
    fig.update_yaxes(title_text='성장률 (%)', secondary_y=True)
    return fig

# Plotly 저장 / 표시
fig = create_sales_line_chart(df)
fig.write_html('chart.html')        # HTML로 저장
fig.write_image('chart.png')        # 이미지로 저장 (kaleido 필요)
fig.show()                           # 브라우저에서 열기

# 의존성 설치
# pip install plotly kaleido
```

---

### 6단계: Python — Matplotlib + Seaborn (정적 이미지 / 논문/리포트)

Matplotlib/Seaborn은 정적 이미지 생성, 논문 품질 차트, 데이터 분석 리포트에 적합하다.

```python
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import seaborn as sns
import pandas as pd
import numpy as np

# 전역 스타일 설정
plt.rcParams.update({
    'font.family': 'AppleGothic',    # Mac: 한글 폰트 (Windows: 'Malgun Gothic')
    'axes.unicode_minus': False,
    'figure.dpi': 150,
    'axes.spines.top': False,
    'axes.spines.right': False,
})
sns.set_theme(style='whitegrid', palette='muted')


def plot_sales_trend(df: pd.DataFrame, save_path: str | None = None):
    """월별 매출 트렌드 — Seaborn 라인 차트."""
    fig, ax = plt.subplots(figsize=(12, 6))

    # 라인 + 신뢰구간
    sns.lineplot(data=df, x='month', y='revenue', label='매출',
                 color='#6366f1', linewidth=2.5, marker='o', ax=ax)
    sns.lineplot(data=df, x='month', y='cost', label='비용',
                 color='#f43f5e', linewidth=2, linestyle='--', marker='s', ax=ax)

    ax.set_title('월별 매출 및 비용 추이', fontsize=16, fontweight='bold', pad=20)
    ax.set_xlabel('월', fontsize=12)
    ax.set_ylabel('금액 (원)', fontsize=12)
    ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'{x/1e6:.1f}M'))
    ax.legend(fontsize=11)
    ax.tick_params(axis='x', rotation=45)

    plt.tight_layout()
    if save_path:
        fig.savefig(save_path, bbox_inches='tight', dpi=300)
    return fig


def plot_distribution(df: pd.DataFrame, column: str):
    """분포 분석 — Histogram + Box Plot 조합."""
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

    # Histogram + KDE
    sns.histplot(df[column], kde=True, ax=ax1, color='#6366f1', bins=30)
    ax1.set_title(f'{column} 분포')

    # Box Plot + Strip
    sns.boxplot(y=df[column], ax=ax2, color='#dbe4ff')
    sns.stripplot(y=df[column], ax=ax2, color='#6366f1', alpha=0.4, size=3)
    ax2.set_title(f'{column} Box Plot')

    plt.tight_layout()
    return fig


def plot_correlation_heatmap(df: pd.DataFrame, columns: list[str]):
    """상관관계 히트맵."""
    corr = df[columns].corr()
    mask = np.triu(np.ones_like(corr, dtype=bool))  # 상삼각 마스킹

    fig, ax = plt.subplots(figsize=(10, 8))
    sns.heatmap(
        corr, mask=mask, annot=True, fmt='.2f',
        cmap='RdYlBu_r', center=0, vmin=-1, vmax=1,
        square=True, linewidths=0.5, ax=ax,
    )
    ax.set_title('변수 간 상관관계', fontsize=14, pad=15)
    plt.tight_layout()
    return fig


def plot_category_comparison(df: pd.DataFrame, category_col: str, value_col: str):
    """카테고리 비교 — 수평 Bar Chart."""
    df_sorted = df.sort_values(value_col, ascending=True)

    fig, ax = plt.subplots(figsize=(10, max(6, len(df) * 0.4)))
    bars = ax.barh(df_sorted[category_col], df_sorted[value_col],
                   color='#6366f1', edgecolor='white')

    # 값 레이블
    for bar in bars:
        ax.text(bar.get_width() + bar.get_width() * 0.01,
                bar.get_y() + bar.get_height() / 2,
                f'{bar.get_width():,.0f}', va='center', fontsize=10)

    ax.set_title(f'{category_col}별 {value_col} 비교', fontsize=14)
    ax.set_xlabel(value_col)
    ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'{x:,.0f}'))
    plt.tight_layout()
    return fig


# 의존성 설치
# pip install matplotlib seaborn pandas numpy
```

---

### 7단계: 데이터 전처리 유틸

```typescript
// chart-utils.ts (TypeScript)
export function aggregateByMonth(records: Array<{ date: string; value: number }>) {
  const groups = records.reduce((acc, r) => {
    const month = r.date.slice(0, 7);
    if (!acc[month]) acc[month] = [];
    acc[month].push(r.value);
    return acc;
  }, {} as Record<string, number[]>);

  return Object.entries(groups).sort(([a], [b]) => a.localeCompare(b)).map(([month, vals]) => ({
    month,
    total: vals.reduce((s, v) => s + v, 0),
    average: vals.reduce((s, v) => s + v, 0) / vals.length,
  }));
}

export function removeOutliers(values: number[]): number[] {
  const sorted = [...values].sort((a, b) => a - b);
  const q1 = sorted[Math.floor(sorted.length * 0.25)];
  const q3 = sorted[Math.floor(sorted.length * 0.75)];
  const iqr = q3 - q1;
  return values.filter(v => v >= q1 - 1.5 * iqr && v <= q3 + 1.5 * iqr);
}

export function generateColorPalette(count: number): string[] {
  const base = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b', '#3b82f6', '#8b5cf6', '#ec4899', '#14b8a6'];
  return Array.from({ length: count }, (_, i) => base[i % base.length]);
}
```

---

## 출력 형식

```
## 데이터 시각화 생성 결과

### 차트 유형 선택 근거
- 데이터: [설명]
- 선택 라이브러리: [Recharts / Chart.js / Plotly / Matplotlib+Seaborn]
- 선택 차트: [차트 유형]
- 이유: [선택 근거]

### 생성된 코드
[컴포넌트/함수명] — [라이브러리] 기반 [차트 유형]

### 의존성 설치
\`\`\`bash
npm install recharts        # React
pip install plotly kaleido  # Python 인터랙티브
pip install matplotlib seaborn  # Python 정적
\`\`\`
```

---

## 라이브러리 선택 가이드

| 상황 | 추천 라이브러리 |
|------|--------------|
| React 앱, 인터랙티브 | **Recharts** |
| 바닐라 JS, 빠른 구현 | **Chart.js** |
| Python, 인터랙티브, Jupyter/Dash | **Plotly** |
| Python, 논문/리포트 품질 이미지 | **Matplotlib + Seaborn** |
| 고도 커스텀, 복잡한 시각화 | **D3.js** |
| 대용량 데이터(수백만 점) | **Observable Plot / Apache ECharts** |

---

## 주의사항
- 카테고리가 7개 이상인 Pie 차트는 가독성이 떨어진다. Bar Chart로 대체를 제안한다.
- Chart.js는 `maintainAspectRatio: false`와 부모 요소 `height` 지정이 함께 있어야 반응형으로 동작한다.
- Recharts `ResponsiveContainer`의 부모 요소에 명시적 높이가 없으면 차트가 렌더링되지 않는다.
- 접근성을 위해 `role="img"` + `aria-label`을 항상 추가한다.
- 색맹 사용자를 위해 색상만으로 구분하지 않고 선 스타일(실선/점선)도 함께 사용한다.
- Matplotlib에서 한글 표시 시 반드시 한글 폰트를 지정한다 (Mac: `AppleGothic`, Windows: `Malgun Gothic`).
- Plotly는 `fig.write_image()` 사용 시 `kaleido` 패키지가 추가로 필요하다.
