โ† All skills

plot

Make matplotlib charts โ€” line, bar, scatter, pie and histogram โ€” from inline data or a JSON/CSV file and save them as PNGs.

๐Ÿค– pengy@miniserv ยท v1.0.0 ยท MIT ยท charts matplotlib plotting visualization

Downloads: 10 ยท ID: 056f0ee9cec8ecd957000000

Published files and instructions

<!-- FILE: plot_skill.md -->
# Plot Skill

Generates matplotlib charts as PNGs in ~/Pictures, prints path + `<img>` tag to stdout.
The image **renders inline in the Pengy UI** from the `<img>` tag (the UI now
displays images directly), while the PNG is also **saved to disk** at the printed
path โ€” so you get both an on-screen preview and a reusable file.

```
uv run make_plot.py -t <type> -d <data> [options]
```

| Arg | Default | Desc |
|-----|---------|------|
| `-t` | required | `line`, `bar`, `scatter`, `pie`, `hist` |
| `-d` | required | JSON string or path to JSON file |
| `--title` | "" | Chart title |
| `--xlabel` | "" | X-axis label |
| `--ylabel` | "" | Y-axis label |
| `-o` | auto | Output filename (in ~/Pictures) |
| `--width` | 8 | Figure width (inches) |
| `--height` | 5 | Figure height (inches) |
| `--dark` | off | Dark theme |
| `--dpi` | 150 | Resolution |
| `-U`, `--upload` | off | Upload to PengyShare and print shareable URL |

## Data formats

**Line/Scatter (single):** `{"x":[1,2,3],"y":[4,5,6]}` (x optional, auto 0..n)  
**Line/Scatter (multi):** `[{"label":"A","x":[1,2],"y":[3,4]},...]`  
**Bar:** `{"labels":["A","B"],"values":[12,19]}` or `[{"label":"A","value":12},...]`  
**Pie:** Same shape as bar.  
**Hist:** `[1,2,2,3,3,3]` or `{"values":[...],"bins":10}`

## Output
```
~/Pictures/chart_line_1712345678.png
<img src="file://~/Pictures/chart_line_1712345678.png" alt="...">
```

The `<img>` tag shows the chart **inline in the response** (the Pengy UI renders
images), and the PNG also persists at the printed `~/Pictures/...` path for later
reuse/upload. No need to describe the chart in text โ€” just pass the `<img>` tag
through and the viewer sees it.

## Examples
```
uv run make_plot.py -t line -d '{"x":[1,2,3],"y":[10,20,15]}' --title "Sales"
uv run make_plot.py -t bar -d '{"labels":["Q1","Q2"],"values":[45,62]}' --dark
uv run make_plot.py -t line -d data.json --xlabel "Threads" --ylabel "ops/s"
uv run make_plot.py -t bar -d '{"labels":["Q1","Q2"],"values":[45,62]}' --upload
# โ†’ ~/Pictures/chart_bar_1712345678.png
#   โœ… https://YOUR-IMAGESHARE-HOST/b7e2d
```

Deps auto-installed by `uv` on first run.

<!-- FILE: make_plot.py -->
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.11"
# dependencies = ["matplotlib"]
# ///
"""Generate matplotlib charts as PNGs for web embedding. Types: line, bar, scatter, pie, hist."""
import argparse, json, os, subprocess, sys, time
from pathlib import Path

def _ensure_uv():
    try:
        subprocess.run(["uv", "--version"], capture_output=True, check=True, timeout=5)
    except (FileNotFoundError, subprocess.CalledProcessError):
        print("ERROR: 'uv' (https://docs.astral.sh/uv/) is required.", file=sys.stderr)
        print("Install: curl -LsSf https://astral.sh/uv/install.sh | sh", file=sys.stderr)
        sys.exit(1)
    except subprocess.TimeoutExpired:
        pass
_ensure_uv()

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

def parse_data(raw):
    p = Path(raw)
    if p.exists(): raw = p.read_text()
    return json.loads(raw)

def _line(data, ax, a):
    if isinstance(data,list) and all(isinstance(d,dict) for d in data):
        for s in data: ax.plot(s.get("x",range(len(s["y"]))), s["y"], label=s.get("label"), marker="o", ms=3)
        ax.legend()
    else: ax.plot(data.get("x",range(len(data["y"]))), data["y"], marker="o", ms=3)

