📊

Línea elegante, actual vs año anterior

Una sola línea suave cuenta la tendencia; una segunda, punteada y discreta, la compara contra el mismo mes del año anterior. El área bajo la línea del año en curso es puramente decorativa —el eje no arranca en cero, así que quien codifica la magnitud es la línea, no el área—, el eje X marca el año completo solo en enero y julio con una divisoria sutil en el cambio de año, y el último punto lleva su valor y variación interanual escritos junto a la línea en vez de una leyenda.

💡 Cuándo usarlo: Cuando quieras contar tendencia y contexto interanual en un solo vistazo, sin saturar con más de dos series. Si necesitas comparar más de dos periodos a la vez, apilar más líneas punteadas deja de leerse — usa small multiples o un heatmap calendario en su lugar. Y si tu historia no tiene un año anterior completo, quita la línea punteada antes de publicar: un comparativo con huecos disfrazados de cero miente tanto como una barra que no arranca en cero.

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

Vista previa en vivo

Render con datos de muestra · Vega/Vega-Lite

El tooltip por mes y el realce del último punto corren EN VIVO aquí en la web. El atenuado por `__selected__` al hacer clic en un punto 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 spec .json

🧩 Campos que espera

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

Arrastra tus campos a Values en este orden.

🎯 Técnica

