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

### 设计哲学

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

### 函数通用定义规范

```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)。
- 尽量和样式配置分离，不参与样式的问题
"""

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

import matplotlib.pyplot as plt
import numpy as np

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


def plot_line(
    x: np.ndarray | None = None,
    y: np.ndarray | None = None,  # 为了习惯, 把 x 放在前面, 但实际上 y 不可省略
    labels: Optional[List[str]] = None,
    yerr: Optional[np.ndarray] = None,
    error_style: Literal['band', 'bar'] = 'band',
    lines_plot_kwargs: Dict | List[Dict] | None = None,
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    ax: Optional[plt.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])

    if labels is None:
        labels = [None] * n_lines
    elif len(labels) != n_lines:
        raise ValueError('`labels` 的数量必须与 y 的行数相同')

    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 = labels[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 = {**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=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 any(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_dual_line(
    x: np.ndarray | None = None,
    y1: np.ndarray = None,
    y2: np.ndarray = 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[plt.Axes] = None,
    show: bool = _plot_utils.AUTO_SHOW_PLOT,
    save_path: Optional[str] = None,
    auto_close: Optional[bool] = None,
    plot1_kwargs: Optional[Dict] = None,
    plot2_kwargs: Optional[Dict] = None,
    figsize: Optional[Tuple[float, float]] = None,
) -> Tuple[plt.Figure, Tuple[plt.Axes, plt.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 = plot1_kwargs or {}
    p2_kwargs = 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 = [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[List[str]] = None,
    bar_labels: Optional[List[str]] = None,
    comparisons: Optional[List[ComparisonDict]] = 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[plt.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 = []
        for comp in comparisons:
            g1_idx, b1_idx = comp['x1']
            g2_idx, b2_idx = comp['x2']

            new_comp = comp.copy()
            new_comp['x1'] = bar_positions[(g1_idx, b1_idx)]
            new_comp['y1'] = data[g1_idx, b1_idx]
            new_comp['x2'] = bar_positions[(g2_idx, b2_idx)]
            new_comp['y2'] = data[g2_idx, b2_idx]
            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[plt.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[List[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,
        )

    # 处理点大小映射
    if size is not None:
        if isinstance(s, (int, float)):
            # 如果s是标量，使用size数组作为实际大小
            point_sizes = size
        else:
            # 如果s已经是数组，将size作为权重应用
            point_sizes = np.array(s) * np.array(size) / np.mean(size)
    else:
        point_sizes = s

    # 处理分类颜色映射
    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()

        with use_color_cycle(colors, policy='context'):
            # 为每个类别分别绘制散点
            for i, cls in enumerate(unique_classes):
                mask = classes == cls
                ax.scatter(
                    x[mask],
                    y[mask],
                    s=point_sizes[mask]
                    if hasattr(point_sizes, '__len__')
                    else point_sizes,
                    # c=colors[i],
                    alpha=alpha,
                    label=str(cls),
                    **scatter_kwargs,
                )

        # 添加图例
        ax.legend()
    else:
        # 没有分类的简单散点图
        ax.scatter(x, y, s=point_sizes, 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, List[np.ndarray]],
    labels: Optional[List[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[plt.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]

    if labels is None:
        labels = [None] * 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=labels[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 any(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_kde(
    data: Union[np.ndarray, List[np.ndarray]],
    labels: Optional[List[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[plt.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]

    if labels is None:
        labels = [None] * 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': labels[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 any(label is not None for label in 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_box(
    data: Union[np.ndarray, List[np.ndarray]],
    labels: Optional[List[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[plt.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[List[str] | str] = None,
    stacked: bool = False,
    alpha: float = 0.7,
    title: Optional[str] = None,
    xlabel: Optional[str] = None,
    ylabel: Optional[str] = None,
    ax: Optional[plt.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])

    if labels is None:
        labels = [None] * n_areas
    elif isinstance(labels, str):
        labels = [labels] * n_areas
    elif len(labels) != n_areas:
        raise ValueError('`labels` 的数量必须与面积数相同')

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

    if any(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_radar(
    data: np.ndarray,
    categories: List[str],
    labels: Optional[List[str]] = None,
    fill: bool = True,
    alpha: float = 0.25,
    linewidth: float = 2,
    title: Optional[str] = None,
    ax: Optional[plt.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` 数量必须与数据列数相同')

    if labels is None:
        labels = [None] * n_radars
    elif len(labels) != n_radars:
        raise ValueError('`labels` 的数量必须与雷达图数量相同')

    # 设置角度
    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=labels[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 any(labels):
        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[List[str]] = None,
    yticklabels: Optional[List[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[plt.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'] = ''

    sns.heatmap(
        data,
        ax=ax,
        xticklabels=xticklabels,
        yticklabels=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
