Skip to content

Commit

Permalink
[Accessibility] Data analysis scatter chart - use symbol and color to…
Browse files Browse the repository at this point in the history
… differentiate data series (#1667)

* error analysis screen reader

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* fix lint

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* scatter chart

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* lint fix

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* add unit test

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* update test file name

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* fix unit test lint and switch case

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* update color

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* lint

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* fix e2e

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

* fix lint

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>

Signed-off-by: Ruby Zhu <zhenzhu@microsoft.com>
  • Loading branch information
RubyZ10 authored Aug 29, 2022
1 parent f2c5bab commit cb46dc3
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 92 deletions.
17 changes: 17 additions & 0 deletions apps/dashboard-e2e/src/util/ScatterHighchart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { Chart, IChartElement } from "./Chart";

const dReg = /^M ([\d.]+) ([\d.]+) A 3 3 0 1 1 ([\d.]+) ([\d.]+) Z$/;
const qReg =
/^M ([\d.]+) ([\d.]+) L ([\d.]+) ([\d.]+) L ([\d.]+) ([\d.]+) L ([\d.]+) ([\d.]+) Z$/; //quadrangle coordinates

export interface IHighScatter extends IChartElement {
readonly x?: number;
Expand Down Expand Up @@ -58,6 +60,21 @@ export class ScatterHighchart extends Chart<IHighScatter> {
}
const exec = dReg.exec(d);
if (!exec) {
const qExec = qReg.exec(d);
if (qExec) {
const [, ...strCords] = qExec;
const [x1, y1, x2, y2, x3, y3, x4, y4] = strCords.map((s) => Number(s));
return {
bottom: x2,
left: x4,
right: x3,
top: y4,
x: x1,
y1,
y2,
y3
};
}
throw new Error(
`${idx}th path element in svg have invalid "d" attribute`
);
Expand Down
3 changes: 1 addition & 2 deletions libs/core-ui/src/lib/Highchart/getDefaultHighchartOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ export function getDefaultHighchartOptions(theme: ITheme): Highcharts.Options {
},
scatter: {
marker: {
radius: 3,
symbol: "circle"
radius: 3
}
}
},
Expand Down
188 changes: 114 additions & 74 deletions libs/core-ui/src/lib/components/InteractiveLegend.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,85 +11,125 @@ import {
export interface IInteractiveLegendStyles {
root: IStyle;
item: IStyle;
colorBox: IStyle;
circleColorBox: IStyle;
squareColorBox: IStyle;
diamondColorBox: IStyle;
triangleColorBox: IStyle;
triangleDownColorBox: IStyle;
label: IStyle;
editButton: IStyle;
deleteButton: IStyle;
disabledItem: IStyle;
inactiveColorBox: IStyle;
inactiveItem: IStyle;
clickTarget: IStyle;
}

export const interactiveLegendStyles: () => IProcessedStyleSet<IInteractiveLegendStyles> =
() => {
const theme = getTheme();
return mergeStyleSets<IInteractiveLegendStyles>({
clickTarget: {
alignItems: "center",
cursor: "pointer",
display: "flex",
flex: "1",
flexDirection: "row"
},
colorBox: {
borderRadius: "6px",
cursor: "pointer",
display: "inline-block",
height: "12px",
margin: "11px 4px 11px 8px",
width: "12px"
},
deleteButton: {
color: theme.semanticColors.errorText,
display: "inline-block",
width: "16px"
},
disabledItem: {
alignItems: "center",
backgroundColor: theme.semanticColors.disabledBackground,
display: "flex",
flexDirection: "row",
height: "34px",
marginBottom: "1px"
},
editButton: {
color: theme.semanticColors.buttonText,
display: "inline-block",
width: "16px"
},
inactiveColorBox: {
borderRadius: "6px",
cursor: "pointer",
display: "inline-block",
height: "12px",
margin: "11px 4px 11px 8px",
opacity: 0.4,
width: "12px"
},
inactiveItem: {
alignItems: "center",
color: theme.semanticColors.primaryButtonTextDisabled,
display: "flex",
flexDirection: "row",
height: "34px",
marginBottom: "1px"
},
item: {
alignItems: "center",
display: "flex",
flexDirection: "row",
height: "34px",
marginBottom: "1px"
},
label: {
cursor: "pointer",
display: "inline-block",
flex: "1"
},
root: {
paddingBottom: "8px",
paddingTop: "8px"
}
});
};
export const interactiveLegendStyles: (
activated?: boolean,
color?: string
) => IProcessedStyleSet<IInteractiveLegendStyles> = (
activated?: boolean,
color?: string
) => {
const theme = getTheme();
return mergeStyleSets<IInteractiveLegendStyles>({
circleColorBox: {
backgroundColor: color,
borderRadius: "6px",
cursor: "pointer",
display: "inline-block",
height: "12px",
margin: "11px 4px 11px 8px",
opacity: activated ? 1 : 0.4,
width: "12px"
},
clickTarget: {
alignItems: "center",
cursor: "pointer",
display: "flex",
flex: "1",
flexDirection: "row"
},
deleteButton: {
color: theme.semanticColors.errorText,
display: "inline-block",
width: "16px"
},
diamondColorBox: {
backgroundColor: color,
cursor: "pointer",
display: "inline-block",
height: "11px",
margin: "11px 4px 11px 8px",
opacity: activated ? 1 : 0.4,
transform: "rotate(45deg)",
width: "11px"
},
disabledItem: {
alignItems: "center",
backgroundColor: theme.semanticColors.disabledBackground,
display: "flex",
flexDirection: "row",
height: "34px",
marginBottom: "1px"
},
editButton: {
color: theme.semanticColors.buttonText,
display: "inline-block",
width: "16px"
},
inactiveItem: {
alignItems: "center",
color: theme.semanticColors.primaryButtonTextDisabled,
display: "flex",
flexDirection: "row",
height: "34px",
marginBottom: "1px"
},
item: {
alignItems: "center",
display: "flex",
flexDirection: "row",
height: "34px",
marginBottom: "1px"
},
label: {
cursor: "pointer",
display: "inline-block",
flex: "1"
},
root: {
paddingBottom: "8px",
paddingTop: "8px"
},
squareColorBox: {
backgroundColor: color,
cursor: "pointer",
display: "inline-block",
height: "11px",
margin: "11px 4px 11px 8px",
opacity: activated ? 1 : 0.4,
width: "11px"
},
triangleColorBox: {
borderBottom: "12px solid",
borderLeft: "6px solid transparent",
borderRight: "6px solid transparent",
color,
height: 0,
margin: "11px 4px 11px 8px",
opacity: activated ? 1 : 0.4,
width: 0
},
triangleDownColorBox: {
borderLeft: "6px solid transparent",
borderRight: "6px solid transparent",
borderTop: "12px solid",
color,
height: 0,
margin: "11px 4px 11px 8px",
opacity: activated ? 1 : 0.4,
width: 0
}
});
};
10 changes: 8 additions & 2 deletions libs/core-ui/src/lib/components/InteractiveLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from "react";
import { interactiveLegendStyles } from "./InteractiveLegend.styles";
import { InteractiveLegendClickButton } from "./InteractiveLegendClickButton";
import { InteractiveLegendEditAndDeleteButton } from "./InteractiveLegendEditAndDeleteButton";
import { getColorBoxClassName } from "./InteractiveLegendUtils";

export enum SortingState {
Ascending = "ascending",
Expand Down Expand Up @@ -45,14 +46,19 @@ export class InteractiveLegend extends React.PureComponent<IInteractiveLegendPro
}

private buildRowElement(item: ILegendItem, index: number): React.ReactNode {
const colorBoxClassName = getColorBoxClassName(
index,
item.color,
!item.disabled
);
if (item.disabled) {
return (
<div
className={this.classes.disabledItem}
title={item.disabledMessage || ""}
key={index}
>
<div className={this.classes.inactiveColorBox} />
<div className={colorBoxClassName} />
<Text nowrap variant={"medium"} className={this.classes.label}>
{item.name}
</Text>
Expand All @@ -70,9 +76,9 @@ export class InteractiveLegend extends React.PureComponent<IInteractiveLegendPro
<div className={rootClass} key={index}>
<InteractiveLegendClickButton
activated={item.activated}
color={item.color}
index={index}
name={item.name}
colorBoxClassName={colorBoxClassName}
onClick={item.onClick}
/>
<InteractiveLegendEditAndDeleteButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { interactiveLegendStyles } from "./InteractiveLegend.styles";

interface IInteractiveLegendProps {
activated: boolean;
color: string;
index: number;
name: string;
colorBoxClassName?: string;
onClick?: (index: number) => void;
}

export class InteractiveLegendClickButton extends React.PureComponent<IInteractiveLegendProps> {
private readonly classes = interactiveLegendStyles();
private readonly classes = interactiveLegendStyles(this.props.activated);

public render(): React.ReactNode {
return (
Expand All @@ -29,11 +29,8 @@ export class InteractiveLegendClickButton extends React.PureComponent<IInteracti
>
<div
className={
this.props.activated === false
? this.classes.inactiveColorBox
: this.classes.colorBox
this.props.colorBoxClassName || this.classes.circleColorBox
}
style={{ backgroundColor: this.props.color }}
/>
<Text nowrap variant={"medium"} className={this.classes.label}>
{this.props.name}
Expand Down
21 changes: 21 additions & 0 deletions libs/core-ui/src/lib/components/InteractiveLegendUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { interactiveLegendStyles } from "./InteractiveLegend.styles";
import { getColorBoxClassName } from "./InteractiveLegendUtils";

describe("InteractiveLegend", () => {
it("should get the correct colorBox className", () => {
const classes = interactiveLegendStyles();
const classNames = [
classes.circleColorBox,
classes.squareColorBox,
classes.diamondColorBox,
classes.triangleColorBox,
classes.triangleDownColorBox
];
classNames.forEach((className, index) => {
expect(getColorBoxClassName(index)).toEqual(className);
});
});
});
26 changes: 26 additions & 0 deletions libs/core-ui/src/lib/components/InteractiveLegendUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { interactiveLegendStyles } from "./InteractiveLegend.styles";

export function getColorBoxClassName(
index: number,
color?: string,
activated?: boolean
): string {
const classes = interactiveLegendStyles(activated, color);
// this is used as data series symbol in the side panel, the sequence needs to be consist with the sequence of symbols in ScatterUtils getScatterSymbols()
const modIndex = index % 5;
switch (modIndex) {
case 1:
return classes.squareColorBox;
case 2:
return classes.diamondColorBox;
case 3:
return classes.triangleColorBox;
case 4:
return classes.triangleDownColorBox;
default:
return classes.circleColorBox;
}
}
16 changes: 14 additions & 2 deletions libs/core-ui/src/lib/util/FluentUIStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,10 @@ export class FluentUIStyles {

public static fluentUIColorPalette: string[] =
FluentUIStyles.getFlunetUIPalette(getTheme());

public static scatterFluentUIColorPalette: string[] =
FluentUIStyles.getScatterFluentUIPalette(getTheme());
public static plotlyColorHexPalette: string[] =
FluentUIStyles.getFlunetUIPalette(getTheme());

public static plotlyColorPalette: IRGBColor[] =
FluentUIStyles.plotlyColorHexPalette.map((hex) =>
FluentUIStyles.hex2rgb(hex)
Expand Down Expand Up @@ -250,6 +250,18 @@ export class FluentUIStyles {
);
}

public static getScatterFluentUIPalette(theme: ITheme): string[] {
const colors = [];
for (let i = 0; i < 5; i++) {
colors.push(
theme.palette.magentaDark,
theme.palette.orangeLighter,
theme.palette.teal
);
}
return colors;
}

public static getColorsMap(theme: ITheme): Map<keyof IColorNames, string> {
const {
black,
Expand Down
Loading

0 comments on commit cb46dc3

Please sign in to comment.