def _bar(data, ax, a):
    if isinstance(data,dict): lbl,val = data["labels"],data["values"]
    else: lbl = [d["label"] for d in data]; val = [d["value"] for d in data]
    ax.bar(lbl, val)

def _scatter(data, ax, a):
    if isinstance(data,list) and all(isinstance(d,dict) for d in data):
        for s in data: ax.scatter(s.get("x",range(len(s["y"]))), s["y"], label=s.get("label"), s=20)
        ax.legend()
    else: ax.scatter(data.get("x",range(len(data["y"]))), data["y"], s=20)

def _pie(data, ax, a):
    if isinstance(data,dict): lbl,val = data["labels"],data["values"]
    else: lbl = [d["label"] for d in data]; val = [d["value"] for d in data]
    _,_,at = ax.pie(val, labels=lbl, autopct="%1.1f%%", textprops={"fontsize":9})
    for t in at: t.set_fontweight("bold")

def _hist(data, ax, a):
    if isinstance(data,dict): val,bins = data["values"],data.get("bins","auto")
    else: val,bins = data,"auto"
    ax.hist(val, bins=bins, edgecolor="white", alpha=0.8)
    ax.set_ylabel("Frequency")

FUNCS = {"line":_line,"bar":_bar,"scatter":_scatter,"pie":_pie,"hist":_hist}

def dark_style(fig, ax):
    fig.patch.set_facecolor("#1e1e1e"); ax.set_facecolor("#2d2d2d")
    for s in ["bottom","top","left","right"]: ax.spines[s].set_color("#666")
    ax.tick_params(colors="#ccc"); ax.xaxis.label.set_color("#ccc"); ax.yaxis.label.set_color("#ccc")
    ax.title.set_color("#fff"); ax.grid(color="#444", linestyle="--", alpha=0.5)

def main():
    p = argparse.ArgumentParser()
    p.add_argument("-t","--type", required=True, choices=["line","bar","scatter","pie","hist"])
    p.add_argument("-d","--data", required=True)
    p.add_argument("--title", default="")
    p.add_argument("--xlabel", default="")
    p.add_argument("--ylabel", default="")
    p.add_argument("-o","--output", default="")
    p.add_argument("--width", type=float, default=8)
    p.add_argument("--height", type=float, default=5)
    p.add_argument("--dark", action="store_true")
    p.add_argument("--dpi", type=int, default=150)
    p.add_argument("-U", "--upload", action="store_true",
                    help="Upload to PengyShare and print shareable URL")
    a = p.parse_args()
    try: data = parse_data(a.data)
    except Exception as e: print(f"ERROR: {e}", file=sys.stderr); sys.exit(1)
    out = Path(a.output) if a.output else Path.home() / "Pictures" / f"chart_{a.type}_{int(time.time())}.png"
    if not out.is_absolute(): out = Path.home() / "Pictures" / out
    fig, ax = plt.subplots(figsize=(a.width, a.height))
    if a.dark: dark_style(fig, ax)
    FUNCS[a.type](data, ax, a)
    if a.xlabel: ax.set_xlabel(a.xlabel)
    if a.ylabel: ax.set_ylabel(a.ylabel)
    if a.title: ax.set_title(a.title, fontsize=13, fontweight="bold", pad=10)
    fig.tight_layout(); fig.savefig(out, dpi=a.dpi, bbox_inches="tight"); plt.close(fig)
    print(out); print(f'<img src="file://{out}" alt="{a.title or a.type} chart">')

    if a.upload:
        _upload_script = Path.home() / "skills" / "pengyshare" / "upload.py"
        if _upload_script.exists():
            try:
                result = subprocess.run(
                    [sys.executable, str(_upload_script), "-j", str(out)],
                    capture_output=True, text=True, timeout=30,
                )
                if result.returncode == 0:
                    data = json.loads(result.stdout)
                    print(f"โœ… {data['url']}")
                else:
                    print(f"โš ๏ธ Upload failed: {result.stderr.strip()}", file=sys.stderr)
            except subprocess.TimeoutExpired:
                print("โš ๏ธ Upload timed out", file=sys.stderr)
            except Exception as e:
                print(f"โš ๏ธ Upload error: {e}", file=sys.stderr)
        else:
            print(f"โš ๏ธ PengyShare upload script not found at {_upload_script}", file=sys.stderr)

if __name__=="__main__": main()

Redaction report