From 0f3e263feae9cd9e2394f19ceaa53ce7eae0a58f Mon Sep 17 00:00:00 2001 From: Ben Sherman Date: Tue, 26 Sep 2023 04:36:22 -0500 Subject: [PATCH] Use Mermaid diagram in HTML DAG (#4337) Signed-off-by: Ben Sherman Signed-off-by: Paolo Di Tommaso Co-authored-by: Paolo Di Tommaso --- docs/cli.md | 5 +- docs/config.md | 18 ++- docs/images/dag.mmd | 2 +- docs/images/dag.png | Bin 38622 -> 0 bytes docs/tracing.md | 69 ++++++---- .../nextflow/dag/CytoscapeJsRenderer.groovy | 74 ----------- ...erer.groovy => MermaidHtmlRenderer.groovy} | 25 ++-- .../nextflow/dag/MermaidRenderer.groovy | 25 ++-- .../nextflow/trace/GraphObserver.groovy | 8 +- .../dag/cytoscape.js.dag.template.html | 119 ------------------ .../nextflow/dag/mermaid.dag.template.html | 29 +++++ .../nextflow/config/ConfigBuilderTest.groovy | 2 +- .../nextflow/dag/MermaidRendererTest.groovy | 2 +- .../nextflow/trace/GraphObserverTest.groovy | 41 +++--- tests/checks/hello.nf/.checks | 2 +- 15 files changed, 147 insertions(+), 274 deletions(-) delete mode 100644 docs/images/dag.png delete mode 100644 modules/nextflow/src/main/groovy/nextflow/dag/CytoscapeJsRenderer.groovy rename modules/nextflow/src/main/groovy/nextflow/dag/{CytoscapeHtmlRenderer.groovy => MermaidHtmlRenderer.groovy} (55%) delete mode 100644 modules/nextflow/src/main/resources/nextflow/dag/cytoscape.js.dag.template.html create mode 100644 modules/nextflow/src/main/resources/nextflow/dag/mermaid.dag.template.html diff --git a/docs/cli.md b/docs/cli.md index 89b274d881..3fe270f7e1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1226,8 +1226,11 @@ The `run` command is used to execute a local pipeline script or remote pipeline `-with-conda` : Use the specified Conda environment package or file (must end with `.yml` or `.yaml`) -`-with-dag` (`dag-.dot`) +`-with-dag` (`dag-.html`) : Create pipeline DAG file. +: :::{versionchanged} 23.10.0 + The default format was changed from `dot` to `html`. + ::: `-with-docker` : Enable process execution in a Docker container. diff --git a/docs/config.md b/docs/config.md index c013547aaf..2c51b40ddf 100644 --- a/docs/config.md +++ b/docs/config.md @@ -511,15 +511,22 @@ The `dag` scope controls the workflow diagram generated by Nextflow. The following settings are available: `dag.enabled` -: When `true` turns on the generation of the DAG file (default: `false`). +: When `true` enables the generation of the DAG file (default: `false`). `dag.depth` : :::{versionadded} 23.10.0 ::: -: Controls the maximum depth at which to render sub-workflows (default: no limit; only supported by the Mermaid render). +: *Only supported by the HTML and Mermaid renderers.* +: Controls the maximum depth at which to render sub-workflows (default: no limit). + +`dag.direction` +: :::{versionadded} 23.10.0 +::: +: *Only supported by the HTML and Mermaid renderers.* +: Controls the direction of the DAG, can be `'LR'` (left-to-right) or `'TB'` (top-to-bottom) (default: `'TB'`). `dag.file` -: Graph file name (default: `dag-.dot`). +: Graph file name (default: `dag-.html`). `dag.overwrite` : When `true` overwrites any existing DAG file with the same name (default: `false`). @@ -527,9 +534,10 @@ The following settings are available: `dag.verbose` : :::{versionadded} 23.10.0 ::: -: When `false`, channel names are omitted, operators are collapsed, and empty workflow inputs are removed (default: `false`; only supported by the Mermaid render). +: *Only supported by the HTML and Mermaid renderers.* +: When `false`, channel names are omitted, operators are collapsed, and empty workflow inputs are removed (default: `false`). -Read the {ref}`dag-visualisation` page to learn more about the execution graph that can be generated by Nextflow. +Read the {ref}`dag-visualisation` page to learn more about the workflow graph that can be generated by Nextflow. (config-docker)= diff --git a/docs/images/dag.mmd b/docs/images/dag.mmd index 388eaf306b..0963e2c106 100644 --- a/docs/images/dag.mmd +++ b/docs/images/dag.mmd @@ -1,4 +1,4 @@ -flowchart TD +flowchart TB subgraph " " v0["Channel.fromFilePairs"] v1["transcriptome"] diff --git a/docs/images/dag.png b/docs/images/dag.png deleted file mode 100644 index 33ca19f459af3a693250f6e8af54262abffb4077..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38622 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 -with-report [file name] +nextflow run -with-report [file name] ``` The report file name can be specified as an optional parameter following the report option. @@ -151,7 +151,7 @@ Nextflow creates an execution tracing file that contains some useful information In order to create the execution trace file add the `-with-trace` command line option when launching the pipeline execution. For example: ```bash -nextflow run -with-trace +nextflow run -with-trace ``` It will create a file named `trace.txt` in the current directory. The content looks like the above example: @@ -338,7 +338,7 @@ As each process can spawn many tasks, colors are used to identify those tasks be To enable the creation of the timeline report add the `-with-timeline` command line option when launching the pipeline execution. For example: ```bash -nextflow run -with-timeline [file name] +nextflow run -with-timeline [file name] ``` The report file name can be specified as an optional parameter following the timeline option. @@ -347,45 +347,60 @@ The report file name can be specified as an optional parameter following the tim ## DAG visualisation -A Nextflow pipeline is implicitly modelled by a direct acyclic graph (DAG). The vertices in the graph represent the pipeline's processes and operators, while the edges represent the data connections (i.e. channels) between them. +A Nextflow pipeline can be represented as a direct acyclic graph (DAG). The vertices in the graph represent the pipeline's processes and operators, while the edges represent the data dependencies (i.e. channels) between them. -The pipeline execution DAG can be outputted by adding the `-with-dag` option to the run command line. It creates a file named `dag.dot` containing a textual representation of the pipeline execution graph in the [DOT format](http://www.graphviz.org/content/dot-language). +To render the workflow DAG, run your pipeline with the `-with-dag` option. By default, it creates a file named `dag-.html` with the workflow DAG rendered as a [Mermaid](https://mermaid.js.org/) diagram. -The execution DAG can be rendered in a different format by specifying an output file name which has an extension corresponding to the required format. For example: +The workflow DAG can be rendered in a different format by specifying an output file name with a different extension based on the desired format. For example: ```bash -nextflow run -with-dag flowchart.png +nextflow run -with-dag flowchart.png ``` -List of supported file formats: - -| Extension | File format | -| --------- | ------------------------------- | -| dot | Graphviz DOT file | -| html | HTML file | -| mmd | Mermaid diagram | -| pdf | PDF file (\*) | -| png | PNG file (\*) | -| svg | SVG file (\*) | -| gexf | Graph Exchange XML file (Gephi) | +:::{versionadded} 22.06.0-edge +You can use the `-preview` option with `-with-dag` to render the workflow DAG without executing any tasks. +::: -:::{note} -File formats marked with "\*" require the [Graphviz](http://www.graphviz.org) tool to be installed. +:::{versionchanged} 23.10.0 +The default output format was changed from DOT to HTML. ::: -The DAG produced by Nextflow for the [Unistrap](https://github.com/cbcrg/unistrap/) pipeline: +The following file formats are supported: -```{image} images/dag.png -``` +`dot` +: Graphviz [DOT](http://www.graphviz.org/content/dot-language) file -### Mermaid diagram +`gexf` +: Graph Exchange XML file (Gephi) -:::{versionadded} 22.04.0 -::: +`html` +: HTML file with Mermaid diagram +: :::{versionchanged} 23.10.0 + The HTML format was changed to render a Mermaid diagram instead of a Cytoscape diagram. + ::: -Nextflow can render the DAG as a [Mermaid](https://mermaid-js.github.io/) diagram. Mermaid diagrams are particularly useful because they can be embedded in [GitHub Flavored Markdown](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/) without having to render them yourself. You can customize the diagram with CSS, and you can even add links! Visit the [Mermaid documentation](https://mermaid-js.github.io/mermaid/#/flowchart?id=styling-and-classes) for details. +`mmd` +: :::{versionadded} 22.04.0 + ::: +: Mermaid diagram + +`pdf` +: *Requires [Graphviz](http://www.graphviz.org) to be installed* +: Graphviz PDF file + +`png` +: *Requires [Graphviz](http://www.graphviz.org) to be installed* +: Graphviz PNG file + +`svg` +: *Requires [Graphviz](http://www.graphviz.org) to be installed* +: Graphviz SVG file Here is the Mermaid diagram produced by Nextflow for the [rnaseq-nf](https://github.com/nextflow-io/rnaseq-nf) pipeline (using the [Mermaid Live Editor](https://mermaid-js.github.io/mermaid-live-editor/edit) with the `default` theme): +```bash +nextflow run rnaseq-nf -preview -with-dag +``` + ```{mermaid} images/dag.mmd ``` diff --git a/modules/nextflow/src/main/groovy/nextflow/dag/CytoscapeJsRenderer.groovy b/modules/nextflow/src/main/groovy/nextflow/dag/CytoscapeJsRenderer.groovy deleted file mode 100644 index dfcb8246df..0000000000 --- a/modules/nextflow/src/main/groovy/nextflow/dag/CytoscapeJsRenderer.groovy +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2013-2023, Seqera Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package nextflow.dag -import java.nio.file.Path - -/** - * Render the DAG in Cytoscape.js compatible - * JSON to the specified file. - * See http://js.cytoscape.org for more info. - * - * @author Paolo Di Tommaso - * @author Mike Smoot - */ -class CytoscapeJsRenderer implements DagRenderer { - - @Override - void renderDocument(DAG dag, Path file) { - file.text = renderNetwork(dag) - } - - static String renderNetwork(DAG dag) { - def result = [] - result << "elements: {" - - result << "nodes: [" - dag.vertices.each { vertex -> result << renderVertex( vertex ) } - result << "]," - - result << "edges: [" - dag.edges.each { edge -> result << renderEdge( edge ) } - result << "]," - - result << "}," - - return result.join('\n') - } - - private static String renderVertex(vertex) { - String pre = "{ data: { id: '${vertex.getName()}'" - String post = "}, classes: '${vertex.type.name()}' }," - if (vertex.label) { - return pre + ", label: '${vertex.label}'" + post - } else { - return pre + post - } - } - - - private static String renderEdge(edge) { - assert edge.from != null && edge.to != null - String dat = "{ data: { source: '${edge.from.name}', target: '${edge.to.name}'" - if ( edge.label ) { - return dat + ", label: '${edge.label}' } }," - } - else { - return dat + "} }," - } - } - -} diff --git a/modules/nextflow/src/main/groovy/nextflow/dag/CytoscapeHtmlRenderer.groovy b/modules/nextflow/src/main/groovy/nextflow/dag/MermaidHtmlRenderer.groovy similarity index 55% rename from modules/nextflow/src/main/groovy/nextflow/dag/CytoscapeHtmlRenderer.groovy rename to modules/nextflow/src/main/groovy/nextflow/dag/MermaidHtmlRenderer.groovy index 5aff50ffe3..f2d175b89b 100644 --- a/modules/nextflow/src/main/groovy/nextflow/dag/CytoscapeHtmlRenderer.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/dag/MermaidHtmlRenderer.groovy @@ -15,32 +15,31 @@ */ package nextflow.dag + import java.nio.file.Path /** - * Render the DAG in HTML using Cytoscape.js - * to the specified file. - * See http://js.cytoscape.org for more info. + * Render the DAG as a Mermaid diagram embedded in an HTML document. + * See https://mermaid.js.org/ for more info. * - * @author Paolo Di Tommaso - * @author Mike Smoot + * @author Ben Sherman */ -class CytoscapeHtmlRenderer implements DagRenderer { +class MermaidHtmlRenderer implements DagRenderer { @Override void renderDocument(DAG dag, Path file) { - String tmplPage = readTemplate() - String network = CytoscapeJsRenderer.renderNetwork(dag) - file.text = tmplPage.replaceAll(~/\/\* REPLACE_WITH_NETWORK_DATA \*\//, network) + final template = readTemplate() + final network = new MermaidRenderer().renderNetwork(dag) + file.text = template.replace('REPLACE_WITH_NETWORK_DATA', network) } private String readTemplate() { - StringWriter writer = new StringWriter(); - def res = CytoscapeHtmlRenderer.class.getResourceAsStream('cytoscape.js.dag.template.html') + final writer = new StringWriter() + final res = MermaidHtmlRenderer.class.getResourceAsStream('mermaid.dag.template.html') int ch while( (ch=res.read()) != -1 ) { - writer.append(ch as char); + writer.append(ch as char) } - writer.toString(); + writer.toString() } } diff --git a/modules/nextflow/src/main/groovy/nextflow/dag/MermaidRenderer.groovy b/modules/nextflow/src/main/groovy/nextflow/dag/MermaidRenderer.groovy index 76507c4ab4..5538dfed8a 100644 --- a/modules/nextflow/src/main/groovy/nextflow/dag/MermaidRenderer.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/dag/MermaidRenderer.groovy @@ -42,7 +42,16 @@ class MermaidRenderer implements DagRenderer { private int depth = session.config.navigate('dag.depth', -1) as int - private boolean verbose = session.config.navigate('dag.verbose', false) + private String direction = session.config.navigate('dag.direction', 'TB') + + private boolean verbose = session.config.navigate('dag.verbose', false); + + { + if( direction !in ['TB','LR'] ) { + log.warn "Invalid configuration property `dag.direction = '$direction'` - use either: 'TB' (top-bottom) or 'LR' (left-right)" + this.direction = 'TB' + } + } @Override void renderDocument(DAG dag, Path file) { @@ -70,7 +79,7 @@ class MermaidRenderer implements DagRenderer { // render diagram def lines = [] as List - lines << "flowchart TD" + lines << "flowchart ${direction}".toString() // render nodes renderNodeTree(lines, null, nodeTree) @@ -254,23 +263,23 @@ class MermaidRenderer implements DagRenderer { * * @param nodeLookup */ - private Map getNodeTree(Map nodeLookup) { + private Map getNodeTree(Map nodeLookup) { // infer subgraphs of operator nodes final inferredKeys = inferSubgraphKeys(nodeLookup) // construct node tree - def nodeTree = [:] + def nodeTree = [:] as Map for( def node : nodeLookup.values() ) { final vertex = node.vertex // determine the vertex subgraph - def keys = [] + def keys = [] as List if( vertex.type == DAG.Type.PROCESS ) { // extract keys from fully qualified name final result = getSubgraphKeys(vertex.label) - keys = (List)result[0] + keys = (List)result[0] node.label = (String)result[1] } else if( vertex.type == DAG.Type.OPERATOR ) { @@ -306,7 +315,7 @@ class MermaidRenderer implements DagRenderer { * @param nodeLookup */ private Map inferSubgraphKeys(Map nodeLookup) { - def inferredKeys = [:] + def inferredKeys = [:] as Map def queue = nodeLookup .values() .findAll( n -> n.vertex.type == DAG.Type.OPERATOR ) as List @@ -332,7 +341,7 @@ class MermaidRenderer implements DagRenderer { // extract keys from fully qualified process name final keys = process - ? getSubgraphKeys(process.vertex.label)[0] + ? getSubgraphKeys(process.vertex.label)[0] as List : [] // save inferred keys diff --git a/modules/nextflow/src/main/groovy/nextflow/trace/GraphObserver.groovy b/modules/nextflow/src/main/groovy/nextflow/trace/GraphObserver.groovy index a07f611f16..ccadaf9597 100644 --- a/modules/nextflow/src/main/groovy/nextflow/trace/GraphObserver.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/trace/GraphObserver.groovy @@ -21,13 +21,13 @@ import java.nio.file.Path import groovy.transform.PackageScope import groovy.util.logging.Slf4j import nextflow.Session -import nextflow.dag.CytoscapeHtmlRenderer import nextflow.dag.DAG import nextflow.dag.DagRenderer import nextflow.dag.DotRenderer import nextflow.dag.GexfRenderer import nextflow.dag.GraphvizRenderer import nextflow.dag.MermaidRenderer +import nextflow.dag.MermaidHtmlRenderer import nextflow.exception.AbortOperationException import nextflow.file.FileHelper import nextflow.processor.TaskHandler @@ -41,7 +41,7 @@ import nextflow.processor.TaskProcessor @Slf4j class GraphObserver implements TraceObserver { - static public final String DEF_FILE_NAME = "dag-${TraceHelper.launchTimestampFmt()}.dot" + static public final String DEF_FILE_NAME = "dag-${TraceHelper.launchTimestampFmt()}.html" private Path file @@ -61,7 +61,7 @@ class GraphObserver implements TraceObserver { assert file this.file = file this.name = file.baseName - this.format = file.getExtension().toLowerCase() ?: 'dot' + this.format = file.getExtension().toLowerCase() ?: 'html' } @Override @@ -91,7 +91,7 @@ class GraphObserver implements TraceObserver { new DotRenderer(name) else if( format == 'html' ) - new CytoscapeHtmlRenderer() + new MermaidHtmlRenderer() else if( format == 'gexf' ) new GexfRenderer(name) diff --git a/modules/nextflow/src/main/resources/nextflow/dag/cytoscape.js.dag.template.html b/modules/nextflow/src/main/resources/nextflow/dag/cytoscape.js.dag.template.html deleted file mode 100644 index 15258529a2..0000000000 --- a/modules/nextflow/src/main/resources/nextflow/dag/cytoscape.js.dag.template.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - Nextflow Cytoscape.js with Dagre - - - - - - - - - - - -

Nextflow Cytoscape.js with Dagre

-
- - - diff --git a/modules/nextflow/src/main/resources/nextflow/dag/mermaid.dag.template.html b/modules/nextflow/src/main/resources/nextflow/dag/mermaid.dag.template.html new file mode 100644 index 0000000000..2c2a22cc7b --- /dev/null +++ b/modules/nextflow/src/main/resources/nextflow/dag/mermaid.dag.template.html @@ -0,0 +1,29 @@ + + + + + + +
+REPLACE_WITH_NETWORK_DATA
+
+ + + diff --git a/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy b/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy index 420fe7b54a..de690f0d40 100644 --- a/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy @@ -941,7 +941,7 @@ class ConfigBuilderTest extends Specification { then: config.dag instanceof Map config.dag.enabled - config.dag.file == 'dag-20221001.dot' + config.dag.file == 'dag-20221001.html' } def 'should set session weblog options' () { diff --git a/modules/nextflow/src/test/groovy/nextflow/dag/MermaidRendererTest.groovy b/modules/nextflow/src/test/groovy/nextflow/dag/MermaidRendererTest.groovy index 0c61713b77..57821b8c55 100644 --- a/modules/nextflow/src/test/groovy/nextflow/dag/MermaidRendererTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/dag/MermaidRendererTest.groovy @@ -46,7 +46,7 @@ class MermaidRendererTest extends Specification { then: file.text == ''' - flowchart TD + flowchart TB subgraph " " v0[" "] end diff --git a/modules/nextflow/src/test/groovy/nextflow/trace/GraphObserverTest.groovy b/modules/nextflow/src/test/groovy/nextflow/trace/GraphObserverTest.groovy index f3d13bc507..7f9fed32be 100644 --- a/modules/nextflow/src/test/groovy/nextflow/trace/GraphObserverTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/trace/GraphObserverTest.groovy @@ -20,7 +20,7 @@ import java.nio.file.Paths import groovyx.gpars.dataflow.DataflowQueue import nextflow.Session -import nextflow.dag.CytoscapeHtmlRenderer +import nextflow.dag.MermaidHtmlRenderer import nextflow.dag.DAG import nextflow.dag.DotRenderer import nextflow.dag.GraphvizRenderer @@ -112,16 +112,17 @@ class GraphObserverTest extends Specification { then: def result = file.text - // is html result.contains('') result.contains('') - - // contains some of the expected json - result.contains("label: 'Source'") - result.contains("label: 'Process 1'") - result.contains("label: 'Filter'") - result.contains("label: 'Process 2'") + // is mermaid + result.contains('flowchart') + // contains expected nodes + result.contains('Source') + result.contains('Process 1') + result.contains('Process 2') + // contains at least one edge + result.contains('-->') cleanup: file.delete() @@ -180,7 +181,7 @@ class GraphObserverTest extends Specification { file.delete() } - def 'should output a dot file when no extension is specified' () { + def 'should output an html file when no extension is specified' () { given: def folder = Files.createTempDirectory('test') def file = folder.resolve('nope') @@ -192,15 +193,17 @@ class GraphObserverTest extends Specification { then: def result = file.text - // is dot - result.contains("digraph \"${file.baseName}\" {") + // is html + result.contains('') + result.contains('') + // is mermaid + result.contains('flowchart') // contains expected nodes - result.contains('label="Source"') - result.contains('label="Process 1"') - result.contains('label="Filter"') - result.contains('label="Process 2"') + result.contains('Source') + result.contains('Process 1') + result.contains('Process 2') // contains at least one edge - result.contains('->') + result.contains('-->') cleanup: folder.deleteDir() @@ -223,7 +226,7 @@ class GraphObserverTest extends Specification { then: observer.name == 'TheGraph' observer.format == 'html' - observer.createRender() instanceof CytoscapeHtmlRenderer + observer.createRender() instanceof MermaidHtmlRenderer when: observer = new GraphObserver(Paths.get('/path/to/TheGraph.mmd')) @@ -243,7 +246,7 @@ class GraphObserverTest extends Specification { observer = new GraphObserver(Paths.get('/path/to/anonymous')) then: observer.name == 'anonymous' - observer.format == 'dot' - observer.createRender() instanceof DotRenderer + observer.format == 'html' + observer.createRender() instanceof MermaidHtmlRenderer } } diff --git a/tests/checks/hello.nf/.checks b/tests/checks/hello.nf/.checks index e0126df30d..64ff280dd7 100644 --- a/tests/checks/hello.nf/.checks +++ b/tests/checks/hello.nf/.checks @@ -28,4 +28,4 @@ $NXF_RUN -with-report -with-timeline -with-trace -with-dag | tee stdout [ -s report-*.html ] || false [ -s timeline-*.html ] || false [ -s trace-*.txt ] || false -[ -s dag-*.dot ] || false +[ -s dag-*.html ] || false