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>
58 lines
1.9 KiB
Python
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]
|