Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VerticalStackedBarChart] bar gaps and rounded corners #15606

Merged
merged 5 commits into from
Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "minor",
"comment": "[VerticalStackedBarChart] bar gaps and rounded corners",
"packageName": "@fluentui/react-examples",
"email": "[email protected]",
"dependentChangeType": "patch",
"date": "2020-10-20T04:31:07.173Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "minor",
"comment": "[VerticalStackedBarChart] bar gaps and rounded corners",
"packageName": "@uifabric/charting",
"email": "[email protected]",
"dependentChangeType": "patch",
"date": "2020-10-20T04:31:00.016Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { max as d3Max } from 'd3-array';
import { Axis as D3Axis } from 'd3-axis';
import { scaleLinear as d3ScaleLinear, ScaleLinear as D3ScaleLinear } from 'd3-scale';
import { classNamesFunction, getId, getRTL, find, warnDeprecations } from 'office-ui-fabric-react/lib/Utilities';
import { IProcessedStyleSet, IPalette } from 'office-ui-fabric-react/lib/Styling';
import { IPalette } from 'office-ui-fabric-react/lib/Styling';
import { DirectionalHint } from 'office-ui-fabric-react/lib/Callout';
import { ILegend, Legends } from '../Legends/index';
import {
Expand All @@ -28,6 +28,12 @@ type NumericScale = D3ScaleLinear<number, number>;
type StringScale = D3ScaleLinear<string, string>;
const COMPONENT_NAME = 'VERTICAL STACKED BAR CHART';

// When displaying gaps between bars, the max height of the gap is given in the
// props. The actual gap is calculated with this multiplier, with a minimum gap
// of 1 pixel. (If these values are changed, update the comment for barGapMax.)
const barGapMultiplier = 0.2;
const barGapMin = 1;

interface IRefArrayData {
refElement?: SVGGElement | null;
}
Expand All @@ -48,7 +54,6 @@ export class VerticalStackedBarChartBase extends React.Component<
private _isNumeric: boolean;
private _barWidth: number;
private _calloutId: string;
private _classNames: IProcessedStyleSet<IVerticalStackedBarChartStyles>;
private _colors: string[];
private margins: IMargins;
private _isRtl: boolean = getRTL();
Expand Down Expand Up @@ -96,11 +101,6 @@ export class VerticalStackedBarChartBase extends React.Component<
this._isNumeric = this._dataset.length > 0 && typeof this._dataset[0].x === 'number';
const legendBars: JSX.Element = this._getLegendData(this._points, this.props.theme!.palette);

this._classNames = getClassNames(this.props.styles!, {
href: this.props.href!,
theme: this.props.theme!,
});

const calloutProps = {
isCalloutVisible: this.state.isCalloutVisible,
directionalHint: DirectionalHint.topRightEdge,
Expand Down Expand Up @@ -366,28 +366,37 @@ export class VerticalStackedBarChartBase extends React.Component<
yBarScale: NumericScale,
containerHeight: number,
): JSX.Element[] => {
const { barGapMax = 0, barCornerRadius = 0 } = this.props;

const bars = this._points.map((singleChartData: IVerticalStackedChartProps, indexNumber: number) => {
let startingPointOfY = 0;
let yPoint = containerHeight - this.margins.bottom!;
const isCalloutForStack = this.props.isCalloutForStack || false;
const xPoint = xBarScale(this._isNumeric ? (singleChartData.xAxisPoint as number) : indexNumber);

let xPoint: number | string;
if (this._isNumeric) {
xPoint = xBarScale(singleChartData.xAxisPoint as number);
} else {
xPoint = xBarScale(indexNumber);
}
// Removing datapoints with zero data
// Removing data points with zero data
const nonZeroBars = singleChartData.chartData.filter(point => point.data > 0);

// When displaying gaps between the bars, the height of each bar is
// adjusted so that the total of all bars is not changed by the gaps
const totalData = nonZeroBars.reduce((iter, value) => iter + value.data, 0);
const totalHeight = yBarScale(totalData);
const spaces = barGapMax && nonZeroBars.length - 1;
const spaceHeight = spaces && Math.max(barGapMin, Math.min(barGapMax, (totalHeight * barGapMultiplier) / spaces));
const heightValueRatio = (totalHeight - spaceHeight * spaces) / totalData;

if (heightValueRatio < 0) {
return undefined;
}

const singleBar = nonZeroBars.map((point: IVSChartDataPoint, index: number) => {
startingPointOfY = startingPointOfY + point.data;
const color = point.color ? point.color : this._colors[index];
const ref: IRefArrayData = {};

let shouldHighlight = true;
if (this.state.isLegendHovered || this.state.isLegendSelected) {
shouldHighlight = this.state.selectedLegendTitle === point.legend;
}
this._classNames = getClassNames(this.props.styles!, {
const classNames = getClassNames(this.props.styles!, {
theme: this.props.theme!,
shouldHighlight: shouldHighlight,
href: this.props.href,
Expand All @@ -402,14 +411,40 @@ export class VerticalStackedBarChartBase extends React.Component<
onBlur: this._handleMouseOut,
onClick: this._redirectToUrl,
};

const barHeight = heightValueRatio * point.data;
yPoint = yPoint - barHeight - (index ? spaceHeight : 0);

// If set, apply the corner radius to the top of the final bar
if (barCornerRadius && barHeight > barCornerRadius && index === nonZeroBars.length - 1) {
return (
<path
key={index + indexNumber}
className={classNames.opacityChangeOnHover}
d={`
M ${xPoint} ${yPoint + barCornerRadius}
a ${barCornerRadius} ${barCornerRadius} 0 0 1 ${barCornerRadius} ${-barCornerRadius}
h ${this._barWidth - 2 * barCornerRadius}
a ${barCornerRadius} ${barCornerRadius} 0 0 1 ${barCornerRadius} ${barCornerRadius}
v ${barHeight - barCornerRadius}
h ${-this._barWidth}
z
`}
fill={color}
ref={e => (ref.refElement = e)}
{...rectFocusProps}
/>
);
}

return (
<rect
key={index + indexNumber}
className={this._classNames.opacityChangeOnHover}
className={classNames.opacityChangeOnHover}
x={xPoint}
y={containerHeight - this.margins.bottom! - yBarScale(startingPointOfY)}
y={yPoint}
width={this._barWidth}
height={Math.max(yBarScale(point.data), 0)}
height={barHeight}
fill={color}
ref={e => (ref.refElement = e)}
{...rectFocusProps}
Expand All @@ -427,18 +462,13 @@ export class VerticalStackedBarChartBase extends React.Component<
onClick: this._redirectToUrl,
};
return (
<g
key={indexNumber}
id={`${indexNumber}-singleBar`}
data-is-focusable={this.props.isCalloutForStack}
ref={e => (groupRef.refElement = e)}
{...stackFocusProps}
>
<g key={indexNumber} id={`${indexNumber}-singleBar`} ref={e => (groupRef.refElement = e)} {...stackFocusProps}>
{singleBar}
</g>
);
});
return bars;

return bars.filter((bar): bar is JSX.Element => !!bar);
};

private _createNumericBars = (containerHeight: number, containerWidth: number): JSX.Element[] => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ export interface IVerticalStackedBarChartProps extends ICartesianChartProps {
*/
barWidth?: number;

/**
* Gap (max) between bars in a stack. When non-zero, the bars in a stack will
* be separated by gaps. The actual size of each gap is calculated as 20% of
* the height of that stack, with a minimum size of 1px and a maximum given by
* this prop.
* @default 0
*/
barGapMax?: number;

/**
* Corner radius of the bars
* @default 0
*/
barCornerRadius?: number;

/**
* Colors from which to select the color of each bar.
* @deprecated Not using this prop. DIrectly taking color from given data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ import {
IVerticalStackedBarChartProps,
} from '@uifabric/charting';
import { DefaultPalette, IStyle, DefaultFontStyles } from 'office-ui-fabric-react/lib/Styling';
import { DirectionalHint } from 'office-ui-fabric-react';
import { ChoiceGroup, DirectionalHint, IChoiceGroupOption } from 'office-ui-fabric-react';

const options: IChoiceGroupOption[] = [
{ key: 'singleCallout', text: 'Single callout' },
{ key: 'MultiCallout', text: 'Stack callout' },
];

interface IVerticalStackedBarState {
width: number;
height: number;
barGapMax: number;
barCornerRadius: number;
selectedCallout: string;
}

export class VerticalStackedBarChartStyledExample extends React.Component<{}, IVerticalStackedBarState> {
Expand All @@ -20,23 +28,19 @@ export class VerticalStackedBarChartStyledExample extends React.Component<{}, IV
this.state = {
width: 650,
height: 350,
barGapMax: 2,
barCornerRadius: 2,
selectedCallout: 'MultiCallout',
};
}
public render(): JSX.Element {
return <div>{this._basicExample()}</div>;
}

private _onWidthChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ width: parseInt(e.target.value, 10) });
};
private _onHeightChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ height: parseInt(e.target.value, 10) });
};

private _basicExample(): JSX.Element {
const firstChartPoints: IVSChartDataPoint[] = [
{ legend: 'Metadata1', data: 40, color: DefaultPalette.accent },
{ legend: 'Metadata2', data: 5, color: DefaultPalette.blueMid },
{ legend: 'Metadata1', data: 2, color: DefaultPalette.accent },
{ legend: 'Metadata2', data: 1, color: DefaultPalette.blueMid },
{ legend: 'Metadata3', data: 0, color: DefaultPalette.blueLight },
];

Expand Down Expand Up @@ -76,7 +80,7 @@ export class VerticalStackedBarChartStyledExample extends React.Component<{}, IV
return {
xAxis: {
selectors: {
text: { fill: 'black', fontSize: '8px' },
text: { fill: 'black', fontSize: '10px' },
},
},
chart: {
Expand All @@ -94,31 +98,70 @@ export class VerticalStackedBarChartStyledExample extends React.Component<{}, IV

return (
<>
<label>change Width:</label>
<input type="range" value={this.state.width} min={200} max={1000} onChange={this._onWidthChange} />
<label>change Height:</label>
<input type="range" value={this.state.height} min={200} max={1000} onChange={this._onHeightChange} />
<div>
<label>Width:</label>
<input
type="range"
value={this.state.width}
min={200}
max={1000}
onChange={e => this.setState({ width: +e.target.value })}
/>
<label>Height:</label>
<input
type="range"
value={this.state.height}
min={200}
max={1000}
onChange={e => this.setState({ height: +e.target.value })}
/>
</div>
<div>
<label>BarGapMax:</label>
<input
type="range"
value={this.state.barGapMax}
min={0}
max={10}
onChange={e => this.setState({ barGapMax: +e.target.value })}
/>
<label>BarCornerRadius:</label>
<input
type="range"
value={this.state.barCornerRadius}
min={0}
max={10}
onChange={e => this.setState({ barCornerRadius: +e.target.value })}
/>
<ChoiceGroup
options={options}
defaultSelectedKey="MultiCallout"
// eslint-disable-next-line react/jsx-no-bind
onChange={(_ev, option) => option && this.setState({ selectedCallout: option.key })}
label="Pick one"
/>
</div>
<div style={rootStyle}>
<VerticalStackedBarChart
data={data}
height={this.state.height}
width={this.state.width}
{...this.state}
yAxisTickCount={10}
// Just test link
href={'www.google.com'}
// eslint-disable-next-line react/jsx-no-bind
styles={customStyles}
yMaxValue={120}
yMinValue={10}
isCalloutForStack={this.state.selectedCallout === 'MultiCallout'}
calloutProps={{
directionalHint: DirectionalHint.topCenter,
}}
// eslint-disable-next-line react/jsx-no-bind
yAxisTickFormat={(x: number | string) => `${x} h`}
margins={{
bottom: 0,
top: 0,
left: 0,
bottom: 35,
top: 10,
left: 35,
right: 0,
}}
legendProps={{
Expand All @@ -129,8 +172,8 @@ export class VerticalStackedBarChartStyledExample extends React.Component<{}, IV
},
},
}}
// eslint-disable-next-line react/jsx-no-bind, @typescript-eslint/no-explicit-any
onRenderCalloutPerDataPoint={(props: any) =>
// eslint-disable-next-line react/jsx-no-bind
onRenderCalloutPerDataPoint={props =>
props ? (
<ChartHoverCard
XValue={props.xAxisCalloutData}
Expand Down