Categoría · Widgets Nivel de Dificultad · Avanzado Publicado el · 15 de enero de 2024

Widgets de Dashboard con Gráficos y Visualización de Datos

Widgets interactivos de dashboard con varios tipos de gráficos, actualizaciones de datos en tiempo real y diseño responsivo para aplicaciones web modernas

#dashboard #widgets #charts #data-visualization #interactivo #responsive #real-time

Diseño Responsivo

Soporte para Modo Oscuro

No

líneas

193

Compatibilidad del Navegador

No

Vista Previa en Vivo

Interactúa con el componente sin salir de la página.

600px

Widgets de Dashboard con Gráficos y Visualización de Datos

Una colección completa de widgets interactivos de dashboard con varios tipos de gráficos, actualizaciones de datos en tiempo real y patrones de diseño modernos para visualización de datos.

Características

  • Múltiples Tipos de Gráficos: Gráficos de línea, barras, circular, dona, área y medidor
  • Actualizaciones en Tiempo Real: Transmisión de datos en vivo y actualización automática
  • Elementos Interactivos: Efectos hover, tooltips y leyendas clicables
  • Diseño Responsivo: Se adapta a diferentes tamaños de pantalla y orientaciones
  • Temas Personalizables: Esquemas de colores claro, oscuro y personalizados
  • Exportación de Datos: Exportar gráficos como imágenes o datos como CSV/JSON
  • Efectos de Animación: Transiciones suaves y animaciones de carga
  • Accesibilidad: Soporte para lectores de pantalla y navegación por teclado
<div class="dashboard-container">
  <!-- Encabezado del Dashboard -->
  <div class="dashboard-header">
    <h1 class="dashboard-title">Dashboard de Analíticas</h1>
    <div class="dashboard-controls">
      <button class="refresh-btn" data-action="refresh">
        <svg class="icon" viewBox="0 0 24 24">
          <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-8 3.58-8 8s3.58 8 8 8c3.74 0 6.95-2.57 7.9-6h-2.02c-.85 2.37-3.13 4-5.88 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
        </svg>
        Actualizar
      </button>
      <select class="time-range-select">
        <option value="1h">Última Hora</option>
        <option value="24h" selected>Últimas 24 Horas</option>
        <option value="7d">Últimos 7 Días</option>
        <option value="30d">Últimos 30 Días</option>
      </select>
    </div>
  </div>

  <!-- Tarjetas KPI -->
  <div class="kpi-grid">
    <div class="kpi-card" data-kpi="revenue">
      <div class="kpi-icon">
        <svg viewBox="0 0 24 24">
          <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"/>
        </svg>
      </div>
      <div class="kpi-content">
        <div class="kpi-value">$24,567</div>
        <div class="kpi-label">Ingresos Totales</div>
        <div class="kpi-change positive">+12.5%</div>
      </div>
    </div>

    <div class="kpi-card" data-kpi="users">
      <div class="kpi-icon">
        <svg viewBox="0 0 24 24">
          <path d="M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A1.5 1.5 0 0 0 18.54 8H17c-.8 0-1.54.37-2.01.99l-2.98 3.67a.5.5 0 0 0 .39.84h2.6v6h5zm-7.5-10.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5S11 9.17 11 10.5s.67 1.5 1.5 1.5zM5.5 6c1.11 0 2-.89 2-2s-.89-2-2-2-2 .89-2 2 .89 2 2 2zm2.5 16v-7H9.5l.68-3.19c.08-.37.34-.68.71-.8L14 9.5v3.5h-1.5v7h-4.5z"/>
        </svg>
      </div>
      <div class="kpi-content">
        <div class="kpi-value">1,234</div>
        <div class="kpi-label">Usuarios Activos</div>
        <div class="kpi-change positive">+8.2%</div>
      </div>
    </div>

    <div class="kpi-card" data-kpi="orders">
      <div class="kpi-icon">
        <svg viewBox="0 0 24 24">
          <path d="M7 4V2C7 1.45 7.45 1 8 1h8c.55 0 1 .45 1 1v2h4c.55 0 1 .45 1 1s-.45 1-1 1h-1v11c0 1.1-.9 2-2 2H6c-1.1 0-2-.9-2-2V6H3c-.55 0-1-.45-1-1s.45-1 1-1h4zM9 3v1h6V3H9zm7 15V8H8v10h8z"/>
        </svg>
      </div>
      <div class="kpi-content">
        <div class="kpi-value">456</div>
        <div class="kpi-label">Pedidos</div>
        <div class="kpi-change negative">-2.1%</div>
      </div>
    </div>

    <div class="kpi-card" data-kpi="conversion">
      <div class="kpi-icon">
        <svg viewBox="0 0 24 24">
          <path d="M16 6l2.29 2.29-4.88 4.88-4-4L2 16.59 3.41 18l6-6 4 4 6.3-6.29L22 12V6z"/>
        </svg>
      </div>
      <div class="kpi-content">
        <div class="kpi-value">3.2%</div>
        <div class="kpi-label">Tasa de Conversión</div>
        <div class="kpi-change positive">+0.5%</div>
      </div>
    </div>
  </div>

  <!-- Cuadrícula de Gráficos -->
  <div class="charts-grid">
    <!-- Widget de Gráfico de Línea -->
    <div class="chart-widget" data-chart="line">
      <div class="widget-header">
        <h3 class="widget-title">Tendencia de Ingresos</h3>
        <div class="widget-controls">
          <button class="widget-menu-btn">
            <svg viewBox="0 0 24 24">
              <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
            </svg>
          </button>
        </div>
      </div>
      <div class="chart-container">
        <canvas id="lineChart" width="400" height="200"></canvas>
      </div>
    </div>

    <!-- Widget de Gráfico de Barras -->
    <div class="chart-widget" data-chart="bar">
      <div class="widget-header">
        <h3 class="widget-title">Ventas por Categoría</h3>
        <div class="widget-controls">
          <button class="widget-menu-btn">
            <svg viewBox="0 0 24 24">
              <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
            </svg>
          </button>
        </div>
      </div>
      <div class="chart-container">
        <canvas id="barChart" width="400" height="200"></canvas>
      </div>
    </div>

    <!-- Widget de Gráfico Circular -->
    <div class="chart-widget" data-chart="pie">
      <div class="widget-header">
        <h3 class="widget-title">Fuentes de Tráfico</h3>
        <div class="widget-controls">
          <button class="widget-menu-btn">
            <svg viewBox="0 0 24 24">
              <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
            </svg>
          </button>
        </div>
      </div>
      <div class="chart-container">
        <canvas id="pieChart" width="400" height="200"></canvas>
      </div>
    </div>

    <!-- Widget de Gráfico de Medidor -->
    <div class="chart-widget" data-chart="gauge">
      <div class="widget-header">
        <h3 class="widget-title">Puntuación de Rendimiento</h3>
        <div class="widget-controls">
          <button class="widget-menu-btn">
            <svg viewBox="0 0 24 24">
              <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
            </svg>
          </button>
        </div>
      </div>
      <div class="chart-container">
        <canvas id="gaugeChart" width="400" height="200"></canvas>
      </div>
    </div>

    <!-- Widget de Tabla de Datos -->
    <div class="chart-widget table-widget" data-widget="table">
      <div class="widget-header">
        <h3 class="widget-title">Transacciones Recientes</h3>
        <div class="widget-controls">
          <button class="export-btn" data-export="csv">
            <svg viewBox="0 0 24 24">
              <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
            </svg>
            Exportar
          </button>
        </div>
      </div>
      <div class="table-container">
        <table class="data-table">
          <thead>
            <tr>
              <th>ID</th>
              <th>Cliente</th>
              <th>Cantidad</th>
              <th>Estado</th>
              <th>Fecha</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>#1234</td>
              <td>Juan Pérez</td>
              <td>$299.99</td>
              <td><span class="status success">Completado</span></td>
              <td>2024-01-15</td>
            </tr>
            <tr>
              <td>#1235</td>
              <td>María García</td>
              <td>$149.50</td>
              <td><span class="status pending">Pendiente</span></td>
              <td>2024-01-15</td>
            </tr>
            <tr>
              <td>#1236</td>
              <td>Carlos López</td>
              <td>$89.99</td>
              <td><span class="status failed">Fallido</span></td>
              <td>2024-01-14</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <!-- Widget de Actividad en Tiempo Real -->
    <div class="chart-widget activity-widget" data-widget="activity">
      <div class="widget-header">
        <h3 class="widget-title">Actividad en Vivo</h3>
        <div class="widget-controls">
          <div class="live-indicator">
            <span class="live-dot"></span>
            En Vivo
          </div>
        </div>
      </div>
      <div class="activity-container">
        <div class="activity-item">
          <div class="activity-icon">
            <svg viewBox="0 0 24 24">
              <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
            </svg>
          </div>
          <div class="activity-content">
            <div class="activity-text">Nuevo pedido recibido de Juan Pérez</div>
            <div class="activity-time">hace 2 minutos</div>
          </div>
        </div>
        <div class="activity-item">
          <div class="activity-icon">
            <svg viewBox="0 0 24 24">
              <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
            </svg>
          </div>
          <div class="activity-content">
            <div class="activity-text">Reseña de producto enviada</div>
            <div class="activity-time">hace 5 minutos</div>
          </div>
        </div>
        <div class="activity-item">
          <div class="activity-icon">
            <svg viewBox="0 0 24 24">
              <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm3.5 6L12 10.5 8.5 8 12 5.5 15.5 8zM12 13.5l3.5 2.5L12 18.5 8.5 16l3.5-2.5z"/>
            </svg>
          </div>
          <div class="activity-content">
            <div class="activity-text">Usuario registrado</div>
            <div class="activity-time">hace 8 minutos</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
