diff --git a/change/@fluentui-react-charting-2020-10-21-19-09-10-verticalBarChart.json b/change/@fluentui-react-charting-2020-10-21-19-09-10-verticalBarChart.json new file mode 100644 index 0000000000000..38ddc954ceab5 --- /dev/null +++ b/change/@fluentui-react-charting-2020-10-21-19-09-10-verticalBarChart.json @@ -0,0 +1,8 @@ +{ + "type": "minor", + "comment": "Charting: Refactoring Vertical Bar Chart via implementation of Cartesian chart.", + "packageName": "@fluentui/react-charting", + "email": "humbertomakotomorimoto@gmail.com", + "dependentChangeType": "patch", + "date": "2020-10-22T02:09:10.335Z" +} diff --git a/packages/react-charting/src/components/CommonComponents/CartesianChart.base.tsx b/packages/react-charting/src/components/CommonComponents/CartesianChart.base.tsx index 01c68fc4cf08a..cb05a2b9f72cd 100644 --- a/packages/react-charting/src/components/CommonComponents/CartesianChart.base.tsx +++ b/packages/react-charting/src/components/CommonComponents/CartesianChart.base.tsx @@ -203,9 +203,11 @@ export class CartesianChartBase extends React.Component -
(this.legendContainer = e)} className={this._classNames.legendContainer}> - {this.props.legendBars} -
+ {!this.props.hideLegend && ( +
(this.legendContainer = e)} className={this._classNames.legendContainer}> + {this.props.legendBars} +
+ )} {!this.props.hideTooltip && calloutProps!.isCalloutVisible && ( {/** Given custom callout, then it will render */} @@ -336,14 +338,17 @@ export class CartesianChartBase extends React.Component { - const legendContainerComputedStyles = getComputedStyle(this.legendContainer); - const legendContainerHeight = - (this.legendContainer.getBoundingClientRect().height || this.minLegendContainerHeight) + - parseFloat(legendContainerComputedStyles.marginTop || '0') + - parseFloat(legendContainerComputedStyles.marginBottom || '0'); - + let legendContainerHeight; + if (this.props.hideLegend) { + legendContainerHeight = 32; + } else { + const legendContainerComputedStyles = getComputedStyle(this.legendContainer); + legendContainerHeight = + (this.legendContainer.getBoundingClientRect().height || this.minLegendContainerHeight) + + parseFloat(legendContainerComputedStyles.marginTop || '0') + + parseFloat(legendContainerComputedStyles.marginBottom || '0'); + } const container = this.props.parentRef ? this.props.parentRef : this.chartContainer; const currentContainerWidth = container.getBoundingClientRect().width; const currentContainerHeight = diff --git a/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx b/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx index 203f5b97ae5a9..a7469f40a2674 100644 --- a/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx +++ b/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx @@ -1,70 +1,49 @@ import * as React from 'react'; import { max as d3Max } from 'd3-array'; -import { axisRight as d3AxisRight, axisLeft as d3AxisLeft, axisBottom as d3AxisBottom, Axis as D3Axis } from 'd3-axis'; -import { scaleBand as d3ScaleBand, scaleLinear as d3ScaleLinear, ScaleLinear as D3ScaleLinear } from 'd3-scale'; -import { select as d3Select } from 'd3-selection'; -import { format as d3Format } from 'd3-format'; +import { scaleLinear as d3ScaleLinear, ScaleLinear as D3ScaleLinear } from 'd3-scale'; import { classNamesFunction, getId, getRTL } from '@fluentui/react/lib/Utilities'; import { IProcessedStyleSet, IPalette } from '@fluentui/react/lib/Styling'; -import { Callout, DirectionalHint } from '@fluentui/react/lib/Callout'; -import { FocusZone, FocusZoneDirection } from '@fluentui/react-focus'; -import { ILegend, Legends } from '../Legends/index'; -import { ChartHoverCard } from '../../utilities/ChartHoverCard/index'; - +import { DirectionalHint } from '@fluentui/react/lib/Callout'; import { + CartesianChart, + ChartHoverCard, + IBasestate, + IMargins, + ILegend, + IRefArrayData, IVerticalBarChartProps, IVerticalBarChartStyleProps, IVerticalBarChartStyles, IVerticalBarChartDataPoint, -} from './VerticalBarChart.types'; + Legends, +} from '../../index'; +import { FocusZoneDirection } from '@fluentui/react-focus'; +import { ChartTypes, XAxisTypes, NumericAxis, StringAxis } from '../../utilities/index'; const getClassNames = classNamesFunction(); -type NumericAxis = D3Axis; -type StringAxis = D3Axis; - -export interface IVerticalBarChartState { - color: string; - containerWidth: number; - containerHeight: number; - dataForHoverCard: number; - isCalloutVisible: boolean; - isLegendSelected: boolean; - isLegendHovered: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - refSelected: any; +export interface IVerticalBarChartState extends IBasestate { selectedLegendTitle: string; - xCalloutValue?: string; - yCalloutValue?: string; - _width?: number; - _height?: number; -} - -export interface IRefArrayData { - legendText?: string; - refElement?: SVGGElement; + dataPointCalloutProps?: IVerticalBarChartDataPoint; // define this in hover and focus } export class VerticalBarChartBase extends React.Component { private _points: IVerticalBarChartDataPoint[]; private _barWidth: number; - private _yAxisTickCount: number; private _colors: string[]; private _classNames: IProcessedStyleSet; private _refArray: IRefArrayData[]; - private _reqID: number; + private _isNumeric: boolean; private _calloutId: string; - private legendContainer: HTMLDivElement; - private chartContainer: HTMLDivElement; - private minLegendContainerHeight: number = 32; - private margins = { top: 20, right: 20, bottom: 35, left: 40 }; + private margins: IMargins; private _isRtl: boolean = getRTL(); + private _bars: JSX.Element[]; + private _xAxisLabels: string[]; + private _yMax: number; public constructor(props: IVerticalBarChartProps) { super(props); this.state = { color: '', - containerWidth: 0, - containerHeight: 0, dataForHoverCard: 0, isCalloutVisible: false, isLegendSelected: false, @@ -73,195 +52,109 @@ export class VerticalBarChartBase extends React.Component 0 && typeof this._points[0].x === 'number'; - - const xAxis = isNumeric ? this._createNumericXAxis() : this._createStringXAxis(); - const yAxis = this._createYAxis(); - const bars = isNumeric ? this._createNumericBars() : this._createStringBars(); - let legends: JSX.Element; - if (!hideLegend) { - legends = this._getLegendData(this._points, this.props.theme!.palette); - } - - this._classNames = getClassNames(styles!, { - theme: theme!, - className, - width: this.state._width, - height: this.state._height, + this._xAxisLabels = this._points.map((point: IVerticalBarChartDataPoint) => point.x as string); + this._isNumeric = this._points.length > 0 && typeof this._points[0].x === 'number'; + this._yMax = Math.max( + d3Max(this._points, (point: IVerticalBarChartDataPoint) => point.y)!, + this.props.yMaxValue || 0, + ); + const legendBars: JSX.Element = this._getLegendData(this._points, this.props.theme!.palette); + this._classNames = getClassNames(this.props.styles!, { + theme: this.props.theme!, legendColor: this.state.color, - isRtl: this._isRtl, }); - - const svgDimensions = { - width: this.state.containerWidth || 600, - height: this.state.containerHeight || 350, + const calloutProps = { + isCalloutVisible: this.state.isCalloutVisible, + directionalHint: DirectionalHint.topRightEdge, + id: `toolTip${this._calloutId}`, + target: this.state.refSelected, + isBeakVisible: false, + gapSpace: 15, + color: this.state.color, + Legend: this.state.selectedLegendTitle, + XValue: this.state.xCalloutValue, + YValue: this.state.yCalloutValue ? this.state.yCalloutValue : this.state.dataForHoverCard, + ...this.props.calloutProps, + }; + const tickParams = { + tickValues: this.props.tickValues, + tickFormat: this.props.tickFormat, }; - return ( -
(this.chartContainer = rootElem)} - className={this._classNames.root} - > - - - this._setXAxis(node, xAxis)} - className={this._classNames.xAxis} - transform={`translate(0, ${svgDimensions.height - this.margins.bottom})`} - /> - this._setYAxis(node, yAxis)} - className={this._classNames.yAxis} - transform={`translate(${this._isRtl ? svgDimensions.width - this.margins.right - 10 : 40}, 0)`} - /> - {bars} - - - {!hideLegend && ( -
(this.legendContainer = e)} className={this._classNames.legendContainer}> - {legends!} -
- )} - -
+ { + return {this._bars}; + }} + /> ); } private _adjustProps(): void { this._points = this.props.data || []; this._barWidth = this.props.barWidth || 32; - this._yAxisTickCount = this.props.yAxisTickCount || 5; const { palette } = this.props.theme!; this._colors = this.props.colors || [palette.blueLight, palette.blue, palette.blueMid, palette.blueDark]; } - private _fitParentContainer(): void { - const { containerWidth, containerHeight } = this.state; - const { hideLegend = false } = this.props; - - this._reqID = requestAnimationFrame(() => { - let legendContainerHeight; - if (hideLegend) { - legendContainerHeight = 32; - } else { - const legendContainerComputedStyles = getComputedStyle(this.legendContainer); - legendContainerHeight = - (this.legendContainer.getBoundingClientRect().height || (!hideLegend ? this.minLegendContainerHeight : 0)) + - parseFloat(legendContainerComputedStyles.marginTop || '0') + - parseFloat(legendContainerComputedStyles.marginBottom || '0'); - } - - const container = this.props.parentRef ? this.props.parentRef : this.chartContainer; - const currentContainerWidth = container.getBoundingClientRect().width; - const currentContainerHeight = - container.getBoundingClientRect().height > legendContainerHeight - ? container.getBoundingClientRect().height - : 350; - const shouldResize = - containerWidth !== currentContainerWidth || containerHeight !== currentContainerHeight - legendContainerHeight; - if (shouldResize) { - this.setState({ - containerWidth: currentContainerWidth, - containerHeight: currentContainerHeight - legendContainerHeight, - }); - } - }); - } + private _getMargins = (margins: IMargins) => { + this.margins = margins; + }; - private _createNumericXAxis(): NumericAxis { - const xMax = d3Max(this._points, (point: IVerticalBarChartDataPoint) => point.x as number)!; - const xAxisScale = d3ScaleLinear() - .domain(this._isRtl ? [xMax, 0] : [0, xMax]) - .nice() - .range([this.margins.left + this._barWidth, this.state.containerWidth - this.margins.right - this._barWidth]); - const xAxis = d3AxisBottom(xAxisScale).tickPadding(10); - return xAxis; + private _renderCallout(props?: IVerticalBarChartDataPoint): JSX.Element | null { + return props ? ( + + ) : null; } - private _createStringXAxis(): StringAxis { - const xAxisScale = d3ScaleBand() - .domain(this._points.map((point: IVerticalBarChartDataPoint) => point.x as string)) - .range( - this._isRtl - ? [this.state.containerWidth - this.margins.right, this.margins.left] - : [this.margins.left, this.state.containerWidth - this.margins.right], - ); - const xAxis = d3AxisBottom(xAxisScale) - .tickFormat((x: string, index: number) => this._points[index].x as string) - .tickPadding(10); - return xAxis; - } + private _getCustomizedCallout = () => { + return this.props.onRenderCalloutPerDataPoint + ? this.props.onRenderCalloutPerDataPoint(this.state.dataPointCalloutProps, this._renderCallout) + : null; + }; - private _createYAxis(): NumericAxis { - const yMax = d3Max(this._points, (point: IVerticalBarChartDataPoint) => point.y)!; - const interval = Math.ceil(yMax / this._yAxisTickCount); - const domains: Array = [0]; - while (domains[domains.length - 1] < yMax) { - domains.push(domains[domains.length - 1] + interval); - } - const yAxisScale = d3ScaleLinear() - .domain([0, domains[domains.length - 1]]) - .range([this.state.containerHeight - this.margins.bottom, this.margins.top]); - const axis = this._isRtl ? d3AxisRight(yAxisScale) : d3AxisLeft(yAxisScale); - const yAxis = axis - .tickPadding(5) - .tickValues(domains) - .tickFormat(d3Format('.2~s')) - .tickSizeInner(-(this.state.containerWidth - this.margins.left - this.margins.right)); - return yAxis; - } + private _getGraphData = ( + xScale: StringAxis, + yScale: NumericAxis, + containerHeight: number, + containerWidth: number, + ) => { + return (this._bars = this._isNumeric + ? this._createNumericBars(containerHeight, containerWidth) + : this._createStringBars(containerHeight, containerWidth)); + }; - private _createColors(yMax: number): D3ScaleLinear { + private _createColors(): D3ScaleLinear { const increment = this._colors.length <= 1 ? 1 : 1 / (this._colors.length - 1); const domainValues = []; for (let i = 0; i < this._colors.length; i++) { - domainValues.push(increment * i * yMax); + domainValues.push(increment * i * this._yMax); } const colorScale = d3ScaleLinear() .domain(domainValues) @@ -269,33 +162,30 @@ export class VerticalBarChartBase extends React.Component { - const refArray = { legendText: legendTitle, refElement: element }; - this._refArray.push(refArray); + private _refCallback = (element: SVGRectElement, legendTitle: string): void => { + this._refArray.push({ index: legendTitle, refElement: element }); }; private _onBarHover( - customMessage: string, - xVal: string, - pointData: number, + point: IVerticalBarChartDataPoint, color: string, - xAxisCalloutData: string, - yAxisCalloutData: string, mouseEvent: React.MouseEvent, ): void { mouseEvent.persist(); if ( this.state.isLegendSelected === false || - (this.state.isLegendSelected && this.state.selectedLegendTitle === customMessage) + (this.state.isLegendSelected && this.state.selectedLegendTitle === point.legend) ) { this.setState({ refSelected: mouseEvent, isCalloutVisible: true, - dataForHoverCard: pointData, - selectedLegendTitle: customMessage, - color: color, - xCalloutValue: xAxisCalloutData ? xAxisCalloutData : xVal, - yCalloutValue: yAxisCalloutData, + dataForHoverCard: point.y, + selectedLegendTitle: point.legend!, + color: point.color || color, + // To display callout value, if no callout value given, taking given point.x value as a string. + xCalloutValue: point.xAxisCalloutData || point.x.toString(), + yCalloutValue: point.yAxisCalloutData!, + dataPointCalloutProps: point, }); } } @@ -306,147 +196,106 @@ export class VerticalBarChartBase extends React.Component { + private _onBarFocus = (point: IVerticalBarChartDataPoint, refArrayIndexNumber: number, color: string): void => { if ( this.state.isLegendSelected === false || - (this.state.isLegendSelected && this.state.selectedLegendTitle === legendText) + (this.state.isLegendSelected && this.state.selectedLegendTitle === point.legend) ) { this._refArray.forEach((obj: IRefArrayData, index: number) => { - if (obj.legendText === legendText && refArrayIndexNumber === index) { + if (obj.index === point.legend! && refArrayIndexNumber === index) { this.setState({ refSelected: obj.refElement, isCalloutVisible: true, - selectedLegendTitle: legendText, - dataForHoverCard: pointData, - color: color, - xCalloutValue: xAxisCalloutData ? xAxisCalloutData : xVal, - yCalloutValue: yAxisCalloutData, + selectedLegendTitle: point.legend!, + dataForHoverCard: point.y, + color: point.color || color, + xCalloutValue: point.xAxisCalloutData || point.x.toString(), + yCalloutValue: point.yAxisCalloutData!, + dataPointCalloutProps: point, }); } }); } }; - private _createNumericBars(): JSX.Element[] { + private _createNumericBars(containerHeight: number, containerWidth: number): JSX.Element[] { const xMax = d3Max(this._points, (point: IVerticalBarChartDataPoint) => point.x as number)!; - const yMax = d3Max(this._points, (point: IVerticalBarChartDataPoint) => point.y)!; - const { theme, className, styles } = this.props; - const xBarScale = d3ScaleLinear() .domain(this._isRtl ? [xMax, 0] : [0, xMax]) .nice() .range([ - this.margins.left + this._barWidth / 2, - this.state.containerWidth - this.margins.right - this._barWidth - this._barWidth / 2, + this.margins.left! + this._barWidth / 2, + containerWidth - this.margins.right! - this._barWidth - this._barWidth / 2, ]); - const yBarScale = d3ScaleLinear() - .domain([0, yMax]) - .range([0, this.state.containerHeight - this.margins.bottom - this.margins.top]); + .domain([0, this._yMax]) + .range([0, containerHeight - this.margins.bottom! - this.margins.top!]); - const colorScale = this._createColors(yMax); + const colorScale = this._createColors(); const bars = this._points.map((point: IVerticalBarChartDataPoint, index: number) => { let shouldHighlight = true; if (this.state.isLegendHovered || this.state.isLegendSelected) { shouldHighlight = this.state.selectedLegendTitle === point.legend; } - const refArrayIndexNumber = index; - - this._classNames = getClassNames(styles!, { - theme: theme!, - width: this.state._width, - height: this.state._height, - className: className, - shouldHighlight: shouldHighlight, + this._classNames = getClassNames(this.props.styles!, { + theme: this.props.theme!, legendColor: this.state.color, + shouldHighlight: shouldHighlight, }); return ( 0 ? yBarScale(point.y) : 0} + height={Math.max(yBarScale(point.y), 0)} ref={(e: SVGRectElement) => { - this._refCallback(e, point.legend!, refArrayIndexNumber); + this._refCallback(e, point.legend!); }} - onMouseOver={this._onBarHover.bind( - this, - point.legend, - point.x, - point.y, - point.color, - point.xAxisCalloutData!, - point.yAxisCalloutData!, - )} + onMouseOver={this._onBarHover.bind(this, point, colorScale(point.y))} aria-labelledby={this._calloutId} onMouseLeave={this._onBarLeave} - onFocus={this._onBarFocus.bind( - this, - point.legend, - point.x, - point.y, - point.color, - refArrayIndexNumber, - point.xAxisCalloutData!, - point.yAxisCalloutData!, - )} + onFocus={this._onBarFocus.bind(this, point, index, colorScale(point.y))} onBlur={this._onBarLeave} fill={point.color ? point.color : colorScale(point.y)} /> ); }); - return bars; } - private _createStringBars(): JSX.Element[] { - const yMax = d3Max(this._points, (point: IVerticalBarChartDataPoint) => point.y)!; - - const endpointDistance = 0.5 * ((this.state.containerWidth - this.margins.right) / this._points.length); + private _createStringBars(containerHeight: number, containerWidth: number): JSX.Element[] { + const endpointDistance = 0.5 * ((containerWidth - this.margins.right!) / this._points.length); const xBarScale = d3ScaleLinear() .domain(this._isRtl ? [this._points.length - 1, 0] : [0, this._points.length - 1]) .range([ - this.margins.left + endpointDistance - 0.5 * this._barWidth, - this.state.containerWidth - this.margins.right - endpointDistance - 0.5 * this._barWidth, + this.margins.left! + endpointDistance - 0.5 * this._barWidth, + containerWidth - this.margins.right! - endpointDistance - 0.5 * this._barWidth, ]); - const yBarScale = d3ScaleLinear() - .domain([0, yMax]) - .range([0, this.state.containerHeight - this.margins.bottom - this.margins.top]); - - const colorScale = this._createColors(yMax); + .domain([0, this._yMax]) + .range([0, containerHeight - this.margins.bottom! - this.margins.top!]); + const colorScale = this._createColors(); const bars = this._points.map((point: IVerticalBarChartDataPoint, index: number) => { return ( 0 ? yBarScale(point.y) : 0} + height={Math.max(yBarScale(point.y), 0)} aria-labelledby={this._calloutId} - onMouseOver={this._onBarHover.bind( - this, - point.legend!, - point.x, - point.y, - point.color, - point.xAxisCalloutData!, - point.yAxisCalloutData!, - )} + ref={(e: SVGRectElement) => { + this._refCallback(e, point.legend!); + }} + onMouseOver={this._onBarHover.bind(this, point, colorScale(point.y))} onMouseLeave={this._onBarLeave} onBlur={this._onBarLeave} + data-is-focusable={true} + onFocus={this._onBarFocus.bind(this, point, index, colorScale(point.y))} fill={point.color ? point.color : colorScale(point.y)} /> ); @@ -499,7 +348,6 @@ export class VerticalBarChartBase extends React.Component { const color: string = point.color!; // mapping data to the format Legends component needs - const legend: ILegend = { title: point.legend!, color: color, @@ -527,18 +375,4 @@ export class VerticalBarChartBase extends React.Component { - const { className, theme, shouldHighlight, isRtl } = props; - + const { shouldHighlight } = props; return { - root: [ - theme.fonts.medium, - { - width: '100%', - height: '100%', - overflow: 'hidden', - display: 'flex', - flexDirection: 'column', - }, - className, - ], - opacityChangeOnHover: { opacity: shouldHighlight ? '' : '0.1', }, - chartLabel: [ - { - textAlign: 'center', - ...theme.fonts.mediumPlus, - }, - ], - - xAxis: { - selectors: { - text: [ - theme.fonts.tiny, - { - fill: theme.semanticColors.bodyText, - selectors: { - [HighContrastSelectorBlack]: { - fill: 'rgb(179, 179, 179)', - }, - }, - }, - ], - line: { - opacity: 0.1, - width: '1px', - selectors: { - [HighContrastSelectorBlack]: { - opacity: 0.1, - stroke: 'rgb(179, 179, 179)', - }, - }, - }, - path: { - display: 'none', - }, - }, - }, - - yAxis: { - selectors: { - text: [ - theme.fonts.tiny, - { - fill: theme.semanticColors.bodyText, - selectors: { - [HighContrastSelectorBlack]: { - fill: 'rgb(179, 179, 179)', - }, - }, - }, - ], - line: { - opacity: 0.2, - width: '1px', - stroke: theme.semanticColors.bodyText, - selectors: { - [HighContrastSelectorBlack]: { - stroke: 'rgb(179, 179, 179)', - }, - }, - }, - path: { - display: 'none', - }, - g: [ - isRtl && - !isIE11Var && { - textAnchor: 'end', - }, - ], - }, - }, - xAxisTicks: [], yAxisTicks: [ @@ -107,9 +19,5 @@ export const getStyles = (props: IVerticalBarChartStyleProps): IVerticalBarChart transform: 'scaleX(-1)', }, ], - legendContainer: { - marginTop: '8px', - marginLeft: '35px', - }, }; }; diff --git a/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.tsx b/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.tsx index 21d9020795ac7..8bbc224db18bc 100644 --- a/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.tsx +++ b/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { styled } from '../../Utilities'; -import { IVerticalBarChartProps, IVerticalBarChartStyleProps, IVerticalBarChartStyles } from './VerticalBarChart.types'; +import { IVerticalBarChartProps, IVerticalBarChartStyleProps, IVerticalBarChartStyles } from '../../index'; import { VerticalBarChartBase } from './VerticalBarChart.base'; import { getStyles } from './VerticalBarChart.styles'; diff --git a/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.types.ts b/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.types.ts index 69c7f622f6366..febf7df85baba 100644 --- a/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.types.ts +++ b/packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.types.ts @@ -1,199 +1,87 @@ -import { ITheme, IStyle } from '@fluentui/react/lib/Styling'; -import { IStyleFunctionOrObject } from '@fluentui/react/lib/Utilities'; -import { IDataPoint } from '../../types/IDataPoint'; -import { IOverflowSetProps } from '@fluentui/react/lib/OverflowSet'; -import { IFocusZoneProps } from '@fluentui/react-focus'; -import { ILegendsProps } from '../Legends/index'; - -export { IDataPoint, IVerticalBarChartDataPoint } from '../../types/IDataPoint'; - -export interface IVerticalBarChart {} - -export interface IVerticalBarChartProps { +import { IStyle } from '@fluentui/react/lib/Styling'; +import { IRenderFunction, IStyleFunctionOrObject } from '@fluentui/react/lib/Utilities'; +import { + ICartesianChartProps, + ICartesianChartStyleProps, + ICartesianChartStyles, + IDataPoint, + IVerticalBarChartDataPoint, +} from '../../index'; + +export interface IVerticalBarChartProps extends ICartesianChartProps { /** * Data to render in the chart. */ data?: IDataPoint[]; /** - * Width of the chart. - */ - width?: number; - - /** - * Height of the chart. + * Define a custom callout renderer for a data point. */ - height?: number; + onRenderCalloutPerDataPoint?: IRenderFunction; /** * Width of each bar in the chart. */ barWidth?: number; - /** - * Number of ticks on the y-axis. - */ - yAxisTickCount?: number; - /** * Colors from which to select the color of each bar. */ colors?: string[]; - /** - * Label to apply to the whole chart. - */ - chartLabel?: string; - - /** - * Additional CSS class(es) to apply to the VerticalBarChart. - */ - className?: string; - - /** - * Theme (provided through customization.) - */ - theme?: ITheme; - /** * Call to provide customized styling that will layer on top of the variant rules. */ styles?: IStyleFunctionOrObject; - - /** - * Enable the legends to wrap lines if there is not enough space to show all legends on a single line - */ - enabledLegendsWrapLines?: boolean; - - /** - * overflow props for the legends - */ - legendsOverflowProps?: Partial; - - /** - * focus zone props in hover card for legends - */ - focusZonePropsForLegendsInHoverCard?: IFocusZoneProps; - - /** - * text for overflow legends string - */ - legendsOverflowText?: string; - - /** - * decides wether to show/hide legends - * @defaultvalue false - */ - hideLegend?: boolean; - - /** - * Do not show tooltips in chart - * - * @default false - */ - hideTooltip?: boolean; - - /** - * this prop takes its parent as a HTML element to define the width and height of the line chart - */ - parentRef?: HTMLElement | null; - - /** - * props for the legends in the chart - */ - legendProps?: Partial; } -export interface IVerticalBarChartStyleProps { - /** - * Theme (provided through customization.) - */ - theme: ITheme; - - /** - * Additional CSS class(es) to apply to the StackedBarChart. - */ - className?: string; - - /** - * Width of the chart. - */ - width?: number; - - /** - * Height of the chart. - */ - height?: number; - +export interface IVerticalBarChartStyleProps extends ICartesianChartStyleProps { /** * color of the datapoint legend */ legendColor?: string; - - /** - * Link to redirect if click action for graph - */ - href?: string; - - /** - * prop to check if the chart is selcted or hovered upon to determine opacity - */ - shouldHighlight?: boolean; - - /** - * prop to check if the Page is in Rtl - */ - isRtl?: boolean; } -export interface IVerticalBarChartStyles { - /** - * Style for the root element. - */ - root?: IStyle; - +export interface IVerticalBarChartStyles extends ICartesianChartStyles { /** * Style for the chart label. + * @deprecated */ chartLabel?: IStyle; - /** - * Style for the element containing the x-axis. - */ - xAxis?: IStyle; - /** * Style for the line representing the domain of the x-axis. + * @deprecated */ xAxisDomain?: IStyle; /** * Style for the lines representing the ticks along the x-axis. + * @deprecated */ xAxisTicks?: IStyle; /** * Style for the text labeling each tick along the x-axis. + * @deprecated */ xAxisText?: IStyle; - /** - * Style for the element containing the y-axis. - */ - yAxis?: IStyle; - /** * Style for the line representing the domain of the y-axis. + * @deprecated */ yAxisDomain?: IStyle; /** * Style for the lines representing the ticks along the y-axis. + * @deprecated */ yAxisTicks?: IStyle; /** * Style for the text labeling each tick along the y-axis. + * @deprecated */ yAxisText?: IStyle; @@ -201,9 +89,4 @@ export interface IVerticalBarChartStyles { * Style to change the opacity of bars in dataviz when we hover on a single bar or legends */ opacityChangeOnHover: IStyle; - - /** - * Style for the legends container - */ - legendContainer?: IStyle; } diff --git a/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChart.test.tsx.snap b/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChart.test.tsx.snap index fc24d048869c9..9a7e857c702be 100644 --- a/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChart.test.tsx.snap +++ b/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChart.test.tsx.snap @@ -16,7 +16,8 @@ exports[`VerticalBarChart renders VerticalBarChart correctly 1`] = ` overflow: hidden; width: 100%; } - id="VerticalBarChart" + id="chart_1" + role="presentation" >
- + @@ -359,8 +359,5 @@ exports[`VerticalBarChart renders VerticalBarChart correctly 1`] = `
- `; diff --git a/packages/react-charting/src/utilities/utilities.ts b/packages/react-charting/src/utilities/utilities.ts index 27fa8a62c4e9b..80a77d9b36ab5 100644 --- a/packages/react-charting/src/utilities/utilities.ts +++ b/packages/react-charting/src/utilities/utilities.ts @@ -1,5 +1,10 @@ -import { IEventsAnnotationProps } from '../components/LineChart/index'; -import { ILineChartPoints, ILineChartDataPoint, IDataPoint } from '../types/index'; +import { + IEventsAnnotationProps, + ILineChartPoints, + ILineChartDataPoint, + IDataPoint, + IVerticalBarChartDataPoint, +} from '../index'; import { axisRight as d3AxisRight, axisBottom as d3AxisBottom, axisLeft as d3AxisLeft, Axis as D3Axis } from 'd3-axis'; import { max as d3Max, min as d3Min } from 'd3-array'; import { scaleLinear as d3ScaleLinear, scaleTime as d3ScaleTime, scaleBand as d3ScaleBand } from 'd3-scale'; @@ -7,8 +12,8 @@ import { select as d3Select, event as d3Event } from 'd3-selection'; import { format as d3Format } from 'd3-format'; import * as d3TimeFormat from 'd3-time-format'; -type NumericAxis = D3Axis; -type StringAxis = D3Axis; +export type NumericAxis = D3Axis; +export type StringAxis = D3Axis; export enum ChartTypes { AreaChart, @@ -104,7 +109,6 @@ export interface IFitContainerParams { legendContainer: HTMLDivElement; container: HTMLDivElement | null | HTMLElement; } - export const additionalMarginRight: number = 20; /** @@ -194,6 +198,14 @@ export function createStringXAxis(xAxisParams: IXAxisParams, tickParams: ITickPa return xAxisScale; } +/** + * This method uses for creating data points for the y axis. + * @export + * @param {number} maxVal + * @param {number} minVal + * @param {number} splitInto + * @returns {number[]} + */ export function prepareDatapoints(maxVal: number, minVal: number, splitInto: number): number[] { const val = Math.ceil((maxVal - minVal) / splitInto); const dataPointsArray: number[] = [minVal, minVal + val]; @@ -453,6 +465,16 @@ export function getXAxisType(points: ILineChartPoints[]): boolean { return isXAxisDateType; } +/** + * Calculates Domain and range values for Date X axis. + * This method calculates Area chart and line chart. + * @export + * @param {ILineChartPoints[]} points + * @param {IMargins} margins + * @param {number} width + * @param {boolean} isRTL + * @returns {IDomainNRange} + */ export function domainRangeOfDateForAreaChart( points: ILineChartPoints[], margins: IMargins, @@ -483,6 +505,16 @@ export function domainRangeOfDateForAreaChart( : { dStartValue: sDate, dEndValue: lDate, rStartValue, rEndValue }; } +/** + * Calculates Domain and range values for Numeric X axis. + * This method calculates Area cart and line chart. + * @export + * @param {ILineChartPoints[]} points + * @param {IMargins} margins + * @param {number} width + * @param {boolean} isRTL + * @returns {IDomainNRange} + */ export function domainRangeOfNumericForAreaChart( points: ILineChartPoints[], margins: IMargins, @@ -529,7 +561,6 @@ export function domainRangeOfStrForVSBC(margins: IMargins, width: number, isRTL: /** * Calculate domain and range values to the Vertical stacked bar chart - For Numeric axis - * * @export * @param {IDataPoint[]} points * @param {IMargins} margins @@ -555,6 +586,66 @@ export function domainRangeOfVSBCNumeric( : { dStartValue: xMin, dEndValue: xMax, rStartValue: rMax, rEndValue: rMin }; } +/** + * Calculate domain and range values to the Vertical bar chart - For Numeric axis + * @export + * @param {IDataPoint[]} points + * @param {IMargins} margins + * @param {number} containerWidth + * @param {boolean} isRTL + * @param {number} barWidth + * @returns {IDomainNRange} + */ +export function domainRageOfVerticalNumeric( + points: IDataPoint[], + margins: IMargins, + containerWidth: number, + isRTL: boolean, + barWidth: number, +): IDomainNRange { + const xMax = d3Max(points, (point: IVerticalBarChartDataPoint) => point.x as number)!; + const xMin = d3Min(points, (point: IVerticalBarChartDataPoint) => point.x as number)!; + const rMin = margins.left! + barWidth; + const rMax = containerWidth - margins.right! - barWidth; + + return isRTL + ? { dStartValue: xMax, dEndValue: xMin, rStartValue: rMin, rEndValue: rMax } + : { dStartValue: xMin, dEndValue: xMax, rStartValue: rMin, rEndValue: rMax }; +} + +/** + * Calculates Range values to the Vertical bar chart for string axis + * For String axis, we need to give domain values (Not start and end array values) + * So sending 0 as domain values. Domain will be handled at creation of string axis + * @export + * @param {IMargins} margins + * @param {number} containerWidth + * @param {boolean} isRTL + * @returns {IDomainNRange} + */ +export function domainRangeOfStrVertical(margins: IMargins, containerWidth: number, isRTL: boolean): IDomainNRange { + const rMin = margins.left!; + const rMax = containerWidth - margins.right!; + + return isRTL + ? { dStartValue: 0, dEndValue: 0, rStartValue: rMax, rEndValue: rMin } + : { dStartValue: 0, dEndValue: 0, rStartValue: rMin, rEndValue: rMax }; +} + +/** + * For creating X axis, need to calculate x axis domain and range values from given points. + * This may vary based on chart type and type of x axis + * So, this method will define which method need to call based on chart type and axis type. + * @export + * @param {*} points + * @param {IMargins} margins + * @param {number} width + * @param {ChartTypes} chartType + * @param {boolean} isRTL + * @param {XAxisTypes} xAxisType + * @param {number} [barWidth] + * @returns {IDomainNRange} + */ export function getDomainNRangeValues( // eslint-disable-next-line @typescript-eslint/no-explicit-any points: any, @@ -575,6 +666,9 @@ export function getDomainNRangeValues( case ChartTypes.VerticalStackedBarChart: domainNRangeValue = domainRangeOfVSBCNumeric(points, margins, width, isRTL, barWidth!); break; + case ChartTypes.VerticalBarChart: + domainNRangeValue = domainRageOfVerticalNumeric(points, margins, width, isRTL, barWidth!); + break; default: domainNRangeValue = { dStartValue: 0, dEndValue: 0, rStartValue: 0, rEndValue: 0 }; } @@ -592,6 +686,9 @@ export function getDomainNRangeValues( case ChartTypes.VerticalStackedBarChart: domainNRangeValue = domainRangeOfStrForVSBC(margins, width, isRTL); break; + case ChartTypes.VerticalBarChart: + domainNRangeValue = domainRangeOfStrVertical(margins, width, isRTL); + break; default: domainNRangeValue = { dStartValue: 0, dEndValue: 0, rStartValue: 0, rEndValue: 0 }; } @@ -620,6 +717,12 @@ export function findNumericMinMaxOfY(points: ILineChartPoints[]): { startValue: }; } +/** + * Find the minimum and maximum values of the vertical stacked bar chart y axis data point. Used for create y axis. + * @export + * @param {IDataPoint[]} dataset + * @returns {{ startValue: number; endValue: number }} + */ export function findVSBCNumericMinMaxOfY(dataset: IDataPoint[]): { startValue: number; endValue: number } { const yMax = d3Max(dataset, (point: IDataPoint) => point.y)!; const yMin = d3Min(dataset, (point: IDataPoint) => point.y)!; @@ -627,6 +730,23 @@ export function findVSBCNumericMinMaxOfY(dataset: IDataPoint[]): { startValue: n return { startValue: yMin, endValue: yMax }; } +export function findVerticalNumericMinMaxOfY( + points: IVerticalBarChartDataPoint[], +): { startValue: number; endValue: number } { + const yMax = d3Max(points, (point: IVerticalBarChartDataPoint) => point.y)!; + const yMin = d3Min(points, (point: IVerticalBarChartDataPoint) => point.y)!; + + return { startValue: yMin, endValue: yMax }; +} + +/** + * For creating Y axis, need to calculate y axis domain values from given points. This may vary based on chart type. + * So, this method will define which method need to call based on chart type to find out min and max values(For Domain). + * @export + * @param {*} points + * @param {ChartTypes} chartType + * @returns {{ startValue: number; endValue: number }} + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getMinMaxOfYAxis(points: any, chartType: ChartTypes): { startValue: number; endValue: number } { let minMaxValues: { startValue: number; endValue: number }; @@ -639,6 +759,9 @@ export function getMinMaxOfYAxis(points: any, chartType: ChartTypes): { startVal case ChartTypes.VerticalStackedBarChart: minMaxValues = findVSBCNumericMinMaxOfY(points); break; + case ChartTypes.VerticalBarChart: + minMaxValues = findVerticalNumericMinMaxOfY(points); + break; default: minMaxValues = { startValue: 0, endValue: 0 }; } diff --git a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Basic.Example.tsx b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Basic.Example.tsx index 3f2788509cc42..298ae6710a53a 100644 --- a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Basic.Example.tsx +++ b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Basic.Example.tsx @@ -1,13 +1,32 @@ import * as React from 'react'; -import { VerticalBarChart, IVerticalBarChartProps } from '@fluentui/react-charting'; +import { ChartHoverCard, VerticalBarChart, IVerticalBarChartProps } from '@fluentui/react-charting'; import { DefaultPalette } from '@fluentui/react/lib/Styling'; -export class VerticalBarChartBasicExample extends React.Component { +interface IVerticalChartState { + width: number; + height: number; +} +export class VerticalBarChartBasicExample extends React.Component { constructor(props: IVerticalBarChartProps) { super(props); + this.state = { + width: 650, + height: 350, + }; } 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 _basicExample(): JSX.Element { const points = [ { x: 0, @@ -75,11 +94,34 @@ export class VerticalBarChartBasicExample extends React.Component - - + <> + + + + +
+ + props ? ( + + ) : null + } + /> +
+ ); } } diff --git a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Dynamic.Example.tsx b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Dynamic.Example.tsx index 0cced799d1a25..97a5eb6e75736 100644 --- a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Dynamic.Example.tsx +++ b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Dynamic.Example.tsx @@ -49,6 +49,8 @@ export class VerticalBarChartDynamicExample extends React.Component diff --git a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Styled.Example.tsx b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Styled.Example.tsx index e86706fb78230..ef159a9e218f4 100644 --- a/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Styled.Example.tsx +++ b/packages/react-examples/src/react-charting/VerticalBarChart/VerticalBarChart.Styled.Example.tsx @@ -63,7 +63,6 @@ export class VerticalBarChartStyledExample extends React.Component );