Skip to content

Mapping API

This page provides detailed API references for mapping submodules.

Data Loading and Preparation

minexpy.mapping.dataloader

Data loading and preprocessing utilities for geochemical point mapping.

This module implements Step 1 of MinexPy's mapping workflow. It focuses on loading point datasets, validating required fields, handling missing/non-numeric values, projecting coordinates, and applying optional concentration transforms.

Examples:

Prepare point data from a DataFrame:

>>> import pandas as pd
>>> from minexpy.mapping.dataloader import prepare
>>>
>>> df = pd.DataFrame(
...     {
...         "lon": [44.1, 44.2, 44.3],
...         "lat": [36.5, 36.6, 36.7],
...         "Cu_ppm": [12.5, 25.0, 18.2],
...     }
... )
>>> prepared, meta = prepare(
...     data=df,
...     x_col="lon",
...     y_col="lat",
...     value_col="Cu_ppm",
...     source_crs="EPSG:4326",
...     target_crs="EPSG:3857",
...     value_transform="log10",
... )
>>> prepared[["x", "y", "value", "value_raw"]].head()

Invert transformed values for display:

>>> from minexpy.mapping.dataloader import invert_values_for_display
>>> restored = invert_values_for_display(prepared["value"], meta)

GeochemDataWarning

Bases: UserWarning

Warning category for non-fatal data quality issues during preparation.

Source code in minexpy/mapping/dataloader.py
class GeochemDataWarning(UserWarning):
    """Warning category for non-fatal data quality issues during preparation."""

GeochemPrepareMetadata dataclass

Metadata describing cleaning and transformation actions applied to a dataset.

Parameters:

Name Type Description Default
source_crs str

Coordinate reference system identifier for input coordinates.

required
target_crs str

Coordinate reference system identifier for output coordinates.

required
x_column str

Name of the input x-coordinate column.

required
y_column str

Name of the input y-coordinate column.

required
value_column str

Name of the input concentration column.

required
n_input_rows int

Number of rows in input data before cleaning.

required
n_dropped_nan_or_non_numeric int

Number of rows dropped due to missing/non-numeric required fields.

required
n_dropped_projection_invalid int

Number of rows dropped due to non-finite projected coordinates.

required
n_dropped_value_transform_invalid int

Number of rows dropped due to invalid transformed concentration values.

required
n_dropped_duplicates int

Number of duplicate coordinate rows dropped.

required
value_transform_applied bool

Whether a value transformation was applied.

required
value_transform_name str

Name of the value transformation ('none', 'log10', or callable name).

required
can_invert_for_display bool

Whether transformed values can be inverted back for display.

required
inverse_transform_name str

Name of inverse transform, when available.

None
Source code in minexpy/mapping/dataloader.py
@dataclass(frozen=True)
class GeochemPrepareMetadata:
    """
    Metadata describing cleaning and transformation actions applied to a dataset.

    Parameters
    ----------
    source_crs : str
        Coordinate reference system identifier for input coordinates.
    target_crs : str
        Coordinate reference system identifier for output coordinates.
    x_column : str
        Name of the input x-coordinate column.
    y_column : str
        Name of the input y-coordinate column.
    value_column : str
        Name of the input concentration column.
    n_input_rows : int
        Number of rows in input data before cleaning.
    n_dropped_nan_or_non_numeric : int
        Number of rows dropped due to missing/non-numeric required fields.
    n_dropped_projection_invalid : int
        Number of rows dropped due to non-finite projected coordinates.
    n_dropped_value_transform_invalid : int
        Number of rows dropped due to invalid transformed concentration values.
    n_dropped_duplicates : int
        Number of duplicate coordinate rows dropped.
    value_transform_applied : bool
        Whether a value transformation was applied.
    value_transform_name : str
        Name of the value transformation ('none', 'log10', or callable name).
    can_invert_for_display : bool
        Whether transformed values can be inverted back for display.
    inverse_transform_name : str, optional
        Name of inverse transform, when available.
    """

    source_crs: str
    target_crs: str
    x_column: str
    y_column: str
    value_column: str
    n_input_rows: int
    n_dropped_nan_or_non_numeric: int
    n_dropped_projection_invalid: int
    n_dropped_value_transform_invalid: int
    n_dropped_duplicates: int
    value_transform_applied: bool
    value_transform_name: str
    can_invert_for_display: bool
    inverse_transform_name: Optional[str] = None

invert_values_for_display(values, metadata)

Invert transformed concentration values for display when possible.

Parameters:

Name Type Description Default
values array - like

Transformed or untransformed concentration values.

required
metadata GeochemPrepareMetadata

Metadata returned by prepare.

required

Returns:

Type Description
ndarray

Values mapped back to display scale when inversion is available.

Raises:

Type Description
ValueError

If inversion is not available for the applied value transform.

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> from minexpy.mapping.dataloader import prepare, invert_values_for_display
>>> df = pd.DataFrame({"x": [1, 2], "y": [3, 4], "Cu": [10.0, 100.0]})
>>> prepared, meta = prepare(df, "x", "y", "Cu", value_transform="log10")
>>> np.allclose(invert_values_for_display(prepared["value"], meta), prepared["value_raw"])
True
Source code in minexpy/mapping/dataloader.py
def invert_values_for_display(
    values: ArrayLike1D,
    metadata: GeochemPrepareMetadata,
) -> np.ndarray:
    """
    Invert transformed concentration values for display when possible.

    Parameters
    ----------
    values : array-like
        Transformed or untransformed concentration values.
    metadata : GeochemPrepareMetadata
        Metadata returned by `prepare`.

    Returns
    -------
    numpy.ndarray
        Values mapped back to display scale when inversion is available.

    Raises
    ------
    ValueError
        If inversion is not available for the applied value transform.

    Examples
    --------
    >>> import numpy as np
    >>> import pandas as pd
    >>> from minexpy.mapping.dataloader import prepare, invert_values_for_display
    >>> df = pd.DataFrame({"x": [1, 2], "y": [3, 4], "Cu": [10.0, 100.0]})
    >>> prepared, meta = prepare(df, "x", "y", "Cu", value_transform="log10")
    >>> np.allclose(invert_values_for_display(prepared["value"], meta), prepared["value_raw"])
    True
    """
    array = np.asarray(values, dtype=float)

    if not metadata.value_transform_applied or metadata.value_transform_name in {"none", "identity"}:
        return array.copy()

    if metadata.value_transform_name == "log10" and metadata.can_invert_for_display:
        return np.power(10.0, array)

    raise ValueError(
        "Cannot invert values for display because the applied transform does not "
        "define a known inverse in metadata."
    )

prepare(data, x_col, y_col, value_col, source_crs='EPSG:4326', target_crs='EPSG:4326', coordinate_transform=None, value_transform=None, drop_duplicate_coordinates=True)

Load and prepare geochemical point data for mapping workflows.

The function validates required columns, enforces numeric types, removes rows with missing values, projects coordinates, applies optional value transformation, and optionally drops duplicated coordinates.

Parameters:

Name Type Description Default
data DataFrame or path - like

Input geochemical point table, or path to .csv, .xls, or .xlsx.

required
x_col str

Column name for x coordinate (longitude/easting).

required
y_col str

Column name for y coordinate (latitude/northing).

required
value_col str

Column name containing element concentration values.

required
source_crs str

Input coordinate reference system.

'EPSG:4326'
target_crs str

Output coordinate reference system.

'EPSG:4326'
coordinate_transform callable

Optional coordinate hook with signature (x, y) -> (x_new, y_new). When provided, it takes precedence over built-in CRS transforms.

None
value_transform (None, log10, callable)

