Skip to content

Commit 5a1a355

Browse files
committed
PISA - detailed interface views
1 parent bd0a35d commit 5a1a355

2 files changed

Lines changed: 104 additions & 40 deletions

File tree

src/app/extensions/pisa/index.ts

Lines changed: 103 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import { loadMVS } from 'molstar/lib/extensions/mvs/load';
22
import { MVSData, Snapshot } from 'molstar/lib/extensions/mvs/mvs-data';
33
import Builder from 'molstar/lib/extensions/mvs/tree/mvs/mvs-builder';
4-
import { ColorT, ComponentExpressionT } from 'molstar/lib/extensions/mvs/tree/mvs/param-types';
4+
import { MVSNodeParams } from 'molstar/lib/extensions/mvs/tree/mvs/mvs-tree';
5+
import { ComponentExpressionT, HexColorT } from 'molstar/lib/extensions/mvs/tree/mvs/param-types';
56
import { PluginContext } from 'molstar/lib/mol-plugin/context';
67
import { Color } from 'molstar/lib/mol-util/color';
78
import { normalizeColor } from '../../helpers';
89
import { PisaAssembliesData, PisaAssemblyRecord, PisaInterfaceData, PisaTransform } from './types';
910

1011

11-
const ManyDistinctColors: ColorT[] = [
12-
'#1b9e77', '#d95f02', '#7570b3', '#e7298a', '#66a61e', '#e6ab02', '#a6761d',
13-
'#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33', '#a65628', '#f781bf',
14-
'#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', '#e5c494',
12+
const COMPONENT_COLORS: HexColorT[] = [
13+
'#1b9e77', '#d95f02', '#7570b3', '#e7298a', '#66a61e', '#e6ab02', '#a6761d', // Dark-2 without gray
14+
'#1f77b4', '#2ca02c', '#d62728', '#927ba7', '#8c564b', '#e377c2', '#bcbd22', '#17becf', // More non-conflicting colors from other palettes
15+
'#fc8d62', '#9eb9f3', '#ff9da7', '#ffff33', '#8be0a4', '#e15759', '#c69fbb', '#76b7b2', // More non-conflicting colors from other palettes
16+
// 'many-distinct-colors' without grays:
17+
// '#1b9e77', '#d95f02', '#7570b3', '#e7298a', '#66a61e', '#e6ab02', '#a6761d',
18+
// '#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33', '#a65628', '#f781bf',
19+
// '#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', '#e5c494',
1520
];
16-
const DEFAULT_COMPONENT_COLOR: ColorT = '#999999';
21+
const DEFAULT_COMPONENT_COLOR: HexColorT = '#999999';
1722

1823
async function getAssembliesData(pdbId: string): Promise<PisaAssembliesData> {
1924
return await (await fetch(`/tmp/pisa/${pdbId}/assembly.json`)).json();
@@ -31,13 +36,43 @@ function loadStructureData(builder: Builder.Root, pdbId: string) {
3136
}
3237

3338
/** Positive values for lighter interfaces, negative values for darker interfaces. */
34-
const INTERFACE_COLOR_CHANGE_STRENGTH = -1.25;
35-
function bulkColorFn(baseColor: ColorT) {
36-
return Color.toHexStyle(Color.darken(normalizeColor(baseColor), INTERFACE_COLOR_CHANGE_STRENGTH)) as ColorT;
39+
const INTERFACE_COLOR_CHANGE_STRENGTH = -1;
40+
function bulkColorFn(baseColor: HexColorT): HexColorT {
41+
return Color.toHexStyle(Color.darken(normalizeColor(baseColor), INTERFACE_COLOR_CHANGE_STRENGTH)) as HexColorT;
3742
}
38-
function interfaceColorFn(baseColor: ColorT) {
39-
return Color.toHexStyle(Color.lighten(normalizeColor(baseColor), INTERFACE_COLOR_CHANGE_STRENGTH)) as ColorT;
43+
function interfaceColorFn(baseColor: HexColorT): HexColorT {
44+
return Color.toHexStyle(Color.lighten(normalizeColor(baseColor), INTERFACE_COLOR_CHANGE_STRENGTH)) as HexColorT;
4045
}
46+
// const BULK_COLOR_CHANGE_AMOUNT = 0.3;
47+
// const INTERFACE_COLOR_CHANGE_AMOUNT = -0.3;
48+
// function bulkColorFn(baseColor: HexColorT): HexColorT {
49+
// return lighten(baseColor, BULK_COLOR_CHANGE_AMOUNT);
50+
// }
51+
// function interfaceColorFn(baseColor: HexColorT): HexColorT {
52+
// return lighten(baseColor, INTERFACE_COLOR_CHANGE_AMOUNT);
53+
// }
54+
// function hexColorToRgb(hex: HexColorT): [number, number, number] {
55+
// const hexNum = parseInt(hex.replace('#', '0x'));
56+
// return [hexNum >> 16 & 255, hexNum >> 8 & 255, hexNum & 255];
57+
// }
58+
// function rgbToHexColor(rgb: [number, number, number]): HexColorT {
59+
// const hexNum = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
60+
// return '#' + ('000000' + hexNum.toString(16)).slice(-6) as HexColorT;
61+
// }
62+
// function lighten(color: HexColorT, amount: number): HexColorT {
63+
// const rgb = hexColorToRgb(color);
64+
// amount = Math.min(1, Math.max(-1, amount));
65+
// if (amount > 0) {
66+
// rgb[0] = Math.round(rgb[0] * (1 - amount)) + 255 * amount;
67+
// rgb[1] = Math.round(rgb[1] * (1 - amount)) + 255 * amount;
68+
// rgb[2] = Math.round(rgb[2] * (1 - amount)) + 255 * amount;
69+
// } else if (amount < 0) {
70+
// rgb[0] = Math.round(rgb[0] * (1 + amount));
71+
// rgb[1] = Math.round(rgb[1] * (1 + amount));
72+
// rgb[2] = Math.round(rgb[2] * (1 + amount));
73+
// }
74+
// return rgbToHexColor(rgb);
75+
// }
4176

4277
export async function pisaDemo(plugin: PluginContext) {
4378
const pdbId = '3gcb';
@@ -47,19 +82,21 @@ export async function pisaDemo(plugin: PluginContext) {
4782
const interfaceIds = Array.from(new Set(
4883
allAssemblies.flatMap(ass => ass.interfaces.interface.map(int => int.id))
4984
)).sort((a, b) => Number(a) - Number(b));
50-
// Assembly 1: 6x8 molecules
51-
// Assembly 2: 2x8 molecules (big interface)
52-
// Assembly 3: 2x8 molecules (tiny interface)
53-
// ASU (Assembly 4): 8 molecules
5485

5586
const snapshots: Snapshot[] = [];
5687
for (const assembly of allAssemblies) {
57-
snapshots.push(await pisaAsseblyView(pdbId, assembly.id, { ghostRepr: false }));
88+
snapshots.push(await pisaAsseblyView(pdbId, assembly.id, {}));
89+
// snapshots.push(await pisaAsseblyView(pdbId, assembly.id, { reprParams: { type: 'surface' } }));
90+
// snapshots.push(await pisaAsseblyView(pdbId, assembly.id, { reprParams: { type: 'surface', surface_type: 'gaussian' } }));
91+
// snapshots.push(await pisaAsseblyView(pdbId, assembly.id, { reprParams: { type: 'cartoon' } }));
5892
}
5993
for (const interfaceId of interfaceIds) {
6094
snapshots.push(await pisaInterfaceView(pdbId, interfaceId, { ghostMolecules: [] }));
61-
snapshots.push(await pisaInterfaceView(pdbId, interfaceId, { ghostMolecules: [0] }));
62-
snapshots.push(await pisaInterfaceView(pdbId, interfaceId, { ghostMolecules: [1] }));
95+
// snapshots.push(await pisaInterfaceView(pdbId, interfaceId, { ghostMolecules: [0] }));
96+
// snapshots.push(await pisaInterfaceView(pdbId, interfaceId, { ghostMolecules: [1] }));
97+
snapshots.push(await pisaInterfaceView(pdbId, interfaceId, { detailMolecules: [0] }));
98+
snapshots.push(await pisaInterfaceView(pdbId, interfaceId, { detailMolecules: [1] }));
99+
snapshots.push(await pisaInterfaceView(pdbId, interfaceId, { detailMolecules: [0, 1] }));
63100
}
64101
const mvs = MVSData.createMultistate(snapshots);
65102
console.log(MVSData.toMVSJ(mvs))
@@ -80,7 +117,7 @@ export async function pisaDemo(plugin: PluginContext) {
80117
}
81118

82119

83-
export async function pisaAsseblyView(pdbId: string, assemblyId: string, options: {} = {}) {
120+
export async function pisaAsseblyView(pdbId: string, assemblyId: string, options: { reprParams?: Partial<MVSNodeParams<'representation'>> } = {}) {
84121
const assembliesData = await getAssembliesData(pdbId);
85122
const allAssemblies = getAllAssemblies(assembliesData);
86123

@@ -109,6 +146,7 @@ export async function pisaAsseblyView(pdbId: string, assemblyId: string, options
109146
.representation({
110147
type: 'spacefill',
111148
size_factor: component.isLigand ? 1.05 : 1, // distinguish ligands/waters from the main chain, in case of PDB format
149+
...options.reprParams,
112150
})
113151
.color({ color: bulkColorFn(color) });
114152
if (!component.isLigand) {
@@ -138,13 +176,13 @@ export async function pisaAsseblyView(pdbId: string, assemblyId: string, options
138176
const resSelector: ComponentExpressionT[] = interfaceResidues.map(r => ({ auth_seq_id: Number(r.seq_num), pdbx_PDB_ins_code: r.ins_code ?? undefined }));
139177
if (!isSelfInterface) {
140178
const comps = componentIdsByPisaChainId[molecule.chain_id].map(componentId => components[componentId])
141-
markInterface(comps, resSelector, interfaceTooltip);
179+
markInterface(comps, resSelector, { tooltip: interfaceTooltip, color: true });
142180
}
143181
bothSurfaces.push(...resSelector);
144182
}
145183
if (isSelfInterface) {
146184
const comps = componentIdsByPisaChainId[int.interface.molecule[0].chain_id].map(componentId => components[componentId])
147-
markInterface(comps, bothSurfaces, interfaceTooltip);
185+
markInterface(comps, bothSurfaces, { tooltip: interfaceTooltip, color: true });
148186
}
149187
}
150188
const name = assembly.serial_no === '0' ? 'ASU complex' : `Assembly ${assembly.id}`;
@@ -156,7 +194,7 @@ export async function pisaAsseblyView(pdbId: string, assemblyId: string, options
156194
});
157195
}
158196

159-
export async function pisaInterfaceView(pdbId: string, interfaceId: string, options: { ghostMolecules?: (0 | 1)[] } = {}) {
197+
export async function pisaInterfaceView(pdbId: string, interfaceId: string, options: { ghostMolecules?: (0 | 1)[], detailMolecules?: (0 | 1)[] } = {}) {
160198
const assembliesData = await getAssembliesData(pdbId);
161199
const interfaceData = await getInterfaceData(pdbId, interfaceId);
162200
const allAssemblies = getAllAssemblies(assembliesData);
@@ -174,7 +212,9 @@ export async function pisaInterfaceView(pdbId: string, interfaceId: string, opti
174212
.transform(transformFromPisaStyle(molecule));
175213
struct.component().tooltip({ text: `<hr>Component <b>${molecule.chain_id} (${molecule.symop})</b>` });
176214
const componentSelector: ComponentExpressionT = { label_asym_id: component.chainId, label_seq_id: component.seqId }; // chain_id appears to be label_asym_id (if mmcif format)
177-
if (options.ghostMolecules?.includes(i as 0 | 1)) {
215+
const showGhost = options.ghostMolecules?.includes(i as 0 | 1);
216+
const showDetails = options.detailMolecules?.includes(i as 0 | 1);
217+
if (showGhost) {
178218
const reprGhost = struct
179219
.component({ selector: componentSelector })
180220
.representation({
@@ -190,14 +230,21 @@ export async function pisaInterfaceView(pdbId: string, interfaceId: string, opti
190230
})
191231
.color({ color: color })
192232
.opacity({ opacity: .49 });
193-
} else {
233+
}
234+
if (!showGhost || showDetails) {
194235
const repr = struct
195236
.component({ selector: componentSelector })
196237
.representation({
197-
type: 'spacefill',
198-
size_factor: component.isLigand ? 1.05 : 1, // distinguish ligands/waters from the main chain, in case of PDB format
238+
type: showDetails ? 'cartoon' : 'spacefill',
239+
size_factor: (showDetails ? 0.5 : 1)
240+
* (component.isLigand ? 1.05 : 1), // distinguish ligands/waters from the main chain, in case of PDB format
199241
})
200242
.color({ color: bulkColorFn(color) });
243+
const residues = Array.isArray(molecule.residues.residue) ? molecule.residues.residue : [molecule.residues.residue];
244+
const interfaceResidues = residues.filter(r => Number(r.bsa) !== 0); // Secret undocumented knowledge
245+
const resSelector: ComponentExpressionT[] = interfaceResidues.map(r => ({ auth_seq_id: Number(r.seq_num), pdbx_PDB_ins_code: r.ins_code ?? undefined }));
246+
markInterface([{ struct, repr, color }], resSelector, { tooltip: interfaceTooltip, color: !showDetails, ball_and_stick: showDetails });
247+
if (options.detailMolecules?.length) applyElementColors(repr);
201248
if (!component.isLigand) {
202249
// distinguish ligands/waters from the main chain, in case of PDB format
203250
repr
@@ -206,10 +253,6 @@ export async function pisaInterfaceView(pdbId: string, interfaceId: string, opti
206253
.color({ selector: 'branched', color: 'white' })
207254
.color({ selector: 'ion', color: 'white' });
208255
};
209-
const residues = Array.isArray(molecule.residues.residue) ? molecule.residues.residue : [molecule.residues.residue];
210-
const interfaceResidues = residues.filter(r => Number(r.bsa) !== 0); // Secret undocumented knowledge
211-
const resSelector: ComponentExpressionT[] = interfaceResidues.map(r => ({ auth_seq_id: Number(r.seq_num), pdbx_PDB_ins_code: r.ins_code ?? undefined }));
212-
markInterface([{ struct, repr, color }], resSelector, interfaceTooltip);
213256
}
214257
});
215258

@@ -222,15 +265,23 @@ export async function pisaInterfaceView(pdbId: string, interfaceId: string, opti
222265
}
223266

224267

225-
interface ComponentState { struct: Builder.Structure, repr: Builder.Representation, color: ColorT };
268+
interface ComponentState { struct: Builder.Structure, repr: Builder.Representation, color: HexColorT };
226269

227-
function markInterface(theComponents: ComponentState[], interfaceSelector: ComponentExpressionT[], tooltip: string) {
270+
function markInterface(theComponents: ComponentState[], interfaceSelector: ComponentExpressionT[], options: { color?: boolean, ball_and_stick?: boolean, tooltip?: string } = {}) {
228271
for (const component of theComponents) {
229-
const interfaceColor = interfaceColorFn(component.color);
230-
component.repr.color({ selector: interfaceSelector, color: interfaceColor });
231-
component.struct.component({ selector: interfaceSelector }).tooltip({ text: tooltip });
232-
// const bs = component.struct.component({ selector: interfaceSelector }).representation({ type: 'ball_and_stick' }).color({ color: bulkColorFn(component.color) })
233-
// applyElementColors(bs);
272+
if (options.color) {
273+
component.repr.color({ selector: interfaceSelector, color: interfaceColorFn(component.color) });
274+
}
275+
if (options.ball_and_stick) {
276+
const bs = component.struct
277+
.component({ selector: interfaceSelector })
278+
.representation({ type: 'ball_and_stick' })
279+
.color({ color: bulkColorFn(component.color) });
280+
applyElementColors(bs);
281+
}
282+
if (options.tooltip) {
283+
component.struct.component({ selector: interfaceSelector }).tooltip({ text: options.tooltip });
284+
}
234285
}
235286
}
236287

@@ -255,11 +306,24 @@ function componentKey(component: { chain_id: string } & PisaTransform) {
255306
}
256307

257308
function assignComponentColors(assemblies: PisaAssemblyRecord[]) {
258-
const colors = {} as Record<string, ColorT>;
309+
const colors = {} as Record<string, HexColorT>;
259310
let iColor = 0;
311+
function isPolymer(component: { chain_id: string }) {
312+
return !component.chain_id.startsWith('[');
313+
}
314+
// First assign colors to polymers, then ligands
315+
for (const assembly of assemblies) {
316+
for (const molecule of assembly.molecule) {
317+
if (isPolymer(molecule)) {
318+
colors[componentKey(molecule)] ??= COMPONENT_COLORS[iColor++ % COMPONENT_COLORS.length];
319+
}
320+
}
321+
}
260322
for (const assembly of assemblies) {
261323
for (const molecule of assembly.molecule) {
262-
colors[componentKey(molecule)] ??= ManyDistinctColors[iColor++ % ManyDistinctColors.length];
324+
if (!isPolymer(molecule)) {
325+
colors[componentKey(molecule)] ??= COMPONENT_COLORS[iColor++ % COMPONENT_COLORS.length];
326+
}
263327
}
264328
}
265329
return colors;

src/app/viewer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export class PDBeMolstarPlugin {
194194
if (hideButtons.includes('all') || hideButtons.includes('animation')) pdbePluginSpec.config.push([PluginConfig.Viewport.ShowAnimation, false]);
195195
if (hideButtons.includes('all') || hideButtons.includes('trajectory')) pdbePluginSpec.config.push([PluginConfig.Viewport.ShowTrajectoryControls, false]);
196196
pdbePluginSpec.config.push([PluginConfig.Viewport.ShowToggleFullscreen, false]);
197-
// pdbePluginSpec.config.push([PluginConfig.Viewport.ShowIllumination, false]); // debug TODO uncomment
197+
pdbePluginSpec.config.push([PluginConfig.Viewport.ShowIllumination, false]);
198198
pdbePluginSpec.config.push([PluginConfig.Viewport.ShowXR, 'never']);
199199

200200
// override default event bindings

0 commit comments

Comments
 (0)