diff --git a/change/@fluentui-react-charting-cae42a98-eff0-4072-a40f-8a0d2add5253.json b/change/@fluentui-react-charting-cae42a98-eff0-4072-a40f-8a0d2add5253.json new file mode 100644 index 00000000000000..486c9c16921833 --- /dev/null +++ b/change/@fluentui-react-charting-cae42a98-eff0-4072-a40f-8a0d2add5253.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "support negative y values in Vertical Bar chart", + "packageName": "@fluentui/react-charting", + "email": "74965306+Anush2303@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/charts/react-charting/etc/react-charting.api.md b/packages/charts/react-charting/etc/react-charting.api.md index cee4768dbd1925..df25ce137be990 100644 --- a/packages/charts/react-charting/etc/react-charting.api.md +++ b/packages/charts/react-charting/etc/react-charting.api.md @@ -293,6 +293,7 @@ export interface ICartesianChartProps { showXAxisLablesTooltip?: boolean; strokeWidth?: number; styles?: IStyleFunctionOrObject; + supportNegativeData?: boolean; svgProps?: React_2.SVGProps; theme?: ITheme; tickFormat?: string; @@ -1043,7 +1044,7 @@ export interface IModifiedCartesianChartProps extends ICartesianChartProps { createStringYAxis: (yAxisParams: IYAxisParams, dataPoints: string[], isRtl: boolean, barWidth: number | undefined) => ScaleBand; // Warning: (ae-forgotten-export) The symbol "IYAxisParams" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IAxisData" needs to be exported by the entry point index.d.ts - createYAxis: (yAxisParams: IYAxisParams, isRtl: boolean, axisData: IAxisData, isIntegralDataset: boolean, useSecondaryYScale?: boolean) => ScaleLinear; + createYAxis: (yAxisParams: IYAxisParams, isRtl: boolean, axisData: IAxisData, isIntegralDataset: boolean, useSecondaryYScale?: boolean, supportNegativeData?: boolean) => ScaleLinear; culture?: string; customizedCallout?: any; datasetForXAxisDomain?: string[]; diff --git a/packages/charts/react-charting/src/components/AreaChart/__snapshots__/AreaChartRTL.test.tsx.snap b/packages/charts/react-charting/src/components/AreaChart/__snapshots__/AreaChartRTL.test.tsx.snap index 039d25211623aa..99e50bf4ec7823 100644 --- a/packages/charts/react-charting/src/components/AreaChart/__snapshots__/AreaChartRTL.test.tsx.snap +++ b/packages/charts/react-charting/src/components/AreaChart/__snapshots__/AreaChartRTL.test.tsx.snap @@ -4186,12 +4186,12 @@ exports[`Area chart rendering Should render the Area chart with secondary Y axis font-family="sans-serif" font-size="10" id="yAxisGElementSecondarychart_6" - text-anchor="end" + text-anchor="start" transform="translate(610, 0)" > @@ -4219,13 +4219,13 @@ exports[`Area chart rendering Should render the Area chart with secondary Y axis > @@ -4237,13 +4237,13 @@ exports[`Area chart rendering Should render the Area chart with secondary Y axis > @@ -4255,13 +4255,13 @@ exports[`Area chart rendering Should render the Area chart with secondary Y axis > @@ -4273,13 +4273,13 @@ exports[`Area chart rendering Should render the Area chart with secondary Y axis > diff --git a/packages/charts/react-charting/src/components/CommonComponents/CartesianChart.base.tsx b/packages/charts/react-charting/src/components/CommonComponents/CartesianChart.base.tsx index aae7a782bb8924..abc45a725a5e82 100644 --- a/packages/charts/react-charting/src/components/CommonComponents/CartesianChart.base.tsx +++ b/packages/charts/react-charting/src/components/CommonComponents/CartesianChart.base.tsx @@ -387,9 +387,23 @@ export class CartesianChartBase extends React.Component ScaleLinear; /** diff --git a/packages/charts/react-charting/src/components/LineChart/__snapshots__/LineChartRTL.test.tsx.snap b/packages/charts/react-charting/src/components/LineChart/__snapshots__/LineChartRTL.test.tsx.snap index ab55df4f396093..2b6d91024a34ca 100644 --- a/packages/charts/react-charting/src/components/LineChart/__snapshots__/LineChartRTL.test.tsx.snap +++ b/packages/charts/react-charting/src/components/LineChart/__snapshots__/LineChartRTL.test.tsx.snap @@ -5971,12 +5971,12 @@ exports[`Line chart rendering Should render the line chart with secondary Y axis font-family="sans-serif" font-size="10" id="yAxisGElementSecondarychart_9" - text-anchor="end" + text-anchor="start" transform="translate(610, 0)" > @@ -6004,13 +6004,13 @@ exports[`Line chart rendering Should render the line chart with secondary Y axis > @@ -6022,13 +6022,13 @@ exports[`Line chart rendering Should render the line chart with secondary Y axis > @@ -6040,13 +6040,13 @@ exports[`Line chart rendering Should render the line chart with secondary Y axis > @@ -6058,13 +6058,13 @@ exports[`Line chart rendering Should render the line chart with secondary Y axis > diff --git a/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx index 759ed07062a566..8a0c7585df3416 100644 --- a/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx @@ -93,6 +93,7 @@ export class VerticalBarChartBase extends React.Component point.y)!, this.props.yMaxValue || 0, ); + this._yMin = Math.min( + d3Min(this._points, (point: IVerticalBarChartDataPoint) => point.y)!, + this.props.yMinValue || 0, + ); const legendBars: JSX.Element = this._getLegendData(this._points, this.props.theme!.palette); this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, @@ -171,6 +176,7 @@ export class VerticalBarChartBase extends React.Component { const shouldHighlight = this._legendHighlighted(point.legend!) || this._noLegendHighlighted() ? true : false; this._classNames = getClassNames(this.props.styles!, { @@ -700,18 +715,28 @@ export class VerticalBarChartBase extends React.Component ; - } else if (barHeight <= Math.ceil(yBarScale(this._yMax) / 100.0)) { - adjustedBarHeight = Math.ceil(yBarScale(this._yMax) / 100.0); - } else { - adjustedBarHeight = barHeight; + } + // Adjust bar height if it's smaller than the threshold + else if (barHeight <= minBarHeight) { + adjustedBarHeight = minBarHeight; } const xPoint = xBarScale(point.x as number) - this._barWidth / 2; - const yPoint = containerHeight - this.margins.bottom! - adjustedBarHeight; + const yPoint = + containerHeight - + this.margins.bottom! - + (isHeightNegative ? -1 * adjustedBarHeight : adjustedBarHeight) - + yBarScale(yReferencePoint); + const baselineHeight = containerHeight - this.margins.bottom! - yBarScale(yReferencePoint); let startColor = point.color && !useSingleColor ? point.color : colorScale(point.y); let endColor = startColor; @@ -742,7 +767,7 @@ export class VerticalBarChartBase extends React.Component - {this._renderBarLabel(xPoint, yPoint, point.y, point.legend!)} + {this._renderBarLabel(xPoint, yPoint, point.y, point.legend!, isHeightNegative)} ); }); @@ -791,19 +816,29 @@ export class VerticalBarChartBase extends React.Component { - const barHeight: number = Math.max(yBarScale(point.y), 0); - let adjustedBarHeight = 0; - if (barHeight <= 0) { - return ; - } else if (barHeight <= Math.ceil(yBarScale(this._yMax) / 100.0)) { - adjustedBarHeight = Math.ceil(yBarScale(this._yMax) / 100.0); - } else { - adjustedBarHeight = barHeight; + let barHeight: number = yBarScale(point.y) - yBarScale(yReferencePoint); + const isHeightNegative = barHeight < 0; + barHeight = Math.abs(barHeight); + // Calculate threshold for minimum visible bar height + const minBarHeight = this._calculateMinBarHeight(this._yMin, this._yMax, yReferencePoint, yBarScale); + let adjustedBarHeight = barHeight; + + if (barHeight === 0 || (isHeightNegative && !this.props.supportNegativeData)) { + return ; + } + // Adjust bar height if it's smaller than the threshold + else if (barHeight <= minBarHeight) { + adjustedBarHeight = minBarHeight; } const xPoint = xBarScale(point.x); - const yPoint = containerHeight - this.margins.bottom! - adjustedBarHeight; + const yPoint = + containerHeight - + this.margins.bottom! - + (isHeightNegative ? -1 * adjustedBarHeight : adjustedBarHeight) - + yBarScale(yReferencePoint); + const baselineHeight = containerHeight - this.margins.bottom! - yBarScale(yReferencePoint); // Setting the bar width here is safe because there are no dependencies earlier in the code // that rely on the width of bars in vertical bar charts with string x-axis. this._barWidth = getBarWidth(this.props.barWidth, this.props.maxBarWidth, xBarScale.bandwidth()); @@ -839,7 +874,7 @@ export class VerticalBarChartBase extends React.Component - {this._renderBarLabel(xPoint, yPoint, point.y, point.legend!)} + {this._renderBarLabel(xPoint, yPoint, point.y, point.legend!, isHeightNegative)} ); }); @@ -891,6 +926,7 @@ export class VerticalBarChartBase extends React.Component { const shouldHighlight = this._legendHighlighted(point.legend!) || this._noLegendHighlighted() ? true : false; this._classNames = getClassNames(this.props.styles!, { @@ -898,17 +934,28 @@ export class VerticalBarChartBase extends React.Component ; - } else if (barHeight <= Math.ceil(yBarScale(this._yMax) / 100.0)) { - adjustedBarHeight = Math.ceil(yBarScale(this._yMax) / 100.0); - } else { - adjustedBarHeight = barHeight; + + let barHeight: number = yBarScale(point.y) - yBarScale(yReferencePoint); + const isHeightNegative = barHeight < 0; + barHeight = Math.abs(barHeight); + // Calculate threshold for minimum visible bar height + const minBarHeight = this._calculateMinBarHeight(this._yMin, this._yMax, yReferencePoint, yBarScale); + let adjustedBarHeight = barHeight; + + if (barHeight === 0 || (isHeightNegative && !this.props.supportNegativeData)) { + return ; + } + // Adjust bar height if it's smaller than the threshold + else if (barHeight <= minBarHeight) { + adjustedBarHeight = minBarHeight; } const xPoint = xBarScale(point.x as number) - this._barWidth / 2; - const yPoint = containerHeight - this.margins.bottom! - adjustedBarHeight; + const yPoint = + containerHeight - + this.margins.bottom! - + (isHeightNegative ? -1 * adjustedBarHeight : adjustedBarHeight) - + yBarScale(yReferencePoint); + const baselineHeight = containerHeight - this.margins.bottom! - yBarScale(yReferencePoint); let startColor = point.color && !useSingleColor ? point.color : colorScale(point.y); let endColor = startColor; @@ -939,7 +986,7 @@ export class VerticalBarChartBase extends React.Component - {this._renderBarLabel(xPoint, yPoint, point.y, point.legend!)} + {this._renderBarLabel(xPoint, yPoint, point.y, point.legend!, isHeightNegative)} ); }); @@ -1081,6 +1128,7 @@ export class VerticalBarChartBase extends React.Component point.y)! <= 0 && !this._isHavingLine) - ); + return this._points.length === 0 || (this._points.every(point => point.y === 0) && !this._isHavingLine); } private _getChartTitle = (): string => { diff --git a/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChartRTL.test.tsx b/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChartRTL.test.tsx index 34fc1b5b3f4b7a..c677c11883ab44 100644 --- a/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChartRTL.test.tsx +++ b/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChartRTL.test.tsx @@ -17,7 +17,7 @@ import { } from '../../utilities/TestUtility.test'; import { IVerticalBarChartProps } from './VerticalBarChart.types'; import { IVerticalBarChartDataPoint } from '../../index'; -import { chartPointsVBC } from '../../utilities/test-data'; +import { allNegativeChartPointsVBC, chartPointsVBC, negativeChartPointsVBC } from '../../utilities/test-data'; import { axe, toHaveNoViolations } from 'jest-axe'; const { Timezone } = require('../../../scripts/constants'); const env = require('../../../config/tests'); @@ -392,6 +392,26 @@ describe('Vertical bar chart rendering', () => { expect(container).toMatchSnapshot(); }, ); + + testWithoutWait( + 'Should render the vertical bar chart with all negative y value bars correctly', + VerticalBarChart, + { data: allNegativeChartPointsVBC, supportNegativeData: true }, + container => { + //Asset + expect(container).toMatchSnapshot(); + }, + ); + + testWithoutWait( + 'should render the vertical bar chart with some positive and some negative y value bars correctly', + VerticalBarChart, + { data: negativeChartPointsVBC, supportNegativeData: true }, + container => { + //Asset + expect(container).toMatchSnapshot(); + }, + ); }); describe('Vertical bar chart - Subcomponent bar', () => { diff --git a/packages/charts/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChartRTL.test.tsx.snap b/packages/charts/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChartRTL.test.tsx.snap index f1737984799e75..885b42fed8c1dd 100644 --- a/packages/charts/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChartRTL.test.tsx.snap +++ b/packages/charts/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChartRTL.test.tsx.snap @@ -9943,7 +9943,7 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with `; -exports[`Vertical bar chart rendering Should render the vertical bar chart with formatted Date x-axis data 1`] = ` +exports[`Vertical bar chart rendering Should render the vertical bar chart with all negative y value bars correctly 1`] = `
+`; - { - align-items: center; - background: none; - border: none; - cursor: pointer; - display: flex; - outline: transparent; - padding-bottom: 8px; - padding-left: 8px; - padding-right: 8px; - padding-top: 8px; - position: relative; - text-transform: capitalize; - } - &::-moz-focus-inner { - border: 0; - } - .ms-Fabric--isFocusVisible &:focus:after { - border: 1px solid transparent; - bottom: 1px; - content: ""; - left: 1px; - outline: 1px solid #605e5c; - position: absolute; - right: 1px; - top: 1px; - z-index: 1; - } - @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { - outline-color: #605e5c; - } - :host(.ms-Fabric--isFocusVisible) &:focus:after { - border: 1px solid transparent; - bottom: 1px; - content: ""; - left: 1px; - outline: 1px solid #605e5c; - position: absolute; - right: 1px; - top: 1px; - z-index: 1; - } - @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { - outline-color: #605e5c; - } - data-is-focusable="true" - role="option" - tabindex="-1" - > -
-
+
- Dogs -
- -
-
- +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ -
-
-
+ +
+
+ + + + + + + + +`; + +exports[`Vertical bar chart rendering Should render the vertical bar chart with numeric x-axis data 1`] = ` +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 10.0k + + + + - - -
-
- -
+ { + margin-left: 20px; + margin-top: 8px; + } + > +
+
+
+
+
+
@@ -11224,10 +12802,10 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with role="none" >
@@ -11324,10 +12902,10 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with role="none" >
@@ -11425,7 +13003,7 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with
`; -exports[`Vertical bar chart rendering Should render the vertical bar chart with numeric x-axis data 1`] = ` +exports[`Vertical bar chart rendering Should render the vertical bar chart with secondary Y axis 1`] = `
+ + + + + + + + + + + + + + + + + + + + + + + + + 50.0k @@ -11829,7 +13537,7 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with rx="0" tabindex="-1" width="16" - x="584.5384615384615" + x="565.3076923076924" y="114.80000000000001" /> 30.0k @@ -12214,7 +13922,7 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with
`; -exports[`Vertical bar chart rendering Should render the vertical bar chart with secondary Y axis 1`] = ` +exports[`Vertical bar chart rendering Should render the vertical bar chart with string x-axis data 1`] = `
- - - - - - - - - 10,000 + This is a medium long label. - 15,000 + This is a long label This is a long label - 20,000 + This label is as long as the previous one - 25,000 + A short label @@ -12447,7 +14119,7 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with >
+ />
+ />
+ /> + +
+
+
@@ -13133,7 +14790,7 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with
`; -exports[`Vertical bar chart rendering Should render the vertical bar chart with string x-axis data 1`] = ` +exports[`Vertical bar chart rendering should render the vertical bar chart with some positive and some negative y value bars correctly 1`] = `
- This is a medium long label. + 0 - This is a long label This is a long label + 5,000 - This label is as long as the previous one + 10,000 - A short label + 15,000 + + + + + + + + + @@ -13326,7 +15019,25 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with + + + + - 0 + −40k - 880 + −20k - 1.8k + 0 - 2.6k + 20k - 3.5k + 40k - + - + - - - - - + @@ -13583,7 +15271,7 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with &:focus { outline: none; } - data-focuszone-id="FocusZone6" + data-focuszone-id="FocusZone12" data-tabster="{\\"uncontrolled\\": {}}" role="listbox" > @@ -13608,10 +15296,10 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with role="none" > @@ -13693,7 +15381,9 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with line-height: 16px; opacity: ; } - /> + > + First +
-
-
-
diff --git a/packages/charts/react-charting/src/utilities/UtilityUnitTests.test.ts b/packages/charts/react-charting/src/utilities/UtilityUnitTests.test.ts index 01de54c5330fde..4f2a6990c36d79 100644 --- a/packages/charts/react-charting/src/utilities/UtilityUnitTests.test.ts +++ b/packages/charts/react-charting/src/utilities/UtilityUnitTests.test.ts @@ -313,6 +313,26 @@ describe('prepareDatapoints', () => { const result = utils.prepareDatapoints(2, 0, 3, true); matchResult(result); }); + + it('should handle the case where the range spans across zero', () => { + const result = utils.prepareDatapoints(0, -7, 3, false); + matchResult(result); + }); + + it('should return data points when minVal is negative and maxVal is non-negative', () => { + const result = utils.prepareDatapoints(3, -5, 2, false); + matchResult(result); + }); + + it('should return data points for positive only range', () => { + const result = utils.prepareDatapoints(5, 1, 1, false); + matchResult(result); + }); + + it('should return data points for a negative only range', () => { + const result = utils.prepareDatapoints(-1, -5, 1, false); + matchResult(result); + }); }); const createYAxisParams = (yAxisParams?: Partial): utils.IYAxisParams => { diff --git a/packages/charts/react-charting/src/utilities/__snapshots__/UtilityUnitTests.test.ts.snap b/packages/charts/react-charting/src/utilities/__snapshots__/UtilityUnitTests.test.ts.snap index 0bf7279dae1703..6fa3d785f4bac6 100644 --- a/packages/charts/react-charting/src/utilities/__snapshots__/UtilityUnitTests.test.ts.snap +++ b/packages/charts/react-charting/src/utilities/__snapshots__/UtilityUnitTests.test.ts.snap @@ -3784,6 +3784,15 @@ Array [ ] `; +exports[`prepareDatapoints should handle the case where the range spans across zero 1`] = ` +Array [ + -9, + -6, + -3, + 0, +] +`; + exports[`prepareDatapoints should return an array of uniformly distributed data points 1`] = ` Array [ 0, @@ -3793,6 +3802,29 @@ Array [ ] `; +exports[`prepareDatapoints should return data points for a negative only range 1`] = ` +Array [ + -5, + -1, +] +`; + +exports[`prepareDatapoints should return data points for positive only range 1`] = ` +Array [ + 1, + 5, +] +`; + +exports[`prepareDatapoints should return data points when minVal is negative and maxVal is non-negative 1`] = ` +Array [ + -8, + -4, + 0, + 4, +] +`; + exports[`prepareDatapoints should return fractional data points when interval is less than 1 1`] = ` Array [ 0, diff --git a/packages/charts/react-charting/src/utilities/test-data.ts b/packages/charts/react-charting/src/utilities/test-data.ts index 487717f6790560..e6f5dcea99bc75 100644 --- a/packages/charts/react-charting/src/utilities/test-data.ts +++ b/packages/charts/react-charting/src/utilities/test-data.ts @@ -230,3 +230,57 @@ export const chartPointsWithAxisToolTipHBCWA: IHorizontalBarChartWithAxisDataPoi color: DefaultPalette.blue, }, ]; + +export const allNegativeChartPointsVBC = [ + { + x: 0, + y: -10000, + legend: 'First', + color: DefaultPalette.accent, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '10%', + }, + { + x: 10000, + y: -50000, + legend: 'Second', + color: DefaultPalette.blueDark, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '20%', + }, + { + x: 25000, + y: -30000, + legend: 'Third', + color: DefaultPalette.blueMid, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '37%', + }, +]; + +export const negativeChartPointsVBC = [ + { + x: 0, + y: 10000, + legend: 'First', + color: DefaultPalette.accent, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '10%', + }, + { + x: 10000, + y: -50000, + legend: 'Second', + color: DefaultPalette.blueDark, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '20%', + }, + { + x: 25000, + y: 30000, + legend: 'Third', + color: DefaultPalette.blueMid, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '37%', + }, +]; diff --git a/packages/charts/react-charting/src/utilities/utilities.ts b/packages/charts/react-charting/src/utilities/utilities.ts index 3b1980bb409f48..76105824f4e9c8 100644 --- a/packages/charts/react-charting/src/utilities/utilities.ts +++ b/packages/charts/react-charting/src/utilities/utilities.ts @@ -347,7 +347,27 @@ export function prepareDatapoints( : (maxVal - minVal) / splitInto >= 1 ? Math.ceil((maxVal - minVal) / splitInto) : (maxVal - minVal) / splitInto; - const dataPointsArray: number[] = [minVal, minVal + val]; + /* + For cases where we have negative and positive values + The dataPointsArray is filled from 0 to minVal by val difference + Then the array is reversed and values from 0(excluding 0) to maxVal are appended + This ensures presence of 0 to act as an anchor reference. + For simple cases where the scale may not encounter such a need for 0, + We simply fill from minVal to maxVal + */ + const dataPointsArray: number[] = [minVal < 0 && maxVal >= 0 ? 0 : minVal]; + /*For the case of all positive or all negative, we need to add another value + in array for atleast one interval, but in case of mix of positive and negative, + there will always be one more entry that will be added by the logic we have*/ + if (dataPointsArray[0] === minVal) { + dataPointsArray.push(minVal + val); + } + if (minVal < 0 && maxVal >= 0) { + while (dataPointsArray[dataPointsArray.length - 1] > minVal) { + dataPointsArray.push(dataPointsArray[dataPointsArray.length - 1] - val); + } + dataPointsArray.reverse(); + } while (dataPointsArray[dataPointsArray.length - 1] < maxVal) { dataPointsArray.push(dataPointsArray[dataPointsArray.length - 1] + val); } @@ -388,6 +408,7 @@ export function createNumericYAxis( axisData: IAxisData, isIntegralDataset: boolean, useSecondaryYScale: boolean = false, + supportNegativeData: boolean = false, ) { const { yMinMaxValues = { startValue: 0, endValue: 0 }, @@ -408,10 +429,14 @@ export function createNumericYAxis( // maxOfYVal coming from only area chart and Grouped vertical bar chart(Calculation done at base file) const tempVal = maxOfYVal || yMinMaxValues.endValue; const finalYmax = tempVal > yMaxValue ? tempVal : yMaxValue!; - const finalYmin = yMinMaxValues.startValue < yMinValue ? 0 : yMinValue!; + const finalYmin = supportNegativeData + ? Math.min(yMinMaxValues.startValue, yMinValue || 0) + : yMinMaxValues.startValue < yMinValue + ? 0 + : yMinValue!; const domainValues = prepareDatapoints(finalYmax, finalYmin, yAxisTickCount, isIntegralDataset); const yAxisScale = d3ScaleLinear() - .domain([finalYmin, domainValues[domainValues.length - 1]]) + .domain([supportNegativeData ? domainValues[0] : finalYmin, domainValues[domainValues.length - 1]]) .range([containerHeight - margins.bottom!, margins.top! + (eventAnnotationProps! ? eventLabelHeight! : 0)]); const axis = (!isRtl && useSecondaryYScale) || (isRtl && !useSecondaryYScale) ? d3AxisRight(yAxisScale) : d3AxisLeft(yAxisScale); diff --git a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.AllNegative.Example.tsx b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.AllNegative.Example.tsx new file mode 100644 index 00000000000000..6eedd754fe7612 --- /dev/null +++ b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.AllNegative.Example.tsx @@ -0,0 +1,295 @@ +import * as React from 'react'; +import { + VerticalBarChart, + IVerticalBarChartProps, + IVerticalBarChartDataPoint, + ILineChartLineOptions, + DataVizPalette, + getColorFromToken, +} from '@fluentui/react-charting'; +import { DefaultPalette } from '@fluentui/react/lib/Styling'; +import { IRenderFunction } from '@fluentui/react/lib/Utilities'; +import { ChoiceGroup, IChoiceGroupOption } from '@fluentui/react/lib/ChoiceGroup'; +import { Checkbox } from '@fluentui/react/lib/Checkbox'; +import { Toggle } from '@fluentui/react/lib/Toggle'; +import { Label } from '@fluentui/react'; + +interface IVerticalChartState { + width: number; + height: number; + isCalloutselected: boolean; + useSingleColor: boolean; + hideLabels: boolean; + showAxisTitles: boolean; + enableGradient: boolean; + roundCorners: boolean; +} + +const options: IChoiceGroupOption[] = [ + { key: 'basicExample', text: 'Basic Example' }, + { key: 'calloutExample', text: 'Custom Callout Example' }, +]; + +export class VerticalBarChartAllNegativeExample extends React.Component { + constructor(props: IVerticalBarChartProps) { + super(props); + this.state = { + width: 650, + height: 350, + isCalloutselected: false, + useSingleColor: false, + hideLabels: false, + showAxisTitles: true, + enableGradient: false, + roundCorners: false, + }; + } + + public render(): JSX.Element { + return
{this._basicExample()}
; + } + + private _onWidthChange = (e: React.ChangeEvent) => { + this.setState({ width: parseInt(e.target.value, 10) }); + }; + private _onHeightChange = (e: React.ChangeEvent) => { + this.setState({ height: parseInt(e.target.value, 10) }); + }; + + private _onChange = (ev: React.FormEvent, option: IChoiceGroupOption): void => { + if (this.state.isCalloutselected) { + this.setState({ isCalloutselected: false }); + } else { + this.setState({ isCalloutselected: true }); + } + }; + private _onCheckChange = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ useSingleColor: checked }); + }; + private _onHideLabelsCheckChange = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ hideLabels: checked }); + }; + private _onToggleAxisTitlesCheckChange = (ev: React.MouseEvent, checked: boolean) => { + this.forceUpdate(); + this.setState({ showAxisTitles: checked }); + }; + + private _onToggleGradient = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ enableGradient: checked }); + }; + + private _onToggleRoundCorners = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ roundCorners: checked }); + }; + + private _basicExample(): JSX.Element { + const negativePoints: IVerticalBarChartDataPoint[] = [ + { + x: 0, + y: -10000, + legend: 'Oranges', + color: DefaultPalette.accent, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-4%', + lineData: { + y: -7000, + yAxisCalloutData: '-3%', + }, + }, + { + x: 10000, + y: -50000, + legend: 'Dogs', + color: DefaultPalette.blueDark, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-21%', + lineData: { + y: -30000, + yAxisCalloutData: '-12%', + }, + }, + { + x: 25000, + y: -30000, + legend: 'Apples', + color: DefaultPalette.blueMid, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-12%', + lineData: { + y: -3000, + yAxisCalloutData: '-1%', + }, + }, + + { + x: 40000, + y: -13000, + legend: 'Bananas', + color: getColorFromToken(DataVizPalette.color6), + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-5%', + }, + { + x: 52000, + y: -43000, + legend: 'Giraffes', + color: getColorFromToken(DataVizPalette.color11), + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-18%', + lineData: { + y: -30000, + yAxisCalloutData: '-12%', + }, + }, + { + x: 68000, + y: -30000, + legend: 'Cats', + color: DefaultPalette.blueDark, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-12%', + lineData: { + y: -5000, + yAxisCalloutData: '-2%', + }, + }, + { + x: 80000, + y: -20000, + legend: 'Elephants', + color: getColorFromToken(DataVizPalette.color11), + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-8%', + lineData: { + y: -16000, + yAxisCalloutData: '-7%', + }, + }, + { + x: 92000, + y: -45000, + legend: 'Monkeys', + color: getColorFromToken(DataVizPalette.color6), + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-19%', + lineData: { + y: -40000, + yAxisCalloutData: '-16%', + }, + }, + ]; + + const lineOptions: ILineChartLineOptions = { lineBorderWidth: '2' }; + + const rootStyle = { width: `${this.state.width}px`, height: `${this.state.height}px` }; + + return ( + <> + + + + + + + + + +
+ +    + +
+ {this.state.showAxisTitles && ( +
+ , + ) => (props ? defaultRender(props) : null), + })} + hideLabels={this.state.hideLabels} + supportNegativeData={true} + enableReflow={true} + yAxisTitle={this.state.showAxisTitles ? 'Different categories of animals and fruits' : undefined} + xAxisTitle={this.state.showAxisTitles ? 'Values of each category' : undefined} + enableGradient={this.state.enableGradient} + roundCorners={this.state.roundCorners} + /> +
+ )} + {!this.state.showAxisTitles && ( +
+ , + ) => (props ? defaultRender(props) : null), + })} + hideLabels={this.state.hideLabels} + enableReflow={true} + enableGradient={this.state.enableGradient} + roundCorners={this.state.roundCorners} + supportNegativeData={true} + /> +
+ )} + + ); + } +} diff --git a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Negative.Example.tsx b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Negative.Example.tsx new file mode 100644 index 00000000000000..a1f7ad3f79b865 --- /dev/null +++ b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Negative.Example.tsx @@ -0,0 +1,295 @@ +import * as React from 'react'; +import { + VerticalBarChart, + IVerticalBarChartProps, + IVerticalBarChartDataPoint, + ILineChartLineOptions, + DataVizPalette, + getColorFromToken, +} from '@fluentui/react-charting'; +import { DefaultPalette } from '@fluentui/react/lib/Styling'; +import { IRenderFunction } from '@fluentui/react/lib/Utilities'; +import { ChoiceGroup, IChoiceGroupOption } from '@fluentui/react/lib/ChoiceGroup'; +import { Checkbox } from '@fluentui/react/lib/Checkbox'; +import { Toggle } from '@fluentui/react/lib/Toggle'; +import { Label } from '@fluentui/react'; + +interface IVerticalChartState { + width: number; + height: number; + isCalloutselected: boolean; + useSingleColor: boolean; + hideLabels: boolean; + showAxisTitles: boolean; + enableGradient: boolean; + roundCorners: boolean; +} + +const options: IChoiceGroupOption[] = [ + { key: 'basicExample', text: 'Basic Example' }, + { key: 'calloutExample', text: 'Custom Callout Example' }, +]; + +export class VerticalBarChartNegativeExample extends React.Component { + constructor(props: IVerticalBarChartProps) { + super(props); + this.state = { + width: 650, + height: 350, + isCalloutselected: false, + useSingleColor: false, + hideLabels: false, + showAxisTitles: true, + enableGradient: false, + roundCorners: false, + }; + } + + public render(): JSX.Element { + return
{this._basicExample()}
; + } + + private _onWidthChange = (e: React.ChangeEvent) => { + this.setState({ width: parseInt(e.target.value, 10) }); + }; + private _onHeightChange = (e: React.ChangeEvent) => { + this.setState({ height: parseInt(e.target.value, 10) }); + }; + + private _onChange = (ev: React.FormEvent, option: IChoiceGroupOption): void => { + if (this.state.isCalloutselected) { + this.setState({ isCalloutselected: false }); + } else { + this.setState({ isCalloutselected: true }); + } + }; + private _onCheckChange = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ useSingleColor: checked }); + }; + private _onHideLabelsCheckChange = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ hideLabels: checked }); + }; + private _onToggleAxisTitlesCheckChange = (ev: React.MouseEvent, checked: boolean) => { + this.forceUpdate(); + this.setState({ showAxisTitles: checked }); + }; + + private _onToggleGradient = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ enableGradient: checked }); + }; + + private _onToggleRoundCorners = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ roundCorners: checked }); + }; + + private _basicExample(): JSX.Element { + const negativePoints: IVerticalBarChartDataPoint[] = [ + { + x: 0, + y: 10000, + legend: 'Oranges', + color: DefaultPalette.accent, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '4%', + lineData: { + y: 7000, + yAxisCalloutData: '3%', + }, + }, + { + x: 10000, + y: -50000, + legend: 'Dogs', + color: DefaultPalette.blueDark, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-21%', + lineData: { + y: -30000, + yAxisCalloutData: '-12%', + }, + }, + { + x: 25000, + y: 30000, + legend: 'Apples', + color: DefaultPalette.blueMid, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '12%', + lineData: { + y: 3000, + yAxisCalloutData: '1%', + }, + }, + + { + x: 40000, + y: -13000, + legend: 'Bananas', + color: getColorFromToken(DataVizPalette.color6), + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-5%', + }, + { + x: 52000, + y: 43000, + legend: 'Giraffes', + color: getColorFromToken(DataVizPalette.color11), + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '18%', + lineData: { + y: 30000, + yAxisCalloutData: '12%', + }, + }, + { + x: 68000, + y: -30000, + legend: 'Cats', + color: DefaultPalette.blueDark, + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-12%', + lineData: { + y: -5000, + yAxisCalloutData: '-2%', + }, + }, + { + x: 80000, + y: 20000, + legend: 'Elephants', + color: getColorFromToken(DataVizPalette.color11), + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '8%', + lineData: { + y: 16000, + yAxisCalloutData: '7%', + }, + }, + { + x: 92000, + y: -45000, + legend: 'Monkeys', + color: getColorFromToken(DataVizPalette.color6), + xAxisCalloutData: '2020/04/30', + yAxisCalloutData: '-19%', + lineData: { + y: -40000, + yAxisCalloutData: '-16%', + }, + }, + ]; + + const lineOptions: ILineChartLineOptions = { lineBorderWidth: '2' }; + + const rootStyle = { width: `${this.state.width}px`, height: `${this.state.height}px` }; + + return ( + <> + + + + + + + + + +
+ +    + +
+ {this.state.showAxisTitles && ( +
+ , + ) => (props ? defaultRender(props) : null), + })} + hideLabels={this.state.hideLabels} + supportNegativeData={true} + enableReflow={true} + yAxisTitle={this.state.showAxisTitles ? 'Different categories of animals and fruits' : undefined} + xAxisTitle={this.state.showAxisTitles ? 'Values of each category' : undefined} + enableGradient={this.state.enableGradient} + roundCorners={this.state.roundCorners} + /> +
+ )} + {!this.state.showAxisTitles && ( +
+ , + ) => (props ? defaultRender(props) : null), + })} + hideLabels={this.state.hideLabels} + enableReflow={true} + enableGradient={this.state.enableGradient} + roundCorners={this.state.roundCorners} + supportNegativeData={true} + /> +
+ )} + + ); + } +} diff --git a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.doc.tsx b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.doc.tsx index e9eb85c996de69..a9ee43726ddb2e 100644 --- a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.doc.tsx +++ b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.doc.tsx @@ -9,6 +9,8 @@ import { VerticalBarChartTooltipExample } from './VerticalBarChart.AxisTooltip.E import { VerticalBarChartCustomAccessibilityExample } from './VerticalBarChart.CustomAccessibility.Example'; import { VerticalBarChartRotatedLabelExample } from './VerticalBarChart.RotateLabels.Example'; import { VerticalBarChartDateAxisExample } from './VerticalBarChart.DateAxis.Example'; +import { VerticalBarChartNegativeExample } from './VerticalBarChart.Negative.Example'; +import { VerticalBarChartAllNegativeExample } from './VerticalBarChart.AllNegative.Example'; const VerticalBarChartBasicExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Basic.Example.tsx') as string; @@ -24,6 +26,10 @@ const VerticalBarChartRotateLabelsExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.RotateLabels.Example.tsx') as string; const VerticalBarChartDateAxisExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.DateAxis.Example.tsx') as string; +const VerticalBarChartNegativeExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Negative.Example.tsx') as string; +const VerticalBarChartAllNegativeExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.AllNegative.Example.tsx') as string; export const VerticalBarChartPageProps: IDocPageProps = { title: 'VerticalBarChart', @@ -66,6 +72,16 @@ export const VerticalBarChartPageProps: IDocPageProps = { code: VerticalBarChartDateAxisExampleCode, view: , }, + { + title: 'VerticalBarChart Negative Y Values', + code: VerticalBarChartNegativeExampleCode, + view: , + }, + { + title: 'VerticalBarChart All Negative Y Values', + code: VerticalBarChartAllNegativeExampleCode, + view: , + }, ], overview: require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/docs/VerticalBarChartOverview.md'), bestPractices: require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/docs/VerticalBarChartBestPractices.md'), diff --git a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChartPage.tsx b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChartPage.tsx index 46ab459ef37ff9..b6c47bfcca4d73 100644 --- a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChartPage.tsx +++ b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChartPage.tsx @@ -15,6 +15,8 @@ import { VerticalBarChartTooltipExample } from './VerticalBarChart.AxisTooltip.E import { VerticalBarChartCustomAccessibilityExample } from './VerticalBarChart.CustomAccessibility.Example'; import { VerticalBarChartRotatedLabelExample } from './VerticalBarChart.RotateLabels.Example'; import { VerticalBarChartDateAxisExample } from './VerticalBarChart.DateAxis.Example'; +import { VerticalBarChartNegativeExample } from './VerticalBarChart.Negative.Example'; +import { VerticalBarChartAllNegativeExample } from './VerticalBarChart.AllNegative.Example'; const VerticalBarChartBasicExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Basic.Example.tsx') as string; @@ -30,6 +32,10 @@ const VerticalBarChartRotateLabelsExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.RotateLabels.Example.tsx') as string; const VerticalBarChartDateAxisExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.DateAxis.Example.tsx') as string; +const VerticalBarChartNegativeExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Negative.Example.tsx') as string; +const VerticalBarChartAllNegativeExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.AllNegative.Example.tsx') as string; export class VerticalBarChartPage extends React.Component { public render(): JSX.Element { @@ -63,6 +69,12 @@ export class VerticalBarChartPage extends React.Component + + + + + + } propertiesTables={ diff --git a/packages/react-examples/src/react-charting/VerticalBarChart/index.stories.tsx b/packages/react-examples/src/react-charting/VerticalBarChart/index.stories.tsx index 58c8330bcdf1c3..0bfb602a6ec6da 100644 --- a/packages/react-examples/src/react-charting/VerticalBarChart/index.stories.tsx +++ b/packages/react-examples/src/react-charting/VerticalBarChart/index.stories.tsx @@ -7,6 +7,8 @@ import { VerticalBarChartDateAxisExample } from './VerticalBarChart.DateAxis.Exa import { VerticalBarChartDynamicExample } from './VerticalBarChart.Dynamic.Example'; import { VerticalBarChartRotatedLabelExample } from './VerticalBarChart.RotateLabels.Example'; import { VerticalBarChartStyledExample } from './VerticalBarChart.Styled.Example'; +import { VerticalBarChartNegativeExample } from './VerticalBarChart.Negative.Example'; +import { VerticalBarChartAllNegativeExample } from './VerticalBarChart.AllNegative.Example'; export const Basic = () => ; @@ -22,6 +24,10 @@ export const RotatedLabel = () => ; export const Tooltip = () => ; +export const Negative = () => ; + +export const AllNegative = () => ; + export default { title: 'Components/VerticalBarChart', };