Optional concentration transform. 'log10' applies base-10 logarithm. Custom callables must return an array of the same shape.

None
drop_duplicate_coordinates bool

If True, duplicate (x, y) rows are dropped after warning.

True

Returns:

Type Description
(DataFrame, GeochemPrepareMetadata)

Prepared table and metadata describing all cleaning/transformation actions.

Examples:

>>> import pandas as pd
>>> from minexpy.mapping.dataloader import prepare
>>> df = pd.DataFrame(
...     {"x": [44.1, 44.1, 44.2], "y": [36.5, 36.5, 36.6], "Zn": [11.0, 11.0, 15.5]}
... )
>>> prepared, meta = prepare(df, "x", "y", "Zn", value_transform="log10")
>>> prepared[["x", "y", "value", "value_raw"]].head()
Notes

Built-in projection support is intentionally limited to:

  • EPSG:4326 to EPSG:3857
  • EPSG:3857 to EPSG:4326
  • identity when source and target CRS are equal

For other CRS pairs, pass a custom coordinate_transform callable.

References

.. [1] Snyder, J. P. (1987). Map Projections: A Working Manual. U.S. Geological Survey Professional Paper 1395.

Source code in minexpy/mapping/dataloader.py
def prepare(
    data: TabularInput,
    x_col: str,
    y_col: str,
    value_col: str,
    source_crs: str = "EPSG:4326",
    target_crs: str = "EPSG:4326",
    coordinate_transform: Optional[CoordinateTransform] = None,
    value_transform: Optional[Union[str, ValueTransform]] = None,
    drop_duplicate_coordinates: bool = True,
) -> Tuple[pd.DataFrame, GeochemPrepareMetadata]:
    """
    Load and prepare geochemical point data for mapping workflows.

    The function validates required columns, enforces numeric types, removes
    rows with missing values, projects coordinates, applies optional value
    transformation, and optionally drops duplicated coordinates.

    Parameters
    ----------
    data : DataFrame or path-like
        Input geochemical point table, or path to `.csv`, `.xls`, or `.xlsx`.
    x_col : str
        Column name for x coordinate (longitude/easting).
    y_col : str
        Column name for y coordinate (latitude/northing).
    value_col : str
        Column name containing element concentration values.
    source_crs : str, default 'EPSG:4326'
        Input coordinate reference system.
    target_crs : str, default 'EPSG:4326'
        Output coordinate reference system.
    coordinate_transform : callable, optional
        Optional coordinate hook with signature `(x, y) -> (x_new, y_new)`.
        When provided, it takes precedence over built-in CRS transforms.
    value_transform : {None, 'log10', callable}, default None
        Optional concentration transform. `'log10'` applies base-10 logarithm.
        Custom callables must return an array of the same shape.
    drop_duplicate_coordinates : bool, default True
        If True, duplicate `(x, y)` rows are dropped after warning.

    Returns
    -------
    DataFrame, GeochemPrepareMetadata
        Prepared table and metadata describing all cleaning/transformation
        actions.

    Examples
    --------
    >>> import pandas as pd
    >>> from minexpy.mapping.dataloader import prepare
    >>> df = pd.DataFrame(
    ...     {"x": [44.1, 44.1, 44.2], "y": [36.5, 36.5, 36.6], "Zn": [11.0, 11.0, 15.5]}
    ... )
    >>> prepared, meta = prepare(df, "x", "y", "Zn", value_transform="log10")
    >>> prepared[["x", "y", "value", "value_raw"]].head()

    Notes
    -----
    Built-in projection support is intentionally limited to:

    - `EPSG:4326` to `EPSG:3857`
    - `EPSG:3857` to `EPSG:4326`
    - identity when source and target CRS are equal

    For other CRS pairs, pass a custom `coordinate_transform` callable.

    References
    ----------
    .. [1] Snyder, J. P. (1987). Map Projections: A Working Manual.
           U.S. Geological Survey Professional Paper 1395.
    """
    source_crs_normalized = _normalize_crs(source_crs, "source_crs")
    target_crs_normalized = _normalize_crs(target_crs, "target_crs")

    frame = _load_tabular_input(data)
    required_columns = [x_col, y_col, value_col]
    missing_columns = [column for column in required_columns if column not in frame.columns]
    if missing_columns:
        raise KeyError(f"Missing required columns: {missing_columns}")

    n_input_rows = int(len(frame))
    frame = frame.copy()
    for column in required_columns:
        frame[column] = pd.to_numeric(frame[column], errors="coerce")

    valid_required_mask = frame[required_columns].notna().all(axis=1).to_numpy()
    n_dropped_nan_or_non_numeric = int((~valid_required_mask).sum())
    if n_dropped_nan_or_non_numeric > 0:
        warnings.warn(
            f"Dropped {n_dropped_nan_or_non_numeric} rows with missing or non-numeric "
            "required fields.",
            GeochemDataWarning,
        )

    frame = frame.loc[valid_required_mask].copy()
    if frame.empty:
        raise ValueError("No valid rows remain after removing missing/non-numeric required fields")

    x_values = frame[x_col].to_numpy(dtype=float)
    y_values = frame[y_col].to_numpy(dtype=float)
    raw_values = frame[value_col].to_numpy(dtype=float)

    transform_coordinates = _resolve_coordinate_transform(
        source_crs=source_crs_normalized,
        target_crs=target_crs_normalized,
        coordinate_transform=coordinate_transform,
    )
    projected = transform_coordinates(x_values.copy(), y_values.copy())
    if not isinstance(projected, tuple) or len(projected) != 2:
        raise ValueError("coordinate_transform must return a tuple: (x_array, y_array)")

    projected_x = np.asarray(projected[0], dtype=float)
    projected_y = np.asarray(projected[1], dtype=float)
    if projected_x.shape != x_values.shape or projected_y.shape != y_values.shape:
        raise ValueError(
            "coordinate_transform must return arrays with the same shape as input coordinates"
        )

    finite_projection_mask = np.isfinite(projected_x) & np.isfinite(projected_y)
    n_dropped_projection_invalid = int((~finite_projection_mask).sum())
    if n_dropped_projection_invalid > 0:
        warnings.warn(
            f"Dropped {n_dropped_projection_invalid} rows with invalid projected coordinates.",
            GeochemDataWarning,
        )
        frame = frame.loc[finite_projection_mask].copy()
        projected_x = projected_x[finite_projection_mask]
        projected_y = projected_y[finite_projection_mask]
        raw_values = raw_values[finite_projection_mask]

    if frame.empty:
        raise ValueError("No valid rows remain after coordinate projection filtering")

    transformed_values, transform_name, can_invert, inverse_name = _apply_value_transform(
        raw_values, value_transform
    )
    finite_value_mask = np.isfinite(transformed_values)
    n_dropped_value_transform_invalid = int((~finite_value_mask).sum())
    if n_dropped_value_transform_invalid > 0:
        warnings.warn(
            f"Dropped {n_dropped_value_transform_invalid} rows with invalid transformed values.",
            GeochemDataWarning,
        )
        frame = frame.loc[finite_value_mask].copy()
        projected_x = projected_x[finite_value_mask]
        projected_y = projected_y[finite_value_mask]
        raw_values = raw_values[finite_value_mask]
        transformed_values = transformed_values[finite_value_mask]

    if frame.empty:
        raise ValueError("No valid rows remain after value transformation filtering")

    frame["x"] = projected_x
    frame["y"] = projected_y
    frame["value_raw"] = raw_values
    frame["value"] = transformed_values

    duplicate_mask = frame.duplicated(subset=["x", "y"], keep="first").to_numpy()
    n_duplicates = int(duplicate_mask.sum())
    n_dropped_duplicates = 0
    if n_duplicates > 0:
        action = "dropped (keeping first occurrence)" if drop_duplicate_coordinates else "retained"
        warnings.warn(
            f"Detected {n_duplicates} duplicate coordinate rows; duplicates were {action}.",
            GeochemDataWarning,
        )
        if drop_duplicate_coordinates:
            frame = frame.loc[~duplicate_mask].copy()
            n_dropped_duplicates = n_duplicates

    if frame.empty:
        raise ValueError("No valid rows remain after duplicate-coordinate filtering")

    metadata = GeochemPrepareMetadata(
        source_crs=source_crs_normalized,
        target_crs=target_crs_normalized,
        x_column=x_col,
        y_column=y_col,
        value_column=value_col,
        n_input_rows=n_input_rows,
        n_dropped_nan_or_non_numeric=n_dropped_nan_or_non_numeric,
        n_dropped_projection_invalid=n_dropped_projection_invalid,
        n_dropped_value_transform_invalid=n_dropped_value_transform_invalid,
        n_dropped_duplicates=n_dropped_duplicates,
        value_transform_applied=(transform_name != "none"),
        value_transform_name=transform_name,
        can_invert_for_display=can_invert,
        inverse_transform_name=inverse_name,
    )

    return frame.reset_index(drop=True), metadata

