Repository: moshi4/FastDTLmapper (language: python)

Issue:
Add publication-ready gain/loss map plot function
Gain/Loss map visualization using Seaview is enough useful for data analysis.
However, seaview gain/loss map figure is not publication-ready quality.
In order to improve the convenience of user, I'd like to add program that generate
gain/loss map figure nicely.


Add publication-ready gain/loss map plot function
Gain/Loss map visualization using Seaview is enough useful for data analysis.
However, seaview gain/loss map figure is not publication-ready quality.
In order to improve the convenience of user, I'd like to add program that generate
gain/loss map figure nicely.




Discussion / hints:

Fix patch:
diff --git a/.github/workflows/CI.yml b/.github/workflows/ci.yml
similarity index 77%
rename from .github/workflows/CI.yml
rename to .github/workflows/ci.yml
index a9382fb..5939d5e 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/ci.yml
@@ -27,12 +27,17 @@ jobs:
           curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
           echo "$HOME/.poetry/bin" >> $GITHUB_PATH
 
+      # Install GraphViz for GOAtools GOterms plot
+      # Install Qt5 for ete3 tree plot
+      # Install Xvfb for ete3(Qt5) execution in headless server environment
+      - name: Install GraphViz, Qt5, Xvfb
+        run: |
+          sudo apt update -y
+          sudo apt install -y graphviz qt5-default xvfb
+
       - name: Install Dependencies
         run: poetry install -n
 
-      - name: Install GraphViz
-        run: sudo apt install -y graphviz
-
       - name: Run black format check
         run: poetry run black src tests --check --diff --verbose --exclude src/fastdtlmapper/bin
 
@@ -40,7 +45,7 @@ jobs:
         run: poetry run flake8 src tests --max-line-length=88 --ignore=E203,W503,W504 --exclude src/fastdtlmapper/bin
 
       - name: Run pytest
-        run: poetry run pytest tests --tb=line --cov=src --cov-report=xml --cov-report=term
+        run: xvfb-run poetry run pytest tests --tb=line --cov=src --cov-report=xml --cov-report=term
 
       - name: Upload coverage to Codecov
         if: ${{ matrix.python-version==3.8 }}
diff --git a/poetry.lock b/poetry.lock
index ceec68f..66d7e67 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -138,7 +138,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "coverage"
-version = "6.0"
+version = "6.0.2"
 description = "Code coverage measurement for Python"
 category = "dev"
 optional = false
@@ -182,6 +182,14 @@ category = "dev"
 optional = false
 python-versions = ">=2.7"
 
+[[package]]
+name = "ete3"
+version = "3.1.2"
+description = "A Python Environment for (phylogenetic) Tree Exploration"
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "flake8"
 version = "3.9.2"
@@ -603,6 +611,34 @@ category = "main"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
+[[package]]
+name = "pyqt5"
+version = "5.15.5"
+description = "Python bindings for the Qt cross platform application toolkit"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+PyQt5-Qt5 = ">=5.15.2"
+PyQt5-sip = ">=12.8,<13"
+
+[[package]]
+name = "pyqt5-qt5"
+version = "5.15.2"
+description = "The subset of a Qt installation needed by PyQt5."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyqt5-sip"
+version = "12.9.0"
+description = "The sip module support for PyQt5"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
 [[package]]
 name = "pytest"
 version = "6.2.5"
@@ -862,7 +898,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 [metadata]
 lock-version = "1.1"
 python-versions = ">=3.7.1,<3.10"
