diff --git a/malariagen_data/ag3.py b/malariagen_data/ag3.py index 590e76ba6..462002b74 100644 --- a/malariagen_data/ag3.py +++ b/malariagen_data/ag3.py @@ -1893,7 +1893,7 @@ def plot_cnv_hmm_coverage_track( self._bokeh_style_genome_xaxis(fig, region_prepped.contig) fig.add_layout(fig.legend[0], "right") - if show: + if show: # pragma: no cover bkplt.show(fig) return None else: @@ -1986,7 +1986,7 @@ def plot_cnv_hmm_coverage( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bkplt.show(fig) return None else: @@ -2156,7 +2156,7 @@ def plot_cnv_hmm_heatmap_track( ) fig.add_layout(color_bar, "right") - if show: + if show: # pragma: no cover bkplt.show(fig) return None else: @@ -2244,7 +2244,7 @@ def plot_cnv_hmm_heatmap( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bkplt.show(fig) return None else: diff --git a/malariagen_data/anoph/aim_data.py b/malariagen_data/anoph/aim_data.py index cfc8895e6..8a15bcfc7 100644 --- a/malariagen_data/anoph/aim_data.py +++ b/malariagen_data/anoph/aim_data.py @@ -307,7 +307,7 @@ def plot_aim_heatmap( height=max(300, row_height * len(samples) + 100), ) - if show: + if show: # pragma: no cover fig.show(renderer=renderer) return None else: diff --git a/malariagen_data/anoph/genome_features.py b/malariagen_data/anoph/genome_features.py index 0b0f5430d..3ef6542b5 100644 --- a/malariagen_data/anoph/genome_features.py +++ b/malariagen_data/anoph/genome_features.py @@ -277,7 +277,7 @@ def plot_transcript( fig.yaxis.ticker = [] self._bokeh_style_genome_xaxis(fig, parent.contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -383,7 +383,7 @@ def plot_genes( fig.yaxis.axis_label = "Genes" self._bokeh_style_genome_xaxis(fig, contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: diff --git a/malariagen_data/anoph/sample_metadata.py b/malariagen_data/anoph/sample_metadata.py index 1ecffbe57..3a3e2ba90 100644 --- a/malariagen_data/anoph/sample_metadata.py +++ b/malariagen_data/anoph/sample_metadata.py @@ -4,10 +4,11 @@ import ipyleaflet import numpy as np import pandas as pd +import plotly.express as px from numpydoc_decorator import doc from ..util import check_types -from . import base_params, map_params +from . import base_params, map_params, plotly_params from .base import AnophelesBase @@ -712,3 +713,85 @@ def _results_cache_add_analysis_params(self, params: dict): super()._results_cache_add_analysis_params(params) params["cohorts_analysis"] = self._cohorts_analysis params["aim_analysis"] = self._aim_analysis + + @doc( + summary=""" + Plot a bar chart showing the number of samples available, grouped by + some variable such as country or year. + """, + parameters=dict( + x="Name of sample metadata column to plot on the X axis.", + color="Name of the sample metadata column to color bars by.", + sort="If True, sort the bars in size order.", + kwargs="Passed through to px.bar().", + ), + ) + def plot_samples_bar( + self, + x: str, + color: Optional[str] = None, + sort: bool = True, + sample_sets: Optional[base_params.sample_sets] = None, + sample_query: Optional[base_params.sample_query] = None, + template: plotly_params.template = "plotly_white", + width: plotly_params.width = 800, + height: plotly_params.height = 600, + show: plotly_params.show = True, + renderer: plotly_params.renderer = None, + **kwargs, + ) -> plotly_params.figure: + # Load sample metadata. + df_samples = self.sample_metadata( + sample_sets=sample_sets, sample_query=sample_query + ) + + # Special handling for plotting by year. + if x == "year": + # Remove samples with missing year. + df_samples = df_samples.query("year > 0") + + # Construct a long-form dataframe to plot. + if color: + grouper: Union[str, List[str]] = [x, color] + else: + grouper = x + df_plot = df_samples.groupby(grouper).agg({"sample_id": "count"}).reset_index() + + # Deal with request to sort by bar size. + if sort: + df_sort = ( + df_samples.groupby(x) + .agg({"sample_id": "count"}) + .reset_index() + .sort_values("sample_id") + ) + x_order = df_sort[x].values + category_orders = kwargs.get("category_orders", dict()) + category_orders.setdefault(x, x_order) + kwargs["category_orders"] = category_orders + + # Make the plot. + fig = px.bar( + df_plot, + x=x, + y="sample_id", + color=color, + template=template, + width=width, + height=height, + **kwargs, + ) + + # Visual styling. + fig.update_layout( + xaxis_title=x.capitalize(), + yaxis_title="No. samples", + ) + if color: + fig.update_layout(legend_title=color.capitalize()) + + if show: # pragma: no cover + fig.show(renderer=renderer) + return None + else: + return fig diff --git a/malariagen_data/anoph/snp_data.py b/malariagen_data/anoph/snp_data.py index 44e64ad9c..06f528c2e 100644 --- a/malariagen_data/anoph/snp_data.py +++ b/malariagen_data/anoph/snp_data.py @@ -1068,7 +1068,7 @@ def plot_snps( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -1250,7 +1250,7 @@ def plot_snps_track( fig.xaxis.minor_tick_line_color = None fig.xaxis[0].formatter = bokeh.models.NumeralTickFormatter(format="0,0") - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: diff --git a/malariagen_data/anopheles.py b/malariagen_data/anopheles.py index 858faf6fd..de7c211e8 100644 --- a/malariagen_data/anopheles.py +++ b/malariagen_data/anopheles.py @@ -1033,7 +1033,7 @@ def plot_pca_variance( debug("make a bar plot") fig = px.bar(x=x, y=y, **plot_kwargs) - if show: + if show: # pragma: no cover fig.show(renderer=renderer) return None else: @@ -1163,7 +1163,7 @@ def _plot_heterozygosity_track( fig.yaxis.axis_label = "Heterozygosity (bp⁻¹)" self._bokeh_style_genome_xaxis(fig, region.contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return fig @@ -1221,7 +1221,7 @@ def plot_heterozygosity_track( output_backend=output_backend, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -1314,7 +1314,7 @@ def plot_heterozygosity( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig_all) return None else: @@ -1494,7 +1494,7 @@ def plot_roh_track( fig.yaxis.axis_label = "RoH" self._bokeh_style_genome_xaxis(fig, resolved_region.contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -1608,7 +1608,7 @@ def plot_roh( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig_all) return None else: @@ -2625,7 +2625,7 @@ def plot_frequencies_heatmap( if not colorbar: fig.update(layout_coloraxis_showscale=False) - if show: + if show: # pragma: no cover fig.show(renderer=renderer) return None else: @@ -2747,7 +2747,7 @@ def plot_frequencies_time_series( debug("tidy plot") fig.update_layout(yaxis_range=[-0.05, 1.05]) - if show: + if show: # pragma: no cover fig.show(renderer=renderer) return None else: @@ -3005,7 +3005,7 @@ def plot_pca_coords( ) fig.update_traces(marker={"size": marker_size}) - if show: + if show: # pragma: no cover fig.show(renderer=renderer) return None else: @@ -3099,7 +3099,7 @@ def plot_pca_coords_3d( ) fig.update_traces(marker={"size": marker_size}) - if show: + if show: # pragma: no cover fig.show(renderer=renderer) return None else: @@ -3220,7 +3220,7 @@ def plot_diversity_stats( **plot_kwargs, ) - if show: + if show: # pragma: no cover fig1.show(renderer=renderer) fig2.show(renderer=renderer) fig3.show(renderer=renderer) @@ -3315,7 +3315,7 @@ def plot_fst_gwss_track( fig.yaxis.ticker = [0, 1] self._bokeh_style_genome_xaxis(fig, contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -3394,7 +3394,7 @@ def plot_fst_gwss( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -3571,7 +3571,7 @@ def plot_h12_calibration( fig.circle(window_sizes, q50, color="black", fill_color="black", size=8) fig.xaxis.ticker = window_sizes - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -3749,7 +3749,7 @@ def plot_h12_gwss_track( fig.yaxis.ticker = [0, 1] self._bokeh_style_genome_xaxis(fig, contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -3823,7 +3823,7 @@ def plot_h12_gwss( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -4029,7 +4029,7 @@ def plot_h1x_gwss_track( fig.yaxis.ticker = [0, 1] self._bokeh_style_genome_xaxis(fig, contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -4108,7 +4108,7 @@ def plot_h1x_gwss( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -4405,7 +4405,7 @@ def plot_ihs_gwss_track( fig.yaxis.axis_label = "ihs" self._bokeh_style_genome_xaxis(fig, contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -4498,7 +4498,7 @@ def plot_xpehh_gwss( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -4595,7 +4595,7 @@ def plot_ihs_gwss( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -4804,7 +4804,7 @@ def plot_g123_gwss_track( fig.yaxis.ticker = [0, 1] self._bokeh_style_genome_xaxis(fig, contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -4878,7 +4878,7 @@ def plot_g123_gwss( sizing_mode=sizing_mode, ) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -5103,7 +5103,7 @@ def plot_g123_calibration( fig.xaxis.ticker = window_sizes - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -5393,7 +5393,7 @@ def plot_xpehh_gwss_track( fig.yaxis.axis_label = "XP-EHH" self._bokeh_style_genome_xaxis(fig, contig) - if show: + if show: # pragma: no cover bokeh.plotting.show(fig) return None else: @@ -5561,7 +5561,7 @@ def plot_haplotype_clustering( ) ) - if show: + if show: # pragma: no cover fig.show(renderer=renderer) return None else: diff --git a/notebooks/plot_samples.ipynb b/notebooks/plot_samples.ipynb index 6b4556581..32eb39eb8 100644 --- a/notebooks/plot_samples.ipynb +++ b/notebooks/plot_samples.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -20,108 +20,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\nconst JS_MIME_TYPE = 'application/javascript';\n const HTML_MIME_TYPE = 'text/html';\n const EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n const CLASS_NAME = 'output_bokeh rendered_html';\n\n /**\n * Render data to the DOM node\n */\n function render(props, node) {\n const script = document.createElement(\"script\");\n node.appendChild(script);\n }\n\n /**\n * Handle when an output is cleared or removed\n */\n function handleClearOutput(event, handle) {\n const cell = handle.cell;\n\n const id = cell.output_area._bokeh_element_id;\n const server_id = cell.output_area._bokeh_server_id;\n // Clean up Bokeh references\n if (id != null && id in Bokeh.index) {\n Bokeh.index[id].model.document.clear();\n delete Bokeh.index[id];\n }\n\n if (server_id !== undefined) {\n // Clean up Bokeh references\n const cmd_clean = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n cell.notebook.kernel.execute(cmd_clean, {\n iopub: {\n output: function(msg) {\n const id = msg.content.text.trim();\n if (id in Bokeh.index) {\n Bokeh.index[id].model.document.clear();\n delete Bokeh.index[id];\n }\n }\n }\n });\n // Destroy server and session\n const cmd_destroy = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n cell.notebook.kernel.execute(cmd_destroy);\n }\n }\n\n /**\n * Handle when a new output is added\n */\n function handleAddOutput(event, handle) {\n const output_area = handle.output_area;\n const output = handle.output;\n\n // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n return\n }\n\n const toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n\n if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n // store reference to embed id on output_area\n output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n }\n if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n const bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n const script_attrs = bk_div.children[0].attributes;\n for (let i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n }\n\n function register_renderer(events, OutputArea) {\n\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n const toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n const props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[toinsert.length - 1]);\n element.append(toinsert);\n return toinsert\n }\n\n /* Handle when an output is cleared or removed */\n events.on('clear_output.CodeCell', handleClearOutput);\n events.on('delete.Cell', handleClearOutput);\n\n /* Handle when a new output is added */\n events.on('output_added.OutputArea', handleAddOutput);\n\n /**\n * Register the mime type and append_mime function with output_area\n */\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n /* Is output safe? */\n safe: true,\n /* Index of renderer in `output_area.display_order` */\n index: 0\n });\n }\n\n // register the mime type if in Jupyter Notebook environment and previously unregistered\n if (root.Jupyter !== undefined) {\n const events = require('base/js/events');\n const OutputArea = require('notebook/js/outputarea').OutputArea;\n\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n }\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded() {\n const el = document.getElementById(null);\n if (el != null) {\n el.textContent = \"BokehJS is loading...\";\n }\n if (root.Bokeh !== undefined) {\n if (el != null) {\n el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(display_loaded, 100)\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.1.1.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(null)).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));", - "application/vnd.bokehjs_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
MalariaGEN Ag3 API client
\n", - " Please note that data are subject to terms of use,\n", - " for more information see \n", - " the MalariaGEN website or contact data@malariagen.net.\n", - " See also the Ag3 API docs.\n", - "
\n", - " Storage URL\n", - " simplecache::gs://vo_agam_release
\n", - " Data releases available\n", - " 3.0
\n", - " Results cache\n", - " None
\n", - " Cohorts analysis\n", - " 20230223
\n", - " AIM analysis\n", - " 20220528
\n", - " Site filters analysis\n", - " dt_20200416
\n", - " Software version\n", - " malariagen_data 0.0.0
\n", - " Client location\n", - " unknown
\n", - " " - ], - "text/plain": [ - "\n", - "Storage URL : simplecache::gs://vo_agam_release\n", - "Data releases available : 3.0\n", - "Results cache : None\n", - "Cohorts analysis : 20230223\n", - "AIM analysis : 20220528\n", - "Site filters analysis : dt_20200416\n", - "Software version : malariagen_data 0.0.0\n", - "Client location : unknown\n", - "---\n", - "Please note that data are subject to terms of use,\n", - "for more information see https://www.malariagen.net/data\n", - "or contact data@malariagen.net. For API documentation see \n", - "https://malariagen.github.io/vector-data/ag3/api.html" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ag3 = malariagen_data.Ag3(\n", " \"simplecache::gs://vo_agam_release\",\n", @@ -133,25 +34,36 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f94ce545a7024e0db279c58331f45637", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[-2, 20], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_tex…" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], + "source": [ + "ag3.plot_samples_bar(x=\"country\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ag3.plot_samples_bar(x=\"country\", color=\"taxon\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ag3.plot_samples_bar(x=\"year\", color=\"country\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "ag3.plot_samples_interactive_map(\n", " sample_sets=[\"3.0\"],\n", @@ -161,25 +73,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "dd3ca7d42b8745e6880e57d79db77774", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[-2, 20], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_tex…" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Demo using a different basemap provider\n", "# - The map background will appear grey if provision of tiles fails\n", @@ -192,20 +88,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "dict_keys(['mapnik', 'natgeoworldmap', 'opentopomap', 'positron', 'satellite', 'terrain', 'watercolor', 'worldimagery', 'worldstreetmap', 'worldtopomap'])" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# See the available basemap abbreviations\n", "malariagen_data.anoph.map_params.basemap_abbrevs.keys()" @@ -213,25 +98,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cfca3d4c4aaa4e3f9eb869e861593613", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[-2, 20], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_tex…" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Demo using a basemap abbreviation, case-insensitive\n", "ag3.plot_samples_interactive_map(\n", @@ -250,99 +119,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\nconst JS_MIME_TYPE = 'application/javascript';\n const HTML_MIME_TYPE = 'text/html';\n const EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n const CLASS_NAME = 'output_bokeh rendered_html';\n\n /**\n * Render data to the DOM node\n */\n function render(props, node) {\n const script = document.createElement(\"script\");\n node.appendChild(script);\n }\n\n /**\n * Handle when an output is cleared or removed\n */\n function handleClearOutput(event, handle) {\n const cell = handle.cell;\n\n const id = cell.output_area._bokeh_element_id;\n const server_id = cell.output_area._bokeh_server_id;\n // Clean up Bokeh references\n if (id != null && id in Bokeh.index) {\n Bokeh.index[id].model.document.clear();\n delete Bokeh.index[id];\n }\n\n if (server_id !== undefined) {\n // Clean up Bokeh references\n const cmd_clean = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n cell.notebook.kernel.execute(cmd_clean, {\n iopub: {\n output: function(msg) {\n const id = msg.content.text.trim();\n if (id in Bokeh.index) {\n Bokeh.index[id].model.document.clear();\n delete Bokeh.index[id];\n }\n }\n }\n });\n // Destroy server and session\n const cmd_destroy = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n cell.notebook.kernel.execute(cmd_destroy);\n }\n }\n\n /**\n * Handle when a new output is added\n */\n function handleAddOutput(event, handle) {\n const output_area = handle.output_area;\n const output = handle.output;\n\n // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n return\n }\n\n const toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n\n if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n // store reference to embed id on output_area\n output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n }\n if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n const bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n const script_attrs = bk_div.children[0].attributes;\n for (let i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n }\n\n function register_renderer(events, OutputArea) {\n\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n const toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n const props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[toinsert.length - 1]);\n element.append(toinsert);\n return toinsert\n }\n\n /* Handle when an output is cleared or removed */\n events.on('clear_output.CodeCell', handleClearOutput);\n events.on('delete.Cell', handleClearOutput);\n\n /* Handle when a new output is added */\n events.on('output_added.OutputArea', handleAddOutput);\n\n /**\n * Register the mime type and append_mime function with output_area\n */\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n /* Is output safe? */\n safe: true,\n /* Index of renderer in `output_area.display_order` */\n index: 0\n });\n }\n\n // register the mime type if in Jupyter Notebook environment and previously unregistered\n if (root.Jupyter !== undefined) {\n const events = require('base/js/events');\n const OutputArea = require('notebook/js/outputarea').OutputArea;\n\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n }\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded() {\n const el = document.getElementById(null);\n if (el != null) {\n el.textContent = \"BokehJS is loading...\";\n }\n if (root.Bokeh !== undefined) {\n if (el != null) {\n el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(display_loaded, 100)\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.1.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.1.1.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(null)).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));", - "application/vnd.bokehjs_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
MalariaGEN Af1 API client
\n", - " Please note that data are subject to terms of use,\n", - " for more information see \n", - " the MalariaGEN website or contact data@malariagen.net.\n", - "
\n", - " Storage URL\n", - " simplecache::gs://vo_afun_release
\n", - " Data releases available\n", - " 1.0
\n", - " Results cache\n", - " None
\n", - " Cohorts analysis\n", - " 20221129
\n", - " Site filters analysis\n", - " dt_20200416
\n", - " Software version\n", - " malariagen_data 0.0.0
\n", - " Client location\n", - " unknown
\n", - " " - ], - "text/plain": [ - "\n", - "Storage URL : simplecache::gs://vo_afun_release\n", - "Data releases available : 1.0\n", - "Results cache : None\n", - "Cohorts analysis : 20221129\n", - "Site filters analysis : dt_20200416\n", - "Software version : malariagen_data 0.0.0\n", - "Client location : unknown\n", - "---\n", - "Please note that data are subject to terms of use,\n", - "for more information see https://www.malariagen.net/data\n", - "or contact data@malariagen.net." - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "af1 = malariagen_data.Af1(\n", " \"simplecache::gs://vo_afun_release\",\n", @@ -355,25 +134,27 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "af1.plot_samples_bar(x=\"country\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "af1.plot_samples_bar(x=\"year\", color=\"country\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b554ac09959e4e3da81b3012ae43dd50", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[-2, 20], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_tex…" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Demo using a basemap provider the same as one in its curated list\n", "af1.plot_samples_interactive_map(\n", @@ -383,54 +164,18 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "help(ag3.plot_samples_bar)" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Help on method plot_samples_interactive_map in module malariagen_data.anoph.sample_metadata:\n", - "\n", - "plot_samples_interactive_map(sample_sets: Union[Union[Sequence[str], str], NoneType] = None, sample_query: Union[str, NoneType] = None, basemap: Union[Union[str, Dict, ipyleaflet.leaflet.TileLayer, xyzservices.lib.TileProvider], NoneType] = 'mapnik', center: Tuple[int, int] = (-2, 20), zoom: int = 3, height: Union[int, str] = 500, width: Union[int, str] = '100%', min_samples: int = 1, count_by: str = 'taxon') -> ipyleaflet.leaflet.Map method of malariagen_data.ag3.Ag3 instance\n", - " Plot an interactive map showing sampling locations using ipyleaflet.\n", - " \n", - " Parameters\n", - " ----------\n", - " sample_sets : sequence of str or str or None, optional\n", - " List of sample sets and/or releases. Can also be a single sample set\n", - " or release.\n", - " sample_query : str or None, optional\n", - " A pandas query string to be evaluated against the sample metadata, to\n", - " select samples to be included in the returned data.\n", - " basemap : str or Dict or TileLayer or TileProvider or None, optional, default: 'mapnik'\n", - " Basemap from ipyleaflet or other TileLayer provider. Strings are\n", - " abbreviations mapped to corresponding basemaps, available values are\n", - " ['mapnik', 'natgeoworldmap', 'opentopomap', 'positron', 'satellite',\n", - " 'terrain', 'watercolor', 'worldimagery', 'worldstreetmap',\n", - " 'worldtopomap'].\n", - " center : Tuple[int, int], optional, default: (-2, 20)\n", - " Location to center the map.\n", - " zoom : int, optional, default: 3\n", - " Initial zoom level.\n", - " height : int or str, optional, default: 500\n", - " Height of the map in pixels (px) or other units.\n", - " width : int or str, optional, default: '100%'\n", - " Width of the map in pixels (px) or other units.\n", - " min_samples : int, optional, default: 1\n", - " Minimum number of samples required to show a marker for a given\n", - " location.\n", - " count_by : str, optional, default: 'taxon'\n", - " Metadata column to report counts of samples by for each location.\n", - " \n", - " Returns\n", - " -------\n", - " Map\n", - " Ipyleaflet map widget.\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "help(ag3.plot_samples_interactive_map)" ] diff --git a/tests/anoph/test_sample_metadata.py b/tests/anoph/test_sample_metadata.py index 3c29d31f5..1bd1497ba 100644 --- a/tests/anoph/test_sample_metadata.py +++ b/tests/anoph/test_sample_metadata.py @@ -3,6 +3,7 @@ import ipyleaflet import numpy as np import pandas as pd +import plotly.graph_objects as go import pytest from pandas.testing import assert_frame_equal from pytest_cases import parametrize_with_cases @@ -707,3 +708,45 @@ def test_wgs_data_catalog(fixture, api): ] assert df.columns.to_list() == expected_cols assert len(df) == sample_count.loc[sample_set] + + +@parametrize_with_cases("fixture,api", cases=".") +def test_plot_samples_bar(fixture, api): + # By country. + fig = api.plot_samples_bar( + x="country", + show=False, + ) + assert isinstance(fig, go.Figure) + + # By year. + fig = api.plot_samples_bar( + x="year", + show=False, + ) + assert isinstance(fig, go.Figure) + + # By country and taxon. + fig = api.plot_samples_bar( + x="country", + color="taxon", + show=False, + ) + assert isinstance(fig, go.Figure) + + # By year and country. + fig = api.plot_samples_bar( + x="year", + color="country", + show=False, + ) + assert isinstance(fig, go.Figure) + + # Not sorted. + fig = api.plot_samples_bar( + x="country", + color="taxon", + sort=False, + show=False, + ) + assert isinstance(fig, go.Figure)