@malloydata/malloyyo 0.2.25 → 0.2.26
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/dist/index.js
CHANGED
|
@@ -147,8 +147,9 @@ import path from "node:path";
|
|
|
147
147
|
import url from "node:url";
|
|
148
148
|
var HOST_ONLY = "host_only";
|
|
149
149
|
var contentFiles = {
|
|
150
|
-
"dashboards/authoring.md": '---\ndescription: How to author a dashboard \u2014 a self-contained file in the dashboards/ directory that defines its own query; the entry point for dashboard how-to\n---\n\n# Authoring dashboards\n\nA dashboard is a **self-contained `.malloy` file in the `dashboards/`\ndirectory**. The file IS the dashboard: it imports the model parts it needs,\ndefines its query (with the filtering it applies), and tags it. No manifest, and\nfor the basic case no JavaScript. Preview with `malloyyo dashboard dev`; check\nwith `malloyyo lint`. Requires `@malloydata/malloy` 0.0.423+.\n\nRelated: `yo_help dashboards/givens-and-controls` (filter controls),\n`dashboards/grid-layout` (columns/colspan/break), `dashboards/custom-components
|
|
151
|
-
"dashboards/
|
|
150
|
+
"dashboards/authoring.md": '---\ndescription: How to author a dashboard \u2014 a self-contained file in the dashboards/ directory that defines its own query; the entry point for dashboard how-to\n---\n\n# Authoring dashboards\n\nA dashboard is a **self-contained `.malloy` file in the `dashboards/`\ndirectory**. The file IS the dashboard: it imports the model parts it needs,\ndefines its query (with the filtering it applies), and tags it. No manifest, and\nfor the basic case no JavaScript. Preview with `malloyyo dashboard dev`; check\nwith `malloyyo lint`. Requires `@malloydata/malloy` 0.0.423+.\n\nRelated: `yo_help dashboards/givens-and-controls` (filter controls),\n`dashboards/grid-layout` (columns/colspan/break), `dashboards/charts` (the\n`# bar_chart`/`# line_chart` tags and their channel rules),\n`dashboards/custom-components` (a flat `<name>.jsx`), `dashboards/vega-charts`\n(`<VegaChart>`).\n\n> **Need a chart the `# bar_chart`/`# line_chart`/`# shape_map` tags can\'t do?**\n> Use the `<VegaChart>` COMPONENT (a Vega-Lite spec over query rows) \u2014 NOT a `#`\n> tag; there is no `# vega_lite`. See `yo_help dashboards/vega-charts`.\n\n## The layout\n\n```\nmodel/\n ecommerce.malloy # sources, reusable views/measures, # drill tags\n givens.malloy # given: declarations (the filter controls)\n index.malloy # imports/exports sources \u2014 the MCP/data surface ONLY\n dashboards/\n overview.malloy # one dashboard; the FILENAME is its name/slug\n overview.jsx # optional custom component for overview\n```\n\n**The filename is the dashboard\'s name** \u2014 its URL slug, its `# drill` target,\nand the basename of its optional component. Discovery globs\n`dashboards/*.malloy` and compiles EACH as its own entry \u2014 so dashboards are NOT\ndeclared in, or exported through, `index.malloy` (`index.malloy` is just the\n`query`/`describe_source` data surface).\n\n## Preferred: put the query IN the dashboard file\n\nDefine the query right in `dashboards/<name>.malloy` and tag it `# artifact`, so\nthe given mapping (the `where: \u2026 ~ $GIVEN`) is visible next to the dashboard:\n\n```malloy\n// dashboards/overview.malloy\n##! experimental.givens\nimport "../ecommerce.malloy" // BARE import: source + givens in scope\n\n#" Business health at a glance \u2014 sales, margin, orders.\n# artifact { title="Business Overview" } dashboard {columns=6}\nquery: overview is order_items -> {\n where: // the given mapping, HERE\n inventory_items.product_brand ~ $BRAND, // multi-filter where: is\n inventory_items.product_category ~ $CATEGORY, // COMMA separated\n created_at ~ $PERIOD\n # colspan=2\n aggregate: total_sales, total_gross_margin, order_count\n # colspan=3\n nest:\n # line_chart\n sales_trend is by_month\n top_brands\n # shape_map\n sales_by_state\n}\n```\n\nThat\'s a complete dashboard: the runtime auto-renders a title (the tag\'s\n`title`, else the `#"` doc), a control for every given the query references, and\nthe result panel. `# artifact` DECLARES the dashboard; `# dashboard {columns=6}`\nis the renderer tag that draws it as KPI tiles + a card grid \u2014 partners, on the\nsame line. Grid rules: `yo_help dashboards/grid-layout`.\n\n**You know you\'re doing it right when the `where: foo ~ $FOO` is in the DASHBOARD\nfile, not the model.** Keep the model\'s sources/views reusable and given-free;\neach dashboard decides its own filtering.\n\n**The bare import is required for controls.** A control renders only when the\ngiven\'s DECLARATION is in the dashboard file\'s scope \u2014 a bare\n`import "../ecommerce.malloy"` (or `import "../givens.malloy"`) brings them all;\nthe runtime shows a control for exactly the givens the query references. A\nselective `import { order_items } from \u2026` brings the filter but NOT the control.\n\nKeep the FILENAME as the name \u2014 don\'t set `name=`, so the URL, the\n`# drill { to=\u2026 }`, and the component basename all agree (one source of truth).\n\n## Other forms\n\n- **A view of a source you extend in the file** \u2014 tag the dashboard `view:` with\n `# artifact` (runs as `<source> -> <view>`). Good when the dashboard needs\n helper views defined alongside it.\n- **Compose existing views**: a model-level `## artifact { tiles=["a -> b", "c -> d"]\n dashboard_columns=6 }` (`##`, ONE line) names several views. Each tile runs as\n its own query (in parallel), and the results are combined into ONE\n `# dashboard` that Malloy\'s dashboard renderer lays out \u2014 so it looks exactly\n like the equivalent single-query dashboard: `dashboard_columns=N` sets the grid\n and `# colspan=N` / `# break` on the tile VIEWS place them. A tile that returns\n a SINGLE ROW with no group-by (an aggregate view) is merged in as top-level KPI\n tiles rather than a card (its `# colspan` is spread across those KPIs). The\n dashboard paints once the tiles are ready, with a single early paint if one tile\n straggles so a slow tile can\'t hold up the rest. Use for multi-tile /\n cross-source; prefer the inline query whenever a dashboard has its own filtering.\n- A `dashboards/*.malloy` with NO `# artifact`/`## artifact` is a shared INCLUDE\n (skipped by discovery) \u2014 put helper sources/views there for several dashboards\n to import.\n\n## Givens (filter controls)\n\nDeclare givens as `filter<T>` in the MODEL (`givens.malloy` or the source file) \u2014\nthey\'re shared and used by the MCP surface too; each dashboard APPLIES them in\nits `where:`. Full control reference: `yo_help dashboards/givens-and-controls`.\nPer-dashboard starting values go in the tag:\n\n```malloy\n# artifact { title="Ford recalls" givens { MANUFACTURER=f\'Ford Motor Company\' } }\n```\n\n## Drill from a dimension\n\n`# drill` on a source `dimension:` (in the model) makes its cells clickable \u2014\nopening another dashboard (seeding the value) or filtering in place:\n\n```malloy\ndimension:\n # drill { to=[category_explorer, self] }\n category is inventory_items.product_category\n```\n\n`to` is a list; each is a **dashboard slug** (a `dashboards/<slug>.malloy`\nfilename) \u2192 opens it, seeding the value into the given named like the dimension\nUPPER-cased (`category` \u2192 `CATEGORY`), or **`self`** \u2192 filter the current\ndashboard in place. Add `given=` when the target given differs. `lint` VERIFIES\nevery `to=` slug resolves to a real dashboard file (a typo/renamed dashboard\nfails loudly, not at click time).\n\n> **malloy#2979 (fixed in 0.0.423):** a `# drill` on a bare `group_by: name` was\n> dropped when nested through `+ {\u2026}`. Put it on the source `dimension:`, or use\n> `group_by: name is concat(name,\'\')`.\n\n## Custom component (optional)\n\nFor bespoke layout/charts, add a flat sibling `dashboards/<name>.jsx` (or\n`.tsx`). Only React + `@malloyyo/dashboard` importable (sandboxed). A bare\n`<Panel/>` renders the whole dashboard; a `<Panel query="\u2026"/>` /\n`<VegaChart query="\u2026"/>` runs a query DEFINED in this dashboard file (by name) or\na `source -> view`. `lint` checks each `query="\u2026"` still resolves. See `yo_help\ndashboards/custom-components`.\n\n## Rules\n- Each dashboard is one `dashboards/<name>.malloy`; the filename is the slug.\n Prefer the inline `query: \u2026 # artifact` form \u2014 the `where: ~ $GIVEN` lives in\n the dashboard file.\n- Bare-import the model (and/or `givens.malloy`) so the controls render.\n- Givens are `filter<T>` declared in the model; options come from `# suggest {\u2026}`;\n interactivity = setting given values, not rewriting query text.\n- `index.malloy` is the data surface, NOT where dashboards live.\n- If a query/given you need is missing, add it (check with `describe_source`).\n\n## Preview & validate\n`malloyyo dashboard dev` \u2192 open the URL; `.malloy`/`.jsx` edits hot-reload.\n`malloyyo lint` checks each dashboard file on its own: it compiles as its entry;\neach tile/query and `# suggest` compiles; `dashboard_columns` is a positive int;\nthe component compiles and its `query="\u2026"` resolve; no duplicate names, no\norphaned component; every `# drill { to=\u2026 }` resolves. Tight loop: the local\n`malloyyo mcp --develop` server hot-reloads edits \u2014 `query(execute:false)` to\ncompile-check, `execute:true` to run. Don\'t validate against a hosted/claude.ai\nconnector \u2014 it serves the PUBLISHED model (stale until `malloyyo publish`).\n',
|
|
151
|
+
"dashboards/charts.md": "# Charts (`# bar_chart`, `# line_chart`, \u2026)\n\nThe renderer's built-in chart tags cover most dashboard tiles. Tag a nested view\nand it draws:\n\n```malloy\nnest:\n # bar_chart\n sales_by_brand\n # line_chart\n sales_by_month\n```\n\nRelated: `yo_help dashboards/grid-layout` (where tiles sit),\n`dashboards/vega-charts` (`<VegaChart>`, for shapes these tags can't express),\n`dashboards/authoring` (the dashboard file itself).\n\n## Channels: name them, don't leave them to be guessed\n\nA chart has three channels \u2014 **x** (the category axis), **y** (the value), and\n**series** (the colour split). Set them on the tag:\n\n```malloy\n# bar_chart { x=weekday y=drunk }\n# bar_chart { x=nickname y=flight_count series=destination }\n# line_chart { x=sale_date y=['sales', 'cost'] }\n```\n\nLeft unset, the renderer infers them: x prefers a time dimension and otherwise\ntakes the first dimension, y takes the first aggregate, and **series takes any\ndimension left over**. That last rule is the one that surprises people \u2014 see\nbelow. Naming `x` and `y` explicitly costs one line and removes the guesswork.\n\nOther properties: `.stack`, `.size` (`spark`/`xs`/`sm`/`md`/`lg`/`xl`/`2xl`, or\n`size.width` / `size.height`), `.x.limit`, `.series.limit`, and `.independent`\non any channel for nested charts.\n\n## THE RULE: charts count DIMENSIONS, not columns\n\nA chart can carry **one x dimension and one series dimension**. The renderer\ncounts the *dimensions* in the result (`group_by` fields \u2014 not aggregates) and:\n\n- **two dimensions, only `x` named** \u2192 the spare one is promoted to a colour\n series, whether or not you wanted a legend\n- **three or more dimensions, none tagged `series`** \u2192 it refuses:\n *\"Too many dimensions. A bar chart can have at most 2 dimensions: 1 for the x\n axis, and 1 for the series. To use 3+ dimensions, explicitly tag multiple\n fields as series.\"*\n\nThat last sentence is the escape hatch: 3+ dimensions is legal if you say which\nones are the series. What is NOT legal is leaving it to be inferred. If you\ngenuinely want a third dimension in the picture, name it; if you don't, the\nproblem is that a field you didn't think of as a dimension is one.\n\nAggregates are exempt. A result can carry as many measures as you like \u2014 only\nthe ones named in `y` (or the first, if `y` is unset) get plotted, and the rest\nare ignored rather than turned into channels.\n\n**`# hidden` does not exempt a dimension from this count.** It hides a field in\ntables, `big_value` comparisons and `# link` targets; it has no effect on chart\nchannel assignment. A `# hidden` group_by is still a dimension and will still\nbecome a series.\n\n## Sorting a named axis (the common case this rule makes hard)\n\nYou want an axis labelled `Sunday \u2026 Saturday`, or `Jan \u2026 Dec`, in *that* order \u2014\nnot alphabetical. The label has to be a string, but the sort key is a number, and\nthe naive fixes both fail:\n\n```malloy\n// WRONG \u2014 two dimensions: weekday_num becomes a colour series.\ngroup_by:\n # hidden\n weekday_num is day_of_week(exit_date)\n weekday is pick 'Sunday' when day_of_week(exit_date) = 1 \u2026\n```\n\n```malloy\n// ALSO WRONG \u2014 a `select:` stage is a projection, so it has no measures:\n// every column becomes a dimension and a 3-column result is rejected.\n} -> {\n select: weekday is pick 'Sunday' when weekday_num = 1 \u2026 , drunk, avg_price\n}\n```\n\n**Make the sort key a MEASURE.** Measures aren't dimensions, so a sort key\nexpressed as one can never be promoted to a channel:\n\n```malloy\n# bar_chart { x=weekday y=drunk }\nview: by_weekday is {\n group_by: weekday is\n pick 'Sunday' when day_of_week(exit_date) = 1\n pick 'Monday' when day_of_week(exit_date) = 2\n \u2026\n else 'Saturday'\n aggregate:\n drunk\n weekday_sort is min(day_of_week(exit_date)) // orders rows; never a channel\n order_by: weekday_sort asc\n}\n```\n\nOne dimension reaches the chart, the rows arrive in the order you asked for, and\na bar chart preserves query row order for a categorical axis. The same shape\nworks for months (`min(month(date))`), fiscal periods, size buckets, and any\nother \"display a name, sort by a number\" axis.\n\n> `min()` because a sort key needs *some* aggregate function and every row in the\n> group shares the value; `max()` is equally fine.\n\n## Deciding which tile is a chart at all\n\n- **Ranked categories** (top brands, regions by volume) \u2014 `# bar_chart`, ordered\n by the measure. Query order is the bar order.\n- **A value over time** \u2014 `# line_chart` with the time dimension as `x`.\n- **A wide detail table** \u2014 no chart tag. Charting eight columns is worse than\n showing them; give the tile `# colspan=6` instead and label the columns.\n- **Flat data** \u2014 if every bar is within a few percent of the others, the chart\n is saying \"no pattern here\" at the cost of a card. Consider whether the tile\n earns its place.\n\n## Trends by period: drop the incomplete one\n\nA by-year or by-month trend whose final period is partial draws a cliff that\nreads as collapse. Filter to complete periods, using a cutoff that comes from\nthe DATA (e.g. a snapshot/max-date column carried on a joined dimension), not\nfrom the result set \u2014 deriving it from the visible rows will wrongly declare a\nperiod partial as soon as a filter narrows the data.\n\n## Validate\n\n`malloyyo lint` compiles each dashboard and its tiles, but it does not render\nthem \u2014 a chart that compiles can still throw \"Too many dimensions\" in the\nbrowser. See it with `malloyyo dashboard dev`, which runs the queries\nserver-side and renders the real result.\n",
|
|
152
|
+
"dashboards/custom-components.md": '---\ndescription: Custom dashboard UI \u2014 a flat dashboards/<name>.jsx|tsx sibling composing @malloyyo/dashboard widgets/hooks/helpers with your own React\n---\n\n# Custom dashboard components (`dashboards/<name>.jsx`)\n\nThe default UI (auto-rendered controls + panel) covers most dashboards. When it\nisn\'t enough, add ONE file \u2014 a **flat sibling** `dashboards/<name>.jsx` (or\n`.tsx`) next to the dashboard\'s `dashboards/<name>.malloy` (same basename) \u2014 that\ncomposes the runtime\'s widgets/hooks with your own React. You own layout, copy,\nand theming; the `.malloy` file still owns every query and filter. See also\n`yo_help dashboards/authoring` and `dashboards/vega-charts`.\n\n```tsx\nimport React from "react";\nimport { Controls, Given, Search, Select, TimeRange, Panel, filters, useGiven } from "@malloyyo/dashboard";\n\nexport default function Dashboard({ dashboard, givens }) {\n return (\n <div style={{ maxWidth: 860, margin: "0 auto", padding: 24 }}>\n <h1>{dashboard.title}</h1>\n <Controls>\n <Given name="STATE" /> {/* picks the control from the declaration */}\n <Search given="NAME" /> {/* committing input + typeahead + validation */}\n <TimeRange given="PERIOD" presets={[\n { value: "", text: "All time" },\n { value: filters.lastN(1, "day"), text: "Last day" },\n { value: filters.lastN(1, "week"), text: "Last week" },\n { value: filters.lastN(1, "month"), text: "Last month" },\n ]} /> {/* "Custom range\u2026" is always appended */}\n <Select given="MIN_SAMPLE"\n options={[10, 200, 1000].map(n => ({ value: filters.greaterThan(n), text: `> ${n}` }))} />\n </Controls>\n <Panel givens={givens} /> {/* the dashboard itself (its tiles/query) */}\n <Panel query="baby_names -> births_by_decade" givens={givens} /> {/* a specific query */}\n </div>\n );\n}\n```\n\nFrom `@malloyyo/dashboard` (also handed to the component as props):\n- **Widgets** (headless-ish; restyle via className/style or the `--dash-*` CSS\n vars \u2014 see Theming below): `<Controls/>` (all givens, or compose children;\n grows Apply/Reset under `autorun=false`), `<Given name/>`,\n `<Select given [options]/>`, `<Search given/>` (committing input + typeahead +\n inline \u2715 clear), `<MultiSelect given [options]/>` (chip multi-select for a\n `filter<string>` \u2014 commits an exact-match list via `filters.oneOf`),\n `<Range given [min max]/>`, `<TimeRange given [presets]/>` (temporal presets +\n custom range), `<Checkbox given/>` (bound to a boolean given),\n `<VegaChart spec query|malloy|data givens/>` (a Vega-Lite chart over query\n rows \u2014 see `yo_help dashboards/vega-charts`)\n- **Hooks**: `useGiven(name)` \u2192 {value, set, spec};\n `useOptions(name, typed?)` \u2192 {options, loading} (typeahead);\n `useQuery({query|malloy, givens})` \u2192 {rows, loading, error} \u2014 plain rows\n for your own visuals;\n `useUrlState(key, initial)` \u2192 [value, setValue] \u2014 shareable view-state (below)\n- **Helpers**: `filters.oneOf/contains/between/atLeast/\u2026` build\n filter-expression strings with correct escaping; temporal:\n `filters.lastN(7, "day")` \u2192 `\'7 days\'`, `filters.dateRange("2026-01-01",\n "2026-07-01")`, `filters.afterDate/beforeDate`; read back with\n `filters.values/numberRange/threshold/inLast/temporalRange`;\n `filters.isValid(type, src)` checks typed input.\n Never hand-concatenate a filter string.\n **Escaping rule for custom controls:** a filter given\'s value is an\n EXPRESSION, so committing a raw column value is wrong the moment it contains\n a comma/percent/dash (\'Tesla, Inc.\' parses as two alternatives and matches\n nothing). Commit `filters.oneOf(value)` (exact) or\n `filters.contains(term)` (substring), and unwrap for display with\n `filters.values(src)`. The stock `<Select/>` does this automatically;\n `<Search/>` deliberately commits raw text (its input IS a filter\n expression).\n- `<Panel/>` runs against the DASHBOARD\'s own file: a bare `<Panel/>` renders\n the whole dashboard (its tiles); `<Panel query="\u2026"/>` runs a query defined in\n the dashboard file (by name) or a `source -> view`; `<Panel malloy="\u2026"/>` and\n `runData(text, givens)` run arbitrary Malloy as a RESTRICTED query (no import /\n given: / connection.* / raw SQL / ##! flags \u2014 the model\'s governed surface\n only). `lint` checks each hard-coded `query="\u2026"` still resolves.\n\n## Shareable view-state: `useUrlState`\n\n`useState` is invisible to the page that owns the URL, so a component built on\nit has an address bar that never changes \u2014 the result can\'t be shared or\nbookmarked. **`useUrlState(key, initial)` is a `useState` twin whose value lives\nin the URL**, under a `~key` param:\n\n```jsx\nimport { useUrlState } from "@malloyyo/dashboard";\n\nconst [rack, setRack] = useUrlState("rack", ""); // string\nconst [reuse, setReuse] = useUrlState("reuse", false); // boolean\nconst [board, setBoard] = useUrlState("board", "........"); // string\nconst [topN, setTopN] = useUrlState("n", 20); // number\n```\n\n- Same shape as `useState`: `[value, setValue]`, and `setValue` takes a value\n **or** an updater fn (`setBoard(b => \u2026)`).\n- The value comes from the URL on load, else `initial`. Every change is written\n back (debounced, `replaceState` \u2014 no history spam), so the address bar is\n always a shareable link.\n- Typed by `initial`: string / number / boolean / any JSON-serializable value.\n Strings stay readable in the URL (`~rack=retinas`); objects and arrays are\n JSON. A value equal to `initial` is dropped from the URL, so defaults never\n clutter it, and a malformed value falls back to `initial` instead of throwing.\n- Works identically in `malloyyo dashboard dev`, on a bundled static site, and\n on a hosted instance \u2014 including inside the sandboxed iframe, which cannot\n reach the top-level URL on its own. That\'s why this is a hook and not\n something a component can do with `history.replaceState`.\n\n**Use it for view-state, not query parameters.** A `given:` is the governed,\nfilter-typed query contract: it\'s declared in the model, drives the default\ncontrols, and is visible over MCP \u2014 bind those with `useGiven` and they already\nround-trip through the URL as `$NAME`. `useUrlState` is for everything else a\ncustom component needs to make shareable: a letter rack whose real query inputs\n(allowed letters, min/max length) are computed from it in JS, a board layout, a\nmode toggle. The two namespaces (`$NAME` vs `~key`) never collide.\n\n## Theming\n\nEvery widget is styled by the runtime\'s **default Malloyyo theme** (system\nfont, neutral grays, blue accent, auto light/dark following the viewer\'s OS) \u2014\na bare component looks styled with zero effort, so DON\'T hand-hardcode\n`fontFamily`/colors. The theme is CSS custom properties; override any subset by\nsetting them on a wrapper element (more specific than the runtime\'s `:root`):\n\n```tsx\n<div style={{ "--dash-accent": "#e11d48", "--dash-controls-bg": "#faf5ff" }}>\n <Controls /> \u2026\n</div>\n```\n\nVars: `--dash-font`, `--dash-bg`, `--dash-fg`, `--dash-muted`, `--dash-border`,\n`--dash-accent`, `--dash-accent-fg`, `--dash-control-bg`, `--dash-controls-bg`,\n`--dash-chip-bg`, `--dash-chip-fg`, `--dash-panel-bg`, `--dash-radius`,\n`--dash-danger`. `DefaultDashboard` also takes a `theme={{ accent, controlsBg }}`\nprop (camelCase keys \u2192 `--dash-*`). The results `<Panel>` keeps a light surface\nin both light/dark (the Malloy renderer has no dark theme) \u2014 override\n`--dash-panel-bg` if your renderer output is dark-safe.\n\n**Use `--dash-*` and nothing else.** A custom component renders in its own\n**iframe**, so CSS variables defined by the surrounding page \u2014 including the\nbundled site\'s `--line` / `--card` / `--muted` \u2014 are NOT in scope inside it. A\ncomponent styled against those still renders, but every rule referencing them\nresolves to nothing: borders, dividers and panel backgrounds vanish silently\nwhile text and layout survive, so the page looks *almost* right and the cause\nisn\'t obvious. If you\'re porting CSS that has to work both inside the frame and\non a bundled page, resolve each colour once through the chain and use the alias:\n\n```css\n.my-card {\n --edge: var(--dash-border, var(--line, #e4e6eb));\n --surface: var(--dash-panel-bg, var(--card, #fff));\n border: 1px solid var(--edge);\n background: var(--surface);\n}\n```\n\nThis is a class of bug `lint` cannot see and a screenshot can \u2014 look at custom\ncomponents in `malloyyo dashboard dev` before shipping them.\n\nFor charts beyond the Malloy renderer\'s `#` tags, use `<VegaChart>` \u2014\n`yo_help dashboards/vega-charts`.\n',
|
|
152
153
|
"dashboards/givens-and-controls.md": "---\ndescription: Dashboard filter controls \u2014 declare filter<T> givens with # label / suggest / control tags; faceted (related) suggestions\n---\n\n# Dashboard givens & controls\n\nA dashboard's filters are `filter<T>` **givens** declared in the model; the\n`#` tags on each declaration drive its control. This is part of authoring a\ndashboard \u2014 see also `yo_help dashboards/authoring`.\n\n**Declare the filters as `filter<T>` givens** \u2014 never raw strings/numbers.\nA `filter<string>` value accepts one value ('NY'), alternatives ('NY, CA'),\nwildcards ('Ann%'), negation ('-NY'); a `filter<number>` accepts ranges\n('[1910 to 1930]') and comparisons ('> 200'); a `filter<timestamp>` /\n`filter<date>` accepts relative windows ('7 days' = the last 7 days, 'today',\n'last month') and literal ranges ('2026-01-01 to 2026-07-01' \u2014 NO `@` in\nfilter literals). Apply with `~`; `f''` = empty = no filter (the natural\n\"All\"/\"all time\" \u2014 just `col ~ $X`, no `$X = '' or \u2026` dance):\n\n```malloy\n##! experimental { givens }\ngiven:\n # label=\"State\" control=select suggest { source=baby_names dimension=state }\n STATE :: filter<string> is f'NY'\n # label=\"Brand\" suggest { query=brand_suggest dimension=product_brand }\n BRAND :: filter<string> is f''\n # label=\"Names\" control=multiselect suggest { query=name_suggest dimension=name }\n NAMES :: filter<string> is f''\n # label=\"Years\" range_min=1910 range_max=2025\n YEAR_RANGE :: filter<number> is f'[1910 to 1930]'\n # label=\"Time period\"\n PERIOD :: filter<timestamp> is f''\n # label=\"Include rare names\"\n INCLUDE_RARE :: boolean is false\n```\n\nTags on the declaration drive the control (tag syntax is `key=\"value\"` \u2014\nequals, not colon):\n- `label` \u2014 control caption (defaults to the given's name)\n- `suggest { \u2026 }` \u2014 where the control's options come from. NO Malloy code in\n strings \u2014 just names:\n - `suggest { query=brand_suggest dimension=product_brand }` \u2014 the FIRST\n COLUMN of a named query (declare the query in the model \u2014 governed and\n reviewable). PREFER THIS FORM. The query must be in scope where the dashboard\n runs (bring it in with the dashboard file's bare `import`).\n - `suggest { source=baby_names dimension=state }` \u2014 the DISTINCT VALUES of\n a dimension on a source (the source must be in the dashboard's scope)\n A `dimension` (in either form) is what enables SERVER-SIDE TYPEAHEAD: the\n runtime refines the base query with what the user has typed\n (`\u2026 + { where: lower(field) ~ f'll%'; limit: 50 }`, case-insensitive,\n escaped). Without a dimension the fetched list is filtered client-side.\n Runs as a restricted query; lint checks the declaration compiles.\n\n **RELATED (faceted) filters** \u2014 query-form only: a suggest query may\n reference the OTHER givens, and the runtime runs it with the dashboard's\n current values (the suggested given itself is excluded, so the list never\n collapses to the current pick). Brand suggestions narrow when Category is\n set:\n\n ```malloy\n query: brand_suggest is inventory_items -> product_brand + {\n where:\n product_category ~ $CATEGORY, // NOT product_brand ~ $BRAND\n product_department ~ $DEPARTMENT\n limit: 500\n }\n ```\n\n Declare one `*_suggest` per filter, each referencing the others; `f''`\n defaults mean unset filters don't constrain. `source=` suggests can't do\n this (no place for a `where:`) \u2014 another reason to prefer `query=`.\n- `control=select` \u2014 a fixed dropdown instead of a typeahead search box\n- `control=multiselect` \u2014 a tokenized multi-select for a `filter<string>`:\n each pick is a removable chip, the committed value is an exact-match list\n (`Emma, Olivia, Sophia`). Ideal for \"pick several\" filters (names, brands).\n Suggestions come from the given's `suggest {\u2026}` (server-side typeahead when\n it names a dimension). Empty (start from `f''`) = no filter (all).\n- `range_min` / `range_max` \u2014 bounds; makes a filter<number> given a\n dual-thumb range slider\n- anything else passes through in `spec.tags` for custom components\n\nControl picked from the declaration automatically: numeric range tags \u2192\ndual-thumb slider; `filter<timestamp|timestamptz|date>` \u2192 the TimeRange\nwidget (relative presets: Today / Last 7 days / Last 30 days / \u2026 plus a\n\"Custom range\u2026\" from/to date picker); `control=multiselect` \u2192 chip\nmulti-select; suggest + control=select \u2192 dropdown; boolean \u2192 checkbox;\nanything else \u2192 committing search box with typeahead (an inline \u2715 clears it;\na \"Press \u21B5 to apply\" hint shows while the typed draft differs from what's\nrunning \u2014 free text can't safely re-run per keystroke).\nThe suggest-driven options are DATA VALUES only \u2014 options that aren't column\nvalues (custom time presets, threshold buckets) need a custom component\n(`yo_help dashboards/custom-components`) with explicit `{value, text}` options\nwhere value is a filter expression built with `filters.*`.\n\n## When the query re-runs: live (default) vs. Apply\n\nBy default a dashboard is **live** \u2014 every control change re-runs the query\nimmediately (the committing search box is the exception: free text commits on\nEnter/blur, since a half-typed filter is invalid). To batch changes behind an\n**Apply** button instead, set `autorun=false` on the `# artifact` tag:\n\n```malloy\n# artifact { name=\"births-by-name\" title=\"Births by name\" autorun=false }\n```\n\n`autorun=false` makes `<Controls>` grow an Apply/Reset pair \u2014 controls edit a\ndraft and nothing re-runs until Apply. Reach for it when the query is expensive\nor several filters are usually changed together; leave it off (live) otherwise.\n",
|
|
153
154
|
"dashboards/grid-layout.md": "---\ndescription: Dashboard grid layout \u2014 # dashboard {columns=N} with # colspan and # break to place KPI tiles and charts\n---\n\n# Dashboard grid layout (`# dashboard {columns=N}`)\n\nBy default a `# dashboard` result flows its KPI tiles and cards and wraps.\nAdd `{columns=N}` to place them on a fixed **N-column grid** instead \u2014 use\n`columns=6`, which divides evenly into 2- and 3-wide cards.\n\n**Key mechanic:** a tag placed ABOVE `aggregate:` or `nest:` applies to EVERY\nitem declared in that block. So you set card widths once per block, not per\nfield.\n\n```malloy\n# artifact { title=\"Customer Insights\" } dashboard {columns=6}\nview: customer_insights is {\n where: created_at ~ $PERIOD\n # colspan=2\n aggregate: total_sales, user_count, order_count, average_order_value\n # colspan=3\n nest:\n # break\n # bar_chart\n users_by_spend_tier\n sales_by_traffic_source\n # shape_map\n sales_by_state\n # colspan=6\n recent_orders // wide detail table \u2192 full width\n}\n```\n\n## The conventions\n\n- **`# colspan=2` above `aggregate:`** \u2014 each KPI / measure tile spans 2 of 6\n columns \u2192 3 tiles per row.\n- **`# colspan=3` above `nest:`** \u2014 each graph or small table spans 3 \u2192 2 per\n row. Per-item render tags (`# line_chart`, `# bar_chart`, `# shape_map`) still\n go on the individual nested items.\n- **`# colspan=6`** \u2014 a single wide / many-column table gets its own full-width\n line. Tag that one item; a per-item `# colspan` overrides the block default.\n- **`# break` on the FIRST nest item** \u2014 starts the graphs on a fresh row, so\n KPI tiles and charts never share one. The renderer splits fields into a new\n grid at each `# break`. Just always add it: it's a no-op when the tiles\n already fill complete rows, and the fix when they don't (e.g. 4 measures\n leave a lone tile a colspan-3 chart would otherwise pack in beside).\n\n`# colspan` only does anything in columns mode \u2014 without `{columns=N}` the\nlayout is free-flow wrap and colspan is ignored. Clamp colspans to `1..N`.\n\nSee also `yo_help dashboards/vega-charts` for custom charts, and the fuller\nauthoring guide surfaced by the local `malloyyo mcp` server.\n",
|
|
154
155
|
"dashboards/vega-charts.md": '---\ndescription: Custom dashboard charts with Vega-Lite \u2014 the <VegaChart> component, for charts the # renderer tags can\'t do\n---\n\n# Custom charts with Vega-Lite (`<VegaChart>`)\n\nWhen Malloy\'s renderer tags (`# bar_chart`, `# line_chart`, `# shape_map`, \u2026)\ndon\'t cover the chart you want, a dashboard can draw a **Vega-Lite** spec with\nthe `<VegaChart>` component. The chart engine ships in the dashboard runtime, so\nyou author only a JSON spec + a Malloy query \u2014 no library to load.\n\n**It is a COMPONENT, not a `#` tag.** There is no `# vega_lite` or\n`# scatter_chart` tag. `<VegaChart>` lives in a custom component \u2014 a flat sibling\n`dashboards/<name>.jsx` (or `.tsx`) next to the dashboard\'s\n`dashboards/<name>.malloy` \u2014 a different layer from the `#` renderer tags. (The\ndashboard\'s query is declared in the `.malloy` file; the component only\ncustomizes presentation. Preview with `malloyyo dashboard dev`, validate with\n`malloyyo lint`.)\n\n## The recipe\n\n```tsx\nimport { VegaChart } from "@malloyyo/dashboard";\n\n// Encodings point at the query\'s OUTPUT COLUMN NAMES (here: name, births).\nconst spec = {\n mark: { type: "bar", tooltip: true },\n encoding: {\n y: { field: "name", type: "nominal", sort: "-x" },\n x: { field: "births", type: "quantitative" },\n },\n};\n\nexport default function Dashboard({ givens }) {\n return <VegaChart spec={spec} query="births_by_name" givens={givens} />;\n}\n```\n\nThree ways to feed it data:\n- `<VegaChart spec={spec} query="births_by_name" givens={givens}/>` \u2014 a query\n defined in the dashboard\'s `.malloy` file (by name), or a `source -> view`\n- `<VegaChart spec={spec} malloy="source -> view" givens={givens}/>` \u2014 restricted\n Malloy text (same governance as the explore surface: no import / given: /\n connection.* / raw SQL / ##! flags)\n- `<VegaChart spec={spec} data={rows}/>` \u2014 rows you already have from `useQuery`\n\n## Gotchas (the ones that actually bite)\n\n- **Shape the data in Malloy; return FLAT rows.** Do ranking, share/percent\n (`all(x, dim)`), and label lookups (a `pick` for month names) in the QUERY.\n The spec just encodes columns \u2014 it is not the place to reshape data.\n- **Match column names character-for-character.** Run the query once with\n `query(execute:true)` and read the exact output column names; the spec\'s\n `field` values must match them exactly.\n- **The spec\'s `data` is ignored / any `url` is stripped.** The frame has no\n network \u2014 remote data URLs, transform lookups, and remote `image` marks are\n removed. Adapting a Vega-Lite gallery example = delete its\n `"data": {"url": \u2026}` and repoint the encodings; the query rows are inlined for\n you as the dataset.\n- **Nests come back as arrays.** Flatten to plottable rows in the query, or bind\n a nest to its own chart: `<VegaChart data={row.my_nest}/>`.\n- **Interactivity = setting given values**, never rewriting query text per\n interaction. Client-side chart interactions (tooltip, zoom, brush) work;\n anything that calls a server does not.\n- **Reads well:** for normalized/share data use a diverging color scale with\n `domainMid` (e.g. `1/12` for month-share), and sort a discrete axis by a\n companion numeric field (`month_name` sorted by `month_num`) rather than\n alphabetically.\n\n## Validate\n\n`malloyyo lint` checks the query, the givens, AND the component (it compiles,\nand each `query="\u2026"` it references resolves) \u2014 your only pre-browser check. Then\n`malloyyo dashboard dev` to see it render.\n',
|
|
@@ -3873,7 +3874,7 @@ async function launchCmd(mode, opts) {
|
|
|
3873
3874
|
}
|
|
3874
3875
|
|
|
3875
3876
|
// package.json
|
|
3876
|
-
var version = "0.2.
|
|
3877
|
+
var version = "0.2.26";
|
|
3877
3878
|
|
|
3878
3879
|
// src/index.ts
|
|
3879
3880
|
function shortSha(sha) {
|
|
@@ -121,17 +121,57 @@ and **givens** (the parameters that become the dashboard's filter controls).
|
|
|
121
121
|
use. **Only what `index.malloy` exports is visible** to dashboards, `dashboard
|
|
122
122
|
dev`, and the hosted app.
|
|
123
123
|
|
|
124
|
+
Give each exported source a `#"` doc string. It's what `list_sources` shows the
|
|
125
|
+
hosted app and any MCP client, and it's the only place to record the things a
|
|
126
|
+
consumer can't infer — which source answers which question, and any measure
|
|
127
|
+
whose meaning is subtle.
|
|
128
|
+
|
|
124
129
|
For Malloy modeling and givens specifics, lean on the author MCP rather than
|
|
125
130
|
guessing: the repo's `.mcp.json` wires `mcp__malloyyo_author__*`. Call
|
|
126
131
|
`mcp__malloyyo_author__compile` to check files and
|
|
127
132
|
`mcp__malloyyo_author__yo_help` for topics (`develop/working-with-models`).
|
|
128
133
|
|
|
134
|
+
Three shapes that compile, pass `lint`, and still produce a dashboard that looks
|
|
135
|
+
right and isn't:
|
|
136
|
+
|
|
137
|
+
- **A stage-level `where:` zeroes measures that carry their own filters.**
|
|
138
|
+
`count() { where: status = 'Sold' }` needs the sold rows to reach it; a
|
|
139
|
+
`where: in_cellar` on the stage removes them first and the measure reads 0
|
|
140
|
+
everywhere. Constrain the output with `having:` instead.
|
|
141
|
+
- **Grouping finer than the fact scatters the counts.** Group by an attribute
|
|
142
|
+
that only applies to some rows and the measures over the others split across
|
|
143
|
+
groups and read near-zero. Pick the grain the question is asked at; if you
|
|
144
|
+
need both, group at the coarser one and `nest:` the finer.
|
|
145
|
+
- **Detail tables default to raw field names and bare numbers.** `exit_date` and
|
|
146
|
+
`829.57`, and a null renders as `∅`. Tag them — `# label="Opened"`,
|
|
147
|
+
`# label="Paid" currency` — and `coalesce(field, '')` where empty is
|
|
148
|
+
meaningful rather than missing.
|
|
149
|
+
|
|
150
|
+
Aggregate locality is NOT one of these: Malloy's symmetric aggregates keep
|
|
151
|
+
measures honest through a `join_many` fan-out, and it warns (or errors) rather
|
|
152
|
+
than silently answering when an `avg`/`sum` crosses a join without explicit
|
|
153
|
+
locality. `notes.rating.avg()` already averages at the notes grain; ask for
|
|
154
|
+
`source.avg(notes.rating)` only when you want the fan-out-weighted number. See
|
|
155
|
+
`yo_help language/aggregate-locality-symmetric-aggregates`.
|
|
156
|
+
|
|
129
157
|
### 4. Author dashboards and preview live
|
|
130
158
|
|
|
131
|
-
Each dashboard is
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
159
|
+
Each dashboard is **one self-contained `dashboards/<name>.malloy`** — the query,
|
|
160
|
+
its filtering, and the tags that lay it out. The filename is the dashboard's
|
|
161
|
+
name and URL slug.
|
|
162
|
+
|
|
163
|
+
**A `.jsx` is OPTIONAL and most dashboards don't need one.** The default form —
|
|
164
|
+
an inline query tagged `# artifact` + `# dashboard { columns=6 }` — renders KPI
|
|
165
|
+
tiles and a card grid with no React at all. Reach for a sibling
|
|
166
|
+
`dashboards/<name>.jsx` only when you want bespoke layout or presentation the
|
|
167
|
+
tags can't express (a hand-built card, a picker, a Vega chart).
|
|
168
|
+
|
|
169
|
+
Author with the `malloyyo_author` MCP and its `yo_help` topics — **read these,
|
|
170
|
+
don't guess the API**: `dashboards/authoring` (the dashboard file),
|
|
171
|
+
`dashboards/grid-layout` (colspans), `dashboards/charts` (chart tags and their
|
|
172
|
+
channel rules), `dashboards/givens-and-controls` (filters),
|
|
173
|
+
`dashboards/custom-components` and `dashboards/vega-charts` (only if you add a
|
|
174
|
+
`.jsx`).
|
|
135
175
|
|
|
136
176
|
Preview in a browser with live reload:
|
|
137
177
|
|
|
@@ -139,9 +179,24 @@ Preview in a browser with live reload:
|
|
|
139
179
|
malloyyo dashboard dev # serves at http://localhost:4173
|
|
140
180
|
```
|
|
141
181
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
182
|
+
Two things about `dashboard dev` worth knowing:
|
|
183
|
+
|
|
184
|
+
- It runs the queries **server-side**, through the same local DuckDB the CLI
|
|
185
|
+
uses — the browser only renders results. So it works without DuckDB-WASM or
|
|
186
|
+
its CDN extensions, and it is the only way to see a custom component render
|
|
187
|
+
against real data short of publishing.
|
|
188
|
+
- It holds **one long-lived connection set with a warm schema cache** (a
|
|
189
|
+
deliberate tradeoff — refetching schemas per compile reads like a hang on a
|
|
190
|
+
warehouse). The file watcher hot-reloads your `.malloy` edits and re-discovers
|
|
191
|
+
`# artifact` tags, but nothing invalidates that cache, so a change to the
|
|
192
|
+
DATA's shape — a new column in a parquet — needs a restart. The tell is
|
|
193
|
+
`'<column>' is not defined` in the browser while the CLI and `lint` are green.
|
|
194
|
+
|
|
195
|
+
Iterate here until the dashboards look right. **Look at every dashboard before
|
|
196
|
+
you call it done**: `malloyyo lint` compiles the dashboards against the model but
|
|
197
|
+
does not render them, so it cannot see a chart that throws at render time, a
|
|
198
|
+
component whose CSS resolved to nothing, or a column of raw field names.
|
|
199
|
+
`malloyyo test` previews what the hosted claude.ai app would see.
|
|
145
200
|
|
|
146
201
|
### 5. Build the static site
|
|
147
202
|
|
|
@@ -169,6 +224,10 @@ skill (worked example: `malloydata/malloyyo-imdb`).
|
|
|
169
224
|
## Done when
|
|
170
225
|
|
|
171
226
|
- `malloyyo sql -e "SELECT count(*) FROM 'docs/<file>.parquet'"` returns real rows
|
|
172
|
-
- `malloyyo
|
|
227
|
+
- `malloyyo lint` passes
|
|
228
|
+
- **you have LOOKED at every dashboard** in `malloyyo dashboard dev` — each one
|
|
229
|
+
renders with no console errors, every tile has data, and the labels read like
|
|
230
|
+
English rather than column names. `lint` passing is not this; a dashboard can
|
|
231
|
+
compile and still throw at render time.
|
|
173
232
|
- `docs/` has the bundled `*.html` + `.nojekyll` alongside the parquet
|
|
174
233
|
- the Pages URL loads and the dashboards populate (data fetch succeeds)
|