"""
封装一些常用的绘图，方便直接函数式地调用

### 设计哲学

本模块中的函数专注于将数据绘制到坐标轴上，不包含任何主动的样式设置
(如设置标题、坐标轴标签、网格等)。样式设置应由用户在函数调用之后手动完成。

### 函数通用定义规范

```py
def plot_xxx(..., ax=None, show=True, save_path=None, auto_close=None, **kwargs):
    ...
    return fig, ax
```

- 如果给定了 ax，则在其上绘制；否则创建新图表。
- 返回 fig, ax 对象，方便后续操作。
- `show`: 是否调用 plt.show()。
- `save_path`: 保存路径，如果提供则保存图表。
- `auto_close`: 智能关闭图表以释放内存。
- `**kwargs` 用于传递给底层的绘图函数 (如 ax.plot, ax.scatter, sns.heatmap)。
- 尽量和样式配置分离，不参与样式的问题
"""

import typing

from typing import Callable, Dict, List, Literal, Optional, Tuple, Union

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from typing_extensions import NotRequired, Required

from . import _plot_utils
from ._plot_utils import FigAx, FigureLike, _finalize_figure, _prepare_ax
from .mpl_utils import draw_comparison_lines, draw_error_regions
from .mpl_utils.draw import ComparisonDict


class BarComparisonDict(typing.TypedDict, total=False):
    x1: Required[tuple[int, int]]
    x2: Required[tuple[int, int]]
    text: NotRequired[str]
    p_value: NotRequired[float]


def _normalize_labels(
    labels: typing.Sequence[str] | str | None, count: int
) -> list[str | None]:
    if labels is None:
        return [None] * count
    if isinstance(labels, str):
        return [labels] * count
    if len(labels) != count:
        raise ValueError('`labels` 的数量与数据系列数量不一致')
    return list(labels)


def _has_visible_labels(labels: typing.Sequence[str | None]) -> bool:
    return any(label is not None for label in labels)


def plot_line(
    x: np.ndarray | None = None,
    y: np.ndarray | None = None,  # 为了习惯, 把 x 放在前面, 但实际上 y 不可省略
    labels: Optional[typing.Sequence[str]] = None,
    yerr: Optional[np.ndarray] = None,
    error_style: Literal['band', 'bar'] = 'band',
    lines_plot_kwargs: typing.Mapping | typing.Sequence[typing.Mapping] | None = None,
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    figsize: Optional[Tuple[float, float]] = None,
    **plot_kwargs,
) -> FigAx:
    """绘制折线图，支持多条线和误差显示。

    此函数可以绘制单条或多条折线，并支持误差带或误差棒的显示。

    Parameters
    ----------
    - `x` : `np.ndarray | None`, optional
        - X轴数据。如果为None，则使用索引作为X轴数据, by default None
    - `y` : `np.ndarray | None`, optional
        - Y轴数据。必须提供。1D数组绘制单条线，2D数组绘制多条线（每行一条线）, by default None
    - `labels` : `Optional[List[str]]`, optional
        - 每条线的标签，用于图例显示。数量必须与线条数相同, by default None
    - `yerr` : `Optional[np.ndarray]`, optional
        - Y轴误差数据。形状必须与y数据相同, by default None
    - `error_style` : `Literal['band', 'bar']`, optional
        - 误差显示方式。'band'显示误差带，'bar'显示误差棒, by default 'band'
    - `lines_plot_kwargs` : `Dict | List[Dict] | None`, optional
        - 传递给 `ax.plot` 的额外参数
        - 通常不需要指定这个参数; 对于共享的样式, 可以直接用 **plot_kwargs
        - 不过如果希望每条线有不同的样式，可以传递一个字典列表；长度需要和 y 一致
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `xlabel` : `Optional[str]`, optional
        - X轴标签, by default None
    - `ylabel` : `Optional[str]`, optional
        - Y轴标签, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 绘图轴对象。如果为None则创建新图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None
    - `figsize` : `Optional[Tuple[float, float]]`, optional
        - 图表大小, by default None
    Returns
    ----------
    - `FigAx`
        - 图表对象和轴对象的元组

    Raises
    ----------
    - `ValueError`
        - 当y参数为空时抛出
    - `ValueError`
        - 当labels数量与线条数不匹配时抛出
    - `ValueError`
        - 当yerr形状与y数据形状不匹配时抛出

    Examples
    --------
    **简单折线图（单条线）**：
    ```python
    import numpy as np
    x = np.linspace(0, 10, 50)
    y = np.sin(x)
    fig, ax = plot_line(x, y)
    ax.set_title('Simple Line Plot')
    ax.set_xlabel('X values')
    ax.set_ylabel('Y values')
    ```

    **多条折线图**：
    ```python
    x = np.linspace(0, 10, 50)
    y = np.array([np.sin(x), np.cos(x), np.sin(x + np.pi/4)])
    fig, ax = plot_line(x, y, labels=['sin(x)', 'cos(x)', 'sin(x+π/4)'])
    ax.set_title('Multiple Lines')
    ax.legend()
    ```

    **带误差带的折线图**：
    ```python
    x = np.linspace(0, 10, 50)
    y = np.sin(x) + np.random.randn(3, 50) * 0.1
    yerr = np.random.uniform(0.05, 0.15, size=y.shape)
    fig, ax = plot_line(x, y, yerr=yerr,
                        labels=['Trial 1', 'Trial 2', 'Trial 3'],
                        error_style='band')
    ax.set_title('Lines with Error Bands')
    ```

    **带误差棒的折线图**：
    ```python
    fig, ax = plot_line(x, y, yerr=yerr,
                        labels=['Trial 1', 'Trial 2', 'Trial 3'],
                        error_style='bar')
    ax.set_title('Lines with Error Bars')
    ```

    **自定义样式**：
    ```python
    fig, ax = plot_line(x, y, linewidth=2.5, linestyle='--',
                        marker='o', markersize=4, alpha=0.8)
    ax.set_title('Styled Line Plot')
    ```

    **数据形状说明**：

    - `y`为1D数组：绘制单条线
    - `y`为2D数组：每行代表一条线
    - `yerr`形状必须与`y`相同
    - 如果`x`为None，则使用索引[0, 1, 2, ...]作为X轴数据
    """
    fig, ax = _prepare_ax(ax, figsize=figsize)

    if y is None:
        raise ValueError('`y` 参数不能为空')

    y = np.asarray(y)
    if y.ndim == 1:
        y = y.reshape(1, -1)

    n_lines = y.shape[0]
    if x is None:
        x = np.arange(y.shape[1])

    label_list = _normalize_labels(labels, n_lines)

    if yerr is not None:
        yerr = np.asarray(yerr)
        if yerr.ndim == 1:
            yerr = yerr.reshape(1, -1)
        if yerr.shape != y.shape:
            raise ValueError('`yerr` 的形状必须与 `y` 的形状相同')

    for i in range(n_lines):
        current_y = y[i, :]
        current_label = label_list[i]

        # 1. 先绘制主线。ax.plot 会自动处理颜色循环或用户传入的 color kwarg
        kwargs = {}
        if lines_plot_kwargs is not None:
            if isinstance(lines_plot_kwargs, list):
                kwargs = lines_plot_kwargs[i]
            else:
                kwargs = lines_plot_kwargs
        kwargs = {**typing.cast(dict[str, typing.Any], kwargs), **plot_kwargs}
        line = ax.plot(x, current_y, label=current_label, **kwargs)

        # 2. 从返回的 Line2D 对象中获取实际使用的颜色
        actual_color = line[0].get_color()

        # 3. 如果有误差数据，使用获取到的颜色绘制误差区域/棒
        if yerr is not None:
            current_yerr = yerr[i, :]
            if error_style == 'band':
                draw_error_regions(
                    ax,
                    x,
                    current_y,
                    current_yerr,
                    color=typing.cast(str, actual_color),
                    alpha=0.2,
                )
            elif error_style == 'bar':
                # fmt='none' 确保不绘制额外的点或线
                ax.errorbar(
                    x,
                    current_y,
                    yerr=current_yerr,
                    fmt='none',
                    ecolor=actual_color,
                    alpha=0.5,
                )

    if _has_visible_labels(label_list):
        ax.legend()

    # 设置标题和轴标签
    if title is not None:
        ax.set_title(title)
    if xlabel is not None:
        ax.set_xlabel(xlabel)
    if ylabel is not None:
        ax.set_ylabel(ylabel)

    _finalize_figure(fig, show, save_path, auto_close)
    return fig, ax


