diff --git a/client/src/components/brushableHistogram/footer.js b/client/src/components/brushableHistogram/footer.js index d2cdc955..39e800f2 100644 --- a/client/src/components/brushableHistogram/footer.js +++ b/client/src/components/brushableHistogram/footer.js @@ -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}
: {rangeMin} diff --git a/client/src/components/brushableHistogram/histogram.js b/client/src/components/brushableHistogram/histogram.js index 38fa5786..0b777bbd 100644 --- a/client/src/components/brushableHistogram/histogram.js +++ b/client/src/components/brushableHistogram/histogram.js @@ -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(() => { diff --git a/client/src/components/brushableHistogram/histogramMini.js b/client/src/components/brushableHistogram/histogramMini.js deleted file mode 100644 index 1b267215..00000000 --- a/client/src/components/brushableHistogram/histogramMini.js +++ /dev/null @@ -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 ( - - ); -}; - -export default Histogram; diff --git a/client/src/components/brushableHistogram/index.js b/client/src/components/brushableHistogram/index.js index fe06cce9..f59d9e54 100644 --- a/client/src/components/brushableHistogram/index.js +++ b/client/src/components/brushableHistogram/index.js @@ -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 ? ( {!mini ? ( { - 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 (
- {}} style={{ cursor: "pointer", + display: "flex", + justifyContent: "space-between", }} > + + {!geneIsExpanded ? ( + + ) : null} + + + + x + + + y + } + style={{ marginRight: 2 }} + /> + } /> - {/* */} + {/* */}
- - - + {geneIsExpanded ? : null} {/* */}
); @@ -103,17 +172,6 @@ class Gene extends React.Component { export default Gene; -// {}} -// active={false /* todo gene sets */} -// intent="none" -// icon={} -// /> - // {geneIsExpanded ? ( //