Communitygithub.com

Jaschenn/my-skills

Create production-ready static charts with plotnine, in Chinese or English, for slides, documents, weekly/monthly business reviews, and journal papers. It selects the right chart based on what the user wants to compare (ranking, trend, change between two points, composition, contribution to change, target attainment, metric decomposition, distribution, relationship, estimates with intervals, KPI summary), renders it at the exact output size (slide/doc/journal column) as SVG, PDF, and PNG, and automatically checks the layout. Business-review charts follow reporting conventions (▲▼ instead of red/green, color only for anomalies, basis captions, period-aligned comparisons). Use when the user asks to draw, plot, chart, or visualize data, make a figure for a deck, report, business review or paper, or fix a chart that looks 'busy but says nothing.' Not for interactive charts (plotly/echarts) or dashboards.

What is my-skills?

This AI agent skill, named `storytelling-viz`, is designed to produce **static, publication-ready charts** using the `plotnine` library. Its primary value is eliminating the need for manual chart polishing and re-scaling: it enforces a set of rigorous visual and semantic rules to ensure that every chart is immediately usable in a slide deck, business report, or journal article. The skill automatically selects the appropriate chart type from a comprehensive list, including bar ranks, dumbbell spots, waterfall diagrams, KPI cards, driver trees, and many others, based on the user's intent (e.g., ranking, trend, composition). It also handles output formatting by spec (e.g., `spec='slide'` or `spec='journal-1col'`) and performs automatic layout QA checks for overlapping text, clipped elements, and contrast ratios. The skill distinguishes between a 'Business Review' scenario (where it uses neutral ▲▼ symbols for changes, highlights only anomalies in red, and requires a basis caption) and a 'General/Publication' scenario. It supports plain text captions, multi-panel layouts, and exports to SVG, PDF, and PNG with correct font embedding. The intended workflow is: (1) determine the context (Business Review or General), (2) select a function from a decision table based on what is being compared, (3) specify the output format with the `spec=` parameter, (4) render the chart and inspect the QA warnings printed by `figure()` or `save()`, and (5) open the PNG for a final manual checklist. The skill is primarily built for Claude Code or similar agent environments where the agent has access to a Python runtime via `uv run`.

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Jaschenn/my-skills/tree/HEAD/storytelling-viz

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

storytelling-viz: Production-Ready Charts

The goal is a chart that can be placed directly into a presentation, document, or journal, not a hastily drawn bar chart. It must satisfy three points:

  • Correct chart type. The chart type matches what is being compared, the focus is clear, and direct labels are used instead of a legend.
  • Does not crash on real data. Long labels, negative values, missing data, many categories, and outliers must all be displayed correctly.
  • Matches output specifications. Dimensions, font sizes, line widths, format, and font embedding must all comply.

This skill is only about the chart. Topic selection, analysis, and report structure are outside its scope.

Workflow

  1. Identify the context. The context determines default conventions such as colors, footnotes, and numbers (see "Two Context Conventions"):
    • Business Review: Charts for business stakeholders or management showing operational metrics, regardless of frequency or occasion (weekly report, monthly report, daily dashboard, anomaly attribution, management/board/strategy meeting operational data pages). The criterion is "is the content an operational metric, and will the reader make a business decision based on it?", not "is it a weekly report?".
    • General / Publication: Charts for analysis or research documents, experimental results, web pages, paper figures, etc.
    • When in doubt, treat it as a business review (the conventions are stricter, only requiring one extra footnote). For the business review context, you must first read references/business.md, even if the example code seems sufficient.
  2. Select the function based on "what to compare." See the function selection table below; both contexts use the same table.
  3. Choose the output spec. Use spec= (slide, document, journal column, etc.).
  4. Generate the chart and review the automatic check results. figure() / save() will print QA … ok or list each warning; the .qa return value contains the same list of issues (empty list = pass). If there are warnings, fix them; do not deliver.
  5. Open the PNG and check against the checklist. Use Read to open the generated PNG and verify each item against the final checklist.

Quick Start

