@jbrowse/plugin-canvas 4.0.0
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/LICENSE +201 -0
- package/esm/CanvasFeatureRenderer/CanvasFeatureRenderer.d.ts +6 -0
- package/esm/CanvasFeatureRenderer/CanvasFeatureRenderer.js +30 -0
- package/esm/CanvasFeatureRenderer/CanvasFeatureRendering.d.ts +17 -0
- package/esm/CanvasFeatureRenderer/CanvasFeatureRendering.js +192 -0
- package/esm/CanvasFeatureRenderer/configSchema.d.ts +136 -0
- package/esm/CanvasFeatureRenderer/configSchema.js +152 -0
- package/esm/CanvasFeatureRenderer/configSchema2.d.ts +136 -0
- package/esm/CanvasFeatureRenderer/configSchema2.js +5 -0
- package/esm/CanvasFeatureRenderer/doAll.d.ts +21 -0
- package/esm/CanvasFeatureRenderer/doAll.js +42 -0
- package/esm/CanvasFeatureRenderer/drawChevrons.d.ts +1 -0
- package/esm/CanvasFeatureRenderer/drawChevrons.js +25 -0
- package/esm/CanvasFeatureRenderer/drawFeature.d.ts +3 -0
- package/esm/CanvasFeatureRenderer/drawFeature.js +27 -0
- package/esm/CanvasFeatureRenderer/filterSubparts.d.ts +5 -0
- package/esm/CanvasFeatureRenderer/filterSubparts.js +92 -0
- package/esm/CanvasFeatureRenderer/floatingLabels.d.ts +23 -0
- package/esm/CanvasFeatureRenderer/floatingLabels.js +65 -0
- package/esm/CanvasFeatureRenderer/glyphs/box.d.ts +2 -0
- package/esm/CanvasFeatureRenderer/glyphs/box.js +87 -0
- package/esm/CanvasFeatureRenderer/glyphs/cds.d.ts +2 -0
- package/esm/CanvasFeatureRenderer/glyphs/cds.js +93 -0
- package/esm/CanvasFeatureRenderer/glyphs/childGlyphs.d.ts +5 -0
- package/esm/CanvasFeatureRenderer/glyphs/childGlyphs.js +18 -0
- package/esm/CanvasFeatureRenderer/glyphs/glyphUtils.d.ts +14 -0
- package/esm/CanvasFeatureRenderer/glyphs/glyphUtils.js +125 -0
- package/esm/CanvasFeatureRenderer/glyphs/index.d.ts +9 -0
- package/esm/CanvasFeatureRenderer/glyphs/index.js +29 -0
- package/esm/CanvasFeatureRenderer/glyphs/matureProteinRegion.d.ts +2 -0
- package/esm/CanvasFeatureRenderer/glyphs/matureProteinRegion.js +121 -0
- package/esm/CanvasFeatureRenderer/glyphs/processed.d.ts +2 -0
- package/esm/CanvasFeatureRenderer/glyphs/processed.js +53 -0
- package/esm/CanvasFeatureRenderer/glyphs/repeatRegion.d.ts +2 -0
- package/esm/CanvasFeatureRenderer/glyphs/repeatRegion.js +104 -0
- package/esm/CanvasFeatureRenderer/glyphs/segments.d.ts +2 -0
- package/esm/CanvasFeatureRenderer/glyphs/segments.js +65 -0
- package/esm/CanvasFeatureRenderer/glyphs/subfeatures.d.ts +2 -0
- package/esm/CanvasFeatureRenderer/glyphs/subfeatures.js +132 -0
- package/esm/CanvasFeatureRenderer/index.d.ts +3 -0
- package/esm/CanvasFeatureRenderer/index.js +23 -0
- package/esm/CanvasFeatureRenderer/labelUtils.d.ts +9 -0
- package/esm/CanvasFeatureRenderer/labelUtils.js +35 -0
- package/esm/CanvasFeatureRenderer/layout/index.d.ts +3 -0
- package/esm/CanvasFeatureRenderer/layout/index.js +3 -0
- package/esm/CanvasFeatureRenderer/layout/layoutFeature.d.ts +14 -0
- package/esm/CanvasFeatureRenderer/layout/layoutFeature.js +40 -0
- package/esm/CanvasFeatureRenderer/layout/layoutFeatures.d.ts +13 -0
- package/esm/CanvasFeatureRenderer/layout/layoutFeatures.js +59 -0
- package/esm/CanvasFeatureRenderer/layout/layoutUtils.d.ts +19 -0
- package/esm/CanvasFeatureRenderer/layout/layoutUtils.js +129 -0
- package/esm/CanvasFeatureRenderer/makeImageData.d.ts +14 -0
- package/esm/CanvasFeatureRenderer/makeImageData.js +90 -0
- package/esm/CanvasFeatureRenderer/peptides/aggregateAminoAcids.d.ts +8 -0
- package/esm/CanvasFeatureRenderer/peptides/aggregateAminoAcids.js +45 -0
- package/esm/CanvasFeatureRenderer/peptides/drawCDSBackground.d.ts +16 -0
- package/esm/CanvasFeatureRenderer/peptides/drawCDSBackground.js +24 -0
- package/esm/CanvasFeatureRenderer/peptides/drawPeptidesOnCDS.d.ts +15 -0
- package/esm/CanvasFeatureRenderer/peptides/drawPeptidesOnCDS.js +28 -0
- package/esm/CanvasFeatureRenderer/peptides/index.d.ts +5 -0
- package/esm/CanvasFeatureRenderer/peptides/index.js +5 -0
- package/esm/CanvasFeatureRenderer/peptides/peptideUtils.d.ts +5 -0
- package/esm/CanvasFeatureRenderer/peptides/peptideUtils.js +129 -0
- package/esm/CanvasFeatureRenderer/peptides/prepareAminoAcidData.d.ts +9 -0
- package/esm/CanvasFeatureRenderer/peptides/prepareAminoAcidData.js +5 -0
- package/esm/CanvasFeatureRenderer/renderConfig.d.ts +26 -0
- package/esm/CanvasFeatureRenderer/renderConfig.js +35 -0
- package/esm/CanvasFeatureRenderer/types.d.ts +97 -0
- package/esm/CanvasFeatureRenderer/types.js +1 -0
- package/esm/CanvasFeatureRenderer/util.d.ts +22 -0
- package/esm/CanvasFeatureRenderer/util.js +50 -0
- package/esm/CanvasFeatureRenderer/zoomThresholds.d.ts +4 -0
- package/esm/CanvasFeatureRenderer/zoomThresholds.js +8 -0
- package/esm/glyphs/index.d.ts +2 -0
- package/esm/glyphs/index.js +2 -0
- package/esm/index.d.ts +6 -0
- package/esm/index.js +10 -0
- package/package.json +53 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readConfObject } from '@jbrowse/core/configuration';
|
|
2
|
+
import { createSubfeatureLabelMetadata } from '@jbrowse/plugin-linear-genome-view';
|
|
3
|
+
import { createTranscriptFloatingLabel } from "../floatingLabels.js";
|
|
4
|
+
import { builtinGlyphs } from "../glyphs/index.js";
|
|
5
|
+
export function convertToCanvasCoords(layout, offsetX, offsetY) {
|
|
6
|
+
const canvasX = offsetX + layout.x;
|
|
7
|
+
const canvasY = offsetY + layout.y;
|
|
8
|
+
return {
|
|
9
|
+
...layout,
|
|
10
|
+
x: canvasX,
|
|
11
|
+
y: canvasY,
|
|
12
|
+
children: layout.children.map(child => convertToCanvasCoords(child, canvasX, canvasY)),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function findMatchingGlyph(feature, configContext, pluginManager) {
|
|
16
|
+
const builtinMatch = builtinGlyphs.find(glyph => glyph.hasIndexableChildren && glyph.match(feature, configContext));
|
|
17
|
+
if (builtinMatch) {
|
|
18
|
+
return builtinMatch;
|
|
19
|
+
}
|
|
20
|
+
if (pluginManager) {
|
|
21
|
+
const glyphTypes = pluginManager.getGlyphTypes();
|
|
22
|
+
const sortedGlyphs = [...glyphTypes].sort((a, b) => b.priority - a.priority);
|
|
23
|
+
const pluggableMatch = sortedGlyphs.find(glyph => glyph.match?.(feature) && glyph.getChildFeatures);
|
|
24
|
+
if (pluggableMatch) {
|
|
25
|
+
return pluggableMatch;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
export function buildChildrenIndex({ layout, featureLayout, subfeatureCoords, subfeatureInfos, config, configContext, pluginManager, subfeatureLabels, transcriptTypes, labelColor, parentTooltip, }) {
|
|
31
|
+
addChildrenRecursive({
|
|
32
|
+
layout,
|
|
33
|
+
featureLayout,
|
|
34
|
+
subfeatureCoords,
|
|
35
|
+
subfeatureInfos,
|
|
36
|
+
config,
|
|
37
|
+
configContext,
|
|
38
|
+
pluginManager,
|
|
39
|
+
subfeatureLabels,
|
|
40
|
+
transcriptTypes,
|
|
41
|
+
labelColor,
|
|
42
|
+
parentTooltip,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function addChildrenRecursive({ layout, featureLayout, subfeatureCoords, subfeatureInfos, config, configContext, pluginManager, subfeatureLabels, transcriptTypes, labelColor, parentTooltip, }) {
|
|
46
|
+
const feature = featureLayout.feature;
|
|
47
|
+
const featureType = feature.get('type');
|
|
48
|
+
const isGene = featureType === 'gene';
|
|
49
|
+
const matchingGlyph = findMatchingGlyph(feature, configContext, pluginManager);
|
|
50
|
+
const hasTranscriptChildren = isGene &&
|
|
51
|
+
featureLayout.children.some(child => transcriptTypes.includes(child.feature.get('type')));
|
|
52
|
+
const shouldIndexChildren = matchingGlyph || hasTranscriptChildren;
|
|
53
|
+
const showSubfeatureLabels = subfeatureLabels !== 'none';
|
|
54
|
+
if (shouldIndexChildren) {
|
|
55
|
+
for (const child of featureLayout.children) {
|
|
56
|
+
const childFeature = child.feature;
|
|
57
|
+
const childType = childFeature.get('type');
|
|
58
|
+
if (hasTranscriptChildren && !matchingGlyph) {
|
|
59
|
+
if (!transcriptTypes.includes(childType)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const leftPx = child.x;
|
|
64
|
+
const rightPx = child.x + child.totalLayoutWidth;
|
|
65
|
+
const topPx = child.y;
|
|
66
|
+
const bottomPx = child.y + child.totalLayoutHeight;
|
|
67
|
+
subfeatureCoords.push(leftPx, topPx, rightPx, bottomPx);
|
|
68
|
+
const glyphMouseover = matchingGlyph?.getSubfeatureMouseover?.(childFeature);
|
|
69
|
+
const displayLabel = glyphMouseover ??
|
|
70
|
+
String(readConfObject(config, 'subfeatureMouseover', {
|
|
71
|
+
feature: childFeature,
|
|
72
|
+
}) || '');
|
|
73
|
+
subfeatureInfos.push({
|
|
74
|
+
featureId: childFeature.id(),
|
|
75
|
+
parentFeatureId: featureLayout.feature.id(),
|
|
76
|
+
displayLabel,
|
|
77
|
+
type: childType,
|
|
78
|
+
leftPx,
|
|
79
|
+
topPx,
|
|
80
|
+
rightPx,
|
|
81
|
+
bottomPx,
|
|
82
|
+
});
|
|
83
|
+
const floatingLabels = [];
|
|
84
|
+
if (showSubfeatureLabels && displayLabel) {
|
|
85
|
+
const label = createTranscriptFloatingLabel({
|
|
86
|
+
displayLabel,
|
|
87
|
+
featureHeight: child.height,
|
|
88
|
+
subfeatureLabels,
|
|
89
|
+
color: labelColor,
|
|
90
|
+
parentFeatureId: featureLayout.feature.id(),
|
|
91
|
+
subfeatureId: childFeature.id(),
|
|
92
|
+
tooltip: matchingGlyph ? displayLabel : parentTooltip,
|
|
93
|
+
});
|
|
94
|
+
if (label) {
|
|
95
|
+
floatingLabels.push(label);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const childStart = childFeature.get('start');
|
|
99
|
+
const childEnd = childFeature.get('end');
|
|
100
|
+
layout.addRect(childFeature.id(), childStart, childEnd, bottomPx - topPx, childFeature, showSubfeatureLabels && floatingLabels.length > 0
|
|
101
|
+
? createSubfeatureLabelMetadata({
|
|
102
|
+
refName: childFeature.get('refName'),
|
|
103
|
+
floatingLabels,
|
|
104
|
+
totalFeatureHeight: child.height,
|
|
105
|
+
totalLayoutWidth: child.totalLayoutWidth,
|
|
106
|
+
actualTopPx: topPx,
|
|
107
|
+
featureWidth: child.width,
|
|
108
|
+
featureStartBp: childStart,
|
|
109
|
+
featureEndBp: childEnd,
|
|
110
|
+
})
|
|
111
|
+
: { refName: childFeature.get('refName') });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const child of featureLayout.children) {
|
|
115
|
+
addChildrenRecursive({
|
|
116
|
+
layout,
|
|
117
|
+
featureLayout: child,
|
|
118
|
+
subfeatureCoords,
|
|
119
|
+
subfeatureInfos,
|
|
120
|
+
config,
|
|
121
|
+
configContext,
|
|
122
|
+
pluginManager,
|
|
123
|
+
subfeatureLabels,
|
|
124
|
+
transcriptTypes,
|
|
125
|
+
labelColor,
|
|
126
|
+
parentTooltip,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RenderConfigContext } from './renderConfig.ts';
|
|
2
|
+
import type { FlatbushItem, LayoutRecord, RenderArgs, SubfeatureInfo } from './types.ts';
|
|
3
|
+
export declare function makeImageData({ ctx, layoutRecords, canvasWidth, renderArgs, configContext, }: {
|
|
4
|
+
ctx: CanvasRenderingContext2D;
|
|
5
|
+
layoutRecords: LayoutRecord[];
|
|
6
|
+
canvasWidth: number;
|
|
7
|
+
renderArgs: RenderArgs;
|
|
8
|
+
configContext: RenderConfigContext;
|
|
9
|
+
}): {
|
|
10
|
+
flatbush: ArrayBufferLike;
|
|
11
|
+
items: FlatbushItem[];
|
|
12
|
+
subfeatureFlatbush: ArrayBufferLike;
|
|
13
|
+
subfeatureInfos: SubfeatureInfo[];
|
|
14
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readConfObject } from '@jbrowse/core/configuration';
|
|
2
|
+
import { createJBrowseTheme } from '@jbrowse/core/ui';
|
|
3
|
+
import { bpToPx } from '@jbrowse/core/util';
|
|
4
|
+
import Flatbush from '@jbrowse/core/util/flatbush';
|
|
5
|
+
import { checkStopToken2, createStopTokenChecker, } from '@jbrowse/core/util/stopToken';
|
|
6
|
+
import { drawFeature } from "./drawFeature.js";
|
|
7
|
+
import { buildChildrenIndex, convertToCanvasCoords, } from "./layout/layoutUtils.js";
|
|
8
|
+
function buildFlatbush(coords, count) {
|
|
9
|
+
const fb = new Flatbush(Math.max(count, 1));
|
|
10
|
+
if (coords.length) {
|
|
11
|
+
for (let i = 0; i < coords.length; i += 4) {
|
|
12
|
+
fb.add(coords[i], coords[i + 1], coords[i + 2], coords[i + 3]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
fb.add(0, 0, 0, 0);
|
|
17
|
+
}
|
|
18
|
+
fb.finish();
|
|
19
|
+
return fb;
|
|
20
|
+
}
|
|
21
|
+
export function makeImageData({ ctx, layoutRecords, canvasWidth, renderArgs, configContext, }) {
|
|
22
|
+
const { bpPerPx, regions, theme: configTheme, stopToken, layout, peptideDataMap, colorByCDS, pluginManager, } = renderArgs;
|
|
23
|
+
const region = regions[0];
|
|
24
|
+
const theme = createJBrowseTheme(configTheme);
|
|
25
|
+
const drawContext = {
|
|
26
|
+
region,
|
|
27
|
+
bpPerPx,
|
|
28
|
+
configContext,
|
|
29
|
+
theme,
|
|
30
|
+
canvasWidth,
|
|
31
|
+
peptideDataMap,
|
|
32
|
+
colorByCDS,
|
|
33
|
+
};
|
|
34
|
+
const coords = [];
|
|
35
|
+
const items = [];
|
|
36
|
+
const subfeatureCoords = [];
|
|
37
|
+
const subfeatureInfos = [];
|
|
38
|
+
ctx.textBaseline = 'top';
|
|
39
|
+
ctx.textAlign = 'left';
|
|
40
|
+
const { subfeatureLabels, transcriptTypes, config } = configContext;
|
|
41
|
+
const lastCheck = createStopTokenChecker(stopToken);
|
|
42
|
+
for (const record of layoutRecords) {
|
|
43
|
+
const { feature, layout: featureLayout, topPx: recordTopPx, label, description, } = record;
|
|
44
|
+
const featureStartBp = feature.get(region.reversed ? 'end' : 'start');
|
|
45
|
+
const startPx = bpToPx(featureStartBp, region, bpPerPx);
|
|
46
|
+
const canvasLayout = convertToCanvasCoords(featureLayout, startPx, recordTopPx);
|
|
47
|
+
drawFeature(ctx, canvasLayout, drawContext, pluginManager);
|
|
48
|
+
const bounds = {
|
|
49
|
+
left: canvasLayout.x,
|
|
50
|
+
right: canvasLayout.x + canvasLayout.totalLayoutWidth,
|
|
51
|
+
top: canvasLayout.y,
|
|
52
|
+
bottom: canvasLayout.y + canvasLayout.totalLayoutHeight,
|
|
53
|
+
};
|
|
54
|
+
const tooltip = String(readConfObject(config, 'mouseover', { feature, label, description }) ||
|
|
55
|
+
'');
|
|
56
|
+
coords.push(bounds.left, bounds.top, bounds.right, bounds.bottom);
|
|
57
|
+
items.push({
|
|
58
|
+
featureId: feature.id(),
|
|
59
|
+
type: 'box',
|
|
60
|
+
startBp: feature.get('start'),
|
|
61
|
+
endBp: feature.get('end'),
|
|
62
|
+
leftPx: bounds.left,
|
|
63
|
+
rightPx: bounds.right,
|
|
64
|
+
topPx: bounds.top,
|
|
65
|
+
bottomPx: bounds.bottom,
|
|
66
|
+
tooltip,
|
|
67
|
+
});
|
|
68
|
+
buildChildrenIndex({
|
|
69
|
+
layout,
|
|
70
|
+
featureLayout: canvasLayout,
|
|
71
|
+
subfeatureCoords,
|
|
72
|
+
subfeatureInfos,
|
|
73
|
+
config,
|
|
74
|
+
configContext,
|
|
75
|
+
pluginManager,
|
|
76
|
+
subfeatureLabels,
|
|
77
|
+
transcriptTypes,
|
|
78
|
+
labelColor: theme.palette.text.primary,
|
|
79
|
+
parentTooltip: tooltip,
|
|
80
|
+
});
|
|
81
|
+
checkStopToken2(lastCheck);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
flatbush: buildFlatbush(coords, items.length).data,
|
|
85
|
+
items,
|
|
86
|
+
subfeatureFlatbush: buildFlatbush(subfeatureCoords, subfeatureInfos.length)
|
|
87
|
+
.data,
|
|
88
|
+
subfeatureInfos,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface AggregatedAminoAcid {
|
|
2
|
+
aminoAcid: string;
|
|
3
|
+
startIndex: number;
|
|
4
|
+
endIndex: number;
|
|
5
|
+
length: number;
|
|
6
|
+
proteinIndex: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function aggregateAminos(protein: string, g2p: Record<string, number>, featureStart: number, featureEnd: number, strand: number): AggregatedAminoAcid[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function aggregateAminos(protein, g2p, featureStart, featureEnd, strand) {
|
|
2
|
+
const aggregated = [];
|
|
3
|
+
const len = featureEnd - featureStart;
|
|
4
|
+
let currentElt = undefined;
|
|
5
|
+
let currentAminoAcid = null;
|
|
6
|
+
let startIndex = 0;
|
|
7
|
+
let idx = 0;
|
|
8
|
+
for (let i = 0; i < len; i++) {
|
|
9
|
+
const pos = strand === -1 ? featureEnd - i : featureStart + i;
|
|
10
|
+
const elt = g2p[pos];
|
|
11
|
+
if (elt === undefined) {
|
|
12
|
+
console.warn(`No g2p mapping for position ${pos}`);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const aminoAcid = protein[elt] ?? '&';
|
|
16
|
+
if (currentElt === undefined) {
|
|
17
|
+
currentElt = elt;
|
|
18
|
+
currentAminoAcid = aminoAcid;
|
|
19
|
+
startIndex = idx;
|
|
20
|
+
}
|
|
21
|
+
else if (currentElt !== elt) {
|
|
22
|
+
aggregated.push({
|
|
23
|
+
aminoAcid: currentAminoAcid,
|
|
24
|
+
startIndex,
|
|
25
|
+
endIndex: idx - 1,
|
|
26
|
+
length: idx - startIndex,
|
|
27
|
+
proteinIndex: currentElt,
|
|
28
|
+
});
|
|
29
|
+
currentElt = elt;
|
|
30
|
+
currentAminoAcid = aminoAcid;
|
|
31
|
+
startIndex = idx;
|
|
32
|
+
}
|
|
33
|
+
idx++;
|
|
34
|
+
}
|
|
35
|
+
if (currentAminoAcid !== null) {
|
|
36
|
+
aggregated.push({
|
|
37
|
+
aminoAcid: currentAminoAcid,
|
|
38
|
+
startIndex,
|
|
39
|
+
endIndex: idx - 1,
|
|
40
|
+
length: idx - startIndex,
|
|
41
|
+
proteinIndex: currentElt,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return aggregated;
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AggregatedAminoAcid } from './prepareAminoAcidData.ts';
|
|
2
|
+
interface DrawCDSBackgroundArgs {
|
|
3
|
+
ctx: CanvasRenderingContext2D;
|
|
4
|
+
aggregatedAminoAcids: AggregatedAminoAcid[];
|
|
5
|
+
baseColor: string;
|
|
6
|
+
left: number;
|
|
7
|
+
top: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
bpPerPx: number;
|
|
11
|
+
strand: number;
|
|
12
|
+
reversed: boolean;
|
|
13
|
+
canvasWidth: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function drawCDSBackground(args: DrawCDSBackgroundArgs): void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { colord } from '@jbrowse/core/util/colord';
|
|
2
|
+
import { darken, lighten } from '@mui/material';
|
|
3
|
+
import { getEffectiveStrand, getStrandAwareX } from "../util.js";
|
|
4
|
+
export function drawCDSBackground(args) {
|
|
5
|
+
const { ctx, aggregatedAminoAcids, baseColor, left, top, width, height, bpPerPx, strand, reversed, canvasWidth, } = args;
|
|
6
|
+
const effectiveStrand = getEffectiveStrand(strand, reversed);
|
|
7
|
+
const pxPerBp = 1 / bpPerPx;
|
|
8
|
+
const baseHex = colord(baseColor).toHex();
|
|
9
|
+
const color1 = lighten(baseHex, 0.2);
|
|
10
|
+
const color2 = darken(baseHex, 0.1);
|
|
11
|
+
for (let i = 0, l = aggregatedAminoAcids.length; i < l; i++) {
|
|
12
|
+
const aa = aggregatedAminoAcids[i];
|
|
13
|
+
const bgColor = i % 2 === 1 ? color2 : color1;
|
|
14
|
+
const startX = getStrandAwareX(left, width, aa.startIndex, pxPerBp, effectiveStrand);
|
|
15
|
+
const endX = getStrandAwareX(left, width, aa.endIndex + 1, pxPerBp, effectiveStrand);
|
|
16
|
+
const rectLeft = Math.min(startX, endX);
|
|
17
|
+
const rectWidth = Math.abs(startX - endX);
|
|
18
|
+
if (rectLeft + rectWidth < 0 || rectLeft > canvasWidth) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
ctx.fillStyle = bgColor;
|
|
22
|
+
ctx.fillRect(rectLeft, top, rectWidth, height);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AggregatedAminoAcid } from './prepareAminoAcidData.ts';
|
|
2
|
+
interface DrawPeptidesArgs {
|
|
3
|
+
ctx: CanvasRenderingContext2D;
|
|
4
|
+
aggregatedAminoAcids: AggregatedAminoAcid[];
|
|
5
|
+
left: number;
|
|
6
|
+
top: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
bpPerPx: number;
|
|
10
|
+
strand: number;
|
|
11
|
+
reversed: boolean;
|
|
12
|
+
canvasWidth: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function drawPeptidesOnCDS(args: DrawPeptidesArgs): void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getEffectiveStrand, getStrandAwareX } from "../util.js";
|
|
2
|
+
export function drawPeptidesOnCDS(args) {
|
|
3
|
+
const { ctx, aggregatedAminoAcids, left, top, width, height, bpPerPx, strand, reversed, canvasWidth, } = args;
|
|
4
|
+
if (height < 8) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const effectiveStrand = getEffectiveStrand(strand, reversed);
|
|
8
|
+
const pxPerBp = 1 / bpPerPx;
|
|
9
|
+
const fontSize = height;
|
|
10
|
+
ctx.font = `${fontSize}px sans-serif`;
|
|
11
|
+
ctx.textAlign = 'center';
|
|
12
|
+
ctx.textBaseline = 'middle';
|
|
13
|
+
const yCenter = top + height / 2;
|
|
14
|
+
for (const aa of aggregatedAminoAcids) {
|
|
15
|
+
const startX = getStrandAwareX(left, width, aa.startIndex, pxPerBp, effectiveStrand);
|
|
16
|
+
const endX = getStrandAwareX(left, width, aa.endIndex + 1, pxPerBp, effectiveStrand);
|
|
17
|
+
const x = (startX + endX) / 2;
|
|
18
|
+
if (x < 0 || x > canvasWidth) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const isNonTriplet = aa.length % 3 !== 0 || aa.aminoAcid === '&';
|
|
22
|
+
ctx.fillStyle = isNonTriplet ? 'red' : 'black';
|
|
23
|
+
const text = isNonTriplet || aa.aminoAcid === '*' || aa.aminoAcid === '&'
|
|
24
|
+
? aa.aminoAcid
|
|
25
|
+
: `${aa.aminoAcid}${aa.proteinIndex + 1}`;
|
|
26
|
+
ctx.fillText(text, x, yCenter);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { type AggregatedAminoAcid, aggregateAminos, } from './aggregateAminoAcids.ts';
|
|
2
|
+
export { drawCDSBackground } from './drawCDSBackground.ts';
|
|
3
|
+
export { drawPeptidesOnCDS } from './drawPeptidesOnCDS.ts';
|
|
4
|
+
export { fetchPeptideData } from './peptideUtils.ts';
|
|
5
|
+
export { prepareAminoAcidData } from './prepareAminoAcidData.ts';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { aggregateAminos, } from "./aggregateAminoAcids.js";
|
|
2
|
+
export { drawCDSBackground } from "./drawCDSBackground.js";
|
|
3
|
+
export { drawPeptidesOnCDS } from "./drawPeptidesOnCDS.js";
|
|
4
|
+
export { fetchPeptideData } from "./peptideUtils.js";
|
|
5
|
+
export { prepareAminoAcidData } from "./prepareAminoAcidData.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PeptideData } from '../types.ts';
|
|
2
|
+
import type PluginManager from '@jbrowse/core/PluginManager';
|
|
3
|
+
import type { RenderArgsDeserialized } from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType';
|
|
4
|
+
import type { Feature } from '@jbrowse/core/util';
|
|
5
|
+
export declare function fetchPeptideData(pluginManager: PluginManager, renderProps: RenderArgsDeserialized, features: Map<string, Feature>): Promise<Map<string, PeptideData>>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache';
|
|
2
|
+
import { defaultCodonTable, generateCodonTable, revcom, } from '@jbrowse/core/util';
|
|
3
|
+
import { convertCodingSequenceToPeptides } from '@jbrowse/core/util/convertCodingSequenceToPeptides';
|
|
4
|
+
import { firstValueFrom, toArray } from 'rxjs';
|
|
5
|
+
import { shouldRenderPeptideBackground } from "../zoomThresholds.js";
|
|
6
|
+
async function fetchSequence(pluginManager, renderProps, region) {
|
|
7
|
+
const { sessionId, sequenceAdapter } = renderProps;
|
|
8
|
+
if (!sequenceAdapter) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const { dataAdapter } = await getAdapter(pluginManager, sessionId, sequenceAdapter);
|
|
13
|
+
const feats = await firstValueFrom(dataAdapter
|
|
14
|
+
.getFeatures({
|
|
15
|
+
...region,
|
|
16
|
+
refName: region.seqAdapterRefName ?? region.refName,
|
|
17
|
+
start: Math.max(0, region.start),
|
|
18
|
+
})
|
|
19
|
+
.pipe(toArray()));
|
|
20
|
+
return feats[0]?.get('seq');
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.warn('[fetchSequence] Failed to fetch sequence:', error);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const DEFAULT_TRANSCRIPT_TYPES = [
|
|
28
|
+
'mRNA',
|
|
29
|
+
'transcript',
|
|
30
|
+
'primary_transcript',
|
|
31
|
+
'protein_coding_primary_transcript',
|
|
32
|
+
];
|
|
33
|
+
function isTranscriptType(type, transcriptTypes = DEFAULT_TRANSCRIPT_TYPES) {
|
|
34
|
+
return transcriptTypes.includes(type);
|
|
35
|
+
}
|
|
36
|
+
function hasCDSSubfeatures(feature) {
|
|
37
|
+
const subfeatures = feature.get('subfeatures');
|
|
38
|
+
return subfeatures?.some((sub) => sub.get('type') === 'CDS') ?? false;
|
|
39
|
+
}
|
|
40
|
+
function findTranscriptsWithCDS(features) {
|
|
41
|
+
const transcripts = [];
|
|
42
|
+
for (const feature of features.values()) {
|
|
43
|
+
const type = feature.get('type');
|
|
44
|
+
const subfeatures = feature.get('subfeatures');
|
|
45
|
+
if (type === 'gene' && subfeatures?.length) {
|
|
46
|
+
for (const subfeature of subfeatures) {
|
|
47
|
+
const subType = subfeature.get('type');
|
|
48
|
+
if (isTranscriptType(subType) && hasCDSSubfeatures(subfeature)) {
|
|
49
|
+
transcripts.push(subfeature);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (isTranscriptType(type) && hasCDSSubfeatures(feature)) {
|
|
54
|
+
transcripts.push(feature);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return transcripts;
|
|
58
|
+
}
|
|
59
|
+
function extractCDSRegions(feature) {
|
|
60
|
+
const subfeatures = feature.get('subfeatures') || [];
|
|
61
|
+
const featureStart = feature.get('start');
|
|
62
|
+
return subfeatures
|
|
63
|
+
.filter((sub) => sub.get('type') === 'CDS')
|
|
64
|
+
.sort((a, b) => a.get('start') - b.get('start'))
|
|
65
|
+
.map((sub) => ({
|
|
66
|
+
start: sub.get('start') - featureStart,
|
|
67
|
+
end: sub.get('end') - featureStart,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
async function fetchTranscriptPeptides(pluginManager, renderProps, transcript) {
|
|
71
|
+
try {
|
|
72
|
+
const baseRegion = renderProps.regions[0];
|
|
73
|
+
const region = {
|
|
74
|
+
...baseRegion,
|
|
75
|
+
start: transcript.get('start'),
|
|
76
|
+
end: transcript.get('end'),
|
|
77
|
+
};
|
|
78
|
+
let seq = await fetchSequence(pluginManager, renderProps, region);
|
|
79
|
+
if (!seq) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const strand = transcript.get('strand');
|
|
83
|
+
let cds = extractCDSRegions(transcript);
|
|
84
|
+
if (cds.length === 0) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (strand === -1) {
|
|
88
|
+
seq = revcom(seq);
|
|
89
|
+
const seqLen = seq.length;
|
|
90
|
+
cds = cds
|
|
91
|
+
.map(region => ({
|
|
92
|
+
start: seqLen - region.end,
|
|
93
|
+
end: seqLen - region.start,
|
|
94
|
+
}))
|
|
95
|
+
.reverse();
|
|
96
|
+
}
|
|
97
|
+
const sequenceData = { seq, cds };
|
|
98
|
+
try {
|
|
99
|
+
const protein = convertCodingSequenceToPeptides({
|
|
100
|
+
cds,
|
|
101
|
+
sequence: seq,
|
|
102
|
+
codonTable: generateCodonTable(defaultCodonTable),
|
|
103
|
+
});
|
|
104
|
+
return { sequenceData, protein };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.warn(`[fetchTranscriptPeptides] Failed to convert sequence to peptides for ${transcript.id()}:`, error);
|
|
108
|
+
return { sequenceData };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.warn(`[fetchTranscriptPeptides] Failed to fetch sequence for transcript ${transcript.id()}:`, error);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function fetchPeptideData(pluginManager, renderProps, features) {
|
|
117
|
+
const { colorByCDS, bpPerPx } = renderProps;
|
|
118
|
+
const peptideDataMap = new Map();
|
|
119
|
+
if (!colorByCDS || !shouldRenderPeptideBackground(bpPerPx)) {
|
|
120
|
+
return peptideDataMap;
|
|
121
|
+
}
|
|
122
|
+
for (const transcript of findTranscriptsWithCDS(features)) {
|
|
123
|
+
const peptideData = await fetchTranscriptPeptides(pluginManager, renderProps, transcript);
|
|
124
|
+
if (peptideData) {
|
|
125
|
+
peptideDataMap.set(transcript.id(), peptideData);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return peptideDataMap;
|
|
129
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Feature } from '@jbrowse/core/util';
|
|
2
|
+
export interface AggregatedAminoAcid {
|
|
3
|
+
aminoAcid: string;
|
|
4
|
+
startIndex: number;
|
|
5
|
+
endIndex: number;
|
|
6
|
+
length: number;
|
|
7
|
+
proteinIndex: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function prepareAminoAcidData(parent: Feature, protein: string, featureStart: number, featureEnd: number, strand: number): import("./aggregateAminoAcids.ts").AggregatedAminoAcid[];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { genomeToTranscriptSeqMapping } from 'g2p_mapper';
|
|
2
|
+
import { aggregateAminos } from "./aggregateAminoAcids.js";
|
|
3
|
+
export function prepareAminoAcidData(parent, protein, featureStart, featureEnd, strand) {
|
|
4
|
+
return aggregateAminos(protein, genomeToTranscriptSeqMapping(parent.toJSON()).g2p, featureStart, featureEnd, strand);
|
|
5
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
|
|
2
|
+
import type { Feature } from '@jbrowse/core/util';
|
|
3
|
+
export interface CachedConfig<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
isCallback: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function readCachedConfig<T>(cached: CachedConfig<T>, config: AnyConfigurationModel, key: string | string[], feature: Feature): T;
|
|
8
|
+
export interface RenderConfigContext {
|
|
9
|
+
config: AnyConfigurationModel;
|
|
10
|
+
displayMode: string;
|
|
11
|
+
showLabels: boolean;
|
|
12
|
+
showDescriptions: boolean;
|
|
13
|
+
subfeatureLabels: string;
|
|
14
|
+
transcriptTypes: string[];
|
|
15
|
+
containerTypes: string[];
|
|
16
|
+
color1: CachedConfig<string>;
|
|
17
|
+
color2: CachedConfig<string>;
|
|
18
|
+
color3: CachedConfig<string>;
|
|
19
|
+
outline: CachedConfig<string>;
|
|
20
|
+
featureHeight: CachedConfig<number>;
|
|
21
|
+
fontHeight: CachedConfig<number>;
|
|
22
|
+
labelAllowed: boolean;
|
|
23
|
+
geneGlyphMode: string;
|
|
24
|
+
displayDirectionalChevrons: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function createRenderConfigContext(config: AnyConfigurationModel): RenderConfigContext;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readConfObject } from '@jbrowse/core/configuration';
|
|
2
|
+
export function readCachedConfig(cached, config, key, feature) {
|
|
3
|
+
return cached.isCallback
|
|
4
|
+
? readConfObject(config, key, { feature })
|
|
5
|
+
: cached.value;
|
|
6
|
+
}
|
|
7
|
+
function createCachedConfig(config, key, defaultValue) {
|
|
8
|
+
const configSlot = Array.isArray(key)
|
|
9
|
+
? key.reduce((acc, k) => acc?.[k], config)
|
|
10
|
+
: config[key];
|
|
11
|
+
const isCallback = configSlot?.isCallback ?? false;
|
|
12
|
+
const value = isCallback ? defaultValue : readConfObject(config, key);
|
|
13
|
+
return { value, isCallback };
|
|
14
|
+
}
|
|
15
|
+
export function createRenderConfigContext(config) {
|
|
16
|
+
const displayMode = readConfObject(config, 'displayMode');
|
|
17
|
+
return {
|
|
18
|
+
config,
|
|
19
|
+
displayMode,
|
|
20
|
+
showLabels: readConfObject(config, 'showLabels'),
|
|
21
|
+
showDescriptions: readConfObject(config, 'showDescriptions'),
|
|
22
|
+
subfeatureLabels: readConfObject(config, 'subfeatureLabels'),
|
|
23
|
+
transcriptTypes: readConfObject(config, 'transcriptTypes'),
|
|
24
|
+
containerTypes: readConfObject(config, 'containerTypes'),
|
|
25
|
+
geneGlyphMode: readConfObject(config, 'geneGlyphMode'),
|
|
26
|
+
displayDirectionalChevrons: readConfObject(config, 'displayDirectionalChevrons'),
|
|
27
|
+
color1: createCachedConfig(config, 'color1', 'goldenrod'),
|
|
28
|
+
color2: createCachedConfig(config, 'color2', '#f0f'),
|
|
29
|
+
color3: createCachedConfig(config, 'color3', '#357089'),
|
|
30
|
+
outline: createCachedConfig(config, 'outline', ''),
|
|
31
|
+
featureHeight: createCachedConfig(config, 'height', 10),
|
|
32
|
+
fontHeight: createCachedConfig(config, ['labels', 'fontSize'], 12),
|
|
33
|
+
labelAllowed: displayMode !== 'collapse',
|
|
34
|
+
};
|
|
35
|
+
}
|