Skill: panchenbo/chart-data-viz (version 1.0.0)

--- name: chart description: Local-first chart generation engine for trends, comparisons, distributions, and quick visual explanations. Use whenever the user wants to visualize data, compare numbers, plot a trend, turn CSV or JSON into a chart, or decide which chart type fits a dataset best. Generates charts locally and stores outputs in the user's workspace. --- # Chart Turn numbers into clear visuals. ## Core Philosophy 1. Prefer clarity over chart variety. 2. Choose the simplest chart that makes the comparison obvious. 3. Use local generation only. 4. Make outputs reusable for reports, slides, and quick decision-making. ## Runtime Requirements - Python 3 must be available as `python3` - `matplotlib` must be installed - No network access required ## Storage All data is stored locally only under: - `~/.openclaw/workspace/memory/chart/charts.json` - `~/.openclaw/workspace/memory/chart/output/` No cloud sync. No third-party chart APIs. ## Supported Chart Types - `bar`: category comparison - `line`: trend over time - `pie`: simple part-to-whole - `scatter`: relationship between two variables ## Key Workflows - **Suggest**: `suggest_chart.py --labels ... --values ...` - **Generate**: `make_chart.py --type bar --title "..." --labels "A,B,C" --values "10,20,15"` - **History**: `list_charts.py` - **Initialize**: `init_storage.py` ## Scripts | Script | Purpose | |---|---| | `init_storage.py` | Initialize local chart storage | | `make_chart.py` | Generate a chart image from inline data | | `suggest_chart.py` | Recommend the best chart type | | `list_charts.py` | Show previously generated charts |

path: references/philosophy.md
content: # Chart Philosophy

1. Prefer the simplest chart that makes the point obvious.
2. Avoid decorative complexity.
3. Optimize for readability, not novelty.
4. Keep chart generation local and reusable.


path: scripts/init_storage.py
content: #!/usr/bin/env python3
import json
import os
from datetime import datetime

CHART_DIR = os.path.expanduser("~/.openclaw/workspace/memory/chart")
OUTPUT_DIR = os.path.join(CHART_DIR, "output")
CHARTS_FILE = os.path.join(CHART_DIR, "charts.json")

def write_json_if_missing(path, payload):
    if not os.path.exists(path):
        with open(path, "w", encoding="utf-8") as f:
            json.dump(payload, f, indent=2, ensure_ascii=False)

def main():
    os.makedirs(CHART_DIR, exist_ok=True)
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    write_json_if_missing(CHARTS_FILE, {
        "metadata": {
            "version": "1.0.0",
            "created_at": datetime.now().isoformat(),
            "last_updated": datetime.now().isoformat()
        },
        "charts": {}
    })

    print("✓ Chart storage initialized")
    print(f"  {CHARTS_FILE}")
    print(f"  {OUTPUT_DIR}")

if __name__ == "__main__":
    main()


path: scripts/lib/storage.py
content: #!/usr/bin/env python3
import json
import os
from datetime import datetime

CHART_DIR = os.path.expanduser("~/.openclaw/workspace/memory/chart")
OUTPUT_DIR = os.path.join(CHART_DIR, "output")
CHARTS_FILE = os.path.join(CHART_DIR, "charts.json")

def ensure_dir():
    os.makedirs(CHART_DIR, exist_ok=True)
    os.makedirs(OUTPUT_DIR, exist_ok=True)

def _safe_load(path, default):
    ensure_dir()
    if not os.path.exists(path):
        return default
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        return default

def _atomic_save(path, data):
    ensure_dir()
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)
    os.replace(tmp, path)

def load_charts():
    return _safe_load(CHARTS_FILE, {
        "metadata": {
            "version": "1.0.0",
            "created_at": datetime.now().isoformat(),
            "last_updated": datetime.now().isoformat()
        },
        "charts": {}
    })

def save_charts(data):
    data.setdefault("metadata", {})
    data["metadata"]["last_updated"] = datetime.now().isoformat()
    _atomic_save(CHARTS_FILE, data)


path: scripts/list_charts.py
content: #!/usr/bin/env python3
import os
import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)

from lib.storage import load_charts

def main():
    data = load_charts()
    charts = data.get("charts", {})

    if not charts:
        print("No charts found.")
        return

    for chart_id, chart in charts.items():
        print(f"{chart_id} | {chart['type']} | {chart['title']}")
        print(f"  {chart['output_path']}")

if __name__ == "__main__":
    main()


path: scripts/make_chart.py
content: #!/usr/bin/env python3
import argparse
import os
import sys
import uuid
from datetime import datetime

import matplotlib.pyplot as plt

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)

from lib.storage import load_charts, save_charts, OUTPUT_DIR

VALID_TYPES = ["bar", "line", "pie", "scatter"]

def parse_csv(value):
    return [v.strip() for v in value.split(",") if v.strip()]

def parse_float_csv(value):
    return [float(v.strip()) for v in value.split(",") if v.strip()]

def main():
    parser = argparse.ArgumentParser(description="Generate a chart")
    parser.add_argument("--type", choices=VALID_TYPES, required=True)
    parser.add_argument("--title", required=True)
    parser.add_argument("--labels", required=True)
    parser.add_argument("--values", required=True)
    parser.add_argument("--x_values", help="For scatter only")
    args = parser.parse_args()

    labels = parse_csv(args.labels)
    values = parse_float_csv(args.values)

    if args.type != "scatter" and len(labels) != len(values):
        raise SystemExit("labels and values length mismatch")

    chart_id = f"CHT-{str(uuid.uuid4())[:4].upper()}"
    filename = f"{chart_id}.png"
    output_path = os.path.join(OUTPUT_DIR, filename)

    plt.figure(figsize=(8, 5))

    if args.type == "bar":
        plt.bar(labels, values)
        plt.ylabel("Value")

    elif args.type == "line":
        plt.plot(labels, values, marker="o")
        plt.ylabel("Value")

    elif args.type == "pie":
        plt.pie(values, labels=labels, autopct="%1.1f%%")

    elif args.type == "scatter":
        if not args.x_values:
            raise SystemExit("--x_values is required for scatter")
        x_values = parse_float_csv(args.x_values)
        if len(x_values) != len(values):
            raise SystemExit("x_values and values length mismatch")
        plt.scatter(x_values, values)
        plt.xlabel("X")
        plt.ylabel("Y")

    plt.title(args.title)
    if args.type != "pie":
        plt.xticks(rotation=30, ha="right")
    plt.tight_layout()
    plt.savefig(output_path)
    plt.close()

    data = load_charts()
    data["charts"][chart_id] = {
        "id": chart_id,
        "title": args.title,
        "type": args.type,
        "labels": labels,
        "values": values,
        "output_path": output_path,
        "created_at": datetime.now().isoformat()
    }
    save_charts(data)

    print(f"✓ Chart created: {chart_id}")
    print(f"  Title: {args.title}")
    print(f"  Type: {args.type}")
    print(f"  Output: {output_path}")

if __name__ == "__main__":
    main()


path: scripts/suggest_chart.py
content: #!/usr/bin/env python3
import argparse

def parse_csv(value):
    return [v.strip() for v in value.split(",") if v.strip()]

def main():
    parser = argparse.ArgumentParser(description="Suggest best chart type")
    parser.add_argument("--labels", required=True, help="Comma-separated labels")
    parser.add_argument("--values", required=True, help="Comma-separated values")
    parser.add_argument("--x_numeric", action="store_true", help="Treat x-axis as numeric or ordered")
    args = parser.parse_args()

    labels = parse_csv(args.labels)
    values = parse_csv(args.values)

    if len(labels) != len(values):
        raise SystemExit("labels and values length mismatch")

    suggestion = "bar"
    reason = "Best for comparing categories clearly."

    if args.x_numeric:
        suggestion = "line"
        reason = "Best for trends or ordered progression."
    elif len(labels) <= 5:
        suggestion = "bar"
        reason = "Best for quick category comparison."
    elif len(labels) > 8:
        suggestion = "line"
        reason = "Many points usually read better as a trend line."

    print(f"Suggested chart: {suggestion}")
    print(f"Reason: {reason}")

if __name__ == "__main__":
    main()


path: skill.json
content: {
  "name": "chart",
  "version": "1.0.0",
  "description": "Local-first chart generation engine for trends, comparisons, distributions, and quick visual explanations.",
  "entrypoints": [],
  "files": [
    "SKILL.md",
    "skill.json",
    "scripts/init_storage.py",
    "scripts/make_chart.py",
    "scripts/suggest_chart.py",
    "scripts/list_charts.py",
    "scripts/lib/storage.py",
    "references/philosophy.md"
  ]
}


path: _meta.json
content: ownerId: kn7avdzmvgh09erqc7z453jm5982mfga
slug: chart
version: 1.0.0
publishedAt: 1773303445974

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)