1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Gráficos y Estadísticas chartsvgzero-dependencyscattercorrelationregressionstatistics

Scatter Plot

Diagrama de dispersión X/Y para hasta tres grupos con formas distintas y línea de tendencia.

Vista Previa en Vivo

Ad spend vs signups

Per campaign

  • Search
  • Social
  • Video
0 100 200 300 0 20 40 60 80 100 Spend ($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

Uso

Copia y pega este código directamente en tu página de Astro:

astro
---
import { ScatterPlot } from 'astro-component-kit/charts';
---

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

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/lib/ScatterPlot.astro:

astro
---
/**
 * 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).
 */
import ChartFrame from './ChartFrame.astro';
import { scaleLinear, niceTicks, colorAt, formatCompact } from './chart-utils';

interface Pt { x: number; y: number; label?: string }
interface Props {
  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;
}

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) };
  });

const {
  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 = '',
} = Astro.props;

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'];
---

<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) => (
      <g>
        <line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
        <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{formatCompact(t)}</text>
      </g>
    ))}
    {xt.map((t) => (
      <g>
        <line class="cf-grid" x1={x(t)} x2={x(t)} y1={m.t} y2={H - m.b} />
        <text class="cf-label" x={x(t)} y={H - m.b + 16} text-anchor="middle">{formatCompact(t)}</text>
      </g>
    ))}
    <text class="cf-label" x={(m.l + W - m.r) / 2} y={H - 4} text-anchor="middle">{xLabel}</text>
    <text class="cf-label" transform={`translate(12 ${(m.t + H - m.b) / 2}) rotate(-90)`} text-anchor="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)" stroke-opacity="0.45" stroke-width="1.5" />
        <text class="cf-label" x={W - m.r} y={y(icpt + slope * xt[xt.length - 1]) - 8} text-anchor="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>
--- /** * 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). */ import ChartFrame from './ChartFrame.astro'; import { scaleLinear, niceTicks, colorAt, formatCompact } from './chart-utils'; interface Pt { x: number; y: number; label?: string } interface Props { 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; } 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) }; }); const { 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 = '', } = Astro.props; 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']; --- <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) => ( <g> <line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} /> <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{formatCompact(t)}</text> </g> ))} {xt.map((t) => ( <g> <line class="cf-grid" x1={x(t)} x2={x(t)} y1={m.t} y2={H - m.b} /> <text class="cf-label" x={x(t)} y={H - m.b + 16} text-anchor="middle">{formatCompact(t)}</text> </g> ))} <text class="cf-label" x={(m.l + W - m.r) / 2} y={H - 4} text-anchor="middle">{xLabel}</text> <text class="cf-label" transform={`translate(12 ${(m.t + H - m.b) / 2}) rotate(-90)`} text-anchor="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)" stroke-opacity="0.45" stroke-width="1.5" /> <text class="cf-label" x={W - m.r} y={y(icpt + slope * xt[xt.length - 1]) - 8} text-anchor="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>

Información Rápida

Categoría
Gráficos y Estadísticas
Nombre de archivo
ScatterPlot.astro
Dependencias
apexcharts (peer)
Etiquetas
chartsvgzero-dependencyscattercorrelationregressionstatistics