Skip to content

Commit

Permalink
Merge pull request #24 from BoostV/feature/editable_data_points
Browse files Browse the repository at this point in the history
Feature/editable data points
  • Loading branch information
j-or committed Apr 28, 2021
2 parents 577804c + f93b401 commit 4b046eb
Show file tree
Hide file tree
Showing 12 changed files with 899 additions and 127 deletions.
2 changes: 1 addition & 1 deletion components/categorical-variable-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function CategoricalVariableOptions(props: CategoricalVariableOpt
</Grid>
<Grid item xs={4}>
<IconButton size="small" onClick={() => onOptionAdded()}>
<AddIcon />
<AddIcon color="primary"/>
</IconButton>
</Grid>
</Grid>
Expand Down
3 changes: 1 addition & 2 deletions components/categorical-variable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import { Button, IconButton, TextField, Typography } from '@material-ui/core';
import DeleteIcon from "@material-ui/icons/Delete";
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { CategoricalVariableType } from '../types/common';
import CategoricalVariableOptions from './categorical-variable-options';
import { useStyles } from '../styles/categorical-variable.style';
import { CategoricalVariableType } from '../types/common';

type CategoricalVariableProps = {
onAdded: (data: CategoricalVariableType) => void
}

export default function CategoricalVariable(props: CategoricalVariableProps) {
const classes = useStyles()
//TODO: Avoid handling options separately?
const [options, setOptions] = useState([])

const { register, handleSubmit, reset, watch, errors } = useForm<CategoricalVariableType>();
Expand Down
170 changes: 101 additions & 69 deletions components/data-points.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,105 @@
import { Button, Card, CardContent, Table, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from "@material-ui/core";
import { ChangeEvent, useEffect, useState } from "react";
import { ExperimentType, DataPointType } from "../types/common";
import { Card, CardContent, Typography } from "@material-ui/core";
import { useEffect, useReducer } from "react";
import { dataPointsReducer, DataPointsState, DATA_POINTS_TABLE_EDITED, DATA_POINTS_TABLE_EDIT_CANCELLED, DATA_POINTS_TABLE_EDIT_TOGGLED, DATA_POINTS_TABLE_ROW_ADDED, DATA_POINTS_TABLE_ROW_DELETED } from "../reducers/data-points-reducer";
import { ExperimentType, DataPointType, TableDataPoint, TableDataRow, CombinedVariableType } from "../types/common";
import { EditableTable } from "./editable-table";

type DataPointProps = {
experiment: ExperimentType,
onAddDataPoints: (dataPoints: DataPointType[]) => void,
experiment: ExperimentType
onUpdateDataPoints: (dataPoints: DataPointType[][]) => void
}

export default function DataPoints(props: DataPointProps) {
const SCORE = "score"
const { experiment: { valueVariables, categoricalVariables, dataPoints } } = props
const variableNames: string[] = valueVariables.map(item => item.name)
.concat(categoricalVariables.map(item => item.name))
.concat(SCORE)
const [newDataPoints, setNewDataPoints] = useState<DataPointType[]>(createInitialNewPoints())
const SCORE = "score"

function createInitialNewPoints(): DataPointType[] {
return variableNames.map(name => {
export default function DataPoints(props: DataPointProps) {
const { experiment: { valueVariables, categoricalVariables, dataPoints }, onUpdateDataPoints } = props
const combinedVariables: CombinedVariableType[] = (valueVariables as CombinedVariableType[]).concat(categoricalVariables as CombinedVariableType[])

const emptyRow: TableDataRow = {
dataPoints: combinedVariables.map((variable, i) => {
return {
name,
value: ""
name: variable.name,
value: variable.options ? variable.options[0] : "",
options: variable.options,
}
})
}).concat({
name: SCORE,
value: "0",
options: undefined,
}),
isEditMode: true,
isNew: true,
}

const dataPointRows: TableDataRow[] = dataPoints.map((item, i) => {
return {
dataPoints: item.map((point: TableDataPoint, k) => {
return {
...point,
options: combinedVariables[k] ? combinedVariables[k].options : undefined,
}
}),
isEditMode: false,
isNew: false,
}
}
).concat(emptyRow as any)

function onAdd() {
props.onAddDataPoints(newDataPoints)
const initialState: DataPointsState = {
rows: dataPointRows,
prevRows: dataPointRows
}

function onNewPointChange(name: string, pointIndex: number, value: string) {
const newPoints = newDataPoints.map((point, index) => {
if (index !== pointIndex) {
return point
} else {
const newValue: any = point.name === SCORE ? [parseFloat(value)] as number[]: value as string
return {
name,
value: newValue
}
}
})
setNewDataPoints(newPoints)
const [state, dispatch] = useReducer(dataPointsReducer, initialState)

useEffect(() => {
updateDataPoints(state.rows.filter(item => !item.isNew) as TableDataRow[])
}, [state.rows])

function toggleEditMode(rowIndex: number) {
dispatch({ type: DATA_POINTS_TABLE_EDIT_TOGGLED, payload: rowIndex })
}

function cancelEdit(rowIndex: number) {
dispatch({ type: DATA_POINTS_TABLE_EDIT_CANCELLED, payload: rowIndex })
}

function edit(editValue: string, rowIndex: number, itemIndex: number) {
dispatch({ type: DATA_POINTS_TABLE_EDITED, payload: {
itemIndex,
rowIndex,
useArrayForValue: SCORE,
value: editValue
}})
}

function deleteRow(rowIndex: number) {
dispatch({ type: DATA_POINTS_TABLE_ROW_DELETED, payload: rowIndex })
}

function addRow(emptyRow: TableDataRow) {
dispatch({ type: DATA_POINTS_TABLE_ROW_ADDED, payload: emptyRow })
}

function updateDataPoints(dataRows: TableDataRow[]) {
onUpdateDataPoints(dataRows
.map((item, i) => {
return item.dataPoints.map(item => {
return {
name: item.name,
value: item.value,
} as DataPointType
})
})
)
}

function onEditConfirm(row: TableDataRow, rowIndex: number) {
if (row.isNew) {
addRow(emptyRow)
} else {
toggleEditMode(rowIndex)
}
}

return (
Expand All @@ -50,43 +109,16 @@ export default function DataPoints(props: DataPointProps) {
<Typography variant="h6" gutterBottom>
Data points
</Typography>

{variableNames.length > 1 &&
<>
<Table size="small">
<TableHead>
<TableRow>
{variableNames.map((name, index) =>
<TableCell key={index}>{name}</TableCell>
)}
</TableRow>
</TableHead>

<TableBody>
{dataPoints.map((points, pointsIndex) =>
<TableRow key={pointsIndex}>
{points.map((point, pointIndex) => {
if (point.name === SCORE) {
return <TableCell key={pointIndex}>{point.value[0]}</TableCell>
} else {
return <TableCell key={pointIndex}>{point.value}</TableCell>
}
})}
</TableRow>
)}
<TableRow>
{variableNames.map((name, index) =>
<TableCell key={index}>
<TextField fullWidth onChange={(e: ChangeEvent) => onNewPointChange(name, index, (e.target as HTMLInputElement).value)} />
</TableCell>
)}
</TableRow>

</TableBody>
</Table>
<br/>
<Button variant="outlined" onClick={() => onAdd()}>Add</Button>
</>

{combinedVariables.length > 0 &&
<EditableTable
rows={state.rows as TableDataRow[]}
useArrayForValue={SCORE}
onEdit={(editValue: string, rowIndex: number, itemIndex: number) => edit(editValue, rowIndex, itemIndex)}
onEditConfirm={(row: TableDataRow, rowIndex: number) => onEditConfirm(row, rowIndex)}
onEditCancel={(rowIndex: number) => cancelEdit(rowIndex)}
onToggleEditMode={(rowIndex: number) => toggleEditMode(rowIndex)}
onDelete={(rowIndex: number) => deleteRow(rowIndex)} />
}
</CardContent>
</Card>
Expand Down
39 changes: 39 additions & 0 deletions components/editable-table-cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { FormControl, MenuItem, Select, TableCell, TextField } from "@material-ui/core"
import { ChangeEvent } from "react"

type EditableTableCellProps = {
value: string
isEditMode: boolean
options?: string[]
onChange: (value: string) => void
}

export function EditableTableCell(props: EditableTableCellProps) {
const { value, isEditMode, options, onChange } = props

return (
<>
{isEditMode ?
<TableCell>
{options && options.length > 0 ?
<FormControl>
<Select
value={value}
onChange={(e: ChangeEvent<any>) => onChange(e.target.value as string)}
displayEmpty
inputProps={{ 'aria-label': 'select value' }}>
{options.map((item, i) => <MenuItem key={i} value={item}>{item}</MenuItem>)}
</Select>
</FormControl>
:
<TextField
value={value}
onChange={(e: ChangeEvent) => onChange("" + (e.target as HTMLInputElement).value)}/>
}
</TableCell>
:
<TableCell>{value}</TableCell>
}
</>
)
}
82 changes: 82 additions & 0 deletions components/editable-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { IconButton, Table, TableBody, TableCell, TableHead, TableRow } from "@material-ui/core"
import { EditableTableCell } from "./editable-table-cell"
import EditIcon from "@material-ui/icons/Edit"
import CheckCircleIcon from "@material-ui/icons/CheckCircle"
import CancelIcon from "@material-ui/icons/Cancel"
import AddIcon from "@material-ui/icons/Add"
import DeleteIcon from "@material-ui/icons/Delete";
import { TableDataRow } from "../types/common";

type EditableTableProps = {
rows: TableDataRow[]
useArrayForValue: string
onEdit: (editValue: string, rowIndex: number, itemIndex: number) => void
onEditConfirm: (row: TableDataRow, rowIndex: number) => void
onEditCancel: (rowIndex: number) => void
onToggleEditMode: (rowIndex: number) => void
onDelete: (rowIndex: number) => void
}

export function EditableTable(props: EditableTableProps) {
const { rows, useArrayForValue, onEdit, onEditConfirm, onEditCancel, onToggleEditMode, onDelete } = props

return (
<Table size="small">
<TableHead>
<TableRow>
{rows[0].dataPoints.map((item, index) =>
<TableCell key={index}>{item.name}</TableCell>
)}
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, rowIndex) =>
<TableRow key={rowIndex}>
{row.dataPoints.map((item, itemIndex) =>
<EditableTableCell
key={itemIndex}
value={item.name === useArrayForValue ? item.value[0] : item.value}
isEditMode={row.isEditMode}
options={item.options}
onChange={(value: string) => onEdit(value, rowIndex, itemIndex) }/>
)}
<TableCell key={rowIndex}>
{row.isEditMode ?
<>
<IconButton
size="small"
aria-label="confirm edit"
onClick={() => onEditConfirm(row, rowIndex)}>
{row.isNew ? <AddIcon color="primary" /> :
<CheckCircleIcon color="primary" />}
</IconButton>
<IconButton
size="small"
aria-label="cancle edit"
onClick={() => onEditCancel(rowIndex)}>
<CancelIcon color="primary" />
</IconButton>
</> :
<>
<IconButton
size="small"
aria-label="toggle edit"
onClick={() => onToggleEditMode(rowIndex)}>
<EditIcon color="primary" />
</IconButton>
<IconButton
size="small"
aria-label="delete"
onClick={() => onDelete(rowIndex)}>
<DeleteIcon color="primary" />
</IconButton>
</>
}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
}
2 changes: 1 addition & 1 deletion components/variable-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import ValueVariable from './value-variable';
import { Card, CardContent, Grid, IconButton, Radio, Typography } from "@material-ui/core"
import CloseIcon from "@material-ui/icons/Close";
import { useState } from "react"
import { CategoricalVariableType, ValueVariableType } from '../types/common';
import useStyles from '../styles/variable-editor.style';
import { CategoricalVariableType, ValueVariableType } from '../types/common';

type VariableEditorProps = {
addValueVariable: (valueVariable: ValueVariableType) => void
Expand Down
10 changes: 5 additions & 5 deletions pages/experiment/[experimentid].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { useStyles } from '../../styles/experiment.style';
import OptimizerModel from '../../components/optimizer-model';
import OptimizerConfigurator from '../../components/optimizer-configurator';
import { useEffect, useReducer, useState } from 'react';
import { VALUE_VARIABLE_ADDED, EXPERIMENT_DESCRIPTION_UPDATED, EXPERIMENT_NAME_UPDATED, EXPERIMENT_UPDATED, rootReducer, VALUE_VARIABLE_DELETED, CATEGORICAL_VARIABLE_ADDED, CATEGORICAL_VARIABLE_DELETED, CONFIGURATION_UPDATED, RESULT_REGISTERED, DATA_POINTS_ADDED } from '../../reducers/reducers';
import { ValueVariableType, ExperimentType, CategoricalVariableType, OptimizerConfig, ExperimentResultType, DataPointType } from '../../types/common';
import { VALUE_VARIABLE_ADDED, EXPERIMENT_DESCRIPTION_UPDATED, EXPERIMENT_NAME_UPDATED, EXPERIMENT_UPDATED, rootReducer, VALUE_VARIABLE_DELETED, CATEGORICAL_VARIABLE_ADDED, CATEGORICAL_VARIABLE_DELETED, CONFIGURATION_UPDATED, RESULT_REGISTERED, DATA_POINTS_ADDED, DATA_POINTS_UPDATED } from '../../reducers/reducers';
import { ExperimentType, OptimizerConfig, ExperimentResultType, DataPointType, ValueVariableType, CategoricalVariableType } from '../../types/common';
import { initialState } from '../../store';
import { Alert } from '@material-ui/lab';
import ModelEditor from '../../components/model-editor';
Expand Down Expand Up @@ -105,8 +105,8 @@ export default function Experiment() {
dispatch({ type: CONFIGURATION_UPDATED, payload: config})
}

function addDataPoints(dataPoints: DataPointType[]) {
dispatch({ type: DATA_POINTS_ADDED, payload: dataPoints})
function updateDataPoints(dataPoints: DataPointType[][]) {
dispatch({ type: DATA_POINTS_UPDATED, payload: dataPoints})
}

if (error) return <div>Failed to load experiment</div>;
Expand Down Expand Up @@ -165,7 +165,7 @@ export default function Experiment() {
<br/>
<DataPoints
experiment={state.experiment}
onAddDataPoints={(dataPoints: DataPointType[]) => addDataPoints(dataPoints)} />
onUpdateDataPoints={(dataPoints: DataPointType[][]) => updateDataPoints(dataPoints)}/>
<br/>
{state.experiment.results.plots.length > 0 &&
<Card>
Expand Down
Loading

0 comments on commit 4b046eb

Please sign in to comment.