refactor(theme): rename chart palette tokens to hue names + unify across themes#2362
refactor(theme): rename chart palette tokens to hue names + unify across themes#2362elizabetdev wants to merge 18 commits into
Conversation
…oss themes Renames the user-facing chart palette tokens from numeric (chart-1..10) to hue-named (chart-blue, chart-orange, ...) so stored chart configs and the upcoming external API surface are self-documenting. Also unifies the categorical palette across HyperDX and ClickStack onto Observable 10; brand identity moves to the new --color-chart-info semantic token plus the existing per-brand --color-chart-success. Stored configs from #2265 keep working: a Zod preprocess on ChartPaletteTokenSchema transparently maps chart-1..10 to their hue-named equivalents at parse time, preserving the HyperDX slot ordering (slot 1 = brand green = chart-green, etc.). User-visible: the color picker now shows readable swatch labels (Blue, Orange, Red, ...) instead of "Color 1", "Color 2", ...
🦋 Changeset detectedLatest commit: b1e5059 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🔴 Tier 4 — CriticalTouches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD. Why this tier:
Additional context: agent branch ( Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
E2E Test Results✅ All tests passed • 192 passed • 3 skipped • 1261s
Tests ran across 4 shards in parallel. |
Deep Review🟡 P2 -- recommended
🔵 P3 nitpicks (5)
Reviewers (10): correctness, testing, maintainability, project-standards, kieran-typescript, api-contract, adversarial, previous-comments, agent-native, learnings-researcher. Testing gaps:
|
…c gradient defs Two regressions reported against the hue-named palette refactor: 1. Stored Number-tile colors from #2265 (chart-1..chart-10) were silently dropped on dashboard load. `useDashboards` raw-casts the API response (`hdxServer('dashboards').json<Dashboard[]>()`), so `ChartPaletteTokenSchema`'s `preprocess` never runs and `isChartPaletteToken('chart-1')` resolves `tileColor` to undefined until the user re-saves the tile. Add a new `resolveChartPaletteToken(value)` helper in common-utils that accepts both current hue-named tokens and legacy chart-1..10 values. Use it in DBNumberChart (last-mile resolution before `getColorFromCSSToken`) and in ColorSwatchInput's `safeValue` guard so the picker also shows the migrated swatch when reopening a tile saved with a legacy token. 2. After unifying `COLORS` to the Observable 10 palette, HyperDX info-level <Area> fills in HDXMultiSeriesTimeChart referenced `url(#time-chart-lin-grad-<id>-00c28a)`, but the gradient <defs> block iterates `COLORS`, which no longer contains the HyperDX brand-green `#00c28a` returned by `getChartColorInfo()`. Info-level series rendered with no area fill. Build the gradient defs from the union of `COLORS` and every `lineData[i].color` actually in use, so any semantic color (`info`/`success`/`warning`/`error`) returned by the per-brand helpers is guaranteed to have a matching gradient def. Regression tests cover both bugs (resolveChartPaletteToken in guards.test.ts; DBNumberChart legacy-token render path; ColorSwatchInput legacy-token displays migrated swatch).
…ors at fetch time The previous fix wrapped `ChartPaletteTokenSchema` in `z.preprocess` to migrate legacy `chart-1`..`chart-10` tokens at parse time. That forces the schema's `z.input` type to `unknown`, which `validateRequest` (from `zod-express-middleware`) then propagates all the way up to `req.body.tiles[i].config.color`. The mismatch with `createDashboard`'s parameter (typed via `z.infer` = output = `ChartPaletteToken | undefined`) broke `packages/api/src/routers/api/dashboards.ts` with TS2345. Strict input/output equality is more important than burying a migration in the schema. So: - Revert `ChartPaletteTokenSchema` to plain `z.enum(CHART_PALETTE_TOKENS)`. `z.input` == `z.output` again; the api compiles. - Move the legacy migration to a fetch-time normalizer in `packages/app/src/dashboard.ts` (`normalizeDashboardTileColors`). `useDashboards` and `fetchLocalDashboards` now run every tile through it, so the in-memory copy is healed immediately and the persisted copy is updated the next time the dashboard is saved for any reason. Unknown strings pass through untouched (no silent data loss); hue tokens pass through unchanged with tile identity preserved. - Keep the render-time `resolveChartPaletteToken` calls in `DBNumberChart` and `ColorSwatchInput` as defense in depth for any tile constructed in memory (presets, unit tests, etc.) that bypasses the fetch path. Tests: - New `fetchLocalDashboards` cases in `dashboard.test.ts` cover the fetch-time normalizer end-to-end (chart-1..10 migrated, hue tokens pass through, unknown strings preserved, tiles without `color` untouched). - `ChartPaletteTokenSchema` test suite tightened: it now asserts that the schema rejects `chart-1`..`chart-10` (with a comment pointing consumers at the app-side normalizer and `resolveChartPaletteToken`). - `resolveChartPaletteToken` tests in `guards.test.ts` are unchanged and still cover the legacy → hue mapping. Docs: - `agent_docs/data_viz_colors.md` rewritten to explain the two-layer migration (fetch + render) and the rationale for not using `z.preprocess`. - Changeset updated.
…leanups
Three follow-ups on Elizabet's chart palette rename PR after the
deep-review pass:
- Drop the orphan `isChartPaletteToken` re-export in
packages/app/src/utils.ts. Knip flagged it; the two app-side
consumers (ColorSwatchInput, DBNumberChart) both moved to
`resolveChartPaletteToken` earlier in the PR, so nothing in
app/ imports it anymore.
- HDXMultiSeriesTimeChart: filter `lineData[].color` to defined
strings before unioning into the gradient `Set`. The downstream
`c.replace('#', '')` would throw on `undefined` if any future
caller bypassed `setLineColors`.
- common-utils: update the JSDoc on `resolveChartPaletteToken` to
describe the real remaining bypass paths (in-memory tile
construction, ChartEditor form state, unit-test fixtures). The
previous comment cited `useDashboards` as the bypass, but the
new fetch-time `normalizeDashboardTileColors` heals
`useDashboards` / `fetchLocalDashboards` payloads, so the
rationale needed a refresh.
Validated locally: lint, tsc, knip clean; common-utils unit
(1145/1145) and app unit across the touched surface area
(627/627) pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Pushed e103ad3 to take three of the deep-review items off your plate. Knip: dropped the orphan P3 defensive filter on the gradient P3 stale JSDoc on Validated locally: lint, tsc, knip clean; common-utils unit (1145/1145) and app unit across the touched surface area (627/627) pass. What I deliberately did not touch and left for you:
LMK if you want me to pick up any of the above. |
…lor retrieval Updated the data visualization color documentation to clarify the unified categorical color palette based on Observable 10, with `chart-blue` adjusted to match the brand link color. The JavaScript source of truth for categorical hues is now emphasized, eliminating unnecessary CSS variable reads. The `getColorFromCSSToken` and `getColorFromCSSVariable` functions have been refactored to directly return values from `CATEGORICAL_HEX_BY_TOKEN`, improving performance by avoiding layout reads. Tests have been updated to reflect these changes, ensuring consistent behavior across themes.
…nd ClickStack info alignment Co-authored-by: Cursor <cursoragent@cursor.com>
The 10 categorical chart hues are unified across HyperDX and ClickStack, so duplicating them in each theme's `chart-tokens` mixin was redundant. Extract them into a single shared `_chart-categorical-tokens.scss` partial that both theme files `@use` and `@include`. Per-brand semantic tokens (success, info, warning, error) stay local to each theme's mixin, since those genuinely vary by brand. The compiled CSS is byte-identical to before — only comments differ, and selector specificity is preserved because Sass still inlines the mixin body at each `[data-mantine-color-scheme]` call site. Net -47 lines across the theme files despite adding the new partial. Also update the JS-side sync comment in utils.ts to point at the new shared SCSS source and refresh agent_docs/data_viz_colors.md (file reference table, pre-merge checklist, "add a new hue" recipe now require one SCSS edit instead of two). Co-authored-by: Cursor <cursoragent@cursor.com>
Address Deep Review P2 findings on the chart palette rename PR:
1. PATCH/POST validation hole — the fetch-time normalizer only healed
in-memory copies, so dashboards constructed outside the fetch path
(JSON imports via DBDashboardImportPage, presets, MCP payloads)
carried legacy `chart-1`..`chart-10` straight into the strict
server-side `ChartPaletteTokenSchema` and returned a Zod 400. Apply
`normalizeDashboardTileColors` symmetrically at write time in
`useUpdateDashboard` and `useCreateDashboard` (both local and remote
branches). This also converges the DB-side data on next save instead
of holding legacy tokens in storage forever. The normalizer is now
generic over `T extends { tiles?: Tile[] }` and returns the payload
unchanged when tiles is omitted, so partial PATCHes still work.
2. ClickStack legacy slot-flip — pre-rename ClickStack used a different
slot ordering than HyperDX (`--color-chart-1` was brand blue, not
brand green; `--color-chart-2` was orange, not blue; etc.). Since
the migration map preserves HyperDX slot ordering, any ClickStack
dashboard saved via #2265 will visually shift. We chose this
trade-off deliberately over branching the legacy map by
`detectActiveTheme()`: `LEGACY_CHART_PALETTE_TOKEN_MAP` lives in
common-utils (shared with the API), and migration is one-shot
persisted on next save — theme-branching would couple common-utils
to browser DOM state and still produce wrong results for users
whose active theme changed since the original pick. Documented the
trade-off prominently in the changeset and agent_docs so reviewers
and future maintainers can find it.
3. Remote-path test coverage — the existing dashboard test suite mocks
`IS_LOCAL_MODE: true` for the entire file, so the
`hdxServer('dashboards').json<Dashboard[]>().then(...).map(
normalizeDashboardTileColors)` branch had no coverage. Added
`dashboard.remote.test.ts` (separate file because `jest.mock` is
hoisted and per-test isolation via `jest.isolateModulesAsync` proved
unreliable for swapping the constant). Exported `fetchDashboards`
so it can be exercised directly. Tests cover: legacy migration on
the remote path, hue-named tokens passing through unchanged, and
tile identity preservation for the React reconciliation hot path.
Co-authored-by: Cursor <cursoragent@cursor.com>
…ip orphan Two follow-ups on #2362 after the latest pass: - Re-drop the `isChartPaletteToken` re-export in packages/app/src/utils.ts. The earlier dedupe-and-cleanup commit removed it, but the subsequent "unify categorical color palette and streamline color retrieval" pass re-added it as part of the reordered export block. Nothing in `packages/app/` imports it (both former call sites moved to `resolveChartPaletteToken`), the common-utils-level export remains for `guards.test.ts`. Knip CI flagged this on the most recent push. - Add a `dashboard.remote.test.ts` companion that exercises `fetchDashboards` in the non-local branch (`hdxServer('dashboards') .json<>()`). Symmetric coverage with the existing local-path suite in `dashboard.test.ts`; closes the deep-review's fetch-time normalizer testing gap (chart-1..chart-10 migration, hue passthrough, unknown preserved, missing-color skipped). Split into a separate file because `IS_LOCAL_MODE` is bound at module load and `jest.doMock` inside `jest.isolateModules` did not override the hoisted `jest.mock('../config', ...)` factory reliably here; a second file with its own top-level mocks is the cleanest fix. Required exporting `fetchDashboards` from `packages/app/src/dashboard.ts` (it's a thin wrapper around the same `normalizeDashboardTileColors` the existing local-path tests exercise). Comment flags it as test-only; the React layer should stay on `useDashboards`. Validated locally: lint, tsc, knip clean; common-utils unit (1145/1145), and the touched app suites (dashboard, dashboard.remote, utils, ColorSwatchInput, DBNumberChart) all pass (147/147 across the slice). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Pushed 78cfce7 with two more follow-ups after your latest pass: Knip regression revert: P2.3 deep-review item (remote I tried first to add the remote suite into the existing Validated locally: lint, tsc, knip clean; common-utils unit (1145/1145), and the touched app suites all pass (147 across Still on your plate from the deep review:
LMK if you want me to pick any of these up. |
…al cyan Previously HyperDX `--color-chart-info` and `--color-chart-success` both resolved to the brand green `#00c28a`, so info-level series and success fills collapsed into the same color. Point `info` at Observable cyan (`#6cc5b0`, the categorical `chart-cyan` hex) instead. Now both brands follow the same pattern of having `info` reuse a categorical hue (HyperDX → cyan, ClickStack → blue), and HyperDX's two semantic tokens are visually distinct. - HyperDX `_tokens.scss`: update `--color-chart-info` and the per-brand docblock describing the success/info split. - `utils.ts`: mirror the new hex in `SEMANTIC_CHART_PALETTE.hyperdx.info` so the SSR/`getComputedStyle`-failure fallback matches the SCSS. - `agent_docs/data_viz_colors.md`: update the semantic palette description and the HyperDX per-theme bullets. Co-authored-by: Cursor <cursoragent@cursor.com>
…xio/hyperdx into agent/rename-chart-palette-tokens Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # packages/app/src/__tests__/dashboard.remote.test.ts # packages/app/src/dashboard.ts
Surface `--color-chart-info` in the semantic swatch list (Brand toolbar aware) and add an Info Chart Colors By Brand story that compares HyperDX cyan (#6cc5b0) vs ClickStack blue (#437eef) side by side. Also fix the stale getChartColorInfo() JSDoc. Co-authored-by: Cursor <cursoragent@cursor.com>
Update the rename-chart-palette-tokens changeset and data_viz_colors Storybook reference so HyperDX `--color-chart-info` is documented as Observable cyan (#6cc5b0), distinct from success green, and the new InfoChartColorsByBrand story is listed. Co-authored-by: Cursor <cursoragent@cursor.com>
…Stack
`--color-chart-success` and `--color-chart-info` now resolve to categorical
`chart-green` (`#3ca951`) and `chart-blue` (`#437eef`) on both brands. Warning
and error were already shared. With the categorical palette also unified, chart
colors carry no brand identity — that role moves to non-chart UI chrome
(Mantine accent, sidebar gradient, Click UI globals).
- Move semantic chart tokens into a shared `chart-semantic-tokens` SCSS mixin
in `_chart-categorical-tokens.scss` alongside the existing categorical
mixin. Each theme's `chart-tokens` is now a 2-line `@include` of both
shared mixins; the previously-duplicated semantic block is gone. Per-brand
overrides remain possible by declaring the var after the shared `@include`.
- `SEMANTIC_CHART_PALETTE.hyperdx.{success,info}` in `utils.ts` updated to
`#3ca951` / `#437eef` to mirror the SCSS (ClickStack entries were already
these hexes for success and info post-#2362).
- Remove the redundant `InfoChartColorsByBrand` Storybook story and the
per-brand info constants — both columns rendered identical swatches once
info unified. Semantic preview now describes all four tokens as unified.
- Update docs (`agent_docs/data_viz_colors.md`, changeset, JSDoc, inline
comments) to reflect the unified semantic layer and shared-mixin layout.
- Update `utils.test.ts` SSR-fallback expectation to `#3ca951`.
HyperDX brand green (`#00c28a`) is unchanged for Mantine accent, buttons, and
sidebar gradient — only chart `success` moved off it.
Co-authored-by: Cursor <cursoragent@cursor.com>
Address all P0/P1, P2, and P3 items from the deep review on #2362. P0/P1: - DBDashboardImportPage now normalizes legacy chart-N tile colors before the strict DashboardTemplateSchema.safeParse, so templates exported from a pre-rename deploy import without an opaque enum error. P2: - Dashboards POST/PATCH routes mount a migrateLegacyDashboardTileColors middleware that rewrites legacy chart-N before validateRequest runs ChartPaletteTokenSchema. Catches non-React clients (stale-bundle tabs during rolling deploys, CI scripts, MCP, the upcoming external-API parity work) for a one-release deprecation window without weakening z.input/z.output equality. Same shim applied to the on-disk dashboard provisioner task. - Remove duplicate useDashboards/useCreateDashboard/useUpdateDashboard/ useDeleteDashboard from packages/app/src/api.ts; Spotlights.tsx now reaches for the canonical normalizer-guarded useDashboards in packages/app/src/dashboard.ts. Closes the stale React Query cache race surface. - Add mutation-level tests for useCreateDashboard / useUpdateDashboard that capture the mutationFn and assert hue tokens in the outbound hdxServer body. Plus standalone normalizeRawDashboardTileColors tests covering the JSON-import walker. Plus a server-side integration test for the dashboards-router migration shim. - Tighten CATEGORICAL_PALETTE_TOKENS / SEMANTIC_PALETTE_TOKENS to tuple literals (were `readonly ChartPaletteToken[]`, leaked semantic tokens into the categorical subset). Introduce CategoricalChartPaletteToken and add a forward `satisfies Record<...>` + reverse completeness check on CATEGORICAL_HEX_BY_TOKEN so any future drift between the JS hex map and the shared enum becomes a compile error. Also drops the manual `as keyof typeof` cast in the COLORS map. - Extract collectMemoChartGradientHexes from the MemoChart defs JSX and unit-test it: categorical baseline, semantic hex union, dedup, and the undefined-filter guard. Recharts renders awkwardly in jsdom at the container-sized SVG layer, so the helper is the pinnable contract. - Add inline pointer next to LEGACY_CHART_PALETTE_TOKEN_MAP for the ClickStack slot flip (already in changeset + agent_docs). - Append color-chart-info and color-chart-success-highlight to semanticColorsGrouped.ts so designers see them in the inventory. - Replace getChartColorInfo() / getChartColorError() in ChartUtils test expectations with concrete hexes (#437eef, #ff725c) so a helper regression can't pass-by-mirroring. P3: - CHART_PALETTE_TOKENS docstring updated to clarify chart-info is a render-time CSS var, not a picker enum value. - semanticTokenFallback parameter narrowed to non-categorical tokens and the default branch now does an exhaustive `never` assertion (was returning COLORS[0] from an unreachable path). - Collapse SEMANTIC_CHART_PALETTE to a SemanticChartHexes typed shape so both brand entries are pinned to parity through the type system. - Alias --color-chart-warning / --color-chart-error through their matching categorical hue vars (var(--color-chart-orange) / var(--color-chart-red)) for symmetry with success/info. - fetchDashboards export comment rewritten to clarify it's the shared queryFn (not test-only). - LEGACY_CHART_PALETTE_TOKEN_MAP keyed by a narrow LegacyChartPalette TokenKey union so a typo in a legacy slot at edit time is a compile error. - Drop the double cast `tile.config as { color?: unknown }` in normalizeDashboardTileColors now that SavedChartConfig types color properly. - Symmetric normalization on the URL-state local setDashboard read + write paths. - Parameterize the legacy slot test across all 10 slots in both the local and remote dashboard suites. - Strip unresolvable color tokens (stale hexes, hand-edited values, future-rolled-back tokens) at the normalizer boundary so they don't sail into the strict server-side schema and 400 the next save. Validated: lint clean across app/api/common-utils; tsc --noEmit clean across app/api/common-utils; common-utils unit (1163/1163), app unit (1793 passed / 9 skipped). The new dashboard-router migration test type-checks and needs Docker (`make dev-int FILE=dashboard`) to run. Co-authored-by: Cursor <cursoragent@cursor.com>
P2: - Migrate legacy chart-N in GET dashboards controllers so the wire format emits canonical hue-named tokens. Pre-rename Mongo docs can no longer round-trip via non-React HTTP callers (GET -> unmodified PATCH) and 400 the strict ChartPaletteTokenSchema. - Add integration test that seeds a chart-1 dashboard directly into Mongo, then asserts both list and single GET return chart-green. - Revert silent strip of unresolvable colors in normalizeDashboardTileColors. Stale hexes / hand-edited values / forward-rolled future tokens now pass through so the strict server-side schema surfaces a clear error on next save instead of the normalizer quietly dropping the user's chosen color. Matches the import-time policy of normalizeRawDashboardTileColors. - Add provisioner test pinning the inlined walker + non-array tiles early-return. P3: - Extract walkRawDashboardTileColors into common-utils. Four call sites (React fetch/write normalizer, JSON import pre-validation, API write shim, provisioner) now compose over a single per-tile traversal, with the migration policy supplied as a callback. Adds dedicated unit tests pinning the walker contract. - Drop the no-op `as readonly CategoricalChartPaletteToken[]` cast on the COLORS construction. CATEGORICAL_PALETTE_TOKENS is already a readonly tuple of the narrow union so the .map callback is exhaustive without further assertion. - Trim SEMANTIC_CHART_PALETTE comment to the parity-enforcement justification only (drop the aspirational "future override hook" framing). - Add a one-line comment on DBRowTable's PatternTrendChart explaining why the per-row getChartColorInfo() call is intentionally not memoized (memoization would require a theme-class subscription this component doesn't already have, and the per-row getComputedStyle cost is sub-microsecond). Co-authored-by: Cursor <cursoragent@cursor.com>
The dashboards router only exposes a list GET handler; single-dashboard reads on the React side go through `useDashboards` and filter client-side, so there is no `GET /dashboards/:id` route. The new regression test asserted against both endpoints and the single GET hit the express default 404. Drop the single-GET assertion; the list path still exercises `getDashboards` → `healLegacyDashboardTileColors`, and `getDashboard` is still covered in-process via `updateDashboard`'s PATCH-time read. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Renames the chart palette tokens introduced in #2265 from numeric (
chart-1…chart-10) to hue-named (chart-blue,chart-orange,chart-red,chart-cyan,chart-green,chart-pink,chart-purple,chart-light-blue,chart-brown,chart-gray) and unifies the categorical palette across HyperDX and ClickStack onto Observable 10, withchart-blueswapped to the brand link color#437eef. Also unifies semantic chart tokens (success,warning,error,info) across both brands.Why now: the API parity follow-up (#2359) hasn't shipped yet, so this is the last window where the schema is internal-only. Once
chart-1..10lands on the external API surface, renaming becomes a breaking API change. Doing it now is a zero-cost internal cleanup.Why hue names: the user-facing picker shipped with placeholder labels ("Color 1", "Color 2", …) because numeric tokens had no semantic content. Hue-named tokens are self-documenting in the picker UI, in stored configs, and (soon) in the public API.
Why unify the palette: per-theme divergent slot-1 (HyperDX green vs ClickStack blue) was a clever trick but invisible to users — the picker just showed swatches with opaque labels. With hue names,
chart-blueresolving to a different color per theme would be a bug, not a feature.Why unify the semantic layer too: pre-rename,
successandinforesolved to different hexes per brand (HyperDX brand green vs Observable green for success; HyperDX green vs ClickStack blue for info). With the categorical palette unified, having only the semantic layer diverge by brand stopped being a meaningful identity signal — chart colors moved entirely off brand identity duty, while non-chart UI chrome (Mantine accent, sidebar gradient, Click UI globals) still differentiates the two brands. Picking categorical hues for the semantic vars (success→chart-green#3ca951,info→chart-blue#437eef) keeps the chart palette internally consistent on both brands.Why customize
chart-blue: pure Observable blue (#4269d0) is close to but not identical with the brand link color (#437eef, already used for--click-global-color-text-link-defaultand outline). Aligning the categoricalchart-bluewith the brand blue means primary-series charts, link-styled UI, and info-level logs all read as the same hue.What changes
Categorical palette (unified)
--color-chart-{hue}to the same hex — based on Observable 10 withchart-blueswapped to#437eef. All other hues unchanged from Observable 10.CATEGORICAL_HEX_BY_TOKENinutils.tsreplaces the dualCHART_PALETTE+CLICKSTACK_CHART_PALETTEfor categorical colors.COLORSis derived fromCATEGORICAL_PALETTE_TOKENSso positional access stays consistent.getColorFromCSSVariable(i)andgetColorFromCSSToken('chart-{hue}')short-circuit straight fromCATEGORICAL_HEX_BY_TOKEN, skipping thegetComputedStyleround-trip — since the palette is unified across themes, the DOM read was buying back the same hex via a per-series layout read. The--color-chart-{hue}CSS vars remain in the shared SCSS partial as a stylesheet-author affordance (one inlinevar()consumer inLogLevel.tsx, plus devtools inspection) and a future per-brand override hook.Semantic palette (unified)
--color-chart-infotoken preserves the "info-level logs render brand-primary" idiom from before — both brands now resolveinfoto categoricalchart-blue(#437eef).--color-chart-successresolves to categoricalchart-green(#3ca951) on both brands (was HyperDX brand green#00c28a/ ClickStack Observable green#3ca951).--color-chart-warning(#efb118) and--color-chart-error(#ff725c) were already shared.chart-semantic-tokensSCSS mixin alongside the categorical mixin in_chart-categorical-tokens.scss. Each theme'schart-tokensmixin just@includes both mixins — no duplicated semantic block per brand.SEMANTIC_CHART_PALETTEinutils.tskeeps a{hyperdx, clickstack}shape for SSR fallback (currently identical) so a future per-brand override is a one-line change in both layers.getChartColorInfo()helper backs the--color-chart-infotoken. The threegetColorFromCSSVariable(0)callsites previously commented as "primary chart color (blue for ClickStack, green for HyperDX)" now route throughgetChartColorInfo()so the semantic stays intact under unified categorical ordering.DBRowTable's pattern-trend bar fill switches fromCOLORS[0]togetChartColorInfo(), preserving the brand-primary intent under the unified palette.Schema + migration (stored configs from #2265 keep working)
CHART_PALETTE_TOKENSincommon-utils/types.tsbecomes the hue-named enum.LEGACY_CHART_PALETTE_TOKEN_MAPrecords the HyperDX slot-order mapping (chart-1 → chart-green,chart-2 → chart-blue, …,chart-10 → chart-gray).ChartPaletteTokenSchemastays strict — a plainz.enum(CHART_PALETTE_TOKENS). Wrapping it inz.preprocesswas tempting but would force the schema'sz.inputtype tounknown, which poisonsvalidateRequest'sreq.bodyinference all the way up toDashboard.tiles[i].config.colorin the API package. Legacy data heals at three complementary layers instead:normalizeDashboardTileColorsinpackages/app/src/dashboard.tswalks every tile in the response fromuseDashboards/fetchLocalDashboards/fetchDashboards(remote path covered bydashboard.remote.test.ts) and rewrites any legacyconfig.colorto its hue-named equivalent viaresolveChartPaletteToken.useUpdateDashboard/useCreateDashboardalso run the normalizer before persisting, so JSON imports, presets, and MCP-constructed payloads pass the strict server-side validator and converge stored data on next save.DBNumberChartandColorSwatchInputalso callresolveChartPaletteTokenas belt-and-suspenders for any code path that bypasses the fetch normalizer (e.g. an in-memory tile constructed from aTileliteral).guards.test.tsasserts every legacy slot maps to its expected target and that unknown strings still fail validation.UI
ColorSwatchInput'sTOKEN_LABELSswitches from "Color 1" … "Color 10" to "Blue", "Orange", "Red", … — the picker is now self-documenting.ChartColors.stories.tsxderives swatches fromCATEGORICAL_PALETTE_TOKENSso adding a hue requires no story edits beyond the label. Semantic preview includesInfo (Blue)and notes that all four semantic tokens are unified across brands.SCSS structure
_chart-categorical-tokens.scssexports two mixins:chart-categorical-tokens(10 hues) andchart-semantic-tokens(success / warning / error / info + highlights, withsuccessandinfoaliased tovar(--color-chart-green)/var(--color-chart-blue))._tokens.scsschart-tokensmixin is a 2-line@includeof both shared mixins — no per-brand semantic duplication. A future per-brand override is a single declaration after the shared@includes in the relevant brand's mixin.Docs
agent_docs/data_viz_colors.mdrewritten end-to-end: TL;DR table, "Where the colors live" section, recipes, anti-patterns, and pre-merge checklist all reflect the unified palette + shared mixin structure. Also documents the JS-source-of-truth design for categorical hues, the brand-blue customization, and the per-layer migration strategy.What does NOT change
--color-chart-warningand--color-chart-errorkeep their hex values.#00c28a) is still the Mantine accent, button, and sidebar gradient color — only chartsuccessmoved off it to categorical Observable green.getChartColorSuccess/Warning/Error/Info,getColorProps,logLevelColor,semanticKeyedColor, etc.) keep the same signatures and return types.ColorSwatchInput, same schema field.Visible color changes
chart-blue#437eefvia--color-chart-info(was brand green#00c28avia SSR-fallback collapse with success).chart-green#3ca951via--color-chart-success(was brand green#00c28a). Non-chart success UI (buttons, etc.) still uses brand green via Mantine.chart-blue#437eefvia--color-chart-info. Previously rendered as Observable blue (#4269d0) at runtime via the per-brand--color-chart-1, with an SSR fallback bug that returned HyperDX green — this PR cleans that up AND aligns it with the brand link color in one move.chart-bluecategorical: hex changes from Observable blue (#4269d0) to brand blue (#437eef) on both themes. Any saved tile that explicitly picked the (then-numeric) blue slot will now render the brand blue. Legacychart-2saves on HyperDX map tochart-blueviaLEGACY_CHART_PALETTE_TOKEN_MAPand pick up the new hex.COLORS[0](positional palette index, e.g. for SSR placeholders): was#00c28a(HyperDX brand green) on both themes, now#437eef(brand blue) on both themes — but in browser, all real chart series resolve via the helpers anyway, so this only affects the SSR window for unkeyed positional access.Follow-ups (not in this PR)
LEGACY_CHART_PALETTE_TOKEN_MAPmigration can be revisited (e.g. one-shot DB migration to rewrite stored values, then drop the resolver and the fetch/write-time normalizers).Test plan
yarn workspace @hyperdx/common-utils ci:unitclean.yarn workspace @hyperdx/appunit clean.yarn workspace @hyperdx/app lintclean.yarn workspace @hyperdx/common-utils lintclean.yarn workspace @hyperdx/app tsc --noEmitclean.#3ca951,#efb118,#ff725c,#437eef) on both Brand toolbar values.color: chart-Nin the config) and confirm it still renders the expected hue viaresolveChartPaletteToken(render-time) and gets healed bynormalizeDashboardTileColors(fetch-time + write-time).Closes the renaming half of the discussion that was previously explored in (now-closed) #2276. The "unify" half also lands here, scoped across both the categorical and semantic layers — chart color identity moves entirely off brand identity duty.