Deneb Example - Category Enclosures

Deneb/Vega-Lite can be used to add category enclosures to subcategory data in Power BI. These enclosures can be augmented with labels and values to increase the information presented.

I first saw an example of this last month in a post by Abacus Data on what Canadians think about Canada joining the European Union:

and specifically in this visual:

I thought Deneb/Vega-Lite might be a good candidate to apply a similar concept to survey data, and was introduced to the name “enclosures” by Anastasiya Kuznetsova in her recent post:

Further, in the past couple of days, I also saw posts by Kevin Flerlage and Daniel Marsh-Patrick applying a similar concept to an alternative to a stacked bar chart:

The example herein presents synthetic subcategory survey data with positive and negative category enclosures and values.

This example illustrates a number of Deneb/Vega-Lite features, including:
0 - General:

  • a “title” block with subtitle
  • a shared “transform” block with:
    • a “calculate” transform to determine the ID (category ID * 100 + subcategory ID)
  • an outside “layer” block for nested layers for category and subcategory data

1 - Category:

  • a nested “transform” block with:
    • 3x “joinaggregate” transforms to calculate the category maximum ID, the category minimum ID, and the category total percent
    • a “filter” transform to restrict the dataset to only records for the positive and negative categories
    • a “window/rank” transform to rank the subcategories by category
    • a “filter” transform to restrict the dataset to only 1 record each for the positive and negative categories
  • a nested “layer” block with:
    • a “rect” mark with conditional colour (positive=green, negative=red) and 20% opacity
    • a “text” mark for the category label (center alignment, rotated 270 degrees)
    • a “text” mark for the category percent (right alignment)

2 - Subcategory:

  • a nested “layer” block with:
    • a nested “transform” block with:
      • 2x “window/rank” transforms to determine the subcategory rank within its’ category
      • 2x “calculate” transforms to determine the conditional subcategory opacity (positive=ascending, negative=descending)
    • a “bar” mark with conditional colour (positive=green, negative=red, neutral=gray) and conditional opacity (as calculated in the transform above)
    • a “text” mark for the subcategory label (right alignment)
    • a “text” mark for the subcategory percent (left alignment)