/* Contenedor del Dashboard */
.dashboard-container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 2rem;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  min-height: 100vh;
}

/* Encabezado del Dashboard */
.dashboard-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
  padding: 1.5rem;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

.dashboard-title {
  font-size: 2rem;
  font-weight: 700;
  color: #1f2937;
  margin: 0;
}

.dashboard-controls {
  display: flex;
  gap: 1rem;
  align-items: center;
}

.refresh-btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 8px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
}

.refresh-btn:hover {
  background: #2563eb;
  transform: translateY(-1px);
}

.refresh-btn .icon {
  width: 16px;
  height: 16px;
  fill: currentColor;
}

.time-range-select {
  padding: 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  background: white;
  font-size: 0.875rem;
  cursor: pointer;
}

/* Cuadrícula KPI */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1.5rem;
  margin-bottom: 2rem;
}

.kpi-card {
  background: white;
  padding: 1.5rem;
  border-radius: 12px;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  display: flex;
  align-items: center;
  gap: 1rem;
  transition: all 0.3s ease;
  position: relative;
  overflow: hidden;
}

.kpi-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  background: linear-gradient(90deg, #3b82f6, #8b5cf6);
}

.kpi-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 0.1);
}

.kpi-icon {
  width: 48px;
  height: 48px;
  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.kpi-icon svg {
  width: 24px;
  height: 24px;
  fill: white;
}

.kpi-content {
  flex: 1;
}

.kpi-value {
  font-size: 1.875rem;
  font-weight: 700;
  color: #1f2937;
  line-height: 1;
}

.kpi-label {
  font-size: 0.875rem;
  color: #6b7280;
  margin: 0.25rem 0;
}

.kpi-change {
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  display: inline-block;
}

.kpi-change.positive {
  background: #dcfce7;
  color: #166534;
}

.kpi-change.negative {
  background: #fef2f2;
  color: #dc2626;
}

/* Cuadrícula de Gráficos */
.charts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
  gap: 1.5rem;
}

