Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,25 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="AI-powered Python plotting examples that work with YOUR data" />
<title>pyplots - AI-Powered Plotting Examples</title>
<meta name="description" content="library-agnostic, ai-powered python plotting examples. automatically generated, tested, and maintained." />
<title>pyplots.ai</title>

<!-- Canonical -->
<link rel="canonical" href="https://pyplots.ai/" />

<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://pyplots.ai/" />
<meta property="og:title" content="pyplots.ai" />
<meta property="og:description" content="library-agnostic, ai-powered python plotting examples. automatically generated, tested, and maintained." />
<meta property="og:image" content="https://pyplots.ai/og-image.png" />
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding the og:site_name meta property to distinguish the site name from the page title. This helps social media platforms display a more informative preview. Example: <meta property="og:site_name" content="pyplots.ai" />

Suggested change
<meta property="og:image" content="https://pyplots.ai/og-image.png" />
<meta property="og:image" content="https://pyplots.ai/og-image.png" />
<meta property="og:site_name" content="pyplots.ai" />

Copilot uses AI. Check for mistakes.
<meta property="og:site_name" content="pyplots.ai" />

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="pyplots.ai" />
<meta name="twitter:description" content="library-agnostic, ai-powered python plotting examples. automatically generated, tested, and maintained." />
<meta name="twitter:image" content="https://pyplots.ai/og-image.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700;800&display=swap" rel="stylesheet">
Expand Down
Binary file added app/public/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions app/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://pyplots.ai/sitemap.xml
9 changes: 9 additions & 0 deletions app/public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://pyplots.ai/</loc>
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The sitemap.xml is missing the <lastmod> element, which is recommended for search engines to understand when content was last updated. Consider adding it with the current date or deployment date, e.g., <lastmod>2024-12-07</lastmod>.

Suggested change
<loc>https://pyplots.ai/</loc>
<loc>https://pyplots.ai/</loc>
<lastmod>2024-12-07</lastmod>

Copilot uses AI. Check for mistakes.
<lastmod>2025-12-07</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
32 changes: 19 additions & 13 deletions plots/bokeh/scatter/scatter-color-groups/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,25 @@
np.random.seed(42)
n_per_group = 50

data = pd.DataFrame({
"sepal_length": np.concatenate([
np.random.normal(5.0, 0.35, n_per_group),
np.random.normal(5.9, 0.50, n_per_group),
np.random.normal(6.6, 0.60, n_per_group),
]),
"sepal_width": np.concatenate([
np.random.normal(3.4, 0.38, n_per_group),
np.random.normal(2.8, 0.30, n_per_group),
np.random.normal(3.0, 0.30, n_per_group),
]),
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
})
data = pd.DataFrame(
{
"sepal_length": np.concatenate(
[
np.random.normal(5.0, 0.35, n_per_group),
np.random.normal(5.9, 0.50, n_per_group),
np.random.normal(6.6, 0.60, n_per_group),
]
),
"sepal_width": np.concatenate(
[
np.random.normal(3.4, 0.38, n_per_group),
np.random.normal(2.8, 0.30, n_per_group),
np.random.normal(3.0, 0.30, n_per_group),
]
),
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
}
)

# Color palette (from style guide)
colors = ["#306998", "#FFD43B", "#DC2626", "#059669", "#8B5CF6", "#F97316"]
Expand Down
32 changes: 19 additions & 13 deletions plots/matplotlib/scatter/scatter-color-groups/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@
np.random.seed(42)
n_per_group = 50

data = pd.DataFrame({
"sepal_length": np.concatenate([
np.random.normal(5.0, 0.35, n_per_group),
np.random.normal(5.9, 0.50, n_per_group),
np.random.normal(6.6, 0.60, n_per_group),
]),
"sepal_width": np.concatenate([
np.random.normal(3.4, 0.38, n_per_group),
np.random.normal(2.8, 0.30, n_per_group),
np.random.normal(3.0, 0.30, n_per_group),
]),
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
})
data = pd.DataFrame(
{
"sepal_length": np.concatenate(
[
np.random.normal(5.0, 0.35, n_per_group),
np.random.normal(5.9, 0.50, n_per_group),
np.random.normal(6.6, 0.60, n_per_group),
]
),
"sepal_width": np.concatenate(
[
np.random.normal(3.4, 0.38, n_per_group),
np.random.normal(2.8, 0.30, n_per_group),
np.random.normal(3.0, 0.30, n_per_group),
]
),
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
}
)

