Source code for stellium.visualization.grid

"""
SVG Grid Layout (stellium.visualization.grid)

Creates subplot-like grid arrangements of multiple charts in a single SVG file.
Similar to matplotlib's subplot functionality but for astrology charts.
"""

from typing import Any

import svgwrite

from stellium.core.models import CalculatedChart, ObjectType

from .core import ChartRenderer
from .layers import AngleLayer, AspectLayer, HouseCuspLayer, PlanetLayer, ZodiacLayer
from .moon_phase import MoonPhaseLayer
from .palettes import ZodiacPalette
from .themes import ChartTheme, get_theme_default_palette


[docs] def draw_chart_grid( charts: list[CalculatedChart], filename: str = "chart_grid.svg", labels: list[str] | None = None, rows: int | None = None, cols: int | None = None, chart_size: int = 400, padding: int = 30, themes: list[ChartTheme | str] | None = None, zodiac_palettes: list[ZodiacPalette | str] | None = None, aspect_palettes: list[str] | None = None, planet_glyph_palettes: list[str] | None = None, color_sign_info: bool = False, color_zodiac_glyphs: bool = False, moon_phase: bool = True, background_color: str = "#FAFAFA", ) -> str: """ Draw multiple charts in a grid layout in a single SVG file. This function creates a subplot-like arrangement of multiple charts, perfect for comparing different chart styles, themes, or palettes. Args: charts: List of CalculatedChart objects to render filename: Output SVG filename labels: Optional labels for each chart (displayed at bottom) rows: Number of rows (auto-calculated if None) cols: Number of columns (auto-calculated if None) chart_size: Size of each individual chart in pixels padding: Padding between charts in pixels themes: Optional list of themes (one per chart, or None for default) zodiac_palettes: Optional list of zodiac palettes (one per chart) aspect_palettes: Optional list of aspect palettes (one per chart) planet_glyph_palettes: Optional list of planet glyph palettes (one per chart) color_sign_info: Apply adaptive sign info coloring to all charts color_zodiac_glyphs: Apply adaptive zodiac glyph coloring to all charts moon_phase: Show moon phase in all charts background_color: Background color for the entire grid Returns: The filename of the saved chart grid Example: >>> from stellium import ChartBuilder >>> chart = ChartBuilder.from_notable("Albert Einstein").calculate() >>> # Create grid showing same chart with different themes >>> draw_chart_grid( ... charts=[chart] * 4, ... labels=["Classic", "Dark", "Midnight", "Neon"], ... themes=["classic", "dark", "midnight", "neon"], ... rows=2, ... cols=2, ... ) """ num_charts = len(charts) if num_charts == 0: raise ValueError("At least one chart must be provided") # Auto-calculate grid dimensions if not provided if rows is None and cols is None: # Default: try to make a square-ish grid cols = int(num_charts**0.5) + (1 if num_charts**0.5 % 1 > 0 else 0) rows = (num_charts + cols - 1) // cols elif rows is None: rows = (num_charts + cols - 1) // cols elif cols is None: cols = (num_charts + rows - 1) // rows # Ensure we have enough grid cells if rows * cols < num_charts: raise ValueError( f"Grid size {rows}x{cols} is too small for {num_charts} charts" ) # Set up per-chart configurations (cycle if lists are too short) def get_item(lst: list | None, index: int, default: Any = None) -> Any: if lst is None: return default if len(lst) == 0: return default return lst[index % len(lst)] # Calculate total SVG size label_height = 40 if labels else 0 total_width = cols * chart_size + (cols + 1) * padding total_height = rows * (chart_size + label_height) + (rows + 1) * padding # Create main SVG dwg = svgwrite.Drawing( filename=filename, size=(f"{total_width}px", f"{total_height}px"), viewBox=f"0 0 {total_width} {total_height}", profile="full", ) # Add background dwg.add( dwg.rect( insert=(0, 0), size=(f"{total_width}px", f"{total_height}px"), fill=background_color, ) ) # Render each chart in the grid for i, chart in enumerate(charts): if i >= rows * cols: break # Don't overflow the grid row = i // cols col = i % cols # Calculate position x = padding + col * (chart_size + padding) y = padding + row * (chart_size + label_height + padding) # Get configuration for this chart theme = get_item(themes, i) zodiac_palette = get_item(zodiac_palettes, i) aspect_palette = get_item(aspect_palettes, i) planet_glyph_palette = get_item(planet_glyph_palettes, i) label = get_item(labels, i) # Determine theme and palette if theme: theme_enum = ChartTheme(theme) if isinstance(theme, str) else theme if zodiac_palette is None: zodiac_palette = get_theme_default_palette(theme_enum) else: if zodiac_palette is None: zodiac_palette = ZodiacPalette.GREY # Convert zodiac_palette to string if hasattr(zodiac_palette, "value"): zodiac_palette_str = zodiac_palette.value else: zodiac_palette_str = zodiac_palette # Get rotation angle asc_object = chart.get_object("ASC") rotation_angle = asc_object.longitude if asc_object else 0.0 # Create renderer for this chart renderer = ChartRenderer( size=chart_size, rotation=rotation_angle, theme=theme, zodiac_palette=zodiac_palette_str, aspect_palette=aspect_palette, planet_glyph_palette=planet_glyph_palette, color_sign_info=color_sign_info, color_zodiac_glyphs=color_zodiac_glyphs, ) # Create a group for this chart chart_group = dwg.g(transform=f"translate({x},{y})") # Create chart elements (we'll render them into a temporary drawing # then copy the elements into our group) # This is a bit of a workaround since we can't directly share drawings # Create a mini-SVG for this chart mini_dwg = svgwrite.Drawing(size=(chart_size, chart_size)) # Add background and borders mini_dwg.add( mini_dwg.rect( insert=(0, 0), size=(chart_size, chart_size), fill=renderer.style["background_color"], ) ) mini_dwg.add( mini_dwg.circle( center=(renderer.center, renderer.center), r=renderer.radii["outer_border"], fill="none", stroke=renderer.style["border_color"], stroke_width=renderer.style["border_width"], ) ) mini_dwg.add( mini_dwg.circle( center=(renderer.center, renderer.center), r=renderer.radii["aspect_ring_inner"], fill="none", stroke=renderer.style["border_color"], stroke_width=renderer.style["border_width"], ) ) # Get planets to draw planets_to_draw = [ p for p in chart.positions if p.object_type in ( ObjectType.PLANET, ObjectType.ASTEROID, ObjectType.NODE, ObjectType.POINT, ) ] # Assemble layers layers = [ ZodiacLayer(palette=zodiac_palette), HouseCuspLayer(house_system_name=chart.default_house_system), AspectLayer(), PlanetLayer(planet_set=planets_to_draw, radius_key="planet_ring"), AngleLayer(), ] if moon_phase: layers.insert(3, MoonPhaseLayer()) # Render layers for layer in layers: layer.render(renderer, mini_dwg, chart) # Copy all elements from mini_dwg to chart_group for element in mini_dwg.elements: chart_group.add(element) # Add chart group to main drawing dwg.add(chart_group) # Add label if provided if label: label_y = y + chart_size + 25 label_x = x + chart_size // 2 dwg.add( dwg.text( label, insert=(label_x, label_y), text_anchor="middle", font_family="Arial, Helvetica, sans-serif", font_size="14px", font_weight="bold", fill="#333333", ) ) # Save the grid dwg.save() return filename
[docs] def draw_theme_comparison( chart: CalculatedChart, filename: str = "theme_comparison.svg", themes: list[ChartTheme | str] | None = None, chart_size: int = 300, ) -> str: """ Create a grid comparing the same chart rendered in different themes. Args: chart: The chart to render in different themes filename: Output SVG filename themes: List of themes to compare (defaults to all built-in themes) chart_size: Size of each chart in pixels Returns: The filename of the saved comparison grid Example: >>> from stellium import ChartBuilder >>> chart = ChartBuilder.from_notable("Albert Einstein").calculate() >>> draw_theme_comparison(chart, "einstein_themes.svg") """ if themes is None: # Use all built-in themes themes = [ ChartTheme.CLASSIC, ChartTheme.DARK, ChartTheme.MIDNIGHT, ChartTheme.NEON, ChartTheme.SEPIA, ChartTheme.PASTEL, ChartTheme.CELESTIAL, ChartTheme.VIRIDIS, ChartTheme.PLASMA, ChartTheme.INFERNO, ChartTheme.MAGMA, ChartTheme.CIVIDIS, ] # Create labels from theme names labels = [ t.value.title() if hasattr(t, "value") else str(t).title() for t in themes ] # Create grid with one chart per theme return draw_chart_grid( charts=[chart] * len(themes), filename=filename, labels=labels, themes=themes, chart_size=chart_size, cols=4, # 4 columns for nice layout )
[docs] def draw_palette_comparison( chart: CalculatedChart, filename: str = "palette_comparison.svg", palettes: list[ZodiacPalette | str] | None = None, theme: ChartTheme | str = ChartTheme.DARK, chart_size: int = 300, color_zodiac_glyphs: bool = True, ) -> str: """ Create a grid comparing the same chart with different zodiac palettes. Args: chart: The chart to render with different palettes filename: Output SVG filename palettes: List of zodiac palettes to compare (defaults to popular ones) theme: Base theme to use (default: dark, works well with colorful palettes) chart_size: Size of each chart in pixels color_zodiac_glyphs: Enable adaptive zodiac glyph coloring Returns: The filename of the saved comparison grid Example: >>> from stellium import ChartBuilder >>> chart = ChartBuilder.from_notable("Albert Einstein").calculate() >>> draw_palette_comparison(chart, "einstein_palettes.svg") """ if palettes is None: # Use interesting palettes palettes = [ ZodiacPalette.GREY, ZodiacPalette.RAINBOW, ZodiacPalette.ELEMENTAL, ZodiacPalette.VIRIDIS, ZodiacPalette.PLASMA, ZodiacPalette.INFERNO, ZodiacPalette.MAGMA, ZodiacPalette.TURBO, ] # Create labels from palette names labels = [ p.value.title() if hasattr(p, "value") else str(p).title() for p in palettes ] # Create grid with same theme but different palettes return draw_chart_grid( charts=[chart] * len(palettes), filename=filename, labels=labels, themes=[theme] * len(palettes), zodiac_palettes=palettes, chart_size=chart_size, color_zodiac_glyphs=color_zodiac_glyphs, cols=4, )