@datagrok/bio 2.11.41 → 2.12.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/detectors.js +11 -11
  4. package/dist/36.js +1 -1
  5. package/dist/36.js.map +1 -1
  6. package/dist/413.js +1 -1
  7. package/dist/413.js.map +1 -1
  8. package/dist/590.js +1 -1
  9. package/dist/590.js.map +1 -1
  10. package/dist/709.js +1 -1
  11. package/dist/709.js.map +1 -1
  12. package/dist/895.js +1 -1
  13. package/dist/895.js.map +1 -1
  14. package/dist/package-test.js +2 -2
  15. package/dist/package-test.js.map +1 -1
  16. package/dist/package.js +2 -2
  17. package/dist/package.js.map +1 -1
  18. package/package.json +10 -10
  19. package/src/analysis/sequence-activity-cliffs.ts +9 -9
  20. package/src/analysis/sequence-diversity-viewer.ts +3 -3
  21. package/src/analysis/sequence-search-base-viewer.ts +2 -2
  22. package/src/analysis/sequence-similarity-viewer.ts +10 -10
  23. package/src/analysis/sequence-space.ts +26 -23
  24. package/src/calculations/monomerLevelMols.ts +13 -11
  25. package/src/const.ts +5 -0
  26. package/src/package.ts +8 -8
  27. package/src/tests/WebLogo-layout-tests.ts +5 -2
  28. package/src/tests/WebLogo-positions-test.ts +20 -16
  29. package/src/tests/bio-tests.ts +19 -7
  30. package/src/tests/converters-test.ts +4 -4
  31. package/src/tests/detectors-benchmark-tests.ts +5 -5
  32. package/src/tests/detectors-tests.ts +13 -13
  33. package/src/tests/fasta-export-tests.ts +10 -4
  34. package/src/tests/mm-distance-tests.ts +10 -10
  35. package/src/tests/msa-tests.ts +8 -15
  36. package/src/tests/renderers-monomer-placer.ts +3 -3
  37. package/src/tests/renderers-test.ts +6 -8
  38. package/src/tests/splitters-test.ts +14 -13
  39. package/src/tests/to-atomic-level-tests.ts +2 -2
  40. package/src/tests/units-handler-get-region.ts +4 -4
  41. package/src/tests/units-handler-splitted-tests.ts +19 -17
  42. package/src/tests/units-handler-tests.ts +32 -32
  43. package/src/utils/cell-renderer.ts +40 -34
  44. package/src/utils/check-input-column.ts +5 -5
  45. package/src/utils/context-menu.ts +9 -6
  46. package/src/utils/convert.ts +9 -9
  47. package/src/utils/get-region-func-editor.ts +11 -11
  48. package/src/utils/get-region.ts +10 -12
  49. package/src/utils/macromolecule-column-widget.ts +9 -5
  50. package/src/utils/monomer-lib/library-file-manager/event-manager.ts +1 -1
  51. package/src/utils/multiple-sequence-alignment-ui.ts +6 -6
  52. package/src/utils/pepsea.ts +1 -0
  53. package/src/utils/poly-tool/transformation.ts +3 -3
  54. package/src/utils/save-as-fasta.ts +14 -15
  55. package/src/utils/sequence-to-mol.ts +4 -4
  56. package/src/viewers/web-logo-viewer.ts +95 -110
  57. package/src/widgets/bio-substructure-filter.ts +3 -3
  58. package/src/widgets/composition-analysis-widget.ts +26 -19
@@ -6,7 +6,7 @@ import $ from 'cash-dom';
6
6
  import wu from 'wu';
7
7
  import {fromEvent, Observable, Subject, Unsubscribable} from 'rxjs';
8
8
 
9
- import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
9
+ import {SeqHandler} from '@datagrok-libraries/bio/src/utils/seq-handler';
10
10
  import {SeqPalette} from '@datagrok-libraries/bio/src/seq-palettes';