# Color palette (colorblind safe from style guide)
colors = ["#306998", "#FFD43B", "#DC2626"]
Expand Down
32 changes: 19 additions & 13 deletions plots/plotnine/point/scatter-color-groups/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@
np.random.seed(42)
n_per_group = 50

data = pd.DataFrame({
"sepal_length": np.concatenate([
np.random.normal(5.0, 0.35, n_per_group),
np.random.normal(5.9, 0.50, n_per_group),
np.random.normal(6.6, 0.60, n_per_group),
]),
"sepal_width": np.concatenate([
np.random.normal(3.4, 0.38, n_per_group),
np.random.normal(2.8, 0.30, n_per_group),
np.random.normal(3.0, 0.30, n_per_group),
]),
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
})
data = pd.DataFrame(
{
"sepal_length": np.concatenate(
[
np.random.normal(5.0, 0.35, n_per_group),
np.random.normal(5.9, 0.50, n_per_group),
np.random.normal(6.6, 0.60, n_per_group),
]
),
"sepal_width": np.concatenate(
[
np.random.normal(3.4, 0.38, n_per_group),
np.random.normal(2.8, 0.30, n_per_group),
np.random.normal(3.0, 0.30, n_per_group),
]
),
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
}
)

# Color palette (from style guide)
colors = ["#306998", "#FFD43B", "#DC2626"]
Expand Down
29 changes: 19 additions & 10 deletions tests/unit/prompts/test_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"plotnine.md",
"pygal.md",
"highcharts.md",
"letsplot.md",
]


Expand Down Expand Up @@ -81,15 +82,21 @@ def quality_criteria_content(self) -> str:
return (PROMPTS_DIR / "quality-criteria.md").read_text()

def test_plot_generator_has_required_sections(self, plot_generator_content: str) -> None:
"""Plot generator should have Role, Task, Rules sections."""
required_sections = ["## Role", "## Task", "## Rules", "## Output"]
"""Plot generator should have Role, Task, Output sections."""
# Core sections at level 2
required_sections = ["## Role", "## Task", "## Output"]
for section in required_sections:
assert section in plot_generator_content, f"Missing section: {section}"
# Rules can be at level 2 or 3 (### Rules under ## Output)
assert "Rules" in plot_generator_content, "Missing Rules section"

def test_plot_generator_has_code_template(self, plot_generator_content: str) -> None:
"""Plot generator should include a code template."""
assert "```python" in plot_generator_content, "Missing Python code template"
assert "def create_plot" in plot_generator_content, "Missing create_plot function template"
# KISS style: simple scripts with comments, not functions
assert "# Create plot" in plot_generator_content or "plt.savefig" in plot_generator_content, (
"Missing plot creation example"
)

def test_quality_criteria_has_scoring_section(self, quality_criteria_content: str) -> None:
"""Quality criteria should have scoring information."""
Expand All @@ -107,8 +114,9 @@ def test_library_prompt_has_required_sections(self, filename: str) -> None:
content = (LIBRARY_PROMPTS_DIR / filename).read_text()
library_name = filename.replace(".md", "")

# Check for header
assert f"# {library_name}" in content.lower(), f"Missing header for {library_name}"
# Check for header (normalize by removing hyphens for comparison)
content_normalized = content.lower().replace("-", "")
assert f"# {library_name}" in content_normalized, f"Missing header for {library_name}"

# Check for import section
assert "## Import" in content or "import" in content.lower(), f"Missing import section in {filename}"
Expand All @@ -117,12 +125,13 @@ def test_library_prompt_has_required_sections(self, filename: str) -> None:
assert "```python" in content, f"Missing Python code examples in {filename}"

@pytest.mark.parametrize("filename", EXPECTED_LIBRARY_PROMPTS)
def test_library_prompt_has_return_type(self, filename: str) -> None:
"""Each library prompt should specify return type."""
def test_library_prompt_has_save_section(self, filename: str) -> None:
"""Each library prompt should show how to save the plot."""
content = (LIBRARY_PROMPTS_DIR / filename).read_text()
# Either explicit return type section or type hint in code
has_return_type = "## Return Type" in content or "-> " in content
assert has_return_type, f"Missing return type specification in {filename}"
# KISS style: prompts show how to save, not function return types
save_patterns = ["## Save", "savefig", "save(", "write_image", "save_screenshot", "export_png"]
has_save_info = any(pattern in content for pattern in save_patterns)
assert has_save_info, f"Missing save/output section in {filename}"


class TestNoPlaceholders:
Expand Down
Loading