Repository: haddocking/arctic3d

Issue:
create plotly barplot in localise CLI
plotly barplot is better for the web service

Discussion:

Patch:
diff --git a/poetry.lock b/poetry.lock
index 5cf15e9..b9469db 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,9 +1,10 @@
-# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry and should not be changed by hand.
 
 [[package]]
 name = "attrs"
 version = "22.2.0"
 description = "Classes Without Boilerplate"
+category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -22,6 +23,7 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy
 name = "bio"
 version = "1.5.9"
 description = "bio"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -41,6 +43,7 @@ tqdm = "*"
 name = "biopython"
 version = "1.81"
 description = "Freely available tools for computational molecular biology."
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -77,6 +80,7 @@ numpy = "*"
 name = "biothings-client"
 version = "0.2.6"
 description = "Python Client for BioThings API services."
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -96,6 +100,7 @@ jsonld = ["PyLD (>=0.7.2)"]
 name = "certifi"
 version = "2022.12.7"
 description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -107,6 +112,7 @@ files = [
 name = "charset-normalizer"
 version = "3.0.1"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -204,6 +210,7 @@ files = [
 name = "colorama"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
+category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
@@ -215,6 +222,7 @@ files = [
 name = "contourpy"
 version = "1.0.7"
 description = "Python library for calculating contours of 2D quadrilateral grids"
+category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -289,6 +297,7 @@ test-no-images = ["pytest"]
 name = "coverage"
 version = "7.2.7"
 description = "Code coverage measurement for Python"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -364,6 +373,7 @@ toml = ["tomli"]
 name = "cycler"
 version = "0.11.0"
 description = "Composable style cycles"
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -375,6 +385,7 @@ files = [
 name = "defusedxml"
 version = "0.7.1"
 description = "XML bomb protection for Python stdlib modules"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 files = [
@@ -386,6 +397,7 @@ files = [
 name = "et-xmlfile"
 version = "1.1.0"
 description = "An implementation of lxml.xmlfile for the standard library"
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -397,6 +409,7 @@ files = [
 name = "exceptiongroup"
 version = "1.1.0"
 description = "Backport of PEP 654 (exception groups)"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -411,6 +424,7 @@ test = ["pytest (>=6)"]
 name = "fasteners"
 version = "0.18"
 description = "A python package that provides useful locks"
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -422,6 +436,7 @@ files = [
 name = "fonttools"
 version = "4.38.0"
 description = "Tools to manipulate font files"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -447,6 +462,7 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
 name = "gprofiler-official"
 version = "1.0.0"
 description = "Functional enrichment analysis and more via the g:Profiler toolkit"
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -461,6 +477,7 @@ requests = "*"
 name = "griddataformats"
 version = "1.0.1"
 description = "Reading and writing of data on regular grids in Python"
+category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -477,6 +494,7 @@ scipy = "*"
 name = "gsd"
 version = "2.7.0"
 description = "General simulation data file format."
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -512,6 +530,7 @@ numpy = ">=1.9.3"
 name = "hypothesis"
 version = "6.82.0"
 description = "A library for property-based testing"
+category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -544,6 +563,7 @@ zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2023.3)"]
 name = "idna"
 version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -555,6 +575,7 @@ files = [
 name = "importlib-resources"
 version = "5.12.0"
 description = "Read resources from Python packages"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -573,6 +594,7 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec
 name = "iniconfig"
 version = "2.0.0"
 description = "brain-dead simple config-ini parsing"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -584,6 +606,7 @@ files = [
 name = "joblib"
 version = "1.2.0"
 description = "Lightweight pipelining with Python functions"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -595,6 +618,7 @@ files = [
 name = "jsonpickle"
 version = "3.0.1"
 description = "Python library for serializing any arbitrary object graph into JSON"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -607,10 +631,27 @@ docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"]
 testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"]
 testing-libs = ["simplejson", "ujson"]
 
+[[package]]
+name = "kaleido"
+version = "0.2.1"
+description = "Static image export for web-based visualization libraries with zero dependencies"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "kaleido-0.2.1-py2.py3-none-macosx_10_11_x86_64.whl", hash = "sha256:ca6f73e7ff00aaebf2843f73f1d3bacde1930ef5041093fe76b83a15785049a7"},
+    {file = "kaleido-0.2.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:bb9a5d1f710357d5d432ee240ef6658a6d124c3e610935817b4b42da9c787c05"},
+    {file = "kaleido-0.2.1-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:aa21cf1bf1c78f8fa50a9f7d45e1003c387bd3d6fe0a767cfbbf344b95bdc3a8"},
+    {file = "kaleido-0.2.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:845819844c8082c9469d9c17e42621fbf85c2b237ef8a86ec8a8527f98b6512a"},
+    {file = "kaleido-0.2.1-py2.py3-none-win32.whl", hash = "sha256:ecc72635860be616c6b7161807a65c0dbd9b90c6437ac96965831e2e24066552"},
+    {file = "kaleido-0.2.1-py2.py3-none-win_amd64.whl", hash = "sha256:4670985f28913c2d063c5734d125ecc28e40810141bdb0a46f15b76c1d45f23c"},
+]
+
 [[package]]
 name = "kiwisolver"
 version = "1.4.4"
 description = "A fast implementation of the Cassowary constraint solver"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -688,6 +729,7 @@ files = [
 name = "lxml"
 version = "4.9.3"
 description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
 files = [
@@ -795,6 +837,7 @@ source = ["Cython (>=0.29.35)"]
 name = "matplotlib"
 version = "3.7.2"
 description = "Python plotting package"
+category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -857,6 +900,7 @@ python-dateutil = ">=2.7"
 name = "mdanalysis"
 version = "2.5.0"
 description = "An object-oriented toolkit to analyze molecular dynamics trajectories."
+category = "main"
 optional = false
 python-versions = ">=3.9"
 files = [
@@ -892,6 +936,7 @@ packaging = "*"
 scipy = ">=1.5.0"
 threadpoolctl = "*"
 tqdm = ">=4.43.0"
+kaleido = "0.2.1"
 
 [package.extras]
 analysis = ["scikit-learn", "seaborn", "tidynamics (>=1.0.0)"]
@@ -902,6 +947,7 @@ extra-formats = ["chemfiles (>=0.10)", "h5py (>=2.10)", "netCDF4 (>=1.0)", "pyed
 name = "mmtf-python"
 version = "1.1.3"
 description = "A decoding libary for the PDB mmtf format"
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -920,6 +966,7 @@ test = ["coverage"]
 name = "mrcfile"
 version = "1.4.3"
 description = "MRC file I/O library"
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -934,6 +981,7 @@ numpy = ">=1.16.0"
 name = "msgpack"
 version = "1.0.4"
 description = "MessagePack serializer"
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -995,6 +1043,7 @@ files = [
 name = "mygene"
 version = "3.2.2"
 description = "Python Client for MyGene.Info services."
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1009,6 +1058,7 @@ biothings-client = ">=0.2.6"
 name = "networkx"
 version = "3.0"
 description = "Python package for creating and manipulating graphs and networks"
+category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1027,6 +1077,7 @@ test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"]
 name = "numpy"
 version = "1.24.2"
 description = "Fundamental package for array computing in Python"
+category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1064,6 +1115,7 @@ files = [
 name = "openpyxl"
 version = "3.1.2"
 description = "A Python library to read/write Excel 2010 xlsx/xlsm files"
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1078,6 +1130,7 @@ et-xmlfile = "*"
 name = "packaging"
 version = "23.0"
 description = "Core utilities for Python packages"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1089,6 +1142,7 @@ files = [
 name = "pandas"
 version = "2.0.3"
 description = "Powerful data structures for data analysis, time series, and statistics"
+category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1156,6 +1210,7 @@ xml = ["lxml (>=4.6.3)"]
 name = "pdb-tools"
 version = "2.5.0"
 description = "A swiss army knife for PDB files."
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1167,6 +1222,7 @@ files = [
 name = "pdbecif"
 version = "1.5"
 description = "A lightweight pure python package for reading, writing and manipulating mmCIF files distributed by the wwPDB."
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1179,6 +1235,7 @@ files = [
 name = "pillow"
 version = "9.4.0"
 description = "Python Imaging Library (Fork)"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1269,6 +1326,7 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
 name = "platformdirs"
 version = "3.1.1"
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1284,6 +1342,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytes
 name = "plotly"
 version = "5.15.0"
 description = "An open-source, interactive data visualization library for Python"
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1299,6 +1358,7 @@ tenacity = ">=6.2.0"
 name = "pluggy"
 version = "1.0.0"
 description = "plugin and hook calling mechanisms for python"
+category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1314,6 +1374,7 @@ testing = ["pytest", "pytest-benchmark"]
 name = "pooch"
 version = "1.7.0"
 description = "\"Pooch manages your Python library's sample data files: it automatically downloads and stores them in a local directory, with support for versioning and corruption checks.\""
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1335,6 +1396,7 @@ xxhash = ["xxhash (>=1.4.3)"]
 name = "pyparsing"
 version = "3.0.9"
 description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "main"
 optional = false
 python-versions = ">=3.6.8"
 files = [
@@ -1349,6 +1411,7 @@ diagrams = ["jinja2", "railroad-diagrams"]
 name = "pytest"
 version = "7.4.0"
 description = "pytest: simple powerful testing with Python"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1371,6 +1434,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
 name = "pytest-cov"
 version = "4.1.0"
 description = "Pytest plugin for measuring coverage."
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1389,6 +1453,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
 name = "python-dateutil"
 version = "2.8.2"
 description = "Extensions to the standard Python datetime module"
+category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 files = [
@@ -1403,6 +1468,7 @@ six = ">=1.5"
 name = "pytz"
 version = "2022.7.1"
 description = "World timezone definitions, modern and historical"
+category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1414,6 +1480,7 @@ files = [
 name = "requests"
 version = "2.31.0"
 description = "Python HTTP for Humans."
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1435,6 +1502,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 name = "scipy"
 version = "1.11.1"
 description = "Fundamental algorithms for scientific computing in Python"
+category = "main"
 optional = false
 python-versions = "<3.13,>=3.9"
 files = [
@@ -1471,6 +1539,7 @@ test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeo
 name = "six"
 version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -1482,6 +1551,7 @@ files = [
 name = "sortedcontainers"
 version = "2.4.0"
 description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
+category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1493,6 +1563,7 @@ files = [
 name = "tenacity"
 version = "8.1.0"
 description = "Retry code until it succeeds"
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1507,6 +1578,7 @@ doc = ["reno", "sphinx", "tornado (>=4.5)"]
 name = "threadpoolctl"
 version = "3.1.0"
 description = "threadpoolctl"
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1518,6 +1590,7 @@ files = [
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1529,6 +1602,7 @@ files = [
 name = "tqdm"
 version = "4.64.1"
 description = "Fast, Extensible Progress Meter"
+category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
 files = [
@@ -1549,6 +1623,7 @@ telegram = ["requests"]
 name = "tzdata"
 version = "2023.3"
 description = "Provider of IANA time zone data"
+category = "main"
 optional = false
 python-versions = ">=2"
 files = [
@@ -1560,6 +1635,7 @@ files = [
 name = "urllib3"
 version = "1.26.14"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 files = [
@@ -1576,6 +1652,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 name = "zipp"
 version = "3.15.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1590,4 +1667,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.9,<3.12"
-content-hash = "559408bf30e29c970f9268b1f00052ce372a59d37267b1ba8fe32dc7fe227166"
+content-hash = "c7288a3b35c225416a3ca3451849abfea76d023e6e7c150a2e14c8f9170433b5"
diff --git a/pyproject.toml b/pyproject.toml
index dbabbdc..9a8275b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ openpyxl = "3.1.2"
 plotly = "5.15.0"
 pdbecif = "1.5"
 jsonpickle = "3.0.1"
+kaleido = "0.2.1"
 
 [tool.poetry.scripts]
 arctic3d = "arctic3d.cli:maincli"
diff --git a/src/arctic3d/cli_localise.py b/src/arctic3d/cli_localise.py
index d3b6ea0..d24e46a 100644
--- a/src/arctic3d/cli_localise.py
+++ b/src/arctic3d/cli_localise.py
@@ -29,6 +29,17 @@ and biological process (P)::
 
     arctic3d-localise ./example/clustered_interfaces.out \
         --quickgo=F
+
+By default the CLI will output html files and png images. It is possible
+to specify the output format with the format parameter::
+
+    arctic3d-localise ./example/clustered_interfaces.out \
+        --format=pdf
+
+The scale parameter can be used to scale the images::
+
+    arctic3d-localise ./example/clustered_interfaces.out \
+        --scale=2.0
 """
 import argparse
 import os
@@ -42,7 +53,8 @@ from arctic3d.functions import make_request
 from arctic3d.modules.interface import parse_out_partner
 from arctic3d.modules.log import add_log_for_CLI
 from arctic3d.modules.output import (
-    create_barplot,
+    #    create_barplot,
+    create_barplotly,
     create_output_folder,
     parse_clusters,
     setup_output_folder,
@@ -85,6 +97,19 @@ argument_parser.add_argument(
     default="no",
 )
 
+argument_parser.add_argument(
+    "--format",
+    help="produce images in the desired format",
+    required=False,
+    type=str,
+    default="png",
+    choices=["png", "pdf", "svg", "jpeg", "webp"],
+)
+
+argument_parser.add_argument(
+    "--scale", help="scale for images", required=False, type=float, default=4.0
+)
+
 
 def get_uniprot_subcellular_location(prot_data):
     """
@@ -302,7 +327,16 @@ def maincli():
     cli(argument_parser, main)
 
 
-def main(input_arg, run_dir, out_partner, quickgo, weight, log_level="DEBUG"):
+def main(
+    input_arg,
+    run_dir,
+    out_partner,
+    quickgo,
+    weight,
+    format,
+    scale,
+    log_level="DEBUG",
+):
     """Main function."""
     log.setLevel(log_level)
     start_time = time.time()
@@ -413,7 +447,8 @@ def main(input_arg, run_dir, out_partner, quickgo, weight, log_level="DEBUG"):
             # get sorted dictionary
             sort_dict = get_sorted_dict(cl_bins[cluster])
             # plot
-            create_barplot(cluster, sort_dict, max_labels=50)
+            # create_barplot(cluster, sort_dict, max_labels=50)
+            create_barplotly(cluster, sort_dict, format, scale, max_labels=25)
 
     # saving histograms
     os.mkdir("histograms")
diff --git a/src/arctic3d/modules/output.py b/src/arctic3d/modules/output.py
index e0c4e65..7f75c08 100644
--- a/src/arctic3d/modules/output.py
+++ b/src/arctic3d/modules/output.py
@@ -503,6 +503,25 @@ def create_barplot(cluster, sorted_dict, max_labels=70):
     return
 
 
+def create_barplotly(cluster, sorted_dict, format, scale, max_labels=25):
+    """
+    Create horizontal barplot using plotly.
+
+    """
+    labels = shorten_labels(list(sorted_dict.keys())[-max_labels:])
+    values = list(sorted_dict.values())[-max_labels:]
+    fig = go.Figure(go.Bar(x=values, y=labels, orientation="h"))
+    fig_fname = f"cluster_{cluster}.html"
+    fig.write_html(fig_fname)
+    log.info(f"Figure {fig_fname} created")
+    # also export it in png format
+    fig.update_yaxes(tickmode="linear")
+    fig_fname = f"cluster_{cluster}.{format}"
+    fig.write_image(fig_fname, scale=scale)
+    log.info(f"Figure {fig_fname} created")
+    return
+
+
 def get_init_message():
     """
     Get initial message.


Test patch:
diff --git a/tests/test_cli_localise.py b/tests/test_cli_localise.py
index 13a8ecf..6a61f96 100644
--- a/tests/test_cli_localise.py
+++ b/tests/test_cli_localise.py
@@ -50,6 +50,8 @@ def test_localise_cli_empty(empty_cluster_filepath):
         None,
         None,
         None,
+        None,
+        None,
     )
     os.chdir(start_cwd)
     # Check that the output directory has been created