Updating to latest cellxgene client code

Fixes the build error in the old version. 

Reverted the API version since we changed directions from updating the API to refactoring the server structure instead.
This commit is contained in:
Charlotte Weaver
2018-07-05 13:37:26 -07:00
parent 2da62720dd
commit 85506db971
18 changed files with 845 additions and 147 deletions

View File

@@ -0,0 +1,184 @@
// jshint esversion: 6
const BitArray = require("../../src/util/typedCrossfilter/bitArray");
const defaultTestLength = 8;
describe("default select state", () => {
test("newly created Bitarray should be deselected", () => {
const ba = new BitArray(defaultTestLength);
expect(ba).toBeDefined();
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
const dim = ba.allocDimension();
expect(dim).toBeDefined();
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
ba.freeDimension(dim);
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
});
});
describe("select and deselect", () => {
test("selectAll and deselectAll", () => {
const ba = new BitArray(defaultTestLength);
expect(ba).toBeDefined();
const dim1 = ba.allocDimension();
expect(dim1).toBeDefined();
ba.selectAll(dim1);
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(true);
}
const dim2 = ba.allocDimension();
expect(dim2).toBeDefined();
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
ba.selectAll(dim2);
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(true);
}
ba.deselectAll(dim1);
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
ba.deselectAll(dim2);
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
ba.selectAll(dim1);
ba.selectAll(dim2);
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(true);
}
ba.freeDimension(dim1);
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(true);
}
ba.freeDimension(dim2);
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
});
test("selectOne and deselectOne", () => {
const ba = new BitArray(defaultTestLength);
expect(ba).toBeDefined();
const dim = ba.allocDimension();
expect(dim).toBeDefined();
ba.selectOne(dim, 0);
expect(ba.isSelected(0)).toEqual(true);
for (let i = 1; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
ba.deselectOne(dim, 0);
for (let i = 0; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
ba.selectOne(dim, 1);
expect(ba.isSelected(1)).toEqual(true);
expect(ba.isSelected(0)).toEqual(false);
for (let i = 2; i < defaultTestLength; i++) {
expect(ba.isSelected(i)).toEqual(false);
}
ba.selectAll(dim);
ba.deselectOne(dim, defaultTestLength - 1);
expect(ba.isSelected(defaultTestLength - 1)).toEqual(false);
for (let i = 0; i < defaultTestLength - 1; i++) {
expect(ba.isSelected(i)).toEqual(true);
}
});
});
describe("selectionCount", () => {
test("simple", () => {
const ba = new BitArray(defaultTestLength);
expect(ba).toBeDefined();
const dim1 = ba.allocDimension();
expect(dim1).toBeDefined();
const dim2 = ba.allocDimension();
expect(dim2).toBeDefined();
expect(ba.selectionCount).toEqual(0);
ba.selectAll(dim1);
expect(ba.selectionCount).toEqual(0);
ba.selectAll(dim2);
expect(ba.selectionCount).toEqual(defaultTestLength);
for (let i = 0; i < defaultTestLength; i++) {
ba.deselectOne(dim1, i);
expect(ba.selectionCount).toEqual(defaultTestLength - i - 1);
expect(ba.selectionCount).toEqual(ba.countAllOnes());
}
ba.freeDimension(dim1);
ba.freeDimension(dim2);
});
});
describe("fillBySelection", () => {
test("sets values correctly", () => {
const ba = new BitArray(defaultTestLength);
expect(ba).toBeDefined();
const dim = ba.allocDimension();
expect(dim).toBeDefined();
const arr = new Int32Array(defaultTestLength);
arr.fill(0);
const truth = new Int32Array(defaultTestLength);
truth.fill(0);
// initial state should be deselected
ba.fillBySelection(arr, 1, 0);
expect(arr).toEqual(expect.not.arrayContaining([1]));
// selectAll
ba.selectAll(dim);
ba.fillBySelection(arr, 1, 0);
expect(arr).toEqual(expect.not.arrayContaining([0]));
// deselectOne
ba.deselectOne(dim, 3);
ba.fillBySelection(arr, 1, 0);
truth.fill(1);
truth[3] = 0;
expect(arr).toEqual(truth);
// deselectAll
ba.deselectAll(dim);
ba.fillBySelection(arr, 1, 0);
truth.fill(0);
expect(arr).toEqual(truth);
// selectOne
ba.selectOne(dim, 5);
ba.fillBySelection(arr, 6, 1);
truth.fill(1);
truth[5] = 6;
expect(arr).toEqual(truth);
// should be deselected after dimension disposal
ba.freeDimension(dim);
ba.fillBySelection(arr, 3, 9);
truth.fill(9);
expect(arr).toEqual(truth);
});
});

View File

