Search code examples
vega-lite

Ribbon Chart example in Vega-lite


I just wanted to share one way you could design a ribbon chart with vega-lite.

I believe Power BI native charts is one of the only tools that can generate such a chart so I was curious if it was possible with vega-lite.

If someone could somehow connect the area chart from the right side of bar mark to the left side of the bar instead through the center it would be even better.

Would love to hear your thoughts.

enter image description here

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "description": "A chart combining stacked bars with lines, sorted by value",
  "width": {"step": 100},
  "data": {
    "values": [
      {"category": "A", "value": 30, "type": "Type 1"},
      {"category": "A", "value": 20, "type": "Type 2"},
      {"category": "A", "value": 30, "type": "Type 3"},
      {"category": "B", "value": 15, "type": "Type 1"},
      {"category": "B", "value": 25, "type": "Type 2"},
      {"category": "B", "value": 35, "type": "Type 3"},
      {"category": "C", "value": 35, "type": "Type 1"},
      {"category": "C", "value": 22, "type": "Type 2"},
      {"category": "C", "value": 32, "type": "Type 3"}
    ]
  },
  "encoding": {
    "x": {
      "field": "category",
      "type": "nominal",
      "axis": {
        "domain": true,
        "grid": false,
        "ticks": false,
        "labels": true,
        "labelAngle": 0,
        "labelFontSize": 12,
        "labelPadding": 6
      },
      "title": null
    },
    "y": {
      "field": "value",
      "type": "quantitative",
      "axis": {"domain": false, "grid": false, "ticks": false, "labels": false},
      "title": null
    },
    "opacity": {"value": 1},
    "color": {
      "title": null,
      "field": "type",
      "type": "nominal",
      "scale": {"range": ["#008080", "#e6c14f", "#6b8e23"]},
      "legend": {
        "padding": 0,
        "labelFontSize": 11,
        "labelColor": "#706D6C",
        "rowPadding": 8,
        "symbolOpacity": 0.6,
        "symbolType": "square"
      }
    },
    "order": {"field": "value", "type": "quantitative", "sort": "descending"},
    "tooltip": [
      {"field": "category", "type": "nominal", "title": "Category"},
      {"field": "type", "type": "nominal", "title": "Type"},
      {"field": "value", "type": "quantitative", "title": "Value"}
    ]
  },
  "layer": [
    {
      "transform": [
        {
          "window": [{"op": "sum", "field": "value", "as": "sum"}],
          "groupby": ["category", "type"]
        }
      ],
      "mark": {"type": "area", "interpolate": "monotone"},
      "encoding": {
        "opacity": {"value": 0.4},
        "y": {"field": "sum", "type": "quantitative", "stack": "zero"},
        "x": {"field": "category", "type": "nominal"},
        "detail": {"field": "type", "type": "nominal"},
        "order": {
          "aggregate": "max",
          "field": "value",
          "type": "quantitative",
          "sort": "descending"
        }
      }
    },
    {
      "mark": {
        "type": "bar",
        "interpolate": "monotone",
        "width": {"band": 0.4}
      },
      "encoding": {
        "y": {"field": "value", "type": "quantitative", "stack": "zero"},
        "opacity": {"value": 1}
      }
    },
    {
      "transform": [
        {
          "aggregate": [{"op": "sum", "field": "value", "as": "total"}],
          "groupby": ["category"]
        }
      ],
      "mark": {"type": "text", "dy": -10, "fontSize": 12},
      "encoding": {
        "x": {"field": "category", "type": "nominal"},
        "y": {"field": "total", "type": "quantitative"},
        "text": {"field": "total", "type": "quantitative"},
        "color": {"value": "black"}
      }
    },
    {
      "mark": {"type": "text", "dy": 12, "fontSize": 9},
      "encoding": {
        "x": {"field": "category", "type": "nominal"},
        "y": {"field": "value", "type": "quantitative", "stack": "zero"},
        "text": {"field": "value", "type": "quantitative"},
        "color": {"value": "#FFF"}
      }
    }
  ],
  "config": {"view": {"stroke": null}}
}

Thanks


