From fe39bccc23186d0a0f0b51a792d4577aaca88bd1 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Mon, 4 Oct 2021 04:46:14 +0300 Subject: Remove file I/O statements Issue: https://github.com/genenetwork/gn-gemtext-threads/blob/main/topics/gn1-migration-to-gn2/non-clustered-heatmaps-and-flipping.gmi * Remove file I/O from the function. If file I/O is needed, it will be provided outside of this function. --- gn3/heatmaps.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gn3/heatmaps.py b/gn3/heatmaps.py index adbfbc6..42231bf 100644 --- a/gn3/heatmaps.py +++ b/gn3/heatmaps.py @@ -224,7 +224,6 @@ def build_heatmap(traits_names, conn: Any): process_traits_data_for_heatmap( organised, traits_ids, chromosome_names), clustered, - "single_heatmap_{}".format(random_string(10)), y_axis=tuple( ordered_traits_names[traits_ids[order]] for order in traits_order), @@ -355,6 +354,9 @@ def generate_clustered_heatmap( loci_names: Sequence[Sequence[str]] = tuple(), output_dir: str = TMPDIR, colorscale=((0.0, '#0000FF'), (0.5, '#00FF00'), (1.0, '#FF0000'))): + data, clustering_data, x_axis=None, x_label: str = "", y_axis=None, + y_label: str = "", loci_names: Sequence[Sequence[str]] = tuple(), + colorscale=((0.0, '#0000FF'), (0.5, '#00FF00'), (1.0, '#FF0000'))): """ Generate a dendrogram, and heatmaps for each chromosome, and put them all into one plot. @@ -419,6 +421,4 @@ def generate_clustered_heatmap( showlegend=True, showscale=True, selector={"name": x_axis[-1]}) - image_filename = "{}/{}.html".format(output_dir, image_filename_prefix) - fig.write_html(image_filename) - return image_filename, fig + return fig -- cgit v1.2.3 From 0797e53220046d8f36a45d8b09b395b156d8fde7 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Mon, 4 Oct 2021 06:00:11 +0300 Subject: Add typing. Simplify arguments. Issue: https://github.com/genenetwork/gn-gemtext-threads/blob/main/topics/gn1-migration-to-gn2/non-clustered-heatmaps-and-flipping.gmi * Add type-hints to the functions * Merge the axis data and labels to simpler dict arguments to reduce number of parameters to the function. --- gn3/heatmaps.py | 52 ++++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/gn3/heatmaps.py b/gn3/heatmaps.py index 42231bf..00f4353 100644 --- a/gn3/heatmaps.py +++ b/gn3/heatmaps.py @@ -168,7 +168,7 @@ def get_loci_names( __get_trait_loci, [v[1] for v in organised.items()], {}) return tuple(loci_dict[_chr] for _chr in chromosome_names) -def build_heatmap(traits_names, conn: Any): +def build_heatmap(traits_names: Sequence[str], conn: Any) -> go.Figure: """ heatmap function @@ -220,16 +220,20 @@ def build_heatmap(traits_names, conn: Any): zip(traits_ids, [traits[idx]["trait_fullname"] for idx in traits_order])) - return generate_clustered_heatmap( + return clustered_heatmap( process_traits_data_for_heatmap( organised, traits_ids, chromosome_names), clustered, - y_axis=tuple( - ordered_traits_names[traits_ids[order]] - for order in traits_order), - y_label="Traits", - x_axis=chromosome_names, - x_label="Chromosomes", + x_axis={ + "label": "Chromosomes", + "data": chromosome_names + }, + y_axis={ + "label": "Traits", + "data": tuple( + ordered_traits_names[traits_ids[order]] + for order in traits_order) + }, loci_names=get_loci_names(organised, chromosome_names)) def compute_traits_order(slink_data, neworder: tuple = tuple()): @@ -348,37 +352,37 @@ def process_traits_data_for_heatmap(data, trait_names, chromosome_names): for chr_name in chromosome_names] return hdata -def generate_clustered_heatmap( - data, clustering_data, image_filename_prefix, x_axis=None, - x_label: str = "", y_axis=None, y_label: str = "", +def clustered_heatmap( + data: Sequence[Sequence[float]], clustering_data: Sequence[float], + x_axis,#: Dict[Union[str, int], Union[str, Sequence[str]]], + y_axis: Dict[str, Union[str, Sequence[str]]], loci_names: Sequence[Sequence[str]] = tuple(), - output_dir: str = TMPDIR, - colorscale=((0.0, '#0000FF'), (0.5, '#00FF00'), (1.0, '#FF0000'))): - data, clustering_data, x_axis=None, x_label: str = "", y_axis=None, - y_label: str = "", loci_names: Sequence[Sequence[str]] = tuple(), - colorscale=((0.0, '#0000FF'), (0.5, '#00FF00'), (1.0, '#FF0000'))): + colorscale: Sequence[Sequence[Union[float, str]]] = ( + (0.0, '#0000FF'), (0.5, '#00FF00'), (1.0, '#FF0000'))) -> go.Figure: """ Generate a dendrogram, and heatmaps for each chromosome, and put them all into one plot. """ # pylint: disable=[R0913, R0914] - num_cols = 1 + len(x_axis) + x_axis_data = x_axis["data"] + y_axis_data = y_axis["data"] + num_cols = 1 + len(x_axis_data) fig = make_subplots( rows=1, cols=num_cols, shared_yaxes="rows", horizontal_spacing=0.001, - subplot_titles=["distance"] + x_axis, + subplot_titles=["distance"] + x_axis_data, figure=ff.create_dendrogram( - np.array(clustering_data), orientation="right", labels=y_axis)) + np.array(clustering_data), orientation="right", labels=y_axis_data)) hms = [go.Heatmap( name=chromo, x=loci, - y=y_axis, + y=y_axis_data, z=data_array, showscale=False) for chromo, data_array, loci - in zip(x_axis, data, loci_names)] + in zip(x_axis_data, data, loci_names)] for i, heatmap in enumerate(hms): fig.add_trace(heatmap, row=1, col=(i + 2)) @@ -389,10 +393,10 @@ def generate_clustered_heatmap( "xaxis": { "mirror": False, "showgrid": True, - "title": x_label + "title": x_axis["label"] }, "yaxis": { - "title": y_label + "title": y_axis["label"] } }) @@ -420,5 +424,5 @@ def generate_clustered_heatmap( fig.update_traces( showlegend=True, showscale=True, - selector={"name": x_axis[-1]}) + selector={"name": x_axis_data[-1]}) return fig -- cgit v1.2.3 From f71c0d5b04a2bb504acf306be11705ae0515aa14 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Mon, 4 Oct 2021 06:28:25 +0300 Subject: Swap axis labels Issue: https://github.com/genenetwork/gn-gemtext-threads/blob/main/topics/gn1-migration-to-gn2/non-clustered-heatmaps-and-flipping.gmi * Switch the axis labels to make them less confusing for the user. --- gn3/heatmaps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gn3/heatmaps.py b/gn3/heatmaps.py index 00f4353..7e7113d 100644 --- a/gn3/heatmaps.py +++ b/gn3/heatmaps.py @@ -372,7 +372,7 @@ def clustered_heatmap( cols=num_cols, shared_yaxes="rows", horizontal_spacing=0.001, - subplot_titles=["distance"] + x_axis_data, + subplot_titles=[x_axis["label"]] + x_axis_data, figure=ff.create_dendrogram( np.array(clustering_data), orientation="right", labels=y_axis_data)) hms = [go.Heatmap( @@ -393,7 +393,7 @@ def clustered_heatmap( "xaxis": { "mirror": False, "showgrid": True, - "title": x_axis["label"] + "title": "Distance" }, "yaxis": { "title": y_axis["label"] -- cgit v1.2.3 From aeefaad0629ca29e81ac3f0dbe882d7bf09b8711 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Mon, 4 Oct 2021 12:03:42 +0300 Subject: Enable vertical orientation of heatmaps Issue: https://github.com/genenetwork/gn-gemtext-threads/blob/main/topics/gn1-migration-to-gn2/non-clustered-heatmaps-and-flipping.gmi * Update the code to enable the generation of the heatmap in both the horizontal and vertical orientations. --- gn3/heatmaps.py | 91 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/gn3/heatmaps.py b/gn3/heatmaps.py index 7e7113d..ff65652 100644 --- a/gn3/heatmaps.py +++ b/gn3/heatmaps.py @@ -168,7 +168,9 @@ def get_loci_names( __get_trait_loci, [v[1] for v in organised.items()], {}) return tuple(loci_dict[_chr] for _chr in chromosome_names) -def build_heatmap(traits_names: Sequence[str], conn: Any) -> go.Figure: +def build_heatmap( + traits_names: Sequence[str], conn: Any, + vertical: bool = False) -> go.Figure: """ heatmap function @@ -234,6 +236,7 @@ def build_heatmap(traits_names: Sequence[str], conn: Any) -> go.Figure: ordered_traits_names[traits_ids[order]] for order in traits_order) }, + vertical=vertical, loci_names=get_loci_names(organised, chromosome_names)) def compute_traits_order(slink_data, neworder: tuple = tuple()): @@ -357,6 +360,7 @@ def clustered_heatmap( x_axis,#: Dict[Union[str, int], Union[str, Sequence[str]]], y_axis: Dict[str, Union[str, Sequence[str]]], loci_names: Sequence[Sequence[str]] = tuple(), + vertical: bool = False, colorscale: Sequence[Sequence[Union[float, str]]] = ( (0.0, '#0000FF'), (0.5, '#00FF00'), (1.0, '#FF0000'))) -> go.Figure: """ @@ -366,57 +370,66 @@ def clustered_heatmap( # pylint: disable=[R0913, R0914] x_axis_data = x_axis["data"] y_axis_data = y_axis["data"] - num_cols = 1 + len(x_axis_data) + num_plots = 1 + len(x_axis_data) fig = make_subplots( - rows=1, - cols=num_cols, - shared_yaxes="rows", + rows=num_plots if vertical else 1, + cols=1 if vertical else num_plots, + shared_xaxes = "columns" if vertical else False, + shared_yaxes = False if vertical else "rows", + vertical_spacing=0.010, horizontal_spacing=0.001, - subplot_titles=[x_axis["label"]] + x_axis_data, + subplot_titles=["" if vertical else x_axis["label"]] + [ + "Chromosome: {}".format(chromo) if vertical else chromo + for chromo in x_axis_data],#+ x_axis_data, figure=ff.create_dendrogram( - np.array(clustering_data), orientation="right", labels=y_axis_data)) + np.array(clustering_data), + orientation="bottom" if vertical else "right", + labels=y_axis_data)) hms = [go.Heatmap( name=chromo, - x=loci, - y=y_axis_data, + x=y_axis_data if vertical else loci, + y=loci if vertical else y_axis_data, z=data_array, + transpose=vertical, showscale=False) for chromo, data_array, loci in zip(x_axis_data, data, loci_names)] for i, heatmap in enumerate(hms): - fig.add_trace(heatmap, row=1, col=(i + 2)) - - fig.update_layout( - { - "width": 1500, - "height": 800, - "xaxis": { + fig.add_trace( + heatmap, + row=((i + 2) if vertical else 1), + col=(1 if vertical else (i + 2))) + + axes_layouts = { + "{axis}axis{count}".format( + axis=("y" if vertical else "x"), + count=(i+1 if i > 0 else "")): { "mirror": False, - "showgrid": True, - "title": "Distance" - }, - "yaxis": { - "title": y_axis["label"] - } - }) - - x_axes_layouts = { - "xaxis{}".format(i+1 if i > 0 else ""): { - "mirror": False, - "showticklabels": i == 0, - "ticks": "outside" if i == 0 else "" + "showticklabels": i == 0, + "ticks": "outside" if i == 0 else "" } - for i in range(num_cols)} + for i in range(num_plots)} - fig.update_layout( - { - "width": 4000, - "height": 800, - "yaxis": { - "mirror": False, - "ticks": "" - }, - **x_axes_layouts}) + print("vertical?: {} ==> {}".format("T" if vertical else "F", axes_layouts)) + + fig.update_layout({ + "width": 800 if vertical else 4000, + "height": 4000 if vertical else 800, + "{}axis".format("x" if vertical else "y"): { + "mirror": False, + "ticks": "", + "side": "top" if vertical else "left", + "title": y_axis["label"], + "tickangle": 90 if vertical else 0, + "ticklabelposition": "outside top" if vertical else "outside left" + }, + "{}axis".format("y" if vertical else "x"): { + "mirror": False, + "showgrid": True, + "title": "Distance", + "side": "right" if vertical else "top" + }, + **axes_layouts}) fig.update_traces( showlegend=False, colorscale=colorscale, -- cgit v1.2.3 From 7627378dd1eb52055f4e25047ba53a2d2e4c092f Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Wed, 6 Oct 2021 11:25:38 +0300 Subject: Enable vertical and horizontal heatmaps Issue: https://github.com/genenetwork/gn-gemtext-threads/blob/main/topics/gn1-migration-to-gn2/non-clustered-heatmaps-and-flipping.gmi * Update the request endpoint, so that it produces a vertical or horizontal heatmap depending on the user's request. --- gn3/api/heatmaps.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gn3/api/heatmaps.py b/gn3/api/heatmaps.py index 62ca2ad..633a061 100644 --- a/gn3/api/heatmaps.py +++ b/gn3/api/heatmaps.py @@ -17,7 +17,9 @@ def clustered_heatmaps(): Parses the incoming data and responds with the JSON-serialized plotly figure representing the clustered heatmap. """ - traits_names = request.get_json().get("traits_names", tuple()) + heatmap_request = request.get_json() + traits_names = heatmap_request.get("traits_names", tuple()) + vertical = heatmap_request.get("vertical", False) if len(traits_names) < 2: return jsonify({ "message": "You need to provide at least two trait names." @@ -30,7 +32,7 @@ def clustered_heatmaps(): traits_fullnames = [parse_trait_fullname(trait) for trait in traits_names] with io.StringIO() as io_str: - _filename, figure = build_heatmap(traits_fullnames, conn) + figure = build_heatmap(traits_fullnames, conn, vertical=vertical) figure.write_json(io_str) fig_json = io_str.getvalue() return fig_json, 200 -- cgit v1.2.3 From a7fbce242f6683d66452ff02e541aa9b28908f39 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Thu, 14 Oct 2021 07:19:32 +0300 Subject: Allow CORS_ORIGINS to be configurable via the environment Issue: https://github.com/genenetwork/gn-gemtext-threads/blob/main/topics/gn1-migration-to-gn2/non-clustered-heatmaps-and-flipping.gmi * gn3/app.py: setup CORS after all the configuration sources are loaded. * gn3/settings.py: Parse CORS_ORIGINS from the environment variables. Enable the CORS_ORIGINS configuration to be set in the environment variables to give the application some flexibility when launching. --- gn3/app.py | 13 +++++++------ gn3/settings.py | 11 +++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/gn3/app.py b/gn3/app.py index a25332c..3d68b3f 100644 --- a/gn3/app.py +++ b/gn3/app.py @@ -21,12 +21,6 @@ def create_app(config: Union[Dict, str, None] = None) -> Flask: # Load default configuration app.config.from_object("gn3.settings") - CORS( - app, - origins=app.config["CORS_ORIGINS"], - allow_headers=app.config["CORS_HEADERS"], - supports_credentials=True, intercept_exceptions=False) - # Load environment configuration if "GN3_CONF" in os.environ: app.config.from_envvar('GN3_CONF') @@ -37,6 +31,13 @@ def create_app(config: Union[Dict, str, None] = None) -> Flask: app.config.update(config) elif config.endswith(".py"): app.config.from_pyfile(config) + + CORS( + app, + origins=app.config["CORS_ORIGINS"], + allow_headers=app.config["CORS_HEADERS"], + supports_credentials=True, intercept_exceptions=False) + app.register_blueprint(general, url_prefix="/api/") app.register_blueprint(gemma, url_prefix="/api/gemma") app.register_blueprint(rqtl, url_prefix="/api/rqtl") diff --git a/gn3/settings.py b/gn3/settings.py index 150d96d..56ddaba 100644 --- a/gn3/settings.py +++ b/gn3/settings.py @@ -35,10 +35,17 @@ GENOTYPE_FILES = os.environ.get( "GENOTYPE_FILES", "{}/genotype_files/genotype".format(os.environ.get("HOME"))) # CROSS-ORIGIN SETUP -CORS_ORIGINS = [ +def parse_env_cors(default): + origins_str = os.environ.get("CORS_ORIGINS", None) + if origins_str: + return [ + origin.strip() for origin in origins_str.split(",") if origin != ""] + return default + +CORS_ORIGINS = parse_env_cors([ "http://localhost:*", "http://127.0.0.1:*" -] +]) CORS_HEADERS = [ "Content-Type", -- cgit v1.2.3 From 546b37e77c11c5268aa9510b9756f2ed4d60241d Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Thu, 14 Oct 2021 07:31:41 +0300 Subject: Fix some linting issues --- gn3/heatmaps.py | 6 +++--- gn3/settings.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gn3/heatmaps.py b/gn3/heatmaps.py index ff65652..2dd9d07 100644 --- a/gn3/heatmaps.py +++ b/gn3/heatmaps.py @@ -374,8 +374,8 @@ def clustered_heatmap( fig = make_subplots( rows=num_plots if vertical else 1, cols=1 if vertical else num_plots, - shared_xaxes = "columns" if vertical else False, - shared_yaxes = False if vertical else "rows", + shared_xaxes="columns" if vertical else False, + shared_yaxes=False if vertical else "rows", vertical_spacing=0.010, horizontal_spacing=0.001, subplot_titles=["" if vertical else x_axis["label"]] + [ @@ -407,7 +407,7 @@ def clustered_heatmap( "mirror": False, "showticklabels": i == 0, "ticks": "outside" if i == 0 else "" - } + } for i in range(num_plots)} print("vertical?: {} ==> {}".format("T" if vertical else "F", axes_layouts)) diff --git a/gn3/settings.py b/gn3/settings.py index 56ddaba..d5f1d3c 100644 --- a/gn3/settings.py +++ b/gn3/settings.py @@ -36,6 +36,7 @@ GENOTYPE_FILES = os.environ.get( # CROSS-ORIGIN SETUP def parse_env_cors(default): + """Parse comma-separated configuration into list of strings.""" origins_str = os.environ.get("CORS_ORIGINS", None) if origins_str: return [ -- cgit v1.2.3