@@ -0,0 +1,140 @@
// jshint esversion: 6
const PositiveIntervals = require("../../src/util/typedCrossfilter/positiveIntervals");
describe("canonicalize", () => {
test("empty", () => {
expect(PositiveIntervals.canonicalize([])).toEqual([]);
});
test("simple, already correct", () => {
expect(PositiveIntervals.canonicalize([[0, 1]])).toEqual([[0, 1]]);
expect(PositiveIntervals.canonicalize([[0, 1], [2, 3]])).toEqual([
[0, 1],
[2, 3]
]);
});
test("non-canonical, need to be canonicalized", () => {
expect(PositiveIntervals.canonicalize([[0, 1], [1, 2]])).toEqual([[0, 2]]);
expect(PositiveIntervals.canonicalize([[1, 2], [2, 3]])).toEqual([[1, 3]]);
});
});
describe("union", () => {
test("empty range", () => {
expect(PositiveIntervals.union([], [])).toEqual([]);
expect(PositiveIntervals.union([], [[1, 2]])).toEqual([[1, 2]]);
expect(PositiveIntervals.union([], [[1, 2], [3, 4]])).toEqual([
[1, 2],
[3, 4]
]);
expect(PositiveIntervals.union([[3, 4]], [])).toEqual([[3, 4]]);
expect(PositiveIntervals.union([[1, 2], [3, 4]], [])).toEqual([
[1, 2],
[3, 4]
]);
expect(PositiveIntervals.union([[3, 3]], [])).toEqual([[3, 3]]);
expect(PositiveIntervals.union([], [[3, 3]])).toEqual([[3, 3]]);
});
test("simple ranges", () => {
expect(PositiveIntervals.union([[1, 2]], [[2, 3]])).toEqual([[1, 3]]);
expect(PositiveIntervals.union([[2, 3]], [[1, 2]])).toEqual([[1, 3]]);
expect(PositiveIntervals.union([[1, 2]], [[3, 4]])).toEqual([
[1, 2],
[3, 4]
]);
expect(
PositiveIntervals.union([[1, 2], [3, 4]], [[6, 7], [19, 40]])
).toEqual([[1, 2], [3, 4], [6, 7], [19, 40]]);
expect(PositiveIntervals.union([[1, 4]], [[1, 1], [3, 4]])).toEqual([
[1, 4]
]);
expect(PositiveIntervals.union([[3, 3]], [[4, 4]])).toEqual([
[3, 3],
[4, 4]
]);
});
});
describe("intersection", () => {
test("empty range", () => {
expect(PositiveIntervals.intersection([], [])).toEqual([]);
expect(PositiveIntervals.intersection([], [[1, 2]])).toEqual([]);
expect(PositiveIntervals.intersection([[1, 2]], [])).toEqual([]);
});
test("simple", () => {
expect(PositiveIntervals.intersection([[1, 2]], [[2, 3]])).toEqual([]);
expect(PositiveIntervals.intersection([[2, 3]], [[1, 2]])).toEqual([]);
expect(PositiveIntervals.intersection([[1, 10]], [[1, 10]])).toEqual([
[1, 10]
]);
expect(PositiveIntervals.intersection([[1, 10]], [[2, 8]])).toEqual([
[2, 8]
]);
expect(PositiveIntervals.intersection([[2, 8]], [[1, 10]])).toEqual([
[2, 8]
]);
expect(PositiveIntervals.intersection([[1, 10]], [[2, 12]])).toEqual([
[2, 10]
]);
expect(PositiveIntervals.intersection([[2, 12]], [[1, 10]])).toEqual([
[2, 10]
]);
expect(PositiveIntervals.intersection([[1, 10]], [[1, 8]])).toEqual([
[1, 8]
]);
expect(PositiveIntervals.intersection([[1, 8]], [[1, 10]])).toEqual([
[1, 8]
]);
expect(PositiveIntervals.intersection([[1, 10]], [[1, 2], [6, 9]])).toEqual(
[[1, 2], [6, 9]]
);
expect(PositiveIntervals.intersection([[0, 2638]], [[1363, 2638]])).toEqual(
[[1363, 2638]]
);
expect(PositiveIntervals.intersection([[1, 2]], [[1, 2]])).toEqual([
[1, 2]
]);
});
});
describe("difference", () => {
test("empty", () => {
expect(PositiveIntervals.difference([], [])).toEqual([]);
expect(PositiveIntervals.difference([], [[1, 10]])).toEqual([]);
expect(PositiveIntervals.difference([[1, 10]], [])).toEqual([[1, 10]]);
});
test("simple", () => {
expect(PositiveIntervals.difference([[1, 2], [3, 4]], [])).toEqual([
[1, 2],
[3, 4]
]);
expect(PositiveIntervals.difference([[1, 2], [3, 10]], [[5, 10]])).toEqual([
[1, 2],
[3, 5]
]);
expect(PositiveIntervals.difference([[1, 2], [3, 10]], [[0, 5]])).toEqual([
[5, 10]
]);
expect(
PositiveIntervals.difference([[0, 2638]], [[0, 1363], [2055, 2638]])
).toEqual([[1363, 2055]]);
expect(
PositiveIntervals.difference([[0, 1363], [2055, 2638]], [[0, 2638]])
).toEqual([]);
expect(PositiveIntervals.difference([[0, 10]], [[0, 1]])).toEqual([
[1, 10]
]);
expect(PositiveIntervals.difference([[0, 10]], [[1, 2]])).toEqual([
[0, 1],
[2, 10]
]);
expect(PositiveIntervals.difference([[0, 10]], [[9, 10]])).toEqual([
[0, 9]
]);
});
});

