├── .DS_Store ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── AnimationData ├── .DS_Store ├── DAG.js ├── StructureSchema.js ├── gitData.js └── treeStructure.js ├── LICENSE ├── README.md ├── app-icon.png.icns ├── app.js ├── assets ├── .DS_Store ├── 64pxBlue │ ├── .DS_Store │ ├── c.png │ ├── css.png │ ├── file.png │ ├── folder.png │ ├── html.png │ ├── js.png │ ├── json.png │ ├── pdf.png │ ├── png.png │ ├── py.png │ ├── rb.png │ └── sql.png ├── 64pxRed │ ├── .DS_Store │ ├── c.png │ ├── css.png │ ├── file.png │ ├── folder.png │ ├── html.png │ ├── js.png │ ├── json.png │ ├── pdf.png │ ├── png.png │ ├── py.png │ ├── rb.png │ └── sql.png ├── app-icon.png ├── folder-icon.png ├── folder.png ├── git-icon.png ├── git.png ├── readme │ ├── file-structure-animation.png │ ├── git-animation.png │ └── lesson.png ├── setting-icon.png └── x-icon.png ├── components ├── Animation.js ├── Dashboard.js ├── Dropdown.js ├── GitAnimation.js ├── Lesson.js ├── Link.js ├── StructureAnimation.js ├── Terminal.js └── Tree.js ├── css └── style.css ├── gulpfile.js ├── index.html ├── lessons ├── .DS_Store ├── git-on-your-computer.js └── lesson-list.js ├── main.js ├── package.json ├── ptyInternal.js ├── tests └── tests.js └── visualizations ├── dependencies ├── DAGRE-LICENSE └── dagre-d3.js ├── git-visualization.js ├── link-visualization.js └── tree-visualization.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/.DS_Store -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | visualizations/dependencies/ 2 | AnimationData/DAG.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb', 3 | plugins: [ 4 | 'react', 5 | ], 6 | smarttabs: true, 7 | validateIndentation: { 8 | value: 2, 9 | allowTabs: true, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .DS_Store 4 | assets/.DS_Store 5 | test.js 6 | gTerm-darwin-x64/ 7 | gTerm-darwin-x64.zip 8 | -------------------------------------------------------------------------------- /AnimationData/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/AnimationData/.DS_Store -------------------------------------------------------------------------------- /AnimationData/DAG.js: -------------------------------------------------------------------------------- 1 | // Majority of file comes from emberjs https://github.com/emberjs/ember.js/blob/62e52938f48278a6cb838016108f3e35c18c8b3f/packages/ember-application/lib/system/dag.js 2 | 3 | function visit(vertex, fn, visited, path) { 4 | var name = vertex.name, 5 | vertices = vertex.incoming, 6 | names = vertex.incomingNames, 7 | len = names.length, 8 | i; 9 | if (!visited) { 10 | visited = {}; 11 | } 12 | if (!path) { 13 | path = []; 14 | } 15 | if (visited.hasOwnProperty(name)) { 16 | return; 17 | } 18 | path.push(name); 19 | visited[name] = true; 20 | for (i = 0; i < len; i++) { 21 | visit(vertices[names[i]], fn, visited, path); 22 | } 23 | fn(vertex, path); 24 | path.pop(); 25 | } 26 | 27 | function DAG() { 28 | this.names = []; 29 | this.vertices = {}; 30 | } 31 | 32 | DAG.prototype.add = function(name) { 33 | if (!name) { return; } 34 | if (this.vertices.hasOwnProperty(name)) { 35 | return this.vertices[name]; 36 | } 37 | var vertex = { 38 | name: name, incoming: {}, incomingNames: [], hasOutgoing: false, value: null 39 | }; 40 | this.vertices[name] = vertex; 41 | this.names.push(name); 42 | return vertex; 43 | }; 44 | 45 | DAG.prototype.map = function(name, value) { 46 | this.add(name).value = value; 47 | }; 48 | 49 | DAG.prototype.addEdge = function(fromName, toName) { 50 | if (!fromName || !toName || fromName === toName) { 51 | return; 52 | } 53 | var from = this.add(fromName), to = this.add(toName); 54 | if (to.incoming.hasOwnProperty(fromName)) { 55 | return; 56 | } 57 | function checkCycle(vertex, path) { 58 | if (vertex.name === toName) { 59 | throw new Error("cycle detected: " + toName + " <- " + path.join(" <- ")); 60 | } 61 | } 62 | visit(from, checkCycle); 63 | from.hasOutgoing = true; 64 | to.incoming[fromName] = from; 65 | to.incomingNames.push(fromName); 66 | }; 67 | 68 | DAG.prototype.topsort = function(fn) { 69 | var visited = {}, 70 | vertices = this.vertices, 71 | names = this.names, 72 | len = names.length, 73 | i, vertex; 74 | for (i = 0; i < len; i++) { 75 | vertex = vertices[names[i]]; 76 | if (!vertex.hasOutgoing) { 77 | visit(vertex, fn, visited); 78 | } 79 | } 80 | }; 81 | 82 | DAG.prototype.addEdges = function(name, value, before, after) { 83 | var i; 84 | this.map(name, value); 85 | if (before) { 86 | if (typeof before === 'string') { 87 | this.addEdge(name, before); 88 | } else { 89 | for (i = 0; i < before.length; i++) { 90 | this.addEdge(name, before[i]); 91 | } 92 | } 93 | } 94 | if (after) { 95 | if (typeof after === 'string') { 96 | this.addEdge(after, name); 97 | } else { 98 | for (i = 0; i < after.length; i++) { 99 | this.addEdge(after[i], name); 100 | } 101 | } 102 | } 103 | }; 104 | 105 | module.exports = DAG; 106 | -------------------------------------------------------------------------------- /AnimationData/StructureSchema.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /* eslint-disable strict */ 3 | /* eslint-disable no-unused-expressions */ 4 | 5 | // Use strict mode so that we can use let and const. 6 | 'use strict'; 7 | 8 | const exec = require('child_process').exec; 9 | const simpleGit = require('simple-git'); 10 | const path = require('path'); 11 | 12 | const container = { 13 | css: null, 14 | html: null, 15 | js: null, 16 | rb: null, 17 | py: null, 18 | json: null, 19 | pdf: null, 20 | png: null, 21 | }; 22 | 23 | function modifiedAnimation(info, object, item, string) { 24 | for (let i = 0, len = info.length; i < len; i++) { 25 | if (info[i].indexOf(item) > -1) { 26 | object.level = '#ccc'; 27 | object.icon = `assets/64pxRed/${string}.png`; 28 | return; 29 | } 30 | } 31 | return; 32 | } 33 | 34 | function terminalParse(item, object) { 35 | let itemParse; 36 | if (item[item.length - 1] === '/') { 37 | itemParse = 'folder'; 38 | object.name = item.substring(0, item.length - 1); 39 | object.type = 'directory'; 40 | object.icon = 'assets/64pxBlue/folder.png'; 41 | } else { 42 | itemParse = item.replace(/^\w+./, ''); 43 | if (!(itemParse in container)) itemParse = 'file'; 44 | object.icon = `assets/64pxBlue/${itemParse}.png`; 45 | return itemParse; 46 | } 47 | return itemParse; 48 | } 49 | 50 | const schemaMaker = (termOutput, directoryName, modified) => { 51 | let schema = { 52 | name: directoryName, 53 | children: [], 54 | position_x: '-10px', 55 | position_y: '-20px', 56 | value: 40, 57 | icon: 'assets/64pxBlue/folder.png', 58 | level: '#ccc', 59 | }; 60 | 61 | // Loop through reply and put it into a structure D3 can read 62 | termOutput.forEach(index => { 63 | if (index === '' || (index[0] === '.' && index.substring(0, 4) !== '.git')) return undefined; 64 | const elementObj = { 65 | name: index, 66 | level: '#ccc', 67 | }; 68 | if (index.substring(0, 4) === '.git') { 69 | if (index.substring(index.length - 1) === '/') { 70 | elementObj.icon = 'assets/folder.png'; 71 | elementObj.name = index.substring(0, index.length - 1); 72 | } else { 73 | elementObj.icon = 'assets/git.png'; 74 | } 75 | schema.children.push(elementObj); 76 | return undefined; 77 | } 78 | 79 | const temp = terminalParse(index, elementObj); 80 | if (modified) modifiedAnimation(modified, elementObj, index, temp); 81 | schema.children.push(elementObj); 82 | return undefined; 83 | }); 84 | schema = [schema]; 85 | return schema; 86 | }; 87 | 88 | module.exports = { 89 | dataSchema(pwd, asyncWaterfallCallback) { 90 | // child process that gets all items in a directory 91 | const command = `cd ${pwd}; ls -ap;`; 92 | 93 | exec(command, (err, stdout) => { 94 | const stdoutArr = stdout.split('\n'); 95 | const currentDirectoryName = path.parse(pwd).name; 96 | let modifiedFiles; 97 | // git command to check git status 98 | simpleGit(pwd).status((error, i) => { 99 | modifiedFiles = i.modified; 100 | const schema = schemaMaker(stdoutArr, currentDirectoryName, modifiedFiles); 101 | process.send ? process.send({ schema }) : asyncWaterfallCallback(null, schema); 102 | return schema; 103 | }); 104 | }); 105 | }, 106 | schemaMaker, 107 | }; 108 | -------------------------------------------------------------------------------- /AnimationData/gitData.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const exec = require('child_process').exec; 4 | 5 | module.exports = { 6 | gitHistory(pwd) { 7 | const commandForGitHistory = `cd ${pwd};git log --pretty="%h|%p|%s|%d"gi`; 8 | exec(commandForGitHistory, (err, stdout) => { 9 | if (err) { 10 | console.log(err.toString()); 11 | } else { 12 | // splits stdout git log string by new line character 13 | const stdoutArr = stdout.split('\n'); 14 | // makes each string in stdoutArr a subarray split at | 15 | const nestedCommitArr = stdoutArr.map((element) => element.split('|')); 16 | const graphObj = { gitGraph: nestedCommitArr }; 17 | process.send(graphObj); 18 | } 19 | }); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /AnimationData/treeStructure.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: 'Top Level', 4 | value: 15, 5 | children: [ 6 | { 7 | name: 'Level 2: A', 8 | children: [ 9 | { 10 | name: 'Son of A', 11 | }, 12 | { 13 | name: 'Daughter of A', 14 | }, 15 | ], 16 | }, 17 | { 18 | name: 'Level 2: B', 19 | }, 20 | ], 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 An Khong 4 | Copyright (c) 2016 Hamza Surti 5 | Copyright (c) 2016 Isaac Durand 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gTerm 2 | gTerm is a Mac desktop application for beginning developers who are just starting to use Git and the terminal. Built with [Electron](http://electron.atom.io/), gTerm combines a terminal emulator with animations of your file structure and Git status. Our goal is to help you visualize what happens when you run terminal commands. 3 | 4 | ![What you'll see when you use gTerm](http://gterm.xyz/gTermDemo.gif) 5 | 6 | ## Getting started 7 | Currently, gTerm is available only for Mac. To get started... 8 | 9 | - Visit [gTerm.xyz](http://gterm.xyz/) and download our ZIP folder. 10 | - Unzip the folder, right-click the gTerm application, and select "Open". 11 | - You may see a warning that gTerm is "from an unidentified developer". Click the "Open" button to access the app. 12 | 13 | ## Features 14 | 15 | ### Terminal emulator 16 | You can use the terminal emulator at the bottom of our app just like you'd use your Mac's terminal. When you run a command, you'll see not only the text-based response you expect from your terminal but also a visualization above the emulator. 17 | 18 | 19 | ### File structure animation 20 | When you open gTerm, you'll see your current working directory on the left and all the items inside it on the right. Each item has an icon indicating its file type. Red icons indicate Git-tracked files with uncommitted changes, or folders containing files with uncommitted changes. Black icons indicate hidden Git files and folders. 21 | 22 | ![gTerm's file structure animation](/assets/readme/file-structure-animation.png) 23 | 24 | When you use the terminal emulator to change directories or commit changes, the animation will update automatically. You can also navigate through your file structure by clicking the folder icons in the animation. 25 | 26 | To switch to a view of your Git history, use the toggle at the top right of the animation. 27 | 28 | ### Git animation 29 | This animation shows your current repository's commit history, rendered as a directed acyclic graph. Each commit is represented by its hash. When you hover over any commit, you'll see the commit message associated with it. 30 | 31 | ![gTerm's Git animation](/assets/readme/git-animation.png) 32 | 33 | If your current directory is not part of a Git repository, this animation will be blank. 34 | 35 | ### Lesson 36 | For those who are new to the terminal, gTerm includes a lesson on Git and terminal basics, called "Git on your computer". You can access it using the "Lessons" dropdown menu at the top right of the app. 37 | 38 | ![gTerm's introductory lesson](/assets/readme/lesson.png) 39 | 40 | The lesson guides you through the process of running basic commands in our terminal emulator. We'll provide feedback as you go on whether you're running these commands correctly. If you make a mistake, we'll show you a red error message with a helpful hint. Once you've fixed your mistake, you can continue moving through the lesson. 41 | 42 | We hope to add more lessons in the future on topics like GitHub. 43 | -------------------------------------------------------------------------------- /app-icon.png.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/app-icon.png.icns -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | // Our bundle file uses these modules: 4 | const pty = require('pty.js'); 5 | const ipcRenderer = require('electron').ipcRenderer; 6 | -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/.DS_Store -------------------------------------------------------------------------------- /assets/64pxBlue/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/.DS_Store -------------------------------------------------------------------------------- /assets/64pxBlue/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/c.png -------------------------------------------------------------------------------- /assets/64pxBlue/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/css.png -------------------------------------------------------------------------------- /assets/64pxBlue/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/file.png -------------------------------------------------------------------------------- /assets/64pxBlue/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/folder.png -------------------------------------------------------------------------------- /assets/64pxBlue/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/html.png -------------------------------------------------------------------------------- /assets/64pxBlue/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/js.png -------------------------------------------------------------------------------- /assets/64pxBlue/json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/json.png -------------------------------------------------------------------------------- /assets/64pxBlue/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/pdf.png -------------------------------------------------------------------------------- /assets/64pxBlue/png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/png.png -------------------------------------------------------------------------------- /assets/64pxBlue/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/py.png -------------------------------------------------------------------------------- /assets/64pxBlue/rb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/rb.png -------------------------------------------------------------------------------- /assets/64pxBlue/sql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxBlue/sql.png -------------------------------------------------------------------------------- /assets/64pxRed/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/.DS_Store -------------------------------------------------------------------------------- /assets/64pxRed/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/c.png -------------------------------------------------------------------------------- /assets/64pxRed/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/css.png -------------------------------------------------------------------------------- /assets/64pxRed/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/file.png -------------------------------------------------------------------------------- /assets/64pxRed/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/folder.png -------------------------------------------------------------------------------- /assets/64pxRed/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/html.png -------------------------------------------------------------------------------- /assets/64pxRed/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/js.png -------------------------------------------------------------------------------- /assets/64pxRed/json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/json.png -------------------------------------------------------------------------------- /assets/64pxRed/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/pdf.png -------------------------------------------------------------------------------- /assets/64pxRed/png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/png.png -------------------------------------------------------------------------------- /assets/64pxRed/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/py.png -------------------------------------------------------------------------------- /assets/64pxRed/rb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/rb.png -------------------------------------------------------------------------------- /assets/64pxRed/sql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/64pxRed/sql.png -------------------------------------------------------------------------------- /assets/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/app-icon.png -------------------------------------------------------------------------------- /assets/folder-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/folder-icon.png -------------------------------------------------------------------------------- /assets/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/folder.png -------------------------------------------------------------------------------- /assets/git-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/git-icon.png -------------------------------------------------------------------------------- /assets/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/git.png -------------------------------------------------------------------------------- /assets/readme/file-structure-animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/readme/file-structure-animation.png -------------------------------------------------------------------------------- /assets/readme/git-animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/readme/git-animation.png -------------------------------------------------------------------------------- /assets/readme/lesson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/readme/lesson.png -------------------------------------------------------------------------------- /assets/setting-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/setting-icon.png -------------------------------------------------------------------------------- /assets/x-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/assets/x-icon.png -------------------------------------------------------------------------------- /components/Animation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import GitAnimation from './GitAnimation'; 3 | import StructureAnimation from './StructureAnimation'; 4 | 5 | export default class Animation extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.showGit = this.showGit.bind(this); 9 | this.showStructure = this.showStructure.bind(this); 10 | } 11 | 12 | showGit() { 13 | this.props.setStructureAnimationVisibility(false); 14 | } 15 | 16 | showStructure() { 17 | this.props.setStructureAnimationVisibility(true); 18 | } 19 | 20 | buildStyles(structureAnimationVisible) { 21 | const styles = {}; 22 | 23 | styles.padder = { padding: '16px' }; 24 | styles.toggle = { float: 'right', border: '1px solid black', textAlign: 'center', 25 | position: 'absolute', right: '13px', zIndex: 0 }; 26 | styles.git = { borderBottom: '1px solid black', padding: '2px 2px 0px 2px' }; 27 | styles.structure = { padding: '2px 2px 0px 2px' }; 28 | 29 | if (structureAnimationVisible) { 30 | styles.git.backgroundColor = 'transparent'; 31 | styles.structure.backgroundColor = 'lightBlue'; 32 | } else { 33 | styles.git.backgroundColor = 'lightBlue'; 34 | styles.structure.backgroundColor = 'transparent'; 35 | } 36 | 37 | return styles; 38 | } 39 | 40 | // The images in the toggle are from 41 | // https://www.iconfinder.com/icons/172515/folder_opened_icon#size=32 42 | // and https://www.iconfinder.com/icons/83306/git_icon#size=32 43 | render() { 44 | // Render only the animation that is currently selected. 45 | const selectedAnimation = this.props.structureAnimationVisible ? 46 | : ; 47 | 48 | const styles = this.buildStyles(this.props.structureAnimationVisible); 49 | 50 | return ( 51 |
52 |
53 |
54 |
55 | Git view 56 |
57 |
58 | Directory view 59 |
60 |
61 | {selectedAnimation} 62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | Animation.propTypes = { 69 | setStructureAnimationVisibility: React.PropTypes.func, 70 | structureAnimationVisible: React.PropTypes.bool, 71 | }; 72 | -------------------------------------------------------------------------------- /components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | // Import other components 5 | import Animation from './Animation'; 6 | import Lesson from './Lesson'; 7 | import Dropdown from './Dropdown'; 8 | import Terminal from './Terminal'; 9 | 10 | // Import the list of existing lessons 11 | import lessons from './../lessons/lesson-list'; 12 | 13 | export default class Dashboard extends Component { 14 | constructor(props) { 15 | super(props); 16 | this.setErrorVisibility = this.setErrorVisibility.bind(this); 17 | this.setStructureAnimationVisibility = this.setStructureAnimationVisibility.bind(this); 18 | this.setDropdownVisibility = this.setDropdownVisibility.bind(this); 19 | this.showLesson = this.showLesson.bind(this); 20 | this.hideLesson = this.hideLesson.bind(this); 21 | this.changeSlide = this.changeSlide.bind(this); 22 | this.state = { 23 | lessonNumber: undefined, 24 | slideNumber: undefined, 25 | dropdownVisible: props.initialDropdownVisible, 26 | structureAnimationVisible: props.initialStructureAnimationVisible, 27 | lessonVisible: props.initialLessonVisible, 28 | errorVisible: props.initialErrorVisible, 29 | }; 30 | } 31 | 32 | setErrorVisibility(boolean) { 33 | this.setState({ 34 | errorVisible: boolean, 35 | }); 36 | } 37 | 38 | setStructureAnimationVisibility(boolean) { 39 | this.setState({ 40 | structureAnimationVisible: boolean, 41 | }); 42 | } 43 | 44 | setDropdownVisibility() { 45 | this.setState({ 46 | dropdownVisible: !this.state.dropdownVisible, 47 | }); 48 | } 49 | 50 | showLesson(index) { 51 | this.setState({ 52 | lessonNumber: index, 53 | slideNumber: 0, 54 | lessonVisible: true, 55 | }); 56 | } 57 | 58 | hideLesson() { 59 | this.setState({ 60 | lessonVisible: false, 61 | }); 62 | } 63 | 64 | changeSlide(number) { 65 | this.setState({ 66 | slideNumber: number, 67 | }); 68 | } 69 | 70 | buildStyles() { 71 | const styles = {}; 72 | 73 | styles.dashboard = { height: '100%', width: '100%' }; 74 | styles.dropdown = { height: '40px', width: '100%', backgroundColor: 'tranparent', 75 | borderBottom: '1px solid #D2D2D2' }; 76 | styles.main = { height: '100vh', width: '100%' }; 77 | styles.upperHalf = { height: '55%', width: '100%' }; 78 | styles.span = { textAlign: 'center', marginLeft: '10%', bottom: '45%', position: 'absolute', 79 | color: '#B0AEAE', fontSize: '300%', fontFamily: 'monospace' }; 80 | styles.lowerHalf = { height: '45%', width: '100%', backgroundColor: '#151414', 81 | borderTop: '10px solid #D2D2D2' }; 82 | 83 | return styles; 84 | } 85 | 86 | render() { 87 | const styles = this.buildStyles(); 88 | 89 | // Create an array of lesson names to pass down to Dropdown as props. 90 | // (We don't need to pass down all the lesson contents.) 91 | const lessonInfo = lessons.map(lesson => 92 | ({ 93 | name: lesson.name, 94 | iconPath: lesson.iconPath, 95 | }) 96 | ); 97 | 98 | // Render the lesson only if it should be visible. 99 | const lesson = this.state.lessonVisible ? 100 | : undefined; 104 | 105 | return ( 106 |
107 |
108 | 112 |
113 |
114 |
115 | 118 | gTerm 119 |
120 |
121 | {lesson} 122 | 123 |
124 |
125 |
126 | ); 127 | } 128 | } 129 | 130 | Dashboard.propTypes = { 131 | initialDropdownVisible: React.PropTypes.bool, 132 | initialStructureAnimationVisible: React.PropTypes.bool, 133 | initialLessonVisible: React.PropTypes.bool, 134 | initialErrorVisible: React.PropTypes.bool, 135 | }; 136 | 137 | Dashboard.defaultProps = { 138 | initialDropdownVisible: false, 139 | initialStructureAnimationVisible: true, 140 | initialLessonVisible: false, 141 | initialErrorVisible: false, 142 | }; 143 | 144 | render(, document.getElementById('dashboard-container')); 145 | -------------------------------------------------------------------------------- /components/Dropdown.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Dropdown extends Component { 4 | buildLessonList(lessonInfo, clickFunction) { 5 | const styles = {}; 6 | styles.li = { 7 | padding: '15px 15px 15px 15px', 8 | border: '1px solid #F9F9F9', 9 | backgroundColor: 'white', 10 | fontFamily: 'sans-serif', 11 | fontSize: '80%', 12 | color: '#A09E9E', 13 | maxHeight: '0', 14 | transition: 'max-height 500ms ease!important', 15 | }; 16 | styles.ul = { 17 | listStyle: 'none', 18 | padding: '0', 19 | margin: '0', 20 | marginTop: '14px', 21 | }; 22 | 23 | // Create an array of list items, one per lesson. 24 | // Since we only have one lesson right now, we are hard-coding two decoy lessons. 25 | const lessonArr = []; 26 | for (let i = 0; i < 3; i++) { 27 | lessonArr.push( 28 |
  • clickFunction(0)} style={styles.li} key={i}> 29 | {i === 0 ? lessonInfo[i].name : `GitHub ${i}`} 30 |
  • 31 | ); 32 | } 33 | 34 | return
      {lessonArr}
    ; 35 | } 36 | 37 | buildStyle() { 38 | return { 39 | position: 'absolute', 40 | right: 10, 41 | backgroundColor: 'tranparent', 42 | zIndex: 2, 43 | fontFamily: 'sans-serif', 44 | transform: 'translateY(11px)', 45 | color: '#A09E9E', 46 | textAlign: 'right', 47 | }; 48 | } 49 | 50 | render() { 51 | // Create a list of lessons only if the dropdown is visible. 52 | const lessonList = this.props.dropdownVisible ? 53 | this.buildLessonList(this.props.lessonInfo, this.props.showLesson) : undefined; 54 | 55 | const style = this.buildStyle(); 56 | 57 | return ( 58 | 62 | ); 63 | } 64 | } 65 | 66 | Dropdown.propTypes = { 67 | lessonInfo: React.PropTypes.array, 68 | showLesson: React.PropTypes.func, 69 | dropdownVisible: React.PropTypes.bool, 70 | setDropdownVisibility: React.PropTypes.func, 71 | }; 72 | -------------------------------------------------------------------------------- /components/GitAnimation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // We don't need to define ipcRenderer because it will be loaded by the time this file runs. 3 | import React, { Component } from 'react'; 4 | import gitVisualization from './../visualizations/git-visualization.js'; 5 | 6 | export default class GitAnimation extends Component { 7 | 8 | componentDidMount() { 9 | ipcRenderer.send('ready-for-git', '\n'); 10 | ipcRenderer.on('git-graph', (event, nestedCommitArr) => { 11 | gitVisualization.renderGraph(gitVisualization.createGraph(nestedCommitArr)); 12 | }); 13 | window.updateCommitMessage = message => { this.refs.message.textContent = message; }; 14 | } 15 | 16 | buildStyle() { 17 | return { color: 'blue' }; 18 | } 19 | 20 | render() { 21 | const style = this.buildStyle(); 22 | 23 | return ( 24 |
    25 |

    Hover over any commit in your Git history to see the commit message.

    26 |

    Commit message:

    27 |
    28 | 29 | 30 | 31 |
    32 |
    33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components/Lesson.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // We don't need to define ipcRenderer because it will be loaded by the time this file runs. 3 | 4 | import React, { Component } from 'react'; 5 | import lessons from './../lessons/lesson-list'; 6 | 7 | export default class Lesson extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.handleButtonClick = this.handleButtonClick.bind(this); 11 | } 12 | 13 | handleButtonClick(lesson, slideNumber, errorVisible) { 14 | // If this slide has a buttonFunction, run it. 15 | if (lesson[slideNumber].buttonFunction) { 16 | lesson[slideNumber].buttonFunction(); 17 | 18 | // Listen for the result of the buttonFunction test. 19 | ipcRenderer.once('test-result-2', (event, arg) => { 20 | // If the user passed the test (if arg is true), advance and hide the error message. 21 | if (arg) { 22 | this.advance(lesson, slideNumber); 23 | if (errorVisible) this.props.setErrorVisibility(false); 24 | // If the user failed the test, show the error message. 25 | } else { 26 | this.props.setErrorVisibility(true); 27 | } 28 | }); 29 | 30 | // If the slide does not have a buttonFunction, simply advance. 31 | } else { 32 | this.advance(lesson, slideNumber); 33 | } 34 | } 35 | 36 | advance(lesson, slideNumber) { 37 | // If we're on the last slide, go to slide 0. 38 | // If not, go to the next slide. 39 | const destination = slideNumber === lesson.length - 1 ? 0 : slideNumber + 1; 40 | this.props.changeSlide(destination); 41 | } 42 | 43 | buildStyles() { 44 | const styles = {}; 45 | 46 | styles.lesson = { float: 'left', height: '100%', width: '35%', overflow: 'scroll', 47 | fontFamily: 'Helvetica, sans-serif', backgroundColor: 'white' }; 48 | styles.padder = { padding: '16px' }; 49 | styles.img = { float: 'right' }; 50 | styles.error = { color: 'red' }; 51 | 52 | return styles; 53 | } 54 | 55 | // The X icon is from https://www.iconfinder.com/icons/118584/x_icon#size=32 56 | render() { 57 | const currentLesson = lessons[this.props.lessonNumber].content; 58 | const styles = this.buildStyles(); 59 | 60 | // Render the error message only if it should be visible. 61 | const error = this.props.errorVisible ? 62 |

    63 | { currentLesson[this.props.slideNumber].errorMessage } 64 |

    : 65 | undefined; 66 | 67 | return ( 68 |
    69 |
    70 |
    71 | Click here to close this tutorial 74 | Slide {this.props.slideNumber + 1} of {currentLesson.length} 75 | {currentLesson[this.props.slideNumber].lessonText} 76 | {error} 77 | 84 |
    85 |
    86 |
    87 | ); 88 | } 89 | } 90 | 91 | Lesson.propTypes = { 92 | lessonNumber: React.PropTypes.number, 93 | slideNumber: React.PropTypes.number, 94 | errorVisible: React.PropTypes.bool, 95 | changeSlide: React.PropTypes.func, 96 | hideLesson: React.PropTypes.func, 97 | setErrorVisibility: React.PropTypes.func, 98 | }; 99 | -------------------------------------------------------------------------------- /components/Link.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import d3 from 'd3'; 4 | 5 | // Import the d3 functions we'll use to visualize our links. 6 | import linkVisualization from './../visualizations/link-visualization'; 7 | 8 | export default class Path extends Component { 9 | componentDidMount() { 10 | // This function runs when a new path is added to the DOM. 11 | this.d3Node = d3.select(ReactDOM.findDOMNode(this)); 12 | this.d3Node.datum(this.props.data) 13 | .call(linkVisualization.enter, linkVisualization.diagonal, linkVisualization.duration); 14 | } 15 | 16 | componentDidUpdate() { 17 | this.d3Node.datum(this.props.data) 18 | .call(linkVisualization.update, linkVisualization.diagonal, linkVisualization.duration); 19 | } 20 | 21 | buildStyle() { 22 | return { fill: 'none', stroke: '#ccc', strokeWidth: '1.5px' }; 23 | } 24 | 25 | render() { 26 | const id = `linkTo${this.props.data.target.name}`; 27 | const style = this.buildStyle(); 28 | 29 | return ; 30 | } 31 | } 32 | 33 | Path.propTypes = { 34 | data: React.PropTypes.object, 35 | }; 36 | -------------------------------------------------------------------------------- /components/StructureAnimation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // We don't need to define ipcRenderer because it will be loaded by the time this file runs. 3 | /* eslint-disable no-param-reassign */ 4 | 5 | import React, { Component } from 'react'; 6 | import d3 from 'd3'; 7 | import Tree from './Tree'; 8 | import Link from './Link'; 9 | 10 | // Import default file structure data 11 | import treeData from './../AnimationData/treeStructure'; 12 | 13 | // In this component, React is responsible for DOM structure (adding and removing elements) 14 | // and D3 is responsible for styling. 15 | // This blog post provided inspiration: 16 | // https://medium.com/@sxywu/on-d3-react-and-a-little-bit-of-flux-88a226f328f3#.ztcxqykek 17 | 18 | export default class StructureAnimation extends Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | treeData: props.initialTreeData, 23 | margin: props.initialMargin, 24 | windowWidth: props.initialWindowWidth, 25 | windowHeight: props.initialWindowHeight, 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | // This fires on initial load and when the user toggles between the structure and Git views. 31 | ipcRenderer.send('ready-for-schema', '\n'); 32 | ipcRenderer.on('direc-schema', (e, arg) => { 33 | this.updateTree(arg); 34 | }); 35 | } 36 | 37 | updateTree(newSchema) { 38 | this.setState({ 39 | treeData: newSchema, 40 | windowHeight: window.innerHeight, 41 | windowWidth: window.innerWidth, 42 | }); 43 | } 44 | 45 | buildStyles(windowWidth, windowHeight, margin) { 46 | const styles = {}; 47 | 48 | // Create variables to determine the size of the tree and the SVG containing it. 49 | styles.viewBoxWidth = this.state.windowWidth * 7 / 12; 50 | styles.viewBoxHeight = this.state.windowHeight * 7 / 24; 51 | styles.viewBoxString = `0 0 ${styles.viewBoxWidth} ${styles.viewBoxHeight}`; 52 | styles.translationValue = `translate(${margin.left}, ${margin.top})`; 53 | 54 | return styles; 55 | } 56 | 57 | buildTree(viewBoxHeight, viewBoxWidth) { 58 | const layout = {}; 59 | 60 | // Create a tree layout. 61 | // The first argument provided to size() is the maximum x-coordinate D3 will assign. 62 | // The second argument is the maximum y-coordinate. 63 | // We're switching width and height here because d3 by default makes trees that branch 64 | // vertically, and we want a tree that branches horizontally. 65 | // In other words, nodes that are on the same level will have the same y-coordinate 66 | // but different x-coordinates. 67 | const tree = d3.layout.tree() 68 | .size([viewBoxHeight * 0.93, viewBoxWidth * 0.90]); 69 | 70 | // The first node in the array is the root of the tree. 71 | // Set its initial coordinates - where it should enter. 72 | const root = this.state.treeData[0]; 73 | root.x0 = viewBoxHeight / 2; 74 | root.y0 = 0; 75 | 76 | // Create an array of nodes associated with the root. 77 | // (The returned array is basically a flattened version of treeData.) 78 | // Before the next line runs, each node has children, level, name, and value properties. 79 | // After the next line runs, each node also has parent, depth, x, and y properties. 80 | // (D3 tree nodes always have parent, child, depth, x, and y properties.) 81 | // We will pass one node from this array to each Tree as props.data. 82 | layout.nodes = tree.nodes(root); 83 | 84 | // Create an array of objects representing all parent-child links 85 | // in the nodes array we just created. 86 | layout.linkSelection = tree.links(layout.nodes); 87 | 88 | layout.nodes.forEach(d => { 89 | // The default y-coordinates provided by d3.tree will make the tree stretch 90 | // all the way across the screen. 91 | // We want to compress the tree a bit so that there's room for file/directory names 92 | // to the right of the deepest level. 93 | d.y *= 0.8; 94 | 95 | // If the node has a parent, set its initial coordinates to the parent's initial coordinates. 96 | // In other words, the parent and child should enter from the same place. 97 | if (d.parent) { 98 | d.x0 = d.parent.x0; 99 | d.y0 = d.parent.y0; 100 | } 101 | }); 102 | 103 | return layout; 104 | } 105 | 106 | render() { 107 | const styles = this.buildStyles(this.state.windowWidth, this.state.windowHeight, 108 | this.state.margin); 109 | 110 | const layout = this.buildTree(styles.viewBoxHeight, styles.viewBoxWidth); 111 | 112 | // Create a counter variable that we'll use to stagger the items in our animation. 113 | let counter = 1; 114 | 115 | const trees = layout.nodes && layout.nodes.map((node) => { 116 | // Save the starting value of node.y as node.yOriginal so we can use it in the future. 117 | if (node.yOriginal === undefined) node.yOriginal = node.y; 118 | 119 | // Give the node an index property to determine how it will be staggered. 120 | node.index = counter ++; 121 | 122 | // If node.index is odd, adjust node.y, which determines the position of this tree and the 123 | // link to it. 124 | if (node.index % 2 === 1) node.y = node.yOriginal * 0.9; 125 | 126 | // Parse node.name to extract a unique key for this tree. 127 | node.name = node.name.trim(); 128 | const nameEndsWithSlash = node.name.indexOf('/') === node.name.length - 1; 129 | const key = nameEndsWithSlash ? node.name.slice(0, node.name.length - 1) : node.name; 130 | return (); 131 | }); 132 | 133 | const links = layout.linkSelection && layout.linkSelection.map((link) => { 134 | link.target.name = link.target.name.trim(); 135 | const nameEndsWithSlash = link.target.name.indexOf('/') === link.target.name.length - 1; 136 | const key = nameEndsWithSlash ? link.target.name.slice(0, link.target.name.length - 1) : 137 | link.target.name; 138 | return (); 139 | }); 140 | 141 | return ( 142 |
    143 | 144 | 145 | {links} 146 | {trees} 147 | 148 | 149 |
    150 | ); 151 | } 152 | } 153 | 154 | StructureAnimation.defaultProps = { 155 | initialTreeData: treeData, 156 | initialMargin: { top: 0, left: 20 }, 157 | // The initial window dimensions are specified in app.on('ready') in main.js. 158 | initialWindowWidth: 1200, 159 | initialWindowHeight: 700, 160 | }; 161 | 162 | StructureAnimation.propTypes = { 163 | initialTreeData: React.PropTypes.array, 164 | initialMargin: React.PropTypes.object, 165 | initialWindowWidth: React.PropTypes.number, 166 | initialWindowHeight: React.PropTypes.number, 167 | }; 168 | -------------------------------------------------------------------------------- /components/Terminal.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // We don't need to define ipcRenderer because it will be loaded by the time this file runs. 3 | 4 | import React, { Component } from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import Term from 'term.js'; 7 | 8 | export default class Terminal extends Component { 9 | // Once the Terminal component mounts, append a terminal emulator to it. 10 | componentDidMount() { 11 | const mountTerm = ReactDOM.findDOMNode(this); 12 | this.renderTerm(mountTerm); 13 | } 14 | 15 | buildStyle(lessonVisible) { 16 | const style = { float: 'left', height: '100%', backgroundColor: '#151414', 17 | borderColor: '#151414', zIndex: 3 }; 18 | style.width = lessonVisible ? '65%' : '100%'; 19 | return style; 20 | } 21 | 22 | renderTerm(elem) { 23 | // Determine how many rows and columns the terminal emulator should have, based on the size of 24 | // the Terminal component. 25 | const $Terminal = document.getElementById('Terminal'); 26 | const columns = ($Terminal.offsetWidth / 6.71) - 1; 27 | const numRows = Math.floor($Terminal.offsetHeight / 18); 28 | 29 | // Create a new term.js terminal emulator. 30 | const term = new Term({ 31 | cursorBlink: true, 32 | useStyle: true, 33 | cols: columns, 34 | rows: numRows, 35 | }); 36 | 37 | // Append it to the Terminal component. 38 | term.open(elem); 39 | 40 | ipcRenderer.once('term-start-data', (e, arg) => { 41 | term.write(arg); 42 | }); 43 | 44 | term.on('data', (data) => { 45 | ipcRenderer.send('command-message', data); 46 | }); 47 | 48 | ipcRenderer.on('terminal-reply', (event, arg) => { 49 | term.write(arg); 50 | }); 51 | 52 | // Resize the terminal emulator when the window resizes. 53 | window.addEventListener('resize', () => { 54 | const cols = Math.ceil(($Terminal.offsetWidth / 6.71) - 1); 55 | const rows = Math.floor($Terminal.offsetHeight / 18); 56 | const sizeObj = { cols, rows }; 57 | term.resize(cols, rows); 58 | ipcRenderer.send('command-message', sizeObj); 59 | }); 60 | } 61 | 62 | render() { 63 | const style = this.buildStyle(this.props.lessonVisible); 64 | return
    ; 65 | } 66 | } 67 | 68 | Terminal.propTypes = { 69 | lessonVisible: React.PropTypes.bool, 70 | }; 71 | -------------------------------------------------------------------------------- /components/Tree.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import d3 from 'd3'; 4 | 5 | // Import the d3 functions we'll use to visualize our trees. 6 | import treeVisualization from './../visualizations/tree-visualization'; 7 | 8 | export default class Tree extends Component { 9 | 10 | componentDidMount() { 11 | // This function runs when a new file/folder is added to the DOM. 12 | // this.d3Node is a d3 selection - an array of elements D3 can operate on. 13 | this.d3Node = d3.select(ReactDOM.findDOMNode(this)); 14 | this.d3Node.datum(this.props.data) 15 | .call(treeVisualization.enter, treeVisualization.duration); 16 | } 17 | 18 | componentDidUpdate() { 19 | // This function runs when a file/folder already on the DOM is updated. 20 | this.d3Node.datum(this.props.data) 21 | .call(treeVisualization.update, treeVisualization.duration); 22 | } 23 | 24 | buildStyles() { 25 | const styles = {}; 26 | 27 | styles.main = { cursor: 'pointer' }; 28 | styles.image = { fill: '#fff', stroke: 'steelblue', strokeWidth: '1.5px' }; 29 | styles.text = { font: '10px sans-serif' }; 30 | 31 | return styles; 32 | } 33 | 34 | render() { 35 | const styles = this.buildStyles(); 36 | 37 | return ( 38 | 39 | 40 | {this.props.data.name} 41 | 42 | ); 43 | } 44 | } 45 | 46 | Tree.propTypes = { 47 | data: React.PropTypes.object, 48 | }; 49 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-width: 900px; 3 | margin: 0px; 4 | } 5 | 6 | #dashboard-container { 7 | position: absolute; 8 | top: 0; 9 | bottom: 0; 10 | left: 0; 11 | right: 0; 12 | } 13 | 14 | /* For our React components, we relied primarily on inline styling. 15 | In a few situations, using an external stylesheet made more sense. 16 | We've included this styling below. */ 17 | code { 18 | font-family: "DejaVu Sans Mono", "Liberation Mono", monospace; 19 | color: white; 20 | background-color: black; 21 | } 22 | 23 | /* We adapted our button styling adapted from Skeleton: 24 | https://github.com/dhg/Skeleton/blob/master/css/skeleton.css. 25 | Using inline styling for hover/focus would have been verbose: 26 | http://stackoverflow.com/questions/28365233 */ 27 | button { 28 | display: inline-block; 29 | height: 38px; 30 | padding: 0 30px; 31 | text-align: center; 32 | font-family: Helvetica, sans-serif; 33 | font-size: 14px; 34 | line-height: 38px; 35 | letter-spacing: .1rem; 36 | text-transform: uppercase; 37 | text-decoration: none; 38 | color: #FFF; 39 | background-color: #6BCBF8; 40 | border-radius: 4px; 41 | border: none; 42 | cursor: pointer; 43 | outline: none; 44 | } 45 | 46 | button:hover, 47 | button:focus { 48 | background-color: #65C0EB; 49 | } 50 | 51 | /* Styling for the terminal emulator appended to the Terminal component by renderTerm */ 52 | .terminal { 53 | color: #d2d2d2!important; 54 | border: 0!important; 55 | background-color: #151414!important; 56 | margin-top: 7px!important; 57 | bottom: 0px!important; 58 | font-size: 13px; 59 | margin-left: 5px 60 | } 61 | 62 | #Dropdown > span:hover { 63 | cursor: pointer!important; 64 | } 65 | 66 | #Dropdown > ul > li:hover { 67 | background-color: #F9F9F9!important; 68 | color: black!important; 69 | cursor: pointer; 70 | max-height:150px!important; 71 | } 72 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const gulp = require('gulp'); 4 | const browserify = require('browserify'); 5 | const babelify = require('babelify'); 6 | const watchify = require('watchify'); 7 | const source = require('vinyl-source-stream'); 8 | const notify = require('gulp-notify'); 9 | 10 | function handleErrors(...args) { 11 | const argsCopy = Array.prototype.slice.call(args); 12 | notify.onError({ 13 | title: 'Compile Error', 14 | message: '<%= error.message %>', 15 | }).apply(this, argsCopy); 16 | this.emit('end'); // Keeps gulp from hanging on this task 17 | } 18 | 19 | function buildScript(file, watch) { 20 | const props = { 21 | entries: [`./components/${file}`], 22 | debug: true, 23 | transform: babelify.configure({ 24 | presets: ['react', 'es2015', 'stage-0'], 25 | }), 26 | }; 27 | 28 | // Watchify if watch is set to true. Otherwise browserify once. 29 | const bundler = watch ? watchify(browserify(props)) : browserify(props); 30 | 31 | function rebundle() { 32 | const stream = bundler.bundle(); 33 | return stream 34 | .on('error', handleErrors) 35 | .pipe(source('bundle.js')) 36 | .pipe(gulp.dest('./build/')); 37 | } 38 | 39 | bundler.on('update', () => { 40 | const updateStart = Date.now(); 41 | rebundle(); 42 | console.log(`Updated! ${Date.now() - updateStart} ms`); 43 | }); 44 | 45 | // Run it once the first time buildScript is called. 46 | return rebundle(); 47 | } 48 | 49 | // Run once. 50 | gulp.task('scripts', () => buildScript('Dashboard.js', false)); 51 | 52 | // Run 'scripts' task first, then watch for future changes. 53 | gulp.task('default', ['scripts'], () => buildScript('Dashboard.js', true)); 54 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gTerm 6 | 7 | 8 | 9 |
    10 |
    11 |
    12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lessons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSchooled/git-started/93976929b06b731a0c94604c204ad3625c16adba/lessons/.DS_Store -------------------------------------------------------------------------------- /lessons/git-on-your-computer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | /* eslint-disable no-undef */ 3 | // We don't need to define ipcRenderer because it will be loaded by the time this file runs. 4 | 5 | // Import React so we can use JSX. 6 | import React from 'react'; 7 | 8 | // Create a variable to hold the name of the current directory. 9 | // We will update this variable once we start running tests. 10 | let currentDirectory; 11 | 12 | // Create an array of slides to export. 13 | const slides = [ 14 | { 15 | lessonText: 16 |
    17 |

    Welcome!

    18 |

    If you're learning to code, chances are you've heard about something called Git. Git can be intimidating for beginners - but it doesn't have to be!

    19 |

    In this lesson, you'll...

    20 |
      21 |
    • Set up Git
    • 22 |
    • Set up a project
    • 23 |
    • Learn some basic Git commands that you can use to track your new project
    • 24 |
    • Learn a bit about GitHub, a popular website that uses Git
    • 25 |
    26 |

    Don't worry - we'll walk you through each step. Ready to get started?

    27 |
    , 28 | buttonText: 'Ready!', 29 | buttonFunction() { 30 | // Start listening for updates to currentDirectory 31 | ipcRenderer.on('curr-dir', (event, arg) => { 32 | currentDirectory = arg; 33 | }); 34 | ipcRenderer.send('test-result-1', true); 35 | }, 36 | 37 | }, { 38 | lessonText: 39 |
    40 |

    Step 1: Set up Git

    41 |

    Meet the terminal

    42 |

    For this step, we'll use the terminal, which lets you and your Mac communicate using just text.

    43 |

    Software developers use their terminals every day, but no need to worry if this tool is new to you. We've embedded a terminal in this app to help you learn the ropes - it's the black box to the right.

    44 |
    , // ' 45 | buttonText: 'Got it!', 46 | 47 | }, { 48 | lessonText: 49 |
    50 |

    Step 1: Set up Git

    51 |

    Check your version

    52 |

    To check whether your computer already has Git installed, type git --version 53 | and then click Enter.

    54 |

    If you see a version number - like git version 2.6.4 - you have Git 55 | installed, and you're ready for step two.

    56 |

    If not, you can download Git from http://git-scm.com/downloads. Then follow the 57 | directions at the top of this page to confirm that Git is installed correctly.

    58 |
    , // ' 59 | buttonText: "OK - what's next?", 60 | buttonFunction() { 61 | // Check whether the user has installed Git 62 | ipcRenderer.send('command-to-run', 'git --version'); 63 | if (!currentDirectory) ipcRenderer.send('ready-for-dir', '\n'); 64 | ipcRenderer.once('terminal-output', (event, arg) => { 65 | // If terminal-output contains the text 'git version', the user should pass. 66 | // If not, the user shouldn't. 67 | ipcRenderer.send('test-result-1', arg.indexOf('git version') > -1); 68 | }); 69 | }, 70 | errorMessage: "Oops! It looks like you haven't installed Git. Try again and then click the button below.", 71 | 72 | }, { 73 | lessonText: 74 |
    75 |

    Step 2: Set up a project

    76 |

    Make a directory

    77 |

    OK, let's take a quick break from Git to set up our project.

    78 |

    In the past, you've probably used the Finder or a program like Microsoft Word to create new folders and files. For this lesson, we'll use the terminal.

    79 |

    Ready? Type mkdir new-project and then click Enter.

    80 |

    You'll see this folder being added to your current directory above.

    81 |
    , 82 | buttonText: 'Done!', 83 | buttonFunction() { 84 | // Check whether the user has created new-project 85 | const commandToRun = `cd ${currentDirectory}; cd new-project`; 86 | ipcRenderer.send('command-to-run', commandToRun); 87 | ipcRenderer.once('terminal-output', (event, arg) => { 88 | // If we can't cd into new-project, the terminal will create an error, and arg will be a 89 | // string starting with 'Error.' In this case, the user should fail the test, so we'll 90 | // return a falsy value: zero. Otherwise, the user should pass. 91 | ipcRenderer.send('test-result-1', arg.indexOf('Error')); 92 | }); 93 | }, 94 | errorMessage: "Oops! It looks like you haven't created a new directory called 'new-project'. Try again and then click the button below.", 95 | 96 | }, { 97 | lessonText: 98 |
    99 |

    Step 2: Set up a project

    100 |

    Navigate to your new directory

    101 |

    As you just saw, you just created a new folder called "new-project." (The "mkdir" command is short for "make directory," and "directory" is just a fancy word for folder.)

    102 |

    Now type cd new-project and click Enter to navigate to your new directory. (The "cd" command is short for "change directory.") This is where you'll store all the files for your project.

    103 |
    , // ' 104 | buttonText: 'Got it!', 105 | buttonFunction() { 106 | // Check whether the user has navigated into new-project. 107 | // If so, currentDirectory will end with 'new-project' 108 | const endOfCurrentPath = currentDirectory.slice(-11); 109 | ipcRenderer.send('test-result-1', endOfCurrentPath === 'new-project'); 110 | }, 111 | errorMessage: "Oops! It looks like you haven't navigated into the 'new-project' directory. Try again and then click the button below.", 112 | 113 | }, { 114 | lessonText: 115 |
    116 |

    Step 2: Set up a project

    117 |

    Create a file

    118 |

    Now let's add a file to your folder.

    119 |

    In the terminal, type touch new-file.txt and click Enter.

    120 |

    Notice that we haven't been typing the word "git" in the commands we're using to create directories and files and to navigate through them. That's because these commands aren't specific to Git - but we'll get back to Git now.

    121 |
    , 122 | buttonText: 'Onward!', 123 | buttonFunction() { 124 | // This test and subsequent tests depend on the user being inside the new-project 125 | // directory they created. 126 | // Check whether the user has created new-file.txt inside the currentDirectory; 127 | const commandToRun = `cd ${currentDirectory}; ls`; 128 | ipcRenderer.send('command-to-run', commandToRun); 129 | ipcRenderer.once('terminal-output', (event, arg) => { 130 | // If the terminal-output contains 'new-file.txt', the user should pass. 131 | ipcRenderer.send('test-result-1', arg.indexOf('new-file.txt') > -1); 132 | }); 133 | }, 134 | errorMessage: "Oops! It looks like you haven't created a file called 'new-file.txt', or you aren't inside the 'new-project' directory. Try again and then click the button below.", 135 | 136 | }, { 137 | lessonText: 138 |
    139 |

    Step 3: Learn Git commands

    140 |

    git init

    141 |

    Now that we've created a project, we need to tell Git that we want to track it. To do this, type git init and then click Enter. This initializes a new Git repository, or "repo."

    142 |
    , // ' 143 | buttonText: "What's a repo?", 144 | buttonFunction() { 145 | // Check whether the user has initialized their repo successfully 146 | const commandToRun = `cd ${currentDirectory}; git status`; 147 | ipcRenderer.send('command-to-run', commandToRun); 148 | ipcRenderer.once('terminal-output', (event, arg) => { 149 | // If the repo hasn't been initialized, the terminal will create an error, 150 | // and arg will be a string starting with 'Error.' 151 | // In this case, the user should fail the test, so we'll return a falsy value: zero. 152 | // Otherwise, the user should pass. 153 | ipcRenderer.send('test-result-1', arg.indexOf('Error')); 154 | }); 155 | }, 156 | errorMessage: "Oops! It looks like you haven't initialized your repo, or you aren't inside the 'new-project' directory. Try again and then click the button below.", 157 | 158 | }, { 159 | lessonText: 160 |
    161 |

    Step 3: Learn Git commands

    162 |

    git status

    163 |

    A repo contains all the files Git is tracking for a project, as well as the revision history for these files. All this information is stored in a special folder called ".git" inside your project folder.

    164 |

    Your .git folder is hidden by default, so you won't see it in your Finder. However, you can use the terminal to make sure you've initialized your repository correctly. Just type git status and then click Enter. As long as you don't see this error message - fatal: Not a git repository - you're good to go.

    165 |
    , 166 | buttonText: "I'm good to go!", 167 | 168 | }, { 169 | lessonText: 170 |
    171 |

    Step 3: Learn Git commands

    172 |

    git add

    173 |

    So you've initialized a Git repository, and you've created the first file for your project. Now it's time to tell Git that you'd like to track that file.

    174 |

    Type git add new-file.txt and click Enter to add this file to your "staging area." This tells Git that you want to track changes to new-file.txt, and that you're getting ready to make a commit.

    175 |
    , // ' 176 | buttonText: "What's a commit?", 177 | buttonFunction() { 178 | // Check whether the user has git-added new-file.text 179 | const commandToRun = `cd ${currentDirectory}; git status`; 180 | ipcRenderer.send('command-to-run', commandToRun); 181 | ipcRenderer.once('terminal-output', (event, arg) => { 182 | // Assume the user has failed until they prove otherwise. 183 | let didUserPass = false; 184 | // If new-file.txt has been added to the staging area, arg should contain 'Changes to be 185 | // committed' followed by either 'modified: new-file.txt' or 'new file: new-file.txt' 186 | // There should NOT be an instance of 'Changes not staged for commit' between 'Changes to be 187 | // committed' and 'new-file.txt'. Let's check these conditions with regex. 188 | const regExp = /Changes to be committed([\s\S]+)new-file[.]txt/; 189 | const result = regExp.exec(arg); 190 | // If the result is truthy (isn't null), arg contains 'Changes to be committed' followed by 191 | // 'new-file.txt', and we should continue examine the result. 192 | if (result) { 193 | // If the match contains 'Changes not staged for commit', keep didUserPass false. If not, 194 | // change didUserPass to true. 195 | if (result[1].indexOf('Changes not staged for commit') === -1) { 196 | didUserPass = true; 197 | } 198 | } 199 | ipcRenderer.send('test-result-1', didUserPass); 200 | }); 201 | }, 202 | errorMessage: "Oops! It looks like you haven't added new-file.txt to your staging area, or you aren't inside the 'new-project' directory. Try again and then click the button below.", 203 | 204 | }, { 205 | lessonText: 206 |
    207 |

    Step 3: Learn Git commands

    208 |

    git commit -m

    209 |

    You can think of a commit as a snapshot of your code at a specific point in time. Git saves each commit to your repo, along with a message you write to describe the status of the project.

    210 |

    So let's commit - type git commit -m "create file to store text for project" and click Enter. (The "-m" is for "message," and you can replace the bolded text with whatever message you like.)

    211 |

    It's a best practice to commit early and often, and to write descriptive commit messages. 212 | The more descriptive your commit messages are, the easier it will be to find specific 213 | commits when you want to refer back to them in the future!

    214 |
    , 215 | buttonText: "What's next?", 216 | buttonFunction() { 217 | // Check whether the user has git-committed new-file.text. 218 | const commandToRun = `cd ${currentDirectory}; git log; git status`; 219 | ipcRenderer.send('command-to-run', commandToRun); 220 | ipcRenderer.once('terminal-output', (event, arg) => { 221 | let didUserPass = true; 222 | // If arg.indexOf('fatal') is 0, arg starts with 'fatal', 223 | // and there are no commits in the git log. 224 | if (!arg.indexOf('fatal')) { 225 | didUserPass = false; 226 | // Otherwise, check the git status. 227 | } else { 228 | // If it doesn't contain 'nothing to commit, working directory clean', 229 | // the user has uncommitted changes. 230 | if (arg.indexOf('nothing to commit, working directory clean') === -1) didUserPass = false; 231 | // Otherwise, we can be confident that the user committed, 232 | // so we can keep didUserPass true. 233 | } 234 | ipcRenderer.send('test-result-1', didUserPass); 235 | }); 236 | }, 237 | errorMessage: "Oops! It looks like you have changes that you haven't committed, or you aren't inside the 'new-project' directory. Try again and then click the button below.", 238 | 239 | }, { 240 | lessonText: 241 |
    242 |

    Step 3: Learn Git commands

    243 |

    Take a break from Git to edit your file

    244 |

    OK, you've made a commit for this project - the first snapshot of many! But it wasn't a very exciting snapshot, because new-file.txt was empty. So let's add some text to the file and then commit again.

    245 |

    Typically, developers write code using a text editor like Atom or Sublime Text. For this lesson, though, we'll practice editing a file directly from the terminal - something you can do even without Git.

    246 |

    In the terminal, type echo "This will be the best project ever." > new-file.txt and click Enter. (Again, you can replace the bolded part with whatever text you wish.)

    247 |

    In the animation above, you'll notice that new-file.txt turns red when you modify it. This means it has changes that you haven't committed yet.

    248 |
    , 249 | buttonText: 'Done!', 250 | buttonFunction() { 251 | // Check whether the user has edited new-file.text 252 | const commandToRun = `cd ${currentDirectory}; git status`; 253 | ipcRenderer.send('command-to-run', commandToRun); 254 | ipcRenderer.once('terminal-output', (event, arg) => { 255 | // If the user has modified new-file.txt, arg should contain 256 | // 'modified:' + whitespace + 'new-file.txt'. 257 | const regExp = /(modified:\s+new-file[.]txt)/; 258 | ipcRenderer.send('test-result-1', regExp.test(arg)); 259 | }); 260 | }, 261 | errorMessage: 262 | "Oops! It looks like you haven't made any changes to new-file.txt since your last commit, or you aren't inside the 'new-project' directory. Try again and then click the button below.", 263 | 264 | }, { 265 | lessonText: 266 |
    267 |

    Step 3: Learn Git commands

    268 |

    git diff

    269 |

    To make sure we actually edited the file, type git diff and click Enter.

    270 |

    This will show us the differences between the current version of the project and our most 271 | recent commit. Remember: last time we committed, new-file.txt was empty.

    272 |
    , 273 | buttonText: 'I see my changes!', 274 | 275 | }, { 276 | lessonText: 277 |
    278 |

    Congrats!

    279 |

    You've finished our lesson on using Git on your computer.

    280 |

    Now you're ready to start learning about GitHub, a popular website that makes it easy to 281 | back up your Git projects online and collaborate with other developers.

    282 |
    , 283 | buttonText: 'Learn about Github', 284 | 285 | }, { 286 | lessonText: 287 |
    288 |

    The GitHub lesson is coming soon, but it isn't ready yet. Would you like to repeat the 289 | lesson you just finished?

    290 |
    , // ' 291 | buttonText: 'Sure!', 292 | }, 293 | ]; 294 | 295 | export { slides as lesson1 }; 296 | 297 | // A valuable resource for this lesson was https://github.com/jlord/git-it-electron. 298 | -------------------------------------------------------------------------------- /lessons/lesson-list.js: -------------------------------------------------------------------------------- 1 | import { lesson1 } from './git-on-your-computer'; 2 | 3 | // As we create new lessons, we can add new objects to this array. 4 | export default [ 5 | { 6 | name: 'Git on your computer', 7 | content: lesson1, 8 | iconPath: 'assets/git-icon.png', 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable strict */ 2 | 3 | // Use strict mode so that we can use let and const. 4 | 'use strict'; 5 | 6 | const electron = require('electron'); 7 | const app = electron.app; 8 | const ipcMain = require('electron').ipcMain; 9 | const BrowserWindow = electron.BrowserWindow; 10 | const animationDataSchema = require('./AnimationData/StructureSchema'); 11 | const async = require('async'); 12 | 13 | // Require the child_process module so we can communicate with the user's terminal 14 | const exec = require('child_process').exec; 15 | const fork = require('child_process').fork; 16 | 17 | let mainWindow = null; 18 | 19 | function ptyChildProcess() { 20 | const ptyInternal = require.resolve('./ptyInternal'); 21 | const forkProcess = fork(ptyInternal); 22 | 23 | // Prevent the app from showing dummy data on initial load. 24 | ipcMain.on('ready-for-schema', (event, arg) => forkProcess.send({ message: arg })); 25 | 26 | // Ensure that we have Git data to show the first time the users toggles to the Git view. 27 | ipcMain.on('ready-for-git', (event, arg) => forkProcess.send({ message: arg })); 28 | 29 | // For the lesson, ensure that we know the user's current directory before testing whether they 30 | // created a 'new-project' directory. 31 | ipcMain.on('ready-for-dir', (event, arg) => forkProcess.send({ message: arg })); 32 | 33 | // When user inputs data in terminal, start fork and run pty inside. 34 | // Each keystroke is an arg. 35 | ipcMain.on('command-message', (event, arg) => { 36 | forkProcess.send({ message: arg }); 37 | forkProcess.removeAllListeners('message'); 38 | forkProcess.on('message', (message) => { 39 | // Send what is diplayed in terminal 40 | if (message.data) event.sender.send('terminal-reply', message.data); 41 | // Send animation schema 42 | if (message.schema) event.sender.send('direc-schema', message.schema); 43 | // Send Git data 44 | if (message.gitGraph) event.sender.send('git-graph', message.gitGraph); 45 | // Send current directory 46 | if (message.currDir) event.sender.send('curr-dir', message.currDir); 47 | }); 48 | }); 49 | } 50 | 51 | // Run the appropriate test to see whether the user is ready to advance in the lesson. 52 | function slideTests() { 53 | // Listen for commands from the lesson file. 54 | ipcMain.on('command-to-run', (event, arg) => { 55 | // Upon receiving a command, run it in the terminal. 56 | exec(arg, (err, stdout) => { 57 | // Send the terminal's response back to the lesson. 58 | if (err) return event.sender.send('terminal-output', err.toString()); 59 | return event.sender.send('terminal-output', stdout); 60 | }); 61 | }); 62 | 63 | // Listen for a Boolean from the lesson file. 64 | ipcMain.on('test-result-1', (event, arg) => { 65 | // Upon receiving it, send it to the Dashboard. 66 | event.sender.send('test-result-2', arg); 67 | }); 68 | } 69 | 70 | 71 | app.on('window-all-closed', () => { 72 | if (process.platform !== 'darwin') { 73 | app.quit(); 74 | } 75 | }); 76 | 77 | app.on('ready', () => { 78 | const dummy = new BrowserWindow({ show: false }); 79 | // Force replace icon to load 80 | dummy.setProgressBar(-1); 81 | 82 | mainWindow = new BrowserWindow({ 83 | width: 1200, 84 | height: 700, 85 | minWidth: 900, 86 | minHeight: 500, 87 | titleBarStyle: 'hidden-inset', 88 | }); 89 | 90 | mainWindow.loadURL(`file://${__dirname}/index.html`); 91 | 92 | // Initialize fork 93 | mainWindow.webContents.on('did-finish-load', () => { 94 | setTimeout(async.waterfall([ 95 | async.apply(animationDataSchema.dataSchema, process.env.HOME), 96 | (data) => { 97 | mainWindow.webContents.send('direc-schema', data); 98 | }, 99 | ]), 0); 100 | 101 | mainWindow.webContents.send('term-start-data', `${process.env.HOME} $ `); 102 | 103 | ptyChildProcess(); 104 | slideTests(); 105 | }); 106 | 107 | // Set mainWindow back to null when the window is closed. 108 | mainWindow.on('closed', () => { 109 | mainWindow = null; 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gTerm", 3 | "version": "0.1.0", 4 | "main": "main.js", 5 | "scripts": { 6 | "start": "electron .", 7 | "task": "gulp", 8 | "test": "mocha tests/tests.js" 9 | }, 10 | "devDependencies": { 11 | "assert": "^1.3.0", 12 | "babel-eslint": "^6.0.0", 13 | "babel-preset-es2015": "^6.0.15", 14 | "babel-preset-react": "^6.0.15", 15 | "babel-preset-stage-0": "^6.5.0", 16 | "babelify": "^7.2.0", 17 | "browserify": "^10.2.4", 18 | "chromedriver": "^2.21.2", 19 | "electron-prebuilt": "^0.36.9", 20 | "eslint-config-airbnb": "^6.2.0", 21 | "eslint-plugin-react": "^4.2.3", 22 | "gulp": "^3.9.0", 23 | "gulp-notify": "^2.2.0", 24 | "mocha": "^2.4.5", 25 | "selenium-webdriver": "^2.53.1", 26 | "vinyl-source-stream": "^1.1.0", 27 | "watchify": "^3.2.2" 28 | }, 29 | "dependencies": { 30 | "async": "^2.0.0-rc.1", 31 | "chai": "^3.5.0", 32 | "d3": "^3.5.16", 33 | "pty.js": "^0.3.0", 34 | "react": "^0.14", 35 | "react-dom": "^0.14.0", 36 | "simple-git": "^1.28.1", 37 | "term.js": "0.0.7" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ptyInternal.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable strict */ 2 | 3 | 'use strict'; 4 | 5 | const pty = require('pty.js'); 6 | const animationDataSchema = require('./AnimationData/StructureSchema'); 7 | const getGitData = require('./AnimationData/gitData'); 8 | 9 | let currDir; 10 | const ptyTerm = pty.fork('bash', [], { 11 | name: 'xterm-color', 12 | cols: 175, 13 | rows: 17, 14 | cwd: process.env.HOME, 15 | env: process.env, 16 | }); 17 | 18 | // Set the terminal prompt to pwd. 19 | // We can read the bash profile here with the source command. 20 | ptyTerm.write(`PROMPT_COMMAND='PS1=$(pwd)" $ "'\r`); 21 | ptyTerm.write('clear \r'); 22 | 23 | process.once('message', () => animationDataSchema.dataSchema(process.env.HOME)); 24 | 25 | process.on('message', data => { if (!data.message.cols) ptyTerm.write(data.message); }); 26 | 27 | ptyTerm.on('data', data => { 28 | // Find path 29 | process.send({ 30 | data, 31 | }); 32 | const re = /\s[$]\s/g; 33 | if (data.match(re)) { 34 | let temp = data; 35 | temp = temp.replace(re, ''); 36 | currDir = temp; 37 | process.send({ currDir }); 38 | animationDataSchema.dataSchema(currDir); 39 | getGitData.gitHistory(currDir); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | /* eslint-disable no-unused-expressions */ 3 | /* eslint-env mocha */ 4 | 5 | const expect = require('chai').expect; 6 | const StructureSchema = require('../AnimationData/StructureSchema'); 7 | 8 | describe('StructureSchema', () => { 9 | it('should have a dataSchema key', () => { 10 | expect(StructureSchema.dataSchema).to.exist; 11 | }); 12 | it('should return a schema that has a name property when schemaMaker is called', () => { 13 | const schemaResult = StructureSchema.schemaMaker(['stdout', 'banana'], process.env.HOME)[0]; 14 | expect(schemaResult).to.have.property('name', process.env.HOME); 15 | }); 16 | it('schemaMaker should return a schema that has a children property that is an array with a name stdout', () => { 17 | const schemaResult = StructureSchema.schemaMaker(['stdout', 'banana'], process.env.HOME)[0]; 18 | expect(schemaResult).to.have.deep.property('children[0].name', 'stdout'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /visualizations/dependencies/DAGRE-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Chris Pettitt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /visualizations/dependencies/dagre-d3.js: -------------------------------------------------------------------------------- 1 | const d3 = require('d3'); 2 | 3 | ;(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Math.abs(dx) * h) { 367 | // Intersection is top or bottom of rect. 368 | if (dy < 0) { 369 | h = -h; 370 | } 371 | sx = dy === 0 ? 0 : h * dx / dy; 372 | sy = h; 373 | } else { 374 | // Intersection is left or right of rect. 375 | if (dx < 0) { 376 | w = -w; 377 | } 378 | sx = w; 379 | sy = dx === 0 ? 0 : w * dy / dx; 380 | } 381 | 382 | return {x: x + sx, y: y + sy}; 383 | }; 384 | 385 | 386 | },{"dagre":4}],3:[function(require,module,exports){ 387 | module.exports = '0.0.1'; 388 | 389 | },{}],4:[function(require,module,exports){ 390 | /* 391 | Copyright (c) 2012-2013 Chris Pettitt 392 | 393 | Permission is hereby granted, free of charge, to any person obtaining a copy 394 | of this software and associated documentation files (the "Software"), to deal 395 | in the Software without restriction, including without limitation the rights 396 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 397 | copies of the Software, and to permit persons to whom the Software is 398 | furnished to do so, subject to the following conditions: 399 | 400 | The above copyright notice and this permission notice shall be included in 401 | all copies or substantial portions of the Software. 402 | 403 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 404 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 405 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 406 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 407 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 408 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 409 | THE SOFTWARE. 410 | */ 411 | exports.Digraph = require("graphlib").Digraph; 412 | exports.Graph = require("graphlib").Graph; 413 | exports.layout = require("./lib/layout/layout"); 414 | exports.version = require("./lib/version"); 415 | 416 | },{"./lib/layout/layout":6,"./lib/version":11,"graphlib":12}],5:[function(require,module,exports){ 417 | var util = require("../util"); 418 | 419 | module.exports = instrumentedRun; 420 | module.exports.undo = undo; 421 | 422 | function instrumentedRun(g, debugLevel) { 423 | var timer = util.createTimer(); 424 | var reverseCount = util.createTimer().wrap("Acyclic Phase", run)(g); 425 | if (debugLevel >= 2) console.log("Acyclic Phase: reversed " + reverseCount + " edge(s)"); 426 | } 427 | 428 | function run(g) { 429 | var onStack = {}, 430 | visited = {}, 431 | reverseCount = 0; 432 | 433 | function dfs(u) { 434 | if (u in visited) return; 435 | 436 | visited[u] = onStack[u] = true; 437 | g.outEdges(u).forEach(function(e) { 438 | var t = g.target(e), 439 | a; 440 | 441 | if (t in onStack) { 442 | a = g.edge(e); 443 | g.delEdge(e); 444 | a.reversed = true; 445 | ++reverseCount; 446 | g.addEdge(e, t, u, a); 447 | } else { 448 | dfs(t); 449 | } 450 | }); 451 | 452 | delete onStack[u]; 453 | } 454 | 455 | g.eachNode(function(u) { dfs(u); }); 456 | 457 | return reverseCount; 458 | } 459 | 460 | function undo(g) { 461 | g.eachEdge(function(e, s, t, a) { 462 | if (a.reversed) { 463 | delete a.reversed; 464 | g.delEdge(e); 465 | g.addEdge(e, t, s, a); 466 | } 467 | }); 468 | } 469 | 470 | },{"../util":10}],6:[function(require,module,exports){ 471 | var util = require("../util"), 472 | rank = require("./rank"), 473 | acyclic = require("./acyclic"), 474 | Digraph = require("graphlib").Digraph, 475 | Graph = require("graphlib").Graph, 476 | Set = require("graphlib").data.Set; 477 | 478 | module.exports = function() { 479 | // External configuration 480 | var config = { 481 | // How much debug information to include? 482 | debugLevel: 0, 483 | }; 484 | 485 | var timer = util.createTimer(); 486 | 487 | // Phase functions 488 | var 489 | order = require("./order")(), 490 | position = require("./position")(); 491 | 492 | // This layout object 493 | var self = {}; 494 | 495 | self.orderIters = delegateProperty(order.iterations); 496 | 497 | self.nodeSep = delegateProperty(position.nodeSep); 498 | self.edgeSep = delegateProperty(position.edgeSep); 499 | self.universalSep = delegateProperty(position.universalSep); 500 | self.rankSep = delegateProperty(position.rankSep); 501 | self.rankDir = delegateProperty(position.rankDir); 502 | self.debugAlignment = delegateProperty(position.debugAlignment); 503 | 504 | self.debugLevel = util.propertyAccessor(self, config, "debugLevel", function(x) { 505 | timer.enabled(x); 506 | order.debugLevel(x); 507 | position.debugLevel(x); 508 | }); 509 | 510 | self.run = timer.wrap("Total layout", run); 511 | 512 | self._normalize = normalize; 513 | 514 | return self; 515 | 516 | /* 517 | * Constructs an adjacency graph using the nodes and edges specified through 518 | * config. For each node and edge we add a property `dagre` that contains an 519 | * object that will hold intermediate and final layout information. Some of 520 | * the contents include: 521 | * 522 | * 1) A generated ID that uniquely identifies the object. 523 | * 2) Dimension information for nodes (copied from the source node). 524 | * 3) Optional dimension information for edges. 525 | * 526 | * After the adjacency graph is constructed the code no longer needs to use 527 | * the original nodes and edges passed in via config. 528 | */ 529 | function buildAdjacencyGraph(inputGraph) { 530 | var g = new Digraph(); 531 | 532 | inputGraph.eachNode(function(u, value) { 533 | if (value === undefined) value = {}; 534 | g.addNode(u, { 535 | width: value.width, 536 | height: value.height 537 | }); 538 | }); 539 | 540 | inputGraph.eachEdge(function(e, u, v, value) { 541 | if (value === undefined) value = {}; 542 | var newValue = { 543 | e: e, 544 | minLen: value.minLen || 1, 545 | width: value.width || 0, 546 | height: value.height || 0, 547 | points: [] 548 | }; 549 | 550 | g.addEdge(null, u, v, newValue); 551 | 552 | // If input graph is not directed, we create also add a reverse edge. 553 | // After we've run the acyclic algorithm we'll remove one of these edges. 554 | if (!inputGraph.isDirected()) { 555 | g.addEdge(null, v, u, newValue); 556 | } 557 | }); 558 | 559 | return g; 560 | } 561 | 562 | function run(inputGraph) { 563 | var rankSep = self.rankSep(); 564 | var g; 565 | try { 566 | // Build internal graph 567 | g = buildAdjacencyGraph(inputGraph); 568 | 569 | if (g.order() === 0) { 570 | return g; 571 | } 572 | 573 | // Make space for edge labels 574 | g.eachEdge(function(e, s, t, a) { 575 | a.minLen *= 2; 576 | }); 577 | self.rankSep(rankSep / 2); 578 | 579 | // Reverse edges to get an acyclic graph, we keep the graph in an acyclic 580 | // state until the very end. 581 | acyclic(g, config.debugLevel); 582 | 583 | // Our intermediate graph is always directed. However, the input graph 584 | // may be undirected, so we create duplicate edges in opposite directions 585 | // in the buildAdjacencyGraph function. At this point one of each pair of 586 | // edges was reversed, so we remove the redundant edge. 587 | if (!inputGraph.isDirected()) { 588 | removeDupEdges(g); 589 | } 590 | 591 | // Determine the rank for each node. Nodes with a lower rank will appear 592 | // above nodes of higher rank. 593 | rank(g, config.debugLevel); 594 | 595 | // Normalize the graph by ensuring that every edge is proper (each edge has 596 | // a length of 1). We achieve this by adding dummy nodes to long edges, 597 | // thus shortening them. 598 | normalize(g); 599 | 600 | // Order the nodes so that edge crossings are minimized. 601 | order.run(g); 602 | 603 | // Find the x and y coordinates for every node in the graph. 604 | position.run(g); 605 | 606 | // De-normalize the graph by removing dummy nodes and augmenting the 607 | // original long edges with coordinate information. 608 | undoNormalize(g); 609 | 610 | // Reverses points for edges that are in a reversed state. 611 | fixupEdgePoints(g); 612 | 613 | // Reverse edges that were revered previously to get an acyclic graph. 614 | acyclic.undo(g); 615 | 616 | // Construct final result graph and return it 617 | return createFinalGraph(g, inputGraph.isDirected()); 618 | } finally { 619 | self.rankSep(rankSep); 620 | } 621 | } 622 | 623 | function removeDupEdges(g) { 624 | var visited = new Set(); 625 | g.eachEdge(function(e, u, v, value) { 626 | if (visited.has(value.e)) { 627 | g.delEdge(e); 628 | } 629 | visited.add(value.e); 630 | }); 631 | } 632 | 633 | /* 634 | * This function is responsible for "normalizing" the graph. The process of 635 | * normalization ensures that no edge in the graph has spans more than one 636 | * rank. To do this it inserts dummy nodes as needed and links them by adding 637 | * dummy edges. This function keeps enough information in the dummy nodes and 638 | * edges to ensure that the original graph can be reconstructed later. 639 | * 640 | * This method assumes that the input graph is cycle free. 641 | */ 642 | function normalize(g) { 643 | var dummyCount = 0; 644 | g.eachEdge(function(e, s, t, a) { 645 | var sourceRank = g.node(s).rank; 646 | var targetRank = g.node(t).rank; 647 | if (sourceRank + 1 < targetRank) { 648 | for (var u = s, rank = sourceRank + 1, i = 0; rank < targetRank; ++rank, ++i) { 649 | var v = "_D" + (++dummyCount); 650 | var node = { 651 | width: a.width, 652 | height: a.height, 653 | edge: { id: e, source: s, target: t, attrs: a }, 654 | rank: rank, 655 | dummy: true 656 | }; 657 | 658 | // If this node represents a bend then we will use it as a control 659 | // point. For edges with 2 segments this will be the center dummy 660 | // node. For edges with more than two segments, this will be the 661 | // first and last dummy node. 662 | if (i === 0) node.index = 0; 663 | else if (rank + 1 === targetRank) node.index = 1; 664 | 665 | g.addNode(v, node); 666 | g.addEdge(null, u, v, {}); 667 | u = v; 668 | } 669 | g.addEdge(null, u, t, {}); 670 | g.delEdge(e); 671 | } 672 | }); 673 | } 674 | 675 | /* 676 | * Reconstructs the graph as it was before normalization. The positions of 677 | * dummy nodes are used to build an array of points for the original "long" 678 | * edge. Dummy nodes and edges are removed. 679 | */ 680 | function undoNormalize(g) { 681 | g.eachNode(function(u, a) { 682 | if (a.dummy && "index" in a) { 683 | var edge = a.edge; 684 | if (!g.hasEdge(edge.id)) { 685 | g.addEdge(edge.id, edge.source, edge.target, edge.attrs); 686 | } 687 | var points = g.edge(edge.id).points; 688 | points[a.index] = { x: a.x, y: a.y, ul: a.ul, ur: a.ur, dl: a.dl, dr: a.dr }; 689 | g.delNode(u); 690 | } 691 | }); 692 | } 693 | 694 | /* 695 | * For each edge that was reversed during the `acyclic` step, reverse its 696 | * array of points. 697 | */ 698 | function fixupEdgePoints(g) { 699 | g.eachEdge(function(e, s, t, a) { if (a.reversed) a.points.reverse(); }); 700 | } 701 | 702 | function createFinalGraph(g, isDirected) { 703 | var out = isDirected ? new Digraph() : new Graph(); 704 | g.eachNode(function(u, value) { out.addNode(u, value); }); 705 | g.eachEdge(function(e, u, v, value) { 706 | out.addEdge("e" in value ? value.e : e, u, v, value); 707 | delete value.e; 708 | }); 709 | return out; 710 | } 711 | 712 | /* 713 | * Given a function, a new function is returned that invokes the given 714 | * function. The return value from the function is always the `self` object. 715 | */ 716 | function delegateProperty(f) { 717 | return function() { 718 | if (!arguments.length) return f(); 719 | f.apply(null, arguments); 720 | return self; 721 | }; 722 | } 723 | }; 724 | 725 | },{"../util":10,"./acyclic":5,"./order":7,"./position":8,"./rank":9,"graphlib":12}],7:[function(require,module,exports){ 726 | var util = require("../util"); 727 | 728 | module.exports = function() { 729 | var config = { 730 | iterations: 24, // max number of iterations 731 | debugLevel: 0 732 | }; 733 | 734 | var timer = util.createTimer(); 735 | 736 | var self = {}; 737 | 738 | self.iterations = util.propertyAccessor(self, config, "iterations"); 739 | 740 | self.debugLevel = util.propertyAccessor(self, config, "debugLevel", function(x) { 741 | timer.enabled(x); 742 | }); 743 | 744 | self._initOrder = initOrder; 745 | 746 | self.run = timer.wrap("Order Phase", run); 747 | self.crossCount = crossCount; 748 | self.bilayerCrossCount = bilayerCrossCount; 749 | 750 | return self; 751 | 752 | function run(g) { 753 | var layering = initOrder(g); 754 | var bestLayering = copyLayering(layering); 755 | var bestCC = crossCount(g, layering); 756 | 757 | if (config.debugLevel >= 2) { 758 | console.log("Order phase start cross count: " + bestCC); 759 | } 760 | 761 | var cc, i, lastBest; 762 | for (i = 0, lastBest = 0; lastBest < 4 && i < config.iterations; ++i, ++lastBest) { 763 | cc = sweep(g, i, layering); 764 | if (cc < bestCC) { 765 | bestLayering = copyLayering(layering); 766 | bestCC = cc; 767 | lastBest = 0; 768 | } 769 | if (config.debugLevel >= 3) { 770 | console.log("Order phase iter " + i + " cross count: " + bestCC); 771 | } 772 | } 773 | 774 | bestLayering.forEach(function(layer) { 775 | layer.forEach(function(u, i) { 776 | g.node(u).order = i; 777 | }); 778 | }); 779 | 780 | if (config.debugLevel >= 2) { 781 | console.log("Order iterations: " + i); 782 | console.log("Order phase best cross count: " + bestCC); 783 | } 784 | 785 | if (config.debugLevel >= 4) { 786 | console.log("Final layering:"); 787 | bestLayering.forEach(function(layer, i) { 788 | console.log("Layer: " + i, layer); 789 | }); 790 | } 791 | 792 | return bestLayering; 793 | } 794 | 795 | function initOrder(g) { 796 | var layering = []; 797 | g.eachNode(function(n, a) { 798 | var layer = layering[a.rank] || (layering[a.rank] = []); 799 | layer.push(n); 800 | }); 801 | return layering; 802 | } 803 | 804 | /* 805 | * Returns a function that will return the predecessors for a node. This 806 | * function differs from `g.predecessors(u)` in that a predecessor appears 807 | * for each incident edge (`g.predecessors(u)` treats predecessors as a set). 808 | * This allows pseudo-weighting of predecessor nodes. 809 | */ 810 | function multiPredecessors(g) { 811 | return function(u) { 812 | var preds = []; 813 | g.inEdges(u).forEach(function(e) { 814 | preds.push(g.source(e)); 815 | }); 816 | return preds; 817 | }; 818 | } 819 | 820 | /* 821 | * Same as `multiPredecessors(g)` but for successors. 822 | */ 823 | function multiSuccessors(g) { 824 | return function(u) { 825 | var sucs = []; 826 | g.outEdges(u).forEach(function(e) { 827 | sucs.push(g.target(e)); 828 | }); 829 | return sucs; 830 | }; 831 | } 832 | 833 | function sweep(g, iter, layering) { 834 | var i; 835 | if (iter % 2 === 0) { 836 | for (i = 1; i < layering.length; ++i) { 837 | sortLayer(layering[i], multiPredecessors(g), layerPos(layering[i-1])); 838 | } 839 | } else { 840 | for (i = layering.length - 2; i >= 0; --i) { 841 | sortLayer(layering[i], multiSuccessors(g), layerPos(layering[i+1])); 842 | } 843 | } 844 | return crossCount(g, layering); 845 | } 846 | 847 | /* 848 | * Given a list of nodes, a function that returns neighbors of a node, and 849 | * a mapping of the neighbor nodes to their weights, this function sorts 850 | * the node list by the barycenter calculated for each node. 851 | */ 852 | function sortLayer(nodes, neighbors, weights) { 853 | var pos = layerPos(nodes); 854 | var bs = barycenters(nodes, neighbors, weights); 855 | 856 | var toSort = nodes.filter(function(u) { return bs[u] !== -1; }); 857 | toSort.sort(function(x, y) { 858 | return bs[x] - bs[y] || pos[x] - pos[y]; 859 | }); 860 | 861 | for (var i = nodes.length - 1; i >= 0; --i) { 862 | if (bs[nodes[i]] !== -1) { 863 | nodes[i] = toSort.pop(); 864 | } 865 | } 866 | } 867 | 868 | /* 869 | * Given a list of nodes, a function that returns neighbors of a node, and 870 | * a mapping of the neighbor nodes to their weights, this function returns 871 | * a mapping of the input nodes to their calculated barycenters. The 872 | * barycenter values are the average weights of all neighbors of the 873 | * node. If a node has no neighbors it is assigned a barycenter of -1. 874 | */ 875 | function barycenters(nodes, neighbors, weights) { 876 | var bs = {}; // barycenters 877 | 878 | nodes.forEach(function(u) { 879 | var vs = neighbors(u); 880 | var b = -1; 881 | if (vs.length > 0) 882 | b = util.sum(vs.map(function(v) { return weights[v]; })) / vs.length; 883 | bs[u] = b; 884 | }); 885 | 886 | return bs; 887 | } 888 | 889 | function copyLayering(layering) { 890 | return layering.map(function(l) { return l.slice(0); }); 891 | } 892 | 893 | function crossCount(g, layering) { 894 | var cc = 0; 895 | var prevLayer; 896 | layering.forEach(function(layer) { 897 | if (prevLayer) { 898 | cc += bilayerCrossCount(g, prevLayer, layer); 899 | } 900 | prevLayer = layer; 901 | }); 902 | return cc; 903 | } 904 | 905 | /* 906 | * This function searches through a ranked and ordered graph and counts the 907 | * number of edges that cross. This algorithm is derived from: 908 | * 909 | * W. Barth et al., Bilayer Cross Counting, JGAA, 8(2) 179–194 (2004) 910 | */ 911 | function bilayerCrossCount(g, layer1, layer2) { 912 | var layer2Pos = layerPos(layer2); 913 | 914 | var indices = []; 915 | layer1.forEach(function(u) { 916 | var nodeIndices = []; 917 | g.outEdges(u).forEach(function(e) { nodeIndices.push(layer2Pos[g.target(e)]); }); 918 | nodeIndices.sort(function(x, y) { return x - y; }); 919 | indices = indices.concat(nodeIndices); 920 | }); 921 | 922 | var firstIndex = 1; 923 | while (firstIndex < layer2.length) firstIndex <<= 1; 924 | 925 | var treeSize = 2 * firstIndex - 1; 926 | firstIndex -= 1; 927 | 928 | var tree = []; 929 | for (var i = 0; i < treeSize; ++i) { tree[i] = 0; } 930 | 931 | var cc = 0; 932 | indices.forEach(function(i) { 933 | var treeIndex = i + firstIndex; 934 | ++tree[treeIndex]; 935 | var weightSum = 0; 936 | while (treeIndex > 0) { 937 | if (treeIndex % 2) { 938 | cc += tree[treeIndex + 1]; 939 | } 940 | treeIndex = (treeIndex - 1) >> 1; 941 | ++tree[treeIndex]; 942 | } 943 | }); 944 | 945 | return cc; 946 | } 947 | }; 948 | 949 | 950 | function layerPos(layer) { 951 | var pos = {}; 952 | layer.forEach(function(u, i) { pos[u] = i; }); 953 | return pos; 954 | } 955 | 956 | },{"../util":10}],8:[function(require,module,exports){ 957 | var util = require("../util"); 958 | 959 | /* 960 | * The algorithms here are based on Brandes and Köpf, "Fast and Simple 961 | * Horizontal Coordinate Assignment". 962 | */ 963 | module.exports = function() { 964 | // External configuration 965 | var config = { 966 | nodeSep: 50, 967 | edgeSep: 10, 968 | universalSep: null, 969 | rankSep: 30, 970 | rankDir: "TB", 971 | debugLevel: 0 972 | }; 973 | 974 | var timer = util.createTimer(); 975 | 976 | var self = {}; 977 | 978 | self.nodeSep = util.propertyAccessor(self, config, "nodeSep"); 979 | self.edgeSep = util.propertyAccessor(self, config, "edgeSep"); 980 | // If not null this separation value is used for all nodes and edges 981 | // regardless of their widths. `nodeSep` and `edgeSep` are ignored with this 982 | // option. 983 | self.universalSep = util.propertyAccessor(self, config, "universalSep"); 984 | self.rankSep = util.propertyAccessor(self, config, "rankSep"); 985 | self.rankDir = util.propertyAccessor(self, config, "rankDir"); 986 | self.debugLevel = util.propertyAccessor(self, config, "debugLevel", function(x) { 987 | timer.enabled(x); 988 | }); 989 | 990 | self.run = timer.wrap("Position Phase", run); 991 | 992 | return self; 993 | 994 | function run(g) { 995 | var layering = []; 996 | g.eachNode(function(u, node) { 997 | var layer = layering[node.rank] || (layering[node.rank] = []); 998 | layer[node.order] = u; 999 | }); 1000 | 1001 | var conflicts = findConflicts(g, layering); 1002 | 1003 | var xss = {}; 1004 | ["u", "d"].forEach(function(vertDir) { 1005 | if (vertDir === "d") layering.reverse(); 1006 | 1007 | ["l", "r"].forEach(function(horizDir) { 1008 | if (horizDir === "r") reverseInnerOrder(layering); 1009 | 1010 | var dir = vertDir + horizDir; 1011 | var align = verticalAlignment(g, layering, conflicts, vertDir === "u" ? "predecessors" : "successors"); 1012 | xss[dir]= horizontalCompaction(g, layering, align.pos, align.root, align.align); 1013 | 1014 | if (config.debugLevel >= 3) 1015 | debugPositioning(vertDir + horizDir, g, layering, xss[dir]); 1016 | 1017 | if (horizDir === "r") flipHorizontally(xss[dir]); 1018 | 1019 | if (horizDir === "r") reverseInnerOrder(layering); 1020 | }); 1021 | 1022 | if (vertDir === "d") layering.reverse(); 1023 | }); 1024 | 1025 | balance(g, layering, xss); 1026 | g.eachNode(function(v) { 1027 | var xs = []; 1028 | for (var alignment in xss) { 1029 | xDebug(alignment, g, v, xss[alignment][v]); 1030 | xs.push(xss[alignment][v]); 1031 | } 1032 | xs.sort(function(x, y) { return x - y; }); 1033 | x(g, v, (xs[1] + xs[2]) / 2); 1034 | }); 1035 | 1036 | // Translate layout so left edge of bounding rectangle has coordinate 0 1037 | var minX = util.min(g.nodes().map(function(u) { return x(g, u) - width(g, u) / 2; })); 1038 | g.eachNode(function(u) { x(g, u, x(g, u) - minX); }); 1039 | 1040 | // Align y coordinates with ranks 1041 | var posY = 0; 1042 | layering.forEach(function(layer) { 1043 | var maxHeight = util.max(layer.map(function(u) { return height(g, u); })); 1044 | posY += maxHeight / 2; 1045 | layer.forEach(function(u) { y(g, u, posY); }); 1046 | posY += maxHeight / 2 + config.rankSep; 1047 | }); 1048 | }; 1049 | 1050 | /* 1051 | * Generate an ID that can be used to represent any undirected edge that is 1052 | * incident on `u` and `v`. 1053 | */ 1054 | function undirEdgeId(u, v) { 1055 | return u < v 1056 | ? u.toString().length + ":" + u + "-" + v 1057 | : v.toString().length + ":" + v + "-" + u; 1058 | } 1059 | 1060 | function findConflicts(g, layering) { 1061 | var conflicts = {}, // Set of conflicting edge ids 1062 | pos = {}; // Position of node in its layer 1063 | 1064 | if (layering.length <= 2) return conflicts; 1065 | 1066 | layering[1].forEach(function(u, i) { pos[u] = i; }); 1067 | for (var i = 1; i < layering.length - 1; ++i) { 1068 | var prevLayer = layering[i]; 1069 | var currLayer = layering[i+1]; 1070 | var k0 = 0; // Position of the last inner segment in the previous layer 1071 | var l = 0; // Current position in the current layer (for iteration up to `l1`) 1072 | 1073 | // Scan current layer for next node that is incident to an inner segement 1074 | // between layering[i+1] and layering[i]. 1075 | for (var l1 = 0; l1 < currLayer.length; ++l1) { 1076 | var u = currLayer[l1]; // Next inner segment in the current layer or 1077 | // last node in the current layer 1078 | pos[u] = l1; 1079 | 1080 | var k1 = undefined; // Position of the next inner segment in the previous layer or 1081 | // the position of the last element in the previous layer 1082 | if (g.node(u).dummy) { 1083 | var uPred = g.predecessors(u)[0]; 1084 | if (g.node(uPred).dummy) 1085 | k1 = pos[uPred]; 1086 | } 1087 | if (k1 === undefined && l1 === currLayer.length - 1) 1088 | k1 = prevLayer.length - 1; 1089 | 1090 | if (k1 !== undefined) { 1091 | for (; l <= l1; ++l) { 1092 | g.predecessors(currLayer[l]).forEach(function(v) { 1093 | var k = pos[v]; 1094 | if (k < k0 || k > k1) 1095 | conflicts[undirEdgeId(currLayer[l], v)] = true; 1096 | }); 1097 | } 1098 | k0 = k1; 1099 | } 1100 | } 1101 | } 1102 | 1103 | return conflicts; 1104 | } 1105 | 1106 | function verticalAlignment(g, layering, conflicts, relationship) { 1107 | var pos = {}, // Position for a node in its layer 1108 | root = {}, // Root of the block that the node participates in 1109 | align = {}; // Points to the next node in the block or, if the last 1110 | // element in the block, points to the first block's root 1111 | 1112 | layering.forEach(function(layer) { 1113 | layer.forEach(function(u, i) { 1114 | root[u] = u; 1115 | align[u] = u; 1116 | pos[u] = i; 1117 | }); 1118 | }); 1119 | 1120 | layering.forEach(function(layer) { 1121 | var prevIdx = -1; 1122 | layer.forEach(function(v) { 1123 | var related = g[relationship](v), // Adjacent nodes from the previous layer 1124 | mid; // The mid point in the related array 1125 | 1126 | if (related.length > 0) { 1127 | related.sort(function(x, y) { return pos[x] - pos[y]; }); 1128 | mid = (related.length - 1) / 2; 1129 | related.slice(Math.floor(mid), Math.ceil(mid) + 1).forEach(function(u) { 1130 | if (align[v] === v) { 1131 | if (!conflicts[undirEdgeId(u, v)] && prevIdx < pos[u]) { 1132 | align[u] = v; 1133 | align[v] = root[v] = root[u]; 1134 | prevIdx = pos[u]; 1135 | } 1136 | } 1137 | }); 1138 | } 1139 | }); 1140 | }); 1141 | 1142 | return { pos: pos, root: root, align: align }; 1143 | } 1144 | 1145 | // This function deviates from the standard BK algorithm in two ways. First 1146 | // it takes into account the size of the nodes. Second it includes a fix to 1147 | // the original algorithm that is described in Carstens, "Node and Label 1148 | // Placement in a Layered Layout Algorithm". 1149 | function horizontalCompaction(g, layering, pos, root, align) { 1150 | var sink = {}, // Mapping of node id -> sink node id for class 1151 | maybeShift = {}, // Mapping of sink node id -> { class node id, min shift } 1152 | shift = {}, // Mapping of sink node id -> shift 1153 | pred = {}, // Mapping of node id -> predecessor node (or null) 1154 | xs = {}; // Calculated X positions 1155 | 1156 | layering.forEach(function(layer) { 1157 | layer.forEach(function(u, i) { 1158 | sink[u] = u; 1159 | maybeShift[u] = {}; 1160 | if (i > 0) 1161 | pred[u] = layer[i - 1]; 1162 | }); 1163 | }); 1164 | 1165 | function updateShift(toShift, neighbor, delta) { 1166 | if (!(neighbor in maybeShift[toShift])) { 1167 | maybeShift[toShift][neighbor] = delta; 1168 | } else { 1169 | maybeShift[toShift][neighbor] = Math.min(maybeShift[toShift][neighbor], delta); 1170 | } 1171 | } 1172 | 1173 | function placeBlock(v) { 1174 | if (!(v in xs)) { 1175 | xs[v] = 0; 1176 | var w = v; 1177 | do { 1178 | if (pos[w] > 0) { 1179 | var u = root[pred[w]]; 1180 | placeBlock(u); 1181 | if (sink[v] === v) { 1182 | sink[v] = sink[u]; 1183 | } 1184 | var delta = sep(g, pred[w]) + sep(g, w); 1185 | if (sink[v] !== sink[u]) { 1186 | updateShift(sink[u], sink[v], xs[v] - xs[u] - delta); 1187 | } else { 1188 | xs[v] = Math.max(xs[v], xs[u] + delta); 1189 | } 1190 | } 1191 | w = align[w]; 1192 | } while (w !== v); 1193 | } 1194 | } 1195 | 1196 | // Root coordinates relative to sink 1197 | util.values(root).forEach(function(v) { 1198 | placeBlock(v); 1199 | }); 1200 | 1201 | // Absolute coordinates 1202 | // There is an assumption here that we've resolved shifts for any classes 1203 | // that begin at an earlier layer. We guarantee this by visiting layers in 1204 | // order. 1205 | layering.forEach(function(layer) { 1206 | layer.forEach(function(v) { 1207 | xs[v] = xs[root[v]]; 1208 | if (v === root[v] && v === sink[v]) { 1209 | var minShift = 0; 1210 | if (v in maybeShift && Object.keys(maybeShift[v]).length > 0) { 1211 | minShift = util.min(Object.keys(maybeShift[v]) 1212 | .map(function(u) { 1213 | return maybeShift[v][u] + (u in shift ? shift[u] : 0); 1214 | } 1215 | )); 1216 | } 1217 | shift[v] = minShift; 1218 | } 1219 | }); 1220 | }); 1221 | 1222 | layering.forEach(function(layer) { 1223 | layer.forEach(function(v) { 1224 | xs[v] += shift[sink[root[v]]] || 0; 1225 | }); 1226 | }); 1227 | 1228 | return xs; 1229 | } 1230 | 1231 | function findMinCoord(g, layering, xs) { 1232 | return util.min(layering.map(function(layer) { 1233 | var u = layer[0]; 1234 | return xs[u]; 1235 | })); 1236 | } 1237 | 1238 | function findMaxCoord(g, layering, xs) { 1239 | return util.max(layering.map(function(layer) { 1240 | var u = layer[layer.length - 1]; 1241 | return xs[u]; 1242 | })); 1243 | } 1244 | 1245 | function balance(g, layering, xss) { 1246 | var min = {}, // Min coordinate for the alignment 1247 | max = {}, // Max coordinate for the alginment 1248 | smallestAlignment, 1249 | shift = {}; // Amount to shift a given alignment 1250 | 1251 | var smallest = Number.POSITIVE_INFINITY; 1252 | for (var alignment in xss) { 1253 | var xs = xss[alignment]; 1254 | min[alignment] = findMinCoord(g, layering, xs); 1255 | max[alignment] = findMaxCoord(g, layering, xs); 1256 | var w = max[alignment] - min[alignment]; 1257 | if (w < smallest) { 1258 | smallest = w; 1259 | smallestAlignment = alignment; 1260 | } 1261 | } 1262 | 1263 | // Determine how much to adjust positioning for each alignment 1264 | ["u", "d"].forEach(function(vertDir) { 1265 | ["l", "r"].forEach(function(horizDir) { 1266 | var alignment = vertDir + horizDir; 1267 | shift[alignment] = horizDir === "l" 1268 | ? min[smallestAlignment] - min[alignment] 1269 | : max[smallestAlignment] - max[alignment]; 1270 | }); 1271 | }); 1272 | 1273 | // Find average of medians for xss array 1274 | for (var alignment in xss) { 1275 | g.eachNode(function(v) { 1276 | xss[alignment][v] += shift[alignment]; 1277 | }); 1278 | } 1279 | } 1280 | 1281 | function flipHorizontally(xs) { 1282 | for (var u in xs) { 1283 | xs[u] = -xs[u]; 1284 | } 1285 | } 1286 | 1287 | function reverseInnerOrder(layering) { 1288 | layering.forEach(function(layer) { 1289 | layer.reverse(); 1290 | }); 1291 | } 1292 | 1293 | function width(g, u) { 1294 | switch (config.rankDir) { 1295 | case "LR": return g.node(u).height; 1296 | default: return g.node(u).width; 1297 | } 1298 | } 1299 | 1300 | function height(g, u) { 1301 | switch(config.rankDir) { 1302 | case "LR": return g.node(u).width; 1303 | default: return g.node(u).height; 1304 | } 1305 | } 1306 | 1307 | function sep(g, u) { 1308 | if (config.universalSep !== null) { 1309 | return config.universalSep; 1310 | } 1311 | var w = width(g, u); 1312 | var s = g.node(u).dummy ? config.edgeSep : config.nodeSep; 1313 | return (w + s) / 2; 1314 | } 1315 | 1316 | function x(g, u, x) { 1317 | switch (config.rankDir) { 1318 | case "LR": 1319 | if (arguments.length < 3) { 1320 | return g.node(u).y; 1321 | } else { 1322 | g.node(u).y = x; 1323 | } 1324 | break; 1325 | default: 1326 | if (arguments.length < 3) { 1327 | return g.node(u).x; 1328 | } else { 1329 | g.node(u).x = x; 1330 | } 1331 | } 1332 | } 1333 | 1334 | function xDebug(name, g, u, x) { 1335 | switch (config.rankDir) { 1336 | case "LR": 1337 | if (arguments.length < 3) { 1338 | return g.node(u)[name]; 1339 | } else { 1340 | g.node(u)[name] = x; 1341 | } 1342 | break; 1343 | default: 1344 | if (arguments.length < 3) { 1345 | return g.node(u)[name]; 1346 | } else { 1347 | g.node(u)[name] = x; 1348 | } 1349 | } 1350 | } 1351 | 1352 | function y(g, u, y) { 1353 | switch (config.rankDir) { 1354 | case "LR": 1355 | if (arguments.length < 3) { 1356 | return g.node(u).x; 1357 | } else { 1358 | g.node(u).x = y; 1359 | } 1360 | break; 1361 | default: 1362 | if (arguments.length < 3) { 1363 | return g.node(u).y; 1364 | } else { 1365 | g.node(u).y = y; 1366 | } 1367 | } 1368 | } 1369 | 1370 | function debugPositioning(align, g, layering, xs) { 1371 | layering.forEach(function(l, li) { 1372 | var u, xU; 1373 | l.forEach(function(v) { 1374 | var xV = xs[v]; 1375 | if (u) { 1376 | var s = sep(g, u) + sep(g, v); 1377 | if (xV - xU < s) 1378 | console.log("Position phase: sep violation. Align: " + align + ". Layer: " + li + ". " + 1379 | "U: " + u + " V: " + v + ". Actual sep: " + (xV - xU) + " Expected sep: " + s); 1380 | } 1381 | u = v; 1382 | xU = xV; 1383 | }); 1384 | }); 1385 | } 1386 | }; 1387 | 1388 | },{"../util":10}],9:[function(require,module,exports){ 1389 | var util = require("../util"), 1390 | components = require("graphlib").alg.components, 1391 | filter = require("graphlib").filter; 1392 | PriorityQueue = require("graphlib").data.PriorityQueue, 1393 | Set = require("graphlib").data.Set; 1394 | 1395 | module.exports = function(g, debugLevel) { 1396 | var timer = util.createTimer(debugLevel >= 1); 1397 | timer.wrap("Rank phase", function() { 1398 | initRank(g); 1399 | 1400 | components(g).forEach(function(cmpt) { 1401 | var subgraph = g.filterNodes(filter.nodesFromList(cmpt)); 1402 | feasibleTree(subgraph); 1403 | normalize(subgraph); 1404 | }); 1405 | })(); 1406 | }; 1407 | 1408 | function initRank(g) { 1409 | var minRank = {}; 1410 | var pq = new PriorityQueue(); 1411 | 1412 | g.eachNode(function(u) { 1413 | pq.add(u, g.inEdges(u).length); 1414 | minRank[u] = 0; 1415 | }); 1416 | 1417 | while (pq.size() > 0) { 1418 | var minId = pq.min(); 1419 | if (pq.priority(minId) > 0) { 1420 | throw new Error("Input graph is not acyclic: " + g.toString()); 1421 | } 1422 | pq.removeMin(); 1423 | 1424 | var rank = minRank[minId]; 1425 | g.node(minId).rank = rank; 1426 | 1427 | g.outEdges(minId).forEach(function(e) { 1428 | var target = g.target(e); 1429 | minRank[target] = Math.max(minRank[target], rank + (g.edge(e).minLen || 1)); 1430 | pq.decrease(target, pq.priority(target) - 1); 1431 | }); 1432 | } 1433 | } 1434 | 1435 | function feasibleTree(g) { 1436 | var remaining = new Set(g.nodes()), 1437 | minLen = []; // Array of {u, v, len} 1438 | 1439 | // Collapse multi-edges and precompute the minLen, which will be the 1440 | // max value of minLen for any edge in the multi-edge. 1441 | var minLenMap = {}; 1442 | g.eachEdge(function(e, u, v, edge) { 1443 | var id = incidenceId(u, v); 1444 | if (!(id in minLenMap)) { 1445 | minLen.push(minLenMap[id] = { u: u, v: v, len: 1 }); 1446 | } 1447 | minLenMap[id].len = Math.max(minLenMap[id].len, edge.minLen || 1); 1448 | }); 1449 | 1450 | function slack(mle /* minLen entry*/) { 1451 | return Math.abs(g.node(mle.u).rank - g.node(mle.v).rank) - mle.len; 1452 | } 1453 | 1454 | // Remove arbitrary node - it is effectively the root of the spanning tree. 1455 | remaining.remove(g.nodes()[0]); 1456 | 1457 | // Finds the next edge with the minimum slack. 1458 | function findMinSlack() { 1459 | var result, 1460 | eSlack = Number.POSITIVE_INFINITY; 1461 | minLen.forEach(function(mle /* minLen entry */) { 1462 | if (remaining.has(mle.u) !== remaining.has(mle.v)) { 1463 | var mleSlack = slack(mle); 1464 | if (mleSlack < eSlack) { 1465 | if (!remaining.has(mle.u)) { 1466 | result = { treeNode: mle.u, graphNode: mle.v, len: mle.len}; 1467 | } else { 1468 | result = { treeNode: mle.v, graphNode: mle.u, len: -mle.len }; 1469 | } 1470 | eSlack = mleSlack; 1471 | } 1472 | } 1473 | }); 1474 | 1475 | return result; 1476 | } 1477 | 1478 | while (remaining.size() > 0) { 1479 | var result = findMinSlack(); 1480 | remaining.remove(result.graphNode); 1481 | g.node(result.graphNode).rank = g.node(result.treeNode).rank + result.len; 1482 | } 1483 | } 1484 | 1485 | function normalize(g) { 1486 | var m = util.min(g.nodes().map(function(u) { return g.node(u).rank; })); 1487 | g.eachNode(function(u, node) { node.rank -= m; }); 1488 | } 1489 | 1490 | /* 1491 | * This id can be used to group (in an undirected manner) multi-edges 1492 | * incident on the same two nodes. 1493 | */ 1494 | function incidenceId(u, v) { 1495 | return u < v ? u.length + ":" + u + "-" + v : v.length + ":" + v + "-" + u; 1496 | } 1497 | 1498 | },{"../util":10,"graphlib":12}],10:[function(require,module,exports){ 1499 | /* 1500 | * Returns the smallest value in the array. 1501 | */ 1502 | exports.min = function(values) { 1503 | return Math.min.apply(null, values); 1504 | }; 1505 | 1506 | /* 1507 | * Returns the largest value in the array. 1508 | */ 1509 | exports.max = function(values) { 1510 | return Math.max.apply(null, values); 1511 | }; 1512 | 1513 | /* 1514 | * Returns `true` only if `f(x)` is `true` for all `x` in `xs`. Otherwise 1515 | * returns `false`. This function will return immediately if it finds a 1516 | * case where `f(x)` does not hold. 1517 | */ 1518 | exports.all = function(xs, f) { 1519 | for (var i = 0; i < xs.length; ++i) { 1520 | if (!f(xs[i])) { 1521 | return false; 1522 | } 1523 | } 1524 | return true; 1525 | }; 1526 | 1527 | /* 1528 | * Accumulates the sum of elements in the given array using the `+` operator. 1529 | */ 1530 | exports.sum = function(values) { 1531 | return values.reduce(function(acc, x) { return acc + x; }, 0); 1532 | }; 1533 | 1534 | /* 1535 | * Returns an array of all values in the given object. 1536 | */ 1537 | exports.values = function(obj) { 1538 | return Object.keys(obj).map(function(k) { return obj[k]; }); 1539 | }; 1540 | 1541 | exports.createTimer = function(enabled) { 1542 | var self = {}; 1543 | 1544 | // Default to disabled 1545 | enabled = enabled || false; 1546 | 1547 | self.enabled = function(x) { 1548 | if (!arguments.length) return enabled; 1549 | enabled = x; 1550 | return self; 1551 | }; 1552 | 1553 | self.wrap = function(name, func) { 1554 | return function() { 1555 | var start = enabled ? new Date().getTime() : null; 1556 | try { 1557 | return func.apply(null, arguments); 1558 | } finally { 1559 | if (start) console.log(name + " time: " + (new Date().getTime() - start) + "ms"); 1560 | } 1561 | }; 1562 | }; 1563 | 1564 | return self; 1565 | }; 1566 | 1567 | exports.propertyAccessor = function(self, config, field, setHook) { 1568 | return function(x) { 1569 | if (!arguments.length) return config[field]; 1570 | config[field] = x; 1571 | if (setHook) setHook(x); 1572 | return self; 1573 | }; 1574 | }; 1575 | 1576 | },{}],11:[function(require,module,exports){ 1577 | module.exports = '0.3.0'; 1578 | 1579 | },{}],12:[function(require,module,exports){ 1580 | exports.Graph = require("./lib/Graph"); 1581 | exports.Digraph = require("./lib/Digraph"); 1582 | exports.CGraph = require("./lib/CGraph"); 1583 | exports.CDigraph = require("./lib/CDigraph"); 1584 | require("./lib/graph-converters"); 1585 | 1586 | exports.alg = { 1587 | isAcyclic: require("./lib/alg/isAcyclic"), 1588 | components: require("./lib/alg/components"), 1589 | dijkstra: require("./lib/alg/dijkstra"), 1590 | dijkstraAll: require("./lib/alg/dijkstraAll"), 1591 | findCycles: require("./lib/alg/findCycles"), 1592 | floydWarshall: require("./lib/alg/floydWarshall"), 1593 | prim: require("./lib/alg/prim"), 1594 | tarjan: require("./lib/alg/tarjan"), 1595 | topsort: require("./lib/alg/topsort") 1596 | }; 1597 | 1598 | exports.converter = { 1599 | json: require("./lib/converter/json.js") 1600 | }; 1601 | 1602 | exports.data = { 1603 | PriorityQueue: require("./lib/data/PriorityQueue"), 1604 | Set: require("./lib/data/Set") 1605 | }; 1606 | 1607 | var filter = require("./lib/filter"); 1608 | exports.filter = { 1609 | all: filter.all, 1610 | nodesFromList: filter.nodesFromList 1611 | }; 1612 | 1613 | exports.version = require("./lib/version"); 1614 | 1615 | },{"./lib/CDigraph":14,"./lib/CGraph":15,"./lib/Digraph":16,"./lib/Graph":17,"./lib/alg/components":18,"./lib/alg/dijkstra":19,"./lib/alg/dijkstraAll":20,"./lib/alg/findCycles":21,"./lib/alg/floydWarshall":22,"./lib/alg/isAcyclic":23,"./lib/alg/prim":24,"./lib/alg/tarjan":25,"./lib/alg/topsort":26,"./lib/converter/json.js":28,"./lib/data/PriorityQueue":29,"./lib/data/Set":30,"./lib/filter":31,"./lib/graph-converters":32,"./lib/version":34}],13:[function(require,module,exports){ 1616 | var filter = require("./filter"), 1617 | /* jshint -W079 */ 1618 | Set = require("./data/Set"); 1619 | 1620 | module.exports = BaseGraph; 1621 | 1622 | function BaseGraph() { 1623 | // The value assigned to the graph itself. 1624 | this._value = undefined; 1625 | 1626 | // Map of node id -> { id, value } 1627 | this._nodes = {}; 1628 | 1629 | // Map of edge id -> { id, u, v, value } 1630 | this._edges = {}; 1631 | 1632 | // Used to generate a unique id in the graph 1633 | this._nextId = 0; 1634 | } 1635 | 1636 | // Number of nodes 1637 | BaseGraph.prototype.order = function() { 1638 | return Object.keys(this._nodes).length; 1639 | }; 1640 | 1641 | // Number of edges 1642 | BaseGraph.prototype.size = function() { 1643 | return Object.keys(this._edges).length; 1644 | }; 1645 | 1646 | // Accessor for graph level value 1647 | BaseGraph.prototype.graph = function(value) { 1648 | if (arguments.length === 0) { 1649 | return this._value; 1650 | } 1651 | this._value = value; 1652 | }; 1653 | 1654 | BaseGraph.prototype.hasNode = function(u) { 1655 | return u in this._nodes; 1656 | }; 1657 | 1658 | BaseGraph.prototype.node = function(u, value) { 1659 | var node = this._strictGetNode(u); 1660 | if (arguments.length === 1) { 1661 | return node.value; 1662 | } 1663 | node.value = value; 1664 | }; 1665 | 1666 | BaseGraph.prototype.nodes = function() { 1667 | var nodes = []; 1668 | this.eachNode(function(id) { nodes.push(id); }); 1669 | return nodes; 1670 | }; 1671 | 1672 | BaseGraph.prototype.eachNode = function(func) { 1673 | for (var k in this._nodes) { 1674 | var node = this._nodes[k]; 1675 | func(node.id, node.value); 1676 | } 1677 | }; 1678 | 1679 | BaseGraph.prototype.hasEdge = function(e) { 1680 | return e in this._edges; 1681 | }; 1682 | 1683 | BaseGraph.prototype.edge = function(e, value) { 1684 | var edge = this._strictGetEdge(e); 1685 | if (arguments.length === 1) { 1686 | return edge.value; 1687 | } 1688 | edge.value = value; 1689 | }; 1690 | 1691 | BaseGraph.prototype.edges = function() { 1692 | var es = []; 1693 | this.eachEdge(function(id) { es.push(id); }); 1694 | return es; 1695 | }; 1696 | 1697 | BaseGraph.prototype.eachEdge = function(func) { 1698 | for (var k in this._edges) { 1699 | var edge = this._edges[k]; 1700 | func(edge.id, edge.u, edge.v, edge.value); 1701 | } 1702 | }; 1703 | 1704 | BaseGraph.prototype.incidentNodes = function(e) { 1705 | var edge = this._strictGetEdge(e); 1706 | return [edge.u, edge.v]; 1707 | }; 1708 | 1709 | BaseGraph.prototype.addNode = function(u, value) { 1710 | if (u === undefined || u === null) { 1711 | do { 1712 | u = "_" + (++this._nextId); 1713 | } while (this.hasNode(u)); 1714 | } else if (this.hasNode(u)) { 1715 | throw new Error("Graph already has node '" + u + "':\n" + this.toString()); 1716 | } 1717 | this._nodes[u] = { id: u, value: value }; 1718 | return u; 1719 | }; 1720 | 1721 | BaseGraph.prototype.delNode = function(u) { 1722 | this._strictGetNode(u); 1723 | this.incidentEdges(u).forEach(function(e) { this.delEdge(e); }, this); 1724 | delete this._nodes[u]; 1725 | }; 1726 | 1727 | // inMap and outMap are opposite sides of an incidence map. For example, for 1728 | // Graph these would both come from the _incidentEdges map, while for Digraph 1729 | // they would come from _inEdges and _outEdges. 1730 | BaseGraph.prototype._addEdge = function(e, u, v, value, inMap, outMap) { 1731 | this._strictGetNode(u); 1732 | this._strictGetNode(v); 1733 | 1734 | if (e === undefined || e === null) { 1735 | do { 1736 | e = "_" + (++this._nextId); 1737 | } while (this.hasEdge(e)); 1738 | } 1739 | else if (this.hasEdge(e)) { 1740 | throw new Error("Graph already has edge '" + e + "':\n" + this.toString()); 1741 | } 1742 | 1743 | this._edges[e] = { id: e, u: u, v: v, value: value }; 1744 | addEdgeToMap(inMap[v], u, e); 1745 | addEdgeToMap(outMap[u], v, e); 1746 | 1747 | return e; 1748 | }; 1749 | 1750 | // See note for _addEdge regarding inMap and outMap. 1751 | BaseGraph.prototype._delEdge = function(e, inMap, outMap) { 1752 | var edge = this._strictGetEdge(e); 1753 | delEdgeFromMap(inMap[edge.v], edge.u, e); 1754 | delEdgeFromMap(outMap[edge.u], edge.v, e); 1755 | delete this._edges[e]; 1756 | }; 1757 | 1758 | BaseGraph.prototype.copy = function() { 1759 | var copy = new this.constructor(); 1760 | copy.graph(this.graph); 1761 | this.eachNode(function(u, value) { copy.addNode(u, value); }); 1762 | this.eachEdge(function(e, u, v, value) { copy.addEdge(e, u, v, value); }); 1763 | return copy; 1764 | }; 1765 | 1766 | BaseGraph.prototype.filterNodes = function(filter) { 1767 | var copy = this.copy(); 1768 | this.nodes().forEach(function(u) { 1769 | if (!filter(u)) { 1770 | copy.delNode(u); 1771 | } 1772 | }); 1773 | return copy; 1774 | }; 1775 | 1776 | BaseGraph.prototype._strictGetNode = function(u) { 1777 | var node = this._nodes[u]; 1778 | if (node === undefined) { 1779 | throw new Error("Node '" + u + "' is not in graph:\n" + this.toString()); 1780 | } 1781 | return node; 1782 | }; 1783 | 1784 | BaseGraph.prototype._strictGetEdge = function(e) { 1785 | var edge = this._edges[e]; 1786 | if (edge === undefined) { 1787 | throw new Error("Edge '" + e + "' is not in graph:\n" + this.toString()); 1788 | } 1789 | return edge; 1790 | }; 1791 | 1792 | function addEdgeToMap(map, v, e) { 1793 | (map[v] || (map[v] = new Set())).add(e); 1794 | } 1795 | 1796 | function delEdgeFromMap(map, v, e) { 1797 | var vEntry = map[v]; 1798 | vEntry.remove(e); 1799 | if (vEntry.size() === 0) { 1800 | delete map[v]; 1801 | } 1802 | } 1803 | 1804 | 1805 | },{"./data/Set":30,"./filter":31}],14:[function(require,module,exports){ 1806 | var Digraph = require("./Digraph"), 1807 | compoundify = require("./compoundify"); 1808 | 1809 | var CDigraph = compoundify(Digraph); 1810 | 1811 | module.exports = CDigraph; 1812 | 1813 | CDigraph.fromDigraph = function(src) { 1814 | var g = new CDigraph(), 1815 | graphValue = src.graph(); 1816 | 1817 | if (graphValue !== undefined) { 1818 | g.graph(graphValue); 1819 | } 1820 | 1821 | src.eachNode(function(u, value) { 1822 | if (value === undefined) { 1823 | g.addNode(u); 1824 | } else { 1825 | g.addNode(u, value); 1826 | } 1827 | }); 1828 | src.eachEdge(function(e, u, v, value) { 1829 | if (value === undefined) { 1830 | g.addEdge(null, u, v); 1831 | } else { 1832 | g.addEdge(null, u, v, value); 1833 | } 1834 | }); 1835 | return g; 1836 | }; 1837 | 1838 | CDigraph.prototype.toString = function() { 1839 | return "CDigraph " + JSON.stringify(this, null, 2); 1840 | }; 1841 | 1842 | },{"./Digraph":16,"./compoundify":27}],15:[function(require,module,exports){ 1843 | var Graph = require("./Graph"), 1844 | compoundify = require("./compoundify"); 1845 | 1846 | var CGraph = compoundify(Graph); 1847 | 1848 | module.exports = CGraph; 1849 | 1850 | CGraph.fromGraph = function(src) { 1851 | var g = new CGraph(), 1852 | graphValue = src.graph(); 1853 | 1854 | if (graphValue !== undefined) { 1855 | g.graph(graphValue); 1856 | } 1857 | 1858 | src.eachNode(function(u, value) { 1859 | if (value === undefined) { 1860 | g.addNode(u); 1861 | } else { 1862 | g.addNode(u, value); 1863 | } 1864 | }); 1865 | src.eachEdge(function(e, u, v, value) { 1866 | if (value === undefined) { 1867 | g.addEdge(null, u, v); 1868 | } else { 1869 | g.addEdge(null, u, v, value); 1870 | } 1871 | }); 1872 | return g; 1873 | }; 1874 | 1875 | CGraph.prototype.toString = function() { 1876 | return "CGraph " + JSON.stringify(this, null, 2); 1877 | }; 1878 | 1879 | },{"./Graph":17,"./compoundify":27}],16:[function(require,module,exports){ 1880 | /* 1881 | * This file is organized with in the following order: 1882 | * 1883 | * Exports 1884 | * Graph constructors 1885 | * Graph queries (e.g. nodes(), edges() 1886 | * Graph mutators 1887 | * Helper functions 1888 | */ 1889 | 1890 | var util = require("./util"), 1891 | BaseGraph = require("./BaseGraph"), 1892 | /* jshint -W079 */ 1893 | Set = require("./data/Set"); 1894 | 1895 | module.exports = Digraph; 1896 | 1897 | /* 1898 | * Constructor to create a new directed multi-graph. 1899 | */ 1900 | function Digraph() { 1901 | BaseGraph.call(this); 1902 | 1903 | /*! Map of sourceId -> {targetId -> Set of edge ids} */ 1904 | this._inEdges = {}; 1905 | 1906 | /*! Map of targetId -> {sourceId -> Set of edge ids} */ 1907 | this._outEdges = {}; 1908 | } 1909 | 1910 | Digraph.prototype = new BaseGraph(); 1911 | Digraph.prototype.constructor = Digraph; 1912 | 1913 | /* 1914 | * Always returns `true`. 1915 | */ 1916 | Digraph.prototype.isDirected = function() { 1917 | return true; 1918 | }; 1919 | 1920 | /* 1921 | * Returns all successors of the node with the id `u`. That is, all nodes 1922 | * that have the node `u` as their source are returned. 1923 | * 1924 | * If no node `u` exists in the graph this function throws an Error. 1925 | * 1926 | * @param {String} u a node id 1927 | */ 1928 | Digraph.prototype.successors = function(u) { 1929 | this._strictGetNode(u); 1930 | return Object.keys(this._outEdges[u]) 1931 | .map(function(v) { return this._nodes[v].id; }, this); 1932 | }; 1933 | 1934 | /* 1935 | * Returns all predecessors of the node with the id `u`. That is, all nodes 1936 | * that have the node `u` as their target are returned. 1937 | * 1938 | * If no node `u` exists in the graph this function throws an Error. 1939 | * 1940 | * @param {String} u a node id 1941 | */ 1942 | Digraph.prototype.predecessors = function(u) { 1943 | this._strictGetNode(u); 1944 | return Object.keys(this._inEdges[u]) 1945 | .map(function(v) { return this._nodes[v].id; }, this); 1946 | }; 1947 | 1948 | /* 1949 | * Returns all nodes that are adjacent to the node with the id `u`. In other 1950 | * words, this function returns the set of all successors and predecessors of 1951 | * node `u`. 1952 | * 1953 | * @param {String} u a node id 1954 | */ 1955 | Digraph.prototype.neighbors = function(u) { 1956 | return Set.unionAll([this.successors(u), this.predecessors(u)]).keys(); 1957 | }; 1958 | 1959 | /* 1960 | * Returns all nodes in the graph that have no in-edges. 1961 | */ 1962 | Digraph.prototype.sources = function() { 1963 | var self = this; 1964 | return this._filterNodes(function(u) { 1965 | // This could have better space characteristics if we had an inDegree function. 1966 | return self.inEdges(u).length === 0; 1967 | }); 1968 | }; 1969 | 1970 | /* 1971 | * Returns all nodes in the graph that have no out-edges. 1972 | */ 1973 | Digraph.prototype.sinks = function() { 1974 | var self = this; 1975 | return this._filterNodes(function(u) { 1976 | // This could have better space characteristics if we have an outDegree function. 1977 | return self.outEdges(u).length === 0; 1978 | }); 1979 | }; 1980 | 1981 | /* 1982 | * Returns the source node incident on the edge identified by the id `e`. If no 1983 | * such edge exists in the graph this function throws an Error. 1984 | * 1985 | * @param {String} e an edge id 1986 | */ 1987 | Digraph.prototype.source = function(e) { 1988 | return this._strictGetEdge(e).u; 1989 | }; 1990 | 1991 | /* 1992 | * Returns the target node incident on the edge identified by the id `e`. If no 1993 | * such edge exists in the graph this function throws an Error. 1994 | * 1995 | * @param {String} e an edge id 1996 | */ 1997 | Digraph.prototype.target = function(e) { 1998 | return this._strictGetEdge(e).v; 1999 | }; 2000 | 2001 | /* 2002 | * Returns an array of ids for all edges in the graph that have the node 2003 | * `target` as their target. If the node `target` is not in the graph this 2004 | * function raises an Error. 2005 | * 2006 | * Optionally a `source` node can also be specified. This causes the results 2007 | * to be filtered such that only edges from `source` to `target` are included. 2008 | * If the node `source` is specified but is not in the graph then this function 2009 | * raises an Error. 2010 | * 2011 | * @param {String} target the target node id 2012 | * @param {String} [source] an optional source node id 2013 | */ 2014 | Digraph.prototype.inEdges = function(target, source) { 2015 | this._strictGetNode(target); 2016 | var results = Set.unionAll(util.values(this._inEdges[target])).keys(); 2017 | if (arguments.length > 1) { 2018 | this._strictGetNode(source); 2019 | results = results.filter(function(e) { return this.source(e) === source; }, this); 2020 | } 2021 | return results; 2022 | }; 2023 | 2024 | /* 2025 | * Returns an array of ids for all edges in the graph that have the node 2026 | * `source` as their source. If the node `source` is not in the graph this 2027 | * function raises an Error. 2028 | * 2029 | * Optionally a `target` node may also be specified. This causes the results 2030 | * to be filtered such that only edges from `source` to `target` are included. 2031 | * If the node `target` is specified but is not in the graph then this function 2032 | * raises an Error. 2033 | * 2034 | * @param {String} source the source node id 2035 | * @param {String} [target] an optional target node id 2036 | */ 2037 | Digraph.prototype.outEdges = function(source, target) { 2038 | this._strictGetNode(source); 2039 | var results = Set.unionAll(util.values(this._outEdges[source])).keys(); 2040 | if (arguments.length > 1) { 2041 | this._strictGetNode(target); 2042 | results = results.filter(function(e) { return this.target(e) === target; }, this); 2043 | } 2044 | return results; 2045 | }; 2046 | 2047 | /* 2048 | * Returns an array of ids for all edges in the graph that have the `u` as 2049 | * their source or their target. If the node `u` is not in the graph this 2050 | * function raises an Error. 2051 | * 2052 | * Optionally a `v` node may also be specified. This causes the results to be 2053 | * filtered such that only edges between `u` and `v` - in either direction - 2054 | * are included. IF the node `v` is specified but not in the graph then this 2055 | * function raises an Error. 2056 | * 2057 | * @param {String} u the node for which to find incident edges 2058 | * @param {String} [v] option node that must be adjacent to `u` 2059 | */ 2060 | Digraph.prototype.incidentEdges = function(u, v) { 2061 | if (arguments.length > 1) { 2062 | return Set.unionAll([this.outEdges(u, v), this.outEdges(v, u)]).keys(); 2063 | } else { 2064 | return Set.unionAll([this.inEdges(u), this.outEdges(u)]).keys(); 2065 | } 2066 | }; 2067 | 2068 | /* 2069 | * Returns a string representation of this graph. 2070 | */ 2071 | Digraph.prototype.toString = function() { 2072 | return "Digraph " + JSON.stringify(this, null, 2); 2073 | }; 2074 | 2075 | /* 2076 | * Adds a new node with the id `u` to the graph and assigns it the value 2077 | * `value`. If a node with the id is already a part of the graph this function 2078 | * throws an Error. 2079 | * 2080 | * @param {String} u a node id 2081 | * @param {Object} [value] an optional value to attach to the node 2082 | */ 2083 | Digraph.prototype.addNode = function(u, value) { 2084 | u = BaseGraph.prototype.addNode.call(this, u, value); 2085 | this._inEdges[u] = {}; 2086 | this._outEdges[u] = {}; 2087 | return u; 2088 | }; 2089 | 2090 | /* 2091 | * Removes a node from the graph that has the id `u`. Any edges incident on the 2092 | * node are also removed. If the graph does not contain a node with the id this 2093 | * function will throw an Error. 2094 | * 2095 | * @param {String} u a node id 2096 | */ 2097 | Digraph.prototype.delNode = function(u) { 2098 | BaseGraph.prototype.delNode.call(this, u); 2099 | delete this._inEdges[u]; 2100 | delete this._outEdges[u]; 2101 | }; 2102 | 2103 | /* 2104 | * Adds a new edge to the graph with the id `e` from a node with the id `source` 2105 | * to a node with an id `target` and assigns it the value `value`. This graph 2106 | * allows more than one edge from `source` to `target` as long as the id `e` 2107 | * is unique in the set of edges. If `e` is `null` the graph will assign a 2108 | * unique identifier to the edge. 2109 | * 2110 | * If `source` or `target` are not present in the graph this function will 2111 | * throw an Error. 2112 | * 2113 | * @param {String} [e] an edge id 2114 | * @param {String} source the source node id 2115 | * @param {String} target the target node id 2116 | * @param {Object} [value] an optional value to attach to the edge 2117 | */ 2118 | Digraph.prototype.addEdge = function(e, source, target, value) { 2119 | return BaseGraph.prototype._addEdge.call(this, e, source, target, value, 2120 | this._inEdges, this._outEdges); 2121 | }; 2122 | 2123 | /* 2124 | * Removes an edge in the graph with the id `e`. If no edge in the graph has 2125 | * the id `e` this function will throw an Error. 2126 | * 2127 | * @param {String} e an edge id 2128 | */ 2129 | Digraph.prototype.delEdge = function(e) { 2130 | BaseGraph.prototype._delEdge.call(this, e, this._inEdges, this._outEdges); 2131 | }; 2132 | 2133 | // Unlike BaseGraph.filterNodes, this helper just returns nodes that 2134 | // satisfy a predicate. 2135 | Digraph.prototype._filterNodes = function(pred) { 2136 | var filtered = []; 2137 | this.eachNode(function(u) { 2138 | if (pred(u)) { 2139 | filtered.push(u); 2140 | } 2141 | }); 2142 | return filtered; 2143 | }; 2144 | 2145 | 2146 | },{"./BaseGraph":13,"./data/Set":30,"./util":33}],17:[function(require,module,exports){ 2147 | /* 2148 | * This file is organized with in the following order: 2149 | * 2150 | * Exports 2151 | * Graph constructors 2152 | * Graph queries (e.g. nodes(), edges() 2153 | * Graph mutators 2154 | * Helper functions 2155 | */ 2156 | 2157 | var util = require("./util"), 2158 | BaseGraph = require("./BaseGraph"), 2159 | /* jshint -W079 */ 2160 | Set = require("./data/Set"); 2161 | 2162 | module.exports = Graph; 2163 | 2164 | /* 2165 | * Constructor to create a new undirected multi-graph. 2166 | */ 2167 | function Graph() { 2168 | BaseGraph.call(this); 2169 | 2170 | /*! Map of nodeId -> { otherNodeId -> Set of edge ids } */ 2171 | this._incidentEdges = {}; 2172 | } 2173 | 2174 | Graph.prototype = new BaseGraph(); 2175 | Graph.prototype.constructor = Graph; 2176 | 2177 | /* 2178 | * Always returns `false`. 2179 | */ 2180 | Graph.prototype.isDirected = function() { 2181 | return false; 2182 | }; 2183 | 2184 | /* 2185 | * Returns all nodes that are adjacent to the node with the id `u`. 2186 | * 2187 | * @param {String} u a node id 2188 | */ 2189 | Graph.prototype.neighbors = function(u) { 2190 | this._strictGetNode(u); 2191 | return Object.keys(this._incidentEdges[u]) 2192 | .map(function(v) { return this._nodes[v].id; }, this); 2193 | }; 2194 | 2195 | /* 2196 | * Returns an array of ids for all edges in the graph that are incident on `u`. 2197 | * If the node `u` is not in the graph this function raises an Error. 2198 | * 2199 | * Optionally a `v` node may also be specified. This causes the results to be 2200 | * filtered such that only edges between `u` and `v` are included. If the node 2201 | * `v` is specified but not in the graph then this function raises an Error. 2202 | * 2203 | * @param {String} u the node for which to find incident edges 2204 | * @param {String} [v] option node that must be adjacent to `u` 2205 | */ 2206 | Graph.prototype.incidentEdges = function(u, v) { 2207 | this._strictGetNode(u); 2208 | if (arguments.length > 1) { 2209 | this._strictGetNode(v); 2210 | return v in this._incidentEdges[u] ? this._incidentEdges[u][v].keys() : []; 2211 | } else { 2212 | return Set.unionAll(util.values(this._incidentEdges[u])).keys(); 2213 | } 2214 | }; 2215 | 2216 | /* 2217 | * Returns a string representation of this graph. 2218 | */ 2219 | Graph.prototype.toString = function() { 2220 | return "Graph " + JSON.stringify(this, null, 2); 2221 | }; 2222 | 2223 | /* 2224 | * Adds a new node with the id `u` to the graph and assigns it the value 2225 | * `value`. If a node with the id is already a part of the graph this function 2226 | * throws an Error. 2227 | * 2228 | * @param {String} u a node id 2229 | * @param {Object} [value] an optional value to attach to the node 2230 | */ 2231 | Graph.prototype.addNode = function(u, value) { 2232 | u = BaseGraph.prototype.addNode.call(this, u, value); 2233 | this._incidentEdges[u] = {}; 2234 | return u; 2235 | }; 2236 | 2237 | /* 2238 | * Removes a node from the graph that has the id `u`. Any edges incident on the 2239 | * node are also removed. If the graph does not contain a node with the id this 2240 | * function will throw an Error. 2241 | * 2242 | * @param {String} u a node id 2243 | */ 2244 | Graph.prototype.delNode = function(u) { 2245 | BaseGraph.prototype.delNode.call(this, u); 2246 | delete this._incidentEdges[u]; 2247 | }; 2248 | 2249 | /* 2250 | * Adds a new edge to the graph with the id `e` between a node with the id `u` 2251 | * and a node with an id `v` and assigns it the value `value`. This graph 2252 | * allows more than one edge between `u` and `v` as long as the id `e` 2253 | * is unique in the set of edges. If `e` is `null` the graph will assign a 2254 | * unique identifier to the edge. 2255 | * 2256 | * If `u` or `v` are not present in the graph this function will throw an 2257 | * Error. 2258 | * 2259 | * @param {String} [e] an edge id 2260 | * @param {String} u the node id of one of the adjacent nodes 2261 | * @param {String} v the node id of the other adjacent node 2262 | * @param {Object} [value] an optional value to attach to the edge 2263 | */ 2264 | Graph.prototype.addEdge = function(e, u, v, value) { 2265 | return BaseGraph.prototype._addEdge.call(this, e, u, v, value, 2266 | this._incidentEdges, this._incidentEdges); 2267 | }; 2268 | 2269 | /* 2270 | * Removes an edge in the graph with the id `e`. If no edge in the graph has 2271 | * the id `e` this function will throw an Error. 2272 | * 2273 | * @param {String} e an edge id 2274 | */ 2275 | Graph.prototype.delEdge = function(e) { 2276 | BaseGraph.prototype._delEdge.call(this, e, this._incidentEdges, this._incidentEdges); 2277 | }; 2278 | 2279 | 2280 | },{"./BaseGraph":13,"./data/Set":30,"./util":33}],18:[function(require,module,exports){ 2281 | var Set = require("../data/Set"); 2282 | 2283 | module.exports = components; 2284 | 2285 | /** 2286 | * Finds all [connected components][] in a graph and returns an array of these 2287 | * components. Each component is itself an array that contains the ids of nodes 2288 | * in the component. 2289 | * 2290 | * This function only works with undirected Graphs. 2291 | * 2292 | * [connected components]: http://en.wikipedia.org/wiki/Connected_component_(graph_theory) 2293 | * 2294 | * @param {Graph} g the graph to search for components 2295 | */ 2296 | function components(g) { 2297 | var results = []; 2298 | var visited = new Set(); 2299 | 2300 | function dfs(v, component) { 2301 | if (!visited.has(v)) { 2302 | visited.add(v); 2303 | component.push(v); 2304 | g.neighbors(v).forEach(function(w) { 2305 | dfs(w, component); 2306 | }); 2307 | } 2308 | } 2309 | 2310 | g.nodes().forEach(function(v) { 2311 | var component = []; 2312 | dfs(v, component); 2313 | if (component.length > 0) { 2314 | results.push(component); 2315 | } 2316 | }); 2317 | 2318 | return results; 2319 | } 2320 | 2321 | },{"../data/Set":30}],19:[function(require,module,exports){ 2322 | var PriorityQueue = require("../data/PriorityQueue"), 2323 | Digraph = require("../Digraph"); 2324 | 2325 | module.exports = dijkstra; 2326 | 2327 | /** 2328 | * This function is an implementation of [Dijkstra's algorithm][] which finds 2329 | * the shortest path from **source** to all other nodes in **g**. This 2330 | * function returns a map of `u -> { distance, predecessor }`. The distance 2331 | * property holds the sum of the weights from **source** to `u` along the 2332 | * shortest path or `Number.POSITIVE_INFINITY` if there is no path from 2333 | * **source**. The predecessor property can be used to walk the individual 2334 | * elements of the path from **source** to **u** in reverse order. 2335 | * 2336 | * This function takes an optional `weightFunc(e)` which returns the 2337 | * weight of the edge `e`. If no weightFunc is supplied then each edge is 2338 | * assumed to have a weight of 1. This function throws an Error if any of 2339 | * the traversed edges have a negative edge weight. 2340 | * 2341 | * This function takes an optional `incidentFunc(u)` which returns the ids of 2342 | * all edges incident to the node `u` for the purposes of shortest path 2343 | * traversal. By default this function uses the `g.outEdges` for Digraphs and 2344 | * `g.incidentEdges` for Graphs. 2345 | * 2346 | * This function takes `O((|E| + |V|) * log |V|)` time. 2347 | * 2348 | * [Dijkstra's algorithm]: http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm 2349 | * 2350 | * @param {Graph} g the graph to search for shortest paths from **source** 2351 | * @param {Object} source the source from which to start the search 2352 | * @param {Function} [weightFunc] optional weight function 2353 | * @param {Function} [incidentFunc] optional incident function 2354 | */ 2355 | function dijkstra(g, source, weightFunc, incidentFunc) { 2356 | var results = {}, 2357 | pq = new PriorityQueue(); 2358 | 2359 | weightFunc = weightFunc || function() { return 1; }; 2360 | incidentFunc = incidentFunc || (g.isDirected() 2361 | ? function(u) { return g.outEdges(u); } 2362 | : function(u) { return g.incidentEdges(u); }); 2363 | 2364 | g.nodes().forEach(function(u) { 2365 | var distance = u === source ? 0 : Number.POSITIVE_INFINITY; 2366 | results[u] = { distance: distance }; 2367 | pq.add(u, distance); 2368 | }); 2369 | 2370 | var u, uEntry; 2371 | while (pq.size() > 0) { 2372 | u = pq.removeMin(); 2373 | uEntry = results[u]; 2374 | if (uEntry.distance === Number.POSITIVE_INFINITY) { 2375 | break; 2376 | } 2377 | 2378 | incidentFunc(u).forEach(function(e) { 2379 | var incidentNodes = g.incidentNodes(e), 2380 | v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1], 2381 | vEntry = results[v], 2382 | weight = weightFunc(e), 2383 | distance = uEntry.distance + weight; 2384 | 2385 | if (weight < 0) { 2386 | throw new Error("dijkstra does not allow negative edge weights. Bad edge: " + e + " Weight: " + weight); 2387 | } 2388 | 2389 | if (distance < vEntry.distance) { 2390 | vEntry.distance = distance; 2391 | vEntry.predecessor = u; 2392 | pq.decrease(v, distance); 2393 | } 2394 | }); 2395 | } 2396 | 2397 | return results; 2398 | } 2399 | 2400 | },{"../Digraph":16,"../data/PriorityQueue":29}],20:[function(require,module,exports){ 2401 | var dijkstra = require("./dijkstra"); 2402 | 2403 | module.exports = dijkstraAll; 2404 | 2405 | /** 2406 | * This function finds the shortest path from each node to every other 2407 | * reachable node in the graph. It is similar to [alg.dijkstra][], but 2408 | * instead of returning a single-source array, it returns a mapping of 2409 | * of `source -> alg.dijksta(g, source, weightFunc, incidentFunc)`. 2410 | * 2411 | * This function takes an optional `weightFunc(e)` which returns the 2412 | * weight of the edge `e`. If no weightFunc is supplied then each edge is 2413 | * assumed to have a weight of 1. This function throws an Error if any of 2414 | * the traversed edges have a negative edge weight. 2415 | * 2416 | * This function takes an optional `incidentFunc(u)` which returns the ids of 2417 | * all edges incident to the node `u` for the purposes of shortest path 2418 | * traversal. By default this function uses the `outEdges` function on the 2419 | * supplied graph. 2420 | * 2421 | * This function takes `O(|V| * (|E| + |V|) * log |V|)` time. 2422 | * 2423 | * [alg.dijkstra]: dijkstra.js.html#dijkstra 2424 | * 2425 | * @param {Graph} g the graph to search for shortest paths from **source** 2426 | * @param {Function} [weightFunc] optional weight function 2427 | * @param {Function} [incidentFunc] optional incident function 2428 | */ 2429 | function dijkstraAll(g, weightFunc, incidentFunc) { 2430 | var results = {}; 2431 | g.nodes().forEach(function(u) { 2432 | results[u] = dijkstra(g, u, weightFunc, incidentFunc); 2433 | }); 2434 | return results; 2435 | } 2436 | 2437 | },{"./dijkstra":19}],21:[function(require,module,exports){ 2438 | var tarjan = require("./tarjan"); 2439 | 2440 | module.exports = findCycles; 2441 | 2442 | /* 2443 | * Given a Digraph **g** this function returns all nodes that are part of a 2444 | * cycle. Since there may be more than one cycle in a graph this function 2445 | * returns an array of these cycles, where each cycle is itself represented 2446 | * by an array of ids for each node involved in that cycle. 2447 | * 2448 | * [alg.isAcyclic][] is more efficient if you only need to determine whether 2449 | * a graph has a cycle or not. 2450 | * 2451 | * [alg.isAcyclic]: isAcyclic.js.html#isAcyclic 2452 | * 2453 | * @param {Digraph} g the graph to search for cycles. 2454 | */ 2455 | function findCycles(g) { 2456 | return tarjan(g).filter(function(cmpt) { return cmpt.length > 1; }); 2457 | } 2458 | 2459 | },{"./tarjan":25}],22:[function(require,module,exports){ 2460 | var Digraph = require("../Digraph"); 2461 | 2462 | module.exports = floydWarshall; 2463 | 2464 | /** 2465 | * This function is an implementation of the [Floyd-Warshall algorithm][], 2466 | * which finds the shortest path from each node to every other reachable node 2467 | * in the graph. It is similar to [alg.dijkstraAll][], but it handles negative 2468 | * edge weights and is more efficient for some types of graphs. This function 2469 | * returns a map of `source -> { target -> { distance, predecessor }`. The 2470 | * distance property holds the sum of the weights from `source` to `target` 2471 | * along the shortest path of `Number.POSITIVE_INFINITY` if there is no path 2472 | * from `source`. The predecessor property can be used to walk the individual 2473 | * elements of the path from `source` to `target` in reverse order. 2474 | * 2475 | * This function takes an optional `weightFunc(e)` which returns the 2476 | * weight of the edge `e`. If no weightFunc is supplied then each edge is 2477 | * assumed to have a weight of 1. 2478 | * 2479 | * This function takes an optional `incidentFunc(u)` which returns the ids of 2480 | * all edges incident to the node `u` for the purposes of shortest path 2481 | * traversal. By default this function uses the `outEdges` function on the 2482 | * supplied graph. 2483 | * 2484 | * This algorithm takes O(|V|^3) time. 2485 | * 2486 | * [Floyd-Warshall algorithm]: https://en.wikipedia.org/wiki/Floyd-Warshall_algorithm 2487 | * [alg.dijkstraAll]: dijkstraAll.js.html#dijkstraAll 2488 | * 2489 | * @param {Graph} g the graph to search for shortest paths from **source** 2490 | * @param {Function} [weightFunc] optional weight function 2491 | * @param {Function} [incidentFunc] optional incident function 2492 | */ 2493 | function floydWarshall(g, weightFunc, incidentFunc) { 2494 | var results = {}, 2495 | nodes = g.nodes(); 2496 | 2497 | weightFunc = weightFunc || function() { return 1; }; 2498 | incidentFunc = incidentFunc || (g.isDirected() 2499 | ? function(u) { return g.outEdges(u); } 2500 | : function(u) { return g.incidentEdges(u); }); 2501 | 2502 | nodes.forEach(function(u) { 2503 | results[u] = {}; 2504 | results[u][u] = { distance: 0 }; 2505 | nodes.forEach(function(v) { 2506 | if (u !== v) { 2507 | results[u][v] = { distance: Number.POSITIVE_INFINITY }; 2508 | } 2509 | }); 2510 | incidentFunc(u).forEach(function(e) { 2511 | var incidentNodes = g.incidentNodes(e), 2512 | v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1], 2513 | d = weightFunc(e); 2514 | if (d < results[u][v].distance) { 2515 | results[u][v] = { distance: d, predecessor: u }; 2516 | } 2517 | }); 2518 | }); 2519 | 2520 | nodes.forEach(function(k) { 2521 | var rowK = results[k]; 2522 | nodes.forEach(function(i) { 2523 | var rowI = results[i]; 2524 | nodes.forEach(function(j) { 2525 | var ik = rowI[k]; 2526 | var kj = rowK[j]; 2527 | var ij = rowI[j]; 2528 | var altDistance = ik.distance + kj.distance; 2529 | if (altDistance < ij.distance) { 2530 | ij.distance = altDistance; 2531 | ij.predecessor = kj.predecessor; 2532 | } 2533 | }); 2534 | }); 2535 | }); 2536 | 2537 | return results; 2538 | } 2539 | 2540 | },{"../Digraph":16}],23:[function(require,module,exports){ 2541 | var topsort = require("./topsort"); 2542 | 2543 | module.exports = isAcyclic; 2544 | 2545 | /* 2546 | * Given a Digraph **g** this function returns `true` if the graph has no 2547 | * cycles and returns `false` if it does. This algorithm returns as soon as it 2548 | * detects the first cycle. 2549 | * 2550 | * Use [alg.findCycles][] if you need the actual list of cycles in a graph. 2551 | * 2552 | * [alg.findCycles]: findCycles.js.html#findCycles 2553 | * 2554 | * @param {Digraph} g the graph to test for cycles 2555 | */ 2556 | function isAcyclic(g) { 2557 | try { 2558 | topsort(g); 2559 | } catch (e) { 2560 | if (e instanceof topsort.CycleException) return false; 2561 | throw e; 2562 | } 2563 | return true; 2564 | } 2565 | 2566 | },{"./topsort":26}],24:[function(require,module,exports){ 2567 | var Graph = require("../Graph"), 2568 | PriorityQueue = require("../data/PriorityQueue"); 2569 | 2570 | module.exports = prim; 2571 | 2572 | /** 2573 | * [Prim's algorithm][] takes a connected undirected graph and generates a 2574 | * [minimum spanning tree][]. This function returns the minimum spanning 2575 | * tree as an undirected graph. This algorithm is derived from the description 2576 | * in "Introduction to Algorithms", Third Edition, Cormen, et al., Pg 634. 2577 | * 2578 | * This function takes a `weightFunc(e)` which returns the weight of the edge 2579 | * `e`. It throws an Error if the graph is not connected. 2580 | * 2581 | * This function takes `O(|E| log |V|)` time. 2582 | * 2583 | * [Prim's algorithm]: https://en.wikipedia.org/wiki/Prim's_algorithm 2584 | * [minimum spanning tree]: https://en.wikipedia.org/wiki/Minimum_spanning_tree 2585 | * 2586 | * @param {Graph} g the graph used to generate the minimum spanning tree 2587 | * @param {Function} weightFunc the weight function to use 2588 | */ 2589 | function prim(g, weightFunc) { 2590 | var result = new Graph(), 2591 | parents = {}, 2592 | pq = new PriorityQueue(); 2593 | 2594 | if (g.order() === 0) { 2595 | return result; 2596 | } 2597 | 2598 | g.eachNode(function(u) { 2599 | pq.add(u, Number.POSITIVE_INFINITY); 2600 | result.addNode(u); 2601 | }); 2602 | 2603 | // Start from an arbitrary node 2604 | pq.decrease(g.nodes()[0], 0); 2605 | 2606 | var init = false; 2607 | while (pq.size() > 0) { 2608 | var u = pq.removeMin(); 2609 | if (u in parents) { 2610 | result.addEdge(null, u, parents[u]); 2611 | } else if (init) { 2612 | throw new Error("Input graph is not connected: " + g); 2613 | } else { 2614 | init = true; 2615 | } 2616 | 2617 | g.incidentEdges(u).forEach(function(e) { 2618 | var incidentNodes = g.incidentNodes(e), 2619 | v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1], 2620 | pri = pq.priority(v); 2621 | if (pri !== undefined) { 2622 | var edgeWeight = weightFunc(e); 2623 | if (edgeWeight < pri) { 2624 | parents[v] = u; 2625 | pq.decrease(v, edgeWeight); 2626 | } 2627 | } 2628 | }); 2629 | } 2630 | 2631 | return result; 2632 | } 2633 | 2634 | },{"../Graph":17,"../data/PriorityQueue":29}],25:[function(require,module,exports){ 2635 | var Digraph = require("../Digraph"); 2636 | 2637 | module.exports = tarjan; 2638 | 2639 | /** 2640 | * This function is an implementation of [Tarjan's algorithm][] which finds 2641 | * all [strongly connected components][] in the directed graph **g**. Each 2642 | * strongly connected component is composed of nodes that can reach all other 2643 | * nodes in the component via directed edges. A strongly connected component 2644 | * can consist of a single node if that node cannot both reach and be reached 2645 | * by any other specific node in the graph. Components of more than one node 2646 | * are guaranteed to have at least one cycle. 2647 | * 2648 | * This function returns an array of components. Each component is itself an 2649 | * array that contains the ids of all nodes in the component. 2650 | * 2651 | * [Tarjan's algorithm]: http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm 2652 | * [strongly connected components]: http://en.wikipedia.org/wiki/Strongly_connected_component 2653 | * 2654 | * @param {Digraph} g the graph to search for strongly connected components 2655 | */ 2656 | function tarjan(g) { 2657 | if (!g.isDirected()) { 2658 | throw new Error("tarjan can only be applied to a directed graph. Bad input: " + g); 2659 | } 2660 | 2661 | var index = 0, 2662 | stack = [], 2663 | visited = {}, // node id -> { onStack, lowlink, index } 2664 | results = []; 2665 | 2666 | function dfs(u) { 2667 | var entry = visited[u] = { 2668 | onStack: true, 2669 | lowlink: index, 2670 | index: index++ 2671 | }; 2672 | stack.push(u); 2673 | 2674 | g.successors(u).forEach(function(v) { 2675 | if (!(v in visited)) { 2676 | dfs(v); 2677 | entry.lowlink = Math.min(entry.lowlink, visited[v].lowlink); 2678 | } else if (visited[v].onStack) { 2679 | entry.lowlink = Math.min(entry.lowlink, visited[v].index); 2680 | } 2681 | }); 2682 | 2683 | if (entry.lowlink === entry.index) { 2684 | var cmpt = [], 2685 | v; 2686 | do { 2687 | v = stack.pop(); 2688 | visited[v].onStack = false; 2689 | cmpt.push(v); 2690 | } while (u !== v); 2691 | results.push(cmpt); 2692 | } 2693 | } 2694 | 2695 | g.nodes().forEach(function(u) { 2696 | if (!(u in visited)) { 2697 | dfs(u); 2698 | } 2699 | }); 2700 | 2701 | return results; 2702 | } 2703 | 2704 | },{"../Digraph":16}],26:[function(require,module,exports){ 2705 | var Digraph = require("../Digraph"); 2706 | 2707 | module.exports = topsort; 2708 | topsort.CycleException = CycleException; 2709 | 2710 | /* 2711 | * Given a graph **g**, this function returns an ordered list of nodes such 2712 | * that for each edge `u -> v`, `u` appears before `v` in the list. If the 2713 | * graph has a cycle it is impossible to generate such a list and 2714 | * **CycleException** is thrown. 2715 | * 2716 | * See [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting) 2717 | * for more details about how this algorithm works. 2718 | * 2719 | * @param {Digraph} g the graph to sort 2720 | */ 2721 | function topsort(g) { 2722 | if (!g.isDirected()) { 2723 | throw new Error("topsort can only be applied to a directed graph. Bad input: " + g); 2724 | } 2725 | 2726 | var visited = {}; 2727 | var stack = {}; 2728 | var results = []; 2729 | 2730 | function visit(node) { 2731 | if (node in stack) { 2732 | throw new CycleException(); 2733 | } 2734 | 2735 | if (!(node in visited)) { 2736 | stack[node] = true; 2737 | visited[node] = true; 2738 | g.predecessors(node).forEach(function(pred) { 2739 | visit(pred); 2740 | }); 2741 | delete stack[node]; 2742 | results.push(node); 2743 | } 2744 | } 2745 | 2746 | var sinks = g.sinks(); 2747 | if (g.order() !== 0 && sinks.length === 0) { 2748 | throw new CycleException(); 2749 | } 2750 | 2751 | g.sinks().forEach(function(sink) { 2752 | visit(sink); 2753 | }); 2754 | 2755 | return results; 2756 | } 2757 | 2758 | function CycleException() {} 2759 | 2760 | CycleException.prototype.toString = function() { 2761 | return "Graph has at least one cycle"; 2762 | }; 2763 | 2764 | },{"../Digraph":16}],27:[function(require,module,exports){ 2765 | // This file provides a helper function that mixes-in Dot behavior to an 2766 | // existing graph prototype. 2767 | 2768 | var /* jshint -W079 */ 2769 | Set = require("./data/Set"); 2770 | 2771 | module.exports = compoundify; 2772 | 2773 | // Extends the given SuperConstructor with the ability for nodes to contain 2774 | // other nodes. A special node id `null` is used to indicate the root graph. 2775 | function compoundify(SuperConstructor) { 2776 | function Constructor() { 2777 | SuperConstructor.call(this); 2778 | 2779 | // Map of object id -> parent id (or null for root graph) 2780 | this._parents = {}; 2781 | 2782 | // Map of id (or null) -> children set 2783 | this._children = { null: new Set() }; 2784 | } 2785 | 2786 | Constructor.prototype = new SuperConstructor(); 2787 | Constructor.prototype.constructor = Constructor; 2788 | 2789 | Constructor.prototype.parent = function(u, parent) { 2790 | this._strictGetNode(u); 2791 | 2792 | if (arguments.length < 2) { 2793 | return this._parents[u]; 2794 | } 2795 | 2796 | if (u === parent) { 2797 | throw new Error("Cannot make " + u + " a parent of itself"); 2798 | } 2799 | if (parent !== null) { 2800 | this._strictGetNode(parent); 2801 | } 2802 | 2803 | this._children[this._parents[u]].remove(u); 2804 | this._parents[u] = parent; 2805 | this._children[parent].add(u); 2806 | }; 2807 | 2808 | Constructor.prototype.children = function(u) { 2809 | if (u !== null) { 2810 | this._strictGetNode(u); 2811 | } 2812 | return this._children[u].keys(); 2813 | }; 2814 | 2815 | Constructor.prototype.addNode = function(u, value) { 2816 | u = SuperConstructor.prototype.addNode.call(this, u, value); 2817 | this._parents[u] = null; 2818 | this._children[u] = new Set(); 2819 | this._children[null].add(u); 2820 | return u; 2821 | }; 2822 | 2823 | Constructor.prototype.delNode = function(u) { 2824 | // Promote all children to the parent of the subgraph 2825 | var parent = this.parent(u); 2826 | this._children[u].keys().forEach(function(child) { 2827 | this.parent(child, parent); 2828 | }, this); 2829 | 2830 | this._children[parent].remove(u); 2831 | delete this._parents[u]; 2832 | delete this._children[u]; 2833 | 2834 | return SuperConstructor.prototype.delNode.call(this, u); 2835 | }; 2836 | 2837 | Constructor.prototype.copy = function() { 2838 | var copy = SuperConstructor.prototype.copy.call(this); 2839 | this.nodes().forEach(function(u) { 2840 | copy.parent(u, this.parent(u)); 2841 | }, this); 2842 | return copy; 2843 | }; 2844 | 2845 | return Constructor; 2846 | } 2847 | 2848 | },{"./data/Set":30}],28:[function(require,module,exports){ 2849 | var Graph = require("../Graph"), 2850 | Digraph = require("../Digraph"), 2851 | CGraph = require("../CGraph"), 2852 | CDigraph = require("../CDigraph"); 2853 | 2854 | exports.decode = function(nodes, edges, Ctor) { 2855 | Ctor = Ctor || Digraph; 2856 | 2857 | if (typeOf(nodes) !== "Array") { 2858 | throw new Error("nodes is not an Array"); 2859 | } 2860 | 2861 | if (typeOf(edges) !== "Array") { 2862 | throw new Error("edges is not an Array"); 2863 | } 2864 | 2865 | if (typeof Ctor === "string") { 2866 | switch(Ctor) { 2867 | case "graph": Ctor = Graph; break; 2868 | case "digraph": Ctor = Digraph; break; 2869 | case "cgraph": Ctor = CGraph; break; 2870 | case "cdigraph": Ctor = CDigraph; break; 2871 | default: throw new Error("Unrecognized graph type: " + Ctor); 2872 | } 2873 | } 2874 | 2875 | var graph = new Ctor(); 2876 | 2877 | nodes.forEach(function(u) { 2878 | graph.addNode(u.id, u.value); 2879 | }); 2880 | 2881 | // If the graph is compound, set up children... 2882 | if (graph.parent) { 2883 | nodes.forEach(function(u) { 2884 | if (u.children) { 2885 | u.children.forEach(function(v) { 2886 | graph.parent(v, u.id); 2887 | }); 2888 | } 2889 | }); 2890 | } 2891 | 2892 | edges.forEach(function(e) { 2893 | graph.addEdge(e.id, e.u, e.v, e.value); 2894 | }); 2895 | 2896 | return graph; 2897 | }; 2898 | 2899 | exports.encode = function(graph) { 2900 | var nodes = []; 2901 | var edges = []; 2902 | 2903 | graph.eachNode(function(u, value) { 2904 | var node = {id: u, value: value}; 2905 | if (graph.children) { 2906 | var children = graph.children(u); 2907 | if (children.length) { 2908 | node.children = children; 2909 | } 2910 | } 2911 | nodes.push(node); 2912 | }); 2913 | 2914 | graph.eachEdge(function(e, u, v, value) { 2915 | edges.push({id: e, u: u, v: v, value: value}); 2916 | }); 2917 | 2918 | var type; 2919 | if (graph instanceof CDigraph) { 2920 | type = "cdigraph"; 2921 | } else if (graph instanceof CGraph) { 2922 | type = "cgraph"; 2923 | } else if (graph instanceof Digraph) { 2924 | type = "digraph"; 2925 | } else if (graph instanceof Graph) { 2926 | type = "graph"; 2927 | } else { 2928 | throw new Error("Couldn't determine type of graph: " + graph); 2929 | } 2930 | 2931 | return { nodes: nodes, edges: edges, type: type }; 2932 | }; 2933 | 2934 | function typeOf(obj) { 2935 | return Object.prototype.toString.call(obj).slice(8, -1); 2936 | } 2937 | 2938 | },{"../CDigraph":14,"../CGraph":15,"../Digraph":16,"../Graph":17}],29:[function(require,module,exports){ 2939 | module.exports = PriorityQueue; 2940 | 2941 | /** 2942 | * A min-priority queue data structure. This algorithm is derived from Cormen, 2943 | * et al., "Introduction to Algorithms". The basic idea of a min-priority 2944 | * queue is that you can efficiently (in O(1) time) get the smallest key in 2945 | * the queue. Adding and removing elements takes O(log n) time. A key can 2946 | * have its priority decreased in O(log n) time. 2947 | */ 2948 | function PriorityQueue() { 2949 | this._arr = []; 2950 | this._keyIndices = {}; 2951 | } 2952 | 2953 | /** 2954 | * Returns the number of elements in the queue. Takes `O(1)` time. 2955 | */ 2956 | PriorityQueue.prototype.size = function() { 2957 | return this._arr.length; 2958 | }; 2959 | 2960 | /** 2961 | * Returns the keys that are in the queue. Takes `O(n)` time. 2962 | */ 2963 | PriorityQueue.prototype.keys = function() { 2964 | return this._arr.map(function(x) { return x.key; }); 2965 | }; 2966 | 2967 | /** 2968 | * Returns `true` if **key** is in the queue and `false` if not. 2969 | */ 2970 | PriorityQueue.prototype.has = function(key) { 2971 | return key in this._keyIndices; 2972 | }; 2973 | 2974 | /** 2975 | * Returns the priority for **key**. If **key** is not present in the queue 2976 | * then this function returns `undefined`. Takes `O(1)` time. 2977 | * 2978 | * @param {Object} key 2979 | */ 2980 | PriorityQueue.prototype.priority = function(key) { 2981 | var index = this._keyIndices[key]; 2982 | if (index !== undefined) { 2983 | return this._arr[index].priority; 2984 | } 2985 | }; 2986 | 2987 | /** 2988 | * Returns the key for the minimum element in this queue. If the queue is 2989 | * empty this function throws an Error. Takes `O(1)` time. 2990 | */ 2991 | PriorityQueue.prototype.min = function() { 2992 | if (this.size() === 0) { 2993 | throw new Error("Queue underflow"); 2994 | } 2995 | return this._arr[0].key; 2996 | }; 2997 | 2998 | /** 2999 | * Inserts a new key into the priority queue. If the key already exists in 3000 | * the queue this function returns `false`; otherwise it will return `true`. 3001 | * Takes `O(n)` time. 3002 | * 3003 | * @param {Object} key the key to add 3004 | * @param {Number} priority the initial priority for the key 3005 | */ 3006 | PriorityQueue.prototype.add = function(key, priority) { 3007 | if (!(key in this._keyIndices)) { 3008 | var entry = {key: key, priority: priority}; 3009 | var index = this._arr.length; 3010 | this._keyIndices[key] = index; 3011 | this._arr.push(entry); 3012 | this._decrease(index); 3013 | return true; 3014 | } 3015 | return false; 3016 | }; 3017 | 3018 | /** 3019 | * Removes and returns the smallest key in the queue. Takes `O(log n)` time. 3020 | */ 3021 | PriorityQueue.prototype.removeMin = function() { 3022 | this._swap(0, this._arr.length - 1); 3023 | var min = this._arr.pop(); 3024 | delete this._keyIndices[min.key]; 3025 | this._heapify(0); 3026 | return min.key; 3027 | }; 3028 | 3029 | /** 3030 | * Decreases the priority for **key** to **priority**. If the new priority is 3031 | * greater than the previous priority, this function will throw an Error. 3032 | * 3033 | * @param {Object} key the key for which to raise priority 3034 | * @param {Number} priority the new priority for the key 3035 | */ 3036 | PriorityQueue.prototype.decrease = function(key, priority) { 3037 | var index = this._keyIndices[key]; 3038 | if (priority > this._arr[index].priority) { 3039 | throw new Error("New priority is greater than current priority. " + 3040 | "Key: " + key + " Old: " + this._arr[index].priority + " New: " + priority); 3041 | } 3042 | this._arr[index].priority = priority; 3043 | this._decrease(index); 3044 | }; 3045 | 3046 | PriorityQueue.prototype._heapify = function(i) { 3047 | var arr = this._arr; 3048 | var l = 2 * i, 3049 | r = l + 1, 3050 | largest = i; 3051 | if (l < arr.length) { 3052 | largest = arr[l].priority < arr[largest].priority ? l : largest; 3053 | if (r < arr.length) { 3054 | largest = arr[r].priority < arr[largest].priority ? r : largest; 3055 | } 3056 | if (largest !== i) { 3057 | this._swap(i, largest); 3058 | this._heapify(largest); 3059 | } 3060 | } 3061 | }; 3062 | 3063 | PriorityQueue.prototype._decrease = function(index) { 3064 | var arr = this._arr; 3065 | var priority = arr[index].priority; 3066 | var parent; 3067 | while (index > 0) { 3068 | parent = index >> 1; 3069 | if (arr[parent].priority < priority) { 3070 | break; 3071 | } 3072 | this._swap(index, parent); 3073 | index = parent; 3074 | } 3075 | }; 3076 | 3077 | PriorityQueue.prototype._swap = function(i, j) { 3078 | var arr = this._arr; 3079 | var keyIndices = this._keyIndices; 3080 | var tmp = arr[i]; 3081 | arr[i] = arr[j]; 3082 | arr[j] = tmp; 3083 | keyIndices[arr[i].key] = i; 3084 | keyIndices[arr[j].key] = j; 3085 | }; 3086 | 3087 | },{}],30:[function(require,module,exports){ 3088 | var util = require("../util"); 3089 | 3090 | module.exports = Set; 3091 | 3092 | /** 3093 | * Constructs a new Set with an optional set of `initialKeys`. 3094 | * 3095 | * It is important to note that keys are coerced to String for most purposes 3096 | * with this object, similar to the behavior of JavaScript's Object. For 3097 | * example, the following will add only one key: 3098 | * 3099 | * var s = new Set(); 3100 | * s.add(1); 3101 | * s.add("1"); 3102 | * 3103 | * However, the type of the key is preserved internally so that `keys` returns 3104 | * the original key set uncoerced. For the above example, `keys` would return 3105 | * `[1]`. 3106 | */ 3107 | function Set(initialKeys) { 3108 | this._size = 0; 3109 | this._keys = {}; 3110 | 3111 | if (initialKeys) { 3112 | initialKeys.forEach(function(key) { this.add(key); }, this); 3113 | } 3114 | } 3115 | 3116 | /** 3117 | * Applies the [intersect](#intersect) function to all sets in the given array 3118 | * and returns the result as a new Set. 3119 | * 3120 | * @param {Set[]} sets the sets to intersect 3121 | */ 3122 | Set.intersectAll = function(sets) { 3123 | if (sets.length === 0) { 3124 | return new Set(); 3125 | } 3126 | 3127 | var result = new Set(sets[0].keys()); 3128 | sets.forEach(function(set) { 3129 | result = result.intersect(set); 3130 | }); 3131 | return result; 3132 | }; 3133 | 3134 | /** 3135 | * Applies the [union](#union) function to all sets in the given array and 3136 | * returns the result as a new Set. 3137 | * 3138 | * @param {Set[]} sets the sets to union 3139 | */ 3140 | Set.unionAll = function(sets) { 3141 | var result = new Set(); 3142 | sets.forEach(function(set) { 3143 | result = result.union(set); 3144 | }); 3145 | return result; 3146 | }; 3147 | 3148 | /** 3149 | * Returns the size of this set in `O(1)` time. 3150 | */ 3151 | Set.prototype.size = function() { 3152 | return this._size; 3153 | }; 3154 | 3155 | /** 3156 | * Returns the keys in this set. Takes `O(n)` time. 3157 | */ 3158 | Set.prototype.keys = function() { 3159 | return util.values(this._keys); 3160 | }; 3161 | 3162 | /** 3163 | * Tests if a key is present in this Set. Returns `true` if it is and `false` 3164 | * if not. Takes `O(1)` time. 3165 | */ 3166 | Set.prototype.has = function(key) { 3167 | return key in this._keys; 3168 | }; 3169 | 3170 | /** 3171 | * Adds a new key to this Set if it is not already present. Returns `true` if 3172 | * the key was added and `false` if it was already present. Takes `O(1)` time. 3173 | */ 3174 | Set.prototype.add = function(key) { 3175 | if (!(key in this._keys)) { 3176 | this._keys[key] = key; 3177 | ++this._size; 3178 | return true; 3179 | } 3180 | return false; 3181 | }; 3182 | 3183 | /** 3184 | * Removes a key from this Set. If the key was removed this function returns 3185 | * `true`. If not, it returns `false`. Takes `O(1)` time. 3186 | */ 3187 | Set.prototype.remove = function(key) { 3188 | if (key in this._keys) { 3189 | delete this._keys[key]; 3190 | --this._size; 3191 | return true; 3192 | } 3193 | return false; 3194 | }; 3195 | 3196 | /** 3197 | * Returns a new Set that only contains elements in both this set and the 3198 | * `other` set. They keys come from this set. 3199 | * 3200 | * If `other` is not a Set it is treated as an Array. 3201 | * 3202 | * @param {Set} other the other set with which to perform an intersection 3203 | */ 3204 | Set.prototype.intersect = function(other) { 3205 | // If the other Set does not look like a Set... 3206 | if (!other.keys) { 3207 | other = new Set(other); 3208 | } 3209 | var result = new Set(); 3210 | this.keys().forEach(function(k) { 3211 | if (other.has(k)) { 3212 | result.add(k); 3213 | } 3214 | }); 3215 | return result; 3216 | }; 3217 | 3218 | /** 3219 | * Returns a new Set that contains all of the keys in `this` set and `other` 3220 | * set. If a key is in `this` set, it is used in preference to the `other` set. 3221 | * 3222 | * If `other` is not a Set it is treated as an Array. 3223 | * 3224 | * @param {Set} other the other set with which to perform a union 3225 | */ 3226 | Set.prototype.union = function(other) { 3227 | if (!(other instanceof Set)) { 3228 | other = new Set(other); 3229 | } 3230 | var result = new Set(this.keys()); 3231 | other.keys().forEach(function(k) { 3232 | result.add(k); 3233 | }); 3234 | return result; 3235 | }; 3236 | 3237 | },{"../util":33}],31:[function(require,module,exports){ 3238 | /* jshint -W079 */ 3239 | var Set = require("./data/Set"); 3240 | 3241 | exports.all = function() { 3242 | return function() { return true; }; 3243 | }; 3244 | 3245 | exports.nodesFromList = function(nodes) { 3246 | var set = new Set(nodes); 3247 | return function(u) { 3248 | return set.has(u); 3249 | }; 3250 | }; 3251 | 3252 | },{"./data/Set":30}],32:[function(require,module,exports){ 3253 | var Graph = require("./Graph"), 3254 | Digraph = require("./Digraph"); 3255 | 3256 | // Side-effect based changes are lousy, but node doesn't seem to resolve the 3257 | // requires cycle. 3258 | 3259 | /** 3260 | * Returns a new directed graph using the nodes and edges from this graph. The 3261 | * new graph will have the same nodes, but will have twice the number of edges: 3262 | * each edge is split into two edges with opposite directions. Edge ids, 3263 | * consequently, are not preserved by this transformation. 3264 | */ 3265 | Graph.prototype.toDigraph = 3266 | Graph.prototype.asDirected = function() { 3267 | var g = new Digraph(); 3268 | this.eachNode(function(u, value) { g.addNode(u, value); }); 3269 | this.eachEdge(function(e, u, v, value) { 3270 | g.addEdge(null, u, v, value); 3271 | g.addEdge(null, v, u, value); 3272 | }); 3273 | return g; 3274 | }; 3275 | 3276 | /** 3277 | * Returns a new undirected graph using the nodes and edges from this graph. 3278 | * The new graph will have the same nodes, but the edges will be made 3279 | * undirected. Edge ids are preserved in this transformation. 3280 | */ 3281 | Digraph.prototype.toGraph = 3282 | Digraph.prototype.asUndirected = function() { 3283 | var g = new Graph(); 3284 | this.eachNode(function(u, value) { g.addNode(u, value); }); 3285 | this.eachEdge(function(e, u, v, value) { 3286 | g.addEdge(e, u, v, value); 3287 | }); 3288 | return g; 3289 | }; 3290 | 3291 | },{"./Digraph":16,"./Graph":17}],33:[function(require,module,exports){ 3292 | // Returns an array of all values for properties of **o**. 3293 | exports.values = function(o) { 3294 | return Object.keys(o).map(function(k) { return o[k]; }); 3295 | }; 3296 | 3297 | },{}],34:[function(require,module,exports){ 3298 | module.exports = '0.5.7'; 3299 | 3300 | },{}]},{},[1]) 3301 | ; 3302 | -------------------------------------------------------------------------------- /visualizations/git-visualization.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // We don't need to define dagreD3 because it will be loaded by the time this file runs. 3 | /* eslint-disable no-param-reassign */ 4 | 5 | import d3 from 'd3'; 6 | import DAG from '../AnimationData/DAG'; 7 | 8 | const gitVisualization = {}; 9 | 10 | gitVisualization.renderGraph = (graph) => { 11 | // Grab the nodes and links we want to display. 12 | const nodes = graph.nodes; 13 | const links = graph.links; 14 | 15 | // Grab the element where we want to display the graph. 16 | const graphElem = document.getElementById('git-g'); 17 | 18 | // Clear anything currently displayed in this element. 19 | d3.select(graphElem).selectAll('*').remove(); 20 | 21 | // Create a D3 selection with this element. 22 | const svg = d3.select(graphElem); 23 | 24 | // Create a new instance of dagreD3's renderer constructor. 25 | // dagreD3 is the global.dagreD3 object from dagre-d3.js. 26 | const renderer = new dagreD3.Renderer(); 27 | 28 | // Append our graph to the page. 29 | const layout = dagreD3.layout().rankDir('LR'); 30 | renderer.layout(layout) 31 | .run(dagreD3.json.decode(nodes, links), svg.append('g')); 32 | 33 | // Adjust the height our SVG to fit the content. 34 | const h = document.querySelector('#git-g g').getBoundingClientRect().height; 35 | let newHeight = h + 40; 36 | newHeight = newHeight < 80 ? 80 : newHeight; 37 | const $svg = document.getElementById('git-svg'); 38 | $svg.setAttribute('height', newHeight); 39 | 40 | // Add zoom functionality. 41 | d3.select($svg).call(d3.behavior.zoom().on('zoom', () => { 42 | const ev = d3.event; 43 | svg.select('g') 44 | .attr('transform', `translate(${ev.translate}) scale(${ev.scale})`); 45 | })); 46 | }; 47 | 48 | gitVisualization.createGraph = (nestedCommitArr) => { 49 | const gitGraph = new DAG(); 50 | for (let i = 0; i < nestedCommitArr.length - 1; i++) { 51 | if (nestedCommitArr[i][1].match(/\s/)) { 52 | nestedCommitArr[i][1] = nestedCommitArr[i][1].split(/\s/); 53 | } 54 | gitGraph.addEdges(nestedCommitArr[i][0], nestedCommitArr[i][2], null, nestedCommitArr[i][1]); 55 | } 56 | 57 | const result = {}; 58 | const nodes = []; 59 | const links = []; 60 | const names = gitGraph.names; 61 | const vertices = gitGraph.vertices; 62 | let linkNum = 1; 63 | 64 | // For each commit in the names array... 65 | for (let i = 0; i < names.length; i++) { 66 | // Create a node and push it to the nodes array. 67 | const node = {}; 68 | const hash = names[i]; 69 | node.id = hash; 70 | node.value = {}; 71 | node.value.label = hash; 72 | node.value.message = vertices[hash].value || 'No commit message available'; 73 | nodes.push(node); 74 | 75 | // Create a link for each of the commit's parents and push it to the links array. 76 | const parents = vertices[hash].incomingNames; 77 | for (let j = 0; j < parents.length; j ++) { 78 | const link = {}; 79 | link.u = hash; 80 | link.v = parents[j]; 81 | link.value = {}; 82 | link.value.label = `link ${linkNum}`; 83 | links.push(link); 84 | linkNum ++; 85 | } 86 | } 87 | 88 | result.nodes = nodes; 89 | result.links = links; 90 | return result; 91 | }; 92 | 93 | export default gitVisualization; 94 | -------------------------------------------------------------------------------- /visualizations/link-visualization.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | 3 | const linkVisualization = {}; 4 | 5 | // Create a diagonal generator, a type of path data generator. 6 | linkVisualization.diagonal = d3.svg.diagonal() 7 | .projection((d) => [d.y, d.x]); 8 | 9 | // Set a duration to use for all our transitions. 10 | linkVisualization.duration = 450; 11 | 12 | // Set the attributes for links that are new to the DOM. 13 | linkVisualization.enter = (selection, diagonal, duration) => { 14 | selection.attr('d', d => { 15 | const o = { x: d.target.x0, y: d.target.y0 }; 16 | return diagonal({ source: o, target: o }); 17 | }) 18 | .style('stroke', d => d.target.level) 19 | .style('stroke-width', 0.5); 20 | 21 | linkVisualization.update(selection, diagonal, duration); 22 | }; 23 | 24 | // Transition new and updated links to their new position. 25 | linkVisualization.update = (selection, diagonal, duration) => { 26 | selection.transition() 27 | .duration(duration) 28 | .attr('d', diagonal) 29 | .style('stroke', d => d.target.level) 30 | .style('stroke-width', 0.5); 31 | }; 32 | 33 | export default linkVisualization; 34 | -------------------------------------------------------------------------------- /visualizations/tree-visualization.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // We don't need to define ipcRenderer because it will be loaded by the time this file runs. 3 | /* eslint-disable no-confusing-arrow */ 4 | 5 | import linkVisualization from './link-visualization'; 6 | 7 | const treeVisualization = {}; 8 | 9 | treeVisualization.duration = linkVisualization.duration; 10 | 11 | // Set the attributes for nodes that are new to the DOM, including placing them in their initial 12 | // position (x0, y0). 13 | treeVisualization.enter = (selection, duration) => { 14 | // Translate this node d.y0 units right and d.x0 units down. 15 | selection.attr('transform', d => `translate(${d.y0},${d.x0})`); 16 | 17 | selection.select('image') 18 | .on('click', d => { 19 | if (d.type) { 20 | const commandString = `cd ${d.name.replace(/ /g, '\\ ')} \n\r`; 21 | ipcRenderer.send('command-message', commandString); 22 | } 23 | if (d.value || d.children) { 24 | const commandString = `cd .. \n\r`; 25 | ipcRenderer.send('command-message', commandString); 26 | } 27 | }); 28 | 29 | treeVisualization.update(selection, duration); 30 | }; 31 | 32 | // Transition new and updated nodes to their new positions. 33 | treeVisualization.update = (selection, duration) => { 34 | const transition = selection.transition() 35 | .duration(duration) 36 | .attr('transform', d => `translate(${d.y},${d.x})`); 37 | 38 | // Update the x, y, width, and height for proper scaling 39 | // y must always be half of the height 40 | const scale = 16; 41 | 42 | transition.select('image') 43 | .attr('xlink:href', d => d.icon) 44 | .attr('x', d => d.position_x ? d.position_x : 0) 45 | .attr('y', d => d.position_y ? d.position_y : scale * -(1 / 2)) 46 | .attr('width', d => d.value ? d.value : scale) 47 | .attr('height', d => d.value ? d.value : scale); 48 | 49 | transition.select('text') 50 | .attr('x', d => d.value ? -9 : 17) 51 | .attr('y', d => { 52 | if (d.value) return 25; 53 | return undefined; 54 | }) 55 | .attr('dy', '.35em') 56 | .attr('text-anchor', 'start') 57 | .style('fill-opacity', 1) 58 | .style('font-size', d => d.value ? 10 : 7) 59 | .text(d => d.name) 60 | .style('fill-opacity', 1) 61 | .style('fill', '#A09E9E'); 62 | }; 63 | 64 | export default treeVisualization; 65 | --------------------------------------------------------------------------------