├── .gitignore ├── .idea ├── .gitignore ├── hnx-widget.iml ├── modules.xml └── vcs.xml ├── .npmrc ├── .storybook ├── main.js └── preview.js ├── DISCLAIMER ├── LICENSE ├── README.md ├── demo └── src │ └── index.js ├── notebooks ├── Testing Session.ipynb └── example widget.ipynb ├── package-lock.json ├── package.json ├── screenshots ├── hnx-bipartite-view.png ├── hnx-brush-edges.gif ├── hnx-brush-nodes.gif ├── hnx-dual-view.png └── hnx-widget-screenshot.png ├── setup-develop.sh ├── src ├── HypernetxWidgetDualView.js ├── HypernetxWidgetView.js ├── NavigableSVG.css ├── NavigableSVG.js ├── bars.js ├── checkboxEl.js ├── colorButton.js ├── colorPalette.js ├── colorScale.js ├── css │ └── hnxStyle.css ├── fontSizeMenu.js ├── functions.js ├── helpMenu.js ├── hnx-widget.css ├── iconWithTooltip.js ├── index.js ├── loadTable.js ├── nodeSizeMenu.js ├── radioButton.js ├── removeButton.js ├── showButton.js ├── stories │ ├── data │ │ ├── biggerProps.json │ │ ├── props-with-metadata.json │ │ ├── props-with-radius.json │ │ └── props.json │ ├── hnx-widget-basics.stories.js │ ├── hnx-widget-brush.stories.js │ ├── hnx-widget-encodings.stories.js │ ├── hnx-widget-interactions.stories.js │ ├── hnx-widget-loadings.stories.js │ ├── hnx-widget-navigation.stories.js │ ├── hnx-widget-position.stories.js │ └── hnx-widget-responsive.stories.js ├── switches.js ├── toolbar.js ├── visibilityButton.js └── widget.js ├── update-develop.sh └── widget ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── dist ├── hnxwidget-0.1.0a0-py2.py3-none-any.whl ├── hnxwidget-0.1.0a1-py2.py3-none-any.whl ├── hnxwidget-0.1.0a2-py2.py3-none-any.whl ├── hnxwidget-0.1.0a3-py2.py3-none-any.whl ├── hnxwidget-0.1.0a4-py2.py3-none-any.whl ├── hnxwidget-0.1.0b0-py2.py3-none-any.whl ├── hnxwidget-0.1.0b1-py2.py3-none-any.whl └── hnxwidget-0.1.0b2-py2.py3-none-any.whl ├── hnx-widget.json ├── hnxwidget ├── __init__.py ├── _version.py ├── example.py ├── hypernetx_widget.py ├── react_jupyter_widget.py ├── static │ └── extension.js ├── stats.py └── util.py ├── js ├── README.md ├── lib │ ├── ReactPlugin.js │ ├── embed.js │ ├── extension.js │ ├── index.js │ └── labplugin.js ├── package-lock.json ├── package.json └── webpack.config.js ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | .ipynb_checkpoints/ 3 | build/ 4 | *.py[cod] 5 | node_modules/ 6 | 7 | # Compiled javascript 8 | hnxwidget/static/ 9 | 10 | # OS X 11 | .DS_Store 12 | lib 13 | es 14 | widget/js/dist 15 | widget/dist 16 | widget/hnxwidget/static 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/hnx-widget.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # //registry.npmjs.org/:_authToken=${NPM_TOKEN} 2 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials" 9 | ] 10 | } -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | 2 | export const parameters = { 3 | actions: { argTypesRegex: "^on[A-Z].*" }, 4 | } -------------------------------------------------------------------------------- /DISCLAIMER: -------------------------------------------------------------------------------- 1 | This material was prepared as an account of work sponsored by an agency of the United States Government. Neither the United States Government nor the United States Department of Energy, nor Battelle, nor any of their employees, nor any jurisdiction or organization that has cooperated in the development of these materials, makes any warranty, express or implied, or assumes any legal liability or responsibility for the accuracy, completeness, or usefulness or any information, apparatus, product, software, or process disclosed, or represents that its use would not infringe privately owned rights. 2 | 3 | Reference herein to any specific commercial product, process, or service by trade name, trademark, manufacturer, or otherwise does not necessarily constitute or imply its endorsement, recommendation, or favoring by the United States Government or any agency thereof, or Battelle Memorial Institute. The views and opinions of authors expressed herein do not necessarily state or reflect those of the United States Government or any agency thereof. 4 | 5 | PACIFIC NORTHWEST NATIONAL LABORATORY 6 | operated by 7 | BATTELLE 8 | for the 9 | UNITED STATES DEPARTMENT OF ENERGY 10 | under Contract DE-AC05-76RL01830 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | hypernetx-widget 2 | 3 | Copyright © 2021, Battelle Memorial Institute 4 | 5 | 1. Battelle Memorial Institute (hereinafter Battelle) hereby grants permission to any person or entity lawfully obtaining a copy of this software and associated documentation files (hereinafter “the Software”) to redistribute and use the Software in source and binary forms, with or without modification. Such person or entity may use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and may permit others to do so, subject to the following conditions: 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Other than as used herein, neither the name Battelle Memorial Institute or Battelle may be used in any form whatsoever without the express written consent of Battelle. 9 | 2. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BATTELLE OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | An interactive demo of `hnxwidget` is available here: https://pnnl.github.io/hypernetx-widget/ 3 | 4 | # Installation 5 | `hnxwidget` is now [available on PyPi](https://pypi.org/project/hnxwidget/), simply "pip install" it: 6 | 7 | ```sh 8 | pip install hnxwidget 9 | ``` 10 | 11 | # Getting Started 12 | After a successful installation, you can copy/paste the fragment below into a jupyter notebook cell. Executing the cell will produce an interactive hypergraph visualization. It is required that the last line of the cell returns the widget. 13 | 14 | ```py 15 | from hnxwidget import HypernetxWidget 16 | import hypernetx as hnx 17 | 18 | scenes = { 19 | 0: ('FN', 'TH'), 20 | 1: ('TH', 'JV'), 21 | 2: ('BM', 'FN', 'JA'), 22 | 3: ('JV', 'JU', 'CH', 'BM'), 23 | 4: ('JU', 'CH', 'BR', 'CN', 'CC', 'JV', 'BM'), 24 | 5: ('TH', 'GP'), 25 | 6: ('GP', 'MP'), 26 | 7: ('MA', 'GP'), 27 | } 28 | 29 | H = hnx.Hypergraph(scenes) 30 | HypernetxWidget(H) 31 | ``` 32 | ![Screenshot of HNX Widget](screenshots/hnx-widget-screenshot.png) 33 | 34 | A more in depth demonstration of the widget is found in `/notebooks/example widget.ipynb`. 35 | 36 | # Install from source 37 | If you just want to use the tool with the most recent updates, this installation is not recommended. Instead, see above. This installation is intended for people who are developing the JavaScript portion of the library. It will setup the Node.js environments, download packages, etc. To get setup as a developer, run 38 | 39 | ```sh 40 | bash setup-develop.sh 41 | ``` 42 | 43 | # How to Uninstall 44 | ```sh 45 | jupyter nbextension uninstall hnxwidget 46 | pip unistall hnxwidget 47 | ``` 48 | # Using the tool 49 | The tool has two main interfaces, the hypergraph visualization and the nodes & edges panel. 50 | 51 | ## Layout 52 | The hypergraph visualization is an Euler diagram that shows nodes as circles and hyper edges as outlines containing the nodes/circles they contain. The visualization uses a force directed optimization to perform the layout. This algorithm is not perfect and sometimes gives results that the user might want to improve upon. The visualization allows the user to drag nodes and position them directly at any time. The algorithm will re-position any nodes that are not specified by the user. Ctrl (Windows) or Command (Mac) clicking a node will release a pinned node it to be re-positioned by the algorithm. 53 | 54 | ## Views 55 | ### Dual 56 | `hnxwidget` allows you to view the hypergraph and its dual side-by-side. Click the "View fullscreen dual" button in the "View" toolbar to activate this feature. The screenshot below shows an example of the dual view. 57 | 58 | ![Screenshot of dual view](screenshots/hnx-dual-view.png) 59 | 60 | ### Bipartite 61 | Instead of an Euler diagram, the hypergraph can be viewed as a bipartite graph. In this case hyper edges are represented as squares (with nodes still being represented as circles). Black lines are drawn between nodes and hyper edges to indicate that node belongs to that hyper edge. Click the "Convert to bipartite" button in the Edges toolbar to display edges this way. The screenshot below shows an example of the bipartite view. 62 | 63 | ![Screenshot of bipartite view](screenshots/hnx-bipartite-view.png) 64 | 65 | 66 | 67 | ## Selection 68 | Nodes and edges can be selected by clicking them. Nodes and edges can be selected independently of each other, i.e., it is possible to select an edge without selecting the nodes it contains. Multiple nodes and edges can be selected, by holding down Shift while clicking. Shift clicking an already selected node will de-select it. Clicking the background will de-select all nodes and edges. Dragging a selected node will drag all selected nodes, keeping their relative placement. 69 | 70 | Multiple nodes can be selected using the "Brush select nodes" button in the Selection toolbar. This selects all nodes within the rectangle specified by the user by dragging the brush. The animation below shows rectangular brushing of nodes. 71 | 72 | ![Animation of selecting multiple nodes with a rectangular brush](screenshots/hnx-brush-nodes.gif) 73 | 74 | Multiple edges can be selected using the "Brush select edges" button in the Selection toolbar. This selects all edges that contain exactly one endpoint of the brush (either the start or the end). This is useful, for example, for selecting all edges that contain a node by dragging the brush from that node to any area outside the hypergraph. The animation below shows linear brushing of edges. 75 | 76 | ![Animation of selecting multiple edges with a linear brush](screenshots/hnx-brush-edges.gif) 77 | 78 | Selected nodes can be hidden (having their appearance minimized) or removed completely from the visualization. Hiding a node or edge will not cause a change in the layout, wheras removing a node or edge will. The selection can also be expanded. Buttons in the toolbar allow for selecting all nodes contained within selected edges, and selecting all edges containing any selected nodes. 79 | 80 | The toolbar also contains buttons to select all nodes (or edges), un-select all nodes (or edges), or reverse the selected nodes (or edges). An advanced user might: 81 | 82 | * **Select all nodes not in an edge by:** select an edge, select all nodes in that edge, then reverse the selected nodes to select every node not in that edge. 83 | * **Traverse the graph by:** selecting a start node, then alternating select all edges containing selected nodes and selecting all nodes within selected edges 84 | * **Pin Everything by:** hitting the button to select all nodes, then drag any node slightly to activate the pinning for all nodes. 85 | 86 | ## Side Panel 87 | Details on nodes and edges are visible in the side panel. For both nodes and edges, a table shows the node name, degree (or size for edges), its selection state, removed state, and color. These properties can also be controlled directly from this panel. The color of nodes and edges can be set in bulk here as well, for example, coloring by degree. If custom data is passed in for nodes or edges (see `example widget.ipynb`), this data can be encoded with color using the "Color by" dropdown. 88 | 89 | ## Other Features 90 | Nodes with identical edge membership can be collapsed into a super node, which can be helpful for larger hypergraphs. Dragging any node in a super node will drag the entire super node. This feature is available as a toggle in the nodes panel. 91 | 92 | 93 | # Notice: 94 | This computer software was prepared by Battelle Memorial Institute, hereinafter the Contractor, under Contract No. DE-AC05-76RL01830 with the Department of Energy (DOE). All rights in the computer software are reserved by DOE on behalf of the United States Government and the Contractor as provided in the Contract. You are authorized to use this computer software for Governmental purposes but it is not to be released or distributed to the public. NEITHER THE GOVERNMENT NOR THE CONTRACTOR MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. This notice including this sentence must appear on any copies of this computer software. -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render} from 'react-dom' 3 | 4 | import {debounce} from 'lodash'; 5 | 6 | import Grid from '@material-ui/core/Grid'; 7 | import Typography from '@material-ui/core/Typography' 8 | import TextField from '@material-ui/core/TextField'; 9 | 10 | import {HypernetxWidget} from '../../src/' 11 | 12 | const defaultUserInput = `{ 13 | "0": ["FN", "TH"], 14 | "1": ["TH", "JV"], 15 | "2": ["BM", "FN", "JA"], 16 | "3": ["JV", "JU", "CH", "BM"], 17 | "4": ["JU", "CH", "BR", "CN", "CC", "JV", "BM"], 18 | "5": ["TH", "GP"], 19 | "6": ["GP", "MP"], 20 | "7": ["MA", "GP"] 21 | }`; 22 | 23 | const emitChange = debounce( 24 | (value, onChange) => onChange && onChange(value), 25 | 300 26 | ); 27 | 28 | function JSONTextField({defaultValue, onChange, ...props}) { 29 | const [userValue, setUserValue] = React.useState(defaultValue); 30 | const [error, setError] = React.useState(); 31 | 32 | const handleValidate = value => { 33 | try { 34 | // check that value can be parsed 35 | const parsedValue = JSON.parse(value); 36 | 37 | // check that object is the right schema 38 | if (typeof(parsedValue) === 'object' && !Array.isArray(parsedValue)) { 39 | const invalid = Object.entries(parsedValue) 40 | .filter(([k, v]) => !Array.isArray(v)); 41 | 42 | if (Object.keys(parsedValue).length === 0) { 43 | setError('Input is empty') 44 | } else if (invalid.length) { 45 | setError(`Values for {${invalid[0][0]}} are not arrays of strings`) 46 | } else { 47 | emitChange(parsedValue, onChange); 48 | setError(undefined); 49 | } 50 | } else { 51 | setError('Input is not an Object') 52 | } 53 | } catch(e) { 54 | setError(String(e)); 55 | } 56 | } 57 | 58 | const handleChange = ev => { 59 | const value = ev.target.value; 60 | 61 | setUserValue(value); 62 | handleValidate(value); 63 | } 64 | 65 | return 73 | } 74 | 75 | function Demo() { 76 | const [incidenceDict, setIncidenceDict] = React.useState(JSON.parse(defaultUserInput)); 77 | 78 | const nodesSet = new Map(); 79 | const edges = Object.entries(incidenceDict) 80 | .map(([uid, elements]) => { 81 | elements.forEach(uid => { 82 | nodesSet.set(uid, {uid}); 83 | }); 84 | 85 | return {uid, elements} 86 | }); 87 | 88 | const nodes = Array.from(nodesSet.values()); 89 | 90 | return 91 | 92 | hnxwidget Demonstration 93 | 94 | GitHub Repository: 95 | github.com/pnnl/hypernetx-widget 96 | 97 | 98 | 99 | This is hypergraph visualization tool that uses an Euler diagram--nodes 100 | are circles and hyper edges are outlines (rubber bands) 101 | containing the nodes/circles. 102 | 103 | The input data being visualized in the tool can be edited using 104 | the text area on the right. The input is in the same format as the 105 | constructor for a HypernetX 106 | object--a dictionary mapping edges to lists of nodes. 107 | 108 | 109 | 110 | 111 | 112 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | } 125 | 126 | render(, document.querySelector('#demo')) 127 | -------------------------------------------------------------------------------- /notebooks/example widget.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "application/vnd.jupyter.widget-view+json": { 11 | "model_id": "3d4e867482ae44f5b742260bea8cdf88", 12 | "version_major": 2, 13 | "version_minor": 0 14 | }, 15 | "text/plain": [ 16 | "HypernetxWidget(component='HypernetxWidget', props={'nodes': [{'uid': 'JV'}, {'uid': 'GP'}, {'uid': 'BR'}, {'u…" 17 | ] 18 | }, 19 | "metadata": {}, 20 | "output_type": "display_data" 21 | } 22 | ], 23 | "source": [ 24 | "from hnxwidget import HypernetxWidget\n", 25 | "import hypernetx as hnx\n", 26 | "\n", 27 | "scenes = {\n", 28 | " 0: ('FN', 'TH'),\n", 29 | " 1: ('TH', 'JV'),\n", 30 | " 2: ('BM', 'FN', 'JA'),\n", 31 | " 3: ('JV', 'JU', 'CH', 'BM'),\n", 32 | " 4: ('JU', 'CH', 'BR', 'CN', 'CC', 'JV', 'BM'),\n", 33 | " 5: ('TH', 'GP'),\n", 34 | " 6: ('GP', 'MP'),\n", 35 | " 7: ('MA', 'GP'),\n", 36 | "}\n", 37 | "\n", 38 | "H = hnx.Hypergraph(scenes)\n", 39 | "self = HypernetxWidget(H)\n", 40 | "self" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "metadata": {}, 47 | "outputs": [ 48 | { 49 | "data": { 50 | "text/plain": [ 51 | "{'pos': {},\n", 52 | " 'node_fill': {},\n", 53 | " 'edge_stroke': {},\n", 54 | " 'selected_nodes': {},\n", 55 | " 'selected_edges': {},\n", 56 | " 'hidden_nodes': {},\n", 57 | " 'hidden_edges': {},\n", 58 | " 'removed_nodes': {},\n", 59 | " 'removed_edges': {}}" 60 | ] 61 | }, 62 | "execution_count": 2, 63 | "metadata": {}, 64 | "output_type": "execute_result" 65 | } 66 | ], 67 | "source": [ 68 | "self.state" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 3, 74 | "metadata": { 75 | "scrolled": false 76 | }, 77 | "outputs": [ 78 | { 79 | "data": { 80 | "application/vnd.jupyter.widget-view+json": { 81 | "model_id": "f7da4ced6ac74a9d925446172c7310e4", 82 | "version_major": 2, 83 | "version_minor": 0 84 | }, 85 | "text/plain": [ 86 | "HypernetxWidget(component='HypernetxWidget', props={'nodes': [{'uid': 'JV'}, {'uid': 'GP'}, {'uid': 'BR'}, {'u…" 87 | ] 88 | }, 89 | "metadata": {}, 90 | "output_type": "display_data" 91 | } 92 | ], 93 | "source": [ 94 | "HypernetxWidget(H, **self.state)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 4, 100 | "metadata": { 101 | "scrolled": false 102 | }, 103 | "outputs": [ 104 | { 105 | "data": { 106 | "application/vnd.jupyter.widget-view+json": { 107 | "model_id": "a588c96ae4c741ae85857e7abc596850", 108 | "version_major": 2, 109 | "version_minor": 0 110 | }, 111 | "text/plain": [ 112 | "HypernetxWidget(component='HypernetxWidget', props={'nodes': [{'uid': 'JV'}, {'uid': 'GP'}, {'uid': 'BR'}, {'u…" 113 | ] 114 | }, 115 | "metadata": {}, 116 | "output_type": "display_data" 117 | } 118 | ], 119 | "source": [ 120 | "from hnxwidget import HypernetxWidgetView, HypernetxWidget\n", 121 | "\n", 122 | "self = HypernetxWidget(\n", 123 | " H,\n", 124 | " collapse_nodes=True\n", 125 | "# node_labels={'JV': 'Jean Valjean'},\n", 126 | "# edge_labels={0: 'LABEL'},\n", 127 | "# with_node_labels=False,\n", 128 | "# with_edge_labels=False\n", 129 | ")\n", 130 | "\n", 131 | "self" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": 5, 137 | "metadata": {}, 138 | "outputs": [ 139 | { 140 | "data": { 141 | "application/vnd.jupyter.widget-view+json": { 142 | "model_id": "4241b9fe31914240a807ba971c022910", 143 | "version_major": 2, 144 | "version_minor": 0 145 | }, 146 | "text/plain": [ 147 | "HypernetxWidget(component='HypernetxWidget', props={'nodes': [{'uid': 'JV'}, {'uid': 'GP'}, {'uid': 'BR'}, {'u…" 148 | ] 149 | }, 150 | "metadata": {}, 151 | "output_type": "display_data" 152 | } 153 | ], 154 | "source": [ 155 | "import numpy as np\n", 156 | "import pandas as pd\n", 157 | "\n", 158 | "def create_random_data(rows, cols):\n", 159 | " return pd.DataFrame(\n", 160 | " np.random.random((len(rows), len(cols))),\n", 161 | " index=rows,\n", 162 | " columns=cols\n", 163 | " )\n", 164 | "\n", 165 | "self = HypernetxWidget(\n", 166 | " H,\n", 167 | " node_data=create_random_data(list(H), ['a', 'b']).round(3),\n", 168 | " edge_data=create_random_data(list(H.edges), ['c', 'd']).round(3),\n", 169 | " node_labels={'JV': 'Jean Valjean'},\n", 170 | " edge_labels={0: 'some edge'}\n", 171 | ")\n", 172 | "self" 173 | ] 174 | } 175 | ], 176 | "metadata": { 177 | "kernelspec": { 178 | "display_name": "Python 3", 179 | "language": "python", 180 | "name": "python3" 181 | }, 182 | "language_info": { 183 | "codemirror_mode": { 184 | "name": "ipython", 185 | "version": 3 186 | }, 187 | "file_extension": ".py", 188 | "mimetype": "text/x-python", 189 | "name": "python", 190 | "nbconvert_exporter": "python", 191 | "pygments_lexer": "ipython3", 192 | "version": "3.8.5" 193 | } 194 | }, 195 | "nbformat": 4, 196 | "nbformat_minor": 2 197 | } 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hnx-widget", 3 | "version": "0.1.1-beta.0", 4 | "main": "lib/index.js", 5 | "module": "es/index.js", 6 | "files": [ 7 | "css", 8 | "es", 9 | "lib", 10 | "umd" 11 | ], 12 | "scripts": { 13 | "build": "nwb build-react-component", 14 | "build:watch": "nodemon -w src -x 'nwb build-react-component --no-demo'", 15 | "clean": "nwb clean-module && nwb clean-demo", 16 | "start": "nwb serve-react-demo", 17 | "test": "nwb test-react", 18 | "test:coverage": "nwb test-react --coverage", 19 | "test:watch": "nwb test-react --server", 20 | "storybook": "start-storybook -p 6006", 21 | "build-storybook": "build-storybook" 22 | }, 23 | "peerDependencies": { 24 | "react": "16.x" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.2.2", 28 | "@storybook/addon-actions": "^6.1.9", 29 | "@storybook/addon-essentials": "^6.1.9", 30 | "@storybook/addon-links": "^6.1.9", 31 | "@storybook/react": "^6.1.9", 32 | "babel-loader": "^8.0.4", 33 | "core-js": "^3.6.5", 34 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 35 | "nodemon": "^2.0.4", 36 | "nwb": "^0.25.2", 37 | "react": "^16.14.0", 38 | "react-dom": "^16.14.0", 39 | "react-is": "^16.13.1", 40 | "webpack": "^4.44.1" 41 | }, 42 | "author": "", 43 | "homepage": "", 44 | "license": "BSD", 45 | "repository": "", 46 | "keywords": [ 47 | "react-component" 48 | ], 49 | "dependencies": { 50 | "@material-ui/core": "^4.12.3", 51 | "@material-ui/icons": "^4.9.1", 52 | "@material-ui/lab": "^4.0.0-alpha.57", 53 | "d3-array": "^2.11.0", 54 | "d3-brush": "^2.1.0", 55 | "d3-drag": "^2.0.0", 56 | "d3-force": "^2.1.1", 57 | "d3-hierarchy": "^2.0.0", 58 | "d3-polygon": "^2.0.0", 59 | "d3-quadtree": "^2.0.0", 60 | "d3-scale": "^3.2.3", 61 | "d3-scale-chromatic": "^2.0.0", 62 | "d3-selection": "^2.0.0", 63 | "js-graph-algorithms": "^1.0.18", 64 | "jsx-runtime": "^1.2.0", 65 | "lodash": "^4.17.21", 66 | "react-color": "^2.19.3", 67 | "react-colorscales": "^0.7.3", 68 | "react-sizeme": "^3.0.1", 69 | "react-window": "^1.8.6", 70 | "victory": "^35.4.3" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /screenshots/hnx-bipartite-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/screenshots/hnx-bipartite-view.png -------------------------------------------------------------------------------- /screenshots/hnx-brush-edges.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/screenshots/hnx-brush-edges.gif -------------------------------------------------------------------------------- /screenshots/hnx-brush-nodes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/screenshots/hnx-brush-nodes.gif -------------------------------------------------------------------------------- /screenshots/hnx-dual-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/screenshots/hnx-dual-view.png -------------------------------------------------------------------------------- /screenshots/hnx-widget-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/screenshots/hnx-widget-screenshot.png -------------------------------------------------------------------------------- /setup-develop.sh: -------------------------------------------------------------------------------- 1 | npm install 2 | npm run build -- --copy-files --no-demo 3 | cd widget/js 4 | npm install 5 | npm run prepublish 6 | cd .. 7 | pip install -e . 8 | cd .. 9 | jupyter nbextension install --py --symlink --sys-prefix hnxwidget 10 | jupyter nbextension enable --py --sys-prefix hnxwidget 11 | jupyter labextension install js 12 | -------------------------------------------------------------------------------- /src/HypernetxWidgetDualView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import HypernetxWidgetView from './HypernetxWidgetView' 4 | 5 | export const HypernetxWidgetDualView = ({ 6 | nodes, 7 | edges, 8 | ...props 9 | }) => { 10 | 11 | const edgesMap = new Map(); 12 | 13 | edges.forEach(({uid, elements}) => 14 | elements.forEach(e => { 15 | if (!edgesMap.has(e)) { 16 | edgesMap.set(e, []) 17 | } 18 | edgesMap.get(e).push(uid); 19 | }) 20 | ); 21 | 22 | const dualProps = {}; 23 | dualProps.edgeStroke = props.nodeFill; 24 | dualProps.edgeLabelColor = props.nodeLabelColor; 25 | dualProps.nodeData = props.edgeData; 26 | dualProps.nodeLabels = props.edgeLabels; 27 | dualProps.edgeLabels = props.nodeLabels; 28 | dualProps.withEdgeLabels = props.withNodeLabels; 29 | dualProps.withNodeLabels = props.withEdgeLabels; 30 | dualProps.nodeFill = props.edgeStroke; 31 | dualProps.selectedNodes = props.selectedEdges; 32 | dualProps.hiddenNodes = props.hiddenEdges; 33 | dualProps.removedNodes = props.removedEdges; 34 | dualProps.selectedEdges = props.selectedNodes; 35 | dualProps.hiddenEdges = props.hiddenNodes; 36 | dualProps.removedEdges = props.removedNodes; 37 | dualProps.nodeFontSize = props.edgeFontSize; 38 | dualProps.edgeFontSize = props.nodeFontSize; 39 | dualProps.onClickNodes = props.onClickEdges; 40 | dualProps.onClickEdges = props.onClickNodes; 41 | 42 | return ({uid}))} 44 | edges={ 45 | Array.from(edgesMap.entries()) 46 | .map(([uid, elements]) => ({uid, elements})) 47 | } 48 | {...props} 49 | {...dualProps} 50 | /> 51 | } 52 | 53 | export default HypernetxWidgetDualView; 54 | -------------------------------------------------------------------------------- /src/HypernetxWidgetView.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useMemo} from 'react' 2 | 3 | import { withSize } from 'react-sizeme' 4 | 5 | import {debounce, throttle} from 'lodash' 6 | 7 | import {brush} from 'd3-brush' 8 | import {drag} from 'd3-drag' 9 | import {group, maxIndex, merge, mean, min, max, range, sum, extent} from 'd3-array' 10 | import {pack, hierarchy} from 'd3-hierarchy' 11 | import {select} from 'd3-selection' 12 | import {forceSimulation, forceLink, forceManyBody, forceCenter, forceCollide} from 'd3-force' 13 | import {polygonCentroid, polygonContains, polygonHull} from 'd3-polygon' 14 | import {quadtree} from 'd3-quadtree' 15 | 16 | import {TopologicalSort, DiGraph} from 'js-graph-algorithms' 17 | 18 | import {NavigableSVG} from './NavigableSVG' 19 | 20 | import './hnx-widget.css' 21 | 22 | // export const now = () => +(new Date()); 23 | export const now = () => new Date().toLocaleString(); 24 | 25 | const throttledConsole = throttle(console.log, 1000); 26 | 27 | const styleEncodings = { 28 | Stroke: 'stroke', 29 | StrokeWidth: 'stroke-width', 30 | Fill: 'fill' 31 | } 32 | 33 | const encodeProps = (selection, key, props) => { 34 | Object.entries(props).forEach(([k, encoding]) => { 35 | const style = styleEncodings[k.replace(/edge|node|edgeLabel|nodeLabel/, '')] 36 | if (style && encoding) { 37 | selection.attr(style, d => encoding[key(d)]); 38 | } 39 | }) 40 | } 41 | 42 | const createHandleSelection = callback => (ev, data) => { 43 | if (!(ev.ctrlKey || ev.metaKey)) { 44 | ev.stopPropagation(); 45 | ev.preventDefault(); 46 | callback(ev, data); 47 | } 48 | } 49 | 50 | const forceMultiDragBehavior = (selection, simulation, elements, unpinned) => { 51 | const [width, height] = simulation.size; 52 | 53 | function dragstarted(event) { 54 | // subject.x, subject.y is the location of node 55 | const {x, y, uid} = event.subject; 56 | 57 | if (sum(elements, d => d.uid === uid) === 0) { 58 | elements = [event.subject]; 59 | } 60 | 61 | // loop over each child group of nodes 62 | elements.forEach(d => { 63 | d.dx = d.x - x; 64 | d.dy = d.y - y; 65 | }); 66 | 67 | const dr = 5; // TODO: fix dr hard coding 68 | 69 | event.subject.dxRange = [ 70 | min(elements, ({numBands=-1, dx, r}) => dx - r - dr*(1 + numBands)), 71 | max(elements, ({numBands=-1, dx, r}) => dx + r + dr*(1 + numBands)) 72 | ]; 73 | 74 | event.subject.dyRange = [ 75 | min(elements, ({numBands=-1, dy, r}) => dy - r - dr*(1 + numBands)), 76 | max(elements, ({numBands=-1, dy, r}) => dy + r + dr*(1 + numBands)) 77 | ]; 78 | } 79 | 80 | function dragged(event) { 81 | simulation.alphaTarget(0.3).restart(); 82 | 83 | 84 | // event.x, event.y is the location of the drag 85 | const {dx, dy, dxRange, dyRange} = event.subject; 86 | const [minDx, maxDx] = dxRange; 87 | const [minDy, maxDy] = dyRange; 88 | 89 | let {x, y} = event; 90 | 91 | x = Math.min(width - maxDx, Math.max(x, -minDx)); 92 | y = Math.min(height - maxDy, Math.max(y, -minDy)); 93 | 94 | elements.forEach(d => { 95 | d.fx = x + d.dx; 96 | d.fy = y + d.dy; 97 | d.pinned = now(); 98 | }) 99 | } 100 | 101 | function dragended(event) { 102 | simulation.alphaTarget(0); 103 | } 104 | 105 | function unfix(event, d) { 106 | if (event) { 107 | // event.stopPropagation(); 108 | event.preventDefault(); 109 | } 110 | 111 | d.fx = undefined; 112 | d.fy = undefined; 113 | } 114 | 115 | selection 116 | .each(function(d) { 117 | if (d.pinned < unpinned) { 118 | unfix(undefined, d) 119 | } 120 | }) 121 | .on('click.force', (ev, d) => { 122 | if ((ev.ctrlKey || ev.metaKey) && d.fx !== undefined) { 123 | ev.stopPropagation(); 124 | unfix(ev, d); 125 | simulation.alpha(0.3).restart(); 126 | } 127 | }) 128 | .call(drag() 129 | .on('start', dragstarted) 130 | .on('drag', dragged) 131 | .on('end', dragended) 132 | ); 133 | } 134 | 135 | const forceEdgeDragBehavior = (selection, simulation) => { 136 | const [width, height] = simulation.size; 137 | 138 | function dragstarted(event) { 139 | // subject.x, subject.y is the location of node 140 | const {x, y, elements} = event.subject; 141 | 142 | // loop over each child group of nodes 143 | elements.forEach(d => { 144 | d.dx = d.x - x; 145 | d.dy = d.y - y; 146 | }); 147 | 148 | event.subject.dxRange = [ 149 | min(elements, d => d.dx - d.r), 150 | max(elements, d => d.dx + d.r) 151 | ]; 152 | 153 | 154 | event.subject.dyRange = [ 155 | min(elements, d => d.dy - d.r), 156 | max(elements, d => d.dy + d.r) 157 | ]; 158 | } 159 | 160 | function dragged(event) { 161 | simulation.alphaTarget(0.3).restart(); 162 | 163 | // event.x, event.y is the location of the drag 164 | const {dx, dy, elements, dxRange, dyRange} = event.subject; 165 | const [minDx, maxDx] = dxRange; 166 | const [minDy, maxDy] = dyRange; 167 | 168 | let {x, y} = event; 169 | 170 | x = Math.min(width - maxDx, Math.max(x, -minDx)); 171 | y = Math.min(height - maxDy, Math.max(y, -minDy)); 172 | 173 | elements.forEach(d => { 174 | d.fx = x + d.dx; 175 | d.fy = y + d.dy; 176 | d.pinned = now(); 177 | }) 178 | } 179 | 180 | function dragended(event) { 181 | simulation.alphaTarget(0); 182 | } 183 | 184 | selection 185 | .call(drag() 186 | .on('start', dragstarted) 187 | .on('drag', dragged) 188 | .on('end', dragended) 189 | ); 190 | } 191 | 192 | const createTooltipData = (ev, uid, {xOffset=3, labels={}, data}) => { 193 | return { 194 | x: ev.offsetX, 195 | y: ev.offsetY, 196 | xOffset, 197 | title: uid in labels ? `${labels[uid]} (${uid})` : uid, 198 | content: data ? data[uid] : undefined 199 | } 200 | } 201 | 202 | const classedByDict = (selection, props) => 203 | Object.entries(props) 204 | .forEach(([className, dict={}]) => 205 | selection.classed(className, d => dict[d.uid]) 206 | ) 207 | 208 | const Nodes = ({internals, simulation, nodeData, onClickNodes=Object, onChangeTooltip=Object, withNodeLabels=true, nodeFill, nodeStroke, nodeStrokeWidth, selectedNodes={}, hiddenNodes, removedNodes, nodeLabels={}, unpinned, bipartite, nodeFontSize={}, _model}) => 209 | { 210 | 211 | const selectedInternals = internals.filter(({children}) => 212 | sum(children, d => selectedNodes[d.uid]) 213 | ); 214 | 215 | const groups = select(ele) 216 | .selectAll('g.group') 217 | .data(internals) 218 | .join(enter => { 219 | const g = enter.append('g'); 220 | g.append('circle').classed('internal', true); 221 | return g; 222 | }) 223 | .classed('group', true) 224 | .classed('error', d => !bipartite && d.violations > 0) 225 | .call(forceMultiDragBehavior, simulation, selectedInternals, unpinned); 226 | 227 | groups.select('circle.internal') 228 | .attr('r', d => d.r); 229 | 230 | const circles = groups.selectAll('g') 231 | .data(d => d.children) 232 | .join( 233 | enter => { 234 | const g = enter.append('g') 235 | g.append('circle').classed('bottom', true); 236 | g.append('circle').classed('top', true) 237 | .attr('fill', 'url(#checkerboard)'); 238 | g.append('text'); 239 | return g; 240 | } 241 | ) 242 | .attr('transform', d => `translate(${d.x}, ${d.y})`) 243 | .on('click.selection', createHandleSelection(onClickNodes)) 244 | .on('mouseover', (ev, d) => 245 | d.height === 0 && 246 | onChangeTooltip(createTooltipData(ev, d.data.uid, {xOffset: d.r + 3, labels: nodeLabels, data: nodeData})) 247 | ) 248 | .on('mouseout', (ev, d) => d.height === 0 && onChangeTooltip()) 249 | .call(encodeProps, d => d.data.uid, {nodeFill, nodeStroke, nodeStrokeWidth}) 250 | .call(classedByDict, {'selected': selectedNodes, 'hiddenState': hiddenNodes}) 251 | 252 | circles.select('circle.bottom') 253 | .attr('r', d => d.r); 254 | 255 | circles.select('circle.top') 256 | .attr('r', d => d.r); 257 | 258 | circles.select('text') 259 | .text(d => d.data.uid in nodeLabels ? nodeLabels[d.data.uid] : d.data.uid) 260 | .style('font-size', d => nodeFontSize[d.uid] ? String(nodeFontSize[d.uid]) + 'pt' : undefined) 261 | .style('visibility', withNodeLabels ? undefined : 'hidden'); 262 | 263 | const updateModel = throttle(() => { 264 | if (_model) { 265 | const pos = merge(internals.map(d => d.children)) 266 | .map(d => ([d.data.uid, [d.parent.x + d.x, d.parent.y + d.y]])); 267 | 268 | _model.set('pos', Object.fromEntries(pos)); 269 | _model.save(); 270 | } 271 | }, 1000); 272 | 273 | simulation.on('tick.nodes', d => { 274 | // throttledConsole('ticking', simulation.alpha(), simulation.alphaTarget()); 275 | 276 | groups 277 | .attr('transform', d => `translate(${d.x},${d.y})`) 278 | .classed('fixed', d => d.fx !== undefined) 279 | .classed('error', d => !bipartite && d.violations > 0); 280 | 281 | updateModel(); 282 | }); 283 | }}/> 284 | 285 | const HyperEdges = ({internals, edges, simulation, edgeData, dx=15, dr=5, nControlPoints=24, withEdgeLabels=true, edgeStroke, edgeStrokeWidth, selectedEdges, hiddenEdges, removedEdges, edgeLabels={}, edgeFontSize={}, onClickEdges=Object, onChangeTooltip=Object}) => 286 | { 287 | const controlPoints = range(nControlPoints) 288 | .map(i => { 289 | const theta = 2*Math.PI*i/nControlPoints; 290 | return [Math.cos(theta), Math.sin(theta)]; 291 | }); 292 | 293 | const groups = select(ele) 294 | .selectAll('g.edge') 295 | .data(edges) 296 | .join( 297 | enter => { 298 | const g = enter.append('g').classed('edge', true); 299 | 300 | g.append('path') 301 | .attr('stroke', 'black'); 302 | 303 | const gLabel = g.append('g').classed('label', true); 304 | 305 | gLabel.append('line') 306 | 307 | gLabel.append('circle'); 308 | 309 | gLabel.append('text'); 310 | 311 | return g; 312 | } 313 | ) 314 | .attr('fill', d => edgeStroke && d.uid in edgeStroke ? edgeStroke[d.uid] : 'black') 315 | .attr('stroke', d => edgeStroke && d.uid in edgeStroke ? edgeStroke[d.uid] : 'black') 316 | .on('mouseover', (ev, d) => 317 | onChangeTooltip(createTooltipData(ev, d.uid, {labels: edgeLabels, data: edgeData})) 318 | ) 319 | .on('mouseout', () => onChangeTooltip()) 320 | .on('click.selection', createHandleSelection(onClickEdges)) 321 | .call(forceEdgeDragBehavior, simulation) 322 | .call(classedByDict, {'selected': selectedEdges, 'hiddenState': hiddenEdges}); 323 | 324 | 325 | groups.select('path') 326 | .call(encodeProps, d => d.uid, {edgeStroke, edgeStrokeWidth}); 327 | 328 | groups.select('.label text') 329 | .style('font-size', d => edgeFontSize[d.uid] ? String(edgeFontSize[d.uid]) + 'pt' : undefined) 330 | .text(d => d.uid in edgeLabels ? edgeLabels[d.uid] : d.uid) 331 | 332 | groups.select('g.label') 333 | .style('visibility', withEdgeLabels ? undefined : 'hidden'); 334 | 335 | const xValue = d => d[0]; 336 | const yValue = d => d[1]; 337 | 338 | const rightMost = points => { 339 | const idx = maxIndex(points, xValue); 340 | 341 | return idx !== -1 342 | ? points[idx] 343 | : points[0]; 344 | } 345 | 346 | const length = ([x1, y1], [x2, y2]) => 347 | Math.sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)) 348 | 349 | const getCandidateLabelAnchors = (points, ranges=[.5, .25, .75], minLength=10, dx=1, r=15) => { 350 | const midpoints = []; 351 | 352 | const pointsWithAngle = points 353 | .map((point, i, a) => { 354 | const [x1, y1] = point; 355 | const [x2, y2] = a[(i + 1)%a.length]; 356 | 357 | const angle = Math.atan2(y1 - y2, x1 - x2) - Math.PI/2; 358 | 359 | const textPoint = [ 360 | r*Math.cos(angle), 361 | r*Math.sin(angle) 362 | ]; 363 | 364 | const length = Math.sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)); 365 | 366 | if (length >= minLength && !polygonContains(points, [x1 + dx, y1])) { 367 | ranges.forEach(a => 368 | midpoints.push({ 369 | point: [ 370 | a*(x2 - x1) + x1, 371 | a*(y2 - y1) + y1, 372 | ], 373 | textPoint 374 | }) 375 | ); 376 | } 377 | 378 | return {point, textPoint}; 379 | }); 380 | 381 | return midpoints.length ? midpoints : pointsWithAngle; 382 | } 383 | 384 | simulation.on('tick.hulls', d => { 385 | const renderedLabels = []; 386 | 387 | internals.forEach(d => d.numBands = 0); 388 | 389 | groups.select('path') 390 | .attr('d', d => { 391 | const {elements} = d; 392 | let points = []; 393 | 394 | elements.forEach(ele => { 395 | const {r, x, y} = ele; 396 | const level = ele.numBands++; 397 | 398 | controlPoints.forEach(([cx, cy]) => { 399 | const radius = r + dr*(1 + level); 400 | return points.push([radius*cx + x, radius*cy + y]); 401 | }) 402 | }); 403 | 404 | points = points.length === 0 405 | ? controlPoints.map(([cx, cy]) => ([d.x + 2*dr*cx, d.y + 2*dr*cy])) 406 | : polygonHull(points); 407 | 408 | d.points = points; 409 | d.centroid = polygonCentroid(points); 410 | 411 | const candidatePoints = getCandidateLabelAnchors(points); 412 | 413 | const bestPoint = maxIndex( 414 | candidatePoints.map(({point}) => 415 | renderedLabels.length === 0 416 | ? 0 417 | : min(renderedLabels, ([lx, ly]) => 418 | Math.abs(point[1] - ly) 419 | ) 420 | ) 421 | ); 422 | 423 | const {point, textPoint} = candidatePoints[bestPoint]; 424 | d.markerLocation = point; 425 | d.textLocation = textPoint; 426 | 427 | renderedLabels.push(point); 428 | 429 | return 'M' + points.map(d => d.join(',')).join('L') + 'Z' 430 | }); 431 | 432 | groups.select('g.label') 433 | .attr('transform', d => `translate(${d.markerLocation[0]},${d.markerLocation[1]})`); 434 | 435 | groups.select('.label circle') 436 | .attr('r', 3); 437 | 438 | groups.select('.label line') 439 | .attr('x1', d => 0) 440 | .attr('y1', d => 0) 441 | .attr('x2', d => d.textLocation[0]) 442 | .attr('y2', d => d.textLocation[1]); 443 | 444 | groups.select('.label text') 445 | .attr('x', d => d.textLocation[0]) 446 | .attr('y', d => d.textLocation[1]) 447 | .attr('dx', '.1em') 448 | .style('text-anchor', 'start'); 449 | 450 | }); 451 | }}/> 452 | 453 | const BipartiteLinks = ({links, simulation}) => 454 | { 455 | const lines = select(ele) 456 | .selectAll('line') 457 | .data(links) 458 | .join('line') 459 | .style('stroke', 'black'); 460 | 461 | simulation.on('tick.bipartite-lines', d => { 462 | lines 463 | .attr('x1', d => d.source.x) 464 | .attr('y1', d => d.source.y) 465 | .attr('x2', d => d.target.x) 466 | .attr('y2', d => d.target.y); 467 | }); 468 | 469 | }}/> 470 | 471 | const BipartiteEdges = ({internals, edges, simulation, edgeLabels, edgeData, edgeStroke, edgeStrokeWidth, selectedNodes={}, selectedEdges={}, hiddenEdges={}, unpinned, withEdgeLabels, onClickEdges=Object, onChangeTooltip=Object}) => 472 | { 473 | const selectedInternals = internals.filter(({children}) => 474 | sum(children, d => selectedNodes[d.uid]) 475 | ); 476 | 477 | const rectDimensions = selection => 478 | selection 479 | .attr('width', d => d.width) 480 | .attr('height', d => d.width) 481 | .attr('x', d => -d.width/2) 482 | .attr('y', d => -d.width/2); 483 | 484 | const groups = select(ele) 485 | .selectAll('g') 486 | .data(edges) 487 | .join(enter => { 488 | const g = enter.append('g') 489 | .attr('fill', d => edgeStroke && d.uid in edgeStroke ? edgeStroke[d.uid] : 'black'); 490 | 491 | g.append('rect') 492 | .call(rectDimensions) 493 | .attr('stroke', 'black') 494 | .call(encodeProps, d => d.uid, {edgeStroke, edgeStrokeWidth}) 495 | 496 | g.append('text') 497 | .text(d => edgeLabels && d.uid in edgeLabels ? edgeLabels[d.uid] : d.uid) 498 | 499 | return g; 500 | }) 501 | .call(classedByDict, {'selected': selectedEdges, 'hiddenState': hiddenEdges}) 502 | .on('mouseover', (ev, d) => 503 | onChangeTooltip(createTooltipData(ev, d.uid, {labels: edgeLabels, data: edgeData})) 504 | ) 505 | .on('mouseout', () => onChangeTooltip()) 506 | .call(forceMultiDragBehavior, simulation, selectedInternals, unpinned) 507 | .on('click.selection', createHandleSelection(onClickEdges)) 508 | 509 | groups.select('text') 510 | .style('visibility', withEdgeLabels ? undefined : 'hidden'); 511 | 512 | simulation.on('tick.bipartite-edges', d => { 513 | groups 514 | .attr('transform', d => `translate(${d.x},${d.y})`) 515 | .classed('fixed', d => d.fx !== undefined); 516 | }); 517 | 518 | }}/> 519 | 520 | const Tooltip = ({x, y, xOffset=20, title, content={}}) => 521 |
522 | 526 | 527 | 528 | 531 | 533 | 534 | 535 | 536 | { Object.entries(content) 537 | .map(([k, v]) => 538 | 539 | 540 | 541 | 542 | ) 543 | } 544 | 545 |
529 | {title} 530 | 532 |
{k}{v}
546 |
547 | 548 | const performCollapseNodes = ({nodes, edges, collapseNodes, nodeSize={}}) => { 549 | 550 | const edgesOfNodes = new Map( 551 | nodes.map(d => ([d.uid, []])) 552 | ); 553 | 554 | edges.forEach(d => 555 | d.elements.forEach(k => 556 | edgesOfNodes.get(k).push(d.uid) 557 | ) 558 | ); 559 | 560 | const grouped = group( 561 | nodes, 562 | ({uid}) => collapseNodes 563 | ? edgesOfNodes.get(uid).sort().join(',') 564 | : uid 565 | ); 566 | 567 | const tree = Array.from(grouped.values()) 568 | .map(elements => ({elements})); 569 | 570 | // construct a simple hierarchy out of the nodes 571 | return hierarchy({elements: tree}, d => d.elements) 572 | .sum(({uid}) => nodeSize[uid] || 1); 573 | } 574 | 575 | const sortHyperEdges = edges => { 576 | // sort hyper edges 577 | // edges that are enclosed are drawn last 578 | // when there is a tie, the smaller edge is drawn last 579 | const G = new DiGraph(edges.length); 580 | 581 | for (let i = 0; i < edges.length; i++) { 582 | const si = new Set(edges[i].elements.map(d => d.uid)); 583 | const nsi = si.size; 584 | 585 | for (let j = i + 1; j < edges.length; j++) { 586 | const sj = edges[j].elements.map(d => d.uid); 587 | const nsj = sj.length 588 | const nsij = sum(sj, d => si.has(d)); 589 | 590 | if (nsij === nsi) { 591 | // j contains i 592 | G.addEdge(i, j); 593 | } else if (nsij === nsj) { 594 | // i contains j 595 | G.addEdge(j, i); 596 | } else if (nsij > 0 && nsi > nsj) { 597 | // neither contains the other, they overlap, and i is bigger 598 | G.addEdge(j, i); 599 | } else if (nsij > 0 && nsj > nsi) { 600 | // neither contains the other, they overlap, and j is bigger 601 | G.addEdge(i, j); 602 | } 603 | } 604 | } 605 | 606 | return new TopologicalSort(G) 607 | .order() 608 | .map(i => edges[i]); 609 | } 610 | 611 | // source: https://github.com/d3/d3-quadtree#quadtree_visit 612 | function search(quadtree, xmin, ymin, xmax, ymax) { 613 | const results = []; 614 | const x = quadtree.x(); 615 | const y = quadtree.y(); 616 | 617 | quadtree.visit(function(node, x1, y1, x2, y2) { 618 | if (!node.length) { 619 | do { 620 | var d = node.data; 621 | if (x(d) >= xmin && x(d) < xmax && y(d) >= ymin && y(d) < ymax) { 622 | results.push(d); 623 | } 624 | } while (node = node.next); 625 | } 626 | return x1 >= xmax || y1 >= ymax || x2 < xmin || y2 < ymin; 627 | }); 628 | return results; 629 | } 630 | 631 | // source: https://math.stackexchange.com/questions/2193720/find-a-point-on-a-line-segment-which-is-the-closest-to-other-point-not-on-the-li 632 | // const _zero2D = [0, 0] 633 | 634 | // function closestPointBetween2D(P, A, B) { 635 | // const v = [B[0] - A[0], B[1] - A[1]] 636 | // const u = [A[0] - P[0], A[1] - P[1]] 637 | // const vu = v[0] * u[0] + v[1] * u[1] 638 | // const vv = v[0] ** 2 + v[1] ** 2 639 | // const t = -vu / vv 640 | // if (t >= 0 && t <= 1) return _vectorToSegment2D(t, _zero2D, A, B) 641 | // const g0 = _sqDiag2D(_vectorToSegment2D(0, P, A, B)) 642 | // const g1 = _sqDiag2D(_vectorToSegment2D(1, P, A, B)) 643 | // return g0 <= g1 ? A : B 644 | // } 645 | 646 | // function _vectorToSegment2D(t, P, A, B) { 647 | // return [ 648 | // (1 - t) * A[0] + t * B[0] - P[0], 649 | // (1 - t) * A[1] + t * B[1] - P[1], 650 | // ] 651 | // } 652 | 653 | // function _sqDiag2D(P) { return P[0] ** 2 + P[1] ** 2 } 654 | 655 | const planarForce = (nodes, edges) => { 656 | const px = d => d[0]; 657 | const py = d => d[1]; 658 | 659 | function force(alpha) { 660 | // naive implementation 661 | // for each combination of node and edge 662 | 663 | nodes.forEach(v => v.violations = 0); 664 | 665 | const qt = quadtree() 666 | .x(d => d.x) 667 | .y(d => d.y) 668 | .addAll(nodes); 669 | 670 | edges.forEach(({points, centroid, elementSet=new Set()}) => { 671 | if (points) { 672 | const [xmin, xmax] = extent(points, px); 673 | const [ymin, ymax] = extent(points, py); 674 | 675 | search(qt, xmin, ymin, xmax, ymax) 676 | .forEach(v => { 677 | const {x, y, uid} = v; 678 | 679 | if (!elementSet.has(uid) && polygonContains(points, [x, y])) { 680 | v.violations += 1; 681 | 682 | const [cx, cy] = centroid; 683 | const dx = x - cx; 684 | const dy = y - cy; 685 | const r = Math.sqrt(dx*dx + dy*dy); 686 | 687 | v.vx += dx/r*alpha; 688 | v.vy += dy/r*alpha; 689 | } 690 | }); 691 | } 692 | }); 693 | } 694 | 695 | force.initialize = () => { 696 | } 697 | 698 | return force; 699 | } 700 | 701 | const intervalIntersect = (s1, s2, t1, t2) => 702 | !(s2 < t1 || t2 < s1) 703 | 704 | const NodeRectangularBrush = ({simulation, onClickNodes=Object}) => { 705 | return { 706 | const handleBrush = ev => { 707 | // because nodes are potentially moving we just do this the slow way 708 | 709 | if (!ev.selection) return; 710 | 711 | const [p1, p2] = ev.selection; 712 | const [x1, y1] = p1; 713 | const [x2, y2] = p2; 714 | 715 | const selectedNodes = simulation.nodes() 716 | .filter(({children, r, x, y}) => 717 | children !== undefined && 718 | intervalIntersect(x - r, x + r, x1, x2) && 719 | intervalIntersect(y - r, y + r, y1, y2) 720 | ); 721 | 722 | g.call(brush().clear); 723 | 724 | onClickNodes(ev, selectedNodes); 725 | } 726 | 727 | const g = select(ele) 728 | .call( 729 | brush() 730 | .extent([[0, 0], simulation.size]) 731 | .on('end', handleBrush) 732 | ); 733 | }}/> 734 | } 735 | 736 | const EdgeLinearBrush = ({simulation, onClickEdges=Object}) => { 737 | const getPointerLocation = ev => { 738 | const {offsetX, offsetY} = ev.sourceEvent; 739 | return [offsetX, offsetY]; 740 | } 741 | 742 | return { 743 | const g = select(ele); 744 | 745 | let start = [0, 0]; 746 | let end = [0, 0]; 747 | 748 | // The coordiantes of the bounding box are in the right 749 | // coordinate system, but are not ordered according to 750 | // how the user dragged. So we'll use the start and end 751 | // ordering to tease this out. 752 | const getBrushLine = ev => { 753 | const [sx, sy] = start; 754 | const [ex, ey] = end; 755 | const [[x0, y0], [x1, y1]] = ev.selection; 756 | 757 | return [ 758 | [sx > ex ? x0 : x1, sy > ey ? y0 : y1], 759 | [sx > ex ? x1 : x0, sy > ey ? y1 : y0] 760 | ]; 761 | } 762 | 763 | const handleStart = ev => 764 | start = getPointerLocation(ev); 765 | 766 | const handleBrush = ev => { 767 | end = getPointerLocation(ev); 768 | 769 | g.selectAll('line') 770 | .data([getBrushLine(ev)]) 771 | .join('line') 772 | .attr('x1', d => d[0][0]) 773 | .attr('y1', d => d[0][1]) 774 | .attr('x2', d => d[1][0]) 775 | .attr('y2', d => d[1][1]) 776 | .style('visibility', 'visible'); 777 | } 778 | 779 | const handleEnd = ev => { 780 | g.select('line') 781 | .style('visibility', 'hidden') 782 | 783 | if (!ev.selection) { 784 | onClickEdges(ev, []); 785 | return; 786 | }; 787 | 788 | const [pointerStart, pointerEnd] = getBrushLine(ev); 789 | 790 | const selectedEdges = simulation.nodes() 791 | .filter(({points}) => 792 | points !== undefined && 793 | (polygonContains(points, pointerStart) ^ polygonContains(points, pointerEnd)) 794 | ); 795 | 796 | onClickEdges(ev, selectedEdges); 797 | } 798 | 799 | g.call( 800 | brush() 801 | .extent([[0, 0], simulation.size]) 802 | .on('start', handleStart) 803 | .on('brush', handleBrush) 804 | .on('end', handleEnd) 805 | ); 806 | }}/> 807 | } 808 | 809 | 810 | export const HypernetxWidgetView = ({nodes, edges, removedNodes, removedEdges, pinned, size, aspect=1, ignorePlanarForce, pos={}, collapseNodes, nodeSize, selectionMode, navigation, ...props}) => { 811 | let {width, height} = size; 812 | 813 | if (height === null) { 814 | height = width/aspect; 815 | } else if (width === null) { 816 | width = height*aspect; 817 | } 818 | 819 | const derivedProps = useMemo( 820 | () => { 821 | removedNodes = removedNodes || {}; 822 | removedEdges = removedEdges || {}; 823 | 824 | nodes = nodes.filter(({uid}) => !removedNodes[uid]); 825 | edges = edges.filter(({uid}) => !removedEdges[uid]) 826 | .map(({elements, ...rest}) => ({ 827 | elements: elements.filter(uid => !removedNodes[uid]), 828 | ...rest 829 | })); 830 | 831 | const tree = performCollapseNodes({nodes, edges, collapseNodes, nodeSize}) 832 | .each((d, i) => d.uid = 'uid' in d.data ? d.data.uid : i); 833 | 834 | const nodesMap = new Map( 835 | tree.leaves().map(d => ([d.uid, d])) 836 | ); 837 | 838 | // replace node ids with references to actual nodes 839 | edges = edges 840 | .map(({elements, ...rest}) => { 841 | const edge = new Map( 842 | elements.map( 843 | v => ([nodesMap.get(v).parent.uid, nodesMap.get(v).parent]) 844 | ) 845 | ); 846 | 847 | const elementsAry = Array.from(edge.values()) 848 | 849 | return { 850 | r: 0, width: 30, // this is interacting with the force algorithm, rename to fix 851 | elements: elementsAry, 852 | elementSet: new Set(elementsAry.map(d => d.uid)), 853 | ...rest 854 | } 855 | }); 856 | 857 | edges = sortHyperEdges(edges); 858 | 859 | // 860 | 861 | const radius = d => Math.sqrt(d.value/Math.PI); 862 | 863 | const rootRadius = radius(tree); 864 | const scale = Math.min(width, height)/(10*rootRadius); 865 | 866 | const layout = pack() 867 | .padding(2) 868 | .radius(d => scale*radius(d)) 869 | .size([width, height]) 870 | (tree); 871 | 872 | // pre-specified children 873 | // set internal node position to the mean of 874 | // its children contained in pos 875 | tree.each(d => { 876 | if (d.depth === 1) { 877 | const children = d.children.filter(c => c.data.uid in pos); 878 | 879 | if (children.length > 0) { 880 | d.fx = mean(children, c => pos[c.data.uid][0]); 881 | d.fy = mean(children, c => pos[c.data.uid][1]); 882 | 883 | d.pinned = now(); 884 | } 885 | } 886 | }) 887 | 888 | // adjust position of the children relative to their parents 889 | 890 | tree.leaves().forEach(d => { 891 | d.x -= d.parent.x; 892 | d.y -= d.parent.y; 893 | }); 894 | 895 | const internals = tree.children; 896 | 897 | // setup the force simulation 898 | 899 | const links = []; 900 | edges.forEach(source => 901 | source.elements.forEach(target => 902 | links.push({source, target}) 903 | ) 904 | ); 905 | 906 | return {links, edges, internals}; 907 | }, 908 | [nodes, edges, removedNodes, removedEdges, collapseNodes, nodeSize] 909 | ); 910 | 911 | const {bipartite, unpinned} = props; 912 | 913 | const [simulation] = useState(forceSimulation()); 914 | 915 | // re-initialize the simulation if certain variables have changed 916 | useMemo(() => { 917 | const {links, edges, internals} = derivedProps; 918 | 919 | const {dr=5} = props; // TODO: fix dr hard coding 920 | 921 | function boundNode(d) { 922 | const {r=0, numBands=-1} = d; 923 | const drMax = (numBands + 1)*dr 924 | 925 | d.x = Math.max(r + drMax, Math.min(width - r - drMax, d.x)); 926 | d.y = Math.max(r + drMax, Math.min(height - r - drMax, d.y)); 927 | } 928 | 929 | // save the old values in the simulation 930 | 931 | const nodeSimulationValues = new Map(); 932 | const edgeSimulationValues = new Map(); 933 | 934 | simulation.nodes().forEach(({children, x, y, vx, vy, fx, fy, pinned, uid}) => { 935 | if (children === undefined) { 936 | edgeSimulationValues.set(uid, ({x, y, vx, vy, pinned})); 937 | } else { 938 | children.forEach(c => 939 | nodeSimulationValues.set(c.uid, ({ 940 | x: x + c.x, 941 | y: y + c.y, 942 | fx: fx + c.x, 943 | fy: fy + c.y, 944 | vx, vy, pinned 945 | })) 946 | ) 947 | } 948 | }); 949 | 950 | // restore the old values if available 951 | 952 | const recallSimulationValues = (d, key, agg=mean) => { 953 | d[key] = agg(d.children, c => (nodeSimulationValues.get(c.uid) || {})[key]); 954 | } 955 | 956 | internals.forEach(d => { 957 | recallSimulationValues(d, 'pinned', min); 958 | recallSimulationValues(d, 'fx'); 959 | recallSimulationValues(d, 'fy'); 960 | recallSimulationValues(d, 'x'); 961 | recallSimulationValues(d, 'y'); 962 | recallSimulationValues(d, 'vx'); 963 | recallSimulationValues(d, 'vy'); 964 | }); 965 | 966 | edges.forEach(d => { 967 | const values = edgeSimulationValues.get(d.uid) || {}; 968 | 969 | d.x = values.x; 970 | d.y = values.y; 971 | d.vx = values.vx; 972 | d.vy = values.vy; 973 | d.pinned = values.pinned; 974 | }); 975 | 976 | const nodes = [...internals, ...edges]; 977 | 978 | simulation.nodes(nodes) 979 | .force('charge', forceManyBody().strength(-150).distanceMax(300)) 980 | .force('link', forceLink(links).distance(30)) 981 | .force('center', forceCenter(width/2, height/2)) 982 | .force('collide', forceCollide().radius(d => 2*d.r || 0)) 983 | .force('bound', () => simulation.nodes().forEach(boundNode)); 984 | 985 | simulation.size = [width, height]; 986 | 987 | if (!bipartite && !ignorePlanarForce) { 988 | simulation.force('planar', planarForce(internals, edges)); 989 | } 990 | 991 | if (simulation.alpha() < .3) { 992 | simulation.alpha(.3).restart(); 993 | } 994 | 995 | 996 | }, [derivedProps, bipartite, width, height, unpinned]); 997 | 998 | // if (pinned) { 999 | // // pinning all nodes 1000 | // console.log('pinning all'); 1001 | 1002 | // simulation.nodes().forEach(d => { 1003 | // d.pinned = now(); 1004 | // d.fx = d.x; 1005 | // d.fy = d.y; 1006 | // }) 1007 | // } 1008 | 1009 | const [tooltip, setTooltip] = React.useState(); 1010 | 1011 | // debounce to improve rendering performance 1012 | // when user is mousing quickly 1013 | const handleTooltip = debounce(setTooltip, 200); 1014 | 1015 | const allProps = { 1016 | simulation, 1017 | ...derivedProps, 1018 | ...props, 1019 | onChangeTooltip: handleTooltip 1020 | }; 1021 | 1022 | const {onClickNodes=Object, onClickEdges=Object} = props; 1023 | 1024 | const handleClearSelection = ev => { 1025 | if (!(ev.shiftKey || ev.ctrlKey || ev.metaKey)) { 1026 | onClickNodes(ev, []); 1027 | onClickEdges(ev, []); 1028 | } 1029 | } 1030 | 1031 | return
1032 | { tooltip && 1033 | 1034 | } 1035 | 1036 | 1037 | 1038 | 1039 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | { !bipartite && } 1047 | 1048 | { bipartite && 1049 | 1050 | 1051 | 1052 | 1053 | } 1054 | 1055 | 1056 | 1057 | { selectionMode === 'node-brush' && 1058 | 1059 | } 1060 | 1061 | { selectionMode === 'edge-brush' && 1062 | 1063 | } 1064 | 1065 | 1066 | 1067 |
1068 | } 1069 | 1070 | export default withSize()(HypernetxWidgetView) 1071 | 1072 | // todo: 1073 | // labels, tooltips (data) 1074 | // move drag handling to individual nodes 1075 | // change DOM order of super-node groups 1076 | // convex hull test to decollide nodes and hulls that shouldn't intersect 1077 | -------------------------------------------------------------------------------- /src/NavigableSVG.css: -------------------------------------------------------------------------------- 1 | svg.navigable-svg > rect.navigation-handle { 2 | fill-opacity: 0; 3 | stroke: black; 4 | } 5 | 6 | svg.navigable-svg > rect.navigation-handle.pan { 7 | cursor: move; 8 | } 9 | 10 | svg.navigable-svg > rect.navigation-handle.zoom-in { 11 | cursor: zoom-in; 12 | } 13 | 14 | svg.navigable-svg > rect.navigation-handle.zoom-out { 15 | cursor: zoom-out; 16 | } -------------------------------------------------------------------------------- /src/NavigableSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import {select} from 'd3-selection' 4 | import {drag} from 'd3-drag' 5 | 6 | import './NavigableSVG.css' 7 | 8 | export const RESET = 'reset' 9 | export const PAN = 'pan' 10 | export const ZOOM_IN = 'zoom-in' 11 | export const ZOOM_OUT = 'zoom-out' 12 | 13 | export const NavigableSVG = ({children, navigation, scale=1.5, width, height, ...props}) => { 14 | 15 | const drawRect = [PAN, ZOOM_IN, ZOOM_OUT] 16 | .indexOf(navigation) !== -1; 17 | 18 | return { 24 | if (!ele) return; 25 | 26 | const svg = select(ele); 27 | const rect = svg.select('rect.navigation-handle'); 28 | 29 | const getViewBox = () => 30 | svg.attr('viewBox') 31 | .split(' ') 32 | .map(Number); 33 | 34 | let viewBoxStart, evStart; 35 | 36 | const handleDragStart = ev => { 37 | viewBoxStart = getViewBox(); 38 | evStart = ev; 39 | } 40 | 41 | const handleDrag = ev => { 42 | const [viewX, viewY, viewWidth, viewHeight] = viewBoxStart; 43 | 44 | const dx = evStart.sourceEvent.pageX - ev.sourceEvent.pageX; 45 | const dy = evStart.sourceEvent.pageY - ev.sourceEvent.pageY; 46 | 47 | const viewBox = [ 48 | viewX + dx*viewWidth/width, 49 | viewY + dy*viewHeight/height, 50 | viewWidth, 51 | viewHeight 52 | ].join(' '); 53 | 54 | svg.attr('viewBox', viewBox); 55 | } 56 | 57 | const handleZoom = (ev, scale) => { 58 | const [viewX, viewY, viewWidth, viewHeight] = getViewBox(); 59 | const newViewWidth = viewWidth*scale; 60 | const newViewHeight = viewHeight*scale; 61 | 62 | // when zooming out, prevent zooming out too far 63 | if (newViewWidth >= width) { 64 | return svg.attr('viewBox', `0 0 ${width} ${height}`); 65 | } 66 | 67 | // project mouse event into viewBox? 68 | const x = viewX + ev.offsetX*viewWidth/width; 69 | const y = viewY + ev.offsetY*viewHeight/height; 70 | 71 | const viewBox = [ 72 | x - newViewWidth/2, 73 | y - newViewHeight/2, 74 | newViewWidth, 75 | newViewHeight 76 | ].join(' '); 77 | 78 | svg.attr('viewBox', viewBox); 79 | } 80 | 81 | const handleZoomIn = ev => 82 | handleZoom(ev, 1/scale) 83 | 84 | const handleZoomOut = ev => 85 | handleZoom(ev, scale) 86 | 87 | if (navigation === PAN) { 88 | rect.call( 89 | drag() 90 | .on('start', handleDragStart) 91 | .on('drag', handleDrag) 92 | ); 93 | } else if (navigation === ZOOM_IN) { 94 | rect.on('click', handleZoomIn); 95 | } else if (navigation === ZOOM_OUT) { 96 | rect.on('click', handleZoomOut); 97 | } else if (navigation === RESET) { 98 | svg.attr('viewBox', `0 0 ${width} ${height}`); 99 | } 100 | }} 101 | > 102 | { children } 103 | { drawRect && 104 | 108 | } 109 | 110 | } 111 | 112 | export default NavigableSVG 113 | -------------------------------------------------------------------------------- /src/bars.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { max, range, min } from "d3-array"; 3 | import { 4 | VictoryBar, 5 | VictoryChart, 6 | VictoryAxis, 7 | VictorySelectionContainer, 8 | } from "victory"; 9 | import { Button } from "@material-ui/core"; 10 | import { scaleLinear } from "d3-scale"; 11 | import { VictoryTheme } from "victory-core"; 12 | 13 | const Bars = ({ type, origMax, freqData, onValueChange }) => { 14 | const [filterVal, setFilterVal] = React.useState([]); 15 | const maxX = max(freqData.map((d) => d.x)); 16 | const minX = min(freqData.map((d) => d.x)); 17 | const maxY = max(freqData.map((d) => d.y)); 18 | 19 | const newScale = scaleLinear() 20 | .domain([0, maxX]) 21 | .nice() 22 | .ticks() 23 | .filter((x) => x === Math.ceil(x)); 24 | 25 | const handleBrush = (points) => { 26 | let elements = points.data.map((d) => d.x); 27 | onValueChange(elements, type); 28 | setFilterVal(elements); 29 | }; 30 | 31 | const handleSelect = (elem) => { 32 | const clickedState = filterVal.includes(elem); 33 | 34 | if (clickedState) { 35 | setFilterVal([...filterVal].filter((x) => x !== elem)); 36 | onValueChange( 37 | [...filterVal].filter((x) => x !== elem), 38 | type 39 | ); 40 | } else { 41 | setFilterVal([...filterVal, elem]); 42 | onValueChange([...filterVal, elem], type); 43 | } 44 | }; 45 | 46 | const handleClearSelect = () => { 47 | onValueChange([], type); 48 | setFilterVal([]); 49 | }; 50 | return ( 51 |
52 |
53 |
61 | {type === "node" 62 | ? "Node degree distribution" 63 | : "Edge Size Distribution"} 64 |
65 | 80 |
81 |
88 | handleBrush(points[0])} 99 | onSelectionCleared={handleClearSelect} 100 | /> 101 | } 102 | > 103 | { 111 | if (filterVal.flat().includes(datum.x)) { 112 | return "#42a5f5"; 113 | } else { 114 | return "grey"; 115 | } 116 | }, 117 | stroke: "white", 118 | strokeWidth: 1, 119 | }, 120 | }} 121 | events={[ 122 | { 123 | target: "data", 124 | eventHandlers: { 125 | onMouseDown: (e) => e.stopPropagation(), 126 | onClick: () => { 127 | return [ 128 | { 129 | target: "data", 130 | mutation: (props) => { 131 | handleSelect(props.datum.x); 132 | }, 133 | }, 134 | ]; 135 | }, 136 | }, 137 | }, 138 | ]} 139 | /> 140 | 150 | 161 | 162 |
163 |
164 | ); 165 | }; 166 | 167 | export default Bars; 168 | -------------------------------------------------------------------------------- /src/checkboxEl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@material-ui/core/Checkbox'; 3 | 4 | const CheckboxEl = ({ label, checkState, sendCheck }) => { 5 | const [check, setCheck] = React.useState(checkState); 6 | 7 | const handleCheck = (e) => { 8 | setCheck(e); 9 | sendCheck(label, e); 10 | } 11 | 12 | return ( 13 |
14 | handleCheck(event.currentTarget.checked)}/> 15 |
16 | ) 17 | } 18 | 19 | export default CheckboxEl 20 | -------------------------------------------------------------------------------- /src/colorButton.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { ChromePicker } from "react-color"; 3 | import { Palette } from "@material-ui/icons"; 4 | import { IconButton } from "@material-ui/core"; 5 | import "./css/hnxStyle.css"; 6 | 7 | import { debounce, throttle } from "lodash"; 8 | 9 | const ColorButton = ({ label, color, onEachColorChange }) => { 10 | // const [paletteColor, setColor] = React.useState("#000000ff"); 11 | const [palette, setPalette] = React.useState(false); 12 | 13 | const handleClick = () => { 14 | setPalette(!palette); 15 | }; 16 | const handleClose = () => { 17 | setPalette(false); 18 | }; 19 | 20 | const handleChangeColor = (label, color) => { 21 | // const RGB = color.rgb; 22 | // const rgbaStr = 23 | // "rgba(" + RGB.r + ", " + RGB.g + ", " + RGB.b + ", " + RGB.a + ")"; 24 | onEachColorChange(label, color.hsl); 25 | // setColor(rgbToHex(rgbaStr)); 26 | }; 27 | 28 | const debouncedChangeColor = useMemo( 29 | () => throttle(handleChangeColor, 200), 30 | [] 31 | ); 32 | return ( 33 |
34 |
35 | 36 | 37 | 38 | 39 | {palette ? ( 40 |
41 |
handleClose()} /> 42 | debouncedChangeColor(label, c)} 45 | /> 46 |
47 | ) : null} 48 |
49 |
50 | ); 51 | }; 52 | 53 | export default ColorButton; 54 | -------------------------------------------------------------------------------- /src/colorPalette.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | FormControl, 4 | MenuItem, 5 | Select, 6 | InputLabel, 7 | IconButton, 8 | Tooltip, 9 | } from "@material-ui/core"; 10 | import ColorScale from "./colorScale"; 11 | import { makeStyles } from "@material-ui/core/styles"; 12 | import { range } from "d3-array"; 13 | import { 14 | getScheme, 15 | rgbToHex, 16 | contPalettes, 17 | getCategoricalScheme, 18 | categoricalPalettes, 19 | } from "./functions.js"; 20 | import { ChromePicker } from "react-color"; 21 | import { Palette, PaletteOutlined } from "@material-ui/icons"; 22 | 23 | const useStyles = makeStyles((theme) => ({ 24 | formControl: { 25 | margin: theme.spacing(1), 26 | paddingLeft: "15px", 27 | }, 28 | customSelect: { 29 | paddingRight: "5px", 30 | "& .MuiSelect-root": { 31 | minWidth: 50, 32 | }, 33 | "& .MuiSelect-selectMenu": { 34 | overflow: "visible", 35 | }, 36 | "& .MuiFormLabel-root": { 37 | fontSize: "15px", 38 | }, 39 | }, 40 | colorItem: { 41 | "& .MuiSelect-root": { 42 | width: 100, 43 | maxHeight: "16px", 44 | minHeight: "16px", 45 | }, 46 | "& .MuiSelect-select": { 47 | paddingTop: "2px", 48 | }, 49 | "& .MuiFormLabel-root": { 50 | width: 200, 51 | fontSize: "15px", 52 | }, 53 | }, 54 | menuItem: { 55 | paddingTop: "3px", 56 | paddingBottom: "3px", 57 | paddingLeft: "2px", 58 | paddingRight: "2px", 59 | whiteSpace: "normal", 60 | }, 61 | })); 62 | 63 | const ColorPalette = ({ 64 | type, 65 | data, 66 | metadata, 67 | defaultColors, 68 | currColors, 69 | onPaletteChange, 70 | currGroup, 71 | currPalette, 72 | onCurrDataChange, 73 | onAllColorChange, 74 | // colors, 75 | }) => { 76 | const colorsAreSame = new Set(Object.values(currColors)).size === 1; 77 | 78 | const columns = 79 | metadata !== undefined 80 | ? Object.keys(Object.values(metadata)[0]).concat("Id") 81 | : ["Id", "Degree"]; 82 | 83 | const classes = useStyles(); 84 | const [group, setGroup] = React.useState(currGroup); 85 | 86 | const createDataObj = (data, metadata) => { 87 | const obj = {}; 88 | if (metadata !== undefined) { 89 | if (group === "Id") { 90 | Object.keys(metadata).map((d) => { 91 | obj[d] = d; 92 | }); 93 | } else { 94 | Object.entries(metadata).map((d) => { 95 | obj[d[0]] = d[1][group]; 96 | }); 97 | } 98 | return obj; 99 | } else { 100 | if (group === "Id") { 101 | Object.keys(data).map((d) => { 102 | obj[d] = d; 103 | }); 104 | return obj; 105 | } else { 106 | return data; 107 | } 108 | } 109 | }; 110 | const dataObj = createDataObj(data, metadata); 111 | const isDiscrete = (dataObj) => { 112 | const values = Object.values(dataObj); 113 | if (typeof values[0] === "number") { 114 | return new Set(values).size === values.size; 115 | } else { 116 | return true; 117 | } 118 | }; 119 | const myPalette = isDiscrete(dataObj) ? categoricalPalettes : contPalettes; 120 | const [palette, setPalette] = React.useState(currPalette); 121 | // console.log(palette); 122 | 123 | const [allPaletteOpen, setAllPaletteOpen] = React.useState(false); 124 | 125 | const handlePalette = (event) => { 126 | setPalette(event.target.value); 127 | if (event.target.value === "default") { 128 | onPaletteChange(type, defaultColors); 129 | } else { 130 | if (type === "node") { 131 | const nodeColorPalette = assignColors(group, event.target.value); 132 | onPaletteChange(type, nodeColorPalette); 133 | onCurrDataChange(group, event.target.value, type); 134 | } else { 135 | const edgeColorPalette = assignColors(group, event.target.value); 136 | onPaletteChange(type, edgeColorPalette); 137 | onCurrDataChange(group, event.target.value, type); 138 | } 139 | } 140 | }; 141 | 142 | const handleClick = () => { 143 | setAllPaletteOpen(!allPaletteOpen); 144 | }; 145 | 146 | const handleClose = () => { 147 | setAllPaletteOpen(false); 148 | }; 149 | const [paletteColor, setPaletteColor] = React.useState( 150 | colorsAreSame ? Object.values(currColors)[0] : "#000000ff" 151 | ); 152 | 153 | const handleGroup = (event) => { 154 | setGroup(event.target.value); 155 | setPalette("default"); 156 | }; 157 | 158 | const assignColors = (group, palette) => { 159 | const schemeObj = {}; 160 | var colorObj = {}; 161 | const values = Object.values(dataObj); 162 | const unique = Array.from(new Set(values)).sort(); 163 | 164 | const bins = unique.length; 165 | if (!isDiscrete(dataObj)) { 166 | range(bins).map((x, i) => { 167 | schemeObj[unique[i]] = rgbToHex(getScheme(palette)((x + 1) / bins)); 168 | }); 169 | 170 | Object.entries(dataObj).map((d) => { 171 | colorObj[d[0]] = schemeObj[d[1]]; 172 | }); 173 | } else { 174 | const modifiedScheme = []; 175 | range(bins).map((x, i) => { 176 | let idx = x % getCategoricalScheme(palette).length; 177 | modifiedScheme.push(getCategoricalScheme(palette)[idx]); 178 | }); 179 | 180 | if (getCategoricalScheme(palette).length > bins) { 181 | Object.entries(dataObj).map((d, i) => { 182 | let idx = unique.indexOf(d[1]); 183 | colorObj[d[0]] = modifiedScheme[idx]; 184 | }); 185 | } else { 186 | Object.entries(dataObj).map((d, i) => { 187 | colorObj[d[0]] = modifiedScheme[i]; 188 | }); 189 | } 190 | } 191 | return colorObj; 192 | }; 193 | 194 | const getColorArray = (name) => { 195 | if (isDiscrete(dataObj)) { 196 | return getCategoricalScheme(name); 197 | } else { 198 | const colorScheme = getScheme(name); 199 | const k = [0.2, 0.4, 0.6, 0.8, 1]; 200 | const result = k.map((x) => colorScheme(x)); 201 | return result; 202 | } 203 | }; 204 | 205 | const handleChangeColor = (color) => { 206 | const RGB = color.rgb; 207 | const rgbaStr = 208 | "rgba(" + RGB.r + ", " + RGB.g + ", " + RGB.b + ", " + RGB.a + ")"; 209 | setPaletteColor(rgbToHex(rgbaStr)); 210 | onAllColorChange(rgbToHex(rgbaStr), type); 211 | }; 212 | 213 | return ( 214 |
215 |
224 | {"Colors"} 225 |
226 |
227 | 230 | {"Change colors of all " + type + "s"} 231 |
232 | } 233 | > 234 | 235 | {colorsAreSame ? ( 236 | 237 | ) : ( 238 | 241 | )} 242 | 243 | 244 |
245 | 246 | {allPaletteOpen ? ( 247 |
248 |
249 | handleChangeColor(c)} 252 | /> 253 |
254 | ) : null} 255 |
256 | 257 | Color by 258 | 265 | 266 | 267 | 268 | Color palette 269 | 279 | 280 |
281 |
282 | ); 283 | }; 284 | 285 | export default ColorPalette; 286 | -------------------------------------------------------------------------------- /src/colorScale.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ColorScale = ({ name, colorArray }) => { 4 | // console.log(colorArray); 5 | return ( 6 |
7 |
15 |
16 | {colorArray.map((c, i) => ( 17 |
28 | ))} 29 |
30 |
{name}
31 |
32 |
33 | ); 34 | }; 35 | 36 | export default ColorScale; 37 | -------------------------------------------------------------------------------- /src/css/hnxStyle.css: -------------------------------------------------------------------------------- 1 | .tableCont{ 2 | width: 490px; 3 | height: 300px; 4 | overflow-y: auto; 5 | } 6 | 7 | .hideButton{ 8 | visibility: hidden; 9 | } 10 | 11 | .hoverShowButton:hover .hideButton{ 12 | visibility: visible; 13 | } 14 | 15 | .showButton { 16 | visibility: visible; 17 | } 18 | 19 | .hbarCont { 20 | border-width: 0.1mm; 21 | border-style: solid; 22 | border-color: black; 23 | width:25px; 24 | /*height:10px;*/ 25 | margin-right: 5px; 26 | display: inline-block; 27 | } 28 | 29 | .hbar{ 30 | min-width: 1px; 31 | height: 1em; 32 | background-color: gray; 33 | } 34 | 35 | .barChart { 36 | display: inline-block; 37 | border-width: 0.1mm; 38 | border-style: solid; 39 | border-color: white; 40 | background-color: #42a5f5; 41 | width: 9px; 42 | max-height: inherit; 43 | min-height: 1px; 44 | 45 | } 46 | 47 | .palettePopUp { 48 | position: absolute; 49 | z-index: 5; 50 | top: 120px; 51 | left:310px; 52 | } 53 | 54 | .popover-allColors{ 55 | position: absolute; 56 | z-index: 999; 57 | 58 | bottom: 5px; 59 | left: 100px; 60 | /*right: 50px;*/ 61 | } 62 | 63 | .bar { 64 | display: inline-block; 65 | border-width: 0.1mm; 66 | border-style: solid; 67 | border-color: white; 68 | background-color: #42a5f5; 69 | width: 9px; 70 | max-height: inherit; 71 | min-height: 1px; 72 | 73 | } 74 | 75 | .cover { 76 | position: fixed; 77 | top: 0px; 78 | right: 0px; 79 | bottom: 0px; 80 | left: 0px; 81 | } 82 | 83 | .colorSetting { 84 | display: flex; 85 | flex-direction: row; 86 | } 87 | 88 | .colorButtonCont{ 89 | width: 100%; 90 | padding-top: 10px; 91 | display: flex; 92 | flex-direction: row; 93 | justify-content: flex-start; 94 | 95 | } 96 | 97 | .MenuItem{ 98 | color: blue; 99 | } 100 | .MenuItem.selected { 101 | color: blue; 102 | } 103 | 104 | .box{ 105 | width: 95%; 106 | display: inline-block; 107 | opacity: 0.7; 108 | } 109 | .stack-top{ 110 | padding: 3px; 111 | top: 10%; 112 | position: absolute; 113 | color: #404040; 114 | } 115 | -------------------------------------------------------------------------------- /src/fontSizeMenu.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FormControl from "@material-ui/core/FormControl"; 3 | import { InputLabel, MenuItem, Select } from "@material-ui/core"; 4 | import { makeStyles } from "@material-ui/core/styles"; 5 | const useStyles = makeStyles((theme) => ({ 6 | customSelect: { 7 | "& .MuiFormLabel-root": { 8 | fontSize: "15px", 9 | }, 10 | }, 11 | })); 12 | const sizeArr = [ 13 | "hide labels", 14 | 8, 15 | 10, 16 | 12, 17 | 14, 18 | 16, 19 | 18, 20 | 20, 21 | 22, 22 | 24, 23 | 26, 24 | 28, 25 | 36, 26 | 48, 27 | ]; 28 | 29 | const FontSizeMenu = ({ type, currSize, onSizeChange }) => { 30 | const classes = useStyles(); 31 | 32 | const [size, setSize] = React.useState(currSize[type]); 33 | const handleSize = (event) => { 34 | setSize(event.target.value); 35 | onSizeChange(type, event.target.value); 36 | }; 37 | return ( 38 |
39 |
47 | Font size 48 |
49 | 50 | {/* Size */} 51 | 58 | 59 |
60 | ); 61 | }; 62 | 63 | export default FontSizeMenu; 64 | -------------------------------------------------------------------------------- /src/functions.js: -------------------------------------------------------------------------------- 1 | import * as scale from "d3-scale-chromatic"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | 4 | export const numberRange = (start, end) => { 5 | return new Array(end - start).fill().map((d, i) => i + start); 6 | }; 7 | export const descendingComparator = (a, b, orderBy) => { 8 | a[orderBy] = a[orderBy] || false; 9 | b[orderBy] = b[orderBy] || false; 10 | 11 | if (b[orderBy] < a[orderBy]) { 12 | return -1; 13 | } 14 | if (b[orderBy] > a[orderBy]) { 15 | return 1; 16 | } 17 | return 0; 18 | }; 19 | 20 | export const getComparator = (order, orderBy) => { 21 | return order === "desc" 22 | ? (a, b) => descendingComparator(a, b, orderBy) 23 | : (a, b) => -descendingComparator(a, b, orderBy); 24 | }; 25 | 26 | export const stableSort = (array, comparator) => { 27 | const stabilizedThis = array.map((el, index) => [el, index]); 28 | stabilizedThis.sort((a, b) => { 29 | const order = comparator(a[0], b[0]); 30 | if (order !== 0) return order; 31 | return a[1] - b[1]; 32 | }); 33 | return stabilizedThis.map((el) => el[0]); 34 | }; 35 | 36 | export const rgbToHex = (rgbString) => { 37 | var a, 38 | rgb = rgbString 39 | .replace(/\s/g, "") 40 | .match(/^rgba?\((\d+),(\d+),(\d+),?([^,\s)]+)?/i), 41 | alpha = ((rgb && rgb[4]) || "").trim(), 42 | hex = rgb 43 | ? (rgb[1] | (1 << 8)).toString(16).slice(1) + 44 | (rgb[2] | (1 << 8)).toString(16).slice(1) + 45 | (rgb[3] | (1 << 8)).toString(16).slice(1) 46 | : rgbString; 47 | if (alpha !== "") { 48 | a = alpha; 49 | } else { 50 | a = 0o1; 51 | } 52 | 53 | a = ((a * 255) | (1 << 8)).toString(16).slice(1); 54 | hex = hex + a; 55 | 56 | return "#" + hex; 57 | }; 58 | 59 | export const hslToHex = (h, s, l) => { 60 | l /= 100; 61 | const a = (s * Math.min(l, 1 - l)) / 100; 62 | const f = (n) => { 63 | const k = (n + h / 30) % 12; 64 | const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); 65 | return Math.round(255 * color) 66 | .toString(16) 67 | .padStart(2, "0"); // convert to Hex and prefix "0" if needed 68 | }; 69 | return `#${f(0)}${f(8)}${f(4)}`; 70 | }; 71 | 72 | export const getRGB = (strRGB) => { 73 | let split = strRGB.split(/[()]+/).filter(function (e) { 74 | return e; 75 | }); 76 | let rgbVal = split[1].split(", "); 77 | return rgbVal; 78 | }; 79 | 80 | export const getNodeDegree = (nodeData, edgeData, uid) => { 81 | var degree = 0; 82 | 83 | edgeData.map((e) => 84 | e.elements.map((v) => { 85 | if (v === uid) { 86 | degree += 1; 87 | } 88 | }) 89 | ); 90 | return degree; 91 | }; 92 | 93 | export const getEdgeSize = (nodeData, edgeData, edgeIdx) => { 94 | const nodeElems = edgeData[edgeIdx].elements; 95 | return nodeElems.length; 96 | }; 97 | 98 | export const getValueFreq = (obj) => { 99 | let results = []; 100 | const valueMap = new Map(); 101 | Object.values(obj).map((x) => { 102 | if (!valueMap.has(x)) { 103 | valueMap.set(x, 1); 104 | } else { 105 | let currCt = valueMap.get(x); 106 | valueMap.set(x, currCt + 1); 107 | } 108 | }); 109 | Array.from(valueMap).map((x) => { 110 | results.push({ x: x[0], y: x[1] }); 111 | }); 112 | return results.sort((a, b) => a.x - b.x); 113 | }; 114 | 115 | export const showButtonStyles = makeStyles((theme) => ({ 116 | customButton: { 117 | // width: '100%', 118 | "& .MuiToggleButton-root": { 119 | color: "#5c6bc0", 120 | border: "1px solid #5c6bc0", 121 | }, 122 | "& .Mui-selected": { 123 | backgroundColor: "#ECECEC", 124 | }, 125 | }, 126 | })); 127 | 128 | export const accordianStyles = makeStyles((theme) => ({ 129 | root: { 130 | width: "100%", 131 | 132 | // '& .Mui-expanded':{ 133 | // padding: 0 134 | // }, 135 | "& .MuiAccordionDetails-root": { 136 | padding: 0, 137 | }, 138 | "& .MuiAccordianSummary-root.Mui-expanded": { 139 | minHeight: "30px", 140 | }, 141 | }, 142 | })); 143 | 144 | export const allPalettes = [ 145 | "Blues", 146 | "Greens", 147 | "Greys", 148 | "Oranges", 149 | "Purples", 150 | "Reds", 151 | "Bu-Gn", 152 | "Bu-Pu", 153 | "Gn-Bu", 154 | "Or-Rd", 155 | "Pu-Bu-Gn", 156 | "Pu-Bu", 157 | "Pu-Rd", 158 | "Rd-Pu", 159 | "Yl-Gn-Bu", 160 | "Yl-Gn", 161 | "Yl-Or-Bn", 162 | "Yl-Or-Rd", 163 | "Bn-BuGn", 164 | "PuRd-Gn", 165 | "Pink-YlGn", 166 | "Pu-Or", 167 | "Rd-Bu", 168 | "Rd-Grey", 169 | "Rd-Yl-Bu", 170 | "Spectral", 171 | "Turbo", 172 | "Viridis", 173 | "Inferno", 174 | "Plasma", 175 | "Cividis", 176 | "Warm", 177 | "Cool", 178 | "Rainbow", 179 | "Sinebow", 180 | ]; 181 | 182 | export const discretePalettes = [ 183 | "Bn-BuGn", 184 | "PuRd-Gn", 185 | "Pink-YlGn", 186 | "Pu-Or", 187 | "Rd-Bu", 188 | "Rd-Grey", 189 | "Rd-Yl-Bu", 190 | "Spectral", 191 | "Turbo", 192 | "Viridis", 193 | "Inferno", 194 | "Plasma", 195 | "Cividis", 196 | "Warm", 197 | "Cool", 198 | "Rainbow", 199 | "Sinebow", 200 | ]; 201 | 202 | export const contPalettes = [ 203 | "Blues", 204 | "Greens", 205 | "Greys", 206 | "Oranges", 207 | "Purples", 208 | "Reds", 209 | "Bu-Gn", 210 | "Bu-Pu", 211 | "Gn-Bu", 212 | "Or-Rd", 213 | "Pu-Bu-Gn", 214 | "Pu-Bu", 215 | "Pu-Rd", 216 | "Rd-Pu", 217 | "Yl-Gn-Bu", 218 | "Yl-Gn", 219 | "Yl-Or-Bn", 220 | "Yl-Or-Rd", 221 | ]; 222 | 223 | export const categoricalPalettes = [ 224 | "Accent8", 225 | "Dark8", 226 | "Set8", 227 | "Pastel8", 228 | "Set9", 229 | "Pastel9", 230 | "Category10", 231 | "Tableau10", 232 | "Paired12", 233 | "Set12", 234 | ]; 235 | export const getCategoricalScheme = (color) => { 236 | if (color === "Accent8") { 237 | return scale.schemeAccent; 238 | } else if (color === "Dark8") { 239 | return scale.schemeDark2; 240 | } else if (color === "Set8") { 241 | return scale.schemeSet2; 242 | } else if (color === "Pastel8") { 243 | return scale.schemePastel2; 244 | } else if (color === "Set9") { 245 | return scale.schemeSet1; 246 | } else if (color === "Pastel9") { 247 | return scale.schemePastel1; 248 | } else if (color === "Category10") { 249 | return scale.schemeCategory10; 250 | } else if (color === "Tableau10") { 251 | return scale.schemeTableau10; 252 | } else if (color === "Paired12") { 253 | return scale.schemePaired; 254 | } else if (color === "Set12") { 255 | return scale.schemeSet3; 256 | } 257 | }; 258 | export const getScheme = (color) => { 259 | if (color === "Blues") { 260 | return scale.interpolateBlues; 261 | } else if (color === "Greens") { 262 | return scale.interpolateGreens; 263 | } else if (color === "Greys") { 264 | return scale.interpolateGreys; 265 | } else if (color === "Oranges") { 266 | return scale.interpolateOranges; 267 | } else if (color === "Purples") { 268 | return scale.interpolatePurples; 269 | } else if (color === "Reds") { 270 | return scale.interpolateReds; 271 | } else if (color === "Bu-Gn") { 272 | return scale.interpolateBuGn; 273 | } else if (color === "Bu-Pu") { 274 | return scale.interpolateBuPu; 275 | } else if (color === "Gn-Bu") { 276 | return scale.interpolateGnBu; 277 | } else if (color === "Or-Rd") { 278 | return scale.interpolateOrRd; 279 | } else if (color === "Pu-Bu-Gn") { 280 | return scale.interpolatePuBuGn; 281 | } else if (color === "Pu-Bu") { 282 | return scale.interpolatePuBu; 283 | } else if (color === "Pu-Rd") { 284 | return scale.interpolatePuRd; 285 | } else if (color === "Rd-Pu") { 286 | return scale.interpolateRdPu; 287 | } else if (color === "Yl-Gn-Bu") { 288 | return scale.interpolateYlGnBu; 289 | } else if (color === "Yl-Gn") { 290 | return scale.interpolateYlGn; 291 | } else if (color === "Yl-Or-Bn") { 292 | return scale.interpolateYlOrBr; 293 | } else if (color === "Yl-Or-Rd") { 294 | return scale.interpolateYlOrRd; 295 | } else if (color === "Bn-BuGn") { 296 | return scale.interpolateBrBG; 297 | } else if (color === "PuRd-Gn") { 298 | return scale.interpolatePRGn; 299 | } else if (color === "Pink-YlGn") { 300 | return scale.interpolatePiYG; 301 | } else if (color === "Pu-Or") { 302 | return scale.interpolatePuOr; 303 | } else if (color === "Rd-Bu") { 304 | return scale.interpolateRdBu; 305 | } else if (color === "Rd-Grey") { 306 | return scale.interpolateRdGy; 307 | } else if (color === "Rd-Yl-Bu") { 308 | return scale.interpolateRdYlBu; 309 | } else if (color === "Spectral") { 310 | return scale.interpolateSpectral; 311 | } else if (color === "Turbo") { 312 | return scale.interpolateTurbo; 313 | } else if (color === "Viridis") { 314 | return scale.interpolateViridis; 315 | } else if (color === "Inferno") { 316 | return scale.interpolateInferno; 317 | } else if (color === "Plasma") { 318 | return scale.interpolatePlasma; 319 | } else if (color === "Cividis") { 320 | return scale.interpolateCividis; 321 | } else if (color === "Warm") { 322 | return scale.interpolateWarm; 323 | } else if (color === "Cool") { 324 | return scale.interpolateCool; 325 | } else if (color === "Cubehelix") { 326 | return scale.interpolateCubehelixDefault; 327 | } else if (color === "Rainbow") { 328 | return scale.interpolateRainbow; 329 | } else if (color === "Sinebow") { 330 | return scale.interpolateSinebow; 331 | } 332 | }; 333 | -------------------------------------------------------------------------------- /src/helpMenu.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogContentText, 6 | DialogTitle, 7 | IconButton, 8 | Typography, 9 | } from "@material-ui/core"; 10 | import { makeStyles } from "@material-ui/core/styles"; 11 | import CloseIcon from "@material-ui/icons/Close"; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | root: { 15 | "& .MuiDialogTitle-root": { 16 | paddingTop: "8px", 17 | paddingBottom: "5px", 18 | paddingRight: "6px", 19 | }, 20 | "& .MuiTypography-h6": { 21 | display: "flex", 22 | justifyContent: "space-between", 23 | }, 24 | }, 25 | })); 26 | 27 | const HelpMenu = ({ state, onOpenChange }) => { 28 | const handleClose = () => { 29 | onOpenChange(false); 30 | }; 31 | 32 | const classes = useStyles(); 33 | return ( 34 |
35 | 41 | 42 | Using the tool 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | The tool has two main interfaces, the hypergraph visualization and 54 | the nodes & edges panel. 55 | 56 | Layout 57 | 58 | The hypergraph visualization is an Euler diagram that shows nodes 59 | as circles and hyper edges as outlines containing the 60 | nodes/circles they contain. The visualization uses a force 61 | directed optimization to perform the layout. This algorithm is not 62 | perfect and sometimes gives results that the user might want to 63 | improve upon. The visualization allows the user to drag nodes and 64 | position them directly at any time. The algorithm will re-position 65 | any nodes that are not specified by the user. Ctrl (Windows) or 66 | Command (Mac) clicking a node will release a pinned node it to be 67 | re-positioned by the algorithm. 68 | 69 | Selection 70 | 71 | Nodes and edges can be selected by clicking them. Nodes and edges 72 | can be selected independently of each other, i.e., it is possible 73 | to select an edge without selecting the nodes it contains. 74 | Multiple nodes and edges can be selected, by holding down Shift 75 | while clicking. Shift clicking an already selected node will 76 | de-select it. Clicking the background will de-select all nodes and 77 | edges. Dragging a selected node will drag all selected nodes, 78 | keeping their relative placement. 79 | 80 | 81 | Selected nodes can be hidden (having their appearance minimized) 82 | or removed completely from the visualization. Hiding a node or 83 | edge will not cause a change in the layout, whereas removing a 84 | node or edge will. The selection can also be expanded. Buttons in 85 | the toolbar allow for selecting all nodes contained within 86 | selected edges, and selecting all edges containing any selected 87 | nodes. 88 | 89 | 90 | The toolbar also contains buttons to select all nodes (or edges), 91 | un-select all nodes (or edges), or reverse the selected nodes (or 92 | edges). An advanced user might: 93 | 94 | 95 |
  • 96 | 101 | {"Select all nodes not in an edge by: "} 102 | 103 | 104 | select an edge, select all nodes in that edge, then reverse 105 | the selected nodes to select every node not in that edge. 106 | 107 |
  • 108 |
  • 109 | 110 | {"Traverse the graph by: "} 111 | 112 | 113 | selecting a start node, then alternating select all edges 114 | containing selected nodes and selecting all nodes within 115 | selected edges{" "} 116 | 117 |
  • 118 |
  • 119 | 120 | {"Pin Everything by: "} 121 | 122 | 123 | hitting the button to select all nodes, then drag any node 124 | slightly to activate the pinning for all nodes. 125 | 126 |
  • 127 |
    128 | Side Panel 129 | 130 | Details on nodes and edges are visible in the side panel. For both 131 | nodes and edges, a table shows the node name, degree (or size for 132 | edges), its selection state, removed state, and color. These 133 | properties can also be controlled directly from this panel. The 134 | color of nodes and edges can be set in bulk here as well, for 135 | example, coloring by degree. 136 | 137 | Other Features 138 | 139 | Nodes with identical edge membership can be collapsed into a super 140 | node, which can be helpful for larger hypergraphs. Dragging any 141 | node in a super node will drag the entire super node. This feature 142 | is available as a toggle in the nodes panel. 143 | 144 | 145 | The hypergraph can also be visualized as a bipartite graph 146 | (similar to a traditional node-link diagram). Toggling this 147 | feature will preserve the locations of the nodes between the 148 | bipartite and the Euler diagrams. 149 | 150 |
    151 |
    152 |
    153 |
    154 | ); 155 | }; 156 | 157 | export default HelpMenu; 158 | -------------------------------------------------------------------------------- /src/hnx-widget.css: -------------------------------------------------------------------------------- 1 | .hnx-widget-view svg { 2 | -webkit-user-select: none; 3 | user-select: none; 4 | } 5 | 6 | .hnx-widget-view .nodes .fixed .internal { 7 | stroke: gray; 8 | stroke-width: 3; 9 | stroke-dasharray: 6,3; 10 | } 11 | 12 | .hnx-widget-view .edges .fixed rect { 13 | stroke-width: 3; 14 | stroke-dasharray: 6,3; 15 | } 16 | 17 | .hnx-widget-view .nodes { 18 | cursor: pointer; 19 | } 20 | 21 | .hnx-widget-view .nodes .internal { 22 | fill: white; 23 | } 24 | 25 | .hnx-widget-view .nodes g:hover circle.bottom, 26 | .hnx-widget-view .nodes .selected circle.bottom { 27 | stroke: black; 28 | stroke-width: 4; 29 | fill-opacity: .2; 30 | } 31 | 32 | .hnx-widget-view .nodes .selected { 33 | cursor: move; 34 | } 35 | 36 | .hnx-widget-view .nodes .top { 37 | visibility: hidden; 38 | pointer-events: none; 39 | } 40 | 41 | .hnx-widget-view .nodes .error .top { 42 | visibility: inherit; 43 | } 44 | 45 | #checkerboard rect:nth-child(3n) { 46 | fill-opacity: 0; 47 | } 48 | 49 | #checkerboard rect { 50 | fill: white; 51 | } 52 | 53 | .hnx-widget-view .nodes text { 54 | fill: darkgray; 55 | alignment-baseline: middle; 56 | text-anchor: middle; 57 | pointer-events: none; 58 | } 59 | 60 | .hnx-widget-view .edges { 61 | stroke-width: 1.5; 62 | } 63 | 64 | .hnx-widget-view .edges text{ 65 | alignment-baseline: middle; 66 | text-anchor: middle; 67 | font-style: italic; 68 | stroke-width: 0; 69 | } 70 | 71 | .hnx-widget-view .edges path { 72 | pointer-events: visibleStroke; 73 | stroke-width: 1.5; 74 | fill-opacity: 0; 75 | } 76 | 77 | .hnx-widget-view .bipartite.edges rect { 78 | fill-opacity: 1; 79 | fill: white; 80 | stroke-width: 1.5; 81 | } 82 | 83 | .hnx-widget-view .edges :hover path, 84 | .hnx-widget-view .edges :hover rect { 85 | fill-opacity: .5; 86 | fill: inherit; 87 | /*stroke-dasharray: 6,3;*/ 88 | stroke-width: 3; 89 | cursor: pointer; 90 | } 91 | 92 | .hnx-widget-view .edges .selected path, 93 | .hnx-widget-view .edges .selected rect { 94 | fill: inherit; 95 | stroke-width: 5; 96 | fill-opacity: .2; 97 | } 98 | 99 | .hnx-widget-view table.hnx-tooltip { 100 | border: 1px solid black; 101 | background: rgb(255, 255, 255, .75); 102 | margin: .5em; 103 | padding: .5em; 104 | max-width: 150px; 105 | border-collapse: unset; 106 | border-spacing: unset; 107 | } 108 | 109 | .hnx-widget-view table th { 110 | text-align: center; 111 | } 112 | 113 | .hnx-widget-view table th, .hnx-widget-view table td { 114 | padding: inherit; 115 | } 116 | 117 | .hnx-widget-view table td:first-child { 118 | padding-right: .5em; 119 | } 120 | 121 | .hnx-widget-view .hiddenState { 122 | opacity: 0.1; 123 | } 124 | 125 | .hnx-widget-view .edge-brush rect.selection, 126 | .hnx-widget-view .edge-brush rect.handle { 127 | visibility: hidden; 128 | } 129 | 130 | .hnx-widget-view .edge-brush line { 131 | stroke: lightgray; 132 | stroke-width: 1px; 133 | } -------------------------------------------------------------------------------- /src/iconWithTooltip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Tooltip from "@material-ui/core/Tooltip"; 3 | 4 | const IconWithTooltip = ({ text, iconImage }) => { 5 | return ( 6 |
    7 | {text}
    } 9 | > 10 | {iconImage} 11 | 12 |
    13 | ); 14 | }; 15 | 16 | export default IconWithTooltip; 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // import props from './stories/data/props.json' 4 | 5 | import Widget from './widget.js'; 6 | import HypernetxWidgetView from './HypernetxWidgetView'; 7 | 8 | export {Widget as HypernetxWidget, HypernetxWidgetView}; 9 | -------------------------------------------------------------------------------- /src/loadTable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Table, 5 | TableBody, 6 | TableCell, 7 | TableContainer, 8 | TableHead, 9 | TableRow, 10 | TableSortLabel, 11 | Paper, 12 | Checkbox, 13 | Tooltip, 14 | MenuItem, 15 | Select, 16 | } from "@material-ui/core"; 17 | import CheckboxEl from "./checkboxEl.js"; 18 | import ColorButton from "./colorButton.js"; 19 | import VisibilityButton from "./visibilityButton.js"; 20 | import RemoveButton from "./removeButton"; 21 | import { makeStyles } from "@material-ui/core/styles"; 22 | import { max } from "d3-array"; 23 | import { getComparator, stableSort } from "./functions.js"; 24 | import "./css/hnxStyle.css"; 25 | import { 26 | VisibilityOutlined, 27 | RemoveCircleOutlineOutlined, 28 | PaletteOutlined, 29 | } from "@material-ui/icons"; 30 | 31 | const tableStyles = makeStyles((theme) => ({ 32 | customTable: { 33 | "& .MuiTableCell-sizeSmall": { 34 | whitespace: "nowrap", 35 | padding: "0px 0px 0px 0px", 36 | fontSize: "10px", 37 | fontWeight: 400, 38 | margin: "0px", 39 | }, 40 | "& .MuiTableCell-paddingNone": { 41 | fontSize: "12px", 42 | fontWeight: 500, 43 | align: "center", 44 | }, 45 | "& .MuiButton-text": { 46 | padding: "0px 0px 0px 0px", 47 | }, 48 | "& .MuiTableSortLabel-icon": { 49 | fontSize: "14px", 50 | padding: "0px 0px 0px 0px", 51 | margin: "0px", 52 | }, 53 | "& .MuiTableSortLabel-root": { 54 | fontSize: "12px", 55 | }, 56 | }, 57 | visuallyHidden: { 58 | border: 0, 59 | clip: "rect(0 0 0 0)", 60 | height: 1, 61 | margin: -1, 62 | overflow: "hidden", 63 | padding: 0, 64 | position: "absolute", 65 | top: 20, 66 | width: 1, 67 | }, 68 | })); 69 | 70 | function EnhancedTableHead(props) { 71 | const { 72 | datatype, 73 | data, 74 | metadata, 75 | usercols, 76 | classes, 77 | onSelectAllClick, 78 | order, 79 | orderBy, 80 | numSelected, 81 | rowCount, 82 | onRequestSort, 83 | onUserCol, 84 | } = props; 85 | 86 | const createSortHandler = (property) => (event) => { 87 | onRequestSort(event, property); 88 | }; 89 | 90 | const metaHeadCells = [ 91 | { id: "value", label: datatype === "node" ? "Degree" : "Size" }, 92 | { id: "uid", label: "Label" }, 93 | { id: "user", label: "UserDefined" }, 94 | { id: "hidden", label: "Visibility" }, 95 | { id: "removed", label: "Remove" }, 96 | { id: "color", label: "Color" }, 97 | ]; 98 | 99 | const headCells = [ 100 | { id: "value", label: datatype === "node" ? "Degree" : "Size" }, 101 | { id: "uid", label: "Label" }, 102 | { id: "hidden", label: "Visibility" }, 103 | { id: "removed", label: "Remove" }, 104 | { id: "color", label: "Color" }, 105 | ]; 106 | 107 | const headers = metadata ? metaHeadCells : headCells; 108 | 109 | const [userCol, setUserCol] = React.useState(props.usercols[0] || ""); 110 | 111 | const handleUserCol = (e) => { 112 | setUserCol(e.target.value); 113 | onUserCol(e.target.value); 114 | }; 115 | 116 | return ( 117 | 118 | 119 | 120 | x.selected === true).includes(false)} 124 | indeterminate={ 125 | data.map((x) => x.selected === true).includes(false) && 126 | data.map((x) => x.selected === true).includes(true) 127 | } 128 | /> 129 | 130 | {headers.map((headCell) => ( 131 | 136 | 141 | {headCell.id === "user" && metadata !== undefined && ( 142 |
    143 | 154 |
    155 | )} 156 | {(headCell.id === "uid" || headCell.id === "value") && ( 157 |
    {headCell.label}
    158 | )} 159 | {headCell.id === "hidden" && ( 160 | 163 | {"Hide/show " + datatype + "s"} 164 |
    165 | } 166 | > 167 | 171 | 172 | )} 173 | {headCell.id === "removed" && ( 174 | 177 | {"Remove/show " + datatype + "s"} 178 | 179 | } 180 | > 181 | 182 | 183 | )} 184 | 185 | {headCell.id === "color" && ( 186 | 189 | {"Color " + datatype + "s"} 190 | 191 | } 192 | > 193 | 194 | 195 | )} 196 | 197 | {orderBy === headCell.id ? ( 198 | 199 | {order === "desc" ? "sorted descending" : "sorted ascending"} 200 | 201 | ) : null} 202 | 203 | 204 | ))} 205 | 206 | 207 | ); 208 | } 209 | 210 | EnhancedTableHead.propTypes = { 211 | datatype: PropTypes.string.isRequired, 212 | data: PropTypes.array.isRequired, 213 | metadata: PropTypes.object, 214 | usercols: PropTypes.array.isRequired, 215 | classes: PropTypes.object.isRequired, 216 | onRequestSort: PropTypes.func.isRequired, 217 | onSelectAllClick: PropTypes.func.isRequired, 218 | order: PropTypes.oneOf(["asc", "desc"]).isRequired, 219 | orderBy: PropTypes.string.isRequired, 220 | rowCount: PropTypes.number.isRequired, 221 | onUserCol: PropTypes.func.isRequired, 222 | }; 223 | 224 | const LoadTable = ({ 225 | type, 226 | metadata, 227 | data, 228 | onColorChange, 229 | onVisibleChange, 230 | onSelectedChange, 231 | onRemovedChange, 232 | onSelectAllChange, 233 | }) => { 234 | // columns from metadata to add 235 | const addColumns = metadata 236 | ? Object.keys(Object.values(metadata)[0]).filter( 237 | (d) => d !== "Degree" && d !== "Size" 238 | ) 239 | : []; 240 | 241 | const fullData = []; 242 | if (metadata) { 243 | Object.entries(metadata).map((m) => 244 | data.map((d) => { 245 | if (d.uid === m[0]) { 246 | let combinedObj = { ...d, ...m[1] }; 247 | fullData.push(combinedObj); 248 | } 249 | }) 250 | ); 251 | } 252 | 253 | const classes = tableStyles(); 254 | const calcBar = (i) => { 255 | const values = data.map((x) => x.value); 256 | return String(100 * (i / max(values))) + "%"; 257 | }; 258 | 259 | const [order, setOrder] = React.useState("asc"); 260 | const [orderBy, setOrderBy] = React.useState("label"); 261 | const [userCol, setUserCol] = React.useState(addColumns[0] || ""); 262 | 263 | const handleRequestSort = (event, property) => { 264 | const isAsc = orderBy === property && order === "asc"; 265 | setOrder(isAsc ? "desc" : "asc"); 266 | setOrderBy(property); 267 | }; 268 | 269 | const handleSelectAllClick = (event) => { 270 | if (event.target.checked) { 271 | onSelectAllChange(type, true); 272 | } else { 273 | onSelectAllChange(type, false); 274 | } 275 | }; 276 | 277 | const getColor = (label, color) => { 278 | onColorChange(type, label, color); 279 | }; 280 | 281 | const getVisibility = (label, visibility) => { 282 | onVisibleChange(type, label, visibility); 283 | }; 284 | 285 | const getCheck = (label, check) => { 286 | onSelectedChange(type, label, check); 287 | }; 288 | 289 | const getRemove = (label, remove) => { 290 | onRemovedChange(type, label, remove); 291 | }; 292 | 293 | const formatData = (value) => { 294 | if (typeof value === "number") { 295 | if (value % 1 !== 0) { 296 | return Number(value.toFixed(2)); 297 | } 298 | return value; 299 | } else if (typeof value === "boolean") { 300 | return +value; 301 | } else { 302 | return value; 303 | } 304 | }; 305 | 306 | return ( 307 |
    308 | 316 | 322 | setUserCol(col)} 334 | /> 335 | 336 | {stableSort( 337 | metadata ? fullData : data, 338 | getComparator(order, orderBy === "user" ? userCol : orderBy) 339 | ).map((x, i) => ( 340 | 341 | 342 | 347 | 348 | 349 | 350 |
    351 |
    352 |
    356 |
    357 | 358 |
    {+x.value}
    359 |
    360 | 361 | 362 |
    371 | {x.uid} 372 |
    373 |
    374 | {metadata !== undefined && ( 375 | 378 | {formatData(x[userCol])} 379 | 380 | )} 381 | 382 | 387 | 388 | 389 | 394 | 395 | 396 | 401 | 402 | 403 | {/*{x[userCol]}*/} 404 | 405 | ))} 406 | 407 |
    408 |
    409 |
    410 | ); 411 | }; 412 | 413 | export default LoadTable; 414 | -------------------------------------------------------------------------------- /src/nodeSizeMenu.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FormControl from "@material-ui/core/FormControl"; 3 | import { InputLabel, MenuItem, Select } from "@material-ui/core"; 4 | import { makeStyles } from "@material-ui/core/styles"; 5 | const useStyles = makeStyles((theme) => ({ 6 | customSelect: { 7 | "& .MuiFormLabel-root": { 8 | fontSize: "13px", 9 | }, 10 | // "& .MuiSelect-select.MuiSelect-select": { 11 | // fontSize: "13px", 12 | // }, 13 | }, 14 | })); 15 | 16 | const NodeSizeMenu = ({ currGroup, metadata, onGroupChange }) => { 17 | const classes = useStyles(); 18 | const [group, setGroup] = React.useState(currGroup); 19 | const handleSize = (event) => { 20 | setGroup(event.target.value); 21 | onGroupChange(event.target.value, metadata); 22 | }; 23 | 24 | const columns = 25 | metadata !== undefined 26 | ? Object.keys(Object.values(metadata)[0]).concat("None") 27 | : ["None", "Degree"]; 28 | 29 | return ( 30 |
    31 |
    40 | {"Node size"} 41 |
    42 | 43 | 50 | 51 |
    52 | ); 53 | }; 54 | 55 | export default NodeSizeMenu; 56 | -------------------------------------------------------------------------------- /src/radioButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Radio from '@material-ui/core/Radio'; 3 | import RadioGroup from '@material-ui/core/RadioGroup'; 4 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 5 | import FormControl from '@material-ui/core/FormControl'; 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | import clsx from 'clsx'; 8 | 9 | const useStyles = makeStyles({ 10 | root: { 11 | '&:hover': { 12 | backgroundColor: 'transparent', 13 | }, 14 | }, 15 | icon: { 16 | borderRadius: '50%', 17 | width: 16, 18 | height: 16, 19 | boxShadow: 'inset 0 0 0 1px rgba(16,22,26,.2), inset 0 -1px 0 rgba(16,22,26,.1)', 20 | backgroundColor: '#f5f8fa', 21 | backgroundImage: 'linear-gradient(180deg,hsla(0,0%,100%,.8),hsla(0,0%,100%,0))', 22 | '$root.Mui-focusVisible &': { 23 | outline: '2px auto rgba(19,124,189,.6)', 24 | outlineOffset: 2, 25 | }, 26 | 'input:hover ~ &': { 27 | backgroundColor: '#ebf1f5', 28 | }, 29 | 'input:disabled ~ &': { 30 | boxShadow: 'none', 31 | background: 'rgba(206,217,224,.5)', 32 | }, 33 | }, 34 | checkedIcon: { 35 | backgroundColor: '#137cbd', 36 | backgroundImage: 'linear-gradient(180deg,hsla(0,0%,100%,.1),hsla(0,0%,100%,0))', 37 | '&:before': { 38 | display: 'block', 39 | width: 16, 40 | height: 16, 41 | backgroundImage: 'radial-gradient(#fff,#fff 28%,transparent 32%)', 42 | content: '""', 43 | }, 44 | 'input:hover ~ &': { 45 | backgroundColor: '#106ba3', 46 | }, 47 | }, 48 | checkboxLabel: { 49 | fontSize: 14, 50 | } 51 | }); 52 | 53 | function StyledRadio(props) { 54 | const classes = useStyles(); 55 | 56 | return ( 57 | } 62 | icon={} 63 | {...props} 64 | /> 65 | ); 66 | } 67 | 68 | 69 | const RadioButton = ({ type, sendRadio }) => { 70 | const [selectedValue, setSelectedValue] = React.useState("selected"); 71 | 72 | const handleChange = event => { 73 | setSelectedValue(event.target.value); 74 | sendRadio(type, event.target.value); 75 | } 76 | 77 | const classes = useStyles(); 78 | return
    79 | 80 | 81 | } label="Show selected" /> 82 | } label="Show hidden" /> 83 | 84 | 85 |
    86 | } 87 | 88 | export default RadioButton 89 | -------------------------------------------------------------------------------- /src/removeButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RemoveCircleOutlineOutlinedIcon from "@material-ui/icons/RemoveCircleOutlineOutlined"; 3 | import { IconButton } from "@material-ui/core"; 4 | 5 | const RemoveButton = ({ label, remove, onRemoveChange }) => { 6 | const handleRemove = () => { 7 | onRemoveChange(label, !remove); 8 | }; 9 | 10 | return ( 11 |
    12 |
    13 | 14 | {remove ? ( 15 | 16 | ) : ( 17 | 18 | )} 19 | 20 |
    21 |
    22 | ); 23 | }; 24 | 25 | export default RemoveButton; 26 | -------------------------------------------------------------------------------- /src/showButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ToggleButton from '@material-ui/lab/ToggleButton'; 3 | import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'; 4 | import { showButtonStyles } from './functions.js'; 5 | 6 | const ShowButton = ({ type, sendButton, currButton }) => { 7 | const classes = showButtonStyles(); 8 | const [selectedValue, setSelectedValue] = React.useState(currButton); 9 | 10 | const handleChange = (event, newValue) => { 11 | setSelectedValue(newValue); 12 | sendButton(type, newValue); 13 | } 14 | 15 | 16 | return
    17 | 23 | 24 | Show selected 25 | 26 | 27 | 28 | Show hidden 29 | 30 | 31 | 32 | None 33 | 34 | 35 | 36 |
    37 | } 38 | 39 | export default ShowButton 40 | -------------------------------------------------------------------------------- /src/stories/data/biggerProps.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "uid": "a", 5 | "value": 1 6 | }, 7 | { 8 | "uid": "b", 9 | "value": 1 10 | }, 11 | { 12 | "uid": "c", 13 | "value": 1 14 | }, 15 | { 16 | "uid": "d", 17 | "value": 1 18 | }, 19 | { 20 | "uid": "e", 21 | "value": 1 22 | }, 23 | { 24 | "uid": "f", 25 | "value": 1 26 | }, 27 | { 28 | "uid": "g", 29 | "value": 1 30 | }, 31 | { 32 | "uid": "h", 33 | "value": 1 34 | }, 35 | { 36 | "uid": "i", 37 | "value": 1 38 | }, 39 | { 40 | "uid": "j", 41 | "value": 1 42 | }, 43 | { 44 | "uid": "k", 45 | "value": 1 46 | }, 47 | { 48 | "uid": "l", 49 | "value": 1 50 | }, 51 | { 52 | "uid": "m", 53 | "value": 1 54 | }, 55 | { 56 | "uid": "o", 57 | "value": 1 58 | }, 59 | { 60 | "uid": "p", 61 | "value": 1 62 | }, 63 | { 64 | "uid": "n", 65 | "value": 1 66 | }, 67 | { 68 | "uid": "q", 69 | "value": 1 70 | }, 71 | { 72 | "uid": "r", 73 | "value": 1 74 | }, 75 | { 76 | "uid": "s", 77 | "value": 1 78 | } 79 | ], 80 | "edges": [ 81 | { 82 | "uid": "1", 83 | "elements": [ 84 | "a", 85 | "b" 86 | ] 87 | }, 88 | { 89 | "uid": "2", 90 | "elements": [ 91 | "b", 92 | "c" 93 | ] 94 | }, 95 | { 96 | "uid": "3", 97 | "elements": [ 98 | "b", 99 | "c", 100 | "d" 101 | ] 102 | }, 103 | { 104 | "uid": "4", 105 | "elements": [ 106 | "a", 107 | "c", 108 | "d" 109 | ] 110 | }, 111 | { 112 | "uid": "5", 113 | "elements": [ 114 | "a", 115 | "c", 116 | "d", 117 | "e" 118 | ] 119 | }, 120 | { 121 | "uid": "6", 122 | "elements": [ 123 | "d", 124 | "e", 125 | "f" 126 | ] 127 | }, 128 | { 129 | "uid": "7", 130 | "elements": [ 131 | "d", 132 | "e", 133 | "g" 134 | ] 135 | }, 136 | { 137 | "uid": "8", 138 | "elements": [ 139 | "d", 140 | "h" 141 | ] 142 | }, 143 | { 144 | "uid": "9", 145 | "elements": [ 146 | "e", 147 | "f", 148 | "g" 149 | ] 150 | }, 151 | { 152 | "uid": "10", 153 | "elements": [ 154 | "d", 155 | "f", 156 | "g" 157 | ] 158 | }, 159 | { 160 | "uid": "11", 161 | "elements": [ 162 | "f", 163 | "g", 164 | "i" 165 | ] 166 | }, 167 | { 168 | "uid": "12", 169 | "elements": [ 170 | "g", 171 | "i", 172 | "j" 173 | ] 174 | }, 175 | { 176 | "uid": "13", 177 | "elements": [ 178 | "i", 179 | "k" 180 | ] 181 | }, 182 | { 183 | "uid": "14", 184 | "elements": [ 185 | "k", 186 | "l" 187 | ] 188 | }, 189 | { 190 | "uid": "15", 191 | "elements": [ 192 | "l", 193 | "m" 194 | ] 195 | }, 196 | { 197 | "uid": "16", 198 | "elements": [ 199 | "i", 200 | "j", 201 | "m", 202 | "o", 203 | "p" 204 | ] 205 | }, 206 | { 207 | "uid": "17", 208 | "elements": [ 209 | "j", 210 | "m", 211 | "n", 212 | "o", 213 | "p" 214 | ] 215 | }, 216 | { 217 | "uid": "18", 218 | "elements": [ 219 | "o", 220 | "p", 221 | "q", 222 | "r", 223 | "s" 224 | ] 225 | } 226 | ], 227 | "edgeStroke": { 228 | "1": "#1f77b4ff", 229 | "2": "#ff7f0eff", 230 | "3": "#2ca02cff", 231 | "4": "#d62728ff", 232 | "5": "#9467bdff", 233 | "6": "#8c564bff", 234 | "7": "#e377c2ff", 235 | "8": "#7f7f7fff", 236 | "9": "#bcbd22ff", 237 | "10": "#17becfff", 238 | "11": "#1f77b4ff", 239 | "12": "#ff7f0eff", 240 | "13": "#2ca02cff", 241 | "14": "#d62728ff", 242 | "15": "#9467bdff", 243 | "16": "#8c564bff", 244 | "17": "#e377c2ff", 245 | "18": "#7f7f7fff" 246 | }, 247 | "edgeStrokeWidth": { 248 | "1": 2, 249 | "2": 2, 250 | "3": 2, 251 | "4": 2, 252 | "5": 2, 253 | "6": 2, 254 | "7": 2, 255 | "8": 2, 256 | "9": 2, 257 | "10": 2, 258 | "11": 2, 259 | "12": 2, 260 | "13": 2, 261 | "14": 2, 262 | "15": 2, 263 | "16": 2, 264 | "17": 2, 265 | "18": 2 266 | }, 267 | "edgeLabelColor": { 268 | "1": "#1f77b4ff", 269 | "2": "#ff7f0eff", 270 | "3": "#2ca02cff", 271 | "4": "#d62728ff", 272 | "5": "#9467bdff", 273 | "6": "#8c564bff", 274 | "7": "#e377c2ff", 275 | "8": "#7f7f7fff", 276 | "9": "#bcbd22ff", 277 | "10": "#17becfff", 278 | "11": "#1f77b4ff", 279 | "12": "#ff7f0eff", 280 | "13": "#2ca02cff", 281 | "14": "#d62728ff", 282 | "15": "#9467bdff", 283 | "16": "#8c564bff", 284 | "17": "#e377c2ff", 285 | "18": "#7f7f7fff" 286 | }, 287 | "pos": { 288 | "a": [ 289 | 244.44444444444446, 290 | 400 291 | ], 292 | "b": [ 293 | 400, 294 | 336.3636363636364 295 | ], 296 | "c": [ 297 | 302.77777777777777, 298 | 304.5454545454545 299 | ], 300 | "d": [ 301 | 244.44444444444446, 302 | 256.8181818181818 303 | ], 304 | "e": [ 305 | 186.11111111111111, 306 | 304.5454545454545 307 | ], 308 | "f": [ 309 | 127.77777777777777, 310 | 256.8181818181818 311 | ], 312 | "g": [ 313 | 186.11111111111111, 314 | 209.0909090909091 315 | ], 316 | "h": [ 317 | 302.77777777777777, 318 | 209.0909090909091 319 | ], 320 | "i": [ 321 | 127.77777777777777, 322 | 145.45454545454544 323 | ], 324 | "j": [ 325 | 186.11111111111111, 326 | 145.45454545454544 327 | ], 328 | "k": [ 329 | 50, 330 | 145.45454545454544 331 | ], 332 | "l": [ 333 | 50, 334 | 81.81818181818181 335 | ], 336 | "m": [ 337 | 127.77777777777777, 338 | 81.81818181818181 339 | ], 340 | "n": [ 341 | 176.38888888888889, 342 | 50 343 | ], 344 | "o": [ 345 | 225, 346 | 129.54545454545456 347 | ], 348 | "p": [ 349 | 225, 350 | 81.81818181818181 351 | ], 352 | "q": [ 353 | 302.77777777777777, 354 | 129.54545454545456 355 | ], 356 | "r": [ 357 | 341.6666666666667, 358 | 105.68181818181819 359 | ], 360 | "s": [ 361 | 302.77777777777777, 362 | 81.81818181818181 363 | ] 364 | }, 365 | "_model": "IPY_MODEL_8c5539cb60ca408182c776dc823ba6b0" 366 | } -------------------------------------------------------------------------------- /src/stories/data/props-with-metadata.json: -------------------------------------------------------------------------------- 1 | {"nodes": [{"uid": "JV", "value": 1}, {"uid": "BR", "value": 1}, {"uid": "TH", "value": 1}, {"uid": "MA", "value": 1}, {"uid": "JA", "value": 1}, {"uid": "CC", "value": 1}, {"uid": "MP", "value": 1}, {"uid": "GP", "value": 1}, {"uid": "JU", "value": 1}, {"uid": "CN", "value": 1}, {"uid": "BM", "value": 1}, {"uid": "FN", "value": 1}, {"uid": "CH", "value": 1}], "edges": [{"uid": "0", "elements": ["FN", "TH"]}, {"uid": "1", "elements": ["JV", "TH"]}, {"uid": "2", "elements": ["BM", "FN", "JA"]}, {"uid": "3", "elements": ["JU", "BM", "JV", "CH"]}, {"uid": "4", "elements": ["JV", "BR", "JU", "CH", "CN", "BM", "CC"]}, {"uid": "5", "elements": ["TH", "GP"]}, {"uid": "6", "elements": ["MP", "GP"]}, {"uid": "7", "elements": ["MA", "GP"]}], "edgeStroke": {"0": "#1f77b4ff", "1": "#ff7f0eff", "2": "#2ca02cff", "3": "#d62728ff", "4": "#9467bdff", "5": "#8c564bff", "6": "#e377c2ff", "7": "#7f7f7fff"}, "edgeStrokeWidth": {"0": 2, "1": 2, "2": 2, "3": 2, "4": 2, "5": 2, "6": 2, "7": 2}, "edgeLabelColor": {"0": "#1f77b4ff", "1": "#ff7f0eff", "2": "#2ca02cff", "3": "#d62728ff", "4": "#9467bdff", "5": "#8c564bff", "6": "#e377c2ff", "7": "#7f7f7fff"}, "nodeData": {"BM": {"Degree": 3, "Centrality": 0.17424242424242425}, "BR": {"Degree": 1, "Centrality": 0}, "CC": {"Degree": 1, "Centrality": 0}, "CH": {"Degree": 2, "Centrality": 0}, "CN": {"Degree": 1, "Centrality": 0}, "FN": {"Degree": 2, "Centrality": 0.09090909090909091}, "GP": {"Degree": 3, "Centrality": 0.3181818181818182}, "JA": {"Degree": 1, "Centrality": 0}, "JU": {"Degree": 2, "Centrality": 0}, "JV": {"Degree": 3, "Centrality": 0.3333333333333333}, "MA": {"Degree": 1, "Centrality": 0}, "MP": {"Degree": 1, "Centrality": 0}, "TH": {"Degree": 3, "Centrality": 0.4166666666666667}}, "edgeData": {"0": {"Degree": 2, "Centrality": 0.15873015873015875}, "1": {"Degree": 2, "Centrality": 0.3333333333333333}, "2": {"Degree": 3, "Centrality": 0.047619047619047616}, "3": {"Degree": 4, "Centrality": 0.015873015873015872}, "4": {"Degree": 7, "Centrality": 0.015873015873015872}, "5": {"Degree": 2, "Centrality": 0.47619047619047616}, "6": {"Degree": 2, "Centrality": 0}, "7": {"Degree": 2, "Centrality": 0}}} -------------------------------------------------------------------------------- /src/stories/data/props-with-radius.json: -------------------------------------------------------------------------------- 1 | {"nodes": [{"uid": "JU", "value": 1}, {"uid": "JA", "value": 1}, {"uid": "CC", "value": 1}, {"uid": "CH", "value": 1}, {"uid": "BR", "value": 1}, {"uid": "CN", "value": 1}, {"uid": "GP", "value": 1}, {"uid": "JV", "value": 1}, {"uid": "TH", "value": 1}, {"uid": "MA", "value": 1}, {"uid": "FN", "value": 1}, {"uid": "MP", "value": 1}, {"uid": "BM", "value": 1}], "edges": [{"uid": "0", "elements": ["TH", "FN"]}, {"uid": "1", "elements": ["JV", "TH"]}, {"uid": "2", "elements": ["JA", "BM", "FN"]}, {"uid": "3", "elements": ["JU", "JV", "BM", "CH"]}, {"uid": "4", "elements": ["JU", "CC", "CH", "BR", "CN", "JV", "BM"]}, {"uid": "5", "elements": ["GP", "TH"]}, {"uid": "6", "elements": ["GP", "MP"]}, {"uid": "7", "elements": ["GP", "MA"]}], "edgeStroke": {"0": "#1f77b4ff", "1": "#ff7f0eff", "2": "#2ca02cff", "3": "#d62728ff", "4": "#9467bdff", "5": "#8c564bff", "6": "#e377c2ff", "7": "#7f7f7fff"}, "edgeStrokeWidth": {"0": 2, "1": 2, "2": 2, "3": 2, "4": 2, "5": 2, "6": 2, "7": 2}, "edgeLabelColor": {"0": "#1f77b4ff", "1": "#ff7f0eff", "2": "#2ca02cff", "3": "#d62728ff", "4": "#9467bdff", "5": "#8c564bff", "6": "#e377c2ff", "7": "#7f7f7fff"}, "nodeRadius": {"BM": 3, "BR": 1, "CC": 1, "CH": 2, "CN": 1, "FN": 2, "GP": 3, "JA": 1, "JU": 2, "JV": 3, "MA": 1, "MP": 1, "TH": 3}} -------------------------------------------------------------------------------- /src/stories/data/props.json: -------------------------------------------------------------------------------- 1 | {"nodes": [{"uid": "FN", "value": 1}, {"uid": "TH", "value": 1}, {"uid": "JV", "value": 1}, {"uid": "BM", "value": 1}, {"uid": "JA", "value": 1}, {"uid": "JU", "value": 1}, {"uid": "CH", "value": 1}, {"uid": "BR", "value": 1}, {"uid": "CN", "value": 1}, {"uid": "CC", "value": 1}, {"uid": "GP", "value": 1}, {"uid": "MP", "value": 1}, {"uid": "MA", "value": 1}], "edges": [{"uid": "0", "elements": ["FN", "TH"], "level": 0}, {"uid": "1", "elements": ["TH", "JV"], "level": 0}, {"uid": "2", "elements": ["BM", "FN", "JA"], "level": 0}, {"uid": "3", "elements": ["JV", "JU", "CH", "BM"], "level": 0}, {"uid": "4", "elements": ["JU", "CH", "BR", "CN", "CC", "JV", "BM"], "level": 1}, {"uid": "5", "elements": ["TH", "GP"], "level": 0}, {"uid": "6", "elements": ["GP", "MP"], "level": 0}, {"uid": "7", "elements": ["MA", "GP"], "level": 0}], "edgeStroke": {"0": "#1f77b4ff", "1": "#ff7f0eff", "2": "#2ca02cff", "3": "#d62728ff", "4": "#9467bdff", "5": "#8c564bff", "6": "#e377c2ff", "7": "#7f7f7fff"}, "edgeStrokeWidth": {"0": 2, "1": 2, "2": 2, "3": 2, "4": 2, "5": 2, "6": 2, "7": 2}, "edgeLabelColor": {"0": "#1f77b4ff", "1": "#ff7f0eff", "2": "#2ca02cff", "3": "#d62728ff", "4": "#9467bdff", "5": "#8c564bff", "6": "#e377c2ff", "7": "#7f7f7fff"}, "nodeData": {"FN": {"a": 0.592, "b": 0.821}, "TH": {"a": 0.812, "b": 0.671}, "JV": {"a": 0.567, "b": 0.567}, "BM": {"a": 0.102, "b": 0.699}, "JA": {"a": 0.089, "b": 0.936}, "JU": {"a": 0.241, "b": 0.881}, "CH": {"a": 0.53, "b": 0.923}, "BR": {"a": 0.451, "b": 0.916}, "CN": {"a": 0.697, "b": 0.933}, "CC": {"a": 0.841, "b": 0.574}, "GP": {"a": 0.32, "b": 0.136}, "MP": {"a": 0.951, "b": 0.9}, "MA": {"a": 0.136, "b": 0.749}}, "edgeData": {"0": {"c": 0.235, "d": 0.492}, "1": {"c": 0.691, "d": 0.204}, "2": {"c": 0.616, "d": 0.68}, "3": {"c": 0.125, "d": 0.572}, "4": {"c": 0.69, "d": 0.953}, "5": {"c": 0.664, "d": 0.653}, "6": {"c": 0.735, "d": 0.176}, "7": {"c": 0.84, "d": 0.675}}, "nodeLabels": {"JV": "Jean Valjean"}, "edgeLabels": {"0": "some edge"}} -------------------------------------------------------------------------------- /src/stories/hnx-widget-basics.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Grid from '@material-ui/core/Grid'; 4 | 5 | import HypernetxWidgetView from '../HypernetxWidgetView'; 6 | import {HypernetxWidgetDualView} from '../HypernetxWidgetDualView'; 7 | 8 | import props from './data/props.json' 9 | 10 | 11 | export default { 12 | title: 'HNX Widget SVG/Basics', 13 | }; 14 | 15 | export const Euler = () => 16 | 17 | 18 | export const EulerDual = () => 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | export const EulerWithoutPlanarForce = () => 30 | 31 | 32 | export const EulerCollapsed = () => 33 | 34 | 35 | export const EulerCollapsedWithoutPlanarForce = () => 36 | 37 | 38 | export const Bipartite = () => 39 | 40 | 41 | export const BipartiteCollapsed = () => 42 | 43 | -------------------------------------------------------------------------------- /src/stories/hnx-widget-brush.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import HypernetxWidgetView from '../HypernetxWidgetView'; 4 | 5 | import props from './data/props.json' 6 | 7 | 8 | export default { 9 | title: 'HNX Widget SVG/Brushing', 10 | }; 11 | 12 | export const NodeBrush = () => 13 | 17 | 18 | export const EdgeBrush = () => 19 | 23 | 24 | -------------------------------------------------------------------------------- /src/stories/hnx-widget-encodings.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // import {HypernetxWidget} from '..' 4 | import HypernetxWidgetView from '../HypernetxWidgetView'; 5 | 6 | import props from './data/props.json' 7 | import propsWithRadius from './data/props-with-radius.json' 8 | 9 | export default { 10 | title: 'HNX Widget SVG/Encodings', 11 | }; 12 | 13 | // console.log(props) 14 | 15 | const nodes = {'JV': true, 'TH': true}; 16 | const edges = {'1': true, '2': true}; 17 | 18 | export const SelectedNodes = () => 19 | 20 | 21 | export const SelectedEdges = () => 22 | 23 | 24 | export const HiddenNodes = () => 25 | 26 | 27 | export const HiddenEdges = () => 28 | 29 | 30 | export const RemovedNodes = () => 31 | 32 | 33 | export const RemovedEdges = () => 34 | 35 | 36 | export const WithRadius = () => 37 | 38 | 39 | export const WithRadiusCollapsed = () => 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/stories/hnx-widget-interactions.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import IconButton from '@material-ui/core/IconButton' 4 | import LockOpen from '@material-ui/icons/LockOpen' 5 | 6 | // import {HypernetxWidget} from '..' 7 | import HypernetxWidgetView, {now} from '../HypernetxWidgetView'; 8 | 9 | import props from './data/props.json' 10 | 11 | export default { 12 | title: 'HNX Widget SVG/Interactions', 13 | }; 14 | 15 | export const LogNodeClick = () => 16 | 17 | 18 | export const LogEdgeClick = () => 19 | 20 | 21 | function UnpinButton() { 22 | const [unpinned, setUnpinned] = React.useState(now()); 23 | 24 | return
    25 | { unpinned } 26 | setUnpinned(now())}> 27 | 28 | 29 | 30 |
    31 | } 32 | 33 | export const UnPin = () => 34 | -------------------------------------------------------------------------------- /src/stories/hnx-widget-loadings.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Widget from "../widget.js"; 3 | import props from "./data/props.json"; 4 | import metaprops from "./data/props-with-metadata.json"; 5 | import "../css/hnxStyle.css"; 6 | 7 | export default { 8 | title: "HNX Widget SVG/Loadings", 9 | }; 10 | 11 | const nodeColorDict = { 12 | BM: "rgba(193, 122, 158, 0.5)", 13 | BR: "rgba(150, 213, 95, 0.7)", 14 | CC: "rgba(88, 148, 150, 0.8)", 15 | CH: "rgba(136, 25, 118, 0.8)", 16 | CN: "rgba(176, 220, 206, 0.7)", 17 | FN: "rgba(79, 255, 88, 0.7)", 18 | GP: "rgba(75, 184, 25, 0.9)", 19 | JA: "rgba(213, 59, 43, 0.8)", 20 | JU: "rgba(80, 35, 135, 0.2)", 21 | JV: "rgba(209, 137, 72, 0.5)", 22 | MA: "rgba(227, 134, 90, 0.7)", 23 | MP: "rgba(190, 222, 243, 0.6)", 24 | TH: "rgba(156, 10, 167, 0.6)", 25 | }; 26 | 27 | const nodeHiddenDict = { 28 | BM: true, 29 | BR: false, 30 | CC: false, 31 | CH: false, 32 | CN: true, 33 | FN: false, 34 | GP: false, 35 | JA: false, 36 | JU: false, 37 | JV: false, 38 | MA: false, 39 | MP: false, 40 | TH: false, 41 | }; 42 | const nodeRemovedDict = { 43 | BM: false, 44 | BR: false, 45 | CC: false, 46 | CH: false, 47 | CN: true, 48 | FN: false, 49 | GP: false, 50 | JA: false, 51 | JU: false, 52 | JV: false, 53 | MA: false, 54 | MP: false, 55 | TH: false, 56 | }; 57 | 58 | const edgeHiddenDict = { 59 | 0: false, 60 | 1: false, 61 | 2: true, 62 | 3: false, 63 | 4: false, 64 | 5: false, 65 | 6: true, 66 | 7: false, 67 | }; 68 | 69 | const edgeRemovedDict = { 70 | 0: false, 71 | 1: false, 72 | 2: true, 73 | 3: false, 74 | 4: false, 75 | 5: false, 76 | 6: false, 77 | 7: false, 78 | }; 79 | const edgeColorDict = { 80 | 0: "rgba(207, 238, 73, 0.1)", 81 | 1: "rgba(115, 4, 64, 0.8)", 82 | 2: "rgba(134, 128, 107, 0.9)", 83 | 3: "rgba(244, 148, 117, 0.9)", 84 | 4: "rgba(36, 34, 132, 0.8)", 85 | 5: "rgba(73, 84, 27, 0.2)", 86 | 6: "rgba(98, 213, 173, 1)", 87 | 7: "rgba(0, 0, 0, 1)", 88 | }; 89 | 90 | // console.log({...props}); 91 | // export const MainComponent = () =>
    92 | // 100 | //
    101 | 102 | export const WithMetaData = () => ( 103 |
    104 | 105 |
    106 | ); 107 | -------------------------------------------------------------------------------- /src/stories/hnx-widget-navigation.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import NavigableSVG, {PAN, RESET, ZOOM_IN, ZOOM_OUT} from '../NavigableSVG' 4 | 5 | export default { 6 | title: 'HNX Widget SVG/Navigation', 7 | }; 8 | 9 | const props = { 10 | width: 400, 11 | height: 300 12 | }; 13 | 14 | const Pattern = () => 15 | 16 | 17 | 18 | How do we make font size constant? 19 | 20 | 21 | function SimpleNavigation() { 22 | const [nav, setNav] = React.useState(); 23 | 24 | const options = [undefined, RESET, PAN, ZOOM_IN, ZOOM_OUT]; 25 | 26 | const handleClick = ev => 27 | console.log(ev.target.value) || 28 | setNav(ev.target.value) 29 | 30 | return
    31 | { options.map(d => 32 |
    33 | 36 |
    37 | ) 38 | } 39 | 40 | 41 | 42 | 43 |
    44 | } 45 | 46 | export const Default = () => 47 | 48 | 49 | 50 | 51 | export const Pan = () => 52 | 53 | 54 | 55 | 56 | export const ZoomIn = () => 57 | 58 | 59 | 60 | 61 | export const ZoomOut = () => 62 | 63 | 64 | 65 | 66 | export const Interactive = () => 67 | 68 | -------------------------------------------------------------------------------- /src/stories/hnx-widget-position.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // import {HypernetxWidget} from '..' 4 | import HypernetxWidgetView, {HypernetxWidget} from '..'; 5 | 6 | import props from './data/biggerProps.json' 7 | 8 | console.log(props); 9 | 10 | const TestBackboneModel = () => { 11 | const state = {}; 12 | 13 | return { 14 | save: () => console.log('Saving', state), 15 | set: (key, value) => state[key] = value 16 | }; 17 | } 18 | 19 | export const TestPositionInput = () => 20 | 24 | 25 | export const TestPositionInputWithoutModel = () => 26 | 29 | 30 | export const TestPositionInputWithFullUI = () => 31 | 35 | 36 | export const TestWithoutPositionInputWithFullUI = () => 37 | 42 | 43 | export default { 44 | title: 'HNX Widget SVG/Position IO', 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /src/stories/hnx-widget-responsive.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import HypernetxWidgetView from '../HypernetxWidgetView'; 4 | 5 | import props from './data/props.json' 6 | 7 | 8 | export default { 9 | title: 'HNX Widget SVG/Responsive', 10 | }; 11 | 12 | export const Default = () => 13 |
    14 | 15 |
    16 | 17 | export const WidthConstrained = () => 18 |
    19 | 20 |
    21 | 22 | export const HeightConstrained = () => 23 |
    24 | 25 |
    26 | 27 | export const WidthAndHeightConstrained = () => 28 |
    29 | 30 |
    31 | 32 | -------------------------------------------------------------------------------- /src/switches.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormControlLabel, FormGroup, Switch } from "@material-ui/core"; 3 | 4 | const Switches = ({ currData, dataType, onSwitchChange }) => { 5 | const [state, setState] = React.useState({ 6 | collapseNodes: currData.collapseState, 7 | bipartite: currData.bipartiteState, 8 | }); 9 | 10 | React.useEffect(() => { 11 | setState({ ...state, collapseNodes: currData.collapseState }); 12 | }, [currData.collapseState]); 13 | 14 | React.useEffect(() => { 15 | setState({ ...state, bipartite: currData.bipartiteState }); 16 | }, [currData.bipartiteState]); 17 | 18 | const handleChange = (event) => { 19 | setState({ ...state, [event.target.name]: event.target.checked }); 20 | onSwitchChange(dataType, { 21 | ...state, 22 | [event.target.name]: event.target.checked, 23 | }); 24 | }; 25 | 26 | return ( 27 |
    35 | {/**/} 36 | {/**/} 45 | {/* }*/} 46 | {/* label={
    Show labels
    }*/} 47 | {/*/>*/} 48 | {dataType === "node" && ( 49 | 58 | } 59 | label={
    Collapse nodes
    } 60 | /> 61 | )} 62 | {dataType === "edge" && ( 63 | 72 | } 73 | label={
    Bipartite
    } 74 | /> 75 | )} 76 | {/*
    */} 77 |
    78 | ); 79 | }; 80 | 81 | export default Switches; 82 | -------------------------------------------------------------------------------- /src/toolbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab"; 3 | import { 4 | VisibilityOutlined, 5 | VisibilityOff, 6 | SettingsBackupRestore, 7 | PictureInPicture, 8 | SelectAll, 9 | FlipCameraAndroid, 10 | BubbleChart, 11 | LinearScale, 12 | Navigation, 13 | ZoomIn, 14 | ZoomOut, 15 | OpenWith, 16 | Transform, 17 | ZoomOutMap, 18 | HelpOutlined, 19 | LocationOff, 20 | RemoveCircleOutlineOutlined, 21 | CallMadeOutlined, 22 | Flip, 23 | Block, 24 | } from "@material-ui/icons"; 25 | import { makeStyles } from "@material-ui/core/styles"; 26 | import IconWithTooltip from "./iconWithTooltip"; 27 | 28 | const toggleStyle = makeStyles((theme) => ({ 29 | toggleButton: { 30 | "& .MuiToggleButton-root": { 31 | fontSize: "9px", 32 | height: 30, 33 | maxWidth: 100, 34 | }, 35 | "& .Mui-selected": { 36 | color: "black", 37 | }, 38 | "& .Mui-disabled": { 39 | pointerEvents: "auto", 40 | }, 41 | }, 42 | })); 43 | const Toolbar = ({ 44 | category, 45 | dataType, 46 | currToggle, 47 | selectionState, 48 | onSelectionChange, 49 | }) => { 50 | const classes = toggleStyle(); 51 | const [selectionType, setSelectionType] = React.useState(null); 52 | 53 | React.useEffect(() => { 54 | setSelectionType(currToggle); 55 | }, [currToggle]); 56 | 57 | const handleSelection = (event, newSelection) => { 58 | if (newSelection === null) { 59 | if (selectionType === "collapse") { 60 | onSelectionChange(dataType, "undo-collapse"); 61 | } else if (selectionType === "bipartite") { 62 | onSelectionChange(dataType, "undo-bipartite"); 63 | } else { 64 | onSelectionChange(dataType, selectionType); 65 | } 66 | } else { 67 | setSelectionType(newSelection); 68 | if (dataType === undefined) { 69 | onSelectionChange(category, newSelection); 70 | } else { 71 | onSelectionChange(dataType, newSelection); 72 | } 73 | } 74 | }; 75 | 76 | // console.log(selectionType); 77 | 78 | return ( 79 |
    80 |
    83 | {category === "Data" ? dataType : category} 84 |
    85 | {category === "Data" && ( 86 | 93 | 94 | } 97 | /> 98 | 99 | 103 | 108 | ) : ( 109 | 110 | ) 111 | } 112 | /> 113 | 114 | 118 | } 121 | /> 122 | 123 | 127 | } 134 | /> 135 | 136 | 137 | } /> 138 | 139 | 143 | } /> 144 | 145 | 149 | } 152 | /> 153 | 154 | {dataType === "Nodes" && ( 155 | 156 | } /> 157 | 158 | )} 159 | {dataType === "Nodes" && ( 160 | 161 | } 164 | /> 165 | 166 | )} 167 | 168 | {dataType === "Edges" && ( 169 | 170 | } 173 | /> 174 | 175 | )} 176 | 177 | )} 178 | 179 | {/*{category === "Graph" && (*/} 180 | {/* */} 187 | {/* */} 188 | {/* }*/} 191 | {/* />*/} 192 | {/* */} 193 | {/* */} 194 | {/* }*/} 197 | {/* />*/} 198 | {/* */} 199 | {/* */} 200 | {/* }*/} 203 | {/* />*/} 204 | {/* */} 205 | {/* */} 206 | {/*)}*/} 207 | 208 | {category === "Selection" && ( 209 | 216 | 217 | } 220 | /> 221 | 222 | 223 | } 226 | /> 227 | 228 | 229 | } 232 | /> 233 | 234 | 235 | )} 236 | 237 | {category === "Navigation" && ( 238 | 245 | 246 | } 249 | /> 250 | 251 | 252 | } /> 253 | 254 | 255 | } /> 256 | 257 | 258 | } /> 259 | 260 | 261 | )} 262 | 263 | {category === "View" && ( 264 | 271 | 272 | } 275 | /> 276 | 277 | 278 | } 281 | /> 282 | 283 | 284 | } 287 | /> 288 | 289 | 290 | )} 291 |
    292 | ); 293 | }; 294 | 295 | export default Toolbar; 296 | -------------------------------------------------------------------------------- /src/visibilityButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IconButton } from "@material-ui/core"; 3 | import { Visibility, VisibilityOff } from "@material-ui/icons"; 4 | 5 | const VisibilityButton = ({ label, visibility, onVisibilityChange }) => { 6 | const handleShow = () => { 7 | // setShow(!show); 8 | onVisibilityChange(label, !visibility); 9 | }; 10 | 11 | return ( 12 |
    13 |
    14 | 15 | {visibility ? ( 16 | 17 | ) : ( 18 | 19 | )} 20 | 21 |
    22 |
    23 | ); 24 | }; 25 | 26 | export default VisibilityButton; 27 | -------------------------------------------------------------------------------- /update-develop.sh: -------------------------------------------------------------------------------- 1 | npm run build -- --copy-files --no-demo 2 | cd widget/js 3 | npm run prepublish 4 | cd ../../ 5 | -------------------------------------------------------------------------------- /widget/MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include hnxwidget/static *.* 2 | include hnx-widget.json 3 | -------------------------------------------------------------------------------- /widget/README.md: -------------------------------------------------------------------------------- 1 | hnx-widget 2 | =============================== 3 | 4 | A widget for interractive visualization of the hypernetx package. 5 | 6 | Installation 7 | ------------ 8 | 9 | To install use pip: 10 | 11 | $ pip install hnxwidget 12 | $ jupyter nbextension enable --py --sys-prefix hnxwidget 13 | 14 | To install for jupyterlab 15 | 16 | $ jupyter labextension install hnxwidget 17 | 18 | For a development installation (requires npm), 19 | 20 | $ git clone https://github.com/PNNL/hnx-widget.git 21 | $ cd hnx-widget 22 | $ pip install -e . 23 | $ jupyter nbextension install --py --symlink --sys-prefix hnxwidget 24 | $ jupyter nbextension enable --py --sys-prefix hnxwidget 25 | $ jupyter labextension install js 26 | 27 | When actively developing your extension, build Jupyter Lab with the command: 28 | 29 | $ jupyter lab --watch 30 | 31 | This takes a minute or so to get started, but then automatically rebuilds JupyterLab when your javascript changes. 32 | 33 | Note on first `jupyter lab --watch`, you may need to touch a file to get Jupyter Lab to open. 34 | 35 | -------------------------------------------------------------------------------- /widget/RELEASE.md: -------------------------------------------------------------------------------- 1 | - To release a new version of hnxwidget on PyPI: 2 | 3 | Update _version.py (set release version, remove 'dev') 4 | git add the _version.py file and git commit 5 | `python setup.py sdist upload` 6 | `python setup.py bdist_wheel upload` 7 | `git tag -a X.X.X -m 'comment'` 8 | Update _version.py (add 'dev' and increment minor) 9 | git add and git commit 10 | git push 11 | git push --tags 12 | 13 | - To release a new version of hnx-widget on NPM: 14 | 15 | Update `js/package.json` with new npm package version 16 | 17 | ``` 18 | # clean out the `dist` and `node_modules` directories 19 | git clean -fdx 20 | npm install 21 | npm publish 22 | ``` -------------------------------------------------------------------------------- /widget/dist/hnxwidget-0.1.0a0-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/widget/dist/hnxwidget-0.1.0a0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /widget/dist/hnxwidget-0.1.0a1-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/widget/dist/hnxwidget-0.1.0a1-py2.py3-none-any.whl -------------------------------------------------------------------------------- /widget/dist/hnxwidget-0.1.0a2-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/widget/dist/hnxwidget-0.1.0a2-py2.py3-none-any.whl -------------------------------------------------------------------------------- /widget/dist/hnxwidget-0.1.0a3-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/widget/dist/hnxwidget-0.1.0a3-py2.py3-none-any.whl -------------------------------------------------------------------------------- /widget/dist/hnxwidget-0.1.0a4-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/widget/dist/hnxwidget-0.1.0a4-py2.py3-none-any.whl -------------------------------------------------------------------------------- /widget/dist/hnxwidget-0.1.0b0-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/widget/dist/hnxwidget-0.1.0b0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /widget/dist/hnxwidget-0.1.0b1-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/widget/dist/hnxwidget-0.1.0b1-py2.py3-none-any.whl -------------------------------------------------------------------------------- /widget/dist/hnxwidget-0.1.0b2-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnnl/hypernetx-widget/58e795d6a362dcd3bbd9ccff462052956a40f4f8/widget/dist/hnxwidget-0.1.0b2-py2.py3-none-any.whl -------------------------------------------------------------------------------- /widget/hnx-widget.json: -------------------------------------------------------------------------------- 1 | { 2 | "load_extensions": { 3 | "hnx-widget/extension": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /widget/hnxwidget/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import version_info, __version__ 2 | 3 | from .hypernetx_widget import * 4 | 5 | def _jupyter_nbextension_paths(): 6 | """Called by Jupyter Notebook Server to detect if it is a valid nbextension and 7 | to install the widget 8 | 9 | Returns 10 | ======= 11 | section: The section of the Jupyter Notebook Server to change. 12 | Must be 'notebook' for widget extensions 13 | src: Source directory name to copy files from. Webpack outputs generated files 14 | into this directory and Jupyter Notebook copies from this directory during 15 | widget installation 16 | dest: Destination directory name to install widget files to. Jupyter Notebook copies 17 | from `src` directory into /nbextensions/ directory 18 | during widget installation 19 | require: Path to importable AMD Javascript module inside the 20 | /nbextensions/ directory 21 | """ 22 | return [{ 23 | 'section': 'notebook', 24 | 'src': 'static', 25 | 'dest': 'hnx-widget', 26 | 'require': 'hnx-widget/extension' 27 | }] 28 | -------------------------------------------------------------------------------- /widget/hnxwidget/_version.py: -------------------------------------------------------------------------------- 1 | # Module version 2 | 3 | version_info = (0, 1, 1, 'beta', 1) 4 | 5 | # Module version stage suffix map 6 | _specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} 7 | 8 | # Module version accessible using hnxwidget.__version__ 9 | __version__ = '%s.%s.%s%s'%(version_info[0], version_info[1], version_info[2], 10 | '' if version_info[3]=='final' else _specifier_[version_info[3]]+str(version_info[4])) 11 | -------------------------------------------------------------------------------- /widget/hnxwidget/example.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | from traitlets import Unicode 3 | 4 | # See js/lib/example.js for the frontend counterpart to this file. 5 | 6 | @widgets.register 7 | class HelloWorld(widgets.DOMWidget): 8 | """An example widget.""" 9 | 10 | # Name of the widget view class in front-end 11 | _view_name = Unicode('HelloView').tag(sync=True) 12 | 13 | # Name of the widget model class in front-end 14 | _model_name = Unicode('HelloModel').tag(sync=True) 15 | 16 | # Name of the front-end module containing widget view 17 | _view_module = Unicode('hnx-widget').tag(sync=True) 18 | 19 | # Name of the front-end module containing widget model 20 | _model_module = Unicode('hnx-widget').tag(sync=True) 21 | 22 | # Version of the front-end module containing widget view 23 | _view_module_version = Unicode('^0.1.0').tag(sync=True) 24 | # Version of the front-end module containing widget model 25 | _model_module_version = Unicode('^0.1.0').tag(sync=True) 26 | 27 | # Widget specific property. 28 | # Widget properties are defined as traitlets. Any property tagged with `sync=True` 29 | # is automatically synced to the frontend *any* time it changes in Python. 30 | # It is synced back to Python from the frontend *any* time the model is touched. 31 | value = Unicode('Hello World!').tag(sync=True) 32 | -------------------------------------------------------------------------------- /widget/hnxwidget/hypernetx_widget.py: -------------------------------------------------------------------------------- 1 | from .react_jupyter_widget import ReactJupyterWidget 2 | 3 | import ipywidgets as widgets 4 | from traitlets import Dict 5 | 6 | import matplotlib.pyplot as plt 7 | from matplotlib.colors import to_rgba_array, to_hex 8 | import numpy as np 9 | 10 | from .util import get_set_layering, inflate_kwargs 11 | 12 | from itertools import chain 13 | 14 | converters = { 15 | 'edgecolor': 'Stroke', 16 | 'edgecolors': 'Stroke', 17 | 'facecolor': 'Fill', 18 | 'facecolors': 'Fill', 19 | 'color': 'Fill', 20 | 'colors': 'Fill', 21 | 'linewidths': 'StrokeWidth', 22 | 'linewidth': 'StrokeWidth', 23 | } 24 | 25 | def to_camel_case(s): 26 | return ''.join([ 27 | si.title() if i > 0 else si 28 | for i, si in enumerate(s.split('_')) 29 | ]) 30 | 31 | def prepare_kwargs(items, kwargs, prefix=''): 32 | return { 33 | prefix + converters.get(k, k): 34 | dict(zip(items, hex_array(v) if 'color' in k else v)) 35 | for k, v in inflate_kwargs(items, kwargs).items() 36 | } 37 | 38 | def rename_kwargs(**kwargs): 39 | return { 40 | converters.get(k, to_camel_case(k)): v 41 | for k, v in kwargs.items() 42 | } 43 | 44 | def hex_array(values): 45 | return [ 46 | to_hex(c, keep_alpha=True) 47 | for c in to_rgba_array(values) 48 | ] 49 | 50 | def hnx_kwargs_to_props(V, E, 51 | nodes_kwargs={}, 52 | edges_kwargs={}, 53 | node_labels_kwargs={}, 54 | edge_labels_kwargs={}, 55 | **kwargs 56 | ): 57 | # reproduce default hnx coloring behaviors 58 | edges_kwargs = edges_kwargs.copy() 59 | edges_kwargs.setdefault('edgecolors', plt.cm.tab10(np.arange(len(E))%10)) 60 | edges_kwargs.setdefault('linewidths', 2) 61 | 62 | # props = kwargs.copy() 63 | props = {} 64 | props.update(prepare_kwargs(V, nodes_kwargs, prefix='node')) 65 | props.update(prepare_kwargs(V, node_labels_kwargs, prefix='nodeLabel')) 66 | props.update(prepare_kwargs(E, edges_kwargs, prefix='edge')) 67 | props.update(prepare_kwargs(E, edge_labels_kwargs, prefix='edgeLabel')) 68 | 69 | # if not otherwise specified, set the edge label color 70 | # to be the same as the edge color 71 | props.setdefault('edgeLabelColor', props['edgeStroke']) 72 | 73 | return {**props, **rename_kwargs(**kwargs)} 74 | 75 | @widgets.register 76 | class HypernetxWidgetView(ReactJupyterWidget): 77 | pos = Dict().tag(sync=True) 78 | node_fill = Dict().tag(sync=True) 79 | edge_stroke = Dict().tag(sync=True) 80 | selected_nodes = Dict().tag(sync=True) 81 | selected_edges = Dict().tag(sync=True) 82 | hidden_nodes = Dict().tag(sync=True) 83 | hidden_edges = Dict().tag(sync=True) 84 | removed_nodes = Dict().tag(sync=True) 85 | removed_edges = Dict().tag(sync=True) 86 | 87 | @property 88 | def state(self): 89 | return { 90 | 'pos': self.pos, 91 | 'node_fill': self.node_fill, 92 | 'edge_stroke': self.edge_stroke, 93 | 'selected_nodes': self.selected_nodes, 94 | 'selected_edges': self.selected_edges, 95 | 'hidden_nodes': self.hidden_nodes, 96 | 'hidden_edges': self.hidden_edges, 97 | 'removed_nodes': self.removed_nodes, 98 | 'removed_edges': self.removed_edges 99 | } 100 | 101 | def get_index(self, selection={}): 102 | return [ 103 | k 104 | for k, v in selection.items() 105 | if v 106 | ] 107 | 108 | @property 109 | def selected_node_data(self): 110 | return self.node_data.loc[self.get_index(self.selected_nodes)] 111 | 112 | @property 113 | def selected_edge_data(self): 114 | return self.edge_data.loc[self.get_index(self.selected_edges)] 115 | 116 | def __init__(self, H, 117 | collapse=True, 118 | node_styles={}, 119 | with_color=True, 120 | **kwargs 121 | ): 122 | incidence_dict = {k: list(v) for k, v in H.incidence_dict.items()}\ 123 | if H.__class__.__name__ == 'Hypergraph'\ 124 | else H 125 | 126 | def get_property(id, value, default): 127 | if value is None: 128 | return default 129 | elif hasattr(value, 'get'): 130 | return value.get(id, default) 131 | else: 132 | return value 133 | 134 | V = set(chain.from_iterable(incidence_dict.values())) 135 | E = list(incidence_dict) 136 | 137 | nodes = [{'uid': uid} for uid in V] 138 | 139 | # js friendly representation of the hypergraph 140 | edges = [ 141 | { 142 | 'uid': str(uid), 143 | 'elements': elements 144 | } 145 | for uid, elements in incidence_dict.items() 146 | ] 147 | 148 | if 'node_data' in kwargs: 149 | self.node_data = kwargs['node_data'] 150 | kwargs['node_data'] = self.node_data.T.to_dict() 151 | 152 | if 'edge_data' in kwargs: 153 | self.edge_data = kwargs['edge_data'] 154 | kwargs['edge_data'] = self.edge_data.T.to_dict() 155 | 156 | super().__init__( 157 | nodes=nodes, 158 | edges=edges, 159 | **hnx_kwargs_to_props(V, E, **kwargs) 160 | ) 161 | 162 | @widgets.register 163 | class HypernetxWidget(HypernetxWidgetView): 164 | pass -------------------------------------------------------------------------------- /widget/hnxwidget/react_jupyter_widget.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | from traitlets import Unicode, Dict 3 | 4 | @widgets.register 5 | class ReactJupyterWidget(widgets.DOMWidget): 6 | """An example widget.""" 7 | _view_name = Unicode('ReactView').tag(sync=True) 8 | _model_name = Unicode('ReactModel').tag(sync=True) 9 | _view_module = Unicode('hnx-widget').tag(sync=True) 10 | _model_module = Unicode('hnx-widget').tag(sync=True) 11 | _view_module_version = Unicode('^0.1.0').tag(sync=True) 12 | _model_module_version = Unicode('^0.1.0').tag(sync=True) 13 | 14 | component = Unicode().tag(sync=True) 15 | props = Dict().tag(sync=True) 16 | 17 | def __init__(self, **kwargs): 18 | super().__init__() 19 | 20 | self.component = self.__class__.__name__ 21 | self.props = kwargs 22 | 23 | -------------------------------------------------------------------------------- /widget/hnxwidget/static/extension.js: -------------------------------------------------------------------------------- 1 | define(function() { return /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // getDefaultExport function for compatibility with non-harmony modules 48 | /******/ __webpack_require__.n = function(module) { 49 | /******/ var getter = module && module.__esModule ? 50 | /******/ function getDefault() { return module['default']; } : 51 | /******/ function getModuleExports() { return module; }; 52 | /******/ __webpack_require__.d(getter, 'a', getter); 53 | /******/ return getter; 54 | /******/ }; 55 | /******/ 56 | /******/ // Object.prototype.hasOwnProperty.call 57 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 58 | /******/ 59 | /******/ // __webpack_public_path__ 60 | /******/ __webpack_require__.p = ""; 61 | /******/ 62 | /******/ // Load entry module and return exports 63 | /******/ return __webpack_require__(__webpack_require__.s = 0); 64 | /******/ }) 65 | /************************************************************************/ 66 | /******/ ([ 67 | /* 0 */ 68 | /***/ (function(module, exports, __webpack_require__) { 69 | 70 | // This file contains the javascript that is run when the notebook is loaded. 71 | // It contains some requirejs configuration and the `load_ipython_extension` 72 | // which is required for any notebook extension. 73 | // 74 | // Some static assets may be required by the custom widget javascript. The base 75 | // url for the notebook is not known at build time and is therefore computed 76 | // dynamically. 77 | __webpack_require__.p = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/hnx-widget'; 78 | 79 | 80 | // Configure requirejs 81 | if (window.require) { 82 | window.require.config({ 83 | map: { 84 | "*" : { 85 | "hnx-widget": "nbextensions/hnx-widget/index", 86 | } 87 | } 88 | }); 89 | } 90 | 91 | // Export the required load_ipython_extension 92 | module.exports = { 93 | load_ipython_extension: function() {} 94 | }; 95 | 96 | 97 | /***/ }) 98 | /******/ ])});; -------------------------------------------------------------------------------- /widget/hnxwidget/stats.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | import hypernetx as hnx 4 | from hypernetx import algorithms as algo 5 | 6 | def compute_stats(H): 7 | 8 | def compute_stats_helper(H, stats): 9 | return pd.DataFrame({ 10 | k: func(H) 11 | for k, func in stats.items() 12 | }) 13 | 14 | node_stats = { 15 | 'Degree': lambda H: dict(zip(H, hnx.degree_dist(H))) 16 | } 17 | 18 | edges_stats = { 19 | 'Centrality': algo.s_centrality_measures.s_betweenness_centrality 20 | } 21 | 22 | D = H.dual() 23 | 24 | node_data = pd.concat((compute_stats_helper(H, node_stats), compute_stats_helper(D, edges_stats)), axis=1) 25 | edge_data = pd.concat((compute_stats_helper(D, node_stats), compute_stats_helper(H, edges_stats)), axis=1) 26 | 27 | return { 28 | 'node_data': node_data, 29 | 'edge_data': edge_data 30 | } -------------------------------------------------------------------------------- /widget/hnxwidget/util.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2018 Battelle Memorial Institute 2 | # All rights reserved. 3 | 4 | from itertools import combinations 5 | 6 | import numpy as np 7 | 8 | import networkx as nx 9 | 10 | def inflate(items, v): 11 | if type(v) in {str, tuple, int, float}: 12 | return [v]*len(items) 13 | elif callable(v): 14 | return [v(i) for i in items] 15 | elif type(v) not in {list, np.ndarray} and hasattr(v, '__getitem__'): 16 | return [v[i] for i in items] 17 | return v 18 | 19 | def inflate_kwargs(items, kwargs): 20 | ''' 21 | Helper function to expand keyword arguments. 22 | 23 | Parameters 24 | ---------- 25 | n: int 26 | length of resulting list if argument is expanded 27 | kwargs: dict 28 | keyword arguments to be expanded 29 | 30 | Returns 31 | ------- 32 | dict 33 | dictionary with same keys as kwargs and whose values are lists of length n 34 | ''' 35 | 36 | return { 37 | k: inflate(items, v) 38 | for k, v in kwargs.items() 39 | } 40 | 41 | def transpose_inflated_kwargs(inflated): 42 | return [ 43 | dict(zip(inflated, v)) 44 | for v in zip(*inflated.values()) 45 | ] 46 | 47 | def get_frozenset_label(S, count=False, override={}): 48 | ''' 49 | Helper function for rendering the labels of possibly collapsed nodes and edges 50 | 51 | Parameters 52 | ---------- 53 | S: iterable 54 | list of entities to be labeled 55 | count: bool 56 | True if labels should be counts of entities instead of list 57 | 58 | Returns 59 | ------- 60 | dict 61 | mapping of entity to its string representation 62 | ''' 63 | def helper(v): 64 | if type(v) == frozenset: 65 | if count and len(v) > 1: 66 | return f'x {len(v)}' 67 | elif count: 68 | return '' 69 | else: 70 | return ', '.join([str(override.get(s, s)) for s in v]) 71 | return str(v) 72 | 73 | return {v: override.get(v, helper(v)) for v in S} 74 | 75 | def get_line_graph(H, collapse=True): 76 | ''' 77 | Computes the line graph, a directed graph, where a directed edge (u, v) 78 | exists if the edge u is a subset of the edge v in the hypergraph. 79 | 80 | Parameters 81 | ---------- 82 | H: Hypergraph 83 | the entity to be drawn 84 | collapse: bool 85 | True if edges should be added if hyper edges are identical 86 | 87 | Returns 88 | ------- 89 | networkx.DiGraph 90 | A directed graph 91 | ''' 92 | D = nx.DiGraph() 93 | 94 | V = {edge: set(nodes) 95 | for edge, nodes in H.edges.elements.items()} 96 | 97 | D.add_nodes_from(V) 98 | 99 | for u, v in combinations(V, 2): 100 | if V[u] != V[v] or not collapse: 101 | if V[u].issubset(V[v]): 102 | D.add_edge(u, v) 103 | elif V[v].issubset(V[u]): 104 | D.add_edge(v, u) 105 | 106 | return D 107 | 108 | def get_set_layering(H, collapse=True): 109 | ''' 110 | Computes a layering of the edges in the hyper graph. 111 | 112 | In this layering, each edge is assigned a level. An edge u will be above 113 | (e.g., have a smaller level value) another edge v if v is a subset of u. 114 | 115 | Parameters 116 | ---------- 117 | H: Hypergraph 118 | the entity to be drawn 119 | collapse: bool 120 | True if edges should be added if hyper edges are identical 121 | 122 | Returns 123 | ------- 124 | dict 125 | a mapping of vertices in H to integer levels 126 | ''' 127 | 128 | D = get_line_graph(H, collapse=collapse) 129 | 130 | levels = {} 131 | 132 | for v in nx.topological_sort(D): 133 | parent_levels = [levels[u] for u, _ in D.in_edges(v)] 134 | levels[v] = max(parent_levels) + 1 if len(parent_levels) else 0 135 | 136 | return levels 137 | -------------------------------------------------------------------------------- /widget/js/README.md: -------------------------------------------------------------------------------- 1 | A widget for interractive visualization of the hypernetx package. 2 | 3 | Package Install 4 | --------------- 5 | 6 | **Prerequisites** 7 | - [node](http://nodejs.org/) 8 | 9 | ```bash 10 | npm install --save hnx-widget 11 | ``` 12 | -------------------------------------------------------------------------------- /widget/js/lib/ReactPlugin.js: -------------------------------------------------------------------------------- 1 | var widgets = require('@jupyter-widgets/base'); 2 | var _ = require('lodash'); 3 | 4 | var React = require('react'); 5 | var ReactDOM = require('react-dom'); 6 | 7 | var components = require('../../../lib'); 8 | 9 | // Custom Model. Custom widgets models must at least provide default values 10 | // for model attributes, including 11 | // 12 | // - `_view_name` 13 | // - `_view_module` 14 | // - `_view_module_version` 15 | // 16 | // - `_model_name` 17 | // - `_model_module` 18 | // - `_model_module_version` 19 | // 20 | // when different from the base class. 21 | 22 | // When serialiazing the entire widget state for embedding, only values that 23 | // differ from the defaults will be specified. 24 | var ReactModel = widgets.DOMWidgetModel.extend({ 25 | defaults: _.extend(widgets.DOMWidgetModel.prototype.defaults(), { 26 | _model_name : 'ReactModel', 27 | _view_name : 'ReactModel', 28 | _model_module : 'ipy-nlpvis', 29 | _view_module : 'ipy-nlpvis', 30 | _model_module_version : '0.1.0', 31 | _view_module_version : '0.1.0', 32 | props : {}, 33 | pos: {} 34 | }) 35 | }); 36 | 37 | 38 | // Custom View. Renders the widget model. 39 | var ReactView = widgets.DOMWidgetView.extend({ 40 | render: function() { 41 | this.props_changed(); 42 | this.model.on('change:props', this.props_changed, this); 43 | }, 44 | 45 | props_changed: function() { 46 | 47 | var name = this.model.get('component') 48 | var view = components[name]; 49 | var props = this.model.get('props'); 50 | 51 | props['_model'] = this.model 52 | 53 | if (view) { 54 | var component = React.createElement(view, props, null); 55 | ReactDOM.render(component, this.el); 56 | } else { 57 | console.error(`Unable to render component. ${name} not found`); 58 | } 59 | } 60 | }); 61 | 62 | 63 | module.exports = { 64 | ReactModel : ReactModel, 65 | ReactView : ReactView 66 | }; 67 | -------------------------------------------------------------------------------- /widget/js/lib/embed.js: -------------------------------------------------------------------------------- 1 | // Entry point for the unpkg bundle containing custom model definitions. 2 | // 3 | // It differs from the notebook bundle in that it does not need to define a 4 | // dynamic baseURL for the static assets and may load some css that would 5 | // already be loaded by the notebook otherwise. 6 | 7 | // Export widget models and views, and the npm package version number. 8 | module.exports = require('./ReactPlugin.js'); 9 | module.exports['version'] = require('../package.json').version; 10 | -------------------------------------------------------------------------------- /widget/js/lib/extension.js: -------------------------------------------------------------------------------- 1 | // This file contains the javascript that is run when the notebook is loaded. 2 | // It contains some requirejs configuration and the `load_ipython_extension` 3 | // which is required for any notebook extension. 4 | // 5 | // Some static assets may be required by the custom widget javascript. The base 6 | // url for the notebook is not known at build time and is therefore computed 7 | // dynamically. 8 | __webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/hnx-widget'; 9 | 10 | 11 | // Configure requirejs 12 | if (window.require) { 13 | window.require.config({ 14 | map: { 15 | "*" : { 16 | "hnx-widget": "nbextensions/hnx-widget/index", 17 | } 18 | } 19 | }); 20 | } 21 | 22 | // Export the required load_ipython_extension 23 | module.exports = { 24 | load_ipython_extension: function() {} 25 | }; 26 | -------------------------------------------------------------------------------- /widget/js/lib/index.js: -------------------------------------------------------------------------------- 1 | // Export widget models and views, and the npm package version number. 2 | module.exports = require('./ReactPlugin.js'); 3 | module.exports['version'] = require('../package.json').version; 4 | -------------------------------------------------------------------------------- /widget/js/lib/labplugin.js: -------------------------------------------------------------------------------- 1 | var plugin = require('./index'); 2 | var base = require('@jupyter-widgets/base'); 3 | 4 | module.exports = { 5 | id: 'hnx-widget', 6 | requires: [base.IJupyterWidgetRegistry], 7 | activate: function(app, widgets) { 8 | widgets.registerWidget({ 9 | name: 'hnx-widget', 10 | version: plugin.version, 11 | exports: plugin 12 | }); 13 | }, 14 | autoStart: true 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /widget/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hnx-widget", 3 | "version": "0.1.0", 4 | "description": "A widget for interactive visualization of the hypernetx package.", 5 | "author": "Dustin Arendt", 6 | "main": "lib/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/PNNL/hnx-widget.git" 10 | }, 11 | "keywords": [ 12 | "jupyter", 13 | "widgets", 14 | "ipython", 15 | "ipywidgets", 16 | "jupyterlab-extension" 17 | ], 18 | "files": [ 19 | "lib/**/*.js", 20 | "dist/*.js" 21 | ], 22 | "scripts": { 23 | "clean": "rimraf dist/", 24 | "prepublish": "webpack", 25 | "build": "webpack", 26 | "watch": "webpack --watch -d", 27 | "test": "echo \"Error: no test specified\" && exit 1" 28 | }, 29 | "devDependencies": { 30 | "rimraf": "^2.6.1", 31 | "webpack": "^3.5.5" 32 | }, 33 | "dependencies": { 34 | "@jupyter-widgets/base": "^1.1 || ^2 || ^3", 35 | "lodash": "^4.17.4" 36 | }, 37 | "jupyterlab": { 38 | "extension": "lib/labplugin" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /widget/js/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var version = require('./package.json').version; 3 | 4 | // Custom webpack rules are generally the same for all webpack bundles, hence 5 | // stored in a separate local variable. 6 | var rules = [ 7 | { test: /\.css$/, use: ['style-loader', 'css-loader']} 8 | ] 9 | 10 | 11 | module.exports = [ 12 | {// Notebook extension 13 | // 14 | // This bundle only contains the part of the JavaScript that is run on 15 | // load of the notebook. This section generally only performs 16 | // some configuration for requirejs, and provides the legacy 17 | // "load_ipython_extension" function which is required for any notebook 18 | // extension. 19 | // 20 | entry: './lib/extension.js', 21 | output: { 22 | filename: 'extension.js', 23 | path: path.resolve(__dirname, '..', 'hnxwidget', 'static'), 24 | libraryTarget: 'amd' 25 | } 26 | }, 27 | {// Bundle for the notebook containing the custom widget views and models 28 | // 29 | // This bundle contains the implementation for the custom widget views and 30 | // custom widget. 31 | // It must be an amd module 32 | // 33 | entry: './lib/index.js', 34 | output: { 35 | filename: 'index.js', 36 | path: path.resolve(__dirname, '..', 'hnxwidget', 'static'), 37 | libraryTarget: 'amd' 38 | }, 39 | devtool: 'source-map', 40 | module: { 41 | rules: rules 42 | }, 43 | externals: ['@jupyter-widgets/base'] 44 | }, 45 | {// Embeddable hnx-widget bundle 46 | // 47 | // This bundle is generally almost identical to the notebook bundle 48 | // containing the custom widget views and models. 49 | // 50 | // The only difference is in the configuration of the webpack public path 51 | // for the static assets. 52 | // 53 | // It will be automatically distributed by unpkg to work with the static 54 | // widget embedder. 55 | // 56 | // The target bundle is always `dist/index.js`, which is the path required 57 | // by the custom widget embedder. 58 | // 59 | entry: './lib/embed.js', 60 | output: { 61 | filename: 'index.js', 62 | path: path.resolve(__dirname, 'dist'), 63 | libraryTarget: 'amd', 64 | publicPath: 'https://unpkg.com/hnx-widget@' + version + '/dist/' 65 | }, 66 | devtool: 'source-map', 67 | module: { 68 | rules: rules 69 | }, 70 | externals: ['@jupyter-widgets/base'] 71 | } 72 | ]; 73 | -------------------------------------------------------------------------------- /widget/setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /widget/setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from setuptools import setup, find_packages, Command 3 | from setuptools.command.sdist import sdist 4 | from setuptools.command.build_py import build_py 5 | from setuptools.command.egg_info import egg_info 6 | from subprocess import check_call 7 | import os 8 | import sys 9 | import platform 10 | from distutils import log 11 | 12 | here = os.path.dirname(os.path.abspath(__file__)) 13 | node_root = os.path.join(here, 'js') 14 | is_repo = os.path.exists(os.path.join(here, '.git')) 15 | 16 | npm_path = os.pathsep.join([ 17 | os.path.join(node_root, 'node_modules', '.bin'), 18 | os.environ.get('PATH', os.defpath), 19 | ]) 20 | 21 | log.set_verbosity(log.DEBUG) 22 | log.info('setup.py entered') 23 | log.info('$PATH=%s' % os.environ['PATH']) 24 | 25 | name = 'hnxwidget' 26 | LONG_DESCRIPTION = 'A widget for interactive visualization of the hypernetx package.' 27 | 28 | 29 | def js_prerelease(command, strict=False): 30 | """Decorator for building minified js/css prior to another command.""" 31 | class DecoratedCommand(command): 32 | def run(self): 33 | jsdeps = self.distribution.get_command_obj('jsdeps') 34 | if not is_repo and all(os.path.exists(t) for t in jsdeps.targets): 35 | # sdist, nothing to do 36 | command.run(self) 37 | return 38 | 39 | try: 40 | self.distribution.run_command('jsdeps') 41 | except Exception as e: 42 | missing = [t for t in jsdeps.targets if not os.path.exists(t)] 43 | if strict or missing: 44 | log.warn('rebuilding js and css failed') 45 | if missing: 46 | log.error('missing files: %s' % missing) 47 | raise e 48 | else: 49 | log.warn('rebuilding js and css failed (not a problem)') 50 | log.warn(str(e)) 51 | command.run(self) 52 | update_package_data(self.distribution) 53 | return DecoratedCommand 54 | 55 | 56 | def update_package_data(distribution): 57 | """Update package_data to catch changes during setup.""" 58 | build_py = distribution.get_command_obj('build_py') 59 | # distribution.package_data = find_package_data() 60 | # re-init build_py options which load package_data 61 | build_py.finalize_options() 62 | 63 | 64 | class NPM(Command): 65 | description = 'install package.json dependencies using npm' 66 | 67 | user_options = [] 68 | 69 | node_modules = os.path.join(node_root, 'node_modules') 70 | 71 | targets = [ 72 | os.path.join(here, 'hnxwidget', 'static', 'extension.js'), 73 | os.path.join(here, 'hnxwidget', 'static', 'index.js') 74 | ] 75 | 76 | def initialize_options(self): 77 | pass 78 | 79 | def finalize_options(self): 80 | pass 81 | 82 | def get_npm_name(self): 83 | npm_name = 'npm'; 84 | if platform.system() == 'Windows': 85 | npm_name = 'npm.cmd'; 86 | 87 | return npm_name; 88 | 89 | def has_npm(self): 90 | npm_name = self.get_npm_name(); 91 | try: 92 | check_call([npm_name, '--version']) 93 | return True 94 | except: 95 | return False 96 | 97 | def should_run_npm_install(self): 98 | return self.has_npm() 99 | 100 | def run(self): 101 | has_npm = self.has_npm() 102 | if not has_npm: 103 | log.error("`npm` unavailable. If you're running this command using sudo, make sure `npm` is available to sudo") 104 | 105 | env = os.environ.copy() 106 | env['PATH'] = npm_path 107 | 108 | if self.should_run_npm_install(): 109 | log.info("Installing build dependencies with npm. This may take a while...") 110 | npm_name = self.get_npm_name(); 111 | check_call([npm_name, 'install'], cwd=node_root, stdout=sys.stdout, stderr=sys.stderr) 112 | os.utime(self.node_modules, None) 113 | 114 | for t in self.targets: 115 | if not os.path.exists(t): 116 | msg = 'Missing file: %s' % t 117 | if not has_npm: 118 | msg += '\nnpm is required to build a development version of a widget extension' 119 | raise ValueError(msg) 120 | 121 | # update package data in case this created new files 122 | update_package_data(self.distribution) 123 | 124 | version_ns = {} 125 | with open(os.path.join(here, 'hnxwidget', '_version.py')) as f: 126 | exec(f.read(), {}, version_ns) 127 | 128 | setup_args = dict( 129 | name=name, 130 | version=version_ns['__version__'], 131 | description='A widget for interractive visualization of the hypernetx package.', 132 | long_description=LONG_DESCRIPTION, 133 | include_package_data=True, 134 | data_files=[ 135 | ('share/jupyter/nbextensions/hnx-widget', [ 136 | 'hnxwidget/static/extension.js', 137 | 'hnxwidget/static/index.js', 138 | 'hnxwidget/static/index.js.map', 139 | ],), 140 | ('etc/jupyter/nbconfig/notebook.d' ,['hnx-widget.json']) 141 | ], 142 | install_requires=[ 143 | 'ipywidgets>=7.0.0', 144 | ], 145 | packages=find_packages(), 146 | zip_safe=False, 147 | cmdclass={ 148 | 'build_py': js_prerelease(build_py), 149 | 'egg_info': js_prerelease(egg_info), 150 | 'sdist': js_prerelease(sdist, strict=True), 151 | 'jsdeps': NPM, 152 | }, 153 | author='Dustin Arendt', 154 | author_email='dustin.arendt@pnnl.gov', 155 | url='https://github.com/pnnl/hypernetx-widget', 156 | keywords=[ 157 | 'ipython', 158 | 'jupyter', 159 | 'widgets', 160 | ], 161 | classifiers=[ 162 | 'Development Status :: 4 - Beta', 163 | 'Framework :: IPython', 164 | 'Intended Audience :: Developers', 165 | 'Intended Audience :: Science/Research', 166 | 'Topic :: Multimedia :: Graphics', 167 | 'Programming Language :: Python :: 2', 168 | 'Programming Language :: Python :: 2.7', 169 | 'Programming Language :: Python :: 3', 170 | 'Programming Language :: Python :: 3.3', 171 | 'Programming Language :: Python :: 3.4', 172 | 'Programming Language :: Python :: 3.5', 173 | 'Programming Language :: Python :: 3.6', 174 | 'Programming Language :: Python :: 3.7', 175 | ], 176 | ) 177 | 178 | setup(**setup_args) 179 | --------------------------------------------------------------------------------