From c6e940304f1376fb7e1608189e3011ee937dfdc2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Dec 2025 22:17:35 +0000 Subject: [PATCH 1/9] spec: add pie-basic specification Created from issue #142 --- specs/pie-basic.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 specs/pie-basic.md diff --git a/specs/pie-basic.md b/specs/pie-basic.md new file mode 100644 index 0000000000..1ff6a0236a --- /dev/null +++ b/specs/pie-basic.md @@ -0,0 +1,84 @@ +# pie-basic: Basic Pie Chart + +A fundamental pie chart that visualizes proportions and percentages of categorical data as slices of a circular chart. Each slice represents a category's share of the whole, making it ideal for showing composition and distribution across a small number of categories. + +## Data Requirements + +| Column | Type | Required | Description | +|--------|------|----------|-------------| +| category | string | Yes | Category names for each slice | +| value | numeric | Yes | Numeric values representing each category's proportion | + +## Optional Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| figsize | tuple | (10, 8) | Figure size as (width, height) | +| title | string | None | Plot title | +| colors | list | None | Custom color palette for slices | +| startangle | float | 90 | Starting angle for first slice (degrees from positive x-axis) | +| autopct | string | '%1.1f%%' | Format string for percentage labels | +| explode | list | None | Offset distances for each slice (0-0.1 typical) | +| shadow | bool | False | Add shadow effect for 3D appearance | +| labels | list | None | Custom labels (defaults to category names) | +| legend | bool | True | Display legend | +| legend_loc | string | 'best' | Legend location | + +## Quality Criteria + +- [ ] All slices are clearly distinguishable with sufficient color contrast +- [ ] Percentage labels are readable and do not overlap with slice boundaries +- [ ] Category labels or legend clearly identify each slice +- [ ] Percentages sum to approximately 100% (within rounding tolerance) +- [ ] Chart is circular (equal aspect ratio maintained) +- [ ] Small slices (< 5%) remain visible and labeled appropriately +- [ ] Color palette is colorblind-friendly +- [ ] Title is positioned clearly above the chart +- [ ] Legend does not overlap with the pie chart +- [ ] Type hints and input validation are present + +## Expected Output + +A circular pie chart divided into colored slices, where each slice's arc length is proportional to its value relative to the total. The chart should display: +- Clear slice boundaries with distinct colors for each category +- Percentage labels positioned inside or near each slice +- Category names either as direct labels, in a legend, or both +- A centered, balanced appearance with no visual distortion +- Professional styling with appropriate spacing between elements + +The chart should be immediately readable, allowing viewers to quickly compare relative proportions and identify the largest and smallest categories. + +## Tags + +pie, composition, proportions, categorical, basic, 2d + +## Use Cases + +- Market share distribution showing company percentages in an industry +- Budget allocation breakdown displaying spending across departments +- Survey response analysis visualizing answer percentages +- Portfolio composition showing asset class distribution in investments +- Demographic breakdown displaying population segments by age or region +- Resource utilization showing time or capacity allocation across projects + +## Example Data + +```python +import pandas as pd + +data = pd.DataFrame({ + 'category': ['Product A', 'Product B', 'Product C', 'Product D', 'Other'], + 'value': [35, 25, 20, 15, 5] +}) + +fig = create_plot(data, 'category', 'value', title='Market Share Distribution') +``` + +## Implementation Notes + +- Validate that values are non-negative +- Handle cases where values sum to zero gracefully +- Consider using a donut hole (wedgeprops) for modern aesthetics in some libraries +- For many categories (> 7), consider grouping small slices into "Other" +- Ensure aspect ratio is equal to prevent elliptical distortion +- Position percentage labels to avoid overlap, especially for small slices From ad72e63b559174a5e8302a6923447f79a81a4ae3 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:25:30 +0000 Subject: [PATCH 2/9] feat(pygal): implement pie-basic (#151) ## Summary Implements `pie-basic` for **pygal** library. **Parent Issue:** #142 **Sub-Issue:** #149 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/pygal/pie/pie-basic/default.py` ## Features - Visualizes proportions of categorical data as colored pie slices - Supports custom colors with PyPlots.ai palette as default - Legend with configurable position (right, bottom, top) - Inner radius parameter for donut chart variant - Percentage labels displayed on each slice - Input validation for empty data, missing columns, and negative values ## Quality Checklist - [x] Type hints on all parameters and return type - [x] Google-style docstring with Args, Returns, Raises, Example - [x] Input validation for empty data and missing columns - [x] Non-negative value validation - [x] Colorblind-friendly palette (PyPlots.ai colors) - [x] Tested and generates plot.png successfully - [x] Code formatted with ruff Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/pygal/pie/pie-basic/default.py | 147 +++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 plots/pygal/pie/pie-basic/default.py diff --git a/plots/pygal/pie/pie-basic/default.py b/plots/pygal/pie/pie-basic/default.py new file mode 100644 index 0000000000..ca44c93104 --- /dev/null +++ b/plots/pygal/pie/pie-basic/default.py @@ -0,0 +1,147 @@ +""" +pie-basic: Basic Pie Chart +Library: pygal +""" + +import pandas as pd +import pygal +from pygal.style import Style + + +# PyPlots.ai color palette +PYPLOTS_COLORS = ( + "#306998", # Python Blue (Primary) + "#FFD43B", # Python Yellow + "#DC2626", # Signal Red + "#059669", # Teal Green + "#8B5CF6", # Violet + "#F97316", # Orange +) + + +def create_plot( + data: pd.DataFrame, + category: str, + value: str, + figsize: tuple[int, int] = (1600, 900), + title: str | None = None, + colors: list[str] | None = None, + startangle: float = 90, + legend: bool = True, + legend_loc: str = "right", + inner_radius: float = 0, + **kwargs, +) -> pygal.Pie: + """ + Create a basic pie chart for visualizing proportions of categorical data. + + Args: + data: Input DataFrame containing the data to plot. + category: Column name for category labels. + value: Column name for numeric values representing each slice's proportion. + figsize: Figure size as (width, height) in pixels. + title: Optional plot title. + colors: Custom color palette for slices (defaults to PyPlots palette). + startangle: Starting angle for first slice in degrees (not used in pygal). + legend: Whether to display legend. + legend_loc: Legend location ('right', 'bottom', or 'top'). + inner_radius: Inner radius for donut chart (0-1, 0 for solid pie). + **kwargs: Additional parameters passed to pygal.Pie. + + Returns: + pygal.Pie chart object. + + Raises: + ValueError: If data is empty or contains negative values. + KeyError: If required columns are not found in data. + + Example: + >>> data = pd.DataFrame({ + ... 'category': ['Product A', 'Product B', 'Product C'], + ... 'value': [35, 25, 40] + ... }) + >>> chart = create_plot(data, 'category', 'value', title='Market Share') + """ + # Input validation + if data.empty: + raise ValueError("Data cannot be empty") + + for col in [category, value]: + if col not in data.columns: + available = ", ".join(data.columns) + raise KeyError(f"Column '{col}' not found. Available: {available}") + + # Handle missing values + clean_data = data[[category, value]].dropna() + + if clean_data.empty: + raise ValueError("Data cannot be empty after removing missing values") + + # Validate non-negative values + if (clean_data[value] < 0).any(): + raise ValueError("Pie chart values must be non-negative") + + # Check if all values sum to zero + total = clean_data[value].sum() + if total == 0: + raise ValueError("Values sum to zero; cannot create pie chart") + + # Use provided colors or default PyPlots palette + chart_colors = tuple(colors) if colors else PYPLOTS_COLORS + + # Create custom style + custom_style = Style( + background="white", + plot_background="white", + foreground="#333333", + foreground_strong="#333333", + foreground_subtle="#666666", + colors=chart_colors, + font_family="Inter, DejaVu Sans, Arial, Helvetica, sans-serif", + title_font_size=20, + legend_font_size=16, + value_font_size=14, + tooltip_font_size=14, + ) + + # Determine legend position + legend_at_bottom = legend_loc == "bottom" + legend_box_size = 16 if legend else 0 + + # Create chart + chart = pygal.Pie( + width=figsize[0], + height=figsize[1], + title=title, + style=custom_style, + show_legend=legend, + legend_at_bottom=legend_at_bottom, + legend_box_size=legend_box_size, + inner_radius=inner_radius, + print_values=True, + value_formatter=lambda x: f"{x:.1f}%", + **kwargs, + ) + + # Add each category as a separate slice with percentage value + for _, row in clean_data.iterrows(): + cat_name = str(row[category]) + cat_value = float(row[value]) + percentage = (cat_value / total) * 100 + chart.add(cat_name, [{"value": percentage, "label": cat_name}]) + + return chart + + +if __name__ == "__main__": + # Sample data for testing + sample_data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} + ) + + # Create plot + chart = create_plot(sample_data, "category", "value", title="Market Share Distribution") + + # Save to PNG + chart.render_to_png("plot.png") + print("Plot saved to plot.png") From 245ec9b98d6a8f78d5b897cb1b73a5778ed838cb Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:25:45 +0000 Subject: [PATCH 3/9] feat(seaborn): implement pie-basic (#152) ## Summary Implements `pie-basic` for **seaborn** library. - Uses matplotlib's pie chart with seaborn styling context (seaborn has no native pie function) - Applies PyPlots.ai color palette with fallback for many categories - Includes all optional parameters from spec (colors, explode, shadow, legend, etc.) **Parent Issue:** #142 **Sub-Issue:** #144 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/seaborn/pie/pie-basic/default.py` ## Notes Seaborn does not have a native pie chart function. This implementation uses `matplotlib.pyplot.pie()` within a seaborn styling context (`sns.set_theme()`) to maintain consistent aesthetics with other seaborn visualizations in the project. Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/seaborn/pie/pie-basic/default.py | 184 +++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 plots/seaborn/pie/pie-basic/default.py diff --git a/plots/seaborn/pie/pie-basic/default.py b/plots/seaborn/pie/pie-basic/default.py new file mode 100644 index 0000000000..dbaf1eb03a --- /dev/null +++ b/plots/seaborn/pie/pie-basic/default.py @@ -0,0 +1,184 @@ +""" +pie-basic: Basic Pie Chart +Library: seaborn + +Note: Seaborn does not have a native pie chart function. This implementation uses +matplotlib's pie chart with seaborn's styling context for consistent aesthetics. +""" + +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sns + + +if TYPE_CHECKING: + from matplotlib.figure import Figure + + +# PyPlots.ai color palette +PYPLOTS_COLORS = [ + "#306998", # Python Blue (Primary) + "#FFD43B", # Python Yellow + "#DC2626", # Signal Red + "#059669", # Teal Green + "#8B5CF6", # Violet + "#F97316", # Orange +] + + +def create_plot( + data: pd.DataFrame, + category: str, + value: str, + figsize: tuple[float, float] = (10, 8), + title: str | None = None, + colors: list[str] | None = None, + startangle: float = 90, + autopct: str = "%1.1f%%", + explode: list[float] | None = None, + shadow: bool = False, + labels: list[str] | None = None, + legend: bool = True, + legend_loc: str = "best", + **kwargs, +) -> "Figure": + """ + Create a basic pie chart visualizing proportions of categorical data. + + A fundamental pie chart where each slice represents a category's share of the + whole, ideal for showing composition and distribution across a small number + of categories. + + Note: Seaborn does not have a native pie chart function. This implementation + uses matplotlib's pie chart with seaborn's styling context. + + Args: + data: Input DataFrame containing category and value columns + category: Column name for category labels + value: Column name for numeric values (proportions) + figsize: Figure size as (width, height) in inches + title: Plot title (optional) + colors: Custom color palette for slices (uses PyPlots.ai palette if None) + startangle: Starting angle for first slice in degrees from positive x-axis + autopct: Format string for percentage labels + explode: Offset distances for each slice (0-0.1 typical) + shadow: Add shadow effect for 3D appearance + labels: Custom labels (defaults to category names if None) + legend: Whether to display legend + legend_loc: Legend location + **kwargs: Additional parameters passed to ax.pie() + + Returns: + Matplotlib Figure object + + Raises: + ValueError: If data is empty or contains negative values + KeyError: If required columns not found in data + + Example: + >>> data = pd.DataFrame({ + ... 'category': ['Product A', 'Product B', 'Product C'], + ... 'value': [35, 40, 25] + ... }) + >>> fig = create_plot(data, 'category', 'value', title='Market Share') + """ + # Input validation + if data.empty: + raise ValueError("Data cannot be empty") + + for col in [category, value]: + if col not in data.columns: + available = ", ".join(data.columns) + raise KeyError(f"Column '{col}' not found. Available: {available}") + + # Extract values and validate + values = data[value].values + categories = data[category].values + + if (values < 0).any(): + raise ValueError("Pie chart values cannot be negative") + + if values.sum() == 0: + raise ValueError("Sum of values cannot be zero") + + # Set seaborn style context for consistent aesthetics + sns.set_theme(style="white") + + # Create figure with equal aspect ratio to prevent elliptical distortion + fig, ax = plt.subplots(figsize=figsize) + ax.set_aspect("equal") + + # Determine colors + if colors is None: + # Extend palette if needed for more categories + n_categories = len(categories) + if n_categories <= len(PYPLOTS_COLORS): + pie_colors = PYPLOTS_COLORS[:n_categories] + else: + # Use seaborn color palette for many categories + pie_colors = sns.color_palette("husl", n_categories) + else: + pie_colors = colors + + # Determine labels + pie_labels = labels if labels is not None else categories + + # Create pie chart + wedges, texts, autotexts = ax.pie( + values, + labels=pie_labels if not legend else None, + autopct=autopct, + startangle=startangle, + explode=explode, + shadow=shadow, + colors=pie_colors, + wedgeprops={"edgecolor": "white", "linewidth": 1.5}, + textprops={"fontsize": 12}, + pctdistance=0.75, + **kwargs, + ) + + # Style percentage labels + for autotext in autotexts: + autotext.set_fontsize(11) + autotext.set_fontweight("bold") + autotext.set_color("white") + + # Add legend if requested + if legend: + ax.legend( + wedges, + pie_labels, + title=category, + loc=legend_loc, + bbox_to_anchor=(1, 0, 0.5, 1), + frameon=True, + facecolor="white", + edgecolor="gray", + fontsize=11, + ) + + # Set title if provided + if title is not None: + ax.set_title(title, fontsize=16, fontweight="semibold", pad=20) + + # Layout adjustment + plt.tight_layout() + + return fig + + +if __name__ == "__main__": + # Sample data for testing + sample_data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} + ) + + # Create plot + fig = create_plot(sample_data, "category", "value", title="Market Share Distribution") + + # Save - ALWAYS use 'plot.png'! + plt.savefig("plot.png", dpi=300, bbox_inches="tight") + print("Plot saved to plot.png") From e279ba314f1c3bd9afaaf1020fef0a64091bc4fd Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:25:53 +0000 Subject: [PATCH 4/9] feat(plotly): implement pie-basic (#155) ## Summary Implements `pie-basic` for **plotly** library. - Implements basic pie chart using Plotly's `go.Pie` trace - Uses PyPlots.ai color palette for consistent visual identity - Supports all optional parameters from spec (colors, explode, legend, startangle, etc.) **Parent Issue:** #142 **Sub-Issue:** #145 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/plotly/pie/pie-basic/default.py` ## Features - Input validation for empty data, missing columns, and negative values - Customizable percentage label formatting via autopct parameter - Legend positioning with matplotlib-to-Plotly location mapping - Circular aspect ratio enforcement for proper visualization - Interactive hover tooltips showing label, value, and percentage - Configurable slice explosion for emphasis - White slice borders for clear visual separation Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/plotly/pie/pie-basic/default.py | 217 ++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 plots/plotly/pie/pie-basic/default.py diff --git a/plots/plotly/pie/pie-basic/default.py b/plots/plotly/pie/pie-basic/default.py new file mode 100644 index 0000000000..4ef5cfcebd --- /dev/null +++ b/plots/plotly/pie/pie-basic/default.py @@ -0,0 +1,217 @@ +""" +pie-basic: Basic Pie Chart +Library: plotly +""" + +from typing import TYPE_CHECKING + +import pandas as pd +import plotly.graph_objects as go + + +if TYPE_CHECKING: + pass + +# PyPlots.ai color palette +PYPLOTS_COLORS = [ + "#306998", # Python Blue + "#FFD43B", # Python Yellow + "#DC2626", # Signal Red + "#059669", # Teal Green + "#8B5CF6", # Violet + "#F97316", # Orange +] + + +def create_plot( + data: pd.DataFrame, + category: str, + value: str, + figsize: tuple[int, int] = (1600, 900), + title: str | None = None, + colors: list[str] | None = None, + startangle: float = 90, + autopct: str = "%1.1f%%", + explode: list[float] | None = None, + shadow: bool = False, + labels: list[str] | None = None, + legend: bool = True, + legend_loc: str = "best", + **kwargs, +) -> go.Figure: + """ + Create a basic pie chart for categorical data composition. + + A fundamental pie chart that visualizes proportions and percentages of + categorical data as slices of a circular chart. Each slice represents + a category's share of the whole, making it ideal for showing composition + and distribution across a small number of categories. + + Args: + data: Input DataFrame containing the data to plot. + category: Column name for category names (slice labels). + value: Column name for numeric values (slice sizes). + figsize: Figure size as (width, height) in pixels. Defaults to (1600, 900). + title: Plot title. Defaults to None. + colors: Custom color palette for slices. Defaults to PyPlots palette. + startangle: Starting angle for first slice in degrees. Defaults to 90. + autopct: Format string for percentage labels. Defaults to '%1.1f%%'. + explode: Offset distances for each slice (0-0.1 typical). Defaults to None. + shadow: Add shadow effect (not fully supported in Plotly). Defaults to False. + labels: Custom labels (defaults to category names). Defaults to None. + legend: Display legend. Defaults to True. + legend_loc: Legend location (Plotly uses different positioning). Defaults to 'best'. + **kwargs: Additional parameters passed to go.Pie. + + Returns: + Plotly Figure object containing the pie chart. + + Raises: + ValueError: If data is empty or contains negative values. + KeyError: If required columns are not found in data. + + Example: + >>> data = pd.DataFrame({ + ... 'category': ['Product A', 'Product B', 'Product C', 'Product D'], + ... 'value': [35, 25, 20, 20] + ... }) + >>> fig = create_plot(data, 'category', 'value', title='Market Share') + """ + # Input validation + if data.empty: + raise ValueError("Data cannot be empty") + + for col in [category, value]: + if col not in data.columns: + available = ", ".join(data.columns) + raise KeyError(f"Column '{col}' not found. Available: {available}") + + # Validate non-negative values + if (data[value] < 0).any(): + raise ValueError("Pie chart values must be non-negative") + + # Handle case where all values sum to zero + if data[value].sum() == 0: + raise ValueError("Pie chart values cannot all be zero") + + # Get data values + categories = labels if labels is not None else data[category].tolist() + values = data[value].tolist() + + # Set colors - use custom colors or default PyPlots palette + color_sequence = colors if colors is not None else PYPLOTS_COLORS + # Extend colors if needed + while len(color_sequence) < len(values): + color_sequence = color_sequence + PYPLOTS_COLORS + slice_colors = color_sequence[: len(values)] + + # Handle explode/pull parameter + pull_values = explode if explode is not None else [0] * len(values) + # Ensure pull_values has correct length + if len(pull_values) < len(values): + pull_values = list(pull_values) + [0] * (len(values) - len(pull_values)) + + # Create figure + fig = go.Figure() + + # Build texttemplate from autopct format + # Convert matplotlib format to plotly format + if "%%" in autopct: + # Parse the format string - e.g., '%1.1f%%' -> '%{percent:.1%}' + try: + # Extract precision from format like '%1.1f%%' + import re + + match = re.search(r"%(\d+)\.(\d+)f%%", autopct) + if match: + precision = int(match.group(2)) + text_template = f"%{{percent:.{precision}%}}" + else: + text_template = "%{percent:.1%}" + except Exception: + text_template = "%{percent:.1%}" + else: + text_template = "%{percent:.1%}" + + # Add pie trace + fig.add_trace( + go.Pie( + labels=categories, + values=values, + marker={"colors": slice_colors, "line": {"color": "white", "width": 2}}, + textinfo="percent", + texttemplate=text_template, + textfont={"size": 14, "family": "Inter, DejaVu Sans, Arial, sans-serif"}, + textposition="inside", + insidetextorientation="horizontal", + pull=pull_values, + rotation=startangle, + showlegend=legend, + hovertemplate="%{label}
Value: %{value}
Percentage: %{percent}", + **kwargs, + ) + ) + + # Configure legend position based on legend_loc + legend_config = { + "font": {"size": 16, "family": "Inter, DejaVu Sans, Arial, sans-serif"}, + "bgcolor": "rgba(255, 255, 255, 1)", + "bordercolor": "rgba(0, 0, 0, 0.3)", + "borderwidth": 1, + } + + # Map matplotlib legend locations to Plotly positions + if legend_loc in ["right", "center right"]: + legend_config.update({"x": 1.02, "y": 0.5, "xanchor": "left", "yanchor": "middle"}) + elif legend_loc in ["left", "center left"]: + legend_config.update({"x": -0.15, "y": 0.5, "xanchor": "right", "yanchor": "middle"}) + elif legend_loc in ["upper right"]: + legend_config.update({"x": 1.02, "y": 1, "xanchor": "left", "yanchor": "top"}) + elif legend_loc in ["upper left"]: + legend_config.update({"x": -0.15, "y": 1, "xanchor": "right", "yanchor": "top"}) + elif legend_loc in ["lower right"]: + legend_config.update({"x": 1.02, "y": 0, "xanchor": "left", "yanchor": "bottom"}) + elif legend_loc in ["lower left"]: + legend_config.update({"x": -0.15, "y": 0, "xanchor": "right", "yanchor": "bottom"}) + else: + # Default 'best' - place on the right + legend_config.update({"x": 1.02, "y": 0.5, "xanchor": "left", "yanchor": "middle"}) + + # Update layout with styling + fig.update_layout( + title={ + "text": title, + "x": 0.5, + "xanchor": "center", + "font": {"size": 20, "family": "Inter, DejaVu Sans, Arial, sans-serif", "weight": 600}, + } + if title + else None, + template="plotly_white", + width=figsize[0], + height=figsize[1], + showlegend=legend, + legend=legend_config if legend else None, + margin={"l": 40, "r": 150 if legend else 40, "t": 80 if title else 40, "b": 40}, + paper_bgcolor="white", + plot_bgcolor="white", + ) + + # Ensure pie chart is circular (equal aspect ratio) + fig.update_layout(yaxis={"scaleanchor": "x", "scaleratio": 1}) + + return fig + + +if __name__ == "__main__": + # Sample data for testing + sample_data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} + ) + + # Create plot + fig = create_plot(sample_data, "category", "value", title="Market Share Distribution") + + # Save + fig.write_image("plot.png", width=1600, height=900, scale=2) + print("Plot saved to plot.png") From 7309a12a3d0e9f4c1d4798ec9117501a26b508ca Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:26:01 +0000 Subject: [PATCH 5/9] feat(altair): implement pie-basic (#154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements `pie-basic` for **altair** library. **Parent Issue:** #142 **Sub-Issue:** #147 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/altair/arc/pie-basic/default.py` ## Features - Basic pie chart using Altair's `mark_arc()` - Percentage labels on slices with configurable format - PyPlots.ai default color palette (#306998, #FFD43B, #DC2626, #059669, #8B5CF6, #F97316) - Legend with configurable position (right, left, top, bottom) - Support for donut style via `inner_radius` parameter - Configurable start angle (default: 90° - starts from 12 o'clock) - Tooltips showing category and value - Input validation for: - Empty data - Missing columns - Negative values - Zero sum values ## Code Quality - Type hints on all parameters and return type - Google-style docstring with Args, Returns, Raises, Example - Follows PyPlots.ai style guide (colors, fonts, dimensions) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/altair/arc/pie-basic/default.py | 158 ++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 plots/altair/arc/pie-basic/default.py diff --git a/plots/altair/arc/pie-basic/default.py b/plots/altair/arc/pie-basic/default.py new file mode 100644 index 0000000000..fe0493305c --- /dev/null +++ b/plots/altair/arc/pie-basic/default.py @@ -0,0 +1,158 @@ +""" +pie-basic: Basic Pie Chart +Library: altair +""" + +import altair as alt +import pandas as pd + + +# PyPlots.ai default color palette +PYPLOTS_COLORS = ["#306998", "#FFD43B", "#DC2626", "#059669", "#8B5CF6", "#F97316"] + + +def create_plot( + data: pd.DataFrame, + category: str, + value: str, + *, + title: str | None = None, + colors: list[str] | None = None, + startangle: float = 90, + show_labels: bool = True, + label_format: str = ".1%", + legend: bool = True, + legend_loc: str = "right", + inner_radius: float = 0, + outer_radius: float = 150, + **kwargs, +) -> alt.Chart: + """ + Create a basic pie chart visualizing proportions of categorical data. + + A fundamental pie chart where each slice represents a category's share of the whole, + ideal for showing composition and distribution across a small number of categories. + + Args: + data: Input DataFrame containing the data to plot. + category: Column name for category labels (slice names). + value: Column name for numeric values (slice sizes). + title: Plot title. Defaults to None. + colors: Custom color palette for slices. Defaults to PyPlots.ai palette. + startangle: Starting angle for first slice in degrees. Defaults to 90. + show_labels: Whether to show percentage labels on slices. Defaults to True. + label_format: Format string for percentage labels. Defaults to ".1%". + legend: Whether to display legend. Defaults to True. + legend_loc: Legend location ('right', 'left', 'top', 'bottom'). Defaults to 'right'. + inner_radius: Inner radius for donut style (0 for solid pie). Defaults to 0. + outer_radius: Outer radius of the pie. Defaults to 150. + **kwargs: Additional parameters. + + Returns: + Altair Chart object. + + Raises: + ValueError: If data is empty or values contain negative numbers. + KeyError: If required columns are not found in data. + + Example: + >>> data = pd.DataFrame({ + ... 'category': ['Product A', 'Product B', 'Product C'], + ... 'value': [35, 25, 40] + ... }) + >>> chart = create_plot(data, 'category', 'value', title='Market Share') + """ + # Input validation + if data.empty: + raise ValueError("Data cannot be empty") + + for col in [category, value]: + if col not in data.columns: + available = ", ".join(data.columns) + raise KeyError(f"Column '{col}' not found. Available: {available}") + + # Validate non-negative values + if (data[value] < 0).any(): + raise ValueError("Pie chart values must be non-negative") + + # Handle case where all values are zero + total = data[value].sum() + if total == 0: + raise ValueError("Sum of values cannot be zero") + + # Use custom colors or default palette + color_palette = colors if colors is not None else PYPLOTS_COLORS + + # Calculate the starting angle in radians (Altair uses radians, offset from 12 o'clock) + # Altair's theta starts from 3 o'clock (0 degrees), so we need to adjust + # To start from 12 o'clock (90 degrees from 3 o'clock), we use theta2Offset + start_offset = (startangle - 90) * 3.14159 / 180 + + # Create base chart with arc mark + base = alt.Chart(data).encode( + theta=alt.Theta(f"{value}:Q", stack=True), + color=alt.Color( + f"{category}:N", + scale=alt.Scale(range=color_palette), + legend=alt.Legend(title=category, orient=legend_loc, labelFontSize=16, titleFontSize=16) + if legend + else None, + ), + tooltip=[alt.Tooltip(f"{category}:N", title="Category"), alt.Tooltip(f"{value}:Q", title="Value")], + ) + + # Create the pie/arc chart + pie = base.mark_arc( + innerRadius=inner_radius, + outerRadius=outer_radius, + stroke="#ffffff", + strokeWidth=2, + theta2Offset=start_offset, + thetaOffset=start_offset, + ) + + # Add percentage labels if requested + if show_labels: + # Calculate percentage for labels + data_with_pct = data.copy() + data_with_pct["_percentage"] = data_with_pct[value] / total + + # Create text labels positioned at the middle of each arc + text = ( + alt.Chart(data_with_pct) + .mark_text(radius=outer_radius * 0.7, fontSize=14, fontWeight="bold", color="#FFFFFF") + .encode(theta=alt.Theta(f"{value}:Q", stack=True), text=alt.Text("_percentage:Q", format=label_format)) + .transform_calculate(theta2Offset=str(start_offset), thetaOffset=str(start_offset)) + ) + + # Layer pie and text + chart = alt.layer(pie, text) + else: + chart = pie + + # Set chart dimensions and title + chart = chart.properties(width=400, height=400) + + if title is not None: + chart = chart.properties(title=alt.TitleParams(text=title, fontSize=20, anchor="middle", fontWeight=600)) + + # Configure chart appearance + chart = chart.configure_view(strokeWidth=0).configure_legend( + labelFontSize=16, titleFontSize=16, symbolSize=200, padding=10 + ) + + return chart + + +if __name__ == "__main__": + # Sample data for testing + sample_data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} + ) + + # Create plot + fig = create_plot(sample_data, "category", "value", title="Market Share Distribution") + + # Save + fig.save("plot.png", scale_factor=2.0) + print("Plot saved to plot.png") From a4f194f3e7a1466d1292e9ea7868db585f3aa9eb Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:26:09 +0000 Subject: [PATCH 6/9] feat(bokeh): implement pie-basic (#156) ## Summary Implements `pie-basic` for **bokeh** library. **Parent Issue:** #142 **Sub-Issue:** #146 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/bokeh/custom/pie-basic/default.py` ## Details Bokeh does not have a native pie chart method, so this implementation uses `wedge` glyphs to construct the pie chart manually. The implementation: - Uses wedge glyphs to draw pie slices with proper angles - Includes percentage labels positioned inside slices (for slices >= 5%) - Provides interactive tooltips showing category, value, and percentage - Supports customizable colors (defaults to PyPlots style palette) - Includes a configurable legend - Validates input data (empty data, missing columns, non-negative values) - Follows PyPlots.ai style guide (colors, dimensions, typography) - Falls back to HTML export when selenium is not available for PNG export Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/bokeh/custom/pie-basic/default.py | 221 ++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 plots/bokeh/custom/pie-basic/default.py diff --git a/plots/bokeh/custom/pie-basic/default.py b/plots/bokeh/custom/pie-basic/default.py new file mode 100644 index 0000000000..bf784be68d --- /dev/null +++ b/plots/bokeh/custom/pie-basic/default.py @@ -0,0 +1,221 @@ +""" +pie-basic: Basic Pie Chart +Library: bokeh +""" + +import math +from typing import TYPE_CHECKING + +import pandas as pd +from bokeh.models import ColumnDataSource, Label, Legend, LegendItem +from bokeh.plotting import figure + + +if TYPE_CHECKING: + from bokeh.plotting import figure as Figure + +# PyPlots.ai style colors +PYPLOTS_COLORS = [ + "#306998", # Python Blue + "#FFD43B", # Python Yellow + "#DC2626", # Signal Red + "#059669", # Teal Green + "#8B5CF6", # Violet + "#F97316", # Orange +] + + +def create_plot( + data: pd.DataFrame, + category: str, + value: str, + title: str | None = None, + colors: list[str] | None = None, + startangle: float = 90, + legend: bool = True, + legend_loc: str = "right", + **kwargs, +) -> "Figure": + """ + Create a basic pie chart using Bokeh wedge glyphs. + + Bokeh does not have a native pie chart method, so this implementation + uses wedge glyphs to construct the pie chart manually. + + Args: + data: Input DataFrame containing category and value columns + category: Column name for category labels (slice names) + value: Column name for numeric values (slice sizes) + title: Plot title (optional) + colors: Custom color palette for slices (defaults to PyPlots colors) + startangle: Starting angle for first slice in degrees (default: 90) + legend: Whether to display legend (default: True) + legend_loc: Legend location - 'right', 'left', 'above', 'below' (default: 'right') + **kwargs: Additional parameters passed to figure + + Returns: + Bokeh figure object + + Raises: + ValueError: If data is empty or values are all zero/negative + KeyError: If required columns not found in data + + Example: + >>> data = pd.DataFrame({ + ... 'category': ['A', 'B', 'C'], + ... 'value': [30, 50, 20] + ... }) + >>> fig = create_plot(data, 'category', 'value', title='Distribution') + """ + # Input validation + if data.empty: + raise ValueError("Data cannot be empty") + + for col in [category, value]: + if col not in data.columns: + available = ", ".join(data.columns) + raise KeyError(f"Column '{col}' not found. Available: {available}") + + # Validate numeric values + if not pd.api.types.is_numeric_dtype(data[value]): + raise ValueError(f"Column '{value}' must contain numeric values") + + if (data[value] < 0).any(): + raise ValueError("Pie chart values must be non-negative") + + total = data[value].sum() + if total == 0: + raise ValueError("Sum of values cannot be zero") + + # Prepare data + plot_data = data.copy() + plot_data["angle"] = plot_data[value] / total * 2 * math.pi + plot_data["percentage"] = plot_data[value] / total * 100 + + # Calculate cumulative angles for wedge positioning + plot_data["end_angle"] = plot_data["angle"].cumsum() + plot_data["start_angle"] = plot_data["end_angle"] - plot_data["angle"] + + # Apply start angle offset (convert degrees to radians, adjust for Bokeh's coordinate system) + start_rad = math.radians(startangle - 90) + plot_data["start_angle"] = plot_data["start_angle"] + start_rad + plot_data["end_angle"] = plot_data["end_angle"] + start_rad + + # Assign colors + if colors is None: + colors = PYPLOTS_COLORS + # Cycle through colors if more categories than colors + num_categories = len(plot_data) + plot_data["color"] = [colors[i % len(colors)] for i in range(num_categories)] + + # Create ColumnDataSource + source = ColumnDataSource(plot_data) + + # Create figure - use range to ensure circular aspect ratio + # Set frame dimensions to maintain 16:9 overall but circular pie + fig_width = kwargs.get("width", 1600) + fig_height = kwargs.get("height", 900) + + p = figure( + width=fig_width, + height=fig_height, + title=title, + tools="hover", + tooltips=[(category.capitalize(), f"@{category}"), ("Value", f"@{value}"), ("Percentage", "@percentage{0.1}%")], + x_range=(-1.2, 2.0 if legend else 1.2), + y_range=(-1.2, 1.2), + ) + + # Draw wedges (pie slices) + renderers = p.wedge( + x=0, + y=0, + radius=0.9, + start_angle="start_angle", + end_angle="end_angle", + line_color="white", + line_width=2, + fill_color="color", + source=source, + ) + + # Add percentage labels inside slices + for _, row in plot_data.iterrows(): + # Calculate label position at middle of wedge, 60% from center + mid_angle = (row["start_angle"] + row["end_angle"]) / 2 + label_radius = 0.55 + + x = label_radius * math.cos(mid_angle) + y = label_radius * math.sin(mid_angle) + + # Only show percentage label if slice is large enough + if row["percentage"] >= 5: + label = Label( + x=x, + y=y, + text=f"{row['percentage']:.1f}%", + text_font_size="14pt", + text_align="center", + text_baseline="middle", + text_color="white" if row["percentage"] >= 10 else "black", + ) + p.add_layout(label) + + # Configure legend + if legend: + legend_items = [] + for i, cat in enumerate(plot_data[category]): + legend_items.append(LegendItem(label=str(cat), renderers=[renderers], index=i)) + + leg = Legend( + items=legend_items, + location="center", + label_text_font_size="16pt", + background_fill_color="white", + background_fill_alpha=1.0, + border_line_color="black", + border_line_width=1, + ) + + p.add_layout(leg, legend_loc) + + # Style configuration + p.axis.visible = False + p.grid.visible = False + p.outline_line_color = None + + # Title styling + if title: + p.title.text_font_size = "20pt" + p.title.align = "center" + + # Background + p.background_fill_color = "white" + + return p + + +if __name__ == "__main__": + # Sample data for testing + sample_data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} + ) + + # Create plot + fig = create_plot(sample_data, "category", "value", title="Market Share Distribution") + + # Save - try PNG first, fall back to HTML if selenium not available + try: + from bokeh.io import export_png + + export_png(fig, filename="plot.png") + print("Plot saved to plot.png") + except RuntimeError as e: + if "selenium" in str(e).lower(): + from bokeh.io import output_file, save + + output_file("plot.html") + save(fig) + print("Plot saved to plot.html (selenium not available for PNG export)") + else: + raise From fc536d79e9db1be31015cfd02a0891025a394253 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:26:29 +0000 Subject: [PATCH 7/9] feat(highcharts): implement pie-basic (#157) ## Summary Implements `pie-basic` for **highcharts** library. **Parent Issue:** #142 **Sub-Issue:** #150 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/highcharts/pie/pie-basic/default.py` ## Features - Basic pie chart with proportional slices - PyPlots style guide colors (Python Blue, Yellow, Red, etc.) - All optional parameters from spec: - `colors`: Custom color palette - `startangle`: Starting angle for first slice - `explode`: Offset distances for each slice - `shadow`: 3D shadow effect - `legend`: Toggle legend display - `legend_loc`: Legend positioning - `autopct`: Percentage label format - Proper input validation (empty data, missing columns, negative values, zero sum) - Google-style docstrings with type hints - PNG export via Selenium headless Chrome ## Testing - Tested with sample data from spec - Verified plot.png output (1600x761 PNG) - Passed ruff formatting and linting Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/highcharts/pie/pie-basic/default.py | 287 ++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 plots/highcharts/pie/pie-basic/default.py diff --git a/plots/highcharts/pie/pie-basic/default.py b/plots/highcharts/pie/pie-basic/default.py new file mode 100644 index 0000000000..293b3b2cca --- /dev/null +++ b/plots/highcharts/pie/pie-basic/default.py @@ -0,0 +1,287 @@ +""" +pie-basic: Basic Pie Chart +Library: highcharts + +A fundamental pie chart that visualizes proportions and percentages of categorical data +as slices of a circular chart. Each slice represents a category's share of the whole. + +Note: Highcharts requires a license for commercial use. +""" + +from typing import Optional + +import pandas as pd +from highcharts_core.chart import Chart +from highcharts_core.options import HighchartsOptions +from highcharts_core.options.series.pie import PieSeries + + +# Style guide colors +PYPLOTS_COLORS = [ + "#306998", # Python Blue (Primary) + "#FFD43B", # Python Yellow + "#DC2626", # Signal Red + "#059669", # Teal Green + "#8B5CF6", # Violet + "#F97316", # Orange +] + + +def create_plot( + data: pd.DataFrame, + category: str, + value: str, + figsize: tuple[int, int] = (10, 8), + title: Optional[str] = None, + colors: Optional[list[str]] = None, + startangle: float = 90, + autopct: str = "%1.1f%%", + explode: Optional[list[float]] = None, + shadow: bool = False, + labels: Optional[list[str]] = None, + legend: bool = True, + legend_loc: str = "best", + width: int = 1600, + height: int = 900, + **kwargs, +) -> Chart: + """ + Create a basic pie chart from DataFrame. + + Args: + data: Input DataFrame with categorical and numeric data + category: Column name for category names (slice labels) + value: Column name for numeric values (slice proportions) + figsize: Figure size as (width, height) in inches (legacy, use width/height instead) + title: Plot title + colors: Custom color palette for slices (defaults to PyPlots style guide colors) + startangle: Starting angle for first slice in degrees (from positive x-axis) + autopct: Format string for percentage labels + explode: Offset distances for each slice (0-0.1 typical) + shadow: Add shadow effect for 3D appearance + labels: Custom labels (defaults to category names) + legend: Whether to display legend + legend_loc: Legend location (e.g., 'best', 'right', 'left') + width: Figure width in pixels (default: 1600) + height: Figure height in pixels (default: 900) + **kwargs: Additional parameters passed to chart options + + Returns: + Highcharts Chart object + + Raises: + ValueError: If data is empty or contains negative values + KeyError: If required columns are not found in data + + Example: + >>> data = pd.DataFrame({ + ... 'category': ['Product A', 'Product B', 'Product C'], + ... 'value': [35, 25, 40] + ... }) + >>> chart = create_plot(data, 'category', 'value', title='Market Share') + """ + # Input validation + if data.empty: + raise ValueError("Data cannot be empty") + + for col in [category, value]: + if col not in data.columns: + available = ", ".join(data.columns.tolist()) + raise KeyError(f"Column '{col}' not found. Available: {available}") + + # Validate non-negative values + if (data[value] < 0).any(): + raise ValueError("Pie chart values must be non-negative") + + # Check if all values sum to zero + if data[value].sum() == 0: + raise ValueError("Sum of values cannot be zero") + + # Get colors (use provided or default to PyPlots style guide) + slice_colors = colors if colors is not None else PYPLOTS_COLORS + + # Get labels (use provided or default to category names) + slice_labels = labels if labels is not None else data[category].tolist() + + # Create chart with container ID for rendering + chart = Chart(container="container") + chart.options = HighchartsOptions() + + # Chart configuration + chart.options.chart = {"type": "pie", "width": width, "height": height, "backgroundColor": "#ffffff"} + + # Title with style guide typography + if title: + chart.options.title = { + "text": title, + "style": { + "fontSize": "20px", + "fontWeight": "600", + "fontFamily": "Inter, DejaVu Sans, Arial, Helvetica, sans-serif", + }, + } + else: + chart.options.title = {"text": None} + + # Build data points for pie series + pie_data = [] + for i, (cat, val) in enumerate(zip(data[category].tolist(), data[value].tolist(), strict=True)): + point = { + "name": slice_labels[i] if i < len(slice_labels) else cat, + "y": val, + "color": slice_colors[i % len(slice_colors)], + } + + # Apply explode if provided + if explode is not None and i < len(explode) and explode[i] > 0: + point["sliced"] = True + point["selected"] = True + + pie_data.append(point) + + # Create pie series + series = PieSeries() + series.data = pie_data + series.name = value + + # Configure data labels to show percentages + # Parse autopct format for decimal places (e.g., '%1.1f%%' -> 1 decimal) + decimal_places = 1 + if autopct and "." in autopct: + try: + decimal_places = int(autopct.split(".")[1][0]) + except (IndexError, ValueError): + decimal_places = 1 + + # Pie series options + series.show_in_legend = legend + series.start_angle = startangle + series.shadow = shadow + + # Data labels configuration + series.data_labels = { + "enabled": True, + "format": f"{{point.percentage:.{decimal_places}f}}%", + "distance": 20, + "style": { + "fontSize": "14px", + "fontWeight": "normal", + "fontFamily": "Inter, DejaVu Sans, Arial, Helvetica, sans-serif", + "textOutline": "2px white", + }, + } + + chart.add_series(series) + + # Plot options for pie + chart.options.plot_options = { + "pie": { + "allowPointSelect": True, + "cursor": "pointer", + "showInLegend": legend, + "startAngle": startangle, + "shadow": shadow, + "center": ["50%", "50%"], + "size": "75%", + } + } + + # Legend configuration + if legend: + # Map legend_loc to Highcharts position + legend_config = { + "enabled": True, + "align": "right", + "verticalAlign": "middle", + "layout": "vertical", + "itemStyle": {"fontSize": "16px", "fontFamily": "Inter, DejaVu Sans, Arial, Helvetica, sans-serif"}, + "backgroundColor": "#ffffff", + "borderWidth": 1, + "borderRadius": 5, + } + + if legend_loc in ["left"]: + legend_config["align"] = "left" + elif legend_loc in ["right"]: + legend_config["align"] = "right" + elif legend_loc in ["top", "upper center"]: + legend_config["align"] = "center" + legend_config["verticalAlign"] = "top" + legend_config["layout"] = "horizontal" + elif legend_loc in ["bottom", "lower center"]: + legend_config["align"] = "center" + legend_config["verticalAlign"] = "bottom" + legend_config["layout"] = "horizontal" + + chart.options.legend = legend_config + else: + chart.options.legend = {"enabled": False} + + # Tooltip configuration + chart.options.tooltip = { + "pointFormat": "{point.percentage:.1f}%
Value: {point.y}", + "style": {"fontSize": "14px", "fontFamily": "Inter, DejaVu Sans, Arial, Helvetica, sans-serif"}, + } + + # Credits + chart.options.credits = {"enabled": False} + + return chart + + +if __name__ == "__main__": + import tempfile + import time + import urllib.request + from pathlib import Path + + from selenium import webdriver + from selenium.webdriver.chrome.options import Options + + # Sample data for testing (from spec) + sample_data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} + ) + + # Create plot + chart = create_plot(sample_data, "category", "value", title="Market Share Distribution") + + # Download Highcharts JS (required for headless Chrome which can't load CDN) + highcharts_url = "https://code.highcharts.com/highcharts.js" + with urllib.request.urlopen(highcharts_url, timeout=30) as response: + highcharts_js = response.read().decode("utf-8") + + # Export to PNG via Selenium screenshot + 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("--disable-gpu") + chrome_options.add_argument("--window-size=1600,900") + + driver = webdriver.Chrome(options=chrome_options) + driver.get(f"file://{temp_path}") + time.sleep(5) # 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") From c6d7bf49f34a01dd0849a01c6e28b43892b8df11 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:26:57 +0000 Subject: [PATCH 8/9] feat(matplotlib): implement pie-basic (#153) ## Summary Implements `pie-basic` for **matplotlib** library. **Parent Issue:** #142 **Sub-Issue:** #143 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/matplotlib/pie/pie-basic/default.py` ## Features - Creates basic pie charts with customizable colors, labels, and legend - Uses PyPlots.ai color palette by default (colorblind-friendly) - Supports percentage labels with configurable format - Includes input validation for empty data, missing columns, negative values, and zero sums - Equal aspect ratio ensures circular shape - Full type hints and Google-style docstrings - Follows all style guide requirements (font sizes, legend styling, etc.) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/matplotlib/pie/pie-basic/default.py | 169 ++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 plots/matplotlib/pie/pie-basic/default.py diff --git a/plots/matplotlib/pie/pie-basic/default.py b/plots/matplotlib/pie/pie-basic/default.py new file mode 100644 index 0000000000..3ed1855eae --- /dev/null +++ b/plots/matplotlib/pie/pie-basic/default.py @@ -0,0 +1,169 @@ +""" +pie-basic: Basic Pie Chart +Library: matplotlib +""" + +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import pandas as pd + + +if TYPE_CHECKING: + from matplotlib.figure import Figure + + +# PyPlots.ai color palette +PYPLOTS_COLORS = [ + "#306998", # Python Blue + "#FFD43B", # Python Yellow + "#DC2626", # Signal Red + "#059669", # Teal Green + "#8B5CF6", # Violet + "#F97316", # Orange +] + + +def create_plot( + data: pd.DataFrame, + category: str, + value: str, + figsize: tuple[float, float] = (10, 8), + title: str | None = None, + colors: list[str] | None = None, + startangle: float = 90, + autopct: str = "%1.1f%%", + explode: list[float] | None = None, + shadow: bool = False, + labels: list[str] | None = None, + legend: bool = True, + legend_loc: str = "best", + **kwargs, +) -> "Figure": + """ + Create a basic pie chart showing proportions of categorical data. + + Args: + data: Input DataFrame containing category and value columns. + category: Column name for category labels. + value: Column name for numeric values. + figsize: Figure size as (width, height). Defaults to (10, 8). + title: Plot title. Defaults to None. + colors: Custom color palette for slices. Defaults to PyPlots palette. + startangle: Starting angle for first slice in degrees. Defaults to 90. + autopct: Format string for percentage labels. Defaults to '%1.1f%%'. + explode: Offset distances for each slice. Defaults to None. + shadow: Add shadow effect for 3D appearance. Defaults to False. + labels: Custom labels for slices. Defaults to category values. + legend: Display legend. Defaults to True. + legend_loc: Legend location. Defaults to 'best'. + **kwargs: Additional parameters passed to ax.pie(). + + Returns: + Matplotlib Figure object. + + Raises: + ValueError: If data is empty or values contain negatives or sum to zero. + KeyError: If required columns are not found in data. + + Example: + >>> data = pd.DataFrame({ + ... 'category': ['A', 'B', 'C'], + ... 'value': [30, 50, 20] + ... }) + >>> fig = create_plot(data, 'category', 'value', title='Distribution') + """ + # Input validation + if data.empty: + raise ValueError("Data cannot be empty") + + for col in [category, value]: + if col not in data.columns: + available = ", ".join(data.columns) + raise KeyError(f"Column '{col}' not found. Available: {available}") + + values = data[value] + if (values < 0).any(): + raise ValueError("Values cannot be negative for pie charts") + + if values.sum() == 0: + raise ValueError("Values cannot all be zero") + + # Prepare data + categories = data[category].tolist() + pie_values = values.tolist() + slice_labels = labels if labels is not None else categories + + # Use PyPlots colors if not provided + if colors is None: + n_slices = len(pie_values) + colors = (PYPLOTS_COLORS * ((n_slices // len(PYPLOTS_COLORS)) + 1))[:n_slices] + + # Create figure + fig, ax = plt.subplots(figsize=figsize) + + # Configure text properties for style guide compliance + textprops = {"fontsize": 14, "fontfamily": ["Inter", "DejaVu Sans", "Arial", "Helvetica", "sans-serif"]} + + # Plot pie chart + wedges, texts, autotexts = ax.pie( + pie_values, + labels=slice_labels if not legend else None, + colors=colors, + startangle=startangle, + autopct=autopct, + explode=explode, + shadow=shadow, + textprops=textprops, + **kwargs, + ) + + # Style percentage labels + for autotext in autotexts: + autotext.set_fontsize(14) + autotext.set_fontweight("bold") + + # Ensure circular shape (equal aspect ratio) + ax.set_aspect("equal") + + # Add title if provided + if title: + ax.set_title( + title, + fontsize=20, + fontweight="semibold", + fontfamily=["Inter", "DejaVu Sans", "Arial", "Helvetica", "sans-serif"], + pad=20, + ) + + # Add legend if requested + if legend: + ax.legend( + wedges, + slice_labels, + loc=legend_loc, + fontsize=16, + frameon=True, + facecolor="white", + edgecolor="black", + framealpha=1.0, + ) + + # Tight layout + plt.tight_layout() + + return fig + + +if __name__ == "__main__": + # Sample data for testing + sample_data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} + ) + + # Create plot + fig = create_plot(sample_data, "category", "value", title="Market Share Distribution") + + # Save - ALWAYS use 'plot.png'! + plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white") + print("Plot saved to plot.png") From 7e84af90418308d4d796b40942e61fd27f258922 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:27:45 +0000 Subject: [PATCH 9/9] feat(plotnine): implement pie-basic (#158) ## Summary Implements `pie-basic` for **plotnine** library. **Parent Issue:** #142 **Sub-Issue:** #148 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/plotnine/pie/pie-basic/default.py` ## Notes - plotnine (ggplot2 for Python) does not support `coord_polar()` as of version 0.15.x - This implementation uses matplotlib directly (plotnine's underlying rendering engine) while maintaining a compatible interface - All spec parameters are supported: colors, startangle, autopct, explode, shadow, labels, legend - Uses PyPlots.ai color palette by default - Full input validation for empty data, missing columns, and negative values Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/plotnine/pie/pie-basic/default.py | 195 ++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 plots/plotnine/pie/pie-basic/default.py diff --git a/plots/plotnine/pie/pie-basic/default.py b/plots/plotnine/pie/pie-basic/default.py new file mode 100644 index 0000000000..cf8942839e --- /dev/null +++ b/plots/plotnine/pie/pie-basic/default.py @@ -0,0 +1,195 @@ +""" +pie-basic: Basic Pie Chart +Library: plotnine + +A fundamental pie chart that visualizes proportions and percentages of categorical data +as slices of a circular chart. + +Note: plotnine (ggplot2 for Python) does not support coord_polar() as of version 0.15.x, +which is required for true pie charts in the grammar of graphics. This implementation +uses matplotlib directly (plotnine's underlying engine) to create the pie chart while +maintaining a compatible interface and following PyPlots.ai style guidelines. +""" + +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import pandas as pd + + +if TYPE_CHECKING: + from matplotlib.figure import Figure + +# PyPlots.ai color palette +PYPLOTS_COLORS = [ + "#306998", # Python Blue (Primary) + "#FFD43B", # Python Yellow + "#DC2626", # Signal Red + "#059669", # Teal Green + "#8B5CF6", # Violet + "#F97316", # Orange +] + + +def create_plot( + data: pd.DataFrame, + category: str, + value: str, + figsize: tuple[float, float] = (10, 8), + title: str | None = None, + colors: list[str] | None = None, + startangle: float = 90, + autopct: str = "%1.1f%%", + explode: list[float] | None = None, + shadow: bool = False, + labels: list[str] | None = None, + legend: bool = True, + legend_loc: str = "best", + **kwargs, +) -> "Figure": + """ + Create a basic pie chart. + + Note: plotnine does not support polar coordinates (coord_polar), so this + implementation uses matplotlib directly while maintaining a compatible interface. + + Args: + data: Input DataFrame containing category and value columns + category: Column name for category names (slice labels) + value: Column name for numeric values (slice proportions) + figsize: Figure size as (width, height) + title: Plot title (optional) + colors: Custom color palette for slices (defaults to PyPlots palette) + startangle: Starting angle for first slice in degrees (default 90 = top) + autopct: Format string for percentage labels + explode: Offset distances for each slice (0-0.1 typical) + shadow: Add shadow effect for 3D appearance + labels: Custom labels (defaults to category names) + legend: Display legend + legend_loc: Legend location ('best', 'upper right', 'lower left', etc.) + **kwargs: Additional parameters passed to matplotlib pie() + + Returns: + matplotlib Figure object + + Raises: + ValueError: If data is empty or values contain negative numbers + KeyError: If required columns are not found in data + + Example: + >>> data = pd.DataFrame({ + ... 'category': ['A', 'B', 'C', 'D'], + ... 'value': [35, 25, 20, 20] + ... }) + >>> fig = create_plot(data, 'category', 'value', title='Distribution') + """ + # Input validation + if data.empty: + raise ValueError("Data cannot be empty") + + for col in [category, value]: + if col not in data.columns: + available = ", ".join(data.columns) + raise KeyError(f"Column '{col}' not found. Available: {available}") + + # Validate non-negative values + if (data[value] < 0).any(): + raise ValueError("Pie chart values must be non-negative") + + # Handle case where all values sum to zero + total = data[value].sum() + if total == 0: + raise ValueError("Total of values cannot be zero") + + # Prepare data + values = data[value].tolist() + category_labels = labels if labels is not None else data[category].astype(str).tolist() + + # Validate labels length if custom labels provided + if labels is not None and len(labels) != len(data): + raise ValueError(f"Labels length ({len(labels)}) must match data length ({len(data)})") + + # Set up colors + n_categories = len(data) + if colors is None: + # Extend palette if more categories than colors + plot_colors = (PYPLOTS_COLORS * ((n_categories // len(PYPLOTS_COLORS)) + 1))[:n_categories] + else: + if len(colors) < n_categories: + plot_colors = (colors * ((n_categories // len(colors)) + 1))[:n_categories] + else: + plot_colors = colors[:n_categories] + + # Create figure with white background + fig, ax = plt.subplots(figsize=figsize, facecolor="white") + ax.set_facecolor("white") + + # Configure explode + pie_explode = explode if explode is not None else None + if pie_explode is not None and len(pie_explode) != n_categories: + raise ValueError(f"Explode length ({len(pie_explode)}) must match data length ({n_categories})") + + # Configure text properties for percentage labels + textprops = {"fontsize": 14, "fontweight": "bold", "color": "white"} + + # Create pie chart - hide labels on the pie itself since we'll use legend + wedges, texts, autotexts = ax.pie( + values, + labels=None if legend else category_labels, # Labels on pie only if no legend + autopct=autopct if autopct else None, + startangle=startangle, + colors=plot_colors, + explode=pie_explode, + shadow=shadow, + textprops=textprops, + wedgeprops={"linewidth": 1, "edgecolor": "white"}, + **kwargs, + ) + + # Style the percentage labels with better contrast + for autotext in autotexts: + autotext.set_fontsize(14) + autotext.set_fontweight("bold") + # Use white for darker colors, dark for lighter colors + autotext.set_color("white") + + # Add legend if requested + if legend: + ax.legend( + wedges, + category_labels, + title=category, + loc=legend_loc, + fontsize=14, + title_fontsize=16, + frameon=True, + facecolor="white", + edgecolor="gray", + framealpha=1.0, + ) + + # Add title if provided + if title: + ax.set_title(title, fontsize=20, fontweight="semibold", pad=20) + + # Ensure equal aspect ratio for circular pie + ax.set_aspect("equal") + + # Adjust layout + fig.tight_layout() + + return fig + + +if __name__ == "__main__": + # Sample data for testing + sample_data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} + ) + + # Create plot + fig = create_plot(sample_data, "category", "value", title="Market Share Distribution") + + # Save + fig.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white") + print("Plot saved to plot.png")