We've all seen them—those lifeless pie charts buried in dashboards, silently displaying data with no context, no story, and no spark. A few weeks ago, I found myself staring at one and thinking, "There has to be a better way." I didn't want just another chart; I wanted a visual that helped people explore, discover, and actually enjoy interacting with data.

That's how this idea was born: an interactive pie chart built entirely in Power BI using DAX, SVG, and a little embedded JavaScript—all inside a calculated column. The goal? To make profit analysis not only informative, but engaging and intuitive.


The Challenge: Making Data Come Alive

The task seemed simple: create a pie chart that compares current year profit, previous year profit, and the delta between them. But I wasn't satisfied with static slices. I wanted a chart that would:

  • Establish a visual hierarchy to highlight what matters

  • Use smooth animations for a polished feel

  • Include interactive elements that encourage exploration

  • Tell a clear story through thoughtful design


The Technical Deep Dive

1. Starting with the Math

Any strong visualization starts with solid math. I calculated key metrics using DAX:

dax
VAR _cy = [Profit] VAR _py = [PY Profit] VAR _delta = _cy - _py
VAR _absDelta = ABS(_delta)
VAR _total = _cy + _py

I didn't want the delta to be just another slice. Instead, it had to visually represent the change between years—an addition or subtraction depending on whether profits went up or down. This required careful angle calculations to reflect that narrative visually.


2. The SVG Path Challenge

SVG paths are what make this chart tick. Each slice is drawn based on:

  • Start and end angles (in radians)

  • X and Y coordinates for the arc's endpoints

  • A flag for arcs greater than 180 degrees

  • Careful ordering to avoid overlap

Here's a simplified breakdown in DAX:

dax
VAR _cyStartAngle = -90 // Start at top
VAR _cyEndAngle = _cyStartAngle + 360 * _cyPct
VAR _deltaStartAngle = IF(_delta > 0, _cyEndAngle + 360 * _pyPct, _cyEndAngle)

Negative deltas were especially tricky. I had to "subtract" a visual wedge from the current year slice, requiring realignment of angles and rethinking slice positioning for clarity and intuitive storytelling.


3. Making It Interactive

Static visuals don't hold attention. I wanted users to hover, highlight, and explore the chart with ease. But since everything lives within Power BI's DAX and HTML constraints, I had to get creative.

I embedded CSS animations and JavaScript event handlers directly into the generated HTML:

css
.slice
.slice:hover

Then I linked the chart slices with the legend via JavaScript:

javascript
item.addEventListener('mouseenter', () => slice.classList.add('hovered'));

Now, when users hover over a legend item, the corresponding chart slice highlights—and vice versa. It creates a seamless, two-way interaction.


Solving the Tricky Parts

This project pushed the limits of what DAX and Power BI can do. Here are a few of the hardest challenges I tackled—and how I solved them:

🔧 Precision in Coordinates

SVG rendering gets wonky when your coordinates have too many or too few decimals.

Solution: I rounded everything to 4 decimal places using DAX's ROUND() function. This avoided visual gaps while keeping paths smooth and reliable.


🎬 Animation Timing

Animations needed to be snappy but not jarring. After testing various timing functions, I settled on:

  • 0.8s for initial "bubble" reveal (feels cinematic)

  • 0.3s for hover effects (feels responsive)

  • Ease-out for scaling (feels natural)


🎨 Delta Slice Coloring

The delta slice needed to relate visually to the profit it was changing, while still standing out.

Solution: I assigned its color conditionally:

dax
VAR _deltaFillColor = IF(_delta > 0, _cyFillColor, _pyFillColor)

Then I added a dashed border to distinguish it as a delta, not a static value. This subtle touch reinforces its meaning without cluttering the chart.


📱 Responsive Layout

The chart had to work across various screen sizes.

Solution: I used SVG's viewBox and preserveAspectRatio for scalable vector rendering, and wrapped the whole layout in CSS flexbox containers for a clean, responsive structure.


The Results: Visuals with Purpose

This pie chart does more than just show data—it tells a story the moment you see it. Users can instantly grasp:

  1. The relative size of current vs. previous year profit

  2. The direction of change (up or down)

  3. The magnitude of that change

  4. Exact values via tooltips and interactive legend

More importantly, people enjoy using it. The interactivity makes them want to explore the data instead of just glancing over it. And that, to me, is what real data storytelling looks like.


Lessons Learned

Here's what I took away from the experience:

📐 Precision Powers Design

Good math and geometry aren't optional—they're what separate an okay chart from an exceptional one.

🧠 Constraints Inspire Creativity

Working within Power BI's limitations forced me to invent solutions I wouldn't have considered otherwise.

🤝 UX Over Complexity

A complex solution means nothing if the user doesn't find it intuitive and meaningful.

⚡ Performance is a Feature

Animations and interactions aren't just for flair—they drive usability and engagement.


The Bigger Picture

This project wasn't just about one pie chart—it was about a mindset shift.

