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
JavaScript
;
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
}]);
}(); // 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;