Search code examples
vega-lite

Vega lite KPI Card with target line and conditional text


How can I create a Vega lite KPI Card with target line, conditional text and gradient area chart showing trend?


Solution

  • Try this spec and adjust as needed.

    enter image description here

    {
      "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
      "usermeta": {"embedOptions": {"renderer": "svg"}},
      "title": {
        "text": "Sales YTD",
        "anchor": "start",
        "offset": -17,
        "font": "Arial",
        "fontSize": 16,
        "fontWeight": 400,
        "color": "#a0a0a0"
      },
      "data": {
        "values": [
          {"date": "2023-01-01", "value": 6000, "target": 10000},
          {"date": "2023-02-01", "value": 8000, "target": 10000},
          {"date": "2023-03-01", "value": 9000, "target": 10000},
          {"date": "2023-04-01", "value": 10000, "target": 12000},
          {"date": "2023-05-01", "value": 8000, "target": 12000},
          {"date": "2023-06-01", "value": 14000, "target": 12000},
          {"date": "2023-07-01", "value": 6000, "target": 14000},
          {"date": "2023-08-01", "value": 15000, "target": 14000},
          {"date": "2023-09-01", "value": 18000, "target": 14000},
          {"date": "2023-10-01", "value": 12000, "target": 16000},
          {"date": "2023-11-01", "value": 12000, "target": 16000},
          {"date": "2023-12-01", "value": 14000, "target": 16000}
        ]
      },
      "height": 100,
      "width": 250,
      "layer": [
        {
          "mark": {
            "type": "area",
            "color": {
              "x1": 1,
              "y1": 1,
              "x2": 1,
              "y2": 0,
              "gradient": "linear",
              "stops": [
                {"offset": 0, "color": "white"},
                {"offset": 1, "color": "#ebebeb"}
              ]
            }
          },
          "encoding": {
            "x": {"field": "date", "type": "temporal", "axis": null},
            "y": {"field": "value", "type": "quantitative", "axis": null}
          }
        },
        {
          "mark": {
            "type": "line",
            "color": "#9c9c9c",
            "strokeWidth": 1,
            "strokeDash": [3, 3]
          },
          "encoding": {
            "x": {"field": "date", "type": "temporal", "axis": null},
            "y": {"field": "target", "type": "quantitative", "axis": null}
          }
        },
        {
          "mark": {"type": "line", "size": 1, "tooltip": true},
          "encoding": {
            "color": {"value": "#000"},
            "x": {"field": "date", "type": "temporal"},
            "y": {"field": "value", "type": "quantitative"}
          }
        },
        {
          "transform": [
            {
              "joinaggregate": [{"op": "max", "field": "date", "as": "latest_date"}]
            },
            {"filter": "datum.date == datum.latest_date"}
          ],
          "mark": {
            "type": "point",
            "tooltip": true,
            "fill": "white",
            "strokeWidth": 1
          },
          "encoding": {
            "color": {
              "condition": {
                "test": "datum.value >= datum.target",
                "value": "green"
              },
              "value": "red"
            },
            "opacity": {"value": 1},
            "size": {"value": 70},
            "x": {"field": "date", "type": "temporal"},
            "y": {"field": "value", "type": "quantitative"}
          }
        },
        {
          "transform": [
            {"joinaggregate": [{"op": "max", "field": "value", "as": "maxVal"}]},
            {
              "joinaggregate": [{"op": "max", "field": "date", "as": "latest_date"}]
            },
            {"filter": "datum.date == datum.latest_date"},
            {
              "calculate": "timeFormat(datum.date,'%b-%Y') + ': '+ format(datum.value,'$,.2s')",
              "as": "Title"
            }
          ],
          "mark": {
            "type": "text",
            "dx": 0,
            "dy": -45,
            "xOffset": 0,
            "yOffset": -10,
            "angle": 0,
            "align": "right",
            "baseline": "bottom",
            "font": "sans-serif",
            "fontSize": 18,
            "fontStyle": "normal",
            "fontWeight": "normal",
            "limit": 0
          },
          "encoding": {
            "x": {"field": "date", "type": "temporal"},
            "y": {"field": "maxVal", "type": "quantitative"},
            "text": {"field": "Title"}
          }
        },
        {
          "transform": [
            {"joinaggregate": [{"op": "max", "field": "value", "as": "maxVal"}]},
            {
              "joinaggregate": [{"op": "max", "field": "date", "as": "latest_date"}]
            },
            {"filter": "datum.date == datum.latest_date"},
            {
              "calculate": "'⚋ Target: '+ format(datum.target,'$,.2s')",
              "as": "targetText"
            }
          ],
          "mark": {
            "type": "text",
            "dx": 0,
            "dy": -25,
            "xOffset": 0,
            "yOffset": -10,
            "angle": 0,
            "align": "right",
            "baseline": "bottom",
            "font": "sans-serif",
            "fontSize": 12,
            "fontStyle": "normal",
            "fontWeight": "normal",
            "limit": 0,
            "color": {"expr": "datum.value < datum.target ? 'red' : 'green'"}
          },
          "encoding": {
            "x": {"field": "date", "type": "temporal"},
            "y": {"field": "maxVal", "type": "quantitative"},
            "text": {"field": "targetText"}
          }
        },
        {
          "transform": [
            {"joinaggregate": [{"op": "max", "field": "value", "as": "maxVal"}]},
            {"calculate": "toDate(datum.date)", "as": "parsed_date"},
            {
              "window": [{"op": "rank", "as": "rank"}],
              "sort": [{"field": "parsed_date", "order": "descending"}]
            },
            {"filter": "datum.rank == 1 || datum.rank == 2"},
            {"calculate": "datum.rank == 1 ? datum.value : 0", "as": "val1"},
            {"calculate": "datum.rank == 2 ? datum.value : 0", "as": "val2"},
            {"joinaggregate": [{"op": "max", "field": "val1", "as": "val1X"}]},
            {"joinaggregate": [{"op": "max", "field": "val2", "as": "val2X"}]},
            {"calculate": "max(datum.val1X) - max(datum.val2X)", "as": "finalVal"},
            {
              "calculate": "datum.rank == 1 ? (datum.finalVal > 0 ? '▲ ' : datum.finalVal < 0 ? '▼ ' : '▷ ') + 'Change this month: ' + (datum.finalVal > 0 ? '+' : '') + format(datum.finalVal,'$,.2s') : ''",
              "as": "subTitle"
            }
          ],
          "mark": {
            "type": "text",
            "dx": 0,
            "dy": -15,
            "xOffset": 0,
            "yOffset": -15,
            "angle": 0,
            "align": "right",
            "baseline": "top",
            "font": "sans-serif",
            "fontSize": 12,
            "fontStyle": "normal",
            "fontWeight": "normal",
            "limit": 0,
            "color": {
              "expr": "datum.finalVal < 0 ? 'red' : datum.finalVal > 0 ? 'green' : 'gray'"
            }
          },
          "encoding": {
            "x": {"field": "date", "type": "temporal"},
            "y": {"field": "maxVal", "type": "quantitative"},
            "text": {"field": "subTitle"}
          }
        }
      ],
      "view": {"stroke": "transparent"},
      "config": {}
    }