Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add display configuration options to the Item Graph #813

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions pydatalab/src/pydatalab/routes/v0_1/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ def get_graph_cy_format(item_id: Optional[str] = None, collection_id: Optional[s
drawn_elements = set()
node_collections = set()
for document in all_documents:
for relationship in document.get("relationships", []):
# for some reason, document["relationships"] is sometimes equal to None, so we
# need this `or` statement.
for relationship in document.get("relationships") or []:
# only considering child-parent relationships
if relationship.get("type") == "collections" and not collection_id:
collection_data = flask_mongo.db.collections.find_one(
Expand Down Expand Up @@ -113,7 +115,7 @@ def get_graph_cy_format(item_id: Optional[str] = None, collection_id: Optional[s
)
continue

for relationship in document.get("relationships", []):
for relationship in document.get("relationships") or []:
# only considering child-parent relationships:
if relationship.get("relation") not in ("parent", "is_part_of"):
continue
Expand Down
3 changes: 2 additions & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
"crypto-browserify": "^3.12.0",
"cytoscape": "^3.23.0",
"cytoscape-cola": "^2.5.1",
"cytoscape-dagre": "^2.4.0",
"cytoscape-elk": "^2.1.0",
"cytoscape-euler": "^1.2.3",
"cytoscape-fcose": "^2.2.0",
"date-fns": "^2.29.3",
"highlight.js": "^11.7.0",
"markdown-it": "^13.0.1",
Expand Down
1 change: 1 addition & 0 deletions webapp/src/components/CollectionSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
multiple
label="collection_id"
:filterable="false"
placeholder="type to search..."
@search="debouncedAsyncSearch"
>
<template #no-options="{ searching }">
Expand Down
239 changes: 183 additions & 56 deletions webapp/src/components/ItemGraph.vue
Original file line number Diff line number Diff line change
@@ -1,61 +1,126 @@
<template>
<div v-if="showOptions" class="dflex text-right">
<div class="btn-group mr-2" role="group">
<button
:class="graphStyle == 'elk-stress' ? 'btn btn-default active' : 'btn btn-default'"
@click="graphStyle = 'elk-stress'"
>
stress
</button>
<button
:class="graphStyle == 'cola' ? 'btn btn-default active' : 'btn btn-default'"
@click="graphStyle = 'cola'"
>
force
</button>
<button
:class="graphStyle == 'elk-layered-down' ? 'btn btn-default active' : 'btn btn-default'"
@click="graphStyle = 'elk-layered-down'"
>
horizontal
</button>
<button
:class="graphStyle == 'elk-layered-right' ? 'btn btn-default active' : 'btn btn-default'"
@click="graphStyle = 'elk-layered-right'"
>
vertical
</button>
<div v-if="showOptions" class="sidebar ml-4">
<button
class="btn btn-default options-button mb-2"
@click="optionsDisplayed = !optionsDisplayed"
>
configure
</button>
<div v-show="optionsDisplayed">
<label for="graph-style"
>Graph layout:
<font-awesome-icon v-show="layoutIsRunning" class="ml-2 text-muted" icon="spinner" spin
/></label>
<div id="graph-style" class="btn-group mr-2" role="group">
<button
:class="graphStyle == 'euler' ? 'btn btn-default active' : 'btn btn-default'"
@click="
graphStyle = 'euler';
updateAndRunLayout();
"
>
euler
</button>
<button
:class="graphStyle == 'cola' ? 'btn btn-default active' : 'btn btn-default'"
@click="
graphStyle = 'cola';
updateAndRunLayout();
"
>
cola
</button>
<button
:class="graphStyle == 'fcose' ? 'btn btn-default active' : 'btn btn-default'"
@click="
graphStyle = 'fcose';
updateAndRunLayout();
"
>
fCoSE
</button>
<button
:class="graphStyle == 'elk-disco' ? 'btn btn-default active' : 'btn btn-default'"
@click="
graphStyle = 'elk-disco';
updateAndRunLayout();
"
>
pack
</button>
<button
:class="graphStyle == 'random' ? 'btn btn-default active' : 'btn btn-default'"
@click="
graphStyle = 'random';
updateAndRunLayout();
"
>
random
</button>
<button
:class="graphStyle == 'elk-stress' ? 'btn btn-default active' : 'btn btn-default'"
@click="
graphStyle = 'elk-stress';
updateAndRunLayout();
"
>
stress (slow)
</button>
</div>

<label for="ignore-items">Ignore connections to items:</label>
<ItemSelect
id="ignore-items"
v-model="ignoreItems"
multiple
@option:selected="removeItemFromGraph"
@option:deselected="readdItemToGraph"
/>

<label for="ignore-collections">Ignore connections to collections:</label>
<CollectionSelect id="ignore-collections" v-model="ignoreCollections" multiple />

<div class="form-group form-check mt-3">
<input
id="label-starting-materials-by-name"
v-model="labelStartingMaterialsByName"
class="form-check-input"
type="checkbox"
/>
<label class="form-check-label" for="label-starting-materials-by-name">
label starting materials by name</label
>
</div>
</div>
</div>
<div id="cy" v-bind="$attrs" />
</template>

<script>
// import { getItemGraph } from "@/server_fetch_utils.js";
import ItemSelect from "@/components/ItemSelect.vue";
import CollectionSelect from "@/components/CollectionSelect.vue";
import { itemTypes } from "@/resources.js";
import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre";
import cola from "cytoscape-cola";
import elk from "cytoscape-elk";
import euler from "cytoscape-euler";
import fcose from "cytoscape-fcose";

cytoscape.use(dagre);
cytoscape.use(cola);
cytoscape.use(euler);
cytoscape.use(elk);
cytoscape.use(fcose);

const layoutOptions = {
"elk-layered-down": {
"elk-disco": {
name: "elk",
animate: true,
elk: {
algorithm: "layered",
"elk.direction": "DOWN",
algorithm: "disco",
},
},
"elk-layered-right": {
name: "elk",
elk: {
algorithm: "layered",
"elk.direction": "RIGHT",
},
random: {
name: "random",
},
"elk-stress": {
name: "elk",
Expand All @@ -65,19 +130,36 @@ const layoutOptions = {
},
cola: {
name: "cola",
animate: "true",
animate: true,
centerGraph: false,
},
euler: {
name: "euler",
animate: true,
pull: 0.002,
},
fcose: {
name: "fcose",
animate: true,
randomize: false,
packComponents: true,
},
};

export default {
name: "ItemGraph",
components: {
ItemSelect,
CollectionSelect,
},
props: {
graphData: {
type: Object,
default: null,
},
defaultGraphStyle: {
type: String,
default: "elk-stress",
default: "euler",
},
showOptions: {
type: Boolean,
Expand All @@ -87,26 +169,57 @@ export default {
data() {
return {
graphStyle: this.defaultGraphStyle,
optionsDisplayed: false,
ignoreItems: [],
removedNodeData: {},
ignoreCollections: [],
labelStartingMaterialsByName: true,
layoutIsRunning: true,
};
},
watch: {
graphData() {
this.generateCyNetworkPlot();
},
graphStyle() {
console.log("graphStyle changed");
ignoreCollections() {
this.generateCyNetworkPlot();
},
labelStartingMaterialsByName() {
this.generateCyNetworkPlot();
},
},
async mounted() {
this.generateCyNetworkPlot();
async created() {
if (typeof this.cy !== "undefined") {
this.generateCyNetworkPlot();
}
},
methods: {
removeItemFromGraph(event) {
const itemToIgnore = event[event.length - 1];
const node = this.cy.$(`node[id="${itemToIgnore.item_id}"]`);
if (node) {
this.removedNodeData[itemToIgnore.item_id] = this.cy.remove(
node.union(node.connectedEdges()),
);
}
},
readdItemToGraph(event) {
if (this.removedNodeData && this.removedNodeData[event.item_id]) {
this.removedNodeData[event.item_id].restore();
delete this.removedNodeData[event.item_id];
}
},
updateAndRunLayout() {
this.layout && this.layout.stop();
this.layoutIsRunning = true;
this.layout = this.cy.layout(layoutOptions[this.graphStyle]);
this.layout.run();
},
generateCyNetworkPlot() {
if (!this.graphData) {
return;
}
var cy = cytoscape({
this.cy = cytoscape({
container: document.getElementById("cy"),
elements: this.graphData,
userPanningEnabled: true,
Expand All @@ -120,11 +233,15 @@ export default {
{
selector: "node",
style: {
"background-color": "#11479e",
label: "data(id)",
},
},

{
selector: 'node[type = "starting_materials"]',
style: {
label: this.labelStartingMaterialsByName ? "data(name)" : "data(id)",
},
},
{
selector: "edge",
style: {
Expand All @@ -140,7 +257,7 @@ export default {
});

// set colors of each of the nodes by type
cy.nodes().each(function (element) {
this.cy.nodes().each(function (element) {
element.style(
"background-color",
element.data("special") == 1
Expand All @@ -152,22 +269,29 @@ export default {
element.style("shape"), element.data("shape") == "triangle" ? "triangle" : "ellipse";
});

this.cy.on("layoutstart", () => {
this.layoutIsRunning = true;
});

this.cy.on("layoutstop", () => {
this.layoutIsRunning = false;
});

// tapdragover and tapdragout are mouseover and mouseout events
// that also work with touch screens
cy.on("tapdragover", "node", function (evt) {
this.cy.on("tapdragover", "node", function (evt) {
var node = evt.target;
node.style("opacity", 0.8);
node.style("border-width", 3);
node.style("border-color", "black");
});
cy.on("tapdragout", "node", function (evt) {
this.cy.on("tapdragout", "node", function (evt) {
var node = evt.target;
node.style("opacity", 1);
node.style("border-width", node.data("special") == 1 ? 2 : 0);
node.style("border-color", "grey");
});

cy.on("click", "node", function (evt) {
this.cy.on("click", "node", function (evt) {
var node = evt.target;
if (node.data("type") == "collections") {
window.open(`/collections/${node.data("id").replace("Collection: ", "")}`, "_blank");
Expand All @@ -180,14 +304,17 @@ export default {
};
</script>

<style>
#flex-container {
flex-flow: column;
<style scoped>
.sidebar {
position: fixed;
max-width: 400px;
z-index: 100;
background-color: rgba(255, 255, 255, 0.95);
}

#cy {
width: 100%;
height: 800px;
height: 90vh;
/* display: block;*/
}
</style>
Loading