257 | (Shift-Enter to compile and run.)
258 | Compile and run after each keystroke
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 | `
268 |
269 | var helloWorld = []byte(`package main
270 |
271 | import (
272 | "math/rand"
273 | "os"
274 | "time"
275 | "github.com/ajstarks/svgo"
276 | )
277 |
278 | func rn(n int) int { return rand.Intn(n) }
279 |
280 | func main() {
281 | canvas := svg.New(os.Stdout)
282 | width := 500
283 | height := 500
284 | nstars := 250
285 | style := "font-size:48pt;fill:white;text-anchor:middle"
286 |
287 | rand.Seed(time.Now().Unix())
288 | canvas.Start(width, height)
289 | canvas.Rect(0,0,width,height)
290 | for i := 0; i < nstars; i++ {
291 | canvas.Circle(rn(width), rn(height), rn(3), "fill:white")
292 | }
293 | canvas.Circle(width/2, height, width/2, "fill:rgb(77, 117, 232)")
294 | canvas.Text(width/2, height*4/5, "hello, world", style)
295 | canvas.End()
296 | }`)
297 |
298 | var helloWorldFloat = []byte(`
299 | package main
300 |
301 | import (
302 | "math/rand"
303 | "os"
304 | "time"
305 | "github.com/ajstarks/svgo/float"
306 | )
307 |
308 | func rn(n float64) float64 { return rand.Float64() * n }
309 |
310 | func main() {
311 | canvas := svg.New(os.Stdout)
312 | width := 500.0
313 | height := 500.0
314 | nstars := 250
315 | style := "font-size:48pt;fill:white;text-anchor:middle"
316 |
317 | rand.Seed(time.Now().Unix())
318 | canvas.Start(width, height)
319 | canvas.Rect(0,0,width,height)
320 | for i := 0; i < nstars; i++ {
321 | canvas.Circle(rn(width), rn(height), rn(3), "fill:white")
322 | }
323 | canvas.Circle(width/2, height, width/2, "fill:rgb(77, 117, 232)")
324 | canvas.Text(width/2, height*4/5, "hello, world", style)
325 | canvas.End()
326 | }`)
327 |
--------------------------------------------------------------------------------
/svgplot/svgplot.go:
--------------------------------------------------------------------------------
1 | //svgplot -- plot data (a stream of x,y coordinates)
2 | // +build !appengine
3 |
4 | package main
5 |
6 | import (
7 | "flag"
8 | "fmt"
9 | "io"
10 | "math"
11 | "os"
12 |
13 | "github.com/ajstarks/svgo"
14 | )
15 |
16 | // rawdata defines data as float64 x,y coordinates
17 | type rawdata struct {
18 | x float64
19 | y float64
20 | }
21 |
22 | type options map[string]bool
23 | type attributes map[string]string
24 | type measures map[string]int
25 |
26 | // plotset defines plot metadata
27 | type plotset struct {
28 | opt options
29 | attr attributes
30 | size measures
31 | }
32 |
33 | var (
34 | canvas = svg.New(os.Stdout)
35 | plotopt = options{}
36 | plotattr = attributes{}
37 | plotnum = measures{}
38 | ps = plotset{plotopt, plotattr, plotnum}
39 | plotw, ploth, plotc, gwidth, gheight, gutter, beginx, beginy int
40 | )
41 |
42 | const (
43 | globalfmt = "font-family:%s;font-size:%dpt;stroke-width:%dpx"
44 | linestyle = "fill:none;stroke:"
45 | linefmt = "fill:none;stroke:%s"
46 | barfmt = linefmt + ";stroke-width:%dpx"
47 | ticfmt = "stroke:rgb(200,200,200);stroke-width:1px"
48 | labelfmt = ticfmt + ";text-anchor:end;fill:black"
49 | textfmt = "stroke:none;baseline-shift:-33.3%"
50 | smallint = -(1 << 30)
51 | )
52 |
53 | // init initializes command flags and sets default options
54 | func init() {
55 |
56 | // boolean options
57 | showx := flag.Bool("showx", false, "show the xaxis")
58 | showy := flag.Bool("showy", false, "show the yaxis")
59 | showbar := flag.Bool("showbar", false, "show data bars")
60 | area := flag.Bool("area", false, "area chart")
61 | connect := flag.Bool("connect", true, "connect data points")
62 | showdot := flag.Bool("showdot", false, "show dots")
63 | showbg := flag.Bool("showbg", true, "show the background color")
64 | showfile := flag.Bool("showfile", false, "show the filename")
65 | sameplot := flag.Bool("sameplot", false, "plot on the same frame")
66 |
67 | // attributes
68 | bgcolor := flag.String("bgcolor", "rgb(240,240,240)", "plot background color")
69 | barcolor := flag.String("barcolor", "gray", "bar color")
70 | dotcolor := flag.String("dotcolor", "black", "dot color")
71 | linecolor := flag.String("linecolor", "gray", "line color")
72 | areacolor := flag.String("areacolor", "gray", "area color")
73 | font := flag.String("font", "Calibri,sans", "font")
74 | labelcolor := flag.String("labelcolor", "black", "label color")
75 | plotlabel := flag.String("label", "", "plot label")
76 |
77 | // sizes
78 | dotsize := flag.Int("dotsize", 2, "dot size")
79 | linesize := flag.Int("linesize", 2, "line size")
80 | barsize := flag.Int("barsize", 2, "bar size")
81 | fontsize := flag.Int("fontsize", 11, "font size")
82 | xinterval := flag.Int("xint", 10, "x axis interval")
83 | yinterval := flag.Int("yint", 4, "y axis interval")
84 | ymin := flag.Int("ymin", smallint, "y minimum")
85 | ymax := flag.Int("ymax", smallint, "y maximum")
86 |
87 | // meta options
88 | flag.IntVar(&beginx, "bx", 100, "initial x")
89 | flag.IntVar(&beginy, "by", 50, "initial y")
90 | flag.IntVar(&plotw, "pw", 500, "plot width")
91 | flag.IntVar(&ploth, "ph", 500, "plot height")
92 | flag.IntVar(&plotc, "pc", 2, "plot columns")
93 | flag.IntVar(&gutter, "gutter", ploth/10, "gutter")
94 | flag.IntVar(&gwidth, "width", 1024, "canvas width")
95 | flag.IntVar(&gheight, "height", 768, "canvas height")
96 |
97 | flag.Parse()
98 |
99 | // fill in the plotset -- all options, attributes, and sizes
100 | plotopt["showx"] = *showx
101 | plotopt["showy"] = *showy
102 | plotopt["showbar"] = *showbar
103 | plotopt["area"] = *area
104 | plotopt["connect"] = *connect
105 | plotopt["showdot"] = *showdot
106 | plotopt["showbg"] = *showbg
107 | plotopt["showfile"] = *showfile
108 | plotopt["sameplot"] = *sameplot
109 |
110 | plotattr["bgcolor"] = *bgcolor
111 | plotattr["barcolor"] = *barcolor
112 | plotattr["linecolor"] = *linecolor
113 | plotattr["dotcolor"] = *dotcolor
114 | plotattr["areacolor"] = *areacolor
115 | plotattr["font"] = *font
116 | plotattr["label"] = *plotlabel
117 | plotattr["labelcolor"] = *labelcolor
118 |
119 | plotnum["dotsize"] = *dotsize
120 | plotnum["linesize"] = *linesize
121 | plotnum["fontsize"] = *fontsize
122 | plotnum["xinterval"] = *xinterval
123 | plotnum["yinterval"] = *yinterval
124 | plotnum["barsize"] = *barsize
125 | plotnum["ymin"] = *ymin
126 | plotnum["ymax"] = *ymax
127 | }
128 |
129 | // fmap maps world data to document coordinates
130 | func fmap(value float64, low1 float64, high1 float64, low2 float64, high2 float64) float64 {
131 | return low2 + (high2-low2)*(value-low1)/(high1-low1)
132 | }
133 |
134 | // doplot opens a file and makes a plot
135 | func doplot(x, y int, location string) {
136 | var f *os.File
137 | var err error
138 | if len(location) > 0 {
139 | if plotopt["showfile"] {
140 | plotattr["label"] = location
141 | }
142 | f, err = os.Open(location)
143 | } else {
144 | f = os.Stdin
145 | }
146 | if err != nil {
147 | fmt.Fprintf(os.Stderr, "%v\n", err)
148 | return
149 | }
150 | nd, data := readxy(f)
151 | f.Close()
152 |
153 | if nd >= 2 {
154 | plot(x, y, plotw, ploth, ps, data)
155 | }
156 | }
157 |
158 | // plot places a plot at the specified location with the specified dimemsions
159 | // usinng the specified settings, using the specified data
160 | func plot(x, y, w, h int, settings plotset, d []rawdata) {
161 | nd := len(d)
162 | if nd < 2 {
163 | fmt.Fprintf(os.Stderr, "%d is not enough points to plot\n", len(d))
164 | return
165 | }
166 | // Compute the minima and maxima
167 | maxx, minx := d[0].x, d[0].x
168 | maxy, miny := d[0].y, d[0].y
169 | for _, v := range d {
170 |
171 | if v.x > maxx {
172 | maxx = v.x
173 | }
174 | if v.y > maxy {
175 | maxy = v.y
176 | }
177 | if v.x < minx {
178 | minx = v.x
179 | }
180 | if v.y < miny {
181 | miny = v.y
182 | }
183 | }
184 |
185 | if settings.size["ymin"] != smallint {
186 | miny = float64(settings.size["ymin"])
187 | }
188 | if settings.size["ymax"] != smallint {
189 | maxy = float64(settings.size["ymax"])
190 | }
191 | // Prepare for a area or line chart by allocating
192 | // polygon coordinates; for the hrizon plot, you need two extra coordinates
193 | // for the extrema.
194 | needpoly := settings.opt["area"] || settings.opt["connect"]
195 | var xpoly, ypoly []int
196 | if needpoly {
197 | xpoly = make([]int, nd+2)
198 | ypoly = make([]int, nd+2)
199 | // preload the extrema of the polygon,
200 | // the bottom left and bottom right of the plot's rectangle
201 | xpoly[0] = x
202 | ypoly[0] = y + h
203 | xpoly[nd+1] = x + w
204 | ypoly[nd+1] = y + h
205 | }
206 | // Draw the plot's bounding rectangle
207 | if settings.opt["showbg"] && !settings.opt["sameplot"] {
208 | canvas.Rect(x, y, w, h, "fill:"+settings.attr["bgcolor"])
209 | }
210 | // Loop through the data, drawing items as specified
211 | spacer := 10
212 | canvas.Gstyle(fmt.Sprintf(globalfmt,
213 | settings.attr["font"], settings.size["fontsize"], settings.size["linesize"]))
214 |
215 | for i, v := range d {
216 | xp := int(fmap(v.x, minx, maxx, float64(x), float64(x+w)))
217 | yp := int(fmap(v.y, miny, maxy, float64(y), float64(y-h)))
218 |
219 | if needpoly {
220 | xpoly[i+1] = xp
221 | ypoly[i+1] = yp + h
222 | }
223 | if settings.opt["showbar"] {
224 | canvas.Line(xp, yp+h, xp, y+h,
225 | fmt.Sprintf(barfmt, settings.attr["barcolor"], settings.size["barsize"]))
226 | }
227 | if settings.opt["showdot"] {
228 | canvas.Circle(xp, yp+h, settings.size["dotsize"], "fill:"+settings.attr["dotcolor"])
229 | }
230 | if settings.opt["showx"] {
231 | if i%settings.size["xinterval"] == 0 {
232 | canvas.Text(xp, (y+h)+(spacer*2), fmt.Sprintf("%d", int(v.x)), "text-anchor:middle")
233 | canvas.Line(xp, (y + h), xp, (y+h)+spacer, ticfmt)
234 | }
235 | }
236 | }
237 | // Done constructing the points for the area or line plots, display them in one shot
238 | if settings.opt["area"] {
239 | canvas.Polygon(xpoly, ypoly, "fill:"+settings.attr["areacolor"])
240 | }
241 |
242 | if settings.opt["connect"] {
243 | canvas.Polyline(xpoly[1:nd+1], ypoly[1:nd+1], linestyle+settings.attr["linecolor"])
244 | }
245 | // Put on the y axis labels, if specified
246 | if settings.opt["showy"] {
247 | bot := math.Floor(miny)
248 | top := math.Ceil(maxy)
249 | yrange := top - bot
250 | interval := yrange / float64(settings.size["yinterval"])
251 | canvas.Gstyle(labelfmt)
252 | for yax := bot; yax <= top; yax += interval {
253 | yaxp := fmap(yax, bot, top, float64(y), float64(y-h))
254 | canvas.Text(x-spacer, int(yaxp)+h, fmt.Sprintf("%.1f", yax), textfmt)
255 | canvas.Line(x-spacer, int(yaxp)+h, x, int(yaxp)+h)
256 | }
257 | canvas.Gend()
258 | }
259 | // Finally, tack on the label, if specified
260 | if len(settings.attr["label"]) > 0 {
261 | canvas.Text(x, y+spacer, settings.attr["label"], "font-size:120%;fill:"+settings.attr["labelcolor"])
262 | }
263 |
264 | canvas.Gend()
265 | }
266 |
267 | // readxy reads coordinates (x,y float64 values) from a io.Reader
268 | func readxy(f io.Reader) (int, []rawdata) {
269 | var (
270 | r rawdata
271 | err error
272 | n, nf int
273 | )
274 | data := make([]rawdata, 1)
275 | for ; err == nil; n++ {
276 | if n > 0 {
277 | data = append(data, r)
278 | }
279 | nf, err = fmt.Fscan(f, &data[n].x, &data[n].y)
280 | if nf != 2 {
281 | continue
282 | }
283 | }
284 | return n - 1, data[0 : n-1]
285 | }
286 |
287 | // plotgrid places plots on a grid, governed by a number of columns.
288 | func plotgrid(x, y int, files []string) {
289 | px := x
290 | for i, f := range files {
291 | if i > 0 && i%plotc == 0 && !plotopt["sameplot"] {
292 | px = x
293 | y += (ploth + gutter)
294 | }
295 | doplot(px, y, f)
296 | if !plotopt["sameplot"] {
297 | px += (plotw + gutter)
298 | }
299 | }
300 | }
301 |
302 | // main plots data from specified files or standard input in a
303 | // grid where plotc specifies the number of columns.
304 | func main() {
305 | canvas.Start(gwidth, gheight)
306 | canvas.Rect(0, 0, gwidth, gheight, "fill:white")
307 | filenames := flag.Args()
308 | if len(filenames) == 0 {
309 | doplot(beginx, beginy, "")
310 | } else {
311 | plotgrid(beginx, beginy, filenames)
312 | }
313 | canvas.End()
314 | }
315 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Creative Commons Attribution 4.0 International Public License
2 |
3 | By exercising the Licensed Rights (defined below), You accept and agree to
4 | be bound by the terms and conditions of this Creative Commons Attribution
5 | 4.0 International Public License ("Public License"). To the extent this
6 | Public License may be interpreted as a contract, You are granted the
7 | Licensed Rights in consideration of Your acceptance of these terms and
8 | conditions, and the Licensor grants You such rights in consideration
9 | of benefits the Licensor receives from making the Licensed Material
10 | available under these terms and conditions.
11 |
12 | Section 1 – Definitions.
13 |
14 | Adapted Material means material subject to Copyright and Similar Rights
15 | that is derived from or based upon the Licensed Material and in which
16 | the Licensed Material is translated, altered, arranged, transformed, or
17 | otherwise modified in a manner requiring permission under the Copyright
18 | and Similar Rights held by the Licensor. For purposes of this Public
19 | License, where the Licensed Material is a musical work, performance,
20 | or sound recording, Adapted Material is always produced where the
21 | Licensed Material is synched in timed relation with a moving image.
22 | Adapter's License means the license You apply to Your Copyright and
23 | Similar Rights in Your contributions to Adapted Material in accordance
24 | with the terms and conditions of this Public License. Copyright and
25 | Similar Rights means copyright and/or similar rights closely related to
26 | copyright including, without limitation, performance, broadcast, sound
27 | recording, and Sui Generis Database Rights, without regard to how the
28 | rights are labeled or categorized. For purposes of this Public License,
29 | the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar
30 | Rights. Effective Technological Measures means those measures that,
31 | in the absence of proper authority, may not be circumvented under laws
32 | fulfilling obligations under Article 11 of the WIPO Copyright Treaty
33 | adopted on December 20, 1996, and/or similar international agreements.
34 | Exceptions and Limitations means fair use, fair dealing, and/or any other
35 | exception or limitation to Copyright and Similar Rights that applies to
36 | Your use of the Licensed Material. Licensed Material means the artistic
37 | or literary work, database, or other material to which the Licensor
38 | applied this Public License. Licensed Rights means the rights granted
39 | to You subject to the terms and conditions of this Public License, which
40 | are limited to all Copyright and Similar Rights that apply to Your use
41 | of the Licensed Material and that the Licensor has authority to license.
42 | Licensor means the individual(s) or entity(ies) granting rights under
43 | this Public License. Share means to provide material to the public by
44 | any means or process that requires permission under the Licensed Rights,
45 | such as reproduction, public display, public performance, distribution,
46 | dissemination, communication, or importation, and to make material
47 | available to the public including in ways that members of the public
48 | may access the material from a place and at a time individually chosen
49 | by them. Sui Generis Database Rights means rights other than copyright
50 | resulting from Directive 96/9/EC of the European Parliament and of the
51 | Council of 11 March 1996 on the legal protection of databases, as amended
52 | and/or succeeded, as well as other essentially equivalent rights anywhere
53 | in the world. You means the individual or entity exercising the Licensed
54 | Rights under this Public License. Your has a corresponding meaning.
55 | Section 2 – Scope.
56 |
57 | License grant. Subject to the terms and conditions of this Public
58 | License, the Licensor hereby grants You a worldwide, royalty-free,
59 | non-sublicensable, non-exclusive, irrevocable license to exercise the
60 | Licensed Rights in the Licensed Material to: reproduce and Share the
61 | Licensed Material, in whole or in part; and produce, reproduce, and
62 | Share Adapted Material. Exceptions and Limitations. For the avoidance
63 | of doubt, where Exceptions and Limitations apply to Your use, this
64 | Public License does not apply, and You do not need to comply with
65 | its terms and conditions. Term. The term of this Public License is
66 | specified in Section 6(a). Media and formats; technical modifications
67 | allowed. The Licensor authorizes You to exercise the Licensed Rights in
68 | all media and formats whether now known or hereafter created, and to make
69 | technical modifications necessary to do so. The Licensor waives and/or
70 | agrees not to assert any right or authority to forbid You from making
71 | technical modifications necessary to exercise the Licensed Rights,
72 | including technical modifications necessary to circumvent Effective
73 | Technological Measures. For purposes of this Public License, simply making
74 | modifications authorized by this Section 2(a)(4) never produces Adapted
75 | Material. Downstream recipients. Offer from the Licensor – Licensed
76 | Material. Every recipient of the Licensed Material automatically receives
77 | an offer from the Licensor to exercise the Licensed Rights under the terms
78 | and conditions of this Public License. No downstream restrictions. You
79 | may not offer or impose any additional or different terms or conditions
80 | on, or apply any Effective Technological Measures to, the Licensed
81 | Material if doing so restricts exercise of the Licensed Rights by any
82 | recipient of the Licensed Material. No endorsement. Nothing in this
83 | Public License constitutes or may be construed as permission to assert
84 | or imply that You are, or that Your use of the Licensed Material is,
85 | connected with, or sponsored, endorsed, or granted official status by,
86 | the Licensor or others designated to receive attribution as provided in
87 | Section 3(a)(1)(A)(i). Other rights.
88 |
89 | Moral rights, such as the right of integrity, are not licensed under
90 | this Public License, nor are publicity, privacy, and/or other similar
91 | personality rights; however, to the extent possible, the Licensor waives
92 | and/or agrees not to assert any such rights held by the Licensor to the
93 | limited extent necessary to allow You to exercise the Licensed Rights, but
94 | not otherwise. Patent and trademark rights are not licensed under this
95 | Public License. To the extent possible, the Licensor waives any right
96 | to collect royalties from You for the exercise of the Licensed Rights,
97 | whether directly or through a collecting society under any voluntary or
98 | waivable statutory or compulsory licensing scheme. In all other cases
99 | the Licensor expressly reserves any right to collect such royalties.
100 | Section 3 – License Conditions.
101 |
102 | Your exercise of the Licensed Rights is expressly made subject to the
103 | following conditions.
104 |
105 | Attribution.
106 |
107 | If You Share the Licensed Material (including in modified form), You must:
108 |
109 | retain the following if it is supplied by the Licensor with the Licensed
110 | Material: identification of the creator(s) of the Licensed Material and
111 | any others designated to receive attribution, in any reasonable manner
112 | requested by the Licensor (including by pseudonym if designated); a
113 | copyright notice; a notice that refers to this Public License; a notice
114 | that refers to the disclaimer of warranties; a URI or hyperlink to the
115 | Licensed Material to the extent reasonably practicable; indicate if You
116 | modified the Licensed Material and retain an indication of any previous
117 | modifications; and indicate the Licensed Material is licensed under this
118 | Public License, and include the text of, or the URI or hyperlink to,
119 | this Public License. You may satisfy the conditions in Section 3(a)(1)
120 | in any reasonable manner based on the medium, means, and context in which
121 | You Share the Licensed Material. For example, it may be reasonable to
122 | satisfy the conditions by providing a URI or hyperlink to a resource
123 | that includes the required information. If requested by the Licensor,
124 | You must remove any of the information required by Section 3(a)(1)(A)
125 | to the extent reasonably practicable. If You Share Adapted Material You
126 | produce, the Adapter's License You apply must not prevent recipients of
127 | the Adapted Material from complying with this Public License. Section 4
128 | – Sui Generis Database Rights.
129 |
130 | Where the Licensed Rights include Sui Generis Database Rights that apply
131 | to Your use of the Licensed Material:
132 |
133 | for the avoidance of doubt, Section 2(a)(1) grants You the right to
134 | extract, reuse, reproduce, and Share all or a substantial portion of the
135 | contents of the database; if You include all or a substantial portion of
136 | the database contents in a database in which You have Sui Generis Database
137 | Rights, then the database in which You have Sui Generis Database Rights
138 | (but not its individual contents) is Adapted Material; and You must comply
139 | with the conditions in Section 3(a) if You Share all or a substantial
140 | portion of the contents of the database. For the avoidance of doubt,
141 | this Section 4 supplements and does not replace Your obligations under
142 | this Public License where the Licensed Rights include other Copyright and
143 | Similar Rights. Section 5 – Disclaimer of Warranties and Limitation
144 | of Liability.
145 |
146 | Unless otherwise separately undertaken by the Licensor, to the
147 | extent possible, the Licensor offers the Licensed Material as-is and
148 | as-available, and makes no representations or warranties of any kind
149 | concerning the Licensed Material, whether express, implied, statutory,
150 | or other. This includes, without limitation, warranties of title,
151 | merchantability, fitness for a particular purpose, non-infringement,
152 | absence of latent or other defects, accuracy, or the presence or absence
153 | of errors, whether or not known or discoverable. Where disclaimers of
154 | warranties are not allowed in full or in part, this disclaimer may not
155 | apply to You. To the extent possible, in no event will the Licensor
156 | be liable to You on any legal theory (including, without limitation,
157 | negligence) or otherwise for any direct, special, indirect, incidental,
158 | consequential, punitive, exemplary, or other losses, costs, expenses,
159 | or damages arising out of this Public License or use of the Licensed
160 | Material, even if the Licensor has been advised of the possibility of
161 | such losses, costs, expenses, or damages. Where a limitation of liability
162 | is not allowed in full or in part, this limitation may not apply to You.
163 | The disclaimer of warranties and limitation of liability provided above
164 | shall be interpreted in a manner that, to the extent possible, most
165 | closely approximates an absolute disclaimer and waiver of all liability.
166 | Section 6 – Term and Termination.
167 |
168 | This Public License applies for the term of the Copyright and Similar
169 | Rights licensed here. However, if You fail to comply with this
170 | Public License, then Your rights under this Public License terminate
171 | automatically. Where Your right to use the Licensed Material has
172 | terminated under Section 6(a), it reinstates:
173 |
174 | automatically as of the date the violation is cured, provided it is
175 | cured within 30 days of Your discovery of the violation; or upon express
176 | reinstatement by the Licensor. For the avoidance of doubt, this Section
177 | 6(b) does not affect any right the Licensor may have to seek remedies
178 | for Your violations of this Public License. For the avoidance of doubt,
179 | the Licensor may also offer the Licensed Material under separate terms
180 | or conditions or stop distributing the Licensed Material at any time;
181 | however, doing so will not terminate this Public License. Sections 1,
182 | 5, 6, 7, and 8 survive termination of this Public License. Section 7
183 | – Other Terms and Conditions.
184 |
185 | The Licensor shall not be bound by any additional or different terms or
186 | conditions communicated by You unless expressly agreed. Any arrangements,
187 | understandings, or agreements regarding the Licensed Material not stated
188 | herein are separate from and independent of the terms and conditions of
189 | this Public License. Section 8 – Interpretation.
190 |
191 | For the avoidance of doubt, this Public License does not, and shall not be
192 | interpreted to, reduce, limit, restrict, or impose conditions on any use
193 | of the Licensed Material that could lawfully be made without permission
194 | under this Public License. To the extent possible, if any provision of
195 | this Public License is deemed unenforceable, it shall be automatically
196 | reformed to the minimum extent necessary to make it enforceable. If
197 | the provision cannot be reformed, it shall be severed from this Public
198 | License without affecting the enforceability of the remaining terms
199 | and conditions. No term or condition of this Public License will be
200 | waived and no failure to comply consented to unless expressly agreed
201 | to by the Licensor. Nothing in this Public License constitutes or may
202 | be interpreted as a limitation upon, or waiver of, any privileges and
203 | immunities that apply to the Licensor or You, including from the legal
204 | processes of any jurisdiction or authority. Creative Commons is not
205 | a party to its public licenses. Notwithstanding, Creative Commons may
206 | elect to apply one of its public licenses to material it publishes and
207 | in those instances will be considered the “Licensor.” The text of
208 | the Creative Commons public licenses is dedicated to the public domain
209 | under the CC0 Public Domain Dedication. Except for the limited purpose of
210 | indicating that material is shared under a Creative Commons public license
211 | or as otherwise permitted by the Creative Commons policies published at
212 | creativecommons.org/policies, Creative Commons does not authorize the
213 | use of the trademark “Creative Commons” or any other trademark or
214 | logo of Creative Commons without its prior written consent including,
215 | without limitation, in connection with any unauthorized modifications
216 | to any of its public licenses or any other arrangements, understandings,
217 | or agreements concerning use of licensed material. For the avoidance of
218 | doubt, this paragraph does not form part of the public licenses.
219 |
220 | Creative Commons may be contacted at creativecommons.org.
221 |
--------------------------------------------------------------------------------
/barchart/barchart.go:
--------------------------------------------------------------------------------
1 | // barchart - bar chart
2 | package main
3 |
4 | import (
5 | "encoding/xml"
6 | "flag"
7 | "fmt"
8 | "io"
9 | "math"
10 | "os"
11 | "strconv"
12 | "strings"
13 |
14 | "github.com/ajstarks/svgo"
15 | )
16 |
17 | var (
18 | width, height, fontsize, barheight, gutter, cornerRadius, labelimit int
19 | bgcolor, barcolor, title, inbar, valformat string
20 | showtitle, showdata, showgrid, showscale, endtitle, trace, stick bool
21 | )
22 |
23 | const (
24 | gstyle = "font-family:Calibri,sans-serif;font-size:%dpx"
25 | borderstyle = "stroke:lightgray;stroke-width:1px"
26 | scalestyle = "text-anchor:middle;font-size:75%"
27 | btitlestyle = "font-style:italic;font-size:150%;text-anchor:"
28 | notestyle = "font-style:italic;text-anchor:"
29 | datastyle = "text-anchor:end;fill:"
30 | titlestyle = "text-anchor:start;font-size:300%"
31 | labelstyle = "fill:black;baseline-shift:-25%"
32 | )
33 |
34 | // a Barchart Defintion
35 | //
36 | // This is a note
37 | // More expository text
38 | //
39 | //
40 | //
41 | //
42 | //
43 | //
44 |
45 | type Barchart struct {
46 | Top int `xml:"top,attr"`
47 | Left int `xml:"left,attr"`
48 | Right int `xml:"right,attr"`
49 | Title string `xml:"title,attr"`
50 | Bdata []bdata `xml:"bdata"`
51 | Note []note `xml:"note"`
52 | }
53 |
54 | type bdata struct {
55 | Title string `xml:"title,attr"`
56 | Scale string `xml:"scale,attr"`
57 | Color string `xml:"color,attr"`
58 | Unit string `xml:"unit,attr"`
59 | Showdata bool `xml:"showdata,attr"`
60 | Showgrid bool `xml:"showgrid,attr"`
61 | Samebar bool `xml:"samebar,attr"`
62 | Bitem []bitem `xml:"bitem"`
63 | Bstack []bstack `xml:"bstack"`
64 | Note []note `xml:"note"`
65 | }
66 |
67 | type bitem struct {
68 | Name string `xml:"name,attr"`
69 | Value float64 `xml:"value,attr"`
70 | Color string `xml:"color,attr"`
71 | Samebar bool `xml:"samebar,attr"`
72 | }
73 |
74 | type bstack struct {
75 | Name string `xml:"name,attr"`
76 | Value string `xml:"value,attr"`
77 | Color string `xml:"color,attr"`
78 | }
79 |
80 | type note struct {
81 | Text string `xml:",chardata"`
82 | }
83 |
84 | // dobc does file i/o
85 | func dobc(location string, s *svg.SVG) {
86 | var f *os.File
87 | var err error
88 | if len(location) > 0 {
89 | f, err = os.Open(location)
90 | } else {
91 | f = os.Stdin
92 | }
93 | if err == nil {
94 | readbc(f, s)
95 | f.Close()
96 | } else {
97 | fmt.Fprintf(os.Stderr, "%v\n", err)
98 | os.Exit(1)
99 | }
100 | }
101 |
102 | // readbc reads and parses the XML specification
103 | func readbc(r io.Reader, s *svg.SVG) {
104 | var bc Barchart
105 | if err := xml.NewDecoder(r).Decode(&bc); err == nil {
106 | drawbc(bc, s)
107 | } else {
108 | fmt.Fprintf(os.Stderr, "%v\n", err)
109 | os.Exit(1)
110 | }
111 | }
112 |
113 | // drawbc draws the bar chart
114 | func drawbc(bg Barchart, canvas *svg.SVG) {
115 |
116 | if bg.Left == 0 {
117 | bg.Left = 250
118 | }
119 | if bg.Right == 0 {
120 | bg.Right = 50
121 | }
122 | if bg.Top == 0 {
123 | bg.Top = 50
124 | }
125 | if len(title) > 0 {
126 | bg.Title = title
127 | }
128 | labelimit = bg.Left / 8
129 | cr := cornerRadius
130 | maxwidth := width - (bg.Left + bg.Right)
131 | x := bg.Left
132 | y := bg.Top
133 | sep := 10
134 | color := barcolor
135 | scfmt := "%v"
136 | canvas.Title(bg.Title)
137 |
138 | // for each bdata element...
139 | for _, b := range bg.Bdata {
140 | if trace {
141 | fmt.Fprintf(os.Stderr, "# %s\n", b.Title)
142 | }
143 | // overide the color if specified
144 | if len(b.Color) > 0 {
145 | color = b.Color
146 | } else {
147 | color = barcolor
148 | }
149 | // extract the scale data from the XML attributes
150 | // if not specified, compute the scale factors
151 | sc := strings.Split(b.Scale, ",")
152 | var scalemin, scalemax, scaleincr float64
153 |
154 | switch {
155 | case len(sc) != 3:
156 | if len(b.Bitem) > 0 {
157 | scalemin, scalemax, scaleincr = scalevalues(b.Bitem)
158 | }
159 | if len(b.Bstack) > 0 {
160 | scalemin, scalemax, scaleincr = scalestack(b.Bstack)
161 | }
162 | case len(sc) == 3:
163 | var scerr error
164 | scalemin, scerr = strconv.ParseFloat(sc[0], 64)
165 | if scerr != nil {
166 | scalemin = 0
167 | }
168 | scalemax, scerr = strconv.ParseFloat(sc[1], 64)
169 | if scerr != nil {
170 | scalemax = 100
171 | }
172 | scaleincr, scerr = strconv.ParseFloat(sc[2], 64)
173 | if scerr != nil {
174 | scaleincr = 20
175 | }
176 | default:
177 | scalemin, scalemax, scaleincr = 0, 100, 20
178 | }
179 | // label the graph
180 | if trace {
181 | println("label")
182 | }
183 | canvas.Text(x, y, b.Title, btitlestyle+anchor())
184 |
185 | y += sep * 2
186 | chartop := y
187 |
188 | // draw the data items
189 | canvas.Gstyle(datastyle + color)
190 |
191 | // stacked bars
192 | for _, stack := range b.Bstack {
193 | if trace {
194 | fmt.Fprintf(os.Stderr, "%s~%s\n", stack.Value, stack.Name)
195 | }
196 | stackdata := stackvalues(stack.Value)
197 | if len(stackdata) < 1 {
198 | continue
199 | }
200 | sx := x
201 | canvas.Text(x-sep, y+barheight/2, textlimit(stack.Name, labelimit), labelstyle)
202 | barop := colorange(1.0, 0.3, len(stackdata))
203 | for ns, sd := range stackdata {
204 | dw := vmap(sd, scalemin, scalemax, 0, float64(maxwidth))
205 | if len(stack.Color) > 0 {
206 | canvas.Roundrect(sx, y, int(dw), barheight, cr, cr, fmt.Sprintf("fill:%s;fill-opacity:%.2f", stack.Color, barop[ns]))
207 | } else {
208 | canvas.Roundrect(sx, y, int(dw), barheight, cr, cr, fmt.Sprintf("fill-opacity:%.2f", barop[ns]))
209 | }
210 |
211 | if (showdata || b.Showdata) && sd > 0 {
212 | var valuestyle = "fill-opacity:1;font-style:italic;font-size:75%;text-anchor:middle;baseline-shift:-25%;"
213 | var ditem string
214 | var datax int
215 | if len(b.Unit) > 0 {
216 | ditem = fmt.Sprintf(valformat+"%s", sd, b.Unit)
217 | } else {
218 | ditem = fmt.Sprintf(valformat, sd)
219 | }
220 | if len(inbar) > 0 {
221 | valuestyle += inbar
222 | } else {
223 | valuestyle += "fill:black"
224 | }
225 | datax = sx + int(dw)/2
226 | canvas.Text(datax, y+barheight/2, ditem, valuestyle)
227 | }
228 | sx += int(dw)
229 | }
230 | y += barheight + gutter
231 | }
232 |
233 | // plain bars
234 | for _, d := range b.Bitem {
235 | if trace {
236 | fmt.Fprintf(os.Stderr, "%.2f~%s\n", d.Value, d.Name)
237 | }
238 | canvas.Text(x-sep, y+barheight/2, textlimit(d.Name, labelimit), labelstyle)
239 | dw := vmap(d.Value, scalemin, scalemax, 0, float64(maxwidth))
240 | var barop float64
241 | if b.Samebar {
242 | barop = 0.3
243 | } else {
244 | barop = 1.0
245 | }
246 |
247 | if stick {
248 | makestick(x, y, int(dw), d.Color, canvas)
249 | } else {
250 | makebar(x, y, int(dw), barheight, cr, d.Color, barop, canvas)
251 | }
252 |
253 | if showdata || b.Showdata {
254 | var valuestyle = "fill-opacity:1;font-style:italic;font-size:75%;text-anchor:start;baseline-shift:-25%;"
255 | var ditem string
256 | var datax int
257 | if len(b.Unit) > 0 {
258 | ditem = fmt.Sprintf(valformat+"%s", d.Value, b.Unit)
259 | } else {
260 | ditem = fmt.Sprintf(valformat, d.Value)
261 | }
262 | if len(inbar) > 0 {
263 | valuestyle += inbar
264 | datax = x + fontsize/2
265 | } else {
266 | valuestyle += "fill:black"
267 | datax = x + int(dw) + fontsize/2
268 | }
269 | canvas.Text(datax, y+barheight/2, ditem, valuestyle)
270 | }
271 | if !d.Samebar {
272 | y += barheight + gutter
273 | }
274 | }
275 | canvas.Gend()
276 |
277 | // draw the scale and borders
278 | chartbot := y + gutter
279 | if showgrid || b.Showgrid {
280 | canvas.Line(x, chartop, x+maxwidth, chartop, borderstyle) // top border
281 | canvas.Line(x, chartbot-gutter, x+maxwidth, chartbot-gutter, borderstyle) // bottom border
282 | }
283 | if showscale {
284 | if scaleincr < 1 {
285 | scfmt = "%.1f"
286 | } else {
287 | scfmt = "%0.f"
288 | }
289 | canvas.Gstyle(scalestyle)
290 | if trace {
291 | println(scalemin, scalemax, scaleincr)
292 | }
293 | if scaleincr > 0 && scalemin < scalemax {
294 | for sc := scalemin; sc <= scalemax; sc += scaleincr {
295 | scx := vmap(sc, scalemin, scalemax, 0, float64(maxwidth))
296 | canvas.Text(x+int(scx), chartbot+fontsize, fmt.Sprintf(scfmt, sc))
297 | if showgrid || b.Showgrid {
298 | canvas.Line(x+int(scx), chartbot, x+int(scx), chartop, borderstyle) // grid line
299 | }
300 | }
301 | }
302 | canvas.Gend()
303 | }
304 |
305 | // apply the note if present
306 | if len(b.Note) > 0 {
307 | canvas.Gstyle(notestyle + anchor())
308 | y += fontsize * 2
309 | leading := 3
310 | for _, note := range b.Note {
311 | canvas.Text(bg.Left, y, note.Text)
312 | y += fontsize + leading
313 | }
314 | canvas.Gend()
315 | }
316 | y += sep * 7 // advance vertically for the next chart
317 | }
318 | // if requested, place the title below the last chart
319 | if showtitle && len(bg.Title) > 0 {
320 | y += fontsize * 2
321 | canvas.Text(bg.Left, y, bg.Title, titlestyle)
322 | }
323 | // apply overall note if present
324 | if len(bg.Note) > 0 {
325 | canvas.Gstyle(notestyle + anchor())
326 | y += fontsize * 2
327 | leading := 3
328 | for _, note := range bg.Note {
329 | canvas.Text(bg.Left, y, note.Text)
330 | y += fontsize + leading
331 | }
332 | canvas.Gend()
333 | }
334 | }
335 |
336 | // nakebar draws the rectangle to represent the value
337 | func makebar(x, y, w, h, radius int, color string, op float64, canvas *svg.SVG) {
338 | if len(color) > 0 {
339 | canvas.Roundrect(x, y, w, h, radius, radius, fmt.Sprintf("fill:%s;fill-opacity:%.2f", color, op))
340 | } else {
341 | canvas.Roundrect(x, y, w, barheight, radius, radius, fmt.Sprintf("fill-opacity:%.2f", op))
342 | }
343 | }
344 |
345 | // makestick draws a dotted line ending with a circle to represent the value
346 | func makestick(x, y, w int, color string, canvas *svg.SVG) {
347 | y += fontsize * 60 / 100
348 | canvas.Line(x, y, x+w, y, "stroke-width:2;stroke-dasharray:3;stroke:rgb(170,170,170)")
349 | if len(color) > 0 {
350 | canvas.Circle(x+w, y, fontsize/4, "fill:"+color)
351 | } else {
352 | canvas.Circle(x+w, y, fontsize/4)
353 | }
354 | }
355 |
356 | func anchor() string {
357 | if endtitle {
358 | return "end"
359 | }
360 | return "start"
361 | }
362 |
363 | // vmap maps one interval to another
364 | func vmap(value float64, low1 float64, high1 float64, low2 float64, high2 float64) float64 {
365 | return low2 + (high2-low2)*(value-low1)/(high1-low1)
366 | }
367 |
368 | // maxitem finds the maxima is a collection of bar items
369 | func maxitem(data []bitem) float64 {
370 | if len(data) == 0 {
371 | return 100
372 | }
373 | max := -math.SmallestNonzeroFloat64
374 | for _, d := range data {
375 | if d.Value > max {
376 | max = d.Value
377 | }
378 | }
379 | return max
380 | }
381 |
382 | // maxstack finds the maxima is a stack of bars
383 | func maxstack(stacks []bstack) float64 {
384 | if len(stacks) == 0 {
385 | return 100
386 | }
387 | max := -math.SmallestNonzeroFloat64
388 | for _, s := range stacks {
389 | sv := stackvalues(s.Value)
390 | sum := 0.0
391 | for _, d := range sv {
392 | sum += d
393 | }
394 | if sum > max {
395 | max = sum
396 | }
397 | }
398 | return max
399 | }
400 |
401 | // scale values returns the min, max, increment from a set of bar items
402 | func scalevalues(data []bitem) (float64, float64, float64) {
403 | var m, max, increment float64
404 | rui := 5
405 | m = maxitem(data)
406 | max = roundup(m, 100)
407 | if max > 2 {
408 | increment = roundup(max/float64(rui), 10)
409 | } else {
410 | increment = 0.4
411 | }
412 | return 0, max, increment
413 | }
414 |
415 | // scalestack returns the min, max, increment from a stack of bars
416 | func scalestack(data []bstack) (float64, float64, float64) {
417 | var m, max, increment float64
418 | rui := 5
419 | m = maxstack(data)
420 | max = roundup(m, 100)
421 | if max > 2 {
422 | increment = roundup(max/float64(rui), 10)
423 | } else {
424 | increment = 0.4
425 | }
426 | return 0, max, increment
427 | }
428 |
429 | // roundup rounds a floating point number up
430 | func roundup(n float64, m int64) float64 {
431 | i := int64(n)
432 | if i <= 2 {
433 | return 2
434 | }
435 | for ; i%m != 0; i++ {
436 | }
437 | return float64(i)
438 | }
439 |
440 | // stack value returns the values from the value string of a stack
441 | func stackvalues(s string) []float64 {
442 | v := strings.Split(s, "/")
443 | if len(v) <= 0 {
444 | return nil
445 | }
446 | vals := make([]float64, len(v))
447 | for i, x := range v {
448 | f, err := strconv.ParseFloat(x, 64)
449 | if err != nil {
450 | vals[i] = 0
451 | } else {
452 | vals[i] = f
453 | }
454 | }
455 | return vals
456 | }
457 |
458 | // colorange evenly distributes opacity across a range of values
459 | func colorange(start, end float64, n int) []float64 {
460 | v := make([]float64, n)
461 | v[0] = start
462 | v[n-1] = end
463 | if n == 2 {
464 | return v
465 | }
466 | incr := (end - start) / float64(n-1)
467 | for i := 1; i < n-1; i++ {
468 | v[i] = v[i-1] + incr
469 | }
470 | return v
471 | }
472 |
473 | // textlimit returns an elided string
474 | func textlimit(s string, n int) string {
475 | l := len(s)
476 | if l <= n {
477 | return s
478 | }
479 |
480 | return s[0:n-3] + "..."
481 | }
482 |
483 | // init sets up the command flags
484 | func init() {
485 | flag.StringVar(&bgcolor, "bg", "white", "background color")
486 | flag.StringVar(&barcolor, "bc", "rgb(200,200,200)", "bar color")
487 | flag.StringVar(&valformat, "vfmt", "%v", "value format")
488 | flag.IntVar(&width, "w", 1024, "width")
489 | flag.IntVar(&height, "h", 800, "height")
490 | flag.IntVar(&barheight, "bh", 20, "bar height")
491 | flag.IntVar(&gutter, "g", 5, "gutter")
492 | flag.IntVar(&cornerRadius, "cr", 0, "corner radius")
493 | flag.IntVar(&fontsize, "f", 18, "fontsize (px)")
494 | flag.BoolVar(&showscale, "showscale", false, "show scale")
495 | flag.BoolVar(&showgrid, "showgrid", false, "show grid")
496 | flag.BoolVar(&showdata, "showdata", true, "show data values")
497 | flag.BoolVar(&showtitle, "showtitle", false, "show title")
498 | flag.BoolVar(&endtitle, "endtitle", false, "align title to the end")
499 | flag.BoolVar(&trace, "trace", false, "show name/value pairs")
500 | flag.BoolVar(&stick, "stick", false, "use ball and stick")
501 | flag.StringVar(&inbar, "inbar", "", "data in bar format")
502 | flag.StringVar(&title, "t", "", "title")
503 | }
504 |
505 | // for every input file (or stdin), draw a bar graph
506 | // as specified by command flags
507 | func main() {
508 | flag.Parse()
509 | canvas := svg.New(os.Stdout)
510 | canvas.Start(width, height)
511 | canvas.Rect(0, 0, width, height, "fill:"+bgcolor)
512 | canvas.Gstyle(fmt.Sprintf(gstyle, fontsize))
513 | if len(flag.Args()) == 0 {
514 | dobc("", canvas)
515 | } else {
516 | for _, f := range flag.Args() {
517 | dobc(f, canvas)
518 | }
519 | }
520 | canvas.Gend()
521 | canvas.End()
522 | }
523 |
--------------------------------------------------------------------------------