From 04ee83fcaff424068b072a37fd0c2319b8467d45 Mon Sep 17 00:00:00 2001 From: joehart2001 Date: Fri, 29 May 2026 17:50:13 +0100 Subject: [PATCH] replace downlaod with direct html links --- .../app/bulk_crystal/phonons/app_phonons.py | 3 -- .../phonons/interactive_helpers.py | 36 ++++++++++----- ml_peg/app/utils/register_callbacks.py | 46 ++++--------------- 3 files changed, 32 insertions(+), 53 deletions(-) diff --git a/ml_peg/app/bulk_crystal/phonons/app_phonons.py b/ml_peg/app/bulk_crystal/phonons/app_phonons.py index 59aa2701a..6ddb9d200 100644 --- a/ml_peg/app/bulk_crystal/phonons/app_phonons.py +++ b/ml_peg/app/bulk_crystal/phonons/app_phonons.py @@ -29,7 +29,6 @@ build_serialized_scatter_content, resolve_scatter_selection, ) -from ml_peg.app.utils.register_callbacks import register_image_download_callbacks from ml_peg.calcs import CALCS_ROOT DATA_PATH = APP_ROOT / "data" / "bulk_crystal" / "phonons" @@ -54,8 +53,6 @@ class PhononApp(BaseApp): def register_callbacks(self) -> None: """Register scatter/dispersion callbacks via shared helpers.""" - register_image_download_callbacks() - with SCATTER_PATH.open(encoding="utf8") as handle: interactive_data = json.load(handle) diff --git a/ml_peg/app/bulk_crystal/phonons/interactive_helpers.py b/ml_peg/app/bulk_crystal/phonons/interactive_helpers.py index 7ab907dc9..6b61beac7 100644 --- a/ml_peg/app/bulk_crystal/phonons/interactive_helpers.py +++ b/ml_peg/app/bulk_crystal/phonons/interactive_helpers.py @@ -12,8 +12,6 @@ import matplotlib -from ml_peg.app.utils.build_components import build_image_download_controls - matplotlib.use("Agg") from dash import dcc, html from matplotlib import gridspec @@ -317,27 +315,41 @@ def render_dispersion_component( if not image_src: return None - filename = f"{label}_phonon_dispersion.png" if label else "phonon_dispersion.png" + stem = f"{label}_phonon_dispersion" if label else "phonon_dispersion" + link_style = { + "display": "inline-flex", + "alignItems": "center", + "gap": "8px", + "marginTop": "4px", + "marginBottom": "0px", + } if uris: - download_controls = build_image_download_controls(label or "dispersion", uris) + download_controls = html.Div( + [ + html.A( + fmt.upper(), + href=uri, + download=f"{stem}.{fmt}", + className="download-button plot-download-button", + style={"width": "60px"}, + ) + for fmt, uri in uris.items() + ], + style=link_style, + ) else: download_controls = html.Div( html.A( "Download plot", href=image_src, - download=filename, + download=f"{stem}.png", className="download-button plot-download-button", style={"width": "112px"}, ), - style={ - "display": "flex", - "justifyContent": "flex-end", - "marginTop": "12px", - "marginBottom": "0px", - }, + style=link_style, ) children = [ - html.H4(label), + html.H4(label, style={"marginTop": "10px", "marginBottom": "4px"}), download_controls, html.Img( src=image_src, diff --git a/ml_peg/app/utils/register_callbacks.py b/ml_peg/app/utils/register_callbacks.py index 8d2c0d4db..0107b23da 100644 --- a/ml_peg/app/utils/register_callbacks.py +++ b/ml_peg/app/utils/register_callbacks.py @@ -2,7 +2,6 @@ from __future__ import annotations -import base64 from copy import deepcopy from typing import Any, Literal @@ -1150,55 +1149,26 @@ def register_image_download_callbacks() -> None: """ Register one generic image download callback once per Dash app. - Unlike the table download (which asks the browser to capture the live DOM), - this callback decodes a pre-rendered image already stored as a base64 data - URI in a ``dcc.Store``. The phonon dispersion plot is rendered server-side - via kaleido at analysis time, so the full-resolution export is available - without re-rendering in the browser. + The image payloads are already stored in the browser as data URIs. Keeping + this callback client-side avoids posting large phonon dispersion images back + to Dash, which can exceed request-size limits. """ app = dash.get_app() output = Output({"type": "image-download", "index": MATCH}, "data") if str(output) in app.callback_map: return - @callback( + app.clientside_callback( + ClientsideFunction( + namespace="image_download", + function_name="downloadImage", + ), output, Input({"type": "image-download-button", "index": MATCH}, "n_clicks"), State({"type": "image-download-format", "index": MATCH}, "value"), State({"type": "image-download-target", "index": MATCH}, "data"), prevent_initial_call=True, - optional=True, ) - def _download_image(n_clicks, fmt, uris): - """ - Decode the stored data URI and trigger a browser file download. - - Parameters - ---------- - n_clicks - Number of button clicks. - fmt - Selected download format (``"png"``, ``"svg"``, or ``"json"``). - uris - Mapping of format keys to base64 data URIs. - - Returns - ------- - dict - Dash ``dcc.send_bytes`` payload for the Download component. - """ - if not n_clicks or not uris or not fmt: - raise PreventUpdate - uri = uris.get(fmt) - if not uri: - raise PreventUpdate - data = base64.b64decode(uri.split(",")[1]) - mime = { - "png": "image/png", - "svg": "image/svg+xml", - "json": "application/json", - }.get(fmt, "application/octet-stream") - return dcc.send_bytes(data, f"phonon_dispersion.{fmt}", type=mime) def register_download_callbacks(table_id: str) -> None: