diff --git a/README.md b/README.md index 9fccd363..3c2d8538 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Development skills for AI coding agents. Plug into your favorite AI coding tool | `pptx-generator` | Generate, edit, and read PowerPoint presentations. Create from scratch with PptxGenJS (cover, TOC, content, section divider, summary slides), edit existing PPTX via XML workflows, or extract text with markitdown. | | `minimax-xlsx` | Open, create, read, analyze, edit, or validate Excel/spreadsheet files (.xlsx, .xlsm, .csv, .tsv). Covers creating new xlsx from scratch via XML templates, reading and analyzing with pandas, editing existing files with zero format loss, formula recalculation, validation, and professional financial formatting. | | `minimax-docx` | Professional DOCX document creation, editing, and formatting using OpenXML SDK (.NET). Three pipelines: create new documents from scratch, fill/edit content in existing documents, or apply template formatting with XSD validation gate-check. | +| `minimax-chart` | Generate publication-ready data visualizations from CSV, JSON, or inline data. Auto-detects chart type (bar, line, scatter, pie, heatmap, histogram). Four style presets (modern, vibrant, academic, dark). Outputs PNG, SVG, or PDF. | ## Installation diff --git a/README_zh.md b/README_zh.md index 57902fb6..f4dd7499 100644 --- a/README_zh.md +++ b/README_zh.md @@ -20,6 +20,7 @@ | `pptx-generator` | 生成、编辑和读取 PowerPoint 演示文稿。支持用 PptxGenJS 从零创建(封面、目录、内容、分节页、总结页),通过 XML 工作流编辑现有 PPTX,或用 markitdown 提取文本。 | | `minimax-xlsx` | 打开、创建、读取、分析、编辑或验证 Excel/电子表格文件(.xlsx、.xlsm、.csv、.tsv)。支持通过 XML 模板从零创建 xlsx、使用 pandas 读取分析、零格式损失编辑现有文件、公式重算与验证、专业财务格式化。 | | `minimax-docx` | 基于 OpenXML SDK(.NET)的专业 DOCX 文档创建、编辑与排版。三条流水线:从零创建新文档、填写/编辑现有文档内容、应用模板格式并通过 XSD 验证门控检查。 | +| `minimax-chart` | 从 CSV、JSON 或内联数据生成出版级数据可视化。自动检测图表类型(柱状图、折线图、散点图、饼图、热力图、直方图)。四种样式预设(现代、鲜明、学术、暗色)。输出 PNG、SVG 或 PDF。 | ## 安装 diff --git a/skills/minimax-chart/SKILL.md b/skills/minimax-chart/SKILL.md new file mode 100644 index 00000000..0c485d18 --- /dev/null +++ b/skills/minimax-chart/SKILL.md @@ -0,0 +1,159 @@ +--- +name: minimax-chart +description: | + Generate publication-ready data visualizations from CSV, JSON, or inline data. + Use when: creating charts, graphs, plots, data visualizations, infographics, + turning data into visuals, visualizing CSV files, making bar charts, line charts, + pie charts, heatmaps, scatter plots. + Triggers: chart, graph, plot, visualize data, data visualization, bar chart, + line chart, pie chart, heatmap, scatter plot, CSV to chart. +license: MIT +metadata: + version: "1.0" + category: data-tools + output_format: png/svg/pdf + sources: + - matplotlib documentation + - seaborn documentation +--- + +# MiniMax Chart + +Generate publication-ready charts and data visualizations. + +## Prerequisites + +Before starting, ensure: + +1. **Python venv** is activated with dependencies from [requirements.txt](references/requirements.txt) installed + +If matplotlib is not installed, set it up first: +```bash +pip install matplotlib pandas +``` + +## Workflow + +### Step 0: Data Input + +Accept data in one of these formats: + +1. **CSV file** - `python3 scripts/chart_create.py data.csv -o chart.png` +2. **JSON file** - array of objects or `{labels: [], values: []}` structure +3. **Inline markdown table** - parse from user message +4. **Clipboard / pasted data** - tab-separated or comma-separated text + +If the user pastes raw data, save it to a temporary CSV before processing. + +### Step 1: Data Analysis + +Examine the data to determine: + +- **Column types**: numeric, categorical, datetime, text +- **Row count**: affects chart density and readability +- **Relationships**: correlations, time series patterns, distributions + +Print a brief summary: +``` +Data: 4 columns, 12 rows + - month (datetime): Jan 2024 - Dec 2024 + - revenue (numeric): range 45K - 120K + - expenses (numeric): range 30K - 85K + - category (categorical): 3 unique values +``` + +### Step 2: Chart Type Selection + +**Auto-detect the best chart type based on data shape:** + +| Data Pattern | Recommended Chart | Why | +|---|---|---| +| 1 category + 1 numeric | Bar chart | Compare values across categories | +| Datetime + 1-3 numeric | Line chart | Show trends over time | +| 2 numeric columns | Scatter plot | Show correlation | +| 1 category (proportions) | Pie / donut chart | Show parts of a whole | +| Matrix / grid of values | Heatmap | Show intensity across 2 dimensions | +| Multiple groups + numeric | Grouped bar chart | Compare groups side by side | +| Distribution of 1 numeric | Histogram | Show value distribution | +| Category + subcategory + value | Stacked bar chart | Show composition within groups | + +Present the recommendation and ask: +> "Based on your data, I recommend a {chart type}. Want to go with that, or prefer a different chart type?" + +### Step 3: Style Selection + +Offer style presets: + +| Style | Description | Best for | +|---|---|---| +| `modern` | Clean, minimal, muted palette | Business reports, presentations | +| `vibrant` | Bold colors, high contrast | Marketing, social media | +| `academic` | Serif fonts, grayscale-friendly | Papers, publications | +| `dark` | Dark background, neon accents | Dashboards, tech content | + +Default to `modern` unless the user specifies otherwise. + +### Step 4: Generate Chart + +**Tool**: `scripts/chart_create.py` + +```bash +python3 scripts/chart_create.py data.csv \ + --type bar \ + --style modern \ + --title "Monthly Revenue 2024" \ + --xlabel "Month" --ylabel "Revenue ($)" \ + -o chart.png +``` + +**Common options:** +```bash +# Line chart with multiple series +python3 scripts/chart_create.py data.csv --type line --columns revenue,expenses -o trend.png + +# Pie chart from a single column +python3 scripts/chart_create.py data.csv --type pie --label-col category --value-col count -o breakdown.png + +# Scatter plot with trend line +python3 scripts/chart_create.py data.csv --type scatter --x price --y sales --trend -o correlation.png + +# Heatmap from a matrix CSV +python3 scripts/chart_create.py matrix.csv --type heatmap --cmap viridis -o heatmap.png + +# Export as SVG for web use +python3 scripts/chart_create.py data.csv --type bar -o chart.svg --format svg +``` + +### Step 5: Iterate + +Show the generated chart to the user. Offer adjustments: + +- Change colors: `--palette "#2563EB,#DC2626,#16A34A"` +- Adjust size: `--width 12 --height 6` +- Add annotations: `--annotate` +- Change chart type: `--type line` +- Export different format: `--format svg` or `--format pdf` + +Regenerate until the user is satisfied. + +### Step 6: Deliver + +Output format: +1. Chart file path +2. Brief description of what the chart shows + +``` +Chart created: "Monthly Revenue 2024" + File: chart.png (1200x800, 45KB) + Type: bar chart + Data: 12 months, revenue range $45K-$120K +``` + +## Rules + +- Always label axes. Charts without labels are useless. +- Use readable font sizes. Title: 16pt, axis labels: 12pt, tick labels: 10pt. +- Limit pie charts to 6 slices max. Group small values as "Other". +- For time series, always sort by date on the x-axis. +- Default output size: 10x6 inches at 150 DPI (1500x900px). Adjust for specific needs. +- Prefer PNG for general use, SVG for web, PDF for print. diff --git a/skills/minimax-chart/references/chart-types.md b/skills/minimax-chart/references/chart-types.md new file mode 100644 index 00000000..c9e3c441 --- /dev/null +++ b/skills/minimax-chart/references/chart-types.md @@ -0,0 +1,58 @@ +# Chart Type Selection Guide + +How to pick the right chart for your data. + +## Decision Tree + +``` +What are you showing? +├── Comparison across categories +│ ├── Few categories (< 7) → Bar chart +│ ├── Many categories (7+) → Horizontal bar chart +│ └── Parts of a whole → Pie / donut chart (max 6 slices) +├── Change over time +│ ├── Single metric → Line chart +│ ├── Multiple metrics → Multi-line chart +│ └── Cumulative → Area chart +├── Relationship between variables +│ ├── 2 numeric variables → Scatter plot +│ └── With trend → Scatter + trend line +├── Distribution +│ ├── Single variable → Histogram +│ └── Across categories → Box plot +└── Intensity / density + └── 2D grid of values → Heatmap +``` + +## Chart Type Reference + +| Chart | Best for | Avoid when | +|-------|----------|------------| +| **Bar** | Comparing categories | More than 15 categories | +| **Line** | Trends over time | Unordered categories | +| **Scatter** | Correlation between 2 variables | Categorical data | +| **Pie** | Parts of a whole | More than 6 slices | +| **Histogram** | Value distribution | Categorical data | +| **Heatmap** | Intensity across 2 dimensions | Few data points | +| **Grouped bar** | Comparing groups within categories | Too many groups (>4) | +| **Stacked bar** | Composition within categories | When individual values matter more than totals | + +## Style Guidelines + +### Colors +- Use consistent colors for the same data series across multiple charts +- Avoid red/green together (color blindness) +- Use sequential colormaps (e.g., `YlOrRd`) for heatmaps +- Use distinct colors for categorical data + +### Labels +- Always label both axes +- Include units in axis labels: "Revenue ($K)" not just "Revenue" +- Use title case for titles, sentence case for labels +- Rotate long x-axis labels 45 degrees + +### Sizing +- Default: 10x6 inches at 150 DPI (1500x900px) +- Presentations: 12x7 inches at 200 DPI +- Print: 8x5 inches at 300 DPI +- Thumbnails: 6x4 inches at 100 DPI diff --git a/skills/minimax-chart/references/requirements.txt b/skills/minimax-chart/references/requirements.txt new file mode 100644 index 00000000..b66be8f5 --- /dev/null +++ b/skills/minimax-chart/references/requirements.txt @@ -0,0 +1,2 @@ +matplotlib>=3.7.0 +pandas>=2.0.0 diff --git a/skills/minimax-chart/scripts/chart_create.py b/skills/minimax-chart/scripts/chart_create.py new file mode 100644 index 00000000..ec494780 --- /dev/null +++ b/skills/minimax-chart/scripts/chart_create.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +""" +Chart generator — create publication-ready charts from CSV/JSON data. + +Usage: + python chart_create.py data.csv -o chart.png + python chart_create.py data.csv --type line --columns revenue,expenses -o trend.png + python chart_create.py data.csv --type pie --label-col category --value-col count -o pie.png + python chart_create.py data.csv --type scatter --x price --y sales --trend -o scatter.png + +Requires: matplotlib, pandas +""" + +import argparse +import json +import os +import sys + +try: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + import pandas as pd +except ImportError: + raise SystemExit("ERROR: matplotlib and pandas are required.\n pip install matplotlib pandas") + +# Style presets +STYLES = { + "modern": { + "colors": ["#2563EB", "#DC2626", "#16A34A", "#F59E0B", "#8B5CF6", "#06B6D4"], + "bg": "#FFFFFF", + "grid_color": "#E5E7EB", + "text_color": "#1F2937", + "font_family": "sans-serif", + }, + "vibrant": { + "colors": ["#FF6B6B", "#4ECDC4", "#45B7D1", "#FFA07A", "#98D8C8", "#F7DC6F"], + "bg": "#FFFFFF", + "grid_color": "#E0E0E0", + "text_color": "#2D3436", + "font_family": "sans-serif", + }, + "academic": { + "colors": ["#2C3E50", "#7F8C8D", "#BDC3C7", "#95A5A6", "#34495E", "#ABB2B9"], + "bg": "#FFFFFF", + "grid_color": "#D5D8DC", + "text_color": "#2C3E50", + "font_family": "serif", + }, + "dark": { + "colors": ["#00D2FF", "#FF6B6B", "#A8E6CF", "#FFD93D", "#C792EA", "#FF8A65"], + "bg": "#1A1A2E", + "grid_color": "#2D2D44", + "text_color": "#E0E0E0", + "font_family": "sans-serif", + }, +} + + +def _load_data(path: str) -> pd.DataFrame: + """Load data from CSV or JSON file.""" + ext = os.path.splitext(path)[1].lower() + if ext == ".csv": + return pd.read_csv(path) + elif ext == ".tsv": + return pd.read_csv(path, sep="\t") + elif ext == ".json": + with open(path) as f: + data = json.load(f) + if isinstance(data, list): + return pd.DataFrame(data) + elif "labels" in data and "values" in data: + return pd.DataFrame({"label": data["labels"], "value": data["values"]}) + else: + return pd.DataFrame(data) + else: + return pd.read_csv(path) + + +def _apply_style(style_name: str): + """Apply a style preset to matplotlib.""" + style = STYLES.get(style_name, STYLES["modern"]) + plt.rcParams.update({ + "figure.facecolor": style["bg"], + "axes.facecolor": style["bg"], + "axes.edgecolor": style["grid_color"], + "axes.grid": True, + "grid.color": style["grid_color"], + "grid.alpha": 0.5, + "text.color": style["text_color"], + "axes.labelcolor": style["text_color"], + "xtick.color": style["text_color"], + "ytick.color": style["text_color"], + "font.family": style["font_family"], + "font.size": 10, + "axes.titlesize": 16, + "axes.labelsize": 12, + }) + return style["colors"] + + +def _auto_detect_type(df: pd.DataFrame) -> str: + """Auto-detect the best chart type based on data shape.""" + numeric_cols = df.select_dtypes(include=["number"]).columns.tolist() + cat_cols = df.select_dtypes(include=["object", "category"]).columns.tolist() + + # Try to detect datetime columns + for col in cat_cols[:]: + try: + pd.to_datetime(df[col]) + cat_cols.remove(col) + except (ValueError, TypeError): + pass + + if len(numeric_cols) == 2 and len(cat_cols) == 0: + return "scatter" + elif len(numeric_cols) == 1 and len(cat_cols) == 1: + if len(df) <= 6: + return "pie" + return "bar" + elif len(numeric_cols) >= 2 and len(cat_cols) >= 1: + return "line" + elif len(numeric_cols) == 1 and len(cat_cols) == 0: + return "histogram" + else: + return "bar" + + +def _chart_bar(df, args, colors): + fig, ax = plt.subplots(figsize=(args.width, args.height)) + cols = args.columns.split(",") if args.columns else df.select_dtypes(include=["number"]).columns.tolist()[:3] + x_col = args.label_col or df.columns[0] + x = range(len(df)) + width = 0.8 / len(cols) + for i, col in enumerate(cols): + offset = (i - len(cols) / 2 + 0.5) * width + ax.bar([xi + offset for xi in x], df[col], width=width, label=col, color=colors[i % len(colors)]) + ax.set_xticks(x) + ax.set_xticklabels(df[x_col], rotation=45, ha="right") + if len(cols) > 1: + ax.legend() + return fig, ax + + +def _chart_line(df, args, colors): + fig, ax = plt.subplots(figsize=(args.width, args.height)) + cols = args.columns.split(",") if args.columns else df.select_dtypes(include=["number"]).columns.tolist()[:5] + x_col = args.label_col or df.columns[0] + for i, col in enumerate(cols): + ax.plot(df[x_col], df[col], marker="o", markersize=4, label=col, color=colors[i % len(colors)], linewidth=2) + ax.legend() + plt.xticks(rotation=45, ha="right") + return fig, ax + + +def _chart_scatter(df, args, colors): + fig, ax = plt.subplots(figsize=(args.width, args.height)) + x_col = args.x or df.select_dtypes(include=["number"]).columns[0] + y_col = args.y or df.select_dtypes(include=["number"]).columns[1] + ax.scatter(df[x_col], df[y_col], color=colors[0], alpha=0.7, s=50) + if args.trend: + import numpy as np + z = np.polyfit(df[x_col].astype(float), df[y_col].astype(float), 1) + p = np.poly1d(z) + ax.plot(sorted(df[x_col]), p(sorted(df[x_col])), "--", color=colors[1], linewidth=1.5, alpha=0.7) + return fig, ax + + +def _chart_pie(df, args, colors): + fig, ax = plt.subplots(figsize=(args.width, args.height)) + label_col = args.label_col or df.columns[0] + value_col = args.value_col or df.select_dtypes(include=["number"]).columns[0] + wedges, texts, autotexts = ax.pie( + df[value_col], labels=df[label_col], colors=colors[:len(df)], + autopct="%1.1f%%", startangle=90, + ) + for t in autotexts: + t.set_fontsize(9) + ax.axis("equal") + return fig, ax + + +def _chart_histogram(df, args, colors): + fig, ax = plt.subplots(figsize=(args.width, args.height)) + col = args.columns.split(",")[0] if args.columns else df.select_dtypes(include=["number"]).columns[0] + ax.hist(df[col], bins=args.bins, color=colors[0], edgecolor="white", alpha=0.8) + return fig, ax + + +def _chart_heatmap(df, args, colors): + fig, ax = plt.subplots(figsize=(args.width, args.height)) + numeric = df.select_dtypes(include=["number"]) + cmap = args.cmap or "YlOrRd" + im = ax.imshow(numeric.values, cmap=cmap, aspect="auto") + ax.set_xticks(range(len(numeric.columns))) + ax.set_xticklabels(numeric.columns, rotation=45, ha="right") + if df.columns[0] not in numeric.columns: + ax.set_yticks(range(len(df))) + ax.set_yticklabels(df.iloc[:, 0]) + fig.colorbar(im, ax=ax) + return fig, ax + + +CHART_TYPES = { + "bar": _chart_bar, + "line": _chart_line, + "scatter": _chart_scatter, + "pie": _chart_pie, + "histogram": _chart_histogram, + "heatmap": _chart_heatmap, +} + + +def main(): + p = argparse.ArgumentParser(description="Chart generator from CSV/JSON data") + p.add_argument("data", help="Input data file (CSV, TSV, or JSON)") + p.add_argument("-o", "--output", required=True, help="Output file path (png/svg/pdf)") + p.add_argument("--type", default="auto", choices=list(CHART_TYPES.keys()) + ["auto"], help="Chart type (default: auto-detect)") + p.add_argument("--style", default="modern", choices=list(STYLES.keys()), help="Visual style preset (default: modern)") + p.add_argument("--title", default="", help="Chart title") + p.add_argument("--xlabel", default="", help="X-axis label") + p.add_argument("--ylabel", default="", help="Y-axis label") + p.add_argument("--columns", default="", help="Comma-separated column names to plot") + p.add_argument("--label-col", default="", help="Column to use for labels/categories") + p.add_argument("--value-col", default="", help="Column to use for values (pie charts)") + p.add_argument("--x", default="", help="X-axis column (scatter)") + p.add_argument("--y", default="", help="Y-axis column (scatter)") + p.add_argument("--trend", action="store_true", help="Add trend line (scatter)") + p.add_argument("--palette", default="", help="Custom colors (comma-separated hex codes)") + p.add_argument("--width", type=float, default=10, help="Figure width in inches (default: 10)") + p.add_argument("--height", type=float, default=6, help="Figure height in inches (default: 6)") + p.add_argument("--dpi", type=int, default=150, help="Output DPI (default: 150)") + p.add_argument("--format", default="", help="Output format override (png/svg/pdf)") + p.add_argument("--bins", type=int, default=20, help="Number of bins for histogram (default: 20)") + p.add_argument("--cmap", default="", help="Colormap for heatmap (default: YlOrRd)") + p.add_argument("--annotate", action="store_true", help="Add value annotations on bars") + args = p.parse_args() + + if not os.path.exists(args.data): + raise SystemExit(f"ERROR: Data file not found: {args.data}") + + df = _load_data(args.data) + print(f"Data loaded: {len(df.columns)} columns, {len(df)} rows") + for col in df.columns: + dtype = "numeric" if pd.api.types.is_numeric_dtype(df[col]) else "text" + print(f" {col} ({dtype}): {df[col].nunique()} unique values") + + chart_type = args.type + if chart_type == "auto": + chart_type = _auto_detect_type(df) + print(f"Auto-detected chart type: {chart_type}") + + colors = _apply_style(args.style) + if args.palette: + colors = args.palette.split(",") + + chart_fn = CHART_TYPES[chart_type] + fig, ax = chart_fn(df, args, colors) + + if args.title: + ax.set_title(args.title, pad=15, fontweight="bold") + if args.xlabel: + ax.set_xlabel(args.xlabel) + if args.ylabel: + ax.set_ylabel(args.ylabel) + + if args.annotate and chart_type == "bar": + for container in ax.containers: + ax.bar_label(container, fmt="%.0f", fontsize=8, padding=2) + + plt.tight_layout() + + fmt = args.format or os.path.splitext(args.output)[1].lstrip(".") + os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True) + fig.savefig(args.output, dpi=args.dpi, format=fmt or "png", bbox_inches="tight") + plt.close(fig) + + size = os.path.getsize(args.output) + print(f"\nOK: {size:,} bytes -> {args.output}") + print(f" Type: {chart_type}") + print(f" Style: {args.style}") + print(f" Size: {int(args.width * args.dpi)}x{int(args.height * args.dpi)}px @ {args.dpi} DPI") + + +if __name__ == "__main__": + main()