Deneb/Vega-Lite JSON Code:
{
  "title": {
    "anchor": "start",
    "align": "left",
    "offset": 20,
    "text": "Power BI Category Enclosures using Deneb",
    "font": "Verdana",
    "fontSize": 24,
    "fontWeight": "bold",
    "fontStyle": "normal",
    "subtitle": "Synthetic Survey Data",
    "subtitleFont": "Verdana",
    "subtitleFontSize": 16,
    "subtitleFontWeight": "normal",
    "subtitleFontStyle": "italic"
  },
  "data": {
    "name": "dataset"
  },
  "transform": [
    {
      "calculate": "( datum['Category ID'] * 100 ) + ( datum['Subcategory ID'] * 1 )",
      "as": "_id"
    }
  ],
  "encoding": {
    "y": {
      "field": "_id",
      "type": "nominal",
      "axis": null
    },
    "x": {
      "field": "Percent",
      "type": "quantitative",
      "scale": {
        "domain": [
          -0.15,
          0.50
        ]
      },
      "axis": {
        "values": [
          0.0,
          0.1,
          0.2,
          0.3,
          0.4,
          0.5
        ],
        "labels": false,
        "ticks": false,
        "domain": false,
        "title": null
      }
    }
  },
  "layer": [
    {
      "name": "CATEGORY",
      "transform": [
        {
          "joinaggregate": [
            {
              "op": "min",
              "field": "_id",
              "as": "_category_min"
            },
            {
              "op": "max",
              "field": "_id",
              "as": "_category_max"
            },
            {
              "op": "sum",
              "field": "Percent",
              "as": "_category_percent"
            }
          ],
          "groupby": [
            "Category ID"
          ]
        },
        {
          "filter": "datum['Category'] != 'Neutral'"
        },
        {
          "window": [
            {
              "op": "rank",
              "as": "_subcategory_id_rank"
            }
          ],
          "groupby": [
            "Category ID"
          ],
          "sort": [
            {
              "field": "Subcategory ID",
              "order": "ascending"
            }
          ]
        },
        {
          "filter": "datum['_subcategory_id_rank'] == 1"
        }
      ],
      "layer": [
        {
          "name": "CATEGORY_BAR",
          "mark": {
            "type": "rect",
            "cornerRadius": 8,
            "yOffset": -49,
            "y2Offset": 49,
            "color": {
              "expr": "datum['Category'] == 'Positive' ? 'green' : 'red'"
            },
            "opacity": 0.2
          },
          "encoding": {
            "x": {
              "datum": -0.15
            },
            "x2": {
              "datum": 0.45
            },
            "y": {
              "field": "_category_min"
            },
            "y2": {
              "field": "_category_max"
            }
          }
        },
        {
          "name": "CATEGORY_LABEL",
          "mark": {
            "type": "text",
            "align": "center",
            "angle": 270,
            "fontSize": 24,
            "fontWeight": "bold",
            "yOffset": 50
          },
          "encoding": {
            "text": {
              "field": "Category"
            },
            "x": {
              "datum": -0.12
            }
          }
        },
        {
          "name": "CATEGORY_PERCENT",
          "mark": {
            "type": "text",
            "align": "right",
            "fontSize": 18,
            "fontWeight": "bold",
            "yOffset": 50
          },
          "encoding": {
            "text": {
              "field": "_category_percent",
              "format": ".0%"
            },
            "x": {
              "datum": 0.44
            }
          }
        }
      ]
    },
    {
      "name": "SUBCATEGORY",
      "layer": [
        {
          "name": "SUBCATEGORY_BAR",
          "transform": [
            {
              "window": [
                {
                  "op": "rank",
                  "as": "_subcategory_rank_asc"
                }
              ],
              "groupby": [
                "Category"
              ],
              "sort": [
                {
                  "field": "Subcategory ID",
                  "order": "ascending"
                }
              ]
            },
            {
              "window": [
                {
                  "op": "rank",
                  "as": "_subcategory_rank_desc"
                }
              ],
              "groupby": [
                "Category"
              ],
              "sort": [
                {
                  "field": "Subcategory ID",
                  "order": "descending"
                }
              ]
            },
            {
              "calculate": "datum['Category'] == 'Positive' ? datum['_subcategory_rank_asc'] : datum['Category'] == 'Negative' ? datum['_subcategory_rank_desc'] : 1",
              "as": "_subcategory_opacity"
            },
            {
              "calculate": "1 - ( datum['_subcategory_opacity'] * 0.4 )",
              "as": "_subcategory_opacity"
            }
          ],
          "mark": {
            "type": "bar",
            "cornerRadius": 8,
            "height": {
              "band": 0.7
            },
            "color": {
              "expr": "datum['Category'] == 'Positive' ? 'green' : datum['Category'] == 'Negative' ? 'red' : '#C9C9C9'"
            },
            "opacity": {
              "expr": "datum['_subcategory_opacity']"
            }
          }
        },
        {
          "name": "SUBCATEGORY_LABEL",
          "mark": {
            "type": "text",
            "align": "right",
            "xOffset": -10,
            "fontSize": 14
          },
          "encoding": {
            "text": {
              "field": "Subcategory"
            },
            "x": {
              "datum": 0
            }
          }
        },
        {
          "name": "SUBCATEGORY_PERCENT",
          "mark": {
            "type": "text",
            "align": "left",
            "xOffset": 10,
            "fontSize": 14
          },
          "encoding": {
            "text": {
              "field": "Percent",
              "format": ".0%"
            }
          }
        }
      ]
    }
  ]
}

Also included is the development sample PBIX using, as noted above, synthetic survey data.

NOTE: This was a quick-and-dirty development effort, and the first method that worked for each design element was left as-is; this prototype could undoubtably benefit greatly from refinement, but as the point of this effort was to produce a proof-of-concept, design enhancements are left as exercises for further development.

The intent of this example was not to provide a finished visual, but rather to explore the use of the Deneb custom visual and the Vega-Lite language within Power BI and to serve as a starting point for further development.

This example is provided as-is for information purposes only, and its use is solely at the discretion of the end user; no responsibility is assumed by the author.

Greg
Deneb Example - Category Enclosures - V4.pbix (4.6 MB)

1 Like

marking as solved