Create interactive inline Chart.js graphs directly in the chat from live Coot data. Use this skill whenever the user asks to plot, graph, chart, or visualise any per-residue data from Coot — B-factors, density correlations, Ramachandran probabilities, rotamer scores, or any other per-residue metric. Also use when the user asks to overlay secondary structure on a graph, or to compare metrics across chains. Prefer this approach over any file-based graphing (e.g. Pygal) — it is faster, interacti...
Scanned 9/1/2026
Install to Claude Code
npx -y skills add pemsley/coot --skill inline-graphs --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Inline Graphs?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/pemsley-inline-graphs)More formats (shields.io, HTML) on the badges page.
---
name: coot-inline-graphs
description: >
Create interactive inline Chart.js graphs directly in the chat from live Coot data.
Use this skill whenever the user asks to plot, graph, chart, or visualise any
per-residue data from Coot — B-factors, density correlations, Ramachandran
probabilities, rotamer scores, or any other per-residue metric. Also use when
the user asks to overlay secondary structure on a graph, or to compare metrics
across chains. Prefer this approach over any file-based graphing (e.g. Pygal)
— it is faster, interactive, and renders inline in the conversation.
---
# 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
1. **Get data from Coot** — fetch per-residue metrics using the Python API
2. **Get secondary structure** — call `add_header_secondary_structure_info()` then
`get_header_secondary_structure_info()` if overlays are wanted
3. **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
```python
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
```python
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
```python
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.
```python
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:
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
```
For secondary structure annotation overlays, also load:
```html
<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.
```javascript
const data = [
{"resno": 1, "resname": "ASP", "mean_b": 34.95},
// ... all residues
];
```
### Canvas sizing
Always wrap `<canvas>` in a `<div>` with explicit height:
```html
<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:
```javascript
const boxHeightUnits = Math.round(22 * yAxisMax / 280);
const boxYMax = yAxisMax;
const boxYMin = yAxisMax - boxHeightUnits;
```
Build a `resnoToIndex` lookup first (maps residue number → bar index):
```javascript
const resnoToIndex = {};
data.forEach((d, i) => { resnoToIndex[d.resno] = i; });
```
### Annotation spec
```javascript
// 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:
```javascript
// 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:
```javascript
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
```javascript
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:
```html
<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:
```html
<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:
```javascript
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!