diff --git a/apps/dashboard-e2e/src/util/ScatterHighchart.ts b/apps/dashboard-e2e/src/util/ScatterHighchart.ts index 95a06e8499..2b3a0ff9e2 100644 --- a/apps/dashboard-e2e/src/util/ScatterHighchart.ts +++ b/apps/dashboard-e2e/src/util/ScatterHighchart.ts @@ -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; @@ -58,6 +60,21 @@ export class ScatterHighchart extends Chart { } 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` ); diff --git a/libs/core-ui/src/lib/Highchart/getDefaultHighchartOptions.ts b/libs/core-ui/src/lib/Highchart/getDefaultHighchartOptions.ts index fcab126305..4b84927ead 100644 --- a/libs/core-ui/src/lib/Highchart/getDefaultHighchartOptions.ts +++ b/libs/core-ui/src/lib/Highchart/getDefaultHighchartOptions.ts @@ -50,8 +50,7 @@ export function getDefaultHighchartOptions(theme: ITheme): Highcharts.Options { }, scatter: { marker: { - radius: 3, - symbol: "circle" + radius: 3 } } }, diff --git a/libs/core-ui/src/lib/components/InteractiveLegend.styles.ts b/libs/core-ui/src/lib/components/InteractiveLegend.styles.ts index 5bb725f08f..3ad6727294 100644 --- a/libs/core-ui/src/lib/components/InteractiveLegend.styles.ts +++ b/libs/core-ui/src/lib/components/InteractiveLegend.styles.ts @@ -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 = - () => { - const theme = getTheme(); - return mergeStyleSets({ - 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 = ( + activated?: boolean, + color?: string +) => { + const theme = getTheme(); + return mergeStyleSets({ + 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 + } + }); +}; diff --git a/libs/core-ui/src/lib/components/InteractiveLegend.tsx b/libs/core-ui/src/lib/components/InteractiveLegend.tsx index 8d392ac9eb..083134a928 100644 --- a/libs/core-ui/src/lib/components/InteractiveLegend.tsx +++ b/libs/core-ui/src/lib/components/InteractiveLegend.tsx @@ -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", @@ -45,6 +46,11 @@ export class InteractiveLegend extends React.PureComponent -
+
{item.name} @@ -70,9 +76,9 @@ export class InteractiveLegend extends React.PureComponent void; } export class InteractiveLegendClickButton extends React.PureComponent { - private readonly classes = interactiveLegendStyles(); + private readonly classes = interactiveLegendStyles(this.props.activated); public render(): React.ReactNode { return ( @@ -29,11 +29,8 @@ export class InteractiveLegendClickButton extends React.PureComponent
{this.props.name} diff --git a/libs/core-ui/src/lib/components/InteractiveLegendUtils.test.ts b/libs/core-ui/src/lib/components/InteractiveLegendUtils.test.ts new file mode 100644 index 0000000000..18d40b42d2 --- /dev/null +++ b/libs/core-ui/src/lib/components/InteractiveLegendUtils.test.ts @@ -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); + }); + }); +}); diff --git a/libs/core-ui/src/lib/components/InteractiveLegendUtils.ts b/libs/core-ui/src/lib/components/InteractiveLegendUtils.ts new file mode 100644 index 0000000000..e24e595453 --- /dev/null +++ b/libs/core-ui/src/lib/components/InteractiveLegendUtils.ts @@ -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; + } +} diff --git a/libs/core-ui/src/lib/util/FluentUIStyles.ts b/libs/core-ui/src/lib/util/FluentUIStyles.ts index 5332215b9f..7be0006901 100644 --- a/libs/core-ui/src/lib/util/FluentUIStyles.ts +++ b/libs/core-ui/src/lib/util/FluentUIStyles.ts @@ -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) @@ -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 { const { black, diff --git a/libs/dataset-explorer/src/lib/ScatterUtils.test.ts b/libs/dataset-explorer/src/lib/ScatterUtils.test.ts new file mode 100644 index 0000000000..3baf3e0726 --- /dev/null +++ b/libs/dataset-explorer/src/lib/ScatterUtils.test.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { getScatterSymbols } from "./ScatterUtils"; + +describe("ScatterUtils", () => { + it("should generate correct symbols", () => { + const expectedSymbols = [ + "circle", + "square", + "diamond", + "triangle", + "triangle-down", + "circle", + "square", + "diamond", + "triangle", + "triangle-down", + "circle", + "square", + "diamond", + "triangle", + "triangle-down" + ]; + expect(getScatterSymbols()).toEqual(expectedSymbols); + }); +}); diff --git a/libs/dataset-explorer/src/lib/ScatterUtils.ts b/libs/dataset-explorer/src/lib/ScatterUtils.ts new file mode 100644 index 0000000000..e79cc1cd7f --- /dev/null +++ b/libs/dataset-explorer/src/lib/ScatterUtils.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export function getScatterSymbols(): string[] { + const symbols = []; + for (let i = 0; i < 3; i++) { + symbols.push("circle", "square", "diamond", "triangle", "triangle-down"); + } + return symbols; +} diff --git a/libs/dataset-explorer/src/lib/SidePanel.tsx b/libs/dataset-explorer/src/lib/SidePanel.tsx index 79a6bf0494..a2ab35d6b2 100644 --- a/libs/dataset-explorer/src/lib/SidePanel.tsx +++ b/libs/dataset-explorer/src/lib/SidePanel.tsx @@ -14,8 +14,8 @@ import { Cohort, ChartTypes, IGenericChartProps, - FluentUIStyles, - InteractiveLegend + InteractiveLegend, + FluentUIStyles } from "@responsible-ai/core-ui"; import { localization } from "@responsible-ai/localization"; import _ from "lodash"; @@ -49,6 +49,7 @@ export class SidePanel extends React.Component { public render(): React.ReactNode { const classNames = datasetExplorerTabStyles(); const colorSeries = this.buildColorLegend(); + const scatterColors = FluentUIStyles.scatterFluentUIColorPalette; return ( { items={colorSeries.map((name, i) => { return { activated: true, - color: FluentUIStyles.fluentUIColorPalette[i], + color: scatterColors[i], index: i, name }; diff --git a/libs/dataset-explorer/src/lib/generatePlotlyProps.ts b/libs/dataset-explorer/src/lib/generatePlotlyProps.ts index 68aaad3fd0..b0dcfbfb44 100644 --- a/libs/dataset-explorer/src/lib/generatePlotlyProps.ts +++ b/libs/dataset-explorer/src/lib/generatePlotlyProps.ts @@ -18,6 +18,7 @@ import Plotly, { DataTransform } from "plotly.js"; import { basePlotlyProperties } from "./basePlotlyProperties"; import { buildCustomData } from "./buildCustomData"; import { buildHoverTemplate } from "./buildHoverTemplate"; +import { getScatterSymbols } from "./ScatterUtils"; export function generatePlotlyProps( jointData: JointDataset, @@ -88,11 +89,14 @@ export function generatePlotlyProps( const styles = jointData.metaDict[ chartProps.colorAxis.property ].sortedCategoricalValues?.map((label, index) => { + const symbols = getScatterSymbols(); + const colors = FluentUIStyles.scatterFluentUIColorPalette; return { target: index, value: { marker: { - color: FluentUIStyles.fluentUIColorPalette[index] + color: colors[index], + symbol: symbols[index] }, name: label } diff --git a/libs/dataset-explorer/src/lib/getDatasetScatter.ts b/libs/dataset-explorer/src/lib/getDatasetScatter.ts index 7d6024e595..e1757f9917 100644 --- a/libs/dataset-explorer/src/lib/getDatasetScatter.ts +++ b/libs/dataset-explorer/src/lib/getDatasetScatter.ts @@ -8,6 +8,7 @@ import { JointDataset } from "@responsible-ai/core-ui"; import { IPlotlyProperty } from "@responsible-ai/mlchartlib"; +import { PointMarkerOptionsObject } from "highcharts"; import _ from "lodash"; import { buildScatterTemplate } from "./buildScatterTemplate"; @@ -17,6 +18,7 @@ export interface IDatasetExplorerSeries { name?: string; color: any; data: IDatasetExplorerData[]; + marker?: PointMarkerOptionsObject; } export interface IDatasetExplorerData { x: number; @@ -77,7 +79,10 @@ export function getDatasetScatter( result.push({ color: styles?.[index].value.marker?.color || getPrimaryChartColor(getTheme()), - data: d + data: d, + marker: { + symbol: (styles?.[index].value.marker?.symbol as string) || "circle" + } }); }); return result; diff --git a/libs/dataset-explorer/src/lib/getGroupedData.ts b/libs/dataset-explorer/src/lib/getGroupedData.ts index 45d009f9c9..64afde3854 100644 --- a/libs/dataset-explorer/src/lib/getGroupedData.ts +++ b/libs/dataset-explorer/src/lib/getGroupedData.ts @@ -50,7 +50,10 @@ export function getGroupedData( result.push({ color: styles?.[index].value.marker?.color || getPrimaryChartColor(getTheme()), - data: d + data: d, + marker: { + symbol: styles?.[index].value.marker?.symbol || "circle" + } }); }); return result;