def plot_dual_line(
    x: np.ndarray | None = None,
    y1: np.ndarray | None = None,
    y2: np.ndarray | None = None,
    label1: Optional[str] = None,
    label2: Optional[str] = None,
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel1: Optional[str] = None,
    ylabel2: Optional[str] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    plot1_kwargs: Optional[typing.Mapping] = None,
    plot2_kwargs: Optional[typing.Mapping] = None,
    figsize: Optional[Tuple[float, float]] = None,
) -> tuple[FigureLike, tuple[Axes, Axes]]:
    """绘制双Y轴折线图，专用于在同一图表上比较两个不同量纲的序列。

    此函数创建一个共享X轴但具有独立Y轴的图表，非常适合绘制训练/验证曲线
    （例如，损失与准确率）。

    ### 设计说明
    - 左侧Y轴 (`ax1`) 用于绘制 `y1` 数据。
    - 右侧Y轴 (`ax2`) 用于绘制 `y2` 数据。
    - 函数会自动处理图例的合并。
    - 为了提高可读性，默认会将Y轴刻度线的颜色与对应线条的颜色匹配。
      用户可以在返回的 `ax1` 和 `ax2` 对象上进一步自定义样式。

    Parameters
    ----------
    - `x` : `np.ndarray | None`, optional
        - 共享的X轴数据。如果为None，则使用索引作为X轴数据, by default None
    - `y1` : `np.ndarray`, optional
        - 左侧Y轴的数据, by default None
    - `y2` : `np.ndarray`, optional
        - 右侧Y轴的数据, by default None
    - `label1` : `Optional[str]`, optional
        - `y1` 数据的标签，用于图例, by default None
    - `label2` : `Optional[str]`, optional
        - `y2` 数据的标签，用于图例, by default None
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `xlabel` : `Optional[str]`, optional
        - X轴标签, by default None
    - `ylabel1` : `Optional[str]`, optional
        - 左侧Y轴标签, by default None
    - `ylabel2` : `Optional[str]`, optional
        - 右侧Y轴标签, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 主绘图轴对象（将作为左侧Y轴）。如果为None则创建新图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None
    - `plot1_kwargs` : `Optional[Dict]`, optional
        - 传递给 `y1` 绘图函数 (`ax1.plot`) 的额外参数, by default None
    - `plot2_kwargs` : `Optional[Dict]`, optional
        - 传递给 `y2` 绘图函数 (`ax2.plot`) 的额外参数, by default None
    - `figsize` : `Optional[Tuple[float, float]]`, optional
        - 图表大小, by default None
    Returns
    ----------
    - `Tuple[plt.Figure, Tuple[plt.Axes, plt.Axes]]`
        - 图表对象以及一个包含左、右两个轴对象的元组 (`fig`, (`ax1`, `ax2`))

    Raises
    ----------
    - `ValueError`
        - 当 `y1` 或 `y2` 参数为空时抛出

    Examples
    --------
    ```python
     plot_dual_line(
        x=epochs,
        y1=train_loss,
        y2=val_acc,
        label1='Loss',
        label2='Accuracy',
        plot1_kwargs={'linestyle': '--', 'marker': 'o', 'markersize': 3},
        plot2_kwargs={'linestyle': ':', 'linewidth': 2.5}
    )
    ```
    """
    if y1 is None or y2 is None:
        raise ValueError('`y1` 和 `y2` 参数不能为空')

    # 使用 _prepare_ax 创建主轴 ax1
    fig, ax1 = _prepare_ax(ax, figsize=figsize)

    # 创建共享 x 轴的次轴 ax2
    ax2 = ax1.twinx()

    # 准备 x 轴数据
    if x is None:
        x = np.arange(y1.shape[0])

    # 准备绘图参数
    p1_kwargs = dict(plot1_kwargs or {})
    p2_kwargs = dict(plot2_kwargs or {})

    # 设置默认颜色，避免两个图都用 'C0'
    p1_kwargs.setdefault('color', 'C0')
    p2_kwargs.setdefault('color', 'C1')

    # 绘制第一条线
    line1 = ax1.plot(x, y1, label=label1, **p1_kwargs)

    # 绘制第二条线
    line2 = ax2.plot(x, y2, label=label2, **p2_kwargs)

    # 为了可读性，设置Y轴刻度颜色与线条颜色一致
    # 这是轻微的样式干预，但对于双轴图至关重要
    ax1.tick_params(axis='y', colors=line1[0].get_color())
    ax2.tick_params(axis='y', colors=line2[0].get_color())

    # 合并图例
    lines = line1 + line2
    labels = [str(line.get_label()) for line in lines]
    if any(labels):
        # loc='best' 有时会选择不好的位置，尤其是在双轴图中
        # 将图例放在图表外部或指定位置通常更好，但这里保持简单
        ax1.legend(lines, labels, loc='best')

    # 设置标题和轴标签
    if title is not None:
        ax1.set_title(title)
    if xlabel is not None:
        ax1.set_xlabel(xlabel)
    if ylabel1 is not None:
        ax1.set_ylabel(ylabel1)
    if ylabel2 is not None:
        ax2.set_ylabel(ylabel2)

    _finalize_figure(fig, show, save_path, auto_close)

    return fig, (ax1, ax2)


