From 952578680ac65c52d8cab5103bce26e65a0bf89b Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 16 Oct 2018 16:29:20 -0700 Subject: [PATCH] Remove heatmap, add diffexp info to histograms (#339) * Create Release_Validation_Recipe.md * compose middle ware for redux devtools * update package.json and lock * remove heatmap, add to left side bar * redux devtools extension --- client/package-lock.json | 63 +++++----- client/package.json | 1 + .../components/brushableHistogram/index.js | 113 +++++++++++++++++- .../src/components/categorical/categorical.js | 9 +- .../src/components/continuous/continuous.js | 2 - .../components/expression/diffExpHeatmap.js | 2 +- client/src/components/geneExpression/index.js | 5 + client/src/components/graph/graph.js | 8 +- client/src/components/leftsidebar.js | 82 ++++--------- client/src/reducers/index.js | 6 +- docs/Release_Validation_Recipe.md | 75 ++++++++++++ 11 files changed, 266 insertions(+), 100 deletions(-) create mode 100644 docs/Release_Validation_Recipe.md diff --git a/client/package-lock.json b/client/package-lock.json index e5a6a17e..637e0de5 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2054,7 +2054,7 @@ }, "babel-plugin-syntax-object-rest-spread": { "version": "6.13.0", - "resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", "dev": true }, @@ -2489,7 +2489,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -2526,7 +2526,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -2580,7 +2580,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -3258,7 +3258,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -3271,7 +3271,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -3945,7 +3945,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -4522,7 +4522,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { @@ -4699,7 +4699,7 @@ }, "events": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" }, "evp_bytestokey": { @@ -4993,7 +4993,7 @@ }, "finalhandler": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "dev": true, "requires": { @@ -5717,7 +5717,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, @@ -6096,7 +6096,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -6172,7 +6172,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { @@ -6395,7 +6395,7 @@ }, "is-builtin-module": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { @@ -7394,7 +7394,7 @@ }, "json5": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, @@ -7484,7 +7484,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -7813,7 +7813,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -7938,7 +7938,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, @@ -7983,7 +7983,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -9695,7 +9695,7 @@ }, "parse-asn1": { "version": "5.1.1", - "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", "dev": true, "requires": { @@ -10291,7 +10291,7 @@ }, "react-redux": { "version": "5.0.7", - "resolved": "http://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz", "integrity": "sha512-5VI8EV5hdgNgyjfmWzBbdrqUkrVRKlyTKk1sGH3jzM2M2Mhj/seQgPXaz6gVAj2lz/nz688AdTqMO18Lr24Zhg==", "requires": { "hoist-non-react-statics": "^2.5.0", @@ -10355,7 +10355,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -10698,6 +10698,11 @@ "symbol-observable": "^1.2.0" } }, + "redux-devtools-extension": { + "version": "2.13.5", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.5.tgz", + "integrity": "sha512-QQ9BRy77oURHMdGys9rfQcCQDzXZ1T4oW+eUyE5Cg7DNVau69HJzc4YNDMOmpi0Dzpi1zOQgQ2rUpgJta4Lfqg==" + }, "redux-thunk": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", @@ -11368,7 +11373,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -11555,7 +11560,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -12009,7 +12014,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -12245,7 +12250,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, @@ -12884,7 +12889,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -13542,7 +13547,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { @@ -13650,7 +13655,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { diff --git a/client/package.json b/client/package.json index 25142f8d..f0531953 100644 --- a/client/package.json +++ b/client/package.json @@ -47,6 +47,7 @@ "react-icons": "^3.2.0", "react-redux": "^5.0.6", "redux": "^4.0.1", + "redux-devtools-extension": "^2.13.5", "redux-thunk": "^2.2.0", "regl": "^1.3.1", "scroll-speed": "^1.0.0", diff --git a/client/src/components/brushableHistogram/index.js b/client/src/components/brushableHistogram/index.js index f78e6c73..f1f97f78 100644 --- a/client/src/components/brushableHistogram/index.js +++ b/client/src/components/brushableHistogram/index.js @@ -16,7 +16,10 @@ import actions from "../../actions"; @connect(state => ({ world: state.controls.world, + scatterplotXXaccessor: state.controls.scatterplotXXaccessor, + scatterplotYYaccessor: state.controls.scatterplotYYaccessor, crossfilter: state.controls.crossfilter, + differential: state.differential, initializeRanges: _.get(state.controls.world, "summary.obs"), colorAccessor: state.controls.colorAccessor, colorScale: state.controls.colorScale, @@ -208,8 +211,38 @@ class HistogramBrush extends React.Component { } } + handleSetGeneAsScatterplotX() { + return () => { + const { dispatch, field } = this.props; + dispatch({ + type: "set scatterplot x", + data: field + }); + }; + } + + handleSetGeneAsScatterplotY() { + return () => { + const { dispatch, field } = this.props; + dispatch({ + type: "set scatterplot y", + data: field + }); + }; + } + render() { - const { field, ranges, colorAccessor, isUserDefined } = this.props; + const { + field, + colorAccessor, + isUserDefined, + isDiffExp, + diffExp_avgDiff, + diffExp_set1AvgExp, + diffExp_set2AvgExp, + scatterplotXXaccessor, + scatterplotYYaccessor + } = this.props; return (
) : null} + {isDiffExp ? ( + + + 1: + {` ${diffExp_set1AvgExp.toPrecision(2)}`} + + + 2: + {` ${diffExp_set2AvgExp.toPrecision(2)}`} + + + {`Av. Diff: ${diffExp_avgDiff.toFixed(2)}`} + + + X + + + Y + + + ) : null}
{ this.setState({ isExpanded: !isExpanded }); @@ -155,7 +153,7 @@ class Category extends React.Component { // backgroundColor: this.props.colorAccessor === this.props.metadataField ? globals.brightBlue : "inherit", display: "inline-block", position: "relative", - top: -1, + top: 2, cursor: "pointer" }} > @@ -196,6 +194,7 @@ class Categories extends React.Component { // overflow: "auto", }} > +

Categorical Metadata

{_.map(ranges, (value, key) => { const isColorField = key.includes("color") || key.includes("Color"); if (value.options && !isColorField && key !== "name") { diff --git a/client/src/components/continuous/continuous.js b/client/src/components/continuous/continuous.js index 74242411..37f0d1ba 100644 --- a/client/src/components/continuous/continuous.js +++ b/client/src/components/continuous/continuous.js @@ -37,8 +37,6 @@ class Continuous extends React.Component { }; } - componentDidUpdate() {} - render() { const { ranges, obsAnnotations, schema } = this.props; if (schema && !this.continuousChecked) { diff --git a/client/src/components/expression/diffExpHeatmap.js b/client/src/components/expression/diffExpHeatmap.js index c416a7c2..002a9711 100644 --- a/client/src/components/expression/diffExpHeatmap.js +++ b/client/src/components/expression/diffExpHeatmap.js @@ -3,7 +3,7 @@ import React from "react"; import _ from "lodash"; import memoize from "memoize-one"; import { connect } from "react-redux"; -import ReactAutocomplete from "react-autocomplete"; /* http://emilebres.github.io/react-virtualized-checkbox/ */ +// import ReactAutocomplete from "react-autocomplete"; /* http://emilebres.github.io/react-virtualized-checkbox/ */ import getContrast from "font-color-contrast"; // https://www.npmjs.com/package/font-color-contrast import { FaPaintBrush } from "react-icons/fa"; import * as d3 from "d3"; diff --git a/client/src/components/geneExpression/index.js b/client/src/components/geneExpression/index.js index f1466648..34d0205f 100644 --- a/client/src/components/geneExpression/index.js +++ b/client/src/components/geneExpression/index.js @@ -93,6 +93,8 @@ class GeneExpression extends React.Component { style={{ display: "inline-block", marginLeft: 7, + position: "relative", + top: 5, color: globals.brightBlue, fontSize: 22 }} @@ -135,6 +137,9 @@ class GeneExpression extends React.Component { field={name} ranges={d3.extent(values)} isDiffExp + diffExp_avgDiff={value[1]} + diffExp_set1AvgExp={value[4]} + diffExp_set2AvgExp={value[5]} /> ); } diff --git a/client/src/components/graph/graph.js b/client/src/components/graph/graph.js index 1ae48e56..80dd1c4e 100644 --- a/client/src/components/graph/graph.js +++ b/client/src/components/graph/graph.js @@ -393,7 +393,9 @@ class Graph extends React.Component { border: mode === "brush" ? "1px solid black" : "1px solid white", backgroundColor: "white", - padding: 5, + padding: "5px 5px 2px 5px", + position: "relative", + top: 2, marginRight: 10, borderRadius: 3 }} @@ -413,7 +415,9 @@ class Graph extends React.Component { border: mode === "zoom" ? "1px solid black" : "1px solid white", backgroundColor: "white", - padding: 5, + padding: "5px 5px 2px 5px", + position: "relative", + top: 2, marginRight: 10, borderRadius: 3 }} diff --git a/client/src/components/leftsidebar.js b/client/src/components/leftsidebar.js index b47aa31f..4f9cbbc7 100644 --- a/client/src/components/leftsidebar.js +++ b/client/src/components/leftsidebar.js @@ -12,7 +12,9 @@ import DynamicScatterplot from "./scatterplot/scatterplot"; @connect(state => ({ responsive: state.responsive, - datasetTitle: _.get(state.config, "displayNames.dataset") + datasetTitle: _.get(state.config, "displayNames.dataset"), + scatterplotXXaccessor: state.controls.scatterplotXXaccessor, + scatterplotYYaccessor: state.controls.scatterplotYYaccessor })) class LeftSideBar extends React.Component { constructor(props) { @@ -24,86 +26,48 @@ class LeftSideBar extends React.Component { render() { const { currentTab } = this.state; - const { responsive, datasetTitle } = this.props; + const { + responsive, + datasetTitle, + scatterplotXXaccessor, + scatterplotYYaccessor + } = this.props; /* this magic number should be made less fragile, if cellxgene logo or tabs change, this must as well */ - const metadataSectionPadding = currentTab === "metadata" ? 88 : 450; + const metadataSectionPadding = + scatterplotXXaccessor && scatterplotYYaccessor ? 450 : 0; return (

- cellxgene   + cellxgene:   {datasetTitle}

-
- - -
- {currentTab === "metadata" ? : null} - {currentTab === "metadata" ? : null} - {currentTab === "metadata" ? : null} - {currentTab === "expression" ? : null} + + +
- {currentTab === "expression" ? : null} + {scatterplotXXaccessor && scatterplotYYaccessor ? ( + + ) : null}
{currentTab === "metadata" ? : null} diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 5aad19d8..5e4b7e25 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -3,6 +3,7 @@ import { combineReducers, createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import updateURLMiddleware from "../middleware/updateURLMiddleware"; import updateCellColors from "../middleware/updateCellColors"; +import { composeWithDevTools } from "redux-devtools-extension"; import config from "./config"; import differential from "./differential"; @@ -18,8 +19,9 @@ const Reducer = combineReducers({ const store = createStore( Reducer, - window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), - applyMiddleware(thunk, updateURLMiddleware, updateCellColors) + composeWithDevTools( + applyMiddleware(thunk, updateURLMiddleware, updateCellColors) + ) ); export default store; diff --git a/docs/Release_Validation_Recipe.md b/docs/Release_Validation_Recipe.md new file mode 100644 index 00000000..b8bed44d --- /dev/null +++ b/docs/Release_Validation_Recipe.md @@ -0,0 +1,75 @@ +# Pre-release Validation Plan +_This document contains a general purpose test plan for validating cellxgene prior to release_. + +All steps are expected to pass with no errors or malfunctions. Tester should check: +* CLI for errors (eg, build error, engine error) +* Browser console for errors +* Expected CLI and/or browser UI function + +## [Backend Test](#backend-test) + +### Getting started build & install validation: +Goal: validate build, install and example demo correctness, per Getting Started instructions. + +1. Clean clone of the cellxgene repo into a local directory +2. Follow build & install instructions from the Getting Started guide + * Confirm no build or install errors + * Confirm `cellxgene --help` functions correctly +3. Follow the example data set demo startup from Getting Started guide and confirm front-end data loads correctly +4. Confirm all package (and other) version dependencies are correct and match + +### Tabula Muris +Goal: basic functional validation of b/e functions using Tabula Muris data and the scanpy engine. + +1. Precondition: cellxgene built & installed. +2. Load the Tabula Muris data set: `cellxgene --title 'T. Muris' scanpy directory-name/` +3. Verify all metadata selectors display and have correct type/options: + * *TODO:* _need list of metadata and their type_ + * ... +4. Verify default graph display has expected layout. _need screen shot of expected layout_ +5. Verify selection controls work as expected: + * continuous metadata field + * categorical metadata field + * graph/lasso select +6. Verify color by metadata type +7. Select two cell sets and confirm differential expression compute succeeds +6. Verify expression scatter plot is correct + + +## [Front-end Compatibility Test](#frontend-compatability-test) +Goal: verify front-end UI compatibility with a given browser variant/version/platform. + +1. Start back-end on PBM3K data set +2. Load UI +3. Verify all major UI modes/functions: + * Graph display + * Metadata selector display + * Title display + * Selection - single and multiple fields - correctly display in cluster graph + * Regraph & reset function correctly + * Differential expression calc & scatter plot display + * All graphs maintain consistent selection state + * All selection widgets (eg, continuous metadata selector) maintain correct status (consistent with graph displays) +4. Verify overall performance is reasonable/interactive +5. Verify no errors on CLI or browser console + + +## [End-to-end Functional Test](#e2e-functional-test) +Goal: confirm end-to-end functional behavior is as expected. + +### ScanPy engine + +1. Basic data load and display + * ... +2. Select & multi-select of metadata and coordinates + * ... +3. Color by metadata + * ... +4. Regraph / reset + * ... +5. Differential expression: scatterplot, top-N genes, etc. + * ... +6. Color by expression + * ... +7. Arbitrary gene expression + * ...