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

            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 : }
        

Generated by: LCOV version 2.0-1