def plot_bar(
    data: np.ndarray,
    yerr: Optional[np.ndarray] = None,
    orientation: Literal['vertical', 'horizontal'] = 'vertical',
    group_labels: Optional[typing.Sequence[str]] = None,
    bar_labels: Optional[typing.Sequence[str]] = None,
    comparisons: Optional[typing.Sequence[BarComparisonDict]] = None,
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    mark_value: bool = False,
    value_format: str = '.3f',
    value_offset: Optional[float] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    figsize: Optional[Tuple[float, float]] = None,
    **bar_kwargs,
) -> FigAx:
    """绘制条形图，支持简单条形图和分组条形图。

    此函数可以绘制单组条形图（1D数据）或分组条形图（2D数据），并支持误差棒和统计比较线。

    根据 orientation 的不同，分别绘制垂直或水平条形图。
    - ax.bar：绘制垂直条形图
    - ax.barh：绘制水平条形图

    可以使用 bar_kwargs 传递额外的参数给条形图绘制函数。

    Parameters
    ----------
    - `data` : `np.ndarray`
        - 条形图数据。1D数组绘制简单条形图，2D数组绘制分组条形图。
        - 对于2D数组，形状为(n_groups, n_bars_per_group)
    - `yerr` : `Optional[np.ndarray]`, optional
        - 误差数据。形状必须与data相同, by default None
    - `orientation` : `Literal['vertical', 'horizontal']`, optional
        - 条形图方向。'vertical'为垂直，'horizontal'为水平, by default 'vertical'
    - `group_labels` : `Optional[List[str]]`, optional
        - 分组标签。对于1D数据，这是每个条形的标签；对于2D数据，这是每个组的标签, by default None
    - `bar_labels` : `Optional[List[str]]`, optional
        - 条形标签。仅用于2D数据，表示每组内不同条形的标签（用于图例）, by default None
    - `comparisons` : `Optional[List[ComparisonDict]]`, optional
        - 统计比较线配置。格式为[{'x1': (group_idx, bar_idx), 'x2': (group_idx, bar_idx), ...}], by default None
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `xlabel` : `Optional[str]`, optional
        - X轴标签, by default None
    - `ylabel` : `Optional[str]`, optional
        - Y轴标签, by default None
    - `mark_value` : `bool`, optional
        - 是否在条形顶部标注数值, by default False
    - `value_format` : `str`, optional
        - 数值格式化字符串，遵循Python格式化语法, by default '.3f'
    - `value_offset` : `Optional[float]`, optional
        - 文本标注的偏移量。如果为None则自动计算, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 绘图轴对象。如果为None则创建新图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None

    Returns
    ----------
    - `FigAx`
        - 图表对象和轴对象的元组

    Raises
    ----------
    - `ValueError`
        - 当data维度不是1D或2D时抛出
    - `ValueError`
        - 当group_labels数量与数据不匹配时抛出
    - `ValueError`
        - 当bar_labels数量与每组条形数不匹配时抛出

    Examples
    --------
    **简单条形图（1D数据）**：
    ```python
    import numpy as np
    data = np.array([10, 15, 12, 8])
    fig, ax = plot_bar(data, group_labels=['A', 'B', 'C', 'D'])
    ax.set_title('Simple Bar Chart')
    ax.set_ylabel('Values')
    ```

    **分组条形图（2D数据）**：
    ```python
    # 3组，每组2个条形
    data = np.array([[10, 12], [15, 20], [12, 8]])
    fig, ax = plot_bar(data,
                       group_labels=['Group1', 'Group2', 'Group3'],
                       bar_labels=['Control', 'Treatment'])
    ax.set_title('Grouped Bar Chart')
    ax.set_ylabel('Score')
    ```

    **带误差棒的条形图**：
    ```python
    data = np.array([[10, 12], [15, 20], [12, 8]])
    errors = np.array([[1, 1.2], [1.5, 2], [1.1, 0.8]])
    fig, ax = plot_bar(data, yerr=errors,
                       group_labels=['Pre', 'Post', 'Follow'],
                       bar_labels=['Control', 'Treatment'])
    ```

    **水平条形图**：
    ```python
    fig, ax = plot_bar(data, orientation='horizontal',
                       group_labels=['Group A', 'Group B', 'Group C'],
                       bar_labels=['Baseline', 'Intervention'])
    ax.set_xlabel('Values')
    ```

    **带数值标注的条形图**：
    ```python
    # 简单条形图带数值标注
    data = np.array([10.234, 15.678, 12.456, 8.123])
    fig, ax = plot_bar(data, mark_value=True, value_format='.2f',
                       group_labels=['A', 'B', 'C', 'D'])

    # 分组条形图带数值标注和误差棒
    data = np.array([[10.5, 12.3], [15.8, 20.1], [12.2, 8.7]])
    errors = np.array([[1.0, 1.2], [1.5, 2.0], [1.1, 0.8]])
    fig, ax = plot_bar(data, yerr=errors, mark_value=True,
                       group_labels=['Pre', 'Post', 'Follow'],
                       bar_labels=['Control', 'Treatment'])
    ```

    **带统计比较线的条形图**：
    ```python
    # 比较线配置：比较第1组的两个条形，p值为0.05
    comparisons = [
        {'x1': (0, 0), 'x2': (0, 1), 'p_value': 0.05},  # 第1组内比较
        {'x1': (1, 0), 'x2': (1, 1), 'p_value': 0.001}, # 第2组内比较
        {'x1': (0, 1), 'x2': (2, 1), 'text': 'Significant!'} # 跨组比较
    ]
    fig, ax = plot_bar(data, comparisons=comparisons,
                       group_labels=['Pre', 'Post', 'Follow'],
                       bar_labels=['Control', 'Treatment'])
    ```

    **理解group_labels和bar_labels**：

    - 对于1D数据：`group_labels`是每个条形的标签
    - 对于2D数据：`group_labels`是每个组的标签（X轴），`bar_labels`是每组内条形的标签（图例）
    - 数据形状为(n_groups, n_bars_per_group)

    **comparison参数详解**：

    - `x1`, `x2`：元组(group_idx, bar_idx)，指定要比较的两个条形
    - `p_value`：p值，会自动格式化显示（如 p<0.001, p=0.05, ns）
    - `text`：自定义文本，替代p值显示
    - `y_offset`：比较线的垂直偏移量（可选）
    """
    fig, ax = _prepare_ax(ax, figsize=figsize)
    data = np.asarray(data)

    # --- 数据校验部分 ---
    # 处理1D数据，转换为2D以统一处理
    if data.ndim == 1:
        data = data.reshape(-1, 1)
    elif data.ndim != 2:
        raise ValueError('`data` 必须是 1D 或 2D numpy 数组')

    n_groups, n_bars_per_group = data.shape

    # 对于1D数据转换的情况，group_labels实际上是每个条形的标签
    if group_labels and len(group_labels) != n_groups:
        raise ValueError(
            f'`group_labels` 数量 ({len(group_labels)}) 必须与数据组数 ({n_groups}) 相同'
        )

    # bar_labels只在多条形分组时使用
    if n_bars_per_group > 1 and bar_labels and len(bar_labels) != n_bars_per_group:
        raise ValueError(
            f'`bar_labels` 数量 ({len(bar_labels)}) 必须与每组条形数 ({n_bars_per_group}) 相同'
        )

    # 处理yerr数据
    if yerr is not None:
        yerr = np.asarray(yerr)
        if yerr.ndim == 1 and data.shape[1] == 1:
            yerr = yerr.reshape(-1, 1)
        if yerr.shape != data.shape:
            raise ValueError(
                f'`yerr` 形状 {yerr.shape} 必须与 `data` 形状 {data.shape} 相同'
            )
    # --- 数据校验结束 ---

    bar_width = 0.8 / n_bars_per_group
    group_positions = np.arange(n_groups)

    plot_func = ax.bar if orientation == 'vertical' else ax.barh
    # err_kw = 'yerr' if orientation == 'vertical' else 'xerr'

    bar_positions = {}

    # 从 kwargs 中分离出 errorbar 的特定样式参数
    error_kw = bar_kwargs.pop('error_kw', {})
    # 设置更美观的默认误差棒样式
    default_error_kw = {
        'fmt': 'none',  # 不绘制数据点
        'capsize': 4,  # 设置顶盖大小
        'elinewidth': 1.5,  # 设置误差线宽度
        'alpha': 0.9,
    }
    # 用户定义的 error_kw 可以覆盖默认值
    error_kw = {**default_error_kw, **error_kw}

    for i in range(n_bars_per_group):
        offset = (i - (n_bars_per_group - 1) / 2) * bar_width
        positions = group_positions + offset
        values = data[:, i]
        errors = yerr[:, i] if yerr is not None else None

        for grp_idx, pos in enumerate(positions):
            bar_positions[(grp_idx, i)] = pos

        bar_plot_kwargs = bar_kwargs.copy()
        if orientation == 'vertical':
            bar_plot_kwargs['width'] = bar_width
            bars = plot_func(
                positions,
                values,
                label=bar_labels[i] if bar_labels else None,
                **bar_plot_kwargs,
            )
        else:  # horizontal
            bar_plot_kwargs['height'] = bar_width
            bars = plot_func(
                positions,
                values,
                label=bar_labels[i] if bar_labels else None,
                **bar_plot_kwargs,
            )

        # --- 手动绘制误差棒 ---
        if errors is not None:
            # 获取刚刚绘制的条形的颜色
            # 如果用户指定了全局颜色，所有条形颜色一样
            bar_color = bars[0].get_facecolor()

            # 使用获取到的颜色和自定义样式来绘制误差棒
            if orientation == 'vertical':
                ax.errorbar(
                    x=positions, y=values, yerr=errors, ecolor=bar_color, **error_kw
                )
            else:  # horizontal
                ax.errorbar(
                    x=values, y=positions, xerr=errors, ecolor=bar_color, **error_kw
                )

    if orientation == 'vertical':
        ax.set_xticks(group_positions)
        if group_labels:
            ax.set_xticklabels(group_labels)
    else:
        ax.set_yticks(group_positions)
        if group_labels:
            ax.set_yticklabels(group_labels)

    if comparisons:
        processed_comparisons: list[ComparisonDict] = []
        for comp in comparisons:
            g1_idx, b1_idx = comp['x1']
            g2_idx, b2_idx = comp['x2']

            new_comp: ComparisonDict = {
                'x1': float(bar_positions[(g1_idx, b1_idx)]),
                'y1': float(data[g1_idx, b1_idx]),
                'x2': float(bar_positions[(g2_idx, b2_idx)]),
                'y2': float(data[g2_idx, b2_idx]),
            }
            if 'text' in comp:
                new_comp['text'] = comp['text']
            if 'p_value' in comp:
                new_comp['p_value'] = comp['p_value']
            processed_comparisons.append(new_comp)
        draw_comparison_lines(ax, processed_comparisons)

    # --- 添加数值标注 ---
    if mark_value:
        # 计算自动偏移量
        if value_offset is None:
            if orientation == 'vertical':
                y_range = ax.get_ylim()[1] - ax.get_ylim()[0]
                auto_offset = y_range * 0.02  # 2% of y range
            else:  # horizontal
                x_range = ax.get_xlim()[1] - ax.get_xlim()[0]
                auto_offset = x_range * 0.02  # 2% of x range
        else:
            auto_offset = value_offset

        # 遍历所有条形添加数值标注
        for group_idx in range(n_groups):
            for bar_idx in range(n_bars_per_group):
                value = data[group_idx, bar_idx]
                pos = bar_positions[(group_idx, bar_idx)]

                # 计算误差棒的偏移量
                error_offset = 0
                if yerr is not None:
                    error_offset = yerr[group_idx, bar_idx]

                if orientation == 'vertical':
                    text_x = pos
                    text_y = value + error_offset + auto_offset
                    ha, va = 'center', 'bottom'
                else:  # horizontal
                    text_x = value + error_offset + auto_offset
                    text_y = pos
                    ha, va = 'left', 'center'

                ax.text(
                    text_x,
                    text_y,
                    f'{value:{value_format}}',
                    ha=ha,
                    va=va,
                    fontweight='bold',
                    fontsize=9,
                )

    if bar_labels:
        ax.legend()

    # 设置标题和轴标签
    if title is not None:
        ax.set_title(title)
    if xlabel is not None:
        ax.set_xlabel(xlabel)
    if ylabel is not None:
        ax.set_ylabel(ylabel)

    _finalize_figure(fig, show, save_path, auto_close)
    return fig, ax


