Search code examples
vega-litevega

Vega Stacked waffle chart into vega lite


Does anyone know if it is possible to convert this vega spec from sanddance into a vega-lite spec? I really like the idea of every data point being a separate box so you can hover to get the information.

In my case I want to create a new chart where my main groups are resources and each work orders will be colored by status. So for the current month you can see which work orders are overdue, postponed, completed, cancelled etc. I will demo this later.

enter image description here

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "data": [
    {
      "name": "input",
      "url": "https://microsoft.github.io/SandDance/sample-data/titanicmaster.tsv",
      "format": {"parse": "auto", "type": "tsv", "delimiter": "\t"}
    },
    {"name": "data_source", "source": "input", "transform": []},
    {
      "name": "data_topcolorlookup",
      "source": "data_source",
      "transform": [
        {"type": "aggregate", "groupby": ["Class"]},
        {"type": "window", "ops": ["count"], "as": ["__SandDance__TopIndex"]},
        {"type": "filter", "expr": "datum[\"__SandDance__TopIndex\"] <= 19"}
      ]
    },
    {
      "name": "data_legend",
      "source": "data_source",
      "transform": [
        {
          "type": "lookup",
          "from": "data_topcolorlookup",
          "key": "Class",
          "fields": ["Class"],
          "values": ["Class"],
          "as": ["__SandDance__TopColor"]
        },
        {
          "type": "formula",
          "expr": "datum[\"__SandDance__TopColor\"] == null ? '__Other' : datum[\"__SandDance__TopColor\"]",
          "as": "__SandDance__TopColor"
        },
        {
          "type": "joinaggregate",
          "groupby": ["Department"],
          "ops": ["count"],
          "as": ["agg_1_aggregate_value"]
        },
        {
          "type": "extent",
          "field": "agg_1_aggregate_value",
          "signal": "agg_1_count_extent"
        },
        {
          "type": "stack",
          "groupby": ["Department"],
          "as": ["square_2_stack0", "square_2_stack1"],
          "sort": {"field": "Class", "order": "ascending"}
        }
      ]
    },
    {
      "name": "band_0_accumulative",
      "source": "data_legend",
      "transform": [
        {"type": "aggregate", "groupby": ["Department"], "ops": ["count"]}
      ]
    }
  ],
  "marks": [
    {
      "type": "group",
      "encode": {
        "update": {
          "x": {"signal": "PlotOffsetLeft - RoleFacet_AxesAdjustSignalX"},
          "y": {"signal": "PlotOffsetTop"},
          "height": {"signal": "PlotHeightOut - RoleFacet_AxesAdjustSignalY"},
          "width": {"signal": "PlotWidthOut + RoleFacet_AxesAdjustSignalX"}
        }
      },
      "marks": [
        {
          "name": "square_2",
          "type": "rect",
          "from": {"data": "output"},
          "encode": {
            "update": {
              "height": {
                "signal": "(((band_0_bandwidth) / ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled))))) - min(0.1 * ((band_0_bandwidth) / (ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled)))) - 1)), 1))"
              },
              "width": [
                {"test": "datum.__SandDance__Collapsed", "value": 0},
                {"test": "datum.__SandDance__Collapsed", "value": 0},
                {
                  "signal": "(((aggMaxExtentScaled) / ceil(aggMaxExtent / ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled)))))) - min(0.1 * ((band_0_bandwidth) / (ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled)))) - 1)), 1))"
                }
              ],
              "z": {"value": 0},
              "depth": [
                {"test": "datum.__SandDance__Collapsed", "value": 0},
                {"value": 0}
              ],
              "x": [
                {"test": "datum.__SandDance__Collapsed", "signal": "0"},
                {"test": "datum.__SandDance__Collapsed", "signal": "0"},
                {"field": "__SandDance__X"}
              ],
              "y": {"field": "__SandDance__Y"},
              "fill": {
                "scale": "scale_color",
                "field": "__SandDance__TopColor"
              },
              "opacity": {"signal": "Mark_OpacitySignal"}
            }
          }
        }
      ],
      "axes": [
        {
          "scale": "scale_band_0_y",
          "orient": "left",
          "domain": true,
          "ticks": true,
          "domainColor": "#0b0b0b",
          "tickColor": "#0b0b0b",
          "tickSize": 10,
          "title": "Department",
          "titleAlign": "right",
          "titleAngle": {"signal": "Text_AngleYSignal"},
          "titleColor": "#0b0b0b",
          "titleFontSize": {"signal": "Text_TitleSizeSignal"},
          "titleLimit": 100,
          "titlePadding": 60,
          "labels": true,
          "labelAlign": "right",
          "labelBaseline": "middle",
          "labelAngle": {"signal": "Text_AngleYSignal"},
          "labelColor": "#0b0b0b",
          "labelFontSize": {"signal": "Text_SizeSignal"},
          "labelLimit": 100
        },
        {
          "scale": "scale_agg_1",
          "orient": "bottom",
          "domain": true,
          "ticks": true,
          "domainColor": "#0b0b0b",
          "tickColor": "#0b0b0b",
          "tickSize": 10,
          "title": "Count",
          "titleAlign": "left",
          "titleAngle": {"signal": "Text_AngleXSignal"},
          "titleColor": "#0b0b0b",
          "titleFontSize": {"signal": "Text_TitleSizeSignal"},
          "titleLimit": 100,
          "titlePadding": 30,
          "labels": true,
          "labelAlign": "left",
          "labelBaseline": "top",
          "labelAngle": {"signal": "Text_AngleXSignal"},
          "labelColor": "#0b0b0b",
          "labelFontSize": {"signal": "Text_SizeSignal"},
          "labelLimit": 100
        }
      ],
      "data": [
        {
          "name": "output",
          "source": "data_legend",
          "transform": [
            {
              "type": "formula",
              "expr": "0 + floor((datum[\"square_2_stack0\"]) / ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled))))) * ((((aggMaxExtentScaled) / ceil(aggMaxExtent / ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled)))))) - min(0.1 * ((band_0_bandwidth) / (ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled)))) - 1)), 1)) + min(0.1 * ((band_0_bandwidth) / (ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled)))) - 1)), 1))",
              "as": "__SandDance__X"
            },
            {
              "type": "formula",
              "expr": "0 + scale(\"scale_band_0_y\", datum[\"Department\"]) + (band_0_bandwidth) / ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled)))) * ((datum[\"square_2_stack0\"]) % ceil(sqrt(aggMaxExtent * ((band_0_bandwidth) / (aggMaxExtentScaled)))))",
              "as": "__SandDance__Y"
            }
          ]
        }
      ]
    }
  ],
  "signals": [
    {
      "name": "RoleZ_ProportionSignal",
      "value": 0.6,
      "bind": {
        "name": "Z scale proportion to Y",
        "debounce": 250,
        "input": "range",
        "min": 0.1,
        "max": 2,
        "step": 0.1
      }
    },
    {
      "name": "RoleZ_HeightSignal",
      "update": "ViewportHeight * RoleZ_ProportionSignal"
    },
    {
      "name": "Text_ScaleSignal",
      "value": 1.2,
      "bind": {
        "name": "Text scale",
        "debounce": 250,
        "input": "range",
        "min": 0.5,
        "max": 2,
        "step": 0.1
      }
    },
    {"name": "Text_SizeSignal", "update": "Text_ScaleSignal * 10"},
    {"name": "Text_TitleSizeSignal", "update": "Text_ScaleSignal * 15"},
    {"name": "Text_AngleXSignal", "value": 30},
    {"name": "Text_AngleYSignal", "value": 0},
    {"name": "Mark_OpacitySignal", "value": 1},
    {"name": "MinCellWidth", "update": "140"},
    {
      "name": "MinCellHeight",
      "update": "max((180), (length(data(\"band_0_accumulative\")) * 15))"
    },
    {"name": "ViewportHeight", "update": "max(MinCellHeight, 1253)"},
    {"name": "ViewportWidth", "update": "max(MinCellWidth, 2252)"},
    {"name": "PlotOffsetLeft", "update": "120"},
    {"name": "PlotOffsetTop", "update": "0"},
    {"name": "PlotOffsetBottom", "update": "120"},
    {"name": "PlotOffsetRight", "update": "0"},
    {"name": "RoleFacet_AxesAdjustSignalX", "update": "0"},
    {"name": "RoleFacet_AxesAdjustSignalY", "update": "0"},
    {
      "name": "PlotHeightIn",
      "update": "ViewportHeight - PlotOffsetBottom + RoleFacet_AxesAdjustSignalY"
    },
    {
      "name": "PlotWidthIn",
      "update": "ViewportWidth - PlotOffsetLeft - PlotOffsetRight"
    },
    {"name": "PlotHeightOut", "update": "PlotHeightIn"},
    {"name": "PlotWidthOut", "update": "PlotWidthIn"},
    {
      "name": "height",
      "update": "PlotOffsetTop + PlotHeightOut + PlotOffsetBottom - RoleFacet_AxesAdjustSignalY"
    },
    {
      "name": "width",
      "update": "PlotWidthOut + PlotOffsetLeft + PlotOffsetRight"
    },
    {
      "name": "RoleColor_BinCountSignal",
      "value": 7,
      "bind": {
        "name": "Color bin count",
        "debounce": 250,
        "input": "range",
        "min": 1,
        "max": 20,
        "step": 1
      }
    },
    {
      "name": "RoleColor_ReverseSignal",
      "value": false,
      "bind": {"name": "Color reverse", "input": "checkbox"}
    },
    {"name": "band_0_bandwidth", "update": "bandwidth(\"scale_band_0_y\")"},
    {"name": "aggMaxExtent", "update": "agg_1_count_extent[1]"},
    {
      "name": "aggMaxExtentScaled",
      "update": "scale(\"scale_agg_1\", aggMaxExtent)"
    }
  ],
  "legends": [
    {
      "orient": "none",
      "title": "Class",
      "fill": "scale_color",
      "encode": {"symbols": {"update": {"shape": {"value": "square"}}}}
    }
  ],
  "scales": [
    {
      "name": "scale_color",
      "type": "ordinal",
      "domain": {
        "data": "data_legend",
        "field": "__SandDance__TopColor",
        "sort": true
      },
      "range": {"scheme": "category10"},
      "reverse": {"signal": "RoleColor_ReverseSignal"}
    },
    {
      "type": "band",
      "name": "scale_band_0_y",
      "range": [0, {"signal": "PlotHeightIn"}],
      "padding": 0.1,
      "domain": {"data": "data_legend", "field": "Department", "sort": true},
      "reverse": true
    },
    {
      "type": "linear",
      "name": "scale_agg_1",
      "domain": [0, {"signal": "aggMaxExtent"}],
      "range": [0, {"signal": "PlotWidthIn"}],
      "nice": true,
      "zero": true,
      "reverse": false
    },
    {
      "name": "scale_square_2_z",
      "type": "linear",
      "range": [0, {"signal": "(PlotHeightIn) * RoleZ_ProportionSignal"}],
      "round": true,
      "reverse": false,
      "domain": {"data": "data_legend", "field": "TicketCost"},
      "zero": true,
      "nice": true
    }
  ],
  "config": {}
}

