pimco-charts/app/models/chart_spec.py
Vadym Samoilenko d52f088243 Fix bar charts, fonts, axis controls, donut support, and Playwright export
Issue 1 – Bar charts blank/lines only:
- Silent fall-through on unsupported chart_types (donut, stacked_bar, area)
  now raises ValueError instead of producing axes-only output
- Zero-width bars on duplicate/single dates fixed via sorted-gap calculation
- Donut chart type added (ring with percentage labels)
- Pie/donut routing now triggers on any() instead of all()

Issue 2 – Axis controls not applying:
- AxisSpec gains date_min/date_max (x-axis clamping via prompts)
- y-bounds no longer silently widened when user sets min_val/max_val
- Tick clamping: ticks outside user range are dropped not widened
- New dual_y_axis layout with independent left/right Y-axes and y_axis_side per series
- Endpoint Y-axis labels (min/max) always render even when spacing is tight

Issue 3+4 – Font fallback & InDesign compatibility:
- Replace CairoSVG with Playwright/headless Chromium for PNG and PDF export
- Chromium honours @font-face base64 data URIs → Roboto Condensed in all exports
- PDF output contains embedded TTF subsets and real text operators (selectable
  in InDesign/Illustrator, no path-outlining, consistent across regions)
- FastAPI lifespan manages persistent Playwright browser instance

Issue 5 – Stroke weight drift:
- All stroke_width values now carry explicit "px" unit suffix
- SVG root gets width="…px" height="…px" so 1 SVG px = 0.75 PDF pt exactly

AI improvements:
- Prompts document date_min/date_max, scale_kind, dual_y_axis, donut
- Rule 9 softened: user-specified ranges are honoured even if they crop data
- Refinement uses deep-merge so tick_interval/min_val/date_min are never
  accidentally reset to None when Claude modifies unrelated fields
- New donut few-shot example added

Library upgrades: anthropic 0.84→0.97, fastapi 0.135→0.136,
pandas 3.0.1→3.0.2, pydantic 2.12→2.13, uvicorn 0.41→0.46;
cairosvg removed, playwright 1.58.0 added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 13:15:26 +01:00

58 lines
1.9 KiB
Python

"""Pydantic models for the chart specification - the contract between AI and renderer."""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, Field
class AxisSpec(BaseModel):
label: str | None = None
suffix: str | None = None
date_format: str | None = None
min_val: float | None = None
max_val: float | None = None
tick_interval: float | None = None
date_min: str | None = None # ISO date string or year "2015" — clamps x-axis range
date_max: str | None = None
scale_kind: Literal["date", "category"] = "date"
class ShadedFillSpec(BaseModel):
reference_series: str = Field(description="Label of the reference series to shade against")
above_color: Literal["blue", "pink"] = "blue"
below_color: Literal["blue", "pink"] = "pink"
class SeriesSpec(BaseModel):
label: str
data_column: str = Field(description="Column name from uploaded data")
chart_type: Literal["line", "bar", "stacked_bar", "area", "pie", "donut"] = "line"
color_index: int | None = None
line_style: Literal["solid", "dashed", "dotted"] = "solid"
line_weight: float | None = None
shaded_fill: ShadedFillSpec | None = None
y_axis_side: Literal["primary", "secondary"] = "primary" # for dual_y_axis layout
class AnnotationSpec(BaseModel):
type: Literal["ellipse", "callout", "label"]
x_start: str | None = None
x_end: str | None = None
y_start: float | None = None
y_end: float | None = None
text: str | None = None
class PanelSpec(BaseModel):
title: str
subtitle: str | None = None
x_axis: AxisSpec
y_axis: AxisSpec
secondary_y_axis: AxisSpec | None = None # right-side axis for dual_y_axis layout
series: list[SeriesSpec]
annotations: list[AnnotationSpec] = Field(default_factory=list)
class ChartSpec(BaseModel):
layout: Literal["single", "dual_panel", "dual_y_axis"] = "single"
panels: list[PanelSpec]