Line data Source code
1 : import type { TopLevelSpec } from 'vega-lite';
2 :
3 : import { loadVega, type VegaModules } from './vegalite_loader.js';
4 : import { withViewEncodings } from './vegalite_spec.js';
5 : import { createVegaLiteConfig, resolveChartColors } from './vegalite_theme.js';
6 :
7 : /// The size of a chart image in CSS pixels, the image has `scale` times as many pixels
8 : export const CHART_IMAGE_WIDTH = 800;
9 : export const CHART_IMAGE_HEIGHT = 500;
10 : export const CHART_IMAGE_SCALE = 2;
11 :
12 : /// The background of chart images, the images take the light colors of the theme in both color schemes
13 1 : const CHART_IMAGE_BACKGROUND = '#ffffff';
14 : /// The margin of chart images in CSS pixels, the charts of the app have none and touch the edges of the image
15 1 : const CHART_IMAGE_PADDING = 16;
16 :
17 : /// The Vega log level that only logs errors (vega.Error)
18 1 : const VEGA_LOG_LEVEL_ERROR = 1;
19 :
20 : export interface ChartImageOptions {
21 : /// The width in CSS pixels
22 : width?: number;
23 : /// The height in CSS pixels
24 : height?: number;
25 : /// The pixels per CSS pixel
26 : scale?: number;
27 : /// Load the Vega modules, for tests
28 : loader?: () => Promise<VegaModules>;
29 : }
30 :
31 : /// A chart as PNG
32 : export interface ChartImage {
33 : png: Uint8Array;
34 : /// The width in pixels
35 : width: number;
36 : /// The height in pixels
37 : height: number;
38 : }
39 :
40 : /// Render a Vega-Lite spec with the theme of the app as PNG, without a DOM element.
41 : /// The rows are the data of the spec, like the query result of a `@vis vegalite` tag.
42 1 : export async function renderVegaLitePng(spec: TopLevelSpec, rows: readonly object[], options: ChartImageOptions = {}): Promise<ChartImage> {
43 1 : const { vega, vegaLite, expressionInterpreter } = await (options.loader ?? loadVega)();
44 1 : const sizedSpec = {
45 1 : ...withViewEncodings(spec),
46 1 : data: { values: rows },
47 1 : width: options.width ?? CHART_IMAGE_WIDTH,
48 1 : height: options.height ?? CHART_IMAGE_HEIGHT,
49 1 : autosize: { type: 'fit', contains: 'padding' },
50 1 : } as TopLevelSpec;
51 1 : const config = { ...createVegaLiteConfig(resolveChartColors('light')), background: CHART_IMAGE_BACKGROUND, padding: CHART_IMAGE_PADDING };
52 1 : const compiled = vegaLite.compile(sizedSpec, { config }).spec;
53 : // The expression interpreter avoids the eval of Vega that CSP forbids, it needs the parsed ASTs
54 1 : const view = new vega.View(vega.parse(compiled, undefined, { ast: true }), {
55 1 : renderer: 'none',
56 1 : expr: expressionInterpreter,
57 1 : logLevel: VEGA_LOG_LEVEL_ERROR,
58 1 : });
59 1 : try {
60 1 : await view.runAsync();
61 1 : const canvas = await view.toCanvas(options.scale ?? CHART_IMAGE_SCALE);
62 1 : const blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve, 'image/png'));
63 1 : if (blob == null) throw new Error('The chart cannot be encoded as PNG');
64 1 : return { png: new Uint8Array(await blob.arrayBuffer()), width: canvas.width, height: canvas.height };
65 1 : } finally {
66 1 : view.finalize();
67 1 : }
68 1 : }
|