Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Publication-quality visualizations for biomedical and genomics data. Use when creating volcano plots, heatmaps, UMAP plots, dot plots, survival curves, forest plots, or multi-panel figures. Includes scanpy, matplotlib, seaborn, plotly workflows with journal-ready aesthetics and proper statistical annotations.
.claude/skills/data-visualization-biomedical/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 119% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 40% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 29% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 155% | 0% |
| case-11 | ✓→✗ | ▼ Worse | 53% | 0% |
<!--
#
#
-->
pythonimport matplotlib.pyplot as plt import seaborn as sns import numpy as np import pandas as pd # Nature/Blood style settings plt.rcParams.update({ 'font.family': 'Arial', 'font.size': 8, 'axes.labelsize': 8, 'axes.titlesize': 9, 'xtick.labelsize': 7, 'ytick.labelsize': 7, 'legend.fontsize': 7, 'figure.dpi': 300, 'savefig.dpi': 300, 'savefig.bbox': 'tight', 'axes.linewidth': 0.5, 'xtick.major.width': 0.5, 'ytick.major.width': 0.5, }) # Color palettes NATURE_COLORS = ['#E64B35', '#4DBBD5', '#00A087', '#3C5488', '#F39B7F', '#8491B4'] BLOOD_COLORS = ['#D62728', '#1F77B4', '#2CA02C', '#FF7F0E', '#9467BD', '#8C564B']
pythondef volcano_plot(df, log2fc_col='log2FC', pval_col='pval_adj', gene_col='gene', fc_thresh=1, pval_thresh=0.05, highlight_genes=None, figsize=(4, 4)): """Publication-quality volcano plot.""" fig, ax = plt.subplots(figsize=figsize) df = df.copy() df['-log10pval'] = -np.log10(df[pval_col].clip(lower=1e-300)) # Categorize points df['category'] = 'NS' df.loc[(df[log2fc_col] > fc_thresh) & (df[pval_col] < pval_thresh), 'category'] = 'Up' df.loc[(df[log2fc_col] < -fc_thresh) & (df[pval_col] < pval_thresh), 'category'] = 'Down' colors = {'NS': '#CCCCCC', 'Up': '#E64B35', 'Down': '#4DBBD5'} for cat, color in colors.items(): subset = df[df['category'] == cat] ax.scatter(subset[log2fc_col], subset['-log10pval'], c=color, s=10, alpha=0.7, edgecolors='none', label=cat) # Add threshold lines ax.axhline(-np.log10(pval_thresh), color='grey', linestyle='--', linewidth=0.5) ax.axvline(-fc_thresh, color='grey', linestyle='--', linewidth=0.5) ax.axvline(fc_thresh, color='grey', linestyle='--', linewidth=0.5) # Label specific genes if highlight_genes: for gene in highlight_genes: if gene in df[gene_col].values: row = df[df[gene_col] == gene].iloc[0] ax.annotate(gene, (row[log2fc_col], row['-log10pval']), fontsize=6, ha='center') ax.set_xlabel('log₂ Fold Change') ax.set_ylabel('-log₁₀ Adjusted P-value') ax.legend(frameon=False, loc='upper right') plt.tight_layout() return fig, ax
pythonimport scipy.cluster.hierarchy as sch from matplotlib.colors import LinearSegmentedColormap def clustered_heatmap(data, row_labels=None, col_labels=None, cmap='RdBu_r', center=0, figsize=(8, 10), row_cluster=True, col_cluster=True): """Hierarchically clustered heatmap.""" # Clustering if row_cluster: row_linkage = sch.linkage(data, method='ward') row_order = sch.dendrogram(row_linkage, no_plot=True)['leaves'] data = data[row_order, :] if row_labels is not None: row_labels = [row_labels[i] for i in row_order] if col_cluster: col_linkage = sch.linkage(data.T, method='ward') col_order = sch.dendrogram(col_linkage, no_plot=True)['leaves'] data = data[:, col_order] if col_labels is not None: col_labels = [col_labels[i] for i in col_order] fig, ax = plt.subplots(figsize=figsize) im = ax.imshow(data, aspect='auto', cmap=cmap, vmin=center-np.abs(data).max(), vmax=center+np.abs(data).max()) if row_labels: ax.set_yticks(range(len(row_labels))) ax.set_yticklabels(row_labels) if col_labels: ax.set_xticks(range(len(col_labels))) ax.set_xticklabels(col_labels, rotation=45, ha='right') plt.colorbar(im, ax=ax, shrink=0.5, label='Expression (z-score)') plt.tight_layout() return fig, ax
pythonimport scanpy as sc def enhanced_dotplot(adata, genes, groupby, figsize=(10, 8)): """Enhanced dot plot with proper visibility.""" sc.pl.dotplot( adata, var_names=genes, groupby=groupby, expression_cutoff=0.0001, mean_only_expressed=False, standard_scale='None', smallest_dot=0.1, dot_max=1.0, cmap='Reds', colorbar_title='Mean expression', size_title='Fraction of cells (%)', figsize=figsize, show=False ) plt.tight_layout() return plt.gcf() def multi_batch_umap(adata, color_by, batch_key='batch', figsize_per=(4, 4)): """UMAP plots per batch.""" batches = adata.obs[batch_key].unique() n_batches = len(batches) fig, axes = plt.subplots(1, n_batches, figsize=(figsize_per[0]*n_batches, figsize_per[1])) if n_batches == 1: axes = [axes] for ax, batch in zip(axes, batches): adata_batch = adata[adata.obs[batch_key] == batch] sc.pl.umap(adata_batch, color=color_by, ax=ax, show=False, title=f'{batch}') plt.tight_layout() return fig
pythonfrom scipy import stats def add_significance(ax, x1, x2, y, h, p_value): """Add significance bar to plot.""" ax.plot([x1, x1, x2, x2], [y, y+h, y+h, y], 'k-', linewidth=0.5) if p_value < 0.0001: sig = '****' elif p_value < 0.001: sig = '***' elif p_value < 0.01: sig = '**' elif p_value < 0.05: sig = '*' else: sig = 'ns' ax.text((x1+x2)/2, y+h, sig, ha='center', va='bottom', fontsize=8)
pythonfrom matplotlib.gridspec import GridSpec def create_figure_panel(n_rows, n_cols, width_ratios=None, height_ratios=None): """Create multi-panel figure.""" fig = plt.figure(figsize=(3*n_cols, 3*n_rows)) gs = GridSpec(n_rows, n_cols, figure=fig, width_ratios=width_ratios or [1]*n_cols, height_ratios=height_ratios or [1]*n_rows, wspace=0.3, hspace=0.3) axes = [] for i in range(n_rows): row = [] for j in range(n_cols): ax = fig.add_subplot(gs[i, j]) row.append(ax) axes.append(row) return fig, axes def label_panels(axes, labels=None, fontsize=12, fontweight='bold'): """Add A, B, C... labels to panels.""" if labels is None: labels = [chr(65+i) for i in range(len(axes))] # A, B, C... for ax, label in zip(axes, labels): ax.text(-0.15, 1.05, label, transform=ax.transAxes, fontsize=fontsize, fontweight=fontweight, va='top')
pythondef save_figure(fig, filename, formats=['pdf', 'png', 'svg']): """Save in multiple formats for journals.""" for fmt in formats: fig.savefig(f"{filename}.{fmt}", format=fmt, dpi=300, bbox_inches='tight', facecolor='white', edgecolor='none') print(f"Saved: {filename}.{{{'|'.join(formats)}}}")
See references/color_guidelines.md for accessibility standards. See scripts/figure_templates.py for pre-built templates.
<!-- AUTHOR_SIGNATURE: 9a7f3c2e-MD-BABU-MIA-2026-MSSM-SECURE -->
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 29,254 | 16,965 | -42% | 1 | 1 | 0% | 5,074 | 6,439 | +27% | 0 | 0 | — |
case-02 | fail→fail | 17,184 | 15,283 | -11% | 1 | 1 | 0% | 3,598 | 5,689 | +58% | 0 | 0 | — |
case-03 | fail→fail | 18,913 | 21,443 | +13% | 1 | 1 | 0% | 4,134 | 7,351 | +78% | 0 | 0 | — |
case-04 | fail→fail | 14,616 | 16,107 | +10% | 1 | 1 | 0% | 2,862 | 4,268 | +49% | 0 | 0 | — |
case-05 | fail→pass | 12,648 | 10,545 | -17% | 1 | 1 | 0% | 1,965 | 4,308 | +119% | 0 | 0 | — |
case-06 | fail→pass | 11,875 | 3,616 | -70% | 1 | 1 | 0% | 2,318 | 3,256 | +40% | 0 | 0 | — |
case-21 | pass→pass | 10,876 | 11,612 | +7% | 1 | 1 | 0% | 2,082 | 5,012 | +141% | 0 | 0 | — |
case-22 | pass→pass | 16,476 | 17,018 | +3% | 1 | 1 | 0% | 2,807 | 6,126 | +118% | 0 | 0 | — |
case-11 | pass→fail | 20,409 | 18,316 | -10% | 1 | 1 | 0% | 4,257 | 6,493 | +53% | 0 | 0 | — |
case-07 | fail→pass | 12,056 | 2,972 | -75% | 1 | 1 | 0% | 2,456 | 3,157 | +29% | 0 | 0 | — |
case-08 | fail→fail | 16,362 | 13,591 | -17% | 1 | 1 | 0% | 3,143 | 5,216 | +66% | 0 | 0 | — |
case-09 | fail→fail | 19,058 | 16,868 | -11% | 1 | 1 | 0% | 3,157 | 6,025 | +91% | 0 | 0 | — |
case-10 | fail→fail | 13,621 | 4,421 | -68% | 1 | 1 | 0% | 2,429 | 3,378 | +39% | 0 | 0 | — |
case-12 | pass→fail | 12,089 | 9,964 | -18% | 1 | 1 | 0% | 2,637 | 4,671 | +77% | 0 | 0 | — |
case-13 | pass→pass | 6,939 | 2,931 | -58% | 1 | 1 | 0% | 1,416 | 3,150 | +122% | 0 | 0 | — |
case-14 | pass→pass | 8,087 | 2,685 | -67% | 1 | 1 | 0% | 1,458 | 3,144 | +116% | 0 | 0 | — |
case-15 | fail→pass | 7,750 | 5,724 | -26% | 1 | 1 | 0% | 1,454 | 3,703 | +155% | 0 | 0 | — |
case-20 | pass→pass | 14,561 | 13,396 | -8% | 1 | 1 | 0% | 2,922 | 5,320 | +82% | 0 | 0 | — |
case-16 | fail→fail | 12,961 | 12,891 | -1% | 1 | 1 | 0% | 2,702 | 5,199 | +92% | 0 | 0 | — |
case-17 | pass→pass | 14,890 | 10,296 | -31% | 1 | 1 | 0% | 2,813 | 4,532 | +61% | 0 | 0 | — |
case-18 | pass→pass | 9,104 | 5,637 | -38% | 1 | 1 | 0% | 1,595 | 3,553 | +123% | 0 | 0 | — |
case-19 | fail→fail | 13,268 | 7,762 | -41% | 1 | 1 | 0% | 2,348 | 4,003 | +70% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases. 3 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 7/24/2026 | +45% |
Other measured skills in the registry, with their headline benchmark lift.