UNPKG

@attivio/suit

Version:

Attivio SUIT, the Search UI Toolkit, is a library for creating search clients for searching the Attivio platform.

385 lines (353 loc) 12.9 kB
var _class2, _temp; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } // @Flow import SearchDocument from '../api/SearchDocument'; import FieldNames from '../api/FieldNames'; import GraphNode from '../api/GraphNode'; import GraphEdge from '../api/GraphEdge'; import StringUtils from './StringUtils'; export var GraphDefinition = function GraphDefinition(nodes, edges) { _classCallCheck(this, GraphDefinition); this.nodes = nodes; this.edges = edges; }; var BASE_GRAPH_OPTIONS = { width: '100%', height: '100%', layout: { // randomSeed: 10000, improvedLayout: false }, configure: false, interaction: { dragNodes: true, hover: true, hoverConnectedEdges: false, multiselect: true, keyboard: false, navigationButtons: false, selectConnectedEdges: false, tooltipDelay: 100 }, edges: { chosen: false, color: { color: '#606060', hover: '#424242' }, smooth: false, width: 1 }, nodes: { borderWidth: 0, borderWidthSelected: 1, color: { border: '#fff', background: '#fff', highlight: { border: '#888', background: '#eee' }, hover: { border: '#888', background: '#eee' } }, font: { color: '#000', size: 11, face: 'arial', align: 'left' }, labelHighlightBold: false, shadow: false, shape: 'box', shapeProperties: { borderRadius: 0 } }, physics: { stabilization: { enabled: true, iterations: 5000, fit: true } }, groups: { document: { borderWidth: 0, font: { size: 14 }, margin: 10, color: { border: '#fff', background: '#fff', highlight: { border: '#888', background: '#eee' }, hover: { border: '#888', background: '#eee' } } } } }; var BASE_GROUP_OPTIONS = { borderWidth: 2, color: { border: '#57b3e2', background: '#fff', highlight: { border: '#57b3e2', background: '#eee' }, hover: { border: '#57b3e2', background: '#eee' } }, shapeProperties: { borderDashes: [4, 2] } }; /** * Utility functions used by the KnowledgeGraphPanel component to construct the queries * needed for populating it's graph. */ var KnowledgeGraphUtils = (_temp = _class2 = function () { function KnowledgeGraphUtils() { _classCallCheck(this, KnowledgeGraphUtils); } /** * Build up the query to use to get the knowledge graph's documents. The results of the * query will be the main document (the one whose ID is passed in) with any related * documents listed as its children. * * @param docId the ID of the document to look up. * @param table the name of the table containing the document, if you want to exclude * joining to other documents in the same table (pass null to join with * any document, regardless of its table) * @param linkingFields the list of fields to look for links in (e.g., entity fields) * @param maxLinkedDocs the maximum number of additional documents to return * @param entityName * @param entityValue */ KnowledgeGraphUtils.buildQuery = function buildQuery(docId, table, tableField, linkingFields, maxLinkedDocs, entityName, entityValue) { // eslint-disable-line max-len // We need to escape any backslashes in the document ID to ensure they pass through the query engine correctly var escapedDocId = docId.replace(/\\/g, '\\\\\\\\'); // If there is an entity name and value, query on those instead. if (entityName && entityValue) { // We boost the existing document ID so it will appear at the top of the result list return 'BOOST("' + entityName + '":"' + entityValue + '", ' + FieldNames.ID + ':"' + escapedDocId + '")'; } var primaryQuery = 'QUERY("' + FieldNames.ID + ':\\"' + escapedDocId + '\\"", qlang=advanced)'; // Don't join against documents from the same table as the primary one (if it has a table) var notTable = tableField && table ? 'NOT(' + tableField + ':"' + table + '"), ' : '*, '; var outerClauses = linkingFields.map(function (field) { return 'OUTER(' + notTable + 'on="' + field + '", alias=' + field + ', rollup=' + maxLinkedDocs + ')'; }); var outerClausesList = outerClauses.join(', '); var query = 'JOIN(' + primaryQuery + ', ' + outerClausesList + ')'; return query; }; KnowledgeGraphUtils.makeDocNode = function makeDocNode(doc, id) { var isPrimary = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var label = StringUtils.wrapLabel(doc.getFirstValue('title')); var docId = doc.getFirstValue(FieldNames.ID); var table = doc.getFirstValue('table'); var title = isPrimary ? table + ' [Main Document]' : 'Related ' + table + ' Document'; if (!label) { label = '[This document has no title]'; } var node = new GraphNode(id, label, title, 'document'); node.physics = false; if (isPrimary) { node.borderWidth = 1; node.color = { border: KnowledgeGraphUtils.MAIN_DOCUMENT_BORDER_COLOR }; node.shadow = { enabled: true, color: KnowledgeGraphUtils.MAIN_DOCUMENT_BORDER_COLOR, size: 10, x: 0, y: 0 }; } else { // Non-primary nodes can be double clicked, so we need to know their IDs node.docId = docId; } return node; }; KnowledgeGraphUtils.makeLinkingNode = function makeLinkingNode(fieldName, fieldValue, id) { var title = '' + fieldName.charAt(0).toLocaleUpperCase() + fieldName.slice(1); var node = new GraphNode(id, StringUtils.wrapLabel(fieldValue), title, fieldName); return node; }; /** * Create a graph from the search results. * The main document should have children that represent the others. * If firstIsPrimary is false, no document will be the primary (shadowed) one. * If filterOutSingletons is false, then external "leaf" entity nodes will be shown */ KnowledgeGraphUtils.searchResultsToGraph = function searchResultsToGraph(mainDoc, linkingFields) { var firstIsPrimary = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; var filterOutSingletons = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; var nodes = []; var edges = []; var docNodes = new Map(); var linkingNodes = new Map(); var nodeId = firstIsPrimary ? 0 : 1; // Get the parent and child docs into the same list... var allDocs = mainDoc.children; var newMainDoc = mainDoc; newMainDoc.children = []; allDocs.unshift(newMainDoc); // Remove any duplicate documents since they'll just muck up the graph var filteredDocs = []; allDocs.forEach(function (doc) { var existingDoc = filteredDocs.find(function (filteredDoc) { return doc.getFirstValue(FieldNames.ID) === filteredDoc.getFirstValue(FieldNames.ID); }); if (!existingDoc) { filteredDocs.push(doc); } }); // Loop through the docs filteredDocs.forEach(function (doc) { var docNode = KnowledgeGraphUtils.makeDocNode(doc, nodeId, nodeId === 0); nodeId += 1; nodes.push(docNode); docNodes.set(doc.getFirstValue(FieldNames.ID), docNode); // Loop through the linking fields... linkingFields.forEach(function (field) { var fieldValues = doc.getAllValues(field); if (fieldValues && fieldValues.length > 0) { // For each field value, add a node fieldValues.forEach(function (fieldValue) { // See if we already have a node for this field/value combination var linkingNodeKey = field + ':' + fieldValue; var existingNode = linkingNodes.get(linkingNodeKey); if (existingNode) { // Make sure the existing node isn't already connected to the document node // (some documents may have the same value multiple times in a given field) var existingEdge = edges.find(function (edge) { return edge.from === docNode.id && edge.to === existingNode.id; }); if (!existingEdge) { // Connect the existing node to the document node... edges.push(new GraphEdge(docNode.id, existingNode.id)); } } else { var newNode = KnowledgeGraphUtils.makeLinkingNode(field, fieldValue, nodeId); nodeId += 1; nodes.push(newNode); linkingNodes.set(linkingNodeKey, newNode); // Add an edge between the document node and the linking node edges.push(new GraphEdge(docNode.id, newNode.id)); } }); } }); }); // Now, position doc nodes var numDocNodes = docNodes.size; var step = 2 * Math.PI / numDocNodes; var angle = step / 2; // Starting point not straight up... var angledDocNodes = new Map(); var entries = Array.from(docNodes.entries()); entries.forEach(function (entry) { var dn = entry[1]; dn.x = Math.cos(angle) * 400 + 500; dn.y = Math.sin(angle) * 400 + 500; angle += step; angledDocNodes.set(entry[0], dn); }); if (filterOutSingletons) { // Filter out any nodes which only have one edge coming into them and which aren't document nodes... var nodesToEdgesMap = KnowledgeGraphUtils.calculateNodesToEdges(edges); var filteredNodes = nodes.filter(function (node) { if (node.group !== 'document') { var nodeEdges = nodesToEdgesMap.get(node.id); if (nodeEdges && nodeEdges.length > 1) { return true; } if (nodeEdges.length === 1) { if (nodeEdges[0].from === 0 || nodeEdges[0].to === 0) { // Also include nodes directly connected to the primary one (which always has ID 0) return true; } } return false; // Single edged node! } return true; }); nodes = filteredNodes; } return new GraphDefinition(nodes, edges); }; KnowledgeGraphUtils.calculateNodesToEdges = function calculateNodesToEdges(edges) { var result = new Map(); edges.forEach(function (edge) { var fromNodeEdgeList = result.get(edge.from); if (!fromNodeEdgeList) { fromNodeEdgeList = []; result.set(edge.from, fromNodeEdgeList); } fromNodeEdgeList.push(edge); var toNodeEdgeList = result.get(edge.to); if (!toNodeEdgeList) { toNodeEdgeList = []; result.set(edge.to, toNodeEdgeList); } toNodeEdgeList.push(edge); }); return result; }; // Generates a color hexcode based on ASCII codes of characters in a string; // The hex codes generated are unique for each string. KnowledgeGraphUtils.stringToColor = function stringToColor(str) { var hash = 0; var i = 0; for (i = 0; i < str.length; i += 1) { hash = str.charCodeAt(i) + ((hash << 5) - hash); // eslint-disable-line no-bitwise } var color = '#'; for (i = 0; i < 3; i += 1) { var value = hash >> i * 8 & 0xff; // eslint-disable-line no-bitwise color += ('00' + value.toString(16)).substr(-2); } return color; }; KnowledgeGraphUtils.calculateGraphOptions = function calculateGraphOptions() { var entityNames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var entityColors = arguments[1]; var options = JSON.parse(JSON.stringify(BASE_GRAPH_OPTIONS)); var groups = options.groups; entityColors.forEach(function (color, entity) { var entityOptions = JSON.parse(JSON.stringify(BASE_GROUP_OPTIONS)); entityOptions.color.border = color; entityOptions.color.highlight.border = color; entityOptions.color.hover.border = color; groups[entity] = entityOptions; }); entityNames.forEach(function (entityName) { if (!entityColors.has(entityName)) { // Make up a random color for this unknown entity var entityOptions = JSON.parse(JSON.stringify(BASE_GROUP_OPTIONS)); var customColor = KnowledgeGraphUtils.stringToColor(entityName); entityOptions.color.border = customColor; entityOptions.color.highlight.border = customColor; entityOptions.color.hover.border = customColor; groups[entityName] = entityOptions; } }); return options; }; return KnowledgeGraphUtils; }(), _class2.MAIN_DOCUMENT_BORDER_COLOR = '#333', _temp); export { KnowledgeGraphUtils as default };