Skip to content

Commit

Permalink
Support progress bar for skill.
Browse files Browse the repository at this point in the history
  • Loading branch information
niyue committed Mar 1, 2022
1 parent 6d07dc5 commit 1de0a68
Show file tree
Hide file tree
Showing 15 changed files with 281 additions and 244 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist
tests/__pycache__
skillmap/__pycache__
__pycache__
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# 0.3.0 - 2022-03-01
* support progress bar for skill
# 0.2.3 - 2022-02-27
* add a default skill map name and mark python 3.8 to be the min version
# 0.2.2 - 2022-02-27
Expand Down
5 changes: 1 addition & 4 deletions docs/skillmap_descriptor.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@
* `name`: [optional] the name of the skillmap/group/skill. It will be used as a label in the diagram. .
* `icon`: [optional] a fontawsome icon name. It will be used as an icon in the diagram. You can find the fontawsome icon list [here](https://fontawesome.com/v4.7.0/icons/).
* `requires`: [optional] a list of strings. It indicates a list of skill groups or skills to be learned before this learning this skill group/skill. where each string is a toml table name of a group/skill. It will be rendered as an edge(s) from one node to another.
* `status`: [optional] only applies to a `skill` toml table. It indicates the status of the skill. Nodes will be rendered with different colors according to different statuses. It can be one of the following:
* `new`: the skill is newly unveiled and not master/learned yet (default value if not specified)
* `beingLearned`: the skill is being leanred currently.
* `learned`: the skill is learned.
* `progress`: [optional] only applies to a `skill` toml table. It is a fraction number string like `1/3` that indicates the learning progression of the skill. A progress bar like `■□□` will be shown in the skill node to visualize the progress. Skill nodes will be rendered with different colors according to different progresses (zero progress, ongoing, finished).
* locked skill: if a skill table doesn't have name or icon, it will be rendered as a locked skill (a grey box + lock icon + `???` as name).

## Example
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "skillmap"
version = "0.2.3"
version = "0.3.0"
description = "Skillmap generates a skill tree from a toml file"
authors = ["Yue Ni <[email protected]>"]
readme = "README.md"
Expand Down
11 changes: 2 additions & 9 deletions skillmap/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from distutils.util import strtobool

from skillmap.skillmap_parser import SkillMapParser
from skillmap.skillmap_visitor import SkillMapVisitor
from skillmap.nodes.skillmap_node import create_skillmap_graph

import importlib.metadata

Expand All @@ -32,13 +32,6 @@ def _skillmap_parser():
help="show version number",
)

# parser.add_argument(
# "-f",
# "--format",
# type=str,
# default="mermaid",
# help="export format, [mermaid] are supported",
# )
return parser


Expand All @@ -50,7 +43,7 @@ def parse_sys_args(sys_args):

def generate(skillmap_file, format = None):
skillmap_dict = SkillMapParser().parse(skillmap_file)
skillmap_graph = SkillMapVisitor().visit(skillmap_dict)
skillmap_graph = create_skillmap_graph(skillmap_dict)
return skillmap_graph


Expand Down
Empty file added skillmap/nodes/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions skillmap/nodes/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
SECTION_SEPARATOR = "%" * 30

def get_required_node_edges(qualified_node_id, required_node_ids):
node_requires = [
f"{required_node_id}-->{qualified_node_id}"
for required_node_id in required_node_ids
]
return "\n".join(node_requires)


def get_icon(dict_value):
icon = ""
if "icon" in dict_value:
icon = f"fa:fa-{dict_value['icon']}"
return icon


def get_node_content(items, multi_lines_layout=True):
separator = "<br/>" if multi_lines_layout else " "
# join non empty items
return separator.join([i for i in items if i])
76 changes: 76 additions & 0 deletions skillmap/nodes/group_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from skillmap.nodes.common import get_icon, get_node_content, get_required_node_edges, SECTION_SEPARATOR
from skillmap.nodes.skill_node import create_skill_node


def _qualify(group_id):
return f"groups.{group_id}"


def _qualified_skill_id(qualified_group_id, skill_id):
return f"{qualified_group_id}.skills.{skill_id}"


def create_groups_edges(map_id, groups):
group_ids = [
_qualify(group_id)
for group_id, group_value in groups.items()
if "requires" not in group_value # all groups without requires
]
groups_edges = "\n".join([f"{map_id}-->{gid}" for gid in group_ids])
return groups_edges


def get_group_skills_list(qualified_group_id, group_skills):
group_skills = [
create_skill_node(
_qualified_skill_id(qualified_group_id, skill_id), skill_value
)
for skill_id, skill_value in group_skills.items()
]
return "\n".join(group_skills)


