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

Experiments algo detail view #345

Merged
merged 4 commits into from
Oct 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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