mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-27 04:48:11 +08:00
Lasso (#586)
* lasso working * break out invert into own function * action * add spatial dimension to crossfilter, in support of polygon lasso * improve comments on new dimension API * lasso vs zoom
This commit is contained in:
committed by
Charlotte Weaver
parent
2e9525741f
commit
dbb3a309a9
@@ -4,6 +4,7 @@ import _ from "lodash";
|
||||
import * as kvCache from "./keyvalcache";
|
||||
import summarizeAnnotations from "./summarizeAnnotations";
|
||||
import { layoutDimensionName, obsAnnoDimensionName } from "../nameCreators";
|
||||
import Crossfilter from "../typedCrossfilter";
|
||||
import { sliceByIndex } from "../typedCrossfilter/util";
|
||||
|
||||
/*
|
||||
@@ -231,7 +232,12 @@ export function createVarDimension(
|
||||
crossfilter,
|
||||
geneName
|
||||
) {
|
||||
return crossfilter.dimension(_worldVarDataCache[geneName], Float32Array);
|
||||
// return crossfilter.dimension(_worldVarDataCache[geneName], Float32Array);
|
||||
return crossfilter.dimension(
|
||||
Crossfilter.ScalarDimension,
|
||||
_worldVarDataCache[geneName],
|
||||
Float32Array
|
||||
);
|
||||
}
|
||||
|
||||
export function createObsDimensionMap(crossfilter, world) {
|
||||
@@ -246,9 +252,14 @@ export function createObsDimensionMap(crossfilter, world) {
|
||||
.filter(anno => anno.name !== "name")
|
||||
.transform((result, anno) => {
|
||||
const dimType = deduceDimensionType(anno, anno.name);
|
||||
// XXX if dimtype is a scalar, we may be able to do better?
|
||||
if (dimType) {
|
||||
if (dimType === "enum") {
|
||||
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
||||
Crossfilter.EnumDimension,
|
||||
r => r[anno.name]
|
||||
);
|
||||
} else {
|
||||
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
||||
Crossfilter.ScalarDimension,
|
||||
r => r[anno.name],
|
||||
dimType
|
||||
);
|
||||
@@ -259,13 +270,10 @@ export function createObsDimensionMap(crossfilter, world) {
|
||||
/*
|
||||
Add crossfilter dimensions allowing filtering on layout
|
||||
*/
|
||||
dimensionMap[layoutDimensionName("X")] = crossfilter.dimension(
|
||||
dimensionMap[layoutDimensionName("XY")] = crossfilter.dimension(
|
||||
Crossfilter.SpatialDimension,
|
||||
obsLayout.X,
|
||||
Float32Array
|
||||
);
|
||||
dimensionMap[layoutDimensionName("Y")] = crossfilter.dimension(
|
||||
obsLayout.Y,
|
||||
Float32Array
|
||||
obsLayout.Y
|
||||
);
|
||||
|
||||
return dimensionMap;
|
||||
|
||||
@@ -27,6 +27,8 @@ more complex API. In a few cases, elements of that API were incorporated.
|
||||
https://github.com/square/crossfilter/
|
||||
|
||||
*/
|
||||
// XXX replace
|
||||
import { polygonContains } from "d3";
|
||||
|
||||
import PositiveIntervals from "./positiveIntervals";
|
||||
import BitArray from "./bitArray";
|
||||
@@ -66,14 +68,22 @@ class TypedCrossfilter {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
dimension(value, valueArrayType) {
|
||||
/*
|
||||
Create a crossfilter dimension, upon which filtering (subselection) can
|
||||
be done. Each dimension is typed, and has a particular set of filtering
|
||||
semantics.
|
||||
* ScalarDimension - backed by TypedArray values, supporting filtering
|
||||
by value (within a value range, or one or more exact values)
|
||||
* EnumDimension - backed by an enumeration (eg, strings, bools), filtering
|
||||
by one or more enum categories.
|
||||
* SpatialDimension - backed by 2D points, filter by containment within
|
||||
various shapes (currently supports within Rectangle and within Polygon).
|
||||
Call this method to create a dimension, passing arguments appropriate for
|
||||
the dimension constructor.
|
||||
*/
|
||||
dimension(DimensionType, ...rest) {
|
||||
const id = this.selection.allocDimension();
|
||||
let dim;
|
||||
if (valueArrayType === "enum") {
|
||||
dim = new EnumDimension(value, this, id);
|
||||
} else {
|
||||
dim = new ScalarDimension(value, valueArrayType, this, id);
|
||||
}
|
||||
const dim = new DimensionType(this, id, ...rest);
|
||||
this.filters.push({ id, dim });
|
||||
dim.filterAll();
|
||||
return dim;
|
||||
@@ -115,13 +125,34 @@ class TypedCrossfilter {
|
||||
}
|
||||
}
|
||||
|
||||
// Base dimension type - value must be a scalar type (eg, int, float),
|
||||
// and value array must be a TypedArray.
|
||||
//
|
||||
class ScalarDimension {
|
||||
constructor(value, ValueArrayType, xfltr, id) {
|
||||
// Base dimension type - not exported.
|
||||
class _Dimension {
|
||||
constructor(xfltr, id) {
|
||||
this.crossfilter = xfltr;
|
||||
this._id = id;
|
||||
this.groups = [];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.crossfilter._freeDimension(this._id);
|
||||
return this;
|
||||
}
|
||||
|
||||
id() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
_filterUpdate() {
|
||||
this.crossfilter.updateTime += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar dimension type - value must be a scalar type (eg, int, float),
|
||||
// and value array must be a TypedArray.
|
||||
//
|
||||
class ScalarDimension extends _Dimension {
|
||||
constructor(xfltr, id, value, ValueArrayType) {
|
||||
super(xfltr, id);
|
||||
|
||||
// current selection filter, expressed as PostiveIntervals.
|
||||
this.currentFilter = [];
|
||||
@@ -151,9 +182,6 @@ class ScalarDimension {
|
||||
|
||||
// create sort index
|
||||
this.index = makeSortIndex(array);
|
||||
|
||||
// groups, if any
|
||||
this.groups = [];
|
||||
}
|
||||
|
||||
_createValueArray(value, array) {
|
||||
@@ -167,15 +195,6 @@ class ScalarDimension {
|
||||
return larray;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.crossfilter._freeDimension(this._id);
|
||||
return this;
|
||||
}
|
||||
|
||||
id() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
// Argument is an array of intervals indicating records newly selected/filtered
|
||||
//
|
||||
_updateFilters(newFilter) {
|
||||
@@ -209,7 +228,7 @@ class ScalarDimension {
|
||||
);
|
||||
|
||||
this.currentFilter = cNewFilter;
|
||||
this.crossfilter.updateTime += 1;
|
||||
this._filterUpdate();
|
||||
}
|
||||
|
||||
// filter by value - exact match
|
||||
@@ -355,8 +374,8 @@ class ScalarDimension {
|
||||
// strings, which can be mapped into an fixed numeric range [0..n).
|
||||
//
|
||||
class EnumDimension extends ScalarDimension {
|
||||
constructor(value, xfltr, id) {
|
||||
super(value, Uint32Array, xfltr, id);
|
||||
constructor(xfltr, id, value) {
|
||||
super(xfltr, id, value, Uint32Array);
|
||||
}
|
||||
|
||||
_createValueArray(value, array) {
|
||||
@@ -408,6 +427,127 @@ class EnumDimension extends ScalarDimension {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Super simple 2D spatial dimension, supporting basic "filter within"
|
||||
operations.
|
||||
*/
|
||||
class SpatialDimension extends _Dimension {
|
||||
constructor(xfltr, id, X, Y) {
|
||||
super(xfltr, id);
|
||||
|
||||
if (X.length !== Y.length && X.length !== this.crossfilter.data.length) {
|
||||
throw new RangeError(
|
||||
"SpatialDimension values must have same dimensionality as crossfilter"
|
||||
);
|
||||
}
|
||||
this.X = X;
|
||||
this.Y = Y;
|
||||
|
||||
this.Xindex = makeSortIndex(X);
|
||||
this.Yindex = makeSortIndex(Y);
|
||||
}
|
||||
|
||||
filterAll() {
|
||||
this.crossfilter.selection.selectAll(this._id);
|
||||
this._filterUpdate();
|
||||
}
|
||||
|
||||
filterNone() {
|
||||
this.crossfilter.selection.deselectAll(this._id);
|
||||
this._filterUpdate();
|
||||
}
|
||||
|
||||
/*
|
||||
this could be smarter, but we don't currently use it...
|
||||
*/
|
||||
filterWithinRect(northwest, southeast) {
|
||||
const [x0, y0] = northwest;
|
||||
const [x1, y1] = southeast;
|
||||
const { X, Y } = this;
|
||||
const seln = this.crossfilter.selection;
|
||||
const { _id } = this;
|
||||
seln.deselectAll(_id);
|
||||
for (let i = 0, l = this.X.length; i < l; i += 1) {
|
||||
const x = X[i];
|
||||
const y = Y[i];
|
||||
if (x0 <= x && x < x1 && y0 <= y && y < y1) {
|
||||
seln.selectOne(_id, i);
|
||||
}
|
||||
}
|
||||
this._filterUpdate();
|
||||
}
|
||||
|
||||
/*
|
||||
Relatively brute force filter by polygon. Polygon is array of points, where
|
||||
each point is [x,y]. Eg, [[x0,y0], [x1,y1], ...].
|
||||
|
||||
Currently uses d3.polygonContains() to test for polygon inclusion, which itself
|
||||
uses a ray casting (crossing number) algorithm. There are a series of optimizations
|
||||
to make this faster:
|
||||
* first sliced by X or Y, using an index on the axis
|
||||
* then the polygon bounding box is used for trivial rejection
|
||||
* then the polygon test is applied
|
||||
*/
|
||||
filterWithinPolygon(polygon) {
|
||||
/* return bounding box of the polygon */
|
||||
function polygonBoundingBox(pg) {
|
||||
let minX = Number.MAX_VALUE;
|
||||
let minY = Number.MAX_VALUE;
|
||||
let maxX = Number.MIN_VALUE;
|
||||
let maxY = Number.MIN_VALUE;
|
||||
for (let i = 0, l = pg.length; i < l; i += 1) {
|
||||
const p = pg[i];
|
||||
const x = p[0];
|
||||
const y = p[1];
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
return [minX, minY, maxX, maxY];
|
||||
}
|
||||
|
||||
const [minX, minY, maxX, maxY] = polygonBoundingBox(polygon);
|
||||
const { X, Y } = this;
|
||||
let slice;
|
||||
let index;
|
||||
if (maxY - minY > maxX - minX) {
|
||||
slice = [
|
||||
lowerBoundIndirect(X, this.Xindex, minX, 0, X.length),
|
||||
upperBoundIndirect(X, this.Xindex, maxX, 0, X.length)
|
||||
];
|
||||
index = this.Xindex;
|
||||
} else {
|
||||
slice = [
|
||||
lowerBoundIndirect(Y, this.Yindex, minY, 0, Y.length),
|
||||
upperBoundIndirect(Y, this.Yindex, maxY, 0, Y.length)
|
||||
];
|
||||
index = this.Yindex;
|
||||
}
|
||||
|
||||
const seln = this.crossfilter.selection;
|
||||
const { _id } = this;
|
||||
const testWithin = polygonContains; // d3.polygonContains()
|
||||
seln.deselectAll(_id);
|
||||
|
||||
for (let i = slice[0], e = slice[1]; i < e; i += 1) {
|
||||
const rid = index[i];
|
||||
const x = X[rid];
|
||||
const y = Y[rid];
|
||||
if (
|
||||
minX <= x &&
|
||||
x < maxX &&
|
||||
minY <= y &&
|
||||
y < maxY &&
|
||||
testWithin(polygon, [x, y])
|
||||
) {
|
||||
seln.selectOne(_id, rid);
|
||||
}
|
||||
}
|
||||
this._filterUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// Groups! Map/reduce
|
||||
//
|
||||
class ScalarGroup {
|
||||
@@ -611,5 +751,6 @@ crossfilter.BitArray = BitArray;
|
||||
crossfilter.TypedCrossfilter = TypedCrossfilter;
|
||||
crossfilter.ScalarDimension = ScalarDimension;
|
||||
crossfilter.EnumDimension = EnumDimension;
|
||||
crossfilter.SpatialDimension = SpatialDimension;
|
||||
|
||||
export default crossfilter;
|
||||
|
||||
Reference in New Issue
Block a user