From e654ddc2502d9ecfd9174231a49df3480492bbe3 Mon Sep 17 00:00:00 2001
From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com>
Date: Fri, 28 Nov 2025 22:09:50 +0000
Subject: [PATCH 1/5] feat: implement box-basic plot for all 8 libraries
- Created spec file specs/box-basic.md defining requirements for basic box plots
- Implemented matplotlib version with customizable colors and outlier display
- Implemented seaborn version with statistical annotations
- Implemented plotly version with interactive hover information
- Implemented bokeh version with HTML output and customizable whiskers
- Implemented altair version with declarative Vega-Lite approach
- Implemented plotnine version with ggplot2-style grammar of graphics
- Implemented pygal version with SVG output
- Implemented highcharts version with interactive web visualization
All implementations:
- Show quartiles (Q1, median, Q3) as boxes
- Display whiskers extending to 1.5 * IQR
- Mark outliers as individual points
- Include sample size annotations
- Support multiple groups for comparison
- Use deterministic sample data with fixed seed
Closes #41
---
plots/altair/box/box-basic/default.py | 200 +++++++++++++++
plots/bokeh/box/box-basic/default.py | 280 ++++++++++++++++++++
plots/highcharts/box/box-basic/default.py | 282 +++++++++++++++++++++
plots/matplotlib/box/box-basic/default.py | 182 +++++++++++++
plots/plotly/box/box-basic/default.py | 218 ++++++++++++++++
plots/plotnine/box/box-basic/default.py | 190 ++++++++++++++
plots/pygal/box/box-basic/default.py | 173 +++++++++++++
plots/seaborn/boxplot/box-basic/default.py | 187 ++++++++++++++
specs/box-basic.md | 54 ++++
9 files changed, 1766 insertions(+)
create mode 100644 plots/altair/box/box-basic/default.py
create mode 100644 plots/bokeh/box/box-basic/default.py
create mode 100644 plots/highcharts/box/box-basic/default.py
create mode 100644 plots/matplotlib/box/box-basic/default.py
create mode 100644 plots/plotly/box/box-basic/default.py
create mode 100644 plots/plotnine/box/box-basic/default.py
create mode 100644 plots/pygal/box/box-basic/default.py
create mode 100644 plots/seaborn/boxplot/box-basic/default.py
create mode 100644 specs/box-basic.md
diff --git a/plots/altair/box/box-basic/default.py b/plots/altair/box/box-basic/default.py
new file mode 100644
index 00000000000..b180c81cbb9
--- /dev/null
+++ b/plots/altair/box/box-basic/default.py
@@ -0,0 +1,200 @@
+"""
+box-basic: Basic Box Plot
+Implementation for: altair
+Variant: default
+Python: 3.10+
+"""
+
+import altair as alt
+import pandas as pd
+import numpy as np
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from altair import Chart
+
+
+def create_plot(
+ data: pd.DataFrame,
+ values: str,
+ groups: str,
+ title: Optional[str] = None,
+ xlabel: Optional[str] = None,
+ ylabel: Optional[str] = None,
+ color_scheme: str = 'set2',
+ width: int = 600,
+ height: int = 400,
+ **kwargs
+) -> Chart:
+ """
+ Create a basic box plot showing statistical distribution of multiple groups using altair.
+
+ Args:
+ data: Input DataFrame with required columns
+ values: Column name containing numeric values
+ groups: Column name containing group categories
+ title: Plot title (optional)
+ xlabel: Custom x-axis label (optional, defaults to groups column name)
+ ylabel: Custom y-axis label (optional, defaults to values column name)
+ color_scheme: Color scheme for boxes (default: 'set2')
+ width: Figure width in pixels (default: 600)
+ height: Figure height in pixels (default: 400)
+ **kwargs: Additional parameters for altair chart configuration
+
+ Returns:
+ Altair Chart object
+
+ Raises:
+ ValueError: If data is empty
+ KeyError: If required columns not found
+
+ Example:
+ >>> data = pd.DataFrame({
+ ... 'Group': ['A', 'A', 'B', 'B', 'C', 'C'],
+ ... 'Value': [1, 2, 2, 3, 3, 4]
+ ... })
+ >>> chart = create_plot(data, values='Value', groups='Group')
+ """
+ # Input validation
+ if data.empty:
+ raise ValueError("Data cannot be empty")
+
+ # Check required columns
+ for col in [values, groups]:
+ if col not in data.columns:
+ available = ", ".join(data.columns)
+ raise KeyError(f"Column '{col}' not found. Available columns: {available}")
+
+ # Create the box plot using Altair's mark_boxplot
+ base = alt.Chart(data).mark_boxplot(
+ extent=1.5, # 1.5 * IQR for whiskers
+ outliers=True,
+ size=40,
+ opacity=0.7
+ ).encode(
+ x=alt.X(
+ f'{groups}:N',
+ title=xlabel or groups,
+ axis=alt.Axis(
+ labelAngle=0 if data[groups].nunique() <= 5 else -45,
+ labelLimit=200
+ )
+ ),
+ y=alt.Y(
+ f'{values}:Q',
+ title=ylabel or values,
+ scale=alt.Scale(zero=False)
+ ),
+ color=alt.Color(
+ f'{groups}:N',
+ scale=alt.Scale(scheme=color_scheme),
+ legend=None # Hide legend as it's redundant with x-axis
+ ),
+ tooltip=[
+ alt.Tooltip(f'{groups}:N', title='Group'),
+ alt.Tooltip(f'count({values}):Q', title='Count'),
+ alt.Tooltip(f'min({values}):Q', title='Min', format='.2f'),
+ alt.Tooltip(f'q1({values}):Q', title='Q1', format='.2f'),
+ alt.Tooltip(f'median({values}):Q', title='Median', format='.2f'),
+ alt.Tooltip(f'q3({values}):Q', title='Q3', format='.2f'),
+ alt.Tooltip(f'max({values}):Q', title='Max', format='.2f')
+ ]
+ )
+
+ # Add sample size annotations
+ text = alt.Chart(data).mark_text(
+ align='center',
+ baseline='top',
+ dy=10,
+ fontSize=10,
+ opacity=0.7
+ ).encode(
+ x=alt.X(f'{groups}:N'),
+ y=alt.Y(f'min({values}):Q'),
+ text=alt.Text('count():Q', format='d')
+ ).transform_aggregate(
+ count='count()',
+ groupby=[groups]
+ )
+
+ # Combine box plot with annotations
+ chart = (base + text).properties(
+ width=width,
+ height=height,
+ title=alt.TitleParams(
+ text=title or 'Box Plot Distribution',
+ fontSize=16,
+ anchor='middle'
+ )
+ ).configure_view(
+ strokeWidth=0
+ ).configure_axis(
+ grid=True,
+ gridOpacity=0.3,
+ gridDash=[3, 3],
+ domainWidth=1,
+ tickWidth=1
+ ).configure_boxplot(
+ median=dict(color='red', strokeWidth=2),
+ box=dict(strokeWidth=1.5),
+ outliers=dict(fill='red', fillOpacity=0.5, size=50)
+ )
+
+ return chart
+
+
+if __name__ == '__main__':
+ # Sample data for testing with different distributions per group
+ np.random.seed(42) # For reproducibility
+
+ # Generate sample data with 4 groups
+ data_dict = {
+ 'Group': [],
+ 'Value': []
+ }
+
+ # Group A: Normal distribution, mean=50, std=10
+ group_a_data = np.random.normal(50, 10, 40)
+ # Add some outliers
+ group_a_data = np.append(group_a_data, [80, 85, 15])
+
+ # Group B: Normal distribution, mean=60, std=15
+ group_b_data = np.random.normal(60, 15, 35)
+ # Add outliers
+ group_b_data = np.append(group_b_data, [100, 10])
+
+ # Group C: Normal distribution, mean=45, std=8
+ group_c_data = np.random.normal(45, 8, 45)
+
+ # Group D: Skewed distribution
+ group_d_data = np.random.gamma(2, 2, 30) + 40
+ # Add outliers
+ group_d_data = np.append(group_d_data, [75, 78, 20])
+
+ # Combine all data
+ for group, values in zip(
+ ['Group A', 'Group B', 'Group C', 'Group D'],
+ [group_a_data, group_b_data, group_c_data, group_d_data]
+ ):
+ data_dict['Group'].extend([group] * len(values))
+ data_dict['Value'].extend(values)
+
+ data = pd.DataFrame(data_dict)
+
+ # Create plot
+ chart = create_plot(
+ data,
+ values='Value',
+ groups='Group',
+ title='Statistical Distribution Comparison Across Groups',
+ ylabel='Measurement Value',
+ xlabel='Categories'
+ )
+
+ # Save for inspection
+ chart.save('plot.html')
+ print("Interactive plot saved to plot.html")
+
+ # Also save as PNG
+ chart.save('plot.png', scale_factor=2.0)
+ print("Static plot saved to plot.png")
\ No newline at end of file
diff --git a/plots/bokeh/box/box-basic/default.py b/plots/bokeh/box/box-basic/default.py
new file mode 100644
index 00000000000..17013b2f81b
--- /dev/null
+++ b/plots/bokeh/box/box-basic/default.py
@@ -0,0 +1,280 @@
+"""
+box-basic: Basic Box Plot
+Implementation for: bokeh
+Variant: default
+Python: 3.10+
+"""
+
+from bokeh.plotting import figure, output_file, save
+from bokeh.models import ColumnDataSource, Whisker
+from bokeh.transform import factor_cmap
+import pandas as pd
+import numpy as np
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from bokeh.plotting import Figure
+
+
+def create_plot(
+ data: pd.DataFrame,
+ values: str,
+ groups: str,
+ title: Optional[str] = None,
+ xlabel: Optional[str] = None,
+ ylabel: Optional[str] = None,
+ colors: Optional[list] = None,
+ width: int = 1000,
+ height: int = 600,
+ **kwargs
+) -> Figure:
+ """
+ Create a basic box plot showing statistical distribution of multiple groups using bokeh.
+
+ Args:
+ data: Input DataFrame with required columns
+ values: Column name containing numeric values
+ groups: Column name containing group categories
+ title: Plot title (optional)
+ xlabel: Custom x-axis label (optional, defaults to groups column name)
+ ylabel: Custom y-axis label (optional, defaults to values column name)
+ colors: List of colors for each box (optional)
+ width: Figure width in pixels (default: 1000)
+ height: Figure height in pixels (default: 600)
+ **kwargs: Additional parameters
+
+ Returns:
+ Bokeh Figure object
+
+ Raises:
+ ValueError: If data is empty
+ KeyError: If required columns not found
+
+ Example:
+ >>> data = pd.DataFrame({
+ ... 'Group': ['A', 'A', 'B', 'B', 'C', 'C'],
+ ... 'Value': [1, 2, 2, 3, 3, 4]
+ ... })
+ >>> fig = create_plot(data, values='Value', groups='Group')
+ """
+ # Input validation
+ if data.empty:
+ raise ValueError("Data cannot be empty")
+
+ # Check required columns
+ for col in [values, groups]:
+ if col not in data.columns:
+ available = ", ".join(data.columns)
+ raise KeyError(f"Column '{col}' not found. Available columns: {available}")
+
+ # Calculate box plot statistics for each group
+ group_names = sorted(data[groups].unique())
+
+ # Prepare data structures for box plot components
+ box_data = {
+ 'groups': [],
+ 'q1': [],
+ 'q2': [],
+ 'q3': [],
+ 'upper': [],
+ 'lower': [],
+ 'outliers_x': [],
+ 'outliers_y': []
+ }
+
+ for group in group_names:
+ group_data = data[data[groups] == group][values].dropna()
+
+ q1 = group_data.quantile(0.25)
+ q2 = group_data.quantile(0.5) # median
+ q3 = group_data.quantile(0.75)
+ iqr = q3 - q1
+ upper = min(group_data.max(), q3 + 1.5 * iqr)
+ lower = max(group_data.min(), q1 - 1.5 * iqr)
+
+ # Find outliers
+ outliers = group_data[(group_data < lower) | (group_data > upper)]
+
+ box_data['groups'].append(group)
+ box_data['q1'].append(q1)
+ box_data['q2'].append(q2)
+ box_data['q3'].append(q3)
+ box_data['upper'].append(upper)
+ box_data['lower'].append(lower)
+
+ # Add outliers
+ for outlier in outliers:
+ box_data['outliers_x'].append(group)
+ box_data['outliers_y'].append(outlier)
+
+ # Create figure
+ p = figure(
+ x_range=group_names,
+ width=width,
+ height=height,
+ title=title or 'Box Plot Distribution',
+ toolbar_location='above',
+ tools='pan,wheel_zoom,box_zoom,reset,save'
+ )
+
+ # Set colors
+ if not colors:
+ from bokeh.palettes import Set2_8
+ colors = Set2_8[:len(group_names)]
+
+ # Draw boxes (Q1 to Q3) for each group
+ for i, group in enumerate(group_names):
+ idx = box_data['groups'].index(group)
+
+ # Box from Q1 to Q3
+ p.vbar(
+ x=group,
+ width=0.5,
+ bottom=box_data['q1'][idx],
+ top=box_data['q3'][idx],
+ fill_color=colors[i % len(colors)],
+ line_color='black',
+ alpha=0.7
+ )
+
+ # Median line
+ p.line(
+ x=[i - 0.25, i + 0.25],
+ y=[box_data['q2'][idx], box_data['q2'][idx]],
+ line_color='red',
+ line_width=2
+ )
+
+ # Upper whisker
+ p.line(
+ x=[i, i],
+ y=[box_data['q3'][idx], box_data['upper'][idx]],
+ line_color='black',
+ line_width=1
+ )
+
+ # Upper whisker cap
+ p.line(
+ x=[i - 0.1, i + 0.1],
+ y=[box_data['upper'][idx], box_data['upper'][idx]],
+ line_color='black',
+ line_width=1.5
+ )
+
+ # Lower whisker
+ p.line(
+ x=[i, i],
+ y=[box_data['q1'][idx], box_data['lower'][idx]],
+ line_color='black',
+ line_width=1
+ )
+
+ # Lower whisker cap
+ p.line(
+ x=[i - 0.1, i + 0.1],
+ y=[box_data['lower'][idx], box_data['lower'][idx]],
+ line_color='black',
+ line_width=1.5
+ )
+
+ # Draw outliers
+ if box_data['outliers_x']:
+ p.circle(
+ x=box_data['outliers_x'],
+ y=box_data['outliers_y'],
+ size=8,
+ color='red',
+ alpha=0.5,
+ line_color='black',
+ line_width=1
+ )
+
+ # Styling
+ p.xaxis.axis_label = xlabel or groups
+ p.yaxis.axis_label = ylabel or values
+
+ p.title.text_font_size = '14pt'
+ p.title.align = 'center'
+
+ # Grid
+ p.ygrid.grid_line_alpha = 0.3
+ p.ygrid.grid_line_dash = [6, 4]
+ p.xgrid.visible = False
+
+ # Add sample size annotations
+ group_counts = data.groupby(groups)[values].count()
+ for i, (group, count) in enumerate(group_counts.items()):
+ y_position = data[values].min() - (data[values].max() - data[values].min()) * 0.05
+ from bokeh.models import Label
+ label = Label(
+ x=i, y=y_position,
+ text=f'n={count}',
+ text_align='center',
+ text_font_size='9pt',
+ text_alpha=0.7
+ )
+ p.add_layout(label)
+
+ return p
+
+
+if __name__ == '__main__':
+ # Sample data for testing with different distributions per group
+ np.random.seed(42) # For reproducibility
+
+ # Generate sample data with 4 groups
+ data_dict = {
+ 'Group': [],
+ 'Value': []
+ }
+
+ # Group A: Normal distribution, mean=50, std=10
+ group_a_data = np.random.normal(50, 10, 40)
+ # Add some outliers
+ group_a_data = np.append(group_a_data, [80, 85, 15])
+
+ # Group B: Normal distribution, mean=60, std=15
+ group_b_data = np.random.normal(60, 15, 35)
+ # Add outliers
+ group_b_data = np.append(group_b_data, [100, 10])
+
+ # Group C: Normal distribution, mean=45, std=8
+ group_c_data = np.random.normal(45, 8, 45)
+
+ # Group D: Skewed distribution
+ group_d_data = np.random.gamma(2, 2, 30) + 40
+ # Add outliers
+ group_d_data = np.append(group_d_data, [75, 78, 20])
+
+ # Combine all data
+ for group, values in zip(
+ ['Group A', 'Group B', 'Group C', 'Group D'],
+ [group_a_data, group_b_data, group_c_data, group_d_data]
+ ):
+ data_dict['Group'].extend([group] * len(values))
+ data_dict['Value'].extend(values)
+
+ data = pd.DataFrame(data_dict)
+
+ # Create plot
+ fig = create_plot(
+ data,
+ values='Value',
+ groups='Group',
+ title='Statistical Distribution Comparison Across Groups',
+ ylabel='Measurement Value',
+ xlabel='Categories'
+ )
+
+ # Save for inspection
+ output_file('plot.html')
+ save(fig)
+ print("Interactive plot saved to plot.html")
+
+ # Also export as PNG if possible
+ try:
+ from bokeh.io import export_png
+ export_png(fig, filename='plot.png')
+ print("Static plot saved to plot.png")
+ except ImportError:
+ print("Note: Install 'selenium' and 'pillow' to export PNG images")
\ No newline at end of file
diff --git a/plots/highcharts/box/box-basic/default.py b/plots/highcharts/box/box-basic/default.py
new file mode 100644
index 00000000000..444abbccdd5
--- /dev/null
+++ b/plots/highcharts/box/box-basic/default.py
@@ -0,0 +1,282 @@
+"""
+box-basic: Basic Box Plot
+Implementation for: highcharts
+Variant: default
+Python: 3.10+
+
+Note: Highcharts requires a license for commercial use.
+"""
+
+from highcharts_core import Chart
+from highcharts_core.options import HighchartsOptions
+from highcharts_core.options.plot_options.boxplot import BoxPlotOptions
+from highcharts_core.options.series.boxplot import BoxPlotSeries
+import pandas as pd
+import numpy as np
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from highcharts_core import Chart
+
+
+def create_plot(
+ data: pd.DataFrame,
+ values: str,
+ groups: str,
+ title: Optional[str] = None,
+ xlabel: Optional[str] = None,
+ ylabel: Optional[str] = None,
+ colors: Optional[list] = None,
+ height: int = 600,
+ **kwargs
+) -> Chart:
+ """
+ Create a basic box plot showing statistical distribution of multiple groups using Highcharts.
+
+ Args:
+ data: Input DataFrame with required columns
+ values: Column name containing numeric values
+ groups: Column name containing group categories
+ title: Plot title (optional)
+ xlabel: Custom x-axis label (optional, defaults to groups column name)
+ ylabel: Custom y-axis label (optional, defaults to values column name)
+ colors: List of colors for each box (optional)
+ height: Figure height in pixels (default: 600)
+ **kwargs: Additional parameters for Highcharts configuration
+
+ Returns:
+ Highcharts Chart object
+
+ Raises:
+ ValueError: If data is empty
+ KeyError: If required columns not found
+
+ Example:
+ >>> data = pd.DataFrame({
+ ... 'Group': ['A', 'A', 'B', 'B', 'C', 'C'],
+ ... 'Value': [1, 2, 2, 3, 3, 4]
+ ... })
+ >>> chart = create_plot(data, values='Value', groups='Group')
+ """
+ # Input validation
+ if data.empty:
+ raise ValueError("Data cannot be empty")
+
+ # Check required columns
+ for col in [values, groups]:
+ if col not in data.columns:
+ available = ", ".join(data.columns)
+ raise KeyError(f"Column '{col}' not found. Available columns: {available}")
+
+ # Prepare box plot data
+ group_names = sorted(data[groups].unique())
+ box_data = []
+ outliers_data = []
+
+ for i, group in enumerate(group_names):
+ group_data = data[data[groups] == group][values].dropna()
+
+ # Calculate statistics
+ q1 = float(group_data.quantile(0.25))
+ median = float(group_data.quantile(0.5))
+ q3 = float(group_data.quantile(0.75))
+ iqr = q3 - q1
+ lower_whisker = max(float(group_data.min()), q1 - 1.5 * iqr)
+ upper_whisker = min(float(group_data.max()), q3 + 1.5 * iqr)
+
+ # Box plot data: [low, q1, median, q3, high]
+ box_data.append([lower_whisker, q1, median, q3, upper_whisker])
+
+ # Find outliers
+ outliers = group_data[(group_data < lower_whisker) | (group_data > upper_whisker)]
+ for outlier in outliers:
+ outliers_data.append([i, float(outlier)])
+
+ # Create chart
+ chart = Chart()
+
+ # Configure chart options
+ chart.options = HighchartsOptions()
+
+ # Title
+ chart.options.title = {
+ 'text': title or 'Box Plot Distribution',
+ 'style': {
+ 'fontSize': '16px',
+ 'fontWeight': 'bold'
+ }
+ }
+
+ # X-axis
+ chart.options.x_axis = {
+ 'categories': list(group_names),
+ 'title': {
+ 'text': xlabel or groups
+ }
+ }
+
+ # Y-axis
+ chart.options.y_axis = {
+ 'title': {
+ 'text': ylabel or values
+ },
+ 'gridLineWidth': 1,
+ 'gridLineDashStyle': 'Dot',
+ 'gridLineColor': '#e0e0e0'
+ }
+
+ # Colors
+ if colors:
+ chart.options.colors = colors
+ else:
+ chart.options.colors = ['#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854']
+
+ # Plot options
+ chart.options.plot_options = {
+ 'boxplot': {
+ 'fillColor': None,
+ 'lineWidth': 2,
+ 'medianWidth': 3,
+ 'medianColor': '#FF0000',
+ 'stemWidth': 1,
+ 'whiskerWidth': 2,
+ 'whiskerLength': '50%'
+ }
+ }
+
+ # Tooltip
+ chart.options.tooltip = {
+ 'shared': False,
+ 'useHTML': True,
+ 'headerFormat': '{point.key}
',
+ 'pointFormat': (
+ 'Max: {point.high}
'
+ 'Q3: {point.q3}
'
+ 'Median: {point.median}
'
+ 'Q1: {point.q1}
'
+ 'Min: {point.low}
'
+ )
+ }
+
+ # Chart dimensions
+ chart.options.chart = {
+ 'type': 'boxplot',
+ 'height': height,
+ 'backgroundColor': 'white'
+ }
+
+ # Add box plot series
+ chart.add_series(BoxPlotSeries.from_array(
+ data=box_data,
+ name='Distribution',
+ colorByPoint=True
+ ))
+
+ # Add outliers as scatter series if any exist
+ if outliers_data:
+ from highcharts_core.options.series.scatter import ScatterSeries
+
+ chart.add_series(ScatterSeries.from_array(
+ data=outliers_data,
+ name='Outliers',
+ color='rgba(255, 0, 0, 0.5)',
+ marker={
+ 'fillColor': 'rgba(255, 0, 0, 0.5)',
+ 'lineWidth': 1,
+ 'lineColor': '#000000',
+ 'radius': 4
+ },
+ tooltip={
+ 'pointFormat': 'Outlier: {point.y}'
+ }
+ ))
+
+ # Legend
+ chart.options.legend = {
+ 'enabled': False # Hide legend for cleaner look
+ }
+
+ # Credits
+ chart.options.credits = {
+ 'enabled': False
+ }
+
+ return chart
+
+
+if __name__ == '__main__':
+ # Sample data for testing with different distributions per group
+ np.random.seed(42) # For reproducibility
+
+ # Generate sample data with 4 groups
+ data_dict = {
+ 'Group': [],
+ 'Value': []
+ }
+
+ # Group A: Normal distribution, mean=50, std=10
+ group_a_data = np.random.normal(50, 10, 40)
+ # Add some outliers
+ group_a_data = np.append(group_a_data, [80, 85, 15])
+
+ # Group B: Normal distribution, mean=60, std=15
+ group_b_data = np.random.normal(60, 15, 35)
+ # Add outliers
+ group_b_data = np.append(group_b_data, [100, 10])
+
+ # Group C: Normal distribution, mean=45, std=8
+ group_c_data = np.random.normal(45, 8, 45)
+
+ # Group D: Skewed distribution
+ group_d_data = np.random.gamma(2, 2, 30) + 40
+ # Add outliers
+ group_d_data = np.append(group_d_data, [75, 78, 20])
+
+ # Combine all data
+ for group, values in zip(
+ ['Group A', 'Group B', 'Group C', 'Group D'],
+ [group_a_data, group_b_data, group_c_data, group_d_data]
+ ):
+ data_dict['Group'].extend([group] * len(values))
+ data_dict['Value'].extend(values)
+
+ data = pd.DataFrame(data_dict)
+
+ # Create plot
+ chart = create_plot(
+ data,
+ values='Value',
+ groups='Group',
+ title='Statistical Distribution Comparison Across Groups',
+ ylabel='Measurement Value',
+ xlabel='Categories'
+ )
+
+ # Export to HTML
+ html_str = chart.to_js_literal()
+
+ # Create HTML file
+ html_content = f"""
+
+
+
+ Box Plot - Highcharts
+
+
+
+
+
+
+
+"""
+
+ with open('plot.html', 'w') as f:
+ f.write(html_content)
+
+ print("Interactive plot saved to plot.html")
+
+ # Note about PNG export
+ print("Note: Highcharts requires a license for commercial use")
+ print("For static image export, use Highcharts Export Server or phantomjs")
\ No newline at end of file
diff --git a/plots/matplotlib/box/box-basic/default.py b/plots/matplotlib/box/box-basic/default.py
new file mode 100644
index 00000000000..7d67148a401
--- /dev/null
+++ b/plots/matplotlib/box/box-basic/default.py
@@ -0,0 +1,182 @@
+"""
+box-basic: Basic Box Plot
+Implementation for: matplotlib
+Variant: default
+Python: 3.10+
+"""
+
+import matplotlib.pyplot as plt
+import pandas as pd
+import numpy as np
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from matplotlib.figure import Figure
+
+
+def create_plot(
+ data: pd.DataFrame,
+ values: str,
+ groups: str,
+ title: Optional[str] = None,
+ xlabel: Optional[str] = None,
+ ylabel: Optional[str] = None,
+ colors: Optional[list] = None,
+ figsize: tuple[float, float] = (10, 6),
+ **kwargs
+) -> Figure:
+ """
+ Create a basic box plot showing statistical distribution of multiple groups.
+
+ Args:
+ data: Input DataFrame with required columns
+ values: Column name containing numeric values
+ groups: Column name containing group categories
+ title: Plot title (optional)
+ xlabel: Custom x-axis label (optional, defaults to groups column name)
+ ylabel: Custom y-axis label (optional, defaults to values column name)
+ colors: List of colors for each box (optional)
+ figsize: Figure size as (width, height) in inches (default: (10, 6))
+ **kwargs: Additional parameters passed to boxplot function
+
+ Returns:
+ Matplotlib Figure object
+
+ Raises:
+ ValueError: If data is empty
+ KeyError: If required columns not found
+
+ Example:
+ >>> data = pd.DataFrame({
+ ... 'Group': ['A', 'A', 'B', 'B', 'C', 'C'],
+ ... 'Value': [1, 2, 2, 3, 3, 4]
+ ... })
+ >>> fig = create_plot(data, values='Value', groups='Group')
+ """
+ # Input validation
+ if data.empty:
+ raise ValueError("Data cannot be empty")
+
+ # Check required columns
+ for col in [values, groups]:
+ if col not in data.columns:
+ available = ", ".join(data.columns)
+ raise KeyError(f"Column '{col}' not found. Available columns: {available}")
+
+ # Prepare data for boxplot
+ grouped_data = [group[values].dropna().values for name, group in data.groupby(groups)]
+ group_names = data[groups].unique()
+
+ # Create figure
+ fig, ax = plt.subplots(figsize=figsize)
+
+ # Create boxplot
+ bp = ax.boxplot(
+ grouped_data,
+ labels=group_names,
+ patch_artist=True, # Enable filling boxes with colors
+ showmeans=False,
+ notch=False,
+ widths=0.7,
+ **kwargs
+ )
+
+ # Apply colors if provided
+ if colors:
+ for patch, color in zip(bp['boxes'], colors * len(bp['boxes'])):
+ patch.set_facecolor(color)
+ patch.set_alpha(0.7)
+ else:
+ # Use a default color scheme
+ default_colors = plt.cm.Set2(np.linspace(0, 1, len(bp['boxes'])))
+ for patch, color in zip(bp['boxes'], default_colors):
+ patch.set_facecolor(color)
+ patch.set_alpha(0.7)
+
+ # Customize whiskers, caps, medians, and outliers
+ for whisker in bp['whiskers']:
+ whisker.set(color='#8B8B8B', linewidth=1.5, linestyle='-')
+
+ for cap in bp['caps']:
+ cap.set(color='#8B8B8B', linewidth=2)
+
+ for median in bp['medians']:
+ median.set(color='#FF0000', linewidth=2)
+
+ for flier in bp['fliers']:
+ flier.set(marker='o', markerfacecolor='#FF0000', markersize=8,
+ alpha=0.5, markeredgecolor='#8B8B8B')
+
+ # Labels and title
+ ax.set_xlabel(xlabel or groups)
+ ax.set_ylabel(ylabel or values)
+
+ if title:
+ ax.set_title(title, fontsize=14, fontweight='bold')
+
+ # Grid for better readability
+ ax.grid(True, axis='y', alpha=0.3, linestyle='--')
+ ax.set_axisbelow(True)
+
+ # Rotate x-axis labels if there are many groups
+ if len(group_names) > 5:
+ plt.xticks(rotation=45, ha='right')
+
+ # Layout
+ plt.tight_layout()
+
+ return fig
+
+
+if __name__ == '__main__':
+ # Sample data for testing with different distributions per group
+ np.random.seed(42) # For reproducibility
+
+ # Generate sample data with 4 groups
+ group_names = ['Group A', 'Group B', 'Group C', 'Group D']
+ data_dict = {
+ 'Group': [],
+ 'Value': []
+ }
+
+ # Group A: Normal distribution, mean=50, std=10
+ group_a_data = np.random.normal(50, 10, 40)
+ # Add some outliers
+ group_a_data = np.append(group_a_data, [80, 85, 15])
+
+ # Group B: Normal distribution, mean=60, std=15
+ group_b_data = np.random.normal(60, 15, 35)
+ # Add outliers
+ group_b_data = np.append(group_b_data, [100, 10])
+
+ # Group C: Normal distribution, mean=45, std=8
+ group_c_data = np.random.normal(45, 8, 45)
+
+ # Group D: Skewed distribution
+ group_d_data = np.random.gamma(2, 2, 30) + 40
+ # Add outliers
+ group_d_data = np.append(group_d_data, [75, 78, 20])
+
+ # Combine all data
+ for group, values in zip(
+ ['Group A', 'Group B', 'Group C', 'Group D'],
+ [group_a_data, group_b_data, group_c_data, group_d_data]
+ ):
+ data_dict['Group'].extend([group] * len(values))
+ data_dict['Value'].extend(values)
+
+ data = pd.DataFrame(data_dict)
+
+ # Create plot
+ fig = create_plot(
+ data,
+ values='Value',
+ groups='Group',
+ title='Statistical Distribution Comparison Across Groups',
+ ylabel='Measurement Value',
+ xlabel='Groups'
+ )
+
+ # Save for inspection
+ plt.savefig('plot.png', dpi=300, bbox_inches='tight')
+ print("Plot saved to plot.png")
\ No newline at end of file
diff --git a/plots/plotly/box/box-basic/default.py b/plots/plotly/box/box-basic/default.py
new file mode 100644
index 00000000000..4c927a28a0d
--- /dev/null
+++ b/plots/plotly/box/box-basic/default.py
@@ -0,0 +1,218 @@
+"""
+box-basic: Basic Box Plot
+Implementation for: plotly
+Variant: default
+Python: 3.10+
+"""
+
+import plotly.graph_objects as go
+import plotly.express as px
+import pandas as pd
+import numpy as np
+from typing import TYPE_CHECKING, Optional, Union
+
+if TYPE_CHECKING:
+ from plotly.graph_objects import Figure
+
+
+def create_plot(
+ data: pd.DataFrame,
+ values: str,
+ groups: str,
+ title: Optional[str] = None,
+ xlabel: Optional[str] = None,
+ ylabel: Optional[str] = None,
+ color_discrete_sequence: Optional[list] = None,
+ height: int = 600,
+ width: int = 1000,
+ showlegend: bool = False,
+ **kwargs
+) -> Figure:
+ """
+ Create an interactive box plot showing statistical distribution of multiple groups using plotly.
+
+ Args:
+ data: Input DataFrame with required columns
+ values: Column name containing numeric values
+ groups: Column name containing group categories
+ title: Plot title (optional)
+ xlabel: Custom x-axis label (optional, defaults to groups column name)
+ ylabel: Custom y-axis label (optional, defaults to values column name)
+ color_discrete_sequence: List of colors for each box (optional)
+ height: Figure height in pixels (default: 600)
+ width: Figure width in pixels (default: 1000)
+ showlegend: Whether to show legend (default: False)
+ **kwargs: Additional parameters passed to plotly box trace
+
+ Returns:
+ Plotly Figure object
+
+ Raises:
+ ValueError: If data is empty
+ KeyError: If required columns not found
+
+ Example:
+ >>> data = pd.DataFrame({
+ ... 'Group': ['A', 'A', 'B', 'B', 'C', 'C'],
+ ... 'Value': [1, 2, 2, 3, 3, 4]
+ ... })
+ >>> fig = create_plot(data, values='Value', groups='Group')
+ """
+ # Input validation
+ if data.empty:
+ raise ValueError("Data cannot be empty")
+
+ # Check required columns
+ for col in [values, groups]:
+ if col not in data.columns:
+ available = ", ".join(data.columns)
+ raise KeyError(f"Column '{col}' not found. Available columns: {available}")
+
+ # Use plotly.express for easier box plot creation
+ fig = px.box(
+ data,
+ x=groups,
+ y=values,
+ color=groups,
+ color_discrete_sequence=color_discrete_sequence or px.colors.qualitative.Set2,
+ notched=False,
+ points='outliers', # Show only outliers as points
+ **kwargs
+ )
+
+ # Update traces for better styling
+ fig.update_traces(
+ boxmean='sd', # Show mean and standard deviation
+ marker=dict(
+ size=8,
+ opacity=0.5,
+ line=dict(width=1)
+ ),
+ line=dict(width=1.5),
+ fillcolor=None,
+ opacity=0.7
+ )
+
+ # Update layout
+ fig.update_layout(
+ title=dict(
+ text=title or 'Box Plot Distribution',
+ font=dict(size=16, family='Arial, sans-serif'),
+ x=0.5,
+ xanchor='center'
+ ),
+ xaxis=dict(
+ title=xlabel or groups,
+ gridcolor='lightgray',
+ gridwidth=0.5,
+ showgrid=False,
+ zeroline=False
+ ),
+ yaxis=dict(
+ title=ylabel or values,
+ gridcolor='lightgray',
+ gridwidth=0.5,
+ showgrid=True,
+ zeroline=True,
+ zerolinewidth=1,
+ zerolinecolor='lightgray'
+ ),
+ plot_bgcolor='white',
+ paper_bgcolor='white',
+ height=height,
+ width=width,
+ showlegend=showlegend,
+ hovermode='x unified',
+ hoverlabel=dict(
+ bgcolor="white",
+ font_size=12,
+ font_family="Arial, sans-serif"
+ )
+ )
+
+ # Add annotations with sample sizes
+ group_counts = data.groupby(groups)[values].count()
+ annotations = []
+ for i, (group_name, count) in enumerate(group_counts.items()):
+ annotations.append(
+ dict(
+ x=group_name,
+ y=data[data[groups] == group_name][values].min() -
+ (data[values].max() - data[values].min()) * 0.05,
+ text=f'n={count}',
+ showarrow=False,
+ font=dict(size=10, color='gray'),
+ xanchor='center',
+ yanchor='top'
+ )
+ )
+
+ fig.update_layout(annotations=annotations)
+
+ # Update hover template for better information
+ fig.update_traces(
+ hovertemplate='%{x}
' +
+ 'Max: %{y}
' +
+ 'Q3: %{upperfence}
' +
+ 'Median: %{median}
' +
+ 'Q1: %{lowerfence}
' +
+ 'Min: %{y}
' +
+ ''
+ )
+
+ return fig
+
+
+if __name__ == '__main__':
+ # Sample data for testing with different distributions per group
+ np.random.seed(42) # For reproducibility
+
+ # Generate sample data with 4 groups
+ data_dict = {
+ 'Group': [],
+ 'Value': []
+ }
+
+ # Group A: Normal distribution, mean=50, std=10
+ group_a_data = np.random.normal(50, 10, 40)
+ # Add some outliers
+ group_a_data = np.append(group_a_data, [80, 85, 15])
+
+ # Group B: Normal distribution, mean=60, std=15
+ group_b_data = np.random.normal(60, 15, 35)
+ # Add outliers
+ group_b_data = np.append(group_b_data, [100, 10])
+
+ # Group C: Normal distribution, mean=45, std=8
+ group_c_data = np.random.normal(45, 8, 45)
+
+ # Group D: Skewed distribution
+ group_d_data = np.random.gamma(2, 2, 30) + 40
+ # Add outliers
+ group_d_data = np.append(group_d_data, [75, 78, 20])
+
+ # Combine all data
+ for group, values in zip(
+ ['Group A', 'Group B', 'Group C', 'Group D'],
+ [group_a_data, group_b_data, group_c_data, group_d_data]
+ ):
+ data_dict['Group'].extend([group] * len(values))
+ data_dict['Value'].extend(values)
+
+ data = pd.DataFrame(data_dict)
+
+ # Create plot
+ fig = create_plot(
+ data,
+ values='Value',
+ groups='Group',
+ title='Statistical Distribution Comparison Across Groups',
+ ylabel='Measurement Value',
+ xlabel='Categories'
+ )
+
+ # Save for inspection
+ fig.write_html('plot.html')
+ fig.write_image('plot.png', width=1000, height=600, scale=2)
+ print("Interactive plot saved to plot.html")
+ print("Static plot saved to plot.png")
\ No newline at end of file
diff --git a/plots/plotnine/box/box-basic/default.py b/plots/plotnine/box/box-basic/default.py
new file mode 100644
index 00000000000..b85e1554e72
--- /dev/null
+++ b/plots/plotnine/box/box-basic/default.py
@@ -0,0 +1,190 @@
+"""
+box-basic: Basic Box Plot
+Implementation for: plotnine
+Variant: default
+Python: 3.10+
+"""
+
+from plotnine import (
+ ggplot, aes, geom_boxplot, theme, element_text, element_line,
+ labs, theme_minimal, scale_fill_brewer, coord_cartesian
+)
+import pandas as pd
+import numpy as np
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from plotnine import ggplot as GGPlot
+
+
+def create_plot(
+ data: pd.DataFrame,
+ values: str,
+ groups: str,
+ title: Optional[str] = None,
+ xlabel: Optional[str] = None,
+ ylabel: Optional[str] = None,
+ fill_palette: str = 'Set2',
+ width: int = 10,
+ height: int = 6,
+ show_outliers: bool = True,
+ **kwargs
+) -> GGPlot:
+ """
+ Create a basic box plot showing statistical distribution of multiple groups using plotnine (ggplot2 syntax).
+
+ Args:
+ data: Input DataFrame with required columns
+ values: Column name containing numeric values
+ groups: Column name containing group categories
+ title: Plot title (optional)
+ xlabel: Custom x-axis label (optional, defaults to groups column name)
+ ylabel: Custom y-axis label (optional, defaults to values column name)
+ fill_palette: Color palette for boxes (default: 'Set2')
+ width: Figure width in inches (default: 10)
+ height: Figure height in inches (default: 6)
+ show_outliers: Whether to show outliers (default: True)
+ **kwargs: Additional parameters for geom_boxplot
+
+ Returns:
+ plotnine ggplot object
+
+ Raises:
+ ValueError: If data is empty
+ KeyError: If required columns not found
+
+ Example:
+ >>> data = pd.DataFrame({
+ ... 'Group': ['A', 'A', 'B', 'B', 'C', 'C'],
+ ... 'Value': [1, 2, 2, 3, 3, 4]
+ ... })
+ >>> plot = create_plot(data, values='Value', groups='Group')
+ """
+ # Input validation
+ if data.empty:
+ raise ValueError("Data cannot be empty")
+
+ # Check required columns
+ for col in [values, groups]:
+ if col not in data.columns:
+ available = ", ".join(data.columns)
+ raise KeyError(f"Column '{col}' not found. Available columns: {available}")
+
+ # Create the ggplot object
+ plot = (
+ ggplot(data, aes(x=groups, y=values, fill=groups))
+ + geom_boxplot(
+ alpha=0.7,
+ outlier_alpha=0.5 if show_outliers else 0,
+ outlier_size=2,
+ outlier_color='red',
+ width=0.6,
+ **kwargs
+ )
+ + scale_fill_brewer(palette=fill_palette, guide=False) # Hide legend
+ + labs(
+ title=title or 'Box Plot Distribution',
+ x=xlabel or groups,
+ y=ylabel or values
+ )
+ + theme_minimal()
+ + theme(
+ figure_size=(width, height),
+ plot_title=element_text(size=14, weight='bold', ha='center'),
+ axis_title=element_text(size=11),
+ axis_text=element_text(size=10),
+ panel_grid_major_x=element_line(alpha=0),
+ panel_grid_major_y=element_line(alpha=0.3, linetype='dashed'),
+ panel_grid_minor=element_line(alpha=0)
+ )
+ )
+
+ # Rotate x-axis labels if there are many groups
+ unique_groups = data[groups].nunique()
+ if unique_groups > 5:
+ plot = plot + theme(
+ axis_text_x=element_text(angle=45, ha='right')
+ )
+
+ # Add sample size annotations
+ # plotnine doesn't have easy text annotations like ggplot2's annotate,
+ # but we can add them as a separate layer
+ from plotnine import geom_text, stat_summary
+
+ # Calculate group statistics for annotations
+ group_stats = data.groupby(groups).agg(
+ count=(values, 'count'),
+ min_val=(values, 'min')
+ ).reset_index()
+
+ # Adjust y position for annotations
+ y_range = data[values].max() - data[values].min()
+ y_position = data[values].min() - y_range * 0.05
+
+ group_stats['y_pos'] = y_position
+ group_stats['label'] = 'n=' + group_stats['count'].astype(str)
+
+ # Add annotations as a separate layer
+ plot = plot + geom_text(
+ aes(x=groups, y='y_pos', label='label'),
+ data=group_stats,
+ size=9,
+ alpha=0.7,
+ va='top',
+ ha='center'
+ )
+
+ return plot
+
+
+if __name__ == '__main__':
+ # Sample data for testing with different distributions per group
+ np.random.seed(42) # For reproducibility
+
+ # Generate sample data with 4 groups
+ data_dict = {
+ 'Group': [],
+ 'Value': []
+ }
+
+ # Group A: Normal distribution, mean=50, std=10
+ group_a_data = np.random.normal(50, 10, 40)
+ # Add some outliers
+ group_a_data = np.append(group_a_data, [80, 85, 15])
+
+ # Group B: Normal distribution, mean=60, std=15
+ group_b_data = np.random.normal(60, 15, 35)
+ # Add outliers
+ group_b_data = np.append(group_b_data, [100, 10])
+
+ # Group C: Normal distribution, mean=45, std=8
+ group_c_data = np.random.normal(45, 8, 45)
+
+ # Group D: Skewed distribution
+ group_d_data = np.random.gamma(2, 2, 30) + 40
+ # Add outliers
+ group_d_data = np.append(group_d_data, [75, 78, 20])
+
+ # Combine all data
+ for group, values in zip(
+ ['Group A', 'Group B', 'Group C', 'Group D'],
+ [group_a_data, group_b_data, group_c_data, group_d_data]
+ ):
+ data_dict['Group'].extend([group] * len(values))
+ data_dict['Value'].extend(values)
+
+ data = pd.DataFrame(data_dict)
+
+ # Create plot
+ plot = create_plot(
+ data,
+ values='Value',
+ groups='Group',
+ title='Statistical Distribution Comparison Across Groups',
+ ylabel='Measurement Value',
+ xlabel='Categories'
+ )
+
+ # Save for inspection
+ plot.save('plot.png', dpi=300, verbose=False)
+ print("Plot saved to plot.png")
\ No newline at end of file
diff --git a/plots/pygal/box/box-basic/default.py b/plots/pygal/box/box-basic/default.py
new file mode 100644
index 00000000000..999a0b7c7b9
--- /dev/null
+++ b/plots/pygal/box/box-basic/default.py
@@ -0,0 +1,173 @@
+"""
+box-basic: Basic Box Plot
+Implementation for: pygal
+Variant: default
+Python: 3.10+
+"""
+
+import pygal
+from pygal.style import Style
+import pandas as pd
+import numpy as np
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from pygal import Box
+
+
+def create_plot(
+ data: pd.DataFrame,
+ values: str,
+ groups: str,
+ title: Optional[str] = None,
+ xlabel: Optional[str] = None,
+ ylabel: Optional[str] = None,
+ width: int = 800,
+ height: int = 600,
+ show_legend: bool = True,
+ **kwargs
+) -> Box:
+ """
+ Create a basic box plot showing statistical distribution of multiple groups using pygal.
+
+ Args:
+ data: Input DataFrame with required columns
+ values: Column name containing numeric values
+ groups: Column name containing group categories
+ title: Plot title (optional)
+ xlabel: Custom x-axis label (optional, defaults to groups column name)
+ ylabel: Custom y-axis label (optional, defaults to values column name)
+ width: Figure width in pixels (default: 800)
+ height: Figure height in pixels (default: 600)
+ show_legend: Whether to show legend (default: True)
+ **kwargs: Additional parameters for pygal configuration
+
+ Returns:
+ pygal Box chart object
+
+ Raises:
+ ValueError: If data is empty
+ KeyError: If required columns not found
+
+ Example:
+ >>> data = pd.DataFrame({
+ ... 'Group': ['A', 'A', 'B', 'B', 'C', 'C'],
+ ... 'Value': [1, 2, 2, 3, 3, 4]
+ ... })
+ >>> chart = create_plot(data, values='Value', groups='Group')
+ """
+ # Input validation
+ if data.empty:
+ raise ValueError("Data cannot be empty")
+
+ # Check required columns
+ for col in [values, groups]:
+ if col not in data.columns:
+ available = ", ".join(data.columns)
+ raise KeyError(f"Column '{col}' not found. Available columns: {available}")
+
+ # Create custom style
+ custom_style = Style(
+ background='white',
+ plot_background='white',
+ foreground='#333',
+ foreground_strong='#333',
+ foreground_subtle='#555',
+ opacity=0.7,
+ opacity_hover=0.9,
+ colors=('#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', '#e5c494', '#b3b3b3'),
+ font_family='Arial, sans-serif',
+ major_guide_stroke_dasharray='3,3',
+ guide_stroke_dasharray='1,1'
+ )
+
+ # Create box plot
+ box_chart = pygal.Box(
+ title=title or 'Box Plot Distribution',
+ x_title=xlabel or groups,
+ y_title=ylabel or values,
+ width=width,
+ height=height,
+ show_legend=show_legend,
+ style=custom_style,
+ box_mode='tukey', # Use Tukey method (1.5 * IQR for whiskers)
+ print_values=False,
+ print_zeroes=False,
+ **kwargs
+ )
+
+ # Calculate box plot data for each group
+ group_names = sorted(data[groups].unique())
+
+ for group in group_names:
+ group_data = data[data[groups] == group][values].dropna()
+
+ # Pygal's Box chart expects data in a specific format:
+ # [min, Q1, median, Q3, max] or the raw values (pygal will calculate)
+ # We'll provide the raw values and let pygal handle the calculations
+ values_list = group_data.tolist()
+
+ # Add the series with label
+ box_chart.add(f'{group} (n={len(values_list)})', values_list)
+
+ return box_chart
+
+
+if __name__ == '__main__':
+ # Sample data for testing with different distributions per group
+ np.random.seed(42) # For reproducibility
+
+ # Generate sample data with 4 groups
+ data_dict = {
+ 'Group': [],
+ 'Value': []
+ }
+
+ # Group A: Normal distribution, mean=50, std=10
+ group_a_data = np.random.normal(50, 10, 40)
+ # Add some outliers
+ group_a_data = np.append(group_a_data, [80, 85, 15])
+
+ # Group B: Normal distribution, mean=60, std=15
+ group_b_data = np.random.normal(60, 15, 35)
+ # Add outliers
+ group_b_data = np.append(group_b_data, [100, 10])
+
+ # Group C: Normal distribution, mean=45, std=8
+ group_c_data = np.random.normal(45, 8, 45)
+
+ # Group D: Skewed distribution
+ group_d_data = np.random.gamma(2, 2, 30) + 40
+ # Add outliers
+ group_d_data = np.append(group_d_data, [75, 78, 20])
+
+ # Combine all data
+ for group, values in zip(
+ ['Group A', 'Group B', 'Group C', 'Group D'],
+ [group_a_data, group_b_data, group_c_data, group_d_data]
+ ):
+ data_dict['Group'].extend([group] * len(values))
+ data_dict['Value'].extend(values)
+
+ data = pd.DataFrame(data_dict)
+
+ # Create plot
+ chart = create_plot(
+ data,
+ values='Value',
+ groups='Group',
+ title='Statistical Distribution Comparison Across Groups',
+ ylabel='Measurement Value',
+ xlabel='Categories'
+ )
+
+ # Save as SVG
+ chart.render_to_file('plot.svg')
+ print("SVG plot saved to plot.svg")
+
+ # Also save as PNG if cairosvg is available
+ try:
+ chart.render_to_png('plot.png')
+ print("PNG plot saved to plot.png")
+ except ImportError:
+ print("Note: Install 'cairosvg' to export PNG images")
\ No newline at end of file
diff --git a/plots/seaborn/boxplot/box-basic/default.py b/plots/seaborn/boxplot/box-basic/default.py
new file mode 100644
index 00000000000..5fd27779763
--- /dev/null
+++ b/plots/seaborn/boxplot/box-basic/default.py
@@ -0,0 +1,187 @@
+"""
+box-basic: Basic Box Plot
+Implementation for: seaborn
+Variant: default
+Python: 3.10+
+"""
+
+import matplotlib.pyplot as plt
+import seaborn as sns
+import pandas as pd
+import numpy as np
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from matplotlib.figure import Figure
+
+
+def create_plot(
+ data: pd.DataFrame,
+ values: str,
+ groups: str,
+ title: Optional[str] = None,
+ xlabel: Optional[str] = None,
+ ylabel: Optional[str] = None,
+ palette: Optional[str] = 'Set2',
+ figsize: tuple[float, float] = (10, 6),
+ showfliers: bool = True,
+ **kwargs
+) -> Figure:
+ """
+ Create a basic box plot showing statistical distribution of multiple groups using seaborn.
+
+ Args:
+ data: Input DataFrame with required columns
+ values: Column name containing numeric values
+ groups: Column name containing group categories
+ title: Plot title (optional)
+ xlabel: Custom x-axis label (optional, defaults to groups column name)
+ ylabel: Custom y-axis label (optional, defaults to values column name)
+ palette: Color palette name for boxes (default: 'Set2')
+ figsize: Figure size as (width, height) in inches (default: (10, 6))
+ showfliers: Whether to show outliers (default: True)
+ **kwargs: Additional parameters passed to seaborn boxplot function
+
+ Returns:
+ Matplotlib Figure object
+
+ Raises:
+ ValueError: If data is empty
+ KeyError: If required columns not found
+
+ Example:
+ >>> data = pd.DataFrame({
+ ... 'Group': ['A', 'A', 'B', 'B', 'C', 'C'],
+ ... 'Value': [1, 2, 2, 3, 3, 4]
+ ... })
+ >>> fig = create_plot(data, values='Value', groups='Group')
+ """
+ # Input validation
+ if data.empty:
+ raise ValueError("Data cannot be empty")
+
+ # Check required columns
+ for col in [values, groups]:
+ if col not in data.columns:
+ available = ", ".join(data.columns)
+ raise KeyError(f"Column '{col}' not found. Available columns: {available}")
+
+ # Create figure
+ fig, ax = plt.subplots(figsize=figsize)
+
+ # Create boxplot with seaborn
+ sns.boxplot(
+ data=data,
+ x=groups,
+ y=values,
+ palette=palette,
+ ax=ax,
+ showfliers=showfliers,
+ width=0.7,
+ linewidth=1.5,
+ fliersize=6,
+ **kwargs
+ )
+
+ # Customize the appearance
+ # Set median line color to be more visible
+ for patch in ax.artists:
+ # Get the current face color
+ r, g, b, a = patch.get_facecolor()
+ # Set the box face color with some transparency
+ patch.set_facecolor((r, g, b, 0.7))
+ # Set edge color
+ patch.set_edgecolor('black')
+ patch.set_linewidth(1.2)
+
+ # Style the median lines
+ for line in ax.lines:
+ # Median lines are the ones inside the boxes
+ if line.get_linestyle() == '-' and line.get_marker() == 'None':
+ line.set_color('red')
+ line.set_linewidth(2)
+
+ # Labels and title
+ ax.set_xlabel(xlabel or groups)
+ ax.set_ylabel(ylabel or values)
+
+ if title:
+ ax.set_title(title, fontsize=14, fontweight='bold', pad=20)
+
+ # Grid for better readability
+ ax.grid(True, axis='y', alpha=0.3, linestyle='--')
+ ax.set_axisbelow(True)
+
+ # Rotate x-axis labels if there are many groups
+ unique_groups = data[groups].nunique()
+ if unique_groups > 5:
+ plt.xticks(rotation=45, ha='right')
+
+ # Add some statistical annotations
+ # Calculate and display the number of data points per group
+ group_counts = data.groupby(groups)[values].count()
+ y_bottom = ax.get_ylim()[0]
+ for i, (group_name, count) in enumerate(group_counts.items()):
+ ax.text(i, y_bottom, f'n={count}', ha='center', va='top', fontsize=9, alpha=0.7)
+
+ # Apply seaborn style for better aesthetics
+ sns.despine(ax=ax)
+
+ # Layout
+ plt.tight_layout()
+
+ return fig
+
+
+if __name__ == '__main__':
+ # Sample data for testing with different distributions per group
+ np.random.seed(42) # For reproducibility
+
+ # Generate sample data with 4 groups
+ data_dict = {
+ 'Group': [],
+ 'Value': []
+ }
+
+ # Group A: Normal distribution, mean=50, std=10
+ group_a_data = np.random.normal(50, 10, 40)
+ # Add some outliers
+ group_a_data = np.append(group_a_data, [80, 85, 15])
+
+ # Group B: Normal distribution, mean=60, std=15
+ group_b_data = np.random.normal(60, 15, 35)
+ # Add outliers
+ group_b_data = np.append(group_b_data, [100, 10])
+
+ # Group C: Normal distribution, mean=45, std=8
+ group_c_data = np.random.normal(45, 8, 45)
+
+ # Group D: Skewed distribution
+ group_d_data = np.random.gamma(2, 2, 30) + 40
+ # Add outliers
+ group_d_data = np.append(group_d_data, [75, 78, 20])
+
+ # Combine all data
+ for group, values in zip(
+ ['Group A', 'Group B', 'Group C', 'Group D'],
+ [group_a_data, group_b_data, group_c_data, group_d_data]
+ ):
+ data_dict['Group'].extend([group] * len(values))
+ data_dict['Value'].extend(values)
+
+ data = pd.DataFrame(data_dict)
+
+ # Create plot
+ fig = create_plot(
+ data,
+ values='Value',
+ groups='Group',
+ title='Statistical Distribution Comparison Across Groups',
+ ylabel='Measurement Value',
+ xlabel='Categories',
+ palette='Set2'
+ )
+
+ # Save for inspection
+ plt.savefig('plot.png', dpi=300, bbox_inches='tight')
+ print("Plot saved to plot.png")
\ No newline at end of file
diff --git a/specs/box-basic.md b/specs/box-basic.md
new file mode 100644
index 00000000000..0475384e3d8
--- /dev/null
+++ b/specs/box-basic.md
@@ -0,0 +1,54 @@
+# box-basic
+
+## Description
+A basic box plot (box-and-whisker plot) showing the statistical distribution of multiple groups. The plot displays quartiles (Q1, median, Q3) as boxes, whiskers extending to show the range within 1.5 * IQR (interquartile range), and individual points for outliers beyond the whiskers.
+
+## Data Requirements
+- **Structure**: One numeric column for values and one categorical column for groups
+- **Minimum Data**: At least 5 data points per group for meaningful statistics
+- **Data Types**:
+ - Values: Numeric (float or int)
+ - Groups: Categorical (string or numeric)
+
+## Visual Requirements
+### Core Elements
+- **Boxes**: Rectangle from Q1 to Q3 for each group
+- **Median Line**: Horizontal line at median within each box
+- **Whiskers**: Lines extending from box to min(Q3 + 1.5*IQR, max_value) and max(Q1 - 1.5*IQR, min_value)
+- **Outliers**: Individual points for values beyond whiskers
+- **X-axis**: Categorical groups
+- **Y-axis**: Value scale
+
+### Styling
+- **Colors**: Different colors for each box (optional, but enhances readability)
+- **Box Width**: Proportional to available space, with gaps between boxes
+- **Grid**: Horizontal grid lines for value reference
+- **Labels**: Clear axis labels and title
+
+## Implementation Requirements
+### Data Generation
+Generate sample data with:
+- 4-5 groups (e.g., "Group A", "Group B", "Group C", "Group D")
+- 30-50 data points per group
+- Different distributions per group (e.g., different means and spreads)
+- Some outliers in at least 2 groups
+- Use deterministic random seed for reproducibility
+
+### Key Features
+1. **Statistical Display**: Show quartiles, median, range, and outliers
+2. **Multiple Groups**: Compare distributions across categories
+3. **Clear Labeling**: Title, axis labels, and group names
+4. **Visual Clarity**: Distinguish boxes, whiskers, and outliers
+
+## Example Use Cases
+- Comparing performance metrics across different teams
+- Analyzing price distributions across product categories
+- Examining test scores across different classes
+- Visualizing sensor readings from multiple devices
+- Comparing response times across server regions
+
+## Notes
+- Box plots are excellent for comparing distributions and identifying outliers
+- They provide a compact summary of data distribution
+- Particularly useful when sample sizes vary across groups
+- Non-parametric visualization (doesn't assume normal distribution)
\ No newline at end of file
From e820c806c615bb5b263bac85f5a4c60e3a8e32c4 Mon Sep 17 00:00:00 2001
From: Markus Neusinger
Date: Fri, 28 Nov 2025 23:41:33 +0100
Subject: [PATCH 2/5] chore: update dependencies and improve code formatting
- Added PNG export dependencies for Altair, Plotly, and Bokeh
- Refactored code for consistency in string formatting and variable naming
- Enhanced box plot creation functions across multiple libraries
---
.../{box => boxplot}/box-basic/default.py | 155 ++++++------
.../{box => custom}/box-basic/default.py | 158 +++++-------
.../{box => boxplot}/box-basic/default.py | 146 +++++------
.../{box => boxplot}/box-basic/default.py | 73 +++---
plots/plotly/box/box-basic/default.py | 150 ++++++-----
.../{box => boxplot}/box-basic/default.py | 93 ++++---
plots/pygal/box/box-basic/default.py | 68 ++---
plots/seaborn/boxplot/box-basic/default.py | 64 ++---
pyproject.toml | 4 +
.../v1.0.0-draft/code-generation-rules.md | 192 +++++++++++++-
uv.lock | 234 ++++++++++++++++++
11 files changed, 828 insertions(+), 509 deletions(-)
rename plots/altair/{box => boxplot}/box-basic/default.py (53%)
rename plots/bokeh/{box => custom}/box-basic/default.py (63%)
rename plots/highcharts/{box => boxplot}/box-basic/default.py (67%)
rename plots/matplotlib/{box => boxplot}/box-basic/default.py (73%)
rename plots/plotnine/{box => boxplot}/box-basic/default.py (72%)
diff --git a/plots/altair/box/box-basic/default.py b/plots/altair/boxplot/box-basic/default.py
similarity index 53%
rename from plots/altair/box/box-basic/default.py
rename to plots/altair/boxplot/box-basic/default.py
index b180c81cbb9..6444f74d87c 100644
--- a/plots/altair/box/box-basic/default.py
+++ b/plots/altair/boxplot/box-basic/default.py
@@ -5,10 +5,12 @@
Python: 3.10+
"""
+from typing import TYPE_CHECKING, Optional
+
import altair as alt
-import pandas as pd
import numpy as np
-from typing import TYPE_CHECKING, Optional
+import pandas as pd
+
if TYPE_CHECKING:
from altair import Chart
@@ -21,10 +23,10 @@ def create_plot(
title: Optional[str] = None,
xlabel: Optional[str] = None,
ylabel: Optional[str] = None,
- color_scheme: str = 'set2',
+ color_scheme: str = "set2",
width: int = 600,
height: int = 400,
- **kwargs
+ **kwargs,
) -> Chart:
"""
Create a basic box plot showing statistical distribution of multiple groups using altair.
@@ -66,92 +68,72 @@ def create_plot(
raise KeyError(f"Column '{col}' not found. Available columns: {available}")
# Create the box plot using Altair's mark_boxplot
- base = alt.Chart(data).mark_boxplot(
- extent=1.5, # 1.5 * IQR for whiskers
- outliers=True,
- size=40,
- opacity=0.7
- ).encode(
- x=alt.X(
- f'{groups}:N',
- title=xlabel or groups,
- axis=alt.Axis(
- labelAngle=0 if data[groups].nunique() <= 5 else -45,
- labelLimit=200
- )
- ),
- y=alt.Y(
- f'{values}:Q',
- title=ylabel or values,
- scale=alt.Scale(zero=False)
- ),
- color=alt.Color(
- f'{groups}:N',
- scale=alt.Scale(scheme=color_scheme),
- legend=None # Hide legend as it's redundant with x-axis
- ),
- tooltip=[
- alt.Tooltip(f'{groups}:N', title='Group'),
- alt.Tooltip(f'count({values}):Q', title='Count'),
- alt.Tooltip(f'min({values}):Q', title='Min', format='.2f'),
- alt.Tooltip(f'q1({values}):Q', title='Q1', format='.2f'),
- alt.Tooltip(f'median({values}):Q', title='Median', format='.2f'),
- alt.Tooltip(f'q3({values}):Q', title='Q3', format='.2f'),
- alt.Tooltip(f'max({values}):Q', title='Max', format='.2f')
- ]
+ base = (
+ alt.Chart(data)
+ .mark_boxplot(
+ extent=1.5, # 1.5 * IQR for whiskers
+ outliers=True,
+ size=40,
+ opacity=0.7,
+ )
+ .encode(
+ x=alt.X(
+ f"{groups}:N",
+ title=xlabel or groups,
+ axis=alt.Axis(labelAngle=0 if data[groups].nunique() <= 5 else -45, labelLimit=200),
+ ),
+ y=alt.Y(f"{values}:Q", title=ylabel or values, scale=alt.Scale(zero=False)),
+ color=alt.Color(
+ f"{groups}:N",
+ scale=alt.Scale(scheme=color_scheme),
+ legend=None, # Hide legend as it's redundant with x-axis
+ ),
+ tooltip=[
+ alt.Tooltip(f"{groups}:N", title="Group"),
+ alt.Tooltip(f"count({values}):Q", title="Count"),
+ alt.Tooltip(f"min({values}):Q", title="Min", format=".2f"),
+ alt.Tooltip(f"q1({values}):Q", title="Q1", format=".2f"),
+ alt.Tooltip(f"median({values}):Q", title="Median", format=".2f"),
+ alt.Tooltip(f"q3({values}):Q", title="Q3", format=".2f"),
+ alt.Tooltip(f"max({values}):Q", title="Max", format=".2f"),
+ ],
+ )
)
# Add sample size annotations
- text = alt.Chart(data).mark_text(
- align='center',
- baseline='top',
- dy=10,
- fontSize=10,
- opacity=0.7
- ).encode(
- x=alt.X(f'{groups}:N'),
- y=alt.Y(f'min({values}):Q'),
- text=alt.Text('count():Q', format='d')
- ).transform_aggregate(
- count='count()',
- groupby=[groups]
+ text = (
+ alt.Chart(data)
+ .mark_text(align="center", baseline="top", dy=10, fontSize=10, opacity=0.7)
+ .encode(x=alt.X(f"{groups}:N"), y=alt.Y(f"min({values}):Q"), text=alt.Text("count():Q", format="d"))
+ .transform_aggregate(count="count()", groupby=[groups])
)
# Combine box plot with annotations
- chart = (base + text).properties(
- width=width,
- height=height,
- title=alt.TitleParams(
- text=title or 'Box Plot Distribution',
- fontSize=16,
- anchor='middle'
+ chart = (
+ (base + text)
+ .properties(
+ width=width,
+ height=height,
+ title=alt.TitleParams(text=title or "Box Plot Distribution", fontSize=16, anchor="middle"),
+ )
+ .configure_view(strokeWidth=0)
+ .configure_axis(grid=True, gridOpacity=0.3, gridDash=[3, 3], domainWidth=1, tickWidth=1)
+ .configure_boxplot(
+ median={"color": "red", "strokeWidth": 2},
+ box={"strokeWidth": 1.5},
+ outliers={"fill": "red", "fillOpacity": 0.5, "size": 50},
)
- ).configure_view(
- strokeWidth=0
- ).configure_axis(
- grid=True,
- gridOpacity=0.3,
- gridDash=[3, 3],
- domainWidth=1,
- tickWidth=1
- ).configure_boxplot(
- median=dict(color='red', strokeWidth=2),
- box=dict(strokeWidth=1.5),
- outliers=dict(fill='red', fillOpacity=0.5, size=50)
)
return chart
-if __name__ == '__main__':
+if __name__ == "__main__":
# Sample data for testing with different distributions per group
np.random.seed(42) # For reproducibility
# Generate sample data with 4 groups
- data_dict = {
- 'Group': [],
- 'Value': []
- }
+ data_dict = {"Group": [], "Value": []}
# Group A: Normal distribution, mean=50, std=10
group_a_data = np.random.normal(50, 10, 40)
@@ -173,28 +155,29 @@ def create_plot(
# Combine all data
for group, values in zip(
- ['Group A', 'Group B', 'Group C', 'Group D'],
- [group_a_data, group_b_data, group_c_data, group_d_data]
+ ["Group A", "Group B", "Group C", "Group D"],
+ [group_a_data, group_b_data, group_c_data, group_d_data],
+ strict=False,
):
- data_dict['Group'].extend([group] * len(values))
- data_dict['Value'].extend(values)
+ data_dict["Group"].extend([group] * len(values))
+ data_dict["Value"].extend(values)
data = pd.DataFrame(data_dict)
# Create plot
chart = create_plot(
data,
- values='Value',
- groups='Group',
- title='Statistical Distribution Comparison Across Groups',
- ylabel='Measurement Value',
- xlabel='Categories'
+ values="Value",
+ groups="Group",
+ title="Statistical Distribution Comparison Across Groups",
+ ylabel="Measurement Value",
+ xlabel="Categories",
)
# Save for inspection
- chart.save('plot.html')
+ chart.save("plot.html")
print("Interactive plot saved to plot.html")
# Also save as PNG
- chart.save('plot.png', scale_factor=2.0)
- print("Static plot saved to plot.png")
\ No newline at end of file
+ chart.save("plot.png", scale_factor=2.0)
+ print("Static plot saved to plot.png")
diff --git a/plots/bokeh/box/box-basic/default.py b/plots/bokeh/custom/box-basic/default.py
similarity index 63%
rename from plots/bokeh/box/box-basic/default.py
rename to plots/bokeh/custom/box-basic/default.py
index 17013b2f81b..1fb6f77ee37 100644
--- a/plots/bokeh/box/box-basic/default.py
+++ b/plots/bokeh/custom/box-basic/default.py
@@ -5,13 +5,14 @@
Python: 3.10+
"""
-from bokeh.plotting import figure, output_file, save
-from bokeh.models import ColumnDataSource, Whisker
-from bokeh.transform import factor_cmap
-import pandas as pd
-import numpy as np
from typing import TYPE_CHECKING, Optional
+import numpy as np
+import pandas as pd
+from bokeh.models import ColumnDataSource
+from bokeh.plotting import figure, output_file, save
+
+
if TYPE_CHECKING:
from bokeh.plotting import Figure
@@ -26,7 +27,7 @@ def create_plot(
colors: Optional[list] = None,
width: int = 1000,
height: int = 600,
- **kwargs
+ **kwargs,
) -> Figure:
"""
Create a basic box plot showing statistical distribution of multiple groups using bokeh.
@@ -72,14 +73,14 @@ def create_plot(
# Prepare data structures for box plot components
box_data = {
- 'groups': [],
- 'q1': [],
- 'q2': [],
- 'q3': [],
- 'upper': [],
- 'lower': [],
- 'outliers_x': [],
- 'outliers_y': []
+ "groups": [],
+ "q1": [],
+ "q2": [],
+ "q3": [],
+ "upper": [],
+ "lower": [],
+ "outliers_x": [],
+ "outliers_y": [],
}
for group in group_names:
@@ -95,106 +96,79 @@ def create_plot(
# Find outliers
outliers = group_data[(group_data < lower) | (group_data > upper)]
- box_data['groups'].append(group)
- box_data['q1'].append(q1)
- box_data['q2'].append(q2)
- box_data['q3'].append(q3)
- box_data['upper'].append(upper)
- box_data['lower'].append(lower)
+ box_data["groups"].append(group)
+ box_data["q1"].append(q1)
+ box_data["q2"].append(q2)
+ box_data["q3"].append(q3)
+ box_data["upper"].append(upper)
+ box_data["lower"].append(lower)
# Add outliers
for outlier in outliers:
- box_data['outliers_x'].append(group)
- box_data['outliers_y'].append(outlier)
+ box_data["outliers_x"].append(group)
+ box_data["outliers_y"].append(outlier)
# Create figure
p = figure(
x_range=group_names,
width=width,
height=height,
- title=title or 'Box Plot Distribution',
- toolbar_location='above',
- tools='pan,wheel_zoom,box_zoom,reset,save'
+ title=title or "Box Plot Distribution",
+ toolbar_location="above",
+ tools="pan,wheel_zoom,box_zoom,reset,save",
)
# Set colors
if not colors:
from bokeh.palettes import Set2_8
- colors = Set2_8[:len(group_names)]
+
+ colors = Set2_8[: len(group_names)]
# Draw boxes (Q1 to Q3) for each group
for i, group in enumerate(group_names):
- idx = box_data['groups'].index(group)
+ idx = box_data["groups"].index(group)
# Box from Q1 to Q3
p.vbar(
x=group,
width=0.5,
- bottom=box_data['q1'][idx],
- top=box_data['q3'][idx],
+ bottom=box_data["q1"][idx],
+ top=box_data["q3"][idx],
fill_color=colors[i % len(colors)],
- line_color='black',
- alpha=0.7
+ line_color="black",
+ alpha=0.7,
)
# Median line
- p.line(
- x=[i - 0.25, i + 0.25],
- y=[box_data['q2'][idx], box_data['q2'][idx]],
- line_color='red',
- line_width=2
- )
+ p.line(x=[i - 0.25, i + 0.25], y=[box_data["q2"][idx], box_data["q2"][idx]], line_color="red", line_width=2)
# Upper whisker
- p.line(
- x=[i, i],
- y=[box_data['q3'][idx], box_data['upper'][idx]],
- line_color='black',
- line_width=1
- )
+ p.line(x=[i, i], y=[box_data["q3"][idx], box_data["upper"][idx]], line_color="black", line_width=1)
# Upper whisker cap
p.line(
- x=[i - 0.1, i + 0.1],
- y=[box_data['upper'][idx], box_data['upper'][idx]],
- line_color='black',
- line_width=1.5
+ x=[i - 0.1, i + 0.1], y=[box_data["upper"][idx], box_data["upper"][idx]], line_color="black", line_width=1.5
)
# Lower whisker
- p.line(
- x=[i, i],
- y=[box_data['q1'][idx], box_data['lower'][idx]],
- line_color='black',
- line_width=1
- )
+ p.line(x=[i, i], y=[box_data["q1"][idx], box_data["lower"][idx]], line_color="black", line_width=1)
# Lower whisker cap
p.line(
- x=[i - 0.1, i + 0.1],
- y=[box_data['lower'][idx], box_data['lower'][idx]],
- line_color='black',
- line_width=1.5
+ x=[i - 0.1, i + 0.1], y=[box_data["lower"][idx], box_data["lower"][idx]], line_color="black", line_width=1.5
)
- # Draw outliers
- if box_data['outliers_x']:
- p.circle(
- x=box_data['outliers_x'],
- y=box_data['outliers_y'],
- size=8,
- color='red',
- alpha=0.5,
- line_color='black',
- line_width=1
- )
+ # Draw outliers using ColumnDataSource (required for categorical x-axis)
+ if box_data["outliers_x"]:
+ outlier_source = ColumnDataSource(data={"x": box_data["outliers_x"], "y": box_data["outliers_y"]})
+ p.scatter(x="x", y="y", source=outlier_source, size=8, color="red", alpha=0.5, line_color="black", line_width=1)
# Styling
p.xaxis.axis_label = xlabel or groups
p.yaxis.axis_label = ylabel or values
- p.title.text_font_size = '14pt'
- p.title.align = 'center'
+ p.title.text_font_size = "14pt"
+ p.title.align = "center"
# Grid
p.ygrid.grid_line_alpha = 0.3
@@ -203,30 +177,22 @@ def create_plot(
# Add sample size annotations
group_counts = data.groupby(groups)[values].count()
- for i, (group, count) in enumerate(group_counts.items()):
+ for i, (_group, count) in enumerate(group_counts.items()):
y_position = data[values].min() - (data[values].max() - data[values].min()) * 0.05
from bokeh.models import Label
- label = Label(
- x=i, y=y_position,
- text=f'n={count}',
- text_align='center',
- text_font_size='9pt',
- text_alpha=0.7
- )
+
+ label = Label(x=i, y=y_position, text=f"n={count}", text_align="center", text_font_size="9pt", text_alpha=0.7)
p.add_layout(label)
return p
-if __name__ == '__main__':
+if __name__ == "__main__":
# Sample data for testing with different distributions per group
np.random.seed(42) # For reproducibility
# Generate sample data with 4 groups
- data_dict = {
- 'Group': [],
- 'Value': []
- }
+ data_dict = {"Group": [], "Value": []}
# Group A: Normal distribution, mean=50, std=10
group_a_data = np.random.normal(50, 10, 40)
@@ -248,33 +214,35 @@ def create_plot(
# Combine all data
for group, values in zip(
- ['Group A', 'Group B', 'Group C', 'Group D'],
- [group_a_data, group_b_data, group_c_data, group_d_data]
+ ["Group A", "Group B", "Group C", "Group D"],
+ [group_a_data, group_b_data, group_c_data, group_d_data],
+ strict=False,
):
- data_dict['Group'].extend([group] * len(values))
- data_dict['Value'].extend(values)
+ data_dict["Group"].extend([group] * len(values))
+ data_dict["Value"].extend(values)
data = pd.DataFrame(data_dict)
# Create plot
fig = create_plot(
data,
- values='Value',
- groups='Group',
- title='Statistical Distribution Comparison Across Groups',
- ylabel='Measurement Value',
- xlabel='Categories'
+ values="Value",
+ groups="Group",
+ title="Statistical Distribution Comparison Across Groups",
+ ylabel="Measurement Value",
+ xlabel="Categories",
)
# Save for inspection
- output_file('plot.html')
+ output_file("plot.html")
save(fig)
print("Interactive plot saved to plot.html")
# Also export as PNG if possible
try:
from bokeh.io import export_png
- export_png(fig, filename='plot.png')
+
+ export_png(fig, filename="plot.png")
print("Static plot saved to plot.png")
except ImportError:
- print("Note: Install 'selenium' and 'pillow' to export PNG images")
\ No newline at end of file
+ print("Note: Install 'selenium' and 'pillow' to export PNG images")
diff --git a/plots/highcharts/box/box-basic/default.py b/plots/highcharts/boxplot/box-basic/default.py
similarity index 67%
rename from plots/highcharts/box/box-basic/default.py
rename to plots/highcharts/boxplot/box-basic/default.py
index 444abbccdd5..646aef624c9 100644
--- a/plots/highcharts/box/box-basic/default.py
+++ b/plots/highcharts/boxplot/box-basic/default.py
@@ -7,16 +7,13 @@
Note: Highcharts requires a license for commercial use.
"""
-from highcharts_core import Chart
+from typing import Optional
+
+import numpy as np
+import pandas as pd
+from highcharts_core.chart import Chart
from highcharts_core.options import HighchartsOptions
-from highcharts_core.options.plot_options.boxplot import BoxPlotOptions
from highcharts_core.options.series.boxplot import BoxPlotSeries
-import pandas as pd
-import numpy as np
-from typing import TYPE_CHECKING, Optional
-
-if TYPE_CHECKING:
- from highcharts_core import Chart
def create_plot(
@@ -28,7 +25,7 @@ def create_plot(
ylabel: Optional[str] = None,
colors: Optional[list] = None,
height: int = 600,
- **kwargs
+ **kwargs,
) -> Chart:
"""
Create a basic box plot showing statistical distribution of multiple groups using Highcharts.
@@ -100,119 +97,91 @@ def create_plot(
# Title
chart.options.title = {
- 'text': title or 'Box Plot Distribution',
- 'style': {
- 'fontSize': '16px',
- 'fontWeight': 'bold'
- }
+ "text": title or "Box Plot Distribution",
+ "style": {"fontSize": "16px", "fontWeight": "bold"},
}
# X-axis
- chart.options.x_axis = {
- 'categories': list(group_names),
- 'title': {
- 'text': xlabel or groups
- }
- }
+ chart.options.x_axis = {"categories": list(group_names), "title": {"text": xlabel or groups}}
# Y-axis
chart.options.y_axis = {
- 'title': {
- 'text': ylabel or values
- },
- 'gridLineWidth': 1,
- 'gridLineDashStyle': 'Dot',
- 'gridLineColor': '#e0e0e0'
+ "title": {"text": ylabel or values},
+ "gridLineWidth": 1,
+ "gridLineDashStyle": "Dot",
+ "gridLineColor": "#e0e0e0",
}
# Colors
if colors:
chart.options.colors = colors
else:
- chart.options.colors = ['#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854']
+ chart.options.colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854"]
# Plot options
chart.options.plot_options = {
- 'boxplot': {
- 'fillColor': None,
- 'lineWidth': 2,
- 'medianWidth': 3,
- 'medianColor': '#FF0000',
- 'stemWidth': 1,
- 'whiskerWidth': 2,
- 'whiskerLength': '50%'
+ "boxplot": {
+ "fillColor": None,
+ "lineWidth": 2,
+ "medianWidth": 3,
+ "medianColor": "#FF0000",
+ "stemWidth": 1,
+ "whiskerWidth": 2,
+ "whiskerLength": "50%",
}
}
# Tooltip
chart.options.tooltip = {
- 'shared': False,
- 'useHTML': True,
- 'headerFormat': '{point.key}
',
- 'pointFormat': (
- 'Max: {point.high}
'
- 'Q3: {point.q3}
'
+ "shared": False,
+ "useHTML": True,
+ "headerFormat": "{point.key}
",
+ "pointFormat": (
+ "Max: {point.high}
"
+ "Q3: {point.q3}
"
'Median: {point.median}
'
- 'Q1: {point.q1}
'
- 'Min: {point.low}
'
- )
+ "Q1: {point.q1}
"
+ "Min: {point.low}
"
+ ),
}
# Chart dimensions
- chart.options.chart = {
- 'type': 'boxplot',
- 'height': height,
- 'backgroundColor': 'white'
- }
+ chart.options.chart = {"type": "boxplot", "height": height, "backgroundColor": "white"}
# Add box plot series
- chart.add_series(BoxPlotSeries.from_array(
- data=box_data,
- name='Distribution',
- colorByPoint=True
- ))
+ chart.add_series(BoxPlotSeries.from_array(data=box_data, name="Distribution", colorByPoint=True))
# Add outliers as scatter series if any exist
if outliers_data:
from highcharts_core.options.series.scatter import ScatterSeries
- chart.add_series(ScatterSeries.from_array(
- data=outliers_data,
- name='Outliers',
- color='rgba(255, 0, 0, 0.5)',
- marker={
- 'fillColor': 'rgba(255, 0, 0, 0.5)',
- 'lineWidth': 1,
- 'lineColor': '#000000',
- 'radius': 4
- },
- tooltip={
- 'pointFormat': 'Outlier: {point.y}'
- }
- ))
+ chart.add_series(
+ ScatterSeries.from_array(
+ data=outliers_data,
+ name="Outliers",
+ color="rgba(255, 0, 0, 0.5)",
+ marker={"fillColor": "rgba(255, 0, 0, 0.5)", "lineWidth": 1, "lineColor": "#000000", "radius": 4},
+ tooltip={"pointFormat": "Outlier: {point.y}"},
+ )
+ )
# Legend
chart.options.legend = {
- 'enabled': False # Hide legend for cleaner look
+ "enabled": False # Hide legend for cleaner look
}
# Credits
- chart.options.credits = {
- 'enabled': False
- }
+ chart.options.credits = {"enabled": False}
return chart
-if __name__ == '__main__':
+if __name__ == "__main__":
# Sample data for testing with different distributions per group
np.random.seed(42) # For reproducibility
# Generate sample data with 4 groups
- data_dict = {
- 'Group': [],
- 'Value': []
- }
+ data_dict = {"Group": [], "Value": []}
# Group A: Normal distribution, mean=50, std=10
group_a_data = np.random.normal(50, 10, 40)
@@ -234,22 +203,23 @@ def create_plot(
# Combine all data
for group, values in zip(
- ['Group A', 'Group B', 'Group C', 'Group D'],
- [group_a_data, group_b_data, group_c_data, group_d_data]
+ ["Group A", "Group B", "Group C", "Group D"],
+ [group_a_data, group_b_data, group_c_data, group_d_data],
+ strict=False,
):
- data_dict['Group'].extend([group] * len(values))
- data_dict['Value'].extend(values)
+ data_dict["Group"].extend([group] * len(values))
+ data_dict["Value"].extend(values)
data = pd.DataFrame(data_dict)
# Create plot
chart = create_plot(
data,
- values='Value',
- groups='Group',
- title='Statistical Distribution Comparison Across Groups',
- ylabel='Measurement Value',
- xlabel='Categories'
+ values="Value",
+ groups="Group",
+ title="Statistical Distribution Comparison Across Groups",
+ ylabel="Measurement Value",
+ xlabel="Categories",
)
# Export to HTML
@@ -272,11 +242,11 @@ def create_plot(