Grid Generation

minexpy.mapping.gridding

Grid creation utilities for geochemical mapping workflows.

This module implements Step 2 of MinexPy's mapping workflow. It builds a regular 2D grid (base layer) from prepared point coordinates so later steps can perform interpolation and map rendering.

Examples:

Create a mesh from prepared point data:

>>> import pandas as pd
>>> from minexpy.mapping import create_grid
>>> data = pd.DataFrame(
...     {
...         "x": [100.0, 130.0, 140.0, 170.0],
...         "y": [200.0, 210.0, 240.0, 260.0],
...     }
... )
>>> grid = create_grid(data, cell_size=10.0)
>>> grid.Xi.shape
(8, 10)

GridDefinition dataclass

Container for mesh grid geometry and metadata.

Parameters:

Name Type Description Default
x_col str

Name of the x-coordinate column used for grid creation.

required
y_col str

Name of the y-coordinate column used for grid creation.

required
raw_extent tuple of float

Unpadded data extent as (xmin, xmax, ymin, ymax).

required
padded_extent tuple of float

Padded grid extent as (xmin, xmax, ymin, ymax).

required
cell_size float

Uniform grid spacing used for both x and y axes.

required
padding_ratio float

Relative padding applied to each axis range.

required
xi ndarray

One-dimensional x-axis coordinates of the grid.

required
yi ndarray

One-dimensional y-axis coordinates of the grid.

required
Xi ndarray

Two-dimensional x-coordinate mesh from numpy.meshgrid.

required
Yi ndarray

Two-dimensional y-coordinate mesh from numpy.meshgrid.

required
grid_points ndarray

Flattened grid nodes of shape (n_nodes, 2) for interpolation.

required
nx int

Number of nodes along x-axis.

required
ny int

Number of nodes along y-axis.

required
n_nodes int

Total node count (nx * ny).

required
Source code in minexpy/mapping/gridding.py
@dataclass(frozen=True)
class GridDefinition:
    """
    Container for mesh grid geometry and metadata.

    Parameters
    ----------
    x_col : str
        Name of the x-coordinate column used for grid creation.
    y_col : str
        Name of the y-coordinate column used for grid creation.
    raw_extent : tuple of float
        Unpadded data extent as ``(xmin, xmax, ymin, ymax)``.
    padded_extent : tuple of float
        Padded grid extent as ``(xmin, xmax, ymin, ymax)``.
    cell_size : float
        Uniform grid spacing used for both x and y axes.
    padding_ratio : float
        Relative padding applied to each axis range.
    xi : numpy.ndarray
        One-dimensional x-axis coordinates of the grid.
    yi : numpy.ndarray
        One-dimensional y-axis coordinates of the grid.
    Xi : numpy.ndarray
        Two-dimensional x-coordinate mesh from ``numpy.meshgrid``.
    Yi : numpy.ndarray
        Two-dimensional y-coordinate mesh from ``numpy.meshgrid``.
    grid_points : numpy.ndarray
        Flattened grid nodes of shape ``(n_nodes, 2)`` for interpolation.
    nx : int
        Number of nodes along x-axis.
    ny : int
        Number of nodes along y-axis.
    n_nodes : int
        Total node count (``nx * ny``).
    """

    x_col: str
    y_col: str
    raw_extent: Extent
    padded_extent: Extent
    cell_size: float
    padding_ratio: float
    xi: np.ndarray
    yi: np.ndarray
    Xi: np.ndarray
    Yi: np.ndarray
    grid_points: np.ndarray
    nx: int
    ny: int
    n_nodes: int

create_grid(data, cell_size, x_col='x', y_col='y', padding_ratio=0.05)

Build a regular mesh grid from prepared geochemical point coordinates.

The function computes data extent, applies relative padding, creates one-dimensional grid axes, builds a 2D mesh using numpy.meshgrid, and provides a flattened (x, y) node array for interpolation routines.

Parameters:

Name Type Description Default
data DataFrame

Prepared point dataset, typically returned by minexpy.mapping.prepare. Must contain finite numeric coordinate columns.

required
cell_size float

Uniform grid spacing for x and y axes. Must be finite and greater than zero.

required
x_col str

Name of x-coordinate column.

'x'
y_col str

Name of y-coordinate column.

'y'
padding_ratio float

Relative padding added to each side of the data extent. Must be finite and non-negative.

0.05

Returns:

Type Description
GridDefinition

Dataclass containing extents, axes, mesh arrays, flattened points, and node counts.

Raises:

Type Description
ValueError

If input data is invalid, coordinate values are non-finite, axis ranges are zero, or numeric parameters are invalid.

KeyError

If x_col or y_col does not exist in data.

Examples:

>>> import pandas as pd
>>> from minexpy.mapping import create_grid
>>> prepared = pd.DataFrame(
...     {
...         "x": [0.0, 100.0, 200.0],
...         "y": [0.0, 50.0, 100.0],
...     }
... )
>>> grid = create_grid(prepared, cell_size=25.0, padding_ratio=0.10)
>>> grid.grid_points.shape[1]
2
Notes

This step only creates mesh geometry. It does not interpolate concentration values. Interpolation is handled in a later mapping step.

References

.. [1] Burrough, P. A., & McDonnell, R. A. (1998). Principles of Geographical Information Systems. Oxford University Press.

