mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-27 06:18:12 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b28d51dfa | ||
|
|
40ad283107 | ||
|
|
0f8d7a55de | ||
|
|
b6f946ec8a | ||
|
|
dbb3a309a9 | ||
|
|
2e9525741f | ||
|
|
585a5808b9 | ||
|
|
6f464f4f92 | ||
|
|
08ea7d5137 | ||
|
|
ad9be3cdd7 | ||
|
|
f737cc4ee4 | ||
|
|
1103272b95 | ||
|
|
2df7161cd8 | ||
|
|
d31c05c970 | ||
|
|
07db2eb3ee | ||
|
|
a6d2a2e119 | ||
|
|
f87e4bfbd3 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.5.1
|
current_version = 0.6.0
|
||||||
|
|
||||||
[bumpversion:file:setup.py]
|
[bumpversion:file:setup.py]
|
||||||
search = version="{current_version}"
|
search = version="{current_version}"
|
||||||
|
|||||||
+2
-2
@@ -8,8 +8,8 @@ cache:
|
|||||||
install:
|
install:
|
||||||
- set -eo pipefail
|
- set -eo pipefail
|
||||||
- pip install flake8
|
- pip install flake8
|
||||||
- ./bin/build-client
|
- make build
|
||||||
- pip install -e .
|
- make install
|
||||||
- pip install -r server/requirements-dev.txt
|
- pip install -r server/requirements-dev.txt
|
||||||
- docker build .
|
- docker build .
|
||||||
script:
|
script:
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ To prepare from an existing `.h5ad` file use
|
|||||||
cellxgene prepare dataset.h5ad --output=dataset-processed.h5ad
|
cellxgene prepare dataset.h5ad --output=dataset-processed.h5ad
|
||||||
```
|
```
|
||||||
|
|
||||||
This will load the input data, perform PCA and nearest neighbor calculations, compute `umap` and `tsne` layouts and `louvain` cluster assignments, and save the results in a new file called `dataset-processed.h5ad` that can be loaded using `cellxgene launch`. Data can be loaded from several formats, including `.h5ad` `.loom` and a `10-Genomics-formatted` `mtx` directory. Several options are available, including running one of the preprocessing `recipes` included with `scanpy`, which include steps like cell filtering and gene selection. To learn more about the `recipes` please see the `scanpy` [documentation](https://github.com/theislab/scanpy/blob/master/scanpy/preprocessing/recipes.py).
|
This will load the input data, perform PCA and nearest neighbor calculations, compute `umap` and `tsne` layouts and `louvain` cluster assignments, and save the results in a new file called `dataset-processed.h5ad` that can be loaded using `cellxgene launch`. Data can be loaded from several formats, including `.h5ad` `.loom` and a `10-Genomics-formatted` `mtx` directory. Several options are available, including running one of the preprocessing `recipes` included with `scanpy`, which include steps like cell filtering and gene selection. To learn more about the `recipes` please see the `scanpy` [documentation](https://scanpy.readthedocs.io/en/latest/api/index.html#recipes).
|
||||||
|
|
||||||
Depending on the options chosen, `prepare` can take a long time to run (a few minutes for datasets with 10-100k cells, up to an hour or more for datasets with >100k cells). If you want `prepare` to run faster we recommend using the `sparse` option and only computing the layout for `umap`, using a call like this
|
Depending on the options chosen, `prepare` can take a long time to run (a few minutes for datasets with 10-100k cells, up to an hour or more for datasets with >100k cells). If you want `prepare` to run faster we recommend using the `sparse` option and only computing the layout for `umap`, using a call like this
|
||||||
|
|
||||||
@@ -191,9 +191,13 @@ Currently this is not supported directly, but you should be able to do this manu
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
> I tried to `pip install cellxgene` and got a weird error I don't understand
|
> I tried to `pip install cellxgene` and got a weird error about missing paths to an HDF5 library?
|
||||||
|
|
||||||
This may happen, especially as we work out bugs in our installation process! Please create a new [Github issue](https://github.com/chanzuckerberg/cellxgene/issues), explain what you did, and include all the error messages you saw. It'd also be super helpful if you call `pip freeze` and include the full output alongside your issue.
|
You probably just need to install HDF5 first. If you're on a mac, you can simply `brew install hdf5` and then try `pip install cellxgene` again.
|
||||||
|
|
||||||
|
> I tried to `pip install cellxgene` and got another weird error I don't understand
|
||||||
|
|
||||||
|
This may happen, especially as we work out bugs in our installation process! Please create a new [Github issue](https://github.com/chanzuckerberg/cellxgene/issues), explain what you did, and include all the error messages you saw. It'd also be super helpful if you call `pip freeze` and include the full output alongside your issue.
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
|
||||||
CELLXGENE_DIR=$(dirname $DIR)
|
|
||||||
|
|
||||||
cd $CELLXGENE_DIR
|
|
||||||
|
|
||||||
npm install --prefix client/ client
|
|
||||||
npm run --prefix client build
|
|
||||||
rm -rf server/app/web/static
|
|
||||||
mkdir -p server/app/web/static/img
|
|
||||||
cp client/build/index.html server/app/web/templates/
|
|
||||||
cp -r client/build/static server/app/web/
|
|
||||||
|
|
||||||
cp client/build/favicon.png server/app/web/static/img
|
|
||||||
cp client/build/service-worker.js server/app/web/static/js/
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
|
||||||
CELLXGENE_DIR=$(dirname $DIR)
|
|
||||||
|
|
||||||
echo "Uninstalling cellxgene"
|
|
||||||
yes | pip uninstall cellxgene
|
|
||||||
echo "removing node_modules"
|
|
||||||
rm -rf $CELLXGENE_DIR/client/node_modules
|
|
||||||
echo "removing client_build"
|
|
||||||
rm -rf $CELLXGENE_DIR/client/build
|
|
||||||
echo "removing dist"
|
|
||||||
rm -rf $CELLXGENE_DIR/dist
|
|
||||||
echo "removing egg-info"
|
|
||||||
rm -rf $CELLXGENE_DIR/cellxgene.egg-info
|
|
||||||
echo "removing static files"
|
|
||||||
rm -f $CELLXGENE_DIR/server/app/web/templates/index.html
|
|
||||||
rm -rf $CELLXGENE_DIR/server/app/web/static
|
|
||||||
echo "cellxgene cleanup complete"
|
|
||||||
|
|
||||||
@@ -187,11 +187,8 @@ describe("createObsDimensionMap", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(dimensionMap[layoutDimensionName("X")]).toBeInstanceOf(
|
expect(dimensionMap[layoutDimensionName("XY")]).toBeInstanceOf(
|
||||||
Crossfilter.ScalarDimension
|
Crossfilter.SpatialDimension
|
||||||
);
|
|
||||||
expect(dimensionMap[layoutDimensionName("Y")]).toBeInstanceOf(
|
|
||||||
Crossfilter.ScalarDimension
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,22 +102,20 @@ const someData = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function groupReduce(data, valueMap, valueReduce, valueInit) {
|
function groupReduce(data, valueMap, valueReduce, valueInit) {
|
||||||
return _
|
return _.reduce(
|
||||||
.reduce(
|
data,
|
||||||
data,
|
(acc, value) => {
|
||||||
(acc, value) => {
|
const k = valueMap(value);
|
||||||
const k = valueMap(value);
|
let r = _.find(acc, o => o.key === k);
|
||||||
let r = _.find(acc, o => o.key === k);
|
if (!r) {
|
||||||
if (!r) {
|
r = { key: k, value: valueInit() };
|
||||||
r = { key: k, value: valueInit() };
|
acc.push(r);
|
||||||
acc.push(r);
|
}
|
||||||
}
|
r.value = valueReduce(r.value, value);
|
||||||
r.value = valueReduce(r.value, value);
|
return acc;
|
||||||
return acc;
|
},
|
||||||
},
|
[]
|
||||||
[]
|
).sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
||||||
)
|
|
||||||
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupCount(data, map) {
|
function groupCount(data, map) {
|
||||||
@@ -139,7 +137,11 @@ describe("typedCrossfilter", () => {
|
|||||||
expect(payments.size()).toEqual(someData.length);
|
expect(payments.size()).toEqual(someData.length);
|
||||||
expect(payments.all()).toEqual(someData);
|
expect(payments.all()).toEqual(someData);
|
||||||
|
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.quantity,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
expect(quantity).toBeDefined();
|
expect(quantity).toBeDefined();
|
||||||
expect(quantity.id()).toBeDefined();
|
expect(quantity.id()).toBeDefined();
|
||||||
|
|
||||||
@@ -150,10 +152,22 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("filterAll and filterNone", () => {
|
test("filterAll and filterNone", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
expect(quantity).toBeDefined();
|
expect(quantity).toBeDefined();
|
||||||
expect(tip).toBeDefined();
|
expect(tip).toBeDefined();
|
||||||
@@ -198,10 +212,22 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("filterExact", () => {
|
test("filterExact", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
quantity.filterExact(1);
|
quantity.filterExact(1);
|
||||||
expect(payments.countFiltered()).toEqual(
|
expect(payments.countFiltered()).toEqual(
|
||||||
@@ -222,10 +248,22 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("filterRange", () => {
|
test("filterRange", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
tip.filterRange([0, 91]);
|
tip.filterRange([0, 91]);
|
||||||
expect(payments.allFiltered()).toEqual(
|
expect(payments.allFiltered()).toEqual(
|
||||||
@@ -251,10 +289,22 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("filterEnum", () => {
|
test("filterEnum", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
type.filterEnum(["tab", "cash"]);
|
type.filterEnum(["tab", "cash"]);
|
||||||
expect(payments.allFiltered()).toEqual(
|
expect(payments.allFiltered()).toEqual(
|
||||||
@@ -274,15 +324,31 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("more than 32 dimensions", () => {
|
test("more than 32 dimensions", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
// Create a bunch of fake dimensions to ensure we can handle > 32
|
// Create a bunch of fake dimensions to ensure we can handle > 32
|
||||||
let dimMap = {};
|
let dimMap = {};
|
||||||
for (let i = 0; i < 65; i++) {
|
for (let i = 0; i < 65; i++) {
|
||||||
dimMap[i] = payments.dimension(r => Math.random(), Float32Array);
|
dimMap[i] = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => Math.random(),
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
expect(dimMap[i]).toBeDefined();
|
expect(dimMap[i]).toBeDefined();
|
||||||
expect(dimMap[i].id()).toBeDefined();
|
expect(dimMap[i].id()).toBeDefined();
|
||||||
}
|
}
|
||||||
@@ -304,10 +370,22 @@ describe("typedCrossfilter", () => {
|
|||||||
test("group, default mapping, default reducer, no filter", () => {
|
test("group, default mapping, default reducer, no filter", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
|
|
||||||
var quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
var tip = payments.dimension(r => r.tip, Int32Array);
|
crossfilter.ScalarDimension,
|
||||||
var type = payments.dimension(r => r.type, "enum");
|
r => r.quantity,
|
||||||
var total = payments.dimension(r => r.total, Int32Array);
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
|
||||||
_.each(
|
_.each(
|
||||||
{
|
{
|
||||||
@@ -331,9 +409,17 @@ describe("typedCrossfilter", () => {
|
|||||||
// custom mapping in groups only works for scalar types. Enums do not
|
// custom mapping in groups only works for scalar types. Enums do not
|
||||||
// currently implement it.
|
// currently implement it.
|
||||||
|
|
||||||
const tip = payments.dimension(r => r.tip, Int32Array);
|
const tip = payments.dimension(
|
||||||
const totalX10 = payments.dimension(r => r.total * 10, Int32Array);
|
crossfilter.ScalarDimension,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
r => r.tip,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const totalX10 = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total * 10,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
const paymentsByTip_A = tip.group();
|
const paymentsByTip_A = tip.group();
|
||||||
const paymentsByTip_B = tip.group(r => 10 * r);
|
const paymentsByTip_B = tip.group(r => 10 * r);
|
||||||
@@ -370,8 +456,12 @@ describe("typedCrossfilter", () => {
|
|||||||
test("group, default map, custom reducer, no filters", () => {
|
test("group, default map, custom reducer, no filters", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
|
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
const total = payments.dimension(
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
const paymentsByTotal = total.group();
|
const paymentsByTotal = total.group();
|
||||||
const paymentsByType = type.group();
|
const paymentsByType = type.group();
|
||||||
@@ -407,9 +497,17 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
|
|
||||||
const tip = payments.dimension(r => r.tip, Int32Array);
|
const tip = payments.dimension(
|
||||||
const total = payments.dimension(r => r.total, Int32Array);
|
crossfilter.ScalarDimension,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
r => r.tip,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
const paymentsByTip = tip.group();
|
const paymentsByTip = tip.group();
|
||||||
const paymentsByTotal = total.group();
|
const paymentsByTotal = total.group();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const nodeModules = path.resolve("node_modules");
|
|||||||
|
|
||||||
const babelOptions = require("../babel/babel.prod");
|
const babelOptions = require("../babel/babel.prod");
|
||||||
|
|
||||||
const publicPath = "/";
|
const publicPath = "";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: "production",
|
mode: "production",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.CELLXGENE = {};
|
window.CELLXGENE = {};
|
||||||
window.CELLXGENE.API = {
|
window.CELLXGENE.API = {
|
||||||
prefix: "{{ prefix | safe }}",
|
prefix: window.location.href + "api/",
|
||||||
version: "v0.2/"
|
version: "v0.2/"
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Generated
+2521
-1970
File diff suppressed because it is too large
Load Diff
+16
-18
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "cellxgene",
|
"name": "cellxgene",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
|
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
|
||||||
"repository": "https://github.com/chanzuckerberg/cellxgene",
|
"repository": "https://github.com/chanzuckerberg/cellxgene",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"backend-dev": "python3.6 -m venv cellxgene && source cellxgene/bin/activate && yes | pip uninstall cellxgene || true && pip install -e .. && cellxgene launch ",
|
||||||
"build": "npm run clean && webpack --config configuration/webpack/webpack.config.prod.js",
|
"build": "npm run clean && webpack --config configuration/webpack/webpack.config.prod.js",
|
||||||
"dev": "npm run clean && webpack --config configuration/webpack/webpack.config.dev.js",
|
"dev": "npm run clean && webpack --config configuration/webpack/webpack.config.dev.js",
|
||||||
"clean": "rimraf build",
|
"clean": "rimraf build",
|
||||||
@@ -19,10 +20,9 @@
|
|||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "./configuration/eslint/eslint.js"
|
"extends": "./configuration/eslint/eslint.js"
|
||||||
},
|
},
|
||||||
"nyc": {
|
"eslintIgnore": [
|
||||||
"sourceMap": false,
|
"src/util/stateManager/matrix_generated.js"
|
||||||
"instrument": false
|
],
|
||||||
},
|
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"eslint-scope": "3.7.1"
|
"eslint-scope": "3.7.1"
|
||||||
},
|
},
|
||||||
@@ -76,30 +76,28 @@
|
|||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
"babel-jest": "^23.6.0",
|
"babel-jest": "^23.6.0",
|
||||||
"babel-loader": "^8.0.0",
|
"babel-loader": "^8.0.0",
|
||||||
"babel-plugin-istanbul": "^5.1.0",
|
|
||||||
"babel-preset-modern-browsers": "^12.0.0",
|
"babel-preset-modern-browsers": "^12.0.0",
|
||||||
"chalk": "^2.4.1",
|
"chalk": "^2.4.2",
|
||||||
"connect-history-api-fallback": "^1.3.0",
|
"connect-history-api-fallback": "^1.6.0",
|
||||||
"copy-webpack-plugin": "^4.6.0",
|
"copy-webpack-plugin": "^4.6.0",
|
||||||
"css-loader": "^1.0.1",
|
"css-loader": "^1.0.1",
|
||||||
"eslint": "^5.8.0",
|
"eslint": "^5.13.0",
|
||||||
"eslint-config-airbnb": "^17.1.0",
|
"eslint-config-airbnb": "^17.1.0",
|
||||||
"eslint-config-prettier": "^3.1.0",
|
"eslint-config-prettier": "^4.0.0",
|
||||||
"eslint-loader": "^2.1.1",
|
"eslint-loader": "^2.1.2",
|
||||||
"eslint-plugin-filenames": "^1.3.2",
|
"eslint-plugin-filenames": "^1.3.2",
|
||||||
"eslint-plugin-import": "^2.14.0",
|
"eslint-plugin-import": "^2.16.0",
|
||||||
"eslint-plugin-jest": "^21.27.2",
|
"eslint-plugin-jest": "^22.2.2",
|
||||||
"eslint-plugin-jsx-a11y": "^6.1.1",
|
"eslint-plugin-jsx-a11y": "^6.2.1",
|
||||||
"eslint-plugin-react": "^7.11.1",
|
"eslint-plugin-react": "^7.12.4",
|
||||||
"express": "^4.14.0",
|
"express": "^4.14.0",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^2.0.0",
|
||||||
"html-webpack-inline-source-plugin": "0.0.10",
|
"html-webpack-inline-source-plugin": "0.0.10",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"jest": "^23.5.0",
|
"jest": "^24.1.0",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"mini-css-extract-plugin": "^0.4.1",
|
"mini-css-extract-plugin": "^0.4.1",
|
||||||
"nyc": "^13.0.1",
|
"rimraf": "^2.6.3",
|
||||||
"rimraf": "^2.5.4",
|
|
||||||
"serve-favicon": "^2.3.0",
|
"serve-favicon": "^2.3.0",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"sw-precache-webpack-plugin": "^0.11.5",
|
"sw-precache-webpack-plugin": "^0.11.5",
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ A "user" error - eg, bad input
|
|||||||
export const postUserErrorToast = message =>
|
export const postUserErrorToast = message =>
|
||||||
ErrorToastTopCenter.show({ message, intent: Intent.WARNING });
|
ErrorToastTopCenter.show({ message, intent: Intent.WARNING });
|
||||||
|
|
||||||
|
/*
|
||||||
|
A toast the user must dismiss manually, because they need to act on its information,
|
||||||
|
ie., 8 bulk add genes out of 40 were bad. Manually see which ones and fix.
|
||||||
|
*/
|
||||||
|
export const keepAroundErrorToast = message =>
|
||||||
|
ErrorToastTopCenter.show({ message, timeout: 0, intent: Intent.WARNING });
|
||||||
|
|
||||||
/*
|
/*
|
||||||
a hard network error
|
a hard network error
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,12 +6,21 @@ import _ from "lodash";
|
|||||||
import fuzzysort from "fuzzysort";
|
import fuzzysort from "fuzzysort";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { MenuItem, Button } from "@blueprintjs/core";
|
import {
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
InputGroup,
|
||||||
|
ControlGroup
|
||||||
|
} from "@blueprintjs/core";
|
||||||
import { Suggest } from "@blueprintjs/select";
|
import { Suggest } from "@blueprintjs/select";
|
||||||
import HistogramBrush from "../brushableHistogram";
|
import HistogramBrush from "../brushableHistogram";
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import actions from "../../actions";
|
import actions from "../../actions";
|
||||||
import { postUserErrorToast } from "../framework/toasters";
|
import {
|
||||||
|
postUserErrorToast,
|
||||||
|
keepAroundErrorToast
|
||||||
|
} from "../framework/toasters";
|
||||||
import ExpressionButtons from "./expressionButtons";
|
import ExpressionButtons from "./expressionButtons";
|
||||||
import finiteExtent from "../../util/finiteExtent";
|
import finiteExtent from "../../util/finiteExtent";
|
||||||
|
|
||||||
@@ -67,6 +76,14 @@ const filterGenes = (query, genes) => {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
class GeneExpression extends React.Component {
|
class GeneExpression extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
bulkAdd: "",
|
||||||
|
tab: "autosuggest"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
handleClick(g) {
|
handleClick(g) {
|
||||||
const { world, dispatch, userDefinedGenes } = this.props;
|
const { world, dispatch, userDefinedGenes } = this.props;
|
||||||
const gene = g.target;
|
const gene = g.target;
|
||||||
@@ -87,6 +104,37 @@ class GeneExpression extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleBulkAddClick() {
|
||||||
|
const { world, dispatch, userDefinedGenes } = this.props;
|
||||||
|
const { bulkAdd } = this.state;
|
||||||
|
|
||||||
|
/*
|
||||||
|
test:
|
||||||
|
Apod,,, Cd74,, ,,, Foo, Bar-2,,
|
||||||
|
*/
|
||||||
|
if (bulkAdd !== "") {
|
||||||
|
const genes = _.pull(_.uniq(bulkAdd.split(/[ ,]+/)), "");
|
||||||
|
|
||||||
|
genes.forEach(gene => {
|
||||||
|
if (userDefinedGenes.indexOf(gene) !== -1) {
|
||||||
|
keepAroundErrorToast("That gene already exists");
|
||||||
|
} else if (!_.find(world.varAnnotations, { name: gene })) {
|
||||||
|
keepAroundErrorToast(
|
||||||
|
`${gene} doesn't appear to be a valid gene name.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(actions.requestUserDefinedGene(gene));
|
||||||
|
dispatch({
|
||||||
|
type: "user defined gene",
|
||||||
|
data: gene
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ bulkAdd: "" });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
world,
|
world,
|
||||||
@@ -95,6 +143,8 @@ class GeneExpression extends React.Component {
|
|||||||
differential
|
differential
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const { tab, bulkAdd } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -111,39 +161,105 @@ class GeneExpression extends React.Component {
|
|||||||
Selected Genes
|
Selected Genes
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
style={{ padding: globals.leftSidebarSectionPadding }}
|
style={{
|
||||||
className="bp3-control-group"
|
padding: globals.leftSidebarSectionPadding
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Suggest
|
|
||||||
disabled={true}
|
|
||||||
closeOnSelect
|
|
||||||
openOnKeyDown
|
|
||||||
resetOnSelect
|
|
||||||
itemDisabled={userDefinedGenesLoading ? () => true : () => false}
|
|
||||||
noResults={<MenuItem disabled text="No matching genes." />}
|
|
||||||
onItemSelect={g => {
|
|
||||||
/* this happens on 'enter' */
|
|
||||||
this.handleClick(g);
|
|
||||||
}}
|
|
||||||
inputValueRenderer={g => {
|
|
||||||
return "";
|
|
||||||
}}
|
|
||||||
itemListPredicate={filterGenes}
|
|
||||||
itemRenderer={renderGene.bind(this)}
|
|
||||||
items={
|
|
||||||
world && world.varAnnotations
|
|
||||||
? world.varAnnotations
|
|
||||||
: [{ name: "No genes", n_counts: "" }]
|
|
||||||
}
|
|
||||||
popoverProps={{ minimal: true }}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
className="bp3-button bp3-intent-primary"
|
active={tab === "autosuggest"}
|
||||||
loading={userDefinedGenesLoading}
|
style={{ marginRight: 5 }}
|
||||||
|
minimal
|
||||||
|
small
|
||||||
|
onClick={() => {
|
||||||
|
this.setState({ tab: "autosuggest" });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Add
|
Autosuggest
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={tab === "bulkadd"}
|
||||||
|
minimal
|
||||||
|
small
|
||||||
|
onClick={() => {
|
||||||
|
this.setState({ tab: "bulkadd" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bulk add genes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{tab === "autosuggest" ? (
|
||||||
|
<ControlGroup
|
||||||
|
style={{
|
||||||
|
paddingLeft: globals.leftSidebarSectionPadding,
|
||||||
|
paddingBottom: globals.leftSidebarSectionPadding
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Suggest
|
||||||
|
closeOnSelect
|
||||||
|
openOnKeyDown
|
||||||
|
resetOnSelect
|
||||||
|
itemDisabled={
|
||||||
|
userDefinedGenesLoading ? () => true : () => false
|
||||||
|
}
|
||||||
|
noResults={<MenuItem disabled text="No matching genes." />}
|
||||||
|
onItemSelect={g => {
|
||||||
|
/* this happens on 'enter' */
|
||||||
|
this.handleClick(g);
|
||||||
|
}}
|
||||||
|
inputValueRenderer={g => {
|
||||||
|
return "";
|
||||||
|
}}
|
||||||
|
itemListPredicate={filterGenes}
|
||||||
|
itemRenderer={renderGene.bind(this)}
|
||||||
|
items={
|
||||||
|
world && world.varAnnotations
|
||||||
|
? world.varAnnotations
|
||||||
|
: [{ name: "No genes" }]
|
||||||
|
}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="bp3-button bp3-intent-primary"
|
||||||
|
loading={userDefinedGenesLoading}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</ControlGroup>
|
||||||
|
) : null}
|
||||||
|
{tab === "bulkadd" ? (
|
||||||
|
<div style={{ paddingLeft: globals.leftSidebarSectionPadding }}>
|
||||||
|
<form
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handleBulkAddClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormGroup
|
||||||
|
helperText="Add a list of genes (comma delimited)"
|
||||||
|
labelFor="text-input-bulk-add"
|
||||||
|
>
|
||||||
|
<ControlGroup>
|
||||||
|
<InputGroup
|
||||||
|
onChange={e => {
|
||||||
|
this.setState({ bulkAdd: e.target.value });
|
||||||
|
}}
|
||||||
|
id="text-input-bulk-add"
|
||||||
|
placeholder="Apod, Cd74, ..."
|
||||||
|
value={bulkAdd}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
intent="primary"
|
||||||
|
onClick={this.handleBulkAddClick.bind(this)}
|
||||||
|
loading={userDefinedGenesLoading}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</ControlGroup>
|
||||||
|
</FormGroup>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{world && userDefinedGenes.length > 0
|
{world && userDefinedGenes.length > 0
|
||||||
? _.map(userDefinedGenes, (geneName, index) => {
|
? _.map(userDefinedGenes, (geneName, index) => {
|
||||||
const values = world.varDataCache[geneName];
|
const values = world.varDataCache[geneName];
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Graph extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
svg: null,
|
svg: null,
|
||||||
brush: null,
|
brush: null,
|
||||||
mode: "brush"
|
mode: "lasso"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,9 @@ class Graph extends React.Component {
|
|||||||
this.handleBrushSelectAction.bind(this),
|
this.handleBrushSelectAction.bind(this),
|
||||||
this.handleBrushDeselectAction.bind(this),
|
this.handleBrushDeselectAction.bind(this),
|
||||||
responsive,
|
responsive,
|
||||||
this.graphPaddingRight
|
this.graphPaddingRight,
|
||||||
|
this.handleLassoStart.bind(this),
|
||||||
|
this.handleLassoEnd.bind(this)
|
||||||
);
|
);
|
||||||
this.setState({ svg: newSvg, brush });
|
this.setState({ svg: newSvg, brush });
|
||||||
}
|
}
|
||||||
@@ -251,54 +253,54 @@ class Graph extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invertPoint(pin) {
|
||||||
|
const { responsive } = this.props;
|
||||||
|
const { regl, camera, offset } = this.state;
|
||||||
|
|
||||||
|
const gl = regl._gl;
|
||||||
|
|
||||||
|
// get aspect ratio
|
||||||
|
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
|
||||||
|
|
||||||
|
// compute inverse view matrix
|
||||||
|
const inverse = mat4.invert([], camera.view());
|
||||||
|
|
||||||
|
// transform screen coordinates -> cell coordinates
|
||||||
|
const x = (2 * pin[0]) / (responsive.width - this.graphPaddingRight) - 1;
|
||||||
|
const y = 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
|
||||||
|
const pout = [
|
||||||
|
x * inverse[14] * aspect + inverse[12],
|
||||||
|
y * inverse[14] + inverse[13]
|
||||||
|
];
|
||||||
|
return [(pout[0] + 1) / 2 + offset[0], (pout[1] + 1) / 2 + offset[1]];
|
||||||
|
}
|
||||||
|
|
||||||
handleBrushSelectAction() {
|
handleBrushSelectAction() {
|
||||||
/*
|
/*
|
||||||
This conditional handles procedural brush deselect. Brush emits
|
This conditional handles procedural brush deselect. Brush emits
|
||||||
an event on procedural deselect because it is move: null
|
an event on procedural deselect because it is move: null
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
const { camera, offset } = this.state;
|
|
||||||
const { dispatch, responsive } = this.props;
|
|
||||||
|
|
||||||
if (d3.event.sourceEvent !== null) {
|
|
||||||
/*
|
|
||||||
No idea why d3 event scope works like this
|
|
||||||
but apparently
|
|
||||||
it does
|
|
||||||
https://bl.ocks.org/EfratVil/0e542f5fc426065dd1d4b6daaa345a9f
|
|
||||||
*/
|
|
||||||
const s = d3.event.selection;
|
|
||||||
const gl = this.state.regl._gl;
|
|
||||||
/*
|
|
||||||
event describing brush position:
|
event describing brush position:
|
||||||
@-------|
|
@-------|
|
||||||
| |
|
| |
|
||||||
| |
|
| |
|
||||||
|-------@
|
|-------@
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
No idea why d3 event scope works like this
|
||||||
|
but apparently
|
||||||
|
it does
|
||||||
|
https://bl.ocks.org/EfratVil/0e542f5fc426065dd1d4b6daaa345a9f
|
||||||
|
*/
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
// get aspect ratio
|
if (d3.event.sourceEvent !== null) {
|
||||||
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
|
const s = d3.event.selection;
|
||||||
|
|
||||||
// compute inverse view matrix
|
|
||||||
const inverse = mat4.invert([], camera.view());
|
|
||||||
|
|
||||||
// transform screen coordinates -> cell coordinates
|
|
||||||
const invert = pin => {
|
|
||||||
const x =
|
|
||||||
(2 * pin[0]) / (responsive.width - this.graphPaddingRight) - 1;
|
|
||||||
const y =
|
|
||||||
2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
|
|
||||||
const pout = [
|
|
||||||
x * inverse[14] * aspect + inverse[12],
|
|
||||||
y * inverse[14] + inverse[13]
|
|
||||||
];
|
|
||||||
return [(pout[0] + 1) / 2 + offset[0], (pout[1] + 1) / 2 + offset[1]];
|
|
||||||
};
|
|
||||||
|
|
||||||
const brushCoords = {
|
const brushCoords = {
|
||||||
northwest: invert([s[0][0], s[0][1]]),
|
northwest: this.invertPoint([s[0][0], s[0][1]]),
|
||||||
southeast: invert([s[1][0], s[1][1]])
|
southeast: this.invertPoint([s[1][0], s[1][1]])
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -330,6 +332,25 @@ class Graph extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLassoStart() {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
// reset selected points when starting a new polygon
|
||||||
|
// making it easier for the user to make the next selection
|
||||||
|
dispatch({
|
||||||
|
type: "lasso started"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// when a lasso is completed, filter to the points within the lasso polygon
|
||||||
|
handleLassoEnd(polygon) {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "lasso selection",
|
||||||
|
polygon: polygon.map(xy => this.invertPoint(xy)) // transform the polygon
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleOpacityRangeChange(e) {
|
handleOpacityRangeChange(e) {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -412,13 +433,18 @@ class Graph extends React.Component {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div>
|
<div>
|
||||||
<div className="bp3-button-group">
|
<div className="bp3-button-group">
|
||||||
<Tooltip content="Lasso cells" position="left">
|
<Tooltip content="Lasso selection" position="left">
|
||||||
<Button
|
<Button
|
||||||
className="bp3-button bp3-icon-select"
|
|
||||||
type="button"
|
type="button"
|
||||||
active={mode === "brush"}
|
className="bp3-button bp3-icon-polygon-filter"
|
||||||
|
active={mode === "lasso"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.setState({ mode: "brush" });
|
this.handleBrushDeselectAction();
|
||||||
|
// this.restartReglLoop();
|
||||||
|
this.setState({ mode: "lasso" });
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -451,7 +477,7 @@ class Graph extends React.Component {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: mode === "brush" ? "inherit" : "none"
|
display: mode === "lasso" ? "inherit" : "none"
|
||||||
}}
|
}}
|
||||||
id="graphAttachPoint"
|
id="graphAttachPoint"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// https://bl.ocks.org/pbeshai/8008075f9ce771ee8be39e8c38907570
|
||||||
|
|
||||||
|
import * as d3 from "d3";
|
||||||
|
|
||||||
|
const Lasso = () => {
|
||||||
|
const dispatch = d3.dispatch("start", "end");
|
||||||
|
|
||||||
|
const polygonToPath = polygon =>
|
||||||
|
`M${polygon.map(d => d.join(",")).join("L")}`;
|
||||||
|
|
||||||
|
const distance = (pt1, pt2) =>
|
||||||
|
Math.sqrt((pt2[0] - pt1[0]) ** 2 + (pt2[1] - pt1[1]) ** 2);
|
||||||
|
|
||||||
|
// distance last point has to be to first point before it auto closes when mouse is released
|
||||||
|
const closeDistance = 75;
|
||||||
|
|
||||||
|
const lasso = svg => {
|
||||||
|
let lassoPolygon;
|
||||||
|
let lassoPath;
|
||||||
|
let closePath;
|
||||||
|
|
||||||
|
const handleDragStart = () => {
|
||||||
|
lassoPolygon = [d3.mouse(svg.node())]; // current x y of mouse within element
|
||||||
|
|
||||||
|
if (lassoPath) {
|
||||||
|
lassoPath.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
lassoPath = g
|
||||||
|
.append("path")
|
||||||
|
.attr("fill", "#0bb")
|
||||||
|
.attr("fill-opacity", 0.1)
|
||||||
|
.attr("stroke", "#0bb")
|
||||||
|
.attr("stroke-dasharray", "3, 3");
|
||||||
|
|
||||||
|
closePath = g
|
||||||
|
.append("line")
|
||||||
|
.attr("x2", lassoPolygon[0][0])
|
||||||
|
.attr("y2", lassoPolygon[0][1])
|
||||||
|
.attr("stroke", "#0bb")
|
||||||
|
.attr("stroke-dasharray", "3, 3")
|
||||||
|
.attr("opacity", 0);
|
||||||
|
|
||||||
|
dispatch.call("start", lasso, lassoPolygon);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrag = () => {
|
||||||
|
const point = d3.mouse(svg.node());
|
||||||
|
lassoPolygon.push(point);
|
||||||
|
lassoPath.attr("d", polygonToPath(lassoPolygon));
|
||||||
|
|
||||||
|
// indicate if we are within closing distance
|
||||||
|
if (
|
||||||
|
distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <
|
||||||
|
closeDistance
|
||||||
|
) {
|
||||||
|
closePath
|
||||||
|
.attr("x1", point[0])
|
||||||
|
.attr("y1", point[1])
|
||||||
|
.attr("opacity", 1);
|
||||||
|
} else {
|
||||||
|
closePath.attr("opacity", 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
// remove the close path
|
||||||
|
closePath.remove();
|
||||||
|
closePath = null;
|
||||||
|
|
||||||
|
// succesfully closed
|
||||||
|
if (
|
||||||
|
distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <
|
||||||
|
closeDistance
|
||||||
|
) {
|
||||||
|
lassoPath.attr("d", `${polygonToPath(lassoPolygon)}Z`);
|
||||||
|
dispatch.call("end", lasso, lassoPolygon);
|
||||||
|
|
||||||
|
// otherwise cancel
|
||||||
|
} else {
|
||||||
|
lassoPath.remove();
|
||||||
|
lassoPath = null;
|
||||||
|
lassoPolygon = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// append a <g> with a rect
|
||||||
|
const g = svg.append("g").attr("class", "lasso-group");
|
||||||
|
const bbox = svg.node().getBoundingClientRect();
|
||||||
|
const area = g
|
||||||
|
.append("rect")
|
||||||
|
.attr("width", bbox.width)
|
||||||
|
.attr("height", bbox.height)
|
||||||
|
.attr("fill", "tomato")
|
||||||
|
.attr("opacity", 0);
|
||||||
|
|
||||||
|
const drag = d3
|
||||||
|
.drag()
|
||||||
|
.on("start", handleDragStart)
|
||||||
|
.on("drag", handleDrag)
|
||||||
|
.on("end", handleDragEnd);
|
||||||
|
|
||||||
|
area.call(drag);
|
||||||
|
|
||||||
|
lasso.reset = () => {
|
||||||
|
if (lassoPath) {
|
||||||
|
lassoPath.remove();
|
||||||
|
lassoPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
lassoPolygon = null;
|
||||||
|
if (closePath) {
|
||||||
|
closePath.remove();
|
||||||
|
closePath = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
lasso.on = (type, callback) => {
|
||||||
|
dispatch.on(type, callback);
|
||||||
|
return lasso;
|
||||||
|
};
|
||||||
|
|
||||||
|
return lasso;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Lasso;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import styles from "./graph.css";
|
import styles from "./graph.css";
|
||||||
|
import Lasso from "./setupLasso";
|
||||||
|
|
||||||
/******************************************
|
/******************************************
|
||||||
*******************************************
|
*******************************************
|
||||||
@@ -12,7 +13,9 @@ export default (
|
|||||||
handleBrushSelectAction,
|
handleBrushSelectAction,
|
||||||
handleBrushDeselectAction,
|
handleBrushDeselectAction,
|
||||||
responsive,
|
responsive,
|
||||||
graphPaddingRight
|
graphPaddingRight,
|
||||||
|
handleLassoStart,
|
||||||
|
handleLassoEnd
|
||||||
) => {
|
) => {
|
||||||
const svg = d3
|
const svg = d3
|
||||||
.select("#graphAttachPoint")
|
.select("#graphAttachPoint")
|
||||||
@@ -32,9 +35,16 @@ export default (
|
|||||||
.attr("class", "graph_brush")
|
.attr("class", "graph_brush")
|
||||||
.call(brush);
|
.call(brush);
|
||||||
|
|
||||||
|
const lassoInstance = Lasso()
|
||||||
|
.on("end", handleLassoEnd)
|
||||||
|
.on("start", handleLassoStart);
|
||||||
|
|
||||||
|
const lasso = svg.call(lassoInstance);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
svg,
|
svg,
|
||||||
brushContainer,
|
brushContainer,
|
||||||
brush
|
brush,
|
||||||
|
lasso
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Colors } from "@blueprintjs/core";
|
||||||
|
|
||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
/* these will be either (preferably) specified or inferred */
|
/* these will be either (preferably) specified or inferred */
|
||||||
export const categories = [
|
export const categories = [
|
||||||
@@ -44,8 +46,8 @@ export const configDefaults = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* colors */
|
/* colors */
|
||||||
export const blue = "#4a90e2";
|
export const blue = Colors.BLUE3;
|
||||||
export const hcaBlue = "#1c7cc7";
|
export const linkBlue = Colors.BLUE5;
|
||||||
export const lightestGrey = "rgb(249,249,249)";
|
export const lightestGrey = "rgb(249,249,249)";
|
||||||
export const lighterGrey = "rgb(245,245,245)";
|
export const lighterGrey = "rgb(245,245,245)";
|
||||||
export const lightGrey = "rgb(211,211,211)";
|
export const lightGrey = "rgb(211,211,211)";
|
||||||
|
|||||||
Vendored
+21
-10
@@ -1,6 +1,8 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { polygonContains } from "d3";
|
||||||
|
|
||||||
import { World, kvCache, WorldUtil } from "../util/stateManager";
|
import { World, kvCache, WorldUtil } from "../util/stateManager";
|
||||||
import parseRGB from "../util/parseRGB";
|
import parseRGB from "../util/parseRGB";
|
||||||
import Crossfilter from "../util/typedCrossfilter";
|
import Crossfilter from "../util/typedCrossfilter";
|
||||||
@@ -479,27 +481,36 @@ const Controls = (
|
|||||||
User Events
|
User Events
|
||||||
*******************************/
|
*******************************/
|
||||||
case "graph brush selection change": {
|
case "graph brush selection change": {
|
||||||
state.dimensionMap[layoutDimensionName("X")].filterRange([
|
state.dimensionMap[layoutDimensionName("XY")].filterWithinRect(
|
||||||
action.brushCoords.northwest[0],
|
action.brushCoords.northwest,
|
||||||
action.brushCoords.southeast[0]
|
action.brushCoords.southeast
|
||||||
]);
|
);
|
||||||
state.dimensionMap[layoutDimensionName("Y")].filterRange([
|
|
||||||
action.brushCoords.southeast[1],
|
|
||||||
action.brushCoords.northwest[1]
|
|
||||||
]);
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graphBrushSelection: action.brushCoords
|
graphBrushSelection: action.brushCoords
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "lasso deselect":
|
||||||
case "graph brush deselect": {
|
case "graph brush deselect": {
|
||||||
state.dimensionMap[layoutDimensionName("X")].filterAll();
|
state.dimensionMap[layoutDimensionName("XY")].filterAll();
|
||||||
state.dimensionMap[layoutDimensionName("Y")].filterAll();
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graphBrushSelection: null
|
graphBrushSelection: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "lasso selection": {
|
||||||
|
const { polygon } = action;
|
||||||
|
const dXY = state.dimensionMap[layoutDimensionName("XY")];
|
||||||
|
if (polygon.length < 3) {
|
||||||
|
// single point or a line is not a polygon, and is therefore a deselect
|
||||||
|
dXY.filterAll();
|
||||||
|
} else {
|
||||||
|
dXY.filterWithinPolygon(polygon);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state
|
||||||
|
};
|
||||||
|
}
|
||||||
case "continuous metadata histogram brush": {
|
case "continuous metadata histogram brush": {
|
||||||
const name = makeContinuousDimensionName(
|
const name = makeContinuousDimensionName(
|
||||||
action.continuousNamespace,
|
action.continuousNamespace,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import { combineReducers, createStore, applyMiddleware } from "redux";
|
import { combineReducers, createStore, applyMiddleware } from "redux";
|
||||||
import thunk from "redux-thunk";
|
import thunk from "redux-thunk";
|
||||||
|
import { composeWithDevTools } from "redux-devtools-extension";
|
||||||
import updateURLMiddleware from "../middleware/updateURLMiddleware";
|
import updateURLMiddleware from "../middleware/updateURLMiddleware";
|
||||||
import updateCellColors from "../middleware/updateCellColors";
|
import updateCellColors from "../middleware/updateCellColors";
|
||||||
import { composeWithDevTools } from "redux-devtools-extension";
|
|
||||||
|
|
||||||
import config from "./config";
|
import config from "./config";
|
||||||
import differential from "./differential";
|
import differential from "./differential";
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const doFetch = async (url, acceptType) => {
|
|||||||
Accept: acceptType
|
Accept: acceptType
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if (res.ok && res.headers.get("Content-Type") === acceptType) {
|
if (res.ok && res.headers.get("Content-Type").includes(acceptType)) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
// else an error
|
// else an error
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import _ from "lodash";
|
|||||||
import * as kvCache from "./keyvalcache";
|
import * as kvCache from "./keyvalcache";
|
||||||
import summarizeAnnotations from "./summarizeAnnotations";
|
import summarizeAnnotations from "./summarizeAnnotations";
|
||||||
import { layoutDimensionName, obsAnnoDimensionName } from "../nameCreators";
|
import { layoutDimensionName, obsAnnoDimensionName } from "../nameCreators";
|
||||||
|
import Crossfilter from "../typedCrossfilter";
|
||||||
import { sliceByIndex } from "../typedCrossfilter/util";
|
import { sliceByIndex } from "../typedCrossfilter/util";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -231,7 +232,12 @@ export function createVarDimension(
|
|||||||
crossfilter,
|
crossfilter,
|
||||||
geneName
|
geneName
|
||||||
) {
|
) {
|
||||||
return crossfilter.dimension(_worldVarDataCache[geneName], Float32Array);
|
// return crossfilter.dimension(_worldVarDataCache[geneName], Float32Array);
|
||||||
|
return crossfilter.dimension(
|
||||||
|
Crossfilter.ScalarDimension,
|
||||||
|
_worldVarDataCache[geneName],
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createObsDimensionMap(crossfilter, world) {
|
export function createObsDimensionMap(crossfilter, world) {
|
||||||
@@ -246,9 +252,14 @@ export function createObsDimensionMap(crossfilter, world) {
|
|||||||
.filter(anno => anno.name !== "name")
|
.filter(anno => anno.name !== "name")
|
||||||
.transform((result, anno) => {
|
.transform((result, anno) => {
|
||||||
const dimType = deduceDimensionType(anno, anno.name);
|
const dimType = deduceDimensionType(anno, anno.name);
|
||||||
// XXX if dimtype is a scalar, we may be able to do better?
|
if (dimType === "enum") {
|
||||||
if (dimType) {
|
|
||||||
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
||||||
|
Crossfilter.EnumDimension,
|
||||||
|
r => r[anno.name]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
||||||
|
Crossfilter.ScalarDimension,
|
||||||
r => r[anno.name],
|
r => r[anno.name],
|
||||||
dimType
|
dimType
|
||||||
);
|
);
|
||||||
@@ -259,13 +270,10 @@ export function createObsDimensionMap(crossfilter, world) {
|
|||||||
/*
|
/*
|
||||||
Add crossfilter dimensions allowing filtering on layout
|
Add crossfilter dimensions allowing filtering on layout
|
||||||
*/
|
*/
|
||||||
dimensionMap[layoutDimensionName("X")] = crossfilter.dimension(
|
dimensionMap[layoutDimensionName("XY")] = crossfilter.dimension(
|
||||||
|
Crossfilter.SpatialDimension,
|
||||||
obsLayout.X,
|
obsLayout.X,
|
||||||
Float32Array
|
obsLayout.Y
|
||||||
);
|
|
||||||
dimensionMap[layoutDimensionName("Y")] = crossfilter.dimension(
|
|
||||||
obsLayout.Y,
|
|
||||||
Float32Array
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return dimensionMap;
|
return dimensionMap;
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ more complex API. In a few cases, elements of that API were incorporated.
|
|||||||
https://github.com/square/crossfilter/
|
https://github.com/square/crossfilter/
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
// XXX replace
|
||||||
|
import { polygonContains } from "d3";
|
||||||
|
|
||||||
import PositiveIntervals from "./positiveIntervals";
|
import PositiveIntervals from "./positiveIntervals";
|
||||||
import BitArray from "./bitArray";
|
import BitArray from "./bitArray";
|
||||||
@@ -66,14 +68,22 @@ class TypedCrossfilter {
|
|||||||
return this.data;
|
return this.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
dimension(value, valueArrayType) {
|
/*
|
||||||
|
Create a crossfilter dimension, upon which filtering (subselection) can
|
||||||
|
be done. Each dimension is typed, and has a particular set of filtering
|
||||||
|
semantics.
|
||||||
|
* ScalarDimension - backed by TypedArray values, supporting filtering
|
||||||
|
by value (within a value range, or one or more exact values)
|
||||||
|
* EnumDimension - backed by an enumeration (eg, strings, bools), filtering
|
||||||
|
by one or more enum categories.
|
||||||
|
* SpatialDimension - backed by 2D points, filter by containment within
|
||||||
|
various shapes (currently supports within Rectangle and within Polygon).
|
||||||
|
Call this method to create a dimension, passing arguments appropriate for
|
||||||
|
the dimension constructor.
|
||||||
|
*/
|
||||||
|
dimension(DimensionType, ...rest) {
|
||||||
const id = this.selection.allocDimension();
|
const id = this.selection.allocDimension();
|
||||||
let dim;
|
const dim = new DimensionType(this, id, ...rest);
|
||||||
if (valueArrayType === "enum") {
|
|
||||||
dim = new EnumDimension(value, this, id);
|
|
||||||
} else {
|
|
||||||
dim = new ScalarDimension(value, valueArrayType, this, id);
|
|
||||||
}
|
|
||||||
this.filters.push({ id, dim });
|
this.filters.push({ id, dim });
|
||||||
dim.filterAll();
|
dim.filterAll();
|
||||||
return dim;
|
return dim;
|
||||||
@@ -115,13 +125,34 @@ class TypedCrossfilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base dimension type - value must be a scalar type (eg, int, float),
|
// Base dimension type - not exported.
|
||||||
// and value array must be a TypedArray.
|
class _Dimension {
|
||||||
//
|
constructor(xfltr, id) {
|
||||||
class ScalarDimension {
|
|
||||||
constructor(value, ValueArrayType, xfltr, id) {
|
|
||||||
this.crossfilter = xfltr;
|
this.crossfilter = xfltr;
|
||||||
this._id = id;
|
this._id = id;
|
||||||
|
this.groups = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.crossfilter._freeDimension(this._id);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
id() {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
_filterUpdate() {
|
||||||
|
this.crossfilter.updateTime += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar dimension type - value must be a scalar type (eg, int, float),
|
||||||
|
// and value array must be a TypedArray.
|
||||||
|
//
|
||||||
|
class ScalarDimension extends _Dimension {
|
||||||
|
constructor(xfltr, id, value, ValueArrayType) {
|
||||||
|
super(xfltr, id);
|
||||||
|
|
||||||
// current selection filter, expressed as PostiveIntervals.
|
// current selection filter, expressed as PostiveIntervals.
|
||||||
this.currentFilter = [];
|
this.currentFilter = [];
|
||||||
@@ -151,9 +182,6 @@ class ScalarDimension {
|
|||||||
|
|
||||||
// create sort index
|
// create sort index
|
||||||
this.index = makeSortIndex(array);
|
this.index = makeSortIndex(array);
|
||||||
|
|
||||||
// groups, if any
|
|
||||||
this.groups = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_createValueArray(value, array) {
|
_createValueArray(value, array) {
|
||||||
@@ -167,15 +195,6 @@ class ScalarDimension {
|
|||||||
return larray;
|
return larray;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.crossfilter._freeDimension(this._id);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
id() {
|
|
||||||
return this._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Argument is an array of intervals indicating records newly selected/filtered
|
// Argument is an array of intervals indicating records newly selected/filtered
|
||||||
//
|
//
|
||||||
_updateFilters(newFilter) {
|
_updateFilters(newFilter) {
|
||||||
@@ -209,7 +228,7 @@ class ScalarDimension {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.currentFilter = cNewFilter;
|
this.currentFilter = cNewFilter;
|
||||||
this.crossfilter.updateTime += 1;
|
this._filterUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter by value - exact match
|
// filter by value - exact match
|
||||||
@@ -355,8 +374,8 @@ class ScalarDimension {
|
|||||||
// strings, which can be mapped into an fixed numeric range [0..n).
|
// strings, which can be mapped into an fixed numeric range [0..n).
|
||||||
//
|
//
|
||||||
class EnumDimension extends ScalarDimension {
|
class EnumDimension extends ScalarDimension {
|
||||||
constructor(value, xfltr, id) {
|
constructor(xfltr, id, value) {
|
||||||
super(value, Uint32Array, xfltr, id);
|
super(xfltr, id, value, Uint32Array);
|
||||||
}
|
}
|
||||||
|
|
||||||
_createValueArray(value, array) {
|
_createValueArray(value, array) {
|
||||||
@@ -408,6 +427,127 @@ class EnumDimension extends ScalarDimension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Super simple 2D spatial dimension, supporting basic "filter within"
|
||||||
|
operations.
|
||||||
|
*/
|
||||||
|
class SpatialDimension extends _Dimension {
|
||||||
|
constructor(xfltr, id, X, Y) {
|
||||||
|
super(xfltr, id);
|
||||||
|
|
||||||
|
if (X.length !== Y.length && X.length !== this.crossfilter.data.length) {
|
||||||
|
throw new RangeError(
|
||||||
|
"SpatialDimension values must have same dimensionality as crossfilter"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.X = X;
|
||||||
|
this.Y = Y;
|
||||||
|
|
||||||
|
this.Xindex = makeSortIndex(X);
|
||||||
|
this.Yindex = makeSortIndex(Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterAll() {
|
||||||
|
this.crossfilter.selection.selectAll(this._id);
|
||||||
|
this._filterUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
filterNone() {
|
||||||
|
this.crossfilter.selection.deselectAll(this._id);
|
||||||
|
this._filterUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
this could be smarter, but we don't currently use it...
|
||||||
|
*/
|
||||||
|
filterWithinRect(northwest, southeast) {
|
||||||
|
const [x0, y0] = northwest;
|
||||||
|
const [x1, y1] = southeast;
|
||||||
|
const { X, Y } = this;
|
||||||
|
const seln = this.crossfilter.selection;
|
||||||
|
const { _id } = this;
|
||||||
|
seln.deselectAll(_id);
|
||||||
|
for (let i = 0, l = this.X.length; i < l; i += 1) {
|
||||||
|
const x = X[i];
|
||||||
|
const y = Y[i];
|
||||||
|
if (x0 <= x && x < x1 && y0 <= y && y < y1) {
|
||||||
|
seln.selectOne(_id, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._filterUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Relatively brute force filter by polygon. Polygon is array of points, where
|
||||||
|
each point is [x,y]. Eg, [[x0,y0], [x1,y1], ...].
|
||||||
|
|
||||||
|
Currently uses d3.polygonContains() to test for polygon inclusion, which itself
|
||||||
|
uses a ray casting (crossing number) algorithm. There are a series of optimizations
|
||||||
|
to make this faster:
|
||||||
|
* first sliced by X or Y, using an index on the axis
|
||||||
|
* then the polygon bounding box is used for trivial rejection
|
||||||
|
* then the polygon test is applied
|
||||||
|
*/
|
||||||
|
filterWithinPolygon(polygon) {
|
||||||
|
/* return bounding box of the polygon */
|
||||||
|
function polygonBoundingBox(pg) {
|
||||||
|
let minX = Number.MAX_VALUE;
|
||||||
|
let minY = Number.MAX_VALUE;
|
||||||
|
let maxX = Number.MIN_VALUE;
|
||||||
|
let maxY = Number.MIN_VALUE;
|
||||||
|
for (let i = 0, l = pg.length; i < l; i += 1) {
|
||||||
|
const p = pg[i];
|
||||||
|
const x = p[0];
|
||||||
|
const y = p[1];
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
return [minX, minY, maxX, maxY];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [minX, minY, maxX, maxY] = polygonBoundingBox(polygon);
|
||||||
|
const { X, Y } = this;
|
||||||
|
let slice;
|
||||||
|
let index;
|
||||||
|
if (maxY - minY > maxX - minX) {
|
||||||
|
slice = [
|
||||||
|
lowerBoundIndirect(X, this.Xindex, minX, 0, X.length),
|
||||||
|
upperBoundIndirect(X, this.Xindex, maxX, 0, X.length)
|
||||||
|
];
|
||||||
|
index = this.Xindex;
|
||||||
|
} else {
|
||||||
|
slice = [
|
||||||
|
lowerBoundIndirect(Y, this.Yindex, minY, 0, Y.length),
|
||||||
|
upperBoundIndirect(Y, this.Yindex, maxY, 0, Y.length)
|
||||||
|
];
|
||||||
|
index = this.Yindex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seln = this.crossfilter.selection;
|
||||||
|
const { _id } = this;
|
||||||
|
const testWithin = polygonContains; // d3.polygonContains()
|
||||||
|
seln.deselectAll(_id);
|
||||||
|
|
||||||
|
for (let i = slice[0], e = slice[1]; i < e; i += 1) {
|
||||||
|
const rid = index[i];
|
||||||
|
const x = X[rid];
|
||||||
|
const y = Y[rid];
|
||||||
|
if (
|
||||||
|
minX <= x &&
|
||||||
|
x < maxX &&
|
||||||
|
minY <= y &&
|
||||||
|
y < maxY &&
|
||||||
|
testWithin(polygon, [x, y])
|
||||||
|
) {
|
||||||
|
seln.selectOne(_id, rid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._filterUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Groups! Map/reduce
|
// Groups! Map/reduce
|
||||||
//
|
//
|
||||||
class ScalarGroup {
|
class ScalarGroup {
|
||||||
@@ -611,5 +751,6 @@ crossfilter.BitArray = BitArray;
|
|||||||
crossfilter.TypedCrossfilter = TypedCrossfilter;
|
crossfilter.TypedCrossfilter = TypedCrossfilter;
|
||||||
crossfilter.ScalarDimension = ScalarDimension;
|
crossfilter.ScalarDimension = ScalarDimension;
|
||||||
crossfilter.EnumDimension = EnumDimension;
|
crossfilter.EnumDimension = EnumDimension;
|
||||||
|
crossfilter.SpatialDimension = SpatialDimension;
|
||||||
|
|
||||||
export default crossfilter;
|
export default crossfilter;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# cellxgene REST API 0.2 specification
|
# cellxgene REST API 0.2 specification
|
||||||
|
|
||||||
_Note:_ this document lacks any information about the binary encoding utilized by various routes. This will be added at a later date.
|
_Note:_ this document is increasingly divergent from the code base and should be read with great suspicion. For example, it lacks any information
|
||||||
|
about the binary encoding used by various routes, and has incorrect information about "required" routes and features. We may update it at a
|
||||||
|
later date when the protocol stabilizes a bit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Items marked as (_future_) are intended for future implementation, and are included in the design to round out the concept, and highlight what we would do when/if we needed more functionality. The (_future_) items are not currently used by the cellxgene web application, and may be omitted from any backend - see [Current Front-End Dependencies](#current-front-end-dependencies) for more details.
|
Items marked as (_future_) are intended for future implementation, and are included in the design to round out the concept, and highlight what we would do when/if we needed more functionality. The (_future_) items are not currently used by the cellxgene web application, and may be omitted from any backend - see [Current Front-End Dependencies](#current-front-end-dependencies) for more details.
|
||||||
|
|
||||||
|
|||||||
+19
-15
@@ -20,18 +20,14 @@ Follow these steps to create a release.
|
|||||||
|
|
||||||
1. Preparation:
|
1. Preparation:
|
||||||
- python3.6 environment, and a cellxgene clone
|
- python3.6 environment, and a cellxgene clone
|
||||||
- install required tools: `pip install -r requirements-dev.txt`
|
|
||||||
- Define the release version number, using [semantic versioning](https://semver.org/),
|
- Define the release version number, using [semantic versioning](https://semver.org/),
|
||||||
and specifying all three digits (eg, 0.3.0)
|
and specifying all three digits (eg, 0.3.0)
|
||||||
- Write the release title and release notes and add to
|
- Write the release title and release notes and add to
|
||||||
[release notes document](https://docs.google.com/document/d/1KnHwkYfhyWO5H8BDcMu7y3ogjvq5Yi4OwpmZ8DB6w0Y/edit)
|
[release notes document](https://docs.google.com/document/d/1KnHwkYfhyWO5H8BDcMu7y3ogjvq5Yi4OwpmZ8DB6w0Y/edit)
|
||||||
2. Create a release branch, eg, `release-version`
|
2. Create a release branch, eg, `release-version`
|
||||||
3. In the release branch:
|
3. In the release branch:
|
||||||
- Run `bumpversion --config-file .bumpversion.cfg [major | minor | patch]`,
|
- Run `make release-stage-1 PART=[major | minor | patch]` where you choose major/minor/patch depending on which part of the version
|
||||||
where you choose major/minor/patch depending on which part of the version
|
|
||||||
is being bumped (eg, 0.2.9->0.3 is minor).
|
is being bumped (eg, 0.2.9->0.3 is minor).
|
||||||
- Clean up existing environment using `bin/clean`
|
|
||||||
- Build the JS asserts using `bin/build-client`
|
|
||||||
4. Commit and push the new branch
|
4. Commit and push the new branch
|
||||||
5. Create a PR for the release.
|
5. Create a PR for the release.
|
||||||
- [optional] As needed, conduct PR review.
|
- [optional] As needed, conduct PR review.
|
||||||
@@ -44,18 +40,26 @@ Follow these steps to create a release.
|
|||||||
- Type title `Release {version num}`
|
- Type title `Release {version num}`
|
||||||
- [optional] Check pre-release if this release is not ready for production
|
- [optional] Check pre-release if this release is not ready for production
|
||||||
- Publish Release
|
- Publish Release
|
||||||
8. Publish to pypi by performing the following steps (assumes you have `setuptools`
|
8. Publish to pypi by performing the following steps (assumes you that you have registered for pypi,
|
||||||
and `twine` installed, that you have registered for pypi, and that you have
|
and that you have write access to the cellxgene pypi package):
|
||||||
write access to the cellxgene pypi package):
|
- Build the distribution and upload to test pypi `make release-stage-2`
|
||||||
- Build the distribution by calling `python setup.py sdist`
|
- [optional] Test the test installation in a fresh virtual environment using `make install-release-test`
|
||||||
inside the top-level directory
|
- Upload the package to real pypi using `make release-stage-final`
|
||||||
- [optional] Upload the package to test pypi
|
|
||||||
`twine upload --repository-url https://test.pypi.org/legacy/ dist/*`
|
|
||||||
- [optional] Test the test installation in a fresh virtual environment using
|
|
||||||
`pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple cellxgene`
|
|
||||||
- Upload the package to real pypi using `twine upload dist/*`
|
|
||||||
- [optional] Test the installation in a fresh virtual environment using
|
- [optional] Test the installation in a fresh virtual environment using
|
||||||
`pip install cellxgene`
|
`pip install cellxgene`
|
||||||
|
- **Troubleshooting**:
|
||||||
|
- Fails to upload to test.pypi: pypi doesn't allow you to reupload a release with the same version number,
|
||||||
|
if you accidentally burned a release number you want to use on prod, you have a couple options.
|
||||||
|
1) OPTION 1: Create distribution `make pydist`; test release locally `pip install dist/<release tarball>`;
|
||||||
|
then upload to prod `make release-stage-final`.
|
||||||
|
2) OPTION 2: (DANGER) release directly to prod: `make release-burned`.
|
||||||
|
3) OPTION 3: If the release was burned on prod as well run from Step 3 again with option
|
||||||
|
PART=patch until you get to an unburned version.
|
||||||
|
- The release doesn't install or fails your tests when you install it: Delete it from pypi - Go to pypi.org, sign in,
|
||||||
|
go to the cellxgene package, click manage, then in the options drop down, click delete and
|
||||||
|
follow the instructions. You will not be able to use that release number again. If it is a minor bug
|
||||||
|
and not a major regression, you can just release a patch.
|
||||||
|
|
||||||
|
|
||||||
The optional steps are for testing purposes, and are recommended
|
The optional steps are for testing purposes, and are recommended
|
||||||
for publishing any major releases, and any releases that significantly
|
for publishing any major releases, and any releases that significantly
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
### How to set up a testing environment for changes related to web hosting.
|
||||||
|
|
||||||
|
We often get PRs related to someone using a server to host cellxgene externally or on a local network (ex. https://github.com/chanzuckerberg/cellxgene/pull/568 ). Here is how you can test these changes locally.
|
||||||
|
|
||||||
|
We are going to run docker containers for cellxgene and an apache server running a reverse proxy on a local docker network. We run the cellxgene container without exposing any ports so that we cannot access it directly, only through the apache server. We can also update our cellxgene Dockerfile so that we can install a local build instead of having to deploy to pypi.
|
||||||
|
|
||||||
|
1 Create Docker network, this allows the containers to communicate with each other.
|
||||||
|
|
||||||
|
```
|
||||||
|
docker network create cxg
|
||||||
|
```
|
||||||
|
|
||||||
|
2 Create and run cellxgene container
|
||||||
|
|
||||||
|
(optional) To install cellxgene from the local codebase
|
||||||
|
|
||||||
|
a Create sdist file
|
||||||
|
`make pydist`
|
||||||
|
|
||||||
|
b Update Dockerfile to install from dist
|
||||||
|
|
||||||
|
```
|
||||||
|
FROM ubuntu:bionic
|
||||||
|
|
||||||
|
ENV LC_ALL=C.UTF-8
|
||||||
|
ENV LANG=C.UTF-8
|
||||||
|
COPY [ "dist/", "/cellxgene/dist/" ]
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y build-essential libxml2-dev python3-dev python3-pip zlib1g-dev && \
|
||||||
|
pip3 install /cellxgene/dist/cellxgene-0.5.1.tar.gz
|
||||||
|
|
||||||
|
ENTRYPOINT ["cellxgene"]
|
||||||
|
```
|
||||||
|
|
||||||
|
(required) Build container
|
||||||
|
`docker build . -t cellxgene`
|
||||||
|
|
||||||
|
3 Create the proxy container
|
||||||
|
|
||||||
|
In a separate directory create these two files
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
|
||||||
|
```
|
||||||
|
FROM rgoyard/apache-proxy:latest
|
||||||
|
ADD proxy.conf /conf/
|
||||||
|
```
|
||||||
|
|
||||||
|
proxy.conf
|
||||||
|
|
||||||
|
```
|
||||||
|
ProxyPass "/data/" http://cellxgene:5005/
|
||||||
|
ProxyPassReverse "/data/" http://cellxgene:5005/
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the container
|
||||||
|
`docker build -t proxy .`
|
||||||
|
|
||||||
|
4 Run containers and attach to network
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -d -p 80:80 --network cxg --name proxy proxy
|
||||||
|
docker run -v "$PWD/example-dataset/:/data/" --name cellxgene --network cxg cellxgene launch --host 0.0.0.0 data/pbmc3k.h5ad
|
||||||
|
```
|
||||||
|
|
||||||
|
5 Go to served site
|
||||||
|
|
||||||
|
http://localhost/data/
|
||||||
+1
-2
@@ -1,9 +1,8 @@
|
|||||||
theme: jekyll-theme-cayman
|
theme: jekyll-theme-cayman
|
||||||
show_downloads: false
|
show_downloads: false
|
||||||
|
baseurl: /cellxgene
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
- title: Home
|
|
||||||
url: /
|
|
||||||
- title: Data
|
- title: Data
|
||||||
url: data.html
|
url: data.html
|
||||||
- title: FAQ
|
- title: FAQ
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<h1 class="project-name">{{ site.title | default: site.github.repository_name }}</h1>
|
<h1 class="project-name">{{ site.title | default: site.github.repository_name }}</h1>
|
||||||
<h2 class="project-tagline">{{ site.description | default: site.github.project_tagline }}</h2>
|
<h2 class="project-tagline">{{ site.description | default: site.github.project_tagline }}</h2>
|
||||||
{% if site.nav %}
|
{% if site.nav %}
|
||||||
|
<a href="{{ site.baseurl }}/" class="btn">Home</a>
|
||||||
{% for item in site.nav %}
|
{% for item in site.nav %}
|
||||||
<a href="{{ item.url }}" class="btn">{{ item.title }}</a>
|
<a href="{{ item.url }}" class="btn">{{ item.title }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
@import "{{ site.theme }}";
|
||||||
+3
-3
@@ -14,14 +14,14 @@ description: Data
|
|||||||
### Examination of single cells from primary human pancreas tissue
|
### Examination of single cells from primary human pancreas tissue
|
||||||
cells: 2,544
|
cells: 2,544
|
||||||
tissue(s): pancreas
|
tissue(s): pancreas
|
||||||
data: [GEO Series GSE81547](https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=GSE81547)
|
data: [Human Cell Atlas Data Portal](https://prod.data.humancellatlas.org/explore/projects?filter=%5B%7B%22facetName%22%3A%22organ%22%2C%22terms%22%3A%5B%22pancreas%22%5D%7D%2C%7B%22facetName%22%3A%22project%22%2C%22terms%22%3A%5B%22Single+cell+transcriptome+analysis+of+human+pancreas%22%5D%7D%5D)
|
||||||
paper: [Enge, Martin, et al.](https://www.cell.com/cell/fulltext/S0092-8674(17)31053-X?_returnURL=https%3A%2F%2Flinkinghub.elsevier.com%2Fretrieve%2Fpii%2FS009286741731053X%3Fshowall%3Dtrue)
|
paper: [Enge, Martin, et al.](https://www.cell.com/cell/fulltext/S0092-8674(17)31053-X?_returnURL=https%3A%2F%2Flinkinghub.elsevier.com%2Fretrieve%2Fpii%2FS009286741731053X%3Fshowall%3Dtrue)
|
||||||
|
|
||||||
### Tabula Muris
|
### Tabula Muris
|
||||||
cells: 53,800
|
cells: 53,800
|
||||||
tissue(s): muscle, pancreas, bone, large intestine, heart, brain, fat, mammary gland, tongue , diaphragm, bladder, spleen, thymus, lung , skin, liver, trachea, kidney
|
tissue(s): muscle, pancreas, bone, large intestine, heart, brain, fat, mammary gland, tongue , diaphragm, bladder, spleen, thymus, lung , skin, liver, trachea, kidney
|
||||||
data: [Tabula Muris Data for Python](https://github.com/czbiohub/tabula-muris-vignettes/tree/master/data)
|
data: [Tabula Muris Data](https://github.com/czbiohub/tabula-muris-vignettes/tree/master/data)
|
||||||
paper: [Tabula Muris Consortium.](https://www.nature.com/articles/s41586-018-0590-4)
|
paper: [Tabula Muris Consortium](https://www.nature.com/articles/s41586-018-0590-4)
|
||||||
|
|
||||||
### Transcriptional profiling of 1.3 million brain cells
|
### Transcriptional profiling of 1.3 million brain cells
|
||||||
cells: 1,330,000
|
cells: 1,330,000
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
BUILDDIR := build
|
||||||
|
CLIENTBUILD := $(BUILDDIR)/client
|
||||||
|
SERVERBUILD := $(BUILDDIR)/server
|
||||||
|
CLEANFILES := $(BUILDDIR)/ client/build dist cellxgene.egg-info
|
||||||
|
|
||||||
|
PART ?= patch
|
||||||
|
|
||||||
|
# BUILDING PACKAGE
|
||||||
|
|
||||||
|
build : clean build-server
|
||||||
|
@echo "done"
|
||||||
|
|
||||||
|
build-server : build-client
|
||||||
|
mkdir -p $(SERVERBUILD)
|
||||||
|
cp -r server/* $(SERVERBUILD)
|
||||||
|
cp -r client/build/ $(CLIENTBUILD)
|
||||||
|
mkdir -p $(SERVERBUILD)/app/web/static/img
|
||||||
|
cp $(CLIENTBUILD)/index.html $(SERVERBUILD)/app/web/templates/
|
||||||
|
cp -r $(CLIENTBUILD)/static $(SERVERBUILD)/app/web/
|
||||||
|
cp $(CLIENTBUILD)/favicon.png $(SERVERBUILD)/app/web/static/img
|
||||||
|
cp $(CLIENTBUILD)/service-worker.js $(SERVERBUILD)/app/web/static/js/
|
||||||
|
cp MANIFEST.in README.md setup.cfg setup.py $(BUILDDIR)
|
||||||
|
|
||||||
|
build-client :
|
||||||
|
npm install --prefix client/ client
|
||||||
|
npm run --prefix client build
|
||||||
|
|
||||||
|
# If you are actively developing in the server folder use this, dirties the source tree
|
||||||
|
build-for-server-dev : clean-server build-client
|
||||||
|
mkdir -p server/app/web/static/img
|
||||||
|
cp client/build/index.html server/app/web/templates/
|
||||||
|
cp -r client/build/static server/app/web/
|
||||||
|
cp client/build/favicon.png server/app/web/static/img
|
||||||
|
cp client/build/service-worker.js server/app/web/static/js/
|
||||||
|
|
||||||
|
clean : clean-lite clean-server
|
||||||
|
rm -rf client/node_modules
|
||||||
|
|
||||||
|
# cleaning node_modules is the longest one, so we avoid that if possible
|
||||||
|
clean-lite :
|
||||||
|
rm -rf $(CLEANFILES)
|
||||||
|
|
||||||
|
clean-server :
|
||||||
|
rm -f server/app/web/templates/index.html
|
||||||
|
rm -rf server/app/web/static
|
||||||
|
|
||||||
|
.PHONY : build build-server build-client build-for-server-dev clean clean-lite clean-server
|
||||||
|
|
||||||
|
# CREATING DISTRIBUTION RELEASE
|
||||||
|
|
||||||
|
pydist : build
|
||||||
|
cd $(BUILDDIR); python setup.py sdist -d ../dist
|
||||||
|
@echo "done"
|
||||||
|
|
||||||
|
.PHONY : pydist
|
||||||
|
|
||||||
|
# RELEASE HELPERS
|
||||||
|
|
||||||
|
# create new version to commit to master
|
||||||
|
release-stage-1 : dev-env bump clean-lite gen-package-lock
|
||||||
|
@echo "Version bumped part:$(PART) and client built. Ready to commit and push"
|
||||||
|
|
||||||
|
# build dist and release to dev pypi
|
||||||
|
release-stage-2 : dev-env pydist twine
|
||||||
|
@echo "Dist built and uploaded to test.pypi.org"
|
||||||
|
@echo "Test the install `make install-release-test` and then upload to Pypi prod"
|
||||||
|
@echo "`make twine-prod`"
|
||||||
|
|
||||||
|
release-stage-final: twine-prod
|
||||||
|
@echo "Release uploaded to pypi.org"
|
||||||
|
|
||||||
|
# DANGER: releases directly to prod
|
||||||
|
# use this if you accidently burned a test release version number,
|
||||||
|
release-burned : dev-env pydist twine-prod
|
||||||
|
@echo "Dist built and uploaded to pypi.org"
|
||||||
|
@echo "Test the install `make install-release`"
|
||||||
|
|
||||||
|
dev-env :
|
||||||
|
pip install -r server/requirements-dev.txt
|
||||||
|
|
||||||
|
# give PART=[major, minor, part] as param to make bump
|
||||||
|
bump :
|
||||||
|
bumpversion --config-file .bumpversion.cfg $(PART)
|
||||||
|
|
||||||
|
twine :
|
||||||
|
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
|
||||||
|
|
||||||
|
twine-prod :
|
||||||
|
twine upload dist/*
|
||||||
|
|
||||||
|
# quicker than re-building client
|
||||||
|
gen-package-lock :
|
||||||
|
npm install --prefix client/ client
|
||||||
|
|
||||||
|
.PHONY : release-stage-1 release-stage-2 release-stage-final release-burned dev-env bump twine twine-prod gen-package-lock
|
||||||
|
|
||||||
|
# INSTALL
|
||||||
|
|
||||||
|
# setup.py sucks when you have your library in a separate folder, adding these in to help setup envs
|
||||||
|
|
||||||
|
# install from build directory
|
||||||
|
install : uninstall
|
||||||
|
cd $(BUILDDIR); pip install -e .
|
||||||
|
|
||||||
|
# install from source tree for development
|
||||||
|
install-dev : uninstall
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# install from test.pypi to test your release
|
||||||
|
install-release-test : uninstall
|
||||||
|
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple cellxgene
|
||||||
|
@echo "Installed cellxgene from test.pypi.org, now run and smoke test"
|
||||||
|
|
||||||
|
# install from pypi to test your release
|
||||||
|
install-release : uninstall
|
||||||
|
pip install cellxgene
|
||||||
|
@echo "Installed cellxgene from pypi.org"
|
||||||
|
|
||||||
|
uninstall :
|
||||||
|
yes | pip uninstall cellxgene || true
|
||||||
|
|
||||||
|
.PHONY : install install-dev install-release-test install-release uninstall
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
const ui = SwaggerUIBundle({
|
const ui = SwaggerUIBundle({
|
||||||
url: window.location.origin + "/api/swagger.json",
|
url: window.location.href.replace(/\/swagger$/, "") + "/api/swagger.json",
|
||||||
dom_id: '#swagger-ui',
|
dom_id: '#swagger-ui',
|
||||||
deepLinking: true,
|
deepLinking: true,
|
||||||
presets: [
|
presets: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from flask import Blueprint, render_template, send_from_directory, current_app, request
|
from flask import Blueprint, render_template, send_from_directory, current_app
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("webapp", __name__, template_folder="templates")
|
bp = Blueprint("webapp", __name__, template_folder="templates")
|
||||||
@@ -7,9 +7,8 @@ bp = Blueprint("webapp", __name__, template_folder="templates")
|
|||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
def index():
|
def index():
|
||||||
url_base = request.url_root + "api/"
|
|
||||||
dataset_title = current_app.config["DATASET_TITLE"]
|
dataset_title = current_app.config["DATASET_TITLE"]
|
||||||
return render_template("index.html", prefix=url_base, datasetTitle=dataset_title)
|
return render_template("index.html", datasetTitle=dataset_title)
|
||||||
|
|
||||||
|
|
||||||
# renders swagger documentation
|
# renders swagger documentation
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ from .prepare import prepare
|
|||||||
|
|
||||||
|
|
||||||
@click.group(name="cellxgene", context_settings=dict(max_content_width=85))
|
@click.group(name="cellxgene", context_settings=dict(max_content_width=85))
|
||||||
@click.version_option(version="0.5.1", prog_name="cellxgene", message="[%(prog)s] Version %(version)s")
|
@click.version_option(version="0.6.0", prog_name="cellxgene", message="[%(prog)s] Version %(version)s")
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from server.app.util.utils import custom_format_warning
|
|||||||
show_default=True,
|
show_default=True,
|
||||||
help="Provide verbose output, including warnings and all server requests.",
|
help="Provide verbose output, including warnings and all server requests.",
|
||||||
)
|
)
|
||||||
@click.option("--debug", "-d", is_flag=True, default=False, show_default=True, help="Run in debug mode.")
|
@click.option("--debug", is_flag=True, default=False, show_default=True, help="Run in debug mode.")
|
||||||
@click.option(
|
@click.option(
|
||||||
"--open",
|
"--open",
|
||||||
"-o",
|
"-o",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ with open("server/requirements.txt") as fh:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="cellxgene",
|
name="cellxgene",
|
||||||
version="0.5.1",
|
version="0.6.0",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
url="https://github.com/chanzuckerberg/cellxgene",
|
url="https://github.com/chanzuckerberg/cellxgene",
|
||||||
license="MIT",
|
license="MIT",
|
||||||
|
|||||||
Reference in New Issue
Block a user