@datagrok/helm 2.1.13 → 2.1.15

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.
@@ -2,15 +2,22 @@ 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 {_package} from './package';
6
- import {findMonomers, parseHelm, getParts} from './utils';
5
+ import wu from 'wu';
6
+
7
7
  import {printLeftOrCentered} from '@datagrok-libraries/bio/src/utils/cell-renderer';
8
8
  import {errorToConsole} from '@datagrok-libraries/utils/src/to-console';
9
9
 
10
+ import {findMonomers, parseHelm} from './utils';
11
+ import {HelmMonomerPlacer} from './helm-monomer-placer';
12
+ import {TAGS as helmTAGS} from './constants';
13
+
10
14
  const enum tempTAGS {
11
15
  helmSumMaxLengthWords = 'helm-sum-maxLengthWords',
12
16
  helmMaxLengthWords = 'helm-maxLengthWords',
17
+
18
+ helmPlacer = 'bio-helmPlacer',
13
19
  }
20
+
14
21
  // Global flag is for replaceAll
15
22
  const helmGapStartRe = /\{(\*\.)+/g;
16
23
  const helmGapIntRe = /\.(\*\.)+/g;
@@ -37,52 +44,62 @@ export class HelmCellRenderer extends DG.GridCellRenderer {
37
44
  try { tableCol = gridCell.tableColumn; } catch { }
38
45
  if (!tableCol) return;
39
46
 
40
- const colTemp: TempType[] = tableCol.temp ?? new Array<TempType>(tableCol.length);
41
- // Exit if no missed monomers (tags are not presented in colTemp)
42
- if (!colTemp || Object.keys(colTemp).length == 0) return;
43
-
44
- const maxLengthWordsSum: { [pos: number]: number } = colTemp[tempTAGS.helmSumMaxLengthWords];
45
- const maxLengthWords: { [pos: number]: number } = colTemp[tempTAGS.helmMaxLengthWords];
47
+ const helmPlacer = HelmMonomerPlacer.getOrCreate(tableCol);
48
+ const [allParts, lengths, sumLengths] = helmPlacer.getCellAllPartsLengths(gridCell.tableRowIndex);
46
49
 
47
- const maxIndex = Object.values(maxLengthWords).length - 1;
48
- const argsX = e.offsetX - gridCell.gridColumn.left + (gridCell.gridColumn.left - gridCell.bounds.x);
50
+ const maxIndex = Object.values(lengths).length - 1;
51
+ const argsX = e.offsetX - gridCell.bounds.x;
49
52
  let left = 0;
50
53
  let right = maxIndex;
51
54
  let found = false;
52
- maxLengthWordsSum[maxIndex + 1] = argsX + 1;
55
+ let iterCount: number = 0;
56
+
53
57
  let mid = 0;
54
- if (argsX > maxLengthWordsSum[0]) {
55
- while (!found) {
58
+ if (argsX > sumLengths[0]) {
59
+ while (!found && iterCount < sumLengths.length) {
56
60
  mid = Math.floor((right + left) / 2);
57
- if (argsX >= maxLengthWordsSum[mid] && argsX <= maxLengthWordsSum[mid + 1]) {
61
+ if (argsX >= sumLengths[mid] && argsX <= sumLengths[mid + 1]) {
58
62
  left = mid;
59
63
  found = true;
60
- } else if (argsX < maxLengthWordsSum[mid]) {
64
+ } else if (argsX < sumLengths[mid]) {
61
65
  right = mid - 1;
62
- } else if (argsX > maxLengthWordsSum[mid + 1]) {
66
+ } else if (argsX > sumLengths[mid + 1]) {
63
67
  left = mid + 1;
64
68
  }
65
69
  if (left == right)
66
70
  found = true;
71
+
72
+ iterCount++;
67
73
  }
68
74
  }
69
- left = (argsX >= maxLengthWordsSum[left]) ? left + 1 : left;
70
- const monomers = findMonomers(gridCell.cell.value);
71
- const s: string = gridCell.cell.value ?? '';
72
- const subParts: string[] = parseHelm(s);
73
- const allParts: string[] = getParts(subParts, s);
75
+ left = (argsX >= sumLengths[left]) ? left : left - 1; // correct left to between sumLengths
76
+
77
+ const seq: string = gridCell.cell.value;
78
+ const monomerList = parseHelm(seq);
79
+ const monomers = new Set<string>(monomerList);
80
+ const missedMonomers = findMonomers(monomerList);
81
+
74
82
  const tooltipMessage: HTMLElement[] = [];
75
- for (let partI = 0; partI < allParts.length; ++partI) {
76
- if (monomers.has(allParts[partI])) {
83
+ for (const [part, partI] of wu.enumerate(allParts)) {
84
+ if (missedMonomers.has(part)) {
77
85
  tooltipMessage[partI] = ui.divV([
78
86
  ui.divText(`Monomer ${allParts[partI]} not found.`),
79
87
  ui.divText('Open the Context Panel, then expand Manage Libraries')
80
88
  ]);
89
+ } else if (monomers.has(part)) {
90
+ const elList = [ui.div(part)];
91
+ const monomer = helmPlacer.getMonomer(part);
92
+ if (monomer) {
93
+ const options = {autoCrop: true, autoCropMargin: 0, suppressChiralText: true};
94
+ const monomerSvg = grok.chem.svgMol(monomer.smiles, undefined, undefined, options);
95
+ elList.push(monomerSvg);
96
+ }
97
+ tooltipMessage[partI] = ui.divV(elList);
81
98
  }
82
99
  }
83
100
 
84
101
  (((tooltipMessage[left]?.childNodes.length ?? 0) > 0)) ?
85
- ui.tooltip.show(ui.div(tooltipMessage[left]), e.x + 16, e.y + 16) :
102
+ ui.tooltip.show(tooltipMessage[left], e.x + 16, e.y + 16) :
86
103
  ui.tooltip.hide();
87
104
  } catch (err: any) {
88
105
  const errMsg: string = errorToConsole(err);
@@ -93,20 +110,22 @@ export class HelmCellRenderer extends DG.GridCellRenderer {
93
110
  render(g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number,
94
111
  gridCell: DG.GridCell, cellStyle: DG.GridCellStyle
95
112
  ) {
113
+ /* Can not do anything without tableColumn containing temp */
114
+ let tableCol: DG.Column | null = null;
115
+ try { tableCol = gridCell.tableColumn; } catch { }
116
+ if (!tableCol) return;
117
+
96
118
  g.save();
97
119
  try {
98
- /* Can not do anything without tableColumn containing temp */
99
- let tableCol: DG.Column | null = null;
100
- try { tableCol = gridCell.tableColumn; } catch { }
101
- if (!tableCol) return;
102
-
103
120
  const grid = gridCell.gridRow !== -1 ? gridCell.grid : undefined;
104
- const undefinedColor = 'rgb(100,100,100)';
105
- const grayColor = '#808080';
121
+ const missedColor = 'red';
122
+ const monomerColor: string = '#404040';
123
+ const frameColor: string = '#C0C0C0';
106
124
 
107
- const missedMonomers = findMonomers(gridCell.cell.value);
108
- const s: string = gridCell.cell.value ?? '';
109
- const subParts: string[] = parseHelm(s);
125
+ const seq = gridCell.cell.value;
126
+ const monomerList: string[] = parseHelm(seq);
127
+ const monomers: Set<string> = new Set<string>(monomerList);
128
+ const missedMonomers: Set<string> = findMonomers(monomerList);
110
129
 
111
130
  if (missedMonomers.size == 0) {
112
131
  const host = ui.div([], {style: {width: `${w}px`, height: `${h}px`}});
@@ -122,40 +141,37 @@ export class HelmCellRenderer extends DG.GridCellRenderer {
122
141
  if (!grid) {
123
142
  const r = window.devicePixelRatio;
124
143
  h = 28;
125
- g.canvas.height = h*r;
144
+ g.canvas.height = h * r;
126
145
  g.canvas.style.height = `${h}px`;
127
146
  }
128
- const maxLengthWords: number[] = tableCol.temp[tempTAGS.helmMaxLengthWords] ?? [];
129
- if (subParts.length > maxLengthWords.length)
130
- maxLengthWords.push(...(new Array<number>(subParts.length - maxLengthWords.length).fill(-1)));
131
147
 
132
148
  w = grid ? Math.min(grid.canvas.width - x, w) : g.canvas.width - x;
133
149
  g.save();
134
150
  g.beginPath();
135
151
  g.rect(x, y, w, h);
136
152
  g.clip();
153
+ g.transform(1, 0, 0, 1, x, y);
137
154
  g.font = '12px monospace';
138
155
  g.textBaseline = 'top';
139
- let x1 = x;
140
- const allParts: string[] = getParts(subParts, s);
156
+ const helmPlacer = HelmMonomerPlacer.getOrCreate(tableCol);
157
+ const [allParts, lengths, sumLengths] = helmPlacer.getCellAllPartsLengths(gridCell.tableRow.idx);
158
+
141
159
  for (let i = 0; i < allParts.length; ++i) {
142
- maxLengthWords[i] = Math.max(maxLengthWords[i], allParts[i].length * 7); /* What is 7, width of char ? */
143
- const color = missedMonomers.has(allParts[i]) ? 'red' : grayColor;
144
- g.fillStyle = undefinedColor;
145
- x1 = printLeftOrCentered(x1, y, w, h, g, allParts[i], color, 0, true, 1.0);
160
+ const part: string = allParts[i];
161
+ const color: string =
162
+ part === '.' || part.endsWith('{') || part.startsWith('}') ? frameColor :
163
+ missedMonomers.has(part) ? missedColor :
164
+ monomers.has(part) ? monomerColor :
165
+ frameColor;
166
+ g.fillStyle = color;
167
+ printLeftOrCentered(sumLengths[i], 0, w, h, g, allParts[i], color, 0, true, 1.0);
146
168
  }
147
-
148
- const maxLengthWordSum: number[] = new Array<number>(maxLengthWords.length);
149
- maxLengthWordSum[0] = maxLengthWords[0];
150
- for (let partI = 1; partI < allParts.length; partI++)
151
- maxLengthWordSum[partI] = maxLengthWordSum[partI - 1] + maxLengthWords[partI];
152
-
153
- tableCol.temp = {
154
- [tempTAGS.helmSumMaxLengthWords]: maxLengthWordSum,
155
- [tempTAGS.helmMaxLengthWords]: maxLengthWords
156
- };
157
- return;
158
169
  }
170
+ } catch (err: any) {
171
+ const errMsg = err instanceof Error ? err.message : err.toString();
172
+ const errStack = err instanceof Error ? err.stack : undefined;
173
+ tableCol.setTag(helmTAGS.cellRendererRenderError, JSON.stringify({message: errMsg, stack: errStack}));
174
+ throw err;
159
175
  } finally {
160
176
  g.restore();
161
177
  }
package/src/constants.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  export const jsonSdfMonomerLibDict = {
2
- "monomerType": null,
3
- "smiles": null,
4
- "name": "MonomerName",
5
- "author": null,
6
- "molfile": "molecule",
7
- "naturalAnalog": "MonomerNaturalAnalogCode",
2
+ "monomerType": null,
3
+ "smiles": null,
4
+ "name": "MonomerName",
5
+ "author": null,
6
+ "molfile": "molecule",
7
+ "naturalAnalog": "MonomerNaturalAnalogCode",
8
8
  "rgroups": "MonomerCaps",
9
- "createDate": null,
10
- "id": null,
11
- "polymerType": "MonomerType",
9
+ "createDate": null,
10
+ "id": null,
11
+ "polymerType": "MonomerType",
12
12
  "symbol": "MonomerCode"
13
13
  }
14
14
 
@@ -24,10 +24,14 @@ export type WebEditorMonomer = {
24
24
  };
25
25
 
26
26
  export const SMILES = 'smiles';
27
- export const RGROUPS = "rgroups";
28
- export const MONOMER_SYMBOL = "symbol";
29
- export const RGROUP_CAP_GROUP_SMILES = "capGroupSmiles";
30
- export const RGROUP_ALTER_ID = "alternateId";
31
- export const RGROUP_CAP_GROUP_NAME = "capGroupName";
32
- export const RGROUP_LABEL = "label";
33
- export const SDF_MONOMER_NAME = "MonomerName";
27
+ export const RGROUPS = 'rgroups';
28
+ export const MONOMER_SYMBOL = 'symbol';
29
+ export const RGROUP_CAP_GROUP_SMILES = 'capGroupSmiles';
30
+ export const RGROUP_ALTER_ID = 'alternateId';
31
+ export const RGROUP_CAP_GROUP_NAME = 'capGroupName';
32
+ export const RGROUP_LABEL = 'label';
33
+ export const SDF_MONOMER_NAME = 'MonomerName';
34
+
35
+ export const enum TAGS {
36
+ cellRendererRenderError = '.cell-renderer.render.error',
37
+ }
@@ -0,0 +1,78 @@
1
+ import * as grok from 'datagrok-api/grok';
2
+ import * as ui from 'datagrok-api/ui';
3
+ import * as DG from 'datagrok-api/dg';
4
+
5
+ import wu from 'wu';
6
+
7
+ import {IMonomerLib, Monomer} from '@datagrok-libraries/bio/src/types/index';
8
+
9
+ import {getParts, parseHelm} from './utils';
10
+ import {getMonomerLib} from './package';
11
+
12
+ export const enum Temps {
13
+ helmMonomerPlacer = 'bio-helmMonomerPlacer',
14
+ }
15
+
16
+ export class HelmMonomerPlacer {
17
+ private _allPartsList: string[][] = null;
18
+ private _lengthsList: number[][] = null;
19
+
20
+ private monomerLib: IMonomerLib;
21
+
22
+ public monomerCharWidth: number = 7;
23
+ public leftPadding: number = 5;
24
+
25
+ constructor(public readonly col: DG.Column<string>) {
26
+ this.col.dataFrame.onDataChanged.subscribe();
27
+ this.monomerLib = getMonomerLib();
28
+ }
29
+
30
+ /** @param rowIdx Row index of the table {@link DG.DataFrame}, HelmMonomerPlacer is {@link DG.Column} based */
31
+ public getCellAllPartsLengths(rowIdx: number): [string[], number[], number[]] {
32
+ if (this._allPartsList === null)
33
+ this._allPartsList = new Array<string[]>(this.col.length).fill(null);
34
+
35
+ if (this._lengthsList === null)
36
+ this._lengthsList = new Array<number[]>(this.col.length).fill(null);
37
+
38
+ const [allParts, lengths] = this.getCellMonomerLengthsForSeq(rowIdx);
39
+
40
+ const sumLengths: number[] = new Array<number>(lengths.length + 1);
41
+ sumLengths[0] = this.leftPadding; // padding
42
+ for (let pos: number = 1; pos < sumLengths.length; pos++)
43
+ sumLengths[pos] = sumLengths[pos - 1] + lengths[pos - 1];
44
+ return [allParts, lengths, sumLengths];
45
+ }
46
+
47
+ private getCellMonomerLengthsForSeq(rowIdx: number): [string[], number[]] {
48
+ const allParts: string[] = this._allPartsList[rowIdx] = this.getAllParts(rowIdx);
49
+ const lengths: number[] = this._lengthsList[rowIdx] = new Array<number>(allParts.length);
50
+
51
+ for (const [part, partI] of wu.enumerate(allParts)) {
52
+ const partWidth: number = part.length * this.monomerCharWidth;
53
+ lengths[partI] = partWidth;
54
+ }
55
+
56
+ return [allParts, lengths];
57
+ }
58
+
59
+ getAllParts(rowIdx: number): string[] {
60
+ const seq: string | null = this.col.get(rowIdx);
61
+ const subParts: string[] = parseHelm(seq);
62
+ return seq ? getParts(subParts, seq) : [];
63
+ }
64
+
65
+ getMonomer(monomerSymbol: any): Monomer | null {
66
+ let res: Monomer = null;
67
+ for (const polymerType of this.monomerLib.getPolymerTypes()) {
68
+ res = this.monomerLib.getMonomer(polymerType, monomerSymbol);
69
+ if (res) break;
70
+ }
71
+ return res ?? null;
72
+ }
73
+
74
+ public static getOrCreate(col: DG.Column<string>): HelmMonomerPlacer {
75
+ if (!(Temps.helmMonomerPlacer in col.temp)) col.temp[Temps.helmMonomerPlacer] = new HelmMonomerPlacer(col);
76
+ return col.temp[Temps.helmMonomerPlacer];
77
+ }
78
+ }
@@ -4,6 +4,7 @@ import {runTests, tests, TestContext} from '@datagrok-libraries/utils/src/test';
4
4
 
5
5
  import './tests/helm-tests.ts';
6
6
  import './tests/findMonomers-tests';
7
+ import './tests/renderers-tests';
7
8
 
8
9
  export const _package = new DG.Package();
9
10
  export {tests};
package/src/package.ts CHANGED
@@ -17,10 +17,13 @@ let monomerLib: IMonomerLib | null = null;
17
17
 
18
18
  //tags: init
19
19
  export async function initHelm(): Promise<void> {
20
- return Promise.all([new Promise((resolve, reject) => {
21
- // @ts-ignore
22
- dojo.ready(function() { resolve(null); });
23
- }), grok.functions.call('Bio:getBioLib')])
20
+ return Promise.all([
21
+ new Promise((resolve, reject) => {
22
+ // @ts-ignore
23
+ dojo.ready(function() { resolve(null); });
24
+ }),
25
+ grok.functions.call('Bio:getBioLib'),
26
+ ])
24
27
  .then(([_, lib]: [void, IMonomerLib]) => {
25
28
  monomerLib = lib;
26
29
  rewriteLibraries(); // initHelm()
@@ -51,6 +54,10 @@ export async function initHelm(): Promise<void> {
51
54
  });
52
55
  }
53
56
 
57
+ export function getMonomerLib(): IMonomerLib {
58
+ return monomerLib;
59
+ }
60
+
54
61
  function rewriteLibraries() {
55
62
  org.helm.webeditor.Monomers.clear();
56
63
  monomerLib.getPolymerTypes().forEach((polymerType) => {
@@ -4,7 +4,7 @@ import * as DG from 'datagrok-api/dg';
4
4
 
5
5
  import {_package} from '../package-test';
6
6
  import {after, before, category, delay, expect, expectObject, test} from '@datagrok-libraries/utils/src/test';
7
- import {findMonomers} from '../utils';
7
+ import {findMonomers, parseHelm} from '../utils';
8
8
  import {getMonomerLibHelper, IMonomerLibHelper} from '@datagrok-libraries/bio/src/monomer-works/monomer-utils';
9
9
 
10
10
  const LIB_STORAGE_NAME = 'Libraries';
@@ -50,7 +50,8 @@ category('findMonomers', () => {
50
50
  }
51
51
 
52
52
  function _testFindMonomers(testHelmValue: string, tgtMissedSet: Set<string>): void {
53
- const resMissedSet: Set<string> = findMonomers(testHelmValue);
53
+ const monomerSymbolList: string[] = parseHelm(testHelmValue);
54
+ const resMissedSet: Set<string> = findMonomers(monomerSymbolList);
54
55
  expectObject(resMissedSet, tgtMissedSet);
55
56
  }
56
57
  });
@@ -2,11 +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 {_package} from '../package-test';
6
5
  import {after, before, category, delay, expect, test, expectArray} from '@datagrok-libraries/utils/src/test';
7
6
  //import {findMonomers, helmToFasta, helmToPeptide, helmToRNA, initHelm} from '../package';
8
7
  import {parseHelm} from '../utils';
9
8
 
9
+ import {_package} from '../package-test';
10
10
 
11
11
  category('Helm', () => {
12
12
  //These tests require webservice that is not present on test stand
@@ -0,0 +1,35 @@
1
+ import * as grok from 'datagrok-api/grok';
2
+ import * as ui from 'datagrok-api/ui';
3
+ import * as DG from 'datagrok-api/dg';
4
+
5
+ import $ from 'cash-dom';
6
+
7
+ import {category, expect, test, awaitCheck, delay} from '@datagrok-libraries/utils/src/test';
8
+ import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
9
+ import {TAGS as helmTAGS} from '../constants';
10
+
11
+ category('renderers', () => {
12
+ test('missedInLib', async () => { await _testMissedInLib(); });
13
+ });
14
+
15
+ async function _testMissedInLib() {
16
+ const df: DG.DataFrame = await grok.dapi.files.readCsv('System:AppData/Helm/tests/sample_HELM-missedInLib.csv');
17
+ const helmCol: DG.Column<string> = df.getCol('HELM');
18
+
19
+ const tv: DG.TableView = grok.shell.addTableView(df);
20
+ await awaitCheck(() => {
21
+ return $(tv.root).find('.d4-grid canvas').length > 0;
22
+ }, 'Table view canvas not found', 100);
23
+
24
+ expect(helmCol.semType, DG.SEMTYPE.MACROMOLECULE);
25
+ expect(helmCol.getTag(DG.TAGS.UNITS), NOTATION.HELM);
26
+ expect(helmCol.getTag(DG.TAGS.CELL_RENDERER), 'helm');
27
+
28
+ const cellRendererErrorJson: string = helmCol.getTag(helmTAGS.cellRendererRenderError);
29
+ if (!!cellRendererErrorJson) {
30
+ const cellRendererError: any = JSON.parse(cellRendererErrorJson);
31
+ const err = new Error(cellRendererError['message']);
32
+ err.stack = cellRendererError['stack'];
33
+ throw err;
34
+ }
35
+ }
package/src/utils.ts CHANGED
@@ -60,7 +60,7 @@ export function parseHelm(s: string) {
60
60
  const firstPiece = monomer.substring(0, monomer.indexOf('('));
61
61
  const thirdPiece = monomer.substring(monomer.lastIndexOf(')') + 1);
62
62
  const secondPiece = monomer.substring(firstPiece.length + 1, monomer.length - thirdPiece.length - 1);
63
- const elements =[firstPiece, secondPiece, thirdPiece];
63
+ const elements = [firstPiece, secondPiece, thirdPiece];
64
64
  for (const el of elements)
65
65
  ss.push(el);
66
66
  } else {
@@ -94,7 +94,8 @@ export function parseHelm(s: string) {
94
94
  // return new Set(helmPartList.filter((val) => !monomerNameList.includes(val)));
95
95
  // }
96
96
 
97
- export function findMonomers(helmString: string) {
97
+ /** Searches monomers of {@link helmString} for missed. */
98
+ export function findMonomers(monomerSymbolList: string[]): Set<string> {
98
99
  //@ts-ignore
99
100
  const types = Object.keys(org.helm.webeditor.monomerTypeList());
100
101
  const monomers: any = [];
@@ -107,8 +108,7 @@ export function findMonomers(helmString: string) {
107
108
  monomerNames.push(monomers[i][k].id);
108
109
  });
109
110
  }
110
- const splitString = parseHelm(helmString);
111
- return new Set(splitString.filter((val) => !monomerNames.includes(val)));
111
+ return new Set(monomerSymbolList.filter((val) => !monomerNames.includes(val)));
112
112
  }
113
113
 
114
114
  function split(s: string, sep: string) {
@@ -192,14 +192,14 @@ function unescape(s: string) {
192
192
 
193
193
  return s.replace(/[\\]./g, function(m) {
194
194
  switch (m) {
195
- case '\\r':
196
- return '\r';
197
- case '\\n':
198
- return '\n';
199
- case '\\t':
200
- return '\t';
201
- default:
202
- return m.substring(1);
195
+ case '\\r':
196
+ return '\r';
197
+ case '\\n':
198
+ return '\n';
199
+ case '\\t':
200
+ return '\t';
201
+ default:
202
+ return m.substring(1);
203
203
  }
204
204
  });
205
205
  }