diff --git a/dcm/dcm_assessment.py b/dcm/dcm_assessment.py index 2aaaf1c..c7f5670 100644 --- a/dcm/dcm_assessment.py +++ b/dcm/dcm_assessment.py @@ -310,38 +310,45 @@ def update_achievement_view(self, step: int = 0): step = 0 if self.achievement_index + step < len(self.learner.achievements): self.achievement_index += step - self.index_view.text = self.get_index_str() - achievement = self.current_achievement - self.webserver.render_dcm( - self.dcm, - self.learner, - selected_paths=[achievement.path], - clear_assessment=False - ) - self.button_row.achievement = achievement - self.button_row.set_button_states(achievement) - competence_element = self.competence_tree.lookup_by_path(achievement.path) - if not competence_element: - ui.notify("invalid path: {achievement.path}") - self.markdown_view.content = f"⚠️ {achievement.path}" - else: - if hasattr(competence_element, "path"): - if competence_element.url: - link = Link.create( - competence_element.url, competence_element.path - ) - else: - link = competence_element.path - else: - link = "⚠️ - competence element path missing" - self.link_view.content = link - description = competence_element.description or "" - if isinstance(competence_element, CompetenceArea): - aspect = competence_element.aspect - description = f"### {aspect.name}\n\n**{competence_element.name}**:\n\n{description}" - if isinstance(competence_element, CompetenceFacet): - area = competence_element.area - description = f"### {area.name}\n\n**{competence_element.name}**:\n\n{description}" - self.markdown_view.content = description else: ui.notify("Done!") + self.update_current_achievement_view() + + def update_current_achievement_view(self): + """ + show the current achievement + """ + self.index_view.text = self.get_index_str() + achievement = self.current_achievement + self.webserver.render_dcm( + self.dcm, + self.learner, + selected_paths=[achievement.path], + clear_assessment=False + ) + self.button_row.achievement = achievement + self.button_row.set_button_states(achievement) + competence_element = self.competence_tree.lookup_by_path(achievement.path) + if not competence_element: + ui.notify("invalid path: {achievement.path}") + self.markdown_view.content = f"⚠️ {achievement.path}" + else: + if hasattr(competence_element, "path"): + if competence_element.url: + link = Link.create( + competence_element.url, competence_element.path + ) + else: + link = competence_element.path + else: + link = "⚠️ - competence element path missing" + self.link_view.content = link + description = competence_element.description or "" + if isinstance(competence_element, CompetenceArea): + aspect = competence_element.aspect + description = f"### {aspect.name}\n\n**{competence_element.name}**:\n\n{description}" + if isinstance(competence_element, CompetenceFacet): + area = competence_element.area + description = f"### {area.name}\n\n**{competence_element.name}**:\n\n{description}" + self.markdown_view.content = description + diff --git a/dcm/dcm_chart.py b/dcm/dcm_chart.py index efe3010..5735ae1 100644 --- a/dcm/dcm_chart.py +++ b/dcm/dcm_chart.py @@ -13,7 +13,7 @@ DynamicCompetenceMap, Learner, ) -from dcm.svg import SVG, DonutSegment, SVGConfig +from dcm.svg import SVG, DonutSegment, SVGConfig, SVGNodeConfig class DcmChart: @@ -26,6 +26,107 @@ def __init__(self, dcm: DynamicCompetenceMap): Constructor """ self.dcm = dcm + + def precalculate_segments(self, competence_tree: CompetenceTree) -> dict: + """ + Pre-calculate the DonutSegment for each element in the CompetenceTree + and store it in a dictionary by path for quick lookup. + + Args: + competence_tree (CompetenceTree): The competence tree to precalculate the segments for. + + Returns: + dict: A dictionary mapping paths to their corresponding DonutSegment. + """ + segment_by_path = {} + full_circle = 360 + aspect_angle = full_circle / len(competence_tree.aspects) if competence_tree.aspects else full_circle + + for aspect in competence_tree.aspects: + start_angle_aspect = 0 + for area_index, area in enumerate(aspect.areas): + end_angle_aspect = start_angle_aspect + aspect_angle + + # Create a DonutSegment for the area + segment_by_path[area.path] = DonutSegment( + inner_radius=self.tree_radius, + outer_radius=self.tree_radius * 2, # Modify as needed + start_angle=start_angle_aspect, + end_angle=end_angle_aspect, + fill="white" # Default fill color for segments with no elements + ) + + # Calculate and store segments for sub-elements (facets) + for facet_index, facet in enumerate(area.facets): + facet_angle = aspect_angle / len(area.facets) + start_angle_facet = start_angle_aspect + (facet_index * facet_angle) + end_angle_facet = start_angle_facet + facet_angle + + segment_by_path[facet.path] = DonutSegment( + inner_radius=self.tree_radius * 2, # Modify as needed + outer_radius=self.tree_radius * 3, # Modify as needed + start_angle=start_angle_facet, + end_angle=end_angle_facet, + fill=facet.color_code if facet.color_code else "white" + ) + + # Update the start angle for the next area within the same aspect. + start_angle_aspect = end_angle_aspect + + return segment_by_path + + def generate_svg_from_segments(self, + competence_tree:CompetenceTree, + config: Optional[SVGConfig] = None) -> str: + """ + Generate the SVG markup using pre-calculated DonutSegment objects stored in segments_by_path. + + Args: + competence_tree(CompetenceTree): a competence tree + segments_by_path (dict): A dictionary mapping element paths to their corresponding DonutSegment objects. + config (SVGConfig, optional): The configuration for the SVG canvas and legend. Defaults to default values. + + Returns: + str: The SVG markup. + """ + if config is None: + config = SVGConfig() # Use default configuration if none provided + svg=self.prepare_and_add_inner_circle(config, competence_tree=competence_tree) + # Pre-calculate segments for the competence tree + segments_by_path = self.precalculate_segments(competence_tree) + # Iterate over the segments and add them to the SVG + for path, segment in segments_by_path.items(): + element=competence_tree.lookup_by_path(path) + config=self.get_element_config(element) + svg.add_donut_segment(config, segment) + return svg.get_svg_markup() + + def prepare_and_add_inner_circle(self, + config, + competence_tree:CompetenceTree, + lookup_url:str=None): + """ + prepare the SVG markup generation and add + the inner_circle + """ + self.lookup_url = ( + competence_tree.lookup_url if competence_tree.lookup_url else lookup_url + ) + + svg = SVG(config) + self.svg = svg + config = svg.config + # center of circle + self.cx = config.width // 2 + self.cy = (config.total_height - config.legend_height) // 2 + self.tree_radius = config.width / 2 / 8 + self.circle_config = competence_tree.to_svg_node_config( + x=self.cx, + y=self.cy, + width=self.tree_radius + ) + svg.add_circle(config=self.circle_config) + return svg def generate_svg( self, @@ -54,16 +155,8 @@ def generate_svg( self.save_svg_to_file(svg_markup, filename) return svg_markup - def add_donut_segment(self, - svg: SVG, - element: CompetenceElement, - segment: DonutSegment, - level_color=None, - achievement_level=None - )->DonutSegment: + def get_element_config(self,element:CompetenceElement)->SVGNodeConfig: """ - create a donut segment for the - given competence element and add it to the given SVG """ element_url = ( element.url @@ -74,11 +167,25 @@ def add_donut_segment(self, ) show_as_popup = element.url is None element_config = element.to_svg_node_config( - url=element_url, - show_as_popup=show_as_popup, - x=self.cx, - y=self.cy, + url=element_url, + show_as_popup=show_as_popup, + x=self.cx, + y=self.cy, ) + return element_config + + def add_donut_segment(self, + svg: SVG, + element: CompetenceElement, + segment: DonutSegment, + level_color=None, + achievement_level=None + )->DonutSegment: + """ + create a donut segment for the + given competence element and add it to the given SVG + """ + element_config=self.get_element_config(element) if level_color: element_config.fill = level_color # Set the color @@ -236,25 +343,12 @@ def generate_svg_markup( if competence_tree is None: competence_tree = self.dcm.competence_tree self.selected_paths=selected_paths - svg = SVG(config) - self.svg = svg - config = svg.config - # center of circle - self.cx = config.width // 2 - self.cy = (config.total_height - config.legend_height) // 2 self.levels = ["aspects", "areas", "facets"] - self.tree_radius = config.width / 2 / 8 - - self.lookup_url = ( - competence_tree.lookup_url if competence_tree.lookup_url else lookup_url - ) - - circle_config = competence_tree.to_svg_node_config( - x=self.cx, - y=self.cy, - width=self.tree_radius - ) - svg.add_circle(config=circle_config) + + svg=self.prepare_and_add_inner_circle( + config, + competence_tree, + lookup_url) segment = DonutSegment( inner_radius=0, @@ -267,7 +361,7 @@ def generate_svg_markup( learner=learner, segment=segment, ) - if config.legend_height > 0: + if svg.config.legend_height > 0: competence_tree.add_legend(svg) return svg.get_svg_markup(with_java_script=with_java_script) diff --git a/dcm/dcm_webserver.py b/dcm/dcm_webserver.py index b934bcd..fa98a15 100644 --- a/dcm/dcm_webserver.py +++ b/dcm/dcm_webserver.py @@ -210,7 +210,8 @@ async def render(self, _click_args=None): input_source = self.input if input_source: name = self.get_basename_without_extension(input_source) - ui.notify(f"rendering {name}") + with self.container: + ui.notify(f"rendering {name}") definition = self.do_read_input(input_source) # Determine the format based on the file extension markup = "json" if input_source.endswith(".json") else "yaml" @@ -278,7 +279,7 @@ async def home(self, _client: Client): self.setup_menu() - with ui.element("div").classes("w-full"): + with ui.element("div").classes("w-full") as self.container: with ui.splitter() as splitter: with splitter.before: extensions = {"json": ".json", "yaml": ".yaml"} diff --git a/dcm/svg.py b/dcm/svg.py index b1e2db3..93ddbba 100644 --- a/dcm/svg.py +++ b/dcm/svg.py @@ -64,13 +64,14 @@ class SVGNodeConfig: @dataclass class DonutSegment: """ - a donut segment + A donut segment representing a + section of a donut chart. """ - inner_radius: float outer_radius: float start_angle: Optional[float] = 0.0 end_angle: Optional[float] = 360.0 + fill: Optional[str] = None # Optional fill color for the segment class SVG: diff --git a/tests/test_competence_map.py b/tests/test_competence_map.py index 8f7af5d..5c1ec61 100644 --- a/tests/test_competence_map.py +++ b/tests/test_competence_map.py @@ -137,3 +137,21 @@ def testCompetenceMap(self): dcm_chart.generate_svg(svg_file, config=svg_config) markup_check = MarkupCheck(self, dcm) markup_check.check_markup(svg_file=svg_file, svg_config=svg_config) + + def test_generate_svg_from_segments(self): + """ + Test the SVG generation from precalculated DonutSegment objects. + """ + for markup, examples in self.example_definitions.items(): + + for example_name, dcm in examples.items(): + # Now you can perform assertions to verify that the data was loaded correctly + self.assertIsNotNone(dcm) + dcm_chart = DcmChart(dcm) + + # Generate SVG markup + svg_config=SVGConfig() + svg_markup = dcm_chart.generate_svg_from_segments(dcm.competence_tree,config=svg_config) + svg_file = f"/tmp/{example_name}_{markup}_segments.svg" + with open(svg_file, "w") as file: + file.write(svg_markup) \ No newline at end of file