Skip to content

Commit ce38780

Browse files
MarkusNeusingerclaudegithub-actions[bot]
authored
update(area-basic): highcharts — comprehensive quality review (#4180)
## Summary Updated **highcharts** implementation for **area-basic**. **Changes:** Comprehensive quality review fixing all review weaknesses (score 74 → REJECTED). ### Changes - Removed Y-axis min:1000 → set to 1500 with startOnTick:false — eliminates ~30% wasted vertical space (VQ-05) - Increased marginBottom + added X-axis title margin/offset — ensures "Day of Month" renders visible (VQ-06) - Removed unnecessary standalone plot.html generation — only plot.png output (CQ-05) - Enabled legend with styled positioning for series identification (SC-05) - Reduced marker radius from 8 to 6 for better visual density - Added subtitle "Daily Website Visitors Over One Month" for data context - Added tickInterval:1 on X-axis to show every day number - Disabled Highcharts credits watermark - Softened gradient bottom stop from 0.1 to 0.05 opacity - Quality self-assessment: 88/100 ## Test Plan - [x] Preview images uploaded to GCS staging - [x] Implementation file passes ruff format/check - [x] Metadata YAML updated with current versions - [ ] Automated review triggered --- Generated with [Claude Code](https://claude.com/claude-code) `/update` command --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 179e8d3 commit ce38780

6 files changed

Lines changed: 141 additions & 121 deletions

File tree

agentic/workflows/modules/agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def parse_json(output: str, target_type: Type[T] = None) -> Any:
144144
return [target_type.model_validate(item) for item in parsed]
145145
return target_type.model_validate(parsed)
146146
return parsed
147-
except (json.JSONDecodeError, ValueError):
147+
except json.JSONDecodeError, ValueError:
148148
pass
149149

150150
# Strategy 2: Strip markdown code fences
@@ -162,7 +162,7 @@ def parse_json(output: str, target_type: Type[T] = None) -> Any:
162162
return [target_type.model_validate(item) for item in parsed]
163163
return target_type.model_validate(parsed)
164164
return parsed
165-
except (json.JSONDecodeError, ValueError):
165+
except json.JSONDecodeError, ValueError:
166166
pass
167167

168168
# Strategy 3: Find first JSON array or object in output
@@ -182,7 +182,7 @@ def parse_json(output: str, target_type: Type[T] = None) -> Any:
182182
return [target_type.model_validate(item) for item in parsed]
183183
return target_type.model_validate(parsed)
184184
return parsed
185-
except (json.JSONDecodeError, ValueError):
185+
except json.JSONDecodeError, ValueError:
186186
continue
187187

188188
raise json.JSONDecodeError("No valid JSON found in output", output, 0)

agentic/workflows/modules/orchestrator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,5 @@ def extract_run_id(stdout: str) -> str | None:
4545
try:
4646
data = json.loads(stdout.strip())
4747
return data.get("run_id")
48-
except (json.JSONDecodeError, ValueError):
48+
except json.JSONDecodeError, ValueError:
4949
return None

agentic/workflows/modules/state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def from_stdin(cls) -> Optional["WorkflowState"]:
172172
state = cls(run_id=run_id, prompt=data.get("prompt", ""))
173173
state.data = data
174174
return state
175-
except (json.JSONDecodeError, EOFError):
175+
except json.JSONDecodeError, EOFError:
176176
return None
177177

178178
def to_stdout(self) -> None:

core/images.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
import subprocess
4646

4747
_HAS_PNGQUANT = subprocess.run(["pngquant", "--version"], capture_output=True).returncode == 0
48-
except (FileNotFoundError, subprocess.SubprocessError):
48+
except FileNotFoundError, subprocess.SubprocessError:
4949
_HAS_PNGQUANT = False
5050

5151

@@ -196,7 +196,7 @@ def create_comparison_image(
196196
# Header bar
197197
header_text = f"{library} · {spec_id}" if library and spec_id else library or spec_id
198198
if header_text:
199-
header_font = _get_font(28, weight=700)
199+
header_font = _get_font(28, weight=700, local_only=True)
200200
bbox = draw.textbbox((0, 0), header_text, font=header_font)
201201
text_w = bbox[2] - bbox[0]
202202
text_h = bbox[3] - bbox[1]
@@ -208,7 +208,7 @@ def create_comparison_image(
208208
)
209209

210210
# Labels
211-
label_font = _get_font(22, weight=400)
211+
label_font = _get_font(22, weight=400, local_only=True)
212212
before_label = "BEFORE (current)"
213213
after_label = "AFTER (updated)"
214214

@@ -242,7 +242,7 @@ def create_comparison_image(
242242
canvas.paste(before_img, (bx, by))
243243
else:
244244
# Gray placeholder
245-
placeholder_font = _get_font(20, weight=400)
245+
placeholder_font = _get_font(20, weight=400, local_only=True)
246246
placeholder_text = "No previous version"
247247
pb = draw.textbbox((0, 0), placeholder_text, font=placeholder_font)
248248
pw = pb[2] - pb[0]
@@ -276,9 +276,12 @@ def _fit_image(img: Image.Image, max_width: int, max_height: int) -> Image.Image
276276
# =============================================================================
277277

278278

279-
def _get_monolisa_font_path() -> Path | None:
279+
def _get_monolisa_font_path(local_only: bool = False) -> Path | None:
280280
"""Get path to MonoLisa font, downloading from GCS if needed.
281281
282+
Args:
283+
local_only: If True, only return the font if already cached locally.
284+
282285
Returns:
283286
Path to font file, or None if unavailable.
284287
"""
@@ -288,6 +291,9 @@ def _get_monolisa_font_path() -> Path | None:
288291
if cached_font.exists():
289292
return cached_font
290293

294+
if local_only:
295+
return None
296+
291297
# Try to download from GCS
292298
try:
293299
from google.cloud import storage
@@ -305,17 +311,20 @@ def _get_monolisa_font_path() -> Path | None:
305311
return None
306312

307313

308-
def _get_font(size: int = 32, weight: int = 700) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
314+
def _get_font(
315+
size: int = 32, weight: int = 700, local_only: bool = False
316+
) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
309317
"""Get a suitable font for text rendering.
310318
311319
Tries to load MonoLisa from GCS cache, falls back to system fonts.
312320
313321
Args:
314322
size: Font size in pixels
315323
weight: Font weight (100-900, default 700 for bold like website)
324+
local_only: If True, skip GCS download and only use locally cached or system fonts.
316325
"""
317-
# Try MonoLisa first (downloaded from GCS)
318-
monolisa_path = _get_monolisa_font_path()
326+
# Try MonoLisa (from local cache, or download from GCS if allowed)
327+
monolisa_path = _get_monolisa_font_path(local_only=local_only)
319328
if monolisa_path:
320329
try:
321330
font = ImageFont.truetype(str(monolisa_path), size)

plots/area-basic/implementations/highcharts.py

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" pyplots.ai
22
area-basic: Basic Area Chart
33
Library: highcharts 1.10.3 | Python 3.14.2
4-
Quality: 74/100 | Created: 2025-12-23
4+
Quality: 91/100 | Created: 2025-12-23
55
"""
66

77
import tempfile
@@ -31,14 +31,15 @@
3131
chart = Chart(container="container")
3232
chart.options = HighchartsOptions()
3333

34-
# Chart configuration
34+
# Chart configuration — generous bottom margin to ensure x-axis title renders fully
3535
chart.options.chart = {
3636
"type": "area",
3737
"width": 4800,
3838
"height": 2700,
3939
"backgroundColor": "#ffffff",
40-
"marginBottom": 200,
41-
"marginLeft": 200,
40+
"marginBottom": 300,
41+
"marginLeft": 220,
42+
"spacingBottom": 40,
4243
}
4344

4445
# Title
@@ -47,39 +48,58 @@
4748
"style": {"fontSize": "72px", "fontWeight": "bold"},
4849
}
4950

50-
# X-axis
51+
# Subtitle for data context
52+
chart.options.subtitle = {
53+
"text": "Daily Website Visitors Over One Month",
54+
"style": {"fontSize": "42px", "color": "#666666"},
55+
}
56+
57+
# X-axis — explicit margin and offset to prevent title clipping
5158
chart.options.x_axis = {
52-
"title": {"text": "Day of Month", "style": {"fontSize": "48px"}},
53-
"labels": {"style": {"fontSize": "36px"}},
59+
"title": {"text": "Day of Month", "style": {"fontSize": "48px"}, "margin": 30},
60+
"labels": {"style": {"fontSize": "36px"}, "y": 45},
5461
"gridLineWidth": 1,
5562
"gridLineColor": "rgba(0, 0, 0, 0.1)",
63+
"tickInterval": 1,
5664
}
5765

58-
# Y-axis — set min close to data range to avoid wasted whitespace
66+
# Y-axis — min near data floor to maximize visual resolution of the data range
5967
chart.options.y_axis = {
6068
"title": {"text": "Daily Visitors (count)", "style": {"fontSize": "48px"}},
6169
"labels": {"style": {"fontSize": "36px"}},
6270
"gridLineWidth": 1,
6371
"gridLineColor": "rgba(0, 0, 0, 0.1)",
64-
"min": 1000,
72+
"min": 1500,
73+
"startOnTick": False,
6574
}
6675

6776
# Plot options with semi-transparent fill and gradient
6877
chart.options.plot_options = {
6978
"area": {
7079
"fillColor": {
7180
"linearGradient": {"x1": 0, "y1": 0, "x2": 0, "y2": 1},
72-
"stops": [[0, "rgba(48, 105, 152, 0.5)"], [1, "rgba(48, 105, 152, 0.1)"]],
81+
"stops": [[0, "rgba(48, 105, 152, 0.5)"], [1, "rgba(48, 105, 152, 0.05)"]],
7382
},
7483
"lineWidth": 4,
75-
"marker": {"enabled": True, "radius": 8, "fillColor": "#306998"},
84+
"marker": {"enabled": True, "radius": 6, "fillColor": "#306998"},
7685
"color": "#306998",
7786
"tooltip": {"headerFormat": "<b>Day {point.x}</b><br/>", "pointFormat": "Visitors: {point.y:,.0f}"},
7887
}
7988
}
8089

81-
# Legend
82-
chart.options.legend = {"enabled": False}
90+
# Legend — enabled with styling for single series identification
91+
chart.options.legend = {
92+
"enabled": True,
93+
"itemStyle": {"fontSize": "36px", "fontWeight": "normal"},
94+
"align": "right",
95+
"verticalAlign": "top",
96+
"layout": "horizontal",
97+
"x": -40,
98+
"y": 60,
99+
}
100+
101+
# Credits off
102+
chart.options.credits = {"enabled": False}
83103

84104
# Add series
85105
series = AreaSeries()
@@ -111,22 +131,6 @@
111131
f.write(html_content)
112132
temp_path = f.name
113133

114-
# Also save HTML for interactive version
115-
with open("plot.html", "w", encoding="utf-8") as f:
116-
# For standalone HTML, use CDN link
117-
standalone_html = f"""<!DOCTYPE html>
118-
<html>
119-
<head>
120-
<meta charset="utf-8">
121-
<script src="https://code.highcharts.com/highcharts.js"></script>
122-
</head>
123-
<body style="margin:0;">
124-
<div id="container" style="width: 100%; height: 100vh;"></div>
125-
<script>{html_str}</script>
126-
</body>
127-
</html>"""
128-
f.write(standalone_html)
129-
130134
chrome_options = Options()
131135
chrome_options.add_argument("--headless")
132136
chrome_options.add_argument("--no-sandbox")

0 commit comments

Comments
 (0)