Source code in minexpy/mapping/gridding.py
def create_grid(
    data: pd.DataFrame,
    cell_size: float,
    x_col: str = "x",
    y_col: str = "y",
    padding_ratio: float = 0.05,
) -> GridDefinition:
    """
    Build a regular mesh grid from prepared geochemical point coordinates.

    The function computes data extent, applies relative padding, creates
    one-dimensional grid axes, builds a 2D mesh using ``numpy.meshgrid``, and
    provides a flattened ``(x, y)`` node array for interpolation routines.

    Parameters
    ----------
    data : pandas.DataFrame
        Prepared point dataset, typically returned by
        ``minexpy.mapping.prepare``. Must contain finite numeric coordinate
        columns.
    cell_size : float
        Uniform grid spacing for x and y axes. Must be finite and greater
        than zero.
    x_col : str, default 'x'
        Name of x-coordinate column.
    y_col : str, default 'y'
        Name of y-coordinate column.
    padding_ratio : float, default 0.05
        Relative padding added to each side of the data extent. Must be
        finite and non-negative.

    Returns
    -------
    GridDefinition
        Dataclass containing extents, axes, mesh arrays, flattened points,
        and node counts.

    Raises
    ------
    ValueError
        If input data is invalid, coordinate values are non-finite, axis
        ranges are zero, or numeric parameters are invalid.
    KeyError
        If ``x_col`` or ``y_col`` does not exist in ``data``.

    Examples
    --------
    >>> import pandas as pd
    >>> from minexpy.mapping import create_grid
    >>> prepared = pd.DataFrame(
    ...     {
    ...         "x": [0.0, 100.0, 200.0],
    ...         "y": [0.0, 50.0, 100.0],
    ...     }
    ... )
    >>> grid = create_grid(prepared, cell_size=25.0, padding_ratio=0.10)
    >>> grid.grid_points.shape[1]
    2

    Notes
    -----
    This step only creates mesh geometry. It does not interpolate
    concentration values. Interpolation is handled in a later mapping step.

    References
    ----------
    .. [1] Burrough, P. A., & McDonnell, R. A. (1998). Principles of
           Geographical Information Systems. Oxford University Press.
    """
    if not isinstance(data, pd.DataFrame):
        raise ValueError("data must be a pandas DataFrame")
    if data.empty:
        raise ValueError("data must not be empty")
    if x_col not in data.columns or y_col not in data.columns:
        missing = [name for name in (x_col, y_col) if name not in data.columns]
        raise KeyError(f"Missing coordinate columns: {missing}")

    if not np.isfinite(cell_size) or cell_size <= 0:
        raise ValueError("cell_size must be a finite float greater than zero")
    if not np.isfinite(padding_ratio) or padding_ratio < 0:
        raise ValueError("padding_ratio must be a finite float greater than or equal to zero")

    x_values = pd.to_numeric(data[x_col], errors="coerce").to_numpy(dtype=float)
    y_values = pd.to_numeric(data[y_col], errors="coerce").to_numpy(dtype=float)

    finite_mask = np.isfinite(x_values) & np.isfinite(y_values)
    if not finite_mask.all():
        invalid_count = int((~finite_mask).sum())
        raise ValueError(
            f"Coordinate columns must contain only finite numeric values; "
            f"found {invalid_count} invalid rows"
        )

    xmin = float(np.min(x_values))
    xmax = float(np.max(x_values))
    ymin = float(np.min(y_values))
    ymax = float(np.max(y_values))
    raw_extent: Extent = (xmin, xmax, ymin, ymax)

    x_range = xmax - xmin
    y_range = ymax - ymin
    if x_range == 0:
        raise ValueError("x range is zero; cannot create 2D grid from identical x values")
    if y_range == 0:
        raise ValueError("y range is zero; cannot create 2D grid from identical y values")

    x_pad = x_range * float(padding_ratio)
    y_pad = y_range * float(padding_ratio)

    xmin_pad = xmin - x_pad
    xmax_pad = xmax + x_pad
    ymin_pad = ymin - y_pad
    ymax_pad = ymax + y_pad
    padded_extent: Extent = (xmin_pad, xmax_pad, ymin_pad, ymax_pad)

    xi = np.arange(xmin_pad, xmax_pad + cell_size, cell_size, dtype=float)
    yi = np.arange(ymin_pad, ymax_pad + cell_size, cell_size, dtype=float)

    Xi, Yi = np.meshgrid(xi, yi)
    grid_points = np.c_[Xi.ravel(), Yi.ravel()]

    nx = int(xi.size)
    ny = int(yi.size)
    n_nodes = int(grid_points.shape[0])

    return GridDefinition(
        x_col=x_col,
        y_col=y_col,
        raw_extent=raw_extent,
        padded_extent=padded_extent,
        cell_size=float(cell_size),
        padding_ratio=float(padding_ratio),
        xi=xi,
        yi=yi,
        Xi=Xi,
        Yi=Yi,
        grid_points=grid_points,
        nx=nx,
        ny=ny,
        n_nodes=n_nodes,
    )

Interpolation

minexpy.mapping.interpolation

Interpolation utilities for geochemical mapping workflows.

This module implements Step 3 of MinexPy's mapping workflow. It interpolates prepared point values onto a regular grid using multiple methods.

Examples:

Run interpolation with the dispatcher:

>>> import pandas as pd
>>> from minexpy.mapping import create_grid, interpolate
>>> prepared = pd.DataFrame(
...     {
...         "x": [0.0, 50.0, 100.0, 100.0],
...         "y": [0.0, 100.0, 0.0, 100.0],
...         "value": [10.0, 15.0, 20.0, 30.0],
...     }
... )
>>> grid = create_grid(prepared, cell_size=10.0)
>>> result = interpolate(prepared, grid, method="idw")
>>> result.Z.shape == (grid.ny, grid.nx)
True

InterpolationResult dataclass

Container for interpolation outputs and diagnostics.

Parameters:

Name Type Description Default
grid GridDefinition

Grid metadata and geometry used for interpolation.

required
Z ndarray

Interpolated values on grid with shape (grid.ny, grid.nx).

required
method str

Interpolation method identifier.

required
value_col str

Name of the source value column used from input data.

required
valid_mask ndarray

Boolean mask where interpolated values are finite.

required
parameters dict

Method parameters used during interpolation.

required
converged bool

Convergence status for iterative methods (minimum curvature).

None
iterations int

Number of iterations performed by iterative methods.

None
max_change float

Final maximum absolute iteration update for iterative methods.

None
Source code in minexpy/mapping/interpolation.py
@dataclass(frozen=True)
class InterpolationResult:
    """
    Container for interpolation outputs and diagnostics.

    Parameters
    ----------
    grid : GridDefinition
        Grid metadata and geometry used for interpolation.
    Z : numpy.ndarray
        Interpolated values on grid with shape ``(grid.ny, grid.nx)``.
    method : str
        Interpolation method identifier.
    value_col : str
        Name of the source value column used from input data.
    valid_mask : numpy.ndarray
        Boolean mask where interpolated values are finite.
    parameters : dict
        Method parameters used during interpolation.
    converged : bool, optional
        Convergence status for iterative methods (minimum curvature).
    iterations : int, optional
        Number of iterations performed by iterative methods.
    max_change : float, optional
        Final maximum absolute iteration update for iterative methods.
    """

    grid: GridDefinition
    Z: np.ndarray
    method: str
    value_col: str
    valid_mask: np.ndarray
    parameters: Dict[str, object]
    converged: Optional[bool] = None
    iterations: Optional[int] = None
    max_change: Optional[float] = None

interpolate(data, grid, method='triangulation', value_col='value', **kwargs)

Dispatch interpolation to one of the supported methods.

Parameters:

Name Type Description Default
data DataFrame

Prepared point data, typically returned by minexpy.mapping.prepare.

required
grid GridDefinition

Grid definition returned by minexpy.mapping.create_grid.

required
method (nearest, triangulation, idw, minimum_curvature)

Interpolation method name.

'nearest'
value_col str

Value column name from data.

'value'
**kwargs dict

Method-specific keyword arguments forwarded to the selected interpolation function.

{}

Returns:

Type Description
InterpolationResult

Interpolation result object containing surface and diagnostics.

Raises:

Type Description
ValueError

If method is not supported.

