@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.
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +47 -0
- package/package.json +54 -0
- package/skills/malloy/SKILL.md +45 -0
- package/skills/malloy-analysis/SKILL.md +76 -0
- package/skills/malloy-analysis-pitfalls/SKILL.md +68 -0
- package/skills/malloy-analysis-report/SKILL.md +125 -0
- package/skills/malloy-analyze/SKILL.md +262 -0
- package/skills/malloy-charts/SKILL.md +417 -0
- package/skills/malloy-debug/SKILL.md +121 -0
- package/skills/malloy-define/SKILL.md +194 -0
- package/skills/malloy-discover/SKILL.md +182 -0
- package/skills/malloy-document/SKILL.md +165 -0
- package/skills/malloy-getting-started/SKILL.md +57 -0
- package/skills/malloy-gotchas-modeling/SKILL.md +277 -0
- package/skills/malloy-gotchas-queries/SKILL.md +132 -0
- package/skills/malloy-gotchas-rendering/SKILL.md +124 -0
- package/skills/malloy-html-data-app-embedding/SKILL.md +39 -0
- package/skills/malloy-html-data-app-runtime/SKILL.md +158 -0
- package/skills/malloy-html-data-apps/SKILL.md +165 -0
- package/skills/malloy-html-data-apps/reference/lazy-load.md +84 -0
- package/skills/malloy-html-data-apps/reference/verification-harness.md +121 -0
- package/skills/malloy-lookml-review/SKILL.md +79 -0
- package/skills/malloy-lookml-review/reference/_concepts.md +42 -0
- package/skills/malloy-lookml-review/reference/build-derived-tables.md +110 -0
- package/skills/malloy-lookml-review/reference/build-unnest.md +47 -0
- package/skills/malloy-lookml-review/reference/curate-visibility.md +66 -0
- package/skills/malloy-lookml-review/reference/discover.md +102 -0
- package/skills/malloy-lookml-review/reference/document.md +28 -0
- package/skills/malloy-lookml-review/reference/propose-fields.md +75 -0
- package/skills/malloy-lookml-review/reference/review-coverage.md +102 -0
- package/skills/malloy-materialization-tuning/SKILL.md +103 -0
- package/skills/malloy-model/SKILL.md +320 -0
- package/skills/malloy-model/reference/access-modifiers.md +21 -0
- package/skills/malloy-model/reference/analysis-to-model.md +82 -0
- package/skills/malloy-model/reference/bridge-tables.md +51 -0
- package/skills/malloy-model/reference/normalized-schemas.md +66 -0
- package/skills/malloy-model/reference/query-sources.md +57 -0
- package/skills/malloy-modeling/SKILL.md +174 -0
- package/skills/malloy-notebook-chat/SKILL.md +19 -0
- package/skills/malloy-notebooks/SKILL.md +225 -0
- package/skills/malloy-patterns/SKILL.md +117 -0
- package/skills/malloy-phrase-detection/SKILL.md +75 -0
- package/skills/malloy-publish/SKILL.md +111 -0
- package/skills/malloy-queries/SKILL.md +270 -0
- package/skills/malloy-review/SKILL.md +160 -0
- package/skills/malloy-review/reference/output-template.md +164 -0
- package/skills/malloy-review/reference/rubric-correctness.md +161 -0
- package/skills/malloy-review/reference/rubric-documentation.md +69 -0
- package/skills/malloy-review/reference/rubric-governance.md +37 -0
- package/skills/malloy-review/reference/rubric-queries.md +78 -0
- package/skills/malloy-review/reference/rubric-rendering.md +127 -0
- package/skills/malloy-review/reference/rubric-structure.md +68 -0
- package/skills/malloy-review/reference/rubric-style.md +59 -0
- package/skills/malloy-review/reference/scope-resolution.md +100 -0
- package/skills/malloy-review/reference/severity-taxonomy.md +139 -0
- 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
|