Line data Source code
1 : import type { TopLevelSpec } from 'vega-lite';
2 :
3 : import type { ChartColors } from '../../ui/vegalite/vegalite_theme.js';
4 :
5 : /// The params of the bounds of the time window, in milliseconds since the epoch
6 1 : export const WINDOW_START_SIGNAL = 'windowStart';
7 1 : export const WINDOW_END_SIGNAL = 'windowEnd';
8 :
9 : /// The dataset of the rows of a chart
10 1 : export const ROWS_DATASET = 'rows';
11 :
12 : // The axis labels are compact for small charts and leave out the zero of the baseline
13 :
14 : /// An axis label of a duration in seconds, e.g. 850µs, 12ms, 1.2s
15 1 : const DURATION_LABEL = "datum.value == 0 ? '' : datum.value >= 1 ? format(datum.value, '~r') + 's'"
16 : + " : datum.value >= 0.001 ? format(datum.value * 1000, '~r') + 'ms'"
17 : + " : format(datum.value * 1000000, '~r') + 'µs'";
18 : /// An axis label of a memory size in MB, e.g. 64MB, 1.5GB
19 1 : const MEMORY_LABEL = "datum.value == 0 ? '' : datum.value >= 1024 ? format(datum.value / 1024, '~r') + 'GB' : format(datum.value, '~r') + 'MB'";
20 : /// An axis label of a count, e.g. 12, 1.2k, 3M
21 1 : const COUNT_LABEL = "datum.value == 0 ? '' : format(datum.value, '~s')";
22 : /// An axis label of a load, e.g. 50%
23 1 : const LOAD_LABEL = "datum.value == 0 ? '' : format(datum.value, '.0%')";
24 :
25 : /// The y axis of a small chart: a gridline at the baseline and one above it
26 1 : const Y_AXIS = { tickCount: 2, labelPadding: 3 };
27 :
28 1 : const WINDOW_PARAMS = [
29 1 : { name: WINDOW_START_SIGNAL, value: 0 },
30 1 : { name: WINDOW_END_SIGNAL, value: 1 },
31 1 : ];
32 :
33 : /// The x channel of the time of a row in the sliding window
34 1 : const TIME_X = {
35 1 : field: 't',
36 1 : type: 'temporal',
37 1 : title: null,
38 1 : scale: { domainMin: { expr: WINDOW_START_SIGNAL }, domainMax: { expr: WINDOW_END_SIGNAL } },
39 1 : axis: { format: '%H:%M', tickCount: 3, labelPadding: 3, labelSeparation: 8 },
40 1 : } as const;
41 :
42 : /// The kind of a value of queries, it picks the axis labels
43 : export type QueryValueKind = 'duration' | 'count' | 'memory';
44 :
45 1 : const QUERY_VALUE_LABELS: Record<QueryValueKind, string> = {
46 1 : duration: DURATION_LABEL,
47 1 : count: COUNT_LABEL,
48 1 : memory: MEMORY_LABEL,
49 1 : };
50 :
51 : /// A rule at the row nearest to the pointer with a tooltip of that row
52 1 : function hoverRule(colors: ChartColors, tooltip: object[]) {
53 1 : return {
54 1 : data: { name: ROWS_DATASET },
55 1 : params: [{
56 1 : name: 'hover',
57 1 : select: { type: 'point', encodings: ['x'], nearest: true, on: 'pointerover', clear: 'pointerout' },
58 1 : }],
59 1 : mark: { type: 'rule', color: colors.title },
60 1 : encoding: {
61 1 : x: TIME_X,
62 1 : opacity: { condition: { param: 'hover', empty: false, value: 1 }, value: 0 },
63 1 : tooltip,
64 1 : },
65 1 : };
66 1 : }
67 :
68 : /// A field of a row in the tooltip of a chart
69 : export interface TooltipField {
70 : /// The field of the row
71 : field: string;
72 : /// The title in the tooltip
73 : title: string;
74 : }
75 :
76 : /// A value per bucket of time as bars that span their bucket, with a tooltip of the bucket on its bar.
77 : /// The rows are `{ t0, t1, clock, ... }` with the bucket from t0 to t1 and its time of day as clock.
78 : /// A chart draws one bar per bucket, its marks do not grow with the queries of the window.
79 1 : export function bucketChartSpec(field: string, kind: QueryValueKind, tooltip: readonly TooltipField[]): TopLevelSpec {
80 1 : return {
81 1 : params: WINDOW_PARAMS,
82 1 : data: { name: ROWS_DATASET },
83 : // A gap of 1px between the bars
84 1 : mark: { type: 'bar', clip: true, xOffset: 0.5, x2Offset: -0.5 },
85 1 : encoding: {
86 1 : x: { ...TIME_X, field: 't0' },
87 1 : x2: { field: 't1' },
88 : // The bars span a range of time, they need a range of values as well to not draw horizontally
89 1 : y2: { datum: 0 },
90 1 : y: {
91 1 : field,
92 1 : type: 'quantitative',
93 1 : title: null,
94 1 : axis: { ...Y_AXIS, labelExpr: QUERY_VALUE_LABELS[kind], ...(kind === 'count' ? { tickMinStep: 1 } : {}) },
95 1 : },
96 1 : tooltip: [{ field: 'clock', title: 'Time' }, ...tooltip],
97 1 : },
98 1 : } as TopLevelSpec;
99 1 : }
100 :
101 : /// The kind of the values of a series chart, it picks the axis labels and the tooltip format
102 : export type SeriesKind = 'memory' | 'load';
103 :
104 1 : const SERIES_LABELS: Record<SeriesKind, string> = {
105 1 : memory: MEMORY_LABEL,
106 1 : load: LOAD_LABEL,
107 1 : };
108 1 : const SERIES_TOOLTIP_FORMATS: Record<SeriesKind, string> = {
109 1 : memory: '.4~r',
110 1 : load: '.0%',
111 1 : };
112 :
113 : /// A series of a series chart
114 : export interface ChartSeries {
115 : /// The name of the series in the rows and the tooltip
116 : name: string;
117 : /// The color of the series
118 : color: (colors: ChartColors) => string;
119 : /// Draw an area wash under the line
120 : area?: boolean;
121 : }
122 :
123 : /// A line per series over time, with a rule and a tooltip of all series at the time nearest to the pointer.
124 : /// The rows are `{ t, series, value }`.
125 1 : export function seriesSpec(colors: ChartColors, series: readonly ChartSeries[], kind: SeriesKind): TopLevelSpec {
126 1 : const color = {
127 1 : field: 'series',
128 1 : type: 'nominal',
129 1 : scale: { domain: series.map(s => s.name), range: series.map(s => s.color(colors)) },
130 1 : legend: null,
131 1 : };
132 1 : const y = {
133 1 : field: 'value',
134 1 : type: 'quantitative',
135 1 : title: null,
136 1 : scale: kind === 'load' ? { domain: [0, 1] } : { zero: true },
137 1 : axis: { ...Y_AXIS, labelExpr: SERIES_LABELS[kind] },
138 1 : };
139 1 : const areas = series.filter(s => s.area).map(s => s.name);
140 1 : return {
141 1 : params: WINDOW_PARAMS,
142 1 : layer: [
143 1 : ...(areas.length > 0 ? [{
144 1 : data: { name: ROWS_DATASET },
145 1 : transform: [{ filter: { field: 'series', oneOf: areas } }],
146 1 : mark: { type: 'area', opacity: 0.1, clip: true },
147 1 : encoding: { x: TIME_X, y, color },
148 1 : }] : []),
149 1 : {
150 1 : data: { name: ROWS_DATASET },
151 1 : mark: { type: 'line', clip: true },
152 1 : encoding: { x: TIME_X, y, color },
153 1 : },
154 1 : {
155 1 : ...hoverRule(colors, [
156 1 : { field: 't', type: 'temporal', format: '%H:%M:%S', title: 'Time' },
157 1 : ...series.map(s => ({ field: s.name, type: 'quantitative', format: SERIES_TOOLTIP_FORMATS[kind], title: s.name })),
158 1 : ]),
159 1 : transform: [{ pivot: 'series', value: 'value', groupby: ['t'] }],
160 1 : },
161 1 : ],
162 1 : } as TopLevelSpec;
163 1 : }
|