diff --git a/change/@fluentui-react-charts-51a0e8da-4489-49c2-a76b-c81c283c0cd4.json b/change/@fluentui-react-charts-51a0e8da-4489-49c2-a76b-c81c283c0cd4.json new file mode 100644 index 00000000000000..cb29cc5f9d5649 --- /dev/null +++ b/change/@fluentui-react-charts-51a0e8da-4489-49c2-a76b-c81c283c0cd4.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "implement stacked hbc with axis and support for negative x values", + "packageName": "@fluentui/react-charts", + "email": "anushgupta@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/charts/react-charts/library/etc/react-charts.api.md b/packages/charts/react-charts/library/etc/react-charts.api.md index 607b996f5e6292..c7830365bea894 100644 --- a/packages/charts/react-charts/library/etc/react-charts.api.md +++ b/packages/charts/react-charts/library/etc/react-charts.api.md @@ -1035,6 +1035,7 @@ export interface ModifiedCartesianChartProps extends CartesianChartProps { // (undocumented) getAxisData?: any; getDomainMargins?: (containerWidth: number) => Margins; + getDomainNRangeValues?: (points: HorizontalBarChartWithAxisDataPoint[], margins: Margins, width: number, chartType: ChartTypes, isRTL: boolean, xAxisType: XAxisTypes, barWidth: number, tickValues: Date[] | number[] | string[] | undefined, shiftX: number) => IDomainNRange; getGraphData?: any; getmargins?: (margins: Margins) => void; getMinMaxOfYAxis?: (points: DataPoint[], yAxisType: YAxisType | undefined, useSecondaryYScale?: boolean) => { diff --git a/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.tsx b/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.tsx index d7417cee2d8526..ad455c8516af95 100644 --- a/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.tsx +++ b/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.tsx @@ -217,18 +217,31 @@ export const CartesianChart: React.FunctionComponent { startValue: number; endValue: number }; + + /**Add commentMore actions + * Get the domain and range values + */ + getDomainNRangeValues?: ( + points: HorizontalBarChartWithAxisDataPoint[], + margins: Margins, + width: number, + chartType: ChartTypes, + isRTL: boolean, + xAxisType: XAxisTypes, + barWidth: number, + tickValues: Date[] | number[] | string[] | undefined, + shiftX: number, + ) => IDomainNRange; } diff --git a/packages/charts/react-charts/library/src/components/DeclarativeChart/PlotlySchemaAdapter.ts b/packages/charts/react-charts/library/src/components/DeclarativeChart/PlotlySchemaAdapter.ts index 3044eb07057898..7fe79f44a8b4e9 100644 --- a/packages/charts/react-charts/library/src/components/DeclarativeChart/PlotlySchemaAdapter.ts +++ b/packages/charts/react-charts/library/src/components/DeclarativeChart/PlotlySchemaAdapter.ts @@ -522,11 +522,12 @@ export const transformPlotlyJsonToHorizontalBarWithAxisProps = ( const chartData: HorizontalBarChartWithAxisDataPoint[] = input.data .map((series: PlotData, index: number) => { return (series.y as Datum[]).map((yValue: string, i: number) => { - const color = getColor(yValue, colorMap, isDarkTheme); + const legendName = series.name ?? yValue; + const color = getColor(legendName, colorMap, isDarkTheme); return { x: series.x[i], y: yValue, - legend: yValue, + legend: legendName, color, } as HorizontalBarChartWithAxisDataPoint; }); diff --git a/packages/charts/react-charts/library/src/components/DeclarativeChart/__snapshots__/PlotlySchemaAdapterUT.test.tsx.snap b/packages/charts/react-charts/library/src/components/DeclarativeChart/__snapshots__/PlotlySchemaAdapterUT.test.tsx.snap index d879c4aeb11900..65525408889f52 100644 --- a/packages/charts/react-charts/library/src/components/DeclarativeChart/__snapshots__/PlotlySchemaAdapterUT.test.tsx.snap +++ b/packages/charts/react-charts/library/src/components/DeclarativeChart/__snapshots__/PlotlySchemaAdapterUT.test.tsx.snap @@ -372,12 +372,12 @@ Object { "minValue": undefined, "segments": Array [ Object { - "color": "#c19c00", + "color": "#57811b", "legend": "Segment 1", "size": 250, }, Object { - "color": "#c8d1fa", + "color": "#b146c2", "legend": "Segment 2", "size": 150, }, @@ -2841,140 +2841,140 @@ Object { "chartTitle": "PHP Framework Popularity at Work - SitePoint, 2015", "data": Array [ Object { - "color": "#4f6bed", - "legend": "Aura", + "color": "#cf87da", + "legend": "Votes", "x": 10, "y": "Aura", }, Object { - "color": "#d0b232", - "legend": "Drupal", + "color": "#cf87da", + "legend": "Votes", "x": 11, "y": "Drupal", }, Object { - "color": "#c36bd1", - "legend": "TYPO3 Flow", + "color": "#cf87da", + "legend": "Votes", "x": 17, "y": "TYPO3 Flow", }, Object { - "color": "#73aa24", - "legend": "FuelPHP", + "color": "#cf87da", + "legend": "Votes", "x": 25, "y": "FuelPHP", }, Object { - "color": "#d77440", - "legend": "Kohana", + "color": "#cf87da", + "legend": "Votes", "x": 35, "y": "Kohana", }, Object { - "color": "#4fa1e1", - "legend": "Typo 3", + "color": "#cf87da", + "legend": "Votes", "x": 35, "y": "Typo 3", }, Object { - "color": "#27ac22", - "legend": "Simple MVC Framework", + "color": "#cf87da", + "legend": "Votes", "x": 42, "y": "Simple MVC Framework", }, Object { - "color": "#a083c9", - "legend": "Silex", + "color": "#cf87da", + "legend": "Votes", "x": 65, "y": "Silex", }, Object { - "color": "#4cb4b7", - "legend": "Slim", + "color": "#cf87da", + "legend": "Votes", "x": 79, "y": "Slim", }, Object { - "color": "#ee5fb7", - "legend": "Phalcon", + "color": "#cf87da", + "legend": "Votes", "x": 169, "y": "Phalcon", }, Object { - "color": "#93a4f4", - "legend": "We use a CMS for everything", + "color": "#cf87da", + "legend": "Votes", "x": 203, "y": "We use a CMS for everything", }, Object { - "color": "#ae8c00", - "legend": "No Framework", + "color": "#cf87da", + "legend": "Votes", "x": 243, "y": "No Framework", }, Object { - "color": "#b146c2", - "legend": "CakePHP", + "color": "#cf87da", + "legend": "Votes", "x": 255, "y": "CakePHP", }, Object { - "color": "#57811b", - "legend": "Zend Framework 1", + "color": "#cf87da", + "legend": "Votes", "x": 274, "y": "Zend Framework 1", }, Object { - "color": "#ca5010", - "legend": "Company Internal Framework", + "color": "#cf87da", + "legend": "Votes", "x": 378, "y": "Company Internal Framework", }, Object { - "color": "#3a96dd", - "legend": "Zend Framework 2", + "color": "#cf87da", + "legend": "Votes", "x": 390, "y": "Zend Framework 2", }, Object { - "color": "#13a10e", - "legend": "Yii 1", + "color": "#cf87da", + "legend": "Votes", "x": 407, "y": "Yii 1", }, Object { - "color": "#9373c0", - "legend": "PHPixie", + "color": "#cf87da", + "legend": "Votes", "x": 418, "y": "PHPixie", }, Object { - "color": "#2aa0a4", - "legend": "Yii 2", + "color": "#cf87da", + "legend": "Votes", "x": 504, "y": "Yii 2", }, Object { - "color": "#e3008c", - "legend": "CodeIgniter", + "color": "#cf87da", + "legend": "Votes", "x": 597, "y": "CodeIgniter", }, Object { - "color": "#637cef", - "legend": "Nette", + "color": "#cf87da", + "legend": "Votes", "x": 671, "y": "Nette", }, Object { - "color": "#dac157", - "legend": "Symfony2", + "color": "#cf87da", + "legend": "Votes", "x": 1067, "y": "Symfony2", }, Object { "color": "#cf87da", - "legend": "Laravel", + "legend": "Votes", "x": 1659, "y": "Laravel", }, @@ -3072,42 +3072,42 @@ Object { ], "nodes": Array [ Object { - "color": "#ea38a6", + "color": "#dac157", "name": "Remain+No – 28", "nodeId": 0, }, Object { - "color": "#038387", + "color": "#637cef", "name": "Leave+No – 16", "nodeId": 1, }, Object { - "color": "#8764b8", + "color": "#e3008c", "name": "Remain+Yes – 21", "nodeId": 2, }, Object { - "color": "#11910d", + "color": "#2aa0a4", "name": "Leave+Yes – 14", "nodeId": 3, }, Object { - "color": "#3487c7", + "color": "#9373c0", "name": "Didn’t vote in at least one referendum – 21", "nodeId": 4, }, Object { - "color": "#d06228", + "color": "#13a10e", "name": "46 – No", "nodeId": 5, }, Object { - "color": "#689920", + "color": "#3a96dd", "name": "39 – Yes", "nodeId": 6, }, Object { - "color": "#ba58c9", + "color": "#ca5010", "name": "14 – Don’t know / would not vote", "nodeId": 7, }, diff --git a/packages/charts/react-charts/library/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.test.tsx b/packages/charts/react-charts/library/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.test.tsx index 054147e8dde566..779142dc9365f1 100644 --- a/packages/charts/react-charts/library/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.test.tsx +++ b/packages/charts/react-charts/library/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.test.tsx @@ -66,6 +66,83 @@ const chartPointsHBCWA: HorizontalBarChartWithAxisDataPoint[] = [ }, ]; +const stackedChartPointsHBCWA: HorizontalBarChartWithAxisDataPoint[] = [ + { + x: 10000, + y: 'Q1', + legend: 'Product A', + color: '#0078d4', + xAxisCalloutData: '10K', + yAxisCalloutData: 'Q1', + }, + { + x: -5000, + y: 'Q1', + legend: 'Product B', + color: '#ff8c00', + xAxisCalloutData: '-5K', + yAxisCalloutData: 'Q1', + }, + { + x: 8000, + y: 'Q1', + legend: 'Product C', + color: '#107c10', + xAxisCalloutData: '8K', + yAxisCalloutData: 'Q1', + }, + + { + x: -7000, + y: 'Q2', + legend: 'Product A', + color: '#0078d4', + xAxisCalloutData: '-7K', + yAxisCalloutData: 'Q2', + }, + { + x: 12000, + y: 'Q2', + legend: 'Product B', + color: '#ff8c00', + xAxisCalloutData: '12K', + yAxisCalloutData: 'Q2', + }, + { + x: 3000, + y: 'Q2', + legend: 'Product C', + color: '#107c10', + xAxisCalloutData: '3K', + yAxisCalloutData: 'Q2', + }, + + { + x: 15000, + y: 'Q3', + legend: 'Product A', + color: '#0078d4', + xAxisCalloutData: '15K', + yAxisCalloutData: 'Q3', + }, + { + x: -4000, + y: 'Q3', + legend: 'Product B', + color: '#ff8c00', + xAxisCalloutData: '-4K', + yAxisCalloutData: 'Q3', + }, + { + x: 5000, + y: 'Q3', + legend: 'Product C', + color: '#107c10', + xAxisCalloutData: '5K', + yAxisCalloutData: 'Q3', + }, +]; + const chartPointsWithStringYAxisHBCWA: HorizontalBarChartWithAxisDataPoint[] = [ { y: 'String One', @@ -185,10 +262,10 @@ describe('Horizontal bar chart with axis - Subcomponent bar', () => { // Assert const bars = screen.getAllByText((content, element) => element!.tagName.toLowerCase() === 'rect'); expect(bars).toHaveLength(4); - expect(bars[0].getAttribute('fill')).toEqual('#00bcf2'); - expect(bars[1].getAttribute('fill')).toEqual('#00188f'); - expect(bars[2].getAttribute('fill')).toEqual('#002050'); - expect(bars[3].getAttribute('fill')).toEqual('#0078d4'); + expect(bars[0].getAttribute('fill')).toEqual('#0078d4'); + expect(bars[1].getAttribute('fill')).toEqual('#002050'); + expect(bars[2].getAttribute('fill')).toEqual('#00188f'); + expect(bars[3].getAttribute('fill')).toEqual('#00bcf2'); }, ); @@ -321,10 +398,10 @@ describe('Horizontal bar chart with axis - Subcomponent Labels', () => { const bars = screen.getAllByText((content, element) => element!.tagName.toLowerCase() === 'rect'); // Assert expect(bars).toHaveLength(4); - expect(bars[0]).toHaveAttribute('opacity', '0.1'); + expect(bars[0]).toHaveAttribute('opacity', '1'); expect(bars[1]).toHaveAttribute('opacity', '0.1'); expect(bars[2]).toHaveAttribute('opacity', '0.1'); - expect(bars[3]).toHaveAttribute('opacity', '1'); + expect(bars[3]).toHaveAttribute('opacity', '0.1'); }, ); @@ -362,10 +439,10 @@ describe('Horizontal bar chart with axis - Subcomponent Labels', () => { expect(getByClass(container, /calloutDateTimeContainer/i)).toBeDefined(); const xAxisCallOutData = getByClass(container, /calloutContentX/i); expect(xAxisCallOutData).toBeDefined(); - expect(xAxisCallOutData[0].textContent).toEqual('5000 '); + expect(xAxisCallOutData[0].textContent).toEqual('1000 '); const yAxisCallOutData = getByClass(container, /calloutContentY/i); expect(yAxisCallOutData).toBeDefined(); - expect(yAxisCallOutData[0].textContent).toEqual('2000'); + expect(yAxisCallOutData[0].textContent).toEqual('1000'); }, ); @@ -397,6 +474,11 @@ describe('HorizontalBarChartWithAxis snapShot testing', () => { expect(component).toMatchSnapshot(); }); + it('renders Stacked HorizontalBarChartWithAxis correctly', () => { + let component = render(); + expect(component).toMatchSnapshot(); + }); + it('renders hideLegend correctly', () => { let component = render(); expect(component).toMatchSnapshot(); diff --git a/packages/charts/react-charts/library/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.tsx b/packages/charts/react-charts/library/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.tsx index c394bbf3f6359b..be8610c21d7317 100644 --- a/packages/charts/react-charts/library/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.tsx +++ b/packages/charts/react-charts/library/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.tsx @@ -32,6 +32,10 @@ import { useRtl, DataVizPalette, getColorFromToken, + computeLongestBars, + IDomainNRange, + domainRangeOfNumericForHorizontalBarChartWithAxis, + groupChartDataByYValue, } from '../../utilities/index'; type ColorScale = (_p?: number) => string; @@ -62,7 +66,10 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent(null); + const X_ORIGIN: number = 0; const [color, setColor] = React.useState(''); const [dataForHoverCard, setDataForHoverCard] = React.useState(0); @@ -167,10 +174,40 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent + _yAxisType === YAxisType.NumericAxis + ? _createNumericBars( + containerHeight, + containerWidth, + xElement!, + yElement!, + singleBarData, + xBarScale, + yBarScale, + ) + : _createStringBars( + containerHeight, + containerWidth, + xElement!, + yElement!, + singleBarData, + xBarScale, + yBarScale, + ), + ) + .flat(); + + return (_bars = allBars); } function _createColors(): D3ScaleLinear | ColorScale { @@ -251,11 +288,13 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent point.x as number)!; const yMax = d3Max(_points, (point: HorizontalBarChartWithAxisDataPoint) => point.y as number)!; const xBarScale = d3ScaleLinear() - .domain(_isRtl ? [xMax, 0] : [0, xMax]) + .domain(xDomain) .nice() .range([_margins.left!, containerWidth - _margins.right!]); const yBarScale = d3ScaleLinear() @@ -263,7 +302,6 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent point.x as number)!; // please note these padding default values must be consistent in here // and CatrtesianChartBase w for more details refer example // http://using-d3js.com/04_07_ordinal_scales.html @@ -273,7 +311,7 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent { const aValue = typeof a.y === 'number' ? a.y : parseFloat(a.y); const bValue = typeof b.y === 'number' ? b.y : parseFloat(b.y); return bValue - aValue; }); + let prevWidthPositive = 0; + let prevWidthNegative = 0; + let prevPoint = 0; + + const totalPositiveBars = singleBarData.filter( + (point: HorizontalBarChartWithAxisDataPoint) => point.x >= X_ORIGIN, + ).length; + const totalNegativeBars = singleBarData.length - totalPositiveBars; + let currPositiveCounter = 0; + let currNegativeCounter = 0; + const bars = sortedBars.map((point: HorizontalBarChartWithAxisDataPoint, index: number) => { let shouldHighlight = true; if (isLegendHovered || isLegendSelected) { shouldHighlight = _isLegendHighlighted(point.legend); } + if (point.x >= X_ORIGIN) { + ++currPositiveCounter; + } + if (point.x < X_ORIGIN) { + ++currNegativeCounter; + } + const barStartX = _isRtl + ? containerWidth - + (_margins.right! + Math.max(xBarScale(point.x + X_ORIGIN), xBarScale(X_ORIGIN)) - _margins.left!) + : Math.min(xBarScale(point.x + X_ORIGIN), xBarScale(X_ORIGIN)); const barHeight: number = Math.max(yBarScale(point.y), 0); if (barHeight < 1) { return ; @@ -315,18 +378,37 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent X_ORIGIN ? (prevWidthPositive += prevBarWidth) : (prevWidthNegative += prevBarWidth); + const currentWidth = Math.abs(xBarScale(point.x + X_ORIGIN) - xBarScale(X_ORIGIN)); + const gapWidthLTR = + currentWidth > 2 && + ((point.x > X_ORIGIN && currPositiveCounter !== totalPositiveBars) || + (point.x < X_ORIGIN && (totalPositiveBars !== 0 || currNegativeCounter > 1))) + ? 2 + : 0; + const gapWidthRTL = + currentWidth > 2 && + ((point.x > X_ORIGIN && (totalNegativeBars !== 0 || currPositiveCounter > 1)) || + (point.x < X_ORIGIN && currNegativeCounter !== totalNegativeBars)) + ? 2 + : 0; + let xStart = X_ORIGIN; + if (_isRtl) { + xStart = point.x > X_ORIGIN ? barStartX - prevWidthPositive : barStartX + prevWidthNegative; + } else { + xStart = point.x > X_ORIGIN ? barStartX + prevWidthPositive : barStartX - prevWidthNegative; + } + prevPoint = point.x; + return ( { _refCallback(e, point.legend!); @@ -389,14 +471,37 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent { + let prevWidthPositive = 0; + let prevWidthNegative = 0; + let prevPoint = 0; + const totalPositiveBars = singleBarData.filter( + (point: HorizontalBarChartWithAxisDataPoint) => point.x >= X_ORIGIN, + ).length; + const totalNegativeBars = singleBarData.length - totalPositiveBars; + let currPositiveCounter = 0; + let currNegativeCounter = 0; + const bars = singleBarData.map((point: HorizontalBarChartWithAxisDataPoint, index: number) => { let shouldHighlight = true; if (isLegendHovered || isLegendSelected) { shouldHighlight = _isLegendHighlighted(point.legend); } + if (point.x >= X_ORIGIN) { + ++currPositiveCounter; + } + if (point.x < X_ORIGIN) { + ++currNegativeCounter; + } + const barStartX = _isRtl + ? containerWidth - + (_margins.right! + Math.max(xBarScale(point.x + X_ORIGIN), xBarScale(X_ORIGIN)) - _margins.left!) + : Math.min(xBarScale(point.x + X_ORIGIN), xBarScale(X_ORIGIN)); const barHeight: number = Math.max(yBarScale(point.y), 0); if (barHeight < 1) { return ; @@ -411,20 +516,37 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent 0 ? (prevWidthPositive += prevBarWidth) : (prevWidthNegative += prevBarWidth); + const currentWidth = Math.abs(xBarScale(point.x + X_ORIGIN) - xBarScale(X_ORIGIN)); + const gapWidthLTR = + currentWidth > 2 && + ((point.x > X_ORIGIN && currPositiveCounter !== totalPositiveBars) || + (point.x < X_ORIGIN && (totalPositiveBars !== 0 || currNegativeCounter > 1))) + ? 2 + : 0; + const gapWidthRTL = + currentWidth > 2 && + ((point.x > X_ORIGIN && (totalNegativeBars !== 0 || currPositiveCounter > 1)) || + (point.x < X_ORIGIN && currNegativeCounter !== totalNegativeBars)) + ? 2 + : 0; + prevPoint = point.x; + let xStart = X_ORIGIN; + if (_isRtl) { + xStart = point.x > X_ORIGIN ? barStartX - prevWidthPositive : barStartX + prevWidthNegative; + } else { + xStart = point.x > X_ORIGIN ? barStartX + prevWidthPositive : barStartX - prevWidthNegative; + } return ( = {}; data.forEach((point: HorizontalBarChartWithAxisDataPoint, _index: number) => { // eslint-disable-next-line @typescript-eslint/no-shadow const color: string = useSingleColor ? (props.colors ? _createColors()(1) : getNextColor(1, 0)) : point.color!; + mapLegendToColor[point.legend!] = color; + }); + Object.entries(mapLegendToColor).forEach(([legendTitle, color]) => { // mapping data to the format Legends component needs const legend: Legend = { - title: point.legend!, + title: legendTitle, color, hoverAction: () => { _handleChartMouseLeave(); - _onLegendHover(point.legend!); + _onLegendHover(legendTitle); }, // eslint-disable-next-line @typescript-eslint/no-shadow onMouseOutAction: (isLegendSelected?: boolean) => { @@ -598,6 +724,33 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent @@ -995,57 +995,6 @@ exports[`Horizontal bar chart with axis rendering Should render the Horizontal b class="fui-Overflow fui-legend__resizableArea" style="text-align: unset;" > - - - @@ -1078,7 +1029,7 @@ Object { >
@@ -1477,60 +1428,60 @@ Object { @@ -2027,60 +1978,60 @@ Object { @@ -2242,7 +2193,7 @@ Object { >
@@ -2641,60 +2592,60 @@ Object { @@ -3191,60 +3142,60 @@ Object { @@ -3800,60 +3751,60 @@ Object { @@ -4195,205 +4146,1451 @@ Object { + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + +
+
+
+
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`HorizontalBarChartWithAxis snapShot testing renders Stacked HorizontalBarChartWithAxis correctly 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+ +
+ , + "container":
+ @@ -4959,60 +6213,60 @@ Object { @@ -5416,60 +6670,60 @@ Object { @@ -5903,7 +7157,7 @@ Object { - - -
@@ -6053,7 +7258,7 @@ Object {
, @@ -6424,7 +7629,7 @@ Object { - - -
@@ -6634,7 +7790,7 @@ Object { >
@@ -7004,7 +8160,7 @@ Object { - - -
@@ -7520,7 +8627,7 @@ Object { - - - diff --git a/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts b/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts index 532cb206e1e572..649620b4fb08f5 100644 --- a/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts +++ b/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts @@ -17,6 +17,7 @@ const { Timezone } = require('../../scripts/constants'); const env = require('../../config/tests'); // Reference to the test plan: packages\react-charting\docs\TestPlans\Utilities\UnitTests.md +const X_ORIGIN = 0; describe('Unit test to format data to localized string', () => { test('Should return undefined when data provided is undefined', () => { @@ -965,12 +966,37 @@ describe('domainRangeOfNumericForHorizontalBarChartWithAxis', () => { }; it('should return domain and range values correctly for numeric x-axis', () => { - const result = utils.domainRangeOfNumericForHorizontalBarChartWithAxis(points, margins, 100, false, 1); + const result = utils.domainRangeOfNumericForHorizontalBarChartWithAxis(points, margins, 100, false, 1, X_ORIGIN); matchResult(result); }); it('should return domain and range values correctly for numeric x-axis when layout direction is RTL', () => { - const result = utils.domainRangeOfNumericForHorizontalBarChartWithAxis(points, margins, 100, true, 1); + const result = utils.domainRangeOfNumericForHorizontalBarChartWithAxis(points, margins, 100, true, 1, X_ORIGIN); + matchResult(result); + }); +}); + +describe('domainRangeOfNumericForHorizontalBarChartWithAxisStacked', () => { + const points: HorizontalBarChartWithAxisDataPoint[] = [ + { x: 10, y: 20 }, + { x: 20, y: 40 }, + { x: 30, y: 40 }, + { x: 50, y: 20 }, + ]; + const margins: utils.IMargins = { + left: 5, + right: 10, + top: 0, + bottom: 0, + }; + + it('should return domain and range values correctly for numeric x-axis', () => { + const result = utils.domainRangeOfNumericForHorizontalBarChartWithAxis(points, margins, 100, false, 1, X_ORIGIN); + matchResult(result); + }); + + it('should return domain and range values correctly for numeric x-axis when layout direction is RTL', () => { + const result = utils.domainRangeOfNumericForHorizontalBarChartWithAxis(points, margins, 100, true, 1, X_ORIGIN); matchResult(result); }); }); @@ -1125,6 +1151,7 @@ describe('getDomainNRangeValues', () => { 16, undefined, 1, + X_ORIGIN, ); matchResult(result); }); diff --git a/packages/charts/react-charts/library/src/utilities/__snapshots__/UtilityUnitTests.test.ts.snap b/packages/charts/react-charts/library/src/utilities/__snapshots__/UtilityUnitTests.test.ts.snap index c9782dd860e8e7..708957ad84af17 100644 --- a/packages/charts/react-charts/library/src/utilities/__snapshots__/UtilityUnitTests.test.ts.snap +++ b/packages/charts/react-charts/library/src/utilities/__snapshots__/UtilityUnitTests.test.ts.snap @@ -4029,6 +4029,24 @@ Object { } `; +exports[`domainRangeOfNumericForHorizontalBarChartWithAxisStacked should return domain and range values correctly for numeric x-axis 1`] = ` +Object { + "dEndValue": 60, + "dStartValue": 0, + "rEndValue": 90, + "rStartValue": 6, +} +`; + +exports[`domainRangeOfNumericForHorizontalBarChartWithAxisStacked should return domain and range values correctly for numeric x-axis when layout direction is RTL 1`] = ` +Object { + "dEndValue": 0, + "dStartValue": 60, + "rEndValue": 89, + "rStartValue": 5, +} +`; + exports[`domainRangeOfVSBCNumeric should return domain and range values correctly for numeric x-axis 1`] = ` Object { "dEndValue": 30, diff --git a/packages/charts/react-charts/library/src/utilities/utilities.ts b/packages/charts/react-charts/library/src/utilities/utilities.ts index 888388f4ce629a..3eabf97e65178c 100644 --- a/packages/charts/react-charts/library/src/utilities/utilities.ts +++ b/packages/charts/react-charts/library/src/utilities/utilities.ts @@ -1245,6 +1245,59 @@ export function domainRangeOfNumericForScatterChart( : { dStartValue: xMin, dEndValue: xMax, rStartValue, rEndValue }; } +/** + * Groups HorizontalBarChart With Axis data based on YValue + * Used for stacked case + * @param {IHorizontalBarChartWithAxisDataPoint[]} chartData + * @returns {IHorizontalBarChartWithAxisDataPoint[][]} + */ +export function groupChartDataByYValue( + chartData: HorizontalBarChartWithAxisDataPoint[], +): HorizontalBarChartWithAxisDataPoint[][] { + const map: Record = {}; + chartData.forEach(dataPoint => { + const key = dataPoint.y; + if (!map[key]) { + map[key] = []; + } + map[key].push(dataPoint); + }); + + return Object.values(map); +} + +/** + * Calculates maximum domain values for Numeric x axis for both positive and negative values + * works for Horizontal Bar Chart With axis + * @param {HorizontalBarChartWithAxisDataPoint[][]} stackedChartData + * @returns {number} + */ +export function computeLongestBars( + stackedChartData: HorizontalBarChartWithAxisDataPoint[][], + X_ORIGIN: number, +): { + longestPositiveBar: number; + longestNegativeBar: number; +} { + let longestPositiveBar = 0; + let longestNegativeBar = 0; + + stackedChartData.forEach((group: HorizontalBarChartWithAxisDataPoint[]) => { + const positiveBarTotal = group.reduce( + (acc: number, point: HorizontalBarChartWithAxisDataPoint) => acc + (point.x > 0 ? point.x : 0), + X_ORIGIN, + ); + const negativeBarTotal = group.reduce( + (acc: number, point: HorizontalBarChartWithAxisDataPoint) => acc + (point.x < 0 ? point.x : 0), + X_ORIGIN, + ); + + longestPositiveBar = Math.max(longestPositiveBar, positiveBarTotal); + longestNegativeBar = Math.min(longestNegativeBar, negativeBarTotal); + }); + return { longestPositiveBar, longestNegativeBar }; +} + /** * Calculates Domain and range values for Numeric X axis. * This method calculates Horizontal Chart with Axis @@ -1261,14 +1314,17 @@ export function domainRangeOfNumericForHorizontalBarChartWithAxis( containerWidth: number, isRTL: boolean, shiftX: number, + X_ORIGIN?: number, ): IDomainNRange { - const xMax = d3Max(points, (point: HorizontalBarChartWithAxisDataPoint) => point.x as number)!; + const longestBars = computeLongestBars(groupChartDataByYValue(points), X_ORIGIN!); + const xMax = longestBars.longestPositiveBar; + const xMin = longestBars.longestNegativeBar; const rMin = isRTL ? margins.left! : margins.left! + shiftX; const rMax = isRTL ? containerWidth - margins.right! - shiftX : containerWidth - margins.right!; return isRTL - ? { dStartValue: xMax, dEndValue: 0, rStartValue: rMin, rEndValue: rMax } - : { dStartValue: 0, dEndValue: xMax, rStartValue: rMin, rEndValue: rMax }; + ? { dStartValue: xMax, dEndValue: Math.min(xMin, X_ORIGIN!), rStartValue: rMin, rEndValue: rMax } + : { dStartValue: Math.min(xMin, X_ORIGIN!), dEndValue: xMax, rStartValue: rMin, rEndValue: rMax }; } /** @@ -1474,6 +1530,7 @@ export function getDomainNRangeValues( barWidth: number, tickValues: number[] | Date[] | string[] | undefined, shiftX: number, + X_ORIGIN?: number, ): IDomainNRange { let domainNRangeValue: IDomainNRange; if (xAxisType === XAxisTypes.NumericAxis) { @@ -1489,7 +1546,14 @@ export function getDomainNRangeValues( domainNRangeValue = domainRageOfVerticalNumeric(points, margins, width, isRTL, barWidth!); break; case ChartTypes.HorizontalBarChartWithAxis: - domainNRangeValue = domainRangeOfNumericForHorizontalBarChartWithAxis(points, margins, width, isRTL, shiftX); + domainNRangeValue = domainRangeOfNumericForHorizontalBarChartWithAxis( + points, + margins, + width, + isRTL, + shiftX, + X_ORIGIN!, + ); break; case ChartTypes.ScatterChart: domainNRangeValue = domainRangeOfNumericForScatterChart(points, margins, width, isRTL);