Featuring Design System

ScatterChart

두 변수의 상관관계를 점으로 시각화하는 차트 컨테이너.

개요

ScatterChart는 recharts의 ScatterChart를 얇게 래핑한 컨테이너입니다. XAxis, YAxis를 모두 type="number"로 사용해 데이터 포인트의 분포를 표현합니다.

  • Scatter 시리즈fill, stroke, data, activeShape, label $토큰 지원
  • ZAxis 조합 — 세 번째 차원(버블 크기) 표현으로 버블 차트 구성

언제 사용하나요

  • 두 변수의 상관관계 — 팔로워 수 vs 참여율
  • 군집 시각화 — 여러 그룹의 분포 비교
  • 이상치 탐지 — 분포에서 벗어난 포인트 식별
  • 버블 차트ZAxis로 세 번째 지표 추가

Usage

기본 사용법

() => {
const data = [
  { followers: 12000, engagement: 4.2 },
  { followers: 35000, engagement: 3.1 },
  { followers: 8000, engagement: 6.8 },
  { followers: 120000, engagement: 1.9 },
  { followers: 50000, engagement: 2.7 },
  { followers: 6000, engagement: 8.5 },
  { followers: 200000, engagement: 1.2 },
  { followers: 25000, engagement: 4.9 },
];
return (
  <ResponsiveContainer width="100%" height={300}>
    <ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
      <CartesianGrid />
      <XAxis
        dataKey="followers"
        name="팔로워 수"
        tickFormatter={(v) => (v / 1000).toFixed(0) + 'K'}
      />
      <YAxis dataKey="engagement" name="참여율" unit="%" />
      <Scatter name="인플루언서" data={data} fill="$primary-50" />
      <ChartTooltip cursor={{ strokeDasharray: '3 3' }} />
    </ScatterChart>
  </ResponsiveContainer>
);
}

버블 차트 (ZAxis)

ZAxis는 re-export된 recharts 컴포넌트입니다. 데이터 포인트의 크기를 세 번째 차원으로 매핑합니다.

() => {
const data = [
  { followers: 12000, engagement: 4.2, reach: 8500 },
  { followers: 35000, engagement: 3.1, reach: 22000 },
  { followers: 8000, engagement: 6.8, reach: 5500 },
  { followers: 120000, engagement: 1.9, reach: 80000 },
  { followers: 50000, engagement: 2.7, reach: 33000 },
];
return (
  <ResponsiveContainer width="100%" height={300}>
    <ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
      <CartesianGrid />
      <XAxis dataKey="followers" name="팔로워" tickFormatter={(v) => (v / 1000).toFixed(0) + 'K'} />
      <YAxis dataKey="engagement" name="참여율" unit="%" />
      <ZAxis dataKey="reach" range={[60, 400]} name="도달" />
      <Scatter name="인플루언서" data={data} fill="$primary-50" fillOpacity={0.6} />
      <ChartTooltip cursor={{ strokeDasharray: '3 3' }} />
    </ScatterChart>
  </ResponsiveContainer>
);
}

다중 그룹

여러 Scatter를 사용해 그룹을 비교합니다.

() => {
const groupA = [
  { x: 10, y: 5 },
  { x: 30, y: 3 },
  { x: 70, y: 2 },
  { x: 5, y: 7 },
];
const groupB = [
  { x: 18, y: 4 },
  { x: 45, y: 2 },
  { x: 90, y: 1.5 },
  { x: 7, y: 6 },
];
return (
  <ResponsiveContainer width="100%" height={300}>
    <ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
      <CartesianGrid />
      <XAxis dataKey="x" type="number" name="X" />
      <YAxis dataKey="y" type="number" name="Y" />
      <Scatter name="그룹 A" data={groupA} fill="$primary-50" />
      <Scatter name="그룹 B" data={groupB} fill="$teal-50" />
      <ChartTooltip cursor={{ strokeDasharray: '3 3' }} />
      <ChartLegend />
    </ScatterChart>
  </ResponsiveContainer>
);
}