# /// script
# dependencies = ["plotnine>=0.15,<0.16", "pandas", "pillow", "pypdf"]
# ///
import sys; sys.path.insert(0, "<skill>/scripts")
from viz_common import figure, titled, set_lang, save
import charts as C          # General chart functions
import biz as B             # Business review chart functions (can be mixed with C)

set_lang("en")                                    # or "zh" (default)
c = C.bar_rank(df, "channel", "gmv", highlight="POS", top_n=8, fmt="num")
figure([[(titled(c, title="POS is the only channel that shrank"), 1)]],
       spec="doc", name="fig1", outdir="out")
save(c, "fig2", spec="journal-1col", outdir="out")   # Journal single chart: no title

# Business review: same figure(), but you must write a metric footnote
k = B.kpi_cards([dict(label="GMV (MTD)", value=16.5e6, prev=15.9e6, target=57e6, progress=0.29)])
figure([[(k, 1)]], spec="doc", size=(6.5, 2), name="kpi", outdir="out",
       caption="Basis: paid-order GMV (USD); MTD to Oct 9 vs Sep 1–9.")

Function Selection

Find the row based on what you are comparing. C. = charts.py, B. = biz.py; functions marked with ★ are designed for business review but can be used in any context.

What to compareFunctionNotes
Ranking, composition at a point in time, positive/negative valuesC.bar_rank(df, cat, value, highlight=, top_n=, fmt=)Automatically sorts; merges categories beyond top_n into "Other"; value written inside or outside the bar
Who caused the change (scale + growth rate + contribution by dimension)★ B.contrib_table(df, dim, prev, cur, top_n=, growth_alert=)Scale data bars, ▲▼ growth rate, change amount, share of total change; total row at top
Trend: 1–N lines togetherC.line_trend(df, x, y, group=, highlight=, band=, ref=)Direct labels at the end; automatic two-layer month labels for monthly data; breaks for missing data; band= for confidence intervals
Trend: multiple groups viewed separatelyC.small_multiples(df, x, y, facet, highlight=)All panels share the same y-axis scale; gray background lines are other groups
Trend: one operational metric + period-over-period comparison★ B.trend(df, date, value, compare="month"/"year", events=, as_of=, period_end=)Automatically overlays aligned comparison lines; annotates events; dashed line + estimate for incomplete periods
Progress: cumulative value vs target★ B.cumulative_vs_target(df, date, value, target, start, end, as_of=, prior=)Cumulative actual, target progress line, prior period cumulative, daily average-based estimate
Actual vs Target (multiple rows)★ B.bullet(df, cat, actual, target, progress=)Actual bar + target vertical line + dashed "expected completion at current progress" line; rows behind progress turn red
Actual vs Range (prediction interval, normal range)C.range_dot(df, cat, low, high, value, flag="below", bench=)Out-of-range rows use focus color; bench= specifies a reference row
Two values per group (before/after, plan/actual)C.dumbbell(df, cat, start, end, labels=, highlight=)Open circle = start, filled circle = end; sorted by end value; each row shows change rate
First and last periods, large differences in group sizesC.slope(df, cat, start, end, mode="change", groups=)When mode="change", all lines start from 0%; rows with a start of 0 are removed and a WARNING is printed
Waterfall: previous period → contributions → current periodC.waterfall(df, cat, delta, start=, end_label=)Increases/decreases use neutral color + positive/negative signs by default; pass up=PAL.good, down=PAL.bad when good/bad have business meaning
Metric decomposition (GMV = users × frequency × AOV)★ B.driver_tree(nodes)Each node shows current value, ▲▼ change, contribution (logarithmic share for multiplication, change amount for addition)
Composition: percentages of multiple groupsC.stacked_100(df, cat, part, value, order=, focus_cat=)Parts in natural order, single-color gradient; legend is in chart.legend
2D table (group × time, group × group)C.heatmap(df, x, y, value, fmt=)White to single color, scale uses 2%–98% percentile; dark cells have white text
Scale + Ratio (application count and approval rate)★ B.bar_line_dual(df, x, bar, line, line_range=)The only allowed dual-axis chart: scale bars (left) + ratio line (right); ratio axis starts at 0 or a fixed range
DistributionC.distribution(df, value, group=, kind="box"/"strip"/"hist")Sorted by median; whiskers at 1.5×IQR; sample points if many
Relationship between two continuous variablesC.scatter(df, x, y, label=, fit="linear", groups=, x_title=, y_title=)OLS + confidence band, shows R²; only labels highlighted points; legend in chart.legend when groups= is used
Estimates with confidence intervalsC.point_range(df, cat, est, low, high, ref=0)Forest plot; right side shows "estimate [lower, upper]"
Summary of a few key numbers (top of a report)★ B.kpi_cards([dict(label, value, prev, target, progress, spark, …), …])Large numbers, ▲▼ change, attainment rate vs time progress, mini sparkline; only turns red for anomalies

★ Parameter details for functions (kpi_cards fields, driver_tree node structure, trend's events= / as_of=, cumulative_vs_target's prior=, anomaly rules, period-over-period alignment) are in references/business.md.

How to choose when two rows seem similar:

  • B.contrib_table vs C.waterfall: If you need to show scale, growth rate, and share simultaneously, or if there are more than about 8 dimensions → use the table. If you want to explain "how the previous period step-by-step became the current period", with no more than about 8 items → use a waterfall chart. They are often used side-by-side on attribution pages.
  • C.line_trend vs C.small_multiples: If there is one line to emphasize, or no more than 4 lines → use line_trend (for more than 4 lines, add highlight=). If you need to see individual shapes of groups, and all groups have similar importance → use small_multiples. For a single journal column (3.5in), more than 5 facets won't fit; use line_trend + highlight=.
  • For two values per group, first check scale: if scales are similar (max / min < about 10x) → use C.dumbbell. If they differ by more than an order of magnitude, small groups become hard to see on a shared scale; instead, directly plot the change rate: if no more than about 8 groups → use C.slope(mode="change"). For more groups → first calculate a change rate column, then use C.bar_rank(df, cat, "chg", fmt="pct"), and write the absolute scale into the labels or a side table.

Common parameters and return values for all functions:

  • highlight=: A category name, list, or function. Selected items use the focus color, others turn gray, and the category name is also bolded in the same color.
  • fmt=: "num", "pct", "pp", "int", "raw", a format string, or a function; nd= controls decimal places.
  • Returns a panel object, to be placed directly into figure() / save(). chart + plotnine layers can be further layered on top. chart.legend is a single-line color-word legend that titled() uses automatically.

When there is no suitable function, draw it yourself:

  1. Use C.Frame(w, h, xr, yr, left=…, …) to reserve whitespace around the plot based on the actual width of text (text_width). Use C.axis_range(lo, hi) for axis ranges.
  2. On C.Canvas(), draw elements using rect, seg, pt, text, line, band, then generate the plot with cv.plot(fr).
  3. Wrap it as C.Chart(build), where build(w, h) calculates the layout only after the final dimensions are known.

Do not hard-code coordinates or axis ranges in your code.

Two Context Conventions

The functions themselves draw the same way in both contexts; the difference lies in these default conventions. There is no "context" parameter in the code: ★ functions inherently use business review styling (▲▼, red only for anomalies) and will make figure() print a WARNING if a metric footnote is missing. For C. charts on the same page, you must pass the appropriate parameters according to the table below (e.g., caption=, highlight=, up= / down= colors).

ItemGeneral / PublicationBusiness Review
Increases/DecreasesUse good / bad only when good/bad has meaning, and must be accompanied by a sign or textAlways use ▲▼ + neutral color, never red/green for increases/decreases
When to use redOnly to indicate "bad"Only when judged as anomalous (behind schedule / exceeding threshold / exceeding historical ±2σ); favorable anomalies are only bolded
Directing attention (e.g., channel dragging down growth)highlight= (focus color orange)Same; orange only means "look here," not good/bad; whether it is anomalous is expressed by ▲▼ and red
Metric footnoteRecommendedRequired (caption=): numerator, denominator, currency, time window, comparison basis; missing it will cause figure() to emit a WARNING
Comparison basisAs needed for analysisDefault: month-over-month, current month cumulative vs same days of previous month, target attainment; daily traffic metrics aligned by day of week
Incomplete current periodLight color or dashed + "As of X date"Same, plus draw an estimate for the full period scaled by progress
Number formattingPer domain conventionsK/M/B for amounts; ratio changes in pp, growth rates in %; unit written in title or annotation
Dual axisAvoidOnly "scale bar + ratio line" is allowed (B.bar_line_dual)
Pie / Donut chartsAvoid as much as possibleForbidden
Common specsdoc, web, journal-*Use slide for PPT; use doc for Feishu / Lark / Notion documents or chats (PNG illustration); use doc when the user has not specified the placement

