diff --git a/README.md b/README.md
index 7ccedf9..8a9938a 100644
--- a/README.md
+++ b/README.md
@@ -39,10 +39,12 @@ func main() {
developFeature1.Note("xxが担当")
reviewDevelopFeature1 := g.Task("レビュー対応")
- developFeature2 := g.Task("feature2開発").Note("yyが担当")
+ developFeature2 := g.Task("feature2開発")
+ developFeature2.Note("yyが担当")
reviewDevelopFeature2 := g.Task("レビュー対応")
- prepareInfra := g.Task("インフラ準備").Note("zzが担当")
+ prepareInfra := g.Task("インフラ準備")
+ prepareInfra.Note("zzが担当")
test := g.Task("結合テスト")
release := g.Task("リリース")
@@ -53,7 +55,7 @@ func main() {
reviewDesign.Con(prepareInfra).Con(test)
test.Con(release).Con(finish)
- g.Done(design, reviewDesign, developFeature2, finish)
+ g.Done(design, reviewDesign, developFeature1, reviewDevelopFeature1, developFeature2)
uml, err := dag.UML()
if err != nil {
@@ -69,14 +71,14 @@ func main() {
rectangle "ゴール(目的)" as 1
usecase "設計" as 2 #DarkGray
usecase "レビュー対応" as 3 #DarkGray
-usecase "feature1開発" as 4
+usecase "feature1開発" as 4 #DarkGray
note left
xxが担当
end note
-usecase "レビュー対応" as 5
+usecase "レビュー対応" as 5 #DarkGray
usecase "結合テスト" as 9
usecase "リリース" as 10
-usecase "finish" as 11 #DarkGray
+usecase "finish" as 11
usecase "feature2開発" as 6 #DarkGray
note left
yyが担当
@@ -107,8 +109,104 @@ end note
![image](dag.svg)
+### Critical path
+
+1. `go run main.go > dag.pu`
+
+```go
+package main
+
+import (
+ "fmt"
+ "os"
+
+ g "github.com/ddddddO/gdag"
+)
+
+func main() {
+ var dag *g.Node = g.DAG("ゴール(目的)")
+
+ var design *g.Node = g.Task("設計").Hour(10)
+ reviewDesign := g.Task("レビュー対応").Hour(2)
+
+ developFeature1 := g.Task("feature1開発").Hour(20)
+ developFeature1.Note("xxが担当")
+ reviewDevelopFeature1 := g.Task("レビュー対応").Hour(1.5)
+
+ developFeature2 := g.Task("feature2開発").Hour(15)
+ developFeature2.Note("yyが担当")
+ reviewDevelopFeature2 := g.Task("レビュー対応").Hour(1.5)
+
+ prepareInfra := g.Task("インフラ準備").Hour(15)
+ prepareInfra.Note("zzが担当")
+
+ test := g.Task("結合テスト").Hour(20)
+ release := g.Task("リリース").Hour(2)
+ finish := g.Task("finish")
+
+ dag.Con(design).Con(reviewDesign).Con(developFeature1).Con(reviewDevelopFeature1).Con(test)
+ reviewDesign.Con(developFeature2).Con(reviewDevelopFeature2).Con(test)
+ reviewDesign.Con(prepareInfra).Con(test)
+ test.Con(release).Con(finish)
+
+ g.Done(design, reviewDesign, developFeature1, reviewDevelopFeature1, developFeature2)
+
+ // If you do not want to represent critical path, use `dag.UMLNoCritical()`.
+ uml, err := dag.UML()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println(uml)
+}
+```
+
+```
+@startuml
+rectangle "ゴール(目的)" as 1
+usecase "設計 (10.0h)" as 2 #DarkGray-Yellow
+usecase "レビュー対応 (2.0h)" as 3 #DarkGray-Yellow
+usecase "feature1開発 (20.0h)" as 4 #DarkGray-Yellow
+note left
+xxが担当
+end note
+usecase "レビュー対応 (1.5h)" as 5 #DarkGray-Yellow
+usecase "結合テスト (20.0h)" as 9 #Yellow
+usecase "リリース (2.0h)" as 10 #Yellow
+usecase "finish" as 11 #Yellow
+usecase "feature2開発 (15.0h)" as 6 #DarkGray
+note left
+yyが担当
+end note
+usecase "レビュー対応 (1.5h)" as 7
+usecase "インフラ準備 (15.0h)" as 8
+note left
+zzが担当
+end note
+
+1 --> 2
+2 --> 3
+3 --> 4
+4 --> 5
+5 --> 9
+9 --> 10
+10 --> 11
+3 --> 6
+6 --> 7
+7 --> 9
+3 --> 8
+8 --> 9
+
+@enduml
+```
+
+2. dag.pu to png or svg
+![image](dag_critical.svg)
+
## Mermaid
+※ Mermaid method does not support critical paths.
+
1. `go run main.go`
```go
@@ -124,22 +222,22 @@ import (
func main() {
var dag *g.Node = g.DAG("ゴール(目的)")
- var design *g.Node = g.Task("設計")
- reviewDesign := g.Task("レビュー対応")
+ var design *g.Node = g.Task("設計").Hour(10)
+ reviewDesign := g.Task("レビュー対応").Hour(2)
- developFeature1 := g.Task("feature1開発")
- developFeature1.Note("noop")
- reviewDevelopFeature1 := g.Task("レビュー対応")
+ developFeature1 := g.Task("feature1開発").Hour(20)
+ developFeature1.Note("xxが担当")
+ reviewDevelopFeature1 := g.Task("レビュー対応").Hour(1.5)
- developFeature2 := g.Task("feature2開発")
- developFeature2.Note("noop")
- reviewDevelopFeature2 := g.Task("レビュー対応")
+ developFeature2 := g.Task("feature2開発").Hour(15)
+ developFeature2.Note("yyが担当")
+ reviewDevelopFeature2 := g.Task("レビュー対応").Hour(1.5)
- prepareInfra := g.Task("インフラ準備")
- prepareInfra.Note("noop")
+ prepareInfra := g.Task("インフラ準備").Hour(15)
+ prepareInfra.Note("zzが担当")
- test := g.Task("結合テスト")
- release := g.Task("リリース")
+ test := g.Task("結合テスト").Hour(20)
+ release := g.Task("リリース").Hour(2)
finish := g.Task("finish")
dag.Con(design).Con(reviewDesign).Con(developFeature1).Con(reviewDevelopFeature1).Con(test)
@@ -147,7 +245,7 @@ func main() {
reviewDesign.Con(prepareInfra).Con(test)
test.Con(release).Con(finish)
- g.Done(design, reviewDesign, developFeature2, finish)
+ g.Done(design, reviewDesign, developFeature1, reviewDevelopFeature1, developFeature2)
mermaid, err := dag.Mermaid()
if err != nil {
@@ -156,23 +254,22 @@ func main() {
}
fmt.Println(mermaid)
}
-
```
```
graph TD
classDef doneColor fill:#868787
1("ゴール(目的)")
-2(["設計"]):::doneColor
-3(["レビュー対応"]):::doneColor
-4(["feature1開発"])
-5(["レビュー対応"])
-9(["結合テスト"])
-10(["リリース"])
-11(["finish"]):::doneColor
-6(["feature2開発"]):::doneColor
-7(["レビュー対応"])
-8(["インフラ準備"])
+2(["設計 (10.0h)"]):::doneColor
+3(["レビュー対応 (2.0h)"]):::doneColor
+4(["feature1開発 (20.0h)"]):::doneColor
+5(["レビュー対応 (1.5h)"]):::doneColor
+9(["結合テスト (20.0h)"])
+10(["リリース (2.0h)"])
+11(["finish"])
+6(["feature2開発 (15.0h)"]):::doneColor
+7(["レビュー対応 (1.5h)"])
+8(["インフラ準備 (15.0h)"])
1 --> 2
2 --> 3
@@ -194,16 +291,16 @@ classDef doneColor fill:#868787
graph TD
classDef doneColor fill:#868787
1("ゴール(目的)")
-2(["設計"]):::doneColor
-3(["レビュー対応"]):::doneColor
-4(["feature1開発"])
-5(["レビュー対応"])
-9(["結合テスト"])
-10(["リリース"])
-11(["finish"]):::doneColor
-6(["feature2開発"]):::doneColor
-7(["レビュー対応"])
-8(["インフラ準備"])
+2(["設計 (10.0h)"]):::doneColor
+3(["レビュー対応 (2.0h)"]):::doneColor
+4(["feature1開発 (20.0h)"]):::doneColor
+5(["レビュー対応 (1.5h)"]):::doneColor
+9(["結合テスト (20.0h)"])
+10(["リリース (2.0h)"])
+11(["finish"])
+6(["feature2開発 (15.0h)"]):::doneColor
+7(["レビュー対応 (1.5h)"])
+8(["インフラ準備 (15.0h)"])
1 --> 2
2 --> 3
@@ -219,7 +316,6 @@ classDef doneColor fill:#868787
8 --> 9
```
-
## CheckList
1. `go run main.go`
diff --git a/dag.svg b/dag.svg
index 5de6151..498c2ca 100644
--- a/dag.svg
+++ b/dag.svg
@@ -1,59 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/dag_critical.svg b/dag_critical.svg
new file mode 100644
index 0000000..12c0d40
--- /dev/null
+++ b/dag_critical.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/mermaid.go b/mermaid.go
index ea1013e..4704151 100644
--- a/mermaid.go
+++ b/mermaid.go
@@ -57,6 +57,9 @@ func (*mermaidGenerator) renderComponent(node *Node) string {
ret += s
case usecase:
s := fmt.Sprintf("%d([\"%s\"])", node.index, node.text)
+ if node.hour > 0 {
+ s = fmt.Sprintf("%d([\"%s (%.1fh)\"])", node.index, node.text, node.hour)
+ }
if len(node.colorMermaid) != 0 {
s += fmt.Sprintf(":::%s", node.colorMermaid)
}
diff --git a/node.go b/node.go
index 0432698..33e0724 100644
--- a/node.go
+++ b/node.go
@@ -1,15 +1,18 @@
package gdag
type Node struct {
- nodeType nodeType
- index int // mermaidの識別子としても利用する
- text string
- note string
+ nodeType nodeType
+ index int // mermaidの識別子としても利用する
+ text string
+ note string
+ hour float64 // 見積時間
+
color string // done: #DarkGray
colorMermaid string // done: doneColor
// parent *Node // TODO: 現状、中間ノードのためにおいてる
downstream []*Node
+ startPoint bool // レンダリング処理の最初の node ということ
}
type nodeType string
@@ -85,6 +88,11 @@ func (current *Node) Note(note string) *Node {
return current
}
+func (current *Node) Hour(hour float64) *Node {
+ current.hour = hour
+ return current
+}
+
func (current *Node) isDone() bool {
return current.color == colorDone
}
diff --git a/node_short.go b/node_short.go
index fba9686..3621830 100644
--- a/node_short.go
+++ b/node_short.go
@@ -19,3 +19,8 @@ func (upstream *Node) C(current *Node) *Node {
func (current *Node) N(note string) *Node {
return current.Note(note)
}
+
+// H is short name of Hour func.
+func (current *Node) H(hour float64) *Node {
+ return current.Hour(hour)
+}
diff --git a/plantuml.go b/plantuml.go
index b164ff8..f551f72 100644
--- a/plantuml.go
+++ b/plantuml.go
@@ -6,22 +6,104 @@ import (
// UML outputs dag PlantUML format.
func (start *Node) UML() (string, error) {
- ug := newUMLGenerator()
+ start.startPoint = true
+ cc := newCriticalPathCalculator()
+ ug := newUMLGenerator(cc.getCriticalPaths(start))
ret := "@startuml" + "\n"
ret += ug.generateComponents(start) + "\n"
ret += ug.generateRelations(start) + "\n"
ret += "@enduml"
+
+ start.startPoint = false
+ return ret, nil
+}
+
+// UMLNoCritical outputs dag PlantUML format that does not represent critical path.
+func (start *Node) UMLNoCritical() (string, error) {
+ start.startPoint = true
+ ug := newUMLGenerator(nil)
+
+ ret := "@startuml" + "\n"
+ ret += ug.generateComponents(start) + "\n"
+ ret += ug.generateRelations(start) + "\n"
+ ret += "@enduml"
+
+ start.startPoint = false
return ret, nil
}
+type criticalPath struct {
+ path map[int]struct{} // key は Node の index
+ sumHour float64
+}
+
+func (cp *criticalPath) contains(n *Node) bool {
+ _, ok := cp.path[n.index]
+ return ok
+}
+
+type criticalPathCalculator struct {
+ allPaths [][]*Node // start からすべてのパス
+}
+
+func newCriticalPathCalculator() *criticalPathCalculator {
+ return &criticalPathCalculator{}
+}
+
+func (cc *criticalPathCalculator) getCriticalPaths(start *Node) []*criticalPath {
+ cc.walk(start, []*Node{})
+
+ criticalPaths := []*criticalPath{}
+ for _, path := range cc.allPaths {
+ critical := &criticalPath{path: map[int]struct{}{}}
+ for _, n := range path {
+ critical.path[n.index] = struct{}{}
+ critical.sumHour += n.hour
+ }
+
+ if critical.sumHour == 0 {
+ continue
+ }
+ if len(criticalPaths) == 0 {
+ criticalPaths = append(criticalPaths, critical)
+ continue
+ }
+ if critical.sumHour == criticalPaths[0].sumHour {
+ criticalPaths = append(criticalPaths, critical)
+ continue
+ }
+ if critical.sumHour > criticalPaths[0].sumHour {
+ criticalPaths = []*criticalPath{critical}
+ continue
+ }
+ }
+
+ return criticalPaths
+}
+
+func (cc *criticalPathCalculator) walk(current *Node, path []*Node) {
+ path = append(path, current)
+
+ if len(current.downstream) == 0 {
+ cc.allPaths = append(cc.allPaths, path)
+ return
+ }
+
+ for _, n := range current.downstream {
+ cc.walk(n, path)
+ }
+}
+
type umlGenerator struct {
+ criticalPaths []*criticalPath
uniqueComponents map[int]struct{}
uniqueRelations map[string]struct{}
}
-func newUMLGenerator() *umlGenerator {
+func newUMLGenerator(criticalPaths []*criticalPath) *umlGenerator {
return ¨Generator{
+ criticalPaths: criticalPaths,
uniqueComponents: map[int]struct{}{},
uniqueRelations: map[string]struct{}{},
}
@@ -37,21 +119,33 @@ func (ug *umlGenerator) generateComponent(node *Node) string {
}
ug.uniqueComponents[node.index] = struct{}{}
- ret := (*umlGenerator)(nil).renderComponent(node)
+ ret := ug.renderComponent(node)
for _, d := range node.downstream {
ret += ug.generateComponent(d)
}
return ret
}
-func (*umlGenerator) renderComponent(node *Node) string {
+func (ug *umlGenerator) renderComponent(node *Node) string {
ret := ""
switch node.nodeType {
case rectangle, usecase:
s := fmt.Sprintf("%s \"%s\" as %d", node.nodeType, node.text, node.index)
+ if node.hour > 0 {
+ s = fmt.Sprintf("%s \"%s (%.1fh)\" as %d", node.nodeType, node.text, node.hour, node.index)
+ }
+
if len(node.color) != 0 {
s += fmt.Sprintf(" %s", node.color)
+ if ug.isCritical(node) && !node.isDAG() {
+ s += fmt.Sprintf("-%s", "Yellow")
+ }
+ } else {
+ if ug.isCritical(node) && !node.isDAG() {
+ s += fmt.Sprintf(" %s", "#Yellow")
+ }
}
+
s += "\n"
ret += s
}
@@ -61,6 +155,15 @@ func (*umlGenerator) renderComponent(node *Node) string {
return ret
}
+func (ug *umlGenerator) isCritical(current *Node) bool {
+ for _, cp := range ug.criticalPaths {
+ if cp.contains(current) {
+ return true
+ }
+ }
+ return false
+}
+
func (ug *umlGenerator) generateRelations(start *Node) string {
return ug.generateRelation(start, "")
}