11
11
  import {
12
12
  monomerToShort, pickUpPalette, pickUpSeqCol, TAGS as bioTAGS, positionSeparator
@@ -17,8 +17,7 @@ import {
17
17
  } from '@datagrok-libraries/bio/src/viewers/web-logo';
18
18
  import {errorToConsole} from '@datagrok-libraries/utils/src/to-console';
19
19
  import {intToHtmlA} from '@datagrok-libraries/utils/src/color';
20
- import {ISeqSplitted} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
21
- import {errInfo} from '@datagrok-libraries/bio/src/utils/err-info';
20
+ import {GAP_SYMBOL, ISeqSplitted} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
22
21
  import {testEvent} from '@datagrok-libraries/utils/src/test';
23
22
  import {PromiseSyncer} from '@datagrok-libraries/bio/src/utils/syncer';
24
23
 
@@ -81,12 +80,6 @@ export class PositionMonomerInfo {
81
80
  }
82
81
 
83
82
  export class PositionInfo {
84
- /** Position in sequence */
85
- public readonly pos: number;
86
-
87
- /** Position name from column tag*/
88
- public readonly name: string;
89
-
90
83
  private readonly _label: string | undefined;
91
84
  public get label(): string { return !!this._label ? this._label : this.name; }
92
85
 
@@ -105,11 +98,12 @@ export class PositionInfo {
105
98
  * @param {number} rowCount Count of elements in column
106
99
  * @param {number} sumForHeightCalc Sum of all monomer counts for height calculation
107
100
  */
108
- constructor(pos: number, name: string, freqs?: { [m: string]: PositionMonomerInfo },
101
+ constructor(
102
+ /** Position in sequence */ public readonly pos: number,
103
+ /** Position name from column tag*/ public readonly name: string,
104
+ freqs?: { [m: string]: PositionMonomerInfo },
109
105
  options?: { sumRowCount?: number, sumValueForHeight?: number, label?: string }
110
106
  ) {
111
- this.pos = pos;
112
- this.name = name;
113
107
  this._freqs = freqs ?? {};
114
108
 
115
109
  if (options?.sumRowCount) this.sumRowCount = options.sumRowCount;
@@ -172,8 +166,7 @@ export class PositionInfo {
172
166
  }
173
167
  }
174
168
 
175
- calcScreen(
176
- isGap: (m: string) => boolean, posIdx: number, firstVisiblePosIdx: number,
169
+ calcScreen(posIdx: number, firstVisiblePosIdx: number,
177
170
  absoluteMaxHeight: number, heightMode: PositionHeight, alphabetSizeLog: number,
178
171
  positionWidthWithMargin: number, positionWidth: number, dpr: number, positionLabelsHeight: number
179
172
  ): void {
@@ -184,13 +177,13 @@ export class PositionInfo {
184
177
 
185
178
  const entries = Object.entries(this._freqs)
186
179
  .sort((a, b) => {
187
- if (!isGap(a[0]) && !isGap(b[0]))
180
+ if (a[0] !== GAP_SYMBOL && b[0] !== GAP_SYMBOL)
188
181
  return b[1].value - a[1].value;
189
- else if (isGap(a[0]) && isGap(b[0]))
182
+ else if (a[0] === GAP_SYMBOL && b[0] === GAP_SYMBOL)
190
183
  return 0;
191
- else if (isGap(a[0]))
184
+ else if (a[0] === GAP_SYMBOL)
192
185
  return -1;
193
- else /* (isGap(b[0])) */
186
+ else /* (b[0] === GAP_SYMBOL) */
194
187
  return +1;
195
188
  });
196
189
  for (const [_m, pmi] of entries) {
@@ -204,11 +197,10 @@ export class PositionInfo {
204
197
  }
205
198
 
206
199
  render(g: CanvasRenderingContext2D,
207
- isGap: (m: string) => boolean,
208
200
  fontStyle: string, uppercaseLetterAscent: number, uppercaseLetterHeight: number, cp: SeqPalette
209
201
  ) {
210
202
  for (const [monomer, pmInfo] of Object.entries(this._freqs)) {
211
- if (!isGap(monomer)) {
203
+ if (monomer !== GAP_SYMBOL) {
212
204
  const monomerTxt = monomerToShort(monomer, 5);
213
205
  const b = pmInfo.bounds!;
214
206
  const left = b.left;
@@ -240,6 +232,8 @@ export class PositionInfo {
240
232
  }
241
233
 
242
234
  buildCompositionTable(palette: SeqPalette): HTMLTableElement {
235
+ if ('-' in this._freqs)
236
+ throw new Error(`Unexpected monomer symbol '-'.`);
243
237
  return buildCompositionTable(palette,
244
238
  Object.assign({}, ...Object.entries(this._freqs)
245
239
  .map(([m, pmi]) => ({[m]: pmi.rowCount})))
@@ -305,7 +299,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
305
299
 
306
300
  private viewed: boolean = false;
307
301
 
308
- private unitsHandler: UnitsHandler | null;
302
+ private seqHandler: SeqHandler | null;
309
303
  private initialized: boolean = false;
310
304
 
311
305
  // private readonly colorScheme: ColorScheme = ColorSchemes[NucleotidesWebLogo.residuesSet];
@@ -375,7 +369,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
375
369
  }
376
370
 
377
371
  public get positionMarginValue(): number {
378
- if (this.positionMarginState === PositionMarginStates.AUTO && this.unitsHandler!.getAlphabetIsMultichar() === true)
372
+ if (this.positionMarginState === PositionMarginStates.AUTO && this.seqHandler!.getAlphabetIsMultichar() === true)
379
373
  return this.positionMargin;
380
374
  else if (this.positionMarginState === PositionMarginStates.ON)
381
375
  return this.positionMargin;
@@ -387,7 +381,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
387
381
  super();
388
382
 
389
383
  this.textBaseline = 'top';
390
- this.unitsHandler = null;
384
+ this.seqHandler = null;
391
385
 
392
386
  // -- Data --
393
387
  this.sequenceColumnName = this.string(PROPS.sequenceColumnName, defaults.sequenceColumnName,
@@ -591,7 +585,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
591
585
  }
592
586
  if (this.seqCol) {
593
587
  try {
594
- this.unitsHandler = UnitsHandler.getOrCreate(this.seqCol);
588
+ this.seqHandler = SeqHandler.forColumn(this.seqCol);
595
589
 
596
590
  this.palette = pickUpPalette(this.seqCol);
597
591
  this.render(WlRenderLevel.Freqs, 'updateSeqCol()');
@@ -602,7 +596,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
602
596
  throw err;
603
597
  }
604
598
  if (!this.seqCol) {
605
- this.unitsHandler = null;
599
+ this.seqHandler = null;
606
600
  this.positionNames = [];
607
601
  this.positionLabels = [];
608
602
  this.startPosition = -1;
@@ -616,13 +610,13 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
616
610
  private getFilter(): DG.BitSet {
617
611
  let dfFilterRes: DG.BitSet;
618
612
  switch (this.filterSource) {
619
- case FilterSources.Filtered:
620
- dfFilterRes = this.dataFrame.filter;
621
- break;
613
+ case FilterSources.Filtered:
614
+ dfFilterRes = this.dataFrame.filter;
615
+ break;
622
616
 
623
- case FilterSources.Selected:
624
- dfFilterRes = this.dataFrame.selection.trueCount === 0 ? this.dataFrame.filter : this.dataFrame.selection;
625
- break;
617
+ case FilterSources.Selected:
618
+ dfFilterRes = this.dataFrame.selection.trueCount === 0 ? this.dataFrame.filter : this.dataFrame.selection;
619
+ break;
626
620
  }
627
621
  return dfFilterRes;
628
622
  }
@@ -816,45 +810,43 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
816
810
  super.onPropertyChanged(property);
817
811
 
818
812
  switch (property.name) {
819
- case PROPS.sequenceColumnName:
820
- this.updateSeqCol();
821
- break;
822
- case PROPS.sequenceColumnName:
823
- case PROPS.startPositionName:
824
- case PROPS.endPositionName:
825
- case PROPS.filterSource:
826
- case PROPS.shrinkEmptyTail:
827
- case PROPS.skipEmptyPositions:
828
- case PROPS.positionHeight: {
829
- this.render(WlRenderLevel.Freqs, `onPropertyChanged( ${property.name} )`);
830
- break;
831
- }
832
-
833
- case PROPS.valueColumnName:
834
- case PROPS.valueAggrType: {
835
- this.render(WlRenderLevel.Freqs, `onPropertyChanged( ${property.name} )`);
836
- break;
837
- }
813
+ case PROPS.sequenceColumnName:
814
+ this.updateSeqCol();
815
+ break;
816
+ case PROPS.sequenceColumnName:
817
+ case PROPS.startPositionName:
818
+ case PROPS.endPositionName:
819
+ case PROPS.filterSource:
820
+ case PROPS.shrinkEmptyTail:
821
+ case PROPS.skipEmptyPositions:
822
+ case PROPS.positionHeight: {
823
+ this.render(WlRenderLevel.Freqs, `onPropertyChanged( ${property.name} )`);
824
+ break;
825
+ }
826
+ case PROPS.valueColumnName:
827
+ case PROPS.valueAggrType: {
828
+ this.render(WlRenderLevel.Freqs, `onPropertyChanged( ${property.name} )`);
829
+ break;
830
+ }
831
+ case PROPS.minHeight:
832
+ case PROPS.maxHeight:
833
+ case PROPS.positionWidth:
834
+ case PROPS.showPositionLabels:
835
+ case PROPS.fixWidth:
836
+ case PROPS.fitArea:
837
+ case PROPS.horizontalAlignment:
838
+ case PROPS.verticalAlignment:
839
+ case PROPS.positionMargin:
840
+ case PROPS.positionMarginState: {
838
841
  // this.positionWidth obtains a new value
839
842
  // this.updateSlider updates this._positionWidth
840
- case PROPS.minHeight:
841
- case PROPS.maxHeight:
842
- case PROPS.positionWidth:
843
- case PROPS.showPositionLabels:
844
- case PROPS.fixWidth:
845
- case PROPS.fitArea:
846
- case PROPS.horizontalAlignment:
847
- case PROPS.verticalAlignment:
848
- case PROPS.positionMargin:
849
- case PROPS.positionMarginState: {
850
- this.render(WlRenderLevel.Layout, `onPropertyChanged(${property.name})`);
851
- break;
852
- }
853
-
854
- case PROPS.backgroundColor: {
855
- this.render(WlRenderLevel.Render, `onPropertyChanged(${property.name})`);
856
- break;
857
- }
843
+ this.render(WlRenderLevel.Layout, `onPropertyChanged(${property.name})`);
844
+ break;
845
+ }
846
+ case PROPS.backgroundColor: {
847
+ this.render(WlRenderLevel.Render, `onPropertyChanged(${property.name})`);
848
+ break;
849
+ }
858
850
  }
859
851
  }
860
852
 
@@ -928,8 +920,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
928
920
  protected _removeEmptyPositions() {
929
921
  if (this.skipEmptyPositions) {
930
922
  this.positions = wu(this.positions).filter((pi) => {
931
- const gapSymbol: string = this.unitsHandler!.defaultGapSymbol;
932
- return !pi.hasMonomer(gapSymbol) || pi.getFreq(gapSymbol).rowCount !== pi.sumRowCount;
923
+ return !pi.hasMonomer(GAP_SYMBOL) || pi.getFreq(GAP_SYMBOL).rowCount !== pi.sumRowCount;
933
924
  }).toArray();
934
925
  }
935
926
  }
@@ -962,9 +953,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
962
953
  // region updatePositions
963
954
 
964
955
  const dfFilter = this.getFilter();
965
- const maxLength: number = dfFilter.trueCount === 0 ? this.unitsHandler!.maxLength :
966
- wu.enumerate(this.unitsHandler!.splitted).map(([mList, rowI]) => {
967
- return dfFilter.get(rowI) && !!mList ? mList.length : 0;
956
+ const maxLength: number = dfFilter.trueCount === 0 ? this.seqHandler!.maxLength :
957
+ wu.count(0).take(this.seqHandler!.length).map((rowIdx) => {
958
+ const mList = this.seqHandler!.getSplitted(rowIdx);
959
+ return dfFilter.get(rowIdx) && !!mList ? mList.length : 0;
968
960
  }).reduce((max, l) => Math.max(max, l), 0);
969
961
 
970
962
  /** positionNames and positionLabel can be set up through the column's tags only */
@@ -985,7 +977,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
985
977
  // endregion updatePositions
986
978
 
987
979
  const length: number = this.startPosition <= this.endPosition ? this.endPosition - this.startPosition + 1 : 0;
988
- this.unitsHandler = UnitsHandler.getOrCreate(this.seqCol);
980
+ this.seqHandler = SeqHandler.forColumn(this.seqCol);
989
981
  const posCount: number = this.startPosition <= this.endPosition ? this.endPosition - this.startPosition + 1 : 0;
990
982
  this.positions = new Array(posCount);
991
983
  for (let jPos = 0; jPos < length; jPos++) {
@@ -997,17 +989,16 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
997
989
 
998
990
  // 2022-05-05 askalkin instructed to show WebLogo based on filter (not selection)
999
991
  const dfRowCount = this.dataFrame.rowCount;
1000
- const splitted = this.unitsHandler.splitted;
1001
992
 
1002
993
  for (let jPos = 0; jPos < length; ++jPos) {
994
+ const pi = this.positions[jPos];
1003
995
  // Here we want to build lists of values for every monomer in position jPos
1004
996
  for (let rowI = 0; rowI < dfRowCount; ++rowI) {
1005
997
  if (dfFilter.get(rowI)) {
1006
- const seqMList: ISeqSplitted = splitted[rowI];
1007
- const m: string = seqMList[this.startPosition + jPos] || this.unitsHandler.defaultGapSymbol;
1008
- const pi = this.positions[jPos];
1009
- const pmi = pi.getFreq(m);
1010
998
  ++pi.sumRowCount;
999
+ const seqMList: ISeqSplitted = this.seqHandler.getSplitted(rowI);
1000
+ const cm: string = seqMList.getCanonical(this.startPosition + jPos);
1001
+ const pmi = pi.getFreq(cm);
1011
1002
  pmi.value = ++pmi.rowCount;
1012
1003
  }
1013
1004
  }
@@ -1024,10 +1015,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1024
1015
 
1025
1016
  for (let rowI = 0; rowI < dfRowCount; ++rowI) {
1026
1017
  if (dfFilter.get(rowI)) { // respect the filter
1027
- const seqMList: ISeqSplitted = splitted[rowI];
1028
- const m: string = seqMList[this.startPosition + jPos] || this.unitsHandler.defaultGapSymbol;
1018
+ const seqMList: ISeqSplitted = this.seqHandler.getSplitted(rowI);
1019
+ const cm: string = seqMList.getCanonical(this.startPosition + jPos);
1029
1020
  const value: number | null = valueCol.get(rowI);
1030
- this.positions[jPos].getFreq(m).push(value);
1021
+ this.positions[jPos].getFreq(cm).push(value);
1031
1022
  }
1032
1023
  }
1033
1024
  this.positions[jPos].aggregate(this.valueAggrType);
@@ -1066,7 +1057,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1066
1057
  `this.positions.length = ${this.positions.length}, jPos = ${jPos}`);
1067
1058
  continue;
1068
1059
  }
1069
- this.positions[jPos].calcScreen((m) => { return this.unitsHandler!.isGap(m); },
1060
+ this.positions[jPos].calcScreen(
1070
1061
  jPos, this.slider.min, absoluteMaxHeight, this.positionHeight,
1071
1062
  alphabetSizeLog, this._positionWidthWithMargin, this._positionWidth, dpr, positionLabelsHeight);
1072
1063
  }
@@ -1078,9 +1069,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1078
1069
  if (this.seqCol && !this.palette) {
1079
1070
  this.msgHost!.innerText = `Unknown palette (column semType: '${this.seqCol.semType}').`;
1080
1071
  this.msgHost!.style.display = '';
1081
- } else {
1072
+ } else
1082
1073
  this.msgHost!.style.display = 'none';
1083
- }
1084
1074
  }
1085
1075
 
1086
1076
  if (!this.seqCol || !this.dataFrame || !this.palette || this.host == null || this.slider == null)
@@ -1123,10 +1113,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1123
1113
  // Hacks to scale uppercase characters to target rectangle
1124
1114
  const uppercaseLetterAscent = 0.25;
1125
1115
  const uppercaseLetterHeight = 12.2;
1126
- for (let jPos = firstPos; jPos <= lastPos; jPos++) {
1127
- this.positions[jPos].render(g, (m) => { return this.unitsHandler!.isGap(m); },
1128
- fontStyle, uppercaseLetterAscent, uppercaseLetterHeight, this.palette);
1129
- }
1116
+ for (let jPos = firstPos; jPos <= lastPos; jPos++)
1117
+ this.positions[jPos].render(g, fontStyle, uppercaseLetterAscent, uppercaseLetterHeight, this.palette);
1130
1118
  } finally {
1131
1119
  g.restore();
1132
1120
  }
@@ -1141,18 +1129,16 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1141
1129
  return;
1142
1130
  }
1143
1131
  this.requestedRenderLevel = WlRenderLevel.None;
1144
- this.renderInt(renderLevel)
1145
- .catch((err: any) => {
1146
- const [errMsg, errStack] = errInfo(err);
1147
- _package.logger.error(errMsg, undefined, errStack);
1148
- });
1132
+ this.viewSyncer.sync(logPrefix, async () => {
1133
+ await this.renderInt(renderLevel);
1134
+ });
1149
1135
  }
1150
1136
 
1151
1137
  private _lastWidth: number;
1152
1138
  private _lastHeight: number;
1153
1139
 
1154
1140
  public getAlphabetSize(): number {
1155
- return this.unitsHandler?.getAlphabetSize() ?? 0;
1141
+ return this.seqHandler?.getAlphabetSize() ?? 0;
1156
1142
  }
1157
1143
 
1158
1144
  // -- Handle events --
@@ -1218,10 +1204,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1218
1204
  tooltipRows.push(pi.buildCompositionTable(this.palette!));
1219
1205
  const tooltipEl = ui.divV(tooltipRows);
1220
1206
  ui.tooltip.show(tooltipEl, args.x + 16, args.y + 16);
1221
- } else if (pi !== null && monomer && this.dataFrame && this.seqCol && this.unitsHandler) {
1207
+ } else if (pi !== null && monomer && this.dataFrame && this.seqCol && this.seqHandler) {
1222
1208
  // Monomer at position tooltip
1223
1209
  // const monomerAtPosSeqCount = countForMonomerAtPosition(
1224
- // this.dataFrame, this.unitsHandler!, this.getFilter(), monomer, atPI);
1210
+ // this.dataFrame, this.seqHandler!, this.getFilter(), monomer, atPI);
1225
1211
  const pmi = pi.getFreq(monomer);
1226
1212
 
1227
1213
  const tooltipRows = [
@@ -1233,9 +1219,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1233
1219
  tooltipRows.push(ui.div(`${this.valueAggrType}: ${pmi.value.toFixed(3)}`));
1234
1220
  const tooltipEl = ui.divV(tooltipRows);
1235
1221
  ui.tooltip.show(tooltipEl, args.x + 16, args.y + 16);
1236
- } else {
1222
+ } else
1237
1223
  ui.tooltip.hide();
1238
- }
1239
1224
  } catch (err: any) {
1240
1225
  const errMsg = errorToConsole(err);
1241
1226
  _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseMove() error:\n` + errMsg);
@@ -1250,10 +1235,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1250
1235
  const [pi, monomer] = this.getMonomer(this.canvas.getCursorPosition(args, dpr), dpr);
1251
1236
 
1252
1237
  // prevents deselect all rows if we miss monomer bounds
1253
- if (pi !== null && monomer !== null && this.dataFrame && this.seqCol && this.unitsHandler) {
1238
+ if (pi !== null && monomer !== null && this.dataFrame && this.seqCol && this.seqHandler) {
1254
1239
  // Calculate a new BitSet object for selection to prevent interfering with existing
1255
1240
  const selBS: DG.BitSet = DG.BitSet.create(this.dataFrame.selection.length, (rowI: number) => {
1256
- return checkSeqForMonomerAtPos(this.dataFrame, this.unitsHandler!, this.getFilter(), rowI, monomer, pi);
1241
+ return checkSeqForMonomerAtPos(this.dataFrame, this.seqHandler!, this.getFilter(), rowI, monomer, pi);
1257
1242
  });
1258
1243
  this.dataFrame.selection.init((i) => selBS.get(i));
1259
1244
  }
@@ -1288,8 +1273,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1288
1273
  const callLog = `invalidate(${caller ? ` <- ${caller} ` : ''})`;
1289
1274
  const logPrefix = `${this.viewerToLog()}.${callLog}`;
1290
1275
  // Put the event trigger in the tail of the synced calls queue.
1276
+ this.render(WlRenderLevel.None, callLog); // Put render request to the syncer
1291
1277
  this.viewSyncer.sync(`${logPrefix}`, async () => {
1292
- this.render(WlRenderLevel.None, callLog);
1293
1278
  this._onRendered.next();
1294
1279
  });
1295
1280
  }
@@ -1340,23 +1325,23 @@ function renderPositionLabels(g: CanvasRenderingContext2D,
1340
1325
  }
1341
1326
 
1342
1327
  export function checkSeqForMonomerAtPos(
1343
- df: DG.DataFrame, unitsHandler: UnitsHandler, filter: DG.BitSet, rowI: number, monomer: string, at: PositionInfo,
1328
+ df: DG.DataFrame, sh: SeqHandler, filter: DG.BitSet, rowI: number, monomer: string, at: PositionInfo,
1344
1329
  ): boolean {
1345
- const seqMList: ISeqSplitted = unitsHandler.splitted[rowI];
1346
- const seqM = at.pos < seqMList.length ? seqMList[at.pos] : null;
1347
- return ((seqM === monomer) || (seqM === '' && monomer === unitsHandler.defaultGapSymbol));
1330
+ const seqMList: ISeqSplitted = sh.getSplitted(rowI);
1331
+ const seqCM: string | null = at.pos < seqMList.length ? seqMList.getCanonical(at.pos) : null;
1332
+ return seqCM !== null && seqCM === monomer;
1348
1333
  }
1349
1334
 
1350
1335
  export function countForMonomerAtPosition(
1351
- df: DG.DataFrame, uh: UnitsHandler, filter: DG.BitSet, monomer: string, at: PositionInfo
1336
+ df: DG.DataFrame, sh: SeqHandler, filter: DG.BitSet, monomer: string, at: PositionInfo
1352
1337
  ): number {
1353
1338
  let count = 0;
1354
1339
  let rowI = -1;
1355
1340
  while ((rowI = filter.findNext(rowI, true)) != -1) {
1356
- const seqMList: ISeqSplitted = uh.splitted[rowI];
1341
+ const seqMList: ISeqSplitted = sh.getSplitted(rowI);
1357
1342
  const seqMPos: number = at.pos;
1358
- const seqM: string | null = seqMPos < seqMList.length ? seqMList[seqMPos] : null;
1359
- if (seqM === monomer) count++;
1343
+ const seqCM: string | null = seqMPos < seqMList.length ? seqMList.getCanonical(seqMPos) : null;
1344
+ if (seqCM !== null && seqCM === monomer) count++;
1360
1345
  }
1361
1346
  return count;
1362
1347
  }
@@ -10,14 +10,14 @@ import * as grok from 'datagrok-api/grok';
10
10
 
11
11
  import wu from 'wu';
12
12
  import $ from 'cash-dom';
13
- import {fromEvent, Observable, Subject, Subscription, Unsubscribable} from 'rxjs';
13
+ import {fromEvent, Observable, Subject, Unsubscribable} from 'rxjs';
14
14
 
15
15
  import {TAGS as bioTAGS, NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
16
16
  import {errInfo} from '@datagrok-libraries/bio/src/utils/err-info';
17
17
  import {delay, testEvent} from '@datagrok-libraries/utils/src/test';
18
18
  import {getHelmHelper} from '@datagrok-libraries/bio/src/helm/helm-helper';
19
19
  import {IHelmWebEditor, IWebEditorApp} from '@datagrok-libraries/bio/src/helm/types';
20
- import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
20
+ import {SeqHandler} from '@datagrok-libraries/bio/src/utils/seq-handler';
21
21
  import {IRenderer} from '@datagrok-libraries/bio/src/types/renderer';
22
22
  import {ILogger} from '@datagrok-libraries/bio/src/utils/logger';
23
23
  import {PromiseSyncer} from '@datagrok-libraries/bio/src/utils/syncer';
@@ -120,7 +120,7 @@ export class BioSubstructureFilter extends DG.Filter implements IRenderer {
120
120
  this.filterSyncer.sync(logPrefix, async () => {
121
121
  superAttach(dataFrame);
122
122
  this.column = dataFrame.columns.bySemType(DG.SEMTYPE.MACROMOLECULE);
123
- const uh = UnitsHandler.getOrCreate(this.column!);
123
+ const sh = SeqHandler.forColumn(this.column!);
124
124
  this.columnName ??= this.column?.name;
125
125
  this.notation ??= this.column?.getTag(DG.TAGS.UNITS);
126
126
 
@@ -8,7 +8,8 @@ import {TAGS as bioTAGS, ALPHABET, getPaletteByType} from '@datagrok-libraries/b
8
8
  import {SeqPalette} from '@datagrok-libraries/bio/src/seq-palettes';
9
9
  import {UnknownSeqPalettes} from '@datagrok-libraries/bio/src/unknown';
10
10
  import '../../css/composition-analysis.css';
11
- import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
11
+ import {SeqHandler} from '@datagrok-libraries/bio/src/utils/seq-handler';
12
+ import {GAP_SYMBOL} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
12
13
 
13
14
 
14
15
  export function getCompositionAnalysisWidget(val: DG.SemanticValue): DG.Widget {
@@ -17,24 +18,24 @@ export function getCompositionAnalysisWidget(val: DG.SemanticValue): DG.Widget {
17
18
  const alphabet = val.cell.column.tags[bioTAGS.alphabet];
18
19
  let palette: SeqPalette = UnknownSeqPalettes.Color;
19
20
  switch (alphabet) {
20
- case ALPHABET.DNA:
21
- case ALPHABET.RNA:
22
- palette = getPaletteByType(ALPHABET.DNA);
23
- break;
24
- case ALPHABET.PT:
25
- palette = getPaletteByType(ALPHABET.PT);
26
- break;
27
- default:
28
- break;
21
+ case ALPHABET.DNA:
22
+ case ALPHABET.RNA:
23
+ palette = getPaletteByType(ALPHABET.DNA);
24
+ break;
25
+ case ALPHABET.PT:
26
+ palette = getPaletteByType(ALPHABET.PT);
27
+ break;
28
+ default:
29
+ break;
29
30
  }
30
31
 
31
32
  const counts: { [m: string]: number } = {};
32
- const uh = UnitsHandler.getOrCreate(val.cell.column);
33
- const splitter = uh.getSplitter();
34
- const parts = splitter(val.value);
35
- wu(parts).filter((p) => !!p && p !== '').forEach((m: string) => {
36
- const count = counts[m] || 0;
37
- counts[m] = count + 1;
33
+ const sh = SeqHandler.forColumn(val.cell.column as DG.Column<string>);
34
+ const rowIdx = val.cell.rowIndex;
35
+ const parts = sh.getSplitted(rowIdx);
36
+ wu(parts.canonicals).filter((cm) => cm !== GAP_SYMBOL).forEach((cm) => {
37
+ const count = counts[cm] || 0;
38
+ counts[cm] = count + 1;
38
39
  });
39
40
  const table = buildCompositionTable(palette, counts);
40
41
  Array.from(table.rows).forEach((row) => {
@@ -57,15 +58,21 @@ export function buildCompositionTable(palette: SeqPalette, counts: { [m: string]
57
58
  const maxRatio = maxValue! / sumValue;
58
59
  const elMap: { [m: string]: HTMLElement } = Object.assign({}, ...Array.from(Object.entries(counts))
59
60
  .sort((a, b) => b[1] - a[1])
60
- .map(([m, value]) => {
61
+ .map(([cm, value]) => {
61
62
  const ratio = value / sumValue;
62
- const color = palette.get(m);
63
+ const color = palette.get(cm);
63
64
  const barDiv = ui.div('', {classes: 'macromolecule-cell-comp-analysis-bar'});
64
65
  barDiv.style.width = `${50 * ratio / maxRatio}px`;
65
66
  barDiv.style.backgroundColor = color;
67
+ if (GAP_SYMBOL === cm) {
68
+ barDiv.style.borderWidth = '1px';
69
+ barDiv.style.borderStyle = 'solid';
70
+ barDiv.style.borderColor = DG.Color.toHtml(DG.Color.lightGray);
71
+ }
72
+ const displayMonomer: string = GAP_SYMBOL === cm ? '-' : cm;
66
73
  const valueDiv = ui.div(`${(100 * ratio).toFixed(2)}%`);
67
74
  const el = ui.div([barDiv, valueDiv], {classes: 'macromolecule-cell-comp-analysis-value'});
68
- return ({[m]: el});
75
+ return ({[displayMonomer]: el});
69
76
  }));
70
77
 
71
78
  const table = ui.tableFromMap(elMap);