Source code for stellium.visualization.layout.measurer

"""Measures elements before rendering."""

from stellium.core.comparison import Comparison
from stellium.core.models import CalculatedChart
from stellium.core.multichart import MultiChart
from stellium.visualization.config import ChartVisualizationConfig, Dimensions
from stellium.visualization.extended_canvas import _filter_objects_for_tables


[docs] class ContentMeasurer: """ Measures chart elements without rendering them. This is crucial for calculating layout before creating the SVG. """ def _get_house_systems_to_display( self, chart: CalculatedChart | Comparison | MultiChart, config: ChartVisualizationConfig, ) -> list[str]: """Determine which house systems to display in tables. Returns list of house system names based on config settings. """ # Get the actual chart object (for Comparison/MultiChart, use first chart) if isinstance(chart, MultiChart): actual_chart = chart.charts[0] elif isinstance(chart, Comparison): actual_chart = chart.chart1 else: actual_chart = chart if not actual_chart.house_systems: return [] # Check config for house_systems setting config_systems = config.wheel.house_systems if config_systems == "all": # Show all available house systems return list(actual_chart.house_systems.keys()) elif isinstance(config_systems, list): # Show specific systems (filter to only those available) return [s for s in config_systems if s in actual_chart.house_systems] # Default: just show the default house system if actual_chart.default_house_system: return [actual_chart.default_house_system] return list(actual_chart.house_systems.keys())[:1] # First available
[docs] def measure_position_table( self, chart: CalculatedChart | Comparison | MultiChart, config: ChartVisualizationConfig, ) -> Dimensions: """ Measure position table dimensions. For comparison/multichart with 2 charts, this measures TWO side-by-side tables. """ # Get filtered objects if isinstance(chart, MultiChart): # For MultiChart, measure based on number of charts if chart.chart_count >= 2: chart1_objects = _filter_objects_for_tables( chart.charts[0].positions, config.tables.object_types ) chart2_objects = _filter_objects_for_tables( chart.charts[1].positions, config.tables.object_types ) num_rows = max(len(chart1_objects), len(chart2_objects)) num_tables = min(chart.chart_count, 2) # Max 2 tables side by side else: objects = _filter_objects_for_tables( chart.charts[0].positions, config.tables.object_types ) num_rows = len(objects) num_tables = 1 elif isinstance(chart, Comparison): chart1_objects = _filter_objects_for_tables( chart.chart1.positions, config.tables.object_types ) chart2_objects = _filter_objects_for_tables( chart.chart2.positions, config.tables.object_types ) num_rows = max(len(chart1_objects), len(chart2_objects)) num_tables = 2 else: # Is a single chart objects = _filter_objects_for_tables( chart.positions, config.tables.object_types ) num_rows = len(objects) num_tables = 1 # Get config values col_widths = config.tables.position_col_widths padding = config.tables.padding gap_between_cols = config.tables.gap_between_columns gap_between_tables = config.tables.gap_between_tables line_height = 16 # Match DEFAULT_STYLE in extended_canvas.py # Determine which columns are shown col_names = ["planet", "sign", "degree"] # TODO: Make show_house and show_speed configurable show_house = True show_speed = True # Get house systems to display (may be multiple) house_systems = ( self._get_house_systems_to_display(chart, config) if show_house else [] ) # Add a "house" column for each house system for _ in house_systems: col_names.append("house") if show_speed: col_names.append("speed") # Calculate single table width: padding + columns + gaps + padding single_table_width = 2 * padding # left and right padding for i, col_name in enumerate(col_names): single_table_width += col_widths.get(col_name, 50) if i < len(col_names) - 1: # Add gap between columns single_table_width += gap_between_cols # Account for multiple tables (for comparison charts) total_width = (single_table_width * num_tables) + ( gap_between_tables * (num_tables - 1) ) # Height: padding + header + rows + padding # For comparison: also add title height (20px) above each table title_height = 20 if num_tables == 2 else 0 header_height = line_height rows_height = num_rows * line_height total_height = padding + title_height + header_height + rows_height + padding return Dimensions(total_width, total_height)
[docs] def measure_house_table( self, chart: CalculatedChart | Comparison | MultiChart, config: ChartVisualizationConfig, ) -> Dimensions: """ Measure house cusp table dimensions. Always 12 rows (houses). For multiple house systems, adds columns (sign + degree) for each system. For comparison/multichart, shows separate tables for each chart. """ # Get config values col_widths = config.tables.house_col_widths padding = config.tables.padding gap_between_cols = config.tables.gap_between_columns gap_between_tables = config.tables.gap_between_tables line_height = 16 # Match DEFAULT_STYLE in extended_canvas.py # Get house systems to display house_systems = self._get_house_systems_to_display(chart, config) num_house_systems = max(len(house_systems), 1) # At least 1 # Build column list: house number + (sign, degree) for each system col_names = ["house"] for _ in range(num_house_systems): col_names.extend(["sign", "degree"]) # Calculate single table width: padding + columns + gaps + padding single_table_width = 2 * padding # left and right padding for i, col_name in enumerate(col_names): single_table_width += col_widths.get(col_name, 50) if i < len(col_names) - 1: # Add gap between columns single_table_width += gap_between_cols # For comparisons/multicharts with 2+ charts, we have 2 chart tables if isinstance(chart, MultiChart): num_chart_tables = min(chart.chart_count, 2) elif isinstance(chart, Comparison): num_chart_tables = 2 else: num_chart_tables = 1 total_width = (single_table_width * num_chart_tables) + ( gap_between_tables * (num_chart_tables - 1) ) # Height: padding + title (for comparison) + header + 12 rows + padding title_height = 20 if num_chart_tables == 2 else 0 total_height = ( padding + title_height + line_height + (12 * line_height) + padding ) return Dimensions(total_width, total_height)
[docs] def measure_aspectarian( self, chart: CalculatedChart | Comparison | MultiChart, config: ChartVisualizationConfig, ) -> Dimensions: """ Measure aspectarian grid dimensions. Triangle for single charts, square for comparisons/multicharts. """ cell_size = config.tables.aspectarian_cell_size if isinstance(chart, MultiChart) and chart.chart_count >= 2: # Square grid: chart1 objects × chart2 objects chart1_objects = _filter_objects_for_tables( chart.charts[0].positions, config.tables.object_types ) chart2_objects = _filter_objects_for_tables( chart.charts[1].positions, config.tables.object_types ) # Add 1 for header row/column width = (len(chart2_objects) + 1) * cell_size height = (len(chart1_objects) + 1) * cell_size elif isinstance(chart, Comparison): # Square grid: chart1 objects × chart2 objects chart1_objects = _filter_objects_for_tables( chart.chart1.positions, config.tables.object_types ) chart2_objects = _filter_objects_for_tables( chart.chart2.positions, config.tables.object_types ) # Add 1 for header row/column width = (len(chart2_objects) + 1) * cell_size height = (len(chart1_objects) + 1) * cell_size else: # Triangle grid - single chart objects = _filter_objects_for_tables( chart.positions, config.tables.object_types ) num_objects = len(objects) # Triangle: width and height both num_objects * cell_size # (includes header row/col) width = num_objects * cell_size height = num_objects * cell_size return Dimensions(width, height)
[docs] def measure_corner_element( self, element_name: str, chart: CalculatedChart | Comparison | MultiChart, config: ChartVisualizationConfig, ) -> Dimensions: """ Estimate dimensions for corner elements. These are roughly fixed size, but we can be more precise by counting lines of text, etc. """ # TODO: Make these more precise estimates = { "chart_info": Dimensions(200, 100), "aspect_counts": Dimensions(150, 80), "element_modality": Dimensions(120, 90), "chart_shape": Dimensions(150, 60), } return estimates.get(element_name, Dimensions(100, 80))