Data visualization isn't just about showing numbers. It's about guiding your audience to discover insights. To do that, we need to move beyond static visuals and embrace design, interactivity, and storytelling as first-class citizens of BI work.


Want to see the full DAX code behind this visualization?
The complete implementation includes edge-case handling, responsiveness, accessibility, and everything else needed to make this chart production-ready for your Power BI environment.

VAR _cy = [Profit] VAR _py = [PY Profit] VAR _delta = _cy – _py
VAR _absDelta = ABS(_delta)

VAR _total = _cy + _py

VAR _cyPct = DIVIDE(_cy, _total, 0)
VAR _pyPct = DIVIDE(_py, _total, 0)
VAR _deltaPct = DIVIDE(_absDelta, _total, 0)

VAR _cyStartAngle = -90
VAR _cyEndAngle = _cyStartAngle + 360 * _cyPct

VAR _deltaStartAngle = IF(_delta > 0, _cyEndAngle + 360 * _pyPct, _cyEndAngle)
VAR _deltaEndAngle = _deltaStartAngle + 360 * _deltaPct

VAR _pyStartAngle = IF(_delta > 0, _cyEndAngle + 360, _cyEndAngle)
VAR _pyEndAngle = -90

VAR _cyLargeArc = IF(_cyPct > 0.5, 1, 0)
VAR _pyLargeArc = IF(_pyPct > 0.5, 1, 0)
VAR _deltaLargeArc = IF(_deltaPct > 0.5, 1, 0)

VAR _baseSize = 100
VAR _borderWidth = 0.7
VAR _borderWidth_delta = 0.4
VAR _borderColor = “#888888”
VAR _dashArray = “1.5,1”

VAR _shadowSpread = 6
VAR _shadowOpacity = 0.8
VAR _shadowStdDev = 1.5

VAR _hoverScale = 1.08
VAR _maxExtraPad = (_baseSize * (_hoverScale – 1)) / 2
VAR _padding = _borderWidth + _maxExtraPad + _shadowSpread

VAR _viewBoxStart = -_padding
VAR _viewBoxSize = _baseSize + 2 * _padding

VAR _radius = (_baseSize / 2) – _padding
VAR _center = _baseSize / 2
VAR _toRadians = PI() / 180

VAR _cyStartX = ROUND(_center + _radius * COS(_cyStartAngle * _toRadians), 4)
VAR _cyStartY = ROUND(_center + _radius * SIN(_cyStartAngle * _toRadians), 4)
VAR _cyEndX = ROUND(_center + _radius * COS(_cyEndAngle * _toRadians), 4)
VAR _cyEndY = ROUND(_center + _radius * SIN(_cyEndAngle * _toRadians), 4)

VAR _pyStartX = ROUND(_center + _radius * COS(_pyStartAngle * _toRadians), 4)
VAR _pyStartY = ROUND(_center + _radius * SIN(_pyStartAngle * _toRadians), 4)
VAR _pyEndX = ROUND(_center + _radius * COS(_pyEndAngle * _toRadians), 4)
VAR _pyEndY = ROUND(_center + _radius * SIN(_pyEndAngle * _toRadians), 4)

VAR _deltaStartX = ROUND(_center + _radius * COS(_deltaStartAngle * _toRadians), 4)
VAR _deltaStartY = ROUND(_center + _radius * SIN(_deltaStartAngle * _toRadians), 4)
VAR _deltaEndX = ROUND(_center + _radius * COS(_deltaEndAngle * _toRadians), 4)
VAR _deltaEndY = ROUND(_center + _radius * SIN(_deltaEndAngle * _toRadians), 4)

VAR _cyPath =
“M” & _center & “,” & _center &
” L” & _cyStartX & “,” & _cyStartY &
” A” & _radius & “,” & _radius & ” 0 ” & _cyLargeArc & “,1 ” & _cyEndX & “,” & _cyEndY & ” Z”

VAR _pyPath =
“M” & _center & “,” & _center &
” L” & _pyStartX & “,” & _pyStartY &
” A” & _radius & “,” & _radius & ” 0 ” & _pyLargeArc & “,1 ” & _pyEndX & “,” & _pyEndY & ” Z”

VAR _deltaPath =
“M” & _center & “,” & _center &
” L” & _deltaStartX & “,” & _deltaStartY &
” A” & _radius & “,” & _radius & ” 0 ” & _deltaLargeArc & “,1 ” & _deltaEndX & “,” & _deltaEndY & ” Z”

VAR _cyFillColor = “#2196F3”
VAR _pyFillColor = “#FFC107”
VAR _deltaFillColor = IF(_delta > 0, _cyFillColor, _pyFillColor)
VAR _deltaBorderColor = “#000000”

