@datagrok/bio 2.15.8 → 2.15.10

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.
@@ -24,8 +24,8 @@
24
24
  "colors": {
25
25
  "default": {
26
26
  "line": "#202020",
27
- "text": "#202020",
28
- "background": "#60FF40"
27
+ "text": "#20C010",
28
+ "background": "#F0F0F0"
29
29
  }
30
30
  }
31
31
  },
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Aleksandr Tanas",
6
6
  "email": "atanas@datagrok.ai"
7
7
  },
8
- "version": "2.15.8",
8
+ "version": "2.15.10",
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",
@@ -37,12 +37,12 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@biowasm/aioli": "^3.1.0",
40
- "@datagrok-libraries/bio": "^5.44.4",
40
+ "@datagrok-libraries/bio": "^5.44.5",
41
41
  "@datagrok-libraries/chem-meta": "^1.2.7",
42
- "@datagrok-libraries/math": "^1.2.0",
43
- "@datagrok-libraries/ml": "^6.7.1",
44
- "@datagrok-libraries/tutorials": "^1.4.2",
45
- "@datagrok-libraries/utils": "^4.3.4",
42
+ "@datagrok-libraries/math": "^1.2.1",
43
+ "@datagrok-libraries/ml": "^6.7.3",
44
+ "@datagrok-libraries/tutorials": "^1.4.3",
45
+ "@datagrok-libraries/utils": "^4.3.6",
46
46
  "@webgpu/types": "^0.1.40",
47
47
  "ajv": "^8.12.0",
48
48
  "ajv-errors": "^3.0.0",
@@ -5,6 +5,7 @@ import * as ui from 'datagrok-api/ui';
5
5
  import {getGridCellColTemp, CellRendererBackBase} from '@datagrok-libraries/bio/src/utils/cell-renderer-back-base';
6
6
  import {SeqHandler} from '@datagrok-libraries/bio/src/utils/seq-handler';
7
7
  import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
8
+ import {GridCell} from 'datagrok-api/dg';
8
9
 