def get_group_status(group_skills):
for _, skill_value in group_skills.items():
skill_status = skill_value.get("status", "")
if skill_status not in ("new", ""):
return "normal"
return "new"


def create_group_subgraph(group_id, group_value):
qualified_group_id = _qualify(group_id)
group_name = group_value.get("name", "")
group_icon = get_icon(group_value)
group_icon_label = get_node_content([group_icon, group_name], False)
group_skills_list = get_group_skills_list(
qualified_group_id, group_value.get("skills", {})
)
group_status = get_group_status(group_value.get("skills", {}))

group_requires_list = get_required_node_edges(
qualified_group_id, group_value.get("requires", [])
)

group_id_and_name = f"subgraph {qualified_group_id}[{group_icon_label}]"
group_style = f"class {qualified_group_id} {group_status}SkillGroup;"
group_subgraph_end = "end"
sections = [
SECTION_SEPARATOR,
group_id_and_name,
group_skills_list,
group_subgraph_end,
group_style,
group_requires_list,
]

group_graph = "\n".join(sections)
return group_graph


def create_group_subgraphs(groups):
group_graphs = [
create_group_subgraph(group_id, group_value)
for group_id, group_value in groups.items()
]
return "\n\n".join(group_graphs)
50 changes: 50 additions & 0 deletions skillmap/nodes/skill_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from skillmap.nodes.common import get_icon, get_node_content, get_required_node_edges
from fractions import Fraction
from enum import Enum

class Status(Enum):
NEW = "new"
BEING_LEANRED = "beingLearned"
LEARNED = "learned"
UNKNOWN = "unknown"

def _is_locked_skill_value(skill_value):
icon = skill_value.get("icon", None)
status = skill_value.get("status", None)
return icon == "lock" and status == "unknown"

def get_progress(skill_value):
if _is_locked_skill_value(skill_value):
return ("", Status.UNKNOWN)

progress_string = skill_value.get("progress", None)
if progress_string:
Fraction(progress_string)
current, total = map(int, progress_string.split("/"))
status = Status.NEW
if current > 0:
status = Status.BEING_LEANRED if current < total else Status.LEARNED

return (f"{'■' * current}{'□' * (total - current)}", status)
else:
return ("", Status.NEW)


def create_skill_node(skill_id, skill_value):
if not skill_value:
locked_skill_value = {"name": "???", "icon": "lock", "status": "unknown"}
skill_value = locked_skill_value
skill_name = skill_value.get("name", "")
skill_icon = get_icon(skill_value)
skill_progress, skill_status = get_progress(skill_value)
skill_icon_label = get_node_content([skill_icon, skill_name, skill_progress])
skill_id_and_name = f"{skill_id}(\"{skill_icon_label}\")"
skill_style = f"class {skill_id} {skill_status.value}Skill;"
skill_requires = get_required_node_edges(skill_id, skill_value.get("requires", []))
sections = [
skill_id_and_name,
skill_style,
skill_requires,
]
skill_graph = "\n".join(sections)
return skill_graph
43 changes: 43 additions & 0 deletions skillmap/nodes/skillmap_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from skillmap.theme_loader import load_theme
from skillmap.nodes.common import get_icon, get_node_content, SECTION_SEPARATOR
from skillmap.nodes.group_node import create_group_subgraphs, create_groups_edges
import re


def alphanumerize(s):
return re.sub(r"\W+", "_", s)


def get_orientation(skill_map_dict):
orientation = skill_map_dict.get("orientation", "TD")
if orientation not in ["TD", "TB", "BT", "RL", "LR"]:
orientation = "TD"
return orientation


# generate a mermaid graph from a skill map toml dict
def create_skillmap_graph(skill_map):
skill_map_dict = skill_map.get("skillmap", {})
map_name = skill_map_dict.get("name", "unamed_skill_map")
map_id = alphanumerize(map_name)
theme = skill_map_dict.get("theme", "ocean")
orientation = get_orientation(skill_map_dict)
map_icon = get_icon(skill_map_dict)
map_icon_label = get_node_content([map_icon, map_name])

map_to_group_edges = create_groups_edges(map_id, skill_map.get("groups", {}))
map_group_subgraphs = create_group_subgraphs(skill_map.get("groups", {}))

skill_map_node = f"{map_id}({map_icon_label})"
skill_map_node_style = f"class {map_id} normalSkillGroup;"
skill_map_header = f"flowchart {orientation}"
sections = [
skill_map_header,
skill_map_node,
map_group_subgraphs,
SECTION_SEPARATOR,
map_to_group_edges,
load_theme(theme),
skill_map_node_style,
]
return "\n".join(sections)
Loading

0 comments on commit 1de0a68

Please sign in to comment.