Source code for stellium.visualization.composer

import svgwrite

from stellium.core.comparison import Comparison
from stellium.core.models import CalculatedChart
from stellium.core.multichart import MultiChart
from stellium.core.multiwheel import MultiWheel
from stellium.visualization.config import ChartVisualizationConfig
from stellium.visualization.core import ChartRenderer
from stellium.visualization.extended_canvas import (
    AspectarianLayer,
    HouseCuspTableLayer,
    PositionTableLayer,
)
from stellium.visualization.layer_factory import LayerFactory
from stellium.visualization.layout.engine import LayoutEngine, LayoutResult
from stellium.visualization.themes import get_theme_style


[docs] class ChartComposer: """ Main orchestrator for chart visualization. This is the new public API that replaces draw_chart() and draw_comparison_chart(). """ def __init__(self, config: ChartVisualizationConfig): self.config = config self.layout_engine = LayoutEngine(config) self.layer_factory = LayerFactory(config)
[docs] def compose( self, chart: CalculatedChart | Comparison | MultiWheel | MultiChart, to_string: bool = False, ) -> str: """ Compose and render a complete chart visualization. This is a pure, testable pipeline: 1. Calculate layout 2. Create SVG canvas 3. Create layers 4. Render layers 5. Save """ # Step 1: Calculate complete layout layout = self.layout_engine.calculate_layout(chart) # Step 2: Create canvas with correct dimensions canvas = self._create_canvas(layout) # Step 3: Create renderer with calculated radii renderer = self._create_renderer(layout, chart) # Step 4: Create configured layers layers = self.layer_factory.create_layers(chart, layout) # Step 5: Render all layers for layer in layers: layer.render(renderer, canvas, chart) # Step 6: Render tables (separate from layers) if self.config.tables.enabled: self._render_tables(canvas, renderer, chart, layout) if to_string: return canvas.tostring() else: # Step 7: Save canvas.save() return self.config.filename
def _create_canvas(self, layout: LayoutResult) -> svgwrite.Drawing: """Create SVG canvas with correct dimensions (only once).""" dims = layout.canvas_dimensions dwg = svgwrite.Drawing( filename=self.config.filename, size=(f"{dims.width}px", f"{dims.height}px"), viewBox=f"0 0 {dims.width} {dims.height}", profile="full", ) # Background dwg.add( dwg.rect( insert=(0, 0), size=(f"{dims.width}px", f"{dims.height}px"), fill=self._get_background_color(), ) ) return dwg def _create_renderer( self, layout: LayoutResult, chart: CalculatedChart | Comparison | MultiWheel | MultiChart, ) -> ChartRenderer: """Create renderer with pre-calculated radii.""" renderer = ChartRenderer( size=layout.wheel_size, rotation=self._get_rotation_angle(chart), theme=self.config.wheel.theme, zodiac_palette=self.config.wheel.zodiac_palette, aspect_palette=self.config.wheel.aspect_palette, planet_glyph_palette=self.config.wheel.planet_glyph_palette, color_sign_info=self.config.wheel.color_sign_info, ) # Set the pre-calculated radii renderer.radii = layout.wheel_radii # Set position offsets renderer.x_offset = int(layout.wheel_position.x) renderer.y_offset = int(layout.wheel_position.y) # Set header height for layers that need to account for it renderer.header_height = layout.header_height return renderer def _get_background_color(self) -> str: """Get background color from theme or default.""" if self.config.wheel.theme: style = get_theme_style(self.config.wheel.theme) return style.get("background_color", "#FFFFFF") return "#FFFFFF" def _get_rotation_angle( self, chart: CalculatedChart | Comparison | MultiWheel | MultiChart ) -> float: """ Calculate chart rotation based on ASC. For single charts, rotates to put ASC on the left. For comparisons, uses chart1's ASC. For multiwheels/multicharts, uses the innermost chart's (chart[0]) ASC. """ if isinstance(chart, MultiChart): chart = chart.charts[0] # Use innermost chart elif isinstance(chart, MultiWheel): chart = chart.charts[0] # Use innermost chart elif isinstance(chart, Comparison): chart = chart.chart1 # Get the ASC angle angles = chart.get_angles() asc = next((a for a in angles if a.name == "ASC"), None) return asc.longitude if asc else 0.0 def _render_tables( self, canvas: svgwrite.Drawing, renderer: ChartRenderer, chart: CalculatedChart | Comparison, layout: LayoutResult, ) -> None: """ Render table layers using calculated layout positions. This delegates to the adapted table layers from extended_canvas. """ is_comparison = isinstance(chart, Comparison) # Build style override from theme colors extended_style = { "text_color": renderer.style.get("planets", {}).get( "info_color", "#333333" ), "header_color": renderer.style.get("planets", {}).get( "glyph_color", "#222222" ), "grid_color": renderer.style.get("zodiac", {}).get("line_color", "#CCCCCC"), } # Render positions table if enabled if self.config.tables.show_positions and "positions" in layout.tables: table_bbox = layout.tables["positions"] # Create position table layer with layout-calculated position position_layer = PositionTableLayer( x_offset=int(table_bbox.position.x), y_offset=int(table_bbox.position.y), object_types=self.config.tables.object_types, config=self.config, style_override=extended_style, ) position_layer.render(renderer, canvas, chart) # Render houses table if enabled if self.config.tables.show_houses and "houses" in layout.tables: table_bbox = layout.tables["houses"] # Create house cusp table layer house_layer = HouseCuspTableLayer( x_offset=int(table_bbox.position.x), y_offset=int(table_bbox.position.y), config=self.config, style_override=extended_style, ) house_layer.render(renderer, canvas, chart) # Render aspectarian if enabled if self.config.tables.show_aspectarian and "aspectarian" in layout.tables: table_bbox = layout.tables["aspectarian"] # Determine aspectarian mode if is_comparison: # For comparison charts, default to cross-chart aspects _mode = "cross-chart" # Can make configurable later else: _mode = "internal" # Create aspectarian layer aspectarian_layer = AspectarianLayer( x_offset=int(table_bbox.position.x), y_offset=int(table_bbox.position.y), object_types=self.config.tables.object_types, config=self.config, style_override=extended_style, detailed=self.config.tables.aspectarian_detailed, ) aspectarian_layer.render(renderer, canvas, chart)