Merge branch 'master' of https://github.com/chanzuckerberg/cellxgene into bkmartinjr-crossfiltergroups

This commit is contained in:
bkmartinjr
2018-08-04 13:06:08 -07:00
16 changed files with 181 additions and 193 deletions
+1 -1
View File
@@ -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
+53 -23
View File
@@ -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
<img align="right" width="350" height="218" src="./example-dataset/cellxgene-demo.gif" pad="50px">
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
Weve 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.*
+5 -3
View File
@@ -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": {
+2 -2
View File
@@ -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 {
<div
style={{ position: "fixed", left: window.innerWidth / 2, top: 150 }}
>
<PulseLoader color="rgb(0,0,0)" size="10px" margin="4px" />
{/*<PulseLoader color="rgb(0,0,0)" size="10px" margin="4px" />*/}
<span
style={{ fontFamily: globals.accentFont, fontStyle: "italic" }}
>
@@ -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}
<input
onChange={this.handleToggleAllClick.bind(this)}
ref={el => 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
+3 -17
View File
@@ -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",
@@ -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 () => {
+3 -13
View File
@@ -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,
@@ -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())
);
}
}
+20 -22
View File
@@ -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 *
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

+30 -17
View File
@@ -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)
+17 -21
View File
@@ -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)
+9 -2
View File
@@ -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
+1
View File
@@ -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