mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-07 14:28:12 +08:00
Parametrization
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
import { loadUserColorConfig } from "../util/stateManager/colorHelpers";
|
import { loadUserColorConfig } from "../util/stateManager/colorHelpers";
|
||||||
import * as selnActions from "./selection";
|
import * as selnActions from "./selection";
|
||||||
import * as annoActions from "./annotation";
|
import * as annoActions from "./annotation";
|
||||||
|
import * as spatialActions from "./spatial";
|
||||||
import * as viewActions from "./viewStack";
|
import * as viewActions from "./viewStack";
|
||||||
import * as embActions from "./embedding";
|
import * as embActions from "./embedding";
|
||||||
import * as genesetActions from "./geneset";
|
import * as genesetActions from "./geneset";
|
||||||
@@ -272,4 +273,5 @@ export default {
|
|||||||
genesetDelete: genesetActions.genesetDelete,
|
genesetDelete: genesetActions.genesetDelete,
|
||||||
genesetAddGenes: genesetActions.genesetAddGenes,
|
genesetAddGenes: genesetActions.genesetAddGenes,
|
||||||
genesetDeleteGenes: genesetActions.genesetDeleteGenes,
|
genesetDeleteGenes: genesetActions.genesetDeleteGenes,
|
||||||
|
requestSpatialMetadata: spatialActions.requestSpatialMetadata,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import * as globals from "../globals";
|
||||||
|
|
||||||
|
export const requestSpatialMetadata = () => async (dispatch) => {
|
||||||
|
dispatch({ type: "request spatial metadata started" });
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${globals.API.prefix}${globals.API.version}spatial/meta`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}),
|
||||||
|
credentials: "include",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
|
||||||
|
return null; // TODO need a dispatch //dispatchDiffExpErrors(dispatch, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await res.json();
|
||||||
|
|
||||||
|
/* then send the success case action through */
|
||||||
|
return dispatch({
|
||||||
|
type: "request spatial metadata success",
|
||||||
|
data: response,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return dispatch({
|
||||||
|
type: "request spatial metadata error",
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -23,6 +23,8 @@ class App extends React.Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
dispatch(actions.requestSpatialMetadata());
|
||||||
|
|
||||||
/* listen for url changes, fire one when we start the app up */
|
/* listen for url changes, fire one when we start the app up */
|
||||||
window.addEventListener("popstate", this._onURLChanged);
|
window.addEventListener("popstate", this._onURLChanged);
|
||||||
this._onURLChanged();
|
this._onURLChanged();
|
||||||
|
|||||||
@@ -45,35 +45,15 @@ export default function drawSpatialImageRegl(regl) {
|
|||||||
|
|
||||||
attributes: {
|
attributes: {
|
||||||
a_texCoord: [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0],
|
a_texCoord: [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0],
|
||||||
// a_position: [
|
a_position: regl.prop("rectCoords"),
|
||||||
// 10, 0,
|
|
||||||
// 10 + regl.prop("img_width"), 0,
|
|
||||||
// 10, 0 + regl.prop("img_height"),
|
|
||||||
// 10, 0 + regl.prop("img_height"),
|
|
||||||
// 10 + regl.prop("img_width"), 0,
|
|
||||||
// 10 + regl.prop("img_width"), 0 + regl.prop("img_height"),
|
|
||||||
// ],
|
|
||||||
a_position: [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0 + 1921,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0 + 2000,
|
|
||||||
0,
|
|
||||||
0 + 2000,
|
|
||||||
0 + 1921,
|
|
||||||
0,
|
|
||||||
0 + 1921,
|
|
||||||
0 + 2000,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
uniforms: {
|
uniforms: {
|
||||||
projView: regl.prop("projView"),
|
projView: regl.prop("projView"),
|
||||||
u_image: regl.prop("spatialImageAsTexture"),
|
u_image: regl.prop("spatialImageAsTexture"),
|
||||||
color: [1, 0, 0, 1],
|
color: [1, 0, 0, 1],
|
||||||
u_resolution: [1921, 2000],
|
u_resolution: [regl.prop("imageWidth"), regl.prop("imageHeight")],
|
||||||
|
image_width: regl.prop("imageWidth"),
|
||||||
// translate:
|
// translate:
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ function createModelTF() {
|
|||||||
colors: state.colors,
|
colors: state.colors,
|
||||||
pointDilation: state.pointDilation,
|
pointDilation: state.pointDilation,
|
||||||
genesets: state.genesets.genesets,
|
genesets: state.genesets.genesets,
|
||||||
|
spatial: state.spatial.metadata,
|
||||||
}))
|
}))
|
||||||
class Graph extends React.Component {
|
class Graph extends React.Component {
|
||||||
static createReglState(canvas) {
|
static createReglState(canvas) {
|
||||||
@@ -110,19 +111,20 @@ class Graph extends React.Component {
|
|||||||
return !shallowEqual(props.watchProps, prevProps.watchProps);
|
return !shallowEqual(props.watchProps, prevProps.watchProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
computePointPositions = memoize((X, Y, modelTF) => {
|
computePointPositions = memoize((X, Y, modelTF, spatialMetadata) => {
|
||||||
/*
|
/*
|
||||||
compute the model coordinate for each point
|
compute the model coordinate for each point
|
||||||
*/
|
*/
|
||||||
console.log({ X }, { Y });
|
console.log({ X }, { Y });
|
||||||
const positions = new Float32Array(2 * X.length);
|
const positions = new Float32Array(2 * X.length);
|
||||||
for (let i = 0, len = X.length; i < len; i += 1) {
|
for (let i = 0, len = X.length; i < len; i += 1) {
|
||||||
const p = vec2.fromValues(X[i], Y[i]);
|
// TODO: Introduce the feature flag here
|
||||||
|
// const p = vec2.fromValues(X[i], Y[i]);
|
||||||
|
const p = this.rescalePointForSpatial(X[i], Y[i], spatialMetadata);
|
||||||
vec2.transformMat3(p, p, modelTF);
|
vec2.transformMat3(p, p, modelTF);
|
||||||
positions[2 * i] = p[0];
|
positions[2 * i] = p[0];
|
||||||
positions[2 * i + 1] = p[1];
|
positions[2 * i + 1] = p[1];
|
||||||
}
|
}
|
||||||
console.log({ transformed: positions });
|
|
||||||
return positions;
|
return positions;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,6 +240,7 @@ class Graph extends React.Component {
|
|||||||
colorBuffer: null,
|
colorBuffer: null,
|
||||||
flagBuffer: null,
|
flagBuffer: null,
|
||||||
drawSpatialImage: null,
|
drawSpatialImage: null,
|
||||||
|
spatial: null,
|
||||||
|
|
||||||
// component rendering derived state - these must stay synchronized
|
// component rendering derived state - these must stay synchronized
|
||||||
// with the reducer state they were generated from.
|
// with the reducer state they were generated from.
|
||||||
@@ -452,6 +455,33 @@ class Graph extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rescalePointForSpatial = (x, y, spatialMetadata) => {
|
||||||
|
// console.log({spatialMetadata});
|
||||||
|
const translate = vec2.fromValues(
|
||||||
|
spatialMetadata.inverseTranslate[0],
|
||||||
|
spatialMetadata.inverseTranslate[1]
|
||||||
|
);
|
||||||
|
const min = vec2.fromValues(
|
||||||
|
spatialMetadata.inverseMin[0],
|
||||||
|
spatialMetadata.inverseMin[1]
|
||||||
|
);
|
||||||
|
const scalefactor = spatialMetadata.scaleref;
|
||||||
|
const wh = vec2.fromValues(
|
||||||
|
spatialMetadata.imageWidth,
|
||||||
|
spatialMetadata.imageHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply the inverse transform
|
||||||
|
const p = vec2.fromValues(x, y);
|
||||||
|
vec2.sub(p, p, translate);
|
||||||
|
vec2.scale(p, p, spatialMetadata.inverseScale);
|
||||||
|
vec2.add(p, p, min);
|
||||||
|
|
||||||
|
vec2.scale(p, p, scalefactor);
|
||||||
|
vec2.div(p, p, wh);
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
setReglCanvas = (canvas) => {
|
setReglCanvas = (canvas) => {
|
||||||
this.reglCanvas = canvas;
|
this.reglCanvas = canvas;
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -534,9 +564,12 @@ class Graph extends React.Component {
|
|||||||
crossfilter,
|
crossfilter,
|
||||||
pointDilation,
|
pointDilation,
|
||||||
viewport,
|
viewport,
|
||||||
|
spatial,
|
||||||
} = props.watchProps;
|
} = props.watchProps;
|
||||||
const { modelTF } = this.state;
|
const { modelTF } = this.state;
|
||||||
|
|
||||||
|
console.log({ spatial });
|
||||||
|
|
||||||
const [layoutDf, colorDf, pointDilationDf] = await this.fetchData(
|
const [layoutDf, colorDf, pointDilationDf] = await this.fetchData(
|
||||||
annoMatrix,
|
annoMatrix,
|
||||||
layoutChoice,
|
layoutChoice,
|
||||||
@@ -547,7 +580,7 @@ class Graph extends React.Component {
|
|||||||
const { currentDimNames } = layoutChoice;
|
const { currentDimNames } = layoutChoice;
|
||||||
const X = layoutDf.col(currentDimNames[0]).asArray();
|
const X = layoutDf.col(currentDimNames[0]).asArray();
|
||||||
const Y = layoutDf.col(currentDimNames[1]).asArray();
|
const Y = layoutDf.col(currentDimNames[1]).asArray();
|
||||||
const positions = this.computePointPositions(X, Y, modelTF);
|
const positions = this.computePointPositions(X, Y, modelTF, spatial.data);
|
||||||
|
|
||||||
const colorTable = this.updateColorTable(colorsProp, colorDf);
|
const colorTable = this.updateColorTable(colorsProp, colorDf);
|
||||||
const colors = this.computePointColors(colorTable.rgb);
|
const colors = this.computePointColors(colorTable.rgb);
|
||||||
@@ -579,6 +612,7 @@ class Graph extends React.Component {
|
|||||||
flags,
|
flags,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
spatial,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -760,7 +794,6 @@ class Graph extends React.Component {
|
|||||||
const { positions, colors, flags, height, width } = asyncProps;
|
const { positions, colors, flags, height, width } = asyncProps;
|
||||||
this.cachedAsyncProps = asyncProps;
|
this.cachedAsyncProps = asyncProps;
|
||||||
const { pointBuffer, colorBuffer, flagBuffer } = this.state;
|
const { pointBuffer, colorBuffer, flagBuffer } = this.state;
|
||||||
console.log({ pos2: positions });
|
|
||||||
let needToRenderCanvas = false;
|
let needToRenderCanvas = false;
|
||||||
|
|
||||||
if (height !== prevAsyncProps?.height || width !== prevAsyncProps?.width) {
|
if (height !== prevAsyncProps?.height || width !== prevAsyncProps?.width) {
|
||||||
@@ -824,20 +857,21 @@ class Graph extends React.Component {
|
|||||||
projectionTF,
|
projectionTF,
|
||||||
drawSpatialImage
|
drawSpatialImage
|
||||||
) {
|
) {
|
||||||
const { annoMatrix } = this.props;
|
const { annoMatrix, spatial } = this.props;
|
||||||
if (!this.reglCanvas || !annoMatrix) return;
|
if (!this.reglCanvas || !annoMatrix) return;
|
||||||
|
|
||||||
const { schema } = annoMatrix;
|
const { schema } = annoMatrix;
|
||||||
const cameraTF = camera.view();
|
const cameraTF = camera.view();
|
||||||
const projView = mat3.multiply(mat3.create(), projectionTF, cameraTF);
|
const projView = mat3.multiply(mat3.create(), projectionTF, cameraTF);
|
||||||
const { width, height } = this.reglCanvas;
|
const { width, height } = this.reglCanvas;
|
||||||
|
const imW = spatial.data.imageWidth;
|
||||||
|
const imH = spatial.data.imageHeight;
|
||||||
|
|
||||||
regl.poll();
|
regl.poll();
|
||||||
regl.clear({
|
regl.clear({
|
||||||
depth: 1,
|
depth: 1,
|
||||||
color: [0, 0, 0, 0],
|
color: [0, 0, 0, 0],
|
||||||
});
|
});
|
||||||
console.log({ pointBuffer });
|
|
||||||
console.log({ projView });
|
|
||||||
drawPoints({
|
drawPoints({
|
||||||
distance: camera.distance(),
|
distance: camera.distance(),
|
||||||
color: colorBuffer,
|
color: colorBuffer,
|
||||||
@@ -850,8 +884,9 @@ class Graph extends React.Component {
|
|||||||
});
|
});
|
||||||
drawSpatialImage({
|
drawSpatialImage({
|
||||||
projView,
|
projView,
|
||||||
img_width: this.spatialImage.width,
|
imageWidth: imW,
|
||||||
img_height: this.spatialImage.height,
|
imageHeight: imH,
|
||||||
|
rectCoords: [0, 0, imW, 0, 0, imH, 0, imH, imW, 0, imW, imH],
|
||||||
spatialImageAsTexture: regl.texture({
|
spatialImageAsTexture: regl.texture({
|
||||||
data: this.spatialImage,
|
data: this.spatialImage,
|
||||||
wrapS: "clamp",
|
wrapS: "clamp",
|
||||||
@@ -869,10 +904,14 @@ class Graph extends React.Component {
|
|||||||
layoutChoice,
|
layoutChoice,
|
||||||
pointDilation,
|
pointDilation,
|
||||||
crossfilter,
|
crossfilter,
|
||||||
|
spatial,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { modelTF, projectionTF, camera, viewport, regl } = this.state;
|
const { modelTF, projectionTF, camera, viewport, regl } = this.state;
|
||||||
const cameraTF = camera?.view()?.slice();
|
const cameraTF = camera?.view()?.slice();
|
||||||
|
|
||||||
|
console.log("---RENDER");
|
||||||
|
console.log({ props: this.props });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="graph-wrapper"
|
id="graph-wrapper"
|
||||||
@@ -939,6 +978,7 @@ class Graph extends React.Component {
|
|||||||
pointDilation,
|
pointDilation,
|
||||||
crossfilter,
|
crossfilter,
|
||||||
viewport,
|
viewport,
|
||||||
|
spatial,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Async.Pending initial>
|
<Async.Pending initial>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import continuousSelection from "./continuousSelection";
|
|||||||
import graphSelection from "./graphSelection";
|
import graphSelection from "./graphSelection";
|
||||||
import colors from "./colors";
|
import colors from "./colors";
|
||||||
import differential from "./differential";
|
import differential from "./differential";
|
||||||
|
import spatial from "./spatial";
|
||||||
import layoutChoice from "./layoutChoice";
|
import layoutChoice from "./layoutChoice";
|
||||||
import controls from "./controls";
|
import controls from "./controls";
|
||||||
import annotations from "./annotations";
|
import annotations from "./annotations";
|
||||||
@@ -38,6 +39,7 @@ const Reducer = undoable(
|
|||||||
["colors", colors],
|
["colors", colors],
|
||||||
["controls", controls],
|
["controls", controls],
|
||||||
["differential", differential],
|
["differential", differential],
|
||||||
|
["spatial", spatial],
|
||||||
["centroidLabels", centroidLabels],
|
["centroidLabels", centroidLabels],
|
||||||
["pointDilation", pointDialation],
|
["pointDilation", pointDialation],
|
||||||
["autosave", autosave],
|
["autosave", autosave],
|
||||||
@@ -51,6 +53,7 @@ const Reducer = undoable(
|
|||||||
"colors",
|
"colors",
|
||||||
"controls",
|
"controls",
|
||||||
"differential",
|
"differential",
|
||||||
|
"spatial",
|
||||||
"layoutChoice",
|
"layoutChoice",
|
||||||
"centroidLabels",
|
"centroidLabels",
|
||||||
"genesets",
|
"genesets",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const Spatial = (
|
||||||
|
state = {
|
||||||
|
loading: null,
|
||||||
|
error: null,
|
||||||
|
metadata: null,
|
||||||
|
},
|
||||||
|
action
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "request spatial metadata started":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
case "request spatial metadata success":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
metadata: action,
|
||||||
|
};
|
||||||
|
case "request spatial metadata error":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
error: action.data,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Spatial;
|
||||||
@@ -195,6 +195,11 @@ class SpatialImageAPI(Resource):
|
|||||||
def get(self, data_adaptor):
|
def get(self, data_adaptor):
|
||||||
return common_rest.spatial_image_get(request, data_adaptor)
|
return common_rest.spatial_image_get(request, data_adaptor)
|
||||||
|
|
||||||
|
class SpatialMetaAPI(Resource):
|
||||||
|
@rest_get_data_adaptor
|
||||||
|
def get(self, data_adaptor):
|
||||||
|
return data_adaptor.get_spatial_metadata()
|
||||||
|
|
||||||
|
|
||||||
def get_api_base_resources(bp_base):
|
def get_api_base_resources(bp_base):
|
||||||
"""Add resources that are accessed from the api url"""
|
"""Add resources that are accessed from the api url"""
|
||||||
@@ -229,6 +234,7 @@ def get_api_dataroot_resources(bp_dataroot):
|
|||||||
add_resource(LayoutObsAPI, "/layout/obs")
|
add_resource(LayoutObsAPI, "/layout/obs")
|
||||||
# Spatial routes
|
# Spatial routes
|
||||||
add_resource(SpatialImageAPI, "/spatial/image")
|
add_resource(SpatialImageAPI, "/spatial/image")
|
||||||
|
add_resource(SpatialMetaAPI, "/spatial/meta")
|
||||||
return api
|
return api
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,40 @@ class AnndataAdaptor(DataAdaptor):
|
|||||||
def get_spatial(self):
|
def get_spatial(self):
|
||||||
return self.data.uns["spatial"]
|
return self.data.uns["spatial"]
|
||||||
|
|
||||||
|
def get_spatial_metadata(self):
|
||||||
|
spatial = self.get_spatial()
|
||||||
|
|
||||||
|
resolution = "hires"
|
||||||
|
|
||||||
|
if len(list(spatial)) == 0:
|
||||||
|
raise Exception("uns does not have spatial information")
|
||||||
|
|
||||||
|
library_id = list(spatial)[0]
|
||||||
|
|
||||||
|
if "images" not in spatial[library_id]:
|
||||||
|
raise Exception("spatial information does not contain images")
|
||||||
|
|
||||||
|
if resolution not in spatial[library_id]["images"]:
|
||||||
|
raise Exception(f"spatial information does not contain requested resolution '{resolution}'")
|
||||||
|
|
||||||
|
scaleref = spatial[library_id]["scalefactors"][f"tissue_{resolution}_scalef"]
|
||||||
|
(h, w, _) = spatial[library_id]["images"][resolution].shape
|
||||||
|
|
||||||
|
A = self.data.obsm["X_spatial"]
|
||||||
|
min = np.nanmin(A, axis=0)
|
||||||
|
max = np.nanmax(A, axis=0)
|
||||||
|
scale = np.amax(max - min)
|
||||||
|
translate = 0.5 - ((max - min) / scale / 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"imageWidth": w,
|
||||||
|
"imageHeight": h,
|
||||||
|
"scaleref": scaleref,
|
||||||
|
"inverseScale": int(scale),
|
||||||
|
"inverseTranslate": translate.tolist(),
|
||||||
|
"inverseMin": min.tolist(),
|
||||||
|
}
|
||||||
|
|
||||||
def get_embedding_names(self):
|
def get_embedding_names(self):
|
||||||
"""
|
"""
|
||||||
Return pre-computed embeddings.
|
Return pre-computed embeddings.
|
||||||
|
|||||||
@@ -363,10 +363,15 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
|
|
||||||
print(f"scale {scale}, translate {translate}")
|
print(f"scale {scale}, translate {translate}")
|
||||||
|
|
||||||
A = embedding * 0.17011142
|
# if True: # if visium
|
||||||
A = np.column_stack([A[:, 0] / 1921, A[:, 1] / 2000])
|
# self.data.uns["spatial"]
|
||||||
|
|
||||||
normalized_layout = A.astype(dtype=np.float32)
|
|
||||||
|
# adata.uns["spatial"]['V1_Adult_Mouse_Brain']["scalefactors"]["tissue_hires_scalef"]
|
||||||
|
# A = embedding * 0.17011142
|
||||||
|
# A = np.column_stack([A[:, 0] / 1921, A[:, 1] / 2000])
|
||||||
|
|
||||||
|
# normalized_layout = A.astype(dtype=np.float32)
|
||||||
return normalized_layout
|
return normalized_layout
|
||||||
|
|
||||||
def layout_to_fbs_matrix(self, fields):
|
def layout_to_fbs_matrix(self, fields):
|
||||||
|
|||||||
Reference in New Issue
Block a user