From f5de97ad2524414f1a41c912e384fd85fe64a9e4 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:53:12 +0000 Subject: [PATCH 1/9] feat(altair): implement pie-basic (#352) ## Summary Implements `pie-basic` for **altair** library. **Parent Issue:** #206 **Sub-Issue:** #265 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/altair/arc/pie-basic/default.py` ## Changes - Simplified implementation to follow KISS principle (script style, no function wrapper) - Uses PyPlots.ai default color palette for consistent styling - Includes percentage labels on pie slices - Adds interactive tooltips showing category, value, and percentage - Properly configured legend and chart dimensions Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/altair/arc/pie-basic/default.py | 194 +++++++------------------- 1 file changed, 48 insertions(+), 146 deletions(-) diff --git a/plots/altair/arc/pie-basic/default.py b/plots/altair/arc/pie-basic/default.py index fe0493305c..a56cc8fff7 100644 --- a/plots/altair/arc/pie-basic/default.py +++ b/plots/altair/arc/pie-basic/default.py @@ -7,152 +7,54 @@ 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 +# Data +data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} +) - # 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 +# Calculate percentages for labels +total = data["value"].sum() +data["percentage"] = data["value"] / total - # 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") +# PyPlots.ai default color palette +PYPLOTS_COLORS = ["#306998", "#FFD43B", "#DC2626", "#059669", "#8B5CF6", "#F97316"] - # Save - fig.save("plot.png", scale_factor=2.0) - print("Plot saved to plot.png") +# Create pie chart using arc mark +# Altair uses theta encoding for pie/arc charts +base = alt.Chart(data).encode( + theta=alt.Theta("value:Q", stack=True), + color=alt.Color( + "category:N", + scale=alt.Scale(range=PYPLOTS_COLORS), + legend=alt.Legend(title="Category", orient="right", labelFontSize=16, titleFontSize=16), + ), + tooltip=[ + alt.Tooltip("category:N", title="Category"), + alt.Tooltip("value:Q", title="Value"), + alt.Tooltip("percentage:Q", title="Share", format=".1%"), + ], +) + +# Create the pie with arc mark +pie = base.mark_arc(innerRadius=0, outerRadius=300, stroke="#ffffff", strokeWidth=2) + +# Add percentage labels on slices +text = base.mark_text(radius=200, fontSize=20, fontWeight="bold", color="#FFFFFF").encode( + text=alt.Text("percentage:Q", format=".1%") +) + +# Combine pie and labels +chart = alt.layer(pie, text).properties( + width=800, + height=800, + title=alt.TitleParams(text="Market Share Distribution", fontSize=20, anchor="middle", fontWeight="bold"), +) + +# Configure chart appearance +chart = chart.configure_view(strokeWidth=0).configure_legend( + labelFontSize=16, titleFontSize=16, symbolSize=200, padding=20 +) + +# Save - scale_factor=3 gives 2400x2400 from 800x800 base +# For pie charts, square aspect ratio is more appropriate +chart.save("plot.png", scale_factor=3.0) From cb0ea5f72f8d45b74834031816149ab1d4b7bcb2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:53:26 +0000 Subject: [PATCH 2/9] feat(matplotlib): implement pie-basic (#347) ## Summary Implements `pie-basic` for **matplotlib** library. **Parent Issue:** #206 **Sub-Issue:** #235 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/matplotlib/pie/pie-basic/default.py` ## Changes - Simplified implementation following KISS principle (no functions, no classes) - Uses style guide colors: Python Blue, Python Yellow, Signal Red, Teal Green, Violet - Proper figure sizing (16x9 aspect ratio) for 4800x2700px output at 300 DPI - Clear percentage labels and category labels with appropriate font sizes - Slight explode effect for better visual separation Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/matplotlib/pie/pie-basic/default.py | 182 ++++------------------ 1 file changed, 29 insertions(+), 153 deletions(-) diff --git a/plots/matplotlib/pie/pie-basic/default.py b/plots/matplotlib/pie/pie-basic/default.py index 3ed1855eae..5ef2be8847 100644 --- a/plots/matplotlib/pie/pie-basic/default.py +++ b/plots/matplotlib/pie/pie-basic/default.py @@ -3,167 +3,43 @@ 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, - ) +# Data +data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} +) - # 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, - ) +# Colors from style guide +colors = ["#306998", "#FFD43B", "#DC2626", "#059669", "#8B5CF6"] - # Tight layout - plt.tight_layout() +# Create plot +fig, ax = plt.subplots(figsize=(16, 9)) - return fig +wedges, texts, autotexts = ax.pie( + data["value"], + labels=data["category"], + colors=colors, + autopct="%1.1f%%", + startangle=90, + explode=[0.02] * len(data), + wedgeprops={"linewidth": 2, "edgecolor": "white"}, +) +# Style the text +for text in texts: + text.set_fontsize(20) +for autotext in autotexts: + autotext.set_fontsize(16) + autotext.set_color("white") + autotext.set_fontweight("bold") -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]} - ) +ax.set_title("Basic Pie Chart", fontsize=20, fontweight="bold", pad=20) - # Create plot - fig = create_plot(sample_data, "category", "value", title="Market Share Distribution") +# Equal aspect ratio ensures circular pie +ax.set_aspect("equal") - # Save - ALWAYS use 'plot.png'! - plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white") - print("Plot saved to plot.png") +plt.tight_layout() +plt.savefig("plot.png", dpi=300, bbox_inches="tight") From 10f5a58b27efc4c38a00f0069e1410303ca304a8 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:53:57 +0000 Subject: [PATCH 3/9] feat(seaborn): implement pie-basic (#358) ## Summary Implements `pie-basic` for **seaborn** library. **Parent Issue:** #206 **Sub-Issue:** #243 **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 - Uses matplotlib's `ax.pie()` with seaborn's styling context (`sns.set_theme(style="white")`) for consistent aesthetics - Follows KISS script style (no functions, no type hints) - Uses PyPlots.ai color palette - Configured for 16:9 aspect ratio output at 300 DPI - Font sizes follow style guide (title: 20pt, legend/labels: 16pt) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/seaborn/pie/pie-basic/default.py | 220 +++++++------------------ 1 file changed, 58 insertions(+), 162 deletions(-) diff --git a/plots/seaborn/pie/pie-basic/default.py b/plots/seaborn/pie/pie-basic/default.py index dbaf1eb03a..7cdcf12f58 100644 --- a/plots/seaborn/pie/pie-basic/default.py +++ b/plots/seaborn/pie/pie-basic/default.py @@ -6,20 +6,14 @@ 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) + "#306998", # Python Blue "#FFD43B", # Python Yellow "#DC2626", # Signal Red "#059669", # Teal Green @@ -27,158 +21,60 @@ "#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") +# Data from spec +data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} +) + +# Set seaborn style for consistent aesthetics +sns.set_theme(style="white") + +# Create figure (16:9 aspect ratio for 4800x2700 at 300 DPI) +fig, ax = plt.subplots(figsize=(16, 9)) + +# Extract data +categories = data["category"].tolist() +values = data["value"].tolist() + +# Use PyPlots colors +colors = PYPLOTS_COLORS[: len(categories)] + +# Create pie chart +wedges, texts, autotexts = ax.pie( + values, + autopct="%1.1f%%", + startangle=90, + colors=colors, + wedgeprops={"edgecolor": "white", "linewidth": 2}, + textprops={"fontsize": 16}, + pctdistance=0.7, +) + +# Style percentage labels +for autotext in autotexts: + autotext.set_fontsize(16) + autotext.set_fontweight("bold") + autotext.set_color("white") + +# Ensure circular shape +ax.set_aspect("equal") + +# Add legend +ax.legend( + wedges, + categories, + title="Category", + loc="center left", + bbox_to_anchor=(1.0, 0.5), + fontsize=16, + title_fontsize=16, + frameon=True, + facecolor="white", + edgecolor="gray", +) + +# Title +ax.set_title("Market Share Distribution", fontsize=20, fontweight="semibold", pad=20) + +plt.tight_layout() +plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white") From 759606c0b4afe6fdc264c384ee670d3037cb7d81 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:54:34 +0000 Subject: [PATCH 4/9] feat(plotly): implement pie-basic (#363) ## Summary Implements `pie-basic` for **plotly** library. **Parent Issue:** #206 **Sub-Issue:** #251 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/plotly/pie/pie-basic/default.py` ## Changes - Simplified implementation following KISS pattern (no functions, sequential code) - Uses PyPlots color palette from style guide - Proper 16:9 aspect ratio (4800x2700px via scale=3) - Clean Plotly white template with legend on right side - Labels with percentages displayed inside pie slices Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/plotly/pie/pie-basic/default.py | 229 ++++---------------------- 1 file changed, 28 insertions(+), 201 deletions(-) diff --git a/plots/plotly/pie/pie-basic/default.py b/plots/plotly/pie/pie-basic/default.py index 4ef5cfcebd..77fe25f676 100644 --- a/plots/plotly/pie/pie-basic/default.py +++ b/plots/plotly/pie/pie-basic/default.py @@ -3,215 +3,42 @@ 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() +# Data +data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} +) - # 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)] +# Color palette from style guide +colors = ["#306998", "#FFD43B", "#DC2626", "#059669", "#8B5CF6"] - # 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( +# Create plot +fig = go.Figure( + data=[ 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"}, + labels=data["category"], + values=data["value"], + marker={"colors": colors, "line": {"color": "white", "width": 2}}, + textinfo="label+percent", + textfont={"size": 16}, textposition="inside", insidetextorientation="horizontal", - pull=pull_values, - rotation=startangle, - showlegend=legend, hovertemplate="%{label}
Value: %{value}
Percentage: %{percent}", - **kwargs, + rotation=90, ) - ) - - # 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") + ] +) + +# Layout +fig.update_layout( + title={"text": "Basic Pie Chart", "font": {"size": 20}, "x": 0.5, "xanchor": "center"}, + legend={"font": {"size": 16}, "orientation": "v", "yanchor": "middle", "y": 0.5, "xanchor": "left", "x": 1.02}, + template="plotly_white", + margin={"l": 50, "r": 150, "t": 80, "b": 50}, +) + +# Save (4800 x 2700 px using scale=3 with 1600x900 base) +fig.write_image("plot.png", width=1600, height=900, scale=3) From 9a3e634e5e74b8e1f5d7aaf287ee9143bc0a5980 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:54:56 +0000 Subject: [PATCH 5/9] feat(pygal): implement pie-basic (#354) ## Summary Implements `pie-basic` for **pygal** library. **Parent Issue:** #206 **Sub-Issue:** #273 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/pygal/pie/pie-basic/default.py` ## Changes - Simplified implementation following KISS principles - Removed function wrapper, type hints, and docstrings per plot-generator.md guidelines - Direct sequential code matching gallery example style - Uses PyPlots color palette from style guide - 4800x2700 px output with recommended font sizes - Legend at bottom for clear visibility Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/pygal/pie/pie-basic/default.py | 165 +++++---------------------- 1 file changed, 31 insertions(+), 134 deletions(-) diff --git a/plots/pygal/pie/pie-basic/default.py b/plots/pygal/pie/pie-basic/default.py index ca44c93104..1e9f933de1 100644 --- a/plots/pygal/pie/pie-basic/default.py +++ b/plots/pygal/pie/pie-basic/default.py @@ -3,145 +3,42 @@ 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 +# Data +categories = ["Product A", "Product B", "Product C", "Product D", "Other"] +values = [35, 25, 20, 15, 5] + +# Custom style matching default-style-guide.md colors +custom_style = Style( + background="white", + plot_background="white", + foreground="#333333", + foreground_strong="#333333", + foreground_subtle="#666666", + colors=("#306998", "#FFD43B", "#DC2626", "#059669", "#8B5CF6"), + title_font_size=60, + legend_font_size=48, + value_font_size=48, + tooltip_font_size=36, ) +# Create pie chart +chart = pygal.Pie( + width=4800, + height=2700, + title="Market Share Distribution", + style=custom_style, + inner_radius=0, + show_legend=True, + legend_at_bottom=True, +) -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") +# Add data slices +for category, value in zip(categories, values, strict=True): + chart.add(category, value) - # Save to PNG - chart.render_to_png("plot.png") - print("Plot saved to plot.png") +# Save as PNG +chart.render_to_png("plot.png") From b63611f5356c1df972ef2010170b8b4a748233d4 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:56:21 +0000 Subject: [PATCH 6/9] feat(plotnine): implement pie-basic (#370) ## Summary Implements `pie-basic` for **plotnine** library. **Parent Issue:** #206 **Sub-Issue:** #270 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/plotnine/pie/pie-basic/default.py` ## Details - Simplified to KISS pattern (no functions, sequential script) - Uses matplotlib directly since plotnine lacks `coord_polar()` support - Applies PyPlots.ai color palette - Uses 16:9 aspect ratio (figsize 16x9 at 300 dpi) - Legend positioned outside pie for clarity - Percentage labels displayed on each slice Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/plotnine/pie/pie-basic/default.py | 229 ++++++------------------ 1 file changed, 53 insertions(+), 176 deletions(-) diff --git a/plots/plotnine/pie/pie-basic/default.py b/plots/plotnine/pie/pie-basic/default.py index cf8942839e..7ef7381672 100644 --- a/plots/plotnine/pie/pie-basic/default.py +++ b/plots/plotnine/pie/pie-basic/default.py @@ -2,27 +2,22 @@ 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. +Note: plotnine does not support coord_polar() required for pie charts. +This implementation uses matplotlib directly while following PyPlots.ai style. """ -from typing import TYPE_CHECKING - import matplotlib.pyplot as plt import pandas as pd -if TYPE_CHECKING: - from matplotlib.figure import Figure +# Data +data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} +) # PyPlots.ai color palette -PYPLOTS_COLORS = [ - "#306998", # Python Blue (Primary) +colors = [ + "#306998", # Python Blue "#FFD43B", # Python Yellow "#DC2626", # Signal Red "#059669", # Teal Green @@ -30,166 +25,48 @@ "#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") +# Create figure (16:9 aspect ratio for 4800x2700 at 300 dpi) +fig, ax = plt.subplots(figsize=(16, 9), facecolor="white") +ax.set_facecolor("white") + +# Create pie chart +wedges, texts, autotexts = ax.pie( + data["value"], + autopct="%1.1f%%", + startangle=90, + colors=colors[: len(data)], + textprops={"fontsize": 16, "fontweight": "bold", "color": "white"}, + wedgeprops={"linewidth": 2, "edgecolor": "white"}, +) + +# Style percentage labels +for autotext in autotexts: + autotext.set_fontsize(16) + autotext.set_fontweight("bold") + autotext.set_color("white") + +# Add legend +ax.legend( + wedges, + data["category"], + title="Category", + loc="center left", + bbox_to_anchor=(1.0, 0.5), + fontsize=16, + title_fontsize=20, + frameon=True, + facecolor="white", + edgecolor="gray", +) + +# Title +ax.set_title("Basic Pie Chart", fontsize=20, fontweight="semibold", pad=20) + +# Ensure circular shape +ax.set_aspect("equal") + +# Adjust layout +plt.tight_layout() + +# Save +plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white") From 19fdc386e51c6929d2721879d78ebd6c6fdda41e Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:56:30 +0000 Subject: [PATCH 7/9] feat(highcharts): implement pie-basic (#371) ## Summary Implements `pie-basic` for **highcharts** library. **Parent Issue:** #206 **Sub-Issue:** #276 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/highcharts/pie/pie-basic/default.py` ## Changes - Rewrote implementation to follow KISS principles (simple sequential script) - Removed function wrapper and class structure - Updated to 4800x2700 px dimensions per style guide - Added proper Selenium element screenshot for exact dimensions - Uses PyPlots color palette from style guide Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/highcharts/pie/pie-basic/default.py | 361 +++++++--------------- 1 file changed, 105 insertions(+), 256 deletions(-) diff --git a/plots/highcharts/pie/pie-basic/default.py b/plots/highcharts/pie/pie-basic/default.py index 6eb2507d5f..0cffc89188 100644 --- a/plots/highcharts/pie/pie-basic/default.py +++ b/plots/highcharts/pie/pie-basic/default.py @@ -1,24 +1,29 @@ """ 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 tempfile +import time +import urllib.request +from pathlib import Path import pandas as pd from highcharts_core.chart import Chart from highcharts_core.options import HighchartsOptions from highcharts_core.options.series.pie import PieSeries +from selenium import webdriver +from selenium.webdriver.chrome.options import Options + +# Data (from spec) +data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} +) # Style guide colors -PYPLOTS_COLORS = [ - "#306998", # Python Blue (Primary) +COLORS = [ + "#306998", # Python Blue "#FFD43B", # Python Yellow "#DC2626", # Signal Red "#059669", # Teal Green @@ -26,262 +31,106 @@ "#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%", - } +# Create chart +chart = Chart(container="container") +chart.options = HighchartsOptions() + +# Chart configuration (4800 x 2700 px per style guide) +chart.options.chart = {"type": "pie", "width": 4800, "height": 2700, "backgroundColor": "#ffffff"} + +# Title +chart.options.title = {"text": "Market Share Distribution", "style": {"fontSize": "48px", "fontWeight": "600"}} + +# Build pie data with colors +pie_data = [] +for i, row in data.iterrows(): + pie_data.append({"name": row["category"], "y": row["value"], "color": COLORS[i % len(COLORS)]}) + +# Create pie series +series = PieSeries() +series.data = pie_data +series.name = "Market Share" +series.show_in_legend = True + +# Data labels with percentages +series.data_labels = { + "enabled": True, + "format": "{point.name}: {point.percentage:.1f}%", + "distance": 40, + "style": {"fontSize": "32px", "fontWeight": "normal", "textOutline": "3px white"}, +} + +chart.add_series(series) + +# Plot options for pie +chart.options.plot_options = { + "pie": { + "allowPointSelect": True, + "cursor": "pointer", + "showInLegend": True, + "center": ["50%", "50%"], + "size": "70%", } - - # 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""" +} + +# Legend configuration +chart.options.legend = { + "enabled": True, + "align": "right", + "verticalAlign": "middle", + "layout": "vertical", + "itemStyle": {"fontSize": "32px"}, +} + +# Tooltip +chart.options.tooltip = { + "pointFormat": "{point.percentage:.1f}%
Value: {point.y}", + "style": {"fontSize": "28px"}, +} + +# Disable credits +chart.options.credits = {"enabled": False} + +# Download Highcharts JS (required for headless Chrome) +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") + +# Generate HTML with inline scripts +html_str = chart.to_js_literal() +html_content = f""" -
+
""" - # Write temp HTML and take screenshot - with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") 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") +# Write temp HTML +with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: + f.write(html_content) + temp_path = f.name + +# Take screenshot with Selenium +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=5000,3000") + +driver = webdriver.Chrome(options=chrome_options) +driver.get(f"file://{temp_path}") +time.sleep(5) # Wait for chart to render + +# Screenshot the chart container element for exact dimensions +container = driver.find_element("id", "container") +container.screenshot("plot.png") +driver.quit() + +# Clean up temp file +Path(temp_path).unlink() From b9fb5746f502c6a1ee764a979028565903fa2971 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:58:11 +0000 Subject: [PATCH 8/9] feat(letsplot): implement pie-basic (#377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements `pie-basic` for **letsplot** library. **Parent Issue:** #206 **Sub-Issue:** #279 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/letsplot/pie/pie-basic/default.py` ## Details - Uses lets-plot's `geom_pie()` with `stat='identity'` for pre-computed values - Applies PyPlots.ai color palette via `scale_fill_manual()` - Output dimensions: 4800×2700px (1600×900 base scaled 3x) - Includes title, legend with proper theming using `theme_void()` + custom `theme()` Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- plots/letsplot/pie/pie-basic/default.py | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 plots/letsplot/pie/pie-basic/default.py diff --git a/plots/letsplot/pie/pie-basic/default.py b/plots/letsplot/pie/pie-basic/default.py new file mode 100644 index 0000000000..d83e329af1 --- /dev/null +++ b/plots/letsplot/pie/pie-basic/default.py @@ -0,0 +1,56 @@ +""" +pie-basic: Basic Pie Chart +Library: lets-plot +""" + +import pandas as pd +from lets_plot import ( + LetsPlot, + aes, + element_text, + geom_pie, + ggplot, + ggsave, + ggsize, + labs, + scale_fill_manual, + theme, + theme_void, +) + + +LetsPlot.setup_html() + +# PyPlots.ai color palette +PYPLOTS_COLORS = [ + "#306998", # Python Blue + "#FFD43B", # Python Yellow + "#DC2626", # Signal Red + "#059669", # Teal Green + "#8B5CF6", # Violet + "#F97316", # Orange +] + +# Data +data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} +) + +# Create pie chart +plot = ( + ggplot(data) + + geom_pie(aes(slice="value", fill="category"), stat="identity", stroke=1.5, color="white", size=20) + + scale_fill_manual(values=PYPLOTS_COLORS) + + labs(title="Market Share Distribution", fill="Category") + + theme_void() + + theme( + plot_title=element_text(size=20, face="bold", hjust=0.5), + legend_title=element_text(size=16), + legend_text=element_text(size=14), + legend_position="right", + ) + + ggsize(1600, 900) +) + +# Save - scale 3x to get 4800 x 2700 px +ggsave(plot, "plot.png", path=".", scale=3) From e34da262742cd363d4295bef0a09db8a4ec6c561 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:33:42 +0000 Subject: [PATCH 9/9] feat(bokeh): implement pie-basic (#365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements `pie-basic` for **bokeh** library. **Parent Issue:** #206 **Sub-Issue:** #259 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/bokeh/custom/pie-basic/default.py` ## Changes - Simplified implementation to follow KISS style (no functions, no type hints) - Uses wedge glyphs to create pie chart (Bokeh has no native pie chart method) - Output size: 4800 × 2700 px per style guide - Features: - PyPlots color palette - Percentage labels on slices (hidden for slices < 5%) - Interactive tooltips with category, value, and percentage - Legend on right side - Centered title Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> --- plots/bokeh/custom/pie-basic/default.py | 300 ++++++++---------------- 1 file changed, 101 insertions(+), 199 deletions(-) diff --git a/plots/bokeh/custom/pie-basic/default.py b/plots/bokeh/custom/pie-basic/default.py index bf784be68d..294b3671d5 100644 --- a/plots/bokeh/custom/pie-basic/default.py +++ b/plots/bokeh/custom/pie-basic/default.py @@ -4,18 +4,15 @@ """ import math -from typing import TYPE_CHECKING import pandas as pd +from bokeh.io import export_png 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 = [ +COLORS = [ "#306998", # Python Blue "#FFD43B", # Python Yellow "#DC2626", # Signal Red @@ -24,198 +21,103 @@ "#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, +# Data +data = pd.DataFrame( + {"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]} +) + +# Calculate angles for pie slices +total = data["value"].sum() +data["angle"] = data["value"] / total * 2 * math.pi +data["percentage"] = data["value"] / total * 100 + +# Calculate cumulative angles for wedge positioning +data["end_angle"] = data["angle"].cumsum() +data["start_angle"] = data["end_angle"] - data["angle"] + +# Apply start angle offset (start from top, 90 degrees) +start_rad = math.radians(90 - 90) # Adjust for Bokeh coordinate system +data["start_angle"] = data["start_angle"] + start_rad +data["end_angle"] = data["end_angle"] + start_rad + +# Assign colors (cycle if more categories than colors) +data["color"] = [COLORS[i % len(COLORS)] for i in range(len(data))] + +# Create ColumnDataSource +source = ColumnDataSource(data) + +# Create figure - 4800 x 2700 px as per style guide +p = figure( + width=4800, + height=2700, + title="Market Share Distribution", + tools="hover", + tooltips=[("Category", "@category"), ("Value", "@value"), ("Percentage", "@percentage{0.1}%")], + x_range=(-1.2, 2.0), + 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 data.iterrows(): + 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 label if slice is large enough + if row["percentage"] >= 5: + label = Label( + x=x, + y=y, + text=f"{row['percentage']:.1f}%", + text_font_size="48pt", + text_align="center", + text_baseline="middle", + text_color="white" if row["percentage"] >= 10 else "black", ) - - 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 + p.add_layout(label) + +# Create legend +legend_items = [] +for i, cat in enumerate(data["category"]): + legend_items.append(LegendItem(label=str(cat), renderers=[renderers], index=i)) + +leg = Legend( + items=legend_items, + location="center", + label_text_font_size="48pt", + background_fill_color="white", + background_fill_alpha=1.0, + border_line_color="black", + border_line_width=1, +) +p.add_layout(leg, "right") + +# Style configuration +p.axis.visible = False +p.grid.visible = False +p.outline_line_color = None + +# Title styling +p.title.text_font_size = "60pt" +p.title.align = "center" + +# Background +p.background_fill_color = "white" + +# Save +export_png(p, filename="plot.png")