Skip to content

Instantly share code, notes, and snippets.

@victorcolina22
Last active January 3, 2026 05:27
Show Gist options
  • Select an option

  • Save victorcolina22/008d7b156dfce854c956a2fb16b1dbfa to your computer and use it in GitHub Desktop.

Select an option

Save victorcolina22/008d7b156dfce854c956a2fb16b1dbfa to your computer and use it in GitHub Desktop.
Crea tu propio Pie Chart con React + TypeScript + TailwindCSS

Aprende a crear tu propio Pie Chart

Tabla de Contenidos

  1. Introducción
  2. Conceptos Fundamentales
  3. Estructura del Componente
  4. Tipos y Interfaces
  5. Funcionamiento Interno
  6. Atributos SVG Explicados
  7. Validación y Manejo de Errores
  8. Optimización y Performance
  9. Accesibilidad
  10. Casos de Uso y Ejemplos
  11. Guía para Crear un PieChart desde Cero

Introducción

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.

¿Por qué este enfoque?

  • 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

Conceptos Fundamentales

1. Círculos SVG y Stroke

Un círculo SVG tiene un perímetro calculado por la fórmula: 2 * π * radio

const circumference = 2 * Math.PI * radius;

2. Stroke Dasharray

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)

3. Stroke Dashoffset

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.


Estructura del Componente

Jerarquía de Elementos

svg (contenedor principal)
└── g (grupo de transformación)
    └── circle (segmento individual)
    └── circle (segmento individual)
    └── ...

Flujo de Renderizado

  1. Validación: Se filtran y validan los segmentos de entrada
  2. Cálculos: Se calculan radio, circunferencia y valores de dash
  3. Renderizado: Se generan los círculos con los atributos calculados

Tipos y Interfaces

ChartSegment

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 segmento
  • color: Color visual del segmento, acepta cualquier formato CSS válido
  • label?: Opcional, usado para <title> y accesibilidad

CircleChartProps

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 casos
  • strokeWidth = 20: Grosor visible pero no excesivo para el tamaño por defecto

Funcionamiento Interno

1. Cálculo del Radio

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.

2. Cálculo de la Circunferencia

const circumference = 2 * Math.PI * radius;

Explicación: Fórmula matemática del perímetro de un círculo.

3. Cálculo del Dash Array

const dashArray = (segment.value / 100) * circumference;

Explicación: Convertimos el porcentaje a longitud absoluta basada en la circunferencia total.

4. Cálculo del Offset

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.


Atributos SVG Explicados

Atributos del <svg>

<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

Atributos del <g> (Grupo)

<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.

Atributos del <circle>

<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.


Validación y Manejo de Errores

Función validateSegments

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:

  1. Suma total: Advierte si los valores suman más del 100%
  2. Valores negativos: Filtra segmentos con valor <= 0
  3. 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

Optimización y Performance

useMemo para Cálculos Pesados

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

Keys Estables y Únicas

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

Accesibilidad

Atributos ARIA

role="img"
aria-label={ariaLabel || `Chart with ${validSegments.length} segments`}

Propósito:

  • role="img": Indica a lectores de pantalla que es una imagen
  • aria-label: Proporciona una descripción textual del contenido

Elementos <title>

{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

Casos de Uso y Ejemplos

Ejemplo Básico

const segments = [
  { value: 30, color: "#3b82f6", label: "Ventas" },
  { value: 45, color: "#10b981", label: "Marketing" },
  { value: 25, color: "#f59e0b", label: "Soporte" },
];

<CircleChart segments={segments} />

Ejemplo Personalizado

<CircleChart
  size={300}
  strokeWidth={30}
  segments={segments}
  className="my-chart"
  aria-label="Distribución de presupuesto 2024"
/>

Manejo de Datos Vacíos

// Si segments está vacío, muestra un círculo gris
<CircleChart segments={[]} />

Guía para Crear un PieChart desde Cero

Paso 1: Configurar el Proyecto

npx create-react-app my-pie-chart --template typescript
cd my-pie-chart

Paso 2: Definir los Tipos

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

Paso 3: Crear el Componente Básico

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

Paso 4: Implementar la Lógica de Cálculos

const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;

const chartData = segments.map((segment) => ({
  ...segment,
  dashArray: (segment.value / 100) * circumference,
  circumference,
  radius,
}));

Paso 5: Crear la Estructura SVG

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

Paso 6: Añadir Validación

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

Paso 7: Optimizar con useMemo

const chartData = useMemo(() => {
  return validSegments.map((segment) => ({
    ...segment,
    dashArray: (segment.value / 100) * circumference,
    circumference,
    radius,
  }));
}, [validSegments, size, strokeWidth]);

Paso 8: Añadir Accesibilidad

return (
  <svg
    role="img"
    aria-label={`Chart with ${validSegments.length} segments`}
    // ... otros atributos
  >
    {/* ... contenido */}
  </svg>
);

Paso 9: Manejar Edge Cases

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

Paso 10: Probar y Refinar

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

Resumen de Conceptos Clave

  1. SVG Circle Technique: Usar stroke-dasharray y stroke-dashoffset para crear segmentos
  2. Mathematical Foundation: Basado en circunferencia y porcentajes
  3. React Patterns: useMemo, keys únicas, validación de props
  4. Accessibility: ARIA labels, roles, elementos descriptivos
  5. Error Handling: Validación de datos y edge cases
  6. Performance: Memoización y renderizado eficiente

Con esta guía completa, puedes crear tu propio componente PieChart funcional, accesible y optimizado desde cero.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment