mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-05 05:38: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
@@ -187,11 +187,8 @@ describe("createObsDimensionMap", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(dimensionMap[layoutDimensionName("X")]).toBeInstanceOf(
|
expect(dimensionMap[layoutDimensionName("XY")]).toBeInstanceOf(
|
||||||
Crossfilter.ScalarDimension
|
Crossfilter.SpatialDimension
|
||||||
);
|
|
||||||
expect(dimensionMap[layoutDimensionName("Y")]).toBeInstanceOf(
|
|
||||||
Crossfilter.ScalarDimension
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,22 +102,20 @@ const someData = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function groupReduce(data, valueMap, valueReduce, valueInit) {
|
function groupReduce(data, valueMap, valueReduce, valueInit) {
|
||||||
return _
|
return _.reduce(
|
||||||
.reduce(
|
data,
|
||||||
data,
|
(acc, value) => {
|
||||||
(acc, value) => {
|
const k = valueMap(value);
|
||||||
const k = valueMap(value);
|
let r = _.find(acc, o => o.key === k);
|
||||||
let r = _.find(acc, o => o.key === k);
|
if (!r) {
|
||||||
if (!r) {
|
r = { key: k, value: valueInit() };
|
||||||
r = { key: k, value: valueInit() };
|
acc.push(r);
|
||||||
acc.push(r);
|
}
|
||||||
}
|
r.value = valueReduce(r.value, value);
|
||||||
r.value = valueReduce(r.value, value);
|
return acc;
|
||||||
return acc;
|
},
|
||||||
},
|
[]
|
||||||
[]
|
).sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
||||||
)
|
|
||||||
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupCount(data, map) {
|
function groupCount(data, map) {
|
||||||
@@ -139,7 +137,11 @@ describe("typedCrossfilter", () => {
|
|||||||
expect(payments.size()).toEqual(someData.length);
|
expect(payments.size()).toEqual(someData.length);
|
||||||
expect(payments.all()).toEqual(someData);
|
expect(payments.all()).toEqual(someData);
|
||||||
|
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.quantity,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
expect(quantity).toBeDefined();
|
expect(quantity).toBeDefined();
|
||||||
expect(quantity.id()).toBeDefined();
|
expect(quantity.id()).toBeDefined();
|
||||||
|
|
||||||
@@ -150,10 +152,22 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("filterAll and filterNone", () => {
|
test("filterAll and filterNone", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
expect(quantity).toBeDefined();
|
expect(quantity).toBeDefined();
|
||||||
expect(tip).toBeDefined();
|
expect(tip).toBeDefined();
|
||||||
@@ -198,10 +212,22 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("filterExact", () => {
|
test("filterExact", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
quantity.filterExact(1);
|
quantity.filterExact(1);
|
||||||
expect(payments.countFiltered()).toEqual(
|
expect(payments.countFiltered()).toEqual(
|
||||||
@@ -222,10 +248,22 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("filterRange", () => {
|
test("filterRange", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
tip.filterRange([0, 91]);
|
tip.filterRange([0, 91]);
|
||||||
expect(payments.allFiltered()).toEqual(
|
expect(payments.allFiltered()).toEqual(
|
||||||
@@ -251,10 +289,22 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("filterEnum", () => {
|
test("filterEnum", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
type.filterEnum(["tab", "cash"]);
|
type.filterEnum(["tab", "cash"]);
|
||||||
expect(payments.allFiltered()).toEqual(
|
expect(payments.allFiltered()).toEqual(
|
||||||
@@ -274,15 +324,31 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
test("more than 32 dimensions", () => {
|
test("more than 32 dimensions", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
const tip = payments.dimension(r => r.tip, Float32Array);
|
crossfilter.ScalarDimension,
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
r => r.quantity,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
// Create a bunch of fake dimensions to ensure we can handle > 32
|
// Create a bunch of fake dimensions to ensure we can handle > 32
|
||||||
let dimMap = {};
|
let dimMap = {};
|
||||||
for (let i = 0; i < 65; i++) {
|
for (let i = 0; i < 65; i++) {
|
||||||
dimMap[i] = payments.dimension(r => Math.random(), Float32Array);
|
dimMap[i] = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => Math.random(),
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
expect(dimMap[i]).toBeDefined();
|
expect(dimMap[i]).toBeDefined();
|
||||||
expect(dimMap[i].id()).toBeDefined();
|
expect(dimMap[i].id()).toBeDefined();
|
||||||
}
|
}
|
||||||
@@ -304,10 +370,22 @@ describe("typedCrossfilter", () => {
|
|||||||
test("group, default mapping, default reducer, no filter", () => {
|
test("group, default mapping, default reducer, no filter", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
|
|
||||||
var quantity = payments.dimension(r => r.quantity, Int32Array);
|
const quantity = payments.dimension(
|
||||||
var tip = payments.dimension(r => r.tip, Int32Array);
|
crossfilter.ScalarDimension,
|
||||||
var type = payments.dimension(r => r.type, "enum");
|
r => r.quantity,
|
||||||
var total = payments.dimension(r => r.total, Int32Array);
|
Int32Array
|
||||||
|
);
|
||||||
|
const tip = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.tip,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
|
||||||
_.each(
|
_.each(
|
||||||
{
|
{
|
||||||
@@ -331,9 +409,17 @@ describe("typedCrossfilter", () => {
|
|||||||
// custom mapping in groups only works for scalar types. Enums do not
|
// custom mapping in groups only works for scalar types. Enums do not
|
||||||
// currently implement it.
|
// currently implement it.
|
||||||
|
|
||||||
const tip = payments.dimension(r => r.tip, Int32Array);
|
const tip = payments.dimension(
|
||||||
const totalX10 = payments.dimension(r => r.total * 10, Int32Array);
|
crossfilter.ScalarDimension,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
r => r.tip,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const totalX10 = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total * 10,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
const paymentsByTip_A = tip.group();
|
const paymentsByTip_A = tip.group();
|
||||||
const paymentsByTip_B = tip.group(r => 10 * r);
|
const paymentsByTip_B = tip.group(r => 10 * r);
|
||||||
@@ -370,8 +456,12 @@ describe("typedCrossfilter", () => {
|
|||||||
test("group, default map, custom reducer, no filters", () => {
|
test("group, default map, custom reducer, no filters", () => {
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
|
|
||||||
const total = payments.dimension(r => r.total, Float32Array);
|
const total = payments.dimension(
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Float32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
const paymentsByTotal = total.group();
|
const paymentsByTotal = total.group();
|
||||||
const paymentsByType = type.group();
|
const paymentsByType = type.group();
|
||||||
@@ -407,9 +497,17 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
|
|
||||||
const tip = payments.dimension(r => r.tip, Int32Array);
|
const tip = payments.dimension(
|
||||||
const total = payments.dimension(r => r.total, Int32Array);
|
crossfilter.ScalarDimension,
|
||||||
const type = payments.dimension(r => r.type, "enum");
|
r => r.tip,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const total = payments.dimension(
|
||||||
|
crossfilter.ScalarDimension,
|
||||||
|
r => r.total,
|
||||||
|
Int32Array
|
||||||
|
);
|
||||||
|
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
||||||
|
|
||||||
const paymentsByTip = tip.group();
|
const paymentsByTip = tip.group();
|
||||||
const paymentsByTotal = total.group();
|
const paymentsByTotal = total.group();
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Graph extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
svg: null,
|
svg: null,
|
||||||
brush: null,
|
brush: null,
|
||||||
mode: "brush"
|
mode: "lasso"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,9 @@ class Graph extends React.Component {
|
|||||||
this.handleBrushSelectAction.bind(this),
|
this.handleBrushSelectAction.bind(this),
|
||||||
this.handleBrushDeselectAction.bind(this),
|
this.handleBrushDeselectAction.bind(this),
|
||||||
responsive,
|
responsive,
|
||||||
this.graphPaddingRight
|
this.graphPaddingRight,
|
||||||
|
this.handleLassoStart.bind(this),
|
||||||
|
this.handleLassoEnd.bind(this)
|
||||||
);
|
);
|
||||||
this.setState({ svg: newSvg, brush });
|
this.setState({ svg: newSvg, brush });
|
||||||
}
|
}
|
||||||
@@ -251,54 +253,54 @@ class Graph extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invertPoint(pin) {
|
||||||
|
const { responsive } = this.props;
|
||||||
|
const { regl, camera, offset } = this.state;
|
||||||
|
|
||||||
|
const gl = regl._gl;
|
||||||
|
|
||||||
|
// get aspect ratio
|
||||||
|
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
|
||||||
|
|
||||||
|
// compute inverse view matrix
|
||||||
|
const inverse = mat4.invert([], camera.view());
|
||||||
|
|
||||||
|
// transform screen coordinates -> cell coordinates
|
||||||
|
const x = (2 * pin[0]) / (responsive.width - this.graphPaddingRight) - 1;
|
||||||
|
const y = 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
|
||||||
|
const pout = [
|
||||||
|
x * inverse[14] * aspect + inverse[12],
|
||||||
|
y * inverse[14] + inverse[13]
|
||||||
|
];
|
||||||
|
return [(pout[0] + 1) / 2 + offset[0], (pout[1] + 1) / 2 + offset[1]];
|
||||||
|
}
|
||||||
|
|
||||||
handleBrushSelectAction() {
|
handleBrushSelectAction() {
|
||||||
/*
|
/*
|
||||||
This conditional handles procedural brush deselect. Brush emits
|
This conditional handles procedural brush deselect. Brush emits
|
||||||
an event on procedural deselect because it is move: null
|
an event on procedural deselect because it is move: null
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
const { camera, offset } = this.state;
|
|
||||||
const { dispatch, responsive } = this.props;
|
|
||||||
|
|
||||||
if (d3.event.sourceEvent !== null) {
|
|
||||||
/*
|
|
||||||
No idea why d3 event scope works like this
|
|
||||||
but apparently
|
|
||||||
it does
|
|
||||||
https://bl.ocks.org/EfratVil/0e542f5fc426065dd1d4b6daaa345a9f
|
|
||||||
*/
|
|
||||||
const s = d3.event.selection;
|
|
||||||
const gl = this.state.regl._gl;
|
|
||||||
/*
|
|
||||||
event describing brush position:
|
event describing brush position:
|
||||||
@-------|
|
@-------|
|
||||||
| |
|
| |
|
||||||
| |
|
| |
|
||||||
|-------@
|
|-------@
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
No idea why d3 event scope works like this
|
||||||
|
but apparently
|
||||||
|
it does
|
||||||
|
https://bl.ocks.org/EfratVil/0e542f5fc426065dd1d4b6daaa345a9f
|
||||||
|
*/
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
// get aspect ratio
|
if (d3.event.sourceEvent !== null) {
|
||||||
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
|
const s = d3.event.selection;
|
||||||
|
|
||||||
// compute inverse view matrix
|
|
||||||
const inverse = mat4.invert([], camera.view());
|
|
||||||
|
|
||||||
// transform screen coordinates -> cell coordinates
|
|
||||||
const invert = pin => {
|
|
||||||
const x =
|
|
||||||
(2 * pin[0]) / (responsive.width - this.graphPaddingRight) - 1;
|
|
||||||
const y =
|
|
||||||
2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
|
|
||||||
const pout = [
|
|
||||||
x * inverse[14] * aspect + inverse[12],
|
|
||||||
y * inverse[14] + inverse[13]
|
|
||||||
];
|
|
||||||
return [(pout[0] + 1) / 2 + offset[0], (pout[1] + 1) / 2 + offset[1]];
|
|
||||||
};
|
|
||||||
|
|
||||||
const brushCoords = {
|
const brushCoords = {
|
||||||
northwest: invert([s[0][0], s[0][1]]),
|
northwest: this.invertPoint([s[0][0], s[0][1]]),
|
||||||
southeast: invert([s[1][0], s[1][1]])
|
southeast: this.invertPoint([s[1][0], s[1][1]])
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -330,6 +332,25 @@ class Graph extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLassoStart() {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
// reset selected points when starting a new polygon
|
||||||
|
// making it easier for the user to make the next selection
|
||||||
|
dispatch({
|
||||||
|
type: "lasso started"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// when a lasso is completed, filter to the points within the lasso polygon
|
||||||
|
handleLassoEnd(polygon) {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "lasso selection",
|
||||||
|
polygon: polygon.map(xy => this.invertPoint(xy)) // transform the polygon
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleOpacityRangeChange(e) {
|
handleOpacityRangeChange(e) {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -412,13 +433,18 @@ class Graph extends React.Component {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div>
|
<div>
|
||||||
<div className="bp3-button-group">
|
<div className="bp3-button-group">
|
||||||
<Tooltip content="Lasso cells" position="left">
|
<Tooltip content="Lasso selection" position="left">
|
||||||
<Button
|
<Button
|
||||||
className="bp3-button bp3-icon-select"
|
|
||||||
type="button"
|
type="button"
|
||||||
active={mode === "brush"}
|
className="bp3-button bp3-icon-polygon-filter"
|
||||||
|
active={mode === "lasso"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.setState({ mode: "brush" });
|
this.handleBrushDeselectAction();
|
||||||
|
// this.restartReglLoop();
|
||||||
|
this.setState({ mode: "lasso" });
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -451,7 +477,7 @@ class Graph extends React.Component {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: mode === "brush" ? "inherit" : "none"
|
display: mode === "lasso" ? "inherit" : "none"
|
||||||
}}
|
}}
|
||||||
id="graphAttachPoint"
|
id="graphAttachPoint"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// https://bl.ocks.org/pbeshai/8008075f9ce771ee8be39e8c38907570
|
||||||
|
|
||||||
|
import * as d3 from "d3";
|
||||||
|
|
||||||
|
const Lasso = () => {
|
||||||
|
const dispatch = d3.dispatch("start", "end");
|
||||||
|
|
||||||
|
const polygonToPath = polygon =>
|
||||||
|
`M${polygon.map(d => d.join(",")).join("L")}`;
|
||||||
|
|
||||||
|
const distance = (pt1, pt2) =>
|
||||||
|
Math.sqrt((pt2[0] - pt1[0]) ** 2 + (pt2[1] - pt1[1]) ** 2);
|
||||||
|
|
||||||
|
// distance last point has to be to first point before it auto closes when mouse is released
|
||||||
|
const closeDistance = 75;
|
||||||
|
|
||||||
|
const lasso = svg => {
|
||||||
|
let lassoPolygon;
|
||||||
|
let lassoPath;
|
||||||
|
let closePath;
|
||||||
|
|
||||||
|
const handleDragStart = () => {
|
||||||
|
lassoPolygon = [d3.mouse(svg.node())]; // current x y of mouse within element
|
||||||
|
|
||||||
|
if (lassoPath) {
|
||||||
|
lassoPath.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
lassoPath = g
|
||||||
|
.append("path")
|
||||||
|
.attr("fill", "#0bb")
|
||||||
|
.attr("fill-opacity", 0.1)
|
||||||
|
.attr("stroke", "#0bb")
|
||||||
|
.attr("stroke-dasharray", "3, 3");
|
||||||
|
|
||||||
|
closePath = g
|
||||||
|
.append("line")
|
||||||
|
.attr("x2", lassoPolygon[0][0])
|
||||||
|
.attr("y2", lassoPolygon[0][1])
|
||||||
|
.attr("stroke", "#0bb")
|
||||||
|
.attr("stroke-dasharray", "3, 3")
|
||||||
|
.attr("opacity", 0);
|
||||||
|
|
||||||
|
dispatch.call("start", lasso, lassoPolygon);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrag = () => {
|
||||||
|
const point = d3.mouse(svg.node());
|
||||||
|
lassoPolygon.push(point);
|
||||||
|
lassoPath.attr("d", polygonToPath(lassoPolygon));
|
||||||
|
|
||||||
|
// indicate if we are within closing distance
|
||||||
|
if (
|
||||||
|
distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <
|
||||||
|
closeDistance
|
||||||
|
) {
|
||||||
|
closePath
|
||||||
|
.attr("x1", point[0])
|
||||||
|
.attr("y1", point[1])
|
||||||
|
.attr("opacity", 1);
|
||||||
|
} else {
|
||||||
|
closePath.attr("opacity", 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
// remove the close path
|
||||||
|
closePath.remove();
|
||||||
|
closePath = null;
|
||||||
|
|
||||||
|
// succesfully closed
|
||||||
|
if (
|
||||||
|
distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <
|
||||||
|
closeDistance
|
||||||
|
) {
|
||||||
|
lassoPath.attr("d", `${polygonToPath(lassoPolygon)}Z`);
|
||||||
|
dispatch.call("end", lasso, lassoPolygon);
|
||||||
|
|
||||||
|
// otherwise cancel
|
||||||
|
} else {
|
||||||
|
lassoPath.remove();
|
||||||
|
lassoPath = null;
|
||||||
|
lassoPolygon = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// append a <g> with a rect
|
||||||
|
const g = svg.append("g").attr("class", "lasso-group");
|
||||||
|
const bbox = svg.node().getBoundingClientRect();
|
||||||
|
const area = g
|
||||||
|
.append("rect")
|
||||||
|
.attr("width", bbox.width)
|
||||||
|
.attr("height", bbox.height)
|
||||||
|
.attr("fill", "tomato")
|
||||||
|
.attr("opacity", 0);
|
||||||
|
|
||||||
|
const drag = d3
|
||||||
|
.drag()
|
||||||
|
.on("start", handleDragStart)
|
||||||
|
.on("drag", handleDrag)
|
||||||
|
.on("end", handleDragEnd);
|
||||||
|
|
||||||
|
area.call(drag);
|
||||||
|
|
||||||
|
lasso.reset = () => {
|
||||||
|
if (lassoPath) {
|
||||||
|
lassoPath.remove();
|
||||||
|
lassoPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
lassoPolygon = null;
|
||||||
|
if (closePath) {
|
||||||
|
closePath.remove();
|
||||||
|
closePath = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
lasso.on = (type, callback) => {
|
||||||
|
dispatch.on(type, callback);
|
||||||
|
return lasso;
|
||||||
|
};
|
||||||
|
|
||||||
|
return lasso;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Lasso;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import styles from "./graph.css";
|
import styles from "./graph.css";
|
||||||
|
import Lasso from "./setupLasso";
|
||||||
|
|
||||||
/******************************************
|
/******************************************
|
||||||
*******************************************
|
*******************************************
|
||||||
@@ -12,7 +13,9 @@ export default (
|
|||||||
handleBrushSelectAction,
|
handleBrushSelectAction,
|
||||||
handleBrushDeselectAction,
|
handleBrushDeselectAction,
|
||||||
responsive,
|
responsive,
|
||||||
graphPaddingRight
|
graphPaddingRight,
|
||||||
|
handleLassoStart,
|
||||||
|
handleLassoEnd
|
||||||
) => {
|
) => {
|
||||||
const svg = d3
|
const svg = d3
|
||||||
.select("#graphAttachPoint")
|
.select("#graphAttachPoint")
|
||||||
@@ -32,9 +35,16 @@ export default (
|
|||||||
.attr("class", "graph_brush")
|
.attr("class", "graph_brush")
|
||||||
.call(brush);
|
.call(brush);
|
||||||
|
|
||||||
|
const lassoInstance = Lasso()
|
||||||
|
.on("end", handleLassoEnd)
|
||||||
|
.on("start", handleLassoStart);
|
||||||
|
|
||||||
|
const lasso = svg.call(lassoInstance);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
svg,
|
svg,
|
||||||
brushContainer,
|
brushContainer,
|
||||||
brush
|
brush,
|
||||||
|
lasso
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+21
-10
@@ -1,6 +1,8 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { polygonContains } from "d3";
|
||||||
|
|
||||||
import { World, kvCache, WorldUtil } from "../util/stateManager";
|
import { World, kvCache, WorldUtil } from "../util/stateManager";
|
||||||
import parseRGB from "../util/parseRGB";
|
import parseRGB from "../util/parseRGB";
|
||||||
import Crossfilter from "../util/typedCrossfilter";
|
import Crossfilter from "../util/typedCrossfilter";
|
||||||
@@ -479,27 +481,36 @@ const Controls = (
|
|||||||
User Events
|
User Events
|
||||||
*******************************/
|
*******************************/
|
||||||
case "graph brush selection change": {
|
case "graph brush selection change": {
|
||||||
state.dimensionMap[layoutDimensionName("X")].filterRange([
|
state.dimensionMap[layoutDimensionName("XY")].filterWithinRect(
|
||||||
action.brushCoords.northwest[0],
|
action.brushCoords.northwest,
|
||||||
action.brushCoords.southeast[0]
|
action.brushCoords.southeast
|
||||||
]);
|
);
|
||||||
state.dimensionMap[layoutDimensionName("Y")].filterRange([
|
|
||||||
action.brushCoords.southeast[1],
|
|
||||||
action.brushCoords.northwest[1]
|
|
||||||
]);
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graphBrushSelection: action.brushCoords
|
graphBrushSelection: action.brushCoords
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "lasso deselect":
|
||||||
case "graph brush deselect": {
|
case "graph brush deselect": {
|
||||||
state.dimensionMap[layoutDimensionName("X")].filterAll();
|
state.dimensionMap[layoutDimensionName("XY")].filterAll();
|
||||||
state.dimensionMap[layoutDimensionName("Y")].filterAll();
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graphBrushSelection: null
|
graphBrushSelection: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "lasso selection": {
|
||||||
|
const { polygon } = action;
|
||||||
|
const dXY = state.dimensionMap[layoutDimensionName("XY")];
|
||||||
|
if (polygon.length < 3) {
|
||||||
|
// single point or a line is not a polygon, and is therefore a deselect
|
||||||
|
dXY.filterAll();
|
||||||
|
} else {
|
||||||
|
dXY.filterWithinPolygon(polygon);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state
|
||||||
|
};
|
||||||
|
}
|
||||||
case "continuous metadata histogram brush": {
|
case "continuous metadata histogram brush": {
|
||||||
const name = makeContinuousDimensionName(
|
const name = makeContinuousDimensionName(
|
||||||
action.continuousNamespace,
|
action.continuousNamespace,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import _ from "lodash";
|
|||||||
import * as kvCache from "./keyvalcache";
|
import * as kvCache from "./keyvalcache";
|
||||||
import summarizeAnnotations from "./summarizeAnnotations";
|
import summarizeAnnotations from "./summarizeAnnotations";
|
||||||
import { layoutDimensionName, obsAnnoDimensionName } from "../nameCreators";
|
import { layoutDimensionName, obsAnnoDimensionName } from "../nameCreators";
|
||||||
|
import Crossfilter from "../typedCrossfilter";
|
||||||
import { sliceByIndex } from "../typedCrossfilter/util";
|
import { sliceByIndex } from "../typedCrossfilter/util";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -231,7 +232,12 @@ export function createVarDimension(
|
|||||||
crossfilter,
|
crossfilter,
|
||||||
geneName
|
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) {
|
export function createObsDimensionMap(crossfilter, world) {
|
||||||
@@ -246,9 +252,14 @@ export function createObsDimensionMap(crossfilter, world) {
|
|||||||
.filter(anno => anno.name !== "name")
|
.filter(anno => anno.name !== "name")
|
||||||
.transform((result, anno) => {
|
.transform((result, anno) => {
|
||||||
const dimType = deduceDimensionType(anno, anno.name);
|
const dimType = deduceDimensionType(anno, anno.name);
|
||||||
// XXX if dimtype is a scalar, we may be able to do better?
|
if (dimType === "enum") {
|
||||||
if (dimType) {
|
|
||||||
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
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],
|
r => r[anno.name],
|
||||||
dimType
|
dimType
|
||||||
);
|
);
|
||||||
@@ -259,13 +270,10 @@ export function createObsDimensionMap(crossfilter, world) {
|
|||||||
/*
|
/*
|
||||||
Add crossfilter dimensions allowing filtering on layout
|
Add crossfilter dimensions allowing filtering on layout
|
||||||
*/
|
*/
|
||||||
dimensionMap[layoutDimensionName("X")] = crossfilter.dimension(
|
dimensionMap[layoutDimensionName("XY")] = crossfilter.dimension(
|
||||||
|
Crossfilter.SpatialDimension,
|
||||||
obsLayout.X,
|
obsLayout.X,
|
||||||
Float32Array
|
obsLayout.Y
|
||||||
);
|
|
||||||
dimensionMap[layoutDimensionName("Y")] = crossfilter.dimension(
|
|
||||||
obsLayout.Y,
|
|
||||||
Float32Array
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return dimensionMap;
|
return dimensionMap;
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ more complex API. In a few cases, elements of that API were incorporated.
|
|||||||
https://github.com/square/crossfilter/
|
https://github.com/square/crossfilter/
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
// XXX replace
|
||||||
|
import { polygonContains } from "d3";
|
||||||
|
|
||||||
import PositiveIntervals from "./positiveIntervals";
|
import PositiveIntervals from "./positiveIntervals";
|
||||||
import BitArray from "./bitArray";
|
import BitArray from "./bitArray";
|
||||||
@@ -66,14 +68,22 @@ class TypedCrossfilter {
|
|||||||
return this.data;
|
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();
|
const id = this.selection.allocDimension();
|
||||||
let dim;
|
const dim = new DimensionType(this, id, ...rest);
|
||||||
if (valueArrayType === "enum") {
|
|
||||||
dim = new EnumDimension(value, this, id);
|
|
||||||
} else {
|
|
||||||
dim = new ScalarDimension(value, valueArrayType, this, id);
|
|
||||||
}
|
|
||||||
this.filters.push({ id, dim });
|
this.filters.push({ id, dim });
|
||||||
dim.filterAll();
|
dim.filterAll();
|
||||||
return dim;
|
return dim;
|
||||||
@@ -115,13 +125,34 @@ class TypedCrossfilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base dimension type - value must be a scalar type (eg, int, float),
|
// Base dimension type - not exported.
|
||||||
// and value array must be a TypedArray.
|
class _Dimension {
|
||||||
//
|
constructor(xfltr, id) {
|
||||||
class ScalarDimension {
|
|
||||||
constructor(value, ValueArrayType, xfltr, id) {
|
|
||||||
this.crossfilter = xfltr;
|
this.crossfilter = xfltr;
|
||||||
this._id = id;
|
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.
|
// current selection filter, expressed as PostiveIntervals.
|
||||||
this.currentFilter = [];
|
this.currentFilter = [];
|
||||||
@@ -151,9 +182,6 @@ class ScalarDimension {
|
|||||||
|
|
||||||
// create sort index
|
// create sort index
|
||||||
this.index = makeSortIndex(array);
|
this.index = makeSortIndex(array);
|
||||||
|
|
||||||
// groups, if any
|
|
||||||
this.groups = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_createValueArray(value, array) {
|
_createValueArray(value, array) {
|
||||||
@@ -167,15 +195,6 @@ class ScalarDimension {
|
|||||||
return larray;
|
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
|
// Argument is an array of intervals indicating records newly selected/filtered
|
||||||
//
|
//
|
||||||
_updateFilters(newFilter) {
|
_updateFilters(newFilter) {
|
||||||
@@ -209,7 +228,7 @@ class ScalarDimension {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.currentFilter = cNewFilter;
|
this.currentFilter = cNewFilter;
|
||||||
this.crossfilter.updateTime += 1;
|
this._filterUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter by value - exact match
|
// filter by value - exact match
|
||||||
@@ -355,8 +374,8 @@ class ScalarDimension {
|
|||||||
// strings, which can be mapped into an fixed numeric range [0..n).
|
// strings, which can be mapped into an fixed numeric range [0..n).
|
||||||
//
|
//
|
||||||
class EnumDimension extends ScalarDimension {
|
class EnumDimension extends ScalarDimension {
|
||||||
constructor(value, xfltr, id) {
|
constructor(xfltr, id, value) {
|
||||||
super(value, Uint32Array, xfltr, id);
|
super(xfltr, id, value, Uint32Array);
|
||||||
}
|
}
|
||||||
|
|
||||||
_createValueArray(value, array) {
|
_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
|
// Groups! Map/reduce
|
||||||
//
|
//
|
||||||
class ScalarGroup {
|
class ScalarGroup {
|
||||||
@@ -611,5 +751,6 @@ crossfilter.BitArray = BitArray;
|
|||||||
crossfilter.TypedCrossfilter = TypedCrossfilter;
|
crossfilter.TypedCrossfilter = TypedCrossfilter;
|
||||||
crossfilter.ScalarDimension = ScalarDimension;
|
crossfilter.ScalarDimension = ScalarDimension;
|
||||||
crossfilter.EnumDimension = EnumDimension;
|
crossfilter.EnumDimension = EnumDimension;
|
||||||
|
crossfilter.SpatialDimension = SpatialDimension;
|
||||||
|
|
||||||
export default crossfilter;
|
export default crossfilter;
|
||||||
|
|||||||
Reference in New Issue
Block a user