Skip to content

Commit

Permalink
Merge pull request #2 from niyue/feature/progress-bar
Browse files Browse the repository at this point in the history
Support progress bar with different styles, and refactor the code to break the graph generation implementation into multiple files.
  • Loading branch information
niyue authored Mar 2, 2022
2 parents 1de0a68 + 7e3ad7a commit 05a57a3
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 43 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# 0.3.0 - 2022-03-01
* support progress bar for skill
# 0.3.0 - 2022-03-02
* support different styles of 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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ This project borrows inspiration and ideas from two sources:
1. https://hacks.mozilla.org/2018/10/webassemblys-post-mvp-future/
2. https://github.com/nikomatsakis/skill-tree

# Features
* skill tree/map generation
* specify pre-requisite skills
* multiple themes
* multiple skill progress bar styles
# Installation
```
pip install skillmap
Expand Down Expand Up @@ -43,6 +48,7 @@ icon = "rocket"

* Each node can have a string label and an fontawsome icon.
* Skills with different statuses will be shown with different colors.
* Each skill may have a progress bar to indicate its learning progress.
* Unnamed skill will be shown as a locked skill.
* Pre-requisite skills will be connected with an directed edge.
* You can embed the generated mermaid diagram into github markdown directly, but the fontawesome icons in the diagrams are not shown by github so far.
Expand All @@ -56,4 +62,6 @@ icon = "rocket"
* install several tools to make hot reloading to work
* [`entr`](https://github.com/eradman/entr), run arbitrary commands when files change
* [Visual Studio Code](https://code.visualstudio.com) + [Markdown Preview Enhanced Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced)
* Basically, use `entr` to watch toml file changes, and generate a `md` makrdown file using `skillmap` every time when toml file changes. And use `vscode` + `Markdown Preview Enhanced` extension to open this generated markdown file. Check out `build_sample` and `dev_sample` in [justfile](justfile) to see how to make hot reloading work
* Basically, use `entr` to watch toml file changes, and generate a `md` makrdown file using `skillmap` every time when toml file changes. And use `vscode` + `Markdown Preview Enhanced` extension to open this generated markdown file. Check out `build_sample` and `dev_sample` in [justfile](justfile) to see how to make hot reloading work
# Known issues
* Sometimes, the group's text will be clipped when rendered in mermaid. And you have to edit the generated file slightly and then change it back to ask mermaid to refersh the diagram to avoid clipping. It is probably a bug for mermaid as far as I can tell.
Binary file modified docs/images/ocean_theme_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/orientation_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 10 additions & 4 deletions docs/skillmap_descriptor.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,29 @@
* BT - bottom to top
* RL - right to left
* LR - left to right
* `progress_bar_style`: [optional] an integer value as different styles of progress bar. When a skill is specified with a progress (e.g. "1/3"), this property can be used to tell skillmap to render the skill progress bar in different styles. There are currently 28 different styles you can choose from. Simply specify a value between `0` ~ `27` to give it a try. For example, here are two styles `💚🤍🤍`/`■□□` you can choose from.
## group/skill toml tables
* The `group`/`skill` toml table can have some fields:
* `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.
* `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).
* `status`: [optional] a string value indicating the status of the skill. Three valid values are supported [new|beingLearned|learned]. When specifying a different status, different colors will be used when rendering the node. Usually, you simply use `progress` to indicate the status. If you do NOT want a progress bar but just want some simple status, you can specify this property.
* 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
```toml
[groups.learn_python]
name = "learn python"
icon = "rocket"
[groups.learn_python.skills.string_literal_usage]
name = "string literal"
icon = "book"
progress = "1/3"
[groups.learn_python.skills.print]
name = "print statement"
icon = "printer"
[groups.learn_python.skills.string]
name = "string literal"
icon = "book"
requires = ["groups.learn_python.skills.string_literal_usage"]

[groups.program_with_python]
name = "program with python"
Expand All @@ -49,6 +53,8 @@ requires = ["groups.learn_python"]
In this exmaple, there are:
* two groups: `groups.learn_python` and `groups.program_with_python`
* `groups.learn_python` has two skills:
* `groups.learn_python.skills.string_literal_usage`
* this skill's learning progress is `1/3`
* `groups.learn_python.skills.print`
* `groups.learn_python.skills.string`
* this skill requires `groups.learn_python.skills.string_literal_usage` to be learned first
* `groups.program_with_python` requires `groups.learn_python` to be learned first. When drawn in the diagram, it will be rendered as an edge from `groups.learn_python` to `groups.program_with_python`.
39 changes: 33 additions & 6 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
#!/usr/bin/env just --justfile
set dotenv-load := true

