diff --git a/mypy.ini b/mypy.ini --- a/mypy.ini +++ b/mypy.ini @@ -13,3 +13,15 @@ [mypy-ndjson.*] ignore_missing_imports = True + +[mypy-dash.*] +ignore_missing_imports = True + +[mypy-dash_core_components.*] +ignore_missing_imports = True + +[mypy-dash_html_components.*] +ignore_missing_imports = True + +[mypy-plotly.*] +ignore_missing_imports = True \ No newline at end of file diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ plotly pandas numpy +dash dulwich diff --git a/swh/scanner/cli.py b/swh/scanner/cli.py --- a/swh/scanner/cli.py +++ b/swh/scanner/cli.py @@ -13,6 +13,8 @@ from .scanner import run from .model import Tree +from .plot import generate_sunburst +from .dashboard import run_app from .exceptions import InvalidDirectoryPath from swh.core.cli import CONTEXT_SETTINGS @@ -80,8 +82,11 @@ default="text", help="select the output format", ) +@click.option( + "-i", "--interactive", is_flag=True, help="show the result in a dashboard" +) @click.pass_context -def scan(ctx, root_path, api_url, patterns, format): +def scan(ctx, root_path, api_url, patterns, format, interactive): """Scan a source code project to discover files and directories already present in the archive""" sre_patterns = set() @@ -95,7 +100,13 @@ loop = asyncio.get_event_loop() loop.run_until_complete(run(root_path, api_url, source_tree, sre_patterns)) - source_tree.show(format) + if interactive: + root = PosixPath(root_path) + directories = source_tree.getDirectoriesInfo(root) + figure = generate_sunburst(directories, root) + run_app(figure, source_tree) + else: + source_tree.show(format) if __name__ == "__main__": diff --git a/swh/scanner/dashboard.py b/swh/scanner/dashboard.py new file mode 100644 --- /dev/null +++ b/swh/scanner/dashboard.py @@ -0,0 +1,24 @@ +# Copyright (C) 2020 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from .model import Tree + +import plotly.graph_objects as go +import dash +import dash_core_components as dcc +import dash_html_components as html + + +def run_app(graph_obj: go, source: Tree): + app = dash.Dash(__name__) + fig = go.Figure().add_trace(graph_obj) + + fig.update_layout(height=800,) + + app.layout = html.Div( + [html.Div([html.Div([dcc.Graph(id="sunburst_chart", figure=fig),]),]),] + ) + + app.run_server(debug=True, use_reloader=False) diff --git a/swh/scanner/model.py b/swh/scanner/model.py --- a/swh/scanner/model.py +++ b/swh/scanner/model.py @@ -12,7 +12,7 @@ import ndjson -from .plot import sunburst +from .plot import generate_sunburst, offline_plot from .exceptions import InvalidObjectType from swh.model.identifiers import DIRECTORY, CONTENT @@ -73,7 +73,8 @@ elif format == "sunburst": root = self.path directories = self.getDirectoriesInfo(root) - sunburst(directories, root) + sunburst = generate_sunburst(directories, root) + offline_plot(sunburst) def printChildren(self, isatty: bool, inc: int = 1) -> None: for path, node in self.children.items(): diff --git a/swh/scanner/plot.py b/swh/scanner/plot.py --- a/swh/scanner/plot.py +++ b/swh/scanner/plot.py @@ -18,8 +18,8 @@ from typing import List, Dict, Tuple from pathlib import PosixPath -from plotly.offline import offline # type: ignore -import plotly.graph_objects as go # type: ignore +from plotly.offline import offline +import plotly.graph_objects as go import pandas as pd # type: ignore import numpy as np # type: ignore @@ -236,8 +236,10 @@ return df -def sunburst(directories: Dict[PosixPath, Tuple[int, int]], root: PosixPath) -> None: - """Show the sunburst chart from the directories given in input. +def generate_sunburst( + directories: Dict[PosixPath, Tuple[int, int]], root: PosixPath +) -> go.Sunburst: + """Generate a sunburst chart from the directories given in input. """ max_depth = compute_max_depth(list(directories.keys()), root) @@ -251,24 +253,29 @@ dirs_df, levels_columns, metrics_columns, str(root) ) - fig = go.Figure() - fig.add_trace( - go.Sunburst( - labels=hierarchical_df["id"], - parents=hierarchical_df["parent"], - values=hierarchical_df["contents"], - branchvalues="total", - marker=dict( - colors=hierarchical_df["known"], - colorscale="matter", - cmid=50, - showscale=True, - ), - hovertemplate="""%{label} -
Files: %{value} -
Known: %{color:.2f}%""", - name="", - ) + sunburst = go.Sunburst( + labels=hierarchical_df["id"], + parents=hierarchical_df["parent"], + values=hierarchical_df["contents"], + branchvalues="total", + marker=dict( + colors=hierarchical_df["known"], + colorscale="matter", + cmid=50, + showscale=True, + ), + hovertemplate="""%{label} +
Files: %{value} +
Known: %{color:.2f}%""", + name="", ) - offline.plot(fig, filename="sunburst.html") + return sunburst + + +def offline_plot(graph_object: go): + """Plot a graph object to an html file + """ + fig = go.Figure() + fig.add_trace(graph_object) + offline.plot(fig, filename="chart.html")