From 2814a1bc0e9ec25c49511e9702dbd17df96fdc34 Mon Sep 17 00:00:00 2001 From: Elena Erokhina Date: Wed, 18 Mar 2026 13:27:41 +0100 Subject: [PATCH 1/3] use graph-maker plugin --- model/src/index.ts | 211 ++++++++++++++++++++++++++++---------- pnpm-workspace.yaml | 24 ++--- ui/src/app.ts | 18 ++-- ui/src/pages/JUsage.vue | 45 ++------ ui/src/pages/Settings.vue | 20 ++-- ui/src/pages/VJUsage.vue | 53 +++------- ui/src/pages/VUsage.vue | 50 +++------ 7 files changed, 222 insertions(+), 199 deletions(-) diff --git a/model/src/index.ts b/model/src/index.ts index debcc60..d4593e9 100644 --- a/model/src/index.ts +++ b/model/src/index.ts @@ -1,9 +1,14 @@ -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; @@ -11,16 +16,126 @@ export type BlockArgs = { 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({ +// --------------------------------------------------------------------------- +// 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('v1') + .upgradeLegacy(({ 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('v2', ({ vUsagePlotState: _v, jUsagePlotState: _j, vjUsagePlotState: _vj, ...rest }) => rest) + .init(() => ({ defaultBlockLabel: getDefaultBlockLabel({ allele: false, isSingleCell: false, @@ -28,44 +143,25 @@ export const model = BlockModel.create() customBlockLabel: '', scChain: 'A', allele: false, - }) - - .withUiState({ 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: [ @@ -81,33 +177,22 @@ 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' }, @@ -115,8 +200,20 @@ export const model = BlockModel.create() { type: 'link', href: '/vjUsage', label: 'V/J Gene Usage' }, ]) - .done(2); + .plugin(vUsagePlugin, { + blockColumns: (ctx) => ctx.outputs?.resolve('pf')?.getPColumns(), + }) + + .plugin(jUsagePlugin, { + blockColumns: (ctx) => ctx.outputs?.resolve('pf')?.getPColumns(), + }) + + .plugin(vjUsagePlugin, { + blockColumns: (ctx) => ctx.outputs?.resolve('pf')?.getPColumns(), + }) + + .done(); -export type BlockOutputs = InferOutputsType; +export type BlockOutputs = InferOutputsType; export { getDefaultBlockLabel } from './label'; diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 89860fb..cf7e721 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 @@ -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 diff --git a/ui/src/app.ts b/ui/src/app.ts index 568dbd1..c6d0574 100644 --- a/ui/src/app.ts +++ b/ui/src/app.ts @@ -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); @@ -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, }); diff --git a/ui/src/pages/JUsage.vue b/ui/src/pages/JUsage.vue index 62afec7..57ea837 100644 --- a/ui/src/pages/JUsage.vue +++ b/ui/src/pages/JUsage.vue @@ -1,6 +1,6 @@ diff --git a/ui/src/pages/Settings.vue b/ui/src/pages/Settings.vue index 91906c9..6337437 100644 --- a/ui/src/pages/Settings.vue +++ b/ui/src/pages/Settings.vue @@ -1,24 +1,28 @@ diff --git a/ui/src/pages/VJUsage.vue b/ui/src/pages/VJUsage.vue index 1b9b465..ddc1e5c 100644 --- a/ui/src/pages/VJUsage.vue +++ b/ui/src/pages/VJUsage.vue @@ -1,6 +1,6 @@ diff --git a/ui/src/pages/VUsage.vue b/ui/src/pages/VUsage.vue index 1b34994..e346c08 100644 --- a/ui/src/pages/VUsage.vue +++ b/ui/src/pages/VUsage.vue @@ -1,6 +1,6 @@