def plot_scatter(
    x: np.ndarray,
    y: np.ndarray,
    classes: Optional[np.ndarray] = None,
    size: Optional[np.ndarray] = None,
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    figsize: Optional[Tuple[float, float]] = None,
    colors: Optional[typing.Sequence[str]] = None,
    alpha: float = 0.7,
    s: Union[float, np.ndarray] = 50,
    hue: Optional[np.ndarray] = None,  # 向后兼容参数
    **scatter_kwargs,
) -> FigAx:
    """绘制散点图，支持颜色和大小映射。

    此函数基于matplotlib的scatter实现，支持按分类变量着色和按数值变量调整点大小。
    相比seaborn实现，提供更直接的控制和更好的性能。

    Parameters
    ----------
    - `x` : `np.ndarray`
        - X轴数据
    - `y` : `np.ndarray`
        - Y轴数据
    - `classes` : `Optional[np.ndarray]`, optional
        - 用于颜色映射的分类变量，每个类别将使用不同颜色, by default None
    - `size` : `Optional[np.ndarray]`, optional
        - 用于大小映射的数值变量, by default None
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `xlabel` : `Optional[str]`, optional
        - X轴标签, by default None
    - `ylabel` : `Optional[str]`, optional
        - Y轴标签, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 绘图轴对象。如果为None则创建新图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None
    - `figsize` : `Optional[Tuple[float, float]]`, optional
        - 图表大小, by default None
    - `colors` : `Optional[List[str]]`, optional
        - 自定义颜色列表，用于不同类别。如果为None则使用默认调色板, by default None
    - `alpha` : `float`, optional
        - 点的透明度, by default 0.7
    - `s` : `Union[float, np.ndarray]`, optional
        - 点的大小。可以是标量或数组, by default 50
    - `hue` : `Optional[np.ndarray]`, optional
        - **已弃用**: 用于向后兼容的参数，请使用 `classes` 替代, by default None
    Returns
    ----------
    - `FigAx`
        - 图表对象和轴对象的元组

    Examples
    --------
    **基本散点图**：
    ```python
    import numpy as np
    x = np.random.rand(100)
    y = x * 2 + np.random.randn(100) * 0.5
    fig, ax = plot_scatter(x, y)
    ax.set_title('Basic Scatter Plot')
    ax.set_xlabel('X values')
    ax.set_ylabel('Y values')
    ```

    **按分类着色的散点图**：
    ```python
    classes_data = np.random.choice(['Group A', 'Group B', 'Group C'], 100)
    fig, ax = plot_scatter(x, y, classes=classes_data)
    ax.set_title('Scatter Plot with Categorical Coloring')
    ax.legend(title='Groups')
    ```

    **按数值调整点大小**：
    ```python
    size_data = np.random.rand(100) * 100
    fig, ax = plot_scatter(x, y, size=size_data)
    ax.set_title('Scatter Plot with Size Mapping')
    ```

    **同时使用颜色和大小映射**：
    ```python
    fig, ax = plot_scatter(x, y, classes=classes_data, size=size_data, alpha=0.8)
    ax.set_title('Scatter Plot with Classes and Size')
    ```

    **自定义颜色和样式**：
    ```python
    custom_colors = ['#FF6B6B', '#4ECDC4', '#45B7D1']
    fig, ax = plot_scatter(x, y, classes=classes_data, colors=custom_colors,
                           alpha=0.6, s=80, edgecolors='black', linewidths=0.5)
    ax.set_title('Styled Scatter Plot')
    ```

    **参数说明**：

    - `classes`：分类变量，每个唯一值将分配不同颜色
    - `size`：数值变量，用于调整点的大小
    - `colors`：自定义颜色列表，按类别顺序使用
    - `alpha`：透明度，0-1之间
    - `s`：点大小，可以是固定值或数组
    - 其他参数会传递给`matplotlib.pyplot.scatter`
    """
    import warnings

    from .color_palette import PredefinedColorSchemes

    fig, ax = _prepare_ax(ax, figsize=figsize)

    # 向后兼容处理: 如果使用了旧的 hue 参数
    if hue is not None and classes is None:
        warnings.warn(
            "参数 'hue' 已弃用，请使用 'classes' 参数。在未来版本中 'hue' 参数将被移除。",
            DeprecationWarning,
            stacklevel=2,
        )
        classes = hue
    elif hue is not None and classes is not None:
        warnings.warn(
            "同时提供了 'hue' 和 'classes' 参数，将使用 'classes' 参数。",
            UserWarning,
            stacklevel=2,
        )

    # 处理点大小映射
    point_sizes_scalar: float | None = None
    point_sizes_array: np.ndarray | None = None

    if size is not None:
        if isinstance(s, (int, float)):
            # 如果s是标量，使用size数组作为实际大小
            point_sizes_array = np.asarray(size, dtype=float)
        else:
            # 如果s已经是数组，将size作为权重应用
            point_sizes_array = (
                np.asarray(s, dtype=float)
                * np.asarray(size, dtype=float)
                / float(np.mean(size))
            )
    else:
        if s is None:
            point_sizes_scalar = None
        elif np.isscalar(s):
            point_sizes_scalar = float(np.asarray(s, dtype=float).reshape(-1)[0].item())
        else:
            point_sizes_array = np.asarray(s, dtype=float)

    # 处理分类颜色映射
    if classes is not None:
        unique_classes = np.unique(classes)

        from .mpl_utils import get_current_color_cycle, use_color_cycle

        colors = colors or get_current_color_cycle()

        color_cycle_ctx = use_color_cycle(colors, policy='context')
        if color_cycle_ctx is None:
            raise AssertionError('Expected a matplotlib color cycle context manager')

        with color_cycle_ctx:
            # 为每个类别分别绘制散点
            for i, cls in enumerate(unique_classes):
                mask = classes == cls
                ax.scatter(
                    x[mask],
                    y[mask],
                    s=point_sizes_array[mask]
                    if point_sizes_array is not None
                    else point_sizes_scalar,
                    # c=colors[i],
                    alpha=alpha,
                    label=str(cls),
                    **scatter_kwargs,
                )

        # 添加图例
        ax.legend()
    else:
        # 没有分类的简单散点图
        ax.scatter(
            x,
            y,
            s=point_sizes_array if point_sizes_array is not None else point_sizes_scalar,
            alpha=alpha,
            **scatter_kwargs,
        )

    # 设置标题和轴标签
    if title is not None:
        ax.set_title(title)
    if xlabel is not None:
        ax.set_xlabel(xlabel)
    if ylabel is not None:
        ax.set_ylabel(ylabel)

    _finalize_figure(fig, show, save_path, auto_close)
    return fig, ax


