[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M73HSFSW64GJBDFE3T7M679B":3,"skill-same:01M73HSFSW64GJBDFE3T7M679B":48},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":29,"ownerHtmlUrl":30,"ownerBlogUrl":31,"ownerDescription":32,"updatedAt":33,"install":34,"resources":47},"01M73HSFSW64GJBDFE3T7M679B","plotly-interactive-plots",null,"Interactive scientific visualization with Plotly. Two APIs: plotly.express (px) for one-liner DataFrame plots, plotly.graph_objects (go) for trace-level control. 40+ chart types with hover, zoom, pan, animation. Exports HTML or static PNG\u002FSVG\u002FPDF via kaleido. Use for volcano plots with gene hover, dose-response dashboards, expression heatmaps, 3D molecular views. Use seaborn for stats; matplotlib for publication figures.","bg-szy\u002FTOP-SKILLS","bg-szy","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F106576190?v=4","01M73A4E8KPV67SFXRY8XZGNBX","ShuZhiyong","01M73A4E96MQ294MHM74TZBVQD",6,"MIT","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fawesome-skills\u002Fplotly-interactive-plots\u002FSKILL.md",[],[],"2026-11-11 15:11:14",false,"---\nname: \"plotly-interactive-plots\"\ndescription: \"Interactive scientific visualization with Plotly. Two APIs: plotly.express (px) for one-liner DataFrame plots, plotly.graph_objects (go) for trace-level control. 40+ chart types with hover, zoom, pan, animation. Exports HTML or static PNG\u002FSVG\u002FPDF via kaleido. Use for volcano plots with gene hover, dose-response dashboards, expression heatmaps, 3D molecular views. Use seaborn for stats; matplotlib for publication figures.\"\nlicense: \"MIT\"\n---\n\n# Plotly Interactive Plots\n\n## Overview\n\nPlotly is a Python library for producing interactive, web-ready figures backed by HTML and JavaScript. It exposes two complementary APIs: `plotly.express` (px) provides a high-level, DataFrame-oriented interface for generating common chart types in one line, while `plotly.graph_objects` (go) offers fine-grained control over every trace, axis, and layout property. Figures are fully interactive by default — supporting hover tooltips, zoom, pan, and click events — and can be embedded in web pages, Jupyter notebooks, or built into web applications using the Dash framework.\n\n## When to Use\n\n- You need hover tooltips that display gene names, p-values, or sample metadata without cluttering the static figure.\n- You are building a multi-panel interactive dashboard for dose-response curves, patient cohorts, or multi-condition comparisons.\n- You want to share figures as self-contained HTML files that non-programmers can explore in a browser.\n- You need 3D scatter or surface plots for structural biology, conformational landscapes, or PCA of high-dimensional data.\n- You are creating heatmaps of gene expression or correlation matrices where users need to zoom into specific gene clusters.\n- You require animation frames to show time-series or treatment-response trajectories.\n- Use `seaborn` instead when you need automatic statistical aggregation (confidence intervals, regression fits) with minimal code.\n- Use `matplotlib` when you need fine-grained control over every axis element for print-ready publication figures at exact journal specifications.\n\n## Prerequisites\n\n- **Python packages**: `plotly`, `kaleido` (static image export), `pandas`, `numpy`\n- **Data requirements**: pandas DataFrames or NumPy arrays; long-form (tidy) data works best with `px`\n- **Environment**: Jupyter Lab\u002FNotebook (inline rendering), or save as HTML for browser display\n\n```bash\npip install plotly kaleido pandas numpy\n```\n\nFor Jupyter Lab inline rendering (if not automatic):\n\n```bash\npip install \"jupyterlab>=3\" ipywidgets\n```\n\n## Quick Start\n\n```python\nimport plotly.express as px\nimport pandas as pd\n\n# Gene expression scatter with hover info\ndf = pd.DataFrame({\n    \"log2FC\": [-3.1, 0.2, 1.8, 2.5, -0.5, 4.1],\n    \"neg_log10_padj\": [8.2, 0.4, 2.1, 6.8, 0.1, 9.3],\n    \"gene\": [\"BRCA1\", \"MYC\", \"TP53\", \"EGFR\", \"CDKN1A\", \"KRAS\"],\n    \"significance\": [\"sig\", \"ns\", \"ns\", \"sig\", \"ns\", \"sig\"],\n})\n\nfig = px.scatter(\n    df, x=\"log2FC\", y=\"neg_log10_padj\",\n    color=\"significance\", hover_name=\"gene\",\n    title=\"Volcano Plot — Treatment vs Control\",\n)\nfig.show()\n```\n\n## Core API\n\n### Module 1: px Scatter and Line — Relational Plots\n\n`px.scatter()` and `px.line()` map DataFrame columns to visual encodings (color, symbol, size) and automatically populate hover tooltips from `hover_data`.\n\n```python\nimport plotly.express as px\nimport pandas as pd\nimport numpy as np\n\n# Dose-response scatter: color by drug, symbol by cell line\nnp.random.seed(42)\ndf = pd.DataFrame({\n    \"dose_uM\": np.tile([0.01, 0.1, 1, 10, 100], 4),\n    \"viability\": np.clip(np.random.normal(\n        [100, 90, 70, 40, 10] * 4, 5), 0, 110),\n    \"drug\": [\"DrugA\"] * 5 + [\"DrugA\"] * 5 + [\"DrugB\"] * 5 + [\"DrugB\"] * 5,\n    \"cell_line\": [\"HCT116\"] * 10 + [\"MCF7\"] * 10,\n    \"replicate\": np.tile([1, 2, 3, 4, 5], 4),\n})\n\nfig = px.scatter(\n    df, x=\"dose_uM\", y=\"viability\",\n    color=\"drug\", symbol=\"cell_line\",\n    log_x=True,\n    hover_data={\"replicate\": True, \"dose_uM\": \":.2f\"},\n    labels={\"viability\": \"Cell Viability (%)\", \"dose_uM\": \"Dose (µM)\"},\n    title=\"Dose-Response by Drug and Cell Line\",\n)\nfig.show()\nprint(f\"Figure has {len(fig.data)} traces\")\n```\n\n```python\n# Time-course gene expression line plot\ntime_df = pd.DataFrame({\n    \"hour\": list(range(0, 25, 4)) * 3,\n    \"expression\": [1.0, 1.8, 3.2, 4.5, 3.8, 2.1, 1.2,\n                   1.0, 2.5, 5.1, 6.8, 5.5, 3.2, 1.8,\n                   1.0, 1.1, 1.0, 1.2, 1.1, 1.0, 0.9],\n    \"gene\": [\"MYC\"] * 7 + [\"EGFR\"] * 7 + [\"GAPDH\"] * 7,\n})\n\nfig = px.line(\n    time_df, x=\"hour\", y=\"expression\",\n    color=\"gene\", markers=True,\n    labels={\"expression\": \"Relative Expression (log2)\", \"hour\": \"Time (h)\"},\n    title=\"Time-Course Gene Expression\",\n)\nfig.update_traces(line=dict(width=2.5), marker=dict(size=8))\nfig.show()\n```\n\n### Module 2: px Statistical Plots — Distributions and Categories\n\n`px.box()`, `px.violin()`, `px.histogram()`, and `px.strip()` produce publication-ready distribution summaries with built-in grouping.\n\n```python\nimport plotly.express as px\nimport pandas as pd\nimport numpy as np\n\n# Violin + strip overlay: expression by cell type\nnp.random.seed(7)\nn = 60\ncell_data = pd.DataFrame({\n    \"expression\": np.concatenate([\n        np.random.normal(4.2, 0.8, n),\n        np.random.normal(6.5, 1.2, n),\n        np.random.normal(2.8, 0.6, n),\n    ]),\n    \"cell_type\": [\"T cell\"] * n + [\"B cell\"] * n + [\"NK cell\"] * n,\n    \"patient_id\": np.tile([f\"P{i:02d}\" for i in range(1, 11)], 18),\n})\n\nfig = px.violin(\n    cell_data, x=\"cell_type\", y=\"expression\",\n    color=\"cell_type\", box=True, points=\"all\",\n    hover_data=[\"patient_id\"],\n    labels={\"expression\": \"CD3E Expression (log2 CPM)\"},\n    title=\"CD3E Expression Across Cell Types\",\n)\nfig.update_traces(jitter=0.3, pointpos=-1.5)\nfig.show()\nprint(f\"Cells per type: {cell_data.groupby('cell_type').size().to_dict()}\")\n```\n\n```python\n# Histogram with rug: distribution of fold changes\nfc_df = pd.DataFrame({\n    \"log2FC\": np.concatenate([\n        np.random.normal(0.1, 0.8, 500),   # not DE genes\n        np.random.normal(2.5, 0.4, 50),    # upregulated\n        np.random.normal(-2.3, 0.4, 40),   # downregulated\n    ]),\n    \"category\": [\"background\"] * 500 + [\"up\"] * 50 + [\"down\"] * 40,\n})\n\nfig = px.histogram(\n    fc_df, x=\"log2FC\", color=\"category\",\n    nbins=60, barmode=\"overlay\", opacity=0.7,\n    marginal=\"rug\",\n    labels={\"log2FC\": \"log2 Fold Change\", \"count\": \"Gene Count\"},\n    title=\"Distribution of Fold Changes (DESeq2 Results)\",\n    color_discrete_map={\"background\": \"gray\", \"up\": \"crimson\", \"down\": \"steelblue\"},\n)\nfig.show()\n```\n\n### Module 3: px Heatmap and Matrix — Gene Expression and Correlations\n\n`px.imshow()` renders 2D arrays or DataFrames as color-encoded matrices, ideal for expression heatmaps and correlation matrices.\n\n```python\nimport plotly.express as px\nimport pandas as pd\nimport numpy as np\n\n# Gene expression heatmap (genes × samples)\nnp.random.seed(12)\ngenes = [f\"Gene_{g}\" for g in [\"BRCA1\", \"TP53\", \"EGFR\", \"MYC\", \"KRAS\",\n                                 \"CDKN1A\", \"RB1\", \"PTEN\", \"VHL\", \"APC\"]]\nsamples = [f\"S{i:02d}\" for i in range(1, 9)]\n\nexpr_matrix = pd.DataFrame(\n    np.random.normal(0, 1.5, (10, 8)) +\n    np.array([2, -1, 3, -2, 1, -3, 0, 2, -1, 3]).reshape(-1, 1),\n    index=genes, columns=samples,\n)\n\nfig = px.imshow(\n    expr_matrix,\n    color_continuous_scale=\"RdBu_r\",\n    color_continuous_midpoint=0,\n    aspect=\"auto\",\n    labels={\"color\": \"log2 Expression (z-score)\"},\n    title=\"Gene Expression Heatmap\",\n)\nfig.update_xaxes(side=\"top\")\nfig.update_layout(width=600, height=500)\nfig.show()\nprint(f\"Heatmap shape: {expr_matrix.shape} (genes × samples)\")\n```\n\n```python\n# Correlation matrix heatmap\nfrom itertools import combinations\n\nmarkers = [\"IL6\", \"TNF\", \"CXCL10\", \"IFNg\", \"IL10\", \"IL1B\", \"CCL2\", \"IL17A\"]\nnp.random.seed(3)\nraw = np.random.multivariate_normal(\n    mean=np.zeros(8),\n    cov=np.eye(8) * 0.3 + 0.7,\n    size=80,\n)\ncorr_df = pd.DataFrame(raw, columns=markers).corr()\n\nfig = px.imshow(\n    corr_df,\n    color_continuous_scale=\"RdBu_r\",\n    color_continuous_midpoint=0,\n    zmin=-1, zmax=1,\n    text_auto=\".2f\",\n    title=\"Cytokine Correlation Matrix (n=80 patients)\",\n)\nfig.update_traces(textfont_size=10)\nfig.show()\n```\n\n### Module 4: go Graph Objects — Full Trace Control\n\n`plotly.graph_objects` provides fine-grained access to every trace property: marker symbols, error bars, fill areas, and multi-trace layouts. Essential when `px` lacks the flexibility you need.\n\n```python\nimport plotly.graph_objects as go\nimport numpy as np\n\n# Volcano plot built from scratch with go.Scatter\nnp.random.seed(99)\nn_genes = 5000\nlog2fc = np.random.normal(0, 1.2, n_genes)\npval = np.random.uniform(0, 1, n_genes) ** 2  # skew toward low p-values\nneg_log10_p = -np.log10(pval + 1e-300)\ngene_names = [f\"Gene_{i:04d}\" for i in range(n_genes)]\n\n# Classify genes\nsig_mask = (np.abs(log2fc) > 1.5) & (neg_log10_p > 3)\nup_mask = sig_mask & (log2fc > 0)\ndown_mask = sig_mask & (log2fc \u003C 0)\nns_mask = ~sig_mask\n\nfig = go.Figure()\n\n# Non-significant background\nfig.add_trace(go.Scatter(\n    x=log2fc[ns_mask], y=neg_log10_p[ns_mask],\n    mode=\"markers\",\n    name=\"Not significant\",\n    marker=dict(color=\"lightgray\", size=4, opacity=0.5),\n    text=[gene_names[i] for i in np.where(ns_mask)[0]],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb>\u003Cbr>log2FC: %{x:.2f}\u003Cbr>-log10(p): %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n))\n\n# Upregulated\nfig.add_trace(go.Scatter(\n    x=log2fc[up_mask], y=neg_log10_p[up_mask],\n    mode=\"markers\",\n    name=f\"Up ({up_mask.sum()} genes)\",\n    marker=dict(color=\"crimson\", size=7, opacity=0.8),\n    text=[gene_names[i] for i in np.where(up_mask)[0]],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb>\u003Cbr>log2FC: %{x:.2f}\u003Cbr>-log10(p): %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n))\n\n# Downregulated\nfig.add_trace(go.Scatter(\n    x=log2fc[down_mask], y=neg_log10_p[down_mask],\n    mode=\"markers\",\n    name=f\"Down ({down_mask.sum()} genes)\",\n    marker=dict(color=\"steelblue\", size=7, opacity=0.8),\n    text=[gene_names[i] for i in np.where(down_mask)[0]],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb>\u003Cbr>log2FC: %{x:.2f}\u003Cbr>-log10(p): %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n))\n\n# Threshold lines\nfig.add_hline(y=3, line_dash=\"dash\", line_color=\"black\", line_width=1)\nfig.add_vline(x=1.5, line_dash=\"dash\", line_color=\"black\", line_width=1)\nfig.add_vline(x=-1.5, line_dash=\"dash\", line_color=\"black\", line_width=1)\n\nfig.update_layout(\n    title=\"Volcano Plot (Treatment vs Control, n=5000 genes)\",\n    xaxis_title=\"log2 Fold Change\",\n    yaxis_title=\"-log10(adjusted p-value)\",\n    legend=dict(x=0.01, y=0.99),\n    width=750, height=550,\n)\nfig.show()\nprint(f\"Up: {up_mask.sum()}, Down: {down_mask.sum()}, NS: {ns_mask.sum()}\")\n```\n\n```python\n# Bar chart with error bars: mean ± SEM per treatment group\ngroups = [\"Vehicle\", \"DrugA 1µM\", \"DrugA 10µM\", \"DrugB 1µM\", \"DrugB 10µM\"]\nmeans = [100.0, 82.3, 54.7, 91.2, 68.5]\nsems = [3.2, 4.1, 3.8, 3.5, 4.7]\n\nfig = go.Figure(go.Bar(\n    x=groups, y=means,\n    error_y=dict(type=\"data\", array=sems, visible=True),\n    marker_color=[\"gray\", \"lightsalmon\", \"crimson\", \"lightblue\", \"steelblue\"],\n    hovertemplate=\"%{x}\u003Cbr>Mean: %{y:.1f}%\u003Cbr>SEM: ±%{error_y.array:.1f}%\u003Cextra>\u003C\u002Fextra>\",\n))\n\nfig.update_layout(\n    title=\"Cell Viability by Treatment (Mean ± SEM, n=6)\",\n    yaxis_title=\"Viability (%)\", yaxis_range=[0, 120],\n    xaxis_title=\"Treatment Group\",\n    showlegend=False,\n)\nfig.show()\n```\n\n### Module 5: 3D and Specialized Charts\n\nPlotly supports 3D scatter, surface plots, parallel coordinates, and treemaps — chart types unavailable in seaborn or standard matplotlib.\n\n```python\nimport plotly.express as px\nimport numpy as np\nimport pandas as pd\n\n# 3D PCA scatter: cell clusters in embedding space\nnp.random.seed(42)\nn_per_cluster = 80\ncluster_centers = {\"T cell\": [3, 2, 1], \"B cell\": [-3, 1, 2], \"Monocyte\": [0, -3, -1]}\n\nrecords = []\nfor ctype, center in cluster_centers.items():\n    coords = np.random.normal(center, 0.8, (n_per_cluster, 3))\n    for row in coords:\n        records.append({\n            \"PC1\": row[0], \"PC2\": row[1], \"PC3\": row[2],\n            \"cell_type\": ctype,\n            \"score\": np.random.uniform(0.5, 1.0),\n        })\n\npca_df = pd.DataFrame(records)\n\nfig = px.scatter_3d(\n    pca_df, x=\"PC1\", y=\"PC2\", z=\"PC3\",\n    color=\"cell_type\", size=\"score\", opacity=0.7,\n    hover_data={\"score\": \":.3f\"},\n    title=\"3D PCA — Single-Cell Transcriptomics\",\n)\nfig.update_traces(marker=dict(sizeref=0.04))\nfig.show()\nprint(f\"Total cells: {len(pca_df)}, clusters: {pca_df['cell_type'].nunique()}\")\n```\n\n```python\nimport plotly.graph_objects as go\nimport numpy as np\nimport pandas as pd\n\n# Parallel coordinates: multi-parameter drug screen\nnp.random.seed(5)\nn_compounds = 200\ndrug_df = pd.DataFrame({\n    \"MW\": np.random.normal(380, 60, n_compounds),\n    \"logP\": np.random.uniform(-1, 6, n_compounds),\n    \"HBA\": np.random.randint(2, 10, n_compounds),\n    \"HBD\": np.random.randint(0, 6, n_compounds),\n    \"IC50_nM\": np.random.lognormal(4, 1.5, n_compounds),\n    \"selectivity\": np.random.uniform(1, 100, n_compounds),\n})\n\nfig = px.parallel_coordinates(\n    drug_df,\n    color=\"IC50_nM\",\n    color_continuous_scale=\"RdYlGn_r\",\n    dimensions=[\"MW\", \"logP\", \"HBA\", \"HBD\", \"IC50_nM\", \"selectivity\"],\n    labels={\n        \"MW\": \"MW (Da)\", \"logP\": \"logP\",\n        \"HBA\": \"H-Bond Acceptors\", \"HBD\": \"H-Bond Donors\",\n        \"IC50_nM\": \"IC50 (nM)\", \"selectivity\": \"Selectivity Index\",\n    },\n    title=\"Drug Candidate Properties — Parallel Coordinates\",\n)\nfig.show()\n```\n\n### Module 6: Subplots and Export\n\n`make_subplots()` creates multi-panel layouts with shared axes, mixed chart types, and independent traces per panel. `fig.write_html()` exports interactive figures; `fig.write_image()` exports static files via kaleido.\n\n```python\nfrom plotly.subplots import make_subplots\nimport plotly.graph_objects as go\nimport numpy as np\n\n# Two-panel: raw data + summary statistics\nnp.random.seed(77)\ndoses = [0.01, 0.1, 1, 10, 100]\ndrugs = {\"DrugA\": {\"EC50\": 1.0, \"hill\": 1.5}, \"DrugB\": {\"EC50\": 8.0, \"hill\": 0.9}}\n\ndef hill_curve(dose, ec50, hill, top=100, bottom=0):\n    return bottom + (top - bottom) \u002F (1 + (ec50 \u002F dose) ** hill)\n\nfig = make_subplots(\n    rows=1, cols=2,\n    subplot_titles=[\"Dose-Response Curves\", \"IC50 Comparison\"],\n    shared_yaxes=False,\n)\n\ncolors = {\"DrugA\": \"crimson\", \"DrugB\": \"steelblue\"}\nic50_values = []\n\nfor drug, params in drugs.items():\n    # Smooth fit curve\n    x_fit = np.logspace(-2, 2, 200)\n    y_fit = hill_curve(x_fit, params[\"EC50\"], params[\"hill\"])\n    fig.add_trace(go.Scatter(\n        x=x_fit, y=y_fit, mode=\"lines\",\n        name=f\"{drug} fit\", line=dict(color=colors[drug], width=2.5),\n    ), row=1, col=1)\n\n    # Noisy data points\n    y_data = [hill_curve(d, params[\"EC50\"], params[\"hill\"]) +\n              np.random.normal(0, 4) for d in doses]\n    fig.add_trace(go.Scatter(\n        x=doses, y=y_data, mode=\"markers\",\n        name=f\"{drug} data\", marker=dict(color=colors[drug], size=9),\n        showlegend=False,\n    ), row=1, col=1)\n    ic50_values.append(params[\"EC50\"])\n\n# Bar chart of IC50 values\nfig.add_trace(go.Bar(\n    x=list(drugs.keys()), y=ic50_values,\n    marker_color=list(colors.values()),\n    showlegend=False,\n    hovertemplate=\"%{x}\u003Cbr>IC50: %{y:.2f} µM\u003Cextra>\u003C\u002Fextra>\",\n), row=1, col=2)\n\nfig.update_xaxes(type=\"log\", title_text=\"Dose (µM)\", row=1, col=1)\nfig.update_yaxes(title_text=\"Viability (%)\", row=1, col=1)\nfig.update_xaxes(title_text=\"Drug\", row=1, col=2)\nfig.update_yaxes(title_text=\"IC50 (µM)\", row=1, col=2)\nfig.update_layout(title=\"Dose-Response Dashboard\", height=450, width=850)\nfig.show()\nprint(f\"Subplots: {len(fig.data)} traces across 2 panels\")\n```\n\n```python\n# Export to HTML (interactive) and PNG (static)\n# Requires: pip install kaleido\n\nfig.write_html(\"dose_response_dashboard.html\")\nprint(\"Saved: dose_response_dashboard.html (interactive, shareable)\")\n\nfig.write_image(\"dose_response_dashboard.png\", width=1200, height=600, scale=2)\nprint(\"Saved: dose_response_dashboard.png (300 DPI equivalent with scale=2)\")\n\nfig.write_image(\"dose_response_dashboard.svg\")\nprint(\"Saved: dose_response_dashboard.svg (vector, editable in Inkscape\u002FIllustrator)\")\n```\n\n## Common Workflows\n\n### Workflow 1: Interactive Volcano Plot with Gene Annotations\n\n**Goal**: Build a fully annotated volcano plot from DESeq2 results, with gene-name hover tooltips, threshold lines, and highlighted hit labels for sharing as HTML.\n\n```python\nimport plotly.graph_objects as go\nimport pandas as pd\nimport numpy as np\n\n# Simulate DESeq2 output (replace with pd.read_csv(\"deseq2_results.csv\"))\nnp.random.seed(42)\nn = 3000\ndf = pd.DataFrame({\n    \"gene\": [f\"GENE_{i:04d}\" for i in range(n)],\n    \"log2FC\": np.random.normal(0, 1.0, n),\n    \"padj\": np.clip(np.random.exponential(0.1, n), 1e-20, 1.0),\n    \"baseMean\": np.random.lognormal(5, 1.5, n),\n})\n# Inject some hits\ndf.loc[:20, \"log2FC\"] = np.random.uniform(2.5, 5, 21)\ndf.loc[:20, \"padj\"] = np.random.uniform(1e-15, 1e-5, 21)\ndf.loc[21:35, \"log2FC\"] = np.random.uniform(-4, -2, 15)\ndf.loc[21:35, \"padj\"] = np.random.uniform(1e-12, 1e-4, 15)\n\ndf[\"neg_log10_padj\"] = -np.log10(df[\"padj\"].clip(1e-300))\n\n# Classify\nFC_THRESH, P_THRESH = 1.5, 2.0   # |log2FC| > 1.5, -log10(padj) > 2\ndf[\"category\"] = \"NS\"\ndf.loc[(df[\"log2FC\"] > FC_THRESH) & (df[\"neg_log10_padj\"] > P_THRESH), \"category\"] = \"Up\"\ndf.loc[(df[\"log2FC\"] \u003C -FC_THRESH) & (df[\"neg_log10_padj\"] > P_THRESH), \"category\"] = \"Down\"\n\ncolor_map = {\"NS\": \"lightgray\", \"Up\": \"crimson\", \"Down\": \"steelblue\"}\nsize_map = {\"NS\": 4, \"Up\": 7, \"Down\": 7}\nopacity_map = {\"NS\": 0.4, \"Up\": 0.85, \"Down\": 0.85}\n\nfig = go.Figure()\n\nfor cat in [\"NS\", \"Up\", \"Down\"]:\n    sub = df[df[\"category\"] == cat]\n    fig.add_trace(go.Scatter(\n        x=sub[\"log2FC\"], y=sub[\"neg_log10_padj\"],\n        mode=\"markers\",\n        name=f\"{cat} (n={len(sub)})\",\n        marker=dict(\n            color=color_map[cat],\n            size=size_map[cat],\n            opacity=opacity_map[cat],\n        ),\n        customdata=sub[[\"gene\", \"padj\", \"baseMean\"]].values,\n        hovertemplate=(\n            \"\u003Cb>%{customdata[0]}\u003C\u002Fb>\u003Cbr>\"\n            \"log2FC: %{x:.3f}\u003Cbr>\"\n            \"padj: %{customdata[1]:.2e}\u003Cbr>\"\n            \"baseMean: %{customdata[2]:.1f}\u003Cextra>\u003C\u002Fextra>\"\n        ),\n    ))\n\n# Threshold lines\nfig.add_hline(y=P_THRESH, line_dash=\"dot\", line_color=\"black\", line_width=1.2,\n              annotation_text=f\"padj=0.01\", annotation_position=\"right\")\nfig.add_vline(x=FC_THRESH, line_dash=\"dot\", line_color=\"black\", line_width=1.2)\nfig.add_vline(x=-FC_THRESH, line_dash=\"dot\", line_color=\"black\", line_width=1.2)\n\n# Label top 5 upregulated hits by significance\ntop_up = df[df[\"category\"] == \"Up\"].nlargest(5, \"neg_log10_padj\")\nfor _, row in top_up.iterrows():\n    fig.add_annotation(\n        x=row[\"log2FC\"], y=row[\"neg_log10_padj\"],\n        text=row[\"gene\"], showarrow=True,\n        arrowhead=2, arrowsize=1, arrowcolor=\"crimson\",\n        font=dict(size=9, color=\"crimson\"),\n        xshift=8, yshift=5,\n    )\n\nfig.update_layout(\n    title=\"Volcano Plot — Treatment vs Control (DESeq2)\",\n    xaxis_title=\"log2 Fold Change\",\n    yaxis_title=\"-log10(adjusted p-value)\",\n    legend=dict(x=0.01, y=0.99, bordercolor=\"lightgray\", borderwidth=1),\n    width=800, height=560,\n    plot_bgcolor=\"white\",\n)\nfig.update_xaxes(showgrid=True, gridcolor=\"lightgray\", zeroline=True, zerolinecolor=\"darkgray\")\nfig.update_yaxes(showgrid=True, gridcolor=\"lightgray\")\n\nfig.write_html(\"volcano_interactive.html\")\nprint(f\"Up: {(df.category=='Up').sum()}, Down: {(df.category=='Down').sum()}\")\nprint(\"Saved: volcano_interactive.html\")\n```\n\n### Workflow 2: Multi-Panel Dose-Response Dashboard with make_subplots\n\n**Goal**: Display dose-response curves for multiple drugs across cell lines in a grid layout with a shared color scale and consistent formatting.\n\n```python\nfrom plotly.subplots import make_subplots\nimport plotly.graph_objects as go\nimport numpy as np\nimport pandas as pd\n\n# Simulated IC50 data for 3 drugs × 3 cell lines\nnp.random.seed(10)\ndrugs = [\"DrugA\", \"DrugB\", \"DrugC\"]\ncell_lines = [\"HCT116\", \"MCF7\", \"A549\"]\ndoses = np.logspace(-2, 2, 7)  # 0.01 to 100 µM\n\ndef hill(x, ec50, hill_n, top=100, bottom=0):\n    return bottom + (top - bottom) \u002F (1 + (ec50 \u002F x) ** hill_n)\n\nec50_table = {\n    (\"DrugA\", \"HCT116\"): 0.5, (\"DrugA\", \"MCF7\"): 2.0, (\"DrugA\", \"A549\"): 8.0,\n    (\"DrugB\", \"HCT116\"): 5.0, (\"DrugB\", \"MCF7\"): 0.8, (\"DrugB\", \"A549\"): 15.0,\n    (\"DrugC\", \"HCT116\"): 12.0, (\"DrugC\", \"MCF7\"): 6.0, (\"DrugC\", \"A549\"): 1.2,\n}\n\npalette = px_colors = [\"#EF553B\", \"#636EFA\", \"#00CC96\", \"#AB63FA\", \"#FFA15A\",\n                        \"#19D3F3\", \"#FF6692\", \"#B6E880\", \"#FF97FF\"]\n\nfig = make_subplots(\n    rows=len(drugs), cols=len(cell_lines),\n    subplot_titles=[f\"{d} \u002F {c}\" for d in drugs for c in cell_lines],\n    shared_xaxes=True, shared_yaxes=True,\n    vertical_spacing=0.08, horizontal_spacing=0.04,\n)\n\nfor r, drug in enumerate(drugs, start=1):\n    for c, cell_line in enumerate(cell_lines, start=1):\n        ec50 = ec50_table[(drug, cell_line)]\n        x_fit = np.logspace(-2, 2, 200)\n        y_fit = hill(x_fit, ec50, hill_n=1.5)\n\n        # Noisy replicate data\n        y_data = np.array([hill(d, ec50, 1.5) + np.random.normal(0, 5) for d in doses])\n\n        color = palette[(r - 1) * len(cell_lines) + (c - 1)]\n        show_legend = (c == 1 and r == 1)\n\n        fig.add_trace(go.Scatter(\n            x=x_fit, y=y_fit, mode=\"lines\",\n            line=dict(color=color, width=2),\n            name=f\"{drug}\u002F{cell_line}\",\n            showlegend=False,\n            hovertemplate=f\"{drug} in {cell_line}\u003Cbr>Dose: %{{x:.2f}} µM\u003Cbr>Viability: %{{y:.1f}}%\u003Cextra>\u003C\u002Fextra>\",\n        ), row=r, col=c)\n\n        fig.add_trace(go.Scatter(\n            x=doses, y=np.clip(y_data, 0, 110), mode=\"markers\",\n            marker=dict(color=color, size=7, opacity=0.8),\n            showlegend=False,\n            hovertemplate=f\"Measured\u003Cbr>Dose: %{{x:.2f}} µM\u003Cbr>Viability: %{{y:.1f}}%\u003Cextra>\u003C\u002Fextra>\",\n        ), row=r, col=c)\n\n        # IC50 annotation\n        fig.add_annotation(\n            x=np.log10(ec50), y=50,\n            text=f\"IC50={ec50:.1f}µM\",\n            font=dict(size=8), showarrow=False,\n            xref=f\"x{(r-1)*len(cell_lines)+c if (r-1)*len(cell_lines)+c > 1 else ''}\",\n            yref=f\"y{(r-1)*len(cell_lines)+c if (r-1)*len(cell_lines)+c > 1 else ''}\",\n        )\n\n# Apply log scale to all x-axes\nfor i in range(1, len(drugs) * len(cell_lines) + 1):\n    axis_key = f\"xaxis{i if i > 1 else ''}\"\n    fig.layout[axis_key].update(type=\"log\", title_text=\"Dose (µM)\" if i > 6 else \"\")\n\nfor i in range(1, len(drugs) * len(cell_lines) + 1):\n    axis_key = f\"yaxis{i if i > 1 else ''}\"\n    fig.layout[axis_key].update(range=[-5, 115],\n                                 title_text=\"Viability (%)\" if i in [1, 4, 7] else \"\")\n\nfig.update_layout(\n    title=\"Dose-Response Dashboard — 3 Drugs × 3 Cell Lines\",\n    height=700, width=900,\n)\nfig.write_html(\"dose_response_dashboard.html\")\nprint(\"Saved: dose_response_dashboard.html\")\nprint(f\"Grid: {len(drugs)} drugs × {len(cell_lines)} cell lines = {len(drugs)*len(cell_lines)} panels\")\n```\n\n## Key Parameters\n\n| Parameter | Module \u002F Function | Default | Range \u002F Options | Effect |\n|-----------|------------------|---------|-----------------|--------|\n| `color` | `px.*` | `None` | Column name | Maps a DataFrame column to trace color; auto-assigns palette |\n| `hover_data` | `px.*` | `{}` | Dict or list of column names | Extra columns shown in hover tooltip |\n| `log_x` \u002F `log_y` | `px.*` | `False` | `True`, `False` | Apply log10 scale to x or y axis |\n| `facet_col` \u002F `facet_row` | `px.*` | `None` | Column name | Split into subplot grid by a categorical variable |\n| `color_continuous_scale` | `px.imshow`, `px.scatter` | `\"plasma\"` | `\"RdBu_r\"`, `\"Viridis\"`, `\"Hot\"`, etc. | Colormap for continuous color mapping |\n| `color_continuous_midpoint` | `px.imshow` | `None` | Any numeric | Centers the diverging colormap at this value (use `0` for z-scores) |\n| `barmode` | `px.histogram`, `px.bar` | `\"relative\"` | `\"relative\"`, `\"overlay\"`, `\"group\"` | How multiple bar traces are displayed |\n| `opacity` | `go.Scatter`, `px.*` | `1.0` | `0.0`–`1.0` | Point\u002Fbar transparency |\n| `size` \u002F `sizeref` | `go.Scatter` | `6` \u002F auto | Positive numeric | Marker size; `sizeref` normalizes sizes across traces |\n| `line_dash` | `fig.add_hline`, `go.Scatter` | `\"solid\"` | `\"solid\"`, `\"dash\"`, `\"dot\"`, `\"dashdot\"` | Line style for reference lines and traces |\n| `shared_xaxes` \u002F `shared_yaxes` | `make_subplots` | `False` | `True`, `False`, `\"rows\"`, `\"cols\"` | Link axes across subplot panels |\n| `scale` | `fig.write_image` | `1` | `1`–`4` | Resolution multiplier for PNG export (use `2` for ~150 DPI) |\n\n## Best Practices\n\n1. **Prefer `px` for DataFrame data, fall back to `go` for multi-trace composition.** Use `px.scatter()` and its siblings for 80% of plots. Switch to `go` when you need traces with different types in the same figure (e.g., scatter + filled area) or need fine-grained per-trace control.\n\n   ```python\n   # Correct: px for simple grouped plots\n   fig = px.box(df, x=\"treatment\", y=\"expression\", color=\"genotype\")\n\n   # Correct: go when px cannot express the structure\n   fig = go.Figure()\n   fig.add_trace(go.Scatter(x=x, y=y_upper, fill=\"tonexty\", ...))\n   fig.add_trace(go.Scatter(x=x, y=y_lower, ...))\n   ```\n\n2. **Always include `hovertemplate` for scientific figures.** The default tooltip shows raw coordinates without units or gene names. A custom template with `customdata` provides full biological context.\n\n   ```python\n   fig.add_trace(go.Scatter(\n       customdata=df[[\"gene\", \"padj\"]].values,\n       hovertemplate=\"\u003Cb>%{customdata[0]}\u003C\u002Fb>\u003Cbr>padj: %{customdata[1]:.2e}\u003Cextra>\u003C\u002Fextra>\",\n   ))\n   ```\n\n3. **Export HTML for sharing, PNG\u002FSVG for journals.** `fig.write_html()` produces a self-contained file with no external dependencies. Use `scale=2` or higher with `write_image()` to achieve sufficient resolution for print.\n\n4. **Don't use `fig.show()` in batch scripts.** In non-interactive contexts (CI, HPC, cron jobs), `fig.show()` may open a browser window or fail. Use `write_html()` or `write_image()` exclusively.\n\n5. **Use `color_continuous_midpoint=0` for diverging palettes on z-score data.** Without it, the midpoint color defaults to the data midpoint, not zero, misrepresenting symmetric fold changes or correlations.\n\n   ```python\n   fig = px.imshow(corr_matrix, color_continuous_scale=\"RdBu_r\",\n                   color_continuous_midpoint=0, zmin=-1, zmax=1)\n   ```\n\n6. **Set `plot_bgcolor=\"white\"` for publication figures.** Plotly defaults to a light-gray grid background. White background with subtle gridlines is cleaner for most scientific contexts.\n\n   ```python\n   fig.update_layout(plot_bgcolor=\"white\")\n   fig.update_xaxes(showgrid=True, gridcolor=\"lightgray\")\n   fig.update_yaxes(showgrid=True, gridcolor=\"lightgray\")\n   ```\n\n## Common Recipes\n\n### Recipe: Dropdown Menu to Toggle Between Conditions\n\nWhen to use: Overlay multiple conditions in one figure with a dropdown button to show\u002Fhide individual traces cleanly.\n\n```python\nimport plotly.graph_objects as go\nimport numpy as np\n\nconditions = [\"Untreated\", \"DrugA\", \"DrugB\"]\ncolors = [\"gray\", \"crimson\", \"steelblue\"]\nnp.random.seed(1)\nx = np.linspace(0, 24, 49)\n\nfig = go.Figure()\nfor i, (cond, color) in enumerate(zip(conditions, colors)):\n    y = np.sin(x \u002F 4 + i * 0.5) * (1 - i * 0.2) + np.random.normal(0, 0.05, len(x))\n    fig.add_trace(go.Scatter(\n        x=x, y=y, mode=\"lines+markers\",\n        name=cond, line=dict(color=color, width=2),\n        visible=(i == 0),  # only first trace visible initially\n    ))\n\n# One button per condition (shows only that trace)\nbuttons = []\nfor i, cond in enumerate(conditions):\n    visibility = [j == i for j in range(len(conditions))]\n    buttons.append(dict(label=cond, method=\"update\",\n                        args=[{\"visible\": visibility}, {\"title\": f\"Gene Expression — {cond}\"}]))\n\n# \"Show All\" button\nbuttons.append(dict(label=\"Show All\", method=\"update\",\n                    args=[{\"visible\": [True] * len(conditions)}, {\"title\": \"Gene Expression — All Conditions\"}]))\n\nfig.update_layout(\n    updatemenus=[dict(type=\"dropdown\", x=0.01, y=1.15, showactive=True, buttons=buttons)],\n    title=\"Gene Expression — Untreated\",\n    xaxis_title=\"Time (h)\", yaxis_title=\"Relative Expression\",\n)\nfig.show()\n```\n\n### Recipe: Annotating Specific Hits with Arrows\n\nWhen to use: Label outliers, drug hits, or significant genes directly on the figure without cluttering non-annotated points.\n\n```python\nimport plotly.graph_objects as go\nimport numpy as np\nimport pandas as pd\n\nnp.random.seed(33)\ndf = pd.DataFrame({\n    \"x\": np.random.normal(0, 1.5, 300),\n    \"y\": np.random.normal(0, 1.5, 300),\n    \"gene\": [f\"G{i:03d}\" for i in range(300)],\n})\n# Inject top hits\nhits = pd.DataFrame({\n    \"x\": [3.2, -2.8, 2.5, -3.5],\n    \"y\": [4.1, 3.8, -3.2, -2.9],\n    \"gene\": [\"BRCA1\", \"TP53\", \"EGFR\", \"KRAS\"],\n})\n\nfig = go.Figure()\nfig.add_trace(go.Scatter(\n    x=df[\"x\"], y=df[\"y\"], mode=\"markers\",\n    marker=dict(color=\"lightgray\", size=5, opacity=0.6),\n    text=df[\"gene\"],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb>\u003Cbr>x: %{x:.2f}, y: %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n    name=\"Background\",\n))\nfig.add_trace(go.Scatter(\n    x=hits[\"x\"], y=hits[\"y\"], mode=\"markers\",\n    marker=dict(color=\"crimson\", size=10, symbol=\"diamond\"),\n    text=hits[\"gene\"],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb> [HIT]\u003Cbr>x: %{x:.2f}, y: %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n    name=\"Hits\",\n))\n\nfor _, row in hits.iterrows():\n    fig.add_annotation(\n        x=row[\"x\"], y=row[\"y\"],\n        text=f\"\u003Cb>{row['gene']}\u003C\u002Fb>\",\n        showarrow=True, arrowhead=2, arrowwidth=1.5,\n        arrowcolor=\"crimson\", font=dict(size=11, color=\"crimson\"),\n        ax=25, ay=-30,  # arrow offset in pixels\n        bgcolor=\"rgba(255,255,255,0.7)\", bordercolor=\"crimson\", borderwidth=1,\n    )\n\nfig.update_layout(\n    title=\"Hit Identification with Arrow Annotations\",\n    xaxis_title=\"Score A\", yaxis_title=\"Score B\",\n    plot_bgcolor=\"white\",\n)\nfig.show()\n```\n\n## Troubleshooting\n\n| Problem | Cause | Solution |\n|---------|-------|----------|\n| `ValueError: kaleido is required for static image export` | `kaleido` not installed | `pip install kaleido`; verify with `import kaleido` |\n| Blank figure in Jupyter Notebook | Renderer not configured | Run `import plotly.io as pio; pio.renderers.default = \"notebook\"` or upgrade JupyterLab to ≥3 |\n| `fig.show()` opens blank browser tab | No data in figure or offline renderer issue | Check `len(fig.data) > 0`; use `pio.renderers.default = \"browser\"` |\n| Hover tooltips show wrong values | `customdata` index mismatch in `hovertemplate` | Verify `customdata` column order matches `%{customdata[N]}` indices in template |\n| Colors not assigned consistently across traces | `px` re-orders palette when category counts differ | Use `color_discrete_map={\"Cat1\": \"#color1\", ...}` to pin colors explicitly |\n| `write_image` produces blurry PNG | Default `scale=1` is too low for print | Use `fig.write_image(\"fig.png\", scale=2)` for 150 DPI or `scale=4` for 300 DPI |\n| Subplots x-axes not all log-scaled after `shared_xaxes=True` | Shared axis only synchronizes range, not type | Iterate over all `xaxis` keys in `fig.layout` and set `type=\"log\"` explicitly |\n| Large datasets slow to render in browser | Too many individual points in a single scatter trace | Downsample background noise points; keep labeled hits as a separate, smaller trace |\n| `fig.update_layout` does not apply to subplot axes | Multi-panel figures use indexed axes (`xaxis2`, `xaxis3`) | Use `fig.update_xaxes()` (applies to all) or target `fig.layout[\"xaxis2\"]` explicitly |\n\n## Related Skills\n\n- **seaborn-statistical-plots** — use for statistical aggregation (confidence intervals, regression), publication-quality static figures with minimal code, and when matplotlib-level output is required\n- **matplotlib-scientific-plotting** — use for full control over every figure element, custom layouts, embedded text rendering, and journal-specification figure preparation\n- **pydeseq2-differential-expression** — volcano plot outputs from DESeq2 results are a primary input for the interactive volcano workflow above\n- **scanpy-scrna-seq** — Scanpy's UMAP embeddings can be visualized interactively in 3D with `px.scatter_3d`\n\n## References\n\n- [Plotly Python Documentation](https:\u002F\u002Fplotly.com\u002Fpython\u002F) — official API reference, examples gallery, and getting-started guides\n- [Plotly Express API Reference](https:\u002F\u002Fplotly.com\u002Fpython-api-reference\u002Fplotly.express.html) — complete `px` function signatures and parameters\n- [Plotly Graph Objects Reference](https:\u002F\u002Fplotly.com\u002Fpython\u002Fgraph-objects\u002F) — full `go` trace and layout attribute reference\n- [Plotly GitHub Repository](https:\u002F\u002Fgithub.com\u002Fplotly\u002Fplotly.py) — source code, issue tracker, changelog\n- [Kaleido Static Image Export](https:\u002F\u002Fgithub.com\u002Fplotly\u002FKaleido) — dependency for `write_image()` PNG\u002FSVG\u002FPDF export\n","# Plotly Interactive Plots\n\n## Overview\n\nPlotly is a Python library for producing interactive, web-ready figures backed by HTML and JavaScript. It exposes two complementary APIs: `plotly.express` (px) provides a high-level, DataFrame-oriented interface for generating common chart types in one line, while `plotly.graph_objects` (go) offers fine-grained control over every trace, axis, and layout property. Figures are fully interactive by default — supporting hover tooltips, zoom, pan, and click events — and can be embedded in web pages, Jupyter notebooks, or built into web applications using the Dash framework.\n\n## When to Use\n\n- You need hover tooltips that display gene names, p-values, or sample metadata without cluttering the static figure.\n- You are building a multi-panel interactive dashboard for dose-response curves, patient cohorts, or multi-condition comparisons.\n- You want to share figures as self-contained HTML files that non-programmers can explore in a browser.\n- You need 3D scatter or surface plots for structural biology, conformational landscapes, or PCA of high-dimensional data.\n- You are creating heatmaps of gene expression or correlation matrices where users need to zoom into specific gene clusters.\n- You require animation frames to show time-series or treatment-response trajectories.\n- Use `seaborn` instead when you need automatic statistical aggregation (confidence intervals, regression fits) with minimal code.\n- Use `matplotlib` when you need fine-grained control over every axis element for print-ready publication figures at exact journal specifications.\n\n## Prerequisites\n\n- **Python packages**: `plotly`, `kaleido` (static image export), `pandas`, `numpy`\n- **Data requirements**: pandas DataFrames or NumPy arrays; long-form (tidy) data works best with `px`\n- **Environment**: Jupyter Lab\u002FNotebook (inline rendering), or save as HTML for browser display\n\n```bash\npip install plotly kaleido pandas numpy\n```\n\nFor Jupyter Lab inline rendering (if not automatic):\n\n```bash\npip install \"jupyterlab>=3\" ipywidgets\n```\n\n## Quick Start\n\n```python\nimport plotly.express as px\nimport pandas as pd\n\n# Gene expression scatter with hover info\ndf = pd.DataFrame({\n    \"log2FC\": [-3.1, 0.2, 1.8, 2.5, -0.5, 4.1],\n    \"neg_log10_padj\": [8.2, 0.4, 2.1, 6.8, 0.1, 9.3],\n    \"gene\": [\"BRCA1\", \"MYC\", \"TP53\", \"EGFR\", \"CDKN1A\", \"KRAS\"],\n    \"significance\": [\"sig\", \"ns\", \"ns\", \"sig\", \"ns\", \"sig\"],\n})\n\nfig = px.scatter(\n    df, x=\"log2FC\", y=\"neg_log10_padj\",\n    color=\"significance\", hover_name=\"gene\",\n    title=\"Volcano Plot — Treatment vs Control\",\n)\nfig.show()\n```\n\n## Core API\n\n### Module 1: px Scatter and Line — Relational Plots\n\n`px.scatter()` and `px.line()` map DataFrame columns to visual encodings (color, symbol, size) and automatically populate hover tooltips from `hover_data`.\n\n```python\nimport plotly.express as px\nimport pandas as pd\nimport numpy as np\n\n# Dose-response scatter: color by drug, symbol by cell line\nnp.random.seed(42)\ndf = pd.DataFrame({\n    \"dose_uM\": np.tile([0.01, 0.1, 1, 10, 100], 4),\n    \"viability\": np.clip(np.random.normal(\n        [100, 90, 70, 40, 10] * 4, 5), 0, 110),\n    \"drug\": [\"DrugA\"] * 5 + [\"DrugA\"] * 5 + [\"DrugB\"] * 5 + [\"DrugB\"] * 5,\n    \"cell_line\": [\"HCT116\"] * 10 + [\"MCF7\"] * 10,\n    \"replicate\": np.tile([1, 2, 3, 4, 5], 4),\n})\n\nfig = px.scatter(\n    df, x=\"dose_uM\", y=\"viability\",\n    color=\"drug\", symbol=\"cell_line\",\n    log_x=True,\n    hover_data={\"replicate\": True, \"dose_uM\": \":.2f\"},\n    labels={\"viability\": \"Cell Viability (%)\", \"dose_uM\": \"Dose (µM)\"},\n    title=\"Dose-Response by Drug and Cell Line\",\n)\nfig.show()\nprint(f\"Figure has {len(fig.data)} traces\")\n```\n\n```python\n# Time-course gene expression line plot\ntime_df = pd.DataFrame({\n    \"hour\": list(range(0, 25, 4)) * 3,\n    \"expression\": [1.0, 1.8, 3.2, 4.5, 3.8, 2.1, 1.2,\n                   1.0, 2.5, 5.1, 6.8, 5.5, 3.2, 1.8,\n                   1.0, 1.1, 1.0, 1.2, 1.1, 1.0, 0.9],\n    \"gene\": [\"MYC\"] * 7 + [\"EGFR\"] * 7 + [\"GAPDH\"] * 7,\n})\n\nfig = px.line(\n    time_df, x=\"hour\", y=\"expression\",\n    color=\"gene\", markers=True,\n    labels={\"expression\": \"Relative Expression (log2)\", \"hour\": \"Time (h)\"},\n    title=\"Time-Course Gene Expression\",\n)\nfig.update_traces(line=dict(width=2.5), marker=dict(size=8))\nfig.show()\n```\n\n### Module 2: px Statistical Plots — Distributions and Categories\n\n`px.box()`, `px.violin()`, `px.histogram()`, and `px.strip()` produce publication-ready distribution summaries with built-in grouping.\n\n```python\nimport plotly.express as px\nimport pandas as pd\nimport numpy as np\n\n# Violin + strip overlay: expression by cell type\nnp.random.seed(7)\nn = 60\ncell_data = pd.DataFrame({\n    \"expression\": np.concatenate([\n        np.random.normal(4.2, 0.8, n),\n        np.random.normal(6.5, 1.2, n),\n        np.random.normal(2.8, 0.6, n),\n    ]),\n    \"cell_type\": [\"T cell\"] * n + [\"B cell\"] * n + [\"NK cell\"] * n,\n    \"patient_id\": np.tile([f\"P{i:02d}\" for i in range(1, 11)], 18),\n})\n\nfig = px.violin(\n    cell_data, x=\"cell_type\", y=\"expression\",\n    color=\"cell_type\", box=True, points=\"all\",\n    hover_data=[\"patient_id\"],\n    labels={\"expression\": \"CD3E Expression (log2 CPM)\"},\n    title=\"CD3E Expression Across Cell Types\",\n)\nfig.update_traces(jitter=0.3, pointpos=-1.5)\nfig.show()\nprint(f\"Cells per type: {cell_data.groupby('cell_type').size().to_dict()}\")\n```\n\n```python\n# Histogram with rug: distribution of fold changes\nfc_df = pd.DataFrame({\n    \"log2FC\": np.concatenate([\n        np.random.normal(0.1, 0.8, 500),   # not DE genes\n        np.random.normal(2.5, 0.4, 50),    # upregulated\n        np.random.normal(-2.3, 0.4, 40),   # downregulated\n    ]),\n    \"category\": [\"background\"] * 500 + [\"up\"] * 50 + [\"down\"] * 40,\n})\n\nfig = px.histogram(\n    fc_df, x=\"log2FC\", color=\"category\",\n    nbins=60, barmode=\"overlay\", opacity=0.7,\n    marginal=\"rug\",\n    labels={\"log2FC\": \"log2 Fold Change\", \"count\": \"Gene Count\"},\n    title=\"Distribution of Fold Changes (DESeq2 Results)\",\n    color_discrete_map={\"background\": \"gray\", \"up\": \"crimson\", \"down\": \"steelblue\"},\n)\nfig.show()\n```\n\n### Module 3: px Heatmap and Matrix — Gene Expression and Correlations\n\n`px.imshow()` renders 2D arrays or DataFrames as color-encoded matrices, ideal for expression heatmaps and correlation matrices.\n\n```python\nimport plotly.express as px\nimport pandas as pd\nimport numpy as np\n\n# Gene expression heatmap (genes × samples)\nnp.random.seed(12)\ngenes = [f\"Gene_{g}\" for g in [\"BRCA1\", \"TP53\", \"EGFR\", \"MYC\", \"KRAS\",\n                                 \"CDKN1A\", \"RB1\", \"PTEN\", \"VHL\", \"APC\"]]\nsamples = [f\"S{i:02d}\" for i in range(1, 9)]\n\nexpr_matrix = pd.DataFrame(\n    np.random.normal(0, 1.5, (10, 8)) +\n    np.array([2, -1, 3, -2, 1, -3, 0, 2, -1, 3]).reshape(-1, 1),\n    index=genes, columns=samples,\n)\n\nfig = px.imshow(\n    expr_matrix,\n    color_continuous_scale=\"RdBu_r\",\n    color_continuous_midpoint=0,\n    aspect=\"auto\",\n    labels={\"color\": \"log2 Expression (z-score)\"},\n    title=\"Gene Expression Heatmap\",\n)\nfig.update_xaxes(side=\"top\")\nfig.update_layout(width=600, height=500)\nfig.show()\nprint(f\"Heatmap shape: {expr_matrix.shape} (genes × samples)\")\n```\n\n```python\n# Correlation matrix heatmap\nfrom itertools import combinations\n\nmarkers = [\"IL6\", \"TNF\", \"CXCL10\", \"IFNg\", \"IL10\", \"IL1B\", \"CCL2\", \"IL17A\"]\nnp.random.seed(3)\nraw = np.random.multivariate_normal(\n    mean=np.zeros(8),\n    cov=np.eye(8) * 0.3 + 0.7,\n    size=80,\n)\ncorr_df = pd.DataFrame(raw, columns=markers).corr()\n\nfig = px.imshow(\n    corr_df,\n    color_continuous_scale=\"RdBu_r\",\n    color_continuous_midpoint=0,\n    zmin=-1, zmax=1,\n    text_auto=\".2f\",\n    title=\"Cytokine Correlation Matrix (n=80 patients)\",\n)\nfig.update_traces(textfont_size=10)\nfig.show()\n```\n\n### Module 4: go Graph Objects — Full Trace Control\n\n`plotly.graph_objects` provides fine-grained access to every trace property: marker symbols, error bars, fill areas, and multi-trace layouts. Essential when `px` lacks the flexibility you need.\n\n```python\nimport plotly.graph_objects as go\nimport numpy as np\n\n# Volcano plot built from scratch with go.Scatter\nnp.random.seed(99)\nn_genes = 5000\nlog2fc = np.random.normal(0, 1.2, n_genes)\npval = np.random.uniform(0, 1, n_genes) ** 2  # skew toward low p-values\nneg_log10_p = -np.log10(pval + 1e-300)\ngene_names = [f\"Gene_{i:04d}\" for i in range(n_genes)]\n\n# Classify genes\nsig_mask = (np.abs(log2fc) > 1.5) & (neg_log10_p > 3)\nup_mask = sig_mask & (log2fc > 0)\ndown_mask = sig_mask & (log2fc \u003C 0)\nns_mask = ~sig_mask\n\nfig = go.Figure()\n\n# Non-significant background\nfig.add_trace(go.Scatter(\n    x=log2fc[ns_mask], y=neg_log10_p[ns_mask],\n    mode=\"markers\",\n    name=\"Not significant\",\n    marker=dict(color=\"lightgray\", size=4, opacity=0.5),\n    text=[gene_names[i] for i in np.where(ns_mask)[0]],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb>\u003Cbr>log2FC: %{x:.2f}\u003Cbr>-log10(p): %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n))\n\n# Upregulated\nfig.add_trace(go.Scatter(\n    x=log2fc[up_mask], y=neg_log10_p[up_mask],\n    mode=\"markers\",\n    name=f\"Up ({up_mask.sum()} genes)\",\n    marker=dict(color=\"crimson\", size=7, opacity=0.8),\n    text=[gene_names[i] for i in np.where(up_mask)[0]],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb>\u003Cbr>log2FC: %{x:.2f}\u003Cbr>-log10(p): %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n))\n\n# Downregulated\nfig.add_trace(go.Scatter(\n    x=log2fc[down_mask], y=neg_log10_p[down_mask],\n    mode=\"markers\",\n    name=f\"Down ({down_mask.sum()} genes)\",\n    marker=dict(color=\"steelblue\", size=7, opacity=0.8),\n    text=[gene_names[i] for i in np.where(down_mask)[0]],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb>\u003Cbr>log2FC: %{x:.2f}\u003Cbr>-log10(p): %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n))\n\n# Threshold lines\nfig.add_hline(y=3, line_dash=\"dash\", line_color=\"black\", line_width=1)\nfig.add_vline(x=1.5, line_dash=\"dash\", line_color=\"black\", line_width=1)\nfig.add_vline(x=-1.5, line_dash=\"dash\", line_color=\"black\", line_width=1)\n\nfig.update_layout(\n    title=\"Volcano Plot (Treatment vs Control, n=5000 genes)\",\n    xaxis_title=\"log2 Fold Change\",\n    yaxis_title=\"-log10(adjusted p-value)\",\n    legend=dict(x=0.01, y=0.99),\n    width=750, height=550,\n)\nfig.show()\nprint(f\"Up: {up_mask.sum()}, Down: {down_mask.sum()}, NS: {ns_mask.sum()}\")\n```\n\n```python\n# Bar chart with error bars: mean ± SEM per treatment group\ngroups = [\"Vehicle\", \"DrugA 1µM\", \"DrugA 10µM\", \"DrugB 1µM\", \"DrugB 10µM\"]\nmeans = [100.0, 82.3, 54.7, 91.2, 68.5]\nsems = [3.2, 4.1, 3.8, 3.5, 4.7]\n\nfig = go.Figure(go.Bar(\n    x=groups, y=means,\n    error_y=dict(type=\"data\", array=sems, visible=True),\n    marker_color=[\"gray\", \"lightsalmon\", \"crimson\", \"lightblue\", \"steelblue\"],\n    hovertemplate=\"%{x}\u003Cbr>Mean: %{y:.1f}%\u003Cbr>SEM: ±%{error_y.array:.1f}%\u003Cextra>\u003C\u002Fextra>\",\n))\n\nfig.update_layout(\n    title=\"Cell Viability by Treatment (Mean ± SEM, n=6)\",\n    yaxis_title=\"Viability (%)\", yaxis_range=[0, 120],\n    xaxis_title=\"Treatment Group\",\n    showlegend=False,\n)\nfig.show()\n```\n\n### Module 5: 3D and Specialized Charts\n\nPlotly supports 3D scatter, surface plots, parallel coordinates, and treemaps — chart types unavailable in seaborn or standard matplotlib.\n\n```python\nimport plotly.express as px\nimport numpy as np\nimport pandas as pd\n\n# 3D PCA scatter: cell clusters in embedding space\nnp.random.seed(42)\nn_per_cluster = 80\ncluster_centers = {\"T cell\": [3, 2, 1], \"B cell\": [-3, 1, 2], \"Monocyte\": [0, -3, -1]}\n\nrecords = []\nfor ctype, center in cluster_centers.items():\n    coords = np.random.normal(center, 0.8, (n_per_cluster, 3))\n    for row in coords:\n        records.append({\n            \"PC1\": row[0], \"PC2\": row[1], \"PC3\": row[2],\n            \"cell_type\": ctype,\n            \"score\": np.random.uniform(0.5, 1.0),\n        })\n\npca_df = pd.DataFrame(records)\n\nfig = px.scatter_3d(\n    pca_df, x=\"PC1\", y=\"PC2\", z=\"PC3\",\n    color=\"cell_type\", size=\"score\", opacity=0.7,\n    hover_data={\"score\": \":.3f\"},\n    title=\"3D PCA — Single-Cell Transcriptomics\",\n)\nfig.update_traces(marker=dict(sizeref=0.04))\nfig.show()\nprint(f\"Total cells: {len(pca_df)}, clusters: {pca_df['cell_type'].nunique()}\")\n```\n\n```python\nimport plotly.graph_objects as go\nimport numpy as np\nimport pandas as pd\n\n# Parallel coordinates: multi-parameter drug screen\nnp.random.seed(5)\nn_compounds = 200\ndrug_df = pd.DataFrame({\n    \"MW\": np.random.normal(380, 60, n_compounds),\n    \"logP\": np.random.uniform(-1, 6, n_compounds),\n    \"HBA\": np.random.randint(2, 10, n_compounds),\n    \"HBD\": np.random.randint(0, 6, n_compounds),\n    \"IC50_nM\": np.random.lognormal(4, 1.5, n_compounds),\n    \"selectivity\": np.random.uniform(1, 100, n_compounds),\n})\n\nfig = px.parallel_coordinates(\n    drug_df,\n    color=\"IC50_nM\",\n    color_continuous_scale=\"RdYlGn_r\",\n    dimensions=[\"MW\", \"logP\", \"HBA\", \"HBD\", \"IC50_nM\", \"selectivity\"],\n    labels={\n        \"MW\": \"MW (Da)\", \"logP\": \"logP\",\n        \"HBA\": \"H-Bond Acceptors\", \"HBD\": \"H-Bond Donors\",\n        \"IC50_nM\": \"IC50 (nM)\", \"selectivity\": \"Selectivity Index\",\n    },\n    title=\"Drug Candidate Properties — Parallel Coordinates\",\n)\nfig.show()\n```\n\n### Module 6: Subplots and Export\n\n`make_subplots()` creates multi-panel layouts with shared axes, mixed chart types, and independent traces per panel. `fig.write_html()` exports interactive figures; `fig.write_image()` exports static files via kaleido.\n\n```python\nfrom plotly.subplots import make_subplots\nimport plotly.graph_objects as go\nimport numpy as np\n\n# Two-panel: raw data + summary statistics\nnp.random.seed(77)\ndoses = [0.01, 0.1, 1, 10, 100]\ndrugs = {\"DrugA\": {\"EC50\": 1.0, \"hill\": 1.5}, \"DrugB\": {\"EC50\": 8.0, \"hill\": 0.9}}\n\ndef hill_curve(dose, ec50, hill, top=100, bottom=0):\n    return bottom + (top - bottom) \u002F (1 + (ec50 \u002F dose) ** hill)\n\nfig = make_subplots(\n    rows=1, cols=2,\n    subplot_titles=[\"Dose-Response Curves\", \"IC50 Comparison\"],\n    shared_yaxes=False,\n)\n\ncolors = {\"DrugA\": \"crimson\", \"DrugB\": \"steelblue\"}\nic50_values = []\n\nfor drug, params in drugs.items():\n    # Smooth fit curve\n    x_fit = np.logspace(-2, 2, 200)\n    y_fit = hill_curve(x_fit, params[\"EC50\"], params[\"hill\"])\n    fig.add_trace(go.Scatter(\n        x=x_fit, y=y_fit, mode=\"lines\",\n        name=f\"{drug} fit\", line=dict(color=colors[drug], width=2.5),\n    ), row=1, col=1)\n\n    # Noisy data points\n    y_data = [hill_curve(d, params[\"EC50\"], params[\"hill\"]) +\n              np.random.normal(0, 4) for d in doses]\n    fig.add_trace(go.Scatter(\n        x=doses, y=y_data, mode=\"markers\",\n        name=f\"{drug} data\", marker=dict(color=colors[drug], size=9),\n        showlegend=False,\n    ), row=1, col=1)\n    ic50_values.append(params[\"EC50\"])\n\n# Bar chart of IC50 values\nfig.add_trace(go.Bar(\n    x=list(drugs.keys()), y=ic50_values,\n    marker_color=list(colors.values()),\n    showlegend=False,\n    hovertemplate=\"%{x}\u003Cbr>IC50: %{y:.2f} µM\u003Cextra>\u003C\u002Fextra>\",\n), row=1, col=2)\n\nfig.update_xaxes(type=\"log\", title_text=\"Dose (µM)\", row=1, col=1)\nfig.update_yaxes(title_text=\"Viability (%)\", row=1, col=1)\nfig.update_xaxes(title_text=\"Drug\", row=1, col=2)\nfig.update_yaxes(title_text=\"IC50 (µM)\", row=1, col=2)\nfig.update_layout(title=\"Dose-Response Dashboard\", height=450, width=850)\nfig.show()\nprint(f\"Subplots: {len(fig.data)} traces across 2 panels\")\n```\n\n```python\n# Export to HTML (interactive) and PNG (static)\n# Requires: pip install kaleido\n\nfig.write_html(\"dose_response_dashboard.html\")\nprint(\"Saved: dose_response_dashboard.html (interactive, shareable)\")\n\nfig.write_image(\"dose_response_dashboard.png\", width=1200, height=600, scale=2)\nprint(\"Saved: dose_response_dashboard.png (300 DPI equivalent with scale=2)\")\n\nfig.write_image(\"dose_response_dashboard.svg\")\nprint(\"Saved: dose_response_dashboard.svg (vector, editable in Inkscape\u002FIllustrator)\")\n```\n\n## Common Workflows\n\n### Workflow 1: Interactive Volcano Plot with Gene Annotations\n\n**Goal**: Build a fully annotated volcano plot from DESeq2 results, with gene-name hover tooltips, threshold lines, and highlighted hit labels for sharing as HTML.\n\n```python\nimport plotly.graph_objects as go\nimport pandas as pd\nimport numpy as np\n\n# Simulate DESeq2 output (replace with pd.read_csv(\"deseq2_results.csv\"))\nnp.random.seed(42)\nn = 3000\ndf = pd.DataFrame({\n    \"gene\": [f\"GENE_{i:04d}\" for i in range(n)],\n    \"log2FC\": np.random.normal(0, 1.0, n),\n    \"padj\": np.clip(np.random.exponential(0.1, n), 1e-20, 1.0),\n    \"baseMean\": np.random.lognormal(5, 1.5, n),\n})\n# Inject some hits\ndf.loc[:20, \"log2FC\"] = np.random.uniform(2.5, 5, 21)\ndf.loc[:20, \"padj\"] = np.random.uniform(1e-15, 1e-5, 21)\ndf.loc[21:35, \"log2FC\"] = np.random.uniform(-4, -2, 15)\ndf.loc[21:35, \"padj\"] = np.random.uniform(1e-12, 1e-4, 15)\n\ndf[\"neg_log10_padj\"] = -np.log10(df[\"padj\"].clip(1e-300))\n\n# Classify\nFC_THRESH, P_THRESH = 1.5, 2.0   # |log2FC| > 1.5, -log10(padj) > 2\ndf[\"category\"] = \"NS\"\ndf.loc[(df[\"log2FC\"] > FC_THRESH) & (df[\"neg_log10_padj\"] > P_THRESH), \"category\"] = \"Up\"\ndf.loc[(df[\"log2FC\"] \u003C -FC_THRESH) & (df[\"neg_log10_padj\"] > P_THRESH), \"category\"] = \"Down\"\n\ncolor_map = {\"NS\": \"lightgray\", \"Up\": \"crimson\", \"Down\": \"steelblue\"}\nsize_map = {\"NS\": 4, \"Up\": 7, \"Down\": 7}\nopacity_map = {\"NS\": 0.4, \"Up\": 0.85, \"Down\": 0.85}\n\nfig = go.Figure()\n\nfor cat in [\"NS\", \"Up\", \"Down\"]:\n    sub = df[df[\"category\"] == cat]\n    fig.add_trace(go.Scatter(\n        x=sub[\"log2FC\"], y=sub[\"neg_log10_padj\"],\n        mode=\"markers\",\n        name=f\"{cat} (n={len(sub)})\",\n        marker=dict(\n            color=color_map[cat],\n            size=size_map[cat],\n            opacity=opacity_map[cat],\n        ),\n        customdata=sub[[\"gene\", \"padj\", \"baseMean\"]].values,\n        hovertemplate=(\n            \"\u003Cb>%{customdata[0]}\u003C\u002Fb>\u003Cbr>\"\n            \"log2FC: %{x:.3f}\u003Cbr>\"\n            \"padj: %{customdata[1]:.2e}\u003Cbr>\"\n            \"baseMean: %{customdata[2]:.1f}\u003Cextra>\u003C\u002Fextra>\"\n        ),\n    ))\n\n# Threshold lines\nfig.add_hline(y=P_THRESH, line_dash=\"dot\", line_color=\"black\", line_width=1.2,\n              annotation_text=f\"padj=0.01\", annotation_position=\"right\")\nfig.add_vline(x=FC_THRESH, line_dash=\"dot\", line_color=\"black\", line_width=1.2)\nfig.add_vline(x=-FC_THRESH, line_dash=\"dot\", line_color=\"black\", line_width=1.2)\n\n# Label top 5 upregulated hits by significance\ntop_up = df[df[\"category\"] == \"Up\"].nlargest(5, \"neg_log10_padj\")\nfor _, row in top_up.iterrows():\n    fig.add_annotation(\n        x=row[\"log2FC\"], y=row[\"neg_log10_padj\"],\n        text=row[\"gene\"], showarrow=True,\n        arrowhead=2, arrowsize=1, arrowcolor=\"crimson\",\n        font=dict(size=9, color=\"crimson\"),\n        xshift=8, yshift=5,\n    )\n\nfig.update_layout(\n    title=\"Volcano Plot — Treatment vs Control (DESeq2)\",\n    xaxis_title=\"log2 Fold Change\",\n    yaxis_title=\"-log10(adjusted p-value)\",\n    legend=dict(x=0.01, y=0.99, bordercolor=\"lightgray\", borderwidth=1),\n    width=800, height=560,\n    plot_bgcolor=\"white\",\n)\nfig.update_xaxes(showgrid=True, gridcolor=\"lightgray\", zeroline=True, zerolinecolor=\"darkgray\")\nfig.update_yaxes(showgrid=True, gridcolor=\"lightgray\")\n\nfig.write_html(\"volcano_interactive.html\")\nprint(f\"Up: {(df.category=='Up').sum()}, Down: {(df.category=='Down').sum()}\")\nprint(\"Saved: volcano_interactive.html\")\n```\n\n### Workflow 2: Multi-Panel Dose-Response Dashboard with make_subplots\n\n**Goal**: Display dose-response curves for multiple drugs across cell lines in a grid layout with a shared color scale and consistent formatting.\n\n```python\nfrom plotly.subplots import make_subplots\nimport plotly.graph_objects as go\nimport numpy as np\nimport pandas as pd\n\n# Simulated IC50 data for 3 drugs × 3 cell lines\nnp.random.seed(10)\ndrugs = [\"DrugA\", \"DrugB\", \"DrugC\"]\ncell_lines = [\"HCT116\", \"MCF7\", \"A549\"]\ndoses = np.logspace(-2, 2, 7)  # 0.01 to 100 µM\n\ndef hill(x, ec50, hill_n, top=100, bottom=0):\n    return bottom + (top - bottom) \u002F (1 + (ec50 \u002F x) ** hill_n)\n\nec50_table = {\n    (\"DrugA\", \"HCT116\"): 0.5, (\"DrugA\", \"MCF7\"): 2.0, (\"DrugA\", \"A549\"): 8.0,\n    (\"DrugB\", \"HCT116\"): 5.0, (\"DrugB\", \"MCF7\"): 0.8, (\"DrugB\", \"A549\"): 15.0,\n    (\"DrugC\", \"HCT116\"): 12.0, (\"DrugC\", \"MCF7\"): 6.0, (\"DrugC\", \"A549\"): 1.2,\n}\n\npalette = px_colors = [\"#EF553B\", \"#636EFA\", \"#00CC96\", \"#AB63FA\", \"#FFA15A\",\n                        \"#19D3F3\", \"#FF6692\", \"#B6E880\", \"#FF97FF\"]\n\nfig = make_subplots(\n    rows=len(drugs), cols=len(cell_lines),\n    subplot_titles=[f\"{d} \u002F {c}\" for d in drugs for c in cell_lines],\n    shared_xaxes=True, shared_yaxes=True,\n    vertical_spacing=0.08, horizontal_spacing=0.04,\n)\n\nfor r, drug in enumerate(drugs, start=1):\n    for c, cell_line in enumerate(cell_lines, start=1):\n        ec50 = ec50_table[(drug, cell_line)]\n        x_fit = np.logspace(-2, 2, 200)\n        y_fit = hill(x_fit, ec50, hill_n=1.5)\n\n        # Noisy replicate data\n        y_data = np.array([hill(d, ec50, 1.5) + np.random.normal(0, 5) for d in doses])\n\n        color = palette[(r - 1) * len(cell_lines) + (c - 1)]\n        show_legend = (c == 1 and r == 1)\n\n        fig.add_trace(go.Scatter(\n            x=x_fit, y=y_fit, mode=\"lines\",\n            line=dict(color=color, width=2),\n            name=f\"{drug}\u002F{cell_line}\",\n            showlegend=False,\n            hovertemplate=f\"{drug} in {cell_line}\u003Cbr>Dose: %{{x:.2f}} µM\u003Cbr>Viability: %{{y:.1f}}%\u003Cextra>\u003C\u002Fextra>\",\n        ), row=r, col=c)\n\n        fig.add_trace(go.Scatter(\n            x=doses, y=np.clip(y_data, 0, 110), mode=\"markers\",\n            marker=dict(color=color, size=7, opacity=0.8),\n            showlegend=False,\n            hovertemplate=f\"Measured\u003Cbr>Dose: %{{x:.2f}} µM\u003Cbr>Viability: %{{y:.1f}}%\u003Cextra>\u003C\u002Fextra>\",\n        ), row=r, col=c)\n\n        # IC50 annotation\n        fig.add_annotation(\n            x=np.log10(ec50), y=50,\n            text=f\"IC50={ec50:.1f}µM\",\n            font=dict(size=8), showarrow=False,\n            xref=f\"x{(r-1)*len(cell_lines)+c if (r-1)*len(cell_lines)+c > 1 else ''}\",\n            yref=f\"y{(r-1)*len(cell_lines)+c if (r-1)*len(cell_lines)+c > 1 else ''}\",\n        )\n\n# Apply log scale to all x-axes\nfor i in range(1, len(drugs) * len(cell_lines) + 1):\n    axis_key = f\"xaxis{i if i > 1 else ''}\"\n    fig.layout[axis_key].update(type=\"log\", title_text=\"Dose (µM)\" if i > 6 else \"\")\n\nfor i in range(1, len(drugs) * len(cell_lines) + 1):\n    axis_key = f\"yaxis{i if i > 1 else ''}\"\n    fig.layout[axis_key].update(range=[-5, 115],\n                                 title_text=\"Viability (%)\" if i in [1, 4, 7] else \"\")\n\nfig.update_layout(\n    title=\"Dose-Response Dashboard — 3 Drugs × 3 Cell Lines\",\n    height=700, width=900,\n)\nfig.write_html(\"dose_response_dashboard.html\")\nprint(\"Saved: dose_response_dashboard.html\")\nprint(f\"Grid: {len(drugs)} drugs × {len(cell_lines)} cell lines = {len(drugs)*len(cell_lines)} panels\")\n```\n\n## Key Parameters\n\n| Parameter | Module \u002F Function | Default | Range \u002F Options | Effect |\n|-----------|------------------|---------|-----------------|--------|\n| `color` | `px.*` | `None` | Column name | Maps a DataFrame column to trace color; auto-assigns palette |\n| `hover_data` | `px.*` | `{}` | Dict or list of column names | Extra columns shown in hover tooltip |\n| `log_x` \u002F `log_y` | `px.*` | `False` | `True`, `False` | Apply log10 scale to x or y axis |\n| `facet_col` \u002F `facet_row` | `px.*` | `None` | Column name | Split into subplot grid by a categorical variable |\n| `color_continuous_scale` | `px.imshow`, `px.scatter` | `\"plasma\"` | `\"RdBu_r\"`, `\"Viridis\"`, `\"Hot\"`, etc. | Colormap for continuous color mapping |\n| `color_continuous_midpoint` | `px.imshow` | `None` | Any numeric | Centers the diverging colormap at this value (use `0` for z-scores) |\n| `barmode` | `px.histogram`, `px.bar` | `\"relative\"` | `\"relative\"`, `\"overlay\"`, `\"group\"` | How multiple bar traces are displayed |\n| `opacity` | `go.Scatter`, `px.*` | `1.0` | `0.0`–`1.0` | Point\u002Fbar transparency |\n| `size` \u002F `sizeref` | `go.Scatter` | `6` \u002F auto | Positive numeric | Marker size; `sizeref` normalizes sizes across traces |\n| `line_dash` | `fig.add_hline`, `go.Scatter` | `\"solid\"` | `\"solid\"`, `\"dash\"`, `\"dot\"`, `\"dashdot\"` | Line style for reference lines and traces |\n| `shared_xaxes` \u002F `shared_yaxes` | `make_subplots` | `False` | `True`, `False`, `\"rows\"`, `\"cols\"` | Link axes across subplot panels |\n| `scale` | `fig.write_image` | `1` | `1`–`4` | Resolution multiplier for PNG export (use `2` for ~150 DPI) |\n\n## Best Practices\n\n1. **Prefer `px` for DataFrame data, fall back to `go` for multi-trace composition.** Use `px.scatter()` and its siblings for 80% of plots. Switch to `go` when you need traces with different types in the same figure (e.g., scatter + filled area) or need fine-grained per-trace control.\n\n   ```python\n   # Correct: px for simple grouped plots\n   fig = px.box(df, x=\"treatment\", y=\"expression\", color=\"genotype\")\n\n   # Correct: go when px cannot express the structure\n   fig = go.Figure()\n   fig.add_trace(go.Scatter(x=x, y=y_upper, fill=\"tonexty\", ...))\n   fig.add_trace(go.Scatter(x=x, y=y_lower, ...))\n   ```\n\n2. **Always include `hovertemplate` for scientific figures.** The default tooltip shows raw coordinates without units or gene names. A custom template with `customdata` provides full biological context.\n\n   ```python\n   fig.add_trace(go.Scatter(\n       customdata=df[[\"gene\", \"padj\"]].values,\n       hovertemplate=\"\u003Cb>%{customdata[0]}\u003C\u002Fb>\u003Cbr>padj: %{customdata[1]:.2e}\u003Cextra>\u003C\u002Fextra>\",\n   ))\n   ```\n\n3. **Export HTML for sharing, PNG\u002FSVG for journals.** `fig.write_html()` produces a self-contained file with no external dependencies. Use `scale=2` or higher with `write_image()` to achieve sufficient resolution for print.\n\n4. **Don't use `fig.show()` in batch scripts.** In non-interactive contexts (CI, HPC, cron jobs), `fig.show()` may open a browser window or fail. Use `write_html()` or `write_image()` exclusively.\n\n5. **Use `color_continuous_midpoint=0` for diverging palettes on z-score data.** Without it, the midpoint color defaults to the data midpoint, not zero, misrepresenting symmetric fold changes or correlations.\n\n   ```python\n   fig = px.imshow(corr_matrix, color_continuous_scale=\"RdBu_r\",\n                   color_continuous_midpoint=0, zmin=-1, zmax=1)\n   ```\n\n6. **Set `plot_bgcolor=\"white\"` for publication figures.** Plotly defaults to a light-gray grid background. White background with subtle gridlines is cleaner for most scientific contexts.\n\n   ```python\n   fig.update_layout(plot_bgcolor=\"white\")\n   fig.update_xaxes(showgrid=True, gridcolor=\"lightgray\")\n   fig.update_yaxes(showgrid=True, gridcolor=\"lightgray\")\n   ```\n\n## Common Recipes\n\n### Recipe: Dropdown Menu to Toggle Between Conditions\n\nWhen to use: Overlay multiple conditions in one figure with a dropdown button to show\u002Fhide individual traces cleanly.\n\n```python\nimport plotly.graph_objects as go\nimport numpy as np\n\nconditions = [\"Untreated\", \"DrugA\", \"DrugB\"]\ncolors = [\"gray\", \"crimson\", \"steelblue\"]\nnp.random.seed(1)\nx = np.linspace(0, 24, 49)\n\nfig = go.Figure()\nfor i, (cond, color) in enumerate(zip(conditions, colors)):\n    y = np.sin(x \u002F 4 + i * 0.5) * (1 - i * 0.2) + np.random.normal(0, 0.05, len(x))\n    fig.add_trace(go.Scatter(\n        x=x, y=y, mode=\"lines+markers\",\n        name=cond, line=dict(color=color, width=2),\n        visible=(i == 0),  # only first trace visible initially\n    ))\n\n# One button per condition (shows only that trace)\nbuttons = []\nfor i, cond in enumerate(conditions):\n    visibility = [j == i for j in range(len(conditions))]\n    buttons.append(dict(label=cond, method=\"update\",\n                        args=[{\"visible\": visibility}, {\"title\": f\"Gene Expression — {cond}\"}]))\n\n# \"Show All\" button\nbuttons.append(dict(label=\"Show All\", method=\"update\",\n                    args=[{\"visible\": [True] * len(conditions)}, {\"title\": \"Gene Expression — All Conditions\"}]))\n\nfig.update_layout(\n    updatemenus=[dict(type=\"dropdown\", x=0.01, y=1.15, showactive=True, buttons=buttons)],\n    title=\"Gene Expression — Untreated\",\n    xaxis_title=\"Time (h)\", yaxis_title=\"Relative Expression\",\n)\nfig.show()\n```\n\n### Recipe: Annotating Specific Hits with Arrows\n\nWhen to use: Label outliers, drug hits, or significant genes directly on the figure without cluttering non-annotated points.\n\n```python\nimport plotly.graph_objects as go\nimport numpy as np\nimport pandas as pd\n\nnp.random.seed(33)\ndf = pd.DataFrame({\n    \"x\": np.random.normal(0, 1.5, 300),\n    \"y\": np.random.normal(0, 1.5, 300),\n    \"gene\": [f\"G{i:03d}\" for i in range(300)],\n})\n# Inject top hits\nhits = pd.DataFrame({\n    \"x\": [3.2, -2.8, 2.5, -3.5],\n    \"y\": [4.1, 3.8, -3.2, -2.9],\n    \"gene\": [\"BRCA1\", \"TP53\", \"EGFR\", \"KRAS\"],\n})\n\nfig = go.Figure()\nfig.add_trace(go.Scatter(\n    x=df[\"x\"], y=df[\"y\"], mode=\"markers\",\n    marker=dict(color=\"lightgray\", size=5, opacity=0.6),\n    text=df[\"gene\"],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb>\u003Cbr>x: %{x:.2f}, y: %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n    name=\"Background\",\n))\nfig.add_trace(go.Scatter(\n    x=hits[\"x\"], y=hits[\"y\"], mode=\"markers\",\n    marker=dict(color=\"crimson\", size=10, symbol=\"diamond\"),\n    text=hits[\"gene\"],\n    hovertemplate=\"\u003Cb>%{text}\u003C\u002Fb> [HIT]\u003Cbr>x: %{x:.2f}, y: %{y:.2f}\u003Cextra>\u003C\u002Fextra>\",\n    name=\"Hits\",\n))\n\nfor _, row in hits.iterrows():\n    fig.add_annotation(\n        x=row[\"x\"], y=row[\"y\"],\n        text=f\"\u003Cb>{row['gene']}\u003C\u002Fb>\",\n        showarrow=True, arrowhead=2, arrowwidth=1.5,\n        arrowcolor=\"crimson\", font=dict(size=11, color=\"crimson\"),\n        ax=25, ay=-30,  # arrow offset in pixels\n        bgcolor=\"rgba(255,255,255,0.7)\", bordercolor=\"crimson\", borderwidth=1,\n    )\n\nfig.update_layout(\n    title=\"Hit Identification with Arrow Annotations\",\n    xaxis_title=\"Score A\", yaxis_title=\"Score B\",\n    plot_bgcolor=\"white\",\n)\nfig.show()\n```\n\n## Troubleshooting\n\n| Problem | Cause | Solution |\n|---------|-------|----------|\n| `ValueError: kaleido is required for static image export` | `kaleido` not installed | `pip install kaleido`; verify with `import kaleido` |\n| Blank figure in Jupyter Notebook | Renderer not configured | Run `import plotly.io as pio; pio.renderers.default = \"notebook\"` or upgrade JupyterLab to ≥3 |\n| `fig.show()` opens blank browser tab | No data in figure or offline renderer issue | Check `len(fig.data) > 0`; use `pio.renderers.default = \"browser\"` |\n| Hover tooltips show wrong values | `customdata` index mismatch in `hovertemplate` | Verify `customdata` column order matches `%{customdata[N]}` indices in template |\n| Colors not assigned consistently across traces | `px` re-orders palette when category counts differ | Use `color_discrete_map={\"Cat1\": \"#color1\", ...}` to pin colors explicitly |\n| `write_image` produces blurry PNG | Default `scale=1` is too low for print | Use `fig.write_image(\"fig.png\", scale=2)` for 150 DPI or `scale=4` for 300 DPI |\n| Subplots x-axes not all log-scaled after `shared_xaxes=True` | Shared axis only synchronizes range, not type | Iterate over all `xaxis` keys in `fig.layout` and set `type=\"log\"` explicitly |\n| Large datasets slow to render in browser | Too many individual points in a single scatter trace | Downsample background noise points; keep labeled hits as a separate, smaller trace |\n| `fig.update_layout` does not apply to subplot axes | Multi-panel figures use indexed axes (`xaxis2`, `xaxis3`) | Use `fig.update_xaxes()` (applies to all) or target `fig.layout[\"xaxis2\"]` explicitly |\n\n## Related Skills\n\n- **seaborn-statistical-plots** — use for statistical aggregation (confidence intervals, regression), publication-quality static figures with minimal code, and when matplotlib-level output is required\n- **matplotlib-scientific-plotting** — use for full control over every figure element, custom layouts, embedded text rendering, and journal-specification figure preparation\n- **pydeseq2-differential-expression** — volcano plot outputs from DESeq2 results are a primary input for the interactive volcano workflow above\n- **scanpy-scrna-seq** — Scanpy's UMAP embeddings can be visualized interactively in 3D with `px.scatter_3d`\n\n## References\n\n- [Plotly Python Documentation](https:\u002F\u002Fplotly.com\u002Fpython\u002F) — official API reference, examples gallery, and getting-started guides\n- [Plotly Express API Reference](https:\u002F\u002Fplotly.com\u002Fpython-api-reference\u002Fplotly.express.html) — complete `px` function signatures and parameters\n- [Plotly Graph Objects Reference](https:\u002F\u002Fplotly.com\u002Fpython\u002Fgraph-objects\u002F) — full `go` trace and layout attribute reference\n- [Plotly GitHub Repository](https:\u002F\u002Fgithub.com\u002Fplotly\u002Fplotly.py) — source code, issue tracker, changelog\n- [Kaleido Static Image Export](https:\u002F\u002Fgithub.com\u002Fplotly\u002FKaleido) — dependency for `write_image()` PNG\u002FSVG\u002FPDF export\n",{"name":5,"license":15,"description":7},"master","63f17866001c5ca854bf2fa0b678ce277133de5b","skills\u002Fawesome-skills\u002Fplotly-interactive-plots",0,"https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS","https:\u002F\u002Fbg-szy.github.io\u002FTOP-SKILLS\u002F","https:\u002F\u002Fgithub.com\u002Fbg-szy","https:\u002F\u002Fwww.ecust.edu.cn\u002F","a student of East China University of Science and Technology","2026-11-15 03:38:02",{"source":8,"skillName":5,"commands":35,"prompt":45,"zipAvailable":46},[36,39,42],{"tool":37,"command":38},"npx","npx skills add bg-szy\u002FTOP-SKILLS --skill plotly-interactive-plots",{"tool":40,"command":41},"bunx","bunx skills add bg-szy\u002FTOP-SKILLS --skill plotly-interactive-plots",{"tool":43,"command":44},"pnpm","pnpm dlx skills add bg-szy\u002FTOP-SKILLS --skill plotly-interactive-plots","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fawesome-skills\u002Fplotly-interactive-plots\u002FSKILL.md",true,[],[49,57,64,71,78,85],{"skillNo":50,"name":51,"nameZh":6,"description":52,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":53,"categories":54,"keywords":55,"publishedAt":56},"01M7DXW9N826PKXK174PVW3038","serpmaven","Scrape Google search results (SERP) to JSON with SERP Maven (serpmaven) - a real Chrome browser on a residential proxy with sticky sessions. Run it as a CLI, a Python library, a local HTTP search API (`serve`), an MCP server or a Docker image; set the proxy login, workers, pages and cost tracking. Use when the user wants Google results, rank tracking, a self-hosted SERP API, or live Google search for an AI agent.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Fserpmaven\u002FSKILL.md",[],[],"2026-11-15 14:19:17",{"skillNo":58,"name":59,"nameZh":6,"description":60,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":61,"categories":62,"keywords":63,"publishedAt":56},"01M7DXVHSTWDV65475QEQ915M7","revnu-landing-page-audit","Find out why a landing page isn't turning visitors into signups, demos or sales. Audits one page against what the ad, post or search that sends people there promised, then checks the first screen, proof, call to action, friction and mobile. Returns at most 7 prioritized fixes, plus what is fine and what could not be checked; rewrites specific lines and fixes the page in the repo when asked. Never promises a conversion lift. Works without a Revnu account. Use when the user says or asks: \"audit my landing page\", \"why isn't this page converting\", \"review my homepage\", \"my ads get clicks but no signups\", \"people bounce from the pricing page\", \"CRO review\", \"improve conversion rate\", \"is my hero section clear\", \"roast my landing page\", \"rewrite my headline\", \"what's wrong with this page\", \"landing page feedback\", \"make the signup page convert better\". Not for SEO or AI-search audits, page speed engineering on its own, or building a new page from scratch.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Frevnu-landing-page-audit\u002FSKILL.md",[],[],{"skillNo":65,"name":66,"nameZh":6,"description":67,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":68,"categories":69,"keywords":70,"publishedAt":56},"01M7DXV8X2D7B3VTD4HHYYHTGR","revnu-growth-context","Write down once who buys, what they're stuck on, what it costs, what proof you have and what you've already tried, so every growth task starts from facts instead of a questionnaire. Creates or updates .agents\u002Frevnu-growth.md from the repo, the live site and the user, and imports an existing product-marketing.md. Use when the user says \"set up our marketing context\", \"who is our ideal customer\", \"write our ICP\", \"describe the product for marketing\", \"positioning doc\", \"what's our brand voice\", \"remember this about our customers\", \"log what we tried\", \"what have we learned from marketing\", \"we changed pricing, update the context\", \"update our growth notes\", \"summarize the business for the agent\", \"import my product-marketing.md\", \"start a growth doc\", or before a first marketing, SEO, outreach or launch task when no context file exists. Works without a Revnu account.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Frevnu-growth-context\u002FSKILL.md",[],[],{"skillNo":72,"name":73,"nameZh":6,"description":74,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":75,"categories":76,"keywords":77,"publishedAt":56},"01M7DXV0DGJ1G0A60PAHC6J3C5","revnu-get-listed","Get onto the third-party pages AI answers and search cite when buyers ask for the best tool in your category: \"best X\" roundups, directories, review sites, GitHub awesome lists, community threads and comparison pages. Finds which ones are cited, ranks them by fit and effort, and drafts each submission, PR or pitch by the target's own rules. Use when the user says \"get us listed\", \"which directories should we submit to\", \"how do we get into best-of lists\", \"why does ChatGPT recommend competitors and not us\", \"get mentioned in AI answers\", \"submit to awesome lists\", \"get on G2\", \"where are our competitors listed\", \"pitch listicle authors\", \"find backlink opportunities\", \"launch directories\", \"get into roundups\", \"Product Hunt alternatives\", \"where should we be listed\", \"get cited by Perplexity\". Never submits, pays or posts without a yes. Works without a Revnu account.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Frevnu-get-listed\u002FSKILL.md",[],[],{"skillNo":79,"name":80,"nameZh":6,"description":81,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":84,"publishedAt":56},"01M7DXTSPMGT9D6DSS0HP9BSTA","revnu-ai-readiness-audit","Audit whether your website gives ChatGPT, Perplexity, Claude, Gemini and Google's AI Overviews something worth quoting, and fix the pages that don't. Use when the user says or asks \"is my site ready for AI search\", \"audit my site for AI answers\", \"AEO audit\", \"GEO audit\", \"why doesn't ChatGPT quote our pricing\", \"make our pages citable by AI\", \"do I need llms.txt\", \"does schema help with ChatGPT\", \"check our structured data\", \"is our content visible without JavaScript\", \"AI SEO audit\", \"optimize our homepage for AI answers\", \"LLM SEO check\", \"why does AI get our product wrong\", \"fix our site for AI Overviews\". Checks fetchability and indexability, answers in the raw HTML, entity facts, schema (including JS-injected markup), headings, dates and llms.txt, then gives at most 10 prioritised fixes and edits the repo when asked. Works without a Revnu account.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Frevnu-ai-readiness-audit\u002FSKILL.md",[],[],{"skillNo":86,"name":87,"nameZh":6,"description":88,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":89,"categories":90,"keywords":91,"publishedAt":56},"01M7DXSZHCVTRS620GC3S97JZV","proxy-troubleshooting","Diagnose residential proxy failures, with NodeMaven's gateway as the reference - 407 with correct credentials, 406, 410, 500, a connection that hangs, ERR_TUNNEL_CONNECTION_FAILED in a browser, Playwright or Puppeteer ignoring proxy credentials, a sticky session that changes IP, geo-targeting that is silently ignored, passwords with special characters, and unexpected traffic from headless Chrome. Use when a proxy connection fails, returns the wrong location, or costs more traffic than expected.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Fproxy-troubleshooting\u002FSKILL.md",[],[]]