@datagrok/peptides 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/model.ts CHANGED
@@ -14,9 +14,9 @@ import * as CR from './utils/cell-renderer';
14
14
  import {mutationCliffsWidget} from './widgets/mutation-cliffs';
15
15
  import {getDistributionAndStats, getDistributionWidget} from './widgets/distribution';
16
16
  import {getStats, Stats} from './utils/statistics';
17
- import {LogoSummary} from './viewers/logo-summary';
17
+ import {LogoSummaryTable} from './viewers/logo-summary';
18
18
  import {getSettingsDialog} from './widgets/settings';
19
- import {getMonomerWorks} from './package';
19
+ import {_package, getMonomerWorksInstance, getTreeHelperInstance} from './package';
20
20
  import {findMutations} from './utils/algorithms';
21
21
  import {splitAlignedSequences} from '@datagrok-libraries/bio/src/utils/splitter';
22
22
  import {IMonomerLib} from '@datagrok-libraries/bio/src/types';
@@ -24,6 +24,12 @@ import {SeqPalette} from '@datagrok-libraries/bio/src/seq-palettes';
24
24
  import {MonomerWorks} from '@datagrok-libraries/bio/src/monomer-works/monomer-works';
25
25
  import {pickUpPalette, TAGS as bioTAGS} from '@datagrok-libraries/bio/src/utils/macromolecule';
26
26
 
