Skip to content

Commit 6a4c57b

Browse files
committed
optimize
Signed-off-by: Qxisylolo <[email protected]>
1 parent b9341e5 commit 6a4c57b

File tree

5 files changed

+275
-59
lines changed

5 files changed

+275
-59
lines changed

src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.test.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ import {
88
thresholdsToGradient,
99
symbolOpposite,
1010
getGradientConfig,
11+
generateThresholds,
1112
} from './bar_gauge_utils';
1213
import { AxisColumnMappings, Threshold, VisFieldType } from '../types';
1314
import { BarGaugeChartStyle } from './bar_gauge_vis_config';
1415

16+
jest.mock('../theme/default_colors', () => ({
17+
getColors: jest.fn(() => ({ statusGreen: '#00FF00' })),
18+
}));
19+
1520
describe('bar_gauge_utils', () => {
1621
describe('getBarOrientation', () => {
1722
const mockAxisColumnMappings: AxisColumnMappings = {
@@ -123,4 +128,186 @@ describe('bar_gauge_utils', () => {
123128
expect(result).toBeUndefined();
124129
});
125130
});
131+
132+
describe('generateThresholds', () => {
133+
const mockThresholds: Threshold[] = [
134+
{ value: 10, color: '#FF0000' },
135+
{ value: 50, color: '#FFFF00' },
136+
{ value: 80, color: '#0037ffff' },
137+
];
138+
139+
afterEach(() => {
140+
jest.restoreAllMocks();
141+
});
142+
143+
describe('Basic functionality', () => {
144+
it('should handle empty thresholds array', () => {
145+
const result = generateThresholds(0, 100, [], '#BLUE', []);
146+
147+
expect(result.mergedThresholds).toHaveLength(1);
148+
expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' });
149+
expect(result.valueThresholds).toEqual([]);
150+
});
151+
152+
it('should use default color when baseColor is undefined', () => {
153+
const result = generateThresholds(0, 100, [], undefined, []);
154+
155+
expect(result.mergedThresholds).toHaveLength(1);
156+
expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#00FF00' });
157+
});
158+
159+
it('should process normal thresholds correctly', () => {
160+
const result = generateThresholds(0, 100, mockThresholds, '#BLUE', []);
161+
162+
expect(result.mergedThresholds).toHaveLength(4);
163+
expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' });
164+
expect(result.mergedThresholds[1]).toEqual({ value: 10, color: '#FF0000' });
165+
expect(result.mergedThresholds[2]).toEqual({ value: 50, color: '#FFFF00' });
166+
expect(result.mergedThresholds[3]).toEqual({ value: 80, color: '#0037ffff' });
167+
});
168+
});
169+
170+
describe('Threshold filtering and range handling', () => {
171+
it('should filter thresholds above maxBase', () => {
172+
const result = generateThresholds(0, 60, mockThresholds, '#BLUE', []);
173+
174+
expect(result.mergedThresholds).toHaveLength(3);
175+
expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' });
176+
expect(result.mergedThresholds[1]).toEqual({ value: 10, color: '#FF0000' });
177+
expect(result.mergedThresholds[2]).toEqual({ value: 50, color: '#FFFF00' });
178+
// Should not include the threshold with value 80
179+
});
180+
181+
it('should handle minBase higher than first threshold', () => {
182+
const result = generateThresholds(25, 100, mockThresholds, '#BLUE', []);
183+
184+
expect(result.mergedThresholds).toHaveLength(3);
185+
expect(result.mergedThresholds[0]).toEqual({ value: 25, color: '#FF0000' });
186+
expect(result.mergedThresholds[1]).toEqual({ value: 50, color: '#FFFF00' });
187+
expect(result.mergedThresholds[2]).toEqual({ value: 80, color: '#0037ffff' });
188+
});
189+
190+
it('should handle minBase higher than all thresholds', () => {
191+
const result = generateThresholds(90, 100, mockThresholds, '#BLUE', []);
192+
193+
expect(result.mergedThresholds).toHaveLength(1);
194+
expect(result.mergedThresholds[0]).toEqual({ value: 90, color: '#0037ffff' });
195+
});
196+
});
197+
198+
describe('Duplicate threshold handling', () => {
199+
it('should handle duplicate threshold values by keeping the latest', () => {
200+
const duplicateThresholds: Threshold[] = [
201+
{ value: 10, color: '#FF0000' },
202+
{ value: 50, color: '#FFFF00' },
203+
{ value: 50, color: '#00FFFF' }, // Duplicate value, different color
204+
{ value: 80, color: '#00FF00' },
205+
];
206+
207+
const result = generateThresholds(0, 100, duplicateThresholds, '#BLUE', []);
208+
209+
expect(result.mergedThresholds).toHaveLength(4);
210+
expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' });
211+
expect(result.mergedThresholds[1]).toEqual({ value: 10, color: '#FF0000' });
212+
expect(result.mergedThresholds[2]).toEqual({ value: 50, color: '#00FFFF' }); // Latest color
213+
expect(result.mergedThresholds[3]).toEqual({ value: 80, color: '#00FF00' });
214+
});
215+
});
216+
217+
describe('Value stops processing', () => {
218+
it('should process value stops correctly', () => {
219+
const valueStops = [15, 45, 75];
220+
const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops);
221+
222+
expect(result.valueThresholds).toHaveLength(3);
223+
expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' }); // Uses threshold at 10
224+
expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' }); // Uses threshold at 10
225+
expect(result.valueThresholds[2]).toEqual({ value: 75, color: '#FFFF00' }); // Uses threshold at 50
226+
});
227+
228+
it('should filter value stops outside range', () => {
229+
const valueStops = [5, 15, 45, 95, 105]; // 105 is above maxBase, 5 is below minBase (when minBase > 0)
230+
const result = generateThresholds(10, 90, mockThresholds, '#BLUE', valueStops);
231+
232+
// Should only include stops between minBase (10) and maxBase (90)
233+
expect(result.valueThresholds).toHaveLength(2);
234+
expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' });
235+
expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' });
236+
});
237+
238+
it('should handle duplicate value stops', () => {
239+
const valueStops = [15, 15, 45, 45, 75]; // Duplicates
240+
const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops);
241+
242+
expect(result.valueThresholds).toHaveLength(3); // Should deduplicate
243+
expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' });
244+
expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' });
245+
expect(result.valueThresholds[2]).toEqual({ value: 75, color: '#FFFF00' });
246+
});
247+
248+
it('should handle unsorted value stops', () => {
249+
const valueStops = [75, 15, 45]; // Unsorted
250+
const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops);
251+
252+
expect(result.valueThresholds).toHaveLength(3);
253+
expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' });
254+
expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' });
255+
expect(result.valueThresholds[2]).toEqual({ value: 75, color: '#FFFF00' });
256+
});
257+
258+
it('should handle empty value stops array', () => {
259+
const result = generateThresholds(0, 100, mockThresholds, '#BLUE', []);
260+
261+
expect(result.valueThresholds).toEqual([]);
262+
});
263+
});
264+
265+
describe('Edge cases', () => {
266+
it('should handle single threshold', () => {
267+
const singleThreshold: Threshold[] = [{ value: 50, color: '#FF0000' }];
268+
const result = generateThresholds(0, 100, singleThreshold, '#BLUE', [25, 75]);
269+
270+
expect(result.mergedThresholds).toHaveLength(2);
271+
expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' });
272+
expect(result.mergedThresholds[1]).toEqual({ value: 50, color: '#FF0000' });
273+
274+
expect(result.valueThresholds).toHaveLength(2);
275+
expect(result.valueThresholds[0]).toEqual({ value: 25, color: '#BLUE' });
276+
expect(result.valueThresholds[1]).toEqual({ value: 75, color: '#FF0000' });
277+
});
278+
279+
it('should handle minBase equal to maxBase', () => {
280+
const result = generateThresholds(50, 50, mockThresholds, '#BLUE', []);
281+
282+
expect(result.mergedThresholds).toHaveLength(1);
283+
expect(result.mergedThresholds[0]).toEqual({ value: 50, color: '#FFFF00' });
284+
});
285+
286+
it('should handle value stops equal to threshold values', () => {
287+
const valueStops = [10, 50, 80]; // Exact threshold values
288+
const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops);
289+
290+
expect(result.valueThresholds).toHaveLength(3);
291+
expect(result.valueThresholds[0]).toEqual({ value: 10, color: '#FF0000' });
292+
expect(result.valueThresholds[1]).toEqual({ value: 50, color: '#FFFF00' });
293+
expect(result.valueThresholds[2]).toEqual({ value: 80, color: '#0037ffff' });
294+
});
295+
296+
it('should handle negative values', () => {
297+
const negativeThresholds: Threshold[] = [
298+
{ value: -50, color: '#FF0000' },
299+
{ value: 0, color: '#FFFF00' },
300+
{ value: 50, color: '#00FF00' },
301+
];
302+
303+
const result = generateThresholds(-100, 100, negativeThresholds, '#BLUE', [-25, 25]);
304+
305+
expect(result.mergedThresholds).toHaveLength(4);
306+
expect(result.mergedThresholds[0]).toEqual({ value: -100, color: '#BLUE' });
307+
expect(result.valueThresholds).toHaveLength(2);
308+
expect(result.valueThresholds[0]).toEqual({ value: -25, color: '#FF0000' });
309+
expect(result.valueThresholds[1]).toEqual({ value: 25, color: '#FFFF00' });
310+
});
311+
});
312+
});
126313
});