.chart-widget {
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  transition: all 0.3s ease;
}

.chart-widget:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 0.1);
}

.widget-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.5rem;
  border-bottom: 1px solid #f3f4f6;
}

.widget-title {
  font-size: 1.125rem;
  font-weight: 600;
  color: #1f2937;
  margin: 0;
}

.widget-controls {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.widget-menu-btn {
  width: 32px;
  height: 32px;
  border: none;
  background: #f9fafb;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.2s ease;
}

.widget-menu-btn:hover {
  background: #f3f4f6;
}

.widget-menu-btn svg {
  width: 16px;
  height: 16px;
  fill: #6b7280;
}

.chart-container {
  padding: 1.5rem;
  position: relative;
}

.chart-container canvas {
  max-width: 100%;
  height: auto;
}

/* Widget de Tabla */
.table-widget {
  grid-column: 1 / -1;
}

.table-container {
  overflow-x: auto;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
}

.data-table th,
.data-table td {
  padding: 1rem;
  text-align: left;
  border-bottom: 1px solid #f3f4f6;
}

.data-table th {
  background: #f9fafb;
  font-weight: 600;
  color: #374151;
  font-size: 0.875rem;
}

.data-table td {
  color: #6b7280;
}

.status {
  padding: 0.25rem 0.75rem;
  border-radius: 9999px;
  font-size: 0.75rem;
  font-weight: 500;
}

.status.success {
  background: #dcfce7;
  color: #166534;
}

.status.pending {
  background: #fef3c7;
  color: #92400e;
}

.status.failed {
  background: #fef2f2;
  color: #dc2626;
}

.export-btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  background: #f9fafb;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  font-size: 0.875rem;
  cursor: pointer;
  transition: all 0.2s ease;
}

.export-btn:hover {
  background: #f3f4f6;
}

.export-btn svg {
  width: 14px;
  height: 14px;
  fill: #6b7280;
}

/* Widget de Actividad */
.activity-widget {
  grid-column: span 1;
}

.live-indicator {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  color: #059669;
  font-weight: 500;
}

.live-dot {
  width: 8px;
  height: 8px;
  background: #059669;
  border-radius: 50%;
  animation: pulse 2s infinite;
}

.activity-container {
  padding: 1.5rem;
}

.activity-item {
  display: flex;
  gap: 1rem;
  padding: 1rem 0;
  border-bottom: 1px solid #f3f4f6;
}

.activity-item:last-child {
  border-bottom: none;
}

.activity-icon {
  width: 40px;
  height: 40px;
  background: #f3f4f6;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.activity-icon svg {
  width: 20px;
  height: 20px;
  fill: #6b7280;
}

.activity-content {
  flex: 1;
}

.activity-text {
  font-size: 0.875rem;
  color: #374151;
  margin-bottom: 0.25rem;
}

.activity-time {
  font-size: 0.75rem;
  color: #9ca3af;
}

/* Diseño Responsivo */
@media (max-width: 768px) {
  .dashboard-container {
    padding: 1rem;
  }
  
  .dashboard-header {
    flex-direction: column;
    gap: 1rem;
    align-items: stretch;
  }
  
  .dashboard-controls {
    justify-content: center;
  }
  
  .kpi-grid {
    grid-template-columns: 1fr;
  }
  
  .charts-grid {
    grid-template-columns: 1fr;
  }
  
  .table-widget {
    grid-column: 1;
  }
}

/* Animaciones */
@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.chart-widget {
  animation: fadeInUp 0.6s ease-out;
}

.kpi-card {
  animation: fadeInUp 0.6s ease-out;
}

/* Soporte para Tema Oscuro */
@media (prefers-color-scheme: dark) {
  .dashboard-container {
    background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
  }
  
  .dashboard-header,
  .kpi-card,
  .chart-widget {
    background: #374151;
    color: #f9fafb;
  }
  
  .dashboard-title,
  .kpi-value,
  .widget-title {
    color: #f9fafb;
  }
  
  .kpi-label,
  .data-table td {
    color: #d1d5db;
  }
  
  .data-table th {
    background: #4b5563;
    color: #f9fafb;
  }
  
  .widget-menu-btn {
    background: #4b5563;
  }
  
  .widget-menu-btn:hover {
    background: #6b7280;
  }
}
// Clase Principal del Dashboard
class WidgetsDashboard {
  constructor(container, options = {}) {
    this.container = typeof container === 'string' ? document.querySelector(container) : container;
    this.options = {
      autoRefresh: true,
      refreshInterval: 30000, // 30 segundos
      enableAnimations: true,
      theme: 'auto', // 'light', 'dark', 'auto'
      locale: 'es-ES',
      ...options
    };
    
    this.charts = new Map();
    this.refreshTimer = null;
    this.isVisible = true;
    
    this.init();
  }
  
  init() {
    this.setupEventListeners();
    this.initializeCharts();
    this.startAutoRefresh();
    this.setupIntersectionObserver();
    this.setupAccessibility();
    
    // Disparar evento de inicialización
    this.dispatchEvent('dashboard:initialized', { dashboard: this });
  }
  
