1 estrella en GitHub = 1 componente nuevo.

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

Sankey Chart

Diagrama de flujo de dos columnas donde el grosor de cada cinta codifica el volumen.

Vista Previa en Vivo

Where signups come from

Acquisition channel → plan chosen

Organic 1.2k Paid ads 570 Referral 360 Partners 180 Free 1.2k Pro 690 Team 350 Enterprise 60
View data
FromToValueShare
OrganicFree82035.2%
OrganicPro31013.3%
OrganicTeam903.9%
Paid adsFree41017.6%
Paid adsPro1606.9%
ReferralPro2209.4%
ReferralTeam1406.0%
PartnersTeam1205.2%
PartnersEnterprise602.6%

Uso

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

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

<SankeyChart
  flows={[
    { source: 'Organic', target: 'Free', value: 820 },
    { source: 'Organic', target: 'Pro', value: 310 },
    { source: 'Referral', target: 'Pro', value: 220 },
  ]}
/>
--- import { SankeyChart } from 'astro-component-kit/charts'; --- <SankeyChart flows={[ { source: 'Organic', target: 'Free', value: 820 }, { source: 'Organic', target: 'Pro', value: 310 }, { source: 'Referral', target: 'Pro', value: 220 }, ]} />

Instalación Manual

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

astro
---
/**
 * SankeyChart — Two-column flow diagram: sources on the left, targets on the
 * right, ribbons whose thickness encodes the flow volume.
 *
 * @param {Array<{source: string, target: string, value: number}>} flows - Links between nodes.
 * @param {string} suffix - Value suffix.
 * @param {number} height - SVG height in viewBox units (width is 640).
 * @param {string[]} colors - Optional per-source colors.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
import ChartFrame from './ChartFrame.astro';
import { colorAt, formatValue, sum } from './chart-utils';

interface Props {
  title?: string;
  subtitle?: string;
  flows?: Array<{ source: string; target: string; value: number }>;
  prefix?: string;
  suffix?: string;
  height?: number;
  colors?: string[];
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

const {
  title = 'Where signups come from',
  subtitle = 'Acquisition channel → plan chosen',
  flows = [
    { source: 'Organic', target: 'Free', value: 820 },
    { source: 'Organic', target: 'Pro', value: 310 },
    { source: 'Organic', target: 'Team', value: 90 },
    { source: 'Paid ads', target: 'Free', value: 410 },
    { source: 'Paid ads', target: 'Pro', value: 160 },
    { source: 'Referral', target: 'Pro', value: 220 },
    { source: 'Referral', target: 'Team', value: 140 },
    { source: 'Partners', target: 'Team', value: 120 },
    { source: 'Partners', target: 'Enterprise', value: 60 },
  ],
  prefix = '',
  suffix = '',
  height = 320,
  colors,
  variant = 'glass',
  size = 'md',
  className = '',
} = Astro.props;

const W = 640;
const H = height;
const nodeW = 12;
const gap = 14;
const xL = 110;
const xR = W - 110 - nodeW;
const sources = [...new Set(flows.map((f) => f.source))];
const targets = [...new Set(flows.map((f) => f.target))];
const total = sum(flows.map((f) => f.value)) || 1;
const scaleFor = (count: number) => (H - gap * (count - 1)) / total;
const k = Math.min(scaleFor(sources.length), scaleFor(targets.length));

const layout = (names: string[], key: 'source' | 'target') => {
  const used = names.reduce((a, nm) => a + sum(flows.filter((f) => f[key] === nm).map((f) => f.value)) * k, 0) + gap * (names.length - 1);
  let y = (H - used) / 2;
  return Object.fromEntries(names.map((nm) => {
    const v = sum(flows.filter((f) => f[key] === nm).map((f) => f.value));
    const node = { y, h: v * k, v, offset: 0 };
    y += v * k + gap;
    return [nm, node];
  }));
};
const L = layout(sources, 'source');
const R = layout(targets, 'target');
const srcColor = Object.fromEntries(sources.map((s, i) => [s, colorAt(i, colors)]));

const links = flows.map((f) => {
  const s = L[f.source];
  const t = R[f.target];
  const h = f.value * k;
  const y0 = s.y + s.offset + h / 2;
  const y1 = t.y + t.offset + h / 2;
  s.offset += h;
  t.offset += h;
  const x0 = xL + nodeW;
  const x1 = xR;
  const mx = (x0 + x1) / 2;
  return { ...f, h, d: `M${x0},${y0} C${mx},${y0} ${mx},${y1} ${x1},${y1}`, color: srcColor[f.source] };
});
const fmt = (v: number) => formatValue(v, prefix, suffix);
---

<ChartFrame
  title={title}
  subtitle={subtitle}
  variant={variant}
  size={size}
  className={className}
  table={{ headers: ['From', 'To', 'Value', 'Share'], rows: flows.map((f) => [f.source, f.target, fmt(f.value), `${((f.value / total) * 100).toFixed(1)}%`]) }}
>
  <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: flow from ${sources.length} sources to ${targets.length} targets`}>
    {links.map((l) => (
      <path class="cf-mark cf-dim sk__link" d={l.d} fill="none" stroke={l.color} stroke-opacity="0.38" stroke-width={Math.max(1, l.h)}
        tabindex="0" role="img" aria-label={`${l.source} to ${l.target}: ${fmt(l.value)}`}
        data-tip-title={`${l.source} → ${l.target}`} data-tip={`${l.color}::${fmt(l.value)} · ${((l.value / total) * 100).toFixed(1)}%`} />
    ))}
    {sources.map((s) => (
      <g>
        <rect x={xL} y={L[s].y} width={nodeW} height={L[s].h} rx="3" fill={srcColor[s]} />
        <text class="cf-label cf-label--strong" x={xL - 8} y={L[s].y + L[s].h / 2} text-anchor="end">{s}</text>
        <text class="cf-label" x={xL - 8} y={L[s].y + L[s].h / 2 + 14} text-anchor="end">{fmt(L[s].v)}</text>
      </g>
    ))}
    {targets.map((t) => (
      <g>
        <rect x={xR} y={R[t].y} width={nodeW} height={R[t].h} rx="3" fill="var(--c-text-2, #a3a3a3)" />
        <text class="cf-label cf-label--strong" x={xR + nodeW + 8} y={R[t].y + R[t].h / 2}>{t}</text>
        <text class="cf-label" x={xR + nodeW + 8} y={R[t].y + R[t].h / 2 + 14}>{fmt(R[t].v)}</text>
      </g>
    ))}
  </svg>
</ChartFrame>

<style>
  .sk__link { transition: stroke-opacity 0.2s ease; }
  .sk__link:hover, .sk__link:focus-visible { stroke-opacity: 0.7; }
</style>
--- /** * SankeyChart — Two-column flow diagram: sources on the left, targets on the * right, ribbons whose thickness encodes the flow volume. * * @param {Array<{source: string, target: string, value: number}>} flows - Links between nodes. * @param {string} suffix - Value suffix. * @param {number} height - SVG height in viewBox units (width is 640). * @param {string[]} colors - Optional per-source colors. * @param {string} variant - Card style (glass|solid|ghost). * @param {string} size - Density (sm|md|lg). */ import ChartFrame from './ChartFrame.astro'; import { colorAt, formatValue, sum } from './chart-utils'; interface Props { title?: string; subtitle?: string; flows?: Array<{ source: string; target: string; value: number }>; prefix?: string; suffix?: string; height?: number; colors?: string[]; variant?: 'glass' | 'solid' | 'ghost'; size?: 'sm' | 'md' | 'lg'; className?: string; } const { title = 'Where signups come from', subtitle = 'Acquisition channel → plan chosen', flows = [ { source: 'Organic', target: 'Free', value: 820 }, { source: 'Organic', target: 'Pro', value: 310 }, { source: 'Organic', target: 'Team', value: 90 }, { source: 'Paid ads', target: 'Free', value: 410 }, { source: 'Paid ads', target: 'Pro', value: 160 }, { source: 'Referral', target: 'Pro', value: 220 }, { source: 'Referral', target: 'Team', value: 140 }, { source: 'Partners', target: 'Team', value: 120 }, { source: 'Partners', target: 'Enterprise', value: 60 }, ], prefix = '', suffix = '', height = 320, colors, variant = 'glass', size = 'md', className = '', } = Astro.props; const W = 640; const H = height; const nodeW = 12; const gap = 14; const xL = 110; const xR = W - 110 - nodeW; const sources = [...new Set(flows.map((f) => f.source))]; const targets = [...new Set(flows.map((f) => f.target))]; const total = sum(flows.map((f) => f.value)) || 1; const scaleFor = (count: number) => (H - gap * (count - 1)) / total; const k = Math.min(scaleFor(sources.length), scaleFor(targets.length)); const layout = (names: string[], key: 'source' | 'target') => { const used = names.reduce((a, nm) => a + sum(flows.filter((f) => f[key] === nm).map((f) => f.value)) * k, 0) + gap * (names.length - 1); let y = (H - used) / 2; return Object.fromEntries(names.map((nm) => { const v = sum(flows.filter((f) => f[key] === nm).map((f) => f.value)); const node = { y, h: v * k, v, offset: 0 }; y += v * k + gap; return [nm, node]; })); }; const L = layout(sources, 'source'); const R = layout(targets, 'target'); const srcColor = Object.fromEntries(sources.map((s, i) => [s, colorAt(i, colors)])); const links = flows.map((f) => { const s = L[f.source]; const t = R[f.target]; const h = f.value * k; const y0 = s.y + s.offset + h / 2; const y1 = t.y + t.offset + h / 2; s.offset += h; t.offset += h; const x0 = xL + nodeW; const x1 = xR; const mx = (x0 + x1) / 2; return { ...f, h, d: `M${x0},${y0} C${mx},${y0} ${mx},${y1} ${x1},${y1}`, color: srcColor[f.source] }; }); const fmt = (v: number) => formatValue(v, prefix, suffix); --- <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className} table={{ headers: ['From', 'To', 'Value', 'Share'], rows: flows.map((f) => [f.source, f.target, fmt(f.value), `${((f.value / total) * 100).toFixed(1)}%`]) }} > <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: flow from ${sources.length} sources to ${targets.length} targets`}> {links.map((l) => ( <path class="cf-mark cf-dim sk__link" d={l.d} fill="none" stroke={l.color} stroke-opacity="0.38" stroke-width={Math.max(1, l.h)} tabindex="0" role="img" aria-label={`${l.source} to ${l.target}: ${fmt(l.value)}`} data-tip-title={`${l.source} → ${l.target}`} data-tip={`${l.color}::${fmt(l.value)} · ${((l.value / total) * 100).toFixed(1)}%`} /> ))} {sources.map((s) => ( <g> <rect x={xL} y={L[s].y} width={nodeW} height={L[s].h} rx="3" fill={srcColor[s]} /> <text class="cf-label cf-label--strong" x={xL - 8} y={L[s].y + L[s].h / 2} text-anchor="end">{s}</text> <text class="cf-label" x={xL - 8} y={L[s].y + L[s].h / 2 + 14} text-anchor="end">{fmt(L[s].v)}</text> </g> ))} {targets.map((t) => ( <g> <rect x={xR} y={R[t].y} width={nodeW} height={R[t].h} rx="3" fill="var(--c-text-2, #a3a3a3)" /> <text class="cf-label cf-label--strong" x={xR + nodeW + 8} y={R[t].y + R[t].h / 2}>{t}</text> <text class="cf-label" x={xR + nodeW + 8} y={R[t].y + R[t].h / 2 + 14}>{fmt(R[t].v)}</text> </g> ))} </svg> </ChartFrame> <style> .sk__link { transition: stroke-opacity 0.2s ease; } .sk__link:hover, .sk__link:focus-visible { stroke-opacity: 0.7; } </style>

Información Rápida

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