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")