1 звезда на GitHub = 1 новый компонент.

Поставить звезду
Графики и статистикаchartsvgzero-dependencyscattercorrelationregressionstatistics

Scatter Plot

Диаграмма рассеяния X/Y для трёх групп с разными формами маркеров и линией тренда.

Живое превью

Ad spend vs signups

Per campaign

  • Search
  • Social
  • Video
0100200300020406080100Spend ($k)Signups
View data
GroupSpend ($k)Signups
Search549
Search4285
Search79189
Search2192
Search58152
Search95205
Search3799
Search74184
Search1655
Search53139
Search90193
Search3290
Search69159
Search1147
Search48149
Search85205
Social529
Social4242
Social7987
Social2162
Social58103
Social95135
Social3790
Social74137
Social162
Social5358
Social90156
Social3227
Social69101
Social1118
Video525
Video4251
Video7989
Video2152
Video5880
Video9581
Video3780
Video7497
Video1635
Video5360

Использование

Скопируйте и вставьте этот код в ваш React-компонент:

tsx
import { ScatterPlot } from 'react-component-kit-ui/charts';

<ScatterPlot groups={[
    { name: 'Search', points: [{ x: 10, y: 40 }, { x: 25, y: 70 }, { x: 40, y: 98 }] },
  ]} xLabel="Spend ($k)" yLabel="Signups" trend />

Ручная установка

Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/ScatterPlot.tsx + ScatterPlot.css:

ScatterPlot.tsx
'use client';

import ChartFrame from './ChartFrame';
import { scaleLinear, niceTicks, colorAt, formatCompact } from './chart-utils';

interface Pt { x: number; y: number; label?: string }

const gen = (seed: number, n: number, slope: number, base: number, noise: number): Pt[] =>
  Array.from({ length: n }, (_, i) => {
    const r = Math.sin((i + 1) * seed) * 10000;
    const j = r - Math.floor(r);
    const xv = 5 + ((i * 37) % 95);
    return { x: xv, y: Math.round(base + slope * xv + (j - 0.5) * noise) };
  });

