Skip to content

Commit

Permalink
Merge pull request #345 from EpistasisLab/Experiments_AlgoDetailView
Browse files Browse the repository at this point in the history
Experiments algo detail view
  • Loading branch information
mgstauffer authored Oct 12, 2021
2 parents b3facea + d3fdc09 commit 6c110e9
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import React from 'react';
import { Form, Dropdown, Header } from 'semantic-ui-react';
import { Form, Dropdown, Header, Icon, Button } from 'semantic-ui-react';

function ExperimentFilters({
filters,
Expand All @@ -38,6 +38,17 @@ function ExperimentFilters({
<span className="filters">
<Form inverted>
<Form.Group>
<Form.Field
inline
color="blue"
style={{backgroundColor: 'transparent'}}
size="small"
compact
icon={filters.viewMode === "expanded" ? "minus square outline" : "plus square outline"}
control={Button}
className="reset"
onClick={() => updateQuery('viewMode', filters.viewMode === "simple" ? "expanded" : "simple")}
/>
<Form.Field
inline
label="Status:"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,25 @@ function ExperimentsTableHeader({
shouldDisplayParams,
shouldDisplayErrorMessage,
orderedParamKeys,
sort,
sortSingle,
updateQuery
}){
// for sorting to work, clickedColumn string must be defined as keyPath for value
// ex: scores-accuracy_score -> getIn([scores, accuracy_score])
const onSort = (clickedColumn) => {
let direction;
if(clickedColumn === sort.column) {
direction = sort.direction === 'ascending' ? 'descending' : 'ascending';
if(clickedColumn === sortSingle.column) {
direction = sortSingle.direction === 'ascending' ? 'descending' : 'ascending';
} else {
direction = 'ascending';
}

updateQuery('col', clickedColumn);
updateQuery('direction', direction);
updateQuery(sortSingle.columnKey, clickedColumn);
updateQuery(sortSingle.directionKey, direction);
};

const getIsSorted = (column) => {
return (sort.column === column && sort.direction) || null;
return (sortSingle.column === column && sortSingle.direction) || null;
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,20 @@ import { Table } from 'semantic-ui-react';
function ExperimentsTable({
experiments,
filters,
sort,
sortSingle,
updateQuery
}) {
//'experiments' is an array of experiments, and in the case of viewMode 'simple' will
// may have multiple different algorithm types. In the case of viewMode 'expanded' it
// will have experiments only of the same algorithm.
const selectedStatus = filters.status.selected;

const selectedDataset = filters.dataset.selected;

const selectedAlgorithm = filters.algorithm.selected;

let selectedAlgorithm = filters.viewMode === "simple" ? filters.algorithm.selected : experiments[0].algorithm;
const currentParameters = experiments[0].params;

const shouldDisplayQuality = selectedStatus === 'suggested';

const shouldDisplayAwards = selectedDataset !== 'all';

const shouldDisplayParams = selectedAlgorithm !== 'all' && Object.keys(currentParameters).length > 0;

let shouldDisplayParams = filters.viewMode === "expanded" || (selectedAlgorithm !== 'all' && Object.keys(currentParameters).length > 0);
const shouldDisplayErrorMessage = selectedStatus === 'fail';

const orderedParamKeys = Object.keys(currentParameters).sort();

return (
Expand All @@ -72,7 +67,7 @@ function ExperimentsTable({
shouldDisplayParams={shouldDisplayParams}
shouldDisplayErrorMessage={shouldDisplayErrorMessage}
orderedParamKeys={orderedParamKeys}
sort={sort}
sortSingle={sortSingle}
updateQuery={updateQuery}
/>
<ExperimentsTableBody
Expand Down
77 changes: 62 additions & 15 deletions lab/webapp/src/components/Experiments/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { withRouter } from 'react-router';
import {
getVisibleExperiments,
getFilters,
getSort
getSortAll
} from 'data/experiments';
import * as actions from 'data/experiments/actions';
import SceneHeader from '../SceneHeader';
Expand All @@ -40,18 +40,23 @@ import ExperimentFilters from './components/ExperimentFilters';
import ExperimentsTable from './components/ExperimentsTable';
import { Segment, Header, Loader } from 'semantic-ui-react';
import { hashHistory } from 'react-router';
import { formatAlgorithm } from 'utils/formatter';

class Experiments extends Component {
constructor(props) {
super(props);
this.updateQuery = this.updateQuery.bind(this);
this.resetQuery = this.resetQuery.bind(this);
this.getTables = this.getTables.bind(this);
}

componentDidMount() {
this.props.fetchExperiments();
}

/**
* URL/path query is used to pass props to this Experiments component, as best I understand.
*/
updateQuery(key, value) {
const { location } = this.props;
const nextLocation = Object.assign({}, location);
Expand All @@ -68,6 +73,11 @@ class Experiments extends Component {
if(key === 'dataset' && value !== 'all'){
this.updateQuery('prediction','all');
}
//Special check for settings viewMode. If we set 'expanded' mode,
// make sure the algorithm filter is set to all
if(key === 'viewMode' && value === 'expanded'){
this.updateQuery('algorithm','all');
}
}

resetQuery() {
Expand All @@ -79,8 +89,56 @@ class Experiments extends Component {
hashHistory.push(nextLocation);
}

/** Generate either a single table showing a single or all algorithms, or an array of tables with one for each algorithm type
* that has one or more experiments.
*/
getTables() {
const { experiments, fetchExperiments, filters, sortAll } = this.props;

if (experiments.list.length == 0 ) {
return (
<Header inverted size="small" content="No results available." />
)
}

// We expect to iterate below over the values in an object each of
// is an array of experiments.
// There's either a single value for viewMode 'simple', or one or more
// values for viewMode 'expanded' in which each array holds experiments
// of a single algorithm type. Note that these have been filtered by view filters.
let groupedExperiments = {};
if(filters.viewMode === "simple" ){
groupedExperiments = {simple: experiments.list}
}
else {
//
groupedExperiments = experiments.byAlgorithm;
}

let result = [];
Object.keys(groupedExperiments).forEach( key => {
let experiments = groupedExperiments[key];
if(experiments.length > 0){
result.push((
<Segment inverted attached="bottom" key={experiments[0]._id}>
<React.Fragment>
{filters.viewMode === "simple" ? undefined : <Header as='h3'>{formatAlgorithm(experiments[0].algorithm)}</Header>}
<ExperimentsTable
experiments={experiments}
filters={filters}
sortSingle={sortAll[key]}
updateQuery={this.updateQuery}
/>
</React.Fragment>
</Segment>
))
}
})
return result;
}

render() {
const { experiments, fetchExperiments, filters, sort } = this.props;
const { experiments, fetchExperiments, filters } = this.props;

if(experiments.isFetching) {
return (
Expand Down Expand Up @@ -108,18 +166,7 @@ class Experiments extends Component {
resetQuery={this.resetQuery}
/>
</Segment>
<Segment inverted attached="bottom">
{experiments.list.length > 0 ? (
<ExperimentsTable
experiments={experiments.list}
filters={filters}
sort={sort}
updateQuery={this.updateQuery}
/>
) : (
<Header inverted size="small" content="No results available." />
)}
</Segment>
{this.getTables()}
</div>
);
}
Expand All @@ -128,7 +175,7 @@ class Experiments extends Component {
const mapStateToProps = (state, props) => ({
experiments: getVisibleExperiments(state, props),
filters: getFilters(state, props),
sort: getSort(state, props)
sortAll: getSortAll(state, props)
});

export { Experiments };
Expand Down
11 changes: 6 additions & 5 deletions lab/webapp/src/components/Results/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,19 @@ class Results extends Component {
let testList = [];
let expScores = experiment.data.scores;

keyList.forEach(scoreKey => {
if(typeof expScores[scoreKey].toFixed === 'function'){
if(typeof(expScores) === 'object'){
keyList.forEach(scoreKey => {
if(expScores[scoreKey] && typeof expScores[scoreKey].toFixed === 'function'){
let tempLabel;
scoreKey.includes('train')
? tempLabel = 'Train (' + expScores[scoreKey].toFixed(2) + ')'
: tempLabel = 'Test (' + expScores[scoreKey].toFixed(2) + ')';
testList.push(
[tempLabel, expScores[scoreKey]]
)
);
}
}
);
});
}

return testList;
}
Expand Down
102 changes: 90 additions & 12 deletions lab/webapp/src/data/experiments/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ import selected from './selected';
import { formatDataset, formatAlgorithm } from 'utils/formatter';
import experiment from './selected';

/**
* List/array of all experiments
*/
const list = (state = [], action) => {
switch(action.type) {
case FETCH_EXPERIMENTS_SUCCESS:
Expand All @@ -56,6 +59,28 @@ const list = (state = [], action) => {
}
};

/**
* Parse the array of experiments and group by algorithm type.
* Returns an object, with each key named for an algorithm and
* holding an array of experiments that use that algorithm.
*/
const byAlgorithm = (state = {}, action) => {
switch(action.type) {
case FETCH_EXPERIMENTS_SUCCESS:
//payload is an array of experiment objects
let result = {}
action.payload.forEach( exp => {
if( !result.hasOwnProperty(exp.algorithm)) {
result[exp.algorithm] = []
}
result[exp.algorithm].push(exp);
})
return result;
default:
return state;
}
}

const isFetching = (state = false, action) => {
switch(action.type) {
case FETCH_EXPERIMENTS_REQUEST:
Expand All @@ -79,6 +104,7 @@ const error = (state = null, action) => {

const experiments = combineReducers({
list,
byAlgorithm,
isFetching,
error,
selected
Expand Down Expand Up @@ -143,6 +169,12 @@ export const getFilters = createSelector(
filters.status.options = statusOptions.map(option => ({ text: option, value: option }));
filters.status.selected = query.status || 'all';

// Top-level options for showing all experiments by algorithm in separate tables,
// with algorithm detail views
// "simple" is for single table including multiple algorithms (if algorithm filter is 'all').
// "expanded" for multiple tables, one for each algorithm, and showing algorithm details
filters.viewMode = query.viewMode || "simple";

return filters;
}
);
Expand All @@ -159,23 +191,69 @@ const formatOptionText = (filterKey, text) => {
return text;
};

export const getSort = createSelector(
[getQuery],
(query) => ({
column: query.col || null,
direction: query.direction || null
})
/**
* Get sort settings for simple and expanded view. Returns object with
* one key for 'simple' sorting for showing all experiments
* together, and a key for each algorithm used in the
* current experiments, with the current sorting setting for each one.
*/
export const getSortAll = createSelector(
[getExperiments, getQuery],
(experiments, query) => {
let result = {};
let colPrefix = 'col_';
let dirPrefix = 'direction_';

// First create the sorting set for the simple view, i.e. all experiments in one table
result.simple = {
column: query[colPrefix+'simple'] || null,
columnKey: colPrefix+'simple', //Key/id used in query string
direction: query[dirPrefix+'simple'] || null,
directionKey: dirPrefix+'simple'
}

//For each algorithm used in the list of experiments, create a sorting set so
// we can view experiments in separate tables for algorithm, and sort each
// table separately.
Object.keys(experiments.byAlgorithm).forEach( alg => {
let colKey = colPrefix + alg;
let dirKey = dirPrefix + alg;
result[alg] = {
column: query[colKey] || null,
columnKey: colKey,
direction: query[dirKey] || null,
directionKey: dirKey
}
})

return result;
}
);

export const getVisibleExperiments = createSelector(
[getExperiments, getFilters, getSort],
(experiments, filters, sort) => {
const sortedList = experiments.list
[getExperiments, getFilters, getSortAll],
(experiments, filters, sortAll) => {
//Sort the list of all experiments for simple view
const sortedSimple = experiments.list
.slice(0) // clone array
.filter(filterBy(filters))
.sort(sortBy(sort));

return Object.assign({}, experiments, { list: sortedList });
.sort(sortBy(sortAll.simple));

//Sort each algorithm's list separately for expanded view
//And remove algorithms with no experiments to view after filtering.
let byAlg = Object.assign({}, experiments.byAlgorithm);
Object.keys(byAlg).forEach( alg => {
const sorted = byAlg[alg]
.slice(0)
.filter(filterBy(filters))
.sort(sortBy(sortAll[alg])) //separate sort settings for each algorithm
if(sorted.length > 0)
byAlg[alg] = sorted;
else
delete byAlg[alg];
})

return Object.assign({}, experiments, { list: sortedSimple, byAlgorithm: byAlg });
}
);

Expand Down

0 comments on commit 6c110e9

Please sign in to comment.