Convert data files (CSV/Excel/JSON) into interactive ECharts HTML. Supports 32 chart types, 3 theme presets (default/classic/dark), multi-file merging, and LLM data transformation code (sandboxed execution). For full CLI parameters, flag semantics, multi-chart/multi-file details, capability boundaries, error code table, complete delivery interpretation specification, and FAQ, see REFERENCE.md。
Activation Triggers
TRIGGER
- The user provides / points to a tabular data file and requests a chart, visualization, or chart generation
- The user mentions "draw a chart", "make a chart", "generate a chart", "data visualization", "chart", or "visualization"
DO-NOT-TRIGGER(If any one matches, recommend another skill instead; do not force this one)
- What is wanted is a report/weekly report/retrospective/briefing document with a conclusion narrative → use a report-type skill (this skill produces charts + traceable facts + candidate insight signals; it does not write narrative documents)
- There is no data file (pure text description, verbal idea)
- A backend dashboard is needed, or non-tabular content such as images/audio/video
- Map rendering: built-in China provincial-level (
china, including Hong Kong, Macao, and Taiwan) and world country-level (world, exact match on English country names); finer regions such as city-level/district-level require the user to provide their own GeoJSON file (--geo-path); this skill does not download it on the user's behalf
Workflow (profile first, then select the type)
chart_type is a required CLI parameter, while the basis for type selection (cardinality, distribution, granularity, entity vs. detail) originally only exists in the plot_stats after the type is selected—deciding first and gathering evidence later would turn type selection into guesswork. So there are two paths depending on whether the user has clearly stated their intent:
A. The user's intent is clear(e.g. "compare sales by region", "look at monthly trends", "show the composition of proportions") → Directly pick the chart type + axes + aggregation basis according to the type selection table, and produce the chart.Do notrun profile just to follow the process. If the data cannot support that intent (a trend is wanted but there is no time column, or proportions are wanted but all rows are detail rows), state this honestly and give feasible alternatives (for example, replace "trend" with comparison by category); do not force a chart.
B. The user only provided data and did not say what they want to see(the most frequent case) → two commands:
python {skill_base}/scripts/cli.py <file> --profile(one command; read-only, no rendering, no disk writes), obtaining: each column's dtype/cardinality/missing rate/sample/statistics, entity granularity (grain), candidate insight signals (signals), suspected non-data rows (row_quality)。- You decidewhich 1–3 charts to produce: read
columns's dtype/sample/cardinality to determine which column is an ID, which is a dimension, and which is a measure, combine this withsignalsto pick out findings worth discussing, and based on that define the chart type + axes + aggregation basis, then produce the chart directly (--transform-codeaggregation/pivot +--titleconclusion +--annotationinterpretation). When delivering, explain "why these charts".
signalsThe ones most worth prioritizing for charting indivergent_category(a certain category changing against the trend) andtrend: they are the easiest to be masked by the overall average, and are exactly the findings that "the user did not think of themselves but understands at a glance".
MUST NOT pushes the responsibility for type selection back onto the user ("Do you want to see trends, comparisons, or proportions?")—which column is an ID, which is a dimension, and what chart to produce is work the agent should judge on its own from the objective facts of the profile, not a menu for the user to choose from.
Contract (8 items, MUST)
- Column names are normalized after parsing: converted to lowercase, special characters →
_(e.g.总学时→总_学时), Chinese characters are preserved;--x-axis/--y-axis/transform must reference normalized column names - transform sandbox: available variables are only
df/pd/np(np.select/np.whereavailable), supports multiple statements (;or newline separated), must produce a DataFrame namedresult; import/open/try/class definitions are forbidden (blacklist + AST whitelist enforced validation, violations return an error withsuggestion) - pie/bar/treemap/funnel etc. read data as "1 categorical column + 1 numeric column" (the
name/valuein the docs refers to therole, not a requirement that the column be named this—keeping the original column names is more readable, and you don't need to write an extra rename); for categorical frequency charts, first aggregate into these two columns with transform, then use--x-axis <分类列> --y-axis <数值列>to specify - stdout on success:
success/html_path/chart_type/title/source_rows/plotted_rows/unique_entities/data_rows/data_preview(first 10 rows of plotting data, for calibration checks)/annotation_source/plot_stats(full statistical summary of plotting data, for writing interpretation; covers all 32 categories);assumptions(automatic choices made by the CLI) andadvisories(caliber reminders) appear only when non-empty—absence of this field means "no automatic choices/no reminders this time", not missing - For calibration, read
data_preview+data_rowsdirectly from stdout, do not open the HTML to search for data——data_previewis the first 10 rows of the x/y columns in the rendered frame(same source and same frame as the render, but excluding auxiliary columns not involved in plotting);data_rowsis the actual number of plotted rows. When you need to check auxiliary columns produced by transform, useprint(df.columns)/print(df.head())inside transform (goes to stderr, does not pollute stdout) or separately--dry-run - For aggregation caliber reconciliation, read
source_rows/plotted_rows/unique_entities:source_rows=number of raw rows before transform,plotted_rows=actual number of plotted rows,unique_entities=number of deduplicated entities (if there is--label-coluse that column, otherwise use the x column).Note:plotted_rows == source_rowsonly indicates "no aggregation", while row-based charts like scatter/box plots inherently should not aggregate—the criterion must be used together with chart-type semantics, don't treat it as a generic error signal. The true mechanical criterion is the "suspected non-aggregation" inadvisories: the number of distinct x-axis values < the number of rows and the chart type requires "one x corresponds to one y"; in this case the max/min/mean fromplot_statsare row-level rather than for that dimension as a whole - When intent is unclear, you MUST first run
--profile(python {skill_base}/scripts/cli.py <file> --profile(chart type not needed). It returns each column's dtype/cardinality/missing rate/sample/statistics,grain、signals、row_quality—these areobjective facts, and are the basis for selection; which column is an ID, which is a dimension, and what chart to produce are for you to judge from these facts, profile does not ghostwrite (it does not produce role classification or chart type suggestions) - Before delivery you MUST read
assumptions+advisories:assumptionsare the automatic choices the CLI made for you (automatic x/y value selection, etc.),advisoriesare reminders that "generation succeeded but you may have wanted another caliber" (suspected non-aggregation, too many pie chart categories, partial downgrade of numeric columns…). Both must be echoed in the delivery message—assumptions the user disagrees with can be regenerated with one sentence
Golden examples (templates, copy and change column names)
1 Categorical frequency → pie/bar(most frequent scenario):
python {skill_base}/scripts/cli.py data.xlsx bar --title "标题" --output-dir ./out \
--x-axis name --y-axis value \
--transform-code "result = df['类别列'].fillna('未标注').value_counts().rename_axis('name').reset_index(name='value')"
(gauge/liquid must add --target <目标值>when you want a "completion rate"; without it plot_stats.achievement is null. For environment self-check use python {skill_base}/scripts/cli.py --doctor。)
2 Multiple charts batch + escaping pitfalls(≥2 images MUST be batched; when transform contains Chinese/quotes, do not pass it directly in the shell --charts, write it into a JSON file and use --charts-file):
python {skill_base}/scripts/cli.py data.xlsx --sheet "Sheet1" \
--charts-file charts.json --output-dir ./out
charts.json Each item:type(required) + title/x_axis/y_axis(string or array) /transform_code(single-image level) /label_col/color_by/annotation. The configuration is written by the agent itself—based on the objective facts of --profile (dtype/cardinality/samples/signals), decide what chart to produce and what axes to configure.
3-group aggregation → bar:
python {skill_base}/scripts/cli.py data.xlsx bar --title "标题" --output-dir ./out \
--x-axis 分组列 --y-axis 数值列 \
--transform-code "result = df.groupby('分组列')['数值列'].sum().reset_index()"
(reset_index() the column names afterward are still "group column/value column", and the axes can reference them directly; changing to rename_axis('name')+reset_index(name='value') is also fine, but the axes must be changed accordingly to name/value—the two must be consistent.)
Scope pitfall: Before aggregating, think clearly about "by data row vs by deduplicated entity"—when counting entity attributes (such as the hours structure of each course), first drop_duplicates; after generation, check against data_preview (if the sum of each row's value equals the original row count rather than the entity count, you forgot to deduplicate).
Hard Constraints (MUST follow)
- MUST use the CLI workflow(
data_parser.py→cli.py), do not write your own script as a substitute. - Dirty headers MUST use CLI flags(
--skiprows N/--header-row N/--drop-rows 0,1/--sheet, see REFERENCE.md for semantics), N is determined by the actual data (first run once without flags to see the raw layout), and must not be fixed arbitrarily.Note:--skiprows Nand--header-row Nare the same behavior (both are "use row N as the header, discard the row above it"), choose either one; do not think of N as two different decisions; it is recommended to standardize on--header-row。- two-row header + score row/subheader row(
--header-row Ncan only choose "which row becomes the column names", and cannot both preserve the main header and merge the subheaders) use thethree-step approach: ① run--profile, readrow_quality.suspicious_non_data_rows(oradvisories) to locate the remaining non-data rows; ② use--drop-rows 0,1(position is given by profile, do not guess) or--transform-code "result = df.drop(index=[0,1])"to drop rows; ③ rerun--profileto confirm it is clean(rowsbecomes fewer,row_qualityzero hits) before generating the chart.
- two-row header + score row/subheader row(
- Column renaming/reshaping/aggregation MUST use
--transform-code. The parsing layer only solves "which row is the header"; all other cleaning belongs to transform. - To change the display names of axes/series, MUST use
--x-name/--y-name/--series-name(s), do not rely on changing column names. Changing column names will change both axis names and series names (both are bound to column names), and for charts that originally hardcode default text (heatmap "Heatmap"/gauge "Gauge"/sankey "Sankey chart", etc.), changing column names also cannot change the series names. Display names are the only clean entry point:--x-namechanges the x-axis,--y-namechanges the y-axis,--series-name(single) or--series-names(multiple, in visible series order) changes series names. - MUST report unsupported scenarios: For things the CLI truly does not support (such as nested JSON more than 1 level deep), first explain to the user and give suggestions; do not silently bypass.
- MUST NOT Hardcode absolute paths in the generated code; resolve paths at runtime.
- Do not proactively pass
--lang; the CLI automatically follows the data language. Pass it only when the user explicitly requests a certain language. - MUST deliver with interpretation: When delivering charts, you must attach a textual interpretation written by the LLM, and inject it into the HTML via
--annotation(see "Interpretation Delivery Specification" below); do not deliver only a bare chart. In the successful output, theannotation_sourceThe interpretation source will be indicated:'user'= what you passed in,'default'= filled in by the code using a template (in this case the output will also carry an advisory)——Before delivery, confirm it isuser'。 - MUST explicitly pass
--output-dir, pointing to a user-visible directory (such as./out, the Desktop directory, or an explicit subdirectory under the current working directory). The default value./smart_charts_outputwill be written to disk under the current working directory, making the artifact hard for the user to find. {skill_base}= skill root directory(containingSKILL.md). If the current host does not replace this placeholder, locate it once first and then change it to an absolute path for use:SKILL_DIR=$(dirname "$(find "$HOME" -maxdepth 6 -type f -path "*smart-charts/scripts/cli.py" | head -1)")/.., then use$SKILL_DIR/scripts/cli.py. Do not assemble the path from memory.
Default policy (no confirmation from the user)
Generating a chart is a cheap and reversible action (regeneration takes 1-10s, with zero external side effects). The chart type, multi-file merge strategy, and value semantics are all decided internally by the agent, without interrupting the user——basisis: the user's explicit intent (branch A) or --profile the objective facts provided (dtype/cardinality/sample/signals, branch B).
Post-hoc review instead of pre-hoc confirmation: In the delivery message, explicitly list the key assumptions made this time (e.g., "chose line, because month is a time series column", "multiple files were concatenated vertically because their column names are exactly the same, and a source_file column has been injected", "sales are measured by amount"). If the user disagrees with any assumption, they can ask in one sentence to change the semantics/change the type/change the merge method and regenerate.
The only point where user intervention is required: see the "still failing" branch of Exit Criteria.
Exit Criteria (mechanically determinable)
- ✅ Success: stdout is
{"chart": {"success": true, ...}}(in multi-chart mode,{"charts": [...], "summary": ...}), andhtml_pathpoints to a file that exists and is non-empty → use the same stdout'sdata_preview/data_rows/plotted_rows/unique_entitiesto check the aggregation semantics (by row vs by deduplicated entity), and after confirming it is correct, deliver it with a textual interpretation. - ℹ️
--dry-rundoes not count as delivery:html_pathis null and is not written to disk; it is only used for trial-and-error column names or trial aggregation scripts.Writing the interpretation does not require running it first——the formal output already carriesplot_stats。 - ❌ Failure:
success: falseor exit code 1 → readerror.details.suggestion, fix it and retry;at most 2 retries for the same step。 - 🛑 Still failing (the only point where user intervention is required): report
code_name、suggestionand the fixes already attempted truthfully to the user, give suggestions, and wait for the user's decision.Must notsilently fall back to a self-written script (violates constraints 1/4).
Chart Types selection table
Before selecting, check the Required Format; if it does not match, use transform code to adapt it.
Scale hint: for multi-column charts such as heatmap / boxplot / radar, if the scales of the columns differ greatly (e.g., a mix of a maximum score of 10 and a maximum score of 100), you need to use transform code to normalize them first; otherwise the small-scale columns will be dominated by the large-scale columns.
| ID | Best For | Trigger Keywords | y_axis | Required DataFrame Format |
|---|---|---|---|---|
line |
Time-series trends | trend, change, over time, 趋势, 变化, 走势 | 1~N | 1 category/time + 1~N numeric |
bar |
Category comparison | compare, rank, difference, 对比, 比较, 排名, 差异 | 1~N | 1 category + 1~N numeric |
area |
Cumulative change | cumulative, change, 累计, 变化 | 1~N | 1 category/time + 1~N numeric |
pie |
Composition/share | share, composition, proportion, 占比, 构成, 比例 | 1 | 1 name + 1 value |
scatter |
Correlation | correlation, relationship, scatter, 相关, 关系, 散点 | 1 | 2 numeric, or 1 category + 1 numeric |
radar |
Multi-dimension comparison | multi-dimension, comprehensive, radar, 多维, 综合, 雷达 | N | 1 indicator + N numeric |
heatmap |
Density/cross-tab | density, cross, matrix, heatmap, density, cross, matrix, heatmap | 1~N | 1 category + 1~N numeric (each column is a "column" matrix; for cross tables, first use pivot_table to spread into columns) |
treemap |
Hierarchical proportion | hierarchy, proportion, nested, hierarchy, proportion, nested | 1 | 1 name + 1 value |
graph |
Entity relationships | relationship, network, topology, relationship, network, topology | special | source + target (+ value) |
boxplot |
Distribution/outliers | distribution, outlier, quartile, distribution, outlier, quartile | N | N numeric |
waterfall |
Incremental change | increment, change, waterfall, increment, change, waterfall | 1 | 1 category + 1 numeric (increments) |
gauge |
KPI progress | progress, kpi, achievement, progress, KPI, achievement | 1 | 1 numeric (mean used) |
sankey |
Flow transfer | flow, transfer, sankey, flow, flow, transfer | special | source + target + value |
funnel |
Conversion rate | conversion, funnel, churn, conversion, funnel, churn | 1 | 1 name + 1 value |
sunburst |
Single-level proportion | proportion, sunburst, proportion, ratio | 1 | 1 name + 1 value |
wordcloud |
Frequency/keywords | word frequency, keywords, text, word frequency, keywords, word cloud | 1 | 1 name + 1 value |
histogram |
Distribution shape | distribution, histogram, distribution, histogram | 1 | 1 numeric column(--x-axis or --y-axis both work; numeric --x-axis preferred) |
stacked_bar |
Composition over categories | composition, stacked, stacked, composition | 1~N | 1 category + 1~N numeric |
bubble |
3-variable correlation | bubble, 3-variable, bubble, three-variable | 2 | 2 numeric + 1 size |
pareto |
80/20 analysis | pareto, 80/20, Pareto, 80/20 | 1 | 1 category + 1 numeric |
combo |
Dual-axis comparison | dual-axis, combo, dual-axis, combo | 1~N | 1 category + 1 bar + 1~N line |
venn |
Set overlap (2~3 sets) | overlap, intersection, venn, intersection, overlap, Venn | 1 | 1 name + 1 value; name intersection rows as A∩B(separators: ∩ & + × and) |
mindmap |
Hierarchical ideas | mind map, outline, mind map, mind map, outline | 1 | 1 parent + 1 child (categorical columns) |
orgchart |
Organization structure | org chart, hierarchy, org chart, reporting relationship, hierarchy | 1 | 1 parent + 1 child (categorical columns) |
liquid |
Percentage/progress | liquid, progress, percent, liquid, progress, percentage, completion rate | 1 | 1 numeric (mean used) |
spreadsheet |
Raw table / pivot display | table, detail, spreadsheet, table, detail, list | N | any columns (x/y optionally filter displayed columns; use transform for aggregation) |
map |
Geo choropleth | map, region, province, geo, map, region, province, region | 1 | 1 region name + 1 numeric (default China provinces;--geo/--geo-path to switch to any region) |
lines |
Route/migration on map | route, migration, flow map, route, migration, flow map | special | source + target (+ value) (default province-level names;--geo/--geo-path to switch to any region) |
effect_scatter |
Animated scatter highlights | ripple, highlight, scatter animation, ripple scatter, animated scatter | 1 | 2 numeric, or 1 category + 1 numeric |
calendar |
Daily heatmap (GitHub-style) | calendar heatmap, daily activity, calendar heatmap, daily activity | 1 | 1 date + 1 numeric |
pictorial_bar |
Symbolic/iconic bars | pictorial, icon bar, symbol bar, pictorial bar, icon bar | 1~N | 1 category + 1~N numeric |
theme_river |
Theme evolution over time | theme river, event evolution, theme river, event evolution | 1~N | 1 date + 1~N numeric |
gauge/liquidTo output "achievement rate" you must pass--target <目标值>:achievement= mean ÷ target. If not passed, this field isnull(an automatically inferred range is not a business target, so the ratio has no business meaning), and the output will carry an advisory.
y_axis cardinality key: 1 = only first column used; 1~N = each column becomes a series; N = multiple columns expected; 2 = exactly 2 numeric columns required; special = relationship charts (graph/sankey/lines) are determined by --x-axis(source)+ --y-axis(target, optional value)explicitly specify; no column-name glossary auto-detection is performed. For scatter/bubble/boxplot, the identity column is explicitly specified by --label-col ; if not passed, identity is not displayed (tooltip only shows coordinate values).
Transform Code Contract
Contract (enforced by the sandbox; violations receive an error with suggestion ; just fix it as prompted):
- The only available variables are
df,pd,np; you must produce aresultnamedpd.DataFrame - Do not modify in place
df(usedf.copy()or chained operations) - When the raw data already matches the target format, do not pass
--transform-code
Common transform patterns:
- Long→multi-series:
result = df.pivot_table(index='<time>', columns='<category>', values='<value>', aggfunc='sum').reset_index() - Long→pie (filter):
result = df[df['metric']=='revenue'][['category','value']].rename(columns={'category':'name'}) - Wide→long:
result = df.melt(id_vars=['date'], var_name='name', value_name='value') - Aggregate→bar:
result = df.groupby('<category>')['<value>'].sum().reset_index() - Rename columns:
result = df.rename(columns={'来源':'source','去向':'target','金额':'value'}) - Compute delta→waterfall:
tmp = df.copy(); tmp['delta'] = tmp['profit'].diff().fillna(tmp['profit'].iloc[0]); result = tmp[['month','delta']] - Rename messy/uninformative column names (after
--header-rowleaves columns likescore_a,unnamed_3):result = df.rename(columns={'unnamed_0':'student_id','unnamed_1':'name','score_a':'homework_score','score_b':'exam_score'}) - Forward-fill merged cells (when only the first row of a group is populated):
result = df.ffill() - Drop non-data rows (the positions are given by
--profileinrow_quality.suspicious_non_data_rows, do not guess; this is equivalent to using--drop-rowsto write to disk before plotting, but in transform it is more intuitive):result = df.drop(index=[0,1]) - Combine sub-headers into a single column name (when
--header-row Nflattens one row but loses context):result = df.rename(columns={c: f'{c}_score' for c in df.columns if c not in ['student_id','name']})
A routine step for verifying cleaning results: the
print(df.head())/print(df.columns)in transform is redirected to stderr, without polluting the JSON contract of stdout—so you can directly
Delivery Annotation
When delivering each chart, you must attach a passage oftext interpretation written by the LLM(not template text), then use --annotation to inject it into the HTML (the "Chart Description" block below the chart).
The interpretation should answer "what is the most noteworthy finding in this dataset", not restate what is on the chart. The skill is responsible for calculating facts; judging which finding is worth mentioning and stating it clearly is the agent's job—so the structure of the interpretation is "conclusion → evidence → boundary", not "what the chart is → what the maximum value is".
Citable fact sources (three places):
profile.signals— candidate insights before plotting (trend direction and percentage change, head concentration, counter-trend categories, outliers, strongly correlated pairs)plot_stats/data_preview— this chart'sstatistics of the actually plotted data(the final standard is based on this)assumptions/advisories— automatic choices and standard reminders made by the CLI for you
Standard process:
- When the intent is unclear, first
--profile, pick out the findings to present fromsignals, and based on that decide the chart type and aggregation standard (see the B branch of "Workflow"). - Formally generate once with
--annotation "解读文字".The formal output itself already includesplot_statsanddata_preview, so there is no need to first run--dry-runto get the numbers. - Read that run's
plot_statsto verify the numbers you want to present;after finishing the interpretation, look back: if you find that the standard/chart needs to be changed, then regenerate a second time—regeneration is a cheap and reversible action (1~10s).
--dry-runUse it only when "you want to see the numbers first and clearly do not want to write to disk" (such as trying out column names or testing aggregation scripts). Treating it as a required step before writing the interpretation will double the number of calls with no benefit at all.
Minimum structure of the interpretation (2~4 sentences, conclusion first):
- Finding: give the conclusion in one sentence. Write "South China sales halved starting in April, while the other three regions still grew in the same period", not "this chart shows monthly sales by region".
- Evidence: 1~2 specific values + corresponding labels + source (from
plot_statsorprofile.signals)。 - Boundary: explain the standard in one sentence (aggregation method, coverage, sample size), and echo the assumptions in
assumptions/advisories—if the user disagrees with any assumption, it can be regenerated in one sentence.
Write the conclusion in the title, add the standard in the subtitle:--title Use a conclusion-style phrase (subject + verb + object + value, such as "revenue increased 23% year over year"); the numbers are taken directly from plot_stats, do not use noun phrases ("monthly revenue"); put context such as time range/filter conditions/data source in --subtitle, do not stuff it into the title.
Hard boundaries:
- Every number in the interpretation must have its source traceable in
plot_stats/data_preview/profile.signals— do not fabricate from impression;x_cardinalityis the deduplicated count, expressed according to the semantics of column x (if x is "name", say "59 students" rather than "59 categories"). - Only state facts that the evidence can support; "why/how" can be elaborated based on context, but must be labeled as speculation and must not be mixed in with facts.
advisoriesWhen "suspected unaggregated" appears, aggregate according to the prompt first before writing the interpretation— otherwise the extreme values/averages you cite are row-level values, not the overall values for that dimension, and the conclusion will be wrong (tested: when plotting a line chart of month × region details,plot_statsreported "June highest 156" but it was actually only one region, East China).
See REFERENCE.md for the complete specification and examples.