From 00f21916f80679449b8da5f5445a3d9b1f21a48e Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Wed, 13 May 2026 14:19:51 +0100 Subject: [PATCH 1/6] refactor(theme): unify chart palette across themes via shared partial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both themes now consume the same chart palette (categorical 1-10 + semantic success/warning/error + their highlight variants) from a new `packages/app/src/theme/themes/_chart-tokens.scss` partial. Previously the values lived in four byte-similar-but-not-identical blocks across `hyperdx/_tokens.scss` and `clickstack/_tokens.scss` (dark + light each). Behavior change for HyperDX: the categorical palette now leads with ClickStack-style blue (`#437eef`) instead of HyperDX brand green (`#00c28a`), and semantic success becomes Observable green (`#3ca951`) rather than brand green. Single-series HyperDX charts will render in blue going forward; status pills using `getChartColorSuccess()` shift to the more saturated Observable green. This continues the Click UI convergence — chart-color identity stops being a brand differentiator between the two themes. Implementation: - New `_chart-tokens.scss` defines three mixins: `categorical-chart-tokens`, `semantic-chart-tokens`, and the convenience `chart-tokens` that includes both. The same comment about CSS specificity that previously lived inline in `clickstack/_tokens.scss` moves there to explain why scheme blocks must redeclare these vars. - `hyperdx/_tokens.scss` adds `@use '../chart-tokens';` and replaces both inline chart blocks (dark + light) with `@include chart-tokens.chart-tokens;`. Net loss of ~40 lines. - `clickstack/_tokens.scss` does the same. Net loss of another ~40 lines. The "duplication is required" comment is gone now that the duplication is centralized. - `_base-tokens.scss` is unchanged. It already `@include`s `hyperdx-tokens.dark-mode-tokens` for SSR; that mixin transitively includes the chart tokens, so the SSR fallback layer keeps working. JS palette consolidation in `utils.ts` (CHART_PALETTE + CLICKSTACK_CHART_PALETTE -> single CHART_PALETTE, COLORS reorder, detectActiveTheme cleanup) and storybook + doc updates land in follow-up commits in this PR. Verified each partial compiles via `npx sass`. Co-authored-by: Cursor --- .../app/src/theme/themes/_chart-tokens.scss | 52 +++++++++++++++++ .../src/theme/themes/clickstack/_tokens.scss | 58 ++++--------------- .../app/src/theme/themes/hyperdx/_tokens.scss | 56 ++++-------------- 3 files changed, 74 insertions(+), 92 deletions(-) create mode 100644 packages/app/src/theme/themes/_chart-tokens.scss diff --git a/packages/app/src/theme/themes/_chart-tokens.scss b/packages/app/src/theme/themes/_chart-tokens.scss new file mode 100644 index 0000000000..b2c2fc04f8 --- /dev/null +++ b/packages/app/src/theme/themes/_chart-tokens.scss @@ -0,0 +1,52 @@ +/* Shared Chart Color Tokens + * + * The categorical and semantic chart palettes are intentionally identical + * across the HyperDX and ClickStack themes. Rather than duplicate hex + * values across four scheme blocks (HyperDX dark/light + ClickStack + * dark/light), both themes `@use` this partial and `@include` the + * `chart-tokens` mixin inside their own scope selectors. + * + * Source: Observable 10 categorical palette + * https://observablehq.com/@d3/color-schemes + * + * Note: chart vars do not currently differ between dark and light schemes. + * If that changes, split this into `chart-tokens-dark` / `chart-tokens-light` + * and adjust the per-theme `@include`s accordingly. + */ + +@mixin categorical-chart-tokens { + /* Categorical palette - 10 distinct, accessible hues + * NOTE: These vars are intentionally duplicated into both dark and light + * scheme blocks via the consuming theme files. CSS specificity requires + * them to be defined within each [data-mantine-color-scheme] selector; + * declaring them only on `.theme-*` would cause the scheme-scoped block + * to take precedence and reset them to undefined. */ + --color-chart-1: #437eef; /* Blue - Primary */ + --color-chart-2: #efb118; /* Orange */ + --color-chart-3: #ff725c; /* Red */ + --color-chart-4: #6cc5b0; /* Cyan */ + --color-chart-5: #3ca951; /* Green */ + --color-chart-6: #ff8ab7; /* Pink */ + --color-chart-7: #a463f2; /* Purple */ + --color-chart-8: #97bbf5; /* Light Blue */ + --color-chart-9: #9c6b4e; /* Brown */ + --color-chart-10: #9498a0; /* Gray */ +} + +@mixin semantic-chart-tokens { + /* Semantic chart colors */ + --color-chart-success: #3ca951; /* Green */ + --color-chart-warning: #efb118; /* Orange */ + --color-chart-error: #ff725c; /* Red */ + + /* Semantic chart colors - highlighted (hover/selection states) */ + --color-chart-success-highlight: #80d9b3; + --color-chart-warning-highlight: #f5c94d; + --color-chart-error-highlight: #ffa090; +} + +/* Convenience mixin that includes both categorical and semantic tokens. */ +@mixin chart-tokens { + @include categorical-chart-tokens; + @include semantic-chart-tokens; +} diff --git a/packages/app/src/theme/themes/clickstack/_tokens.scss b/packages/app/src/theme/themes/clickstack/_tokens.scss index 0c77f5498c..83751397df 100644 --- a/packages/app/src/theme/themes/clickstack/_tokens.scss +++ b/packages/app/src/theme/themes/clickstack/_tokens.scss @@ -4,6 +4,12 @@ /* Uses Click UI design tokens */ +/* Chart color tokens (categorical + semantic) are intentionally shared + * with the HyperDX theme. They live in `_chart-tokens.scss` and are + * `@include`d below so a single hex change updates both themes. */ + +@use '../chart-tokens'; + /* Dark Mode */ .theme-clickstack[data-mantine-color-scheme='dark'] { /* Brand Palette - Yellow/Gold */ @@ -172,33 +178,8 @@ --color-json-array: #ffd966; --color-json-punctuation: #666980; - /* - * Chart Colors - Observable 10 categorical palette - * NOTE: These colors are intentionally duplicated in both dark and light mode sections. - * CSS specificity requires them to be defined within each [data-mantine-color-scheme] selector - * to ensure they're applied correctly. A shared .theme-clickstack section would have lower - * specificity and could be overridden by other styles. - */ - --color-chart-1: #437eef; /* Blue - Primary */ - --color-chart-2: #efb118; /* Orange */ - --color-chart-3: #ff725c; /* Red */ - --color-chart-4: #6cc5b0; /* Cyan */ - --color-chart-5: #3ca951; /* Green */ - --color-chart-6: #ff8ab7; /* Pink */ - --color-chart-7: #a463f2; /* Purple */ - --color-chart-8: #97bbf5; /* Light Blue */ - --color-chart-9: #9c6b4e; /* Brown */ - --color-chart-10: #9498a0; /* Gray */ - - /* Chart Semantic Colors */ - --color-chart-success: #3ca951; /* Green */ - --color-chart-warning: #efb118; /* Orange */ - --color-chart-error: #ff725c; /* Red */ - - /* Chart Semantic Colors - Highlighted (for hover/selection states) */ - --color-chart-success-highlight: #80d9b3; - --color-chart-warning-highlight: #f5c94d; - --color-chart-error-highlight: #ffa090; + /* Chart Colors - shared across themes via the chart-tokens partial. */ + @include chart-tokens.chart-tokens; /* Mantine Overrides */ --mantine-color-body: var(--color-bg-body) !important; @@ -379,27 +360,8 @@ --color-json-array: #997300; --color-json-punctuation: #868e96; - /* Chart Colors - See dark mode section for explanation of why duplication is required */ - --color-chart-1: #437eef; /* Blue - Primary */ - --color-chart-2: #efb118; /* Orange */ - --color-chart-3: #ff725c; /* Red */ - --color-chart-4: #6cc5b0; /* Cyan */ - --color-chart-5: #3ca951; /* Green */ - --color-chart-6: #ff8ab7; /* Pink */ - --color-chart-7: #a463f2; /* Purple */ - --color-chart-8: #97bbf5; /* Light Blue */ - --color-chart-9: #9c6b4e; /* Brown */ - --color-chart-10: #9498a0; /* Gray */ - - /* Chart Semantic Colors */ - --color-chart-success: #3ca951; /* Green */ - --color-chart-warning: #efb118; /* Orange */ - --color-chart-error: #ff725c; /* Red */ - - /* Chart Semantic Colors - Highlighted (for hover/selection states) */ - --color-chart-success-highlight: #80d9b3; - --color-chart-warning-highlight: #f5c94d; - --color-chart-error-highlight: #ffa090; + /* Chart Colors - shared across themes via the chart-tokens partial. */ + @include chart-tokens.chart-tokens; /* Mantine Overrides */ --mantine-color-body: var(--color-bg-body) !important; diff --git a/packages/app/src/theme/themes/hyperdx/_tokens.scss b/packages/app/src/theme/themes/hyperdx/_tokens.scss index 275e1b3e90..1e8456a0fc 100644 --- a/packages/app/src/theme/themes/hyperdx/_tokens.scss +++ b/packages/app/src/theme/themes/hyperdx/_tokens.scss @@ -6,8 +6,14 @@ * These mixins define all design tokens for HyperDX theme. * They are used both here (for .theme-hyperdx scoped selectors) and * in _base-tokens.scss (for unscoped fallback selectors during SSR). + * + * Chart color tokens (categorical + semantic) are intentionally shared + * with the ClickStack theme. They live in `_chart-tokens.scss` and are + * `@include`d here so a single hex change updates both themes. */ +@use '../chart-tokens'; + @mixin dark-mode-tokens { /* Backgrounds */ --color-bg-body: var(--mantine-color-dark-9); @@ -92,29 +98,10 @@ --color-json-array: var(--mantine-color-green-3); --color-json-punctuation: var(--mantine-color-dark-4); - /* Chart Colors - Brand green first, then Observable palette - Note: 1-indexed CSS vars map to 0-indexed COLORS array in utils.ts - e.g., --color-chart-1 corresponds to COLORS[0] */ - --color-chart-1: #00c28a; /* Green - Brand (primary) */ - --color-chart-2: #4269d0; /* Blue */ - --color-chart-3: #efb118; /* Orange */ - --color-chart-4: #ff725c; /* Red */ - --color-chart-5: #6cc5b0; /* Cyan */ - --color-chart-6: #ff8ab7; /* Pink */ - --color-chart-7: #a463f2; /* Purple */ - --color-chart-8: #97bbf5; /* Light Blue */ - --color-chart-9: #9c6b4e; /* Brown */ - --color-chart-10: #9498a0; /* Gray */ - - /* Chart Semantic Colors */ - --color-chart-success: #00c28a; /* Green - Brand */ - --color-chart-warning: #efb118; /* Orange */ - --color-chart-error: #ff725c; /* Red */ - - /* Chart Semantic Colors - Highlighted (for hover/selection states) */ - --color-chart-success-highlight: #80d9b3; - --color-chart-warning-highlight: #f5c94d; - --color-chart-error-highlight: #ffa090; + /* Chart Colors - shared across themes via the chart-tokens partial. + 1-indexed CSS vars map to 0-indexed COLORS array in utils.ts + (e.g., --color-chart-1 corresponds to COLORS[0]). */ + @include chart-tokens.chart-tokens; /* Mantine Overrides */ --mantine-color-body: var(--color-bg-body) !important; @@ -206,27 +193,8 @@ --color-json-array: var(--mantine-color-green-7); --color-json-punctuation: var(--mantine-color-dark-5); - /* Chart Colors - Brand green first, then Observable palette (same for light mode) */ - --color-chart-1: #00c28a; /* Green - Brand (primary) */ - --color-chart-2: #4269d0; /* Blue */ - --color-chart-3: #efb118; /* Orange */ - --color-chart-4: #ff725c; /* Red */ - --color-chart-5: #6cc5b0; /* Cyan */ - --color-chart-6: #ff8ab7; /* Pink */ - --color-chart-7: #a463f2; /* Purple */ - --color-chart-8: #97bbf5; /* Light Blue */ - --color-chart-9: #9c6b4e; /* Brown */ - --color-chart-10: #9498a0; /* Gray */ - - /* Chart Semantic Colors */ - --color-chart-success: #00c28a; /* Green - Brand */ - --color-chart-warning: #efb118; /* Orange */ - --color-chart-error: #ff725c; /* Red */ - - /* Chart Semantic Colors - Highlighted (for hover/selection states) */ - --color-chart-success-highlight: #80d9b3; - --color-chart-warning-highlight: #f5c94d; - --color-chart-error-highlight: #ffa090; + /* Chart Colors - shared across themes via the chart-tokens partial. */ + @include chart-tokens.chart-tokens; /* Mantine Overrides */ --mantine-color-body: var(--color-bg-body); From 099447e85178054c169fcb15dd2ef131c0420b7e Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Wed, 13 May 2026 14:24:10 +0100 Subject: [PATCH 2/6] refactor(utils): single chart palette aligned with shared SCSS partial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapses CHART_PALETTE and CLICKSTACK_CHART_PALETTE in `packages/app/src/utils.ts` into a single CHART_PALETTE that mirrors the new `_chart-tokens.scss` partial. Reorders the exported COLORS array so COLORS[0] is blue (matches the new --color-chart-1) and removes the brand-green-first ordering. Knock-on simplifications now that there is only one palette: - `getSemanticChartColor` no longer takes per-theme fallbacks. Signature: `(cssVarName, fallback) -> string`. It still reads the live CSS var first; the fallback only matters for SSR or when `getComputedStyle` throws. - `detectActiveTheme()` is removed entirely. Its only consumer was `getSemanticChartColor`'s per-theme fallback branch. - The SSR/hydration-mismatch caveats in the JSDoc are gone too. Both themes resolve every chart var to the same hex now, so fallback == live value. Verified via `yarn ci:unit src/__tests__/ChartUtils.test.ts` — all 28 tests pass. Tests reference colors positionally as `COLORS[0]`, `COLORS[1]` etc., so reordering is transparent to them. Storybook label reorder and data-viz doc updates land next. Co-authored-by: Cursor --- packages/app/src/utils.ts | 120 +++++++++----------------------------- 1 file changed, 27 insertions(+), 93 deletions(-) diff --git a/packages/app/src/utils.ts b/packages/app/src/utils.ts index 294f31946f..4b25583cbd 100644 --- a/packages/app/src/utils.ts +++ b/packages/app/src/utils.ts @@ -353,30 +353,12 @@ export const getLogLevelClass = (lvl: string | undefined) => { : undefined; }; -// Chart color palette - single source of truth -// Colors from Observable categorical palette, with custom brand green +// Chart color palette - single source of truth, shared across themes. +// Mirrors `packages/app/src/theme/themes/_chart-tokens.scss`. Hex changes +// must update both this object and the SCSS partial. // https://observablehq.com/@d3/color-schemes const CHART_PALETTE = { - green: '#00c28a', // Brand green (Mantine green.5) - used as primary chart color - blue: '#4269d0', - orange: '#efb118', - red: '#ff725c', - cyan: '#6cc5b0', - pink: '#ff8ab7', - purple: '#a463f2', - lightBlue: '#97bbf5', - brown: '#9c6b4e', - gray: '#9498a0', - // Highlighted variants (lighter shades for hover/selection states) - greenHighlight: '#80d9b3', - redHighlight: '#ffa090', - orangeHighlight: '#f5c94d', -} as const; - -// ClickStack theme chart color palette - Observable 10 categorical palette -// https://observablehq.com/@d3/color-schemes -const CLICKSTACK_CHART_PALETTE = { - blue: '#437EEF', // Primary color for ClickStack + blue: '#437eef', // Primary categorical color orange: '#efb118', red: '#ff725c', cyan: '#6cc5b0', @@ -392,15 +374,17 @@ const CLICKSTACK_CHART_PALETTE = { orangeHighlight: '#f5c94d', } as const; -// Ordered array for chart series - green first for brand consistency (HyperDX default) -// Maps to CSS variables: COLORS[0] -> --color-chart-1, COLORS[1] -> --color-chart-2, etc. -// NOTE: This is a fallback for SSR. In browser, getColorFromCSSVariable() reads from CSS variables +// Ordered array for chart series - blue first to match the categorical +// palette in `_chart-tokens.scss`. Maps positionally to the CSS vars: +// COLORS[0] -> --color-chart-1, COLORS[1] -> --color-chart-2, etc. +// NOTE: SSR fallback only. In the browser, getColorFromCSSVariable() reads +// the live CSS var so any future runtime overrides take effect. export const COLORS = [ - CHART_PALETTE.green, // 1 - Brand green (primary) - HyperDX default - CHART_PALETTE.blue, // 2 - CHART_PALETTE.orange, // 3 - CHART_PALETTE.red, // 4 - CHART_PALETTE.cyan, // 5 + CHART_PALETTE.blue, // 1 + CHART_PALETTE.orange, // 2 + CHART_PALETTE.red, // 3 + CHART_PALETTE.cyan, // 4 + CHART_PALETTE.green, // 5 CHART_PALETTE.pink, // 6 CHART_PALETTE.purple, // 7 CHART_PALETTE.lightBlue, // 8 @@ -408,52 +392,26 @@ export const COLORS = [ CHART_PALETTE.gray, // 10 ]; -/** - * Detects the active theme by checking for theme classes on documentElement. - * Returns 'clickstack' if theme-clickstack class is present, 'hyperdx' otherwise. - * Note: classList.contains() is O(1) and fast - no caching needed. - */ -function detectActiveTheme(): 'clickstack' | 'hyperdx' { - if (typeof window === 'undefined') { - // SSR: default to hyperdx (can't detect theme without DOM) - return 'hyperdx'; - } - - try { - const isClickStack = - document.documentElement.classList.contains('theme-clickstack'); - return isClickStack ? 'clickstack' : 'hyperdx'; - } catch { - // Fallback if DOM access fails - return 'hyperdx'; - } -} - /** * Reads chart color from CSS variable based on index. * CSS variables handle theme switching automatically via theme classes on documentElement. * Falls back to COLORS array if CSS variable is not available (SSR or getComputedStyle fails). * - * Note on SSR/Hydration: During SSR, this returns fallback colors (HyperDX green palette). - * On client hydration, it reads from CSS variables which may differ for ClickStack theme. - * This is expected behavior - charts typically render after data fetching (client-side), - * so hydration mismatches are rare. If needed, wrap chart components with suppressHydrationWarning. + * Both themes resolve `--color-chart-N` to the same hex (see + * `_chart-tokens.scss`), so the SSR fallback matches the live value. */ function getColorFromCSSVariable(index: number): string { const colorArrayLength = COLORS.length; if (typeof window === 'undefined') { - // SSR: fallback to default colors (HyperDX palette) return COLORS[index % colorArrayLength]; } try { const cssVarName = `--color-chart-${(index % colorArrayLength) + 1}`; - // Read from documentElement - CSS variables cascade from theme classes const computedStyle = getComputedStyle(document.documentElement); const color = computedStyle.getPropertyValue(cssVarName).trim(); - // Only use CSS variable if it's actually set (non-empty) if (color && color !== '') { return color; } @@ -461,7 +419,6 @@ function getColorFromCSSVariable(index: number): string { // Fallback if getComputedStyle fails } - // Fallback to default colors return COLORS[index % colorArrayLength]; } @@ -479,21 +436,16 @@ export function hashCode(str: string) { } /** - * Gets theme-aware chart color from CSS variable or falls back to palette. - * Reads from --color-chart-{type} CSS variable, falls back to theme-appropriate palette. - * - * Note on SSR/Hydration: During SSR, returns HyperDX colors as default. - * On client, reads from CSS variables for accurate theme colors. - * Charts typically render client-side after data fetching, minimizing hydration issues. + * Reads a semantic chart CSS variable, falling back to the shared palette. + * Both themes resolve these vars to the same hex (see `_chart-tokens.scss`), + * so the SSR fallback matches the live value. */ function getSemanticChartColor( cssVarName: string, - hyperdxColor: string, - clickstackColor: string, + fallbackColor: string, ): string { if (typeof window === 'undefined') { - // SSR: use HyperDX as default (can't detect theme without DOM) - return hyperdxColor; + return fallbackColor; } try { @@ -506,43 +458,27 @@ function getSemanticChartColor( // Fallback if getComputedStyle fails } - // Fallback to theme-appropriate palette - const activeTheme = detectActiveTheme(); - return activeTheme === 'clickstack' ? clickstackColor : hyperdxColor; + return fallbackColor; } -// Semantic colors for log levels (theme-aware) -// These are functions that read from CSS variables with theme-appropriate fallbacks +// Semantic colors for log levels — resolved via CSS variables with a hex +// fallback for SSR / failed `getComputedStyle` reads. export function getChartColorSuccess(): string { - return getSemanticChartColor( - '--color-chart-success', - CHART_PALETTE.green, - CLICKSTACK_CHART_PALETTE.green, - ); + return getSemanticChartColor('--color-chart-success', CHART_PALETTE.green); } export function getChartColorWarning(): string { - return getSemanticChartColor( - '--color-chart-warning', - CHART_PALETTE.orange, - CLICKSTACK_CHART_PALETTE.orange, - ); + return getSemanticChartColor('--color-chart-warning', CHART_PALETTE.orange); } export function getChartColorError(): string { - return getSemanticChartColor( - '--color-chart-error', - CHART_PALETTE.red, - CLICKSTACK_CHART_PALETTE.red, - ); + return getSemanticChartColor('--color-chart-error', CHART_PALETTE.red); } -// Highlighted variants (theme-aware) export function getChartColorSuccessHighlight(): string { return getSemanticChartColor( '--color-chart-success-highlight', CHART_PALETTE.greenHighlight, - CLICKSTACK_CHART_PALETTE.greenHighlight, ); } @@ -550,7 +486,6 @@ export function getChartColorErrorHighlight(): string { return getSemanticChartColor( '--color-chart-error-highlight', CHART_PALETTE.redHighlight, - CLICKSTACK_CHART_PALETTE.redHighlight, ); } @@ -558,7 +493,6 @@ export function getChartColorWarningHighlight(): string { return getSemanticChartColor( '--color-chart-warning-highlight', CHART_PALETTE.orangeHighlight, - CLICKSTACK_CHART_PALETTE.orangeHighlight, ); } From 62b9f1a558b631797ba48d0cd86a5fdeaf280026 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Wed, 13 May 2026 14:30:45 +0100 Subject: [PATCH 3/6] docs+story: align with unified chart palette Updates the storybook label list and the data viz colors guide to match the new shared palette. Both files previously described a per-theme split (HyperDX brand-green-first vs ClickStack blue-first); after this PR's prior commits the palettes are identical, so the docs were factually wrong. Storybook (`ChartColors.stories.tsx`): - Reorders COLOR_LABELS so slot 1 is "Blue (Primary)", matching the new COLORS array and `--color-chart-1`. Old order had "Green (Brand)" at position 0, which would now render as a blue swatch labeled "Green". Data viz doc (`agent_docs/data_viz_colors.md`): - Replaces the per-theme "where the colors live" tables with a single shared palette table. - Adds a "Why one palette across both themes" subsection explaining the two reasons we collapsed the per-theme split (brand-green doubling as --color-chart-success in HyperDX; runtime detectActiveTheme() branch required for the SSR fallback). - Replaces the green-first / blue-first "Per-theme considerations" section with a short note that theme branding now lives in Mantine accent + Click UI globals + chrome, not in chart colors. The "yellow accent stays out of the categorical palette" rationale moves here intact. - Simplifies the "Adding new entries" section: hex changes now land in two places (`_chart-tokens.scss` + `CHART_PALETTE` in `utils.ts`) instead of four. - Updates the pre-merge checklist: "HyperDX <-> ClickStack toggle should *not* change chart colors anymore"; the only legitimate visual diff is theme chrome. - Updates the file reference summary so the new `_chart-tokens.scss` partial appears as the primary entry, with the two `_tokens.scss` files listed as consumers. - Refreshes JS line-number references to match the slimmer `utils.ts`. Co-authored-by: Cursor --- agent_docs/data_viz_colors.md | 331 +++++++++--------- .../app/src/theme/ChartColors.stories.tsx | 8 +- 2 files changed, 173 insertions(+), 166 deletions(-) diff --git a/agent_docs/data_viz_colors.md b/agent_docs/data_viz_colors.md index 72438f54c1..3563708e37 100644 --- a/agent_docs/data_viz_colors.md +++ b/agent_docs/data_viz_colors.md @@ -15,12 +15,18 @@ consumption patterns: | **Semantic (success/warn/err)**| Status indicators, log levels, deltas | CSS vars `--color-chart-{success,...}` | `getChartColorSuccess/Warning/Error()` | | **Heatmap continuous** | `DBHeatmapChart` density gradients | `darkPalette`/`lightPalette` arrays | Imported directly from `DBHeatmapChart.tsx` | +The categorical and semantic palettes are **identical across the HyperDX +and ClickStack themes** — they're defined once in +`packages/app/src/theme/themes/_chart-tokens.scss` and `@include`d by +both themes. Theme branding still differentiates UI chrome (Mantine +accent, Click UI globals); chart colors do not. + **Hard rules**: -- **Never** pass a hex color to a chart series. Always go through one of the - helpers above so theme switching works. +- **Never** pass a hex color to a chart series. Always go through one of + the helpers above so dark/light scheme switching works. - **Never** map log levels to raw Mantine colors (`red.5`, `yellow.6`). - Use `logLevelColor()` / `getColorProps()` — they pick the theme-correct + Use `logLevelColor()` / `getColorProps()` — they pick the correct semantic chart color. - The categorical palette and the heatmap palette are **different things**. Don't reuse `--color-chart-N` for heatmap density; don't reuse the heatmap @@ -30,25 +36,51 @@ consumption patterns: ### Categorical series palette (`--color-chart-1` through `--color-chart-10`) -| Theme | File | Index 1 (primary) | -| ----------- | ----------------------------------------------------------------------- | ----------------- | -| HyperDX | `packages/app/src/theme/themes/hyperdx/_tokens.scss` (lines ~95–117) | `#00c28a` brand green | -| ClickStack | `packages/app/src/theme/themes/clickstack/_tokens.scss` (lines ~175–201)| `#437eef` Observable blue | - -The same ten slots in both themes use the same hue families (blue, orange, -red, cyan, green, pink, purple, light blue, brown, gray) — only **slot 1** -differs: - -- **HyperDX** leads with brand green, then Observable colors. -- **ClickStack** leads with Observable blue (Click UI accent yellow doesn't - pass contrast on a typical chart background, so we don't use it as a series - color — see "Per-theme considerations" below). - -The vars are defined identically inside the dark and light selectors. That -duplication is intentional and called out in `_tokens.scss`: CSS specificity -requires it because the parent selectors `[data-mantine-color-scheme='dark']` -and `[data-mantine-color-scheme='light']` would otherwise drop the vars on -scheme switch. +Defined once in **`packages/app/src/theme/themes/_chart-tokens.scss`** +and consumed by both themes via `@include chart-tokens.chart-tokens` +inside their dark and light scheme blocks: + +| Slot | Hex | Hue | +| ---- | --------- | --------- | +| 1 | `#437eef` | Blue (primary) | +| 2 | `#efb118` | Orange | +| 3 | `#ff725c` | Red | +| 4 | `#6cc5b0` | Cyan | +| 5 | `#3ca951` | Green | +| 6 | `#ff8ab7` | Pink | +| 7 | `#a463f2` | Purple | +| 8 | `#97bbf5` | Light blue| +| 9 | `#9c6b4e` | Brown | +| 10 | `#9498a0` | Gray | + +Source: [Observable 10 categorical palette](https://observablehq.com/@d3/color-schemes). +Designed to be distinguishable on both dark and light backgrounds and +for color-vision-deficient viewers. + +#### Why one palette across both themes + +Originally HyperDX led with brand green (`#00c28a`) and ClickStack led +with Observable blue. That coupled "brand identity" with "chart slot 1", +which in practice caused two problems: + +- The HyperDX brand-green also doubled as `--color-chart-success`, so + success pills and primary chart series shared a hue. +- Per-theme palette ordering required a runtime `detectActiveTheme()` + branch in the JS fallback path and an SSR/hydration mismatch caveat. + +Unifying on one palette removes both concerns. Brand identity stays in +the UI chrome (Mantine accent yellow vs green, sidebar gradient, etc.); +chart-color identity is now a stable, theme-agnostic contract. + +#### Why scheme blocks still redeclare + +Even though the values are identical between dark and light, both +scheme blocks `@include chart-tokens.chart-tokens` rather than declaring +the vars at a parent selector. The reason is CSS specificity: the rest +of the design tokens are scoped to +`[data-mantine-color-scheme='dark|light']` and would otherwise win the +cascade and reset the chart vars to `unset`. The shared partial keeps +the values in lockstep without sacrificing specificity. ### Semantic chart colors @@ -61,26 +93,37 @@ scheme switch. --color-chart-error-highlight ``` -Defined in both `_tokens.scss` files. **HyperDX success uses brand green -(`#00c28a`)**; **ClickStack success uses Observable green (`#3ca951`)** so it -doesn't collide with the yellow brand accent. Warning and error are the same -across themes (orange `#efb118`, red `#ff725c`). +Also defined in `_chart-tokens.scss`. Identical across themes: + +| Var | Hex | +| ---------------------------------- | --------- | +| `--color-chart-success` | `#3ca951` | +| `--color-chart-warning` | `#efb118` | +| `--color-chart-error` | `#ff725c` | +| `--color-chart-success-highlight` | `#80d9b3` | +| `--color-chart-warning-highlight` | `#f5c94d` | +| `--color-chart-error-highlight` | `#ffa090` | + +Note that `--color-chart-success` (`#3ca951`) is **not** the same as +the categorical green at slot 5 (`#3ca951`) — they happen to coincide +today but they're two different vars with different intents. Treat +them as independent contracts. ### JavaScript fallback (`packages/app/src/utils.ts`) -The CSS vars are the source of truth at runtime, but two palette objects in -`utils.ts` are the SSR fallback **and** the storybook reference: +The CSS vars are the source of truth at runtime, but a single +`CHART_PALETTE` object in `utils.ts` mirrors them as the SSR fallback +**and** the storybook reference: ```text -CHART_PALETTE # HyperDX (green-first), lines ~356-374 -CLICKSTACK_CHART_PALETTE # ClickStack (blue-first), lines ~376-393 -COLORS # Exported, ordered, HyperDX-default array, lines ~398-409 +CHART_PALETTE # blue-first, lines ~360-378 +COLORS # Exported, ordered, slot-1-is-blue array, lines ~382-393 ``` `COLORS[0]` corresponds to `--color-chart-1`, `COLORS[1]` to `--color-chart-2`, -and so on. **Keep them in sync.** If you change a hex in one place, change it -in all three (HyperDX SCSS, ClickStack SCSS, and `CHART_PALETTE` / -`CLICKSTACK_CHART_PALETTE` / `COLORS`). +and so on. **Keep them in sync.** A hex change must update both +`_chart-tokens.scss` and `CHART_PALETTE`. Adding a new color also +requires updating `COLORS`. ### Reader functions (`packages/app/src/utils.ts`) @@ -100,15 +143,11 @@ These are the only functions React code should call: Internals worth knowing: - `getColorFromCSSVariable(index)` reads `--color-chart-{index+1}` from - `documentElement` via `getComputedStyle`. On SSR or if the var is missing, - it falls back to `COLORS[index % COLORS.length]`. -- `getSemanticChartColor(varName, hyperdxFallback, clickstackFallback)` does - the same for the semantic vars and uses `detectActiveTheme()` (checks for - the `theme-clickstack` class on ``) to pick the correct fallback when - CSS isn't available. -- During SSR, semantic readers return the **HyperDX** default — the - hydration mismatch window is tiny because charts render after data fetch - on the client. + `documentElement` via `getComputedStyle`. On SSR or if the var is + missing, it falls back to `COLORS[index % COLORS.length]`. +- `getSemanticChartColor(varName, fallback)` does the same for the + semantic vars. Single-argument fallback — both themes resolve to the + same hex now, so the SSR fallback always matches the live value. ### Heatmap palette (component-local) @@ -119,7 +158,7 @@ Internals worth knowing: - Selected at the call site by `useMantineColorScheme()` and `colorScheme === 'light' ? lightPalette : darkPalette`. -These are **scheme-aware, not brand-aware**: HyperDX dark and ClickStack +These are **scheme-aware, not theme-aware**: HyperDX dark and ClickStack dark share the same heatmap gradient, same for light. Red is intentionally omitted from the high end so it can be reserved for error overlays. @@ -129,59 +168,34 @@ duplicate the arrays in another component. ### Trace and delta-specific colors -A few component-local accents that are **not** part of the categorical or -semantic palettes: +A few component-local accents that are **not** part of the categorical +or semantic palettes: - `ALL_SPANS_COLOR = 'var(--mantine-color-blue-6)'` in `packages/app/src/components/deltaChartUtils.ts` — the "all spans" - reference bar in `DBDeltaChart`. Keep using this var; don't replace it - with `--color-chart-1` (it's a comparison reference, not a series). -- Trace waterfall span tints in `DBTraceWaterfallChart.tsx` — derived from - span attributes, not from this palette. + reference bar in `DBDeltaChart`. Keep using this var; don't replace + it with `--color-chart-1` (it's a comparison reference, not a series). +- Trace waterfall span tints in `DBTraceWaterfallChart.tsx` — derived + from span attributes, not from this palette. ## Storybook reference The visual reference for the categorical and semantic palettes is the -storybook story at: - -```1:22:packages/app/src/theme/ChartColors.stories.tsx -import React from 'react'; - -import { - COLORS, - getChartColorError, - getChartColorSuccess, - getChartColorWarning, -} from '@/utils'; - -// Labels for chart colors - brand green first, then Observable palette -const COLOR_LABELS = [ - 'Green (Brand)', - 'Blue', - 'Orange', - 'Red', - 'Cyan', - 'Pink', - 'Purple', - 'Light Blue', - 'Brown', - 'Gray', -]; -``` - -It renders `AllChartColors`, `BarChartPreview`, `LineChartPreview`, -`SemanticColorsPreview`, and `AccessibilityCheck`. Run storybook in the -`app` package to inspect both schemes side by side. +storybook story at `packages/app/src/theme/ChartColors.stories.tsx`. It +renders `AllChartColors`, `BarChartPreview`, `LineChartPreview`, +`SemanticColorsPreview`, and `AccessibilityCheck`. Both schemes look +identical across the two brand themes (since chart vars are shared); +toggle dark/light to verify legibility. ## How to consume (recipes) ### Multi-series time-series chart -`ChartUtils.tsx → setLineColors` already wires the categorical palette per -series via `getColorProps(index, level)` in +`ChartUtils.tsx → setLineColors` already wires the categorical palette +per series via `getColorProps(index, level)` in `packages/app/src/ChartUtils.tsx`. **You should not have to think about -chart colors when adding a new chart that goes through `seriesToTimeSeries` -/ `setLineColors`** — they handle it. +chart colors when adding a new chart that goes through +`seriesToTimeSeries` / `setLineColors`** — they handle it. If you're rendering a custom chart outside that pipeline: @@ -195,9 +209,10 @@ const series = data.map((s, i) => ({ })); ``` -`level` (the second arg) is used to override with semantic colors when the -label looks like a log level (`'error'`, `'warn'`, `'info'`, etc.). Pass -`s.label` if it might encode a log level, otherwise pass an empty string. +`level` (the second arg) is used to override with semantic colors when +the label looks like a log level (`'error'`, `'warn'`, `'info'`, etc.). +Pass `s.label` if it might encode a log level, otherwise pass an empty +string. ### Status pill / delta indicator @@ -208,19 +223,20 @@ import { getChartColorError, getChartColorSuccess } from '@/utils'; ``` -These functions return resolved hex strings, so they can be used in inline -styles or passed to libraries that don't understand CSS vars (e.g. -`uPlot`'s canvas fills). +These functions return resolved hex strings, so they can be used in +inline styles or passed to libraries that don't understand CSS vars +(e.g. `uPlot`'s canvas fills). -If you're styling a DOM element with regular CSS, prefer the var directly: +If you're styling a DOM element with regular CSS, prefer the var +directly: ```tsx ``` -The var route reacts instantly to theme switches without re-running React. -Use the function form only when you need a string at compute time -(canvas/WebGL, library config objects, etc.). +The var route reacts instantly to scheme switches without re-running +React. Use the function form only when you need a string at compute +time (canvas/WebGL, library config objects, etc.). ### Heatmap @@ -237,10 +253,10 @@ reconstruct it. ### Pie / donut where slice order matters -The categorical palette is **ordered for distinguishability** — adjacent -slots are designed to contrast. If you're drawing a pie chart where the -largest slice should always be the most prominent color, sort your data -first and let the index map to the palette naturally: +The categorical palette is **ordered for distinguishability** — +adjacent slots are designed to contrast. If you're drawing a pie chart +where the largest slice should always be the most prominent color, sort +your data first and let the index map to the palette naturally: ```tsx const sorted = data.toSorted((a, b) => b.value - a.value); @@ -250,76 +266,66 @@ const slices = sorted.map((d, i) => ({ })); ``` -`ChartUtils.tsx → buildPieChartData` already does this — the comment "Sort -in descending order so the largest slice is always first and gets the -first color in the palette" is at line ~444. +`ChartUtils.tsx → buildPieChartData` already does this — the comment +"Sort in descending order so the largest slice is always first and gets +the first color in the palette" is at line ~444. ## Per-theme considerations -### HyperDX (green-first) - -- Slot 1 is brand green (`#00c28a`) so single-series charts feel - on-brand without any extra config. -- Semantic success **also** uses brand green, so success indicators and - primary series share a hue. This is intentional but worth knowing: - if a chart juxtaposes a "success" pill with a green series, that's - expected, not a bug. - -### ClickStack (blue-first) - -- The brand accent is **yellow** (`--palette-brand-300: #faff69`). It is - **not** in the chart palette and should not be added — yellow on a - light background fails contrast, and yellow as a series color reads as - "warning" in most contexts. -- Slot 1 falls back to Observable blue (`#437eef`). -- Semantic success is **Observable green** (`#3ca951`), distinct from - the yellow brand accent. Don't try to "brand" success with yellow. +The categorical and semantic chart palettes are **theme-agnostic** — +both HyperDX and ClickStack resolve every `--color-chart-*` var to the +same hex. Theme branding lives elsewhere: -### Both themes +- **Mantine accent**: `green` for HyperDX, `yellow` for ClickStack. + Affects `