sample_toml := "tests/url_shortener.toml"
sample_md := "dist/url_shortener.md"
sample_png := "dist/url_shortener.png"
built_wheel := "./dist/skillmap-*-py3-none-any.whl"

# build and install package into system python for every python file change
dev:
find skillmap -iname "*.py" -o -iname "*.cmake" -iname "pyproject.toml" | entr -s "poetry build && pip install ./dist/skillmap-*-any.whl --force-reinstall --no-dependencies"
find skillmap -iname "*.py" -iname "pyproject.toml" | entr -s "poetry build && pip install {{ built_wheel }} --force-reinstall"

# install package into system python
setup:
# install the library into system python
rm -fr ./dist
poetry build && pip install ./dist/skillmap-*-py3-none-any.whl --force-reinstall
poetry build && pip install {{ built_wheel }} --force-reinstall

# publish package to pypi
publish:
poetry build
poetry publish

build_sample src="tests/url_shortener.toml" dest="dist/url_shortener.md":
echo '```mermaid' > {{ dest }} && skillmap {{ src }} >> {{ dest }} && echo '```' >> {{ dest }}
# generate markdown from source toml file
generate src dest:
echo '```mermaid' > {{ dest }} && poetry run skillmap {{ src }} >> {{ dest }} && echo '```' >> {{ dest }}

# generate png from source toml file
png src dest:
# mermaid cli (https://github.com/mermaid-js/mermaid-cli) needs to be installed
poetry run skillmap {{ src }} | mmdc -o {{ dest }}

# generate markdown for sample skillmap
generate_sample:
just generate {{ sample_toml }} {{ sample_md }}

# generate png for sample skillmap
png_sample:
just png {{ sample_toml }} {{ sample_png }}

# develop sample skillmap by hot reloading the file and generated results
dev_sample:
find "tests" -iname "*.toml" | entr -s "just generate_sample"

dev_sample src="tests":
find {{ src }} -iname "*.toml" | entr -s "just build_sample"