27
+ import {DistanceMatrix} from '@datagrok-libraries/bio/src/trees/distance-matrix';
28
+ import {StringMetricsNames} from '@datagrok-libraries/ml/src/typed-metrics';
29
+ import {ITreeHelper} from '@datagrok-libraries/bio/src/trees/tree-helper';
30
+ import {TAGS as treeTAGS} from '@datagrok-libraries/bio/src/trees';
31
+ import {createDistanceMatrixWorker} from './utils/worker-creator';
32
+
27
33
  export type SummaryStats = {
28
34
  minCount: number, maxCount: number,
29
35
  minMeanDifference: number, maxMeanDifference: number,
@@ -32,25 +38,38 @@ export type SummaryStats = {
32
38
  };
33
39
  export type PositionStats = { [monomer: string]: Stats } & { general: SummaryStats };
34
40
  export type MonomerPositionStats = { [position: string]: PositionStats } & { general: SummaryStats };
41
+ export type ClusterStats = {[cluster: string]: Stats};
42
+ export enum CLUSTER_TYPE {
43
+ ORIGINAL = 'original',
44
+ CUSTOM = 'custom',
45
+ };
46
+ export type ClusterType = `${CLUSTER_TYPE}`;
47
+ export type ClusterTypeStats = {[clusterType in ClusterType]: ClusterStats};
48
+ export enum VIEWER_TYPE {
49
+ MONOMER_POSITION = 'Monomer-Position',
50
+ MOST_POTENT_RESIDUES = 'Most Potent Residues',
51
+ LOGO_SUMMARY_TABLE = 'Logo Summary Table',
52
+ DENDROGRAM = 'Dendrogram',
53
+ };
35
54
 
36
55
  export class PeptidesModel {
37
56
  static modelName = 'peptidesModel';
38
57
 
39
58
  settingsSubject: rxjs.Subject<type.PeptidesSettings> = new rxjs.Subject();
40
59
  _mutatinCliffsSelectionSubject: rxjs.Subject<undefined> = new rxjs.Subject();
41
- _newClusterSubject: rxjs.Subject<undefined> = new rxjs.Subject();
42
- _removeClusterSubject: rxjs.Subject<undefined> = new rxjs.Subject();
43
- _filterChangedSubject: rxjs.Subject<undefined> = new rxjs.Subject();
60
+ _newClusterSubject: rxjs.Subject<undefined> = new rxjs.Subject();
61
+ _removeClusterSubject: rxjs.Subject<undefined> = new rxjs.Subject();
62
+ _filterChangedSubject: rxjs.Subject<undefined> = new rxjs.Subject();
44
63
 
45
64
  _isUpdating: boolean = false;
46
65
  isBitsetChangedInitialized = false;
47
66
  isCellChanging = false;
67
+ isUserChangedSelection = true;
48
68
 
49
69
  df: DG.DataFrame;
50
70
  splitCol!: DG.Column<boolean>;
51
- edf: DG.DataFrame | null = null;
52
71
  _monomerPositionStats?: MonomerPositionStats;
53
- _clusterStats?: {[cluster: string]: Stats};
72
+ _clusterStats?: ClusterTypeStats;
54
73
  _mutationCliffsSelection!: type.PositionToAARList;
55
74
  _invariantMapSelection!: type.PositionToAARList;
56
75
  _logoSummarySelection!: string[];
@@ -58,9 +77,6 @@ export class PeptidesModel {
58
77
  isInitialized = false;
59
78
  _analysisView?: DG.TableView;
60
79
 
61
- isPeptideSpaceChangingBitset = false;
62
- isChangingEdfBitset = false;
63
-
64
80
  monomerMap: { [key: string]: { molfile: string, fullName: string } } = {};
65
81
  monomerLib: IMonomerLib | null = null; // To get monomers from lib(s)
66
82
  monomerWorks: MonomerWorks | null = null; // To get processed monomers
@@ -79,6 +95,9 @@ export class PeptidesModel {
79
95
  _mostPotentResiduesDf?: DG.DataFrame;
80
96
  _matrixDf?: DG.DataFrame;
81
97
  _splitSeqDf?: DG.DataFrame;
98
+ _distanceMatrix!: DistanceMatrix;
99
+ _treeHelper!: ITreeHelper;
100
+ _dm!: DistanceMatrix;
82
101
 
83
102
  private constructor(dataFrame: DG.DataFrame) {
84
103
  this.df = dataFrame;
@@ -91,6 +110,11 @@ export class PeptidesModel {
91
110
  return dataFrame.temp[PeptidesModel.modelName] as PeptidesModel;
92
111
  }
93
112
 
113
+ get treeHelper(): ITreeHelper {
114
+ this._treeHelper ??= getTreeHelperInstance();
115
+ return this._treeHelper;
116
+ }
117
+
94
118
  get monomerPositionDf(): DG.DataFrame {
95
119
  this._monomerPositionDf ??= this.createMonomerPositionDf();
96
120
  return this._monomerPositionDf;
@@ -156,12 +180,12 @@ export class PeptidesModel {
156
180
  this._substitutionsInfo = si;
157
181
  }
158
182
 
159
- get clusterStats(): {[cluster: string]: Stats} {
183
+ get clusterStats(): ClusterTypeStats {
160
184
  this._clusterStats ??= this.calculateClusterStatistics();
161
185
  return this._clusterStats;
162
186
  }
163
187
 
164
- set clusterStats(clusterStats: {[cluster: string]: Stats}) {
188
+ set clusterStats(clusterStats: ClusterTypeStats) {
165
189
  this._clusterStats = clusterStats;
166
190
  }
167
191
 
@@ -226,7 +250,7 @@ export class PeptidesModel {
226
250
  this._invariantMapSelection = selection;
227
251
  this.df.tags[C.TAGS.FILTER] = JSON.stringify(selection);
228
252
  this.isInvariantMapTrigger = true;
229
- this.fireBitsetChanged(false, true);
253
+ this.fireBitsetChanged(true);
230
254
  this.isInvariantMapTrigger = false;
231
255
  this.analysisView.grid.invalidate();
232
256
  }
@@ -305,17 +329,28 @@ export class PeptidesModel {
305
329
  updateVars.add('mutationCliffs');
306
330
  updateVars.add('stats');
307
331
  break;
308
- // case 'columns':
309
- // updateVars.add('grid');
310
- // break;
332
+ // case 'columns':
333
+ // updateVars.add('grid');
334
+ // break;
311
335
  case 'maxMutations':
312
336
  case 'minActivityDelta':
313
337
  updateVars.add('mutationCliffs');
314
338
  break;
339
+ case 'showDendrogram':
340
+ updateVars.add('dendrogram');
341
+ break;
342
+ case 'showLogoSummaryTable':
343
+ updateVars.add('logoSummaryTable');
344
+ break;
345
+ case 'showMonomerPosition':
346
+ updateVars.add('monomerPosition');
347
+ break;
348
+ case 'showMostPotentResidues':
349
+ updateVars.add('mostPotentResidues');
350
+ break;
315
351
  }
316
352
  }
317
353
  this.df.setTag('settings', JSON.stringify(this._settings));
318
- // this.updateDefault();
319
354
  for (const variable of updateVars) {
320
355
  switch (variable) {
321
356
  case 'activity':
@@ -324,8 +359,8 @@ export class PeptidesModel {
324
359
  case 'mutationCliffs':
325
360
  const scaledActivityCol: DG.Column<number> = this.df.getCol(C.COLUMNS_NAMES.ACTIVITY_SCALED);
326
361
  //TODO: set categories ordering the same to share compare indexes instead of strings
327
- const monomerColumns: type.RawColumn[] = this.df.columns.bySemTypeAll(C.SEM_TYPES.MONOMER).map(extractMonomerInfo);
328
- this.substitutionsInfo = findMutations(scaledActivityCol.getRawData(), monomerColumns, this.settings);
362
+ const monomerCols: type.RawColumn[] = this.df.columns.bySemTypeAll(C.SEM_TYPES.MONOMER).map(extractMonomerInfo);
363
+ this.substitutionsInfo = findMutations(scaledActivityCol.getRawData(), monomerCols, this.settings);
329
364
  break;
330
365
  case 'stats':
331
366
  this.monomerPositionStats = this.calculateMonomerPositionStatistics();
@@ -336,6 +371,21 @@ export class PeptidesModel {
336
371
  case 'grid':
337
372
  this.updateGrid();
338
373
  break;
374
+ case 'dendrogram':
375
+ this.settings.showDendrogram ? this.addDendrogram() : this.closeViewer(VIEWER_TYPE.DENDROGRAM);
376
+ break;
377
+ case 'logoSummaryTable':
378
+ this.settings.showLogoSummaryTable ? this.addLogoSummaryTable() :
379
+ this.closeViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE);
380
+ break;
381
+ case 'monomerPosition':
382
+ this.settings.showMonomerPosition ? this.addMonomerPosition() :
383
+ this.closeViewer(VIEWER_TYPE.MONOMER_POSITION);
384
+ break;
385
+ case 'mostPotentResidues':
386
+ this.settings.showMostPotentResidues ? this.addMostPotentResidues() :
387
+ this.closeViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES);
388
+ break;
339
389
  }
340
390
  }
341
391
 
@@ -347,9 +397,6 @@ export class PeptidesModel {
347
397
  const positions = this.splitSeqDf.columns.names();
348
398
  const matrixDf = this.matrixDf
349
399
  .groupBy([C.COLUMNS_NAMES.MONOMER])
350
- // .pivot(C.COLUMNS_NAMES.POSITION)
351
- // .add('values')
352
- // .add('first', C.COLUMNS_NAMES.MEAN_DIFFERENCE, '')
353
400
  .aggregate();
354
401
  for (const pos of positions)
355
402
  matrixDf.columns.addNewString(pos);
@@ -400,7 +447,7 @@ export class PeptidesModel {
400
447
  acc.addTitle(ui.h1(`${filterAndSelectionBs.trueCount} selected rows${filteredTitlePart}`));
401
448
  if (filterAndSelectionBs.anyTrue) {
402
449
  acc.addPane('Actions', () => {
403
- const newViewButton = ui.button('New view', async () => await trueModel.createNewView(),
450
+ const newViewButton = ui.button('New view', () => trueModel.createNewView(),
404
451
  'Creates a new view from current selection');
405
452
  const newCluster = ui.button('New cluster', () => trueModel._newClusterSubject.next(),
406
453
  'Creates a new cluster from selection');
@@ -423,7 +470,8 @@ export class PeptidesModel {
423
470
 
424
471
  this.createScaledCol();
425
472
 
426
- this.initSelections();
473
+ this.initInvariantMapSelection();
474
+ this.initMutationCliffsSelection();
427
475
 
428
476
  this.setWebLogoInteraction();
429
477
  this.webLogoBounds = {};
@@ -437,15 +485,25 @@ export class PeptidesModel {
437
485
  this.postProcessGrids();
438
486
  }
439
487
 
440
- initSelections(): void {
488
+ initInvariantMapSelection(cleanInit = false): void {
441
489
  const tempInvariantMapSelection: type.PositionToAARList = this.invariantMapSelection;
442
- const mutationCliffsSelection: type.PositionToAARList = this.mutationCliffsSelection;
443
490
  const positionColumns = this.splitSeqDf.columns.names();
444
491
  for (const pos of positionColumns) {
445
- tempInvariantMapSelection[pos] ??= [];
446
- mutationCliffsSelection[pos] ??= [];
492
+ if (cleanInit || !tempInvariantMapSelection.hasOwnProperty(pos))
493
+ tempInvariantMapSelection[pos] = [];
447
494
  }
495
+
448
496
  this.invariantMapSelection = tempInvariantMapSelection;
497
+ }
498
+
499
+ initMutationCliffsSelection(cleanInit = false): void {
500
+ const mutationCliffsSelection: type.PositionToAARList = this.mutationCliffsSelection;
501
+ const positionColumns = this.splitSeqDf.columns.names();
502
+ for (const pos of positionColumns) {
503
+ if (cleanInit || !mutationCliffsSelection.hasOwnProperty(pos))
504
+ mutationCliffsSelection[pos] = [];
505
+ }
506
+
449
507
  this.mutationCliffsSelection = mutationCliffsSelection;
450
508
  }
451
509
 
@@ -538,93 +596,96 @@ export class PeptidesModel {
538
596
  return monomerPositionObject;
539
597
  }
540
598
 
541
- getSummaryStats(generalObj: SummaryStats, stats: Stats | null = null, summaryStats: SummaryStats | null = null): void {
599
+ getSummaryStats(genObj: SummaryStats, stats: Stats | null = null, summaryStats: SummaryStats | null = null): void {
542
600
  if (stats == null && summaryStats == null)
543
601
  throw new Error(`MonomerPositionStatsError: either stats or summaryStats must be present`);
544
602
 
545
603
  const possibleMaxCount = stats?.count ?? summaryStats!.maxCount;
546
- generalObj.maxCount ??= possibleMaxCount;
547
- if (generalObj.maxCount < possibleMaxCount)
548
- generalObj.maxCount = possibleMaxCount;
604
+ genObj.maxCount ??= possibleMaxCount;
605
+ if (genObj.maxCount < possibleMaxCount)
606
+ genObj.maxCount = possibleMaxCount;
549
607
 
550
608
  const possibleMinCount = stats?.count ?? summaryStats!.minCount;
551
- generalObj.minCount ??= possibleMinCount;
552
- if (generalObj.minCount > possibleMinCount)
553
- generalObj.minCount = possibleMinCount;
609
+ genObj.minCount ??= possibleMinCount;
610
+ if (genObj.minCount > possibleMinCount)
611
+ genObj.minCount = possibleMinCount;
554
612
 
555
613
  const possibleMaxMeanDifference = stats?.meanDifference ?? summaryStats!.maxMeanDifference;
556
- generalObj.maxMeanDifference ??= possibleMaxMeanDifference;
557
- if (generalObj.maxMeanDifference < possibleMaxMeanDifference)
558
- generalObj.maxMeanDifference = possibleMaxMeanDifference;
614
+ genObj.maxMeanDifference ??= possibleMaxMeanDifference;
615
+ if (genObj.maxMeanDifference < possibleMaxMeanDifference)
616
+ genObj.maxMeanDifference = possibleMaxMeanDifference;
559
617
 
560
618
  const possibleMinMeanDifference = stats?.meanDifference ?? summaryStats!.minMeanDifference;
561
- generalObj.minMeanDifference ??= possibleMinMeanDifference;
562
- if (generalObj.minMeanDifference > possibleMinMeanDifference)
563
- generalObj.minMeanDifference = possibleMinMeanDifference;
619
+ genObj.minMeanDifference ??= possibleMinMeanDifference;
620
+ if (genObj.minMeanDifference > possibleMinMeanDifference)
621
+ genObj.minMeanDifference = possibleMinMeanDifference;
564
622
 
565
623
  const possibleMaxPValue = stats?.pValue ?? summaryStats!.maxPValue;
566
- generalObj.maxPValue ??= possibleMaxPValue;
567
- if (generalObj.maxPValue < possibleMaxPValue)
568
- generalObj.maxPValue = possibleMaxPValue;
624
+ genObj.maxPValue ??= possibleMaxPValue;
625
+ if (genObj.maxPValue < possibleMaxPValue)
626
+ genObj.maxPValue = possibleMaxPValue;
569
627
 
570
628
  const possibleMinPValue = stats?.pValue ?? summaryStats!.minPValue;
571
- generalObj.minPValue ??= possibleMinPValue;
572
- if (generalObj.minPValue > possibleMinPValue)
573
- generalObj.minPValue = possibleMinPValue;
629
+ genObj.minPValue ??= possibleMinPValue;
630
+ if (genObj.minPValue > possibleMinPValue)
631
+ genObj.minPValue = possibleMinPValue;
574
632
 
575
633
  const possibleMaxRatio = stats?.ratio ?? summaryStats!.maxRatio;
576
- generalObj.maxRatio ??= possibleMaxRatio;
577
- if (generalObj.maxRatio < possibleMaxRatio)
578
- generalObj.maxRatio = possibleMaxRatio;
634
+ genObj.maxRatio ??= possibleMaxRatio;
635
+ if (genObj.maxRatio < possibleMaxRatio)
636
+ genObj.maxRatio = possibleMaxRatio;
579
637
 
580
638
  const possibleMinRatio = stats?.ratio ?? summaryStats!.minRatio;
581
- generalObj.minRatio ??= possibleMinRatio;
582
- if (generalObj.minRatio > possibleMinRatio)
583
- generalObj.minRatio = possibleMinRatio;
639
+ genObj.minRatio ??= possibleMinRatio;
640
+ if (genObj.minRatio > possibleMinRatio)
641
+ genObj.minRatio = possibleMinRatio;
584
642
  }
585
643
 
586
- calculateClusterStatistics(): {[cluster: string]: Stats} {
587
- const originalClustersCol = this.df.getCol(this.settings.clustersColumnName!);
588
- const originalClustersColData = originalClustersCol.getRawData();
589
- const originalClustersColCategories = originalClustersCol.categories;
644
+ calculateClusterStatistics(): ClusterTypeStats {
645
+ const origClustCol = this.df.getCol(this.settings.clustersColumnName!);
646
+ const origClustColData = origClustCol.getRawData();
647
+ const origClustColCat = origClustCol.categories;
590
648
 
591
- const customClustersColumnsList = wu(this.customClusters).toArray();
649
+ const customClustColList = wu(this.customClusters).toArray();
650
+ const customClustColDataList = customClustColList.map((v) => v.toList() as boolean[]);
651
+ const customClustColNamesList = customClustColList.map((v) => v.name);
592
652
 
593
- const activityColData: type.RawData = this.df.getCol(C.COLUMNS_NAMES.ACTIVITY_SCALED).getRawData();
594
- const activityColLen = activityColData.length;
595
-
596
- // const resultStats: Stats[] = new Array(originalClustersColCategories.length + customClustersColumnsList.length);
597
- const resultStats: {[cluster: string]: Stats} = {};
598
-
599
- const clusterCount = originalClustersColCategories.length + customClustersColumnsList.length;
600
- for (let clusterIdx = 0; clusterIdx < clusterCount; ++clusterIdx) {
601
- const customClusterIdx = clusterIdx - originalClustersColCategories.length;
602
- const customClusterColData = customClustersColumnsList[customClusterIdx]?.toList();
603
- const isAcitvityIdxValid = customClusterIdx < 0 ?
604
- (i: number) => clusterIdx == originalClustersColData[i] :
605
- (i: number) => customClusterColData[i];
606
-
607
- const mask: boolean[] = new Array(activityColLen);
608
- let trueCount = 0;
609
- for (let maskIdx = 0; maskIdx < activityColLen; ++maskIdx) {
610
- mask[maskIdx] = isAcitvityIdxValid(maskIdx);
611
-
612
- if (mask[maskIdx])
613
- ++trueCount;
614
- }
653
+ const rowCount = this.df.rowCount;
615
654
 
616
- const maskInfo = {
617
- trueCount: trueCount,
618
- falseCount: activityColLen - trueCount,
619
- mask: mask,
620
- };
655
+ const origClustMasks: boolean[][] = Array.from({length: origClustColCat.length},
656
+ () => new Array(rowCount) as boolean[]);
657
+ const customClustMasks: boolean[][] = Array.from({length: customClustColList.length},
658
+ () => new Array(rowCount) as boolean[]);
621
659
 
622
- const stats = getStats(activityColData, maskInfo);
623
- const clusterName = customClusterIdx < 0 ? originalClustersColCategories[clusterIdx] :
624
- customClustersColumnsList[customClusterIdx].name;
625
- resultStats[clusterName] = stats;
660
+ // get original cluster masks in one pass
661
+ // complexity is O(N * (M + 1)) where N is the number of rows and M is the number of custom clusters
662
+ for (let rowIdx = 0; rowIdx < rowCount; ++rowIdx) {
663
+ origClustMasks[origClustColData[rowIdx]][rowIdx] = true;
664
+ for (let customClustIdx = 0; customClustIdx < customClustColList.length; ++customClustIdx)
665
+ customClustMasks[customClustIdx][rowIdx] = customClustColDataList[customClustIdx][rowIdx];
626
666
  }
627
667
 
668
+ const activityColData: type.RawData = this.df.getCol(C.COLUMNS_NAMES.ACTIVITY_SCALED).getRawData();
669
+
670
+ const origClustStats: ClusterStats = {};
671
+ const customClustStats: ClusterStats = {};
672
+
673
+ for (let clustType = 0; clustType < 2; ++clustType) {
674
+ const masks = clustType == 0 ? origClustMasks : customClustMasks;
675
+ const clustNames = clustType == 0 ? origClustColCat : customClustColNamesList;
676
+ const resultStats = clustType == 0 ? origClustStats : customClustStats;
677
+ for (let maskIdx = 0; maskIdx < masks.length; ++maskIdx) {
678
+ const mask = masks[maskIdx];
679
+ const trueCount = mask.filter((v) => v).length;
680
+ const maskInfo = {trueCount: trueCount, falseCount: rowCount - trueCount, mask: mask};
681
+ const stats = getStats(activityColData, maskInfo);
682
+ resultStats[clustNames[maskIdx]] = stats;
683
+ }
684
+ }
685
+
686
+ const resultStats = {} as ClusterTypeStats;
687
+ resultStats[CLUSTER_TYPE.ORIGINAL] = origClustStats;
688
+ resultStats[CLUSTER_TYPE.CUSTOM] = customClustStats;
628
689
  return resultStats;
629
690
  }
630
691
 
@@ -760,12 +821,13 @@ export class PeptidesModel {
760
821
  return 0;
761
822
  }).filter((v) => v != 'general');
762
823
 
763
- this.webLogoBounds[col.name] =
764
- CR.drawLogoInBounds(ctx, bounds, stats, sortedStatsOrder, this.df.rowCount, this.cp, this.headerSelectedMonomers[col.name]);
824
+ this.webLogoBounds[col.name] = CR.drawLogoInBounds(ctx, bounds, stats, sortedStatsOrder, this.df.rowCount,
825
+ this.cp, this.headerSelectedMonomers[col.name]);
765
826
  gcArgs.preventDefault();
766
827
  }
767
828
  } catch (e) {
768
- console.warn(`PeptidesHeaderLogoError: couldn't render WebLogo for column \`${col!.name}\`. See original error below.`);
829
+ console.warn(`PeptidesHeaderLogoError: couldn't render WebLogo for column \`${col!.name}\`. ` +
830
+ `See original error below.`);
769
831
  console.warn(e);
770
832
  } finally {
771
833
  ctx.restore();
@@ -787,7 +849,7 @@ export class PeptidesModel {
787
849
  const tooltipElements: HTMLDivElement[] = [];
788
850
  const monomerName = aar.toLowerCase();
789
851
 
790
- const mw = getMonomerWorks();
852
+ const mw = getMonomerWorksInstance();
791
853
  const mol = mw?.getCappedRotatedMonomer('PEPTIDE', aar);
792
854
 
793
855
  if (mol) {
@@ -830,67 +892,13 @@ export class PeptidesModel {
830
892
  const distributionTable = DG.DataFrame.fromColumns([activityCol, splitCol]);
831
893
  const das = getDistributionAndStats(distributionTable, stats, `${position} : ${aar}`, 'Other', true);
832
894
  const resultMap: { [key: string]: any } = {...das.tableMap, ...colResults};
833
- const distroStatsElem = wrapDistroAndStatsDefault(das.labels, das.histRoot, resultMap);
895
+ const distroStatsElem = wrapDistroAndStatsDefault(das.labels, das.histRoot, resultMap, true);
834
896
 
835
897
  ui.tooltip.show(distroStatsElem, x, y);
836
898
 
837
899
  return distroStatsElem;
838
900
  }
839
901
 
840
- showTooltipCluster(cluster: number, x: number, y: number, clusterName: string): HTMLDivElement | null {
841
- const bs = this.df.filter;
842
- const filteredDf = bs.anyFalse ? this.df.clone(bs) : this.df;
843
-
844
- const activityCol = filteredDf.getCol(C.COLUMNS_NAMES.ACTIVITY_SCALED);
845
- const activityColData = activityCol.getRawData();
846
- //TODO: use bitset instead of splitCol
847
- const clusterCol = filteredDf.getCol(this.settings.clustersColumnName!);
848
- const clusterColData = clusterCol.getRawData();
849
- let splitCol = DG.Column.bool(C.COLUMNS_NAMES.SPLIT_COL, activityCol.length);
850
- const indexes: number[] = [];
851
- splitCol.init((i) => {
852
- const result = clusterColData[i] == cluster;
853
- if (result)
854
- indexes.push(i);
855
- return result;
856
- });
857
- if (splitCol.max == 0)
858
- splitCol = filteredDf.getCol(clusterName);
859
- const distDf = DG.DataFrame.fromColumns([activityCol, splitCol]);
860
-
861
- let stats: Stats;
862
- if (bs.anyFalse) {
863
- const trueCount = splitCol.stats.sum;
864
- const maskInfo = {
865
- trueCount: trueCount,
866
- falseCount: activityColData.length - trueCount,
867
- mask: splitCol.toList() as boolean[],
868
- };
869
- stats = getStats(activityColData, maskInfo);
870
- } else
871
- stats = this.clusterStats[clusterName];
872
-
873
- if (!stats.count)
874
- return null;
875
-
876
- const colResults: {[colName: string]: number} = {};
877
- for (const [col, agg] of Object.entries(this.settings.columns || {})) {
878
- const currentCol = filteredDf.getCol(col);
879
- const currentColData = currentCol.getRawData();
880
- const tempCol = DG.Column.float('', indexes.length);
881
- tempCol.init((i) => currentColData[indexes[i]]);
882
- colResults[`${agg}(${col})`] = tempCol.stats[agg as keyof DG.Stats] as number;
883
- }
884
-
885
- const das = getDistributionAndStats(distDf, stats, `Cluster: ${clusterName}`, 'Other', true, splitCol.name);
886
- const resultMap: {[key: string]: any} = {...das.tableMap, ...colResults};
887
- const tooltip = wrapDistroAndStatsDefault(das.labels, das.histRoot, resultMap, true);
888
-
889
- ui.tooltip.show(tooltip, x, y);
890
-
891
- return tooltip;
892
- }
893
-
894
902
  modifyMonomerPositionSelection(aar: string, position: string, isInvariantMapSelection: boolean): void {
895
903
  const tempSelection = isInvariantMapSelection ? this.invariantMapSelection : this.mutationCliffsSelection;
896
904
  const tempSelectionAt = tempSelection[position];
@@ -925,44 +933,48 @@ export class PeptidesModel {
925
933
  const filter = this.df.filter;
926
934
  const clusterCol = this.df.col(this.settings.clustersColumnName!);
927
935
 
928
- const changeSelectionBitset = (currentBitset: DG.BitSet): void => {
929
- const clusterColCategories = clusterCol?.categories;
930
- const clusterColData = clusterCol?.getRawData();
931
-
932
- const edfSelection = this.edf?.selection;
933
- if (this.isPeptideSpaceChangingBitset) {
934
- if (edfSelection == null)
935
- return;
936
-
937
- currentBitset.init((i) => edfSelection.get(i) || false, false);
938
- return;
939
- }
936
+ const changeSelectionBitset = (currentBitset: DG.BitSet, posList: type.RawColumn[], clustColCat: string[],
937
+ clustColData: type.RawData, customClust: {[key: string]: boolean[]}): void => {
938
+ const getBitAt = (i: number): boolean => {
939
+ for (const posRawCol of posList) {
940
+ if (this.mutationCliffsSelection[posRawCol.name].includes(posRawCol.cat![posRawCol.rawData[i]]))
941
+ return true;
942
+ }
940
943
 
941
- const updateEdfSelection = (): void => {
942
- this.isChangingEdfBitset = true;
943
- edfSelection?.copyFrom(currentBitset);
944
- this.isChangingEdfBitset = false;
945
- };
944
+ const currentOrigClust = clustColCat[clustColData[i]];
945
+ if (typeof currentOrigClust === undefined)
946
+ return false;
946
947
 
947
- const positionList = Object.keys(this.mutationCliffsSelection);
948
+ for (const clust of this.logoSummarySelection) {
949
+ if (clust === currentOrigClust)
950
+ return true;
948
951
 
949
- //TODO: move out
950
- const getBitAt = (i: number): boolean => {
951
- for (const position of positionList) {
952
- const positionCol: DG.Column<string> = this.df.getCol(position);
953
- if (this.mutationCliffsSelection[position].includes(positionCol.get(i)!))
952
+ if (Object.hasOwn(customClust, clust) && customClust[clust][i] === true)
954
953
  return true;
955
954
  }
956
- return (typeof clusterColData != 'undefined' && typeof clusterColCategories != 'undefined' &&
957
- this.logoSummarySelection.includes(clusterColCategories![clusterColData![i]])) ||
958
- this.logoSummarySelection.some((cluster) => this.df.columns.contains(cluster) && this.df.get(cluster, i));
955
+
956
+ return false;
959
957
  };
960
958
  currentBitset.init((i) => getBitAt(i), false);
961
-
962
- updateEdfSelection();
963
959
  };
964
960
 
965
- selection.onChanged.subscribe(() => changeSelectionBitset(selection));
961
+ selection.onChanged.subscribe(() => {
962
+ if (this.isUserChangedSelection)
963
+ return;
964
+
965
+ const positionList: type.RawColumn[] = Object.keys(this.mutationCliffsSelection).map((pos) => {
966
+ const posCol = this.df.getCol(pos);
967
+ return {name: pos, cat: posCol.categories, rawData: posCol.getRawData()};
968
+ });
969
+
970
+ const clustColCat = clusterCol?.categories ?? [];
971
+ const clustColData = clusterCol?.getRawData() ?? new Int32Array(0);
972
+ const customClust: {[key: string]: boolean[]} = {};
973
+ for (const clust of this.customClusters)
974
+ customClust[clust.name] = clust.toList();
975
+
976
+ changeSelectionBitset(selection, positionList, clustColCat, clustColData, customClust);
977
+ });
966
978
 
967
979
  filter.onChanged.subscribe(() => {
968
980
  const positionList = Object.keys(this.invariantMapSelection);
@@ -988,8 +1000,8 @@ export class PeptidesModel {
988
1000
  this.isBitsetChangedInitialized = true;
989
1001
  }
990
1002
 
991
- fireBitsetChanged(isPeptideSpaceSource: boolean = false, fireFilterChanged: boolean = false): void {
992
- this.isPeptideSpaceChangingBitset = isPeptideSpaceSource;
1003
+ fireBitsetChanged(fireFilterChanged: boolean = false): void {
1004
+ this.isUserChangedSelection = false;
993
1005
  this.df.selection.fireChanged();
994
1006
  if (fireFilterChanged)
995
1007
  this.df.filter.fireChanged();
@@ -1002,8 +1014,7 @@ export class PeptidesModel {
1002
1014
  for (const pane of acc.panes)
1003
1015
  pane.expanded = true;
1004
1016
  }
1005
-
1006
- this.isPeptideSpaceChangingBitset = false;
1017
+ this.isUserChangedSelection = true;
1007
1018
  }
1008
1019
 
1009
1020
  postProcessGrids(): void {
@@ -1027,6 +1038,41 @@ export class PeptidesModel {
1027
1038
  }
1028
1039
  }
1029
1040
 
1041
+ closeViewer(viewerType: VIEWER_TYPE): void {
1042
+ const viewer = this.findViewer(viewerType);
1043
+ viewer?.detach();
1044
+ viewer?.close();
1045
+ }
1046
+
1047
+ findViewerNode(viewerType: VIEWER_TYPE): DG.DockNode | null {
1048
+ for (const node of this.analysisView.dockManager.rootNode.children) {
1049
+ if (node.container.containerElement.innerHTML.includes(viewerType))
1050
+ return node;
1051
+ }
1052
+ return null;
1053
+ }
1054
+
1055
+ async addDendrogram(): Promise<void> {
1056
+ const pi = DG.TaskBarProgressIndicator.create('Calculating distance matrix...');
1057
+ try {
1058
+ const pepColValues: string[] = this.df.getCol(this.settings.sequenceColumnName!).toList();
1059
+ this._dm ??= new DistanceMatrix(await createDistanceMatrixWorker(pepColValues, StringMetricsNames.Levenshtein));
1060
+ const leafCol = this.df.col('~leaf-id') ?? this.df.columns.addNewString('~leaf-id').init((i) => i.toString());
1061
+ const treeNode = await this.treeHelper.hierarchicalClusteringByDistance(this._dm, 'ward');
1062
+
1063
+ this.df.setTag(treeTAGS.NEWICK, this.treeHelper.toNewick(treeNode));
1064
+ const leafOrdering = this.treeHelper.getLeafList(treeNode).map((leaf) => parseInt(leaf.name));
1065
+ this.analysisView.grid.setRowOrder(leafOrdering);
1066
+ const dendrogramViewer = await this.df.plot.fromType('Dendrogram', {nodeColumnName: leafCol.name}) as DG.JsViewer;
1067
+
1068
+ this.analysisView.dockManager.dock(dendrogramViewer, DG.DOCK_TYPE.LEFT, null, 'Dendrogram', 0.25);
1069
+ } catch (e) {
1070
+ _package.logger.error(e as string);
1071
+ } finally {
1072
+ pi.close();
1073
+ }
1074
+ }
1075
+
1030
1076
  getSplitColValueAt(index: number, aar: string, position: string, aarLabel: string): string {
1031
1077
  const currentAAR = this.df.get(position, index) as string;
1032
1078
  return currentAAR === aar ? aarLabel : C.CATEGORIES.OTHER;
@@ -1051,30 +1097,50 @@ export class PeptidesModel {
1051
1097
  const settingsButton = ui.iconFA('wrench', () => getSettingsDialog(this), 'Peptides analysis settings');
1052
1098
  this.analysisView.setRibbonPanels([[settingsButton]], false);
1053
1099
  this.isRibbonSet = true;
1100
+ grok.events.onResetFilterRequest.subscribe(() => {
1101
+ this.isInvariantMapTrigger = true;
1102
+ this.initInvariantMapSelection(true);
1103
+ this.isInvariantMapTrigger = false;
1104
+ });
1054
1105
  }
1055
1106
 
1056
1107
  this.updateGrid();
1057
- this.fireBitsetChanged(false, true);
1108
+ this.fireBitsetChanged(true);
1058
1109
  this.analysisView.grid.invalidate();
1059
1110
  }
1060
1111
 
1061
- async addViewers(): Promise<void> {
1062
- const dockManager = this.analysisView.dockManager;
1063
- const dfPlt = this.df.plot;
1064
-
1065
- const mutationCliffsViewer = await dfPlt.fromType('peptide-sar-viewer') as MonomerPosition;
1066
- const mostPotentResiduesViewer = await dfPlt.fromType('peptide-sar-viewer-vertical') as MostPotentResiduesViewer;
1067
- if (this.settings.clustersColumnName)
1068
- await this.addLogoSummaryTableViewer();
1112
+ findViewer(viewerType: VIEWER_TYPE): DG.Viewer | null {
1113
+ return wu(this.analysisView.viewers).find((v) => v.type === viewerType) || null;
1114
+ }
1069
1115
 
1070
- const mcNode = dockManager.dock(mutationCliffsViewer, DG.DOCK_TYPE.DOWN, null, mutationCliffsViewer.name);
1116
+ async addLogoSummaryTable(): Promise<void> {
1117
+ this.closeViewer(VIEWER_TYPE.MONOMER_POSITION);
1118
+ this.closeViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES);
1119
+ const logoSummaryTable = await this.df.plot.fromType(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable;
1120
+ this.analysisView.dockManager.dock(logoSummaryTable, DG.DOCK_TYPE.RIGHT, null, VIEWER_TYPE.LOGO_SUMMARY_TABLE);
1121
+ if (this.settings.showMonomerPosition)
1122
+ await this.addMonomerPosition();
1123
+ if (this.settings.showMostPotentResidues)
1124
+ await this.addMostPotentResidues();
1125
+ }
1071
1126
 
1072
- dockManager.dock(mostPotentResiduesViewer, DG.DOCK_TYPE.RIGHT, mcNode, mostPotentResiduesViewer.name, 0.3);
1127
+ async addMonomerPosition(): Promise<void> {
1128
+ const monomerPosition = await this.df.plot.fromType(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition;
1129
+ const mostPotentResidues = this.findViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResiduesViewer | null;
1130
+ const dm = this.analysisView.dockManager;
1131
+ const [dockType, refNode, ratio] = mostPotentResidues === null ? [DG.DOCK_TYPE.DOWN, null, undefined] :
1132
+ [DG.DOCK_TYPE.LEFT, this.findViewerNode(VIEWER_TYPE.MOST_POTENT_RESIDUES), 0.7];
1133
+ dm.dock(monomerPosition, dockType, refNode, VIEWER_TYPE.MONOMER_POSITION, ratio);
1073
1134
  }
1074
1135
 
1075
- async addLogoSummaryTableViewer(): Promise<void> {
1076
- const logoSummary = await this.df.plot.fromType('logo-summary-viewer') as LogoSummary;
1077
- this.analysisView.dockManager.dock(logoSummary, DG.DOCK_TYPE.RIGHT, null, 'Logo Summary Table');
1136
+ async addMostPotentResidues(): Promise<void> {
1137
+ const mostPotentResidues =
1138
+ await this.df.plot.fromType(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResiduesViewer;
1139
+ const monomerPosition = this.findViewer(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition | null;
1140
+ const dm = this.analysisView.dockManager;
1141
+ const [dockType, refNode, ratio] = monomerPosition === null ? [DG.DOCK_TYPE.DOWN, null, undefined] :
1142
+ [DG.DOCK_TYPE.RIGHT, this.findViewerNode(VIEWER_TYPE.MONOMER_POSITION), 0.3];
1143
+ dm.dock(mostPotentResidues, dockType, refNode, VIEWER_TYPE.MOST_POTENT_RESIDUES, ratio);
1078
1144
  }
1079
1145
 
1080
1146
  addNewCluster(clusterName: string): void {
@@ -1084,7 +1150,7 @@ export class PeptidesModel {
1084
1150
  this.analysisView.grid.col(newClusterCol.name)!.visible = false;
1085
1151
  }
1086
1152
 
1087
- async createNewView(): Promise<void> {
1153
+ createNewView(): void {
1088
1154
  const rowMask = this.getCompoundBitest();
1089
1155
  if (!rowMask.anyTrue)
1090
1156
  return grok.shell.warning('Cannot create a new view, there are no visible selected rows in your dataset');