11import React , { Component } from 'react'
22import PropTypes from 'prop-types'
3+ import roundVertices from './helpers/roundVertices'
34
4- const pathData = ( ) => {
5- return {
5+ const pdFactory = halfRowHeight => ( ) => {
6+ const obj = {
67 data : [ ] ,
7- moveTo ( x , y ) {
8- this . data . push ( `M${ x } ,${ y } ` )
9- return this
10- } ,
11- lineTo ( x , y ) {
12- this . data . push ( `L${ x } ,${ y } ` )
8+ push ( x , y , isMerged ) {
9+ if ( this . data . length === 0 ) {
10+ this . data . push ( [ x , y ] )
11+ return this
12+ }
13+
14+ const lastPos = this . data [ this . data . length - 1 ]
15+ if ( lastPos [ 0 ] !== x ) {
16+ if ( isMerged ) { // prefer go straight first, then switch lane
17+ this . data . push ( [ lastPos [ 0 ] , lastPos [ 1 ] - halfRowHeight ] )
18+ this . data . push ( [ x , y ] )
19+ } else { // prefer switch lane first, then go straight
20+ this . data . push ( [ x , lastPos [ 1 ] - halfRowHeight ] )
21+ this . data . push ( [ x , y ] )
22+ }
23+ } else {
24+ this . data . push ( [ x , y ] )
25+ }
26+
27+ const length = this . data . length
28+
29+ if ( length >= 3 && this . data [ length - 1 ] [ 0 ] === this . data [ length - 2 ] [ 0 ] === this . data [ length - 3 ] [ 0 ] ) {
30+ const last = this . data . pop ( )
31+ this . data . pop ( ) // pluck the second to last pos, it's useless
32+ this . data . push ( last )
33+ }
1334 return this
1435 } ,
36+
1537 value ( ) {
16- return this . data . join ( '' )
38+ return roundVertices ( this . data ) . reduce ( ( acc , point , index ) => {
39+ if ( index === 0 ) {
40+ acc += `M${ point [ 0 ] } ,${ point [ 1 ] } `
41+ } else if ( point . length === 2 ) {
42+ acc += `L${ point [ 0 ] } ,${ point [ 1 ] } `
43+ } else {
44+ const [ x_s , y_s ] = point [ 0 ] // startPoint
45+ const [ x_c , y_c ] = point [ 1 ] // ctrlPoint
46+ const [ x_e , y_e ] = point [ 2 ] // endPoint
47+
48+ acc += `L${ x_s } ,${ y_s } `
49+ acc += `C${ x_c } ,${ y_c } ,${ x_e } ,${ y_e } ,${ x_e } ,${ y_e } `
50+ }
51+
52+ return acc
53+ } , '' )
1754 }
1855 }
56+
57+ return obj
1958}
2059
2160class GitGraph extends Component {
22- constructor ( props ) {
23- super ( props )
24- this . commitsCount = 0
25- if ( props . commits && props . commits . length ) {
26- this . commitsCount = props . commits . length
27- }
61+ shouldComponentUpdate ( nextProps ) {
62+ return (
63+ ! this . props . commitsState ||
64+ this . props . commitsState !== nextProps . commitsState ||
65+ this . props . rowHeight !== nextProps . rowHeight ||
66+ this . props . colWidth !== nextProps . colWidth
67+ )
2868 }
2969
30- shouldComponentUpdate ( ) {
31- if ( this . commitsCount === this . props . commits . length ) {
32- return false
33- } else {
34- this . commitsCount = this . props . commits . length
35- return true
36- }
37- }
70+ posX = ( col ) => ( col + 1 ) * this . props . colWidth
71+ posY = ( row ) => ( row + 0.5 ) * this . props . rowHeight
3872
3973 render ( ) {
40- const { commits, circleRadius, colWidth, rowHeight } = this . props
41- const posX = col => ( col + 1 ) * colWidth
42- const posY = row => ( row + 1 ) * rowHeight - rowHeight / 2
43- const pathProps = { strokeWidth : 2 , fill : 'none' }
74+ const orphanage = new Map ( ) // a place to shelter children who haven't found their parents yet
75+
76+ const state = this . props . commitsState
77+ const getCol = state . getCol
78+
79+ const commits = Array . from ( state . commits . values ( ) )
80+ const { circleRadius, colWidth, rowHeight } = this . props
81+ const { posX, posY } = this
82+
83+ const pd = pdFactory ( rowHeight / 2 )
4484
4585 let pathsList = [ ]
4686 let circlesList = [ ]
4787 let maxCol = 0
88+
4889 commits . forEach ( ( commit , commitIndex ) => {
90+ if ( ! commit . isRoot ) {
91+ // register parent count of this commit
92+ orphanage . set ( commit . id , commit . parentIds . length )
93+ }
4994 maxCol = Math . max ( maxCol , commit . col )
5095
5196 const x = posX ( commit . col )
5297 const y = posY ( commitIndex )
5398
5499 // draw path from current commit to its children
55100 const paths = commit . children . map ( ( child ) => {
56- const childIndex = commits . indexOf ( child )
57- const pathKey = `p_${ commit . id } _${ child . id } `
58-
59- let d , strokeColor
60- // case 1: child on the same col, draw a straight line
61- if ( child . col === commit . col ) {
62- d = pathData ( )
63- . moveTo ( x , y )
64- . lineTo ( posX ( child . col ) , posY ( childIndex ) )
65- . value ( )
66- strokeColor = child . branch . color
101+ if ( orphanage . has ( child . id ) ) {
102+ const parentCount = orphanage . get ( child . id ) - 1
103+ if ( parentCount <= 0 ) {
104+ orphanage . delete ( child . id )
105+ } else {
106+ orphanage . set ( child . id , parentCount )
107+ }
67108 }
68- // case 2: child has one parent, that's a branch out
69- else if ( child . parentIds . length === 1 ) {
70- d = pathData ( )
71- . moveTo ( x , y )
72- . lineTo ( posX ( child . col ) , y - rowHeight / 2 )
73- . lineTo ( posX ( child . col ) , posY ( childIndex ) )
74- . value ( )
75- strokeColor = child . branch . color
109+
110+ const childIndex = commits . indexOf ( child )
111+ const pathKey = `p_${ commit . shortId } _${ child . shortId } `
112+
113+ let d , strokeColor , pathLaneId , isMerged = false
114+ switch ( commit . relationToChild ( child ) ) {
115+ case 'NORMAL' :
116+ strokeColor = child . lane . color
117+ pathLaneId = child . laneId
118+ break
119+ case 'DIVERGED' :
120+ strokeColor = child . lane . color
121+ pathLaneId = child . laneId
122+ break
123+ case 'MERGED' :
124+ strokeColor = commit . lane . color
125+ pathLaneId = commit . laneId
126+ isMerged = true
127+ break
76128 }
77- // case 3: child has more than one parent
78- else {
79- // case 3-1: if current commit is base of merge, that's a branch out, too
80- if ( commit . isBaseOfMerge ( child ) ) {
81- d = pathData ( )
82- . moveTo ( x , y )
83- . lineTo ( posX ( child . col ) , y - rowHeight / 2 )
84- . lineTo ( posX ( child . col ) , posY ( childIndex ) )
85- . value ( )
86- strokeColor = child . branch . color
87- }
88- // case 3-2: other than that, it's a merge
89- else {
90- d = pathData ( )
91- . moveTo ( x , y )
92- . lineTo ( x , posY ( childIndex ) + rowHeight / 2 )
93- . lineTo ( posX ( child . col ) , posY ( childIndex ) )
94- . value ( )
95- strokeColor = commit . branch . color
129+
130+ d = pd ( ) . push ( x , y )
131+
132+ for ( let i = commitIndex - 1 ; i >= childIndex ; i -- ) {
133+ let colAtIndex = getCol ( i , pathLaneId )
134+ if ( i > childIndex ) {
135+ d . push ( posX ( colAtIndex ) , posY ( i ) )
136+ } else {
137+ d . push ( posX ( child . col ) , posY ( childIndex ) , isMerged )
96138 }
97139 }
98140
99- return < path d = { d } id = { pathKey } key = { pathKey } stroke = { strokeColor } { ...pathProps } />
141+ d = d . value ( )
142+
143+ return (
144+ < path d = { d }
145+ stroke = { strokeColor }
146+ id = { pathKey }
147+ key = { pathKey }
148+ strokeWidth = '2'
149+ fill = 'none'
150+ />
151+ )
100152 } )
101153
102154 const circle = (
103155 < circle
104156 key = { `c_${ commit . id } ` }
105157 cx = { x } cy = { y } r = { circleRadius }
106- fill = { commit . branch . color }
158+ fill = { commit . lane . color }
107159 strokeWidth = '1'
108160 stroke = '#fff'
109161 /> )
@@ -112,6 +164,27 @@ class GitGraph extends Component {
112164 circlesList = circlesList . concat ( circle )
113165 } )
114166
167+ // render orphan pathes
168+ const orphans = Array . from ( orphanage . keys ( ) ) . map ( id => state . commits . get ( id ) )
169+ const lastIndex = commits . length
170+ const orphanPaths = orphans . map ( ( orphan ) => {
171+ const strokeColor = orphan . lane . color
172+ const pathLaneId = orphan . laneId
173+ const orphanIndex = orphan . index
174+
175+ let d = pd ( )
176+ d . push ( posX ( getCol ( lastIndex - 1 , pathLaneId ) ) , posY ( lastIndex ) )
177+ for ( let i = lastIndex - 1 ; i >= orphanIndex ; i -- ) {
178+ d . push ( posX ( getCol ( i , pathLaneId ) ) , posY ( i ) )
179+ }
180+
181+ d = d . value ( )
182+ const pathKey = `future_${ orphan . id } `
183+ return < path id = { pathKey } key = { pathKey } d = { d } stroke = { strokeColor } strokeWidth = '2' fill = 'none' />
184+ } )
185+
186+ // end render orphan pathes
187+ pathsList = pathsList . concat ( orphanPaths )
115188 const width = colWidth * ( maxCol + 2 )
116189 if ( typeof this . props . onWidthChange === 'function' ) this . props . onWidthChange ( width )
117190
@@ -124,13 +197,12 @@ class GitGraph extends Component {
124197}
125198
126199
127- const { string, number, arrayOf, shape, } = PropTypes
128- const branchType = shape ( { color : string . isRequired } )
200+ const { string, number, arrayOf, shape, object } = PropTypes
201+ const laneType = shape ( { color : string . isRequired } )
129202
130203const commitShapeConfig = {
131204 id : string . isRequired ,
132- col : number . isRequired ,
133- branch : branchType ,
205+ lane : laneType ,
134206}
135207
136208const commitType = shape ( {
@@ -139,7 +211,7 @@ const commitType = shape({
139211} )
140212
141213GitGraph . propTypes = {
142- commits : arrayOf ( commitType ) . isRequired ,
214+ commitsState : object . isRequired ,
143215 circleRadius : number . isRequired ,
144216 colWidth : number . isRequired ,
145217 rowHeight : number . isRequired ,
0 commit comments