Source code in minexpy/mapping/interpolation.py
def interpolate(
    data: pd.DataFrame,
    grid: GridDefinition,
    method: str = "triangulation",
    value_col: str = "value",
    **kwargs: object,
) -> InterpolationResult:
    """
    Dispatch interpolation to one of the supported methods.

    Parameters
    ----------
    data : pandas.DataFrame
        Prepared point data, typically returned by ``minexpy.mapping.prepare``.
    grid : GridDefinition
        Grid definition returned by ``minexpy.mapping.create_grid``.
    method : {'nearest', 'triangulation', 'idw', 'minimum_curvature'}, default 'triangulation'
        Interpolation method name.
    value_col : str, default 'value'
        Value column name from ``data``.
    **kwargs : dict
        Method-specific keyword arguments forwarded to the selected
        interpolation function.

    Returns
    -------
    InterpolationResult
        Interpolation result object containing surface and diagnostics.

    Raises
    ------
    ValueError
        If ``method`` is not supported.
    """
    method_name = method.strip().lower()

    if method_name == "nearest":
        return interpolate_nearest(data, grid, value_col=value_col, **kwargs)
    if method_name == "triangulation":
        return interpolate_triangulation(data, grid, value_col=value_col, **kwargs)
    if method_name == "idw":
        return interpolate_idw(data, grid, value_col=value_col, **kwargs)
    if method_name == "minimum_curvature":
        return interpolate_minimum_curvature(data, grid, value_col=value_col, **kwargs)

    raise ValueError(
        "Unknown interpolation method. Supported methods are: "
        "'nearest', 'triangulation', 'idw', 'minimum_curvature'."
    )

interpolate_idw(data, grid, value_col='value', power=2.0, k=12, radius=None, eps=1e-12)

Interpolate values using inverse distance weighting (IDW).

Parameters:

Name Type Description Default
data DataFrame

Prepared point data.

required
grid GridDefinition

Grid definition.

required
value_col str

Value column name from data.

'value'
power float

IDW distance exponent.

2.0
k int

Maximum number of nearest neighbors considered per grid node.

12
radius float

Optional maximum neighbor distance. If provided, neighbors farther than radius are ignored.

None
eps float

Small positive value used to stabilize weight computation near zero distance.

1e-12

Returns:

Type Description
InterpolationResult

Result with IDW interpolated surface.

Notes

If a grid node coincides with one or more samples, exact sample value matching is used instead of weighted averaging.

Source code in minexpy/mapping/interpolation.py
def interpolate_idw(
    data: pd.DataFrame,
    grid: GridDefinition,
    value_col: str = "value",
    power: float = 2.0,
    k: int = 12,
    radius: Optional[float] = None,
    eps: float = 1e-12,
) -> InterpolationResult:
    """
    Interpolate values using inverse distance weighting (IDW).

    Parameters
    ----------
    data : pandas.DataFrame
        Prepared point data.
    grid : GridDefinition
        Grid definition.
    value_col : str, default 'value'
        Value column name from ``data``.
    power : float, default 2.0
        IDW distance exponent.
    k : int, default 12
        Maximum number of nearest neighbors considered per grid node.
    radius : float, optional
        Optional maximum neighbor distance. If provided, neighbors farther than
        ``radius`` are ignored.
    eps : float, default 1e-12
        Small positive value used to stabilize weight computation near zero
        distance.

    Returns
    -------
    InterpolationResult
        Result with IDW interpolated surface.

    Notes
    -----
    If a grid node coincides with one or more samples, exact sample value
    matching is used instead of weighted averaging.
    """
    if not np.isfinite(power) or power <= 0:
        raise ValueError("power must be a finite float greater than zero")
    if not isinstance(k, int) or k <= 0:
        raise ValueError("k must be a positive integer")
    if radius is not None and (not np.isfinite(radius) or radius <= 0):
        raise ValueError("radius must be a finite float greater than zero when provided")
    if not np.isfinite(eps) or eps <= 0:
        raise ValueError("eps must be a finite float greater than zero")

    points, values = _prepare_interpolation_inputs(
        data=data, grid=grid, value_col=value_col, min_points=1
    )

    neighbor_count = min(k, points.shape[0])
    tree = cKDTree(points)

    query_kwargs: Dict[str, object] = {"k": neighbor_count}
    if radius is not None:
        query_kwargs["distance_upper_bound"] = float(radius)

    distances, indices = tree.query(grid.grid_points, **query_kwargs)
    distances = np.asarray(distances, dtype=float)
    indices = np.asarray(indices, dtype=int)

    if distances.ndim == 1:
        distances = distances[:, np.newaxis]
        indices = indices[:, np.newaxis]

    z_flat = np.full(grid.n_nodes, np.nan, dtype=float)
    n_samples = points.shape[0]

    for node_idx in range(grid.n_nodes):
        d_row = distances[node_idx]
        i_row = indices[node_idx]

        valid = np.isfinite(d_row) & (i_row >= 0) & (i_row < n_samples)
        if not np.any(valid):
            continue

        d_valid = d_row[valid]
        i_valid = i_row[valid]
        v_valid = values[i_valid]

        zero_mask = d_valid <= eps
        if np.any(zero_mask):
            z_flat[node_idx] = float(np.mean(v_valid[zero_mask]))
            continue

        weights = 1.0 / np.power(d_valid + eps, power)
        z_flat[node_idx] = float(np.sum(weights * v_valid) / np.sum(weights))

    Z = z_flat.reshape(grid.ny, grid.nx)

    return _build_result(
        grid=grid,
        Z=Z,
        method="idw",
        value_col=value_col,
        parameters={
            "power": float(power),
            "k": int(k),
            "radius": None if radius is None else float(radius),
            "eps": float(eps),
        },
    )

interpolate_minimum_curvature(data, grid, value_col='value', max_iter=2000, tolerance=0.0001, relaxation=1.0, mask_outside_hull=False)

Interpolate values using iterative grid-based minimum curvature.

Parameters:

Name Type Description Default
data DataFrame

Prepared point data.

required
grid GridDefinition

Grid definition.

required
value_col str

Value column name from data.

'value'
max_iter int

Maximum number of solver iterations.

2000
tolerance float

Convergence threshold on maximum absolute node update.

1e-4
relaxation float

Relaxation factor applied to each node update.

1.0
mask_outside_hull bool

If True, nodes outside convex hull are masked to NaN after solving.

False

Returns:

Type Description
InterpolationResult

Result with minimum-curvature interpolated surface and convergence diagnostics.

Notes

The solver enforces sample constraints on nearest grid nodes at every iteration and minimizes surface roughness via a discrete biharmonic condition in free nodes.

References

.. [1] Briggs, I. C. (1974). Machine contouring using minimum curvature. Geophysics, 39(1), 39-48.

