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