├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── assets ├── andres-iniesta.jpg ├── brand-noise.png ├── courtois.jpg ├── cover-noise.png ├── daniel-alves.jpg ├── danielle-derossi.jpg ├── darker-noise.png ├── david-alaba.jpg ├── david-silva.jpg ├── default-bg.png ├── diego-costa.jpg ├── eden-hazard.jpg ├── favicon.ico ├── fonts │ ├── Icons.eot │ ├── Icons.svg │ ├── Icons.ttf │ └── Icons.woff ├── gareth-bale.jpg ├── gary-cahill.jpg ├── gerard-pique.jpg ├── harry-kane.jpg ├── hexa-pattern.png ├── illustration-top-white.svg ├── illustration-top.svg ├── james-rodriguez.jpg ├── leo-messi.jpg ├── logotype-black.svg ├── logotype-color.svg ├── logotype-white.svg ├── manuel-neuer.jpg ├── marcelo.jpg ├── mario-gotze.jpg ├── muller.jpg ├── neymar.jpg ├── obi-mikel.jpg ├── pattern-hexas.svg ├── petr-cech.jpg ├── phillip-lahm.jpg ├── ronaldo.jpg ├── sergio-aguero.jpg ├── share-image.png ├── share-image.psd ├── thiago-silva.jpg ├── vincent-kompany.jpg ├── ws-logo-white.svg ├── ws-logo.svg ├── yaya-toure.jpg └── zlatan-ibrahimovic.jpg ├── client ├── algorithms │ ├── ArticulationPointSearch.js │ ├── BipartitenessAlgorithm.js │ ├── BreadthFirstSearch.js │ ├── DepthFirstSearch.js │ └── TarjanStrongConnection.js ├── blog │ ├── nodes.md │ ├── references.js │ ├── section1.js │ ├── section2.js │ ├── section3.js │ ├── section4.js │ ├── section5.js │ └── section6.js ├── components │ ├── BlogHeading.js │ ├── BlogHeading.styl │ ├── ContentSection.js │ ├── ContentSection.styl │ ├── PlotContainer.js │ ├── PlotContainer.styl │ ├── PlotExplanation.js │ ├── PlotExplanation.styl │ ├── PlotHeader.js │ ├── PlotHeader.styl │ ├── ShareButtons.js │ └── ShareButtons.styl ├── constants │ └── ItemTypes.js ├── containers │ ├── AlgoVizContainer.js │ ├── AlgoVizContainer.styl │ ├── AppContainer.js │ ├── AppContainer.styl │ ├── BlogContainer.js │ ├── BlogContainer.styl │ ├── ContentsContainer.js │ └── ContentsContainer.styl ├── dataStructures │ ├── Queue.js │ └── Stack.js ├── main.js ├── models │ └── NodeModel.js └── utils.js ├── fonts ├── icomoon.eot ├── icomoon.svg ├── icomoon.ttf └── icomoon.woff ├── index.html ├── package.json ├── style ├── icons-selection.json ├── icons.css ├── main.styl ├── pure.css └── variables.styl ├── webpack.config.js └── webpack.dev.server.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015" , "stage-0"], 3 | "env": { 4 | "start": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // I want to use babel-eslint for parsing! 3 | "parser": "babel-eslint", 4 | "ecmaFeatures": { 5 | "jsx": true, 6 | "classes": true, 7 | "modules": true, 8 | }, 9 | "env": { 10 | // I write for browser 11 | "browser": true, 12 | // in CommonJS 13 | "node": true 14 | }, 15 | "globals": { 16 | "process": false, 17 | "require": false, 18 | "define": false, 19 | "console": false, 20 | }, 21 | // To give you an idea how to override rule options: 22 | "rules": { 23 | "quotes": [2, "single"], 24 | "semi": [2, "always"], 25 | "strict": [2, "never"], 26 | "eol-last": [0], 27 | "no-mixed-requires": [0], 28 | "no-underscore-dangle": [0], 29 | "no-bitwise": 2, 30 | "camelcase": 2, 31 | "eqeqeq": 2, 32 | "wrap-iife": [2, "inside"], 33 | "no-use-before-define": [2, "nofunc"], 34 | "no-caller": 2, 35 | "no-undef": 2, 36 | "new-cap": 2, 37 | "react/jsx-uses-react": 2, 38 | "react/jsx-uses-vars": 2, 39 | "react/react-in-jsx-scope": 2 40 | }, 41 | "plugins": [ 42 | "react" 43 | ] 44 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **.DS_Store 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Graph Algorithms Visualised 2 | 3 | Source code for our blog post A visual guide to [Graph Traversal Algorithms](https://workshape.github.io/visual-graph-algorithms/). 4 | -------------------------------------------------------------------------------- /assets/andres-iniesta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/andres-iniesta.jpg -------------------------------------------------------------------------------- /assets/brand-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/brand-noise.png -------------------------------------------------------------------------------- /assets/courtois.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/courtois.jpg -------------------------------------------------------------------------------- /assets/cover-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/cover-noise.png -------------------------------------------------------------------------------- /assets/daniel-alves.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/daniel-alves.jpg -------------------------------------------------------------------------------- /assets/danielle-derossi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/danielle-derossi.jpg -------------------------------------------------------------------------------- /assets/darker-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/darker-noise.png -------------------------------------------------------------------------------- /assets/david-alaba.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/david-alaba.jpg -------------------------------------------------------------------------------- /assets/david-silva.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/david-silva.jpg -------------------------------------------------------------------------------- /assets/default-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/default-bg.png -------------------------------------------------------------------------------- /assets/diego-costa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/diego-costa.jpg -------------------------------------------------------------------------------- /assets/eden-hazard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/eden-hazard.jpg -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/favicon.ico -------------------------------------------------------------------------------- /assets/fonts/Icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/fonts/Icons.eot -------------------------------------------------------------------------------- /assets/fonts/Icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/fonts/Icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/fonts/Icons.ttf -------------------------------------------------------------------------------- /assets/fonts/Icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/fonts/Icons.woff -------------------------------------------------------------------------------- /assets/gareth-bale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/gareth-bale.jpg -------------------------------------------------------------------------------- /assets/gary-cahill.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/gary-cahill.jpg -------------------------------------------------------------------------------- /assets/gerard-pique.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/gerard-pique.jpg -------------------------------------------------------------------------------- /assets/harry-kane.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/harry-kane.jpg -------------------------------------------------------------------------------- /assets/hexa-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/hexa-pattern.png -------------------------------------------------------------------------------- /assets/illustration-top-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 18 | 20 | 22 | 23 | 24 | 25 | 30 | 52 | 55 | 57 | 63 | 64 | 65 | 70 | 92 | 95 | 97 | 103 | 104 | 105 | 106 | 107 | 110 | 111 | 112 | 113 | 116 | 117 | 118 | 119 | 122 | 123 | 124 | 125 | 128 | 129 | 130 | 131 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /assets/illustration-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 18 | 20 | 21 | 22 | 23 | 28 | 51 | 54 | 56 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 82 | 83 | 84 | 85 | 88 | 89 | 90 | 91 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /assets/james-rodriguez.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/james-rodriguez.jpg -------------------------------------------------------------------------------- /assets/leo-messi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/leo-messi.jpg -------------------------------------------------------------------------------- /assets/logotype-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/logotype-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/logotype-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/manuel-neuer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/manuel-neuer.jpg -------------------------------------------------------------------------------- /assets/marcelo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/marcelo.jpg -------------------------------------------------------------------------------- /assets/mario-gotze.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/mario-gotze.jpg -------------------------------------------------------------------------------- /assets/muller.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/muller.jpg -------------------------------------------------------------------------------- /assets/neymar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/neymar.jpg -------------------------------------------------------------------------------- /assets/obi-mikel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/obi-mikel.jpg -------------------------------------------------------------------------------- /assets/petr-cech.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/petr-cech.jpg -------------------------------------------------------------------------------- /assets/phillip-lahm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/phillip-lahm.jpg -------------------------------------------------------------------------------- /assets/ronaldo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/ronaldo.jpg -------------------------------------------------------------------------------- /assets/sergio-aguero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/sergio-aguero.jpg -------------------------------------------------------------------------------- /assets/share-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/share-image.png -------------------------------------------------------------------------------- /assets/share-image.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/share-image.psd -------------------------------------------------------------------------------- /assets/thiago-silva.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/thiago-silva.jpg -------------------------------------------------------------------------------- /assets/vincent-kompany.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/vincent-kompany.jpg -------------------------------------------------------------------------------- /assets/ws-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 16 | 17 | 18 | 24 | 31 | 36 | 42 | 60 | 64 | 78 | 87 | 96 | 97 | 99 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /assets/ws-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 16 | 17 | 18 | 24 | 31 | 36 | 42 | 60 | 64 | 78 | 87 | 96 | 97 | 99 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /assets/yaya-toure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/yaya-toure.jpg -------------------------------------------------------------------------------- /assets/zlatan-ibrahimovic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/assets/zlatan-ibrahimovic.jpg -------------------------------------------------------------------------------- /client/algorithms/ArticulationPointSearch.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Promise from 'bluebird'; 3 | 4 | import { promiseSequence } from '../utils'; 5 | 6 | /** 7 | * Depth First Search Algorithm Class 8 | */ 9 | export default class ArticulationPointSearch { 10 | /** 11 | * @constructor 12 | * 13 | * @param {Array} nodes An array of NodeModel objects 14 | * @param {React/Component} component A React Component 15 | * @param {Number} pause 16 | */ 17 | constructor(nodes, component, pause = 500) { 18 | this.nodes = nodes; 19 | this.component = component; 20 | this.stack = []; 21 | this.visitOrder = []; 22 | this.explanation = []; 23 | this.currentVisitIndex = 0; 24 | this.pause = pause; 25 | } 26 | 27 | /** 28 | * Log a message (used for incremental updates about algorithms progress) 29 | * 30 | * @param {String} text 31 | */ 32 | log(text) { 33 | console.log(text); 34 | this.explanation.push(text); 35 | this.component.setState({ 36 | explanation: _.assign([],this.explanation) 37 | }); 38 | } 39 | 40 | /** 41 | * Start the search process 42 | * 43 | * @param {NodeModel} startNode 44 | * 45 | * @return {Promise} 46 | */ 47 | start(startNode) { 48 | if (!this.nodes || this.nodes.length === 0) { 49 | this.log(' | No nodes to search!'); 50 | return Promise.reject(new Error('No Nodes to search!')); 51 | } 52 | startNode = startNode || this.nodes[0]; 53 | 54 | this.log(` | Start at ${startNode.id}`); 55 | return this.search(startNode) 56 | .then(() => { 57 | this.log(' | Search Complete!'); 58 | let unvisitedPoints = this.nodes.filter((n) => !n.visited); 59 | let articulationPoints = this.nodes.filter((n) => n.articulationPoint); 60 | if (unvisitedPoints.length === 0 && articulationPoints.length === 0) { 61 | this.log(' | This graph is Biconnected!'); 62 | } else { 63 | this.log(' | This graph is not Biconnected'); 64 | if (articulationPoints.length > 0) { 65 | this.log(` | Articulation Points: ${articulationPoints.map((n) => n.id).join(', ')}`); 66 | } 67 | if (unvisitedPoints.length > 0) { 68 | this.log(` | Unvisited Points: ${unvisitedPoints.map((n) => n.id).join(', ')}`); 69 | } 70 | } 71 | }); 72 | } 73 | 74 | /** 75 | * Update the component's state with the nodes array that 76 | * reflects current status of search 77 | * 78 | * @param {NodeModel} currentNode 79 | * 80 | * @return {Promise} 81 | */ 82 | updateComponent(currentNode, parentNode) { 83 | return Promise.resolve() 84 | .then(() => { 85 | let updateState = this.nodes.map((node) => { 86 | if (node.id === currentNode.id) { 87 | node = currentNode; 88 | node.current = true; 89 | } else { 90 | node.current = false; 91 | } 92 | return node; 93 | }); 94 | 95 | this.component.setState({ 96 | nodes: updateState, 97 | visitOrder: _.assign([], this.visitOrder) 98 | }); 99 | 100 | return Promise.delay(this.pause); 101 | }); 102 | } 103 | 104 | /** 105 | * Search a node 106 | * 107 | * - This function triggers an update to the UI 108 | * - If the node has not been visited it marks it as visited 109 | * - If the node has no children or all children have been visited it will move back to parent 110 | * - If the node has children that are still be visited it will move to next available child 111 | * 112 | * @param {NodeModel} node 113 | * 114 | * @return {Promise} 115 | */ 116 | search(node, parent) { 117 | this.log(` | Visit ${node.id}`); 118 | node.childVisitCount = 0; 119 | node.visited = true; 120 | 121 | node.visitIndex = this.currentVisitIndex++; 122 | node.lowLink = node.visitIndex; 123 | 124 | return this.updateComponent(node, parent) 125 | .then(() => { 126 | return promiseSequence(node.children.map((c) => () => { 127 | var child = this.nodes.find((n) => n.id === c); 128 | if (!child.visited) { 129 | node.childVisitCount++; 130 | child.visitedFrom = node.id; 131 | 132 | return this.search(child, node) 133 | .then(() => { 134 | node.lowLink = Math.min(node.lowLink, child.lowLink); 135 | 136 | if (node.visitedFrom === null && node.childVisitCount > 1) { 137 | this.log(' | Node is root and has more than one child - it\'s an articulation point!'); 138 | node.articulationPoint = true; 139 | } else if (node.visitedFrom !== null && child.lowLink >= node.visitIndex) { 140 | this.log(` | Node ${node.id} - parent ${node.visitedFrom}`); 141 | this.log(' | Node is not root but one of it\'s children has a back link to an ancestor - it\'s an articulation point!'); 142 | node.articulationPoint = true; 143 | } 144 | }); 145 | } else { 146 | return Promise.resolve() 147 | .then(() => { 148 | if (child.id !== node.visitedFrom) { 149 | node.lowLink = Math.min(node.lowLink, child.visitIndex); 150 | } 151 | }); 152 | } 153 | })); 154 | }); 155 | } 156 | } -------------------------------------------------------------------------------- /client/algorithms/BipartitenessAlgorithm.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Promise from 'bluebird'; 3 | 4 | import Queue from '../dataStructures/Queue'; 5 | import { promiseSequence, promiseWhile } from '../utils'; 6 | 7 | /** 8 | * Depth First Search Algorithm Class 9 | */ 10 | export default class BipartitenessAlgorithm { 11 | /** 12 | * @constructor 13 | * 14 | * @param {Array} nodes An array of NodeModel objects 15 | * @param {React/Component} component A React Component 16 | * @param {Number} pause 17 | */ 18 | constructor(nodes, component, pause = 500) { 19 | this.nodes = nodes; 20 | this.component = component; 21 | this.queue = new Queue(); 22 | this.visitOrder = []; 23 | this.explanation = []; 24 | this.pause = pause; 25 | } 26 | 27 | /** 28 | * Log a message (used for incremental updates about algorithms progress) 29 | * 30 | * @param {String} text 31 | */ 32 | log(text) { 33 | console.log(text); 34 | this.explanation.push(text); 35 | this.component.setState({ 36 | explanation: _.assign([],this.explanation) 37 | }); 38 | } 39 | 40 | /** 41 | * Start the search process 42 | * 43 | * @param {NodeModel} startNode 44 | * 45 | * @return {Promise} 46 | */ 47 | start(startNode) { 48 | if (!this.nodes || this.nodes.length === 0) { 49 | this.log(' | No nodes to search!'); 50 | return Promise.reject(new Error('No Nodes to search!')); 51 | } 52 | 53 | startNode = startNode || this.nodes[0]; 54 | 55 | let startNodes = [ startNode ].concat(this.nodes.filter((n) => n.parents.length === 0 && n.id !== startNode.id)); 56 | 57 | let searches = startNodes.map((n) => () => { 58 | this.log(` | Start at ${n.id}`); 59 | return this.search(n) 60 | }); 61 | 62 | return promiseSequence(searches) 63 | .then(() => { 64 | this.log(' | Queue empty, BFS complete!'); 65 | let groupings = _.uniq(this.nodes.map((n) => n.group)); 66 | if (groupings.length === 2) { 67 | this.log(' | This graph is a bipartite graph!'); 68 | this.log(' | Disjoint Sets') 69 | this.log(` | Set 1: ${this.nodes.filter((n) => n.group === 1).map((n) => n.id).join(', ')}`) 70 | this.log(` | Set 2: ${this.nodes.filter((n) => n.group === 2).map((n) => n.id).join(', ')}`) 71 | } else { 72 | this.log(' | This graph is not a bipartite graph!'); 73 | this.log(` | ${groupings.length} groups had to be used`); 74 | } 75 | }) 76 | .catch((e) => { 77 | this.log(e); 78 | }); 79 | } 80 | 81 | /** 82 | * Update the component's state with the nodes array that 83 | * reflects current status of search 84 | * 85 | * @param {NodeModel} currentNode 86 | * 87 | * @return {Promise} 88 | */ 89 | updateComponent(currentNode, parentId) { 90 | return Promise.resolve() 91 | .then(() => { 92 | let updateState = this.nodes.map((node) => { 93 | node.current = (node.id === currentNode.id); 94 | if (parentId && node.id === currentNode.id) { 95 | node.visitedFrom = parentId; 96 | } 97 | return node; 98 | }); 99 | 100 | this.component.setState({ 101 | nodes: updateState, 102 | visitOrder: _.assign([], this.visitOrder) 103 | }); 104 | 105 | return Promise.delay(this.pause); 106 | }); 107 | } 108 | 109 | /** 110 | * Search a node 111 | * 112 | * - This function triggers an update to the UI 113 | * - If the node has not been visited it marks it as visited 114 | * - If the node has no children or all children have been visited it will move back to parent 115 | * - If the node has children that are still be visited it will move to next available child 116 | * 117 | * @param {NodeModel} node 118 | * 119 | * @return {Promise} 120 | */ 121 | search(node) { 122 | if (!node) { 123 | return; 124 | } 125 | 126 | return this.updateComponent(node) 127 | .then(() => { 128 | if (!node.visited) { 129 | this.log(` | Enqueued ${node.id}`); 130 | this.queue.enqueue([node.id, null]); 131 | } 132 | 133 | return promiseWhile(this.queue.isNotEmpty.bind(this.queue), () => { 134 | let edge = this.queue.dequeue(); 135 | let currentNode = this.nodes.find((n) => n.id === edge[0]); 136 | let parentId = null; 137 | if (!currentNode.visited) { 138 | parentId = edge[1]; 139 | this.markAsVisited(currentNode); 140 | this.enqueueUnvisitedChildren(currentNode); 141 | } 142 | this.groupNode(currentNode); 143 | return this.updateComponent(currentNode, parentId); 144 | }); 145 | }); 146 | } 147 | 148 | enqueueUnvisitedChildren(parent) { 149 | let sortedChildren = this.sortChildren(parent); 150 | sortedChildren.forEach((childId) => { 151 | let node = this.nodes.find((n) => n.id === childId); 152 | if (!node.visited) { 153 | this.log(` | Enqueued ${node.id}`); 154 | this.queue.enqueue([node.id, parent.id]); 155 | } 156 | }); 157 | } 158 | 159 | groupNode(node) { 160 | if (node.parents.length === 0) { 161 | this.log(` | - set node ${node.id} to group 1`); 162 | node.group = 1; 163 | } else { 164 | let parentGroups = _.sortedUniq( 165 | node.parents 166 | .map((p) => { 167 | let parent = this.nodes.find((n) => n.id === p); 168 | return parent.group; 169 | }) 170 | .filter((n) => n !== null) 171 | .sort() 172 | ); 173 | 174 | if (parentGroups.length === 0) { 175 | this.log(` | - set node ${node.id} to group 1`); 176 | node.group = 1; 177 | } else if (parentGroups.length === 1) { 178 | let group = (parentGroups[0] === 1) ? 2 : 1; 179 | this.log(` | - set node ${node.id} to group ${group}`); 180 | node.group = group; 181 | } else { 182 | console.log('parentGroups not 1 or 0', parentGroups); 183 | let group = parentGroups[parentGroups.length-1] + 1; 184 | this.log(` | - set node ${node.id} to group ${group}`); 185 | node.group = group; 186 | } 187 | } 188 | } 189 | 190 | /** 191 | * Marks a Node as visited, updates the stack and output order 192 | * 193 | * @param {NodeModel} node 194 | * @param {NodeModel} parent 195 | */ 196 | markAsVisited(node, parent) { 197 | this.log(` | Visit ${node.id}`); 198 | node.visited = true; 199 | this.visitOrder.push(node); 200 | } 201 | 202 | /** 203 | * Sorts the children of a NodeModel by natural ordering 204 | * 205 | * @param {NodeModel} node 206 | * 207 | * @return {Array} 208 | */ 209 | sortChildren(node) { 210 | return node.children.sort((a,b) => { 211 | if (a < b) return -1; 212 | if (a > b) return 1; 213 | return 0; 214 | }); 215 | } 216 | } -------------------------------------------------------------------------------- /client/algorithms/BreadthFirstSearch.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Promise from 'bluebird'; 3 | 4 | import Queue from '../dataStructures/Queue'; 5 | import { promiseWhile } from '../utils'; 6 | 7 | /** 8 | * Depth First Search Algorithm Class 9 | */ 10 | export default class BreadthFirstSearch { 11 | /** 12 | * @constructor 13 | * 14 | * @param {Array} nodes An array of NodeModel objects 15 | * @param {React/Component} component A React Component 16 | * @param {Number} pause 17 | */ 18 | constructor(nodes, component, pause = 500) { 19 | this.nodes = nodes; 20 | this.component = component; 21 | this.queue = new Queue(); 22 | this.visitOrder = []; 23 | this.explanation = []; 24 | this.pause = pause; 25 | } 26 | 27 | /** 28 | * Log a message (used for incremental updates about algorithms progress) 29 | * 30 | * @param {String} text 31 | */ 32 | log(text) { 33 | console.log(text); 34 | this.explanation.push(text); 35 | this.component.setState({ 36 | explanation: _.assign([],this.explanation) 37 | }); 38 | } 39 | 40 | /** 41 | * Start the search process 42 | * 43 | * @param {NodeModel} startNode 44 | * 45 | * @return {Promise} 46 | */ 47 | start(startNode) { 48 | if (!this.nodes || this.nodes.length === 0) { 49 | this.log(' | No nodes to search!'); 50 | return Promise.reject(new Error('No Nodes to search!')); 51 | } 52 | startNode = startNode || this.nodes[0]; 53 | 54 | this.log(` | Start at ${startNode.id}`); 55 | return this.search(startNode) 56 | .then(() => { 57 | this.log(' | Queue empty, BFS complete!'); 58 | this.log(' | Search Steps'); 59 | this.log(' | ------------'); 60 | this.visitOrder.forEach((node, id) => this.log(` | Step ${id+1} -> Node ${node.id}`) ); 61 | }) 62 | .catch((e) => { 63 | this.log(e); 64 | }); 65 | } 66 | 67 | /** 68 | * Update the component's state with the nodes array that 69 | * reflects current status of search 70 | * 71 | * @param {NodeModel} currentNode 72 | * 73 | * @return {Promise} 74 | */ 75 | updateComponent(currentNode, parentId) { 76 | return Promise.resolve() 77 | .then(() => { 78 | let updateState = this.nodes.map((node) => { 79 | node.current = (node.id === currentNode.id); 80 | if (parentId && node.id === currentNode.id) { 81 | node.visitedFrom = parentId; 82 | } 83 | return node; 84 | }); 85 | 86 | this.component.setState({ 87 | nodes: updateState, 88 | visitOrder: _.assign([], this.visitOrder) 89 | }); 90 | 91 | return Promise.delay(this.pause); 92 | }); 93 | } 94 | 95 | /** 96 | * Search a node 97 | * 98 | * - This function triggers an update to the UI 99 | * - If the node has not been visited it marks it as visited 100 | * - If the node has no children or all children have been visited it will move back to parent 101 | * - If the node has children that are still be visited it will move to next available child 102 | * 103 | * @param {NodeModel} node 104 | * 105 | * @return {Promise} 106 | */ 107 | search(node) { 108 | if (!node) { 109 | return; 110 | } 111 | 112 | return this.updateComponent(node) 113 | .then(() => { 114 | if (!node.visited) { 115 | this.log(` | Enqueued ${node.id}`); 116 | this.queue.enqueue([node.id, null]); 117 | } 118 | 119 | return promiseWhile(this.queue.isNotEmpty.bind(this.queue), () => { 120 | let edge = this.queue.dequeue(); 121 | let currentNode = this.nodes.find((n) => n.id === edge[0]); 122 | let parentId = null; 123 | if (!currentNode.visited) { 124 | parentId = edge[1]; 125 | this.markAsVisited(currentNode); 126 | this.enqueueUnvisitedChildren(currentNode); 127 | } 128 | 129 | return this.updateComponent(currentNode, parentId); 130 | }); 131 | }); 132 | } 133 | 134 | enqueueUnvisitedChildren(parent) { 135 | let sortedChildren = this.sortChildren(parent); 136 | sortedChildren.forEach((childId) => { 137 | let node = this.nodes.find((n) => n.id === childId); 138 | if (!node.visited) { 139 | this.log(` | Enqueued ${node.id}`); 140 | this.queue.enqueue([node.id, parent.id]); 141 | } 142 | }); 143 | } 144 | 145 | /** 146 | * Marks a Node as visited, updates the stack and output order 147 | * 148 | * @param {NodeModel} node 149 | */ 150 | markAsVisited(node) { 151 | this.log(` | Visit ${node.id}`); 152 | node.visited = true; 153 | this.visitOrder.push(node); 154 | } 155 | 156 | /** 157 | * Sorts the children of a NodeModel by natural ordering 158 | * 159 | * @param {NodeModel} node 160 | * 161 | * @return {Array} 162 | */ 163 | sortChildren(node) { 164 | return node.children.sort((a,b) => { 165 | if (a < b) return -1; 166 | if (a > b) return 1; 167 | return 0; 168 | }); 169 | } 170 | } -------------------------------------------------------------------------------- /client/algorithms/DepthFirstSearch.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Promise from 'bluebird'; 3 | 4 | /** 5 | * Depth First Search Algorithm Class 6 | */ 7 | export default class DepthFirstSearch { 8 | /** 9 | * @constructor 10 | * 11 | * @param {Array} nodes An array of NodeModel objects 12 | * @param {React/Component} component A React Component 13 | * @param {Number} pause 14 | */ 15 | constructor(nodes, component, pause = 500) { 16 | this.nodes = nodes; 17 | this.component = component; 18 | this.stack = []; 19 | this.visitOrder = []; 20 | this.explanation = []; 21 | this.pause = pause; 22 | } 23 | 24 | /** 25 | * Log a message (used for incremental updates about algorithms progress) 26 | * 27 | * @param {String} text 28 | */ 29 | log(text) { 30 | console.log(text); 31 | this.explanation.push(text); 32 | this.component.setState({ 33 | explanation: _.assign([],this.explanation) 34 | }); 35 | } 36 | 37 | /** 38 | * Start the search process 39 | * 40 | * @param {NodeModel} startNode 41 | * 42 | * @return {Promise} 43 | */ 44 | start(startNode) { 45 | if (!this.nodes || this.nodes.length === 0) { 46 | this.log(' | No nodes to search!'); 47 | return Promise.reject(new Error('No Nodes to search!')); 48 | } 49 | startNode = startNode || this.nodes[0]; 50 | 51 | this.log(` | Start at ${startNode.id}`); 52 | return this.search(startNode) 53 | .catch((e) => { 54 | this.log(e.message); 55 | this.log(' | Search Steps'); 56 | this.log(' | ------------'); 57 | this.visitOrder.forEach((node, id) => this.log(` | Step ${id+1} -> Node ${node.id}`) ); 58 | }); 59 | } 60 | 61 | /** 62 | * Update the component's state with the nodes array that 63 | * reflects current status of search 64 | * 65 | * @param {NodeModel} currentNode 66 | * 67 | * @return {Promise} 68 | */ 69 | updateComponent(currentNode, parentNode) { 70 | return Promise.resolve() 71 | .then(() => { 72 | let updateState = this.nodes.map((node) => { 73 | if (node.id === currentNode.id) { 74 | node.current = true; 75 | if (parentNode) { 76 | node.visitedFrom = parentNode.id; 77 | } 78 | } else { 79 | node.current = false; 80 | } 81 | 82 | return node; 83 | }); 84 | this.component.setState({ 85 | nodes: updateState, 86 | visitOrder: _.assign([], this.visitOrder) 87 | }); 88 | 89 | return Promise.delay(this.pause); 90 | }); 91 | } 92 | 93 | /** 94 | * Search a node 95 | * 96 | * - This function triggers an update to the UI 97 | * - If the node has not been visited it marks it as visited 98 | * - If the node has no children or all children have been visited it will move back to parent 99 | * - If the node has children that are still be visited it will move to next available child 100 | * 101 | * @param {NodeModel} node 102 | * 103 | * @return {Promise} 104 | */ 105 | search(node, parent) { 106 | if (parent) { 107 | console.log('Parent set', 'parent is', parent.id, 'child is', node.id); 108 | } 109 | if (!node) { 110 | return; 111 | } 112 | 113 | return this.updateComponent(node, parent) 114 | .then(() => { 115 | if (!node.visited) { 116 | this.markAsVisited(node); 117 | } 118 | 119 | if (!node.hasChildren() || this.allChildrenVisited(node)) { 120 | return this.moveToParent(); 121 | } else { 122 | return this.moveToNextChild(); 123 | } 124 | }); 125 | } 126 | /** 127 | * Returns a node that sits at the top of the stack 128 | * 129 | * @return {NodeModel} [description] 130 | * 131 | * @throws {Error} If the stack is empty 132 | */ 133 | peek() { 134 | if (this.stack.length === 0) { 135 | throw new Error(' | Stack empty, DFS complete!'); 136 | } 137 | 138 | return this.stack[this.stack.length - 1]; 139 | } 140 | 141 | /** 142 | * Moves back to the parent node and triggers a UI update 143 | * 144 | * @return {Promise} 145 | */ 146 | moveToParent() { 147 | this.log(' | Move Up!'); 148 | this.stack.pop(); 149 | let currentNode = this.peek(); 150 | this.updateComponent(currentNode); 151 | return this.search(currentNode); 152 | } 153 | 154 | /** 155 | * Moves to the next available child 156 | * 157 | * @return {Promise} 158 | */ 159 | moveToNextChild() { 160 | this.log(' | Move Down!'); 161 | let parent = this.peek(); 162 | let sortedChildren = this.sortChildren(parent); 163 | 164 | let unvisitedChildren = sortedChildren.filter((x) => { 165 | let node = this.nodes.find((n) => n.id === x); 166 | return !node.visited; 167 | }); 168 | 169 | if (unvisitedChildren.length === 0) { 170 | return Promise.resolve(); 171 | } else { 172 | let nextId = unvisitedChildren.pop(); 173 | let next = this.nodes.find((x) => x.id === nextId); 174 | return this.search(next, parent); 175 | } 176 | } 177 | 178 | /** 179 | * Marks a Node as visited, updates the stack and output order 180 | * 181 | * @param {NodeModel} node 182 | */ 183 | markAsVisited(node) { 184 | this.log(` | Visit ${node.id}`); 185 | node.visited = true; 186 | this.stack.push(node); 187 | this.visitOrder.push(node); 188 | } 189 | 190 | /** 191 | * Sorts the children of a NodeModel by natural ordering 192 | * 193 | * @param {NodeModel} node 194 | * 195 | * @return {Array} 196 | */ 197 | sortChildren(node) { 198 | return node.children.sort((a,b) => { 199 | if (a > b) return -1; 200 | if (a < b) return 1; 201 | return 0; 202 | }); 203 | } 204 | 205 | /** 206 | * Checks to see if all children of a NodeModel has been visited or not 207 | * 208 | * @param {NodeModel} node 209 | * 210 | * @return {Boolean} 211 | */ 212 | allChildrenVisited(node) { 213 | let output = node.children.reduce((prev, current) => { 214 | let currentNode = this.nodes.find((x) => x.id === current); 215 | return prev && currentNode.visited; 216 | }, true); 217 | return output; 218 | } 219 | } -------------------------------------------------------------------------------- /client/algorithms/TarjanStrongConnection.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Promise from 'bluebird'; 3 | 4 | import { promiseSequence } from '../utils'; 5 | import Stack from '../dataStructures/Stack'; 6 | 7 | /** 8 | * Tarjan Strongly Connected Components Graph Algorithm Class 9 | */ 10 | export default class TarjanStrongConnection { 11 | /** 12 | * @constructor 13 | * 14 | * @param {Array} nodes An array of NodeModel objects 15 | * @param {React/Component} component A React Component 16 | * @param {Number} pause 17 | */ 18 | constructor(nodes, component, pause = 500) { 19 | this.nodes = nodes; 20 | this.component = component; 21 | this.visitIndex = 0; 22 | this.stack = new Stack(); 23 | this.visitOrder = []; 24 | this.explanation = []; 25 | this.components = []; 26 | this.pause = pause; 27 | } 28 | 29 | /** 30 | * Log a message (used for incremental updates about algorithms progress) 31 | * 32 | * @param {String} text 33 | */ 34 | log(text) { 35 | console.log(text); 36 | this.explanation.push(text); 37 | this.component.setState({ 38 | explanation: _.assign([],this.explanation) 39 | }); 40 | } 41 | 42 | /** 43 | * Start the search process 44 | * 45 | * @param {NodeModel} startNode 46 | * 47 | * @return {Promise} 48 | */ 49 | start(startNode) { 50 | if (!this.nodes || this.nodes.length === 0) { 51 | this.log(' | No nodes to search!'); 52 | return Promise.reject(new Error('No Nodes to search!')); 53 | } 54 | 55 | let searchNodes = this.nodes.map((node) => () => this.search(node)); 56 | 57 | return promiseSequence(searchNodes) 58 | .then(() => { 59 | this.log(' | Strongly Connected Components'); 60 | this.log(' | ------------'); 61 | this.components.forEach((component, id) => this.log(` | Component ${id} -> ${component.join(', ')}`) ); 62 | this.component.setState({ 63 | components: _.assign([], this.components) 64 | }); 65 | }); 66 | } 67 | 68 | /** 69 | * Update the component's state with the nodes array that 70 | * reflects current status of search 71 | * 72 | * @param {NodeModel} currentNode 73 | * 74 | * @return {Promise} 75 | */ 76 | updateComponent(currentNode, parentNode) { 77 | return Promise.resolve() 78 | .then(() => { 79 | let updateState = this.nodes.map((node) => { 80 | if (node.id === currentNode.id) { 81 | node.current = true; 82 | if (parentNode) { 83 | node.visitedFrom = parentNode.id; 84 | } 85 | } else { 86 | node.current = false; 87 | } 88 | 89 | return node; 90 | }); 91 | this.component.setState({ 92 | nodes: updateState, 93 | components: _.assign([], this.components), 94 | visitOrder: _.assign([], this.visitOrder) 95 | }); 96 | 97 | return Promise.delay(this.pause); 98 | }); 99 | } 100 | 101 | /** 102 | * Search a node 103 | * 104 | * - This function triggers an update to the UI 105 | * - If the node has not been visited it marks it as visited 106 | * - If the node has no children or all children have been visited it will move back to parent 107 | * - If the node has children that are still be visited it will move to next available child 108 | * 109 | * @param {NodeModel} node 110 | * 111 | * @return {Promise} 112 | */ 113 | search(node, parent) { 114 | if (!node) { 115 | return Promise.resolve(); 116 | } 117 | 118 | if (node.visitIndex !== null) { 119 | return Promise.resolve(); 120 | } 121 | 122 | return this.updateComponent(node, parent) 123 | .then(() => { 124 | this.markAsVisited(node); 125 | 126 | let childTasks = node.children.map((childId) => { 127 | let child = this.nodes.find((x) => x.id === childId); 128 | if (child.visitIndex === null) { 129 | return () => { 130 | return this.search(child, node).then(() => { 131 | let updatedChild = this.nodes.find((x) => x.id === childId); 132 | node.lowLink = Math.min(node.lowLink, updatedChild.lowLink); 133 | return Promise.resolve(); 134 | }); 135 | }; 136 | } else if (child.onStack) { 137 | return () => { 138 | node.lowLink = Math.min(node.lowLink, child.visitIndex); 139 | return Promise.resolve(); 140 | }; 141 | } else { 142 | return () => { 143 | return Promise.resolve(); 144 | }; 145 | } 146 | }); 147 | 148 | return promiseSequence(childTasks) 149 | .then(() => { 150 | if (node.lowLink === node.visitIndex) { 151 | let component = [ node.id ]; 152 | let otherNodeId = this.stack.pop(); 153 | let otherNode = this.nodes.find((x) => x.id === otherNodeId); 154 | otherNode.onStack = false; 155 | while (otherNodeId !== node.id) { 156 | component.push(otherNode.id); 157 | otherNodeId = this.stack.pop(); 158 | otherNode = this.nodes.find((x) => x.id === otherNodeId); 159 | otherNode.onStack = false; 160 | } 161 | this.components.push(component); 162 | return Promise.resolve(); 163 | } 164 | }); 165 | }); 166 | } 167 | 168 | /** 169 | * Marks a Node as visited, updates the stack and output order 170 | * 171 | * @param {NodeModel} node 172 | */ 173 | markAsVisited(node) { 174 | this.log(` | Visit ${node.id}`); 175 | node.visited = true; 176 | node.visitIndex = this.visitIndex; 177 | node.lowLink = this.visitIndex; 178 | node.onStack = true; 179 | this.visitIndex++; 180 | this.stack.push(node.id); 181 | this.visitOrder.push(node); 182 | } 183 | } -------------------------------------------------------------------------------- /client/blog/nodes.md: -------------------------------------------------------------------------------- 1 | Ingredients: 2 | - variety of teaching oneself engineering 3 | - veriety of ways of actually engineering stuff - front end/back-end/hardware 4 | 5 | One of the things I noticed whilst reading these pieces, and their comment streams, was that a 6 | sizeable proportion of developers felt frustration when they discovered in an interview that were to be quizzed 7 | on Computer Science 101. 8 | 9 | 10 | Needs to represent the dynamic nature of the state of Graph Search -------------------------------------------------------------------------------- /client/blog/references.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ShareButtons from '../components/ShareButtons'; 3 | 4 | export default (
5 |

