LCOV - code coverage report
Current view: top level - dashql-app/src/ui/vegalite - vegalite_theme.ts Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 149 149
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import type { Config } from 'vega-lite';
       2              : import type { EmbedOptions } from 'vega-embed';
       3              : 
       4              : import type { ColorScheme } from '../../config/color_scheme.js';
       5              : 
       6              : import './vegalite_tooltip.css';
       7              : 
       8              : /// The categorical colors of series in the order they are assigned, as chart color tokens of the app.
       9              : /// The tokens default to the base colors in @ankoh/dashql-ui/styles/tokens.css, flavors override them.
      10              : /// The default order is validated: adjacent colors stay apart for color-blind readers (CVD ΔE >= 16).
      11            1 : export const CATEGORICAL_COLOR_TOKENS = [
      12            1 :     '--chart_color_1',
      13            1 :     '--chart_color_2',
      14            1 :     '--chart_color_3',
      15            1 :     '--chart_color_4',
      16            1 :     '--chart_color_5',
      17            1 :     '--chart_color_6',
      18            1 :     '--chart_color_7',
      19            1 :     '--chart_color_8',
      20            1 : ] as const;
      21              : 
      22              : /// The fallbacks of the color tokens, used when a token is not defined, e.g. outside of the app styles.
      23              : /// They are the light colors of the theme.
      24            1 : const TOKEN_FALLBACKS: Record<string, string> = {
      25            1 :     '--chart_color_1': '#0969da',
      26            1 :     '--chart_color_2': '#e16f24',
      27            1 :     '--chart_color_3': '#8250df',
      28            1 :     '--chart_color_4': '#1a7f37',
      29            1 :     '--chart_color_5': '#e85aad',
      30            1 :     '--chart_color_6': '#bf8700',
      31            1 :     '--chart_color_7': '#54aeff',
      32            1 :     '--chart_color_8': '#953800',
      33            1 :     '--color_danger_fg': '#cf222e',
      34            1 :     '--color_border_muted': '#eaeef2',
      35            1 :     '--color_neutral_fg': '#6e7781',
      36            1 :     '--color_fg_muted': '#57606a',
      37            1 :     '--color_fg': '#24292f',
      38            1 :     '--color_bg_card': '#ffffff',
      39            1 : };
      40              : 
      41              : /// The colors of the app that charts use, resolved to values that the canvas understands.
      42              : export interface ChartColors {
      43              :     /// The colors of series in the order they are assigned
      44              :     categorical: string[];
      45              :     /// The color of a single series
      46              :     primary: string;
      47              :     /// The color of failures
      48              :     critical: string;
      49              :     /// The color of grid lines
      50              :     grid: string;
      51              :     /// The color of axis labels and legends
      52              :     label: string;
      53              :     /// The color of titles and reference lines
      54              :     title: string;
      55              :     /// The color of text in marks
      56              :     text: string;
      57              :     /// The color of the surface below the chart, the outline of points
      58              :     surface: string;
      59              : }
      60              : 
      61              : /// Resolve color tokens of the app to rgb values.
      62              : /// The tokens may reference other variables, calc() and light-dark(), which the canvas renderer of Vega cannot
      63              : /// resolve. They resolve in the color scheme of the document, or in the given one.
      64            1 : function resolveColorTokens(tokens: readonly string[], scheme?: ColorScheme): Map<string, string> {
      65            1 :     const resolved = new Map<string, string>();
      66            1 :     const rootStyle = getComputedStyle(document.documentElement);
      67            1 :     const probe = document.createElement('span');
      68            1 :     probe.style.display = 'none';
      69            1 :     if (scheme !== undefined) {
      70            1 :         probe.style.colorScheme = scheme;
      71              :     }
      72            1 :     document.body.appendChild(probe);
      73            1 :     try {
      74            1 :         for (const token of tokens) {
      75            1 :             if (rootStyle.getPropertyValue(token).trim() === '') {
      76            1 :                 resolved.set(token, TOKEN_FALLBACKS[token]);
      77            1 :                 continue;
      78            1 :             }
      79            1 :             probe.style.color = `var(${token})`;
      80            1 :             resolved.set(token, getComputedStyle(probe).color);
      81            1 :         }
      82            1 :     } finally {
      83            1 :         probe.remove();
      84            1 :     }
      85            1 :     return resolved;
      86            1 : }
      87              : 
      88              : /// The chart colors of the current app styles, in the color scheme of the document or in the given one.
      89              : /// Charts drawn on a canvas resolve them again when the color scheme changes, see useColorScheme.
      90            1 : export function resolveChartColors(scheme?: ColorScheme): ChartColors {
      91            1 :     const tokens = [...CATEGORICAL_COLOR_TOKENS, ...Object.keys(TOKEN_FALLBACKS)];
      92            1 :     const colors = resolveColorTokens([...new Set(tokens)], scheme);
      93            1 :     return {
      94            1 :         categorical: CATEGORICAL_COLOR_TOKENS.map(token => colors.get(token)!),
      95            1 :         primary: colors.get('--chart_color_1')!,
      96            1 :         critical: colors.get('--color_danger_fg')!,
      97            1 :         grid: colors.get('--color_border_muted')!,
      98            1 :         label: colors.get('--color_neutral_fg')!,
      99            1 :         title: colors.get('--color_fg_muted')!,
     100            1 :         text: colors.get('--color_fg')!,
     101            1 :         surface: colors.get('--color_bg_card')!,
     102            1 :     };
     103            1 : }
     104              : 
     105              : /// The fallback fonts of charts behind the font of the app
     106            1 : const FALLBACK_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
     107              : 
     108              : /// The font of the app, a theme token that the app flavor may override
     109            1 : function resolveFont(): string {
     110            1 :     const family = getComputedStyle(document.documentElement).getPropertyValue('--font_family').trim();
     111            1 :     return family === '' ? FALLBACK_FONT : `${family}, ${FALLBACK_FONT}`;
     112            1 : }
     113              : 
     114              : /// The Vega-Lite config of all charts of the app: recessive axes and grids, thin marks, square bars.
     115            1 : export function createVegaLiteConfig(colors: ChartColors): Config {
     116            1 :     return {
     117            1 :         background: 'transparent',
     118            1 :         font: resolveFont(),
     119            1 :         padding: 0,
     120            1 :         view: { stroke: null },
     121            1 :         range: { category: colors.categorical },
     122            1 :         mark: { color: colors.primary },
     123            1 :         bar: { cornerRadius: 0, binSpacing: 1, continuousBandSize: 2 },
     124            1 :         line: { strokeWidth: 2, strokeCap: 'round', strokeJoin: 'round' },
     125            1 :         area: { line: false },
     126            1 :         point: { filled: true, size: 50, stroke: colors.surface, strokeWidth: 2, opacity: 1 },
     127            1 :         rule: { color: colors.title, strokeWidth: 1 },
     128            1 :         tick: { thickness: 2 },
     129            1 :         text: { color: colors.text, fontSize: 11 },
     130            1 :         axis: {
     131            1 :             domain: false,
     132            1 :             ticks: false,
     133            1 :             grid: true,
     134            1 :             gridColor: colors.grid,
     135            1 :             gridWidth: 1,
     136            1 :             labelColor: colors.label,
     137            1 :             labelFontSize: 10,
     138            1 :             labelPadding: 6,
     139            1 :             titleColor: colors.title,
     140            1 :             titleFontSize: 11,
     141            1 :             titleFontWeight: 500,
     142            1 :             titlePadding: 8,
     143            1 :         },
     144            1 :         axisX: { grid: false, labelFlush: true, labelOverlap: true },
     145            1 :         axisBand: { grid: false },
     146            1 :         legend: {
     147            1 :             labelColor: colors.label,
     148            1 :             labelFontSize: 11,
     149            1 :             titleColor: colors.title,
     150            1 :             titleFontSize: 11,
     151            1 :             titleFontWeight: 500,
     152            1 :             symbolSize: 64,
     153            1 :             symbolStrokeWidth: 0,
     154            1 :         },
     155            1 :         header: { labelColor: colors.title, titleColor: colors.title, labelFontSize: 11, titleFontSize: 11 },
     156            1 :         title: { color: colors.text, fontSize: 13, fontWeight: 600, anchor: 'start', offset: 8 },
     157            1 :     };
     158            1 : }
     159              : 
     160              : /// The Vega-Lite config of chart previews in small boxes, e.g. on the dataflow canvas: the config of all charts with
     161              : /// smaller text and marks, fewer ticks and horizontal labels on the x axis.
     162            1 : export function createVegaLitePreviewConfig(colors: ChartColors): Config {
     163            1 :     const base = createVegaLiteConfig(colors);
     164            1 :     return {
     165            1 :         ...base,
     166            1 :         line: { ...base.line, strokeWidth: 1.5 },
     167            1 :         point: { ...base.point, size: 24, strokeWidth: 1 },
     168            1 :         text: { ...base.text, fontSize: 9 },
     169            1 :         axis: {
     170            1 :             ...base.axis,
     171            1 :             labelFontSize: 8,
     172            1 :             labelPadding: 3,
     173            1 :             labelLimit: 64,
     174            1 :             tickCount: 4,
     175            1 :             titleFontSize: 9,
     176            1 :             titlePadding: 4,
     177            1 :         },
     178            1 :         axisX: { ...base.axisX, labelAngle: 0 },
     179            1 :         legend: { ...base.legend, labelFontSize: 8, titleFontSize: 9, symbolSize: 32 },
     180            1 :         header: { ...base.header, labelFontSize: 9, titleFontSize: 9 },
     181            1 :         title: { ...base.title, fontSize: 10, offset: 4 },
     182            1 :     };
     183            1 : }
     184              : 
     185              : /// The name of the theme of the Vega tooltips, styled by the class dashql-theme
     186            1 : export const VEGA_TOOLTIP_THEME = 'dashql';
     187              : 
     188              : /// The embed options of all charts of the app.
     189              : /// The canvas renderer draws many marks fast, the expression interpreter avoids the eval that CSP forbids.
     190            1 : export function createEmbedOptions(
     191            1 :     colors: ChartColors,
     192            1 :     expressionInterpreter: EmbedOptions['expr'],
     193            1 : ): EmbedOptions {
     194            1 :     return {
     195            1 :         actions: false,
     196            1 :         renderer: 'canvas',
     197            1 :         ast: true,
     198            1 :         expr: expressionInterpreter,
     199            1 :         config: createVegaLiteConfig(colors),
     200            1 :         tooltip: { theme: VEGA_TOOLTIP_THEME },
     201            1 :         logLevel: VEGA_LOG_LEVEL_ERROR,
     202            1 :     };
     203            1 : }
     204              : 
     205              : /// The Vega log level that only logs errors (vega.Error).
     206              : /// Charts are embedded with empty datasets and filled by updates, and windows can be empty,
     207              : /// on empty data the extent transforms warn "Infinite extent" for every scale.
     208            1 : const VEGA_LOG_LEVEL_ERROR = 1;
        

Generated by: LCOV version 2.0-1