Reasons and counterexamples are in references/rules.md (business review sections are B1–B7).

Output Specifications (spec=)

specDimensions (inches)Body font sizeDefault formatsUse case
slide / slide-4x313.33×7.5 / 10×7.512 / 11.5Outline SVG, editable SVG, PNG (150dpi)Presentation slides
web8×4.510.5SVG, PNG (200dpi)Web pages, emails
doc6.5×49PDF, SVG, PNG (300dpi)A single chart in a document body
a4 / letterFull page10PDF, SVG, PNGFull-page report
journal-1col / -1.5col / -2col3.5 / 5.35 / 7.2 wide7–7.5PDF, SVG, PNG (600dpi)Journals (89 / 136 / 183 mm)
  • size=(w, h) overrides the default dimensions, e.g., a taller journal figure or a shorter row of KPI cards.
  • Font sizes, line widths, and point sizes scale with the spec; PDF uses Type 42 embedded fonts, meeting journal requirements.
  • Journal charts have no title; figure captions are placed in the text body. For a single chart: use save(chart, name, spec="journal-1col"). For multi-panel figures: use figure(rows, spec="journal-2col"), without passing title=, and use titled(chart, title="a") for panel labels.
  • figure() produces a whole page (optionally with title, subtitle, metric footnote, multiple panels). save(chart, "fig2", spec="journal-1col") exports only a single chart, without page title or footnote. Business review requires footnotes, so use figure().
  • Two types of SVG: <name>.svg has text converted to outlines, looking the same everywhere; <name>_editable.svg preserves text, editable in Figma / PPT, but the recipient's computer must have the same fonts. Use the latter for editable text in PPT, otherwise use the former or PNG.
  • slide / web do not produce PDF by default; pass formats=("pdf", "png") if needed.

Combining Multi-Panel Layouts

  • figure(rows, title=, subtitle=, caption=, spec=): rows=[[(panel, span), …], …], where span is the relative width within a row.
  • Row height is automatically allocated: rows containing only text or tables take height based on content; the remaining space is divided equally among the other rows.
  • titled(chart, title=, legend=, note=): Adds a panel title, legend row, and annotation above the chart. If legend= is not provided, chart.legend is used automatically.
  • Titles support rich text markup: **bold**; [phrase](focus) makes it bold and colored. Colors can be focus, brand, good, bad, muted, ink, or #hex.
  • Other panels: text_panel(para(...)) for paragraphs, table_panel(rows, header=) for small tables (number columns automatically right-aligned), column(a, b) to stack two panels vertically.
  • Each panel is rendered individually at its final dimensions, so font sizes are in actual points. When stitching SVGs, element IDs are automatically prefixed to avoid conflicts.

