"""
Box plot implementation for Rekha.
"""
import pandas as pd
from .base import BasePlot
[docs]
class BoxPlot(BasePlot):
"""Create a box plot with consistent Rekha interface."""
[docs]
def __init__(self, data=None, x=None, y=None, color=None, **kwargs):
"""
Create a box plot.
Parameters
----------
**kwargs
Additional parameters passed to BasePlot
"""
# Initialize base plot
super().__init__(data=data, x=x, y=y, color=color, **kwargs)
# Create the plot
if self.is_faceted:
self._create_faceted_plot()
else:
self._create_plot()
self._finalize_plot()
self._show_legend_if_needed()
def _create_plot(self):
"""Create the box plot."""
if isinstance(self.data, pd.DataFrame):
if self.x and not self.color:
# Group by x
groups = self.data.groupby(self.x, observed=True)[self.y].apply(list)
# Get consistent colors and order for x categories
categories, colors = self._get_consistent_colors_and_order(groups.index)
# Reorder groups to match the ordered categories
groups = groups[categories]
positions = range(len(groups))
bp = self.ax.boxplot(
groups.values,
positions=positions,
patch_artist=True,
**self.plot_kwargs,
)
# Color the boxes with consistent colors
for i, box in enumerate(bp["boxes"]):
# Get color from the consistent mapping
color = colors[i]
box.set_facecolor(color)
box.set_alpha(0.7)
# Apply grayscale patterns
if self.grayscale_friendly:
patterns = self._get_bw_patterns()
box.set_hatch(patterns["hatches"][i % len(patterns["hatches"])])
box.set_edgecolor("black" if not self.dark_mode else "white")
box.set_linewidth(1.5)
# Style whiskers, caps, medians, and fliers
self._style_boxplot_elements(bp)
self.ax.set_xticks(positions)
self.ax.set_xticklabels(groups.index)
elif self.color:
# Group by color for side-by-side boxes
# Get consistent colors and order for categories
unique_categories = self.data[self.color].unique()
categories, colors = self._get_consistent_colors_and_order(
unique_categories
)
# Create a color mapping dict for easy lookup
color_map = {cat: colors[i] for i, cat in enumerate(categories)}
color_groups = self.data.groupby(self.color, observed=True)
n_colors = len(color_groups)
width = 0.8 / n_colors
for i, (color_name, color_data) in enumerate(color_groups):
if self.x:
x_groups = color_data.groupby(self.x, observed=True)[
self.y
].apply(list)
positions = [j + i * width for j in range(len(x_groups))]
else:
x_groups = pd.Series([color_data[self.y].values])
positions = [i * width]
bp = self.ax.boxplot(
x_groups.values,
positions=positions,
widths=width * 0.8,
patch_artist=True,
label=str(color_name),
**self.plot_kwargs,
)
# Color the boxes
for box in bp["boxes"]:
box.set_facecolor(color_map[color_name])
box.set_alpha(0.7)
# Apply grayscale patterns
if self.grayscale_friendly:
patterns = self._get_bw_patterns()
box.set_hatch(
patterns["hatches"][i % len(patterns["hatches"])]
)
box.set_edgecolor(
"black" if not self.dark_mode else "white"
)
box.set_linewidth(1.5)
# Style whiskers, caps, medians, and fliers
self._style_boxplot_elements(bp)
if self.x:
self.ax.set_xticks(
[j + width * (n_colors - 1) / 2 for j in range(len(x_groups))] # type: ignore[possibly-unbound]
)
self.ax.set_xticklabels(x_groups.index) # type: ignore[possibly-unbound]
self._add_legend_with_spacing()
else:
# Single box plot
bp = self.ax.boxplot(
[self.data[self.y].values], patch_artist=True, **self.plot_kwargs
)
# Determine color for this series
color = self._get_next_color()
bp["boxes"][0].set_facecolor(color)
bp["boxes"][0].set_alpha(0.7)
# Apply grayscale patterns
if self.grayscale_friendly:
patterns = self._get_bw_patterns()
bp["boxes"][0].set_hatch(patterns["hatches"][0])
bp["boxes"][0].set_edgecolor(
"black" if not self.dark_mode else "white"
)
bp["boxes"][0].set_linewidth(1.5)
# Style whiskers, caps, medians, and fliers
self._style_boxplot_elements(bp)
def _style_boxplot_elements(self, bp):
"""Style boxplot whiskers, caps, medians, and fliers for proper theme visibility."""
# Set color based on theme
element_color = self.colors["text"]
# Style whiskers (vertical lines extending from boxes)
for whisker in bp["whiskers"]:
whisker.set_color(element_color)
whisker.set_linewidth(1.5)
# Style caps (horizontal lines at end of whiskers)
for cap in bp["caps"]:
cap.set_color(element_color)
cap.set_linewidth(1.5)
# Style medians (horizontal lines in middle of boxes)
for median in bp["medians"]:
median.set_color(element_color)
median.set_linewidth(2)
# Style fliers (outlier points)
for flier in bp["fliers"]:
flier.set_markerfacecolor(element_color)
flier.set_markeredgecolor(element_color)
flier.set_markersize(4)
def _create_faceted_plot(self):
"""Create faceted box plots."""
for i, row_val in enumerate(self.facet_row_values):
for j, col_val in enumerate(self.facet_col_values):
ax = self._get_wrapped_axes(i, j)
# Get data for this facet
facet_data = self._get_facet_data(row_val, col_val)
if len(facet_data) == 0:
continue
# Temporarily set the current axis and data
original_ax = self.ax
original_data = self.data
self.ax = ax
self.data = facet_data
# Create the plot for this facet
self._create_plot()
# Restore original data and axis
self.ax = original_ax
self.data = original_data
# Apply faceted finalization
self._finalize_faceted_plot()
self._show_legend_if_needed()
[docs]
def box(data=None, x=None, y=None, color=None, **kwargs):
"""
Create a box plot with Rekha styling.
Parameters
----------
data : DataFrame, dict, or None
The data to plot
x : str, optional
Column name for grouping variable
y : str, list, array, or None
Column name or data for box plot values
color : str, optional
Column name for color grouping
facet_col : str, optional
Column to use for creating subplots (facets)
facet_row : str, optional
Column to use for creating subplot rows
title : str, optional
Plot title
labels : dict, optional
Dictionary mapping column names to display labels
dark_mode : bool, default False
Whether to use dark theme
figsize : tuple, default (10, 6)
Figure size (width, height)
grayscale_friendly : bool, default False
Whether to add patterns for grayscale printing
**kwargs
Additional styling parameters
Returns
-------
BoxPlot
Box plot object with matplotlib figure and axes
Examples
--------
>>> import rekha as rk
>>> import pandas as pd
>>> import numpy as np
>>> df = pd.DataFrame({
... 'category': ['A', 'B', 'A', 'B', 'A', 'B'],
... 'values': [1, 2, 3, 4, 5, 6]
... })
>>> fig = rk.box(df, x='category', y='values', title='Box Plot')
>>> fig.show()
"""
return BoxPlot(data=data, x=x, y=y, color=color, **kwargs)