Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/bulk data import export #93

Merged
merged 15 commits into from
Sep 9, 2021
Merged
106 changes: 61 additions & 45 deletions components/data-points.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { EditableTable } from "./editable-table";
import SwapVertIcon from '@material-ui/icons/SwapVert';
import { TitleCard } from './title-card';
import useStyles from "../styles/data-points.style";
import DownloadCSVButton from "./download-csv-button";
import UploadCSVButton from "./upload-csv-button";

type DataPointProps = {
valueVariables: ValueVariableType[]
Expand All @@ -20,32 +22,36 @@ type UpdateFnType = (rowIndex: number, ...args: any[]) => void
const SCORE = "score"

export default function DataPoints(props: DataPointProps) {
const { valueVariables, categoricalVariables, dataPoints, onUpdateDataPoints} = props
const { valueVariables, categoricalVariables, dataPoints, onUpdateDataPoints } = props
const classes = useStyles()
const [state, dispatch] = useReducer(dataPointsReducer, { rows: [], prevRows: [] })
const isLoadingState = state.rows.length === 0
const global = useGlobal()
const newestFirst = global.state.dataPointsNewestFirst

useEffect(() => {
dispatch({ type: 'setInitialState', payload: buildState()})
dispatch({ type: 'setInitialState', payload: buildState(dataPoints) })
}, [valueVariables, categoricalVariables])

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

const buildState = (dataPoints: DataPointType[][]): DataPointsState => {
const combinedVariables: CombinedVariableType[] = buildCombinedVariables()
const emptyRow: TableDataRow = buildEmptyRow()
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,
}
return {
dataPoints: item.map((point: TableDataPoint, k) => {
return {
...point,
options: combinedVariables[k] ? combinedVariables[k].options : undefined,
}
}),
isEditMode: false,
isNew: false,
}
}
).concat(emptyRow as any)

return {
Expand All @@ -54,7 +60,7 @@ export default function DataPoints(props: DataPointProps) {
}
}

const buildCombinedVariables = (): CombinedVariableType[] => {
const buildCombinedVariables = (): CombinedVariableType[] => {
return (valueVariables as CombinedVariableType[]).concat(categoricalVariables as CombinedVariableType[])
}

Expand All @@ -76,38 +82,36 @@ export default function DataPoints(props: DataPointProps) {
}
}

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

