Source code for sec_interp.core.utils.rendering

from __future__ import annotations

"""Rendering Utilities Module.

This module provides visualization and coordinate transformation utilities for profile rendering.
"""

import math
from collections.abc import Callable

from sec_interp.core.types import GeologySegment


[docs] def calculate_bounds( topo_data: list[tuple[float, float]], geol_data: list[GeologySegment] | None = None, ) -> dict[str, float]: """Calculate the bounding box for all profile data with padding. Calculates the minimum and maximum distance and elevation across topography and optional geological segments. Args: topo_data: List of (distance, elevation) tuples for topography. geol_data: Optional list of geological segments. Returns: Bounds containing 'min_d', 'max_d', 'min_e', 'max_e' with 5% padding. """ dists = [p[0] for p in topo_data] elevs = [p[1] for p in topo_data] if geol_data: for segment in geol_data: dists.extend([p[0] for p in segment.points]) elevs.extend([p[1] for p in segment.points]) min_d, max_d = min(dists), max(dists) min_e, max_e = min(elevs), max(elevs) # Avoid division by zero if max_d == min_d: max_d = min_d + 100 if max_e == min_e: max_e = min_e + 10 # Add 5% padding d_range = max_d - min_d e_range = max_e - min_e return { "min_d": min_d - d_range * 0.05, "max_d": max_d + d_range * 0.05, "min_e": min_e - e_range * 0.05, "max_e": max_e + e_range * 0.05, }
[docs] def create_coordinate_transform( bounds: dict[str, float], view_w: int, view_h: int, margin: int, vert_exag: float = 1.0, ) -> Callable[[float, float], tuple[float, float]]: """Create a coordinate transformation function for screen projection. Returns a function that transforms data coordinates (distance, elevation) to pixel coordinates (x, y) based on the provided view dimensions. Args: bounds: Dictionary with 'min_d', 'max_d', 'min_e', 'max_e'. view_w: Screen/view width in pixels. view_h: Screen/view height in pixels. margin: Plot margin in pixels. vert_exag: Vertical exaggeration multiplier (default 1.0). Returns: A function `transform(dist, elev) -> (x, y)` converting data to pixels. """ data_w = bounds["max_d"] - bounds["min_d"] data_h = bounds["max_e"] - bounds["min_e"] # Calculate potential scales for each axis potential_scale_x = (view_w - 2 * margin) / data_w potential_scale_y = (view_h - 2 * margin) / data_h # Use the smaller scale as the base to ensure everything fits # This gives us a 1:1 aspect ratio when vert_exag = 1.0 base_scale = min(potential_scale_x, potential_scale_y) # Apply base scale to both axes scale_x = base_scale scale_y = base_scale * vert_exag # Apply vertical exaggeration def transform(dist: float, elev: float) -> tuple[float, float]: """Convert data coordinates to screen coordinates.""" x = margin + (dist - bounds["min_d"]) * scale_x y = view_h - margin - (elev - bounds["min_e"]) * scale_y return x, y return transform
[docs] def calculate_interval(data_range: float) -> float: """Calculate a 'nice' interval for axis labels based on the data range. Args: data_range: The total range of data values (e.g., max_d - min_d). Returns: A human-readable interval (e.g., 1, 2, 5, 10, etc.) for grid lines. """ magnitude = 10 ** math.floor(math.log10(data_range)) normalized = data_range / magnitude if normalized < 2: return magnitude * 0.5 if normalized < 5: return magnitude return magnitude * 2