* 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:
Colin Megill
2019-02-08 11:48:51 -08:00
committed by Charlotte Weaver
parent 2e9525741f
commit dbb3a309a9
8 changed files with 564 additions and 146 deletions
+69 -43
View File
@@ -40,7 +40,7 @@ class Graph extends React.Component {
this.state = {
svg: null,
brush: null,
mode: "brush"
mode: "lasso"
};
}
@@ -202,7 +202,9 @@ class Graph extends React.Component {
this.handleBrushSelectAction.bind(this),
this.handleBrushDeselectAction.bind(this),
responsive,
this.graphPaddingRight
this.graphPaddingRight,
this.handleLassoStart.bind(this),
this.handleLassoEnd.bind(this)
);
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() {
/*
This conditional handles procedural brush deselect. Brush emits
an event on procedural deselect because it is move: null
This conditional handles procedural brush deselect. Brush emits
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:
@-------|
| |
| |
|-------@
*/
/*
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
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
// 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]];
};
if (d3.event.sourceEvent !== null) {
const s = d3.event.selection;
const brushCoords = {
northwest: invert([s[0][0], s[0][1]]),
southeast: invert([s[1][0], s[1][1]])
northwest: this.invertPoint([s[0][0], s[0][1]]),
southeast: this.invertPoint([s[1][0], s[1][1]])
};
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) {
const { dispatch } = this.props;
dispatch({
@@ -412,13 +433,18 @@ class Graph extends React.Component {
</Tooltip>
<div>
<div className="bp3-button-group">
<Tooltip content="Lasso cells" position="left">
<Tooltip content="Lasso selection" position="left">
<Button
className="bp3-button bp3-icon-select"
type="button"
active={mode === "brush"}
className="bp3-button bp3-icon-polygon-filter"
active={mode === "lasso"}
onClick={() => {
this.setState({ mode: "brush" });
this.handleBrushDeselectAction();
// this.restartReglLoop();
this.setState({ mode: "lasso" });
}}
style={{
cursor: "pointer"
}}
/>
</Tooltip>
@@ -451,7 +477,7 @@ class Graph extends React.Component {
>
<div
style={{
display: mode === "brush" ? "inherit" : "none"
display: mode === "lasso" ? "inherit" : "none"
}}
id="graphAttachPoint"
/>
+127
View File
@@ -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
import * as d3 from "d3";
import styles from "./graph.css";
import Lasso from "./setupLasso";
/******************************************
*******************************************
@@ -12,7 +13,9 @@ export default (
handleBrushSelectAction,
handleBrushDeselectAction,
responsive,
graphPaddingRight
graphPaddingRight,
handleLassoStart,
handleLassoEnd
) => {
const svg = d3
.select("#graphAttachPoint")
@@ -32,9 +35,16 @@ export default (
.attr("class", "graph_brush")
.call(brush);
const lassoInstance = Lasso()
.on("end", handleLassoEnd)
.on("start", handleLassoStart);
const lasso = svg.call(lassoInstance);
return {
svg,
brushContainer,
brush
brush,
lasso
};
};