Data and Analytics Blog Posts
cancel
Showing results for 
Search instead for 
Did you mean: 
abhimanyu_sharma
Contributor
310

A practical walk-through of building a Power BI-style decomposition tree as an SAP Analytics Cloud custom widget. We will take it from a blank canvas to a smooth, data-bound, production-feeling visualization. This post is written for SAC analytics developers and anyone who has stared at the custom widget SDK wondering exactly where to start.

Quick side note before we jump in: I used Claude AI to help write the code, which made shipping this significantly faster and saved me from a lot of trial and error!

The Gap in the Market

SAP Analytics Cloud has a highly capable chart catalog. However, if you have ever used Power BI's decomposition tree, you know the one major feature it lacks is a left-to-right hierarchical drill-down. You know the exact interaction: each node has a value bar, you click to expand the next level, and you can instantly answer the question of where a specific number is coming from.

This is easily one of the most loved analytics interactions in the BI space. It is exactly how finance, operations, and revenue teams investigate variance across the board: Total → Region → Country → Customer → Product. In SAC, you can build a tree table or a sunburst, but neither of those options feels quite like that intuitive drill-down.

So, I decided to build one. This post covers the story of how it works, what surprised me along the way, and how you can use the final result.

What We Are Building

The goal was to create a custom widget that delivers on all fronts:

  • Appears in the SAC widget picker just like any built-in chart.

  • Lets designers drag dimensions and a measure into a Builder feed (the standard SAC data-binding user experience).

  • Renders a left-to-right tree starting with a synthetic Total root, followed by one column per dimension level. Each node needs a label, a value, and a horizontal bar sized by the measure.

  • Animates expand and collapse actions smoothly.

  • Survives high-cardinality dimensions (like 10,000 unique customers) without freezing the user's browser.

  • Plays nicely with linked analysis filters originating from other widgets.

                          ┌──────────────┐
                          │  Region      │
                          │  Europe      │
                          │  ███████░░   │
                          └──────────────┘
                          ┌──────────────┐
┌──────────────┐    ┌──→  │  Region      │
│  Total       │    │     │  Americas    │
│  Sales       │ ───┤     │  █████░░░░   │
│  ████████    │    │     └──────────────┘
└──────────────┘    │     ┌──────────────┐
                    └──→  │  Region      │
                          │  Asia        │
                          │  ███░░░░░░   │
                          └──────────────┘

Click Americas and it reveals USA, Canada, and Mexico as a third column. Click any of those and it reveals their respective customers.

A Custom Widget Primer

If you have never built one before, SAC custom widgets are essentially Web Components loaded by SAC directly into your story. Each widget consists of a JSON manifest plus one or more JavaScript files and an icon. The JSON declares the widget's name, properties, methods, events, and optionally a data binding feed configuration. The JavaScript files register custom HTML elements (com-yourname-widget) and implement the lifecycle hooks that SAC calls natively (onCustomWidgetBeforeUpdate, onCustomWidgetAfterUpdate, etc.).

You upload the JSON to SAC, then upload a ZIP file containing the JS files and the icon. SAC hosts these resources and serves them to the browser whenever your widget is placed on a canvas.

Architecture Overview

Three main files handle all of the heavy lifting:

  • decomposition_tree.json: The manifest. This declares properties, methods, events, and the data binding configurations (the dimensions feed and the measure feed).

  • decomposition_tree.js: The main Web Component. This handles the D3 layout, click handling, and transitions.

  • decomposition_tree_styling.js: The Styling panel. This controls the color picker, value scale, Top-N settings, and the default expand depth.

Step 1: The Manifest

The JSON manifest is mostly administrative bookkeeping, but two specific parts are critical.

Web Components

"webcomponents": [
  { "kind": "main",    "tag": "com-example-decomposition-tree",         "url": "/decomposition_tree.js" },
  { "kind": "styling", "tag": "com-example-decomposition-tree-styling", "url": "/decomposition_tree_styling.js" }
]

Using forward-slash prefixed URLs ensures the exact same JSON works whether you do a ZIP upload or choose to self-host.

Data Binding

"dataBindings": {
  "treeData": {
    "feeds": [
      { "id": "dimensions", "type": "dimension",            "description": "Hierarchy levels, outer to inner" },
      { "id": "measure",    "type": "mainStructureMember",  "description": "Bar size and node aggregation" }
    ]
  }
}

This single block does an incredible amount of work for us. It tells SAC to auto-generate the feed-config Builder, wires up the data fetch when designers bind a model, and exposes the final result set to the widget as this.treeData.data and this.treeData.metadata at runtime.

Step 2: From a Flat Result Set to a Tree

When dimensions and a measure are bound, SAC hands the widget a flat array of rows that looks something like this:

