diff --git a/interactive_viewer/src/KubeDiagrams.png b/interactive_viewer/src/KubeDiagrams.png
new file mode 100644
index 0000000..05d89c7
Binary files /dev/null and b/interactive_viewer/src/KubeDiagrams.png differ
diff --git a/interactive_viewer/src/index.html b/interactive_viewer/src/index.html
new file mode 100644
index 0000000..efdb1be
--- /dev/null
+++ b/interactive_viewer/src/index.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ KubeDiagrams Interactive Viewer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Interactive Viewer
+
+
+
+
+
\ No newline at end of file
diff --git a/interactive_viewer/src/script/defaultStyle.js b/interactive_viewer/src/script/defaultStyle.js
new file mode 100644
index 0000000..ff7f140
--- /dev/null
+++ b/interactive_viewer/src/script/defaultStyle.js
@@ -0,0 +1,92 @@
+let clusterStyleList = [];
+let nodeStyleList = [];
+let edgeStyleList = [];
+
+function getDefaultGlobalNodeStyleFromNodeValues(node) {
+ return {
+ selector : ".nodeStyle" + nodeStyleList.length,
+ style : {
+ 'color': node.data.fontcolor,
+ 'font-size': node.data.fontsize,
+ 'font-family': node.data.fontfamily,
+ }
+ };
+}
+
+function getDefaultClusterStyleFromClusterValues(cluster) {
+ return {
+ selector : ".clusterStyle" + clusterStyleList.length,
+ style : {
+ 'border-style': cluster.data.bs,
+ 'border-color': cluster.data.bc,
+ 'background-color': cluster.data.bgcolor,
+ }
+ };
+}
+
+function getDefaultEdgeStyleFromEdgeValues(edge) {
+ return {
+ selector : ".edgeStyle" + edgeStyleList.length,
+ style : {
+ 'line-color': edge.data.color,
+ 'target-arrow-color': edge.data.color,
+ 'source-arrow-color': edge.data.color,
+ 'line-style': edge.data.line_style,
+ 'color': edge.data.fontcolor,
+ 'font-size': edge.data.fontsize,
+ 'font-family': edge.data.fontfamily,
+ }
+ };
+}
+
+const clusterClosedStyle = {
+ 'text-wrap': 'wrap',
+ 'text-max-width': '100px',
+ 'text-valign': 'center',
+ 'text-halign': 'center',
+ 'text-margin-y': 0,
+ 'width': 50,
+ 'height': 20,
+ }
+
+const clusterOpenStyle = {
+ 'text-valign': 'top',
+ 'text-wrap': 'none',
+ 'text-margin-y': 15,
+ 'padding': '15px',
+ }
+
+const defaultGroupNodeStyle = {
+ 'background-fit': 'cover',
+ 'background-image': 'data(image)',
+ 'background-opacity': 0,
+ 'background-clip': 'node',
+ 'width': 100,
+ 'height': 100,
+ 'text-valign' : 'bottom',
+ 'text-wrap': 'wrap',
+ }
+
+const defaultGlobalNodeStyle = {
+ 'content': 'data(label)',
+ 'min-zoomed-font-size': '8'
+ }
+
+const defaultEdgeStyle = {
+ 'source-endpoint': 'outside-to-node-or-label',
+ 'target-endpoint': 'outside-to-node-or-label',
+ 'width': 1,
+ 'curve-style': 'bezier',
+ 'label': 'data(xlabel)',
+ 'min-zoomed-font-size': '8',
+ }
+
+const defaultEdgeDirForward = {
+ 'target-arrow-shape': 'triangle',
+ 'target-arrow-fill': 'filled',
+ }
+
+const defaultEdgeDirBack = {
+ 'source-arrow-shape': 'triangle',
+ 'source-arrow-fill': 'filled',
+ }
\ No newline at end of file
diff --git a/interactive_viewer/src/script/itemAndFunctionMenus.js b/interactive_viewer/src/script/itemAndFunctionMenus.js
new file mode 100644
index 0000000..09d1b0d
--- /dev/null
+++ b/interactive_viewer/src/script/itemAndFunctionMenus.js
@@ -0,0 +1,35 @@
+const itemOpenClose = {
+ id: 'co',
+ content: 'close/open',
+ tooltipText: 'close/open',
+ selector: 'node[group = "cluster"]',
+ onClickFunction: function (event) {
+ const cluster = event.target;
+ if (cluster.data('isClose')) {
+ openCluster(cluster);
+ }
+ else {
+ closeCluster(cluster);
+ }
+ },
+ }
+
+/**
+ * Open a cluster
+ * @param {*} cluster
+ */
+function openCluster(cluster) {
+ cluster.children().style('display', 'element');
+ cluster.data('isClose', false);
+ cluster.style(clusterOpenStyle);
+}
+
+/**
+ * Close a cluster
+ * @param {*} cluster
+ */
+function closeCluster(cluster) {
+ cluster.children().style('display', 'none');
+ cluster.data('isClose', true);
+ cluster.style(clusterClosedStyle);
+}
\ No newline at end of file
diff --git a/interactive_viewer/src/script/layout.js b/interactive_viewer/src/script/layout.js
new file mode 100644
index 0000000..779830c
--- /dev/null
+++ b/interactive_viewer/src/script/layout.js
@@ -0,0 +1,22 @@
+const layoutDagre = {name: 'dagre',
+ rankDir: 'TB',
+ ranker: 'network-simplex',
+ nodeSep: 25,
+ avoidOverlap: true,
+ fit: true,
+}
+
+const layoutKlay = {
+ name: 'klay',
+ klay: {
+ direction: 'DOWN',
+ layoutHierarchy: true,
+ spacing: 50,
+ edgeSpacingFactor: 0,
+ fixedAlignment: 'RIGHTUP',
+ nodeLayering:'INTERACTIVE',
+ }
+
+}
+
+const layoutList = [layoutDagre, layoutKlay];
\ No newline at end of file
diff --git a/interactive_viewer/src/script/main.js b/interactive_viewer/src/script/main.js
new file mode 100644
index 0000000..a1e6f8c
--- /dev/null
+++ b/interactive_viewer/src/script/main.js
@@ -0,0 +1,362 @@
+cytoscape.use(cytoscapeDagre);
+cytoscape.use(cytoscapeKlay);
+
+let cy;
+let currentLayout;
+
+/**
+ * initialise a cytoscape instance, create layout selector buttons, add event listeners on save buttons and add
+ * an event listener on the file input button to load the cytoscape graph.
+ */
+function setUp() {
+ cy = getCyGraph();
+ currentLayout = layoutList[0];
+ createLayoutSelectorButton();
+ document.getElementById("savePNG").addEventListener("click", () => { saveFile("png")});
+ document.getElementById("saveJPG").addEventListener("click", () => { saveFile("jpg")});
+ document.getElementById('fileInput').addEventListener('change', readFileAndloadCytoscapeGraph);
+}
+
+
+/**
+ * return a cytoscape graph.
+ * @returns
+ */
+function getCyGraph() {
+ return cytoscape({
+ container: document.getElementById('paper'),
+ elements: undefined,
+ hideEdgesOnViewport: true,
+ pixelRatio: window.devicePixelRatio,
+ style: [
+ {
+ selector: 'node',
+ style: defaultGlobalNodeStyle
+ },
+ {
+ selector: 'node[group = "cluster"]',
+ style: clusterOpenStyle
+ },
+ {
+ selector: 'node[group = "node"]',
+ style: defaultGroupNodeStyle
+ },
+ {
+ selector: 'edge',
+ style: defaultEdgeStyle
+ },
+ {
+ selector: 'edge[dir = "forward"]',
+ style: defaultEdgeDirForward
+ },
+ {
+ selector: 'edge[dir = "back"]',
+ style: defaultEdgeDirBack
+ }
+ ]
+ });
+}
+
+/**
+ * Read a dot_json file to add nodes and edges in a elements list used to load a cytoscape instance.
+ * @param {*} event
+ */
+function readFileAndloadCytoscapeGraph(event) {
+ const file = event.target.files[0];
+ const reader = new FileReader();
+ const elements = [];
+
+ reader.onload = function(e) {
+ let content = e.target.result;
+ const json = JSON.parse(content);
+ const nodes = json.objects;
+ const edges = json.edges;
+ addNodesElementsParsedFromNodesJson(elements, nodes);
+ addNodesEdgesParsedFromEdgesJson(elements, edges);
+ load_cytoscape(elements);
+ };
+ reader.readAsText(file);
+}
+
+/**
+ * Remove precedent elements before to add the new elements then create tool tip and context menus for the
+ * cytoscape instance.
+ * @param {*} elements
+ */
+function load_cytoscape(elements) {
+ cy.nodes().remove();
+ cy.add(elements);
+ createTooltip("node");
+ createTooltip("edge");
+ createAndGetContextMenu(cy);
+ cy.layout(currentLayout).run();
+}
+
+function load_layout(layout) {
+ currentLayout = layout;
+ cy.layout(currentLayout).run();
+}
+
+/**
+ * Add nodes elements created from nodesJson data list in the elements list.
+ * @param {*} elements
+ * @param {*} nodesJson - nodes data list
+ * @returns
+ */
+function addNodesElementsParsedFromNodesJson(elements, nodesJson) {
+ let parent = {};
+
+ for (let i in nodesJson) {
+ let node = {
+ data: {
+ id: nodesJson[i]._gvid,
+ group: (nodesJson[i].nodes) ? 'cluster' : 'node',
+ isClose : false,
+ label: (nodesJson[i].label.includes('<') && nodesJson[i].label.includes('>')) ? nodesJson[i].tooltip : nodesJson[i].label,
+ bs: getCorrespondingBorderStyle(nodesJson[i].style),
+ bgcolor: nodesJson[i].bgcolor ?? 'blue',
+ bc: nodesJson[i].pencolor ?? 'gray',
+ parent: parent[nodesJson[i]._gvid] ?? '',
+ fontsize: nodesJson[i].fontsize ?? '',
+ fontfamily: nodesJson[i].fontname ?? '',
+ fontcolor: nodesJson[i].fontcolor ?? '',
+ image: (nodesJson[i].image) ? getURLImage(nodesJson[i].image) : '',
+ tooltip: nodesJson[i].tooltip ?? ''
+ }
+ }
+
+ if (nodesJson[i].nodes) {
+ for (let child of nodesJson[i].nodes) {
+ parent[child] = nodesJson[i]._gvid;
+ }
+ }
+
+ if (nodesJson[i].subgraphs) {
+ for (let sub of nodesJson[i].subgraphs) {
+ parent[sub] = nodesJson[i]._gvid;
+ }
+ }
+ addClassToElement(node);
+ elements.push(node);
+ }
+
+ return elements;
+}
+
+/**
+ * Add nodes elements created from edgesJson data list in the elements list.
+ * @param {*} elements
+ * @param {*} edgesJson
+ */
+function addNodesEdgesParsedFromEdgesJson(elements, edgesJson) {
+ for (let e in edgesJson) {
+ let edge = {
+ data: {
+ id: 'e' + edgesJson[e]._gvid,
+ group: 'edge',
+ dir: edgesJson[e].dir,
+ source: edgesJson[e].tail,
+ target: edgesJson[e].head,
+ color: edgesJson[e].color,
+ line_style: edgesJson[e].style ?? 'solid',
+ xlabel: edgesJson[e].xlabel ?? '',
+ fontsize: edgesJson[e].fontsize ?? '',
+ fontfamily: edgesJson[e].fontname ?? '',
+ fontcolor: edgesJson[e].fontcolor ?? '',
+ tooltip: edgesJson[e].tooltip ?? '',
+ }
+ }
+ addClassToElement(edge);
+ elements.push(edge);
+ }
+}
+
+/**
+ * Get border style based on the what's in the style parameter.
+ * @param {*} style
+ * @returns
+ */
+function getCorrespondingBorderStyle(style) {
+ if (style.includes('dashed')) {
+ return 'dashed';
+ }
+ else {
+ return 'solid';
+ }
+}
+
+/**
+ * Get the corresponding url image based on the local url of the image parameter get from a dot_jon file.
+ * @param {*} image
+ * @returns
+ */
+function getURLImage(image) {
+ let idx = image.indexOf("resources\/")
+ if(idx != -1) {
+ return "https://raw.githubusercontent.com/mingrammer/diagrams/refs/heads/master/" + image.slice(image.indexOf("resources"))
+ } else {
+ idx = image.indexOf("bin\/icons\/")
+ if(idx != -1) {
+ return "https://raw.githubusercontent.com/philippemerle/KubeDiagrams/refs/heads/main/" + image.slice(image.indexOf("bin\/icons\/"))
+ } else {
+ return image
+ }
+ }
+}
+
+/**
+ * Create a HTML tooltip with the events of the cytoscape instance
+ * @param {*} elementType
+ */
+function createTooltip(elementType) {
+ const tooltip = document.getElementById('tooltip');
+ let to;
+
+ cy.on('mouseover', elementType, function(event) {
+ const element = event.target;
+ to = setTimeout(() => {
+ tooltip.style.display = 'block';
+ tooltip.innerText = element.data('tooltip');
+ }, 1000);
+ });
+
+ cy.on('mouseout', elementType, function(event) {
+ clearTimeout(to);
+ tooltip.style.display = 'none';
+ });
+
+ cy.on('mousemove', function(event) {
+ tooltip.style.left = (event.originalEvent.pageX + 10) + 'px';
+ tooltip.style.top = (event.originalEvent.pageY + 10) + 'px';
+ });
+}
+
+/**
+ * Create layout selector buttons after the fileInput
+ */
+function createLayoutSelectorButton() {
+ const layoutButtons = document.getElementById("layoutButtons");
+ for (let layout of layoutList) {
+ let button = document.createElement("button")
+ button.id = layout.name;
+ button.textContent = layout.name;
+ button.addEventListener("click", () => {load_layout(layout)} );
+ layoutButtons.appendChild(button);
+ }
+}
+
+/**
+ * Save file in png or jpg format.
+ * @param {*} format
+ */
+function saveFile(format) {
+ const img = getFileAs(format);
+
+ const ele = document.createElement('a');
+ ele.href = img;
+ ele.download = 'graph.' + format;
+ ele.click();
+}
+
+/**
+ * Get a representation of the graph as an image of the requested format
+ * @param {*} format
+ * @returns
+ */
+function getFileAs(format) {
+ switch (format) {
+ case "png": return cy.png({full : true});
+ case "jpg": return cy.jpg({full : true});
+ }
+}
+
+/**
+ * Create and return a context menus for the cytoscape instance
+ * @param {*} cy
+ * @returns
+ */
+function createAndGetContextMenu(cy) {
+ return cy.contextMenus({
+ evtType: 'cxttap',
+ menuItems: [itemOpenClose]
+ })
+}
+
+/**
+ * Check the element's group and then add a style class based on the group and his values.
+ * @param {*} element
+ */
+function addClassToElement(element) {
+ let classesName = [];
+ let style;
+ if (element.data.group == "edge") {
+ style = getDefaultEdgeStyleFromEdgeValues(element);
+ classesName.push(findAndGetClassStyle(style, edgeStyleList).selector.replace(/^\./, ''));
+ }
+ else {
+ style = getDefaultGlobalNodeStyleFromNodeValues(element);
+ classesName.push(findAndGetClassStyle(style, nodeStyleList).selector.replace(/^\./, ''));
+ if (element.data.group == "cluster") {
+ style = getDefaultClusterStyleFromClusterValues(element);
+ classesName.push(findAndGetClassStyle(style, clusterStyleList).selector.replace(/^\./, ''))
+ }
+ }
+ element.classes = classesName.join(' ');
+}
+
+/**
+ * Look for a style class similar to the styleTarget in the styleList. if a style class is founded,
+ * so the method return the style class founded otherwise add the styleTarget in the cytoscape instance and
+ * in the styleList then return the styleTarget
+ * @param {*} styleTarget
+ * @param {*} styleList
+ * @returns
+ */
+function findAndGetClassStyle(styleTarget, styleList) {
+ for (let style of styleList) {
+ if (equals(styleTarget.style, style.style)) {
+ return style;
+ }
+ }
+ addStyleToCytoscapeGraph(styleTarget);
+ styleList.push(styleTarget);
+ return styleTarget;
+}
+
+/**
+ * Add a new style in the style attribute of the cytoscape instance.
+ * @param {*} style
+ */
+function addStyleToCytoscapeGraph(style) {
+ const existingStyle = cy.style().json();
+ cy.style([...existingStyle, style]);
+}
+
+/**
+ * Check if two object are similar.
+ * @param {*} o1
+ * @param {*} o2
+ * @returns
+ */
+function equals(o1, o2) {
+ if (o1 === o2) return true;
+ if (typeof o1 !== "object" || typeof o2 !== "object" || o1 == null || o2 == null) {
+ return false;
+ }
+
+ const k1 = Object.keys(o1);
+ const k2 = Object.keys(o2);
+
+ if (k1.length !== k2.length) return false;
+
+ for (let k of k1) {
+ if (!k2.includes(k) || !equals(o1[k], o2[k])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+window.addEventListener("load", setUp);
\ No newline at end of file
diff --git a/interactive_viewer/src/style.css b/interactive_viewer/src/style.css
new file mode 100644
index 0000000..bf62cfa
--- /dev/null
+++ b/interactive_viewer/src/style.css
@@ -0,0 +1,24 @@
+#paper {
+ width: 100%;
+ height: 80vh;
+ display: block;
+ margin: auto;
+ border: solid;
+}
+.buttons {
+ display: flex;
+ gap: 10px;
+}
+#layoutButtons {
+ display: flex;
+ gap: 4px;
+}
+#tooltip {
+ position: absolute;
+ display: none;
+ background: white;
+ border: solid;
+ padding: 5px 10px;
+ border-radius: 4px;
+ font-size: 15px;
+}
\ No newline at end of file