  setupEventListeners() {
    // Botón de actualización
    const refreshBtn = this.container.querySelector('.refresh-btn');
    if (refreshBtn) {
      refreshBtn.addEventListener('click', () => this.refreshData());
    }
    
    // Selector de rango de tiempo
    const timeRangeSelect = this.container.querySelector('.time-range-select');
    if (timeRangeSelect) {
      timeRangeSelect.addEventListener('change', (e) => {
        this.updateTimeRange(e.target.value);
      });
    }
    
    // Botones de exportación
    const exportBtns = this.container.querySelectorAll('.export-btn');
    exportBtns.forEach(btn => {
      btn.addEventListener('click', (e) => {
        const format = e.currentTarget.dataset.export || 'csv';
        this.exportData(format);
      });
    });
    
    // Menús de widgets
    const menuBtns = this.container.querySelectorAll('.widget-menu-btn');
    menuBtns.forEach(btn => {
      btn.addEventListener('click', (e) => {
        this.toggleWidgetMenu(e.currentTarget);
      });
    });
    
    // Eventos de visibilidad
    document.addEventListener('visibilitychange', () => {
      this.isVisible = !document.hidden;
      if (this.isVisible) {
        this.refreshData();
      }
    });
    
    // Eventos de redimensionamiento
    window.addEventListener('resize', this.debounce(() => {
      this.resizeCharts();
    }, 250));
  }
  
  initializeCharts() {
    // Inicializar gráfico de línea
    this.initLineChart();
    
    // Inicializar gráfico de barras
    this.initBarChart();
    
    // Inicializar gráfico circular
    this.initPieChart();
    
    // Inicializar gráfico de medidor
    this.initGaugeChart();
  }
  
