Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create professional PDF reports with text, tables, and embedded images using reportlab. Works with ANY LLM provider (GPT, Gemini, Claude, etc.).
.claude/skills/microck-data-export-pdf/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 150% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 14% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 108% | 0% |
| case-21 | ✗→✓ | ▲ Improved | 164% | 0% |
| case-07 | ✓→✓ | = Same ✓ | 98% | 0% |
This skill enables you to create professional PDF reports containing analysis summaries, formatted tables, and embedded visualizations. Unlike cloud-hosted solutions, this skill uses the reportlab Python library and executes locally in your environment, making it compatible with ALL LLM providers including GPT, Gemini, Claude, DeepSeek, and Qwen.
pythonfrom reportlab.lib.pagesizes import letter, A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch from reportlab.lib import colors from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, Image from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY from datetime import datetime import matplotlib.pyplot as plt
python# Create PDF file pdf_filename = "analysis_report.pdf" doc = SimpleDocTemplate(pdf_filename, pagesize=letter) story = [] # Container for PDF elements # Get default styles styles = getSampleStyleSheet() title_style = styles['Title'] heading_style = styles['Heading1'] normal_style = styles['Normal'] # Add title story.append(Paragraph("Analysis Report", title_style)) story.append(Spacer(1, 0.2*inch)) # Add date date_text = f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}" story.append(Paragraph(date_text, normal_style)) story.append(Spacer(1, 0.3*inch)) # Build PDF doc.build(story) print(f"✅ PDF saved to: {pdf_filename}")
pythonstory = [] # Title story.append(Paragraph("Single-Cell RNA-seq Analysis Report", title_style)) story.append(Spacer(1, 0.2*inch)) # Section heading story.append(Paragraph("1. Overview", heading_style)) story.append(Spacer(1, 0.1*inch)) # Paragraph text overview_text = """ This report summarizes the single-cell RNA-seq analysis performed on the dataset. The analysis includes quality control, normalization, dimensionality reduction, clustering, and cell type annotation. """ story.append(Paragraph(overview_text, normal_style)) story.append(Spacer(1, 0.2*inch))
python# Prepare table data table_data = [ ['Metric', 'Value'], # Header ['Total Cells', '5,000'], ['Total Genes', '20,000'], ['Mean Genes/Cell', '2,500'], ['Median UMIs/Cell', '10,000'] ] # Create table table = Table(table_data, colWidths=[2.5*inch, 2*inch]) # Style table table.setStyle(TableStyle([ # Header styling ('BACKGROUND', (0, 0), (-1, 0), colors.grey), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 12), # Body styling ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ('GRID', (0, 0), (-1, -1), 1, colors.black), ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'), ('FONTSIZE', (0, 1), (-1, -1), 10), ])) story.append(table) story.append(Spacer(1, 0.3*inch))
python# Save matplotlib figure first fig, ax = plt.subplots(figsize=(6, 4)) # ... create your plot ... plot_filename = "temp_plot.png" fig.savefig(plot_filename, dpi=150, bbox_inches='tight') plt.close(fig) # Add image to PDF story.append(Paragraph("2. UMAP Visualization", heading_style)) story.append(Spacer(1, 0.1*inch)) img = Image(plot_filename, width=4*inch, height=3*inch) story.append(img) story.append(Spacer(1, 0.2*inch))
pythonfrom reportlab.lib.pagesizes import letter from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.units import inch from reportlab.lib import colors from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image from datetime import datetime import matplotlib.pyplot as plt import pandas as pd def create_analysis_report(adata, output_path="analysis_report.pdf"): """Create comprehensive PDF analysis report""" # Initialize PDF doc = SimpleDocTemplate(output_path, pagesize=letter) story = [] styles = getSampleStyleSheet() # Title story.append(Paragraph("Single-Cell RNA-seq Analysis Report", styles['Title'])) story.append(Spacer(1, 0.2*inch)) story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", styles['Normal'])) story.append(Spacer(1, 0.3*inch)) # Overview story.append(Paragraph("1. Dataset Overview", styles['Heading1'])) story.append(Spacer(1, 0.1*inch)) overview_data = [ ['Metric', 'Value'], ['Total Cells', f'{adata.n_obs:,}'], ['Total Genes', f'{adata.n_vars:,}'], ['Observations', ', '.join(adata.obs.columns[:5].tolist())], ] table = Table(overview_data, colWidths=[2.5*inch, 3.5*inch]) table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.grey), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('GRID', (0, 0), (-1, -1), 1, colors.black), ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ])) story.append(table) story.append(Spacer(1, 0.3*inch)) # Cluster distribution if 'clusters' in adata.obs: story.append(Paragraph("2. Cluster Distribution", styles['Heading1'])) story.append(Spacer(1, 0.1*inch)) cluster_counts = adata.obs['clusters'].value_counts().sort_index() cluster_data = [['Cluster', 'Cell Count', 'Percentage']] total_cells = adata.n_obs for cluster, count in cluster_counts.items(): percentage = (count / total_cells) * 100 cluster_data.append([str(cluster), str(count), f'{percentage:.1f}%']) table = Table(cluster_data, colWidths=[1.5*inch, 1.5*inch, 1.5*inch]) table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.grey), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('GRID', (0, 0), (-1, -1), 1, colors.black), ('BACKGROUND', (0, 1), (-1, -1), colors.lightblue), ])) story.append(table) story.append(Spacer(1, 0.3*inch)) # Visualization (if UMAP exists) if 'X_umap' in adata.obsm: story.append(Paragraph("3. UMAP Visualization", styles['Heading1'])) story.append(Spacer(1, 0.1*inch)) # Create UMAP plot fig, ax = plt.subplots(figsize=(6, 5)) scatter = ax.scatter( adata.obsm['X_umap'][:, 0], adata.obsm['X_umap'][:, 1], c=adata.obs['clusters'].astype('category').cat.codes if 'clusters' in adata.obs else 'blue', s=5, alpha=0.5 ) ax.set_xlabel('UMAP1') ax.set_ylabel('UMAP2') ax.set_title('UMAP Projection') plot_path = 'temp_umap.png' fig.savefig(plot_path, dpi=150, bbox_inches='tight') plt.close(fig) img = Image(plot_path, width=5*inch, height=4*inch) story.append(img) # Build PDF doc.build(story) print(f"✅ PDF report saved to: {output_path}") return output_path # Usage create_analysis_report(adata, "my_analysis_report.pdf")
letter (US) or A4 (international) for standard documentsleftMargin, rightMargin, etc.pythonfrom reportlab.lib.pagesizes import letter from reportlab.pdfgen import canvas def add_header_footer(canvas_obj, doc): canvas_obj.saveState() # Header canvas_obj.setFont('Helvetica', 9) canvas_obj.drawString(inch, letter[1] - 0.5*inch, "Analysis Report") # Footer canvas_obj.drawString(inch, 0.5*inch, f"Page {doc.page}") canvas_obj.restoreState() doc = SimpleDocTemplate(pdf_filename, pagesize=letter) doc.build(story, onFirstPage=add_header_footer, onLaterPages=add_header_footer)
pythonfrom reportlab.platypus import Frame, PageTemplate frame1 = Frame(doc.leftMargin, doc.bottomMargin, doc.width/2-6, doc.height, id='col1') frame2 = Frame(doc.leftMargin+doc.width/2+6, doc.bottomMargin, doc.width/2-6, doc.height, id='col2') doc.addPageTemplates([PageTemplate(id='TwoCol', frames=[frame1, frame2])])
python# Highlight significant results for i, row in enumerate(deg_results): if row['qvalue'] < 0.05: table.setStyle(TableStyle([ ('BACKGROUND', (0, i+1), (-1, i+1), colors.yellow) ]))
pythonqc_metrics = { 'Total Cells': adata.n_obs, 'Median Genes/Cell': int(adata.obs['n_genes'].median()), 'Median UMIs/Cell': int(adata.obs['n_counts'].median()), 'Mean Mito %': f"{adata.obs['percent_mito'].mean():.2f}%" } table_data = [['Metric', 'Value']] + [[k, str(v)] for k, v in qc_metrics.items()] # ... create table as shown above
python# Top 10 upregulated genes top_genes = deg_df.nlargest(10, 'log2FC')[['gene', 'log2FC', 'qvalue']] table_data = [['Gene', 'log2FC', 'Q-value']] for _, row in top_genes.iterrows(): table_data.append([row['gene'], f"{row['log2FC']:.2f}", f"{row['qvalue']:.2e}"])
Solution:
pythonimport subprocess subprocess.check_call(['pip', 'install', 'reportlab'])
Solution: Ensure image path is correct and file exists before adding to PDF:
pythonimport os if os.path.exists(plot_filename): img = Image(plot_filename, width=4*inch, height=3*inch) story.append(img)
Solution: Reduce column widths or font size:
pythontable = Table(data, colWidths=[1.5*inch, 1.5*inch, 2*inch]) table.setStyle(TableStyle([('FONTSIZE', (0, 0), (-1, -1), 8)]))
reportlab (pure Python, widely supported)| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-06 | fail→pass | 12,018 | 10,122 | -16% | 1 | 1 | 0% | 2,222 | 5,544 | +150% | 0 | 0 | — |
case-07 | pass→pass | 14,503 | 9,548 | -34% | 1 | 1 | 0% | 2,713 | 5,371 | +98% | 0 | 0 | — |
case-01 | fail→pass | 31,080 | 16,295 | -48% | 1 | 1 | 0% | 6,199 | 7,074 | +14% | 0 | 0 | — |
case-02 | pass→pass | 11,429 | 8,737 | -24% | 1 | 1 | 0% | 2,120 | 5,302 | +150% | 0 | 0 | — |
case-03 | pass→pass | 10,486 | 9,425 | -10% | 1 | 1 | 0% | 1,917 | 5,548 | +189% | 0 | 0 | — |
case-04 | pass→pass | 16,985 | 12,297 | -28% | 1 | 1 | 0% | 3,034 | 5,867 | +93% | 0 | 0 | — |
case-05 | pass→pass | 20,745 | 19,060 | -8% | 1 | 1 | 0% | 4,222 | 7,576 | +79% | 0 | 0 | — |
case-08 | fail→pass | 16,043 | 13,547 | -16% | 1 | 1 | 0% | 2,983 | 6,196 | +108% | 0 | 0 | — |
case-09 | pass→pass | 8,689 | 8,508 | -2% | 1 | 1 | 0% | 1,635 | 5,223 | +219% | 0 | 0 | — |
case-10 | pass→pass | 13,266 | 16,994 | +28% | 1 | 1 | 0% | 2,696 | 7,351 | +173% | 0 | 0 | — |
case-11 | pass→pass | 15,318 | 10,723 | -30% | 1 | 1 | 0% | 3,128 | 5,805 | +86% | 0 | 0 | — |
case-12 | pass→pass | 12,821 | 10,024 | -22% | 1 | 1 | 0% | 2,440 | 5,510 | +126% | 0 | 0 | — |
case-13 | pass→pass | 8,479 | 5,460 | -36% | 1 | 1 | 0% | 1,683 | 4,641 | +176% | 0 | 0 | — |
case-14 | pass→pass | 12,575 | 10,072 | -20% | 1 | 1 | 0% | 2,575 | 5,572 | +116% | 0 | 0 | — |
case-15 | pass→pass | 15,912 | 11,748 | -26% | 1 | 1 | 0% | 3,096 | 5,964 | +93% | 0 | 0 | — |
case-16 | pass→pass | 16,701 | 11,875 | -29% | 1 | 1 | 0% | 3,457 | 5,944 | +72% | 0 | 0 | — |
case-17 | pass→pass | 6,271 | 3,245 | -48% | 1 | 1 | 0% | 1,198 | 4,126 | +244% | 0 | 0 | — |
case-18 | pass→pass | 12,713 | 7,535 | -41% | 1 | 1 | 0% | 2,148 | 4,914 | +129% | 0 | 0 | — |
case-19 | pass→pass | 17,596 | 12,093 | -31% | 1 | 1 | 0% | 3,166 | 5,980 | +89% | 0 | 0 | — |
case-20 | pass→pass | 5,449 | 3,821 | -30% | 1 | 1 | 0% | 1,053 | 4,271 | +306% | 0 | 0 | — |
case-21 | fail→pass | 9,382 | 5,472 | -42% | 1 | 1 | 0% | 1,668 | 4,408 | +164% | 0 | 0 | — |
case-22 | pass→pass | 11,429 | 8,234 | -28% | 1 | 1 | 0% | 1,953 | 5,049 | +159% | 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 +18 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.
Other measured skills in the registry, with their headline benchmark lift.