diff --git a/src/documentation/visualizations/about_rendering.malloynb b/src/documentation/visualizations/about_rendering.malloynb
index 5e6d8efb..3763859e 100644
--- a/src/documentation/visualizations/about_rendering.malloynb
+++ b/src/documentation/visualizations/about_rendering.malloynb
@@ -1,25 +1,24 @@
>>>markdown
-# Rendering Documentation
-The latest and most up to date documentation for the Malloy renderer are now found in GitHub:
+# Rendering Results
-- [Renderer Tag Documentation](https://github.com/malloydata/malloy/blob/main/packages/malloy-render/docs/renderer_tags_overview.md)
-- [Renderer Tag Cheatsheet](https://github.com/malloydata/malloy/blob/main/packages/malloy-render/docs/renderer_tag_cheatsheet.md)
+Malloy's rendering library interprets tags to control how query results are displayed.
-For developers wishing to implement custom renderers, a plugin system is available:
-- [Plugin System Overview](https://github.com/malloydata/malloy/blob/main/packages/malloy-render/docs/plugin-system.md)
-- [Plugin Quick Start](https://github.com/malloydata/malloy/blob/main/packages/malloy-render/docs/plugin-quick-start.md)
-- [Plugin API Reference](https://github.com/malloydata/malloy/blob/main/packages/malloy-render/docs/plugin-api-reference.md)
->>>markdown
- # Using Render Tags
+## Documentation
+
+- [Renderer Tags](renderer_tags.malloynb) - Complete guide to all visualization and formatting tags
+- [Tag Cheatsheet](renderer_cheatsheet.malloynb) - Quick reference for all tags
+
+## Plugin System
-When Malloy runs a query, it returns two things. The *results* of the query and *metadata* about the results. The metadata are the schema for the results, including type information. Malloy also provides a mechanism to tag things in the source code and return tags with this meta data.
+- [Custom Plugins](plugins.malloynb) - Build your own renderers for specialized visualizations
-In Malloy, anything that can be named can be tagged. A tag starts with a `#`. Tags that start on a new line attach the tag the thing on the following line. For more details about how tagging works, see the [Tags](../language/tags.malloynb) section.
+---
-Malloy's rendering library interprets these tags to change how results are rendered.
+## How Tags Work
-## Tagging individual elements
-In the query below, the measure `percent_of_total` is tagged as a percentage. Any time `percent_of_total` is used in a query, Malloy's rendering library will be displayed as a percentage.
+When Malloy runs a query, it returns results and metadata including schema and tags. Tags start with `#` and attach to the following item.
+
+For details on tag syntax, see [Tags](../language/tags.malloynb).
>>>malloy
source: flights is duckdb.table('../data/flights.parquet') extend {
measure:
@@ -31,23 +30,18 @@ source: flights is duckdb.table('../data/flights.parquet') extend {
#(docs) size=small limit=5000
run: flights -> {
group_by: carrier
- aggregate:
- flight_count
+ aggregate:
+ flight_count
percent_of_flights
}
->>>malloy
-#(docs) size=small limit=5000
-run: duckdb.table('../data/flights.parquet') -> {
- group_by: carrier
- aggregate: flight_count is count()
-}
>>>markdown
-Simply adding `# bar_chart` before the query tags it and tells the rendering library to show the result as a bar chart. See the docs on the [Bar Chart tag](./bar_charts.malloynb) for more information.
+## Quick Examples
>>>malloy
#(docs) size=large limit=5000
# bar_chart
-run: duckdb.table('../data/flights.parquet') -> {
+run: duckdb.table('../data/flights.parquet') -> {
group_by: carrier
aggregate: flight_count is count()
-}
\ No newline at end of file
+ limit: 10
+}
diff --git a/src/documentation/visualizations/plugins.malloynb b/src/documentation/visualizations/plugins.malloynb
new file mode 100644
index 00000000..992ada27
--- /dev/null
+++ b/src/documentation/visualizations/plugins.malloynb
@@ -0,0 +1,46 @@
+>>>markdown
+# Custom Renderer Plugins
+
+The Malloy Render plugin system lets you create custom visualizations for specific field types or data patterns.
+
+## Use Cases
+
+- Custom chart types not included in the standard renderer
+- Domain-specific visualizations (maps, diagrams, etc.)
+- Branded or styled components
+- Interactive visualizations with custom behavior
+
+## Getting Started
+
+Plugins can be built using SolidJS (recommended) or direct DOM manipulation. They match fields based on tags and field types.
+
+```typescript
+const MyPluginFactory: RenderPluginFactory = {
+ name: 'my_plugin',
+
+ matches: (field, fieldTag, fieldType) => fieldTag.has('my_plugin'),
+
+ create: (field) => ({
+ name: 'my_plugin',
+ field,
+ renderMode: 'solidjs',
+ sizingStrategy: 'fixed',
+ renderComponent: (props) =>
{props.dataColumn.value}
,
+ getMetadata: () => ({ type: 'my_plugin' })
+ })
+};
+```
+
+Then tag fields in Malloy to use your plugin:
+
+```malloy
+source: data extend {
+ dimension: status # my_plugin
+}
+```
+
+## Documentation
+
+- [Plugin System Overview](https://github.com/malloydata/malloy/blob/main/packages/malloy-render/docs/plugin-system.md) - Architecture and concepts
+- [Plugin Quick Start](https://github.com/malloydata/malloy/blob/main/packages/malloy-render/docs/plugin-quick-start.md) - Minimal examples
+- [Plugin API Reference](https://github.com/malloydata/malloy/blob/main/packages/malloy-render/docs/plugin-api-reference.md) - Complete type definitions
diff --git a/src/documentation/visualizations/renderer_cheatsheet.malloynb b/src/documentation/visualizations/renderer_cheatsheet.malloynb
new file mode 100644
index 00000000..e0629748
--- /dev/null
+++ b/src/documentation/visualizations/renderer_cheatsheet.malloynb
@@ -0,0 +1,72 @@
+>>>markdown
+# Renderer Tag Cheatsheet
+
+| Tag | Description | Details | Example |
+| :-------------------------------- | :----------------------------------------- | :------------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------- |
+| **Charts & Visualization** | | | |
+| `# bar_chart` | Render data as a bar chart. | Base tag for bar chart configuration. Axes/series often inferred. | `view: my_view is { # bar_chart ... }` |
+| `# bar_chart.stack` | Stack bars in a series. | Boolean property. Use when a `series` field is defined. | `view: my_view is { # bar_chart.stack ... }` |
+| `# bar_chart.size` | Set preset chart size. | Values: `spark`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`. | `view: my_view is { # bar_chart { size=lg } ... }` |
+| `# bar_chart.size.width` | Set specific chart width. | Value is in pixels. Overrides preset width. | `view: my_view is { # bar_chart { size.width=450 } ... }` |
+| `# bar_chart.size.height` | Set specific chart height. | Value is in pixels. Overrides preset height. | `view: my_view is { # bar_chart { size.height=300 } ... }` |
+| `# bar_chart.x` | Define x-axis field. | Assigns a field to the category axis. | `view: my_view is { # bar_chart { x=category } ... }` |
+| `# bar_chart.x.limit` | Limit categories on x-axis. | Numeric value. Limits bars shown. Auto-calculated if not set. | `view: my_view is { # bar_chart { x.limit=10 } ... }` |
+| `# bar_chart.x.independent` | Control x-axis scale sharing. | Boolean (`true`/`false`). Overrides auto-sharing based on cardinality (default shares if <=20). | `view: my_nest is { # bar_chart { x.independent } ...}` |
+| `# bar_chart.y` | Define y-axis field(s). | Assigns field(s) to the value axis. Use array `['m1', 'm2']` for measure series. | `view: my_view is { # bar_chart { y=total_sales } ... }`
`view: my_view is { # bar_chart { y=['Sales', 'Cost'] } ... }` |
+| `# bar_chart.y.independent` | Control y-axis scale sharing. | Boolean (`true`/`false`). Default is shared. Use `true` for independent scales in nested charts. | `view: my_nest is { # bar_chart { y.independent } ... }` |
+| `# bar_chart.series` | Define series field. | Assigns a field for grouping/coloring bars. | `view: my_view is { # bar_chart { series=department } ... }` |
+| `# bar_chart.series.limit` | Limit number of series shown. | Numeric value or `auto`. Default `auto` uses 20. Series ranked by sum of Y-values. | `view: my_view is { # bar_chart { series.limit=5 } ... }`
`view: my_view is { # bar_chart { series.limit=auto } ... }` |
+| `# bar_chart.series.independent` | Control series legend/color sharing. | Boolean (`true`/`false`). Overrides auto-sharing based on cardinality (default shares if <=20). | `view: my_nest is { # bar_chart { series.independent } ... }` |
+| `# bar_chart.title` | Set chart title. | String value. | `view: my_view is { # bar_chart { title='Sales Distribution' } ... }` |
+| `# bar_chart.subtitle` | Set chart subtitle. | String value. | `view: my_view is { # bar_chart { subtitle='Q1 Data' } ... }` |
+| `# line_chart` | Render data as a line chart. | Base tag for line chart configuration. Axes/series often inferred. | `view: my_view is { # line_chart ... }` |
+| `# line_chart.zero_baseline` | Control if y-axis includes zero. | Boolean (`true`/`false`). Default `false` (unlike bar charts). `true` forces axis to start at zero. | `view: my_view is { # line_chart { zero_baseline=true } ... }` |
+| `# line_chart.size` | Set preset chart size. | Values: `spark`, `xs`...`2xl`. | `view: my_view is { # line_chart { size=spark } ... }` |
+| `# line_chart.interpolate` | Set line interpolation mode. | E.g., `step`. | `view: my_view is { # line_chart { interpolate=step } ... }` |
+| `# line_chart.x` | Define x-axis field. | Assigns a field (often time-based) to the x-axis. | `view: my_view is { # line_chart { x=sale_date } ... }` |
+| `# line_chart.x.independent` | Control x-axis scale sharing. | Boolean (`true`/`false`). Overrides auto-sharing (default shares if <=20). | `view: my_nest is { # line_chart { x.independent } ... }` |
+| `# line_chart.y` | Define y-axis field(s). | Assigns field(s) to the value axis. Use array `['m1', 'm2']` for multiple lines from measures. | `view: my_view is { # line_chart { y=value } ... }`
`view: my_view is { # line_chart { y=['MetricA', 'MetricB'] } ... }` |
+| `# line_chart.y.independent` | Control y-axis scale sharing. | Boolean (`true`/`false`). Default is shared, unless `series.limit` is used. | `view: my_nest is { # line_chart { y.independent } ... }` |
+| `# line_chart.series` | Define series field. | Assigns a field for multiple lines. | `view: my_view is { # line_chart { series=category } ... }` |
+| `# line_chart.series.limit` | Limit number of series lines. | Numeric value or `auto`. Default `auto` uses 12. Series ranked by sum of Y-values. Implies `y.independent=true`. | `view: my_view is { # line_chart { series.limit=5 } ... }`
`view: my_view is { # line_chart { series.limit=auto } ... }` |
+| `# line_chart.series.independent` | Control series legend/color sharing. | Boolean (`true`/`false`). Overrides auto-sharing (default shares if <=20). | `view: my_nest is { # line_chart { series.independent } ... }` |
+| `# line_chart.title` | Set chart title. | String value. | `view: my_view is { # line_chart { title='Trend Over Time' } ... }` |
+| `# line_chart.subtitle` | Set chart subtitle. | String value. | `view: my_view is { # line_chart { subtitle='Daily Values' } ... }` |
+| `# scatter_chart` | Render as scatter plot. | Legacy renderer. Uses field order (x, y, color, size, shape). | `view: my_view is { # scatter_chart ... }` |
+| **Layout & Structure** | | | |
+| `# dashboard` | Render nested views as a dashboard. | Base tag for dashboard layout. | `view: my_dashboard is { # dashboard nest: ... }` |
+| `# dashboard.table.max_height` | Set max height for tables in dashboard. | Numeric pixel value or `'none'`. | `view: my_dashboard is { # dashboard { table.max_height=400 } ... }` |
+| `# break` | Insert layout break in dashboard. | Applied to a `nest:` definition within a dashboard. | `view: my_dashboard is { nest: chart1 is {...} # break nest: chart2 is {...} }` |
+| `# table` | Render data as a table. | Often the default for nested data. | `view: my_table is { # table ... }` |
+| `# table.pivot` | Pivot table dimensions into columns. | Use `# pivot` for automatic, or `# pivot { dimensions=["d1"] }` for specific dimensions. | `nest: my_pivot is { # pivot ... }`
`nest: my_pivot is { # pivot { dimensions=["country"] } ... }` |
+| `# table.flatten` | Flatten nested record into columns. | Applied to a `nest:` definition. Nested query must not have `group_by`. | `nest: metrics is { # flatten ... }` |
+| `# table.size=fill` | Make table fill container width. | Boolean property applied via value. | `view: full_width_table is { # table.size=fill ... }` |
+| `# table.column.width` | Set specific column width. | Apply to a field. Value: `xs`, `sm`, `md`, `lg`, `xl`, `2xl` or pixels. | `dimension: my_col is ... # column { width=lg }`
`dimension: my_col is ... # column { width=150 }` |
+| `# table.column.height` | Set specific row height for column cells. | Apply to a field. Value in pixels. | `dimension: my_col is ... # column { height=50 }` |
+| `# table.column.word_break` | Control word breaking in cells. | Value: `break_all`. Apply to a field. | `dimension: long_text is ... # column { word_break=break_all }` |
+| `# list` | Render first field as list. | Comma-separated values from the first non-hidden field of a nest. | `nest: top_items is { # list ... }` |
+| `# list_detail` | Render first two fields as list. | Comma-separated `value (detail)` from first two non-hidden fields. | `nest: item_details is { # list_detail ... }` |
+| `# transpose` | Transpose table rows/columns. | Applied at the view level. | `view: metrics_row is { # transpose ... }` |
+| **Field Formatting/Rendering** | | | |
+| `# currency` | Format number as currency. | Default is USD ($). Specify units like `euro` or `pound`. | `measure: revenue is ... # currency`
`measure: revenue_eur is ... # currency=euro` |
+| `# percent` | Format number as percentage. | Multiplies by 100, adds '%'. | `measure: margin is ... # percent` |
+| `# number` | Format number with `ssf` string. | Provide format string as value. | `measure: my_num is ... # number="#,##0.0"` |
+| `# duration` | Format number as duration. | Default input unit: `seconds`. Specify units (`nanoseconds`..`days`). | `measure: avg_time is ... # duration`
`measure: compute is ... # duration=milliseconds` |
+| `# duration.terse` | Use abbreviated duration units. | E.g., `ns`, `s`, `m`, `h`, `d`. | `measure: short_time is ... # duration.terse` |
+| `# duration.number` | Format numeric parts of duration. | Uses `ssf` format string. | `measure: precise_duration is ... # duration { number="0.00" }` |
+| `# image` | Render string as image URL. | Applied to a string field containing a URL. | `dimension: logo_url is ... # image` |
+| `# image.height` | Set image height. | CSS value (e.g., `40px`). | `dimension: logo_url is ... # image { height=40px }` |
+| `# image.width` | Set image width. | CSS value (e.g., `100px`). | `dimension: logo_url is ... # image { width=100px }` |
+| `# image.alt` | Set image alt text. | Literal string value. | `dimension: logo_url is ... # image { alt='Company Logo' }` |
+| `# image.alt.field` | Use another field for alt text. | Value is relative path to field (e.g., `field_name`, `'../parent_field'`). | `dimension: logo_url is ... # image { alt.field=product_name }` |
+| `# link` | Render field as hyperlink. | Applied to a field whose value is the link text. | `dimension: url is ... # link` |
+| `# link.url_template` | Template for link href. | String where `$$` is replaced by value (from this field or `.field`). Appends if `$$` is missing. | `dimension: name is ... # link { url_template="https://example.com/$$" }` |
+| `# link.field` | Use another field's value for href. | Value is relative path to the field containing the href data. | `dimension: link_text is 'Search' # link { field=query_term url_template="https://google.com/search?q=$$" }` |
+| **Utilities & Configuration** | | | |
+| `# hidden` | Hide field from rendering. | Field remains in data, just not displayed in tables/dashboards. | `dimension: internal_id is ... # hidden` |
+| `# label` | Override display name/title. | String value. Applied to fields or dashboard items. | `measure: total_revenue is ... # label="Total Sales ($)"` |
+| `# tooltip` | Include nested view in tooltip. | Applied to a `nest:`. Can contain render hints for the tooltip itself. | `nest: details is { # tooltip ... }`
`nest: chart_tip is { # tooltip bar_chart.size=xs ... }` |
+| `# size` | Set preset size (legacy). | Prefer `.size` on specific renderer tags. Values: `spark`, `xs`...`2xl`. Applied to view/nest. | `view: my_view is { # size=lg ... }` |
+| `# theme` | Apply theme style overrides (View level). | Contains CSS-like properties (e.g., `tableBodyColor`, `tableRowHeight`). | `view: my_view is { # theme { tableBodyColor=red } ... }` |
+| `## theme` | Apply theme style overrides (Model level). | Contains CSS-like properties. Sets defaults for the model. | `## theme { tableBodyColor=blue }` |
+| `## renderer_legacy` | Use legacy HTML renderer for model. | Model-level tag. No properties. | `## renderer_legacy` |
diff --git a/src/documentation/visualizations/renderer_tags.malloynb b/src/documentation/visualizations/renderer_tags.malloynb
new file mode 100644
index 00000000..e886764c
--- /dev/null
+++ b/src/documentation/visualizations/renderer_tags.malloynb
@@ -0,0 +1,547 @@
+>>>markdown
+# Renderer Tags
+
+Control how Malloy renders query results using tags. Tags are annotations that tell the renderer how to display data.
+
+## Basic Syntax
+
+- Tags are prefixed with `#`: `# tag_name`
+- Tags with properties: `# tag_name { property=value }`
+- Boolean properties use dot notation: `# tag_name.property`
+- Nested properties: `# bar_chart { y.independent }`
+
+---
+
+## Chart Tags
+
+### Bar Chart
+
+Renders data as a bar chart. The renderer infers x (first dimension), y (first measure), and series (second dimension) from query structure.
+>>>malloy
+source: flights is duckdb.table('../data/flights.parquet') extend {
+ measure: flight_count is count()
+}
+>>>malloy
+#(docs) size=large
+# bar_chart
+run: flights -> {
+ group_by: carrier
+ aggregate: flight_count
+ limit: 10
+}
+>>>markdown
+
+**Properties:**
+
+| Property | Description | Example |
+|----------|-------------|---------|
+| `.stack` | Stack bars when series present | `# bar_chart.stack` |
+| `.size` | Size preset: `spark`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl` | `# bar_chart { size=lg }` |
+| `.size.width` | Pixel width | `# bar_chart { size.width=300 }` |
+| `.size.height` | Pixel height | `# bar_chart { size.height=220 }` |
+| `.x` | Field for x-axis | `# bar_chart { x=category }` |
+| `.x.limit` | Limit number of bars | `# bar_chart { x.limit=10 }` |
+| `.y` | Field(s) for y-axis | `# bar_chart { y=total }` or `{ y=['sales', 'cost'] }` |
+| `.y.independent` | Independent y-axis per nested chart | `# bar_chart { y.independent }` |
+| `.series` | Field for grouping/coloring | `# bar_chart { series=category }` |
+| `.series.limit` | Limit series shown (default 20) | `# bar_chart { series.limit=5 }` |
+| `.title` | Chart title | `# bar_chart { title='Sales' }` |
+
+#### Stacked Bar Chart
+
+Add a second dimension to create stacked bars:
+>>>malloy
+#(docs) size=large
+# bar_chart.stack
+run: flights -> {
+ group_by: origin, carrier
+ aggregate: flight_count
+ limit: 25
+}
+>>>markdown
+
+### Line Chart
+
+Renders data as a line chart. Similar inference to bar charts.
+>>>malloy
+#(docs) size=large
+# line_chart
+run: flights -> {
+ group_by: dep_time.month
+ aggregate: flight_count
+ order_by: 1
+}
+>>>markdown
+
+**Properties:**
+
+| Property | Description | Example |
+|----------|-------------|---------|
+| `.zero_baseline` | Force y-axis to start at zero | `# line_chart { zero_baseline=true }` |
+| `.interpolate` | Line interpolation mode | `# line_chart { interpolate=step }` |
+| `.series.limit` | Limit lines shown (default 12) | `# line_chart { series.limit=5 }` |
+
+#### Multi-Series Line Chart
+
+Use explicit axis tags to control which fields map to x, y, and series:
+
+```malloy
+# line_chart { series.limit=5 }
+run: flights -> {
+ group_by:
+ # x
+ departure_month is dep_time.month
+ aggregate:
+ # y
+ flight_count
+ group_by:
+ # series
+ carrier
+ order_by: departure_month
+}
+```
+
+---
+
+## Embedded Field Tags
+
+Tag fields directly to control chart axis mapping, overriding automatic inference.
+
+### `# x` - X-Axis Field
+
+Mark a dimension for the X-axis:
+>>>malloy
+#(docs) size=large
+# bar_chart
+run: flights -> {
+ # x
+ group_by: carrier
+ aggregate: flight_count
+ limit: 10
+}
+>>>markdown
+
+### `# y` - Y-Axis Field(s)
+
+Mark aggregates for the Y-axis. Multiple `# y` fields create a measure series:
+>>>malloy
+#(docs) size=large
+# bar_chart
+run: flights -> {
+ group_by: carrier
+ # y
+ aggregate: flight_count
+ # y
+ aggregate: total_distance is distance.sum() / 1000000
+ limit: 10
+}
+>>>markdown
+
+### `# series` - Series Grouping
+
+Mark a dimension for series grouping (different colors):
+>>>malloy
+#(docs) size=large
+# line_chart { series.limit=5 }
+run: flights -> {
+ # x
+ group_by: dep_time.month
+ # series
+ group_by: carrier
+ aggregate: flight_count
+ order_by: 1
+}
+>>>markdown
+
+---
+
+## Advanced Chart Features
+
+### Understanding Axis Independence
+
+By default, nested charts **share axes** when there are ≤20 unique values. This helps compare across charts.
+
+**Shared axes (default):** All nested charts use the same scale:
+>>>malloy
+#(docs) size=large
+run: flights -> {
+ group_by: origin
+ aggregate: flight_count
+ # bar_chart
+ nest: by_carrier is {
+ group_by: carrier
+ aggregate: flight_count
+ limit: 5
+ }
+ limit: 3
+}
+>>>markdown
+
+**Independent axes:** Each nested chart scales to its own data:
+>>>malloy
+#(docs) size=large
+run: flights -> {
+ group_by: origin
+ aggregate: flight_count
+ # bar_chart { y.independent }
+ nest: by_carrier is {
+ group_by: carrier
+ aggregate: flight_count
+ limit: 5
+ }
+ limit: 3
+}
+>>>markdown
+
+### Dimension Series vs Measure Series
+
+**Dimension Series:** A second dimension creates grouped or stacked bars:
+>>>malloy
+#(docs) size=large
+# bar_chart
+run: flights -> {
+ where: origin ? 'SFO' | 'LAX' | 'JFK'
+ group_by: carrier, origin
+ aggregate: flight_count
+ limit: 30
+}
+>>>markdown
+
+**Measure Series:** Multiple `# y` aggregates create series from different measures:
+>>>malloy
+#(docs) size=large
+# bar_chart
+run: flights -> {
+ group_by: carrier
+ # y
+ aggregate: flight_count
+ # y
+ aggregate: total_distance is distance.sum() / 1000000
+ limit: 10
+}
+>>>markdown
+
+### Sparklines
+
+Use `size="spark"` for compact inline charts:
+>>>malloy
+#(docs) size=large
+run: flights -> {
+ group_by: carrier
+ aggregate: flight_count
+ # bar_chart size="spark"
+ nest: trend is {
+ group_by: dep_time.month
+ aggregate: flight_count
+ order_by: 1
+ limit: 12
+ }
+ limit: 5
+}
+>>>markdown
+
+### Handling Many Lines
+
+Use `series.limit` to show only the top N series by total Y-value:
+>>>malloy
+#(docs) size=large
+# line_chart { series.limit=5 }
+run: flights -> {
+ # x
+ group_by: dep_time.month
+ # series
+ group_by: carrier
+ aggregate: flight_count
+ order_by: 1
+}
+>>>markdown
+
+---
+
+## Tooltip Interactions
+
+Tooltips can contain simple values, nested tables, or nested charts.
+
+### Simple Value Tooltips
+>>>malloy
+#(docs) size=large
+# bar_chart
+run: flights -> {
+ group_by: carrier
+ aggregate: flight_count
+ # tooltip
+ aggregate: total_distance is distance.sum()
+ limit: 10
+}
+>>>markdown
+
+### Chart Tooltips
+
+Add `# tooltip` with a chart tag to show mini-charts on hover:
+>>>malloy
+#(docs) size=large
+# bar_chart
+run: flights -> {
+ group_by: carrier
+ aggregate: flight_count
+ # tooltip bar_chart.size=xs
+ nest: by_month is {
+ group_by: dep_time.month
+ aggregate: flight_count
+ order_by: 1
+ limit: 6
+ }
+ limit: 10
+}
+>>>markdown
+
+---
+
+## Layout Tags
+
+### Dashboard
+
+Arranges nested views into a dashboard layout. Put chart tags ABOVE the `nest:` keyword:
+>>>malloy
+#(docs) size=large
+# dashboard
+run: flights -> {
+ nest: total is {
+ aggregate: flight_count
+ }
+ # bar_chart
+ nest: by_carrier is {
+ group_by: carrier
+ aggregate: flight_count
+ limit: 5
+ }
+ # line_chart
+ nest: by_month is {
+ group_by: dep_time.month
+ aggregate: flight_count
+ order_by: 1
+ }
+}
+>>>markdown
+
+**Properties:**
+
+| Property | Description | Example |
+|----------|-------------|---------|
+| `.table.max_height` | Max height for tables in tiles | `# dashboard { table.max_height=400 }` |
+| `# break` | Force new row (on nested view) | See example below |
+
+### Table
+
+Explicitly render as a table (often the default).
+
+**Properties:**
+
+| Property | Description | Example |
+|----------|-------------|---------|
+| `# pivot` | Pivot dimensions to columns | `# pivot` |
+| `# flatten` | Flatten single-row nested record | `# flatten` |
+| `.size=fill` | Fill container width | `# table.size=fill` |
+| `# column { width }` | Column width | `# column { width=lg }` |
+
+#### Pivot Syntax
+
+```malloy
+run: flights -> {
+ group_by: carrier
+ # pivot
+ nest: by_year is {
+ group_by: dep_time.year
+ aggregate: flight_count
+ }
+}
+```
+
+### List and List Detail
+
+Render nested results as comma-separated lists.
+>>>malloy
+#(docs) size=small
+run: flights -> {
+ group_by: origin
+ # list
+ nest: carriers is {
+ group_by: carrier
+ limit: 3
+ }
+ limit: 5
+}
+>>>markdown
+
+`# list_detail` shows value with detail in parentheses:
+>>>malloy
+#(docs) size=small
+run: flights -> {
+ group_by: origin
+ # list_detail
+ nest: carriers is {
+ group_by: carrier
+ aggregate: flight_count
+ limit: 3
+ }
+ limit: 5
+}
+>>>markdown
+
+### Transpose
+
+Swap rows and columns. Useful for single-row results with many measures.
+
+```malloy
+# transpose
+run: flights -> {
+ aggregate:
+ flight_count
+ total_distance is distance.sum()
+ avg_distance is distance.avg()
+}
+```
+
+---
+
+## Field Formatting Tags
+
+### Currency
+>>>malloy
+#(docs) size=small
+run: duckdb.table('../data/order_items.parquet') -> {
+ aggregate:
+ # currency
+ total_sales is sale_price.sum()
+}
+>>>markdown
+
+#### Currency Scaling
+
+Scale large currency values for readability:
+>>>malloy
+#(docs) size=small
+run: duckdb.table('../data/order_items.parquet') -> {
+ aggregate:
+ # currency { scale=thousands }
+ sales_k is sale_price.sum()
+ # currency { scale=millions decimals=2 }
+ sales_m is sale_price.sum()
+ # currency { scale=thousands no_suffix }
+ sales_raw is sale_price.sum()
+}
+>>>markdown
+
+| Property | Description |
+|----------|-------------|
+| `scale=thousands` | Divide by 1000, add "K" suffix |
+| `scale=millions` | Divide by 1M, add "M" suffix |
+| `decimals=2` | Control decimal places |
+| `no_suffix` | Show scaled number without suffix |
+
+### Percent
+>>>malloy
+#(docs) size=small
+run: flights -> {
+ group_by: carrier
+ aggregate:
+ flight_count
+ # percent
+ percent_of_total is flight_count / all(flight_count)
+ limit: 5
+}
+>>>markdown
+
+### Number Format
+
+Use `ssf` format strings (Excel/Sheets style):
+>>>malloy
+#(docs) size=small
+run: flights -> {
+ aggregate:
+ # number="#,##0"
+ total_flights is count()
+}
+>>>markdown
+
+### Duration
+
+Format numbers as human-readable durations:
+
+| Property | Description |
+|----------|-------------|
+| `# duration` | Default (seconds) |
+| `# duration=milliseconds` | Input is milliseconds |
+| `# duration.terse` | Abbreviated (s, m, h, d) |
+
+### Image
+
+Render string as image:
+
+```malloy
+dimension: product_image is image_url # image { height=50px }
+```
+
+### Link
+
+Render as hyperlink:
+
+```malloy
+dimension: product_page is name # link { url_template="https://example.com/$$" }
+```
+
+---
+
+## Utility Tags
+
+| Tag | Description | Example |
+|-----|-------------|---------|
+| `# hidden` | Hide field from display | `dimension: id # hidden` |
+| `# label` | Override display name | `measure: total # label="Total Sales"` |
+| `# tooltip` | Include in chart tooltip | `nest: details is { # tooltip ... }` |
+
+### Label Limitations
+
+`# label` overrides display names for:
+- Table column headers ✓
+- Dashboard card titles ✓
+
+**Note:** `# label` does **not** work for chart axis labels. For chart titles, use the `.title` and `.subtitle` properties:
+>>>malloy
+#(docs) size=large
+# bar_chart { title='Custom Title' subtitle='With subtitle' }
+run: flights -> {
+ group_by: carrier
+ aggregate: flight_count
+ limit: 5
+}
+>>>markdown
+
+---
+
+## Theme Configuration
+
+Apply theme overrides at model level (`##`) or view level (`#`):
+
+```malloy
+## theme { tableBodyColor=blue }
+
+source: my_source is ... extend {
+ view: styled is {
+ # theme { tableRowHeight=40 }
+ select: *
+ }
+}
+```
+
+---
+
+## Legacy Renderer
+
+Use the older HTML renderer for an entire model:
+
+```malloy
+## renderer_legacy
+
+source: my_source is ...
+```
diff --git a/src/table_of_contents.json b/src/table_of_contents.json
index 943bdb04..771a130f 100644
--- a/src/table_of_contents.json
+++ b/src/table_of_contents.json
@@ -291,6 +291,18 @@
{
"title": "Overview",
"link": "/visualizations/about_rendering.malloynb"
+ },
+ {
+ "title": "Renderer Tags",
+ "link": "/visualizations/renderer_tags.malloynb"
+ },
+ {
+ "title": "Tag Cheatsheet",
+ "link": "/visualizations/renderer_cheatsheet.malloynb"
+ },
+ {
+ "title": "Custom Plugins",
+ "link": "/visualizations/plugins.malloynb"
}
]
},