-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
feat(charts): Update V9 Charts to use DataVizGradientPalette #33323
base: master
Are you sure you want to change the base?
Changes from 20 commits
e5e79fc
b362372
758f388
09b1252
eef1cf2
0c562b5
2eb7d67
942d9a3
d3cc0db
5d90039
f64134a
d9aa34d
40f628c
355faa6
18988c5
61e5ad7
fc6d1c4
e10f630
8a3d7bf
751e2e0
c58d7af
0cc5413
f75812e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "patch", | ||
"comment": "Introduce gradients and rounded corners to v9 charts", | ||
"packageName": "@fluentui/react-charts-preview", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ import { ChartDataPoint } from '../index'; | |
import { ArcProps } from './index'; | ||
import { format as d3Format } from 'd3-format'; | ||
import { formatValueWithSIPrefix, useRtl } from '../../../utilities/index'; | ||
import { useId } from '@fluentui/react-utilities'; | ||
|
||
// Create a Arc within Donut Chart variant which uses these default styles and this styled subcomponent. | ||
/** | ||
|
@@ -13,7 +14,7 @@ import { formatValueWithSIPrefix, useRtl } from '../../../utilities/index'; | |
*/ | ||
export const Arc: React.FunctionComponent<ArcProps> = React.forwardRef<HTMLDivElement, ArcProps>( | ||
(props, forwardedRef) => { | ||
const arc = d3Arc(); | ||
const arc = d3Arc().cornerRadius(5); | ||
const currentRef = React.createRef<SVGPathElement>(); | ||
const _isRTL: boolean = useRtl(); | ||
const classes = useArcStyles_unstable(props); | ||
|
@@ -90,33 +91,58 @@ export const Arc: React.FunctionComponent<ArcProps> = React.forwardRef<HTMLDivEl | |
//TO DO 'replace' is throwing error | ||
const id = props.uniqText! + props.data!.data.legend!.replace(/\s+/, '') + props.data!.data.data; | ||
const opacity: number = props.activeArc === props.data!.data.legend || props.activeArc === '' ? 1 : 0.1; | ||
|
||
const clipId = useId('Arc_clip') + `${props.gradient[0]}_${props.gradient[1]}`; | ||
const gradientFill = `conic-gradient( | ||
from ${props.data?.startAngle}rad, | ||
${props.gradient[0]}, | ||
${props.gradient[1]} ${props.data?.endAngle}rad | ||
)`; | ||
|
||
const pathData = arc({ ...props.data!, innerRadius: props.innerRadius, outerRadius: props.outerRadius })!; | ||
const focusPathData = arc({ ...props.focusData!, innerRadius: props.innerRadius, outerRadius: props.outerRadius })!; | ||
|
||
return ( | ||
<g ref={currentRef}> | ||
{!!focusedArcId && focusedArcId === id && ( | ||
// TODO innerradius and outerradius were absent | ||
<path | ||
id={id + 'focusRing'} | ||
d={arc({ ...props.focusData!, innerRadius: props.innerRadius, outerRadius: props.outerRadius })!} | ||
d={focusPathData} | ||
className={classes.focusRing} | ||
/> | ||
)} | ||
<path | ||
// TODO innerradius and outerradius were absent | ||
id={id} | ||
d={arc({ ...props.data!, innerRadius: props.innerRadius, outerRadius: props.outerRadius })!} | ||
className={classes.root} | ||
style={{ fill: props.color, cursor: href ? 'pointer' : 'default' }} | ||
d={pathData} | ||
style={{ fill: 'transparent', cursor: href ? 'pointer' : 'default' }} | ||
onFocus={_onFocus.bind(this, props.data!.data, id)} | ||
data-is-focusable={props.activeArc === props.data!.data.legend || props.activeArc === ''} | ||
onMouseOver={_hoverOn.bind(this, props.data!.data)} | ||
onMouseMove={_hoverOn.bind(this, props.data!.data)} | ||
onMouseLeave={_hoverOff} | ||
onBlur={_onBlur} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why removed the opacity There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to previous comment, the element is to always be transparent, so opacity is unnecessary |
||
opacity={opacity} | ||
onClick={props.data?.data.onClick} | ||
aria-label={_getAriaLabel()} | ||
role="img" | ||
/> | ||
{/* clipping mask */} | ||
<clipPath id={clipId}> | ||
<path d={pathData} /> | ||
</clipPath> | ||
{/* div to attach conic-gradient fill to */} | ||
<foreignObject x="-50%" y="-50%" width="100%" height="100%" clipPath={`url(#${clipId})`}> | ||
<div | ||
className={classes.root} | ||
style={{ | ||
width: '100%', | ||
height: '100%', | ||
background: gradientFill, | ||
opacity, | ||
}} | ||
/> | ||
</foreignObject> | ||
{_renderArcLabel(classes.arcLabel)} | ||
</g> | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ import { DonutChartProps } from './DonutChart.types'; | |
import { useDonutChartStyles_unstable } from './useDonutChartStyles.styles'; | ||
import { ChartDataPoint } from '../../DonutChart'; | ||
import { convertToLocaleString } from '../../utilities/locale-util'; | ||
import { getColorFromToken, getNextColor } from '../../utilities/index'; | ||
import { getNextGradient } from '../../utilities/index'; | ||
import { Legend, Legends } from '../../index'; | ||
import { useId } from '@fluentui/react-utilities'; | ||
import { useFocusableGroup } from '@fluentui/react-tabster'; | ||
|
@@ -76,13 +76,13 @@ const DonutChartBase: React.FunctionComponent<DonutChartProps> = React.forwardRe | |
}); | ||
return elevatedData; | ||
} | ||
|
||
function _createLegends(chartData: ChartDataPoint[]): JSX.Element { | ||
const legendDataItems = chartData.map((point: ChartDataPoint, index: number) => { | ||
const color: string = point.color!; | ||
// mapping data to the format Legends component needs | ||
const legend: Legend = { | ||
const pointLegend: Legend = { | ||
title: point.legend!, | ||
color, | ||
color: point.gradient![0], | ||
action: () => { | ||
if (selectedLegend === point.legend) { | ||
setSelectedLegend(''); | ||
|
@@ -98,7 +98,7 @@ const DonutChartBase: React.FunctionComponent<DonutChartProps> = React.forwardRe | |
setActiveLegend(''); | ||
}, | ||
}; | ||
return legend; | ||
return pointLegend; | ||
}); | ||
const legends = ( | ||
<Legends | ||
|
@@ -115,7 +115,7 @@ const DonutChartBase: React.FunctionComponent<DonutChartProps> = React.forwardRe | |
setPopoverOpen(selectedLegend === '' || selectedLegend === data.legend); | ||
setValue(data.data!.toString()); | ||
setLegend(data.legend); | ||
setColor(data.color!); | ||
setColor(data.gradient![0]); | ||
setXCalloutValue(data.xAxisCalloutData!); | ||
setYCalloutValue(data.yAxisCalloutData!); | ||
setFocusedArcId(id); | ||
|
@@ -128,7 +128,7 @@ const DonutChartBase: React.FunctionComponent<DonutChartProps> = React.forwardRe | |
setPopoverOpen(selectedLegend === '' || selectedLegend === data.legend); | ||
setValue(data.data!.toString()); | ||
setLegend(data.legend); | ||
setColor(data.color!); | ||
setColor(data.gradient![0]); | ||
setXCalloutValue(data.xAxisCalloutData!); | ||
setYCalloutValue(data.yAxisCalloutData!); | ||
setDataPointCalloutProps(data); | ||
|
@@ -190,17 +190,11 @@ const DonutChartBase: React.FunctionComponent<DonutChartProps> = React.forwardRe | |
); | ||
} | ||
|
||
function _addDefaultColors(donutChartDataPoint?: ChartDataPoint[]): ChartDataPoint[] { | ||
function _addDefaultGradients(donutChartDataPoint?: ChartDataPoint[]): ChartDataPoint[] { | ||
return donutChartDataPoint | ||
? donutChartDataPoint.map((item, index) => { | ||
let defaultColor: string; | ||
if (typeof item.color === 'undefined') { | ||
defaultColor = getNextColor(index, 0); | ||
} else { | ||
defaultColor = getColorFromToken(item.color); | ||
} | ||
return { ...item, defaultColor }; | ||
}) | ||
return { ...item, gradient: item.gradient ?? getNextGradient(index, 0) }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happens if the gradient colors are undefined There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if But if |
||
}) | ||
: []; | ||
} | ||
|
||
|
@@ -252,7 +246,7 @@ const DonutChartBase: React.FunctionComponent<DonutChartProps> = React.forwardRe | |
} | ||
|
||
const { data, hideLegend = false } = props; | ||
const points = _addDefaultColors(data?.chartData); | ||
const points = _addDefaultGradients(data?.chartData); | ||
|
||
const classes = useDonutChartStyles_unstable(props); | ||
|
||
|
@@ -263,6 +257,7 @@ const DonutChartBase: React.FunctionComponent<DonutChartProps> = React.forwardRe | |
const chartData = _elevateToMinimums(points.filter((d: ChartDataPoint) => d.data! >= 0)); | ||
const valueInsideDonut = _valueInsideDonut(props.valueInsideDonut!, chartData!); | ||
const focusAttributes = useFocusableGroup(); | ||
|
||
return !_isChartEmpty() ? ( | ||
<div | ||
className={classes.root} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,9 +80,9 @@ describe('Donut chart interactions', () => { | |
fireEvent.mouseOver(legend!); | ||
|
||
// Assert | ||
const getById = queryAllByAttribute.bind(null, 'id'); | ||
expect(getById(container, /Pie.*?second/i)[0]).toHaveAttribute('opacity', '0.1'); | ||
expect(getById(container, /Pie.*?third/i)[0]).toHaveAttribute('opacity', '0.1'); | ||
const getByClass = queryAllByAttribute.bind(null, 'class'); | ||
expect(getByClass(container, /fui-donut-arc__root/i)[1]).toHaveStyle('opacity: 0.1'); | ||
expect(getByClass(container, /fui-donut-arc__root/i)[2]).toHaveStyle('opacity: 0.1'); | ||
}); | ||
|
||
test('Should select legend on single mouse click on legends', () => { | ||
|
@@ -95,8 +95,8 @@ describe('Donut chart interactions', () => { | |
fireEvent.click(legend!); | ||
|
||
// Assert | ||
const getById = queryAllByAttribute.bind(null, 'id'); | ||
expect(getById(container, /Pie.*?second/i)[0]).toHaveAttribute('opacity', '0.1'); | ||
const getByClass = queryAllByAttribute.bind(null, 'class'); | ||
expect(getByClass(container, /fui-donut-arc__root/i)[1]).toHaveStyle('opacity: 0.1'); | ||
const firstLegend = screen.queryByText('first')?.closest('button'); | ||
expect(firstLegend).toHaveAttribute('aria-selected', 'true'); | ||
expect(firstLegend).toHaveAttribute( | ||
|
@@ -115,8 +115,8 @@ describe('Donut chart interactions', () => { | |
|
||
//single click on first legend | ||
fireEvent.click(legend!); | ||
const getById = queryAllByAttribute.bind(null, 'id'); | ||
expect(getById(container, /Pie.*?second/i)[0]).toHaveAttribute('opacity', '0.1'); | ||
const getByClass = queryAllByAttribute.bind(null, 'class'); | ||
expect(getByClass(container, /fui-donut-arc__root/i)[1]).toHaveStyle('opacity: 0.1'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dont use class names. These are internal and can change anytime There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'll switch to id |
||
const firstLegend = screen.queryByText('first')?.closest('button'); | ||
expect(firstLegend).toHaveAttribute('aria-selected', 'true'); | ||
expect(firstLegend).toHaveAttribute( | ||
|
@@ -138,13 +138,13 @@ describe('Donut chart interactions', () => { | |
const legend = screen.queryByText('first'); | ||
expect(legend).toBeDefined(); | ||
fireEvent.mouseOver(legend!); | ||
const getById = queryAllByAttribute.bind(null, 'id'); | ||
expect(getById(container, /Pie.*?second/i)[0]).toHaveAttribute('opacity', '0.1'); | ||
const getByClass = queryAllByAttribute.bind(null, 'class'); | ||
expect(getByClass(container, /fui-donut-arc__root/i)[1]).toHaveStyle('opacity: 0.1'); | ||
fireEvent.mouseOut(legend!); | ||
|
||
// Assert | ||
expect(getById(container, /Pie.*?first/i)[0]).toHaveAttribute('opacity', '1'); | ||
expect(getById(container, /Pie.*?second/i)[0]).toHaveAttribute('opacity', '1'); | ||
expect(getByClass(container, /fui-donut-arc__root/i)[0]).toHaveStyle('opacity: 1'); | ||
expect(getByClass(container, /fui-donut-arc__root/i)[1]).toHaveStyle('opacity: 1'); | ||
}); | ||
|
||
test('Should display correct callout data on mouse move', async () => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why removed the classname?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved the classname to the
<div>
that renders the gradients. The<path>
element won't be visible, so the class and its styles aren't needed.Test also break if both elements have the class.