The real world uses of testing whether a graph is bipartite, but it is a good introduction to the topic of 6 | Graph Coloring which is an interesting area of study.

7 | 8 |

Pseudocode

9 |

This algorithm, because it is an extension of BFS, uses a queue to maintain which node to visit next. For each 10 | node it assigns a color based upon the colour(s) assigned to it's parents. Once all nodes have been assigned a color 11 | if we end with using 2 colours, then the graph is bipartite and its nodes can be seperated into two disjoint sets.

12 | 13 | function bipartiteness( graph )
14 |   queue = new Queue()
15 |
16 |   for ( node in graph.nodesWithoutParents() )
17 |       search( node )
18 |
19 |   return graph.groups().length == 2
20 |
21 |   function search( node )
22 |     queue.enqueue( node )
23 |
24 |     while ( !queue.empty() )
25 |       current = queue.dequeue()
26 |
27 |       if ( !current.visited )
28 |         current.visited = true
29 |         for ( child in current.unvisitedChildren() )
30 |           queue.enqueue( child )
31 |
32 |       parentGroups = current.parentGroupsSet()
33 |
34 |       if ( parentGroups.length == 0 )
35 |         current.group = 1
36 |
37 |       else if ( parentGroups.length == 1 )
38 |         current.group = ( parentGroups[0] == 1 ) ? 2 : 1
39 |
40 |       else
41 |         current.group = parentGroups[parentGroups.length - 1] + 1
42 |
43 | 44 |

