- );
- }
-);
-
-const HistogramHeader = React.memo(
- ({
- fieldId,
- isColorBy,
- onColorByClick,
- onRemoveClick,
- isScatterPlotX,
- isScatterPlotY,
- onScatterPlotXClick,
- onScatterPlotYClick,
- isObs,
- }) => {
- /*
- Render the toolbar for the histogram. Props:
- * fieldId - field identifier, used for various IDs
- * isColorBy - true/false, is this the current color-by
- * onColorByClick - color-by click handler
- * onRemoveClick - optional handler for remove. Button will not render if not defined.
- * isScatterPlotX - optional, true/false if currently the X scatterplot field
- * isScatterPlotY - optional, true/false if currently the Y scatterplot field
- * onScatterPlotXClick - optional, handler for scatterPlot X button.
- * onScatterPlotYClick - optional, handler for scatterPlot X button.
-
- Scatterplot controls will not render if either handler unspecified.
- */
-
- const memoizedColorByCallback = useCallback(
- () => onColorByClick(fieldId, isObs),
- [fieldId, isObs]
- );
-
- return (
-
- );
- }
-);
-
-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 (
-
- );
-};
+import HistogramHeader from "./header";
+import Histogram from "./histogram";
+import HistogramFooter from "./footer";
+import StillLoading from "./loading";
+import ErrorLoading from "./error";
@connect((state, ownProps) => {
const { isObs, isUserDefined, isDiffExp, field } = ownProps;
diff --git a/client/src/components/brushableHistogram/loading.js b/client/src/components/brushableHistogram/loading.js
new file mode 100644
index 00000000..7a195c78
--- /dev/null
+++ b/client/src/components/brushableHistogram/loading.js
@@ -0,0 +1,42 @@
+import React from "react";
+import { Button } from "@blueprintjs/core";
+
+import * as globals from "../../globals";
+
+const StillLoading = ({ zebra, displayName }) => {
+ /*
+ Render a loading indicator for the field.
+ */
+ return (
+
+
+
+
+ {displayName}
+
+
+
+
+
+
+ );
+};
+
+export default StillLoading;
diff --git a/client/src/components/brushableHistogram/maybeScientific.js b/client/src/components/brushableHistogram/maybeScientific.js
new file mode 100644
index 00000000..0a3da3d1
--- /dev/null
+++ b/client/src/components/brushableHistogram/maybeScientific.js
@@ -0,0 +1,17 @@
+import significantDigits from "../../util/significantDigits";
+
+export default function maybeScientific(x) {
+ let format = ",";
+ const _ticks = x.ticks(4);
+
+ if (x.domain().some((n) => Math.abs(n) >= 10000)) {
+ /*
+ heuristic: if the last tick d3 wants to render has one significant
+ digit ie., 2000, render 2e+3, but if it's anything else ie., 42000000 render
+ 4.20e+n
+ */
+ format = significantDigits(_ticks[_ticks.length - 1]) === 1 ? ".0e" : ".2e";
+ }
+
+ return format;
+}
diff --git a/client/src/components/geneExpression/gene.js b/client/src/components/geneExpression/gene.js
index 81823ca1..c5f5e4cc 100644
--- a/client/src/components/geneExpression/gene.js
+++ b/client/src/components/geneExpression/gene.js
@@ -7,8 +7,9 @@ import { connect } from "react-redux";
import { AnchorButton, Icon } from "@blueprintjs/core";
import Truncate from "../util/truncate";
+import HistogramBrush from "../brushableHistogram";
-import TestMiniHisto from "./test_miniHisto";
+// import TestMiniHisto from "./test_miniHisto";
import * as globals from "../../globals";
import GeneMenus from "./menus/geneMenus";
@@ -85,7 +86,7 @@ class Gene extends React.Component {
{gene}
-
+ {/* */}
@@ -112,6 +113,7 @@ class Gene extends React.Component {
icon={}
/>
+
);
}
diff --git a/client/src/components/geneExpression/geneSet.js b/client/src/components/geneExpression/geneSet.js
index 42a6a3fd..be94d669 100644
--- a/client/src/components/geneExpression/geneSet.js
+++ b/client/src/components/geneExpression/geneSet.js
@@ -4,15 +4,15 @@
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
-import { AnchorButton, Icon } from "@blueprintjs/core";
+import { AnchorButton, Icon, Tooltip, Position } from "@blueprintjs/core";
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
import actions from "../../actions";
import Gene from "./gene";
import { memoize } from "../../util/dataframe/util";
import Truncate from "../util/truncate";
-import TestMiniHisto from "./test_miniHisto";
+// import TestMiniHisto from "./test_miniHisto";
import * as globals from "../../globals";
-import GenesetMenus from "./menus/genesetMenus";
+// import GenesetMenus from "./menus/genesetMenus";
@connect((state, ownProps) => {
return {
@@ -140,7 +140,7 @@ class GeneSet extends React.Component {
style={{
maxWidth:
globals.leftSidebarWidth -
- 240 /* todo_genesets this magic number determines how much of a long geneset name we see, and will be tweaked as we build */,
+ 150 /* todo_genesets this magic number determines how much of a long geneset name we see, and will be tweaked as we build */,
}}
data-testid={`${setName}:geneset-label`}
>
@@ -149,16 +149,28 @@ class GeneSet extends React.Component {