plot
Make matplotlib charts โ line, bar, scatter, pie and histogram โ from inline data or a JSON/CSV file and save them as PNGs.
Downloads: 10 ยท ID: 056f0ee9cec8ecd957000000
Make matplotlib charts โ line, bar, scatter, pie and histogram โ from inline data or a JSON/CSV file and save them as PNGs.
Downloads: 10 ยท ID: 056f0ee9cec8ecd957000000
<!-- 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()