1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Gráficos y Estadísticaschartsvgzero-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

Organic1.2kPaid ads570Referral360Partners180Free1.2kPro690Team350Enterprise60
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 componente React:

tsx
import { SankeyChart } from 'react-component-kit-ui/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/SankeyChart.tsx + SankeyChart.css:

SankeyChart.tsx
'use client';

import ChartFrame from './ChartFrame';
import { colorAt, formatValue, sum } from './chart-utils';
import './SankeyChart.css';

/**
 * 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).
 */
export interface SankeyChartProps {
  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;
}

export function SankeyChart({
  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 = '',
}: SankeyChartProps) {
  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);
  return (
    <>
      <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`} data-rk-efc23b="">
          {links.map((l, __i: number) => (
            <path key={__i} className="cf-mark cf-dim sk__link" d={l.d} fill="none" stroke={l.color} strokeOpacity="0.38" strokeWidth={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)}%`} data-rk-efc23b="" />
          ))}
          {sources.map((s, __i: number) => (
            <g key={__i} data-rk-efc23b="">
              <rect x={xL} y={L[s].y} width={nodeW} height={L[s].h} rx="3" fill={srcColor[s]} data-rk-efc23b="" />
              <text className="cf-label cf-label--strong" x={xL - 8} y={L[s].y + L[s].h / 2} textAnchor="end" data-rk-efc23b="">{s}</text>
              <text className="cf-label" x={xL - 8} y={L[s].y + L[s].h / 2 + 14} textAnchor="end" data-rk-efc23b="">{fmt(L[s].v)}</text>
            </g>
          ))}
          {targets.map((t, __i: number) => (
            <g key={__i} data-rk-efc23b="">
              <rect x={xR} y={R[t].y} width={nodeW} height={R[t].h} rx="3" fill="var(--c-text-2, #a3a3a3)" data-rk-efc23b="" />
              <text className="cf-label cf-label--strong" x={xR + nodeW + 8} y={R[t].y + R[t].h / 2} data-rk-efc23b="">{t}</text>
              <text className="cf-label" x={xR + nodeW + 8} y={R[t].y + R[t].h / 2 + 14} data-rk-efc23b="">{fmt(R[t].v)}</text>
            </g>
          ))}
        </svg>
      </ChartFrame>
    </>
  );
}

export default SankeyChart;
SankeyChart.css
/* SankeyChart — ported from astro-component-kit (charts/SankeyChart.astro) */

.sk__link:where([data-rk-efc23b]) { transition: stroke-opacity 0.2s ease; }
.sk__link:hover:where([data-rk-efc23b]), .sk__link:focus-visible:where([data-rk-efc23b]) { stroke-opacity: 0.7; }

Información Rápida

Categoría
Gráficos y Estadísticas
Nombre de archivo
SankeyChart.tsx
Dependencias
Ninguna — React + CSS puro
Etiquetas
chartsvgzero-dependencysankeyflowattributionjourney