Skip to content

Commit b53299a

Browse files
Anush2303Anush
andauthored
feat(react-charts): change foreground color based on contrast ratio in chart table (microsoft#35101)
Co-authored-by: Anush <[email protected]>
1 parent 344cc2b commit b53299a

File tree

3 files changed

+122
-20
lines changed

3 files changed

+122
-20
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "change foreground color based on contrast ratio in chart table",
4+
"packageName": "@fluentui/react-charts",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/charts/react-charts/library/src/components/ChartTable/ChartTable.tsx

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@ import { useChartTableStyles } from './useChartTableStyles.styles';
44
import { useRtl } from '../../utilities/utilities';
55
import { ImageExportOptions } from '../../types/index';
66
import { toImage } from '../../utilities/image-export-utils';
7+
import { tokens } from '@fluentui/react-theme';
8+
import * as d3 from 'd3-color';
9+
import { getColorContrast } from '../../utilities/colors';
10+
11+
function invertHexColor(hex: string): string {
12+
const color = d3.color(hex);
13+
if (!color) {
14+
return tokens.colorNeutralForeground1!;
15+
}
16+
const rgb = color.rgb();
17+
return d3.rgb(255 - rgb.r, 255 - rgb.g, 255 - rgb.b).formatHex();
18+
}
19+
20+
function getSafeBackgroundColor(foreground?: string, background?: string): string {
21+
const fallbackFg = tokens.colorNeutralForeground1;
22+
const fallbackBg = tokens.colorNeutralBackground1;
23+
24+
const fg = d3.color(foreground || fallbackFg);
25+
const bg = d3.color(background || fallbackBg);
26+
if (!fg || !bg) {
27+
return fallbackBg;
28+
}
29+
const contrast = getColorContrast(fg.formatHex(), bg.formatHex());
30+
if (contrast >= 3) {
31+
return bg.formatHex();
32+
}
33+
34+
const invertedBg = invertHexColor(bg.formatHex());
35+
const invertedContrast = getColorContrast(fg.formatHex(), invertedBg);
36+
return invertedContrast >= 3 ? invertedBg : fallbackBg;
37+
}
738

839
export const ChartTable: React.FunctionComponent<ChartTableProps> = React.forwardRef<HTMLDivElement, ChartTableProps>(
940
(props, forwardedRef) => {
@@ -27,6 +58,39 @@ export const ChartTable: React.FunctionComponent<ChartTableProps> = React.forwar
2758
return <div>No data available</div>;
2859
}
2960

61+
const bgColorSet = new Set<string>();
62+
headers.forEach(header => {
63+
const bg = header?.style?.backgroundColor;
64+
const normalized = d3.color(bg || '')?.formatHex();
65+
if (normalized) {
66+
bgColorSet.add(normalized);
67+
}
68+
});
69+
let sharedBackgroundColor: string | undefined;
70+
let useSharedBackground = false;
71+
72+
/*
73+
If we have only one or two unique background colors, we can consider using a shared background color
74+
for the table headers. This is to ensure better contrast with the foreground text.
75+
For size 1, we will consider that as default color if it satisfies the contrast ratio.
76+
There could also be a scenario where backgroundcolor array is of size 2, for eg: ["dimsgray", "gray"],
77+
which will assign 1st column header bg color to dimsgray and rest to gray. so our logic of shared background
78+
color won't run here. So will consider for size 2 as well.
79+
For size greater than this, we will consider that user wants different colors and will let color contrast fail
80+
if any.
81+
*/
82+
if (bgColorSet.size === 1 || bgColorSet.size === 2) {
83+
const candidateBg = bgColorSet.size === 1 ? Array.from(bgColorSet)[0] : Array.from(bgColorSet)[1];
84+
for (const header of headers) {
85+
const fg = header?.style?.color;
86+
if (fg && getColorContrast(fg, candidateBg) >= 3) {
87+
sharedBackgroundColor = candidateBg;
88+
useSharedBackground = true;
89+
break;
90+
}
91+
}
92+
}
93+
3094
return (
3195
<div
3296
ref={el => (_rootElem.current = el)}
@@ -51,22 +115,41 @@ export const ChartTable: React.FunctionComponent<ChartTableProps> = React.forwar
51115
>
52116
<thead>
53117
<tr>
54-
{headers.map((header, idx) => (
55-
<th key={idx} className={classes.headerCell} style={header?.style}>
56-
{header.value}
57-
</th>
58-
))}
118+
{headers.map((header, idx) => {
119+
const style = { ...header?.style };
120+
const fg = style.color;
121+
const bg = style.backgroundColor;
122+
123+
if (useSharedBackground) {
124+
style.backgroundColor = sharedBackgroundColor;
125+
} else if (fg || bg) {
126+
style.backgroundColor = getSafeBackgroundColor(fg, bg);
127+
}
128+
return (
129+
<th key={idx} className={classes.headerCell} style={style}>
130+
{header.value}
131+
</th>
132+
);
133+
})}
59134
</tr>
60135
</thead>
61136
{rows && rows.length > 0 && (
62137
<tbody>
63138
{rows.map((row, rowIdx) => (
64139
<tr key={rowIdx}>
65-
{row.map((cell, colIdx) => (
66-
<td key={colIdx} className={classes.bodyCell} style={cell?.style}>
67-
{cell.value}
68-
</td>
69-
))}
140+
{row.map((cell, colIdx) => {
141+
const style = { ...cell?.style };
142+
const fg = style.color;
143+
const bg = style.backgroundColor;
144+
if (fg || bg) {
145+
style.backgroundColor = getSafeBackgroundColor(fg, bg);
146+
}
147+
return (
148+
<td key={colIdx} className={classes.bodyCell} style={style}>
149+
{cell.value}
150+
</td>
151+
);
152+
})}
70153
</tr>
71154
))}
72155
</tbody>

packages/charts/react-charts/library/src/components/DeclarativeChart/PlotlySchemaAdapter.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,7 +1381,7 @@ export const transformPlotlyJsonToChartTableProps = (
13811381
let fontColor: React.CSSProperties['color'] | undefined;
13821382

13831383
if (Array.isArray(fontColorRaw)) {
1384-
const colorEntry = fontColorRaw[colIndex];
1384+
const colorEntry = fontColorRaw[colIndex] ?? fontColorRaw[0];
13851385
if (Array.isArray(colorEntry)) {
13861386
fontColor = typeof colorEntry[0] === 'string' ? colorEntry[0] : undefined;
13871387
} else if (typeof colorEntry === 'string') {
@@ -1395,17 +1395,24 @@ export const transformPlotlyJsonToChartTableProps = (
13951395
let fontSize: React.CSSProperties['fontSize'] | undefined;
13961396

13971397
if (Array.isArray(fontSizeRaw)) {
1398-
fontSize = Array.isArray(fontSizeRaw[0]) ? fontSizeRaw[0][colIndex] : fontSizeRaw[colIndex];
1398+
const fontSizeEntry = fontSizeRaw[colIndex] ?? fontSizeRaw[0];
1399+
fontSize = Array.isArray(fontSizeRaw[0])
1400+
? fontSizeRaw[0][colIndex] ?? fontSizeRaw[0][0]
1401+
: typeof fontSizeEntry === 'number'
1402+
? fontSizeEntry
1403+
: undefined;
13991404
} else if (typeof fontSizeRaw === 'number') {
14001405
fontSize = fontSizeRaw;
14011406
}
14021407

14031408
const updatedColIndex = colIndex >= 1 ? 1 : 0;
14041409
const fillColorRaw = header?.fill?.color;
1405-
const backgroundColor = Array.isArray(fillColorRaw) ? fillColorRaw[updatedColIndex] : fillColorRaw;
1410+
const backgroundColor = Array.isArray(fillColorRaw)
1411+
? fillColorRaw[updatedColIndex] ?? fillColorRaw[0]
1412+
: fillColorRaw;
14061413

14071414
const textAlignRaw = header?.align;
1408-
const textAlign = Array.isArray(textAlignRaw) ? textAlignRaw[colIndex] : textAlignRaw;
1415+
const textAlign = Array.isArray(textAlignRaw) ? textAlignRaw[colIndex] ?? textAlignRaw[0] : textAlignRaw;
14091416

14101417
const style: React.CSSProperties = {
14111418
...(typeof fontColor === 'string' ? { color: fontColor } : {}),
@@ -1418,7 +1425,10 @@ export const transformPlotlyJsonToChartTableProps = (
14181425
});
14191426
};
14201427
const columns = tableData.cells?.values ?? [];
1421-
const cells = tableData.cells!.font ? tableData.cells! : input.layout?.template?.data?.table![0].cells;
1428+
const cells =
1429+
tableData.cells && Object.keys(tableData.cells).length > 0
1430+
? tableData.cells
1431+
: input.layout?.template?.data?.table?.[0]?.cells;
14221432
const rows = columns[0].map((_, rowIndex: number) =>
14231433
columns.map((col, colIndex) => {
14241434
const cellValue = col[rowIndex];
@@ -1432,7 +1442,7 @@ export const transformPlotlyJsonToChartTableProps = (
14321442
const rawFontColor = cells?.font?.color;
14331443
let fontColor: React.CSSProperties['color'] | undefined;
14341444
if (Array.isArray(rawFontColor)) {
1435-
const entry = rawFontColor[colIndex];
1445+
const entry = rawFontColor[colIndex] ?? rawFontColor[0];
14361446
const colorValue = Array.isArray(entry) ? entry[rowIndex] : entry;
14371447
fontColor = typeof colorValue === 'string' ? colorValue : undefined;
14381448
} else if (typeof rawFontColor === 'string') {
@@ -1442,7 +1452,7 @@ export const transformPlotlyJsonToChartTableProps = (
14421452
const rawFontSize = cells?.font?.size;
14431453
let fontSize: React.CSSProperties['fontSize'] | undefined;
14441454
if (Array.isArray(rawFontSize)) {
1445-
const entry = rawFontSize[colIndex];
1455+
const entry = rawFontSize[colIndex] ?? rawFontSize[0];
14461456
const fontSizeValue = Array.isArray(entry) ? entry[rowIndex] : entry;
14471457
fontSize = typeof fontSizeValue === 'number' ? fontSizeValue : undefined;
14481458
} else if (typeof rawFontSize === 'number') {
@@ -1453,14 +1463,14 @@ export const transformPlotlyJsonToChartTableProps = (
14531463
const rawBackgroundColor = cells?.fill?.color;
14541464
let backgroundColor: React.CSSProperties['backgroundColor'] | undefined;
14551465
if (Array.isArray(rawBackgroundColor)) {
1456-
const entry = rawBackgroundColor[updatedColIndex];
1466+
const entry = rawBackgroundColor[updatedColIndex] ?? rawBackgroundColor[0];
14571467
const colorValue = Array.isArray(entry) ? entry[rowIndex] : entry;
14581468
backgroundColor = typeof colorValue === 'string' ? colorValue : undefined;
14591469
} else if (typeof rawBackgroundColor === 'string') {
14601470
backgroundColor = rawBackgroundColor;
14611471
}
14621472

1463-
const rawTextAlign = Array.isArray(cells?.align) ? cells.align[colIndex] : cells?.align;
1473+
const rawTextAlign = Array.isArray(cells?.align) ? cells.align[colIndex] ?? cells.align[0] : cells?.align;
14641474
const textAlign = rawTextAlign as React.CSSProperties['textAlign'] | undefined;
14651475

14661476
const style: React.CSSProperties = {
@@ -1486,7 +1496,9 @@ export const transformPlotlyJsonToChartTableProps = (
14861496
return {
14871497
headers: normalizeHeaders(
14881498
tableData.header?.values ?? [],
1489-
tableData.header?.font ? tableData.header : input.layout?.template?.data?.table![0].header,
1499+
tableData.header && Object.keys(tableData.header).length > 0
1500+
? tableData.header
1501+
: input.layout?.template?.data?.table![0].header,
14901502
),
14911503
rows,
14921504
width: input.layout?.width,

0 commit comments

Comments
 (0)