# HG changeset patch # User Sean E. Russell # Date 1462023573 14400 # Sat Apr 30 09:39:33 2016 -0400 # Node ID 70e26d4c9a43008b71c4200b5dc3591004a946cd # Parent f7a32cf887db58e6586f04c51172936623600330 Adds CSS support and classes; breaking change. diff --git a/Makefile b/Makefile --- a/Makefile +++ b/Makefile @@ -4,6 +4,6 @@ -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" diff --git a/cmd/orgchart/highlightrules.go b/cmd/orgchart/highlightrules.go --- a/cmd/orgchart/highlightrules.go +++ b/cmd/orgchart/highlightrules.go @@ -18,20 +18,24 @@ // 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: -// ,, +// ,, // but this is not CSV -- commas are separators, and can't be escaped. func makeRuleSet(set []string) (Ruleset, error) { rv := make(Ruleset, len(set)) diff --git a/cmd/orgchart/main.go b/cmd/orgchart/main.go --- a/cmd/orgchart/main.go +++ b/cmd/orgchart/main.go @@ -16,6 +16,7 @@ "encoding/csv" "fmt" "io" + "io/ioutil" "os" "regexp" "strconv" @@ -32,15 +33,28 @@ 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: ,,. 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: ,.") 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 @@ } *sup = *sup && found } - renderSVG(os.Stdout, rs, *sup) + renderSVG(os.Stdout, rs, *sup, css) } /************************************************************************** diff --git a/cmd/orgchart/main_test.go b/cmd/orgchart/main_test.go --- a/cmd/orgchart/main_test.go +++ b/cmd/orgchart/main_test.go @@ -146,3 +146,27 @@ 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, "") +} diff --git a/cmd/orgchart/svg.go b/cmd/orgchart/svg.go --- a/cmd/orgchart/svg.go +++ b/cmd/orgchart/svg.go @@ -2,6 +2,7 @@ import ( "io" + "strconv" "github.com/ajstarks/svgo" ) @@ -57,12 +58,12 @@ } } -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 @@ 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 @@ 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 @@ 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 + "'" +} diff --git a/org.css b/org.css new file mode 100644 --- /dev/null +++ b/org.css @@ -0,0 +1,10 @@ +.contractor { + fill: yellow; +} +.convert { + stroke-dasharray: 5,5; + fill: orange; +} +.manager { + fill: lightgreen; +} diff --git a/vendor/manifest b/vendor/manifest new file mode 100644 --- /dev/null +++ b/vendor/manifest @@ -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