malloydata/publisher/malloy-charts

Claude Code and agent skill by malloydata/publisher

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Read before choosing a chart or adding a chart annotation. Chart types, tag syntax and scale rules, KPI cards, dashboards, sparklines, and the renderer mistakes that fail silently.

Install

npx vskill@latest install malloydata/publisher/malloy-charts

SKILL.md

Chart Selection for Malloy

Malloy uses Vega-Lite under the hood. # tags control visualization. Call search_malloy_docs with topic "rendering" for the full tag reference (or see https://docs.malloydata.dev/documentation/visualizations/overview).
This file is about the Malloy renderer's `#` tags. It applies when the view itself is rendered - a tile or cell in a report (either layout), an explore result. It does not apply to an app that draws its own charts with a vendored chart library: there the tag vocabulary is irrelevant and the full form vocabulary is available, so the approximations below (a funnel as a bar chart, a treemap as a nested table) are the wrong advice. Design to whichever vocabulary the surface actually uses.
Tool names are written bare here - get_context, execute_query, search_malloy_docs. The exact prefixed name depends on the host surface; match each against the tools you actually have.

Decision Tree: Which Chart?

Data ShapeDefault Choice
Aggregates only (no group_by)# big_value
1 time column + 1 measure# line_chart
1 category + 1 measure# bar_chart
2 numeric columns# scatter_chart
Geographic (US states) + 1 measure# shape_map
Route data (lat/lon pairs)# segment_map
Multiple perspectives# dashboard with nest:
Nested query to pivot# pivot
Filtered aggregates side-by-side# flatten
Detailed rowsDefault table (no annotation)
GoalRenderer
Compare categories# bar_chart (sort by value, limit ~15)
Show composition# bar_chart.stack
Trend over time# line_chart
Highlight KPIs# big_value with # label
Correlation# scatter_chart
Compare dimensions# dashboard (nest chart views)
Before/after# transpose or # pivot
Multiple metrics per categoryDefault table, # flatten, or y=['a','b']

Constraints:

  • ONE aggregate per chart view (charts render only the first; use y=['a','b'] for multi-measure)
  • No fixed scale on measure definitions: use # currency not # currency=usd0m. The same measure renders at many granularities, and usd0m turns $500 into $0.0M. Add a scale only in a view, after confirming the value range with a query.
  • One tag per line, each directly above the field it styles. Two # tags on one line do not work.
  • Alias joined fields in group_by before order_by
  • Define measures in source, not in views

Chart Types

# bar_chart

Data shape: group_by = x-axis, aggregate = y-axis, optional 2nd group_by = series.

malloy
# bar_chart
view: by_carrier is { group_by: carrier, aggregate: flight_count, order_by: flight_count desc, limit: 10 }
# bar_chart.stack
view: by_region is { group_by: category, region, aggregate: revenue }
# bar_chart { y=['revenue','cost'] }
view: rev_vs_cost is { group_by: category, aggregate: revenue, cost }

Key properties: .stack, .size (spark/xs/sm/md/lg/xl/2xl), .x, .x.limit, .y (supports y=['a','b']), .series, .series.limit (default 20), .title, .subtitle, .x.independent, .y.independent

Field role tags: # x, # y, # series on individual fields to assign roles explicitly.

# line_chart

Data shape: group_by (temporal/numeric) = x-axis, aggregate = y-axis, optional 2nd group_by = series.

malloy
# line_chart
view: trend is { group_by: order_month, aggregate: revenue, order_by: order_month }
# line_chart { size=spark }
view: mini_trend is { group_by: order_month, aggregate: revenue, order_by: order_month }

Key properties: .zero_baseline, .interpolate (e.g., step), .size, .y (supports y=['a','b']), .series.limit (default 12), .title, .subtitle

# scatter_chart

Data shape: Fields by position: x, y, color, size (bubble), shape.

malloy
# scatter_chart
view: correlation is { group_by: customer_id, aggregate: avg_price, total_quantity }
# shape_map

Choropleth. US states only. Fields: state name, value.

malloy
# shape_map
view: by_state is { group_by: state, aggregate: revenue }
# segment_map

Route map. US only. Fields: start_lat, start_lon, end_lat, end_lon, color.

Layout Types

# big_value

KPI cards. Aggregates only, no group_by.

malloy
# big_value
view: summary is {
aggregate:
# label="Revenue"
# currency
revenue
# label="Orders"
# number=auto
order_count
}

Properties: .size, .sparkline=<nested_view_name>, .comparison_field, .comparison_label, .down_is_good

Put # label on every measure. Without it the card shows the raw field name, which is often unclear.

# dashboard