Source code in minexpy/mapping/interpolation.py
def interpolate_minimum_curvature(
    data: pd.DataFrame,
    grid: GridDefinition,
    value_col: str = "value",
    max_iter: int = 2000,
    tolerance: float = 1e-4,
    relaxation: float = 1.0,
    mask_outside_hull: bool = False,
) -> InterpolationResult:
    """
    Interpolate values using iterative grid-based minimum curvature.

    Parameters
    ----------
    data : pandas.DataFrame
        Prepared point data.
    grid : GridDefinition
        Grid definition.
    value_col : str, default 'value'
        Value column name from ``data``.
    max_iter : int, default 2000
        Maximum number of solver iterations.
    tolerance : float, default 1e-4
        Convergence threshold on maximum absolute node update.
    relaxation : float, default 1.0
        Relaxation factor applied to each node update.
    mask_outside_hull : bool, default False
        If True, nodes outside convex hull are masked to NaN after solving.

    Returns
    -------
    InterpolationResult
        Result with minimum-curvature interpolated surface and convergence
        diagnostics.

    Notes
    -----
    The solver enforces sample constraints on nearest grid nodes at every
    iteration and minimizes surface roughness via a discrete biharmonic
    condition in free nodes.

    References
    ----------
    .. [1] Briggs, I. C. (1974). Machine contouring using minimum curvature.
           Geophysics, 39(1), 39-48.
    """
    if not isinstance(max_iter, int) or max_iter <= 0:
        raise ValueError("max_iter must be a positive integer")
    if not np.isfinite(tolerance) or tolerance <= 0:
        raise ValueError("tolerance must be a finite float greater than zero")
    if not np.isfinite(relaxation) or relaxation <= 0:
        raise ValueError("relaxation must be a finite float greater than zero")

    points, values = _prepare_interpolation_inputs(
        data=data, grid=grid, value_col=value_col, min_points=3
    )

    # Initialize with nearest-neighbor field for stable starting surface.
    init_result = interpolate_nearest(data=data, grid=grid, value_col=value_col)
    Z = init_result.Z.copy()

    xi = grid.xi
    yi = grid.yi

    x_indices = np.abs(points[:, 0][:, np.newaxis] - xi[np.newaxis, :]).argmin(axis=1)
    y_indices = np.abs(points[:, 1][:, np.newaxis] - yi[np.newaxis, :]).argmin(axis=1)

    fixed_mask = np.zeros((grid.ny, grid.nx), dtype=bool)
    fixed_values = np.full((grid.ny, grid.nx), np.nan, dtype=float)
    accum_sum = np.zeros((grid.ny, grid.nx), dtype=float)
    accum_count = np.zeros((grid.ny, grid.nx), dtype=int)

    for sample_idx in range(points.shape[0]):
        row = int(y_indices[sample_idx])
        col = int(x_indices[sample_idx])
        accum_sum[row, col] += float(values[sample_idx])
        accum_count[row, col] += 1
        fixed_mask[row, col] = True

    positive_count = accum_count > 0
    fixed_values[positive_count] = accum_sum[positive_count] / accum_count[positive_count]
    Z[fixed_mask] = fixed_values[fixed_mask]

    converged = False
    max_change = np.inf
    iterations = 0

    use_biharmonic = grid.nx >= 5 and grid.ny >= 5

    for iteration in range(1, max_iter + 1):
        max_change_iter = 0.0
        Z_prev = Z.copy()

        if use_biharmonic:
            for row in range(2, grid.ny - 2):
                for col in range(2, grid.nx - 2):
                    if fixed_mask[row, col]:
                        continue

                    z_new = (
                        8.0
                        * (
                            Z_prev[row + 1, col]
                            + Z_prev[row - 1, col]
                            + Z_prev[row, col + 1]
                            + Z_prev[row, col - 1]
                        )
                        - 2.0
                        * (
                            Z_prev[row + 1, col + 1]
                            + Z_prev[row + 1, col - 1]
                            + Z_prev[row - 1, col + 1]
                            + Z_prev[row - 1, col - 1]
                        )
                        - (
                            Z_prev[row + 2, col]
                            + Z_prev[row - 2, col]
                            + Z_prev[row, col + 2]
                            + Z_prev[row, col - 2]
                        )
                    ) / 20.0

                    updated = Z_prev[row, col] + relaxation * (z_new - Z_prev[row, col])
                    delta = abs(updated - Z_prev[row, col])
                    if delta > max_change_iter:
                        max_change_iter = delta
                    Z[row, col] = updated
        else:
            for row in range(1, grid.ny - 1):
                for col in range(1, grid.nx - 1):
                    if fixed_mask[row, col]:
                        continue

                    z_new = 0.25 * (
                        Z_prev[row + 1, col]
                        + Z_prev[row - 1, col]
                        + Z_prev[row, col + 1]
                        + Z_prev[row, col - 1]
                    )
                    updated = Z_prev[row, col] + relaxation * (z_new - Z_prev[row, col])
                    delta = abs(updated - Z_prev[row, col])
                    if delta > max_change_iter:
                        max_change_iter = delta
                    Z[row, col] = updated

        Z[fixed_mask] = fixed_values[fixed_mask]
        iterations = iteration
        max_change = max_change_iter

        if max_change_iter < tolerance:
            converged = True
            break

    if mask_outside_hull:
        outside_mask = _outside_hull_mask(points, grid)
        Z = Z.copy()
        Z[outside_mask] = np.nan

    return _build_result(
        grid=grid,
        Z=Z,
        method="minimum_curvature",
        value_col=value_col,
        parameters={
            "max_iter": int(max_iter),
            "tolerance": float(tolerance),
            "relaxation": float(relaxation),
            "mask_outside_hull": bool(mask_outside_hull),
        },
        converged=bool(converged),
        iterations=int(iterations),
        max_change=float(max_change),
    )

interpolate_nearest(data, grid, value_col='value')

Interpolate values to grid nodes using nearest neighbor assignment.

Parameters:

Name Type Description Default
data DataFrame

Prepared point data.

required
grid GridDefinition

Grid definition.

required
value_col str

Value column name from data.

'value'

Returns:

Type Description
InterpolationResult

Result with nearest-neighbor interpolated surface.

Examples:

>>> import pandas as pd
>>> from minexpy.mapping import create_grid, interpolate_nearest
>>> d = pd.DataFrame({"x": [0, 10], "y": [0, 10], "value": [1.0, 2.0]})
>>> g = create_grid(d, cell_size=5.0)
>>> out = interpolate_nearest(d, g)
>>> out.Z.shape == (g.ny, g.nx)
True
Notes

This method is local and piecewise-constant. It does not smooth between sample locations.

Source code in minexpy/mapping/interpolation.py
def interpolate_nearest(
    data: pd.DataFrame,
    grid: GridDefinition,
    value_col: str = "value",
) -> InterpolationResult:
    """
    Interpolate values to grid nodes using nearest neighbor assignment.

    Parameters
    ----------
    data : pandas.DataFrame
        Prepared point data.
    grid : GridDefinition
        Grid definition.
    value_col : str, default 'value'
        Value column name from ``data``.

    Returns
    -------
    InterpolationResult
        Result with nearest-neighbor interpolated surface.

    Examples
    --------
    >>> import pandas as pd
    >>> from minexpy.mapping import create_grid, interpolate_nearest
    >>> d = pd.DataFrame({"x": [0, 10], "y": [0, 10], "value": [1.0, 2.0]})
    >>> g = create_grid(d, cell_size=5.0)
    >>> out = interpolate_nearest(d, g)
    >>> out.Z.shape == (g.ny, g.nx)
    True

    Notes
    -----
    This method is local and piecewise-constant. It does not smooth between
    sample locations.
    """
    points, values = _prepare_interpolation_inputs(
        data=data, grid=grid, value_col=value_col, min_points=1
    )

    tree = cKDTree(points)
    _, neighbor_indices = tree.query(grid.grid_points, k=1)
    Z = values[neighbor_indices].reshape(grid.ny, grid.nx)

    return _build_result(
        grid=grid,
        Z=Z,
        method="nearest",
        value_col=value_col,
        parameters={},
    )

interpolate_triangulation(data, grid, value_col='value', kind='linear')

Interpolate values using triangulation-based griddata interpolation.

Parameters:

Name Type Description Default
data DataFrame

Prepared point data.

required
grid GridDefinition

Grid definition.

required
value_col str

Value column name from data.

'value'
kind (linear, cubic)

Triangulation interpolation mode passed to scipy.interpolate.griddata.

'linear'

Returns:

Type Description
InterpolationResult

Result with triangulation-based interpolated surface.

Raises:

Type Description
ValueError

If kind is unsupported.

Notes

Grid nodes outside the convex hull of input points are returned as NaN.

