From 2803299b6db1d7468ee7633bf79149d4d295e413 Mon Sep 17 00:00:00 2001 From: forrestbao Date: Mon, 12 May 2025 22:16:26 -0500 Subject: [PATCH 01/16] turn on autorun when an app opens without manual trigger --- frontend/src/components/FunixFunction/InputPanel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/FunixFunction/InputPanel.tsx b/frontend/src/components/FunixFunction/InputPanel.tsx index eac00f5..9ae9b2a 100644 --- a/frontend/src/components/FunixFunction/InputPanel.tsx +++ b/frontend/src/components/FunixFunction/InputPanel.tsx @@ -56,7 +56,7 @@ const InputPanel = (props: { const [tempOutput, setTempOutput] = useState(null); const tempOutputRef = React.useRef(null); - const [autoRun, setAutoRun] = useState(false); + const [autoRun, setAutoRun] = useState(true); const lock = useRef(false); const isLarge = @@ -382,7 +382,7 @@ const InputPanel = (props: { { setAutoRun(() => event.target.checked); @@ -390,7 +390,7 @@ const InputPanel = (props: { disabled={!props.preview.autorun} /> } - label="Continuously Run" + label="Auto Re-run" /> From 79969dec26f2ed457b5ebd5c98af07fec1cc9949 Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Sat, 17 May 2025 19:31:37 +0800 Subject: [PATCH 02/16] fix: replace `NaN` for mpld3 --- backend/funix/decorator/magic.py | 86 ++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/backend/funix/decorator/magic.py b/backend/funix/decorator/magic.py index 2ff428c..b9b02f7 100644 --- a/backend/funix/decorator/magic.py +++ b/backend/funix/decorator/magic.py @@ -10,6 +10,7 @@ However, their logic is complex, with a lot of if-else, no comments and no unit tests, so it is not very good to infer the types of parameters, the types of return values and the rough logic. """ + import ast import io import json @@ -383,6 +384,21 @@ def get_dataframe_json(dataframe) -> dict: return json.loads(dataframe.to_json(orient="records")) +class NanToNull(json.JSONEncoder): + @staticmethod + def nan_to_null(obj): + if isinstance(obj, dict): + return {k: NanToNull.nan_to_null(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [NanToNull.nan_to_null(v) for v in obj] + elif isinstance(obj, float) and obj != obj: + return None + return obj + + def encode(self, obj, *args, **kwargs): + return super().encode(NanToNull.nan_to_null(obj), *args, **kwargs) + + def get_figure(figure) -> dict: """ Converts a matplotlib figure to a dictionary for drawing on the frontend @@ -409,6 +425,8 @@ def get_figure(figure) -> dict: raise Exception("if you use matplotlib, you must install mpld3") fig = mpld3.fig_to_dict(figure) + fig = json.loads(json.dumps(fig, cls=NanToNull)) + matplotlib.pyplot.close() return fig else: @@ -620,10 +638,10 @@ def anal_function_result( __ipython_display.Image, ), ): - call_result[ - position - ] = handle_ipython_audio_image_video( - call_result[position] + call_result[position] = ( + handle_ipython_audio_image_video( + call_result[position] + ) ) if single_return_type == "Figure": call_result[position] = get_figure(call_result[position]) @@ -645,16 +663,18 @@ def anal_function_result( if isinstance(call_result[position], list): if __ipython_use: call_result[position] = [ - handle_ipython_audio_image_video(single) - if isinstance( - single, - ( - __ipython_display.Audio, - __ipython_display.Video, - __ipython_display.Image, - ), + ( + handle_ipython_audio_image_video(single) + if isinstance( + single, + ( + __ipython_display.Audio, + __ipython_display.Video, + __ipython_display.Image, + ), + ) + else get_static_uri(single) ) - else get_static_uri(single) for single in call_result[position] ] else: @@ -699,16 +719,18 @@ def anal_function_result( if __ipython_use: call_result = [ [ - handle_ipython_audio_image_video(single) - if isinstance( - single, - ( - __ipython_display.Audio, - __ipython_display.Video, - __ipython_display.Image, - ), + ( + handle_ipython_audio_image_video(single) + if isinstance( + single, + ( + __ipython_display.Audio, + __ipython_display.Video, + __ipython_display.Image, + ), + ) + else get_static_uri(single) ) - else get_static_uri(single) for single in call_result[0] ] ] @@ -719,16 +741,18 @@ def anal_function_result( else: if __ipython_use: call_result = [ - handle_ipython_audio_image_video(call_result[0]) - if isinstance( - call_result[0], - ( - __ipython_display.Audio, - __ipython_display.Video, - __ipython_display.Image, - ), + ( + handle_ipython_audio_image_video(call_result[0]) + if isinstance( + call_result[0], + ( + __ipython_display.Audio, + __ipython_display.Video, + __ipython_display.Image, + ), + ) + else get_static_uri(call_result[0]) ) - else get_static_uri(call_result[0]) ] else: call_result = [get_static_uri(call_result[0])] From 6a2253b088f92d76ea0b178b71d402b40d4a9a0c Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Sat, 17 May 2025 20:58:34 +0800 Subject: [PATCH 03/16] feat: more auto-run options - `always` (str) & `True` (bool), no auto-run checkbox and run button, always run when the form changes - `disable` (str) & `False` (bool), no auto-run checkbox but only run button - `toggleable` (str), display auto-run checkbox and run button, user can manually set the auto-run option --- backend/funix/decorator/__init__.py | 10 ++- backend/funix/hint/__init__.py | 2 + .../components/FunixFunction/InputPanel.tsx | 66 +++++++++++-------- frontend/src/shared/index.ts | 2 +- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index aaa7ee5..373e6c8 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -55,6 +55,7 @@ from funix.hint import ( AcceptableWidgetsList, ArgumentConfigType, + AutoRunType, ConditionalVisibleType, DestinationType, DirectionType, @@ -232,7 +233,7 @@ def funix( rate_limit: RateLimiter = None, reactive: ReactiveType = None, print_to_web: bool = False, - autorun: bool = False, + autorun: AutoRunType = False, disable: bool = False, figure_to_image: bool = False, keep_last: bool = False, @@ -508,6 +509,11 @@ def _function_reactive_update(): app_.post(f"/update/{function_id}")(_function_reactive_update) app_.post(f"/update/{endpoint}")(_function_reactive_update) + real_autorun = autorun + + if isinstance(autorun, bool): + real_autorun = "disable" if not autorun else "always" + decorated_functions_list_append( app_.name, { @@ -518,7 +524,7 @@ def _function_reactive_update(): "id": function_id, "websocket": need_websocket, "reactive": has_reactive_params, - "autorun": autorun, + "autorun": real_autorun, "keepLast": keep_last, "width": width if width else ["50%", "50%"], "class": is_class_method, diff --git a/backend/funix/hint/__init__.py b/backend/funix/hint/__init__.py index bbf9881..55124e9 100644 --- a/backend/funix/hint/__init__.py +++ b/backend/funix/hint/__init__.py @@ -263,6 +263,8 @@ class ConditionalVisible(TypedDict): Document is on the way """ +AutoRunType = bool | Literal["always", "disable", "toggleable"] + ComponentMuiComponents = [ "@mui/material/TextField", "@mui/material/Switch", diff --git a/frontend/src/components/FunixFunction/InputPanel.tsx b/frontend/src/components/FunixFunction/InputPanel.tsx index a3a067f..ef0786e 100644 --- a/frontend/src/components/FunixFunction/InputPanel.tsx +++ b/frontend/src/components/FunixFunction/InputPanel.tsx @@ -26,7 +26,6 @@ import _ from "lodash"; import { Form } from "@rjsf/mui"; import validator from "@rjsf/validator-ajv8"; import { RJSFSchema } from "@rjsf/utils"; -import TemplateString from "../Common/TemplateString"; const InputPanel = (props: { detail: FunctionDetail; @@ -58,7 +57,11 @@ const InputPanel = (props: { const [tempOutput, setTempOutput] = useState(null); const tempOutputRef = React.useRef(null); - const [autoRun, setAutoRun] = useState(props.preview.autorun); + const [autoRun, setAutoRun] = useState( + props.preview.autorun === "always" || props.preview.autorun === "toggleable" + ? true + : false, + ); const lock = useRef(false); const isLarge = @@ -159,7 +162,11 @@ const InputPanel = (props: { }, 100)(); } - if (props.preview.autorun && autoRun) { + if ( + (props.preview.autorun === "always" || + props.preview.autorun === "toggleable") && + autoRun + ) { _.debounce(() => { handleSubmitWithoutHistory(formData).then(); }, 100)(); @@ -386,33 +393,36 @@ const InputPanel = (props: { alignItems="center" > - { - setAutoRun(() => event.target.checked); - }} - disabled={!props.preview.autorun} - defaultChecked={props.preview.autorun} - /> - } - label={theme?.funix_autorun_label || "Auto-run"} - /> + {props.preview.autorun === "toggleable" && ( + { + setAutoRun(() => event.target.checked); + }} + defaultChecked={true} + /> + } + label={theme?.funix_autorun_label || "Auto-run"} + /> + )} - + {props.preview.autorun !== "always" && ( + + )} diff --git a/frontend/src/shared/index.ts b/frontend/src/shared/index.ts index 1541724..a8bdc76 100644 --- a/frontend/src/shared/index.ts +++ b/frontend/src/shared/index.ts @@ -81,7 +81,7 @@ export type FunctionPreview = { /** * autorun */ - autorun: boolean; + autorun: "always" | "disable" | "toggleable"; /** * keep last history */ From 45f29f841087b9322603f02e67f7f60df3faa4f9 Mon Sep 17 00:00:00 2001 From: forrestbao Date: Sat, 17 May 2025 15:40:41 -0500 Subject: [PATCH 04/16] fix minor bug --- frontend/src/components/FunixFunction/InputPanel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/FunixFunction/InputPanel.tsx b/frontend/src/components/FunixFunction/InputPanel.tsx index 6aa4f8e..ef0786e 100644 --- a/frontend/src/components/FunixFunction/InputPanel.tsx +++ b/frontend/src/components/FunixFunction/InputPanel.tsx @@ -57,7 +57,6 @@ const InputPanel = (props: { const [tempOutput, setTempOutput] = useState(null); const tempOutputRef = React.useRef(null); - const [autoRun, setAutoRun] = useState(true); const [autoRun, setAutoRun] = useState( props.preview.autorun === "always" || props.preview.autorun === "toggleable" ? true From 737f5bd67deb0b0571025f3af4e2d95c36260535 Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Sun, 18 May 2025 06:47:09 +0800 Subject: [PATCH 05/16] fix: `figure_to_image` for tuple --- backend/funix/decorator/magic.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/funix/decorator/magic.py b/backend/funix/decorator/magic.py index b9b02f7..8735d9b 100644 --- a/backend/funix/decorator/magic.py +++ b/backend/funix/decorator/magic.py @@ -12,6 +12,7 @@ """ import ast +import base64 import io import json from importlib import import_module @@ -441,16 +442,18 @@ def get_figure_image(figure) -> str: figure (matplotlib.figure.Figure): The figure to convert Returns: - str: The converted image with static URI + # str: The converted image with static URI + str: The converted image with base64 """ import matplotlib.pyplot matplotlib.pyplot.close() with io.BytesIO() as buf: - figure.savefig(buf, format="png") + figure.savefig(buf, format="png", bbox_inches="tight") buf.seek(0) - return get_static_uri(buf.getvalue()) + base64_image = base64.b64encode(buf.getvalue()).decode("utf-8") + return f"data:image/png;base64,{base64_image}" class LambdaVisitor(ast.NodeVisitor): @@ -810,6 +813,13 @@ def parse_function_annotation( full_type_name ] parsed_return_annotation_list.append(return_annotation_type_name) + if figure_to_image: + for i, return_annotation_type_name in enumerate( + parsed_return_annotation_list + ): + if return_annotation_type_name == "Figure": + parsed_return_annotation_list[i] = "FigureImage" + return_type_parsed = parsed_return_annotation_list else: if hasattr(function_signature.return_annotation, "__annotations__"): From 5c7e039f01f383b43ff7d635aadee6f9c17402d4 Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Sun, 18 May 2025 07:44:15 +0800 Subject: [PATCH 06/16] fix: image size and change layout `width` to percentage --- .../FunixFunction/ObjectFieldExtendedTemplate.tsx | 2 +- .../FunixFunction/OutputComponents/OutputMedias.tsx | 10 +--------- frontend/src/components/FunixFunction/OutputPanel.tsx | 5 ++++- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx b/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx index 23ec078..a224668 100644 --- a/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx +++ b/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx @@ -594,7 +594,7 @@ const ObjectFieldExtendedTemplate = (props: ObjectFieldTemplateProps) => { if (rowElement) { rowElement = ( {rowElement} diff --git a/frontend/src/components/FunixFunction/OutputComponents/OutputMedias.tsx b/frontend/src/components/FunixFunction/OutputComponents/OutputMedias.tsx index 5fa67d1..dc30b2c 100644 --- a/frontend/src/components/FunixFunction/OutputComponents/OutputMedias.tsx +++ b/frontend/src/components/FunixFunction/OutputComponents/OutputMedias.tsx @@ -44,7 +44,6 @@ export default function OutputMedias(props: { height: "auto", maxWidth: "100%", maxHeight: "100%", - minWidth: "65%", }; return ( @@ -56,14 +55,7 @@ export default function OutputMedias(props: { {isPDF ? ( ) : ( - + )} ); diff --git a/frontend/src/components/FunixFunction/OutputPanel.tsx b/frontend/src/components/FunixFunction/OutputPanel.tsx index c0accb6..396cc05 100644 --- a/frontend/src/components/FunixFunction/OutputPanel.tsx +++ b/frontend/src/components/FunixFunction/OutputPanel.tsx @@ -451,7 +451,10 @@ const OutputPanel = (props: { itemElement = {item.content ?? ""}; } rowElements.push( - + {itemElement} , ); From 440cd58cbd2350be4ba8c5631af92aa3f419df48 Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Mon, 19 May 2025 01:40:10 +0800 Subject: [PATCH 07/16] feat: use matplotlib webagg code to replace mpld3 --- backend/funix/__init__.py | 2 +- backend/funix/app/__init__.py | 112 +++++++++++++++++- backend/funix/decorator/magic.py | 50 ++------ backend/funix/requirements.txt | 1 - frontend/config-overrides.js | 39 +++--- frontend/public/_images/back.png | Bin 0 -> 380 bytes frontend/public/_images/back_large.png | Bin 0 -> 620 bytes frontend/public/_images/filesave.png | Bin 0 -> 458 bytes frontend/public/_images/filesave_large.png | Bin 0 -> 720 bytes frontend/public/_images/forward.png | Bin 0 -> 357 bytes frontend/public/_images/forward_large.png | Bin 0 -> 593 bytes frontend/public/_images/hand.png | Bin 0 -> 979 bytes frontend/public/_images/help.png | Bin 0 -> 472 bytes frontend/public/_images/help_large.png | Bin 0 -> 747 bytes frontend/public/_images/home.png | Bin 0 -> 468 bytes frontend/public/_images/home_large.png | Bin 0 -> 790 bytes frontend/public/_images/matplotlib.png | Bin 0 -> 1283 bytes frontend/public/_images/matplotlib_large.png | Bin 0 -> 3088 bytes frontend/public/_images/move.png | Bin 0 -> 481 bytes frontend/public/_images/move_large.png | Bin 0 -> 767 bytes .../public/_images/qt4_editor_options.png | Bin 0 -> 380 bytes .../_images/qt4_editor_options_large.png | Bin 0 -> 619 bytes frontend/public/_images/subplots.png | Bin 0 -> 445 bytes frontend/public/_images/subplots_large.png | Bin 0 -> 662 bytes frontend/public/_images/zoom_to_rect.png | Bin 0 -> 530 bytes .../public/_images/zoom_to_rect_large.png | Bin 0 -> 1016 bytes frontend/public/index.html | 7 ++ .../OutputComponents/OutputPlot.tsx | 61 +++++++--- .../components/FunixFunction/OutputPanel.tsx | 3 +- pyproject.toml | 1 - 30 files changed, 198 insertions(+), 78 deletions(-) create mode 100644 frontend/public/_images/back.png create mode 100644 frontend/public/_images/back_large.png create mode 100644 frontend/public/_images/filesave.png create mode 100644 frontend/public/_images/filesave_large.png create mode 100644 frontend/public/_images/forward.png create mode 100644 frontend/public/_images/forward_large.png create mode 100644 frontend/public/_images/hand.png create mode 100644 frontend/public/_images/help.png create mode 100644 frontend/public/_images/help_large.png create mode 100644 frontend/public/_images/home.png create mode 100644 frontend/public/_images/home_large.png create mode 100644 frontend/public/_images/matplotlib.png create mode 100644 frontend/public/_images/matplotlib_large.png create mode 100644 frontend/public/_images/move.png create mode 100644 frontend/public/_images/move_large.png create mode 100644 frontend/public/_images/qt4_editor_options.png create mode 100644 frontend/public/_images/qt4_editor_options_large.png create mode 100644 frontend/public/_images/subplots.png create mode 100644 frontend/public/_images/subplots_large.png create mode 100644 frontend/public/_images/zoom_to_rect.png create mode 100644 frontend/public/_images/zoom_to_rect_large.png diff --git a/backend/funix/__init__.py b/backend/funix/__init__.py index ee0cf21..3aac867 100644 --- a/backend/funix/__init__.py +++ b/backend/funix/__init__.py @@ -18,7 +18,7 @@ import funix.decorator.theme as theme import funix.decorator.widget as widget import funix.hint as hint -from funix.app import app, enable_funix_host_checker +from funix.app import app, sock, enable_funix_host_checker from funix.config.switch import GlobalSwitchOption from funix.frontend import run_open_frontend, start from funix.jupyter import jupyter diff --git a/backend/funix/app/__init__.py b/backend/funix/app/__init__.py index 8287a26..5926d04 100644 --- a/backend/funix/app/__init__.py +++ b/backend/funix/app/__init__.py @@ -7,6 +7,7 @@ import os import re from datetime import datetime, timezone +from io import BytesIO from pathlib import Path from urllib.parse import urlparse from uuid import uuid4 @@ -22,6 +23,8 @@ from funix.frontend import start from funix.hint import LogLevel +from json import loads + app = Flask(__name__) app.secret_key = GlobalSwitchOption.get_session_key() app.config.update( @@ -32,6 +35,113 @@ app.json.sort_keys = False sock = Sock(app) +matplotlib_figure_manager = {} + + +class MatplotlibWebsocket: + supports_binary = True + + def __init__(self, manager=None, ws=None): + self.manager = manager + self.ws = ws + if self.manager is not None: + self.manager.add_web_socket(self) + self.supports_binary = True + + def on_close(self): + self.manager.remove_web_socket(self) + self.ws.close() + + def set_manager(self, manager): + self.manager = manager + self.manager.add_web_socket(self) + + def set_ws(self, ws): + self.ws = ws + + def on_message(self, message: dict): + if message["type"] == "supports_binary": + self.supports_binary = message["value"] + else: + self.manager.handle_json(message) + + def send_json(self, content: dict): + self.ws.send(json.dumps(content)) + + def send_binary(self, content: bytes): + if self.supports_binary: + self.ws.send(content) + else: + data_uri = "data:image/png;base64," + content.decode("utf-8") + self.ws.send(data_uri) + + +matplotlib_figure_managers = {} + + +def add_sock_route(flask_sock: Sock): + @flask_sock.route("/ws-plot/") + def __ws_plot(ws, figure_id: str): + """ + WebSocket route for plot. + + Routes: + /ws-plot: The WebSocket route for plot. + + Parameters: + ws (Any): The WebSocket. + + Returns: + None + """ + if figure_id not in matplotlib_figure_managers: + manager_class = MatplotlibWebsocket( + matplotlib_figure_manager[int(figure_id)], ws + ) + matplotlib_figure_managers[int(figure_id)] = manager_class + else: + manager_class = matplotlib_figure_managers[int(figure_id)] + manager_class.set_ws(ws) + while True: + data = ws.receive() + if data is not None: + dict_data = loads(data) + manager_class.on_message(dict_data) + + +def add_app_route(flask_app: Flask): + @flask_app.route("/plot-download//") + def __ws_plot(figure_id: str, format: str): + """ + Download the plot. + + Routes: + /plot-download: The route to download the plot. + + Parameters: + figure_id (str): The figure id. + format (str): The format of the plot. + + Returns: + None + """ + buffer = BytesIO() + manager = matplotlib_figure_manager[int(figure_id)] + manager.canvas.figure.savefig(buffer, format=format) + buffer.seek(0) + buffer_name = f"plot.{format}" + return Response( + buffer, + mimetype="application/octet-stream", + headers={ + "Content-Disposition": f"attachment; filename={buffer_name}", + "Content-Length": str(len(buffer.getvalue())), + }, + ) + + +add_sock_route(sock) + def funix_auto_cors(response: Response) -> Response: if "HTTP_ORIGIN" not in request.environ: @@ -63,7 +173,7 @@ def get_new_app_and_sock_for_jupyter() -> tuple[Flask, Sock]: SESSION_TYPE="filesystem", ) new_sock = Sock(new_app) - + add_sock_route(new_sock) new_app.after_request(funix_auto_cors) start(new_app) diff --git a/backend/funix/decorator/magic.py b/backend/funix/decorator/magic.py index 8735d9b..6a48ed1 100644 --- a/backend/funix/decorator/magic.py +++ b/backend/funix/decorator/magic.py @@ -16,7 +16,7 @@ import io import json from importlib import import_module -from inspect import Parameter, Signature, getsource, signature +from inspect import Parameter, Signature, getsource, signature, getfile from re import Match, search from types import ModuleType from typing import Any, Callable @@ -36,6 +36,7 @@ get_function_uuid_with_id, get_class_method_funix, ) +from funix.app import matplotlib_figure_manager __matplotlib_use = False """ @@ -43,10 +44,10 @@ """ try: - # From now on, Funix no longer mandates matplotlib and mpld3 + # From now on, Funix no longer mandates matplotlib import matplotlib - matplotlib.use("Agg") # No display + matplotlib.use("WebAgg") __matplotlib_use = True except: pass @@ -67,12 +68,6 @@ pass -mpld3: ModuleType | None = None -""" -The mpld3 module. -""" - - def get_type_dict(annotation: any) -> dict: """ Get the type dict of the annotation. @@ -385,21 +380,6 @@ def get_dataframe_json(dataframe) -> dict: return json.loads(dataframe.to_json(orient="records")) -class NanToNull(json.JSONEncoder): - @staticmethod - def nan_to_null(obj): - if isinstance(obj, dict): - return {k: NanToNull.nan_to_null(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [NanToNull.nan_to_null(v) for v in obj] - elif isinstance(obj, float) and obj != obj: - return None - return obj - - def encode(self, obj, *args, **kwargs): - return super().encode(NanToNull.nan_to_null(obj), *args, **kwargs) - - def get_figure(figure) -> dict: """ Converts a matplotlib figure to a dictionary for drawing on the frontend @@ -411,25 +391,17 @@ def get_figure(figure) -> dict: dict: The converted figure Raises: - Exception: If matplotlib or mpld3 is not installed + Exception: If matplotlib is not installed """ - global mpld3 - import matplotlib + from matplotlib.backends.backend_webagg import _BackendWebAgg - if __matplotlib_use: - if mpld3 is None: - try: - import matplotlib.pyplot + new_fig_manger = _BackendWebAgg.new_figure_manager_given_figure + manager = new_fig_manger(id(figure), figure) - mpld3 = import_module("mpld3") - except: - raise Exception("if you use matplotlib, you must install mpld3") + matplotlib_figure_manager[id(figure)] = manager - fig = mpld3.fig_to_dict(figure) - fig = json.loads(json.dumps(fig, cls=NanToNull)) - - matplotlib.pyplot.close() - return fig + if __matplotlib_use: + return {"fig": manager.num} else: raise Exception("Install matplotlib to use this function") diff --git a/backend/funix/requirements.txt b/backend/funix/requirements.txt index 6457a66..871a5af 100644 --- a/backend/funix/requirements.txt +++ b/backend/funix/requirements.txt @@ -1,7 +1,6 @@ flask>=2.2.2 functions-framework==3.* requests>=2.28.1 -mpld3>=0.5.8 plac>=1.3.5 gitignore-parser>=0.1.9 flask-sock>=0.7.0 diff --git a/frontend/config-overrides.js b/frontend/config-overrides.js index 5c15a1a..3d4367c 100644 --- a/frontend/config-overrides.js +++ b/frontend/config-overrides.js @@ -4,14 +4,16 @@ const webpack = require("webpack"); const scripts = process.env.REACT_APP_IN_FUNIX ? ` - - + + + ` : ` - - + + + `; module.exports = function override(config) { @@ -19,9 +21,9 @@ module.exports = function override(config) { config.plugins.push( new webpack.DefinePlugin({ "process.env.REACT_APP_MUI_PRO_LICENSE_KEY": JSON.stringify( - process.env.MUI_PRO_LICENSE_KEY + process.env.MUI_PRO_LICENSE_KEY, ), - }) + }), ); } @@ -42,26 +44,29 @@ module.exports = function override(config) { config.plugins.push( new SaveRemoteFilePlugin([ { - url: "https://d3js.org/d3.v5.js", - filepath: "static/js/d3.v5.js", + url: "https://code.jquery.com/jquery-3.7.1.min.js", + filepath: "static/js/jquery-3.7.1.min.js", hash: false, }, { - url: "https://mpld3.github.io/js/mpld3.v0.5.8.js", - filepath: "static/js/mpld3.v0.5.8.js", + url: "https://cdn.jsdelivr.net/gh/matplotlib/matplotlib@v3.10.x/lib/matplotlib/backends/web_backend/js/mpl.js", + filepath: "static/js/mpl.js", hash: false, }, { - url: "https://code.jquery.com/jquery-3.7.1.min.js", - filepath: "static/js/jquery-3.7.1.min.js", + url: "https://cdn.jsdelivr.net/gh/matplotlib/matplotlib@v3.10.x/lib/matplotlib/backends/web_backend/css/fbm.css", + filepath: "static/css/fbm.css", hash: false, - } - ]) + }, + { + url: "https://cdn.jsdelivr.net/gh/matplotlib/matplotlib@v3.10.x/lib/matplotlib/backends/web_backend/css/mpl.css", + filepath: "static/css/mpl.css", + hash: false, + }, + ]), ); } - config.plugins.push( - new ProgressBarPlugin(), - ); + config.plugins.push(new ProgressBarPlugin()); return config; }; diff --git a/frontend/public/_images/back.png b/frontend/public/_images/back.png new file mode 100644 index 0000000000000000000000000000000000000000..e3c4b5815487bc1e5fef303915d494e3aaf015d4 GIT binary patch literal 380 zcmV-?0fYXDP)lw*$|-oI|U_b17q^aQv&DFDM~=zfg_+(tx&Flxb0G?KS{z~cHJ7vY6+{0qkG}=XVCaDedS)0e;&I@e*ofzAad+grkG-AQ z8TUQ#2XC{xv-3RrF|*4U5uumUmo`AsybGkAo)`EKx&vd(Ctx0U0Dg-|*=q+P60Cql z;0cgg6@jl_`v?f1Z2)hqGBBfBAOgN#_rMpG0>E+?_<9|x6R?1atV;qA^RgkMb)K9PV1cxM4E@J<52nihC#0d4SB0vh2(1XF{Hj4>t$eD8YT z>y&^$mTlUl0vrKbBJ$X>H^!CEVSnXA)Qe<_VIbJ#yNq4T+UL)1U< zQa}*b^fzya68IPp#9N{S_TG#lep-giD1lAjBpi!ViY;5Zj6xaUnDj><0q%f7>zP=I z#3k@8$xNI9_JOIc2menASO6|8+O$grD2T{&+r1XwLK&*>gw2)v2TZ8=m`f-zu0o(D zG3+4m7Z_6^&>(TDLBhSEo5VFRszN}EMDi0?K@^3*8-lSB1i>%tY{WuPt8}SSSZZtIJJ{I>f{Nk`*a=oav<+IM5)_qyKgS}oxURF=WN`x? z80K>K&N*}M{H;kMOXMq1Ei+qlcF`9nsbOaKz(kV35wHr3Mf~Hg zZv&^mrDv=NW#|=<)C6W?#QQ?#SH0x$iEP)og!G+h}dXhC02s}fT`{5G#Fng77`E(e}GzA2$82E>Vx=WEbb)x z*1fykyZ5qpa9>#1otxR2@69s1+@7c^O~zR40IH%T5F3pru+1eDk!j!zuxEt6e*y1- z6;z=%nKXperILRP~F8j|7ekTxRpFJhPtlDSngBovBWUzcBQ!0{ehf z%YKc3OpKxmea(1LYClueb>K9xVu*5=s0{(g zC9x_ZQ!blY5r9k*3&3@ke(ear0Ew<4z91qKw(ckhfZwTTFR*CqIS+Fc zaa2TZ1K%w9PT)w$M5_8hMBV|1Qqg`}CzL=Rm@LrQ4qWk^%Lf}qf82f;x`gfq>Sv(` zJQ~JCp&7UYd<2e{u>FV6z)e+sQ__9_K|!SFnH$4!JS^0*jE0~k5F0Il*a)gZA~H_$ z1))kA&B9OM32;qS|M(h(25|>CA7~STW55pJqOU>d5?Tt>FM}W-p&2mGLCe6~K>I8l zW;3%zXRQHLS;zNOwOg!K3=ug=$)(x&D%28)jg~-cv;<)RQ5G|1-|~4dBR0TIpl9?aNw#?x4tR$bhOr!4nJ zFmC{1M)f27YZ(|A**#qxLp(aSPOqciXWh~2lYjQzoI6u}mDlXTYpZs%D=2MODtfNA zA}i>C$PFego9Lo{MZC|xA9$A#_iCx9L!e=~qfUeL18wo7w+9Wxlk&xS8uImz@SJEZ zX||MSmOoIMw8?tn?{GoK(EC~686sS?KCj}~uW;u{+@hlFGl!fxZkVz}%%9T~Z}RYm z%#&BbJ9_*C3;4eLzsPVS;_Tk)0@gJ)&*u4_Gupn-Rea~Oumi#CDogp3J_Z+WXORmj zoqfja+k?B&m710kD$Xu)H~U}3gr4=zWP5T<|IZn|3+&ULJOhRkgQu&X%Q~loCIAhH Bj}HI< literal 0 HcmV?d00001 diff --git a/frontend/public/_images/forward_large.png b/frontend/public/_images/forward_large.png new file mode 100644 index 0000000000000000000000000000000000000000..de65815bba205d862508648dc1fe0d0ed2b058a3 GIT binary patch literal 593 zcmV-X0BL>zt zNMS2Tilke}6WFH`B#pv8c6<`Zk0zRwiSL004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00004XF*Lt006O$eEU(800001 zb5ch_0Itp)=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01m_e01m_fl`9S#0009j zNkl^l2+K1CW>GPt=!n6RxJh#F>2i(&?cf~5N(R6 zO@!EH4BECxszoJAkU@%;VOkpKXfyg|-p9S~cHXEnlOJ!KMRec|mwWG=-#NegJLdrW zVU`CBG)hF#07GMmE|B6XE*m^UlyV7w zANO~MM-K!Li+xskWBoM;WKS_vP#~|^_r?3sXseaK@7w9zI$V2Q5;#*V*An1}2>By@ zy_eq(4?oCK#_}(@k>k~_y&ULfn!TBuZdx`|fogv!dG2WI>~20thuK(n1y_%9nQhtA zv9%5_TIS=6!DW?JN`E<9O|1t!9`(HIYZn0KIN+@}A25Z1k1?Q(deh}N{eJ-ry<2UQ zAe%k?69R#X0F;!eKq)OCei9iPjW^^npwM3E5q&-a!RiqBEBv4-j2Esj(vf>)3N5Y- zwk>I-m4*1vAf+s`%a9VAQ6*3rK-oLsFhw3fw#Rb-jy!~j*~1sJpPDyH)N+?|EvP|N zO$frZVJLC=ClqwY(wSo+Ow*(7-7^`NWV7noUg)c4s$5Z(%vx9i zER)0fZylWjScU~e3t)r@W^-a%T(KRLIFilMX?3SBwn-OF;3L;;Z`Np`M0`UJa+MNk zU@e*{NG+>S8mgq$`WS%PZEPw-KlH{rA!imnAosmo} z%~egTB#sFofDy_sMNWV8)+(LOstBW)I!HUPObfWhL15H{O-yD1)P<#S?iEZ29eu4{+ z2}V9(^dq}^k&Eb***9_dW3N9pRyQQ-P;I5QZPoaL>-$lIg?KlA!tEM3gA>2XX^~L}I002ovPDHLkV1j<( BsGtA< literal 0 HcmV?d00001 diff --git a/frontend/public/_images/help.png b/frontend/public/_images/help.png new file mode 100644 index 0000000000000000000000000000000000000000..a52fbbe819e2fcbb1c1d5f8f143fc8d069688b3a GIT binary patch literal 472 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEX7WqAsj$Z!;#Vf4nJ zFmC{1M)f27Yk`76C9V-A!TD(=<%vb947rIV1v&X8IhjccWvNBQnfZANMtTN%MtTML z#U&B&jPiK&SODM>mC86_nJR{Ht~oqG92>H6!I&RfjDz$oMC;uzx5 z`F6@iFJ?y(*Ymt}PEDaloIF?8a{Q9ydQ{(WW636gEnW(~S@jMA{|{;XY-~sp7Sx?) z(_PD*cq7Fqn~5u~VnhD4>hlKqn7%&t$}w@9Jpe7mLmAJ!PXuc{1r zO&`P;h<;ugBl>m0`&l*Y?r2IG3O2hD$$M@uho zwR_=Yy?RF7@zS@alxkN7wZ*W{x+i5|wE6s2##OJR_vU^TTHB|58yE`=p00i_>zopr E02Lj!oB#j- literal 0 HcmV?d00001 diff --git a/frontend/public/_images/help_large.png b/frontend/public/_images/help_large.png new file mode 100644 index 0000000000000000000000000000000000000000..3f3d4dfed7e99421bd024c14046bf4616b4ca2ff GIT binary patch literal 747 zcmVqZlEAow3ozPGS*Kh@=uh zt29YU!QR5c(o#^cu@D3i!6G7pMU;S|f`x@3Xc1C~VANnE8bvE}zs2r?guR>H+qXM8 z*dHDo`)=m{|CxC+vulzVWDLa?P!+9#*k}#JMr$B8S_84s8i^wG?Y0}H@YNgIJCvw{4NvH}_j zl94@6cqh8lSPhgAKTEP3g~V?H2e*v@l5O#R0pVHPDa}!%`28eBT~z!tV02p;0Coef zN{GKl)0eq`feQtNuLEqY{x&dHmCyZ^5;y`J2>j=Xq_ZAmb-dJ%1@eD@ zb6#Y182BB?zmc@&MdrSQB)v1UtH7b0bO*TZ$!BTd4J_*TF0_kelqYi1*?s?(8=v&002ovPDHLkV1l@aH3$Fz literal 0 HcmV?d00001 diff --git a/frontend/public/_images/home.png b/frontend/public/_images/home.png new file mode 100644 index 0000000000000000000000000000000000000000..6e5fdebb357c5fe4e87fa01756bfe33b40ac25f4 GIT binary patch literal 468 zcmV;_0W1EAP)K)UivJQ4|L7-$^6IB{#>G znj(i*AxbjZYH!P-$Oiip8eAF*YP0?UA#{nB=9&VhYOWzEE!#}X)8G|u@2B7QS_%mr zIPl(cIM4IkbIyAsGaaNIIZ*Wf1c!?tA}&P4$B6hG5tpm@hlt3C*o=sYj>63B>9~On zWbq%EF1asN{7Oe{58ysLtN|8yP|3t=W(}EdJkh*s$%V0gz>7cZ*T@r8$gL?IE}?BHr15?H>+VjFW`$ifM+FO z6!Yc$6L&fYY_02~s_;I};ZqmzZC@#Ks)zps{WZaG>E}8oaUI`VfN%Q-rf>~wrNtw^ zZBay=#bCMSOMhmUY^`lyX1>C%LvE|Q=^b0tzXx@*cu~H`aJx;>UI<=i<{U)C(Y6Ht z&TG1#1gn36_q7?=@v)1DQy6N5-)H7Z$q(a1BfplJZ(9-Ur~LxE8&S2c-0X$`0000< KMNUMnLSTaA*3XUr literal 0 HcmV?d00001 diff --git a/frontend/public/_images/home_large.png b/frontend/public/_images/home_large.png new file mode 100644 index 0000000000000000000000000000000000000000..3357bfeb9011141ebce16febf6539f610f12ecc9 GIT binary patch literal 790 zcmV+x1L^#UP)ZDa=0MrI&wWCqek zW*}`;#ej%3iO3BR`DUtLM3y+XgCe4_niP?4uV$(mw1D-%b3kJ?3bb1I?<`yeF8K5d zdIv-#2RsB?EX(VFVG-FKvS%;@B61wK2{b0q76Sv`)|!Elnj0VrYpnftB`1vw~$4Bst87Eh` ze*#Cm4G@LwilgCGqOdjsw<;80T>Zd&lEZxos82KiECmJ~SL(Fkv;a>6iZ9d%u*Tet zvkuN3U{R8RPRl{Xv)YdP-6cf50d|%McSn+eI-)nq0{Inx!+IF_QAP2E`VJg&g!>vG zr82M(c#H|F*>|)C!sFF#dN&v8X^88sCk9ph0&I=#Zj1A3fK~ui9b>AVKgd4n)5eq$ z3V^pnA2~L(1It1-tA>H6s@i2+5RnJKj*!h}{0Lz>%07*qoM6N<$f|>C}$p8QV literal 0 HcmV?d00001 diff --git a/frontend/public/_images/matplotlib.png b/frontend/public/_images/matplotlib.png new file mode 100644 index 0000000000000000000000000000000000000000..8eedfa7cc8fc7d68be0e1781421f845ef4f54db0 GIT binary patch literal 1283 zcmV+e1^oJnP)KUMsYHnhAA*w2*)Hms87jo^amzGnL<1Y!95F*iMi>j-)=YZ^ z*NzP67Y599ForTl(r#T{SGux${2^Ofc3Z=fyve6CiCzC@)`6|PvZVo`c}ddz+x5I}c# z_ce#ZVX<1R)f~sU1c`_!W&sczjYgBp7$y`)pU7%GEoWW+B#5IXIoqWBBF3OOf1V+|_mSrtG z&r?Iy$8>WeLsWw4b>#DpVhFDe^v$S>=Xo-jOdZjcObZx|MpHN(o(??l{_AvWOO!x) zQ4E=>x`@8|@mVr=9-2{!!C+ux1PTfYly0}%8yy%LAE)ZgTH=fE2&~T`p67{(=yHEA zHT=4ay2l@|qZeThP$kVhZ>EU6SlI}hXN_)-8scBCJDy}tth zezrGYdH!vj`XL*}P2bLF9ECz5*XeY{0FcBmOo~dSDg=Oz6Sb(_t%0Sr9z4$@YvF@f z`d&4-u?0}1-h$_slOQ4laUS;L^RVuveUL~bg59ZBs|y*1k>01(YM-yHtSpsCBsl*Q zi;a1mm=ijQmR1X{-3TF1_bU2%IYO5>+)TP3K1nkE9&5of5ADISoTmiI0U$FoGu>vh z*+jXyxvJFEQ~*Fy_zwU;mYlFb(}Uqa51PI_gp>!?;>OZaj9jqdqCJe9>`lmfdR1I< z07y$qgHowfi>9uBBocv`d@(^8slYmo6M>);=E3D?>Aj9CX9lrY@i2~?cj4Ig+Y^e& zoO-SzZ`G%p@ zYF`{39TmL(o7nBNaeoyJ`$vd~sN%?0+SRm^&UCiY!I$S#9;q)fKd<;))+p8ef}D}6={R{1062(bj9R491aT+g^~ckYPD8- zJf7GVx(jFjJd5-tX`lr>t`A-Z!!X#es~(M$d00RD0!F@j1LlTr00173$H#G84S@ef z1;=r&`uh6XiHV800;+5kWX<#N=+$f_OOgQq(v%de`lJCT#+G2^oFVkL)xhufqpq&* z;Isjez_`9w*gION>b2!`$$6GombE}=T+Fad5VzpXbn4h1&cxqlvArJ__ZnqCO91aV|aW(NwKd!)Z0K-T% t8qG?jQmKx9wzym_hs|a?P5<+3`4^p@EBvCxN5=pF002ovPDHLkV1lx`QPKba literal 0 HcmV?d00001 diff --git a/frontend/public/_images/matplotlib_large.png b/frontend/public/_images/matplotlib_large.png new file mode 100644 index 0000000000000000000000000000000000000000..c7dcfe6cb8255b332d9c6d15d8049ec63c3141e5 GIT binary patch literal 3088 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 XUOpjj zNu`1|mKJG6v9L{H3jc_Jwz=L?>;*xH9}vXG+JC^p3lbF-tx`mb;EAY-uf^_}z1+?2 z-p=fz4yKM-Ng z%)S*h_Kn1!fyW;9>ptWGzY_qEG!2{tTJh8paM!VX1?a`Ue*hoM?3a&!?^MFf_JMbU zXGx1G%RidgD_@^UG(MU8FQ9Iu3Xs&6^hi=)(v7M}N$N`aD(Rl2R_fvjBJ3V8l_R)a z#2vVna@fx?LBt+=0+QN9_xqK^^fVly3b!Z;u zfj=j30(edRyP9|OfX+}wMs#~LK|~KYKDHv0v;aH<&N`0m8d0Y$4d;Nau&CLV)8zywYj?s zy!5bt?%Uk;5)fZ^mnjw_HVM%<@cj=KOvT40W`%r*_{fAJf{_m@Ag73+KomvK-Qo}{_6I=X z086r(Z@_{_5x#(g;6UK85DXHx5`lv+Soj3O>b;0gVYWM^UFf{zn)fE>w0V=JvQ5q! zWTgWGNn2S3z*$bgS|1Qe7r?02HgMUC0C)pV68%XpOSXLtT%~5c2qcYx!z_XTe+1V5 zB#=}{I+Zl+2)_f4<8u$TNd+i?d&(Q}3=E^$7?_1wm0O?)^CMulQBZ?BmbAfZoMk}+ zyvl1=cv8>;Po)vQBxrzt0tdkRs_+HDFYvuE->gYm;okzv>)Hs94p)*+aNFg_&h}i= zl%JXb`?jB=9=4~Fwt)}Zb2?VMgY(pr|0fHA(Aek;ZW{qFz)jcJUA08H&}=1OC#xX$ aZ_5|SEVtE#bgR$+0000h3!~Ebq?|g_u;2X8G zB$B{)3dF!WNF;-A6-WU;MItHu4FTXmD)<(O4l}{@~VHC!H&vZ@7hLTB2 zc4DKHk{BCX+hJ{Iq(U+M(9)8+-+21K4-+P)t_Wz*5QzZkkdfk`G`Wc=s}T1Lopvlc2nR zv>@3edL(rK3qaASK7d^_dx;olDI2H|9qekHtxwZ+pq#RZqZnPEq`H$&%xne1E$RkJ zDFYN^bOrDGo~AZqix!EtXzj;XB&iEnCOqVAGrNu0{_1-msVeEt!=WU7CxFkQal%7> z0;XGZC16Zae}dmfGdl*_E;|G2z&Y8LUM(_YNmIapm&F6H35>R6Hwg?TRB&%*2cZhu n&~XQN^>B(K#-CYsxxdMuDB^0ix|8Qv00000NkvXXu0mjf9r?Kn literal 0 HcmV?d00001 diff --git a/frontend/public/_images/subplots_large.png b/frontend/public/_images/subplots_large.png new file mode 100644 index 0000000000000000000000000000000000000000..4440af1759ee448e56727ad09e6861eab94ecca5 GIT binary patch literal 662 zcmV;H0%`q;P)8C$X}$ zvrw=TML}f2+D0raECM2GW2GRb5p41c>}<5r#wJaO5G?!&7RJItj2g7q4HmCmST{2} z=g!@oVdFkIZ6Oc3~sVm7PV@=Ze zaV1-rQ_7oI(mHUcLbhnTla*Bhsat4oKr>S(k-F>}3`n{L+%3@Fj+zef$YsIy>;Ax& z2>>U63kC72trPLc<<($d;!mNX6<7ft7Q{a%_?CI(@_sOodJ1)^F4A|lFD52do764z zbwrwxk-7qBfX9@t!gt`l?X?l)7Rzb+N!c<*r_flDv=i7@a(Tn{pA^4FS3pt+_&sFw zB;85!Y_bLW4^9`bkwc^ zNpFFx6|xQ6WBt*UQvymE2mk;8 literal 0 HcmV?d00001 diff --git a/frontend/public/_images/zoom_to_rect.png b/frontend/public/_images/zoom_to_rect.png new file mode 100644 index 0000000000000000000000000000000000000000..12afa252481c3d00f5486cca5d9016638c7c0e9a GIT binary patch literal 530 zcmV+t0`2{YP)8Ha8aOF6wg$BR?&~V_ns>4fo?;YnS&{<@#;DF~qSERvU(AJRp zYiW0e>B5qsm~6>_q$^XDX6DzXGlu_L72o6Oz{g69e{vC14&{ zp>@MU%r!6_yX^8zVlvmHsK0LuerdR?*2!*wHj}bL6z~k#0=@zVz!zWxm;gEw-0L!v z%2A9Q*U;n!81<{-zrxHmY5iw3lTtVEB7J#p3Cz&C8^3@dGegOXdy8bvG}4862fM!j UB#x~4YybcN07*qoM6N<$f@tI9jQ{`u literal 0 HcmV?d00001 diff --git a/frontend/public/_images/zoom_to_rect_large.png b/frontend/public/_images/zoom_to_rect_large.png new file mode 100644 index 0000000000000000000000000000000000000000..5963603bb681f76a1f2ce3e3fc120b5a089c27fa GIT binary patch literal 1016 zcmVRRoj*g}@Ln zAR@g<@f+(#T?0y~3BVel0jLM&^LK~z0b3@L&uAdw_ybH2^dMUlJA{jEiwhjJ5dKCXBm~ zQmP8L3zTF{7>S4I1^NiRxbuP8=-p`vFv)@Al!&Z%$Qubm&ljY{*m`XkI1W^YjA9AU z40MI?39vemDFo~cDbx)t_9_J9fx{uZy};C127of)howk6P#SsB!IlschvFL8Wg&kX zM|_~hc1w)`U}7W#KnnO^AzvC-e7a5^aLZC>Q&a;hEu`m?5Fe;jWvSKX%9DAWUW6Gf zmNsvQ$TQ%&p}ATqRqVo(X`s%K*DWG99ku!3gyHvmpvHwD(?G2u@1Cn7Pu#b(nL2X> z{uoe7rGUwXyeA%_eDD(ZZfGlYAqb8@p(Ss?Rgn)wWEl8rXfAT$2pSl)C~}2TY9{)MkqsYRIQ|$Ay*WQM~JRA)gP0}Cz00vwBD0)5(lw(wu};xiPv5@O;k9(VQt zZ&Qf)VLaa#nF`GaDI~xfV3o^g0Oi1u5IzA;dUuMY=sJgjf__-v0W3>z0x9(2R0V7R z&I3Okx%z>IL=2=&>~<`iWO$iynYTG@!gcX&#e6=DsR^GQq#AgfkhnqM0Ps4l3Eyq7 z0N4z4#t?o6n3v|Oh-<>jK){FsmID`no)7`U=tt6R!0aqs6%HL@G!gj|QxHn2GW6AW zCQyui82N<062B6W{*eEdqu)@>v7`+EO(Jr8#II3CU^D`K*tv{EUF<0|$|gEOOx#F( z3MGm3bh-x2wWQ^X$Pnb7dLd3}c0000 + Funix diff --git a/frontend/src/components/FunixFunction/OutputComponents/OutputPlot.tsx b/frontend/src/components/FunixFunction/OutputComponents/OutputPlot.tsx index 63a2434..c03e9b8 100644 --- a/frontend/src/components/FunixFunction/OutputComponents/OutputPlot.tsx +++ b/frontend/src/components/FunixFunction/OutputComponents/OutputPlot.tsx @@ -1,24 +1,16 @@ -import { useLayoutEffect, useRef } from "react"; import { Box } from "@mui/material"; +import { useRef } from "react"; + +type PlotCode = { + fig: number | string; +}; export default function OutputPlot(props: { - plotCode: string; + plotCode: PlotCode; indexId: string; + backend: URL; }) { - const drawLock = useRef(false); - - useLayoutEffect(() => { - if (drawLock.current) { - return; - } - if (document.querySelector(`#plot-${props.indexId}`)?.innerHTML === "") { - const plot = JSON.parse(props.plotCode); - // @ts-expect-error: i got mpld3 here - mpld3.draw_figure(`plot-${props.indexId}`, plot); - drawLock.current = true; - } - }, []); - + const lock = useRef(false); return ( -
+
{ + if (ref) { + if (lock.current) { + return; + } + lock.current = true; + const websocket = + (props.backend.protocol === "https:" ? "wss" : "ws") + + "://" + + props.backend.host + + "/ws-plot/" + + props.plotCode.fig; + + // @ts-expect-error that's good here + new mpl.figure( + props.plotCode.fig, + new WebSocket(websocket), + (figure: any, format: string) => { + window.open( + new URL( + `/plot-download/${props.plotCode.fig}/${format}`, + props.backend, + ), + ); + }, + ref, + ); + } + }} + /> ); } diff --git a/frontend/src/components/FunixFunction/OutputPanel.tsx b/frontend/src/components/FunixFunction/OutputPanel.tsx index 396cc05..68c7e24 100644 --- a/frontend/src/components/FunixFunction/OutputPanel.tsx +++ b/frontend/src/components/FunixFunction/OutputPanel.tsx @@ -262,8 +262,9 @@ const OutputPanel = (props: { case "Figure": return ( ); case "Dataframe": diff --git a/pyproject.toml b/pyproject.toml index 2f7c94f..469f279 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ dependencies = [ "flask-sock>=0.7.0", "SQLAlchemy>=2.0.23", "matplotlib>=3.4.3", - "mpld3>=0.5.8", "pandas>=2.0.3", "docstring_parser>=0.16", ] From 1f3aee56e87435348f9c77498cb06824014c943f Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Mon, 19 May 2025 06:21:24 +0800 Subject: [PATCH 08/16] fix: use `tight_layout` to fix padding --- backend/funix/decorator/magic.py | 3 +++ backend/funix/requirements.txt | 1 + pyproject.toml | 1 + 3 files changed, 5 insertions(+) diff --git a/backend/funix/decorator/magic.py b/backend/funix/decorator/magic.py index 6a48ed1..01f564b 100644 --- a/backend/funix/decorator/magic.py +++ b/backend/funix/decorator/magic.py @@ -393,8 +393,11 @@ def get_figure(figure) -> dict: Raises: Exception: If matplotlib is not installed """ + import matplotlib.pyplot from matplotlib.backends.backend_webagg import _BackendWebAgg + matplotlib.pyplot.tight_layout() + new_fig_manger = _BackendWebAgg.new_figure_manager_given_figure manager = new_fig_manger(id(figure), figure) diff --git a/backend/funix/requirements.txt b/backend/funix/requirements.txt index 871a5af..ab2fd28 100644 --- a/backend/funix/requirements.txt +++ b/backend/funix/requirements.txt @@ -8,3 +8,4 @@ SQLAlchemy>=2.0.23 docstring_parser>=0.16 matplotlib>=3.4.3 pandas>=2.0.3 +tornado>=6.4.2 diff --git a/pyproject.toml b/pyproject.toml index 469f279..a5fa976 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "matplotlib>=3.4.3", "pandas>=2.0.3", "docstring_parser>=0.16", + "tornado>=6.4.2" ] [project.optional-dependencies] From fff751e9253f872a827b469bd7f5a070826b7852 Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Tue, 20 May 2025 02:34:19 +0800 Subject: [PATCH 09/16] fix: hide widget title --- frontend/public/index.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/public/index.html b/frontend/public/index.html index 20794c9..019574e 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -28,6 +28,10 @@ .mpl-toolbar select { display: none; } + + .ui-widget-header { + display: none; + } Funix From 00c6f3446c70466dc8a36fc063bf1e43e48f2ce0 Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Tue, 20 May 2025 07:26:51 +0800 Subject: [PATCH 10/16] feat: enable `figure_to_image` default --- backend/funix/decorator/__init__.py | 2 +- backend/funix/decorator/magic.py | 4 +- frontend/config-overrides.js | 7 - frontend/public/index.html | 1 + frontend/public/static/js/mpl.js | 698 ++++++++++++++++++ .../components/FunixFunction/OutputPanel.tsx | 8 +- 6 files changed, 709 insertions(+), 11 deletions(-) create mode 100644 frontend/public/static/js/mpl.js diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 373e6c8..742053c 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -235,7 +235,7 @@ def funix( print_to_web: bool = False, autorun: AutoRunType = False, disable: bool = False, - figure_to_image: bool = False, + figure_to_image: bool = True, keep_last: bool = False, app_and_sock: tuple[Flask, Sock] | None = None, jupyter_class: bool = False, diff --git a/backend/funix/decorator/magic.py b/backend/funix/decorator/magic.py index 01f564b..4a4e5b1 100644 --- a/backend/funix/decorator/magic.py +++ b/backend/funix/decorator/magic.py @@ -48,6 +48,8 @@ import matplotlib matplotlib.use("WebAgg") + matplotlib.rcParams["figure.autolayout"] = True + matplotlib.rcParams["savefig.bbox"] = "tight" __matplotlib_use = True except: pass @@ -396,7 +398,7 @@ def get_figure(figure) -> dict: import matplotlib.pyplot from matplotlib.backends.backend_webagg import _BackendWebAgg - matplotlib.pyplot.tight_layout() + # matplotlib.pyplot.tight_layout() new_fig_manger = _BackendWebAgg.new_figure_manager_given_figure manager = new_fig_manger(id(figure), figure) diff --git a/frontend/config-overrides.js b/frontend/config-overrides.js index 3d4367c..e4200f6 100644 --- a/frontend/config-overrides.js +++ b/frontend/config-overrides.js @@ -5,13 +5,11 @@ const webpack = require("webpack"); const scripts = process.env.REACT_APP_IN_FUNIX ? ` - ` : ` - `; @@ -48,11 +46,6 @@ module.exports = function override(config) { filepath: "static/js/jquery-3.7.1.min.js", hash: false, }, - { - url: "https://cdn.jsdelivr.net/gh/matplotlib/matplotlib@v3.10.x/lib/matplotlib/backends/web_backend/js/mpl.js", - filepath: "static/js/mpl.js", - hash: false, - }, { url: "https://cdn.jsdelivr.net/gh/matplotlib/matplotlib@v3.10.x/lib/matplotlib/backends/web_backend/css/fbm.css", filepath: "static/css/fbm.css", diff --git a/frontend/public/index.html b/frontend/public/index.html index 019574e..2737522 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -14,6 +14,7 @@ rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> +