Análisis aclaratorio vs análisis exploratorio en dashboards: cómo diseñarlos, cuándo usarlos y cómo llevarlos a Power BI
1. Introducción
No todos los cuadros de mando tienen el mismo propósito. Dos dashboards pueden utilizar exactamente los mismos datos, los mismos KPIs e incluso gráficos parecidos, pero inducir comportamientos completamente distintos en el usuario.
Un dashboard no debería evaluarse únicamente por si “se ve bonito”, si tiene muchos gráficos o si incluye filtros avanzados. La pregunta más importante es:
¿Qué tipo de pregunta permite responder y qué comportamiento analítico induce en el usuario?
En la práctica profesional de Business Intelligence, analítica de datos y reporting, es fundamental distinguir entre dos enfoques:
- Análisis aclaratorio, también llamado explanatory o clarificatory analysis.
- Análisis exploratorio, también llamado exploratory analysis.
El primero se utiliza cuando ya existe una conclusión o una línea argumental que queremos comunicar con claridad. El segundo se utiliza cuando todavía estamos investigando los datos y necesitamos libertad para descubrir patrones, anomalías, relaciones o hipótesis.
Esta diferencia afecta directamente a:
- La selección de gráficos.
- La cantidad de información visible.
- El nivel de interacción.
- La navegación.
- Los filtros.
- La jerarquía visual.
- El uso de texto explicativo.
- La estructura del informe.
- La futura implementación en herramientas como Power BI.
Un dashboard aclaratorio se parece más a una presentación ejecutiva basada en datos. Un dashboard exploratorio se parece más a un entorno de investigación analítica.
Ambos son valiosos, pero no sirven para lo mismo.
2. ¿Qué es el análisis aclaratorio?
El análisis aclaratorio tiene como objetivo principal comunicar una conclusión de forma clara, rápida y accionable.
No parte de la pregunta “¿qué puedo encontrar?”, sino de una afirmación o hipótesis ya analizada previamente:
“Las ventas han caído porque la región Norte ha incumplido el objetivo, especialmente en la categoría Software dentro del segmento Pyme.”
El dashboard aclaratorio organiza los datos para que el usuario entienda:
- Qué ha pasado.
- Cuánto ha pasado.
- Dónde ha pasado.
- Qué lo explica.
- Qué decisión debería considerarse.
Características principales
Un dashboard aclaratorio suele tener las siguientes características:
1. Comunicación de conclusiones
Su finalidad no es que el usuario navegue indefinidamente por los datos, sino que comprenda una conclusión relevante.
Ejemplo:
“La caída del 8,4 % en ventas se explica principalmente por el mal desempeño de Norte y del producto Software.”
2. Storytelling con datos
El diseño sigue una narrativa:
- Situación general.
- Desviación frente al objetivo.
- Identificación del foco del problema.
- Causa principal.
- Recomendación o decisión.
3. Presentación de KPIs relevantes
Los KPIs no se muestran como decoración. Se seleccionan porque apoyan la historia.
Ejemplos:
- Ventas totales.
- Variación frente al periodo anterior.
- Cumplimiento del objetivo.
- Margen.
- Desviación absoluta.
- Región con mayor impacto negativo.
4. Jerarquía visual
El usuario debe saber dónde mirar primero.
Normalmente:
- KPIs arriba.
- Gráfico principal en el centro.
- Causas o descomposición debajo.
- Texto explicativo destacado.
- Colores usados con intención.
5. Reducción de ruido
Se eliminan elementos que no ayudan a la conclusión:
- Filtros innecesarios.
- Gráficos secundarios.
- Tablas extensas.
- Demasiadas dimensiones simultáneas.
- Colores decorativos.
6. Guiar la atención del usuario
El diseñador controla el recorrido analítico.
No se espera que el usuario “descubra” la historia por sí mismo. El dashboard la presenta de forma explícita.
7. Contextualización de resultados
Un número aislado no basta. Hay que compararlo con:
- Objetivo.
- Periodo anterior.
- Presupuesto.
- Media histórica.
- Benchmark.
- Región equivalente.
- Producto comparable.
8. Interacción limitada y deliberada
Puede haber interacción, pero no debería ser el centro del diseño.
Por ejemplo:
- Cambiar entre ventas y margen.
- Seleccionar periodo.
- Ver tooltip.
- Abrir detalle complementario.
Pero el dashboard debería responder la pregunta principal sin requerir muchos clics.
Preguntas típicas que responde
Un dashboard aclaratorio responde preguntas como:
- ¿Cómo han evolucionado las ventas?
- ¿Por qué hemos incumplido el objetivo?
- ¿Qué región explica la caída?
- ¿Qué productos están generando el problema?
- ¿Cuál es la principal causa de la desviación?
- ¿Dónde debemos actuar primero?
- ¿Qué mensaje debe llevarse la dirección?
3. ¿Qué es el análisis exploratorio?
El análisis exploratorio tiene como objetivo principal investigar los datos libremente para descubrir patrones, relaciones, anomalías o hipótesis.
No parte necesariamente de una conclusión cerrada. Parte de preguntas abiertas:
“¿Qué segmentos están detrás de la caída?”
“¿La reducción de margen afecta a todos los productos?”
“¿Hay regiones con comportamientos atípicos?”
“¿Existe relación entre volumen vendido y rentabilidad?”
El usuario controla el recorrido analítico.
Características principales
1. Descubrimiento de patrones
El dashboard permite detectar tendencias, agrupaciones o comportamientos no evidentes.
2. Investigación de hipótesis
El usuario puede formular hipótesis y comprobarlas:
- “¿La caída se concentra en clientes enterprise?”
- “¿El problema aparece solo en marzo?”
- “¿El margen cae cuando aumenta el volumen?”
3. Comparación de dimensiones
Un dashboard exploratorio permite cambiar entre diferentes perspectivas:
- Región.
- Producto.
- Segmento.
- Cliente.
- Periodo.
- Canal.
- Vendedor.
- Categoría.
4. Filtrado dinámico
Los filtros son una parte central de la experiencia:
- Rango temporal.
- Segmento.
- Producto.
- Región.
- Cliente.
- Métrica.
- Top N.
- Comparación entre grupos.
5. Drill-down
Permite bajar de un nivel agregado a otro más detallado:
- País → región → ciudad.
- Categoría → producto.
- Año → trimestre → mes.
- Cliente → cuenta → operación.
6. Cambio de perspectivas
El usuario puede cambiar dimensiones y métricas.
Por ejemplo:
- Ver ventas por región.
- Luego margen por producto.
- Luego unidades por segmento.
- Luego desviación frente a objetivo por mes.
7. Exploración de anomalías
Debe permitir detectar elementos atípicos:
- Productos con alto volumen y bajo margen.
- Regiones con fuerte desviación.
- Clientes con caída repentina.
- Meses fuera de patrón.
8. Mayor libertad para el usuario
A diferencia del dashboard aclaratorio, aquí el diseñador no controla completamente la narrativa. Diseña un entorno de análisis.
Preguntas típicas que responde
- ¿Qué ocurre si analizamos únicamente determinados clientes?
- ¿Qué regiones tienen un comportamiento diferente?
- ¿Existe alguna relación entre margen y volumen?
- ¿Qué productos presentan comportamientos atípicos?
- ¿Cómo cambia el resultado al seleccionar distintos periodos?
- ¿Qué segmentos están detrás de una determinada tendencia?
- ¿Qué combinación de región y producto explica la desviación?
- ¿Qué hipótesis merece investigarse después?
4. Diferencias entre análisis aclaratorio y exploratorio
| Criterio | Dashboard aclaratorio | Dashboard exploratorio |
|---|---|---|
| Objetivo | Comunicar una conclusión | Descubrir patrones o hipótesis |
| Usuario principal | Dirección, negocio, stakeholders | Analistas, controllers, equipos de BI |
| Tipo de pregunta | Cerrada o semiestructurada | Abierta e investigativa |
| Ejemplo de pregunta | “¿Por qué caímos frente al objetivo?” | “¿Qué segmentos explican distintos comportamientos?” |
| Nivel de interacción | Bajo o moderado | Alto |
| Cantidad de información | Seleccionada y resumida | Amplia y multidimensional |
| Libertad del usuario | Limitada | Alta |
| Storytelling | Muy importante | Secundario |
| Navegación | Lineal o guiada | Flexible |
| Filtros | Pocos y deliberados | Muchos y dinámicos |
| Drill-down | Limitado, si aporta a la historia | Central para investigar |
| KPIs | Pocos, priorizados | Varios, cambiables o segmentables |
| Gráficos | Seleccionados para explicar | Seleccionados para comparar y explorar |
| Complejidad visual | Baja o moderada | Moderada o alta |
| Riesgo de sobrecarga | Bajo si está bien diseñado | Alto si no se estructura bien |
| Toma de decisiones | Rápida, ejecutiva | Analítica, diagnóstica |
| Caso de uso típico | Comité de dirección | Análisis comercial o financiero detallado |
| Rol del diseñador | Guía la lectura | Crea un entorno de investigación |
| Rol del usuario | Interpreta y decide | Explora, filtra y compara |
| Relación con Power BI | Página ejecutiva, resumen, narrativa | Página analítica, drill-down, segmentadores |
| Métricas | Preseleccionadas | Parametrizables o comparables |
| Texto explicativo | Muy recomendable | Menos frecuente, más contextual |
| Anotaciones | Clave para dirigir la atención | Útiles para explicar hallazgos puntuales |
| Tablas detalladas | Limitadas o ausentes | Frecuentes |
| Tooltips | Complementarios | Muy importantes |
| Diseño visual | Jerarquía fuerte | Flexibilidad y densidad controlada |
Ejemplo concreto
Imaginemos que las ventas han caído un 8 % en junio.
Un dashboard aclaratorio diría:
“Las ventas de junio caen un 8 % frente a mayo. El 62 % de la desviación procede de la región Norte, especialmente del producto Software en clientes Pyme. La prioridad debería ser revisar pipeline y pricing en esa combinación.”
Un dashboard exploratorio permitiría investigar:
- ¿La caída se da en todas las regiones?
- ¿Afecta solo a Software?
- ¿Es un problema de unidades o precio medio?
- ¿El margen también cae?
- ¿Hay clientes concretos detrás?
- ¿Se repite en años anteriores?
- ¿Es una anomalía o una tendencia?
La diferencia no es simplemente que uno tenga menos filtros que otro. La diferencia fundamental es:
En el dashboard aclaratorio controla el recorrido el diseñador.
En el dashboard exploratorio controla el recorrido el usuario.
5. Un mismo dataset, dos dashboards diferentes
A continuación se utilizan los mismos datos ficticios de ventas para construir dos dashboards conceptualmente distintos.
Dataset común
El dataset contiene ventas mensuales por:
- Mes.
- Región.
- Producto.
- Segmento de cliente.
- Ventas.
- Objetivo.
- Unidades.
- Margen.
Las dimensiones son:
- Regiones: Norte, Sur, Este, Oeste.
- Productos: Software, Hardware, Servicios, Soporte.
- Segmentos: Pyme, Enterprise, Sector Público.
- Meses: enero a junio de 2025.
La caída se diseña intencionalmente en junio, especialmente en la región Norte y el producto Software, para poder mostrar cómo un mismo dato puede explicarse o explorarse según el objetivo.
6. Dashboard 1 — Análisis aclaratorio
Objetivo
Este dashboard debe responder:
¿Qué ha ocurrido y cuál es la explicación principal?
Está diseñado para un director comercial. Por eso:
- Muestra pocos KPIs.
- Tiene una narrativa clara.
- Destaca la desviación.
- Reduce la interacción.
- Usa anotaciones.
- No pretende ser una herramienta de exploración libre.
Decisiones de diseño
- KPIs arriba: resumen inmediato.
- Gráfico temporal: muestra la caída.
- Objetivo vs resultado: contextualiza el incumplimiento.
- Ranking de regiones: identifica dónde está el problema.
- Causas de desviación: muestra qué combinación explica la caída.
- Texto explicativo: conduce a la conclusión.
- Interacción mínima: selector de métrica para no distraer.
Código completo HTML
Guarda el siguiente contenido como dashboard-aclaratorio.html y ábrelo directamente en un navegador.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Dashboard Aclaratorio - Ventas</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
:root{
--bg:#f4f6f8;
--card:#ffffff;
--text:#1f2937;
--muted:#6b7280;
--blue:#2563eb;
--red:#dc2626;
--green:#16a34a;
--amber:#f59e0b;
--grid:#e5e7eb;
}
*{box-sizing:border-box}
body{
margin:0;
font-family:Inter,Segoe UI,Arial,sans-serif;
background:var(--bg);
color:var(--text);
}
.container{
max-width:1280px;
margin:0 auto;
padding:28px;
}
.header{
display:flex;
justify-content:space-between;
gap:20px;
align-items:flex-start;
margin-bottom:22px;
}
h1{
margin:0;
font-size:30px;
letter-spacing:-0.5px;
}
.subtitle{
color:var(--muted);
margin-top:8px;
max-width:840px;
line-height:1.45;
}
.control{
background:white;
border:1px solid var(--grid);
padding:10px 12px;
border-radius:12px;
box-shadow:0 1px 2px rgba(0,0,0,.04);
}
select{
border:1px solid var(--grid);
padding:8px 10px;
border-radius:8px;
background:white;
}
.kpi-grid{
display:grid;
grid-template-columns:repeat(4,1fr);
gap:16px;
margin-bottom:18px;
}
.card{
background:var(--card);
border:1px solid var(--grid);
border-radius:16px;
padding:18px;
box-shadow:0 2px 6px rgba(15,23,42,.05);
}
.kpi-label{
color:var(--muted);
font-size:13px;
margin-bottom:8px;
}
.kpi-value{
font-size:28px;
font-weight:800;
}
.kpi-note{
margin-top:6px;
font-size:13px;
color:var(--muted);
}
.negative{color:var(--red)}
.positive{color:var(--green)}
.grid{
display:grid;
grid-template-columns:2fr 1fr;
gap:16px;
margin-bottom:16px;
}
.grid-3{
display:grid;
grid-template-columns:1.2fr 1fr 1fr;
gap:16px;
}
.card h2{
margin:0 0 6px;
font-size:18px;
}
.card p{
color:var(--muted);
margin:0 0 14px;
font-size:14px;
}
.chart{
width:100%;
height:300px;
}
.small-chart{
width:100%;
height:260px;
}
.annotation{
background:#fff7ed;
border-left:5px solid var(--amber);
padding:16px 18px;
border-radius:12px;
line-height:1.5;
}
.annotation strong{color:#9a3412}
.badge{
display:inline-block;
background:#fee2e2;
color:#991b1b;
padding:4px 8px;
border-radius:999px;
font-size:12px;
font-weight:700;
}
svg text{
font-family:Inter,Segoe UI,Arial,sans-serif;
font-size:12px;
fill:#374151;
}
.axis{stroke:#9ca3af;stroke-width:1}
.gridline{stroke:#e5e7eb;stroke-width:1}
.tooltip{
position:absolute;
pointer-events:none;
background:#111827;
color:white;
padding:8px 10px;
border-radius:8px;
font-size:12px;
display:none;
z-index:10;
}
@media(max-width:900px){
.kpi-grid,.grid,.grid-3{grid-template-columns:1fr}
.header{flex-direction:column}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div>
<h1>Caída de ventas: explicación ejecutiva</h1>
<div class="subtitle">
Análisis aclaratorio para Dirección Comercial. El objetivo es explicar qué ocurrió en junio de 2025,
dónde se concentra la desviación y cuál es la causa principal.
</div>
</div>
<div class="control">
Métrica:
<select id="metricSelect">
<option value="sales">Ventas</option>
<option value="margin">Margen</option>
</select>
</div>
</div>
<div class="kpi-grid">
<div class="card">
<div class="kpi-label">Ventas junio</div>
<div class="kpi-value" id="kpiSales">-</div>
<div class="kpi-note" id="kpiSalesNote">-</div>
</div>
<div class="card">
<div class="kpi-label">Cumplimiento objetivo</div>
<div class="kpi-value negative" id="kpiTarget">-</div>
<div class="kpi-note">Resultado frente al objetivo de junio</div>
</div>
<div class="card">
<div class="kpi-label">Variación vs mayo</div>
<div class="kpi-value negative" id="kpiChange">-</div>
<div class="kpi-note">Cambio mensual</div>
</div>
<div class="card">
<div class="kpi-label">Principal causa</div>
<div class="kpi-value" style="font-size:21px" id="kpiCause">-</div>
<div class="kpi-note">Mayor contribución negativa</div>
</div>
</div>
<div class="grid">
<div class="card">
<h2>Evolución temporal</h2>
<p>La tendencia muestra una ruptura clara en junio frente a los meses previos.</p>
<div id="lineChart" class="chart"></div>
</div>
<div class="card">
<h2>Junio: objetivo vs resultado</h2>
<p>La brecha frente al objetivo confirma el incumplimiento.</p>
<div id="targetChart" class="chart"></div>
</div>
</div>
<div class="grid-3">
<div class="card">
<h2>Ranking de regiones</h2>
<p>La región Norte presenta la mayor desviación negativa.</p>
<div id="regionChart" class="small-chart"></div>
</div>
<div class="card">
<h2>Causas de la desviación</h2>
<p>Combinaciones región-producto que más explican la caída frente al objetivo.</p>
<div id="causeChart" class="small-chart"></div>
</div>
<div class="card">
<h2>Conclusión ejecutiva</h2>
<div class="annotation" id="narrative"></div>
</div>
</div>
</div>
<div id="tooltip" class="tooltip"></div>
<script>
const months = ["Ene","Feb","Mar","Abr","May","Jun"];
const regions = ["Norte","Sur","Este","Oeste"];
const products = ["Software","Hardware","Servicios","Soporte"];
const segments = ["Pyme","Enterprise","Sector Público"];
function buildData(){
const data=[];
months.forEach((m,mi)=>{
regions.forEach((r,ri)=>{
products.forEach((p,pi)=>{
segments.forEach((s,si)=>{
let base = 65000 + ri*9000 + pi*7000 + si*6000;
let season = 1 + mi*0.035;
let sales = base * season;
if(m==="Jun" && r==="Norte") sales *= 0.72;
if(m==="Jun" && r==="Norte" && p==="Software") sales *= 0.68;
if(m==="Jun" && r==="Norte" && p==="Hardware") sales *= 0.82;
if(m==="Jun" && p==="Servicios" && s==="Enterprise") sales *= 1.05;
let target = base * (1 + mi*0.045) * 1.04;
let units = Math.round(sales / (900 + pi*180));
let marginRate = 0.28 + pi*0.035 - ri*0.01;
if(m==="Jun" && r==="Norte" && p==="Software") marginRate -= 0.06;
let margin = sales * marginRate;
data.push({
month:m, monthIndex:mi, region:r, product:p, segment:s,
sales:Math.round(sales),
target:Math.round(target),
units:units,
margin:Math.round(margin)
});
});
});
});
});
return data;
}
const data = buildData();
function fmt(n){return new Intl.NumberFormat("es-ES",{maximumFractionDigits:0}).format(n);}
function money(n){return fmt(n)+" €";}
function pct(n){return (n*100).toFixed(1).replace(".",",")+" %";}
function sum(rows, field){return rows.reduce((a,b)=>a+b[field],0);}
function group(rows, key){
const m = {};
rows.forEach(d=>{
const k = typeof key==="function" ? key(d) : d[key];
if(!m[k]) m[k]=[];
m[k].push(d);
});
return m;
}
function byMonth(metric){
return months.map(m=>{
const rows=data.filter(d=>d.month===m);
return {label:m, value:sum(rows,metric), target:sum(rows,"target")};
});
}
function clear(el){document.getElementById(el).innerHTML="";}
function scale(v,min,max,a,b){
if(max===min) return (a+b)/2;
return a + (v-min)*(b-a)/(max-min);
}
function drawLine(containerId, series, metricLabel){
const el=document.getElementById(containerId);
const w=el.clientWidth, h=el.clientHeight;
const pad={l:55,r:25,t:25,b:45};
const max=Math.max(...series.map(d=>Math.max(d.value,d.target)))*1.08;
const min=0;
const x=i=>scale(i,0,series.length-1,pad.l,w-pad.r);
const y=v=>scale(v,min,max,h-pad.b,pad.t);
const lineActual=series.map((d,i)=>`${x(i)},${y(d.value)}`).join(" ");
const lineTarget=series.map((d,i)=>`${x(i)},${y(d.target)}`).join(" ");
let svg=`<svg width="${w}" height="${h}">`;
for(let i=0;i<5;i++){
const yy=scale(i,0,4,h-pad.b,pad.t);
svg+=`<line class="gridline" x1="${pad.l}" x2="${w-pad.r}" y1="${yy}" y2="${yy}"></line>`;
}
svg+=`<polyline points="${lineTarget}" fill="none" stroke="#9ca3af" stroke-width="3" stroke-dasharray="6 5"></polyline>`;
svg+=`<polyline points="${lineActual}" fill="none" stroke="#2563eb" stroke-width="4"></polyline>`;
series.forEach((d,i)=>{
const color=d.label==="Jun" ? "#dc2626" : "#2563eb";
svg+=`<circle cx="${x(i)}" cy="${y(d.value)}" r="5" fill="${color}">
<title>${d.label}: ${metricLabel} ${money(d.value)} | Objetivo ${money(d.target)}</title></circle>`;
svg+=`<text x="${x(i)}" y="${h-18}" text-anchor="middle">${d.label}</text>`;
});
svg+=`<text x="${pad.l}" y="15" fill="#6b7280">Actual</text>`;
svg+=`<line x1="${pad.l+55}" x2="${pad.l+85}" y1="11" y2="11" stroke="#2563eb" stroke-width="4"></line>`;
svg+=`<text x="${pad.l+105}" y="15" fill="#6b7280">Objetivo</text>`;
svg+=`<line x1="${pad.l+170}" x2="${pad.l+200}" y1="11" y2="11" stroke="#9ca3af" stroke-width="3" stroke-dasharray="6 5"></line>`;
svg+=`</svg>`;
el.innerHTML=svg;
}
function drawVerticalBars(containerId, items, valueField, colorFn){
const el=document.getElementById(containerId);
const w=el.clientWidth, h=el.clientHeight;
const pad={l:60,r:20,t:30,b:55};
const max=Math.max(...items.map(d=>d[valueField]))*1.15;
const barW=(w-pad.l-pad.r)/items.length*0.55;
let svg=`<svg width="${w}" height="${h}">`;
for(let i=0;i<5;i++){
const yy=scale(i,0,4,h-pad.b,pad.t);
svg+=`<line class="gridline" x1="${pad.l}" x2="${w-pad.r}" y1="${yy}" y2="${yy}"></line>`;
}
items.forEach((d,i)=>{
const x=pad.l+i*((w-pad.l-pad.r)/items.length)+barW*0.4;
const y=scale(d[valueField],0,max,h-pad.b,pad.t);
const bh=h-pad.b-y;
svg+=`<rect x="${x}" y="${y}" width="${barW}" height="${bh}" rx="7" fill="${colorFn(d,i)}">
<title>${d.label}: ${money(d[valueField])}</title></rect>`;
svg+=`<text x="${x+barW/2}" y="${h-18}" text-anchor="middle">${d.label}</text>`;
});
svg+=`</svg>`;
el.innerHTML=svg;
}
function drawHorizontalBars(containerId, items, field, options={}){
const el=document.getElementById(containerId);
const w=el.clientWidth, h=el.clientHeight;
const pad={l:105,r:25,t:20,b:25};
const values=items.map(d=>Math.abs(d[field]));
const max=Math.max(...values)*1.15;
const rowH=(h-pad.t-pad.b)/items.length;
let svg=`<svg width="${w}" height="${h}">`;
items.forEach((d,i)=>{
const y=pad.t+i*rowH+rowH*0.18;
const bw=scale(Math.abs(d[field]),0,max,0,w-pad.l-pad.r);
const color=d[field]<0 ? "#dc2626" : "#16a34a";
svg+=`<text x="8" y="${y+rowH*0.45}">${d.label}</text>`;
svg+=`<rect x="${pad.l}" y="${y}" width="${bw}" height="${rowH*0.55}" rx="6" fill="${color}">
<title>${d.label}: ${money(d[field])}</title></rect>`;
svg+=`<text x="${pad.l+bw+6}" y="${y+rowH*0.4}" fill="#6b7280">${money(d[field])}</text>`;
});
svg+=`</svg>`;
el.innerHTML=svg;
}
function update(){
const metric=document.getElementById("metricSelect").value;
const metricLabel=metric==="sales" ? "Ventas" : "Margen";
const jun=data.filter(d=>d.month==="Jun");
const may=data.filter(d=>d.month==="May");
const junValue=sum(jun,metric);
const mayValue=sum(may,metric);
const junTarget=sum(jun,"target");
const targetRatio=junValue/junTarget;
const change=(junValue-mayValue)/mayValue;
document.getElementById("kpiSales").textContent=money(junValue);
document.getElementById("kpiSalesNote").innerHTML=`Junio 2025 · <span class="badge">${metricLabel}</span>`;
document.getElementById("kpiTarget").textContent=pct(targetRatio);
document.getElementById("kpiChange").textContent=pct(change);
const regGroups=group(jun,"region");
const regItems=Object.keys(regGroups).map(r=>({
label:r,
value:sum(regGroups[r],metric),
deviation:sum(regGroups[r],metric)-sum(regGroups[r],"target")
})).sort((a,b)=>a.deviation-b.deviation);
const causeGroups=group(jun,d=>d.region+" · "+d.product);
const causes=Object.keys(causeGroups).map(k=>({
label:k,
deviation:sum(causeGroups[k],metric)-sum(causeGroups[k],"target")
})).sort((a,b)=>a.deviation-b.deviation).slice(0,5);
document.getElementById("kpiCause").textContent=causes[0].label;
drawLine("lineChart", byMonth(metric), metricLabel);
drawVerticalBars("targetChart", [
{label:"Objetivo", value:junTarget},
{label:"Resultado", value:junValue}
], "value", (d)=>d.label==="Resultado" ? "#dc2626" : "#9ca3af");
drawHorizontalBars("regionChart", regItems.map(d=>({label:d.label, deviation:d.deviation})), "deviation");
drawHorizontalBars("causeChart", causes, "deviation");
const impact=Math.abs(causes[0].deviation);
const totalNeg=causes.filter(c=>c.deviation<0).reduce((a,b)=>a+Math.abs(b.deviation),0);
const share=impact/totalNeg;
document.getElementById("narrative").innerHTML=`
<strong>Conclusión:</strong> en junio el resultado queda por debajo del objetivo y cae frente a mayo.
La desviación no está distribuida de forma homogénea: se concentra principalmente en
<strong>${causes[0].label}</strong>, que representa aproximadamente <strong>${pct(share)}</strong>
de las principales desviaciones negativas identificadas.
<br><br>
La lectura recomendada para Dirección Comercial es priorizar una revisión específica del pipeline,
pricing y acciones comerciales en esa combinación antes de plantear medidas generales para todo el negocio.
`;
}
document.getElementById("metricSelect").addEventListener("change",update);
window.addEventListener("resize",update);
update();
</script>
</body>
</html>7. Dashboard 2 — Análisis exploratorio
Objetivo
Este dashboard debe responder:
¿Qué puedo descubrir si exploro los datos desde diferentes perspectivas?
Está diseñado para un analista. Por eso incluye:
- Filtros dinámicos.
- Segmentadores.
- Selector temporal.
- Cambio de dimensión.
- Cambio de métrica.
- Drill-down aproximado.
- Tabla detallada.
- Visualización de relación entre margen y volumen.
- Comparación entre segmentos.
- Tooltips.
Decisiones de diseño
A diferencia del dashboard aclaratorio:
- No hay una conclusión única predefinida.
- El usuario puede cambiar el recorrido.
- Se favorece la comparación.
- Se muestran más datos.
- Se permite investigar anomalías.
- La tabla es importante.
- Los filtros son protagonistas.
Código completo HTML
Guarda el siguiente contenido como dashboard-exploratorio.html.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Dashboard Exploratorio - Ventas</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
:root{
--bg:#0f172a;
--panel:#111827;
--card:#ffffff;
--soft:#f8fafc;
--text:#111827;
--muted:#64748b;
--blue:#2563eb;
--cyan:#0891b2;
--green:#16a34a;
--red:#dc2626;
--amber:#d97706;
--border:#e5e7eb;
}
*{box-sizing:border-box}
body{
margin:0;
font-family:Inter,Segoe UI,Arial,sans-serif;
background:#eef2f7;
color:var(--text);
}
.layout{
display:grid;
grid-template-columns:290px 1fr;
min-height:100vh;
}
.sidebar{
background:var(--panel);
color:white;
padding:22px;
}
.sidebar h1{
font-size:22px;
margin:0 0 6px;
}
.sidebar p{
color:#cbd5e1;
font-size:13px;
line-height:1.45;
margin-bottom:20px;
}
.filter-block{
border-top:1px solid rgba(255,255,255,.12);
padding-top:16px;
margin-top:16px;
}
.filter-block h3{
font-size:13px;
text-transform:uppercase;
letter-spacing:.08em;
color:#93c5fd;
margin:0 0 10px;
}
label{
display:block;
font-size:14px;
margin:7px 0;
}
select,input[type="range"]{
width:100%;
padding:8px;
border-radius:8px;
border:1px solid #334155;
background:#1f2937;
color:white;
}
.checkbox-row{
display:flex;
align-items:center;
gap:8px;
}
.main{
padding:24px;
}
.topbar{
display:flex;
justify-content:space-between;
align-items:flex-start;
gap:20px;
margin-bottom:16px;
}
.topbar h2{
margin:0;
font-size:27px;
}
.topbar .desc{
color:var(--muted);
margin-top:6px;
}
.kpis{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:12px;
margin-bottom:16px;
}
.card{
background:white;
border:1px solid var(--border);
border-radius:16px;
padding:15px;
box-shadow:0 2px 8px rgba(15,23,42,.05);
}
.kpi-label{
font-size:12px;
color:var(--muted);
}
.kpi-value{
font-size:22px;
font-weight:800;
margin-top:5px;
}
.grid{
display:grid;
grid-template-columns:1.4fr 1fr;
gap:16px;
margin-bottom:16px;
}
.grid-3{
display:grid;
grid-template-columns:1fr 1fr 1fr;
gap:16px;
margin-bottom:16px;
}
.card h3{
margin:0 0 6px;
font-size:17px;
}
.card .hint{
color:var(--muted);
font-size:13px;
margin:0 0 10px;
}
.chart{
height:300px;
width:100%;
}
.small-chart{
height:260px;
width:100%;
}
svg text{
font-family:Inter,Segoe UI,Arial,sans-serif;
font-size:12px;
fill:#374151;
}
.gridline{stroke:#e5e7eb;stroke-width:1}
table{
width:100%;
border-collapse:collapse;
font-size:13px;
}
th,td{
border-bottom:1px solid var(--border);
padding:8px;
text-align:left;
}
th{
background:#f8fafc;
color:#475569;
position:sticky;
top:0;
}
.table-wrap{
max-height:330px;
overflow:auto;
}
button{
background:#2563eb;
color:white;
border:none;
border-radius:8px;
padding:8px 10px;
cursor:pointer;
}
button.secondary{
background:#334155;
}
.note{
background:#eff6ff;
border-left:4px solid #2563eb;
padding:12px;
border-radius:10px;
color:#1e3a8a;
font-size:13px;
}
.pill{
display:inline-block;
background:#e0f2fe;
color:#075985;
border-radius:999px;
padding:3px 8px;
font-size:12px;
margin-left:6px;
}
@media(max-width:1000px){
.layout{grid-template-columns:1fr}
.sidebar{position:relative}
.kpis,.grid,.grid-3{grid-template-columns:1fr}
}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<h1>Explorador de ventas</h1>
<p>
Entorno de análisis libre. Cambia filtros, dimensiones y métricas para investigar patrones,
segmentos, anomalías y posibles causas.
</p>
<div class="filter-block">
<h3>Periodo</h3>
<label>Desde
<select id="fromMonth"></select>
</label>
<label>Hasta
<select id="toMonth"></select>
</label>
</div>
<div class="filter-block">
<h3>Región</h3>
<div id="regionFilters"></div>
</div>
<div class="filter-block">
<h3>Producto</h3>
<div id="productFilters"></div>
</div>
<div class="filter-block">
<h3>Segmento</h3>
<label>
Segmento cliente
<select id="segmentSelect">
<option value="Todos">Todos</option>
</select>
</label>
<label>
Comparar con
<select id="compareSegment">
<option value="Ninguno">Ninguno</option>
</select>
</label>
</div>
<div class="filter-block">
<h3>Análisis</h3>
<label>
Dimensión principal
<select id="dimensionSelect">
<option value="region">Región</option>
<option value="product">Producto</option>
<option value="segment">Segmento</option>
<option value="month">Mes</option>
</select>
</label>
<label>
Métrica
<select id="metricSelect">
<option value="sales">Ventas</option>
<option value="margin">Margen</option>
<option value="units">Unidades</option>
<option value="deviation">Desviación vs objetivo</option>
</select>
</label>
</div>
<div class="filter-block">
<button id="resetBtn" class="secondary">Restablecer filtros</button>
</div>
</aside>
<main class="main">
<div class="topbar">
<div>
<h2>Dashboard exploratorio <span class="pill" id="activeContext">Todos los datos</span></h2>
<div class="desc">
Haz clic en una barra para hacer drill-down aproximado. Por ejemplo: Región → Producto.
</div>
</div>
<div class="note">
Este diseño no intenta contar una única historia; facilita que el analista formule nuevas preguntas.
</div>
</div>
<section class="kpis">
<div class="card"><div class="kpi-label">Ventas</div><div class="kpi-value" id="kpiSales">-</div></div>
<div class="card"><div class="kpi-label">Margen</div><div class="kpi-value" id="kpiMargin">-</div></div>
<div class="card"><div class="kpi-label">Unidades</div><div class="kpi-value" id="kpiUnits">-</div></div>
<div class="card"><div class="kpi-label">Objetivo</div><div class="kpi-value" id="kpiTarget">-</div></div>
<div class="card"><div class="kpi-label">Cumplimiento</div><div class="kpi-value" id="kpiAttainment">-</div></div>
</section>
<section class="grid">
<div class="card">
<h3>Evolución temporal</h3>
<p class="hint">Comprueba cómo cambia la tendencia con los filtros activos.</p>
<div id="lineChart" class="chart"></div>
</div>
<div class="card">
<h3>Distribución por dimensión</h3>
<p class="hint">Cambia la dimensión o haz clic en una barra para profundizar.</p>
<div id="barChart" class="chart"></div>
</div>
</section>
<section class="grid-3">
<div class="card">
<h3>Relación margen-volumen</h3>
<p class="hint">Cada punto representa una combinación región-producto-segmento.</p>
<div id="scatterChart" class="small-chart"></div>
</div>
<div class="card">
<h3>Comparación de segmentos</h3>
<p class="hint">Selecciona un segmento de comparación en la barra lateral.</p>
<div id="compareChart" class="small-chart"></div>
</div>
<div class="card">
<h3>Posibles anomalías</h3>
<p class="hint">Elementos con baja rentabilidad o desviación negativa relevante.</p>
<div id="anomalyList" class="small-chart"></div>
</div>
</section>
<section class="card">
<h3>Detalle de registros</h3>
<p class="hint">Tabla filtrada. Útil para validar hipótesis y localizar combinaciones concretas.</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Mes</th><th>Región</th><th>Producto</th><th>Segmento</th>
<th>Ventas</th><th>Objetivo</th><th>Desviación</th><th>Unidades</th><th>Margen</th>
</tr>
</thead>
<tbody id="detailTable"></tbody>
</table>
</div>
</section>
</main>
</div>
<script>
const months = ["Ene","Feb","Mar","Abr","May","Jun"];
const regions = ["Norte","Sur","Este","Oeste"];
const products = ["Software","Hardware","Servicios","Soporte"];
const segments = ["Pyme","Enterprise","Sector Público"];
function buildData(){
const data=[];
months.forEach((m,mi)=>{
regions.forEach((r,ri)=>{
products.forEach((p,pi)=>{
segments.forEach((s,si)=>{
let base = 65000 + ri*9000 + pi*7000 + si*6000;
let season = 1 + mi*0.035;
let sales = base * season;
if(m==="Jun" && r==="Norte") sales *= 0.72;
if(m==="Jun" && r==="Norte" && p==="Software") sales *= 0.68;
if(m==="Jun" && r==="Norte" && p==="Hardware") sales *= 0.82;
if(m==="Jun" && p==="Servicios" && s==="Enterprise") sales *= 1.05;
let target = base * (1 + mi*0.045) * 1.04;
let units = Math.round(sales / (900 + pi*180));
let marginRate = 0.28 + pi*0.035 - ri*0.01;
if(m==="Jun" && r==="Norte" && p==="Software") marginRate -= 0.06;
let margin = sales * marginRate;
data.push({
month:m, monthIndex:mi, region:r, product:p, segment:s,
sales:Math.round(sales),
target:Math.round(target),
units:units,
margin:Math.round(margin)
});
});
});
});
});
return data;
}
const data = buildData();
function fmt(n){return new Intl.NumberFormat("es-ES",{maximumFractionDigits:0}).format(n);}
function money(n){return fmt(n)+" €";}
function pct(n){return (n*100).toFixed(1).replace(".",",")+" %";}
function sum(rows, field){
if(field==="deviation") return rows.reduce((a,b)=>a+(b.sales-b.target),0);
return rows.reduce((a,b)=>a+b[field],0);
}
function group(rows, key){
const m={};
rows.forEach(d=>{
const k=typeof key==="function" ? key(d) : d[key];
if(!m[k]) m[k]=[];
m[k].push(d);
});
return m;
}
function scale(v,min,max,a,b){
if(max===min) return (a+b)/2;
return a + (v-min)*(b-a)/(max-min);
}
function checkedValues(name){
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(x=>x.value);
}
function setupControls(){
months.forEach((m,i)=>{
fromMonth.innerHTML += `<option value="${i}">${m}</option>`;
toMonth.innerHTML += `<option value="${i}">${m}</option>`;
});
fromMonth.value=0;
toMonth.value=months.length-1;
regions.forEach(r=>{
regionFilters.innerHTML += `<label class="checkbox-row"><input type="checkbox" name="region" value="${r}" checked> ${r}</label>`;
});
products.forEach(p=>{
productFilters.innerHTML += `<label class="checkbox-row"><input type="checkbox" name="product" value="${p}" checked> ${p}</label>`;
});
segments.forEach(s=>{
segmentSelect.innerHTML += `<option value="${s}">${s}</option>`;
compareSegment.innerHTML += `<option value="${s}">${s}</option>`;
});
document.querySelectorAll("select,input").forEach(el=>el.addEventListener("change",update));
resetBtn.addEventListener("click",()=>{
fromMonth.value=0; toMonth.value=months.length-1;
document.querySelectorAll('input[name="region"],input[name="product"]').forEach(x=>x.checked=true);
segmentSelect.value="Todos";
compareSegment.value="Ninguno";
dimensionSelect.value="region";
metricSelect.value="sales";
update();
});
}
function filteredData(ignoreSegment=false){
const from=+fromMonth.value, to=+toMonth.value;
const rs=checkedValues("region");
const ps=checkedValues("product");
const seg=segmentSelect.value;
return data.filter(d=>
d.monthIndex>=from && d.monthIndex<=to &&
rs.includes(d.region) &&
ps.includes(d.product) &&
(ignoreSegment || seg==="Todos" || d.segment===seg)
);
}
function drawLine(rows){
const el=document.getElementById("lineChart");
const w=el.clientWidth,h=el.clientHeight;
const pad={l:55,r:20,t:25,b:45};
const metric=metricSelect.value==="deviation" ? "sales" : metricSelect.value;
const items=months.map((m,i)=>{
const r=rows.filter(d=>d.month===m);
return {label:m, index:i, value:sum(r,metric), target:sum(r,"target")};
}).filter(d=>d.index>=+fromMonth.value && d.index<=+toMonth.value);
const max=Math.max(...items.map(d=>Math.max(d.value,d.target)),1)*1.1;
const x=i=>scale(i,0,Math.max(items.length-1,1),pad.l,w-pad.r);
const y=v=>scale(v,0,max,h-pad.b,pad.t);
let svg=`<svg width="${w}" height="${h}">`;
for(let i=0;i<5;i++){
const yy=scale(i,0,4,h-pad.b,pad.t);
svg+=`<line class="gridline" x1="${pad.l}" x2="${w-pad.r}" y1="${yy}" y2="${yy}"></line>`;
}
svg+=`<polyline points="${items.map((d,i)=>`${x(i)},${y(d.target)}`).join(" ")}" fill="none" stroke="#94a3b8" stroke-width="3" stroke-dasharray="5 5"></polyline>`;
svg+=`<polyline points="${items.map((d,i)=>`${x(i)},${y(d.value)}`).join(" ")}" fill="none" stroke="#2563eb" stroke-width="4"></polyline>`;
items.forEach((d,i)=>{
svg+=`<circle cx="${x(i)}" cy="${y(d.value)}" r="5" fill="#2563eb">
<title>${d.label}: ${money(d.value)} | Objetivo ${money(d.target)}</title></circle>`;
svg+=`<text x="${x(i)}" y="${h-18}" text-anchor="middle">${d.label}</text>`;
});
svg+=`</svg>`;
el.innerHTML=svg;
}
function drawBar(rows){
const el=document.getElementById("barChart");
const w=el.clientWidth,h=el.clientHeight;
const pad={l:110,r:25,t:20,b:25};
const dim=dimensionSelect.value;
const metric=metricSelect.value;
const g=group(rows,dim);
const items=Object.keys(g).map(k=>({label:k,value:sum(g[k],metric), rows:g[k]}))
.sort((a,b)=>Math.abs(b.value)-Math.abs(a.value));
const max=Math.max(...items.map(d=>Math.abs(d.value)),1)*1.15;
const rowH=(h-pad.t-pad.b)/Math.max(items.length,1);
let svg=`<svg width="${w}" height="${h}">`;
items.forEach((d,i)=>{
const y=pad.t+i*rowH+rowH*.18;
const bw=scale(Math.abs(d.value),0,max,0,w-pad.l-pad.r);
const color=d.value<0 ? "#dc2626" : "#2563eb";
svg+=`<text x="8" y="${y+rowH*.43}">${d.label}</text>`;
svg+=`<rect class="bar-drill" data-label="${d.label}" x="${pad.l}" y="${y}" width="${bw}" height="${rowH*.55}" rx="6" fill="${color}">
<title>${d.label}: ${metric==="units" ? fmt(d.value) : money(d.value)}</title></rect>`;
svg+=`<text x="${pad.l+bw+6}" y="${y+rowH*.42}" fill="#64748b">${metric==="units" ? fmt(d.value) : money(d.value)}</text>`;
});
svg+=`</svg>`;
el.innerHTML=svg;
document.querySelectorAll(".bar-drill").forEach(bar=>{
bar.addEventListener("click",()=>{
const label=bar.getAttribute("data-label");
const dim=dimensionSelect.value;
if(dim==="region"){
document.querySelectorAll('input[name="region"]').forEach(x=>x.checked=(x.value===label));
dimensionSelect.value="product";
}else if(dim==="product"){
document.querySelectorAll('input[name="product"]').forEach(x=>x.checked=(x.value===label));
dimensionSelect.value="segment";
}else if(dim==="segment"){
segmentSelect.value=label;
dimensionSelect.value="month";
}
update();
});
});
}
function drawScatter(rows){
const el=document.getElementById("scatterChart");
const w=el.clientWidth,h=el.clientHeight;
const pad={l:50,r:20,t:20,b:42};
const g=group(rows,d=>d.region+" | "+d.product+" | "+d.segment);
const points=Object.keys(g).map(k=>{
const r=g[k];
const sales=sum(r,"sales");
const margin=sum(r,"margin");
const units=sum(r,"units");
return {label:k, sales, margin, units, marginRate:margin/sales};
});
const maxUnits=Math.max(...points.map(p=>p.units),1);
const maxRate=Math.max(...points.map(p=>p.marginRate),.1);
const minRate=Math.min(...points.map(p=>p.marginRate),0);
let svg=`<svg width="${w}" height="${h}">`;
for(let i=0;i<4;i++){
const yy=scale(i,0,3,h-pad.b,pad.t);
svg+=`<line class="gridline" x1="${pad.l}" x2="${w-pad.r}" y1="${yy}" y2="${yy}"></line>`;
}
points.forEach(p=>{
const x=scale(p.units,0,maxUnits,pad.l,w-pad.r);
const y=scale(p.marginRate,minRate,maxRate,h-pad.b,pad.t);
const color=p.marginRate<0.23 ? "#dc2626" : "#0891b2";
svg+=`<circle cx="${x}" cy="${y}" r="6" fill="${color}" opacity=".8">
<title>${p.label}
Unidades: ${fmt(p.units)}
Margen %: ${pct(p.marginRate)}
Ventas: ${money(p.sales)}</title></circle>`;
});
svg+=`<text x="${w/2}" y="${h-8}" text-anchor="middle">Unidades</text>`;
svg+=`<text x="10" y="18">Margen %</text>`;
svg+=`</svg>`;
el.innerHTML=svg;
}
function drawCompare(rows){
const el=document.getElementById("compareChart");
const w=el.clientWidth,h=el.clientHeight;
const segA=segmentSelect.value==="Todos" ? segments[0] : segmentSelect.value;
const segB=compareSegment.value;
if(segB==="Ninguno"){
el.innerHTML=`<div style="padding:30px;color:#64748b">Selecciona un segmento de comparación para analizar diferencias.</div>`;
return;
}
const rowsAll=filteredData(true);
const items=months.map(m=>{
const a=rowsAll.filter(d=>d.month===m && d.segment===segA);
const b=rowsAll.filter(d=>d.month===m && d.segment===segB);
return {label:m,a:sum(a,"sales"),b:sum(b,"sales")};
}).filter((d,i)=>i>=+fromMonth.value && i<=+toMonth.value);
const pad={l:45,r:20,t:20,b:45};
const max=Math.max(...items.map(d=>Math.max(d.a,d.b)),1)*1.1;
const x=i=>scale(i,0,Math.max(items.length-1,1),pad.l,w-pad.r);
const y=v=>scale(v,0,max,h-pad.b,pad.t);
let svg=`<svg width="${w}" height="${h}">`;
svg+=`<polyline points="${items.map((d,i)=>`${x(i)},${y(d.a)}`).join(" ")}" fill="none" stroke="#2563eb" stroke-width="3"></polyline>`;
svg+=`<polyline points="${items.map((d,i)=>`${x(i)},${y(d.b)}`).join(" ")}" fill="none" stroke="#dc2626" stroke-width="3"></polyline>`;
items.forEach((d,i)=>{
svg+=`<text x="${x(i)}" y="${h-18}" text-anchor="middle">${d.label}</text>`;
svg+=`<circle cx="${x(i)}" cy="${y(d.a)}" r="4" fill="#2563eb"><title>${segA}: ${money(d.a)}</title></circle>`;
svg+=`<circle cx="${x(i)}" cy="${y(d.b)}" r="4" fill="#dc2626"><title>${segB}: ${money(d.b)}</title></circle>`;
});
svg+=`<text x="10" y="15" fill="#2563eb">${segA}</text>`;
svg+=`<text x="90" y="15" fill="#dc2626">${segB}</text>`;
svg+=`</svg>`;
el.innerHTML=svg;
}
function drawAnomalies(rows){
const el=document.getElementById("anomalyList");
const g=group(rows,d=>d.region+" · "+d.product+" · "+d.segment);
const items=Object.keys(g).map(k=>{
const r=g[k];
const sales=sum(r,"sales");
const target=sum(r,"target");
const margin=sum(r,"margin");
return {label:k, deviation:sales-target, marginRate:margin/sales, sales};
}).sort((a,b)=>a.deviation-b.deviation).slice(0,6);
let html=`<div style="font-size:13px">`;
items.forEach(x=>{
const color=x.deviation<0 ? "#dc2626" : "#16a34a";
html+=`<div style="border-bottom:1px solid #e5e7eb;padding:8px 0">
<strong>${x.label}</strong><br>
<span style="color:${color}">Desviación: ${money(x.deviation)}</span> ·
Margen: ${pct(x.marginRate)}
</div>`;
});
html+=`</div>`;
el.innerHTML=html;
}
function updateTable(rows){
const tbody=document.getElementById("detailTable");
const sorted=[...rows].sort((a,b)=>a.monthIndex-b.monthIndex || a.region.localeCompare(b.region));
tbody.innerHTML=sorted.map(d=>`
<tr>
<td>${d.month}</td><td>${d.region}</td><td>${d.product}</td><td>${d.segment}</td>
<td>${money(d.sales)}</td><td>${money(d.target)}</td>
<td style="color:${d.sales-d.target<0 ? '#dc2626':'#16a34a'}">${money(d.sales-d.target)}</td>
<td>${fmt(d.units)}</td><td>${money(d.margin)}</td>
</tr>
`).join("");
}
function update(){
const rows=filteredData();
const sales=sum(rows,"sales");
const margin=sum(rows,"margin");
const units=sum(rows,"units");
const target=sum(rows,"target");
kpiSales.textContent=money(sales);
kpiMargin.textContent=money(margin);
kpiUnits.textContent=fmt(units);
kpiTarget.textContent=money(target);
kpiAttainment.textContent=target ? pct(sales/target) : "-";
const context=[
months[+fromMonth.value]+"-"+months[+toMonth.value],
checkedValues("region").length+" regiones",
checkedValues("product").length+" productos",
segmentSelect.value
].join(" · ");
activeContext.textContent=context;
drawLine(rows);
drawBar(rows);
drawScatter(rows);
drawCompare(rows);
drawAnomalies(rows);
updateTable(rows);
}
window.addEventListener("resize",update);
setupControls();
update();
</script>
</body>
</html>8. Comparación visual de los dos dashboards
Dashboard aclaratorio
Flujo analítico:
Datos → Selección → Narrativa → Conclusión → Decisión
El diseñador selecciona los datos relevantes, construye una narrativa y dirige al usuario hacia una conclusión.
Características visibles:
- Menos filtros.
- Más texto explicativo.
- KPIs priorizados.
- Gráficos con intención narrativa.
- Colores usados para destacar desviaciones.
- Conclusión visible sin explorar.
Dashboard exploratorio
Flujo analítico:
Datos → Pregunta → Exploración → Filtro → Comparación → Nueva hipótesis → Descubrimiento
El usuario formula preguntas, filtra, compara y cambia de perspectiva.
Características visibles:
- Más filtros.
- Más dimensiones.
- Más interacción.
- Más densidad informativa.
- Tablas de detalle.
- Tooltips.
- Drill-down.
- Comparación dinámica.
La diferencia fundamental no está en usar más o menos filtros, sino en:
Quién controla el recorrido analítico: el diseñador o el usuario.
9. Diseñar un dashboard HTML pensando en su futura implementación en Power BI
HTML puede ser muy útil para prototipar dashboards, validar diseños e ilustrar comportamientos. Sin embargo, es importante entender una limitación clave:
Un dashboard HTML no se exporta directamente a Power BI como si fuera un archivo
.pbix.
Hay que distinguir entre:
- Exportar/importar directamente
No existe una conversión automática estándar de HTML, CSS y JavaScript a Power BI. - Reutilizar el modelo de datos
Sí es posible reutilizar la estructura conceptual: tablas, dimensiones, hechos y relaciones. - Reutilizar las métricas
Las métricas definidas conceptualmente pueden convertirse en medidas DAX. - Reutilizar la estructura conceptual del dashboard
Layout, jerarquía visual, páginas y flujo analítico sí pueden trasladarse. - Reimplementar visualizaciones en Power BI
Los gráficos deben reconstruirse usando visuales nativos o personalizados. - Utilizar HTML como prototipo
HTML es excelente para validar diseño antes de construir el informe definitivo.
Tabla de equivalencias
| Elemento HTML | Equivalente en Power BI | Facilidad de migración | Observaciones |
|---|---|---|---|
| KPI | Card, KPI visual | Alta | Medidas DAX equivalentes |
| Tarjetas | Cards, multi-row cards | Alta | Fácil si las métricas están claras |
| Gráfico de líneas | Line chart | Alta | Eje temporal y medidas |
| Barras | Bar/column chart | Alta | Visual nativo |
| Ranking | Bar chart ordenado, Top N | Alta | Puede requerir filtro Top N |
| Tabla | Table, Matrix | Alta | Directo si el modelo está bien definido |
| Filtros | Filter pane | Alta | Equivalente natural |
| Segmentadores | Slicers | Alta | Muy fácil |
| Drill-down | Jerarquías, drill mode | Alta | Mejor con dimensiones jerárquicas |
| Tooltips | Report page tooltip | Media-alta | Pueden ser más potentes en Power BI |
| Navegación | Buttons, bookmarks, pages | Media | Requiere configuración manual |
| Anotaciones | Text boxes, shapes | Alta | Fácil de replicar |
| Texto explicativo | Text box, smart narrative | Alta | Power BI permite texto fijo o dinámico |
| Colores | Theme JSON | Alta | Conviene definir paleta |
| Temas | Power BI themes | Alta | Mejor si se documenta desde el prototipo |
| Layout | Canvas Power BI | Media | Hay que reconstruirlo manualmente |
| Interacciones | Edit interactions, slicers | Media | No todas las interacciones JS son replicables |
| JavaScript | No equivalente nativo | Baja | Salvo visuales personalizados |
| CSS | Tema, formato visual | Media-baja | No se migra directamente |
| Lógica de negocio | DAX, Power Query, modelo semántico | Alta si está documentada | Mala si está oculta en JS |
10. Arquitectura recomendada
Una buena arquitectura para diseñar primero en HTML y después llevar a Power BI sería:
Datos → Modelo semántico → Métricas → Visualización → Interacción
1. Datos
Definir claramente:
- Tabla de hechos: ventas.
- Dimensiones: fecha, región, producto, cliente/segmento.
- Campos numéricos: ventas, objetivo, unidades, margen.
2. Modelo semántico
Separar dimensiones y hechos:
FactVentasDimFechaDimRegionDimProductoDimSegmento
3. Métricas
Definir medidas independientes de la visualización.
Ejemplos conceptuales:
Ventas = SUM(FactVentas[Ventas])
Objetivo = SUM(FactVentas[Objetivo])
Margen = SUM(FactVentas[Margen])
Desviacion = [Ventas] - [Objetivo]
Cumplimiento = DIVIDE([Ventas], [Objetivo])
Variacion Mes Anterior =
DIVIDE(
[Ventas] - [Ventas Mes Anterior],
[Ventas Mes Anterior]
)4. Visualización
Construir los gráficos a partir de medidas reutilizables, no de cálculos escondidos dentro del frontend.
5. Interacción
Diseñar interacciones que tengan equivalente en Power BI:
- Slicers.
- Drill-down.
- Tooltips.
- Cross-filtering.
- Bookmarks.
- Navegación entre páginas.
Recomendación clave
Evita introducir cálculos de negocio complejos dentro de JavaScript si luego quieres llevar el dashboard a Power BI.
Mala idea:
if(region === "Norte" && product === "Software"){
scoreRiesgo = ventas * 0.73 - objetivo * 1.18;
}Mejor:
- Definir
Desviación. - Definir
Cumplimiento. - Definir
Margen %. - Definir reglas de clasificación documentadas.
- Convertirlas posteriormente a DAX.
11. Mala migración vs buena migración
Mala práctica
Una mala estrategia consiste en crear un dashboard HTML espectacular cuya lógica depende completamente de:
- JavaScript.
- Cálculos realizados en el frontend.
- Datos embebidos sin modelo.
- Interacciones imposibles de reproducir en Power BI.
- CSS demasiado específico.
- Gráficos sin equivalente en visuales nativos.
- Animaciones decorativas.
- Métricas no documentadas.
Problema:
El prototipo se ve bien, pero no sirve como base real para Power BI. Hay que reinterpretarlo y reconstruirlo desde cero.
Buena práctica
Una buena estrategia consiste en diseñar el prototipo HTML con:
- Modelo de datos claro.
- Dimensiones y medidas bien definidas.
- Métricas reutilizables.
- Visualizaciones estándar.
- Interacciones equivalentes a Power BI.
- Jerarquía visual reproducible.
- Diseño basado en componentes.
- Paleta de colores documentada.
- Reglas de negocio separadas de la presentación.
Ventaja:
El prototipo HTML se convierte en una especificación funcional y visual para Power BI.
12. Casos de uso empresariales
| Caso de uso | Objetivo | Usuario | Tipo de preguntas | Enfoque recomendado | Por qué |
|---|---|---|---|---|---|
| Dashboard de dirección | Comunicar situación y decisiones | Comité ejecutivo | ¿Vamos bien o mal? ¿Dónde actuar? | Aclaratorio | Necesita síntesis y foco |
| Seguimiento comercial | Controlar ventas y objetivos | Director comercial, responsables regionales | ¿Quién cumple? ¿Dónde hay desviaciones? | Mixto | Resumen ejecutivo + exploración por equipo |
| Control financiero | Explicar desviaciones presupuestarias | CFO, controller | ¿Por qué nos desviamos del presupuesto? | Aclaratorio | Importa explicar causas y decisiones |
| Investigación de clientes | Entender comportamiento de segmentos | Analistas, CRM | ¿Qué clientes cambian de patrón? | Exploratorio | Requiere segmentar y comparar |
| Análisis de marketing | Evaluar campañas y audiencias | Marketing analyst | ¿Qué canal funciona mejor? | Exploratorio | Muchas hipótesis y dimensiones |
| Detección de anomalías | Encontrar comportamientos atípicos | Riesgo, operaciones, datos | ¿Qué valores se salen del patrón? | Exploratorio | Requiere investigar excepciones |
| Control operativo | Monitorizar procesos | Operaciones | ¿Hay incidencias críticas? | Mixto | Alertas aclaratorias + detalle exploratorio |
| Análisis de inventario | Optimizar stock y rotación | Supply chain | ¿Dónde hay exceso o rotura? | Mixto | KPIs claros y análisis por SKU |
13. ¿Cuál de los dos es mejor?
Ninguno es universalmente mejor.
El análisis aclaratorio es mejor cuando necesitamos:
- Comunicar una conclusión.
- Facilitar una decisión ejecutiva.
- Explicar una desviación.
- Presentar resultados a dirección.
- Reducir ambigüedad.
- Alinear a la organización.
El análisis exploratorio es mejor cuando necesitamos:
- Investigar causas.
- Descubrir patrones.
- Generar hipótesis.
- Comparar segmentos.
- Analizar anomalías.
- Profundizar en datos multidimensionales.
En muchos entornos empresariales ambos enfoques deben convivir.
Arquitectura recomendada en Power BI
Un informe Power BI podría estructurarse así:
| Página | Enfoque | Propósito |
|---|---|---|
| Página 1 | Aclaratorio | Resumen ejecutivo |
| Página 2 | Aclaratorio/diagnóstico | Explicación de desviaciones |
| Página 3 | Exploratorio | Análisis libre por dimensiones |
| Página 4 | Detalle | Tabla transaccional o granular |
Ejemplo:
- Página 1 → Resumen ejecutivo / análisis aclaratorio
KPIs principales, tendencia, conclusión. - Página 2 → Explicación de desviaciones
Descomposición por región, producto y segmento. - Página 3 → Análisis exploratorio
Segmentadores, drill-down, comparaciones. - Página 4 → Detalle
Tabla filtrable con registros.
Esta combinación permite que dirección tenga claridad y que los analistas tengan profundidad.
14. Conclusión
El diseño de un dashboard debe comenzar por la pregunta analítica y no por el gráfico.
Antes de elegir una visualización, una paleta de colores o una herramienta, debemos decidir si queremos:
Explicar lo que sabemos
o
Descubrir lo que todavía no sabemos
Si el objetivo es comunicar una conclusión, el dashboard debe ser aclaratorio: jerárquico, enfocado, narrativo y orientado a decisión.
Si el objetivo es investigar, el dashboard debe ser exploratorio: flexible, interactivo, multidimensional y abierto a nuevas hipótesis.
El error habitual consiste en mezclar ambos enfoques sin intención: dashboards ejecutivos llenos de filtros innecesarios o dashboards analíticos tan guiados que impiden descubrir nada nuevo.
Un buen profesional de BI no diseña simplemente gráficos. Diseña experiencias analíticas.
Checklist: ¿Estoy diseñando un dashboard aclaratorio o exploratorio?
Responde estas preguntas antes de diseñar:
- ¿Ya conozco la conclusión principal?
- ¿El usuario necesita decidir rápido?
- ¿El dashboard será usado por dirección?
- ¿Necesito contar una historia con datos?
- ¿Hay una desviación concreta que explicar?
- ¿Puedo eliminar filtros sin perder valor?
- ¿La información principal debe verse sin clics?
- ¿Necesito destacar una causa principal?
- ¿El texto explicativo aporta claridad?
- ¿El usuario espera investigar hipótesis?
- ¿Necesita cambiar dimensiones libremente?
- ¿Los filtros son parte central del análisis?
- ¿Hace falta drill-down?
- ¿Debe haber tabla de detalle?
- ¿El usuario comparará segmentos?
- ¿Se buscan anomalías o patrones ocultos?
- ¿La navegación debe ser guiada o libre?
- ¿Estoy diseñando para comunicar o para descubrir?
- ¿Las métricas están definidas fuera de la visualización?
- ¿El diseño podrá trasladarse a Power BI sin rehacer toda la lógica?
Tabla final de decisión
| Si tu objetivo es… | Utiliza… |
|---|---|
| Comunicar una conclusión | Análisis aclaratorio |
| Investigar una causa | Análisis exploratorio |
| Contar una historia | Análisis aclaratorio |
| Descubrir patrones | Análisis exploratorio |
| Facilitar una decisión ejecutiva | Análisis aclaratorio |
| Investigar hipótesis | Análisis exploratorio |
| Explicar una desviación presupuestaria | Análisis aclaratorio |
| Comparar muchos segmentos | Análisis exploratorio |
| Presentar resultados a dirección | Análisis aclaratorio |
| Analizar anomalías | Análisis exploratorio |
| Reducir ruido informativo | Análisis aclaratorio |
| Dar libertad analítica al usuario | Análisis exploratorio |
| Diseñar una página ejecutiva en Power BI | Análisis aclaratorio |
| Diseñar una página de investigación en Power BI | Análisis exploratorio |
| Priorizar una acción concreta | Análisis aclaratorio |
| Entender comportamientos no evidentes | Análisis exploratorio |
