Commit 2f82beb0 authored by Maximilian Lapie's avatar Maximilian Lapie

Merge branch 'maximilian/newGraph' into 'master'

Added a few things and bug fixes

See merge request !9
parents 44352114 e069416f
......@@ -307,40 +307,45 @@
}
],
"fieldNames": {
"dateFields": ["isoStartDate", "isoEndDate"],
"geoLocalisation": "latLong"
},
"nodeTypes2": {
"date": {
"labelField": "startDate",
"color": "label",
"color": "#FF7F00",
"icon": ""
},
"regest": {
"labelField": "ident",
"color": "label",
"color": "#FF7F00",
"icon": ""
},
"event": {
"labelField": "label",
"color": "label",
"color": "#FF55FF",
"icon": ""
},
"person": {
"labelField": "label",
"color": "label",
"color": "#FF0000",
"icon": ""
},
"place": {
"labelField": "normalizedGerman",
"color": "label",
"color": "#BB7000",
"icon": ""
},
"indPlace": {
"labelField": "label",
"color": "label",
"color": "#BB7000",
"icon": ""
},
"lemma": {
"labelField": "lemma",
"color": "label",
"color": "#41C4F4",
"icon": ""
},
"literature": {
......@@ -355,12 +360,12 @@
},
"thing": {
"labelField": "label",
"color": "label",
"color": "#00A000",
"icon": ""
},
"entry": {
"labelField": "label",
"color": "label",
"color": "#789FFF",
"icon": ""
}
},
......@@ -407,7 +412,8 @@
],
"filterable": {
"date": ["regest"]
"date": ["regest"],
"keywords": ["regest"]
},
"source": {
......
import IdSet from "../../misc/customTypes/IdSet"
export class Node2d {
constructor(id, label, type, color=null, data=null, size=1, hide=false){
this.id = id
this.label = label
this.type = type
this.targets = new Set()
this.parents = new Set()
this.targets = new IdSet()
this.parents = new IdSet()
this.color = color ? color : "#DCDCDC"
this.data = data
this.size = size
......@@ -24,46 +26,87 @@ export class Node2d {
})
getEdgesId = () => {
const targets = [...this.targets].map(targetId => `${this.id} ${targetId}`)
const parents = [...this.parents].map(parentId => `${parentId} ${this.id}`)
const targets = this.targets.ids().map(targetId => `${this.id} ${targetId}`)
const parents = this.parents.ids().map(parentId => `${parentId} ${this.id}`)
return targets.concat(parents)
}
getParents = () => [...this.parents]
getParents = (relationType="all") => this.parents.toArray(link => relationType === "all" || link.type === relationType)
getTargets = () => [...this.targets]
getTargets = (relationType="all") => this.targets.toArray(link => relationType === "all" || link.type === relationType)
getRelated= () => [...this.getParents(), ...this.getTargets()]
getRelated= (relationType="all") => [...this.getParents(relationType), ...this.getTargets(relationType)]
addTarget = targetId => {this.targets.add(targetId)}
addTarget = targetObj => this.targets.addOne(targetObj)
addParent = parentId => {this.parents.add(parentId)}
addParent = parentObj => this.parents.addOne(parentObj)
removeTarget = targetId => {this.targets.delete(targetId)}
removeTargets = targetIds => this.targets.removeByIds(targetIds)
removeParent = parentId => {this.parents.delete(parentId)}
removeParents = parentIds => this.parents.removeByIds(parentIds)
childrenSummary = (nodes, excludeHidden=false) => {
let valueCounts = {}
nodes.collectFromKeys([...this.targets]).forEach(n => {
let valueCounts = {nodes: {}, links: {}}
nodes.collectFromKeys(this.targets.ids()).forEach(n => {
if (!(n.excluded && excludeHidden)){
if (valueCounts[n.type] && !(n.excluded && excludeHidden))
valueCounts[n.type] += 1
if (valueCounts.nodes[n.type] && !(n.excluded && excludeHidden))
valueCounts.nodes[n.type] += 1
else
valueCounts[n.type] = 1
valueCounts.nodes[n.type] = 1
}})
this.targets.forEachEntry(link => {
if (valueCounts.links[link.type])
valueCounts.links[link.type] += 1
else
valueCounts.links[link.type] = 1
})
return valueCounts
}
parentSummary = (nodes, excludeHidden=false) => {
let valueCounts = {}
nodes.collectFromKeys([...this.parents]).forEach(n => {
let valueCounts = {nodes: {}, links: {}}
nodes.collectFromKeys(this.parents.ids()).forEach(n => {
if (!(n.excluded && excludeHidden)){
if (valueCounts[n.type])
valueCounts[n.type] += 1
if (valueCounts.nodes[n.type] && !(n.excluded && excludeHidden))
valueCounts.nodes[n.type] += 1
else
valueCounts[n.type] = 1
}})
valueCounts.nodes[n.type] = 1
}})
this.parents.forEachEntry(link => {
if (valueCounts.links[link.type])
valueCounts.links[link.type] += 1
else
valueCounts.links[link.type] = 1
})
return valueCounts
}
relatedSummary = (nodes, excludeHidden=false) => {
let valueCounts = {nodes: {}, links: {}}
const relatedIds = this.parents.ids().concat(this.targets.ids())
nodes.collectFromKeys(relatedIds).forEach(n => {
if (!(n.excluded && excludeHidden)){
if (valueCounts.nodes[n.type] && !(n.excluded && excludeHidden))
valueCounts.nodes[n.type] += 1
else
valueCounts.nodes[n.type] = 1
}})
this.parents.forEachEntry(link => {
if (valueCounts.links[link.type])
valueCounts.links[link.type] += 1
else
valueCounts.links[link.type] = 1
})
this.targets.forEachEntry(link => {
if (valueCounts.links[link.type])
valueCounts.links[link.type] += 1
else
valueCounts.links[link.type] = 1
})
return valueCounts
}
}
......
This diff is collapsed.
......@@ -7,7 +7,6 @@ export class Graph2D {
edges;
selectedNodeId;
selectedNodeColor;
expandedNodes;
hoverTimeout;
constructor(dbConnector, idList, cullList){
......@@ -15,7 +14,7 @@ export class Graph2D {
this.graph2d = new Graph2d(dbConnector, idList, cullList);
this.cy = null;
this.nodes = [];
this.expandedNodes = new IdSet();
this.showClearMenu = true;
}
showGraph2D = () => {
......@@ -37,12 +36,12 @@ export class Graph2D {
'background-color': 'data(color)',
'text-background-color': 'white',
'text-background-opacity': 1,
'text-background-padding': '1px',
'text-background-padding': '5px',
'text-border-opacity': 1,
'text-border-color': 'white',
'text-border-width': '1px',
'text-border-style': 'solid',
'text-margin-y': '-2px'
'text-margin-y': '-6px'
}
},
{
......@@ -64,7 +63,7 @@ export class Graph2D {
}
});
var layout = this.cy.layout({
let layout = this.cy.layout({
name: 'cose',
nodeDimensionsIncludeLabels: true,
animate: false,
......@@ -76,11 +75,12 @@ export class Graph2D {
this.filterByDate([document.getElementById('filter-date-start'), document.getElementById('filter-date-end')]);
this.applyFilterToGraph();
this.cy.elements().nodes().on('cxttap', (e) => this.displayContextMenu(e));
this.cy.elements().nodes().on('mouseover', (e) => this.highlightNode(e));
this.cy.elements().nodes().on('mouseout', (e) => this.deEmphasizeNode(e));
this.cy.on('tapstart', (e) => { this.hideContextMenu(e); this.hideGraphMenu(e); });
this.cy.on('cxttap', this.displayGraphMenu);
this.cy.on('tapstart', this.hideContextMenu);
this.cy.on('cxttap', 'node', this.displayContextMenu);
this.cy.on('mouseover', 'node', this.highlightNode);
this.cy.on('mouseout', 'node', this.deEmphasizeNode);
}
showTooltip = () => {
......@@ -102,6 +102,8 @@ export class Graph2D {
});
associatedSearchRows.map(row => row.getElementsByTagName("td")[0].style.backgroundColor = "rgb(255, 255, 70)");
this.showTooltip();
this.showClearMenu = false;
}
hideTooltip = () => {
......@@ -123,15 +125,17 @@ export class Graph2D {
});
associatedSearchRows.map(row => row.getElementsByTagName("td")[0].style.backgroundColor = this.selectedNodeColor + "aa");
this.hideTooltip();
this.showClearMenu = true;
}
deleteNode = () => {
this.hideContextMenu();
this.graph2d.removeComponents([this.selectedNodeId])((type, id) => {
if (type === "node") {
this.cy.remove('node[id = \'' + id + '\']')
}
});
this.graph2d.mutate().then(() => this.hideContextMenu());
}
displayNewElements = (type, element) => {
......@@ -144,10 +148,7 @@ export class Graph2D {
group: 'nodes',
data: element,
position: { x: cX + 200 * Math.cos(a), y: cY + 200 * Math.sin(a) }
})
.on('cxttap', (e) => this.displayContextMenu(e))
.on('mouseover', (e) => this.highlightNode(e))
.on('mouseout', (e) => this.deEmphasizeNode(e));
});
} else {
this.cy.add({
group: 'edges',
......@@ -157,77 +158,53 @@ export class Graph2D {
}
}
expandOrCollapse = (type, summaryNodeTypes, collapseFunction, getNodesFunction) => {
expandNodes = (id, nodeType='all', relationType="all") => {
this.hideContextMenu();
let expandedNode = this.expandedNodes.get(this.selectedNodeId);
let displayNodes = true;
if (expandedNode) {
if (expandedNode.val.has(type)) {
collapseFunction([this.selectedNodeId], type);
expandedNode.val.delete(type);
displayNodes = false;
} else {
if (type === "all") {
if (summaryNodeTypes.length !== expandedNode.val.size) {
summaryNodeTypes.forEach(expandedNode.val.add, expandedNode.val);
} else {
collapseFunction([this.selectedNodeId], type);
summaryNodeTypes.forEach(expandedNode.val.delete, expandedNode.val);
displayNodes = false;
}
} else {
expandedNode.val.add(type);
}
}
} else {
const newExpandedNode = new Set();
if (type === "all") {
summaryNodeTypes.forEach(newExpandedNode.add, newExpandedNode);
} else {
newExpandedNode.add(type);
}
this.expandedNodes.addOne({id: this.selectedNodeId, val: newExpandedNode});
}
if (displayNodes) {
getNodesFunction([this.selectedNodeId], type)(this.displayNewElements);
this.graph2d.mutate().then(() => {
this.applyFilterToGraph();
});
} else {
this.graph2d.getRelated([id], nodeType, relationType)(this.displayNewElements);
this.graph2d.mutate().then(() => {
this.applyFilterToGraph();
}
}
displayNodeParents = (type = "all") => {
const parentsSummaryTypes = Object.keys(this.graph2d.getNodeSummary(this.selectedNodeId).parents);
this.expandOrCollapse(type, parentsSummaryTypes, this.graph2d.collapseParents, this.graph2d.getParents);
});
}
displayNodeChildren = (type = "all") => {
const childrenSummaryTypes = Object.keys(this.graph2d.getNodeSummary(this.selectedNodeId).children);
this.expandOrCollapse(type, childrenSummaryTypes, this.graph2d.collapseChildren, this.graph2d.getChildren);
collapseNodes = (id, nodeType='all', relationType="all") => {
this.hideContextMenu();
this.graph2d.collapseRelated([id], nodeType, relationType);
this.applyFilterToGraph();
}
getContextSubMenu(menuTitle, menuElements, clickFunction, subMenuDom) {
if (Object.keys(menuElements).length !== 0) {
var newDiv = document.createElement("div");
newDiv.className = "menuItem";
newDiv.innerHTML = "<i class='fa fa-fw'></i><b>" + menuTitle + "</b>";
newDiv.addEventListener('click', () => clickFunction());
extraInfo.appendChild(newDiv);
for (let menuElement in menuElements) {
newDiv = document.createElement("div");
newDiv.className = "menuItem";
let elementIcon = this.dbConnector.settings.nodeTypes.find(x => x.name === menuElement).icon;
newDiv.innerHTML = "<i class='fa fa-fw " + elementIcon + "'></i>" + menuElement + "(" + menuElements[menuElement] + ")";
newDiv.addEventListener('click', () => clickFunction(menuElement));
subMenuDom.appendChild(newDiv);
}
getContextSubMenu = (menuTitle, menuElements, clickFunction, subMenuDom) => {
let newDiv = document.createElement("div");
newDiv.innerHTML = "<i class='fa fa-fw'></i><b>" + menuTitle + "</b>";
let nodeNb = 0;
for (let node in menuElements.nodes) {
nodeNb += menuElements.nodes[node];
}
let newSubDiv = document.createElement("div");
newSubDiv.className = "menuItem";
newSubDiv.innerHTML = "<i class='fa fa-fw'></i><i class='fa fa-fw'></i>All(" + nodeNb + ")";
newSubDiv.addEventListener('click', () => clickFunction(this.selectedNodeId));
newDiv.appendChild(newSubDiv);
for (let node in menuElements.nodes) {
let newSubDiv = document.createElement("div");
newSubDiv.className = "menuItem";
newSubDiv.innerHTML = "<i class='fa fa-fw'></i><i class='fa fa-fw'></i>" + node + "(" + menuElements.nodes[node] + ")";
newSubDiv.addEventListener('click', () => clickFunction(this.selectedNodeId, node));
newDiv.appendChild(newSubDiv);
}
for (let link in menuElements.links) {
let newSubDiv = document.createElement("div");
newSubDiv.className = "menuItem";
newSubDiv.innerHTML = "<i class='fa fa-fw'></i><i class='fa fa-fw'></i>" + link + "(" + menuElements.links[link] + ")";
newSubDiv.addEventListener('click', () => clickFunction(this.selectedNodeId, "all", link));
newDiv.appendChild(newSubDiv);
}
subMenuDom.appendChild(newDiv);
}
displayContextMenu = (e) => {
var d = document.getElementById("contextMenu");
this.hideGraphMenu();
let d = document.getElementById("contextMenu");
d.style.position = "absolute";
d.style.left = e.renderedPosition.x + 'px';
d.style.top = e.renderedPosition.y + 'px';
......@@ -239,12 +216,36 @@ export class Graph2D {
extraInfo.innerHTML = '';
const nodeSummary = this.graph2d.getNodeSummary(this.selectedNodeId);
this.getContextSubMenu("Parents", nodeSummary.parents, this.displayNodeParents, extraInfo);
this.getContextSubMenu("Children", nodeSummary.children, this.displayNodeChildren, extraInfo);
this.getContextSubMenu("Expand", nodeSummary, this.expandNodes, extraInfo);
this.getContextSubMenu("Collapse", nodeSummary, this.collapseNodes, extraInfo);
}
clearGraph = () => {
this.graph2d = new Graph2d(this.dbConnector, [], []);
this.hideGraphMenu();
this.addNodeIds([]);
}
hideGraphMenu = () => {
let d = document.getElementById("graph2DMenu");
d.style.display = 'none';
}
displayGraphMenu = (e) => {
if (this.showClearMenu) {
this.hideContextMenu();
let d = document.getElementById("graph2DMenu");
d.style.position = "absolute";
d.style.left = e.renderedPosition.x + 'px';
d.style.top = e.renderedPosition.y + 'px';
d.style.display = 'inline-block';
document.getElementById("clear2DGraph").addEventListener("click", this.clearGraph);
}
}
hideContextMenu = () => {
var d = document.getElementById("contextMenu");
let d = document.getElementById("contextMenu");
d.style.display = 'none';
}
......@@ -264,7 +265,7 @@ export class Graph2D {
filterByDate = (dateValues) => {
const startDate = new Date(dateValues[0].innerHTML);
const endDate = new Date(dateValues[1].innerHTML);
this.graph2d.filterByDate(startDate, endDate);
this.graph2d.filter([], startDate, endDate);
this.applyFilterToGraph();
}
}
\ No newline at end of file
......@@ -370,6 +370,14 @@ intergraph .tooltip-arrow {
border: 1px solid black;
background-color: white;
display: none;
z-index: 2;
}
#graph2DMenu {
border: 1px solid black;
background-color: white;
display: none;
z-index: 2;
}
#nodeId {
......@@ -393,7 +401,7 @@ intergraph .tooltip-arrow {
#load-autocompletion {
position: absolute;
left: 0;
display: flex;
display: none;
height: 100vh;
width: 100%;
align-items: center;
......
......@@ -77,10 +77,11 @@ function setupDateFilter(app, newGraph, id, start, end) {
function start(settings, login, password) {
let url = settings.database.url;
let dbConnector = new dbConnectorConstructor(settings, url, login, password);
document.getElementById("load-autocompletion").style.display = "flex";
const entrySearch = new EntrySearch(dbConnector, () => {
document.getElementById("load-autocompletion").style.visibility = "hidden";
document.getElementById("load-autocompletion").style.display = "none";
$('#keyword').focus();
})
});
dbConnector.test().then(result => {
if (!result) {
......@@ -91,8 +92,7 @@ function start(settings, login, password) {
let app = new App(dbConnector);
const graph2D = new Graph2D(dbConnector, [], []);
//testGraph2d(dbConnector)
testGraph2d(dbConnector)
// Setup interface
$('#db_name').html(settings.database.name);
//$('#keyword').focus();
......@@ -224,9 +224,12 @@ function start(settings, login, password) {
$('#generate-btn').show();
$('#addToGraphButton').hide();
$('#contextMenu').hide();
$('#graph2DMenu').hide();
}
});
$('#graphselector #cytocanvas').trigger("click"); // To set cytocanvas as default canvas
$( "#queryButton" ).click(query);
$( "#keyword" ).keypress(function(e) {
if(e.which == 13) {
......@@ -400,6 +403,11 @@ function start(settings, login, password) {
}
requestAnimationFrame(animate);
$("#filter-content").on("input", () => {
const keywords = $("#filter-content").val().split(" ")
graph2D.graph2d.filterByKeywords(keywords)
graph2D.applyFilterToGraph()
})
return app;
}
......@@ -425,6 +433,8 @@ module.exports = {
settingsList.forEach((s, i) => {
options[i] = s.database.name;
});
// setTimeout(() => $(".swal2-confirm").click(), 100)
// setTimeout(() => $(".swal2-confirm").click(), 150)
return alert.select('Choose a database : ', null, options).then((choice) => {
let settings = settingsList[choice];
return alert.credentials('Connexion', settings.database.login, !!settings.database.password ? settings.database.password : "").then((credentials) => {
......@@ -433,5 +443,6 @@ module.exports = {
return app;
});
});
}
};
\ No newline at end of file
......@@ -75,8 +75,8 @@
<button type="button" class="btn" id="toggle-right-panel-canvas"><i class="fa fa-chevron-left" aria-hidden="true"></i></button>
<div id="topButtons">
<div id="graphselector" class="btn-group btn-group-toggle">
<button type="button" id="threecanvas" class="btn active">Threecanvas</button>
<button type="button" id="cytocanvas" class="btn">Cytocanvas</button>
<button type="button" id="cytocanvas" class="btn active">Cytocanvas</button>
<button type="button" id="threecanvas" class="btn">Threecanvas</button>
</div>
<div id="generate-btn-container" hidden>
<button id="generate-btn" class="btn btn-primary" disabled>Generate graph</button>
......@@ -101,8 +101,12 @@
<div id="cy"></div>
<div id="contextMenu" oncontextmenu="return false">
<div><i class="fa fa-fw"></i><b>Node</b></div>
<div class="menuItem" id="deleteNode"><i class="fa fa-trash fa-fw"></i>Remove node</div>
<div id="extraInfo"></div>
<div class="menuItem" id="deleteNode"><i class="fa fa-trash fa-fw"></i>Remove node</div>
</div>
<div id="graph2DMenu" oncontextmenu="return false">
<div><i class="fa fa-fw"></i><b>Graph</b></div>
<div class="menuItem" id="clear2DGraph"><i class="fa fa-fw"></i>Clear Graph</div>
</div>
</div>
<div id="filters">
......
......@@ -74,7 +74,7 @@ module.exports = class Queries{
let types = this.settings.keywordSearch.selection.map((e) => e.layer);
let i = types.indexOf(type);
let query = this.settings.keywordSearch.selection[i].query;
keyword = keyword.replace("(", "\\\\(").replace(")", "\\\\)").replace(".", "\\\\.")
keyword = keyword.replace("(", "\\\\(").replace(")", "\\\\)").replace(".", "\\\\.").replace("]", "\\\\]").replace("[", "\\\\[")
query = query.replace(/%KEYWORD%/g, keyword);
query = query.replace(/%CULLS%/g, culls);
return query;
......
/**
* This module defines a new IdSet Class
* The IdSet class is made to behave like a Set, but the uses the field "id" of a class as key
* This allows us to use a Set like class but with object rather than primitive types
* This allows us to use a Set-like class but with object rather than primitive types
* */
export default class IdSet {
constructor(iterable){
this._entries = {}
if (iterable)
iterable.forEach(obj => {this._entries[obj.id] = obj})
this._keys = new Set()
if (iterable){
iterable.forEach(obj => {
this._entries[obj.id] = obj
this._keys.add(obj.id)
})
}
}
ids = () => [...this._keys]
addOne = obj => {
if (obj && !this._entries[obj.id])
if (obj && !this._keys.has(obj.id)){
this._entries[obj.id] = obj
this._keys.add(obj.id)
}
}
get = (id, condFn=null) => {
......@@ -26,6 +35,7 @@ export default class IdSet {
return elem
}
removeOne = obj => {
this._keys.delete(obj.id)
delete this._entries[obj.id]
}
......@@ -33,6 +43,9 @@ export default class IdSet {
collectFromKeys = idList => idList.map(id => this._entries[id])
/**
* if condFn is true, call f1 else call f2
*/
filterApply = (condFn, f1, f2) => {
for (let id in this._entries){
if (condFn(this._entries[id]))
......@@ -43,7 +56,7 @@ export default class IdSet {
}
forEachEntry = (f) => {
[...this._entries].forEach(e => f(e))
Object.values(this._entries).forEach(e => f(e))
}
remove = objs => objs.forEach(obj => this.removeOne(obj))
......@@ -52,6 +65,7 @@ export default class IdSet {
removeByIds = idList => {
idList.forEach(id => {
this._keys.delete(id)
delete this._entries[id]
})
}
......
......@@ -32,7 +32,7 @@ export class EntrySearch {
getNodeName = (node, type) => node.properties[this._dbConnector.settings.nodeTypes2[type].labelField]
searchEntry = (searchString, type) => {
if (this._entries[type])
if (this._entries[type] && searchString.length > 2)
return this._entries[type].filter(x => x && x.toLowerCase().includes(searchString.toLowerCase()))
else
return []
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment