📊

Barras y línea con doble eje

Un combo honesto: las barras llevan el importe y la línea el porcentaje, cada una con su propia escala Y. La clave es `resolve: {scale: {y: "independent"}}` — sin esa línea Vega-Lite fusiona las dos escalas y el porcentaje queda aplastado contra el cero frente a los millones. Clic en un mes para filtrar el resto del informe.

💡 Cuándo usarlo: Cuando las dos series tienen UNIDADES DISTINTAS (dinero y porcentaje, unidades y precio medio): ahí el doble eje informa. Si las dos son de la misma unidad, no lo uses — dos series en un eje único comparan de verdad y el doble eje solo exagera. Y recuerda que el eje derecho no arranca en cero a propósito: es una perilla, no un descuido.

Vega-Lite combo intermedio ✓ Cross-filter ✓ Tooltip ✓ Context menu ✓ Selección

Vista previa en vivo

Render con datos de muestra · Vega/Vega-Lite

El tooltip combinado (periodo, importe y porcentaje en una sola tarjeta) corre EN VIVO aquí en la web. El atenuado por `__selected__` lo enciende Power BI cuando otro visual filtra este; la galería simula el clic en la propia vista.

Cargando vista previa…

Vista previa renderizada con el runtime de Deneb · MIT © Daniel Marsh-Patrick

🔽 Descargar .pbix 🔽 Descargar spec .json

🧩 Campos que espera

  1. __0__ YearMonth texto
  2. __1__ Total Sales número medida
  3. __2__ Margin % número medida

Arrastra tus campos a Values en este orden.

🎯 Técnica

Motor
Vega-Lite 6.4.1
Marks
bar, line, point, text

Cómo usarlo en Power BI

  1. Agrega el visual Deneb a tu reporte e ingresa a su editor.
  2. Arrastra tus campos a Values en este orden: YearMonth , Total Sales , Margin % .
  3. Pega el spec (botón «Copiar spec» arriba) en el editor JSON de Deneb.
  4. Mapea tus columnas y ajusta a tu gusto. La interactividad nativa ya viene configurada.