Solution

  • I have updated the above Ribbon chart with a cleaner layout. The bars are now thinner so the area marks flow through the bars in a better way. When you click on a ribbon it will reduce the opacity on the other ribbons and also text marks.

    enter image description here

    Full spec:

    {
      "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
      "description": "A chart combining stacked bars with lines, sorted by value",
      "width": 700,
      "height": 300,
      "data": {
        "values": [
          {"category": "A", "value": 30, "type": "Type 1", "pos": "start"},
          {"category": "A", "value": 20, "type": "Type 2", "pos": "start"},
          {"category": "A", "value": 30, "type": "Type 3", "pos": "start"},
          {"category": "B", "value": 15, "type": "Type 1", "pos": "-"},
          {"category": "B", "value": 45, "type": "Type 2", "pos": "-"},
          {"category": "B", "value": 35, "type": "Type 3", "pos": "-"},
          {"category": "C", "value": 35, "type": "Type 1", "pos": "-"},
          {"category": "C", "value": 22, "type": "Type 2", "pos": "-"},
          {"category": "C", "value": 32, "type": "Type 3", "pos": "-"},
          {"category": "D", "value": 30, "type": "Type 1", "pos": "-"},
          {"category": "D", "value": 20, "type": "Type 2", "pos": "-"},
          {"category": "D", "value": 40, "type": "Type 3", "pos": "-"},
          {"category": "E", "value": 15, "type": "Type 1", "pos": "-"},
          {"category": "E", "value": 25, "type": "Type 2", "pos": "-"},
          {"category": "E", "value": 35, "type": "Type 3", "pos": "-"},
          {"category": "F", "value": 35, "type": "Type 1", "pos": "end"},
          {"category": "F", "value": 22, "type": "Type 2", "pos": "end"},
          {"category": "F", "value": 18, "type": "Type 3", "pos": "end"}
        ]
      },
      "encoding": {
        "x": {
          "field": "category",
          "type": "nominal",
          "axis": {
            "domain": true,
            "grid": false,
            "domainColor": "#dddddd",
            "ticks": false,
            "labels": true,
            "labelAngle": 0,
            "labelFontSize": 12,
            "labelPadding": 6
          },
          "title": null
        },
        "y": {
          "field": "value",
          "type": "quantitative",
          "axis": {"domain": false, "grid": false, "ticks": false, "labels": false},
          "title": null
        },
        "opacity": {"value": 1},
        "color": {
          "title": null,
          "field": "type",
          "type": "nominal",
          "scale": {"range": ["#008080", "#e6c14f", "#6b8e23"]},
          "legend": {
            "padding": 0,
            "labelFontSize": 11,
            "labelColor": "#706D6C",
            "rowPadding": 8,
            "symbolOpacity": 0.6,
            "symbolType": "square"
          }
        },
        "order": {"field": "value", "type": "quantitative", "sort": "descending"},
        "tooltip": [
          {"field": "category", "type": "nominal", "title": "Category"},
          {"field": "type", "type": "nominal", "title": "Type"},
          {"field": "value", "type": "quantitative", "title": "Value"}
        ]
      },
      "layer": [
        {
          "transform": [
            {
              "window": [{"op": "sum", "field": "value", "as": "sumx"}],
              "groupby": ["category", "type"]
            }
          ],
          "params": [
            {"name": "click", "select": {"type": "point", "encodings": ["color"]}}
          ],
          "mark": {"type": "bar"},
          "encoding": {
            "opacity": {"value": 0},
            "y": {"field": "sumx", "type": "quantitative", "stack": "zero"},
            "x": {"field": "category", "type": "nominal"},
            "order": {
              "aggregate": "max",
              "field": "value",
              "type": "quantitative",
              "sort": "descending"
            }
          }
        },
        {
          "transform": [
            {
              "window": [{"op": "sum", "field": "value", "as": "sum"}],
              "groupby": ["category", "type"]
            }
          ],
          "mark": {"type": "area", "interpolate": "monotone"},
          "encoding": {
            "opacity": {
              "condition": {"param": "click", "value": 0.6},
              "value": 0.1
            },
            "y": {"field": "sum", "type": "quantitative", "stack": "zero"},
            "x": {"field": "category", "type": "nominal"},
            "detail": {"field": "type", "type": "nominal"},
            "order": {
              "aggregate": "max",
              "field": "value",
              "type": "quantitative",
              "sort": "descending"
            }
          }
        },
        {
          "mark": {"type": "bar", "width": {"band": 0.15}},
          "encoding": {
            "y": {"field": "value", "type": "quantitative", "stack": "zero"},
            "opacity": {"value": 0.2},
            "color": {"value": "#FFF"}
          }
        },
        {
          "transform": [{"filter": "datum.pos == 'start'"}],
          "mark": {"type": "bar", "width": {"band": 0.2}},
          "encoding": {
            "y": {"field": "value", "type": "quantitative", "stack": "zero"},
            "opacity": {"condition": {"param": "click", "value": 1}, "value": 0.1},
            "xOffset": {"value": -13}
          }
        },
        {
          "transform": [{"filter": "datum.pos == 'end'"}],
          "mark": {"type": "bar", "width": {"band": 0.2}},
          "encoding": {
            "y": {"field": "value", "type": "quantitative", "stack": "zero"},
            "opacity": {"condition": {"param": "click", "value": 1}, "value": 0.1},
            "xOffset": {"value": 13}
          }
        },
        {
          "transform": [
            {"filter": "datum.pos == '-'"},
            {
              "aggregate": [{"op": "sum", "field": "value", "as": "total"}],
              "groupby": ["category"]
            }
          ],
          "mark": {
            "type": "text",
            "dy": -12,
            "dx": 0,
            "fontSize": 12,
            "fontWeight": 600
          },
          "encoding": {
            "x": {"field": "category", "type": "nominal"},
            "y": {"field": "total", "type": "quantitative"},
            "text": {"field": "total", "type": "quantitative"},
            "color": {"value": "black"}
          }
        },
        {
          "transform": [{"filter": "datum.pos == '-'"}],
          "mark": {"type": "text", "dy": 12, "dx": 0, "fontSize": 10},
          "encoding": {
            "x": {"field": "category", "type": "nominal"},
            "y": {"field": "value", "type": "quantitative", "stack": "zero"},
            "text": {"field": "value", "type": "quantitative"},
            "color": {"value": "#000"},
            "opacity": {"condition": {"param": "click", "value": 1}, "value": 0.2}
          }
        },
        {
          "transform": [
            {"filter": "datum.pos == 'start'"},
            {
              "aggregate": [{"op": "sum", "field": "value", "as": "total"}],
              "groupby": ["category"]
            }
          ],
          "mark": {
            "type": "text",
            "dy": -12,
            "dx": -13,
            "fontSize": 13,
            "fontWeight": 800
          },
          "encoding": {
            "x": {"field": "category", "type": "nominal"},
            "y": {"field": "total", "type": "quantitative"},
            "text": {"field": "total", "type": "quantitative"},
            "color": {"value": "black"}
          }
        },
        {
          "transform": [{"filter": "datum.pos == 'start'"}],
          "mark": {"type": "text", "dy": 12, "dx": -13, "fontSize": 11},
          "encoding": {
            "x": {"field": "category", "type": "nominal"},
            "y": {"field": "value", "type": "quantitative", "stack": "zero"},
            "text": {"field": "value", "type": "quantitative"},
            "color": {"value": "#FFF"},
            "opacity": {"condition": {"param": "click", "value": 1}, "value": 0.2}
          }
        },
        {
          "transform": [
            {"filter": "datum.pos == 'end'"},
            {
              "aggregate": [{"op": "sum", "field": "value", "as": "total"}],
              "groupby": ["category"]
            }
          ],
          "mark": {
            "type": "text",
            "dy": -12,
            "dx": 13,
            "fontSize": 13,
            "fontWeight": 800
          },
          "encoding": {
            "x": {"field": "category", "type": "nominal"},
            "y": {"field": "total", "type": "quantitative"},
            "text": {"field": "total", "type": "quantitative"},
            "color": {"value": "black"}
          }
        },
        {
          "transform": [{"filter": "datum.pos == 'end'"}],
          "mark": {"type": "text", "dy": 12, "dx": 13, "fontSize": 11},
          "encoding": {
            "x": {"field": "category", "type": "nominal"},
            "y": {"field": "value", "type": "quantitative", "stack": "zero"},
            "text": {"field": "value", "type": "quantitative"},
            "color": {"value": "#FFF"},
            "opacity": {"condition": {"param": "click", "value": 1}, "value": 0.2}
          }
        }
      ],
      "config": {"view": {"stroke": null}}
    }
    

    Adam (APB Reports)