# HG changeset patch # User Sean E. Russell # Date 1428492572 14400 # Wed Apr 08 07:29:32 2015 -0400 # Node ID 171ae5e4b74c428b2d2e6e4c154e67fe3365b54d # Parent f5c54d312c853218a96596db3150b65f5fc69c87 Breaks out areas of concern into files; adds command-line filtering; fixes a height calculation bug. diff --git a/cmd/orgchart/main.go b/cmd/orgchart/main.go --- a/cmd/orgchart/main.go +++ b/cmd/orgchart/main.go @@ -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 @@ **************************************************************************/ 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 @@ 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 @@ 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 @@ } 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;`) -} diff --git a/cmd/orgchart/person.go b/cmd/orgchart/person.go new file mode 100644 --- /dev/null +++ b/cmd/orgchart/person.go @@ -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)} +} diff --git a/cmd/orgchart/rank.go b/cmd/orgchart/rank.go --- a/cmd/orgchart/rank.go +++ b/cmd/orgchart/rank.go @@ -64,10 +64,30 @@ } 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) diff --git a/cmd/orgchart/svg.go b/cmd/orgchart/svg.go new file mode 100644 --- /dev/null +++ b/cmd/orgchart/svg.go @@ -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;`) +}