diff --git a/pydatalab/pyproject.toml b/pydatalab/pyproject.toml index 0f239fc7a..9cf109e70 100644 --- a/pydatalab/pyproject.toml +++ b/pydatalab/pyproject.toml @@ -21,13 +21,13 @@ classifiers = [ requires-python = ">= 3.10, < 3.12" dependencies = [ - "bokeh ~= 2.4, < 3.0", "matplotlib ~= 3.8", "periodictable ~= 1.7", "pydantic[email, dotenv] < 2.0", "pint ~= 0.24", "pandas[excel] ~= 2.2", "pymongo ~= 4.7", + "bokeh==3.4.3", ] [project.urls] diff --git a/pydatalab/src/pydatalab/apps/echem/blocks.py b/pydatalab/src/pydatalab/apps/echem/blocks.py index c3f5e7e1f..3e2b34e29 100644 --- a/pydatalab/src/pydatalab/apps/echem/blocks.py +++ b/pydatalab/src/pydatalab/apps/echem/blocks.py @@ -218,9 +218,9 @@ def plot_cycle(self): if layout is not None: # Don't overwrite the previous plot data in cases where the plot is not generated # for a 'normal' reason - self.data["bokeh_plot_data"] = bokeh.embed.json_item( - layout, theme=bokeh_plots.DATALAB_BOKEH_THEME - ) + script, div = bokeh.embed.components(layout, theme=bokeh_plots.DATALAB_BOKEH_THEME) + + self.data["bokeh_plot_data"] = {"script": script, "div": div} return @property diff --git a/pydatalab/src/pydatalab/apps/eis/__init__.py b/pydatalab/src/pydatalab/apps/eis/__init__.py index 3a46ac6b4..ff3591d52 100644 --- a/pydatalab/src/pydatalab/apps/eis/__init__.py +++ b/pydatalab/src/pydatalab/apps/eis/__init__.py @@ -65,4 +65,6 @@ def generate_eis_plot(self): tools=HoverTool(tooltips=[("Frequency [Hz]", "@{Frequency [Hz]}")]), ) - self.data["bokeh_plot_data"] = bokeh.embed.json_item(plot, theme=DATALAB_BOKEH_THEME) + script, div = bokeh.embed.components(plot, theme=DATALAB_BOKEH_THEME) + + self.data["bokeh_plot_data"] = {"script": script, "div": div} diff --git a/pydatalab/src/pydatalab/apps/ftir/__init__.py b/pydatalab/src/pydatalab/apps/ftir/__init__.py index 9778f3e8e..d35253e31 100644 --- a/pydatalab/src/pydatalab/apps/ftir/__init__.py +++ b/pydatalab/src/pydatalab/apps/ftir/__init__.py @@ -102,4 +102,6 @@ def generate_ftir_plot(self): if ftir_data is not None: layout = self._format_ftir_plot(ftir_data) - self.data["bokeh_plot_data"] = bokeh.embed.json_item(layout, theme=DATALAB_BOKEH_THEME) + script, div = bokeh.embed.components(layout, theme=DATALAB_BOKEH_THEME) + + self.data["bokeh_plot_data"] = {"script": script, "div": div} diff --git a/pydatalab/src/pydatalab/apps/nmr/blocks.py b/pydatalab/src/pydatalab/apps/nmr/blocks.py index c5131958e..2b906b38f 100644 --- a/pydatalab/src/pydatalab/apps/nmr/blocks.py +++ b/pydatalab/src/pydatalab/apps/nmr/blocks.py @@ -235,7 +235,7 @@ def generate_nmr_plot(self, parse: bool = True): self.data["bokeh_plot_data"] = self.make_nmr_plot(df, self.data["metadata"]) @classmethod - def make_nmr_plot(cls, df: pd.DataFrame, metadata: dict[str, Any]) -> str: + def make_nmr_plot(cls, df: pd.DataFrame, metadata: dict[str, Any]) -> dict[str, str]: """Create a Bokeh plot for the NMR data stored in the dataframe and metadata.""" nucleus_label = metadata.get("nucleus") or "" # replace numbers with superscripts @@ -270,4 +270,5 @@ def make_nmr_plot(cls, df: pd.DataFrame, metadata: dict[str, Any]) -> str: # of the layout in the current implementation, but this could be fragile. bokeh_layout.children[1].x_range.flipped = True - return bokeh.embed.json_item(bokeh_layout, theme=DATALAB_BOKEH_THEME) + script, div = bokeh.embed.components(bokeh_layout, theme=DATALAB_BOKEH_THEME) + return {"script": script, "div": div} diff --git a/pydatalab/src/pydatalab/apps/raman/blocks.py b/pydatalab/src/pydatalab/apps/raman/blocks.py index 77f22c1cb..0f9584697 100644 --- a/pydatalab/src/pydatalab/apps/raman/blocks.py +++ b/pydatalab/src/pydatalab/apps/raman/blocks.py @@ -245,4 +245,6 @@ def generate_raman_plot(self): point_size=3, ) - self.data["bokeh_plot_data"] = bokeh.embed.json_item(p, theme=DATALAB_BOKEH_THEME) + script, div = bokeh.embed.components(p, theme=DATALAB_BOKEH_THEME) + + self.data["bokeh_plot_data"] = {"script": script, "div": div} diff --git a/pydatalab/src/pydatalab/apps/tga/blocks.py b/pydatalab/src/pydatalab/apps/tga/blocks.py index 1c47b645d..bef317901 100644 --- a/pydatalab/src/pydatalab/apps/tga/blocks.py +++ b/pydatalab/src/pydatalab/apps/tga/blocks.py @@ -53,7 +53,8 @@ def _plot_ms_data(cls, ms_data): max_vals: list[tuple[str, float]] = [] data_key: str = ( - "Partial pressure [mbar] or Ion Current [A]" # default value for data key if missing + # default value for data key if missing + "Partial pressure [mbar] or Ion Current [A]" ) for species in ms_data["data"]: @@ -100,4 +101,6 @@ def _plot_ms_data(cls, ms_data): grid.append(plots[i : i + M]) p = gridplot(grid, sizing_mode="scale_width", toolbar_location="below") - return bokeh.embed.json_item(p, theme=DATALAB_BOKEH_GRID_THEME) + script, div = bokeh.embed.components(p, theme=DATALAB_BOKEH_GRID_THEME) + + return {"script": script, "div": div} diff --git a/pydatalab/src/pydatalab/apps/uvvis/__init__.py b/pydatalab/src/pydatalab/apps/uvvis/__init__.py index 7bbab793a..0f5c8e8dc 100644 --- a/pydatalab/src/pydatalab/apps/uvvis/__init__.py +++ b/pydatalab/src/pydatalab/apps/uvvis/__init__.py @@ -169,4 +169,6 @@ def generate_absorbance_plot(self): _names = [Path(file["location"]).name for file in file_info[1:]] if len(absorbance_data) > 0: layout = self._format_UV_Vis_plot(absorbance_data, names=_names) - self.data["bokeh_plot_data"] = bokeh.embed.json_item(layout, theme=DATALAB_BOKEH_THEME) + script, div = bokeh.embed.components(layout, theme=DATALAB_BOKEH_THEME) + + self.data["bokeh_plot_data"] = {"script": script, "div": div} diff --git a/pydatalab/src/pydatalab/apps/xrd/blocks.py b/pydatalab/src/pydatalab/apps/xrd/blocks.py index 5fb501997..869b4aeee 100644 --- a/pydatalab/src/pydatalab/apps/xrd/blocks.py +++ b/pydatalab/src/pydatalab/apps/xrd/blocks.py @@ -59,7 +59,8 @@ def load_pattern( df, peak_data = compute_cif_pxrd( location, wavelength=wavelength or cls.defaults["wavelength"] ) - theoretical = True # Track whether this is a computed PXRD that does not need background subtraction + # Track whether this is a computed PXRD that does not need background subtraction + theoretical = True else: columns = ["twotheta", "intensity", "error"] @@ -271,4 +272,6 @@ def generate_xrd_plot(self) -> None: point_size=3, ) - self.data["bokeh_plot_data"] = bokeh.embed.json_item(p, theme=DATALAB_BOKEH_THEME) + script, div = bokeh.embed.components(p, theme=DATALAB_BOKEH_THEME) + + self.data["bokeh_plot_data"] = {"script": script, "div": div} diff --git a/pydatalab/src/pydatalab/blocks/common.py b/pydatalab/src/pydatalab/blocks/common.py index 0ef5a434d..3b8baa8f5 100644 --- a/pydatalab/src/pydatalab/blocks/common.py +++ b/pydatalab/src/pydatalab/blocks/common.py @@ -172,4 +172,6 @@ def plot_df(self): show_table=True, ) - self.data["bokeh_plot_data"] = bokeh.embed.json_item(plot, theme=DATALAB_BOKEH_THEME) + script, div = bokeh.embed.components(plot, theme=DATALAB_BOKEH_THEME) + + self.data["bokeh_plot_data"] = {"script": script, "div": div} diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index 906693141..391d9e1ea 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -29,6 +29,42 @@ COLORS = Dark2[8] TOOLS = "box_zoom, reset, tap, crosshair, save" +PLOT_WIDTH = 620 +PLOT_HEIGHT = 410 +PLOT_MIN_WIDTH = 620 +PLOT_MIN_HEIGHT = 410 + +DEFAULT_FIGURE_CONFIG = { + "width": PLOT_WIDTH, + "height": PLOT_HEIGHT, + "min_width": PLOT_MIN_WIDTH, + "min_height": PLOT_MIN_HEIGHT, + "sizing_mode": "scale_width", + "toolbar_location": "above", + "tools": TOOLS, +} + +GRID_PLOT_CONFIG = { + "width": PLOT_WIDTH // 2 - 20, + "height": PLOT_HEIGHT, + "min_width": PLOT_MIN_WIDTH // 2 - 20, + "min_height": PLOT_MIN_HEIGHT, + "sizing_mode": "scale_width", + "toolbar_location": "above", + "tools": TOOLS, +} + +FULL_WIDTH_CONFIG = { + "width": PLOT_WIDTH, + "height": PLOT_HEIGHT - 40, + "min_width": PLOT_MIN_WIDTH, + "min_height": PLOT_MIN_HEIGHT - 40, + "sizing_mode": "scale_width", + "toolbar_location": "above", + "tools": TOOLS, +} + + SELECTABLE_CALLBACK_x = """ var column = cb_obj.value; if (circle1) {circle1.glyph.x.field = column;} @@ -66,7 +102,7 @@ style = { "attrs": { # apply defaults to Figure properties - "Figure": { + "figure": { "toolbar_location": "above", "outline_line_color": None, "min_border_right": 10, @@ -96,12 +132,11 @@ } } - """Additional style suitable for grid plots""" grid_style = { "attrs": { # apply defaults to Figure properties - "Figure": { + "figure": { "toolbar_location": "above", "outline_line_color": None, "min_border_right": 10, @@ -131,11 +166,64 @@ } } - DATALAB_BOKEH_THEME = Theme(json=style) DATALAB_BOKEH_GRID_THEME = Theme(json=grid_style) +def create_standard_figure(**kwargs): + """ + Creates a Bokeh figure with standardized dimensions. + + Args: + **kwargs: Additional parameters that override the default values. + + Returns: + Bokeh figure with standardized dimensions. + """ + config = DEFAULT_FIGURE_CONFIG.copy() + config.update(kwargs) + + p = figure(**config) + p.toolbar.logo = "grey" + return p + + +def create_grid_figure(**kwargs): + """ + Creates a Bokeh figure for grid use with adapted dimensions. + + Args: + **kwargs: Additional parameters that override the default values. + + Returns: + Bokeh figure with responsive dimensions for grid layout. + """ + config = GRID_PLOT_CONFIG.copy() + config.update(kwargs) + + p = figure(**config) + p.toolbar.logo = "grey" + return p + + +def create_full_width_figure(**kwargs): + """ + Creates a full-width Bokeh figure (for tables, timelines, etc.). + + Args: + **kwargs: Additional parameters that override the default values. + + Returns: + Full-width responsive Bokeh figure. + """ + config = FULL_WIDTH_CONFIG.copy() + config.update(kwargs) + + p = figure(**config) + p.toolbar.logo = "grey" + return p + + def selectable_axes_plot( df: dict[str, pd.DataFrame] | list[pd.DataFrame] | pd.DataFrame, x_options: list[str] | None = None, @@ -225,16 +313,12 @@ def selectable_axes_plot( x_axis_label = x_default if label_x else "" y_axis_label = y_label if label_y else "" - p = figure( - sizing_mode="scale_width", - aspect_ratio=kwargs.pop("aspect_ratio", 1.5), + p = create_standard_figure( x_axis_label=x_axis_label, y_axis_label=y_axis_label, - tools=TOOLS, title=plot_title, **kwargs, ) - p.toolbar.logo = "grey" if tools: if isinstance(tools, list): @@ -299,7 +383,7 @@ def selectable_axes_plot( y_default = y_default[0] circles = ( - p.circle( + p.scatter( x=x_default, y=y_default, source=source, @@ -431,9 +515,6 @@ def double_axes_echem_plot( x_options = [opt for opt in x_options if opt in df.columns] - common_options = {"aspect_ratio": 1.5, "tools": TOOLS} - common_options.update(**kwargs) - if mode == "normal": mode = None @@ -452,23 +533,25 @@ def double_axes_echem_plot( # normal plot # x_label = "Capacity (mAh/g)" if x_default == "Capacity normalized" else x_default x_label = x_default - p1 = figure(x_axis_label=x_label, y_axis_label="voltage (V)", **common_options) + + if mode == "dQ/dV": + p1 = create_grid_figure(x_axis_label=x_label, y_axis_label="voltage (V)", **kwargs) + else: + p1 = create_standard_figure(x_axis_label=x_label, y_axis_label="voltage (V)", **kwargs) + p1.xaxis.ticker.desired_num_ticks = 5 plots.append(p1) # the differential plot if mode in ("dQ/dV", "dV/dQ"): if mode == "dQ/dV": - p2 = figure( - x_axis_label=mode, - y_axis_label="voltage (V)", - y_range=p1.y_range, - **common_options, + p2 = create_grid_figure( + x_axis_label=mode, y_axis_label="voltage (V)", y_range=p1.y_range, **kwargs ) p2.xaxis.ticker.desired_num_ticks = 3 else: - p2 = figure( - x_axis_label=x_default, y_axis_label=mode, x_range=p1.x_range, **common_options + p2 = create_standard_figure( + x_axis_label=x_default, y_axis_label=mode, x_range=p1.x_range, **kwargs ) p2.xaxis.ticker.desired_num_ticks = 5 plots.append(p2) @@ -476,10 +559,10 @@ def double_axes_echem_plot( elif mode == "final capacity" and cycle_summary is not None: palette = Accent[3] - p3 = figure( + p3 = create_standard_figure( x_axis_label="Cycle number", y_axis_label="capacity (mAh/g)" if normalized else "capacity (mAh)", - **common_options, + **kwargs, ) p3.line( @@ -490,7 +573,7 @@ def double_axes_echem_plot( line_width=2, color=palette[0], ) - p3.circle( + p3.scatter( x="full cycle", y="charge capacity (mAh/g)" if normalized else "charge capacity (mAh)", source=cycle_summary, @@ -509,10 +592,11 @@ def double_axes_echem_plot( line_width=2, color=palette[2], ) - p3.triangle( + p3.scatter( x="full cycle", y="discharge capacity (mAh/g)" if normalized else "discharge capacity (mAh)", source=cycle_summary, + marker="triangle", fill_color="white", hatch_color=palette[2], line_width=2, @@ -564,7 +648,7 @@ def double_axes_echem_plot( peaks, _ = find_peaks(dvdq_array, prominence=5) peak_locs = group.iloc[peaks] - p2.circle(x=x, y=y, source=peak_locs) + p2.scatter(x=x, y=y, source=peak_locs) if ind == 0: lines.append(line) @@ -614,9 +698,10 @@ def double_axes_echem_plot( p.js_on_event(DoubleTap, CustomJS(args=dict(p=p), code="p.reset.emit()")) if mode == "dQ/dV": - grid = [[p1, p2], [xaxis_select]] + grid = gridplot([[p1, p2], [xaxis_select]], sizing_mode="scale_width", merge_tools=False) + elif mode == "dV/dQ": - grid = [[p1], [p2]] + grid = gridplot([[p1], [p2]], sizing_mode="scale_width", merge_tools=False) elif mode == "final capacity": if cycle_summary is not None: save_data = Button(label="Download .csv", button_type="primary", width_policy="min") @@ -625,11 +710,13 @@ def double_axes_echem_plot( code=GENERATE_CSV_CALLBACK, ) save_data.js_on_click(save_data_callback) - grid = [[save_data], [p3]] + grid = gridplot([[save_data], [p3]], sizing_mode="scale_width", merge_tools=False) else: warnings.warn("Unable to generate cycle summary plot for this dataset.") return None else: - grid = [[p1], [xaxis_select], [yaxis_select]] + grid = gridplot( + [[p1], [xaxis_select], [yaxis_select]], sizing_mode="scale_width", merge_tools=False + ) - return gridplot(grid, sizing_mode="scale_width", toolbar_location="below") + return grid diff --git a/pydatalab/uv.lock b/pydatalab/uv.lock index 4fef7c426..387027880 100644 --- a/pydatalab/uv.lock +++ b/pydatalab/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.10, <3.12" resolution-markers = [ "python_full_version < '3.11' and platform_python_implementation != 'PyPy'", @@ -177,20 +176,22 @@ wheels = [ [[package]] name = "bokeh" -version = "2.4.3" +version = "3.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "contourpy" }, { name = "jinja2" }, { name = "numpy" }, { name = "packaging" }, + { name = "pandas" }, { name = "pillow" }, { name = "pyyaml" }, { name = "tornado" }, - { name = "typing-extensions" }, + { name = "xyzservices" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/01/c7622f3f8c6440f4a66ed58bfe5a2a499c2cc8551e00a298ceb94ccc3c70/bokeh-2.4.3.tar.gz", hash = "sha256:ef33801161af379665ab7a34684f2209861e3aefd5c803a21fbbb99d94874b03", size = 17722836 } +sdist = { url = "https://files.pythonhosted.org/packages/c1/a8/4f750e1febe3b8d448794a7b85cd0efd2eb649eb915c990bcdd3650a5f3b/bokeh-3.4.3.tar.gz", hash = "sha256:b7c22fb0f7004b04f12e1b7b26ee0269a26737a08ded848fb58f6a34ec1eb155", size = 6410715 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/06/706a9c43436cd0c3e2f4b94e93ae837e74965e59565c596b727974a74169/bokeh-2.4.3-py3-none-any.whl", hash = "sha256:104d2f0a4ca7774ee4b11e545aa34ff76bf3e2ad6de0d33944361981b65da420", size = 18508622 }, + { url = "https://files.pythonhosted.org/packages/65/5d/e96520996607c9b894a4716ddedc447a68351b0a8729f26ac012c8e7041b/bokeh-3.4.3-py3-none-any.whl", hash = "sha256:c6f33817f866fc67fbeb5df79cd13a8bb592c05c591f3fd7f4f22b824f7afa01", size = 7017720 }, ] [[package]] @@ -562,7 +563,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "bokeh", specifier = "~=2.4,<3.0" }, + { name = "bokeh", specifier = "==3.4.3" }, { name = "datalab-app-plugin-insitu", marker = "extra == 'app-plugins-git'", git = "https://github.com/datalab-org/datalab-app-plugin-insitu.git?rev=v0.1.6" }, { name = "datalab-server", extras = ["apps", "chat", "server"], marker = "extra == 'all'" }, { name = "flask", marker = "extra == 'server'", specifier = "~=3.0" }, @@ -598,7 +599,6 @@ requires-dist = [ { name = "transformers", marker = "extra == 'chat'", specifier = "~=4.42" }, { name = "werkzeug", marker = "extra == 'server'", specifier = "~=3.0" }, ] -provides-extras = ["server", "apps", "app-plugins-git", "chat", "deploy", "all"] [package.metadata.requires-dev] dev = [ @@ -2567,20 +2567,21 @@ wheels = [ [[package]] name = "tornado" -version = "6.4.1" +version = "6.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/66/398ac7167f1c7835406888a386f6d0d26ee5dbf197d8a571300be57662d3/tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9", size = 500623 } +sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/d9/c33be3c1a7564f7d42d87a8d186371a75fd142097076767a5c27da941fef/tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8", size = 435924 }, - { url = "https://files.pythonhosted.org/packages/2e/0f/721e113a2fac2f1d7d124b3279a1da4c77622e104084f56119875019ffab/tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14", size = 433883 }, - { url = "https://files.pythonhosted.org/packages/13/cf/786b8f1e6fe1c7c675e79657448178ad65e41c1c9765ef82e7f6f765c4c5/tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4", size = 437224 }, - { url = "https://files.pythonhosted.org/packages/e4/8e/a6ce4b8d5935558828b0f30f3afcb2d980566718837b3365d98e34f6067e/tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842", size = 436597 }, - { url = "https://files.pythonhosted.org/packages/22/d4/54f9d12668b58336bd30defe0307e6c61589a3e687b05c366f804b7faaf0/tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3", size = 436797 }, - { url = "https://files.pythonhosted.org/packages/cf/3f/2c792e7afa7dd8b24fad7a2ed3c2f24a5ec5110c7b43a64cb6095cc106b8/tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f", size = 437516 }, - { url = "https://files.pythonhosted.org/packages/71/63/c8fc62745e669ac9009044b889fc531b6f88ac0f5f183cac79eaa950bb23/tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4", size = 436958 }, - { url = "https://files.pythonhosted.org/packages/94/d4/f8ac1f5bd22c15fad3b527e025ce219bd526acdbd903f52053df2baecc8b/tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698", size = 436882 }, - { url = "https://files.pythonhosted.org/packages/4b/3e/a8124c21cc0bbf144d7903d2a0cadab15cadaf683fa39a0f92bc567f0d4d/tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d", size = 438092 }, - { url = "https://files.pythonhosted.org/packages/d9/2f/3f2f05e84a7aff787a96d5fb06821323feb370fe0baed4db6ea7b1088f32/tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7", size = 438532 }, + { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948 }, + { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112 }, + { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672 }, + { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019 }, + { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252 }, + { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930 }, + { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351 }, + { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328 }, + { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396 }, + { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840 }, + { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596 }, ] [[package]] @@ -2740,6 +2741,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/ea/53d1fe468e63e092cf16e2c18d16f50c29851242f9dd12d6a66e0d7f0d02/XlsxWriter-3.2.0-py3-none-any.whl", hash = "sha256:ecfd5405b3e0e228219bcaf24c2ca0915e012ca9464a14048021d21a995d490e", size = 159925 }, ] +[[package]] +name = "xyzservices" +version = "2025.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/af/c0f7f97bb320d14c089476f487b81f733238cc5603e0914f2e409f49d589/xyzservices-2025.4.0.tar.gz", hash = "sha256:6fe764713648fac53450fbc61a3c366cb6ae5335a1b2ae0c3796b495de3709d8", size = 1134722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/7d/b77455d7c7c51255b2992b429107fab811b2e36ceaf76da1e55a045dc568/xyzservices-2025.4.0-py3-none-any.whl", hash = "sha256:8d4db9a59213ccb4ce1cf70210584f30b10795bff47627cdfb862b39ff6e10c9", size = 90391 }, +] + [[package]] name = "yarl" version = "1.17.0" diff --git a/webapp/public/index.html b/webapp/public/index.html index 527cc131b..844449db2 100644 --- a/webapp/public/index.html +++ b/webapp/public/index.html @@ -6,21 +6,10 @@