Charts.utils.test.ts224 lines · main
1import { describe, expect, it } from 'vitest'
2
3import { computeYAxisDomain, normalizeStackedSeriesData } from './Charts.utils'
4
5const IOPS_DATA = [
6 { timestamp: 1, disk_iops_write: 1200, disk_iops_read: 24203, disk_iops_max: 25000 },
7 { timestamp: 2, disk_iops_write: 400, disk_iops_read: 3200, disk_iops_max: 25000 },
8 { timestamp: 3, disk_iops_write: 100, disk_iops_read: 900, disk_iops_max: 25000 },
9]
10
11const IOPS_VISIBLE = ['disk_iops_write', 'disk_iops_read']
12
13describe('computeYAxisDomain', () => {
14 describe('percentage charts with max line hidden', () => {
15 it('returns [0, yMaxFromVisible] to zoom in on the data', () => {
16 expect(
17 computeYAxisDomain({
18 isPercentage: true,
19 showMaxValue: false,
20 yMaxFromVisible: 75,
21 maxAttributeKey: 'cpu_usage_max',
22 showMaxLine: false,
23 data: [{ cpu_busy: 75, cpu_usage_max: 100 }],
24 visibleAttributeNames: ['cpu_busy'],
25 })
26 ).toEqual([0, 75])
27 })
28
29 it('still zooms in even when a maxAttributeKey is present', () => {
30 expect(
31 computeYAxisDomain({
32 isPercentage: true,
33 showMaxValue: false,
34 yMaxFromVisible: 60,
35 maxAttributeKey: 'cpu_usage_max',
36 showMaxLine: true,
37 data: [{ cpu_busy: 60, cpu_usage_max: 100 }],
38 visibleAttributeNames: ['cpu_busy'],
39 })
40 ).toEqual([0, 60])
41 })
42 })
43
44 describe('no max reference line', () => {
45 it('returns auto when maxAttributeKey is undefined', () => {
46 expect(
47 computeYAxisDomain({
48 isPercentage: false,
49 showMaxValue: false,
50 yMaxFromVisible: 5000,
51 maxAttributeKey: undefined,
52 showMaxLine: false,
53 data: IOPS_DATA,
54 visibleAttributeNames: IOPS_VISIBLE,
55 })
56 ).toEqual(['auto', 'auto'])
57 })
58
59 it('returns auto when showMaxLine is false', () => {
60 expect(
61 computeYAxisDomain({
62 isPercentage: false,
63 showMaxValue: true,
64 yMaxFromVisible: 5000,
65 maxAttributeKey: 'disk_iops_max',
66 showMaxLine: false,
67 data: IOPS_DATA,
68 visibleAttributeNames: IOPS_VISIBLE,
69 })
70 ).toEqual(['auto', 'auto'])
71 })
72 })
73
74 describe('max reference line not yet loaded (value is 0)', () => {
75 it('returns auto when diskConfig has not loaded and reference line value is 0', () => {
76 const dataWithZeroMax = IOPS_DATA.map((p) => ({ ...p, disk_iops_max: 0 }))
77 expect(
78 computeYAxisDomain({
79 isPercentage: false,
80 showMaxValue: true,
81 yMaxFromVisible: 5000,
82 maxAttributeKey: 'disk_iops_max',
83 showMaxLine: true,
84 data: dataWithZeroMax,
85 visibleAttributeNames: IOPS_VISIBLE,
86 })
87 ).toEqual(['auto', 'auto'])
88 })
89 })
90
91 describe('explicit domain with reference line', () => {
92 it('uses the reference line value when bars stay below it', () => {
93 // All stacked bar totals (1000, 500) are well below maxRefValue (25000)
94 expect(
95 computeYAxisDomain({
96 isPercentage: false,
97 showMaxValue: true,
98 yMaxFromVisible: 800,
99 maxAttributeKey: 'disk_iops_max',
100 showMaxLine: true,
101 data: [
102 { disk_iops_write: 400, disk_iops_read: 600, disk_iops_max: 25000 },
103 { disk_iops_write: 200, disk_iops_read: 300, disk_iops_max: 25000 },
104 ],
105 visibleAttributeNames: IOPS_VISIBLE,
106 })
107 ).toEqual([0, 25000])
108 })
109
110 it('uses the stacked bar total when it exceeds the reference line', () => {
111 // Stacked total at first point: 24203 + 1200 = 25403 > 25000
112 expect(
113 computeYAxisDomain({
114 isPercentage: false,
115 showMaxValue: true,
116 yMaxFromVisible: 24203,
117 maxAttributeKey: 'disk_iops_max',
118 showMaxLine: true,
119 data: IOPS_DATA,
120 visibleAttributeNames: IOPS_VISIBLE,
121 })
122 ).toEqual([0, 25403])
123 })
124
125 it('domain min is always 0', () => {
126 const [min] = computeYAxisDomain({
127 isPercentage: false,
128 showMaxValue: true,
129 yMaxFromVisible: 100,
130 maxAttributeKey: 'disk_iops_max',
131 showMaxLine: true,
132 data: IOPS_DATA,
133 visibleAttributeNames: IOPS_VISIBLE,
134 }) as [number, number]
135 expect(min).toBe(0)
136 })
137
138 it('works for database connections chart (single bar series, no stacking)', () => {
139 const data = [
140 { pg_stat_database_num_backends: 45, max_db_connections: 60 },
141 { pg_stat_database_num_backends: 52, max_db_connections: 60 },
142 ]
143 expect(
144 computeYAxisDomain({
145 isPercentage: false,
146 showMaxValue: true,
147 yMaxFromVisible: 52,
148 maxAttributeKey: 'max_db_connections',
149 showMaxLine: true,
150 data,
151 visibleAttributeNames: ['pg_stat_database_num_backends'],
152 })
153 ).toEqual([0, 60])
154 })
155
156 it('handles non-numeric values in data gracefully', () => {
157 const data = [
158 { disk_iops_write: 'bad', disk_iops_read: null, disk_iops_max: 25000 },
159 { disk_iops_write: 500, disk_iops_read: 1000, disk_iops_max: 25000 },
160 ]
161 expect(
162 computeYAxisDomain({
163 isPercentage: false,
164 showMaxValue: true,
165 yMaxFromVisible: 1000,
166 maxAttributeKey: 'disk_iops_max',
167 showMaxLine: true,
168 data: data as Record<string, unknown>[],
169 visibleAttributeNames: IOPS_VISIBLE,
170 })
171 ).toEqual([0, 25000])
172 })
173 })
174})
175
176describe('normalizeStackedSeriesData', () => {
177 it('normalizes each stacked point to 100%', () => {
178 const normalized = normalizeStackedSeriesData({
179 data: [{ system: 25, user: 25, idle: 100 }],
180 attributeNames: ['system', 'user', 'idle'],
181 })
182
183 expect(normalized[0].system).toBeCloseTo(16.6666666667)
184 expect(normalized[0].user).toBeCloseTo(16.6666666667)
185 expect(normalized[0].idle).toBeCloseTo(66.6666666666)
186 expect(
187 Number(normalized[0].system) + Number(normalized[0].user) + Number(normalized[0].idle)
188 ).toBeCloseTo(100)
189 })
190
191 it('leaves empty stacks unchanged', () => {
192 const empty = [{ system: 0, user: 0, idle: 0 }]
193
194 expect(
195 normalizeStackedSeriesData({
196 data: empty,
197 attributeNames: ['system', 'user', 'idle'],
198 })
199 ).toEqual(empty)
200 })
201
202 it('leaves data unchanged when attributeNames is empty', () => {
203 const data = [{ system: 50, user: 30, timestamp: 1000 }]
204
205 expect(
206 normalizeStackedSeriesData({
207 data,
208 attributeNames: [],
209 })
210 ).toEqual(data)
211 })
212
213 it('preserves non-stacked keys (timestamps, metadata) unchanged', () => {
214 const data = [{ system: 60, user: 80, period_start: '2024-01-01', timestamp: 1000 }]
215 const normalized = normalizeStackedSeriesData({
216 data,
217 attributeNames: ['system', 'user'],
218 })
219
220 expect(normalized[0].period_start).toBe('2024-01-01')
221 expect(normalized[0].timestamp).toBe(1000)
222 expect(Number(normalized[0].system) + Number(normalized[0].user)).toBeCloseTo(100)
223 })
224})