diff --git a/plots/altair/boxplot/box-basic/default.py b/plots/altair/boxplot/box-basic/default.py new file mode 100644 index 00000000000..b2835f2ef08 --- /dev/null +++ b/plots/altair/boxplot/box-basic/default.py @@ -0,0 +1,179 @@ +""" +box-basic: Basic Box Plot +Implementation for: altair +Variant: default +Python: 3.10+ +""" + +from typing import TYPE_CHECKING, Optional + +import altair as alt +import numpy as np +import pandas as pd + + +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 = 800, + height: int = 450, + **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: 800) + height: Figure height in pixels (default: 450) + **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={"color": "red", "strokeWidth": 2}, + box={"strokeWidth": 1.5}, + outliers={"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], + strict=False, + ): + 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 PNG + chart.save("plot.png", scale_factor=2.0) + print("Plot saved to plot.png") diff --git a/plots/bokeh/custom/box-basic/default.py b/plots/bokeh/custom/box-basic/default.py new file mode 100644 index 00000000000..4760fc06591 --- /dev/null +++ b/plots/bokeh/custom/box-basic/default.py @@ -0,0 +1,248 @@ +""" +box-basic: Basic Box Plot +Implementation for: bokeh +Variant: default +Python: 3.10+ +""" + +from typing import TYPE_CHECKING, Optional + +import numpy as np +import pandas as pd +from bokeh.models import ColumnDataSource, FixedTicker, Label, Whisker +from bokeh.plotting import figure + + +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 = 1600, + height: int = 900, + **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: 1600) + height: Figure height in pixels (default: 900) + **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()) + n_groups = len(group_names) + + # Prepare data for box plot + stats = {"x": [], "q1": [], "q2": [], "q3": [], "upper": [], "lower": [], "group": []} + outliers = {"x": [], "y": []} + + 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) + 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 + outlier_data = group_data[(group_data < lower) | (group_data > upper)] + for val in outlier_data: + outliers["x"].append(i) + outliers["y"].append(val) + + # Set colors + if not colors: + from bokeh.palettes import Set2_8 + + colors = Set2_8[:n_groups] + + # Create figure with numeric x-axis + p = figure( + width=width, + height=height, + title=title or "Box Plot Distribution", + toolbar_location="above", + tools="pan,wheel_zoom,box_zoom,reset,save", + ) + + source = ColumnDataSource(data=stats) + + # Draw boxes (Q1 to Q3) + box_width = 0.5 + for i, color in enumerate(colors): + p.vbar( + x=i, + width=box_width, + bottom=stats["q1"][i], + top=stats["q3"][i], + fill_color=color, + line_color="black", + alpha=0.7, + ) + + # 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, + ) + + # 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) + + # Set x-axis to show group names + p.xaxis.ticker = FixedTicker(ticks=list(range(n_groups))) + p.xaxis.major_label_overrides = dict(enumerate(group_names)) + + # 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" + 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() + 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 + + +if __name__ == "__main__": + # Sample data for testing with different distributions per group + np.random.seed(42) + + data_dict = {"Group": [], "Value": []} + + # Group A: Normal distribution + group_a_data = np.random.normal(50, 10, 40) + group_a_data = np.append(group_a_data, [80, 85, 15]) + + # Group B: Normal distribution + group_b_data = np.random.normal(60, 15, 35) + group_b_data = np.append(group_b_data, [100, 10]) + + # 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 + 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], + strict=False, + ): + 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 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", webdriver=driver) + driver.quit() + print("Plot saved to plot.png") diff --git a/plots/highcharts/boxplot/box-basic/default.py b/plots/highcharts/boxplot/box-basic/default.py new file mode 100644 index 00000000000..aac3b70725c --- /dev/null +++ b/plots/highcharts/boxplot/box-basic/default.py @@ -0,0 +1,275 @@ +""" +box-basic: Basic Box Plot +Implementation for: highcharts +Variant: default +Python: 3.10+ + +Note: Highcharts requires a license for commercial use. +""" + +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.series.boxplot import BoxPlotSeries + + +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 = 1600, + height: int = 900, + **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) + width: Figure width in pixels (default: 1600) + height: Figure height in pixels (default: 900) + **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", "width": width, "height": height, "backgroundColor": "white"} + + # Add box plot series + 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 + + 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 = { + "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], + strict=False, + ): + 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 PNG via Selenium screenshot + import tempfile + import time + from pathlib import Path + + from selenium import webdriver + from selenium.webdriver.chrome.options import Options + + # Generate HTML content + html_str = chart.to_js_literal() + html_content = f""" + + + + + + + +
+ + +""" + + # Write temp HTML and take screenshot + with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f: + f.write(html_content) + 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=1600,900") + + 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/matplotlib/boxplot/box-basic/default.py b/plots/matplotlib/boxplot/box-basic/default.py new file mode 100644 index 00000000000..4fe6fa2776c --- /dev/null +++ b/plots/matplotlib/boxplot/box-basic/default.py @@ -0,0 +1,181 @@ +""" +box-basic: Basic Box Plot +Implementation for: matplotlib +Variant: default +Python: 3.10+ +""" + +from typing import TYPE_CHECKING, Optional + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + + +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] = (16, 9), + **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: (16, 9)) + **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, + tick_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"]), 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, 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 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], + strict=False, + ): + 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") diff --git a/plots/plotly/box/box-basic/default.py b/plots/plotly/box/box-basic/default.py new file mode 100644 index 00000000000..1cf05b7dfeb --- /dev/null +++ b/plots/plotly/box/box-basic/default.py @@ -0,0 +1,206 @@ +""" +box-basic: Basic Box Plot +Implementation for: plotly +Variant: default +Python: 3.10+ +""" + +from typing import TYPE_CHECKING, Optional + +import numpy as np +import pandas as pd +import plotly.express as px + + +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 = 900, + width: int = 1600, + 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: 900) + width: Figure width in pixels (default: 1600) + 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={"size": 8, "opacity": 0.5, "line": {"width": 1}}, + line={"width": 1.5}, + fillcolor=None, + opacity=0.7, + ) + + # Update layout + fig.update_layout( + 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={"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( + { + "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}
" + + "" + ) + + 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], + strict=False, + ): + 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 as PNG + 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 new file mode 100644 index 00000000000..65226940b6a --- /dev/null +++ b/plots/plotnine/boxplot/box-basic/default.py @@ -0,0 +1,183 @@ +""" +box-basic: Basic Box Plot +Implementation for: plotnine +Variant: default +Python: 3.10+ +""" + +from typing import TYPE_CHECKING, Optional + +import numpy as np +import pandas as pd +from plotnine import ( + aes, + element_line, + element_text, + geom_boxplot, + ggplot, + labs, + scale_fill_brewer, + theme, + theme_minimal, +) + + +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 = 16, + height: int = 9, + 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: 16) + height: Figure height in inches (default: 9) + 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(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( + 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 + + # 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], + strict=False, + ): + 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") diff --git a/plots/pygal/box/box-basic/default.py b/plots/pygal/box/box-basic/default.py new file mode 100644 index 00000000000..a71dedf8872 --- /dev/null +++ b/plots/pygal/box/box-basic/default.py @@ -0,0 +1,166 @@ +""" +box-basic: Basic Box Plot +Implementation for: pygal +Variant: default +Python: 3.10+ +""" + +from typing import TYPE_CHECKING, Optional + +import numpy as np +import pandas as pd +import pygal +from pygal.style import Style + + +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 = 1600, + height: int = 900, + 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: 1600) + height: Figure height in pixels (default: 900) + 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], + strict=False, + ): + 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 PNG + chart.render_to_png("plot.png") + print("Plot saved to plot.png") diff --git a/plots/seaborn/boxplot/box-basic/default.py b/plots/seaborn/boxplot/box-basic/default.py new file mode 100644 index 00000000000..d2a1cd1f30d --- /dev/null +++ b/plots/seaborn/boxplot/box-basic/default.py @@ -0,0 +1,189 @@ +""" +box-basic: Basic Box Plot +Implementation for: seaborn +Variant: default +Python: 3.10+ +""" + +from typing import TYPE_CHECKING, Optional + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import seaborn as sns + + +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] = (16, 9), + 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: (16, 9)) + 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, + hue=groups, + palette=palette, + ax=ax, + showfliers=showfliers, + width=0.7, + linewidth=1.5, + fliersize=6, + legend=False, + **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], + strict=False, + ): + 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") diff --git a/pyproject.toml b/pyproject.toml index fb45f97fb8e..a3fe29e367a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,12 @@ 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 + "webdriver-manager>=4.0.0", # auto-install chromedriver + "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 2854167fd66..5a113fc69e7 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 @@ -119,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 @@ -156,19 +195,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 @@ -198,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) @@ -206,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 @@ -272,6 +333,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/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 diff --git a/uv.lock b/uv.lock index 146d9fd6041..c858ae9e521 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" @@ -154,6 +182,39 @@ 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/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" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -188,6 +249,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" @@ -277,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" @@ -286,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" @@ -652,6 +748,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 +798,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 +974,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 +1224,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" @@ -1179,8 +1344,10 @@ dependencies = [ all = [ { name = "altair" }, { name = "bokeh" }, + { name = "cairosvg" }, { name = "highcharts-core" }, { name = "httpx" }, + { name = "kaleido" }, { name = "mypy" }, { name = "plotly" }, { name = "plotnine" }, @@ -1191,6 +1358,9 @@ all = [ { name = "pytest-cov" }, { name = "ruff" }, { name = "seaborn" }, + { name = "selenium" }, + { name = "vl-convert-python" }, + { name = "webdriver-manager" }, ] dev = [ { name = "pre-commit" }, @@ -1199,11 +1369,16 @@ dev = [ plotting = [ { name = "altair" }, { name = "bokeh" }, + { name = "cairosvg" }, { name = "highcharts-core" }, + { name = "kaleido" }, { name = "plotly" }, { name = "plotnine" }, { name = "pygal" }, { name = "seaborn" }, + { name = "selenium" }, + { name = "vl-convert-python" }, + { name = "webdriver-manager" }, ] test = [ { name = "httpx" }, @@ -1222,11 +1397,13 @@ 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" }, { 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 +1423,23 @@ 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" }, + { name = "webdriver-manager", marker = "extra == 'plotting'", specifier = ">=4.0.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 +1482,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 +1698,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 +1742,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" @@ -1566,6 +1802,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" @@ -1585,6 +1833,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 +1912,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 +1987,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 +2034,38 @@ 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" +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" +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 +2075,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"