@datagrok/bio 2.21.0 → 2.21.2

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/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Leonid Stolbov",
6
6
  "email": "lstolbov@datagrok.ai"
7
7
  },
8
- "version": "2.21.0",
8
+ "version": "2.21.2",
9
9
  "description": "Bioinformatics support (import/export of sequences, conversion, visualization, analysis). [See more](https://github.com/datagrok-ai/public/blob/master/packages/Bio/README.md) for details.",
10
10
  "repository": {
11
11
  "type": "git",
@@ -44,12 +44,12 @@
44
44
  ],
45
45
  "dependencies": {
46
46
  "@biowasm/aioli": "^3.1.0",
47
- "@datagrok-libraries/bio": "^5.51.0",
47
+ "@datagrok-libraries/bio": "^5.51.2",
48
48
  "@datagrok-libraries/chem-meta": "^1.2.7",
49
49
  "@datagrok-libraries/math": "^1.2.4",
50
50
  "@datagrok-libraries/ml": "^6.10.0",
51
51
  "@datagrok-libraries/tutorials": "^1.6.1",
52
- "@datagrok-libraries/utils": "^4.5.0",
52
+ "@datagrok-libraries/utils": "^4.5.7",
53
53
  "datagrok-api": "^1.25.0",
54
54
  "@webgpu/types": "^0.1.40",
55
55
  "ajv": "^8.12.0",
package/src/package.ts CHANGED
@@ -524,7 +524,6 @@ export async function activityCliffs(table: DG.DataFrame, molecules: DG.Column<s
524
524
  //tags: dim-red-preprocessing-function
525
525
  //meta.supportedSemTypes: Macromolecule
526
526
  //meta.supportedTypes: string
527
- //meta.supportedUnits: fasta,separator,helm
528
527
  //meta.supportedDistanceFunctions: Hamming,Levenshtein,Monomer chemical distance,Needlemann-Wunsch
529
528
  //input: column col {semType: Macromolecule}
530
529
  //input: string metric
@@ -209,8 +209,22 @@ export class MonomerLibBase implements IMonomerLibBase {
209
209
  {style: {display: 'flex', flexDirection: 'row', justifyContent: 'center', margin: '6px'}}));
210
210
 
211
211
  // Source
212
- if (monomer.symbol != GAP_SYMBOL)
213
- res.append(ui.divText(monomer.lib?.source ?? 'Missed in libraries'));
212
+ if (monomer.symbol != GAP_SYMBOL) {
213
+ let source = monomer.lib?.source;
214
+ if (!source)
215
+ res.append(ui.divText('Missed in libraries'));
216
+ else {
217
+ // remove.json extension
218
+ if (source.endsWith('.json'))
219
+ source = source.substring(0, source.length - 5);
220
+ // convert campelCase/snake_case/kebab-case to space separated words
221
+ source = source
222
+ .replace(/_/g, ' ')
223
+ .replace(/-/g, ' ')
224
+ .replace(/([a-z])([a-z])([A-Z])/g, '$1$2 $3');
225
+ res.append(ui.divText(source));
226
+ }
227
+ }
214
228
  } else {
215
229
  res.append(ui.divV([
216
230
  ui.divText(`Monomer '${monomerSymbol}' of type '${polymerType}' not found.`),
@@ -191,23 +191,23 @@ export class MonomerManager implements IMonomerManager {
191
191
  args.context.tableView.id !== (this.tv!.id ?? '') || !args.item || !args.item.isTableCell || (args.item.tableRowIndex ?? -1) < 0)
192
192
  return;
193
193
  const rowIdx = args.item.tableRowIndex;
194
- args.menu.item('Edit Monomer', () => {
195
- this.cloneMonomer(this.tv!.dataFrame.rows.get(rowIdx));
194
+ args.menu.item('Edit Monomer', async () => {
195
+ await this.editMonomer(this.tv!.dataFrame.rows.get(rowIdx));
196
196
  });
197
197
  if (this.tv!.dataFrame.selection.trueCount > 0) {
198
- args.menu.item('Remove Selected Monomers', () => {
199
- const monomers = Array.from(this.tv!.dataFrame.selection.getSelectedIndexes())
200
- .map((r) => monomerFromDfRow(this.tv!.dataFrame.rows.get(r)));
198
+ args.menu.item('Remove Selected Monomers', async () => {
199
+ const monomers = await Promise.all(Array.from(this.tv!.dataFrame.selection.getSelectedIndexes())
200
+ .map((r) => monomerFromDfRow(this.tv!.dataFrame.rows.get(r))));
201
201
  this._newMonomerForm.removeMonomers(monomers, this.libInput.value!);
202
202
  });
203
- args.menu.item('Selection To New Library', () => {
204
- const monomers = Array.from(this.tv!.dataFrame.selection.getSelectedIndexes())
205
- .map((r) => monomerFromDfRow(this.tv!.dataFrame.rows.get(r)));
203
+ args.menu.item('Selection To New Library', async () => {
204
+ const monomers = await Promise.all(Array.from(this.tv!.dataFrame.selection.getSelectedIndexes())
205
+ .map((r) => monomerFromDfRow(this.tv!.dataFrame.rows.get(r))));
206
206
  this.createNewLibDialog(monomers);
207
207
  });
208
208
  } else {
209
- args.menu.item('Remove Monomer', () => {
210
- const monomer = monomerFromDfRow(this.tv!.dataFrame.rows.get(rowIdx));
209
+ args.menu.item('Remove Monomer', async () => {
210
+ const monomer = await monomerFromDfRow(this.tv!.dataFrame.rows.get(rowIdx));
211
211
  this._newMonomerForm.removeMonomers([monomer], this.libInput.value!);
212
212
  });
213
213
  }
@@ -271,9 +271,9 @@ export class MonomerManager implements IMonomerManager {
271
271
  this._newMonomerForm.setEmptyMonomer();
272
272
  }, 'Add New Monomer');
273
273
 
274
- const editButton = ui.icons.edit(() => {
274
+ const editButton = ui.icons.edit(async () => {
275
275
  if ((this.tv?.dataFrame?.currentRowIdx ?? -1) < 0) return;
276
- this.cloneMonomer(this.tv!.dataFrame.rows.get(this.tv!.dataFrame.currentRowIdx));
276
+ await this.editMonomer(this.tv!.dataFrame.rows.get(this.tv!.dataFrame.currentRowIdx));
277
277
  }, 'Edit Monomer');
278
278
 
279
279
  const deleteButton = ui.icons.delete(async () => {
@@ -282,16 +282,16 @@ export class MonomerManager implements IMonomerManager {
282
282
  if (currentRowIdx < 0 && selectedRows.length === 0) return;
283
283
 
284
284
  if (selectedRows.length > 0) {
285
- const monomers = selectedRows.map((r) => monomerFromDfRow(this.tv!.dataFrame.rows.get(r)));
285
+ const monomers = await Promise.all(selectedRows.map((r) => monomerFromDfRow(this.tv!.dataFrame.rows.get(r))));
286
286
  await this._newMonomerForm.removeMonomers(monomers, this.libInput.value!);
287
287
  return;
288
288
  }
289
- const monomer = monomerFromDfRow(this.tv!.dataFrame.rows.get(currentRowIdx));
289
+ const monomer = await monomerFromDfRow(this.tv!.dataFrame.rows.get(currentRowIdx));
290
290
  await this._newMonomerForm.removeMonomers([monomer], this.libInput.value!);
291
291
  });
292
292
 
293
293
  ui.tooltip.bind(deleteButton, () =>
294
- `${(this.tv?.dataFrame?.selection?.getSelectedIndexes() ?? []).length > 0 ? 'Delete selected monomers' : 'Delete monomer'}`);
294
+ `${(this.tv?.dataFrame?.selection?.trueCount ?? 0) > 0 ? 'Delete selected monomers' : 'Delete monomer'}`);
295
295
 
296
296
  const downloadButton = ui.iconFA('arrow-to-bottom', async () => {
297
297
  const libName = this.libInput.value;
@@ -333,8 +333,8 @@ export class MonomerManager implements IMonomerManager {
333
333
  return this.tv;
334
334
  }
335
335
 
336
- cloneMonomer(dfRow: DG.Row): Monomer {
337
- this._newMonomer = monomerFromDfRow(dfRow);
336
+ async editMonomer(dfRow: DG.Row): Promise<Monomer> {
337
+ this._newMonomer = await monomerFromDfRow(dfRow);
338
338
  this._newMonomerForm.setMonomer(this._newMonomer);
339
339
  return this._newMonomer;
340
340
  }
@@ -384,6 +384,17 @@ export class MonomerManager implements IMonomerManager {
384
384
  const rgroup = monomers[i].rgroups.find((rg) => rg.label === rgName);
385
385
  return rgroup ? getCaseInvariantValue(rgroup, HELM_RGROUP_FIELDS.CAP_GROUP_SMILES) : '';
386
386
  });
387
+ let date: number | null = null;
388
+
389
+ if (monomers[i].createDate) {
390
+ try {
391
+ date = Date.parse(monomers[i].createDate!);
392
+ } catch (e) {
393
+ console.error(`Error parsing date ${monomers[i].createDate}`);
394
+ }
395
+ }
396
+
397
+
387
398
  df.rows.setValues(i, [
388
399
  molSmiles,
389
400
  monomers[i].symbol,
@@ -394,23 +405,30 @@ export class MonomerManager implements IMonomerManager {
394
405
  monomers[i].polymerType,
395
406
  monomers[i].naturalAnalog,
396
407
  monomers[i].author,
397
- monomers[i].createDate,
408
+ date,
398
409
  monomers[i].id,
399
410
  JSON.stringify(monomers[i].meta ?? {}),
400
411
  monomers[i].lib?.source ?? '',
401
412
  ]);
413
+ // something is wrong with setting dates, so setting it manually for now
414
+ try {
415
+ if (date)
416
+ df.col(MONOMER_DF_COLUMN_NAMES.CREATE_DATE)?.set(i, date, false);
417
+ } catch (e) {
418
+ console.error(`Error setting date ${monomers[i].createDate}`);
419
+ }
402
420
  }
403
421
  df.col(MONOMER_DF_COLUMN_NAMES.MONOMER)!.semType = DG.SEMTYPE.MOLECULE;
404
422
  uniqueRgroupNames.forEach((rgName) => {
405
423
  df.col(rgName)!.semType = DG.SEMTYPE.MOLECULE;
406
424
  });
407
425
  df.currentRowIdx = -1;
408
- // eslint-disable-next-line rxjs/no-ignored-subscription
409
- df.onCurrentRowChanged.subscribe((_) => {
426
+ // eslint-disable-next-line rxjs/no-ignored-subscription, rxjs/no-async-subscribe
427
+ df.onCurrentRowChanged.subscribe(async (_) => {
410
428
  try {
411
429
  if (df.currentRowIdx === -1 || this._newMonomerForm.molChanged)
412
430
  return;
413
- this.cloneMonomer(df.rows.get(df.currentRowIdx));
431
+ await this.editMonomer(df.rows.get(df.currentRowIdx));
414
432
  } catch (e) {
415
433
  console.error(e);
416
434
  }
@@ -753,7 +771,7 @@ class MonomerForm implements INewMonomerForm {
753
771
  this.metaGrid.render();
754
772
  this.rgroupsGridRoot.style.display = 'flex';
755
773
  this.onMonomerInputChanged();
756
- if (!monomer.naturalAnalog) {
774
+ if (!monomer.naturalAnalog && monomer.polymerType) {
757
775
  mostSimilarNaturalAnalog(capSmiles(monomer.smiles, this.rgroupsGrid.items as RGroup[]), monomer.polymerType).then((mostSimilar) => {
758
776
  if (mostSimilar)
759
777
  this.monomerNaturalAnalogInput.value = mostSimilar;
@@ -824,7 +842,7 @@ class MonomerForm implements INewMonomerForm {
824
842
  this.molSketcher.root,
825
843
  this.inputsTabControl.root,
826
844
  saveB,
827
- ], {classes: 'ui-form', style: {paddingLeft: '10px', overflow: 'scroll'}});
845
+ ], {classes: 'ui-form', style: {paddingLeft: '10px', overflow: 'scroll', maxWidth: 'unset'}});
828
846
  }
829
847
 
830
848
  get fieldInputs() {
@@ -933,7 +951,7 @@ class MonomerForm implements INewMonomerForm {
933
951
  infoTable = this.getMonomerInfoTable(libJSON[existingMonomerIdx]);
934
952
  promptMessage = `Monomer with symbol '${monomer.symbol}' already exists in library ${libName}.\nAre you sure you want to overwrite it?`;
935
953
  } else if ((existingStructureIdx ?? -1) >= 0) {
936
- const m = monomerFromDfRow(this.getMonomersDataFrame()!.rows.get(existingStructureIdx!));
954
+ const m = await monomerFromDfRow(this.getMonomersDataFrame()!.rows.get(existingStructureIdx!));
937
955
  infoTable = this.getMonomerInfoTable(m);
938
956
  promptMessage = `Monomer with the same structure already exists in library ${libName} with different symbol (${m.symbol}).\nAre you sure you want to duplicate it?`;
939
957
  }
@@ -1098,12 +1116,22 @@ function capSmiles(smiles: string, rgroups: RGroup[]) {
1098
1116
  return newSmiles;
1099
1117
  }
1100
1118
 
1101
- function monomerFromDfRow(dfRow: DG.Row): Monomer {
1119
+ async function monomerFromDfRow(dfRow: DG.Row): Promise<Monomer> {
1102
1120
  // hacky way for now, but meta object for now only supports key value pairs and not nested objects
1103
- const metaJSON = JSON.parse(dfRow.get(MONOMER_DF_COLUMN_NAMES.META) ?? '{}');
1104
- for (const key in metaJSON) {
1105
- if (typeof metaJSON[key] === 'object')
1106
- metaJSON[key] = JSON.stringify(metaJSON[key]);
1121
+ let metaJSON: any;
1122
+ try {
1123
+ metaJSON = JSON.parse(dfRow.get(MONOMER_DF_COLUMN_NAMES.META) ?? '{}');
1124
+ for (const key in metaJSON) {
1125
+ if (typeof metaJSON[key] === 'object')
1126
+ metaJSON[key] = JSON.stringify(metaJSON[key]);
1127
+ // post-fix for detecting null values in meta object
1128
+ if ((typeof metaJSON[key] === 'string' && (metaJSON[key] === DG.INT_NULL.toString() || metaJSON[key] === DG.FLOAT_NULL.toString())) ||
1129
+ (typeof metaJSON[key] === 'number' && (metaJSON[key] === DG.INT_NULL || metaJSON[key] === DG.FLOAT_NULL))
1130
+ )
1131
+ metaJSON[key] = null;
1132
+ }
1133
+ } catch (e) {
1134
+ console.error(e);
1107
1135
  }
1108
1136
  const smiles = dfRow.get(MONOMER_DF_COLUMN_NAMES.MONOMER);
1109
1137
  if (!smiles)
@@ -1118,16 +1146,30 @@ function monomerFromDfRow(dfRow: DG.Row): Monomer {
1118
1146
  console.error(e);
1119
1147
  }
1120
1148
 
1149
+ let naturalAnalog = dfRow.get(MONOMER_DF_COLUMN_NAMES.NATURAL_ANALOG);
1150
+ const polymerType = dfRow.get(MONOMER_DF_COLUMN_NAMES.POLYMER_TYPE);
1151
+ let rGroups: RGroup[] = [];
1152
+ try {
1153
+ rGroups = JSON.parse(dfRow.get(MONOMER_DF_COLUMN_NAMES.R_GROUPS) ?? '[]');
1154
+ if (!naturalAnalog && polymerType) {
1155
+ const mostSimilar = await mostSimilarNaturalAnalog(capSmiles(smiles, rGroups), polymerType);
1156
+ if (mostSimilar)
1157
+ naturalAnalog = mostSimilar;
1158
+ }
1159
+ } catch (_) {
1160
+ rGroups ??= [];
1161
+ }
1162
+
1121
1163
  return {
1122
1164
  symbol: dfRow.get(MONOMER_DF_COLUMN_NAMES.SYMBOL),
1123
1165
  name: dfRow.get(MONOMER_DF_COLUMN_NAMES.NAME),
1124
1166
  molfile: molfile,
1125
1167
  smiles: smiles,
1126
- polymerType: dfRow.get(MONOMER_DF_COLUMN_NAMES.POLYMER_TYPE),
1168
+ polymerType: polymerType,
1127
1169
  monomerType: dfRow.get(MONOMER_DF_COLUMN_NAMES.MONOMER_TYPE),
1128
- naturalAnalog: dfRow.get(MONOMER_DF_COLUMN_NAMES.NATURAL_ANALOG),
1170
+ naturalAnalog: naturalAnalog,
1129
1171
  id: dfRow.get(MONOMER_DF_COLUMN_NAMES.ID),
1130
- rgroups: JSON.parse(dfRow.get(MONOMER_DF_COLUMN_NAMES.R_GROUPS) ?? '[]'),
1172
+ rgroups: rGroups,
1131
1173
  meta: metaJSON,
1132
1174
  author: dfRow.get(MONOMER_DF_COLUMN_NAMES.AUTHOR),
1133
1175
  createDate: dfRow.get(MONOMER_DF_COLUMN_NAMES.CREATE_DATE),
@@ -0,0 +1,4 @@
1
+
2
+ export function numbersWithinMaxDiff(a: number, b: number, maxDiff: number): boolean {
3
+ return Math.abs(a - b) <= maxDiff;
4
+ }
@@ -33,6 +33,7 @@ import {AggFunc, getAgg} from '../utils/agg';
33
33
  import {buildCompositionTable} from '../widgets/composition-analysis-widget';
34
34
 
35
35
  import {_package, getMonomerLibHelper} from '../package';
36
+ import {numbersWithinMaxDiff} from './utils';
36
37
 
37
38
  declare global {
38
39
  interface HTMLCanvasElement {
@@ -207,11 +208,11 @@ export class PositionInfo {
207
208
 
208
209
  render(g: CanvasRenderingContext2D,
209
210
  fontStyle: string, uppercaseLetterAscent: number, uppercaseLetterHeight: number,
210
- biotype: HelmType, monomerLib: IMonomerLibBase | null
211
+ biotype: HelmType, monomerLib: IMonomerLibBase | null, maxMonomerLetters: number
211
212
  ) {
212
213
  for (const [monomer, pmInfo] of Object.entries(this._freqs)) {
213
214
  if (monomer !== GAP_SYMBOL) {
214
- const monomerTxt = monomerToShort(monomer, 5);
215
+ const monomerTxt = monomerToShort(monomer, maxMonomerLetters);
215
216
  const b = pmInfo.bounds!;
216
217
  const left = b.left;
217
218
 
@@ -285,6 +286,7 @@ export enum PROPS {
285
286
  fitArea = 'fitArea',
286
287
  minHeight = 'minHeight',
287
288
  maxHeight = 'maxHeight',
289
+ maxMonomerLetters = 'maxMonomerLetters',
288
290
  showPositionLabels = 'showPositionLabels',
289
291
  positionMarginState = 'positionMarginState',
290
292
  positionMargin = 'positionMargin',
@@ -347,7 +349,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
347
349
 
348
350
  public minHeight: number;
349
351
  public backgroundColor: number = 0xFFFFFFFF;
350
- public maxHeight: number;
352
+ public maxHeight: number | null;
353
+ public maxMonomerLetters: number;
351
354
  public showPositionLabels: boolean;
352
355
  public positionMarginState: PositionMarginStates;
353
356
  /** Gets value from properties or setOptions */ public positionMargin: number = 0;
@@ -399,32 +402,32 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
399
402
 
400
403
  // -- Data --
401
404
  this.sequenceColumnName = this.string(PROPS.sequenceColumnName, defaults.sequenceColumnName,
402
- {category: PROPS_CATS.DATA, semType: DG.SEMTYPE.MACROMOLECULE});
405
+ {category: PROPS_CATS.DATA, semType: DG.SEMTYPE.MACROMOLECULE, description: 'Column with sequences'});
403
406
  const aggExcludeList = [DG.AGG.KEY, DG.AGG.PIVOT, DG.AGG.MISSING_VALUE_COUNT, DG.AGG.SKEW, DG.AGG.KURT,
404
407
  DG.AGG.SELECTED_ROWS_COUNT];
405
408
  const aggChoices = Object.values(DG.AGG).filter((agg) => !aggExcludeList.includes(agg));
406
- this.valueAggrType = this.string(PROPS.valueAggrType, defaults.valueAggrType,
407
- {category: PROPS_CATS.DATA, choices: aggChoices}) as DG.AggregationType;
408
409
  this.valueColumnName = this.string(PROPS.valueColumnName, defaults.valueColumnName,
409
- {category: PROPS_CATS.DATA, columnTypeFilter: 'numerical'});
410
+ {category: PROPS_CATS.DATA, columnTypeFilter: 'numerical', description: 'Column with values used in aggregation for position heights'});
411
+ this.valueAggrType = this.string(PROPS.valueAggrType, defaults.valueAggrType,
412
+ {category: PROPS_CATS.DATA, choices: aggChoices, description: 'Aggregation method for value column'}) as DG.AggregationType;
410
413
  this.startPositionName = this.string(PROPS.startPositionName, defaults.startPositionName,
411
- {category: PROPS_CATS.DATA});
414
+ {category: PROPS_CATS.DATA, description: 'Position or column name for start position. If column with given name is found, its values will be used as start position. If number is supplied, it will be used as start position of WebLogo.'});
412
415
  this.endPositionName = this.string(PROPS.endPositionName, defaults.endPositionName,
413
- {category: PROPS_CATS.DATA});
416
+ {category: PROPS_CATS.DATA, description: 'Position or column name for end position. If column with given name is found, its values will be used as end position. If number is supplied, it will be used as end position of WebLogo.'});
414
417
  this.skipEmptySequences = this.bool(PROPS.skipEmptySequences, defaults.skipEmptySequences,
415
- {category: PROPS_CATS.DATA});
418
+ {category: PROPS_CATS.DATA, description: 'Skip sequences which are empty in all positions'});
416
419
  this.skipEmptyPositions = this.bool(PROPS.skipEmptyPositions, defaults.skipEmptyPositions,
417
- {category: PROPS_CATS.DATA});
420
+ {category: PROPS_CATS.DATA, description: 'Skip positions which are empty in all sequences'});
418
421
  this.shrinkEmptyTail = this.bool(PROPS.shrinkEmptyTail, defaults.shrinkEmptyTail,
419
- {category: PROPS_CATS.DATA});
422
+ {category: PROPS_CATS.DATA, description: 'Skip empty tail (if found for all sequences within a subset) in WebLogo'});
420
423
 
421
424
  // -- Style --
422
425
  this.backgroundColor = this.int(PROPS.backgroundColor, defaults.backgroundColor,
423
- {category: PROPS_CATS.STYLE});
426
+ {category: PROPS_CATS.STYLE, description: 'Background color of WebLogo canvas'});
424
427
  this.positionHeight = this.string(PROPS.positionHeight, defaults.positionHeight,
425
- {category: PROPS_CATS.STYLE, choices: Object.values(PositionHeight)}) as PositionHeight;
428
+ {category: PROPS_CATS.STYLE, choices: Object.values(PositionHeight), description: 'Monomer-Position height mode. Entropy of 100%(full height)'}) as PositionHeight;
426
429
  this._positionWidth = this.positionWidth = this.float(PROPS.positionWidth, defaults.positionWidth,
427
- {category: PROPS_CATS.STYLE/* editor: 'slider', min: 4, max: 64, postfix: 'px' */});
430
+ {category: PROPS_CATS.LAYOUT, editor: 'slider', min: 4, max: 100, description: 'Width of position in WebLogo. If "Fit Area" is enabled, width will be calculated to fit horizontal space'});
428
431
 
429
432
  // -- Layout --
430
433
  this.verticalAlignment = this.string(PROPS.verticalAlignment, defaults.verticalAlignment,
@@ -434,23 +437,25 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
434
437
  this.fixWidth = this.bool(PROPS.fixWidth, defaults.fixWidth,
435
438
  {category: PROPS_CATS.LAYOUT, userEditable: false});
436
439
  this.fitArea = this.bool(PROPS.fitArea, defaults.fitArea,
437
- {category: PROPS_CATS.LAYOUT});
438
- this.minHeight = this.float(PROPS.minHeight, defaults.minHeight,
439
- {category: PROPS_CATS.LAYOUT/*, editor: 'slider', min: 25, max: 250, postfix: 'px'*/});
440
- this.maxHeight = this.float(PROPS.maxHeight, defaults.maxHeight,
441
- {category: PROPS_CATS.LAYOUT/*, editor: 'slider', min: 25, max: 500, postfix: 'px'*/});
440
+ {category: PROPS_CATS.LAYOUT, description: 'Fit WebLogo to the horizontal space. Calculates position width as maximum between provided width value and value needed to fit whole horizontal space.'});
441
+ this.minHeight = this.int(PROPS.minHeight, defaults.minHeight,
442
+ {category: PROPS_CATS.LAYOUT, editor: 'slider', min: 10, max: 250, description: 'Minimum height of WebLogo'});
443
+ this.maxHeight = this.int(PROPS.maxHeight, defaults.maxHeight,
444
+ {category: PROPS_CATS.LAYOUT, editor: 'slider', min: 25, max: Math.max(window.innerHeight ?? 0, 1000), nullable: true, description: 'Maximum height of WebLogo'});
445
+ this.maxMonomerLetters = this.int(PROPS.maxMonomerLetters, defaults.maxMonomerLetters,
446
+ {category: PROPS_CATS.LAYOUT, editor: 'slider', min: 1, max: 40, description: 'Maximum monomer letters to display before shortening'});
442
447
  this.showPositionLabels = this.bool(PROPS.showPositionLabels, defaults.showPositionLabels,
443
- {category: PROPS_CATS.LAYOUT});
448
+ {category: PROPS_CATS.LAYOUT, description: 'Show position labels on top of the weblogo'});
444
449
  this.positionMarginState = this.string(PROPS.positionMarginState, defaults.positionMarginState,
445
- {category: PROPS_CATS.LAYOUT, choices: Object.values(PositionMarginStates)}) as PositionMarginStates;
450
+ {category: PROPS_CATS.LAYOUT, choices: Object.values(PositionMarginStates), description: 'Calculate optimal margins between positions or turn it on/off'}) as PositionMarginStates;
446
451
  let defaultValueForPositionMargin = 0;
447
452
  if (this.positionMarginState === 'auto') defaultValueForPositionMargin = 4;
448
453
  this.positionMargin = this.int(PROPS.positionMargin, defaultValueForPositionMargin,
449
- {category: PROPS_CATS.LAYOUT, min: 0, max: 16});
454
+ {category: PROPS_CATS.LAYOUT, min: 0, max: 25, description: 'Margin between positions in WebLogo'});
450
455
 
451
456
  // -- Behavior --
452
457
  this.filterSource = this.string(PROPS.filterSource, defaults.filterSource,
453
- {category: PROPS_CATS.BEHAVIOR, choices: Object.values(FilterSources)}) as FilterSources;
458
+ {category: PROPS_CATS.BEHAVIOR, choices: Object.values(FilterSources), description: 'Data source for weblogo. Selected or filtered rows.'}) as FilterSources;
454
459
 
455
460
  const style: DG.SliderOptions = {style: 'barbell'};
456
461
  this.slider = ui.rangeSlider(0, 100, 0, 20, false, style);
@@ -673,15 +678,15 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
673
678
 
674
679
  if (this.fixWidth)
675
680
  this.calcLayoutFixWidth(dpr);
676
- else if (this.fitArea)
677
- this.calcLayoutFitArea(dpr);
681
+ // else if (this.fitArea)
682
+ // this.calcLayoutFitArea(dpr);
678
683
  else
679
684
  this.calcLayoutNoFitArea(dpr);
680
685
 
681
686
  this.slider.root.style.width = `${this.host.clientWidth}px`;
682
687
  }
683
688
 
684
- /** */
689
+ /** Only used from outside, useful for embedding weblogo into a viewer or smth else*/
685
690
  private calcLayoutFixWidth(dpr: number): void {
686
691
  if (!this.host || !this.canvas || !this.slider) return; // for es-lint
687
692
 
@@ -689,7 +694,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
689
694
  this.canvas.classList.add('bio-wl-fitArea');
690
695
 
691
696
  const areaWidth: number = this._positionWidthWithMargin * this.Length;
692
- const areaHeight: number = Math.min(Math.max(this.minHeight, this.root.clientHeight), this.maxHeight);
697
+ const areaHeight: number = Math.min(Math.max(this.minHeight, this.root.clientHeight), this.maxHeight ?? this.root.clientHeight);
693
698
 
694
699
  this.host.style.justifyContent = HorizontalAlignments.LEFT;
695
700
  this.host.style.removeProperty('margin-left');
@@ -712,15 +717,21 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
712
717
  private calcLayoutNoFitArea(dpr: number): void {
713
718
  if (!this.host || !this.canvas || !this.slider) return; // for es-lint
714
719
 
715
- const areaWidth: number = this._positionWidthWithMargin * this.Length;
716
- const areaHeight: number = Math.min(Math.max(this.minHeight, this.root.clientHeight), this.maxHeight);
720
+ let areaWidth: number = this._positionWidthWithMargin * this.Length;
721
+ let width = Math.min(this.root.clientWidth, areaWidth);
722
+ if (this.fitArea && areaWidth < this.root.clientWidth) { // if weblogo has some space to grow
723
+ this._positionWidth = Math.floor(this._positionWidth * this.root.clientWidth / areaWidth);
724
+ this._positionWidthWithMargin = this._positionWidth + this.positionMarginValue;
725
+ areaWidth = this._positionWidthWithMargin * this.Length;
726
+ width = Math.min(this.root.clientWidth, areaWidth);
727
+ }
728
+ const areaHeight: number = Math.min(Math.max(this.minHeight, this.root.clientHeight), this.maxHeight ?? this.root.clientHeight);
717
729
 
718
730
  const height = areaHeight;
719
- const width = Math.min(this.root.clientWidth, areaWidth);
720
731
 
721
732
  this.canvas.style.width = `${width}px`;
722
733
  this.canvas.style.height = `${height}px`;
723
- this.host.style.width = `${width}px`;
734
+ this.host.style.width = `${this.root.clientWidth}px`;
724
735
  this.host.style.height = `${this.root.clientHeight}px`;
725
736
 
726
737
  // host style flex-direction: row;
@@ -733,10 +744,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
733
744
 
734
745
  if (this.root.clientHeight < this.minHeight) {
735
746
  this.host.style.alignContent = 'start'; /* For vertical scroller to work properly */
736
- this.host.style.width = `${width + 6}px`; /* */
747
+ this.host.style.width = `${width + 8}px`; /* */
737
748
  }
738
749
 
739
- this.host.style.width = `${this.host}px`;
750
+ //this.host.style.width = `${this.host}px`;
740
751
 
741
752
  const sliderVisibility = areaWidth > width;
742
753
  this.setSliderVisibility(sliderVisibility);
@@ -744,30 +755,32 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
744
755
  this.slider.root.style.removeProperty('display');
745
756
  this.host.style.justifyContent = 'left'; /* For horizontal scroller to prevent */
746
757
  this.host.style.height = `${this.root.clientHeight - this.slider.root.offsetHeight}px`;
758
+ this.canvas.style.height = `${height - this.slider.root.offsetHeight}px`;
747
759
  this.slider.root.style.top = `${this.host.offsetHeight}px`;
748
760
 
749
761
  let newMin = Math.min(Math.max(0, this.slider.min), this.Length - 0.001);
750
762
  let newMax = Math.min(Math.max(0, this.slider.max), this.Length - 0.001);
751
763
 
752
- const visibleLength = this.root.clientWidth / this._positionWidthWithMargin;
764
+ const visibleLength = this.canvas.clientWidth / this._positionWidthWithMargin;
753
765
  newMax = Math.min(Math.max(newMin, 0) + visibleLength, this.Length - 0.001);
754
766
  newMin = Math.max(0, Math.min(newMax, this.Length - 0.001) - visibleLength);
755
767
 
756
- this.slider.setValues(0, Math.max(this.Length - 0.001), newMin, newMax);
768
+ this.safeUpdateSlider(0, Math.max(this.Length - 0.001), newMin, newMax);
757
769
  } else {
758
770
  //
759
- this.slider.setValues(0, Math.max(0, this.Length - 0.001), 0, Math.max(0, this.Length - 0.001));
771
+ this.safeUpdateSlider(0, Math.max(0, this.Length - 0.001), 0, Math.max(0, this.Length - 0.001));
760
772
  }
761
773
 
762
774
  this.canvas.width = width * dpr;
763
775
  this.canvas.height = height * dpr;
764
776
  }
765
777
 
778
+ /** Deprecated and currently not in use. */
766
779
  private calcLayoutFitArea(dpr: number): void {
767
780
  if (!this.host || !this.canvas || !this.slider) return; // for es-lint
768
781
 
769
782
  const originalAreaWidthWoMargins: number = this._positionWidth * this.Length;
770
- const originalAreaHeight: number = Math.min(Math.max(this.minHeight, this.root.clientHeight), this.maxHeight);
783
+ const originalAreaHeight: number = Math.min(Math.max(this.minHeight, this.root.clientHeight), this.maxHeight ?? this.root.clientHeight);
771
784
 
772
785
  // TODO: scale
773
786
  const xScale = originalAreaWidthWoMargins > 0 ?
@@ -819,16 +832,25 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
819
832
  newMax = Math.min(Math.max(newMin, 0) + visibleLength, this.Length - 0.001);
820
833
  newMin = Math.max(0, Math.min(newMax, this.Length - 0.001) - visibleLength);
821
834
 
822
- this.slider.setValues(0, Math.max(0, this.Length - 0.001), newMin, newMax);
835
+ this.safeUpdateSlider(0, Math.max(0, this.Length - 0.001), newMin, newMax);
823
836
  } else {
824
837
  //
825
- this.slider.setValues(0, Math.max(0, this.Length - 0.001), 0, Math.max(0, this.Length - 0.001));
838
+ this.safeUpdateSlider(0, Math.max(0, this.Length - 0.001), 0, Math.max(0, this.Length - 0.001));
826
839
  }
827
840
 
828
841
  this.canvas.width = width * dpr;
829
842
  this.canvas.height = height * dpr;
830
843
  }
831
844
 
845
+ /** makes sure that slider actually needs to be updated before updating it. if values are same, no update */
846
+ private safeUpdateSlider(minRange: number, maxRange: number, min: number, max: number): void {
847
+ if (!numbersWithinMaxDiff(minRange, this.slider.minRange, 0.1) ||
848
+ !numbersWithinMaxDiff(maxRange, this.slider.maxRange, 0.1) ||
849
+ !numbersWithinMaxDiff(min, this.slider.min, 0.1) ||
850
+ !numbersWithinMaxDiff(max, this.slider.max, 0.1))
851
+ this.slider.setValues(minRange, maxRange, min, max);
852
+ }
853
+
832
854
 
833
855
  /** Handler of property change events.
834
856
  * @param {DG.Property} property - property which was changed.
@@ -857,6 +879,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
857
879
  }
858
880
  case PROPS.minHeight:
859
881
  case PROPS.maxHeight:
882
+ case PROPS.maxMonomerLetters:
860
883
  case PROPS.positionWidth:
861
884
  case PROPS.showPositionLabels:
862
885
  case PROPS.fixWidth:
@@ -1147,7 +1170,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1147
1170
  const uppercaseLetterHeight = 12.2;
1148
1171
  const biotype = this.seqHandler!.defaultBiotype;
1149
1172
  for (let jPos = firstPos; jPos <= lastPos; jPos++)
1150
- this.positions[jPos].render(g, fontStyle, uppercaseLetterAscent, uppercaseLetterHeight, biotype, this.monomerLib);
1173
+ this.positions[jPos].render(g, fontStyle, uppercaseLetterAscent, uppercaseLetterHeight, biotype, this.monomerLib, this.maxMonomerLetters);
1151
1174
  } finally {
1152
1175
  g.restore();
1153
1176
  }
@@ -1186,9 +1209,18 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1186
1209
  min: this.slider.min, max: this.slider.max,
1187
1210
  maxRange: this.slider.maxRange
1188
1211
  };
1189
- _package.logger.debug(
1190
- `${this.toLog()}.sliderOnValuesChanged( ${JSON.stringify(val)} ), start`);
1191
- this.render(WlRenderLevel.Layout, 'sliderOnValuesChanged');
1212
+ // _package.logger.debug(
1213
+ // `${this.toLog()}.sliderOnValuesChanged( ${JSON.stringify(val)} ), start`);
1214
+ // this.render(WlRenderLevel.Layout, 'sliderOnValuesChanged');
1215
+ if (this.canvas && this.canvas.offsetWidth > 0 && (this._positionWidthWithMargin ?? 0) > 0) {
1216
+ const newRange = val.max - val.min;
1217
+ const newPositionWidth = this.canvas.offsetWidth / newRange - this.positionMarginValue;
1218
+ // if the range was changed on the slider
1219
+ if (!numbersWithinMaxDiff(newPositionWidth, this.positionWidth, 0.1))
1220
+ this.getProperty(PROPS.positionWidth)?.set(this, newPositionWidth);
1221
+ else // if range was not changed, but slider was moved
1222
+ this.render(WlRenderLevel.Layout, 'sliderOnValuesChanged');
1223
+ }
1192
1224
  } catch (err: any) {
1193
1225
  const errMsg = errorToConsole(err);
1194
1226
  _package.logger.error(`${this.toLog()}.sliderOnValuesChanged() error:\n` + errMsg);
@@ -1239,6 +1271,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1239
1271
  tooltipRows.push(pi.buildCompositionTable(biotype, this.monomerLib));
1240
1272
  }
1241
1273
  const tooltipEl = ui.divV(tooltipRows);
1274
+ tooltipEl.style.maxHeight = '80vh';
1242
1275
  ui.tooltip.show(tooltipEl, args.x + 16, args.y + 16);
1243
1276
  } else if (pi !== null && monomer && this.dataFrame && this.seqCol && this.seqHandler) {
1244
1277
  // Monomer at position tooltip