mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-07 04:38:11 +08:00
Add color mapping to the bar chart (#1218)
* Add color mapping to the bar chart. * Change histogram color generation from Viridis to InterpolateCool. * Coloring of Histrogram based up the x axis instead of y axis. Coloring of Histrogram based up the x axis instead of y axis. * Respond to feedback from @colinmegill * Only color histograms that are selected for colorby * Add some small refactors to BrushableHistogram's componentDidUpdate * Fix histogram coloring and binning * Reuse binning functions from util/dataframe/histogram.js; this fixes an issue with there being near-zero width bins * Fix color mapping so that it matches the scale in the legend * Do not attempt to plot bins if the calculated binWidth is zero; this can happen if all values are the same * Refactor the function that draws the histogram a bit * Respond to feedback from @bkmartinjr Co-authored-by: Donald Paul Herman <Donaldpherman@hotmail.com>
This commit is contained in:
co-authored by
Donald Paul Herman
parent
1e8d89f8f2
commit
86cbe64ae8
@@ -10,9 +10,10 @@ import { Button, ButtonGroup, Tooltip } from "@blueprintjs/core";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import memoize from "memoize-one";
|
import memoize from "memoize-one";
|
||||||
|
import { interpolateCool } from "d3-scale-chromatic";
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import actions from "../../actions";
|
import actions from "../../actions";
|
||||||
import { linspace } from "../../util/range";
|
import { histogramContinuous } from "../../util/dataframe/histogram";
|
||||||
import { makeContinuousDimensionName } from "../../util/nameCreators";
|
import { makeContinuousDimensionName } from "../../util/nameCreators";
|
||||||
|
|
||||||
@connect((state, ownProps) => {
|
@connect((state, ownProps) => {
|
||||||
@@ -51,7 +52,6 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
recalculate expensive stuff, notably bins, summaries, etc.
|
recalculate expensive stuff, notably bins, summaries, etc.
|
||||||
*/
|
*/
|
||||||
const histogramCache = {};
|
const histogramCache = {};
|
||||||
const values = col.asArray();
|
|
||||||
const summary = col.summarize();
|
const summary = col.summarize();
|
||||||
const { min: domainMin, max: domainMax } = summary;
|
const { min: domainMin, max: domainMax } = summary;
|
||||||
const numBins = 40;
|
const numBins = 40;
|
||||||
@@ -61,17 +61,14 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
.domain([domainMin, domainMax])
|
.domain([domainMin, domainMax])
|
||||||
.range([this.marginLeft, this.marginLeft + this.width]);
|
.range([this.marginLeft, this.marginLeft + this.width]);
|
||||||
|
|
||||||
const [xStart, xStop] = histogramCache.x.domain();
|
histogramCache.bins = histogramContinuous(col, numBins, [domainMin, domainMax]);
|
||||||
const histThresholds = linspace(xStart, xStop, numBins + 1);
|
histogramCache.binWidth = (domainMax - domainMin) / numBins;
|
||||||
|
|
||||||
histogramCache.bins = d3
|
histogramCache.binStart = i => domainMin + i * histogramCache.binWidth;
|
||||||
.histogram()
|
histogramCache.binEnd = i => domainMin + (i + 1) * histogramCache.binWidth;
|
||||||
.domain(histogramCache.x.domain())
|
|
||||||
.thresholds(histThresholds)(values);
|
const yMax = histogramCache.bins.reduce((l, r) => l > r ? l : r);
|
||||||
|
|
||||||
const yMax = histogramCache.bins
|
|
||||||
.map(b => b.length)
|
|
||||||
.reduce((a, b) => Math.max(a, b));
|
|
||||||
histogramCache.y = d3
|
histogramCache.y = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.domain([0, yMax])
|
.domain([0, yMax])
|
||||||
@@ -93,67 +90,54 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { field } = this.props;
|
const { field, colorAccessor } = this.props;
|
||||||
const { x, y, bins, svgRef } = this._histogram;
|
|
||||||
|
|
||||||
this.renderAxesBrushBins(x, y, bins, svgRef, field);
|
this.renderHistogram(this._histogram, field, colorAccessor);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { field, world } = this.props;
|
const { field, world, continuousSelectionRange: range, colorAccessor } = this.props;
|
||||||
const { x, y, bins, svgRef } = this._histogram;
|
const { x } = this._histogram;
|
||||||
let { brushXselection, brushX } = this.state;
|
let { brushX, brushXselection } = this.state;
|
||||||
let forceBrushUpdate = false;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Update our axis if the underlying dataframe column has changed
|
|
||||||
*/
|
|
||||||
const dfColumn = HistogramBrush.getColumn(world, field);
|
const dfColumn = HistogramBrush.getColumn(world, field);
|
||||||
const oldDfColumn = HistogramBrush.getColumn(
|
const oldDfColumn = HistogramBrush.getColumn(prevProps.world, prevProps.field);
|
||||||
prevProps.world,
|
|
||||||
prevProps.field
|
const rangeChanged = range !== prevProps.continuousSelectionRange;
|
||||||
);
|
const dfChanged = dfColumn !== oldDfColumn;
|
||||||
if (dfColumn !== oldDfColumn) {
|
const colorSelectionChanged = prevProps.colorAccessor !== colorAccessor;
|
||||||
({ brushXselection, brushX } = this.renderAxesBrushBins(
|
|
||||||
x,
|
if (dfChanged || colorSelectionChanged) {
|
||||||
y,
|
({brushX, brushXselection} = this.renderHistogram(this._histogram, field, colorAccessor));
|
||||||
bins,
|
|
||||||
svgRef,
|
|
||||||
field
|
|
||||||
));
|
|
||||||
forceBrushUpdate = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if the selection has changed, ensure that the brush correctly reflects
|
if the selection has changed, ensure that the brush correctly reflects
|
||||||
the underlying selection.
|
the underlying selection.
|
||||||
*/
|
*/
|
||||||
const { continuousSelectionRange: range } = this.props;
|
if ((dfChanged || rangeChanged || colorSelectionChanged) && brushXselection) {
|
||||||
if (forceBrushUpdate || range !== prevProps.continuousSelectionRange) {
|
const selection = d3.brushSelection(brushXselection.node());
|
||||||
if (brushXselection) {
|
if (!range && selection) {
|
||||||
const selection = d3.brushSelection(brushXselection.node());
|
/* no active selection - clear brush */
|
||||||
if (!range && selection) {
|
brushXselection.call(brushX.move, null);
|
||||||
/* no active selection - clear brush */
|
} else if (range && !selection) {
|
||||||
brushXselection.call(brushX.move, null);
|
/* there is an active selection, but no brush - set the brush */
|
||||||
} else if (range && !selection) {
|
const x0 = x(range[0]);
|
||||||
/* there is an active selection, but no brush - set the brush */
|
const x1 = x(range[1]);
|
||||||
const x0 = x(range[0]);
|
brushXselection.call(brushX.move, [x0, x1]);
|
||||||
const x1 = x(range[1]);
|
} else if (range && selection) {
|
||||||
|
/* there is an active selection and a brush - make sure they match */
|
||||||
|
const moveDeltaThreshold = 1;
|
||||||
|
const x0 = x(range[0]);
|
||||||
|
const x1 = x(range[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]);
|
brushXselection.call(brushX.move, [x0, x1]);
|
||||||
} else if (range && selection) {
|
|
||||||
/* there is an active selection and a brush - make sure they match */
|
|
||||||
const moveDeltaThreshold = 1;
|
|
||||||
const x0 = x(range[0]);
|
|
||||||
const x1 = x(range[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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,12 +298,14 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
drawHistogram(svgRef) {
|
drawHistogram(svgRef) {
|
||||||
const { field, world } = this.props;
|
const { field, world } = this.props;
|
||||||
const col = HistogramBrush.getColumn(world, field);
|
const col = HistogramBrush.getColumn(world, field);
|
||||||
const histogramCache = this.calcHistogramCache(col);
|
this._histogram = {
|
||||||
const { x, y, bins } = histogramCache;
|
...this.calcHistogramCache(col),
|
||||||
this._histogram = { x, y, bins, svgRef };
|
svgRef
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAxesBrushBins(x, y, bins, svgRef, field) {
|
renderHistogram(histogram, field, colorAccessor) {
|
||||||
|
const { x, y, bins, svgRef, binStart, binEnd, binWidth } = histogram;
|
||||||
const svg = d3.select(svgRef);
|
const svg = d3.select(svgRef);
|
||||||
|
|
||||||
/* Remove everything */
|
/* Remove everything */
|
||||||
@@ -333,18 +319,30 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
.attr("class", "histogram-container")
|
.attr("class", "histogram-container")
|
||||||
.attr("transform", `translate(${this.marginLeft},${this.marginTop})`);
|
.attr("transform", `translate(${this.marginLeft},${this.marginTop})`);
|
||||||
|
|
||||||
/* BINS */
|
const colorScale = d3.scaleSequential(interpolateCool).domain([0, bins.length]);
|
||||||
container
|
|
||||||
.insert("g", "*")
|
const histogramScale = d3
|
||||||
.attr("fill", "#bbb")
|
.scaleLinear()
|
||||||
.selectAll("rect")
|
.domain(x.domain())
|
||||||
.data(bins)
|
.range([
|
||||||
.enter()
|
colorScale.domain()[1],
|
||||||
.append("rect")
|
colorScale.domain()[0]
|
||||||
.attr("x", d => x(d.x0) + 1)
|
]); /* we flip this to make colors dark if high in the color scale */
|
||||||
.attr("y", d => y(d.length))
|
|
||||||
.attr("width", d => Math.abs(x(d.x1) - x(d.x0) - 1))
|
if (binWidth > 0) {
|
||||||
.attr("height", d => y(0) - y(d.length));
|
/* 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", colorAccessor === field ? (d, i) => colorScale(histogramScale(binStart(i))) : "#bbb");
|
||||||
|
}
|
||||||
|
|
||||||
// BRUSH
|
// BRUSH
|
||||||
// Note the brushable area is bounded by the data on three sides, but goes down to cover the x-axis
|
// Note the brushable area is bounded by the data on three sides, but goes down to cover the x-axis
|
||||||
@@ -508,9 +506,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
id={`histogram_${fieldForId}_svg`}
|
id={`histogram_${fieldForId}_svg`}
|
||||||
data-testclass="histogram-plot"
|
data-testclass="histogram-plot"
|
||||||
data-testid={`histogram-${field}-plot`}
|
data-testid={`histogram-${field}-plot`}
|
||||||
ref={svgRef => {
|
ref={svgRef => this.drawHistogram(svgRef)}
|
||||||
this.drawHistogram(svgRef);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
Reference in New Issue
Block a user