mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-02 21:48:12 +08:00
coordinate system fixes for embedded graph (#768)
* change pan speed to 1 per issue #722 * correct handle scaling of graph when aspect ratio less than one * add package lock * add invert to our scale functions * correctly transform to/from gl coordinates * remove unused import * fix naming of import * update smoke tests
This commit is contained in:
@@ -26,8 +26,8 @@ export const datasets = {
|
|||||||
cellsets: {
|
cellsets: {
|
||||||
lasso: [
|
lasso: [
|
||||||
{
|
{
|
||||||
"coordinates-as-percent": { x1: 0.25, y1: 0.25, x2: 0.35, y2: 0.35 },
|
"coordinates-as-percent": { x1: 0.05, y1: 0.25, x2: 0.15, y2: 0.35 },
|
||||||
count: "26"
|
count: "43"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
categorical: [
|
categorical: [
|
||||||
@@ -91,8 +91,8 @@ export const datasets = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
lasso: {
|
lasso: {
|
||||||
"coordinates-as-percent": { x1: 0.45, y1: 0.45, x2: 0.5, y2: 0.5 },
|
"coordinates-as-percent": { x1: 0.45, y1: 0.05, x2: 0.5, y2: 0.1 },
|
||||||
count: "67"
|
count: "65"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scatter: {
|
scatter: {
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ describe("scatter plot", async () => {
|
|||||||
|
|
||||||
describe("clipping", async () => {
|
describe("clipping", async () => {
|
||||||
test("clip continuous", async () => {
|
test("clip continuous", async () => {
|
||||||
await cxgActions.clip(data.clip.min, data.clip.max)
|
await cxgActions.clip(data.clip.min, data.clip.max);
|
||||||
const histId = `histogram-${data.clip.metadata}-plot-brush`;
|
const histId = `histogram-${data.clip.metadata}-plot-brush`;
|
||||||
const coords = await cxgActions.calcDragCoordinates(
|
const coords = await cxgActions.calcDragCoordinates(
|
||||||
histId,
|
histId,
|
||||||
@@ -281,16 +281,13 @@ describe("clipping", async () => {
|
|||||||
await cxgActions.drag(histId, coords.start, coords.end);
|
await cxgActions.drag(histId, coords.start, coords.end);
|
||||||
const cellCount = await cxgActions.cellSet(1);
|
const cellCount = await cxgActions.cellSet(1);
|
||||||
expect(cellCount).toBe(data.clip.count);
|
expect(cellCount).toBe(data.clip.count);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("clip gene", async () => {
|
test("clip gene", async () => {
|
||||||
await utils.typeInto("gene-search", data.clip.gene);
|
await utils.typeInto("gene-search", data.clip.gene);
|
||||||
await page.keyboard.press("Enter");
|
await page.keyboard.press("Enter");
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(`[data-testid='histogram-${data.clip.gene}']`);
|
||||||
`[data-testid='histogram-${data.clip.gene}']`
|
await cxgActions.clip(data.clip.min, data.clip.max);
|
||||||
);
|
|
||||||
await cxgActions.clip(data.clip.min, data.clip.max)
|
|
||||||
const histId = `histogram-${data.clip.gene}-plot-brush`;
|
const histId = `histogram-${data.clip.gene}-plot-brush`;
|
||||||
const coords = await cxgActions.calcDragCoordinates(
|
const coords = await cxgActions.calcDragCoordinates(
|
||||||
histId,
|
histId,
|
||||||
|
|||||||
Generated
+5
@@ -8324,6 +8324,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.0.0.tgz",
|
||||||
"integrity": "sha512-PD4mVH/C/Zs64kOozeFnKY8ybhgwxXXQYGWdB4h68krAHknWJgk9uKOn6z8YElh5//vs++90pb6csrTIDWnexA=="
|
"integrity": "sha512-PD4mVH/C/Zs64kOozeFnKY8ybhgwxXXQYGWdB4h68krAHknWJgk9uKOn6z8YElh5//vs++90pb6csrTIDWnexA=="
|
||||||
},
|
},
|
||||||
|
"gl-vec3": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/gl-vec3/-/gl-vec3-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-jduKUqT0SGH02l8Yl+mV1yVsDfYgQAJyXGxkJQGyxPLHRiW25DwVIRPt6uvhrEMHftJfqhqKthRcyZqNEl9Xdw=="
|
||||||
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"font-color-contrast": "^1.0.3",
|
"font-color-contrast": "^1.0.3",
|
||||||
"fuzzysort": "^1.1.4",
|
"fuzzysort": "^1.1.4",
|
||||||
"gl-mat4": "^1.1.4",
|
"gl-mat4": "^1.1.4",
|
||||||
|
"gl-vec3": "^1.1.3",
|
||||||
"gl-matrix": "^3.0.0",
|
"gl-matrix": "^3.0.0",
|
||||||
"is-number": "^7.0.0",
|
"is-number": "^7.0.0",
|
||||||
"key-pressed": "0.0.1",
|
"key-pressed": "0.0.1",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
const mat4 = require("gl-mat4");
|
const mat4 = require("gl-mat4");
|
||||||
|
const vec3 = require("gl-vec3");
|
||||||
|
|
||||||
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
|
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
|
||||||
|
|
||||||
@@ -38,7 +39,20 @@ export default function(regl) {
|
|||||||
uniforms: {
|
uniforms: {
|
||||||
distance: regl.prop("distance"),
|
distance: regl.prop("distance"),
|
||||||
view: regl.prop("view"),
|
view: regl.prop("view"),
|
||||||
projection: ({viewportWidth, viewportHeight}) => mat4.perspective([], Math.PI / 2, viewportWidth / viewportHeight, 0.01, 1000)
|
projection: ({ viewportWidth, viewportHeight }) => {
|
||||||
|
const aspectRatio = viewportWidth / viewportHeight;
|
||||||
|
let m = mat4.perspective(
|
||||||
|
[],
|
||||||
|
Math.PI / 2,
|
||||||
|
viewportWidth / viewportHeight,
|
||||||
|
0.01,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
if (aspectRatio < 1) {
|
||||||
|
m = mat4.scale(m, m, vec3.fromValues(1, 1, 1 / aspectRatio));
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
count: regl.prop("count"),
|
count: regl.prop("count"),
|
||||||
|
|||||||
@@ -175,18 +175,19 @@ class Graph extends React.Component {
|
|||||||
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
||||||
const glScaleY = scaleLinear([0, 1], [1, -1]);
|
const glScaleY = scaleLinear([0, 1], [1, -1]);
|
||||||
|
|
||||||
const offset = [d3.mean(X) - 0.5, d3.mean(Y) - 0.5];
|
|
||||||
|
|
||||||
for (let i = 0, { positions } = renderCache; i < nObs; i += 1) {
|
for (let i = 0, { positions } = renderCache; i < nObs; i += 1) {
|
||||||
positions[2 * i] = glScaleX(X[i] - offset[0]);
|
positions[2 * i] = glScaleX(X[i]);
|
||||||
positions[2 * i + 1] = glScaleY(Y[i] - offset[1]);
|
positions[2 * i + 1] = glScaleY(Y[i]);
|
||||||
}
|
}
|
||||||
pointBuffer({
|
pointBuffer({
|
||||||
data: renderCache.positions,
|
data: renderCache.positions,
|
||||||
dimension: 2
|
dimension: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
stateChanges.offset = offset;
|
stateChanges.transform = {
|
||||||
|
glScaleX,
|
||||||
|
glScaleY
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colors for each point - a cached value that only changes when
|
// Colors for each point - a cached value that only changes when
|
||||||
@@ -271,11 +272,11 @@ class Graph extends React.Component {
|
|||||||
mode !== prevState.mode ||
|
mode !== prevState.mode ||
|
||||||
stateChanges.svg
|
stateChanges.svg
|
||||||
) {
|
) {
|
||||||
const { tool, container, offset } = this.state;
|
const { tool, container, transform } = this.state;
|
||||||
this.selectionToolUpdate(
|
this.selectionToolUpdate(
|
||||||
stateChanges.tool ? stateChanges.tool : tool,
|
stateChanges.tool ? stateChanges.tool : tool,
|
||||||
stateChanges.container ? stateChanges.container : container,
|
stateChanges.container ? stateChanges.container : container,
|
||||||
stateChanges.offset ? stateChanges.offset : offset
|
stateChanges.transform ? stateChanges.transform : transform
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,7 +436,7 @@ class Graph extends React.Component {
|
|||||||
this.setState({ pendingClipPercentiles: null });
|
this.setState({ pendingClipPercentiles: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
brushToolUpdate(tool, container, offset) {
|
brushToolUpdate(tool, container, transform) {
|
||||||
/*
|
/*
|
||||||
this is called from componentDidUpdate(), so be very careful using
|
this is called from componentDidUpdate(), so be very careful using
|
||||||
anything from this.state, which may be updated asynchronously.
|
anything from this.state, which may be updated asynchronously.
|
||||||
@@ -449,8 +450,14 @@ class Graph extends React.Component {
|
|||||||
if there is a selection, make sure the brush tool matches
|
if there is a selection, make sure the brush tool matches
|
||||||
*/
|
*/
|
||||||
const screenCoords = [
|
const screenCoords = [
|
||||||
this.mapPointToScreen(currentSelection.brushCoords.northwest, offset),
|
this.mapPointToScreen(
|
||||||
this.mapPointToScreen(currentSelection.brushCoords.southeast, offset)
|
currentSelection.brushCoords.northwest,
|
||||||
|
transform
|
||||||
|
),
|
||||||
|
this.mapPointToScreen(
|
||||||
|
currentSelection.brushCoords.southeast,
|
||||||
|
transform
|
||||||
|
)
|
||||||
];
|
];
|
||||||
if (!toolCurrentSelection) {
|
if (!toolCurrentSelection) {
|
||||||
/* tool is not selected, so just move the brush */
|
/* tool is not selected, so just move the brush */
|
||||||
@@ -477,7 +484,7 @@ class Graph extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lassoToolUpdate(tool, container, offset) {
|
lassoToolUpdate(tool, container, transform) {
|
||||||
/*
|
/*
|
||||||
this is called from componentDidUpdate(), so be very careful using
|
this is called from componentDidUpdate(), so be very careful using
|
||||||
anything from this.state, which may be updated asynchronously.
|
anything from this.state, which may be updated asynchronously.
|
||||||
@@ -488,7 +495,7 @@ class Graph extends React.Component {
|
|||||||
if there is a current selection, make sure the lasso tool matches
|
if there is a current selection, make sure the lasso tool matches
|
||||||
*/
|
*/
|
||||||
const polygon = currentSelection.polygon.map(p =>
|
const polygon = currentSelection.polygon.map(p =>
|
||||||
this.mapPointToScreen(p, offset)
|
this.mapPointToScreen(p, transform)
|
||||||
);
|
);
|
||||||
tool.move(polygon);
|
tool.move(polygon);
|
||||||
} else {
|
} else {
|
||||||
@@ -496,7 +503,7 @@ class Graph extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectionToolUpdate(tool, container, offset) {
|
selectionToolUpdate(tool, container, transform) {
|
||||||
/*
|
/*
|
||||||
this is called from componentDidUpdate(), so be very careful using
|
this is called from componentDidUpdate(), so be very careful using
|
||||||
anything from this.state, which may be updated asynchronously.
|
anything from this.state, which may be updated asynchronously.
|
||||||
@@ -504,10 +511,10 @@ class Graph extends React.Component {
|
|||||||
const { selectionTool } = this.props;
|
const { selectionTool } = this.props;
|
||||||
switch (selectionTool) {
|
switch (selectionTool) {
|
||||||
case "brush":
|
case "brush":
|
||||||
this.brushToolUpdate(tool, container, offset);
|
this.brushToolUpdate(tool, container, transform);
|
||||||
break;
|
break;
|
||||||
case "lasso":
|
case "lasso":
|
||||||
this.lassoToolUpdate(tool, container, offset);
|
this.lassoToolUpdate(tool, container, transform);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
/* punt? */
|
/* punt? */
|
||||||
@@ -564,7 +571,8 @@ class Graph extends React.Component {
|
|||||||
accounting for current pan/zoom camera.
|
accounting for current pan/zoom camera.
|
||||||
*/
|
*/
|
||||||
const { responsive } = this.props;
|
const { responsive } = this.props;
|
||||||
const { regl, camera, offset } = this.state;
|
const { regl, camera, transform } = this.state;
|
||||||
|
const { glScaleX, glScaleY } = transform;
|
||||||
|
|
||||||
const gl = regl._gl;
|
const gl = regl._gl;
|
||||||
|
|
||||||
@@ -579,19 +587,20 @@ class Graph extends React.Component {
|
|||||||
const y = 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
|
const y = 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
|
||||||
const pout = [
|
const pout = [
|
||||||
x * inverse[14] * aspect + inverse[12],
|
x * inverse[14] * aspect + inverse[12],
|
||||||
y * inverse[14] + inverse[13]
|
-(y * inverse[14] + inverse[13])
|
||||||
];
|
];
|
||||||
|
|
||||||
return [(pout[0] + 1) / 2 + offset[0], (pout[1] + 1) / 2 + offset[1]];
|
return [glScaleX.invert(pout[0]), glScaleY.invert(pout[1])];
|
||||||
}
|
}
|
||||||
|
|
||||||
mapPointToScreen(xyCell, offset) {
|
mapPointToScreen(xyCell, transform) {
|
||||||
/*
|
/*
|
||||||
Map an XY coordinate from cell/point domain to screen range. Inverse
|
Map an XY coordinate from cell/point domain to screen range. Inverse
|
||||||
of mapScreenToPoint()
|
of mapScreenToPoint()
|
||||||
*/
|
*/
|
||||||
const { responsive } = this.props;
|
const { responsive } = this.props;
|
||||||
const { regl, camera } = this.state;
|
const { regl, camera } = this.state;
|
||||||
|
const { glScaleX, glScaleY } = transform;
|
||||||
|
|
||||||
const gl = regl._gl;
|
const gl = regl._gl;
|
||||||
|
|
||||||
@@ -603,12 +612,9 @@ class Graph extends React.Component {
|
|||||||
|
|
||||||
// variable names are choosen to reflect inverse of those used
|
// variable names are choosen to reflect inverse of those used
|
||||||
// in mapScreenToPoint().
|
// in mapScreenToPoint().
|
||||||
const pout = [
|
const pout = [glScaleX(xyCell[0]), glScaleY(xyCell[1])];
|
||||||
(xyCell[0] - offset[0]) * 2 - 1,
|
|
||||||
(xyCell[1] - offset[1]) * 2 - 1
|
|
||||||
];
|
|
||||||
const x = (pout[0] - inverse[12]) / aspect / inverse[14];
|
const x = (pout[0] - inverse[12]) / aspect / inverse[14];
|
||||||
const y = (pout[1] - inverse[13]) / inverse[14];
|
const y = (-pout[1] - inverse[13]) / inverse[14];
|
||||||
|
|
||||||
const pin = [
|
const pin = [
|
||||||
Math.round(((x + 1) * (responsive.width - this.graphPaddingRight)) / 2),
|
Math.round(((x + 1) * (responsive.width - this.graphPaddingRight)) / 2),
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ const mp = require("mouse-position");
|
|||||||
const mb = require("mouse-pressed");
|
const mb = require("mouse-pressed");
|
||||||
const key = require("key-pressed");
|
const key = require("key-pressed");
|
||||||
|
|
||||||
const panSpeed = 0.4;
|
const panSpeed = 1.0; // changed from 0.4 to 1.0 per issue #722
|
||||||
const scaleSpeed = 0.5;
|
const scaleSpeed = 0.5;
|
||||||
const scaleMax = 3;
|
const scaleMax = 3;
|
||||||
// const scaleMin = 1.15
|
|
||||||
const scaleMin = 1.03;
|
const scaleMin = 1.03;
|
||||||
|
|
||||||
function attachCamera(canvas, opts) {
|
function attachCamera(canvas, opts) {
|
||||||
|
|||||||
@@ -9,8 +9,14 @@
|
|||||||
// this is is equivalent to d3.scaleLinear().domain([0,1]).range([-1,1])
|
// this is is equivalent to d3.scaleLinear().domain([0,1]).range([-1,1])
|
||||||
|
|
||||||
export default (domain, range) => {
|
export default (domain, range) => {
|
||||||
const domainStart = domain[0];
|
const domainStart = domain[0];
|
||||||
const scale = (range[1] - range[0]) / (domain[1] - domain[0]);
|
const scale = (range[1] - range[0]) / (domain[1] - domain[0]);
|
||||||
const rangeStart = range[0];
|
const invScale = 1 / scale;
|
||||||
return value => (value - domainStart) * scale + rangeStart;
|
const rangeStart = range[0];
|
||||||
|
const f = value => (value - domainStart) * scale + rangeStart;
|
||||||
|
|
||||||
|
// inverter
|
||||||
|
f.invert = value => (value - rangeStart) * invScale + domainStart;
|
||||||
|
|
||||||
|
return f;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user