mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-27 05:28:11 +08:00
mini
This commit is contained in:
@@ -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]}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user