Skip to content

Commit b899159

Browse files
committed
✨ Feat: Added html component and tested in streamlit and quarto reports
1 parent 1717320 commit b899159

File tree

7 files changed

+195
-4
lines changed

7 files changed

+195
-4
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Interactive Chart Example</title>
5+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
6+
</head>
7+
<body>
8+
<h1>Interactive Chart</h1>
9+
<canvas id="myChart" width="400" height="200"></canvas>
10+
<script>
11+
var ctx = document.getElementById('myChart').getContext('2d');
12+
var myChart = new Chart(ctx, {
13+
type: 'line',
14+
data: {
15+
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
16+
datasets: [{
17+
label: 'Sample Data',
18+
data: [12, 19, 3, 5, 2, 3, 9],
19+
borderColor: 'rgba(75, 192, 192, 1)',
20+
borderWidth: 1
21+
}]
22+
}
23+
});
24+
</script>
25+
</body>
26+
</html>

docs/example_data/MicW2Graph/MicW2Graph_config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ sections:
6767
title: Test Md
6868
file_path: /Users/asaru/Documents/DTU/MoNA/vuegen/docs/example_data/MicW2Graph/1_Exploratory_Data_Analysis/3_Extra_info/1_test_md.md
6969
description: ''
70+
- component_type: html
71+
title: Plot
72+
file_path: /Users/asaru/Documents/DTU/MoNA/vuegen/docs/example_data/MicW2Graph/1_Exploratory_Data_Analysis/3_Extra_info/plot.html
73+
description: ''
7074
- title: Microbial Association Networks
7175
description: ''
7276
subsections:

vuegen/config_manager.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Dict, List, Optional, Tuple, Union
66

77
from . import report as r
8-
from .utils import assert_enum_value, get_logger
8+
from .utils import assert_enum_value, get_logger, is_pyvis_html
99

1010

1111
class ConfigManager:
@@ -80,6 +80,12 @@ def _create_component_config_fromfile(self, file_path: Path) -> Dict[str, str]:
8080
elif file_ext in [fmt.value_with_dot for fmt in r.DataFrameFormat if fmt not in [r.DataFrameFormat.CSV, r.DataFrameFormat.TXT]]:
8181
component_config ["component_type"] = r.ComponentType.DATAFRAME.value
8282
component_config ["file_format"] = next(fmt.value for fmt in r.DataFrameFormat if fmt.value_with_dot == file_ext)
83+
elif file_ext == ".html":
84+
if is_pyvis_html(file_path):
85+
component_config["component_type"] = r.ComponentType.PLOT.value
86+
component_config["plot_type"] = r.PlotType.INTERACTIVE_NETWORK.value
87+
else:
88+
component_config["component_type"] = r.ComponentType.HTML.value
8389
# Check for network formats
8490
elif file_ext in [fmt.value_with_dot for fmt in r.NetworkFormat]:
8591
component_config ["component_type"] = r.ComponentType.PLOT.value
@@ -370,6 +376,8 @@ def _create_component(self, component_data: dict) -> r.Component:
370376
return self._create_dataframe_component(component_data)
371377
elif component_type == r.ComponentType.MARKDOWN:
372378
return self._create_markdown_component(component_data)
379+
elif component_type == r.ComponentType.HTML:
380+
return self._create_html_component(component_data)
373381
elif component_type == r.ComponentType.APICALL:
374382
return self._create_apicall_component(component_data)
375383
elif component_type == r.ComponentType.CHATBOT:
@@ -450,6 +458,27 @@ def _create_markdown_component(self, component_data: dict) -> r.Markdown:
450458
caption = component_data.get('caption')
451459
)
452460

461+
def _create_html_component(self, component_data: dict) -> r.Html:
462+
"""
463+
Creates an Html component.
464+
465+
Parameters
466+
----------
467+
component_data : dict
468+
A dictionary containing hml component metadata.
469+
470+
Returns
471+
-------
472+
Html
473+
An Html object populated with the provided metadata.
474+
"""
475+
return r.Html(
476+
title = component_data['title'],
477+
logger = self.logger,
478+
file_path = component_data['file_path'],
479+
caption = component_data.get('caption')
480+
)
481+
453482
def _create_apicall_component(self, component_data: dict) -> r.APICall:
454483
"""
455484
Creates an APICall component.

vuegen/quarto_reportview.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,12 @@ def _generate_subsection(self, subsection, is_report_static, is_report_revealjs)
237237

238238
if component.component_type == r.ComponentType.PLOT:
239239
subsection_content.extend(self._generate_plot_content(component, is_report_static))
240-
241240
elif component.component_type == r.ComponentType.DATAFRAME:
242241
subsection_content.extend(self._generate_dataframe_content(component, is_report_static))
243-
244242
elif component.component_type == r.ComponentType.MARKDOWN:
245243
subsection_content.extend(self._generate_markdown_content(component))
244+
elif component.component_type == r.ComponentType.HTML and not is_report_static:
245+
subsection_content.extend(self._generate_html_content(component))
246246
else:
247247
self.report.logger.warning(f"Unsupported component type '{component.component_type}' in subsection: {subsection.title}")
248248

@@ -489,6 +489,41 @@ def _generate_markdown_content(self, markdown) -> List[str]:
489489
self.report.logger.info(f"Successfully generated content for Markdown: '{markdown.title}'")
490490
return markdown_content
491491

492+
def _generate_html_content(self, html) -> List[str]:
493+
"""
494+
Adds an HTML component to the report.
495+
496+
Parameters
497+
----------
498+
html : Html
499+
The HTML component to add to the report. This could be a local file path or a URL.
500+
501+
Returns
502+
-------
503+
list : List[str]
504+
The list of content lines for embedding the HTML.
505+
"""
506+
html_content = []
507+
508+
# Add title
509+
html_content.append(f'### {html.title}')
510+
511+
try:
512+
# Embed the HTML in an iframe
513+
iframe_src = html.file_path if is_url(html.file_path) else os.path.join("..", html.file_path)
514+
iframe_code = f"""
515+
<div style="text-align: center;">
516+
<iframe src="{iframe_src}" alt="{html.title}" width="800px" height="630px"></iframe>
517+
</div>\n"""
518+
html_content.append(iframe_code)
519+
520+
except Exception as e:
521+
self.report.logger.error(f"Error generating content for HTML: {html.title}. Error: {str(e)}")
522+
raise
523+
524+
self.report.logger.info(f"Successfully generated content for HTML: '{html.title}'")
525+
return html_content
526+
492527
def _generate_image_content(self, image_path: str, alt_text: str = "", width: int = 650, height: int = 400) -> str:
493528
"""
494529
Adds an image to the content list in an HTML format with a specified width and height.

vuegen/report.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class ComponentType(StrEnum):
3535
PLOT = auto()
3636
DATAFRAME = auto()
3737
MARKDOWN = auto()
38+
HTML = auto()
3839
APICALL = auto()
3940
CHATBOT = auto()
4041

@@ -380,10 +381,21 @@ class Markdown(Component):
380381
"""
381382
def __init__(self, title: str, logger: logging.Logger, file_path: str=None, caption: str=None):
382383
"""
383-
Initializes a DataFrame object.
384+
Initializes a Markdown object.
384385
"""
385386
super().__init__(title = title, logger = logger, component_type=ComponentType.MARKDOWN,
386387
file_path=file_path, caption=caption)
388+
389+
class Html(Component):
390+
"""
391+
An html component within a subsection of a report.
392+
"""
393+
def __init__(self, title: str, logger: logging.Logger, file_path: str=None, caption: str=None):
394+
"""
395+
Initializes an html object.
396+
"""
397+
super().__init__(title = title, logger = logger, component_type=ComponentType.HTML,
398+
file_path=file_path, caption=caption)
387399

388400
class APICall(Component):
389401
"""

vuegen/streamlit_reportview.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,8 @@ def _generate_subsection(self, subsection) -> tuple[List[str], List[str]]:
298298
subsection_content.extend(self._generate_dataframe_content(component))
299299
elif component.component_type == r.ComponentType.MARKDOWN:
300300
subsection_content.extend(self._generate_markdown_content(component))
301+
elif component.component_type == r.ComponentType.HTML:
302+
subsection_content.extend(self._generate_html_content(component))
301303
elif component.component_type == r.ComponentType.APICALL:
302304
subsection_content.extend(self._generate_apicall_content(component))
303305
elif component.component_type == r.ComponentType.CHATBOT:
@@ -517,6 +519,52 @@ def _generate_markdown_content(self, markdown) -> List[str]:
517519
self.report.logger.info(f"Successfully generated content for Markdown: '{markdown.title}'")
518520
return markdown_content
519521

522+
def _generate_html_content(self, html) -> List[str]:
523+
"""
524+
Generate content for an HTML component in a Streamlit app.
525+
526+
Parameters
527+
----------
528+
html : HTML
529+
The HTML component to generate content for.
530+
531+
Returns
532+
-------
533+
list : List[str]
534+
The list of content lines for the HTML display.
535+
"""
536+
html_content = []
537+
538+
# Add title
539+
html_content.append(self._format_text(text=html.title, type='header', level=4, color='#2b8cbe'))
540+
541+
try:
542+
if is_url(html.file_path):
543+
# If it's a URL, fetch content dynamically
544+
html_content.append(f"""
545+
response = requests.get('{html.file_path}')
546+
response.raise_for_status()
547+
html_content = response.text\n""")
548+
else:
549+
# If it's a local file
550+
html_content.append(f"""
551+
with open('{os.path.join("..", html.file_path)}', 'r', encoding='utf-8') as html_file:
552+
html_content = html_file.read()\n""")
553+
554+
# Display HTML content using Streamlit
555+
html_content.append("st.components.v1.html(html_content, height=600, scrolling=True)\n")
556+
557+
except Exception as e:
558+
self.report.logger.error(f"Error generating content for HTML: {html.title}. Error: {str(e)}")
559+
raise
560+
561+
# Add caption if available
562+
if html.caption:
563+
html_content.append(self._format_text(text=html.caption, type='caption', text_align="left"))
564+
565+
self.report.logger.info(f"Successfully generated content for HTML: '{html.title}'")
566+
return html_content
567+
520568
def _generate_apicall_content(self, apicall) -> List[str]:
521569
"""
522570
Generate content for a Markdown component.

vuegen/utils.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,43 @@ def is_url(filepath: str) -> bool:
108108
return bool(parsed_url.scheme and parsed_url.netloc)
109109

110110

111+
import os
112+
from bs4 import BeautifulSoup
113+
114+
def is_pyvis_html(filepath: str) -> bool:
115+
"""
116+
Check if the provided HTML file is a Pyvis network visualization.
117+
118+
Parameters
119+
----------
120+
filepath : str
121+
The path to the HTML file to check.
122+
123+
Returns
124+
-------
125+
bool
126+
True if the input HTML file is a Pyvis network, meaning:
127+
- It contains a `<div>` element with `id="mynetwork"`.
128+
- The `<body>` only contains `<div>` and `<script>` elements.
129+
Returns False otherwise.
130+
131+
"""
132+
# Parse the HTML file
133+
with open(filepath, "r", encoding="utf-8") as f:
134+
soup = BeautifulSoup(f, "html.parser")
135+
136+
# Validate both conditions
137+
pyvis_identifier_valid = bool(soup.find("div", {"id": "mynetwork"}))
138+
139+
# Count top-level elements inside <body>
140+
body_children = [tag.name for tag in soup.body.find_all(recursive=False)]
141+
142+
# A pure Pyvis file should contain only "div" and "script" elements in <body>
143+
body_structure_valid = set(body_children) <= {"div", "script"}
144+
145+
# Both conditions must be true
146+
return pyvis_identifier_valid and body_structure_valid
147+
111148
## FILE_SYSTEM
112149
def create_folder(directory_path: str, is_nested: bool = False) -> bool:
113150
"""

0 commit comments

Comments
 (0)