├── .gitignore ├── example-images ├── example-arp.png ├── example-box.png ├── example-minimal.png ├── example-topology.png └── example-bgp-state-machine.png ├── example-json ├── minimal.json ├── link-flip.json ├── bgp-state-machine.json ├── arp.json ├── data-core.json ├── data-lag-example.json └── data.json ├── LICENSE ├── index-full-screen-example.html ├── example-scripts ├── ruter.php └── juniper-snmp-arp.py ├── dataset-format-example.js ├── index.html ├── README.md └── topoflow.js /.gitignore: -------------------------------------------------------------------------------- 1 | favicon.ico -------------------------------------------------------------------------------- /example-images/example-arp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldev/peeeew-topoflow/HEAD/example-images/example-arp.png -------------------------------------------------------------------------------- /example-images/example-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldev/peeeew-topoflow/HEAD/example-images/example-box.png -------------------------------------------------------------------------------- /example-images/example-minimal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldev/peeeew-topoflow/HEAD/example-images/example-minimal.png -------------------------------------------------------------------------------- /example-images/example-topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldev/peeeew-topoflow/HEAD/example-images/example-topology.png -------------------------------------------------------------------------------- /example-images/example-bgp-state-machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldev/peeeew-topoflow/HEAD/example-images/example-bgp-state-machine.png -------------------------------------------------------------------------------- /example-json/minimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "node 1": { 4 | "x": 300, 5 | "y": 200 6 | }, 7 | "node 2": { 8 | "x": 800, 9 | "y": 100 10 | }, 11 | "node 3": { 12 | "x": 1000, 13 | "y": 200 14 | } 15 | }, 16 | "links": [ 17 | { 18 | "from": "node 1", 19 | "to": "node 2", 20 | "rate_out": "Some megabyters", 21 | "rate_in": "Even more gigabyters" 22 | }, 23 | { 24 | "from": "node 2", 25 | "to": "node 3", 26 | "rate": "12.3 G", 27 | "type": "1way" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /example-json/link-flip.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "text": { 4 | "link_prevent_upside_down": true 5 | } 6 | }, 7 | "nodes": { 8 | "node 1": { 9 | "x": 100, 10 | "y": 100 11 | }, 12 | "node 2": { 13 | "x": 900, 14 | "y": 100 15 | }, 16 | "node 3": { 17 | "x": 600, 18 | "y": 600 19 | }, 20 | "node 4": { 21 | "x": 1400, 22 | "y": 600 23 | } 24 | }, 25 | "links": [ 26 | { 27 | "from": "node 2", 28 | "to": "node 1", 29 | "rate_in": "test" 30 | }, 31 | { 32 | "from": "node 2", 33 | "to": "node 3", 34 | "rate_in": "test4" 35 | }, 36 | { 37 | "from": "node 3", 38 | "to": "node 1", 39 | "type": "1way", 40 | "rate": "test2" 41 | }, 42 | { 43 | "from": "node 4", 44 | "to": "node 2", 45 | "rate_in": "test3" 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /index-full-screen-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Topoflow example code 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /example-scripts/ruter.php: -------------------------------------------------------------------------------- 1 | options->text->node_position = "center"; 14 | $GLOBALS["obj_json"]->options->node_radius = 20; 15 | $GLOBALS["obj_json"]->options->link->width = 3; 16 | $GLOBALS["obj_json"]->options->link->spacing = 10; 17 | 18 | $url = "https://api.entur.io/realtime/v1/rest/vm?datasetId=RUT"; 19 | $xml = simplexml_load_file($url); 20 | $time = time(); 21 | 22 | foreach($xml->ServiceDelivery->VehicleMonitoringDelivery->VehicleActivity as $bus){ 23 | 24 | if(!in_array($bus->MonitoredVehicleJourney->OriginName,$stops)){ 25 | array_push($stops,(string) $bus->MonitoredVehicleJourney->OriginName); 26 | } 27 | if(!in_array($bus->MonitoredVehicleJourney->DestinationName,$stops)){ 28 | array_push($stops,(string) $bus->MonitoredVehicleJourney->DestinationName); 29 | } 30 | 31 | $link= new StdClass; 32 | 33 | $link->to = (string) $bus->MonitoredVehicleJourney->DestinationName; 34 | $link->from = (string) $bus->MonitoredVehicleJourney->OriginName; 35 | 36 | $link->type = "1way"; 37 | $link->rate = (string) $bus->MonitoredVehicleJourney->MonitoredCall->StopPointName; 38 | $link->load = (float) $bus->ProgressBetweenStops->Percentage; 39 | //$link->load_out = 100 -(string) $bus->ProgressBetweenStops->Percentage; 40 | 41 | $GLOBALS["obj_json"]->links[] = $link; 42 | } 43 | 44 | shuffle($stops); 45 | 46 | foreach($stops as $stop){ 47 | $i++; 48 | $GLOBALS["obj_json"]->nodes->{$stop}->x = $x_center + $radius * cos(2* M_PI * $i / sizeof($stops)); 49 | $GLOBALS["obj_json"]->nodes->{$stop}->y = $y_center + $radius * sin(2* M_PI * $i / sizeof($stops)); 50 | 51 | } 52 | 53 | echo json_encode($obj_json); 54 | ?> -------------------------------------------------------------------------------- /example-json/bgp-state-machine.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "text": { 4 | "node_position": "center", 5 | "link_follow_angle": false 6 | }, 7 | "node_radius": 60, 8 | "colors": { 9 | "circle_fill": "#fff", 10 | "circle_outline": "#000", 11 | "arrow_pointer": "#000", 12 | "svg_background_color": "#fff", 13 | "link": "#000", 14 | "node_text": "#000" 15 | } 16 | }, 17 | "nodes": { 18 | "Establised": { 19 | "x": 300, 20 | "y": 350 21 | }, 22 | "Openconf.": { 23 | "x": 600, 24 | "y": 250 25 | }, 26 | "Opensent": { 27 | "x": 900, 28 | "y": 350 29 | }, 30 | "Active": { 31 | "x": 900, 32 | "y": 650 33 | }, 34 | "Connect": { 35 | "x": 600, 36 | "y": 750 37 | }, 38 | "Idle": { 39 | "x": 300, 40 | "y": 650 41 | }, 42 | "Loop Est.": { 43 | "x": 100, 44 | "y": 150 45 | }, 46 | "Loop Idle": { 47 | "x": 100, 48 | "y": 800 49 | }, 50 | "Loop Conn.": { 51 | "x": 600, 52 | "y": 930 53 | }, 54 | "Loop Act.": { 55 | "x": 1100, 56 | "y": 800 57 | }, 58 | "Loop Ope.": { 59 | "x": 600, 60 | "y": 70 61 | } 62 | }, 63 | "links": [ 64 | {"from": "Connect", "to": "Active"}, 65 | {"from": "Active", "to": "Opensent"}, 66 | {"from": "Establised", "to": "Loop Est."}, 67 | {"from": "Idle", "to": "Loop Idle"}, 68 | {"from": "Connect", "to": "Loop Conn."}, 69 | {"from": "Active", "to": "Loop Act."}, 70 | {"from": "Openconf.", "to": "Loop Ope."}, 71 | {"from": "Establised", "to": "Idle", "type": "1way"}, 72 | {"from": "Idle", "to": "Connect", "type": "1way"}, 73 | {"from": "Connect", "to": "Opensent", "type": "1way"}, 74 | {"from": "Opensent", "to": "Openconf.", "type": "1way"}, 75 | {"from": "Opensent", "to": "Idle", "type": "1way"}, 76 | {"from": "Active", "to": "Idle", "type": "1way"}, 77 | {"from": "Openconf.", "to": "Establised", "type": "1way"}, 78 | {"from": "Openconf.", "to": "Idle", "type": "1way"} 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /example-json/arp.json: -------------------------------------------------------------------------------- 1 | {"nodes": {"194.19.23.129": {"x": 886.3703305156273, "y": 603.5276180410083}, "192.168.10.2": {"x": 846.4101615137755, "y": 700.0}, "10.0.10.2": {"x": 782.842712474619, "y": 782.842712474619}, "10.0.10.7": {"x": 700.0, "y": 846.4101615137754}, "10.0.10.9": {"x": 603.5276180410083, "y": 886.3703305156273}, "10.0.10.12": {"x": 500.0, "y": 900.0}, "10.0.10.13": {"x": 396.4723819589917, "y": 886.3703305156273}, "10.0.10.18": {"x": 300.0000000000001, "y": 846.4101615137755}, "10.0.10.20": {"x": 217.15728752538104, "y": 782.842712474619}, "10.0.10.21": {"x": 153.5898384862245, "y": 700.0}, "10.0.10.24": {"x": 113.6296694843727, "y": 603.5276180410084}, "10.0.10.25": {"x": 100.0, "y": 500.00000000000006}, "10.0.10.26": {"x": 113.6296694843727, "y": 396.47238195899166}, "10.0.10.31": {"x": 153.5898384862245, "y": 300.0000000000001}, "10.0.10.47": {"x": 217.15728752538087, "y": 217.15728752538115}, "10.0.10.51": {"x": 299.99999999999983, "y": 153.58983848622466}, "10.0.10.60": {"x": 396.4723819589917, "y": 113.6296694843727}, "10.0.10.64": {"x": 499.99999999999994, "y": 100.0}, "10.0.10.99": {"x": 603.527618041008, "y": 113.62966948437264}, "10.0.10.101": {"x": 700.0, "y": 153.58983848622455}, "10.0.10.102": {"x": 782.842712474619, "y": 217.15728752538092}, "10.0.10.133": {"x": 846.4101615137754, "y": 299.99999999999983}, "10.0.10.199": {"x": 886.3703305156273, "y": 396.4723819589914}, "10.0.10.200": {"x": 900.0, "y": 499.9999999999999}, "gw": {"x": 500, "y": 500}}, "links": [{"from": "gw", "to": "194.19.23.129"}, {"from": "gw", "to": "192.168.10.2"}, {"from": "gw", "to": "10.0.10.2"}, {"from": "gw", "to": "10.0.10.7"}, {"from": "gw", "to": "10.0.10.9"}, {"from": "gw", "to": "10.0.10.12"}, {"from": "gw", "to": "10.0.10.13"}, {"from": "gw", "to": "10.0.10.18"}, {"from": "gw", "to": "10.0.10.20"}, {"from": "gw", "to": "10.0.10.21"}, {"from": "gw", "to": "10.0.10.24"}, {"from": "gw", "to": "10.0.10.25"}, {"from": "gw", "to": "10.0.10.26"}, {"from": "gw", "to": "10.0.10.31"}, {"from": "gw", "to": "10.0.10.47"}, {"from": "gw", "to": "10.0.10.51"}, {"from": "gw", "to": "10.0.10.60"}, {"from": "gw", "to": "10.0.10.64"}, {"from": "gw", "to": "10.0.10.99"}, {"from": "gw", "to": "10.0.10.101"}, {"from": "gw", "to": "10.0.10.102"}, {"from": "gw", "to": "10.0.10.133"}, {"from": "gw", "to": "10.0.10.199"}, {"from": "gw", "to": "10.0.10.200"}], "options": {"node_radius": 45, "text": {"node_position": "center"}, "link": {"width": 3}}} -------------------------------------------------------------------------------- /example-scripts/juniper-snmp-arp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Python3 code. Requires "easysnmp" and "json" python3 modules 4 | This code is to generate a JSON object that can be rendered in Topoflow 5 | It collects the ARP table from a Juniper router and generates the corresponding JSON 6 | ''' 7 | 8 | from easysnmp import Session 9 | import math 10 | import json 11 | 12 | # the object that will be converted to JSON 13 | dataset = { 14 | 'nodes': {}, 15 | 'links': [], 16 | 'options' = { 17 | 'node_radius': 45, 18 | 'text': { 19 | 'node_position': 'center' 20 | }, 21 | 'link': { 22 | 'width': 3 23 | } 24 | } 25 | } 26 | 27 | # Gets the ARP table via SNMP 28 | host = 'gw' 29 | session = Session(hostname=host, community='xxxxx', version=2) 30 | mib = 'iso.3.6.1.2.1.4.35.1.4' 31 | arp = session.walk(mib) 32 | 33 | 34 | 35 | # list to hold all IP addresses collected 36 | l = [] 37 | 38 | # Iterate over all ARP items 39 | for item in arp: 40 | # We need to get the SNMP ifindex (part of the SNMP key) to check what kind of ARP entry it is. E.g. if it's itself, or an actual IP neighbor 41 | x = item.oid.replace(mib + '.', '') 42 | parts = x.split('.') 43 | # Validate that the key has the correct length. This filters out some "garbage" for us 44 | if len(parts) == 7: 45 | # holds the ifIndex. We will use this to run separate SNMP get's 46 | ifindex = parts[0] 47 | 48 | # ip address in ARP table 49 | ip = '%s.%s.%s.%s' % (parts[3], parts[4], parts[5], parts[6]) 50 | 51 | # Get the ipNetToPhysicalType value. "3" = actual known neighbor, not itself or some internal addresses related to internal interfaces 52 | y = '%s.%s.%s.%s' % ('iso.3.6.1.2.1.4.35.1.6', ifindex, '1.4', ip) 53 | if session.get(y).value == "3": 54 | l.append(ip) 55 | 56 | # variables used to calculate the "circle" the nodes should form 57 | iterator = 0 58 | radius = 400 59 | x_center = 500 60 | y_center = 500 61 | num = len(l) 62 | for ip in l: 63 | iterator += 1 64 | x_pos = x_center + radius * math.cos(2 * math.pi * iterator / num) 65 | y_pos = y_center + radius * math.sin(2 * math.pi * iterator / num) 66 | dataset['nodes'][ip] = { 67 | 'x': x_pos, 68 | 'y': y_pos 69 | } 70 | 71 | # add links 72 | dataset['links'].append({ 73 | 'from': host, 74 | 'to': ip 75 | }) 76 | 77 | # Add a center node to represent the current node 78 | dataset['nodes'][host] = { 79 | 'x': x_center, 80 | 'y': y_center 81 | } 82 | 83 | print(json.dumps(dataset)) -------------------------------------------------------------------------------- /dataset-format-example.js: -------------------------------------------------------------------------------- 1 | class topoflow{ 2 | constructor(){ 3 | /* 4 | This is just the example syntax, and this variable will not be used anywhere 5 | */ 6 | this.main_dataset_suggested_format = { 7 | "links": [ 8 | { 9 | "from": "00a-core-1", 10 | "to": "00a-core-2", 11 | "links": [ 12 | { 13 | "type": "1way", 14 | "load_out": "20", 15 | "load_in": "20", 16 | "rate_out": "331 M", 17 | "rate_in": "337 M", 18 | "state": "up" 19 | }, 20 | { 21 | "type": "1way", 22 | "max_out": "20", 23 | "max_in": "20", 24 | "rate_out": "331", 25 | "rate_in": "337 M", 26 | "state": "up" 27 | } 28 | ] 29 | }, 30 | { 31 | "from": "00a-core-1", 32 | "to": "00b-core-1", 33 | "links": [ 34 | { 35 | "type": "2way", 36 | "load_out": "20", 37 | "load_in": "20", 38 | "rate_out": "331 M", 39 | "rate_in": "337 M", 40 | "state": "up" 41 | }, 42 | { 43 | "type": "2way", 44 | "load_out": "20", 45 | "load_in": "20", 46 | "rate_out": "331 M", 47 | "rate_in": "337 M", 48 | "state": "up" 49 | } 50 | ] 51 | }, 52 | { 53 | "from": "00a-core-1", 54 | "to": "00a-core-2", 55 | "links": [ 56 | { 57 | "type": "1way", 58 | "load_out": "20", 59 | "load_in": "20", 60 | "rate_out": "331 M", 61 | "rate_in": "337 M", 62 | "state": "up" 63 | } 64 | ] 65 | }, 66 | ] 67 | }; 68 | } 69 | } -------------------------------------------------------------------------------- /example-json/data-core.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "link": { 4 | "width": 3 5 | }, 6 | "text": { 7 | "node_position": "center" 8 | }, 9 | "colors": { 10 | "svg_background_color": "#000", 11 | "circle_fill": "#000" 12 | } 13 | }, 14 | "links": [ 15 | { 16 | "from": "00a-core-1", 17 | "to": "00b-core-1", 18 | "rate_rx": "1826 Mbit\/s" 19 | }, 20 | { 21 | "from": "00a-core-1", 22 | "to": "00a-core-agg-1", 23 | "rate_rx": "68 Mbit\/s" 24 | }, 25 | { 26 | "from": "00a-core-1", 27 | "to": "00a-core-2", 28 | "rate_rx": "118 Mbit\/s" 29 | }, 30 | { 31 | "from": "00a-core-2", 32 | "to": "00b-core-2", 33 | "rate_rx": "616 Mbit\/s" 34 | }, 35 | { 36 | "from": "00a-core-2", 37 | "to": "00a-core-agg-1", 38 | "rate_rx": "853 Mbit\/s" 39 | }, 40 | { 41 | "from": "00a-core-2", 42 | "to": "00a-core-1", 43 | "rate_rx": "0 Mbit\/s" 44 | }, 45 | { 46 | "from": "00b-core-1", 47 | "to": "00b-core-2", 48 | "rate_rx": "1971 Mbit\/s" 49 | }, 50 | { 51 | "from": "00b-core-1", 52 | "to": "00b-core-agg-1", 53 | "rate_rx": "1602 Mbit\/s" 54 | }, 55 | { 56 | "from": "00b-core-1", 57 | "to": "00b-core-2", 58 | "rate_rx": "0 Mbit\/s" 59 | }, 60 | { 61 | "from": "00b-core-2", 62 | "to": "00b-core-agg-1", 63 | "rate_rx": "393 Mbit\/s" 64 | }, 65 | { 66 | "from": "00b-core-2", 67 | "to": "00a-core-2", 68 | "rate_rx": "1248 Mbit\/s" 69 | }, 70 | { 71 | "from": "00b-core-2", 72 | "to": "00b-core-1", 73 | "rate_rx": "0 Mbit\/s" 74 | } 75 | ], 76 | "nodes": { 77 | "00a-core-1": { 78 | "x": 500, 79 | "y": 100 80 | }, 81 | "00a-core-2": { 82 | "x": 500, 83 | "y": 400 84 | }, 85 | "00b-core-1": { 86 | "x": 900, 87 | "y": 100 88 | }, 89 | "00b-core-2": { 90 | "x": 900, 91 | "y": 400 92 | }, 93 | "00a-core-agg-1": { 94 | "x": 100, 95 | "y": 400 96 | }, 97 | "00b-core-agg-1": { 98 | "x": 1300, 99 | "y": 400 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /example-json/data-lag-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "link": { 4 | "offset": 20, 5 | "width": 10 6 | }, 7 | "svg_width": "100%", 8 | "svg_height": "100%" 9 | }, 10 | "links": [ 11 | { 12 | "from": "00a-core-1", 13 | "to": "00b-core-1", 14 | "rate_out": "1", 15 | "rate_in": "10" 16 | }, 17 | { 18 | "from": "00a-core-1", 19 | "to": "00b-core-1", 20 | "rate_out": "1", 21 | "rate_in": "10" 22 | }, 23 | { 24 | "from": "00a-core-1", 25 | "to": "00b-core-1", 26 | "rate_out": "1", 27 | "rate_in": "10" 28 | }, 29 | { 30 | "from": "00a-core-1", 31 | "to": "00a-core-2", 32 | "rate_out": "2", 33 | "rate_in": "11" 34 | }, 35 | { 36 | "from": "00a-core-2", 37 | "to": "00b-core-2", 38 | "rate_out": "3", 39 | "rate_in": "12" 40 | }, 41 | { 42 | "from": "00a-core-2", 43 | "to": "00a-core-1", 44 | "rate_out": "4", 45 | "rate_in": "13" 46 | }, 47 | { 48 | "from": "00b-core-1", 49 | "to": "00b-core-2", 50 | "rate_out": "5", 51 | "rate_in": "14" 52 | }, 53 | { 54 | "from": "00b-core-1", 55 | "to": "00b-core-2", 56 | "rate_out": "6", 57 | "rate_in": "15" 58 | }, 59 | { 60 | "from": "00b-core-1", 61 | "to": "00b-core-2", 62 | "rate_out": "7", 63 | "rate_in": "16" 64 | }, 65 | { 66 | "from": "00b-core-1", 67 | "to": "00b-core-2", 68 | "rate_out": "8", 69 | "rate_in": "17" 70 | }, 71 | { 72 | "from": "00b-core-2", 73 | "to": "00b-core-3", 74 | "rate": "1333333337", 75 | "type": "1way" 76 | }, 77 | { 78 | "from": "00b-core-2", 79 | "to": "00b-core-3", 80 | "rate": "1333333337", 81 | "type": "1way" 82 | } 83 | ], 84 | "nodes": { 85 | "00a-core-1": { 86 | "x": 100, 87 | "y": 100, 88 | "text_position" : "top" 89 | }, 90 | "00a-core-2": { 91 | "x": 500, 92 | "y": 400 93 | }, 94 | "00b-core-1": { 95 | "x": 900, 96 | "y": 100, 97 | "text_position" : "top" 98 | }, 99 | "00b-core-2": { 100 | "x": 900, 101 | "y": 400 102 | }, 103 | "00b-core-3": { 104 | "x": 500, 105 | "y": 700 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Topoflow example code 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 60 | 61 | 62 | 63 | 64 | 65 |
66 | Select JSON: data.json 67 | data-core.json 68 | bgp-state-machine.json 69 | minimal.json 70 | link-flip.json 71 | data-lag-example.json 72 | arp.json 73 | Entur fullscreen page (only works if server can process PHP...) 74 |
75 | 76 | 77 | 78 | 79 | 80 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /example-json/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "text": { 4 | "link_follow_angle": true, 5 | "link_prevent_upside_down": true, 6 | "node_position": "center" 7 | }, 8 | "node_radius": 70, 9 | "svg_height": 1200, 10 | "svg_width": 1600 11 | }, 12 | "boxes": [ 13 | { 14 | "corner_a_x": 50, 15 | "corner_a_y": 600, 16 | "corner_b_x": 1450, 17 | "corner_b_y": 1000, 18 | "background_color": "#333", 19 | "border_color": "#f00", 20 | "border_width": 5, 21 | "opacity": 50, 22 | "text_placement": "top", 23 | "text": "Inet" 24 | }, 25 | { 26 | "corner_a_x": 50, 27 | "corner_a_y": 10, 28 | "corner_b_x": 1450, 29 | "corner_b_y": 500 30 | } 31 | ], 32 | "nodes": { 33 | "00a-core-1": { 34 | "x": 550, 35 | "y": 100, 36 | "type": "router" 37 | }, 38 | "00a-core-2": { 39 | "x": 550, 40 | "y": 400, 41 | "type": "router" 42 | }, 43 | "00b-core-1": { 44 | "x": 950, 45 | "y": 100, 46 | "type": "router" 47 | }, 48 | "00b-core-2": { 49 | "x": 950, 50 | "y": 400, 51 | "type": "router" 52 | }, 53 | 54 | "00a-core-agg-1": { 55 | "x": 150, 56 | "y": 400 57 | }, 58 | "00b-core-agg-1": { 59 | "x": 1350, 60 | "y": 400 61 | }, 62 | 63 | "00a-fw-inet-1": { 64 | "x": 150, 65 | "y": 700 66 | }, 67 | "00b-fw-inet-1": { 68 | "x": 1350, 69 | "y": 700 70 | }, 71 | 72 | "00a-gw-inet-1": { 73 | "x": 500, 74 | "y": 700, 75 | "state": "down" 76 | }, 77 | "00b-gw-inet-1": { 78 | "x": 1000, 79 | "y": 700 80 | }, 81 | 82 | "internjet": { 83 | "x": 750, 84 | "y": 900, 85 | "type": "cloud" 86 | } 87 | }, 88 | "links": [ 89 | { 90 | "from": "00a-core-1", 91 | "to": "00a-core-2", 92 | "max_out": "20", 93 | "max_in": "20", 94 | "rate_out": "331 M", 95 | "rate_in": "337 M", 96 | "state": "up" 97 | }, 98 | { 99 | "from": "00a-core-1", 100 | "to": "00b-core-1", 101 | "max_out": "20", 102 | "max_in": "20", 103 | "rate_out": "12 M", 104 | "rate_in": "12 Mbit", 105 | "state": "up" 106 | }, 107 | { 108 | "from": "00b-core-1", 109 | "to": "00b-core-2", 110 | "max_out": "20", 111 | "max_in": "20", 112 | "rate_out": "1 Gbit", 113 | "rate_in": "111 Mbit", 114 | "state": "down" 115 | }, 116 | { 117 | "from": "00a-core-2", 118 | "to": "00b-core-2", 119 | "max_out": "20", 120 | "max_in": "20", 121 | "rate_out": "331 Mbit", 122 | "rate_in": "51 Mbit", 123 | "state": "up" 124 | }, 125 | { 126 | "from": "00a-core-1", 127 | "to": "00a-core-agg-1", 128 | "max_out": "20", 129 | "max_in": "20", 130 | "rate_out": "1331 Mbit", 131 | "rate_in": "4331 Mbit", 132 | "state": "up" 133 | }, 134 | { 135 | "from": "00a-core-2", 136 | "to": "00a-core-agg-1", 137 | "max_out": "20", 138 | "max_in": "20", 139 | "rate_out": "0", 140 | "rate_in": "0", 141 | "state": "up" 142 | }, 143 | { 144 | "from": "00b-core-1", 145 | "to": "00b-core-agg-1", 146 | "max_out": "20", 147 | "max_in": "20", 148 | "rate_out": "55Mbit", 149 | "rate_in": "151Mbit", 150 | "state": "up" 151 | }, 152 | { 153 | "from": "00b-core-2", 154 | "to": "00b-core-agg-1", 155 | "max_out": "20", 156 | "max_in": "20", 157 | "rate_out": "5578Mbit", 158 | "rate_in": "33M", 159 | "state": "up" 160 | }, 161 | { 162 | "from": "00a-core-agg-1", 163 | "to": "00a-fw-inet-1", 164 | "max_out": "20", 165 | "max_in": "20", 166 | "rate_out": "0", 167 | "rate_in": "0", 168 | "state": "up" 169 | }, 170 | { 171 | "from": "00b-core-agg-1", 172 | "to": "00b-fw-inet-1", 173 | "max_out": "20", 174 | "max_in": "20", 175 | "rate_out": "331Mbit", 176 | "rate_in": "331Mbit", 177 | "state": "up" 178 | }, 179 | { 180 | "from": "00a-fw-inet-1", 181 | "to": "00a-gw-inet-1", 182 | "max_out": "20", 183 | "max_in": "20", 184 | "rate_out": "0", 185 | "rate_in": "0", 186 | "state": "down" 187 | }, 188 | { 189 | "from": "00a-fw-inet-1", 190 | "to": "00a-gw-inet-1", 191 | "max_out": "20", 192 | "max_in": "20", 193 | "rate_out": "0", 194 | "rate_in": "0", 195 | "state": "up" 196 | }, 197 | { 198 | "from": "00b-fw-inet-1", 199 | "to": "00b-gw-inet-1", 200 | "max_out": "20", 201 | "max_in": "20", 202 | "rate_out": "331Mbit", 203 | "rate_in": "331Mbit", 204 | "state": "up" 205 | }, 206 | { 207 | "from": "00a-gw-inet-1", 208 | "to": "internjet", 209 | "max_out": "20", 210 | "max_in": "20", 211 | "rate_out": "0", 212 | "rate_in": "0", 213 | "state": "down" 214 | }, 215 | { 216 | "from": "00b-gw-inet-1", 217 | "to": "internjet", 218 | "max_out": "20", 219 | "max_in": "20", 220 | "rate_out": "331Mbit", 221 | "rate_in": "331Mbit", 222 | "state": "up" 223 | }, 224 | { 225 | "from": "00a-core-2", 226 | "to": "internjet", 227 | "rate": "1333333337", 228 | "type": "1way" 229 | } 230 | ] 231 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # peeeew-topoflow 2 | Topology flowchart ("network weathermap"), with live values, drawn by JavaScript and SVG with your web browser. Relies on JSON files to feed topology/data. 3 | 4 | **Disclaimer: This is not production ready in any way, we're still adding basic stuff and basicly making it work as intended.** 5 | 6 | ## Dependancies 7 | PS: No need to install anything :-) 8 | * Relies on D3 javascript library - https://d3js.org/. This is being loaded from the d3js CDN. 9 | * Will soon utilize the excellent graphics from the ecceman's affinity repo for network graphics - https://github.com/ecceman/affinity. The vector images we want to use will be included in the javascript directly (drawn by D3 into the SVG object). 10 | * Relies (... barely) on jQuery. This is being loaded from the jQuery CDN. Dependancy will probably be removed later on. 11 | 12 | 13 | # Why? 14 | * Ẁhen you need something to quick draw a topology flow map. 15 | * When you need something lighter than php-weathermap for network weathermaps. 16 | * When you do not want to run anything serverside. 17 | * When you need something to update constantly, like once a second. 18 | 19 | 20 | # Sample images 21 | ![BGP state machine as example](/example-images/example-bgp-state-machine.png) 22 | 23 | ![Sample network topology](/example-images/example-topology.png) 24 | 25 | ![ARP map](/example-images/example-arp.png) 26 | 27 | 28 | # Minimal sample JSON file 29 | Located at example-json/minimal.json 30 | ```json 31 | { 32 | "nodes": { 33 | "node 1": { 34 | "x": 300, 35 | "y": 500 36 | }, 37 | "node 2": { 38 | "x": 800, 39 | "y": 500 40 | }, 41 | "node 3": { 42 | "x": 1300, 43 | "y": 500 44 | } 45 | }, 46 | "links": [ 47 | { 48 | "from": "node 1", 49 | "to": "node 2", 50 | "rate_out": "Some megabyters", 51 | "rate_in": "Even more gigabyters" 52 | }, 53 | { 54 | "from": "node 2", 55 | "to": "node 3", 56 | "rate": "12.3 G", 57 | "type": "1way" 58 | } 59 | ] 60 | } 61 | ``` 62 | 63 | 64 | # Minimal HTML example 65 | ```html 66 | 67 | 68 | 69 | 70 | 71 | 75 | 76 | 77 | 78 | 79 | 88 | 89 | 90 | ``` 91 | 92 | 93 | # Sample of minimal drawing 94 | ![Minimal network topology](/example-images/example-minimal.png) 95 | 96 | 97 | # Script library 98 | We will share some scripts here to generate JSON data files for topoflow. Feel free to modify them to your own liking. Most of them will probably be written in Python3. 99 | 100 | 101 | # Todo (somewhat in prioritized order) 102 | 1. Optimization - only define things once 103 | 2. Making the terms less network specific, as topoflow could be used to other things than network topologies 104 | * Perhaps rename "rate" to "text" on the links? Since it can be with any text. 105 | 3. Figure out how to use the Ecceman stencils for symbols, and include some of them in Topoflow. 106 | 4. Remove the jQuery requirement 107 | 108 | # Format/options 109 | Here are all the available things to set and toggle in json file topoflow loads. 110 | 111 | ## links 112 | ### Example 113 | ``` 114 | { 115 | "links": [ 116 | { 117 | "from": "00a-core-1", 118 | "to": "00b-core-1", 119 | "rate_out": "1", 120 | "rate_in": "10" 121 | }, 122 | { 123 | ... 124 | } 125 | ] 126 | } 127 | ``` 128 | 129 | ### Definitions 130 | * `type` 131 | * Defines if a link is unidirectional or bidirectional 132 | * 1way 133 | * 2way (default) 134 | * `state` 135 | * Defines if the link is up or down 136 | * up (default) 137 | * down 138 | * `from` (required) 139 | * Name of the "from" node 140 | * `to` (required) 141 | * Name of the "to" node 142 | * `rate` (only applicable to type: 1way) 143 | * Defines the current rate of the link. Shown as overlay on the drawn link. Free text. 144 | * `rate_in` (only applicable to type: 2way) 145 | * Defines the current inbound rate of the link. Shown as overlay on the drawn link. Free text. 146 | * `rate_out` (only applicable to type: 2way) 147 | * Defines the current outbound rate of the link. Shown as overlay on the drawn link. Free text. 148 | * `load` (only applicable to type: 1way) 149 | * Defines the current load on the unidirectional link. 150 | * Only accepts int from 0 to 100. 151 | * Colors are customizable through `options -> colors -> load_colors` [] 152 | * `load_in` (only applicable to type: 2way) 153 | * Defines the current inbound load on the bidirectional link. 154 | * Only accepts int from 0 to 100. 155 | * Colors are customizable through `options -> colors -> load_colors` [] 156 | * `load_out` (only applicable to type: 2way) 157 | * Defines the current outbound load on the bidirectional link. 158 | * Only accepts int from 0 to 100. 159 | * Colors are customizable through `options -> colors -> load_colors` [] 160 | 161 | 162 | 163 | ## nodes 164 | ### Example 165 | ``` 166 | { 167 | "nodes": { 168 | "node1": { 169 | "x": 100, 170 | "y": 100, 171 | "text_position" : "top" 172 | }, 173 | "node2": { 174 | "x": 500, 175 | "y": 400 176 | } 177 | } 178 | } 179 | ``` 180 | 181 | ### Definitions 182 | * `x` (required) 183 | * The x axis position, in points 184 | * `y` (required) 185 | * The y axis position, in points 186 | * `text_position` 187 | * Where should the text of the node name be placed in relation to the node? 188 | * center 189 | * top 190 | * top-left 191 | * top-right 192 | * bottom (default) 193 | * bottom-left 194 | * bottom-right 195 | * right 196 | * left 197 | 198 | ## boxes 199 | Used to draw boxes in the background. Usefull for grouping of nodes. Boxes will be drawn in the background of everything else. 200 | 201 | NB: "links" cannot be drawn from box to box directly. 202 | 203 | ![Box usage](/example-images/example-box.png) 204 | 205 | ### Example 206 | ``` 207 | { 208 | "boxes": [ 209 | { 210 | "corner_a_x": 50, 211 | "corner_a_y": 600, 212 | "corner_b_x": 1450, 213 | "corner_b_y": 1000, 214 | "background_color": "#333", 215 | "border_color": "#f00", 216 | "border_width": 5, 217 | "opacity": 50, 218 | "text_placement": "top", <---- Not implemented yet 219 | "text": "Inet" <---- Not implemented yet 220 | }, 221 | { 222 | "corner_a_x": 50, 223 | "corner_a_y": 10, 224 | "corner_b_x": 1450, 225 | "corner_b_y": 500 226 | } 227 | ] 228 | } 229 | ``` 230 | 231 | 232 | ## options 233 | Options can, in addition to through the JSON file, be provided through the following javascript calls: 234 | ``` 235 | let tf = new topoflow(); 236 | tf.overwrite_options({ 237 | 'colors': { 238 | 'svg_background_color': '#333', 239 | 'circle_fill': '#333' 240 | }, 241 | 'node_radius': 60 242 | }); 243 | tf.run(json_source); 244 | ``` 245 | 246 | ### Example 247 | In the JSON file 248 | ``` 249 | { 250 | "options": { 251 | "text": { 252 | "node_position": "center", 253 | "link_follow_angle": false 254 | }, 255 | "node_radius": 60, 256 | "colors": { 257 | "circle_fill": "#fff", 258 | "circle_outline": "#000", 259 | "arrow_pointer": "#000", 260 | "svg_background_color": "#fff", 261 | "link": "#000", 262 | "node_text": "#000" 263 | } 264 | } 265 | } 266 | ``` 267 | 268 | ### Definitions 269 | Everything under options are ... optional. 270 | 271 | Possible options to toggle 272 | * `text` 273 | * `node_position` Choose where to draw the text relative to the node 274 | * See nodes -> definitions -> text_position for possible values 275 | * Default: bottom 276 | * `link_follow_angle` Toggle if the text should follow the angle of the link or not. Makes sense if you are using wide links, and want the text to be "inside" the link 277 | * Default: true (boolean) 278 | * `link_prevent_upside_down` Prevents the link text to be shown upside down 279 | * Default: true (boolean) 280 | * `node_offset` The offset from the node center to the text. 281 | * This is multiplied by `node_radius` 282 | * Default: 1.4 283 | * `node_radius` 284 | * node radius in points - e.g. the size of the node 285 | * Default: 40 286 | * `colors` 287 | As we're lazy, here is the code directly from topoflow.js 288 | ``` 289 | 'colors': { 290 | 'circle_fill': '#000', 291 | 'circle_outline': '#fff', 292 | 'circle_outline_down': '#f00', 293 | 'arrow_pointer': '#fff', 294 | 'svg_background_color': '#000', 295 | 'link': '#fff', 296 | 'link_down': '#f00', 297 | 'link_text': '#f0a', 298 | 'node_text': '#fff', 299 | 'load': [ 300 | '#f00', // >0% 301 | '#ff0', // >33% 302 | '#0f0' // > 66% 303 | ] 304 | } 305 | ``` 306 | * `link` 307 | * `width` Width of link 308 | * Default: 20 points 309 | * `spacing` Spacing between muliple links between the same two nodes. Ignored if there is only one link between the two nodes. 310 | * Default: 20 points 311 | * `svg_height` Height of the element 312 | * Can be set either as points (1000) or as percent ("100%") 313 | * Default: 1000 (points/pixels) 314 | * `svg_width` Width of the element 315 | * Can be set either as points (1000) or as percent ("100%") 316 | * Default: 1500 (points/pixels) 317 | * `boxes` 318 | * `background_color` Background color of all boxes unless overridden 319 | * Default: "#cccccc" 320 | * `border_color` Border color ("stroke color") 321 | * Defaults: "#666666" 322 | * `border_width` Width of the border 323 | * Defaults: 3 (points) 324 | * `radius` Corner radius, value covers both "rx" and "ry" 325 | * Defaults: 10 (points) 326 | * `opacity` Opacity of the box. Covers both background and border 327 | * Defaults: "50%" 328 | * `border_dashed` Whether or not the border should be "dashed" by default 329 | * Defaults: true 330 | * `text_placement` Where to place the text field in the box. Text is optional. This value is not implemented yet 331 | * Defaults: "top" 332 | 333 | 334 | # Made by who 335 | Two network engineers, not satisfied with what we could find in the open source world. 336 | * Marius Larsen 337 | * Jonas H. Lindstad -------------------------------------------------------------------------------- /topoflow.js: -------------------------------------------------------------------------------- 1 | /* 2 | Version: 2022-12-04 3 | */ 4 | 5 | class topoflow{ 6 | constructor(){ 7 | /* 8 | Initialize the SVG element 9 | */ 10 | this.svg_container = d3.select("#canvas"); 11 | this.svg = this.svg_container.append("g").attr('class', 'main_group'); 12 | 13 | /* 14 | ######################### 15 | # # 16 | # VARIABLES # 17 | # # 18 | ######################### 19 | */ 20 | 21 | /* 22 | Defines the default options for the maps. This is to be able to override options by each JSON source. 23 | */ 24 | this.options = { 25 | /* 26 | All colors definitions goes here 27 | */ 28 | 'colors': { 29 | 'circle_fill': '#000', 30 | 'circle_outline': '#fff', 31 | 'circle_outline_down': '#f00', 32 | 'arrow_pointer': '#fff', 33 | 'svg_background_color': '#000', 34 | 'link': '#fff', 35 | 'link_down': '#f00', 36 | 'link_text': '#000', 37 | 'node_text': '#fff', 38 | 'load': [ 39 | '#f00', // >0% 40 | '#ff0', // >33% 41 | '#0f0' // > 66% 42 | ] 43 | }, 44 | 45 | /* 46 | All text/font properties goes here 47 | */ 48 | 'text': { 49 | /* 50 | Where node text position should be. Supported: 51 | * bottom, top, right, left 52 | * (which will be drawn centered) 53 | */ 54 | 'node_position': 'bottom', 55 | 56 | /* 57 | Whether or not the optional text on the links should be rotated along the link or not 58 | false: no rotation 59 | true: text is rotated along the link 60 | */ 61 | 'link_follow_angle': true, 62 | 63 | /* 64 | Prevents the link text from being displayed upside down (180 degrees) 65 | */ 66 | 'link_prevent_upside_down': true, 67 | 68 | /* 69 | Default offset of text to node. This is multiplied by "node_radius" 70 | */ 71 | 'node_offset': 1.4, 72 | 73 | /* 74 | Text size for nodes. Defaults to ???. Not implemented yet 75 | */ 76 | 'text_size_nodes': "unknown", 77 | 78 | /* 79 | Text size for links. Defaults to ???. Not implemented yet 80 | */ 81 | 'text_size_links': "unknown", 82 | }, 83 | 84 | 85 | /* 86 | Sets the radius of the node, if node type is not specified, or node type is "circle" 87 | default: 40 88 | */ 89 | 'node_radius': 40, 90 | 91 | /* 92 | Link properties goes here 93 | */ 94 | 'link': { 95 | /* 96 | Width of the link in points 97 | */ 98 | 'width': 20, 99 | 100 | /* 101 | Then drawing multiple links between the same two nodes, this spacing will be used to space the links evenly out. 102 | In points 103 | */ 104 | 'spacing': 20 105 | }, 106 | 107 | /* 108 | Sets the height and width of the element. Can be specified either as percent ("100%") or pixels ("1000") 109 | */ 110 | 'svg_width': 1500, 111 | 'svg_height': 1000, 112 | 113 | /* 114 | Not implemented yet 115 | */ 116 | 'display_fullscreen': false, 117 | 118 | /* 119 | Boxes properties goes here. Needs more comments :-) 120 | */ 121 | 'boxes': { 122 | 'background_color': "#cccccc", 123 | 'border_color': "#666666", 124 | 125 | /* 126 | Width of the border 127 | */ 128 | 'border_width': 3, 129 | 130 | /* 131 | Corner radius, value covers both "rx" and "ry" 132 | */ 133 | 'radius': 10, 134 | 135 | /* 136 | Covers both fill and stroke 137 | */ 138 | 'opacity': "50%", 139 | 140 | 141 | 142 | 'text_placement': "top", 143 | 144 | /* 145 | Whether or not the border should be "dashed" by default. 146 | fefault: true 147 | */ 148 | 'border_dashed': true 149 | } 150 | }; 151 | 152 | 153 | // var node_radius = 40; 154 | 155 | this.markerBoxWidth = 20; 156 | this.markerBoxHeight = 20; 157 | this.arrowPoints = [[0, 0], [0, 20], [20, 10]]; 158 | 159 | 160 | /* 161 | New, "experimental" dataset 162 | */ 163 | this.main_dataset = { 164 | 'links': [], 165 | 'nodes': [] 166 | }; 167 | 168 | 169 | /* 170 | To hold all the magic. All the links and nodes 171 | dataset.nodes.blabla 172 | dataset.links.blabla 173 | */ 174 | let dataset = {}; 175 | 176 | 177 | /* 178 | Required parameters for adding a node 179 | */ 180 | let required_node_parameters = [ 181 | 'x', // x coordinate 182 | 'y', // y coordinate 183 | 'name' // name of node 184 | ] 185 | 186 | 187 | /* 188 | Required parameters for adding a link 189 | */ 190 | let required_link_parameters = [ 191 | 'x1', // start x coordinate 192 | 'y1', // start y coordinate 193 | 'x2', // end y coordinate 194 | 'y2' // end y coordinate 195 | ] 196 | 197 | let load_color_steps = {}; 198 | 199 | /* 200 | Create marker(s) 201 | Logic ensures that exists inside the element 202 | */ 203 | let defs = d3.select("defs"); 204 | if(defs.size() == 0){ 205 | console.log(' not found, creating it'); 206 | defs = this.svg_container.append('defs'); 207 | } 208 | 209 | defs.append('marker') 210 | .attr('id', 'arrow') 211 | .attr('class', 'arrow') 212 | .attr('viewBox', [0, 0, this.markerBoxWidth, this.markerBoxHeight]) 213 | .attr('refX', 1) // 1 point overlap on marker and line 214 | .attr('refY', 10) 215 | .attr('markerWidth', this.markerBoxWidth) 216 | .attr('markerHeight', this.markerBoxHeight) 217 | .attr('orient', 'auto-start-reverse') 218 | .attr('markerUnits', 'userSpaceOnUse') // Needed, or the arrow head will inherit the stroke-width of the line "parent" 219 | .attr('fill', this.options.colors.arrow_pointer) 220 | .append('path') 221 | .attr('d', d3.line()(this.arrowPoints)); 222 | } 223 | 224 | 225 | /** 226 | * Will provide a list of link spacing placements. This enables us to draw multiple links between nodes. 227 | * In the example "number_of_links" being 5 and "link_spacing" being 10 will wield the following result: [-20, -10, 0, 10, 20]. 228 | * If number_of_links === 1, the result will be [0] ("center the link") 229 | 230 | * @param {number} number_of_links: Number of links, to calculate the correct spacing 231 | * @returns {list}: spacing values, from lowest to highest 232 | */ 233 | calculate_spacing(number_of_links){ 234 | /* 235 | Prevents "division by zero" crash 236 | */ 237 | if(number_of_links === 0){ 238 | return false; 239 | } 240 | 241 | /* 242 | Quick and dirty "if its 1, lets just return the center position" 243 | */ 244 | if(number_of_links === 1){ 245 | return [0]; 246 | } 247 | 248 | /* 249 | Do the calculations 250 | */ 251 | let data = []; 252 | let half_link_spacing= this.options.link.spacing/2; 253 | let lowest_spacing = (half_link_spacing*number_of_links-half_link_spacing)*-1; 254 | // let i = lowest_spacing; // why do i need this? 255 | for (let i = lowest_spacing; i <= lowest_spacing*-1; i += this.options.link.spacing){ 256 | data.push(i); 257 | } 258 | return data; 259 | } 260 | 261 | 262 | /** 263 | Used for creating the "steps" in load coloring. 264 | options.colors.load is now an array with n colors like this: 265 | We need to make it an object with steps, like this: 266 | [ 267 | { 268 | "step": 75, 269 | "color": "#aaf" 270 | }, 271 | { 272 | "step": 50, 273 | "color": "#00f" 274 | }, 275 | { 276 | "step": 25, 277 | "color": "#fa0" 278 | }, 279 | { 280 | "step": 0, 281 | "color": "#f0a" 282 | } 283 | ] 284 | 285 | @returns array of objects with color steps 286 | */ 287 | calculate_load_color_streps(){ 288 | // let load = parseInt(load); 289 | let new_load_array = []; 290 | let number_of_colors = this.options.colors.load.length; 291 | for (let i = 0; i < number_of_colors; i++){ 292 | new_load_array.push({ 293 | step: Math.floor((100/number_of_colors)*i), 294 | color: this.options.colors.load[i] 295 | }); 296 | } 297 | return new_load_array.reverse(); // flip the array, as it's used for "matching upwards" 298 | 299 | } 300 | 301 | /** 302 | Then we can do "for each of the colors starting from high to low, if load is higher than color use color, else iterate further" 303 | Uses the array created with calculate_load_color_streps() 304 | 305 | @param {float|int} load (percent) 306 | @returns {string} hex link color (e.g. "#f0a") 307 | */ 308 | link_load_color(load){ 309 | if(load === undefined){ 310 | return this.options.colors.link; 311 | } 312 | let color_steps = this.load_color_steps; 313 | for(var i = 0; i < color_steps.length; i++){ 314 | if(load > color_steps[i].step){ 315 | return color_steps[i].color; 316 | } 317 | } 318 | return this.options.colors.link; 319 | } 320 | 321 | 322 | 323 | /** 324 | * Will draw a node on the map 325 | 326 | * @param {object} Parameters. See "required_node_parameters" variable for list of required variables. 327 | * @return {boolean} True on success, false on error (like missing parameters). Check console output for errors. 328 | */ 329 | draw_node(args){ 330 | /* 331 | Validating args to confirm required node parameters 332 | */ 333 | for(prop in this.required_node_parameters){ 334 | if(this.required_node_parameters[prop] in args !== true){ 335 | console.log('Error: unable to draw node. Missing parameter "' + this.required_node_parameters[prop] + '"'); 336 | return false; 337 | } 338 | } 339 | 340 | console.log('Drawing node ' + args.name); 341 | 342 | /* 343 | Add node 344 | 345 | Jquery stuff for hilighting link - not currently in use 346 | .on("click", function(){ 347 | console.log(d3.select(this)); 348 | }) 349 | .on("mouseover", function(d) { 350 | d3.select(this).style("fill", "#3236a8"); 351 | }).on("mouseout", function(d) { 352 | d3.select(this).style("fill", this.options.colors.circle_fill); 353 | }) 354 | 355 | */ 356 | let node = this.svg.append("circle") 357 | .attr('cx', args.x) 358 | .attr('cy', args.y) 359 | .attr('r', this.options.node_radius) 360 | .attr('stroke-width', 5) 361 | .attr('stroke', this.options.colors.circle_outline) 362 | .style('fill', this.options.colors.circle_fill) 363 | .attr('data-node-name', 'asdf'); 364 | 365 | /* 366 | Where the node text is supposed be drawn. 367 | This is the default fallback, if nothing is defined in $text_pos_defs 368 | */ 369 | let node_text_location_y = args.y+(this.options.node_radius*this.options.text.node_offset); 370 | let node_text_location_x = args.x; 371 | let text_anchor = 'middle'; 372 | let text_position = args.text_position; 373 | 374 | 375 | 376 | /* 377 | Text position definitions 378 | @todo: Separate function: draw_node_text(x, y, placement, text) 379 | */ 380 | let text_pos_defs = { 381 | 'center': { 382 | 'position_y': args.y 383 | }, 384 | 'top': { 385 | 'position_y': args.y-(this.options.node_radius*this.options.text.node_offset) 386 | }, 387 | 'top-left': { 388 | 'anchor': 'end', 389 | 'position_x': args.x-(this.options.node_radius*this.options.text.node_offset*0.7), 390 | 'position_y': args.y-(this.options.node_radius*this.options.text.node_offset*0.7) 391 | }, 392 | 'top-right': { 393 | 'anchor': 'start', 394 | 'position_x': args.x+(this.options.node_radius*this.options.text.node_offset*0.7), 395 | 'position_y': args.y-(this.options.node_radius*this.options.text.node_offset*0.7) 396 | }, 397 | 'bottom': { 398 | 'position_y': args.y+(this.options.node_radius*this.options.text.node_offset) 399 | }, 400 | 'bottom-left': { 401 | 'anchor': 'end', 402 | 'position_x': args.x-(this.options.node_radius*this.options.text.node_offset*0.7), 403 | 'position_y': args.y+(this.options.node_radius*this.options.text.node_offset*0.7) 404 | }, 405 | 'bottom-right': { 406 | 'anchor': 'start', 407 | 'position_x': args.x+(this.options.node_radius*this.options.text.node_offset*0.7), 408 | 'position_y': args.y+(this.options.node_radius*this.options.text.node_offset*0.7) 409 | }, 410 | 'right': { 411 | 'anchor': 'start', 412 | 'position_x': args.x+(this.options.node_radius*this.options.text.node_offset) 413 | }, 414 | 'left': { 415 | 'anchor': 'end', 416 | 'position_x': args.x-(this.options.node_radius*this.options.text.node_offset) 417 | }, 418 | } 419 | 420 | /* 421 | override the default settings - e.g. "{options: {text: {node_position: xxx}}}" is set in the JSON object 422 | */ 423 | if('text' in this.options && 'node_position' in this.options.text){ 424 | if(this.options.text.node_position in text_pos_defs){ 425 | if('anchor' in text_pos_defs[this.options.text.node_position]){ 426 | text_anchor = text_pos_defs[this.options.text.node_position]['anchor']; 427 | } 428 | 429 | if('position_x' in text_pos_defs[this.options.text.node_position]){ 430 | node_text_location_x = text_pos_defs[this.options.text.node_position]['position_x']; 431 | } 432 | 433 | if('position_y' in text_pos_defs[this.options.text.node_position]){ 434 | node_text_location_y = text_pos_defs[this.options.text.node_position]['position_y']; 435 | } 436 | }else{ 437 | console.log('Unknown options.text.node_position, falling back'); 438 | } 439 | } 440 | 441 | 442 | /* 443 | Override the previous text position settings if it's defined at the node level in the JSON file 444 | */ 445 | if(args.text_position in text_pos_defs){ 446 | if('anchor' in text_pos_defs[args.text_position]){ 447 | text_anchor = text_pos_defs[args.text_position]['anchor']; 448 | } 449 | 450 | if('position_x' in text_pos_defs[args.text_position]){ 451 | node_text_location_x = text_pos_defs[args.text_position]['position_x']; 452 | } 453 | 454 | if('position_y' in text_pos_defs[args.text_position]){ 455 | node_text_location_y = text_pos_defs[args.text_position]['position_y']; 456 | } 457 | } 458 | 459 | /* 460 | 461 | 462 | /* 463 | Draw text. 464 | */ 465 | this.svg.append("text") 466 | .attr('class', 'node-text') 467 | .attr('x', node_text_location_x) 468 | .attr('y', node_text_location_y) 469 | .attr('text-anchor', text_anchor) 470 | .attr('dominant-baseline', 'middle') 471 | .style('fill', this.options.colors.node_text) 472 | .text(args.name) 473 | 474 | /* 475 | if('state' in args){ 476 | if(args.state == 'down'){ 477 | console.log('node down'); 478 | node.attr('stroke', this.options.colors.circle_outline_down); 479 | } 480 | } 481 | */ 482 | } 483 | 484 | /* 485 | arg must contain: 486 | * from 487 | * to 488 | */ 489 | draw_link_2way(args){ 490 | try{ 491 | console.log('Drawing regular (2way) link from ' + args.from + ' to ' + args.to, args); 492 | 493 | // global settings 494 | let split_point = 0.5; // Where the arrows on the bidirectional link (it's 1 link with arrows in the middle...) will be placed 495 | let text_pos = 0.5; // places the text not in the center of the link (between the arrows), but halfway between center of link and node 496 | let arrow_offset = 20; // Size of the arrow in points 497 | 498 | /* 499 | The Math.atan2() function returns the angle in the plane (in radians) between the positive x-axis and the ray from (0,0) to the point (x,y), for Math.atan2(y,x) 500 | */ 501 | let angle_a_to_b = Math.atan2(this.main_dataset.nodes[args.to].y - this.main_dataset.nodes[args.from].y, this.main_dataset.nodes[args.to].x - this.main_dataset.nodes[args.from].x); 502 | 503 | /* 504 | degrees will be between -180 and 180. 505 | 0 degrees = regular way to read text 506 | */ 507 | let degrees = angle_a_to_b*(180/Math.PI); 508 | 509 | let sin_to_angle = Math.sin(angle_a_to_b); 510 | let cos_to_angle = Math.cos(angle_a_to_b); 511 | 512 | /* 513 | Link coloring based on load 514 | */ 515 | let link_color_in = this.link_load_color(args.load_in); 516 | let link_color_out = this.link_load_color(args.load_out); 517 | 518 | /* 519 | Adjust for spacing (spacing between parallell links?) 520 | */ 521 | let spacing_x = sin_to_angle * args.spacing; 522 | let spacing_y = cos_to_angle * args.spacing; 523 | 524 | let to_node_pos_x = parseInt(this.main_dataset.nodes[args.to].x) - spacing_x; 525 | let to_node_pos_y = parseInt(this.main_dataset.nodes[args.to].y) + spacing_y; 526 | let from_node_pos_x = parseInt(this.main_dataset.nodes[args.from].x) - spacing_x; 527 | let from_node_pos_y = parseInt(this.main_dataset.nodes[args.from].y) + spacing_y; 528 | 529 | /* 530 | Caclulate half way point 531 | */ 532 | 533 | let halfway_pos_x = to_node_pos_x-(to_node_pos_x-from_node_pos_x)*split_point; 534 | let halfway_pos_y = to_node_pos_y-(to_node_pos_y-from_node_pos_y)*split_point; 535 | 536 | /* 537 | To flip the text rotation the easy way for humans to read (e.g. never upside down) 538 | */ 539 | let text_degrees = 0; 540 | if(this.options.text.link_follow_angle === true){ 541 | text_degrees = degrees; 542 | if(this.options.text.link_prevent_upside_down === true && (text_degrees > 90 || text_degrees < -90)){ 543 | text_degrees += 180; 544 | } 545 | } 546 | 547 | /* 548 | Assign rate (used bandwidth) 549 | */ 550 | let rate_in = '', 551 | rate_out = ''; 552 | 553 | if('rate_in' in args){ 554 | rate_in = args.rate_in; 555 | } 556 | if('rate_out' in args){ 557 | rate_out = args.rate_out; 558 | } 559 | 560 | /* 561 | Assign state 562 | */ 563 | let link_state = 'up'; 564 | if('state' in args){ 565 | if(args.state == 'down'){ 566 | link_state = 'down'; 567 | } 568 | } 569 | 570 | /* 571 | Onclick functions - will be implementet some time... 572 | 573 | .on("click", function(){ 574 | console.log(d3.select(this).attr('y1')); 575 | } 576 | .on("mouseover", function(d) { 577 | d3.select(this).style("stroke", "#3236a8"); 578 | }) 579 | .on("mouseout", function(d) { 580 | d3.select(this).style("stroke", this.options.colors.link); 581 | }) 582 | .on("click", function(){ 583 | console.log(d3.select(this).attr('y1')); 584 | } 585 | */ 586 | 587 | let link_a_b = this.svg 588 | .append('line') 589 | .attr('class', 'link link-' + link_state) 590 | .attr('x1', from_node_pos_x) 591 | .attr('y1', from_node_pos_y) 592 | .attr('x2', (halfway_pos_x - cos_to_angle * arrow_offset) ) 593 | .attr('y2', (halfway_pos_y - sin_to_angle * arrow_offset) ) 594 | .attr('stroke', link_color_out) 595 | .attr('stroke-width', this.options.link.width) 596 | .attr('marker-end', 'url(#arrow)'); 597 | 598 | let link_b_a = this.svg 599 | .append('line') 600 | .attr('class', 'link link-' + link_state) 601 | .attr('x1', to_node_pos_x) 602 | .attr('y1', to_node_pos_y) 603 | .attr('x2', (halfway_pos_x- Math.cos(angle_a_to_b + Math.PI) * arrow_offset) ) 604 | .attr('y2', (halfway_pos_y- Math.sin(angle_a_to_b + Math.PI) * arrow_offset) ) 605 | .attr('stroke', link_color_in) 606 | .attr('stroke-width', this.options.link.width) 607 | .attr('marker-end', 'url(#arrow)') 608 | ; 609 | 610 | /* 611 | Draw link as "link down" 612 | */ 613 | if(link_state == 'down'){ 614 | link_a_b.attr('stroke', this.options.colors.link_down); 615 | link_b_a.attr('stroke', this.options.colors.link_down); 616 | } 617 | 618 | /* 619 | Do not draw text on links if the link is down 620 | */ 621 | if(link_state !== 'down'){ 622 | this.draw_text_on_link({ 623 | /* 624 | So you will hate your self a bit less in the future: 625 | x = + ( * ) + - 626 | */ 627 | x: from_node_pos_x + ((to_node_pos_x - from_node_pos_x) * (split_point * text_pos)) + (cos_to_angle * this.options.node_radius / 2) - (cos_to_angle * arrow_offset / 2), 628 | y: from_node_pos_y + ((to_node_pos_y - from_node_pos_y) * (split_point * text_pos)) + (sin_to_angle * this.options.node_radius / 2) - (sin_to_angle * arrow_offset / 2), 629 | text: rate_in, 630 | degrees: text_degrees 631 | }); 632 | 633 | this.draw_text_on_link({ 634 | x: to_node_pos_x-((to_node_pos_x-from_node_pos_x)*split_point*text_pos)-(cos_to_angle * this.options.node_radius / 2)+(cos_to_angle * arrow_offset / 2), 635 | y: to_node_pos_y-((to_node_pos_y-from_node_pos_y)*split_point*text_pos)-(sin_to_angle * this.options.node_radius / 2)+(sin_to_angle * arrow_offset / 2), 636 | text: rate_out, 637 | degrees: text_degrees 638 | }); 639 | } 640 | }catch(err){ 641 | console.error('Error in draw_link_2way():'); 642 | console.error(err); 643 | } 644 | } 645 | 646 | /* 647 | * Used for drawing a 1 way link 648 | * @param args 649 | */ 650 | draw_link_1way(args){ 651 | try{ 652 | console.log('Drawing 1way link from ' + args.from + ' to ' + args.to + ' with the following args', args); 653 | 654 | //Global settings 655 | let text_pos = 0.5; 656 | let arrow_offset = 20; 657 | 658 | /* 659 | The Math.atan2() function returns the angle in the plane (in radians) between the positive x-axis and the ray from (0,0) to the point (x,y), for Math.atan2(y,x) 660 | */ 661 | let angle_a_to_b = Math.atan2(this.main_dataset.nodes[args.to].y - this.main_dataset.nodes[args.from].y, this.main_dataset.nodes[args.to].x - this.main_dataset.nodes[args.from].x); 662 | let degrees = angle_a_to_b*(180/Math.PI) 663 | let sin_to_angle = Math.sin(angle_a_to_b); 664 | let cos_to_angle = Math.cos(angle_a_to_b); 665 | 666 | /* 667 | Link coloring based on load 668 | */ 669 | let link_color = this.link_load_color(args.load); 670 | 671 | /* 672 | Adjust for offset 673 | */ 674 | let spacing_x = sin_to_angle * args.spacing; 675 | let spacing_y = cos_to_angle * args.spacing; 676 | 677 | let to_node_pos_x = (parseInt(this.main_dataset.nodes[args.to].x) - spacing_x) - cos_to_angle * (this.options.node_radius + arrow_offset); 678 | let to_node_pos_y = (parseInt(this.main_dataset.nodes[args.to].y) + spacing_y) - sin_to_angle * (this.options.node_radius + arrow_offset); 679 | let from_node_pos_x = parseInt(this.main_dataset.nodes[args.from].x) - spacing_x; 680 | let from_node_pos_y = parseInt(this.main_dataset.nodes[args.from].y) + spacing_y; 681 | 682 | if('reversed' in args && args['reversed'] == true){ 683 | to_node_pos_x = (parseInt(this.main_dataset.nodes[args.from].x) - spacing_x) + cos_to_angle * (this.options.node_radius + arrow_offset); 684 | to_node_pos_y = (parseInt(this.main_dataset.nodes[args.from].y) + spacing_y) + sin_to_angle * (this.options.node_radius + arrow_offset); 685 | from_node_pos_x = parseInt(this.main_dataset.nodes[args.to].x) - spacing_x; 686 | from_node_pos_y = parseInt(this.main_dataset.nodes[args.to].y) + spacing_y; 687 | } 688 | 689 | /* 690 | To flip the text rotation the easy way for humans to read (e.g. never upside down) 691 | */ 692 | let text_degrees = 0; 693 | if(this.options.text.link_follow_angle === true){ 694 | text_degrees = degrees; 695 | if(this.options.text.link_prevent_upside_down === true && (text_degrees > 90 || text_degrees < -90)){ 696 | text_degrees += 180; 697 | } 698 | } 699 | 700 | 701 | /* 702 | Assign rate (used bandwidth) 703 | */ 704 | let rate = '', 705 | rate_out = ''; 706 | 707 | if('rate' in args){ 708 | rate = args.rate; 709 | } 710 | 711 | 712 | /* 713 | Assign state 714 | */ 715 | let link_state = 'up'; 716 | if('state' in args && args['state'] == 'down'){ 717 | link_state = 'down'; 718 | } 719 | 720 | let link_a_b = this.svg 721 | .append('line') 722 | .attr('class', 'link link-' + link_state) 723 | .attr('x1', from_node_pos_x) 724 | .attr('y1', from_node_pos_y) 725 | .attr('x2', to_node_pos_x) 726 | .attr('y2', to_node_pos_y) 727 | .attr('stroke', link_color) 728 | .attr('stroke-width', this.options.link.width) 729 | .attr('marker-end', 'url(#arrow)') 730 | .on("click", function(){ 731 | console.log(d3.select(this).attr('y1')); 732 | }); 733 | 734 | /* 735 | Draw link as "link down" 736 | */ 737 | if(link_state == 'down'){ 738 | link_a_b.attr('stroke', this.options.colors.link_down); 739 | } 740 | 741 | /* 742 | Do not draw text on links if the link is down 743 | */ 744 | if(link_state !== 'down'){ 745 | this.draw_text_on_link({ 746 | x: from_node_pos_x + ((to_node_pos_x - from_node_pos_x) * text_pos) + (cos_to_angle * this.options.node_radius) / 2, 747 | y: from_node_pos_y + ((to_node_pos_y - from_node_pos_y) * text_pos) + (sin_to_angle * this.options.node_radius) / 2, 748 | text: rate, 749 | degrees: text_degrees 750 | }); 751 | } 752 | }catch(err){ 753 | console.error('Error in draw_link_1way():', err); 754 | } 755 | } 756 | 757 | /** 758 | * Used for placing text on links 759 | * @param {object} args: must contain 'x', 'y' and 'text' 760 | * @return {boolean} 761 | */ 762 | draw_text_on_link(args){ 763 | let newly_drawn_text = this.svg.append("text") 764 | .attr('class', 'link-text') 765 | .attr('x', args.x) 766 | .attr('y', args.y) 767 | .attr('transform', 'rotate(' + args.degrees + ', ' + (args.x) + ', ' + (args.y) + ')') // rotates the text 768 | .attr('text-anchor', 'middle') 769 | .attr('dominant-baseline', 'middle') 770 | .style('fill', this.options.colors.link_text) 771 | .text(args.text) 772 | return true; 773 | } 774 | 775 | 776 | /* 777 | Draw boxes with arguments 778 | @param {object} args: must contain "corner_a_x", "corner_a_y", "corner_b_x", "corner_b_y" as a minimum 779 | Optional key-values in $args: 780 | rounded_corners 781 | background_color: # 782 | border_color: #hex 783 | border_width: 784 | opacity: 785 | text_placement: (see node text placement as name ref.) 786 | text: 787 | */ 788 | draw_box(args){ 789 | console.log('draw_box called with the following args:', args); 790 | let box_properties = { 791 | 'background_color': args.background_color || this.options.boxes.background_color, 792 | 'border_color': args.border_color || this.options.boxes.border_color, 793 | 'border_width': args.border_width || this.options.boxes.border_width, 794 | 'radius': args.radius || this.options.boxes.radius 795 | }; 796 | if('dashed' in args && args.dashed == false){ 797 | 798 | }else{ 799 | box_properties['border_dashed'] = box_properties.border_width; 800 | } 801 | 802 | 803 | 804 | 805 | console.log('box_properties: ', box_properties); 806 | 807 | /* 808 | let box_background_color = args.background_color || this.options.boxes.background_color; 809 | let box_border_color = args.border_color || this.options.boxes.border_color; 810 | */ 811 | /* 812 | Fra options 813 | 'background_color': "#cccccc", 814 | 'border_color': "#666666", 815 | 'border_width': 3, 816 | 'opacity': 50, 817 | 'text_placement': "top", 818 | */ 819 | 820 | let new_box = this.svg.append("rect") 821 | .attr('x', args.corner_a_x) 822 | .attr('y', args.corner_a_y) 823 | .attr('width', args.corner_b_x-args.corner_a_x) 824 | .attr('height', args.corner_b_y-args.corner_a_y) 825 | .attr('fill', box_properties.background_color) 826 | .attr('stroke', box_properties.border_color) 827 | .attr('stroke-width', box_properties.border_width) 828 | .attr('stroke-dasharray', box_properties.border_dashed) 829 | .attr('rx', box_properties.radius) 830 | return true; 831 | 832 | } 833 | 834 | 835 | /** 836 | Used for overwriting the default options. 837 | Calling set_default_options() from class initialization overwrites the default options 838 | "options" from the JSON file overwrites set_default_options() 839 | */ 840 | overwrite_options(args){ 841 | console.log('Attempting to overwrite the current options with this', args); 842 | 843 | for(const [key, value] of Object.entries(args)){ 844 | // Check if the value is an object 845 | if(key in this.options && typeof value === 'object' && value !== null){ 846 | for(const [key2, value2] of Object.entries(args[key])){ 847 | this.options[key][key2] = args[key][key2]; 848 | } 849 | }else if(key in this.options){ 850 | this.options[key] = args[key]; 851 | }else{ 852 | console.log('Not accepting option "' + key + '"'); 853 | } 854 | } 855 | } 856 | 857 | 858 | /* 859 | Populate the dataset 860 | */ 861 | run(json_file){ 862 | console.log('run() called (json file: ' + json_file + ')'); 863 | // prevent caching 864 | let class_this = this; // because getJSON overwrites "this" 865 | $.getJSON(json_file, {_: new Date().getTime()}) 866 | .done(function(data){ 867 | 868 | console.log('data loaded from json file', data); 869 | let dataset = data; 870 | 871 | 872 | /** 873 | * Populates a new "main dataset", which will make us be able to detect multiple links 874 | @todo: Factor away the "exploding dataset". Will iterate over an object for each key in another object. 875 | @todo: More about the topic: https://stackoverflow.com/questions/13964155/get-javascript-object-from-array-of-objects-by-value-of-property 876 | */ 877 | try{ 878 | // loop over each link object in the JSON dataset 879 | $.each(data.links, function(not_in_use, outer_links_loop){ 880 | let state_machine_multiple_link_detected = false; 881 | let link_type = outer_links_loop.type || '2way'; 882 | let link_state = outer_links_loop.state || 'up'; 883 | 884 | // loop over each link in the json provided data 885 | $.each(class_this.main_dataset.links, function(inner_links_loop_index, inner_links_loop){ 886 | // if we've seen the [from, to] or [to, from] pair before, append to that 887 | if((inner_links_loop.to === outer_links_loop.to && inner_links_loop.from === outer_links_loop.from) || (inner_links_loop.to === outer_links_loop.from && inner_links_loop.from === outer_links_loop.to)){ 888 | state_machine_multiple_link_detected = true; 889 | console.log("Multiple link detected (" + outer_links_loop.to + ", " + outer_links_loop.from + "), inner_links_loop_index (" + inner_links_loop_index + ")"); 890 | 891 | if(link_type == '2way'){ 892 | /* 893 | 2 way link 894 | */ 895 | // checkin if a -> b 896 | if(inner_links_loop.to === outer_links_loop.to && inner_links_loop.from === outer_links_loop.from){ 897 | class_this.main_dataset['links'][inner_links_loop_index]['links'].push({ 898 | 'type': link_type, 899 | 'state': link_state, 900 | 'rate_in': outer_links_loop.rate_in, 901 | 'rate_out': outer_links_loop.rate_out, 902 | 'load_in': outer_links_loop.load_in, 903 | 'load_out': outer_links_loop.load_out 904 | }) 905 | } 906 | 907 | // checkin if b -> a 908 | if(inner_links_loop.to === outer_links_loop.from && inner_links_loop.from === outer_links_loop.to){ 909 | class_this.main_dataset['links'][inner_links_loop_index]['links'].push({ 910 | 'type': link_type, 911 | 'state': link_state, 912 | 'rate_out': outer_links_loop.rate_in, // reversed 913 | 'rate_in': outer_links_loop.rate_out, // reversed 914 | 'load_in': outer_links_loop.load_out, // reversed 915 | 'load_out': outer_links_loop.load_in // reversed 916 | }) 917 | } 918 | }else{ 919 | /* 920 | 1 way link 921 | */ 922 | if(inner_links_loop.to === outer_links_loop.to && inner_links_loop.from === outer_links_loop.from){ 923 | class_this.main_dataset['links'][inner_links_loop_index]['links'].push({ 924 | 'type': link_type, 925 | 'state': link_state, 926 | 'rate': outer_links_loop.rate, 927 | 'load': outer_links_loop.load, 928 | }) 929 | } 930 | 931 | if(inner_links_loop.to === outer_links_loop.from && inner_links_loop.from === outer_links_loop.to){ 932 | class_this.main_dataset['links'][inner_links_loop_index]['links'].push({ 933 | 'type': link_type, 934 | 'state': link_state, 935 | 'rate': outer_links_loop.rate, 936 | 'load': outer_links_loop.load, 937 | 'reversed' : true 938 | }) 939 | } 940 | } 941 | } 942 | }); 943 | if(state_machine_multiple_link_detected === false){ 944 | if(link_type == '2way'){ 945 | /* 946 | 2 way link 947 | */ 948 | class_this.main_dataset['links'].push({ 949 | 'to': outer_links_loop.to, 950 | 'from': outer_links_loop.from, 951 | 'links': [{ 952 | 'type': link_type, 953 | 'state': link_state, 954 | 'rate_in': outer_links_loop.rate_in, 955 | 'rate_out': outer_links_loop.rate_out, 956 | 'load_in': outer_links_loop.load_in, 957 | 'load_out': outer_links_loop.load_out 958 | }] 959 | }); 960 | }else{ 961 | /* 962 | 1 way link 963 | */ 964 | class_this.main_dataset['links'].push({ 965 | 'to': outer_links_loop.to, 966 | 'from': outer_links_loop.from, 967 | 'links': [{ 968 | 'type': link_type, 969 | 'state': link_state, 970 | 'rate': outer_links_loop.rate, 971 | 'load': outer_links_loop.load, 972 | }] 973 | }); 974 | } 975 | } 976 | }); 977 | // Copy over the nodes from JSON data to $main_dataset 978 | if('nodes' in data){ 979 | class_this.main_dataset.nodes = data.nodes; 980 | }else{ 981 | console.error('No nodes found in JSON data'); 982 | } 983 | }catch(error){ 984 | console.error(error); 985 | } 986 | 987 | 988 | 989 | /** 990 | * Conditionally overrides the default options from the options in the loaded JSON object "data" 991 | * Will only override if the option is already defined in the "options" object 992 | @todo Fix ugly hax, actually iterate recursively through object 993 | */ 994 | try{ 995 | if('options' in data){ 996 | for(const [key, value] of Object.entries(data.options)){ 997 | // Check if the value is an object 998 | if(key in class_this.options && typeof value === 'object' && value !== null){ 999 | for(const [key2, value2] of Object.entries(data.options[key])){ 1000 | class_this.options[key][key2] = data.options[key][key2]; 1001 | } 1002 | }else if(key in class_this.options){ 1003 | class_this.options[key] = data.options[key]; 1004 | } 1005 | 1006 | } 1007 | } 1008 | }catch(error){ 1009 | console.error('Error while parsing "options" from JSON: ' + error); 1010 | } 1011 | console.log('options now', class_this.options); 1012 | 1013 | 1014 | /* 1015 | Draw boxes 1016 | */ 1017 | try{ 1018 | $.each(data.boxes, function(not_in_use, boxes_args){ 1019 | console.log('boxes_args in loop: ', boxes_args); 1020 | class_this.draw_box(boxes_args); 1021 | }); 1022 | }catch(error){ 1023 | console.error('Error while drawing boxes: ' + error); 1024 | } 1025 | 1026 | 1027 | /* 1028 | Find the links in the dataset, and draw them 1029 | Note:_ the "main_dataset.links" is a bit unclear, as that is a collection of nodes, and in that a collection of link between nodes 1030 | */ 1031 | try{ 1032 | // Generate load color steps 1033 | class_this.load_color_steps = class_this.calculate_load_color_streps(); 1034 | console.log('load_color_steps', class_this.load_color_steps); 1035 | 1036 | 1037 | $.each(class_this.main_dataset.links, function(not_in_use, link_props){ 1038 | let link_to = link_props.to; 1039 | let link_from = link_props.from; 1040 | let number_of_links = link_props.links.length; 1041 | 1042 | console.log('Processing a total of ' + number_of_links+ ' links from ' + link_to + ' to ' + link_from); 1043 | let link_spacing_array = class_this.calculate_spacing(number_of_links); 1044 | $.each(link_props.links, function(link_index, link){ 1045 | /* 1046 | Draw each separate link 1047 | */ 1048 | let spacing = link_spacing_array[link_index]; 1049 | 1050 | /* 1051 | Merge data into a new object to feed the draw_link*() functions 1052 | */ 1053 | let new_properties_formated = Object.assign({}, link, {'to': link_to, 'from': link_from, 'spacing': spacing}); 1054 | console.log('new_properties_formated: ', new_properties_formated); 1055 | 1056 | // The draw_* functions does not need to know of the type (1way, 2way), as it's dedicated functions beaing called for each type. 1057 | delete new_properties_formated.type; 1058 | 1059 | // console.log('new_properties_formated', new_properties_formated) 1060 | 1061 | if(link.type == '2way'){ 1062 | class_this.draw_link_2way(new_properties_formated); 1063 | }else{ 1064 | class_this.draw_link_1way(new_properties_formated); 1065 | } 1066 | 1067 | }); 1068 | }); 1069 | }catch(error){ 1070 | console.error('Error while drawing links: ' + error); 1071 | } 1072 | 1073 | 1074 | try{ 1075 | $.each(class_this.main_dataset.nodes, function(node_name, node_prop){ 1076 | node_prop.name = node_name; 1077 | class_this.draw_node(node_prop); 1078 | }); 1079 | }catch(error){ 1080 | console.error('Error while drawing nodes: ' + error); 1081 | } 1082 | 1083 | 1084 | 1085 | /* 1086 | Applying the colors defined in options 1087 | */ 1088 | 1089 | // background color of SVG 1090 | class_this.svg_container.style('background-color', class_this.options.colors.svg_background_color); 1091 | 1092 | // set size of the object 1093 | class_this.svg_container.attr('height', class_this.options.svg_height); 1094 | class_this.svg_container.attr('width', class_this.options.svg_width); 1095 | 1096 | // Link arrow color 1097 | d3.select("#arrow").style('fill', class_this.options.colors.arrow_pointer); 1098 | }) 1099 | .fail( 1100 | function(jqXHR, textStatus, errorThrown){ 1101 | console.error('Unable to load JSON file "' + json_file + '", status: ' + errorThrown) 1102 | } 1103 | ); 1104 | } 1105 | } --------------------------------------------------------------------------------