diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a8595e2..3a4e1b6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -53,9 +53,9 @@ jobs: # path: docs/_build/ # --- Streamlit example deployment --- - - name: Fix Absolute Paths in Streamlit Scripts - run: | - find docs/streamlit_report/sections -type f -name "*.py" -exec sed -i 's|/home/runner/work/vuegen/vuegen/docs/||g' {} + + #- name: Fix Absolute Paths in Streamlit Scripts + # run: | + # find docs/streamlit_report/sections -type f -name "*.py" -exec sed -i 's|/home/runner/work/vuegen/vuegen/docs/||g' {} + - name: Publish Streamlit report to streamlit-example branch if: startsWith(github.ref, 'refs/tags') uses: peaceiris/actions-gh-pages@v4 diff --git a/.gitignore b/.gitignore index 07a36e9..6c57dc2 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,5 @@ docs/images/UML_diagrams/ docs/images/Graphical_abstract/ docs/images/Nfcore_module_figure docs/presentations/ -docs/example_data/Earth_microbiome_vuegen_demo_notebook_test/ -docs/vuegen_case_study_earth_microbiome_test.ipynb -test.py \ No newline at end of file +basic_example_vuegen_demo_notebook_config.yaml +earth_microbiome_vuegen_demo_notebook_config.yaml \ No newline at end of file diff --git a/docs/images/vuegen_classdiagram_noattmeth.png b/docs/images/vuegen_classdiagram_noattmeth.png index 0016c90..9b044a2 100644 Binary files a/docs/images/vuegen_classdiagram_noattmeth.png and b/docs/images/vuegen_classdiagram_noattmeth.png differ diff --git a/docs/index.md b/docs/index.md index 1aff99b..a06f429 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,7 +35,7 @@ vuegen_Chatbot_configfile ```{toctree} :maxdepth: 2 -:caption: Modules +:caption: API Reference :hidden: reference/vuegen diff --git a/docs/vuegen_basic_case_study.ipynb b/docs/vuegen_basic_case_study.ipynb index 3c17cdd..431511e 100644 --- a/docs/vuegen_basic_case_study.ipynb +++ b/docs/vuegen_basic_case_study.ipynb @@ -139,10 +139,9 @@ "outputs": [], "source": [ "# Imports\n", - "import os\n", "import yaml\n", "from vuegen import report_generator\n", - "from vuegen.utils import get_logger, load_yaml_config\n", + "from vuegen.utils import load_yaml_config\n", "\n", "if IN_COLAB:\n", " import urllib" @@ -444,7 +443,7 @@ ], "metadata": { "kernelspec": { - "display_name": "vuegen_py312", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -458,9 +457,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.9" + "version": "3.12.6" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index cd3881b..545da41 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -8,7 +8,7 @@ import pandas as pd from . import report as r -from .utils import create_folder, is_url, sort_imports +from .utils import create_folder, get_relative_file_path, is_url, sort_imports class QuartoReportView(r.ReportView): @@ -365,6 +365,7 @@ def _create_yaml_header(self) -> str: r.ReportType.PDF: """ pdf: toc: false + fig-align: center margin: - bottom=40mm include-in-header: @@ -568,13 +569,13 @@ def _generate_plot_content(self, plot) -> List[str]: try: if plot.plot_type == r.PlotType.STATIC: plot_content.append( - self._generate_image_content(plot.file_path, width=950) + self._generate_image_content(plot.file_path, width="90%") ) elif plot.plot_type == r.PlotType.PLOTLY: plot_content.append(self._generate_plot_code(plot)) if self.is_report_static: plot_content.append( - f"""fig_plotly.write_image("{static_plot_path.resolve().as_posix()}")\n```\n""" + f"""fig_plotly.write_image("{static_plot_path.relative_to("quarto_report").as_posix()}")\n```\n""" ) plot_content.append(self._generate_image_content(static_plot_path)) else: @@ -583,7 +584,7 @@ def _generate_plot_content(self, plot) -> List[str]: plot_content.append(self._generate_plot_code(plot)) if self.is_report_static: plot_content.append( - f"""fig_altair.save("{static_plot_path.resolve().as_posix()}")\n```\n""" + f"""fig_altair.save("{static_plot_path.relative_to("quarto_report").as_posix()}")\n```\n""" ) plot_content.append(self._generate_image_content(static_plot_path)) else: @@ -655,8 +656,9 @@ def _generate_plot_code(self, plot, output_file="") -> str: response.raise_for_status() plot_json = response.text\n""" else: # If it's a local file + plot_rel_path = get_relative_file_path(plot.file_path, base_path="..") plot_code += f""" -with open('{(Path("..") / plot.file_path).as_posix()}', 'r') as plot_file: +with open('{plot_rel_path.as_posix()}', 'r') as plot_file: plot_json = json.load(plot_file)\n""" # Add specific code for each visualization tool if plot.plot_type == r.PlotType.PLOTLY: @@ -669,13 +671,13 @@ def _generate_plot_code(self, plot, output_file="") -> str: plot_json_str = json.dumps(plot_json)\n # Create the plotly plot fig_plotly = pio.from_json(plot_json_str) -fig_plotly.update_layout(width=950, height=500)\n""" +fig_plotly.update_layout(autosize=False, width=950, height=400, margin=dict(b=50, t=50, l=50, r=50))\n""" elif plot.plot_type == r.PlotType.ALTAIR: plot_code += """ # Convert JSON to string plot_json_str = json.dumps(plot_json)\n # Create the plotly plot -fig_altair = alt.Chart.from_json(plot_json_str).properties(width=900, height=400)\n""" +fig_altair = alt.Chart.from_json(plot_json_str).properties(width=900, height=370)\n""" elif plot.plot_type == r.PlotType.INTERACTIVE_NETWORK: # Generate the HTML embedding for interactive networks if is_url(plot.file_path) and plot.file_path.endswith(".html"): @@ -734,16 +736,17 @@ def _generate_dataframe_content(self, dataframe) -> List[str]: ) # Build the file path (URL or local file) - file_path = ( - dataframe.file_path - if is_url(dataframe.file_path) - else Path("..") / dataframe.file_path - ) + if is_url(dataframe.file_path): + df_file_path = dataframe.file_path + else: + df_file_path = get_relative_file_path( + dataframe.file_path, base_path=".." + ) # Load the DataFrame using the correct function read_function = read_function_mapping[file_extension] dataframe_content.append( - f"""df = pd.{read_function.__name__}('{file_path.as_posix()}')\n""" + f"""df = pd.{read_function.__name__}('{df_file_path.as_posix()}')\n""" ) # Display the dataframe @@ -798,9 +801,10 @@ def _generate_markdown_content(self, markdown) -> List[str]: markdown_content = response.text\n""" ) else: # If it's a local file + md_rel_path = get_relative_file_path(markdown.file_path, base_path="..") markdown_content.append( f""" -with open('{(Path("..") / markdown.file_path).as_posix()}', 'r') as markdown_file: +with open('{md_rel_path.as_posix()}', 'r') as markdown_file: markdown_content = markdown_file.read()\n""" ) @@ -822,6 +826,39 @@ def _generate_markdown_content(self, markdown) -> List[str]: ) return markdown_content + def _show_dataframe(self, dataframe) -> List[str]: + """ + Appends either a static image or an interactive representation of a DataFrame to the content list. + + Parameters + ---------- + dataframe : DataFrame + The DataFrame object containing the data to display. + + Returns + ------- + list : List[str] + The list of content lines for the DataFrame. + """ + dataframe_content = [] + if self.is_report_static: + # Generate path for the DataFrame image + df_image = ( + Path(self.static_dir) / f"{dataframe.title.replace(' ', '_')}.png" + ) + dataframe_content.append( + f"df.dfi.export('{Path(df_image).relative_to('quarto_report').as_posix()}', max_rows=10, max_cols=5, table_conversion='matplotlib')\n```\n" + ) + # Use helper method to add centered image content + dataframe_content.append(self._generate_image_content(df_image)) + else: + # Append code to display the DataFrame interactively + dataframe_content.append( + """show(df, classes="display nowrap compact", lengthMenu=[3, 5, 10])\n```\n""" + ) + + return dataframe_content + def _generate_html_content(self, html) -> List[str]: """ Adds an HTML component to the report. @@ -843,14 +880,13 @@ def _generate_html_content(self, html) -> List[str]: try: # Embed the HTML in an iframe - iframe_src = ( - html.file_path - if is_url(html.file_path) - else Path("..") / html.file_path - ) + if is_url(html.file_path): + html_file_path = html.file_path + else: + html_file_path = get_relative_file_path(html.file_path, base_path="..") iframe_code = f"""
- +
\n""" html_content.append(iframe_code) @@ -866,7 +902,7 @@ def _generate_html_content(self, html) -> List[str]: return html_content def _generate_image_content( - self, image_path: str, alt_text: str = "", width: int = 650, height: int = 400 + self, image_path: str, alt_text: str = "", width: str = "90%" ) -> str: """ Adds an image to the content list in an HTML format with a specified width and height. @@ -889,47 +925,10 @@ def _generate_image_content( """ if is_url(image_path): src = image_path - return ( - f"""![]({src}){{fig-alt={alt_text} width={width} height={height}}}\n""" - ) else: - src = Path(image_path).resolve() - return ( - f"""![](/{src}){{fig-alt={alt_text} width={width} height={height}}}\n""" - ) - - def _show_dataframe(self, dataframe) -> List[str]: - """ - Appends either a static image or an interactive representation of a DataFrame to the content list. - - Parameters - ---------- - dataframe : DataFrame - The DataFrame object containing the data to display. + src = get_relative_file_path(image_path, base_path="..").as_posix() - Returns - ------- - list : List[str] - The list of content lines for the DataFrame. - """ - dataframe_content = [] - if self.is_report_static: - # Generate path for the DataFrame image - df_image = ( - Path(self.static_dir) / f"{dataframe.title.replace(' ', '_')}.png" - ) - dataframe_content.append( - f"df.dfi.export('{Path(df_image).resolve().as_posix()}', max_rows=10, max_cols=5, table_conversion='matplotlib')\n```\n" - ) - # Use helper method to add centered image content - dataframe_content.append(self._generate_image_content(df_image)) - else: - # Append code to display the DataFrame interactively - dataframe_content.append( - """show(df, classes="display nowrap compact", lengthMenu=[3, 5, 10])\n```\n""" - ) - - return dataframe_content + return f"""![]({src}){{fig-alt={alt_text} width={width}}}\n""" def _generate_component_imports(self, component: r.Component) -> List[str]: """ diff --git a/src/vuegen/streamlit_reportview.py b/src/vuegen/streamlit_reportview.py index 3809c96..bb007fb 100644 --- a/src/vuegen/streamlit_reportview.py +++ b/src/vuegen/streamlit_reportview.py @@ -9,7 +9,7 @@ from streamlit.web import cli as stcli from . import report as r -from .utils import create_folder, generate_footer, is_url +from .utils import create_folder, generate_footer, get_relative_file_path, is_url from .utils.variables import make_valid_identifier @@ -585,8 +585,9 @@ def _generate_plot_content(self, plot) -> List[str]: # Add content for the different plot types try: if plot.plot_type == r.PlotType.STATIC: + plot_rel_path = get_relative_file_path(plot.file_path) plot_content.append( - f"\nst.image('{plot.file_path}', caption='{plot.caption}', use_column_width=True)\n" + f"\nst.image('{plot_rel_path.as_posix()}', caption='{plot.caption}', use_column_width=True)\n" ) elif plot.plot_type == r.PlotType.PLOTLY: plot_content.append(self._generate_plot_code(plot)) @@ -601,7 +602,7 @@ def _generate_plot_content(self, plot) -> List[str]: # Otherwise, create and save a new pyvis network from the netowrkx graph html_plot_file = ( Path(self.static_dir) / f"{plot.title.replace(' ', '_')}.html" - ) + ).resolve() _ = plot.create_and_save_pyvis_network( networkx_graph, html_plot_file ) @@ -616,13 +617,13 @@ def _generate_plot_content(self, plot) -> List[str]: f""" response = requests.get('{html_plot_file}') response.raise_for_status() -html_data = response.text\n""" +html_content = response.text\n""" ) else: plot_content.append( f""" -with open('{html_plot_file}', 'r') as f: - html_data = f.read()\n""" +with open('{Path(html_plot_file).relative_to(Path.cwd())}', 'r') as html_file: + html_content = html_file.read()\n""" ) # Append the code for additional information (nodes and edges count) @@ -669,8 +670,9 @@ def _generate_plot_code(self, plot) -> str: response.raise_for_status() plot_json = json.loads(response.text)\n""" else: # If it's a local file + plot_rel_path = get_relative_file_path(plot.file_path) plot_code = f""" -with open('{Path(plot.file_path).as_posix()}', 'r') as plot_file: +with open('{plot_rel_path.as_posix()}', 'r') as plot_file: plot_json = json.load(plot_file)\n""" # Add specific code for each visualization tool @@ -693,7 +695,7 @@ def _generate_plot_code(self, plot) -> str: control_layout = st.checkbox('Add panel to control layout', value=True) net_html_height = 1200 if control_layout else 630 # Load HTML into HTML component for display on Streamlit -st.components.v1.html(html_data, height=net_html_height)\n""" +st.components.v1.html(html_content, height=net_html_height)\n""" return plot_code def _generate_dataframe_content(self, dataframe) -> List[str]: @@ -739,8 +741,14 @@ def _generate_dataframe_content(self, dataframe) -> List[str]: # Load the DataFrame using the correct function read_function = read_function_mapping[file_extension] + + # Build the file path (URL or local file) + if is_url(dataframe.file_path): + df_file_path = dataframe.file_path + else: + df_file_path = get_relative_file_path(dataframe.file_path) dataframe_content.append( - f"""df = pd.{read_function.__name__}('{dataframe.file_path}')\n""" + f"""df = pd.{read_function.__name__}('{df_file_path.as_posix()}')\n""" ) # Displays a DataFrame using AgGrid with configurable options. @@ -817,9 +825,10 @@ def _generate_markdown_content(self, markdown) -> List[str]: markdown_content = response.text\n""" ) else: # If it's a local file + md_rel_path = get_relative_file_path(markdown.file_path) markdown_content.append( f""" -with open('{(Path("..") / markdown.file_path).as_posix()}', 'r') as markdown_file: +with open('{md_rel_path.as_posix()}', 'r') as markdown_file: markdown_content = markdown_file.read()\n""" ) # Code to display md content @@ -875,11 +884,11 @@ def _generate_html_content(self, html) -> List[str]: response.raise_for_status() html_content = response.text\n""" ) - else: - # If it's a local file + else: # If it's a local file + html_rel_path = get_relative_file_path(html.file_path) html_content.append( f""" -with open('{(Path("..") / html.file_path).as_posix()}', 'r', encoding='utf-8') as html_file: +with open('{html_rel_path.as_posix()}', 'r', encoding='utf-8') as html_file: html_content = html_file.read()\n""" ) diff --git a/src/vuegen/utils/__init__.py b/src/vuegen/utils/__init__.py index 0084208..05b5b8f 100644 --- a/src/vuegen/utils/__init__.py +++ b/src/vuegen/utils/__init__.py @@ -174,6 +174,35 @@ def create_folder(directory_path: str, is_nested: bool = False) -> bool: raise OSError(f"Error creating directory '{directory_path}': {e}") +def get_relative_file_path(file_path: str, base_path: str = "") -> Path: + """ + Returns the relative file path of a given file with respect to + the current working directory (CWD). + + This method will resolve the absolute path of the given file and + return a relative path with respect to the directory where the script is + being executed. Optionally, a base path can be added (e.g., "../"). + + Parameters + ---------- + file_path : str + The full file path to be converted to a relative path. + base_path : str, optional + The base path to be prepended to the relative path, default is an empty string. + + Returns + ------- + Path + The file path relative to the CWD. + """ + rel_path = Path(file_path).resolve().relative_to(Path.cwd().resolve()) + + if base_path: + rel_path = Path(base_path) / rel_path + + return rel_path + + def get_parser(prog_name: str, others: dict = {}) -> argparse.Namespace: """ Initiates argparse.ArgumentParser() and adds common arguments.