import React from "react";
import PropTypes from "prop-types";
import { withStyles } from "@material-ui/core/styles";
import { fetchMatrix } from "../utilFunctions/fetchFunctions";
import { addSubheaders } from "../utilFunctions/parseMatrix/parseElements";
import { CenteredColumn, MultiColumns } from "../Containers";
import { FullscreenDialog, FullscreenHeader } from "./FullscreenDialog";
import TextWithHoverTooltip from "../DataDisplay/TextWithHoverTooltip";
import VirtualizedMatrix, { estimateMatrixHeight, estimateMatrixWidth } from "../DataDisplay/VirtualizedMatrix";
import { estimateTableHeight } from "../DataDisplay/VirtualizedTable";
import TraitsTable from "../DataDisplay/TraitsTable";
import StyledCircularProgress from "../Feedback/StyledCircularProgress";
import Fade from "@material-ui/core/Fade";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
const StyledMenu = withStyles(theme => ({
list: {
backgroundColor: theme.palette.background.sub,
color: theme.palette.text.main2
}
}), {name: "ContextMenu"})(props => <Menu {...props} />);
/**
* <h3>Overview</h3>
* Uses the {@link VirtualizedMatrix} and {@link VirtualizedTable} to display misclassification matrix.
* All tables are vertically centered. A context menu is going to pop up after right clicking on a matrix.
* It's possible to hide deviations as sometimes they don't provide useful information.
*
* @constructor
* @category Dialogs
* @param props {Object}
* @param {number|Object} [props.cellDimensions] - Dimensions of a cell from the {@link VirtualizedMatrix}.
* @param {number} props.cellDimensions.x - The width of a matrix cell.
* @param {number} props.cellDimensions.y - The height of a matrix cell.
* @param {function} props.onClose - Callback fired when dialog requests to be closed.
* @param {function} props.onMatrixRefresh - Callback fired when matrix refreshed it's content.
* @param {function} props.onSnackbarOpen - Callback fired when the component request to display an error.
* @param {boolean} props.open - If <code>true</code> the dialog will show up.
* @param {string} props.projectId - The identifier of a selected project.
* @param {boolean} props.refreshNeeded - If <code>true</code> dialog will refresh it's content.
* @param {string} props.resource - The name of a selected resource.
* @param {function} [props.saveMatrix] - Callback fired when user requests to save matrix.
* @param {string} props.serverBase - The host in the URL of an API call.
* @param {React.ReactNode} props.title - The content of the {@link FullscreenHeader}.
* @param {Object} [props.queryParams] - The query parameters in the URL of an API call.
* @param {string} props.queryParams.typeOfMatrix - The type of a matrix to fetch.
* @param {number} [props.queryParams.numberOfFold] - The index of a selected fold.
* @returns {React.PureComponent}
*/
class MatrixDialog extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
loading: {
matrix: false
},
requestIndex: {
matrix: 0
},
misclassification: [],
deviations: [],
traits: null,
domain: [],
heightDeviation: 0,
heightMatrix: 0,
heightTraits: 0,
mouseX: null,
mouseY: null,
widthDeviation: 0,
widthMatrix: 0
};
}
componentDidMount() {
this._isMounted = true;
if (this.props.refreshNeeded) this.props.onMatrixRefresh();
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevProps.projectId !== this.props.projectId) {
this.setState({
misclassification: [],
deviations: [],
traits: null,
domain: []
});
return;
}
if (!prevProps.open && this.props.open) {
if (this.props.refreshNeeded) {
this.getMatrix(this.props.onMatrixRefresh)
return;
}
const { misclassification } = this.state;
if (misclassification.length === 0) {
this.getMatrix();
return;
}
}
if (prevProps.queryParams != null && this.props.queryParams != null
&& prevProps.queryParams.hasOwnProperty("numberOfFold")
&& this.props.queryParams.hasOwnProperty("numberOfFold")
&& prevProps.queryParams.numberOfFold !== this.props.queryParams.numberOfFold) {
this.setState({
misclassification: [],
deviations: [],
traits: null,
domain: [],
heightDeviation: 0,
heightMatrix: 0,
heightTraits: 0,
widthDeviation: 0,
widthMatrix: 0
});
}
}
componentWillUnmount() {
this._isMounted = false;
}
getMatrix = (finallyCallback) => {
let localRequestIndex = 0;
this.setState(({loading, requestIndex}) => {
localRequestIndex = requestIndex.matrix;
return {
loading: { ...loading, matrix: true },
requestIndex: { ...requestIndex, matrix: localRequestIndex + 1 }
};
}, () => {
const { projectId, resource, serverBase, queryParams } = this.props;
const pathParams = { projectId };
fetchMatrix(
resource, pathParams, queryParams, serverBase
).then(result => {
if (this._isMounted && result != null && result.hasOwnProperty("value")
&& result.hasOwnProperty("decisionsDomain") && result.hasOwnProperty("traits")) {
this.setState(({requestIndex}) => {
if (requestIndex.matrix !== localRequestIndex + 1) {
return { };
}
return {
requestIndex: { ...requestIndex, matrix: 0 },
misclassification: result.value,
deviations: result.hasOwnProperty("Deviation of value") ? result["Deviation of value"] : [],
domain: result.decisionsDomain,
traits: result.traits
}
}, this.updateTablesHeight);
} else {
this.setState(({requestIndex}) => ({
requestIndex: { ...requestIndex, matrix: 0 }
}));
}
}).catch(exception => {
this.props.onSnackbarOpen(exception);
}).finally(() => {
if (this._isMounted) {
this.setState(({loading}) => ({
loading: { ...loading, matrix: false }
}));
}
if (typeof finallyCallback === "function") finallyCallback();
});
});
};
getTooltip = (abbrev) => {
switch(abbrev) {
case "gmean": return { text: "GMean", tooltip: "Geometric Mean"};
case "mae": return { text: "MAE", tooltip: "Mean Absolute Error"};
case "rmse": return { text: "RMSE", tooltip: "Root Mean Square Error"};
default: return {};
}
};
cellRenderer = ({cellData, dataKey}) => {
const abbrevs = ["gmean", "mae", "rmse"];
let displayedText = cellData;
let displayedTooltip = cellData
if (abbrevs.includes(cellData)) {
const tooltip = this.getTooltip(cellData);
displayedText = tooltip.text;
displayedTooltip = tooltip.tooltip;
}
return (
<React.Fragment>
{cellData &&
<TextWithHoverTooltip
roundNumbers={false}
text={displayedText}
TooltipProps={{
id: dataKey
}}
tooltipTitle={displayedTooltip}
TypographyProps={{
style: {cursor: "default"}
}}
/>
}
</React.Fragment>
)
}
onContextMenuOpen = (event) => {
event.preventDefault();
this.setState({
mouseX: event.clientX - 2,
mouseY: event.clientY - 4
});
};
onContextMenuClose = () => {
this.setState({
mouseX: null,
mouseY: null
});
};
onSave = () => {
this.props.saveMatrix();
this.onContextMenuClose();
};
updateTablesHeight = () => {
const { misclassification, deviations, traits } = this.state;
const displayedTraits = deviations.length === 0 ? this.prepareTraitsWithoutDeviation(traits) : { ...traits };
this.setState({
heightMatrix: estimateMatrixHeight(misclassification),
widthMatrix: estimateMatrixWidth(misclassification),
heightDeviation: deviations.length > 0 ? estimateMatrixHeight(deviations) : 0,
widthDeviation: deviations.length > 0 ? estimateMatrixWidth(deviations) : 0,
heightTraits: estimateTableHeight(Object.keys(displayedTraits))
});
};
prepareTraitsWithoutDeviation = (traits) => {
return Object.keys(traits).map(key => {
if (key.toLowerCase().includes("deviation")) {
return { };
}
return { [key]: traits[key] };
}).reduce((previousValue, currentValue) => {
return { ...previousValue, ...currentValue};
});
};
render() {
const {
loading,
misclassification,
deviations,
traits,
domain,
heightDeviation,
heightMatrix,
heightTraits,
mouseX,
mouseY,
widthDeviation,
widthMatrix,
} = this.state;
const {
cellDimensions,
open,
onClose,
title
} = this.props;
const numberOfColumns = deviations.length === 0 ? 2 : 3;
return (
<FullscreenDialog open={open} onClose={onClose}>
<FullscreenHeader
id={"matrix-details-header"}
onClose={onClose}
title={title}
/>
<MultiColumns numberOfColumns={loading.matrix ? 1 : numberOfColumns}>
{loading.matrix &&
<StyledCircularProgress />
}
{!loading.matrix && Array.isArray(misclassification) && misclassification.length > 0 &&
<CenteredColumn
height={heightMatrix}
InnerWrapperProps={{
onContextMenu: this.onContextMenuOpen
}}
maxWidth={`${90 / numberOfColumns}%`}
width={widthMatrix}
>
<VirtualizedMatrix
cellDimensions={cellDimensions}
matrix={addSubheaders(domain, misclassification)}
type={"Misclassification matrix"}
/>
</CenteredColumn>
}
{!loading.matrix && Array.isArray(deviations) && deviations.length > 0 &&
<CenteredColumn
height={heightDeviation}
InnerWrapperProps={{
onContextMenu: this.onContextMenuOpen
}}
maxWidth={`${90 / numberOfColumns}%`}
width={widthDeviation}
>
<VirtualizedMatrix
cellDimensions={cellDimensions}
matrix={addSubheaders(domain, deviations)}
type={"Deviations"}
/>
</CenteredColumn>
}
{!loading.matrix && traits != null &&
<CenteredColumn
height={heightTraits}
minWidth={`${90 / numberOfColumns}%`}
width={`calc(90% - ${widthMatrix + widthDeviation}px)`}
>
<TraitsTable
cellRenderer={this.cellRenderer}
columnsLabels={{key: "Name", value: "Value"}}
ratio={0.9}
traits={traits}
/>
</CenteredColumn>
}
</MultiColumns>
<StyledMenu
anchorPosition={
mouseX !== null && mouseY !== null
? { top: mouseY, left: mouseX }
: undefined
}
anchorReference={"anchorPosition"}
keepMounted={true}
onClose={this.onContextMenuClose}
open={mouseY !== null}
TransitionComponent={Fade}
>
<MenuItem onClick={this.onSave}>Save to file</MenuItem>
</StyledMenu>
</FullscreenDialog>
)
}
}
MatrixDialog.propTypes = {
cellDimensions: PropTypes.oneOfType([
PropTypes.number,
PropTypes.exact({
x: PropTypes.number,
y: PropTypes.number,
})
]),
onClose: PropTypes.func,
onMatrixRefresh: PropTypes.func,
onSnackbarOpen: PropTypes.func,
open: PropTypes.bool.isRequired,
projectId: PropTypes.string,
refreshNeeded: PropTypes.bool,
resource: PropTypes.string,
saveMatrix: PropTypes.func,
serverBase: PropTypes.string,
title: PropTypes.node.isRequired,
queryParams: PropTypes.shape({
typeOfMatrix: PropTypes.string,
numberOfFold: PropTypes.number
})
};
MatrixDialog.defaultProps = {
refreshNeeded: false
};
export default MatrixDialog;
Source