UNPKG

js-fault-tree-analyzer

Version:

A JavaScript library for parsing JSON fault tree descriptions and rendering them as interactive SVG graphics with customizable themes

1,247 lines (1,195 loc) 45.5 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: false }), e; } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) { t && (r = t); var n = 0, F = function () {}; return { s: F, n: function () { return n >= r.length ? { done: true } : { done: false, value: r[n++] }; }, e: function (r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = true, u = false; return { s: function () { t = t.call(r); }, n: function () { var r = t.next(); return a = r.done, r; }, e: function (r) { u = true, o = r; }, f: function () { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: true, configurable: true, writable: true }) : e[r] = t, e; } function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = true, o = false; try { if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = true, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } /** * JavaScript Fault Tree Analyser * * A JavaScript implementation for parsing JSON fault tree descriptions * and rendering them as SVG graphics. */ // Color theme definitions var COLOR_THEMES = { light: { name: "Light Theme", // background: "#ffffff", borderWidth: "1.3", text: "#000000", labelBox: { normal: "#fdfbec", degraded: "#ffeb3b", critical: "#ff6969" }, labelBoxBorder: { normal: "#cccccc", degraded: "#e2be0a", critical: "#c62828" }, quantityBox: { normal: "#fdfbec", degraded: "#fff3c4", critical: "#ffcdd2" }, quantityBoxBorder: { normal: "#cccccc", degraded: "#e2be0a", critical: "#c62828" }, symbols: { normal: "#fdfbec", degraded: "#ffeb3b", critical: "#f44336" }, symbolsBorder: { normal: "#cccccc", degraded: "#e2be0a", critical: "#c62828" }, connections: "#cccccc" }, dark: { name: "Dark Theme", background: "#1a1a1a", borderWidth: "1.5", text: "#ffffff", labelBox: { normal: "#2d3748", degraded: "#d69e2e", critical: "#e53e3e" }, labelBoxBorder: { normal: "#cbd5e0", degraded: "#f6ad55", critical: "#fc8181" }, quantityBox: { normal: "#1a202c", degraded: "#744210", critical: "#742a2a" }, quantityBoxBorder: { normal: "#cbd5e0", degraded: "#f6ad55", critical: "#fc8181" }, symbols: { normal: "#4a5568", degraded: "#d69e2e", critical: "#e53e3e" }, symbolsBorder: { normal: "#cbd5e0", degraded: "#f6ad55", critical: "#fc8181" }, connections: "#cbd5e0" }, highContrast: { name: "High Contrast", background: "#ffffff", borderWidth: "2.0", text: "#000000", labelBox: { normal: "#ffffff", degraded: "#ffff00", critical: "#ff0000" }, labelBoxBorder: { normal: "#000000", degraded: "#000000", critical: "#000000" }, quantityBox: { normal: "#f5f5f5", degraded: "#ffffcc", critical: "#ffcccc" }, quantityBoxBorder: { normal: "#000000", degraded: "#000000", critical: "#000000" }, symbols: { normal: "#ffffff", degraded: "#ffff00", critical: "#ff0000" }, symbolsBorder: { normal: "#000000", degraded: "#000000", critical: "#000000" }, connections: "#000000" }, ocean: { name: "Ocean Theme", // background: "#f0f8ff", borderWidth: "1.4", text: "#1e3a8a", labelBox: { normal: "#dbeafe", degraded: "#fce8b7", critical: "#ffe2e2" }, labelBoxBorder: { normal: "#b1c0f3", degraded: "#f8bd78", critical: "#f2a8a8" }, quantityBox: { normal: "#eff6ff", degraded: "#fce8b7", critical: "#fee2e2" }, quantityBoxBorder: { normal: "#b1c0f3", degraded: "#f8bd78", critical: "#f2a8a8" }, symbols: { normal: "#bfdbfe", degraded: "#fddc87", critical: "#ffa0a0" }, symbolsBorder: { normal: "#b1c0f3", degraded: "#f5b56b", critical: "#ec7e7e" }, connections: "#b1c0f3" } }; // Constants for SVG rendering var SVG_CONSTANTS = { PAGE_MARGIN: 10, DEFAULT_FONT_SIZE: 10, DEFAULT_LINE_SPACING: 1.3, TIME_HEADER_MARGIN: 20, TIME_HEADER_Y_OFFSET: -25, TIME_HEADER_FONT_SIZE: 16, EVENT_BOUNDING_WIDTH: 120, EVENT_BOUNDING_HEIGHT: 195, LABEL_BOX_Y_OFFSET: -65, LABEL_BOX_WIDTH: 108, LABEL_BOX_HEIGHT: 70, LABEL_BOX_TARGET_RATIO: 5.4, LABEL_MIN_LINE_LENGTH: 16, IDENTIFIER_BOX_Y_OFFSET: -13, IDENTIFIER_BOX_WIDTH: 108, IDENTIFIER_BOX_HEIGHT: 24, SYMBOL_Y_OFFSET: 20, // Compact spacing SYMBOL_SLOTS_HALF_WIDTH: 30, // OR Gate dimensions // OR Gate dimensions (reduced by ~25%) OR_GATE_APEX_HEIGHT: 28, OR_GATE_NECK_HEIGHT: -8, OR_GATE_BODY_HEIGHT: 27, OR_GATE_SLANT_DROP: 1.5, OR_GATE_SLANT_RUN: 4.5, OR_GATE_SLING_RISE: 26, OR_GATE_GROIN_RISE: 22, OR_GATE_HALF_WIDTH: 25, // AND Gate dimensions (reduced by ~25%) AND_GATE_NECK_HEIGHT: 8, AND_GATE_BODY_HEIGHT: 25, AND_GATE_SLING_RISE: 31, AND_GATE_HALF_WIDTH: 24, // Event dimensions (reduced by ~25%) BASIC_EVENT_RADIUS: 28, UNDEVELOPED_EVENT_HALF_HEIGHT: 28, UNDEVELOPED_EVENT_HALF_WIDTH: 40, HOUSE_EVENT_APEX_HEIGHT: 28, HOUSE_EVENT_SHOULDER_HEIGHT: 18, HOUSE_EVENT_BODY_HEIGHT: 20, HOUSE_EVENT_HALF_WIDTH: 27, QUANTITY_BOX_Y_OFFSET: -105, // Position above the symbol (negative value) QUANTITY_BOX_WIDTH: 108, QUANTITY_BOX_HEIGHT: 32, INPUT_CONNECTOR_BUS_Y_OFFSET: 70, // Reduced connection line length INPUT_CONNECTOR_BUS_HALF_HEIGHT: 10 }; // Enums var NodeType = { GATE: "GATE", EVENT: "EVENT" }; var GateType = { OR: "OR", AND: "AND"}; var EventType = { BASIC: "BASIC", EXTERNAL: "EXTERNAL", UNDEVELOPED: "UNDEVELOPED", HOUSE: "HOUSE" }; var NodeStatus = { NORMAL: "NORMAL", DEGRADED: "DEGRADED", CRITICAL: "CRITICAL" }; var JSONFaultTreeParser = /*#__PURE__*/function () { function JSONFaultTreeParser() { _classCallCheck(this, JSONFaultTreeParser); this.nodeMap = new Map(); this.rootNode = null; } return _createClass(JSONFaultTreeParser, [{ key: "parse", value: function parse(jsonData) { // If jsonData is a string, parse it as JSON var faultTreeData = typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData; // Clear previous data this.nodeMap.clear(); // Parse the tree structure this.rootNode = this.parseNode(faultTreeData); return { rootNode: this.rootNode, nodeMap: this.nodeMap }; } }, { key: "parseNode", value: function parseNode(nodeData) { var node = { id: nodeData.id, label: nodeData.label, type: nodeData.type, children: [], metadata: nodeData.metadata || {} }; // Add type-specific properties if (nodeData.type === NodeType.GATE) { node.gateType = nodeData.gateType || GateType.OR; } else if (nodeData.type === NodeType.EVENT) { node.eventType = nodeData.eventType || EventType.BASIC; } // Add probability and risk score if they exist if (nodeData.probability !== undefined) { node.probability = nodeData.probability; } if (nodeData.riskScore !== undefined) { node.riskScore = nodeData.riskScore; } // Add status (default to NORMAL if not specified) node.status = nodeData.status || NodeStatus.NORMAL; // Store in node map this.nodeMap.set(node.id, node); // Parse children recursively if (nodeData.children && nodeData.children.length > 0) { var _iterator = _createForOfIteratorHelper(nodeData.children), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var childData = _step.value; var childNode = this.parseNode(childData); node.children.push(childNode); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } return node; } }]); }(); var JSONFaultTreeRenderer = /*#__PURE__*/function () { function JSONFaultTreeRenderer(parsedData) { var theme = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "light"; _classCallCheck(this, JSONFaultTreeRenderer); this.rootNode = parsedData.rootNode; this.nodeMap = parsedData.nodeMap; this.layout = null; this.theme = COLOR_THEMES[theme] || COLOR_THEMES.light; } return _createClass(JSONFaultTreeRenderer, [{ key: "render", value: function render() { this.computeLayout(); return this.generateSVG(); } }, { key: "computeLayout", value: function computeLayout() { var layout = { nodes: new Map(), width: 0, height: 0 }; if (!this.rootNode) { this.layout = layout; return; } // Layout the tree starting from root var treeLayout = this.layoutSubtree(this.rootNode); // Copy positions to layout var _iterator2 = _createForOfIteratorHelper(treeLayout.nodes), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var _step2$value = _slicedToArray(_step2.value, 2), nodeId = _step2$value[0], position = _step2$value[1]; layout.nodes.set(nodeId, position); } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } layout.width = treeLayout.width; layout.height = treeLayout.height; this.layout = layout; } }, { key: "layoutSubtree", value: function layoutSubtree(node) { var positions = new Map(); var subtreeInfo = this.calculateSubtreeInfo(node); // Position the root at the top center var rootWidth = subtreeInfo.width; var rootX = rootWidth / 2; positions.set(node.id, { x: rootX, y: 50, id: node.id }); // Recursively position children this.positionChildren(node, rootX, 50 + SVG_CONSTANTS.EVENT_BOUNDING_HEIGHT, positions); return { nodes: positions, width: rootWidth, height: subtreeInfo.height * SVG_CONSTANTS.EVENT_BOUNDING_HEIGHT + 50 }; } }, { key: "calculateSubtreeInfo", value: function calculateSubtreeInfo(node) { var visited = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : new Set(); if (visited.has(node.id)) { return { width: SVG_CONSTANTS.EVENT_BOUNDING_WIDTH, height: 1 }; } visited.add(node.id); if (!node.children || node.children.length === 0) { // Leaf node return { width: SVG_CONSTANTS.EVENT_BOUNDING_WIDTH, height: 1 }; } // Calculate combined width and max height of children var totalWidth = 0; var maxHeight = 1; var _iterator3 = _createForOfIteratorHelper(node.children), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var child = _step3.value; var childInfo = this.calculateSubtreeInfo(child, visited); totalWidth += childInfo.width; maxHeight = Math.max(maxHeight, childInfo.height + 1); } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } return { width: Math.max(SVG_CONSTANTS.EVENT_BOUNDING_WIDTH, totalWidth), height: maxHeight }; } }, { key: "positionChildren", value: function positionChildren(parentNode, parentX, currentY, positions) { var _this = this; if (!parentNode.children || parentNode.children.length === 0) { return; } // Calculate child widths var childWidths = parentNode.children.map(function (child) { var childInfo = _this.calculateSubtreeInfo(child); return childInfo.width; }); var totalChildWidth = childWidths.reduce(function (sum, width) { return sum + width; }, 0); // Position children centered under parent var startX = parentX - totalChildWidth / 2; for (var i = 0; i < parentNode.children.length; i++) { var child = parentNode.children[i]; var childWidth = childWidths[i]; var childX = startX + childWidth / 2; positions.set(child.id, { x: childX, y: currentY, id: child.id }); // Recursively position grandchildren this.positionChildren(child, childX, currentY + SVG_CONSTANTS.EVENT_BOUNDING_HEIGHT, positions); startX += childWidth; } } }, { key: "generateSVG", value: function generateSVG() { var margin = SVG_CONSTANTS.PAGE_MARGIN; var width = this.layout.width + 2 * margin; this.layout.height + 2 * margin; // Calculate proper viewBox to include all content var minY = Math.min.apply(Math, _toConsumableArray(Array.from(this.layout.nodes.values()).map(function (pos) { return pos.y + SVG_CONSTANTS.LABEL_BOX_Y_OFFSET - SVG_CONSTANTS.LABEL_BOX_HEIGHT / 2; }))); var maxY = Math.max.apply(Math, _toConsumableArray(Array.from(this.layout.nodes.values()).map(function (pos) { return pos.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET + SVG_CONSTANTS.BASIC_EVENT_RADIUS; }))); var viewBoxTop = minY - margin; var viewBoxHeight = maxY - minY + 2 * margin; // Generate unique ID for this SVG to scope styles var svgId = "fault-tree-".concat(Math.random().toString(36).substr(2, 9)); var svg = "<svg id=\"".concat(svgId, "\" width=\"").concat(width, "\" height=\"").concat(viewBoxHeight, "\" viewBox=\"0 ").concat(viewBoxTop, " ").concat(width, " ").concat(viewBoxHeight, "\" xmlns=\"http://www.w3.org/2000/svg\">"); svg += this.generateStyles(svgId); // Render connections first (so they appear under text) svg += this.renderLabelConnectors(); svg += this.renderConnections(); // Render nodes last (so text appears on top) var _iterator4 = _createForOfIteratorHelper(this.layout.nodes), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { var _step4$value = _slicedToArray(_step4.value, 2), nodeId = _step4$value[0], position = _step4$value[1]; svg += this.renderNode(nodeId, position); } } catch (err) { _iterator4.e(err); } finally { _iterator4.f(); } svg += "</svg>"; return svg; } }, { key: "generateStyles", value: function generateStyles(svgId) { var theme = this.theme; return "\n <style>\n #".concat(svgId, " {").concat(theme.background ? "\n background-color: ".concat(theme.background, ";") : '', "\n }\n #").concat(svgId, " circle, #").concat(svgId, " path, #").concat(svgId, " polygon, #").concat(svgId, " rect {\n fill: ").concat(theme.symbols.normal, ";\n }\n #").concat(svgId, " circle, #").concat(svgId, " path, #").concat(svgId, " polygon, #").concat(svgId, " polyline, #").concat(svgId, " rect {\n stroke: ").concat(theme.symbolsBorder.normal, ";\n stroke-width: ").concat(theme.borderWidth, ";\n }\n #").concat(svgId, " polyline {\n fill: none;\n stroke: ").concat(theme.connections, ";\n }\n #").concat(svgId, " text {\n dominant-baseline: middle;\n font-family: Consolas, Cousine, \"Courier New\", monospace;\n font-size: ").concat(SVG_CONSTANTS.DEFAULT_FONT_SIZE, "px;\n text-anchor: middle;\n white-space: pre;\n fill: ").concat(theme.text, ";\n }\n #").concat(svgId, " .time-header {\n font-size: ").concat(SVG_CONSTANTS.TIME_HEADER_FONT_SIZE, "px;\n }\n\n /* Label box status colors and borders */\n #").concat(svgId, " .label-box.status-normal {\n fill: ").concat(theme.labelBox.normal, ";\n stroke: ").concat(theme.labelBoxBorder.normal, ";\n }\n #").concat(svgId, " .label-box.status-degraded {\n fill: ").concat(theme.labelBox.degraded, ";\n stroke: ").concat(theme.labelBoxBorder.degraded, ";\n }\n #").concat(svgId, " .label-box.status-critical {\n fill: ").concat(theme.labelBox.critical, ";\n stroke: ").concat(theme.labelBoxBorder.critical, ";\n }\n\n /* Quantity box status colors and borders */\n #").concat(svgId, " .quantity-box.status-normal {\n fill: ").concat(theme.quantityBox.normal, ";\n stroke: ").concat(theme.quantityBoxBorder.normal, ";\n }\n #").concat(svgId, " .quantity-box.status-degraded {\n fill: ").concat(theme.quantityBox.degraded, ";\n stroke: ").concat(theme.quantityBoxBorder.degraded, ";\n }\n #").concat(svgId, " .quantity-box.status-critical {\n fill: ").concat(theme.quantityBox.critical, ";\n stroke: ").concat(theme.quantityBoxBorder.critical, ";\n }\n\n /* Symbol status colors and borders */\n #").concat(svgId, " .symbol.status-normal {\n fill: ").concat(theme.symbols.normal, ";\n stroke: ").concat(theme.symbolsBorder.normal, ";\n }\n #").concat(svgId, " .symbol.status-degraded {\n fill: ").concat(theme.symbols.degraded, ";\n stroke: ").concat(theme.symbolsBorder.degraded, ";\n }\n #").concat(svgId, " .symbol.status-critical {\n fill: ").concat(theme.symbols.critical, ";\n stroke: ").concat(theme.symbolsBorder.critical, ";\n }\n </style>"); } }, { key: "renderNode", value: function renderNode(nodeId, position) { var node = this.nodeMap.get(nodeId); var svg = ""; // Render label box and text svg += this.renderLabelBox(position, node.status); svg += this.renderLabelText(position, node.label); // Skip identifier text for cleaner appearance // Render symbol if (node.type === NodeType.EVENT) { svg += this.renderEventSymbol(position, node); } else if (node.type === NodeType.GATE) { svg += this.renderGateSymbol(position, node); } // Render quantity box if probability or risk score exists if (node.probability !== undefined || node.riskScore !== undefined) { console.log("Rendering quantity box for ".concat(node.id, ": p=").concat(node.probability, ", r=").concat(node.riskScore)); svg += this.renderQuantityBox(position, node.status); svg += this.renderQuantityText(position, node); } return svg; } }, { key: "renderLabelBox", value: function renderLabelBox(position, status) { var x = position.x - SVG_CONSTANTS.LABEL_BOX_WIDTH / 2; var y = position.y - SVG_CONSTANTS.LABEL_BOX_HEIGHT / 2 + SVG_CONSTANTS.LABEL_BOX_Y_OFFSET; var statusClass = this.getStatusClass(status); return "<rect x=\"".concat(x, "\" y=\"").concat(y, "\" width=\"").concat(SVG_CONSTANTS.LABEL_BOX_WIDTH, "\" height=\"").concat(SVG_CONSTANTS.LABEL_BOX_HEIGHT, "\" class=\"label-box ").concat(statusClass, "\"/>"); } }, { key: "renderLabelText", value: function renderLabelText(position, label) { if (!label) return ""; var x = position.x; var y = position.y + SVG_CONSTANTS.LABEL_BOX_Y_OFFSET; // Simple text wrapping var words = label.split(" "); var lines = []; var currentLine = ""; var _iterator5 = _createForOfIteratorHelper(words), _step5; try { for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { var word = _step5.value; if (currentLine.length + word.length + 1 <= SVG_CONSTANTS.LABEL_MIN_LINE_LENGTH) { currentLine += (currentLine ? " " : "") + word; } else { if (currentLine) lines.push(currentLine); currentLine = word; } } } catch (err) { _iterator5.e(err); } finally { _iterator5.f(); } if (currentLine) lines.push(currentLine); var svg = ""; var lineHeight = SVG_CONSTANTS.DEFAULT_FONT_SIZE * SVG_CONSTANTS.DEFAULT_LINE_SPACING; var startY = y - (lines.length - 1) * lineHeight / 2; for (var i = 0; i < lines.length; i++) { var lineY = startY + i * lineHeight; svg += "<text x=\"".concat(x, "\" y=\"").concat(lineY, "\">").concat(this.escapeXML(lines[i]), "</text>"); } return svg; } }, { key: "renderQuantityBox", value: function renderQuantityBox(position, status) { var x = position.x - SVG_CONSTANTS.QUANTITY_BOX_WIDTH / 2 + 5; // 5 is a slight offset for x // Position directly over the label box, cover half var y = position.y + SVG_CONSTANTS.LABEL_BOX_Y_OFFSET + SVG_CONSTANTS.QUANTITY_BOX_HEIGHT / 2 + 2; // offset for border: 2 var statusClass = this.getStatusClass(status); return "<rect x=\"".concat(x, "\" y=\"").concat(y, "\" width=\"").concat(SVG_CONSTANTS.QUANTITY_BOX_WIDTH, "\" height=\"").concat(SVG_CONSTANTS.QUANTITY_BOX_HEIGHT, "\" class=\"quantity-box ").concat(statusClass, "\"/>"); } }, { key: "renderQuantityText", value: function renderQuantityText(position, node) { var x = position.x; // Match the same positioning as the quantity box - center over half of label box var y = position.y + SVG_CONSTANTS.LABEL_BOX_Y_OFFSET + SVG_CONSTANTS.QUANTITY_BOX_HEIGHT + 2; var lines = []; // Add probability if it exists if (node.probability !== undefined) { lines.push("p = ".concat(this.formatNumber(node.probability))); } // Add risk score if it exists if (node.riskScore !== undefined) { lines.push("r = ".concat(this.formatNumber(node.riskScore))); } if (lines.length === 0) return ""; var svg = ""; var lineHeight = SVG_CONSTANTS.DEFAULT_FONT_SIZE * SVG_CONSTANTS.DEFAULT_LINE_SPACING; var startY = y - (lines.length - 1) * lineHeight / 2; for (var i = 0; i < lines.length; i++) { var lineY = startY + i * lineHeight; svg += "<text x=\"".concat(x, "\" y=\"").concat(lineY, "\">").concat(this.escapeXML(lines[i]), "</text>"); } return svg; } }, { key: "formatNumber", value: function formatNumber(value) { if (typeof value === "number") { if (value === 0) return "0"; if (value >= 0.01) return value.toFixed(3); return value.toExponential(2); } return value.toString(); } }, { key: "renderEventSymbol", value: function renderEventSymbol(position, node) { var x = position.x; var y = position.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET; switch (node.eventType) { case EventType.UNDEVELOPED: return this.renderUndevelopedEvent(x, y, node.status); case EventType.HOUSE: return this.renderHouseEvent(x, y, node.status); case EventType.EXTERNAL: return this.renderUndevelopedEvent(x, y, node.status); // Use diamond for external events default: // BASIC return this.renderBasicEvent(x, y, node.status); } } }, { key: "renderGateSymbol", value: function renderGateSymbol(position, node) { var x = position.x; var y = position.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET; switch (node.gateType) { case GateType.AND: return this.renderAndGate(x, y, node.status); case GateType.OR: default: return this.renderOrGate(x, y, node.status); } } }, { key: "renderBasicEvent", value: function renderBasicEvent(x, y) { var status = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : NodeStatus.NORMAL; var statusClass = this.getStatusClass(status); return "<circle cx=\"".concat(x, "\" cy=\"").concat(y, "\" r=\"").concat(SVG_CONSTANTS.BASIC_EVENT_RADIUS, "\" class=\"symbol ").concat(statusClass, "\"/>"); } }, { key: "renderUndevelopedEvent", value: function renderUndevelopedEvent(x, y) { var status = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : NodeStatus.NORMAL; var statusClass = this.getStatusClass(status); var points = [[x, y - SVG_CONSTANTS.UNDEVELOPED_EVENT_HALF_HEIGHT], [x - SVG_CONSTANTS.UNDEVELOPED_EVENT_HALF_WIDTH, y], [x, y + SVG_CONSTANTS.UNDEVELOPED_EVENT_HALF_HEIGHT], [x + SVG_CONSTANTS.UNDEVELOPED_EVENT_HALF_WIDTH, y]].map(function (p) { return p.join(","); }).join(" "); return "<polygon points=\"".concat(points, "\" class=\"symbol ").concat(statusClass, "\"/>"); } }, { key: "renderHouseEvent", value: function renderHouseEvent(x, y) { var status = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : NodeStatus.NORMAL; var statusClass = this.getStatusClass(status); var points = [[x, y - SVG_CONSTANTS.HOUSE_EVENT_APEX_HEIGHT], [x - SVG_CONSTANTS.HOUSE_EVENT_HALF_WIDTH, y - SVG_CONSTANTS.HOUSE_EVENT_SHOULDER_HEIGHT], [x - SVG_CONSTANTS.HOUSE_EVENT_HALF_WIDTH, y + SVG_CONSTANTS.HOUSE_EVENT_BODY_HEIGHT], [x + SVG_CONSTANTS.HOUSE_EVENT_HALF_WIDTH, y + SVG_CONSTANTS.HOUSE_EVENT_BODY_HEIGHT], [x + SVG_CONSTANTS.HOUSE_EVENT_HALF_WIDTH, y - SVG_CONSTANTS.HOUSE_EVENT_SHOULDER_HEIGHT]].map(function (p) { return p.join(","); }).join(" "); return "<polygon points=\"".concat(points, "\" class=\"symbol ").concat(statusClass, "\"/>"); } }, { key: "renderOrGate", value: function renderOrGate(x, y) { var status = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : NodeStatus.NORMAL; var statusClass = this.getStatusClass(status); var apex_x = x; var apex_y = y - SVG_CONSTANTS.OR_GATE_APEX_HEIGHT; var left_x = x - SVG_CONSTANTS.OR_GATE_HALF_WIDTH; var right_x = x + SVG_CONSTANTS.OR_GATE_HALF_WIDTH; var ear_y = y - SVG_CONSTANTS.OR_GATE_NECK_HEIGHT; var toe_y = y + SVG_CONSTANTS.OR_GATE_BODY_HEIGHT; var left_slant_x = apex_x - SVG_CONSTANTS.OR_GATE_SLANT_RUN; var right_slant_x = apex_x + SVG_CONSTANTS.OR_GATE_SLANT_RUN; var slant_y = apex_y + SVG_CONSTANTS.OR_GATE_SLANT_DROP; var sling_y = ear_y - SVG_CONSTANTS.OR_GATE_SLING_RISE; var groin_x = x; var groin_y = toe_y - SVG_CONSTANTS.OR_GATE_GROIN_RISE; var path = ["M".concat(apex_x, ",").concat(apex_y), "C".concat(left_slant_x, ",").concat(slant_y, " ").concat(left_x, ",").concat(sling_y, " ").concat(left_x, ",").concat(ear_y), "L".concat(left_x, ",").concat(toe_y), "Q".concat(groin_x, ",").concat(groin_y, " ").concat(right_x, ",").concat(toe_y), "L".concat(right_x, ",").concat(ear_y), "C".concat(right_x, ",").concat(sling_y, " ").concat(right_slant_x, ",").concat(slant_y, " ").concat(apex_x, ",").concat(apex_y), "Z"].join(" "); return "<path d=\"".concat(path, "\" class=\"symbol ").concat(statusClass, "\"/>"); } }, { key: "renderAndGate", value: function renderAndGate(x, y) { var status = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : NodeStatus.NORMAL; var statusClass = this.getStatusClass(status); var left_x = x - SVG_CONSTANTS.AND_GATE_HALF_WIDTH; var right_x = x + SVG_CONSTANTS.AND_GATE_HALF_WIDTH; var ear_y = y - SVG_CONSTANTS.AND_GATE_NECK_HEIGHT; var toe_y = y + SVG_CONSTANTS.AND_GATE_BODY_HEIGHT; var sling_y = ear_y - SVG_CONSTANTS.AND_GATE_SLING_RISE; var path = ["M".concat(left_x, ",").concat(toe_y), "L".concat(right_x, ",").concat(toe_y), "L".concat(right_x, ",").concat(ear_y), "C".concat(right_x, ",").concat(sling_y, " ").concat(left_x, ",").concat(sling_y, " ").concat(left_x, ",").concat(ear_y), "L".concat(left_x, ",").concat(toe_y), "Z"].join(" "); return "<path d=\"".concat(path, "\" class=\"symbol ").concat(statusClass, "\"/>"); } }, { key: "getStatusClass", value: function getStatusClass(status) { switch (status) { case NodeStatus.CRITICAL: return "status-critical"; case NodeStatus.DEGRADED: return "status-degraded"; default: return "status-normal"; } } }, { key: "renderLabelConnectors", value: function renderLabelConnectors() { var svg = ""; // Render connector from each symbol to its label box var _iterator6 = _createForOfIteratorHelper(this.layout.nodes), _step6; try { for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { var _step6$value = _slicedToArray(_step6.value, 2), nodeId = _step6$value[0], position = _step6$value[1]; svg += this.renderLabelConnector(position); } } catch (err) { _iterator6.e(err); } finally { _iterator6.f(); } return svg; } }, { key: "renderLabelConnector", value: function renderLabelConnector(position) { var x = position.x; var labelBottom = position.y + SVG_CONSTANTS.LABEL_BOX_Y_OFFSET + SVG_CONSTANTS.LABEL_BOX_HEIGHT / 2; // Determine symbol top based on node type var nodeId = position.id; var node = this.nodeMap.get(nodeId); var symbolTop; if (node.type === NodeType.EVENT) { // For events switch (node.eventType) { case EventType.UNDEVELOPED: case EventType.EXTERNAL: symbolTop = position.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET - SVG_CONSTANTS.UNDEVELOPED_EVENT_HALF_HEIGHT; break; case EventType.HOUSE: symbolTop = position.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET - SVG_CONSTANTS.HOUSE_EVENT_APEX_HEIGHT; break; default: // BASIC symbolTop = position.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET - SVG_CONSTANTS.BASIC_EVENT_RADIUS; break; } } else if (node.type === NodeType.GATE) { // For gates switch (node.gateType) { case GateType.AND: symbolTop = position.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET - SVG_CONSTANTS.AND_GATE_SLING_RISE; break; default: // OR gate symbolTop = position.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET - SVG_CONSTANTS.OR_GATE_APEX_HEIGHT; break; } } else { // Fallback symbolTop = position.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET - SVG_CONSTANTS.OR_GATE_APEX_HEIGHT; } return "<polyline points=\"".concat(x, ",").concat(labelBottom, " ").concat(x, ",").concat(symbolTop, "\"/>"); } }, { key: "renderConnections", value: function renderConnections() { var svg = ""; var _iterator7 = _createForOfIteratorHelper(this.nodeMap), _step7; try { for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { var _step7$value = _slicedToArray(_step7.value, 2), nodeId = _step7$value[0], node = _step7$value[1]; if (node.type !== NodeType.GATE || !node.children || node.children.length === 0) continue; var gatePosition = this.layout.nodes.get(nodeId); if (!gatePosition) continue; // Render connector from gate symbol down to horizontal bus svg += this.renderGateConnector(gatePosition); // Render horizontal bus and connections to inputs svg += this.renderInputConnectors(gatePosition, node.children); } } catch (err) { _iterator7.e(err); } finally { _iterator7.f(); } return svg; } }, { key: "renderGateConnector", value: function renderGateConnector(gatePosition) { var x = gatePosition.x; var nodeId = gatePosition.id; var node = this.nodeMap.get(nodeId); var symbolBottom; if (node && node.type === NodeType.GATE && node.gateType === GateType.OR) { // For OR gates, connect to the curved bottom (higher point due to the arc) symbolBottom = gatePosition.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET + SVG_CONSTANTS.OR_GATE_BODY_HEIGHT - SVG_CONSTANTS.OR_GATE_GROIN_RISE; } else { // For AND gates and others, use the flat bottom symbolBottom = gatePosition.y + SVG_CONSTANTS.SYMBOL_Y_OFFSET + SVG_CONSTANTS.AND_GATE_BODY_HEIGHT; } var busY = gatePosition.y + SVG_CONSTANTS.INPUT_CONNECTOR_BUS_Y_OFFSET; return "<polyline points=\"".concat(x, ",").concat(symbolBottom, " ").concat(x, ",").concat(busY, "\"/>"); } }, { key: "renderInputConnectors", value: function renderInputConnectors(gatePosition, children) { var _this2 = this; var svg = ""; if (!children || children.length === 0) return svg; var gateX = gatePosition.x; var busY = gatePosition.y + SVG_CONSTANTS.INPUT_CONNECTOR_BUS_Y_OFFSET; // Calculate positions for inputs var inputPositions = children.map(function (child) { return _this2.layout.nodes.get(child.id); }).filter(function (pos) { return pos; }); if (inputPositions.length === 0) return svg; // Draw horizontal bus var leftmostX = Math.min.apply(Math, _toConsumableArray(inputPositions.map(function (pos) { return pos.x; }))); var rightmostX = Math.max.apply(Math, _toConsumableArray(inputPositions.map(function (pos) { return pos.x; }))); var busLeft = Math.min(leftmostX, gateX - SVG_CONSTANTS.SYMBOL_SLOTS_HALF_WIDTH); var busRight = Math.max(rightmostX, gateX + SVG_CONSTANTS.SYMBOL_SLOTS_HALF_WIDTH); svg += "<polyline points=\"".concat(busLeft, ",").concat(busY, " ").concat(busRight, ",").concat(busY, "\"/>"); // Draw vertical connections to each input var _iterator8 = _createForOfIteratorHelper(inputPositions), _step8; try { for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) { var inputPosition = _step8.value; var inputX = inputPosition.x; var inputTopY = inputPosition.y + SVG_CONSTANTS.LABEL_BOX_Y_OFFSET - SVG_CONSTANTS.LABEL_BOX_HEIGHT / 2; svg += "<polyline points=\"".concat(inputX, ",").concat(busY, " ").concat(inputX, ",").concat(inputTopY, "\"/>"); } } catch (err) { _iterator8.e(err); } finally { _iterator8.f(); } return svg; } }, { key: "escapeXML", value: function escapeXML(text) { return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;"); } }]); }(); // Main JSFTA-JSON class var JSFTA_JSON = /*#__PURE__*/function () { function JSFTA_JSON() { _classCallCheck(this, JSFTA_JSON); } return _createClass(JSFTA_JSON, null, [{ key: "parse", value: function parse(jsonData) { var parser = new JSONFaultTreeParser(); return parser.parse(jsonData); } }, { key: "render", value: function render(parsedData) { var theme = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "light"; var renderer = new JSONFaultTreeRenderer(parsedData, theme); return renderer.render(); } }, { key: "parseAndRender", value: function parseAndRender(jsonData) { var theme = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "light"; var parsedData = JSFTA_JSON.parse(jsonData); return JSFTA_JSON.render(parsedData, theme); } }, { key: "getAvailableThemes", value: function getAvailableThemes() { return Object.keys(COLOR_THEMES).map(function (key) { return { key: key, name: COLOR_THEMES[key].name }; }); } }]); }(); // Export for use as ES module /** * React component for rendering fault tree diagrams */ var FaultTreeDiagram = function FaultTreeDiagram(_ref) { var data = _ref.data, _ref$theme = _ref.theme, theme = _ref$theme === void 0 ? 'light' : _ref$theme, _ref$enableBlinking = _ref.enableBlinking, enableBlinking = _ref$enableBlinking === void 0 ? true : _ref$enableBlinking, _ref$className = _ref.className, className = _ref$className === void 0 ? '' : _ref$className, _ref$style = _ref.style, style = _ref$style === void 0 ? {} : _ref$style, _ref$onError = _ref.onError, onError = _ref$onError === void 0 ? null : _ref$onError, _ref$onRender = _ref.onRender, onRender = _ref$onRender === void 0 ? null : _ref$onRender; var containerRef = React.useRef(null); var _useState = React.useState(null), _useState2 = _slicedToArray(_useState, 2); _useState2[0]; var setError = _useState2[1]; React.useEffect(function () { if (!data || !containerRef.current) return; try { setError(null); // Parse and render the fault tree var svg = JSFTA_JSON.parseAndRender(data, theme); // Update container content containerRef.current.innerHTML = svg; // Apply blinking class if enabled if (enableBlinking) { containerRef.current.classList.add('fault-tree-blink'); } else { containerRef.current.classList.remove('fault-tree-blink'); } // Call onRender callback if provided if (onRender) { onRender(svg); } } catch (err) { var errorMessage = "Failed to render fault tree: ".concat(err.message); setError(errorMessage); if (containerRef.current) { containerRef.current.innerHTML = "<div style=\"color: red; padding: 10px; border: 1px solid red; border-radius: 4px;\">".concat(errorMessage, "</div>"); } // Call onError callback if provided if (onError) { onError(err); } } }, [data, theme, enableBlinking, onError, onRender]); return /*#__PURE__*/React.createElement("div", { ref: containerRef, className: "fault-tree-container ".concat(className), style: _objectSpread2({ width: '100%', height: 'auto', minHeight: '200px' }, style) }); }; /** * Hook for managing fault tree themes */ var useFaultTreeThemes = function useFaultTreeThemes() { var _useState3 = React.useState('light'), _useState4 = _slicedToArray(_useState3, 2), currentTheme = _useState4[0], setCurrentTheme = _useState4[1]; var _useState5 = React.useState([]), _useState6 = _slicedToArray(_useState5, 2), availableThemes = _useState6[0], setAvailableThemes = _useState6[1]; React.useEffect(function () { setAvailableThemes(JSFTA_JSON.getAvailableThemes()); }, []); var changeTheme = function changeTheme(themeName) { setCurrentTheme(themeName); }; return { currentTheme: currentTheme, availableThemes: availableThemes, changeTheme: changeTheme, setTheme: setCurrentTheme }; }; /** * Hook for managing blinking animation */ var useFaultTreeBlinking = function useFaultTreeBlinking() { var initialState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; var _useState7 = React.useState(initialState), _useState8 = _slicedToArray(_useState7, 2), isBlinking = _useState8[0], setIsBlinking = _useState8[1]; var toggleBlinking = function toggleBlinking() { setIsBlinking(function (prev) { return !prev; }); }; var enableBlinking = function enableBlinking() { setIsBlinking(true); }; var disableBlinking = function disableBlinking() { setIsBlinking(false); }; return { isBlinking: isBlinking, toggleBlinking: toggleBlinking, enableBlinking: enableBlinking, disableBlinking: disableBlinking, setBlinking: setIsBlinking }; }; /** * Higher-order component that provides fault tree functionality */ var withFaultTree = function withFaultTree(WrappedComponent) { return /*#__PURE__*/React.forwardRef(function (props, ref) { var themes = useFaultTreeThemes(); var blinking = useFaultTreeBlinking(); return /*#__PURE__*/React.createElement(WrappedComponent, _extends({ ref: ref }, props, { faultTree: _objectSpread2(_objectSpread2(_objectSpread2({}, themes), blinking), {}, { FaultTreeAnalyzer: JSFTA_JSON }) })); }); }; exports.default = FaultTreeDiagram; exports.useFaultTreeBlinking = useFaultTreeBlinking; exports.useFaultTreeThemes = useFaultTreeThemes; exports.withFaultTree = withFaultTree;