@malloydata/render 0.0.423 → 0.0.425

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,916 @@
1
+ # Malloy Renderer Tag Documentation
2
+
3
+ This document outlines the tags used to control visualizations generated by the custom Malloy renderer. Add these tags as annotations to your Malloy queries (`.malloy` files) to influence how the resulting data is displayed.
4
+
5
+ **Basic Syntax:**
6
+
7
+ - Tags are prefixed with `#` followed by a space: `# tag_name` (e.g., `# currency`)
8
+ - Tags with properties: `# tag_name { property1=value property2 ... }` (e.g., `# bar_chart { size=lg stack }`)
9
+ - Boolean properties can often be added using dot notation: `# tag_name.property` (e.g., `# bar_chart.stack`)
10
+ - Nested properties can target specific aspects: `# bar_chart { y.independent }`
11
+
12
+ ---
13
+
14
+ ## Chart Tags
15
+
16
+ These tags render the entire result set as a specific chart type.
17
+
18
+ ### `# bar_chart`
19
+
20
+ Renders the data as a bar chart. If `x`, `y`, and `series` fields aren't explicitly specified via tags, the renderer attempts to infer them from the query structure (typically first dimension for x, first measure for y, second dimension for series).
21
+
22
+ **Properties:**
23
+
24
+ - `.stack`: Stacks bars when a series is present.
25
+ - Syntax: `# bar_chart.stack` or `# bar_chart { stack }`
26
+ - `.size`: Controls chart size presets.
27
+ - Values: `spark`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`.
28
+ - Syntax: `# bar_chart { size=lg }` or `# bar_chart.size=lg` (Legacy: `# size=lg` on the view)
29
+ - `.size.width`: Sets a specific pixel width for the chart plot area.
30
+ - Syntax: `# bar_chart { size.width=300 }`
31
+ - `.size.height`: Sets a specific pixel height for the chart plot area.
32
+ - Syntax: `# bar_chart { size.height=220 }`
33
+ - `.x`: Specifies the field for the x-axis (category axis).
34
+ - Syntax: `# bar_chart { x=field_name }`
35
+ - `.x.limit`: Limits the number of bars shown on the x-axis. Automatically calculated based on chart width if not specified.
36
+ - Syntax: `# bar_chart { x.limit=10 }`
37
+ - `.x.independent`: Controls whether the x-axis scale is shared across nested charts. See "Automatic Axis/Legend Sharing" below.
38
+ - Syntax: `# bar_chart { x.independent }` or `# bar_chart { x.independent=false }`
39
+ - `.y`: Specifies the field(s) for the y-axis (value axis). Can be a single field name or an array for measure-based series.
40
+ - Syntax: `# bar_chart { y=measure_name }` or `# bar_chart { y=['measure1', 'measure2'] }`
41
+ - `.y.independent`: Controls whether the y-axis scale is shared across nested charts (default is shared).
42
+ - Syntax: `# bar_chart { y.independent }`
43
+ - `.series`: Specifies the field used for grouping/coloring bars.
44
+ - Syntax: `# bar_chart { series=dimension_name }`
45
+ - `.series.limit`: Limits the number of series shown (default `auto`, which uses 20 for bar charts). When limiting is active, series are ranked by their total sum of Y-values and the top N series are shown.
46
+ - Syntax: `# bar_chart { series.limit=5 }` or `# bar_chart { series.limit=auto }`
47
+ - `.series.independent`: Controls whether the series legend/colors are shared across nested charts. See "Automatic Axis/Legend Sharing" below.
48
+ - Syntax: `# bar_chart { series.independent }` or `# bar_chart { series.independent=false }`
49
+ - `.title`: Sets the main title for the chart.
50
+ - Syntax: `# bar_chart { title='My Chart Title' }`
51
+ - `.subtitle`: Sets a subtitle displayed below the title.
52
+ - Syntax: `# bar_chart { subtitle='Data for Q1' }`
53
+
54
+ **Automatic Axis/Legend Sharing (Bar Chart):**
55
+
56
+ - **X-Axis:** By default, the x-axis domain (the set of categories shown) is shared across nested charts _if_ the number of distinct x-values is small (currently <= 20). This keeps alignment consistent for comparable small multiples. If there are more than 20 distinct values, each nested chart gets its own independent x-axis domain.
57
+ - **Series Legend/Colors:** Similarly, the series domain (legend items and colors) is shared across nested charts if the number of distinct series values is small (currently <= 20).
58
+ - **Overriding:** You can force sharing by using `.independent=false` (e.g., `# bar_chart { x.independent=false }`) or force independence by using `.independent` or `.independent=true` (e.g., `# bar_chart { series.independent }`).
59
+ - **Y-Axis:** The y-axis is shared by default unless explicitly set via `# bar_chart { y.independent }`.
60
+
61
+ **Examples:**
62
+
63
+ ```
64
+ // Simple bar chart, axes inferred
65
+ # bar_chart
66
+ view: my_bars is {
67
+ group_by: category
68
+ aggregate: total_sales
69
+ limit: 10
70
+ }
71
+
72
+ // Stacked bar chart with explicit axes and size
73
+ # bar_chart { stack x=sale_month series=category y=total_sales size=lg }
74
+ view: sales_by_month is { ... }
75
+
76
+ // Measure-based series
77
+ # bar_chart { x=brand y=['Sales $', 'Cost $'] }
78
+ view: sales_vs_cost is { ... }
79
+
80
+ // Independent Y-axis in a nested chart
81
+ view: parent_view is {
82
+ group_by: region
83
+ nest: by_brand is {
84
+ # bar_chart { y.independent title='Sales by Brand' }
85
+ group_by: brand
86
+ aggregate: total_sales
87
+ limit: 5
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### `# line_chart`
93
+
94
+ Renders the data as a line chart. Inference for `x`, `y`, and `series` is similar to `# bar_chart`.
95
+
96
+ **Properties:**
97
+
98
+ - `.zero_baseline`: Controls if the y-axis must include zero. Default is `false` for line charts (unlike bar charts which always include zero). Set to `true` to force the y-axis to start at zero.
99
+ - Syntax: `# line_chart.zero_baseline=true` or `# line_chart { zero_baseline=true }`
100
+ - `.size`: Controls chart size presets (e.g., `spark`).
101
+ - Syntax: `# line_chart { size=spark }` (Legacy: `# size=spark` on the view)
102
+ - `.interpolate`: Sets the line interpolation mode.
103
+ - Values: e.g., `step`.
104
+ - Syntax: `# line_chart { interpolate=step }`
105
+ - `.x`, `.y`, `.series`, `.title`, `.subtitle`: Similar to `# bar_chart` properties.
106
+ - `.y.independent`: Controls y-axis independence across nested charts. Also automatically set if `series.limit` is applied.
107
+ - `.series.limit`: Limits the number of lines (series) shown (default `auto`, which uses 12 for line charts). When limiting is active, series are ranked by their total sum of Y-values and the top N series are shown. Setting this automatically enables `y.independent=true`.
108
+ - `.series.independent`: Controls legend/color sharing across nested charts. See "Automatic Axis/Legend Sharing" below.
109
+
110
+ **Automatic Axis/Legend Sharing (Line Chart):**
111
+
112
+ - **X-Axis:** Similar to bar charts, the x-axis domain is shared by default if distinct x-values <= 20, otherwise independent.
113
+ - **Series Legend/Colors:** Shared by default if distinct series values <= 20, otherwise independent.
114
+ - **Overriding:** Use `.independent=false` to force sharing or `.independent` / `.independent=true` to force independence (e.g., `# line_chart { x.independent }`, `# line_chart { series.independent=false }`).
115
+ - **Y-Axis:** Shared by default unless explicitly set via `# line_chart { y.independent }` or if `.series.limit` is used.
116
+
117
+ **Examples:**
118
+
119
+ ```
120
+ // Basic line chart
121
+ # line_chart
122
+ view: sales_trend is {
123
+ group_by: sale_date.month
124
+ aggregate: total_sales
125
+ }
126
+
127
+ // Line chart with y-axis not starting at zero
128
+ # line_chart { zero_baseline=false }
129
+ view: stock_price is { ... }
130
+
131
+ // Multi-series line chart with limited series (implies y.independent)
132
+ # line_chart { series.limit=5 }
133
+ view: multi_brand_trend is {
134
+ group_by: sale_date.day, brand
135
+ aggregate: daily_sales
136
+ }
137
+ ```
138
+
139
+ ### `# scatter_chart`
140
+
141
+ _Note: This currently uses the legacy renderer._
142
+ Renders the data as a scatter plot. Configuration relies mainly on field order (x, y, color, size, shape).
143
+
144
+ **Properties:**
145
+
146
+ - _(No specific nested properties identified in the current codebase examples for configuration via the tag itself, likely relies on field order/type)_
147
+
148
+ **Example:**
149
+
150
+ ```
151
+ # scatter_chart
152
+ view: cost_vs_price is {
153
+ group_by: retail_price, cost, brand // x, y, color/shape inferred
154
+ }
155
+ ```
156
+
157
+ ### `# big_value`
158
+
159
+ Renders aggregate values as prominent metric cards, similar to KPI tiles in dashboards. This is ideal for displaying key metrics at a glance.
160
+
161
+ **Important:** Only works with aggregate fields (measures). Does not support `group_by` dimensions.
162
+
163
+ **Properties:**
164
+
165
+ - `.size`: Controls card size preset.
166
+ - Values: `sm`, `md` (default), `lg`
167
+ - Syntax: `# big_value { size=lg }`
168
+ - `.neutral_threshold`: Percentage threshold below which changes are shown as neutral (dash) instead of up/down arrows. Default is `0.05` (5%).
169
+ - Syntax: `# big_value { neutral_threshold=0.1 }`
170
+
171
+ ---
172
+
173
+ #### Feature 1: Comparison Delta Indicators
174
+
175
+ Show change vs a baseline value with colored up/down arrows (▲ green / ▼ red).
176
+
177
+ **Field-level Properties (for comparison fields):**
178
+
179
+ Add these properties to the comparison field (not the primary metric):
180
+
181
+ - `.comparison_field`: The field name of the primary metric this value compares to.
182
+ - Syntax: `# big_value { comparison_field=current_sales }`
183
+ - `.comparison_label`: Optional label shown next to the delta indicator.
184
+ - Syntax: `# big_value { comparison_label='vs Prior Year' }`
185
+ - `.comparison_format`: How to calculate and display the change.
186
+ - Values: `pct` (percentage change, default) or `ppt` (percentage point difference)
187
+ - Syntax: `# big_value { comparison_format=ppt }`
188
+ - `.down_is_good`: Set to `true` if a decrease should be shown as positive (green). Useful for cost or delay metrics.
189
+ - Syntax: `# big_value { down_is_good=true }`
190
+
191
+ **Example:**
192
+
193
+ ```malloy
194
+ # big_value
195
+ view: sales_comparison is {
196
+ aggregate:
197
+ # label="Current Sales"
198
+ # currency
199
+ current_sales is revenue.sum()
200
+
201
+ // Shows: ▲ 8.7% vs Prior Year (green)
202
+ # big_value { comparison_field=current_sales comparison_label='vs Prior Year' }
203
+ # hidden
204
+ prior_sales is revenue.sum() * 0.92
205
+ }
206
+ ```
207
+
208
+ ---
209
+
210
+ #### Feature 2: Documentation Tooltips
211
+
212
+ Add info icons with hover descriptions using `# description` tags.
213
+
214
+ **Example:**
215
+
216
+ ```malloy
217
+ # big_value
218
+ view: documented_metrics is {
219
+ aggregate:
220
+ # description="Total revenue from completed sales. Updated daily."
221
+ # label="Total Revenue"
222
+ # currency
223
+ total_revenue is sales.sum()
224
+
225
+ # description="Win rate = Won / (Won + Lost). Industry average is 25%."
226
+ # label="Win Rate"
227
+ # percent
228
+ win_rate is won.sum() / (won.sum() + lost.sum())
229
+ }
230
+ ```
231
+
232
+ ---
233
+
234
+ #### Feature 3: Sparkline Trends
235
+
236
+ Show mini line or bar charts alongside the metric value.
237
+
238
+ **How it works:**
239
+
240
+ 1. Add `sparkline=nest_name` to the metric field to reference a sparkline nest
241
+ 2. Define the sparkline data as a nested view with `# line_chart { size=spark }` or `# bar_chart { size=spark }`
242
+ 3. Mark the nest as `# hidden` to prevent it from rendering separately
243
+
244
+ **Metric Field Properties:**
245
+
246
+ - `.sparkline`: The name of the nested view containing sparkline data.
247
+ - Syntax: `# big_value { sparkline=trend }`
248
+
249
+ **Example:**
250
+
251
+ ```malloy
252
+ # big_value
253
+ view: sales_with_trend is {
254
+ aggregate:
255
+ # label="Total Sales"
256
+ # currency
257
+ # big_value { sparkline=trend }
258
+ total_sales is revenue.sum()
259
+
260
+ // Line sparkline showing trend over time
261
+ # line_chart { size=spark }
262
+ # hidden
263
+ nest: trend is {
264
+ group_by: order_date.month
265
+ aggregate: monthly_sales is revenue.sum()
266
+ order_by: order_date.month
267
+ limit: 12
268
+ }
269
+ }
270
+
271
+ // Bar sparkline
272
+ # big_value
273
+ view: with_bar_sparkline is {
274
+ aggregate:
275
+ # label="Order Count"
276
+ # big_value { sparkline=order_trend }
277
+ order_count is count()
278
+
279
+ # bar_chart { size=spark }
280
+ # hidden
281
+ nest: order_trend is {
282
+ group_by: region
283
+ aggregate: orders is count()
284
+ limit: 8
285
+ }
286
+ }
287
+ ```
288
+
289
+ ---
290
+
291
+ #### Formatting
292
+
293
+ Big Value cards respect field formatting tags:
294
+ - `# currency` - Format as currency (e.g., $1,234.56)
295
+ - `# currency=euro` - Format as euros (€)
296
+ - `# currency=pound` - Format as pounds (£)
297
+ - `# percent` - Format as percentage
298
+ - `# number="#,##0.0"` - Custom number format
299
+ - `# number=big` - Abbreviate large numbers (1.2M, 3.5B)
300
+
301
+ ---
302
+
303
+ #### Complete Example (All Features)
304
+
305
+ ```malloy
306
+ # big_value
307
+ view: executive_dashboard is {
308
+ aggregate:
309
+ # description="Total revenue this period. Target is $10M."
310
+ # label="Revenue"
311
+ # currency
312
+ # big_value { sparkline=revenue_trend }
313
+ revenue is sales.sum()
314
+
315
+ # big_value { comparison_field=revenue comparison_label='vs Target' }
316
+ # hidden
317
+ target_revenue is 10000000
318
+
319
+ # description="On-time delivery rate. SLA requires 95%."
320
+ # label="On-Time Rate"
321
+ # percent
322
+ ontime_rate is count() { where: delivered_on_time } / count()
323
+
324
+ # big_value { comparison_field=ontime_rate comparison_label='vs SLA' comparison_format=ppt }
325
+ # hidden
326
+ sla_target is 0.95
327
+
328
+ // Sparkline showing revenue trend
329
+ # line_chart { size=spark }
330
+ # hidden
331
+ nest: revenue_trend is {
332
+ group_by: order_date.month
333
+ aggregate: monthly_revenue is sales.sum()
334
+ order_by: order_date.month
335
+ limit: 12
336
+ }
337
+ }
338
+ ```
339
+
340
+ ---
341
+
342
+ ## Data Limiting and Performance
343
+
344
+ Both bar charts and line charts implement automatic data limiting to ensure good performance and readability. Understanding these limits helps you configure charts effectively.
345
+
346
+ ### Series Limiting
347
+
348
+ **Default Limits:**
349
+ - **Line Charts:** 12 series (when `series.limit=auto`)
350
+ - **Bar Charts:** 20 series (when `series.limit=auto`)
351
+
352
+ **How Series Are Selected:**
353
+ When the number of unique series values exceeds the limit, the renderer:
354
+ 1. Calculates the total sum of Y-values for each series
355
+ 2. Ranks series by their total sum (highest to lowest)
356
+ 3. Shows only the top N series based on the limit
357
+ 4. Displays a message like "Showing 12 of 45 series"
358
+
359
+ **Automatic Y-Axis Independence:**
360
+ When series limiting is active, the renderer automatically sets `y.independent=true` to prevent misleading comparisons between charts that may be showing different subsets of series.
361
+
362
+ ### Data Point Limiting
363
+
364
+ **Line Charts:**
365
+ - Maximum of 5,000 data points per chart
366
+ - When exceeded, shows message indicating data is limited
367
+ - Affects the total number of plotted points after series filtering
368
+
369
+ **Bar Charts:**
370
+ - X-axis limiting based on available visual space
371
+ - Automatically calculates how many bars can fit given the chart width and bar grouping
372
+ - Can be overridden with `x.limit=N`
373
+
374
+ ### Visual Cues for Limited Data
375
+
376
+ When data is limited, charts display informational messages:
377
+ - **Series limiting:** "Showing 12 of 45 series"
378
+ - **Data point limiting:** Indicates when data has been truncated
379
+
380
+ ### Nested Chart Behavior
381
+
382
+ **Shared vs Independent Domains:**
383
+ - **Root/Global charts:** Use the globally calculated top N series
384
+ - **Nested charts:** May calculate their own local limits unless sharing is forced
385
+ - **Automatic sharing:** Domains are shared when ≤20 distinct values, otherwise independent
386
+
387
+ **Override Examples:**
388
+ ```malloy
389
+ // Force showing all series (no limit)
390
+ # line_chart { series.limit=1000 }
391
+
392
+ // Use only top 5 series
393
+ # bar_chart { series.limit=5 }
394
+
395
+ // Force Y-axis sharing even with series limiting
396
+ # line_chart { series.limit=8 y.independent=false }
397
+
398
+ // Override bar chart X-axis limiting
399
+ # bar_chart { x.limit=15 }
400
+ ```
401
+
402
+ ---
403
+
404
+ ## Layout & Structure Tags
405
+
406
+ ### `# dashboard`
407
+
408
+ Arranges multiple nested views into a dashboard layout. Each nested view is rendered in its own tile.
409
+
410
+ The dashboard has two layout modes. Flex mode is the default: tiles flow into a row and wrap onto a new row when it fills. Columns mode is entered with `columns=N`: tiles are placed into N equal-width columns.
411
+
412
+ **Properties:**
413
+
414
+ - `.table.max_height`: Sets a maximum pixel height for tables rendered within dashboard tiles. Useful for controlling layout with large tables. Set to `'none'` to disable max height.
415
+ - Syntax: `# dashboard { table.max_height=400 }` or `# dashboard { table.max_height=none }`
416
+ - `.columns` (integer): Enters columns mode with N equal-width columns. A tile occupies one column by default and wraps onto a new row when a row fills. Use `# colspan` to make a tile occupy more than one column.
417
+ - Syntax: `# dashboard { columns=3 }`
418
+ - `.gap` (pixels): Sets the spacing between tiles, in both flex mode and columns mode. Default is 16.
419
+ - Syntax: `# dashboard { gap=24 }`
420
+
421
+ **Per-tile tags (applied to a nested view):**
422
+
423
+ - `# colspan` (integer): In columns mode, makes the tile occupy that many columns (one by default). When a row's tiles total more than the column count, the overflow wraps onto the next row. Ignored in flex mode.
424
+ - `# break`: Starts a new row. Tiles after a `# break` begin a fresh row. Works in both flex mode and columns mode.
425
+
426
+ **Example:**
427
+
428
+ ```
429
+ # dashboard
430
+ view: sales_overview is {
431
+ nest: kpis is { select: total_sales, avg_margin } // Renders as text/single value
432
+
433
+ # break
434
+ nest: top_products_table is { // Renders as table
435
+ group_by: product
436
+ aggregate: total_sales
437
+ limit: 5
438
+ }
439
+
440
+ nest: sales_by_category_chart is { // Renders as bar chart
441
+ # bar_chart
442
+ group_by: category
443
+ aggregate: total_sales
444
+ }
445
+ }
446
+ ```
447
+
448
+ ### `# table`
449
+
450
+ Explicitly renders data as a table. This is often the default for nested results if no other renderer tag is specified.
451
+
452
+ **Properties:**
453
+
454
+ - `.flatten`: Flattens a _single-row_ nested record's fields into the parent table as columns. The nested query should not have `group_by`.
455
+ - Syntax: `# flatten` (applied to the `nest:` definition)
456
+ - `.pivot`: Transforms a nested query into horizontal columns where dimension values become column headers
457
+ - Dimensions (fields in `group_by`) become column headers
458
+ - Measures (fields in `aggregate`) become cell values
459
+ - Column headers display as "DimensionValue: measure_name"
460
+ - Maximum 30 pivot columns.
461
+ - Syntax: `# pivot` (applied to a `nest:` or a query)
462
+ - `.dimensions`: Specifies which fields to use as pivot dimensions. If not specified, all `group_by` fields are used.
463
+ - Syntax: `# pivot { dimensions=[field1, field2] }`
464
+ - `.transpose`: Rotates a table so rows become columns and columns become rows. Original column names become row headers, original row values become column headers. Default limit of 20 transpose columns.
465
+ - Syntax: `# transpose` (applied to the view)
466
+ - `.limit`: Override the default column limit. Syntax: `# transpose.limit=100`
467
+ - `.size=fill`: Makes the table attempt to fill the width of its container.
468
+ - Syntax: `# table.size=fill`
469
+ - `# column` (applied to a specific field within the view): Controls individual column appearance.
470
+ - `.width`: Sets column width. Accepts named sizes (`xs`, `sm`, `md`, `lg`, `xl`, `2xl`) or pixel values.
471
+ - Syntax: `# column { width=lg }` or `# column { width=150 }`
472
+ - `.height`: Sets a specific row height for cells in this column (in pixels).
473
+ - Syntax: `# column { height=40 }`
474
+ - `.word_break=break_all`: Allows long words/strings without spaces to break and wrap within the cell.
475
+ - Syntax: `# column { word_break=break_all }`
476
+
477
+ **Examples:**
478
+
479
+ ```malloy
480
+ // Column width and flatten
481
+ view: detailed_table is {
482
+ group_by:
483
+ category
484
+ # column { width=lg }
485
+ brand
486
+ aggregate: total_sales
487
+
488
+ # flatten
489
+ nest: metrics is {
490
+ aggregate: avg_price, total_cost
491
+ }
492
+ }
493
+
494
+ // Pivot - dimensions auto-detected from group_by
495
+ view: sales_by_category is {
496
+ group_by: category
497
+ aggregate: total_sales is sales.sum()
498
+
499
+ # pivot
500
+ nest: by_department is {
501
+ group_by: department
502
+ aggregate:
503
+ avg_price is price.avg()
504
+ total_units is units.sum()
505
+ }
506
+ }
507
+ // Creates columns: Men: avg_price | Men: total_units | Women: avg_price | Women: total_units
508
+
509
+ // Pivot with explicit dimensions
510
+ view: regional_sales is {
511
+ group_by: year
512
+ # pivot { dimensions=[region] }
513
+ nest: by_region is {
514
+ group_by: region
515
+ aggregate:
516
+ total_sales is sales.sum()
517
+ avg_margin is margin.avg()
518
+ }
519
+ }
520
+
521
+ // Transpose - swap rows and columns
522
+ # transpose
523
+ view: metrics_summary is {
524
+ group_by: category
525
+ aggregate:
526
+ avg_price is price.avg()
527
+ total_sales is sales.sum()
528
+ product_count is count()
529
+ }
530
+ // Instead of categories as rows, displays as:
531
+ // | | Jeans | Outerwear |
532
+ // |---------------|--------|-----------|
533
+ // | avg_price | 97.41 | 145.37 |
534
+ // | total_sales | 104,776| 92,269 |
535
+ // | product_count | 1,024 | 856 |
536
+ ```
537
+
538
+ ### `# list`
539
+
540
+ Renders the first non-hidden field of a nested result as a comma-separated list.
541
+
542
+ **Example:**
543
+
544
+ ```
545
+ view: product_summary is {
546
+ group_by: category
547
+ # list
548
+ nest: top_brands is {
549
+ group_by: brand
550
+ aggregate: total_sales
551
+ order_by: 2 desc
552
+ limit: 3
553
+ }
554
+ }
555
+ // Output might look like: Clothing: Brand A, Brand B, Brand C
556
+ ```
557
+
558
+ ### `# list_detail`
559
+
560
+ Similar to `# list`, but uses the first two non-hidden fields to render items as `value (detail)`.
561
+
562
+ **Example:**
563
+
564
+ ```
565
+ view: brand_counts is {
566
+ group_by: category
567
+ # list_detail
568
+ nest: brand_details is {
569
+ group_by: brand
570
+ aggregate: num_products is count()
571
+ order_by: 2 desc
572
+ limit: 3
573
+ }
574
+ }
575
+ // Output might look like: Clothing: Brand A (15), Brand B (12), Brand C (10)
576
+ ```
577
+
578
+ ---
579
+
580
+ ## Field-Level Formatting & Rendering Tags
581
+
582
+ These tags are typically applied directly to individual fields within a query.
583
+
584
+ ### `# currency`
585
+
586
+ Formats a numeric field as currency. Supports both **shorthand** and **verbose** syntax for scaling and formatting options.
587
+
588
+ #### Shorthand Syntax (Recommended)
589
+
590
+ Pattern: `# currency={code}{decimals}{scale}`
591
+
592
+ - `{code}`: Currency code (`usd`, `eur`, `gbp`)
593
+ - `{decimals}`: Number of decimal places (0-4)
594
+ - `{scale}`: Scale letter (`k`, `m`, `b`, `t`, `q`) - case insensitive
595
+
596
+ | Tag | Output | Description |
597
+ |-----|--------|-------------|
598
+ | `# currency=usd` | $412 | USD, default decimals |
599
+ | `# currency=usd0` | $412 | 0 decimals |
600
+ | `# currency=usd2` | $412.12 | 2 decimals |
601
+ | `# currency=usd0k` | $412k | thousands, 0 decimals |
602
+ | `# currency=usd1m` | $412.1m | millions, 1 decimal |
603
+ | `# currency=usd0b` | $1b | billions, 0 decimals |
604
+ | `# currency=eur2m` | €412.12m | Euro, millions, 2 decimals |
605
+ | `# currency=gbp1b` | £1.4b | Pound, billions, 1 decimal |
606
+
607
+ #### Verbose Syntax (Advanced Options)
608
+
609
+ For suffix format control, use verbose syntax:
610
+
611
+ ```malloy
612
+ # currency { scale=m decimals=2 suffix=finance } // $412.12MM
613
+ # currency { scale=k suffix=none } // $412 (no suffix)
614
+ # currency { scale=auto } // auto-scale like number=big
615
+ ```
616
+
617
+ **Scale Options:** `k` (thousands), `m` (millions), `b` (billions), `t` (trillions), `q` (quadrillions), `auto`
618
+
619
+ **Suffix Format Options:**
620
+
621
+ | Format | k | m | b | t | q |
622
+ |--------|---|---|---|---|---|
623
+ | `letter` | K | M | B | T | Q |
624
+ | `lower` (shorthand default) | k | m | b | t | q |
625
+ | `word` | Thousand | Million | Billion | Trillion | Quadrillion |
626
+ | `short` | K | Mil | Bil | Tril | Quad |
627
+ | `finance` | M | MM | B | T | Q |
628
+ | `scientific` | ×10³ | ×10⁶ | ×10⁹ | ×10¹² | ×10¹⁵ |
629
+ | `none` | (empty) | (empty) | (empty) | (empty) | (empty) |
630
+
631
+ **Example:**
632
+
633
+ ```malloy
634
+ source: financials extend {
635
+ # currency=usd
636
+ measure: total_revenue is sales.sum()
637
+
638
+ # currency=usd0m
639
+ measure: revenue_millions is sales.sum()
640
+
641
+ # currency=eur2k
642
+ measure: euro_thousands is eu_sales.sum()
643
+
644
+ # currency { scale=m suffix=finance decimals=2 }
645
+ measure: revenue_finance is sales.sum() // $42.54MM
646
+ }
647
+ ```
648
+
649
+ ### `# percent`
650
+
651
+ Formats a numeric field as a percentage (value \* 100, adds %).
652
+
653
+ **Example:**
654
+
655
+ ```
656
+ measure: margin_pct is (revenue - cost) / revenue # percent
657
+ ```
658
+
659
+ ### `# number`
660
+
661
+ Formats a numeric field. Supports **shorthand** syntax for scaling, **verbose** syntax for advanced options, and **ssf format strings** for custom formatting.
662
+
663
+ #### Shorthand Syntax (Recommended)
664
+
665
+ Pattern: `# number={decimals}{scale}` or `# number=auto`
666
+
667
+ - `{decimals}`: Number of decimal places (0-4)
668
+ - `{scale}`: Scale letter (`k`, `m`, `b`, `t`, `q`) - case insensitive
669
+
670
+ | Tag | Output | Description |
671
+ |-----|--------|-------------|
672
+ | `# number=0` | 11 | 0 decimals |
673
+ | `# number=1` | 11.2 | 1 decimal |
674
+ | `# number=2` | 11.23 | 2 decimals |
675
+ | `# number=0k` | 64k | thousands, 0 decimals |
676
+ | `# number=1k` | 64.2k | thousands, 1 decimal |
677
+ | `# number=0m` | 43m | millions, 0 decimals |
678
+ | `# number=1m` | 42.5m | millions, 1 decimal |
679
+ | `# number=0b` | 1b | billions, 0 decimals |
680
+ | `# number=1t` | 1.4t | trillions, 1 decimal |
681
+ | `# number=auto` | 1.2m | auto-scale (same as `number=big`) |
682
+ | `# number=id` | 123456789 | identifier format (no commas) |
683
+
684
+ #### Verbose Syntax (Advanced Options)
685
+
686
+ For suffix format control, use verbose syntax:
687
+
688
+ ```malloy
689
+ # number { scale=m decimals=2 suffix=word } // 42.54 Million
690
+ # number { scale=auto suffix=letter } // 1.2M (uppercase)
691
+ # number { scale=k suffix=none } // 64 (no suffix)
692
+ ```
693
+
694
+ **Scale Options:** `k` (thousands), `m` (millions), `b` (billions), `t` (trillions), `q` (quadrillions), `auto`
695
+
696
+ **Suffix Format Options:** Same as currency (see above)
697
+
698
+ #### SSF Format String
699
+
700
+ For custom formatting, use an ssf format string:
701
+
702
+ ```malloy
703
+ # number="#,##0.00" // 1,234.56
704
+ # number="0.0%" // percent format
705
+ ```
706
+
707
+ **Example:**
708
+
709
+ ```malloy
710
+ source: metrics extend {
711
+ # number=1m
712
+ measure: users_millions is users.count()
713
+
714
+ # number=auto
715
+ measure: auto_scaled is amount.sum()
716
+
717
+ # number { scale=m suffix=word }
718
+ measure: word_format is amount.sum() // 1.2 Million
719
+
720
+ # number=id
721
+ measure: order_id is id.max() // 123456789 (no commas)
722
+ }
723
+ ```
724
+
725
+ ### `# duration`
726
+
727
+ Formats a numeric field representing a duration into a human-readable string (e.g., "1h 2m", "30s").
728
+
729
+ **Properties:**
730
+
731
+ - `. [unit]`: Specifies the unit of the input number if not seconds (e.g., `nanoseconds`, `milliseconds`, `minutes`, `hours`, `days`). Default is `seconds`.
732
+ - Syntax: `# duration=milliseconds`
733
+ - `.terse`\*\*: Uses abbreviated units (ns, µs, ms, s, m, h, d).
734
+ - Syntax: `# duration.terse` or `# duration { terse }`
735
+ - `.number`: Apply an `ssf` format string to the numeric parts of the duration.
736
+ - Syntax: `# duration { number="0.0" }`
737
+
738
+ **Example:**
739
+
740
+ ```
741
+ measure: avg_session_time is session_length.avg() # duration=seconds
742
+ measure: total_compute_time is compute_nanos.sum() # duration=nanoseconds terse
743
+ ```
744
+
745
+ ### `# image`
746
+
747
+ Renders a string field value as an image (`<img>` tag). Assumes the field contains a valid image URL.
748
+
749
+ **Properties:**
750
+
751
+ - `.height`: Sets the image height (e.g., `40px`).
752
+ - Syntax: `# image { height=40px }`
753
+ - `.width`: Sets the image width.
754
+ - Syntax: `# image { width=100px }`
755
+ - `.alt`: Specifies alt text for the image.
756
+ - `.alt.[text]`: Sets literal alt text.
757
+ - Syntax: `# image { alt=Logo }`
758
+ - `.alt.field`: Uses the value of another field (specified by relative path like `brand_name` or `'../parent_field'`) for the alt text.
759
+ - Syntax: `# image { alt.field=brand_name }`
760
+
761
+ **Example:**
762
+
763
+ ```
764
+ dimension: product_image is image_url # image { height=50px alt.field=product_name }
765
+ ```
766
+
767
+ ### `# link`
768
+
769
+ Renders a field value as a hyperlink (`<a>` tag).
770
+
771
+ **Properties:**
772
+
773
+ - `.url_template`: A template string where `$$` is replaced by the value (from this field or the `.field` property) to form the `href`. If `$$` isn't present, the value is appended.
774
+ - Syntax: `# link { url_template="https://example.com/products/$$" }`
775
+ - `.field`: Use the value from a _different_ field (specified by relative path) to substitute into `url_template` or use as the `href`. The original field's value is still used as the link text.
776
+ - Syntax: `# link { url_template="https://.../$$" field=product_id }`
777
+
778
+ **Example:**
779
+
780
+ ```
781
+ dimension: product_page is product_name # link { url_template="[https://example.com/products/$$](https://example.com/products/$$)" field=product_id }
782
+ dimension: product_id is ...
783
+ ```
784
+
785
+ ---
786
+
787
+ ## Utility Tags
788
+
789
+ ### `# hidden`
790
+
791
+ Hides a field from being rendered in tables or dashboards, though it remains in the data.
792
+
793
+ **Example:**
794
+
795
+ ```
796
+ # hidden
797
+ dimension: internal_id is id
798
+ ```
799
+
800
+ ### `# label`
801
+
802
+ Overrides the default display name (label/title) for a field or dashboard item.
803
+
804
+ **Example:**
805
+
806
+ ```
807
+ # label="Total Sales ($)"
808
+ measure: total_revenue is sales.sum()
809
+ ```
810
+
811
+ ### `# description`
812
+
813
+ Adds a description to a field, shown as an info icon tooltip in `# big_value` cards.
814
+
815
+ **Example:**
816
+
817
+ ```
818
+ measure: total_revenue is sales.sum() # description="Total revenue from completed sales. Updated daily."
819
+ ```
820
+
821
+ ### `# break`
822
+
823
+ Used within a `# dashboard` tag, applied to a `nest:`, to create a visual break (often forcing subsequent items onto a new line/section).
824
+
825
+ **Example:**
826
+
827
+ ```
828
+ # dashboard
829
+ view: my_dashboard is {
830
+ nest: kpi1 is { ... }
831
+ nest: kpi2 is { ... }
832
+ # break // Next item will likely start on a new row
833
+ nest: main_chart is { ... }
834
+ }
835
+ ```
836
+
837
+ ### `# tooltip`
838
+
839
+ Marks a nested field/view to be included in the tooltip of its parent chart or visualization. Can also include rendering instructions for how the tooltip content itself should be rendered.
840
+
841
+ **Example:**
842
+
843
+ ```
844
+ view: sales_by_brand_chart is {
845
+ # bar_chart
846
+ group_by: brand
847
+ aggregate: total_sales
848
+
849
+ // Show top 3 products in the tooltip for each brand bar
850
+ nest: top_products is { # tooltip
851
+ group_by: product
852
+ aggregate: total_sales
853
+ order_by: 2 desc
854
+ limit: 3
855
+ }
856
+
857
+ // Show category count in tooltip, formatted as a tiny bar chart
858
+ nest: category_breakdown is { # tooltip bar_chart.size=xs
859
+ group_by: category
860
+ aggregate: num_items is count()
861
+ }
862
+ }
863
+ ```
864
+
865
+ ---
866
+
867
+ ## Configuration Tags
868
+
869
+ ### `# size`
870
+
871
+ _(Likely legacy, prefer `.size` property on specific renderer tags)_ Sets a preset size for some renderers (charts, tables) when applied at the view or nest level.
872
+
873
+ - Values: `spark`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`.
874
+ - Syntax: `# size=lg` (applied to a view or nest)
875
+
876
+ **Example:**
877
+
878
+ ```
879
+ view: big_chart is {
880
+ # size=lg // Apply large size to default renderer (likely table or chart)
881
+ ...
882
+ }
883
+ ```
884
+
885
+ ### `# theme`
886
+
887
+ _(Model/View Level)_ Applies theme overrides to control styling (colors, fonts, sizes). Can be applied at the model level (`## theme { ... }`) for defaults or at the view level (`# theme { ... }`) for specific overrides.
888
+
889
+ **Properties:**
890
+
891
+ - _(Various CSS-like properties)_ e.g., `tableBodyColor`, `tableHeaderColor`, `tableRowHeight`, `fontFamily`.
892
+
893
+ **Example:**
894
+
895
+ ```
896
+ ## theme { tableBodyColor=blue } // Model-level default
897
+
898
+ source: my_source is ... extend {
899
+ view: my_view is {
900
+ # theme { tableBodyColor=red } // Override for this view
901
+ select: \*
902
+ }
903
+ }
904
+ ```
905
+
906
+ ### `# renderer_legacy`
907
+
908
+ _(Model Level)_ Applied at the top of a `.malloy` file using `## renderer_legacy` to indicate that the _entire_ model should use the older HTML table/chart renderers instead of the newer web component-based renderer.
909
+
910
+ **Example:**
911
+
912
+ ```
913
+ ## renderer_legacy
914
+
915
+ source: my_source is ...
916
+ ```