Motor
Vega-Lite 6.4.1
Marks
area, line, point, rule, 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 , Sales PY .
  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": "fc622293-6037-4425-af9b-ffe1e1d0fd3b",
      "generated": "2026-08-18T00:00:00.000Z",
      "previewImageBase64PNG": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
      "name": "Línea elegante, actual vs año anterior",
      "description": "Línea de ventas mes a mes con área degradada decorativa bajo el año en curso y una línea punteada de comparación contra el mismo mes del año anterior. Eje X con un solo tick por trimestre; el último punto lleva su valor y variación interanual escritos junto a la línea. Autor: Cristobal Salcedo Beltrán.",
      "author": "Cristobal Salcedo Beltrán"
    },
    "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\": \"#4B5563\",\n    \"labelFontSize\": 12,\n    \"titleColor\": \"#0F172A\",\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": "Ventas del periodo — línea sólida y área degradada decorativa",
        "kind": "measure",
        "type": "numeric"
      },
      {
        "key": "__2__",
        "name": "Sales PY",
        "description": "Ventas del mismo mes, año anterior — línea punteada de comparación (SAMEPERIODLASTYEAR; en blanco cuando no hay año anterior, nunca cero)",
        "kind": "measure",
        "type": "numeric"
      }
    ]
  },
  "config": {
    "background": "transparent",
    "font": "Segoe UI",
    "view": {
      "stroke": "transparent"
    },
    "axis": {
      "grid": false,
      "domain": false,
      "ticks": false,
      "labelColor": "#4B5563",
      "labelFontSize": 12,
      "titleColor": "#0F172A",
      "titleFontWeight": "normal"
    },
    "text": {
      "font": "Segoe UI"
    }
  },
  "title": {
    "text": "Ventas, mes a mes · 2023–2024",
    "subtitle": [
      "Línea sólida: año en curso · línea punteada: mismo mes, año anterior",
      "Contoso Retail · haz clic en un mes para filtrar el resto del informe"
    ],
    "anchor": "start",
    "font": "Segoe UI",
    "fontSize": 19,
    "fontWeight": 600,
    "color": "#0F172A",
    "subtitleFont": "Segoe UI",
    "subtitleFontSize": 12,
    "subtitleFontWeight": "normal",
    "subtitleColor": "#5B6675",
    "subtitleLineHeight": 17,
    "subtitlePadding": 10,
    "offset": 18
  },
  "height": 340,
  "padding": {
    "left": 4,
    "top": 4,
    "bottom": 4,
    "right": 16
  },
  "data": {
    "name": "dataset"
  },
  "transform": [
    {
      "calculate": "['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'][+slice(datum['__0__'],-2)-1] + ' ' + slice(datum['__0__'],0,4)",
      "as": "periodoLabel"
    },
    {
      "calculate": "isValid(datum['__2__']) ? (datum['__1__'] - datum['__2__']) / datum['__2__'] : null",
      "as": "yoy"
    },
    {
      "window": [
        {
          "op": "row_number",
          "as": "ordenReciente"
        }
      ],
      "sort": [
        {
          "field": "__0__",
          "order": "descending"
        }
      ]
    },
    {
      "window": [
        {
          "op": "lag",
          "field": "__0__",
          "as": "yearMonthPrev"
        }
      ]
    }
  ],
  "encoding": {
    "x": {
      "field": "__0__",
      "type": "nominal",
      "sort": "ascending",
      "title": null,
      "scale": {
        "paddingInner": 0,
        "paddingOuter": 0.02
      },
      "axis": {
        "grid": false,
        "domain": false,
        "ticks": false,
        "labelAngle": 0,
        "labelPadding": 12,
        "labelFontSize": 12,
        "labelColor": "#475569",
        "labelFontWeight": 500,
        "labelExpr": "indexof(['01','07'], slice(datum.value,-2)) >= 0 ? (['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic'][+slice(datum.value,-2)-1] + ' ' + slice(datum.value,0,4)) : ''"
      }
    }
  },
  "layer": [
    {
      "description": "Divisoria sutil en el cambio de año — detectada genéricamente (año de la fila != año de la fila anterior, vía window \"lag\"), no hardcodeada a un año literal.",
      "transform": [
        {
          "filter": "isValid(datum.yearMonthPrev) && slice(datum.yearMonthPrev,0,4) != slice(datum['__0__'],0,4)"
        }
      ],
      "mark": {
        "type": "rule",
        "strokeDash": [
          2,
          3
        ],
        "color": "#CBD5E1",
        "strokeWidth": 1
      },
      "encoding": {}
    },
    {
      "description": "Área degradada bajo la línea del año en curso, puramente decorativa: la magnitud la codifica la LÍNEA, no el área — el eje de abajo no arranca en cero (ver 'scale'). Lleva también el único grid horizontal del gráfico.",
      "mark": {
        "type": "area",
        "interpolate": "monotone",
        "line": false,
        "color": {
          "gradient": "linear",
          "x1": 0,
          "y1": 1,
          "x2": 0,
          "y2": 0,
          "stops": [
            {
              "offset": 0,
              "color": "rgba(46,107,224,0)"
            },
            {
              "offset": 1,
              "color": "rgba(46,107,224,0.22)"
            }
          ]
        }
      },
      "encoding": {
        "y": {
          "field": "__1__",
          "type": "quantitative",
          "title": null,
          "scale": {
            "zero": false,
            "nice": true,
            "padding": 10
          },
          "axis": {
            "grid": true,
            "gridColor": "#E2E8F0",
            "gridDash": [
              2,
              2
            ],
            "format": "$.3~s",
            "labelColor": "#4B5563",
            "labelPadding": 8,
            "tickCount": 5
          },
          "stack": null
        }
      }
    },
    {
      "description": "Línea del mismo mes, año anterior (Sales PY). Solo tiene valor a partir del segundo año de historia — igual que SAMEPERIODLASTYEAR en DAX, lo que no tiene comparativo queda en blanco, nunca en cero.",
      "mark": {
        "type": "line",
        "interpolate": "monotone",
        "strokeWidth": 1.75,
        "strokeDash": [
          5,
          3
        ],
        "strokeCap": "round",
        "color": "#9AA5B1"
      },
      "encoding": {
        "y": {
          "field": "__2__",
          "type": "quantitative"
        }
      }
    },
    {
      "description": "Línea del año en curso. No se atenúa con el cross-filter: recorre todos los periodos, y apagarla por el clic de uno solo no diría nada.",
      "mark": {
        "type": "line",
        "interpolate": "monotone",
        "strokeWidth": 3,
        "strokeCap": "round",
        "strokeJoin": "round",
        "color": "#2E6BE0"
      },
      "encoding": {
        "y": {
          "field": "__1__",
          "type": "quantitative"
        }
      }
    },
    {
      "description": "Nodo de cada mes, con halo blanco para leerse sobre la línea y el área. Estos SÍ se atenúan con el cross-filter: cada punto pertenece a un periodo.",
      "mark": {
        "type": "point",
        "filled": true,
        "size": 42,
        "color": "#2E6BE0",
        "stroke": "#FFFFFF",
        "strokeWidth": 1.5
      },
      "encoding": {
        "y": {
          "field": "__1__",
          "type": "quantitative"
        },
        "opacity": {
          "condition": {
            "test": "datum.__selected__ === 'off'",
            "value": 0.25
          },
          "value": 1
        },
        "tooltip": [
          {
            "field": "periodoLabel",
            "type": "nominal",
            "title": "Periodo"
          },
          {
            "field": "__1__",
            "type": "quantitative",
            "title": "Ventas",
            "format": "$,.0f"
          },
          {
            "field": "__2__",
            "type": "quantitative",
            "title": "Año anterior",
            "format": "$,.0f"
          }
        ]
      }
    },
    {
      "description": "Realce del último periodo: halo doble, sin filtro de selección (siempre visible aunque otro punto esté seleccionado).",
      "transform": [
        {
          "filter": "datum.ordenReciente === 1"
        }
      ],
      "mark": {
        "type": "point",
        "filled": true,
        "size": 130,
        "color": "#2E6BE0",
        "stroke": "#FFFFFF",
        "strokeWidth": 2.5
      },
      "encoding": {
        "y": {
          "field": "__1__",
          "type": "quantitative"
        }
      }
    },
    {
      "description": "Valor del último periodo, escrito junto a la línea — sustituye a la leyenda.",
      "transform": [
        {
          "filter": "datum.ordenReciente === 1"
        }
      ],
      "mark": {
        "type": "text",
        "align": "right",
        "dx": -8,
        "dy": 20,
        "fontSize": 12.5,
        "fontWeight": 600,
        "color": "#0F172A"
      },
      "encoding": {
        "y": {
          "field": "__1__",
          "type": "quantitative"
        },
        "text": {
          "value": {
            "expr": "datum.periodoLabel + ' · ' + format(datum['__1__'], '$,.3~s')"
          }
        }
      }
    },
    {
      "description": "Variación interanual del último periodo. Verde/teal si crece, rojo/rosa si cae — el mismo par semántico que la tarjeta bullet IBCS de la galería.",
      "transform": [
        {
          "filter": "datum.ordenReciente === 1"
        },
        {
          "filter": "isValid(datum.yoy)"
        }
      ],
      "mark": {
        "type": "text",
        "align": "right",
        "dx": -8,
        "dy": 36,
        "fontSize": 11,
        "fontWeight": 600
      },
      "encoding": {
        "y": {
          "field": "__1__",
          "type": "quantitative"
        },
        "text": {
          "value": {
            "expr": "(datum.yoy >= 0 ? '+' : '') + format(datum.yoy, '.1%') + ' interanual'"
          }
        },
        "color": {
          "condition": {
            "test": "datum.yoy >= 0",
            "value": "#0F766E"
          },
          "value": "#BE123C"
        }
      }
    }
  ]
}
¡Contáctame!