From 2f26af6e18bdce5abce3ae499eedb724a5d70abe Mon Sep 17 00:00:00 2001 From: Wolfgang Fahl Date: Tue, 16 Jan 2024 11:43:12 +0100 Subject: [PATCH] fixes achievement coloring and segment handling --- dcm/dcm_assessment.py | 11 ++- dcm/dcm_chart.py | 132 +++++++++++++++++++++++++++++------ dcm/dcm_core.py | 27 +++++++ dcm/dcm_webserver.py | 49 +++++++++---- tests/test_competence_map.py | 4 +- 5 files changed, 182 insertions(+), 41 deletions(-) diff --git a/dcm/dcm_assessment.py b/dcm/dcm_assessment.py index f07f029..2aaaf1c 100644 --- a/dcm/dcm_assessment.py +++ b/dcm/dcm_assessment.py @@ -204,8 +204,8 @@ def setup_ui(self): total=self.total, desc="self assessment", unit="facets" ) self.progress_bar.reset() - facet_element_name=self.competence_tree.element_names["facet"] - area_element_name=self.competence_tree.element_names["area"] + facet_element_name=self.competence_tree.element_names.get("facet") or "facet" + area_element_name=self.competence_tree.element_names.get("area") or "area" with ui.row() as self.navigation_row: ui.button("", @@ -305,7 +305,6 @@ def update_achievement_view(self, step: int = 0): display the active achievement as the step indicates """ self.show_progress() - self.webserver.render_dcm(self.dcm, self.learner, clear_assessment=False) if self.achievement_index + step < 0: ui.notify("first achievement reached!") step = 0 @@ -313,6 +312,12 @@ def update_achievement_view(self, step: int = 0): 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) diff --git a/dcm/dcm_chart.py b/dcm/dcm_chart.py index 6cc1759..efe3010 100644 --- a/dcm/dcm_chart.py +++ b/dcm/dcm_chart.py @@ -3,8 +3,8 @@ @author: wf """ -from dataclasses import dataclass from typing import List, Optional +import copy from dcm.dcm_core import ( CompetenceElement, @@ -53,19 +53,18 @@ def generate_svg( if filename: self.save_svg_to_file(svg_markup, filename) return svg_markup - - def generate_donut_segment_for_element( - self, - svg: SVG, - element: CompetenceElement, - learner: Learner, - segment: DonutSegment, - ): + + def add_donut_segment(self, + svg: SVG, + element: CompetenceElement, + segment: DonutSegment, + level_color=None, + achievement_level=None + )->DonutSegment: """ - generate a donut segment for a given element of - the CompetenceTree + create a donut segment for the + given competence element and add it to the given SVG """ - # Add the element segment as a donut segment element_url = ( element.url if element.url @@ -80,13 +79,73 @@ def generate_donut_segment_for_element( x=self.cx, y=self.cy, ) + + if level_color: + element_config.fill = level_color # Set the color + if element.path in self.selected_paths: + element_config.element_class = "selected" + + if achievement_level is not None: + total_levels = self.dcm.competence_tree.total_valid_levels + relative_radius = (segment.outer_radius - segment.inner_radius) * (achievement_level / total_levels) + segment.outer_radius = segment.inner_radius + relative_radius + + result=svg.add_donut_segment(config=element_config, segment=segment) + return result + + def generate_donut_segment_for_achievement( + self, + svg: SVG, + learner: Learner, + element: CompetenceElement, + segment: DonutSegment, + )->DonutSegment: + """ + generate a donut segment for the + learner's achievements + corresponding to the given path and return it's segment definition + """ + achievement = learner.achievements_by_path.get(element.path, None) + result=None + if achievement and achievement.level: + # Retrieve the color for the achievement level + level_color = self.dcm.competence_tree.get_level_color(achievement.level) + + if level_color: + # set the color and radius of + # the segment for achievement + # make sure we don't interfere with the segment calculations + segment=copy.deepcopy(segment) + result=self.add_donut_segment(svg, element, segment, level_color, achievement.level) + return result + + def generate_donut_segment_for_element( + self, + svg: SVG, + element: CompetenceElement, + learner: Learner, + segment: DonutSegment, + )->DonutSegment: + """ + generate a donut segment for a given element of + the CompetenceTree + """ + # Simply create the donut segment without considering the achievement + result=self.add_donut_segment( + svg=svg, + element=element, + segment=segment + ) # check learner achievements if learner: - achievement = learner.achievements_by_path.get(element.path, None) - if achievement and achievement.level: - element_config.element_class = "selected" - svg.add_donut_segment(config=element_config, segment=segment) - + _learner_segment=self.generate_donut_segment_for_achievement( + svg=svg, + learner=learner, + element=element, + segment=segment + ) + return result + def generate_pie_elements( self, level: int, @@ -119,7 +178,10 @@ def generate_pie_elements( end_angle, ) self.generate_donut_segment_for_element( - svg, element, learner, segment=sub_segment + svg, + element, + learner, + segment=sub_segment ) start_angle = end_angle if level + 1 < len(self.levels): @@ -135,19 +197,45 @@ def generate_svg_markup( self, competence_tree: CompetenceTree = None, learner: Learner = None, + selected_paths: List=[], config: SVGConfig = None, with_java_script: bool = True, lookup_url: str = "", ) -> str: """ - generate the SVG markup for the given CompetenceTree and learner + Generate the SVG markup for the given CompetenceTree and Learner. This method + creates an SVG representation of the competence map, which visualizes the + structure and levels of competencies, along with highlighting the learner's + achievements if provided. - Args: + Args: + competence_tree (CompetenceTree, optional): The competence tree structure + to be visualized. If None, the competence tree of the DcmChart instance + will be used. Defaults to None. + learner (Learner, optional): The learner whose achievements are to be + visualized on the competence tree. If None, no learner-specific + information will be included in the SVG. Defaults to None. + selected_paths (List, optional): A list of paths that should be highlighted + in the SVG. These paths typically represent specific competencies or + achievements. Defaults to an empty list. + config (SVGConfig, optional): Configuration for the SVG canvas and legend. + If None, default configuration settings are used. Defaults to None. + with_java_script (bool, optional): Indicates whether to include JavaScript + in the SVG for interactivity. Defaults to True. + lookup_url (str, optional): Base URL for linking to detailed descriptions + or information about the competence elements. If not provided, links + will not be generated. Defaults to an empty string. - """ + Returns: + str: A string containing the SVG markup for the competence map. + + Raises: + ValueError: If there are inconsistencies or issues with the provided data + that prevent the creation of a valid SVG. + """ 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 diff --git a/dcm/dcm_core.py b/dcm/dcm_core.py index 6945e8d..8b92bac 100644 --- a/dcm/dcm_core.py +++ b/dcm/dcm_core.py @@ -233,6 +233,33 @@ def handle_error(msg): return facet handle_error(f"invalid path for lookup {path}") return None + + @property + def total_valid_levels(self) -> int: + """ + Calculate the total number of levels excluding + levels with a level of 0. + + Returns: + int: The total number of valid levels. + """ + level_count= len([level for level in self.levels if level.level != 0]) + return level_count + + def get_level_color(self, achievement_level: int) -> Optional[str]: + """ + Retrieve the color associated with a specific achievement level. + + Args: + achievement_level (int): The level of achievement to get the color for. + + Returns: + Optional[str]: The color code associated with the given level, or None if not found. + """ + for level in self.levels: + if level.level == achievement_level: + return level.color_code + return None def to_pretty_json(self): """ diff --git a/dcm/dcm_webserver.py b/dcm/dcm_webserver.py index 50afc35..b934bcd 100644 --- a/dcm/dcm_webserver.py +++ b/dcm/dcm_webserver.py @@ -4,7 +4,7 @@ @author: wf """ import os -from typing import Optional +from typing import List,Optional from urllib.parse import urlparse from fastapi import HTTPException @@ -229,23 +229,42 @@ async def render(self, _click_args=None): except BaseException as ex: self.handle_exception(ex, self.do_trace) - def render_dcm(self, dcm, learner: Learner = None, clear_assessment: bool = True): + def render_dcm(self, + dcm, + learner: Learner = None, + selected_paths: List=[], + clear_assessment: bool = True + ): """ render the dynamic competence map + + Args: + dcm(DynamicCompetenceMap) + selected_paths (List, optional): A list of paths that should be highlighted + in the SVG. These paths typically represent specific competencies or + achievements. Defaults to an empty list. + """ - if clear_assessment and self.assessment: - try: - self.assessment_row.clear() - except Exception as ex: - ui.notify(str(ex)) - self.assessment = None - self.dcm = dcm - self.assessment_button.enable() - dcm_chart = DcmChart(dcm) - svg = dcm_chart.generate_svg_markup(learner=learner, with_java_script=False) - # Use the new get_java_script method to get the JavaScript - self.svg_view.content = svg - self.svg_view.update() + try: + if clear_assessment and self.assessment: + try: + self.assessment_row.clear() + except Exception as ex: + ui.notify(str(ex)) + self.assessment = None + self.dcm = dcm + self.assessment_button.enable() + dcm_chart = DcmChart(dcm) + svg = dcm_chart.generate_svg_markup( + learner=learner, + selected_paths=selected_paths, + with_java_script=False + ) + # Use the new get_java_script method to get the JavaScript + self.svg_view.content = svg + self.svg_view.update() + except Exception as ex: + self.handle_exception(ex, self.do_trace) async def home(self, _client: Client): """Generates the home page with a selection of examples and diff --git a/tests/test_competence_map.py b/tests/test_competence_map.py index 04a998b..8f7af5d 100644 --- a/tests/test_competence_map.py +++ b/tests/test_competence_map.py @@ -54,7 +54,9 @@ def test_element_lookup(self): self.assertTrue(example_name in examples) example = examples[example_name] path="greta/4/1/2" - facet = example.competence_tree.lookup_by_path(path) + ct=example.competence_tree + self.assertEqual(4,ct.total_valid_levels) + facet = ct.lookup_by_path(path) self.assertIsNotNone(facet) self.assertIsInstance(facet, CompetenceFacet) html = facet.as_html()