-content-hash = "cd3e2c7306fc34a5f5b7a9b4d09ceb82fa81467b25157bf973d0697e765e85bc"
+content-hash = "ccf3655dc5313a0aec5b885607660ede1448750a15f058a69c285c5a488b6517"
 
 [metadata.files]
 appnope = [
@@ -980,41 +1016,39 @@ colorama = [
     {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
 ]
 coverage = [
-    {file = "coverage-6.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:3dfb23cc180b674a11a559183dff9655beb9da03088f3fe3c4f3a6d200c86f05"},
-    {file = "coverage-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5dd5ae0a9cd55d71f1335c331e9625382239b8cede818fb62d8d2702336dbf8"},
-    {file = "coverage-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8426fec5ad5a6e8217921716b504e9b6e1166dc147e8443b4855e329db686282"},
-    {file = "coverage-6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aa5d4d43fa18cc9d0c6e02a83de0b9729b5451a9066574bd276481474f0a53ab"},
-    {file = "coverage-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78dd3eeb8f5ff26d2113c41836bac04a9ea91be54c346826b54a373133c8c53"},
-    {file = "coverage-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:581fddd2f883379bd5af51da9233e0396b6519f3d3eeae4fb88867473be6d56e"},
-    {file = "coverage-6.0-cp310-cp310-win32.whl", hash = "sha256:43bada49697a62ffa0283c7f01bbc76aac562c37d4bb6c45d56dd008d841194e"},
-    {file = "coverage-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa816e97cfe1f691423078dffa39a18106c176f28008db017b3ce3e947c34aa5"},
-    {file = "coverage-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5c191e01b23e760338f19d8ba2470c0dad44c8b45e41ac043b2db84efc62f695"},
-    {file = "coverage-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:274a612f67f931307706b60700f1e4cf80e1d79dff6c282fc9301e4565e78724"},
-    {file = "coverage-6.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9dbfcbc56d8de5580483cf2caff6a59c64d3e88836cbe5fb5c20c05c29a8808"},
-    {file = "coverage-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e63490e8a6675cee7a71393ee074586f7eeaf0e9341afd006c5d6f7eec7c16d7"},
-    {file = "coverage-6.0-cp36-cp36m-win32.whl", hash = "sha256:72f8c99f1527c5a8ee77c890ea810e26b39fd0b4c2dffc062e20a05b2cca60ef"},
-    {file = "coverage-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:88f1810eb942e7063d051d87aaaa113eb5fd5a7fd2cda03a972de57695b8bb1a"},
-    {file = "coverage-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:befb5ffa9faabef6dadc42622c73de168001425258f0b7e402a2934574e7a04b"},
-    {file = "coverage-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dbda34e8e26bd86606ba8a9c13ccb114802e01758a3d0a75652ffc59a573220"},
-    {file = "coverage-6.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b4ee5815c776dfa3958ba71c7cd4cdd8eb40d79358a18352feb19562fe4408c4"},
-    {file = "coverage-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d82cbef1220703ce56822be7fbddb40736fc1a928ac893472df8aff7421ae0aa"},
-    {file = "coverage-6.0-cp37-cp37m-win32.whl", hash = "sha256:d795a2c92fe8cb31f6e9cd627ee4f39b64eb66bf47d89d8fcf7cb3d17031c887"},
-    {file = "coverage-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6e216e4021c934246c308fd3e0d739d9fa8a3f4ea414f584ab90ef9c1592f282"},
-    {file = "coverage-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8305e14112efb74d0b5fec4df6e41cafde615c2392a7e51c84013cafe945842c"},
-    {file = "coverage-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4865dc4a7a566147cbdc2b2f033a6cccc99a7dcc89995137765c384f6c73110b"},
-    {file = "coverage-6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:25df2bc53a954ba2ccf230fa274d1de341f6aa633d857d75e5731365f7181749"},
-    {file = "coverage-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08fd55d2e00dac4c18a2fa26281076035ec86e764acdc198b9185ce749ada58f"},
-    {file = "coverage-6.0-cp38-cp38-win32.whl", hash = "sha256:11ce082eb0f7c2bbfe96f6c8bcc3a339daac57de4dc0f3186069ec5c58da911c"},
-    {file = "coverage-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:7844a8c6a0fee401edbf578713c2473e020759267c40261b294036f9d3eb6a2d"},
-    {file = "coverage-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bea681309bdd88dd1283a8ba834632c43da376d9bce05820826090aad80c0126"},
-    {file = "coverage-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e735ab8547d8a1fe8e58dd765d6f27ac539b395f52160d767b7189f379f9be7a"},
-    {file = "coverage-6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7593a49300489d064ebb6c58539f52cbbc4a2e6a4385de5e92cae1563f88a425"},
-    {file = "coverage-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adb0f4c3c8ba8104378518a1954cbf3d891a22c13fd0e0bf135391835f44f288"},
-    {file = "coverage-6.0-cp39-cp39-win32.whl", hash = "sha256:8da0c4a26a831b392deaba5fdd0cd7838d173b47ce2ec3d0f37be630cb09ef6e"},
-    {file = "coverage-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:7af2f8e7bb54ace984de790e897f858e88068d8fbc46c9490b7c19c59cf51822"},
-    {file = "coverage-6.0-pp36-none-any.whl", hash = "sha256:82b58d37c47d93a171be9b5744bcc96a0012cbf53d5622b29a49e6be2097edd7"},
-    {file = "coverage-6.0-pp37-none-any.whl", hash = "sha256:fff04bfefb879edcf616f1ce5ea6f4a693b5976bdc5e163f8464f349c25b59f0"},
-    {file = "coverage-6.0.tar.gz", hash = "sha256:17983f6ccc47f4864fd16d20ff677782b23d1207bf222d10e4d676e4636b0872"},
+    {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"},
+    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"},
+    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"},
+    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"},
+    {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"},
+    {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"},
+    {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"},
+    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"},
+    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"},
+    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"},
+    {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"},
+    {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"},
+    {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"},
+    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"},
+    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"},
+    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"},
+    {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"},
+    {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"},
+    {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"},
+    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"},
+    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"},
+    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"},
+    {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"},
+    {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"},
+    {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"},
+    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"},
+    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"},
+    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"},
+    {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"},
+    {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"},
+    {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"},
+    {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"},
+    {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"},
 ]
 debugpy = [
     {file = "debugpy-1.4.3-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:88b17d7c2130968f75bdc706a33f46a8a6bb90f09512ea3bd984659d446ee4f4"},
@@ -1047,6 +1081,9 @@ entrypoints = [
     {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"},
     {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
 ]
+ete3 = [
+    {file = "ete3-3.1.2.tar.gz", hash = "sha256:4fc987b8c529889d6608fab1101f1455cb5cbd42722788de6aea9c7d0a8e59e9"},
+]
 flake8 = [
     {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
     {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
@@ -1233,6 +1270,42 @@ pyparsing = [
     {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
     {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
 ]
+pyqt5 = [
+    {file = "PyQt5-5.15.5-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:521130eea1eaac55cc6867b1dc627d292b6468fb8e525ce2a015cdf39028d6e8"},
+    {file = "PyQt5-5.15.5-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:5966fb291f316f8e35bc8775dda63acf1bb9855baeb5af3e33d3e7c4f1cd98d4"},
+    {file = "PyQt5-5.15.5-cp36-abi3-win32.whl", hash = "sha256:b8e23c1a3fe1b7749c9106f36fba0bd4676dc77bcacca95304c6b840b782e24d"},
+    {file = "PyQt5-5.15.5-cp36-abi3-win_amd64.whl", hash = "sha256:85e76b7a96995b9da12083850bf2a9f4f0aeba2b0b99461b3337ad7e44f428c3"},
+    {file = "PyQt5-5.15.5.tar.gz", hash = "sha256:b411b7a8fa03901c9feb1dcbac7ea1fc3ce20b9ae682762b777cd5398749ca2b"},
+]
+pyqt5-qt5 = [
+    {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"},
+    {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"},
+    {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"},
+    {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"},
+]
+pyqt5-sip = [
+    {file = "PyQt5_sip-12.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d5bca2fc222d58e8093ee8a81a6e3437067bb22bc3f86d06ec8be721e15e90a"},
+    {file = "PyQt5_sip-12.9.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:d59af63120d1475b2bf94fe8062610720a9be1e8940ea146c7f42bb449d49067"},
+    {file = "PyQt5_sip-12.9.0-cp310-cp310-win32.whl", hash = "sha256:0fc9aefacf502696710b36cdc9fa2a61487f55ee883dbcf2c2a6477e261546f7"},
+    {file = "PyQt5_sip-12.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:485972daff2fb0311013f471998f8ec8262ea381bded244f9d14edaad5f54271"},
+    {file = "PyQt5_sip-12.9.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:d85002238b5180bce4b245c13d6face848faa1a7a9e5c6e292025004f2fd619a"},
+    {file = "PyQt5_sip-12.9.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:83c3220b1ca36eb8623ba2eb3766637b19eb0ce9f42336ad8253656d32750c0a"},
+    {file = "PyQt5_sip-12.9.0-cp36-cp36m-win32.whl", hash = "sha256:d8b2bdff7bbf45bc975c113a03b14fd669dc0c73e1327f02706666a7dd51a197"},
+    {file = "PyQt5_sip-12.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:69a3ad4259172e2b1aa9060de211efac39ddd734a517b1924d9c6c0cc4f55f96"},
+    {file = "PyQt5_sip-12.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42274a501ab4806d2c31659170db14c282b8313d2255458064666d9e70d96206"},
+    {file = "PyQt5_sip-12.9.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a8701892a01a5a2a4720872361197cc80fdd5f49c8482d488ddf38c9c84f055"},
+    {file = "PyQt5_sip-12.9.0-cp37-cp37m-win32.whl", hash = "sha256:ac57d796c78117eb39edd1d1d1aea90354651efac9d3590aac67fa4983f99f1f"},
+    {file = "PyQt5_sip-12.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4347bd81d30c8e3181e553b3734f91658cfbdd8f1a19f254777f906870974e6d"},
+    {file = "PyQt5_sip-12.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c446971c360a0a1030282a69375a08c78e8a61d568bfd6dab3dcc5cf8817f644"},
+    {file = "PyQt5_sip-12.9.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fc43f2d7c438517ee33e929e8ae77132749c15909afab6aeece5fcf4147ffdb5"},
+    {file = "PyQt5_sip-12.9.0-cp38-cp38-win32.whl", hash = "sha256:055581c6fed44ba4302b70eeb82e979ff70400037358908f251cd85cbb3dbd93"},
+    {file = "PyQt5_sip-12.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:c5216403d4d8d857ec4a61f631d3945e44fa248aa2415e9ee9369ab7c8a4d0c7"},
+    {file = "PyQt5_sip-12.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a25b9843c7da6a1608f310879c38e6434331aab1dc2fe6cb65c14f1ecf33780e"},
+    {file = "PyQt5_sip-12.9.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:dd05c768c2b55ffe56a9d49ce6cc77cdf3d53dbfad935258a9e347cbfd9a5850"},
+    {file = "PyQt5_sip-12.9.0-cp39-cp39-win32.whl", hash = "sha256:4f8e05fe01d54275877c59018d8e82dcdd0bc5696053a8b830eecea3ce806121"},
+    {file = "PyQt5_sip-12.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:b09f4cd36a4831229fb77c424d89635fa937d97765ec90685e2f257e56a2685a"},
+    {file = "PyQt5_sip-12.9.0.tar.gz", hash = "sha256:d3e4489d7c2b0ece9d203ae66e573939f7f60d4d29e089c9f11daa17cfeaae32"},
+]
 pytest = [
     {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
     {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
diff --git a/pyproject.toml b/pyproject.toml
index 28c4b3a..39d9967 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ enable = true
 [tool.poetry.scripts]
 FastDTLmapper = "fastdtlmapper.scripts.FastDTLmapper:main"
 FastDTLgoea = "fastdtlmapper.scripts.FastDTLgoea:main"
+plot_gain_loss_map = "fastdtlmapper.scripts.plot_gain_loss_map:main"
 
 [tool.poetry.dependencies]
 python = ">=3.7.1,<3.10"
@@ -39,6 +40,8 @@ scipy = "^1.7.1"
 goatools = "^1.1.6"
 pandas = "^1.3.3"
 numpy = "^1.21.2"
+ete3 = "^3.1.2"
+PyQt5 = "^5.15.5"
 
 [tool.poetry.dev-dependencies]
 flake8 = "^3.9.2"
diff --git a/src/fastdtlmapper/scripts/plot_gain_loss_map.py b/src/fastdtlmapper/scripts/plot_gain_loss_map.py
new file mode 100644
index 0000000..ccf40fe
--- /dev/null
+++ b/src/fastdtlmapper/scripts/plot_gain_loss_map.py
@@ -0,0 +1,335 @@
+import argparse
+import re
+from dataclasses import dataclass
+from typing import List, Optional
+
+from ete3 import NodeStyle, TextFace, Tree, TreeStyle
+
+
+@dataclass
+class Args:
+    """Arguments DataClass"""
+
+    map_tree_infile: str
+    plot_outfile: str
+    plot_scale: int
+    plot_margin: int
+    plot_width: Optional[int]
+    title: Optional[str]
+    ladderize: bool
+    edit_mode: bool
+    color_gene_num: str
+    color_gain_num: str
+    color_loss_num: str
+    gain_symbol: str
+    loss_symbol: str
+    fsize_node: int
+    fsize_leaf: int
+    fsize_num: int
+    fsize_title: int
+    fsize_legend: int
+
+
+def main(args: Args = None):
+    """Plot gain/loss map main function"""
+    # Get command line arguments
+    if args is None:
+        args = get_args()
+
+    # Convert to ultrametric tree for evenly spaced tree plot
+    # (Adjust minimum branch length = 1.0)
+    tree = Tree(args.map_tree_infile, format=1)
+    max_depth = max([len(n.get_ancestors()) for n in tree.iter_leaves()])
+    tree.convert_to_ultrametric(tree_length=max_depth, strategy="balanced")
+
+    if args.ladderize:
+        tree.ladderize()
+
+    for node in tree.traverse():
+        # Get node name, gene number, gain/loss number
+        pattern = r"^(.+) \| (\d+) \[gain=(\d+) los=(\d+)\]"
+        result = re.match(pattern, node.name)
+        if result is None or None in result.groups():
+            print(f"Input '{args.map_tree_infile}' is invalid gain/loss map file!!")
+            exit(1)
+        node_name, gene_num, gain_num, los_num = result.groups()
+        node.name = node_name
+
+        # Define NodeStyle (no display style)
+        node_style = NodeStyle()
+        node_style["size"] = 0
+        node_style["shape"] = "square"
+        node.set_style(node_style)
+
+        # Define node name TextFace (normal string style)
+        node_name_face = TextFace(node_name, bold=True, fstyle="italic")
+        # Define gene num TextFace (color rectangle style)
+        gene_num_face = TextFace(" " + gene_num, fsize=args.fsize_num, bold=True)
+        gene_num_face.background.color = args.color_gene_num
+        gene_num_face.border.width = 1
+        gene_num_face.margin_left, gene_num_face.margin_right = 0, 2
+        # Define gain num TextFace (color string with symbol)
+        gain_num_face = TextFace(
+            args.gain_symbol + gain_num,
+            fgcolor=args.color_gain_num,
+            fsize=args.fsize_num,
+        )
+        gain_num_face.margin_bottom, gain_num_face.margin_right = 0, 2
+        # Define gene num TextFace (color string with symbol)
+        los_num_face = TextFace(
+            args.loss_symbol + los_num,
+            fgcolor=args.color_loss_num,
+            fsize=args.fsize_num,
+        )
+        los_num_face.margin_top, los_num_face.margin_right = 0, 2
+
+        # Add defined TextFaces to Node(or Leaf)
+        if node.is_leaf():
+            node_name_face.fsize = args.fsize_leaf
+            node_name_face.margin_left, node_name_face.margin_right = 3, 3
+            node.add_face(node_name_face, column=0, position="branch-right")
+            node.add_face(gene_num_face, column=1, position="branch-right")
+            node.add_face(gain_num_face, column=2, position="branch-right")
+            node.add_face(los_num_face, column=2, position="branch-right")
+        else:
+            node_name_face.fsize = args.fsize_node
+            node.add_face(gain_num_face, column=0, position="float")
+            node.add_face(los_num_face, column=0, position="float")
+            node.add_face(TextFace(""), column=1, position="float")
+            node.add_face(gene_num_face, column=1, position="float")
+            node.add_face(node_name_face, column=1, position="float")
+
+    # Define TreeStyle
+    ts = TreeStyle()
+    ts.show_leaf_name = False
+    ts.show_scale = False
+    ts.branch_vertical_margin = args.plot_margin
+    ts.scale = args.plot_scale
+
+    # Define title
+    if args.title:
+        ts.title.add_face(
+            TextFace(args.title, fsize=args.fsize_title, bold=True), column=0
+        )
+        ts.title.add_face(TextFace(""), column=0)
+
+    # Define Legend
+    ts.legend_position = 3
+    gene_legend_face = TextFace("Gene Number", fsize=args.fsize_legend, bold=True)
+    gene_legend_face.border.width = 1
+    gene_legend_face.background.color = args.color_gene_num
+    gene_legend_face.margin_left, gene_legend_face.margin_right = 5, 0
+    gain_legend_face = TextFace(
+        args.gain_symbol + "Gain Number",
+        fsize=args.fsize_legend,
+        fgcolor=args.color_gain_num,
+    )
+    loss_legend_face = TextFace(
+        args.loss_symbol + "Loss Number\n",
+        fsize=args.fsize_legend,
+        fgcolor=args.color_loss_num,
+    )
+    ts.legend.add_face(gene_legend_face, column=0)
+    ts.legend.add_face(gain_legend_face, column=0)
+    ts.legend.add_face(loss_legend_face, column=0)
+
+    if args.edit_mode:
+        # Launch interactive tree editor
+        tree.show(tree_style=ts)
+
+    # Plot gain/loss map tree
+    tree.render(args.plot_outfile, tree_style=ts, w=args.plot_width)
+
+
+def get_args(argv: Optional[List[str]] = None) -> Args:
+    """Get arguments
+
+    Returns:
+        Args: Args Class
+    """
+    parser = argparse.ArgumentParser(description="Plot gain/loss map tool")
+
+    ###########################################################################
+    # Input/Output file path options
+    ###########################################################################
+    parser.add_argument(
+        "-i",
+        "--infile",
+        required=True,
+        type=str,
+        help="Input gain/loss map result file",
+        metavar="I",
+    )
+    parser.add_argument(
+        "-o",
+        "--outfile",
+        required=True,
+        type=str,
+        help="Output plot file (*.png|*.svg|*.pdf)",
+        metavar="O",
+    )
+    ###########################################################################
+    # Plot style options
+    ###########################################################################
+    default_plot_scale = 90
+    parser.add_argument(
+        "--plot_scale",
+        type=int,
+        help=f"Horizontal branch scale plot parameter (Default: {default_plot_scale})",
+        default=default_plot_scale,
+        metavar="",
+    )
+    default_plot_margin = 0
+    parser.add_argument(
+        "--plot_margin",
+        type=int,
+        help=f"Vertical margin plot parameter (Default: {default_plot_margin})",
+        default=default_plot_margin,
+        metavar="",
+    )
+    default_plot_width = None
+    parser.add_argument(
+        "--plot_width",
+        type=int,
+        help=f"Pixel width size plot parameter (Default: {default_plot_width})",
+        default=default_plot_width,
+        metavar="",
+    )
+    default_title = None
+    parser.add_argument(
+        "--title",
+        type=str,
+        help=f"Plot figure title (Default: {default_title})",
+        default=default_title,
+        metavar="",
+    )
+    parser.add_argument(
+        "--ladderize",
+        help="Plot ladderized style tree (Default: off)",
+        action="store_true",
+    )
+    parser.add_argument(
+        "--edit_mode",
+        help="Launch interactive tree editor (Default: off)",
+        action="store_true",
+    )
+    ###########################################################################
+    # Plot color options
+    ###########################################################################
+    default_color_gene = "lightgreen"
+    parser.add_argument(
+        "--color_gene_num",
+        type=str,
+        help=f"Plot color of gene number (Default: '{default_color_gene}')",
+        default=default_color_gene,
+        metavar="",
+    )
+    default_color_gain = "red"
+    parser.add_argument(
+        "--color_gain_num",
+        type=str,
+        help=f"Plot color of gain number (Default: '{default_color_gain}')",
+        default=default_color_gain,
+        metavar="",
+    )
+    default_color_loss = "blue"
+    parser.add_argument(
+        "--color_loss_num",
+        type=str,
+        help=f"Plot color of loss number (Default: '{default_color_loss}')",
+        default=default_color_loss,
+        metavar="",
+    )
+    ###########################################################################
+    # Plot gain/loss symbol options
+    ###########################################################################
+    default_gain_symbol = " ▲ "
+    parser.add_argument(
+        "--gain_symbol",
+        type=str,
+        help=f"Plot symbol of gain gene (Default: '{default_gain_symbol}')",
+        default=default_gain_symbol,
+        metavar="",
+    )
+    default_loss_symbol = " ▼ "
+    parser.add_argument(
+        "--loss_symbol",
+        type=str,
+        help=f"Plot symbol of loss gene (Default: '{default_loss_symbol}')",
+        default=default_loss_symbol,
+        metavar="",
+    )
+    ###########################################################################
+    # Plot font size options
+    ###########################################################################
+    default_fsize_node = 8
+    parser.add_argument(
+        "--fsize_node",
+        type=int,
+        help=f"Plot font size of node name (Default: {default_fsize_node})",
+        default=default_fsize_node,
+        metavar="",
+    )
+    default_fsize_leaf = 10
+    parser.add_argument(
+        "--fsize_leaf",
+        type=int,
+        help=f"Plot font size of leaf(species) name (Default: {default_fsize_leaf})",
+        default=default_fsize_leaf,
+        metavar="",
+    )
+    default_fsize_num = 8
+    parser.add_argument(
+        "--fsize_num",
+        type=int,
+        help=f"Plot font size of gene/gain/loss number (Default: {default_fsize_num})",
+        default=default_fsize_num,
+        metavar="",
+    )
+    default_fsize_title = 30
+    parser.add_argument(
+        "--fsize_title",
+        type=int,
+        help=f"Plot font size of title (Default: {default_fsize_title})",
+        default=default_fsize_title,
+        metavar="",
+    )
+    default_fsize_legend = 8
+    parser.add_argument(
+        "--fsize_legend",
+        type=int,
+        help=f"Plot font size of legend (Default: {default_fsize_legend})",
+        default=default_fsize_legend,
+        metavar="",
+    )
+
+    args = parser.parse_args(argv)
+
+    # Outfile extension validation check
+    if args.outfile[-4:] not in (".png", ".svg", ".pdf"):
+        parser.error("-o/--outfile extension must be *.png|*.svg|*.pdf")
+
+    return Args(
+        args.infile,
+        args.outfile,
+        args.plot_scale,
+        args.plot_margin,
+        args.plot_width,
+        args.title,
+        args.ladderize,
+        args.edit_mode,
+        args.color_gene_num,
+        args.color_gain_num,
+        args.color_loss_num,
+        args.gain_symbol,
+        args.loss_symbol,
+        args.fsize_node,
+        args.fsize_leaf,
+        args.fsize_num,
+        args.fsize_title,
+        args.fsize_legend,
+    )
+
+
+if __name__ == "__main__":
+    main()


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 1365f90..4ce175c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -121,3 +121,14 @@ def goea_association_file(data_dir: Path) -> Path:
 def goea_result_file(data_dir: Path) -> Path:
     """GOEA goatools result file fixture"""
     return data_dir / "goea" / "goea_result.tsv"
+
+
+###########################################################
+# plot_gain_loss_map testdata fixture
+###########################################################
+
+
+@pytest.fixture(scope="session")
+def gain_loss_map_nwk_file(data_dir: Path) -> Path:
+    """gain loss map newick file fixture"""
+    return data_dir / "all_gain_loss_map.nwk"
diff --git a/tests/data/all_gain_loss_map.nwk b/tests/data/all_gain_loss_map.nwk
new file mode 100644
index 0000000..da72eae
--- /dev/null
+++ b/tests/data/all_gain_loss_map.nwk
@@ -0,0 +1,1 @@
+((insp01 | 4430 [gain=2816 los=90]:0.684975,((((insp02 | 2850 [gain=1096 los=422]:0.124628,insp03 | 3280 [gain=1162 los=58]:0.124628)N006 | 2176 [gain=443 los=406]:0.257137,insp04 | 2525 [gain=716 los=330]:0.381764)N005 | 2139 [gain=356 los=728]:0.083451,((insp05 | 3815 [gain=1256 los=747]:0.388686,(((insp06 | 4228 [gain=1810 los=996]:0.270187,((insp07 | 6311 [gain=3560 los=870]:0.204279,insp08 | 4268 [gain=1504 los=857]:0.204279)N013 | 3621 [gain=281 los=523]:0.035805,(insp09 | 3167 [gain=989 los=1415]:0.208855,insp10 | 4762 [gain=1724 los=555]:0.208855)N014 | 3593 [gain=377 los=647]:0.031229)N012 | 3863 [gain=900 los=451]:0.030104)N011 | 3414 [gain=534 los=355]:0.026523,insp11 | 3337 [gain=1052 los=950]:0.296710)N010 | 3235 [gain=571 los=1110]:0.052497,(((((insp12 | 5368 [gain=770 los=236]:0.015703,insp13 | 5039 [gain=583 los=378]:0.015703)N019 | 4834 [gain=724 los=570]:0.141013,insp14 | 5393 [gain=1595 los=882]:0.156716)N018 | 4680 [gain=621 los=728]:0.080264,insp15 | 5408 [gain=1737 los=1116]:0.236980)N017 | 4787 [gain=516 los=747]:0.058583,(insp16 | 6027 [gain=2168 los=1007]:0.274289,(insp17 | 5498 [gain=3119 los=1515]:0.234242,insp18 | 4474 [gain=1421 los=841]:0.234242)N021 | 3894 [gain=260 los=1232]:0.040047)N020 | 4866 [gain=437 los=589]:0.021275)N016 | 5018 [gain=887 los=394]:0.030598,(insp19 | 5704 [gain=2045 los=690]:0.293260,(insp20 | 4451 [gain=1789 los=1037]:0.236459,insp21 | 6630 [gain=3514 los=583]:0.236459)N023 | 3699 [gain=430 los=1080]:0.056802)N022 | 4349 [gain=599 los=775]:0.032901)N015 | 4525 [gain=943 los=192]:0.023046)N009 | 3774 [gain=652 los=184]:0.039479)N008 | 3306 [gain=691 los=286]:0.036002,(insp22 | 2475 [gain=758 los=951]:0.360947,insp23 | 6254 [gain=4034 los=448]:0.360947)N024 | 2668 [gain=288 los=521]:0.063741)N007 | 2901 [gain=494 los=104]:0.040527)N004 | 2511 [gain=460 los=82]:0.062321,insp24 | 3561 [gain=1998 los=570]:0.527535)N003 | 2133 [gain=593 los=164]:0.157440)N002 | 1704 [gain=757 los=1]:0.497301,((outsp01 | 4185 [gain=2190 los=312]:0.851644,outsp02 | 3574 [gain=1957 los=690]:0.851644)N026 | 2307 [gain=556 los=208]:0.305632,(outsp03 | 4189 [gain=2209 los=740]:0.440819,outsp04 | 7769 [gain=5211 los=162]:0.440819)N027 | 2720 [gain=873 los=112]:0.716457)N025 | 1959 [gain=1011 los=0]:0.025001)N001 | 948 [gain=948 los=0];
diff --git a/tests/scripts/test_plot_gain_loss_map.py b/tests/scripts/test_plot_gain_loss_map.py
new file mode 100644
index 0000000..90b73ca
--- /dev/null
+++ b/tests/scripts/test_plot_gain_loss_map.py
@@ -0,0 +1,46 @@
+from pathlib import Path
+
+import pytest
+from fastdtlmapper.scripts.plot_gain_loss_map import get_args, main
+
+
+def test_plot_gain_loss_map_default_param(
+    gain_loss_map_nwk_file: Path,
+    tmp_path: Path,
+):
+    """test plot_gain_loss_map with default param"""
+    outfile = tmp_path / "all_gain_loss_map.png"
+    argv = f"-i {gain_loss_map_nwk_file} -o {outfile}"
+    args = get_args(argv.split(" "))
+    main(args)
+    assert outfile.is_file()
+
+
+def test_plot_gain_loss_map_user_param(
+    gain_loss_map_nwk_file: Path,
+    tmp_path: Path,
+):
+    """test plot_gain_loss_map with user param"""
+    outfile = tmp_path / "all_gain_loss_map.svg"
+    # Perfect plot test required confirming figure contents
+    # It's impossible, so Color,Symbol,FontSize options are ignored
+    argv = (
+        f"-i {gain_loss_map_nwk_file} -o {outfile} --plot_margin 100 "
+        + "--plot_width 1000 --title helloworld --ladderize"
+    )
+    args = get_args(argv.split(" "))
+    main(args)
+    assert outfile.is_file()
+
+
+def test_plot_gain_loss_map_ng_extension(
+    gain_loss_map_nwk_file: Path,
+    tmp_path: Path,
+):
+    """test plot_gain_loss_map with outfile extension error"""
+    outfile_invalid_ext = tmp_path / "all_gain_loss_map.invalid"
+    argv = f"-i {gain_loss_map_nwk_file} -o {outfile_invalid_ext}"
+    with pytest.raises(SystemExit) as e:
+        get_args(argv.split(" "))
+    assert e.type == SystemExit
+    assert e.value.code == 2