Ver el spec Vega-Lite completo
spec.vega-lite.json
{
  "$schema": "https://vega.github.io/schema/vega-lite/v6.json",
  "usermeta": {
    "information": {
      "uuid": "ed9a6ce2-9feb-4331-a7b7-9c04f7a8d615",
      "generated": "2026-08-14T03:03:36.904Z",
      "previewImageBase64PNG": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
      "name": "Barras y línea con doble eje",
      "description": "Combo de barras y línea con dos escalas Y independientes: la barra lleva el importe y la línea el porcentaje. Autor: Cristobal Salcedo Beltrán.",
      "author": "Cristobal Salcedo"
    },
    "deneb": {
      "build": "1.9.0",
      "metaVersion": 1,
      "provider": "vegaLite",
      "providerVersion": "6.4.1"
    },
    "interactivity": {
      "tooltip": true,
      "contextMenu": true,
      "selection": true,
      "selectionMode": "simple",
      "highlight": false,
      "dataPointLimit": 50
    },
    "config": "{\n  \"background\": \"transparent\",\n  \"font\": \"Segoe UI\",\n  \"view\": {\n    \"stroke\": \"transparent\"\n  },\n  \"axis\": {\n    \"grid\": false,\n    \"domain\": false,\n    \"ticks\": false,\n    \"labelColor\": \"#605E5C\",\n    \"labelFontSize\": 12,\n    \"titleColor\": \"#252423\",\n    \"titleFontWeight\": \"normal\"\n  },\n  \"text\": {\n    \"font\": \"Segoe UI\"\n  }\n}",
    "dataset": [
      {
        "key": "__0__",
        "name": "YearMonth",
        "description": "Periodo del eje X (texto YYYY-MM: ordena igual lexicográfica que cronológicamente)",
        "kind": "column",
        "type": "text"
      },
      {
        "key": "__1__",
        "name": "Total Sales",
        "description": "Importe: altura de las barras, eje izquierdo",
        "kind": "measure",
        "type": "numeric"
      },
      {
        "key": "__2__",
        "name": "Margin %",
        "description": "Porcentaje: altura de la línea, eje derecho",
        "kind": "measure",
        "type": "numeric"
      }
    ]
  },
  "config": {
    "background": "transparent",
    "font": "Segoe UI",
    "view": {
      "stroke": "transparent"
    },
    "axis": {
      "grid": false,
      "domain": false,
      "ticks": false,
      "labelColor": "#605E5C",
      "labelFontSize": 12,
      "titleColor": "#252423",
      "titleFontWeight": "normal"
    },
    "text": {
      "font": "Segoe UI"
    }
  },
  "title": {
    "text": "Ventas y margen, mes a mes · 2024",
    "subtitle": [
      "Barras: ventas netas · Línea: margen bruto sobre ventas",
      "Contoso Retail · haz clic en un mes para filtrar el resto del informe"
    ],
    "anchor": "start",
    "font": "Segoe UI",
    "fontSize": 19,
    "fontWeight": 600,
    "color": "#1B1A19",
    "subtitleFont": "Segoe UI",
    "subtitleFontSize": 12,
    "subtitleFontWeight": "normal",
    "subtitleColor": "#8A8886",
    "subtitleLineHeight": 17,
    "subtitlePadding": 10,
    "offset": 18
  },
  "height": 320,
  "padding": {
    "left": 4,
    "top": 4,
    "bottom": 4,
    "right": 6
  },
  "data": {
    "name": "dataset"
  },
  "transform": [
    {
      "window": [
        {
          "op": "row_number",
          "as": "ordenMargen"
        }
      ],
      "sort": [
        {
          "field": "__2__",
          "order": "descending"
        }
      ]
    }
  ],
  "encoding": {
    "x": {
      "field": "__0__",
      "type": "nominal",
      "sort": "ascending",
      "title": null,
      "scale": {
        "paddingInner": 0.38,
        "paddingOuter": 0.24
      },
      "axis": {
        "labelAngle": 0,
        "labelPadding": 10,
        "grid": false,
        "labelExpr": "['','Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic'][+slice(datum.value, -2)]",
        "labelFontSize": 11
      }
    },
    "tooltip": [
      {
        "field": "__0__",
        "type": "nominal",
        "title": "Periodo"
      },
      {
        "field": "__1__",
        "type": "quantitative",
        "title": "Ventas",
        "format": "$,.0f"
      },
      {
        "field": "__2__",
        "type": "quantitative",
        "title": "Margen",
        "format": ".1%"
      }
    ]
  },
  "layer": [
    {
      "description": "Barras: el importe del periodo. Su escala Y (eje IZQUIERDO) arranca en cero siempre, porque la longitud de la barra es lo que codifica la magnitud. Es la única capa que lleva el grid horizontal.",
      "mark": {
        "type": "bar",
        "cornerRadiusEnd": 3,
        "color": "#9CB8D0"
      },
      "encoding": {
        "y": {
          "field": "__1__",
          "type": "quantitative",
          "title": null,
          "scale": {
            "zero": true,
            "nice": true
          },
          "axis": {
            "grid": true,
            "gridColor": "#F0EEEC",
            "gridWidth": 1,
            "format": "$.3~s",
            "labelPadding": 8,
            "tickCount": 4
          }
        },
        "opacity": {
          "condition": {
            "test": "datum.__selected__ === 'off'",
            "value": 0.25
          },
          "value": 1
        }
      }
    },
    {
      "description": "Las tres capas del porcentaje viven en un grupo anidado con la escala Y COMPARTIDA entre ellas. Sin este anidamiento, el resolve independent de abajo daría a la capa de etiquetas (filtrada a 2 filas) su propio dominio y las etiquetas caerían fuera de la línea.",
      "encoding": {
        "y": {
          "field": "__2__",
          "type": "quantitative",
          "title": null,
          "scale": {
            "zero": false,
            "nice": true,
            "padding": 14
          },
          "axis": {
            "orient": "right",
            "grid": false,
            "format": ".0%",
            "labelColor": "#A85F1B",
            "labelPadding": 8,
            "tickCount": 4
          }
        }
      },
      "resolve": {
        "scale": {
          "y": "shared"
        }
      },
      "layer": [
        {
          "description": "La línea del porcentaje. No se atenúa con el cross-filter: una línea recorre todos los periodos, atenuarla por el clic de uno solo no querría decir nada.",
          "mark": {
            "type": "line",
            "interpolate": "monotone",
            "strokeWidth": 2.5,
            "strokeCap": "round",
            "strokeJoin": "round",
            "color": "#D4762A"
          }
        },
        {
          "description": "Los nodos, con halo blanco para que se lean sobre la barra. Estos SÍ se atenúan con el cross-filter, porque cada punto pertenece a un periodo.",
          "mark": {
            "type": "point",
            "filled": true,
            "size": 58,
            "color": "#D4762A",
            "stroke": "#FFFFFF",
            "strokeWidth": 1.6
          },
          "encoding": {
            "opacity": {
              "condition": {
                "test": "datum.__selected__ === 'off'",
                "value": 0.25
              },
              "value": 1
            }
          }
        },
        {
          "description": "Una sola etiqueta, en el PICO del porcentaje. Nombra la serie (sustituye a la leyenda) y da su valor máximo. Va en el máximo y no en el último punto porque encima de un máximo local el espacio está libre por definición: cualquier otra posición acaba pisando la propia línea.",
          "transform": [
            {
              "filter": "datum.ordenMargen === 1"
            }
          ],
          "mark": {
            "type": "text",
            "align": "center",
            "dy": -16,
            "fontSize": 12,
            "fontWeight": 600,
            "color": "#A85F1B"
          },
          "encoding": {
            "text": {
              "value": {
                "expr": "'Margen ' + format(datum['__2__'], '.1%')"
              }
            }
          }
        }
      ]
    }
  ],
  "resolve": {
    "scale": {
      "y": "independent"
    }
  }
}
¡Contáctame!