├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs └── react-commits-graph.png ├── examples ├── commits.json ├── demo.js ├── index.html └── run.js ├── index.js ├── package.json └── src ├── commits-graph-mixin.coffee ├── commits-graph.coffee ├── generate-graph-data.coffee └── svg-path-data.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore docs files 2 | _gh_pages 3 | _site 4 | .ruby-version 5 | 6 | # Numerous always-ignore extensions 7 | *.diff 8 | *.err 9 | *.orig 10 | *.log 11 | *.rej 12 | *.swo 13 | *.swp 14 | *.zip 15 | *.vi 16 | *~ 17 | 18 | # OS or Editor folders 19 | .DS_Store 20 | ._* 21 | Thumbs.db 22 | .cache 23 | .project 24 | .settings 25 | .tmproj 26 | *.esproj 27 | nbproject 28 | *.sublime-project 29 | *.sublime-workspace 30 | .idea 31 | 32 | # Komodo 33 | *.komodoproject 34 | .komodotools 35 | 36 | # Folders to ignore 37 | node_modules 38 | bower_components 39 | 40 | lib/ 41 | examples/bundle.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | examples/ 4 | docs/ 5 | Cakefile 6 | *.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 James Friend 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-commits-graph 2 | 3 | a react component to render an svg graph of git commits 4 | 5 | adapted from [tclh123/commits-graph](https://github.com/tclh123/commits-graph) 6 | 7 | ![react-commits-graph](docs/react-commits-graph.png) 8 | 9 | ## example 10 | 11 | code to generate the graph above 12 | 13 | ```js 14 | var React = require('react') 15 | var CommitsGraph = require('react-commits-graph') 16 | var commits = require('./commits.json') 17 | 18 | var selected = null 19 | 20 | function handleClick(sha) { 21 | selected = sha 22 | render() 23 | } 24 | 25 | function render() { 26 | React.render( 27 | 32 | ), document.body) 33 | } 34 | 35 | render() 36 | ``` 37 | 38 | expected structure of `commits` prop: 39 | ```js 40 | [ 41 | { 42 | "parents": [ 43 | "82aa2102c8291f56f8dfefce1dce40d8a0dd686b", 44 | "175dfbbdbf8734069efaafced5a531dbf77c3a57" 45 | ], 46 | "sha": "5a7e04df76e21f9ba4a48098b6b26f19b51b99b1" 47 | }, 48 | { 49 | "parents": [ 50 | "90113cac59463df2e182e48444b8395658ebf840" 51 | ], 52 | "sha": "175dfbbdbf8734069efaafced5a531dbf77c3a57" 53 | }, 54 | ... 55 | ] 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/react-commits-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsdf/react-commits-graph/170ab272020e1dc8b960ca6110f23c91524013f3/docs/react-commits-graph.png -------------------------------------------------------------------------------- /examples/commits.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "parents": [ 4 | "82aa2102c8291f56f8dfefce1dce40d8a0dd686b", 5 | "175dfbbdbf8734069efaafced5a531dbf77c3a57" 6 | ], 7 | "sha": "5a7e04df76e21f9ba4a48098b6b26f19b51b99b1" 8 | }, 9 | { 10 | "parents": [ 11 | "90113cac59463df2e182e48444b8395658ebf840" 12 | ], 13 | "sha": "175dfbbdbf8734069efaafced5a531dbf77c3a57" 14 | }, 15 | { 16 | "parents": [ 17 | "82aa2102c8291f56f8dfefce1dce40d8a0dd686b" 18 | ], 19 | "sha": "90113cac59463df2e182e48444b8395658ebf840" 20 | }, 21 | { 22 | "parents": [ 23 | "395037182d9c34de2c26afc65b045ee875c93d25", 24 | "3f3d28b62f46f40959e31d54dbe21941801e91e3" 25 | ], 26 | "sha": "82aa2102c8291f56f8dfefce1dce40d8a0dd686b" 27 | }, 28 | { 29 | "parents": [ 30 | "6c7fa94319d3ab727b5606ad2bd0a732e65e7012" 31 | ], 32 | "sha": "3f3d28b62f46f40959e31d54dbe21941801e91e3" 33 | }, 34 | { 35 | "parents": [ 36 | "639f3e8d021af55953b2d9ee55bf83a0193c7797" 37 | ], 38 | "sha": "6c7fa94319d3ab727b5606ad2bd0a732e65e7012" 39 | }, 40 | { 41 | "parents": [ 42 | "f22fae5a4b3bcdcd8583f9cd25da8fbacad8fcf3" 43 | ], 44 | "sha": "639f3e8d021af55953b2d9ee55bf83a0193c7797" 45 | }, 46 | { 47 | "parents": [ 48 | "3b4cb078964d91ed9e9d422bd61087c552a928a4" 49 | ], 50 | "sha": "f22fae5a4b3bcdcd8583f9cd25da8fbacad8fcf3" 51 | }, 52 | { 53 | "parents": [ 54 | "f6b061794ccd2f9a46daefe082cae360dea85a41" 55 | ], 56 | "sha": "3b4cb078964d91ed9e9d422bd61087c552a928a4" 57 | }, 58 | { 59 | "parents": [ 60 | "2e00fd89a9c551e51d3f7a44044b4d29ed33bbd2" 61 | ], 62 | "sha": "f6b061794ccd2f9a46daefe082cae360dea85a41" 63 | }, 64 | { 65 | "parents": [ 66 | "395037182d9c34de2c26afc65b045ee875c93d25" 67 | ], 68 | "sha": "2e00fd89a9c551e51d3f7a44044b4d29ed33bbd2" 69 | }, 70 | { 71 | "parents": [ 72 | "4cde659b0b50e3ffe4ef73def1a077d47c241b44" 73 | ], 74 | "sha": "395037182d9c34de2c26afc65b045ee875c93d25" 75 | }, 76 | { 77 | "parents": [ 78 | "8e0b15259fbb7dd339b9aca646501b4203973874" 79 | ], 80 | "sha": "4cde659b0b50e3ffe4ef73def1a077d47c241b44" 81 | }, 82 | { 83 | "parents": [ 84 | "acaf8ad845d28fb9b4119bcfc1500c9ce1a903b5" 85 | ], 86 | "sha": "8e0b15259fbb7dd339b9aca646501b4203973874" 87 | }, 88 | { 89 | "parents": [ 90 | "4335d3ab62d8b5531508a27e622ad186d86bcdc7" 91 | ], 92 | "sha": "acaf8ad845d28fb9b4119bcfc1500c9ce1a903b5" 93 | }, 94 | { 95 | "parents": [ 96 | "cd10df3588ca6858af0201c28b58a8259289e3f1" 97 | ], 98 | "sha": "4335d3ab62d8b5531508a27e622ad186d86bcdc7" 99 | }, 100 | { 101 | "parents": [ 102 | "496f8a4e3aaa53630c573076a9d988e020cbf1ca" 103 | ], 104 | "sha": "cd10df3588ca6858af0201c28b58a8259289e3f1" 105 | }, 106 | { 107 | "parents": [ 108 | "ee9f4af79ff6fab50f7ebdf823f06dfdbc000b24" 109 | ], 110 | "sha": "496f8a4e3aaa53630c573076a9d988e020cbf1ca" 111 | }, 112 | { 113 | "parents": [ 114 | "4b247b9f19b0b345755aaae1914a280e66d09a0c" 115 | ], 116 | "sha": "ee9f4af79ff6fab50f7ebdf823f06dfdbc000b24" 117 | }, 118 | { 119 | "parents": [ 120 | "2cb54f702fa5f0ffd2435dbae7b05d52f8124c1e" 121 | ], 122 | "sha": "4b247b9f19b0b345755aaae1914a280e66d09a0c" 123 | }, 124 | { 125 | "parents": [ 126 | "69d9c1b99fa70108614a453a91c57c2945261dfa" 127 | ], 128 | "sha": "2cb54f702fa5f0ffd2435dbae7b05d52f8124c1e" 129 | }, 130 | { 131 | "parents": [ 132 | "503867bfcf9b5b615909225af2410e8225c8a6a4" 133 | ], 134 | "sha": "69d9c1b99fa70108614a453a91c57c2945261dfa" 135 | }, 136 | { 137 | "parents": [ 138 | "d62b312eb28f396ff67afecdd68ca9bcf4adf722" 139 | ], 140 | "sha": "503867bfcf9b5b615909225af2410e8225c8a6a4" 141 | }, 142 | { 143 | "parents": [ 144 | "071e7bde5b16832be2a2e48f1f5c91733644adf5" 145 | ], 146 | "sha": "d62b312eb28f396ff67afecdd68ca9bcf4adf722" 147 | }, 148 | { 149 | "parents": [ 150 | "e72dd3608da184c8760d011bd4320b902f5fdc0b" 151 | ], 152 | "sha": "071e7bde5b16832be2a2e48f1f5c91733644adf5" 153 | }, 154 | { 155 | "parents": [ 156 | "0e2a0bfb87ae21c79518a6c6dc6aa9aee0bf6dfb" 157 | ], 158 | "sha": "e72dd3608da184c8760d011bd4320b902f5fdc0b" 159 | }, 160 | { 161 | "parents": [ 162 | "e41d31976f002cee56a917dde2c1c75615e39d63" 163 | ], 164 | "sha": "0e2a0bfb87ae21c79518a6c6dc6aa9aee0bf6dfb" 165 | }, 166 | { 167 | "parents": [ 168 | "96cd2f670d5829716a9a98d543d063f4fe304026" 169 | ], 170 | "sha": "e41d31976f002cee56a917dde2c1c75615e39d63" 171 | }, 172 | { 173 | "parents": [ 174 | "7d9b3577930547e3549f630cf21160fdbcaea87e" 175 | ], 176 | "sha": "96cd2f670d5829716a9a98d543d063f4fe304026" 177 | }, 178 | { 179 | "parents": [ 180 | "876ba955da6788d60180261ec884b34fc73e574c" 181 | ], 182 | "sha": "7d9b3577930547e3549f630cf21160fdbcaea87e" 183 | }, 184 | { 185 | "parents": [ 186 | "cc74c787c3e6f174700fac0f146acf67fe4233f4" 187 | ], 188 | "sha": "876ba955da6788d60180261ec884b34fc73e574c" 189 | }, 190 | { 191 | "parents": [ 192 | "50a5dae8710e0d7cd5aaa9bd240ff2ac2135f463" 193 | ], 194 | "sha": "cc74c787c3e6f174700fac0f146acf67fe4233f4" 195 | }, 196 | { 197 | "parents": [ 198 | "ac7dcd4513d30a970d3576be4492f82815a5964b" 199 | ], 200 | "sha": "50a5dae8710e0d7cd5aaa9bd240ff2ac2135f463" 201 | }, 202 | { 203 | "parents": [], 204 | "sha": "ac7dcd4513d30a970d3576be4492f82815a5964b" 205 | } 206 | ] -------------------------------------------------------------------------------- /examples/demo.js: -------------------------------------------------------------------------------- 1 | 2 | React = require('react') 3 | CommitsGraph = require('../') 4 | commits = require('./commits.json') 5 | 6 | var graphEl = document.querySelector('#graph') 7 | 8 | var selected = null 9 | 10 | function handleClick(sha) { 11 | selected = sha 12 | render() 13 | } 14 | 15 | function render() { 16 | React.renderComponent(CommitsGraph({ 17 | commits: commits, 18 | onClick: handleClick, 19 | selected: selected, 20 | height: window.innerHeight, 21 | width: window.innerWidth, 22 | }), graphEl) 23 | } 24 | 25 | window.addEventListener('resize', render) 26 | 27 | render() 28 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | exec = require('child_process').exec 4 | spawn = require('child_process').spawn 5 | 6 | exec('git rev-parse --show-toplevel', function(err, stdout, stderr) { 7 | REPO_ROOT = stdout.split('\n')[0] 8 | EXAMPLES_DIR = REPO_ROOT+'/examples' 9 | 10 | BROWSERIFY_ARGS = [ 11 | '--extension=".coffee"', 12 | '--outfile', EXAMPLES_DIR+'/bundle.js', 13 | '--entry', EXAMPLES_DIR+'/demo.js', 14 | ] 15 | 16 | exec('npm run prepublish', function(err, stdout, stderr) { 17 | if (process.argv[2] == 'watch') { 18 | console.log('watch and build') 19 | spawn('npm', ['run', 'watch'], { stdio: 'inherit' }) 20 | spawn('watchify', ['-v'].concat(BROWSERIFY_ARGS), { stdio: 'inherit' }) 21 | } else { 22 | console.log('build once') 23 | spawn('browserify', BROWSERIFY_ARGS, { stdio: 'inherit' }) 24 | } 25 | spawn('http-server', [EXAMPLES_DIR], { stdio: 'inherit' }) 26 | exec('open http://localhost:8080') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/commits-graph'); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-commits-graph", 3 | "version": "0.2.0", 4 | "description": "a react component to render an svg graph of git commits", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "prepublish": "mkdir -p lib/ && rm lib/*.js && npm run build", 9 | "build": "coffee -cbo lib/ src/*.coffee", 10 | "watch": "coffee -cbwo lib/ src/*.coffee", 11 | "demo": "examples/run.js", 12 | "demo-watch": "examples/run.js watch" 13 | }, 14 | "keywords": [ 15 | "react-component", 16 | "react", 17 | "git", 18 | "graph", 19 | "visualization" 20 | ], 21 | "author": "James Friend", 22 | "licenses": [ 23 | { 24 | "type": "MIT", 25 | "url": "https://raw.github.com/jsdf/react-commits-graph/master/LICENSE" 26 | } 27 | ], 28 | "homepage": "https://github.com/jsdf/react-commits-graph", 29 | "bugs": "https://github.com/jsdf/react-commits-graph/issues", 30 | "repository": { 31 | "type": "git", 32 | "url": "git://github.com/jsdf/react-commits-graph.git" 33 | }, 34 | "devDependencies": { 35 | "coffee-script": "^1.9.0", 36 | "react": "^0.12.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commits-graph-mixin.coffee: -------------------------------------------------------------------------------- 1 | React = require 'react' 2 | 3 | generateGraphData = require './generate-graph-data' 4 | 5 | SVGPathData = require './svg-path-data' 6 | 7 | COLOURS = [ 8 | "#e11d21", 9 | "#fbca04", 10 | "#009800", 11 | "#006b75", 12 | "#207de5", 13 | "#0052cc", 14 | "#5319e7", 15 | "#f7c6c7", 16 | "#fad8c7", 17 | "#fef2c0", 18 | "#bfe5bf", 19 | "#c7def8", 20 | "#bfdadc", 21 | "#bfd4f2", 22 | "#d4c5f9", 23 | "#cccccc", 24 | "#84b6eb", 25 | "#e6e6e6", 26 | "#ffffff", 27 | "#cc317c", 28 | ] 29 | 30 | classSet = (classes...) -> classes.filter(Boolean).join(' ') 31 | 32 | getColour = (branch) -> 33 | n = COLOURS.length 34 | COLOURS[branch % n] 35 | 36 | branchCount = (data) -> 37 | maxBranch = -1 38 | i = 0 39 | 40 | while i < data.length 41 | j = 0 42 | 43 | while j < data[i][2].length 44 | if maxBranch < data[i][2][j][0] or maxBranch < data[i][2][j][1] 45 | maxBranch = Math.max.apply(Math, [ 46 | data[i][2][j][0] 47 | data[i][2][j][1] 48 | ]) 49 | j++ 50 | i++ 51 | maxBranch + 1 52 | 53 | distance = (point1, point2) -> 54 | xs = 0 55 | ys = 0 56 | xs = point2.x - point1.x 57 | xs = xs * xs 58 | ys = point2.y - point1.y 59 | ys = ys * ys 60 | Math.sqrt xs + ys 61 | 62 | CommitsGraphMixin = 63 | getDefaultProps: -> 64 | y_step: 20 65 | x_step: 20 66 | dotRadius: 3 67 | lineWidth: 2 68 | selected: null 69 | mirror: false 70 | unstyled: false 71 | 72 | componentWillReceiveProps: -> 73 | @graphData = null 74 | @branchCount = null 75 | 76 | cursorPoint: (e) -> 77 | svg = @getDOMNode() 78 | svgPoint = svg.createSVGPoint() 79 | svgPoint.x = e.clientX 80 | svgPoint.y = e.clientY 81 | svgPoint.matrixTransform svg.getScreenCTM().inverse() 82 | 83 | handleClick: (e) -> 84 | cursorLoc = @cursorPoint(e) 85 | 86 | smallestDistance = Infinity 87 | closestCommit = null 88 | for commit in @renderedCommitsPositions 89 | commitDistance = distance(cursorLoc, commit) 90 | if commitDistance < smallestDistance 91 | smallestDistance = commitDistance 92 | closestCommit = commit 93 | 94 | @props.onClick?(closestCommit.sha) 95 | 96 | getGraphData: -> 97 | @graphData ||= generateGraphData(@props.commits) 98 | 99 | getBranchCount: -> 100 | @branchCount ||= branchCount(@getGraphData()) 101 | 102 | getWidth: -> 103 | return @props.width if @props.width? 104 | @getContentWidth() 105 | 106 | getContentWidth: -> 107 | (@getBranchCount() + 0.5) * @props.x_step 108 | 109 | getHeight: -> 110 | return @props.height if @props.height? 111 | @getContentHeight() 112 | 113 | getContentHeight: -> 114 | (@getGraphData().length + 2) * @props.y_step 115 | 116 | getInvert: -> 117 | if @props.mirror 118 | 0 - @props.width 119 | else 120 | 0 121 | 122 | getOffset: -> 123 | @getWidth() / 2 - @getContentWidth() / 2 124 | 125 | renderRouteNode: (svgPathDataAttribute, branch) -> 126 | unless @props.unstyled 127 | colour = getColour(branch) 128 | style = 129 | 'stroke': colour 130 | 'stroke-width': @props.lineWidth 131 | 'fill': 'none' 132 | 133 | classes = "commits-graph-branch-#{branch}" 134 | 135 | React.DOM.path 136 | d: svgPathDataAttribute 137 | style: style 138 | className: classes 139 | 140 | renderRoute: (commit_idx, [from, to, branch]) -> 141 | {x_step, y_step} = @props 142 | offset = @getOffset() 143 | invert = @getInvert() 144 | 145 | svgPath = new SVGPathData 146 | 147 | from_x = offset + invert + (from + 1) * x_step 148 | from_y = (commit_idx + 0.5) * y_step 149 | to_x = offset + invert + (to + 1) * x_step 150 | to_y = (commit_idx + 0.5 + 1) * y_step 151 | 152 | svgPath.moveTo(from_x, from_y) 153 | if from_x is to_x 154 | svgPath.lineTo(to_x, to_y) 155 | else 156 | svgPath.bezierCurveTo( 157 | from_x - x_step / 4, from_y + y_step / 3 * 2, 158 | to_x + x_step / 4, to_y - y_step / 3 * 2, 159 | to_x, to_y 160 | ) 161 | 162 | @renderRouteNode(svgPath.toString(), branch) 163 | 164 | renderCommitNode: (x, y, sha, dot_branch) -> 165 | radius = @props.dotRadius 166 | 167 | unless @props.unstyled 168 | colour = getColour(dot_branch) 169 | if sha is @props.selected 170 | strokeColour = '#000' 171 | strokeWidth = 2 172 | else 173 | strokeColour = colour 174 | strokeWidth = 1 175 | style = 176 | 'stroke': strokeColour 177 | 'stroke-width': strokeWidth 178 | 'fill': colour 179 | 180 | selectedClass = 'selected' if @props.selected 181 | classes = classSet("commits-graph-branch-#{dot_branch}", selectedClass) 182 | 183 | React.DOM.circle 184 | cx: x 185 | cy: y 186 | r: radius 187 | style: style 188 | onClick: @handleClick 189 | 'data-sha': sha 190 | className: classes 191 | 192 | renderCommit: (idx, [sha, dot, routes_data]) -> 193 | [dot_offset, dot_branch] = dot 194 | 195 | # draw dot 196 | {x_step, y_step} = @props 197 | offset = @getOffset() 198 | invert = @getInvert() 199 | 200 | x = offset + invert + (dot_offset + 1) * x_step 201 | y = (idx + 0.5) * y_step 202 | 203 | commitNode = @renderCommitNode(x, y, sha, dot_branch) 204 | 205 | routeNodes = for route, index in routes_data 206 | @renderRoute(idx, route) 207 | 208 | @renderedCommitsPositions.push {x, y, sha} 209 | 210 | [commitNode, routeNodes] 211 | 212 | renderGraph: -> 213 | # reset lookup table of commit node locations 214 | @renderedCommitsPositions = [] 215 | 216 | allCommitNodes = [] 217 | allRouteNodes = [] 218 | 219 | for commit, index in @getGraphData() 220 | [commitNode, routeNodes] = @renderCommit(index, commit) 221 | allCommitNodes.push commitNode 222 | allRouteNodes = allRouteNodes.concat routeNodes 223 | 224 | children = [].concat allRouteNodes, allCommitNodes 225 | 226 | height = @getHeight() 227 | width = @getWidth() 228 | unless @props.unstyled 229 | style = {height, width, cursor: 'pointer'} 230 | 231 | svgProps = {height, width, style, children} 232 | 233 | React.DOM.svg 234 | onClick: @handleClick 235 | height: height 236 | width: width 237 | style: style 238 | children: children 239 | 240 | module.exports = CommitsGraphMixin -------------------------------------------------------------------------------- /src/commits-graph.coffee: -------------------------------------------------------------------------------- 1 | React = require 'react' 2 | 3 | CommitsGraphMixin = require './commits-graph-mixin' 4 | 5 | CommitsGraph = React.createClass 6 | displayName: 'CommitsGraph' 7 | 8 | mixins: [ 9 | CommitsGraphMixin, 10 | ] 11 | 12 | render: -> 13 | @renderGraph() 14 | 15 | module.exports = CommitsGraph 16 | -------------------------------------------------------------------------------- /src/generate-graph-data.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Generate preformatted data of commits graph. 3 | ### 4 | 5 | 6 | generateGraphData = (commits) -> 7 | ### 8 | Generate graph data. 9 | 10 | :param commits: a list of commit, which should have 11 | `sha`, `parents` properties. 12 | :returns: data nodes, a json list of 13 | [ 14 | sha, 15 | [offset, branch], //dot 16 | [ 17 | [from, to, branch], // route 1 18 | [from, to, branch], // route 2 19 | [from, to, branch], 20 | ] // routes 21 | ], // node 22 | ### 23 | 24 | nodes = [] 25 | branchIndex = [0] 26 | reserve = [] 27 | branches = {} 28 | 29 | getBranch = (sha) -> 30 | unless branches[sha]? 31 | branches[sha] = branchIndex[0] 32 | reserve.push(branchIndex[0]) 33 | branchIndex[0]++ 34 | branches[sha] 35 | 36 | for commit in commits 37 | branch = getBranch(commit.sha) 38 | numParents = commit.parents.length 39 | offset = reserve.indexOf(branch) 40 | routes = [] 41 | 42 | if numParents is 1 43 | if branches[commit.parents[0]]? 44 | # create branch 45 | for b, i in reserve[offset + 1..] 46 | routes.push [i + offset + 1, i + offset + 1 - 1, b] 47 | for b, i in reserve[...offset] 48 | routes.push [i, i, b] 49 | remove(reserve, branch) 50 | routes.push([offset, reserve.indexOf(branches[commit.parents[0]]), branch]) 51 | else 52 | # straight 53 | for b, i in reserve 54 | routes.push [i, i, b] 55 | branches[commit.parents[0]] = branch 56 | else if numParents is 2 57 | # merge branch 58 | branches[commit.parents[0]] = branch 59 | for b, i in reserve 60 | routes.push [i, i, b] 61 | otherBranch = getBranch(commit.parents[1]) 62 | routes.push([offset, reserve.indexOf(otherBranch), otherBranch]) 63 | 64 | node = Node(commit.sha, offset, branch, routes) 65 | nodes.push(node) 66 | 67 | nodes 68 | 69 | remove = (list, item) -> 70 | list.splice(list.indexOf(item), 1) 71 | list 72 | 73 | Node = (sha, offset, branch, routes) -> 74 | [sha, [offset, branch], routes] 75 | 76 | module.exports = generateGraphData 77 | -------------------------------------------------------------------------------- /src/svg-path-data.coffee: -------------------------------------------------------------------------------- 1 | # a canvas like api for building an svg path data attribute 2 | class SVGPathData 3 | constructor: -> 4 | @commands = [] 5 | toString: -> 6 | @commands.join(' ') 7 | moveTo: (x,y) -> 8 | @commands.push "M #{x},#{y}" 9 | lineTo: (x,y) -> 10 | @commands.push "L #{x},#{y}" 11 | closePath: (x,y) -> 12 | @commands.push "Z" 13 | bezierCurveTo: (cp1x, cp1y, cp2x, cp2y, x, y) -> 14 | @commands.push "C #{cp1x}, #{cp1y}, #{cp2x}, #{cp2y}, #{x}, #{y}" 15 | quadraticCurveTo: (cp1x, cp1y, x, y) -> 16 | @commands.push "Q #{cp1x}, #{cp1y}, #{x}, #{y}" 17 | 18 | module.exports = SVGPathData --------------------------------------------------------------------------------