9
10
  export class MacromoleculeCustomCellRenderer extends DG.GridCellRenderer {
10
11
  get name(): string { return 'sequence'; }
@@ -24,9 +25,16 @@ export class MacromoleculeCustomCellRenderer extends DG.GridCellRenderer {
24
25
  return back;
25
26
  }
26
27
 
27
- override onMouseLeave(gridCell: DG.GridCell, e: MouseEvent) {
28
+ override onMouseEnter(gridCell: GridCell, e: MouseEvent) {
28
29
  const back = this.getRendererBack(gridCell);
29
- back.onMouseLeave(gridCell, e);
30
+ back.onMouseEnter(gridCell, e);
31
+ }
32
+
33
+ override onMouseLeave(gridCell: DG.GridCell, e: MouseEvent) {
34
+ // TODO: We get gridCell from another column here, so we can not get back object from the column rendered.
35
+ ui.tooltip.hide();
36
+ // const back = this.getRendererBack(gridCell);
37
+ // back.onMouseLeave(gridCell, e);
30
38
  }
31
39
 
32
40
  override onMouseMove(gridCell: DG.GridCell, e: MouseEvent) {
@@ -102,10 +102,11 @@ export class MacromoleculeSequenceCellRenderer extends DG.GridCellRenderer {
102
102
  ): void {
103
103
  const logPrefix: string = 'MacromoleculeSequenceCellRenderer.render()';
104
104
 
105
- const [gridCol, tableCol, _temp] =
105
+ const [gridCol, tableCol, temp] =
106
106
  getGridCellColTemp<string, MonomerPlacer>(gridCell);
107
107
  if (!tableCol) return;
108
108
  const tableColTemp: TempType = tableCol.temp;
109
+ const sh = SeqHandler.forColumn(tableCol);
109
110
 
110
111
  let gapLength = 0;
111
112
  const msaGapLength = 8;
@@ -121,31 +122,16 @@ export class MacromoleculeSequenceCellRenderer extends DG.GridCellRenderer {
121
122
  maxLengthOfMonomer = !isNaN(v) && v ? v : 50;
122
123
  }
123
124
 
124
- const [_gc, _tc, temp] =
125
- getGridCellColTemp<string, MonomerPlacer>(gridCell);
126
125
  let seqColTemp: MonomerPlacer = temp.rendererBack;
127
126
  if (!seqColTemp) {
128
127
  seqColTemp = temp.rendererBack = new MonomerPlacer(gridCol, tableCol, _package.logger, maxLengthOfMonomer,
129
128
  () => {
130
- const sh = SeqHandler.forColumn(tableCol);
131
129
  return {
132
- seqHandler: sh,
133
130
  monomerCharWidth: 7, separatorWidth: !sh.isMsa() ? gapLength : msaGapLength,
134
131
  monomerToShort: monomerToShortFunction,
135
132
  };
136
133
  });
137
- }
138
-
139
- if (
140
- tableCol.temp[MmcrTemps.rendererSettingsChanged] === rendererSettingsChangedState.true ||
141
- seqColTemp.monomerLengthLimit != maxLengthOfMonomer
142
- ) {
143
- gapLength = tableColTemp[MmcrTemps.gapLength] as number ?? gapLength;
144
- // this event means that the mm renderer settings have changed,
145
- // particularly monomer representation and max width.
146
- seqColTemp.setMonomerLengthLimit(maxLengthOfMonomer);
147
- seqColTemp.setSeparatorWidth(seqColTemp.isMsa() ? msaGapLength : gapLength);
148
- tableCol.temp[MmcrTemps.rendererSettingsChanged] = rendererSettingsChangedState.false;
134
+ tableCol.temp[MmcrTemps.rendererSettingsChanged] === rendererSettingsChangedState.true;
149
135
  }
150
136
 
151
137
  seqColTemp.render(g, x, y, w, h, gridCell, _cellStyle);
@@ -269,15 +255,13 @@ export function drawMoleculeDifferenceOnCanvas(
269
255
 
270
256
  let color1 = undefinedColor;
271
257
  if (monomerLib) {
272
- const wem1 = monomerLib.getWebEditorMonomer(biotype, amino1)!;
273
- color1 = wem1.backgroundcolor!;
258
+ color1 = monomerLib.getMonomerTextColor(biotype, amino1);
274
259
  }
275
260
 
276
261
  if (amino1 != amino2) {
277
262
  let color2 = undefinedColor;
278
263
  if (monomerLib) {
279
- const wem2 = monomerLib.getWebEditorMonomer(biotype, amino2)!;
280
- color2 = wem2.backgroundcolor!;
264
+ color2 = monomerLib.getMonomerTextColor(biotype, amino2);
281
265
  }
282
266
  const subX0 = printLeftOrCentered(g, amino1, updatedX, updatedY - vShift, w, h,
283
267
  {color: color1, pivot: 0, left: true});
@@ -41,8 +41,7 @@ export class MonomerCellRendererBack extends CellRendererWithMonomerLibBackBase
41
41
  if (this.monomerLib) {
42
42
  const alphabet = this.tableCol.getTag(bioTAGS.alphabet);
43
43
  const biotype = alphabet === ALPHABET.RNA || alphabet === ALPHABET.DNA ? HelmTypes.NUCLEOTIDE : HelmTypes.AA;
44
- const wem = this.monomerLib.getWebEditorMonomer(biotype, symbol)!;
45
- color = wem.backgroundcolor!;
44
+ color = this.monomerLib.getMonomerTextColor(biotype, symbol);
46
45
  }
47
46
 
48
47
  //cell width of monomer should dictate how many characters can be displayed
@@ -6,15 +6,18 @@ import wu from 'wu';
6
6
  import {Observable, Subject} from 'rxjs';
7
7
 
8
8
  import {IMonomerLibBase, Monomer, RGroup} from '@datagrok-libraries/bio/src/types/index';
9
- import {HelmAtom, HelmType, IWebEditorMonomer, MonomerType, PolymerType} from '@datagrok-libraries/bio/src/helm/types';
9
+ import {HelmAtom, HelmType, IMonomerColors, IWebEditorMonomer, MonomerType, PolymerType} from '@datagrok-libraries/bio/src/helm/types';
10
10
  import {getMonomerHandleArgs} from '@datagrok-libraries/bio/src/helm/helm-helper';
11
11
  import {helmTypeToPolymerType} from '@datagrok-libraries/bio/src/monomer-works/monomer-works';
12
12
  import {HelmTypes, PolymerTypes} from '@datagrok-libraries/bio/src/helm/consts';
13
- import {HELM_REQUIRED_FIELD as REQ, HELM_RGROUP_FIELDS as RGP} from '@datagrok-libraries/bio/src/utils/const';
13
+ import {HELM_OPTIONAL_FIELDS as OPT, HELM_REQUIRED_FIELD as REQ, HELM_RGROUP_FIELDS as RGP} from '@datagrok-libraries/bio/src/utils/const';
14
14
  import {GAP_SYMBOL, GapOriginals, NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule/consts';
15
+ import {Vector} from '@datagrok-libraries/utils/src/type-declarations';
16
+ import {vectorAdd, vectorDotProduct, vectorLength} from '@datagrok-libraries/utils/src/vector-operations';
15
17
 
16
18
  import {AmbiguousWebEditorMonomer, GapWebEditorMonomer, MissingWebEditorMonomer} from './web-editor-monomer-dummy';
17
19
  import {LibraryWebEditorMonomer} from './web-editor-monomer-of-library';
20
+ import {naturalMonomerColors} from './monomer-colors';
18
21
 
19
22
  import {_package} from '../../package';
20
23
 
@@ -24,6 +27,10 @@ const ambMonomerRe = RegExp(String.raw`\(${monomerRe}(,${monomerRe})+\)`);
24
27
 
25
28
  export type MonomerLibDataType = { [polymerType: string]: { [monomerSymbol: string]: Monomer } };
26
29
 
30
+ const whiteColorV = new Vector([255.0, 255.0, 255.0]);
31
+ const blackColorV = new Vector([0.0, 0.0, 0.0]);
32
+ const maxTextColorVLen = vectorLength(whiteColorV) * 0.7;
33
+
27
34
  export class MonomerLibBase implements IMonomerLibBase {
28
35
  protected _isEmpty: boolean;
29
36
  get isEmpty(): boolean { return this._isEmpty; }
@@ -143,7 +150,7 @@ export class MonomerLibBase implements IMonomerLibBase {
143
150
  resWem = m.wem = LibraryWebEditorMonomer.fromMonomer(biotype, m, this);
144
151
  }
145
152
 
146
- return resWem!;
153
+ return resWem;
147
154
  }
148
155
 
149
156
  getTooltip(biotype: HelmType, monomerSymbol: string): HTMLElement {
@@ -155,10 +162,18 @@ export class MonomerLibBase implements IMonomerLibBase {
155
162
  const symbol = monomer[REQ.SYMBOL];
156
163
  const _name = monomer[REQ.NAME];
157
164
  const wem = this.getWebEditorMonomer(biotype, monomerSymbol)!;
165
+
158
166
  const htmlColor = wem.backgroundcolor;
159
167
  res.append(ui.divH([
160
- ui.div([symbol], {style: {fontWeight: 'bolder', textWrap: 'nowrap', marginRight: '6px', color: htmlColor}}),
161
- ui.div([monomer.name])
168
+ ui.div([symbol], {
169
+ style: {
170
+ /* fontWeight: 'bolder', */ textWrap: 'nowrap', marginLeft: '4px', marginRight: '4px',
171
+ color: wem.textcolor, backgroundColor: wem.backgroundcolor, borderColor: wem.linecolor,
172
+ borderWidth: '1px', borderStyle: 'solid', borderRadius: '2px', padding: '3px',
173
+ minWidth: '24px', textAlign: 'center',
174
+ }
175
+ }),
176
+ ui.div([monomer.name], {style: {padding: '4px'}}),
162
177
  ], {style: {display: 'flex', flexDirection: 'row', justifyContent: 'left'}}));
163
178
 
164
179
  // Structure
@@ -190,6 +205,77 @@ export class MonomerLibBase implements IMonomerLibBase {
190
205
  return res;
191
206
  }
192
207
 
208
+ getMonomerTextColor(biotype: HelmType, symbol: string): string {
209
+ const colors: IMonomerColors = this.getMonomerColors(biotype, symbol);
210
+ const htmlToA = (html: string): number[] => {
211
+ const rgbM = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(html);
212
+ if (rgbM)
213
+ return [parseInt(rgbM[1]), parseInt(rgbM[2]), parseInt(rgbM[3])];
214
+
215
+ const n = DG.Color.fromHtml(colors.textcolor!);
216
+ return [DG.Color.r(n), DG.Color.g(n), DG.Color.b(n)];
217
+ };
218
+
219
+ const textColorA = htmlToA(colors.textcolor!);
220
+ const textColorAdjV = new Vector([...textColorA.map((v) => v + 1)]);
221
+ const cosTextToWhite = vectorDotProduct(textColorAdjV, whiteColorV) /
222
+ (vectorLength(textColorAdjV) * vectorLength(whiteColorV));
223
+
224
+ const backColorA = htmlToA(colors.backgroundcolor!);
225
+ const backColorAdjV = new Vector([...backColorA.map((v) => v + 0.01)]);
226
+ const cosBackToWhite = vectorDotProduct(backColorAdjV, whiteColorV) /
227
+ (vectorLength(backColorAdjV) * vectorLength(whiteColorV));
228
+
229
+ let resColorA;
230
+ if (cosBackToWhite < cosTextToWhite)
231
+ resColorA = backColorA;
232
+ else
233
+ resColorA = textColorA;
234
+
235
+ let resColorV = new Vector(resColorA);
236
+ const resColorLen = vectorLength(resColorV);
237
+ if (resColorLen > maxTextColorVLen) resColorV = vectorAdd(blackColorV, resColorV, maxTextColorVLen / resColorLen);
238
+ return `rgb(${resColorV[0]}, ${resColorV[1]}, ${resColorV[2]})`;
239
+ }
240
+
241
+ getMonomerColors(biotype: HelmType, symbol: string): IMonomerColors {
242
+ const currentMonomerSchema = 'default';
243
+ let monomerSchema: string = currentMonomerSchema;
244
+
245
+ const polymerType = helmTypeToPolymerType(biotype);
246
+ const monomer = this.getMonomer(polymerType, symbol)!;
247
+ let res: any;
248
+ if (monomer) {
249
+ if (monomer.meta && monomer.meta.colors) {
250
+ const monomerColors: { [colorSchemaName: string]: any } = monomer.meta.colors;
251
+ if (!(currentMonomerSchema in monomerColors)) monomerSchema = 'default';
252
+ res = monomerColors[monomerSchema];
253
+ }
254
+
255
+ if (!res) {
256
+ const biotypeColors: { [symbol: string]: string } | undefined = naturalMonomerColors[biotype];
257
+ const nColor: string = biotypeColors?.[monomer.symbol];
258
+ if (nColor) {
259
+ const nTextColor = DG.Color.toHtml(DG.Color.getContrastColor(DG.Color.fromHtml(nColor)));
260
+ res = {textColor: nTextColor, lineColor: '#202020', backgroundColor: nColor};
261
+ }
262
+ }
263
+
264
+ const naSymbol: string | undefined = monomer[OPT.NATURAL_ANALOG];
265
+ if (!res && naSymbol) {
266
+ return this.getMonomerColors(biotype, naSymbol);
267
+ }
268
+ }
269
+
270
+ if (!res)
271
+ res = {textColor: "#202020", lineColor: "#202020", backgroundColor: "#A0A0A0"};
272
+
273
+ return {
274
+ textcolor: res.text ?? res.textColor,
275
+ linecolor: res.line ?? res.lineColor,
276
+ backgroundcolor: res.background ?? res.backgroundColor
277
+ } as IMonomerColors;
278
+ }
193
279
 
194
280
  getRS(smiles: string): { [r: string]: string } {
195
281
  const newS = smiles.match(/(?<=\[)[^\][]*(?=])/gm);
@@ -16,6 +16,7 @@ import {MonomerLibManager} from '../lib-manager';
16
16
  import {LIB_PATH} from '../consts';
17
17
 
18
18
  import '../../../../css/monomer-manager.css';
19
+ import { MONOMER_RENDERER_TAGS } from '@datagrok-libraries/bio/src/utils/cell-renderer';
19
20
 
20
21
  // columns of monomers dataframe, note that rgroups is hidden and will be displayed as separate columns
21
22
  export enum MONOMER_DF_COLUMN_NAMES {
@@ -50,6 +51,16 @@ export const MONOMER_DF_COLUMNS = {
50
51
 
51
52
 
52
53
  export class MonomerManager implements IMonomerManager {
54
+
55
+ private adjustColWidths() {
56
+ setTimeout(() => {
57
+ if (this.tv?.grid) {
58
+ this.tv!.grid.col(MONOMER_DF_COLUMN_NAMES.NAME)!.width = 100;
59
+ this.tv!.grid.col(MONOMER_DF_COLUMN_NAMES.SYMBOL)!.width = 70;
60
+ }
61
+ }, 200);
62
+ }
63
+
53
64
  public static readonly VIEW_NAME = 'Manage Monomers';
54
65
  private _newMonomer: Monomer = DUMMY_MONOMER;
55
66
  private _newMonomerForm: MonomerForm;
@@ -63,7 +74,11 @@ export class MonomerManager implements IMonomerManager {
63
74
  this.monomerLib = monomerLibManamger.getBioLib();
64
75
  this._newMonomerForm = new MonomerForm(monomerLibManamger, () => this.activeMonomerLib, async () => {
65
76
  const df = await this.getMonomersDf(this.libInput.value!);
66
- this.tv?.dataFrame && (this.tv.dataFrame = df);
77
+ if (this.tv?.dataFrame) {
78
+ this.tv.dataFrame = df;
79
+ this.adjustColWidths();
80
+ }
81
+
67
82
  }, () => this.tv?.dataFrame);
68
83
  }
69
84
 
@@ -150,7 +165,7 @@ export class MonomerManager implements IMonomerManager {
150
165
  const df = await this.getMonomersDf(fileName);
151
166
  this.tv = DG.TableView.create(df, true);
152
167
  //const f = tv.filters();
153
- this.tv.grid.col(MONOMER_DF_COLUMN_NAMES.NAME)!.width = 100;
168
+ this.adjustColWidths();
154
169
  this.tv.subs.push(
155
170
  grok.events.onContextMenu.subscribe(({args}) => {
156
171
  if (!args || !args.menu || !args.context || args.context.type !== DG.VIEWER.GRID || !args.context.tableView ||
@@ -200,7 +215,7 @@ export class MonomerManager implements IMonomerManager {
200
215
  libName && (this.libInput.value = libName);
201
216
  const df = await this.getMonomersDf(libName);
202
217
  this.tv.dataFrame = df;
203
- this.tv.grid.col(MONOMER_DF_COLUMN_NAMES.NAME)!.width = 100;
218
+ this.adjustColWidths();
204
219
  return this.tv;
205
220
  }
206
221
 
@@ -262,6 +277,7 @@ export class MonomerManager implements IMonomerManager {
262
277
  try {
263
278
  const df = await this.getMonomersDf(this.libInput.value!);
264
279
  this.tv!.dataFrame = df;
280
+ this.adjustColWidths();
265
281
  } catch (e) {
266
282
  console.error(e);
267
283
  }
@@ -311,6 +327,8 @@ export class MonomerManager implements IMonomerManager {
311
327
  df.columns.addNew(rgroupName, DG.COLUMN_TYPE.STRING);
312
328
  }
313
329
  }
330
+ df.col(MONOMER_DF_COLUMN_NAMES.SYMBOL)!.semType = 'Monomer';
331
+ df.col(MONOMER_DF_COLUMN_NAMES.SYMBOL)!.setTag(MONOMER_RENDERER_TAGS.applyToBackground, 'true');
314
332
 
315
333
 
316
334
  for (let i = 0; i < monomers.length; i++) {
@@ -470,16 +488,29 @@ class MonomerForm implements INewMonomerForm {
470
488
  monomerNaturalAnalogInput: DG.InputBase<string | null>;
471
489
  rgroupsGrid: ItemsGrid;
472
490
  metaGrid: ItemsGrid;
491
+ colors: {
492
+ line: string,
493
+ background: string,
494
+ text: string
495
+ };
496
+ colorsEditor: ColorsEditor;
473
497
  saveButton: HTMLButtonElement;
474
498
  rgroupsGridRoot: HTMLElement;
475
499
  private _molChanged: boolean = false;
476
500
  get molChanged() { return this._molChanged; }
477
501
  private saveValidationResult?: string | null = null;
478
502
  private triggerMolChange: boolean = true; // makes sure that change is not triggered by copying the molecule from grid
503
+ inputsTabControl: DG.TabControl;
479
504
  constructor(public monomerLibManager: MonomerLibManager,
480
505
  private getMonomerLib: () => IMonomerLib | null, private refreshTable: () => Promise<void>,
481
506
  private getMonomersDataFrame: () => DG.DataFrame | undefined) {
482
507
  const monomerTypes = ['PEPTIDE', 'RNA', 'CHEM', 'BLOB', 'G'];
508
+ this.colors = {
509
+ line: '#000000',
510
+ background: '#000000',
511
+ text: '#000000',
512
+ };
513
+ this.colorsEditor = new ColorsEditor(this.colors);
483
514
  this.molSketcher = new DG.chem.Sketcher();
484
515
  this.molSketcher.root.classList.add('monomer-manager-sketcher');
485
516
  this.polymerTypeInput = ui.input.choice('Polymer Type', {value: 'PEPTIDE', items: monomerTypes,
@@ -576,6 +607,24 @@ class MonomerForm implements INewMonomerForm {
576
607
  ];
577
608
  this.metaGrid = new ItemsGrid(metaProps, []);
578
609
  this.onMonomerInputChanged();
610
+
611
+
612
+ const mainInputsDiv = ui.divV([
613
+ this.polymerTypeInput,
614
+ this.monomerTypeInput,
615
+ this.monomerSymbolInput,
616
+ this.monomerNameInput,
617
+ this.monomerIdInput,
618
+ this.monomerNaturalAnalogInput,
619
+ ]);
620
+
621
+ this.inputsTabControl = ui.tabControl({
622
+ 'Monomer': mainInputsDiv,
623
+ 'R-groups': this.rgroupsGridRoot,
624
+ 'Meta': ui.divV([this.metaGrid.root]),
625
+ 'Colors': this.colorsEditor.form,
626
+ }, false);
627
+
579
628
  }
580
629
 
581
630
  onMonomerInputChanged() {
@@ -603,13 +652,12 @@ class MonomerForm implements INewMonomerForm {
603
652
  this.monomerIdInput.value = monomer.id;
604
653
  this.monomerNaturalAnalogInput.value = monomer.naturalAnalog ?? null;
605
654
  this.rgroupsGrid.items = resolveRGroupInfo(monomer.rgroups);
606
- this.metaGrid.items = Object.entries(monomer.meta ?? {}).map(([k, v]) => {
655
+ this.metaGrid.items = Object.entries(monomer.meta ?? {}).filter(([k, v]) => k?.toLowerCase() !== 'colors').map(([k, v]) => {
607
656
  return {Property: k, Value: v};
608
657
  });
609
658
  this.rgroupsGrid.render();
610
659
  this.metaGrid.render();
611
660
  this.rgroupsGridRoot.style.display = 'flex';
612
-
613
661
  this.onMonomerInputChanged();
614
662
  if (!monomer.naturalAnalog) {
615
663
  mostSimilarNaturalAnalog(capSmiles(monomer.smiles, this.rgroupsGrid.items as RGroup[]), monomer.polymerType).then((mostSimilar) => {
@@ -617,20 +665,50 @@ class MonomerForm implements INewMonomerForm {
617
665
  this.monomerNaturalAnalogInput.value = mostSimilar;
618
666
  });
619
667
  }
668
+ const colorsString = monomer.meta?.colors ?? '';
669
+ let colorsObj: Partial<typeof this.colors> = {};
670
+ try {
671
+ colorsObj = colorsString ? JSON.parse(colorsString)?.default : {};
672
+ } catch (e) {
673
+ console.error(e);
674
+ }
675
+
676
+ this.colorsEditor.colors = {
677
+ line: colorsObj.line ?? '#000000',
678
+ background: colorsObj.background ?? '#000000',
679
+ text: colorsObj.text ?? '#000000',
680
+ };
620
681
  }
621
682
 
622
683
  validateInputs(): string | null | undefined {
684
+ const rGroupsPane = this.inputsTabControl.panes.find((p) => p.name?.toLowerCase() === 'r-groups');
685
+ rGroupsPane && (rGroupsPane.header.style.removeProperty('background-color'));
623
686
  if (!this.molSketcher.getSmiles()) return 'Monomer Molecule field is required';
624
- for (const i of [this.polymerTypeInput, this.monomerTypeInput, this.monomerSymbolInput, this.monomerNameInput, this.monomerIdInput, this.monomerNaturalAnalogInput]) {
687
+ for (const i of [this.polymerTypeInput, this.monomerTypeInput, this.monomerSymbolInput, this.monomerNameInput, this.monomerIdInput]) {
625
688
  if (i.value == null || i.value === '')
626
689
  return `${i.caption} field is required`;
627
690
  }
628
- if (this.rgroupsGrid.items.length < 1) return 'At least 1 R-group is required';
629
- for (const item of this.rgroupsGrid.items) {
630
- for (const [k, v] of Object.entries(item))
631
- if (!v) return `R-group ${k} field is required for ${item[HELM_RGROUP_FIELDS.LABEL]}`;
691
+ let rgroupError: string | null | undefined = null;
692
+ if (this.rgroupsGrid.items.length < 1)
693
+ rgroupError = 'At least one R-group is required';
694
+ if (!rgroupError) {
695
+ outerFor:
696
+ for (const item of this.rgroupsGrid.items) {
697
+ for (const [k, v] of Object.entries(item))
698
+ if (!v){
699
+ rgroupError = `R-group ${k} field is required for ${item[HELM_RGROUP_FIELDS.LABEL]}`;
700
+ break outerFor;
701
+ }
702
+ }
703
+ }
704
+
705
+ if (!rgroupError && this.rgroupsGrid.hasErrors()){
706
+ rgroupError = 'R-group fields contain errors';
707
+ }
708
+ if (rgroupError) {
709
+ rGroupsPane && (rGroupsPane.header.style.setProperty('background-color', '#ff000030'));
710
+ return rgroupError;
632
711
  }
633
- if (this.rgroupsGrid.hasErrors()) return 'R-group fields contain errors';
634
712
  return null;
635
713
  }
636
714
 
@@ -643,26 +721,14 @@ class MonomerForm implements INewMonomerForm {
643
721
  }
644
722
 
645
723
  get form() {
646
- const mainInputsDiv = ui.divV([
647
- this.polymerTypeInput,
648
- this.monomerTypeInput,
649
- this.monomerSymbolInput,
650
- this.monomerNameInput,
651
- this.monomerIdInput,
652
- this.monomerNaturalAnalogInput,
653
- ]);
654
-
655
- const inputsPanel = ui.tabControl({
656
- 'Monomer': mainInputsDiv,
657
- 'R-groups': this.rgroupsGridRoot,
658
- 'Meta': ui.divV([this.metaGrid.root]),
659
- }, false);
660
- inputsPanel.header.style.marginBottom = '10px';
724
+
725
+ this.inputsTabControl.root.classList.add('monomer-manager-form-tab-control');
726
+ this.inputsTabControl.header.style.marginBottom = '10px';
661
727
  const saveB = ui.buttonsInput([this.saveButton]);
662
728
  ui.tooltip.bind(saveB, () => this.saveValidationResult ?? 'Save monomer to library');
663
729
  return ui.divV([
664
730
  this.molSketcher.root,
665
- inputsPanel.root,
731
+ this.inputsTabControl.root,
666
732
  saveB,
667
733
  ], {classes: 'ui-form', style: {paddingLeft: '10px', overflow: 'scroll'}});
668
734
  }
@@ -750,8 +816,12 @@ class MonomerForm implements INewMonomerForm {
750
816
  const saveLib = async () => {
751
817
  try {
752
818
  // first remove the existing monomer with that symbol
753
- libJSON = libJSON.filter((m) => m.symbol !== monomer.symbol || m.polymerType !== monomer.polymerType);
754
- libJSON.push({...monomer, lib: undefined, wem: undefined});
819
+ const monomerIdx = libJSON.findIndex((m) => m.symbol === monomer.symbol && m.polymerType === monomer.polymerType);
820
+ if (monomerIdx >= 0) {
821
+ libJSON[monomerIdx] = {...monomer, lib: undefined, wem: undefined};
822
+ } else {
823
+ libJSON.push({...monomer, lib: undefined, wem: undefined});
824
+ }
755
825
  await grok.dapi.files.writeAsText(LIB_PATH + libName, JSON.stringify(libJSON));
756
826
  await (await MonomerLibManager.getInstance()).loadLibraries(true);
757
827
  await this.refreshTable();
@@ -802,6 +872,13 @@ class MonomerForm implements INewMonomerForm {
802
872
  this.metaGrid.items.filter((item) => (!!item['Property']) && (!!item['Value'])).forEach((item) => {
803
873
  meta[item['Property']] = item['Value'];
804
874
  });
875
+ const addingItem = this.metaGrid.addingItem;
876
+ if (addingItem && addingItem['Property'] && addingItem['Value']) {
877
+ meta[addingItem['Property']] = addingItem['Value'];
878
+ }
879
+ //console.log(this.metaGrid.addingItem);
880
+ if (this.colorsEditor.colors.line !== '#000000' || this.colorsEditor.colors.background !== '#000000' || this.colorsEditor.colors.text !== '#000000')
881
+ meta.colors = {default: this.colorsEditor.colors};
805
882
  const monomer: Monomer = {
806
883
  symbol: this.monomerSymbolInput.value,
807
884
  name: this.monomerNameInput.value,
@@ -929,3 +1006,42 @@ function monomerFromDfRow(dfRow: DG.Row): Monomer {
929
1006
  createDate: dfRow.get(MONOMER_DF_COLUMN_NAMES.CREATE_DATE),
930
1007
  };
931
1008
  }
1009
+
1010
+ class ColorsEditor {
1011
+ private _colors: { line: string, background: string, text: string };
1012
+ private _colorInputs: { [key in keyof ColorsEditor['_colors']]: DG.InputBase<string> };
1013
+ constructor(colors: { line: string, background: string, text: string }) {
1014
+ this._colors = colors;
1015
+ this._colorInputs = {
1016
+ line: ui.input.color('Line', {value: colors.line, onValueChanged: (v) => this._colors.line = v}),
1017
+ background: ui.input.color('Background', {value: colors.background, onValueChanged: (v) => this._colors.background = v}),
1018
+ text: ui.input.color('Text', {value: colors.text, onValueChanged: (v) => this._colors.text = v}),
1019
+ };
1020
+ }
1021
+
1022
+ get colors() {
1023
+ return this._colors;
1024
+ }
1025
+
1026
+ set colors(cols: { line: string, background: string, text: string }) {
1027
+ //need to convert to hex as the input accepts only hex
1028
+ const colsHex = {
1029
+ line: DG.Color.toHtml(DG.Color.fromHtml(cols.line ?? '#000000')),
1030
+ background: DG.Color.toHtml(DG.Color.fromHtml(cols.background ?? '#000000')),
1031
+ text: DG.Color.toHtml(DG.Color.fromHtml(cols.text ?? '#000000')),
1032
+ };
1033
+
1034
+ this._colors = colsHex;
1035
+ for (const key in this._colorInputs) {
1036
+ this._colorInputs[key as keyof ColorsEditor['_colors']].value = colsHex[key as keyof ColorsEditor['_colors']];
1037
+ }
1038
+ }
1039
+
1040
+ get colorsMetaFormat() {
1041
+ return {colors: {default: this._colors}};
1042
+ }
1043
+
1044
+ get form() {
1045
+ return ui.form(Object.values(this._colorInputs));
1046
+ }
1047
+ }
@@ -2,12 +2,11 @@ import * as grok from 'datagrok-api/grok';
2
2
  import * as ui from 'datagrok-api/ui';
3
3
  import * as DG from 'datagrok-api/dg';
4
4
 
5
- import {HelmType, IMonomerColors, IWebEditorMonomer, MonomerType, PolymerType, WebEditorRGroups} from '@datagrok-libraries/bio/src/helm/types';
5
+ import {HelmType, IWebEditorMonomer, MonomerType, PolymerType, WebEditorRGroups} from '@datagrok-libraries/bio/src/helm/types';
6
6
  import {IMonomerLibBase, Monomer} from '@datagrok-libraries/bio/src/types/index';
7
7
  import {HELM_OPTIONAL_FIELDS as OPT, HELM_REQUIRED_FIELD as REQ, HELM_RGROUP_FIELDS as RGP} from '@datagrok-libraries/bio/src/utils/const';
8
8
 
9
- import {BrokenWebEditorMonomer, MissingWebEditorMonomer} from './web-editor-monomer-dummy';
10
- import {naturalMonomerColors} from './monomer-colors';
9
+ import {BrokenWebEditorMonomer} from './web-editor-monomer-dummy';
11
10
 
12
11
  export class LibraryWebEditorMonomer implements IWebEditorMonomer {
13
12
  public get rs(): number { return Object.keys(this.at).length; }
@@ -59,7 +58,7 @@ export class LibraryWebEditorMonomer implements IWebEditorMonomer {
59
58
  monomer[REQ.MONOMER_TYPE],
60
59
  at);
61
60
 
62
- const colors = getMonomerColors(biotype, monomer, monomerLib);
61
+ const colors = monomerLib.getMonomerColors(biotype, monomer[REQ.SYMBOL]);
63
62
  if (colors) {
64
63
  res.textcolor = colors?.textcolor;
65
64
  res.linecolor = colors?.linecolor;
@@ -69,41 +68,3 @@ export class LibraryWebEditorMonomer implements IWebEditorMonomer {
69
68
  return res;
70
69
  }
71
70
  }
72
-
73
- function getMonomerColors(biotype: HelmType, monomer: Monomer, monomerLib?: IMonomerLibBase): IMonomerColors | null {
74
- const currentMonomerSchema = 'default';
75
- let monomerSchema: string = currentMonomerSchema;
76
-
77
- let res: any;
78
- if (monomer.meta && monomer.meta.colors) {
79
- const monomerColors: { [colorSchemaName: string]: any } = monomer.meta.colors;
80
- if (!(currentMonomerSchema in monomerColors)) monomerSchema = 'default';
81
- res = monomerColors[monomerSchema];
82
- }
83
-
84
- if (!res) {
85
- const biotypeColors: { [symbol: string]: string } | undefined = naturalMonomerColors[biotype];
86
- const nColor: string = biotypeColors?.[monomer.symbol];
87
- if (nColor) {
88
- const nTextColor = DG.Color.toHtml(DG.Color.getContrastColor(DG.Color.fromHtml(nColor)));
89
- res = {textColor: nTextColor, lineColor: '#202020', backgroundColor: nColor};
90
- }
91
- }
92
-
93
- const naSymbol: string | undefined = monomer[OPT.NATURAL_ANALOG];
94
- if (!res && naSymbol) {
95
- const polymerType = monomer[REQ.POLYMER_TYPE];
96
- const naMonomer = monomerLib?.getMonomer(polymerType, naSymbol);
97
- if (naMonomer)
98
- return getMonomerColors(biotype, naMonomer);
99
- }
100
-
101
- if (!res)
102
- res = {textColor: "#202020", lineColor: "#202020", backgroundColor: "#A0A0A0"};
103
-
104
- return !res ? null : {
105
- textcolor: res.text ?? res.textColor,
106
- linecolor: res.line ?? res.lineColor,
107
- backgroundcolor: res.background ?? res.backgroundColor
108
- } as IMonomerColors;
109
- }