def plot_distribution(
    data: Union[np.ndarray, typing.Sequence[np.ndarray]],
    labels: Optional[typing.Sequence[str]] = None,
    kind: Literal['hist', 'kde', 'ecdf'] = 'hist',
    show_kde: bool = False,
    show_rug: bool = False,
    orientation: Literal['vertical', 'horizontal'] = 'vertical',
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    figsize: Optional[Tuple[float, float]] = None,
    **sns_kwargs,
) -> FigAx:
    """绘制分布图，支持直方图、核密度估计和经验累积分布函数。

    此函数可以绘制单个或多个分布，支持多种分布显示方式。

    根据 kind 的不同，分别调用:

    - `sns.histplot`：绘制直方图
    - `sns.kdeplot`：绘制核密度估计
    - `sns.ecdfplot`：绘制经验累积分布函数

    可以通过 sns_kwargs 传递额外的参数给 seaborn 的绘图函数。

    Parameters
    ----------
    - `data` : `Union[np.ndarray, List[np.ndarray]]`
        - 分布数据。可以是单个1D数组或多个1D数组的列表
    - `labels` : `Optional[List[str]]`, optional
        - 每个分布的标签，用于图例显示, by default None
    - `kind` : `Literal['hist', 'kde', 'ecdf']`, optional
        - 分布图类型。'hist'为直方图，'kde'为核密度估计，'ecdf'为经验累积分布函数, by default 'hist'
    - `show_kde` : `bool`, optional
        - 是否在直方图上叠加核密度估计曲线, by default False
    - `show_rug` : `bool`, optional
        - 是否显示边界地毯图（数据点分布）, by default False
    - `orientation` : `Literal['vertical', 'horizontal']`, optional
        - 分布图方向, by default 'vertical'
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `xlabel` : `Optional[str]`, optional
        - X轴标签, by default None
    - `ylabel` : `Optional[str]`, optional
        - Y轴标签, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 绘图轴对象。如果为None则创建新图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None
    - `figsize` : `Optional[Tuple[float, float]]`, optional
        - 图表大小, by default None
    Returns
    ----------
    - `FigAx`
        - 图表对象和轴对象的元组

    Examples
    --------
    **单个分布直方图**：
    ```python
    import numpy as np
    data = np.random.normal(0, 1, 1000)
    fig, ax = plot_distribution(data, kind='hist', bins=30)
    ax.set_title('Normal Distribution')
    ax.set_xlabel('Value')
    ax.set_ylabel('Frequency')
    ```

    **多个分布比较**：
    ```python
    data1 = np.random.normal(0, 1, 1000)
    data2 = np.random.normal(2, 1.5, 1000)
    data3 = np.random.exponential(1, 1000)
    fig, ax = plot_distribution([data1, data2, data3],
                                labels=['Normal(0,1)', 'Normal(2,1.5)', 'Exp(1)'],
                                kind='hist', alpha=0.6)
    ax.set_title('Multiple Distributions')
    ax.legend()
    ```

    **直方图叠加KDE**：
    ```python
    fig, ax = plot_distribution(data, kind='hist', show_kde=True,
                                bins=30, alpha=0.7)
    ax.set_title('Histogram with KDE Overlay')
    ```

    **核密度估计**：
    ```python
    fig, ax = plot_distribution([data1, data2],
                                labels=['Distribution A', 'Distribution B'],
                                kind='kde', fill=True, alpha=0.3)
    ax.set_title('KDE Plots')
    ```

    **经验累积分布函数**：
    ```python
    fig, ax = plot_distribution([data1, data2],
                                labels=['Dataset 1', 'Dataset 2'],
                                kind='ecdf')
    ax.set_title('Empirical CDF')
    ax.set_xlabel('Value')
    ax.set_ylabel('Cumulative Probability')
    ```

    **水平分布图**：
    ```python
    fig, ax = plot_distribution(data, kind='hist',
                                orientation='horizontal', bins=25)
    ax.set_title('Horizontal Distribution')
    ```

    **带边界地毯图**：
    ```python
    fig, ax = plot_distribution(data, kind='hist', show_rug=True,
                                bins=20, alpha=0.7)
    ax.set_title('Distribution with Rug Plot')
    ```

    **参数说明**：

    - `kind='hist'`：直方图，可配合`bins`, `alpha`等参数
    - `kind='kde'`：核密度估计，可配合`fill`, `bw_adjust`等参数
    - `kind='ecdf'`：累积分布函数
    - `show_kde=True`：在直方图上叠加KDE曲线
    - `show_rug=True`：显示数据点的边界地毯图
    - `orientation`：'vertical'或'horizontal'
    """
    import seaborn as sns

    fig, ax = _prepare_ax(ax, figsize=figsize)

    if isinstance(data, np.ndarray) and data.ndim == 1:
        data = [data]

    label_list = _normalize_labels(labels, len(data))

    orient_kw = 'x' if orientation == 'vertical' else 'y'

    plot_map = {'hist': sns.histplot, 'kde': sns.kdeplot, 'ecdf': sns.ecdfplot}
    plot_func = plot_map[kind]

    if kind == 'hist':
        sns_kwargs['kde'] = show_kde

    for i, d in enumerate(data):
        # 1. 先绘制主图
        plot_func(ax=ax, label=label_list[i], **{orient_kw: d, **sns_kwargs})

        # 2. 如果需要，手动绘制 rug plot，并确保颜色匹配
        if show_rug and kind in ['hist', 'kde']:
            # 从刚刚绘制的图形中获取颜色，以确保匹配
            # histplot 创建 patches, kdeplot/ecdfplot 创建 lines
            color = None
            if kind == 'hist' and ax.patches:
                # 获取最后一个 patch (bar) 的颜色
                color = ax.patches[-1].get_facecolor()
            elif ax.lines:
                # 获取最后一条 line 的颜色
                color = ax.lines[-1].get_color()

            # 从 sns_kwargs 中移除可能与 rugplot 冲突的参数
            rug_kwargs = sns_kwargs.copy()
            rug_kwargs.pop('bins', None)
            rug_kwargs.pop('kde', None)

            sns.rugplot(ax=ax, color=color, **{orient_kw: d, **rug_kwargs})

    if _has_visible_labels(label_list):
        ax.legend()

    # 设置标题和轴标签
    if title is not None:
        ax.set_title(title)
    if xlabel is not None:
        ax.set_xlabel(xlabel)
    if ylabel is not None:
        ax.set_ylabel(ylabel)

    _finalize_figure(fig, show, save_path, auto_close)
    return fig, ax


