diff --git a/app/index.html b/app/index.html
index 75fb64b57d..0ed5adc3a9 100644
--- a/app/index.html
+++ b/app/index.html
@@ -4,8 +4,25 @@
-
-
pyplots - AI-Powered Plotting Examples
+
+ pyplots.ai
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/og-image.png b/app/public/og-image.png
new file mode 100644
index 0000000000..964eecd1e4
Binary files /dev/null and b/app/public/og-image.png differ
diff --git a/app/public/robots.txt b/app/public/robots.txt
new file mode 100644
index 0000000000..0a50661f44
--- /dev/null
+++ b/app/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://pyplots.ai/sitemap.xml
diff --git a/app/public/sitemap.xml b/app/public/sitemap.xml
new file mode 100644
index 0000000000..9d31fb2932
--- /dev/null
+++ b/app/public/sitemap.xml
@@ -0,0 +1,9 @@
+
+
+
+ https://pyplots.ai/
+ 2025-12-07
+ weekly
+ 1.0
+
+
diff --git a/plots/bokeh/scatter/scatter-color-groups/default.py b/plots/bokeh/scatter/scatter-color-groups/default.py
index 5b0fe00573..77106ff43d 100644
--- a/plots/bokeh/scatter/scatter-color-groups/default.py
+++ b/plots/bokeh/scatter/scatter-color-groups/default.py
@@ -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"]
diff --git a/plots/matplotlib/scatter/scatter-color-groups/default.py b/plots/matplotlib/scatter/scatter-color-groups/default.py
index 7f45e2c195..8270328253 100644
--- a/plots/matplotlib/scatter/scatter-color-groups/default.py
+++ b/plots/matplotlib/scatter/scatter-color-groups/default.py
@@ -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"]
diff --git a/plots/plotnine/point/scatter-color-groups/default.py b/plots/plotnine/point/scatter-color-groups/default.py
index 5c63833376..367559f3c5 100644
--- a/plots/plotnine/point/scatter-color-groups/default.py
+++ b/plots/plotnine/point/scatter-color-groups/default.py
@@ -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"]
diff --git a/tests/unit/prompts/test_prompts.py b/tests/unit/prompts/test_prompts.py
index 6fdf74a00c..660d6428e8 100644
--- a/tests/unit/prompts/test_prompts.py
+++ b/tests/unit/prompts/test_prompts.py
@@ -38,6 +38,7 @@
"plotnine.md",
"pygal.md",
"highcharts.md",
+ "letsplot.md",
]
@@ -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."""
@@ -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}"
@@ -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: