feat(native.system): add build-visualizations and build-dashboard-controls - #27
Merged
thinhnguyentruong merged 5 commits intoAug 15, 2026
Merged
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
datbth
reviewed
Aug 4, 2026
Comment on lines
+93
to
+95
| * **A data block owns its data and shape — never its look or its filtering (strip both from generate_viz's output).** generate_viz builds each chart as a *standalone report*, so its output bundles three things: the **data + shape** (yours — keep), the **look** (the theme's — strip), and the chart's **own filters** (the controls' — strip). The durable test for any `settings`/`filter` it emitted: *would the theme or a control otherwise provide this?* If yes, strip it; if removing it changes what the chart **says**, keep it. Applying the test: | ||
| * **Look → strip** (theme-owned): series/mark colors, fonts, data labels, axis titles, point markers, legend/gridline styling. | ||
| * **Own filters → strip** (controls-owned): the viz-level date window (`matches 'today' / 'this month'`) and the baked-in period comparison (Period-over-Period — `pop_settings`, or a MetricKpi `display_mode: 'compare by number' / 'compare by percent'`). On a dashboard the date filter sets the window, the Period Comparison control the comparison, the drill the grain, dimension filters the segmenting — the chart itself does none of it. Left in, they hide rows and go empty when the data doesn't reach 'today'; a KPI's "vs previous period" then rightly comes from the control, not the KPI. |
Collaborator
There was a problem hiding this comment.
Isn't this too restrictive on the local filters?
|
|
||
| Worked example — a `CombinationChart` back from generate_viz: **strip** `series { color: … }` (look), `filter { … 'matches' 'this month' }` (a control provides the date), and `pop_settings { … }` (the Period Comparison control provides the compare); **keep** `sorts`, `format`, `group_values_into`, `row_limit` (data + shape). | ||
| * **MetricKpi** blocks get `settings { hide_label: true }` at the block level (a sibling of `viz:`) — and no other block setting. | ||
| * **Prefer the dataset's predefined metrics** over re-deriving equivalents. |
Collaborator
There was a problem hiding this comment.
generate_viz picks the metrics. So I believe this line is irrelevant
| * **A data block owns its data and shape — never its look or its filtering (strip both from generate_viz's output).** generate_viz builds each chart as a *standalone report*, so its output bundles three things: the **data + shape** (yours — keep), the **look** (the theme's — strip), and the chart's **own filters** (the controls' — strip). The durable test for any `settings`/`filter` it emitted: *would the theme or a control otherwise provide this?* If yes, strip it; if removing it changes what the chart **says**, keep it. Applying the test: | ||
| * **Look → strip** (theme-owned): series/mark colors, fonts, data labels, axis titles, point markers, legend/gridline styling. | ||
| * **Own filters → strip** (controls-owned): the viz-level date window (`matches 'today' / 'this month'`) and the baked-in period comparison (Period-over-Period — `pop_settings`, or a MetricKpi `display_mode: 'compare by number' / 'compare by percent'`). On a dashboard the date filter sets the window, the Period Comparison control the comparison, the drill the grain, dimension filters the segmenting — the chart itself does none of it. Left in, they hide rows and go empty when the data doesn't reach 'today'; a KPI's "vs previous period" then rightly comes from the control, not the KPI. | ||
| * **Keep** (data + shape): `group_values_into`, `sorts`, number/date `format`s (incl. `pattern: 'inherited'`), `row_limit`/`pagination_size`, aggregations; **color that IS the data** (a `RetentionHeatmap`/`Heatmap` scale, `ScaleFormat`/`conditional_formats` on a table); and a `filter`/`conditions` that's part of what the chart fundamentally is (a returns chart by definition `status = returned`, a "new customers" KPI by definition `status = new`) — but NOT one that merely restates what a control filters. |
Collaborator
There was a problem hiding this comment.
- I don't think this (listing every property) is scalable when we add more features in the future
- The rule about filter conflicts with above rules and hence is confusing, hard to follow
Comment on lines
+70
to
+86
| **Dynamic content block (`MarkdownViz`) — a data-bound HTML/Markdown template.** It pairs a **dataset query** (bound to fields) with a **`content:` template** that references those fields with `{{ … }}` placeholders (`{% … %}` for loops); Holistics injects live values on render. Division of labour: **generate_viz scaffolds the query** — the `dataset` + `rows`/`values` field bindings, the error-prone part — from an AQL explore; **you write only the `content:` template**, the HTML/CSS presentation. Shape: | ||
|
|
||
| ```aml | ||
| block <card>: VizBlock { | ||
| label: 'Card Title' | ||
| viz: MarkdownViz { | ||
| dataset: <dataset_name> | ||
| rows: [ VizFieldFull { ref: r(<model>.<dimension>) label: 'Product' } ] // dimensions (generate_viz fills these) | ||
| values: [ VizFieldFull { ref: r(<model>.<measure>) label: 'Revenue' aggregation: 'sum' } ] // measures | ||
| content: @md | ||
| <div class="card"><h3>{{ rows[0].`Product` }}</h3><p>{{ rows[0].values.`Revenue` }}</p></div> | ||
| ;; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The template syntax hinges on one Holistics-specific fact: the query result is `rows`, and within a row a **dimension is a top-level field while a measure sits under `values`** — ``{{ rows[0].`Product` }}`` vs ``{{ rows[0].values.`Revenue` }}`` (as in the example). Don't memorize the rest — loops, negative indexing, pivot nesting, `.raw` vs `.formatted`, and the bare-value cross-filter drill all live in the docs (**charts → dynamic-content-blocks → syntax reference**), and generate_viz's scaffold is a working starting point. **HTML / CSS / Markdown only — no JavaScript.** |
Collaborator
There was a problem hiding this comment.
Isn't this the responsibility of generate_viz?
| * A dashboard with a time axis gets a date-range filter; one with dimensional breakdowns gets 1–3 dimension filters. Fewer only if the user explicitly wants none. | ||
| * **The control set follows the dashboard's job.** A date-range filter + 1–3 dimension filters suit an overview. A lean scorecard or an operational "what's happening now" view usually wants fewer controls and **no date drill or period comparison** — add a drill only when the job wants time-grain switching, and a PoP only when it wants period comparison, not by default. | ||
| * Every control is fully wired the moment it exists — a declared-but-unwired control is worse than none, because it looks functional. | ||
| * **A FilterBlock never gets a `default`** — it always opens showing all data (an arbitrary default silently hides rows). This does not touch DateDrillBlock/PopBlock defaults, which those constructs require. |
Collaborator
There was a problem hiding this comment.
Is this (no default) really a good default?
never
never is really restrictive
| ### Declaring scope well | ||
|
|
||
| * **Wire a control to every block it can affect, then disable the exceptions.** A filter can affect every block on its dataset — map each one (with the right field). Period Comparison affects every KPI and the trend. The drill affects every time-series block. Start from that full set and turn OFF the ones you don't want (parent-child filter links, cross-tab reach). Don't build the list by guessing which blocks "go with" the control — that's how the trend or a KPI gets silently missed and left on platform defaults. | ||
| * When one filter feeds blocks built on different models, map the semantically right field per block — e.g. a global date filter maps `r(users.sign_up_date)` on customer blocks and `r(orders.created_date)` on order blocks. Never force one field onto every block. |
Collaborator
There was a problem hiding this comment.
Never force one field onto every block
Is this sentence really necessary/accurate?
4 tasks
thinhnguyentruong
force-pushed
the
native-skills/build-visualizations-and-controls
branch
from
August 4, 2026 10:10
fc2a756 to
ac9834a
Compare
thinhnguyentruong
added a commit
that referenced
this pull request
Aug 4, 2026
…opt link Two fixes to build-dashboard, split out of #27 so they can land independently of the two new skills. 1. The description was invalid YAML. It was a single-line plain scalar containing ": " (from "Typical phrasings: ..."), which YAML does not allow — gray-matter raises `incomplete explicit mapping pair`, so the frontmatter would not parse and the skill's whole trigger contract was unreadable. Rewritten as a block scalar with the same four paragraphs it always had; no wording changed. Also adds the `label` field its sibling skills carry. 2. Plan confirmation now closes with an option link. Workflow step 2 presented a plan and then waited on free text. It now ends with `[Yes — build it as planned](#opt)`, which the chat renders as a clickable chip; clicking it sends the link text verbatim as the user's next message. The step states the two consequences of that, since neither is guessable from the markdown: the link is not a tool call, so nothing pauses the agent — it must end its turn and wait; and the label becomes the user's message, so each is written as the instruction they'd have typed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
datbth
pushed a commit
that referenced
this pull request
Aug 4, 2026
…opt link Two fixes to build-dashboard, split out of #27 so they can land independently of the two new skills. 1. The description was invalid YAML. It was a single-line plain scalar containing ": " (from "Typical phrasings: ..."), which YAML does not allow — gray-matter raises `incomplete explicit mapping pair`, so the frontmatter would not parse and the skill's whole trigger contract was unreadable. Rewritten as a block scalar with the same four paragraphs it always had; no wording changed. Also adds the `label` field its sibling skills carry. 2. Plan confirmation now closes with an option link. Workflow step 2 presented a plan and then waited on free text. It now ends with `[Yes — build it as planned](#opt)`, which the chat renders as a clickable chip; clicking it sends the link text verbatim as the user's next message. The step states the two consequences of that, since neither is guessable from the markdown: the link is not a tool call, so nothing pauses the agent — it must end its turn and wait; and the label becomes the user's message, so each is written as the instruction they'd have typed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
thinhnguyentruong
added a commit
that referenced
this pull request
Aug 5, 2026
Addresses @datbth's comments on #27 (the `never` on filter defaults, and the redundant "never force one field onto every block"), and corrects several things that were wrong or over-prescribed. Corrections: - Filters carry an explicit *neutral* default, not no default. Real dashboards write `default { operator: 'matches' value: '$H_NIL$' }`; the old rule said a FilterBlock must never have one, which no workspace dashboard matches. - DateDrillBlock / PopBlock defaults are optional (`default(optional)` in the type defs; product fixtures omit them). The schema showed `default: 'month'` and a year-over-year PoP inline, which quietly made both the house default — undoing the rule two sections up that neither is added by default. - Field vs manual filters are now distinguished. A field filter auto-maps to same-tab blocks on its dataset; a manual one carries no field and filters nothing until mapped per block. Only the field form was documented before. - Drill and pop are dead until mapped, like manual filters, so their cross-tab `disabled: true` scaffolding is gone — unmapped already excludes. The three live-by-default edges still need it. - A viz block takes at most one drill and one pop; the verify step now checks this, since the map-or-disable matrix passes a block mapped by two drills. Rework: - Interactions reorganised into the six edges grouped by default state (live-until-disabled vs dead-until-mapped) — that split decides what to write, and the schema example now mirrors it. - Input section: 5 items to 4, in source -> target order, plain names instead of coined ones ("Slicing needs", "Grain switching"). Dropped the item that restated which controls exist. - Output section: the 1-3 filter count replaced by the test that produces a count (breaks down a block here, low-cardinality, answers something the layout doesn't) plus overlap and job-shape factors. - Workflow 6 steps to 5; "Declaring scope well" folded into "Others", cut to what the interactions section doesn't already say. - Control placement is a principle (position advertises reach) instead of the always-top-of-canvas recipe, which was only right for page-wide controls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
thinhnguyentruong
added a commit
that referenced
this pull request
Aug 5, 2026
Addresses the four review comments on #27, and corrects what checking them turned up. Corrections: - generate_viz returns the whole Viz AML, `content:` included. The skill said it "scaffolds the query" while "you write only the content: template", which is wrong, and the viz-type table repeated it ("you craft its template"). Both now say to describe the card in the `query` instead. - The workflow said "scaffold-then-craft for a dynamic content block", the same mistake in the step an author actually follows. Review comments: - The strip rule is a preference, not an absolute: "usually shouldn't keep", with a carve-out for a filter that is part of what the chart is, and a user instruction outranking the test. - The strip/keep guidance was three enumerated sub-bullets that contradicted each other (one said strip filters, the next said keep some). Collapsed to the test plus the distinction that resolves it; every property enumeration is gone, so it no longer dates as the product gains features. - Dropped "prefer the dataset's predefined metrics" as low-value. Note the parent does still choose the metric — it names one in the generate_aql prompt — so the line was cut for weight, not because it was inaccurate. Rework: - "data block" was invented; the construct is VizBlock. Renamed throughout, including the title and the description's first line, which had been defining the invented term against the real one. - "data job" -> "analytics task". - Input section reshaped to the numbered form the sibling skills use, with the question phrased in the reader's words. - Workflow rebuilt around what generate_viz actually is — a sub-agent that decides anything the prompt leaves unstated. The steps now centre on composing that prompt (name the type, the field roles, the formatting, the exclusions) and checking what returns; retry guidance added, since passing prior errors back is documented as helping it self-correct. - Viz-type table split into preferred vs other types, so the CombinationChart preference sits where the choice is made rather than only in Conventions, and slash-lists that implied interchangeability (ScatterChart/BubbleChart, the three map types) now say when each applies. - The strip rule is stated once, in Conventions, instead of five times. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…trols Two skills that complete the dashboard-building family alongside build-dashboard, splitting the `DashboardBlock` union by owner: - build-visualizations owns every VizBlock — built-in visualizations and dynamic content blocks (MarkdownViz). Covers picking a viz type by data job, authoring via generate_viz rather than by hand, and stripping the decoration and filters that the theme and the dashboard's controls own. - build-dashboard-controls owns FilterBlock / DateDrillBlock / PopBlock and the interactions array. Covers deriving the control set from a dashboard's job, wiring each control to every block it can affect, and disabling parent-child and cross-tab edges that would otherwise fall to platform defaults. Both are model-invocable only (user-invocable: false); build-dashboard stays the user-facing entry point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses @datbth's comments on #27 (the `never` on filter defaults, and the redundant "never force one field onto every block"), and corrects several things that were wrong or over-prescribed. Corrections: - Filters carry an explicit *neutral* default, not no default. Real dashboards write `default { operator: 'matches' value: '$H_NIL$' }`; the old rule said a FilterBlock must never have one, which no workspace dashboard matches. - DateDrillBlock / PopBlock defaults are optional (`default(optional)` in the type defs; product fixtures omit them). The schema showed `default: 'month'` and a year-over-year PoP inline, which quietly made both the house default — undoing the rule two sections up that neither is added by default. - Field vs manual filters are now distinguished. A field filter auto-maps to same-tab blocks on its dataset; a manual one carries no field and filters nothing until mapped per block. Only the field form was documented before. - Drill and pop are dead until mapped, like manual filters, so their cross-tab `disabled: true` scaffolding is gone — unmapped already excludes. The three live-by-default edges still need it. - A viz block takes at most one drill and one pop; the verify step now checks this, since the map-or-disable matrix passes a block mapped by two drills. Rework: - Interactions reorganised into the six edges grouped by default state (live-until-disabled vs dead-until-mapped) — that split decides what to write, and the schema example now mirrors it. - Input section: 5 items to 4, in source -> target order, plain names instead of coined ones ("Slicing needs", "Grain switching"). Dropped the item that restated which controls exist. - Output section: the 1-3 filter count replaced by the test that produces a count (breaks down a block here, low-cardinality, answers something the layout doesn't) plus overlap and job-shape factors. - Workflow 6 steps to 5; "Declaring scope well" folded into "Others", cut to what the interactions section doesn't already say. - Control placement is a principle (position advertises reach) instead of the always-top-of-canvas recipe, which was only right for page-wide controls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addresses the four review comments on #27, and corrects what checking them turned up. Corrections: - generate_viz returns the whole Viz AML, `content:` included. The skill said it "scaffolds the query" while "you write only the content: template", which is wrong, and the viz-type table repeated it ("you craft its template"). Both now say to describe the card in the `query` instead. - The workflow said "scaffold-then-craft for a dynamic content block", the same mistake in the step an author actually follows. Review comments: - The strip rule is a preference, not an absolute: "usually shouldn't keep", with a carve-out for a filter that is part of what the chart is, and a user instruction outranking the test. - The strip/keep guidance was three enumerated sub-bullets that contradicted each other (one said strip filters, the next said keep some). Collapsed to the test plus the distinction that resolves it; every property enumeration is gone, so it no longer dates as the product gains features. - Dropped "prefer the dataset's predefined metrics" as low-value. Note the parent does still choose the metric — it names one in the generate_aql prompt — so the line was cut for weight, not because it was inaccurate. Rework: - "data block" was invented; the construct is VizBlock. Renamed throughout, including the title and the description's first line, which had been defining the invented term against the real one. - "data job" -> "analytics task". - Input section reshaped to the numbered form the sibling skills use, with the question phrased in the reader's words. - Workflow rebuilt around what generate_viz actually is — a sub-agent that decides anything the prompt leaves unstated. The steps now centre on composing that prompt (name the type, the field roles, the formatting, the exclusions) and checking what returns; retry guidance added, since passing prior errors back is documented as helping it self-correct. - Viz-type table split into preferred vs other types, so the CombinationChart preference sits where the choice is made rather than only in Conventions, and slash-lists that implied interchangeability (ScatterChart/BubbleChart, the three map types) now say when each applies. - The strip rule is stated once, in Conventions, instead of five times. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…enerate_viz Closes out the remaining half of @datbth's "isn't this the responsibility of generate_viz?" — the dynamic content block section still carried the template mechanics ({{ … }} / {% … %}), the full MarkdownViz AML shape with its rows/values bindings, and a pointer to the syntax reference. generate_viz authors the block including its template, so all of that is its knowledge, not the caller's. What the caller still owns stays: routing (the three confusable boundaries in "Choosing the viz type" are unchanged) and the content of the prompt — a card needs describing as a finished card, since only the caller knows which card. Also merged the two authoring paragraphs. The second one existed to say a dynamic content block is authored the same way as a built-in, which is a paragraph earning its keep by announcing it isn't needed. One paragraph now covers the flow for every type, and a second names the single axis that varies: what the `query` has to carry. The minimum-output bullet lost "valid HTML/CSS/Markdown, every {{ … }} bound to a declared query field" for the same reason — that is generate_viz's correctness, not a bar the caller can hold. It now states the bar the caller does control: the block shows something a built-in can't, with live values. Note this assumes generate_viz carries its own MarkdownViz authoring context. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
thinhnguyentruong
force-pushed
the
native-skills/build-visualizations-and-controls
branch
from
August 14, 2026 10:18
0598398 to
196aae7
Compare
thinhnguyentruong
marked this pull request as ready for review
August 14, 2026 10:19
…ls a plugin source Mirrors what build-dashboard and build-custom-chart already do: the plugin copy under plugins/holistics-development/skills/ is the source, and the native-skills directory holds a synced copy with a .link back to it. build-visualizations and build-dashboard-controls now ship in the holistics-development plugin alongside build-dashboard, so the three skills that split the DashboardBlock union stay together in both runtimes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
datbth
approved these changes
Aug 14, 2026
thinhnguyentruong
deleted the
native-skills/build-visualizations-and-controls
branch
August 15, 2026 02:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds two skills that complete the dashboard-building family alongside
build-dashboard. Together they split theDashboardBlockunion by owner —build-dashboardkeepsTextBlockplus the dashboard, layout and orchestration:build-visualizationsVizBlock— built-in visualizations and dynamic content blocks (MarkdownViz)build-dashboard-controlsFilterBlock,DateDrillBlock,PopBlock, and theinteractionsarrayBoth are model-invocable only (
user-invocable: false), sobuild-dashboardstays the user-facing entry point.Both follow the same layout as
build-dashboardandbuild-custom-chart: the source lives in theholistics-developmentplugin, andnative-skills/system/holistics-common/holds a synced copy with a.linkback to it. So the three skills that split theDashboardBlockunion ship together in both runtimes.build-visualizationsCovers picking a viz type from the analytics task, authoring the block, and deciding what belongs to the block versus the dashboard around it.
CombinationChartpreferred for any line, area, column, or bar chart since it is the superset of all four. Calls out the three confusable boundaries: static text vs.MarkdownViz, built-in vs.MarkdownViz, andMarkdownVizvs. a custom chart.generate_viz, never hand-written, viagenerate_aql → generate_viz → execute_viz. The skill covers composing that prompt (naming the type, the field roles, the formatting, the exclusions) and passing errors back on retry; the per-type field roles and settings stay withgenerate_vizand the docs rather than being inlined.generate_vizbuilds each chart as a standalone report, so the result carries decoration and a self-contained time window a dashboard block usually shouldn't keep. One test: would the theme or a control otherwise provide this? A filter that is part of what the chart fundamentally is stays; an explicit user instruction outranks the test.build-dashboard-controlsCovers deriving the control set from the dashboard's job and wiring every control explicitly.
interactions: []array at the dashboard level.field filter → viz,filter → filter, andviz → vizare live until disabled; manual filter, drill, and pop do nothing until mapped. That split decides what has to be written, and drives the cross-tab handling and the verification matrix. One hard limit: a viz block takes at most one drill and one pop.Test plan
pnpm validate-links— both new links valid and in sync with their sourcepnpm validate-frontmatter— 33 files pass, now including both new skills (it globsplugins/**, which the plugin source is under)main; CI greenVersion bump and
CHANGELOG.mdforholistics-developmentare left to a separaterelease(plugins.development)commit, matching how the plugin has been released so far.🤖 Generated with Claude Code