---
title: "Bar Charts: Makeover in Vega-Lite"
date:  2025-07-07
draft: false

load_vega: true
tags:
  - dataviz
  - vega-lite
  - makeover
---

## oeuth

Of all tools for data visualization, I mostly enjoy working with Vega-Lite. As I enhance my skills in making the visualizations using it, I become increasingly interesting not just in the "dataviz" part of its capabilities, but also on making it aesthetically compelling, as well as challenging myself to reach the limits of what is possible with Vega-Lite. I am by no means near the limits so far, however I got increasingly interested in replicating the charts I see throughout the internet.

One that caught me attention is Tableau's Viz of the Day: [Seven Bar Charts to Visualise Year-Over-Year Growth](https://public.tableau.com/app/profile/jacob.rothemund/viz/SevenBarChartstoVisualiseYear-Over-YearGrowth/Dashboard)

![](https://static.olehomelchenko.com/blog/tableau-seven-bar-charts-yoy-growth.png)

Here is what my attempt to replicate the charts in vega-lite looks like (each chart can be viewed in vega-editor via burger menu on the right top):

## Initial Dataset

| Category    | Current    | Previous   |
|-------------|------------|------------|
| Appliances  | 22962.174  | 13183.887  |
| Art         | 5122.616   | 4004.32    |
| Binders     | 47133.138  | 29871.657  |
| Envelopes   | 1926.632   | 2698.966   |
| Fasteners   | 604.648    | 667.028    |
| Labels      | 2416.994   | 1276.072   |
| Paper       | 15947.488  | 11683.83   |
| Storage     | 39407.746  | 35680.368  |
| Supplies    | 2727.558   | 7482.186   |

---

## Bar-in-bar

This one looks like it's the most straightofrward one.
Still, before moving forward, we need to perform several transformations that would allow us to calculate percentages and neatly show them as text: 

```json
  {"transform": [
    {
      "calculate": "toNumber(datum.Current)",
      "as": "cur_value"
    },
    {
      "calculate": "toNumber(datum.Previous)",
      "as": "prev_value"
    },
    {
      "calculate": "(datum.cur_value - datum.prev_value) / datum.prev_value",
      "as": "difference"
    },
    {
      "calculate": "datum.difference > 0",
      "as": "is_positive"
    },
    {
      "calculate": "max(datum.cur_value, datum.prev_value)",
      "as": "max_value"
    },
    {
      "calculate": "format(datum.difference, '+.0%') + ' | ' + format(datum.cur_value, '$,.0f')",
      "as": "text_label"
    }
  ]}
```
After this, we can make the following layers:
- Wider `bar` mark with previous value "background"
- Thinner `bar` mark with actual numbers
- `text` mark with calculated `text_label`
- two separate `point` mark layers, each with a different color but more importantly - `triangle-up` and `triangle-down`. We cannot encode the two marks dynamically (or, rather, we cannot make dynamic fill color which is my goal here) so we'll need to do it separately for positive and negative changes.

{{< vega-lite id="chart-bar-in-bar" actions="true">}}
{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "width": 400,
  "height": 400,
  "config": {
    "font": "monospace",
    "axis": {
      "domainOpacity": 0,
      "gridOpacity": 0,
      "tickOpacity": 0,
      "domain": false
    },
    "view": {
      "stroke": null
    },
    "background": "#eee",
    "padding": 20,
    "text": {
      "color": "#444"
    },
    "axisX": {
      "titleColor": "#444"
    },
    "axisY": {
      "labelFontSize": 14,
      "labelColor": "#444"
    }
  },
  "encoding": {
    "y": {
      "field": "Category",
      "sort": {
        "field": "cur_value",
        "order": "descending"
      },
      "title": null
    },
    "x": {
      "field": "cur_value",
      "aggregate": "sum",
      "axis": {
        "grid": false,
        "labels": false,
        "ticks": false
      },
      "title": null
    },
    "tooltip": [
      {
        "field": "Category"
      },
      {
        "field": "cur_value",
        "format": "$,.0f",
        "title": "Current Period"
      },
      {
        "field": "prev_value",
        "format": "$,.0f",
        "title": "Previous Period"
      },
      {
        "field": "difference",
        "format": ",.1%",
        "title": "Difference"
      }
    ]
  },
  "layer": [
    {
      "mark": {
        "type": "bar",
        "opacity": 0.7,
        "color": "lightgrey",
        "size": 34
      },
      "encoding": {
        "x": {
          "field": "Previous",
          "aggregate": "sum"
        }
      }
    },
    {
      "mark": {
        "type": "bar",
        "tooltip": true,
        "opacity": 1,
        "size": 20,
        "color": "slategrey"
      }
    },
    {
      "mark": {
        "type": "text",
        "xOffset": 15,
        "align": "left"
      },
      "encoding": {
        "text": {
          "field": "text_label"
        },
        "x": {
          "field": "max_value"
        }
      }
    },
    {
      "mark": {
        "type": "point",
        "shape": "triangle-up",
        "color": "teal",
        "fill": "teal",
        "size": 80,
        "xOffset": 8
      },
      "encoding": {
        "x": {
          "field": "max_value"
        }
      },
      "transform": [
        {
          "filter": "datum.is_positive"
        }
      ]
    },
    {
      "mark": {
        "type": "point",
        "shape": "triangle-down",
        "color": "red",
        "fill": "red",
        "size": 80,
        "xOffset": 8
      },
      "encoding": {
        "x": {
          "field": "max_value"
        }
      },
      "transform": [
        {
          "filter": "!(datum.is_positive)"
        }
      ]
    }
  ],
  "data": {
    "values": [
      {
        "Category": "Appliances",
        "Current": "22962.174",
        "Previous": "13183.887"
      },
      {
        "Category": "Art",
        "Current": "5122.616",
        "Previous": "4004.32"
      },
      {
        "Category": "Binders",
        "Current": "47133.138",
        "Previous": "29871.657"
      },
      {
        "Category": "Envelopes",
        "Current": "1926.632",
        "Previous": "2698.966"
      },
      {
        "Category": "Fasteners",
        "Current": "604.648",
        "Previous": "667.028"
      },
      {
        "Category": "Labels",
        "Current": "2416.994",
        "Previous": "1276.072"
      },
      {
        "Category": "Paper",
        "Current": "15947.488",
        "Previous": "11683.83"
      },
      {
        "Category": "Storage",
        "Current": "39407.746",
        "Previous": "35680.368"
      },
      {
        "Category": "Supplies",
        "Current": "2727.558",
        "Previous": "7482.186"
      }
    ]
  },
  "transform": [
    {
      "calculate": "toNumber(datum.Current)",
      "as": "cur_value"
    },
    {
      "calculate": "toNumber(datum.Previous)",
      "as": "prev_value"
    },
    {
      "calculate": "(datum.cur_value - datum.prev_value) / datum.prev_value",
      "as": "difference"
    },
    {
      "calculate": "datum.difference > 0",
      "as": "is_positive"
    },
    {
      "calculate": "max(datum.cur_value, datum.prev_value)",
      "as": "max_value"
    },
    {
      "calculate": "format(datum.difference, '+.0%') + ' | ' + format(datum.cur_value, '$,.0f')",
      "as": "text_label"
    }
  ]
}
{{< /vega-lite >}}


## Highlighted Absolute Change

This one largely repeats the bar-in-bar, with the exception that the highlighted changes are achieved by combining `x` and `x2` channels combined with `color`
{{< vega-lite id="chart-highlighted-absolute-change" actions="true">}}
{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "width": 400,
  "height": 400,
  "config": {
    "font": "monospace",
    "axis": {
      "domainOpacity": 0,
      "gridOpacity": 0,
      "tickOpacity": 0,
      "domain": false
    },
    "view": {
      "stroke": null
    },
    "background": "#eee",
    "padding": 20,
    "text": {
      "color": "#444"
    },
    "axisX": {
      "titleColor": "#444"
    },
    "axisY": {
      "labelFontSize": 14,
      "labelColor": "#444"
    }
  },
  "encoding": {
    "y": {
      "field": "Category",
      "sort": {
        "field": "cur_value",
        "order": "descending"
      },
      "title": null
    },
    "x": {
      "field": "cur_value",
      "aggregate": "sum",
      "axis": {
        "grid": false,
        "labels": false,
        "ticks": false
      },
      "title": null
    },
    "tooltip": [
      {
        "field": "Category"
      },
      {
        "field": "cur_value",
        "format": "$,.0f",
        "title": "Current Period"
      },
      {
        "field": "prev_value",
        "format": "$,.0f",
        "title": "Previous Period"
      },
      {
        "field": "difference",
        "format": ",.1%",
        "title": "Difference"
      }
    ]
  },
  "layer": [
    {
      "mark": {
        "type": "bar",
        "opacity": 0.4,
        "color": "lightgrey",
        "size": 34
      },
      "encoding": {
        "x": {
          "field": "prev_value",
          "aggregate": "sum"
        },
        "x2": {
          "field": "cur_value",
          "aggregate": "sum"
        },
        "color": {
          "field": "is_positive_text",
          "legend": {
            "orient": "bottom-right",
            "title": "YoY Growth"
          },
          "scale": {
            "domain": [
              "Positive",
              "Negative"
            ],
            "range": [
              "teal",
              "red"
            ]
          }
        }
      }
    },
    {
      "mark": {
        "type": "bar",
        "tooltip": true,
        "opacity": 1,
        "size": 20,
        "color": "slategrey"
      }
    },
    {
      "mark": {
        "type": "text",
        "xOffset": 15,
        "align": "left"
      },
      "encoding": {
        "text": {
          "field": "text_label"
        },
        "x": {
          "field": "max_value"
        }
      }
    },
    {
      "mark": {
        "type": "point",
        "shape": "triangle-up",
        "color": "teal",
        "fill": "teal",
        "size": 80,
        "xOffset": 8
      },
      "encoding": {
        "x": {
          "field": "max_value"
        }
      },
      "transform": [
        {
          "filter": "datum.is_positive"
        }
      ]
    },
    {
      "mark": {
        "type": "point",
        "shape": "triangle-down",
        "color": "red",
        "fill": "red",
        "size": 80,
        "xOffset": 8
      },
      "encoding": {
        "x": {
          "field": "max_value"
        }
      },
      "transform": [
        {
          "filter": "!(datum.is_positive)"
        }
      ]
    }
  ],
  "data": {
    "values": [
      {
        "Category": "Appliances",
        "Current": "22962.174",
        "Previous": "13183.887"
      },
      {
        "Category": "Art",
        "Current": "5122.616",
        "Previous": "4004.32"
      },
      {
        "Category": "Binders",
        "Current": "47133.138",
        "Previous": "29871.657"
      },
      {
        "Category": "Envelopes",
        "Current": "1926.632",
        "Previous": "2698.966"
      },
      {
        "Category": "Fasteners",
        "Current": "604.648",
        "Previous": "667.028"
      },
      {
        "Category": "Labels",
        "Current": "2416.994",
        "Previous": "1276.072"
      },
      {
        "Category": "Paper",
        "Current": "15947.488",
        "Previous": "11683.83"
      },
      {
        "Category": "Storage",
        "Current": "39407.746",
        "Previous": "35680.368"
      },
      {
        "Category": "Supplies",
        "Current": "2727.558",
        "Previous": "7482.186"
      }
    ]
  },
  "transform": [
    {
      "calculate": "toNumber(datum.Current)",
      "as": "cur_value"
    },
    {
      "calculate": "toNumber(datum.Previous)",
      "as": "prev_value"
    },
    {
      "calculate": "(datum.cur_value - datum.prev_value) / datum.prev_value",
      "as": "difference"
    },
    {
      "calculate": "datum.difference > 0",
      "as": "is_positive"
    },
    {
      "calculate": "datum.difference > 0? 'Positive' : 'Negative'",
      "as": "is_positive_text"
    },
    {
      "calculate": "max(datum.cur_value, datum.prev_value)",
      "as": "max_value"
    },
    {
      "calculate": "format(datum.difference, '+.0%') + ' | ' + format(datum.cur_value, '$,.0f')",
      "as": "text_label"
    }
  ]
}
{{< /vega-lite >}}

## Bullet

Key difference in schema here - instead of highlighted bar mark, we use `tick` with exact value for the previous period.

{{< vega-lite id="chart-bullet" actions="true">}}
{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "width": 400,
  "height": 400,
  "config": {
    "font": "monospace",
    "axis": {
      "domainOpacity": 0,
      "gridOpacity": 0,
      "tickOpacity": 0,
      "domain": false
    },
    "view": {
      "stroke": null
    },
    "background": "#eee",
    "padding": 20,
    "text": {
      "color": "#444"
    },
    "axisX": {
      "titleColor": "#444"
    },
    "axisY": {
      "labelFontSize": 14,
      "labelColor": "#444"
    }
  },
  "encoding": {
    "y": {
      "field": "Category",
      "sort": {
        "field": "cur_value",
        "order": "descending"
      },
      "title": null
    },
    "x": {
      "field": "cur_value",
      "aggregate": "sum",
      "axis": {
        "grid": false,
        "labels": false,
        "ticks": false
      },
      "title": null
    },
    "tooltip": [
      {
        "field": "Category"
      },
      {
        "field": "cur_value",
        "format": "$,.0f",
        "title": "Current Period"
      },
      {
        "field": "prev_value",
        "format": "$,.0f",
        "title": "Previous Period"
      },
      {
        "field": "difference",
        "format": ",.1%",
        "title": "Difference"
      }
    ]
  },
  "layer": [
    {
      "mark": {
        "type": "bar",
        "tooltip": true,
        "opacity": 1,
        "size": 20,
        "color": "slategrey"
      }
    },
    {
      "mark": {
        "type": "tick",
        "opacity": 1,
        "color": "#555",
        "thickness": 4,
        "height": 30,
        "fillOpacity": 0.8
      },
      "encoding": {
        "x": {
          "field": "prev_value",
          "aggregate": "sum"
        }
      }
    },
    {
      "mark": {
        "type": "text",
        "xOffset": 15,
        "align": "left"
      },
      "encoding": {
        "text": {
          "field": "text_label"
        },
        "x": {
          "field": "max_value"
        }
      }
    },
    {
      "mark": {
        "type": "point",
        "shape": "triangle-up",
        "color": "teal",
        "fill": "teal",
        "size": 80,
        "xOffset": 8
      },
      "encoding": {
        "x": {
          "field": "max_value"
        }
      },
      "transform": [
        {
          "filter": "datum.is_positive"
        }
      ]
    },
    {
      "mark": {
        "type": "point",
        "shape": "triangle-down",
        "color": "red",
        "fill": "red",
        "size": 80,
        "xOffset": 8
      },
      "encoding": {
        "x": {
          "field": "max_value"
        }
      },
      "transform": [
        {
          "filter": "!(datum.is_positive)"
        }
      ]
    }
  ],
  "data": {
    "values": [
      {
        "Category": "Appliances",
        "Current": "22962.174",
        "Previous": "13183.887"
      },
      {
        "Category": "Art",
        "Current": "5122.616",
        "Previous": "4004.32"
      },
      {
        "Category": "Binders",
        "Current": "47133.138",
        "Previous": "29871.657"
      },
      {
        "Category": "Envelopes",
        "Current": "1926.632",
        "Previous": "2698.966"
      },
      {
        "Category": "Fasteners",
        "Current": "604.648",
        "Previous": "667.028"
      },
      {
        "Category": "Labels",
        "Current": "2416.994",
        "Previous": "1276.072"
      },
      {
        "Category": "Paper",
        "Current": "15947.488",
        "Previous": "11683.83"
      },
      {
        "Category": "Storage",
        "Current": "39407.746",
        "Previous": "35680.368"
      },
      {
        "Category": "Supplies",
        "Current": "2727.558",
        "Previous": "7482.186"
      }
    ]
  },
  "transform": [
    {
      "calculate": "toNumber(datum.Current)",
      "as": "cur_value"
    },
    {
      "calculate": "toNumber(datum.Previous)",
      "as": "prev_value"
    },
    {
      "calculate": "(datum.cur_value - datum.prev_value) / datum.prev_value",
      "as": "difference"
    },
    {
      "calculate": "datum.difference > 0",
      "as": "is_positive"
    },
    {
      "calculate": "datum.difference > 0? 'Positive' : 'Negative'",
      "as": "is_positive_text"
    },
    {
      "calculate": "max(datum.cur_value, datum.prev_value)",
      "as": "max_value"
    },
    {
      "calculate": "format(datum.difference, '+.0%') + ' | ' + format(datum.cur_value, '$,.0f')",
      "as": "text_label"
    }
  ]
}
{{< /vega-lite >}}


## Direction Arrows


{{< vega-lite id="chart-direction-arrows" actions="true">}}
{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "width": 400,
  "height": 400,
  "config": {
    "font": "monospace",
    "axis": {
      "domainOpacity": 0,
      "gridOpacity": 0,
      "tickOpacity": 0,
      "domain": false
    },
    "view": {
      "stroke": null
    },
    "background": "#eee",
    "padding": 20,
    "text": {
      "color": "#444",
      "opacity": 0.5
    },
    "axisX": {
      "titleColor": "#444"
    },
    "axisY": {
      "labelFontSize": 14,
      "labelColor": "#444"
    }
  },
  "encoding": {
    "y": {
      "field": "Category",
      "sort": {
        "field": "cur_value",
        "order": "descending"
      },
      "title": null
    },
    "x": {
      "field": "cur_value",
      "aggregate": "sum",
      "axis": {
        "grid": false,
        "labels": false,
        "ticks": false
      },
      "title": null
    },
    "tooltip": [
      {
        "field": "Category"
      },
      {
        "field": "cur_value",
        "format": "$,.0f",
        "title": "Current Period"
      },
      {
        "field": "prev_value",
        "format": "$,.0f",
        "title": "Previous Period"
      },
      {
        "field": "difference",
        "format": ",.1%",
        "title": "Difference"
      }
    ]
  },
  "layer": [
    {
      "mark": {
        "type": "bar",
        "opacity": 1,
        "color": "lightgrey",
        "size": 4
      },
      "encoding": {
        "x": {
          "field": "prev_value",
          "aggregate": "sum"
        },
        "x2": {
          "field": "cur_value",
          "aggregate": "sum"
        },
        "color": {
          "field": "is_positive_text",
          "legend": {
            "orient": "bottom-right",
            "title": "YoY Growth",
            "symbolOpacity": 0.3
          },
          "scale": {
            "domain": [
              "Positive",
              "Negative"
            ],
            "range": [
              "teal",
              "red"
            ]
          }
        }
      }
    },
    {
      "mark": {
        "type": "text",
        "xOffset": 15,
        "align": "left"
      },
      "encoding": {
        "text": {
          "field": "text_label"
        },
        "x": {
          "field": "max_value"
        }
      }
    },
    {
      "mark": {
        "type": "point",
        "shape": "triangle-right",
        "color": "teal",
        "fill": "teal",
        "size": 80,
        "xOffset": 0,
        "opacity": 1
      },
      "encoding": {
        "x": {
          "field": "max_value"
        }
      },
      "transform": [
        {
          "filter": "datum.is_positive"
        }
      ]
    },
    {
      "mark": {
        "type": "point",
        "shape": "triangle-left",
        "color": "red",
        "fill": "red",
        "size": 80,
        "xOffset": 0,
        "opacity": 1
      },
      "encoding": {
        "x": {
          "field": "cur_value"
        }
      },
      "transform": [
        {
          "filter": "!(datum.is_positive)"
        }
      ]
    }
  ],
  "data": {
    "values": [
      {
        "Category": "Appliances",
        "Current": "22962.174",
        "Previous": "13183.887"
      },
      {
        "Category": "Art",
        "Current": "5122.616",
        "Previous": "4004.32"
      },
      {
        "Category": "Binders",
        "Current": "47133.138",
        "Previous": "29871.657"
      },
      {
        "Category": "Envelopes",
        "Current": "1926.632",
        "Previous": "2698.966"
      },
      {
        "Category": "Fasteners",
        "Current": "604.648",
        "Previous": "667.028"
      },
      {
        "Category": "Labels",
        "Current": "2416.994",
        "Previous": "1276.072"
      },
      {
        "Category": "Paper",
        "Current": "15947.488",
        "Previous": "11683.83"
      },
      {
        "Category": "Storage",
        "Current": "39407.746",
        "Previous": "35680.368"
      },
      {
        "Category": "Supplies",
        "Current": "2727.558",
        "Previous": "7482.186"
      }
    ]
  },
  "transform": [
    {
      "calculate": "toNumber(datum.Current)",
      "as": "cur_value"
    },
    {
      "calculate": "toNumber(datum.Previous)",
      "as": "prev_value"
    },
    {
      "calculate": "(datum.cur_value - datum.prev_value) / datum.prev_value",
      "as": "difference"
    },
    {
      "calculate": "datum.difference > 0",
      "as": "is_positive"
    },
    {
      "calculate": "datum.difference > 0? 'Positive' : 'Negative'",
      "as": "is_positive_text"
    },
    {
      "calculate": "max(datum.cur_value, datum.prev_value)",
      "as": "max_value"
    },
    {
      "calculate": "format(datum.difference, '+.0%') + ' | ' + format(datum.cur_value, '$,.0f')",
      "as": "text_label"
    }
  ]
}
{{< /vega-lite >}}


## Error Bars