WinslowFan Claude Opus 5.5 commited on
Commit
c46f2c2
·
1 Parent(s): edf650c

Show every residue's resolved state on the order track

Browse files

Co-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
- export function OrderChangeView({ order, length, from, to }: { order: OrderChange; length: number; from: string; to: string }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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="order-track" aria-label="Order changes along the sequence">
471
- {order.ordered_ranges.map(([a, b]) => (
 
 
 
 
 
 
 
 
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 order={t.order} length={a.sequence_length ?? a.sequence.length} from={chainLabel(a.pdb_id, a.auth_asym_id)} to={chainLabel(t.pdb_id, t.auth_asym_id)} />
 
 
 
 
 
 
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
+ }