forked from jupyrdf/ipyradiant
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
jupyrdfGH-118: Add initial example widget.
- Loading branch information
Showing
1 changed file
with
229 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
{ | ||
"cells": [ | ||
{ | ||
"cell_type": "markdown", | ||
"id": "f6f51118", | ||
"metadata": {}, | ||
"source": [ | ||
"## Basic widget for searching a graph to find nodes/edges\n", | ||
"\n", | ||
"Opinionated search. Looking for specific keys from an RDF graph present in the converted networkx graph." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"id": "338ea955", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"import ipywidgets as W\n", | ||
"import traitlets as T\n", | ||
"from IPython.display import JSON, display\n", | ||
"from pathlib import Path\n", | ||
"from networkx import Graph as NXGraph\n", | ||
"from pandas import DataFrame\n", | ||
"from rdflib.graph import Graph as RDFGraph\n", | ||
"from rdflib.term import URIRef" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"id": "54b02ad2", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"from ipyradiant import FileManager, PathLoader\n", | ||
"\n", | ||
"\n", | ||
"lw = FileManager(loader=PathLoader(path=\"data\"))\n", | ||
"# here we hard set what we want the file to be, but ideally a user can choose a file to work with.\n", | ||
"lw.loader.file_picker.value = lw.loader.file_picker.options[\"starwars.ttl\"]\n", | ||
"rdf_graph = lw.graph" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"id": "a946dabd", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# run the converter so that we can work with the networkx graph\n", | ||
"from ipyradiant.rdf2nx import RDF2NX\n", | ||
"\n", | ||
"nx_graph = RDF2NX.convert(rdf_graph)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"id": "ee9363c9", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"def get_types(graph: NXGraph):\n", | ||
" \"\"\"Return the options for node types present in a networkx graph.\"\"\"\n", | ||
" types = set()\n", | ||
" for _, data in nx_graph.nodes(data=True):\n", | ||
" # TODO how to make this more generic, e.g. for custom type URIs\n", | ||
" rdf_type = data.get(\"rdf:type\")\n", | ||
" if not rdf_type:\n", | ||
" continue\n", | ||
" elif type(rdf_type) == URIRef:\n", | ||
" types.add(rdf_type)\n", | ||
" elif type(rdf_type) in {tuple, list}:\n", | ||
" types.update(rdf_type)\n", | ||
"\n", | ||
" s_types = sorted(types)\n", | ||
" \n", | ||
" # TODO need intelligent URI parsing later on\n", | ||
" return [(f\"<< {Path(type_).name} >>\", (type_, \"node\")) for type_ in s_types]\n", | ||
"\n", | ||
"\n", | ||
"def get_predicates(graph: NXGraph):\n", | ||
" \"\"\"Return the options for edge types present in a networkx graph.\"\"\"\n", | ||
" predicates = set()\n", | ||
" predicate_options = []\n", | ||
" for source, target, edge_data in nx_graph.edges(data=True):\n", | ||
" # TODO how to make this more generic\n", | ||
" predicate = edge_data.get(\"predicate\")\n", | ||
" if not predicate:\n", | ||
" continue\n", | ||
" \n", | ||
" # TODO should this be configurable?\n", | ||
" label = edge_data.get(\"_label\") or Path(predicate).name\n", | ||
" if predicate not in predicates:\n", | ||
" predicates.add(predicate)\n", | ||
" predicate_options.append((f\"o-- {label} --o\", (predicate, \"edge\")))\n", | ||
" \n", | ||
" return sorted(predicate_options, key=lambda x: x[0])" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"id": "7839b7bf", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"text_widget = W.Text(placeholder=\"Start typing to search....\")\n", | ||
"select_options = W.Select(rows=4)\n", | ||
"run_button = W.Button(description=\"Run\")\n", | ||
"\n", | ||
"# store on class\n", | ||
"available_graph_types = get_types(nx_graph)\n", | ||
"available_graph_predicates = get_predicates(nx_graph)\n", | ||
"\n", | ||
"\n", | ||
"def return_matches(b):\n", | ||
" \"\"\"Returns nodes/edges that match the user's selection.\"\"\"\n", | ||
" # TODO get nx_graph from widget class\n", | ||
" \n", | ||
" # TODO obv this needs to be linked for real\n", | ||
" try:\n", | ||
" choice, type_ = select_options.value\n", | ||
" except TypeError:\n", | ||
" # there is no selection, so don't do anything\n", | ||
" return\n", | ||
" \n", | ||
" if type_ == \"node\":\n", | ||
" # TODO configure the return types\n", | ||
" # Challenge, node can have multiple types\n", | ||
" matches = set()\n", | ||
" for node, data in nx_graph.nodes(data=True):\n", | ||
" types = data.get(\"rdf:type\")\n", | ||
" if (\n", | ||
" (type(types) == URIRef and types == choice) or \\\n", | ||
" (type(types) in {list, tuple} and choice in types)\n", | ||
" ):\n", | ||
" matches.add(node)\n", | ||
" print(matches)\n", | ||
" \n", | ||
" elif type_ == \"edge\":\n", | ||
" matches = [\n", | ||
" (source, target) \n", | ||
" for source, target, data in nx_graph.edges(data=True) \n", | ||
" if data.get(\"predicate\") == choice\n", | ||
" ]\n", | ||
" print(matches)\n", | ||
"\n", | ||
"def clear_select_options():\n", | ||
" select_options.options = []\n", | ||
" select_options.value = None\n", | ||
"\n", | ||
"\n", | ||
"def update_options(change):\n", | ||
" \"\"\"Update the Select options as the user changes the search string.\"\"\"\n", | ||
" if len(change.new) < 2:\n", | ||
" clear_select_options()\n", | ||
" return\n", | ||
" \n", | ||
" if change.old != change.new:\n", | ||
" # TODO obv this needs to be linked for real\n", | ||
" select_options.options = [\n", | ||
" option for option in [*available_graph_types, *available_graph_predicates]\n", | ||
" if change.new.lower() in option[0].lower()\n", | ||
" ]\n", | ||
"\n", | ||
"text_widget.observe(update_options, \"value\")\n", | ||
"run_button.on_click(return_matches)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"id": "50648f07", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# TODO gridspec layout\n", | ||
"# Note: combobox does not accept tuple for display option vs value\n", | ||
"W.HBox([\n", | ||
" W.VBox([\n", | ||
" text_widget,\n", | ||
" select_options\n", | ||
" ]),\n", | ||
" run_button\n", | ||
"])" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "e01a4d52", | ||
"metadata": {}, | ||
"source": [ | ||
"#### Questions/TODO\n", | ||
"* How would we support chaining multiple selections e.g. Character - homeworld - Planet?\n", | ||
"* Provide recommendations before information is types into the search bar (types, and then predicates)\n", | ||
"* Add to actual graph view (cytoscape)\n", | ||
" * warn user if too many nodes/edges would be added\n", | ||
" * clear view button\n", | ||
"* create actual widget class\n", | ||
"* widget that displays information about the types/edges present in the networkx graph" | ||
] | ||
} | ||
], | ||
"metadata": { | ||
"kernelspec": { | ||
"display_name": "Python 3", | ||
"language": "python", | ||
"name": "python3" | ||
}, | ||
"language_info": { | ||
"codemirror_mode": { | ||
"name": "ipython", | ||
"version": 3 | ||
}, | ||
"file_extension": ".py", | ||
"mimetype": "text/x-python", | ||
"name": "python", | ||
"nbconvert_exporter": "python", | ||
"pygments_lexer": "ipython3", | ||
"version": "3.7.10" | ||
} | ||
}, | ||
"nbformat": 4, | ||
"nbformat_minor": 5 | ||
} |