diff --git a/src/lib/components/barchartv2/Barchart.component.test.tsx b/src/lib/components/barchartv2/Barchart.component.test.tsx index 7d2fa33a1c..08a2fcae11 100644 --- a/src/lib/components/barchartv2/Barchart.component.test.tsx +++ b/src/lib/components/barchartv2/Barchart.component.test.tsx @@ -230,15 +230,37 @@ describe('Barchart', () => { }); }); - describe('Reference line', () => { - it('should render with reference line', () => { - const { Wrapper } = getWrapper(); - render( - - - , - ); - expect(screen.getByText('50')).toBeInTheDocument(); - }); + it('should render stacked bars', () => { + const testStackedBars: BarchartProps['bars'] = [ + { + label: 'Success', + data: [ + ['category1', 10], + ['category2', 20], + ['category3', 30], + ], + color: 'green', + }, + { + label: 'Failed', + data: [ + ['category1', 5], + ['category2', 8], + ['category3', 12], + ], + color: 'red', + }, + ]; + + const { Wrapper } = getWrapper(); + render( + + + , + ); + + expect(screen.getByText('category1')).toBeInTheDocument(); + expect(screen.getByText('category2')).toBeInTheDocument(); + expect(screen.getByText('category3')).toBeInTheDocument(); }); }); diff --git a/src/lib/components/barchartv2/Barchart.component.tsx b/src/lib/components/barchartv2/Barchart.component.tsx index a0a2104ffe..b0e45ae34a 100644 --- a/src/lib/components/barchartv2/Barchart.component.tsx +++ b/src/lib/components/barchartv2/Barchart.component.tsx @@ -13,7 +13,7 @@ import styled, { useTheme } from 'styled-components'; import { computeUnitLabelAndRoundReferenceValue, formatPrometheusDataToChartData, - getMaxValue, + getMaxBarValue, UnitRange, } from './utils'; @@ -111,10 +111,15 @@ const StyledResponsiveContainer = styled(ResponsiveContainer)` const Barchart = (props: BarchartProps) => { const theme = useTheme(); - const { height = 200, bars, type = 'category', unitRange } = props; + const { height = 200, bars, type = 'category', unitRange, stacked } = props; - const { data, rechartsBars } = formatPrometheusDataToChartData(bars, type); - const maxValue = getMaxValue(data); + const { data, rechartsBars } = formatPrometheusDataToChartData( + bars, + type, + stacked, + ); + + const maxValue = getMaxBarValue(data, stacked); const { unitLabel, roundReferenceValue, rechartsData } = computeUnitLabelAndRoundReferenceValue(data, maxValue, unitRange); @@ -128,6 +133,7 @@ const Barchart = (props: BarchartProps) => { dataKey={bar.dataKey} fill={bar.fill} minPointSize={3} + stackId={stacked ? 'stacked' : undefined} /> ))} diff --git a/src/lib/components/barchartv2/utils.test.ts b/src/lib/components/barchartv2/utils.test.ts index 47ef413232..ba3ecc985e 100644 --- a/src/lib/components/barchartv2/utils.test.ts +++ b/src/lib/components/barchartv2/utils.test.ts @@ -1,8 +1,11 @@ +import { BarchartProps } from './Barchart.component'; + import { computeUnitLabelAndRoundReferenceValue, formatPrometheusDataToChartData, - getMaxValue, + getMaxBarValue, getRoundReferenceValue, + sortStackedBars, UnitRange, } from './utils'; @@ -25,19 +28,28 @@ describe('getRoundReferenceValue', () => { }); }); -describe('getMaxValue', () => { +describe('getMaxBarValue', () => { it('should return the maximum value from chart data', () => { const data = [ { category: 'A', value1: 10, value2: 5 }, { category: 'B', value1: 20, value2: 15 }, { category: 'C', value1: 8, value2: 25 }, ]; - expect(getMaxValue(data)).toBe(25); + expect(getMaxBarValue(data)).toBe(25); }); it('should handle single value data', () => { const data = [{ category: 'A', value: 42 }]; - expect(getMaxValue(data)).toBe(42); + expect(getMaxBarValue(data)).toBe(42); + }); + + it('should return the maximum value from stacked data', () => { + const data = [ + { category: 'A', value1: 10, value2: 5 }, + { category: 'B', value1: 20, value2: 15 }, + { category: 'C', value1: 8, value2: 25 }, + ]; + expect(getMaxBarValue(data, true)).toBe(35); }); }); @@ -219,65 +231,150 @@ describe('formatPrometheusDataToChartData', () => { ]); }); }); - describe('computeUnitLabelAndRoundReferenceValue', () => { - it('should compute the unit label and round reference value correctly when reaching threshold', () => { - const data = [ - { - category: 'category1', - success: 1680, - }, - ]; - const maxValue = 1680; - const unitRange: UnitRange = [ - { - threshold: 1000, - label: 'kB', - }, - ]; - const result = computeUnitLabelAndRoundReferenceValue( - data, - maxValue, - unitRange, - ); + describe('Stacked Bar Sorting', () => { + const bars: BarchartProps['bars'] = [ + { + label: 'Small Bar', + data: [ + ['category1', 5], + ['category2', 10], + ['category3', 15], + ], + color: 'blue', + }, + { + label: 'Large Bar', + data: [ + ['category1', 50], + ['category2', 60], + ['category3', 70], + ], + color: 'red', + }, + { + label: 'Medium Bar', + data: [ + ['category1', 20], + ['category2', 25], + ['category3', 30], + ], + color: 'green', + }, + ]; + const type: BarchartProps['type'] = 'category'; + it('should sort bars by average values in descending order when stacked is true', () => { + const result = formatPrometheusDataToChartData(bars, type, true); - expect(result.unitLabel).toBe('kB'); - expect(result.roundReferenceValue).toBe(10); - expect(result.rechartsData).toEqual([ - { - category: 'category1', - success: 1.68, - }, - ]); + // Bars should be sorted by average in descending order (largest first) + expect(result.rechartsBars[0].dataKey).toBe('largebar'); // Average: 60 + expect(result.rechartsBars[1].dataKey).toBe('mediumbar'); // Average: 25 + expect(result.rechartsBars[2].dataKey).toBe('smallbar'); // Average: 10 }); - it('should compute the unit label and round reference value correctly when threshold is 0', () => { - const data = [ - { - category: 'category1', - success: 680, - }, - ]; - const maxValue = 680; - const unitRange: UnitRange = [ - { - threshold: 0, - label: 'B', - }, - { - threshold: 1000, - label: 'kB', - }, - ]; - const result = computeUnitLabelAndRoundReferenceValue( - data, - maxValue, - unitRange, - ); - expect(result.unitLabel).toBe('B'); - expect(result.roundReferenceValue).toBe(1000); - expect(result.rechartsData).toEqual([ - { category: 'category1', success: 680 }, - ]); + it('should not sort bars when stacked is false or undefined', () => { + const result = formatPrometheusDataToChartData(bars, type, false); + + // Bars should maintain original order + expect(result.rechartsBars[0].dataKey).toBe('smallbar'); + expect(result.rechartsBars[1].dataKey).toBe('largebar'); }); }); }); + +describe('computeUnitLabelAndRoundReferenceValue', () => { + it('should compute the unit label and round reference value correctly when reaching threshold', () => { + const data = [ + { + category: 'category1', + success: 1680, + }, + ]; + const maxValue = 1680; + const unitRange: UnitRange = [ + { + threshold: 1000, + label: 'kB', + }, + ]; + const result = computeUnitLabelAndRoundReferenceValue( + data, + maxValue, + unitRange, + ); + + expect(result.unitLabel).toBe('kB'); + expect(result.roundReferenceValue).toBe(10); + expect(result.rechartsData).toEqual([ + { + category: 'category1', + success: 1.68, + }, + ]); + }); + it('should compute the unit label and round reference value correctly when threshold is 0', () => { + const data = [ + { + category: 'category1', + success: 680, + }, + ]; + const maxValue = 680; + const unitRange: UnitRange = [ + { + threshold: 0, + label: 'B', + }, + { + threshold: 1000, + label: 'kB', + }, + ]; + const result = computeUnitLabelAndRoundReferenceValue( + data, + maxValue, + unitRange, + ); + + expect(result.unitLabel).toBe('B'); + expect(result.roundReferenceValue).toBe(1000); + expect(result.rechartsData).toEqual([ + { category: 'category1', success: 680 }, + ]); + }); +}); +describe('sortStackedBars', () => { + const bars = [ + { dataKey: 'bar1', fill: 'blue' }, + { dataKey: 'bar2', fill: 'red' }, + { dataKey: 'bar3', fill: 'green' }, + ]; + const data = [ + { bar1: 10, bar2: 20, bar3: 30 }, + { bar1: 40, bar2: 50, bar3: 60 }, + { bar1: 70, bar2: 80, bar3: 90 }, + ]; + it('should sort bars by average values in descending order when stacked is true', () => { + const result = sortStackedBars(bars, data, true); + expect(result).toEqual([ + { dataKey: 'bar3', fill: 'green' }, + { dataKey: 'bar2', fill: 'red' }, + { dataKey: 'bar1', fill: 'blue' }, + ]); + }); + it('should not sort bars when stacked is false', () => { + const result = sortStackedBars(bars, data, false); + expect(result).toEqual([ + { dataKey: 'bar1', fill: 'blue' }, + { dataKey: 'bar2', fill: 'red' }, + { dataKey: 'bar3', fill: 'green' }, + ]); + }); + it('should not sort bars when stacked is undefined', () => { + const result = sortStackedBars(bars, data, undefined); + expect(result).toEqual([ + { dataKey: 'bar1', fill: 'blue' }, + { dataKey: 'bar2', fill: 'red' }, + { dataKey: 'bar3', fill: 'green' }, + ]); + }); +}); diff --git a/src/lib/components/barchartv2/utils.ts b/src/lib/components/barchartv2/utils.ts index e8518996fc..9a6f61e25a 100644 --- a/src/lib/components/barchartv2/utils.ts +++ b/src/lib/components/barchartv2/utils.ts @@ -27,12 +27,28 @@ export const getMinValue = (data: { [key: string]: string | number }[]) => { return Math.min(...values); }; -export const getMaxValue = (data: { [key: string]: string | number }[]) => { +export const getMaxBarValue = ( + data: { [key: string]: string | number }[], + stacked?: boolean, +) => { const values = data.map((item) => { + // If stacked, we need to filter out category and sum the values in the same object + if (stacked) { + // Get objects keys except category + const filterOutCategory = Object.keys(item).filter( + (key) => key !== 'category', + ); + // Sum the values in the same object (corresponding to one bar) based on the keys + const sumValues = filterOutCategory.reduce((acc, curr) => { + return acc + Number(item[curr]); + }, 0); + return sumValues; + } //filter out the category key const numberValues = Object.keys(item) .filter((key) => key !== 'category') .map((key) => Number(item[key])); + // Get the max value among the values in the object (corresponding to one bar) return Math.max(...numberValues); }); return Math.max(...values); @@ -122,6 +138,7 @@ const findRangeForTimestamp = ( export const formatPrometheusDataToChartData = ( bars: BarchartProps['bars'], type: BarchartProps['type'], + stacked?: boolean, ): { data: { [key: string]: string | number; @@ -131,7 +148,7 @@ export const formatPrometheusDataToChartData = ( fill: string; }[]; } => { - const rechartsBars = bars.map((bar) => ({ + let rechartsBars = bars.map((bar) => ({ dataKey: bar.label.toLowerCase().replace(/\s+/g, ''), fill: bar.color, })); @@ -211,6 +228,9 @@ export const formatPrometheusDataToChartData = ( // Convert map to array (order is preserved for time ranges) const data = Array.from(categoryMap.values()); + // Sort stacked bars + rechartsBars = sortStackedBars(rechartsBars, data, stacked); + return { rechartsBars, data, @@ -297,3 +317,33 @@ export function getUnitLabel( unitLabel: unitRange[index - 1].label, }; } + +// Sort stacked bars by their average values in descending order +// This ensures the largest bars appear at the bottom of the stack +export const sortStackedBars = ( + rechartsBars: { + dataKey: string; + fill: string; + }[], + data: { + [key: string]: string | number; + }[], + stacked?: boolean, +) => { + if (!stacked) { + return rechartsBars; + } + const barAverages = rechartsBars.map((bar) => { + const values = data + .map((item) => Number(item[bar.dataKey]) || 0) + .filter((value) => !isNaN(value)); + const average = + values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; + return { ...bar, average }; + }); + + // Sort by average in descending order (largest first, which will be at bottom in stack) + barAverages.sort((a, b) => b.average - a.average); + // Remove the average property and keep only the bar data + return barAverages.map(({ average, ...bar }) => bar); +}; diff --git a/stories/BarChart/barchart.stories.tsx b/stories/BarChart/barchart.stories.tsx index 9a642a95e1..47bc200214 100644 --- a/stories/BarChart/barchart.stories.tsx +++ b/stories/BarChart/barchart.stories.tsx @@ -329,13 +329,33 @@ export const CategoryWithMissingData: Story = { return ; }, }; +const capacityDataWithUnitRange: BarchartProps['bars'] = [ + { + label: 'Free', + data: [ + ['category1', 2000000], + ['category2', 4000000], + ['category3', 6000000], + ], + color: 'blue', + }, + { + label: 'Used', + data: [ + ['category1', 8000000], + ['category2', 10000000], + ['category3', 12000000], + ], + color: 'lightblue', + }, +]; export const CapacityWithUnitRange: Story = { render: () => { return ( { + return ; + }, +};