Source code in minexpy/mapping/interpolation.py
def interpolate_triangulation(
    data: pd.DataFrame,
    grid: GridDefinition,
    value_col: str = "value",
    kind: str = "linear",
) -> InterpolationResult:
    """
    Interpolate values using triangulation-based griddata interpolation.

    Parameters
    ----------
    data : pandas.DataFrame
        Prepared point data.
    grid : GridDefinition
        Grid definition.
    value_col : str, default 'value'
        Value column name from ``data``.
    kind : {'linear', 'cubic'}, default 'linear'
        Triangulation interpolation mode passed to ``scipy.interpolate.griddata``.

    Returns
    -------
    InterpolationResult
        Result with triangulation-based interpolated surface.

    Raises
    ------
    ValueError
        If ``kind`` is unsupported.

    Notes
    -----
    Grid nodes outside the convex hull of input points are returned as NaN.
    """
    kind_normalized = kind.strip().lower()
    if kind_normalized not in {"linear", "cubic"}:
        raise ValueError("kind must be either 'linear' or 'cubic'")

    points, values = _prepare_interpolation_inputs(
        data=data, grid=grid, value_col=value_col, min_points=3
    )

    z_flat = griddata(
        points=points,
        values=values,
        xi=grid.grid_points,
        method=kind_normalized,
        fill_value=np.nan,
    )
    Z = z_flat.reshape(grid.ny, grid.nx)

    return _build_result(
        grid=grid,
        Z=Z,
        method="triangulation",
        value_col=value_col,
        parameters={"kind": kind_normalized},
    )

Visualization and Map Composition

minexpy.mapping.viz

Final map composition utilities for geochemical mapping workflows.

This module implements Step 4 of MinexPy's mapping workflow by composing preparation, gridding, interpolation, and cartographic layout into one map.

Examples:

Generate a map in one call:

>>> import pandas as pd
>>> from minexpy.mapping import plot_map
>>> df = pd.DataFrame(
...     {
...         "x": [0, 20, 40, 0, 40],
...         "y": [0, 0, 0, 40, 40],
...         "Zn": [10.0, 15.0, 23.0, 18.0, 30.0],
...     }
... )
>>> fig, ax = plot_map(
...     data=df,
...     x_col="x",
...     y_col="y",
...     value_col="Zn",
...     cell_size=5.0,
...     title_parts={"what": "Zn (ppm)", "where": "Area X", "when": "2026"},
... )

plot_map(data=None, prepared=None, prepare_metadata=None, grid=None, interpolation_result=None, x_col='x', y_col='y', value_col='value', source_crs='EPSG:4326', target_crs='EPSG:4326', coordinate_transform=None, value_transform=None, drop_duplicate_coordinates=True, cell_size=None, padding_ratio=0.05, method='idw', interpolation_kwargs=None, title=None, title_parts=None, cmap='viridis', show_contours=False, contour_levels=10, show_points=True, point_size=12.0, point_alpha=0.7, point_color='black', show_north_arrow=True, show_scale_bar=True, show_numeric_scale=True, show_coordinate_grid=True, show_neatline=True, locator_config=None, crs_info=None, footer=None, figsize=(10.0, 8.0), ax=None)

Compose a final geochemical interpolation map with cartographic elements.

This function can run the full mapping pipeline (prepare -> create_grid -> interpolate) or consume precomputed outputs from any stage.

Parameters:

Name Type Description Default
data DataFrame

Raw input table for full-pipeline mode.

None
prepared DataFrame

Preprocessed table from Step 1.

None
prepare_metadata GeochemPrepareMetadata

Step 1 metadata, used for display inversion and CRS annotations.

None
grid GridDefinition

Precomputed grid from Step 2.

None
interpolation_result InterpolationResult

Precomputed interpolation from Step 3.

None
x_col str

Coordinate/value column names used in raw or prepared modes.

'x'
y_col str

Coordinate/value column names used in raw or prepared modes.

'x'
value_col str

Coordinate/value column names used in raw or prepared modes.

'x'
source_crs str

CRS arguments for raw prepare calls when needed.

'EPSG:4326'
target_crs str

CRS arguments for raw prepare calls when needed.

'EPSG:4326'
coordinate_transform callable

Optional coordinate transform hook for raw mode.

None
value_transform (None, log10, callable)

Optional value transform for raw mode.

None
drop_duplicate_coordinates bool

Duplicate handling rule for raw mode.

True
cell_size float

Grid spacing for modes requiring grid construction.

None
padding_ratio float

Grid padding ratio for modes requiring grid construction.

0.05
method str

Interpolation method for modes requiring interpolation construction.

'idw'
interpolation_kwargs dict

Additional keyword arguments for interpolation.

None
title str

Explicit map title.

None
title_parts dict

Structured title parts with optional keys what, where, when.

None
cmap str

Colormap name for interpolated surface.

'viridis'
show_contours bool

If True, overlay contours.

False
contour_levels int

Number of contour levels when contours are enabled.

10
show_points bool

If True, overlay sample points when available.

True
point_size float

Point marker size.

12.0
point_alpha float

Point transparency.

0.7
point_color str

Point color.

'black'
show_north_arrow bool

Draw north arrow.

True
show_scale_bar bool

Draw scale bar.

True
show_numeric_scale bool

Draw numeric scale annotation (1:n) when metric units are available.

True
show_coordinate_grid bool

Draw coordinate grid lines.

True
show_neatline bool

Draw map frame (neatline).

True
locator_config dict

Locator inset configuration with keys: enabled, position, extent, show_main_bbox, frame_label.

None
crs_info dict

CRS metadata dictionary for annotation block.

None
footer str

Free-text map credits/footer content.

None
figsize tuple of float

Figure size when creating a new figure.

(10.0, 8.0)
ax Axes

Existing axes to draw on.

None

Returns:

Type Description
(Figure, Axes)

Final map figure and primary axes.

Examples:

>>> import pandas as pd
>>> from minexpy.mapping import plot_map
>>> df = pd.DataFrame(
...     {"x": [0, 20, 40], "y": [0, 20, 40], "Zn": [12.0, 17.0, 24.0]}
... )
>>> fig, ax = plot_map(df, x_col="x", y_col="y", value_col="Zn", cell_size=5.0)
Notes

Mixed-mode input is allowed. When both upstream and downstream stage objects are provided, downstream precomputed objects take precedence and ignored upstream inputs emit warnings.