def plot_kde(
    data: Union[np.ndarray, typing.Sequence[np.ndarray]],
    labels: Optional[typing.Sequence[str]] = None,
    x_limits: Optional[Tuple[float, float]] = None,
    n_points: int = 200,
    bandwidth: Optional[float] = None,
    fill: bool = False,
    alpha: float = 0.7,
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    figsize: Optional[Tuple[float, float]] = None,
    **plot_kwargs,
) -> FigAx:
    """绘制核密度估计图，简化版本专注于KDE功能。

    这是 plot_distribution 的简化版本，专门用于绘制核密度估计图，
    提供更直观的参数设置和更简单的使用方式。

    Parameters
    ----------
    - `data` : `Union[np.ndarray, List[np.ndarray]]`
        - 数据。可以是单个1D数组或多个1D数组的列表
    - `labels` : `Optional[List[str]]`, optional
        - 每个数据集的标签，用于图例显示, by default None
    - `x_limits` : `Optional[Tuple[float, float]]`, optional
        - X轴范围。如果为None则自动计算, by default None
    - `n_points` : `int`, optional
        - KDE曲线的采样点数, by default 200
    - `bandwidth` : `Optional[float]`, optional
        - KDE带宽参数。如果为None则自动选择, by default None
    - `fill` : `bool`, optional
        - 是否填充曲线下方区域, by default False
    - `alpha` : `float`, optional
        - 透明度, by default 0.7
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `xlabel` : `Optional[str]`, optional
        - X轴标签, by default None
    - `ylabel` : `Optional[str]`, optional
        - Y轴标签, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 绘图轴对象。如果为None则创建新图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None
    - `figsize` : `Optional[Tuple[float, float]]`, optional
        - 图表大小, by default None

    Returns
    ----------
    - `FigAx`
        - 图表对象和轴对象的元组

    Examples
    --------
    **单个数据集的KDE**：
    ```python
    import numpy as np
    data = np.random.normal(0, 1, 1000)
    fig, ax = plot_kde(data)
    ax.set_title('KDE Plot')
    ax.set_xlabel('Value')
    ax.set_ylabel('Density')
    ```

    **多个数据集比较**：
    ```python
    data1 = np.random.normal(0, 1, 1000)
    data2 = np.random.normal(2, 1.5, 1000)
    fig, ax = plot_kde([data1, data2],
                       labels=['Dataset A', 'Dataset B'],
                       fill=True, alpha=0.5)
    ax.legend()
    ```

    **自定义带宽**：
    ```python
    fig, ax = plot_kde(data, bandwidth=0.5, fill=True)
    ax.set_title('KDE with Custom Bandwidth')
    ```
    """
    from scipy.stats import gaussian_kde

    fig, ax = _prepare_ax(ax, figsize=figsize)

    # 标准化数据格式
    if isinstance(data, np.ndarray) and data.ndim == 1:
        data = [data]

    label_list = _normalize_labels(labels, len(data))

    # 计算全局的x范围
    if x_limits is None:
        all_data = np.concatenate(data)
        data_min, data_max = np.min(all_data), np.max(all_data)
        # 稍微扩展范围以获得更好的可视化效果
        data_range = data_max - data_min
        x_limits = (data_min - 0.1 * data_range, data_max + 0.1 * data_range)

    x_range = np.linspace(x_limits[0], x_limits[1], n_points)

    # 为每个数据集绘制KDE
    for i, dataset in enumerate(data):
        if len(dataset) == 0:
            continue

        # 创建KDE对象
        kde = gaussian_kde(dataset)

        # 如果指定了带宽，则设置
        if bandwidth is not None:
            kde.set_bandwidth(bandwidth)

        # 计算密度值
        density = kde(x_range)

        # 处理每个数据集的绘图参数
        current_plot_kwargs = plot_kwargs.copy()

        # 如果plot_kwargs中有列表形式的参数，取对应索引的值
        for key, value in plot_kwargs.items():
            if isinstance(value, (list, tuple)) and len(value) > i:
                current_plot_kwargs[key] = value[i]

        # 绘制曲线
        plot_kw = {'label': label_list[i], **current_plot_kwargs}
        ax.plot(x_range, density, **plot_kw)

        if fill:
            ax.fill_between(x_range, density, alpha=alpha, **current_plot_kwargs)

    # 如果有标签，显示图例
    if _has_visible_labels(label_list):
        ax.legend()

    # 设置标题和轴标签
    if title is not None:
        ax.set_title(title)
    if xlabel is not None:
        ax.set_xlabel(xlabel)
    if ylabel is not None:
        ax.set_ylabel(ylabel)

    _finalize_figure(fig, show, save_path, auto_close)
    return fig, ax


def plot_box(
    data: Union[np.ndarray, typing.Sequence[np.ndarray]],
    labels: Optional[typing.Sequence[str]] = None,
    kind: Literal['box', 'violin', 'boxen'] = 'box',
    orientation: Literal['vertical', 'horizontal'] = 'vertical',
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    figsize: Optional[Tuple[float, float]] = None,
    **sns_kwargs,
) -> FigAx:
    """绘制箱线图类图表，支持箱线图、小提琴图和增强箱线图。

    此函数可以绘制多种类型的分布比较图，适用于比较多个组的数据分布。

    根据 kind 的不同，分别调用:

    - `sns.boxplot`：绘制箱线图
    - `sns.violinplot`：绘制小提琴图
    - `sns.boxenplot`：绘制增强箱线图

    可以通过 sns_kwargs 传递额外的参数给 seaborn 的绘图函数。

    Parameters
    ----------
    - `data` : `Union[np.ndarray, List[np.ndarray]]`
        - 箱线图数据。可以是2D数组（每列一个变量）或1D数组的列表
    - `labels` : `Optional[List[str]]`, optional
        - 每个变量的标签, by default None
    - `kind` : `Literal['box', 'violin', 'boxen']`, optional
        - 图表类型。'box'为箱线图，'violin'为小提琴图，'boxen'为增强箱线图, by default 'box'
    - `orientation` : `Literal['vertical', 'horizontal']`, optional
        - 图表方向, by default 'vertical'
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `xlabel` : `Optional[str]`, optional
        - X轴标签, by default None
    - `ylabel` : `Optional[str]`, optional
        - Y轴标签, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 绘图轴对象。如果为None则创建新图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None
    - `figsize` : `Optional[Tuple[float, float]]`, optional
        - 图表大小, by default None
    Returns
    ----------
    - `FigAx`
        - 图表对象和轴对象的元组

    Examples
    --------
    **基本箱线图（列表输入）**：
    ```python
    import numpy as np
    data = [np.random.normal(i, 1, 100) for i in range(5)]
    fig, ax = plot_box(data, labels=['Group A', 'Group B', 'Group C', 'Group D', 'Group E'])
    ax.set_title('Box Plot Comparison')
    ax.set_ylabel('Values')
    ```

    **箱线图（2D数组输入）**：
    ```python
    data_2d = np.random.randn(100, 4)
    fig, ax = plot_box(data_2d, labels=['Var1', 'Var2', 'Var3', 'Var4'])
    ax.set_title('Box Plot from 2D Array')
    ```

    **小提琴图**：
    ```python
    fig, ax = plot_box(data, labels=['Group A', 'Group B', 'Group C', 'Group D', 'Group E'],
                       kind='violin')
    ax.set_title('Violin Plot')
    ax.set_ylabel('Distribution')
    ```

    **增强箱线图**：
    ```python
    fig, ax = plot_box(data, labels=['Group A', 'Group B', 'Group C', 'Group D', 'Group E'],
                       kind='boxen')
    ax.set_title('Enhanced Box Plot')
    ```

    **水平箱线图**：
    ```python
    fig, ax = plot_box(data, labels=['Group A', 'Group B', 'Group C', 'Group D', 'Group E'],
                       orientation='horizontal')
    ax.set_title('Horizontal Box Plot')
    ax.set_xlabel('Values')
    ```

    **自定义样式**：
    ```python
    fig, ax = plot_box(data, labels=['A', 'B', 'C', 'D', 'E'],
                       kind='violin', palette='Set2', alpha=0.7)
    ax.set_title('Styled Violin Plot')
    ```

    **参数说明**：

    - `data`：可以是1D数组的列表或2D数组（每列一个变量）
    - `kind='box'`：箱线图，显示五数概括
    - `kind='violin'`：小提琴图，显示分布密度
    - `kind='boxen'`：增强箱线图，显示更多分位数
    - `orientation`：'vertical'或'horizontal'
    - 其他参数会传递给相应的seaborn函数
    """
    import seaborn as sns

    fig, ax = _prepare_ax(ax, figsize=figsize)

    plot_map = {'box': sns.boxplot, 'violin': sns.violinplot, 'boxen': sns.boxenplot}
    plot_func = plot_map[kind]

    if isinstance(data, list):
        if labels is None:
            labels = [f'Var {i}' for i in range(len(data))]
        plot_data = np.concatenate(data)
        plot_labels = np.concatenate([[lbl] * len(d) for lbl, d in zip(labels, data)])
    else:
        data = np.asarray(data)
        if data.ndim == 1:
            data = data[:, np.newaxis]
        n_obs, n_vars = data.shape
        if labels is None:
            labels = [f'Var {i}' for i in range(n_vars)]
        plot_data = data.flatten(order='F')
        plot_labels = np.repeat(labels, n_obs)

    orient_kw = (
        {'x': plot_labels, 'y': plot_data}
        if orientation == 'vertical'
        else {'y': plot_labels, 'x': plot_data}
    )

    # 处理 palette 参数以避免 FutureWarning
    # 如果用户传递了 palette 但没有设置 hue，我们需要设置 hue 来避免警告
    if 'palette' in sns_kwargs and 'hue' not in orient_kw:
        if orientation == 'vertical':
            orient_kw['hue'] = plot_labels
        else:
            orient_kw['hue'] = plot_labels
        # 设置 legend=False 以避免显示不必要的图例
        sns_kwargs.setdefault('legend', False)

    plot_func(ax=ax, **orient_kw, **sns_kwargs)

    # 设置标题和轴标签
    if title is not None:
        ax.set_title(title)
    if xlabel is not None:
        ax.set_xlabel(xlabel)
    if ylabel is not None:
        ax.set_ylabel(ylabel)

    _finalize_figure(fig, show, save_path, auto_close)
    return fig, ax


