From 6cbf7ac533573b9ddd2de8d5747b65ed305da409 Mon Sep 17 00:00:00 2001 From: Oliver Stolz Date: Mon, 29 Apr 2024 15:43:49 +0200 Subject: [PATCH 1/4] Enable passing a parsed dotfile for graph creation --- .../code_visualization/element_creation.ts | 13 ++++++---- .../src/code_visualization/graph_creation.ts | 4 +-- client/src/code_visualization/main.ts | 26 ++++++++++++++----- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/client/src/code_visualization/element_creation.ts b/client/src/code_visualization/element_creation.ts index c20dfcb..65227c5 100644 --- a/client/src/code_visualization/element_creation.ts +++ b/client/src/code_visualization/element_creation.ts @@ -11,7 +11,7 @@ import { Point } from '../types/point'; * read out the stored HTML data to create nodes, edges and containers to visualize the PFDL program. * @returns graph elements as valid JSON strings that are accepted by cytoscape */ -export function getGraphElements() { +export function getGraphElements(parsedDotfile = null) { // retrieve graph data const dotfileContent = document.getElementById('graphElementsDiv').innerText; if (dotfileContent == null || dotfileContent.search('graph') == -1) { @@ -23,12 +23,15 @@ export function getGraphElements() { const containers = buildTreeStructure(treeStructure, dotfileString); - // 3rd party package - const parse = require('dotparser'); - const parsedContent = parse(dotfileString)[0]; + if (parsedDotfile == null) { + // parse the dotfile here + // 3rd party package + const parse = require('dotparser'); + parsedDotfile = parse(dotfileString)[0]; + } // convert into utf-8 string - const [nodes, edges] = parseElementsFromDotFileString(parsedContent); + const [nodes, edges] = parseElementsFromDotFileString(parsedDotfile); return getFinalGraphElements(nodes, edges, containers); } diff --git a/client/src/code_visualization/graph_creation.ts b/client/src/code_visualization/graph_creation.ts index ccb682e..54d151f 100644 --- a/client/src/code_visualization/graph_creation.ts +++ b/client/src/code_visualization/graph_creation.ts @@ -13,9 +13,9 @@ import { createTooltips } from './event_handler'; */ const getGraphElements = require('./element_creation').getGraphElements; -export function createGraph(cy) { +export function createGraph(cy, parsedDotfile = null) { // add all graph nodes to cytoscape - const elements = getGraphElements(); + const elements = getGraphElements(parsedDotfile); if (elements == null) { return false; } diff --git a/client/src/code_visualization/main.ts b/client/src/code_visualization/main.ts index 2c1129f..f1c14e3 100644 --- a/client/src/code_visualization/main.ts +++ b/client/src/code_visualization/main.ts @@ -11,13 +11,16 @@ const cy = setupCytoscapeInstance(document.getElementById('cy')); setupEventHandlers(cy); // create the graph -createGraph(cy); +createGraph(cy, null); // global function used to update the dashboard in the standalone browser version -(window as any).createCodeVisualization = function (refreshData = true) { +(window as any).createCodeVisualization = async function ( + refreshData = true, + parsedDotfile = null +) { if (refreshData) { cy.elements().remove(); - const successfullyCreated = createGraph(cy); + const successfullyCreated = createGraph(cy, parsedDotfile); if (!successfullyCreated) { // no elements drawn return; @@ -27,11 +30,20 @@ createGraph(cy); // create and display image of the code visualization if the element 'codeVisuImg' exists const imgElement = document.getElementById('codeVisuImg'); if (imgElement) { - // only exists in standalone browser version (without vscode) - // create img of rotated graph + // only exists in standalone browser version (without vscode) to create img of the rotated graph + + // rotate nodes for img rotateNodes(cy.nodes(), cy.nodes('.container'), false); - const pngToDownload = cy.png({ full: true }); + + // create jpg + const graphBlob = cy.jpg({ + full: true, + output: 'blob-promise' + }); + + // rotate nodes back rotateNodes(cy.nodes(), cy.nodes('.container'), true); - imgElement.setAttribute('src', pngToDownload); + + return graphBlob; } }; From 696cb0096850ebffcf3d6f82d24bf6b1cb753340 Mon Sep 17 00:00:00 2001 From: Oliver Stolz Date: Mon, 29 Apr 2024 15:44:21 +0200 Subject: [PATCH 2/4] Handle error when canvas is undefined --- client/src/canvas/draw_legend.ts | 172 ++++++++++++++++--------------- 1 file changed, 87 insertions(+), 85 deletions(-) diff --git a/client/src/canvas/draw_legend.ts b/client/src/canvas/draw_legend.ts index 944886c..c830683 100644 --- a/client/src/canvas/draw_legend.ts +++ b/client/src/canvas/draw_legend.ts @@ -55,98 +55,100 @@ function drawRoundedRect( // set the canvas properties const canvas = document.getElementById('myCanvas') as HTMLCanvasElement; -const width = window.innerWidth; -canvas.width = width; -const scalingFactor = width / 400; // adjust the legend size according to the screen width -canvas.height = 460 * scalingFactor; -const ctx: CanvasRenderingContext2D = canvas.getContext('2d'); +if (canvas != null) { + const width = window.innerWidth; + canvas.width = width; + const scalingFactor = width / 400; // adjust the legend size according to the screen width + canvas.height = 460 * scalingFactor; + const ctx: CanvasRenderingContext2D = canvas.getContext('2d'); -// style properties for the legend that are found to fit well -//// circles -const circleRadius = 15 * scalingFactor; -const circleX = 30 * scalingFactor; -const orderedCircleColorsAndLabels = [ - ['--component-started-color', 'Component started'], - ['--component-finished-color', 'Component finished'], - ['--component-done-color', 'Component done'], - ['--statement-passed-color', 'Statement passed'], - ['--statement-failed-color', 'Statement failed'], - ['--statement-entry-color', 'Loop / Condition entry'] -]; -//// boxes -const rectX = 17 * scalingFactor; -const rectWidth = 27 * scalingFactor; -const rectBorderRadius = 6 * scalingFactor; -const orderedBoxColorsAndLabels = [ - ['--task-box-color', 'Task'], - ['--service-box-color', 'Service'], - ['--condition-box-color', 'Condition'], - ['--loop-box-color', 'Loop'], - ['--parallel-box-color', 'Parallel'] -]; -//// descriptionText -const textX = 80 * scalingFactor; -//// entries -const lineWidth = 2 * scalingFactor; -const lineHeight = 40 * scalingFactor; -const objectsHeightDifferenceCircleAndBox = -10 * scalingFactor; // the boxes are bigger, so make the gap between the circle and box smaller -let objectsLineStartY = 30 * scalingFactor; -const fontSize = 27 * scalingFactor; -const textHeightDifferenceCircleAndBox = 4 * scalingFactor; // same as for the objects -let textLineStartY = 39 * scalingFactor; + // style properties for the legend that are found to fit well + //// circles + const circleRadius = 15 * scalingFactor; + const circleX = 30 * scalingFactor; + const orderedCircleColorsAndLabels = [ + ['--component-started-color', 'Component started'], + ['--component-finished-color', 'Component finished'], + ['--component-done-color', 'Component done'], + ['--statement-passed-color', 'Statement passed'], + ['--statement-failed-color', 'Statement failed'], + ['--statement-entry-color', 'Loop / Condition entry'] + ]; + //// boxes + const rectX = 17 * scalingFactor; + const rectWidth = 27 * scalingFactor; + const rectBorderRadius = 6 * scalingFactor; + const orderedBoxColorsAndLabels = [ + ['--task-box-color', 'Task'], + ['--service-box-color', 'Service'], + ['--condition-box-color', 'Condition'], + ['--loop-box-color', 'Loop'], + ['--parallel-box-color', 'Parallel'] + ]; + //// descriptionText + const textX = 80 * scalingFactor; + //// entries + const lineWidth = 2 * scalingFactor; + const lineHeight = 40 * scalingFactor; + const objectsHeightDifferenceCircleAndBox = -10 * scalingFactor; // the boxes are bigger, so make the gap between the circle and box smaller + let objectsLineStartY = 30 * scalingFactor; + const fontSize = 27 * scalingFactor; + const textHeightDifferenceCircleAndBox = 4 * scalingFactor; // same as for the objects + let textLineStartY = 39 * scalingFactor; -// draw the legend symbols as coloured circles and rectangles + // draw the legend symbols as coloured circles and rectangles -// draw the circles -for (const [circleColor, circleLabel] of orderedCircleColorsAndLabels) { - // draw the colored circle - ctx.beginPath(); - ctx.arc(circleX, objectsLineStartY, circleRadius, 0, 2 * Math.PI); - ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue( - circleColor - ); - ctx.fill(); - ctx.lineWidth = lineWidth; - ctx.stroke(); + // draw the circles + for (const [circleColor, circleLabel] of orderedCircleColorsAndLabels) { + // draw the colored circle + ctx.beginPath(); + ctx.arc(circleX, objectsLineStartY, circleRadius, 0, 2 * Math.PI); + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue( + circleColor + ); + ctx.fill(); + ctx.lineWidth = lineWidth; + ctx.stroke(); - // draw the label - ctx.font = String(fontSize) + 'px arial'; - ctx.fillStyle = '#000000'; - ctx.fillText(circleLabel, textX, textLineStartY); + // draw the label + ctx.font = String(fontSize) + 'px arial'; + ctx.fillStyle = '#000000'; + ctx.fillText(circleLabel, textX, textLineStartY); - // set the start height for the next line - objectsLineStartY += lineHeight; - textLineStartY += lineHeight; -} + // set the start height for the next line + objectsLineStartY += lineHeight; + textLineStartY += lineHeight; + } -// adjust the start height for the next line when switching from circle to box -objectsLineStartY += objectsHeightDifferenceCircleAndBox; -textLineStartY += textHeightDifferenceCircleAndBox; + // adjust the start height for the next line when switching from circle to box + objectsLineStartY += objectsHeightDifferenceCircleAndBox; + textLineStartY += textHeightDifferenceCircleAndBox; -// draw the boxes -for (const [boxColor, boxLabel] of orderedBoxColorsAndLabels) { - ctx.beginPath(); - drawRoundedRect( - ctx, - rectX, - objectsLineStartY, - rectWidth, - rectWidth, - rectBorderRadius - ); - ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue( - boxColor - ); - ctx.fill(); - ctx.lineWidth = lineWidth; - ctx.stroke(); + // draw the boxes + for (const [boxColor, boxLabel] of orderedBoxColorsAndLabels) { + ctx.beginPath(); + drawRoundedRect( + ctx, + rectX, + objectsLineStartY, + rectWidth, + rectWidth, + rectBorderRadius + ); + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue( + boxColor + ); + ctx.fill(); + ctx.lineWidth = lineWidth; + ctx.stroke(); - // draw the label - ctx.font = String(fontSize) + 'px arial'; - ctx.fillStyle = '#000000'; - ctx.fillText(boxLabel, textX, textLineStartY); + // draw the label + ctx.font = String(fontSize) + 'px arial'; + ctx.fillStyle = '#000000'; + ctx.fillText(boxLabel, textX, textLineStartY); - // set the start height for the next line - objectsLineStartY += lineHeight; - textLineStartY += lineHeight; + // set the start height for the next line + objectsLineStartY += lineHeight; + textLineStartY += lineHeight; + } } From c1bce8219c53bc683f4a948e5e56253e0281170b Mon Sep 17 00:00:00 2001 From: Oliver Stolz Date: Mon, 6 May 2024 12:37:45 +0200 Subject: [PATCH 3/4] Update global dashboard functions --- client/src/code_visualization/main.ts | 60 ++++++++++----------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/client/src/code_visualization/main.ts b/client/src/code_visualization/main.ts index f1c14e3..43b1a30 100644 --- a/client/src/code_visualization/main.ts +++ b/client/src/code_visualization/main.ts @@ -7,43 +7,27 @@ import { setupEventHandlers } from './event_handler'; import { createGraph } from './graph_creation'; import { rotateNodes } from './context_menu'; -const cy = setupCytoscapeInstance(document.getElementById('cy')); -setupEventHandlers(cy); - -// create the graph -createGraph(cy, null); - -// global function used to update the dashboard in the standalone browser version -(window as any).createCodeVisualization = async function ( - refreshData = true, - parsedDotfile = null -) { - if (refreshData) { +const cytoscapeDiv = document.getElementById('cy'); +if (cytoscapeDiv) { + const cy = setupCytoscapeInstance(document.getElementById('cy')); + setupEventHandlers(cy); + + // create the graph + createGraph(cy, null); + // global function to enable the dashboard in the standalone browser version to access the cytoscape instance + (window as any).getCy = function () { + return cy; + }; + // global function used to update the dashboard in the standalone browser version + (window as any).createCodeVisualization = async function ( + parsedDotfile = null + ) { cy.elements().remove(); - const successfullyCreated = createGraph(cy, parsedDotfile); - if (!successfullyCreated) { - // no elements drawn - return; + createGraph(cy, parsedDotfile); + const imgElement = document.getElementById('codeVisuImg'); + if (imgElement) { + // graph should be displayed as an image, rotate it + rotateNodes(cy.nodes(), cy.nodes('.container'), false); } - } - - // create and display image of the code visualization if the element 'codeVisuImg' exists - const imgElement = document.getElementById('codeVisuImg'); - if (imgElement) { - // only exists in standalone browser version (without vscode) to create img of the rotated graph - - // rotate nodes for img - rotateNodes(cy.nodes(), cy.nodes('.container'), false); - - // create jpg - const graphBlob = cy.jpg({ - full: true, - output: 'blob-promise' - }); - - // rotate nodes back - rotateNodes(cy.nodes(), cy.nodes('.container'), true); - - return graphBlob; - } -}; + }; +} From 494ab2b7be75c05aaa554788a47535b3661cdc19 Mon Sep 17 00:00:00 2001 From: Oliver Stolz Date: Tue, 7 May 2024 18:34:29 +0200 Subject: [PATCH 4/4] Enable dashboard to update token nodes --- .../code_visualization/element_creation.ts | 2 +- client/src/code_visualization/main.ts | 47 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/client/src/code_visualization/element_creation.ts b/client/src/code_visualization/element_creation.ts index 65227c5..c79b808 100644 --- a/client/src/code_visualization/element_creation.ts +++ b/client/src/code_visualization/element_creation.ts @@ -162,7 +162,7 @@ const loopTree = ( * @param fileString a JSON string containing the graph objects * @returns an array containing all existing nodes and edges in the dotfile */ -const parseElementsFromDotFileString = ( +export const parseElementsFromDotFileString = ( fileString: any, standaloneTesting = false ) => { diff --git a/client/src/code_visualization/main.ts b/client/src/code_visualization/main.ts index 43b1a30..e571bdb 100644 --- a/client/src/code_visualization/main.ts +++ b/client/src/code_visualization/main.ts @@ -6,6 +6,7 @@ import { setupCytoscapeInstance } from './setup_cytoscape'; import { setupEventHandlers } from './event_handler'; import { createGraph } from './graph_creation'; import { rotateNodes } from './context_menu'; +import { parseElementsFromDotFileString } from './element_creation'; const cytoscapeDiv = document.getElementById('cy'); if (cytoscapeDiv) { @@ -14,14 +15,14 @@ if (cytoscapeDiv) { // create the graph createGraph(cy, null); + // global function to enable the dashboard in the standalone browser version to access the cytoscape instance (window as any).getCy = function () { return cy; }; + // global function used to update the dashboard in the standalone browser version - (window as any).createCodeVisualization = async function ( - parsedDotfile = null - ) { + (window as any).createCodeVisualization = function (parsedDotfile = null) { cy.elements().remove(); createGraph(cy, parsedDotfile); const imgElement = document.getElementById('codeVisuImg'); @@ -30,4 +31,44 @@ if (cytoscapeDiv) { rotateNodes(cy.nodes(), cy.nodes('.container'), false); } }; + + // global function for the dashboard in the standalone browser version to enable the comparison of two dotfiles + (window as any).getElementsFromDotfile = function (dotfile) { + return parseElementsFromDotFileString(dotfile); + }; + + (window as any).flipTokensForNodeIds = function (nodeIds) { + nodeIds.forEach((id) => { + const node = cy.getElementById(id); + const tokenNode = cy.getElementById(id + '_token'); + if (tokenNode.length && tokenNode.inside()) { + // node had a token before, remove it + const newParentId = tokenNode.data('parent'); + node.data('label', ''); + node.move({ parent: newParentId }); + node.classes(['single_node', 'upper_round_node']); + tokenNode.remove(); + } else { + if (tokenNode.removed()) { + tokenNode.restore(); + } else { + // create a new token node for the object + const newTokenNode = { + group: 'nodes', + classes: ['upper_round_node', 'token_label'], + data: { + id: id + '_token', + label: '', + tippyContent: node.label, + parent: node.parentId + } + }; + cy.add(newTokenNode); + } + node.data('label', '•'); + node.move({ parent: node.id + '_token' }); + node.classes(['single_node', 'token_node']); + } + }); + }; }