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
Diseño Responsivo
Sí
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.
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ón | Tipo | Predeterminado | Descripción |
|---|---|---|---|
autoRefresh | Boolean | true | Habilitar actualización automática |
refreshInterval | Number | 30000 | Intervalo de actualización en ms |
enableAnimations | Boolean | true | Habilitar animaciones |
theme | String | 'auto' | Tema: ‘light’, ‘dark’, ‘auto’ |
locale | String | '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
Navegación por Teclado
Tab/Shift+Tab- Navegar entre elementos interactivosEnter/Space- Activar botones y controlesEscape- Cerrar menús y modalesArrow 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
requestAnimationFramepara 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>