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.
Vista previa en vivo
Render con datos de muestra · Vega/Vega-LiteEl 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.
Vista previa renderizada con el runtime de Deneb · MIT © Daniel Marsh-Patrick
🧩 Campos que espera
-
__0__YearMonth texto -
__1__Total Sales número medida -
__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
- Agrega el visual Deneb a tu reporte e ingresa a su editor.
- Arrastra tus campos a Values en este orden: YearMonth , Total Sales , Margin % .
- Pega el spec (botón «Copiar spec» arriba) en el editor JSON de Deneb.
- Mapea tus columnas y ajusta a tu gusto. La interactividad nativa ya viene configurada.
Ver el spec Vega-Lite completo
{
"$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"
}
}
}