mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-11 05:20:57 +08:00
layout change UI (#776)
* add layout to schema * add layout choice action and reducer * multi layout UI * update unit tests * add missing file * update test schema * fix duplicate test id * fix tabs * PR lint * fix pytest
This commit is contained in:
@@ -56,6 +56,10 @@ const aSchemaResponse = {
|
|||||||
categories: fieldDCategories
|
categories: fieldDCategories
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
obs: [{ name: "umap", type: "float32", dims: ["umap_0", "umap_1"] }],
|
||||||
|
var: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ describe("createUniverseFromResponse", () => {
|
|||||||
REST.schema.schema.annotations.obs.length
|
REST.schema.schema.annotations.obs.length
|
||||||
]);
|
]);
|
||||||
expect(universe.obsLayout.dims).toEqual([nObs, 2]);
|
expect(universe.obsLayout.dims).toEqual([nObs, 2]);
|
||||||
expect(universe.obsLayout.colIndex.keys()).toEqual(["X", "Y"]);
|
expect(universe.obsLayout.colIndex.keys()).toEqual(
|
||||||
|
universe.schema.layout.obs[0].dims
|
||||||
|
);
|
||||||
expect(universe.varAnnotations.dims).toEqual([
|
expect(universe.varAnnotations.dims).toEqual([
|
||||||
nVar,
|
nVar,
|
||||||
REST.schema.schema.annotations.var.length
|
REST.schema.schema.annotations.var.length
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ const defaultBigBang = () => {
|
|||||||
/* create crossfilter */
|
/* create crossfilter */
|
||||||
const crossfilter = World.createObsDimensions(
|
const crossfilter = World.createObsDimensions(
|
||||||
new Crossfilter(world.obsAnnotations),
|
new Crossfilter(world.obsAnnotations),
|
||||||
world
|
world,
|
||||||
|
REST.schema.schema.layout.obs[0].dims
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -138,7 +139,9 @@ describe("createWorldFromCurrentSelection", () => {
|
|||||||
expect(world.obsLayout.rowIndex.keys()).toEqual(
|
expect(world.obsLayout.rowIndex.keys()).toEqual(
|
||||||
new Int32Array(matchingIndices)
|
new Int32Array(matchingIndices)
|
||||||
);
|
);
|
||||||
expect(world.obsLayout.colIndex.keys()).toEqual(["X", "Y"]);
|
expect(world.obsLayout.colIndex.keys()).toEqual(
|
||||||
|
world.schema.layout.obs[0].dims
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
Position,
|
Position,
|
||||||
NumericInput,
|
NumericInput,
|
||||||
Icon
|
Icon,
|
||||||
|
RadioGroup,
|
||||||
|
Radio
|
||||||
} from "@blueprintjs/core";
|
} from "@blueprintjs/core";
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
@@ -47,7 +49,8 @@ import { World } from "../../util/stateManager";
|
|||||||
undoDisabled: state["@@undoable/past"].length === 0,
|
undoDisabled: state["@@undoable/past"].length === 0,
|
||||||
redoDisabled: state["@@undoable/future"].length === 0,
|
redoDisabled: state["@@undoable/future"].length === 0,
|
||||||
selectionTool: state.graphSelection.tool,
|
selectionTool: state.graphSelection.tool,
|
||||||
currentSelection: state.graphSelection.selection
|
currentSelection: state.graphSelection.selection,
|
||||||
|
layoutChoice: state.layoutChoice
|
||||||
}))
|
}))
|
||||||
class Graph extends React.Component {
|
class Graph extends React.Component {
|
||||||
static isValidDigitKeyEvent(e) {
|
static isValidDigitKeyEvent(e) {
|
||||||
@@ -81,6 +84,8 @@ class Graph extends React.Component {
|
|||||||
this.graphPaddingBottom = 45;
|
this.graphPaddingBottom = 45;
|
||||||
this.graphPaddingRight = globals.leftSidebarWidth;
|
this.graphPaddingRight = globals.leftSidebarWidth;
|
||||||
this.renderCache = {
|
this.renderCache = {
|
||||||
|
X: null,
|
||||||
|
Y: null,
|
||||||
positions: null,
|
positions: null,
|
||||||
colors: null,
|
colors: null,
|
||||||
sizes: null
|
sizes: null
|
||||||
@@ -140,7 +145,8 @@ class Graph extends React.Component {
|
|||||||
colorRGB,
|
colorRGB,
|
||||||
responsive,
|
responsive,
|
||||||
selectionTool,
|
selectionTool,
|
||||||
currentSelection
|
currentSelection,
|
||||||
|
layoutChoice
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
reglRender,
|
reglRender,
|
||||||
@@ -163,13 +169,15 @@ class Graph extends React.Component {
|
|||||||
if (regl && world) {
|
if (regl && world) {
|
||||||
/* update the regl state */
|
/* update the regl state */
|
||||||
const { obsLayout, nObs } = world;
|
const { obsLayout, nObs } = world;
|
||||||
const X = obsLayout.col("X").asArray();
|
const X = obsLayout.col(layoutChoice.currentDimNames[0]).asArray();
|
||||||
const Y = obsLayout.col("Y").asArray();
|
const Y = obsLayout.col(layoutChoice.currentDimNames[1]).asArray();
|
||||||
|
const { X: prevX, Y: prevY } = renderCache;
|
||||||
|
|
||||||
// X/Y positions for each point - a cached value that only
|
// X/Y positions for each point - a cached value that only
|
||||||
// changes if we have loaded entirely new cell data
|
// changes if we have loaded entirely new cell data
|
||||||
//
|
//
|
||||||
if (!renderCache.positions || world !== prevProps.world) {
|
/* TODO/XXX: we should just memoize this code */
|
||||||
|
if (!renderCache.positions || X !== prevX || Y !== prevY) {
|
||||||
renderCache.positions = new Float32Array(2 * nObs);
|
renderCache.positions = new Float32Array(2 * nObs);
|
||||||
|
|
||||||
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
||||||
@@ -188,6 +196,8 @@ class Graph extends React.Component {
|
|||||||
glScaleX,
|
glScaleX,
|
||||||
glScaleY
|
glScaleY
|
||||||
};
|
};
|
||||||
|
renderCache.X = X;
|
||||||
|
renderCache.Y = Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colors for each point - a cached value that only changes when
|
// Colors for each point - a cached value that only changes when
|
||||||
@@ -436,6 +446,14 @@ class Graph extends React.Component {
|
|||||||
this.setState({ pendingClipPercentiles: null });
|
this.setState({ pendingClipPercentiles: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleLayoutChoiceChange = e => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch({
|
||||||
|
type: "set layout choice",
|
||||||
|
layoutChoice: e.currentTarget.value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
brushToolUpdate(tool, container, transform) {
|
brushToolUpdate(tool, container, transform) {
|
||||||
/*
|
/*
|
||||||
this is called from componentDidUpdate(), so be very careful using
|
this is called from componentDidUpdate(), so be very careful using
|
||||||
@@ -751,7 +769,8 @@ class Graph extends React.Component {
|
|||||||
redoDisabled,
|
redoDisabled,
|
||||||
selectionTool,
|
selectionTool,
|
||||||
clipPercentileMin,
|
clipPercentileMin,
|
||||||
clipPercentileMax
|
clipPercentileMax,
|
||||||
|
layoutChoice
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { mode, pendingClipPercentiles } = this.state;
|
const { mode, pendingClipPercentiles } = this.state;
|
||||||
|
|
||||||
@@ -894,6 +913,49 @@ class Graph extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="bp3-button-group"
|
||||||
|
style={{
|
||||||
|
marginLeft: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
target={
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
data-testid="layout-choice"
|
||||||
|
className="bp3-button bp3-icon-heatmap"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
position={Position.BOTTOM_RIGHT}
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flexDirection: "column",
|
||||||
|
padding: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
label="Layout Choice"
|
||||||
|
onChange={this.handleLayoutChoiceChange}
|
||||||
|
selectedValue={layoutChoice.current}
|
||||||
|
>
|
||||||
|
{layoutChoice.available.map(name => (
|
||||||
|
<Radio label={name} value={name} key={name} />
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="bp3-button-group"
|
className="bp3-button-group"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ const CrossfilterReducer = (
|
|||||||
) => {
|
) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "initial data load complete (universe exists)": {
|
case "initial data load complete (universe exists)": {
|
||||||
const { world } = nextSharedState;
|
const { world, layoutChoice } = nextSharedState;
|
||||||
const crossfilter = World.createObsDimensions(
|
const crossfilter = World.createObsDimensions(
|
||||||
new Crossfilter(world.obsAnnotations),
|
new Crossfilter(world.obsAnnotations),
|
||||||
world
|
world,
|
||||||
|
layoutChoice.currentDimNames
|
||||||
);
|
);
|
||||||
return crossfilter;
|
return crossfilter;
|
||||||
}
|
}
|
||||||
@@ -43,9 +44,13 @@ const CrossfilterReducer = (
|
|||||||
case "set clip quantiles":
|
case "set clip quantiles":
|
||||||
case "set World to current selection": {
|
case "set World to current selection": {
|
||||||
const { userDefinedGenes, diffexpGenes } = prevSharedState.controls;
|
const { userDefinedGenes, diffexpGenes } = prevSharedState.controls;
|
||||||
const { world } = nextSharedState;
|
const { world, layoutChoice } = nextSharedState;
|
||||||
let crossfilter = new Crossfilter(world.obsAnnotations);
|
let crossfilter = new Crossfilter(world.obsAnnotations);
|
||||||
crossfilter = World.createObsDimensions(crossfilter, world);
|
crossfilter = World.createObsDimensions(
|
||||||
|
crossfilter,
|
||||||
|
world,
|
||||||
|
layoutChoice.currentDimNames
|
||||||
|
);
|
||||||
crossfilter = ControlsHelpers.createGeneDimensions(
|
crossfilter = ControlsHelpers.createGeneDimensions(
|
||||||
userDefinedGenes,
|
userDefinedGenes,
|
||||||
diffexpGenes,
|
diffexpGenes,
|
||||||
@@ -55,6 +60,23 @@ const CrossfilterReducer = (
|
|||||||
return crossfilter;
|
return crossfilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "set layout choice": {
|
||||||
|
/*
|
||||||
|
when switching layouts:
|
||||||
|
- delete the existing XY index
|
||||||
|
- add the new XY index (which implicitly selects all on it)
|
||||||
|
*/
|
||||||
|
const { world, layoutChoice } = nextSharedState;
|
||||||
|
return state
|
||||||
|
.delDimension(layoutDimensionName("XY"))
|
||||||
|
.addDimension(
|
||||||
|
layoutDimensionName("XY"),
|
||||||
|
"spatial",
|
||||||
|
world.obsLayout.col(layoutChoice.currentDimNames[0]).asArray(),
|
||||||
|
world.obsLayout.col(layoutChoice.currentDimNames[1]).asArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case "request user defined gene success": {
|
case "request user defined gene success": {
|
||||||
const { world } = prevSharedState;
|
const { world } = prevSharedState;
|
||||||
const gene = action.data.genes[0];
|
const gene = action.data.genes[0];
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const GraphSelection = (
|
|||||||
) => {
|
) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "set clip quantiles":
|
case "set clip quantiles":
|
||||||
case "reset World to eq Universe": {
|
case "reset World to eq Universe":
|
||||||
|
case "set layout choice": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
selection: {
|
selection: {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import graphSelection from "./graphSelection";
|
|||||||
import crossfilter from "./crossfilter";
|
import crossfilter from "./crossfilter";
|
||||||
import colors from "./colors";
|
import colors from "./colors";
|
||||||
import differential from "./differential";
|
import differential from "./differential";
|
||||||
|
import layoutChoice from "./layoutChoice";
|
||||||
import responsive from "./responsive";
|
import responsive from "./responsive";
|
||||||
import controls from "./controls";
|
import controls from "./controls";
|
||||||
import resetCache from "./resetCache";
|
import resetCache from "./resetCache";
|
||||||
@@ -19,31 +20,33 @@ import resetCache from "./resetCache";
|
|||||||
import undoableConfig from "./undoableConfig";
|
import undoableConfig from "./undoableConfig";
|
||||||
|
|
||||||
const Reducer = undoable(
|
const Reducer = undoable(
|
||||||
cascadeReducers([
|
cascadeReducers([
|
||||||
["config", config],
|
["config", config],
|
||||||
["universe", universe],
|
["universe", universe],
|
||||||
["world", world],
|
["world", world],
|
||||||
["categoricalSelection", categoricalSelection],
|
["layoutChoice", layoutChoice],
|
||||||
["continuousSelection", continuousSelection],
|
["categoricalSelection", categoricalSelection],
|
||||||
["graphSelection", graphSelection],
|
["continuousSelection", continuousSelection],
|
||||||
["crossfilter", crossfilter],
|
["graphSelection", graphSelection],
|
||||||
["colors", colors],
|
["crossfilter", crossfilter],
|
||||||
["controls", controls],
|
["colors", colors],
|
||||||
["differential", differential],
|
["controls", controls],
|
||||||
["responsive", responsive],
|
["differential", differential],
|
||||||
["resetCache", resetCache]
|
["responsive", responsive],
|
||||||
]),
|
["resetCache", resetCache]
|
||||||
[
|
]),
|
||||||
"world",
|
[
|
||||||
"categoricalSelection",
|
"world",
|
||||||
"continuousSelection",
|
"categoricalSelection",
|
||||||
"graphSelection",
|
"continuousSelection",
|
||||||
"crossfilter",
|
"graphSelection",
|
||||||
"colors",
|
"crossfilter",
|
||||||
"controls",
|
"colors",
|
||||||
"differential"
|
"controls",
|
||||||
],
|
"differential",
|
||||||
undoableConfig
|
"layoutChoice"
|
||||||
|
],
|
||||||
|
undoableConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = createStore(Reducer, applyMiddleware(thunk));
|
const store = createStore(Reducer, applyMiddleware(thunk));
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
we have a UI heuristic to pick the default layout, based on assumptions
|
||||||
|
about commonly used names. Preferentially, pick in the following order:
|
||||||
|
|
||||||
|
1. "umap"
|
||||||
|
2. "tsne"
|
||||||
|
3. "pca"
|
||||||
|
4. give up, use the first available
|
||||||
|
*/
|
||||||
|
function bestDefaultLayout(layouts) {
|
||||||
|
const preferredNames = ["umap", "tsne", "pca"];
|
||||||
|
const idx = preferredNames.findIndex(name => layouts.indexOf(name) !== -1);
|
||||||
|
if (idx !== -1) return preferredNames[idx];
|
||||||
|
return layouts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LayoutChoice = (
|
||||||
|
state = {
|
||||||
|
available: [], // all available choices
|
||||||
|
current: undefined, // name of the current layout, eg, 'umap'
|
||||||
|
currentDimNames: [] // dimension name
|
||||||
|
},
|
||||||
|
action,
|
||||||
|
nextSharedState
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "initial data load complete (universe exists)":
|
||||||
|
case "reset World to eq Universe": {
|
||||||
|
// set default to default
|
||||||
|
const { schema } = nextSharedState.world;
|
||||||
|
const available = schema.layout.obs.map(v => v.name);
|
||||||
|
const current = bestDefaultLayout(available);
|
||||||
|
const currentDimNames = schema.layout.obsByName[current].dims;
|
||||||
|
return { available, current, currentDimNames };
|
||||||
|
}
|
||||||
|
|
||||||
|
case "set layout choice": {
|
||||||
|
const { schema } = nextSharedState.world;
|
||||||
|
const current = action.layoutChoice;
|
||||||
|
const currentDimNames = schema.layout.obsByName[current].dims;
|
||||||
|
return { ...state, current, currentDimNames };
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LayoutChoice;
|
||||||
@@ -72,7 +72,9 @@ const saveOnActions = new Set([
|
|||||||
"store current cell selection as differential set 2",
|
"store current cell selection as differential set 2",
|
||||||
|
|
||||||
"set World to current selection",
|
"set World to current selection",
|
||||||
"set clip quantiles"
|
"set clip quantiles",
|
||||||
|
|
||||||
|
"set layout choice"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -104,27 +104,11 @@ function LayoutFBSToDataframe(arrayBuffer) {
|
|||||||
throw new Error("Unexpected layout data type returned from server");
|
throw new Error("Unexpected layout data type returned from server");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
TODO: XXX
|
|
||||||
|
|
||||||
TEMPORARY CODE AND COMMENT to support the progressive implementation
|
|
||||||
of multi-layout support. For now, we search for one of the following
|
|
||||||
in the layouts and use it if we find it: umap, then tsne, then pca,
|
|
||||||
then whatever is first in the list.
|
|
||||||
*/
|
|
||||||
let layoutIndex = 0;
|
|
||||||
["umap", "tsne", "pca"].some(name => {
|
|
||||||
const idx = fbs.colIdx.indexOf(`${name}_0`);
|
|
||||||
if (idx !== -1) {
|
|
||||||
layoutIndex = idx;
|
|
||||||
}
|
|
||||||
return idx !== -1;
|
|
||||||
});
|
|
||||||
const df = new Dataframe.Dataframe(
|
const df = new Dataframe.Dataframe(
|
||||||
[fbs.nRows, 2],
|
[fbs.nRows, fbs.nCols],
|
||||||
[fbs.columns[layoutIndex], fbs.columns[layoutIndex + 1]],
|
fbs.columns,
|
||||||
null,
|
null,
|
||||||
new Dataframe.KeyIndex(["X", "Y"])
|
new Dataframe.KeyIndex(fbs.colIdx)
|
||||||
);
|
);
|
||||||
return df;
|
return df;
|
||||||
}
|
}
|
||||||
@@ -172,6 +156,9 @@ export function createUniverseFromResponse(
|
|||||||
universe.schema = schema;
|
universe.schema = schema;
|
||||||
universe.nObs = schema.dataframe.nObs;
|
universe.nObs = schema.dataframe.nObs;
|
||||||
universe.nVar = schema.dataframe.nVar;
|
universe.nVar = schema.dataframe.nVar;
|
||||||
|
/* add defaults, as we can't assume back-end will fully populate schema */
|
||||||
|
if (!schema.layout.var) schema.layout.var = [];
|
||||||
|
if (!schema.layout.obs) schema.layout.obs = [];
|
||||||
|
|
||||||
/* annotations */
|
/* annotations */
|
||||||
universe.obsAnnotations = AnnotationsFBSToDataframe(annotationsObsResponse);
|
universe.obsAnnotations = AnnotationsFBSToDataframe(annotationsObsResponse);
|
||||||
@@ -197,6 +184,12 @@ export function createUniverseFromResponse(
|
|||||||
universe.schema.annotations.varByName = fromEntries(
|
universe.schema.annotations.varByName = fromEntries(
|
||||||
universe.schema.annotations.var.map(v => [v.name, v])
|
universe.schema.annotations.var.map(v => [v.name, v])
|
||||||
);
|
);
|
||||||
|
universe.schema.layout.obsByName = fromEntries(
|
||||||
|
universe.schema.layout.obs.map(v => [v.name, v])
|
||||||
|
);
|
||||||
|
universe.schema.layout.varByName = fromEntries(
|
||||||
|
universe.schema.layout.var.map(v => [v.name, v])
|
||||||
|
);
|
||||||
return universe;
|
return universe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ function deduceDimensionType(attributes, fieldName) {
|
|||||||
return dimensionType;
|
return dimensionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createObsDimensions(crossfilter, world) {
|
export function createObsDimensions(crossfilter, world, XYdimNames) {
|
||||||
/*
|
/*
|
||||||
create and return a crossfilter with a dimension for every obs annotation
|
create and return a crossfilter with a dimension for every obs annotation
|
||||||
for which we have a supported type, *except* 'name'
|
for which we have a supported type, *except* 'name'
|
||||||
@@ -283,8 +283,8 @@ export function createObsDimensions(crossfilter, world) {
|
|||||||
return crossfilter.addDimension(
|
return crossfilter.addDimension(
|
||||||
layoutDimensionName("XY"),
|
layoutDimensionName("XY"),
|
||||||
"spatial",
|
"spatial",
|
||||||
obsLayout.col("X").asArray(),
|
obsLayout.col(XYdimNames[0]).asArray(),
|
||||||
obsLayout.col("Y").asArray()
|
obsLayout.col(XYdimNames[1]).asArray()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class ScanpyEngine(CXGDriver):
|
|||||||
"type": str(self.data.X.dtype),
|
"type": str(self.data.X.dtype),
|
||||||
},
|
},
|
||||||
"annotations": {"obs": [], "var": []},
|
"annotations": {"obs": [], "var": []},
|
||||||
|
"layout": {"obs": []}
|
||||||
}
|
}
|
||||||
for ax in Axis:
|
for ax in Axis:
|
||||||
curr_axis = getattr(self.data, str(ax))
|
curr_axis = getattr(self.data, str(ax))
|
||||||
@@ -140,6 +141,14 @@ class ScanpyEngine(CXGDriver):
|
|||||||
)
|
)
|
||||||
self.schema["annotations"][ax].append(ann_schema)
|
self.schema["annotations"][ax].append(ann_schema)
|
||||||
|
|
||||||
|
for layout in self.config['layout']:
|
||||||
|
layout_schema = {
|
||||||
|
"name": layout,
|
||||||
|
"type": "float32",
|
||||||
|
"dims": [f"{layout}_0", f"{layout}_1"]
|
||||||
|
}
|
||||||
|
self.schema["layout"]["obs"].append(layout_schema)
|
||||||
|
|
||||||
def _load_data(self, data):
|
def _load_data(self, data):
|
||||||
# as of AnnData 0.6.19, backed mode performs initial load fast, but at the
|
# as of AnnData 0.6.19, backed mode performs initial load fast, but at the
|
||||||
# cost of significantly slower access to X data.
|
# cost of significantly slower access to X data.
|
||||||
|
|||||||
@@ -47,5 +47,14 @@
|
|||||||
"type": "int32"
|
"type": "int32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"obs": [
|
||||||
|
{
|
||||||
|
"name": "umap",
|
||||||
|
"type": "float32",
|
||||||
|
"dims": ["umap_0", "umap_1"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user