mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-06 16:48:11 +08:00
performance and correctness fixes to brushable histogram (#1390)
This commit is contained in:
@@ -24,10 +24,10 @@ import { makeContinuousDimensionName } from "../../util/nameCreators";
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
world: state.world,
|
world: state.world,
|
||||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
isScatterplotXXaccessor: state.controls.scatterplotXXaccessor === field,
|
||||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
isScatterplotYYaccessor: state.controls.scatterplotYYaccessor === field,
|
||||||
continuousSelectionRange: state.continuousSelection[myName],
|
continuousSelectionRange: state.continuousSelection[myName],
|
||||||
colorAccessor: state.colors.colorAccessor
|
isColorAccessor: state.colors.colorAccessor === field,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
class HistogramBrush extends React.PureComponent {
|
class HistogramBrush extends React.PureComponent {
|
||||||
@@ -47,7 +47,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
return varData.col(field);
|
return varData.col(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
calcHistogramCache = memoize(col => {
|
calcHistogramCache = memoize((col) => {
|
||||||
/*
|
/*
|
||||||
recalculate expensive stuff, notably bins, summaries, etc.
|
recalculate expensive stuff, notably bins, summaries, etc.
|
||||||
*/
|
*/
|
||||||
@@ -61,13 +61,17 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
.domain([domainMin, domainMax])
|
.domain([domainMin, domainMax])
|
||||||
.range([this.marginLeft, this.marginLeft + this.width]);
|
.range([this.marginLeft, this.marginLeft + this.width]);
|
||||||
|
|
||||||
histogramCache.bins = histogramContinuous(col, numBins, [domainMin, domainMax]);
|
histogramCache.bins = histogramContinuous(col, numBins, [
|
||||||
|
domainMin,
|
||||||
|
domainMax,
|
||||||
|
]);
|
||||||
histogramCache.binWidth = (domainMax - domainMin) / numBins;
|
histogramCache.binWidth = (domainMax - domainMin) / numBins;
|
||||||
|
|
||||||
histogramCache.binStart = i => domainMin + i * histogramCache.binWidth;
|
histogramCache.binStart = (i) => domainMin + i * histogramCache.binWidth;
|
||||||
histogramCache.binEnd = i => domainMin + (i + 1) * histogramCache.binWidth;
|
histogramCache.binEnd = (i) =>
|
||||||
|
domainMin + (i + 1) * histogramCache.binWidth;
|
||||||
|
|
||||||
const yMax = histogramCache.bins.reduce((l, r) => l > r ? l : r);
|
const yMax = histogramCache.bins.reduce((l, r) => (l > r ? l : r));
|
||||||
|
|
||||||
histogramCache.y = d3
|
histogramCache.y = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
@@ -90,32 +94,47 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { field, colorAccessor } = this.props;
|
const { field, isColorAccessor } = this.props;
|
||||||
|
|
||||||
this.renderHistogram(this._histogram, field, colorAccessor);
|
this.renderHistogram(this._histogram, field, isColorAccessor);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { field, world, continuousSelectionRange: range, colorAccessor } = this.props;
|
const {
|
||||||
|
field,
|
||||||
|
world,
|
||||||
|
continuousSelectionRange: range,
|
||||||
|
isColorAccessor,
|
||||||
|
} = this.props;
|
||||||
const { x } = this._histogram;
|
const { x } = this._histogram;
|
||||||
let { brushX, brushXselection } = this.state;
|
let { brushX, brushXselection } = this.state;
|
||||||
|
|
||||||
const dfColumn = HistogramBrush.getColumn(world, field);
|
const dfColumn = HistogramBrush.getColumn(world, field);
|
||||||
const oldDfColumn = HistogramBrush.getColumn(prevProps.world, prevProps.field);
|
const oldDfColumn = HistogramBrush.getColumn(
|
||||||
|
prevProps.world,
|
||||||
|
prevProps.field
|
||||||
|
);
|
||||||
|
|
||||||
const rangeChanged = range !== prevProps.continuousSelectionRange;
|
const rangeChanged = range !== prevProps.continuousSelectionRange;
|
||||||
const dfChanged = dfColumn !== oldDfColumn;
|
const dfChanged = dfColumn !== oldDfColumn;
|
||||||
const colorSelectionChanged = prevProps.colorAccessor !== colorAccessor;
|
const colorSelectionChanged = prevProps.isColorAccessor !== isColorAccessor;
|
||||||
|
|
||||||
if (dfChanged || colorSelectionChanged) {
|
if (dfChanged || colorSelectionChanged) {
|
||||||
({brushX, brushXselection} = this.renderHistogram(this._histogram, field, colorAccessor));
|
({ brushX, brushXselection } = this.renderHistogram(
|
||||||
|
this._histogram,
|
||||||
|
field,
|
||||||
|
isColorAccessor
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if the selection has changed, ensure that the brush correctly reflects
|
if the selection has changed, ensure that the brush correctly reflects
|
||||||
the underlying selection.
|
the underlying selection.
|
||||||
*/
|
*/
|
||||||
if ((dfChanged || rangeChanged || colorSelectionChanged) && brushXselection) {
|
if (
|
||||||
|
(dfChanged || rangeChanged || colorSelectionChanged) &&
|
||||||
|
brushXselection
|
||||||
|
) {
|
||||||
const selection = d3.brushSelection(brushXselection.node());
|
const selection = d3.brushSelection(brushXselection.node());
|
||||||
if (!range && selection) {
|
if (!range && selection) {
|
||||||
/* no active selection - clear brush */
|
/* no active selection - clear brush */
|
||||||
@@ -160,9 +179,9 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
continuousNamespace: {
|
continuousNamespace: {
|
||||||
isObs,
|
isObs,
|
||||||
isUserDefined,
|
isUserDefined,
|
||||||
isDiffExp
|
isDiffExp,
|
||||||
},
|
},
|
||||||
range: [x(d3.event.selection[0]), x(d3.event.selection[1])]
|
range: [x(d3.event.selection[0]), x(d3.event.selection[1])],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -171,9 +190,9 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
continuousNamespace: {
|
continuousNamespace: {
|
||||||
isObs,
|
isObs,
|
||||||
isUserDefined,
|
isUserDefined,
|
||||||
isDiffExp
|
isDiffExp,
|
||||||
},
|
},
|
||||||
range: null
|
range: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -216,9 +235,9 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
continuousNamespace: {
|
continuousNamespace: {
|
||||||
isObs,
|
isObs,
|
||||||
isUserDefined,
|
isUserDefined,
|
||||||
isDiffExp
|
isDiffExp,
|
||||||
},
|
},
|
||||||
range: _range
|
range: _range,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -227,8 +246,8 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
continuousNamespace: {
|
continuousNamespace: {
|
||||||
isObs,
|
isObs,
|
||||||
isUserDefined,
|
isUserDefined,
|
||||||
isDiffExp
|
isDiffExp,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -238,7 +257,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
const { dispatch, field } = this.props;
|
const { dispatch, field } = this.props;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "set scatterplot x",
|
type: "set scatterplot x",
|
||||||
data: field
|
data: field,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,7 +265,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
const { dispatch, field } = this.props;
|
const { dispatch, field } = this.props;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "set scatterplot y",
|
type: "set scatterplot y",
|
||||||
data: field
|
data: field,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -257,7 +276,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: "color by continuous metadata",
|
type: "color by continuous metadata",
|
||||||
colorAccessor: field,
|
colorAccessor: field,
|
||||||
rangeForColorAccessor: ranges
|
rangeForColorAccessor: ranges,
|
||||||
});
|
});
|
||||||
} else if (world.varData.hasCol(field)) {
|
} else if (world.varData.hasCol(field)) {
|
||||||
dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field));
|
dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field));
|
||||||
@@ -268,29 +287,29 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
const {
|
const {
|
||||||
dispatch,
|
dispatch,
|
||||||
field,
|
field,
|
||||||
colorAccessor,
|
isColorAccessor,
|
||||||
scatterplotXXaccessor,
|
isScatterplotXXaccessor,
|
||||||
scatterplotYYaccessor
|
isScatterplotYYaccessor,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "clear user defined gene",
|
type: "clear user defined gene",
|
||||||
data: field
|
data: field,
|
||||||
});
|
});
|
||||||
if (field === colorAccessor) {
|
if (isColorAccessor) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "reset colorscale"
|
type: "reset colorscale",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (field === scatterplotXXaccessor) {
|
if (isScatterplotXXaccessor) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "set scatterplot x",
|
type: "set scatterplot x",
|
||||||
data: null
|
data: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (field === scatterplotYYaccessor) {
|
if (isScatterplotYYaccessor) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "set scatterplot y",
|
type: "set scatterplot y",
|
||||||
data: null
|
data: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -300,11 +319,11 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
const col = HistogramBrush.getColumn(world, field);
|
const col = HistogramBrush.getColumn(world, field);
|
||||||
this._histogram = {
|
this._histogram = {
|
||||||
...this.calcHistogramCache(col),
|
...this.calcHistogramCache(col),
|
||||||
svgRef
|
svgRef,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHistogram(histogram, field, colorAccessor) {
|
renderHistogram(histogram, field, isColorAccessor) {
|
||||||
const { x, y, bins, svgRef, binStart, binEnd, binWidth } = histogram;
|
const { x, y, bins, svgRef, binStart, binEnd, binWidth } = histogram;
|
||||||
const svg = d3.select(svgRef);
|
const svg = d3.select(svgRef);
|
||||||
|
|
||||||
@@ -319,14 +338,16 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
.attr("class", "histogram-container")
|
.attr("class", "histogram-container")
|
||||||
.attr("transform", `translate(${this.marginLeft},${this.marginTop})`);
|
.attr("transform", `translate(${this.marginLeft},${this.marginTop})`);
|
||||||
|
|
||||||
const colorScale = d3.scaleSequential(interpolateCool).domain([0, bins.length]);
|
const colorScale = d3
|
||||||
|
.scaleSequential(interpolateCool)
|
||||||
|
.domain([0, bins.length]);
|
||||||
|
|
||||||
const histogramScale = d3
|
const histogramScale = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.domain(x.domain())
|
.domain(x.domain())
|
||||||
.range([
|
.range([
|
||||||
colorScale.domain()[1],
|
colorScale.domain()[1],
|
||||||
colorScale.domain()[0]
|
colorScale.domain()[0],
|
||||||
]); /* we flip this to make colors dark if high in the color scale */
|
]); /* we flip this to make colors dark if high in the color scale */
|
||||||
|
|
||||||
if (binWidth > 0) {
|
if (binWidth > 0) {
|
||||||
@@ -338,10 +359,15 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
.enter()
|
.enter()
|
||||||
.append("rect")
|
.append("rect")
|
||||||
.attr("x", (d, i) => x(binStart(i)) + 1)
|
.attr("x", (d, i) => x(binStart(i)) + 1)
|
||||||
.attr("y", d => y(d))
|
.attr("y", (d) => y(d))
|
||||||
.attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - 1)
|
.attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - 1)
|
||||||
.attr("height", d => y(0) - y(d))
|
.attr("height", (d) => y(0) - y(d))
|
||||||
.style("fill", colorAccessor === field ? (d, i) => colorScale(histogramScale(binStart(i))) : "#bbb");
|
.style(
|
||||||
|
"fill",
|
||||||
|
isColorAccessor
|
||||||
|
? (d, i) => colorScale(histogramScale(binStart(i)))
|
||||||
|
: "#bbb"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// BRUSH
|
// BRUSH
|
||||||
@@ -350,7 +376,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
.brushX()
|
.brushX()
|
||||||
.extent([
|
.extent([
|
||||||
[x.range()[0], y.range()[1]],
|
[x.range()[0], y.range()[1]],
|
||||||
[x.range()[1], this.marginTop + this.height + this.marginBottom]
|
[x.range()[1], this.marginTop + this.height + this.marginBottom],
|
||||||
])
|
])
|
||||||
/*
|
/*
|
||||||
emit start so that the Undoable history can save an undo point
|
emit start so that the Undoable history can save an undo point
|
||||||
@@ -375,7 +401,11 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
d3
|
d3
|
||||||
.axisBottom(x)
|
.axisBottom(x)
|
||||||
.ticks(4)
|
.ticks(4)
|
||||||
.tickFormat(d3.format(x.domain().some(n => Math.abs(n) >= 10000) ? ".2e" : ","))
|
.tickFormat(
|
||||||
|
d3.format(
|
||||||
|
x.domain().some((n) => Math.abs(n) >= 10000) ? ".2e" : ","
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Y AXIS */
|
/* Y AXIS */
|
||||||
@@ -387,7 +417,11 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
d3
|
d3
|
||||||
.axisRight(y)
|
.axisRight(y)
|
||||||
.ticks(3)
|
.ticks(3)
|
||||||
.tickFormat(d3.format(y.domain().some(n => Math.abs(n) >= 10000) ? ".0e" : ","))
|
.tickFormat(
|
||||||
|
d3.format(
|
||||||
|
y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
/* axis style */
|
/* axis style */
|
||||||
@@ -404,19 +438,19 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
const {
|
const {
|
||||||
field,
|
field,
|
||||||
world,
|
world,
|
||||||
colorAccessor,
|
isColorAccessor,
|
||||||
isUserDefined,
|
isUserDefined,
|
||||||
isDiffExp,
|
isDiffExp,
|
||||||
logFoldChange,
|
logFoldChange,
|
||||||
pvalAdj,
|
pvalAdj,
|
||||||
scatterplotXXaccessor,
|
isScatterplotXXaccessor,
|
||||||
scatterplotYYaccessor,
|
isScatterplotYYaccessor,
|
||||||
zebra
|
zebra,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const fieldForId = field.replace(/\s/g, "_");
|
const fieldForId = field.replace(/\s/g, "_");
|
||||||
const {
|
const {
|
||||||
min: unclippedRangeMin,
|
min: unclippedRangeMin,
|
||||||
max: unclippedRangeMax
|
max: unclippedRangeMax,
|
||||||
} = HistogramBrush.getColumn(world, field, false).summarize();
|
} = HistogramBrush.getColumn(world, field, false).summarize();
|
||||||
const unclippedRangeMinColor =
|
const unclippedRangeMinColor =
|
||||||
world.clipQuantiles.min === 0 ? "#bbb" : globals.blue;
|
world.clipQuantiles.min === 0 ? "#bbb" : globals.blue;
|
||||||
@@ -436,14 +470,14 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
padding: globals.leftSidebarSectionPadding,
|
padding: globals.leftSidebarSectionPadding,
|
||||||
backgroundColor: zebra ? globals.lightestGrey : "white"
|
backgroundColor: zebra ? globals.lightestGrey : "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
paddingBottom: "8px"
|
paddingBottom: "8px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isDiffExp || isUserDefined ? (
|
{isDiffExp || isUserDefined ? (
|
||||||
@@ -456,16 +490,16 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
<Button
|
<Button
|
||||||
data-testid={`plot-x-${field}`}
|
data-testid={`plot-x-${field}`}
|
||||||
onClick={this.handleSetGeneAsScatterplotX}
|
onClick={this.handleSetGeneAsScatterplotX}
|
||||||
active={scatterplotXXaccessor === field}
|
active={isScatterplotXXaccessor}
|
||||||
intent={scatterplotXXaccessor === field ? "primary" : "none"}
|
intent={isScatterplotXXaccessor ? "primary" : "none"}
|
||||||
>
|
>
|
||||||
plot x
|
plot x
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-testid={`plot-y-${field}`}
|
data-testid={`plot-y-${field}`}
|
||||||
onClick={this.handleSetGeneAsScatterplotY}
|
onClick={this.handleSetGeneAsScatterplotY}
|
||||||
active={scatterplotYYaccessor === field}
|
active={isScatterplotYYaccessor}
|
||||||
intent={scatterplotYYaccessor === field ? "primary" : "none"}
|
intent={isScatterplotYYaccessor ? "primary" : "none"}
|
||||||
>
|
>
|
||||||
plot y
|
plot y
|
||||||
</Button>
|
</Button>
|
||||||
@@ -479,7 +513,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
style={{
|
style={{
|
||||||
color: globals.blue,
|
color: globals.blue,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
marginLeft: 7
|
marginLeft: 7,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
remove
|
remove
|
||||||
@@ -492,8 +526,8 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={this.handleColorAction}
|
onClick={this.handleColorAction}
|
||||||
active={colorAccessor === field}
|
active={isColorAccessor}
|
||||||
intent={colorAccessor === field ? "primary" : "none"}
|
intent={isColorAccessor ? "primary" : "none"}
|
||||||
data-testclass="colorby"
|
data-testclass="colorby"
|
||||||
data-testid={`colorby-${field}`}
|
data-testid={`colorby-${field}`}
|
||||||
icon="tint"
|
icon="tint"
|
||||||
@@ -506,12 +540,12 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
id={`histogram_${fieldForId}_svg`}
|
id={`histogram_${fieldForId}_svg`}
|
||||||
data-testclass="histogram-plot"
|
data-testclass="histogram-plot"
|
||||||
data-testid={`histogram-${field}-plot`}
|
data-testid={`histogram-${field}-plot`}
|
||||||
ref={svgRef => this.drawHistogram(svgRef)}
|
ref={(svgRef) => this.drawHistogram(svgRef)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between"
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ color: unclippedRangeMinColor }}>
|
<span style={{ color: unclippedRangeMinColor }}>
|
||||||
@@ -533,7 +567,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "baseline"
|
alignItems: "baseline",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -543,7 +577,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 7,
|
marginLeft: 7,
|
||||||
padding: 2
|
padding: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>p-value (adj):</strong>
|
<strong>p-value (adj):</strong>
|
||||||
|
|||||||
@@ -8,32 +8,20 @@ import { Button } from "@blueprintjs/core";
|
|||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import HistogramBrush from "../brushableHistogram";
|
import HistogramBrush from "../brushableHistogram";
|
||||||
|
|
||||||
@connect(state => ({
|
@connect((state) => ({
|
||||||
obsAnnotations: state.world?.obsAnnotations,
|
obsAnnotations: state.world?.obsAnnotations,
|
||||||
colorAccessor: state.colors.colorAccessor,
|
colorAccessor: state.colors.colorAccessor,
|
||||||
colorScale: state.colors.scale,
|
colorScale: state.colors.scale,
|
||||||
schema: state.world?.schema
|
schema: state.world?.schema,
|
||||||
}))
|
}))
|
||||||
class Continuous extends React.PureComponent {
|
class Continuous extends React.PureComponent {
|
||||||
handleColorAction = key => {
|
|
||||||
return () => {
|
|
||||||
const { dispatch, obsAnnotations } = this.props;
|
|
||||||
const summary = obsAnnotations.col(key).summarize();
|
|
||||||
dispatch({
|
|
||||||
type: "color by continuous metadata",
|
|
||||||
colorAccessor: key,
|
|
||||||
rangeForColorAccessor: summary
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
static renderIsStillLoading(zebra, key) {
|
static renderIsStillLoading(zebra, key) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
style={{
|
style={{
|
||||||
padding: globals.leftSidebarSectionPadding,
|
padding: globals.leftSidebarSectionPadding,
|
||||||
backgroundColor: zebra % 2 === 0 ? globals.lightestGrey : "white"
|
backgroundColor: zebra % 2 === 0 ? globals.lightestGrey : "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -41,7 +29,7 @@ class Continuous extends React.PureComponent {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
justifyItems: "center",
|
justifyItems: "center",
|
||||||
alignItems: "center"
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ minWidth: 30 }} />
|
<div style={{ minWidth: 30 }} />
|
||||||
@@ -51,7 +39,7 @@ class Continuous extends React.PureComponent {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-end"
|
justifyContent: "flex-end",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button minimal loading intent="primary" />
|
<Button minimal loading intent="primary" />
|
||||||
@@ -66,16 +54,16 @@ class Continuous extends React.PureComponent {
|
|||||||
|
|
||||||
const obsIndex = schema.annotations.obs.index;
|
const obsIndex = schema.annotations.obs.index;
|
||||||
const allContinuousNames = schema.annotations.obs.columns
|
const allContinuousNames = schema.annotations.obs.columns
|
||||||
.filter(col => col.type === "int32" || col.type === "float32")
|
.filter((col) => col.type === "int32" || col.type === "float32")
|
||||||
.filter(col => col.name !== obsIndex)
|
.filter((col) => col.name !== obsIndex)
|
||||||
.map(col => col.name);
|
.map((col) => col.name);
|
||||||
|
|
||||||
/* initial value for iterator to simulate index, ranges is an object */
|
/* initial value for iterator to simulate index, ranges is an object */
|
||||||
let zebra = 0;
|
let zebra = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{allContinuousNames.map(key => {
|
{allContinuousNames.map((key) => {
|
||||||
if (!obsAnnotations.hasCol(key)) {
|
if (!obsAnnotations.hasCol(key)) {
|
||||||
// still loading!
|
// still loading!
|
||||||
zebra += 1;
|
zebra += 1;
|
||||||
@@ -98,7 +86,6 @@ class Continuous extends React.PureComponent {
|
|||||||
isObs
|
isObs
|
||||||
zebra={zebra % 2 === 0}
|
zebra={zebra % 2 === 0}
|
||||||
ranges={summary}
|
ranges={summary}
|
||||||
handleColorAction={this.handleColorAction(key)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const ContinuousSelection = (state = {}, action) => {
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[name]: action.range
|
[name]: action.range,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "continuous metadata histogram cancel": {
|
case "continuous metadata histogram cancel": {
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import Crossfilter from "../util/typedCrossfilter";
|
|||||||
import {
|
import {
|
||||||
World,
|
World,
|
||||||
ControlsHelpers,
|
ControlsHelpers,
|
||||||
AnnotationsHelpers
|
AnnotationsHelpers,
|
||||||
} from "../util/stateManager";
|
} from "../util/stateManager";
|
||||||
import {
|
import {
|
||||||
layoutDimensionName,
|
layoutDimensionName,
|
||||||
obsAnnoDimensionName,
|
obsAnnoDimensionName,
|
||||||
userDefinedDimensionName,
|
userDefinedDimensionName,
|
||||||
diffexpDimensionName,
|
diffexpDimensionName,
|
||||||
makeContinuousDimensionName
|
makeContinuousDimensionName,
|
||||||
} from "../util/nameCreators";
|
} from "../util/nameCreators";
|
||||||
|
|
||||||
const XYDimName = layoutDimensionName("XY");
|
const XYDimName = layoutDimensionName("XY");
|
||||||
@@ -124,7 +124,7 @@ const CrossfilterReducerBase = (
|
|||||||
case "request differential expression success": {
|
case "request differential expression success": {
|
||||||
const { world } = prevSharedState;
|
const { world } = prevSharedState;
|
||||||
const varIndexName = world.schema.annotations.var.index;
|
const varIndexName = world.schema.annotations.var.index;
|
||||||
const genes = _.map(action.data, d =>
|
const genes = _.map(action.data, (d) =>
|
||||||
world.varAnnotations.at(d[0], varIndexName)
|
world.varAnnotations.at(d[0], varIndexName)
|
||||||
);
|
);
|
||||||
const crossfilter = _.reduce(
|
const crossfilter = _.reduce(
|
||||||
@@ -216,7 +216,7 @@ const CrossfilterReducerBase = (
|
|||||||
minX,
|
minX,
|
||||||
minY,
|
minY,
|
||||||
maxX,
|
maxX,
|
||||||
maxY
|
maxY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ const CrossfilterReducerBase = (
|
|||||||
const { polygon } = action;
|
const { polygon } = action;
|
||||||
return state.select(XYDimName, {
|
return state.select(XYDimName, {
|
||||||
mode: "within-polygon",
|
mode: "within-polygon",
|
||||||
polygon
|
polygon,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +249,12 @@ const CrossfilterReducerBase = (
|
|||||||
return state.select(name, { mode: "all" });
|
return state.select(name, { mode: "all" });
|
||||||
}
|
}
|
||||||
const [lo, hi] = action.range;
|
const [lo, hi] = action.range;
|
||||||
const newState = state.select(name, { mode: "range", lo, hi });
|
const newState = state.select(name, {
|
||||||
|
mode: "range",
|
||||||
|
lo,
|
||||||
|
hi,
|
||||||
|
inclusive: true, // [lo, hi] incluisve selection
|
||||||
|
});
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,19 +266,19 @@ const CrossfilterReducerBase = (
|
|||||||
const values = categoryValues.filter((v, i) => categoryValueSelected[i]);
|
const values = categoryValues.filter((v, i) => categoryValueSelected[i]);
|
||||||
return state.select(obsAnnoDimensionName(action.metadataField), {
|
return state.select(obsAnnoDimensionName(action.metadataField), {
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values
|
values,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
case "categorical metadata filter none of these": {
|
case "categorical metadata filter none of these": {
|
||||||
return state.select(obsAnnoDimensionName(action.metadataField), {
|
return state.select(obsAnnoDimensionName(action.metadataField), {
|
||||||
mode: "none"
|
mode: "none",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
case "categorical metadata filter all of these": {
|
case "categorical metadata filter all of these": {
|
||||||
return state.select(obsAnnoDimensionName(action.metadataField), {
|
return state.select(obsAnnoDimensionName(action.metadataField), {
|
||||||
mode: "all"
|
mode: "all",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
lowerBound,
|
lowerBound,
|
||||||
binarySearch,
|
binarySearch,
|
||||||
lowerBoundIndirect,
|
lowerBoundIndirect,
|
||||||
upperBoundIndirect
|
upperBoundIndirect,
|
||||||
} from "./sort";
|
} from "./sort";
|
||||||
import { makeSortIndex } from "./util";
|
import { makeSortIndex } from "./util";
|
||||||
|
|
||||||
@@ -101,12 +101,12 @@ export default class ImmutableTypedCrossfilter {
|
|||||||
id,
|
id,
|
||||||
dim,
|
dim,
|
||||||
name,
|
name,
|
||||||
selection: dim.select({ mode: "all" })
|
selection: dim.select({ mode: "all" }),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return new ImmutableTypedCrossfilter(data, dimensions, {
|
return new ImmutableTypedCrossfilter(data, dimensions, {
|
||||||
bitArray
|
bitArray,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ export default class ImmutableTypedCrossfilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new ImmutableTypedCrossfilter(data, dimensions, {
|
return new ImmutableTypedCrossfilter(data, dimensions, {
|
||||||
bitArray
|
bitArray,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,8 +138,8 @@ export default class ImmutableTypedCrossfilter {
|
|||||||
[newName]: {
|
[newName]: {
|
||||||
...dim,
|
...dim,
|
||||||
name: newName,
|
name: newName,
|
||||||
dim: dim.dim.rename(newName)
|
dim: dim.dim.rename(newName),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
return new ImmutableTypedCrossfilter(data, newDimensions, selectionCache);
|
return new ImmutableTypedCrossfilter(data, newDimensions, selectionCache);
|
||||||
}
|
}
|
||||||
@@ -205,19 +205,19 @@ export default class ImmutableTypedCrossfilter {
|
|||||||
If sort index exists in the dimension, assume sort ordered ranges.
|
If sort index exists in the dimension, assume sort ordered ranges.
|
||||||
*/
|
*/
|
||||||
if (oldSeln.index) {
|
if (oldSeln.index) {
|
||||||
dels.forEach(interval =>
|
dels.forEach((interval) =>
|
||||||
bitArray.deselectIndirectFromRange(id, oldSeln.index, interval)
|
bitArray.deselectIndirectFromRange(id, oldSeln.index, interval)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
dels.forEach(interval => bitArray.deselectFromRange(id, interval));
|
dels.forEach((interval) => bitArray.deselectFromRange(id, interval));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newSeln.index) {
|
if (newSeln.index) {
|
||||||
adds.forEach(interval =>
|
adds.forEach((interval) =>
|
||||||
bitArray.selectIndirectFromRange(id, newSeln.index, interval)
|
bitArray.selectIndirectFromRange(id, newSeln.index, interval)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
adds.forEach(interval => bitArray.selectFromRange(id, interval));
|
adds.forEach((interval) => bitArray.selectFromRange(id, interval));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { bitArray };
|
return { bitArray };
|
||||||
@@ -229,12 +229,12 @@ export default class ImmutableTypedCrossfilter {
|
|||||||
if (!this.selectionCache.bitArray) {
|
if (!this.selectionCache.bitArray) {
|
||||||
// console.log("...rebuilding crossfilter cache...");
|
// console.log("...rebuilding crossfilter cache...");
|
||||||
const bitArray = new BitArray(this.data.length);
|
const bitArray = new BitArray(this.data.length);
|
||||||
Object.keys(this.dimensions).forEach(name => {
|
Object.keys(this.dimensions).forEach((name) => {
|
||||||
const { selection } = this.dimensions[name];
|
const { selection } = this.dimensions[name];
|
||||||
const id = bitArray.allocDimension();
|
const id = bitArray.allocDimension();
|
||||||
this.dimensions[name].id = id;
|
this.dimensions[name].id = id;
|
||||||
const { ranges, index } = selection;
|
const { ranges, index } = selection;
|
||||||
ranges.forEach(range => {
|
ranges.forEach((range) => {
|
||||||
if (index) {
|
if (index) {
|
||||||
bitArray.selectIndirectFromRange(id, index, range);
|
bitArray.selectIndirectFromRange(id, index, range);
|
||||||
} else {
|
} else {
|
||||||
@@ -395,7 +395,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
|
|||||||
// only by enumerated dimensions
|
// only by enumerated dimensions
|
||||||
array = this._createValueArray(
|
array = this._createValueArray(
|
||||||
data,
|
data,
|
||||||
i => value[i],
|
(i) => value[i],
|
||||||
new ValueArrayType(data.length)
|
new ValueArrayType(data.length)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -448,7 +448,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
|
|||||||
for (let v = 0, len = values.length; v < len; v += 1) {
|
for (let v = 0, len = values.length; v < len; v += 1) {
|
||||||
const r = [
|
const r = [
|
||||||
lowerBoundIndirect(value, index, values[v], 0, value.length),
|
lowerBoundIndirect(value, index, values[v], 0, value.length),
|
||||||
upperBoundIndirect(value, index, values[v], 0, value.length)
|
upperBoundIndirect(value, index, values[v], 0, value.length),
|
||||||
];
|
];
|
||||||
if (r[0] <= r[1]) {
|
if (r[0] <= r[1]) {
|
||||||
ranges.push(r);
|
ranges.push(r);
|
||||||
@@ -459,12 +459,16 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
|
|||||||
|
|
||||||
selectRange(spec) {
|
selectRange(spec) {
|
||||||
const { value, index } = this;
|
const { value, index } = this;
|
||||||
/* [lo, hi) */
|
/*
|
||||||
const { lo, hi } = spec;
|
if !inclusive: [lo, hi) else [lo, hi]
|
||||||
|
*/
|
||||||
|
const { lo, hi, inclusive } = spec;
|
||||||
const ranges = [];
|
const ranges = [];
|
||||||
const r = [
|
const r = [
|
||||||
lowerBoundIndirect(value, index, lo, 0, value.length),
|
lowerBoundIndirect(value, index, lo, 0, value.length),
|
||||||
lowerBoundIndirect(value, index, hi, 0, value.length)
|
!!inclusive
|
||||||
|
? upperBoundIndirect(value, index, hi, 0, value.length)
|
||||||
|
: lowerBoundIndirect(value, index, hi, 0, value.length),
|
||||||
];
|
];
|
||||||
if (r[0] < r[1]) ranges.push(r);
|
if (r[0] < r[1]) ranges.push(r);
|
||||||
return { ranges, index };
|
return { ranges, index };
|
||||||
@@ -504,7 +508,9 @@ class ImmutableEnumDimension extends ImmutableScalarDimension {
|
|||||||
const { values } = spec;
|
const { values } = spec;
|
||||||
return super.selectExact({
|
return super.selectExact({
|
||||||
mode: spec.mode,
|
mode: spec.mode,
|
||||||
values: values.map(v => binarySearch(enumIndex, v, 0, enumIndex.length))
|
values: values.map((v) =>
|
||||||
|
binarySearch(enumIndex, v, 0, enumIndex.length)
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,13 +599,13 @@ class ImmutableSpatialDimension extends _ImmutableBaseDimension {
|
|||||||
if (maxY - minY > maxX - minX) {
|
if (maxY - minY > maxX - minX) {
|
||||||
slice = [
|
slice = [
|
||||||
lowerBoundIndirect(X, Xindex, minX, 0, length),
|
lowerBoundIndirect(X, Xindex, minX, 0, length),
|
||||||
lowerBoundIndirect(X, Xindex, maxX, 0, length)
|
lowerBoundIndirect(X, Xindex, maxX, 0, length),
|
||||||
];
|
];
|
||||||
index = Xindex;
|
index = Xindex;
|
||||||
} else {
|
} else {
|
||||||
slice = [
|
slice = [
|
||||||
lowerBoundIndirect(Y, Yindex, minY, 0, length),
|
lowerBoundIndirect(Y, Yindex, minY, 0, length),
|
||||||
lowerBoundIndirect(Y, Yindex, maxY, 0, length)
|
lowerBoundIndirect(Y, Yindex, maxY, 0, length),
|
||||||
];
|
];
|
||||||
index = Yindex;
|
index = Yindex;
|
||||||
}
|
}
|
||||||
@@ -632,7 +638,7 @@ class ImmutableSpatialDimension extends _ImmutableBaseDimension {
|
|||||||
export const DimTypes = {
|
export const DimTypes = {
|
||||||
scalar: ImmutableScalarDimension,
|
scalar: ImmutableScalarDimension,
|
||||||
enum: ImmutableEnumDimension,
|
enum: ImmutableEnumDimension,
|
||||||
spatial: ImmutableSpatialDimension
|
spatial: ImmutableSpatialDimension,
|
||||||
};
|
};
|
||||||
|
|
||||||
function isArrayOrTypedArray(x) {
|
function isArrayOrTypedArray(x) {
|
||||||
@@ -682,7 +688,8 @@ function withinPolygon(polygon, x, y) {
|
|||||||
x1 = p[0];
|
x1 = p[0];
|
||||||
y1 = p[1];
|
y1 = p[1];
|
||||||
|
|
||||||
if (((y1 > y) !== (y0 > y)) && (x < (x0 - x1) * (y - y1) / (y0 - y1) + x1)) inside = !inside;
|
if (y1 > y !== y0 > y && x < ((x0 - x1) * (y - y1)) / (y0 - y1) + x1)
|
||||||
|
inside = !inside;
|
||||||
x0 = x1;
|
x0 = x1;
|
||||||
y0 = y1;
|
y0 = y1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user