This commit is contained in:
Colin Megill
2020-10-06 17:32:05 -07:00
parent 86d369e63e
commit 6f23f1e7e5
5 changed files with 160 additions and 277 deletions
@@ -10,6 +10,7 @@ const HistogramFooter = React.memo(
rangeColorMax,
logFoldChange,
pvalAdj,
isObs,
}) => {
/*
Footer of each histogram. Will render range, title, and optionally
@@ -43,7 +44,7 @@ const HistogramFooter = React.memo(
data-testclass="brushable-histogram-field-name"
style={{ fontStyle: "italic" }}
>
{displayName}
{isObs ? displayName : null}
</span>
<div style={{ display: hideRanges ? "block" : "none" }}>
: {rangeMin}
@@ -75,62 +75,64 @@ const Histogram = ({
);
}
// BRUSH
// Note the brushable area is bounded by the data on three sides, but goes down to cover the x-axis
const brushX = d3
.brushX()
.extent([
[x.range()[0], y.range()[1]],
[x.range()[1], marginTop + height + marginBottom],
])
/*
if (!mini) {
// BRUSH
// Note the brushable area is bounded by the data on three sides, but goes down to cover the x-axis
const brushX = d3
.brushX()
.extent([
[x.range()[0], y.range()[1]],
[x.range()[1], marginTop + height + marginBottom],
])
/*
emit start so that the Undoable history can save an undo point
upon drag start, and ignore the subsequent intermediate drag events.
*/
.on("start", onBrush(field, x.invert, "start"))
.on("brush", onBrush(field, x.invert, "brush"))
.on("end", onBrushEnd(field, x.invert));
.on("start", onBrush(field, x.invert, "start"))
.on("brush", onBrush(field, x.invert, "brush"))
.on("end", onBrushEnd(field, x.invert));
const brushXselection = container
.insert("g")
.attr("class", "brush")
.attr("data-testid", `${svgRef.current.dataset.testid}-brushable-area`)
.call(brushX);
const brushXselection = container
.insert("g")
.attr("class", "brush")
.attr("data-testid", `${svgRef.current.dataset.testid}-brushable-area`)
.call(brushX);
/* X AXIS */
container
.insert("g")
.attr("class", "axis axis--x")
.attr("transform", `translate(0,${marginTop + height})`)
.call(
d3
.axisBottom(x)
.ticks(4)
.tickFormat(d3.format(maybeScientific(x)))
);
/* X AXIS */
container
.insert("g")
.attr("class", "axis axis--x")
.attr("transform", `translate(0,${marginTop + height})`)
.call(
d3
.axisBottom(x)
.ticks(4)
.tickFormat(d3.format(maybeScientific(x)))
);
/* Y AXIS */
container
.insert("g")
.attr("class", "axis axis--y")
.attr("transform", `translate(${marginLeft + width},0)`)
.call(
d3
.axisRight(y)
.ticks(3)
.tickFormat(
d3.format(
y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
/* Y AXIS */
container
.insert("g")
.attr("class", "axis axis--y")
.attr("transform", `translate(${marginLeft + width},0)`)
.call(
d3
.axisRight(y)
.ticks(3)
.tickFormat(
d3.format(
y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
)
)
)
);
);
/* axis style */
svg.selectAll(".axis text").style("fill", "rgb(80,80,80)");
svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
/* axis style */
svg.selectAll(".axis text").style("fill", "rgb(80,80,80)");
svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
setBrush({ brushX, brushXselection });
setBrush({ brushX, brushXselection });
}
}, [histogram, isColorBy]);
useEffect(() => {
@@ -1,178 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import { interpolateCool } from "d3-scale-chromatic";
import * as d3 from "d3";
import maybeScientific from "./maybeScientific";
import clamp from "./clamp";
const Histogram = ({
field,
fieldForId,
display,
histogram,
width,
height,
onBrush,
onBrushEnd,
margin,
isColorBy,
selectionRange,
}) => {
const svgRef = useRef(null);
const [brush, setBrush] = useState(null);
useEffect(() => {
/*
Create the d3 histogram
*/
const { marginLeft, marginRight, marginBottom, marginTop } = margin;
const { x, y, bins, binStart, binEnd, binWidth } = histogram;
const svg = d3.select(svgRef.current);
/* Remove everything */
svg.selectAll("*").remove();
/* Set margins within the SVG */
const container = svg
.attr("width", width + marginLeft + marginRight)
.attr("height", height + marginTop + marginBottom)
.append("g")
.attr("class", "histogram-container")
.attr("transform", `translate(${marginLeft},${marginTop})`);
const colorScale = d3
.scaleSequential(interpolateCool)
.domain([0, bins.length]);
const histogramScale = d3
.scaleLinear()
.domain(x.domain())
.range([
colorScale.domain()[1],
colorScale.domain()[0],
]); /* we flip this to make colors dark if high in the color scale */
if (binWidth > 0) {
/* BINS */
container
.insert("g", "*")
.selectAll("rect")
.data(bins)
.enter()
.append("rect")
.attr("x", (d, i) => x(binStart(i)) + 1)
.attr("y", (d) => y(d))
.attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - 1)
.attr("height", (d) => y(0) - y(d))
.style(
"fill",
isColorBy ? (d, i) => colorScale(histogramScale(binStart(i))) : "#bbb"
);
}
// BRUSH
// Note the brushable area is bounded by the data on three sides, but goes down to cover the x-axis
const brushX = d3
.brushX()
.extent([
[x.range()[0], y.range()[1]],
[x.range()[1], marginTop + height + marginBottom],
])
/*
emit start so that the Undoable history can save an undo point
upon drag start, and ignore the subsequent intermediate drag events.
*/
.on("start", onBrush(field, x.invert, "start"))
.on("brush", onBrush(field, x.invert, "brush"))
.on("end", onBrushEnd(field, x.invert));
const brushXselection = container
.insert("g")
.attr("class", "brush")
.attr("data-testid", `${svgRef.current.dataset.testid}-brushable-area`)
.call(brushX);
/* X AXIS */
container
.insert("g")
.attr("class", "axis axis--x")
.attr("transform", `translate(0,${marginTop + height})`)
.call(
d3
.axisBottom(x)
.ticks(4)
.tickFormat(d3.format(maybeScientific(x)))
);
/* Y AXIS */
container
.insert("g")
.attr("class", "axis axis--y")
.attr("transform", `translate(${marginLeft + width},0)`)
.call(
d3
.axisRight(y)
.ticks(3)
.tickFormat(
d3.format(
y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
)
)
);
/* axis style */
svg.selectAll(".axis text").style("fill", "rgb(80,80,80)");
svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
setBrush({ brushX, brushXselection });
}, [histogram, isColorBy]);
useEffect(() => {
/*
paint/update selection brush
*/
if (!brush) return;
const { brushX, brushXselection } = brush;
const selection = d3.brushSelection(brushXselection.node());
if (!selectionRange && selection) {
/* no active selection - clear brush */
brushXselection.call(brushX.move, null);
} else if (selectionRange) {
const { x, domain } = histogram;
const [min, max] = domain;
const x0 = x(clamp(selectionRange[0], [min, max]));
const x1 = x(clamp(selectionRange[1], [min, max]));
if (!selection) {
/* there is an active selection, but no brush - set the brush */
brushXselection.call(brushX.move, [x0, x1]);
} else {
/* there is an active selection and a brush - make sure they match */
const moveDeltaThreshold = 1;
const dX0 = Math.abs(x0 - selection[0]);
const dX1 = Math.abs(x1 - selection[1]);
/*
only update the brush if it is grossly incorrect,
as defined by the moveDeltaThreshold
*/
if (dX0 > moveDeltaThreshold || dX1 > moveDeltaThreshold) {
brushXselection.call(brushX.move, [x0, x1]);
}
}
}
}, [brush, selectionRange]);
return (
<svg
style={{ display }}
width={width}
height={height}
id={`histogram_${fieldForId}_svg`}
data-testclass="histogram-plot"
data-testid={`histogram-${field}-plot`}
ref={svgRef}
/>
);
};
export default Histogram;
@@ -54,11 +54,6 @@ class HistogramBrush extends React.PureComponent {
const marginBottom = 25; // space for X axis & labels
const marginTop = 3;
const marginLeftMini = 10; // Space for 0 tick label on X axis
const marginRightMini = 54; // space for Y axis & labels
const marginBottomMini = 25; // space for X axis & labels
const marginTopMini = 3;
this.state = {
margin: {
marginLeft,
@@ -69,13 +64,13 @@ class HistogramBrush extends React.PureComponent {
width: 340 - marginLeft - marginRight,
height: 135 - marginTop - marginBottom,
marginMini: {
marginLeftMini,
marginRightMini,
marginBottomMini,
marginTopMini,
marginLeft: 0, // Space for 0 tick label on X axis
marginRight: 0, // space for Y axis & labels
marginBottom: 0, // space for X axis & labels
marginTop: 0,
},
widthMini: 340 - marginLeft - marginRight,
heightMini: 135 - marginTop - marginBottom,
widthMini: 120,
heightMini: 15,
};
}
@@ -271,16 +266,20 @@ class HistogramBrush extends React.PureComponent {
// eslint-disable-next-line class-methods-use-this -- instance method allows for memoization per annotation
calcHistogramCache(col, margin, width, height) {
/* make this more structured and doing a forEach */
/*
recalculate expensive stuff, notably bins, summaries, etc.
*/
const histogramCache = {};
const summary = col.summarize();
const histogramCache = {}; /* maybe change this so that it computes ... */
const summary = col.summarize(); /* this is memoized, so it's free the second time you call it */
const { min: domainMin, max: domainMax } = summary;
const numBins = 40;
const { marginTop, marginLeft } = margin;
const { marginTop, marginLeft } = margin; /* changes with mini */
histogramCache.domain = [domainMin, domainMax];
histogramCache.domain = [
domainMin,
domainMax,
]; /* doesn't change with mini */
histogramCache.x = d3
.scaleLinear()
@@ -290,7 +289,7 @@ class HistogramBrush extends React.PureComponent {
histogramCache.bins = histogramContinuous(col, numBins, [
domainMin,
domainMax,
]);
]); /* memoized */
histogramCache.binWidth = (domainMax - domainMin) / numBins;
histogramCache.binStart = (i) => domainMin + i * histogramCache.binWidth;
@@ -377,11 +376,11 @@ class HistogramBrush extends React.PureComponent {
: "histogram-continuous-metadata"
}
style={{
padding: globals.leftSidebarSectionPadding,
padding: mini ? 0 : globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white",
}}
>
{!mini ? (
{!mini && isObs ? (
<HistogramHeader
fieldId={field}
isColorBy={isColorAccessor}
@@ -416,6 +415,7 @@ class HistogramBrush extends React.PureComponent {
/>
{!mini ? (
<HistogramFooter
isObs={isObs}
displayName={field}
hideRanges={asyncProps.isSingleValue}
rangeMin={asyncProps.unclippedRange[0]}
+91 -33
View File
@@ -11,10 +11,18 @@ import HistogramBrush from "../brushableHistogram";
// import TestMiniHisto from "./test_miniHisto";
import * as globals from "../../globals";
import actions from "../../actions";
// import GeneMenus from "./menus/geneMenus";
@connect(() => {
return {};
@connect((state, ownProps) => {
const { gene } = ownProps;
return {
isColorAccessor: state.colors.colorAccessor === gene,
isScatterplotXXaccessor: state.controls.scatterplotXXaccessor === gene,
isScatterplotYYaccessor: state.controls.scatterplotYYaccessor === gene,
};
})
class Gene extends React.Component {
constructor(props) {
@@ -26,10 +34,7 @@ class Gene extends React.Component {
onColorChangeClick = () => {
const { dispatch, gene } = this.props;
dispatch({
type: "color by expression",
colorAccessor: gene,
});
dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(gene));
};
handleGeneExpandClick = () => {
@@ -37,26 +42,42 @@ class Gene extends React.Component {
this.setState({ geneIsExpanded: !geneIsExpanded });
};
handleSetGeneAsScatterplotX = () => {
const { dispatch, gene } = this.props;
dispatch({
type: "set scatterplot x",
data: gene,
});
};
handleSetGeneAsScatterplotY = () => {
const { dispatch, gene } = this.props;
dispatch({
type: "set scatterplot y",
data: gene,
});
};
render() {
const { gene } = this.props;
const {
gene,
isColorAccessor,
isScatterplotXXaccessor,
isScatterplotYYaccessor,
} = this.props;
const { geneIsExpanded } = this.state;
return (
<div>
<div
style={{
marginLeft: 15,
marginRight: 0,
marginTop: 2,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Icon
icon="drag-handle-horizontal"
iconSize={12}
style={{
marginRight: 10,
cursor: "grab",
position: "relative",
top: -2,
}}
/>
<span
role="menuitem"
tabIndex="0"
@@ -65,8 +86,20 @@ class Gene extends React.Component {
onKeyPress={/* todo_genesets */ () => {}}
style={{
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
}}
>
<Icon
icon="drag-handle-horizontal"
iconSize={12}
style={{
marginRight: 7,
cursor: "grab",
position: "relative",
top: 3,
}}
/>
<Truncate>
<span
style={{
@@ -79,22 +112,58 @@ class Gene extends React.Component {
{gene}
</span>
</Truncate>
{!geneIsExpanded ? (
<HistogramBrush isUserDefined field={gene} mini />
) : null}
</span>
<span>
<AnchorButton
minimal
small
data-testid={`plot-x-${gene}`}
onClick={this.handleSetGeneAsScatterplotX}
active={isScatterplotXXaccessor}
intent={isScatterplotXXaccessor ? "primary" : "none"}
style={{ fontWeight: 700, marginRight: 2 }}
>
x
</AnchorButton>
<AnchorButton
minimal
small
data-testid={`plot-y-${gene}`}
onClick={this.handleSetGeneAsScatterplotY}
active={isScatterplotYYaccessor}
intent={isScatterplotYYaccessor ? "primary" : "none"}
style={{ fontWeight: 700, marginRight: 2 }}
>
y
</AnchorButton>
<AnchorButton
minimal
small
data-testclass="maximize"
data-testid={`maximize-${gene}`}
onClick={this.handleGeneExpandClick}
active={false /* todo gene sets */}
active={geneIsExpanded}
intent="none"
icon={<Icon icon="maximize" iconSize={10} />}
style={{ marginRight: 2 }}
/>
<AnchorButton
minimal
small
data-testclass="colorby"
data-testid={`colorby-${gene}`}
onClick={this.onColorChangeClick}
active={isColorAccessor}
intent={isColorAccessor ? "primary" : "none"}
icon={<Icon icon="tint" iconSize={12} />}
/>
{/* <TestMiniHisto /> */}
</span>
{/* <TestMiniHisto /> */}
</div>
<HistogramBrush isUserDefined field={gene} mini={!geneIsExpanded} />
{geneIsExpanded ? <HistogramBrush isUserDefined field={gene} /> : null}
{/* <GeneMenus genesetsEditable gene={gene} /> */}
</div>
);
@@ -103,17 +172,6 @@ class Gene extends React.Component {
export default Gene;
// <AnchorButton
// minimal
// small
// data-testclass="colorby"
// data-testid={`colorby-${gene}`}
// onClick={/* todo gene sets */ () => {}}
// active={false /* todo gene sets */}
// intent="none"
// icon={<Icon icon="tint" iconSize={12} />}
// />
// {geneIsExpanded ? (
// <FaChevronDown
// data-testclass="gene-expand-is-expanded"