feat: paired histogram cells in Storybook (DRC-3390 Stage A)#1398
Conversation
10 captures from `Visualizations/Profile Distribution/*` (5 continuous + 5 discrete; 2 in dark mode via the toolbar). Linked from PR #1398 for at-a-glance review without spinning up Storybook. Signed-off-by: Danyel Fisher <danyel@gmail.com>
…odule Address review findings on PR #1398: 1. Delete the duplicate `components/data/chartColors.ts` and make `theme/chartTheme.ts` the single source of truth. Adds `barLabelColor`/`secondaryTextColor` to `ChartThemeColors`, adds the `ChartBarColors` interface, renames `getBarColors` → `getChartBarColors` (drops the unused `info` field and `INFO_VAL_COLOR` constants). 2. Drop the back-compat re-export block in `HistogramChart.tsx`. All four internal consumers (`HistogramChart`, `TopKBarChart`, the two paired histograms, and the test) now import directly from `../../theme`. `data/index.ts`, `primitives.ts`, and `types/index.ts` no longer duplicate the symbols (they live in `@datarecce/ui/theme`). 3. Normalize the continuous fixtures via a new `continuousFromProportions(edges, baseProps, currentProps, …)` helper so per-bin row proportions sum to 1.0 — tooltip percentages now read as 100% across `continuousOrderAmount`, `continuousStable`, and `continuousAddedOnly` (previously they could sum to >4000%). 4. Replace the hardcoded `#0b1220cc` / `#ffffffcc` trimmed-marker chip background in `PairedHistogramDiscrete` with `${theme.tooltipBackgroundColor}cc` so the chip blends with whatever surface the cell ships into rather than the storybook mock. Story boilerplate cleanup: add `toContinuousProps`, `toDiscreteProps`, `toDiscreteRanksProps` adapters in `fixtures.ts` and use them instead of inlining the snake→camel conversion at every call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Danyel Fisher <danyel@gmail.com>
…arColors Post-review followups on PR #1398: - computeDiscreteSlots and computeRanksSlots now Array.isArray-check values / baseCounts / currentCounts (and baseRanks / currentRanks) before reading .length, so a malformed payload renders an empty SVG instead of crashing the schema row. - Reverted the getBarColors -> getChartBarColors rename; the function and the INFO_VAL_COLOR / INFO_VAL_COLOR_DARK constants are restored to their pre-PR names so @datarecce/ui/theme has zero public surface change on this branch. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Danyel Fisher <danyel@gmail.com>
…3390 Stage A)
Stage A of the DRC-3390 paired-histograms restructure: Storybook-only,
no API or backend dependency. Adds the GA-replacement cell components
for inline profile distributions in SchemaView.
Components shipped:
- PairedHistogramContinuous — quantile-bin constant-area paired histogram;
bar width tracks the bin's quantile span, height tracks density, so bar
area reads as the row-share of that bin.
- PairedHistogramDiscrete — top-K paired histogram with two modes
(discriminated on data.mode): "counts" (default) renders bars sized by
per-side proportion with gap-on-absent semantics; "ranks" renders the
rank-staircase for DuckDB's counts-less approx_top_k payloads.
- ProfileDistributionUnsupportedBanner — one-time MUI Alert for
{status:"unsupported", reason} envelopes (Postgres / MySQL / SQLite /
SQL Server pre-2022).
- pairedHistogramCanvas — shared SVG frame + baseline rule + tooltip
formatters, used by both paired-histogram cells.
Supporting refactors:
- Route HistogramChart / TopKBarChart / PairedHistogramContinuous through
utils/formatters.ts (formatAsAbbreviatedNumber, formatIntervalMinMax)
instead of three near-duplicate local formatters.
- Move ChartBarColors / ChartThemeColors / getChartBarColors /
getChartThemeColors canonical location to theme/chartTheme.ts;
primitives.ts still re-exports them so the public API surface is
unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Danyel Fisher <danyel@gmail.com>
83e8182 to
141faaa
Compare
The `./theme` subpath barrel re-exported `ChartThemeColors` but not its
sibling `ChartBarColors`, even though both live in `chartTheme.ts` and
both are reachable via `@datarecce/ui/primitives`. Consumers following
the inline pointer in `src/index.ts` ("canonical in @datarecce/ui/theme")
to import `ChartBarColors` hit a wall.
One-line fix in the existing re-export block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Danyel Fisher <danyel@gmail.com>
…1398 review Address PR #1398 review feedback (DRC-3390 Stage A): YAGNI (Y1-Y4): - Drop showEndpoints/showMidpoint/showLabels/labelMaxChars props — cell mode is fixed at 140×28, grid mode is out of scope for Stage A. - Drop width/height props — CELL_WIDTH and CELL_HEIGHT are constants. - Drop ariaLabel prop — each cell now uses its hardcoded constant. - Un-export computeContinuousLayout / computeDiscreteSlots / computeRanksSlots from the package barrels; they remain file-level exports so the colocated tests can import them. Style (S2-S4): - Rename pairedHistogramCanvas.tsx → PairedHistogramCanvas.tsx to match the PascalCase convention used by every other file in components/data/. - Replace `${color}cc` string-concatenated alpha with a proper theme field `overlayBackgroundColor` on ChartThemeColors. - Convert 12px → 0.75rem in ProfileDistributionUnsupportedBanner per js/CLAUDE.md (px-for-font-size lint rule). Correctness (C1-C2): - heightForRank: clamp to [0, chartHeight] so a Stage B contract violation (rank > k → negative, rank ≤ 0 → over-tall) renders a well-formed bar instead of a malformed SVG rect. - computeRanksSlots: emit a console.warn alongside the defensive drop, so a Stage B contract violation (value absent from both top-Ks) surfaces in dev tools instead of vanishing silently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Danyel Fisher <danyel@gmail.com>
Now that `showLabels` is gone, `displayValue` on `RenderSlot` is set but never read — the SVG body only consumes `baseH`, `currH`, and `hoverTitle` (which already bakes in the formatted display string). Inline `formatValue(s.value)` at the tooltip call site and drop the field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Danyel Fisher <danyel@gmail.com>
|
Cross-linking from the Stage B backend (#1399) — there's a contract inconsistency here we need to settle together before either lands.
The backend can't fix this with a shared array. All we get back from the warehouse is approx percentiles ( (Related: in Stage B as written, the shared-edge averaging makes Contract both PRs should freeze on:
Purely visual overlay — we're explicitly giving up bin-aligned diffing (no shared "bin 3" to subtract), which is fine for this surface. I'll update the Stage B backend + API types (#1399) to emit per-env edges; this component, its stories, and the |
The continuous cell took a single shared `binEdges`, but constant-area rendering only holds when each env bins on its own quantile edges (see PR #1398 discussion) — with one shared edge array at most one env is constant-area, and the Stage B backend can't honor it without collapsing both densities into one. Replace `binEdges` with independent `baseBinEdges` + `currentBinEdges`. `computeContinuousLayout` now unions both edge sets onto a shared value axis and bars the merged edge grid: every segment lies wholly inside one base bin and one current bin, so both densities are constant across it — which keeps the existing agreement-zone (min) + differential (max-min) renderer working unchanged on the skinnier segments. Constant-area is preserved because subdividing a bin sums its sub-segments back to the bin's area. Fixtures/stories updated to the `base_bin_edges`/`current_bin_edges` payload with genuinely different per-env edges so they exercise the merged grid. The PR-discussion worked example is now a unit test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Danyel Fisher <danyel@gmail.com>
Shared bin edges collapsed base_density and current_density to identical arrays — averaging the two envs' quantiles onto one grid threw away the divergence the paired histogram exists to show, and broke the equal-area property the renderer (PR #1398) depends on. With only approx percentiles from the warehouse (no bin counts), the honest rendering is each env on its OWN quantile edges: density = (1/NUM_BINS)/span. Base and current edges deliberately do not line up; Stage C overlays the two staircases on a shared value axis. - _build_histogram_payload now emits base_bin_edges + current_bin_edges via new _env_edges_and_density helper (fraction-delta / width). - API contract: ProfileDistributionHistogramPayload carries per-env edges. - Drop now-unused _safe_min/_safe_max. - Tests: assert base != current for divergent inputs, equal-area per bin, empty-without-bounds, and the parked point-mass (tie) behaviour. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Danyel Fisher <danyel@gmail.com>
Code Review: PR #1398SHA Stage A is genuinely low-risk: two new SVG leaf cells, a shared canvas helper, a banner, and a chart-theme/formatter consolidation. The new components are defensively written (length-mismatch guards, NaN/degenerate-range fallbacks, rank clamping) and the tests cover merged-edge geometry, gap-on-absent, both palettes, degenerate inputs, and tooltip formatting. Verified locally on BlockersNone. IssuesNone. Notes
Checked and cleared: the |
gcko
left a comment
There was a problem hiding this comment.
GO — 0 blockers, 0 issues, 2 non-blocking notes. Full review: #1398 (comment)
Summary
Stage A of the DRC-3390 paired-histograms 4-stage restructure. Storybook-only, no API or backend
dependency — reviewers can clone this branch, run Storybook, and visually verify the chart cells in isolation.
Why Stage A first
Lowest-risk PR in the restructure: pure additive component-library work, no integration with
SchemaView, no API plumbing, no Python. Reviewablein parallel with Stage B (which owns the backend + API contract). Supersedes the closed #1391
(lifted verbatim minus the wrapper + hook, which move to Stage C).
Components shipped
PairedHistogramContinuous— quantile-bin constant-area paired histogram. Bar widths track quantile spans, heights track density, sobar area reads as the row-share in that bin.
formatValueprop for caller-supplied label formatting.PairedHistogramDiscrete— top-K paired histogram with two rendering modes (discriminated ondata.mode):mode: "counts"(default): paired bars sized by proportion, gap-on-absent semantics — a value with count 0 on one side simply doesn'trender, leaving a visible empty half-slot.
mode: "ranks": rank-staircase for DuckDB's counts-lessapprox_top_kpayloads. Bar height =(k − rank + 1) / k; slots arebase-rank-ordered with current-only values appended right. Stable case reads as two matching descending staircases; order changes show as current
bars zig-zagging against base's smooth descent. Stage B guarantees both-or-neither (no mixed mode).
formatValue?: (v: unknown) => stringprop for caller-supplied label formatting (NULL handling, numeric abbreviation, date rendering). StageC dispatches per
column.type.ProfileDistributionUnsupportedBanner— one-time MUI Alert for{status: "unsupported", reason}envelopes (Postgres / MySQL / SQLite / SQLServer pre-2022).
pairedHistogramCanvas.tsx(shared SVG frame + baseline rule + tooltip formatters),chartColors.ts(theme/palettehelpers extracted from
HistogramChartso the leaf cells don't drag Chart.js into the bundle).CELL_WIDTH = 140,CELL_HEIGHT = 28— sized to fit inside the existing SchemaView row (rowHeight = 35).Test plan
pnpm installclean.pnpm --filter @datarecce/ui type:check— no errors in new files (baseline CSS-module /useTrackLineageRendererrors unchanged).PairedHistogramContinuous,PairedHistogramDiscrete,ProfileDistributionUnsupportedBanner).pnpm --filter @datarecce/storybook buildbuilds clean.pnpm --filter @datarecce/storybook storybook(port 6006) — all 12 stories render.Cell — order amount— bar widths visibly differ across bins (constant-area).Degenerate range— flat checkerboard, no crash.Cell — gap-on-absent— literal visual gaps where one side absent.Cell — trimmed top-K marker— corner "trimmed" marker renders.Ranked

To reproduce:
pnpm install && pnpm --filter @datarecce/storybook storybookfromjs/, then openVisualizations / Profile Distributionin the Storybook sidebar.