- Introducción
- Conceptos Fundamentales
- Estructura del Componente
- Tipos y Interfaces
- Funcionamiento Interno
- Atributos SVG Explicados
- Validación y Manejo de Errores
- Optimización y Performance
- Accesibilidad
- Casos de Uso y Ejemplos
- Guía para Crear un PieChart desde Cero
El componente PieChart es una implementación de un gráfico de tipo donut/pie circular utilizando SVG, React y TypeScript. A diferencia de los gráficos tradicionales que usan elementos <path> complejos, este componente utiliza la técnica de stroke-dasharray y stroke-dashoffset en círculos SVG para crear segmentos circulares.
- Simplicidad: Evita cálculos trigonométricos complejos para paths SVG
- Performance: Los círculos son más rápidos de renderizar que paths complejos
- Mantenibilidad: El código es más legible y fácil de modificar
- Flexibilidad: Permite transiciones suaves y animaciones CSS
Un círculo SVG tiene un perímetro calculado por la fórmula: 2 * π * radio
const circumference = 2 * Math.PI * radius;El atributo stroke-dasharray define el patrón de guiones y espacios en el trazo de un círculo:
<circle stroke-dasharray="50 300" />50: longitud del guion (segmento visible)300: longitud del espacio (segmento invisible)
El atributo stroke-dashoffset desplaza el punto de inicio del patrón de guiones:
<circle stroke-dashoffset="-25" />Un valor negativo mueve el patrón en sentido horario.
svg (contenedor principal)
└── g (grupo de transformación)
└── circle (segmento individual)
└── circle (segmento individual)
└── ...
- Validación: Se filtran y validan los segmentos de entrada
- Cálculos: Se calculan radio, circunferencia y valores de dash
- Renderizado: Se generan los círculos con los atributos calculados
type ChartSegment = {
value: number; // Porcentaje del segmento (0-100)
color: string; // Color del segmento (formato CSS)
label?: string; // Etiqueta opcional para accesibilidad
};Por qué estos atributos:
value: Representa el porcentaje del total que ocupa el segmentocolor: Color visual del segmento, acepta cualquier formato CSS válidolabel?: Opcional, usado para<title>y accesibilidad
type CircleChartProps = {
size?: number; // Tamaño del SVG (cuadrado)
strokeWidth?: number; // Grosor del anillo
segments: ChartSegment[]; // Array de segmentos
className?: string; // Clases CSS adicionales
"aria-label"?: string; // Etiqueta ARIA personalizada
};Valores por defecto y su razón:
size = 200: Tamaño moderado que funciona bien en la mayoría de casosstrokeWidth = 20: Grosor visible pero no excesivo para el tamaño por defecto
const radius = (size - strokeWidth) / 2;Explicación: El radio debe ser menor que la mitad del tamaño para dejar espacio para el grosor del trazo.
const circumference = 2 * Math.PI * radius;Explicación: Fórmula matemática del perímetro de un círculo.
const dashArray = (segment.value / 100) * circumference;Explicación: Convertimos el porcentaje a longitud absoluta basada en la circunferencia total.
const offset = chartData
.slice(0, index)
.reduce((sum, prev) => sum + prev.dashArray, 0);Explicación: Cada segmento debe empezar donde termina el anterior, por lo que acumulamos los offsets anteriores.
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
className={className}
role="img"
aria-label={ariaLabel || `Chart with ${validSegments.length} segments`}
>| Atributo | Propósito | Efecto Visual |
|---|---|---|
width/height |
Dimensiones del elemento | Tamaño físico en píxeles |
viewBox |
Sistema de coordenadas interno | Escala y recorte del contenido |
role="img" |
Accesibilidad | Indica que es una imagen |
aria-label |
Accesibilidad | Descripción para lectores de pantalla |
<g transform={`rotate(-90 ${size / 2} ${size / 2})`}>| Atributo | Propósito | Efecto Visual |
|---|---|---|
transform="rotate(-90 x y)" |
Rotación del contenido | Inicia el gráfico desde arriba (12 en punto) en lugar de la derecha (3 en punto) |
¿Por qué -90 grados?: Los SVG inician el ángulo 0 desde la derecha (3 en punto). Para que el gráfico inicie desde arriba (12 en punto), rotamos -90 grados.
<circle
key={`${segment.color}-${segment.value}-${index}`}
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={segment.color}
strokeWidth={strokeWidth}
strokeDasharray={`${segment.dashArray} ${segment.circumference}`}
strokeDashoffset={-offset}
strokeLinecap="butt"
>| Atributo | Propósito | Efecto Visual |
|---|---|---|
key |
Identificador único de React | Evita re-renders innecesarios |
cx/cy |
Centro del círculo | Posiciona el círculo en el centro del SVG |
r |
Radio | Define el tamaño del círculo |
fill="none" |
Relleno | Sin relleno, solo el borde visible |
stroke |
Color del borde | Color del segmento |
strokeWidth |
Grosor del borde | Ancho del anillo |
strokeDasharray |
Patrón de guiones | Define longitud del segmento visible |
strokeDashoffset |
Desplazamiento del patrón | Posiciona el segmento en el lugar correcto |
strokeLinecap="butt" |
Estilo de terminación | Bordes rectos (no redondeados) |
¿Por qué strokeLinecap="butt"?: Para que los segmentos se unan perfectamente sin solapamientos ni espacios.
const validateSegments = (segments: ChartSegment[]) => {
const total = segments.reduce((sum, seg) => sum + seg.value, 0);
if (total > 100) {
console.warn(`Segment values sum to ${total}%, exceeding 100%`);
}
return segments.filter((seg) => seg.value > 0);
};Validaciones implementadas:
- Suma total: Advierte si los valores suman más del 100%
- Valores negativos: Filtra segmentos con valor <= 0
- Valores cero: Elimina segmentos que no aportarían visualmente
Por qué estas validaciones:
- Previene renderizados incorrectos
- Proporciona feedback útil al desarrollador
- Mantiene la integridad visual del gráfico
const validSegments = useMemo(() => validateSegments(segments), [segments]);
const chartData = useMemo(() => {
// cálculos complejos
}, [validSegments, size, strokeWidth]);Beneficios:
- Evita recálculos en cada render
- Solo se recalcula cuando las dependencias cambian
- Mejora el rendimiento en aplicaciones con renders frecuentes
key={`${segment.color}-${segment.value}-${index}`}Ventajas:
- Identificación única incluso si los segmentos se reordenan
- Evita problemas de reconciliación de React
- Previene re-renders innecesarios
role="img"
aria-label={ariaLabel || `Chart with ${validSegments.length} segments`}Propósito:
role="img": Indica a lectores de pantalla que es una imagenaria-label: Proporciona una descripción textual del contenido
{segment.label && (
<title>{segment.label}: {segment.value}%</title>
)}Beneficios:
- Información detallada al pasar el mouse
- Descripción adicional para lectores de pantalla
- Contexto sobre cada segmento individual
const segments = [
{ value: 30, color: "#3b82f6", label: "Ventas" },
{ value: 45, color: "#10b981", label: "Marketing" },
{ value: 25, color: "#f59e0b", label: "Soporte" },
];
<CircleChart segments={segments} /><CircleChart
size={300}
strokeWidth={30}
segments={segments}
className="my-chart"
aria-label="Distribución de presupuesto 2024"
/>// Si segments está vacío, muestra un círculo gris
<CircleChart segments={[]} />npx create-react-app my-pie-chart --template typescript
cd my-pie-chart// types.ts
export type ChartSegment = {
value: number;
color: string;
label?: string;
};
export type CircleChartProps = {
size?: number;
strokeWidth?: number;
segments: ChartSegment[];
className?: string;
"aria-label"?: string;
};// CircleChart.tsx
import { useMemo } from 'react';
import { CircleChartProps } from './types';
export const CircleChart = ({
size = 200,
strokeWidth = 20,
segments,
}: CircleChartProps) => {
// Paso 4: Implementaremos la lógica aquí
return <div>Componente en construcción</div>;
};const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const chartData = segments.map((segment) => ({
...segment,
dashArray: (segment.value / 100) * circumference,
circumference,
radius,
}));return (
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<g transform={`rotate(-90 ${size / 2} ${size / 2})`}>
{chartData.map((segment, index) => {
const offset = chartData
.slice(0, index)
.reduce((sum, prev) => sum + prev.dashArray, 0);
return (
<circle
key={index}
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={segment.color}
strokeWidth={strokeWidth}
strokeDasharray={`${segment.dashArray} ${segment.circumference}`}
strokeDashoffset={-offset}
/>
);
})}
</g>
</svg>
);const validateSegments = (segments: ChartSegment[]) => {
const total = segments.reduce((sum, seg) => sum + seg.value, 0);
if (total > 100) {
console.warn(`Values sum to ${total}%, exceeding 100%`);
}
return segments.filter((seg) => seg.value > 0);
};
const validSegments = useMemo(() => validateSegments(segments), [segments]);const chartData = useMemo(() => {
return validSegments.map((segment) => ({
...segment,
dashArray: (segment.value / 100) * circumference,
circumference,
radius,
}));
}, [validSegments, size, strokeWidth]);return (
<svg
role="img"
aria-label={`Chart with ${validSegments.length} segments`}
// ... otros atributos
>
{/* ... contenido */}
</svg>
);if (validSegments.length === 0) {
return (
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="#e5e7eb"
strokeWidth={strokeWidth}
/>
</svg>
);
}// App.tsx
const segments = [
{ value: 30, color: "#3b82f6" },
{ value: 45, color: "#10b981" },
{ value: 25, color: "#f59e0b" },
];
function App() {
return (
<div>
<h1>Mi PieChart</h1>
<CircleChart segments={segments} />
</div>
);
}- SVG Circle Technique: Usar
stroke-dasharrayystroke-dashoffsetpara crear segmentos - Mathematical Foundation: Basado en circunferencia y porcentajes
- React Patterns: useMemo, keys únicas, validación de props
- Accessibility: ARIA labels, roles, elementos descriptivos
- Error Handling: Validación de datos y edge cases
- Performance: Memoización y renderizado eficiente
Con esta guía completa, puedes crear tu propio componente PieChart funcional, accesible y optimizado desde cero.