Motivations

45 | 46 |

It's quite safe to say that there is a lot of pent up frustration towards the hiring process in the software 47 | industry today [1 48 | , 2 49 | , 3 50 | , 4 51 | ]. With the plethora of pathways one can take nowadays to become a software developer 52 | combined with how one can choose to apply themselves there is increasing chance for ambiguity when it comes to how 53 | to access the suitability of a candidate for a job.

54 | 55 |

One of the topics that is often raised in these pieces is around some of the foundations of Computer Science - 56 | namely Data Structures and Algorithms. Whilst I am conflicted on their value in technical interviews 57 | I found myself wondering about how we could help people that struggle with these concepts - I'd put myself in this pool. 58 | In my opinion the delivery mechanisms used for educating someone about these topics are outdated and could be improved 59 | (I always used to grow frustrated how lecture slides would often rely upon singular examples to explain algorithms). 60 | Perhaps with a bit more thought to how to improve the learning experience we can help more people understand these basic concepts 61 | that are essentially the building blocks for the subject of Computer Science.

62 | 63 |

So I decided to create something that explains some of these concepts. The target requirements I felt were 64 | important to maximise value and minimise complexity were the following:

65 | 66 | 74 | 75 |

This blog marks the beginning. I've decided to focus on the topic of graph search algorithms here. The hope is 76 | that anyone that is struggling with these algorithms and their applications can gain a better understanding 77 | through running the algorithm and its variations with their own graphs and trees.

78 | 79 |

By enabling readers to test out an algorithm with their own graph/tree structures I believe the reader can 80 | learn and grok the concepts more efficiently. In many ways this reflects the intentions 81 | of Workshape, in so far as, we try to limit the dependence of words when 82 | transferring information in order to minimise ambiguity!

83 | 84 |

It must be said that there are already some great resources out there for teaching people about these types of complex subject 85 | matter with visual cues. I have taken inspiration from two in particular:

86 | 87 |
    88 |
  1. Visualgo - a very comprehensive resource for the visualisation 89 | of algorithms and data structures. Kudos to the team behind this.
  2. 90 |
  3. A Visual Introduction to 91 | Machine Learning - a personal favourite that tells the story of how various statistical techniques 92 | can be used to build models to make predictions to classifcation problems - the visual explanation is 93 | triggered by scrolling!
  4. 94 |
95 | 96 |

Implementation

97 |

For those who want to take a look at the source code, 98 | check it out on our Github. It is written using React, D3 and Bluebird (Promise implementation).

99 | 100 |

Links

101 |
    102 |
  1. F**k You, I Quit - Hiring is Broken
  2. 103 |
  3. So Hiring is Broken - Let's Fix It
  4. 104 |
  5. Hiring is Broken... And It Isn't Worth Fixing
  6. 105 |
  7. Why is hiring broken? It starts at the whiteboard
  8. 106 |
107 | 108 |
109 |

Feedback

110 |

If you have any feedback and would like to share it, or if you see any errors please email Gordon at gordon [at] workshape [dot] io. Also, if you would be interested in 111 | contributing in further content like this please do get in touch!

