M Makefile +3 -3
@@ 4,6 4,6 @@ all:
-t 3,4,5 \
-i "${DATA}" \
-s \
- -c "2,Contractor,fill:yellow" \
- -c "2,Convert,fill:orange" \
- -c "2,[^ ]Manager,fill:lightgreen" > "${WHO}.svg"
+ -c "3,TRUE,contractor" \
+ -c "4,TRUE,convert" \
+ -c "2,TRUE,manager" > "${WHO}.svg"
M cmd/orgchart/highlightrules.go +7 -3
@@ 18,20 18,24 @@ type Rule struct {
// Errors in processing (too few columns in input data, e.g.) are skipped
func (rs Ruleset) Class(cols []string) string {
+ rv := ""
for _, r := range rs {
if r.Col >= len(cols) {
// ERROR -- specified column does not exist. Skip
continue
}
if r.Match.MatchString(cols[r.Col]) {
- return r.Class
+ if len(rv) > 0 {
+ rv += " "
+ }
+ rv += r.Class
}
}
- return ""
+ return rv
}
// WARN: the strings are expected to be in format:
-// <col#>,<regexp>,<color>
+// <col#>,<regexp>,<class>
// but this is not CSV -- commas are separators, and can't be escaped.
func makeRuleSet(set []string) (Ruleset, error) {
rv := make(Ruleset, len(set))
M cmd/orgchart/main.go +19 -5
@@ 16,6 16,7 @@ import (
"encoding/csv"
"fmt"
"io"
+ "io/ioutil"
"os"
"regexp"
"strconv"
@@ 32,15 33,28 @@ func main() {
fltr := goopt.String([]string{"-f"}, "", "Filter for these people's organization (regex)")
coln := goopt.String([]string{"-t"}, "", "Display column numbers, indexed by 0, separated by commas; empty cells are omitted; badly formatted numbers are ignored")
sup := goopt.Flag([]string{"-s"}, []string{}, "Display superior; only used if results are filtered", "")
- hil := goopt.Strings([]string{"-c"}, "", "Coloring. Format: <col#>,<regexp>,<css>. Multiple allowed; first match for each line wins.")
+ hil := goopt.Strings([]string{"-c"}, "", "Column,ValueMatch,ClassName -- sets a CSS class based on a regexp match in a column")
+ cssfile := goopt.String([]string{"-y"}, "", "CSS file to embed")
//lin := goopt.Strings([]string{"-l"}, "", "Box stroke. Format: <col#>,<regexp>.")
goopt.Version = ""
goopt.Summary = "Organization chart generator"
goopt.Parse(nil)
if *csv == "" {
- fmt.Println("CSV is required")
- goopt.Usage()
- os.Exit(1)
+ if len(goopt.Args) == 0 {
+ fmt.Println("CSV is required")
+ goopt.Usage()
+ os.Exit(1)
+ }
+ *csv = goopt.Args[0]
+ }
+ var css string
+ if *cssfile != "" {
+ cssb, err := ioutil.ReadFile(*cssfile)
+ css = string(cssb)
+ if err != nil {
+ fmt.Println(err.Error())
+ os.Exit(1)
+ }
}
fin, err := os.Open(*csv)
defer fin.Close()
@@ 85,7 99,7 @@ func main() {
}
*sup = *sup && found
}
- renderSVG(os.Stdout, rs, *sup)
+ renderSVG(os.Stdout, rs, *sup, css)
}
/**************************************************************************
M cmd/orgchart/main_test.go +24 -0
@@ 146,3 146,27 @@ D2,C2B3`
setBoxH(rs)
assert.Equal(t, 450, height(rs))
}
+
+func TestClass(t *testing.T) {
+ s := `Person,Manager,Is Manager,Is Contractor
+X,,true,false
+Y,,false,true
+Z,,true,true
+W,,false,false`
+ b := bytes.NewBufferString(s)
+ rx, er := makeRuleSet([]string{"2,true,A", "3,true,B"})
+ if er != nil {
+ assert.Fail(t, er.Error())
+ }
+ ps, er := makePeople(b, []int{}, rx)
+ if er != nil {
+ assert.Fail(t, er.Error())
+ }
+ //for k, v := range ps {
+ // fmt.Printf("%s %#v\n", k, v)
+ //}
+ assert.Equal(t, ps["X"].Class, "A")
+ assert.Equal(t, ps["Y"].Class, "B")
+ assert.Equal(t, ps["Z"].Class, "A B")
+ assert.Equal(t, ps["W"].Class, "")
+}
M cmd/orgchart/svg.go +43 -12
@@ 2,6 2,7 @@ package main
import (
"io"
+ "strconv"
"github.com/ajstarks/svgo"
)
@@ 57,12 58,12 @@ func setBoxH(rs Ranks) {
}
}
-func renderSVG(out io.Writer, rs Ranks, sup bool) {
+func renderSVG(out io.Writer, rs Ranks, sup bool, css string) {
setBoxH(rs)
c := svg.New(out)
bgW = boxW + gapW
pageWidth := rs.width() * bgW
- h := height(rs)
+ h := height(rs, sup)
c.Start(pageWidth, h)
c.Filter("blur")
c.FeGaussianBlur(svg.Filterspec{In: "SourceAlpha", Result: "blur"}, 5, 5)
@@ 71,8 72,27 @@ func renderSVG(out io.Writer, rs Ranks,
c.FeGaussianBlur(svg.Filterspec{In: "SourceGraphic", Result: "blur"}, 1, 1)
c.Fend()
c.Def()
+ c.Style(`
+.person {
+ fill: inherit;
+ stroke: black;
+ stroke-width: 1;
+}
+.toptext {
+ text-anchor: middle;
+ font-size: smaller;
+}
+.nametext {
+ text-anchor: middle;
+ font-weight: bold;
+}
+.bottomtext {
+ text-anchor: middle;
+ font-size: smaller;
+}
+` + css)
c.Gid(_id)
- c.Rect(0, 0, boxW, boxH, "fill: inherit; stroke: black; stroke-width: 1;")
+ c.Rect(0, 0, boxW, boxH, "class='person'")
c.Gend()
c.Gid(_root_id)
c.Rect(0, 0, boxW, boxH, "fill: inherit; stroke: black; stroke-width: 5;")
@@ 154,26 174,30 @@ func drawBox(c *svg.SVG, x, y int, p *Pe
if root {
ref = _root_ref
}
- c.Use(x+2, y+3, ref, `filter="url(#blur)"`)
- fill := "fill: white"
if p.Class != "" {
- fill = p.Class
+ c.Use(x+2, y+3, ref, class(p), `filter="url(#blur)"`)
+ c.Use(x, y, ref, class(p))
+ } else {
+ c.Use(x+2, y+3, ref, `filter="url(#blur)"`)
+ c.Use(x, y, ref, "fill: white")
}
- c.Use(x, y, ref, fill)
toff := x + (boxW / 2)
toffY := y + textSize + textPY
if len(p.LineMask) > 0 && p.LineMask[0] && len(p.Lines) > 0 {
- c.Text(toff, y+textSize+textPY, p.Lines[0], "text-anchor: middle; font-size: smaller;")
+ c.Text(toff, y+textSize+textPY, p.Lines[0], "class='toptext'")
toffY += textSize + textPY
}
- c.Text(toff, toffY, p.Name, "text-anchor: middle; font-weight: bold;")
+ c.Text(toff, toffY, p.Name, "class='nametext'")
toffY += textSize + textPY
for i, k := range p.LineMask {
if i == 0 || i >= len(p.Lines) {
continue
}
- if k {
- c.Text(toff, toffY, p.Lines[i], "text-anchor: middle; font-size: smaller;")
+ if k && len(p.Lines[i]) > 0 {
+ txt := "class='bottomtext textrow"
+ txt += strconv.Itoa(i)
+ txt += "'"
+ c.Text(toff, toffY, p.Lines[i], txt)
toffY += textSize + textPY
}
}
@@ 214,7 238,14 @@ func width(p *Person) int {
return p.Width() * (boxW + gapW)
}
-func height(rs Ranks) int {
+func height(rs Ranks, s bool) int {
high := rs.height()
+ if s {
+ high += 1
+ }
return (high * boxH) + ((high - 1) * (gapH / 2)) + marginH
}
+
+func class(p *Person) string {
+ return "class='" + p.Class + "'"
+}
A => org.css +10 -0
@@ 0,0 1,10 @@
+.contractor {
+ fill: yellow;
+}
+.convert {
+ stroke-dasharray: 5,5;
+ fill: orange;
+}
+.manager {
+ fill: lightgreen;
+}
A => vendor/manifest +19 -0
@@ 0,0 1,19 @@
+{
+ "version": 0,
+ "dependencies": [
+ {
+ "importpath": "github.com/ajstarks/svgo",
+ "repository": "https://github.com/ajstarks/svgo",
+ "revision": "672fe547df4e49efc6db67a74391368bcb149b37",
+ "branch": "master",
+ "notests": true
+ },
+ {
+ "importpath": "honnef.co/go/structlayout",
+ "repository": "https://github.com/dominikh/go-structlayout",
+ "revision": "504fdf06ef5c0d83a29c594b3a169af68b9c2e14",
+ "branch": "master",
+ "notests": true
+ }
+ ]
+}
No newline at end of file