From 5161b0ec48c61cda9cc9be118b47194376786af4 Mon Sep 17 00:00:00 2001 From: JohT <7671054+JohT@users.noreply.github.com> Date: Sat, 4 Jan 2025 08:12:17 +0100 Subject: [PATCH 1/7] Rename build artifact archives to distinguish between Java and Typescript analysis. --- .github/workflows/java-code-analysis.yml | 6 +++--- .github/workflows/typescript-code-analysis.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/java-code-analysis.yml b/.github/workflows/java-code-analysis.yml index 96de3e8c8..2b810f4f6 100644 --- a/.github/workflows/java-code-analysis.yml +++ b/.github/workflows/java-code-analysis.yml @@ -142,7 +142,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: code-analysis-logs-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} + name: java-code-analysis-logs-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} path: | ./temp/**/runtime/* ./temp/**/reports/* @@ -153,7 +153,7 @@ jobs: if: success() uses: actions/upload-artifact@v4 with: - name: code-report-results-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} + name: java-code-analysis-results-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} path: ./results if-no-files-found: error retention-days: 5 @@ -164,7 +164,7 @@ jobs: #- name: Archive exported database # uses: actions/upload-artifact@v3 # with: - # name: code-report-database-export-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} + # name: java-code-analysis-database-export-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} # path: ./temp/**/import # if-no-files-found: error # retention-days: 5 diff --git a/.github/workflows/typescript-code-analysis.yml b/.github/workflows/typescript-code-analysis.yml index 1e38d989a..75a56ec4d 100644 --- a/.github/workflows/typescript-code-analysis.yml +++ b/.github/workflows/typescript-code-analysis.yml @@ -156,7 +156,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: code-analysis-logs-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} + name: typescript-code-analysis-logs-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} path: | ./temp/**/runtime/* ./temp/**/reports/* @@ -167,7 +167,7 @@ jobs: if: success() uses: actions/upload-artifact@v4 with: - name: code-report-results-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} + name: typescript-code-analysis-results-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} path: ./results if-no-files-found: error retention-days: 5 @@ -178,7 +178,7 @@ jobs: #- name: Archive exported database # uses: actions/upload-artifact@v3 # with: - # name: code-report-database-export-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} + # name: typescript-code-analysis-database-export-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} # path: ./temp/**/import # if-no-files-found: error # retention-days: 5 From 11d0bf37d5ae6073b46b573b00204e80138444b7 Mon Sep 17 00:00:00 2001 From: JohT <7671054+JohT@users.noreply.github.com> Date: Sat, 4 Jan 2025 12:24:36 +0100 Subject: [PATCH 2/7] Reduce archived build artifacts to only contain the current reports --- .github/workflows/java-code-analysis.yml | 2 +- .github/workflows/typescript-code-analysis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/java-code-analysis.yml b/.github/workflows/java-code-analysis.yml index 2b810f4f6..3d7853260 100644 --- a/.github/workflows/java-code-analysis.yml +++ b/.github/workflows/java-code-analysis.yml @@ -154,7 +154,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: java-code-analysis-results-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} - path: ./results + path: ./temp/**/reports/* if-no-files-found: error retention-days: 5 diff --git a/.github/workflows/typescript-code-analysis.yml b/.github/workflows/typescript-code-analysis.yml index 75a56ec4d..3b21b8a5f 100644 --- a/.github/workflows/typescript-code-analysis.yml +++ b/.github/workflows/typescript-code-analysis.yml @@ -168,7 +168,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: typescript-code-analysis-results-java-${{ matrix.java }}-python-${{ matrix.python }}-miniforge-${{ matrix.miniforge }} - path: ./results + path: ./temp/**/reports/* if-no-files-found: error retention-days: 5 From f3829053375a9813375006b0be52bd9db1b14906 Mon Sep 17 00:00:00 2001 From: JohT <7671054+JohT@users.noreply.github.com> Date: Sun, 5 Jan 2025 08:25:58 +0100 Subject: [PATCH 3/7] Add Cypher query example to set parameters for topological sort algorithms. --- cypher/Topological_Sort/Set_Parameters.cypher | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 cypher/Topological_Sort/Set_Parameters.cypher diff --git a/cypher/Topological_Sort/Set_Parameters.cypher b/cypher/Topological_Sort/Set_Parameters.cypher new file mode 100644 index 000000000..8efaa345d --- /dev/null +++ b/cypher/Topological_Sort/Set_Parameters.cypher @@ -0,0 +1,7 @@ +// Example on how to set the parameters for topological sort in this case for Java Artifacts and Node Similarity + +:params { + "dependencies_projection": "artifact-topology", + "dependencies_projection_node": "Artifact", + "dependencies_projection_weight_property": "weight" +} \ No newline at end of file From 806d6ed8d1a7b1fcc5f2e59da6819464e6da8a36 Mon Sep 17 00:00:00 2001 From: JohT <7671054+JohT@users.noreply.github.com> Date: Wed, 1 Jan 2025 21:07:19 +0100 Subject: [PATCH 4/7] Add scripts to visualize query results with GraphViz --- README.md | 2 + .../convertQueryResultCsvToGraphVizDotFile.sh | 119 ++++++++++++++++++ .../templates/default.template.gv | 17 +++ .../visualization/visualizeQueryResults.sh | 67 ++++++++++ 4 files changed, 205 insertions(+) create mode 100755 scripts/visualization/convertQueryResultCsvToGraphVizDotFile.sh create mode 100644 scripts/visualization/templates/default.template.gv create mode 100755 scripts/visualization/visualizeQueryResults.sh diff --git a/README.md b/README.md index 68a6c7b0f..f771fa364 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ The [Code Structure Analysis Pipeline](./.github/workflows/java-code-analysis.ym - [openTSNE](https://github.com/pavlin-policar/openTSNE) - [wordcloud](https://github.com/amueller/word_cloud) - [Graph Visualization](./graph-visualization/README.md) uses [node.js](https://nodejs.org/de) and the dependencies listed in [package.json](./graph-visualization/package.json). +- [HPCC-Systems (High Performance Computing Cluster) Web-Assembly (JavaScript)](https://github.com/hpcc-systems/hpcc-js-wasm) containing a wrapper for GraphViz to visualize graph structures. +- [GraphViz](https://gitlab.com/graphviz/graphviz) for CLI Graph Visualization - [Check links in markdown documentation (GitHub workflow)](./.github/workflows/check-links-in-documentation.yml) uses [markdown-link-check](https://github.com/tcort/markdown-link-check). **Big shout-out** 📣 to all the creators and contributors of these great libraries 👍. Projects like this wouldn't be possible without them. Feel free to [create an issue](https://github.com/JohT/code-graph-analysis-pipeline/issues/new/choose) if something is missing or wrong in the list. diff --git a/scripts/visualization/convertQueryResultCsvToGraphVizDotFile.sh b/scripts/visualization/convertQueryResultCsvToGraphVizDotFile.sh new file mode 100755 index 000000000..8955465f8 --- /dev/null +++ b/scripts/visualization/convertQueryResultCsvToGraphVizDotFile.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash + +# Converts a Cypher query result in CSV format to a GraphViz DOT (https://graphviz.org/doc/info/lang.html) file for Visualization including layout templates. +# +# The template file is used to define the layout of the graph. +# It needs to contain the markers //Begin-Template and //End-Template to indicate the actual template content. +# If no template is specified, "templates/default.template.gv" is used. + +# Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands) +set -o errexit -o pipefail + +## Get this "scripts/visualization" directory if not already set +# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution. +# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes. +# This way non-standard tools like readlink aren't needed. +VISUALIZATION_SCRIPTS_DIR=${VISUALIZATION_SCRIPTS_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} # Repository directory containing the shell scripts for visualization +echo "convertQueryResultCsvToGraphVizDotFile: VISUALIZATION_SCRIPTS_DIR=${VISUALIZATION_SCRIPTS_DIR}" + +# Internal constants +DEFAULT_TEMPLATE_FILE="templates/default.template.gv" + +# Function to display script usage +usage() { + echo "Usage: $0 --filename path/to/query/result/file.csv [--name name-of-the-graph (default=name of the file)] [--template path/to/the/template/file (default=${DEFAULT_TEMPLATE_FILE})]" + exit 1 +} + +# Default values +inputFilename="" +graphName="" +templateFile="" + +# Parse command line arguments +while [ $# -gt 0 ]; do + key="$1" + case $key in + --filename) + inputFilename="$2" + shift + ;; + --name) + graphName="$2" + shift + ;; + --template) + templateFile="$2" + shift + ;; + *) + echo "convertQueryResultCsvToGraphVizDotFile: Error: Unknown option: ${key}" + usage + ;; + esac + shift +done + +if [ -z "${inputFilename}" ]; then + echo "convertQueryResultCsvToGraphVizDotFile: Error: Missing required option: --filename" + echo "${USAGE}" + exit 1 +fi + +if [ ! -f "${inputFilename}" ]; then + echo "convertQueryResultCsvToGraphVizDotFile: Error: CSV file not found: ${inputFilename}" + echo "${USAGE}" + exit 1 +fi + +number_of_input_file_lines=$(wc -l < "${inputFilename}" | awk '{print $1}') +if [ "${number_of_input_file_lines}" -le 1 ]; then + echo "convertQueryResultCsvToGraphVizDotFile: Info: Input file is empty. Skipping *.dot and *.svg file generation." + return 0 +fi + +if [ -z "${graphName}" ]; then + graphName=$(basename -- "${inputFilename}") + graphName="${graphName%.*}" + echo "convertQueryResultCsvToGraphVizDotFile: Info: Using default graph name: ${graphName}" +fi + +# Replace all dashes in the graphName by underscores +graphName=${graphName//-/_} + +if [ -z "${templateFile}" ]; then + templateFile="${VISUALIZATION_SCRIPTS_DIR}/${DEFAULT_TEMPLATE_FILE}" + echo "convertQueryResultCsvToGraphVizDotFile: Info: Using default template file: ${templateFile}" +fi + +if [ ! -f "${templateFile}" ]; then + echo "convertQueryResultCsvToGraphVizDotFile: Error: Template file not found: ${templateFile}" + echo "${USAGE}" + exit 1 +fi + +templateFileName=$(basename -- "${templateFile}") +inputFilePath=$(dirname "${inputFilename}") +templateFileNameWithoutExtension="${templateFileName%.*}" +outputFilename="${inputFilePath}/${graphName}.gv" + +{ + # Add a comment to the beginning of the file to indicate that it was generated by this script + echo "// This GraphViz dot file was generated by the script convertQueryResultCsvToGraphVizDotFile.sh with ${templateFileNameWithoutExtension}" + echo "" + # Start the graph definition with the graph name + echo "strict digraph ${graphName} {" + # Extract the template content from the template file and remove the begin and end markers + sed -n '/\/\/Begin-Template/,/\/\/End-Template/{//!p;}' "${templateFile}" + # Remove the first (header) line of the CSV file, remove the enclosing double quotes and replace the escaped double quotes by double quotes + awk -F ',' 'NR>1 {print "\t" $1}' "${inputFilename}" \ + | sed 's/^\t\"\"\"/\t"/' \ + | sed 's/^\t\"\\\"\"/\t"/' \ + | sed 's/\\\"\"/"/g' \ + | sed 's/\"\"/"/g' \ + | sed 's/\"$//' + # End the graph definition + echo "}" +} > "${outputFilename}" + + diff --git a/scripts/visualization/templates/default.template.gv b/scripts/visualization/templates/default.template.gv new file mode 100644 index 000000000..84a53a086 --- /dev/null +++ b/scripts/visualization/templates/default.template.gv @@ -0,0 +1,17 @@ +// This is a GraphViz dot template file for the visualization of a graph in lightblue color scheme. +// The main part of the template is marked by the comments "Begin-Template" and "End-Template". +// It also contains a simple example graph. +// +strict digraph lightblue_template { + //Begin-Template + fontname = "Helvetica,Arial,sans-serif"; + node [fontname = "Helvetica,Arial,sans-serif";]; + edge [fontname = "Helvetica,Arial,sans-serif"; fontsize = 10;]; + node [style = filled; fillcolor = "0.560 0.400 0.999";]; + node [color = "0.560 0.900 0.700";]; + edge [color = "0.560 0.900 0.700";]; + //End-Template + "A" -> "B" [penwidth = 1.0; label = 1;]; + "A" -> "C" [penwidth = 3.0; label = 4;]; + "B" -> "V" [penwidth = 2.0; label = 2;]; +} \ No newline at end of file diff --git a/scripts/visualization/visualizeQueryResults.sh b/scripts/visualization/visualizeQueryResults.sh new file mode 100755 index 000000000..bde45f820 --- /dev/null +++ b/scripts/visualization/visualizeQueryResults.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# Visualizes the Cypher query result (CSV format) using GraphViz and outputs it as SVG image. +# +# Requires convertQueryResultCsvToGraphVizDotFile.sh +# +# Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands) +set -o errexit -o pipefail + +## Get this "scripts/visualization" directory if not already set +# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution. +# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes. +# This way non-standard tools like readlink aren't needed. +VISUALIZATION_SCRIPTS_DIR=${VISUALIZATION_SCRIPTS_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} # Repository directory containing the shell scripts for visualization +echo "visualizeQueryResults: VISUALIZATION_SCRIPTS_DIR=${VISUALIZATION_SCRIPTS_DIR}" + +# Read the first unnamed input argument containing the version of the project +inputCsvFileName="" +case "${1}" in + "--"*) ;; # Skipping named command line options to forward them later to the "analyze" command + *) + inputCsvFileName="${1}" + shift || true + ;; +esac + +if [ -z "${inputCsvFileName}" ]; then + echo "visualizeQueryResults: Error: Please specify the CSV query result file as input parameter." + exit 1 +fi + +if [ ! -f "${inputCsvFileName}" ]; then + echo "visualizeQueryResults: Error: CSV file not found: ${inputCsvFileName}" + exit 1 +fi + +number_of_input_file_lines=$(wc -l < "${inputCsvFileName}" | awk '{print $1}') +if [ "${number_of_input_file_lines}" -le 1 ]; then + echo "visualizeQueryResults: Info: Input file is empty. Skipping *.dot and *.svg file generation." + return 0 +fi + +echo "visualizeQueryResults: Info: CSV input file: ${inputCsvFileName}" + +graphName=$(basename -- "${inputCsvFileName}") +graphName="${graphName%.*}" # Remove file extension +graphName=${graphName//-/_} # Replace all dashes in the graphName by underscores +inputCsvFilePath=$(dirname "${inputCsvFileName}") + +echo "visualizeQueryResults: Generating Visualization files ${inputCsvFilePath}/${graphName}.* ..." +source "${VISUALIZATION_SCRIPTS_DIR}/convertQueryResultCsvToGraphVizDotFile.sh" "--filename" "${inputCsvFileName}" "${@}" + +if command -v "dot" &> /dev/null ; then + echo "visualizeQueryResults: Info: Using already installed GraphViz." + dot -T svg "${inputCsvFilePath}/${graphName}.gv" > "${inputCsvFilePath}/${graphName}.svg" + return 0 +fi + +if ! command -v "npx" &> /dev/null ; then + echo "visualizeQueryResults: Error: Command npx (to run npm locally) not found. It's needed for Graph visualization with GraphViz." >&2 + exit 1 +fi + +# Run GraphViz command line interface (CLI) wrapped utilizing WASM (WebAssembly) +# to convert the DOT file to SVG operating system independently. +echo "visualizeQueryResults: Info: Using npx to run GraphViz CLI (Web Assembly Wrapper) for SVG generation." +npx --yes @hpcc-js/wasm-graphviz-cli@1.2.6 -T svg "${inputCsvFilePath}/${graphName}.gv" > "${inputCsvFilePath}/${graphName}.svg" \ No newline at end of file From acc65386cc11ecdb7ddc8ec4bfccc4bfd01000ab Mon Sep 17 00:00:00 2001 From: JohT <7671054+JohT@users.noreply.github.com> Date: Wed, 1 Jan 2025 21:08:04 +0100 Subject: [PATCH 5/7] Visualize internal dependencies with GraphViz --- ..._Artifact_build_levels_for_graphviz.cypher | 31 ++++++++++ ...pt_Module_build_levels_for_graphviz.cypher | 31 ++++++++++ .../InternalDependenciesVisualization.sh | 56 +++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 cypher/Internal_Dependencies/Java_Artifact_build_levels_for_graphviz.cypher create mode 100644 cypher/Internal_Dependencies/Typescript_Module_build_levels_for_graphviz.cypher create mode 100755 scripts/reports/InternalDependenciesVisualization.sh diff --git a/cypher/Internal_Dependencies/Java_Artifact_build_levels_for_graphviz.cypher b/cypher/Internal_Dependencies/Java_Artifact_build_levels_for_graphviz.cypher new file mode 100644 index 000000000..83c802b3b --- /dev/null +++ b/cypher/Internal_Dependencies/Java_Artifact_build_levels_for_graphviz.cypher @@ -0,0 +1,31 @@ +// List of all Java Artifacts and their dependencies with build levels for GraphViz Visualization + + MATCH (sourceForStatistics:Java:Artifact)-[dependencyForStatistics:DEPENDS_ON]->(targetForStatistics:Java:Artifact) + WHERE sourceForStatistics.maxDistanceFromSource IS NOT NULL + AND targetForStatistics.maxDistanceFromSource IS NOT NULL + WITH min(dependencyForStatistics.weight) AS minWeight + ,max(dependencyForStatistics.weight) AS maxWeight + ,max(targetForStatistics.maxDistanceFromSource) AS maxLevel + MATCH (source:Java:Artifact)-[dependency:DEPENDS_ON]->(target:Java:Artifact) + WHERE source.maxDistanceFromSource IS NOT NULL + AND target.maxDistanceFromSource IS NOT NULL + WITH *, toFloat(dependency.weight - minWeight) / toFloat(maxWeight - minWeight) AS normalizedWeight + WITH *, round((normalizedWeight * 5) + 1, 2) AS penWidth + WITH *, source.name + "\\n(level " + coalesce(source.maxDistanceFromSource + "/" + maxLevel, "?") + ")" AS fullSourceName + WITH *, target.name + "\\n(level " + coalesce(target.maxDistanceFromSource + "/" + maxLevel, "?") + ")" AS fullTargetName + WITH *, "\" -> \"" + fullTargetName + + "\" [label = " + dependency.weight + ";" + + " penwidth = " + penWidth + ";" + + " ];" AS graphVizDotNotationEdge + WITH *, "\"" + fullSourceName + coalesce(graphVizDotNotationEdge, "\" [];") AS graphVizDotNotationLine + ORDER BY dependency.weight DESC, target.maxDistanceFromSource DESC +RETURN graphVizDotNotationLine + //Debugging + //,source.name AS sourceName + //,target.name AS targetName + //,penWidth + //,normalizedWeight + //,dependency.weight AS weight + //,minWeight + //,maxWeight +LIMIT 440 \ No newline at end of file diff --git a/cypher/Internal_Dependencies/Typescript_Module_build_levels_for_graphviz.cypher b/cypher/Internal_Dependencies/Typescript_Module_build_levels_for_graphviz.cypher new file mode 100644 index 000000000..667b7e883 --- /dev/null +++ b/cypher/Internal_Dependencies/Typescript_Module_build_levels_for_graphviz.cypher @@ -0,0 +1,31 @@ +// List of all Typescript modules and their dependencies with build levels for GraphViz Visualization + + MATCH (sourceForStatistics:TS:Module)-[dependencyForStatistics:DEPENDS_ON]->(targetForStatistics:TS:Module) + WHERE sourceForStatistics.maxDistanceFromSource IS NOT NULL + AND targetForStatistics.maxDistanceFromSource IS NOT NULL + WITH min(dependencyForStatistics.weight) AS minWeight + ,max(dependencyForStatistics.weight) AS maxWeight + ,max(targetForStatistics.maxDistanceFromSource) AS maxLevel +MATCH (source:TS:Module)-[dependency:DEPENDS_ON]->(target:TS:Module) + WHERE source.maxDistanceFromSource IS NOT NULL + AND target.maxDistanceFromSource IS NOT NULL + WITH *, toFloat(dependency.cardinality - minWeight) / toFloat(maxWeight - minWeight) AS normalizedWeight + WITH *, round((normalizedWeight * 5) + 1, 2) AS penWidth + WITH *, source.rootProjectName + "\\n" + source.name + "\\n(level " + coalesce(source.maxDistanceFromSource + "/" + maxLevel, "?") + ")" AS fullSourceName + WITH *, target.rootProjectName + "\\n" + target.name + "\\n(level " + coalesce(target.maxDistanceFromSource + "/" + maxLevel, "?") + ")" AS fullTargetName + WITH *, "\" -> \"" + fullTargetName + + "\" [label = " + dependency.cardinality + ";" + + " penwidth = " + penWidth + ";" + + " ];" AS graphVizDotNotationEdge + WITH *, "\"" + fullSourceName + coalesce(graphVizDotNotationEdge, "\" [];") AS graphVizDotNotationLine + ORDER BY dependency.weight DESC, target.maxDistanceFromSource DESC +RETURN graphVizDotNotationLine + //Debugging + //,source.name AS sourceName + //,target.name AS targetName + //,penWidth + //,normalizedWeight + //,dependency.cardinality AS weight + //,minWeight + //,maxWeight +LIMIT 440 \ No newline at end of file diff --git a/scripts/reports/InternalDependenciesVisualization.sh b/scripts/reports/InternalDependenciesVisualization.sh new file mode 100755 index 000000000..a6a358d49 --- /dev/null +++ b/scripts/reports/InternalDependenciesVisualization.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# Executes selected "Internal_Dependencies" Cypher queries for GraphViz visualization. +# Visualizes dependencies across artifacts and their build levels (topologically sorted). +# It requires an already running Neo4j graph database with already scanned and analyzed artifacts. +# The reports (csv, dot and svg files) will be written into the sub directory reports/internal-dependencies-visualization. + +# Requires executeQueryFunctions.sh, visualizeQueryResults.sh, cleanupAfterReportGeneration.sh + +# Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands) +set -o errexit -o pipefail + +# Overrideable Constants (defaults also defined in sub scripts) +REPORTS_DIRECTORY=${REPORTS_DIRECTORY:-"reports"} + +## Get this "scripts/reports" directory if not already set +# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution. +# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes. +# This way non-standard tools like readlink aren't needed. +REPORTS_SCRIPT_DIR=${REPORTS_SCRIPT_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} +echo "InternalDependenciesVisualization: REPORTS_SCRIPT_DIR=${REPORTS_SCRIPT_DIR}" + +# Get the "scripts" directory by taking the path of this script and going one directory up. +SCRIPTS_DIR=${SCRIPTS_DIR:-"${REPORTS_SCRIPT_DIR}/.."} # Repository directory containing the shell scripts +echo "InternalDependenciesVisualization SCRIPTS_DIR=${SCRIPTS_DIR}" + +# Get the "scripts/visualization" directory. +VISUALIZATION_SCRIPTS_DIR=${VISUALIZATION_SCRIPTS_DIR:-"${SCRIPTS_DIR}/visualization"} # Repository directory containing the shell scripts for visualization +echo "InternalDependenciesVisualization VISUALIZATION_SCRIPTS_DIR=${VISUALIZATION_SCRIPTS_DIR}" + +# Get the "cypher" directory by taking the path of this script and going two directory up and then to "cypher". +CYPHER_DIR=${CYPHER_DIR:-"${REPORTS_SCRIPT_DIR}/../../cypher"} +echo "InternalDependenciesVisualization CYPHER_DIR=${CYPHER_DIR}" + +INTERNAL_DEPENDENCIES_CYPHER_DIR="${CYPHER_DIR}/Internal_Dependencies" + +# Define functions to execute cypher queries from within a given file +source "${SCRIPTS_DIR}/executeQueryFunctions.sh" + +# Create report directory +REPORT_NAME="internal-dependencies-visualization" +FULL_REPORT_DIRECTORY="${REPORTS_DIRECTORY}/${REPORT_NAME}" +mkdir -p "${FULL_REPORT_DIRECTORY}" + +# Java Artifacts: Dependencies Visualization +reportName="${FULL_REPORT_DIRECTORY}/JavaArtifactBuildLevels" +execute_cypher "${INTERNAL_DEPENDENCIES_CYPHER_DIR}/Java_Artifact_build_levels_for_graphviz.cypher" > "${reportName}.csv" +source "${VISUALIZATION_SCRIPTS_DIR}/visualizeQueryResults.sh" "${reportName}.csv" + +# TypeScript Modules: Dependencies Visualization +reportName="${FULL_REPORT_DIRECTORY}/TypeScriptModuleBuildLevels" +execute_cypher "${INTERNAL_DEPENDENCIES_CYPHER_DIR}/Typescript_Module_build_levels_for_graphviz.cypher" > "${reportName}.csv" +source "${VISUALIZATION_SCRIPTS_DIR}/visualizeQueryResults.sh" "${reportName}.csv" + +# Clean-up after report generation. Empty reports will be deleted. +source "${SCRIPTS_DIR}/cleanupAfterReportGeneration.sh" "${FULL_REPORT_DIRECTORY}" \ No newline at end of file From cd96939c919224a15784bd1c825d55641115e85f Mon Sep 17 00:00:00 2001 From: JohT <7671054+JohT@users.noreply.github.com> Date: Sat, 4 Jan 2025 22:16:13 +0100 Subject: [PATCH 6/7] Visualize longest paths with GraphViz --- ...est_paths_contributors_for_graphviz.cypher | 77 +++++++++++++++++ ...inding_6_Longest_paths_for_graphviz.cypher | 35 ++++++++ cypher/Path_Finding/Set_Parameters.cypher | 2 +- scripts/reports/PathFindingVisualization.sh | 84 +++++++++++++++++++ 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 cypher/Path_Finding/Path_Finding_6_Longest_paths_contributors_for_graphviz.cypher create mode 100644 cypher/Path_Finding/Path_Finding_6_Longest_paths_for_graphviz.cypher create mode 100755 scripts/reports/PathFindingVisualization.sh diff --git a/cypher/Path_Finding/Path_Finding_6_Longest_paths_contributors_for_graphviz.cypher b/cypher/Path_Finding/Path_Finding_6_Longest_paths_contributors_for_graphviz.cypher new file mode 100644 index 000000000..81765164a --- /dev/null +++ b/cypher/Path_Finding/Path_Finding_6_Longest_paths_contributors_for_graphviz.cypher @@ -0,0 +1,77 @@ +// Path Finding - Longest path - Stream - List all dependencies for nodes contributing to longest paths and highlight those paths in the Visualization with GraphViz. + +// Gather global statistics about dependency weights and levels for normalization and node details + MATCH (sourceNodeForStatistics)-[dependencyForStatistics:DEPENDS_ON]->(targetNodeForStatistics) + WHERE $dependencies_projection_node IN LABELS(sourceNodeForStatistics) + AND $dependencies_projection_node IN LABELS(targetNodeForStatistics) + WITH min(dependencyForStatistics[$dependencies_projection_weight_property]) AS minWeight + ,max(dependencyForStatistics[$dependencies_projection_weight_property]) AS maxWeight + ,max(targetNodeForStatistics.maxDistanceFromSource) AS maxLevel + WITH *, 1.0 / toFloat(maxWeight - minWeight) AS weightNormalizationFactor + WITH { minWeight: minWeight, maxLevel: maxLevel, weightNormalizationFactor: weightNormalizationFactor } AS statistics +// -> Main call to execute "longest path" algorithm + CALL gds.dag.longestPath.stream($dependencies_projection + '-cleaned') + YIELD index, totalCost, path + WITH * +// Sort longest paths by their length descending and - if equal - by their index ascending + ORDER BY totalCost DESC, index ASC +// Only take the top 50 longest paths as a compromise between performance and visualization content + LIMIT 50 +// Collect all results of the longest path search as well as all nodes of the longest paths + WITH statistics + ,collect({index: index, distance: toInteger(totalCost), path: path}) AS longestPaths + ,collect(nodes(path)) AS allLongestPathNodes +// Flatten and deduplicate the list of all nodes that contribute to at least one longest path + UNWIND allLongestPathNodes AS longestPathNodes + UNWIND longestPathNodes AS longestPathNode + WITH statistics + ,longestPaths + ,collect(DISTINCT longestPathNode) AS allDistinctLongestPathNodes +// Iterate over all longest paths + UNWIND longestPaths AS longestPath + WITH statistics + ,longestPaths, allDistinctLongestPathNodes + ,[ singleRelationship IN relationships(longestPath.path) | [startNode(singleRelationship), endNode(singleRelationship)] ] AS allLongestPathStartAndEndNodeTuples + ,[ singleRelationship IN relationships(longestPaths[0].path) | [startNode(singleRelationship), endNode(singleRelationship)] ] AS longestPathStartAndEndNodeTuples + ,longestPath.index AS index + ,longestPath.distance AS distance +// -> Main query of all dependencies of nodes contributing to the longest paths + MATCH (source)-[dependency:DEPENDS_ON]->(target) + WHERE $dependencies_projection_node IN labels(source) + AND $dependencies_projection_node IN labels(target) + // Dependent nodes need to be part of at least one longest paths + AND (source IN allDistinctLongestPathNodes AND target IN allDistinctLongestPathNodes) + WITH statistics.maxLevel AS maxLevel + ,statistics.minWeight AS minWeight + ,statistics.weightNormalizationFactor AS weightNormalizationFactor + ,count(index) AS numberOfLongestPathsPassing + ,max(distance) AS lengthOfLongestPathPassing + ,dependency + ,source + ,target + // If there is at least one longest path passing through the dependency then "contributesToALongestPath" is true + ,([source, target] IN allLongestPathStartAndEndNodeTuples) AS contributesToALongestPath + ,([source, target] IN longestPathStartAndEndNodeTuples) AS isPartOfLongestPath + WITH *, dependency[$dependencies_projection_weight_property] AS weight + WITH *, toFloat(weight - minWeight) * weightNormalizationFactor AS normalizedWeight + WITH *, round((normalizedWeight * 5) + 1, 2) AS penWidth + WITH *, source.name + "\\n(level " + source.maxDistanceFromSource + "/" + maxLevel + ")" AS startNodeTitle + WITH *, target.name + "\\n(level " + target.maxDistanceFromSource + "/" + maxLevel + ")" AS endNodeTitle + // The longest path will be highlighted in red. + WITH *, CASE WHEN isPartOfLongestPath THEN "; color=\"red\"" + // Dependencies contributing to the longest path will be highlighted in dark orange. + WHEN contributesToALongestPath THEN "; color=\"darkorange\"" + ELSE "" END AS edgeColor +// Prepare the GraphViz edge attributes for the visualization + WITH *, "[label=" + weight + "; penwidth=" + penWidth + edgeColor + "; ];" AS graphVizEdgeAttributes +// Assemble the final GraphViz DOT notation line for the edge representing the current dependency + WITH *, "\"" + startNodeTitle + "\" -> \"" + endNodeTitle + "\" " + graphVizEdgeAttributes AS graphVizDotNotationLine +RETURN DISTINCT graphVizDotNotationLine +// Debugging +// ,source.name +// ,target.name +// ,numberOfLongestPathsPassing +// ,lengthOfLongestPathPassing +// ,contributesToALongestPath +// ,isPartOfLongestPath + LIMIT 440 \ No newline at end of file diff --git a/cypher/Path_Finding/Path_Finding_6_Longest_paths_for_graphviz.cypher b/cypher/Path_Finding/Path_Finding_6_Longest_paths_for_graphviz.cypher new file mode 100644 index 000000000..4f1152733 --- /dev/null +++ b/cypher/Path_Finding/Path_Finding_6_Longest_paths_for_graphviz.cypher @@ -0,0 +1,35 @@ +// Path Finding - Longest path - Stream - Find the top 100 dependencies contributing to the longest paths for Visualization with GraphViz + + MATCH (sourceNodeForStatistics)-[dependencyForStatistics:DEPENDS_ON]->(targetNodeForStatistics) + WHERE $dependencies_projection_node IN LABELS(sourceNodeForStatistics) + AND $dependencies_projection_node IN LABELS(targetNodeForStatistics) + WITH min(dependencyForStatistics[$dependencies_projection_weight_property]) AS minWeight + ,max(dependencyForStatistics[$dependencies_projection_weight_property]) AS maxWeight + ,max(targetNodeForStatistics.maxDistanceFromSource) AS maxLevel + WITH *, 1.0 / toFloat(maxWeight - minWeight) AS weightNormalizationFactor + CALL gds.dag.longestPath.stream($dependencies_projection + '-cleaned') + YIELD index, totalCost, path + WITH *, toInteger(totalCost) AS distance + ORDER BY distance DESC, index ASC + UNWIND relationships(path) AS pathRelationship + WITH * + ,startNode(pathRelationship) AS startNode + ,endNode(pathRelationship) AS endNode + MATCH (startNode)-[dependency:DEPENDS_ON]->(endNode) + WITH *, dependency[$dependencies_projection_weight_property] AS weight + WITH *, toFloat(weight - minWeight) * weightNormalizationFactor AS normalizedWeight + WITH *, round((normalizedWeight * 5) + 1, 2) AS penWidth + WITH *, startNode.name + "\\n(level " + startNode.maxDistanceFromSource + "/" + maxLevel + ")" AS startNodeTitle + WITH *, endNode.name + "\\n(level " + endNode.maxDistanceFromSource + "/" + maxLevel + ")" AS endNodeTitle + WITH *, "[label=" + weight + "; penwidth=" + penWidth + "; ];" AS graphVizEdgeAttributes + WITH *, "\"" + startNodeTitle + "\" -> \"" + endNodeTitle + "\" " + graphVizEdgeAttributes AS graphVizDotNotationLine +RETURN graphVizDotNotationLine +// Debugging +// RETURN startNode.name AS startNodeName +// ,endNode.name AS endNodeName +// ,dependency[$dependencies_projection_weight_property] AS dependencyWeight +// ,max(distance) AS partOfLongestPathLength +// ,count(DISTINCT index) AS partOfLongestPathCounts +// ,startNode.maxDistanceFromSource AS startNodeLevel +// ,endNode.maxDistanceFromSource AS endNodeLevel +LIMIT 100 \ No newline at end of file diff --git a/cypher/Path_Finding/Set_Parameters.cypher b/cypher/Path_Finding/Set_Parameters.cypher index 6954fb494..55582f533 100644 --- a/cypher/Path_Finding/Set_Parameters.cypher +++ b/cypher/Path_Finding/Set_Parameters.cypher @@ -1,4 +1,4 @@ -// Example on how to set the parameters for centrality in this case for Packages and PageRank +// Example on how to set the parameters for path finding in this case for Packages and PageRank :params { "dependencies_projection": "package-path-finding", diff --git a/scripts/reports/PathFindingVisualization.sh b/scripts/reports/PathFindingVisualization.sh new file mode 100755 index 000000000..6fe72e609 --- /dev/null +++ b/scripts/reports/PathFindingVisualization.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +# Executes selected "Path_Finding" Cypher queries for GraphViz visualization. +# Visualizes Java Artifact and TypeScript Module dependencies with their longest paths. +# It requires an already running Neo4j graph database with already scanned and analyzed artifacts. +# The reports (csv, dot and svg files) will be written into the sub directory reports/path-finding-visualization. + +# Requires executeQueryFunctions.sh, projectionFunctions.sh, visualizeQueryResults.sh, cleanupAfterReportGeneration.sh + +# Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands) +set -o errexit -o pipefail + +# Overrideable Constants (defaults also defined in sub scripts) +REPORTS_DIRECTORY=${REPORTS_DIRECTORY:-"reports"} + +## Get this "scripts/reports" directory if not already set +# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution. +# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes. +# This way non-standard tools like readlink aren't needed. +REPORTS_SCRIPT_DIR=${REPORTS_SCRIPT_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} +echo "PathFindingVisualization: REPORTS_SCRIPT_DIR=${REPORTS_SCRIPT_DIR}" + +# Get the "scripts" directory by taking the path of this script and going one directory up. +SCRIPTS_DIR=${SCRIPTS_DIR:-"${REPORTS_SCRIPT_DIR}/.."} # Repository directory containing the shell scripts +echo "PathFindingVisualization SCRIPTS_DIR=${SCRIPTS_DIR}" + +# Get the "scripts/visualization" directory. +VISUALIZATION_SCRIPTS_DIR=${VISUALIZATION_SCRIPTS_DIR:-"${SCRIPTS_DIR}/visualization"} # Repository directory containing the shell scripts for visualization +echo "PathFindingVisualization VISUALIZATION_SCRIPTS_DIR=${VISUALIZATION_SCRIPTS_DIR}" + +# Get the "cypher" directory by taking the path of this script and going two directory up and then to "cypher". +CYPHER_DIR=${CYPHER_DIR:-"${REPORTS_SCRIPT_DIR}/../../cypher"} +echo "PathFindingVisualization CYPHER_DIR=${CYPHER_DIR}" + +PATH_FINDINGS_CYPHER_DIR="${CYPHER_DIR}/Path_Finding" + +# Define functions to execute cypher queries from within a given file +source "${SCRIPTS_DIR}/executeQueryFunctions.sh" + +# Define functions to create and delete Graph Projections like "createDirectedDependencyProjection" +source "${SCRIPTS_DIR}/projectionFunctions.sh" + +# Create report directory +REPORT_NAME="path-finding-visualization" +FULL_REPORT_DIRECTORY="${REPORTS_DIRECTORY}/${REPORT_NAME}" +mkdir -p "${FULL_REPORT_DIRECTORY}" + +# Java Artifacts: Longest Paths Visualization +ARTIFACT_PROJECTION="dependencies_projection=artifact-path-finding" +ARTIFACT_NODE="dependencies_projection_node=Artifact" +ARTIFACT_WEIGHT="dependencies_projection_weight_property=weight" + +if createDirectedDependencyProjection "${ARTIFACT_PROJECTION}" "${ARTIFACT_NODE}" "${ARTIFACT_WEIGHT}"; then + reportName="JavaArtifactLongestPathsIsolated" + echo "PathFindingVisualization: Creating visualization ${reportName}..." + execute_cypher "${PATH_FINDINGS_CYPHER_DIR}/Path_Finding_6_Longest_paths_for_graphviz.cypher" "${ARTIFACT_PROJECTION}" "${ARTIFACT_NODE}" "${ARTIFACT_WEIGHT}" > "${FULL_REPORT_DIRECTORY}/${reportName}.csv" + source "${VISUALIZATION_SCRIPTS_DIR}/visualizeQueryResults.sh" "${FULL_REPORT_DIRECTORY}/${reportName}.csv" + + reportName="JavaArtifactLongestPaths" + echo "PathFindingVisualization: Creating visualization ${reportName}..." + execute_cypher "${PATH_FINDINGS_CYPHER_DIR}/Path_Finding_6_Longest_paths_contributors_for_graphviz.cypher" "${ARTIFACT_PROJECTION}" "${ARTIFACT_NODE}" "${ARTIFACT_WEIGHT}" > "${FULL_REPORT_DIRECTORY}/${reportName}.csv" + source "${VISUALIZATION_SCRIPTS_DIR}/visualizeQueryResults.sh" "${FULL_REPORT_DIRECTORY}/${reportName}.csv" +fi + +# TypeScript Modules: Longest Paths Visualization +MODULE_LANGUAGE="dependencies_projection_language=Typescript" +MODULE_PROJECTION="dependencies_projection=typescript-module-path-finding" +MODULE_NODE="dependencies_projection_node=Module" +MODULE_WEIGHT="dependencies_projection_weight_property=lowCouplingElement25PercentWeight" + +if createDirectedDependencyProjection "${MODULE_LANGUAGE}" "${MODULE_PROJECTION}" "${MODULE_NODE}" "${MODULE_WEIGHT}"; then + reportName="TypeScriptModuleLongestPathsIsolated" + echo "PathFindingVisualization: Creating visualization ${reportName}..." + execute_cypher "${PATH_FINDINGS_CYPHER_DIR}/Path_Finding_6_Longest_paths_for_graphviz.cypher" "${MODULE_PROJECTION}" "${MODULE_NODE}" "${MODULE_WEIGHT}" > "${FULL_REPORT_DIRECTORY}/${reportName}.csv" + source "${VISUALIZATION_SCRIPTS_DIR}/visualizeQueryResults.sh" "${FULL_REPORT_DIRECTORY}/${reportName}.csv" + + reportName="TypeScriptModuleLongestPaths" + echo "PathFindingVisualization: Creating visualization ${reportName}..." + execute_cypher "${PATH_FINDINGS_CYPHER_DIR}/Path_Finding_6_Longest_paths_contributors_for_graphviz.cypher" "${MODULE_PROJECTION}" "${MODULE_NODE}" "${MODULE_WEIGHT}" > "${FULL_REPORT_DIRECTORY}/${reportName}.csv" + source "${VISUALIZATION_SCRIPTS_DIR}/visualizeQueryResults.sh" "${FULL_REPORT_DIRECTORY}/${reportName}.csv" +fi + +# Clean-up after report generation. Empty reports will be deleted. +source "${SCRIPTS_DIR}/cleanupAfterReportGeneration.sh" "${FULL_REPORT_DIRECTORY}" \ No newline at end of file From 0839c1f9cd4d56427c862c7b6c00a16c6589f658 Mon Sep 17 00:00:00 2001 From: JohT <7671054+JohT@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:05:29 +0100 Subject: [PATCH 7/7] Deprecate graph-visualization nodejs package in favor of GraphViz. --- .github/workflows/java-code-analysis.yml | 9 --------- .github/workflows/typescript-code-analysis.yml | 9 --------- graph-visualization/DEPRECATED.md | 5 +++++ graph-visualization/README.md | 9 +++++++-- scripts/reports/GraphVisualization.sh | 5 ++++- 5 files changed, 16 insertions(+), 21 deletions(-) create mode 100644 graph-visualization/DEPRECATED.md diff --git a/.github/workflows/java-code-analysis.yml b/.github/workflows/java-code-analysis.yml index 3d7853260..beb6e9f0c 100644 --- a/.github/workflows/java-code-analysis.yml +++ b/.github/workflows/java-code-analysis.yml @@ -69,15 +69,6 @@ jobs: distribution: 'adopt' java-version: ${{ matrix.java }} - - name: Setup Node.js for Graph Visualization - uses: actions/setup-node@v4 - with: - node-version-file: 'graph-visualization/.nvmrc' - - - name: Install Node packages for Graph Visualization - working-directory: graph-visualization - run: npm ci - - name: Setup Cache for Conda package manager Miniforge uses: actions/cache@v4 env: diff --git a/.github/workflows/typescript-code-analysis.yml b/.github/workflows/typescript-code-analysis.yml index 3b21b8a5f..348594a8d 100644 --- a/.github/workflows/typescript-code-analysis.yml +++ b/.github/workflows/typescript-code-analysis.yml @@ -69,15 +69,6 @@ jobs: distribution: 'adopt' java-version: ${{ matrix.java }} - - name: Setup Node.js for Graph Visualization - uses: actions/setup-node@v4 - with: - node-version-file: 'graph-visualization/.nvmrc' - - - name: Install Node packages for Graph Visualization - working-directory: graph-visualization - run: npm ci - - name: Setup Cache for Conda package manager Miniforge uses: actions/cache@v4 env: diff --git a/graph-visualization/DEPRECATED.md b/graph-visualization/DEPRECATED.md new file mode 100644 index 000000000..02f64d5f3 --- /dev/null +++ b/graph-visualization/DEPRECATED.md @@ -0,0 +1,5 @@ +# Render Graph Visualizations (Deprecated) + +:warning: **This package is deprecated and might get removed in future.** :warning: + +Visualizations are now done using [GraphViz](https://graphviz.org). Please use the new script [visualizeQueryResults.sh](./../scripts/visualization/visualizeQueryResults.sh) like it is done in [InternalDependenciesVisualization.sh](./../scripts/reports/InternalDependenciesVisualization.sh) to create Graph Visualizations. diff --git a/graph-visualization/README.md b/graph-visualization/README.md index 50e9a24fc..53953de24 100644 --- a/graph-visualization/README.md +++ b/graph-visualization/README.md @@ -1,6 +1,11 @@ -# Render Graph Visualizations +# Render Graph Visualizations (Deprecated) -This [node.js](https://nodejs.org/de) project provides the script [renderVisualizations.js](./renderVisualizations.js) to render all graph visualizations as image files. +:warning: **This package is deprecated and might get removed in future.** :warning: + +Visualizations are now done using [GraphViz](https://graphviz.org). Please use the new script [visualizeQueryResults.sh](./../scripts/visualization/visualizeQueryResults.sh) like it is done in [InternalDependenciesVisualization.sh](./../scripts/reports/InternalDependenciesVisualization.sh) to create Graph Visualizations. + +This [node.js](https://nodejs.org/de) project provides the script [renderVisualizations.js](./renderVisualizations.js) to render graph visualizations as image files. It shows how to use [Puppeteer](https://pptr.dev) to render HTML5 Canvas elements as images, +jimp to manipulate images and neovis to visualize Neo4j Graphs. ## Prerequisites diff --git a/scripts/reports/GraphVisualization.sh b/scripts/reports/GraphVisualization.sh index 99bde269c..6729a1a9f 100755 --- a/scripts/reports/GraphVisualization.sh +++ b/scripts/reports/GraphVisualization.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Creates the "graph-visualization" report (ipynb, md, pdf) based on the Jupyter Notebook "ArtifactDependencies.ipynb". +# DEPRECATED: Creates the "graph-visualization" report using the (now deprecated) nodejs project "graph-visualization". # It contains the hierarchical artifact dependencies graph # Requires executeJupyterNotebook.sh @@ -8,6 +8,9 @@ # Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands) set -o errexit -o pipefail +echo "GraphVisualization: Graph Visualization with the nodejs project 'graph-visualization' is deprecated and will be skipped." +return 0 + # Overrideable Constants (defaults also defined in sub scripts) REPORTS_DIRECTORY=${REPORTS_DIRECTORY:-"reports"}