Source code for stellium.core.multiwheel

"""
MultiWheel chart implementation for 2-4 chart comparisons.

This module provides a unified interface for rendering multiple charts
concentrically inside a single zodiac wheel:
- Biwheel (2 charts): Natal + transits, synastry, etc.
- Triwheel (3 charts): Natal + progressed + transits
- Quadwheel (4 charts): Maximum supported

Ring order (center → out):
- Tiny aspect center (no aspect lines drawn)
- Chart 1 ring (innermost) - houses + objects
- Chart 2 ring
- Chart 3 ring (if present)
- Chart 4 ring (if present)
- Zodiac ring (outermost)

Each chart ring includes:
- Alternating house fills (theme-colored per chart)
- House divider lines (full ring width)
- Planet glyphs with compact info (degree only)
- Position ticks on ring's inner rim
"""

import datetime as dt
import warnings
from dataclasses import dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING

from stellium.core.models import CalculatedChart, ComparisonAspect

if TYPE_CHECKING:
    from stellium.visualization.builder import ChartDrawBuilder


[docs] @dataclass(frozen=True) class MultiWheel: """ Multi-chart comparison supporting 2-4 charts rendered concentrically. All charts are rendered inside the zodiac ring, with Chart 1 as the innermost ring and subsequent charts expanding outward. Attributes: charts: Tuple of 2-4 CalculatedChart objects labels: Optional labels for each chart (auto-generated if empty) cross_aspects: Dict mapping chart index pairs to their cross-aspects calculation_timestamp: When this MultiWheel was created """ # The charts (2-4 required) charts: tuple[CalculatedChart, ...] # Labels for each chart labels: tuple[str, ...] = () # Optional cross-chart aspects (between any pair of charts) # Key: (chart_idx1, chart_idx2), Value: aspects between them cross_aspects: dict[tuple[int, int], tuple[ComparisonAspect, ...]] = field( default_factory=dict ) # Metadata calculation_timestamp: datetime = field( default_factory=lambda: datetime.now(dt.UTC) ) def __post_init__(self) -> None: """Validate chart count and auto-generate labels if needed.""" warnings.warn( "MultiWheel is deprecated, use MultiChart instead. " "See stellium.core.multichart.MultiChart for the unified API.", DeprecationWarning, stacklevel=2, ) if len(self.charts) < 2: raise ValueError("MultiWheel requires at least 2 charts") if len(self.charts) > 4: raise ValueError("MultiWheel supports at most 4 charts") # Auto-generate labels if not provided if not self.labels: default_labels = ("Chart 1", "Chart 2", "Chart 3", "Chart 4") object.__setattr__(self, "labels", default_labels[: len(self.charts)]) @property def chart_count(self) -> int: """Number of charts in this MultiWheel.""" return len(self.charts) @property def chart1(self) -> CalculatedChart: """Primary chart (innermost ring).""" return self.charts[0] @property def chart2(self) -> CalculatedChart: """Second chart.""" return self.charts[1] @property def chart3(self) -> CalculatedChart | None: """Third chart (if present).""" return self.charts[2] if len(self.charts) > 2 else None @property def chart4(self) -> CalculatedChart | None: """Fourth chart (if present).""" return self.charts[3] if len(self.charts) > 3 else None
[docs] def draw(self, filename: str = "multiwheel.svg") -> "ChartDrawBuilder": """ Start building a multiwheel visualization. Args: filename: Output filename for the SVG Returns: ChartDrawBuilder configured for this MultiWheel """ from stellium.visualization.builder import ChartDrawBuilder return ChartDrawBuilder(self).with_filename(filename)
[docs] class MultiWheelBuilder: """ Fluent builder for creating MultiWheel objects. Usage: multiwheel = (MultiWheelBuilder .from_charts([natal, transit, progressed]) .with_labels(["Natal", "Transit", "Progressed"]) .calculate()) # Or simply: multiwheel = MultiWheelBuilder.from_charts([chart1, chart2]).calculate() """ def __init__(self, charts: list[CalculatedChart]) -> None: """ Initialize builder with charts. Args: charts: List of 2-4 CalculatedChart objects Raises: ValueError: If chart count is not 2-4 """ warnings.warn( "MultiWheelBuilder is deprecated, use MultiChartBuilder instead. " "See stellium.core.multichart.MultiChartBuilder for the unified API.", DeprecationWarning, stacklevel=2, ) if len(charts) < 2: raise ValueError("MultiWheel requires at least 2 charts") if len(charts) > 4: raise ValueError("MultiWheel supports at most 4 charts") self._charts = charts self._labels: list[str] = [] self._calculate_cross_aspects: bool = False
[docs] @classmethod def from_charts(cls, charts: list[CalculatedChart]) -> "MultiWheelBuilder": """ Create a MultiWheelBuilder from a list of calculated charts. Args: charts: List of 2-4 CalculatedChart objects Returns: MultiWheelBuilder ready for configuration """ return cls(charts)
[docs] def with_labels(self, labels: list[str]) -> "MultiWheelBuilder": """ Set labels for each chart. Args: labels: List of labels (should match chart count) Returns: self for chaining """ self._labels = labels return self
[docs] def with_cross_aspects(self) -> "MultiWheelBuilder": """ Enable cross-chart aspect calculation. Note: This can be expensive for 3-4 charts as it calculates aspects between all chart pairs. Returns: self for chaining """ self._calculate_cross_aspects = True return self
[docs] def calculate(self) -> MultiWheel: """ Build the MultiWheel object. Returns: Configured MultiWheel ready for visualization """ cross_aspects: dict[tuple[int, int], tuple[ComparisonAspect, ...]] = {} if self._calculate_cross_aspects: # Calculate aspects between each pair of charts from stellium.engines.aspects import CrossChartAspectEngine from stellium.engines.orbs import SimpleOrbEngine engine = CrossChartAspectEngine() orb_engine = SimpleOrbEngine() for i in range(len(self._charts)): for j in range(i + 1, len(self._charts)): aspects = engine.calculate_cross_aspects( list(self._charts[i].positions), list(self._charts[j].positions), orb_engine, ) cross_aspects[(i, j)] = tuple(aspects) return MultiWheel( charts=tuple(self._charts), labels=tuple(self._labels) if self._labels else (), cross_aspects=cross_aspects, )