Commit ·
c46f2c2
1
Parent(s): edf650c
Show every residue's resolved state on the order track
Browse filesCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frontend/src/components/Transitions.tsx
CHANGED
|
@@ -429,8 +429,51 @@ function fmtRanges(r: Array<[number, number]>, max = 8): string {
|
|
| 429 |
* Residues resolved in the partner but not in this chain ("ordered") and the reverse
|
| 430 |
* ("disordered"), on the SEQRES axis. Resolved-state sense, not intrinsic disorder.
|
| 431 |
*/
|
| 432 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
const L = Math.max(1, length);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
return (
|
| 435 |
<div className="orderview">
|
| 436 |
<h3 className="compare__sub">Order ↔ disorder (resolved residues)</h3>
|
|
@@ -467,8 +510,16 @@ export function OrderChangeView({ order, length, from, to }: { order: OrderChang
|
|
| 467 |
)}
|
| 468 |
{order.with_ligand_change && <span className="badge badge--outline">Co-occurs with a ligand change</span>}
|
| 469 |
</div>
|
| 470 |
-
<div className=
|
| 471 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
<span
|
| 473 |
key={`o${a}`}
|
| 474 |
className="order-track__seg order-track__seg--ord"
|
|
@@ -476,7 +527,7 @@ export function OrderChangeView({ order, length, from, to }: { order: OrderChang
|
|
| 476 |
title={`Ordered in ${to}: ${a}–${b}`}
|
| 477 |
/>
|
| 478 |
))}
|
| 479 |
-
{order.disordered_ranges.map(([a, b]) => (
|
| 480 |
<span
|
| 481 |
key={`d${a}`}
|
| 482 |
className="order-track__seg order-track__seg--dis"
|
|
@@ -489,6 +540,22 @@ export function OrderChangeView({ order, length, from, to }: { order: OrderChang
|
|
| 489 |
<span>{L}</span>
|
| 490 |
</span>
|
| 491 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
<p className="muted small orderview__ranges">
|
| 493 |
{order.ordered_ranges.length > 0 && (
|
| 494 |
<>
|
|
|
|
| 429 |
* Residues resolved in the partner but not in this chain ("ordered") and the reverse
|
| 430 |
* ("disordered"), on the SEQRES axis. Resolved-state sense, not intrinsic disorder.
|
| 431 |
*/
|
| 432 |
+
type ResState = 'both' | 'ord' | 'dis' | 'none';
|
| 433 |
+
|
| 434 |
+
/** Contiguous runs of each residue's resolved state in this chain (A) vs the partner (B). */
|
| 435 |
+
function resolvedRuns(L: number, resolvedA: Array<[number, number]>, order: OrderChange) {
|
| 436 |
+
const inA = new Uint8Array(L + 2);
|
| 437 |
+
for (const [a, b] of resolvedA) for (let i = Math.max(1, a); i <= Math.min(L, b); i++) inA[i] = 1;
|
| 438 |
+
const mark = new Array<ResState>(L + 2).fill('none');
|
| 439 |
+
for (let i = 1; i <= L; i++) mark[i] = inA[i] ? 'both' : 'none';
|
| 440 |
+
for (const [a, b] of order.disordered_ranges) for (let i = a; i <= Math.min(L, b); i++) mark[i] = 'dis';
|
| 441 |
+
for (const [a, b] of order.ordered_ranges) for (let i = a; i <= Math.min(L, b); i++) mark[i] = 'ord';
|
| 442 |
+
const runs: Array<{ state: ResState; start: number; end: number }> = [];
|
| 443 |
+
for (let i = 1; i <= L; i++) {
|
| 444 |
+
const last = runs[runs.length - 1];
|
| 445 |
+
if (last && last.state === mark[i] && last.end === i - 1) last.end = i;
|
| 446 |
+
else runs.push({ state: mark[i], start: i, end: i });
|
| 447 |
+
}
|
| 448 |
+
return runs;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
export function OrderChangeView({
|
| 452 |
+
order,
|
| 453 |
+
length,
|
| 454 |
+
from,
|
| 455 |
+
to,
|
| 456 |
+
resolvedFrom,
|
| 457 |
+
}: {
|
| 458 |
+
order: OrderChange;
|
| 459 |
+
length: number;
|
| 460 |
+
from: string;
|
| 461 |
+
to: string;
|
| 462 |
+
/** resolved SEQRES ranges of this chain (A); lets the track show every residue's state */
|
| 463 |
+
resolvedFrom?: Array<[number, number]>;
|
| 464 |
+
}) {
|
| 465 |
const L = Math.max(1, length);
|
| 466 |
+
const runs = resolvedFrom?.length ? resolvedRuns(L, resolvedFrom, order) : null;
|
| 467 |
+
const runTitle = (state: ResState, a: number, b: number) => {
|
| 468 |
+
const span = a === b ? `${a}` : `${a}–${b}`;
|
| 469 |
+
return state === 'both'
|
| 470 |
+
? `${span}: resolved in both`
|
| 471 |
+
: state === 'ord'
|
| 472 |
+
? `${span}: ordered in ${to} (missing in ${from})`
|
| 473 |
+
: state === 'dis'
|
| 474 |
+
? `${span}: resolved only in ${from}`
|
| 475 |
+
: `${span}: not resolved in either structure`;
|
| 476 |
+
};
|
| 477 |
return (
|
| 478 |
<div className="orderview">
|
| 479 |
<h3 className="compare__sub">Order ↔ disorder (resolved residues)</h3>
|
|
|
|
| 510 |
)}
|
| 511 |
{order.with_ligand_change && <span className="badge badge--outline">Co-occurs with a ligand change</span>}
|
| 512 |
</div>
|
| 513 |
+
<div className={`order-track ${runs ? 'order-track--full' : ''}`} aria-label="Resolved residues along the sequence">
|
| 514 |
+
{runs?.map((r) => (
|
| 515 |
+
<span
|
| 516 |
+
key={`${r.state}${r.start}`}
|
| 517 |
+
className={`order-track__seg order-track__seg--${r.state}`}
|
| 518 |
+
style={{ left: `${((r.start - 1) / L) * 100}%`, width: `${((r.end - r.start + 1) / L) * 100}%` }}
|
| 519 |
+
title={runTitle(r.state, r.start, r.end)}
|
| 520 |
+
/>
|
| 521 |
+
))}
|
| 522 |
+
{!runs && order.ordered_ranges.map(([a, b]) => (
|
| 523 |
<span
|
| 524 |
key={`o${a}`}
|
| 525 |
className="order-track__seg order-track__seg--ord"
|
|
|
|
| 527 |
title={`Ordered in ${to}: ${a}–${b}`}
|
| 528 |
/>
|
| 529 |
))}
|
| 530 |
+
{!runs && order.disordered_ranges.map(([a, b]) => (
|
| 531 |
<span
|
| 532 |
key={`d${a}`}
|
| 533 |
className="order-track__seg order-track__seg--dis"
|
|
|
|
| 540 |
<span>{L}</span>
|
| 541 |
</span>
|
| 542 |
</div>
|
| 543 |
+
{runs && (
|
| 544 |
+
<ul className="legend order-legend">
|
| 545 |
+
<li>
|
| 546 |
+
<span className="swatch order-key order-key--both" /> Resolved in both
|
| 547 |
+
</li>
|
| 548 |
+
<li>
|
| 549 |
+
<span className="swatch" style={{ background: 'var(--ord)' }} /> Ordered in {to}
|
| 550 |
+
</li>
|
| 551 |
+
<li>
|
| 552 |
+
<span className="swatch" style={{ background: 'var(--disord)' }} /> Resolved only in {from}
|
| 553 |
+
</li>
|
| 554 |
+
<li>
|
| 555 |
+
<span className="swatch order-key order-key--none" /> Not resolved in either
|
| 556 |
+
</li>
|
| 557 |
+
</ul>
|
| 558 |
+
)}
|
| 559 |
<p className="muted small orderview__ranges">
|
| 560 |
{order.ordered_ranges.length > 0 && (
|
| 561 |
<>
|
frontend/src/pages/ChainPage.tsx
CHANGED
|
@@ -143,7 +143,13 @@ function ComparePanel({
|
|
| 143 |
<p className="muted small">Negative (green) means the partner conformation scores lower, i.e. more favourable.</p>
|
| 144 |
</div>
|
| 145 |
</div>
|
| 146 |
-
<OrderChangeView
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
{show3d && (
|
| 148 |
<StructureDiff
|
| 149 |
a={{ pdb_id: a.pdb_id, auth_asym_id: a.auth_asym_id }}
|
|
|
|
| 143 |
<p className="muted small">Negative (green) means the partner conformation scores lower, i.e. more favourable.</p>
|
| 144 |
</div>
|
| 145 |
</div>
|
| 146 |
+
<OrderChangeView
|
| 147 |
+
order={t.order}
|
| 148 |
+
length={a.sequence_length ?? a.sequence.length}
|
| 149 |
+
from={chainLabel(a.pdb_id, a.auth_asym_id)}
|
| 150 |
+
to={chainLabel(t.pdb_id, t.auth_asym_id)}
|
| 151 |
+
resolvedFrom={a.resolved_ranges}
|
| 152 |
+
/>
|
| 153 |
{show3d && (
|
| 154 |
<StructureDiff
|
| 155 |
a={{ pdb_id: a.pdb_id, auth_asym_id: a.auth_asym_id }}
|
frontend/src/styles/pages.css
CHANGED
|
@@ -2049,3 +2049,35 @@
|
|
| 2049 |
color: var(--faint);
|
| 2050 |
margin-top: -6px;
|
| 2051 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2049 |
color: var(--faint);
|
| 2050 |
margin-top: -6px;
|
| 2051 |
}
|
| 2052 |
+
|
| 2053 |
+
/* full per-residue order track: both / ordered / disordered / neither */
|
| 2054 |
+
.order-track--full {
|
| 2055 |
+
background: transparent;
|
| 2056 |
+
box-shadow: inset 0 0 0 1px var(--line-strong);
|
| 2057 |
+
}
|
| 2058 |
+
|
| 2059 |
+
.order-track__seg--both,
|
| 2060 |
+
.order-key--both {
|
| 2061 |
+
background: color-mix(in srgb, var(--accent) 22%, var(--surface));
|
| 2062 |
+
}
|
| 2063 |
+
|
| 2064 |
+
.order-track__seg--ord {
|
| 2065 |
+
background: var(--ord);
|
| 2066 |
+
}
|
| 2067 |
+
|
| 2068 |
+
.order-track__seg--dis {
|
| 2069 |
+
background: var(--disord);
|
| 2070 |
+
}
|
| 2071 |
+
|
| 2072 |
+
.order-track__seg--none {
|
| 2073 |
+
background: repeating-linear-gradient(135deg, transparent 0 3px, var(--line) 3px 4px);
|
| 2074 |
+
}
|
| 2075 |
+
|
| 2076 |
+
.order-key--none {
|
| 2077 |
+
background: var(--surface);
|
| 2078 |
+
box-shadow: inset 0 0 0 1px var(--line-strong);
|
| 2079 |
+
}
|
| 2080 |
+
|
| 2081 |
+
.order-legend {
|
| 2082 |
+
margin: -8px 0 6px;
|
| 2083 |
+
}
|