  initLineChart() {
    const canvas = this.container.querySelector('#lineChart');
    if (!canvas) return;
    
    const ctx = canvas.getContext('2d');
    const chart = new Chart(ctx, {
      type: 'line',
      data: {
        labels: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun'],
        datasets: [{
          label: 'Ingresos',
          data: [12000, 19000, 15000, 25000, 22000, 30000],
          borderColor: '#3b82f6',
          backgroundColor: 'rgba(59, 130, 246, 0.1)',
          borderWidth: 2,
          fill: true,
          tension: 0.4
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: {
            display: false
          }
        },
        scales: {
          y: {
            beginAtZero: true,
            grid: {
              color: 'rgba(0, 0, 0, 0.1)'
            }
          },
          x: {
            grid: {
              display: false
            }
          }
        },
        animation: {
          duration: this.options.enableAnimations ? 1000 : 0
        }
      }
    });
    
    this.charts.set('line', chart);
  }
  
  initBarChart() {
    const canvas = this.container.querySelector('#barChart');
    if (!canvas) return;
    
    const ctx = canvas.getContext('2d');
    const chart = new Chart(ctx, {
      type: 'bar',
      data: {
        labels: ['Electrónicos', 'Ropa', 'Hogar', 'Deportes', 'Libros'],
        datasets: [{
          label: 'Ventas',
          data: [65, 59, 80, 81, 56],
          backgroundColor: [
            '#3b82f6',
            '#8b5cf6',
            '#10b981',
            '#f59e0b',
            '#ef4444'
          ],
          borderRadius: 4
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: {
            display: false
          }
        },
        scales: {
          y: {
            beginAtZero: true,
            grid: {
              color: 'rgba(0, 0, 0, 0.1)'
            }
          },
          x: {
            grid: {
              display: false
            }
          }
        },
        animation: {
          duration: this.options.enableAnimations ? 1000 : 0
        }
      }
    });
    
    this.charts.set('bar', chart);
  }
  
  initPieChart() {
    const canvas = this.container.querySelector('#pieChart');
    if (!canvas) return;
    
    const ctx = canvas.getContext('2d');
    const chart = new Chart(ctx, {
      type: 'doughnut',
      data: {
        labels: ['Orgánico', 'Directo', 'Social', 'Email', 'Referidos'],
        datasets: [{
          data: [35, 25, 20, 15, 5],
          backgroundColor: [
            '#3b82f6',
            '#8b5cf6',
            '#10b981',
            '#f59e0b',
            '#ef4444'
          ],
          borderWidth: 0
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: {
            position: 'bottom',
            labels: {
              padding: 20,
              usePointStyle: true
            }
          }
        },
        animation: {
          duration: this.options.enableAnimations ? 1000 : 0
        }
      }
    });
    
    this.charts.set('pie', chart);
  }
  
  initGaugeChart() {
    const canvas = this.container.querySelector('#gaugeChart');
    if (!canvas) return;
    
    const ctx = canvas.getContext('2d');
    const chart = new Chart(ctx, {
      type: 'doughnut',
      data: {
        datasets: [{
          data: [85, 15],
          backgroundColor: ['#10b981', '#f3f4f6'],
          borderWidth: 0,
          circumference: 180,
          rotation: 270
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: {
            display: false
          },
          tooltip: {
            enabled: false
          }
        },
        animation: {
          duration: this.options.enableAnimations ? 1500 : 0
        }
      },
      plugins: [{
        afterDraw: (chart) => {
          const { ctx, chartArea } = chart;
          const centerX = (chartArea.left + chartArea.right) / 2;
          const centerY = (chartArea.top + chartArea.bottom) / 2;
          
          ctx.save();
          ctx.font = 'bold 24px Inter';
          ctx.fillStyle = '#1f2937';
          ctx.textAlign = 'center';
          ctx.fillText('85%', centerX, centerY + 10);
          
          ctx.font = '14px Inter';
          ctx.fillStyle = '#6b7280';
          ctx.fillText('Puntuación', centerX, centerY + 30);
          ctx.restore();
        }
      }]
    });
    
    this.charts.set('gauge', chart);
  }
  
  refreshData() {
    const refreshBtn = this.container.querySelector('.refresh-btn');
    if (refreshBtn) {
      refreshBtn.classList.add('loading');
      refreshBtn.disabled = true;
    }
    
    // Simular carga de datos
    setTimeout(() => {
      this.updateKPIs();
      this.updateCharts();
      this.updateActivityFeed();
      
      if (refreshBtn) {
        refreshBtn.classList.remove('loading');
        refreshBtn.disabled = false;
      }
      
      this.dispatchEvent('dashboard:refreshed', { timestamp: Date.now() });
    }, 1000);
  }
  
  updateKPIs() {
    const kpiCards = this.container.querySelectorAll('.kpi-card');
    kpiCards.forEach(card => {
      const valueEl = card.querySelector('.kpi-value');
      const changeEl = card.querySelector('.kpi-change');
      
      if (valueEl && changeEl) {
        // Simular nuevos valores
        const currentValue = parseFloat(valueEl.textContent.replace(/[^0-9.]/g, ''));
        const change = (Math.random() - 0.5) * 0.2; // ±10%
        const newValue = currentValue * (1 + change);
        
        // Animar el cambio de valor
        this.animateValue(valueEl, currentValue, newValue, 1000);
        
        // Actualizar indicador de cambio
        const changePercent = (change * 100).toFixed(1);
        changeEl.textContent = `${change >= 0 ? '+' : ''}${changePercent}%`;
        changeEl.className = `kpi-change ${change >= 0 ? 'positive' : 'negative'}`;
      }
    });
  }
  
  updateCharts() {
    this.charts.forEach((chart, type) => {
      if (type === 'line') {
        // Actualizar datos del gráfico de línea
        const newData = chart.data.datasets[0].data.map(value => 
          value + (Math.random() - 0.5) * 5000
        );
        chart.data.datasets[0].data = newData;
      } else if (type === 'bar') {
        // Actualizar datos del gráfico de barras
        const newData = chart.data.datasets[0].data.map(value => 
          Math.max(0, value + (Math.random() - 0.5) * 20)
        );
        chart.data.datasets[0].data = newData;
      }
      
      chart.update('active');
    });
  }
  
  updateActivityFeed() {
    const activityContainer = this.container.querySelector('.activity-container');
    if (!activityContainer) return;
    
    const activities = [
      'Nuevo pedido recibido de Ana Martín',
      'Producto agregado al inventario',
      'Reseña de cliente enviada',
      'Usuario premium registrado',
      'Pago procesado exitosamente'
    ];
    
    const randomActivity = activities[Math.floor(Math.random() * activities.length)];
    const newItem = this.createActivityItem(randomActivity, 'hace 1 minuto');
    
    activityContainer.insertBefore(newItem, activityContainer.firstChild);
    
    // Mantener solo los últimos 5 elementos
    const items = activityContainer.querySelectorAll('.activity-item');
    if (items.length > 5) {
      items[items.length - 1].remove();
    }
  }
  
  createActivityItem(text, time) {
    const item = document.createElement('div');
    item.className = 'activity-item';
    item.innerHTML = `
      <div class="activity-icon">
        <svg viewBox="0 0 24 24">
          <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
        </svg>
      </div>
      <div class="activity-content">
        <div class="activity-text">${text}</div>
        <div class="activity-time">${time}</div>
      </div>
    `;
    return item;
  }
  
  updateTimeRange(range) {
    // Actualizar datos basados en el rango de tiempo seleccionado
    this.dispatchEvent('dashboard:timeRangeChanged', { range });
    this.refreshData();
  }
  
  exportData(format = 'csv') {
    const data = this.collectDashboardData();
    
    if (format === 'csv') {
      this.exportToCSV(data);
    } else if (format === 'json') {
      this.exportToJSON(data);
    }
    
    this.dispatchEvent('dashboard:dataExported', { format, data });
  }
  
  collectDashboardData() {
    const kpiData = [];
    const kpiCards = this.container.querySelectorAll('.kpi-card');
    
    kpiCards.forEach(card => {
      const label = card.querySelector('.kpi-label')?.textContent;
      const value = card.querySelector('.kpi-value')?.textContent;
      const change = card.querySelector('.kpi-change')?.textContent;
      
      if (label && value) {
        kpiData.push({ label, value, change });
      }
    });
    
    return {
      kpis: kpiData,
      timestamp: new Date().toISOString()
    };
  }
  
  exportToCSV(data) {
    const csvContent = [
      ['Métrica', 'Valor', 'Cambio'],
      ...data.kpis.map(kpi => [kpi.label, kpi.value, kpi.change])
    ].map(row => row.join(',')).join('\n');
    
    this.downloadFile(csvContent, 'dashboard-data.csv', 'text/csv');
  }
  
  exportToJSON(data) {
    const jsonContent = JSON.stringify(data, null, 2);
    this.downloadFile(jsonContent, 'dashboard-data.json', 'application/json');
  }
  
  downloadFile(content, filename, mimeType) {
    const blob = new Blob([content], { type: mimeType });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  }
  
  toggleWidgetMenu(button) {
    // Implementar menú contextual del widget
    console.log('Menú del widget activado', button);
  }
  
  resizeCharts() {
    this.charts.forEach(chart => {
      chart.resize();
    });
  }
  
  startAutoRefresh() {
    if (!this.options.autoRefresh) return;
    
    this.refreshTimer = setInterval(() => {
      if (this.isVisible) {
        this.refreshData();
      }
    }, this.options.refreshInterval);
  }
  
  stopAutoRefresh() {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer);
      this.refreshTimer = null;
    }
  }
  
  setupIntersectionObserver() {
    if (!this.options.enableAnimations) return;
    
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          entry.target.classList.add('animate-in');
        }
      });
    }, {
      threshold: 0.1,
      rootMargin: '50px'
    });
    
    const widgets = this.container.querySelectorAll('.chart-widget, .kpi-card');
    widgets.forEach(widget => observer.observe(widget));
  }
  
  setupAccessibility() {
    // Configurar atributos ARIA
    const widgets = this.container.querySelectorAll('.chart-widget');
    widgets.forEach((widget, index) => {
      widget.setAttribute('role', 'region');
      widget.setAttribute('aria-label', `Widget ${index + 1}`);
    });
    
    // Configurar navegación por teclado
    const focusableElements = this.container.querySelectorAll(
      'button, select, [tabindex]:not([tabindex="-1"])'
    );
    
    focusableElements.forEach(element => {
      element.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          element.click();
        }
      });
    });
  }
  
  // Métodos de utilidad
  animateValue(element, start, end, duration) {
    const startTime = performance.now();
    const isNumber = !isNaN(start) && !isNaN(end);
    
    const animate = (currentTime) => {
      const elapsed = currentTime - startTime;
      const progress = Math.min(elapsed / duration, 1);
      
      if (isNumber) {
        const current = start + (end - start) * this.easeOutCubic(progress);
        const prefix = element.textContent.match(/^[^0-9]*/)[0];
        element.textContent = prefix + Math.round(current).toLocaleString(this.options.locale);
      }
      
      if (progress < 1) {
        requestAnimationFrame(animate);
      }
    };
    
    requestAnimationFrame(animate);
  }
  
  easeOutCubic(t) {
    return 1 - Math.pow(1 - t, 3);
  }
  
  debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }
  
  dispatchEvent(eventName, detail = {}) {
    const event = new CustomEvent(eventName, {
      detail,
      bubbles: true,
      cancelable: true
    });
    this.container.dispatchEvent(event);
  }
  
  // API Pública
  updateWidget(widgetId, data) {
    const widget = this.container.querySelector(`[data-widget="${widgetId}"]`);
    if (widget) {
      // Implementar actualización específica del widget
      this.dispatchEvent('widget:updated', { widgetId, data });
    }
  }
  
  addWidget(config) {
    // Implementar adición dinámica de widgets
    this.dispatchEvent('widget:added', { config });
  }
  
  removeWidget(widgetId) {
    const widget = this.container.querySelector(`[data-widget="${widgetId}"]`);
    if (widget) {
      widget.remove();
      this.dispatchEvent('widget:removed', { widgetId });
    }
  }
  
  getWidgetData(widgetId) {
    // Retornar datos específicos del widget
    return this.collectDashboardData();
  }
  
  setTheme(theme) {
    this.options.theme = theme;
    document.documentElement.setAttribute('data-theme', theme);
    this.dispatchEvent('dashboard:themeChanged', { theme });
  }
  
  destroy() {
    this.stopAutoRefresh();
    
    // Destruir gráficos
    this.charts.forEach(chart => chart.destroy());
    this.charts.clear();
    
    // Remover event listeners
    // (En una implementación real, se almacenarían las referencias)
    
    this.dispatchEvent('dashboard:destroyed');
  }
}