src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { AxisColumnMappings, Threshold, VisFieldType } from '../types';
77
import { BarGaugeChartStyle } from './bar_gauge_vis_config';
8+
import { getColors } from '../theme/default_colors';
89

910
export const getBarOrientation = (
1011
styles: BarGaugeChartStyle,
@@ -80,22 +81,6 @@ export const getGradientConfig = (
8081
};
8182
};
8283

83-
export const processThresholds = (thresholds: Threshold[]) => {
84-
const result: Threshold[] = [];
85-
86-
for (let i = 0; i < thresholds.length; i++) {
87-
const current = thresholds[i];
88-
const next = thresholds[i + 1];
89-
90-
// if the next threshold has the same value, use next
91-
if (next && next.value === current.value) continue;
92-
93-
result.push(current);
94-
}
95-
96-
return result;
97-
};
98-
9984
export const normalizeData = (data: number, start: number, end: number) => {
10085
if (start === end) return null;
10186
// normalize data value between start and end into 0–1 range
@@ -157,3 +142,68 @@ export const generateParams = (
157142

158143
return result;
159144
};
145+
146+
export const generateThresholds = (
147+
minBase: number,
148+
maxBase: number,
149+
thresholds: Threshold[],
150+
baseColor: string | undefined,
151+
valueStops: number[]
152+
) => {
153+
const defaultColor = baseColor ?? getColors().statusGreen;
154+
155+
const filteredThresholds = thresholds.filter((t) => t.value <= maxBase);
156+
const filteredValueStops = valueStops
157+
.filter((v) => v <= maxBase && v >= minBase)
158+
.sort((a, b) => a - b);
159+
const result: Threshold[] = [];
160+
let lastBelowIndex = -1;
161+
let lastThresholdValue: Threshold | undefined;
162+
163+
for (let i = 0; i < filteredThresholds.length; i++) {
164+
const currentThreshold = filteredThresholds[i];
165+
166+
// Handle duplicate values - keep the latest one
167+
if (lastThresholdValue && lastThresholdValue.value === currentThreshold.value) {
168+
result.pop();
169+
}
170+
171+
result.push(currentThreshold);
172+
lastThresholdValue = currentThreshold;
173+
174+
// Track last threshold below minBase
175+
if (minBase >= currentThreshold.value) {
176+
lastBelowIndex = i;
177+
}
178+
}
179+
180+
if (lastBelowIndex !== -1) {
181+
result.splice(0, lastBelowIndex);
182+
result[0] = { ...result[0], value: minBase };
183+
} else {
184+
result.unshift({ value: minBase, color: defaultColor });
185+
}
186+
187+
const valueResults: Threshold[] = [];
188+
if (filteredValueStops.length > 0 && result.length > 0) {
189+
const stops = [...new Set(filteredValueStops)];
190+
191+
let thresholdIndex = 0;
192+
193+
for (const stop of stops) {
194+
while (thresholdIndex < result.length - 1 && result[thresholdIndex + 1].value <= stop) {
195+
thresholdIndex++;
196+
}
197+
198+
// Add valid threshold for this stop
199+
if (result[thresholdIndex].value <= stop) {
200+
valueResults.push({ value: stop, color: result[thresholdIndex].color });
201+
}
202+
}
203+
}
204+
205+
return {
206+
mergedThresholds: result,
207+
valueThresholds: valueResults,
208+
};
209+
};

src/plugins/explore/public/components/visualizations/bar_gauge/to_expression.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ import {
1313
getBarOrientation,
1414
thresholdsToGradient,
1515
symbolOpposite,
16-
processThresholds,
1716
generateParams,
17+
generateThresholds,
1818
} from './bar_gauge_utils';
1919
import { getUnitById, showDisplayValue } from '../style_panel/unit/collection';
20-
import { mergeThresholdsWithBase } from '../style_panel/threshold/threshold_utils';
2120

2221
export const createBarGaugeSpec = (
2322
transformedData: Array<Record<string, any>>,
@@ -51,6 +50,7 @@ export const createBarGaugeSpec = (
5150
let maxNumber: number = -Infinity;
5251
let minNumber: number = Infinity;
5352
let maxTextLength: number = 0;
53+
const valueStops: number[] = [];
5454

5555
const selectedUnit = getUnitById(styleOptions?.unitId);
5656

@@ -75,6 +75,7 @@ export const createBarGaugeSpec = (
7575
[numericField]: isValidNumber ? calculate : null,
7676
displayValue,
7777
});
78+
valueStops.push(...(isValidNumber ? [calculate] : []));
7879
}
7980
}
8081