$ 토큰 fill

Scatter.fill$primary-50, $teal-50 등 토큰을 직접 전달합니다.

() => {
const data = [
  { followers: 12000, engagement: 4.2 },
  { followers: 35000, engagement: 3.1 },
  { followers: 8000, engagement: 6.8 },
  { followers: 120000, engagement: 1.9 },
  { followers: 50000, engagement: 2.7 },
];
return (
  <VStack $css={{ width: '100%', gap: '$spacing-400' }}>
    <ResponsiveContainer width="100%" height={200}>
      <ScatterChart margin={{ top: 10, right: 20, bottom: 20, left: 20 }}>
        <CartesianGrid />
        <XAxis dataKey="followers" name="팔로워" tickFormatter={(v) => (v / 1000).toFixed(0) + 'K'} />
        <YAxis dataKey="engagement" name="참여율" unit="%" />
        <Scatter name="인플루언서" data={data} fill="$primary-50" />
        <ChartTooltip />
      </ScatterChart>
    </ResponsiveContainer>
    <ResponsiveContainer width="100%" height={200}>
      <ScatterChart margin={{ top: 10, right: 20, bottom: 20, left: 20 }}>
        <CartesianGrid />
        <XAxis dataKey="followers" name="팔로워" tickFormatter={(v) => (v / 1000).toFixed(0) + 'K'} />
        <YAxis dataKey="engagement" name="참여율" unit="%" />
        <Scatter name="인플루언서" data={data} fill="$teal-50" />
        <ChartTooltip />
      </ScatterChart>
    </ResponsiveContainer>
  </VStack>
);
}

Slot 토큰 — activeShape / label

activeShape는 호버 상태 포인트, label은 포인트 라벨. 오브젝트일 때 $ 토큰 해석. label 오브젝트에는 fontSize-200 / font.sans / $text-2 기본 typography가 자동 병합되며, 소비자 명시값이 우선합니다.

() => {
const data = [
  { followers: 12000, engagement: 4.2 },
  { followers: 35000, engagement: 3.1 },
  { followers: 8000, engagement: 6.8 },
  { followers: 120000, engagement: 1.9 },
  { followers: 50000, engagement: 2.7 },
];
return (
  <ResponsiveContainer width="100%" height={280}>
    <ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
      <CartesianGrid />
      <XAxis dataKey="followers" name="팔로워" tickFormatter={(v) => (v / 1000).toFixed(0) + 'K'} />
      <YAxis dataKey="engagement" name="참여율" unit="%" />
      <Scatter
        name="인플루언서"
        data={data}
        fill="$primary-50"
        activeShape={{ fill: '$teal-40', stroke: '$primary-60' }}
        label={{ fill: '$text-2', fontSize: 10 }}
      />
      <ChartTooltip cursor={{ strokeDasharray: '3 3' }} />
    </ScatterChart>
  </ResponsiveContainer>
);
}

4분면 분석 — ReferenceLine으로 분할

ReferenceLine을 도메인 중앙에 그어 포지셔닝 맵을 만듭니다. 도메인을 데이터 기반으로 정렬해 사분면이 항상 균등 면적을 갖도록 하세요.