View File

@@ -0,0 +1,276 @@
// jshint esversion: 6
const _ = require("lodash");
const crossfilter = require("../../src/util/typedCrossfilter");
const someData = [
{
date: "2011-11-14T16:17:54Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001"]
},
{
date: "2011-11-14T16:20:19Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001", "005"]
},
{
date: "2011-11-14T16:28:54Z",
quantity: 1,
total: 300,
tip: 200,
type: "visa",
productIDs: ["004", "005"]
},
{
date: "2011-11-14T16:30:43Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002"]
},
{
date: "2011-11-14T16:48:46Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["005"]
},
{
date: "2011-11-14T16:53:41Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "004", "005"]
},
{
date: "2011-11-14T16:54:06Z",
quantity: 1,
total: 100,
tip: 0,
type: "cash",
productIDs: ["001", "002", "003", "004", "005"]
},
{
date: "2011-11-14T16:58:03Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001"]
},
{
date: "2011-11-14T17:07:21Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["004", "005"]
},
{
date: "2011-11-14T17:22:59Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002", "004", "005"]
},
{
date: "2011-11-14T17:25:45Z",
quantity: 2,
total: 200,
tip: 0,
type: "cash",
productIDs: ["002"]
},
{
date: "2011-11-14T17:29:52Z",
quantity: 1,
total: 200,
tip: 100,
type: "visa",
productIDs: ["004"]
}
];
var payments = null;
beforeEach(() => {
payments = crossfilter(someData);
});
describe("typedCrossfilter", () => {
test("alloc and free", () => {
expect(payments).toBeDefined();
expect(payments.size()).toEqual(someData.length);
expect(payments.all()).toEqual(someData);
const quantity = payments.dimension(r => r.quantity, Int32Array);
expect(quantity).toBeDefined();
expect(quantity.id()).toBeDefined();
quantity.dispose();
expect(payments.size()).toEqual(someData.length);
expect(payments.all()).toEqual(someData);
});
test("filterAll and filterNone", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
expect(quantity).toBeDefined();
expect(tip).toBeDefined();
expect(total).toBeDefined();
expect(type).toBeDefined();
// initially, all should be filtered
expect(payments.allFiltered().length).toEqual(payments.size());
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(someData.length);
// filterAll
tip.filterAll(); // should change nothing
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(someData.length);
// ditto
total.filterAll();
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(someData.length);
// filterNone
type.filterNone();
expect(payments.allFiltered()).toEqual([]);
expect(payments.countFiltered()).toEqual(0);
quantity.filterNone();
expect(payments.allFiltered()).toEqual([]);
expect(payments.countFiltered()).toEqual(0);
// invert the first none; should have no effect because type is
// still not filtered
quantity.filterAll();
expect(payments.allFiltered()).toEqual([]);
expect(payments.countFiltered()).toEqual(0);
// filter all of type; should select all
type.filterAll();
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(payments.size());
});
test("filterExact", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
quantity.filterExact(1);
expect(payments.countFiltered()).toEqual(
_.countBy(someData, "quantity")[1]
);
expect(payments.allFiltered()).toEqual(_.filter(someData, { quantity: 1 }));
tip.filterExact(0);
expect(payments.allFiltered()).toEqual(
_.filter(someData, { tip: 0, quantity: 1 })
);
type.filterExact("cash");
expect(payments.allFiltered()).toEqual(
_.filter(someData, { tip: 0, quantity: 1, type: "cash" })
);
});
test("filterRange", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
tip.filterRange([0, 91]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.tip >= 0 && r.tip < 91)
.value()
);
tip.filterRange([0, 90]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.tip >= 0 && r.tip < 90)
.value()
);
tip.filterRange([1, 90]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.tip >= 1 && r.tip < 91)
.value()
);
});
test("filterEnum", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
type.filterEnum(["tab", "cash"]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.type === "cash" || r.type === "tab")
.value()
);
tip.filterEnum([0, 100]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.type === "cash" || r.type === "tab")
.filter(r => r.tip === 0 || r.tip === 100)
.value()
);
});
test("more than 32 dimensions", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
// Create a bunch of fake dimensions to ensure we can handle > 32
let dimMap = {};
for (let i = 0; i < 65; i++) {
dimMap[i] = payments.dimension(r => Math.random(), Float32Array);
expect(dimMap[i]).toBeDefined();
expect(dimMap[i].id()).toBeDefined();
}
// everything should start as selected/filtered
expect(payments.countFiltered()).toEqual(someData.length);
dimMap[0].filterAll();
dimMap[64].filterAll();
expect(payments.countFiltered()).toEqual(someData.length);
dimMap[33].filterNone();
expect(payments.allFiltered()).toEqual([]);
dimMap[33].filterAll();
expect(payments.allFiltered()).toEqual(someData);
});
});

