Source code for figrecipe._diagram._shared._schema

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Timestamp: 2025-01-24
# File: figrecipe/_diagram/_schema.py

"""
Schema definitions for FigRecipe Diagram.

The schema defines paper-specific constraints that Mermaid/Graphviz don't know:
- Paper layout (single/double column, max width)
- Reading direction preferences
- Node emphasis for scientific communication
- Semantic layer grouping
"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Literal, Optional


[docs] class DiagramType(Enum): """Semantic type of diagram - affects layout strategy.""" WORKFLOW = "workflow" # Sequential process, prefer LR/TB flow DECISION = "decision" # Decision tree, prefer TB with branches PIPELINE = "pipeline" # Data pipeline, strict LR with stages HIERARCHY = "hierarchy" # Tree structure, TB with levels COMPARISON = "comparison" # Side-by-side, two columns
[docs] class ColumnLayout(Enum): """Paper column layout.""" SINGLE = "single" # Full width (~170mm) DOUBLE = "double" # Half width (~85mm)
[docs] class SpacingLevel(Enum): """Abstract spacing levels - mapped to backend-specific values.""" TIGHT = "tight" # Publication: minimal whitespace COMPACT = "compact" MEDIUM = "medium" LARGE = "large"
[docs] class PaperMode(Enum): """Paper mode affects layout density and edge visibility.""" DRAFT = "draft" # Full arrows, visible bidirectional, medium spacing PUBLICATION = "publication" # Compact, return edges hidden/dotted
[docs] @dataclass class PaperConstraints: """Paper-specific constraints that affect layout.""" column: ColumnLayout = ColumnLayout.SINGLE max_width_mm: int = 170 reading_direction: Literal["left_to_right", "top_to_bottom"] = "left_to_right" mode: PaperMode = PaperMode.DRAFT # draft: full details, publication: compact emphasize: List[str] = field(default_factory=list) # Node IDs to highlight # Scientific communication hints main_flow: List[str] = field(default_factory=list) # Critical path nodes secondary_flow: List[str] = field(default_factory=list) # Supporting elements return_edges: List[tuple] = field( default_factory=list ) # Edges to hide in publication
[docs] @dataclass class LayoutHints: """Abstract layout hints - compiled to backend directives.""" layers: List[List[str]] = field(default_factory=list) # Nodes grouped by rank alignment: Dict[str, str] = field(default_factory=dict) # Node alignment hints layer_gap: SpacingLevel = SpacingLevel.MEDIUM node_gap: SpacingLevel = SpacingLevel.MEDIUM # Subgraph organization groups: Dict[str, List[str]] = field(default_factory=dict) # Named groups
[docs] @dataclass class NodeSpec: """Specification for a single node.""" id: str label: str shape: Literal["box", "rounded", "diamond", "circle", "stadium", "codeblock"] = ( "box" ) emphasis: Literal["normal", "primary", "success", "warning", "muted"] = "normal"
[docs] def short_label(self, max_chars: int = 20) -> str: """Return truncated label for compact layouts.""" if len(self.label) <= max_chars: return self.label return self.label[: max_chars - 3] + "..."
[docs] @dataclass class EdgeSpec: """Specification for an edge between nodes.""" source: str target: str label: Optional[str] = None style: Literal["solid", "dashed", "dotted"] = "solid" arrow: Literal["normal", "none", "open"] = "normal"
[docs] @dataclass class DiagramSpec: """Complete diagram specification - the semantic layer.""" # Metadata type: DiagramType = DiagramType.WORKFLOW title: str = "" # Paper constraints paper: PaperConstraints = field(default_factory=PaperConstraints) # Layout hints layout: LayoutHints = field(default_factory=LayoutHints) # Content nodes: List[NodeSpec] = field(default_factory=list) edges: List[EdgeSpec] = field(default_factory=list) # Theme theme: Dict[str, str] = field(default_factory=dict)
[docs] @classmethod def from_dict(cls, data: dict) -> "DiagramSpec": """Create DiagramSpec from dictionary (parsed YAML).""" spec = cls() # Parse type if "type" in data: spec.type = DiagramType(data["type"]) spec.title = data.get("title", "") # Parse paper constraints if "paper" in data: p = data["paper"] spec.paper = PaperConstraints( column=ColumnLayout(p.get("column", "single")), max_width_mm=p.get("max_width_mm", 170), reading_direction=p.get("reading_direction", "left_to_right"), mode=PaperMode(p.get("mode", "draft")), emphasize=p.get("emphasize", []), main_flow=p.get("main_flow", []), secondary_flow=p.get("secondary_flow", []), return_edges=[tuple(e) for e in p.get("return_edges", [])], ) # Parse layout hints if "layout" in data: lt = data["layout"] spec.layout = LayoutHints( layers=lt.get("layers", []), alignment=lt.get("alignment", {}), layer_gap=SpacingLevel(lt.get("layer_gap", "medium")), node_gap=SpacingLevel(lt.get("node_gap", "medium")), groups=lt.get("groups", {}), ) # Parse nodes for n in data.get("nodes", []): spec.nodes.append( NodeSpec( id=n["id"], label=n.get("label", n["id"]), shape=n.get("shape", "box"), emphasis=n.get("emphasis", "normal"), ) ) # Parse edges for e in data.get("edges", []): spec.edges.append( EdgeSpec( source=e["from"] if "from" in e else e["source"], target=e["to"] if "to" in e else e["target"], label=e.get("label"), style=e.get("style", "solid"), arrow=e.get("arrow", "normal"), ) ) # Theme spec.theme = data.get("theme", {}) return spec