2 changes: 1 addition & 1 deletion skillmap/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def _skillmap_parser():
"descriptor_toml",
default=False,
type=str,
help="The path to a toml file describing the skillmap",
help="The path to a toml file describing the skillmap. You can find more deetails https://github.com/niyue/skillmap/blob/main/docs/skillmap_descriptor.md",
)
parser.add_argument(
"--version",
Expand Down
23 changes: 15 additions & 8 deletions skillmap/nodes/group_node.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from skillmap.nodes.common import get_icon, get_node_content, get_required_node_edges, SECTION_SEPARATOR
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


Expand All @@ -20,10 +25,12 @@ def create_groups_edges(map_id, groups):
return groups_edges


def get_group_skills_list(qualified_group_id, group_skills):
def get_group_skills_list(qualified_group_id, group_skills, progress_bar_style=0):
group_skills = [
create_skill_node(
_qualified_skill_id(qualified_group_id, skill_id), skill_value
_qualified_skill_id(qualified_group_id, skill_id),
skill_value,
progress_bar_style,
)
for skill_id, skill_value in group_skills.items()
]
Expand All @@ -38,21 +45,21 @@ def get_group_status(group_skills):
return "new"


def create_group_subgraph(group_id, group_value):
def create_group_subgraph(group_id, group_value, progress_bar_style=0):
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", {})
qualified_group_id, group_value.get("skills", {}), progress_bar_style
)
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_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 = [
Expand All @@ -68,9 +75,9 @@ def create_group_subgraph(group_id, group_value):
return group_graph


def create_group_subgraphs(groups):
def create_group_subgraphs(groups, progress_bar_style=0):
group_graphs = [
create_group_subgraph(group_id, group_value)
create_group_subgraph(group_id, group_value, progress_bar_style)
for group_id, group_value in groups.items()
]
return "\n\n".join(group_graphs)
30 changes: 30 additions & 0 deletions skillmap/nodes/progress_bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
PROGRESS_BAR_STYLES = [
'□■',
'▁█',
'⣀⣿',
'░█',
'▒█',
'□▩',
'□▦',
'▱▰',
'▭◼',
'▯▮',
'◯⬤',
'⚐⚑',
'⬜⬛',
'⬜🟩',
'⬜🟦',
'⬜🟧',
'🤍💚',
'🤍💙',
'🤍🧡',
'⚪⚫',
'⚪🟢',
'⚪🔵',
'⚪🟠',
'🌑🌕',
'❕❗',
'🥚🐣',
'💣💥',
'❌✅',
]
36 changes: 24 additions & 12 deletions skillmap/nodes/skill_node.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,56 @@
from skillmap.nodes.common import get_icon, get_node_content, get_required_node_edges
from fractions import Fraction
from enum import Enum
from skillmap.nodes.progress_bar import PROGRESS_BAR_STYLES

class Status(Enum):

class SkillStatus(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):

def get_progress(skill_value, progress_bar_style=0):
if _is_locked_skill_value(skill_value):
return ("", Status.UNKNOWN)
return ("", SkillStatus.UNKNOWN)

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

return (f"{'■' * current}{'□' * (total - current)}", status)
chosen_progress_bar_style = PROGRESS_BAR_STYLES[
progress_bar_style % len(PROGRESS_BAR_STYLES)
]
empty_cell, finished_cell = chosen_progress_bar_style
return (f"{finished_cell * current}{empty_cell * (total - current)}", status)
else:
return ("", Status.NEW)
status_value = skill_value.get("status", "new")

for s in [SkillStatus.NEW, SkillStatus.BEING_LEANRED, SkillStatus.LEARNED]:
if status_value == s.value:
return ("", s)


def create_skill_node(skill_id, skill_value):
def create_skill_node(skill_id, skill_value, progress_bar_style=0):
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_progress, skill_status = get_progress(skill_value, progress_bar_style)
skill_icon_label = get_node_content([skill_icon, skill_name, skill_progress])
skill_id_and_name = f"{skill_id}(\"{skill_icon_label}\")"
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 = [
Expand All @@ -47,4 +59,4 @@ def create_skill_node(skill_id, skill_value):
skill_requires,
]
skill_graph = "\n".join(sections)
return skill_graph
return skill_graph
16 changes: 14 additions & 2 deletions skillmap/nodes/skillmap_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,32 @@ def get_orientation(skill_map_dict):
return orientation


def get_progress_bar_style(skill_map_dict):
progress_bar_style = 0
try:
progress_bar_style = int(skill_map_dict.get("progress_bar_style", 0))
except:
pass
return progress_bar_style


# 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")
progress_bar_style = get_progress_bar_style(skill_map_dict)
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", {}))
map_group_subgraphs = create_group_subgraphs(
skill_map.get("groups", {}), progress_bar_style
)

skill_map_node = f"{map_id}({map_icon_label})"
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 = [
Expand Down
25 changes: 21 additions & 4 deletions tests/nodes/skill_node_test.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
from skillmap.nodes.skill_node import create_skill_node, get_progress, Status
from skillmap.nodes.skill_node import create_skill_node, get_progress, SkillStatus
from skillmap.nodes.progress_bar import PROGRESS_BAR_STYLES

def test_get_0_progress():
progress, status = get_progress({})
assert progress == ""
assert status == Status.NEW
assert status == SkillStatus.NEW

def test_get_fraction_progress():
progress, status = get_progress({"progress": "1/3"})
assert progress == "■□□"
assert status == Status.BEING_LEANRED
assert status == SkillStatus.BEING_LEANRED

def test_get_finished_progress():
progress, status = get_progress({"progress": "3/3"})
assert progress == "■■■"
assert status == SkillStatus.LEARNED

def test_get_different_style_progress_bar():
progress, status = get_progress({"progress": "1/3"}, 1)
assert progress == "█▁▁"
assert status == SkillStatus.BEING_LEANRED

def test_get_very_big_style_progress_bar():
progress, status = get_progress({"progress": "1/3"}, len(PROGRESS_BAR_STYLES) + 1)
assert progress == "█▁▁"
assert status == SkillStatus.BEING_LEANRED


def test_get_0_fraction_progress():
progress, status = get_progress({"progress": "0/4"})
assert progress == "□□□□"
assert status == Status.NEW
assert status == SkillStatus.NEW

def test_create_skill_node():
skill_graph = create_skill_node("s1", {"name": "url validator", "icon": "globe"})
Expand Down
Loading

0 comments on commit 05a57a3

Please sign in to comment.