Source code in minexpy/mapping/viz.py
def plot_map(
    data: Optional[pd.DataFrame] = None,
    prepared: Optional[pd.DataFrame] = None,
    prepare_metadata: Optional[GeochemPrepareMetadata] = None,
    grid: Optional[GridDefinition] = None,
    interpolation_result: Optional[InterpolationResult] = None,
    x_col: str = "x",
    y_col: str = "y",
    value_col: str = "value",
    source_crs: str = "EPSG:4326",
    target_crs: str = "EPSG:4326",
    coordinate_transform: Optional[object] = None,
    value_transform: Optional[object] = None,
    drop_duplicate_coordinates: bool = True,
    cell_size: Optional[float] = None,
    padding_ratio: float = 0.05,
    method: str = "idw",
    interpolation_kwargs: Optional[Dict[str, object]] = None,
    title: Optional[str] = None,
    title_parts: Optional[Dict[str, str]] = None,
    cmap: str = "viridis",
    show_contours: bool = False,
    contour_levels: int = 10,
    show_points: bool = True,
    point_size: float = 12.0,
    point_alpha: float = 0.7,
    point_color: str = "black",
    show_north_arrow: bool = True,
    show_scale_bar: bool = True,
    show_numeric_scale: bool = True,
    show_coordinate_grid: bool = True,
    show_neatline: bool = True,
    locator_config: Optional[Dict[str, object]] = None,
    crs_info: Optional[Dict[str, str]] = None,
    footer: Optional[str] = None,
    figsize: Tuple[float, float] = (10.0, 8.0),
    ax: Optional[Axes] = None,
) -> Tuple[Figure, Axes]:
    """
    Compose a final geochemical interpolation map with cartographic elements.

    This function can run the full mapping pipeline (`prepare` -> `create_grid`
    -> `interpolate`) or consume precomputed outputs from any stage.

    Parameters
    ----------
    data : pandas.DataFrame, optional
        Raw input table for full-pipeline mode.
    prepared : pandas.DataFrame, optional
        Preprocessed table from Step 1.
    prepare_metadata : GeochemPrepareMetadata, optional
        Step 1 metadata, used for display inversion and CRS annotations.
    grid : GridDefinition, optional
        Precomputed grid from Step 2.
    interpolation_result : InterpolationResult, optional
        Precomputed interpolation from Step 3.
    x_col, y_col, value_col : str
        Coordinate/value column names used in raw or prepared modes.
    source_crs, target_crs : str
        CRS arguments for raw `prepare` calls when needed.
    coordinate_transform : callable, optional
        Optional coordinate transform hook for raw mode.
    value_transform : {None, 'log10', callable}, optional
        Optional value transform for raw mode.
    drop_duplicate_coordinates : bool, default True
        Duplicate handling rule for raw mode.
    cell_size : float, optional
        Grid spacing for modes requiring grid construction.
    padding_ratio : float, default 0.05
        Grid padding ratio for modes requiring grid construction.
    method : str, default 'idw'
        Interpolation method for modes requiring interpolation construction.
    interpolation_kwargs : dict, optional
        Additional keyword arguments for interpolation.
    title : str, optional
        Explicit map title.
    title_parts : dict, optional
        Structured title parts with optional keys `what`, `where`, `when`.
    cmap : str, default 'viridis'
        Colormap name for interpolated surface.
    show_contours : bool, default False
        If True, overlay contours.
    contour_levels : int, default 10
        Number of contour levels when contours are enabled.
    show_points : bool, default True
        If True, overlay sample points when available.
    point_size : float, default 12.0
        Point marker size.
    point_alpha : float, default 0.7
        Point transparency.
    point_color : str, default 'black'
        Point color.
    show_north_arrow : bool, default True
        Draw north arrow.
    show_scale_bar : bool, default True
        Draw scale bar.
    show_numeric_scale : bool, default True
        Draw numeric scale annotation (1:n) when metric units are available.
    show_coordinate_grid : bool, default True
        Draw coordinate grid lines.
    show_neatline : bool, default True
        Draw map frame (neatline).
    locator_config : dict, optional
        Locator inset configuration with keys:
        `enabled`, `position`, `extent`, `show_main_bbox`, `frame_label`.
    crs_info : dict, optional
        CRS metadata dictionary for annotation block.
    footer : str, optional
        Free-text map credits/footer content.
    figsize : tuple of float, default (10.0, 8.0)
        Figure size when creating a new figure.
    ax : matplotlib.axes.Axes, optional
        Existing axes to draw on.

    Returns
    -------
    matplotlib.figure.Figure, matplotlib.axes.Axes
        Final map figure and primary axes.

    Examples
    --------
    >>> import pandas as pd
    >>> from minexpy.mapping import plot_map
    >>> df = pd.DataFrame(
    ...     {"x": [0, 20, 40], "y": [0, 20, 40], "Zn": [12.0, 17.0, 24.0]}
    ... )
    >>> fig, ax = plot_map(df, x_col="x", y_col="y", value_col="Zn", cell_size=5.0)

    Notes
    -----
    Mixed-mode input is allowed. When both upstream and downstream stage
    objects are provided, downstream precomputed objects take precedence and
    ignored upstream inputs emit warnings.
    """
    prepared_used, metadata_used, grid_used, interpolation_used = _run_or_resolve_pipeline(
        data=data,
        prepared=prepared,
        prepare_metadata=prepare_metadata,
        grid=grid,
        interpolation_result=interpolation_result,
        x_col=x_col,
        y_col=y_col,
        value_col=value_col,
        source_crs=source_crs,
        target_crs=target_crs,
        coordinate_transform=coordinate_transform,
        value_transform=value_transform,
        drop_duplicate_coordinates=drop_duplicate_coordinates,
        cell_size=cell_size,
        padding_ratio=padding_ratio,
        method=method,
        interpolation_kwargs=interpolation_kwargs,
    )

    Z_display = _resolve_display_surface(
        interpolation_result=interpolation_used,
        prepare_metadata=metadata_used,
    )

    if ax is None:
        fig, map_ax = plt.subplots(figsize=figsize)
        fig.subplots_adjust(right=0.78)
    else:
        map_ax = ax
        fig = map_ax.figure

    colorbar_label = (
        title_parts.get("what", interpolation_used.value_col)
        if title_parts is not None
        else interpolation_used.value_col
    )
    colorbar = _draw_surface_and_colorbar(
        ax=map_ax,
        grid=grid_used,
        Z_display=Z_display,
        cmap=cmap,
        show_contours=show_contours,
        contour_levels=contour_levels,
        colorbar_label=colorbar_label,
    )

    if show_points:
        if prepared_used is None:
            _warn_ignored(
                "Sample point overlay was requested but prepared data was unavailable; points were skipped."
            )
        elif grid_used.x_col not in prepared_used.columns or grid_used.y_col not in prepared_used.columns:
            _warn_ignored(
                "Sample point overlay was requested but coordinate columns were unavailable; points were skipped."
            )
        else:
            map_ax.scatter(
                prepared_used[grid_used.x_col].to_numpy(dtype=float),
                prepared_used[grid_used.y_col].to_numpy(dtype=float),
                s=point_size,
                alpha=point_alpha,
                c=point_color,
                edgecolors="white",
                linewidths=0.3,
                zorder=4,
            )

    map_ax.set_title(_compose_title(title=title, title_parts=title_parts))
    map_ax.set_xlabel(grid_used.x_col)
    map_ax.set_ylabel(grid_used.y_col)
    map_ax.set_aspect("equal", adjustable="box")

    if show_north_arrow:
        _draw_north_arrow(map_ax)

    units, info_lines = _collect_crs_info_lines(
        crs_info=crs_info,
        prepare_metadata=metadata_used,
    )

    if show_scale_bar:
        _draw_scale_bar(
            ax=map_ax,
            grid=grid_used,
            units=units,
        )
    numeric_scale = _compute_numeric_scale_text(
        ax=map_ax,
        grid=grid_used,
        units=units,
        show_numeric_scale=show_numeric_scale,
    )
    if numeric_scale is not None:
        info_lines.insert(0, f"Scale: {numeric_scale}")

    _draw_external_info_panel(fig=fig, colorbar=colorbar, info_lines=info_lines)

    _apply_coordinate_grid_and_neatline(
        ax=map_ax,
        show_coordinate_grid=show_coordinate_grid,
        show_neatline=show_neatline,
    )

    _draw_locator_inset(fig=fig, grid=grid_used, locator_config=locator_config)
    _draw_footer(fig=fig, footer=footer)

    return fig, map_ax

viz(*args, **kwargs)

Alias for :func:plot_map.

Returns:

Type Description
(Figure, Axes)

Final map figure and primary axes.

Source code in minexpy/mapping/viz.py
def viz(*args: object, **kwargs: object) -> Tuple[Figure, Axes]:
    """
    Alias for :func:`plot_map`.

    Returns
    -------
    matplotlib.figure.Figure, matplotlib.axes.Axes
        Final map figure and primary axes.
    """
    return plot_map(*args, **kwargs)