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:
Matt Weiden
2020-03-14 15:24:52 -07:00
committed by GitHub
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 * as d3 from "d3";
import memoize from "memoize-one";
import { interpolateCool } from "d3-scale-chromatic";
import * as globals from "../../globals";
import actions from "../../actions";
import { linspace } from "../../util/range";
import { histogramContinuous } from "../../util/dataframe/histogram";
import { makeContinuousDimensionName } from "../../util/nameCreators";
@connect((state, ownProps) => {
@@ -51,7 +52,6 @@ class HistogramBrush extends React.PureComponent {
recalculate expensive stuff, notably bins, summaries, etc.
*/
const histogramCache = {};
const values = col.asArray();
const summary = col.summarize();
const { min: domainMin, max: domainMax } = summary;
const numBins = 40;
@@ -61,17 +61,14 @@ class HistogramBrush extends React.PureComponent {
.domain([domainMin, domainMax])
.range([this.marginLeft, this.marginLeft + this.width]);
const [xStart, xStop] = histogramCache.x.domain();
const histThresholds = linspace(xStart, xStop, numBins + 1);
histogramCache.bins = histogramContinuous(col, numBins, [domainMin, domainMax]);
histogramCache.binWidth = (domainMax - domainMin) / numBins;
histogramCache.bins = d3
.histogram()
.domain(histogramCache.x.domain())
.thresholds(histThresholds)(values);
histogramCache.binStart = i => domainMin + i * histogramCache.binWidth;
histogramCache.binEnd = i => domainMin + (i + 1) * histogramCache.binWidth;
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
.scaleLinear()
.domain([0, yMax])
@@ -93,67 +90,54 @@ class HistogramBrush extends React.PureComponent {
}
componentDidMount() {
const { field } = this.props;
const { x, y, bins, svgRef } = this._histogram;
const { field, colorAccessor } = this.props;
this.renderAxesBrushBins(x, y, bins, svgRef, field);
this.renderHistogram(this._histogram, field, colorAccessor);
}
componentDidUpdate(prevProps) {
const { field, world } = this.props;
const { x, y, bins, svgRef } = this._histogram;
let { brushXselection, brushX } = this.state;
let forceBrushUpdate = false;
const { field, world, continuousSelectionRange: range, colorAccessor } = this.props;
const { x } = this._histogram;
let { brushX, brushXselection } = this.state;
/*
Update our axis if the underlying dataframe column has changed
*/
const dfColumn = HistogramBrush.getColumn(world, field);
const oldDfColumn = HistogramBrush.getColumn(
prevProps.world,
prevProps.field
);
if (dfColumn !== oldDfColumn) {
({ brushXselection, brushX } = this.renderAxesBrushBins(
x,
y,
bins,
svgRef,
field
));
forceBrushUpdate = true;
const oldDfColumn = HistogramBrush.getColumn(prevProps.world, prevProps.field);
const rangeChanged = range !== prevProps.continuousSelectionRange;
const dfChanged = dfColumn !== oldDfColumn;
const colorSelectionChanged = prevProps.colorAccessor !== colorAccessor;
if (dfChanged || colorSelectionChanged) {
({brushX, brushXselection} = this.renderHistogram(this._histogram, field, colorAccessor));
}
/*
if the selection has changed, ensure that the brush correctly reflects
the underlying selection.
*/
const { continuousSelectionRange: range } = this.props;
if (forceBrushUpdate || range !== prevProps.continuousSelectionRange) {
if (brushXselection) {
const selection = d3.brushSelection(brushXselection.node());
if (!range && selection) {
/* no active selection - clear brush */
brushXselection.call(brushX.move, null);
} else if (range && !selection) {
/* there is an active selection, but no brush - set the brush */
const x0 = x(range[0]);
const x1 = x(range[1]);
if ((dfChanged || rangeChanged || colorSelectionChanged) && brushXselection) {
const selection = d3.brushSelection(brushXselection.node());
if (!range && selection) {
/* no active selection - clear brush */
brushXselection.call(brushX.move, null);
} else if (range && !selection) {
/* there is an active selection, but no brush - set the brush */
const x0 = x(range[0]);
const x1 = x(range[1]);
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]);
} 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) {
const { field, world } = this.props;
const col = HistogramBrush.getColumn(world, field);
const histogramCache = this.calcHistogramCache(col);
const { x, y, bins } = histogramCache;
this._histogram = { x, y, bins, svgRef };
this._histogram = {
...this.calcHistogramCache(col),
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);
/* Remove everything */
@@ -333,18 +319,30 @@ class HistogramBrush extends React.PureComponent {
.attr("class", "histogram-container")
.attr("transform", `translate(${this.marginLeft},${this.marginTop})`);
/* BINS */
container
.insert("g", "*")
.attr("fill", "#bbb")
.selectAll("rect")
.data(bins)
.enter()
.append("rect")
.attr("x", d => x(d.x0) + 1)
.attr("y", d => y(d.length))
.attr("width", d => Math.abs(x(d.x1) - x(d.x0) - 1))
.attr("height", d => y(0) - y(d.length));
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", colorAccessor === field ? (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
@@ -508,9 +506,7 @@ class HistogramBrush extends React.PureComponent {
id={`histogram_${fieldForId}_svg`}
data-testclass="histogram-plot"
data-testid={`histogram-${field}-plot`}
ref={svgRef => {
this.drawHistogram(svgRef);
}}
ref={svgRef => this.drawHistogram(svgRef)}
/>
<div
style={{