() => {
const brands = [
  { brand: 'A', content: 42, views: 88000, fill: '$indigo-50' },
  { brand: 'B', content: 130, views: 360000, fill: '$magenta-50' },
  { brand: 'C', content: 165, views: 130000, fill: '$orange-50' },
  { brand: 'D', content: 180, views: 290000, fill: '$green-60' },
  { brand: 'E', content: 88, views: 95000, fill: '$red-50' },
  { brand: 'F', content: 72, views: 410000, fill: '$teal-50' },
];
const xMax = Math.ceil(Math.max(...brands.map((b) => b.content)) / 200) * 200;
const yMax = Math.ceil(Math.max(...brands.map((b) => b.views)) / 500000) * 500000;
const xMid = xMax / 2;
const yMid = yMax / 2;
return (
  <ResponsiveContainer width="100%" height={360}>
    <ScatterChart margin={{ top: 20, right: 30, bottom: 30, left: 50 }}>
      <CartesianGrid strokeDasharray="3 3" />
      <XAxis type="number" dataKey="content" name="콘텐츠 건수" domain={[0, xMax]} />
      <YAxis type="number" dataKey="views" name="평균 재생수" domain={[0, yMax]}
        tickFormatter={(v) => v === 0 ? '0' : (v / 10000).toFixed(0) + '만'} />
      <ReferenceLine x={xMid} stroke="$secondary-60" />
      <ReferenceLine y={yMid} stroke="$secondary-60" />
      {brands.map((b) => (
        <Scatter key={b.brand} name={b.brand} data={[b]} fill={b.fill}>
          <LabelList dataKey="brand" position="bottom" fontSize={11} fill={b.fill} />
        </Scatter>
      ))}
      <ChartTooltip cursor={{ strokeDasharray: '3 3' }} />
    </ScatterChart>
  </ResponsiveContainer>
);
}

인터랙티브 — 선택 가능한 커스텀 shape

Scatter.shape로 점 모양을 커스텀하면 선택 상태에 따라 강조 효과를 줄 수 있습니다. resolveToken$토큰 문자열을 <circle fill>처럼 SVG 속성에 직접 넣어야 할 때 사용합니다.

() => {
const brands = [
  { brand: 'COSRX', content: 42, views: 88000, fill: '$indigo-50' },
  { brand: 'Torriden', content: 130, views: 360000, fill: '$magenta-50' },
  { brand: 'Anua', content: 165, views: 130000, fill: '$orange-50' },
  { brand: 'mixsoon', content: 180, views: 290000, fill: '$green-60' },
  { brand: 'Round Lab', content: 105, views: 245000, fill: '$primary-60' },
];
const [selected, setSelected] = React.useState('Round Lab');
return (
  <ResponsiveContainer width="100%" height={320}>
    <ScatterChart margin={{ top: 20, right: 30, bottom: 30, left: 50 }}>
      <CartesianGrid strokeDasharray="3 3" />
      <XAxis type="number" dataKey="content" name="콘텐츠 건수" />
      <YAxis type="number" dataKey="views" name="평균 재생수"
        tickFormatter={(v) => (v / 10000).toFixed(0) + '만'} />
      {brands.map((b) => {
        const isSelected = b.brand === selected;
        const resolved = resolveToken(b.fill);
        return (
          <Scatter key={b.brand} name={b.brand} data={[b]} fill={b.fill} isAnimationActive={false}
            onClick={() => setSelected(b.brand)}
            shape={(props) => {
              const { cx = 0, cy = 0 } = props;
              if (isSelected) {
                return (
                  <g style={{ cursor: 'pointer' }}>
                    <circle cx={cx} cy={cy} r={14} fill={resolved} fillOpacity={0.25} />
                    <circle cx={cx} cy={cy} r={8} fill={resolved} />
                  </g>
                );
              }
              return <circle cx={cx} cy={cy} r={5} fill={resolved} style={{ cursor: 'pointer' }} />;
            }}
          >
            <LabelList dataKey="brand" position="bottom" offset={isSelected ? 14 : 6}
              fontSize={isSelected ? 13 : 11} fontWeight={isSelected ? 700 : 400} fill={b.fill} />
          </Scatter>
        );
      })}
      <ChartTooltip cursor={{ strokeDasharray: '3 3' }}
        formatter={(v, name) => name === '평균 재생수' ? [(Number(v) / 10000).toFixed(1) + '만', name] : [v, name]} />
    </ScatterChart>
  </ResponsiveContainer>
);
}

Props

recharts ScatterChart를 래핑합니다. 여기에 명시되지 않은 props는 recharts ScatterChart API를 참고하세요.

ScatterChart

extends recharts ScatterChart

Prop

Type

Scatter

extends recharts Scatter

Prop

Type

ZAxis

recharts re-export. 래핑되지 않음.

Prop

Type