def plot_area(
    x: np.ndarray | None = None,
    y: np.ndarray | None = None,
    labels: Optional[typing.Sequence[str] | str] = None,
    stacked: bool = False,
    alpha: float = 0.7,
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    figsize: Optional[Tuple[float, float]] = None,
    **kwargs,
) -> FigAx:
    """绘制面积图，支持堆叠和非堆叠模式。

    此函数可以绘制单个或多个面积图，支持堆叠显示。

    Parameters
    ----------
    - `x` : `np.ndarray | None`, optional
        - X轴数据。如果为None，则使用索引作为X轴数据, by default None
    - `y` : `np.ndarray | None`, optional
        - Y轴数据。必须提供。1D数组绘制单个面积，2D数组绘制多个面积（每行一个面积）, by default None
    - `labels` : `Optional[List[str]]`, optional
        - 每个面积的标签，用于图例显示, by default None
    - `stacked` : `bool`, optional
        - 是否堆叠显示面积图, by default False
    - `alpha` : `float`, optional
        - 面积透明度, by default 0.7
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `xlabel` : `Optional[str]`, optional
        - X轴标签, by default None
    - `ylabel` : `Optional[str]`, optional
        - Y轴标签, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 绘图轴对象。如果为None则创建新图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None
    - `figsize` : `Optional[Tuple[float, float]]`, optional
        - 图表大小, by default None
    - `**kwargs` : `dict`, optional
        - if stacked is True, 传递给 ax.stackplot 的其他参数
        - if stacked is False, 传递给 ax.fill_between 的其他参数

    Returns
    ----------
    - `FigAx`
        - 图表对象和轴对象的元组

    Raises
    ----------
    - `ValueError`
        - 当y参数为空时抛出
    - `ValueError`
        - 当labels数量与面积数不匹配时抛出

    Examples
    --------
    **单个面积图**：
    ```python
    import numpy as np
    x = np.linspace(0, 10, 100)
    y = np.sin(x)
    fig, ax = plot_area(x, y)
    ax.set_title('Single Area Plot')
    ax.set_xlabel('X values')
    ax.set_ylabel('Y values')
    ```

    **多个非堆叠面积图**：
    ```python
    x = np.linspace(0, 10, 100)
    y1 = np.sin(x)
    y2 = np.cos(x)
    y3 = np.sin(x + np.pi/4)
    y_data = np.array([y1, y2, y3])
    fig, ax = plot_area(x, y_data, labels=['sin(x)', 'cos(x)', 'sin(x+π/4)'],
                        stacked=False, alpha=0.5, figsize=(10, 6))
    ax.set_title('Multiple Non-Stacked Areas')
    ax.legend()
    ```

    **堆叠面积图**：
    ```python
    y_positive = np.abs(y_data)  # 确保为正值
    fig, ax = plot_area(x, y_positive, labels=['Area 1', 'Area 2', 'Area 3'],
                        stacked=True, alpha=0.7)
    ax.set_title('Stacked Area Plot')
    ax.set_ylabel('Cumulative Values')
    ax.legend()
    ```

    **自定义样式的面积图**：
    ```python
    fig, ax = plot_area(x, y_data, labels=['Series A', 'Series B', 'Series C'],
                        stacked=False, alpha=0.6,
                        edgecolor='black', linewidth=1)
    ax.set_title('Styled Area Plot')
    ```

    **无X轴数据的面积图**：
    ```python
    y_simple = np.random.randn(3, 50).cumsum(axis=1)
    fig, ax = plot_area(y=y_simple, labels=['Track 1', 'Track 2', 'Track 3'],
                        stacked=True)
    ax.set_title('Area Plot with Index as X-axis')
    ax.set_xlabel('Time Steps')
    ```

    **参数说明**：

    - `stacked=True`：堆叠模式，使用`ax.stackplot`
    - `stacked=False`：非堆叠模式，使用`ax.fill_between`
    - `alpha`：面积透明度，0-1之间
    - 堆叠模式适用于展示各部分对总量的贡献
    - 非堆叠模式适用于比较不同系列的分布
    """
    fig, ax = _prepare_ax(ax, figsize=figsize)

    if y is None:
        raise ValueError('`y` 参数不能为空')

    y = np.asarray(y)
    if y.ndim == 1:
        y = y.reshape(1, -1)

    n_areas = y.shape[0]
    if x is None:
        x = np.arange(y.shape[1])

    label_list = _normalize_labels(labels, n_areas)

    if stacked:
        # 堆叠面积图
        ax.stackplot(
            x,
            *[y[i, :] for i in range(n_areas)],
            labels=typing.cast(
                typing.Sequence[str], [label or '' for label in label_list]
            ),
            alpha=alpha,
            **kwargs,
        )
    else:
        # 非堆叠面积图
        for i in range(n_areas):
            ax.fill_between(x, y[i, :], alpha=alpha, label=label_list[i], **kwargs)

    if _has_visible_labels(label_list):
        ax.legend()

    # 设置标题和轴标签
    if title is not None:
        ax.set_title(title)
    if xlabel is not None:
        ax.set_xlabel(xlabel)
    if ylabel is not None:
        ax.set_ylabel(ylabel)

    _finalize_figure(fig, show, save_path, auto_close)
    return fig, ax


def plot_radar(
    data: np.ndarray,
    categories: typing.Sequence[str],
    labels: Optional[typing.Sequence[str]] = None,
    fill: bool = True,
    alpha: float = 0.25,
    linewidth: float = 2,
    title: Optional[str] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    figsize: Optional[Tuple[float, float]] = None,
    **mpl_plot_kwargs,
) -> FigAx:
    """绘制雷达图（极坐标图），适用于多维数据比较。

    此函数可以绘制单个或多个雷达图，用于展示多维数据的形状和比较。

    Parameters
    ----------
    - `data` : `np.ndarray`
        - 雷达图数据。1D数组绘制单个雷达图，2D数组绘制多个雷达图（每行一个雷达图）
    - `categories` : `List[str]`
        - 雷达图各个轴的类别标签
    - `labels` : `Optional[List[str]]`, optional
        - 每个雷达图的标签，用于图例显示, by default None
    - `fill` : `bool`, optional
        - 是否填充雷达图面积, by default True
    - `alpha` : `float`, optional
        - 填充面积的透明度, by default 0.25
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 绘图轴对象。如果为None则创建新的极坐标图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None
    - `figsize` : `Optional[Tuple[float, float]]`, optional
        - 图表大小, by default None
    Returns
    ----------
    - `FigAx`
        - 图表对象和轴对象的元组

    Raises
    ----------
    - `ValueError`
        - 当categories数量与数据列数不匹配时抛出
    - `ValueError`
        - 当labels数量与雷达图数量不匹配时抛出

    Examples
    --------
    **单个雷达图**：

    ```python
    import numpy as np
    # 5个维度的数据
    data = np.array([4, 3, 5, 2, 4])
    categories = ['Speed', 'Accuracy', 'Efficiency', 'Reliability', 'Usability']
    fig, ax = plot_radar(data, categories)
    ax.set_title('Performance Radar Chart')
    ```

    **多个雷达图比较**：

    ```python
    # 3个对象，每个5个维度
    data = np.array([
        [4, 3, 5, 2, 4],
        [5, 4, 3, 4, 3],
        [3, 5, 4, 3, 5]
    ])
    categories = ['Speed', 'Accuracy', 'Efficiency', 'Reliability', 'Usability']
    labels = ['Model A', 'Model B', 'Model C']
    fig, ax = plot_radar(data, categories, labels=labels)
    ax.set_title('Model Comparison')
    ax.legend()
    ```

    **不填充的雷达图**：

    ```python
    fig, ax = plot_radar(data, categories, labels=labels, fill=False)
    ax.set_title('Radar Chart (Lines Only)')
    ax.legend()
    ```

    **自定义样式的雷达图**：

    ```python
    fig, ax = plot_radar(data, categories, labels=labels,
                         fill=True, alpha=0.1,
                         linewidth=3, markersize=8)
    ax.set_title('Styled Radar Chart')
    ax.legend()
    ```

    **标准化数据的雷达图**：

    ```python
    # 将数据标准化到0-1范围
    data_norm = (data - data.min(axis=1, keepdims=True)) / (data.max(axis=1, keepdims=True) - data.min(axis=1, keepdims=True))
    fig, ax = plot_radar(data_norm, categories, labels=labels)
    ax.set_title('Normalized Radar Chart')
    ax.set_ylim(0, 1)
    ax.legend()
    ```

    **参数说明**：

    - `data`：1D数组（单个雷达图）或2D数组（多个雷达图，每行一个）
    - `categories`：各个轴的标签，数量必须与数据列数相同
    - `fill=True`：填充雷达图面积
    - `alpha`：填充透明度
    - 雷达图适用于展示多维数据的整体模式
    - 建议数据在相似的尺度范围内，或进行标准化处理
    """
    if ax is None:
        fig, ax = _prepare_ax(ax, figsize=figsize)
    else:
        fig = ax.figure

    data = np.asarray(data)
    if data.ndim == 1:
        data = data.reshape(1, -1)

    n_radars, n_categories = data.shape

    if len(categories) != n_categories:
        raise ValueError('`categories` 数量必须与数据列数相同')

    label_list = _normalize_labels(labels, n_radars)

    # 设置角度
    angles = np.linspace(0, 2 * np.pi, n_categories, endpoint=False).tolist()
    angles += angles[:1]  # 闭合图形

    # 绘制每个雷达图
    for i in range(n_radars):
        values = data[i, :].tolist()
        values += values[:1]  # 闭合图形

        ax.plot(
            angles,
            values,
            'o-',
            linewidth=linewidth,
            label=label_list[i],
            **mpl_plot_kwargs,
        )
        if fill:
            ax.fill(angles, values, alpha=alpha)

    # 设置类别标签
    ax.set_xticks(angles[:-1])
    ax.set_xticklabels(categories)

    # 设置网格
    ax.grid(True)

    if _has_visible_labels(label_list):
        ax.legend()

    # 设置标题
    if title is not None:
        ax.set_title(title)

    _finalize_figure(fig, show, save_path, auto_close)
    return fig, ax


