@malloy-publisher/skills 0.1.0

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/dist/index.d.ts +22 -0
  4. package/dist/index.js +47 -0
  5. package/package.json +54 -0
  6. package/skills/malloy/SKILL.md +45 -0
  7. package/skills/malloy-analysis/SKILL.md +76 -0
  8. package/skills/malloy-analysis-pitfalls/SKILL.md +68 -0
  9. package/skills/malloy-analysis-report/SKILL.md +125 -0
  10. package/skills/malloy-analyze/SKILL.md +262 -0
  11. package/skills/malloy-charts/SKILL.md +417 -0
  12. package/skills/malloy-debug/SKILL.md +121 -0
  13. package/skills/malloy-define/SKILL.md +194 -0
  14. package/skills/malloy-discover/SKILL.md +182 -0
  15. package/skills/malloy-document/SKILL.md +165 -0
  16. package/skills/malloy-getting-started/SKILL.md +57 -0
  17. package/skills/malloy-gotchas-modeling/SKILL.md +277 -0
  18. package/skills/malloy-gotchas-queries/SKILL.md +132 -0
  19. package/skills/malloy-gotchas-rendering/SKILL.md +124 -0
  20. package/skills/malloy-html-data-app-embedding/SKILL.md +39 -0
  21. package/skills/malloy-html-data-app-runtime/SKILL.md +158 -0
  22. package/skills/malloy-html-data-apps/SKILL.md +165 -0
  23. package/skills/malloy-html-data-apps/reference/lazy-load.md +84 -0
  24. package/skills/malloy-html-data-apps/reference/verification-harness.md +121 -0
  25. package/skills/malloy-lookml-review/SKILL.md +79 -0
  26. package/skills/malloy-lookml-review/reference/_concepts.md +42 -0
  27. package/skills/malloy-lookml-review/reference/build-derived-tables.md +110 -0
  28. package/skills/malloy-lookml-review/reference/build-unnest.md +47 -0
  29. package/skills/malloy-lookml-review/reference/curate-visibility.md +66 -0
  30. package/skills/malloy-lookml-review/reference/discover.md +102 -0
  31. package/skills/malloy-lookml-review/reference/document.md +28 -0
  32. package/skills/malloy-lookml-review/reference/propose-fields.md +75 -0
  33. package/skills/malloy-lookml-review/reference/review-coverage.md +102 -0
  34. package/skills/malloy-materialization-tuning/SKILL.md +103 -0
  35. package/skills/malloy-model/SKILL.md +320 -0
  36. package/skills/malloy-model/reference/access-modifiers.md +21 -0
  37. package/skills/malloy-model/reference/analysis-to-model.md +82 -0
  38. package/skills/malloy-model/reference/bridge-tables.md +51 -0
  39. package/skills/malloy-model/reference/normalized-schemas.md +66 -0
  40. package/skills/malloy-model/reference/query-sources.md +57 -0
  41. package/skills/malloy-modeling/SKILL.md +174 -0
  42. package/skills/malloy-notebook-chat/SKILL.md +19 -0
  43. package/skills/malloy-notebooks/SKILL.md +225 -0
  44. package/skills/malloy-patterns/SKILL.md +117 -0
  45. package/skills/malloy-phrase-detection/SKILL.md +75 -0
  46. package/skills/malloy-publish/SKILL.md +111 -0
  47. package/skills/malloy-queries/SKILL.md +270 -0
  48. package/skills/malloy-review/SKILL.md +160 -0
  49. package/skills/malloy-review/reference/output-template.md +164 -0
  50. package/skills/malloy-review/reference/rubric-correctness.md +161 -0
  51. package/skills/malloy-review/reference/rubric-documentation.md +69 -0
  52. package/skills/malloy-review/reference/rubric-governance.md +37 -0
  53. package/skills/malloy-review/reference/rubric-queries.md +78 -0
  54. package/skills/malloy-review/reference/rubric-rendering.md +127 -0
  55. package/skills/malloy-review/reference/rubric-structure.md +68 -0
  56. package/skills/malloy-review/reference/rubric-style.md +59 -0
  57. package/skills/malloy-review/reference/scope-resolution.md +100 -0
  58. package/skills/malloy-review/reference/severity-taxonomy.md +139 -0
  59. package/skills/malloy-scope/SKILL.md +100 -0
@@ -0,0 +1,262 @@
1
+ ---
2
+ name: malloy-analyze
3
+ description: Explore data for insights and build views/dashboards/notebooks. Use when user asks to "analyze this data", "find insights", "explore for patterns", "what's interesting", "what's driving X", "build a dashboard", "create views", or any analysis task. For EDA exploration, start at Step 1. For building views on an existing model, jump to View Patterns.
4
+ ---
5
+
6
+ # Analysis with Malloy
7
+
8
+ This skill covers two workflows:
9
+ - **EDA exploration** (Steps 1-6): iteratively query data, build hypotheses, validate findings
10
+ - **View/dashboard building**: create views, dashboards, notebooks from an existing model
11
+
12
+ > **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.
13
+
14
+ To formalize analysis into a polished semantic model, hand off to the modeling skill's "Starting from Analysis" workflow (`skill:malloy-model`).
15
+
16
+ ## Prerequisites
17
+
18
+ - The Malloy MCP tools must be configured (`get_context`, `execute_query`, `search_malloy_docs`). If they are not available, **STOP** and ensure your host's MCP server is connected.
19
+ - Call `search_malloy_docs` liberally: it has powerful analysis patterns (window functions, cohorts, percent-of-total, nested drill-downs).
20
+
21
+ # EDA WORKFLOW
22
+
23
+ ```
24
+ ORIENT → PROFILE → HYPOTHESIZE → INVESTIGATE → VALIDATE → SYNTHESIZE
25
+ (user) (user) (user)
26
+ ```
27
+
28
+ ## Adaptive Checkpoints
29
+
30
+ The 6-step structure is a framework, not a rigid script.
31
+
32
+ | Situation | Adaptation |
33
+ |-----------|------------|
34
+ | **User has a clear hypothesis** ("what's driving churn?") | Skip HYPOTHESIZE, jump to INVESTIGATE on their question |
35
+ | **Open-ended** ("what's interesting?") | Follow all steps. PROFILE and HYPOTHESIZE are essential |
36
+ | **User wants you to just go** ("explore and show me") | Compress checkpoints, present findings at SYNTHESIZE |
37
+
38
+ ## Step 1: ORIENT: Understand the Data
39
+
40
+ 1. Ground yourself with `get_context`. It returns the package's sources, views, and fields (with their docs), so this is where you learn what data exists.
41
+ 2. Note the source names, the connection they sit on, and the key tables/fields they expose.
42
+ 3. Inspect the existing dimensions, measures, and views the model already defines, then query the data to confirm shape and values.
43
+ 4. Create a working analysis file (this grows throughout the session):
44
+ ```malloy
45
+ source: main_table is conn.table('schema.table') extend { primary_key: pk }
46
+ ```
47
+
48
+ **Output to user:** Brief summary of available data. Ask: *"What questions are you most interested in? Or should I look for what's interesting?"*
49
+
50
+ ## Step 2: PROFILE: Statistical Profiling
51
+
52
+ **Directed analysis** (user has a question): Profile only columns relevant to their question.
53
+ **Open-ended** (no question yet): Profile broadly, looking for surprises.
54
+
55
+ ### Key Profiling Queries
56
+
57
+ **Column overview:** `run: source -> { index: * limit: 100 }`
58
+
59
+ **Numeric distributions:**
60
+ ```malloy
61
+ run: source -> {
62
+ aggregate: min_val is min(col), max_val is max(col), avg_val is avg(col), null_count is count() { where: col is null }
63
+ }
64
+ ```
65
+
66
+ **Categorical breakdown:** `run: source -> { group_by: col, aggregate: n is count(), order_by: n desc, limit: 20 }`
67
+
68
+ **Time range:** Check earliest/latest dates, gaps, seasonality.
69
+
70
+ **Duplicates:** `run: source -> { group_by: pk, aggregate: n is count(), having: n > 1, limit: 10 }`
71
+
72
+ **Add useful profiling dimensions/measures to your analysis file as you go.** Build incrementally.
73
+
74
+ ## Step 3: HYPOTHESIZE: Form Questions
75
+
76
+ **Skip presentation if user already has a clear question.** Use profiling to refine it and jump to INVESTIGATE.
77
+
78
+ | Signal from profiling | Hypothesis type |
79
+ |----------------------|-----------------|
80
+ | Skewed distribution | Outlier analysis |
81
+ | Time patterns | Trend/seasonality |
82
+ | Category imbalance | Segment comparison |
83
+ | Correlated columns | Driver analysis |
84
+ | Unexpected NULLs | Data quality |
85
+
86
+ **CHECKPOINT (open-ended only):** Present 3-5 hypotheses ranked by potential impact. Ask which to pursue.
87
+
88
+ ## Step 4: INVESTIGATE: Deep-Dive
89
+
90
+ ### Outlier Detection
91
+ Search `search_malloy_docs("window functions")` for ranking and percentile patterns.
92
+
93
+ ### Trend Analysis
94
+ ```malloy
95
+ # line_chart
96
+ view: trend is { group_by: period is date_col.month, aggregate: key_metric, order_by: period }
97
+ ```
98
+
99
+ ### Segment Comparison
100
+ ```malloy
101
+ view: segment_comparison is {
102
+ group_by: segment_dim
103
+ aggregate: row_count, key_metric
104
+ nest:
105
+ # line_chart
106
+ trend is { group_by: period is date_col.month, aggregate: key_metric, order_by: period }
107
+ }
108
+ ```
109
+
110
+ ### Driver Analysis
111
+ ```malloy
112
+ run: source -> {
113
+ group_by: candidate_driver
114
+ aggregate: row_count, avg_metric is avg(metric_col),
115
+ high_rate is count() { where: metric_col > threshold } / nullif(count(), 0)
116
+ order_by: high_rate desc
117
+ }
118
+ ```
119
+
120
+ ### Multi-Source Comparison (Source vs Group)
121
+
122
+ Compare each source to its group average using query-as-source:
123
+
124
+ ```malloy
125
+ query: team_stats is source -> { group_by: team, season, aggregate: team_avg is avg(points) }
126
+ query: driver_stats is source -> { group_by: driver, team, season, aggregate: driver_points is sum(points) }
127
+
128
+ source: driver_vs_team is from(driver_stats) extend {
129
+ join_one: ts is from(team_stats) on team = ts.team and season = ts.season
130
+ dimension: advantage is driver_points - ts.team_avg
131
+ }
132
+ ```
133
+
134
+ ### Nested Analysis (Malloy's Superpower)
135
+
136
+ Use `nest:` for multi-level drill-downs in a single query:
137
+ ```malloy
138
+ # dashboard
139
+ view: deep_dive is {
140
+ nest: # big_value
141
+ kpis is { aggregate: # label="Total" total_metric, # label="Count" row_count }
142
+ nest: # bar_chart
143
+ by_dim is { group_by: dim, aggregate: metric, order_by: metric desc, limit: 10 }
144
+ nest: # line_chart
145
+ over_time is { group_by: period is date.month, aggregate: metric, order_by: period }
146
+ }
147
+ ```
148
+
149
+ ### Build As You Go
150
+
151
+ Every useful query should leave an artifact in your `.malloy` file. New dimension? Add it. New measure? Add it. Interesting view? Save it. This file becomes the input for formalizing into a model if the user wants one.
152
+
153
+ ## Step 5: VALIDATE: Triangulate
154
+
155
+ For each finding, validate with at least ONE of:
156
+ - Cross-check with another metric (revenue spiking? do order counts also?)
157
+ - Check the denominator (high rate from tiny sample?)
158
+ - Examine time consistency (pattern or one-time event?)
159
+ - Look at raw data (`select: * where: condition limit: 20`)
160
+ - Check for data artifacts (NULLs, duplicates, encoding)
161
+
162
+ **CHECKPOINT:** Present each finding with: the insight, the evidence, confidence level, and assumptions made.
163
+
164
+ ## Step 6: SYNTHESIZE: Compelling Summary
165
+
166
+ Build a dashboard view that tells the story:
167
+ ```malloy
168
+ # dashboard
169
+ view: analysis_summary is {
170
+ nest: # big_value
171
+ headlines is { aggregate: ... }
172
+ nest: # line_chart
173
+ trend is { ... }
174
+ nest: # bar_chart
175
+ breakdown is { ... }
176
+ }
177
+ ```
178
+
179
+ Document insights as view descriptions: `#(doc) Top 10% of customers drive 62% of revenue.`
180
+
181
+ Present to user: top 3-5 insights, supporting views, open questions, and recommended next steps.
182
+
183
+ **Ready to formalize?** Hand off to the modeling skill's "Starting from Analysis" workflow (`skill:malloy-model`).
184
+
185
+ # VIEW PATTERNS
186
+
187
+ For building views on an existing model (base + joined source files already exist).
188
+
189
+ ## Starter Views (2-3 max initially)
190
+
191
+ 1. **`summary`**: KPI cards (`# big_value`)
192
+ 2. **`by_time`**: Time trend (`# line_chart`)
193
+ 3. **`by_category`**: Category breakdown (`# bar_chart`)
194
+ 4. **`dashboard`**: Nested view combining the above (`# dashboard`)
195
+
196
+ **DRY rule:** Do NOT define measures/dimensions inline in views. Reference existing ones from base source files.
197
+
198
+ ## View Annotations
199
+
200
+ | Annotation | Use For | Notes |
201
+ |-----------|---------|-------|
202
+ | `# big_value` | KPI summary | 2-5 metrics with `# label` on each |
203
+ | `# transpose` | Summary with group_by | Swaps rows/columns |
204
+ | `# dashboard` | Multi-visualization | Tiles nested views |
205
+ | `# line_chart` | Time trend | ONE aggregate only |
206
+ | `# bar_chart` | Category breakdown | ONE aggregate only |
207
+ | (none) | Detailed table | Supports multiple aggregates |
208
+
209
+ **Rules:**
210
+ - One tag per line, never combine annotations on one line
211
+ - One aggregate per chart view, charts render only the first
212
+ - No fixed scale on measures: use `# currency` (no scale); fixed scale only in views after confirming ranges
213
+ - Place chart annotation on the nested view definition, not on `nest:` itself
214
+
215
+ For complete chart reference including scatter_chart, shape_map, sparklines, and all configuration options, see `skill:malloy-charts` or call `search_malloy_docs("rendering")`.
216
+
217
+ ## Field-Level Formatting
218
+
219
+ | Tag | Use For |
220
+ |-----|---------|
221
+ | `# currency` | Monetary values |
222
+ | `# percent` | Rates/percentages |
223
+ | `# number=auto` | Large counts (K/M/B) |
224
+ | `# number=id` | Non-quantity numbers (years, IDs) |
225
+ | `# label="Name"` | Custom display name |
226
+ | `# hidden` | Internal/helper fields |
227
+ | `# duration=seconds` | Time durations |
228
+
229
+ # NOTEBOOKS (.malloynb)
230
+
231
+ Cells delimited by `>>>markdown` or `>>>malloy`. **Never use `>>>malloysql`.**
232
+
233
+ ```
234
+ >>>markdown
235
+ # Sales Analysis
236
+
237
+ >>>malloy
238
+ import "order_analysis.malloy"
239
+
240
+ >>>malloy
241
+ run: order_analysis -> summary
242
+ ```
243
+
244
+ **Compile errors in `.malloynb` are NOT shown in the linter**: only visible on cell execution.
245
+
246
+ A notebook is also the home for a polished, narrated report: alternate `>>>markdown` cells (the story) with `>>>malloy` cells (the views), and let the malloy cells carry the chart tags. For the full cell-shape and report-authoring conventions, see `skill:malloy-notebooks`.
247
+
248
+ ### Interactive Filters
249
+
250
+ **Notebooks do NOT define filters themselves.** When you import a model, the model's `#(filter)` annotations on the source are **inherited and displayed automatically**: the publisher renders the filter widgets, parses caller parameters, and injects `where:` clauses server-side. You don't redeclare them in the consumer. If the analysis needs a knob the model doesn't expose, the right move is to add a `#(filter)` to the source itself (see `skill:malloy-model` § Parameterizable Filters with `#(filter)`), not to wedge filtering into the consumer.
251
+
252
+ The notebook-level `##(filters)` annotation and the dimension-level `#(filter) {"type": "..."}` JSON-blob form are **unsupported legacy syntax**, don't use them. The only supported form is `#(filter) name=... dimension=... type=...` declared above the source.
253
+
254
+ ### View Refinement
255
+
256
+ Use `+` to modify existing views: `run: source -> my_view + { limit: 15, where: status = 'active' }`
257
+
258
+ ## Done
259
+
260
+ Step complete. Output: analysis `.malloy` file with views, insights, and reusable building blocks. For chart/renderer details, see `skill:malloy-gotchas-rendering` or call `search_malloy_docs`. To formalize into a model, hand off to the modeling skill (`skill:malloy-model`).
261
+
262
+ Publishing is out of scope for now: open-source Publisher serves the model from disk, and self-hosters publish via git plus their host's publish path.
@@ -0,0 +1,417 @@
1
+ ---
2
+ name: malloy-charts
3
+ description: Chart selection guidance and renderer reference for Malloy views. Use when choosing visualization types, adding chart annotations, user asks "what chart should I use", "how should I visualize this", or when deciding between bar_chart, line_chart, scatter_chart, etc.
4
+ ---
5
+
6
+ # Chart Selection for Malloy
7
+
8
+ > 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).
9
+
10
+ > **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.
11
+
12
+ ## Decision Tree: Which Chart?
13
+
14
+ | Data Shape | Default Choice |
15
+ |-----------|---------------|
16
+ | Aggregates only (no group_by) | `# big_value` |
17
+ | 1 time column + 1 measure | `# line_chart` |
18
+ | 1 category + 1 measure | `# bar_chart` |
19
+ | 2 numeric columns | `# scatter_chart` |
20
+ | Geographic (US states) + 1 measure | `# shape_map` |
21
+ | Route data (lat/lon pairs) | `# segment_map` |
22
+ | Multiple perspectives | `# dashboard` with `nest:` |
23
+ | Nested query to pivot | `# pivot` |
24
+ | Filtered aggregates side-by-side | `# flatten` |
25
+ | Detailed rows | Default table (no annotation) |
26
+
27
+ | Goal | Renderer |
28
+ |------|---------|
29
+ | Compare categories | `# bar_chart` (sort by value, limit ~15) |
30
+ | Show composition | `# bar_chart.stack` |
31
+ | Trend over time | `# line_chart` |
32
+ | Highlight KPIs | `# big_value` with `# label` |
33
+ | Correlation | `# scatter_chart` |
34
+ | Compare dimensions | `# dashboard` (nest chart views) |
35
+ | Before/after | `# transpose` or `# pivot` |
36
+ | Multiple metrics per category | Default table, `# flatten`, or `y=['a','b']` |
37
+
38
+ **Constraints:**
39
+ - ONE aggregate per chart view (charts render only the first; use `y=['a','b']` for multi-measure)
40
+ - No fixed scale on measure definitions: use `# currency` not `# currency=usd0m`
41
+ - One tag per line
42
+ - Alias joined fields in `group_by` before `order_by`
43
+ - Define measures in source, not in views
44
+
45
+
46
+ ## Chart Types
47
+
48
+ ### `# bar_chart`
49
+
50
+ **Data shape:** `group_by` = x-axis, `aggregate` = y-axis, optional 2nd `group_by` = series.
51
+
52
+ ```malloy
53
+ # bar_chart
54
+ view: by_carrier is { group_by: carrier, aggregate: flight_count, order_by: flight_count desc, limit: 10 }
55
+
56
+ # bar_chart.stack
57
+ view: by_region is { group_by: category, region, aggregate: revenue }
58
+
59
+ # bar_chart { y=['revenue','cost'] }
60
+ view: rev_vs_cost is { group_by: category, aggregate: revenue, cost }
61
+ ```
62
+
63
+ **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`
64
+
65
+ **Field role tags:** `# x`, `# y`, `# series` on individual fields to assign roles explicitly.
66
+
67
+ ### `# line_chart`
68
+
69
+ **Data shape:** `group_by` (temporal/numeric) = x-axis, `aggregate` = y-axis, optional 2nd `group_by` = series.
70
+
71
+ ```malloy
72
+ # line_chart
73
+ view: trend is { group_by: order_month, aggregate: revenue, order_by: order_month }
74
+
75
+ # line_chart { size=spark }
76
+ view: mini_trend is { group_by: order_month, aggregate: revenue, order_by: order_month }
77
+ ```
78
+
79
+ **Key properties:** `.zero_baseline`, `.interpolate` (e.g., `step`), `.size`, `.y` (supports `y=['a','b']`), `.series.limit` (default 12), `.title`, `.subtitle`
80
+
81
+ ### `# scatter_chart`
82
+
83
+ **Data shape:** Fields by position: x, y, color, size (bubble), shape.
84
+
85
+ ```malloy
86
+ # scatter_chart
87
+ view: correlation is { group_by: customer_id, aggregate: avg_price, total_quantity }
88
+ ```
89
+
90
+ ### `# shape_map`
91
+
92
+ Choropleth. US states only. Fields: state name, value.
93
+
94
+ ```malloy
95
+ # shape_map
96
+ view: by_state is { group_by: state, aggregate: revenue }
97
+ ```
98
+
99
+ ### `# segment_map`
100
+
101
+ Route map. US only. Fields: start_lat, start_lon, end_lat, end_lon, color.
102
+
103
+
104
+ ## Layout Types
105
+
106
+ ### `# big_value`
107
+
108
+ KPI cards. Aggregates only, no `group_by`.
109
+
110
+ ```malloy
111
+ # big_value
112
+ view: summary is {
113
+ aggregate:
114
+ # label="Revenue"
115
+ # currency
116
+ revenue
117
+ # label="Orders"
118
+ # number=auto
119
+ order_count
120
+ }
121
+ ```
122
+
123
+ **Properties:** `.size`, `.sparkline=<nested_view_name>`, `.comparison_field`, `.comparison_label`, `.down_is_good`
124
+
125
+ ### `# dashboard`
126
+
127
+ Card-based multi-tile layout. Apply to a view whose body is a nested query; the view's own fields lay out automatically:
128
+
129
+ - `group_by` dimensions -> a row header (repeats once per row; omit for a single block)
130
+ - `aggregate` measures -> KPI cards, one per measure
131
+ - each `nest:` -> a tile, rendered by the tag above it (`# table` default, or `# bar_chart` / `# line_chart` / `# big_value`)
132
+
133
+ **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, `# break` starts a new row, overflow wraps.
134
+
135
+ ```malloy
136
+ // Flex: measures become KPI cards, the nest becomes a tile
137
+ # dashboard
138
+ view: overview is {
139
+ group_by: category
140
+ # currency
141
+ aggregate:
142
+ avg_retail is retail_price.avg()
143
+ sum_retail is retail_price.sum()
144
+ nest:
145
+ # bar_chart
146
+ by_brand is { group_by: brand, aggregate: avg_retail is retail_price.avg(), limit: 10 }
147
+ }
148
+
149
+ // Columns: # colspan widens tiles, # break ends a row
150
+ # dashboard { columns=12 }
151
+ view: layout is {
152
+ group_by: category
153
+ # currency
154
+ aggregate:
155
+ # colspan=4
156
+ avg_retail is retail_price.avg()
157
+ # colspan=4
158
+ sum_retail is retail_price.sum()
159
+ # colspan=4
160
+ max_retail is retail_price.max()
161
+ nest:
162
+ # break
163
+ # colspan=6
164
+ # bar_chart
165
+ # subtitle="Top brands"
166
+ by_brand_chart is { group_by: brand, aggregate: avg_retail is retail_price.avg(), limit: 8 }
167
+ # colspan=6
168
+ by_brand_table is { group_by: brand, aggregate: product_count is count(), limit: 8 }
169
+ }
170
+ ```
171
+
172
+ **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).
173
+
174
+ 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:
175
+
176
+ ```malloy
177
+ # dashboard
178
+ view: kpis is {
179
+ group_by: category
180
+ nest:
181
+ # big_value
182
+ revenue_card is {
183
+ aggregate:
184
+ # label="Revenue"
185
+ # currency
186
+ # big_value { sparkline=trend }
187
+ total_revenue is retail_price.sum()
188
+ # line_chart { size=spark y.independent=true }
189
+ # hidden
190
+ nest: trend is { group_by: bucket is floor(id / 100)::number, aggregate: total_revenue is retail_price.sum(), order_by: bucket, limit: 20 }
191
+ }
192
+ }
193
+ ```
194
+
195
+ **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).
196
+
197
+ ### `# pivot`
198
+
199
+ Pivot nested results into columns. Max 30 pivot columns.
200
+
201
+ ```malloy
202
+ view: sales is {
203
+ group_by: product, aggregate: total
204
+ nest: # pivot
205
+ by_quarter is { group_by: quarter, aggregate: revenue }
206
+ }
207
+ ```
208
+
209
+ ### `# transpose`
210
+
211
+ Swap rows/columns. Good for period comparisons.
212
+
213
+ ```malloy
214
+ # transpose
215
+ view: comparison is {
216
+ aggregate:
217
+ # label="This Month"
218
+ current_revenue
219
+ # label="Last Month"
220
+ prior_revenue
221
+ }
222
+ ```
223
+
224
+ ### `# list` / `# list_detail`
225
+
226
+ List renders as comma-separated values. List_detail shows `value (detail)` pairs.
227
+
228
+ ### `# flatten`
229
+
230
+ Collapse nested record into parent table as columns. Use for side-by-side filtered aggregates:
231
+
232
+ ```malloy
233
+ view: segments is {
234
+ group_by: product, aggregate: total_revenue
235
+ nest: # flatten
236
+ enterprise is { where: segment = 'Enterprise', aggregate: # label="Enterprise" revenue }
237
+ nest: # flatten
238
+ smb is { where: segment = 'SMB', aggregate: # label="SMB" revenue }
239
+ }
240
+ ```
241
+
242
+ ### `# table`
243
+
244
+ Default (implicit). Use explicitly for `.size=fill` property.
245
+
246
+
247
+ ## Field Formatting Tags
248
+
249
+ | Tag | Use For | Shorthand |
250
+ |-----|---------|-----------|
251
+ | `# number` | Numeric formatting | `=auto` (K/M/B), `=id` (no commas), `=1k`, `=1m` |
252
+ | `# percent` | Percentages | (none needed) |
253
+ | `# currency` | Money | `=usd2m` (USD, 2 decimals, millions); scale only in views |
254
+ | `# duration` | Time durations | `=seconds`, `=minutes`, `=hours`, `=days` |
255
+ | `# data_volume` | Storage sizes | `=bytes`, `=kb`, `=mb`, `=gb` |
256
+ | `# link` | Hyperlinks | `.url_template="https://example.com/$$"` |
257
+ | `# image` | Inline images | `.height=40px`, `.width=100px` |
258
+
259
+ **Currency codes:** `usd` ($), `eur`, `gbp`. **Scale:** K/M/B/T/Q or `auto`.
260
+ **Number suffix styles:** `word` ("42.5 million"), `letter` ("42.5M"), `scientific`.
261
+
262
+ ## Utility Tags
263
+
264
+ | Tag | Purpose |
265
+ |-----|---------|
266
+ | `# hidden` | Hide from output (still usable for sorting/references) |
267
+ | `# label="..."` | Override display name |
268
+ | `# description="..."` | Tooltip text |
269
+ | `# tooltip` | Include nested view in chart tooltip |
270
+ | `# break` | Force new dashboard row |
271
+ | `# column { width=sm }` | Table column width |
272
+
273
+ ## Model-Level Defaults
274
+
275
+ ```malloy
276
+ ## viz.line_chart.defaults.y.independent=true
277
+ ## viz.bar_chart.defaults.stack
278
+ ```
279
+
280
+ ## Theming
281
+
282
+ 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:
283
+
284
+ | Annotation | Controls | Modes |
285
+ |-----------|----------|-------|
286
+ | `# theme.palette.series` | Categorical series colors (array) | shared |
287
+ | `# theme.palette.background.{light,dark}` | Chart canvas + table background | per-mode |
288
+ | `# theme.palette.tableHeader.{light,dark}` | Table header text color | per-mode |
289
+ | `# theme.palette.tableHeaderBackground.{light,dark}` | Table header row background | per-mode |
290
+ | `# theme.palette.tableBody.{light,dark}` | Table body text color | per-mode |
291
+ | `# theme.palette.tile.{light,dark}` | Dashboard tile background | per-mode |
292
+ | `# theme.palette.tileTitle.{light,dark}` | Dashboard tile title color | per-mode |
293
+ | `# theme.palette.mapColor.{light,dark}` | Choropleth gradient (`# shape_map` / `# segment_map`) | per-mode |
294
+ | `# theme.font.family` | Font for all rendered text | shared |
295
+ | `# theme.font.size` | Table font size (px) | shared |
296
+
297
+ The seven `palette.*` color keys each take a `.light` and/or `.dark` variant so dark mode gets its own value. `palette.series`, `font.family`, and `font.size` are single values shared across modes.
298
+
299
+ ```malloy
300
+ // Model-wide defaults (## applies to every view in the model):
301
+ ## theme.palette.series = ["#14b3cb", "#e47404", "#1474a4"]
302
+ ## theme.font.family = "Inter, sans-serif"
303
+
304
+ // Per-view override (# applies to this result only; beats the instance theme):
305
+ # theme.palette.background.light = "#fafafa"
306
+ # theme.palette.background.dark = "#111111"
307
+ # theme.palette.tableHeader.dark = "#94a3b8"
308
+ view: revenue_by_month is {
309
+ group_by: month
310
+ aggregate: revenue
311
+ }
312
+ ```
313
+
314
+ **Precedence**, highest to lowest, per key: `# theme.*` on the view, then `## theme.*` model default, then the instance theme, then Publisher's built-in defaults. A per-chart annotation overrides the instance for the keys it sets; unset keys fall through to the instance. (This is the reverse of a bare `@malloydata/render` embed, where the embedder wins: Publisher reads the annotation itself and layers it on top.)
315
+
316
+ Quote values that contain spaces or a leading `#`. The light/dark default (`defaultMode`) and the toggle lock (`allowUserToggle`) are instance-only: set them in the config `theme` block or the editor, not as annotations. The malloy-gotchas-rendering skill lists the annotation forms that look valid but do nothing.
317
+
318
+
319
+ ## Advanced Patterns
320
+
321
+ ### Sparklines in KPI Cards
322
+
323
+ ```malloy
324
+ # big_value { sparkline=trend }
325
+ view: revenue_kpi is {
326
+ aggregate: # label="Revenue" # currency revenue
327
+ nest: # line_chart { size=spark } # hidden
328
+ trend is { group_by: order_date, aggregate: revenue, order_by: order_date }
329
+ }
330
+ ```
331
+
332
+ ### KPIs with Comparison Deltas
333
+
334
+ ```malloy
335
+ # big_value { comparison_field=prior_month comparison_label="vs Last Month" }
336
+ view: rev_delta is {
337
+ aggregate: # label="Revenue" # currency revenue, # hidden prior_month
338
+ }
339
+ ```
340
+
341
+ Use `down_is_good=true` for metrics where decrease is positive (churn, defects).
342
+
343
+ ### Inline Mini-Charts in Table Rows
344
+
345
+ ```malloy
346
+ view: carriers is {
347
+ group_by: carrier, aggregate: flight_count
348
+ nest: # line_chart { size=spark }
349
+ trend is { group_by: month, aggregate: flight_count, order_by: month }
350
+ }
351
+ ```
352
+
353
+ ### Multi-Measure Series
354
+
355
+ ```malloy
356
+ # bar_chart { y=['revenue','cost'] }
357
+ view: rev_vs_cost is { group_by: quarter, aggregate: revenue, cost }
358
+ ```
359
+
360
+ ### Hierarchical Drill-Down
361
+
362
+ ```malloy
363
+ # list_detail
364
+ view: explorer is {
365
+ group_by: region, aggregate: revenue
366
+ nest: # bar_chart
367
+ by_category is { group_by: category, aggregate: revenue, order_by: revenue desc, limit: 10 }
368
+ }
369
+ ```
370
+
371
+ ### Distribution (Histogram)
372
+
373
+ Call `search_malloy_docs("autobin")` for syntax:
374
+ ```malloy
375
+ # bar_chart
376
+ view: price_dist is { group_by: bucket is autobin(price, 20), aggregate: order_count }
377
+ ```
378
+
379
+
380
+ ## Patterns for Missing Chart Types
381
+
382
+ | Desired | Malloy Approximation |
383
+ |---------|---------------------|
384
+ | Pie/donut | `# bar_chart` sorted by value |
385
+ | Treemap | Nested table with `order_by: desc` |
386
+ | Heatmap | `# pivot` with color values |
387
+ | Stacked area | `# line_chart` with series (overlaid lines) |
388
+ | Funnel | `# bar_chart` with ordered stages |
389
+ | Gauge/bullet | `# big_value` with `.comparison_field` |
390
+
391
+
392
+ ## Chart Annotations on Queries with `nest:`
393
+
394
+ A top-level chart tag (e.g., `# bar_chart`) renders only the outer query; any `nest:` views are silently hidden from the rendering (still in raw data). To show nests, use `# dashboard` on the outer query with chart tags on each nest. Otherwise, drop the `nest:`.
395
+
396
+
397
+ ## Common Mistakes
398
+
399
+ | Mistake | Fix |
400
+ |---------|-----|
401
+ | Two aggregates in chart | ONE aggregate, or use `y=['a','b']` |
402
+ | `# currency=usd0m` on measure | `# currency` (no scale) on defs; scale only in views |
403
+ | Chart annotation on `nest:` line | Put on the **view definition** |
404
+ | Tags on same line | One tag per line |
405
+ | Sparkline not showing | Add `# hidden` to nested view AND reference in `.sparkline=` |
406
+ | Pivot > 30 columns | Filter/limit the nested group_by |
407
+
408
+ NOTE: The term 'constructor' is a reserved term in Vega-Lite. If the word 'constructor' appears in the query, it will cause the rendering to fail. Never use it in a query and avoid using it as a dimension in a model.
409
+
410
+ For more patterns, call `search_malloy_docs` with topics like "bar charts", "line charts", "dashboards", "autobin", "percent of total", "comparing timeframes", or "pivots".
411
+
412
+ ## Further Reading
413
+
414
+ - [Visualizations Overview](https://docs.malloydata.dev/documentation/visualizations/overview) - Official docs
415
+ - [Bar Charts](https://docs.malloydata.dev/documentation/visualizations/bar_charts) - Stacked, grouped, series
416
+ - [Bump Charts Blog](https://docs.malloydata.dev/blog/2023-10-26-malloy-bump-chart/) - Ranking over time
417
+ - [Dataviz is Hierarchical](https://docs.malloydata.dev/blog/2024-02-29-hierarchical-viz/) - Nested data visualization philosophy