widgets
advanced
dashboard
widgets
charts
data-visualization
interactivo
responsive
real-time
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">
  
  <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>

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

  
  <div class="charts-grid">
    
    <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>

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

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

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

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

    
    <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>
.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;
}.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;
}.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;
}.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;
}.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;
}.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;
}@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;
  }
}@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;
}@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;
  }
}

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

    this.dispatchEvent('dashboard:initialized', { dashboard: this });
  }
  
  setupEventListeners() {

    const refreshBtn = this.container.querySelector('.refresh-btn');
    if (refreshBtn) {
      refreshBtn.addEventListener('click', () => this.refreshData());
    }

    const timeRangeSelect = this.container.querySelector('.time-range-select');
    if (timeRangeSelect) {
      timeRangeSelect.addEventListener('change', (e) => {
        this.updateTimeRange(e.target.value);
      });
    }

    const exportBtns = this.container.querySelectorAll('.export-btn');
    exportBtns.forEach(btn => {
      btn.addEventListener('click', (e) => {
        const format = e.currentTarget.dataset.export || 'csv';
        this.exportData(format);
      });
    });

    const menuBtns = this.container.querySelectorAll('.widget-menu-btn');
    menuBtns.forEach(btn => {
      btn.addEventListener('click', (e) => {
        this.toggleWidgetMenu(e.currentTarget);
      });
    });

    document.addEventListener('visibilitychange', () => {
      this.isVisible = !document.hidden;
      if (this.isVisible) {
        this.refreshData();
      }
    });

    window.addEventListener('resize', this.debounce(() => {
      this.resizeCharts();
    }, 250));
  }
  
  initializeCharts() {

    this.initLineChart();

    this.initBarChart();

    this.initPieChart();

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

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

        const currentValue = parseFloat(valueEl.textContent.replace(/[^0-9.]/g, ''));
        const change = (Math.random() - 0.5) * 0.2; // ±10%
        const newValue = currentValue * (1 + change);

        this.animateValue(valueEl, currentValue, newValue, 1000);

        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') {

        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') {

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

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

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

    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() {

    const widgets = this.container.querySelectorAll('.chart-widget');
    widgets.forEach((widget, index) => {
      widget.setAttribute('role', 'region');
      widget.setAttribute('aria-label', `Widget ${index + 1}`);
    });

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

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

  updateWidget(widgetId, data) {
    const widget = this.container.querySelector(`[data-widget="${widgetId}"]`);
    if (widget) {

      this.dispatchEvent('widget:updated', { widgetId, data });
    }
  }
  
  addWidget(config) {

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

    return this.collectDashboardData();
  }
  
  setTheme(theme) {
    this.options.theme = theme;
    document.documentElement.setAttribute('data-theme', theme);
    this.dispatchEvent('dashboard:themeChanged', { theme });
  }
  
  destroy() {
    this.stopAutoRefresh();

    this.charts.forEach(chart => chart.destroy());
    this.charts.clear();


    
    this.dispatchEvent('dashboard:destroyed');
  }
}

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

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

if (typeof module !== 'undefined' && module.exports) {
  module.exports = WidgetsDashboard;
}

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

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

Ejemplos de Uso

Dashboard Básico


const dashboard = new WidgetsDashboard('.dashboard-container');

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

dashboard.setTheme('light');

dashboard.refreshData();

Gestión Dinámica de Widgets


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

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

dashboard.removeWidget('old-widget');

Exportación de Datos


dashboard.exportData('csv');

dashboard.exportData('json');

const dashboardData = dashboard.getWidgetData();
console.log('Datos del dashboard:', dashboardData);

Control de Actualización


dashboard.stopAutoRefresh();

dashboard.startAutoRefresh();

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 --dashboard-primary: #3b82f6;
  --dashboard-secondary: #8b5cf6;
  --dashboard-success: #10b981;
  --dashboard-warning: #f59e0b;
  --dashboard-error: #ef4444;--dashboard-bg: #f5f7fa;
  --dashboard-card-bg: #ffffff;
  --dashboard-header-bg: #ffffff;--dashboard-text-primary: #1f2937;
  --dashboard-text-secondary: #6b7280;
  --dashboard-text-muted: #9ca3af;--dashboard-spacing-xs: 0.5rem;
  --dashboard-spacing-sm: 1rem;
  --dashboard-spacing-md: 1.5rem;
  --dashboard-spacing-lg: 2rem;
  --dashboard-spacing-xl: 3rem;--dashboard-border-radius: 12px;
  --dashboard-border-color: #e5e7eb;--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

.dashboard-container[data-theme="custom"] {
  --dashboard-primary: #6366f1;
  --dashboard-secondary: #8b5cf6;
  --dashboard-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}@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

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

Vue

<template>
  <div ref="dashboardContainer" class="dashboard-container">
    
  </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">
      
    </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


<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


<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


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

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

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

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

    
    <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
1686caracteres
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 ->