Solution

  • I managed. Here is the code if anyone wants to contribute and improve. I think it is very interesting to see the unique boxes to illustrate actual records. When you hover you get a detailed tool-tip about that specific record for example.

    enter image description here

    {
      "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
      "data": {"url": "data/cars.json"},
      "transform": [
        {
          "joinaggregate": [{"op": "count", "as": "Count"}],
          "groupby": ["Cylinders", "Origin"]
        },
        {
          "joinaggregate": [{"op": "sum", "field": "Count", "as": "OriginCount"}],
          "groupby": ["Origin"]
        }
      ],
      "facet": {
        "row": {
          "field": "Origin",
          "sort": {"field": "OriginCount", "order": "descending"},
          "header": {
            "title": "",
            "labelAngle": 0,
            "labelPadding": 15,
            "labelAlign": "left",
            "labelFontSize": 12
          }
        }
      },
      "spacing": 5,
      "spec": {
        "width": 300,
        "height": 100,
        "layer": [
          {
            "transform": [
              {
                "window": [{"op": "row_number", "as": "id"}],
                "sort": [{"field": "Cylinders", "order": "ascending"}],
                "groupby": ["Origin"]
              },
              {"calculate": "ceil(datum.id / 10)", "as": "row"},
              {"calculate": "datum.id - datum.row * 10", "as": "col"}
            ],
            "mark": {
              "type": "rect",
              "stroke": "#fff",
              "strokeWidth": 1,
              "filled": true,
              "tooltip": true
            },
            "encoding": {
              "y": {"field": "col", "type": "ordinal", "axis": null},
              "x": {"field": "row", "type": "ordinal", "axis": null},
              "color": {
                "field": "Cylinders",
                "type": "nominal",
                "scale": {"scheme": "set1"}
              },
              "tooltip": [
                {"field": "Origin", "type": "nominal"},
                {"field": "Name", "type": "nominal"},
                {"field": "Cylinders", "type": "nominal"},
                {"field": "Horsepower", "type": "nominal"},
                {"field": "id", "type": "nominal"}
              ]
            }
          },
          {
            "transform": [
              {
                "window": [{"op": "row_number", "as": "id"}],
                "sort": [{"field": "Cylinders", "order": "ascending"}],
                "groupby": ["Origin"]
              },
              {"calculate": "ceil(datum.id / 10)", "as": "row"},
              {"calculate": "datum.id - datum.row * 10", "as": "col"},
              {
                "joinaggregate": [{"op": "max", "field": "id", "as": "maxId"}],
                "groupby": ["Origin"]
              },
              {"filter": "datum.id === datum.maxId"}
            ],
            "mark": {
              "type": "text",
              "align": "left",
              "baseline": "middle",
              "dx": 15,
              "color": "black"
            },
            "encoding": {
              "text": {"field": "id", "type": "nominal"},
              "x": {"field": "row", "type": "ordinal", "axis": null},
              "tooltip": [
                {"field": "Origin", "type": "nominal"},
                {"field": "Name", "type": "nominal"},
                {"field": "Cylinders", "type": "nominal"},
                {"field": "id", "type": "nominal"}
              ]
            }
          }
        ]
      },
      "config": {"view": {"stroke": "transparent"}}
    }
    

    We can also do interesting things like color based on horsepower.

    enter image description here

    Or by Miles per gallon. Here we see Japan has the highest percentage of efficient vehicles.

    enter image description here

    We can even add additional marks to represent other things. Below I have added some marks for priority and criticality.

    enter image description here

    Lots of possibilities here...

    Happy Charting!

    Adam