The PlantUML preprocessor provides a higher level abstraction to generate the diagram descriptions. It can also be used as a programming language to compute the diagrams! The preprocessor provides variables, conditional expressions, looping constructs, and functions, among other facilities. The programs can also take external input through a file.
This also enables having some fun while learning the preprocessor features. In this document, I describe some such examples. These example diagrams are computed by PlantUML, not manually described.
All of them are my original work.
💡 TIP
Some diagrams below are too large to view easily on the webpage. You can open them in a new tab/window or download them for better clarity. The images are in SVG format, allowing you to zoom in for a clearer view.
Table of Contents
- Factorial function computation and drawings
- Fibonacci series computation and drawings
- Customizing generated diagram with user-defined function and lambda function
- Compute and draw Test cricket matches hosting data in hierarchical structure
- Compute and draw Collatz sequence for a range of numbers
- Generate multiple customized diagrams from diagram template
This hacks a state diagram to denote the recursive calls and the computed factorial
value for each number. The !function
feature defines the factorial function that
computes the factorial, creates the states and draws the connections between the states.
The preprocessor not only computes but also draws the recursion!
plantuml_demo/src/preprocessor/factorial_demo.puml
Lines 1 to 31 in bfd3b1c
@startuml | |
!procedure $create_state($name, $attribute) | |
state $name : $attribute | |
!endprocedure | |
!procedure $connect_states($name1, $name2) | |
$name1 -right-> $name2 | |
!endprocedure | |
/' computes the factorial for all numbers from 0 to the specified number, | |
creates a state for each number, with its factorial as the attribute, | |
and connects every state to its preceding state, denoting the recursive | |
nature of the factorial function computation. '/ | |
!function $factorial($n) | |
!assert ($n >= 0) : "input is " + $n + " but should be non-negative." | |
!if ($n == 0) | |
!$value = 1 | |
$create_state($n, $value) | |
!else | |
!$value = $n * $factorial($n - 1) | |
$create_state($n, $value) | |
$connect_states($n, $n - 1) | |
!endif | |
!return $value | |
!endfunction | |
' assign the return value to avoid syntax error | |
!$ignore = $factorial(10) | |
@enduml |
This hacks the preprocessor's JSON parsing utility and !assert
directive to run
"unit tests" for the factorial function. For an invalid input, that is a negative
number, the factorial function raises an assert. However, there is no facility to expect
an assertion or exception (which is fair as the preprocessor is not a unit testing
framework). The "failed" unit test appears as an assertion error in the
generated diagram.
plantuml_demo/src/preprocessor/factorial_demo_test.puml
Lines 1 to 32 in 3166a46
@startuml | |
!include factorial_demo.puml | |
!$test_data = { | |
"tests" : [ | |
{"input": "0", "output": "1"}, | |
{"input": "1", "output": "1"}, | |
{"input": "2", "output": "2"}, | |
{"input": "3", "output": "6"}, | |
{"input": "4", "output": "24"}, | |
{"input": "5", "output": "120"}, | |
{"input": "6", "output": "720"}, | |
{"input": "7", "output": "5040"}, | |
{"input": "8", "output": "40320"}, | |
{"input": "9", "output": "362880"}, | |
{"input": "10", "output": "3628800"}, | |
{"input": "11", "output": "39916800"}, | |
{"input": "12", "output": "479001600"}, | |
/' below input will raise an assert, but there's currently no way to | |
"expect" an assert, preprocessor is not a unit testing framework. ;-) '/ | |
{"input": "-1", "output": "0"} | |
] | |
} | |
!foreach $item in $test_data.tests | |
!$expected = %intval($item.output) | |
!$actual = $factorial(%intval($item.input)) | |
!assert $expected == $actual : "expected [" + $expected + "] actual [" + $actual + "]" | |
!endfor | |
@enduml |
The factorial function is now memoized to reuse the computed factorial value for a number. As the preprocessor does not provide any array or hashmap data structure, string concatenation is hacked to create a hashmap (dictionary).
@startuml | |
/' computes the factorial of the given integer. '/ | |
!function $factorial($n) | |
/' Return value of this function is memoized because it uses recursion. | |
PlantUML preprocessor doesn't provide dictionary or array data structure, | |
hence a "hack" simulates a dictionary. For every input integer, a variable | |
is created with the stringized integer as its name, and the return value is | |
assigned to it. Thus, the variable's name serves as the "key". '/ | |
!if %variable_exists(%string($n)) | |
!return %get_variable_value(%string($n)) | |
!endif | |
!if $n == 0 | |
!$value = 1 | |
!else | |
!$value = $n * $factorial($n - 1) | |
!endif | |
%set_variable_value(%string($n), $value) | |
!return $value | |
!endfunction | |
!procedure $factorial_question_answer_sequence(\ | |
$starting_number = 0,\ | |
$ending_number = 12,\ | |
$color_number_in_question = blue,\ | |
$color_number_in_answer = green) | |
skinparam SequenceMessageAlignment direction | |
!$number = $starting_number | |
!while $number <= $ending_number | |
Question -> Answer : What is factorial of <color:$color_number_in_question>**$number**</color>? | |
Question <- Answer : Factorial of <color:$color_number_in_question>**$number**</color> is <color:$color_number_in_answer>**$factorial($number)**</color>. | |
||| | |
!$number = $number + 1 | |
!endwhile | |
!endprocedure | |
scale 1.5 /' zooms the generated diagram per specified scaling factor '/ | |
$factorial_question_answer_sequence(\ | |
$color_number_in_answer = darkviolet, $color_number_in_question = red\ | |
) | |
@enduml |
The state diagram is once again hacked to draw a fibonacci recursion tree. This diagram is more complicated to compute than the factorial recursion because:
- Each non-leaf node connects to two other nodes.
- There are multiple instances of leaf node and non-leaf nodes in the tree, so a mechanism is needed to identify each such instance.
- As there are multiple leaf nodes, the end of recursion needs to be computed carefully to prevent an accidental endless recursion.
I called this computation out-in because it primarily focusses on drawing the tree, while computing and filling in the fibonacci series numbers along the way.
I created this approach in my early days of learning when I was less well-versed with using the preprocessor for recursion. I created the more intuitive in-out approach shortly afterwards.
@startuml | |
!function $fibonacci($n) | |
!if $n == 0 | |
!return 0 | |
!elseif $n == 1 | |
!return 1 | |
!else | |
/' create named variables because it seems the preprocessor cannot | |
handle two unnamed temporary variables. Following code crashes at | |
"compile time": | |
!return $fibonacci($n - 1) + $fibonacci($n - 2) '/ | |
!$fibonacci_n_minus_1 = $fibonacci($n - 1) | |
!$fibonacci_n_minus_2 = $fibonacci($n - 2) | |
!return $fibonacci_n_minus_1 + $fibonacci_n_minus_2 | |
!endif | |
!endfunction | |
!procedure $create_state($name, $instance_name, $value) | |
state "$name" as $instance_name | |
$instance_name : $value | |
!endprocedure | |
!procedure $connect_states($instance1_name, $instance2_name) | |
$instance1_name -down-> $instance2_name | |
!endprocedure | |
/' draws the fibonacci recursion tree for the specified integer n. '/ | |
!procedure $make_fibonacci_tree($n, $index = 0) | |
!if $n >= 0 | |
!$fibonacci_n = $fibonacci($n) | |
$create_state($n, $index, $fibonacci_n) | |
/' as the same number can appear multiple times in the fibonacci tree, | |
$index is used to create distinct node instances. the procedure is | |
called recursively choosing a different $index value for each call. | |
heap numbering approach is used to compute the $index value. '/ | |
!endif | |
!if $n >= 2 | |
!$left_child_instance_name = $index * 2 + 1 | |
$make_fibonacci_tree($n - 1, $left_child_instance_name) | |
$connect_states($index, $left_child_instance_name) | |
!$right_child_instance_name = $index * 2 + 2 | |
$make_fibonacci_tree($n - 2, $right_child_instance_name) | |
$connect_states($index, $right_child_instance_name) | |
!endif | |
!endprocedure | |
$make_fibonacci_tree(10) | |
@enduml |
This also hacks the state diagram to draw the fibonacci recursion tree. However, it uses an in-out computation, where the primary focus is on computing the fibonacci series numbers, then drawing the nodes (states) around them and connecting them to form the tree.
This code is more intuitive for a human reader as it looks similar to the naive recursive Fibonacci implementation in conventional programming languages.
@startuml | |
!procedure $create_state($name, $instance_name, $value) | |
/' just to demonstrate customization based on value, nothing special about this check. '/ | |
!if $name >= 4 | |
!$color = "#LightSalmon" | |
!else | |
!$color = "" | |
!endif | |
state "$name" as $instance_name $color | |
$instance_name : $value | |
!endprocedure | |
!procedure $connect_states($instance1_name, $instance2_name) | |
$instance1_name -down-> $instance2_name | |
!endprocedure | |
!function $fibonacci($n, $index = 0) | |
!if $n == 0 | |
!$fibonacci_n = 0 | |
!elseif $n == 1 | |
!$fibonacci_n = 1 | |
!else | |
/' preprocessor seems unable to maintain two unnamed temporary variables, | |
hence create local variables to hold return values of the recursive calls. '/ | |
!$fibonacci_n_minus_1 = $fibonacci($n - 1, $index * 2 + 1) | |
!$fibonacci_n_minus_2 = $fibonacci($n - 2, $index * 2 + 2) | |
!$fibonacci_n = $fibonacci_n_minus_1 + $fibonacci_n_minus_2 | |
!endif | |
$create_state($n, $index, $fibonacci_n) | |
!if $n >= 2 | |
$connect_states($index, $index * 2 + 1) | |
$connect_states($index, $index * 2 + 2) | |
!endif | |
!return $fibonacci_n | |
!endfunction | |
!$ignore = $fibonacci(10) | |
@enduml |
The fibonacci computation is memoized using a hack similar to the one used for the factorial function, and the drawing illustrates the memoized computation. The drawings of the recursive and the memoized approach can be a useful visualization to demonstrate how memoization cuts off the recursive calls.
This was an intermediate diagram computation I created while trying to solve the fibonacci recursion tree diagram computation described above.
plantuml_demo/src/preprocessor/full_binary_tree_demo.puml
Lines 1 to 24 in bb16912
@startuml | |
!procedure $connect_entities($name1, $name2) | |
$name1 -down-> $name2 | |
!endprocedure | |
/' draws the full binary tree of the specified height. '/ | |
!procedure $make_full_binary_tree($height, $index = 0) | |
!if $height >= 0 | |
object $index | |
!endif | |
!if $height > 0 | |
!$left_child_instance_name = $index * 2 + 1 | |
$make_full_binary_tree($height - 1, $left_child_instance_name) | |
$connect_entities($index, $left_child_instance_name) | |
!$right_child_instance_name = $index * 2 + 2 | |
$make_full_binary_tree($height - 1, $right_child_instance_name) | |
$connect_entities($index, $right_child_instance_name) | |
!endif | |
!endprocedure | |
$make_full_binary_tree(5) | |
@enduml |
PlantUML preprocessor has first class functions and even lambda functions! This is unexpectedly remarkable for what was not even intended to be a programming language.
I customized the node (state) and the value formatting by passing in a user-defined function and a lambda expression. These are respectively used to draw the leaf nodes with a different background colour, and a different border style for nodes having values up to 10.
plantuml_demo/src/preprocessor/fibonacci_recursive_in_out_with_user_function_demo.puml
Lines 1 to 62 in 536fc83
@startuml | |
!function $is_leaf_node($state_name) | |
!return $state_name == 0 || $state_name == 1 | |
!endfunction | |
!function $always_false($ignore) | |
!return %false() | |
!endfunction | |
/' creates a state with options to highlight it in two ways, | |
based on a user-defined function and a lambda function respectively.'/ | |
!procedure $create_state(\ | |
$name,\ | |
$instance_name,\ | |
$value,\ | |
$name_based_highlight_function = $always_false,\ | |
$value_based_highlight_expression = %false()) | |
!if %call_user_func($name_based_highlight_function, $name) | |
!$background_color = "#technology" | |
!else | |
!$background_color = "" | |
!endif | |
!if %eval($value_based_highlight_expression) | |
!$border_style = "##[bold]blue" | |
!else | |
!$border_style = "" | |
!endif | |
state "$name" as $instance_name $background_color $border_style | |
$instance_name : $value | |
!endprocedure | |
!procedure $connect_states($instance1_name, $instance2_name) | |
$instance1_name -down-> $instance2_name | |
!endprocedure | |
!function $fibonacci($n, $index = 0) | |
!if $n == 0 | |
!$fibonacci_n = 0 | |
!elseif $n == 1 | |
!$fibonacci_n = 1 | |
!else | |
!$fibonacci_n_minus_1 = $fibonacci($n - 1, $index * 2 + 1) | |
!$fibonacci_n_minus_2 = $fibonacci($n - 2, $index * 2 + 2) | |
!$fibonacci_n = $fibonacci_n_minus_1 + $fibonacci_n_minus_2 | |
!endif | |
$create_state($n, $index, $fibonacci_n, "$is_leaf_node", "$value <= 10") | |
!if $n >= 2 | |
$connect_states($index, $index * 2 + 1) | |
$connect_states($index, $index * 2 + 2) | |
!endif | |
!return $fibonacci_n | |
!endfunction | |
!$ignore = $fibonacci(10) | |
@enduml |
This hacks the Work Breakdown Structure (WBS) diagram to represent Test cricket matches hosting data in a hierarchical structure. The JSON data file contains the following information:
- Number of matches played at various cricket grounds.
- Hierarchical structure of the location (city, country) the cricket ground belongs to.
The WBS diagram represents the hierarchy as a tree to any depth, while also adding up the count at every level beneath it. This naturally resembles the Composite design pattern, and the computation follows such a recursive structure.
In addition, the grounds that have hosted 100 or more matches are highlighted.
plantuml_demo/src/preprocessor/test_match_host_wbs_demo.puml
Lines 1 to 101 in 33e1384
@startwbs | |
!input_json_file ?= "src/preprocessor/cricket_grounds_test_matches_hosted.json" | |
!$test_cricket_matches_data = %load_json(input_json_file) | |
!function $repeat_string($input_string, $times) | |
!$str = "" | |
!while $times != 0 | |
!$str = $str + $input_string | |
!$times = $times - 1 | |
!endwhile | |
!return $str | |
!endfunction | |
!function $make_wbs_code($name, $value, $level, $highlight_expression = %false()) | |
!$background_colour = "" | |
!if %eval($highlight_expression) | |
!$background_colour = "[#PaleGreen]" | |
!endif | |
!$wbs_code \ | |
= $repeat_string("*", $level) + $background_colour \ | |
+ " <b>" + $name + "</b> " + $value + %newline() | |
!return $wbs_code | |
!endfunction | |
!function $generate_wbs_code($data, $level = 1) | |
!$code_string = "" | |
!$sum = 0 | |
!foreach $item in $data.list | |
!$inner_item_wbs_code = "" | |
!$inner_item_name = "" | |
!if %strlen($item.list) != 0 /' hack to check if it is a leaf node '/ | |
/' bug/hack: need to store the inner item's name before recursing. below we | |
update a dictionary where each item is mapped to the sum of the counts below | |
it. the preprocessor seems to have either an implementation bug or stack | |
limitation, so $item.name gets replaced at each level, thus the sum gets | |
mapped to the innermost item's name instead.'/ | |
!$inner_item_name = $item.name | |
/' it is not a leaf node, recurse to the inner level. '/ | |
!$inner_item_wbs_code = $generate_wbs_code($item, $level + 1) | |
/' recursive function call at the inner level would have mapped the sum in | |
the dictionary, so add it to the running sum at this level. '/ | |
!$sum = $sum + %get_variable_value($inner_item_name) | |
!else | |
/' it is a leaf node, so it is placed at the next level in the wbs code. '/ | |
!$count = %intval($item.count) | |
!$inner_item_wbs_code \ | |
= $make_wbs_code($item.name, $count, $level + 1, "$value >= 100") | |
/' for a leaf node, the sum is simply its own count. '/ | |
!$sum = $sum + $count | |
!endif | |
/' concatenate the wbs code of this item to the overall wbs code for this level. | |
this works correctly for leaf nodes or non-leaf nodes or mix of the two. leaf | |
node's wbs code is directly computed here. for non-leaf nodes, recursive | |
function call computes the wbs code for lower levels, then that level's wbs code | |
is pre-concatenated outside the loop and returned as the overall wbs code for | |
that level. '/ | |
!$code_string = $code_string + $inner_item_wbs_code | |
!endfor | |
/' pre-concatenate this level's wbs code to the lower level's wbs code '/ | |
!$code_string = $make_wbs_code($data.name, $sum, $level) + $code_string | |
/' map the sum of the counts below this level to this level's name. during the | |
recursion unwinding, the outer level functions can refer to this dictionary entry | |
to update the count at that level. '/ | |
%set_variable_value($data.name, $sum) | |
!return $code_string | |
!endfunction | |
<style> | |
wbsDiagram { | |
LineColor Black | |
arrow { | |
LineColor Blue | |
} | |
} | |
</style> | |
title <size:40>**Test cricket matches hosted by cricket grounds**</size> | |
legend top center | |
<size:24>Last updated: %date()</size> | |
|<#PaleGreen> | Hosted 100 or more matches | | |
end legend | |
/' generates wbs (work breakdown structure) diagram from json data. | |
the json data has a recursive structure, consisting of the following fields: | |
- name: string | |
- count: integer (OR) | |
- list: array of this data structure recursively '/ | |
$generate_wbs_code($test_cricket_matches_data) | |
@endwbs |
{ | |
"name": "All", "list": [ | |
{"name": "India", "list": [ | |
{"name": "Eden Gardens, Kolkata", "count": 42}, | |
{"name": "Chinnaswamy Stadium, Bengaluru", "count": 24}, | |
{"name": "Mumbai", "list": [ | |
{"name": "Wankhede Stadium", "count": 26}, | |
{"name": "Brabourne Stadium", "count": 18}, | |
{"name": "Gymkhana Ground", "count": 1} | |
]}, | |
{"name": "Green Park, Kanpur", "count": 23}, | |
{"name": "Arun Jaitley Stadium, Delhi", "count": 35}, | |
{"name": "Chennai", "list": [ | |
{"name": "Chidambaram Stadium", "count": 34}, | |
{"name": "Nehru Stadium", "count": 9} | |
]}, | |
{"name": "Narendra Modi Stadium, Ahmedabad", "count": 15}, | |
{"name": "Chandigarh", "list": [ | |
{"name": "IS Bindra Stadium", "count": 14}, | |
{"name": "Sector 16 Stadium", "count": 1} | |
]}, | |
{"name": "Nagpur", "list": [ | |
{"name": "VCA Ground", "count": 9}, | |
{"name": "VCA Stadium", "count": 7} | |
]}, | |
{"name": "Hyderabad", "list": [ | |
{"name": "Rajiv Gandhi International Stadium", "count": 6}, | |
{"name": "Lal Bahadur Shastri Stadium", "count": 3} | |
]}, | |
{"name": "Barabati Stadium, Cuttack", "count": 2}, | |
{"name": "Dehradun Cricket Stadium, Dehradun", "count": 1}, | |
{"name": "HPCA Stadium, Dharamsala", "count": 2}, | |
{"name": "Holkar Cricket Stadium, Indore", "count": 3}, | |
{"name": "Sawai Mansingh Stadium, Jaipur", "count": 1}, | |
{"name": "Gandhi Stadium, Jalandhar", "count": 1}, | |
{"name": "Khanderi Cricket Stadium, Rajkot", "count": 3}, | |
{"name": "MCA Stadium, Pune", "count": 2}, | |
{"name": "JSCA International Stadium, Ranchi", "count": 3}, | |
{"name": "ACA-VDCA Stadium, Visakhapatnam", "count": 3}, | |
{"name": "Lucknow", "list": [ | |
{"name": "Ekana Cricket Stadium", "count": 1}, | |
{"name": "KD Singh Stadium", "count": 1}, | |
{"name": "University Ground", "count": 1} | |
]} | |
]}, | |
{"name": "Australia", "list": [ | |
{"name": "Adelaide Oval, Adelaide", "count": 82}, | |
{"name": "Brisbane", "list": [ | |
{"name": "Gabba", "count": 66}, | |
{"name": "Exhibition Ground", "count": 2} | |
]}, | |
{"name": "MCG, Melbourne", "count": 116}, | |
{"name": "Perth", "list": [ | |
{"name": "WACA", "count": 44}, | |
{"name": "Perth Stadium", "count": 4} | |
]}, | |
{"name": "SCG, Sydney", "count": 112}, | |
{"name": "Bellerive Oval, Hobart", "count": 14}, | |
{"name": "Cazaly's Stadium, Cairns", "count": 2}, | |
{"name": "Manuka Oval, Canberra", "count": 1}, | |
{"name": "Marrara Cricket Ground, Darwin", "count": 2} | |
]}, | |
{"name": "Pakistan", "list": [ | |
{"name": "Lahore", "list": [ | |
{"name": "Gaddafi Stadium", "count": 41}, | |
{"name": "Bagh-e-Jinnah", "count": 3} | |
]}, | |
{"name": "National Stadium, Karachi", "count": 43}, | |
{"name": "Karachi", "list": [ | |
{"name": "National Stadium", "count": 47}, | |
{"name": "Defence Housing Authority Stadium", "count": 1} | |
]}, | |
{"name": "Iqbal Stadium, Faisalabad", "count": 24}, | |
{"name": "Rawalpindi", "list": [ | |
{"name": "Rawalpindi Cricket Stadium", "count": 13}, | |
{"name": "Pindi Club Ground", "count": 1} | |
]}, | |
{"name": "Peshawar", "list": [ | |
{"name": "Arbab Niaz Stadium", "count": 6}, | |
{"name": "Peshawar Club Ground", "count": 1} | |
]}, | |
{"name": "Multan", "list": [ | |
{"name": "Multan Cricket Stadium", "count": 6}, | |
{"name": "Ibn-e-Qasim Bagh Stadium", "count": 1} | |
]}, | |
{"name": "Niaz Stadium, Hyderabad", "count": 5}, | |
{"name": "Jinnah Stadium, Sialkot", "count": 4}, | |
{"name": "Bahawal Stadium, Bahawalpur", "count": 1}, | |
{"name": "Jinnah Stadium, Gujranwala", "count": 1}, | |
{"name": "Sheikhupura Stadium, Sheikhupura", "count": 2} | |
]}, | |
{"name": "England", "list": [ | |
{"name": "London", "list": [ | |
{"name": "Kennington Oval", "count": 106}, | |
{"name": "Lord's", "count": 146} | |
]}, | |
{"name": "Trent Bridge, Nottingham", "count": 66}, | |
{"name": "Edgbaston, Birmingham", "count": 55}, | |
{"name": "Headingley, Leeds", "count": 81}, | |
{"name": "Old Trafford, Manchester", "count": 84}, | |
{"name": "Riverside Ground, Chester-Le-Street", "count": 6}, | |
{"name": "Bramall Lane, Sheffield", "count": 1}, | |
{"name": "The Rose Bowl, Southampton", "count": 7}, | |
{"name": "Sophia Gardens, Cardiff", "count": 3} | |
]}, | |
{"name": "South Africa", "list": [ | |
{"name": "Supersport Park, Centurion", "count": 29}, | |
{"name": "Newlands, Cape Town", "count": 60}, | |
{"name": "Johannesburg", "list": [ | |
{"name": "Wanderers Stadium", "count": 44}, | |
{"name": "Old Wanderers", "count": 22}, | |
{"name": "Ellis Park", "count": 6} | |
]}, | |
{"name": "Durban", "list": [ | |
{"name": "Kingsmead", "count": 45}, | |
{"name": "Lord's", "count": 4} | |
]}, | |
{"name": "St George's Park, Port Elizabeth", "count": 32}, | |
{"name": "Mangaung Oval, Bloemfontein", "count": 5}, | |
{"name": "Buffalo Park, East London", "count": 1}, | |
{"name": "Senwes Park, Potchefstroom", "count": 2} | |
]}, | |
{"name": "New Zealand", "list": [ | |
{"name": "Christchurch", "list": [ | |
{"name": "AMI Stadium", "count": 40}, | |
{"name": "Hagley Oval", "count": 13} | |
]}, | |
{"name": "Basin Reserve, Wellington", "count": 68}, | |
{"name": "Eden Park, Auckland", "count": 50}, | |
{"name": "Seddon Park, Hamilton", "count": 28}, | |
{"name": "Dunedin", "list": [ | |
{"name": "Carisbrook", "count": 10}, | |
{"name": "University Oval", "count": 8} | |
]}, | |
{"name": "Bay Oval, Mount Maunganui", "count": 5}, | |
{"name": "McLean Park, Napier", "count": 10} | |
]}, | |
{"name": "Sri Lanka", "list": [ | |
{"name": "Colombo", "list": [ | |
{"name": "Sinhalese Sports Club Ground", "count": 45}, | |
{"name": "P Sara Oval", "count": 22}, | |
{"name": "R Premadasa Stadium", "count": 9}, | |
{"name": "Colombo Cricket Club Ground", "count": 3} | |
]}, | |
{"name": "Galle Stadium, Galle", "count": 44}, | |
{"name": "Kandy", "list": [ | |
{"name": "Asgiriya Stadium", "count": 21}, | |
{"name": "Pallekele Cricket Stadium", "count": 9} | |
]}, | |
{"name": "Tyronne Fernando Stadium, Moratuwa", "count": 4} | |
]}, | |
{"name": "West Indies", "list": [ | |
{"name": "Trinidad & Tobago", "list": [ | |
{"name": "Queen's Park Oval, Port of Spain", "count": 62} | |
]}, | |
{"name": "Barbados", "list": [ | |
{"name": "Kensington Oval, Bridgetown", "count": 55} | |
]}, | |
{"name": "Jamaica", "list": [ | |
{"name": "Sabina Park, Kingston", "count": 54} | |
]}, | |
{"name": "Guyana", "list": [ | |
{"name": "Bourda, Georgetown", "count": 30}, | |
{"name": "Providence Stadium, Providence", "count": 2} | |
]}, | |
{"name": "Antigua & Barbuda", "list": [ | |
{"name": "Antigua Recreation Ground, St. John's", "count": 22}, | |
{"name": "Sir Vivian Richards Stadium, North Sound", "count": 12} | |
]}, | |
{"name": "St Lucia", "list": [ | |
{"name": "Daren Sammy Stadium, Gros Islet", "count": 10} | |
]}, | |
{"name": "St Kitts & Nevis", "list": [ | |
{"name": "Warner Park, Basseterre", "count": 3} | |
]}, | |
{"name": "Dominica", "list": [ | |
{"name": "Windsor Park, Roseau", "count": 6} | |
]}, | |
{"name": "St Vincent", "list": [ | |
{"name": "Arnos Vale Ground, Kingstown", "count": 3} | |
]}, | |
{"name": "Grenada", "list": [ | |
{"name": "National Cricket Stadium, St. George's", "count": 4} | |
]} | |
]}, | |
{"name": "Bangladesh", "list": [ | |
{"name": "Shaheed Chandu Stadium, Bogra", "count": 1}, | |
{"name": "Chittagong", "list": [ | |
{"name": "MA Aziz Stadium", "count": 8}, | |
{"name": "Zahur Ahmed Chowdhury Stadium", "count": 24} | |
]}, | |
{"name": "Dhaka", "list": [ | |
{"name": "Bangabandhu National Stadium", "count": 17}, | |
{"name": "Shere Bangla National Stadium", "count": 27} | |
]}, | |
{"name": "Khan Shaheb Osman Ali Stadium, Fatullah", "count": 2}, | |
{"name": "Sheikh Abu Naser Stadium, Khulna", "count": 3}, | |
{"name": "Sylhet Stadium, Sylhet", "count": 3} | |
]}, | |
{"name": "United Arab Emirates", "list": [ | |
{"name": "Abu Dhabi", "list": [ | |
{"name": "Sheikh Zayed Stadium", "count": 15}, | |
{"name": "Tolerance Oval", "count": 1} | |
]}, | |
{"name": "Dubai Cricket Stadium, Dubai", "count": 13}, | |
{"name": "Sharjah Cricket Stadium, Sharjah", "count": 9} | |
]}, | |
{"name": "Zimbabwe", "list": [ | |
{"name": "Bulawayo", "list": [ | |
{"name": "Queens Sports Club", "count": 25}, | |
{"name": "Bulawayo Athletic Club", "count": 1} | |
]}, | |
{"name": "Harare Sports Club, Harare", "count": 39} | |
]}, | |
{"name": "Ireland", "list": [ | |
{"name": "The Village, Dublin", "count": 1} | |
]} | |
]} |
As a further enhancement, the diagram can be updated semi-automatically. Using Python, I scrape the howstat.com data source , clean and transform the data into a JSON file, which is then used to generate the diagram. With Github Actions, this process is automated and the updated diagram is committed to the repository.
The Github Actions workflow generate-test-cricket-matches-hosted-diagram uses the Python project helicopter and the reusable workflow fly-helicopter from my other repository dragondive/masadora along with my composite action run-plantuml-action.
This diagram computation draws the Collatz sequence using rectangle objects. Multiple separate diagrams are drawn for the positive integers up to 100.
At the time of this writing, the preprocessor does not support modulo division, so I used my primary school (class 1) definition of even number (numbers ending in digits 0, 2, 4, 6, 8) 😸. I also used this hack to wrap the chain like a snake 🐍 for both readability and aesthetics. This overrides the default PlantUML behaviour of connecting the rectangles in a straight line, which can be tiring to read for some long chains.
The Collatz sequence chains are shown only for a few selected numbers below. The complete set of chains is available in the directory diagrams.
plantuml_demo/src/preprocessor/collatz_sequence.puml
Lines 1 to 127 in 63b36f8
@startuml | |
!procedure $connect_entities($name1, $name2, $direction) | |
$name1 -$direction-> $name2 | |
!endprocedure | |
!procedure $create_rectangle($name, $stereotype) | |
rectangle $name <<$stereotype>> | |
!endprocedure | |
!function $is_even($num) | |
/' as the preprocessor currently doesn't have a modulus operator, use this | |
"hack" of checking the last digit to decide if the number is even. | |
the "mathematical approach" of successive subtraction to determine the | |
remainder runs into memory problems at runtime for long sequences. '/ | |
!$num_str = %string($num) | |
!$last_digit = %substr($num_str, %strlen($num_str) -1, 1) | |
!return ($last_digit == "0"\ | |
|| $last_digit == "2"\ | |
|| $last_digit == "4"\ | |
|| $last_digit == "6"\ | |
|| $last_digit == "8") | |
!endfunction | |
' unit tests for the $is_even function :-) | |
!assert $is_even(0) == 1 | |
!assert $is_even(11) == 0 | |
!assert $is_even(292) == 1 | |
!assert $is_even(1663) == 0 | |
!assert $is_even(10824) == 1 | |
!assert $is_even(652665) == 0 | |
!assert $is_even(9188316) == 1 | |
!assert $is_even(79430237) == 0 | |
!assert $is_even(340931238) == 1 | |
!assert $is_even(394237819) == 0 | |
!assert $is_even(518026430) == 1 | |
!function $get_last_hex_digit($num) | |
!$num_str = %dec2hex($num) | |
!$last_digit = %substr($num_str, %strlen($num_str) -1, 1) | |
!return $last_digit | |
!endfunction | |
' unit tests for the $get_last_hex_digit function :-) | |
!assert $get_last_hex_digit(0) == "0" | |
!assert $get_last_hex_digit(17) == "1" | |
!assert $get_last_hex_digit(290) == "2" | |
!assert $get_last_hex_digit(1667) == "3" | |
!assert $get_last_hex_digit(10820) == "4" | |
!assert $get_last_hex_digit(652661) == "5" | |
!assert $get_last_hex_digit(9188310) == "6" | |
!assert $get_last_hex_digit(79430247) == "7" | |
!assert $get_last_hex_digit(340931240) == "8" | |
!assert $get_last_hex_digit(394237817) == "9" | |
!assert $get_last_hex_digit(518026426) == "a" | |
!assert $get_last_hex_digit(390243419) == "b" | |
!assert $get_last_hex_digit(890540140) == "c" | |
!assert $get_last_hex_digit(623489357) == "d" | |
!assert $get_last_hex_digit(216487342) == "e" | |
!assert $get_last_hex_digit(938476831) == "f" | |
!procedure $make_direction_map() | |
/' use this workaround to manually wrap the chain like a snake. :-) | |
otherwise the chain will grow in only one direction, which would be inefficient | |
use of the defined "canvas" and also difficult to read. as the preprocessor currently | |
does not have a modulus operator, use this workaround to map a direction manually. '/ | |
%set_variable_value("0", "right") | |
%set_variable_value("1", "right") | |
%set_variable_value("2", "right") | |
%set_variable_value("3", "right") | |
%set_variable_value("4", "right") | |
%set_variable_value("5", "right") | |
%set_variable_value("6", "right") | |
%set_variable_value("7", "down") | |
%set_variable_value("8", "left") | |
%set_variable_value("9", "left") | |
%set_variable_value("a", "left") | |
%set_variable_value("b", "left") | |
%set_variable_value("c", "left") | |
%set_variable_value("d", "left") | |
%set_variable_value("e", "left") | |
%set_variable_value("f", "down") | |
!endprocedure | |
$make_direction_map() | |
!function $collatz($in_num) | |
!if $is_even($in_num) | |
!return $in_num / 2 | |
!else | |
!return 3 * $in_num + 1 | |
!endif | |
!endfunction | |
!assert $collatz(13) == 40 | |
!assert $collatz(40) == 20 | |
!procedure $make_collatz_sequence($num) | |
!if $num > 0 | |
!$counter = 0 | |
$create_rectangle($num, $counter) | |
!while $num != 1 | |
!$prev = $num | |
!$num = $collatz($num) | |
!$direction = %get_variable_value($get_last_hex_digit($counter)) | |
!$counter = $counter + 1 | |
$create_rectangle($num, $counter) | |
$connect_entities($prev, $num, $direction) | |
!endwhile | |
!else | |
!log input is $num but should be positive. | |
!endif | |
!endprocedure | |
!$count = 0 | |
!$limit = 100 | |
!while $count <= $limit | |
title Collatz sequence for $count | |
$make_collatz_sequence(%intval($count)) | |
!$count = $count + 1 | |
newpage | |
!endwhile | |
@enduml |
A diagram template defines the overall structure with some customizable portions. A JSON file is read to configure those customizable portions to generate multiple diagram instances.
The below demo shows three diagrams being generated for each customer. The customer data specified in JSON format is used to customize the diagrams. Both values and objects appearing in the diagram can be customized.
‼️ MISSING FEATURE/BUGIn the sequence diagrams, the actors not involved on the current page also appear, which is usually not the desired behaviour. This is due to a missing feature/bug in PlantUML reported on the PlantUML forum here.
@startuml | |
!$customer_data = { | |
"customer_list" : [ | |
{ | |
"name": "Initech", | |
"is_premium": "false", | |
"delivery_path": "https://initech.com/tps/", | |
"release_number": "VIII.XV.XXXIX" /' If you could use Roman numerals for the release numbers, that would be great.'/ | |
}, | |
{ | |
"name": "Acme", | |
"is_premium": "true", | |
"delivery_path": "https://acme.com/creativemayhem/", | |
"release_number": "4.1.6" | |
} | |
] | |
} | |
actor Developer | |
actor Tester | |
actor Integrator | |
!foreach $customer in $customer_data.customer_list | |
!assert $customer.name != "" | |
!assert $customer.delivery_path != "" | |
!assert $customer.release_number != "" | |
newpage Customer $customer.name: Developer Workflow | |
Developer -> %string("dev_" + $customer.name) : publish code changes | |
!if ($customer.is_premium == "true") | |
alt if premium feature | |
Developer -> Developer : prepare Premium Order Report | |
Developer -> %string("dev_" + $customer.name) : publish premium feature | |
end | |
!endif | |
newpage Customer $customer.name: Tester Workflow | |
Tester <- %string("dev_" + $customer.name) : checkout %string("RC_" + $customer.release_number) | |
Tester <- Tester : run all tests\nand verify results | |
newpage Customer $customer.name: Integrator Workflow | |
Integrator -> Integrator : integrate all components\nfor project $customer.name | |
Integrator -> Integrator : publish libmagic.so to\n%string($customer.delivery_path + $customer.release_number) | |
!if ($customer.is_premium == "true") | |
alt if premium feature | |
Integrator -> Integrator : publish libpremium.so to\n%string($customer.delivery_path + $customer.release_number) | |
end | |
!endif | |
!endfor | |
@enduml |
Diagrams generated for non-premium customer Initech
Diagrams generated for premium customer Acme