Skip to content

Commit b5e1321

Browse files
feat(highcharts): implement icicle-basic (#2529)
## Implementation: `icicle-basic` - highcharts Implements the **highcharts** version of `icicle-basic`. **File:** `plots/icicle-basic/implementations/highcharts.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20585401363)* --------- 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 993aa82 commit b5e1321

2 files changed

Lines changed: 358 additions & 0 deletions

File tree

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
""" pyplots.ai
2+
icicle-basic: Basic Icicle Chart
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 94/100 | Created: 2025-12-30
5+
"""
6+
7+
import json
8+
import tempfile
9+
import time
10+
import urllib.request
11+
from pathlib import Path
12+
13+
from PIL import Image
14+
from selenium import webdriver
15+
from selenium.webdriver.chrome.options import Options
16+
17+
18+
# Data - File system hierarchy with folders and files
19+
# Hierarchical structure showing directories and file sizes (KB)
20+
# For icicle chart: root at top, children stacked below in rows
21+
hierarchy = {
22+
"Project Files": {
23+
"src": {
24+
"components": {"Header.tsx": 95, "Footer.tsx": 55, "Sidebar.tsx": 110, "Modal.tsx": 78},
25+
"utils": {"helpers.ts": 42, "constants.ts": 28, "validators.ts": 65},
26+
"api": {"client.ts": 88, "endpoints.ts": 56, "types.ts": 34},
27+
},
28+
"docs": {"README.md": 45, "guide.md": 120, "api.md": 85},
29+
"tests": {"test_main.py": 65, "test_utils.py": 48, "test_api.py": 72},
30+
"assets": {
31+
"images": {"logo.png": 125, "banner.jpg": 280, "icons.svg": 45},
32+
"styles": {"main.css": 92, "theme.css": 68},
33+
},
34+
}
35+
}
36+
37+
38+
# Colorblind-safe palette for directory categories
39+
colors = {"Project Files": "#5A5A5A", "src": "#306998", "docs": "#FFD43B", "tests": "#9467BD", "assets": "#17BECF"}
40+
41+
42+
def get_color(name, parent_chain):
43+
"""Get color based on top-level parent category."""
44+
if name in colors:
45+
return colors[name]
46+
for p in parent_chain:
47+
if p in colors:
48+
base = colors[p]
49+
# Lighter shade for deeper levels
50+
if len(parent_chain) >= 2:
51+
return base + "CC" # Add transparency for lighter appearance
52+
return base
53+
return "#888888"
54+
55+
56+
def calc_size(node):
57+
"""Calculate total size of a node (sum of all descendants)."""
58+
if isinstance(node, (int, float)):
59+
return node
60+
return sum(calc_size(v) for v in node.values())
61+
62+
63+
def build_rectangles(node, x, y, width, height, level, parent_chain, rects, level_height):
64+
"""Recursively build rectangles for icicle chart."""
65+
if isinstance(node, (int, float)):
66+
return
67+
68+
total = calc_size(node)
69+
if total == 0:
70+
return
71+
72+
current_x = x
73+
74+
for name, child in node.items():
75+
child_size = calc_size(child)
76+
child_width = (child_size / total) * width if total > 0 else 0
77+
78+
if child_width > 0:
79+
color = get_color(name, parent_chain)
80+
rect = {
81+
"name": name,
82+
"x": current_x,
83+
"y": y,
84+
"width": child_width,
85+
"height": level_height,
86+
"color": color.replace("CC", ""), # Remove transparency suffix
87+
"level": level,
88+
"size": child_size,
89+
"is_leaf": isinstance(child, (int, float)),
90+
}
91+
rects.append(rect)
92+
93+
# Recurse for children
94+
if not isinstance(child, (int, float)):
95+
build_rectangles(
96+
child,
97+
current_x,
98+
y + level_height,
99+
child_width,
100+
height - level_height,
101+
level + 1,
102+
parent_chain + [name],
103+
rects,
104+
level_height,
105+
)
106+
107+
current_x += child_width
108+
109+
110+
# Build all rectangles
111+
# Chart dimensions (leaving margins for title and legend)
112+
chart_x = 100
113+
chart_y = 200
114+
chart_width = 4600
115+
chart_height = 2000
116+
level_height = 400 # Fixed height per level (5 levels max)
117+
118+
rectangles = []
119+
build_rectangles(hierarchy, chart_x, chart_y, chart_width, chart_height, 0, [], rectangles, level_height)
120+
121+
# Generate SVG rectangles for Highcharts renderer
122+
svg_elements = []
123+
for rect in rectangles:
124+
# Determine font size based on level and rectangle size
125+
if rect["level"] == 0:
126+
font_size = 52
127+
elif rect["level"] == 1:
128+
font_size = 40
129+
elif rect["level"] == 2:
130+
font_size = 32
131+
else:
132+
font_size = 28
133+
134+
# Only show label if rectangle is wide enough
135+
min_width_for_label = font_size * len(rect["name"]) * 0.5
136+
show_label = rect["width"] >= min_width_for_label and rect["height"] >= font_size + 10
137+
138+
svg_elements.append(
139+
{
140+
"x": rect["x"],
141+
"y": rect["y"],
142+
"width": rect["width"],
143+
"height": rect["height"],
144+
"color": rect["color"],
145+
"name": rect["name"],
146+
"size": rect["size"],
147+
"is_leaf": rect["is_leaf"],
148+
"font_size": font_size,
149+
"show_label": show_label,
150+
}
151+
)
152+
153+
# Convert to JavaScript array
154+
rects_json = json.dumps(svg_elements)
155+
156+
# Highcharts renderer to draw custom icicle chart
157+
chart_config = f"""
158+
(function() {{
159+
var rects = {rects_json};
160+
161+
// Create chart with renderer
162+
var chart = Highcharts.chart('container', {{
163+
chart: {{
164+
width: 4800,
165+
height: 2700,
166+
backgroundColor: '#ffffff',
167+
events: {{
168+
load: function() {{
169+
var ren = this.renderer;
170+
171+
// Draw rectangles
172+
rects.forEach(function(r) {{
173+
// Draw rectangle
174+
ren.rect(r.x, r.y, r.width - 3, r.height - 3, 2)
175+
.attr({{
176+
fill: r.color,
177+
stroke: '#ffffff',
178+
'stroke-width': 3,
179+
zIndex: 1
180+
}})
181+
.add();
182+
183+
// Draw label if there's room
184+
if (r.show_label) {{
185+
var labelText = r.name;
186+
if (r.is_leaf) {{
187+
labelText = r.name + ' (' + r.size + ' KB)';
188+
}}
189+
190+
// Truncate if too long for rectangle
191+
var maxChars = Math.floor(r.width / (r.font_size * 0.55));
192+
if (labelText.length > maxChars) {{
193+
labelText = labelText.substring(0, maxChars - 2) + '...';
194+
}}
195+
196+
ren.text(labelText, r.x + r.width / 2, r.y + r.height / 2 + r.font_size / 3)
197+
.attr({{
198+
zIndex: 2
199+
}})
200+
.css({{
201+
color: r.color === '#FFD43B' ? '#333333' : '#ffffff',
202+
fontSize: r.font_size + 'px',
203+
fontWeight: r.font_size >= 40 ? 'bold' : 'normal',
204+
textAnchor: 'middle',
205+
textShadow: r.color === '#FFD43B' ? 'none' : '2px 2px 3px rgba(0,0,0,0.5)'
206+
}})
207+
.add();
208+
}}
209+
}});
210+
211+
// Draw legend at bottom
212+
var legendY = 2450;
213+
var legendItems = [
214+
{{ name: 'src', color: '#306998' }},
215+
{{ name: 'docs', color: '#FFD43B' }},
216+
{{ name: 'tests', color: '#9467BD' }},
217+
{{ name: 'assets', color: '#17BECF' }}
218+
];
219+
var legendX = 1600;
220+
var legendSpacing = 400;
221+
222+
legendItems.forEach(function(item, i) {{
223+
// Legend color box
224+
ren.rect(legendX + i * legendSpacing, legendY, 40, 40, 4)
225+
.attr({{
226+
fill: item.color,
227+
stroke: '#333333',
228+
'stroke-width': 2,
229+
zIndex: 3
230+
}})
231+
.add();
232+
233+
// Legend text
234+
ren.text(item.name, legendX + i * legendSpacing + 55, legendY + 30)
235+
.css({{
236+
color: '#333333',
237+
fontSize: '36px',
238+
fontWeight: 'normal'
239+
}})
240+
.add();
241+
}});
242+
}}
243+
}}
244+
}},
245+
title: {{
246+
text: 'icicle-basic · highcharts · pyplots.ai',
247+
style: {{
248+
fontSize: '56px',
249+
fontWeight: 'bold'
250+
}},
251+
y: 60
252+
}},
253+
subtitle: {{
254+
text: 'File System Structure - Directory sizes shown in KB (root at top, children below)',
255+
style: {{
256+
fontSize: '36px'
257+
}},
258+
y: 120
259+
}},
260+
credits: {{
261+
enabled: false
262+
}}
263+
}});
264+
}})();
265+
"""
266+
267+
# Download Highcharts JS
268+
highcharts_url = "https://code.highcharts.com/highcharts.js"
269+
270+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
271+
highcharts_js = response.read().decode("utf-8")
272+
273+
# Generate HTML with inline scripts
274+
html_content = f"""<!DOCTYPE html>
275+
<html>
276+
<head>
277+
<meta charset="utf-8">
278+
<script>{highcharts_js}</script>
279+
</head>
280+
<body style="margin:0; padding:0; background:#ffffff;">
281+
<div id="container" style="width: 4800px; height: 2700px;"></div>
282+
<script>{chart_config}</script>
283+
</body>
284+
</html>"""
285+
286+
# Save HTML for interactive version
287+
with open("plot.html", "w", encoding="utf-8") as f:
288+
standalone_html = f"""<!DOCTYPE html>
289+
<html>
290+
<head>
291+
<meta charset="utf-8">
292+
<title>icicle-basic · highcharts · pyplots.ai</title>
293+
<script src="https://code.highcharts.com/highcharts.js"></script>
294+
<style>
295+
body {{ margin: 0; padding: 20px; font-family: sans-serif; background: #ffffff; }}
296+
#container {{ width: 100%; height: 90vh; min-height: 600px; }}
297+
</style>
298+
</head>
299+
<body>
300+
<div id="container"></div>
301+
<script>{chart_config}</script>
302+
</body>
303+
</html>"""
304+
f.write(standalone_html)
305+
306+
# Write temp HTML and take screenshot
307+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
308+
f.write(html_content)
309+
temp_path = f.name
310+
311+
chrome_options = Options()
312+
chrome_options.add_argument("--headless")
313+
chrome_options.add_argument("--no-sandbox")
314+
chrome_options.add_argument("--disable-dev-shm-usage")
315+
chrome_options.add_argument("--disable-gpu")
316+
chrome_options.add_argument("--window-size=4800,2900")
317+
318+
driver = webdriver.Chrome(options=chrome_options)
319+
driver.get(f"file://{temp_path}")
320+
time.sleep(5) # Wait for chart to render
321+
driver.save_screenshot("plot_raw.png")
322+
driver.quit()
323+
324+
# Crop to exact 4800x2700 dimensions
325+
img = Image.open("plot_raw.png")
326+
img_cropped = img.crop((0, 0, 4800, 2700))
327+
img_cropped.save("plot.png")
328+
Path("plot_raw.png").unlink()
329+
330+
Path(temp_path).unlink() # Clean up temp file
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: highcharts
2+
specification_id: icicle-basic
3+
created: '2025-12-30T00:05:23Z'
4+
updated: '2025-12-30T00:34:30Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20585401363
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: unknown
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/icicle-basic/highcharts/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/icicle-basic/highcharts/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/icicle-basic/highcharts/plot.html
13+
quality_score: 94
14+
review:
15+
strengths:
16+
- Excellent hierarchical visualization with clear parent-child relationships through
17+
spatial adjacency
18+
- Colorblind-safe palette with good contrast between categories
19+
- Realistic file system example matching spec applications
20+
- Creative use of Highcharts renderer API to implement icicle chart (not a native
21+
chart type)
22+
- Well-formatted title following spec-id · library · pyplots.ai convention
23+
- Good label truncation strategy prevents overlap while showing as much text as
24+
possible
25+
weaknesses:
26+
- Some leaf node labels are truncated (e.g., Footer, Modal.tsx) - could show more
27+
with smaller font
28+
- Uses helper functions instead of pure KISS linear code structure

0 commit comments

Comments
 (0)