/**
 * ScatterPlot — X/Y relationship plot for up to 3 groups, with an optional
 * least-squares trend line and per-point tooltips.
 *
 * @param {Array<{name: string, points: Array<{x: number, y: number, label?: string}>}>} groups - Point groups (max 3).
 * @param {string} xLabel - X-axis title.
 * @param {string} yLabel - Y-axis title.
 * @param {boolean} trend - Draw a linear regression line across all points.
 * @param {string[]} colors - Optional per-group colors.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
export interface ScatterPlotProps {
  title?: string;
  subtitle?: string;
  groups?: Array<{ name: string; points: Pt[] }>;
  xLabel?: string;
  yLabel?: string;
  trend?: boolean;
  height?: number;
  colors?: string[];
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function ScatterPlot({
  title = 'Ad spend vs signups',
  subtitle = 'Per campaign',
  groups = [
    { name: 'Search', points: gen(3.1, 16, 2.1, 20, 60) },
    { name: 'Social', points: gen(7.7, 14, 1.3, 10, 70) },
    { name: 'Video', points: gen(5.3, 10, 0.8, 30, 50) },
  ],
  xLabel = 'Spend ($k)',
  yLabel = 'Signups',
  trend = true,
  height = 300,
  colors,
  variant = 'glass',
  size = 'md',
  className = '',
}: ScatterPlotProps) {
  const W = 640;
  const H = height;
  const m = { t: 12, r: 16, b: 40, l: 52 };
  const all = groups.flatMap((g) => g.points);
  const xt = niceTicks(Math.min(0, ...all.map((p) => p.x)), Math.max(...all.map((p) => p.x)), 5);
  const yt = niceTicks(Math.min(0, ...all.map((p) => p.y)), Math.max(...all.map((p) => p.y)), 4);
  const x = scaleLinear(xt[0], xt[xt.length - 1], m.l, W - m.r);
  const y = scaleLinear(yt[0], yt[yt.length - 1], H - m.b, m.t);
  
  // Ordinary least squares over all points.
  const n = all.length;
  const mx = all.reduce((a, p) => a + p.x, 0) / n;
  const my = all.reduce((a, p) => a + p.y, 0) / n;
  const slope = all.reduce((a, p) => a + (p.x - mx) * (p.y - my), 0) / (all.reduce((a, p) => a + (p.x - mx) ** 2, 0) || 1);
  const icpt = my - slope * mx;
  const r2 = (() => {
    const ssTot = all.reduce((a, p) => a + (p.y - my) ** 2, 0) || 1;
    const ssRes = all.reduce((a, p) => a + (p.y - (icpt + slope * p.x)) ** 2, 0);
    return 1 - ssRes / ssTot;
  })();
  const shapes = ['circle', 'square', 'diamond'];
  return (
    <>
      <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className} legend={groups.slice(0, 3).map((g, i) => ({ label: g.name, color: colorAt(i, colors) }))} table={{ headers: ['Group', xLabel, yLabel], rows: groups.flatMap((g) => g.points.map((p) => [p.label ? `${g.name} · ${p.label}` : g.name, p.x, p.y])) }}>
        <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: scatter plot of ${n} points${trend ? `, trend R² ${r2.toFixed(2)}` : ''}`}>
          {yt.map((t, __i: number) => (
            <g key={__i}>
              <line className="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
              <text className="cf-label" x={m.l - 8} y={y(t) + 4} textAnchor="end">{formatCompact(t)}</text>
            </g>
          ))}
          {xt.map((t, __i: number) => (
            <g key={__i}>
              <line className="cf-grid" x1={x(t)} x2={x(t)} y1={m.t} y2={H - m.b} />
              <text className="cf-label" x={x(t)} y={H - m.b + 16} textAnchor="middle">{formatCompact(t)}</text>
            </g>
          ))}
          <text className="cf-label" x={(m.l + W - m.r) / 2} y={H - 4} textAnchor="middle">{xLabel}</text>
          <text className="cf-label" transform={`translate(12 ${(m.t + H - m.b) / 2}) rotate(-90)`} textAnchor="middle">{yLabel}</text>
      
          {trend && (
            <g aria-hidden="true">
              <line x1={x(xt[0])} y1={y(icpt + slope * xt[0])} x2={x(xt[xt.length - 1])} y2={y(icpt + slope * xt[xt.length - 1])} stroke="var(--c-text-1, #fff)" strokeOpacity="0.45" strokeWidth="1.5" />
              <text className="cf-label" x={W - m.r} y={y(icpt + slope * xt[xt.length - 1]) - 8} textAnchor="end">R² {r2.toFixed(2)}</text>
            </g>
          )}
      
          {groups.slice(0, 3).map((g, gi) => {
            const col = colorAt(gi, colors);
            return g.points.map((p) => {
              const px = x(p.x);
              const py = y(p.y);
              const common = {
                class: 'cf-mark cf-dim',
                fill: col,
                'fill-opacity': 0.85,
                stroke: 'var(--chart-surface, #0d0d0f)',
                'stroke-width': 1.5,
                tabindex: 0,
                role: 'img' as const,
                'aria-label': `${g.name}${p.label ? ` ${p.label}` : ''}: ${xLabel} ${p.x}, ${yLabel} ${p.y}`,
                'data-tip-title': p.label ?? g.name,
                'data-tip': `${col}::${g.name}|${xLabel}: ${p.x}|${yLabel}: ${p.y}`,
              };
              return shapes[gi] === 'square'
                ? <rect {...common} x={px - 5} y={py - 5} width="10" height="10" rx="2" />
                : shapes[gi] === 'diamond'
                  ? <path {...common} d={`M${px},${py - 6.5} L${px + 6.5},${py} L${px},${py + 6.5} L${px - 6.5},${py} Z`} />
                  : <circle {...common} cx={px} cy={py} r="5.5" />;
            });
          })}
        </svg>
      </ChartFrame>
    </>
  );
}

export default ScatterPlot;

Краткая информация

Категория
Графики и статистика
Имя файла
ScatterPlot.tsx
Зависимости
Нет — чистый React + CSS
Теги
chartsvgzero-dependencyscattercorrelationregressionstatistics