// Funciones de utilidad globales
function throttle(func, limit) {
  let inThrottle;
  return function() {
    const args = arguments;
    const context = this;
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

function debounce(func, wait, immediate) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    const later = function() {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(context, args);
  };
}

// Auto-inicialización
document.addEventListener('DOMContentLoaded', () => {
  const dashboardContainers = document.querySelectorAll('.dashboard-container');
  dashboardContainers.forEach(container => {
    if (!container.dataset.dashboardInitialized) {
      new WidgetsDashboard(container);
      container.dataset.dashboardInitialized = 'true';
    }
  });
});

// Exportar para uso como módulo
if (typeof module !== 'undefined' && module.exports) {
  module.exports = WidgetsDashboard;
}

// Soporte para AMD
if (typeof define === 'function' && define.amd) {
  define([], () => WidgetsDashboard);
}

// Exportar globalmente
if (typeof window !== 'undefined') {
  window.WidgetsDashboard = WidgetsDashboard;
}

Ejemplos de Uso

Dashboard Básico

// Inicialización básica
const dashboard = new WidgetsDashboard('.dashboard-container');

// Escuchar eventos del dashboard
dashboard.container.addEventListener('dashboard:initialized', (e) => {
  console.log('Dashboard inicializado:', e.detail.dashboard);
});

dashboard.container.addEventListener('dashboard:refreshed', (e) => {
  console.log('Datos actualizados:', e.detail.timestamp);
});

Configuración Personalizada

const dashboard = new WidgetsDashboard('.dashboard-container', {
  autoRefresh: true,
  refreshInterval: 60000, // 1 minuto
  enableAnimations: true,
  theme: 'dark',
  locale: 'es-ES'
});

// Cambiar tema dinámicamente
dashboard.setTheme('light');

// Actualizar datos manualmente
dashboard.refreshData();

Gestión Dinámica de Widgets

// Agregar nuevo widget
dashboard.addWidget({
  type: 'chart',
  chartType: 'line',
  title: 'Nuevas Métricas',
  data: {
    labels: ['Q1', 'Q2', 'Q3', 'Q4'],
    datasets: [{
      label: 'Ventas',
      data: [100, 150, 200, 180]
    }]
  }
});

// Actualizar widget existente
dashboard.updateWidget('revenue-chart', {
  data: newChartData,
  options: updatedOptions
});

// Remover widget
dashboard.removeWidget('old-widget');

Exportación de Datos

// Exportar como CSV
dashboard.exportData('csv');

// Exportar como JSON
dashboard.exportData('json');

// Obtener datos programáticamente
const dashboardData = dashboard.getWidgetData();
console.log('Datos del dashboard:', dashboardData);

Control de Actualización

// Detener actualización automática
dashboard.stopAutoRefresh();

// Reiniciar actualización automática
dashboard.startAutoRefresh();

// Actualizar rango de tiempo
dashboard.updateTimeRange('7d');

Referencia de API

Constructor

new WidgetsDashboard(container, options)

Opciones del Constructor

OpciónTipoPredeterminadoDescripción
autoRefreshBooleantrueHabilitar actualización automática
refreshIntervalNumber30000Intervalo de actualización en ms
enableAnimationsBooleantrueHabilitar animaciones
themeString'auto'Tema: ‘light’, ‘dark’, ‘auto’
localeString'es-ES'Configuración regional

Métodos

refreshData()

Actualiza todos los datos del dashboard manualmente.

dashboard.refreshData();

updateWidget(widgetId, data)

Actualiza un widget específico con nuevos datos.

dashboard.updateWidget('chart-1', {
  data: newData,
  options: newOptions
});

addWidget(config)

Agrega un nuevo widget al dashboard.

dashboard.addWidget({
  type: 'kpi',
  title: 'Nueva Métrica',
  value: 1234,
  change: '+5.2%'
});

removeWidget(widgetId)

Remueve un widget del dashboard.

dashboard.removeWidget('widget-id');

exportData(format)

Exporta los datos del dashboard en el formato especificado.

dashboard.exportData('csv'); // o 'json'

setTheme(theme)

Cambia el tema del dashboard.

dashboard.setTheme('dark'); // 'light', 'dark', 'auto'

updateTimeRange(range)

Actualiza el rango de tiempo para los datos.

dashboard.updateTimeRange('7d'); // '1h', '24h', '7d', '30d'

getWidgetData(widgetId?)

Obtiene los datos de un widget específico o de todo el dashboard.

const data = dashboard.getWidgetData('chart-1');
const allData = dashboard.getWidgetData();

destroy()

Destruye la instancia del dashboard y limpia los recursos.

dashboard.destroy();

Eventos

dashboard:initialized

Se dispara cuando el dashboard se inicializa completamente.

dashboard.container.addEventListener('dashboard:initialized', (e) => {
  console.log('Dashboard listo:', e.detail.dashboard);
});

dashboard:refreshed

Se dispara cuando los datos se actualizan.

dashboard.container.addEventListener('dashboard:refreshed', (e) => {
  console.log('Actualizado en:', e.detail.timestamp);
});

dashboard:timeRangeChanged

Se dispara cuando cambia el rango de tiempo.

dashboard.container.addEventListener('dashboard:timeRangeChanged', (e) => {
  console.log('Nuevo rango:', e.detail.range);
});

dashboard:dataExported

Se dispara cuando se exportan datos.

dashboard.container.addEventListener('dashboard:dataExported', (e) => {
  console.log('Datos exportados:', e.detail.format, e.detail.data);
});

widget:updated

Se dispara cuando se actualiza un widget.

dashboard.container.addEventListener('widget:updated', (e) => {
  console.log('Widget actualizado:', e.detail.widgetId);
});

Clases CSS

Contenedores

  • .dashboard-container - Contenedor principal del dashboard
  • .dashboard-header - Encabezado con título y controles
  • .kpi-grid - Cuadrícula de tarjetas KPI
  • .charts-grid - Cuadrícula de widgets de gráficos

Widgets

  • .kpi-card - Tarjeta de indicador clave de rendimiento
  • .chart-widget - Widget de gráfico
  • .table-widget - Widget de tabla de datos
  • .activity-widget - Widget de actividad en tiempo real

Estados

  • .loading - Estado de carga
  • .error - Estado de error
  • .success - Estado de éxito
  • .animate-in - Animación de entrada

Controles

  • .refresh-btn - Botón de actualización
  • .export-btn - Botón de exportación
  • .widget-menu-btn - Botón de menú del widget
  • .time-range-select - Selector de rango de tiempo

Personalización

Variables CSS

:root {
  /* Colores principales */
  --dashboard-primary: #3b82f6;
  --dashboard-secondary: #8b5cf6;
  --dashboard-success: #10b981;
  --dashboard-warning: #f59e0b;
  --dashboard-error: #ef4444;
  
  /* Colores de fondo */
  --dashboard-bg: #f5f7fa;
  --dashboard-card-bg: #ffffff;
  --dashboard-header-bg: #ffffff;
  
  /* Colores de texto */
  --dashboard-text-primary: #1f2937;
  --dashboard-text-secondary: #6b7280;
  --dashboard-text-muted: #9ca3af;
  
  /* Espaciado */
  --dashboard-spacing-xs: 0.5rem;
  --dashboard-spacing-sm: 1rem;
  --dashboard-spacing-md: 1.5rem;
  --dashboard-spacing-lg: 2rem;
  --dashboard-spacing-xl: 3rem;
  
  /* Bordes */
  --dashboard-border-radius: 12px;
  --dashboard-border-color: #e5e7eb;
  
  /* Sombras */
  --dashboard-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --dashboard-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  --dashboard-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}

Tema Personalizado

/* Tema personalizado */
.dashboard-container[data-theme="custom"] {
  --dashboard-primary: #6366f1;
  --dashboard-secondary: #8b5cf6;
  --dashboard-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

/* Animaciones personalizadas */
@keyframes slideInFromLeft {
  from {
    opacity: 0;
    transform: translateX(-30px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

.custom-animation {
  animation: slideInFromLeft 0.6s ease-out;
}

Integración con Frameworks

React

import React, { useEffect, useRef } from 'react';
import { WidgetsDashboard } from './widgets-dashboard';

function DashboardComponent({ config }) {
  const dashboardRef = useRef(null);
  const instanceRef = useRef(null);
  
  useEffect(() => {
    if (dashboardRef.current && !instanceRef.current) {
      instanceRef.current = new WidgetsDashboard(dashboardRef.current, config);
    }
    
    return () => {
      if (instanceRef.current) {
        instanceRef.current.destroy();
        instanceRef.current = null;
      }
    };
  }, [config]);
  
  return (
    <div ref={dashboardRef} className="dashboard-container">
      {/* Contenido del dashboard */}
    </div>
  );
}

Vue

<template>
  <div ref="dashboardContainer" class="dashboard-container">
    <!-- Contenido del dashboard -->
  </div>
</template>

<script>
import { WidgetsDashboard } from './widgets-dashboard';

export default {
  name: 'DashboardComponent',
  props: {
    config: {
      type: Object,
      default: () => ({})
    }
  },
  mounted() {
    this.dashboard = new WidgetsDashboard(this.$refs.dashboardContainer, this.config);
  },
  beforeUnmount() {
    if (this.dashboard) {
      this.dashboard.destroy();
    }
  }
};
</script>

Angular

import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { WidgetsDashboard } from './widgets-dashboard';

@Component({
  selector: 'app-dashboard',
  template: `
    <div #dashboardContainer class="dashboard-container">
      <!-- Contenido del dashboard -->
    </div>
  `
})
export class DashboardComponent implements OnInit, OnDestroy {
  @ViewChild('dashboardContainer', { static: true }) dashboardContainer!: ElementRef;
  @Input() config: any = {};
  
  private dashboard?: WidgetsDashboard;
  
  ngOnInit() {
    this.dashboard = new WidgetsDashboard(
      this.dashboardContainer.nativeElement,
      this.config
    );
  }
  
  ngOnDestroy() {
    if (this.dashboard) {
      this.dashboard.destroy();
    }
  }
}

Accesibilidad

Soporte ARIA

  • Todos los widgets tienen roles ARIA apropiados
  • Etiquetas descriptivas para lectores de pantalla
  • Estados de carga y error comunicados a tecnologías asistivas
  • Navegación por teclado completa
  • Tab / Shift+Tab - Navegar entre elementos interactivos
  • Enter / Space - Activar botones y controles
  • Escape - Cerrar menús y modales
  • Arrow Keys - Navegar dentro de widgets complejos

Soporte para Lectores de Pantalla

<!-- Ejemplo de estructura accesible -->
<div class="chart-widget" role="region" aria-label="Gráfico de ingresos">
  <div class="widget-header">
    <h3 id="chart-title">Tendencia de Ingresos</h3>
  </div>
  <div class="chart-container" aria-labelledby="chart-title">
    <canvas aria-label="Gráfico que muestra la tendencia de ingresos de los últimos 6 meses"></canvas>
  </div>
</div>

Reducción de Movimiento

@media (prefers-reduced-motion: reduce) {
  .dashboard-container * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Soporte de Navegadores

  • Chrome: 60+
  • Firefox: 55+
  • Safari: 12+
  • Edge: 79+
  • IE: No soportado

Polyfills para Navegadores Antiguos

<!-- Para soporte de IE 11 -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6,IntersectionObserver,CustomEvent"></script>

Consideraciones de Rendimiento

Optimización

  • Uso de requestAnimationFrame para animaciones suaves
  • Debouncing y throttling para eventos de redimensionamiento
  • Lazy loading de gráficos fuera del viewport
  • Virtualización para grandes conjuntos de datos
  • Limpieza automática de recursos al destruir

Mejores Prácticas

// Configurar intervalos de actualización apropiados
const dashboard = new WidgetsDashboard('.dashboard-container', {
  refreshInterval: 60000, // No menos de 30 segundos para datos en tiempo real
  enableAnimations: !window.matchMedia('(prefers-reduced-motion: reduce)').matches
});

// Pausar actualizaciones cuando la pestaña no está visible
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    dashboard.stopAutoRefresh();
  } else {
    dashboard.startAutoRefresh();
  }
});

Gestión de Memoria

  • Destrucción automática de gráficos Chart.js
  • Limpieza de event listeners
  • Cancelación de timers y observadores
  • Liberación de referencias DOM

HTML

60

líneas

CSS

133

líneas


                <div class="dashboard-container">
  <div class="dashboard-grid">
    <!-- Widget de Gráfico de Línea -->
    <div class="widget line-chart-widget">
      <div class="widget-header">
        <h3>Ventas Mensuales</h3>
        <div class="widget-controls">
          <button class="refresh-btn">↻</button>
        </div>
      </div>
      <div class="chart-container">
        <canvas id="lineChart"></canvas>
      </div>
    </div>

    <!-- Widget de Gráfico de Barras -->
    <div class="widget bar-chart-widget">
      <div class="widget-header">
        <h3>Productos Populares</h3>
      </div>
      <div class="chart-container">
        <canvas id="barChart"></canvas>
      </div>
    </div>

    <!-- Widget de Métricas -->
    <div class="widget metrics-widget">
      <div class="widget-header">
        <h3>Métricas Clave</h3>
      </div>
      <div class="metrics-grid">
        <div class="metric-item">
          <div class="metric-value">\$24,580</div>
          <div class="metric-label">Ingresos</div>
          <div class="metric-change positive">+12.5%</div>
        </div>
        <div class="metric-item">
          <div class="metric-value">1,247</div>
          <div class="metric-label">Usuarios</div>
          <div class="metric-change positive">+8.2%</div>
        </div>
        <div class="metric-item">
          <div class="metric-value">89.3%</div>
          <div class="metric-label">Conversión</div>
          <div class="metric-change negative">-2.1%</div>
        </div>
      </div>
    </div>

    <!-- Widget de Gráfico Circular -->
    <div class="widget pie-chart-widget">
      <div class="widget-header">
        <h3>Distribución de Tráfico</h3>
      </div>
      <div class="chart-container">
        <canvas id="pieChart"></canvas>
      </div>
    </div>
  </div>
</div>

              
60líneas
1819caracteres
HTMLIdioma

Fragmentos de Código Relacionados

Explora packs de plantillas

¿Necesitas bloques más grandes? Descubre landings y colecciones de componentes.

Abrir la biblioteca de plantillas →