# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""Configuration dataclasses for :mod:`pycsamt.map`."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Literal, Union
MapTheme = Literal["light", "dark", "publication"]
MapBackend = Literal["plotly", "matplotlib"]
VolumeMode = Literal["fence", "block", "depth", "surface"]
# One interpretation-legend band: ``(rho_min, rho_max, hex_color)`` or, with
# a 4th element, ``(rho_min, rho_max, hex_color, name)`` -- Ω·m linear.
# (``Union`` rather than ``X | Y``: this is evaluated at import time and the
# ``|`` operator on ``tuple[...]`` generics only works on Python >= 3.10.)
GeologyBand = Union[
tuple[float, float, str], tuple[float, float, str, str]
]
GeologyBands = tuple[GeologyBand, ...]
[docs]
@dataclass
class StationMapOptions:
"""Options for 2-D station maps."""
overlay: str = "index"
frequency: float | None = None
frequency_tolerance: float | None = None
# For inversion surveys: the depth (m below surface) at which
# ``overlay="resistivity"`` slices the precomputed sections. ``None``
# keeps the classic behaviour (apparent resistivity at ``frequency``).
depth: float | None = None
# Draw the per-station scatter markers. Turning them off leaves just
# the filled-contour depth slice / value image over the basemap.
show_markers: bool = True
component: str = "xy"
theme: MapTheme = "light"
backend: MapBackend = "plotly"
selected_id: str | None = None
line_filter: tuple[str, ...] | None = None
marker_size: int = 10
basemap: str | None = None
cmap: str = "plasma"
value_range: tuple[float, float] | None = None
log_color: bool = False
opacity: float = 0.92
show_labels: bool = True
label_fontsize: float = 8.0
label_rotation: float = 0.0
show_profiles: bool = True
elevation_mode: Literal["markers", "contours"] = "markers"
show_contours: bool = False
contour_image: bool = False
contour_mode: str = "filled+lines"
contour_levels: int = 12
contour_opacity: float = 0.55
contour_interp: str = "linear"
contour_smooth: float = 1.0
contour_grid_res: int = 150
bearing: float = 0.0
title: str = ""
[docs]
@dataclass
class ProfileMapOptions:
"""Options for pseudosection and profile-view maps."""
quantity: str = "rho"
component: str = "xy"
components: tuple[str, ...] = ("xy", "yx")
theme: MapTheme = "light"
backend: MapBackend = "plotly"
period_range: tuple[float, float] | None = None
phase_range: tuple[float, float] | None = None
value_range: tuple[float, float] | None = None
x_axis: Literal["station", "distance"] = "station"
log_rho: bool = True
height_per_panel: int = 260
show_errbar: bool = False
cmap: str | None = None
by_line: bool = False
line_cols: int | None = None
[docs]
@dataclass
class VolumeMapOptions:
"""Options for 3-D fence, block, and depth-slice maps."""
mode: VolumeMode = "fence"
quantity: str = "resistivity"
component: str = "xy"
theme: MapTheme = "light"
cmap: str = "RdYlBu_r"
depth_range: tuple[float, float] | None = None
period_range: tuple[float, float] | None = None
rho_range: tuple[float, float] | None = None
iso_range: tuple[float, float] | None = None
value_range: tuple[float, float] | None = None
# Percentile clip (low, high) for the *auto* colour range — ignored
# when ``value_range`` pins it explicitly. Keeps a handful of
# extreme cells (e.g. a ModEM volume's above-topography air fill at
# 1e10-1e13 ohm.m) from stretching the whole colourscale so the real
# earth reads as one flat band. ``None`` or ``(0, 100)`` restores the
# raw min/max behaviour.
crange_percentile: tuple[float, float] | None = (2.0, 98.0)
# Hard "hide every cell above this resistivity (ohm.m)" cutoff. Unlike
# ``rho_range`` (a visibility band that deliberately leaves the colour
# scale put), this also drops the excluded cells from the colour-range
# computation — the one knob that reliably removes air / overburden
# fill from a fence/depth/block view and its colourbar at once.
rho_display_max: float | None = None
log_color: bool = True
opacity: float = 0.85
show_contours: bool = False
show_labels: bool = True
n_slices: int = 5
surface_count: int = 12
line_spacing: float = 1.0
azimuth: float = 0.0
topography: bool = True
show_terrain: bool = True
terrain_opacity: float = 0.7
show_stations: bool = False
# A native Plotly Scatter3d marker symbol, or "triangle-down"/
# "triangle-down-open" -- pycsamt's own extension, rendered as real
# geometry since Plotly's 3-D marker enum has no triangle at all
# (see pycsamt.map.volume.TRIANGLE_DOWN_SYMBOLS).
station_symbol: str = "diamond"
station_size: int = 4
station_color: str = "#1f2937"
station_labels: bool = False
# Screen-space rotation (degrees) for the per-station labels. Only
# meaningful with ``station_labels`` -- the labels are drawn as
# scene annotations (``go.Scatter3d`` text cannot rotate) so a
# crowded line can tilt its labels to 45/90 the way a 2-D section
# does. 0 = horizontal.
station_label_angle: float = 0.0
# Which markers get a *label* (markers themselves are unaffected --
# you still see every station's position). ``station_label_names``
# wins when set: only those station ids are labelled. Otherwise
# ``station_label_fraction`` keeps an evenly-spaced fraction per
# line (1.0 = all, 0.5 = every other, ...), first and last always
# kept. Lets a crowded line stay readable without hiding stations.
station_label_fraction: float = 1.0
station_label_names: tuple[str, ...] | None = None
# Cap on how many station markers/labels are drawn per line, evenly
# spaced along that line's own station order (first and last are
# always kept) -- lets a crowded fence/block with many stations
# thin the overlay to, say, 5 or 10 per line instead of every one.
# None (default) shows every station, unchanged from before this
# option existed.
max_stations: int | None = None
aspectmode: str = "data"
# Vertical exaggeration of the 3-D scene. ``None`` keeps
# ``aspectmode``; ``0`` = automatic (depth about half the horizontal
# extent -- a survey tens of km wide over a few km of model otherwise
# reads as a thin sheet); ``> 0`` = that factor. See
# :func:`pycsamt.map.volume.apply_vertical_exaggeration`.
vertical_exaggeration: float | None = None
x_unit: str = "m"
depth_unit: str = "m"
# Fence panels: resample each per-line section onto a denser
# spline grid so it reads as a smooth curtain, not raw stripes.
smooth_sections: bool = True
section_res: int = 100
# Block / iso-surface / anomaly structure smoothing. 0.0 (default)
# renders the volume exactly as reconstructed on the raw
# station/depth lattice -- the current behaviour. A positive value
# (the mapview "Light / Medium / Strong / Very strong" presets pass
# 0.8 / 1.5 / 2.5 / 4.0) first resamples the volume onto a finer
# 3-D lattice -- 2x to ~3.6x denser per axis, higher = denser --
# so Plotly's marching-cubes reconstructs rounded iso-surfaces (the
# Geosoft-voxel look) instead of faceting on the sparse grid, then
# applies a NaN-aware Gaussian whose width scales with that lattice
# and, for a resistivity-band body, feathers the in/out-of-band
# cliff so the envelope is a smooth contour, not a voxel staircase.
volume_smoothing: float = 0.0
title: str = ""
# Interpretation overlay: a resistivity-to-geology legend applied as a
# discrete colour scale instead of ``cmap``'s continuous ramp. A plain
# list of ``(rho_min, rho_max, hex_color)`` bands -- or
# ``(rho_min, rho_max, hex_color, name)`` to also replace the
# colourbar's numeric Ω·m ticks with the band's name -- Ω·m linear,
# kept format-agnostic here; the app layer builds it from a
# ``pycsamt.geology.lithology.RockDatabase`` /
# ``pycsamt.format.geology.GeologyLegend``. ``None`` (default) keeps
# the existing continuous-colourscale behaviour untouched. See
# ``pycsamt.map.styles.geology_colorscale``/``geology_crange``/
# ``geology_colorbar_ticks``.
geology: GeologyBands | None = None
# Whether the on-canvas geology legend (a fixed-row-height swatch
# list in the right margin -- never overlaps, however close two
# bands' resistivities are) is actually drawn when ``geology`` is
# set. ``True`` by default; the Map View toolbar's legend toggle
# flips this to reclaim the plot's full width without discarding
# the applied legend itself. See
# ``pycsamt.map.styles.geology_legend_shapes_annotations``.
geology_legend: bool = True
# Which reading of the legend the colour axis draws when both
# ``geology`` and ``geology_legend`` are set:
# - "swatch" (default) -- Plotly's own colourbar is hidden; a
# non-overlapping on-canvas swatch-chip legend is drawn instead
# (see ``pycsamt.map.styles.geology_legend_shapes_annotations``).
# - "colorbar" -- the earlier reading: Plotly's native colourbar
# stays, with each band's name as a tick label at its own
# position (see ``pycsamt.map.styles.geology_colorbar_ticks``) --
# compact for a short legend, but ticks can crowd/overlap once
# bands are numerous or close in value, which is why "swatch" is
# the default. Both honour ``geology_legend`` for show/hide.
geology_legend_style: str = "swatch"
# Fence / depth-slice pattern *texture* fill -- "solid" (default)
# colours a band flat, exactly as before; "pattern" additionally
# samples each band's assigned pattern (``geology_patterns``) at
# every cell's own physical position, so the swatch's actual
# hatch/stipple/brick shape appears tied to real screen position,
# not just a flat colour. Block / iso-surface are not affected by
# this flag and always render solid -- Plotly's Volume/Isosurface
# traces have no per-cell colour-axis override the way go.Surface's
# ``surfacecolor`` does, so there is no way to texture-map a
# pattern onto that geometry at all; this is a real capability
# gap in Plotly, not a scope choice.
geology_fill: str = "solid"
# ``{band name: ink-density stencil array, values in [0, 1], H x W}``
# for every legend entry that has an assigned pattern -- built by
# the app layer from ``pycsamt.geology.patterns``
# (``pycsamt.app._geology.geology_pattern_stencils_from_store``).
# Only consulted when ``geology_fill == "pattern"``.
geology_patterns: dict | None = None
# Physical repeat size (in ``x_unit``/``depth_unit`` metres) a
# pattern tiles at -- smaller reads as a finer/denser hatch.
pattern_tile_size_m: float = 20.0
[docs]
@dataclass
class ExportOptions:
"""Options used by figure export helpers."""
path: str | Path
format: str | None = None
width: int | None = None
height: int | None = None
scale: float = 2.0
include_plotlyjs: str | bool = "cdn"