def plot_heatmap(
    data: np.ndarray,
    xticklabels: Optional[typing.Sequence[str]] = None,
    yticklabels: Optional[typing.Sequence[str]] = None,
    fn_cell_labels: Optional[Callable[[int, int, float], str]] = None,
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    ax: Optional[Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    figsize: Optional[Tuple[float, float]] = None,
    **sns_heapmap_kwargs,
) -> FigAx:
    """绘制热力图，支持自定义单元格标签。

    此函数基于seaborn的heatmap实现，支持自定义单元格标签生成函数。

    Parameters
    ----------
    - `data` : `np.ndarray`
        - 热力图数据，2D数组
    - `xticklabels` : `Optional[List[str]]`, optional
        - X轴刻度标签, by default None
    - `yticklabels` : `Optional[List[str]]`, optional
        - Y轴刻度标签, by default None
    - `fn_cell_labels` : `Optional[Callable[[int, int, float], str]]`, optional
        - 单元格标签生成函数。接受(行索引, 列索引, 数值)参数，返回标签字符串, by default None
    - `title` : `Optional[str]`, optional
        - 图表标题, by default None
    - `xlabel` : `Optional[str]`, optional
        - X轴标签, by default None
    - `ylabel` : `Optional[str]`, optional
        - Y轴标签, by default None
    - `ax` : `Optional[plt.Axes]`, optional
        - 绘图轴对象。如果为None则创建新图表, by default None
    - `show` : `bool`, optional
        - 是否显示图表, by default True
    - `save_path` : `Optional[str]`, optional
        - 保存路径。如果提供则保存图表, by default None
    - `auto_close` : `Optional[bool]`, optional
        - 是否自动关闭图表释放内存, by default None
    - `figsize` : `Optional[Tuple[float, float]]`, optional
        - 图表大小, by default None
    Returns
    ----------
    - `FigAx`
        - 图表对象和轴对象的元组

    Raises
    ----------
    - `TypeError`
        - 当fn_cell_labels不是可调用对象时抛出

    Examples
    --------
    **基本热力图**：

    ```python
    import numpy as np
    data = np.random.rand(10, 12)
    fig, ax = plot_heatmap(data)
    ax.set_title('Basic Heatmap')
    ```

    **带标签的热力图**：

    ```python
    x_labels = [f'Month {i+1}' for i in range(12)]
    y_labels = [f'City {i+1}' for i in range(10)]
    fig, ax = plot_heatmap(data, xticklabels=x_labels, yticklabels=y_labels)
    ax.set_title('Heatmap with Labels')
    ```

    **带数值标注的热力图**：

    ```python
    fig, ax = plot_heatmap(data, xticklabels=x_labels, yticklabels=y_labels,
                           annot=True, fmt='.2f')
    ax.set_title('Annotated Heatmap')
    ```

    **自定义配色的热力图**：

    ```python
    fig, ax = plot_heatmap(data, xticklabels=x_labels, yticklabels=y_labels,
                           cmap='coolwarm', center=0, annot=True)
    ax.set_title('Custom Color Heatmap')
    ```

    **带颜色条标签的热力图**：

    ```python
    fig, ax = plot_heatmap(data, xticklabels=x_labels, yticklabels=y_labels,
                           cbar_kws={'label': 'Temperature (°C)'})
    ax.set_title('Heatmap with Colorbar Label')
    ```

    **使用自定义标签函数**：

    ```python
    def custom_label(row, col, value):
        return f'({row},{col})\\n{value:.1f}'
    fig, ax = plot_heatmap(data, fn_cell_labels=custom_label)
    ax.set_title('Heatmap with Custom Labels')
    ```

    **相关性矩阵热力图**：

    ```python
    import pandas as pd
    df = pd.DataFrame(np.random.randn(100, 5), columns=['A', 'B', 'C', 'D', 'E'])
    corr_matrix = df.corr().values
    fig, ax = plot_heatmap(corr_matrix,
                           xticklabels=df.columns,
                           yticklabels=df.columns,
                           annot=True, fmt='.2f',
                           cmap='RdBu_r', center=0)
    ax.set_title('Correlation Matrix')
    ```

    **参数说明**：

    - `data`：2D数组，热力图数据
    - `xticklabels`、`yticklabels`：轴标签
    - `fn_cell_labels`：自定义标签函数，接受(row, col, value)参数
    - `annot=True`：显示数值标注
    - `fmt`：数值格式化字符串
    - `cmap`：配色方案
    - `center`：颜色映射的中心值
    - 其他参数会传递给`sns.heatmap`
    """
    import seaborn as sns

    fig, ax = _prepare_ax(ax, figsize=figsize)
    data = np.asarray(data)

    if fn_cell_labels is not None:
        if not callable(fn_cell_labels):
            raise TypeError('`fn_cell_labels` 必须是一个可调用的函数。')

        # 1. 创建一个与 data 同形状的数组，用于存放自定义标签
        rows, cols = data.shape
        annot_array = np.empty_like(data, dtype=object)

        # 2. 遍历并调用函数生成标签
        for r in range(rows):
            for c in range(cols):
                annot_array[r, c] = fn_cell_labels(r, c, data[r, c])

        # 3. 将生成的数组作为 annot 参数，并强制 fmt 为空字符串
        sns_heapmap_kwargs['annot'] = annot_array
        sns_heapmap_kwargs['fmt'] = ''

    heatmap_xticklabels: bool | int | typing.Sequence[str] | Literal['auto'] = (
        xticklabels if xticklabels is not None else 'auto'
    )
    heatmap_yticklabels: bool | int | typing.Sequence[str] | Literal['auto'] = (
        yticklabels if yticklabels is not None else 'auto'
    )

    sns.heatmap(
        data,
        ax=ax,
        xticklabels=heatmap_xticklabels,
        yticklabels=heatmap_yticklabels,
        **sns_heapmap_kwargs,
    )
    # fig.tight_layout()

    # 设置标题和轴标签
    if title is not None:
        ax.set_title(title)
    if xlabel is not None:
        ax.set_xlabel(xlabel)
    if ylabel is not None:
        ax.set_ylabel(ylabel)

    _finalize_figure(fig, show, save_path, auto_close)
    return fig, ax