function toggleEditMode(rowIndex: number) {
const toggleEditMode = (rowIndex: number) => {
dispatch({ type: 'DATA_POINTS_TABLE_EDIT_TOGGLED', payload: rowIndex })
}

function cancelEdit(rowIndex: number) {
const cancelEdit = (rowIndex: number) => {
dispatch({ type: 'DATA_POINTS_TABLE_EDIT_CANCELLED', payload: rowIndex })
}

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

const deleteRow = (rowIndex: number) => {
dispatch({ type: 'DATA_POINTS_TABLE_ROW_DELETED', payload: rowIndex })
}

function addRow(emptyRow: TableDataRow) {
const addRow = (emptyRow: TableDataRow) => {
dispatch({ type: 'DATA_POINTS_TABLE_ROW_ADDED', payload: emptyRow })
}

function updateDataPoints(dataRows: TableDataRow[]) {
const updateDataPoints = (dataRows: TableDataRow[]) => {
onUpdateDataPoints(dataRows
.map((item, i) => {
.map(item => {
return item.dataPoints.map(item => {
return {
name: item.name,
Expand All @@ -118,37 +122,49 @@ export default function DataPoints(props: DataPointProps) {
)
}

function onEditConfirm(row: TableDataRow, rowIndex: number) {
const onEditConfirm = (row: TableDataRow, rowIndex: number) => {
if (row.isNew) {
addRow(buildEmptyRow())
} else {
updateRow(rowIndex, toggleEditMode)
}
}

function updateRow(index: number, updateFn: UpdateFnType, ...args: any[]) {
const updateRow = (index: number, updateFn: UpdateFnType, ...args: any[]) => {
const rowIndex = newestFirst ? state.rows.length - 1 - index : index
updateFn(rowIndex, ...args)
}

const updateTable = (dataPoints: DataPointType[][]) => {
dispatch({ type: 'setInitialState', payload: buildState(dataPoints) })
}

return (
<TitleCard title={
<>
Data points
<Tooltip title="Reverse order">
<IconButton
size="small"
className={classes.titleButton}
onClick={() => global.dispatch({ type: 'setDataPointsNewestFirst', payload: !global.state.dataPointsNewestFirst })}>
<SwapVertIcon fontSize="small" className={classes.titleIcon} />
</IconButton>
</Tooltip>
<Box display="flex" justifyContent="space-between">
<Box>
Data points
</Box>
<Box>
<DownloadCSVButton light/>
<UploadCSVButton light onUpload={(dataPoints: DataPointType[][]) => updateTable(dataPoints)} />
<Tooltip title="Reverse order">
<IconButton
size="small"
className={classes.titleButton}
onClick={() => global.dispatch({ type: 'setDataPointsNewestFirst', payload: !global.state.dataPointsNewestFirst })}>
<SwapVertIcon fontSize="small" className={classes.titleIcon} />
</IconButton>
</Tooltip>
</Box>
</Box>
</>
}>
{buildCombinedVariables().length === 0 && "Data points will appear here"}
{buildCombinedVariables().length > 0 && isLoadingState &&
<CircularProgress size={24}/>
}
<CircularProgress size={24} />
}
{buildCombinedVariables().length > 0 && !isLoadingState &&
<Box className={classes.tableContainer}>
<EditableTable
Expand Down
24 changes: 24 additions & 0 deletions components/download-csv-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { IconButton, Tooltip } from '@material-ui/core';
import { useExperiment } from '../context/experiment-context';
import { dataPointsToCSV } from '../utility/converters';
import { saveCSVToLocalFile } from '../utility/save-to-local-file';
import GetAppIcon from '@material-ui/icons/GetApp';

interface DownloadCSVButtonProps {
light?: boolean
}

const DownloadCSVButton = ({ light }: DownloadCSVButtonProps) => {
const { state: {experiment: {id, dataPoints}} } = useExperiment()
return <Tooltip title="Download CSV">
<IconButton
size="small"
onClick={() => saveCSVToLocalFile(dataPointsToCSV(dataPoints), id + ".csv")}
>
<GetAppIcon fontSize="small" style={{ color: light ? 'white' : '' }}/>
</IconButton>
</Tooltip>

}

export default DownloadCSVButton
4 changes: 2 additions & 2 deletions components/experiment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import React, { useState, useEffect } from 'react';
import { ValueVariableType, CategoricalVariableType, OptimizerConfig, DataPointType } from '../types/common';
import LoadingExperiment from './loading-experiment';
import { NextExperiments } from './next-experiment';
import saveToLocalFile from '../utility/save-to-local-file';
import { saveObjectToLocalFile } from '../utility/save-to-local-file';
import LoadingButton from './loading-button';
import { theme } from '../theme/theme';
import { Plots } from './plots';
Expand Down Expand Up @@ -51,7 +51,7 @@ export default function Experiment(props: ExperimentProps) {
}, [experiment, lastSavedExperiment])

const onDownload = () => {
saveToLocalFile(experiment, experiment.id)
saveObjectToLocalFile(experiment, experiment.id)
}

const onSave = async () => {
Expand Down
45 changes: 45 additions & 0 deletions components/upload-csv-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { IconButton, Input, Tooltip } from '@material-ui/core'
import { useExperiment } from '../context/experiment-context';
import { DataPointType } from '../types/common';
import { csvToDataPoints } from '../utility/converters';
import PublishIcon from '@material-ui/icons/Publish';

const readFile = (file, dataHandler) => {
var result = ""
if (file) {
const reader = new FileReader()
reader.onload = e => dataHandler(e.target.result as string)
reader.readAsText(file)
}
return result
}
interface UploadCSVButtonProps {
light?: boolean
onUpload: (dataPoints: DataPointType[][]) => void
}

const UploadCSVButton = ({ onUpload, light } : UploadCSVButtonProps) => {
const { state: { experiment: { valueVariables, categoricalVariables } } } = useExperiment()
const handleFileUpload = e => readFile(e.target.files[0], data => onUpload(csvToDataPoints(data, valueVariables, categoricalVariables)))

return <Tooltip title="Upload CSV">
<IconButton
component="label"
size="small"
>
<PublishIcon fontSize="small" style={{ color: light ? 'white' : '' }}/>
<Input
type="file"
value=""
style={{ display: 'none' }}
inputProps={{
accept:
".csv"
}}
onChange={handleFileUpload}
/>
</IconButton>
</Tooltip>
}

export default UploadCSVButton
4 changes: 2 additions & 2 deletions types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type ValueVariableType = {
max: number
}

export type VariableType = CategoricalVariableType | ValueVariableType
export type VariableType = CategoricalVariableType | ValueVariableType

export type OptimizerConfig = {
baseEstimator: string
Expand Down Expand Up @@ -66,7 +66,7 @@ export type ScoreDataPointType = {

export type SpaceType = {type: string, name:string, from?: number, to?: number, categories?: string[]}[]

export type TableDataPointValue = string | number | number[]
export type TableDataPointValue = string | number | number[]

export type TableDataPoint = {
name: string
Expand Down
3 changes: 3 additions & 0 deletions utility/__snapshots__/converters.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`converters csvToDataPoints should fail if header is missing 1`] = `"Headers does not match Sukker,Peber,Hvedemel,Kunde,score !== Sukker,Hvedemel,Kunde,score"`;
Loading