diff --git a/.travis.yml b/.travis.yml
index 6fb222c1..00b5150b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,6 +14,6 @@ script:
- set -eo pipefail
- flake8 server/app/
- pytest -s server/test/test_filter.py server/test/test_scanpy_engine.py
- - cellxgene &
+ - cellxgene scanpy example-dataset/ &
- for i in {1..90}; do if http :5005/api/v0.1/initialize > /dev/null; then break; else echo "Waiting for server..."; sleep 1; fi; done
- pytest server/test/test_api.py
diff --git a/README.md b/README.md
index 43c249ed..b7dc89cd 100644
--- a/README.md
+++ b/README.md
@@ -1,38 +1,68 @@
# cellxgene
-A React + Redux web application for exploring large scale single cell RNA sequence data.
+### An interactive, performant explorer for single cell transcriptomics data.
-### Requirements
+
+cellxgene is an open-source experiment in how to bring powerful tools from modern web development to visualize and explore large single-cell transcriptomics datasets.
+Started in the context of the Human Cell Atlas Consortium, cellxgene hopes to both enable scientists to explore their data and to equip developers with scalable, reusable patterns and frameworks for visualizing large scientific datasets.
+
+## Features
+
+* **Visualization at scale:** built with [WebGL](https://www.khronos.org/webgl/), [React](https://reactjs.org/) & [Redux](https://redux.js.org/) to handle visualization of at least 1 million cells.
+
+* **Interactive exploration:** select, cross-filter, and compare subsets of your data with performant indexing and data handling.
+
+* **Flexible API:** the cellxgene client-server model is designed to support a range of existing analysis packages for backend computational tasks (eg scanpy), integrated with client-side visualization via a [REST API](https://restfulapi.net/).
+
+
+## Getting Started
+
+**Requirements**
- OS: OSX, Windows, Linux
- python 3.6
- npm
-- Google Chrome
+- Google Chrome
+**Clone project**
+
+ git clone https://github.com/chanzuckerberg/cellxgene.git
-## Installation
-
-#### clone project
-
- git clone https://github.com/chanzuckerberg/cellxgene.git
-
-#### install client
-
+**Install client**
+
cd cellxgene
- ./bin/build-client
+ ./bin/build-client
-#### To use with virtual env for python (optional, but recommended)
+**To use with virtual env for python**
+(optional, but recommended)
+
+ ENV_NAME=cellxgene
+ python3 -m venv ${ENV_NAME}
+ source ${ENV_NAME}/bin/activate
- ENV_NAME=cellxgene
- python3 -m venv ${ENV_NAME}
- source ${ENV_NAME}/bin/activate
+**Install server**
+
+ python3 setup.py install
-#### install server
+**Run (with demo data)**
+
+ cellxgene --title PBMC3K scanpy example-dataset/
+*In google chrome, navigate to the viewer via the web address printed in your console.
+E.g.,* `Running on http://0.0.0.0:5005/`
- python3 setup.py install
+**Help**
+
+ cellxgene --help
+_For help with the scanpy engine_
+
+ cellxgene scanpy --help
-#### run (with demo data)
+## Contributing
+We warmly welcome contributions from the community. Please submit any bug reports and feature requests through github issues. Please submit any direct contributions via a branch + pull request.
- cellxgene
-
-
-*Thanks to Alex Wolf his help with test data*
+## Inspiration and collaboration
+We’ve been inspired by several other related efforts in this space, including the [UCSC Cell Browswer](http://cells.ucsc.edu/), [Cytoscape](http://www.cytoscape.org/), [Xena](https://xena.ucsc.edu/), [ASAP](https://asap.epfl.ch/), [Gene Pattern](http://genepattern-notebook.org/), & many others; we hope to explore collaborations where useful.
+
+## Reuse
+This project was started with the sole goal of empowering the scientific community to explore and understand their data. As such, we whole-heartedly encourage other scientific tool builders to adopt the patterns, tools, and code from this project, and reach out to us with ideas or questions using Github Issues or Pull Requests. All code is freely available for reuse under the [MIT license](https://opensource.org/licenses/MIT).
+
+*We thank Alex Wolf for the demo dataset.*
diff --git a/client/package.json b/client/package.json
index cffd9aff..c0ff9a79 100644
--- a/client/package.json
+++ b/client/package.json
@@ -33,9 +33,10 @@
"express": "^4.14.0",
"font-color-contrast": "^1.0.3",
"gl-mat4": "^1.1.4",
- "gl-matrix": "^2.4.0",
+ "gl-matrix": "^2.7.1",
"halogen": "^0.2.0",
"hsv2rgb": "^1.1.0",
+ "jest": "^23.4.1",
"key-pressed": "0.0.1",
"lodash": "^4.17.4",
"luma.gl": "^4.0.3",
@@ -43,9 +44,9 @@
"mouse-pressed": "^1.0.0",
"orbit-camera": "^1.0.0",
"query-string": "^5.0.0",
- "react": "^15.3.0",
+ "react": "^16.4.1",
"react-autocomplete": "^1.7.2",
- "react-dom": "^15.3.0",
+ "react-dom": "^16.4.1",
"react-helmet": "^5.2.0",
"react-hot-loader": "^3.0.0-beta.7",
"react-icons": "^2.2.7",
@@ -57,6 +58,7 @@
"scroll-speed": "^1.0.0",
"serve-favicon": "^2.3.0",
"urijs": "^1.19.0",
+ "webpack-hot-middleware": "^2.22.3",
"whatwg-fetch": "^2.0.3"
},
"devDependencies": {
diff --git a/client/src/components/app.js b/client/src/components/app.js
index 56ed2754..73fdaa65 100644
--- a/client/src/components/app.js
+++ b/client/src/components/app.js
@@ -4,7 +4,7 @@ import _ from "lodash";
import Helmet from "react-helmet";
import Container from "./framework/container";
import { connect } from "react-redux";
-import PulseLoader from "halogen/PulseLoader";
+// import PulseLoader from "halogen/PulseLoader";
import LeftSideBar from "./leftsidebar";
import Parallel from "./continuous/parallel";
@@ -69,7 +69,7 @@ class App extends React.Component {
-
+ {/**/}
diff --git a/client/src/components/categorical/categorical.js b/client/src/components/categorical/categorical.js
index 997b3889..184c31cd 100644
--- a/client/src/components/categorical/categorical.js
+++ b/client/src/components/categorical/categorical.js
@@ -30,7 +30,7 @@ class Category extends React.Component {
componentDidUpdate() {
const valuesAsBool = _.values(
this.props.categoricalAsBooleansMap[this.props.metadataField]
- )
+ );
/* count categories toggled on by counting true values */
const categoriesToggledOn = _.values(valuesAsBool).filter(v => v).length;
@@ -80,13 +80,13 @@ class Category extends React.Component {
});
}
handleToggleAllClick() {
- // || this.checkbox.indeterminate === false
+ // || this.checkbox.indeterminate === false
if (this.state.isChecked) {
- console.log('checked, firing toggle none')
+ console.log("checked, firing toggle none");
this.toggleNone();
} else if (!this.state.isChecked) {
- console.log('!checked, firing toggle all')
- this.toggleAll()
+ console.log("!checked, firing toggle all");
+ this.toggleAll();
}
}
render() {
@@ -131,7 +131,7 @@ class Category extends React.Component {
{this.props.metadataField}
this.checkbox = el}
+ ref={el => (this.checkbox = el)}
checked={this.state.isChecked}
type="checkbox"
/>
@@ -164,10 +164,7 @@ class Category extends React.Component {
}
@connect(state => {
- const ranges =
- state.cells.cells && state.cells.cells.data.ranges
- ? state.cells.cells.data.ranges
- : null;
+ const ranges = _.get("state.cells.cells.data.ranges", null);
return {
ranges
diff --git a/client/src/components/continuous/continuous.js b/client/src/components/continuous/continuous.js
index 226b6d0f..b764200f 100644
--- a/client/src/components/continuous/continuous.js
+++ b/client/src/components/continuous/continuous.js
@@ -17,19 +17,9 @@ import HistogramBrush from "./histogramBrush";
import { margin, width, height, createDimensions } from "./util";
@connect(state => {
- const ranges =
- state.cells.cells && state.cells.cells.data.ranges
- ? state.cells.cells.data.ranges
- : null;
- const metadata =
- state.cells.cells && state.cells.cells.data.metadata
- ? state.cells.cells.data.metadata
- : null;
-
- const initializeRanges =
- state.initialize.data && state.initialize.data.data.ranges
- ? state.initialize.data.data.ranges
- : null;
+ const ranges = _.get("state.cells.cells.data.ranges", null);
+ const metadata = _.get("state.cells.cells.data.metadata", null);
+ const initializeRanges = _.get("state.initialize.data.data.ranges", null);
return {
ranges,
@@ -51,10 +41,6 @@ class Continuous extends React.Component {
dimensions: null
};
}
- componentDidMount() {}
- componentWillReceiveProps(nextProps) {}
-
- componentDidMount() {}
handleBrushAction(selection) {
this.props.dispatch({
type: "continuous selection using parallel coords brushing",
diff --git a/client/src/components/continuous/histogramBrush.js b/client/src/components/continuous/histogramBrush.js
index a0d74f6c..e166511d 100644
--- a/client/src/components/continuous/histogramBrush.js
+++ b/client/src/components/continuous/histogramBrush.js
@@ -11,25 +11,13 @@ import FaPaintBrush from "react-icons/lib/fa/paint-brush";
import * as globals from "../../globals";
@connect(state => {
- const ranges =
- state.cells.cells && state.cells.cells.data.ranges
- ? state.cells.cells.data.ranges
- : null;
- const metadata =
- state.cells.cells && state.cells.cells.data.metadata
- ? state.cells.cells.data.metadata
- : null;
-
- const initializeRanges =
- state.initialize.data && state.initialize.data.data.ranges
- ? state.initialize.data.data.ranges
- : null;
+ const initializeRanges = _.get("state.initialize.data.data.ranges", null);
return {
+ initializeRanges,
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
- cellsMetadata: state.controls.cellsMetadata,
- initializeRanges
+ cellsMetadata: state.controls.cellsMetadata
};
})
class HistogramBrush extends React.Component {
@@ -49,9 +37,6 @@ class HistogramBrush extends React.Component {
brush: null
};
}
- componentDidMount() {}
- componentDidUpdate() {}
-
calcHistogramCache(nextProps) {
// recalculate expensive stuff
const allValuesForContinuousFieldAsArray = _.map(
@@ -80,14 +65,6 @@ class HistogramBrush extends React.Component {
componentWillMount() {
this.calcHistogramCache(this.props);
}
- componentWillReceiveProps(nextProps) {
- if (
- this.props.metadataField !== nextProps.metadataField ||
- !this.histogramCache.x
- ) {
- this.calcHistogramCache(nextProps);
- }
- }
onBrush(selection, x) {
return () => {
diff --git a/client/src/components/continuous/parallel.js b/client/src/components/continuous/parallel.js
index 0f53069e..d145f8a0 100644
--- a/client/src/components/continuous/parallel.js
+++ b/client/src/components/continuous/parallel.js
@@ -15,19 +15,9 @@ import drawLinesCanvas from "./drawLinesCanvas";
import { margin, width, height, createDimensions } from "./util";
@connect(state => {
- const ranges =
- state.cells.cells && state.cells.cells.data.ranges
- ? state.cells.cells.data.ranges
- : null;
- const metadata =
- state.cells.cells && state.cells.cells.data.metadata
- ? state.cells.cells.data.metadata
- : null;
-
- const initializeRanges =
- state.initialize.data && state.initialize.data.data.ranges
- ? state.initialize.data.data.ranges
- : null;
+ const ranges = _.get("state.cells.cells.data.ranges", null);
+ const metadata = _.get("state.cells.cells.data.metadata", null);
+ const initializeRanges = _.get("state.initialize.data.data.ranges", null);
return {
ranges,
diff --git a/client/src/components/continuousLegend/index.js b/client/src/components/continuousLegend/index.js
index 34b89161..e0aa3b1a 100644
--- a/client/src/components/continuousLegend/index.js
+++ b/client/src/components/continuousLegend/index.js
@@ -102,27 +102,26 @@ class ContinuousLegend extends React.Component {
super(props);
this.state = {};
}
- componentWillReceiveProps(nextProps) {
+ componentDidUpdate(prevProps) {
if (
- nextProps.colorAccessor !== this.props.colorAccessor ||
- nextProps.responsive.height !== this.props.responsive.height ||
- nextProps.responsive.width !== this.props.responsive.width
+ prevProps.colorAccessor !== this.props.colorAccessor ||
+ prevProps.responsive.height !== this.props.responsive.height ||
+ prevProps.responsive.width !== this.props.responsive.width
) {
/* always remove it, if it's not continuous we don't put it back. */
- d3
- .select("#continuous_legend")
+ d3.select("#continuous_legend")
.selectAll("*")
.remove();
}
- if (nextProps.colorAccessor && nextProps.colorScale) {
+ if (this.props.colorAccessor && this.props.colorScale) {
/* fragile! continuous range is 0 to 1, not [#fa4b2c, ...], make this a flag? */
- if (nextProps.colorScale.range()[0][0] !== "#") {
+ if (this.props.colorScale.range()[0][0] !== "#") {
continuous(
"#continuous_legend",
d3
.scaleSequential(d3.interpolateViridis)
- .domain(nextProps.colorScale.domain())
+ .domain(this.props.colorScale.domain())
);
}
}
diff --git a/client/src/components/graph/graph.js b/client/src/components/graph/graph.js
index c6cf9dd2..f9e3f794 100644
--- a/client/src/components/graph/graph.js
+++ b/client/src/components/graph/graph.js
@@ -117,10 +117,19 @@ class Graph extends React.Component {
reglRender
});
}
- componentWillReceiveProps(nextProps) {
- if (this.state.regl && nextProps.crossfilter) {
+ componentDidUpdate(prevProps, prevState) {
+ if (
+ this.state.reglRender &&
+ this.reglRenderState === "rendering" &&
+ this.state.mode !== "zoom"
+ ) {
+ this.state.reglRender.cancel();
+ this.reglRenderState = "paused";
+ }
+
+ if (this.state.regl && this.props.crossfilter) {
/* update the regl state */
- const crossfilter = nextProps.crossfilter.cells;
+ const crossfilter = this.props.crossfilter.cells;
const cells = crossfilter.all();
const cellCount = cells.length;
@@ -129,7 +138,7 @@ class Graph extends React.Component {
//
if (
!this.renderCache.positions ||
- this.props.crossfilter.cells != nextProps.crossfilter.cells
+ this.props.crossfilter.cells != prevProps.crossfilter.cells
) {
if (!this.renderCache.positions)
this.renderCache.positions = new Float32Array(2 * cellCount);
@@ -161,7 +170,7 @@ class Graph extends React.Component {
// we could add some sort of color-specific indicator to the app state.
if (
!this.renderCache.colors ||
- this.props.cellsMetadata != nextProps.cellsMetadata
+ this.props.cellsMetadata != prevProps.cellsMetadata
) {
if (!this.renderCache.colors)
this.renderCache.colors = new Float32Array(3 * cellCount);
@@ -178,7 +187,7 @@ class Graph extends React.Component {
//
if (
!this.renderCache.sizes ||
- this.props.crossfilter.cells != nextProps.crossfilter.cells
+ this.props.crossfilter.cells != prevProps.crossfilter.cells
) {
this.renderCache.sizes = new Float32Array(cellCount);
}
@@ -200,33 +209,22 @@ class Graph extends React.Component {
if (
/* invisibly handles the initial null vs integer case as well as resize events */
- nextProps.responsive.height !== this.props.responsive.height ||
- nextProps.responsive.width !== this.props.responsive.width
+ prevProps.responsive.height !== this.props.responsive.height ||
+ prevProps.responsive.width !== this.props.responsive.width
) {
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
- d3
- .select("#graphAttachPoint")
+ d3.select("#graphAttachPoint")
.selectAll("svg")
.remove();
const { svg, brush, brushContainer } = setupSVGandBrushElements(
this.handleBrushSelectAction.bind(this),
this.handleBrushDeselectAction.bind(this),
- nextProps.responsive,
+ this.props.responsive,
this.graphPaddingTop
);
this.setState({ svg, brush, brushContainer });
}
}
- componentDidUpdate() {
- if (
- this.state.reglRender &&
- this.reglRenderState === "rendering" &&
- this.state.mode !== "zoom"
- ) {
- this.state.reglRender.cancel();
- this.reglRenderState = "paused";
- }
- }
handleBrushSelectAction() {
/* This conditional handles procedural brush deselect. Brush emits an event on procedural deselect because it is move: null */
if (d3.event.sourceEvent !== null) {
@@ -251,7 +249,7 @@ class Graph extends React.Component {
// transform screen coordinates -> cell coordinates
const invert = pin => {
const x =
- 2 * pin[0] / (this.props.responsive.height - this.graphPaddingTop) -
+ (2 * pin[0]) / (this.props.responsive.height - this.graphPaddingTop) -
1;
const y =
2 *
diff --git a/client/src/components/scatterplot/scatterplot.js b/client/src/components/scatterplot/scatterplot.js
index 8966d20a..9cf72152 100644
--- a/client/src/components/scatterplot/scatterplot.js
+++ b/client/src/components/scatterplot/scatterplot.js
@@ -20,18 +20,9 @@ import { scaleLinear } from "../../util/scaleLinear";
import { margin, width, height, createDimensions } from "./util";
@connect(state => {
- const ranges =
- state.cells.cells && state.cells.cells.data.ranges
- ? state.cells.cells.data.ranges
- : null;
- const metadata =
- state.cells.cells && state.cells.cells.data.metadata
- ? state.cells.cells.data.metadata
- : null;
- const initializeRanges =
- state.initialize.data && state.initialize.data.data.ranges
- ? state.initialize.data.data.ranges
- : null;
+ const ranges = _.get("state.cells.cells.data.ranges", null);
+ const metadata = _.get("state.cells.cells.data.metadata", null);
+ const initializeRanges = _.get("state.initialize.data.data.ranges", null);
return {
ranges,
@@ -123,21 +114,6 @@ class Scatterplot extends React.Component {
colorBuffer
});
}
- componentWillReceiveProps(nextProps) {
- if (
- nextProps.expression &&
- nextProps.expression.data &&
- nextProps.scatterplotXXaccessor &&
- nextProps.scatterplotYYaccessor
- ) {
- const scales = this.setupScales(
- nextProps.expression,
- nextProps.scatterplotXXaccessor,
- nextProps.scatterplotYYaccessor
- );
- this.setState(scales);
- }
- }
componentDidUpdate(prevProps) {
if (
this.state.svg &&
@@ -213,6 +189,22 @@ class Scatterplot extends React.Component {
this.state.sizeBuffer({ data: sizes, dimension: 1 });
this.count = cellCount;
}
+
+ if (
+ this.props.expression &&
+ this.props.expression.data &&
+ this.props.scatterplotXXaccessor &&
+ this.props.scatterplotYYaccessor &&
+ (this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
+ this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
+ ) {
+ const scales = this.setupScales(
+ this.props.expression,
+ this.props.scatterplotXXaccessor,
+ this.props.scatterplotYYaccessor
+ );
+ this.setState(scales);
+ }
}
setupScales(expression, scatterplotXXaccessor, scatterplotYYaccessor) {
const xScale = d3
diff --git a/example-dataset/cellxgene-demo.gif b/example-dataset/cellxgene-demo.gif
new file mode 100644
index 00000000..8a04a852
Binary files /dev/null and b/example-dataset/cellxgene-demo.gif differ
diff --git a/server/app/app.py b/server/app/app.py
index 4cd08ce4..0998273a 100644
--- a/server/app/app.py
+++ b/server/app/app.py
@@ -1,43 +1,32 @@
+import argparse
import os
from flask import Flask
+from flask_caching import Cache
from flask_compress import Compress
from flask_cors import CORS
from flask_restful_swagger_2 import get_swagger_blueprint
-from .web import webapp
from .rest_api.rest import get_api_resources
+from .web import webapp
REACTIVE_LIMIT = 1_000_000
app = Flask(__name__)
+cache = Cache(app, config={"CACHE_TYPE": "simple", "CACHE_DEFAULT_TIMEOUT": 860000})
Compress(app)
CORS(app)
# Config
-CXG_DIR = os.environ.get("CXG_DIRECTORY", default="example-dataset/")
SECRET_KEY = os.environ.get("CXG_SECRET_KEY", default="SparkleAndShine")
-ENGINE = os.environ.get("CXG_ENGINE", default="scanpy")
-TITLE = os.environ.get("DATASET_TITLE", default="PBMC 3K")
-# TODO remove the 2 when this is prod
-CXG_API_BASE = os.environ.get("CXG_API_BASE2", default="http://0.0.0.0:5005/api/")
app.config.update(
SECRET_KEY=SECRET_KEY,
- CXG_API_BASE=CXG_API_BASE,
- ENGINE=ENGINE,
- DATA=CXG_DIR,
- DATASET_TITLE=TITLE
)
-app.config["PROFILE"] = True
-# app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[15])
-
# Application Data
data = None
-if app.config["ENGINE"] == "scanpy":
- from .scanpy_engine.scanpy_engine import ScanpyEngine
- data = ScanpyEngine(app.config["DATA"], schema="data_schema.json")
+
# A list of swagger document objects
docs = []
@@ -53,5 +42,29 @@ app.register_blueprint(
app.add_url_rule("/", endpoint="index")
+def run_scanpy(args):
+ title = args.title
+ if not title:
+ title = os.path.basename(os.path.normpath(args.data_directory))
+ api_base = f"http://0.0.0.0:{args.port}/api/"
+ app.config.update(
+ DATASET_TITLE=title,
+ CXG_API_BASE=api_base
+ )
+
+ from .scanpy_engine.scanpy_engine import ScanpyEngine
+ app.data = ScanpyEngine(args.data_directory, schema="data_schema.json")
+ app.run(host="0.0.0.0", debug=True, port=args.port)
+
+
def main():
- app.run(host="0.0.0.0", debug=True, port=5005)
+ parser = argparse.ArgumentParser(description="Cellxgene is a tool for exploring single cell expression.")
+ parser.add_argument("--title", "-t", help="Title to display -- if this is omitted the title will be the name "
+ "of the directory from the data_directory arg")
+ parser.add_argument("--port", help="Port to run server on.", type=int, default=5005)
+ subparsers = parser.add_subparsers(dest="cellxgene_command")
+ scanpy_subparser = subparsers.add_parser("scanpy", help="run cellxgene using the scanpy engine")
+ scanpy_subparser.add_argument("data_directory", metavar="dir", help="Directory containing data and schema file")
+ scanpy_subparser.set_defaults(func=run_scanpy)
+ args = parser.parse_args()
+ args.func(args)
diff --git a/server/app/rest_api/rest.py b/server/app/rest_api/rest.py
index 40742dc3..c6307658 100644
--- a/server/app/rest_api/rest.py
+++ b/server/app/rest_api/rest.py
@@ -1,10 +1,10 @@
from flask import (
- Blueprint, request
+ Blueprint, request, current_app
)
from flask_restful_swagger_2 import Api, swagger, Resource
-from ..util.utils import make_payload
-from ..util.filter import parse_filter
+from server.app.util.utils import make_payload
+from server.app.util.filter import parse_filter
class InitializeAPI(Resource):
@@ -88,13 +88,13 @@ class InitializeAPI(Resource):
}
})
def get(self):
- from server.app.app import data, REACTIVE_LIMIT
+ from server.app.app import REACTIVE_LIMIT
return make_payload({
- "schema": data.schema,
- "cellcount": data.cell_count,
+ "schema": current_app.data.schema,
+ "cellcount": current_app.data.cell_count,
"reactivelimit": REACTIVE_LIMIT,
- "genes": data.genes(),
- "ranges": data.metadata_ranges(),
+ "genes": current_app.data.genes(),
+ "ranges": current_app.data.metadata_ranges(),
})
@@ -191,7 +191,6 @@ class CellsAPI(Resource):
}
})
def get(self):
- from server.app.app import data
payload = {
"metadata": [],
"cellcount": 0,
@@ -199,12 +198,12 @@ class CellsAPI(Resource):
"ranges": {},
}
# get query params
- cells_filter = parse_filter(request.args, data.schema)
- filtered_data = data.filter_cells(cells_filter)
- payload["metadata"] = data.metadata(filtered_data)
- payload["ranges"] = data.metadata_ranges(filtered_data)
- payload["graph"] = data.create_graph(filtered_data)
- payload["cellcount"] = data.cell_count
+ cells_filter = parse_filter(request.args, current_app.data.schema)
+ filtered_data = current_app.data.filter_cells(cells_filter)
+ payload["metadata"] = current_app.data.metadata(filtered_data)
+ payload["ranges"] = current_app.data.metadata_ranges(filtered_data)
+ payload["graph"] = current_app.data.create_graph(filtered_data)
+ payload["cellcount"] = current_app.data.cell_count
return make_payload(payload)
@@ -251,8 +250,7 @@ class ExpressionAPI(Resource):
}
})
def get(self):
- from server.app.app import data
- expression_data = data.expression()
+ expression_data = current_app.data.expression()
return make_payload(expression_data)
@swagger.doc({
@@ -309,14 +307,13 @@ class ExpressionAPI(Resource):
}
})
def post(self):
- from server.app.app import data
args = request.get_json()
cell_list = args.get("celllist", [])
gene_list = args.get("genelist", [])
if not cell_list and not gene_list:
return make_payload([], "must include celllist and/or genelist parameter", 400)
- expression_data = data.expression(cell_list, gene_list)
+ expression_data = current_app.data.expression(cell_list, gene_list)
if cell_list and len(expression_data["cells"]) < len(cell_list):
return make_payload([], "Some cell ids not available", 400)
@@ -415,7 +412,6 @@ class DifferentialExpressionAPI(Resource):
}
})
def post(self):
- from server.app.app import data
args = request.get_json()
cell_list_1 = args.get("celllist1", [])
cell_list_2 = args.get("celllist2", [])
@@ -425,7 +421,7 @@ class DifferentialExpressionAPI(Resource):
return make_payload([],
"must include celllist1 and celllist2 parameters",
400)
- data = data.diffexp(cell_list_1, cell_list_2, pval, num_genes)
+ data = current_app.data.diffexp(cell_list_1, cell_list_2, pval, num_genes)
return make_payload(data)
diff --git a/server/app/scanpy_engine/scanpy_engine.py b/server/app/scanpy_engine/scanpy_engine.py
index 405003f5..9ede3b45 100644
--- a/server/app/scanpy_engine/scanpy_engine.py
+++ b/server/app/scanpy_engine/scanpy_engine.py
@@ -4,8 +4,9 @@ import numpy as np
import scanpy.api as sc
from scipy import stats
-from ..util.schema_parse import parse_schema
-from ..driver.driver import CXGDriver
+from server.app.app import cache
+from server.app.driver.driver import CXGDriver
+from server.app.util.schema_parse import parse_schema
class ScanpyEngine(CXGDriver):
@@ -41,6 +42,7 @@ class ScanpyEngine(CXGDriver):
def genes(self):
return self.data.var.index.tolist()
+ # Can't seem to cache a view of a dataframe, need to investigate why
def filter_cells(self, filter):
"""
Filter cells from data and return a subset of the data
@@ -69,6 +71,7 @@ class ScanpyEngine(CXGDriver):
cell_idx = np.logical_and(cell_idx, key_idx)
return self.data[cell_idx, :]
+ @cache.memoize()
def metadata_ranges(self, df=None):
metadata_ranges = {}
if not df:
@@ -88,6 +91,7 @@ class ScanpyEngine(CXGDriver):
}
return metadata_ranges
+ @cache.memoize()
def metadata(self, df, fields=None):
"""
Gets metadata key:value for each cells
@@ -101,6 +105,7 @@ class ScanpyEngine(CXGDriver):
metadata[idx]["CellName"] = metadata[idx].pop("cell_name", None)
return metadata
+ @cache.memoize()
def create_graph(self, df):
"""
Computes a n-d layout for cells through dimensionality reduction.
@@ -112,6 +117,7 @@ class ScanpyEngine(CXGDriver):
normalized_graph = (graph - graph.min()) / (graph.max() - graph.min())
return np.hstack((df.obs["cell_name"].values.reshape(len(df.obs.index), 1), normalized_graph)).tolist()
+ @cache.memoize()
def diffexp(self, cell_list_1, cell_list_2, pval, num_genes):
"""
Computes the top differentially expressed genes between two clusters
@@ -158,6 +164,7 @@ class ScanpyEngine(CXGDriver):
},
}
+ @cache.memoize()
def expression(self, cells=None, genes=None):
"""
Retrieves expression for each gene for cells in data frame
diff --git a/server/requirements.txt b/server/requirements.txt
index 7ab96abb..a881e310 100644
--- a/server/requirements.txt
+++ b/server/requirements.txt
@@ -6,6 +6,7 @@ click==6.7
cycler==0.10.0
decorator==4.3.0
Flask==0.12.4
+Flask-Caching==1.4.0
Flask-Compress==1.4.0
Flask-Cors==3.0.6
Flask-RESTful==0.3.6