- layout: _PAD_BOTTOM 100→160, _PAD_LEFT 120→160 to prevent label clipping - axes: fix _format_tick float-comparison bug (1.1→"1.1" not "1.10"); add auto-rotation for categorical x-axis labels when they overflow slot width - engine: tick guard (>20 ticks falls back to nice_ticks); stacked_bar falls back to bar instead of crashing; extend _find_date_column keywords - series: bar width uses 25th-percentile gap instead of min to avoid invisible bars - legend: line series use line swatch with dash pattern; proportional square size - export: @font-face injected in HTML <head> for PDF to prevent T3Font in InDesign - prompts: add categorical bar chart few-shot example; ban stacked_bar; improve tick_interval guidance; add bar chart refine examples Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
87 lines
2.5 KiB
Python
87 lines
2.5 KiB
Python
"""Canvas sizing and panel layout computation."""
|
|
|
|
from __future__ import annotations
|
|
from dataclasses import dataclass
|
|
from app.models.style import LAYOUT
|
|
|
|
_PAD_TOP_MIN = 160
|
|
_PAD_BOTTOM = 160
|
|
_PAD_LEFT = 160
|
|
_PAD_RIGHT_STANDARD = 30
|
|
_PAD_RIGHT_PIE = 320
|
|
_PAD_RIGHT_DUAL_Y = 130 # right-side Y-axis labels for dual_y_axis layout
|
|
|
|
|
|
@dataclass
|
|
class PanelBounds:
|
|
left: float
|
|
right: float
|
|
top: float
|
|
bottom: float
|
|
|
|
@property
|
|
def width(self) -> float:
|
|
return self.right - self.left
|
|
|
|
@property
|
|
def height(self) -> float:
|
|
return self.bottom - self.top
|
|
|
|
|
|
def compute_layout(
|
|
layout_type: str,
|
|
vertical_legend: bool = False,
|
|
pad_top: int | None = None,
|
|
) -> tuple[int, int, list[PanelBounds]]:
|
|
"""Compute canvas dimensions and panel bounds."""
|
|
outer = LAYOUT["margins"]["top"]
|
|
_pad_top = max(pad_top, _PAD_TOP_MIN) if pad_top is not None else _PAD_TOP_MIN
|
|
|
|
if layout_type == "dual_panel":
|
|
dims = LAYOUT["dual_panel"]
|
|
w, h = dims["width"], dims["height"]
|
|
gap = LAYOUT["panel_gap"]
|
|
pad_right = _PAD_RIGHT_PIE if vertical_legend else _PAD_RIGHT_STANDARD
|
|
|
|
plot_left = outer + _PAD_LEFT
|
|
plot_top = outer + _pad_top
|
|
plot_bottom = h - outer - _PAD_BOTTOM
|
|
usable_width = w - outer - _PAD_LEFT - pad_right - outer - gap
|
|
panel_width = usable_width / 2
|
|
|
|
left_panel = PanelBounds(
|
|
left=plot_left,
|
|
right=plot_left + panel_width,
|
|
top=plot_top,
|
|
bottom=plot_bottom,
|
|
)
|
|
right_panel = PanelBounds(
|
|
left=plot_left + panel_width + gap,
|
|
right=w - outer - pad_right,
|
|
top=plot_top,
|
|
bottom=plot_bottom,
|
|
)
|
|
return w, h, [left_panel, right_panel]
|
|
|
|
elif layout_type == "dual_y_axis":
|
|
dims = LAYOUT["dual_y_axis"]
|
|
w, h = dims["width"], dims["height"]
|
|
panel = PanelBounds(
|
|
left=outer + _PAD_LEFT,
|
|
right=w - outer - _PAD_RIGHT_DUAL_Y,
|
|
top=outer + _pad_top,
|
|
bottom=h - outer - _PAD_BOTTOM,
|
|
)
|
|
return w, h, [panel]
|
|
|
|
else: # single
|
|
dims = LAYOUT["single"]
|
|
w, h = dims["width"], dims["height"]
|
|
pad_right = _PAD_RIGHT_PIE if vertical_legend else _PAD_RIGHT_STANDARD
|
|
panel = PanelBounds(
|
|
left=outer + _PAD_LEFT,
|
|
right=w - outer - pad_right,
|
|
top=outer + _pad_top,
|
|
bottom=h - outer - _PAD_BOTTOM,
|
|
)
|
|
return w, h, [panel]
|