[
  { dimensions_0: { id: "...", label: "Americas" },
    dimensions_1: { id: "...", label: "USA" },
    dimensions_2: { id: "...", label: "Acme Corp" },
    measure_0:    { raw: 320000, formatted: "320K", unit: "USD" } },
  // ...
]

To draw a decomposition tree, we must convert this into a hierarchy. The folding process is straightforward. We walk each row, descend through the dimension keys, create child nodes on demand, and accumulate the measure:

const root = { name: "Total", value: 0, children: [], _childMap: {} };

for (const row of rows) {
  const v = Number(row.measure_0?.raw) || 0;
  let cursor = root;
  cursor.value += v;
  for (const dimKey of dimAliases) {
    const label = row[dimKey]?.label || "(empty)";
    let child = cursor._childMap[label];
    if (!child) {
      child = { name: label, value: 0, children: [], _childMap: {} };
      cursor._childMap[label] = child;
      cursor.children.push(child);
    }
    child.value += v;
    cursor = child;
  }
}

That is the entire core algorithm. The rest of the code is just performance and user experience scaffolding built around it.

Step 3: Surviving High Cardinality

The first time I bound a Customer dimension with thousands of distinct values, the browser completely hung. Folding ten thousand rows in memory is fine, but rendering ten thousand SVG groups will destroy performance.

The fix is utilizing a Top N + Others grouping.

At every level of the tree, after sorting the children descending by their measure, keep only the top N values and roll the remainder into a single leaf node labeled "Others (count)".

n.children.sort((a, b) => b.value - a.value);
if (n.children.length > topN) {
  const top  = n.children.slice(0, topN);
  const rest = n.children.slice(topN);
  const othersValue = rest.reduce((s, c) => s + c.value, 0);
  n.children = [...top, {
    name: "Others (" + rest.length + ")",
    value: othersValue,
    _isOthers: true
  }];
}

Step 4: Keeping User State Across Data Refreshes

When a user clicks a bar in a different widget wired up via linked analysis, SAC pushes a new result set to your tree. If you naively recompute the tree, the user's expand state completely vanishes. To fix this, you have to track user actions by path string rather than by node reference.

Every click adds an entry to your tracking sets. After a rebuild, walk the new tree and apply the overrides:

this._root.each(n => {
  const path = pathOf(n);
  if (this._userExpandedPaths.has(path))  this._collapsedNodes.delete(n);
  if (this._userCollapsedPaths.has(path)) this._collapsedNodes.add(n);
});

Step 5: Dimension Chips at Runtime

Story viewers often want to try the tree using different decomposition orders without calling a designer. To solve this, I added a clickable dimension chip bar above the tree. Clicking a chip removes that level from the fold, and clicking a disabled chip adds it right back. The tree naturally re-folds on the fly.

Step 6: Visual Polish

To make this feel like a native, premium product, I added some final touches:

  • A clean system font stack.

  • An 8px corner radius with white nodes on a faint gray background.

  • A 3px blue accent strip on the left edge of nodes that have children, providing a subtle visual affordance that the node can expand.

  • A soft drop-shadow on hover using an SVG filter.

  • A selected-path highlight where clicking a node lights up the specific path from the root directly to that node with blue borders and blue links.

Grab the Code and Try It Out

If you want to dive deeper, check out the full source code, or just grab the files to test this in your own SAP Analytics Cloud tenant, I have put everything up on GitHub : Decomposition Tree 

You can find the full repository here: 

I highly recommend reading through the repository README. It contains the complete configuration table, script API details, and some extra technical notes that did not fit into this post.

How to Upload the Widget to Your SAC Tenant

Getting this running in your own environment only takes a minute or two. You do not need to compile anything. Just follow these steps:

  1. Download the files from the GitHub repository.

  2. Create a ZIP file containing exactly three items: decomposition_tree.js, decomposition_tree_styling.js, and icon.png. Leave the JSON file outside of the ZIP.

  3. Log in to your SAP Analytics Cloud tenant.

  4. Navigate to the Custom Widgets area. You can usually find this by going to the left hand navigation menu, clicking Stories, and selecting the Custom Widgets tab.

  5. Click the + Create icon (or Add button) to upload a new widget.

  6. Upload the manifest first. Select the decomposition_tree.json file from your downloaded folder.

  7. Upload the resources. When SAC prompts you for the resource files, select the ZIP file you created in step two.

Happy decomposing!

2 Comments
spurwar
Product and Topic Expert
Product and Topic Expert
StefanArtini
Participant

@abhimanyu_sharma Very cool! Kudos to you! Since we primarily use parent-child hierarchies, unfortunately it's not usable for us, but I get an idea of ​​what the widget can do.
Screenshot 2026-05-06 064437.png