View File

@@ -9,7 +9,8 @@
"clean": "rimraf build",
"start": "node server/development.js",
"lint": "eslint src",
"prod": "cross-env NODE_ENV=production PORT=3000 node server/production.js"
"prod": "cross-env NODE_ENV=production PORT=3000 node server/production.js",
"test": "jest"
},
"engineStrict": true,
"engines": {
@@ -90,6 +91,7 @@
"gzip-size": "^3.0.0",
"html-webpack-inline-source-plugin": "0.0.6",
"html-webpack-plugin": "^2.22.0",
"jest": "^23.0.1",
"jsdom": "^9.4.1",
"json-loader": "^0.5.4",
"nyc": "^10.0.0",
@@ -106,5 +108,10 @@
"webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.12.2",
"whatwg-fetch": "^2.0.1"
},
"jest": {
"testMatch": [
"**/__tests__/**/?(*.)(spec|test).js?(x)"
]
}
}

View File

@@ -15,7 +15,8 @@ import FaPaintBrush from "react-icons/lib/fa/paint-brush";
@connect(state => {
return {
colorAccessor: state.controls.colorAccessor
colorAccessor: state.controls.colorAccessor,
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap
};
})
class Category extends React.Component {
@@ -26,7 +27,24 @@ class Category extends React.Component {
isExpanded: false
};
}
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;
if (categoriesToggledOn === valuesAsBool.length) {
/* everything is on, so not indeterminate */
this.checkbox.indeterminate = false;
} else if (categoriesToggledOn === 0) {
/* nothing is on, so no */
this.checkbox.indeterminate = false;
} else if (categoriesToggledOn < valuesAsBool.length) {
/* to be explicit... */
this.checkbox.indeterminate = true;
}
}
handleColorChange() {
this.props.dispatch({
type: "color by categorical metadata",
@@ -61,6 +79,16 @@ class Category extends React.Component {
);
});
}
handleToggleAllClick() {
// || this.checkbox.indeterminate === false
if (this.state.isChecked) {
console.log('checked, firing toggle none')
this.toggleNone();
} else if (!this.state.isChecked) {
console.log('!checked, firing toggle all')
this.toggleAll()
}
}
render() {
return (
<div
@@ -102,11 +130,8 @@ class Category extends React.Component {
</span>
{this.props.metadataField}
<input
onChange={
this.state.isChecked
? this.toggleNone.bind(this)
: this.toggleAll.bind(this)
}
onChange={this.handleToggleAllClick.bind(this)}
ref={el => this.checkbox = el}
checked={this.state.isChecked}
type="checkbox"
/>

View File

@@ -7,8 +7,11 @@ https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
import * as globals from "../../globals";
@connect(state => {
console.log("state in histo brush", state)
const ranges =
state.cells.cells && state.cells.cells.data.ranges
? state.cells.cells.data.ranges
@@ -26,7 +29,8 @@ import { connect } from "react-redux";
return {
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
cellsMetadata: state.controls.cellsMetadata
cellsMetadata: state.controls.cellsMetadata,
initializeRanges
};
})
class HistogramBrush extends React.Component {
@@ -154,7 +158,7 @@ class HistogramBrush extends React.Component {
)
.call(d3.axisBottom(x).ticks(5))
.append("text")
.attr("x", 300)
.attr("x", this.width - 2)
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "end")
@@ -164,13 +168,40 @@ class HistogramBrush extends React.Component {
this.setState({ brush, xAxis });
}
}
handleColorAction() {
this.props.dispatch({
type: "color by continuous metadata",
colorAccessor: this.props.metadataField,
rangeMaxForColorAccessor: this.props.initializeRanges[this.props.metadataField].range.max
});
}
render() {
return (
<div
style={{ marginTop: 10 }}
style={{ marginTop: 10, position: "relative" }}
id={"histogram_" + this.props.metadataField}
>
<span
onClick={this.handleColorAction.bind(this)}
style={{
fontSize: 16,
marginLeft: 4,
// padding: this.props.colorAccessor === this.props.metadataField ? 3 : "auto",
borderRadius: 3,
color:
this.props.colorAccessor === this.props.metadataField
? globals.brightBlue
: "black",
// backgroundColor: this.props.colorAccessor === this.props.metadataField ? globals.brightBlue : "inherit",
display: "inline-block",
position: "relative",
left: this.width,
top: this.height - 22,
cursor: "pointer"
}}
>
<FaPaintBrush />
</span>
<svg
width={this.width}
height={this.height}

View File

@@ -103,7 +103,11 @@ class ContinuousLegend extends React.Component {
this.state = {};
}
componentWillReceiveProps(nextProps) {
if (nextProps.colorAccessor !== this.props.colorAccessor) {
if (
nextProps.colorAccessor !== this.props.colorAccessor ||
nextProps.responsive.height !== this.props.responsive.height ||
nextProps.responsive.width !== this.props.responsive.width
) {
/* always remove it, if it's not continuous we don't put it back. */
d3
.select("#continuous_legend")

View File

@@ -164,28 +164,29 @@ class Graph extends React.Component {
nextProps.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("*")
.remove();
const { svg } = setupSVGandBrushElements(
const { svg, brush, brushContainer } = setupSVGandBrushElements(
this.handleBrushSelectAction.bind(this),
this.handleBrushDeselectAction.bind(this),
nextProps.responsive,
this.graphPaddingTop
);
this.setState({ svg });
this.setState({ svg, brush, brushContainer });
}
}
handleBrushSelectAction() {
/*
/* This conditional handles procedural brush deselect. Brush emits an event on procedural deselect because it is move: null */
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 s = d3.event.selection;
/*
event describing brush position:
@-------|
| |
@@ -193,33 +194,47 @@ class Graph extends React.Component {
|-------@
*/
// compute inverse view matrix
const inverse = mat4.invert([], this.state.camera.view());
// compute inverse view matrix
const inverse = mat4.invert([], this.state.camera.view());
// transform screen coordinates -> cell coordinates
const invert = pin => {
const x =
2 * pin[0] / (this.props.responsive.height - this.graphPaddingTop) - 1;
const y =
2 *
(1 - pin[1] / (this.props.responsive.height - this.graphPaddingTop)) -
1;
const pout = [x + inverse[12], y + inverse[13]];
return [(pout[0] + 1) / 2, (pout[1] + 1) / 2];
};
// transform screen coordinates -> cell coordinates
const invert = pin => {
const x =
(2 * pin[0]) / (this.props.responsive.height - this.graphPaddingTop) -
1;
const y =
2 *
(1 -
pin[1] / (this.props.responsive.height - this.graphPaddingTop)) -
1;
const pout = [
x * inverse[14] + inverse[12],
y * inverse[14] + inverse[13]
];
return [(pout[0] + 1) / 2, (pout[1] + 1) / 2];
};
const brushCoords = {
northwest: invert([s[0][0], s[0][1]]),
southeast: invert([s[1][0], s[1][1]])
};
const brushCoords = {
northwest: invert([s[0][0], s[0][1]]),
southeast: invert([s[1][0], s[1][1]])
};
this.props.dispatch({
type: "graph brush selection change",
brushCoords
});
this.props.dispatch({
type: "graph brush selection change",
brushCoords
});
}
}
handleBrushDeselectAction() {
if (!d3.event.selection) {
if (d3.event && !d3.event.selection) {
this.props.dispatch({
type: "graph brush deselect"
});
}
if (!d3.event) {
/* this line clears the brush procedurally, ie., zoom button clicked, not a click away from brush on svg */
this.state.svg.select(".graph_brush").call(this.state.brush.move, null);
this.props.dispatch({
type: "graph brush deselect"
});
@@ -302,6 +317,7 @@ class Graph extends React.Component {
</button>
<button
onClick={() => {
this.handleBrushDeselectAction();
this.setState({ mode: "zoom" });
}}
style={{

View File

@@ -23,21 +23,23 @@ export const setupSVGandBrushElements = (
.attr("height", side)
.attr("class", `${styles.graphSVG}`);
svg.append("g").call(
d3
.brush()
.extent([
[0, 0],
[
responsive.height - graphPaddingTop,
responsive.height - graphPaddingTop
]
])
.on("brush", handleBrushSelectAction)
.on("end", handleBrushDeselectAction)
);
const brush = d3
.brush()
.extent([
[0, 0],
[responsive.height - graphPaddingTop, responsive.height - graphPaddingTop]
])
.on("brush", handleBrushSelectAction)
.on("end", handleBrushDeselectAction);
const brushContainer = svg
.append("g")
.attr("class", "graph_brush")
.call(brush);
return {
svg
svg,
brushContainer,
brush
};
};

View File

@@ -51,6 +51,7 @@ class Scatterplot extends React.Component {
constructor(props) {
super(props);
this.count = 0;
this.axes = false;
this.state = {
svg: null,
// ctx: null,
@@ -63,8 +64,27 @@ class Scatterplot extends React.Component {
componentDidMount() {
const { svg } = setupScatterplot(width, height, margin);
let scales;
/* if we've already got the data, user clicked back and forth between tabs, so render the scatterplot */
if (
this.props.expression &&
this.props.expression.data &&
this.props.scatterplotXXaccessor &&
this.props.scatterplotYYaccessor
) {
scales = this.setupScales(
this.props.expression,
this.props.scatterplotXXaccessor,
this.props.scatterplotYYaccessor
);
this.drawAxesSVG(scales.xScale, scales.yScale, svg);
}
this.setState({
svg
svg,
xScale: scales ? scales.xScale : null,
yScale: scales ? scales.yScale : null
});
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
@@ -104,21 +124,36 @@ class Scatterplot extends React.Component {
});
}
componentWillReceiveProps(nextProps) {
this.maybeSetupScalesAndDrawAxes(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 &&
this.state.xScale &&
this.state.yScale &&
this.props.scatterplotXXaccessor &&
this.props.scatterplotYYaccessor &&
(this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || // was CLU now FTH1 etc
!this.axes) // clicked off the tab and back again, rerender
) {
this.drawAxesSVG(this.state.xScale, this.state.yScale);
this.drawAxesSVG(this.state.xScale, this.state.yScale, this.state.svg);
}
if (
this.props.metadata &&
this.state.regl &&
this.state.pointBuffer &&
this.state.colorBuffer &&
@@ -179,47 +214,32 @@ class Scatterplot extends React.Component {
this.count = cellCount;
}
}
maybeSetupScalesAndDrawAxes(nextProps) {
if (
nextProps.expression &&
nextProps.expression.data &&
nextProps.scatterplotXXaccessor &&
nextProps.scatterplotYYaccessor
) {
const xScale = d3
.scaleLinear()
.domain(
d3.extent(nextProps.expression.data.cells, (cell, i) => {
return cell.e[
nextProps.expression.data.genes.indexOf(
nextProps.scatterplotXXaccessor
)
];
})
)
.range([0, width]);
setupScales(expression, scatterplotXXaccessor, scatterplotYYaccessor) {
const xScale = d3
.scaleLinear()
.domain(
d3.extent(expression.data.cells, (cell, i) => {
return cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)];
})
)
.range([0, width]);
const yScale = d3
.scaleLinear()
.domain(
d3.extent(nextProps.expression.data.cells, cell => {
return cell.e[
nextProps.expression.data.genes.indexOf(
nextProps.scatterplotYYaccessor
)
];
})
)
.range([height, 0]);
const yScale = d3
.scaleLinear()
.domain(
d3.extent(expression.data.cells, cell => {
return cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)];
})
)
.range([height, 0]);
this.setState({
xScale,
yScale
});
}
return {
xScale,
yScale
};
}
drawAxesSVG(xScale, yScale) {
this.state.svg.selectAll("*").remove();
drawAxesSVG(xScale, yScale, svg) {
svg.selectAll("*").remove();
// the axes are much cleaner and easier now. No need to rotate and orient the axis, just call axisBottom, axisLeft etc.
var xAxis = d3.axisBottom().scale(xScale);
@@ -227,28 +247,28 @@ class Scatterplot extends React.Component {
var yAxis = d3.axisLeft().scale(yScale);
// adding axes is also simpler now, just translate x-axis to (0,height) and it's alread defined to be a bottom axis.
this.state.svg
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "x axis")
.call(xAxis);
// y-axis is translated to (0,0)
this.state.svg
svg
.append("g")
.attr("transform", "translate(0,0)")
.attr("class", "y axis")
.call(yAxis);
// adding label. For x-axis, it's at (10, 10), and for y-axis at (width, height-10).
this.state.svg
svg
.append("text")
.attr("x", 10)
.attr("y", 10)
.attr("class", "label")
.text(this.props.scatterplotYYaccessor);
this.state.svg
svg
.append("text")
.attr("x", width)
.attr("y", height - 10)

View File

@@ -48,10 +48,10 @@ export const bolder = 700;
export let API = {
// prefix: "http://api.clustering.czi.technology/api/",
//prefix: "http://tabulamuris.cxg.czi.technology/api/",
prefix: "http://pbmc3k.cxg.czi.technology/api/",
// prefix: "http://pbmc3k.cxg.czi.technology/api/",
// prefix: "http://pbmc33k.cxg.czi.technology/api/",
// prefix: "http://api-staging.clustering.czi.technology/api/",
prefix: "http://api-staging.clustering.czi.technology/api/",
version: "v0.1/"
};

View File

@@ -2,7 +2,7 @@
import _ from "lodash";
import { parseRGB } from "../util/parseRGB";
import { createSchemaByDataSniffing } from "../util/schema";
var crossfilter = require("../util/typedCrossfilter");
import crossfilter from "../util/typedCrossfilter";
// Deduce the correct crossfilter dimension type from a metadata
// schema description.

View File

@@ -140,7 +140,7 @@ class BitArray {
const col = dim >>> 5;
const before = this.bitarray[col * this.length + index];
const after = before | (1 << (dim % 32));
this.bitarray[col] = after;
this.bitarray[col * this.length + index] = after;
}
// deselect index on dimension
@@ -149,7 +149,7 @@ class BitArray {
const col = dim >>> 5;
const before = this.bitarray[col * this.length + index];
const after = before & ~(1 << (dim % 32));
this.bitarray[col] = after;
this.bitarray[col * this.length + index] = after;
}
// select all indices on dimension.
@@ -214,7 +214,8 @@ class BitArray {
const bitmask = this.bitmask[0];
const bitarray = this.bitarray;
for (let i = 0, len = this.length; i < len; i++) {
result[i] = bitarray[i] === bitmask ? selectedValue : deselectedValue;
result[i] =
bitmask && bitarray[i] === bitmask ? selectedValue : deselectedValue;
}
} else {
for (let i = 0, len = this.length; i < len; i++) {
@@ -225,4 +226,4 @@ class BitArray {
}
}
module.exports = BitArray;
export default BitArray;

View File

@@ -29,9 +29,9 @@ more complex API. In a few cases, elements of that API were incorporated.
*/
var PositiveIntervals = require("./positiveIntervals");
var BitArray = require("./bitArray");
var Util = require("./util");
import PositiveIntervals from "./positiveIntervals";
import BitArray from "./bitArray";
import {fillRange, lowerBound, lowerBoundIndirect, upperBound, upperBoundIndirect} from "./util";
class TypedCrossfilter {
constructor(data) {
@@ -65,7 +65,7 @@ class TypedCrossfilter {
_freeDimension(id) {
this.selection.freeDimension(id);
this.filters = this.filters.filter(f => f.id != id);
this.filters = this.filters.filter(f => f._id != id);
}
// return array of all records that are selected/filtered
@@ -105,7 +105,7 @@ class TypedCrossfilter {
class ScalarDimension {
constructor(value, valueArrayType, crossfilter, id) {
this.crossfilter = crossfilter;
this.id = id;
this._id = id;
// current selection filter, expressed as PostiveIntervals.
this.currentFilter = [];
@@ -118,7 +118,7 @@ class ScalarDimension {
this.value = array;
// create sort index
this.index = Util.fillRange(new Uint32Array(this.crossfilter.data.length));
this.index = fillRange(new Uint32Array(this.crossfilter.data.length));
this.index.sort((a, b) => array[a] - array[b]);
}
@@ -133,11 +133,11 @@ class ScalarDimension {
}
dispose() {
this.crossfilter._freeDimension(this.id);
this.crossfilter._freeDimension(this._id);
}
id() {
return this.id;
return this._id;
}
_updateFilters(newFilter) {
@@ -147,26 +147,26 @@ class ScalarDimension {
// more complex work and just clobber everything.
//
if (newFilter.length === 0) {
this.crossfilter.selection.deselectAll(this.id);
this.crossfilter.selection.deselectAll(this._id);
} else if (
newFilter.length === 1 &&
newFilter[0][0] === 0 &&
newFilter[0][1] == this.index.length
) {
this.crossfilter.selection.selectAll(this.id);
this.crossfilter.selection.selectAll(this._id);
} else {
const adds = PositiveIntervals.difference(newFilter, this.currentFilter);
const dels = PositiveIntervals.difference(this.currentFilter, newFilter);
dels.forEach(interval =>
this.crossfilter.selection.deselectIndirectFromRange(
this.id,
this._id,
this.index,
interval
)
);
adds.forEach(interval =>
this.crossfilter.selection.selectIndirectFromRange(
this.id,
this._id,
this.index,
interval
)
@@ -179,14 +179,14 @@ class ScalarDimension {
// filter by value - exact match
filterExact(value) {
const newFilter = [
Util.lowerBoundIndirect(
lowerBoundIndirect(
this.value,
this.index,
value,
0,
this.value.length
),
Util.upperBoundIndirect(
upperBoundIndirect(
this.value,
this.index,
value,
@@ -207,14 +207,14 @@ class ScalarDimension {
const newFilter = [];
for (let v = 0, len = values.length; v < len; v++) {
const intv = [
Util.lowerBoundIndirect(
lowerBoundIndirect(
this.value,
this.index,
values[v],
0,
this.value.length
),
Util.upperBoundIndirect(
upperBoundIndirect(
this.value,
this.index,
values[v],
@@ -233,14 +233,14 @@ class ScalarDimension {
filterRange(range) {
const newFilter = [];
const intv = [
Util.lowerBoundIndirect(
lowerBoundIndirect(
this.value,
this.index,
range[0],
0,
this.value.length
),
Util.upperBoundIndirect(
upperBoundIndirect(
this.value,
this.index,
range[1],
@@ -350,7 +350,7 @@ class EnumDimension extends ScalarDimension {
const enumLen = this.enumIndex.length;
for (let i = 0; i < len; i++) {
const v = value(data[i]);
const e = Util.lowerBound(this.enumIndex, v, 0, enumLen);
const e = lowerBound(this.enumIndex, v, 0, enumLen);
array[i] = e;
}
return array;
@@ -358,14 +358,14 @@ class EnumDimension extends ScalarDimension {
filterExact(value) {
return super.filterExact(
Util.lowerBound(this.enumIndex, value, 0, this.enumIndex.length)
lowerBound(this.enumIndex, value, 0, this.enumIndex.length)
);
}
filterEnum(values) {
return super.filterEnum(
values.map(v =>
Util.lowerBound(this.enumIndex, v, 0, this.enumIndex.length)
lowerBound(this.enumIndex, v, 0, this.enumIndex.length)
)
);
}
@@ -373,7 +373,7 @@ class EnumDimension extends ScalarDimension {
filterRange(range) {
return super.filterEnum(
range.map(v =>
Util.lowerBound(this.enumIndex, v, 0, this.enumIndex.length)
lowerBound(this.enumIndex, v, 0, this.enumIndex.length)
)
);
}
@@ -391,4 +391,4 @@ crossfilter.TypedCrossfilter = TypedCrossfilter;
crossfilter.ScalarDimension = ScalarDimension;
crossfilter.EnumDimension = EnumDimension;
module.exports = crossfilter;
export default crossfilter;

View File

@@ -129,4 +129,4 @@ class PositiveIntervals {
}
}
module.exports = PositiveIntervals;
export default PositiveIntervals;

View File

@@ -8,7 +8,7 @@
// fill an array or typedarray with a sequential range of numbers,
// starting with `start`
//
function fillRange(arr, start = 0) {
export function fillRange(arr, start = 0) {
for (let i = 0, len = arr.length; i < len; i++) {
arr[i] = i + start;
}
@@ -30,7 +30,7 @@ function fillRange(arr, start = 0) {
// a factory version of lowerBound that takes an accessor (rather than having
// a special-cased version for lining the indirection).
//
function lowerBound(valueArray, value, first, last) {
export function lowerBound(valueArray, value, first, last) {
// this is just a binary search
while (first < last) {
const middle = (first + last) >>> 1;
@@ -45,7 +45,7 @@ function lowerBound(valueArray, value, first, last) {
// Inlined performance optimization - used to indirect through a sort map.
//
function lowerBoundIndirect(valueArray, indexArray, value, first, last) {
export function lowerBoundIndirect(valueArray, indexArray, value, first, last) {
// this is just a binary search
while (first < last) {
const middle = (first + last) >>> 1;
@@ -69,7 +69,7 @@ function lowerBoundIndirect(valueArray, indexArray, value, first, last) {
// C++: upper_bound()
// Python: bisect.bisect_right()
//
function upperBound(valueArray, value, first, last) {
export function upperBound(valueArray, value, first, last) {
// this is just a binary search
while (first < last) {
const middle = (first + last) >>> 1;
@@ -84,7 +84,7 @@ function upperBound(valueArray, value, first, last) {
// Inline performance optimization
//
function upperBoundIndirect(valueArray, indexArray, value, first, last) {
export function upperBoundIndirect(valueArray, indexArray, value, first, last) {
// this is just a binary search
while (first < last) {
const middle = (first + last) >>> 1;
@@ -96,11 +96,3 @@ function upperBoundIndirect(valueArray, indexArray, value, first, last) {
}
return first;
}
module.exports = {
fillRange,
lowerBound,
lowerBoundIndirect,
upperBound,
upperBoundIndirect
};