Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/10909.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Simplify threshold logic ([#10909](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10909))
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
thresholdsToGradient,
symbolOpposite,
getGradientConfig,
generateThresholds,
generateValueThresholds,
} from './bar_gauge_utils';
import { AxisColumnMappings, Threshold, VisFieldType } from '../types';
import { BarGaugeChartStyle } from './bar_gauge_vis_config';
import { getColors } from '../theme/default_colors';

describe('bar_gauge_utils', () => {
describe('getBarOrientation', () => {
Expand Down Expand Up @@ -123,4 +126,158 @@ 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).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);

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');

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');

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');

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');

expect(result).toHaveLength(1);
expect(result[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).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 = generateValueThresholds(0, 100, valueStops, mockThresholds);

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 = generateValueThresholds(10, 90, valueStops, mockThresholds);

// Should only include stops between minBase (10) and maxBase (90)
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 = 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 = generateValueThresholds(0, 100, valueStops, mockThresholds);

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 = generateValueThresholds(0, 100, [], mockThresholds);

expect(result).toEqual([]);
});

it('should handle value stops equal to threshold values', () => {
const valueStops = [10, 50, 80]; // Exact threshold values
const result = generateValueThresholds(0, 100, valueStops, mockThresholds);

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' });
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -157,3 +142,75 @@ export const generateParams = (

return result;
};

export const generateThresholds = (
minBase: number,
maxBase: number,
thresholds: Threshold[],
baseColor: string | undefined
) => {
const defaultColor = baseColor ?? getColors().statusGreen;

// 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 result: Threshold[] = [];

const minThreshold: Threshold = { value: minBase, color: defaultColor };
for (const threshold of thresholds) {
if (minThreshold.value >= threshold.value) {
minThreshold.color = threshold.color;
}
if (threshold.value > minThreshold.value && threshold.value <= maxBase) {
result.push(threshold);
}
}
result.unshift(minThreshold);

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 < thresholds.length - 1 &&
thresholds[thresholdIndex + 1].value <= stop
) {
thresholdIndex++;
}

// Add valid threshold for this stop
if (thresholds[thresholdIndex].value <= stop) {
valueThresholds.push({ value: stop, color: thresholds[thresholdIndex].color });
}
}
}
return valueThresholds;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import {
getBarOrientation,
thresholdsToGradient,
symbolOpposite,
processThresholds,
generateParams,
generateThresholds,
generateValueThresholds,
} 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<Record<string, any>>,
Expand Down Expand Up @@ -51,6 +51,7 @@ export const createBarGaugeSpec = (
let maxNumber: number = -Infinity;
let minNumber: number = Infinity;
let maxTextLength: number = 0;
const valueStops: number[] = [];

const selectedUnit = getUnitById(styleOptions?.unitId);

Expand All @@ -75,6 +76,7 @@ export const createBarGaugeSpec = (
[numericField]: isValidNumber ? calculate : null,
displayValue,
});
valueStops.push(...(isValidNumber ? [calculate] : []));
}
}

Expand All @@ -98,36 +100,19 @@ export const createBarGaugeSpec = (
]
: styleOptions?.thresholdOptions?.thresholds;

const { textColor, mergedThresholds } = mergeThresholdsWithBase(
const mergedThresholds = generateThresholds(
minBase,
maxBase,
styleOptions?.thresholdOptions?.baseColor,
styleOptions?.thresholdOptions?.thresholds
styleOptions?.thresholdOptions?.thresholds ?? [],
styleOptions?.thresholdOptions?.baseColor
);

// 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'
? generateValueThresholds(minBase, maxBase, valueStops, mergedThresholds)
: []),
].sort((a, b) => a.value - b.value);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
Loading
Loading