+
+
+
\ No newline at end of file
diff --git a/src/pygraph/templates/__init__.py b/src/pygraph/templates/__init__.py
index 9e1294b..e38d10c 100644
--- a/src/pygraph/templates/__init__.py
+++ b/src/pygraph/templates/__init__.py
@@ -1 +1 @@
-from .models import BaseTemplate as BaseTemplate
+from .models import BaseTemplateOptions as BaseTemplateOptions
diff --git a/src/pygraph/templates/html/base-template.html b/src/pygraph/templates/html/base-template.html
deleted file mode 100644
index bf40a4a..0000000
--- a/src/pygraph/templates/html/base-template.html
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
- {% block head -%}
-
-
- {{jinja['title'] | default ('pygraph')}}
- {% block head_scripts -%}
-
-
-
-
-
-
-
-
- {%- endblock %}
-
- {%- endblock %}
-
-
- {% block content -%}{%- endblock %}
-
-
-
diff --git a/src/pygraph/templates/html/base.html b/src/pygraph/templates/html/base.html
new file mode 100644
index 0000000..0b4ce52
--- /dev/null
+++ b/src/pygraph/templates/html/base.html
@@ -0,0 +1,49 @@
+{# Schema
+ selectors:
+ `see js/tom_select.js`
+ progressor:
+ `see js/progressor.js`
+ style: see html/head_styles.html
+ network:
+ title: str (default: "pygraph")
+ nodes: [Node, ...]
+ nodes: [Edge, ...]
+ options: NetworkOptions
+ google_fonts: list[str] (default: ['JetBrains Mono'])
+ font_awesome: bool (default: True)
+ hover_edge_labels: bool (default: True)
+ style: `see html/head_styles.html`
+#}
+
+
+
+ {{network.title|default('pygraph')}}
+ {% include 'html/head_scripts.html' %}
+ {% include 'html/head_styles.html' %}
+
+
+ {% if selectors %}
+ {% include "html/selectors.html" %}
+ {% endif %}
+ {% if progressor %}
+ {% include "html/progressor.html" %}
+ {% endif %}
+
+ {% block selectors %}{% endblock %}
+
+
+
diff --git a/src/pygraph/templates/html/basic-template.html b/src/pygraph/templates/html/basic-template.html
deleted file mode 100644
index 3bdc520..0000000
--- a/src/pygraph/templates/html/basic-template.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% extends "html/base-template.html" -%}
-{% block title -%}pygraph{% endblock -%}
-{% block content -%}
- {{ super() }}
-
-{% endblock -%}
diff --git a/src/pygraph/templates/html/head_scripts.html b/src/pygraph/templates/html/head_scripts.html
new file mode 100644
index 0000000..1cd604f
--- /dev/null
+++ b/src/pygraph/templates/html/head_scripts.html
@@ -0,0 +1,29 @@
+{% for font in network.google_fonts|default(['JetBrains Mono']) %}
+
+
+{% endfor %}
+
+{% if network.use_font_awesome|default(True) %}
+
+
+{% endif %}
+
+
+
+
+{% if selectors %}
+
+
+
+{% endif %}
\ No newline at end of file
diff --git a/src/pygraph/templates/html/head_styles.html b/src/pygraph/templates/html/head_styles.html
new file mode 100644
index 0000000..c3237d5
--- /dev/null
+++ b/src/pygraph/templates/html/head_styles.html
@@ -0,0 +1,77 @@
+{# Schema
+ network.style:
+ width: ... (default: '100%')
+ height: ... (default: '100%')
+ background: str (default: 'darkslategrey')
+ font_size: int (default: '12px')
+ font_family: str (default: 'JetBrains Mono')
+
+ progressor:
+ enabled: bool (default: True)
+ style (optional):
+ width: str | int (default: '50%')
+ right: str | int (default: '25%')
+ bottom: str | int (default: '5%')
+ background: str (default: '#ffffff5b')
+ color: str (default: '#4caf50ff')
+ radius: str | int (default: '5.5px')
+ blur: str | int | bool (default: '10px')
+#}
+{% set network = network.style or {}%}
+{% set progressor = progressor or {}%}
+
+
\ No newline at end of file
diff --git a/src/pygraph/templates/html/progressor.html b/src/pygraph/templates/html/progressor.html
new file mode 100644
index 0000000..a09316a
--- /dev/null
+++ b/src/pygraph/templates/html/progressor.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/pygraph/templates/html/selectors.html b/src/pygraph/templates/html/selectors.html
new file mode 100644
index 0000000..411c9ca
--- /dev/null
+++ b/src/pygraph/templates/html/selectors.html
@@ -0,0 +1,18 @@
+{% set node = selectors.node or {}%}
+{% set edge = selectors.edge or {}%}
+
+{% if node.enabled|default(True) %}
+
+
+
+
+
+{% endif %}
+{% if edge.enabled|default(True) %}
+
+
+
+
+
+{% endif %}
+
\ No newline at end of file
diff --git a/src/pygraph/templates/js/click_interaction.js b/src/pygraph/templates/js/click_interaction.js
new file mode 100644
index 0000000..587fda4
--- /dev/null
+++ b/src/pygraph/templates/js/click_interaction.js
@@ -0,0 +1,18 @@
+network.on("doubleClick", (event) => {
+ openLink(resolveLink(event))
+ {% if selectors %}
+ || clearFilters();
+ {% endif %}
+});
+
+var clickStack = [];
+network.on("click", async (event) => {
+ const clickedAt = new Date().valueOf();
+ clickStack.push(clickedAt);
+ await new Promise((r) => setTimeout(r, 200));
+ if (clickStack.pop() == clickedAt) {
+ {% if selectors %}
+ updateSelection(event);
+ {% endif %}
+ }
+});
diff --git a/src/pygraph/templates/js/create_network.js b/src/pygraph/templates/js/create_network.js
index 1da379d..22addee 100644
--- a/src/pygraph/templates/js/create_network.js
+++ b/src/pygraph/templates/js/create_network.js
@@ -1,12 +1,6 @@
-const network = new vis.Network(
- document.getElementById('pygraph'),
- {
- nodes: new vis.DataSet({{ data["nodes"] | tojson }}),
- edges: new vis.DataSet({{ data["edges"] | tojson }}),
- },
- {{ data["options"] | tojson }}
-);
-
-{% for handler in pygraph["handlers"] | default ([]) %}
-network.{{ handler }}
-{% endfor %}
\ No newline at end of file
+const data = {
+ nodes: new vis.DataSet({{ network.nodes|tojson() }}),
+ edges: new vis.DataSet({{ network.edges|tojson() }}),
+}
+const options = {{ network.options|tojson(indent=2) }}
+const network = new vis.Network(document.getElementById('pygraph'), data, options);
\ No newline at end of file
diff --git a/src/pygraph/templates/js/draw_title.js b/src/pygraph/templates/js/draw_title.js
deleted file mode 100644
index 0fb6724..0000000
--- a/src/pygraph/templates/js/draw_title.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// ---------------- DRAW TITLE START ----------------------
-{% if "title" in jinja -%}
-data.network.on("beforeDrawing", function (ctx) {
- const central_focus = data.network.getViewPosition();
- const scale = data.network.getScale();
-
- div = document.getElementById("visjsnet").getElementsByClassName("vis-network")[0];
- const height = div.clientHeight;
- const width = div.clientWidth;
-
- const dx = width / 2 - width * 0.05;
- const dy = height / 2 - height * 0.05;
- const x = central_focus.x - dx / scale;
- const y = central_focus.y - dy / scale;
- //const font_size = Math.round(30.0 / scale)
- const font_size = 30.0 / scale;
-
- ctx.font = "" + font_size + "px JetBrains Mono";
- //ctx.font = "30px JetBrains Mono";
- ctx.fillStyle = "#D0D0D0";
- ctx.fillText('{{ jinja["title"] }}', x, y);
-});
-{% endif %}
-// ---------------- DRAW TITLE END ----------------------
\ No newline at end of file
diff --git a/src/pygraph/templates/js/enable_startAnimation.js b/src/pygraph/templates/js/enable_startAnimation.js
deleted file mode 100644
index a81435b..0000000
--- a/src/pygraph/templates/js/enable_startAnimation.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// ---------------- START ANIMATION START ----------------------
-{% with startAnimation = jinja.get("startAnimation", None) %}
-{% if "zoom_factor" in startAnimation and "duration_ms" in startAnimation %}
-data.network.once("afterDrawing",
- function (ctx) {
- data.network.Zoom({{ startAnimation["zoom_factor"] }}, {{ startAnimation["duration_ms"] }})
- }
-);
-{% endif %}
-{% endwith %}
-// ---------------- START ANIMATION END ----------------------
\ No newline at end of file
diff --git a/src/pygraph/templates/js/functions.js b/src/pygraph/templates/js/functions.js
deleted file mode 100644
index b676ee9..0000000
--- a/src/pygraph/templates/js/functions.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// not used anywhere
-function find_heighbors_by_node_id(id) {
- for (const key in data.nodes) {
- const node = data.nodes[key];
-
- if (node["id"] === id) {
- const selected_node = node["id"];
- collected_nodes = data.network.getConnectedNodes(selected_node);
- collected_nodes.push(selected_node);
- return collected_nodes
- }
- }
-
- return null;
-}
diff --git a/src/pygraph/templates/js/helpers.js b/src/pygraph/templates/js/helpers.js
new file mode 100644
index 0000000..38c13ce
--- /dev/null
+++ b/src/pygraph/templates/js/helpers.js
@@ -0,0 +1,26 @@
+// Node Transformers
+let allNodes = () => network.body.data.nodes
+let getNodes = (nodeIds) => allNodes().get(nodeIds)
+let updateNodes = (nodeArray) => allNodes().updateOnly(nodeArray)
+
+// Edge Transformers
+let allEdges = () => network.body.data.edges
+let getEdges = (edgeIds) => allEdges().get(edgeIds)
+let edgesAtNode = (nodeId) => allEdges().get(network.getConnectedEdges(nodeId))
+let nodesAtEdge = (edge) => [edge.to, edge.from]
+let updateEdges = (edgeArray) => allEdges().updateOnly(edgeArray)
+
+// Network Transformers
+let setNetwork = (nodes, edges) => network.setData({ nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges) })
+let getBaseData = () => ({ nodes: data.nodes, edges: data.edges })
+let resetNetwork = () => setNetwork(data.nodes.get(), data.edges.get())
+
+{% if hover_edge_labels %}
+// Label Transformers
+let setFontSize = (val, item) => { item.font.size = val; return item }
+let showLabel = (item) => setFontSize({{edge_label_font_size|default(12)}}, item)
+let hideLabel = (item) => setFontSize(0, item)
+{% endif %}
+
+let openLink = (item) => (item?.{{link_field|default("link")}}) && window.open(item?.{{link_field|default("link")}}) || false
+let resolveLink = (event) => getNodes(event.nodes[0]) || getEdges(event.edges[0]) || undefined
diff --git a/src/pygraph/templates/js/hover_labels.js b/src/pygraph/templates/js/hover_labels.js
new file mode 100644
index 0000000..d8d644c
--- /dev/null
+++ b/src/pygraph/templates/js/hover_labels.js
@@ -0,0 +1,4 @@
+network.on('hoverEdge', (event) => { updateEdges(getEdges([event.edge]).map(showLabel)) });
+network.on('blurEdge', (event) => { updateEdges(getEdges([event.edge]).map(hideLabel)) });
+network.on('hoverNode', (event) => { updateEdges(edgesAtNode(event.node).map(showLabel)) });
+network.on('blurNode', (event) => { updateEdges(edgesAtNode(event.node).map(hideLabel)) });
\ No newline at end of file
diff --git a/src/pygraph/templates/js/init_dataTables.js b/src/pygraph/templates/js/init_dataTables.js
deleted file mode 100644
index 39fc8a8..0000000
--- a/src/pygraph/templates/js/init_dataTables.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// ---------------- DATA TABLES START ----------------------
-tbls = new DataTable('table.display');
-
-tbls.on('click', 'tbody tr', (e) => {
- let classList = e.currentTarget.classList;
-
- if (classList.contains('selected')) {
- classList.remove('selected');
- }
- else {
- tbls.rows('.selected').nodes().each((row) => row.classList.remove('selected'));
- classList.add('selected');
- }
-});
-// ---------------- DATA TABLES END ----------------------
\ No newline at end of file
diff --git a/src/pygraph/templates/js/init_sankey.js b/src/pygraph/templates/js/init_sankey.js
deleted file mode 100644
index b8d48b2..0000000
--- a/src/pygraph/templates/js/init_sankey.js
+++ /dev/null
@@ -1,55 +0,0 @@
-{#
- Required:
- {
- 'sankey': {
- 'node': {
- 'label':[string, ...],
- 'color': [string, ...],
- },
- 'link': {
- 'from_node': [id, ...],
- 'to_node': [id, ...],
- 'value': [number, ...],
- 'label': [str, ...],
- }
- }
- }
-#}
-// ---------------- SANKEY START ----------------------
-init_sankey({{ jinja.get("sankey", {}) | tojson }})
-
-function init_sankey(fig) {
-
- var data = {
- type: "sankey",
- domain: { x: [0, 1], y: [0, 1] },
- orientation: "h",
- valueformat: ".0f",
- valuesuffix: "TWh",
- node: {
- pad: 15,
- thickness: 15,
- line: { color: "black", width: 0.5 },
- label: fig.node.label,
- //color: fig.node.color
- },
-
- link: {
- source: fig.link.from_node_idx,
- target: fig.link.to_node_idx,
- value: fig.link.value,
- //label: fig.link.label
- }
- }
-
- var data = [data]
-
- var layout = {
- title: "Sankey chart",
- width: 1118,
- height: 772,
- font: { size: 10 }
- }
- Plotly.newPlot('sankeyDiv', data, layout)
-};
-// ---------------- SANKEY END ----------------------
\ No newline at end of file
diff --git a/src/pygraph/templates/js/init_tomSelect.js b/src/pygraph/templates/js/init_tomSelect.js
deleted file mode 100644
index cc8fb6d..0000000
--- a/src/pygraph/templates/js/init_tomSelect.js
+++ /dev/null
@@ -1,186 +0,0 @@
-// ---------------- TOM-SELECT START ----------------------
-const eventHandler = function (name) {
- return function () {
- const list_of_selected_values = arguments[0]
- dict = convert_field_value_list_to_dict(list_of_selected_values)
- hide_nodes_by_edge_attribute_values_intersect(dict)
-
- if (data.pygraph.dropdown_auto_close === true) tom_select.close();
- };
-};
-
-tom_select = new TomSelect("#select-all-fields", {
- maxItems: 10,
- maxOptions: null,
- onChange: eventHandler("onChange"),
- //create: true,
- //onItemAdd:function(){
- // this.setTextboxValue('');
- // this.refreshOptions();
- //},
- plugins: ['remove_button'],
-});
-
-function convert_field_value_list_to_dict(list) {
- // converts ["edge,country,LV","edge,country,GB"]
- // to
- // {country: {list: ["LV", "GB"], type="edge"}}
- dict = {}
- for (id in list) {
- const field_value_triplet = list[id].split(",")
- const type = field_value_triplet[0]
- const field = field_value_triplet[1]
- const value = field_value_triplet[2]
-
- if (field in dict) dict[field]["list"].push(value)
- else dict[field] = { "list": [value], "type": type }
- }
- return dict
-}
-
-function hide_nodes_by_edge_attribute_values_intersect(option_groups_dict) {
-
- const selected_nodes_by_field = {}
-
- for (field in option_groups_dict) {
- const dict = option_groups_dict[field]
- const selected_values = dict["list"]
- const node_or_edge = dict["type"]
- selected_nodes_by_field[field] = []
-
- for (id in selected_values) {
- let collected_nodes = []
- const value = selected_values[id];
-
- if (node_or_edge === "ALL" && value === "ALL") {
- collected_nodes = Object.keys(data.nodes);
- data.network.has_hidden_nodes = false;
- }
- else if (node_or_edge === "edge") {
- collected_nodes = get_nodes_by_edge_attribute_value(field, value)
- data.network.has_hidden_nodes = true;
- }
- else if (node_or_edge === "node") {
- collected_nodes = get_nodes_by_attribute_value(field, value);
- data.network.has_hidden_nodes = true;
- }
-
- for (id in collected_nodes) {
- node_id = collected_nodes[id]
- if (selected_nodes_by_field[field].includes(node_id) === false) selected_nodes_by_field[field].push(node_id)
- }
- }
- }
-
- const selected_nodes_intersection = apply_intersect(selected_nodes_by_field);
- changed_nodes = toggle_nodes(selected_nodes_intersection);
-
- data.ds_nodes.update(changed_nodes)
-}
-
-function hide_nodes_by_edge_attribute_values_union(option_groups_dict) {
-
- const selectedNodes = [];
-
- for (field in option_groups_dict) {
- const dict = option_groups_dict[field]
- const selected_values = dict["list"]
- const node_or_edge = dict["type"]
-
- for (id in selected_values) {
- let collected_nodes = []
- const value = selected_values[id];
-
- if (node_or_edge === "ALL" && value === "ALL") {
- collected_nodes = Object.keys(data.nodes);
- data.network.has_hidden_nodes = false;
- }
- else if (node_or_edge === "edge") {
- collected_nodes = get_nodes_by_edge_attribute_value(field, value)
- data.network.has_hidden_nodes = true;
- }
- else if (node_or_edge === "node") {
- collected_nodes = get_nodes_by_attribute_value(field, value);
- data.network.has_hidden_nodes = true;
- }
-
- for (id in collected_nodes) {
- node_value = collected_nodes[id]
- if (selectedNodes.includes(node_value) === false) selectedNodes.push(node_value)
- }
- }
- }
-
- changed_nodes = toggle_nodes(selectedNodes);
-
- data.ds_nodes.update(changed_nodes)
-}
-
-function get_nodes_by_edge_attribute_value(field, value) {
-
- const result = [];
-
- for (const key in data.edges) {
- const edge = data.edges[key];
-
- if (edge[field] == value) {
- if (result.includes(edge.from) === false) result.push(edge.from);
- if (result.includes(edge.to) === false) result.push(edge.to);
- }
- }
-
- return result;
-}
-
-function get_nodes_by_attribute_value(field, value) {
- const collected_nodes = [];
-
- for (const key in data.nodes) {
- const node = data.nodes[key];
-
- if (node[field] == value) {
- collected_nodes.push(node["id"]);
- }
- }
-
- return collected_nodes;
-}
-
-function apply_intersect(nodes_by_field_dict) {
- let field_with_shortest_list = null
- let min_list_len = 100000;
-
- //if nodes_by_field_dict is empty dict - return empty list
- if (nodes_by_field_dict === null || Object.keys(nodes_by_field_dict).length === 0) {
- return [];
- }
-
- //if nodes_by_field_dict contains only one field - just return values
- if (nodes_by_field_dict.length === 1) {
- return nodes_by_field_dict[0];
- }
-
- // find shortest list
- for (field_name in nodes_by_field_dict) {
- const curr_list_len = nodes_by_field_dict[field_name].length
- if (curr_list_len < min_list_len) {
- field_with_shortest_list = field_name
- min_list_len = curr_list_len
- }
- }
-
- // we start from the list of all node ids from the shortest list
- // our goal is to reduce it by compariong with other lists
- let intersection = nodes_by_field_dict[field_with_shortest_list]
-
- // pairwise comparison
- for (field_name in nodes_by_field_dict) {
- if (field_name !== field_with_shortest_list) {
- const nodes_list = nodes_by_field_dict[field_name];
- intersection = intersection.filter((node_id) => nodes_list.includes(node_id))
- }
- }
-
- return intersection;
-}
-// ---------------- TOM-SELECT END ----------------------
diff --git a/src/pygraph/templates/js/progressor.js b/src/pygraph/templates/js/progressor.js
new file mode 100644
index 0000000..20c8826
--- /dev/null
+++ b/src/pygraph/templates/js/progressor.js
@@ -0,0 +1,14 @@
+// Progress Bar
+const progressContainer = document.getElementById('progressContainer')
+const progressBar = document.getElementById('progressBar')
+
+let setProgressBar = (visibility, percent) => {
+ progressContainer.style.visibility = visibility
+ progressBar.style.width = `${percent}%`
+};
+let hideProgressBar = () => setProgressBar('hidden', 0);
+let showProgressBar = (percent) => setProgressBar('visible', percent);
+
+network.on('stabilizationProgress', (event) => {showProgressBar((event.iterations / event.total) * 100)});
+network.on('stabilizationIterationsDone', hideProgressBar);
+network.on('stabilized', hideProgressBar);
\ No newline at end of file
diff --git a/src/pygraph/templates/js/tom_select.js b/src/pygraph/templates/js/tom_select.js
new file mode 100644
index 0000000..72f4c1b
--- /dev/null
+++ b/src/pygraph/templates/js/tom_select.js
@@ -0,0 +1,122 @@
+{# Schema
+ selectors.node:
+ enabled: bool (default: True)
+ placeholder_text: str (default: 'Filter Nodes')
+ max_items: int | None (default: None)
+ max_options: int | None (default: 100)
+ value_field: str (default: "id")
+ label_field: str (default: '"label"')
+ search_field: str (default: '"label"')
+ sort_field: str (default: '"label"')
+ unique: bool (default: False)
+ reset_on_clear: bool (default: True)
+ selectors.edge:
+ enabled: bool (default: True)
+ placeholder_text: str (default: 'Filter Edges')
+ max_items: int | None (default: None)
+ max_options: int | None (default: 100)
+ value_field: str (default: '"label"')
+ label_field: str (default: '"label"')
+ search_field: str (default: '"label"')
+ sort_field: str (default: '"label"')
+ unique: bool (default: False)
+ reset_on_clear: bool (default: True)
+#}
+
+{% set node = selectors.node or {} %}
+{% set edge = selectors.edge or {} %}
+
+{% if node.enabled|default(True) %}
+let changeNodes = (selection) => {
+ if (selection.length == 0) { resetNetwork(); return; }
+ {% if edge.enabled|default(True) %}
+ tomSelectEdges.clear()
+ {% endif %}
+ var { nodes, edges } = getBaseData()
+ edges = edges.get({
+ filter: (edge) =>
+ selection.includes(edge.to) || selection.includes(edge.from),
+ })
+ const nodeIds = [
+ ...new Set(edges.map((edge) => [edge.to, edge.from]).flat()),
+ ]
+ nodes = nodes.get(nodeIds)
+ setNetwork(nodes, edges)
+};
+{% endif %}
+
+{% if edge.enabled|default(True) %}
+let changeEdges = (selection) => {
+ if (selection.length == 0) { resetNetwork(); return; }
+ tomSelectNodes.clear()
+ var { nodes, edges } = getBaseData()
+ edges = edges.get({ filter: (edge) => selection.includes(edge.{{edge.value_field|default("label")}}) })
+ nodes = getNodes([...new Set(edges.map(nodesAtEdge).flat())])
+ setNetwork(nodes, edges)
+};
+{% endif %}
+
+const collator = new Intl.Collator(undefined, {
+ numeric: true,
+ sensitivity: "base",
+});
+
+{% if node.enabled|default(True) %}
+const tomSelectNodes = new TomSelect("#tom-select-nodes", {
+ maxItems: {{node.max_items|default("null")}},
+ maxOptions: {{node.max_options|default(100)}},
+ valueField: "id",
+ labelField: "{{node.label_field|default('label')}}",
+ searchField: "{{node.search_field|string|default('label')}}",
+ sortField: "{{node.sort_field|default('label')}}",
+ options: allNodes()
+ {% if node.unique|default(False) %}
+ .distinct("{{node.value_field|default('id')}}")
+ .map((val) => ({ {{node.value_field|default('id')}}: val }))
+ .sort(collator.compare),
+ {% else %}
+ .get(),
+ {% endif %}
+ create: false,
+ onChange: changeNodes,
+ {% if node.reset_on_clear|default("true") %}
+ onClear: resetNetwork,
+ {% endif %}
+ plugins: {{node.plugins|default(["remove_button", "clear_button"])}},
+});
+{% endif %}
+
+{% if edge.enabled|default(True) %}
+const tomSelectEdges = new TomSelect("#tom-select-edges", {
+ maxItems: {{edge.max_items|default("null")}},
+ maxOptions: {{edge.max_options|default(100)}},
+ valueField: "{{edge.value_field|default('label')}}",
+ labelField: "{{edge.label_field|default('label')}}",
+ searchField: "{{edge.search_field|default('label')}}",
+ sortField: "{{node.sort_field|default('label')}}",
+ options: allEdges()
+ {% if edge.unique|default(False) %}
+ .distinct("{{edge.value_field|default('label')}}")
+ .map((val) => ({ {{edge.value_field|default('label')}}: val }))
+ .sort(collator.compare),
+ {% else %}
+ .get(),
+ {% endif %}
+ create: false,
+ onChange: changeEdges,
+ {% if edge.reset_on_clear|default("true") %}
+ onClear: resetNetwork,
+ {% endif %}
+ plugins: {{edge.plugins|default(["remove_button", "clear_button"])}},
+});
+{% endif %}
+
+// Selection Transformers
+let addItem = (selector, item) => item ? selector.addItem(item, false) || true : false
+let addNodeSelection = (item) => {% if node.enabled|default(True) %}addItem(tomSelectNodes, item?.{{node.value_field|default("id")}}){% else %} undefined {% endif %}
+let addEdgeSelection = (item) => {% if edge.enabled|default(True) %}addItem(tomSelectEdges, item?.{{node.value_field|default("label")}}){% else %} undefined {% endif %}
+let updateSelection = (event) => addNodeSelection(getNodes(event.nodes[0])) || addEdgeSelection(getEdges(event.edges[0]))
+let clearFilters = () => {
+ {% if edge.enabled|default(True) %}tomSelectEdges.clear(false);{% endif %}
+ {% if node.enabled|default(True) %}tomSelectNodes.clear(false);{% endif %}
+}
\ No newline at end of file
diff --git a/src/pygraph/templates/models.py b/src/pygraph/templates/models.py
index 949235e..89a29c3 100644
--- a/src/pygraph/templates/models.py
+++ b/src/pygraph/templates/models.py
@@ -1,10 +1,106 @@
-from typing import (
- Any,
- TypedDict,
-)
+from typing import TypedDict
+from pygraph import EdgeRecord, NetworkOptions, NodeRecord
-class BaseTemplate(TypedDict):
- data: dict[str, Any]
- network: dict[str, Any]
- sankey: dict[str, Any]
+type CSSVal = int | str
+type CSSColor = str
+
+
+class NetworkStyle(TypedDict, total=False):
+ width: CSSVal
+ """Network container width"""
+ height: CSSVal
+ """Network container height"""
+ background: CSSColor
+ """Network container background color"""
+
+
+class NetworkSchema(TypedDict, total=False):
+ nodes: list[NodeRecord]
+ """list of Nodes to include in the network"""
+ edges: list[EdgeRecord]
+ """list of Edges to include in the network"""
+ options: NetworkOptions
+ """Base Network options"""
+ title: str
+ """Page title"""
+ google_fonts: list[str]
+ """Google fonts to include from CDN"""
+ font_awesome: bool
+ """Set to True to include Font Awesome CDN resources (for icon shape)"""
+ hover_edge_labels: bool
+ """Only show edge labels when hovering over a Node or Edge"""
+ style: NetworkStyle
+ """Base style of the Network container"""
+
+
+class ProgressorStyle(TypedDict, total=False):
+ width: CSSVal
+ """Progress Bar width"""
+ right: CSSVal
+ """Progress Bar offset from right of window"""
+ bottom: CSSVal
+ """Progress Bar offset from bottom of window"""
+ background: CSSColor
+ """Background color for Progress Bar"""
+ color: CSSColor
+ """Fill color for Progress Bar"""
+ radius: CSSVal
+ """Corner radius of progress bar"""
+ blur: CSSVal
+ """Optional blur to apply to Progress Bar background"""
+
+
+# May be expanded later
+class ProgressorSchema(TypedDict, total=False):
+ enabled: bool
+ """Pass anything to this to enable the Progress Bar with defaults"""
+ style: ProgressorStyle
+ """Progress Bar styling"""
+
+
+class NodeSelectorSchema(TypedDict, total=False):
+ """See: https://tom-select.js.org/docs/api/"""
+ enabled: bool
+ """Set to False to disable the selector"""
+ placeholder_text: str
+ """Placeholder text for the search box"""
+ max_items: int
+ """Max items to show in dropdown"""
+ max_options: int
+ """Maximum selection options"""
+ value_field: str
+ """Field to use as option value"""
+ label_field: str
+ """Field to use as label value"""
+ search_field: str
+ """Field to use as search value"""
+ sort_field: str
+ """Field to sort by"""
+ unique: bool
+ """Force the options to be unique (will select all matching network objects)"""
+ reset_on_clear: bool
+ """Reset the network when the selection is cleared"""
+
+
+# Currently Identical to NodeSelectorSchema
+class EdgeSelectorSchema(NodeSelectorSchema):
+ """See: https://tom-select.js.org/docs/api/"""
+
+
+class SelectorSchema(TypedDict, total=False):
+ enabled: bool
+ """Pass anything to this to enable the Selectors with defaults"""
+ node: NodeSelectorSchema
+ """Node selector options"""
+ edge: EdgeSelectorSchema
+ """Edge selector options"""
+
+
+class BaseTemplateOptions(TypedDict, total=False):
+ network: NetworkSchema
+ """Network options"""
+ progressor: ProgressorSchema
+ """Progress Bar options"""
+ selectors: SelectorSchema
+ """Search/Selector options"""
diff --git a/src/pygraph/utils.py b/src/pygraph/utils.py
index 9547829..ec972ce 100644
--- a/src/pygraph/utils.py
+++ b/src/pygraph/utils.py
@@ -1,10 +1,7 @@
import itertools
from collections.abc import Iterable
from copy import deepcopy
-from typing import (
- Any,
- Unpack,
-)
+from typing import Unpack
import jinja2
import rustworkx as rx
@@ -15,6 +12,9 @@
Node,
NodeId,
)
+from pygraph.templates import (
+ BaseTemplateOptions,
+)
from pygraph.vis import (
EdgeOptions,
)
@@ -115,7 +115,7 @@ def kevin_bacon(nx: Network, node: Node | NodeId, *, copy: bool = False) -> Netw
return get_neighborhood(nx, node, 7, copy=copy)
-def to_html(nx: Network, template: jinja2.Template, **kwargs: Any) -> str:
+def to_html(nx: Network, template: jinja2.Template, **kwargs: Unpack[BaseTemplateOptions]) -> str:
"""Export the network to an HTML template
The supplied template *MUST* accept `nodes`, `edges`, and `options` values that
@@ -131,6 +131,10 @@ def to_html(nx: Network, template: jinja2.Template, **kwargs: Any) -> str:
Returns:
A string value with the HTML required to render the network
"""
- nx_data = {'data': nx.to_dict()}
- kwargs.update(nx_data)
+ nx_data = nx.to_dict()
+ kwargs.setdefault('network', {})
+ assert 'network' in kwargs
+ kwargs['network']['nodes'] = nx_data['nodes']
+ kwargs['network']['edges'] = nx_data['edges']
+ kwargs['network']['options'] = nx_data['options']
return template.render(**kwargs)