Skip to content

Commit 480a4a4

Browse files
feat(plotly): implement pie-drilldown (#3121)
## Implementation: `pie-drilldown` - plotly Implements the **plotly** version of `pie-drilldown`. **File:** `plots/pie-drilldown/implementations/plotly.py` **Parent Issue:** #3072 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20620475114)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent c9a6424 commit 480a4a4

2 files changed

Lines changed: 282 additions & 0 deletions

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
""" pyplots.ai
2+
pie-drilldown: Drilldown Pie Chart with Click Navigation
3+
Library: plotly 6.5.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import json
8+
9+
import plotly.graph_objects as go
10+
11+
12+
# Hierarchical data structure for company budget breakdown
13+
# Structure: Department -> Team -> Expense Category
14+
# Leaf nodes have direct values, parent nodes derive values from children
15+
hierarchy = {
16+
"All": {"children": ["Engineering", "Marketing", "Operations", "Sales"], "parent": None},
17+
"Engineering": {"children": ["Backend", "Frontend", "DevOps", "QA"], "parent": "All"},
18+
"Marketing": {"children": ["Digital", "Content", "Events", "Brand"], "parent": "All"},
19+
"Operations": {"children": ["HR", "Finance", "Legal", "Facilities"], "parent": "All"},
20+
"Sales": {"children": ["Enterprise", "SMB", "Partnerships"], "parent": "All"},
21+
# Engineering subcategories (leaf nodes with values)
22+
"Backend": {"value": 850000, "parent": "Engineering"},
23+
"Frontend": {"value": 620000, "parent": "Engineering"},
24+
"DevOps": {"value": 480000, "parent": "Engineering"},
25+
"QA": {"value": 350000, "parent": "Engineering"},
26+
# Marketing subcategories
27+
"Digital": {"value": 420000, "parent": "Marketing"},
28+
"Content": {"value": 280000, "parent": "Marketing"},
29+
"Events": {"value": 350000, "parent": "Marketing"},
30+
"Brand": {"value": 180000, "parent": "Marketing"},
31+
# Operations subcategories
32+
"HR": {"value": 320000, "parent": "Operations"},
33+
"Finance": {"value": 290000, "parent": "Operations"},
34+
"Legal": {"value": 410000, "parent": "Operations"},
35+
"Facilities": {"value": 250000, "parent": "Operations"},
36+
# Sales subcategories
37+
"Enterprise": {"value": 680000, "parent": "Sales"},
38+
"SMB": {"value": 420000, "parent": "Sales"},
39+
"Partnerships": {"value": 300000, "parent": "Sales"},
40+
}
41+
42+
# Pre-compute parent values (sum of children)
43+
hierarchy["Engineering"]["value"] = sum(hierarchy[c]["value"] for c in hierarchy["Engineering"]["children"])
44+
hierarchy["Marketing"]["value"] = sum(hierarchy[c]["value"] for c in hierarchy["Marketing"]["children"])
45+
hierarchy["Operations"]["value"] = sum(hierarchy[c]["value"] for c in hierarchy["Operations"]["children"])
46+
hierarchy["Sales"]["value"] = sum(hierarchy[c]["value"] for c in hierarchy["Sales"]["children"])
47+
48+
# Color palette for main categories (colorblind-safe, distinct hues)
49+
colors = {
50+
"Engineering": "#306998", # Blue
51+
"Marketing": "#9467BD", # Purple (changed from yellow for better distinction)
52+
"Operations": "#2CA02C", # Green
53+
"Sales": "#E24A33", # Red-orange (changed from orange for better distinction)
54+
}
55+
56+
# Lighter shades for subcategories (matching parent color families)
57+
sub_colors = {
58+
# Engineering - Blue shades
59+
"Backend": "#4A8BBE",
60+
"Frontend": "#6BA3D6",
61+
"DevOps": "#8CBBEE",
62+
"QA": "#ADD3F5",
63+
# Marketing - Purple shades
64+
"Digital": "#A87ACC",
65+
"Content": "#BB94DB",
66+
"Events": "#CEAEEA",
67+
"Brand": "#E1C8F9",
68+
# Operations - Green shades
69+
"HR": "#4DC04D",
70+
"Finance": "#70D070",
71+
"Legal": "#93E093",
72+
"Facilities": "#B6F0B6",
73+
# Sales - Red-orange shades
74+
"Enterprise": "#E86F5C",
75+
"SMB": "#F09485",
76+
"Partnerships": "#F7B9AE",
77+
}
78+
79+
# Get data for top level view
80+
current_level = "All"
81+
children = hierarchy[current_level]["children"]
82+
values = [hierarchy[child]["value"] for child in children]
83+
total = sum(values)
84+
slice_colors = [colors[child] for child in children]
85+
86+
# Create pie chart
87+
fig = go.Figure()
88+
89+
# Main pie chart with sort=False to preserve order (legend matches clockwise visual order)
90+
fig.add_trace(
91+
go.Pie(
92+
labels=children,
93+
values=values,
94+
hole=0.3, # Donut style for modern look
95+
textinfo="label+percent",
96+
textposition="outside",
97+
textfont={"size": 20},
98+
hovertemplate="<b>%{label}</b><br>"
99+
+ "Value: $%{value:,.0f}<br>"
100+
+ "Percentage: %{percent}<br>"
101+
+ "<extra>Click to drill down</extra>",
102+
marker={"colors": slice_colors, "line": {"color": "white", "width": 3}},
103+
pull=[0.02] * len(children), # Slight separation
104+
sort=False, # Preserve data order so legend matches clockwise visual order
105+
)
106+
)
107+
108+
# Add center text showing current view
109+
fig.add_annotation(
110+
text="<b>Company Budget</b><br>FY 2024",
111+
x=0.5,
112+
y=0.5,
113+
font={"size": 24, "color": "#333333"},
114+
showarrow=False,
115+
xref="paper",
116+
yref="paper",
117+
)
118+
119+
# Add breadcrumb navigation at the top
120+
fig.add_annotation(
121+
text="📍 All Departments",
122+
x=0.02,
123+
y=0.98,
124+
xanchor="left",
125+
yanchor="top",
126+
font={"size": 22, "color": "#306998"},
127+
showarrow=False,
128+
xref="paper",
129+
yref="paper",
130+
bgcolor="rgba(255,255,255,0.9)",
131+
borderpad=8,
132+
)
133+
134+
# Add click instruction
135+
fig.add_annotation(
136+
text="👆 Click any slice to drill down into subcategories",
137+
x=0.5,
138+
y=0.02,
139+
xanchor="center",
140+
yanchor="bottom",
141+
font={"size": 18, "color": "#666666"},
142+
showarrow=False,
143+
xref="paper",
144+
yref="paper",
145+
)
146+
147+
# Update layout
148+
fig.update_layout(
149+
title={
150+
"text": "pie-drilldown · plotly · pyplots.ai",
151+
"font": {"size": 28, "color": "#333333"},
152+
"x": 0.5,
153+
"xanchor": "center",
154+
},
155+
showlegend=True,
156+
legend={
157+
"orientation": "v",
158+
"yanchor": "middle",
159+
"y": 0.5,
160+
"xanchor": "left",
161+
"x": 1.02,
162+
"font": {"size": 18},
163+
"title": {"text": "Departments", "font": {"size": 20}},
164+
},
165+
template="plotly_white",
166+
margin={"l": 60, "r": 200, "t": 100, "b": 80},
167+
)
168+
169+
# Custom JavaScript for drilldown functionality (in HTML output)
170+
# Uses json.dumps for robust serialization (no fragile string replacement)
171+
hierarchy_json = json.dumps(hierarchy)
172+
colors_json = json.dumps(colors)
173+
sub_colors_json = json.dumps(sub_colors)
174+
175+
drilldown_js = f"""
176+
<script>
177+
var hierarchyData = {hierarchy_json};
178+
var colors = {colors_json};
179+
var subColors = {sub_colors_json};
180+
var currentPath = ['All'];
181+
182+
function getValue(nodeName) {{
183+
return hierarchyData[nodeName].value;
184+
}}
185+
186+
function updateBreadcrumb() {{
187+
var breadcrumb = '📍 ' + currentPath.join(' > ');
188+
var annotations = document.querySelectorAll('.annotation-text');
189+
annotations.forEach(function(el) {{
190+
if (el.textContent.startsWith('📍')) {{
191+
el.textContent = breadcrumb;
192+
}}
193+
}});
194+
}}
195+
196+
function updateChart(level) {{
197+
var node = hierarchyData[level];
198+
if (!node.children) return;
199+
200+
var children = node.children;
201+
var values = children.map(c => getValue(c));
202+
var sliceColors = children.map(c => subColors[c] || colors[c] || '#306998');
203+
204+
Plotly.animate('plotly-chart', {{
205+
data: [{{
206+
labels: children,
207+
values: values,
208+
marker: {{colors: sliceColors}}
209+
}}],
210+
layout: {{}}
211+
}}, {{
212+
transition: {{duration: 500, easing: 'cubic-in-out'}},
213+
frame: {{duration: 500}}
214+
}});
215+
updateBreadcrumb();
216+
}}
217+
218+
document.getElementById('plotly-chart').on('plotly_click', function(data) {{
219+
var clickedLabel = data.points[0].label;
220+
if (hierarchyData[clickedLabel] && hierarchyData[clickedLabel].children) {{
221+
currentPath.push(clickedLabel);
222+
updateChart(clickedLabel);
223+
}}
224+
}});
225+
226+
// Double-click to go back up the hierarchy
227+
document.getElementById('plotly-chart').on('plotly_doubleclick', function() {{
228+
if (currentPath.length > 1) {{
229+
currentPath.pop();
230+
updateChart(currentPath[currentPath.length - 1]);
231+
}}
232+
}});
233+
</script>
234+
"""
235+
236+
# Save as PNG (static image for main output)
237+
fig.write_image("plot.png", width=1600, height=900, scale=3)
238+
239+
# Save as HTML (interactive version with drilldown)
240+
html_content = fig.to_html(
241+
full_html=True,
242+
include_plotlyjs=True,
243+
div_id="plotly-chart",
244+
config={
245+
"displayModeBar": True,
246+
"modeBarButtonsToRemove": ["lasso2d", "select2d"],
247+
"toImageButtonOptions": {"format": "png", "width": 4800, "height": 2700},
248+
},
249+
)
250+
251+
# Inject drilldown JavaScript before closing body tag
252+
html_content = html_content.replace("</body>", drilldown_js + "</body>")
253+
254+
with open("plot.html", "w") as f:
255+
f.write(html_content)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library: plotly
2+
specification_id: pie-drilldown
3+
created: '2025-12-31T14:04:59Z'
4+
updated: '2025-12-31T14:26:12Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20620475114
7+
issue: 3072
8+
python_version: 3.13.11
9+
library_version: 6.5.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/pie-drilldown/plotly/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/pie-drilldown/plotly/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/pie-drilldown/plotly/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent use of Plotly interactive capabilities with custom JavaScript for drilldown
17+
navigation
18+
- Professional colorblind-safe color scheme with consistent subcategory shades
19+
- Robust JSON serialization for data passing to JavaScript (no fragile string replacement)
20+
- Clear breadcrumb navigation and user instructions
21+
- Clean donut style with center annotation adds modern aesthetic
22+
- Dual output (PNG + HTML) maximizes utility
23+
weaknesses:
24+
- Minor text overlap between breadcrumb annotation and Marketing slice label in
25+
top-left area
26+
- Static PNG cannot demonstrate the drilldown functionality - consider adjusting
27+
breadcrumb position to avoid overlap

0 commit comments

Comments
 (0)