VAR _cyTooltip = FORMAT(_cy, “$#,##0″) & ” (” & FORMAT(_cyPct, “0%”) & “)”
VAR _pyTooltip = FORMAT(_py, “$#,##0″) & ” (” & FORMAT(_pyPct, “0%”) & “)”
VAR _deltaTooltip = “Δ Profit: ” & FORMAT(_delta, “$#,##0″) & ” (” & FORMAT(_deltaPct, “0%”) & “)”

RETURN
“<div id=’pie21-root’ style=’width: 100%; height: 100%; font-family: sans-serif; color: #333; display: flex; flex-direction: column; align-items: center; justify-content: center; box-sizing: border-box; padding: 1rem;’>
<svg viewBox='” & _viewBoxStart & ” ” & _viewBoxStart & ” ” & _viewBoxSize & ” ” & _viewBoxSize & “‘
xmlns=’http://www.w3.org/2000/svg’
style=’width: 100%; max-height: 70vh; display: block; margin: auto;’
preserveAspectRatio=’xMidYMid meet’>

<defs>
<filter id=’shadow’ x=’-40%’ y=’-40%’ width=’180%’ height=’180%’ color-interpolation-filters=’sRGB’>
<feDropShadow dx=’0′ dy=’0′ stdDeviation='” & _shadowStdDev & “‘ flood-color=’black’ flood-opacity='” & _shadowOpacity & “‘ />
</filter>
<style>
@keyframes bubbleReveal {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.pie-group {
transform-origin: ” & _center & “px ” & _center & “px;
animation: bubbleReveal .8s ease-out forwards;
}
.slice {
transition: transform 0.3s ease, filter 0.3s ease;
transform-origin: ” & _center & “px ” & _center & “px;
}
.slice:hover,
.slice.hovered {
transform: scale(” & _hoverScale & “);
filter: url(#shadow);
}
.legend {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 12px;
font-size: 1rem;
user-select: none;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.3rem;
cursor: pointer;
color: #333;
padding: 0.3rem 0.6rem;
border: 1px solid transparent;
border-radius: 4px;
transition: color 0.3s, border-color 0.3s;
}
.legend-item:hover,
.legend-item.hovered {
color: #000;
border-color: #aaa;
}
</style>
</defs>

<g class=’pie-group’>
<path id=’slice-cy’ class=’slice’
d='” & _cyPath & “‘ fill='” & _cyFillColor & “‘ stroke='” & _borderColor & “‘ stroke-width='” & _borderWidth & “‘>
<title>” & _cyTooltip & “</title>
</path>
<path id=’slice-py’ class=’slice’
d='” & _pyPath & “‘ fill='” & _pyFillColor & “‘ stroke='” & _borderColor & “‘ stroke-width='” & _borderWidth & “‘>
<title>” & _pyTooltip & “</title>
</path>
<path id=’slice-delta’ class=’slice’
d='” & _deltaPath & “‘ fill='” & _deltaFillColor & “‘ stroke='” & _deltaBorderColor & “‘
stroke-width='” & _borderWidth_delta & “‘ stroke-dasharray='” & _dashArray & “‘>
<title>” & _deltaTooltip & “</title>
</path>
</g>
</svg>

<div class=’legend’>
<div class=’legend-item’ tabindex=’0′ data-target=’slice-py’>
<svg width=’13’ height=’13’>
<rect width=’13’ height=’13’ fill='” & _pyFillColor & “‘ stroke='” & _borderColor & “‘ stroke-width=’0.7′ />
</svg>
<span>PY Profit: ” & FORMAT(_py, “$#,##0”) & “</span>
</div>
<div class=’legend-item’ tabindex=’0′ data-target=’slice-cy’>
<svg width=’13’ height=’13’>
<rect width=’13’ height=’13’ fill='” & _cyFillColor & “‘ stroke='” & _borderColor & “‘ stroke-width=’0.7′ />
</svg>
<span>Profit: ” & FORMAT(_cy, “$#,##0”) & “</span>
</div>
<div class=’legend-item’ tabindex=’0′ data-target=’slice-delta’>
<svg width=’13’ height=’13’>
<rect width=’13’ height=’13’ fill='” & _deltaFillColor & “‘ stroke='” & _deltaBorderColor & “‘ stroke-width=’3’ stroke-dasharray='” & _dashArray & “‘ />
</svg>
<span>Δ Profit: ” & FORMAT(_delta, “$#,##0”) & “</span>
</div>
</div>
</div>

<script>
setTimeout(() => {
const root = document.getElementById(‘pie21-root’);
if (!root) return;
root.querySelectorAll(‘.legend-item’).forEach(item => {
const id = item.dataset.target;
const slice = root.querySelector(‘#’ + id);
item.addEventListener(‘mouseenter’, () => slice.classList.add(‘hovered’));
item.addEventListener(‘mouseleave’, () => slice.classList.remove(‘hovered’));
item.addEventListener(‘focus’, () => slice.classList.add(‘hovered’));
item.addEventListener(‘blur’, () => slice.classList.remove(‘hovered’));
});
}, 0);
</script>”