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;
|