112 |
113 | 114 |
115 | Made with 116 | 117 | 118 | by 119 | WorkShape.io 120 | 121 | 122 |
123 | 124 | 125 |
); -------------------------------------------------------------------------------- /client/blog/section1.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (
4 |

Introduction

5 |

Graph Traversal Algorithms are a key topic of study in Computer Science and lay a foundation for 6 | Artificial Intelligence. The various algorithms in this field are used to perform search upon 7 | graph or tree data structures and we unknowingly rely upon these techniques on a daily basis with 8 | our electronically augmented lives.

9 | 10 |

Whilst an appreciation of how they work may be fresh on the minds of recent Comp Sci graduates it 11 | may be a mystery to others. Considering this subject frequently seems to come up in interview processes 12 | for various software engineers roles we thought it would be a good idea to experiment on building a resource 13 | to help anyone understand how some of these algorithms work.

14 | 15 |

In this blog we look at Depth First Search plus Breadth First Search. This resource is interactive and 16 | readers can use the visualisations to see how the algorithms can be applied to search 17 | graphs and solve certain problems. You can run it against various examples or they can draw your own graphs.

18 | 19 |
20 |

Creating your own graph

21 |

You can draw a new node by clicking anywhere on the visualisation canvas. To add an edge 22 | between nodes you have to drag your mouse from one node to another.

23 |
24 |
); -------------------------------------------------------------------------------- /client/blog/section2.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (
4 |

Depth First Search

5 | 6 |

When it comes to algorithms Depth First Search 7 | (DFS) is one of the first things students will be taught at university and it is a gateway for many other important 8 | topics in Computer Science. It is an algorithm for searching or traversing Graph and Tree data structures just like 9 | it's sibling Breadth First Search (BFS).

10 | 11 |

If you run the visualisation below you can see how it works - you can run it on example graphs, or alternatively 12 | draw your own - add nodes by clicking on the canvas below and add edges by dragging from one node to another.

13 |
); -------------------------------------------------------------------------------- /client/blog/section3.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (
4 |

You can see in the visualisation after the algorithm completes the edges that the algorithm traversed in order to first visit a node. 5 | It is important to take note of this when comparing to Breadth First Search. The visit index of each node is labelled in red.

6 | 7 |

Pseudocode

8 |

This pseudocode encapsulates the main principle of DFS using a stack and recursive function calls to explore down a pathway to a leaf node before 9 | backtracking, using a stack, and looking for other routes to other unvisited children.

10 | 11 | function dfs( graph, node )
12 |   stack = new Stack()
13 |   search(node)
14 |
15 |   function search( node )
16 |     if ( !node )
17 |       return
18 |
19 |     if ( !node.visited )
20 |       stack.push( node )
21 |       node.visited = true
22 |
23 |     if ( graph.nodeHasUnvisitedChildren( node ) )
24 |       search( graph.nextUnvisitedChildOf( node ) )
25 |     else
26 |       search( stack.pop() )
27 |
28 | 29 |

The additional functionality in the visualisation for tracking the order in which each node is visited is omitted.

30 | 31 |

Breadth First Search

32 | 33 |

Whilst DFS goes as deep as possible until it reaches a dead end and then back tracks BFS queues up all nodes reachable at a certain depth 34 | and then increases the depth until all nodes are visited. Check out the visualisation below, the same example graph is loaded, pay attention 35 | to the different visit order and edges traversed by each algorithm.

36 |
); -------------------------------------------------------------------------------- /client/blog/section4.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (
4 |

Because the visualisation highlights the edges that are traversed by BFS you can see that it traverses the shortest path to each node - this property of 5 | BFS makes it a good approach for this exact purpose.

6 | 7 |

Pseudocode

8 |

This pseudocode encapsulates the main principle of BFS using a queue to store nodes that are ready to be visited 9 | process and recursive function calls visit all reachable nodes.

10 | 11 | function bfs( graph, node )
12 |   stack = new Queue()
13 | 14 |   queue.enqueue( node )
15 |
16 |   while (queue.notEmpty())
17 |     currentNode = queue.dequeue()
18 |
19 |     if (!currentNode.visited)
20 |       currentNode.visited = true
21 |
22 |       while ( child = graph.nextUnvisitedChildOf( node ) )
23 |         queue.enqueue( child )
24 |
25 | 26 |

Observations Depth First Search and Breadth First Search

27 |

If you have run both algorithms on the same structure you can make a few observations:

28 |
    29 |
  1. The nodes of the graph are visited in a different order (if the depth is greater than 2)
  2. 30 |
  3. If there are multiple edges to a child node, the algorithms will traverse different edges
  4. 31 |
  5. BFS will always traverse the shortest path (based upon intermediate nodes visited) between two nodes
  6. 32 |
  7. Each algorithm will only traverse nodes reachable from the start node, disconnected components are not traversed. You can change the start node to see how this effects what is discovered.
  8. 33 |
34 | 35 |

Applications of Depth First Search

36 | 37 |

The DFS algorithm by itself does not seem immediately useful, in comparison to BFS which is capable of finding shortest paths in nodes. In order to appreciate 38 | its usefulness you have to look to some of the ways in which it can be extended to be applied to certain problem domains. In this blog I am going to demonstrate 39 | two examples, finding strongly connected components, and identifying whether a graph is biconnected or not.

40 | 41 |

Finding Strongly Connected Components

42 | 43 |

A Strongly Connected Component (SCC) is a subgraph where all nodes are reachable from every other node in the group. Robert Tarjan, a 44 | Professor of Computer Science from the US, created an algorithm for identifying SCCs in linear time, O(N), that is based upon DFS. If you take a look at the visualisation below you 45 | can see algorithm identify the SCCs in a graph.

46 |
); -------------------------------------------------------------------------------- /client/blog/section5.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (
4 |

In the real world the uses of finding SCCs can be useful for analysis and understanding of networks. For 5 | example (just off the top of my head):

6 | 13 |

Pseudocode

14 |

This algorithm builds upon DFS using a stack and recursion to explore a pathway until it finds a leaf node or 15 | a node already visisted. Unlike DFS though this algorithm does not take a start point, instead it ensures all 16 | nodes are visited by instigating search on all nodes. At the end of each search cycle if the value of 17 | node.visitIndex is the same as node.lowLink then it creates a new SCC and iteratively pops from the stack and adds 18 | these nodes to the SCC too until it reaches itself again.

19 | 20 | 21 | function tarjan-scc( graph )
22 |   stack = new Stack()
23 |   visitIndex = 0
24 |   componentIndex = 0
25 |
26 |   for ( node in graph )
27 |     if( !v.visitIndex )
28 |       search( node )
29 |
30 |   function search( node )
31 |     node.visitIndex = visitIndex
32 |     node.lowLink = visitIndex
33 |     node.onStack = true
34 |
35 |     stack.push( node )
36 |     visitIndex = visitIndex + 1
37 |
38 |     for ( child in graph.childOf( node ) )
39 |       if ( !child.visitIndex )
40 |         search( child )
41 |         node.lowLink = min( node.lowLink, child.lowLink )
42 |       else if ( child.onStack )
43 |         node.lowLink = min( node.lowLink, child.visitIndex )
44 |
45 |     if (node.lowLink == node.visitIndex)
46 |       node.componentIndex = componentIndex
47 |       componentIndex = componentIndex + 1
48 |
49 |       do
50 |         member = stack.pop()
51 |         member.onStack = false
52 |         member.componentIndex = node.componentIndex
53 |       while(node !== member)
54 |
55 | 56 |

Identifying if a Graph is Biconnected

57 |

A graph is biconnected if any node can be 58 | removed and yet the graph remains connected (all nodes can still be reached from all other nodes). The key to 59 | identifying is a graph is biconnected or not, is by searching for the presence of articulation points, or cut 60 | vertices. These are points, that if removed from a graph results in an increase in the number of connected components 61 | within a graph and basically means that without their presence all remaining nodes would not be reachable from one 62 | another.

63 | 64 |

The most basic way to search for articulation points in a graph is to take a brute force approach and play out 65 | all scenerios. Each node is removed from the graph and we then see if remaining nodes are all connected, if they 66 | are not then the node that was removed is an articulation point. We place the removed node back in and then repeat 67 | the process for all remaining nodes.

68 | 69 |

Whilst this approach will works its time complexity is relatively inefficient at O(N^2). There is an 70 | alternative though that is an extension of DFS that's time complexity is O(N). This algorithm is demonstrated 71 | in the visualisation below.

72 |
); -------------------------------------------------------------------------------- /client/blog/section6.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (
4 |

The major real world use of identifying if a graph is biconnected or not is about identifying whether a graph 5 | has redundancy in place if a node fails. This is especially important in communication network design and travel 6 | networks where it ensures resilience to one node failing.

7 | 8 |

Pseudocode

9 |

Similarly to Tarjan's Strongly Connected Components Algorithm we maintain two important values for each node when traversing 10 | the graph, lowLink and visitIndex with the additional property of the number of children visited. There are two 11 | specific situations that indicate whether a node is an articulation point:

12 | 13 |
    14 |
  1. The node is a the root of the DFS search tree and two or more of its children are visisted from it.
  2. 15 |
  3. The node is not a root but the lowLink value of ones of its children is more than its on visit 16 | index.
  4. 17 |
18 | 19 |

We do need to maintain a stack in this algorithm but we do make use of recursive function calls to traverse 20 | down a path of the graph.

21 | 22 | function aps( graph, node )
23 |   stack = new Stack()
24 |   visitIndex = 0
25 |
26 |   search( node )
27 |
28 |   function search( node )
29 |     node.visitIndex = visitIndex
30 |     node.lowLink = visitIndex
31 |     node.childVisitCount = 0
32 |
33 |     visitIndex = visitIndex + 1
34 |
35 |     for ( child in graph.childOf( node ) )
36 |       if ( !child.visitIndex )
37 |         node.childVisitCount = node.childVisitCount + 1
38 |         child.parent = node
39 |         search( child )
40 |
41 |         node.lowLink = min( node.lowLink, child.lowLink )
42 |
43 |         if ( !node.parent and node.childVisitCount > 1 )
44 |           node.articulationPoint = true
45 |
46 |         if ( node.parent and child.lowLink >= node.visitIndex )
47 |           node.articulationPoint = true
48 |
49 |       else if ( child != node.parent )
50 |         node.lowLink = min( node.lowLink, child.visitIndex )
51 |
52 | 53 |

Applications of Breadth First Search

54 |

As mentioned earlier BFS has usefulness out of the box as it can identify the shortest path between two nodes 55 | in an unweighted graph. You can see this behaviour by running the BFS example. In this blog we'll explore one 56 | other application of BFS, using it to test if a graph is bipartite or not.

57 | 58 |

Identifying is a Graph is Bipartite

59 |

A graph is bipartite if its nodes can be put into two disjoint sets such that every node in the first set 60 | connects to one or more in the second. If you check out the visualisation below you can see a variation of BFS 61 | that has been applied to see if a graph can be broken into these two disjoint sets. When a node is visited it is 62 | coloured according the colour assigned to its parents: 63 |

64 | 69 | 70 |

At the end of the process if only 2 colors are used then the graph is bipartite.

71 |
); -------------------------------------------------------------------------------- /client/components/BlogHeading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './BlogHeading.styl'; 4 | import ShareButtons from './ShareButtons'; 5 | 6 | const BlogHeading = (props) => { 7 | return
8 | 9 |
10 | 11 |

A Visual Guide toGraph Traversal Algorithms

12 | 13 | By 14 | WorkShape.io 15 | 16 | 17 | 18 |
19 | 20 |
; 21 | }; 22 | 23 | export default BlogHeading; -------------------------------------------------------------------------------- /client/components/BlogHeading.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/variables.styl' 2 | 3 | .BlogHeading 4 | background lighten(color-code-bg, 10) 5 | height 100vh 6 | position relative 7 | background-image url(../../assets/cover-noise.png) 8 | background-size 150px 150px 9 | min-height 500px 10 | font-family 'Open Sans', Arial, sans-serif 11 | background-attachment fixed 12 | 13 | &:after 14 | content '' 15 | width 100% 16 | height 100% 17 | position absolute 18 | left 0 19 | top 0 20 | background-image url(../../assets/hexa-pattern.png) 21 | background-size 33px 39px 22 | opacity .2 23 | background-attachment fixed 24 | mix-blend-mode multiply 25 | 26 | .inner 27 | position absolute 28 | width 100% 29 | top 50% 30 | transform translateY(-50%) 31 | text-align center 32 | z-index 1 33 | 34 | @media screen and (max-width breakpoint-medium) 35 | margin-top (menu-width / 2) 36 | 37 | a 38 | color #fff 39 | // font-weight 100 40 | text-shadow rgba(#000, .2) 0 1px 1px 41 | font-weight 100 42 | font-size 23px 43 | 44 | .bottom 45 | display block 46 | font-weight normal 47 | font-size 40px 48 | 49 | .illustration-intro 50 | margin auto 51 | display block 52 | width 320px 53 | max-width 100% 54 | 55 | .made-by 56 | color rgba(#fff, .7) 57 | font-weight 100 58 | margin-top 30px 59 | 60 | img 61 | vertical-align middle 62 | height 20px -------------------------------------------------------------------------------- /client/components/ContentSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './ContentSection.styl'; 4 | 5 | const ContentSection = (props) => { 6 | var subMenu = null; 7 | if (props.subSections) { 8 | subMenu = ; 9 | } 10 | 11 | return
  • 12 | {props.title} 13 | {subMenu} 14 |
  • ; 15 | }; 16 | 17 | export default ContentSection; -------------------------------------------------------------------------------- /client/components/ContentSection.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/variables.styl' 2 | 3 | .ContentSection 4 | margin 10px 20px 5 | 6 | a 7 | color #fff 8 | transition color 0.2s linear 9 | text-decoration none 10 | 11 | &:hover 12 | color color-light-blue -------------------------------------------------------------------------------- /client/components/PlotContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import d3 from 'd3'; 3 | import d3p from 'd3-polygon'; 4 | import _ from 'lodash'; 5 | import cx from 'classnames'; 6 | 7 | import NodeModel from '../models/NodeModel'; 8 | 9 | import './PlotContainer.styl'; 10 | 11 | export default class PlotContainer extends Component { 12 | static propTypes = { 13 | id: PropTypes.number.isRequired, 14 | done: PropTypes.bool.isRequired, 15 | graphType: PropTypes.string.isRequired, 16 | currentId: PropTypes.number.isRequired, 17 | nodes: PropTypes.array.isRequired, 18 | components: PropTypes.array, 19 | visitOrder: PropTypes.array.isRequired, 20 | addNode: PropTypes.func.isRequired, 21 | updateNodes: PropTypes.func.isRequired, 22 | initialiseNodes: PropTypes.func.isRequired 23 | }; 24 | 25 | render() { 26 | return
    ; 27 | } 28 | 29 | componentDidMount() { 30 | this.props.initialiseNodes(this.getDimensions()); 31 | } 32 | 33 | componentDidUpdate() { 34 | this.draw(); 35 | } 36 | 37 | getDimensions() { 38 | let svgElem = d3.select('.PlotContainer').node(); 39 | return { 40 | x: svgElem.clientWidth, 41 | y: svgElem.clientHeight 42 | }; 43 | } 44 | 45 | draw() { 46 | let self = this; 47 | 48 | d3.selectAll(`.PlotContainer#plot${self.props.id} svg`).remove(); 49 | 50 | let svg = d3.select(`.PlotContainer#plot${self.props.id}`) 51 | .append('svg') 52 | // .attr('viewBox', '0 0 500 300') 53 | .classed('done', self.props.done) 54 | .on('click', function() { // We need this context, cant use ES6 anonymous function 55 | if (d3.event.defaultPrevented) return; 56 | let coords = d3.mouse(this); 57 | self.props.addNode(coords[0], coords[1]); 58 | }); 59 | 60 | let defs = svg.append('defs'); 61 | 62 | defs.append('marker') 63 | .attr('id', 'arrowhead') 64 | .attr('class', 'ArrowHead') 65 | .attr('refX', 22) /*must be smarter way to calculate shift*/ 66 | .attr('refY', 4) 67 | .attr('markerWidth', 12) 68 | .attr('markerHeight', 8) 69 | .attr('orient', 'auto') 70 | .append('path') 71 | .attr('d', 'M 0,0 V 8 L12,4 Z'); //this is actual shape for arrowhead 72 | 73 | defs.append('marker') 74 | .attr('id', 'arrowhead-dragging') 75 | .attr('class', 'ArrowHead dragging') 76 | .attr('refX', 10) /*must be smarter way to calculate shift*/ 77 | .attr('refY', 4) 78 | .attr('markerWidth', 12) 79 | .attr('markerHeight', 8) 80 | .attr('orient', 'auto') 81 | .append('path') 82 | .attr('d', 'M 0,0 V 8 L12,4 Z'); //this is actual shape for arrowhead 83 | 84 | defs.append('marker') 85 | .attr('id', 'arrowhead-traversed') 86 | .attr('class', 'ArrowHead traversed') 87 | .attr('refX', 22) /*must be smarter way to calculate shift*/ 88 | .attr('refY', 4) 89 | .attr('markerWidth', 12) 90 | .attr('markerHeight', 8) 91 | .attr('orient', 'auto') 92 | .append('path') 93 | .attr('d', 'M 0,0 V 8 L12,4 Z'); //this is actual shape for arrowhead 94 | 95 | if (this.props.components) { 96 | this.props.components.forEach(this.drawComponent.bind(this)); 97 | } 98 | this.props.nodes.forEach(this.drawEdges.bind(this)); 99 | this.props.nodes.forEach(this.drawNode.bind(this)); 100 | 101 | this.props.nodes.forEach((n) => { 102 | //console.log('Node', n.id, 'Index', n.visitIndex, 'Low Link', n.lowLink); 103 | //console.log(` new NodeModel(${n.id}, ${n.x}, ${n.y}, [${n.children}], [${n.parents}]),`); 104 | }); 105 | } 106 | 107 | drawComponent(component) { 108 | let svg = d3.select(`.PlotContainer#plot${this.props.id} svg`); 109 | 110 | if (component.length === 1) { 111 | let node = this.props.nodes.find((n) => n.id === component[0]); 112 | svg.append('circle') 113 | .attr('class', 'GraphComponent-circle') 114 | .attr('cx', node.x) 115 | .attr('cy', node.y) 116 | .attr('r', 20); 117 | } else { 118 | let points = component.map((c) => { 119 | let node = this.props.nodes.find((n) => n.id === c); 120 | return [node.x, node.y].join(','); 121 | }).join(' '); 122 | 123 | svg.append('polygon') 124 | .attr('class', 'GraphComponent-poly') 125 | .attr('points', points); 126 | } 127 | } 128 | 129 | drawEdges(node) { 130 | let self = this; 131 | let svg = d3.select(`.PlotContainer#plot${this.props.id} svg`); 132 | 133 | node.children.forEach((childId) => { 134 | let child = self.props.nodes.find((n) => n.id === childId); 135 | 136 | let markerId = (child.visitedFrom === node.id) ? '#arrowhead-traversed': '#arrowhead'; 137 | 138 | 139 | svg.append('line') 140 | .attr('class', 'Edge') 141 | .attr('marker-end', `url(${markerId})`) 142 | .attr('x1', node.x) 143 | .attr('y1', node.y) 144 | .attr('x2', child.x) 145 | .attr('y2', child.y) 146 | .classed('traversed', (child.visitedFrom === node.id)) 147 | .classed('not-traversed', (child.visited && child.visitedFrom !== node.id)); 148 | }); 149 | } 150 | 151 | drawNode(node) { 152 | let self = this; 153 | 154 | let svg = d3.select(`.PlotContainer#plot${self.props.id} svg`); 155 | 156 | let drag = d3.behavior.drag() 157 | .on('dragstart', function(d) { 158 | self.props.updateNodes(self.props.nodes.map((n) => { 159 | if (n.id === node.id) { 160 | n.selected = true; 161 | } 162 | 163 | return n; 164 | })); 165 | 166 | d3.select(`.PlotContainer#plot${self.props.id} svg`) 167 | .append('line') 168 | .attr('class', 'Edge dragging') 169 | .attr('x1', node.x) 170 | .attr('y1', node.y) 171 | .attr('x2', node.x) 172 | .attr('y2', node.y); 173 | }) 174 | .on('drag', function(d) { 175 | let draggedLine = d3.select(`.PlotContainer#plot${self.props.id} svg .Edge.dragging`); 176 | let x2 = d3.event.sourceEvent.offsetX - node.size; 177 | let y2 = d3.event.sourceEvent.offsetY - node.size; 178 | let target = d3.event.sourceEvent.target; 179 | 180 | if (target.tagName === 'circle') { 181 | x2 = d3.select(`#${target.id}`).attr('cx'); 182 | y2 = d3.select(`#${target.id}`).attr('cy'); 183 | } 184 | 185 | draggedLine 186 | .attr('marker-end', 'url(#arrowhead-dragging)') 187 | .attr('x1', node.x) 188 | .attr('y1', node.y) 189 | .attr('x2', x2) 190 | .attr('y2', y2); 191 | }) 192 | .on('dragend', function(d) { 193 | let target = d3.event.sourceEvent.target; 194 | let connectId; 195 | 196 | d3.event.sourceEvent.stopPropagation(); 197 | d3.selectAll(`.PlotContainer#plot${self.props.id} svg .Edge.dragging`).remove(); 198 | 199 | if (target.tagName === 'circle') { 200 | connectId = parseInt(target.id.substring(5)); 201 | } 202 | 203 | if (connectId && connectId !== node.id && node.children.filter((c) => c === connectId).length === 0) { 204 | self.props.updateNodes(self.props.nodes.map((n) => { 205 | if (n.id === node.id) { 206 | n.selected = false; 207 | n.children.push(connectId); 208 | if (self.props.graphType === 'undirected') { 209 | n.parents.push(connectId); 210 | } 211 | } 212 | 213 | if (n.id === connectId) { 214 | n.parents.push(node.id); 215 | if (self.props.graphType === 'undirected') { 216 | n.children.push(node.id); 217 | } 218 | } 219 | 220 | return n; 221 | })); 222 | } 223 | }); 224 | 225 | let nodeElem = svg.append('circle') 226 | .attr('class', 'Node') 227 | .attr('id', 'node-'+node.id) 228 | .attr('cx', node.x) 229 | .attr('cy', node.y) 230 | .attr('r', node.size) 231 | .classed('selected', node.selected) 232 | .classed('visited', node.visited) 233 | .classed('articulationPoint', node.articulationPoint) 234 | .classed('current', node.current) 235 | .classed('group-1', node.group === 1) 236 | .classed('group-2', node.group === 2) 237 | .classed('group-3', node.group === 3) 238 | .classed('group-4', node.group === 4) 239 | .classed('group-5', node.group === 5) 240 | .on('mouseover', function() { 241 | d3.select(this).classed('hover', true); 242 | }) 243 | .on('mouseout', function() { 244 | d3.select(this).classed('hover', false); 245 | }) 246 | .call(drag); 247 | 248 | svg.append('text') 249 | .attr('class', 'Node-label') 250 | .attr('id', 'label-'+node.id) 251 | .attr('x', node.x + node.size*1.2) 252 | .attr('y', node.y + node.size*1.5) 253 | .text(node.id); 254 | 255 | let visited = this.props.visitOrder.findIndex((v) => v.id === node.id); 256 | 257 | if (visited > -1) { 258 | svg.append('text') 259 | .attr('class', 'Node-visitLabel') 260 | .attr('id', 'visitlabel-'+node.id) 261 | .attr('x', node.x - (node.size*1.5)) 262 | .attr('y', node.y + node.size*1.5) 263 | .text(visited + 1); 264 | } 265 | } 266 | }; -------------------------------------------------------------------------------- /client/components/PlotContainer.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/variables.styl' 2 | 3 | .PlotContainer 4 | width 55vw 5 | height component-height 6 | // margin 5vh 5vw 7 | 8 | svg 9 | // border-radius 3px 10 | width 100% 11 | height 100% 12 | background-color color-white 13 | 14 | &.done 15 | .Edge 16 | stroke lighten(color-light-blue, 10) 17 | 18 | .ArrowHead 19 | path 20 | fill lighten(color-light-blue, 10) 21 | 22 | .Node 23 | fill lighten(color-light-blue, 10) 24 | 25 | .Node 26 | fill darken(color-light-blue, 10) 27 | 28 | &.selected 29 | stroke darken(color-light-blue, 50) 30 | stroke-width 1 31 | 32 | &.visited 33 | fill color-grey 34 | 35 | &.current 36 | fill darken(color-grey, 20) 37 | 38 | &.articulationPoint 39 | fill lighten(color-red, 20) 40 | 41 | &.hover 42 | stroke darken(color-light-blue, 50) 43 | stroke-width 2 44 | cursor e-resize 45 | 46 | &.group-1 47 | fill #44ccaa 48 | 49 | &.group-2 50 | fill #339988 51 | 52 | &.group-3 53 | fill #88eedd 54 | 55 | &.group-4 56 | fill #88ccbb 57 | 58 | &.group-5 59 | fill #33ccbb 60 | 61 | .ArrowHead 62 | path 63 | fill darken(color-light-blue, 20) 64 | 65 | &.traversed 66 | path 67 | fill color-grey 68 | 69 | .Node-label 70 | font-size 10px 71 | fill #cdcdcd 72 | 73 | .Node-visitLabel 74 | font-size 10px 75 | fill darken(color-red, 20) 76 | 77 | .Edge 78 | stroke darken(color-light-blue, 20) 79 | stroke-width 1 80 | 81 | &.dragging 82 | stroke lighten(color-light-blue, 5) 83 | 84 | &.traversed 85 | stroke color-grey 86 | z-index 99 87 | 88 | &.not-traversed 89 | opacity 0.3 90 | 91 | &:hover 92 | stroke darken(color-light-blue, 50) 93 | 94 | .GraphComponent-circle 95 | fill lighten(color-grey, 20) 96 | opacity 0.4 97 | 98 | .GraphComponent-poly 99 | stroke-width 40 100 | stroke-linejoin round 101 | stroke lighten(color-grey, 20) 102 | fill lighten(color-grey, 20) 103 | opacity 0.4 104 | 105 | @media screen and (max-width: breakpoint-small) 106 | width 100vw -------------------------------------------------------------------------------- /client/components/PlotExplanation.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import d3 from 'd3'; 3 | import _ from 'lodash'; 4 | import cx from 'classnames'; 5 | 6 | import './PlotExplanation.styl'; 7 | 8 | export default class PlotExplanation extends Component { 9 | static propTypes = { 10 | explanation: PropTypes.array.isRequired 11 | }; 12 | 13 | render() { 14 | let { explanation } = this.props; 15 | 16 | return
    17 |

    Algorithm Output

    18 | 21 |
    ; 22 | } 23 | }; -------------------------------------------------------------------------------- /client/components/PlotExplanation.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/variables.styl' 2 | 3 | .PlotExplained 4 | // margin 5vh 0 5 | max-height component-height 6 | width 45vw 7 | background-color color-code-bg 8 | background-image url(../../assets/darker-noise.png) 9 | background-size 150px 150px 10 | color #fff 11 | font-family 'Source Code Pro', sans-serif 12 | overflow hidden 13 | // border-radius 3px 14 | 15 | h2 16 | width 100% 17 | padding 10px 0 18 | background-color rgba(#fff, .1) 19 | font-family 'Open Sans', Arial, sans-serif 20 | font-weight 100 21 | 22 | ul 23 | padding 1vh 1vw 24 | list-style none 25 | max-height inherit 26 | overflow-y scroll 27 | 28 | li 29 | font-size 0.8rem 30 | 31 | &:last-child 32 | margin-bottom 100px 33 | 34 | @media screen and (max-width: breakpoint-medium) 35 | h2 36 | font-size 1.2rem 37 | 38 | @media screen and (max-width: breakpoint-small) 39 | display none -------------------------------------------------------------------------------- /client/components/PlotHeader.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import d3 from 'd3'; 3 | import _ from 'lodash'; 4 | import cx from 'classnames'; 5 | 6 | import './PlotHeader.styl'; 7 | 8 | export default class PlotHeader extends Component { 9 | static propTypes = { 10 | title: PropTypes.string, 11 | nodes: PropTypes.array.isRequired, 12 | algorithms: PropTypes.array.isRequired, 13 | selectedAlgorithm: PropTypes.string, 14 | exampleGraphs: PropTypes.object.isRequired, 15 | selectedExampleGraph: PropTypes.string, 16 | running: PropTypes.bool.isRequired, 17 | startNodeId: PropTypes.number.isRequired, 18 | setStartNodeId: PropTypes.func.isRequired, 19 | onChangeAlgorithm: PropTypes.func.isRequired, 20 | onChangeGraph: PropTypes.func.isRequired, 21 | onClickRun: PropTypes.func.isRequired, 22 | onClickClear: PropTypes.func.isRequired 23 | }; 24 | 25 | render() { 26 | let { 27 | title, 28 | nodes, 29 | algorithms, 30 | selectedAlgorithm, 31 | exampleGraphs, 32 | selectedExampleGraph, 33 | running, 34 | startNodeId, 35 | setStartNodeId, 36 | onChangeGraph, 37 | onChangeAlgorithm, 38 | onClickRun, 39 | onClickClear } = this.props; 40 | 41 | return
    42 |

    {this.props.title || 'Breadth and Depth First Search'}

    43 |
    44 | 45 | 48 | 49 | 52 | 53 | 56 | 59 | 62 |
    63 |
    ; 64 | } 65 | }; -------------------------------------------------------------------------------- /client/components/PlotHeader.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/variables.styl' 2 | 3 | .PlotHeader 4 | padding 1vh 5vw 5 | display flex 6 | justify-content space-between 7 | width 100vw 8 | height 40px 9 | background-color #31bcf0 10 | background-image url(../../assets/brand-noise.png) 11 | background-size 150px 150px 12 | color #fff 13 | 14 | h1, form 15 | display inline-block 16 | 17 | h1 18 | font-size 1.7rem 19 | font-weight 100 20 | text-shadow rgba(#000, .15) 0 1px 1px 21 | 22 | label, button, select 23 | margin 4px 6px 10px 6px 24 | font-size 0.9rem 25 | 26 | @media screen and (max-width: breakpoint-large) 27 | 28 | h1 29 | margin-top 8px 30 | font-size 1.3rem 31 | 32 | label, button, select 33 | margin 6px 6px 10px 6px 34 | font-size 0.8rem 35 | 36 | @media screen and (max-width: breakpoint-medium) 37 | h1 38 | margin-top 9px 39 | font-size 1.1rem 40 | label, button, select 41 | font-size 0.7rem 42 | 43 | select.example-graph, label.example-graph 44 | display none 45 | 46 | @media screen and (max-width: breakpoint-small) 47 | h1 48 | margin-top 11px 49 | font-size 0.9rem 50 | 51 | label, button, select 52 | font-size 0.6rem 53 | 54 | @media screen and (max-width: breakpoint-tiny) 55 | h1 56 | font-size 0.8rem 57 | select.start-node, label.start-node, label.algorithm 58 | display none -------------------------------------------------------------------------------- /client/components/ShareButtons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './ShareButtons.styl'; 4 | 5 | function twitterShare() { 6 | var tags = [ 'workshape' ], 7 | text = 'A Visual Guide to Graph Traversal Algorithms by Workshape.io'; 8 | 9 | let url = `http://workshape.github.io/visual-graph-algorithms`, 10 | twitterUrl = buildTwitterUrl({ url, text, tags }); 11 | 12 | openModal(twitterUrl, 'Tweet', 700, 260); 13 | } 14 | 15 | function facebookShare() { 16 | let url = `http://workshape.github.io/visual-graph-algorithms`, 17 | facebookUrl = `https://www.facebook.com/sharer/sharer.php?u=${url}`; 18 | 19 | openModal(facebookUrl, 'Share', 560, 610); 20 | } 21 | 22 | function buildTwitterUrl(options) { 23 | return [ 24 | `https://twitter.com/intent/tweet?tw_p=tweetbutton`, 25 | `&url=${encodeURI(options.url)}`, 26 | `&text=${options.text}` 27 | ].join(''); 28 | } 29 | 30 | function openModal(url, title = null, width = 700, height = 500) { 31 | let screenSize = getScreenSize(), 32 | options = { 33 | left : screenSize.width / 2 - width / 2, 34 | top : screenSize.height / 2 - height / 2, 35 | titlebar : false, 36 | location : location, 37 | menubar : false, 38 | status : false, 39 | toolbar : false, 40 | width, height 41 | }; 42 | 43 | window.open(url, title, serialize(options)); 44 | } 45 | 46 | function serialize(object) { 47 | var parts = []; 48 | 49 | for (let key in object) { 50 | parts.push(`${key}=${object[key]}`); 51 | } 52 | 53 | return parts.join(','); 54 | } 55 | 56 | function getScreenSize() { 57 | if (document.documentElement && document.documentElement.clientWidth) { 58 | return { 59 | width : document.documentElement.clientWidth, 60 | height : document.documentElement.clientHeight 61 | }; 62 | } 63 | 64 | if (document.body.clientWidth) { 65 | return { 66 | width : document.body.clientWidth, 67 | height : document.body.clientHeight 68 | }; 69 | } 70 | 71 | return { 72 | width : window.innerWidth, 73 | height : window.innerHeight 74 | }; 75 | } 76 | 77 | const ShareButtons = (props) => { 78 | return
    79 | 83 | 87 |
    ; 88 | }; 89 | 90 | export default ShareButtons; -------------------------------------------------------------------------------- /client/components/ShareButtons.styl: -------------------------------------------------------------------------------- 1 | .ShareButtons 2 | margin-top 30px 3 | text-align center 4 | 5 | button 6 | margin 0 10px 7 | padding 15px 40px 8 | font-size 15px 9 | font-weight 100 10 | box-shadow rgba(#000, .1) 0 2px 2px, rgba(#fff, .3) 0 1px 0 0 inset -------------------------------------------------------------------------------- /client/constants/ItemTypes.js: -------------------------------------------------------------------------------- 1 | export const ItemTypes = { 2 | PLAYER: 'Player' 3 | }; -------------------------------------------------------------------------------- /client/containers/AlgoVizContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import d3 from 'd3'; 3 | import _ from 'lodash'; 4 | import cx from 'classnames'; 5 | 6 | import PlotHeader from '../components/PlotHeader'; 7 | import PlotContainer from '../components/PlotContainer'; 8 | import PlotExplanation from '../components/PlotExplanation'; 9 | 10 | import { getExampleGraph, exampleGraphs } from '../utils'; 11 | import NodeModel from '../models/NodeModel'; 12 | import DepthFirstSearch from '../algorithms/DepthFirstSearch'; 13 | import BreadthFirstSearch from '../algorithms/BreadthFirstSearch'; 14 | import BipartitenessAlgorithm from '../algorithms/BipartitenessAlgorithm'; 15 | import TarjanStrongConnection from '../algorithms/TarjanStrongConnection'; 16 | import ArticulationPointSearch from '../algorithms/ArticulationPointSearch'; 17 | 18 | import './AlgoVizContainer.styl'; 19 | 20 | const ALGOS = { 21 | BFS: BreadthFirstSearch, 22 | DFS: DepthFirstSearch, 23 | TSC: TarjanStrongConnection, 24 | APS: ArticulationPointSearch, 25 | BA: BipartitenessAlgorithm 26 | }; 27 | 28 | export default class AlgoVizContainer extends Component { 29 | static propTypes = { 30 | title: PropTypes.string.isRequired, 31 | name: PropTypes.string.isRequired, 32 | selectableAlgorithms: PropTypes.array.isRequired, 33 | graphType: PropTypes.string, 34 | exampleGraph: PropTypes.string, 35 | id: PropTypes.number.isRequired 36 | }; 37 | 38 | constructor(props, context) { 39 | super(props, context); 40 | 41 | this.state = { 42 | running: false, 43 | done: false, 44 | algorithm: this.props.selectableAlgorithms[0], 45 | exampleGraph: this.props.exampleGraph, 46 | startNodeId: 1, 47 | nodes: [], 48 | components: [], 49 | visitOrder: [], 50 | explanation: [], 51 | currentId: 1 52 | }; 53 | } 54 | 55 | initialiseNodes(coords) { 56 | let nodes = _.assign([], getExampleGraph(this.state.exampleGraph)); 57 | 58 | this.setState({ 59 | nodes: nodes, 60 | currentId: (nodes.length === 0) ? 1 : nodes[nodes.length - 1].id + 1 61 | }); 62 | } 63 | 64 | runAlgorithm() { 65 | let selectedAlgorithm = this.state.algorithm; 66 | let searchAlgorithm = new ALGOS[selectedAlgorithm](this.state.nodes, this); 67 | 68 | this.setState({running: true, done: false}); 69 | 70 | let startNode = this.state.nodes.find((n) => n.id === this.state.startNodeId); 71 | 72 | searchAlgorithm.start(startNode) 73 | .then(() => this.setState({ running: false, done: true })) 74 | .catch((e) => { 75 | this.setState({ running: false }); 76 | console.error(e); 77 | }); 78 | } 79 | 80 | setStartNodeId(event) { 81 | this.setState({ 82 | startNodeId: parseInt(event.target.value,10) 83 | }); 84 | } 85 | 86 | addNode(x, y) { 87 | let node = new NodeModel(this.state.currentId, x, y); 88 | this.setState({ 89 | done: false, 90 | nodes: _.concat(this.state.nodes, node), 91 | currentId: this.state.currentId + 1 92 | }); 93 | } 94 | 95 | updateNodes(nodes) { 96 | this.setState({ 97 | done: false, 98 | nodes: nodes 99 | }); 100 | } 101 | 102 | clearNodes() { 103 | this.setState({ 104 | nodes: [], 105 | visitOrder: [], 106 | explanation: [], 107 | components: [], 108 | currentId: 1 109 | }); 110 | } 111 | 112 | resetNodes() { 113 | let update = _.assign([], this.state.nodes); 114 | update.forEach((x) => x.reset()); 115 | 116 | this.setState({ 117 | nodes: update, 118 | visitOrder: [], 119 | explanation: [], 120 | components: [] 121 | }); 122 | } 123 | 124 | onChangeAlgorithm(event) { 125 | this.setState({ algorithm: event.target.value }); 126 | } 127 | 128 | onChangeGraph(event) { 129 | let nodes = _.assign([], getExampleGraph(event.target.value)); 130 | 131 | this.setState({ 132 | done: false, 133 | visitOrder: [], 134 | explanation: [], 135 | components: [], 136 | nodes: nodes, 137 | exampleGraph: event.target.value, 138 | currentId: (nodes.length === 0) ? 1 : nodes[nodes.length - 1].id + 1 139 | }); 140 | } 141 | 142 | onClickRun(event) { 143 | event.preventDefault(); 144 | if (!this.state.running) { 145 | this.resetNodes(); 146 | this.runAlgorithm(); 147 | } 148 | } 149 | 150 | onClickClear(event) { 151 | event.preventDefault(); 152 | if (!this.state.running) { 153 | this.clearNodes(); 154 | } 155 | } 156 | 157 | render() { 158 | let selectableAlgos = this.props.selectableAlgorithms; 159 | let headerProps = { 160 | title: this.props.title, 161 | algorithms: Object.keys(ALGOS).filter((algo) => (!selectableAlgos || selectableAlgos.includes(algo))), 162 | exampleGraphs: exampleGraphs, 163 | selectedExampleGraph: this.state.exampleGraph, 164 | nodes: this.state.nodes, 165 | selectedAlgorithm: this.state.algorithm, 166 | running: this.state.running, 167 | startNodeId: this.state.startNodeId, 168 | setStartNodeId: this.setStartNodeId.bind(this), 169 | onChangeGraph: this.onChangeGraph.bind(this), 170 | onChangeAlgorithm: this.onChangeAlgorithm.bind(this), 171 | onClickRun: this.onClickRun.bind(this), 172 | onClickClear: this.onClickClear.bind(this), 173 | }; 174 | 175 | let containerProps = { 176 | id: this.props.id, 177 | graphType: this.props.graphType || 'directed', 178 | done: this.state.done, 179 | currentId: this.state.currentId, 180 | nodes: this.state.nodes, 181 | components: this.state.components, 182 | visitOrder: this.state.visitOrder, 183 | addNode: this.addNode.bind(this), 184 | updateNodes: this.updateNodes.bind(this), 185 | initialiseNodes: this.initialiseNodes.bind(this) 186 | }; 187 | 188 | return 189 |
    190 | 191 | 192 | 193 |
    194 |
    ; 195 | } 196 | } -------------------------------------------------------------------------------- /client/containers/AlgoVizContainer.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/variables.styl' 2 | 3 | .AlgoVizContainer 4 | width 100vw 5 | background #79dbff 6 | display flex 7 | justify-content start 8 | align-content flex-start 9 | flex-wrap wrap 10 | font-family 'Open Sans', Arial, sans-serif; 11 | -------------------------------------------------------------------------------- /client/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import d3 from 'd3'; 3 | import _ from 'lodash'; 4 | import cx from 'classnames'; 5 | 6 | import BlogContainer from './BlogContainer'; 7 | import AlgoVizContainer from './AlgoVizContainer'; 8 | import ContentsContainer from './ContentsContainer'; 9 | import BlogHeading from '../components/BlogHeading'; 10 | 11 | import './AppContainer.styl'; 12 | 13 | const sections = [ 14 | { title: 'Top', link: '#top'}, 15 | { title: 'Introduction', link: '#introduction'}, 16 | { title: 'Main Visualisation', link: '#main-visualisation'}, 17 | { 18 | title: 'Depth First Search', 19 | link: '#dfs', 20 | subSections: [ 21 | { title: 'Visualisation', link: '#dfs-visualisation' }, 22 | { title: 'Pseudocode', link: '#dfs-pseudocode' }, 23 | ] 24 | }, 25 | { 26 | title: 'Breadth First Search', 27 | link: '#bfs', 28 | subSections: [ 29 | { title: 'Visualisation', link: '#bfs-visualisation' }, 30 | { title: 'Pseudocode', link: '#bfs-pseudocode' }, 31 | ] 32 | }, 33 | { 34 | title: 'Observations Depth First Search and Breadth First Search', 35 | link: '#observations' 36 | }, 37 | { 38 | title: 'Applications of Depth First Search', 39 | link: '#dfs-applications', 40 | subSections: [ 41 | { 42 | title: 'Tarjan\'s Strongly Connected Component Algorithm', 43 | link: '#tscc', 44 | subSections: [ 45 | { title: 'Visualisation', link: '#tscc-visualisation' }, 46 | { title: 'Pseudocode', link: '#tscc-pseudocode' }, 47 | ] 48 | }, 49 | { 50 | title: 'Biconnected Algorithm', 51 | link: '#bp', 52 | subSections: [ 53 | { title: 'Visualisation', link: '#bc-visualisation' }, 54 | { title: 'Pseudocode', link: '#bc-pseudocode' }, 55 | ] 56 | } 57 | ] 58 | }, 59 | { 60 | title: 'Applications of Breadth First Search', 61 | link: '#bfs-applications', 62 | subSections: [ 63 | { 64 | title: 'Bipartiness Algorithm', 65 | link: '#bp', 66 | subSections: [ 67 | { title: 'Visualisation', link: '#bp-visualisation' }, 68 | { title: 'Pseudocode', link: '#bp-pseudocode' }, 69 | ] }, 70 | ] 71 | }, 72 | { title: 'Motivations', link: '#motivations' }, 73 | { title: 'Implementation', link: '#implementation' }, 74 | { title: 'Links', link: '#references' } 75 | ]; 76 | 77 | export default class AppContainer extends Component { 78 | constructor(props, context) { 79 | super(props, context); 80 | 81 | this.state = { 82 | menuVisible: false 83 | }; 84 | } 85 | 86 | toggleMenu() { 87 | this.setState({ 88 | menuVisible: !this.state.menuVisible 89 | }); 90 | } 91 | 92 | render() { 93 | let cssClass = this.state.menuVisible ? 'AppContainer pad-left': 'AppContainer'; 94 | return
    95 | 96 | this.toggleMenu()} /> 97 | 98 | 104 | 105 | 111 | 112 | 118 | 119 | 125 | 126 | 133 | 134 | 141 | 142 |
    ; 143 | } 144 | }; -------------------------------------------------------------------------------- /client/containers/AppContainer.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/variables.styl' 2 | 3 | .AppContainer 4 | background-color #f5f5f5 5 | transition padding 0.5s 6 | overflow-x hidden 7 | 8 | &.pad-left 9 | padding-left 20vw 10 | @media screen and (max-width breakpoint-medium) 11 | &.pad-left 12 | padding-left 0vw -------------------------------------------------------------------------------- /client/containers/BlogContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import d3 from 'd3'; 3 | import _ from 'lodash'; 4 | import cx from 'classnames'; 5 | 6 | import AlgoVizContainer from './AlgoVizContainer'; 7 | 8 | import section1 from '../blog/section1'; 9 | import section2 from '../blog/section2'; 10 | import section3 from '../blog/section3'; 11 | import section4 from '../blog/section4'; 12 | import section5 from '../blog/section5'; 13 | import section6 from '../blog/section6'; 14 | import references from '../blog/references'; 15 | 16 | let sections = { 17 | 1: section1, 18 | 2: section2, 19 | 3: section3, 20 | 4: section4, 21 | 5: section5, 22 | 6: section6, 23 | references: references 24 | }; 25 | 26 | import './BlogContainer.styl'; 27 | 28 | export default class BlogContainer extends Component { 29 | static propTypes = { 30 | sectionId: PropTypes.any.isRequired 31 | }; 32 | 33 | render() { 34 | return
    35 | {sections[this.props.sectionId]} 36 |
    ; 37 | } 38 | }; -------------------------------------------------------------------------------- /client/containers/BlogContainer.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/variables.styl' 2 | 3 | .BlogContainer 4 | display block 5 | max-width 900px 6 | margin: 50px auto 7 | font-family: 'Libre Baskerville', serif; 8 | 9 | h1,h2,h3,h4 10 | display block 11 | margin-top 40px 12 | margin-bottom 40px 13 | 14 | p 15 | display block 16 | margin 40px 0 17 | line-height 30px 18 | color #666 19 | font-size 18px 20 | 21 | .instruction 22 | padding 15px 15px 23 | border-radius 4px 24 | text-align center 25 | background-color lighten(color-code-bg, 50) 26 | 27 | h4 28 | margin 10px 29 | font-size 25px 30 | color #333 31 | 32 | p 33 | margin 10px 34 | 35 | ol, ul 36 | margin 0 30px 30px 37 | color #666 38 | li 39 | margin-bottom 18px 40 | line-height 2rem 41 | a 42 | color black 43 | 44 | &:hover 45 | color lighten(black, 20) 46 | 47 | code 48 | display block 49 | border-radius 4px 50 | // background-color lighten(color-light-grey, 5) 51 | background-color color-code-bg 52 | background-image url(../../assets/darker-noise.png) 53 | background-size 150px 150px 54 | color #fff 55 | line-height 1.5rem 56 | margin 20px 57 | padding 20px 58 | 59 | @media screen and (max-width: breakpoint-large) 60 | width 92vw 61 | 62 | @media screen and (max-width: breakpoint-small) 63 | code 64 | font-size 0.8rem 65 | 66 | @media screen and (max-width: breakpoint-tiny) 67 | code 68 | font-size 0.7rem -------------------------------------------------------------------------------- /client/containers/ContentsContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | 3 | import ContentSection from '../components/ContentSection'; 4 | 5 | import './ContentsContainer.styl'; 6 | 7 | export default class ContentsContainer extends Component { 8 | static propTypes = { 9 | sections: PropTypes.array.isRequired, 10 | visible: PropTypes.bool.isRequired, 11 | toggleMenu: PropTypes.func.isRequired 12 | }; 13 | 14 | render() { 15 | let cssClass = (this.props.visible) ? 'ContentsContainer' : 'ContentsContainer closed'; 16 | return
    17 |
    this.props.toggleMenu()}>
    18 | 19 |
    20 |

    Contents

    21 |
      22 | {this.props.sections.map((section, id) => )} 23 |
    24 |
    25 |
    26 | Made with 27 | 28 | 29 | by Workshape.io 30 | 31 |
    32 |
    ; 33 | } 34 | 35 | }; -------------------------------------------------------------------------------- /client/containers/ContentsContainer.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/variables.styl' 2 | 3 | .ContentsContainer 4 | position fixed 5 | left 0 6 | top 0 7 | background-color #286176 8 | background-image url(../../assets/darker-noise.png) 9 | background-size 150px 150px 10 | color #fff 11 | z-index 999 12 | transition all 0.5s 13 | font-family 'Open Sans', Arial, sans-serif 14 | 15 | .Controls 16 | position relative 17 | font-size 1rem 18 | cursor pointer 19 | padding-left menu-width 20 | 21 | &:before 22 | content "" 23 | position absolute 24 | top 17px 25 | left 17px 26 | width 25px 27 | height 4px 28 | border-top 12px double #fff 29 | border-bottom 4px solid #fff 30 | 31 | &:hover 32 | &:before 33 | opacity .8 34 | 35 | .logo 36 | width 40px 37 | height 40px 38 | position fixed 39 | bottom 10px 40 | left 10px 41 | background-image url(../../assets/logotype-white.svg) 42 | background-size contain 43 | background-repeat no-repeat 44 | background-position center center 45 | display none 46 | 47 | &.closed 48 | 49 | .logo 50 | display block 51 | 52 | .BuiltBy 53 | pointer-events none 54 | 55 | .Menu 56 | padding 20px 57 | width 22vw 58 | height 84.5vh 59 | overflow scroll 60 | transition width 0.5s, margin 0.5s 61 | 62 | li li 63 | font-size 0.9rem 64 | opacity .6 65 | font-weight 100 66 | 67 | .BuiltBy 68 | text-align center 69 | font-weight 100 70 | font-size 0.9rem 71 | width 100% 72 | height 10vh 73 | transition height 0.5s, margin 0.5s, opacity 0.1s 74 | font-size 17px 75 | padding-top 30px 76 | background rgba(#fff, .05) 77 | 78 | span, i 79 | margin 2px 80 | 81 | span 82 | color: #fff 83 | 84 | a, i 85 | color lighten(color-workshape, 15) 86 | 87 | &.closed 88 | 89 | .Menu, 90 | overflow hidden 91 | margin-left -600px 92 | 93 | .BuiltBy 94 | opacity 0 95 | margin-left -600px 96 | 97 | @media screen and (max-width breakpoint-medium) 98 | 99 | .logo 100 | left auto 101 | right 20px 102 | bottom auto 103 | top 10px 104 | display block 105 | 106 | &.closed .Controls 107 | background #fff 108 | box-shadow rgba(#000, .1) 0 2px 2px 109 | width 100% 110 | height menu-width 111 | 112 | &:before 113 | border-color rgba(color-code-bg, .8) 114 | 115 | &.closed .logo 116 | background-image url(../../assets/logotype-color.svg) 117 | 118 | .Menu 119 | width 100vw 120 | transition height 0.5s, margin 0.5s, opacity 0.3s 121 | 122 | .BuiltBy 123 | width 100vw 124 | 125 | &.closed 126 | .BuiltBy 127 | opacity 0 128 | height 0 129 | margin-left 0 130 | 131 | .Menu 132 | height 0 133 | opacity 0 134 | margin-left 0 135 | margin-top -100px 136 | width 100vw -------------------------------------------------------------------------------- /client/dataStructures/Queue.js: -------------------------------------------------------------------------------- 1 | export default class Queue { 2 | /** 3 | * Queue - a First in - First Out (FIFO) data structure 4 | * 5 | * @constructor 6 | */ 7 | constructor() { 8 | this.internal = []; 9 | } 10 | 11 | isNotEmpty() { 12 | return (this.internal && this.internal.length > 0); 13 | } 14 | 15 | enqueue(value) { 16 | this.internal.push(value); 17 | } 18 | 19 | dequeue() { 20 | if (!this.isNotEmpty()) { 21 | throw new Error('Queue is empty!'); 22 | } 23 | 24 | return this.internal.shift(); 25 | } 26 | } -------------------------------------------------------------------------------- /client/dataStructures/Stack.js: -------------------------------------------------------------------------------- 1 | export default class Stack { 2 | /** 3 | * Stack - a Last in Last out (LIFO) data structure 4 | * 5 | * @constructor 6 | */ 7 | constructor() { 8 | this.internal = []; 9 | } 10 | 11 | peek() { 12 | if (!this.internal || this.internal.length === 0) { 13 | throw new Error('Empty Stack'); 14 | } 15 | 16 | return this.internal[this.internal.length - 1]; 17 | } 18 | 19 | pop() { 20 | if (!this.internal || this.internal.length === 0) { 21 | throw new Error('Empty Stack'); 22 | } 23 | 24 | let pop = this.internal.pop(); 25 | console.log('Stack popped', this.internal, pop); 26 | return pop; 27 | } 28 | 29 | push(value) { 30 | this.internal.push(value); 31 | console.log('Stack pushed', this.internal, value); 32 | } 33 | } -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import { render } from 'react-dom'; 2 | import React from 'react'; 3 | 4 | import skrollr from 'skrollr'; 5 | 6 | import AppContainer from './containers/AppContainer'; 7 | 8 | import '../style/pure.css'; 9 | import '../style/icons.css'; 10 | import '../style/main.styl'; 11 | 12 | render( 13 | , 14 | document.getElementById('start-app') 15 | ); 16 | 17 | /** 18 | * We must initialise skrollr AFTER we have rendered our React application 19 | */ 20 | if(!(/Android|iPhone|iPad|iPod|BlackBerry|Windows Phone/i).test(navigator.userAgent || navigator.vendor || window.opera)){ 21 | skrollr.init({ 22 | forceHeight: false 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /client/models/NodeModel.js: -------------------------------------------------------------------------------- 1 | export default class Node { 2 | constructor(id, x, y, children, parents) { 3 | this.id = id; 4 | this.x = x; 5 | this.y = y; 6 | this.children = children || []; 7 | this.parents = parents || []; 8 | 9 | this.size = 10; 10 | this.selected = false; 11 | this.current = false; 12 | 13 | this.visited = false; 14 | this.visitIndex = null; 15 | this.visitedFrom = null; 16 | 17 | this.articulationPoint = false; 18 | this.lowLink = null; 19 | this.group = null; 20 | 21 | this.childVisitCount = 0; 22 | }; 23 | 24 | hasChildren() { 25 | return this.children && this.children.length > 0; 26 | } 27 | 28 | reset() { 29 | this.visited = false; 30 | this.lowLink = null; 31 | this.current = false; 32 | this.visitedFrom = null; 33 | this.visitIndex =null; 34 | this.articulationPoint = false; 35 | this.childVisitCount = 0; 36 | this.group = null; 37 | } 38 | } -------------------------------------------------------------------------------- /client/utils.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import _ from 'lodash'; 3 | 4 | import NodeModel from './models/NodeModel'; 5 | 6 | /** 7 | * Creates a sequence of promises to processed 8 | * 9 | * @param {Array of tasks} tasks 10 | * 11 | * @return {Promise} 12 | */ 13 | export const promiseSequence = (tasks) => { 14 | let current = Promise.resolve(true); 15 | 16 | tasks.map((task) => { 17 | current = current.then(task); 18 | }); 19 | 20 | return current; 21 | }; 22 | 23 | /** 24 | * While loop promisified 25 | * 26 | * @param {Function} predicate function that returns boolean 27 | * @param {Function} action block of action to complete 28 | * 29 | * @return {Promise} 30 | */ 31 | export const promiseWhile = (predicate, action) => { 32 | let loop = () => { 33 | if (!predicate()) { 34 | return; 35 | } 36 | return Promise.resolve(action()).then(loop); 37 | }; 38 | 39 | return Promise.resolve(true).then(loop); 40 | }; 41 | 42 | export const getDirectedTree = () => { 43 | return [ 44 | new NodeModel(1, 41, 50, [2,3, 5]), 45 | 46 | new NodeModel(2, 41, 120, [4,5], [1]), 47 | new NodeModel(3, 97, 90, [5,6,7], [1]), 48 | 49 | new NodeModel(4, 42, 190, [8,9], [2]), 50 | new NodeModel(5, 100, 190, [9], [1, 2,3]), 51 | 52 | new NodeModel(6, 160, 160, null, [3]), 53 | new NodeModel(7, 190, 135, null, [3]), 54 | 55 | new NodeModel(8, 42, 276, null, [4]), 56 | new NodeModel(9, 101, 278, null, [4, 5]) 57 | ]; 58 | }; 59 | 60 | export const getDirectedGraph = () => { 61 | return [ 62 | new NodeModel(1, 81, 138.3125, [2,3,7], []), 63 | new NodeModel(2, 178, 80.3125, [4,7], [1]), 64 | new NodeModel(3, 173, 198.3125, [5,7], [1]), 65 | new NodeModel(4, 321, 72.3125, [6], [2]), 66 | new NodeModel(5, 318, 209.3125, [6], [3]), 67 | new NodeModel(6, 432, 138.3125, [], [4,5,7]), 68 | new NodeModel(7, 319, 136.3125, [6], [1,2,3]) 69 | ]; 70 | }; 71 | 72 | export const getDirectedCycle = () => { 73 | return [ 74 | new NodeModel(1, 86, 76.3125, [2], [7]), 75 | new NodeModel(2, 86, 217.3125, [3], [1]), 76 | new NodeModel(3, 179, 222.3125, [4], [2]), 77 | new NodeModel(4, 287, 222.3125, [5], [3]), 78 | new NodeModel(5, 290, 144.3125, [6], [4]), 79 | new NodeModel(6, 284, 68.3125, [7], [5]), 80 | new NodeModel(7, 176, 68.3125, [1], [6]) 81 | ]; 82 | }; 83 | 84 | export const getDisconnectedComponents = () => { 85 | return [ 86 | new NodeModel(1, 62, 68.3125, [2], [3]), 87 | new NodeModel(2, 59, 231.3125, [3], [1]), 88 | new NodeModel(3, 211, 230.3125, [1], [2]), 89 | new NodeModel(4, 349, 60.3125, [5], [5]), 90 | new NodeModel(5, 344, 233.3125, [4], [4]), 91 | new NodeModel(6, 211, 65.3125, [7], [7]), 92 | new NodeModel(7, 207, 159.3125, [6], [6]) 93 | ]; 94 | }; 95 | 96 | export const getNotBiconnected = () => { 97 | return [ 98 | new NodeModel(1, 55, 32.3125, [2], [2]), 99 | new NodeModel(2, 179, 70.3125, [1,3], [1,3]), 100 | new NodeModel(3, 89, 175.3125, [2,4], [2,4]), 101 | new NodeModel(4, 218, 200.3125, [3], [3]) 102 | ]; 103 | }; 104 | 105 | export const getBiconnected = () => { 106 | return [ 107 | new NodeModel(1, 163, 54.265625, [3,2,5], [3,2,5]), 108 | new NodeModel(2, 301, 94.265625, [4,1,5], [4,1,5]), 109 | new NodeModel(3, 152, 213.265625, [1,4], [1,4]), 110 | new NodeModel(4, 317, 220.265625, [3,2], [3,2]), 111 | new NodeModel(5, 224, 143.265625, [1,2], [1,2]) 112 | ]; 113 | }; 114 | 115 | export const getNotBipartite = () => { 116 | return [ 117 | new NodeModel(1, 234, 30.265625, [2,3,5,6], [2,3,5,6]), 118 | new NodeModel(2, 168, 120.265625, [1,4,5], [1,4,5]), 119 | new NodeModel(3, 323, 109.265625, [1,6,7], [1,6,7]), 120 | new NodeModel(4, 119, 219.265625, [2], [2]), 121 | new NodeModel(5, 202, 216.265625, [1,2], [2,1]), 122 | new NodeModel(6, 281, 216.265625, [1,3], [1,3]), 123 | new NodeModel(7, 365, 213.265625, [3], [3]) 124 | ]; 125 | }; 126 | 127 | export const getBipartite = () => { 128 | return [ 129 | new NodeModel(1, 221, 27.3125, [2,3], [2,3]), 130 | new NodeModel(2, 169, 108.3125, [1,4,5,6], [1,4,5,6]), 131 | new NodeModel(3, 260, 109.3125, [1,6,7], [1,6,7]), 132 | new NodeModel(4, 114, 206.3125, [2,8,9], [2,8,9]), 133 | new NodeModel(5, 184, 206.3125, [2,10,11], [10,11,2]), 134 | new NodeModel(6, 237, 203.3125, [2,3,12,13], [3,12,2,13]), 135 | new NodeModel(7, 322, 203.3125, [3,13,14,15], [3,14,13,15]), 136 | new NodeModel(8, 77, 304.3125, [4], [4]), 137 | new NodeModel(9, 131, 300.3125, [4], [4]), 138 | new NodeModel(10, 163, 302.3125, [5], [5]), 139 | new NodeModel(11, 204, 302.3125, [5], [5]), 140 | new NodeModel(12, 238, 300.3125, [6], [6]), 141 | new NodeModel(13, 279, 301.3125, [6,7], [6,7]), 142 | new NodeModel(14, 326, 302.3125, [7], [7]), 143 | new NodeModel(15, 372, 301.3125, [7], [7]) 144 | ]; 145 | }; 146 | 147 | export const getBiconnectedBipartite = () => { 148 | return [ 149 | new NodeModel(1, 40, 37.3125, [7,2,3], [7,2,3]), 150 | new NodeModel(2, 39, 286.3125, [8,1,4], [8,1,4]), 151 | new NodeModel(3, 122, 114.3125, [1,6,4], [1,6,4]), 152 | new NodeModel(4, 122, 212.3125, [5,3,2], [5,3,2]), 153 | new NodeModel(5, 258, 209.3125, [6,4,8], [6,4,8]), 154 | new NodeModel(6, 251, 106.3125, [3,5,7], [3,5,7]), 155 | new NodeModel(7, 328, 27.3125, [1,8,6], [1,8,6]), 156 | new NodeModel(8, 330, 288.3125, [7,2,5], [7,2,5]) 157 | ]; 158 | }; 159 | 160 | export const exampleGraphs = { 161 | directedTree: getDirectedTree, 162 | directedGraph: getDirectedGraph, 163 | directedCycle: getDirectedCycle, 164 | disconnectedComponents: getDisconnectedComponents, 165 | notBiconnected: getNotBiconnected, 166 | biconnected: getBiconnected, 167 | biconnectedBipartite: getBiconnectedBipartite, 168 | notBipartite: getNotBipartite, 169 | bipartite: getBipartite 170 | }; 171 | 172 | export const getExampleGraph = (key) => { 173 | if (exampleGraphs[key]) { 174 | return exampleGraphs[key](); 175 | } else { 176 | return []; 177 | } 178 | }; 179 | 180 | /** 181 | * Generate an example graph 182 | * 183 | * @param {Number} childCount number of children for each parent 184 | * @param {Number} maxDepth max depth of generated graph 185 | * @param {Number} maxX maximum value of x coord 186 | * @param {Number} maxY maximum value of y coord 187 | * @param {Number} padding padding on outside of drawn graph 188 | * 189 | * @return {Array} set of Nodes 190 | */ 191 | export const generateTree = (childCount, maxDepth, maxX, maxY, padding, prob) => { 192 | prob = prob || 0.7; 193 | var root = new NodeModel(1, maxX/2, padding); 194 | var nodes = []; 195 | var currentDepth = 1; 196 | let totalNodes = _.range(1, maxDepth+1).reduce((sum, curr) => sum + Math.pow(childCount, curr), 0) + 1; 197 | let bottomNodes = Math.pow(childCount, maxDepth); 198 | 199 | return generateChildren(maxX, maxY, root, currentDepth); 200 | 201 | 202 | function generateChildren(mx, my, parent, currentDepth) { 203 | let maxId = (nodes.length > 0) ? nodes[nodes.length-1].id : root.id; 204 | let nodesAtDepth = Math.pow(childCount, currentDepth); 205 | let sectionSize = (mx - padding) / nodesAtDepth; 206 | let sectionSpread = sectionSize * childCount; 207 | let sectionStart = parent.x - (sectionSpread/2) + (sectionSize/2); 208 | let xrange = _.range(sectionStart, sectionStart + sectionSpread, sectionSize); 209 | let dy = Math.floor( (my - (padding*2)) / maxDepth); 210 | 211 | let children = _.range(childCount).map((i) => { 212 | return new NodeModel(++maxId, xrange[i], parent.y + dy); 213 | }); 214 | 215 | children = children.filter((c) => { 216 | return Math.random() <= prob; 217 | }); 218 | 219 | children.forEach((child) => { 220 | parent.children.push(child.id); 221 | }); 222 | 223 | nodes = _.concat(nodes.filter((n) => n.id !== parent.id), parent, children); 224 | 225 | if (currentDepth < maxDepth) { 226 | return _.uniq(_.flatMap(children, (child) => generateChildren(mx, my, child, currentDepth + 1))); 227 | } else { 228 | return nodes; 229 | } 230 | } 231 | }; 232 | 233 | -------------------------------------------------------------------------------- /fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/fonts/icomoon.eot -------------------------------------------------------------------------------- /fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/fonts/icomoon.ttf -------------------------------------------------------------------------------- /fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workshape/graph-algorithms-visualised/aa3cae9b65ce34fe590895ee7ad7490887f965af/fonts/icomoon.woff -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A Visual Guide to Graph Traversal Algorithms by Workshape.io 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
    29 | 30 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visual-graph-algos", 3 | "version": "0.1.0", 4 | "description": "D3 visualisation of Graph Algorithms in a React Component - how controversial", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production webpack", 8 | "lint": "eslint client", 9 | "start": "NODE_ENV=development node webpack.dev.server.js" 10 | }, 11 | "author": "Gordon Dent ", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bluebird": "^3.3.5", 15 | "classnames": "^2.2.3", 16 | "d3": "3.5.16", 17 | "d3-polygon": "^0.2.1", 18 | "extract-text-webpack-plugin": "^1.0.1", 19 | "lodash": "^4.6.1", 20 | "nib": "^1.1.0", 21 | "react": "15.0.1", 22 | "react-dom": "15.0.1", 23 | "skrollr": "^0.6.26" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.6.5", 27 | "babel-eslint": "^6.0.4", 28 | "babel-loader": "^6.2.4", 29 | "babel-preset-es2015": "^6.6.0", 30 | "babel-preset-react": "^6.5.0", 31 | "babel-preset-react-hmre": "^1.1.1", 32 | "babel-preset-stage-0": "^6.5.0", 33 | "css-loader": "^0.23.1", 34 | "eslint": "^2.8.0", 35 | "eslint-plugin-react": "^5.0.1", 36 | "file-loader": "^0.8.5", 37 | "react-hot-loader": "^1.3.0", 38 | "style-loader": "^0.13.0", 39 | "stylus": "^0.54.0", 40 | "stylus-loader": "^1.5.1", 41 | "url-loader": "^0.5.7", 42 | "webpack": "^1.12.14", 43 | "webpack-dev-server": "^1.14.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /style/icons-selection.json: -------------------------------------------------------------------------------- 1 | { 2 | "IcoMoonType": "selection", 3 | "icons": [ 4 | { 5 | "icon": { 6 | "paths": [ 7 | "M839.68 460.8h-655.36c-28.262 0-30.72 22.886-30.72 51.2s2.458 51.2 30.72 51.2h655.36c28.262 0 30.72-22.886 30.72-51.2s-2.458-51.2-30.72-51.2zM839.68 665.6h-655.36c-28.262 0-30.72 22.886-30.72 51.2s2.458 51.2 30.72 51.2h655.36c28.262 0 30.72-22.886 30.72-51.2s-2.458-51.2-30.72-51.2zM184.32 358.4h655.36c28.262 0 30.72-22.886 30.72-51.2s-2.458-51.2-30.72-51.2h-655.36c-28.262 0-30.72 22.886-30.72 51.2s2.458 51.2 30.72 51.2z" 8 | ], 9 | "attrs": [], 10 | "isMulticolor": false, 11 | "tags": [ 12 | "menu" 13 | ], 14 | "grid": 20 15 | }, 16 | "attrs": [], 17 | "properties": { 18 | "order": 1, 19 | "id": 0, 20 | "prevSize": 20, 21 | "code": 59648, 22 | "name": "menu" 23 | }, 24 | "setIdx": 0, 25 | "setId": 1, 26 | "iconIdx": 0 27 | }, 28 | { 29 | "icon": { 30 | "paths": [ 31 | "M1024 226.4c-37.6 16.8-78.2 28-120.6 33 43.4-26 76.6-67.2 92.4-116.2-40.6 24-85.6 41.6-133.4 51-38.4-40.8-93-66.2-153.4-66.2-116 0-210 94-210 210 0 16.4 1.8 32.4 5.4 47.8-174.6-8.8-329.4-92.4-433-219.6-18 31-28.4 67.2-28.4 105.6 0 72.8 37 137.2 93.4 174.8-34.4-1-66.8-10.6-95.2-26.2 0 0.8 0 1.8 0 2.6 0 101.8 72.4 186.8 168.6 206-17.6 4.8-36.2 7.4-55.4 7.4-13.6 0-26.6-1.4-39.6-3.8 26.8 83.4 104.4 144.2 196.2 146-72 56.4-162.4 90-261 90-17 0-33.6-1-50.2-3 93.2 59.8 203.6 94.4 322.2 94.4 386.4 0 597.8-320.2 597.8-597.8 0-9.2-0.2-18.2-0.6-27.2 41-29.4 76.6-66.4 104.8-108.6z" 32 | ], 33 | "attrs": [], 34 | "isMulticolor": false, 35 | "tags": [ 36 | "twitter", 37 | "brand", 38 | "tweet", 39 | "social" 40 | ], 41 | "grid": 16 42 | }, 43 | "attrs": [], 44 | "properties": { 45 | "order": 1, 46 | "id": 1, 47 | "prevSize": 32, 48 | "code": 59649, 49 | "name": "twitter" 50 | }, 51 | "setIdx": 1, 52 | "setId": 0, 53 | "iconIdx": 0 54 | }, 55 | { 56 | "icon": { 57 | "paths": [ 58 | "M608 192h160v-192h-160c-123.514 0-224 100.486-224 224v96h-128v192h128v512h192v-512h160l32-192h-192v-96c0-17.346 14.654-32 32-32z" 59 | ], 60 | "attrs": [], 61 | "isMulticolor": false, 62 | "tags": [ 63 | "facebook", 64 | "brand", 65 | "social" 66 | ], 67 | "grid": 16 68 | }, 69 | "attrs": [], 70 | "properties": { 71 | "order": 1, 72 | "id": 0, 73 | "prevSize": 32, 74 | "code": 59650, 75 | "name": "facebook" 76 | }, 77 | "setIdx": 1, 78 | "setId": 0, 79 | "iconIdx": 1 80 | } 81 | ], 82 | "height": 1024, 83 | "metadata": { 84 | "name": "icomoon" 85 | }, 86 | "preferences": { 87 | "showGlyphs": true, 88 | "showCodes": true, 89 | "showQuickUse": true, 90 | "showQuickUse2": true, 91 | "showSVGs": true, 92 | "fontPref": { 93 | "prefix": "icon-", 94 | "metadata": { 95 | "fontFamily": "icomoon" 96 | }, 97 | "metrics": { 98 | "emSize": 1024, 99 | "baseline": 6.25, 100 | "whitespace": 50 101 | }, 102 | "embed": false 103 | }, 104 | "imagePref": { 105 | "prefix": "icon-", 106 | "png": true, 107 | "useClassSelector": true, 108 | "color": 0, 109 | "bgColor": 16777215 110 | }, 111 | "historySize": 100 112 | } 113 | } -------------------------------------------------------------------------------- /style/icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('../assets/fonts/Icons.eot'); 4 | src: url('../assets/fonts/Icons.eot?#iefix') format('embedded-opentype'), 5 | url('../assets/fonts/Icons.woff') format('woff'), 6 | url('../assets/fonts/Icons.ttf') format('truetype'), 7 | url('../assets/fonts/Icons.svg#Icons') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'icomoon' !important; 15 | speak: none; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | .icon-menu:before { 28 | content: "\e900"; 29 | } 30 | .icon-twitter:before { 31 | content: "\e901"; 32 | } 33 | .icon-facebook:before { 34 | content: "\e902"; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /style/main.styl: -------------------------------------------------------------------------------- 1 | @import './variables.styl' 2 | 3 | * 4 | margin 0 5 | padding 0 6 | 7 | body 8 | height 100vh 9 | padding-left menu-width 10 | 11 | @media screen and (max-width breakpoint-medium) 12 | padding-left 0 13 | 14 | a 15 | color #1d9fcf 16 | text-decoration none 17 | 18 | h1, h2, h3, h4 19 | margin 0 20 | text-align center 21 | 22 | a 23 | color #333 24 | 25 | .pure-button 26 | font-family 'Open Sans', Arial, sans-serif 27 | color #fff 28 | text-transform uppercase 29 | font-size 12px 30 | padding 9px 20px 31 | border-radius 3px 32 | margin-top 2px 33 | 34 | &:hover 35 | background-image linear-gradient(rgba(#000, .1) 40%, rgba(#000, .10)) 36 | 37 | &.button-twitter 38 | background-color: #55acee 39 | 40 | &.button-facebook 41 | background-color: #3b5998 42 | 43 | &.disabled, &.button-disabled 44 | background #1695c5 45 | opacity .3 46 | 47 | &.button-primary 48 | background-color #1695c5 49 | 50 | &.button-secondary 51 | background-color color-red 52 | 53 | .pure-form 54 | 55 | input[type='text'] 56 | input[type='email'] 57 | input[type='password'] 58 | input[type='number'] 59 | select 60 | textarea 61 | margin-top 0 62 | box-shadow none 63 | border 0 64 | margin-right 20px 65 | margin-left 10px 66 | 67 | select 68 | color #333 69 | font-size 12px 70 | margin-top 2px 71 | 72 | label 73 | margin 0 74 | position relative 75 | top -3px 76 | font-size 12px 77 | 78 | .BuiltBy.large 79 | font-size 25px 80 | text-align center 81 | margin-top 50px 82 | font-family 'Open Sans', Arial, sans-serif 83 | font-weight 100 84 | color #666 85 | 86 | img 87 | vertical-align middle 88 | height 28px 89 | 90 | a, i 91 | color color-workshape 92 | 93 | i 94 | margin 0 5px -------------------------------------------------------------------------------- /style/variables.styl: -------------------------------------------------------------------------------- 1 | // See: https://designschool.canva.com/blog/website-color-schemes/ 2 | 3 | // Colours 4 | 5 | color-light-blue = #caebf2 6 | color-grey = #a9a9a9 7 | color-light-grey = #efefef 8 | color-red = #ff3b3f 9 | color-workshape = #e96132 10 | 11 | color-black = #262228 12 | color-white = #ffffff 13 | 14 | component-height = 90vh 15 | 16 | breakpoint-large = 1330px 17 | breakpoint-medium = 1069px 18 | breakpoint-small = 768px 19 | breakpoint-tiny = 565px 20 | 21 | color-code-bg = #255e73 22 | 23 | menu-width = 60px -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | var nib = require('nib'); 5 | 6 | var isDev = (process.env.NODE_ENV !== 'production'); 7 | var appEntry = './client/main'; 8 | 9 | var defineEnvPlugin = new webpack.DefinePlugin({ 10 | __DEV__: isDev 11 | }); 12 | 13 | var entryScripts = [ appEntry ]; 14 | var output = { 15 | path: path.join(__dirname, [ '/', 'build' ].join('')), 16 | filename: 'bundle.js' 17 | }; 18 | 19 | var plugins = [ 20 | defineEnvPlugin, 21 | new ExtractTextPlugin('style.css'), 22 | new webpack.NoErrorsPlugin() 23 | ]; 24 | 25 | var moduleLoaders = [ 26 | { 27 | test: /\.js$/, 28 | loaders: [ 'babel' ], 29 | exclude: /node_modules/, 30 | include: __dirname 31 | }, { 32 | test: /\.css?$/, 33 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader'), 34 | include: __dirname 35 | }, { 36 | test: /\.styl?$/, 37 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader!stylus-loader'), 38 | include: __dirname 39 | } 40 | ]; 41 | 42 | if (isDev) { 43 | output.publicPath = 'http://localhost:3001/'; 44 | plugins.push(new webpack.HotModuleReplacementPlugin()); 45 | entryScripts = [ 46 | 'webpack-dev-server/client?http://localhost:3001', 47 | 'webpack/hot/only-dev-server', 48 | appEntry 49 | ]; 50 | 51 | moduleLoaders = [ 52 | { 53 | test: /\.js$/, 54 | loaders: [ 'react-hot', 'babel' ], 55 | exclude: /node_modules/, 56 | include: __dirname 57 | }, { 58 | test: /\.css?$/, 59 | loaders: [ 'style-loader', 'css-loader' ], 60 | include: __dirname 61 | }, { 62 | test: /\.styl?$/, 63 | loaders: [ 'style-loader', 'css-loader', 'stylus-loader' ], 64 | include: __dirname 65 | } 66 | ]; 67 | } 68 | 69 | moduleLoaders.push([ 70 | {test: /\.gif$/, loader: 'url-loader?limit=100000&mimetype=image/gif'}, 71 | {test: /\.jpg$/, loader: 'url-loader?limit=10000&mimetype=image/jpg'}, 72 | {test: /\.png$/, loader: 'url-loader?limit=10000&mimetype=image/png'}, 73 | {test: /\.svg$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml'}, 74 | {test: /favicon\.ico$/, loader: 'file-loader?name=favicon.ico&limit=100000&mimetype=image/x-icon'}, 75 | {test: /\.eot$/, loader: 'url-loader?limit=10000&mimetype=application/vnd.ms-fontobject'}, 76 | {test: /\.woff$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff'}, 77 | {test: /\.ttf$/, loader: 'url-loader?limit=10000&mimetype=application/x-font-ttf'} 78 | ]); 79 | 80 | module.exports = { 81 | devtool: 'eval', 82 | entry: entryScripts, 83 | output: output, 84 | plugins: plugins, 85 | module: { 86 | loaders: moduleLoaders 87 | }, 88 | stylus: { 89 | use: [ nib() ], 90 | import: ['~nib/lib/nib/index.styl'] 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /webpack.dev.server.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 2 | 3 | var webpack = require('webpack'); 4 | var WebpackDevServer = require('webpack-dev-server'); 5 | 6 | var webpackConfig = require('./webpack.config'); 7 | 8 | var host = 'localhost'; 9 | var devServerPort = 3001; 10 | 11 | new WebpackDevServer(webpack(webpackConfig), { 12 | headers: { 'Access-Control-Allow-Origin': '*' }, 13 | historyApiFallback: true, 14 | hot: true, 15 | noInfo: false, 16 | publicPath: webpackConfig.output.publicPath 17 | }).listen(devServerPort, host, function (err) { 18 | if (err) { 19 | console.log(err); 20 | } 21 | 22 | console.log('Webpack Dev Server running at ' + host + ':' + devServerPort); 23 | }); 24 | --------------------------------------------------------------------------------