Breaks out areas of concern into files; adds command-line filtering; fixes a height calculation bug.
4 files changed, 249 insertions(+), 183 deletions(-)

M cmd/orgchart/main.go
A => cmd/orgchart/person.go
M cmd/orgchart/rank.go
A => cmd/orgchart/svg.go
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;`)
+}