mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-23 05:38:11 +08:00
Merge branch 'master' of https://github.com/chanzuckerberg/cellxgene into bkmartinjr-crossfiltergroups
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
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.*
|
||||
|
||||
+5
-3
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user