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
Acquisition channel → plan chosen
View data
| From | To | Value | Share |
|---|---|---|---|
| Organic | Free | 820 | 35.2% |
| Organic | Pro | 310 | 13.3% |
| Organic | Team | 90 | 3.9% |
| Paid ads | Free | 410 | 17.6% |
| Paid ads | Pro | 160 | 6.9% |
| Referral | Pro | 220 | 9.4% |
| Referral | Team | 140 | 6.0% |
| Partners | Team | 120 | 5.2% |
| Partners | Enterprise | 60 | 2.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