mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 08:58:11 +08:00
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:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user