mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 18:48:11 +08:00
* revert all commits to before Typescript migration * update compat workflow to match latest deps (#2335) * update compat workflow to match latest deps * attempt to debug * attempt to debug * remove debugging code * typo * update deps to match desktop (#2340) * fix: don't run lint with `--fix` on push tests (#2273) * fix: don't run lint with `--fix` on push tests * npx Co-authored-by: maniarathi <mani.arathi@gmail.com> Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com> * rename X_approx_distribution to X_approximate_distribution (#2337) * Correctly handle non-finite numbers in heuristic determination of X distribution (#2342) * handle non-finites explicitly * improve and test edge case handling for distribution estimation * revert debugging changes * code readability * clean up type inferencing (#2332) * unit tests for 64 bit conversion * clean up type handling * type inference tests * more type inference fixes * use schema to determine user intent for data typing * stop using deprecated API * fbs type encoding test * add missing test * add more tests * correctly infer X type for CXG adaptor * lint * fix typo * ts migration * cleanup from PR review * lint * PR review changes * remove unused packages from client (#2359) * remove unused packages from client * add missing peer dep * fix: disable FE auth testing on compatibility tests (#2377) * update: release process (#2277) Co-authored-by: maniarathi <mani.arathi@gmail.com> * fix: remove spaces in param setup (#2380) * delete deploy workflow (#2396) * undo reformatting which now does not pass lint * fix snapshots which changed due to npm dep changes * add missing quoting to snapshot * another snapshot typo fix * TS Revert (2) - replay PR #2347 and #2354 (#2403) * replay edits from PR 2347 * TS Revert (3) - replay edits in PR #2327 (#2404) * replay edits in PR 2327 * TS Revert (4) - replay PR #2355 (#2405) * replay edits in PR 2355 * add additional babel config * reformat with new prettier config Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com> Co-authored-by: maniarathi <mani.arathi@gmail.com> Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
192 lines
5.4 KiB
JavaScript
192 lines
5.4 KiB
JavaScript
import React, { useEffect, useRef, useState } from "react";
|
|
import { interpolateCool } from "d3-scale-chromatic";
|
|
import * as d3 from "d3";
|
|
|
|
import maybeScientific from "../../util/maybeScientific";
|
|
import clamp from "../../util/clamp";
|
|
|
|
const Histogram = ({
|
|
field,
|
|
fieldForId,
|
|
display,
|
|
histogram,
|
|
width,
|
|
height,
|
|
onBrush,
|
|
onBrushEnd,
|
|
margin,
|
|
isColorBy,
|
|
selectionRange,
|
|
mini,
|
|
}) => {
|
|
const svgRef = useRef(null);
|
|
const [brush, setBrush] = useState(null);
|
|
|
|
useEffect(() => {
|
|
/*
|
|
Create the d3 histogram
|
|
*/
|
|
// This is just a constant that's flipped by parent's `mini` boolean
|
|
const {
|
|
LEFT: marginLeft,
|
|
RIGHT: marginRight,
|
|
BOTTOM: marginBottom,
|
|
TOP: marginTop,
|
|
} = margin;
|
|
const { x, y, bins, binStart, binEnd, binWidth } = histogram;
|
|
const svg = d3.select(svgRef.current);
|
|
const binPadding = mini ? 0 : -1;
|
|
const defaultBarColor = mini ? "black" : "#bbb";
|
|
|
|
/* 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)) - binPadding)
|
|
.attr("height", (d) => y(0) - y(d))
|
|
.style(
|
|
"fill",
|
|
isColorBy
|
|
? (d, i) => colorScale(histogramScale(binStart(i)))
|
|
: defaultBarColor
|
|
);
|
|
}
|
|
|
|
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));
|
|
|
|
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;
|