Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 154 additions & 57 deletions model/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,167 @@
import type { GraphMakerState } from '@milaboratories/graph-maker';
import type { InferOutputsType, PlRef } from '@platforma-sdk/model';
import { BlockModel, createPFrameForGraphs } from '@platforma-sdk/model';
import { BlockModelV3, DataModelBuilder } from '@platforma-sdk/model';
import type { GraphMakerState } from '@milaboratories/graph-maker';
import { graphMakerPlugin } from '@milaboratories/graph-maker/plugin';
import { getDefaultBlockLabel } from './label';

export type BlockArgs = {
// ---------------------------------------------------------------------------
// Block data versions
// ---------------------------------------------------------------------------

type OldArgs = {
defaultBlockLabel: string;
customBlockLabel: string;
datasetRef?: PlRef;
scChain?: string;
allele?: boolean;
};

export type UiState = {
type OldUiState = {
weightedFlag: boolean;
vUsagePlotState: GraphMakerState;
jUsagePlotState: GraphMakerState;
vjUsagePlotState: GraphMakerState;
};

/** v1 block data — includes plot states that will be transferred to plugins */
type BlockDataV1 = {
defaultBlockLabel: string;
customBlockLabel: string;
datasetRef?: PlRef;
scChain?: string;
allele?: boolean;
weightedFlag: boolean;
vUsagePlotState: GraphMakerState;
jUsagePlotState: GraphMakerState;
vjUsagePlotState: GraphMakerState;
};

export const model = BlockModel.create()
/** v2 block data — plot states live in plugins */
export type BlockData = {
defaultBlockLabel: string;
customBlockLabel: string;
datasetRef?: PlRef;
scChain?: string;
allele?: boolean;
weightedFlag: boolean;
};

.withArgs<BlockArgs>({
// ---------------------------------------------------------------------------
// Plugin instances
// ---------------------------------------------------------------------------

const vUsagePlugin = graphMakerPlugin.create({
pluginId: 'vUsage',
transferAt: 'v1',
config: {
chartType: 'heatmap',
initialTitle: 'V Usage',
initialTemplate: 'heatmapClustered',
initialState: {
currentTab: 'settings',
layersSettings: { heatmapClustered: { normalizationDirection: null } },
},
readonlyInputs: ['value'],
},
});

const jUsagePlugin = graphMakerPlugin.create({
pluginId: 'jUsage',
transferAt: 'v1',
config: {
chartType: 'heatmap',
initialTitle: 'J Usage',
initialTemplate: 'heatmapClustered',
initialState: {
layersSettings: { heatmapClustered: { normalizationDirection: null } },
},
readonlyInputs: ['value'],
},
});

const vjUsagePlugin = graphMakerPlugin.create({
pluginId: 'vjUsage',
transferAt: 'v1',
config: {
chartType: 'heatmap',
initialTitle: 'V/J Usage',
initialTemplate: 'heatmapClustered',
initialState: {
layersSettings: { heatmapClustered: { normalizationDirection: null } },
},
readonlyInputs: ['value'],
},
});

// ---------------------------------------------------------------------------
// Data model
// ---------------------------------------------------------------------------

const dataModel = new DataModelBuilder()
.from<BlockDataV1>('v1')
.upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({
defaultBlockLabel: args.defaultBlockLabel,
customBlockLabel: args.customBlockLabel,
datasetRef: args.datasetRef,
scChain: args.scChain,
allele: args.allele,
weightedFlag: uiState.weightedFlag,
vUsagePlotState: uiState.vUsagePlotState,
jUsagePlotState: uiState.jUsagePlotState,
vjUsagePlotState: uiState.vjUsagePlotState,
}))
.transfer(vUsagePlugin, (v1) => ({
state: v1.vUsagePlotState,
selection: undefined,
chartType: 'heatmap' as const,
readonlyInputs: ['value'],
allowChartDeleting: false,
allowTitleEditing: false,
}))
.transfer(jUsagePlugin, (v1) => ({
state: v1.jUsagePlotState,
selection: undefined,
chartType: 'heatmap' as const,
readonlyInputs: ['value'],
allowChartDeleting: false,
allowTitleEditing: false,
}))
.transfer(vjUsagePlugin, (v1) => ({
state: v1.vjUsagePlotState,
selection: undefined,
chartType: 'heatmap' as const,
readonlyInputs: ['value'],
allowChartDeleting: false,
allowTitleEditing: false,
}))
.migrate<BlockData>('v2', ({ vUsagePlotState: _v, jUsagePlotState: _j, vjUsagePlotState: _vj, ...rest }) => rest)
.init(() => ({
defaultBlockLabel: getDefaultBlockLabel({
allele: false,
isSingleCell: false,
}),
customBlockLabel: '',
scChain: 'A',
allele: false,
})

.withUiState<UiState>({
weightedFlag: true,
vUsagePlotState: {
title: 'V Usage',
template: 'heatmapClustered',
currentTab: 'settings',
layersSettings: {
heatmapClustered: {
normalizationDirection: null,
},
},
},
jUsagePlotState: {
title: 'J Usage',
template: 'heatmapClustered',
currentTab: null,
layersSettings: {
heatmapClustered: {
normalizationDirection: null,
},
},
},
vjUsagePlotState: {
title: 'V/J Usage',
template: 'heatmapClustered',
currentTab: null,
layersSettings: {
heatmapClustered: {
normalizationDirection: null,
},
},
},
}));

// ---------------------------------------------------------------------------
// Block model
// ---------------------------------------------------------------------------

export const platforma = BlockModelV3.create(dataModel)

.args((data) => {
if (!data.datasetRef) throw new Error('Dataset is required');
return {
customBlockLabel: data.customBlockLabel,
datasetRef: data.datasetRef,
scChain: data.scChain,
allele: data.allele,
};
})

.argsValid((ctx) => ctx.args.datasetRef !== undefined)

.output('datasetOptions', (ctx) =>
ctx.resultPool.getOptions([{
axes: [
Expand All @@ -81,42 +177,43 @@ export const model = BlockModel.create()
annotations: { 'pl7.app/isAnchor': 'true' },
}],
{
// suppress native label of the column (e.g. "Number of Reads") to show only the dataset label
label: { includeNativeLabel: false },
}),
)

.output('datasetSpec', (ctx) => {
if (ctx.args.datasetRef === undefined) {
return undefined;
}

return ctx.resultPool.getPColumnSpecByRef(ctx.args.datasetRef);
})

.outputWithStatus('pf', (ctx) => {
const pCols = ctx.outputs?.resolve('pf')?.getPColumns();
if (pCols === undefined) {
if (ctx.data.datasetRef === undefined) {
return undefined;
}

return createPFrameForGraphs(ctx, pCols);
return ctx.resultPool.getPColumnSpecByRef(ctx.data.datasetRef);
})

.output('isRunning', (ctx) => ctx.outputs?.getIsReadyOrError() === false)

.title(() => 'V/J Usage')

.subtitle((ctx) => ctx.args.customBlockLabel || ctx.args.defaultBlockLabel)
.subtitle((ctx) => ctx.data.customBlockLabel || ctx.data.defaultBlockLabel)

.sections((_) => [
{ type: 'link', href: '/', label: 'V Gene Usage' },
{ type: 'link', href: '/jUsage', label: 'J Gene Usage' },
{ type: 'link', href: '/vjUsage', label: 'V/J Gene Usage' },
])

.done(2);
.plugin(vUsagePlugin, {
blockColumns: (ctx) => ctx.outputs?.resolve('pf')?.getPColumns(),
})
Comment on lines +203 to +205

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The graph-maker plugin is configured to get its blockColumns from an output named pf. However, the pf output, which was present in the previous model version, has been removed in this refactoring. As a result, ctx.outputs?.resolve('pf') will be undefined, and the block will fail to get data for the graphs.

You need to ensure the data is provided to the plugins. This could be done by:

  1. Re-introducing an output that provides the PColumn[] for the graphs. The graph-maker plugin might even offer a utility function for this.
  2. Revisiting the plugin configuration if blockColumns is not the intended way to pipe data in this new setup.


.plugin(jUsagePlugin, {
blockColumns: (ctx) => ctx.outputs?.resolve('pf')?.getPColumns(),
})

.plugin(vjUsagePlugin, {
blockColumns: (ctx) => ctx.outputs?.resolve('pf')?.getPColumns(),
})

.done();

export type BlockOutputs = InferOutputsType<typeof model>;
export type BlockOutputs = InferOutputsType<typeof platforma>;

export { getDefaultBlockLabel } from './label';
24 changes: 12 additions & 12 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ packages:
catalog:
# SDK packages - EXACT VERSIONS (no ^ or ~)
'@milaboratories/ts-builder': 1.2.4
'@milaboratories/ts-configs': 1.2.0
'@platforma-sdk/workflow-tengo': 5.8.1
'@platforma-sdk/model': 1.53.11
'@platforma-sdk/ui-vue': 1.53.11
'@platforma-sdk/tengo-builder': 2.4.14
'@platforma-sdk/package-builder': 3.11.0
'@platforma-sdk/block-tools': 2.6.43
'@milaboratories/ts-configs': 1.2.2
'@platforma-sdk/workflow-tengo': 5.10.1
'@platforma-sdk/model': 1.59.3
'@platforma-sdk/ui-vue': 1.59.3
'@platforma-sdk/tengo-builder': 2.4.30
'@platforma-sdk/package-builder': 3.11.6
'@platforma-sdk/block-tools': 2.6.70
'@platforma-sdk/eslint-config': 1.2.0
'@platforma-sdk/test': 1.53.11
'@milaboratories/helpers': 1.13.0
'@milaboratories/graph-maker': 1.1.222
'@milaboratories/strings': 0.1.0
'@platforma-sdk/test': 1.59.3
'@milaboratories/helpers': 1.13.7
'@milaboratories/graph-maker': 1.2.4
'@milaboratories/strings': 0.1.2

# Common dependencies - can use ^ or ~
'vue': ^3.5.24
Expand All @@ -31,4 +31,4 @@ catalog:
'@changesets/cli': ^2.29.8

# Block-specific dependencies
"@platforma-open/milaboratories.runenv-python-3": ^1.7.5
"@platforma-open/milaboratories.runenv-python-3": ^1.7.9
18 changes: 9 additions & 9 deletions ui/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { isJsonEqual } from '@milaboratories/helpers';
import { getDefaultBlockLabel, model } from '@platforma-open/milaboratories.vj-usage.model';
import { defineApp } from '@platforma-sdk/ui-vue';
import { getDefaultBlockLabel, platforma } from '@platforma-open/milaboratories.vj-usage.model';
import { defineAppV3 } from '@platforma-sdk/ui-vue';
import { computed, watchEffect } from 'vue';
import JUsage from './pages/JUsage.vue';
import VJUsage from './pages/VJUsage.vue';
import VUsage from './pages/VUsage.vue';
import { useIsSingleCell, useScChainOptions } from './utils';

export const sdkPlugin = defineApp(model, (app) => {
app.model.args.customBlockLabel ??= '';
export const sdkPlugin = defineAppV3(platforma, (app) => {
app.model.data.customBlockLabel ??= '';

syncDefaultBlockLabel(app.model);

Expand All @@ -30,22 +30,22 @@ function syncDefaultBlockLabel(model: AppModel) {
const scChainOptions = useScChainOptions(() => model.outputs.datasetSpec);

const datasetLabel = computed(() => {
if (!model.args.datasetRef) return;
if (!model.data.datasetRef) return;
return model.outputs.datasetOptions
?.find((option) => isJsonEqual(option.ref, model.args.datasetRef))
?.find((option) => isJsonEqual(option.ref, model.data.datasetRef))
?.label;
});

// Get chain label for single-cell datasets
const chainLabel = computed(() => {
if (!isSingleCell.value) return;
return scChainOptions.value?.find((o) => o.value === model.args.scChain)?.label;
return scChainOptions.value?.find((o) => o.value === model.data.scChain)?.label;
});

watchEffect(() => {
model.args.defaultBlockLabel = getDefaultBlockLabel({
model.data.defaultBlockLabel = getDefaultBlockLabel({
datasetLabel: datasetLabel.value,
allele: model.args.allele ?? false,
allele: model.data.allele ?? false,
isSingleCell: isSingleCell.value,
chainLabel: chainLabel.value,
});
Expand Down
Loading
Loading