diff --git a/libraries/ml/src/MCL/clustering-view.ts b/libraries/ml/src/MCL/clustering-view.ts index 88c8703a64..d828bb96ed 100644 --- a/libraries/ml/src/MCL/clustering-view.ts +++ b/libraries/ml/src/MCL/clustering-view.ts @@ -8,6 +8,7 @@ import {KnownMetrics} from '../typed-metrics'; import {DistanceAggregationMethod} from '../distance-matrix/types'; import {PreprocessFunctionReturnType} from '../functionEditors/dimensionality-reduction-editor'; import {Options} from '@datagrok-libraries/utils/src/type-declarations'; +import {findTableView} from '../multi-column-dimensionality-reduction/reduce-dimensionality'; import * as rxjs from 'rxjs'; @@ -38,7 +39,7 @@ export async function markovCluster( let tv: DG.TableView | null = null; let sc: DG.ScatterPlotViewer | undefined = scp; if (!sc) { - tv = grok.shell.tableView(df.name) ?? grok.shell.addTableView(df); + tv = findTableView(df) ?? grok.shell.addTableView(df); sc = tv.scatterPlot({...scatterPlotProps, title: 'MCL'}); } diff --git a/libraries/ml/src/MCL/mcl-editor.ts b/libraries/ml/src/MCL/mcl-editor.ts index 20850fea6c..5041cc28c4 100644 --- a/libraries/ml/src/MCL/mcl-editor.ts +++ b/libraries/ml/src/MCL/mcl-editor.ts @@ -69,7 +69,7 @@ export class MCLEditor extends MultiColumnDimReductionEditor { public get params() { return { - table: this.tableInput.value!, + table: this.tableInput.value ?? this.currentTable!, columns: this.columnsInput.value!, methodName: this.methodInput.value!, preprocessingFunctions: this.columnOptEditors.map((it) => it.preProcessingFunction), diff --git a/libraries/ml/src/multi-column-dimensionality-reduction/multi-column-dim-reduction-editor.ts b/libraries/ml/src/multi-column-dimensionality-reduction/multi-column-dim-reduction-editor.ts index 2f398faceb..8659eb880d 100644 --- a/libraries/ml/src/multi-column-dimensionality-reduction/multi-column-dim-reduction-editor.ts +++ b/libraries/ml/src/multi-column-dimensionality-reduction/multi-column-dim-reduction-editor.ts @@ -84,6 +84,8 @@ export type MultiColDimReductionParams = { export class MultiColumnDimReductionEditor { editorSettings: DimReductionEditorOptions = {}; tableInput!: DG.InputBase; + // last resolved table: tableInput resolves value by name and loses the selection if the table is renamed + currentTable: DG.DataFrame | null = null; columnsInput!: DG.InputBase; columnsInputRoot!: HTMLElement; columnOptEditors: DimReductionColumnEditor[] = []; @@ -174,6 +176,7 @@ export class MultiColumnDimReductionEditor { const table = this.tableInput.value; if (!table) return; + this.currentTable = table; ui.empty(this.columnOptEditorsRoot); this.columnFunctionsMap = {}; @@ -332,7 +335,7 @@ export class MultiColumnDimReductionEditor { const usePlaceholders = this.columnOptEditors.length === 0 && columns && columns.length > 0; const n = columns?.length ?? 0; return { - table: this.tableInput.value!, + table: this.tableInput.value ?? this.currentTable!, columns, methodName: this.methodInput.value!, preprocessingFunctions: usePlaceholders ? diff --git a/libraries/ml/src/multi-column-dimensionality-reduction/reduce-dimensionality.ts b/libraries/ml/src/multi-column-dimensionality-reduction/reduce-dimensionality.ts index b247549f2b..7de54b3979 100644 --- a/libraries/ml/src/multi-column-dimensionality-reduction/reduce-dimensionality.ts +++ b/libraries/ml/src/multi-column-dimensionality-reduction/reduce-dimensionality.ts @@ -33,6 +33,11 @@ export function getEmbeddingViewerName(columns: DG.Column[], method: DimReductio return `${method} (${colNames})`; } +export function findTableView(table: DG.DataFrame): DG.TableView | undefined { + // match by stable .dart identity: tableViews returns a fresh toJs wrapper per access, and table names are nullable + return Array.from(grok.shell.tableViews).find((v) => v.dataFrame?.dart === table.dart); +} + export async function multiColReduceDimensionality(table: DG.DataFrame, columns: DG.Column[], method: DimReductionMethods, metrics: KnownMetrics[], weights: number[], preprocessingFunctions: (DG.Func | null | undefined)[], @@ -53,7 +58,7 @@ export async function multiColReduceDimensionality(table: DG.DataFrame, columns: 'must have the same length'); } - const tv = plotEmbeddings ? (uiOptions.tableView ?? grok.shell.tableView(table.name) ?? grok.shell.addTableView(table)) : null; + const tv = plotEmbeddings ? (uiOptions.tableView ?? findTableView(table) ?? grok.shell.addTableView(table)) : null; const doReduce = async () => { const pg = DG.TaskBarProgressIndicator.create( diff --git a/packages/EDA/CHANGELOG.md b/packages/EDA/CHANGELOG.md index 912ea3f95d..83ae4f9a35 100644 --- a/packages/EDA/CHANGELOG.md +++ b/packages/EDA/CHANGELOG.md @@ -1,5 +1,9 @@ # EDA changelog +## v.next + +* 1687: Resolve target table views by DataFrame identity instead of nullable name in dim reduction and MCL flows + ## 1.9.0 (2026-08-26) * Added SVM (classification and regression) diff --git a/packages/EDA/src/package.ts b/packages/EDA/src/package.ts index f803a32d6b..25b411bb39 100644 --- a/packages/EDA/src/package.ts +++ b/packages/EDA/src/package.ts @@ -19,7 +19,7 @@ import {getDbscanWorker} from '@datagrok-libraries/math'; import {DistanceAggregationMethod, DistanceAggregationMethods} from '@datagrok-libraries/ml/src/distance-matrix/types'; import {MultiColumnDimReductionEditor} from '@datagrok-libraries/ml/src/multi-column-dimensionality-reduction/multi-column-dim-reduction-editor'; -import {multiColReduceDimensionality} from +import {multiColReduceDimensionality, findTableView} from '@datagrok-libraries/ml/src/multi-column-dimensionality-reduction/reduce-dimensionality'; import {KnownMetrics} from '@datagrok-libraries/ml/src/typed-metrics'; import {DimReductionMethods} from '@datagrok-libraries/ml/src/multi-column-dimensionality-reduction/types'; @@ -140,7 +140,7 @@ export class PackageFunctions { const resCol = await PackageFunctions.dbScan(df, col1, col2, epsilon, minimumPoints); df.changeColumnType(resCol, 'string'); const colNames = [col1.name, col2.name]; - const tv = grok.shell.tableView(df.name); + const tv = findTableView(df); if (!tv) return; for (const v of tv.viewers) { @@ -278,7 +278,7 @@ export class PackageFunctions { @grok.decorators.param({'type': 'bool', 'options': {'initialValue': 'false', 'description': 'Run the computation on the GPU via WebGPU when available.'}}) useWebGPU: boolean = false, @grok.decorators.param({'type': 'double', 'options': {'initialValue': '2', 'description': 'Inflation factor controlling cluster granularity: higher values yield more, smaller clusters.'}}) inflate: number = 0, @grok.decorators.param({'type': 'int', 'options': {'initialValue': '5', 'description': 'Clusters smaller than this are merged into noise.'}}) minClusterSize: number = 5): Promise { - const tv = grok.shell.tableView(df.name) ?? grok.shell.addTableView(df); + const tv = findTableView(df) ?? grok.shell.addTableView(df); const serializedOptions: string = JSON.stringify({ cols: cols.map((col) => col.name), metrics: metrics, diff --git a/packages/Widgets/CHANGELOG.md b/packages/Widgets/CHANGELOG.md new file mode 100644 index 0000000000..1a5668df82 --- /dev/null +++ b/packages/Widgets/CHANGELOG.md @@ -0,0 +1,5 @@ +# Widgets changelog + +## v.next + +* 1862: Implement the isFiltering contract in RadioButtonFilter, make the no-selection state non-throwing, and give each filter card a unique radio-group name diff --git a/packages/Widgets/src/filters/radio-button-filter.ts b/packages/Widgets/src/filters/radio-button-filter.ts index 5c1836ce45..466cd9dcd7 100644 --- a/packages/Widgets/src/filters/radio-button-filter.ts +++ b/packages/Widgets/src/filters/radio-button-filter.ts @@ -10,8 +10,8 @@ import '../styles/widgets.css'; * 2. Call dataFrame.rows.requestFilter when filtering criteria changes. * */ export class RadioButtonFilter extends DG.Filter { - groupId = 0; - buttonId = 0; + static groupId = 0; + static buttonId = 0; constructor() { super(); @@ -19,13 +19,16 @@ export class RadioButtonFilter extends DG.Filter { this.subs = []; } - get isFiltering() {return true;} + get isFiltering() {return super.isFiltering && this.checkedCategoryId != null;} - get filterSummary() {return this.column!.getCategory(this.checkedCategoryId);} + get filterSummary() { + const categoryIdx = this.checkedCategoryId; + return categoryIdx == null ? '' : this.column!.getCategory(categoryIdx); + } - get checkedCategoryId() { + get checkedCategoryId(): number | null { const checkedInput = this.root.querySelector('input[type=\'radio\']:checked'); - return parseInt(checkedInput!.getAttribute('data-category-id')!); + return checkedInput == null ? null : parseInt(checkedInput.getAttribute('data-category-id')!); } attach(dataFrame: DG.DataFrame) { @@ -51,8 +54,10 @@ export class RadioButtonFilter extends DG.Filter { } applyFilter() { - const indexes = this.column!.getRawData(); const categoryIdx = this.checkedCategoryId; + if (categoryIdx == null) + return; + const indexes = this.column!.getRawData(); const filter = this.dataFrame!.filter; const rowCount = this.dataFrame!.rowCount; @@ -63,12 +68,12 @@ export class RadioButtonFilter extends DG.Filter { } render() { - const name = `radio_${this.groupId++}`; + const name = `radio_${RadioButtonFilter.groupId++}`; $(this.root).empty(); for (let i = 0; i < Math.min(20, this.column!.categories.length); i++) { const category = this.column!.categories[i]; - const id = `rb_${this.buttonId++}`; + const id = `rb_${RadioButtonFilter.buttonId++}`; const radioButon = $(``) .on('change', () => this.dataFrame!.rows.requestFilter()); const label = $(``);