|

Análisis aclaratorio vs análisis exploratorio en dashboards: cómo diseñarlos, cuándo usarlos y cómo llevarlos a Power BI

0
(0)

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:

  1. Análisis aclaratorio, también llamado explanatory o clarificatory analysis.
  2. 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:

  1. Situación general.
  2. Desviación frente al objetivo.
  3. Identificación del foco del problema.
  4. Causa principal.
  5. 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

CriterioDashboard aclaratorioDashboard exploratorio
ObjetivoComunicar una conclusiónDescubrir patrones o hipótesis
Usuario principalDirección, negocio, stakeholdersAnalistas, controllers, equipos de BI
Tipo de preguntaCerrada o semiestructuradaAbierta e investigativa
Ejemplo de pregunta“¿Por qué caímos frente al objetivo?”“¿Qué segmentos explican distintos comportamientos?”
Nivel de interacciónBajo o moderadoAlto
Cantidad de informaciónSeleccionada y resumidaAmplia y multidimensional
Libertad del usuarioLimitadaAlta
StorytellingMuy importanteSecundario
NavegaciónLineal o guiadaFlexible
FiltrosPocos y deliberadosMuchos y dinámicos
Drill-downLimitado, si aporta a la historiaCentral para investigar
KPIsPocos, priorizadosVarios, cambiables o segmentables
GráficosSeleccionados para explicarSeleccionados para comparar y explorar
Complejidad visualBaja o moderadaModerada o alta
Riesgo de sobrecargaBajo si está bien diseñadoAlto si no se estructura bien
Toma de decisionesRápida, ejecutivaAnalítica, diagnóstica
Caso de uso típicoComité de direcciónAnálisis comercial o financiero detallado
Rol del diseñadorGuía la lecturaCrea un entorno de investigación
Rol del usuarioInterpreta y decideExplora, filtra y compara
Relación con Power BIPágina ejecutiva, resumen, narrativaPágina analítica, drill-down, segmentadores
MétricasPreseleccionadasParametrizables o comparables
Texto explicativoMuy recomendableMenos frecuente, más contextual
AnotacionesClave para dirigir la atenciónÚtiles para explicar hallazgos puntuales
Tablas detalladasLimitadas o ausentesFrecuentes
TooltipsComplementariosMuy importantes
Diseño visualJerarquía fuerteFlexibilidad 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:

  1. Exportar/importar directamente
    No existe una conversión automática estándar de HTML, CSS y JavaScript a Power BI.
  2. Reutilizar el modelo de datos
    Sí es posible reutilizar la estructura conceptual: tablas, dimensiones, hechos y relaciones.
  3. Reutilizar las métricas
    Las métricas definidas conceptualmente pueden convertirse en medidas DAX.
  4. Reutilizar la estructura conceptual del dashboard
    Layout, jerarquía visual, páginas y flujo analítico sí pueden trasladarse.
  5. Reimplementar visualizaciones en Power BI
    Los gráficos deben reconstruirse usando visuales nativos o personalizados.
  6. Utilizar HTML como prototipo
    HTML es excelente para validar diseño antes de construir el informe definitivo.

Tabla de equivalencias

