Line data Source code
1 : /// The keys of a Vega-Lite spec that hold the views it composes
2 1 : const VIEW_LIST_KEYS = ['layer', 'concat', 'hconcat', 'vconcat'];
3 :
4 1 : function isObject(value: unknown): value is Record<string, unknown> {
5 1 : return value != null && typeof value === 'object' && !Array.isArray(value);
6 1 : }
7 :
8 : /// Give every view with a mark an encoding, an empty one if it has none.
9 : ///
10 : /// This works around a bug of Vega-Lite (seen in 6.5.0): `getPointOverlay` decides whether line, area and trail
11 : /// marks get a point overlay and reads `encoding.shape` without checking for an encoding. When neither the mark
12 : /// nor the config sets `point`, compiling a view without an encoding throws
13 : /// "Cannot read properties of undefined (reading 'shape')". Bars and the other marks never reach that code.
14 : ///
15 : /// The chart builder creates such views: a new chart is `{ mark: 'bar' }` without fields, switching its mark gives
16 : /// `{ mark: 'line' }`, and a new layer is `{ mark: 'line' }` too. Hand-written specs of scripts can be the same.
17 : /// A view in a layer, a concat, a facet or a repeat fails alike, unless the parent has an encoding that the
18 : /// layers inherit. The empty encoding is added only to the spec that is compiled, every place that compiles
19 : /// Vega-Lite calls this, so the authored spec in the script keeps its keys.
20 : ///
21 : /// Returns the spec itself when no view lacks an encoding.
22 1 : export function withViewEncodings<T>(spec: T): T {
23 1 : if (!isObject(spec)) return spec;
24 1 : let out: Record<string, unknown> | null = null;
25 1 : const set = (key: string, value: unknown) => {
26 1 : out ??= { ...spec };
27 1 : out[key] = value;
28 1 : };
29 1 : if ('mark' in spec && !isObject(spec.encoding)) set('encoding', {});
30 1 : for (const key of VIEW_LIST_KEYS) {
31 1 : const views = spec[key];
32 1 : if (!Array.isArray(views)) continue;
33 1 : const next = views.map(withViewEncodings);
34 1 : if (next.some((view, i) => view !== views[i])) set(key, next);
35 1 : }
36 : // The view of a facet or a repeat
37 1 : if (isObject(spec.spec)) {
38 1 : const view = withViewEncodings(spec.spec);
39 1 : if (view !== spec.spec) set('spec', view);
40 1 : }
41 1 : return (out ?? spec) as T;
42 1 : }
|