Coot Inline Graphs
Coot Inline Graphs
Inline graphs render Chart.js directly in the chat via the visualize:show_widget
tool. Coot supplies the data via Python; the widget renders it with no file I/O,
no external viewer, and full interactivity.
Core workflow
- Get data from Coot — fetch per-residue metrics using the Python API
- Get secondary structure — call
add_header_secondary_structure_info()thenget_header_secondary_structure_info()if overlays are wanted - Render the widget — embed data as JS literals in the Chart.js HTML
Always call visualize:read_me (modules: ["interactive", "chart"]) before the
first visualize:show_widget call in a session.
Step 1 — Fetch per-residue data from Coot
B-factors
def get_bfactor_data(imol, chain_id):
min_res = coot.min_resno_in_chain(imol, chain_id)
max_res = coot.max_resno_in_chain(imol, chain_id)
results = []
for resno in range(min_res, max_res + 1):
atoms = coot.residue_info_py(imol, chain_id, resno, "")
if atoms:
resname = coot.residue_name_py(imol, chain_id, resno, "")
bfactors = [a[1][1] for a in atoms if isinstance(a[1][1], float)]
mean_b = round(sum(bfactors) / len(bfactors), 2) if bfactors else 0
results.append({"resno": resno, "resname": resname, "mean_b": mean_b})
return results
Density correlation
def get_correlation_data(imol, chain_id, imol_map):
stats = coot.map_to_model_correlation_stats_per_residue_range_py(
imol, chain_id, imol_map, 1, 0)
results = []
for entry in stats[0]:
residue_spec = entry[0] # [chain_id, resno, ins_code]
corr_data = entry[1] # [n_points, correlation]
resno = residue_spec[1]
correlation = corr_data[1]
resname = coot.residue_name_py(imol, chain_id, resno, "")
results.append({
"resno": resno,
"resname": resname,
"correlation": round(correlation, 4) if correlation == correlation else None
})
return results
Ramachandran probabilities
def get_rama_data(imol, chain_id):
rama = coot.all_molecule_ramachandran_score_py(imol)
results = []
for entry in rama[5]:
if entry == -1:
continue
phi_psi, res_spec, score, res_names = entry
if res_spec[0] != chain_id:
continue
results.append({
"resno": res_spec[1],
"resname": res_names[1],
"phi": round(phi_psi[0], 1),
"psi": round(phi_psi[1], 1),
"rama_prob": round(score, 4)
})
return results
Step 2 — Fetch secondary structure
Always try get_header_secondary_structure_info() first. If it returns {} or
False, call add_header_secondary_structure_info() to compute it from geometry,
then call get_header_secondary_structure_info() again.
def get_secondary_structure(imol, chain_id):
ss = coot.get_header_secondary_structure_info(imol)
if not isinstance(ss, dict) or (not ss.get('helices') and not ss.get('strands')):
coot.add_header_secondary_structure_info(imol)
ss = coot.get_header_secondary_structure_info(imol)
if not isinstance(ss, dict):
return {'helices': [], 'strands': []}
helices = [h for h in (ss.get('helices') or []) if h['initChainID'] == chain_id]
strands = [s for s in (ss.get('strands') or []) if s['initChainID'] == chain_id]
return {'helices': helices, 'strands': strands}
Important: add_header_secondary_structure_info() will crash Coot if called
on a molecule that already has secondary structure records populated and then
get_header_secondary_structure_info() is called — only call it when the initial
query returns empty. (Bug reported; fix applied to c-interface-build.cc:2876.)
Step 3 — Render the widget
Chart.js setup
Load via CDN. Always use the UMD build:
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
For secondary structure annotation overlays, also load:
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/3.0.1/chartjs-plugin-annotation.min.js"></script>
Data embedding
Embed Coot data as a JS literal directly in the widget HTML. Do not use fetch() or external URLs — the data comes from Coot at render time and is baked in.
const data = [
{"resno": 1, "resname": "ASP", "mean_b": 34.95},
// ... all residues
];
Canvas sizing
Always wrap <canvas> in a <div> with explicit height:
<div style="position: relative; width: 100%; height: 300px;">
<canvas id="chart"></canvas>
</div>
Set responsive: true, maintainAspectRatio: false in Chart.js options.
Never set height directly on the <canvas> element.
Secondary structure overlay
Box annotations sit at the top of the chart as a strip. The box height is computed dynamically so the α/β glyph sits vertically centred:
const boxHeightUnits = Math.round(22 * yAxisMax / 280);
const boxYMax = yAxisMax;
const boxYMin = yAxisMax - boxHeightUnits;
Build a resnoToIndex lookup first (maps residue number → bar index):
const resnoToIndex = {};
data.forEach((d, i) => { resnoToIndex[d.resno] = i; });
Annotation spec
// Helix — purple, semi-opaque, white-ish glyph text
{
type: 'box',
xMin: resnoToIndex[h.initSeqNum] - 0.5,
xMax: resnoToIndex[h.endSeqNum] + 0.5,
yMin: boxYMin,
yMax: boxYMax,
backgroundColor: 'rgba(175,169,236,0.45)',
borderColor: 'rgba(127,119,221,0.8)',
borderWidth: 1,
label: {
display: true,
content: 'α',
position: { x: 'center', y: 'center' },
font: { size: 13, weight: '500' },
color: 'rgba(255,255,255,0.85)'
}
}
// Strand — amber, semi-opaque, white-ish glyph text
{
type: 'box',
xMin: resnoToIndex[s.initSeqNum] - 0.5,
xMax: resnoToIndex[s.endSeqNum] + 0.5,
yMin: boxYMin,
yMax: boxYMax,
backgroundColor: 'rgba(239,159,39,0.35)',
borderColor: 'rgba(186,117,23,0.7)',
borderWidth: 1,
label: {
display: true,
content: 'β',
position: { x: 'center', y: 'center' },
font: { size: 13, weight: '500' },
color: 'rgba(255,255,255,0.85)'
}
}
Threshold colouring
Colour bars relative to a threshold to highlight problem residues:
// Correlation — low is bad
backgroundColor: data.map(d => d.correlation < thresh ? '#378ADD' : '#5DCAA5')
// B-factor — high is bad
backgroundColor: data.map(d => d.mean_b > thresh ? '#378ADD' : '#5DCAA5')
// Ramachandran — low probability is bad
backgroundColor: data.map(d => d.rama_prob < thresh ? '#E24B4A' : '#5DCAA5')
Provide a range slider to let the user adjust threshold interactively. When switching between metrics, update the slider range accordingly:
- Correlation: min=0, max=1, step=0.01, default=0.7
- B-factor: min=0, max=
bMax, step=1, default=20 - Ramachandran: min=0, max=1, step=0.01, default=0.02
Click-to-navigate
Wire bar clicks to sendPrompt() so the user can jump to a residue in Coot:
onClick: (e, els) => {
if (els.length) {
const d = data[els[0].index];
sendPrompt('Navigate to residue ' + d.resno + ' ' + d.resname +
' in chain ' + chainId + ' of the tutorial model');
}
}
Axis labels and ticks
scales: {
x: {
grid: { display: false },
ticks: {
color: '#888780',
font: { size: 9 },
maxRotation: 90,
autoSkip: true,
maxTicksLimit: 30
}
},
y: {
min: 0,
max: yAxisMax,
grid: { color: 'rgba(136,135,128,0.15)' },
ticks: {
color: '#888780',
font: { size: 11 },
callback: v => v + ' Ų' // or '.toFixed(2)' for correlations
}
}
}
Stat cards
Show summary metrics above the chart using the metric card pattern:
<div style="background: var(--color-background-secondary);
border-radius: var(--border-radius-md);
padding: 10px 12px;">
<div style="font-size: 11px; color: var(--color-text-secondary);">Mean B</div>
<div style="font-size: 17px; font-weight: 500; color: var(--color-text-primary);"
id="s-meanb">—</div>
</div>
Use a 4-column grid: residue count, mean metric, count above/below threshold, max or min value as appropriate.
Legend
Always provide a manual legend below the chart — do not use Chart.js default:
<div style="display: flex; gap: 16px; margin-top: 8px;
font-size: 12px; color: var(--color-text-secondary); flex-wrap: wrap;">
<span style="display:flex;align-items:center;gap:4px;">
<span style="width:10px;height:10px;border-radius:2px;background:#5DCAA5;"></span>
Below threshold
</span>
<span style="display:flex;align-items:center;gap:4px;">
<span style="width:10px;height:10px;border-radius:2px;background:#378ADD;"></span>
Above threshold
</span>
<span style="display:flex;align-items:center;gap:4px;">
<span style="width:10px;height:10px;border-radius:2px;
background:rgba(175,169,236,0.45);border:1px solid #7F77DD;"></span>
Helix
</span>
<span style="display:flex;align-items:center;gap:4px;">
<span style="width:10px;height:10px;border-radius:2px;
background:rgba(239,159,39,0.35);border:1px solid #BA7517;"></span>
Strand
</span>
</div>
Tooltips
Include both the primary metric and secondary metric in tooltips:
tooltip: {
callbacks: {
title: items => items[0].label,
label: item => 'Mean B: ' + data[item.dataIndex].mean_b.toFixed(1) + ' Ų',
afterLabel: item => {
const r = data[item.dataIndex].resno;
if (helices.some(h => r >= h.initSeqNum && r <= h.endSeqNum)) return 'α-helix';
if (strands.some(s => r >= s.initSeqNum && r <= s.endSeqNum)) return 'β-strand';
return 'loop/coil';
}
}
}
Number formatting
All numbers reaching the screen must be rounded:
- B-factors:
.toFixed(1)+' Ų' - Correlations:
.toFixed(3) - Ramachandran probabilities:
.toFixed(4) - Axis tick integers:
Math.round()
Why not Pygal?
Pygal requires file I/O, a separate viewer, and a display context. It produces black images in headless environments and is slow. Chart.js in the browser has none of these problems and adds interactivity for free. Do not use Pygal.