@jbrowse/plugin-alignments 2.6.2 → 2.6.3

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.
@@ -0,0 +1,499 @@
1
+ import { lazy } from 'react';
2
+ import { autorun, observable } from 'mobx';
3
+ import { cast, types, addDisposer, getSnapshot } from 'mobx-state-tree';
4
+ import copy from 'copy-to-clipboard';
5
+ import { ConfigurationReference, readConfObject, getConf, } from '@jbrowse/core/configuration';
6
+ import { getRpcSessionId } from '@jbrowse/core/util/tracks';
7
+ import { getEnv, getSession, isSessionModelWithWidgets, getContainingView, SimpleFeature, } from '@jbrowse/core/util';
8
+ import { BaseLinearDisplay, } from '@jbrowse/plugin-linear-genome-view';
9
+ // icons
10
+ import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons';
11
+ import MenuOpenIcon from '@mui/icons-material/MenuOpen';
12
+ import FilterListIcon from '@mui/icons-material/ClearAll';
13
+ // locals
14
+ import LinearPileupDisplayBlurb from './components/LinearPileupDisplayBlurb';
15
+ import { getUniqueTagValues, FilterModel } from '../shared';
16
+ import { createAutorun } from '../util';
17
+ import { ColorByModel } from '../shared/color';
18
+ // async
19
+ const FilterByTagDlg = lazy(() => import('../shared/FilterByTag'));
20
+ const ColorByTagDlg = lazy(() => import('./components/ColorByTag'));
21
+ const SetFeatureHeightDlg = lazy(() => import('./components/SetFeatureHeight'));
22
+ const SetMaxHeightDlg = lazy(() => import('./components/SetMaxHeight'));
23
+ // using a map because it preserves order
24
+ const rendererTypes = new Map([
25
+ ['pileup', 'PileupRenderer'],
26
+ ['svg', 'SvgFeatureRenderer'],
27
+ ]);
28
+ /**
29
+ * #stateModel SharedLinearPileupDisplayMixin
30
+ * #category display
31
+ * extends `BaseLinearDisplay`
32
+ */
33
+ export function SharedLinearPileupDisplayMixin(configSchema) {
34
+ return types
35
+ .compose(BaseLinearDisplay, types.model({
36
+ /**
37
+ * #property
38
+ */
39
+ configuration: ConfigurationReference(configSchema),
40
+ /**
41
+ * #property
42
+ */
43
+ featureHeight: types.maybe(types.number),
44
+ /**
45
+ * #property
46
+ */
47
+ noSpacing: types.maybe(types.boolean),
48
+ /**
49
+ * #property
50
+ */
51
+ fadeLikelihood: types.maybe(types.boolean),
52
+ /**
53
+ * #property
54
+ */
55
+ trackMaxHeight: types.maybe(types.number),
56
+ /**
57
+ * #property
58
+ */
59
+ colorBy: ColorByModel,
60
+ /**
61
+ * #property
62
+ */
63
+ filterBy: types.optional(FilterModel, {}),
64
+ }))
65
+ .volatile(() => ({
66
+ colorTagMap: observable.map({}),
67
+ featureUnderMouseVolatile: undefined,
68
+ tagsReady: false,
69
+ }))
70
+ .views(self => ({
71
+ get autorunReady() {
72
+ const view = getContainingView(self);
73
+ return (view.initialized &&
74
+ self.featureDensityStatsReady &&
75
+ !self.regionTooLarge);
76
+ },
77
+ }))
78
+ .actions(self => ({
79
+ /**
80
+ * #action
81
+ */
82
+ setTagsReady(flag) {
83
+ self.tagsReady = flag;
84
+ },
85
+ /**
86
+ * #action
87
+ */
88
+ setMaxHeight(n) {
89
+ self.trackMaxHeight = n;
90
+ },
91
+ /**
92
+ * #action
93
+ */
94
+ setFeatureHeight(n) {
95
+ self.featureHeight = n;
96
+ },
97
+ /**
98
+ * #action
99
+ */
100
+ setNoSpacing(flag) {
101
+ self.noSpacing = flag;
102
+ },
103
+ /**
104
+ * #action
105
+ */
106
+ setColorScheme(colorScheme) {
107
+ self.colorTagMap = observable.map({}); // clear existing mapping
108
+ self.colorBy = cast(colorScheme);
109
+ if (colorScheme.tag) {
110
+ self.tagsReady = false;
111
+ }
112
+ },
113
+ /**
114
+ * #action
115
+ */
116
+ updateColorTagMap(uniqueTag) {
117
+ // pale color scheme
118
+ // https://cran.r-project.org/web/packages/khroma/vignettes/tol.html
119
+ // e.g. "tol_light"
120
+ const colorPalette = [
121
+ '#BBCCEE',
122
+ 'pink',
123
+ '#CCDDAA',
124
+ '#EEEEBB',
125
+ '#FFCCCC',
126
+ 'lightblue',
127
+ 'lightgreen',
128
+ 'tan',
129
+ '#CCEEFF',
130
+ 'lightsalmon',
131
+ ];
132
+ uniqueTag.forEach(value => {
133
+ if (!self.colorTagMap.has(value)) {
134
+ const totalKeys = [...self.colorTagMap.keys()].length;
135
+ self.colorTagMap.set(value, colorPalette[totalKeys]);
136
+ }
137
+ });
138
+ },
139
+ /**
140
+ * #action
141
+ */
142
+ setFeatureUnderMouse(feat) {
143
+ self.featureUnderMouseVolatile = feat;
144
+ },
145
+ /**
146
+ * #action
147
+ */
148
+ selectFeature(feature) {
149
+ const session = getSession(self);
150
+ if (isSessionModelWithWidgets(session)) {
151
+ const featureWidget = session.addWidget('AlignmentsFeatureWidget', 'alignmentFeature', { featureData: feature.toJSON(), view: getContainingView(self) });
152
+ session.showWidget(featureWidget);
153
+ }
154
+ session.setSelection(feature);
155
+ },
156
+ /**
157
+ * #action
158
+ * uses copy-to-clipboard and generates notification
159
+ */
160
+ copyFeatureToClipboard(feature) {
161
+ const { uniqueId, ...rest } = feature.toJSON();
162
+ const session = getSession(self);
163
+ copy(JSON.stringify(rest, null, 4));
164
+ session.notify('Copied to clipboard', 'success');
165
+ },
166
+ /**
167
+ * #action
168
+ */
169
+ setConfig(conf) {
170
+ self.configuration = conf;
171
+ },
172
+ /**
173
+ * #action
174
+ */
175
+ setFilterBy(filter) {
176
+ self.filterBy = cast(filter);
177
+ },
178
+ }))
179
+ .views(self => ({
180
+ /**
181
+ * #getter
182
+ */
183
+ get rendererConfig() {
184
+ const { featureHeight, noSpacing, trackMaxHeight, rendererTypeName } = self;
185
+ const configBlob = getConf(self, ['renderers', rendererTypeName]) || {};
186
+ return self.rendererType.configSchema.create({
187
+ ...configBlob,
188
+ ...(featureHeight !== undefined ? { height: featureHeight } : {}),
189
+ ...(noSpacing !== undefined ? { noSpacing } : {}),
190
+ ...(trackMaxHeight !== undefined
191
+ ? { maxHeight: trackMaxHeight }
192
+ : {}),
193
+ }, getEnv(self));
194
+ },
195
+ }))
196
+ .views(self => ({
197
+ /**
198
+ * #getter
199
+ */
200
+ get maxHeight() {
201
+ return readConfObject(self.rendererConfig, 'maxHeight');
202
+ },
203
+ /**
204
+ * #getter
205
+ */
206
+ get featureHeightSetting() {
207
+ return readConfObject(self.rendererConfig, 'height');
208
+ },
209
+ /**
210
+ * #getter
211
+ */
212
+ get featureUnderMouse() {
213
+ return self.featureUnderMouseVolatile;
214
+ },
215
+ /**
216
+ * #getter
217
+ */
218
+ renderReady() {
219
+ return self.tagsReady;
220
+ },
221
+ }))
222
+ .views(self => {
223
+ const { trackMenuItems: superTrackMenuItems, renderProps: superRenderProps, } = self;
224
+ return {
225
+ /**
226
+ * #getter
227
+ */
228
+ get rendererTypeName() {
229
+ const viewName = getConf(self, 'defaultRendering');
230
+ const rendererType = rendererTypes.get(viewName);
231
+ if (!rendererType) {
232
+ throw new Error(`unknown alignments view name ${viewName}`);
233
+ }
234
+ return rendererType;
235
+ },
236
+ /**
237
+ * #method
238
+ */
239
+ contextMenuItems() {
240
+ const feat = self.contextMenuFeature;
241
+ return feat
242
+ ? [
243
+ {
244
+ label: 'Open feature details',
245
+ icon: MenuOpenIcon,
246
+ onClick: () => {
247
+ self.clearFeatureSelection();
248
+ if (feat) {
249
+ self.selectFeature(feat);
250
+ }
251
+ },
252
+ },
253
+ {
254
+ label: 'Copy info to clipboard',
255
+ icon: ContentCopyIcon,
256
+ onClick: () => {
257
+ if (feat) {
258
+ self.copyFeatureToClipboard(feat);
259
+ }
260
+ },
261
+ },
262
+ ]
263
+ : [];
264
+ },
265
+ /**
266
+ * #getter
267
+ */
268
+ get DisplayBlurb() {
269
+ return LinearPileupDisplayBlurb;
270
+ },
271
+ /**
272
+ * #method
273
+ */
274
+ renderPropsPre() {
275
+ const { colorTagMap, colorBy, filterBy, rpcDriverName } = self;
276
+ const superProps = superRenderProps();
277
+ return {
278
+ ...superProps,
279
+ notReady: superProps.notReady || !self.renderReady(),
280
+ rpcDriverName,
281
+ displayModel: self,
282
+ colorBy: colorBy ? getSnapshot(colorBy) : undefined,
283
+ filterBy: JSON.parse(JSON.stringify(filterBy)),
284
+ colorTagMap: Object.fromEntries(colorTagMap.toJSON()),
285
+ config: self.rendererConfig,
286
+ async onFeatureClick(_, featureId) {
287
+ const session = getSession(self);
288
+ const { rpcManager } = session;
289
+ try {
290
+ const f = featureId || self.featureIdUnderMouse;
291
+ if (!f) {
292
+ self.clearFeatureSelection();
293
+ }
294
+ else {
295
+ const sessionId = getRpcSessionId(self);
296
+ const { feature } = (await rpcManager.call(sessionId, 'CoreGetFeatureDetails', {
297
+ featureId: f,
298
+ sessionId,
299
+ layoutId: getContainingView(self).id,
300
+ rendererType: 'PileupRenderer',
301
+ }));
302
+ if (feature) {
303
+ self.selectFeature(new SimpleFeature(feature));
304
+ }
305
+ }
306
+ }
307
+ catch (e) {
308
+ console.error(e);
309
+ session.notify(`${e}`);
310
+ }
311
+ },
312
+ onClick() {
313
+ self.clearFeatureSelection();
314
+ },
315
+ // similar to click but opens a menu with further options
316
+ async onFeatureContextMenu(_, featureId) {
317
+ const session = getSession(self);
318
+ const { rpcManager } = session;
319
+ try {
320
+ const f = featureId || self.featureIdUnderMouse;
321
+ if (!f) {
322
+ self.clearFeatureSelection();
323
+ }
324
+ else {
325
+ const sessionId = getRpcSessionId(self);
326
+ const { feature } = (await rpcManager.call(sessionId, 'CoreGetFeatureDetails', {
327
+ featureId: f,
328
+ sessionId,
329
+ layoutId: getContainingView(self).id,
330
+ rendererType: 'PileupRenderer',
331
+ }));
332
+ if (feature) {
333
+ self.setContextMenuFeature(new SimpleFeature(feature));
334
+ }
335
+ }
336
+ }
337
+ catch (e) {
338
+ console.error(e);
339
+ session.notify(`${e}`);
340
+ }
341
+ },
342
+ };
343
+ },
344
+ /**
345
+ * #method
346
+ */
347
+ colorSchemeSubMenuItems() {
348
+ return [
349
+ {
350
+ label: 'Normal',
351
+ onClick: () => self.setColorScheme({ type: 'normal' }),
352
+ },
353
+ {
354
+ label: 'Mapping quality',
355
+ onClick: () => self.setColorScheme({ type: 'mappingQuality' }),
356
+ },
357
+ {
358
+ label: 'Strand',
359
+ onClick: () => self.setColorScheme({ type: 'strand' }),
360
+ },
361
+ {
362
+ label: 'Per-base quality',
363
+ onClick: () => self.setColorScheme({ type: 'perBaseQuality' }),
364
+ },
365
+ {
366
+ label: 'Per-base lettering',
367
+ onClick: () => self.setColorScheme({ type: 'perBaseLettering' }),
368
+ },
369
+ {
370
+ label: 'First-of-pair strand',
371
+ onClick: () => self.setColorScheme({ type: 'stranded' }),
372
+ },
373
+ {
374
+ label: 'Color by tag...',
375
+ onClick: () => {
376
+ getSession(self).queueDialog(doneCallback => [
377
+ ColorByTagDlg,
378
+ { model: self, handleClose: doneCallback },
379
+ ]);
380
+ },
381
+ },
382
+ ];
383
+ },
384
+ /**
385
+ * #method
386
+ */
387
+ trackMenuItems() {
388
+ return [
389
+ ...superTrackMenuItems(),
390
+ {
391
+ label: 'Filter by',
392
+ icon: FilterListIcon,
393
+ onClick: () => {
394
+ getSession(self).queueDialog(doneCallback => [
395
+ FilterByTagDlg,
396
+ { model: self, handleClose: doneCallback },
397
+ ]);
398
+ },
399
+ },
400
+ {
401
+ label: 'Set feature height',
402
+ subMenu: [
403
+ {
404
+ label: 'Normal',
405
+ onClick: () => {
406
+ self.setFeatureHeight(7);
407
+ self.setNoSpacing(false);
408
+ },
409
+ },
410
+ {
411
+ label: 'Compact',
412
+ onClick: () => {
413
+ self.setFeatureHeight(2);
414
+ self.setNoSpacing(true);
415
+ },
416
+ },
417
+ {
418
+ label: 'Manually set height',
419
+ onClick: () => {
420
+ getSession(self).queueDialog(doneCallback => [
421
+ SetFeatureHeightDlg,
422
+ { model: self, handleClose: doneCallback },
423
+ ]);
424
+ },
425
+ },
426
+ ],
427
+ },
428
+ {
429
+ label: 'Set max height...',
430
+ onClick: () => {
431
+ getSession(self).queueDialog(doneCallback => [
432
+ SetMaxHeightDlg,
433
+ { model: self, handleClose: doneCallback },
434
+ ]);
435
+ },
436
+ },
437
+ ];
438
+ },
439
+ };
440
+ })
441
+ .views(self => ({
442
+ renderProps() {
443
+ return self.renderPropsPre();
444
+ },
445
+ }))
446
+ .actions(self => ({
447
+ afterAttach() {
448
+ createAutorun(self, async () => {
449
+ const view = getContainingView(self);
450
+ if (!self.autorunReady) {
451
+ return;
452
+ }
453
+ const { colorBy } = self;
454
+ const { staticBlocks } = view;
455
+ if (colorBy === null || colorBy === void 0 ? void 0 : colorBy.tag) {
456
+ const vals = await getUniqueTagValues(self, colorBy, staticBlocks);
457
+ self.updateColorTagMap(vals);
458
+ }
459
+ self.setTagsReady(true);
460
+ }, { delay: 1000 });
461
+ // autorun synchronizes featureUnderMouse with featureIdUnderMouse
462
+ // asynchronously. this is needed due to how we do not serialize all
463
+ // features from the BAM/CRAM over the rpc
464
+ addDisposer(self, autorun(async () => {
465
+ var _a;
466
+ const session = getSession(self);
467
+ try {
468
+ const featureId = self.featureIdUnderMouse;
469
+ if (((_a = self.featureUnderMouse) === null || _a === void 0 ? void 0 : _a.id()) !== featureId) {
470
+ if (!featureId) {
471
+ self.setFeatureUnderMouse(undefined);
472
+ }
473
+ else {
474
+ const sessionId = getRpcSessionId(self);
475
+ const view = getContainingView(self);
476
+ const { feature } = (await session.rpcManager.call(sessionId, 'CoreGetFeatureDetails', {
477
+ featureId,
478
+ sessionId,
479
+ layoutId: view.id,
480
+ rendererType: 'PileupRenderer',
481
+ }));
482
+ // check featureIdUnderMouse is still the same as the
483
+ // feature.id that was returned e.g. that the user hasn't
484
+ // moused over to a new position during the async operation
485
+ // above
486
+ if (self.featureIdUnderMouse === feature.uniqueId) {
487
+ self.setFeatureUnderMouse(new SimpleFeature(feature));
488
+ }
489
+ }
490
+ }
491
+ }
492
+ catch (e) {
493
+ console.error(e);
494
+ session.notify(`${e}`, 'error');
495
+ }
496
+ }));
497
+ },
498
+ }));
499
+ }
@@ -2,3 +2,4 @@ import PluginManager from '@jbrowse/core/PluginManager';
2
2
  export default function register(pluginManager: PluginManager): void;
3
3
  export { default as linearPileupDisplayStateModelFactory } from './model';
4
4
  export { default as linearPileupDisplayConfigSchemaFactory } from './configSchema';
5
+ export { SharedLinearPileupDisplayMixin } from './SharedLinearPileupDisplayMixin';
@@ -19,3 +19,4 @@ export default function register(pluginManager) {
19
19
  }
20
20
  export { default as linearPileupDisplayStateModelFactory } from './model';
21
21
  export { default as linearPileupDisplayConfigSchemaFactory } from './configSchema';
22
+ export { SharedLinearPileupDisplayMixin } from './SharedLinearPileupDisplayMixin';