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( """ - with open('plot.html', 'w') as f: + 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 + print("For static image export, use Highcharts Export Server or phantomjs") diff --git a/plots/matplotlib/box/box-basic/default.py b/plots/matplotlib/boxplot/box-basic/default.py similarity index 73% rename from plots/matplotlib/box/box-basic/default.py rename to plots/matplotlib/boxplot/box-basic/default.py index 7d67148a401..7b387029cb0 100644 --- a/plots/matplotlib/box/box-basic/default.py +++ b/plots/matplotlib/boxplot/box-basic/default.py @@ -5,10 +5,12 @@ Python: 3.10+ """ +from typing import TYPE_CHECKING, Optional + import matplotlib.pyplot as plt -import pandas as pd import numpy as np -from typing import TYPE_CHECKING, Optional +import pandas as pd + if TYPE_CHECKING: from matplotlib.figure import Figure @@ -23,7 +25,7 @@ def create_plot( ylabel: Optional[str] = None, colors: Optional[list] = None, figsize: tuple[float, float] = (10, 6), - **kwargs + **kwargs, ) -> Figure: """ Create a basic box plot showing statistical distribution of multiple groups. @@ -73,54 +75,53 @@ def create_plot( # Create boxplot bp = ax.boxplot( grouped_data, - labels=group_names, + tick_labels=group_names, patch_artist=True, # Enable filling boxes with colors showmeans=False, notch=False, widths=0.7, - **kwargs + **kwargs, ) # Apply colors if provided if colors: - for patch, color in zip(bp['boxes'], colors * len(bp['boxes'])): + for patch, color in zip(bp["boxes"], colors * len(bp["boxes"]), strict=False): 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): + default_colors = plt.cm.Set2(np.linspace(0, 1, len(bp["boxes"]))) + for patch, color in zip(bp["boxes"], default_colors, strict=False): 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 whisker in bp["whiskers"]: + whisker.set(color="#8B8B8B", linewidth=1.5, linestyle="-") - for cap in bp['caps']: - cap.set(color='#8B8B8B', linewidth=2) + for cap in bp["caps"]: + cap.set(color="#8B8B8B", linewidth=2) - for median in bp['medians']: - median.set(color='#FF0000', 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') + 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') + ax.set_title(title, fontsize=14, fontweight="bold") # Grid for better readability - ax.grid(True, axis='y', alpha=0.3, linestyle='--') + 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') + plt.xticks(rotation=45, ha="right") # Layout plt.tight_layout() @@ -128,16 +129,13 @@ def create_plot( return fig -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 - group_names = ['Group A', 'Group B', 'Group C', 'Group D'] - data_dict = { - 'Group': [], - 'Value': [] - } + 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) @@ -159,24 +157,25 @@ 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='Groups' + 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 + plt.savefig("plot.png", dpi=300, bbox_inches="tight") + print("Plot saved to plot.png") diff --git a/plots/plotly/box/box-basic/default.py b/plots/plotly/box/box-basic/default.py index 4c927a28a0d..4ff2869e2a4 100644 --- a/plots/plotly/box/box-basic/default.py +++ b/plots/plotly/box/box-basic/default.py @@ -5,11 +5,12 @@ Python: 3.10+ """ -import plotly.graph_objects as go -import plotly.express as px -import pandas as pd +from typing import TYPE_CHECKING, Optional + import numpy as np -from typing import TYPE_CHECKING, Optional, Union +import pandas as pd +import plotly.express as px + if TYPE_CHECKING: from plotly.graph_objects import Figure @@ -26,7 +27,7 @@ def create_plot( height: int = 600, width: int = 1000, showlegend: bool = False, - **kwargs + **kwargs, ) -> Figure: """ Create an interactive box plot showing statistical distribution of multiple groups using plotly. @@ -76,102 +77,90 @@ def create_plot( color=groups, color_discrete_sequence=color_discrete_sequence or px.colors.qualitative.Set2, notched=False, - points='outliers', # Show only outliers as points - **kwargs + 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), + boxmean="sd", # Show mean and standard deviation + marker={"size": 8, "opacity": 0.5, "line": {"width": 1}}, + line={"width": 1.5}, fillcolor=None, - opacity=0.7 + 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', + title={ + "text": title or "Box Plot Distribution", + "font": {"size": 16, "family": "Arial, sans-serif"}, + "x": 0.5, + "xanchor": "center", + }, + xaxis={ + "title": xlabel or groups, + "gridcolor": "lightgray", + "gridwidth": 0.5, + "showgrid": False, + "zeroline": False, + }, + yaxis={ + "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" - ) + hovermode="x unified", + hoverlabel={"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()): + 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' - ) + { + "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": {"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}
' + - '' + hovertemplate="%{x}
" + + "Max: %{y}
" + + "Q3: %{upperfence}
" + + "Median: %{median}
" + + "Q1: %{lowerfence}
" + + "Min: %{y}
" + + "" ) return fig -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) @@ -193,26 +182,27 @@ 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 - fig.write_html('plot.html') - fig.write_image('plot.png', width=1000, height=600, scale=2) + 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 + print("Static plot saved to plot.png") diff --git a/plots/plotnine/box/box-basic/default.py b/plots/plotnine/boxplot/box-basic/default.py similarity index 72% rename from plots/plotnine/box/box-basic/default.py rename to plots/plotnine/boxplot/box-basic/default.py index b85e1554e72..82114200ffa 100644 --- a/plots/plotnine/box/box-basic/default.py +++ b/plots/plotnine/boxplot/box-basic/default.py @@ -5,13 +5,22 @@ Python: 3.10+ """ +from typing import TYPE_CHECKING, Optional + +import numpy as np +import pandas as pd from plotnine import ( - ggplot, aes, geom_boxplot, theme, element_text, element_line, - labs, theme_minimal, scale_fill_brewer, coord_cartesian + aes, + element_line, + element_text, + geom_boxplot, + ggplot, + labs, + scale_fill_brewer, + theme, + theme_minimal, ) -import pandas as pd -import numpy as np -from typing import TYPE_CHECKING, Optional + if TYPE_CHECKING: from plotnine import ggplot as GGPlot @@ -24,11 +33,11 @@ def create_plot( title: Optional[str] = None, xlabel: Optional[str] = None, ylabel: Optional[str] = None, - fill_palette: str = 'Set2', + fill_palette: str = "Set2", width: int = 10, height: int = 6, show_outliers: bool = True, - **kwargs + **kwargs, ) -> GGPlot: """ Create a basic box plot showing statistical distribution of multiple groups using plotnine (ggplot2 syntax). @@ -77,75 +86,58 @@ def create_plot( alpha=0.7, outlier_alpha=0.5 if show_outliers else 0, outlier_size=2, - outlier_color='red', + 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 + **kwargs, ) + + scale_fill_brewer(type="qual", 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'), + 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) + 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') - ) + 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 + from plotnine import geom_text # Calculate group statistics for annotations - group_stats = data.groupby(groups).agg( - count=(values, 'count'), - min_val=(values, 'min') - ).reset_index() + 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) + 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' + 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__': +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) @@ -167,24 +159,25 @@ 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 plot = 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 - plot.save('plot.png', dpi=300, verbose=False) - print("Plot saved to plot.png") \ No newline at end of file + plot.save("plot.png", dpi=300, verbose=False) + print("Plot saved to plot.png") diff --git a/plots/pygal/box/box-basic/default.py b/plots/pygal/box/box-basic/default.py index 999a0b7c7b9..d07d6d8779a 100644 --- a/plots/pygal/box/box-basic/default.py +++ b/plots/pygal/box/box-basic/default.py @@ -5,11 +5,13 @@ Python: 3.10+ """ +from typing import TYPE_CHECKING, Optional + +import numpy as np +import pandas as pd 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 @@ -25,7 +27,7 @@ def create_plot( width: int = 800, height: int = 600, show_legend: bool = True, - **kwargs + **kwargs, ) -> Box: """ Create a basic box plot showing statistical distribution of multiple groups using pygal. @@ -68,32 +70,32 @@ def create_plot( # Create custom style custom_style = Style( - background='white', - plot_background='white', - foreground='#333', - foreground_strong='#333', - foreground_subtle='#555', + 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' + 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', + 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) + box_mode="tukey", # Use Tukey method (1.5 * IQR for whiskers) print_values=False, print_zeroes=False, - **kwargs + **kwargs, ) # Calculate box plot data for each group @@ -108,20 +110,17 @@ def create_plot( values_list = group_data.tolist() # Add the series with label - box_chart.add(f'{group} (n={len(values_list)})', values_list) + box_chart.add(f"{group} (n={len(values_list)})", values_list) return box_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) @@ -143,31 +142,32 @@ 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 as SVG - chart.render_to_file('plot.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') + 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 + print("Note: Install 'cairosvg' to export PNG images") diff --git a/plots/seaborn/boxplot/box-basic/default.py b/plots/seaborn/boxplot/box-basic/default.py index 5fd27779763..11ab38c8034 100644 --- a/plots/seaborn/boxplot/box-basic/default.py +++ b/plots/seaborn/boxplot/box-basic/default.py @@ -5,11 +5,13 @@ Python: 3.10+ """ +from typing import TYPE_CHECKING, Optional + import matplotlib.pyplot as plt -import seaborn as sns -import pandas as pd import numpy as np -from typing import TYPE_CHECKING, Optional +import pandas as pd +import seaborn as sns + if TYPE_CHECKING: from matplotlib.figure import Figure @@ -22,10 +24,10 @@ def create_plot( title: Optional[str] = None, xlabel: Optional[str] = None, ylabel: Optional[str] = None, - palette: Optional[str] = 'Set2', + palette: Optional[str] = "Set2", figsize: tuple[float, float] = (10, 6), showfliers: bool = True, - **kwargs + **kwargs, ) -> Figure: """ Create a basic box plot showing statistical distribution of multiple groups using seaborn. @@ -74,13 +76,15 @@ def create_plot( data=data, x=groups, y=values, + hue=groups, palette=palette, ax=ax, showfliers=showfliers, width=0.7, linewidth=1.5, fliersize=6, - **kwargs + legend=False, + **kwargs, ) # Customize the appearance @@ -91,14 +95,14 @@ def create_plot( # 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_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') + if line.get_linestyle() == "-" and line.get_marker() == "None": + line.set_color("red") line.set_linewidth(2) # Labels and title @@ -106,23 +110,23 @@ def create_plot( ax.set_ylabel(ylabel or values) if title: - ax.set_title(title, fontsize=14, fontweight='bold', pad=20) + ax.set_title(title, fontsize=14, fontweight="bold", pad=20) # Grid for better readability - ax.grid(True, axis='y', alpha=0.3, linestyle='--') + 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') + 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) + 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) @@ -133,15 +137,12 @@ def create_plot( return fig -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) @@ -163,25 +164,26 @@ 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', - palette='Set2' + 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 + plt.savefig("plot.png", dpi=300, bbox_inches="tight") + print("Plot saved to plot.png") diff --git a/pyproject.toml b/pyproject.toml index fb45f97fb8e..d8b51e97e41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,10 @@ plotting = [ "plotnine>=0.13.0", "pygal>=3.0.0", "highcharts-core>=1.10.0", + # PNG export dependencies + "vl-convert-python>=1.3.0", # altair PNG export + "kaleido>=0.2.1", # plotly PNG export + "selenium>=4.15.0", # bokeh PNG export ] all = [ "pyplots[test,dev,plotting,typecheck]", diff --git a/rules/generation/v1.0.0-draft/code-generation-rules.md b/rules/generation/v1.0.0-draft/code-generation-rules.md index 2854167fd66..453b80630ea 100644 --- a/rules/generation/v1.0.0-draft/code-generation-rules.md +++ b/rules/generation/v1.0.0-draft/code-generation-rules.md @@ -19,7 +19,7 @@ Define how to generate plot implementation code from Markdown specifications. ### Required 1. **Spec Markdown**: Complete spec file from `specs/{spec-id}.md` -2. **Target Library**: matplotlib, seaborn, plotly, bokeh, or altair +2. **Target Library**: matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, or highcharts 3. **Variant**: default, {style}_style, or py{version} ### Optional @@ -31,6 +31,23 @@ Define how to generate plot implementation code from Markdown specifications. ## Output Requirements +### Directory Structure + +The folder name must match the library's API function name for the plot type: + +| Library | Function | Folder Example | +|---------|----------|----------------| +| matplotlib | `ax.boxplot()` | `plots/matplotlib/boxplot/` | +| seaborn | `sns.boxplot()` | `plots/seaborn/boxplot/` | +| plotly | `go.Box()` | `plots/plotly/box/` | +| pygal | `pygal.Box()` | `plots/pygal/box/` | +| altair | `mark_boxplot()` | `plots/altair/boxplot/` | +| plotnine | `geom_boxplot()` | `plots/plotnine/boxplot/` | +| highcharts | `BoxPlotSeries` | `plots/highcharts/boxplot/` | + +**Fallback for libraries without native function:** Use `custom/` +- Example: Bokeh has no native boxplot → `plots/bokeh/custom/` + ### File Structure ```python @@ -156,19 +173,24 @@ From `docs/architecture/specs-guide.md`: ### Step 2: Library Selection -From `docs/workflow.md` (lines 119-127): +From `docs/workflow.md`: -**Rules**: +**Supported Libraries**: - **matplotlib**: Always implement (universal support) -- **seaborn**: Auto-select for: heatmap, violin, box, pair plots, distributions -- **plotly**: Auto-select for: interactive needs, 3D plots, animations -- **bokeh/altair**: Future support +- **seaborn**: Statistical visualizations (heatmap, violin, box, pair plots, distributions) +- **plotly**: Interactive plots, 3D plots, animations +- **bokeh**: Interactive web-based visualizations +- **altair**: Declarative statistical visualization +- **plotnine**: ggplot2-style plotting (Grammar of Graphics) +- **pygal**: SVG-based charts for web +- **highcharts**: Professional web charts (requires license for commercial use) **Selection Logic**: ``` -if spec mentions "interactive" → plotly +if spec mentions "interactive" → plotly, bokeh else if plot_type in ["heatmap", "violin", "box", "pair"] → seaborn + matplotlib -else → matplotlib (default) +else if spec mentions "ggplot" or "grammar of graphics" → plotnine +else → all libraries (default) ``` ### Step 3: Code Structure @@ -272,6 +294,160 @@ fig.update_layout( return fig ``` +### bokeh + +```python +from bokeh.plotting import figure, output_file, save +from bokeh.models import ColumnDataSource + +# Create figure with categorical x-axis +p = figure(x_range=categories, ...) + +# IMPORTANT: For categorical axes, use ColumnDataSource +source = ColumnDataSource(data={'x': cat_data, 'y': num_data}) +p.scatter(x='x', y='y', source=source) # Use scatter, not circle with categorical + +# Save output +output_file('plot.html') +save(p) + +# PNG export (requires selenium) +try: + from bokeh.io import export_png + export_png(p, filename='plot.png') +except ImportError: + print("Note: Install 'selenium' for PNG export") +``` + +### altair + +```python +import altair as alt + +# Create chart +chart = alt.Chart(data).mark_point().encode(x='x:Q', y='y:Q') + +# Save HTML (always works) +chart.save('plot.html') + +# PNG export (requires vl-convert-python) +try: + chart.save('plot.png', scale_factor=2.0) +except Exception: + print("Note: Install 'vl-convert-python' for PNG export") +``` + +### plotnine + +```python +from plotnine import ggplot, aes, scale_fill_brewer + +# IMPORTANT: Palette types must match the palette name +# - Qualitative: Set1, Set2, Set3, Paired, Pastel1, Pastel2, Dark2, Accent +# - Sequential: Blues, Greens, Reds, Oranges, Purples, Greys, etc. +# - Diverging: RdBu, PiYG, PRGn, BrBG, RdYlBu, etc. + +# ✅ Correct: Set2 is qualitative ++ scale_fill_brewer(type='qual', palette='Set2') + +# ❌ Wrong: Set2 is NOT sequential ++ scale_fill_brewer(type='seq', palette='Set2') +``` + +### highcharts + +**Note:** Highcharts requires a license for commercial use. + +```python +# IMPORTANT: Use correct import path +from highcharts_core.chart import Chart # ✅ Correct +# NOT: from highcharts_core import Chart # ❌ Wrong + +from highcharts_core.options import HighchartsOptions + +# Create chart +chart = Chart() +chart.options = HighchartsOptions() + +# Export to HTML (always works) +html_str = chart.to_js_literal() + +# Static image export requires Highcharts Export Server +``` + +### pygal + +```python +import pygal + +# Create chart +chart = pygal.Bar() +chart.title = 'Title' +chart.add('Series', [1, 2, 3]) + +# Save as SVG (native format) +chart.render_to_file('plot.svg') + +# PNG export (requires cairosvg) +try: + chart.render_to_png('plot.png') +except ImportError: + print("Note: Install 'cairosvg' for PNG export") +``` + +--- + +## API Version Compatibility + +### matplotlib 3.9+ + +```python +# DEPRECATED: labels parameter in boxplot +ax.boxplot(data, labels=group_names) # ❌ Deprecated + +# USE: tick_labels parameter +ax.boxplot(data, tick_labels=group_names) # ✅ Correct +``` + +### seaborn 0.14+ + +```python +# When using palette, always specify hue +# Otherwise seaborn raises a warning + +# ❌ Warning: palette without hue +sns.boxplot(data=df, x='group', y='value', palette='Set2') + +# ✅ Correct: hue with palette +sns.boxplot(data=df, x='group', y='value', hue='group', palette='Set2', legend=False) +``` + +--- + +## Code Quality Checks (Required Before PR) + +Before creating a pull request, **always** run these checks and fix any issues: + +```bash +# 1. Check for linting issues +uv run ruff check . + +# 2. Auto-fix issues (safe fixes) +uv run ruff check . --fix + +# 3. Auto-fix with unsafe fixes if needed +uv run ruff check . --fix --unsafe-fixes + +# 4. Format code +uv run ruff format . +``` + +**Common issues to watch for:** +- `C408`: Use dict literals `{}` instead of `dict()` +- `B905`: Add `strict=False` to `zip()` calls +- `B007`: Prefix unused loop variables with `_` (e.g., `_group`) +- Import ordering (auto-fixed by ruff) + --- ## Self-Optimization Loop diff --git a/uv.lock b/uv.lock index 146d9fd6041..ace9121e1e6 100644 --- a/uv.lock +++ b/uv.lock @@ -154,6 +154,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -188,6 +205,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "choreographer" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "logistro" }, + { name = "simplejson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/47/64a035c6f764450ea9f902cbeba14c8c70316c2641125510066d8f912bfa/choreographer-1.2.1.tar.gz", hash = "sha256:022afd72b1e9b0bcb950420b134e70055a294c791b6f36cfb47d89745b701b5f", size = 43399, upload-time = "2025-11-09T23:04:44.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/9f/d73dfb85d7a5b1a56a99adc50f2074029468168c970ff5daeade4ad819e4/choreographer-1.2.1-py3-none-any.whl", hash = "sha256:9af5385effa3c204dbc337abf7ac74fd8908ced326a15645dc31dde75718c77e", size = 49338, upload-time = "2025-11-09T23:04:43.154Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -652,6 +682,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "kaleido" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "choreographer" }, + { name = "logistro" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pytest-timeout" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/76eec859b71eda803a88ea50ed3f270281254656bb23d19eb0a39aa706a0/kaleido-1.2.0.tar.gz", hash = "sha256:fa621a14423e8effa2895a2526be00af0cf21655be1b74b7e382c171d12e71ef", size = 64160, upload-time = "2025-11-04T21:24:23.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/97/f6de8d4af54d6401d6581a686cce3e3e2371a79ba459a449104e026c08bc/kaleido-1.2.0-py3-none-any.whl", hash = "sha256:c27ed82b51df6b923d0e656feac221343a0dbcd2fb9bc7e6b1db97f61e9a1513", size = 68997, upload-time = "2025-11-04T21:24:21.704Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.9" @@ -686,6 +732,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, ] +[[package]] +name = "logistro" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/90/bfd7a6fab22bdfafe48ed3c4831713cb77b4779d18ade5e248d5dbc0ca22/logistro-2.0.1.tar.gz", hash = "sha256:8446affc82bab2577eb02bfcbcae196ae03129287557287b6a070f70c1985047", size = 8398, upload-time = "2025-11-01T02:41:18.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/6aa79ba3570bddd1bf7e951c6123f806751e58e8cce736bad77b2cf348d7/logistro-2.0.1-py3-none-any.whl", hash = "sha256:06ffa127b9fb4ac8b1972ae6b2a9d7fde57598bf5939cd708f43ec5bba2d31eb", size = 8555, upload-time = "2025-11-01T02:41:17.587Z" }, +] + [[package]] name = "mako" version = "1.3.10" @@ -853,6 +908,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, ] +[[package]] +name = "orjson" +version = "3.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188, upload-time = "2025-10-24T15:50:38.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/e3/54ff63c093cc1697e758e4fceb53164dd2661a7d1bcd522260ba09f54533/orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", size = 243501, upload-time = "2025-10-24T15:49:54.288Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7d/e2d1076ed2e8e0ae9badca65bf7ef22710f93887b29eaa37f09850604e09/orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", size = 128862, upload-time = "2025-10-24T15:49:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/9f/37/ca2eb40b90621faddfa9517dfe96e25f5ae4d8057a7c0cdd613c17e07b2c/orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", size = 130047, upload-time = "2025-10-24T15:49:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/62/1021ed35a1f2bad9040f05fa4cc4f9893410df0ba3eaa323ccf899b1c90a/orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", size = 129073, upload-time = "2025-10-24T15:49:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3f/f84d966ec2a6fd5f73b1a707e7cd876813422ae4bf9f0145c55c9c6a0f57/orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", size = 136597, upload-time = "2025-10-24T15:50:00.12Z" }, + { url = "https://files.pythonhosted.org/packages/32/78/4fa0aeca65ee82bbabb49e055bd03fa4edea33f7c080c5c7b9601661ef72/orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", size = 137515, upload-time = "2025-10-24T15:50:01.57Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9d/0c102e26e7fde40c4c98470796d050a2ec1953897e2c8ab0cb95b0759fa2/orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", size = 136703, upload-time = "2025-10-24T15:50:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/2de7188705b4cdfaf0b6c97d2f7849c17d2003232f6e70df98602173f788/orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", size = 136311, upload-time = "2025-10-24T15:50:04.441Z" }, + { url = "https://files.pythonhosted.org/packages/e0/52/847fcd1a98407154e944feeb12e3b4d487a0e264c40191fb44d1269cbaa1/orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", size = 140127, upload-time = "2025-10-24T15:50:07.398Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ae/21d208f58bdb847dd4d0d9407e2929862561841baa22bdab7aea10ca088e/orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", size = 406201, upload-time = "2025-10-24T15:50:08.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/55/0789d6de386c8366059db098a628e2ad8798069e94409b0d8935934cbcb9/orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", size = 149872, upload-time = "2025-10-24T15:50:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1d/7ff81ea23310e086c17b41d78a72270d9de04481e6113dbe2ac19118f7fb/orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", size = 139931, upload-time = "2025-10-24T15:50:11.623Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/25b886252c50ed64be68c937b562b2f2333b45afe72d53d719e46a565a50/orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", size = 136065, upload-time = "2025-10-24T15:50:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/63/b8/718eecf0bb7e9d64e4956afaafd23db9f04c776d445f59fe94f54bdae8f0/orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", size = 131310, upload-time = "2025-10-24T15:50:14.46Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bf/def5e25d4d8bfce296a9a7c8248109bf58622c21618b590678f945a2c59c/orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", size = 126151, upload-time = "2025-10-24T15:50:15.878Z" }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1068,6 +1158,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + [[package]] name = "pydantic" version = "2.12.4" @@ -1181,6 +1280,7 @@ all = [ { name = "bokeh" }, { name = "highcharts-core" }, { name = "httpx" }, + { name = "kaleido" }, { name = "mypy" }, { name = "plotly" }, { name = "plotnine" }, @@ -1191,6 +1291,8 @@ all = [ { name = "pytest-cov" }, { name = "ruff" }, { name = "seaborn" }, + { name = "selenium" }, + { name = "vl-convert-python" }, ] dev = [ { name = "pre-commit" }, @@ -1200,10 +1302,13 @@ plotting = [ { name = "altair" }, { name = "bokeh" }, { name = "highcharts-core" }, + { name = "kaleido" }, { name = "plotly" }, { name = "plotnine" }, { name = "pygal" }, { name = "seaborn" }, + { name = "selenium" }, + { name = "vl-convert-python" }, ] test = [ { name = "httpx" }, @@ -1227,6 +1332,7 @@ requires-dist = [ { name = "highcharts-core", marker = "extra == 'plotting'", specifier = ">=1.10.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "httpx", marker = "extra == 'test'", specifier = ">=0.27.0" }, + { name = "kaleido", marker = "extra == 'plotting'", specifier = ">=0.2.1" }, { name = "matplotlib", specifier = ">=3.8.0" }, { name = "mypy", marker = "extra == 'typecheck'", specifier = ">=1.8.0" }, { name = "numpy", specifier = ">=1.26.0" }, @@ -1246,11 +1352,22 @@ requires-dist = [ { name = "pyyaml", specifier = ">=6.0.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11.13" }, { name = "seaborn", marker = "extra == 'plotting'", specifier = ">=0.13.0" }, + { name = "selenium", marker = "extra == 'plotting'", specifier = ">=4.15.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" }, + { name = "vl-convert-python", marker = "extra == 'plotting'", specifier = ">=1.3.0" }, ] provides-extras = ["test", "dev", "typecheck", "plotting", "all"] +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + [[package]] name = "pytest" version = "9.0.1" @@ -1293,6 +1410,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1497,6 +1626,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, ] +[[package]] +name = "selenium" +version = "4.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "trio" }, + { name = "trio-websocket" }, + { name = "typing-extensions" }, + { name = "urllib3", extra = ["socks"] }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/a0/60a5e7e946420786d57816f64536e21a29f0554706b36f3cba348107024c/selenium-4.38.0.tar.gz", hash = "sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c", size = 924101, upload-time = "2025-10-25T02:13:06.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/d3/76c8f4a8d99b9f1ebcf9a611b4dd992bf5ee082a6093cfc649af3d10f35b/selenium-4.38.0-py3-none-any.whl", hash = "sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd", size = 9694571, upload-time = "2025-10-25T02:13:04.417Z" }, +] + +[[package]] +name = "simplejson" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1515,6 +1670,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.44" @@ -1585,6 +1749,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, ] +[[package]] +name = "trio" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/ce/0041ddd9160aac0031bcf5ab786c7640d795c797e67c438e15cfedf815c8/trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", size = 605323, upload-time = "2025-10-31T07:18:17.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" }, +] + +[[package]] +name = "trio-websocket" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "outcome" }, + { name = "trio" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" }, +] + [[package]] name = "types-pytz" version = "2025.2.0.20251108" @@ -1633,6 +1828,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + [[package]] name = "uvicorn" version = "0.38.0" @@ -1703,6 +1903,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] +[[package]] +name = "vl-convert-python" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/08/06945bff9655c5b0520a8d1b2550cd8007e106ebec45a33840035420e0d2/vl_convert_python-1.8.0.tar.gz", hash = "sha256:ceca613ca5551c55270a15ca48d0f3a7de1e949e0f127310e9b0f6570ea3fbbb", size = 4651586, upload-time = "2025-05-28T00:06:47.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5a/9dca7d8ff56e82c298e9ef381cfc803e262b85b7c59f2515d0e9f81a75b6/vl_convert_python-1.8.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f663317fc280b07553534195c1e31c4ca882d9c8601430211b078196db5ed227", size = 29956698, upload-time = "2025-05-28T00:06:29.533Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/325e6b5895482b2534e7462c012f237c66ffb02fb3af45eec0accab2f8d4/vl_convert_python-1.8.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:81f6380019ceadf070a79f85aa624475a6568093f70de0e151a32e91ecbcaacf", size = 28831173, upload-time = "2025-05-28T00:06:32.925Z" }, + { url = "https://files.pythonhosted.org/packages/09/fa/1dd944c9e9898e59e31c385bdce215aca543acc555de20b8bf4dc60ddb89/vl_convert_python-1.8.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3388e3913287867b3553c10f81ca2d85268216a5a75e7c71b9c1b59887c1977e", size = 31668750, upload-time = "2025-05-28T00:06:36.158Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/48f6d47a92eaf6f0dd235146307a7eb0d179b78d2faebc53aca3f1e49177/vl_convert_python-1.8.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b51264998e8fcc43dbce801484a950cfe6513cdc4c46b20604ef50989855a617", size = 32970141, upload-time = "2025-05-28T00:06:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6f/29dce05f9167e3a01ab74d79eeadd531bc24cf59e3a7fc3736af476ca431/vl_convert_python-1.8.0-cp37-abi3-win_amd64.whl", hash = "sha256:9f1146b791ed27916f54c45e1d66af53a40eb26e5aaea1892f33eb9a935039ab", size = 31318167, upload-time = "2025-05-28T00:06:44.881Z" }, +] + [[package]] name = "watchfiles" version = "1.1.1" @@ -1737,6 +1950,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, ] +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + [[package]] name = "websockets" version = "15.0.1" @@ -1746,6 +1968,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] +[[package]] +name = "wsproto" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, +] + [[package]] name = "xyzservices" version = "2025.11.0" From 7d3844899aa812b3e670cf7b4f041d664675bc0c Mon Sep 17 00:00:00 2001 From: Markus Neusinger Date: Sat, 29 Nov 2025 00:02:42 +0100 Subject: [PATCH 3/5] feat: enhance box plot generation and export functionality - Refactor box plot data preparation for improved clarity - Implement outlier detection and visualization - Update export method to save plots as PNG using Selenium - Remove HTML export and related code --- plots/altair/boxplot/box-basic/default.py | 8 +- plots/bokeh/custom/box-basic/default.py | 172 ++++++++---------- plots/highcharts/boxplot/box-basic/default.py | 73 +++++--- plots/plotly/box/box-basic/default.py | 6 +- plots/plotnine/boxplot/box-basic/default.py | 2 +- plots/pygal/box/box-basic/default.py | 13 +- pyproject.toml | 1 + .../v1.0.0-draft/code-generation-rules.md | 22 +++ uv.lock | 90 +++++++++ 9 files changed, 248 insertions(+), 139 deletions(-) diff --git a/plots/altair/boxplot/box-basic/default.py b/plots/altair/boxplot/box-basic/default.py index 6444f74d87c..00787ef05d3 100644 --- a/plots/altair/boxplot/box-basic/default.py +++ b/plots/altair/boxplot/box-basic/default.py @@ -174,10 +174,6 @@ def create_plot( xlabel="Categories", ) - # Save for inspection - chart.save("plot.html") - print("Interactive plot saved to plot.html") - - # Also save as PNG + # Save as PNG chart.save("plot.png", scale_factor=2.0) - print("Static plot saved to plot.png") + print("Plot saved to plot.png") diff --git a/plots/bokeh/custom/box-basic/default.py b/plots/bokeh/custom/box-basic/default.py index 1fb6f77ee37..5701d303d71 100644 --- a/plots/bokeh/custom/box-basic/default.py +++ b/plots/bokeh/custom/box-basic/default.py @@ -9,9 +9,8 @@ import numpy as np import pandas as pd -from bokeh.models import ColumnDataSource -from bokeh.plotting import figure, output_file, save - +from bokeh.models import ColumnDataSource, FixedTicker, Label, Whisker +from bokeh.plotting import figure if TYPE_CHECKING: from bokeh.plotting import Figure @@ -70,47 +69,44 @@ def create_plot( # Calculate box plot statistics for each group group_names = sorted(data[groups].unique()) + n_groups = len(group_names) + + # Prepare data for box plot + stats = {"x": [], "q1": [], "q2": [], "q3": [], "upper": [], "lower": [], "group": []} + outliers = {"x": [], "y": []} - # Prepare data structures for box plot components - box_data = { - "groups": [], - "q1": [], - "q2": [], - "q3": [], - "upper": [], - "lower": [], - "outliers_x": [], - "outliers_y": [], - } - - for group in group_names: + for i, group in enumerate(group_names): group_data = data[data[groups] == group][values].dropna() q1 = group_data.quantile(0.25) - q2 = group_data.quantile(0.5) # median + q2 = group_data.quantile(0.5) 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) + stats["x"].append(i) + stats["q1"].append(q1) + stats["q2"].append(q2) + stats["q3"].append(q3) + stats["upper"].append(upper) + stats["lower"].append(lower) + stats["group"].append(group) + # Find outliers - outliers = group_data[(group_data < lower) | (group_data > upper)] + outlier_data = group_data[(group_data < lower) | (group_data > upper)] + for val in outlier_data: + outliers["x"].append(i) + outliers["y"].append(val) - 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) + # Set colors + if not colors: + from bokeh.palettes import Set2_8 - # Add outliers - for outlier in outliers: - box_data["outliers_x"].append(group) - box_data["outliers_y"].append(outlier) + colors = Set2_8[:n_groups] - # Create figure + # Create figure with numeric x-axis p = figure( - x_range=group_names, width=width, height=height, title=title or "Box Plot Distribution", @@ -118,70 +114,74 @@ def create_plot( 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)] + source = ColumnDataSource(data=stats) - # 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 + # Draw boxes (Q1 to Q3) + box_width = 0.5 + for i, color in enumerate(colors): p.vbar( - x=group, - width=0.5, - bottom=box_data["q1"][idx], - top=box_data["q3"][idx], - fill_color=colors[i % len(colors)], + x=i, + width=box_width, + bottom=stats["q1"][i], + top=stats["q3"][i], + fill_color=color, 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 + # Draw median lines + for i in range(n_groups): + p.segment( + x0=i - box_width / 2, + y0=stats["q2"][i], + x1=i + box_width / 2, + y1=stats["q2"][i], + line_color="red", + line_width=2, ) - # 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 whiskers + upper_whisker = Whisker(base="x", upper="upper", lower="q3", source=source, line_color="black") + upper_whisker.upper_head.size = 10 + upper_whisker.lower_head.size = 0 + p.add_layout(upper_whisker) + + lower_whisker = Whisker(base="x", upper="q1", lower="lower", source=source, line_color="black") + lower_whisker.upper_head.size = 0 + lower_whisker.lower_head.size = 10 + p.add_layout(lower_whisker) + + # Draw outliers + if outliers["x"]: + outlier_source = ColumnDataSource(data=outliers) + p.scatter( + x="x", y="y", source=outlier_source, 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) + # Set x-axis to show group names + p.xaxis.ticker = FixedTicker(ticks=list(range(n_groups))) + p.xaxis.major_label_overrides = {i: name for i, name in enumerate(group_names)} - # Styling + # Labels p.xaxis.axis_label = xlabel or groups p.yaxis.axis_label = ylabel or values + # Styling 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) + y_min = data[values].min() + y_range = data[values].max() - y_min + for i, group in enumerate(group_names): + count = group_counts[group] + label = Label( + x=i, y=y_min - y_range * 0.08, text=f"n={count}", text_align="center", text_font_size="9pt", text_alpha=0.7 + ) p.add_layout(label) return p @@ -189,27 +189,23 @@ def create_plot( if __name__ == "__main__": # Sample data for testing with different distributions per group - np.random.seed(42) # For reproducibility + np.random.seed(42) - # Generate sample data with 4 groups data_dict = {"Group": [], "Value": []} - # Group A: Normal distribution, mean=50, std=10 + # Group A: Normal distribution 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: Normal distribution 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: Normal distribution 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 @@ -233,16 +229,8 @@ def create_plot( 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 + # Save as PNG + 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") + export_png(fig, filename="plot.png") + print("Plot saved to plot.png") diff --git a/plots/highcharts/boxplot/box-basic/default.py b/plots/highcharts/boxplot/box-basic/default.py index 646aef624c9..bbeb0a8ff76 100644 --- a/plots/highcharts/boxplot/box-basic/default.py +++ b/plots/highcharts/boxplot/box-basic/default.py @@ -149,21 +149,28 @@ def create_plot( 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)) + box_series = BoxPlotSeries() + box_series.data = box_data + box_series.name = "Distribution" + box_series.color_by_point = True + chart.add_series(box_series) # 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}"}, - ) - ) + scatter_series = ScatterSeries() + scatter_series.data = outliers_data + scatter_series.name = "Outliers" + scatter_series.color = "rgba(255, 0, 0, 0.5)" + scatter_series.marker = { + "fillColor": "rgba(255, 0, 0, 0.5)", + "lineWidth": 1, + "lineColor": "#000000", + "radius": 4, + } + scatter_series.tooltip = {"pointFormat": "Outlier: {point.y}"} + chart.add_series(scatter_series) # Legend chart.options.legend = { @@ -222,31 +229,45 @@ def create_plot( xlabel="Categories", ) - # Export to HTML - html_str = chart.to_js_literal() + # Export to PNG via Selenium screenshot + import tempfile + import time + from pathlib import Path + + from selenium import webdriver + from selenium.webdriver.chrome.options import Options - # Create HTML file + # Generate HTML content + html_str = chart.to_js_literal() html_content = f""" - Box Plot - Highcharts - -
- + +
+ """ - with open("plot.html", "w") as f: + # Write temp HTML and take screenshot + with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) 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") + temp_path = f.name + + chrome_options = Options() + chrome_options.add_argument("--headless") + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--window-size=1000,600") + + driver = webdriver.Chrome(options=chrome_options) + driver.get(f"file://{temp_path}") + time.sleep(1) # Wait for chart to render + driver.save_screenshot("plot.png") + driver.quit() + + Path(temp_path).unlink() # Clean up temp file + print("Plot saved to plot.png") diff --git a/plots/plotly/box/box-basic/default.py b/plots/plotly/box/box-basic/default.py index 4ff2869e2a4..42c48f03494 100644 --- a/plots/plotly/box/box-basic/default.py +++ b/plots/plotly/box/box-basic/default.py @@ -201,8 +201,6 @@ def create_plot( xlabel="Categories", ) - # Save for inspection - fig.write_html("plot.html") + # Save as PNG 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") + print("Plot saved to plot.png") diff --git a/plots/plotnine/boxplot/box-basic/default.py b/plots/plotnine/boxplot/box-basic/default.py index 82114200ffa..e73c0b63978 100644 --- a/plots/plotnine/boxplot/box-basic/default.py +++ b/plots/plotnine/boxplot/box-basic/default.py @@ -90,7 +90,7 @@ def create_plot( width=0.6, **kwargs, ) - + scale_fill_brewer(type="qual", palette=fill_palette, guide=False) # Hide legend + + scale_fill_brewer(type="qual", palette=fill_palette, guide=None) # Hide legend + labs(title=title or "Box Plot Distribution", x=xlabel or groups, y=ylabel or values) + theme_minimal() + theme( diff --git a/plots/pygal/box/box-basic/default.py b/plots/pygal/box/box-basic/default.py index d07d6d8779a..df085779a46 100644 --- a/plots/pygal/box/box-basic/default.py +++ b/plots/pygal/box/box-basic/default.py @@ -161,13 +161,6 @@ def create_plot( 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") + # Save as PNG + chart.render_to_png("plot.png") + print("Plot saved to plot.png") diff --git a/pyproject.toml b/pyproject.toml index d8b51e97e41..0410da30144 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ plotting = [ "vl-convert-python>=1.3.0", # altair PNG export "kaleido>=0.2.1", # plotly PNG export "selenium>=4.15.0", # bokeh PNG export + "cairosvg>=2.7.0", # pygal PNG export ] all = [ "pyplots[test,dev,plotting,typecheck]", diff --git a/rules/generation/v1.0.0-draft/code-generation-rules.md b/rules/generation/v1.0.0-draft/code-generation-rules.md index 453b80630ea..37185a845ec 100644 --- a/rules/generation/v1.0.0-draft/code-generation-rules.md +++ b/rules/generation/v1.0.0-draft/code-generation-rules.md @@ -136,6 +136,28 @@ if __name__ == '__main__': > Namen. Verwende NIEMALS `test_output_matplotlib.png`, `test_output_seaborn.png` oder > ähnliche library-spezifische Namen. +### Output Format Requirements + +**Current Phase: PNG only** + +All plots must output `plot.png`. No HTML, SVG, or interactive outputs. + +| Library | PNG Export Method | +|---------|-------------------| +| matplotlib | `plt.savefig('plot.png', dpi=300, bbox_inches='tight')` | +| seaborn | `plt.savefig('plot.png', dpi=300, bbox_inches='tight')` | +| plotly | `fig.write_image('plot.png', width=1000, height=600, scale=2)` | +| bokeh | `export_png(fig, filename='plot.png')` | +| altair | `chart.save('plot.png', scale_factor=2.0)` | +| plotnine | `plot.save('plot.png', dpi=300)` | +| pygal | `chart.render_to_png('plot.png')` | +| highcharts | Selenium screenshot (see example below) | + +**Future Phase: Interactive HTML** *(not yet implemented)* +- Interactive plots (HTML) planned for future release +- Will enable hover, zoom, pan for plotly/bokeh/altair +- SVG output also planned for pygal + --- ## Generation Process diff --git a/uv.lock b/uv.lock index ace9121e1e6..48990506c5e 100644 --- a/uv.lock +++ b/uv.lock @@ -145,6 +145,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" }, ] +[[package]] +name = "cairocffi" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, +] + +[[package]] +name = "cairosvg" +version = "2.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cairocffi" }, + { name = "cssselect2" }, + { name = "defusedxml" }, + { name = "pillow" }, + { name = "tinycss2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -163,9 +191,25 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, @@ -307,6 +351,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, ] +[[package]] +name = "cssselect2" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -316,6 +373,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -1278,6 +1344,7 @@ dependencies = [ all = [ { name = "altair" }, { name = "bokeh" }, + { name = "cairosvg" }, { name = "highcharts-core" }, { name = "httpx" }, { name = "kaleido" }, @@ -1301,6 +1368,7 @@ dev = [ plotting = [ { name = "altair" }, { name = "bokeh" }, + { name = "cairosvg" }, { name = "highcharts-core" }, { name = "kaleido" }, { name = "plotly" }, @@ -1327,6 +1395,7 @@ requires-dist = [ { name = "anthropic", specifier = ">=0.39.0" }, { name = "asyncpg", specifier = ">=0.29.0" }, { name = "bokeh", marker = "extra == 'plotting'", specifier = ">=3.3.0" }, + { name = "cairosvg", marker = "extra == 'plotting'", specifier = ">=2.7.0" }, { name = "fastapi", specifier = ">=0.104.0" }, { name = "google-cloud-storage", specifier = ">=2.10.0" }, { name = "highcharts-core", marker = "extra == 'plotting'", specifier = ">=1.10.0" }, @@ -1730,6 +1799,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/c0/b28d0fd0347ea38d3610052f479e4b922eb33bb8790817f93cd89e6e08ba/statsmodels-0.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:95af7a9c4689d514f4341478b891f867766f3da297f514b8c4adf08f4fa61d03", size = 9648961, upload-time = "2025-10-30T13:47:24.303Z" }, ] +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + [[package]] name = "tornado" version = "6.5.2" @@ -1950,6 +2031,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, ] +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + [[package]] name = "websocket-client" version = "1.9.0" From 36e5bf5242b6ce65787b37d0bfd5737b2a01d548 Mon Sep 17 00:00:00 2001 From: Markus Neusinger Date: Sat, 29 Nov 2025 00:09:42 +0100 Subject: [PATCH 4/5] feat: update PNG export functionality and add webdriver-manager dependency - Refactor PNG export to use webdriver-manager for automatic chromedriver installation - Update pyproject.toml to include webdriver-manager dependency - Modify lock files to reflect new dependency --- plots/bokeh/custom/box-basic/default.py | 24 ++++++++++++++++++------ pyproject.toml | 1 + uv.lock | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/plots/bokeh/custom/box-basic/default.py b/plots/bokeh/custom/box-basic/default.py index 5701d303d71..7cd63c629ae 100644 --- a/plots/bokeh/custom/box-basic/default.py +++ b/plots/bokeh/custom/box-basic/default.py @@ -12,6 +12,7 @@ from bokeh.models import ColumnDataSource, FixedTicker, Label, Whisker from bokeh.plotting import figure + if TYPE_CHECKING: from bokeh.plotting import Figure @@ -154,13 +155,11 @@ def create_plot( # Draw outliers if outliers["x"]: outlier_source = ColumnDataSource(data=outliers) - p.scatter( - x="x", y="y", source=outlier_source, size=8, color="red", alpha=0.5, line_color="black", line_width=1 - ) + p.scatter(x="x", y="y", source=outlier_source, size=8, color="red", alpha=0.5, line_color="black", line_width=1) # Set x-axis to show group names p.xaxis.ticker = FixedTicker(ticks=list(range(n_groups))) - p.xaxis.major_label_overrides = {i: name for i, name in enumerate(group_names)} + p.xaxis.major_label_overrides = dict(enumerate(group_names)) # Labels p.xaxis.axis_label = xlabel or groups @@ -229,8 +228,21 @@ def create_plot( xlabel="Categories", ) - # Save as PNG + # Save as PNG using webdriver-manager for automatic chromedriver from bokeh.io import export_png + from selenium import webdriver + from selenium.webdriver.chrome.options import Options + from selenium.webdriver.chrome.service import Service + from webdriver_manager.chrome import ChromeDriverManager + + chrome_options = Options() + chrome_options.add_argument("--headless") + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(ChromeDriverManager().install()) + driver = webdriver.Chrome(service=service, options=chrome_options) - export_png(fig, filename="plot.png") + export_png(fig, filename="plot.png", webdriver=driver) + driver.quit() print("Plot saved to plot.png") diff --git a/pyproject.toml b/pyproject.toml index 0410da30144..a3fe29e367a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ plotting = [ "vl-convert-python>=1.3.0", # altair PNG export "kaleido>=0.2.1", # plotly PNG export "selenium>=4.15.0", # bokeh PNG export + "webdriver-manager>=4.0.0", # auto-install chromedriver "cairosvg>=2.7.0", # pygal PNG export ] all = [ diff --git a/uv.lock b/uv.lock index 48990506c5e..c858ae9e521 100644 --- a/uv.lock +++ b/uv.lock @@ -1360,6 +1360,7 @@ all = [ { name = "seaborn" }, { name = "selenium" }, { name = "vl-convert-python" }, + { name = "webdriver-manager" }, ] dev = [ { name = "pre-commit" }, @@ -1377,6 +1378,7 @@ plotting = [ { name = "seaborn" }, { name = "selenium" }, { name = "vl-convert-python" }, + { name = "webdriver-manager" }, ] test = [ { name = "httpx" }, @@ -1425,6 +1427,7 @@ requires-dist = [ { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" }, { name = "vl-convert-python", marker = "extra == 'plotting'", specifier = ">=1.3.0" }, + { name = "webdriver-manager", marker = "extra == 'plotting'", specifier = ">=4.0.0" }, ] provides-extras = ["test", "dev", "typecheck", "plotting", "all"] @@ -2031,6 +2034,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, ] +[[package]] +name = "webdriver-manager" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "python-dotenv" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/4f/6e44478908c5133f680378d687f14ecaa99feed2c535344fcf68d8d21500/webdriver_manager-4.0.2.tar.gz", hash = "sha256:efedf428f92fd6d5c924a0d054e6d1322dd77aab790e834ee767af392b35590f", size = 25940, upload-time = "2024-07-25T08:13:49.331Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/b5/3bd0b038d80950ec13e6a2c8d03ed8354867dc60064b172f2f4ffac8afbe/webdriver_manager-4.0.2-py2.py3-none-any.whl", hash = "sha256:75908d92ecc45ff2b9953614459c633db8f9aa1ff30181cefe8696e312908129", size = 27778, upload-time = "2024-07-25T08:13:47.917Z" }, +] + [[package]] name = "webencodings" version = "0.5.1" From 0b92595fc0a6d0462da1216742efdfa68a7c4842 Mon Sep 17 00:00:00 2001 From: Markus Neusinger Date: Sat, 29 Nov 2025 00:21:22 +0100 Subject: [PATCH 5/5] feat: standardize figure sizes to 16:9 aspect ratio across all libraries - Updated default width and height parameters for box plots - Adjusted figure size in documentation and code comments - Ensured consistent dimensions for Highcharts and other libraries --- plots/altair/boxplot/box-basic/default.py | 8 ++++---- plots/bokeh/custom/box-basic/default.py | 8 ++++---- plots/highcharts/boxplot/box-basic/default.py | 12 +++++++----- plots/matplotlib/boxplot/box-basic/default.py | 4 ++-- plots/plotly/box/box-basic/default.py | 10 +++++----- plots/plotnine/boxplot/box-basic/default.py | 8 ++++---- plots/pygal/box/box-basic/default.py | 8 ++++---- plots/seaborn/boxplot/box-basic/default.py | 4 ++-- .../v1.0.0-draft/code-generation-rules.md | 19 ++++++++++++++++++- 9 files changed, 50 insertions(+), 31 deletions(-) diff --git a/plots/altair/boxplot/box-basic/default.py b/plots/altair/boxplot/box-basic/default.py index 00787ef05d3..b2835f2ef08 100644 --- a/plots/altair/boxplot/box-basic/default.py +++ b/plots/altair/boxplot/box-basic/default.py @@ -24,8 +24,8 @@ def create_plot( xlabel: Optional[str] = None, ylabel: Optional[str] = None, color_scheme: str = "set2", - width: int = 600, - height: int = 400, + width: int = 800, + height: int = 450, **kwargs, ) -> Chart: """ @@ -39,8 +39,8 @@ def create_plot( 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) + width: Figure width in pixels (default: 800) + height: Figure height in pixels (default: 450) **kwargs: Additional parameters for altair chart configuration Returns: diff --git a/plots/bokeh/custom/box-basic/default.py b/plots/bokeh/custom/box-basic/default.py index 7cd63c629ae..4760fc06591 100644 --- a/plots/bokeh/custom/box-basic/default.py +++ b/plots/bokeh/custom/box-basic/default.py @@ -25,8 +25,8 @@ def create_plot( xlabel: Optional[str] = None, ylabel: Optional[str] = None, colors: Optional[list] = None, - width: int = 1000, - height: int = 600, + width: int = 1600, + height: int = 900, **kwargs, ) -> Figure: """ @@ -40,8 +40,8 @@ def create_plot( 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) + width: Figure width in pixels (default: 1600) + height: Figure height in pixels (default: 900) **kwargs: Additional parameters Returns: diff --git a/plots/highcharts/boxplot/box-basic/default.py b/plots/highcharts/boxplot/box-basic/default.py index bbeb0a8ff76..aac3b70725c 100644 --- a/plots/highcharts/boxplot/box-basic/default.py +++ b/plots/highcharts/boxplot/box-basic/default.py @@ -24,7 +24,8 @@ def create_plot( xlabel: Optional[str] = None, ylabel: Optional[str] = None, colors: Optional[list] = None, - height: int = 600, + width: int = 1600, + height: int = 900, **kwargs, ) -> Chart: """ @@ -38,7 +39,8 @@ def create_plot( 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) + width: Figure width in pixels (default: 1600) + height: Figure height in pixels (default: 900) **kwargs: Additional parameters for Highcharts configuration Returns: @@ -146,7 +148,7 @@ def create_plot( } # Chart dimensions - chart.options.chart = {"type": "boxplot", "height": height, "backgroundColor": "white"} + chart.options.chart = {"type": "boxplot", "width": width, "height": height, "backgroundColor": "white"} # Add box plot series box_series = BoxPlotSeries() @@ -247,7 +249,7 @@ def create_plot( -
+
""" @@ -261,7 +263,7 @@ def create_plot( chrome_options.add_argument("--headless") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--disable-dev-shm-usage") - chrome_options.add_argument("--window-size=1000,600") + chrome_options.add_argument("--window-size=1600,900") driver = webdriver.Chrome(options=chrome_options) driver.get(f"file://{temp_path}") diff --git a/plots/matplotlib/boxplot/box-basic/default.py b/plots/matplotlib/boxplot/box-basic/default.py index 7b387029cb0..4fe6fa2776c 100644 --- a/plots/matplotlib/boxplot/box-basic/default.py +++ b/plots/matplotlib/boxplot/box-basic/default.py @@ -24,7 +24,7 @@ def create_plot( xlabel: Optional[str] = None, ylabel: Optional[str] = None, colors: Optional[list] = None, - figsize: tuple[float, float] = (10, 6), + figsize: tuple[float, float] = (16, 9), **kwargs, ) -> Figure: """ @@ -38,7 +38,7 @@ def create_plot( 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)) + figsize: Figure size as (width, height) in inches (default: (16, 9)) **kwargs: Additional parameters passed to boxplot function Returns: diff --git a/plots/plotly/box/box-basic/default.py b/plots/plotly/box/box-basic/default.py index 42c48f03494..1cf05b7dfeb 100644 --- a/plots/plotly/box/box-basic/default.py +++ b/plots/plotly/box/box-basic/default.py @@ -24,8 +24,8 @@ def create_plot( xlabel: Optional[str] = None, ylabel: Optional[str] = None, color_discrete_sequence: Optional[list] = None, - height: int = 600, - width: int = 1000, + height: int = 900, + width: int = 1600, showlegend: bool = False, **kwargs, ) -> Figure: @@ -40,8 +40,8 @@ def create_plot( 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) + height: Figure height in pixels (default: 900) + width: Figure width in pixels (default: 1600) showlegend: Whether to show legend (default: False) **kwargs: Additional parameters passed to plotly box trace @@ -202,5 +202,5 @@ def create_plot( ) # Save as PNG - fig.write_image("plot.png", width=1000, height=600, scale=2) + fig.write_image("plot.png", width=1600, height=900, scale=2) print("Plot saved to plot.png") diff --git a/plots/plotnine/boxplot/box-basic/default.py b/plots/plotnine/boxplot/box-basic/default.py index e73c0b63978..65226940b6a 100644 --- a/plots/plotnine/boxplot/box-basic/default.py +++ b/plots/plotnine/boxplot/box-basic/default.py @@ -34,8 +34,8 @@ def create_plot( xlabel: Optional[str] = None, ylabel: Optional[str] = None, fill_palette: str = "Set2", - width: int = 10, - height: int = 6, + width: int = 16, + height: int = 9, show_outliers: bool = True, **kwargs, ) -> GGPlot: @@ -50,8 +50,8 @@ def create_plot( 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) + width: Figure width in inches (default: 16) + height: Figure height in inches (default: 9) show_outliers: Whether to show outliers (default: True) **kwargs: Additional parameters for geom_boxplot diff --git a/plots/pygal/box/box-basic/default.py b/plots/pygal/box/box-basic/default.py index df085779a46..a71dedf8872 100644 --- a/plots/pygal/box/box-basic/default.py +++ b/plots/pygal/box/box-basic/default.py @@ -24,8 +24,8 @@ def create_plot( title: Optional[str] = None, xlabel: Optional[str] = None, ylabel: Optional[str] = None, - width: int = 800, - height: int = 600, + width: int = 1600, + height: int = 900, show_legend: bool = True, **kwargs, ) -> Box: @@ -39,8 +39,8 @@ def create_plot( 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) + width: Figure width in pixels (default: 1600) + height: Figure height in pixels (default: 900) show_legend: Whether to show legend (default: True) **kwargs: Additional parameters for pygal configuration diff --git a/plots/seaborn/boxplot/box-basic/default.py b/plots/seaborn/boxplot/box-basic/default.py index 11ab38c8034..d2a1cd1f30d 100644 --- a/plots/seaborn/boxplot/box-basic/default.py +++ b/plots/seaborn/boxplot/box-basic/default.py @@ -25,7 +25,7 @@ def create_plot( xlabel: Optional[str] = None, ylabel: Optional[str] = None, palette: Optional[str] = "Set2", - figsize: tuple[float, float] = (10, 6), + figsize: tuple[float, float] = (16, 9), showfliers: bool = True, **kwargs, ) -> Figure: @@ -40,7 +40,7 @@ def create_plot( 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)) + figsize: Figure size as (width, height) in inches (default: (16, 9)) showfliers: Whether to show outliers (default: True) **kwargs: Additional parameters passed to seaborn boxplot function diff --git a/rules/generation/v1.0.0-draft/code-generation-rules.md b/rules/generation/v1.0.0-draft/code-generation-rules.md index 37185a845ec..5a113fc69e7 100644 --- a/rules/generation/v1.0.0-draft/code-generation-rules.md +++ b/rules/generation/v1.0.0-draft/code-generation-rules.md @@ -242,7 +242,7 @@ From `docs/development.md` (lines 182-241): - ✅ Imports: Organized (standard, third-party, local) **Visual Quality**: -- ✅ Figure size: `figsize=(16, 9)` by default (16:9 aspect ratio) +- ✅ Figure size: 16:9 aspect ratio for all libraries (see Standard Plot Sizes below) - ✅ Axis labels: From column names or custom - ✅ Grid: `ax.grid(True, alpha=0.3)` (subtle) - ✅ Font sizes: Readable (≥10pt) @@ -250,6 +250,23 @@ From `docs/development.md` (lines 182-241): - ✅ Tight layout: `plt.tight_layout()` to avoid clipping - ✅ DPI: Always use `dpi=300` when saving for high-quality output +### Standard Plot Sizes (16:9, 300 DPI equivalent) + +All plots must use 16:9 aspect ratio with 300 DPI equivalent quality: + +| Library | Size Parameters | DPI/Scale | Final Resolution | +|---------|-----------------|-----------|------------------| +| matplotlib | `figsize=(16, 9)` | `dpi=300` | 4800×2700 | +| seaborn | `figsize=(16, 9)` | `dpi=300` | 4800×2700 | +| plotnine | `figure_size=(16, 9)` | `dpi=300` | 4800×2700 | +| bokeh | `width=1600, height=900` | (pixel-based) | 1600×900 | +| plotly | `width=1600, height=900` | `scale=2` | 3200×1800 | +| altair | `width=800, height=450` | `scale_factor=2.0` | 1600×900 | +| pygal | `width=1600, height=900` | (SVG/PNG) | 1600×900 | +| highcharts | `width=1600, height=900` | (Screenshot) | 1600×900 | + +**Rationale**: 16:9 is the standard widescreen format, optimal for web display and presentations. 300 DPI ensures print-quality output for publications + --- ## Library-Specific Guidelines