Elemento HTMLEquivalente en Power BIFacilidad de migraciónObservaciones
KPICard, KPI visualAltaMedidas DAX equivalentes
TarjetasCards, multi-row cardsAltaFácil si las métricas están claras
Gráfico de líneasLine chartAltaEje temporal y medidas
BarrasBar/column chartAltaVisual nativo
RankingBar chart ordenado, Top NAltaPuede requerir filtro Top N
TablaTable, MatrixAltaDirecto si el modelo está bien definido
FiltrosFilter paneAltaEquivalente natural
SegmentadoresSlicersAltaMuy fácil
Drill-downJerarquías, drill modeAltaMejor con dimensiones jerárquicas
TooltipsReport page tooltipMedia-altaPueden ser más potentes en Power BI
NavegaciónButtons, bookmarks, pagesMediaRequiere configuración manual
AnotacionesText boxes, shapesAltaFácil de replicar
Texto explicativoText box, smart narrativeAltaPower BI permite texto fijo o dinámico
ColoresTheme JSONAltaConviene definir paleta
TemasPower BI themesAltaMejor si se documenta desde el prototipo
LayoutCanvas Power BIMediaHay que reconstruirlo manualmente
InteraccionesEdit interactions, slicersMediaNo todas las interacciones JS son replicables
JavaScriptNo equivalente nativoBajaSalvo visuales personalizados
CSSTema, formato visualMedia-bajaNo se migra directamente
Lógica de negocioDAX, Power Query, modelo semánticoAlta si está documentadaMala 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:

  • FactVentas
  • DimFecha
  • DimRegion
  • DimProducto
  • DimSegmento

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 usoObjetivoUsuarioTipo de preguntasEnfoque recomendadoPor qué
Dashboard de direcciónComunicar situación y decisionesComité ejecutivo¿Vamos bien o mal? ¿Dónde actuar?AclaratorioNecesita síntesis y foco
Seguimiento comercialControlar ventas y objetivosDirector comercial, responsables regionales¿Quién cumple? ¿Dónde hay desviaciones?MixtoResumen ejecutivo + exploración por equipo
Control financieroExplicar desviaciones presupuestariasCFO, controller¿Por qué nos desviamos del presupuesto?AclaratorioImporta explicar causas y decisiones
Investigación de clientesEntender comportamiento de segmentosAnalistas, CRM¿Qué clientes cambian de patrón?ExploratorioRequiere segmentar y comparar
Análisis de marketingEvaluar campañas y audienciasMarketing analyst¿Qué canal funciona mejor?ExploratorioMuchas hipótesis y dimensiones
Detección de anomalíasEncontrar comportamientos atípicosRiesgo, operaciones, datos¿Qué valores se salen del patrón?ExploratorioRequiere investigar excepciones
Control operativoMonitorizar procesosOperaciones¿Hay incidencias críticas?MixtoAlertas aclaratorias + detalle exploratorio
Análisis de inventarioOptimizar stock y rotaciónSupply chain¿Dónde hay exceso o rotura?MixtoKPIs 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áginaEnfoquePropósito
Página 1AclaratorioResumen ejecutivo
Página 2Aclaratorio/diagnósticoExplicación de desviaciones
Página 3ExploratorioAnálisis libre por dimensiones
Página 4DetalleTabla 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:

  1. ¿Ya conozco la conclusión principal?
  2. ¿El usuario necesita decidir rápido?
  3. ¿El dashboard será usado por dirección?
  4. ¿Necesito contar una historia con datos?
  5. ¿Hay una desviación concreta que explicar?
  6. ¿Puedo eliminar filtros sin perder valor?
  7. ¿La información principal debe verse sin clics?
  8. ¿Necesito destacar una causa principal?
  9. ¿El texto explicativo aporta claridad?
  10. ¿El usuario espera investigar hipótesis?
  11. ¿Necesita cambiar dimensiones libremente?
  12. ¿Los filtros son parte central del análisis?
  13. ¿Hace falta drill-down?
  14. ¿Debe haber tabla de detalle?
  15. ¿El usuario comparará segmentos?
  16. ¿Se buscan anomalías o patrones ocultos?
  17. ¿La navegación debe ser guiada o libre?
  18. ¿Estoy diseñando para comunicar o para descubrir?
  19. ¿Las métricas están definidas fuera de la visualización?
  20. ¿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ónAnálisis aclaratorio
Investigar una causaAnálisis exploratorio
Contar una historiaAnálisis aclaratorio
Descubrir patronesAnálisis exploratorio
Facilitar una decisión ejecutivaAnálisis aclaratorio
Investigar hipótesisAnálisis exploratorio
Explicar una desviación presupuestariaAnálisis aclaratorio
Comparar muchos segmentosAnálisis exploratorio
Presentar resultados a direcciónAnálisis aclaratorio
Analizar anomalíasAnálisis exploratorio
Reducir ruido informativoAnálisis aclaratorio
Dar libertad analítica al usuarioAnálisis exploratorio
Diseñar una página ejecutiva en Power BIAnálisis aclaratorio
Diseñar una página de investigación en Power BIAnálisis exploratorio
Priorizar una acción concretaAnálisis aclaratorio
Entender comportamientos no evidentesAnálisis exploratorio

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuarlo!

Puntuación media 0 / 5. Recuento de votos: 0

Hasta ahora, ¡no hay votos!. Sé el primero en puntuar este contenido.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *