Skip to content

Commit

Permalink
fixes #21
Browse files Browse the repository at this point in the history
  • Loading branch information
WolfgangFahl committed Jan 17, 2024
1 parent 2f26af6 commit 0474791
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 70 deletions.
73 changes: 40 additions & 33 deletions dcm/dcm_assessment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

160 changes: 127 additions & 33 deletions dcm/dcm_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
DynamicCompetenceMap,
Learner,
)
from dcm.svg import SVG, DonutSegment, SVGConfig
from dcm.svg import SVG, DonutSegment, SVGConfig, SVGNodeConfig


class DcmChart:
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions dcm/dcm_webserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"}
Expand Down
5 changes: 3 additions & 2 deletions dcm/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions tests/test_competence_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 0474791

Please sign in to comment.