diff --git a/dev/build_graph.py b/dev/build_graph.py index 7f18e65..58d497d 100644 --- a/dev/build_graph.py +++ b/dev/build_graph.py @@ -44,17 +44,8 @@ ] -HANDLERS: list[str] = [] -HANDLERS.append( -"""on('doubleClick', function (event) { - const node = network.body.data.nodes.get(event.nodes[0]); - if (node) { window.open(node.link) }; -}); -""") - - def build_graph(url: str) -> DirectoryBuilder: - template = ENV.get_template('html/basic-template.html') + template = ENV.get_template('html/base.html') builder = DirectoryBuilder( url, style=GitRepoStyle, @@ -62,24 +53,31 @@ def build_graph(url: str) -> DirectoryBuilder: network_options={'nodes': {'font': {'face': 'JetBrains Mono'}}}, ) nx = builder.web_network() + nx.spring_solve() out = Path(f'docs/ref/repo-graphs/{builder.user}/{builder.repo}.html') out.parent.mkdir(parents=True, exist_ok=True) html = to_html( nx, template, - jinja={ - 'background': '#1f1f1f', - 'font': 'JetBrains Mono', - 'title': builder.repo, - }, - pygraph={'handlers': HANDLERS} + network={}, + progressor={'enabled': True}, + selectors={ + 'node': { + 'placeholder_text': 'Search Files', + 'search_field': 'id', + 'label_field': 'id', + }, + 'edge': { + 'enabled': False, + } + } ) out.write_text(html) return builder def main(): - template = ENV.get_template('html/basic-template.html') + template = ENV.get_template('html/base.html') built: list[DirectoryBuilder] = [] with ThreadPoolExecutor(len(TARGETS), thread_name_prefix='pygraph-') as executor: @@ -104,7 +102,6 @@ def main(): }, ) nx.barnes_hut(gravitationalConstant=-6000, avoidOverlap=0.75) - for builder in built: repo = builder.repo user = str(builder.user) @@ -137,16 +134,20 @@ def main(): builder.delete_directory() print(f'cleaned up {repo}') + nx.spring_solve() out = Path('docs/ref/index.html') html = to_html( nx, template, - jinja={ - 'background': '#1f1f1f', - 'font': 'JetBrains Mono', - 'title': 'Home', + network={}, + progressor={'enabled': True}, + selectors={ + 'edge': {'enabled': False}, + 'node': { + 'placeholder_text': 'Search Repos/Users', + 'search_field': 'id', + }, }, - pygraph={'handlers': HANDLERS} ) print('\nwriting index...') out.write_text(html) diff --git a/docs/ref/index.html b/docs/ref/index.html index fcd9577..0840e52 100644 --- a/docs/ref/index.html +++ b/docs/ref/index.html @@ -1,62 +1,412 @@ + - - - Home - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/peps/pep-authors-all.html b/docs/ref/peps/pep-authors-all.html index c19872e..b8c8e42 100644 --- a/docs/ref/peps/pep-authors-all.html +++ b/docs/ref/peps/pep-authors-all.html @@ -38,7 +38,6 @@ left: 0; padding: 20px; } - .right { z-index: 1; position: fixed; @@ -46,6 +45,25 @@ right: 0; padding: 20px; } + + #progressContainer { + width: 50%; + right: 25%; + bottom: 5%; + background-color: #ffffff5b; + border-radius: 5.5px; + overflow: hidden; + position: absolute; + z-index: 1; + visibility: hidden; + backdrop-filter: blur(10px); + } + #progressBar { + width: 0%; + height: 20px; + background-color: #4caf50; + z-index: 2; + } @@ -62,206 +80,215 @@ +
diff --git a/docs/ref/pygraph-repo.png b/docs/ref/pygraph-repo.png index 98d2455..29ca74a 100644 Binary files a/docs/ref/pygraph-repo.png and b/docs/ref/pygraph-repo.png differ diff --git a/docs/ref/repo-graphs/Qiskit/rustworkx.html b/docs/ref/repo-graphs/Qiskit/rustworkx.html index 8c9a567..8f82f39 100644 --- a/docs/ref/repo-graphs/Qiskit/rustworkx.html +++ b/docs/ref/repo-graphs/Qiskit/rustworkx.html @@ -1,62 +1,499 @@ + - - - rustworkx - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/encode/httpx.html b/docs/ref/repo-graphs/encode/httpx.html index ecb72c5..925326c 100644 --- a/docs/ref/repo-graphs/encode/httpx.html +++ b/docs/ref/repo-graphs/encode/httpx.html @@ -1,62 +1,443 @@ + - - - httpx - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/fastapi/fastapi.html b/docs/ref/repo-graphs/fastapi/fastapi.html index 05de64a..47ffd0d 100644 --- a/docs/ref/repo-graphs/fastapi/fastapi.html +++ b/docs/ref/repo-graphs/fastapi/fastapi.html @@ -1,62 +1,471 @@ + - - - fastapi - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/fastapi/typer.html b/docs/ref/repo-graphs/fastapi/typer.html index 83f8261..4c4fa40 100644 --- a/docs/ref/repo-graphs/fastapi/typer.html +++ b/docs/ref/repo-graphs/fastapi/typer.html @@ -1,62 +1,464 @@ + - - - typer - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/hwelch-fle/arcpie.html b/docs/ref/repo-graphs/hwelch-fle/arcpie.html index 697ab23..624c542 100644 --- a/docs/ref/repo-graphs/hwelch-fle/arcpie.html +++ b/docs/ref/repo-graphs/hwelch-fle/arcpie.html @@ -1,62 +1,401 @@ + - - - arcpie - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/hwelch-fle/cimple.html b/docs/ref/repo-graphs/hwelch-fle/cimple.html index ac7f33d..bdfe079 100644 --- a/docs/ref/repo-graphs/hwelch-fle/cimple.html +++ b/docs/ref/repo-graphs/hwelch-fle/cimple.html @@ -1,62 +1,401 @@ + - - - cimple - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/hwelch-fle/geo-grab.html b/docs/ref/repo-graphs/hwelch-fle/geo-grab.html index 05327f3..095cef8 100644 --- a/docs/ref/repo-graphs/hwelch-fle/geo-grab.html +++ b/docs/ref/repo-graphs/hwelch-fle/geo-grab.html @@ -1,62 +1,394 @@ + - - - geo-grab - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/hwelch-fle/plankapy.html b/docs/ref/repo-graphs/hwelch-fle/plankapy.html index 6d64a21..110c7f4 100644 --- a/docs/ref/repo-graphs/hwelch-fle/plankapy.html +++ b/docs/ref/repo-graphs/hwelch-fle/plankapy.html @@ -1,62 +1,408 @@ + - - - plankapy - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/hwelch-fle/pygraph.html b/docs/ref/repo-graphs/hwelch-fle/pygraph.html index 6a6cbf9..362cca4 100644 --- a/docs/ref/repo-graphs/hwelch-fle/pygraph.html +++ b/docs/ref/repo-graphs/hwelch-fle/pygraph.html @@ -1,62 +1,422 @@ + - - + pygraph - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/ionelmc/pytest-benchmark.html b/docs/ref/repo-graphs/ionelmc/pytest-benchmark.html index 231c846..3e62564 100644 --- a/docs/ref/repo-graphs/ionelmc/pytest-benchmark.html +++ b/docs/ref/repo-graphs/ionelmc/pytest-benchmark.html @@ -1,62 +1,457 @@ + - - - pytest-benchmark - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/pallets/click.html b/docs/ref/repo-graphs/pallets/click.html index e0d5bb3..ec4e958 100644 --- a/docs/ref/repo-graphs/pallets/click.html +++ b/docs/ref/repo-graphs/pallets/click.html @@ -1,62 +1,450 @@ + - - - click - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/pallets/jinja.html b/docs/ref/repo-graphs/pallets/jinja.html index 4d4c2ed..cfb060f 100644 --- a/docs/ref/repo-graphs/pallets/jinja.html +++ b/docs/ref/repo-graphs/pallets/jinja.html @@ -1,62 +1,464 @@ + - - - jinja - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/pytest-dev/pytest.html b/docs/ref/repo-graphs/pytest-dev/pytest.html index 86d0d71..e28a6f6 100644 --- a/docs/ref/repo-graphs/pytest-dev/pytest.html +++ b/docs/ref/repo-graphs/pytest-dev/pytest.html @@ -1,62 +1,485 @@ + - - - pytest - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ No newline at end of file diff --git a/docs/ref/repo-graphs/visjs/vis-network.html b/docs/ref/repo-graphs/visjs/vis-network.html index c8bc9bd..6af6cc5 100644 --- a/docs/ref/repo-graphs/visjs/vis-network.html +++ b/docs/ref/repo-graphs/visjs/vis-network.html @@ -1,62 +1,464 @@ + - - - vis-network - - - - - - - - + + pygraph + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ +
+
+ +
+
+ + +
+ + +
+
+
+ +
+ \ 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)