@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/CHANGELOG.md +15 -0
- package/css/monomer-manager.css +1 -0
- package/dist/package-test.js +2 -2
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +2 -2
- package/dist/package.js.map +1 -1
- package/package.json +3 -3
- package/src/package.ts +0 -1
- package/src/utils/monomer-lib/monomer-lib-base.ts +16 -2
- package/src/utils/monomer-lib/monomer-manager/monomer-manager.ts +74 -32
- package/src/viewers/utils.ts +4 -0
- package/src/viewers/web-logo-viewer.ts +77 -44
- package/test-console-output-1.log +1178 -362
- package/test-record-1.mp4 +0 -0
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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:
|
|
1168
|
+
polymerType: polymerType,
|
|
1127
1169
|
monomerType: dfRow.get(MONOMER_DF_COLUMN_NAMES.MONOMER_TYPE),
|
|
1128
|
-
naturalAnalog:
|
|
1170
|
+
naturalAnalog: naturalAnalog,
|
|
1129
1171
|
id: dfRow.get(MONOMER_DF_COLUMN_NAMES.ID),
|
|
1130
|
-
rgroups:
|
|
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),
|
|
@@ -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,
|
|
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.
|
|
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.
|
|
439
|
-
{category: PROPS_CATS.LAYOUT
|
|
440
|
-
this.maxHeight = this.
|
|
441
|
-
{category: PROPS_CATS.LAYOUT
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
716
|
-
|
|
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 = `${
|
|
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 +
|
|
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.
|
|
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.
|
|
768
|
+
this.safeUpdateSlider(0, Math.max(this.Length - 0.001), newMin, newMax);
|
|
757
769
|
} else {
|
|
758
770
|
//
|
|
759
|
-
this.
|
|
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.
|
|
835
|
+
this.safeUpdateSlider(0, Math.max(0, this.Length - 0.001), newMin, newMax);
|
|
823
836
|
} else {
|
|
824
837
|
//
|
|
825
|
-
this.
|
|
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
|
-
|
|
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
|