performance and correctness fixes to brushable histogram (#1390)

This commit is contained in:
Bruce Martin
2020-04-13 20:03:46 -07:00
committed by GitHub
parent 5c0b8c6296
commit a3df1a9f4a
5 changed files with 152 additions and 119 deletions
@@ -24,10 +24,10 @@ import { makeContinuousDimensionName } from "../../util/nameCreators";
); );
return { return {
world: state.world, world: state.world,
scatterplotXXaccessor: state.controls.scatterplotXXaccessor, isScatterplotXXaccessor: state.controls.scatterplotXXaccessor === field,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, isScatterplotYYaccessor: state.controls.scatterplotYYaccessor === field,
continuousSelectionRange: state.continuousSelection[myName], continuousSelectionRange: state.continuousSelection[myName],
colorAccessor: state.colors.colorAccessor isColorAccessor: state.colors.colorAccessor === field,
}; };
}) })
class HistogramBrush extends React.PureComponent { class HistogramBrush extends React.PureComponent {
@@ -47,7 +47,7 @@ class HistogramBrush extends React.PureComponent {
return varData.col(field); return varData.col(field);
} }
calcHistogramCache = memoize(col => { calcHistogramCache = memoize((col) => {
/* /*
recalculate expensive stuff, notably bins, summaries, etc. recalculate expensive stuff, notably bins, summaries, etc.
*/ */
@@ -61,13 +61,17 @@ 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]);
histogramCache.bins = histogramContinuous(col, numBins, [domainMin, domainMax]); histogramCache.bins = histogramContinuous(col, numBins, [
domainMin,
domainMax,
]);
histogramCache.binWidth = (domainMax - domainMin) / numBins; histogramCache.binWidth = (domainMax - domainMin) / numBins;
histogramCache.binStart = i => domainMin + i * histogramCache.binWidth; histogramCache.binStart = (i) => domainMin + i * histogramCache.binWidth;
histogramCache.binEnd = i => domainMin + (i + 1) * 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.reduce((l, r) => (l > r ? l : r));
histogramCache.y = d3 histogramCache.y = d3
.scaleLinear() .scaleLinear()
@@ -90,32 +94,47 @@ class HistogramBrush extends React.PureComponent {
} }
componentDidMount() { componentDidMount() {
const { field, colorAccessor } = this.props; const { field, isColorAccessor } = this.props;
this.renderHistogram(this._histogram, field, colorAccessor); this.renderHistogram(this._histogram, field, isColorAccessor);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { field, world, continuousSelectionRange: range, colorAccessor } = this.props; const {
field,
world,
continuousSelectionRange: range,
isColorAccessor,
} = this.props;
const { x } = this._histogram; const { x } = this._histogram;
let { brushX, brushXselection } = this.state; let { brushX, brushXselection } = this.state;
const dfColumn = HistogramBrush.getColumn(world, field); const dfColumn = HistogramBrush.getColumn(world, field);
const oldDfColumn = HistogramBrush.getColumn(prevProps.world, prevProps.field); const oldDfColumn = HistogramBrush.getColumn(
prevProps.world,
prevProps.field
);
const rangeChanged = range !== prevProps.continuousSelectionRange; const rangeChanged = range !== prevProps.continuousSelectionRange;
const dfChanged = dfColumn !== oldDfColumn; const dfChanged = dfColumn !== oldDfColumn;
const colorSelectionChanged = prevProps.colorAccessor !== colorAccessor; const colorSelectionChanged = prevProps.isColorAccessor !== isColorAccessor;
if (dfChanged || colorSelectionChanged) { if (dfChanged || colorSelectionChanged) {
({brushX, brushXselection} = this.renderHistogram(this._histogram, field, colorAccessor)); ({ brushX, brushXselection } = this.renderHistogram(
this._histogram,
field,
isColorAccessor
));
} }
/* /*
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.
*/ */
if ((dfChanged || rangeChanged || colorSelectionChanged) && brushXselection) { if (
(dfChanged || rangeChanged || colorSelectionChanged) &&
brushXselection
) {
const selection = d3.brushSelection(brushXselection.node()); const selection = d3.brushSelection(brushXselection.node());
if (!range && selection) { if (!range && selection) {
/* no active selection - clear brush */ /* no active selection - clear brush */
@@ -160,9 +179,9 @@ class HistogramBrush extends React.PureComponent {
continuousNamespace: { continuousNamespace: {
isObs, isObs,
isUserDefined, isUserDefined,
isDiffExp isDiffExp,
}, },
range: [x(d3.event.selection[0]), x(d3.event.selection[1])] range: [x(d3.event.selection[0]), x(d3.event.selection[1])],
}); });
} else { } else {
dispatch({ dispatch({
@@ -171,9 +190,9 @@ class HistogramBrush extends React.PureComponent {
continuousNamespace: { continuousNamespace: {
isObs, isObs,
isUserDefined, isUserDefined,
isDiffExp isDiffExp,
}, },
range: null range: null,
}); });
} }
}; };
@@ -216,9 +235,9 @@ class HistogramBrush extends React.PureComponent {
continuousNamespace: { continuousNamespace: {
isObs, isObs,
isUserDefined, isUserDefined,
isDiffExp isDiffExp,
}, },
range: _range range: _range,
}); });
} else { } else {
dispatch({ dispatch({
@@ -227,8 +246,8 @@ class HistogramBrush extends React.PureComponent {
continuousNamespace: { continuousNamespace: {
isObs, isObs,
isUserDefined, isUserDefined,
isDiffExp isDiffExp,
} },
}); });
} }
}; };
@@ -238,7 +257,7 @@ class HistogramBrush extends React.PureComponent {
const { dispatch, field } = this.props; const { dispatch, field } = this.props;
dispatch({ dispatch({
type: "set scatterplot x", type: "set scatterplot x",
data: field data: field,
}); });
}; };
@@ -246,7 +265,7 @@ class HistogramBrush extends React.PureComponent {
const { dispatch, field } = this.props; const { dispatch, field } = this.props;
dispatch({ dispatch({
type: "set scatterplot y", type: "set scatterplot y",
data: field data: field,
}); });
}; };
@@ -257,7 +276,7 @@ class HistogramBrush extends React.PureComponent {
dispatch({ dispatch({
type: "color by continuous metadata", type: "color by continuous metadata",
colorAccessor: field, colorAccessor: field,
rangeForColorAccessor: ranges rangeForColorAccessor: ranges,
}); });
} else if (world.varData.hasCol(field)) { } else if (world.varData.hasCol(field)) {
dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field)); dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field));
@@ -268,29 +287,29 @@ class HistogramBrush extends React.PureComponent {
const { const {
dispatch, dispatch,
field, field,
colorAccessor, isColorAccessor,
scatterplotXXaccessor, isScatterplotXXaccessor,
scatterplotYYaccessor isScatterplotYYaccessor,
} = this.props; } = this.props;
dispatch({ dispatch({
type: "clear user defined gene", type: "clear user defined gene",
data: field data: field,
}); });
if (field === colorAccessor) { if (isColorAccessor) {
dispatch({ dispatch({
type: "reset colorscale" type: "reset colorscale",
}); });
} }
if (field === scatterplotXXaccessor) { if (isScatterplotXXaccessor) {
dispatch({ dispatch({
type: "set scatterplot x", type: "set scatterplot x",
data: null data: null,
}); });
} }
if (field === scatterplotYYaccessor) { if (isScatterplotYYaccessor) {
dispatch({ dispatch({
type: "set scatterplot y", type: "set scatterplot y",
data: null data: null,
}); });
} }
}; };
@@ -300,11 +319,11 @@ class HistogramBrush extends React.PureComponent {
const col = HistogramBrush.getColumn(world, field); const col = HistogramBrush.getColumn(world, field);
this._histogram = { this._histogram = {
...this.calcHistogramCache(col), ...this.calcHistogramCache(col),
svgRef svgRef,
}; };
} }
renderHistogram(histogram, field, colorAccessor) { renderHistogram(histogram, field, isColorAccessor) {
const { x, y, bins, svgRef, binStart, binEnd, binWidth } = histogram; const { x, y, bins, svgRef, binStart, binEnd, binWidth } = histogram;
const svg = d3.select(svgRef); const svg = d3.select(svgRef);
@@ -319,14 +338,16 @@ 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})`);
const colorScale = d3.scaleSequential(interpolateCool).domain([0, bins.length]); const colorScale = d3
.scaleSequential(interpolateCool)
.domain([0, bins.length]);
const histogramScale = d3 const histogramScale = d3
.scaleLinear() .scaleLinear()
.domain(x.domain()) .domain(x.domain())
.range([ .range([
colorScale.domain()[1], colorScale.domain()[1],
colorScale.domain()[0] colorScale.domain()[0],
]); /* we flip this to make colors dark if high in the color scale */ ]); /* we flip this to make colors dark if high in the color scale */
if (binWidth > 0) { if (binWidth > 0) {
@@ -338,10 +359,15 @@ class HistogramBrush extends React.PureComponent {
.enter() .enter()
.append("rect") .append("rect")
.attr("x", (d, i) => x(binStart(i)) + 1) .attr("x", (d, i) => x(binStart(i)) + 1)
.attr("y", d => y(d)) .attr("y", (d) => y(d))
.attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - 1) .attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - 1)
.attr("height", d => y(0) - y(d)) .attr("height", (d) => y(0) - y(d))
.style("fill", colorAccessor === field ? (d, i) => colorScale(histogramScale(binStart(i))) : "#bbb"); .style(
"fill",
isColorAccessor
? (d, i) => colorScale(histogramScale(binStart(i)))
: "#bbb"
);
} }
// BRUSH // BRUSH
@@ -350,7 +376,7 @@ class HistogramBrush extends React.PureComponent {
.brushX() .brushX()
.extent([ .extent([
[x.range()[0], y.range()[1]], [x.range()[0], y.range()[1]],
[x.range()[1], this.marginTop + this.height + this.marginBottom] [x.range()[1], this.marginTop + this.height + this.marginBottom],
]) ])
/* /*
emit start so that the Undoable history can save an undo point emit start so that the Undoable history can save an undo point
@@ -375,7 +401,11 @@ class HistogramBrush extends React.PureComponent {
d3 d3
.axisBottom(x) .axisBottom(x)
.ticks(4) .ticks(4)
.tickFormat(d3.format(x.domain().some(n => Math.abs(n) >= 10000) ? ".2e" : ",")) .tickFormat(
d3.format(
x.domain().some((n) => Math.abs(n) >= 10000) ? ".2e" : ","
)
)
); );
/* Y AXIS */ /* Y AXIS */
@@ -387,7 +417,11 @@ class HistogramBrush extends React.PureComponent {
d3 d3
.axisRight(y) .axisRight(y)
.ticks(3) .ticks(3)
.tickFormat(d3.format(y.domain().some(n => Math.abs(n) >= 10000) ? ".0e" : ",")) .tickFormat(
d3.format(
y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
)
)
); );
/* axis style */ /* axis style */
@@ -404,19 +438,19 @@ class HistogramBrush extends React.PureComponent {
const { const {
field, field,
world, world,
colorAccessor, isColorAccessor,
isUserDefined, isUserDefined,
isDiffExp, isDiffExp,
logFoldChange, logFoldChange,
pvalAdj, pvalAdj,
scatterplotXXaccessor, isScatterplotXXaccessor,
scatterplotYYaccessor, isScatterplotYYaccessor,
zebra zebra,
} = this.props; } = this.props;
const fieldForId = field.replace(/\s/g, "_"); const fieldForId = field.replace(/\s/g, "_");
const { const {
min: unclippedRangeMin, min: unclippedRangeMin,
max: unclippedRangeMax max: unclippedRangeMax,
} = HistogramBrush.getColumn(world, field, false).summarize(); } = HistogramBrush.getColumn(world, field, false).summarize();
const unclippedRangeMinColor = const unclippedRangeMinColor =
world.clipQuantiles.min === 0 ? "#bbb" : globals.blue; world.clipQuantiles.min === 0 ? "#bbb" : globals.blue;
@@ -436,14 +470,14 @@ class HistogramBrush extends React.PureComponent {
} }
style={{ style={{
padding: globals.leftSidebarSectionPadding, padding: globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white" backgroundColor: zebra ? globals.lightestGrey : "white",
}} }}
> >
<div <div
style={{ style={{
display: "flex", display: "flex",
justifyContent: "flex-end", justifyContent: "flex-end",
paddingBottom: "8px" paddingBottom: "8px",
}} }}
> >
{isDiffExp || isUserDefined ? ( {isDiffExp || isUserDefined ? (
@@ -456,16 +490,16 @@ class HistogramBrush extends React.PureComponent {
<Button <Button
data-testid={`plot-x-${field}`} data-testid={`plot-x-${field}`}
onClick={this.handleSetGeneAsScatterplotX} onClick={this.handleSetGeneAsScatterplotX}
active={scatterplotXXaccessor === field} active={isScatterplotXXaccessor}
intent={scatterplotXXaccessor === field ? "primary" : "none"} intent={isScatterplotXXaccessor ? "primary" : "none"}
> >
plot x plot x
</Button> </Button>
<Button <Button
data-testid={`plot-y-${field}`} data-testid={`plot-y-${field}`}
onClick={this.handleSetGeneAsScatterplotY} onClick={this.handleSetGeneAsScatterplotY}
active={scatterplotYYaccessor === field} active={isScatterplotYYaccessor}
intent={scatterplotYYaccessor === field ? "primary" : "none"} intent={isScatterplotYYaccessor ? "primary" : "none"}
> >
plot y plot y
</Button> </Button>
@@ -479,7 +513,7 @@ class HistogramBrush extends React.PureComponent {
style={{ style={{
color: globals.blue, color: globals.blue,
cursor: "pointer", cursor: "pointer",
marginLeft: 7 marginLeft: 7,
}} }}
> >
remove remove
@@ -492,8 +526,8 @@ class HistogramBrush extends React.PureComponent {
> >
<Button <Button
onClick={this.handleColorAction} onClick={this.handleColorAction}
active={colorAccessor === field} active={isColorAccessor}
intent={colorAccessor === field ? "primary" : "none"} intent={isColorAccessor ? "primary" : "none"}
data-testclass="colorby" data-testclass="colorby"
data-testid={`colorby-${field}`} data-testid={`colorby-${field}`}
icon="tint" icon="tint"
@@ -506,12 +540,12 @@ 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 => this.drawHistogram(svgRef)} ref={(svgRef) => this.drawHistogram(svgRef)}
/> />
<div <div
style={{ style={{
display: "flex", display: "flex",
justifyContent: "space-between" justifyContent: "space-between",
}} }}
> >
<span style={{ color: unclippedRangeMinColor }}> <span style={{ color: unclippedRangeMinColor }}>
@@ -533,7 +567,7 @@ class HistogramBrush extends React.PureComponent {
style={{ style={{
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "baseline" alignItems: "baseline",
}} }}
> >
<span> <span>
@@ -543,7 +577,7 @@ class HistogramBrush extends React.PureComponent {
<span <span
style={{ style={{
marginLeft: 7, marginLeft: 7,
padding: 2 padding: 2,
}} }}
> >
<strong>p-value (adj):</strong> <strong>p-value (adj):</strong>
+9 -22
View File
@@ -8,32 +8,20 @@ import { Button } from "@blueprintjs/core";
import * as globals from "../../globals"; import * as globals from "../../globals";
import HistogramBrush from "../brushableHistogram"; import HistogramBrush from "../brushableHistogram";
@connect(state => ({ @connect((state) => ({
obsAnnotations: state.world?.obsAnnotations, obsAnnotations: state.world?.obsAnnotations,
colorAccessor: state.colors.colorAccessor, colorAccessor: state.colors.colorAccessor,
colorScale: state.colors.scale, colorScale: state.colors.scale,
schema: state.world?.schema schema: state.world?.schema,
})) }))
class Continuous extends React.PureComponent { class Continuous extends React.PureComponent {
handleColorAction = key => {
return () => {
const { dispatch, obsAnnotations } = this.props;
const summary = obsAnnotations.col(key).summarize();
dispatch({
type: "color by continuous metadata",
colorAccessor: key,
rangeForColorAccessor: summary
});
};
};
static renderIsStillLoading(zebra, key) { static renderIsStillLoading(zebra, key) {
return ( return (
<div <div
key={key} key={key}
style={{ style={{
padding: globals.leftSidebarSectionPadding, padding: globals.leftSidebarSectionPadding,
backgroundColor: zebra % 2 === 0 ? globals.lightestGrey : "white" backgroundColor: zebra % 2 === 0 ? globals.lightestGrey : "white",
}} }}
> >
<div <div
@@ -41,7 +29,7 @@ class Continuous extends React.PureComponent {
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
justifyItems: "center", justifyItems: "center",
alignItems: "center" alignItems: "center",
}} }}
> >
<div style={{ minWidth: 30 }} /> <div style={{ minWidth: 30 }} />
@@ -51,7 +39,7 @@ class Continuous extends React.PureComponent {
<div <div
style={{ style={{
display: "flex", display: "flex",
justifyContent: "flex-end" justifyContent: "flex-end",
}} }}
> >
<Button minimal loading intent="primary" /> <Button minimal loading intent="primary" />
@@ -66,16 +54,16 @@ class Continuous extends React.PureComponent {
const obsIndex = schema.annotations.obs.index; const obsIndex = schema.annotations.obs.index;
const allContinuousNames = schema.annotations.obs.columns const allContinuousNames = schema.annotations.obs.columns
.filter(col => col.type === "int32" || col.type === "float32") .filter((col) => col.type === "int32" || col.type === "float32")
.filter(col => col.name !== obsIndex) .filter((col) => col.name !== obsIndex)
.map(col => col.name); .map((col) => col.name);
/* initial value for iterator to simulate index, ranges is an object */ /* initial value for iterator to simulate index, ranges is an object */
let zebra = 0; let zebra = 0;
return ( return (
<div> <div>
{allContinuousNames.map(key => { {allContinuousNames.map((key) => {
if (!obsAnnotations.hasCol(key)) { if (!obsAnnotations.hasCol(key)) {
// still loading! // still loading!
zebra += 1; zebra += 1;
@@ -98,7 +86,6 @@ class Continuous extends React.PureComponent {
isObs isObs
zebra={zebra % 2 === 0} zebra={zebra % 2 === 0}
ranges={summary} ranges={summary}
handleColorAction={this.handleColorAction(key)}
/> />
); );
} }
+1 -1
View File
@@ -15,7 +15,7 @@ const ContinuousSelection = (state = {}, action) => {
); );
return { return {
...state, ...state,
[name]: action.range [name]: action.range,
}; };
} }
case "continuous metadata histogram cancel": { case "continuous metadata histogram cancel": {
+14 -9
View File
@@ -4,14 +4,14 @@ import Crossfilter from "../util/typedCrossfilter";
import { import {
World, World,
ControlsHelpers, ControlsHelpers,
AnnotationsHelpers AnnotationsHelpers,
} from "../util/stateManager"; } from "../util/stateManager";
import { import {
layoutDimensionName, layoutDimensionName,
obsAnnoDimensionName, obsAnnoDimensionName,
userDefinedDimensionName, userDefinedDimensionName,
diffexpDimensionName, diffexpDimensionName,
makeContinuousDimensionName makeContinuousDimensionName,
} from "../util/nameCreators"; } from "../util/nameCreators";
const XYDimName = layoutDimensionName("XY"); const XYDimName = layoutDimensionName("XY");
@@ -124,7 +124,7 @@ const CrossfilterReducerBase = (
case "request differential expression success": { case "request differential expression success": {
const { world } = prevSharedState; const { world } = prevSharedState;
const varIndexName = world.schema.annotations.var.index; const varIndexName = world.schema.annotations.var.index;
const genes = _.map(action.data, d => const genes = _.map(action.data, (d) =>
world.varAnnotations.at(d[0], varIndexName) world.varAnnotations.at(d[0], varIndexName)
); );
const crossfilter = _.reduce( const crossfilter = _.reduce(
@@ -216,7 +216,7 @@ const CrossfilterReducerBase = (
minX, minX,
minY, minY,
maxX, maxX,
maxY maxY,
}); });
} }
@@ -224,7 +224,7 @@ const CrossfilterReducerBase = (
const { polygon } = action; const { polygon } = action;
return state.select(XYDimName, { return state.select(XYDimName, {
mode: "within-polygon", mode: "within-polygon",
polygon polygon,
}); });
} }
@@ -249,7 +249,12 @@ const CrossfilterReducerBase = (
return state.select(name, { mode: "all" }); return state.select(name, { mode: "all" });
} }
const [lo, hi] = action.range; const [lo, hi] = action.range;
const newState = state.select(name, { mode: "range", lo, hi }); const newState = state.select(name, {
mode: "range",
lo,
hi,
inclusive: true, // [lo, hi] incluisve selection
});
return newState; return newState;
} }
@@ -261,19 +266,19 @@ const CrossfilterReducerBase = (
const values = categoryValues.filter((v, i) => categoryValueSelected[i]); const values = categoryValues.filter((v, i) => categoryValueSelected[i]);
return state.select(obsAnnoDimensionName(action.metadataField), { return state.select(obsAnnoDimensionName(action.metadataField), {
mode: "exact", mode: "exact",
values values,
}); });
} }
case "categorical metadata filter none of these": { case "categorical metadata filter none of these": {
return state.select(obsAnnoDimensionName(action.metadataField), { return state.select(obsAnnoDimensionName(action.metadataField), {
mode: "none" mode: "none",
}); });
} }
case "categorical metadata filter all of these": { case "categorical metadata filter all of these": {
return state.select(obsAnnoDimensionName(action.metadataField), { return state.select(obsAnnoDimensionName(action.metadataField), {
mode: "all" mode: "all",
}); });
} }
+30 -23
View File
@@ -5,7 +5,7 @@ import {
lowerBound, lowerBound,
binarySearch, binarySearch,
lowerBoundIndirect, lowerBoundIndirect,
upperBoundIndirect upperBoundIndirect,
} from "./sort"; } from "./sort";
import { makeSortIndex } from "./util"; import { makeSortIndex } from "./util";
@@ -101,12 +101,12 @@ export default class ImmutableTypedCrossfilter {
id, id,
dim, dim,
name, name,
selection: dim.select({ mode: "all" }) selection: dim.select({ mode: "all" }),
} },
}; };
return new ImmutableTypedCrossfilter(data, dimensions, { return new ImmutableTypedCrossfilter(data, dimensions, {
bitArray bitArray,
}); });
} }
@@ -126,7 +126,7 @@ export default class ImmutableTypedCrossfilter {
} }
return new ImmutableTypedCrossfilter(data, dimensions, { return new ImmutableTypedCrossfilter(data, dimensions, {
bitArray bitArray,
}); });
} }
@@ -138,8 +138,8 @@ export default class ImmutableTypedCrossfilter {
[newName]: { [newName]: {
...dim, ...dim,
name: newName, name: newName,
dim: dim.dim.rename(newName) dim: dim.dim.rename(newName),
} },
}; };
return new ImmutableTypedCrossfilter(data, newDimensions, selectionCache); return new ImmutableTypedCrossfilter(data, newDimensions, selectionCache);
} }
@@ -205,19 +205,19 @@ export default class ImmutableTypedCrossfilter {
If sort index exists in the dimension, assume sort ordered ranges. If sort index exists in the dimension, assume sort ordered ranges.
*/ */
if (oldSeln.index) { if (oldSeln.index) {
dels.forEach(interval => dels.forEach((interval) =>
bitArray.deselectIndirectFromRange(id, oldSeln.index, interval) bitArray.deselectIndirectFromRange(id, oldSeln.index, interval)
); );
} else { } else {
dels.forEach(interval => bitArray.deselectFromRange(id, interval)); dels.forEach((interval) => bitArray.deselectFromRange(id, interval));
} }
if (newSeln.index) { if (newSeln.index) {
adds.forEach(interval => adds.forEach((interval) =>
bitArray.selectIndirectFromRange(id, newSeln.index, interval) bitArray.selectIndirectFromRange(id, newSeln.index, interval)
); );
} else { } else {
adds.forEach(interval => bitArray.selectFromRange(id, interval)); adds.forEach((interval) => bitArray.selectFromRange(id, interval));
} }
return { bitArray }; return { bitArray };
@@ -229,12 +229,12 @@ export default class ImmutableTypedCrossfilter {
if (!this.selectionCache.bitArray) { if (!this.selectionCache.bitArray) {
// console.log("...rebuilding crossfilter cache..."); // console.log("...rebuilding crossfilter cache...");
const bitArray = new BitArray(this.data.length); const bitArray = new BitArray(this.data.length);
Object.keys(this.dimensions).forEach(name => { Object.keys(this.dimensions).forEach((name) => {
const { selection } = this.dimensions[name]; const { selection } = this.dimensions[name];
const id = bitArray.allocDimension(); const id = bitArray.allocDimension();
this.dimensions[name].id = id; this.dimensions[name].id = id;
const { ranges, index } = selection; const { ranges, index } = selection;
ranges.forEach(range => { ranges.forEach((range) => {
if (index) { if (index) {
bitArray.selectIndirectFromRange(id, index, range); bitArray.selectIndirectFromRange(id, index, range);
} else { } else {
@@ -395,7 +395,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
// only by enumerated dimensions // only by enumerated dimensions
array = this._createValueArray( array = this._createValueArray(
data, data,
i => value[i], (i) => value[i],
new ValueArrayType(data.length) new ValueArrayType(data.length)
); );
} else { } else {
@@ -448,7 +448,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
for (let v = 0, len = values.length; v < len; v += 1) { for (let v = 0, len = values.length; v < len; v += 1) {
const r = [ const r = [
lowerBoundIndirect(value, index, values[v], 0, value.length), lowerBoundIndirect(value, index, values[v], 0, value.length),
upperBoundIndirect(value, index, values[v], 0, value.length) upperBoundIndirect(value, index, values[v], 0, value.length),
]; ];
if (r[0] <= r[1]) { if (r[0] <= r[1]) {
ranges.push(r); ranges.push(r);
@@ -459,12 +459,16 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
selectRange(spec) { selectRange(spec) {
const { value, index } = this; const { value, index } = this;
/* [lo, hi) */ /*
const { lo, hi } = spec; if !inclusive: [lo, hi) else [lo, hi]
*/
const { lo, hi, inclusive } = spec;
const ranges = []; const ranges = [];
const r = [ const r = [
lowerBoundIndirect(value, index, lo, 0, value.length), lowerBoundIndirect(value, index, lo, 0, value.length),
lowerBoundIndirect(value, index, hi, 0, value.length) !!inclusive
? upperBoundIndirect(value, index, hi, 0, value.length)
: lowerBoundIndirect(value, index, hi, 0, value.length),
]; ];
if (r[0] < r[1]) ranges.push(r); if (r[0] < r[1]) ranges.push(r);
return { ranges, index }; return { ranges, index };
@@ -504,7 +508,9 @@ class ImmutableEnumDimension extends ImmutableScalarDimension {
const { values } = spec; const { values } = spec;
return super.selectExact({ return super.selectExact({
mode: spec.mode, mode: spec.mode,
values: values.map(v => binarySearch(enumIndex, v, 0, enumIndex.length)) values: values.map((v) =>
binarySearch(enumIndex, v, 0, enumIndex.length)
),
}); });
} }
@@ -593,13 +599,13 @@ class ImmutableSpatialDimension extends _ImmutableBaseDimension {
if (maxY - minY > maxX - minX) { if (maxY - minY > maxX - minX) {
slice = [ slice = [
lowerBoundIndirect(X, Xindex, minX, 0, length), lowerBoundIndirect(X, Xindex, minX, 0, length),
lowerBoundIndirect(X, Xindex, maxX, 0, length) lowerBoundIndirect(X, Xindex, maxX, 0, length),
]; ];
index = Xindex; index = Xindex;
} else { } else {
slice = [ slice = [
lowerBoundIndirect(Y, Yindex, minY, 0, length), lowerBoundIndirect(Y, Yindex, minY, 0, length),
lowerBoundIndirect(Y, Yindex, maxY, 0, length) lowerBoundIndirect(Y, Yindex, maxY, 0, length),
]; ];
index = Yindex; index = Yindex;
} }
@@ -632,7 +638,7 @@ class ImmutableSpatialDimension extends _ImmutableBaseDimension {
export const DimTypes = { export const DimTypes = {
scalar: ImmutableScalarDimension, scalar: ImmutableScalarDimension,
enum: ImmutableEnumDimension, enum: ImmutableEnumDimension,
spatial: ImmutableSpatialDimension spatial: ImmutableSpatialDimension,
}; };
function isArrayOrTypedArray(x) { function isArrayOrTypedArray(x) {
@@ -682,7 +688,8 @@ function withinPolygon(polygon, x, y) {
x1 = p[0]; x1 = p[0];
y1 = p[1]; y1 = p[1];
if (((y1 > y) !== (y0 > y)) && (x < (x0 - x1) * (y - y1) / (y0 - y1) + x1)) inside = !inside; if (y1 > y !== y0 > y && x < ((x0 - x1) * (y - y1)) / (y0 - y1) + x1)
inside = !inside;
x0 = x1; x0 = x1;
y0 = y1; y0 = y1;
} }