Add aggregation, binning, granularity, sort, and stacking to the Chart Builder

- Per-channel transforms: aggregate (sum/mean/median/min/max), quantitative
  bin, and temporal timeUnit granularity; bin and aggregate are mutually
  exclusive. A field-less "Count of records" measure (Voyager's count(*)).
- Chart-level sort (rank a categorical axis by its measure) and stacking
  (zero / 100% normalize), each shown only when it applies.
- Field type is a fixed N|O|Q|T segmented control with the column's invalid
  types disabled; SegmentedControl gains APG-correct disabled options.
- A crowded-category-axis warning (a raw measure drawing one mark per row over
  a large dataset) and a disabled-Create hint (says why it's disabled).
- Drop the Create success toast — the new snippet is immediately visible.
- Docs: spec §06, research-doc §8 backlog (incl. the cardinality/extent
  profiling TODO), architecture 01 (stable-selector rule) and 05 (builder-local
  preview), and a profiling breadcrumb.
This commit is contained in:
2026-06-06 18:04:24 +03:00
parent c11afc273d
commit af9ee1e4c0
14 changed files with 982 additions and 119 deletions
@@ -80,9 +80,20 @@
outline-offset: 1px;
}
.channelRow {
/* Each channel is a small block: a top row (label + column) and, when mapped, a
controls row (N|O|Q|T type + contextual transforms). */
.channel {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-3);
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
}
.channelTop {
display: grid;
grid-template-columns: 48px 1fr auto;
grid-template-columns: 44px 1fr;
align-items: center;
gap: var(--space-2);
}
@@ -93,8 +104,15 @@
color: var(--text);
}
.select,
.typeSelect {
.channelControls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
padding-left: 52px;
}
.select {
padding: var(--space-2) var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
@@ -102,14 +120,52 @@
color: var(--text);
font: inherit;
font-size: 13px;
width: 100%;
}
.typeSelect {
/* The N|O|Q|T type control: monospace abbreviations so the four segments line up. */
.typeSeg button {
font-family: var(--font-mono);
font-size: 11px;
padding: var(--space-1) var(--space-2);
}
.transform {
display: inline-flex;
align-items: center;
gap: 6px;
}
.miniLabel {
font-size: 11px;
color: var(--text-placeholder);
}
.mini {
padding: var(--space-1) var(--space-2);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 12px;
}
.toggle {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-secondary);
}
.chartControls {
display: flex;
gap: var(--space-5);
}
.select:focus-visible,
.typeSelect:focus-visible,
.mini:focus-visible,
.dimInput:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
@@ -168,6 +224,15 @@
color: var(--support-warning, var(--text-secondary));
}
/* Explains the disabled Create action (contract 10: a disabled control must say
why). `margin-top: auto` pins it just above the actions so the two read as one. */
.createHint {
margin: auto 0 0;
font-size: 12px;
color: var(--text-secondary);
text-align: right;
}
.actions {
display: flex;
justify-content: flex-end;