From 6a4c57bf3bacea3028ee06f3e8338de968f49c4b Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Wed, 12 Nov 2025 18:05:44 +0800 Subject: [PATCH 1/5] optimize Signed-off-by: Qxisylolo --- .../bar_gauge/bar_gauge_utils.test.ts | 187 ++++++++++++++++++ .../bar_gauge/bar_gauge_utils.ts | 82 ++++++-- .../visualizations/bar_gauge/to_expression.ts | 37 +--- .../visualizations/gauge/to_expression.ts | 2 +- .../visualizations/metric/to_expression.ts | 26 ++- 5 files changed, 275 insertions(+), 59 deletions(-) diff --git a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.test.ts b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.test.ts index df4a650ccf40..06a9d76b0c88 100644 --- a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.test.ts +++ b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.test.ts @@ -8,10 +8,15 @@ import { thresholdsToGradient, symbolOpposite, getGradientConfig, + generateThresholds, } from './bar_gauge_utils'; import { AxisColumnMappings, Threshold, VisFieldType } from '../types'; import { BarGaugeChartStyle } from './bar_gauge_vis_config'; +jest.mock('../theme/default_colors', () => ({ + getColors: jest.fn(() => ({ statusGreen: '#00FF00' })), +})); + describe('bar_gauge_utils', () => { describe('getBarOrientation', () => { const mockAxisColumnMappings: AxisColumnMappings = { @@ -123,4 +128,186 @@ describe('bar_gauge_utils', () => { expect(result).toBeUndefined(); }); }); + + describe('generateThresholds', () => { + const mockThresholds: Threshold[] = [ + { value: 10, color: '#FF0000' }, + { value: 50, color: '#FFFF00' }, + { value: 80, color: '#0037ffff' }, + ]; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('Basic functionality', () => { + it('should handle empty thresholds array', () => { + const result = generateThresholds(0, 100, [], '#BLUE', []); + + expect(result.mergedThresholds).toHaveLength(1); + expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); + expect(result.valueThresholds).toEqual([]); + }); + + it('should use default color when baseColor is undefined', () => { + const result = generateThresholds(0, 100, [], undefined, []); + + expect(result.mergedThresholds).toHaveLength(1); + expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#00FF00' }); + }); + + it('should process normal thresholds correctly', () => { + const result = generateThresholds(0, 100, mockThresholds, '#BLUE', []); + + expect(result.mergedThresholds).toHaveLength(4); + expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); + expect(result.mergedThresholds[1]).toEqual({ value: 10, color: '#FF0000' }); + expect(result.mergedThresholds[2]).toEqual({ value: 50, color: '#FFFF00' }); + expect(result.mergedThresholds[3]).toEqual({ value: 80, color: '#0037ffff' }); + }); + }); + + describe('Threshold filtering and range handling', () => { + it('should filter thresholds above maxBase', () => { + const result = generateThresholds(0, 60, mockThresholds, '#BLUE', []); + + expect(result.mergedThresholds).toHaveLength(3); + expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); + expect(result.mergedThresholds[1]).toEqual({ value: 10, color: '#FF0000' }); + expect(result.mergedThresholds[2]).toEqual({ value: 50, color: '#FFFF00' }); + // Should not include the threshold with value 80 + }); + + it('should handle minBase higher than first threshold', () => { + const result = generateThresholds(25, 100, mockThresholds, '#BLUE', []); + + expect(result.mergedThresholds).toHaveLength(3); + expect(result.mergedThresholds[0]).toEqual({ value: 25, color: '#FF0000' }); + expect(result.mergedThresholds[1]).toEqual({ value: 50, color: '#FFFF00' }); + expect(result.mergedThresholds[2]).toEqual({ value: 80, color: '#0037ffff' }); + }); + + it('should handle minBase higher than all thresholds', () => { + const result = generateThresholds(90, 100, mockThresholds, '#BLUE', []); + + expect(result.mergedThresholds).toHaveLength(1); + expect(result.mergedThresholds[0]).toEqual({ value: 90, color: '#0037ffff' }); + }); + }); + + describe('Duplicate threshold handling', () => { + it('should handle duplicate threshold values by keeping the latest', () => { + const duplicateThresholds: Threshold[] = [ + { value: 10, color: '#FF0000' }, + { value: 50, color: '#FFFF00' }, + { value: 50, color: '#00FFFF' }, // Duplicate value, different color + { value: 80, color: '#00FF00' }, + ]; + + const result = generateThresholds(0, 100, duplicateThresholds, '#BLUE', []); + + expect(result.mergedThresholds).toHaveLength(4); + expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); + expect(result.mergedThresholds[1]).toEqual({ value: 10, color: '#FF0000' }); + expect(result.mergedThresholds[2]).toEqual({ value: 50, color: '#00FFFF' }); // Latest color + expect(result.mergedThresholds[3]).toEqual({ value: 80, color: '#00FF00' }); + }); + }); + + describe('Value stops processing', () => { + it('should process value stops correctly', () => { + const valueStops = [15, 45, 75]; + const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops); + + expect(result.valueThresholds).toHaveLength(3); + expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' }); // Uses threshold at 10 + expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' }); // Uses threshold at 10 + expect(result.valueThresholds[2]).toEqual({ value: 75, color: '#FFFF00' }); // Uses threshold at 50 + }); + + it('should filter value stops outside range', () => { + const valueStops = [5, 15, 45, 95, 105]; // 105 is above maxBase, 5 is below minBase (when minBase > 0) + const result = generateThresholds(10, 90, mockThresholds, '#BLUE', valueStops); + + // Should only include stops between minBase (10) and maxBase (90) + expect(result.valueThresholds).toHaveLength(2); + expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' }); + expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' }); + }); + + it('should handle duplicate value stops', () => { + const valueStops = [15, 15, 45, 45, 75]; // Duplicates + const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops); + + expect(result.valueThresholds).toHaveLength(3); // Should deduplicate + expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' }); + expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' }); + expect(result.valueThresholds[2]).toEqual({ value: 75, color: '#FFFF00' }); + }); + + it('should handle unsorted value stops', () => { + const valueStops = [75, 15, 45]; // Unsorted + const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops); + + expect(result.valueThresholds).toHaveLength(3); + expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' }); + expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' }); + expect(result.valueThresholds[2]).toEqual({ value: 75, color: '#FFFF00' }); + }); + + it('should handle empty value stops array', () => { + const result = generateThresholds(0, 100, mockThresholds, '#BLUE', []); + + expect(result.valueThresholds).toEqual([]); + }); + }); + + describe('Edge cases', () => { + it('should handle single threshold', () => { + const singleThreshold: Threshold[] = [{ value: 50, color: '#FF0000' }]; + const result = generateThresholds(0, 100, singleThreshold, '#BLUE', [25, 75]); + + expect(result.mergedThresholds).toHaveLength(2); + expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); + expect(result.mergedThresholds[1]).toEqual({ value: 50, color: '#FF0000' }); + + expect(result.valueThresholds).toHaveLength(2); + expect(result.valueThresholds[0]).toEqual({ value: 25, color: '#BLUE' }); + expect(result.valueThresholds[1]).toEqual({ value: 75, color: '#FF0000' }); + }); + + it('should handle minBase equal to maxBase', () => { + const result = generateThresholds(50, 50, mockThresholds, '#BLUE', []); + + expect(result.mergedThresholds).toHaveLength(1); + expect(result.mergedThresholds[0]).toEqual({ value: 50, color: '#FFFF00' }); + }); + + it('should handle value stops equal to threshold values', () => { + const valueStops = [10, 50, 80]; // Exact threshold values + const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops); + + expect(result.valueThresholds).toHaveLength(3); + expect(result.valueThresholds[0]).toEqual({ value: 10, color: '#FF0000' }); + expect(result.valueThresholds[1]).toEqual({ value: 50, color: '#FFFF00' }); + expect(result.valueThresholds[2]).toEqual({ value: 80, color: '#0037ffff' }); + }); + + it('should handle negative values', () => { + const negativeThresholds: Threshold[] = [ + { value: -50, color: '#FF0000' }, + { value: 0, color: '#FFFF00' }, + { value: 50, color: '#00FF00' }, + ]; + + const result = generateThresholds(-100, 100, negativeThresholds, '#BLUE', [-25, 25]); + + expect(result.mergedThresholds).toHaveLength(4); + expect(result.mergedThresholds[0]).toEqual({ value: -100, color: '#BLUE' }); + expect(result.valueThresholds).toHaveLength(2); + expect(result.valueThresholds[0]).toEqual({ value: -25, color: '#FF0000' }); + expect(result.valueThresholds[1]).toEqual({ value: 25, color: '#FFFF00' }); + }); + }); + }); }); diff --git a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts index b2058503fc36..a423204782d5 100644 --- a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts +++ b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts @@ -5,6 +5,7 @@ import { AxisColumnMappings, Threshold, VisFieldType } from '../types'; import { BarGaugeChartStyle } from './bar_gauge_vis_config'; +import { getColors } from '../theme/default_colors'; export const getBarOrientation = ( styles: BarGaugeChartStyle, @@ -80,22 +81,6 @@ export const getGradientConfig = ( }; }; -export const processThresholds = (thresholds: Threshold[]) => { - const result: Threshold[] = []; - - for (let i = 0; i < thresholds.length; i++) { - const current = thresholds[i]; - const next = thresholds[i + 1]; - - // if the next threshold has the same value, use next - if (next && next.value === current.value) continue; - - result.push(current); - } - - return result; -}; - export const normalizeData = (data: number, start: number, end: number) => { if (start === end) return null; // normalize data value between start and end into 0–1 range @@ -157,3 +142,68 @@ export const generateParams = ( return result; }; + +export const generateThresholds = ( + minBase: number, + maxBase: number, + thresholds: Threshold[], + baseColor: string | undefined, + valueStops: number[] +) => { + const defaultColor = baseColor ?? getColors().statusGreen; + + const filteredThresholds = thresholds.filter((t) => t.value <= maxBase); + const filteredValueStops = valueStops + .filter((v) => v <= maxBase && v >= minBase) + .sort((a, b) => a - b); + const result: Threshold[] = []; + let lastBelowIndex = -1; + let lastThresholdValue: Threshold | undefined; + + for (let i = 0; i < filteredThresholds.length; i++) { + const currentThreshold = filteredThresholds[i]; + + // Handle duplicate values - keep the latest one + if (lastThresholdValue && lastThresholdValue.value === currentThreshold.value) { + result.pop(); + } + + result.push(currentThreshold); + lastThresholdValue = currentThreshold; + + // Track last threshold below minBase + if (minBase >= currentThreshold.value) { + lastBelowIndex = i; + } + } + + if (lastBelowIndex !== -1) { + result.splice(0, lastBelowIndex); + result[0] = { ...result[0], value: minBase }; + } else { + result.unshift({ value: minBase, color: defaultColor }); + } + + const valueResults: Threshold[] = []; + if (filteredValueStops.length > 0 && result.length > 0) { + const stops = [...new Set(filteredValueStops)]; + + let thresholdIndex = 0; + + for (const stop of stops) { + while (thresholdIndex < result.length - 1 && result[thresholdIndex + 1].value <= stop) { + thresholdIndex++; + } + + // Add valid threshold for this stop + if (result[thresholdIndex].value <= stop) { + valueResults.push({ value: stop, color: result[thresholdIndex].color }); + } + } + } + + return { + mergedThresholds: result, + valueThresholds: valueResults, + }; +}; diff --git a/src/plugins/explore/public/components/visualizations/bar_gauge/to_expression.ts b/src/plugins/explore/public/components/visualizations/bar_gauge/to_expression.ts index fb96cb56550d..5bb45b08f343 100644 --- a/src/plugins/explore/public/components/visualizations/bar_gauge/to_expression.ts +++ b/src/plugins/explore/public/components/visualizations/bar_gauge/to_expression.ts @@ -13,11 +13,10 @@ import { getBarOrientation, thresholdsToGradient, symbolOpposite, - processThresholds, generateParams, + generateThresholds, } from './bar_gauge_utils'; import { getUnitById, showDisplayValue } from '../style_panel/unit/collection'; -import { mergeThresholdsWithBase } from '../style_panel/threshold/threshold_utils'; export const createBarGaugeSpec = ( transformedData: Array>, @@ -51,6 +50,7 @@ export const createBarGaugeSpec = ( let maxNumber: number = -Infinity; let minNumber: number = Infinity; let maxTextLength: number = 0; + const valueStops: number[] = []; const selectedUnit = getUnitById(styleOptions?.unitId); @@ -75,6 +75,7 @@ export const createBarGaugeSpec = ( [numericField]: isValidNumber ? calculate : null, displayValue, }); + valueStops.push(...(isValidNumber ? [calculate] : [])); } } @@ -98,36 +99,18 @@ export const createBarGaugeSpec = ( ] : styleOptions?.thresholdOptions?.thresholds; - const { textColor, mergedThresholds } = mergeThresholdsWithBase( + const { mergedThresholds, valueThresholds } = generateThresholds( minBase, maxBase, + styleOptions?.thresholdOptions?.thresholds ?? [], styleOptions?.thresholdOptions?.baseColor, - styleOptions?.thresholdOptions?.thresholds + valueStops ); - // transfer value to threshold - const valueToThreshold = []; - - for (const record of newRecord) { - for (let i = mergedThresholds.length - 1; i >= 0; i--) { - if (numericField && record[numericField] >= mergedThresholds[i].value) { - valueToThreshold.push({ value: record[numericField], color: mergedThresholds[i].color }); - break; - } - } - } - - // only use value-based thresholds in gradient mode - const finalThreshold = styleOptions?.exclusive.displayMode === 'gradient' ? valueToThreshold : []; - - const completeThreshold = [...mergedThresholds, ...(invalidCase ? [] : finalThreshold)].sort( - (a, b) => a.value - b.value - ); - - // filter out value thresholds that are beyond maxBase, this ensures that the gradient mode on different bar is always aligned. - const processedThresholds = processThresholds( - completeThreshold.filter((t) => t.value <= maxBase) - ); + const processedThresholds = [ + ...mergedThresholds, + ...(styleOptions?.exclusive.displayMode === 'gradient' ? valueThresholds : []), + ].sort((a, b) => a.value - b.value); const gradientParams = generateParams(processedThresholds, styleOptions, isXaxisNumerical); diff --git a/src/plugins/explore/public/components/visualizations/gauge/to_expression.ts b/src/plugins/explore/public/components/visualizations/gauge/to_expression.ts index d76a1d03af3e..d7c70d5deecc 100644 --- a/src/plugins/explore/public/components/visualizations/gauge/to_expression.ts +++ b/src/plugins/explore/public/components/visualizations/gauge/to_expression.ts @@ -64,7 +64,7 @@ export const createGauge = ( minBase, maxBase, // TODO: update to use the color from color palette - styleOptions?.thresholdOptions?.baseColor || colors.statusGreen, + styleOptions?.thresholdOptions?.baseColor, styleOptions?.thresholdOptions?.thresholds, calculatedValue ); diff --git a/src/plugins/explore/public/components/visualizations/metric/to_expression.ts b/src/plugins/explore/public/components/visualizations/metric/to_expression.ts index a0a737bb3e1b..be3ab1c70249 100644 --- a/src/plugins/explore/public/components/visualizations/metric/to_expression.ts +++ b/src/plugins/explore/public/components/visualizations/metric/to_expression.ts @@ -7,13 +7,10 @@ import { MetricChartStyle } from './metric_vis_config'; import { VisColumn, VEGASCHEMA, AxisRole, AxisColumnMappings, Threshold } from '../types'; import { getTooltipFormat } from '../utils/utils'; import { calculatePercentage, calculateValue } from '../utils/calculation'; -import { getColors } from '../theme/default_colors'; +import { getColors, DEFAULT_GREY } from '../theme/default_colors'; import { DEFAULT_OPACITY } from '../constants'; import { getUnitById, showDisplayValue } from '../style_panel/unit/collection'; -import { - mergeThresholdsWithBase, - getMaxAndMinBase, -} from '../style_panel/threshold/threshold_utils'; +import { getMaxAndMinBase } from '../style_panel/threshold/threshold_utils'; export const createSingleMetric = ( transformedData: Array>, @@ -63,8 +60,6 @@ export const createSingleMetric = ( calculatedValue ); - const targetValue = calculatedValue ?? 0; - function targetFillColor( useThresholdColor: boolean, threshold?: Threshold[], @@ -72,15 +67,16 @@ export const createSingleMetric = ( ) { const newThreshold = threshold ?? []; - const newBaseColor = baseColor ?? getColors().statusGreen; + if (calculatedValue === undefined || calculatedValue < minBase) { + return useThresholdColor ? DEFAULT_GREY : colorPalette.text; + } + + let textColor = baseColor ?? getColors().statusGreen; - const { textColor, mergedThresholds } = mergeThresholdsWithBase( - minBase, - maxBase, - newBaseColor, - newThreshold, - calculatedValue - ); + for (let i = 0; i < newThreshold.length; i++) { + const { value, color } = newThreshold[i]; + if (calculatedValue !== undefined && calculatedValue >= value) textColor = color; + } const fillColor = useThresholdColor ? textColor : colorPalette.text; From 78a0a77971b9a943a4f6fde4e442ec554d7c9a6a Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:11:13 +0000 Subject: [PATCH 2/5] Changeset file for PR #10909 created/updated --- changelogs/fragments/10909.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/10909.yml diff --git a/changelogs/fragments/10909.yml b/changelogs/fragments/10909.yml new file mode 100644 index 000000000000..5615779cc6ac --- /dev/null +++ b/changelogs/fragments/10909.yml @@ -0,0 +1,2 @@ +feat: +- Simplify threshold logic ([#10909](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10909)) \ No newline at end of file From dede33c82919863ba3dddc919d031b656c63a3fe Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Wed, 12 Nov 2025 18:13:12 +0800 Subject: [PATCH 3/5] fix Signed-off-by: Qxisylolo --- .../components/visualizations/bar_gauge/bar_gauge_utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts index a423204782d5..91c07ab66fa9 100644 --- a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts +++ b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts @@ -158,18 +158,18 @@ export const generateThresholds = ( .sort((a, b) => a - b); const result: Threshold[] = []; let lastBelowIndex = -1; - let lastThresholdValue: Threshold | undefined; + let lastThreshold: Threshold | undefined; for (let i = 0; i < filteredThresholds.length; i++) { const currentThreshold = filteredThresholds[i]; // Handle duplicate values - keep the latest one - if (lastThresholdValue && lastThresholdValue.value === currentThreshold.value) { + if (lastThreshold && lastThreshold.value === currentThreshold.value) { result.pop(); } result.push(currentThreshold); - lastThresholdValue = currentThreshold; + lastThreshold = currentThreshold; // Track last threshold below minBase if (minBase >= currentThreshold.value) { From f504c6d048753c087acf46377cc85e9fa5dc676a Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Thu, 13 Nov 2025 15:50:40 +0800 Subject: [PATCH 4/5] refactor: simplify generate thresholds logic Signed-off-by: Yulong Ruan --- .../bar_gauge/bar_gauge_utils.ts | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts index 91c07ab66fa9..ceb6558dabc4 100644 --- a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts +++ b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts @@ -152,37 +152,36 @@ export const generateThresholds = ( ) => { const defaultColor = baseColor ?? getColors().statusGreen; - const filteredThresholds = thresholds.filter((t) => t.value <= maxBase); + // sort thresholds by value and dedupe threshold by value + thresholds = thresholds + .sort((t1, t2) => t1.value - t2.value) + .reduce((acc, t) => { + const last = acc.pop(); + if (last) { + if (last.value === t.value) { + return [...acc, t]; + } else { + return [...acc, last, t]; + } + } + return [...acc, t]; + }, [] as Threshold[]); + const filteredValueStops = valueStops .filter((v) => v <= maxBase && v >= minBase) .sort((a, b) => a - b); const result: Threshold[] = []; - let lastBelowIndex = -1; - let lastThreshold: Threshold | undefined; - for (let i = 0; i < filteredThresholds.length; i++) { - const currentThreshold = filteredThresholds[i]; - - // Handle duplicate values - keep the latest one - if (lastThreshold && lastThreshold.value === currentThreshold.value) { - result.pop(); + const minThreshold: Threshold = { value: minBase, color: defaultColor }; + for (const threshold of thresholds) { + if (minThreshold.value >= threshold.value) { + minThreshold.color = threshold.color; } - - result.push(currentThreshold); - lastThreshold = currentThreshold; - - // Track last threshold below minBase - if (minBase >= currentThreshold.value) { - lastBelowIndex = i; + if (threshold.value > minThreshold.value && threshold.value <= maxBase) { + result.push(threshold); } } - - if (lastBelowIndex !== -1) { - result.splice(0, lastBelowIndex); - result[0] = { ...result[0], value: minBase }; - } else { - result.unshift({ value: minBase, color: defaultColor }); - } + result.unshift(minThreshold); const valueResults: Threshold[] = []; if (filteredValueStops.length > 0 && result.length > 0) { From dc51eb106de95067623a986ac3553ad87b808110 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Fri, 14 Nov 2025 11:23:31 +0800 Subject: [PATCH 5/5] small fix Signed-off-by: Qxisylolo --- .../bar_gauge/bar_gauge_utils.test.ts | 172 ++++++++---------- .../bar_gauge/bar_gauge_utils.ts | 38 ++-- .../visualizations/bar_gauge/to_expression.ts | 10 +- .../visualizations/metric/to_expression.ts | 16 +- .../style_panel/threshold/threshold_utils.ts | 2 +- 5 files changed, 103 insertions(+), 135 deletions(-) diff --git a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.test.ts b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.test.ts index 06a9d76b0c88..da945c955ba4 100644 --- a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.test.ts +++ b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.test.ts @@ -9,13 +9,11 @@ import { symbolOpposite, getGradientConfig, generateThresholds, + generateValueThresholds, } from './bar_gauge_utils'; import { AxisColumnMappings, Threshold, VisFieldType } from '../types'; import { BarGaugeChartStyle } from './bar_gauge_vis_config'; - -jest.mock('../theme/default_colors', () => ({ - getColors: jest.fn(() => ({ statusGreen: '#00FF00' })), -})); +import { getColors } from '../theme/default_colors'; describe('bar_gauge_utils', () => { describe('getBarOrientation', () => { @@ -142,56 +140,55 @@ describe('bar_gauge_utils', () => { describe('Basic functionality', () => { it('should handle empty thresholds array', () => { - const result = generateThresholds(0, 100, [], '#BLUE', []); + const result = generateThresholds(0, 100, [], '#BLUE'); - expect(result.mergedThresholds).toHaveLength(1); - expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); - expect(result.valueThresholds).toEqual([]); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ value: 0, color: '#BLUE' }); }); it('should use default color when baseColor is undefined', () => { - const result = generateThresholds(0, 100, [], undefined, []); + const result = generateThresholds(0, 100, [], undefined); - expect(result.mergedThresholds).toHaveLength(1); - expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#00FF00' }); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ value: 0, color: '#00BD6B' }); }); it('should process normal thresholds correctly', () => { - const result = generateThresholds(0, 100, mockThresholds, '#BLUE', []); + const result = generateThresholds(0, 100, mockThresholds, '#BLUE'); - expect(result.mergedThresholds).toHaveLength(4); - expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); - expect(result.mergedThresholds[1]).toEqual({ value: 10, color: '#FF0000' }); - expect(result.mergedThresholds[2]).toEqual({ value: 50, color: '#FFFF00' }); - expect(result.mergedThresholds[3]).toEqual({ value: 80, color: '#0037ffff' }); + expect(result).toHaveLength(4); + expect(result[0]).toEqual({ value: 0, color: '#BLUE' }); + expect(result[1]).toEqual({ value: 10, color: '#FF0000' }); + expect(result[2]).toEqual({ value: 50, color: '#FFFF00' }); + expect(result[3]).toEqual({ value: 80, color: '#0037ffff' }); }); }); describe('Threshold filtering and range handling', () => { it('should filter thresholds above maxBase', () => { - const result = generateThresholds(0, 60, mockThresholds, '#BLUE', []); + const result = generateThresholds(0, 60, mockThresholds, '#BLUE'); - expect(result.mergedThresholds).toHaveLength(3); - expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); - expect(result.mergedThresholds[1]).toEqual({ value: 10, color: '#FF0000' }); - expect(result.mergedThresholds[2]).toEqual({ value: 50, color: '#FFFF00' }); + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ value: 0, color: '#BLUE' }); + expect(result[1]).toEqual({ value: 10, color: '#FF0000' }); + expect(result[2]).toEqual({ value: 50, color: '#FFFF00' }); // Should not include the threshold with value 80 }); it('should handle minBase higher than first threshold', () => { - const result = generateThresholds(25, 100, mockThresholds, '#BLUE', []); + const result = generateThresholds(25, 100, mockThresholds, '#BLUE'); - expect(result.mergedThresholds).toHaveLength(3); - expect(result.mergedThresholds[0]).toEqual({ value: 25, color: '#FF0000' }); - expect(result.mergedThresholds[1]).toEqual({ value: 50, color: '#FFFF00' }); - expect(result.mergedThresholds[2]).toEqual({ value: 80, color: '#0037ffff' }); + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ value: 25, color: '#FF0000' }); + expect(result[1]).toEqual({ value: 50, color: '#FFFF00' }); + expect(result[2]).toEqual({ value: 80, color: '#0037ffff' }); }); it('should handle minBase higher than all thresholds', () => { - const result = generateThresholds(90, 100, mockThresholds, '#BLUE', []); + const result = generateThresholds(90, 100, mockThresholds, '#BLUE'); - expect(result.mergedThresholds).toHaveLength(1); - expect(result.mergedThresholds[0]).toEqual({ value: 90, color: '#0037ffff' }); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ value: 90, color: '#0037ffff' }); }); }); @@ -204,109 +201,82 @@ describe('bar_gauge_utils', () => { { value: 80, color: '#00FF00' }, ]; - const result = generateThresholds(0, 100, duplicateThresholds, '#BLUE', []); + const result = generateThresholds(0, 100, duplicateThresholds, '#BLUE'); - expect(result.mergedThresholds).toHaveLength(4); - expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); - expect(result.mergedThresholds[1]).toEqual({ value: 10, color: '#FF0000' }); - expect(result.mergedThresholds[2]).toEqual({ value: 50, color: '#00FFFF' }); // Latest color - expect(result.mergedThresholds[3]).toEqual({ value: 80, color: '#00FF00' }); + expect(result).toHaveLength(4); + expect(result[0]).toEqual({ value: 0, color: '#BLUE' }); + expect(result[1]).toEqual({ value: 10, color: '#FF0000' }); + expect(result[2]).toEqual({ value: 50, color: '#00FFFF' }); // Latest color + expect(result[3]).toEqual({ value: 80, color: '#00FF00' }); }); }); + }); + + describe('generateValueThresholds', () => { + const mockThresholds: Threshold[] = [ + { value: 10, color: '#FF0000' }, + { value: 50, color: '#FFFF00' }, + { value: 80, color: '#0037ffff' }, + ]; + + afterEach(() => { + jest.restoreAllMocks(); + }); describe('Value stops processing', () => { it('should process value stops correctly', () => { const valueStops = [15, 45, 75]; - const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops); + const result = generateValueThresholds(0, 100, valueStops, mockThresholds); - expect(result.valueThresholds).toHaveLength(3); - expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' }); // Uses threshold at 10 - expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' }); // Uses threshold at 10 - expect(result.valueThresholds[2]).toEqual({ value: 75, color: '#FFFF00' }); // Uses threshold at 50 + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ value: 15, color: '#FF0000' }); // Uses threshold at 10 + expect(result[1]).toEqual({ value: 45, color: '#FF0000' }); // Uses threshold at 10 + expect(result[2]).toEqual({ value: 75, color: '#FFFF00' }); // Uses threshold at 50 }); it('should filter value stops outside range', () => { const valueStops = [5, 15, 45, 95, 105]; // 105 is above maxBase, 5 is below minBase (when minBase > 0) - const result = generateThresholds(10, 90, mockThresholds, '#BLUE', valueStops); + const result = generateValueThresholds(10, 90, valueStops, mockThresholds); // Should only include stops between minBase (10) and maxBase (90) - expect(result.valueThresholds).toHaveLength(2); - expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' }); - expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' }); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ value: 15, color: '#FF0000' }); + expect(result[1]).toEqual({ value: 45, color: '#FF0000' }); }); it('should handle duplicate value stops', () => { const valueStops = [15, 15, 45, 45, 75]; // Duplicates - const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops); - - expect(result.valueThresholds).toHaveLength(3); // Should deduplicate - expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' }); - expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' }); - expect(result.valueThresholds[2]).toEqual({ value: 75, color: '#FFFF00' }); + const result = generateValueThresholds(0, 100, valueStops, mockThresholds); + expect(result).toHaveLength(3); // Should deduplicate + expect(result[0]).toEqual({ value: 15, color: '#FF0000' }); + expect(result[1]).toEqual({ value: 45, color: '#FF0000' }); + expect(result[2]).toEqual({ value: 75, color: '#FFFF00' }); }); it('should handle unsorted value stops', () => { const valueStops = [75, 15, 45]; // Unsorted - const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops); + const result = generateValueThresholds(0, 100, valueStops, mockThresholds); - expect(result.valueThresholds).toHaveLength(3); - expect(result.valueThresholds[0]).toEqual({ value: 15, color: '#FF0000' }); - expect(result.valueThresholds[1]).toEqual({ value: 45, color: '#FF0000' }); - expect(result.valueThresholds[2]).toEqual({ value: 75, color: '#FFFF00' }); + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ value: 15, color: '#FF0000' }); + expect(result[1]).toEqual({ value: 45, color: '#FF0000' }); + expect(result[2]).toEqual({ value: 75, color: '#FFFF00' }); }); it('should handle empty value stops array', () => { - const result = generateThresholds(0, 100, mockThresholds, '#BLUE', []); + const result = generateValueThresholds(0, 100, [], mockThresholds); - expect(result.valueThresholds).toEqual([]); - }); - }); - - describe('Edge cases', () => { - it('should handle single threshold', () => { - const singleThreshold: Threshold[] = [{ value: 50, color: '#FF0000' }]; - const result = generateThresholds(0, 100, singleThreshold, '#BLUE', [25, 75]); - - expect(result.mergedThresholds).toHaveLength(2); - expect(result.mergedThresholds[0]).toEqual({ value: 0, color: '#BLUE' }); - expect(result.mergedThresholds[1]).toEqual({ value: 50, color: '#FF0000' }); - - expect(result.valueThresholds).toHaveLength(2); - expect(result.valueThresholds[0]).toEqual({ value: 25, color: '#BLUE' }); - expect(result.valueThresholds[1]).toEqual({ value: 75, color: '#FF0000' }); - }); - - it('should handle minBase equal to maxBase', () => { - const result = generateThresholds(50, 50, mockThresholds, '#BLUE', []); - - expect(result.mergedThresholds).toHaveLength(1); - expect(result.mergedThresholds[0]).toEqual({ value: 50, color: '#FFFF00' }); + expect(result).toEqual([]); }); it('should handle value stops equal to threshold values', () => { const valueStops = [10, 50, 80]; // Exact threshold values - const result = generateThresholds(0, 100, mockThresholds, '#BLUE', valueStops); - - expect(result.valueThresholds).toHaveLength(3); - expect(result.valueThresholds[0]).toEqual({ value: 10, color: '#FF0000' }); - expect(result.valueThresholds[1]).toEqual({ value: 50, color: '#FFFF00' }); - expect(result.valueThresholds[2]).toEqual({ value: 80, color: '#0037ffff' }); - }); - - it('should handle negative values', () => { - const negativeThresholds: Threshold[] = [ - { value: -50, color: '#FF0000' }, - { value: 0, color: '#FFFF00' }, - { value: 50, color: '#00FF00' }, - ]; - - const result = generateThresholds(-100, 100, negativeThresholds, '#BLUE', [-25, 25]); + const result = generateValueThresholds(0, 100, valueStops, mockThresholds); - expect(result.mergedThresholds).toHaveLength(4); - expect(result.mergedThresholds[0]).toEqual({ value: -100, color: '#BLUE' }); - expect(result.valueThresholds).toHaveLength(2); - expect(result.valueThresholds[0]).toEqual({ value: -25, color: '#FF0000' }); - expect(result.valueThresholds[1]).toEqual({ value: 25, color: '#FFFF00' }); + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ value: 10, color: '#FF0000' }); + expect(result[1]).toEqual({ value: 50, color: '#FFFF00' }); + expect(result[2]).toEqual({ value: 80, color: '#0037ffff' }); }); }); }); diff --git a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts index ceb6558dabc4..4abcc14085c1 100644 --- a/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts +++ b/src/plugins/explore/public/components/visualizations/bar_gauge/bar_gauge_utils.ts @@ -147,8 +147,7 @@ export const generateThresholds = ( minBase: number, maxBase: number, thresholds: Threshold[], - baseColor: string | undefined, - valueStops: number[] + baseColor: string | undefined ) => { const defaultColor = baseColor ?? getColors().statusGreen; @@ -167,9 +166,6 @@ export const generateThresholds = ( return [...acc, t]; }, [] as Threshold[]); - const filteredValueStops = valueStops - .filter((v) => v <= maxBase && v >= minBase) - .sort((a, b) => a - b); const result: Threshold[] = []; const minThreshold: Threshold = { value: minBase, color: defaultColor }; @@ -183,26 +179,38 @@ export const generateThresholds = ( } result.unshift(minThreshold); - const valueResults: Threshold[] = []; - if (filteredValueStops.length > 0 && result.length > 0) { + return result; +}; + +export const generateValueThresholds = ( + minBase: number, + maxBase: number, + valueStops: number[], + thresholds: Threshold[] +) => { + const filteredValueStops = valueStops + .filter((v) => v <= maxBase && v >= minBase) + .sort((a, b) => a - b); + + const valueThresholds: Threshold[] = []; + if (filteredValueStops.length > 0 && thresholds.length > 0) { const stops = [...new Set(filteredValueStops)]; let thresholdIndex = 0; for (const stop of stops) { - while (thresholdIndex < result.length - 1 && result[thresholdIndex + 1].value <= stop) { + while ( + thresholdIndex < thresholds.length - 1 && + thresholds[thresholdIndex + 1].value <= stop + ) { thresholdIndex++; } // Add valid threshold for this stop - if (result[thresholdIndex].value <= stop) { - valueResults.push({ value: stop, color: result[thresholdIndex].color }); + if (thresholds[thresholdIndex].value <= stop) { + valueThresholds.push({ value: stop, color: thresholds[thresholdIndex].color }); } } } - - return { - mergedThresholds: result, - valueThresholds: valueResults, - }; + return valueThresholds; }; diff --git a/src/plugins/explore/public/components/visualizations/bar_gauge/to_expression.ts b/src/plugins/explore/public/components/visualizations/bar_gauge/to_expression.ts index 5bb45b08f343..fd8d0972860d 100644 --- a/src/plugins/explore/public/components/visualizations/bar_gauge/to_expression.ts +++ b/src/plugins/explore/public/components/visualizations/bar_gauge/to_expression.ts @@ -15,6 +15,7 @@ import { symbolOpposite, generateParams, generateThresholds, + generateValueThresholds, } from './bar_gauge_utils'; import { getUnitById, showDisplayValue } from '../style_panel/unit/collection'; @@ -99,17 +100,18 @@ export const createBarGaugeSpec = ( ] : styleOptions?.thresholdOptions?.thresholds; - const { mergedThresholds, valueThresholds } = generateThresholds( + const mergedThresholds = generateThresholds( minBase, maxBase, styleOptions?.thresholdOptions?.thresholds ?? [], - styleOptions?.thresholdOptions?.baseColor, - valueStops + styleOptions?.thresholdOptions?.baseColor ); const processedThresholds = [ ...mergedThresholds, - ...(styleOptions?.exclusive.displayMode === 'gradient' ? valueThresholds : []), + ...(styleOptions?.exclusive.displayMode === 'gradient' + ? generateValueThresholds(minBase, maxBase, valueStops, mergedThresholds) + : []), ].sort((a, b) => a.value - b.value); const gradientParams = generateParams(processedThresholds, styleOptions, isXaxisNumerical); diff --git a/src/plugins/explore/public/components/visualizations/metric/to_expression.ts b/src/plugins/explore/public/components/visualizations/metric/to_expression.ts index be3ab1c70249..b67bf1799fdf 100644 --- a/src/plugins/explore/public/components/visualizations/metric/to_expression.ts +++ b/src/plugins/explore/public/components/visualizations/metric/to_expression.ts @@ -10,7 +10,6 @@ import { calculatePercentage, calculateValue } from '../utils/calculation'; import { getColors, DEFAULT_GREY } from '../theme/default_colors'; import { DEFAULT_OPACITY } from '../constants'; import { getUnitById, showDisplayValue } from '../style_panel/unit/collection'; -import { getMaxAndMinBase } from '../style_panel/threshold/threshold_utils'; export const createSingleMetric = ( transformedData: Array>, @@ -36,12 +35,9 @@ export const createSingleMetric = ( const percentageSize = styles.percentageSize; let numericalValues: number[] = []; - let maxNumber: number = 0; - let minNumber: number = 0; + if (numericField) { numericalValues = transformedData.map((d) => d[numericField]); - maxNumber = Math.max(...numericalValues); - minNumber = Math.min(...numericalValues); } const calculatedValue = calculateValue(numericalValues, styles.valueCalculation); @@ -52,14 +48,6 @@ export const createSingleMetric = ( const displayValue = showDisplayValue(isValidNumber, selectedUnit, calculatedValue); - const { minBase, maxBase } = getMaxAndMinBase( - minNumber, - maxNumber, - styles?.min, - styles?.max, - calculatedValue - ); - function targetFillColor( useThresholdColor: boolean, threshold?: Threshold[], @@ -67,7 +55,7 @@ export const createSingleMetric = ( ) { const newThreshold = threshold ?? []; - if (calculatedValue === undefined || calculatedValue < minBase) { + if (calculatedValue === undefined) { return useThresholdColor ? DEFAULT_GREY : colorPalette.text; } diff --git a/src/plugins/explore/public/components/visualizations/style_panel/threshold/threshold_utils.ts b/src/plugins/explore/public/components/visualizations/style_panel/threshold/threshold_utils.ts index c9d6dbeea7dc..f542d5b14c8e 100644 --- a/src/plugins/explore/public/components/visualizations/style_panel/threshold/threshold_utils.ts +++ b/src/plugins/explore/public/components/visualizations/style_panel/threshold/threshold_utils.ts @@ -25,7 +25,7 @@ export function mergeThresholdsWithBase( // Handle empty thresholds if (!validThresholds || validThresholds.length === 0) { return { - textColor: !targetValue || targetValue < minBase ? DEFAULT_GREY : defaultColor, + textColor: !targetValue ? DEFAULT_GREY : defaultColor, mergedThresholds: [{ value: minBase, color: defaultColor }], }; }