Card-based multi-tile layout. Apply to a view whose body is a nested query; the view's own fields lay out automatically:

  • group_by dimensions -> a row header (repeats once per row; omit for a single block)
  • aggregate measures -> KPI cards, one per measure
  • each nest: -> a tile, rendered by the tag above it (# table default, or # bar_chart / # line_chart / # big_value)

Two modes. Flex (default): tiles flow and wrap; # break forces a new row. Columns: # dashboard { columns=N } lays tiles into N equal columns, # colspan=n widens a tile (the old # span is gone), # break starts a new row, overflow wraps.

malloy
// Flex: measures become KPI cards, the nest becomes a tile
# dashboard
view: overview is {
group_by: category
# currency
aggregate:
avg_retail is retail_price.avg()
sum_retail is retail_price.sum()
nest:
# bar_chart
by_brand is { group_by: brand, aggregate: avg_retail is retail_price.avg(), limit: 10 }
}
// Columns: # colspan widens tiles, # break ends a row
# dashboard { columns=12 }
view: layout is {
group_by: category
# currency
aggregate:
# colspan=4
avg_retail is retail_price.avg()
# colspan=4
sum_retail is retail_price.sum()
# colspan=4
max_retail is retail_price.max()
nest:
# break
# colspan=6
# bar_chart
# subtitle="Top brands"
by_brand_chart is { group_by: brand, aggregate: avg_retail is retail_price.avg(), limit: 8 }
# colspan=6
by_brand_table is { group_by: brand, aggregate: product_count is count(), limit: 8 }
}

Tags: # dashboard { columns=N } (columns mode), { gap=PX } (tile spacing, default 16; never a mode), { table.max_height=PX|none } (cap table tiles). On a measure or nest: # colspan=N (columns mode only), # break (both modes), # subtitle="..." (tile), # borderless (drop card chrome), # label="..." (card title).

For rich KPI cards (sparklines, comparison deltas, several metrics on one card) nest a # big_value view instead of relying on the dashboard's own measures:

malloy
# dashboard
view: kpis is {
group_by: category
nest:
# big_value
revenue_card is {
aggregate:
# label="Revenue"
# currency
# big_value { sparkline=trend }
total_revenue is retail_price.sum()
# line_chart { size=spark y.independent=true }
# hidden
nest: trend is { group_by: bucket is floor(id / 100)::number, aggregate: total_revenue is retail_price.sum(), order_by: bucket, limit: 20 }
}
}

Rules: # dashboard needs a nested-query view (no effect on a scalar). # colspan works only in columns mode and is ignored (warns) in flex. columns is any positive integer; a # colspan over the column count clamps to a full row. Style tiles via the instance theme, or theme the views inside with # theme.* (see Theming below).

# pivot

Pivot nested results into columns. Max 30 pivot columns.

malloy
view: sales is {
group_by: product, aggregate: total
nest: # pivot
by_quarter is { group_by: quarter, aggregate: revenue }
}
# transpose

Swap rows/columns. Good for period comparisons.

malloy
# transpose
view: comparison is {
aggregate:
# label="This Month"
current_revenue
# label="Last Month"
prior_revenue
}
# list / # list_detail

List renders as comma-separated values. List_detail shows value (detail) pairs.

# flatten

Collapse nested record into parent table as columns. Use for side-by-side filtered aggregates:

malloy
view: segments is {
group_by: product, aggregate: total_revenue
nest: # flatten
enterprise is { where: segment = 'Enterprise', aggregate: # label="Enterprise" revenue }
nest: # flatten
smb is { where: segment = 'SMB', aggregate: # label="SMB" revenue }
}
# table

Default (implicit). Use explicitly for .size=fill property.

Field Formatting Tags

TagUse ForShorthand
# numberNumeric formatting=auto (K/M/B), =id (no commas), =1k, =1m
# percentPercentages(none needed)
# currencyMoney=usd2m (USD, 2 decimals, millions); scale only in views
# durationTime durations=seconds, =minutes, =hours, =days
# data_volumeStorage sizes=bytes, =kb, =mb, =gb
# linkHyperlinks.url_template="https://example.com/$$"
# imageInline images.height=40px, .width=100px

# image and # link are fine in a model file. A document held as text (one with a model-level ## artifact tag, compiled at scope append) is refused if it writes either; define the field in the model file and check that edit at scope file.

Currency codes: usd ($), eur, gbp. Scale: K/M/B/T/Q or auto. Number suffix styles: word ("42.5 million"), letter ("42.5M"), scientific.

Utility Tags

TagPurpose
# hiddenHide from output (still usable for sorting/references)
# label="..."Override display name
# description="..."Tooltip text
# tooltipInclude nested view in chart tooltip
# breakForce new dashboard row
# column { width=sm }Table column width

Model-Level Defaults

malloy
## viz.line_chart.defaults.y.independent=true
## viz.bar_chart.defaults.stack

Theming

Publisher styles charts and tables from one structured theme. The instance sets it (in publisher.config.json's theme block or the Settings, then Theme editor); a model overrides it per result with # theme.* annotations, or model-wide with ## theme.*. Per-chart annotations use the same nested palette.* / font.* vocabulary as the config, not flat key names, and they win over the instance theme for the keys they set. The forms:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
95
Repository forks
23
7-day trend score
23

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/malloydata/publisher/malloy-charts/badge)](https://verified-skill.com/skills/malloydata/publisher/malloy-charts)

Something wrong with this skill or its page? Tell us.