M cmd/orgchart/main.go +34 -182
@@ 1,14 1,18 @@
package main
+// TODO: Addl attrs in boxes (title, etc.)
+// TODO: Max-depth (e.g., show only X levels below anchor person)
+// TODO: Report-to (e.g., show anchor person's superior)
+// TODO: Dotted lines
+
import (
"encoding/csv"
"flag"
"fmt"
"io"
"os"
+ "regexp"
"strings"
-
- "github.com/ajstarks/svgo"
)
/**************************************************************************
@@ 34,6 38,7 @@ var pageWidth int
**************************************************************************/
func main() {
c := flag.String("c", "", "CSV input file")
+ o := flag.String("f", "", "Filter for these people's organization (regex)")
flag.Parse()
if *c == "" {
fmt.Println("CSV is required")
@@ 51,99 56,48 @@ func main() {
fmt.Println(e.Error())
os.Exit(1)
}
+ if *o != "" {
+ ps = filter(ps, *o)
+ }
rs := rank(ps)
renderSVG(os.Stdout, rs)
}
/**************************************************************************
- * Person & methods *
+ * Utility functions *
**************************************************************************/
-type Person struct {
- Name string
- Manager *Person
- Reports []*Person
- Contractor bool
-}
-// The index of this person in the person's manager's reports
-func (p *Person) Index() int {
- if p.Manager == nil {
- return 0
+// Filter organization based on names. The provided filter is interpreted as a
+// regex; anybody who is not this person, or in this person's organization, is
+// filtered out. If pat is not a parsable regex, an error is printed on STDOUT
+// and the input map is returned.
+func filter(ps map[string]*Person, pat string) map[string]*Person {
+ re, e := regexp.Compile(pat)
+ if e != nil {
+ fmt.Fprintf(os.Stderr, "bad regexp: %s", e.Error())
+ return ps
}
- for i, k := range p.Manager.Reports {
- if k == p {
- return i
+ rv := make(map[string]*Person)
+ for k, p := range ps {
+ if matches(p, re) {
+ rv[k] = p
}
}
- return 0
-}
-
-func (p *Person) SetManager(m *Person) *Person {
- p.Manager = m
- m.Reports = append(m.Reports, p)
- return p
-}
-
-func (p Person) String() string {
- var mn string
- if p.Manager != nil {
- mn = p.Manager.Name
- }
- rpts := make([]string, len(p.Reports))
- for _, rp := range p.Reports {
- rpts = append(rpts, rp.Name)
- }
- return fmt.Sprintf("%s -> %s, %+v", p.Name, mn, rpts)
+ return rv
}
-func (p Person) Width() int {
- // Width of children =
- // If all children are leaves, then boxW + gapH
- // Else, sum(map(width, children))
- allChildrenLeaves := true
- for _, p := range p.Reports {
- allChildrenLeaves = allChildrenLeaves && (len(p.Reports) == 0)
- if !allChildrenLeaves {
- break
- }
+// Return true if this person's name, or name in this person's upward
+// hierarchy, matches the regexp
+func matches(p *Person, re *regexp.Regexp) bool {
+ if p == nil {
+ return false
}
- if allChildrenLeaves {
- return boxW + gapW
+ if re.MatchString(p.Name) {
+ return true
}
- w := 0
- for _, r := range p.Reports {
- w += r.Width()
- }
- return w
-}
-
-func New(n string) *Person {
- return &Person{Name: n, Reports: make([]*Person, 0)}
+ return matches(p.Manager, re)
}
-/**************************************************************************
- * Box & methods *
- **************************************************************************/
-type Box struct {
- X, Y int
- Leaf bool
-}
-
-func (b Box) out() (int, int) {
- return b.X + boxW/2, b.Y + boxH
-}
-
-func (b Box) in() (int, int) {
- if b.Leaf {
- return b.X, b.Y + (boxH / 2)
- }
- return b.X + boxW/2, b.Y
-}
-
-/**************************************************************************
- * Utility functions *
- **************************************************************************/
-
// Parses a CSV data stream, producing a map of people indexed by the person
// name. Returns an error if a non-EOF read error is encountered
func makePeople(f io.Reader) (map[string]*Person, error) {
@@ 190,7 144,7 @@ func rank(ps map[string]*Person) Ranks {
if rs.contains(p) {
continue
}
- for p.Manager != nil {
+ for p.Manager != nil && ps[p.Manager.Name] != nil {
p = p.Manager
}
rs = addRecursively(p, rs, 0)
@@ 210,105 164,3 @@ func addRecursively(p *Person, rs Ranks,
}
return rs
}
-
-/**************************************************************************
- * SVG generating code *
- **************************************************************************/
-func renderSVG(out io.Writer, rs Ranks) {
- c := svg.New(out)
- pageWidth := rs.width()
- h := rs.height()
- c.Start(pageWidth, h)
- c.Filter("blur")
- c.FeGaussianBlur(svg.Filterspec{In: "SourceAlpha", Result: "blur"}, 5, 5)
- c.Fend()
- c.Filter("lineblur")
- c.FeGaussianBlur(svg.Filterspec{In: "SourceGraphic", Result: "blur"}, 1, 1)
- c.Fend()
- c.Def()
- c.Gid(_id)
- c.Rect(0, 0, boxW, boxH, "fill: inherit; stroke: black; stroke-width: 1;")
- c.Gend()
- c.DefEnd()
- defer c.End()
-
- b := make(map[string]Box)
- xoff := marginW
- for _, p := range rs[0] {
- drawOrg(c, xoff, marginH/2, p, b, false)
- xoff += p.Width()
- }
- for _, r := range rs {
- connectBoxes(c, r, b)
- }
-}
-
-func drawOrg(canvas *svg.SVG, xoffset, yoffset int, p *Person, boxes map[string]Box, leaf bool) {
- drawPerson(canvas, xoffset, yoffset, p.Index(), p, boxes, leaf)
- if len(p.Reports) > 0 {
- isLeaf := true
- for _, report := range p.Reports {
- isLeaf = isLeaf && (len(report.Reports) == 0)
- }
- yoffset += boxH + (gapH / 2)
- for _, q := range p.Reports {
- drawOrg(canvas, xoffset, yoffset, q, boxes, isLeaf)
- if isLeaf {
- yoffset += boxH + (gapH / 2)
- } else {
- xoffset += q.Width()
- }
- }
- }
-}
-
-func drawPerson(canvas *svg.SVG, x, y, ind int, person *Person, boxes map[string]Box, leaf bool) {
- if !leaf {
- x += (person.Width() - (boxW + (gapH / 2))) / 2
- }
- drawBox(canvas, x, y, person.Name, person.Contractor)
- boxes[person.Name] = Box{X: x, Y: y, Leaf: leaf}
-}
-
-func drawBox(c *svg.SVG, x, y int, n string, yn bool) {
- c.Use(x+2, y+3, _ref, `filter="url(#blur)"`)
- fill := "fill: white"
- if yn {
- fill = "fill: yellow"
- }
- c.Use(x, y, _ref, fill)
- toff := x + ((boxW - textPX) / 2) + textPX
- c.Text(toff, y+textPY, n, "text-anchor: middle;")
-}
-
-func connectBoxes(c *svg.SVG, ps Rank, bs map[string]Box) {
- for _, p := range ps {
- b0 := bs[p.Name]
- for _, r := range p.Reports {
- b1 := bs[r.Name]
- connect(c, b0, b1)
- }
- }
-}
-
-func connect(c *svg.SVG, f, t Box) {
- dim := 4
- if t.Leaf {
- dim++
- }
- xs := make([]int, dim)
- ys := make([]int, dim)
- xs[0], ys[0] = f.out()
- xs[dim-1], ys[dim-1] = t.in()
- midY := ys[0] + gapH/4
- xs[1], ys[1] = xs[0], midY
- if t.Leaf {
- midX := xs[4] - 10
- xs[2], ys[2] = midX, midY
- xs[3], ys[3] = midX, ys[4]
- } else {
- xs[2], ys[2] = xs[3], midY
- }
- c.Polyline(xs, ys, `filter="url(#lineblur)" style="fill: none; stroke: indigo; stroke-width: 2;"`)
- c.Polyline(xs, ys, `fill: none; stroke: indigo; stroke-width: 2;`)
-}
A => cmd/orgchart/person.go +69 -0
@@ 0,0 1,69 @@
+package main
+
+import "fmt"
+
+/**************************************************************************
+ * Person & methods *
+ **************************************************************************/
+type Person struct {
+ Name string
+ Manager *Person
+ Reports []*Person
+ Contractor bool
+}
+
+// The index of this person in the person's manager's reports
+func (p *Person) Index() int {
+ if p.Manager == nil {
+ return 0
+ }
+ for i, k := range p.Manager.Reports {
+ if k == p {
+ return i
+ }
+ }
+ return 0
+}
+
+func (p *Person) SetManager(m *Person) *Person {
+ p.Manager = m
+ m.Reports = append(m.Reports, p)
+ return p
+}
+
+func (p Person) String() string {
+ var mn string
+ if p.Manager != nil {
+ mn = p.Manager.Name
+ }
+ rpts := make([]string, len(p.Reports))
+ for _, rp := range p.Reports {
+ rpts = append(rpts, rp.Name)
+ }
+ return fmt.Sprintf("%s -> %s, %+v", p.Name, mn, rpts)
+}
+
+func (p Person) Width() int {
+ // Width of children =
+ // If all children are leaves, then boxW + gapH
+ // Else, sum(map(width, children))
+ allChildrenLeaves := true
+ for _, p := range p.Reports {
+ allChildrenLeaves = allChildrenLeaves && (len(p.Reports) == 0)
+ if !allChildrenLeaves {
+ break
+ }
+ }
+ if allChildrenLeaves {
+ return boxW + gapW
+ }
+ w := 0
+ for _, r := range p.Reports {
+ w += r.Width()
+ }
+ return w
+}
+
+func New(n string) *Person {
+ return &Person{Name: n, Reports: make([]*Person, 0)}
+}
M cmd/orgchart/rank.go +21 -1
@@ 64,10 64,30 @@ func (rs Ranks) width() int {
}
func (rs Ranks) height() int {
- h := (len(rs) * (boxH + gapH)) + (marginH * 2)
+ var idx int
+ switch {
+ case len(rs) == 0:
+ return 0
+ case len(rs) < 3:
+ idx = 0
+ default:
+ idx = len(rs) - 2
+ }
+ high := len(rs) + (maxReports(rs[idx]) - 1)
+ h := (high * boxH) + ((high - 1) * gapH) + (marginH * 2)
return h
}
+func maxReports(r Rank) int {
+ max := 0
+ for _, p := range r {
+ if len(p.Reports) > max {
+ max = len(p.Reports)
+ }
+ }
+ return max
+}
+
func (rs Ranks) Print(o io.Writer) {
for ri, r := range rs {
fmt.Fprintf(o, "Rank %d\n", ri)
A => cmd/orgchart/svg.go +125 -0
@@ 0,0 1,125 @@
+package main
+
+import (
+ "io"
+
+ "github.com/ajstarks/svgo"
+)
+
+/**************************************************************************
+ * SVG generating code *
+ **************************************************************************/
+type Box struct {
+ X, Y int
+ Leaf bool
+}
+
+func (b Box) out() (int, int) {
+ return b.X + boxW/2, b.Y + boxH
+}
+
+func (b Box) in() (int, int) {
+ if b.Leaf {
+ return b.X, b.Y + (boxH / 2)
+ }
+ return b.X + boxW/2, b.Y
+}
+
+func renderSVG(out io.Writer, rs Ranks) {
+ c := svg.New(out)
+ pageWidth := rs.width()
+ h := rs.height()
+ c.Start(pageWidth, h)
+ c.Filter("blur")
+ c.FeGaussianBlur(svg.Filterspec{In: "SourceAlpha", Result: "blur"}, 5, 5)
+ c.Fend()
+ c.Filter("lineblur")
+ c.FeGaussianBlur(svg.Filterspec{In: "SourceGraphic", Result: "blur"}, 1, 1)
+ c.Fend()
+ c.Def()
+ c.Gid(_id)
+ c.Rect(0, 0, boxW, boxH, "fill: inherit; stroke: black; stroke-width: 1;")
+ c.Gend()
+ c.DefEnd()
+ defer c.End()
+
+ b := make(map[string]Box)
+ xoff := marginW
+ for _, p := range rs[0] {
+ drawOrg(c, xoff, marginH/2, p, b, false)
+ xoff += p.Width()
+ }
+ for _, r := range rs {
+ connectBoxes(c, r, b)
+ }
+}
+
+func drawOrg(canvas *svg.SVG, xoffset, yoffset int, p *Person, boxes map[string]Box, leaf bool) {
+ drawPerson(canvas, xoffset, yoffset, p.Index(), p, boxes, leaf)
+ if len(p.Reports) > 0 {
+ isLeaf := true
+ for _, report := range p.Reports {
+ isLeaf = isLeaf && (len(report.Reports) == 0)
+ }
+ yoffset += boxH + (gapH / 2)
+ for _, q := range p.Reports {
+ drawOrg(canvas, xoffset, yoffset, q, boxes, isLeaf)
+ if isLeaf {
+ yoffset += boxH + (gapH / 2)
+ } else {
+ xoffset += q.Width()
+ }
+ }
+ }
+}
+
+func drawPerson(canvas *svg.SVG, x, y, ind int, person *Person, boxes map[string]Box, leaf bool) {
+ if !leaf {
+ x += (person.Width() - (boxW + (gapH / 2))) / 2
+ }
+ drawBox(canvas, x, y, person.Name, person.Contractor)
+ boxes[person.Name] = Box{X: x, Y: y, Leaf: leaf}
+}
+
+func drawBox(c *svg.SVG, x, y int, n string, yn bool) {
+ c.Use(x+2, y+3, _ref, `filter="url(#blur)"`)
+ fill := "fill: white"
+ if yn {
+ fill = "fill: yellow"
+ }
+ c.Use(x, y, _ref, fill)
+ toff := x + ((boxW - textPX) / 2) + textPX
+ c.Text(toff, y+textPY, n, "text-anchor: middle;")
+}
+
+func connectBoxes(c *svg.SVG, ps Rank, bs map[string]Box) {
+ for _, p := range ps {
+ b0 := bs[p.Name]
+ for _, r := range p.Reports {
+ b1 := bs[r.Name]
+ connect(c, b0, b1)
+ }
+ }
+}
+
+func connect(c *svg.SVG, f, t Box) {
+ dim := 4
+ if t.Leaf {
+ dim++
+ }
+ xs := make([]int, dim)
+ ys := make([]int, dim)
+ xs[0], ys[0] = f.out()
+ xs[dim-1], ys[dim-1] = t.in()
+ midY := ys[0] + gapH/4
+ xs[1], ys[1] = xs[0], midY
+ if t.Leaf {
+ midX := xs[4] - 10
+ xs[2], ys[2] = midX, midY
+ xs[3], ys[3] = midX, ys[4]
+ } else {
+ xs[2], ys[2] = xs[3], midY
+ }
+ c.Polyline(xs, ys, `filter="url(#lineblur)" style="fill: none; stroke: indigo; stroke-width: 2;"`)
+ c.Polyline(xs, ys, `fill: none; stroke: indigo; stroke-width: 2;`)
+}