disable zoom in error analysis tree (#469)

* disable zoom

Signed-off-by: Ke Xu <xuke@microsoft.com>

* fix label

Signed-off-by: Ke Xu <xuke@microsoft.com>

* remove transition group

Signed-off-by: Ke Xu <xuke@microsoft.com>

* reformat tree

Signed-off-by: Ke Xu <xuke@microsoft.com>

* fix lint

Signed-off-by: Ke Xu <xuke@microsoft.com>

* add width

Signed-off-by: Ke Xu <xuke@microsoft.com>

* adjust size

Signed-off-by: Ke Xu <xuke@microsoft.com>

* fix lint

Signed-off-by: Ke Xu <xuke@microsoft.com>

* adjust tree

Signed-off-by: Ke Xu <xuke@microsoft.com>

* adjust width & hright

Signed-off-by: Ke Xu <xuke@microsoft.com>

* adjust min width

Signed-off-by: Ke Xu <xuke@microsoft.com>

* update snapshot

Signed-off-by: Ke Xu <xuke@microsoft.com>

* fix lint

Signed-off-by: Ke Xu <xuke@microsoft.com>
This commit is contained in:
xuke444 2021-05-10 22:01:30 -07:00 коммит произвёл GitHub
Родитель deccbb7ffa
Коммит e7785e5fb3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
67 изменённых файлов: 649 добавлений и 1121 удалений

Просмотреть файл

@ -2,10 +2,15 @@
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nrwl/nx"],
"globals": {
"JSX": "readonly"
},
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": [
"plugin:@nrwl/nx/typescript",
"plugin:@nrwl/nx/javascript",
"./.eslintrc.comments.eslintrc",
"./.eslintrc.filenames.eslintrc",
"./.eslintrc.react.eslintrc",
@ -42,19 +47,6 @@
],
2
],
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
],
"camelcase": [
"error",
{
@ -312,19 +304,6 @@
"no-unused-vars": "error",
"no-useless-constructor": "error"
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nrwl/nx/typescript"],
"parserOptions": {
"project": "./tsconfig.*?.json"
},
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nrwl/nx/javascript"],
"rules": {}
}
]
}

Просмотреть файл

@ -17,6 +17,7 @@
],
"@typescript-eslint/member-ordering": ["error"],
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/unbound-method": "error",
"@typescript-eslint/naming-convention": [
"error",

Просмотреть файл

@ -1,4 +1,7 @@
{
"globals": {
"JQuery": "readonly"
},
"overrides": [
{
"files": ["src/plugins/index.js"],

Просмотреть файл

@ -13,7 +13,9 @@ export function describeDatasetExplorer(
name: keyof typeof interpretDatasets
): void {
const datasetShape = interpretDatasets[name];
if (datasetShape.noDataset) return;
if (datasetShape.noDataset) {
return;
}
describe(testName, () => {
beforeEach(() => {
cy.visit(`#/interpret/${name}/light/english/Version-2`);

Просмотреть файл

@ -33,7 +33,7 @@ export function describeDataPointChart(dataShape: IInterpretData): void {
!dataShape.noLocalImportance && !dataShape.noFeatureImportance
? "Select a point to see its local importance"
: "Provide local feature importances to see how each feature impacts individual predictions.";
cy.get(`#subPlotContainer`).should("contain.text", message);
cy.get("#subPlotContainer").should("contain.text", message);
});
it("should select the first point", () => {
props.chart.clickNthPoint(0);

Просмотреть файл

@ -13,7 +13,9 @@ export function describeIndividualFeatureImportance(
name: keyof typeof interpretDatasets
): void {
const datasetShape = interpretDatasets[name];
if (datasetShape.noDataset) return;
if (datasetShape.noDataset) {
return;
}
describe(testName, () => {
beforeEach(() => {
cy.visit(`#/interpret/${name}/light/english/Version-2`);

Просмотреть файл

@ -12,9 +12,15 @@ export function describeModelPerformancePlot(
name: keyof typeof interpretDatasets
): void {
const datasetShape = interpretDatasets[name];
if (datasetShape.noDataset) return;
if (datasetShape.noPredict) return;
if (datasetShape.noY) return;
if (datasetShape.noDataset) {
return;
}
if (datasetShape.noPredict) {
return;
}
if (datasetShape.noY) {
return;
}
describe(testName, () => {
beforeEach(() => {
cy.visit(`#/interpret/${name}/light/english/Version-2`);

Просмотреть файл

@ -1,16 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
declare namespace Cypress {
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
// declare namespace Cypress {
// interface Chainable<Subject> {
// login(email: string, password: string): void;
// }
// }
//
// -- This is a parent command --
Cypress.Commands.add("login", (email, password) => {
console.log("Custom command example: Login", email, password);
});
// Cypress.Commands.add("login", (email, password) => {
// console.log("Custom command example: Login", email, password);
// });
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })

Просмотреть файл

@ -44,9 +44,6 @@ export class App extends React.Component<IAppSetting, IAppState> {
<div
style={{
backgroundColor: theme.semanticColors.bodyBackground,
borderColor: "gray",
borderStyle: "solid",
borderWidth: 10,
height: "calc(100% - 70px)",
minHeight: "500px",
width: "calc(100%-20px)"

Просмотреть файл

@ -16,9 +16,6 @@ exports[`FairnessWizardV1 should render successfully 1`] = `
style={
Object {
"backgroundColor": "#ffffff",
"borderColor": "gray",
"borderStyle": "solid",
"borderWidth": 10,
"height": "calc(100% - 70px)",
"minHeight": "500px",
"width": "calc(100%-20px)",

Просмотреть файл

@ -11,7 +11,6 @@ import {
HelpMessageDict
} from "@responsible-ai/error-analysis";
import { Language } from "@responsible-ai/localization";
import _ from "lodash";
import { ITheme } from "office-ui-fabric-react";
import React from "react";
@ -157,8 +156,9 @@ export class App extends React.Component<IAppProps> {
localUrl: "https://www.bing.com/",
requestDebugML: generateJsonTreeBreastCancer,
requestImportances: createJsonImportancesGenerator(
"dataSummary" in this.props.dataset
? this.props.dataset.dataSummary.featureNames!
"dataSummary" in this.props.dataset &&
this.props.dataset.dataSummary.featureNames
? this.props.dataset.dataSummary.featureNames
: [],
true
),

Просмотреть файл

@ -12,7 +12,6 @@ import {
ModelAssessmentDashboard,
IModelAssessmentDashboardProps
} from "@responsible-ai/model-assessment";
import _ from "lodash";
import { ITheme } from "office-ui-fabric-react";
import React from "react";
@ -62,7 +61,7 @@ export class App extends React.Component<IAppProps> {
modelExplanationData: this.props.modelExplanationData,
requestDebugML: generateJsonTreeAdultCensusIncome,
requestImportances: createJsonImportancesGenerator(
this.props.dataset.featureNames!,
this.props.dataset.featureNames,
false
),
requestMatrix: generateJsonMatrix,

Просмотреть файл

@ -1,16 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
declare namespace Cypress {
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
// declare namespace Cypress {
// interface Chainable<Subject> {
// login(email: string, password: string): void;
// }
// }
//
// -- This is a parent command --
Cypress.Commands.add("login", (email, password) => {
console.log("Custom command example: Login", email, password);
});
// Cypress.Commands.add("login", (email, password) => {
// console.log("Custom command example: Login", email, password);
// });
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })

Просмотреть файл

@ -29,7 +29,7 @@ export class CausalInsightsTab extends React.PureComponent<
typeof ModelAssessmentContext
> = defaultModelAssessmentContext;
constructor(props: ICausalInsightsTabProps) {
public constructor(props: ICausalInsightsTabProps) {
super(props);
this.state = { viewOption: CasualAnalysisOptions.Aggregate };
}
@ -65,10 +65,7 @@ export class CausalInsightsTab extends React.PureComponent<
</Stack>
);
}
private onViewTypeChange = (
item?: PivotItem,
_ev?: React.MouseEvent<HTMLElement>
): void => {
private onViewTypeChange = (item?: PivotItem): void => {
if (
item &&
item.props.itemKey &&

Просмотреть файл

@ -7,7 +7,7 @@ import {
ModelAssessmentContext
} from "@responsible-ai/core-ui";
import { localization } from "@responsible-ai/localization";
import _, { isEqual } from "lodash";
import _ from "lodash";
import {
DetailsList,
DetailsListLayoutMode,
@ -93,11 +93,11 @@ export class CasualAggregateTable extends React.PureComponent<
}
public componentDidUpdate(prevProps: ICasualAggregateTableProps): void {
if (!isEqual(prevProps.data, this.props.data)) {
if (!_.isEqual(prevProps.data, this.props.data)) {
this.forceUpdate();
}
}
private getItems = (columns: IColumn[]) => {
private getItems = (columns: IColumn[]): unknown[] => {
const props = columns.map((c) => c.key);
const items = [];
const len = this.props.data[props[0]].length;

Просмотреть файл

@ -87,7 +87,7 @@ export class CasualIndividualChart extends React.PureComponent<
};
}
public componentDidMount() {
public componentDidMount(): void {
const featuresOption = new Array(
this.context.jointDataset.datasetFeatureCount
)

Просмотреть файл

@ -34,7 +34,7 @@ export class CasualIndividualView extends React.PureComponent<
public context: React.ContextType<
typeof ModelAssessmentContext
> = defaultModelAssessmentContext;
constructor(props: ICasualIndividualViewProps) {
public constructor(props: ICasualIndividualViewProps) {
super(props);
this.state = {
selectedData: undefined

Просмотреть файл

@ -23,7 +23,7 @@ export class TreatmentView extends React.PureComponent<
public context: React.ContextType<
typeof ModelAssessmentContext
> = defaultModelAssessmentContext;
constructor(props: ITreatmentViewProps) {
public constructor(props: ITreatmentViewProps) {
super(props);
this.state = {
showModalHelp: false

Просмотреть файл

@ -31,7 +31,7 @@ export class CasualCallout extends React.Component<
public context: React.ContextType<
typeof ModelAssessmentContext
> = defaultModelAssessmentContext;
constructor(props: Record<string, unknown>) {
public constructor(props: Record<string, unknown>) {
super(props);
this.state = {
showCallout: false

Просмотреть файл

@ -141,13 +141,7 @@ export class CohortList extends React.Component<
case "column1":
if (item.name !== "All data") {
return (
<Link
onClick={() =>
this.props.onEditCohortClick(
this.getErrorCohort.bind(this)(item.name)
)
}
>
<Link onClick={this.onEditCohortClick.bind(this, item.name)}>
{fieldContent}
</Link>
);
@ -158,13 +152,16 @@ export class CohortList extends React.Component<
return <span>{fieldContent}</span>;
}
}
return <div></div>;
return React.Fragment;
}
private getErrorCohort(name: string): ErrorCohort {
return this.props.cohorts.find(
private onEditCohortClick(name: string): void {
const cohort = this.props.cohorts.find(
(errorCohort) => errorCohort.cohort.name === name
)!;
);
if (cohort) {
this.props.onEditCohortClick(cohort);
}
}
private getCohortListItems(): ICohortListItem[] {

Просмотреть файл

@ -73,7 +73,7 @@ export class ShiftCohort extends React.Component<
};
}
public componentDidMount() {
public componentDidMount(): void {
const savedCohorts = this.context.errorCohorts.filter(
(errorCohort) => !errorCohort.isTemporary
);

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT License.
import { IComboBox, IComboBoxOption } from "office-ui-fabric-react";
import React from "react";
export enum WeightVectors {
Equal = "equal",

Просмотреть файл

@ -146,7 +146,6 @@ export class AxisConfigDialog extends React.PureComponent<
);
const minVal = this.getMinValue(selectedMeta);
const maxVal = this.getMaxValue(selectedMeta);
return (
<Panel
id="AxisConfigPanel"

Просмотреть файл

@ -38,8 +38,9 @@ export class SVGToolTip extends React.Component<
}
}
public render(): React.ReactNode {
if (!this.state.svgElement || !this.state.isMouseOver)
if (!this.state.svgElement || !this.state.isMouseOver) {
return React.Fragment;
}
const classNames = SVGToolTipStyles();
return ReactDom.createPortal(
<g
@ -83,7 +84,11 @@ export class SVGToolTip extends React.Component<
this.setState({ isMouseOver: false });
};
private svgPoint = (svg: SVGSVGElement, x: number, y: number) => {
private svgPoint = (
svg: SVGSVGElement,
x: number,
y: number
): { x: number; y: number } => {
const ctm = svg.getScreenCTM();
if (svg.createSVGPoint && ctm) {
let point = svg.createSVGPoint();

Просмотреть файл

@ -3,6 +3,6 @@
import { v4 } from "uuid";
export function getRandomId() {
export function getRandomId(): string {
return `Id_${v4()}`;
}

Просмотреть файл

@ -68,7 +68,7 @@ export class DatasetExplorerTab extends React.PureComponent<
};
}
public componentDidMount() {
public componentDidMount(): void {
let initialCohortIndex: number;
if (this.props.showCohortSelection) {
initialCohortIndex = 0;

Просмотреть файл

@ -11,7 +11,7 @@ import {
} from "@responsible-ai/core-ui";
import { IPlotlyProperty, PlotlyMode } from "@responsible-ai/mlchartlib";
import _ from "lodash";
import { DataTransform } from "plotly.js";
import Plotly, { DataTransform } from "plotly.js";
import { basePlotlyProperties } from "./basePlotlyProperties";
import { buildCustomData } from "./buildCustomData";

Просмотреть файл

@ -16,7 +16,10 @@ export enum ColorPalette {
ErrorAnalysisDarkGreyText = "#555"
}
export function isColorDark(colorStr: string): boolean {
export function isColorDark(colorStr: string | undefined): boolean {
if (!colorStr) {
return false;
}
const val = Lab(colorStr).l;
return val <= 65;
}

Просмотреть файл

@ -59,7 +59,7 @@ export class ErrorAnalysisViewTab extends React.PureComponent<
typeof ModelAssessmentContext
> = defaultModelAssessmentContext;
constructor(props: IErrorAnalysisViewTabProps) {
public constructor(props: IErrorAnalysisViewTabProps) {
super(props);
this.state = { openFeatureList: false };
}
@ -105,7 +105,7 @@ export class ErrorAnalysisViewTab extends React.PureComponent<
);
}
private saveFeatures(features: string[]) {
private saveFeatures(features: string[]): void {
this.props.selectFeatures(features);
this.setState({ openFeatureList: false });
}

Просмотреть файл

@ -181,9 +181,12 @@ export class FeatureList extends React.Component<
styles={searchBoxStyles}
onSearch={this.onSearch.bind(this)}
onClear={this.onSearch.bind(this)}
onChange={(_, newValue?: string): void =>
this.onSearch.bind(this)(newValue!)
}
onChange={(_, newValue?: string): void => {
if (newValue === undefined) {
return;
}
this.onSearch.bind(this)(newValue);
}}
/>
</Stack.Item>
<Customizer
@ -250,12 +253,15 @@ export class FeatureList extends React.Component<
private renderRow: IRenderFunction<IDetailsRowProps> = (
props?: IDetailsRowProps
): JSX.Element | null => {
if (!props) {
return <div></div>;
}
return (
<DetailsRow rowFieldsAs={this.renderRowFields.bind(this)} {...props!} />
<DetailsRow rowFieldsAs={this.renderRowFields.bind(this)} {...props} />
);
};
private renderRowFields(props: IDetailsRowFieldsProps) {
private renderRowFields(props: IDetailsRowFieldsProps): JSX.Element {
if (this.props.isEnabled) {
return <DetailsRowFields {...props} />;
}

Просмотреть файл

@ -245,7 +245,7 @@ export class InstanceView extends React.Component<
this.state.selectionDetails.selectedAllSelectedIndexes
}
selectedCohort={this.props.selectedCohort}
setWhatIfDatapoint={(_: number) => {
setWhatIfDatapoint={(): void => {
// do nothing.
}}
/>
@ -371,7 +371,7 @@ export class InstanceView extends React.Component<
private onRenderLabel = (type: SelectionType) => (
p: IChoiceGroupOption | undefined
) => {
): JSX.Element => {
const classNames = InstanceViewStyles();
let selectionText = "";
switch (type) {
@ -405,8 +405,8 @@ export class InstanceView extends React.Component<
return (
<Stack>
<Stack.Item align="start">
<span id={p!.labelId} className="ms-ChoiceFieldLabel">
{p!.text}
<span id={p?.labelId} className="ms-ChoiceFieldLabel">
{p?.text}
</span>
</Stack.Item>
<Stack.Item align="start" styles={stackItemStyles}>

Просмотреть файл

@ -239,7 +239,7 @@ export class MatrixArea extends React.PureComponent<
selectedCells = new Array<boolean>(matrixLength * rowLength);
} else {
// Need to make a copy so setState re-renders
selectedCells = [...this.state.selectedCells!];
selectedCells = [...selectedCells];
}
const index = j + i * rowLength;
selectedCells[index] = !selectedCells[index];
@ -260,11 +260,11 @@ export class MatrixArea extends React.PureComponent<
this.updateStateFromSelectedCells(selectedCells);
}
private updateStateFromSelectedCells(selectedCells: boolean[]) {
private updateStateFromSelectedCells(selectedCells: boolean[]): void {
// Create a composite filter from the selected cells
const compositeFilter = createCompositeFilterFromCells(
selectedCells,
this.state.jsonMatrix!,
this.state.jsonMatrix,
this.props.selectedFeature1,
this.props.selectedFeature2,
this.props.baseCohort,
@ -273,7 +273,7 @@ export class MatrixArea extends React.PureComponent<
const cells = selectedCells.filter(Boolean).length;
const cohortStats = createCohortStatsFromSelectedCells(
selectedCells,
this.state.jsonMatrix!
this.state.jsonMatrix
);
this.props.updateSelectedCohort(
[],

Просмотреть файл

@ -68,7 +68,7 @@ export function fetchMatrix(
export function createCohortStatsFromSelectedCells(
selectedCells: boolean[],
jsonMatrix: any
jsonMatrix: { matrix: unknown[] }
): CohortStats {
let falseCohortCount = 0;
let totalCohortCount = 0;
@ -108,7 +108,7 @@ export function createCohortStatsFromSelectedCells(
export function createCompositeFilterFromCells(
selectedCells: boolean[],
jsonMatrix: any,
jsonMatrix: { matrix: any[]; category1: any; category2: any },
selectedFeature1: string,
selectedFeature2: string,
baseCohort: ErrorCohort,
@ -151,50 +151,50 @@ export function createCompositeFilterFromCells(
const index = j + i * numCols;
const cellCompositeFilters: ICompositeFilter[] = [];
if (selectedCells[index]) {
if (category1Values.length > 0) {
if (category1Values.length > 0 && keyFeature1) {
if (cat1HasIntervals) {
cellCompositeFilters.push({
arg: [
category1Values[i].minIntervalCat,
category1Values[i].maxIntervalCat
],
column: keyFeature1!,
column: keyFeature1,
method: FilterMethods.InTheRangeOf
});
} else {
let cat1arg = category1Values[i].value;
if (typeof cat1arg == "string") {
cat1arg = baseCohort.jointDataset.metaDict[
keyFeature1!
keyFeature1
].sortedCategoricalValues?.indexOf(cat1arg);
}
cellCompositeFilters.push({
arg: [cat1arg],
column: keyFeature1!,
column: keyFeature1,
method: FilterMethods.Equal
});
}
}
if (category2Values.length > 0) {
if (category2Values.length > 0 && keyFeature2) {
if (cat2HasIntervals) {
cellCompositeFilters.push({
arg: [
category2Values[j].minIntervalCat,
category2Values[j].maxIntervalCat
],
column: keyFeature2!,
column: keyFeature2,
method: FilterMethods.InTheRangeOf
});
} else {
let cat2arg = category2Values[j].value;
if (typeof cat2arg == "string") {
cat2arg = baseCohort.jointDataset.metaDict[
keyFeature2!
keyFeature2
].sortedCategoricalValues?.indexOf(cat2arg);
}
cellCompositeFilters.push({
arg: [cat2arg],
column: keyFeature2!,
column: keyFeature2,
method: FilterMethods.Equal
});
}
@ -218,7 +218,11 @@ export function createCompositeFilterFromCells(
return compositeFilters;
}
export function extractCategories(category: any): [any[], boolean] {
export function extractCategories(category: {
values: any[];
intervalMin: any[];
intervalMax: any[];
}): [any[], boolean] {
if (category === undefined) {
return [[], false];
}

Просмотреть файл

@ -109,7 +109,7 @@ export class MatrixCells extends React.PureComponent<IMatrixCellsProps> {
<div
key={`${i}_${j}cell`}
className={classNames.matrixCell}
onClick={() =>
onClick={(): void =>
this.props.selectedCellHandler(i, j, matrixLength, row.length)
}
role="button"
@ -181,11 +181,11 @@ export class MatrixCells extends React.PureComponent<IMatrixCellsProps> {
return outputMin + (outputMax - outputMin) * value;
}
private colorgradRatio(value: number): string {
private colorgradRatio(value: number): string | undefined {
return d3scaleLinear<string>()
.domain([0, 1])
.interpolate(d3interpolateHcl)
.range([ColorPalette.MinColor, ColorPalette.MaxColor])(value)!;
.range([ColorPalette.MinColor, ColorPalette.MaxColor])(value);
}
private colorLookup(ratio: number): string {
@ -197,7 +197,7 @@ export class MatrixCells extends React.PureComponent<IMatrixCellsProps> {
const rate = ratio / 100;
if (rate > 0.01 && rate <= 1) {
result = this.colorgradRatio(this.mapRange(0, 1, rate));
result = this.colorgradRatio(this.mapRange(0, 1, rate)) || "#ffffff";
} else {
result = "#ffffff";
}

Просмотреть файл

@ -131,7 +131,7 @@ export class Navigation extends React.Component<INavigationProps> {
className="ms-Breadcrumb-itemLink"
href={item.href}
onClick={(e: React.MouseEvent<HTMLElement>): void =>
item.onClick!(e, item)
item.onClick?.(e, item)
}
color="blue"
>

Просмотреть файл

@ -67,7 +67,7 @@ const onRenderDetailsHeader: IRenderFunction<IDetailsHeaderProps> = (
) => <TooltipHost {...tooltipHostProps} />;
return (
<div>
{defaultRender!({
{defaultRender?.({
...props,
onRenderColumnHeaderTooltip,
selectAllVisibility: SelectAllVisibility.hidden

Просмотреть файл

@ -62,8 +62,7 @@ export const treeLegendStyles: () => IProcessedStyleSet<
pointerEvents: "auto"
},
treeLegend: {
paddingLeft: "35px",
zIndex: "-1"
width: "15em"
},
valueBlack: mergeStyles(value, {
color: theme.palette.black,

Просмотреть файл

@ -1,14 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { ErrorCohort, ExpandableText } from "@responsible-ai/core-ui";
import { ErrorCohort } from "@responsible-ai/core-ui";
import { localization } from "@responsible-ai/localization";
import {
IStackStyles,
IStackTokens,
Stack,
Text
} from "office-ui-fabric-react";
import { IStackTokens, Stack, Text } from "office-ui-fabric-react";
import React from "react";
import { ColorPalette } from "../../ColorPalette";
@ -28,13 +23,6 @@ export interface ITreeLegendProps {
const stackTokens: IStackTokens = { childrenGap: 5 };
const cellTokens: IStackTokens = { padding: 10 };
const legendDescriptionPadding: IStackTokens = { padding: "20px 0px 20px 0px" };
const legendDescriptionStyle: IStackStyles = {
root: {
pointerEvents: "auto",
width: 500
}
};
export class TreeLegend extends React.Component<ITreeLegendProps> {
private readonly _errorRateIconId = "errorRateIconId";
@ -43,99 +31,83 @@ export class TreeLegend extends React.Component<ITreeLegendProps> {
const classNames = treeLegendStyles();
return (
<div className={classNames.treeLegend}>
<Stack
styles={legendDescriptionStyle}
tokens={legendDescriptionPadding}
>
<ExpandableText
expandedText={
localization.ErrorAnalysis.TreeView.treeDescriptionExpanded
}
iconName="Info"
variant={"smallPlus"}
>
{localization.ErrorAnalysis.TreeView.treeDescription}
</ExpandableText>
</Stack>
<Stack tokens={stackTokens}>
<Text variant={"xLarge"} block>
Cohort: {this.props.baseCohort.cohort.name}
</Text>
<Stack horizontal>
<Stack>
<Stack horizontal>
<div className={classNames.metricBarBlack}></div>
<Stack tokens={cellTokens}>
<div className={classNames.smallHeader}>
{localization.ErrorAnalysis.errorCoverage}
<InfoCallout
iconId={this._errorCoverageIconId}
infoText={localization.ErrorAnalysis.errorCoverageInfo}
title={localization.ErrorAnalysis.errorCoverageTitle}
></InfoCallout>
</div>
<div className={classNames.valueBlack}>
{this.props.selectedCohort.errorCoverage.toFixed(2)}%
</div>
</Stack>
<Stack>
<Stack horizontal>
<div className={classNames.metricBarBlack}></div>
<Stack tokens={cellTokens}>
<div className={classNames.smallHeader}>
{localization.ErrorAnalysis.errorCoverage}
<InfoCallout
iconId={this._errorCoverageIconId}
infoText={localization.ErrorAnalysis.errorCoverageInfo}
title={localization.ErrorAnalysis.errorCoverageTitle}
></InfoCallout>
</div>
<div className={classNames.valueBlack}>
{this.props.selectedCohort.errorCoverage.toFixed(2)}%
</div>
</Stack>
<svg
width="60"
height="60"
viewBox="-2 -2 56 56"
pointerEvents="auto"
>
<mask id="detailMask">
<rect x="-26" y="-26" width="52" height="52" fill="white" />
</mask>
<g className={classNames.opacityToggleCircle}>
</Stack>
<svg
width="60"
height="60"
viewBox="-2 -2 56 56"
pointerEvents="auto"
>
<mask id="detailMask">
<rect x="-26" y="-26" width="52" height="52" fill="white" />
</mask>
<g className={classNames.opacityToggleCircle}>
<circle
r="26"
className={classNames.node}
style={this.props.nodeDetail.errorColor}
/>
<g
style={this.props.nodeDetail.maskDown}
mask="url(#detailMask)"
className={classNames.nopointer}
>
<circle
r="26"
className={classNames.node}
style={this.props.nodeDetail.errorColor}
fill={ColorPalette.FillStyle}
style={this.props.nodeDetail.maskUp}
/>
<g
style={this.props.nodeDetail.maskDown}
mask="url(#detailMask)"
className={classNames.nopointer}
>
<circle
r="26"
className={classNames.node}
fill={ColorPalette.FillStyle}
style={this.props.nodeDetail.maskUp}
/>
</g>
</g>
</svg>
</Stack>
<Stack>
<Stack horizontal>
<div className={classNames.metricBarRed}></div>
<Stack tokens={cellTokens}>
<div className={classNames.smallHeader}>
{localization.ErrorAnalysis.errorRate}
<InfoCallout
iconId={this._errorRateIconId}
infoText={localization.ErrorAnalysis.errorRateInfo}
title={localization.ErrorAnalysis.errorRateTitle}
></InfoCallout>
</div>
<div className={classNames.valueBlack}>
{this.props.selectedCohort.errorRate.toFixed(2)}%
</div>
</Stack>
</g>
</svg>
</Stack>
<Stack>
<Stack horizontal>
<div className={classNames.metricBarRed}></div>
<Stack tokens={cellTokens}>
<div className={classNames.smallHeader}>
{localization.ErrorAnalysis.errorRate}
<InfoCallout
iconId={this._errorRateIconId}
infoText={localization.ErrorAnalysis.errorRateInfo}
title={localization.ErrorAnalysis.errorRateTitle}
></InfoCallout>
</div>
<div className={classNames.valueBlack}>
{this.props.selectedCohort.errorRate.toFixed(2)}%
</div>
</Stack>
<svg width="60" height="60" viewBox="0 0 40 40">
<g>
<ErrorRateGradient
max={this.props.max}
minPct={0}
selectedCohort={this.props.selectedCohort}
/>
</g>
</svg>
</Stack>
<svg width="60" height="60" viewBox="0 0 40 40">
<g>
<ErrorRateGradient
max={this.props.max}
minPct={0}
selectedCohort={this.props.selectedCohort}
/>
</g>
</svg>
</Stack>
</Stack>
</div>

Просмотреть файл

@ -90,7 +90,7 @@ export class TreeViewNode extends React.Component<ITreeViewNodeProps> {
private getNodeClassName(
classNames: IProcessedStyleSet<ITreeViewRendererStyles>,
ratio: number,
fill: string
fill: string | undefined
): string {
let nodeTextClassName = classNames.nodeText;
if (ratio > 50 && isColorDark(fill)) {

Просмотреть файл

@ -5,7 +5,8 @@ import {
IStyle,
mergeStyleSets,
IProcessedStyleSet,
mergeStyles
mergeStyles,
getTheme
} from "office-ui-fabric-react";
import { ColorPalette } from "../../ColorPalette";
@ -15,32 +16,20 @@ export interface ITreeViewRendererStyles {
clickedNodeFull: IStyle;
detailLines: IStyle;
filledNodeText: IStyle;
innerFrame: IStyle;
legend: IStyle;
linkLabel: IStyle;
linkLabelsTransitionGroup: IStyle;
linksTransitionGroup: IStyle;
mainFrame: IStyle;
node: IStyle;
nodeText: IStyle;
nodesGroup: IStyle;
nopointer: IStyle;
svgOuterFrame: IStyle;
svgContainer: IStyle;
treeDescription: IStyle;
tooltipTransitionGroup: IStyle;
}
export const treeViewRendererStyles: () => IProcessedStyleSet<
ITreeViewRendererStyles
> = () => {
const detailStyle = {
fill: "#777",
font: "normal 12px Arial",
fontFamily: "Arial, Helvetica, sans-serif",
fontWeight: "bold",
pointerEvents: "none",
textAnchor: "middle"
};
const theme = getTheme();
const nodeTextStyle = {
fontSize: "10px",
fontWeight: "bolder",
@ -59,48 +48,27 @@ export const treeViewRendererStyles: () => IProcessedStyleSet<
stroke: "#0078D4",
strokeWidth: 2
},
detailLines: mergeStyles([
detailStyle,
{
textAnchor: "start",
transform: "translate(0px, 10px)"
}
]),
detailLines: {
fill: theme.semanticColors.inputBackground,
fontWeight: "bold",
pointerEvents: "none",
textAnchor: "start",
transform: "translate(0px, 10px)"
},
filledNodeText: mergeStyles([
nodeTextStyle,
{
fill: ColorPalette.ErrorAnalysisLightText
}
]),
innerFrame: {
height: "100%",
margin: "0",
padding: "0",
width: "100%"
},
legend: {
pointerEvents: "none",
position: "absolute"
pointerEvents: "none"
},
linkLabel: {
fill: "#777",
font: "normal 12px Arial",
fontFamily: "Arial, Helvetica, sans-serif",
pointerEvents: "none",
textAnchor: "middle"
},
linkLabelsTransitionGroup: {
transform: "translate(0px, 90px)"
},
linksTransitionGroup: {
transform: "translate(0px, 90px)"
},
mainFrame: {
height: "1100px",
margin: "0",
padding: "0",
width: "100%"
},
node: {
":hover": {
strokeWidth: "3px"
@ -110,9 +78,6 @@ export const treeViewRendererStyles: () => IProcessedStyleSet<
stroke: "#089acc",
strokeWidth: "0px"
},
nodesGroup: {
transform: "translate(0px, 90px)"
},
nodeText: mergeStyles([
nodeTextStyle,
{
@ -122,13 +87,14 @@ export const treeViewRendererStyles: () => IProcessedStyleSet<
nopointer: {
pointerEvents: "none"
},
svgOuterFrame: {
margin: "0",
padding: "0",
svgContainer: {
overflow: "auto",
width: "100%"
},
tooltipTransitionGroup: {
transform: "translate(40px, 90px)"
svgOuterFrame: {
margin: 0,
outline: "none",
padding: 0
},
treeDescription: {
padding: "30px 0px 0px 35px"

Просмотреть файл

@ -7,8 +7,11 @@ import {
FilterMethods,
CohortSource,
CohortStats,
ErrorCohort
ErrorCohort,
ExpandableText,
getRandomId
} from "@responsible-ai/core-ui";
import { localization } from "@responsible-ai/localization";
import { max as d3max } from "d3-array";
import {
stratify as d3stratify,
@ -19,13 +22,17 @@ import { interpolateHcl as d3interpolateHcl } from "d3-interpolate";
import { scaleLinear as d3scaleLinear } from "d3-scale";
import { select } from "d3-selection";
import { linkVertical as d3linkVertical } from "d3-shape";
import { D3ZoomEvent, zoom as d3zoom } from "d3-zoom";
import { IProcessedStyleSet, ITheme } from "office-ui-fabric-react";
import {
IProcessedStyleSet,
ITheme,
mergeStyles,
Stack
} from "office-ui-fabric-react";
import React from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { ColorPalette } from "../../ColorPalette";
import { FilterProps } from "../../FilterProps";
import { IRequestNode } from "../../Interfaces/IErrorAnalysisDashboardProps";
import { HelpMessageDict } from "../../Interfaces/IStringsParam";
import {
INodeDetail,
@ -43,13 +50,19 @@ import {
// Importing this solely to set the selectedPanelId. This component is NOT a statefulContainer
// import StatefulContainer from '../../ap/mixins/statefulContainer.js'
const viewerHeight = 300;
const viewerWidth = 800;
export interface ITreeViewRendererProps {
theme?: ITheme;
messages?: HelpMessageDict;
features: string[];
selectedFeatures: string[];
getTreeNodes?: (request: any[], abortSignal: AbortSignal) => Promise<any[]>;
staticTreeNodes?: any;
getTreeNodes?: (
request: any[],
abortSignal: AbortSignal
) => Promise<IRequestNode[]>;
staticTreeNodes?: { data: IRequestNode[] };
updateSelectedCohort: (
filters: IFilter[],
compositeFilters: ICompositeFilter[],
@ -63,23 +76,6 @@ export interface ITreeViewRendererProps {
setTreeViewState: (treeViewState: ITreeViewRendererState) => void;
}
// Represents the data retrieved from the backend
export interface IRequestNode {
arg: number;
condition: string;
error: number;
id: string;
method: string;
nodeIndex: number;
nodeName: string;
parentId: string;
parentNodeName: string;
pathFromRoot: string;
size: number;
sourceRowKeyHash: string;
success: number;
}
export interface ISVGDatum {
width: number;
height: number;
@ -87,7 +83,6 @@ export interface ISVGDatum {
}
const svgOuterFrame: React.RefObject<SVGSVGElement> = React.createRef();
const treeZoomPane: React.RefObject<SVGSVGElement> = React.createRef();
const errorAvgColor = ColorPalette.ErrorAvgColor;
const errorRatioThreshold = 1;
@ -101,6 +96,16 @@ export class TreeViewRenderer extends React.PureComponent<
this.state = this.props.state;
}
public componentDidMount(): void {
window.addEventListener("resize", this.onResize);
if (!this.state.treeNodes?.[0]) {
this.fetchTreeNodes();
} else {
this.onResize();
this.forceUpdate();
}
}
public componentDidUpdate(prevProps: ITreeViewRendererProps): void {
if (
this.props.selectedFeatures !== prevProps.selectedFeatures ||
@ -111,40 +116,43 @@ export class TreeViewRenderer extends React.PureComponent<
}
}
public componentWillUnmount(): void {
window.removeEventListener("resize", this.onResize);
this.props.setTreeViewState(this.state);
}
public render(): React.ReactNode {
if (!this.state.root) {
return <div></div>;
return React.Fragment;
}
const classNames = treeViewRendererStyles();
const labelPaddingX = 20;
const labelPaddingY = 8;
const labelYOffset = 3;
const rootDescendents = this.state.root.descendants();
const max: number = d3max(
rootDescendents,
(d) => d.data.error / d.data.size
)!;
const zoom = d3zoom<SVGSVGElement, ISVGDatum>()
.scaleExtent([1 / 3, 4])
.on("zoom", this.zoomed.bind(this));
const rootDescendants = this.state.root.descendants();
const max = d3max(rootDescendants, (d) => d.data.error / d.data.size) || 0;
if (svgOuterFrame.current) {
const svg = select<SVGSVGElement, undefined>(
svgOuterFrame.current!
).datum<ISVGDatum>({
const svg = select<SVGSVGElement, undefined>(svgOuterFrame.current).datum<
ISVGDatum
>({
filterBrushEvent: true,
height: this.state.viewerHeight,
width: this.state.viewerWidth
height: viewerHeight,
width: viewerWidth
});
svg.style("pointer-events", "all").call(zoom as any);
svg.style("pointer-events", "all");
}
let pathMin = viewerWidth;
let pathMax = 0;
const linkVertical = d3linkVertical<any, HierarchyPointNode<ITreeNode>>()
.x((d: HierarchyPointNode<ITreeNode>) => d!.x!)
.y((d: HierarchyPointNode<ITreeNode>) => d!.y!);
const linkVertical = d3linkVertical<
unknown,
HierarchyPointNode<ITreeNode>
>()
.x((d: HierarchyPointNode<ITreeNode>) => d.x)
.y((d: HierarchyPointNode<ITreeNode>) => d.y);
// GENERATES LINK DATA BETWEEN NODES
// -------------------------------------------------------------------
// The links between the nodes in the tree view are generated below.
@ -153,18 +161,18 @@ export class TreeViewRenderer extends React.PureComponent<
// or not we highlight it. We use the d3 linkVertical which is a curved
// spline to draw the link. The thickness of the links depends on the
// ratio of data going through the path versus overall data in the tree.
const links = rootDescendents
const links = rootDescendants
.slice(1)
.map((d: HierarchyPointNode<ITreeNode>) => {
const thick = 1 + Math.floor(30 * (d.data.size / this.state.rootSize));
const lineColor = d.data.nodeState.onSelectedPath
? ColorPalette.SelectedLineColor
: ColorPalette.UnselectedLineColor;
const id: string = d.id!;
const id = d.id || "";
const linkVerticalD = linkVertical({ source: d.parent, target: d });
return {
d: linkVerticalD!,
id: id + Math.random(),
d: linkVerticalD || "",
id: id + getRandomId(),
style: { fill: "white", stroke: lineColor, strokeWidth: thick }
};
});
@ -174,43 +182,45 @@ export class TreeViewRenderer extends React.PureComponent<
// Generates the labels on the links. This initially writes the text,
// calculates the bounding box using getTextBB, and then returns the
// properties (x, y, height, width and the text) of the link label.
const linkLabels = rootDescendents
const linkLabels = rootDescendants
.slice(1)
.filter(
(d: HierarchyPointNode<ITreeNode>) => d.data.nodeState.onSelectedPath
)
.map((d: HierarchyPointNode<ITreeNode>) => {
const labelX = d!.x! + (d!.parent!.x! - d!.x!) * 0.5;
const labelY = 4 + d!.y! + (d!.parent!.y! - d!.y!) * 0.5;
let bb: DOMRect;
try {
bb = this.getTextBB(d!.data!.condition!, classNames);
} catch {
bb = new DOMRect(1, 1, 1, 1);
}
return {
const labelX = d.x + (d.parent?.x ? (d.parent.x - d.x) * 0.5 : 0);
const labelY = 4 + d.y + (d.parent?.x ? (d.parent.y - d.y) * 0.5 : 0);
const bb: DOMRect =
this.getTextBB(d.data.condition, classNames) ||
new DOMRect(1, 1, 1, 1);
const element = {
bbHeight: bb.height + labelPaddingY,
bbWidth: bb.width + labelPaddingX,
bbX: 0.5 * (bb.width + labelPaddingX),
bbY: 0.5 * (bb.height + labelPaddingY) + labelYOffset,
id: `linkLabel${d!.id!}`,
bbX: -0.5 * (bb.width + labelPaddingX),
bbY: -0.5 * (bb.height + labelPaddingY) - labelYOffset,
id: `linkLabel${d.id}`,
style: {
display: d.data.nodeState.onSelectedPath ? undefined : "none",
transform: `translate(${labelX}px, ${labelY}px)`
},
text: `${d!.data!.condition!}`
text: d.data.condition
};
if (labelX + element.bbX < pathMin) {
pathMin = labelX + element.bbX;
}
if (labelX + element.bbX + element.bbWidth > pathMax) {
pathMax = labelX + element.bbX + element.bbWidth;
}
return element;
});
// GENERATES THE ACTUAL NODE COMPONENTS AND THEIR INTERACTIONS
// -------------------------------------------------------------------
// The code below generates the circular nodes in the tree view.
const nodeData: Array<HierarchyPointNode<ITreeNode>> = rootDescendents.map(
const nodeData: Array<HierarchyPointNode<ITreeNode>> = rootDescendants.map(
(d: HierarchyPointNode<ITreeNode>): HierarchyPointNode<ITreeNode> => {
let selectedStyle: Record<string, number | string> = {
let selectedStyle: Record<string, number | string | undefined> = {
fill: d.data.errorColor.fill
};
if (d.data.nodeState.onSelectedPath!) {
if (d.data.nodeState.onSelectedPath) {
selectedStyle = { fill: d.data.errorColor.fill, strokeWidth: 3 };
}
@ -220,19 +230,44 @@ export class TreeViewRenderer extends React.PureComponent<
isSelectedLeaf: d.data.nodeState.isSelectedLeaf,
onSelectedPath: d.data.nodeState.onSelectedPath,
style: {
transform: `translate(${d!.x!}px, ${d!.y!}px)`
transform: `translate(${d.x}px, ${d.y}px)`
}
};
return d;
}
);
const x = rootDescendants.map((d) => d.x);
const y = rootDescendants.map((d) => d.y);
const minX = Math.min(Math.min(...x) - 40, pathMin);
//100:tooltip width
const maxX = Math.max(Math.max(...x) + 40 + 100, pathMax);
const minY = Math.min(...y) - 40;
//40:tooltip height
const maxY = Math.max(...y) + 40 + 40;
console.log(minY, maxY);
const containerStyles = mergeStyles({
transform: `translate(${-minX}px, ${-minY}px)`
});
const nodeDetail = this.state.nodeDetail;
const minPct = this.state.rootLocalError * errorRatioThreshold * 100;
const svgWidth = maxX - minX;
const svgHeight = maxY - minY;
return (
<div className={classNames.mainFrame} id="mainFrame">
<div className={classNames.innerFrame}>
<div className={classNames.legend}>
<Stack tokens={{ childrenGap: "l1", padding: "l1" }}>
<Stack.Item>
<ExpandableText
expandedText={
localization.ErrorAnalysis.TreeView.treeDescriptionExpanded
}
iconName="Info"
variant={"smallPlus"}
>
{localization.ErrorAnalysis.TreeView.treeDescription}
</ExpandableText>
</Stack.Item>
<Stack.Item>
<Stack horizontal className={classNames.svgContainer}>
<TreeLegend
selectedCohort={this.props.selectedCohort}
baseCohort={this.props.baseCohort}
@ -240,76 +275,48 @@ export class TreeViewRenderer extends React.PureComponent<
minPct={minPct}
max={max}
/>
</div>
<svg
ref={svgOuterFrame}
className={classNames.svgOuterFrame}
id="svgOuterFrame"
viewBox="0 0 952 1100"
>
<mask id="Mask">
<rect
className="nodeMask"
x="-26"
y="-26"
width="52"
height="52"
fill="white"
/>
</mask>
<g ref={treeZoomPane} className="treeZoomPane">
{/* Tree */}
<TransitionGroup
component="g"
className={classNames.linksTransitionGroup}
>
{links.map((link) => (
<CSSTransition
key={link.id}
in={true}
timeout={200}
className="links"
>
<svg
ref={svgOuterFrame}
className={classNames.svgOuterFrame}
id="svgOuterFrame"
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
style={{
minWidth: svgWidth,
width: svgWidth * 1.5
}}
>
<g className={containerStyles}>
<g>
{links.map((link) => (
<path
key={link.id}
id={link.id}
d={link.d}
style={link.style}
/>
</CSSTransition>
))}
</TransitionGroup>
<g className={classNames.nodesGroup}>
{nodeData.map((node, index) => {
return (
<TreeViewNode
key={index}
node={node}
onSelect={this.onSelectNode}
/>
);
})}
</g>
<TransitionGroup
component="g"
className={classNames.linkLabelsTransitionGroup}
>
{linkLabels.map((linkLabel) => (
<CSSTransition
key={linkLabel.id}
in={true}
timeout={200}
className="linkLabels"
>
))}
</g>
<g>
{nodeData.map((node, index) => {
return (
<TreeViewNode
key={index}
node={node}
onSelect={this.onSelectNode}
/>
);
})}
</g>
<g>
{linkLabels.map((linkLabel) => (
<g
key={linkLabel.id}
style={linkLabel.style}
pointerEvents="none"
>
<rect
x={-linkLabel.bbX}
y={-linkLabel.bbY}
x={linkLabel.bbX}
y={linkLabel.bbY}
width={linkLabel.bbWidth}
height={linkLabel.bbHeight}
fill="white"
@ -326,35 +333,16 @@ export class TreeViewRenderer extends React.PureComponent<
{linkLabel.text}
</text>
</g>
</CSSTransition>
))}
</TransitionGroup>
</g>
</svg>
</div>
</div>
))}
</g>
</g>
</svg>
</Stack>
</Stack.Item>
</Stack>
);
}
public componentDidMount(): void {
window.addEventListener("resize", this.onResize.bind(this));
if (
!this.state.treeNodes ||
this.state.treeNodes.length === 0 ||
!this.state.treeNodes[0]
) {
this.fetchTreeNodes();
} else {
this.onResize();
this.forceUpdate();
}
}
public componentWillUnmount(): void {
window.removeEventListener("resize", this.onResize.bind(this));
this.props.setTreeViewState(this.state);
}
private calculateFilterProps(
node: IRequestNode,
rootErrorSize: number
@ -381,34 +369,10 @@ export class TreeViewRenderer extends React.PureComponent<
return cohortStats;
}
private resizeFunc = (
state: Readonly<ITreeViewRendererState>
): ITreeViewRendererState => {
const height = 300;
const width = 800;
// if (document.querySelector("#mainFrame")) {
// height = document.querySelector("#mainFrame")!.clientHeight;
// width = document.querySelector("#mainFrame")!.clientWidth;
// }
return {
nodeDetail: state.nodeDetail,
request: state.request,
root: state.root,
rootErrorSize: state.rootErrorSize,
rootLocalError: state.rootLocalError,
rootSize: state.rootSize,
selectedNode: state.selectedNode,
transform: state.transform,
treeNodes: state.treeNodes,
viewerHeight: height,
viewerWidth: width
};
private onResize = (): void => {
this.setState({});
};
private onResize(): void {
this.setState(this.resizeFunc);
}
private reloadData(requestTreeNodes: IRequestNode[]): void {
const reloadDataFunc = (
state: Readonly<ITreeViewRendererState>
@ -452,10 +416,12 @@ export class TreeViewRenderer extends React.PureComponent<
const calcMaskShift = globalErrorPerc * 52;
const filterProps = this.calculateFilterProps(node, rootErrorSize);
let heatmapStyle: { fill: string } = { fill: errorAvgColor };
let heatmapStyle: { fill: string | undefined } = {
fill: errorAvgColor
};
if (node.error / node.size > rootLocalError * errorRatioThreshold) {
heatmapStyle = { fill: colorgrad(localErrorPerc)! };
heatmapStyle = { fill: colorgrad(localErrorPerc) };
}
return {
@ -494,7 +460,7 @@ export class TreeViewRenderer extends React.PureComponent<
);
const tempRoot = d3stratify()(treeNodes);
const treemap = d3tree().size([state.viewerWidth, state.viewerHeight]);
const treemap = d3tree().size([viewerWidth, viewerHeight]);
const root = treemap(tempRoot) as HierarchyPointNode<ITreeNode>;
const selectedNode = state.selectedNode;
@ -519,9 +485,7 @@ export class TreeViewRenderer extends React.PureComponent<
rootSize,
selectedNode: root,
transform: state.transform,
treeNodes,
viewerHeight: state.viewerHeight,
viewerWidth: state.viewerWidth
treeNodes
};
};
this.setState(reloadDataFunc);
@ -543,7 +507,7 @@ export class TreeViewRenderer extends React.PureComponent<
private getTextBB(
labelText: string,
classNames: IProcessedStyleSet<ITreeViewRendererStyles>
): DOMRect {
): DOMRect | undefined {
const temp = select(svgOuterFrame.current).append("g");
temp.selectAll("*").remove();
temp
@ -551,7 +515,7 @@ export class TreeViewRenderer extends React.PureComponent<
.attr("className", classNames.linkLabel)
.text(`${labelText}`);
const bb = temp!.node()!.getBBox();
const bb = temp.node()?.getBBox();
temp.selectAll("*").remove();
return bb;
}
@ -560,7 +524,7 @@ export class TreeViewRenderer extends React.PureComponent<
if (!d) {
return;
}
d.data.nodeState!.onSelectedPath = true;
d.data.nodeState.onSelectedPath = true;
this.selectParentNodes(d.parent);
}
@ -568,9 +532,9 @@ export class TreeViewRenderer extends React.PureComponent<
if (!d) {
return;
}
d.data.nodeState!.onSelectedPath = false;
d.data.nodeState!.isSelectedLeaf = false;
this.unselectParentNodes(d.parent!);
d.data.nodeState.onSelectedPath = false;
d.data.nodeState.isSelectedLeaf = false;
this.unselectParentNodes(d.parent);
}
private getFilters(d: HierarchyPointNode<ITreeNode>): IFilter[] {
@ -588,7 +552,7 @@ export class TreeViewRenderer extends React.PureComponent<
column: d.parent.data.nodeName,
method: d.data.method as FilterMethods
};
return [filter, ...this.getFilters(d.parent!)];
return [filter, ...this.getFilters(d.parent)];
}
private onSelectNode = (node: HierarchyPointNode<ITreeNode>): void => {
@ -623,9 +587,7 @@ export class TreeViewRenderer extends React.PureComponent<
rootSize: state.rootSize,
selectedNode: node,
transform: state.transform,
treeNodes: state.treeNodes,
viewerHeight: state.viewerHeight,
viewerWidth: state.viewerWidth
treeNodes: state.treeNodes
};
};
@ -659,7 +621,7 @@ export class TreeViewRenderer extends React.PureComponent<
// Use set timeout as reloadData state update needs to be done outside constructor similar to fetch call
this.onResize();
this.forceUpdate();
this.reloadData(this.props.staticTreeNodes.data as IRequestNode[]);
this.reloadData(this.props.staticTreeNodes.data);
}
return;
}
@ -683,20 +645,7 @@ export class TreeViewRenderer extends React.PureComponent<
.then((result) => {
this.onResize();
this.forceUpdate();
this.reloadData(result as IRequestNode[]);
this.reloadData(result);
});
}
private zoomed(zoomEvent: D3ZoomEvent<any, ISVGDatum>): void {
const newTransform: any = zoomEvent.transform;
select(treeZoomPane.current).attr("transform", newTransform);
if (
this.state.transform === undefined ||
newTransform.x !== this.state.transform.x ||
newTransform.y !== this.state.transform.y ||
newTransform.r !== this.state.transform.r
) {
this.setState({ transform: newTransform });
}
}
}

Просмотреть файл

@ -67,7 +67,7 @@ export class WhatIf extends React.Component<IWhatIfProps, IWhatIfState> {
};
}
public componentDidMount() {
public componentDidMount(): void {
this.createCopyOfFirstRow();
this.buildRowOptions();

Просмотреть файл

@ -25,7 +25,6 @@ import {
CohortStats,
ICompositeFilter,
IFilter,
IGenericChartProps,
WeightVectorOption,
EditCohort,
ShiftCohort
@ -92,11 +91,6 @@ export class ErrorAnalysisDashboard extends React.PureComponent<
public constructor(props: IErrorAnalysisDashboardProps) {
super(props);
this.onModelConfigChanged = this.onModelConfigChanged.bind(this);
this.onConfigChanged = this.onConfigChanged.bind(this);
this.onWhatIfConfigChanged = this.onWhatIfConfigChanged.bind(this);
this.onDependenceChange = this.onDependenceChange.bind(this);
this.handleGlobalTabClick = this.handleGlobalTabClick.bind(this);
this.setSortVector = this.setSortVector.bind(this);
if (this.props.locale) {
localization.setLanguage(this.props.locale);
@ -507,27 +501,11 @@ export class ErrorAnalysisDashboard extends React.PureComponent<
selectedCohort={this.state.selectedCohort}
baseCohort={this.state.baseCohort}
treeViewState={this.state.treeViewState}
setTreeViewState={(
treeViewState: ITreeViewRendererState
) => {
if (this.state.selectedCohort !== this.state.baseCohort) {
this.setState({ treeViewState });
}
}}
setTreeViewState={this.setTreeViewState}
matrixAreaState={this.state.matrixAreaState}
matrixFilterState={this.state.matrixFilterState}
setMatrixAreaState={(matrixAreaState: IMatrixAreaState) => {
if (this.state.selectedCohort !== this.state.baseCohort) {
this.setState({ matrixAreaState });
}
}}
setMatrixFilterState={(
matrixFilterState: IMatrixFilterState
) => {
if (this.state.selectedCohort !== this.state.baseCohort) {
this.setState({ matrixFilterState });
}
}}
setMatrixAreaState={this.setMatrixAreaState}
setMatrixFilterState={this.setMatrixFilterState}
/>
)}
{this.state.viewType === ViewTypeKeys.ExplanationView && (
@ -536,7 +514,7 @@ export class ErrorAnalysisDashboard extends React.PureComponent<
>
<Pivot
selectedKey={this.state.activeGlobalTab}
onLinkClick={this.handleGlobalTabClick.bind(this)}
onLinkClick={this.handleGlobalTabClick}
linkSize={PivotLinkSize.normal}
headersOnly={true}
styles={{ root: classNames.pivotLabelWrapper }}
@ -592,9 +570,7 @@ export class ErrorAnalysisDashboard extends React.PureComponent<
}}
customPoints={this.state.customPoints}
selectedCohort={this.state.selectedCohort}
setWhatIfDatapoint={(index: number) =>
this.setState({ selectedWhatIfIndex: index })
}
setWhatIfDatapoint={this.setWhatIfDatapoint}
/>
)}
</div>
@ -641,7 +617,7 @@ export class ErrorAnalysisDashboard extends React.PureComponent<
currentCohort={this.state.selectedCohort}
invokeModel={this.props.requestPredictions}
customPoints={this.state.customPoints}
addCustomPoint={this.addCustomPoint.bind(this)}
addCustomPoint={this.addCustomPoint}
selectedIndex={this.state.selectedWhatIfIndex}
/>
</Layer>
@ -659,14 +635,33 @@ export class ErrorAnalysisDashboard extends React.PureComponent<
</ModelAssessmentContext.Provider>
);
}
private setTreeViewState = (treeViewState: ITreeViewRendererState): void => {
if (this.state.selectedCohort !== this.state.baseCohort) {
this.setState({ treeViewState });
}
};
private setMatrixAreaState = (matrixAreaState: IMatrixAreaState): void => {
if (this.state.selectedCohort !== this.state.baseCohort) {
this.setState({ matrixAreaState });
}
};
private setMatrixFilterState = (
matrixFilterState: IMatrixFilterState
): void => {
if (this.state.selectedCohort !== this.state.baseCohort) {
this.setState({ matrixFilterState });
}
};
private setWhatIfDatapoint = (index: number): void =>
this.setState({ selectedWhatIfIndex: index });
private addCustomPoint(temporaryPoint: { [key: string]: any }): void {
private addCustomPoint = (temporaryPoint: { [key: string]: any }): void => {
this.setState({
customPoints: [...this.state.customPoints, temporaryPoint],
openWhatIf: false,
predictionTab: PredictionTabKeys.WhatIfDatapointsTab
});
}
};
private updateSelectedCohort(
filters: IFilter[],
@ -717,25 +712,9 @@ export class ErrorAnalysisDashboard extends React.PureComponent<
});
}
private onConfigChanged(newConfig: IGenericChartProps): void {
this.setState({ dataChartConfig: newConfig });
}
private onModelConfigChanged(newConfig: IGenericChartProps): void {
this.setState({ modelChartConfig: newConfig });
}
private onWhatIfConfigChanged(newConfig: IGenericChartProps): void {
this.setState({ whatIfChartConfig: newConfig });
}
private onDependenceChange(newConfig: IGenericChartProps): void {
this.setState({ dependenceProps: newConfig });
}
private handleGlobalTabClick(item: PivotItem | undefined): void {
if (item !== undefined) {
const itemKey: string = item.props.itemKey!;
private handleGlobalTabClick = (item: PivotItem | undefined): void => {
if (item?.props.itemKey) {
const itemKey: string = item.props.itemKey;
const index: GlobalTabKeys = GlobalTabKeys[itemKey];
const predictionTab = PredictionTabKeys.CorrectPredictionTab;
this.setState({
@ -744,7 +723,7 @@ export class ErrorAnalysisDashboard extends React.PureComponent<
predictionTab
});
}
}
};
private setSortVector(): void {
this.setState({

Просмотреть файл

@ -24,6 +24,22 @@ import { IStringsParam } from "./IStringsParam";
* @property {number[][] | number[]} [probabilityY] - model probabilities for output values. Dim(rows) x [Dim(classes)]
*/
// Represents the data retrieved from the backend
export interface IRequestNode {
arg: number;
condition: string;
error: number;
id: string;
method: string;
nodeIndex: number;
nodeName: string;
parentId: string;
parentNodeName: string;
pathFromRoot: string;
size: number;
sourceRowKeyHash: string;
success: number;
}
export interface IErrorAnalysisDashboardProps
extends IExplanationDashboardData,
IOfficeFabricProps {

Просмотреть файл

@ -6,7 +6,7 @@ import { HierarchyPointNode } from "d3-hierarchy";
import { FilterProps } from "./FilterProps";
export interface IErrorColorStyle {
fill: string;
fill: string | undefined;
}
export interface IShowSelectedStyle {
@ -32,8 +32,6 @@ export interface INodeDetail {
export interface ITreeViewRendererState {
request?: AbortController;
nodeDetail: INodeDetail;
viewerWidth: number;
viewerHeight: number;
selectedNode: any;
transform: any;
treeNodes: any[];
@ -74,7 +72,7 @@ export interface IFillStyleUp {
// Contains node state that changes with UI clicks
export interface INodeState {
errorStyle: Record<string, number | string> | undefined;
errorStyle: Record<string, number | string | undefined> | undefined;
onSelectedPath: boolean;
isSelectedLeaf: boolean;
style: ITransform | undefined;
@ -106,8 +104,6 @@ export function createInitialTreeViewState(): ITreeViewRendererState {
rootSize: 0,
selectedNode: undefined,
transform: undefined,
treeNodes: [],
viewerHeight: 0,
viewerWidth: 0
treeNodes: []
};
}

Просмотреть файл

@ -88,7 +88,7 @@ export function constructCols(
columns.push({
fieldName: `${index}`,
isResizable: true,
key: `color`,
key: "color",
maxWidth: 100,
minWidth: 50,
name: "Color"
@ -137,8 +137,8 @@ function pushRowData(
jointDataset: JointDataset,
row: { [key: string]: number }
): void {
if (jointDataset.metaDict[property].isCategorical) {
const categories = jointDataset.metaDict[property].sortedCategoricalValues!;
const categories = jointDataset.metaDict[property].sortedCategoricalValues;
if (jointDataset.metaDict[property].isCategorical && categories) {
tableRow.push(categories[row[property]]);
} else {
tableRow.push(row[property]);

Просмотреть файл

@ -98,7 +98,7 @@ export class CohortEditor extends React.PureComponent<
return (
<>
<Panel
onOuterClick={() => {
onOuterClick={(): number => {
return 0;
}} // https://github.com/microsoft/fluentui/issues/6476
id="cohortEditPanel"
@ -211,11 +211,11 @@ export class CohortEditor extends React.PureComponent<
this.props.onDelete();
};
private readonly onCancelClick = () => {
private readonly onCancelClick = (): void => {
this.setState({ showConfirmation: true });
};
private readonly onCancelConfirm = async () => {
private readonly onCancelConfirm = async (): Promise<void> => {
const callback = this.props.isNewCohort
? this.props.closeCohortEditorPanel
: this.props.closeCohortEditor;
@ -223,7 +223,7 @@ export class CohortEditor extends React.PureComponent<
this.setState({ showConfirmation: false });
};
private readonly onCancelClose = () => {
private readonly onCancelClose = (): void => {
this.setState({ showConfirmation: false });
};

Просмотреть файл

@ -1,370 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import {
ExpandableText,
JointDataset,
ColumnCategories,
cohortKey,
ChartTypes,
IGenericChartProps,
ISelectorConfig,
MissingParametersPlaceholder,
defaultModelAssessmentContext,
ModelAssessmentContext
} from "@responsible-ai/core-ui";
import { localization } from "@responsible-ai/localization";
import { AccessibleChart } from "@responsible-ai/mlchartlib";
import _ from "lodash";
import {
getTheme,
IDropdownOption,
Dropdown,
DefaultButton,
Text,
IChoiceGroupOption
} from "office-ui-fabric-react";
import React from "react";
import { newExplanationDashboardRowErrorSize } from "../../newExplanationDashboardRowErrorSize";
import { AxisConfigDialog } from "../AxisConfigurationDialog/AxisConfigDialog";
import { datasetExplorerTabStyles } from "./DatasetExplorerTab.styles";
import { generatePlotlyProps } from "./generatePlotlyProps";
import { SidePanel } from "./SidePanel";
export interface IDatasetExplorerTabProps {
initialCohortIndex?: number;
}
export interface IDatasetExplorerTabState {
xDialogOpen: boolean;
yDialogOpen: boolean;
colorDialogOpen: boolean;
selectedCohortIndex: number;
calloutVisible: boolean;
chartProps?: IGenericChartProps;
}
export class DatasetExplorerTab extends React.PureComponent<
IDatasetExplorerTabProps,
IDatasetExplorerTabState
> {
public static contextType = ModelAssessmentContext;
public context: React.ContextType<
typeof ModelAssessmentContext
> = defaultModelAssessmentContext;
private readonly chartAndConfigsId = "DatasetExplorerChart";
public constructor(props: IDatasetExplorerTabProps) {
super(props);
let initialCohortIndex = 0;
if (this.props.initialCohortIndex !== undefined) {
initialCohortIndex = this.props.initialCohortIndex;
}
this.state = {
calloutVisible: false,
colorDialogOpen: false,
selectedCohortIndex: initialCohortIndex,
xDialogOpen: false,
yDialogOpen: false
};
}
public componentDidMount() {
this.setState({ chartProps: this.generateDefaultChartAxes() });
}
public render(): React.ReactNode {
const classNames = datasetExplorerTabStyles();
if (!this.context.jointDataset.hasDataset) {
return (
<MissingParametersPlaceholder>
{localization.Interpret.DatasetExplorer.missingParameters}
</MissingParametersPlaceholder>
);
}
if (this.state.chartProps === undefined) {
return <div />;
}
const plotlyProps = generatePlotlyProps(
this.context.jointDataset,
this.state.chartProps,
this.context.errorCohorts.map((errorCohort) => errorCohort.cohort)[
this.state.selectedCohortIndex
]
);
const cohortOptions =
this.state.chartProps.xAxis.property !== cohortKey
? this.context.errorCohorts.map((errorCohort, index) => {
return { key: index, text: errorCohort.cohort.name };
})
: undefined;
const cohortLength = this.context.errorCohorts[
this.state.selectedCohortIndex
].cohort.filteredData.length;
const canRenderChart =
cohortLength < newExplanationDashboardRowErrorSize ||
this.state.chartProps.chartType !== ChartTypes.Scatter;
const yAxisCategories = [
ColumnCategories.Index,
ColumnCategories.Dataset,
ColumnCategories.Outcome
];
if (this.state.chartProps.chartType !== ChartTypes.Scatter) {
yAxisCategories.push(ColumnCategories.None);
}
return (
<div className={classNames.page}>
<div className={classNames.infoWithText}>
<ExpandableText
expandedText={localization.Interpret.DatasetExplorer.helperText}
iconName="Info"
>
{localization.Interpret.DatasetExplorer.collapsedHelperText}
</ExpandableText>
</div>
<div className={classNames.cohortPickerWrapper}>
<Text variant="mediumPlus" className={classNames.cohortPickerLabel}>
{localization.Interpret.ModelPerformance.cohortPickerLabel}
</Text>
{cohortOptions && (
<Dropdown
styles={{ dropdown: { width: 150 } }}
options={cohortOptions}
selectedKey={this.state.selectedCohortIndex}
onChange={this.setSelectedCohort}
/>
)}
</div>
<div className={classNames.mainArea} id={this.chartAndConfigsId}>
<div className={classNames.chartWithAxes}>
{this.state.yDialogOpen && (
<AxisConfigDialog
jointDataset={this.context.jointDataset}
orderedGroupTitles={yAxisCategories}
selectedColumn={this.state.chartProps.yAxis}
canBin={false}
mustBin={false}
canDither={
this.state.chartProps.chartType === ChartTypes.Scatter
}
onAccept={this.onYSet}
onCancel={this.setYOpen.bind(this, false)}
/>
)}
{this.state.xDialogOpen && (
<AxisConfigDialog
jointDataset={this.context.jointDataset}
orderedGroupTitles={[
ColumnCategories.Index,
ColumnCategories.Dataset,
ColumnCategories.Outcome
]}
selectedColumn={this.state.chartProps.xAxis}
canBin={
this.state.chartProps.chartType === ChartTypes.Histogram ||
this.state.chartProps.chartType === ChartTypes.Box
}
mustBin={
this.state.chartProps.chartType === ChartTypes.Histogram ||
this.state.chartProps.chartType === ChartTypes.Box
}
canDither={
this.state.chartProps.chartType === ChartTypes.Scatter
}
onAccept={this.onXSet}
onCancel={this.setXOpen.bind(this, false)}
/>
)}
{this.state.colorDialogOpen && this.state.chartProps.colorAxis && (
<AxisConfigDialog
jointDataset={this.context.jointDataset}
orderedGroupTitles={[
ColumnCategories.Index,
ColumnCategories.Dataset,
ColumnCategories.Outcome
]}
selectedColumn={this.state.chartProps.colorAxis}
canBin={true}
mustBin={false}
canDither={false}
onAccept={this.onColorSet}
onCancel={this.setColorClose}
/>
)}
<div className={classNames.chartWithVertical}>
<div className={classNames.verticalAxis}>
<div className={classNames.rotatedVerticalBox}>
<div>
<DefaultButton
onClick={this.setYOpen.bind(this, true)}
text={
this.context.jointDataset.metaDict[
this.state.chartProps.yAxis.property
].abbridgedLabel
}
title={
this.context.jointDataset.metaDict[
this.state.chartProps.yAxis.property
].label
}
/>
</div>
</div>
</div>
{canRenderChart ? (
<AccessibleChart plotlyProps={plotlyProps} theme={getTheme()} />
) : (
<MissingParametersPlaceholder>
{localization.Interpret.ValidationErrors.datasizeError}
</MissingParametersPlaceholder>
)}
</div>
<div className={classNames.horizontalAxisWithPadding}>
<div className={classNames.paddingDiv}></div>
<div className={classNames.horizontalAxis}>
<div>
<DefaultButton
onClick={this.setXOpen.bind(this, true)}
text={
this.context.jointDataset.metaDict[
this.state.chartProps.xAxis.property
].abbridgedLabel
}
title={
this.context.jointDataset.metaDict[
this.state.chartProps.xAxis.property
].label
}
/>
</div>
</div>
</div>
</div>
<SidePanel
chartProps={this.state.chartProps}
cohorts={this.context.errorCohorts.map(
(errorCohort) => errorCohort.cohort
)}
jointDataset={this.context.jointDataset}
selectedCohortIndex={this.state.selectedCohortIndex}
setColorOpen={this.setColorOpen}
onChartTypeChange={this.onChartTypeChange}
/>
</div>
</div>
);
}
private setSelectedCohort = (
_: React.FormEvent<HTMLDivElement>,
item?: IDropdownOption
): void => {
if (item?.key !== undefined) {
this.setState({ selectedCohortIndex: item.key as number });
}
};
private onChartTypeChange = (
_ev?: React.SyntheticEvent<HTMLElement>,
item?: IChoiceGroupOption
): void => {
const newProps = _.cloneDeep(this.state.chartProps);
if (item?.key === undefined || !newProps) {
return;
}
newProps.chartType = item.key as ChartTypes;
if (newProps.yAxis.property === ColumnCategories.None) {
newProps.yAxis = this.generateDefaultYAxis();
}
this.setState({ chartProps: newProps });
};
private readonly setXOpen = (val: boolean): void => {
if (val && this.state.xDialogOpen === false) {
this.setState({ xDialogOpen: true });
return;
}
this.setState({ xDialogOpen: false });
};
private readonly setColorOpen = (): void => {
this.setState({ colorDialogOpen: true });
};
private readonly setColorClose = (): void => {
this.setState({ colorDialogOpen: false });
};
private readonly setYOpen = (val: boolean): void => {
if (val && this.state.yDialogOpen === false) {
this.setState({ yDialogOpen: true });
return;
}
this.setState({ yDialogOpen: false });
};
private onXSet = (value: ISelectorConfig): void => {
if (!this.state.chartProps) {
return;
}
const newProps = _.cloneDeep(this.state.chartProps);
newProps.xAxis = value;
this.setState({ chartProps: newProps, xDialogOpen: false });
};
private onYSet = (value: ISelectorConfig): void => {
if (!this.state.chartProps) {
return;
}
const newProps = _.cloneDeep(this.state.chartProps);
newProps.yAxis = value;
this.setState({ chartProps: newProps, yDialogOpen: false });
};
private onColorSet = (value: ISelectorConfig): void => {
if (!this.state.chartProps) {
return;
}
const newProps = _.cloneDeep(this.state.chartProps);
newProps.colorAxis = value;
this.setState({ chartProps: newProps, colorDialogOpen: false });
};
private generateDefaultChartAxes(): IGenericChartProps | undefined {
if (!this.context.jointDataset.hasDataset) {
return;
}
const chartProps: IGenericChartProps = {
chartType: ChartTypes.Histogram,
colorAxis: {
options: {},
property: this.context.jointDataset.hasPredictedY
? JointDataset.PredictedYLabel
: JointDataset.IndexLabel
},
xAxis: {
options: {},
property: JointDataset.IndexLabel
},
yAxis: this.generateDefaultYAxis()
};
return chartProps;
}
private generateDefaultYAxis(): ISelectorConfig {
const yKey = JointDataset.DataLabelRoot + "0";
const yIsDithered = this.context.jointDataset.metaDict[yKey]
.treatAsCategorical;
return {
options: {
bin: false,
dither: yIsDithered
},
property: yKey
};
}
}

Просмотреть файл

@ -26,6 +26,7 @@ import {
IDropdownOption,
Slider
} from "office-ui-fabric-react";
import Plotly from "plotly.js";
import React from "react";
import { LoadingSpinner } from "../../SharedComponents/LoadingSpinner";

Просмотреть файл

@ -11,6 +11,7 @@ import { localization } from "@responsible-ai/localization";
import { IPlotlyProperty, AccessibleChart } from "@responsible-ai/mlchartlib";
import _ from "lodash";
import { getTheme, Text } from "office-ui-fabric-react";
import Plotly from "plotly.js";
import React from "react";
import { LoadingSpinner } from "../../SharedComponents/LoadingSpinner";
@ -165,7 +166,9 @@ export class FeatureImportanceBar extends React.PureComponent<
const xText = sortedIndexVector.map((i) => this.props.unsortedX[i]);
const xOriginText = sortedIndexVector.map((i) => {
if (this.props.originX) return this.props.originX[i];
if (this.props.originX) {
return this.props.originX[i];
}
return this.props.unsortedX[i];
});
if (this.props.chartType === ChartTypes.Bar) {

Просмотреть файл

@ -110,7 +110,7 @@ export class GlobalExplanationTab extends React.PureComponent<
};
}
public componentDidMount() {
public componentDidMount(): void {
if (!this.context.jointDataset.hasLocalExplanations) {
return;
}

Просмотреть файл

@ -64,7 +64,7 @@ export class ModelPerformanceTab extends React.PureComponent<
};
}
public componentDidMount() {
public componentDidMount(): void {
this.setState({ chartProps: this.generateDefaultChartAxes() });
}

Просмотреть файл

@ -354,16 +354,18 @@ export class ScatterUtils {
PlotlyUtils.setColorProperty(props, colorOption, modelData, colorbarTitle);
props.data[0].yAccessor = yAccessor;
props.data[0].xAccessor = xAccessor;
props.data[0].datapointLevelAccessors!["text"].path = [
xAccessor,
yAccessor,
colorAccessor
];
props.data[0].datapointLevelAccessors!["text"].mapArgs = [
localization.Interpret.ExplanationScatter.index,
modelData.featureNames[maxIndex],
localization.Interpret.ExplanationScatter.predictedY
];
if (props.data[0].datapointLevelAccessors) {
props.data[0].datapointLevelAccessors["text"].path = [
xAccessor,
yAccessor,
colorAccessor
];
props.data[0].datapointLevelAccessors["text"].mapArgs = [
localization.Interpret.ExplanationScatter.index,
modelData.featureNames[maxIndex],
localization.Interpret.ExplanationScatter.predictedY
];
}
_.set(
props,
@ -417,25 +419,27 @@ export class ScatterUtils {
PlotlyUtils.setColorProperty(props, colorOption, modelData, colorbarTitle);
props.data[0].xAccessor = xAccessor;
props.data[0].yAccessor = yAccessor;
props.data[0].datapointLevelAccessors!["text"].path = [
xAccessor,
yAccessor,
colorAccessor
];
props.data[0].datapointLevelAccessors!["text"].mapArgs = [
localization.formatString(
localization.Interpret.ExplanationScatter.dataLabel,
modelData.featureNames[maxIndex]
),
localization.formatString(
localization.Interpret.ExplanationScatter.importanceLabel,
modelData.featureNames[maxIndex]
),
localization.formatString(
localization.Interpret.ExplanationScatter.dataLabel,
modelData.featureNames[secondIndex]
)
];
if (props.data[0].datapointLevelAccessors) {
props.data[0].datapointLevelAccessors["text"].path = [
xAccessor,
yAccessor,
colorAccessor
];
props.data[0].datapointLevelAccessors["text"].mapArgs = [
localization.formatString(
localization.Interpret.ExplanationScatter.dataLabel,
modelData.featureNames[maxIndex]
),
localization.formatString(
localization.Interpret.ExplanationScatter.importanceLabel,
modelData.featureNames[maxIndex]
),
localization.formatString(
localization.Interpret.ExplanationScatter.dataLabel,
modelData.featureNames[secondIndex]
)
];
}
const yAxisLabel =
modelData.modelType === ModelTypes.Binary

Просмотреть файл

@ -130,7 +130,7 @@ export class WhatIfTab extends React.PureComponent<
};
}
public componentDidMount() {
public componentDidMount(): void {
this.createCopyOfFirstRow();
this.buildRowOptions(0);

Просмотреть файл

@ -3,6 +3,7 @@
import _ from "lodash";
import { ITheme } from "office-ui-fabric-react";
import Plotly from "plotly.js";
import React from "react";
import Plot from "react-plotly.js";
import { v4 } from "uuid";

Просмотреть файл

@ -19,7 +19,7 @@ export class ChartBuilder {
? ", " +
Object.keys(datum.datapointLevelAccessors)
.map((key) => {
return `${key}: [${datum.datapointLevelAccessors![key].path.join(
return `${key}: [${datum.datapointLevelAccessors?.[key].path.join(
", "
)}]`;
})
@ -121,15 +121,18 @@ export class ChartBuilder {
if (datum.sizeAccessor) {
const size =
(row.size * (datum.maxMarkerSize || 40) ** 2) / (2 * maxBubbleValue);
(series.marker!.size as number[]).push(Math.abs(size));
(series.marker?.size as number[]).push(Math.abs(size));
}
if (datum.datapointLevelAccessors !== undefined) {
Object.keys(datum.datapointLevelAccessors).forEach((key) => {
const accessor = datum.datapointLevelAccessors![key];
if (!datum.datapointLevelAccessors) {
return;
}
const accessor = datum.datapointLevelAccessors[key];
const plotlyPath = accessor.plotlyPath;
let value =
accessor.mapFunction !== undefined
? accessorMappingFunctions[accessor.mapFunction!](
? accessorMappingFunctions[accessor.mapFunction](
row[key],
datum,
accessor.mapArgs || []
@ -169,12 +172,16 @@ export class ChartBuilder {
series.y = [];
}
if (datum.sizeAccessor) {
series.marker!.size = [];
if (series.marker) {
series.marker.size = [];
}
}
if (datum.datapointLevelAccessors !== undefined) {
Object.keys(datum.datapointLevelAccessors).forEach((key) => {
const plotlyPath = datum.datapointLevelAccessors![key].plotlyPath;
_.set(series, plotlyPath, []);
const plotlyPath = datum.datapointLevelAccessors?.[key].plotlyPath;
if (plotlyPath) {
_.set(series, plotlyPath, []);
}
});
}
return series;

Просмотреть файл

@ -1,7 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { mergeStyleSets, IStyle, getTheme } from "office-ui-fabric-react";
import {
mergeStyleSets,
IStyle,
getTheme,
IProcessedStyleSet
} from "office-ui-fabric-react";
export interface IAddTabButtonStyles {
callout: IStyle;
@ -9,7 +14,9 @@ export interface IAddTabButtonStyles {
splitter: IStyle;
}
export const addTabButtonStyles = () => {
export const addTabButtonStyles = (): IProcessedStyleSet<
IAddTabButtonStyles
> => {
const theme = getTheme();
return mergeStyleSets<IAddTabButtonStyles>({
button: {

Просмотреть файл

@ -101,19 +101,23 @@ export class AddTabButton extends React.Component<
private onChange = (
_: React.FormEvent<HTMLDivElement>,
option?: IDropdownOption | undefined
) => {
if (!option) return;
): void => {
if (!option) {
return;
}
this.setState({ tabSelected: option.key as GlobalTabKeys });
};
private addTab = () => {
if (!this.state.tabSelected) return;
private addTab = (): void => {
if (!this.state.tabSelected) {
return;
}
this.props.onAdd(this.props.tabIndex, this.state.tabSelected);
this.setState({
isCalloutVisible: false
});
};
private toggleIsCalloutVisible = () => {
private toggleIsCalloutVisible = (): void => {
this.setState((prev) => ({
isCalloutVisible: !prev.isCalloutVisible,
tabSelected: undefined

Просмотреть файл

@ -18,7 +18,7 @@ export class DashboardSettingDeleteButton extends React.PureComponent<
);
}
private removeTab = () => {
private removeTab = (): void => {
this.props.removeTab(this.props.index);
};
}

Просмотреть файл

@ -70,8 +70,10 @@ export class DashboardSettings extends React.PureComponent<
private renderDelete = (
_item?: IModelAssessmentDashboardTab,
index?: number | undefined
) => {
if (!index) return React.Fragment;
): React.ReactNode => {
if (!index) {
return React.Fragment;
}
return (
<DashboardSettingDeleteButton
index={index}

Просмотреть файл

@ -55,13 +55,15 @@ export class MainMenu extends React.PureComponent<
> {
public static contextType = ModelAssessmentContext;
public context: IModelAssessmentContext = defaultModelAssessmentContext;
public componentDidMount() {
public componentDidMount(): void {
this.setState({
editedCohort: this.context.errorCohorts[0]
});
}
public render(): React.ReactNode {
if (!this.state) return React.Fragment;
if (!this.state) {
return React.Fragment;
}
const farItems: ICommandBarItemProps[] = [];
const helpItems: ICommandBarItemProps[] = [
{
@ -162,16 +164,17 @@ export class MainMenu extends React.PureComponent<
);
}
private toggleInfoPanel = () =>
private toggleInfoPanel = (): void =>
this.setState((prev) => ({ infoPanelVisible: !prev.infoPanelVisible }));
private toggleCohortListPanel = () =>
private toggleCohortListPanel = (): void =>
this.setState((prev) => ({
cohortListPanelVisible: !prev.cohortListPanelVisible
}));
private openEditCohort = (editedCohort: ErrorCohort) =>
private openEditCohort = (editedCohort: ErrorCohort): void =>
this.setState({ editCohortVisible: true, editedCohort });
private closeEditCohort = () => this.setState({ editCohortVisible: true });
private closeEditCohort = (): void =>
this.setState({ editCohortVisible: true });
private saveEditCohort = (
originalCohort: ErrorCohort,
editedCohort: ErrorCohort
@ -197,13 +200,13 @@ export class MainMenu extends React.PureComponent<
);
};
private toggleShiftCohort = () =>
private toggleShiftCohort = (): void =>
this.setState((prev) => ({ shiftCohortVisible: !prev.shiftCohortVisible }));
private toggleSaveCohort = () =>
private toggleSaveCohort = (): void =>
this.setState((prev) => ({ saveCohortVisible: !prev.saveCohortVisible }));
private toggleDashboardSettings = () =>
private toggleDashboardSettings = (): void =>
this.setState((prev) => ({
dashboardSettingsVisible: !prev.dashboardSettingsVisible
}));

Просмотреть файл

@ -21,7 +21,7 @@ export interface INavigationProps {
export class Navigation extends React.PureComponent<INavigationProps> {
private navLinkGroups: INavLinkGroup[] = [];
constructor(props: INavigationProps) {
public constructor(props: INavigationProps) {
super(props);
this.navLinkGroups.push({
@ -104,7 +104,7 @@ export class Navigation extends React.PureComponent<INavigationProps> {
}
}
function onRenderGroupHeader(group?: INavLinkGroup) {
function onRenderGroupHeader(group?: INavLinkGroup): JSX.Element {
return (
<h6 style={{ paddingLeft: "20px" }}>
{group ? group.name?.toUpperCase() : ""}
@ -112,7 +112,7 @@ function onRenderGroupHeader(group?: INavLinkGroup) {
);
}
function onRenderLink(link?: INavLink) {
function onRenderLink(link?: INavLink): JSX.Element {
const theme = getTheme();
return (
<Text

Просмотреть файл

@ -6,17 +6,19 @@ import { mergeStyleSets, IStyle } from "office-ui-fabric-react";
export interface IModelAssessmentDashboardStyles {
page: IStyle;
section: IStyle;
buttonSection: IStyle;
}
export const modelAssessmentDashboardStyles = mergeStyleSets<
IModelAssessmentDashboardStyles
>({
buttonSection: {
textAlign: "center"
},
page: {
boxSizing: "border-box",
padding: "16px 40px 0 14px",
width: "100%"
},
section: {
textAlign: "center"
}
section: {}
});

Просмотреть файл

@ -92,125 +92,145 @@ export class ModelAssessmentDashboard extends CohortBasedComponent<
<Stack>
{this.state.activeGlobalTabs[0]?.key !==
GlobalTabKeys.ErrorAnalysisTab && (
<Stack.Item className={modelAssessmentDashboardStyles.section}>
<Stack.Item
className={modelAssessmentDashboardStyles.buttonSection}
>
<AddTabButton tabIndex={0} onAdd={this.addTab} />
</Stack.Item>
)}
{this.state.activeGlobalTabs.map((t, i) => (
<Stack.Item
key={i}
className={modelAssessmentDashboardStyles.section}
>
{t.key === GlobalTabKeys.ErrorAnalysisTab && (
<ErrorAnalysisViewTab
messages={
this.props.stringParams
? this.props.stringParams.contextualHelp
: undefined
}
getTreeNodes={this.props.requestDebugML}
getMatrix={this.props.requestMatrix}
updateSelectedCohort={this.updateSelectedCohort}
features={this.props.dataset.featureNames}
selectedFeatures={this.state.selectedFeatures}
errorAnalysisOption={this.state.errorAnalysisOption}
selectedCohort={this.state.selectedCohort}
baseCohort={this.state.baseCohort}
treeViewState={this.state.treeViewState}
setTreeViewState={(
treeViewState: ITreeViewRendererState
) => {
if (this.state.selectedCohort !== this.state.baseCohort) {
this.setState({ treeViewState });
<>
<Stack.Item
key={i}
className={modelAssessmentDashboardStyles.section}
>
{t.key === GlobalTabKeys.ErrorAnalysisTab && (
<ErrorAnalysisViewTab
messages={
this.props.stringParams
? this.props.stringParams.contextualHelp
: undefined
}
}}
matrixAreaState={this.state.matrixAreaState}
matrixFilterState={this.state.matrixFilterState}
setMatrixAreaState={(matrixAreaState: IMatrixAreaState) => {
if (this.state.selectedCohort !== this.state.baseCohort) {
this.setState({ matrixAreaState });
getTreeNodes={this.props.requestDebugML}
getMatrix={this.props.requestMatrix}
updateSelectedCohort={this.updateSelectedCohort}
features={this.props.dataset.featureNames}
selectedFeatures={this.state.selectedFeatures}
errorAnalysisOption={this.state.errorAnalysisOption}
selectedCohort={this.state.selectedCohort}
baseCohort={this.state.baseCohort}
treeViewState={this.state.treeViewState}
setTreeViewState={(
treeViewState: ITreeViewRendererState
): void => {
if (
this.state.selectedCohort !== this.state.baseCohort
) {
this.setState({ treeViewState });
}
}}
matrixAreaState={this.state.matrixAreaState}
matrixFilterState={this.state.matrixFilterState}
setMatrixAreaState={(
matrixAreaState: IMatrixAreaState
): void => {
if (
this.state.selectedCohort !== this.state.baseCohort
) {
this.setState({ matrixAreaState });
}
}}
setMatrixFilterState={(
matrixFilterState: IMatrixFilterState
): void => {
if (
this.state.selectedCohort !== this.state.baseCohort
) {
this.setState({ matrixFilterState });
}
}}
stringParams={this.props.stringParams}
selectFeatures={(features: string[]): void =>
this.setState({ selectedFeatures: features })
}
}}
setMatrixFilterState={(
matrixFilterState: IMatrixFilterState
) => {
if (this.state.selectedCohort !== this.state.baseCohort) {
this.setState({ matrixFilterState });
importances={this.state.importances}
/>
)}
{t.key === GlobalTabKeys.ModelStatisticsTab && (
<ModelPerformanceTab />
)}
{t.key === GlobalTabKeys.DataExplorerTab && (
<DatasetExplorerTab showCohortSelection={false} />
)}
{t.key === GlobalTabKeys.GlobalExplanationTab && (
<GlobalExplanationTab
cohorts={this.state.cohorts.map(
(cohort) => cohort.cohort
)}
cohortIDs={cohortIDs}
selectedWeightVector={this.state.selectedWeightVector}
weightOptions={this.state.weightVectorOptions}
weightLabels={this.state.weightVectorLabels}
onWeightChange={this.onWeightVectorChange}
explanationMethod={
this.props.modelExplanationData.explanationMethod
}
}}
stringParams={this.props.stringParams}
selectFeatures={(features: string[]): void =>
this.setState({ selectedFeatures: features })
}
importances={this.state.importances}
/>
)}
{t.key === GlobalTabKeys.ModelStatisticsTab && (
<ModelPerformanceTab />
)}
{t.key === GlobalTabKeys.DataExplorerTab && (
<DatasetExplorerTab showCohortSelection={false} />
)}
{t.key === GlobalTabKeys.GlobalExplanationTab && (
<GlobalExplanationTab
cohorts={this.state.cohorts.map((cohort) => cohort.cohort)}
cohortIDs={cohortIDs}
selectedWeightVector={this.state.selectedWeightVector}
weightOptions={this.state.weightVectorOptions}
weightLabels={this.state.weightVectorLabels}
onWeightChange={this.onWeightVectorChange}
explanationMethod={
this.props.modelExplanationData.explanationMethod
}
/>
)}
{t.key === GlobalTabKeys.LocalExplanationTab && (
<InstanceView
messages={
this.props.stringParams
? this.props.stringParams.contextualHelp
: undefined
}
features={this.props.dataset.featureNames}
invokeModel={this.props.requestPredictions}
selectedWeightVector={this.state.selectedWeightVector}
weightOptions={this.state.weightVectorOptions}
weightLabels={this.state.weightVectorLabels}
onWeightChange={this.onWeightVectorChange}
activePredictionTab={this.state.predictionTab}
setActivePredictionTab={(key: PredictionTabKeys): void => {
this.setState({
predictionTab: key
});
}}
customPoints={this.state.customPoints}
selectedCohort={this.state.selectedCohort}
setWhatIfDatapoint={(index: number) =>
this.setState({ selectedWhatIfIndex: index })
}
/>
)}
{t.key === GlobalTabKeys.CausalAnalysisTab && (
<CausalInsightsTab data={this.props.casualAnalysisData} />
)}
{/*
/>
)}
{t.key === GlobalTabKeys.LocalExplanationTab && (
<InstanceView
messages={
this.props.stringParams
? this.props.stringParams.contextualHelp
: undefined
}
features={this.props.dataset.featureNames}
invokeModel={this.props.requestPredictions}
selectedWeightVector={this.state.selectedWeightVector}
weightOptions={this.state.weightVectorOptions}
weightLabels={this.state.weightVectorLabels}
onWeightChange={this.onWeightVectorChange}
activePredictionTab={this.state.predictionTab}
setActivePredictionTab={(
key: PredictionTabKeys
): void => {
this.setState({
predictionTab: key
});
}}
customPoints={this.state.customPoints}
selectedCohort={this.state.selectedCohort}
setWhatIfDatapoint={(index: number): void =>
this.setState({ selectedWhatIfIndex: index })
}
/>
)}
{t.key === GlobalTabKeys.CausalAnalysisTab && (
<CausalInsightsTab data={this.props.casualAnalysisData} />
)}
{/*
{t.key === GlobalTabKeys.CounterfactualsTab && (
<CounterfactualsTab />
)} */}
<AddTabButton tabIndex={i + 1} onAdd={this.addTab} />
</Stack.Item>
</Stack.Item>
<Stack.Item
className={modelAssessmentDashboardStyles.buttonSection}
>
<AddTabButton tabIndex={0} onAdd={this.addTab} />
</Stack.Item>
</>
))}
</Stack>
</div>
</ModelAssessmentContext.Provider>
);
}
private addTab = (index: number, tab: GlobalTabKeys) => {
private addTab = (index: number, tab: GlobalTabKeys): void => {
const tabs = [...this.state.activeGlobalTabs];
tabs.splice(index, 0, { dataCount: 0, key: tab });
this.setState({ activeGlobalTabs: tabs });
};
private removeTab = (index: number) => {
private removeTab = (index: number): void => {
const tabs = [...this.state.activeGlobalTabs];
tabs.splice(index, 1);
this.setState({ activeGlobalTabs: tabs });

Просмотреть файл

@ -10,7 +10,7 @@ import {
ITelemetryMessage,
ICasualAnalysisData
} from "@responsible-ai/core-ui";
import { IStringsParam } from "@responsible-ai/error-analysis";
import { IRequestNode, IStringsParam } from "@responsible-ai/error-analysis";
export interface IModelAssessmentDashboardProps
extends IOfficeFabricProps,
@ -30,7 +30,10 @@ export interface IModelAssessmentDashboardProps
abortSignal: AbortSignal,
explanationAlgorithm?: string
) => Promise<any[]>;
requestDebugML?: (request: any[], abortSignal: AbortSignal) => Promise<any[]>;
requestDebugML?: (
request: any[],
abortSignal: AbortSignal
) => Promise<IRequestNode[]>;
requestMatrix?: (request: any[], abortSignal: AbortSignal) => Promise<any[]>;
requestImportances?: (
request: any[],

Просмотреть файл

@ -62,7 +62,6 @@
"react-dom": "17.0.1",
"react-plotly.js": "^2.5.0",
"react-router-dom": "^5.0.0",
"react-transition-group": "^4.2.4",
"tslib": "^2.2.0",
"uuid": "^8.3.0"
},
@ -103,7 +102,6 @@
"@types/react-dom": "16.9.9",
"@types/react-plotly.js": "^2.2.4",
"@types/react-router-dom": "5.1.6",
"@types/react-transition-group": "^4.2.4",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "4.3.0",
"@typescript-eslint/parser": "4.3.0",

Просмотреть файл

@ -1888,13 +1888,6 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.8.7":
version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740"
integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.8.6":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
@ -3363,13 +3356,6 @@
"@types/history" "*"
"@types/react" "*"
"@types/react-transition-group@^4.2.4":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d"
integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==
dependencies:
"@types/react" "*"
"@types/react@*":
version "16.9.49"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872"
@ -6876,14 +6862,6 @@ dom-accessibility-api@^0.5.4:
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166"
integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==
dom-helpers@^5.0.1:
version "5.2.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b"
integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@ -14171,16 +14149,6 @@ react-test-renderer@^16.0.0-0:
react-is "^16.8.6"
scheduler "^0.19.1"
react-transition-group@^4.2.4:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"