#!/usr/bin/env python3
# File: ./src/scitex/bridge/_stats_plt.py
# Time-stamp: "2024-12-09 10:00:00 (ywatanabe)"
"""
Bridge module for stats ↔ plt integration.
Provides adapters to:
- Add statistical results as annotations on matplotlib axes
- Extract stats from axes with tracked annotations
- Format statistical text for plot display
Coordinate Convention
---------------------
This module uses **axes coordinates** (0-1 normalized) for positioning
when auto-positioning is used (no explicit x, y given). This matches
matplotlib's typical annotation workflow where positions are relative
to the axes bounding box.
- (0, 0) = bottom-left of axes
- (1, 1) = top-right of axes
- Default auto-position: (0.5, 0.95) = top-center
When explicit x, y are provided, they use whatever coordinate system
the caller intends (typically data coordinates unless transform is set).
"""
from typing import Any, List, Optional
# Import GUI classes from FTS (single source of truth)
# StatResult is now a dict - the GUI-specific StatResult is deprecated
StatResult = dict
[docs]
def add_stat_to_axes(
ax,
stat_result: StatResult,
x: Optional[float] = None,
y: Optional[float] = None,
format_style: str = "asterisk",
**kwargs,
) -> Any:
"""
Add a statistical result annotation to a matplotlib axes.
Parameters
----------
ax : matplotlib.axes.Axes or scitex.plt AxisWrapper
The axes to annotate
stat_result : StatResult
The statistical result to display
x : float, optional
X position for the annotation. If None, uses stat_result.positioning
y : float, optional
Y position for the annotation. If None, uses stat_result.positioning
format_style : str
Format style for the text ("asterisk", "compact", "detailed", "publication")
**kwargs
Additional kwargs passed to ax.annotate() or ax.text()
Returns
-------
matplotlib.text.Text or matplotlib.text.Annotation
The created annotation object
"""
# Get formatted text
text = format_stat_for_plot(stat_result, format_style)
# Determine position
# Check if using StatResult positioning
use_stat_positioning = False
if x is None or y is None:
positioning = stat_result.positioning
if positioning and positioning.position:
pos = positioning.position
if x is None:
x = pos.x
if y is None:
y = pos.y
use_stat_positioning = True
# Auto-position: top center of axes in axes coordinates
if x is None:
x = 0.5
if y is None:
y = 0.95
# Default to axes coordinates (0-1) unless user explicitly sets transform
# This makes positioning intuitive: (0.5, 0.9) = top center of plot
if "transform" not in kwargs:
kwargs["transform"] = _get_axes_transform(ax)
kwargs.setdefault("ha", "center")
kwargs.setdefault("va", "top")
# Apply styling from StatResult if available
styling = stat_result.styling
if styling:
kwargs.setdefault("fontsize", styling.font_size_pt)
kwargs.setdefault("fontfamily", styling.font_family)
kwargs.setdefault("color", styling.color)
# Get the actual matplotlib axes
mpl_ax = _get_mpl_axes(ax)
# Create the annotation
annotation = mpl_ax.text(x, y, text, **kwargs)
# Store reference to stat_result on the annotation for later extraction
annotation._scitex_stat_result = stat_result
return annotation
def _get_mpl_axes(ax):
"""Get the underlying matplotlib axes from wrapper or native."""
# Handle scitex AxisWrapper
if hasattr(ax, "_axes_mpl"):
return ax._axes_mpl
# Handle scitex AxesWrapper (multiple axes)
if hasattr(ax, "_axes_scitex"):
axes = ax._axes_scitex
if hasattr(axes, "flat"):
return axes.flat[0]
return axes
# Already matplotlib axes
return ax
def _get_axes_transform(ax):
"""Get the axes transform for positioning."""
mpl_ax = _get_mpl_axes(ax)
return mpl_ax.transAxes
def _parse_stat_annotation(text: str) -> Optional[StatResult]:
"""
Try to parse a text annotation as a statistical result.
Parameters
----------
text : str
Text content to parse
Returns
-------
Optional[StatResult]
Parsed StatResult or None if not parseable
"""
text = text.strip()
def _create_stat_dict(test_type, statistic_name, statistic_value, p_value):
"""Create a simple stat result dict."""
from scitex_stats._utils import p2stars
return {
"test_type": test_type,
"test_category": "other",
"statistic": {"name": statistic_name, "value": statistic_value},
"p_value": p_value,
"stars": p2stars(p_value, ns_symbol=False),
}
# Try to detect asterisks pattern
if text in ["*", "**", "***", "ns", "n.s."]:
stars = text.replace("n.s.", "ns")
# Can't determine actual stats, create placeholder
p_value = {
"***": 0.0001,
"**": 0.005,
"*": 0.03,
"ns": 0.5,
}.get(stars, 0.5)
return _create_stat_dict(
test_type="unknown",
statistic_name="stat",
statistic_value=0.0,
p_value=p_value,
)
# Try to parse patterns like "r = 0.85***" or "(t = 2.5, p < 0.01)"
import re
# Pattern: statistic = value[stars]
match = re.match(r"([a-zA-Z]+)\s*=\s*([\d.-]+)(\*+|ns)?", text)
if match:
stat_name = match.group(1)
stat_value = float(match.group(2))
stars = match.group(3) or "ns"
p_value = {
"***": 0.0001,
"**": 0.005,
"*": 0.03,
"ns": 0.5,
}.get(stars, 0.5)
return _create_stat_dict(
test_type="unknown",
statistic_name=stat_name,
statistic_value=stat_value,
p_value=p_value,
)
return None
__all__ = [
"add_stat_to_axes",
"extract_stats_from_axes",
"format_stat_for_plot",
]
# EOF