@@ -98,36 +99,18 @@ export const createBarGaugeSpec = (
9899
]
99100
: styleOptions?.thresholdOptions?.thresholds;
100101

101-
const { textColor, mergedThresholds } = mergeThresholdsWithBase(
102+
const { mergedThresholds, valueThresholds } = generateThresholds(
102103
minBase,
103104
maxBase,
105+
styleOptions?.thresholdOptions?.thresholds ?? [],
104106
styleOptions?.thresholdOptions?.baseColor,
105-
styleOptions?.thresholdOptions?.thresholds
107+
valueStops
106108
);
107109

108-
// transfer value to threshold
109-
const valueToThreshold = [];
110-
111-
for (const record of newRecord) {
112-
for (let i = mergedThresholds.length - 1; i >= 0; i--) {
113-
if (numericField && record[numericField] >= mergedThresholds[i].value) {
114-
valueToThreshold.push({ value: record[numericField], color: mergedThresholds[i].color });
115-
break;
116-
}
117-
}
118-
}
119-
120-
// only use value-based thresholds in gradient mode
121-
const finalThreshold = styleOptions?.exclusive.displayMode === 'gradient' ? valueToThreshold : [];
122-
123-
const completeThreshold = [...mergedThresholds, ...(invalidCase ? [] : finalThreshold)].sort(
124-
(a, b) => a.value - b.value
125-
);
126-
127-
// filter out value thresholds that are beyond maxBase, this ensures that the gradient mode on different bar is always aligned.
128-
const processedThresholds = processThresholds(
129-
completeThreshold.filter((t) => t.value <= maxBase)
130-
);
110+
const processedThresholds = [
111+
...mergedThresholds,
112+
...(styleOptions?.exclusive.displayMode === 'gradient' ? valueThresholds : []),
113+
].sort((a, b) => a.value - b.value);
131114

132115
const gradientParams = generateParams(processedThresholds, styleOptions, isXaxisNumerical);
133116

src/plugins/explore/public/components/visualizations/gauge/to_expression.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const createGauge = (
6464
minBase,
6565
maxBase,
6666
// TODO: update to use the color from color palette
67-
styleOptions?.thresholdOptions?.baseColor || colors.statusGreen,
67+
styleOptions?.thresholdOptions?.baseColor,
6868
styleOptions?.thresholdOptions?.thresholds,
6969
calculatedValue
7070
);

0 commit comments

Comments
 (0)