Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Reusable plotting functions for common omics visualizations. Custom ggplot2/matplotlib implementations of volcano, MA, PCA, enrichment dotplots, boxplots, and survival curves. Use when creating volcano, MA, or enrichment plots.
.claude/skills/bio-data-visualization-specialized-omics-plots/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-19 | ✗→✓ | ▲ Improved | 52% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 54% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 66% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 36% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 39% | 0% |
<!--
#
#
-->
This skill provides reusable plotting functions for common omics visualizations that can be applied across different analysis types:
For DESeq2/edgeR built-in functions (plotMA, plotPCA, plotDispEsts), see differential-expression/de-visualization. For enrichplot-specific functions (dotplot, cnetplot, emapplot, gseaplot2), see pathway-analysis/enrichment-visualization.
rlibrary(ggplot2) library(ggrepel) volcano_plot <- function(res, fdr = 0.05, lfc = 1, top_n = 10) { res <- res %>% mutate( significance = case_when( padj < fdr & log2FoldChange > lfc ~ 'Up', padj < fdr & log2FoldChange < -lfc ~ 'Down', TRUE ~ 'NS' ), label = ifelse(rank(padj) <= top_n & significance != 'NS', gene, '') ) ggplot(res, aes(log2FoldChange, -log10(pvalue), color = significance)) + geom_point(alpha = 0.6, size = 1.5) + geom_text_repel(aes(label = label), color = 'black', size = 3, max.overlaps = 20) + scale_color_manual(values = c('Up' = '#E64B35', 'Down' = '#4DBBD5', 'NS' = 'grey60')) + geom_vline(xintercept = c(-lfc, lfc), linetype = 'dashed', color = 'grey40') + geom_hline(yintercept = -log10(fdr), linetype = 'dashed', color = 'grey40') + labs(x = expression(Log[2]~Fold~Change), y = expression(-Log[10]~P-value)) + theme_bw() + theme(panel.grid = element_blank()) }
pythonimport matplotlib.pyplot as plt import numpy as np def volcano_plot(df, fdr=0.05, lfc=1, ax=None): if ax is None: fig, ax = plt.subplots(figsize=(8, 6)) sig_up = (df['padj'] < fdr) & (df['log2FoldChange'] > lfc) sig_down = (df['padj'] < fdr) & (df['log2FoldChange'] < -lfc) ns = ~(sig_up | sig_down) ax.scatter(df.loc[ns, 'log2FoldChange'], -np.log10(df.loc[ns, 'pvalue']), c='grey', alpha=0.5, s=10, label='NS') ax.scatter(df.loc[sig_up, 'log2FoldChange'], -np.log10(df.loc[sig_up, 'pvalue']), c='#E64B35', alpha=0.7, s=15, label='Up') ax.scatter(df.loc[sig_down, 'log2FoldChange'], -np.log10(df.loc[sig_down, 'pvalue']), c='#4DBBD5', alpha=0.7, s=15, label='Down') ax.axhline(-np.log10(fdr), ls='--', c='grey', lw=0.8) ax.axvline(-lfc, ls='--', c='grey', lw=0.8) ax.axvline(lfc, ls='--', c='grey', lw=0.8) ax.set_xlabel('Log2 Fold Change') ax.set_ylabel('-Log10 P-value') ax.legend() return ax
rma_plot <- function(res, fdr = 0.05) { res <- res %>% mutate(significant = padj < fdr & !is.na(padj)) ggplot(res, aes(log10(baseMean), log2FoldChange, color = significant)) + geom_point(alpha = 0.5, size = 1) + scale_color_manual(values = c('FALSE' = 'grey60', 'TRUE' = '#E64B35')) + geom_hline(yintercept = 0, color = 'black', linewidth = 0.5) + labs(x = expression(Log[10]~Mean~Expression), y = expression(Log[2]~Fold~Change)) + theme_bw() + theme(panel.grid = element_blank(), legend.position = 'none') }
rpca_plot <- function(vsd, intgroup = 'condition', ntop = 500) { rv <- rowVars(assay(vsd)) select <- order(rv, decreasing = TRUE)[seq_len(min(ntop, length(rv)))] pca <- prcomp(t(assay(vsd)[select, ])) percentVar <- round(100 * pca$sdev^2 / sum(pca$sdev^2), 1) pca_df <- data.frame(PC1 = pca$x[, 1], PC2 = pca$x[, 2], colData(vsd)) ggplot(pca_df, aes(PC1, PC2, color = .data[[intgroup]])) + geom_point(size = 3) + stat_ellipse(level = 0.95, linetype = 'dashed') + labs(x = paste0('PC1 (', percentVar[1], '%)'), y = paste0('PC2 (', percentVar[2], '%)')) + theme_bw() + theme(panel.grid = element_blank()) }
pythonfrom sklearn.decomposition import PCA import matplotlib.pyplot as plt def pca_plot(df, metadata, color_by, ax=None): if ax is None: fig, ax = plt.subplots(figsize=(8, 6)) pca = PCA(n_components=2) pcs = pca.fit_transform(df.T) for group in metadata[color_by].unique(): mask = metadata[color_by] == group ax.scatter(pcs[mask, 0], pcs[mask, 1], label=group, alpha=0.8, s=50) ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]*100:.1f}%)') ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]*100:.1f}%)') ax.legend() return ax
rlibrary(ggplot2) enrichment_dotplot <- function(enrich_result, top_n = 20) { df <- enrich_result %>% arrange(p.adjust) %>% head(top_n) %>% mutate(Description = factor(Description, levels = rev(Description)), GeneRatio_numeric = sapply(strsplit(GeneRatio, '/'), function(x) as.numeric(x[1])/as.numeric(x[2]))) ggplot(df, aes(GeneRatio_numeric, Description, size = Count, color = p.adjust)) + geom_point() + scale_color_gradient(low = '#E64B35', high = '#4DBBD5', trans = 'log10') + scale_size_continuous(range = c(3, 10)) + labs(x = 'Gene Ratio', y = NULL, color = 'Adj. P-value', size = 'Count') + theme_bw() + theme(panel.grid.major.y = element_blank()) }
rlibrary(ggpubr) expression_boxplot <- function(df, gene, group_var) { ggboxplot(df, x = group_var, y = gene, color = group_var, add = 'jitter', palette = 'npg') + stat_compare_means(method = 't.test', label = 'p.signif') + labs(y = paste0(gene, ' Expression')) + theme(legend.position = 'none') }
pythonimport scanpy as sc import matplotlib.pyplot as plt def umap_plot(adata, color, ax=None, **kwargs): if ax is None: fig, ax = plt.subplots(figsize=(8, 6)) sc.pl.umap(adata, color=color, ax=ax, show=False, **kwargs) return ax # With custom styling sc.pl.umap(adata, color='leiden', palette='tab20', frameon=False, title='', legend_loc='on data', legend_fontsize=8)
rlibrary(corrplot) cor_mat <- cor(t(top_genes_mat), method = 'pearson') corrplot(cor_mat, method = 'color', type = 'lower', order = 'hclust', tl.col = 'black', tl.cex = 0.7, col = colorRampPalette(c('#4DBBD5', 'white', '#E64B35'))(100))
rggplot(df, aes(cluster, expression, fill = condition)) + geom_split_violin(alpha = 0.7) + geom_boxplot(width = 0.2, position = position_dodge(0.5), outlier.shape = NA) + scale_fill_manual(values = c('#4DBBD5', '#E64B35')) + theme_bw()
rlibrary(survival) library(survminer) fit <- survfit(Surv(time, status) ~ group, data = df) ggsurvplot(fit, data = df, risk.table = TRUE, pval = TRUE, palette = c('#4DBBD5', '#E64B35'), legend.labs = c('Low', 'High'))
<!-- 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 | 16,931 | 10,269 | -39% | 1 | 1 | 0% | 3,824 | 5,113 | +34% | 0 | 0 | — |
case-02 | fail→fail | 16,524 | 9,984 | -40% | 1 | 1 | 0% | 3,516 | 4,827 | +37% | 0 | 0 | — |
case-03 | fail→fail | 17,336 | 12,787 | -26% | 1 | 1 | 0% | 3,829 | 5,634 | +47% | 0 | 0 | — |
case-04 | fail→fail | 15,493 | 9,961 | -36% | 1 | 1 | 0% | 3,189 | 4,937 | +55% | 0 | 0 | — |
case-18 | pass→pass | 4,874 | 2,914 | -40% | 1 | 1 | 0% | 918 | 3,326 | +262% | 0 | 0 | — |
case-19 | fail→pass | 12,649 | 3,128 | -75% | 1 | 1 | 0% | 2,232 | 3,395 | +52% | 0 | 0 | — |
case-16 | pass→pass | 24,921 | 9,415 | -62% | 1 | 1 | 0% | 2,833 | 4,692 | +66% | 0 | 0 | — |
case-17 | fail→pass | 13,718 | 6,587 | -52% | 1 | 1 | 0% | 2,672 | 4,115 | +54% | 0 | 0 | — |
case-05 | fail→pass | 13,872 | 9,910 | -29% | 1 | 1 | 0% | 2,982 | 4,939 | +66% | 0 | 0 | — |
case-06 | fail→pass | 15,463 | 8,821 | -43% | 1 | 1 | 0% | 3,579 | 4,885 | +36% | 0 | 0 | — |
case-07 | fail→fail | 17,522 | 15,116 | -14% | 1 | 1 | 0% | 3,763 | 6,001 | +59% | 0 | 0 | — |
case-08 | pass→pass | 11,040 | 6,553 | -41% | 1 | 1 | 0% | 2,256 | 4,076 | +81% | 0 | 0 | — |
case-09 | fail→fail | 10,897 | 6,224 | -43% | 1 | 1 | 0% | 2,140 | 3,966 | +85% | 0 | 0 | — |
case-10 | fail→pass | 16,976 | 10,060 | -41% | 1 | 1 | 0% | 3,323 | 4,603 | +39% | 0 | 0 | — |
case-11 | pass→pass | 11,088 | 5,857 | -47% | 1 | 1 | 0% | 2,126 | 4,011 | +89% | 0 | 0 | — |
case-12 | pass→pass | 10,018 | 4,696 | -53% | 1 | 1 | 0% | 1,997 | 3,724 | +86% | 0 | 0 | — |
case-13 | fail→pass | 14,047 | 12,987 | -8% | 1 | 1 | 0% | 2,735 | 5,279 | +93% | 0 | 0 | — |
case-14 | fail→pass | 10,545 | 4,852 | -54% | 1 | 1 | 0% | 2,157 | 3,776 | +75% | 0 | 0 | — |
case-15 | pass→pass | 9,780 | 4,419 | -55% | 1 | 1 | 0% | 2,036 | 3,516 | +73% | 0 | 0 | — |
case-20 | pass→pass | 8,601 | 6,649 | -23% | 1 | 1 | 0% | 1,775 | 4,089 | +130% | 0 | 0 | — |
case-21 | pass→pass | 13,550 | 9,659 | -29% | 1 | 1 | 0% | 2,843 | 4,701 | +65% | 0 | 0 | — |
case-22 | pass→pass | 7,322 | 4,366 | -40% | 1 | 1 | 0% | 1,410 | 3,614 | +156% | 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 +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
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 | +59% |
Other measured skills in the registry, with their headline benchmark lift.