Source code for gridient.tables

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union

# Import actual classes instead of forward declaring
from .values import ExcelSeries, ExcelValue

# Forward declarations
# class ExcelValue:
#     pass
#
# class ExcelSeries:
#     pass
#
# class ExcelStyle:
#     pass

# Use TYPE_CHECKING to avoid circular imports
if TYPE_CHECKING:
    from .layout import ExcelLayout
    from .workbook import ExcelWorkbook

# --- Import other necessary types ---
import logging  # Add logging import

logger = logging.getLogger(__name__)  # Add logger


[docs] @dataclass class ExcelTableColumn: """Represents a single column within an ExcelTable.""" series: ExcelSeries
# TODO: Add column-specific settings like width?
[docs] class ExcelTable: """Represents a table of values with columns and an optional title.""" def __init__( self, title: Optional[str] = None, columns: Optional[List[Union[ExcelTableColumn, ExcelSeries]]] = None, ): self.title = title self.columns: List[ExcelTableColumn] = [] if columns: for col in columns: self.add_column(col)
[docs] def add_column(self, column: Union[ExcelTableColumn, ExcelSeries]) -> None: """Add a column to the table.""" if isinstance(column, ExcelSeries): # Wrap ExcelSeries in ExcelTableColumn if passed directly self.columns.append(ExcelTableColumn(series=column)) elif isinstance(column, ExcelTableColumn): self.columns.append(column) else: raise TypeError("Column must be an ExcelSeries or ExcelTableColumn")
[docs] def get_size(self) -> Tuple[int, int]: """Calculate the size (rows, columns) of the table.""" rows = 0 if self.title: rows += 1 # Row for title if self.columns: # Check if there are columns before adding header row rows += 1 # Row for headers # Calculate max rows needed for data max_data_rows = 0 if self.columns: # Ensure series exists before checking length max_data_rows = max( (len(table_col.series) for table_col in self.columns if table_col.series), default=0, ) rows += max_data_rows cols = len(self.columns) if self.columns else 0 return (rows, cols)
def _assign_child_references( self, start_row: int, start_col: int, sheet_name: str, layout_manager: "ExcelLayout", ref_map: dict, ): """Assign references to all ExcelValue objects within the table's columns.""" from .layout import ExcelLayout if not isinstance(layout_manager, ExcelLayout): logger.error("layout_manager is not an ExcelLayout instance in ExcelTable._assign_child_references") return current_row = start_row if self.title: current_row += 1 current_row += 1 data_start_row = current_row if not self.columns: return for c_idx, table_col in enumerate(self.columns): series = table_col.series if not series: continue current_data_col = start_col + c_idx for r_idx, key in enumerate(series.index): value_obj = series[key] current_data_row = data_start_row + r_idx layout_manager._assign_references_recursive(value_obj, current_data_row, current_data_col, sheet_name, ref_map)
[docs] def write( self, worksheet: Any, row: int, col: int, workbook_wrapper: "ExcelWorkbook", ref_map: dict, column_widths: Optional[Dict[int, float]] = None, ): """Writes the table to Excel.""" current_row = row current_col = col # Write title if exists and track width if self.title: worksheet.write(current_row, current_col, self.title) if column_widths is not None: # Estimate width - assumes title spans first column for now width = len(str(self.title)) + 1.5 column_widths[current_col] = max(column_widths.get(current_col, 0), width) current_row += 1 # Move down for headers # Write headers and track width header_col = current_col max_rows = 0 # Keep track of max rows for data area size calculation if needed later if self.columns: # Only write headers if columns exist for table_col in self.columns: # TODO: Add header styling header_text = table_col.series.name if table_col.series else "" # Handle missing series name worksheet.write(current_row, header_col, header_text) if column_widths is not None: width = len(str(header_text)) + 1.5 column_widths[header_col] = max(column_widths.get(header_col, 0), width) if table_col.series: max_rows = max(max_rows, len(table_col.series)) header_col += 1 current_row += 1 # Move down for data # Write data data_start_row = current_row if self.columns: # Only write data if columns exist for c_idx, table_col in enumerate(self.columns): series = table_col.series if not series: continue # Skip if no series data for this column write_col = current_col + c_idx for r_idx, key in enumerate(series.index): value_obj = series[key] write_row = data_start_row + r_idx # Pass tracker down to ExcelValue.write value_obj.write( worksheet, write_row, write_col, workbook_wrapper, ref_map, column_widths, )
# TODO: Return size/bounding box?
[docs] class ExcelParameterTable: """Specialized table for displaying named parameters (Name, Value, Unit).""" def __init__(self, title: Optional[str] = None, parameters: Optional[List[ExcelValue]] = None): self.title = title self.parameters: List[ExcelValue] = parameters if parameters is not None else []
[docs] def add(self, value: ExcelValue) -> None: """Add a parameter to the table.""" if not isinstance(value, ExcelValue): raise TypeError("Parameter must be an ExcelValue") if not value.name: print(f"Warning: Adding parameter without a name to ParameterTable: {value}") self.parameters.append(value)
[docs] def get_size(self) -> Tuple[int, int]: """Calculate the size (rows, columns) of the parameter table.""" rows = 0 if self.title: rows += 1 # Row for title rows += 1 # Row for headers ("Parameter", "Value", "Unit") rows += len(self.parameters) # One row per parameter cols = 3 # Fixed columns: Parameter, Value, Unit return (rows, cols)
def _assign_child_references( self, start_row: int, start_col: int, sheet_name: str, layout_manager: "ExcelLayout", ref_map: dict, ): """Assign references to the ExcelValue objects in the parameter list.""" from .layout import ExcelLayout if not isinstance(layout_manager, ExcelLayout): logger.error("layout_manager is not an ExcelLayout instance in ExcelParameterTable._assign_child_references") return current_row = start_row if self.title: current_row += 1 current_row += 1 value_col = start_col + 1 for param_value in self.parameters: layout_manager._assign_references_recursive(param_value, current_row, value_col, sheet_name, ref_map) current_row += 1
[docs] def write( self, worksheet: Any, row: int, col: int, workbook_wrapper: "ExcelWorkbook", ref_map: dict, column_widths: Optional[Dict[int, float]] = None, ): """Writes the parameter table to Excel.""" current_row = row # Write title and track width if self.title: worksheet.write(current_row, col, self.title) if column_widths is not None: width = len(str(self.title)) + 1.5 column_widths[col] = max(column_widths.get(col, 0), width) current_row += 1 # Write headers and track widths headers = ["Parameter", "Value", "Unit"] for h_col_offset, header in enumerate(headers): worksheet.write(current_row, col + h_col_offset, header) if column_widths is not None: width = len(str(header)) + 1.5 column_widths[col + h_col_offset] = max(column_widths.get(col + h_col_offset, 0), width) current_row += 1 # Write parameter rows and track widths for param in self.parameters: name_cell = param.name or "" unit_cell = param.unit or "" # Write name and unit as simple strings and track width worksheet.write(current_row, col, name_cell) if column_widths is not None: width = len(str(name_cell)) + 1.5 column_widths[col] = max(column_widths.get(col, 0), width) # Write the ExcelValue - this will handle formulas/styling/formatting and track its width param.write( worksheet, current_row, col + 1, workbook_wrapper, ref_map, column_widths, ) worksheet.write(current_row, col + 2, unit_cell) if column_widths is not None: width = len(str(unit_cell)) + 1.5 column_widths[col + 2] = max(column_widths.get(col + 2, 0), width) current_row += 1
# TODO: Return size/bounding box?