Commit 473ba6d8 authored by Hugh Brown's avatar Hugh Brown

Merge branch 'feature/viz-color-picker' into 'master'

Use colors to group nodes after a pattern search

See merge request crespum/polaris!72
parents 502c68c3 7e79afcc
Pipeline #127836292 passed with stages
in 7 minutes and 10 seconds
......@@ -20,3 +20,11 @@ All other parameters are optional for the graph structure construction. However
## Run the server locally
Execute `polaris viz` and open the indicated host and port, generally this would work: `http://0.0.0.0:8000/dynamic_network_analysis_3d-ui.html`.
## User Interface guide for the the graph HTML5 page
### From the search input field
- **CTRL+RETURN**: every node with the entered pattern will be colored. It can be repeated and colors will rotate.
- **CTRL+SHIFT+RETURN**: reset node color highlighting completely if the search field is empty, or with one color to highlight only the current pattern.
<html><head>
<html>
<head>
<title>WINDOW_TITLE_HERE</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<script src="3d-force-graph.js"></script>
<script src="d3.v5.min.js"></script>
<style type="text/css">
......@@ -76,15 +79,23 @@
const hud_elt = document.getElementById('graph-hud');
const nodeslist_elt = document.getElementById('nodeslist');
const search_elt = document.getElementById('graph-search-input');
const node_base_color = "#BBF";
// unused collection of nicely separated colors
const polaris_color_set = ["#9A6324", "#fffac8", "#3cb44b", "#4644c0",
"#aaffc3", "#e6194B", "#42d4f4", "#f032e6",
"#ffe119", "#f58231", "#ffffff"];
const color_scale = d3.scaleOrdinal().domain([1, polaris_color_set.length]).range(polaris_color_set);
var color_step = 0;
// ---- Graph's routines ---- //
// Converts node info to HTML for screen display
function node_to_html(node) {
if (node.hasOwnProperty("id"))
if (node && typeof node == "object" && node.hasOwnProperty("id"))
return "id: " + node.id
+ ", name: <b>" + node.name
+ "</b>/" + node.group;
// If "node" is already just a string
return node;
}
......@@ -119,9 +130,7 @@
const Graph = ForceGraph3D()(graph_elt)
.jsonUrl('JSON_DATA_FILE_HERE')
.nodeLabel(node => node.name + ':' + node.group)
//.nodeAutoColorBy('group')
.nodeColor(node => node.id.toString().includes(search_elt.value)
&& (search_elt.value.length > 0) ? "#33EE33" : "#BBF")
.nodeColor(node => node_base_color)
.onNodeHover(node => graph_elt.style.cursor = node ? 'pointer' : null)
.onNodeClick(node => {
hud_update("clicked", node);
......@@ -166,6 +175,63 @@
return undefined;
}
// Highlight nodes based on the nodes names
function highlight_nodes(search_str, color, reset_color = false) {
const { nodes, links } = Graph.graphData();
for (node of nodes) {
if (node.name.includes(search_str)){
node.color = color;
} else {
if (reset_color) {
node.color = color;
}
// else don't change the value
}
}
}
// Catching keypress events and launching actions
function searchInputCallback(evt) {
// evt.key is the named key so here we're looking for "Enter"
// NB: evt.which is deprecated so using only keyCode here.
// NB: evt.code should be what evt.keyCode is according to W3C standards.
// Watch that for eventual future changes.
if (evt.keyCode === 13 || evt.key == "Enter") {
// ASCII for Enter (Return Carriage) is 13 (Firefox)
// For line feed this is 10 (chrome) in UIs generally ctrl+Enter is a linefeed.
if (evt.ctrlKey) {
if (evt.shiftKey) {
// highlight nothing and reset
highlight_nodes("", node_base_color, true);
}
// Control key is pressed: node highligting
hud_update("highlighting search pattern", search_elt.value);
color_step += 1;
// if (color_step > polaris_color_set.length) prompt for user input
// to give a new color or do nothing: colors will rotate then.
highlight_nodes(search_elt.value, color_scale(color_step));
Graph.nodeColor(node => (node.color) ? node.color : node_base_color);
} else {
// Control key is NOT pressed: jetpacking
hud_update("searching", search_elt.value);
const node = find_node(search_elt.value);
if (node) {
hud_update("jetpacking", node);
jetpack_to(node);
} else {
hud_update("search stopped", node);
}
}
// Stop processing of the event here.
evt.stopPropagation();
evt.preventDefault();
} // --- end of "Enter" event and derivatives
}
// search: Click event only to check status of datalist
search_elt.addEventListener("click", function () {
if (nodeslist_elt.innerHTML == "") {
......@@ -173,31 +239,8 @@
}
});
// search: Enter press event zoom to node
search_elt.addEventListener('keypress', function (e) {
var key = e.which || e.keyCode;
if (key === 13) { // 13 is enter
if (e.ctrlKey) {
// Control key is pressed: node highligting
// The field search_elt.value was certainly updated
// So we refresh to update color of nodes which contain
// the search string.
Graph.refresh();
} else {
// Control key is NOT pressed: jetpacking
hud_update("searching", search_elt.value);
const node = find_node(search_elt.value);
if (node) {
hud_update("jetpacking", node);
jetpack_to(node);
} else {
hud_update("search stopped", node);
}
}
}
});
// search: Keyboard events
search_elt.addEventListener('keypress', searchInputCallback);
</script>
</body>
......
......@@ -39,29 +39,39 @@ def launch_webserver(json_data_file):
target_directory, target_file = os.path.split(
os.path.abspath(json_data_file))
target_index = os.path.join(target_directory, "index.html")
target_icon = os.path.join(target_directory, "favicon.ico")
target_lib = os.path.join(target_directory, "3d-force-graph.js")
# Read path for source template
html_template = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"dynamic_network_analysis_3d-ui.html")
target_assets = [(os.path.join(target_directory, "3d-force-graph.js"),
"https://deepchaos.space/3d-force-graph.js"),
(os.path.join(target_directory, "d3.v5.min.js"),
"https://deepchaos.space/d3.v5.min.js")]
# Write new index file to be served
with open(target_index, "w") as target_fd:
html_template = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"dynamic_network_analysis_3d-ui.html")
with open(html_template, "r") as template_fd:
for line in template_fd:
target_fd.write(
line.replace("JSON_DATA_FILE_HERE", target_file))
with open(target_icon, "w") as icon_fd:
if "JSON_DATA_FILE_HERE" in line:
target_fd.write(
line.replace("JSON_DATA_FILE_HERE", target_file))
elif "WINDOW_TITLE_HERE" in line:
target_fd.write(
line.replace("WINDOW_TITLE_HERE",
"Polaris - {}".format(json_data_file)))
else:
target_fd.write(line)
# Writing a dummy icon file to avoid http 404 errors
with open(os.path.join(target_directory, "favicon.ico"), "w") as icon_fd:
icon_fd.write("A")
# Check if required JS libs are in target directory
if not os.path.isfile(target_lib):
with open(target_lib, "w") as lib_fd:
LOGGER.info("Downloading dependency: %s", target_lib)
req = requests.get('https://deepchaos.space/3d-force-graph.js')
lib_fd.write(req.text)
for asset in target_assets:
if not os.path.isfile(asset[0]):
with open(asset[0], "w") as lib_fd:
LOGGER.info("Downloading dependency: %s", asset[0])
req = requests.get(asset[1])
lib_fd.write(req.text)
# Setup web directory
# pylint: disable=W0603
......
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