diff --git a/.eslintrc.js b/.eslintrc.js index 57e27db4..8b42a5f9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,12 +6,14 @@ module.exports = { "globals": { }, "plugins": [ - "jsx-a11y" + "jsx-a11y", + "react-hooks" ], "extends": [ 'plugin:react/recommended', "eslint:recommended", - "plugin:jsx-a11y/recommended" + "plugin:jsx-a11y/recommended", + "plugin:react-hooks/recommended" ], "env": { "browser": true, @@ -55,6 +57,7 @@ module.exports = { "no-useless-escape": "off", "no-with": "error", "object-curly-spacing": "off", + "react/react-in-jsx-scope": "off", "react/button-has-type": "error", "react/display-name": "off", "react/jsx-closing-bracket-location": "error", // autofixable diff --git a/babel.config.json b/babel.config.json index 2d4f26a3..fcde8385 100644 --- a/babel.config.json +++ b/babel.config.json @@ -1,6 +1,6 @@ { "presets": [ ["@babel/preset-env", {"loose": true}], - "@babel/preset-react" + ["@babel/preset-react", {"runtime": "automatic"}] ] } diff --git a/package.json b/package.json index 2c15a519..e2b84902 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "eslint": "8", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", "identity-obj-proxy": "^3.0.0", "jest": "29", "messageformat": "2.3.0", diff --git a/src/App.js b/src/App.js index 3033ed7d..95ece78f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,3 @@ -import React, { Component } from "react"; import PropTypes from "prop-types"; import SelectDataset from "./UIComponents/SelectDataset"; import DataDisplay from "./UIComponents/DataDisplay"; @@ -25,39 +24,37 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import I18n from "./i18n"; -class PanelButtons extends Component { - static propTypes = { - panelButtons: PropTypes.object, - currentPanel: PropTypes.string, - setCurrentPanel: PropTypes.func, - onContinue: PropTypes.func, - startSaveTrainedModel: PropTypes.func, - dataToSave: PropTypes.object, - saveStatus: PropTypes.string, - saveResponseData: PropTypes.object, - isSaveComplete: PropTypes.func, - shouldDisplaySaveStatus: PropTypes.func - }; - - onClickPrev = () => { - this.props.setCurrentPanel(this.props.panelButtons.prev.panel); +function PanelButtons({ + panelButtons, + currentPanel, + setCurrentPanel, + onContinue, + startSaveTrainedModel, + dataToSave, + saveStatus, + saveResponseData, + isSaveComplete: isSaveCompleteProp, + shouldDisplaySaveStatus: shouldDisplaySaveStatusProp +}) { + const onClickPrev = () => { + setCurrentPanel(panelButtons.prev.panel); }; - onClickNext = () => { - if (["continue", "finish"].includes(this.props.panelButtons.next.panel)) { - this.props.onContinue(); - } else if (this.props.currentPanel === "saveModel") { - this.props.startSaveTrainedModel(this.props.dataToSave); + const onClickNext = () => { + if (["continue", "finish"].includes(panelButtons.next.panel)) { + onContinue(); + } else if (currentPanel === "saveModel") { + startSaveTrainedModel(dataToSave); } else { - this.props.setCurrentPanel(this.props.panelButtons.next.panel); + setCurrentPanel(panelButtons.next.panel); } }; - localizedSaveMessage = saveStatus => { + const localizedSaveMessage = saveStatus => { return I18n.t(`saveStatus_${saveStatus}`); }; - saveResponseDataMessage = saveResponseData => { + const saveResponseDataMessage = saveResponseData => { // The list of known error types from share_filtering.rb. const errorTypes = ["email", "address", "phone", "profanity"]; @@ -73,74 +70,83 @@ class PanelButtons extends Component { } }; - render() { - const { panelButtons, saveStatus, saveResponseData } = this.props; - - let loadSaveStatus = this.props.isSaveComplete(saveStatus) ? ( - this.localizedSaveMessage(saveStatus) - ) : ( - - ); - - let loadSaveResponseData = this.props.isSaveComplete(saveStatus) - ? this.saveResponseDataMessage(saveResponseData) - : undefined; - - return ( -
- {panelButtons.prev && ( -
- + let loadSaveStatus = isSaveCompleteProp(saveStatus) ? ( + localizedSaveMessage(saveStatus) + ) : ( + + ); + + let loadSaveResponseData = isSaveCompleteProp(saveStatus) + ? saveResponseDataMessage(saveResponseData) + : undefined; + + return ( +
+ {panelButtons.prev && ( +
+ +
+ )} + + {shouldDisplaySaveStatusProp(saveStatus) && + currentPanel === "saveModel" && ( +
+ {loadSaveStatus} + {loadSaveResponseData && ( +
+ {loadSaveResponseData} +
+ )}
)} - {this.props.shouldDisplaySaveStatus(saveStatus) && - this.props.currentPanel === "saveModel" && ( -
- {loadSaveStatus} - {loadSaveResponseData && ( -
- {loadSaveResponseData} -
- )} -
- )} - - {panelButtons.next && ( -
- -
- )} -
- ); - } + {panelButtons.next && ( +
+ +
+ )} +
+ ); } +PanelButtons.propTypes = { + panelButtons: PropTypes.object, + currentPanel: PropTypes.string, + setCurrentPanel: PropTypes.func, + onContinue: PropTypes.func, + startSaveTrainedModel: PropTypes.func, + dataToSave: PropTypes.object, + saveStatus: PropTypes.string, + saveResponseData: PropTypes.object, + isSaveComplete: PropTypes.func, + shouldDisplaySaveStatus: PropTypes.func +}; + const BodyContainer = props => (
{props.children}
); @@ -177,121 +183,117 @@ ContainerFullWidth.propTypes = { children: PropTypes.node }; -class App extends Component { - static propTypes = { - panelButtons: PropTypes.object, - currentPanel: PropTypes.string, - setCurrentPanel: PropTypes.func, - onContinue: PropTypes.func, - resultsPhase: PropTypes.number, - startSaveTrainedModel: PropTypes.func, - dataToSave: PropTypes.object, - saveStatus: PropTypes.string, - saveResponseData: PropTypes.object, - isSaveComplete: PropTypes.func, - shouldDisplaySaveStatus: PropTypes.func - }; - - render() { - const { - panelButtons, - currentPanel, - setCurrentPanel, - onContinue, - resultsPhase, - dataToSave, - startSaveTrainedModel, - saveStatus, - saveResponseData - } = this.props; - - return ( -
- {currentPanel === "selectDataset" && ( - - - - - - - - - )} - - {["dataDisplayLabel", "dataDisplayFeatures"].includes(currentPanel) && ( - - - - - +function App({ + panelButtons, + currentPanel, + setCurrentPanel, + onContinue, + resultsPhase, + startSaveTrainedModel, + dataToSave, + saveStatus, + saveResponseData +}) { + return ( +
+ {currentPanel === "selectDataset" && ( + + + + + + + + + )} + + {["dataDisplayLabel", "dataDisplayFeatures"].includes(currentPanel) && ( + + + + + + + + + + )} + + {currentPanel === "trainModel" && ( + + + + + + )} + + {currentPanel === "generateResults" && ( + + + + + + )} + + {currentPanel === "results" && ( + + + + + {resultsPhase === 1 && ( - + - - )} - - {currentPanel === "trainModel" && ( - - - - - - )} - - {currentPanel === "generateResults" && ( - - - - - - )} - - {currentPanel === "results" && ( - - - - - {resultsPhase === 1 && ( - - - - )} - - )} - - {currentPanel === "saveModel" && ( - - - - - - )} - - {currentPanel === "modelSummary" && ( - - - - - - )} - - -
- ); - } + )} +
+ )} + + {currentPanel === "saveModel" && ( + + + + + + )} + + {currentPanel === "modelSummary" && ( + + + + + + )} + + +
+ ); } +App.propTypes = { + panelButtons: PropTypes.object, + currentPanel: PropTypes.string, + setCurrentPanel: PropTypes.func, + onContinue: PropTypes.func, + resultsPhase: PropTypes.number, + startSaveTrainedModel: PropTypes.func, + dataToSave: PropTypes.object, + saveStatus: PropTypes.string, + saveResponseData: PropTypes.object, + isSaveComplete: PropTypes.func, + shouldDisplaySaveStatus: PropTypes.func +}; + export default connect( state => ({ panelButtons: getPanelButtons(state), diff --git a/src/UIComponents/AddFeatureButton.jsx b/src/UIComponents/AddFeatureButton.jsx index cf5ed65f..cd0dc475 100644 --- a/src/UIComponents/AddFeatureButton.jsx +++ b/src/UIComponents/AddFeatureButton.jsx @@ -1,40 +1,33 @@ /* React component to handle selecting columns as features. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import { connect } from "react-redux"; import { styles } from "../constants"; import { addSelectedFeature } from "../redux"; import I18n from "../i18n"; - -class AddFeatureButton extends Component { - static propTypes = { - column: PropTypes.string, - addSelectedFeature: PropTypes.func.isRequired - }; - - addFeature = (event, column) => { - this.props.addSelectedFeature(column); +function AddFeatureButton({ column, addSelectedFeature }) { + const addFeature = (event, column) => { + addSelectedFeature(column); event.preventDefault(); }; - render() { - const { column } = this.props; - - return ( - - ) - } - + return ( + + ); } +AddFeatureButton.propTypes = { + column: PropTypes.string, + addSelectedFeature: PropTypes.func.isRequired +}; + export default connect( state => ({}), dispatch => ({ diff --git a/src/UIComponents/AnimationDescriptions.jsx b/src/UIComponents/AnimationDescriptions.jsx index 1e0367ad..18fc2244 100644 --- a/src/UIComponents/AnimationDescriptions.jsx +++ b/src/UIComponents/AnimationDescriptions.jsx @@ -1,48 +1,39 @@ /* React component to handle animation descriptions for screen readers. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import I18n from "../i18n"; -class AnimationDescription extends Component { - static propTypes = { - description: PropTypes.string - }; - - render() { - const { description } = this.props; - - return ( -
-
-

- {I18n.t("animationDescriptionsHeader")} -
- {description} -

-
+function AnimationDescription({ description }) { + return ( +
+
+

+ {I18n.t("animationDescriptionsHeader")} +
+ {description} +

- ) - } +
+ ); } -class TrainingAnimationDescription extends Component { - render() { - return ( - - ) - } +AnimationDescription.propTypes = { + description: PropTypes.string +}; + +function TrainingAnimationDescription() { + return ( + + ); } -class TestingAnimationDescription extends Component { - render() { - return ( - - ) - } +function TestingAnimationDescription() { + return ( + + ); } export { TrainingAnimationDescription, TestingAnimationDescription }; diff --git a/src/UIComponents/ColumnDataTypeDropdown.jsx b/src/UIComponents/ColumnDataTypeDropdown.jsx index fdf7e7a4..fe4eb5c6 100644 --- a/src/UIComponents/ColumnDataTypeDropdown.jsx +++ b/src/UIComponents/ColumnDataTypeDropdown.jsx @@ -1,47 +1,42 @@ /* React component to handle setting datatype for selected columns. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import { connect } from "react-redux"; import { setColumnsByDataType } from "../redux"; import { ColumnTypes } from "../constants.js"; import I18n from "../i18n"; -class ColumnDataTypeDropdown extends Component { - static propTypes = { - columnId: PropTypes.string, - currentDataType: PropTypes.oneOf(Object.values(ColumnTypes)), - setColumnsByDataType: PropTypes.func.isRequired - }; - - handleChangeDataType = (event, feature) => { +function ColumnDataTypeDropdown({ columnId, currentDataType, setColumnsByDataType }) { + const handleChangeDataType = (event, feature) => { event.preventDefault(); - this.props.setColumnsByDataType(feature, event.target.value); + setColumnsByDataType(feature, event.target.value); }; - render() { - const { columnId, currentDataType } = this.props; - - return ( -
- -
- ); - } + return ( +
+ +
+ ); } +ColumnDataTypeDropdown.propTypes = { + columnId: PropTypes.string, + currentDataType: PropTypes.oneOf(Object.values(ColumnTypes)), + setColumnsByDataType: PropTypes.func.isRequired +}; + export default connect( state => ({}), dispatch => ({ diff --git a/src/UIComponents/ColumnDetailsCategorical.jsx b/src/UIComponents/ColumnDetailsCategorical.jsx index 67eb1ae4..b3721477 100644 --- a/src/UIComponents/ColumnDetailsCategorical.jsx +++ b/src/UIComponents/ColumnDetailsCategorical.jsx @@ -1,5 +1,4 @@ /* React component to handle showing details of categorical columns. */ -import React, { Component } from "react"; import { connect } from "react-redux"; import { colors, styles } from "../constants"; import { Bar } from "react-chartjs-2"; @@ -9,21 +8,6 @@ import { } from "../selectors/currentColumnSelectors"; import I18n from "../i18n"; -const barData = { - labels: [], - datasets: [ - { - label: "", - backgroundColor: colors.tealTransparent, - borderColor: colors.teal, - borderWidth: 1, - hoverBackgroundColor: "#59cad3", - hoverBorderColor: "white", - data: [] - } - ] -}; - const chartOptions = { scales: { yAxes: [ @@ -38,47 +22,55 @@ const chartOptions = { maintainAspectRatio: false }; -class ColumnDetailsCategorical extends Component { - static propTypes = { - columnDetails: categeoricalColumnDetailsShape +function ColumnDetailsCategorical({ columnDetails }) { + const { id, uniqueOptions, frequencies } = columnDetails; + const labels = uniqueOptions && Object.values(uniqueOptions); + const barData = { + labels, + datasets: [ + { + label: id, + backgroundColor: colors.tealTransparent, + borderColor: colors.teal, + borderWidth: 1, + hoverBackgroundColor: "#59cad3", + hoverBorderColor: "white", + data: labels.map(option => frequencies[option]) + } + ] }; - render() { - const { id, uniqueOptions, frequencies } = this.props.columnDetails; - barData.labels = uniqueOptions && Object.values(uniqueOptions); - barData.datasets[0].data = barData.labels.map(option => { - return frequencies[option]; - }); - barData.datasets[0].label = id; - - const maxLabelsInHistogram = 5; + const maxLabelsInHistogram = 5; - return ( -
-
{I18n.t("columnDetailsInformation")}
-
- {barData.labels.length <= maxLabelsInHistogram && ( - - )} - {barData.labels.length > maxLabelsInHistogram && ( -
- {I18n.t("columnDetailsTooManyLabels", { - "labelCount": barData.labels.length, - "maxLabelCount": maxLabelsInHistogram - })} -
- )} -
+ return ( +
+
{I18n.t("columnDetailsInformation")}
+
+ {labels.length <= maxLabelsInHistogram && ( + + )} + {labels.length > maxLabelsInHistogram && ( +
+ {I18n.t("columnDetailsTooManyLabels", { + "labelCount": labels.length, + "maxLabelCount": maxLabelsInHistogram + })} +
+ )}
- ); - } +
+ ); } +ColumnDetailsCategorical.propTypes = { + columnDetails: categeoricalColumnDetailsShape +}; + export default connect( state => ({ columnDetails: getCategoricalColumnDetails(state) diff --git a/src/UIComponents/ColumnDetailsNumerical.jsx b/src/UIComponents/ColumnDetailsNumerical.jsx index 02f28ed6..3762294d 100644 --- a/src/UIComponents/ColumnDetailsNumerical.jsx +++ b/src/UIComponents/ColumnDetailsNumerical.jsx @@ -1,39 +1,36 @@ /* React component to handle showing details of numerical columns. */ -import React, { Component } from "react"; import { connect } from "react-redux"; import { styles } from "../constants"; import { getNumericalColumnDetails } from "../selectors/currentColumnSelectors"; import { numericalColumnDetailsShape } from "./shapes" import I18n from "../i18n"; -class ColumnDetailsNumerical extends Component { - static propTypes = { - columnDetails: numericalColumnDetailsShape - }; +function ColumnDetailsNumerical({ columnDetails }) { + const { extrema, containsOnlyNumbers } = columnDetails; - render() { - const { extrema, containsOnlyNumbers } = this.props.columnDetails; - - return ( -
-
{I18n.t("columnDetailsInformation")}
- {!containsOnlyNumbers && ( -

{I18n.t("columnDetailsNumericalTypeError")}

- )} - {containsOnlyNumbers && extrema && ( -
- {I18n.t("columnDetailsMinimumValue")} {extrema.min} -
- {I18n.t("columnDetailsMaximumValue")} {extrema.max} -
- {I18n.t("columnDetailsValueRange")} {extrema.range} -
- )} -
- ); - } + return ( +
+
{I18n.t("columnDetailsInformation")}
+ {!containsOnlyNumbers && ( +

{I18n.t("columnDetailsNumericalTypeError")}

+ )} + {containsOnlyNumbers && extrema && ( +
+ {I18n.t("columnDetailsMinimumValue")} {extrema.min} +
+ {I18n.t("columnDetailsMaximumValue")} {extrema.max} +
+ {I18n.t("columnDetailsValueRange")} {extrema.range} +
+ )} +
+ ); } +ColumnDetailsNumerical.propTypes = { + columnDetails: numericalColumnDetailsShape +}; + export default connect( state => ({ columnDetails: getNumericalColumnDetails(state) diff --git a/src/UIComponents/ColumnInspector.jsx b/src/UIComponents/ColumnInspector.jsx index 22d04754..f412c3b2 100644 --- a/src/UIComponents/ColumnInspector.jsx +++ b/src/UIComponents/ColumnInspector.jsx @@ -3,7 +3,6 @@ for selected columns. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import { connect } from "react-redux"; import { getCurrentColumnDetails } from "../selectors/currentColumnSelectors"; import { styles, ColumnTypes } from "../constants.js"; @@ -20,82 +19,78 @@ import { currentColumnInspectorShape } from "./shapes"; import I18n from "../i18n"; import { getLocalizedColumnName } from "../helpers/columnDetails.js"; -class ColumnInspector extends Component { - static propTypes = { - currentColumnDetails: currentColumnInspectorShape, - currentPanel: PropTypes.string, - datasetId: PropTypes.string - }; +function ColumnInspector({ currentColumnDetails, currentPanel, datasetId }) { + const selectingFeatures = currentPanel === "dataDisplayFeatures"; + const selectingLabel = currentPanel === "dataDisplayLabel"; - render() { - const { currentColumnDetails, currentPanel, datasetId } = this.props; + const isCategorical = currentColumnDetails && currentColumnDetails.dataType + === ColumnTypes.CATEGORICAL; + const isNumerical = currentColumnDetails && currentColumnDetails.dataType + === ColumnTypes.NUMERICAL; - const selectingFeatures = currentPanel === "dataDisplayFeatures"; - const selectingLabel = currentPanel === "dataDisplayLabel"; - - const isCategorical = currentColumnDetails && currentColumnDetails.dataType - === ColumnTypes.CATEGORICAL; - const isNumerical = currentColumnDetails && currentColumnDetails.dataType - === ColumnTypes.NUMERICAL; - - if (!currentColumnDetails) { - return null; - } + if (!currentColumnDetails) { + return null; + } - const localizedDataType = I18n.t(`columnType_${currentColumnDetails.dataType}`) - const localizedColumnName = getLocalizedColumnName(datasetId, currentColumnDetails.id); + const localizedDataType = I18n.t(`columnType_${currentColumnDetails.dataType}`) + const localizedColumnName = getLocalizedColumnName(datasetId, currentColumnDetails.id); - return ( - currentColumnDetails && ( -
-
{localizedColumnName}
- + return ( + currentColumnDetails && ( +
+
{localizedColumnName}
+ +
+ {I18n.t("columnInspectorDataType")} +
+ {currentColumnDetails.readOnly && localizedDataType} + {!currentColumnDetails.readOnly && ( + + )} +
+ {currentColumnDetails.description && (
- {I18n.t("columnInspectorDataType")} -
- {currentColumnDetails.readOnly && localizedDataType} - {!currentColumnDetails.readOnly && ( - - )} + {I18n.t("columnInspectorDescription")} +   +
{currentColumnDetails.description}
- {currentColumnDetails.description && ( -
- {I18n.t("columnInspectorDescription")} -   -
{currentColumnDetails.description}
-
- )} - {selectingFeatures && ( -
- - -
- )} - {isCategorical && } - {isNumerical && } -
- {selectingLabel && currentColumnDetails.isSelectable && ( - )} - {selectingFeatures && currentColumnDetails.isSelectable && ( - + {selectingFeatures && ( +
+ + +
)} - -
- ) - ); - } + {isCategorical && } + {isNumerical && } +
+ {selectingLabel && currentColumnDetails.isSelectable && ( + + )} + {selectingFeatures && currentColumnDetails.isSelectable && ( + + )} + +
+ ) + ); } +ColumnInspector.propTypes = { + currentColumnDetails: currentColumnInspectorShape, + currentPanel: PropTypes.string, + datasetId: PropTypes.string +}; + export default connect( state => ({ currentColumnDetails: getCurrentColumnDetails(state), diff --git a/src/UIComponents/CrossTab.jsx b/src/UIComponents/CrossTab.jsx index 2c684d43..4aac9e99 100644 --- a/src/UIComponents/CrossTab.jsx +++ b/src/UIComponents/CrossTab.jsx @@ -3,7 +3,7 @@ each active combination of features values, and corresponding percentage of label values, with a heatmap style applied. */ -import React, { Component } from "react"; +import { useCallback } from "react"; import { connect } from "react-redux"; import { getCrossTabData } from "../selectors/visualizationSelectors"; import { styles } from "../constants.js"; @@ -11,131 +11,127 @@ import ScrollableContent from "./ScrollableContent"; import { crossTabDataShape } from "./shapes"; import I18n from "../i18n"; -class CrossTab extends Component { - static propTypes = { - crossTabData: crossTabDataShape - }; - - getCellStyle = percent => { +function CrossTab({ crossTabData }) { + const getCellStyle = useCallback((percent) => { return { ...styles["crossTabCell" + Math.round(percent / 20)], ...styles.crossTabTableCell }; - }; - - render() { - const { crossTabData } = this.props; + }, []); - // There are a few criteria that affect how big the table looks. We'll not - // render it if any of them are exceeded. - // First, how many columns are there on the left side. - const maxFeaturesInTable = 5; - // Second, how many columns are there in the main table. - const maxUniqueLabelValues = 5; - // Third, how many rows are there. - const maxResults = 5; + // There are a few criteria that affect how big the table looks. We'll not + // render it if any of them are exceeded. + // First, how many columns are there on the left side. + const maxFeaturesInTable = 5; + // Second, how many columns are there in the main table. + const maxUniqueLabelValues = 5; + // Third, how many rows are there. + const maxResults = 5; - const showTable = - crossTabData && - crossTabData.featureNames.length <= maxFeaturesInTable && - crossTabData.uniqueLabelValues.length <= maxUniqueLabelValues && - crossTabData.results.length <= maxResults; + const showTable = + crossTabData && + crossTabData.featureNames.length <= maxFeaturesInTable && + crossTabData.uniqueLabelValues.length <= maxUniqueLabelValues && + crossTabData.results.length <= maxResults; - return ( -
- {crossTabData && !showTable && ( -
-
{I18n.t("crossTabRelationshipHeader")}
+ return ( +
+ {crossTabData && !showTable && ( +
+
{I18n.t("crossTabRelationshipHeader")}
-
{I18n.t("crossTabTooMuchData")}
-
-
- )} +
{I18n.t("crossTabTooMuchData")}
+
+
+ )} - {showTable && ( -
-
{I18n.t("crossTabRelationshipHeader")}
- - - - - + ); + } + )} + + {crossTabData.results.map((result, resultIndex) => { + return ( + + {result.featureValues.map( + (featureValue, featureIndex) => { + return ( + + ); + } + )} + {crossTabData.uniqueLabelValues.map( + (uniqueLabelValue, labelIndex) => { + return ( + + ); + } + )} + + ); + })} + +
- +
{I18n.t("crossTabRelationshipHeader")}
+ + + + + + + + + + + - - - - - - - ); - } - )} - - {crossTabData.results.map((result, resultIndex) => { - return ( - - {result.featureValues.map( - (featureValue, featureIndex) => { - return ( - - ); - } - )} - {crossTabData.uniqueLabelValues.map( - (uniqueLabelValue, labelIndex) => { - return ( - - ); - } - )} - - ); - })} - -
+ +   + + {crossTabData.labelName} +
+
-   - -
- {crossTabData.labelName} -
-
- {crossTabData.featureNames[0]} -
-
- {crossTabData.uniqueLabelValues.map( - (uniqueLabelValue, index) => { - return ( - - {uniqueLabelValue} -
- {featureValue} - - {result.labelPercents[uniqueLabelValue] || 0}% -
-
- - )} - - ); - } + {crossTabData.featureNames[0]} + + +
+ {crossTabData.uniqueLabelValues.map( + (uniqueLabelValue, index) => { + return ( + + {uniqueLabelValue} +
+ {featureValue} + + {result.labelPercents[uniqueLabelValue] || 0}% +
+
+
+ )} +
+ ); } +CrossTab.propTypes = { + crossTabData: crossTabDataShape +}; + export default connect(state => ({ crossTabData: getCrossTabData(state) }))(CrossTab); diff --git a/src/UIComponents/DataCard.jsx b/src/UIComponents/DataCard.jsx index 7ff9740b..eb002f59 100644 --- a/src/UIComponents/DataCard.jsx +++ b/src/UIComponents/DataCard.jsx @@ -1,6 +1,5 @@ /* React component to show information about the currently-selected data set. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import { connect } from "react-redux"; import { styles } from "../constants.js"; import ScrollableContent from "./ScrollableContent"; @@ -8,107 +7,102 @@ import { metadataShape, datasetDetailsShape } from "./shapes.js"; import I18n from "../i18n"; import { getDatasetDetails } from "../helpers/datasetDetails"; -class DataCard extends Component { - static propTypes = { - name: PropTypes.string, - metadata: metadataShape, - datasetDetails: datasetDetailsShape, - dataLength: PropTypes.number, - removedRowsCount: PropTypes.number - }; +function DataCard({ name, metadata, datasetDetails, dataLength, removedRowsCount }) { + const card = metadata && metadata.card; - render() { - - const { name, metadata, datasetDetails, dataLength, removedRowsCount } = this.props; - - const card = metadata && metadata.card; - - const dataLengthLimit = 20000; - if (dataLength > dataLengthLimit) { - window.alert( - I18n.t("dataCardWarningLargeDataset", {"rowCount": dataLengthLimit}) - ); - } + const dataLengthLimit = 20000; + if (dataLength > dataLengthLimit) { + window.alert( + I18n.t("dataCardWarningLargeDataset", {"rowCount": dataLengthLimit}) + ); + } - const removedRowsMsg = - removedRowsCount > 0 - ? I18n.t("dataCardRemovedRows", {"rowCount": removedRowsCount}) - : null; + const removedRowsMsg = + removedRowsCount > 0 + ? I18n.t("dataCardRemovedRows", {"rowCount": removedRowsCount}) + : null; - return ( - dataLength !== 0 && ( -
-
{name || I18n.t("dataCardDefaultHeader")}
- - {card && ( -
-
{datasetDetails.description}
-
- - {I18n.t("dataCardSource")} -
- {metadata.card.source} -
-
+ return ( + dataLength !== 0 && ( +
+
{name || I18n.t("dataCardDefaultHeader")}
+ + {card && ( +
+
{datasetDetails.description}
+
+ + {I18n.t("dataCardSource")} +
+ {metadata.card.source} +
+
+
+ + {I18n.t("dataCardRowCount")} +
+ {dataLength} +
+
+ {metadata.card.lastUpdated && (
- {I18n.t("dataCardRowCount")} + {I18n.t("dataCardLastUpdated")}
- {dataLength} + {metadata.card.lastUpdated}
- {metadata.card.lastUpdated && ( -
- - {I18n.t("dataCardLastUpdated")} -
- {metadata.card.lastUpdated} -
-
- )} + )} - {metadata.card.context.potentialUses && ( -
-
{I18n.t("dataCardPotentialUses")}
-
- {datasetDetails.potentialUses} -
-
- )} - {metadata.card.context.potentialMisuses && ( -
-
{I18n.t("dataCardPotentialMisuses")}
-
- {datasetDetails.potentialMisuses} -
-
- )} -
- )} - {!card && dataLength > 0 && ( -
-
+ {metadata.card.context.potentialUses && (
-
{I18n.t("dataCardRowCount")}
- {dataLength} +
{I18n.t("dataCardPotentialUses")}
+
+ {datasetDetails.potentialUses} +
+ )} + {metadata.card.context.potentialMisuses && (
- {removedRowsMsg && - -   - {removedRowsMsg} - - } +
{I18n.t("dataCardPotentialMisuses")}
+
+ {datasetDetails.potentialMisuses} +
+ )} +
+ )} + {!card && dataLength > 0 && ( +
+
+
+
{I18n.t("dataCardRowCount")}
+ {dataLength}
- )} - -
- ) - ); - } +
+ {removedRowsMsg && + +   + {removedRowsMsg} + + } +
+
+ )} + +
+ ) + ); } +DataCard.propTypes = { + name: PropTypes.string, + metadata: metadataShape, + datasetDetails: datasetDetailsShape, + dataLength: PropTypes.number, + removedRowsCount: PropTypes.number +}; + export default connect(state => ({ name: state.name, metadata: state.metadata, diff --git a/src/UIComponents/DataDisplay.jsx b/src/UIComponents/DataDisplay.jsx index 26ac9162..3bf97a8a 100644 --- a/src/UIComponents/DataDisplay.jsx +++ b/src/UIComponents/DataDisplay.jsx @@ -1,43 +1,38 @@ /* React component to handle displaying imported data. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import { connect } from "react-redux"; import Statement from "./Statement"; import DataTable from "./DataTable"; import { styles } from "../constants"; import I18n from "../i18n"; -class DataDisplay extends Component { - static propTypes = { - data: PropTypes.array - }; - - render() { - const { data } = this.props; - - if (data.length === 0) { - return null; - } +function DataDisplay({ data }) { + if (data.length === 0) { + return null; + } - const rowCount = this.props.data.length; - const rowLimit = 100; - const rowCountMessage = (rowCount <= rowLimit) ? - I18n.t("dataDisplayRowCount", {"rowCount": rowCount}) : - I18n.t("dataDisplayRowCountTruncated", {"rowCount": rowCount, "rowLimit": rowLimit}); - return ( -
- -
- -
-
- {rowCountMessage} -
+ const rowCount = data.length; + const rowLimit = 100; + const rowCountMessage = (rowCount <= rowLimit) ? + I18n.t("dataDisplayRowCount", {"rowCount": rowCount}) : + I18n.t("dataDisplayRowCountTruncated", {"rowCount": rowCount, "rowLimit": rowLimit}); + return ( +
+ +
+
- ); - } +
+ {rowCountMessage} +
+
+ ); } +DataDisplay.propTypes = { + data: PropTypes.array +}; + export default connect( state => ({ data: state.data diff --git a/src/UIComponents/DataTable.jsx b/src/UIComponents/DataTable.jsx index a5606195..1d3a357c 100644 --- a/src/UIComponents/DataTable.jsx +++ b/src/UIComponents/DataTable.jsx @@ -1,57 +1,54 @@ /* React component to handle displaying imported data. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import { connect } from "react-redux"; import { getTableData, setCurrentColumn, setHighlightColumn } from "../redux"; import { styles } from "../constants"; import { getLocalizedColumnName } from "../helpers/columnDetails.js"; -class DataTable extends Component { - static propTypes = { - currentPanel: PropTypes.string, - data: PropTypes.array, - datasetId: PropTypes.string, - labelColumn: PropTypes.string, - selectedFeatures: PropTypes.array, - setCurrentColumn: PropTypes.func, - setHighlightColumn: PropTypes.func, - currentColumn: PropTypes.string, - highlightColumn: PropTypes.string, - reducedColumns: PropTypes.bool, - singleRow: PropTypes.number, - startingRow: PropTypes.number, - noLabel: PropTypes.bool, - hideLabel: PropTypes.bool - }; - - getColumnHeaderStyle = key => { +function DataTable({ + currentPanel, + data, + datasetId, + labelColumn, + selectedFeatures, + setCurrentColumn: setCurrentColumnProp, + setHighlightColumn: setHighlightColumnProp, + currentColumn, + highlightColumn, + reducedColumns, + singleRow, + startingRow, + noLabel, + hideLabel +}) { + const getColumnHeaderStyle = key => { let style; - if (key === this.props.labelColumn) { + if (key === labelColumn) { style = styles.dataDisplayHeaderLabel; - } else if (this.props.selectedFeatures.includes(key)) { + } else if (selectedFeatures.includes(key)) { style = styles.dataDisplayHeaderFeature; } const pointerStyle = - !this.props.reducedColumns && styles.dataDisplayHeaderClickable; + !reducedColumns && styles.dataDisplayHeaderClickable; return { ...styles.dataDisplayHeader, ...pointerStyle, ...style }; }; - getColumnCellStyle = key => { + const getColumnCellStyle = key => { let style; - if (this.props.hideLabel && this.props.labelColumn === key) { + if (hideLabel && labelColumn === key) { style = styles.dataDisplayCellHidden; - } else if (key === this.props.currentColumn) { - if (this.props.currentPanel === "dataDisplayLabel") { + } else if (key === currentColumn) { + if (currentPanel === "dataDisplayLabel") { style = styles.dataDisplayCellSelectedLabel; } else { style = styles.dataDisplayCellSelected; } - } else if (key === this.props.highlightColumn) { - if (this.props.currentPanel === "dataDisplayLabel") { + } else if (key === highlightColumn) { + if (currentPanel === "dataDisplayLabel") { style = styles.dataDisplayCellHighlightedLabel; } else { style = styles.dataDisplayCellHighlighted; @@ -61,107 +58,120 @@ class DataTable extends Component { return { ...styles.dataDisplayCell, ...style }; }; - getColumns = () => { - if (this.props.reducedColumns) { - return Object.keys(this.props.data[0]) + const getColumns = () => { + if (reducedColumns) { + return Object.keys(data[0]) .filter(key => { return ( - (!this.props.noLabel && this.props.labelColumn === key) || - this.props.selectedFeatures.includes(key) + (!noLabel && labelColumn === key) || + selectedFeatures.includes(key) ); }) .sort((key1, key2) => { - return this.props.labelColumn === key1 ? 1 : -1; + return labelColumn === key1 ? 1 : -1; }); } - return Object.keys(this.props.data[0]); + return Object.keys(data[0]); }; - getRows = () => { - if (this.props.singleRow !== undefined) { + const getRows = () => { + if (singleRow !== undefined) { return [ - this.props.data[ - Math.min(this.props.singleRow, this.props.data.length - 1) + data[ + Math.min(singleRow, data.length - 1) ] ]; } else { - return this.props.data.slice(0, 100); + return data.slice(0, 100); } }; - setCurrentColumn = columnId => { - if (!this.props.reducedColumns) { - this.props.setCurrentColumn(columnId); + const handleSetCurrentColumn = columnId => { + if (!reducedColumns) { + setCurrentColumnProp(columnId); } }; - setHighlightColumn = columnId => { - if (!this.props.reducedColumns) { - this.props.setHighlightColumn(columnId); + const handleSetHighlightColumn = columnId => { + if (!reducedColumns) { + setHighlightColumnProp(columnId); } }; - render() { - const { data, startingRow } = this.props; - - if (data.length === 0) { - return null; - } + if (data.length === 0) { + return null; + } - return ( - - - - {this.getColumns().map(columnId => { - return ( - - ); - })} - - - - {this.getRows().map((row, index) => { + return ( +
this.setCurrentColumn(columnId)} - onKeyDown={() => this.setCurrentColumn(columnId)} - onMouseEnter={() => this.setHighlightColumn(columnId)} - onMouseLeave={() => this.setHighlightColumn(undefined)} - > - {getLocalizedColumnName(this.props.datasetId, columnId)} -
+ + + {getColumns().map(columnId => { return ( - - {this.getColumns().map(columnId => { - return ( - - ); - })} - + ); })} - -
this.setCurrentColumn(columnId)} - onKeyDown={() => this.setCurrentColumn(columnId)} - onMouseEnter={() => this.setHighlightColumn(columnId)} - onMouseLeave={() => this.setHighlightColumn(undefined)} - role="gridcell" - > - {startingRow !== undefined && index <= startingRow ? ( -   - ) : ( - row[columnId] - )} -
handleSetCurrentColumn(columnId)} + onKeyDown={() => handleSetCurrentColumn(columnId)} + onMouseEnter={() => handleSetHighlightColumn(columnId)} + onMouseLeave={() => handleSetHighlightColumn(undefined)} + > + {getLocalizedColumnName(datasetId, columnId)} +
- ); - } + + + + {getRows().map((row, index) => { + return ( + + {getColumns().map(columnId => { + return ( + handleSetCurrentColumn(columnId)} + onKeyDown={() => handleSetCurrentColumn(columnId)} + onMouseEnter={() => handleSetHighlightColumn(columnId)} + onMouseLeave={() => handleSetHighlightColumn(undefined)} + role="gridcell" + > + {startingRow !== undefined && index <= startingRow ? ( +   + ) : ( + row[columnId] + )} + + ); + })} + + ); + })} + + + ); } +DataTable.propTypes = { + currentPanel: PropTypes.string, + data: PropTypes.array, + datasetId: PropTypes.string, + labelColumn: PropTypes.string, + selectedFeatures: PropTypes.array, + setCurrentColumn: PropTypes.func, + setHighlightColumn: PropTypes.func, + currentColumn: PropTypes.string, + highlightColumn: PropTypes.string, + reducedColumns: PropTypes.bool, + singleRow: PropTypes.number, + startingRow: PropTypes.number, + noLabel: PropTypes.bool, + hideLabel: PropTypes.bool +}; + export default connect( (state, props) => ({ data: getTableData(state, props.useResultsData), diff --git a/src/UIComponents/GenerateResults.jsx b/src/UIComponents/GenerateResults.jsx index ce53f35d..03fcf9e2 100644 --- a/src/UIComponents/GenerateResults.jsx +++ b/src/UIComponents/GenerateResults.jsx @@ -1,6 +1,6 @@ /* React component to handle training. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { connect } from "react-redux"; import { getTableData, readyToTrain } from "../redux"; import { styles, getFadeOpacity } from "../constants"; @@ -15,203 +15,191 @@ import I18n from "../i18n"; const framesPerCycle = 80; const maxNumItems = 7; -class GenerateResults extends Component { - static propTypes = { - data: PropTypes.array, - readyToTrain: PropTypes.bool, - labelColumn: PropTypes.string, - selectedFeatures: PropTypes.array, - instructionsOverlayActive: PropTypes.bool - }; - - state = { - frame: 0, - animationTimer: undefined, - headOpen: false - }; - - constructor(props) { - super(props); - - this.state = { - frame: 0, - animationTimer: undefined, - finished: false +function GenerateResults({ data, readyToTrain, labelColumn, selectedFeatures, instructionsOverlayActive }) { + const [frame, setFrame] = useState(0); + const [, setFinished] = useState(false); + const frameRef = useRef(0); + const instructionsOverlayActiveRef = useRef(instructionsOverlayActive); + + // Keep ref in sync with prop + useEffect(() => { + instructionsOverlayActiveRef.current = instructionsOverlayActive; + }, [instructionsOverlayActive]); + + const getNumItems = useCallback(() => { + return Math.min(maxNumItems, data.length); + }, [data.length]); + + useEffect(() => { + const animationTimer = setInterval(() => { + const currentFrame = frameRef.current; + const numItems = Math.min(maxNumItems, data.length); + const animationStep = Math.floor(currentFrame / framesPerCycle); + + if (animationStep >= numItems) { + setFinished(true); + } + + if (!instructionsOverlayActiveRef.current) { + frameRef.current = currentFrame + 1; + setFrame(frameRef.current); + } + }, 1000 / 30); + + return () => { + clearInterval(animationTimer); }; - } - - componentDidMount() { - const animationTimer = setInterval( - this.updateAnimation.bind(this), - 1000 / 30 - ); - - this.setState({ animationTimer }); - } - - getNumItems = () => { - return Math.min(maxNumItems, this.props.data.length); - }; - - getShowItemsFadingOut = () => { - return this.props.data.length > maxNumItems; - }; + }, [data.length]); - getAnimationSubstep = () => { - return this.state.frame % framesPerCycle; + const getShowItemsFadingOut = () => { + return data.length > maxNumItems; }; - updateAnimation = () => { - if (this.getAnimationStep() >= this.getNumItems()) { - this.setState({ finished: true }); - } - - if (!this.props.instructionsOverlayActive) { - this.setState({ frame: this.state.frame + 1 }); - } + const getAnimationSubstep = () => { + return frame % framesPerCycle; }; - componentWillUnmount = () => { - if (this.state.animationTimer) { - clearInterval(this.state.animationTimer); - this.setState({ animationTimer: undefined }); - } - }; - - getAnimationProgess = () => { - let amount = (2 * (this.state.frame % framesPerCycle)) / framesPerCycle; + const getAnimationProgress = () => { + let amount = (2 * (frame % framesPerCycle)) / framesPerCycle; amount -= Math.sin(amount * 2 * Math.PI) / (2 * Math.PI); return amount / 2; }; - getAnimationStep = () => { - return Math.floor(this.state.frame / framesPerCycle); + const getAnimationStep = () => { + return Math.floor(frame / framesPerCycle); }; - render() { - const animationProgress = this.getAnimationProgess(); - const translateX = 30 + animationProgress * 40; - const translateY = 15; - const rotateZ = 5 + animationProgress * -10; - const transform = `translateX(-50%) translateY(-50%) rotateZ(${rotateZ}deg)`; - const opacity = getFadeOpacity(animationProgress); - const hideLabel = this.getAnimationSubstep() < framesPerCycle / 2; - const showAnimation = this.getAnimationStep() < this.getNumItems(); - const startFadingAtItem = - this.getNumItems() - (this.getShowItemsFadingOut() ? 1.5 : 0); - const maxFrames = framesPerCycle * startFadingAtItem; - const tableOpacity = - this.state.frame < framesPerCycle - ? this.state.frame / framesPerCycle - : this.state.frame >= maxFrames && - this.state.frame < maxFrames + framesPerCycle - ? 1 - (this.state.frame - maxFrames) / framesPerCycle - : this.state.frame >= maxFrames + framesPerCycle - ? 0 - : 1; - const headMoveAmount = - this.state.frame < framesPerCycle / 4 - ? this.state.frame / (framesPerCycle / 4) - : this.state.frame >= maxFrames + framesPerCycle && - this.state.frame <= maxFrames + 2 * framesPerCycle - ? 1 - (this.state.frame - (maxFrames + framesPerCycle)) / framesPerCycle - : this.state.frame >= maxFrames + 2 * framesPerCycle - ? 0 - : 1; - const botTransformY = -50 - headMoveAmount * 50; - const botContainerTransform = `translateX(-25%) translateY(${botTransformY}%)`; - - // Let's still show the starting row on our very first frame, because we might - // be paused waiting for the overlay to be dismissed. - const startingRow = - this.state.frame === 0 ? undefined : this.getAnimationStep(); - - return ( -
-
- {I18n.t("generateResultsHeader")} + const numItems = getNumItems(); + const animationProgress = getAnimationProgress(); + const translateX = 30 + animationProgress * 40; + const translateY = 15; + const rotateZ = 5 + animationProgress * -10; + const transform = `translateX(-50%) translateY(-50%) rotateZ(${rotateZ}deg)`; + const opacity = getFadeOpacity(animationProgress); + const hideLabel = getAnimationSubstep() < framesPerCycle / 2; + const showAnimation = getAnimationStep() < numItems; + const startFadingAtItem = + numItems - (getShowItemsFadingOut() ? 1.5 : 0); + const maxFrames = framesPerCycle * startFadingAtItem; + const tableOpacity = + frame < framesPerCycle + ? frame / framesPerCycle + : frame >= maxFrames && + frame < maxFrames + framesPerCycle + ? 1 - (frame - maxFrames) / framesPerCycle + : frame >= maxFrames + framesPerCycle + ? 0 + : 1; + const headMoveAmount = + frame < framesPerCycle / 4 + ? frame / (framesPerCycle / 4) + : frame >= maxFrames + framesPerCycle && + frame <= maxFrames + 2 * framesPerCycle + ? 1 - (frame - (maxFrames + framesPerCycle)) / framesPerCycle + : frame >= maxFrames + 2 * framesPerCycle + ? 0 + : 1; + const botTransformY = -50 - headMoveAmount * 50; + const botContainerTransform = `translateX(-25%) translateY(${botTransformY}%)`; + + // Let's still show the starting row on our very first frame, because we might + // be paused waiting for the overlay to be dismissed. + const startingRow = + frame === 0 ? undefined : getAnimationStep(); + + return ( +
+
+ {I18n.t("generateResultsHeader")} +
+ +
+
+
-
+ {showAnimation && (
+ )} +
- {showAnimation && ( -
- -
- )} -
- -
-
- {I18n.t("aiBotHeadAltText")} +
+
+ {I18n.t("aiBotHeadAltText")} + {I18n.t("aiBotBodyAltText")} +
{I18n.t("aiBotBodyAltText")} -
- {I18n.t("aiBotBeamAltText")} -
-
- ); - } + +
+ ); } +GenerateResults.propTypes = { + data: PropTypes.array, + readyToTrain: PropTypes.bool, + labelColumn: PropTypes.string, + selectedFeatures: PropTypes.array, + instructionsOverlayActive: PropTypes.bool +}; + export default connect(state => ({ data: getTableData(state, true), readyToTrain: readyToTrain(state), diff --git a/src/UIComponents/ModelCard.jsx b/src/UIComponents/ModelCard.jsx index 6516f1c7..d0c8a156 100644 --- a/src/UIComponents/ModelCard.jsx +++ b/src/UIComponents/ModelCard.jsx @@ -1,6 +1,5 @@ /* React component to handle displaying the model card. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import { connect } from "react-redux"; import { styles } from "../constants"; import { getLabelToSave, getFeaturesToSave } from "../redux"; @@ -12,157 +11,148 @@ import { datasetDetailsShape, trainedModelDetailsShape, modelCardColumnShape } f import I18n from "../i18n"; import { getLocalizedColumnName } from "../helpers/columnDetails.js"; -class ModelCard extends Component { - static propTypes = { - trainedModelDetails: trainedModelDetailsShape, - selectedFeatures: PropTypes.arrayOf(modelCardColumnShape), - percentCorrect: PropTypes.string, - label: modelCardColumnShape, - features: PropTypes.arrayOf(PropTypes.string), - datasetDetails: datasetDetailsShape - }; - - render() { - const { - trainedModelDetails, - selectedFeatures, - percentCorrect, - label, - features, - datasetDetails - } = this.props; - console.log("trainedModelDetails", trainedModelDetails) - const localizedLabel = getLocalizedColumnName(datasetDetails.name, label.id); - const localizedFeatures = - selectedFeatures.map(feature => getLocalizedColumnName(datasetDetails.name, feature)); - const predictionStatement = I18n.t("predictionStatement", - {"output": localizedLabel, "inputs": localizedFeatures.join(", ")}) - return ( -
- -
- {I18n.t("aiBotAltText")} +function ModelCard({ trainedModelDetails, selectedFeatures, percentCorrect, label, features, datasetDetails }) { + console.log("trainedModelDetails", trainedModelDetails) + const localizedLabel = getLocalizedColumnName(datasetDetails.name, label.id); + const localizedFeatures = + selectedFeatures.map(feature => getLocalizedColumnName(datasetDetails.name, feature)); + const predictionStatement = I18n.t("predictionStatement", + {"output": localizedLabel, "inputs": localizedFeatures.join(", ")}) + return ( +
+ +
+ {I18n.t("aiBotAltText")} +
+
+

{trainedModelDetails.name}

+
+
{I18n.t("modelCardAccuracy")}
+
+

+ + {percentCorrect}% + +

+
-
-

{trainedModelDetails.name}

-
-
{I18n.t("modelCardAccuracy")}
-
+
+
{I18n.t("modelCardIntendedUse")}
+
+ {trainedModelDetails.potentialUses && (

- - {percentCorrect}% - + {trainedModelDetails.potentialUses}

-
+ )}
-
-
{I18n.t("modelCardIntendedUse")}
-
- {trainedModelDetails.potentialUses && ( -

- {trainedModelDetails.potentialUses} -

- )} -
-
-
-
{I18n.t("modelCardLimitations")}
-
- {trainedModelDetails.potentialMisuses && ( -

- {trainedModelDetails.potentialMisuses} -

- )} -
+
+
+
{I18n.t("modelCardLimitations")}
+
+ {trainedModelDetails.potentialMisuses && ( +

+ {trainedModelDetails.potentialMisuses} +

+ )}
-
-
{I18n.t("modelCardDatasetDetails")}
-
- {datasetDetails.description && ( -

- {datasetDetails.description} -

- )} - {datasetDetails.numRows && ( -

- {I18n.t("modelCardDatasetDetailsSize")} -
- {I18n.t( - "modelCardDatasetDetailsSizeCount", - { "rowCount": datasetDetails.numRows})} -

- )} -
+
+
+
{I18n.t("modelCardDatasetDetails")}
+
+ {datasetDetails.description && ( +

+ {datasetDetails.description} +

+ )} + {datasetDetails.numRows && ( +

+ {I18n.t("modelCardDatasetDetailsSize")} +
+ {I18n.t( + "modelCardDatasetDetailsSizeCount", + { "rowCount": datasetDetails.numRows})} +

+ )}
-
-
{I18n.t("modelCardFeaturesAndLabel")}
-
-

{predictionStatement}

-
+
+
+
{I18n.t("modelCardFeaturesAndLabel")}
+
+

{predictionStatement}

-
-
{I18n.t("modelCardLabel")}
-
-

{localizedLabel}

- {label.description &&

{label.description}

} - {!label.values && ( -

- {I18n.t("modelCardPossibleValues")} -
- {I18n.t("modelCardPossibleValuesMinimum")} {label.min} -
- {I18n.t("modelCardPossibleValuesMaximum")} {label.max} -

- )} - {label.values && ( -

- {I18n.t("modelCardPossibleValues")} -
- {label.values.join(" ")} -

- )} -
+
+
+
{I18n.t("modelCardLabel")}
+
+

{localizedLabel}

+ {label.description &&

{label.description}

} + {!label.values && ( +

+ {I18n.t("modelCardPossibleValues")} +
+ {I18n.t("modelCardPossibleValuesMinimum")} {label.min} +
+ {I18n.t("modelCardPossibleValuesMaximum")} {label.max} +

+ )} + {label.values && ( +

+ {I18n.t("modelCardPossibleValues")} +
+ {label.values.join(" ")} +

+ )}
-
-
{I18n.t("modelCardFeatures")}
-
- {features.length > 0 && - features.map((feature, index) => { - return ( -
-

{feature.id}

- {feature.description &&

{feature.description}

} - {!feature.values && ( -

- {I18n.t("modelCardPossibleValues")} -
- {I18n.t("modelCardPossibleValuesMinimum")} {feature.min} -
- {I18n.t("modelCardPossibleValuesMaximum")} {feature.max} -

- )} - {feature.values && ( -

- {I18n.t("modelCardPossibleValues")} -
- {feature.values.join(" ")} -

- )} -
- ); - })} -
+
+
+
{I18n.t("modelCardFeatures")}
+
+ {features.length > 0 && + features.map((feature, index) => { + return ( +
+

{feature.id}

+ {feature.description &&

{feature.description}

} + {!feature.values && ( +

+ {I18n.t("modelCardPossibleValues")} +
+ {I18n.t("modelCardPossibleValuesMinimum")} {feature.min} +
+ {I18n.t("modelCardPossibleValuesMaximum")} {feature.max} +

+ )} + {feature.values && ( +

+ {I18n.t("modelCardPossibleValues")} +
+ {feature.values.join(" ")} +

+ )} +
+ ); + })}
- ); - } +
+ ); } + +ModelCard.propTypes = { + trainedModelDetails: trainedModelDetailsShape, + selectedFeatures: PropTypes.arrayOf(modelCardColumnShape), + percentCorrect: PropTypes.string, + label: modelCardColumnShape, + features: PropTypes.arrayOf(PropTypes.string), + datasetDetails: datasetDetailsShape +}; + export default connect(state => ({ trainedModelDetails: state.trainedModelDetails, selectedFeatures: state.selectedFeatures, diff --git a/src/UIComponents/Predict.jsx b/src/UIComponents/Predict.jsx index a0b74c93..3d75bf9a 100644 --- a/src/UIComponents/Predict.jsx +++ b/src/UIComponents/Predict.jsx @@ -1,6 +1,5 @@ /* React component to handle predicting and displaying predictions. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import { connect } from "react-redux"; import { store } from "../index.js"; import train from "../train"; @@ -18,121 +17,129 @@ import ScrollableContent from "./ScrollableContent"; import I18n from "../i18n"; import { getLocalizedColumnName } from "../helpers/columnDetails.js"; -class Predict extends Component { - static propTypes = { - labelColumn: PropTypes.string, - selectedCategoricalFeatures: PropTypes.array, - selectedNumericalFeatures: PropTypes.array, - uniqueOptionsByColumn: PropTypes.object, - testData: PropTypes.object, - setTestData: PropTypes.func.isRequired, - predictedLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - getPredictAvailable: PropTypes.bool, - extremaByColumn: PropTypes.object, - datasetId: PropTypes.string +function Predict({ + labelColumn, + selectedCategoricalFeatures, + selectedNumericalFeatures, + uniqueOptionsByColumn, + testData, + setTestData, + predictedLabel, + getPredictAvailable: predictAvailable, + extremaByColumn, + datasetId +}) { + const handleChange = (event, feature) => { + setTestData(feature, event.target.value); }; - handleChange = (event, feature) => { - this.props.setTestData(feature, event.target.value); - }; - - onClickPredict = () => { + const onClickPredict = () => { train.onClickPredict(store); }; - render() { - const { datasetId, labelColumn, predictedLabel } = this.props; - return ( -
-
{I18n.t("predictHeader")}
- -
- {this.props.selectedNumericalFeatures.map((feature, index) => { - let min = this.props.extremaByColumn[feature].min.toFixed(2); - let max = this.props.extremaByColumn[feature].max.toFixed(2); + return ( +
+
{I18n.t("predictHeader")}
+ +
+ {selectedNumericalFeatures.map((feature, index) => { + let min = extremaByColumn[feature].min.toFixed(2); + let max = extremaByColumn[feature].max.toFixed(2); - return ( -
- + return ( +
+ +
+ ); + })} +
+
+ {selectedCategoricalFeatures.map((feature, index) => { + return ( +
+
{getLocalizedColumnName(datasetId, feature)} 
+
+
- ); - })} +
+ ); + })} +
+ +
+
+ +
+ {predictedLabel && ( +
+
+ {I18n.t("aiBotAltText")}
-
- {this.props.selectedCategoricalFeatures.map((feature, index) => { - return ( -
-
{getLocalizedColumnName(datasetId, feature)} 
-
- -
-
- ); - })} +
+
{I18n.t("predictAIBotPredicts")}
+
{getLocalizedColumnName(datasetId, labelColumn)}
+
{getLocalizedColumnName(datasetId, predictedLabel)}
- -
-
-
- {this.props.predictedLabel && ( -
-
- {I18n.t("aiBotAltText")} -
-
-
{I18n.t("predictAIBotPredicts")}
-
{getLocalizedColumnName(datasetId, labelColumn)}
-
{getLocalizedColumnName(datasetId, predictedLabel)}
-
-
- )} -
- ); - } + )} +
+ ); } +Predict.propTypes = { + labelColumn: PropTypes.string, + selectedCategoricalFeatures: PropTypes.array, + selectedNumericalFeatures: PropTypes.array, + uniqueOptionsByColumn: PropTypes.object, + testData: PropTypes.object, + setTestData: PropTypes.func.isRequired, + predictedLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + getPredictAvailable: PropTypes.bool, + extremaByColumn: PropTypes.object, + datasetId: PropTypes.string +}; + export default connect( state => ({ testData: state.testData, diff --git a/src/UIComponents/Results.jsx b/src/UIComponents/Results.jsx index 9f32e6a9..8189706b 100644 --- a/src/UIComponents/Results.jsx +++ b/src/UIComponents/Results.jsx @@ -1,6 +1,6 @@ /* React component to handle displaying accuracy results. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; +import { useEffect, useCallback } from "react"; import { connect } from "react-redux"; import { styles } from "../constants"; import { UnconnectedStatement } from "./Statement"; @@ -9,90 +9,87 @@ import ResultsDetails from "./ResultsDetails"; import ScrollableContent from "./ScrollableContent"; import I18n from "../i18n"; -class Results extends Component { - static propTypes = { - historicResults: PropTypes.array, - showResultsDetails: PropTypes.bool, - setShowResultsDetails: PropTypes.func, - setResultsPhase: PropTypes.func - }; - - componentDidMount() { - this.props.setResultsPhase(0); - setTimeout(() => { - this.props.setResultsPhase(1); +function Results({ historicResults, showResultsDetails, setShowResultsDetails, setResultsPhase }) { + useEffect(() => { + setResultsPhase(0); + const timer = setTimeout(() => { + setResultsPhase(1); }, 1000); - } - - showDetails = () => { - this.props.setShowResultsDetails(true); - }; + return () => clearTimeout(timer); + }, [setResultsPhase]); - render() { - const { historicResults, showResultsDetails } = this.props; + const showDetails = useCallback(() => { + setShowResultsDetails(true); + }, [setShowResultsDetails]); - return ( -
- {showResultsDetails && } + return ( +
+ {showResultsDetails && } -
- -
-
{I18n.t("resultsHeader")}
-
{I18n.t("resultsAccuracy")}
-
- {historicResults.map((historicResult, index) => { - return ( -
-
- -
-
- {historicResult.accuracy}% +
+ +
+
{I18n.t("resultsHeader")}
+
{I18n.t("resultsAccuracy")}
+
+ {historicResults.map((historicResult, index) => { + return ( +
+
+ +
+
+ {historicResult.accuracy}% +
+ {index === 0 && ( +
+
- {index === 0 && ( -
- + )} + {index === 0 && historicResults.length > 1 && ( +
+
+ {I18n.t("resultsPreviousResults")}
- )} - {index === 0 && historicResults.length > 1 && ( -
-
- {I18n.t("resultsPreviousResults")} -
-
- {I18n.t("resultsAccuracy")} -
+
+ {I18n.t("resultsAccuracy")}
- )} -
- ); - })} - -
+
+ )} +
+ ); + })} +
- ); - } +
+ ); } +Results.propTypes = { + historicResults: PropTypes.array, + showResultsDetails: PropTypes.bool, + setShowResultsDetails: PropTypes.func, + setResultsPhase: PropTypes.func +}; + export default connect( state => ({ historicResults: state.historicResults, diff --git a/src/UIComponents/ResultsDetails.jsx b/src/UIComponents/ResultsDetails.jsx index 4f18aa1c..ce91762e 100644 --- a/src/UIComponents/ResultsDetails.jsx +++ b/src/UIComponents/ResultsDetails.jsx @@ -1,6 +1,6 @@ /* React component to handle displaying accuracy results. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; +import { useCallback } from "react"; import { connect } from "react-redux"; import { setShowResultsDetails } from "../redux"; import { @@ -15,47 +15,45 @@ import ResultsTable from "./ResultsTable"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faTimes } from "@fortawesome/free-solid-svg-icons"; -class ResultsDetails extends Component { - static propTypes = { - resultsTab: PropTypes.string, - selectedFeatures: PropTypes.array, - labelColumn: PropTypes.string, - percentCorrect: PropTypes.string, - setShowResultsDetails: PropTypes.func, - correctResults: resultsPropType, - incorrectResults: resultsPropType, - }; +function ResultsDetails({ resultsTab, selectedFeatures, labelColumn, percentCorrect, setShowResultsDetails, correctResults, incorrectResults }) { + const onClose = useCallback(() => { + setShowResultsDetails(false); + }, [setShowResultsDetails]); - onClose = () => { - this.props.setShowResultsDetails(false); - }; + const results = + resultsTab === ResultsGrades.CORRECT + ? correctResults + : incorrectResults; - render() { - const results = - this.props.resultsTab === ResultsGrades.CORRECT - ? this.props.correctResults - : this.props.incorrectResults; - - return ( -
-
-
- -
- {!isNaN(this.props.percentCorrect) && } - + return ( +
+
+
+
+ {!isNaN(percentCorrect) && } +
- ); - } +
+ ); } +ResultsDetails.propTypes = { + resultsTab: PropTypes.string, + selectedFeatures: PropTypes.array, + labelColumn: PropTypes.string, + percentCorrect: PropTypes.string, + setShowResultsDetails: PropTypes.func, + correctResults: resultsPropType, + incorrectResults: resultsPropType, +}; + export default connect( state => ({ resultsTab: state.resultsTab, diff --git a/src/UIComponents/ResultsTable.jsx b/src/UIComponents/ResultsTable.jsx index a6fde15f..fb36c929 100644 --- a/src/UIComponents/ResultsTable.jsx +++ b/src/UIComponents/ResultsTable.jsx @@ -1,6 +1,6 @@ /* React component to handle displaying test data and A.I. Bot's guesses. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; +import { useCallback } from "react"; import { connect } from "react-redux"; import { isRegression, setResultsHighlightRow } from "../redux"; import { styles, colors, REGRESSION_ERROR_TOLERANCE } from "../constants"; @@ -8,137 +8,134 @@ import { resultsPropType } from "./shapes"; import I18n from "../i18n"; import { getLocalizedColumnName } from "../helpers/columnDetails.js"; -class ResultsTable extends Component { - static propTypes = { - selectedFeatures: PropTypes.array, - labelColumn: PropTypes.string, - results: resultsPropType, - isRegression: PropTypes.bool, - setResultsHighlightRow: PropTypes.func, - resultsHighlightRow: PropTypes.number, - datasetId: PropTypes.string - }; - - getRowCellStyle = index => { +function ResultsTable({ selectedFeatures, labelColumn, results, isRegression: isRegressionMode, setResultsHighlightRow, resultsHighlightRow, datasetId }) { + const getRowCellStyle = useCallback((index) => { return { ...styles.tableCell, - ...(index === this.props.resultsHighlightRow && + ...(index === resultsHighlightRow && styles.resultsCellHighlight) }; - }; + }, [resultsHighlightRow]); - render() { - const { setResultsHighlightRow, datasetId } = this.props; - const featureCount = this.props.selectedFeatures.length; + const featureCount = selectedFeatures.length; - return ( -
- {this.props.isRegression && ( -
- {I18n.t( - "resultsTablePredictionRange", - {"percentage": REGRESSION_ERROR_TOLERANCE} - )} -
- )} + return ( +
+ {isRegressionMode && ( +
+ {I18n.t( + "resultsTablePredictionRange", + {"percentage": REGRESSION_ERROR_TOLERANCE} + )} +
+ )} -
- - - - - - - - - {this.props.selectedFeatures.map((feature, index) => { - return ( - - ); - })} - - - - - - {this.props.results.examples.map((examples, index) => { +
+
- {I18n.t("resultsTableFeatureHeader")} - - {I18n.t("resultsTableActualValueHeader")} - - {I18n.t("resultsTablePredictedValueHeader")} -
- {getLocalizedColumnName(datasetId, feature)} - - {getLocalizedColumnName(datasetId, this.props.labelColumn)} - - {getLocalizedColumnName(datasetId, this.props.labelColumn)} -
+ + + + + + + + {selectedFeatures.map((feature, index) => { return ( - setResultsHighlightRow(index)} - onMouseLeave={() => setResultsHighlightRow(undefined)} > - {examples.map((example, i) => { - return ( - - ); - })} - - - + {getLocalizedColumnName(datasetId, feature)} + ); })} - -
+ {I18n.t("resultsTableFeatureHeader")} + + {I18n.t("resultsTableActualValueHeader")} + + {I18n.t("resultsTablePredictedValueHeader")} +
- {example} - - {this.props.results.labels[index]} - - {this.props.results.predictedLabels[index]} -
-
+ + {getLocalizedColumnName(datasetId, labelColumn)} + + + {getLocalizedColumnName(datasetId, labelColumn)} + + + + + {results.examples.map((examples, index) => { + return ( + setResultsHighlightRow(index)} + onMouseLeave={() => setResultsHighlightRow(undefined)} + > + {examples.map((example, i) => { + return ( + + {example} + + ); + })} + + {results.labels[index]} + + + {results.predictedLabels[index]} + + + ); + })} + +
- ); - } +
+ ); } +ResultsTable.propTypes = { + selectedFeatures: PropTypes.array, + labelColumn: PropTypes.string, + results: resultsPropType, + isRegression: PropTypes.bool, + setResultsHighlightRow: PropTypes.func, + resultsHighlightRow: PropTypes.number, + datasetId: PropTypes.string +}; + export default connect( state => ({ selectedFeatures: state.selectedFeatures, diff --git a/src/UIComponents/ResultsToggle.jsx b/src/UIComponents/ResultsToggle.jsx index 9d14420d..ba0ca997 100644 --- a/src/UIComponents/ResultsToggle.jsx +++ b/src/UIComponents/ResultsToggle.jsx @@ -1,6 +1,5 @@ /* React component to handle toggling between correct/incorrect test results */ import PropTypes from "prop-types"; -import React, { Component } from "react"; import { connect } from "react-redux"; import { setResultsTab } from "../redux"; import { ResultsGrades, styles } from "../constants"; @@ -8,15 +7,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faTimes, faCheck } from "@fortawesome/free-solid-svg-icons"; import I18n from "../i18n"; -class ResultsToggle extends Component { - static propTypes = { - resultsTab: PropTypes.string, - setResultsTab: PropTypes.func - }; - - getTogglePillStyle = key => { +function ResultsToggle({ resultsTab, setResultsTab }) { + const getTogglePillStyle = key => { let style; - if (key === this.props.resultsTab) { + if (key === resultsTab) { style = { ...styles.pill, ...styles.selectedPill }; } else { style = styles.pill; @@ -24,43 +18,47 @@ class ResultsToggle extends Component { return style; }; - render() { - const resultsTabs = [ - { - key: ResultsGrades.CORRECT, - headerText: I18n.t("correctAnswer"), - icon: faCheck, - iconStyle: styles.correct - }, - { - key: ResultsGrades.INCORRECT, - headerText: I18n.t("incorrectAnswer"), - icon: faTimes, - iconStyle: styles.error - } - ]; - return ( -
-
- {resultsTabs.map(tab => ( -
this.props.setResultsTab(tab.key)} - onKeyDown={() => this.props.setResultsTab(tab.key)} - role="button" - tabIndex={0} - > - {" "} - {tab.headerText} -
- ))} -
+ const resultsTabs = [ + { + key: ResultsGrades.CORRECT, + headerText: I18n.t("correctAnswer"), + icon: faCheck, + iconStyle: styles.correct + }, + { + key: ResultsGrades.INCORRECT, + headerText: I18n.t("incorrectAnswer"), + icon: faTimes, + iconStyle: styles.error + } + ]; + + return ( +
+
+ {resultsTabs.map(tab => ( +
setResultsTab(tab.key)} + onKeyDown={() => setResultsTab(tab.key)} + role="button" + tabIndex={0} + > + {" "} + {tab.headerText} +
+ ))}
- ); - } +
+ ); } +ResultsToggle.propTypes = { + resultsTab: PropTypes.string, + setResultsTab: PropTypes.func +}; + export default connect( state => ({ resultsTab: state.resultsTab diff --git a/src/UIComponents/SaveModel.jsx b/src/UIComponents/SaveModel.jsx index 181ed2a7..9283da2c 100644 --- a/src/UIComponents/SaveModel.jsx +++ b/src/UIComponents/SaveModel.jsx @@ -1,6 +1,6 @@ /* React component to handle saving a trained model. */ import PropTypes from "prop-types"; -import React, { Component } from "react"; +import { useState } from "react"; import { connect } from "react-redux"; import { setTrainedModelDetail } from "../redux"; import { @@ -14,45 +14,34 @@ import ScrollableContent from "./ScrollableContent"; import I18n from "../i18n"; import { getLocalizedColumnName } from "../helpers/columnDetails.js"; -class SaveModel extends Component { - static propTypes = { - trainedModel: PropTypes.object, - setTrainedModelDetail: PropTypes.func, - trainedModelDetails: PropTypes.object, - labelColumn: PropTypes.string, - columnDescriptions: PropTypes.array, - dataDescription: PropTypes.string, - isUserUploadedDataset: PropTypes.bool, - datasetId: PropTypes.string - }; - - constructor(props) { - super(props); - - this.state = { - showColumnDescriptions: this.props.isUserUploadedDataset - }; - } +function SaveModel({ + trainedModel, + setTrainedModelDetail, + trainedModelDetails, + labelColumn, + columnDescriptions, + dataDescription, + isUserUploadedDataset: isUserUploaded, + datasetId +}) { + const [showColumnDescriptions, setShowColumnDescriptions] = useState(isUserUploaded); - toggleColumnDescriptions = () => { - this.setState({ - showColumnDescriptions: !this.state.showColumnDescriptions - }); + const toggleColumnDescriptions = () => { + setShowColumnDescriptions(!showColumnDescriptions); }; - handleChange = (event, field, isColumn) => { - this.props.setTrainedModelDetail(field, event.target.value, isColumn); + const handleChange = (event, field, isColumn) => { + setTrainedModelDetail(field, event.target.value, isColumn); }; - getColumnFields = () => { + const getColumnFields = () => { var fields = []; - for (const columnDescription of this.props.columnDescriptions) { - const datasetId = this.props.datasetId; + for (const columnDescription of columnDescriptions) { const labelType = I18n.t("saveModelColumnTypeLabel"); const featureType = I18n.t("saveModelColumnTypeFeature"); const columnType = - columnDescription.id === this.props.labelColumn ? labelType : featureType + columnDescription.id === labelColumn ? labelType : featureType fields.push({ id: columnDescription.id, isColumn: true, @@ -64,7 +53,7 @@ class SaveModel extends Component { return fields; }; - getUsesFields = () => { + const getUsesFields = () => { var fields = []; fields.push({ id: "potentialUses", @@ -87,145 +76,154 @@ class SaveModel extends Component { return fields; }; - render() { - const nameField = { - id: "name", - text: I18n.t("modelNameLabel") - }; + const nameField = { + id: "name", + text: I18n.t("modelNameLabel") + }; - const dataDescriptionField = { - id: "datasetDescription", - text: I18n.t("datasetDescriptionLabel"), - placeholder: I18n.t("datasetDescriptionPlaceholder"), - answer: this.props.dataDescription - }; + const dataDescriptionField = { + id: "datasetDescription", + text: I18n.t("datasetDescriptionLabel"), + placeholder: I18n.t("datasetDescriptionPlaceholder"), + answer: dataDescription + }; - const arrowIcon = this.state.showColumnDescriptions - ? "fa fa-caret-up" - : "fa fa-caret-down"; + const arrowIcon = showColumnDescriptions + ? "fa fa-caret-up" + : "fa fa-caret-down"; - const columnCount = this.getColumnFields().length; + const columnCount = getColumnFields().length; - return ( -
- - -
- {nameField.text} -   - ({I18n.t("saveModelFieldRequired")}) + return ( +
+ + +
+ {nameField.text} +   + ({I18n.t("saveModelFieldRequired")}) +
+ + handleChange(event, nameField.id, nameField.isColumn) + } + maxLength={ModelNameMaxLength} + /> +
+
+
+ {getUsesFields().map(field => { + return ( +
+
{field.text}
+
{field.description}
+
    + {field.descriptionDetails && + field.descriptionDetails.map((detail, index) => { + return ( +
  • + {detail} +
  • + ); + })} +
+ {!field.answer && ( +
+