Visual Rules (functions execute them by default; follow them when drawing manually)

  • Color hierarchy: focus (orange) marks the one and only thing to look at; it is not inherently good or bad. brand (dark blue) is for regular series. Gray is for backgrounds and references. good / bad usage depends on context (see table above).
  • Same color, same meaning. Text keywords and the corresponding data use the same color. Elements with different meanings must not share a color.
  • Text must be readable. Text contrast ratio must be at least 4.5:1. When using the focus color for text, automatically darken it with readable(color). Text on dark backgrounds uses ink_on(fill). Functions handle this automatically; when drawing manually, Canvas.text also checks and warns.
  • Use direct labels instead of a legend. Axis titles must not be field names. If all bars are labeled with values, remove the y-axis and grid.
  • Sorting must be meaningful. Items with a natural order (phases, grades) use that order; otherwise, sort by value. Place the focus item in the most prominent position. Do not sort alphabetically.
  • Axis. Bar charts must start at 0. Line charts start at 0 when data is close to 0 (zero="auto"). Related panels share the same scale. Dual axes are only allowed for "scale bar + ratio line".
  • Precision. Enough to distinguish. All numbers are calculated from data and formatted using pct / pp / num / fmt_value; do not write them manually.
  • Choose the chart based on what you are comparing: Do not use an absolute stacked bar for proportion changes. Do not draw multiple pie charts for multi-group composition. When there are more than 4 lines, use highlight= to focus or switch to small multiples. If comparing two items with a tree map or radar chart takes more than about 2 seconds, switch to horizontal bars.
  • Mark data issues on the chart. Incomplete current periods use a light color or dashed line with the note "As of X date". Write n for small samples. Break the line for missing data; do not connect it.

Details and rationale are in references/rules.md; design approaches from SWD chart remakes are in references/swd_practices.md.

Language and Typography

  • set_lang("zh" | "en") switches the default font, month labels (Chinese: "1月\n2026", English: "Jan\n2026"), and "其他/Other". Currency units default to K/M/B in both languages; use set_number_style("cn") for 万/亿. Labels containing Chinese characters automatically use a CJK font. T("其他", "Other") retrieves the string in the current language.
  • Chinese character strings are automatically processed by zh(): punctuation converted to full-width, extra spaces next to full-width punctuation removed, URLs and abbreviations preserved.
  • Line-breaking rules: numbers and English words are not broken. Punctuation does not appear at the start of a line. Opening brackets are not left at the end of a line.
  • The program prints the fonts used at startup. If WARNING: no CJK font appears, install Noto Sans SC.

Pre-Delivery Checklist

Automated checks (look at figure() output or .qa):

  • QA … ok: No overlapping text, no text outside the panel, no clipping by the plot area.
  • No contrast warnings, and no warnings like rows do not fit, labels take most of the panel, cells too small, basis caption.

Manual checks (open the PNG):

  • Cover the title, look only at the chart: can you say what it is about? Is there only one focus?
  • Do the numbers in the title and annotations come from the same variable as the values on the chart?
  • Does the chart type match what is being compared (see function selection table)? Is the sorting meaningful? Are related panel scales consistent?
  • Does color usage follow the context conventions? If the color were removed, can good/bad or increases/decreases still be read from symbols or text?
  • Are there no default legends, field names, 3D effects, or gradients?
  • Do the dimensions and font sizes match the target specification (journal body ~7pt, slides ≥12pt)? If PDF was needed, has it been exported?

Additional for business review:

  • Is the metric footnote complete? Has it been cross-checked with other reports (within the same report, the same number is consistent across pages)?
  • Are units, decimal places, and formatting consistent throughout the chart?

Environment and Testing

  • Dependencies: plotnine>=0.15,<0.16 (verified with 0.15.8 + matplotlib 3.11), pandas, pillow, plus pypdf for PDF export. Code is in scripts/viz_common.py (low-level tools, specs, export, QA), scripts/charts.py (C.), and scripts/biz.py (B.).
  • Output location: Set outdir to a directory the user can see; do not write into the skill directory itself.
  • Example galleries: uv run references/gallery.py [outdir] (every C. function, Chinese slides + English journal). uv run references/business_gallery.py [outdir] (every B. function, Chinese slides + English document). Default output is out/ in the current directory.
  • Regression tests: uv run references/tests/edge_cases.py runs 38 sets of edge-case data under two specs (76 total), checking that functions and QA behave as expected. You must rerun this after changing any chart function; all tests must pass before releasing.
  • When plotnine cannot be installed in the sandbox: Use the matplotlib simulator in PYTHONPATH=<skill>/scripts/plotnine_preview first to preview layout and run QA (line width and clipping rules are the same as plotnine). The simulator's QA ok does not count as a delivery standard: pass the script to the user to run locally via uv run, and explicitly tell the user this chart has not been rendered or checked with real plotnine.

Related Skills