├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bar.go ├── box.go ├── chart.go ├── data.go ├── doc.go ├── example ├── bestof.png └── samplecharts.go ├── freefont ├── AUTHORS ├── CREDITS ├── ChangeLog ├── INSTALL ├── README └── sfd │ ├── FreeMono.ttf │ ├── FreeMonoBold.ttf │ ├── FreeMonoBoldOblique.ttf │ ├── FreeMonoOblique.ttf │ ├── FreeSans.ttf │ ├── FreeSansBold.ttf │ ├── FreeSansBoldOblique.ttf │ ├── FreeSansOblique.ttf │ ├── FreeSerif.ttf │ ├── FreeSerifBold.ttf │ ├── FreeSerifBoldItalic.ttf │ └── FreeSerifItalic.ttf ├── go.mod ├── go.sum ├── graphics.go ├── graphics_test.go ├── hist.go ├── imgg ├── font.go ├── image.go └── image_test.go ├── key.go ├── pie.go ├── scatter.go ├── stat.go ├── strip.go ├── style.go ├── style_test.go ├── svgg └── svg.go ├── time.go ├── time_test.go ├── txtg ├── buf.go └── text.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | x*.png 3 | x*.svg 4 | x*.txt 5 | example/example 6 | example/bestof.svg 7 | example/bestof.txt 8 | 9 | .idea 10 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Volker Dobler. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include $(GOROOT)/src/Make.inc 2 | 3 | 4 | TARG=github.com/vdobler/chart 5 | GOFILES=\ 6 | chart.go\ 7 | data.go\ 8 | util.go\ 9 | style.go\ 10 | key.go\ 11 | graphics.go\ 12 | stat.go\ 13 | time.go\ 14 | strip.go\ 15 | scatter.go\ 16 | box.go\ 17 | hist.go\ 18 | bar.go\ 19 | pie.go 20 | 21 | include $(GOROOT)/src/Make.pkg 22 | 23 | DRIVERS=\ 24 | svg\ 25 | txt\ 26 | image 27 | 28 | 29 | samplechart: samplecharts.go install drivers 30 | $(GC) -I. samplecharts.go 31 | $(LD) -L. -o samplecharts samplecharts.$(O) 32 | 33 | format: $(GOFILES) samplecharts.go 34 | gofmt -w $^ 35 | for d in $(DRIVERS); do (cd $$d; make format); done 36 | 37 | drivers: 38 | for d in $(DRIVERS); do (cd $$d; make install || exit 1) || exit 1; done 39 | 40 | CLEAN: 41 | make clean 42 | for d in $(DRIVERS); do (cd $$d; make clean); done -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Charts for Go 3 | ============= 4 | 5 | Basic charts in go. 6 | 7 | This package focuses more on autoscaling, error bars, 8 | and logarithmic plots than on beautifull or marketing 9 | ready charts. 10 | 11 | ## Examples 12 | 13 | ![Some nice charts](https://github.com/vdobler/chart/raw/master/example/bestof.png) 14 | 15 | 16 | ## Chart Types 17 | 18 | The following chart types are implemented: 19 | * Strip Charts 20 | * Scatter / Function-Plot Charts 21 | * Histograms 22 | * Bar and Categorical Bar Charts 23 | * Pie/Ring Charts 24 | * Boxplots 25 | 26 | ## Some Features 27 | * Axis can be linear, logarithmical, categorical or time/date axis. 28 | * Autoscaling with lots of options 29 | * Fine control of tics and labels 30 | 31 | ## Output / Graphic Formats 32 | 33 | Package chart itself provideds the charts/plots itself, the charts/plots 34 | can be output to different graphic drivers. Currently 35 | * txtg: ASCII art charts 36 | * svgg: scalable vector graphics (via github.com/ajstarks/svgo), and 37 | * imgg: Go image.RGBA (via code.google.com/p/draw2d/draw2d/ and code.google.com/p/freetype-go) 38 | are implemented. 39 | 40 | For a quick overview save as xbestof.{png,svg,txt} run 41 | ```bash 42 | $ example/example -best 43 | ``` 44 | A fuller overview can be generated by 45 | ```bash 46 | $ example/example -All 47 | ``` 48 | 49 | ## Quirks 50 | * Style handling (especialy colour) is a bit of a mess . 51 | * Text based charts are cute. But the general graphics would be much easier without. 52 | * Time handling code dates back to pre Go1, it should be reworked. 53 | 54 | 55 | -------------------------------------------------------------------------------- /bar.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | // "os" 7 | // "strings" 8 | ) 9 | 10 | // BarChart draws simple bar charts. 11 | // (Use CategoricalBarChart if your x axis is categorical, that is not numeric.) 12 | // 13 | // Stacking is on a "both bars have _identical_ x values" basis. 14 | type BarChart struct { 15 | XRange, YRange Range 16 | Title string // Title of the chart 17 | Key Key // Key/Legend 18 | Horizontal bool // Display as horizontal bars (unimplemented) 19 | Stacked bool // Display different data sets ontop of each other (default is side by side) 20 | ShowVal int // Display values: 0: don't show; 1: above bar, 2: centerd in bar; 3: at top of bar 21 | SameBarWidth bool // all data sets use the same (smalest of all data sets) bar width 22 | BarWidthFac float64 // if nonzero: scale determined bar width with this factor 23 | Options PlotOptions // visual apperance, nil to use DefaultOptions 24 | Data []BarChartData 25 | } 26 | 27 | // BarChartData encapsulates data sets in a bar chart. 28 | type BarChartData struct { 29 | Name string 30 | Style Style 31 | Samples []Point 32 | } 33 | 34 | // AddData adds the data to the chart. 35 | func (c *BarChart) AddData(name string, data []Point, style Style) { 36 | if len(c.Data) == 0 { 37 | c.XRange.init() 38 | c.YRange.init() 39 | } 40 | c.Data = append(c.Data, BarChartData{name, style, data}) 41 | for _, d := range data { 42 | c.XRange.autoscale(d.X) 43 | c.YRange.autoscale(d.Y) 44 | } 45 | 46 | if name != "" { 47 | c.Key.Entries = append(c.Key.Entries, KeyEntry{Style: style, Text: name, PlotStyle: PlotStyleBox}) 48 | } 49 | } 50 | 51 | // AddDataPair is a convenience method to add all the (x[i],y[i]) pairs to the chart. 52 | func (c *BarChart) AddDataPair(name string, x, y []float64, style Style) { 53 | n := imin(len(x), len(y)) 54 | data := make([]Point, n) 55 | for i := 0; i < n; i++ { 56 | data[i] = Point{X: x[i], Y: y[i]} 57 | } 58 | c.AddData(name, data, style) 59 | } 60 | 61 | func (c *BarChart) rescaleStackedY() { 62 | if !c.Stacked { 63 | return 64 | } 65 | 66 | // rescale y-axis 67 | highSize := 0 68 | if len(c.Data) > 0 { 69 | highSize = len(c.Data[0].Samples) 70 | } 71 | lowSize := 0 72 | if len(c.Data) > 0 { 73 | lowSize = len(c.Data[0].Samples) 74 | } 75 | high := make(map[float64]float64, 2*highSize) 76 | low := make(map[float64]float64, 2*lowSize) 77 | min, max := c.YRange.DataMin, c.YRange.DataMax 78 | for _, d := range c.Data { 79 | for _, p := range d.Samples { 80 | x, y := p.X, p.Y 81 | if y == 0 { 82 | continue 83 | } 84 | if y > 0 { 85 | if cur, ok := high[x]; ok { 86 | high[x] = cur + y 87 | } else { 88 | high[x] = y 89 | } 90 | if high[x] > max { 91 | max = high[x] 92 | } 93 | } else { 94 | if cur, ok := low[x]; ok { 95 | low[x] = cur - y 96 | } else { 97 | low[x] = y 98 | } 99 | if low[x] < min { 100 | min = low[x] 101 | } 102 | } 103 | } 104 | } 105 | 106 | // stacked histograms and y-axis _not_ starting at 0 is 107 | // utterly braindamaged and missleading: Fix to 0 if 108 | // not spaning negativ to positive 109 | if min >= 0 { 110 | c.YRange.DataMin, c.YRange.Min = 0, 0 111 | c.YRange.MinMode.Fixed, c.YRange.MinMode.Value = true, 0 112 | } else { 113 | c.YRange.DataMin, c.YRange.Min = min, min 114 | } 115 | 116 | if max <= 0 { 117 | c.YRange.DataMax, c.YRange.Max = 0, 0 118 | c.YRange.MaxMode.Fixed, c.YRange.MaxMode.Value = true, 0 119 | } else { 120 | c.YRange.DataMax, c.YRange.Max = max, max 121 | } 122 | } 123 | 124 | // Reset chart to state before plotting. 125 | func (c *BarChart) Reset() { 126 | c.XRange.Reset() 127 | c.YRange.Reset() 128 | } 129 | 130 | // Plot renders the chart to the graphics output g. 131 | func (c *BarChart) Plot(g Graphics) { 132 | // layout 133 | layout := layout(g, c.Title, c.XRange.Label, c.YRange.Label, 134 | c.XRange.TicSetting.Hide || c.XRange.TicSetting.HideLabels, 135 | c.YRange.TicSetting.Hide || c.YRange.TicSetting.HideLabels, 136 | &c.Key) 137 | width, height := layout.Width, layout.Height 138 | topm, leftm := layout.Top, layout.Left 139 | numxtics, numytics := layout.NumXtics, layout.NumYtics 140 | font := elementStyle(c.Options, MajorAxisElement).Font 141 | fw, fh, _ := g.FontMetrics(font) 142 | 143 | // Outside bound ranges for bar plots are nicer 144 | leftm += int(2 * fw) 145 | width -= int(2 * fw) 146 | height -= fh 147 | 148 | c.rescaleStackedY() 149 | c.XRange.Setup(numxtics, numxtics+3, width, leftm, false) 150 | c.YRange.Setup(numytics, numytics+2, height, topm, true) 151 | 152 | // Start of drawing 153 | g.Begin() 154 | if c.Title != "" { 155 | drawTitle(g, c.Title, elementStyle(c.Options, TitleElement)) 156 | } 157 | 158 | g.XAxis(c.XRange, topm+height+fh, topm, c.Options) 159 | g.YAxis(c.YRange, leftm-int(2*fw), leftm+width, c.Options) 160 | 161 | xf := c.XRange.Data2Screen 162 | yf := c.YRange.Data2Screen 163 | var sy0 int 164 | switch { 165 | case c.YRange.Min >= 0: 166 | sy0 = yf(c.YRange.Min) 167 | case c.YRange.Min < 0 && c.YRange.Max > 0: 168 | sy0 = yf(0) 169 | case c.YRange.Max <= 0: 170 | sy0 = yf(c.YRange.Max) 171 | default: 172 | fmt.Printf("No f.... idea how this can happen. You've been fiddeling?") 173 | } 174 | 175 | // TODO: gap between bars. 176 | var sbw, fbw int // ScreenBarWidth 177 | 178 | var low, high map[float64]float64 179 | if c.Stacked { 180 | high = make(map[float64]float64, 50) 181 | low = make(map[float64]float64, 50) 182 | } 183 | for dn, data := range c.Data { 184 | mindeltax := c.minimumSampleSep(dn) 185 | // DebugLogger.Printf("Minimum x-distance for set %d: %.3f\n", dn, mindeltax) 186 | if c.Stacked { 187 | sbw = (xf(2*mindeltax) - xf(0)) / 4 188 | fbw = sbw 189 | } else { 190 | // V 191 | // xxx === 000 ... xxx sbw = 3 192 | // xx == 00 ## .. xx == fbw = 11 193 | sbw = (xf(mindeltax)-xf(0))/(len(c.Data)+1) - 1 194 | fbw = len(c.Data)*sbw + len(c.Data) - 1 195 | } 196 | // DebugLogger.Printf("sbw = %d , fbw = %d\n", sbw, fbw) 197 | 198 | bars := make([]Barinfo, 0, len(data.Samples)) 199 | if c.Stacked { 200 | for _, p := range data.Samples { 201 | if _, ok := high[p.X]; !ok { 202 | high[p.X], low[p.X] = 0, 0 203 | } 204 | } 205 | } 206 | for _, p := range data.Samples { 207 | x, y := p.X, p.Y 208 | if y == 0 { 209 | continue 210 | } 211 | 212 | sx := xf(x) - fbw/2 213 | if !c.Stacked { 214 | sx += dn * (sbw + 1) 215 | } 216 | 217 | var sy, sh int 218 | if c.Stacked { 219 | if y > 0 { 220 | top := y + high[x] 221 | sy = yf(top) 222 | sh = yf(high[x]) - sy 223 | high[x] = top 224 | 225 | } else { 226 | bot := low[x] + y 227 | sy = yf(low[x]) 228 | sh = yf(bot) - sy 229 | low[x] = bot 230 | } 231 | } else { 232 | if y > 0 { 233 | sy = yf(y) 234 | sh = sy0 - sy 235 | } else { 236 | sy = sy0 237 | sh = yf(y) - sy0 238 | } 239 | } 240 | bar := Barinfo{x: sx, y: sy, w: sbw, h: sh} 241 | c.addLabel(&bar, y) 242 | bars = append(bars, bar) 243 | 244 | } 245 | g.Bars(bars, data.Style) 246 | 247 | } 248 | 249 | if !c.Key.Hide { 250 | g.Key(layout.KeyX, layout.KeyY, c.Key, c.Options) 251 | } 252 | 253 | g.End() 254 | 255 | /******** old code ************** 256 | 257 | // find bar width 258 | lbw, ubw := c.extremBarWidth() 259 | var barWidth float64 260 | if c.SameBarWidth { 261 | barWidth = lbw 262 | } else { 263 | barWidth = ubw 264 | } 265 | 266 | // set up range and extend if bar would not fit 267 | c.XRange.Setup(numxtics, numxtics+4, width, leftm, false) 268 | c.YRange.Setup(numytics, numytics+2, height, topm, true) 269 | if c.XRange.DataMin-barWidth/2 < c.XRange.Min { 270 | c.XRange.DataMin -= barWidth / 2 271 | } 272 | if c.XRange.DataMax+barWidth > c.XRange.Max { 273 | c.XRange.DataMax += barWidth / 2 274 | } 275 | c.XRange.Setup(numxtics, numxtics+4, width, leftm, false) 276 | 277 | // Start of drawing 278 | g.Begin() 279 | if c.Title != "" { 280 | g.Title(c.Title) 281 | } 282 | 283 | g.XAxis(c.XRange, topm+height, topm) 284 | g.YAxis(c.YRange, leftm, leftm+width) 285 | 286 | xf := c.XRange.Data2Screen 287 | yf := c.YRange.Data2Screen 288 | sy0 := yf(c.YRange.Min) 289 | 290 | barWidth = lbw 291 | for i, data := range c.Data { 292 | if !c.SameBarWidth { 293 | barWidth = c.barWidth(i) 294 | } 295 | sbw := imax(1, xf(2*barWidth)-xf(barWidth)-1) // screen bar width TODO 296 | bars := make([]Barinfo, len(data.Samples)) 297 | 298 | for i, point := range data.Samples { 299 | x, y := point.X, point.Y 300 | sx := xf(x-barWidth/2) + 1 301 | // sw := xf(x+barWidth/2) - sx 302 | sy := yf(y) 303 | sh := sy0 - sy 304 | bars[i].x, bars[i].y = sx, sy 305 | bars[i].w, bars[i].h = sbw, sh 306 | } 307 | g.Bars(bars, data.Style) 308 | } 309 | 310 | if !c.Key.Hide { 311 | g.Key(layout.KeyX, layout.KeyY, c.Key) 312 | } 313 | 314 | g.End() 315 | 316 | 317 | **********************************************************/ 318 | } 319 | 320 | func (c *BarChart) minimumSampleSep(d int) (min float64) { 321 | n := len(c.Data[d].Samples) - 1 322 | if n == 0 { 323 | return 1 324 | } 325 | min = math.MaxFloat64 326 | 327 | for i := 0; i < n; i++ { 328 | sep := math.Abs(c.Data[d].Samples[i].X - c.Data[d].Samples[i+1].X) 329 | if sep < min { 330 | min = sep 331 | } 332 | } 333 | return 334 | } 335 | 336 | func (c *BarChart) addLabel(bar *Barinfo, y float64) { 337 | if c.ShowVal == 0 { 338 | return 339 | } 340 | 341 | var sval string 342 | if math.Abs(y) >= 100 { 343 | sval = fmt.Sprintf("%d", int(y+0.5)) 344 | } else if math.Abs(y) >= 10 { 345 | sval = fmt.Sprintf("%.1f", y) 346 | } else if math.Abs(y) >= 1 { 347 | sval = fmt.Sprintf("%.2f", y) 348 | } else { 349 | sval = fmt.Sprintf("%.3f", y) 350 | } 351 | 352 | var tp string 353 | switch c.ShowVal { 354 | case 1: 355 | if y >= 0 { 356 | tp = "ot" 357 | } else { 358 | tp = "ob" 359 | } 360 | case 2: 361 | if y >= 0 { 362 | tp = "it" 363 | } else { 364 | tp = "ib" 365 | } 366 | case 3: 367 | tp = "c" 368 | } 369 | bar.t = sval 370 | bar.tp = tp 371 | } 372 | -------------------------------------------------------------------------------- /box.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | // "fmt" 7 | // "os" 8 | // "strings" 9 | ) 10 | 11 | // BoxChart represents box charts. 12 | // 13 | // To faciliate standard use of box plots, the method AddSet() exists which will 14 | // calculate the various elents of a box (e.g. med, q3, outliers, ...) from raw 15 | // data. 16 | type BoxChart struct { 17 | XRange, YRange Range // x and y axis 18 | Title string // Title of the chart 19 | Key Key // Key/legend 20 | Options PlotOptions 21 | Data []BoxChartData // the data sets to draw 22 | } 23 | 24 | // BoxChartData encapsulates a data set in a box chart 25 | type BoxChartData struct { 26 | Name string 27 | Style Style 28 | Samples []Box 29 | } 30 | 31 | // AddData adds all boxes in data to the chart. 32 | func (c *BoxChart) AddData(name string, data []Box, style Style) { 33 | c.Data = append(c.Data, BoxChartData{name, style, data}) 34 | ps := PlotStyle(PlotStylePoints | PlotStyleBox) 35 | c.Key.Entries = append(c.Key.Entries, KeyEntry{Text: name, Style: style, PlotStyle: ps}) 36 | // TODO(vodo) min, max 37 | } 38 | 39 | // NextDataSet adds a new (empty) data set to chart. After adding the data set you 40 | // can fill this last data set with AddSet() 41 | func (c *BoxChart) NextDataSet(name string, style Style) { 42 | c.Data = append(c.Data, BoxChartData{name, style, nil}) 43 | ps := PlotStyle(PlotStylePoints | PlotStyleBox) 44 | c.Key.Entries = append(c.Key.Entries, KeyEntry{Text: name, Style: style, PlotStyle: ps}) 45 | } 46 | 47 | // AddSet will add to last data set in the chart one new box calculated from data. 48 | // If outlier is true, than outliers (1.5*IQR from 25/75 percentil) are 49 | // drawn. If outlier is false, than the wiskers extend from min to max. 50 | func (c *BoxChart) AddSet(x float64, data []float64, outlier bool) { 51 | min, lq, med, avg, uq, max := SixvalFloat64(data, 25) 52 | b := Box{X: x, Avg: avg, Med: med, Q1: lq, Q3: uq, Low: min, High: max} 53 | 54 | if len(c.Data) == 0 { 55 | c.Data = make([]BoxChartData, 1) 56 | st := Style{LineColor: color.NRGBA{0, 0, 0, 0xff}, LineWidth: 1, LineStyle: SolidLine} 57 | c.Data[0] = BoxChartData{Name: "", Style: st} 58 | } 59 | 60 | if len(c.Data) == 1 && len(c.Data[0].Samples) == 0 { 61 | c.XRange.DataMin, c.XRange.DataMax = x, x 62 | c.YRange.DataMin, c.YRange.DataMax = min, max 63 | } else { 64 | if x < c.XRange.DataMin { 65 | c.XRange.DataMin = x 66 | } else if x > c.XRange.DataMax { 67 | c.XRange.DataMax = x 68 | } 69 | if min < c.YRange.DataMin { 70 | c.YRange.DataMin = min 71 | } 72 | if max > c.YRange.DataMax { 73 | c.YRange.DataMax = max 74 | } 75 | } 76 | 77 | if outlier { 78 | outliers := make([]float64, 0) 79 | iqr := uq - lq 80 | min, max = max, min 81 | for _, d := range data { 82 | if d > uq+1.5*iqr || d < lq-1.5*iqr { 83 | outliers = append(outliers, d) 84 | } 85 | if d > max && d <= uq+1.5*iqr { 86 | max = d 87 | } 88 | if d < min && d >= lq-1.5*iqr { 89 | min = d 90 | } 91 | } 92 | b.Low, b.High, b.Outliers = min, max, outliers 93 | } 94 | j := len(c.Data) - 1 95 | c.Data[j].Samples = append(c.Data[j].Samples, b) 96 | } 97 | 98 | // Reset chart to state before plotting. 99 | func (c *BoxChart) Reset() { 100 | c.XRange.Reset() 101 | c.YRange.Reset() 102 | } 103 | 104 | // Plot renders the chart to the graphic output g. 105 | func (c *BoxChart) Plot(g Graphics) { 106 | // layout 107 | layout := layout(g, c.Title, c.XRange.Label, c.YRange.Label, 108 | c.XRange.TicSetting.Hide || c.XRange.TicSetting.HideLabels, 109 | c.YRange.TicSetting.Hide || c.YRange.TicSetting.HideLabels, 110 | &c.Key) 111 | width, height := layout.Width, layout.Height 112 | topm, leftm := layout.Top, layout.Left 113 | numxtics, numytics := layout.NumXtics, layout.NumYtics 114 | // fontwidth, fontheight, _ := g.FontMetrics(DataStyle{}) 115 | 116 | g.Begin() 117 | 118 | c.XRange.Setup(numxtics, numxtics+2, width, leftm, false) 119 | c.YRange.Setup(numytics, numytics+1, height, topm, true) 120 | 121 | if c.Title != "" { 122 | drawTitle(g, c.Title, elementStyle(c.Options, TitleElement)) 123 | } 124 | 125 | g.XAxis(c.XRange, topm+height, topm, c.Options) 126 | g.YAxis(c.YRange, leftm, leftm+width, c.Options) 127 | 128 | yf := c.YRange.Data2Screen 129 | nan := math.NaN() 130 | for _, data := range c.Data { 131 | // Samples 132 | nums := len(data.Samples) 133 | bw := width / (2*nums - 1) 134 | 135 | boxes := make([]Box, len(data.Samples)) 136 | for i, d := range data.Samples { 137 | x := float64(c.XRange.Data2Screen(d.X)) 138 | // DebugLogger.Printf("Q1=%.2f Q3=%.3f", d.Q1, d.Q3) 139 | q1, q3 := float64(yf(d.Q1)), float64(yf(d.Q3)) 140 | med, avg := nan, nan 141 | high, low := nan, nan 142 | if !math.IsNaN(d.Med) { 143 | med = float64(yf(d.Med)) 144 | } 145 | if !math.IsNaN(d.Avg) { 146 | avg = float64(yf(d.Avg)) 147 | } 148 | if !math.IsNaN(d.High) { 149 | high = float64(yf(d.High)) 150 | } 151 | if !math.IsNaN(d.Low) { 152 | low = float64(yf(d.Low)) 153 | } 154 | 155 | outliers := make([]float64, len(d.Outliers)) 156 | for j, ol := range d.Outliers { 157 | outliers[j] = float64(c.YRange.Data2Screen(ol)) 158 | } 159 | boxes[i].X = x 160 | boxes[i].Q1 = q1 161 | boxes[i].Q3 = q3 162 | boxes[i].Med = med 163 | boxes[i].Avg = avg 164 | boxes[i].High = high 165 | boxes[i].Low = low 166 | boxes[i].Outliers = outliers 167 | } 168 | g.Boxes(boxes, bw, data.Style) 169 | } 170 | 171 | if !c.Key.Hide { 172 | g.Key(layout.KeyX, layout.KeyY, c.Key, c.Options) 173 | } 174 | 175 | g.End() 176 | } 177 | -------------------------------------------------------------------------------- /chart.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "math" 8 | "time" 9 | ) 10 | 11 | // Chart ist the very simple interface for all charts: They can be plotted to a graphics output. 12 | type Chart interface { 13 | Plot(g Graphics) // Plot the chart to g. 14 | Reset() // Reset any setting made during last plot. 15 | } 16 | 17 | // Expansion determines the way an axis range is expanded to align 18 | // nicely with the tics on the axis. 19 | type Expansion int 20 | 21 | // Suitable values for Expand in RangeMode. 22 | const ( 23 | ExpandNextTic Expansion = iota // Set min/max to next tic really below/above min/max of data. 24 | ExpandToTic // Set to next tic below/above or equal to min/max of data. 25 | ExpandTight // Use data min/max as limit. 26 | ExpandABit // Like ExpandToTic and add/subtract ExpandABitFraction of tic distance. 27 | ) 28 | 29 | // ExpandABitFraction is the fraction of a major tic spacing added during 30 | // axis range expansion with the ExpandABit mode. 31 | var ExpandABitFraction = 0.5 32 | 33 | // RangeMode describes how one end of an axis is set up. There are basically three different main modes: 34 | // * Fixed: Fixed==true. 35 | // Use Value/TValue as fixed value this ignoring the actual data range. 36 | // * Unconstrained autoscaling: Fixed==false && Constrained==false. 37 | // Set range to whatever data requires. 38 | // * Constrained autoscaling: Fixed==false && Constrained==true. 39 | // Scale axis according to data present, but limit scaling to intervall [Lower,Upper] 40 | // For both autoscaling modes Expand defines how much expansion is done below/above 41 | // the lowest/highest data point. 42 | type RangeMode struct { 43 | Fixed bool // If false: autoscaling. If true: use (T)Value/TValue as fixed setting 44 | Constrained bool // If false: full autoscaling. If true: use (T)Lower (T)Upper as limits 45 | Expand Expansion // One of ExpandNextTic, ExpandTight, ExpandABit 46 | Value float64 // Value of end point of axis in Fixed=true mode, ignorder otherwise 47 | TValue time.Time // Same as Value, but used for Date/Time axis 48 | Lower, Upper float64 // Lower and upper limit for constrained autoscaling 49 | TLower, TUpper time.Time // Same s Lower/Upper, but used for Date/Time axis 50 | } 51 | 52 | // GridMode describes the way a grid on the major tics is drawn 53 | type GridMode int 54 | 55 | const ( 56 | GridOff GridMode = iota // No grid lines 57 | GridLines // Grid lines 58 | GridBlocks // Zebra style background 59 | ) 60 | 61 | // MirrorAxis describes if and how an axis is drawn on the oposite side of 62 | // a chart, 63 | type MirrorAxis int 64 | 65 | const ( 66 | MirrorAxisAndTics MirrorAxis = 0 // draw a full mirrored axis including tics 67 | MirrorNothing MirrorAxis = -1 // do not draw a mirrored axis 68 | MirrorAxisOnly MirrorAxis = 1 // just draw a mirrord axis, but omit tics 69 | ) 70 | 71 | // TicSetting describes how (if at all) tics are shown on an axis. 72 | type TicSetting struct { 73 | Hide bool // dont show tics if true 74 | HideLabels bool // don't show tic labels if true 75 | Tics int // 0: across axis, 1: inside, 2: outside, other: off 76 | Minor int // 0: off, 1: auto, >1: number of intervalls (not number of tics!) 77 | Delta float64 // wanted step between major tics. 0 means auto 78 | TDelta TimeDelta // same as Delta, but used for Date/Time axis 79 | Grid GridMode // GridOff, GridLines, GridBlocks 80 | Mirror MirrorAxis // 0: mirror axis and tics, -1: don't mirror anything, 1: mirror axis only (no tics) 81 | 82 | // Format is used to print the tic labels. If unset FmtFloat is used. 83 | Format func(float64) string 84 | 85 | // TFormat is used to print tic labels for date/time axis. 86 | TFormat func(time.Time, TimeDelta) string 87 | 88 | // TLocation allows to fix the timezone in which date/time axis tic labels 89 | // are printed. 90 | TLocation *time.Location 91 | 92 | UserDelta bool // true if Delta or TDelta was input 93 | } 94 | 95 | // Tic describs a single tic on an axis. 96 | type Tic struct { 97 | Pos float64 // position of the tic on the axis (in data coordinates). 98 | LabelPos float64 // position of the label on the axis (in data coordinates). 99 | Label string // the Label of the tic 100 | Align int // alignment of the label: -1: left/top, 0 center, 1 right/bottom (unused) 101 | } 102 | 103 | // Range encapsulates all information about an axis. 104 | type Range struct { 105 | Label string // Label of axis 106 | Log bool // Logarithmic axis? 107 | Time bool // Date/Time axis? 108 | MinMode, MaxMode RangeMode // How to handel min and max of this axis/range 109 | TicSetting TicSetting // How to handle tics. 110 | DataMin, DataMax float64 // Actual min/max values from data. If both zero: not calculated 111 | ShowLimits bool // Display axis Min and Max values on plot 112 | ShowZero bool // Add line to show 0 of this axis 113 | Category []string // If not empty (and neither Log nor Time): Use Category[n] as tic label at pos n+1. 114 | 115 | // The following values are set up during plotting 116 | Min, Max float64 // Actual minium and maximum of this axis/range. 117 | TMin, TMax time.Time // Same as Min/Max, but used for Date/Time axis 118 | Tics []Tic // List of tics to display 119 | 120 | // The following functions are set up during plotting 121 | Norm func(float64) float64 // Function to map [Min:Max] to [0:1] 122 | InvNorm func(float64) float64 // Inverse of Norm() 123 | Data2Screen func(float64) int // Function to map data value to screen position 124 | Screen2Data func(int) float64 // Inverse of Data2Screen 125 | } 126 | 127 | // Fixed is a helper (just reduces typing) functions which turns of autoscaling 128 | // and sets the axis range to [min,max] and the tic distance to delta. 129 | func (r *Range) Fixed(min, max, delta float64) { 130 | r.MinMode.Fixed, r.MaxMode.Fixed = true, true 131 | r.MinMode.Value, r.MaxMode.Value = min, max 132 | r.TicSetting.Delta = delta 133 | } 134 | 135 | // TFixed is the date/time version of Fixed. 136 | func (r *Range) TFixed(min, max time.Time, delta TimeDelta) { 137 | r.MinMode.Fixed, r.MaxMode.Fixed = true, true 138 | r.MinMode.TValue, r.MaxMode.TValue = min, max 139 | r.TicSetting.TDelta = delta 140 | } 141 | 142 | // Reset the fields in r which have been set up during a plot. 143 | func (r *Range) Reset() { 144 | r.Min, r.Max = 0, 0 145 | r.TMin, r.TMax = time.Time{}, time.Time{} 146 | r.Tics = nil 147 | r.Norm, r.InvNorm = nil, nil 148 | r.Data2Screen, r.Screen2Data = nil, nil 149 | 150 | if !r.TicSetting.UserDelta { 151 | r.TicSetting.Delta = 0 152 | r.TicSetting.TDelta = nil 153 | } 154 | } 155 | 156 | // Prepare the range r for use, especially set up all values needed for autoscale() to work properly. 157 | func (r *Range) init() { r.Init() } 158 | func (r *Range) Init() { 159 | // All the min stuff 160 | if r.MinMode.Fixed { 161 | // copy TValue to Value if set and time axis 162 | if r.Time && !r.MinMode.TValue.IsZero() { 163 | r.MinMode.Value = float64(r.MinMode.TValue.Unix()) 164 | } 165 | r.DataMin = r.MinMode.Value 166 | } else if r.MinMode.Constrained { 167 | // copy TLower/TUpper to Lower/Upper if set and time axis 168 | if r.Time && !r.MinMode.TLower.IsZero() { 169 | r.MinMode.Lower = float64(r.MinMode.TLower.Unix()) 170 | } 171 | if r.Time && !r.MinMode.TUpper.IsZero() { 172 | r.MinMode.Upper = float64(r.MinMode.TUpper.Unix()) 173 | } 174 | if r.MinMode.Lower == 0 && r.MinMode.Upper == 0 { 175 | // Constrained but un-initialized: Full autoscaling 176 | r.MinMode.Lower = -math.MaxFloat64 177 | r.MinMode.Upper = math.MaxFloat64 178 | } 179 | r.DataMin = r.MinMode.Upper 180 | } else { 181 | r.DataMin = math.MaxFloat64 182 | } 183 | 184 | // All the max stuff 185 | if r.MaxMode.Fixed { 186 | // copy TValue to Value if set and time axis 187 | if r.Time && !r.MaxMode.TValue.IsZero() { 188 | r.MaxMode.Value = float64(r.MaxMode.TValue.Unix()) 189 | } 190 | r.DataMax = r.MaxMode.Value 191 | } else if r.MaxMode.Constrained { 192 | // copy TLower/TUpper to Lower/Upper if set and time axis 193 | if r.Time && !r.MaxMode.TLower.IsZero() { 194 | r.MaxMode.Lower = float64(r.MaxMode.TLower.Unix()) 195 | } 196 | if r.Time && !r.MaxMode.TUpper.IsZero() { 197 | r.MaxMode.Upper = float64(r.MaxMode.TUpper.Unix()) 198 | } 199 | if r.MaxMode.Lower == 0 && r.MaxMode.Upper == 0 { 200 | // Constrained but un-initialized: Full autoscaling 201 | r.MaxMode.Lower = -math.MaxFloat64 202 | r.MaxMode.Upper = math.MaxFloat64 203 | } 204 | r.DataMax = r.MaxMode.Upper 205 | } else { 206 | r.DataMax = -math.MaxFloat64 207 | } 208 | 209 | // fmt.Printf("At end of init: DataMin / DataMax = %g / %g\n", r.DataMin, r.DataMax) 210 | } 211 | 212 | // Update DataMin and DataMax according to the RangeModes. 213 | func (r *Range) autoscale(x float64) { 214 | 215 | if x < r.DataMin && !r.MinMode.Fixed { 216 | if !r.MinMode.Constrained { 217 | // full autoscaling 218 | r.DataMin = x 219 | } else { 220 | r.DataMin = fmin(fmax(x, r.MinMode.Lower), r.DataMin) 221 | } 222 | } 223 | 224 | if x > r.DataMax && !r.MaxMode.Fixed { 225 | if !r.MaxMode.Constrained { 226 | // full autoscaling 227 | r.DataMax = x 228 | } else { 229 | r.DataMax = fmax(fmin(x, r.MaxMode.Upper), r.DataMax) 230 | } 231 | } 232 | } 233 | 234 | // Units are the SI prefixes for 10^3n 235 | var Units = []string{" y", " z", " a", " f", " p", " n", " µ", "m", " k", " M", " G", " T", " P", " E", " Z", " Y"} 236 | 237 | // FmtFloat yields a string representation of f. E.g. 12345.67 --> "12.3 k"; 0.09876 --> "99 m" 238 | func FmtFloat(f float64) string { 239 | af := math.Abs(f) 240 | if f == 0 { 241 | return "0" 242 | } else if 1 <= af && af < 10 { 243 | return fmt.Sprintf("%.1f", f) 244 | } else if 10 <= af && af <= 1000 { 245 | return fmt.Sprintf("%.0f", f) 246 | } 247 | 248 | if af < 1 { 249 | var p = 8 250 | for math.Abs(f) < 1 && p >= 0 { 251 | f *= 1000 252 | p-- 253 | } 254 | return FmtFloat(f) + Units[p] 255 | } 256 | 257 | var p = 7 258 | for math.Abs(f) > 1000 && p < 16 { 259 | f /= 1000 260 | p++ 261 | } 262 | return FmtFloat(f) + Units[p] 263 | } 264 | 265 | func almostEqual(a, b, d float64) bool { 266 | return math.Abs(a-b) < d 267 | } 268 | 269 | // applyRangeMode returns val constrained by mode. val is considered the upper end of an range/axis 270 | // if upper is true. To allow proper rounding to tic (depending on desired RangeMode) 271 | // the ticDelta has to be provided. Logaritmic axis are selected by log = true and ticDelta 272 | // is ignored: Tics are of the form 1*10^n. 273 | func applyRangeMode(mode RangeMode, val, ticDelta float64, upper, log bool) float64 { 274 | if mode.Fixed { 275 | return mode.Value 276 | } 277 | if mode.Constrained { 278 | if val < mode.Lower { 279 | val = mode.Lower 280 | } else if val > mode.Upper { 281 | val = mode.Upper 282 | } 283 | } 284 | 285 | switch mode.Expand { 286 | case ExpandToTic, ExpandNextTic: 287 | var v float64 288 | if upper { 289 | if log { 290 | v = math.Pow10(int(math.Ceil(math.Log10(val)))) 291 | } else { 292 | v = math.Ceil(val/ticDelta) * ticDelta 293 | } 294 | } else { 295 | if log { 296 | v = math.Pow10(int(math.Floor(math.Log10(val)))) 297 | } else { 298 | v = math.Floor(val/ticDelta) * ticDelta 299 | } 300 | } 301 | if mode.Expand == ExpandNextTic { 302 | if upper { 303 | if log { 304 | if val/v < 2 { // TODO(vodo) use ExpandABitFraction 305 | v *= ticDelta 306 | } 307 | } else { 308 | if almostEqual(v, val, ticDelta/15) { 309 | v += ticDelta 310 | } 311 | } 312 | } else { 313 | if log { 314 | if v/val > 7 { // TODO(vodo) use ExpandABitFraction 315 | v /= ticDelta 316 | } 317 | } else { 318 | if almostEqual(v, val, ticDelta/15) { 319 | v -= ticDelta 320 | } 321 | } 322 | } 323 | } 324 | val = v 325 | case ExpandABit: 326 | if upper { 327 | if log { 328 | val *= math.Pow(10, ExpandABitFraction) 329 | } else { 330 | val += ticDelta * ExpandABitFraction 331 | } 332 | } else { 333 | if log { 334 | val /= math.Pow(10, ExpandABitFraction) 335 | } else { 336 | val -= ticDelta * ExpandABitFraction 337 | } 338 | } 339 | } 340 | 341 | return val 342 | } 343 | 344 | // tApplyRangeMode is the same as applyRangeMode for date/time axis/ranges. 345 | func tApplyRangeMode(mode RangeMode, val time.Time, step TimeDelta, upper bool) (bound time.Time, tic time.Time) { 346 | if mode.Fixed { 347 | bound = mode.TValue 348 | if upper { 349 | tic = RoundDown(val, step) 350 | } else { 351 | tic = RoundUp(val, step) 352 | } 353 | return 354 | } 355 | if mode.Constrained { // TODO(vodo) use T... 356 | sval := val.Unix() 357 | if sval < int64(mode.Lower) { 358 | sval = int64(mode.Lower) 359 | } else if sval > int64(mode.Upper) { 360 | sval = int64(mode.Upper) 361 | } 362 | val = time.Unix(sval, 0).In(val.Location()) 363 | } 364 | 365 | switch mode.Expand { 366 | case ExpandToTic: 367 | if upper { 368 | val = RoundUp(val, step) 369 | } else { 370 | val = RoundDown(val, step) 371 | } 372 | return val, val 373 | case ExpandNextTic: 374 | if upper { 375 | tic = RoundUp(val, step) 376 | } else { 377 | tic = RoundDown(val, step) 378 | } 379 | s := tic.Unix() 380 | if math.Abs(float64(s-val.Unix())/float64(step.Seconds())) < 0.15 { 381 | if upper { 382 | val = RoundUp(time.Unix(s+step.Seconds()/2, 0).In(val.Location()), step) 383 | } else { 384 | val = RoundDown(time.Unix(s-step.Seconds()/2, 0).In(val.Location()), step) 385 | } 386 | } else { 387 | val = tic 388 | } 389 | return val, val 390 | case ExpandABit: 391 | if upper { 392 | tic = RoundDown(val, step) 393 | val = time.Unix(tic.Unix()+step.Seconds()/2, 0).In(val.Location()) 394 | } else { 395 | tic = RoundUp(val, step) 396 | val = time.Unix(tic.Unix()-step.Seconds()/2, 0).In(val.Location()) 397 | } 398 | return 399 | 400 | } 401 | 402 | return val, val 403 | } 404 | 405 | func f2d(x float64) string { 406 | s := int64(x) 407 | t := time.Unix(s, 0) 408 | return t.Format("2006-01-02 15:04:05 (Mon)") 409 | } 410 | 411 | func (r *Range) tSetup(desiredNumberOfTics, maxNumberOfTics int, delta, mindelta float64) { 412 | DebugLogger.Printf("Data: [ %s : %s ] --> delta/mindelta = %.3g/%.3g (desired %d/max %d)\n", 413 | f2d(r.DataMin), f2d(r.DataMax), delta, mindelta, desiredNumberOfTics, maxNumberOfTics) 414 | 415 | var td TimeDelta 416 | if r.TicSetting.TDelta != nil { 417 | td = r.TicSetting.TDelta 418 | r.TicSetting.UserDelta = true 419 | } else { 420 | td = MatchingTimeDelta(delta, 3) 421 | r.TicSetting.UserDelta = false 422 | } 423 | r.ShowLimits = true 424 | 425 | // Set up time tic delta 426 | mint := time.Unix(int64(r.DataMin), 0) 427 | maxt := time.Unix(int64(r.DataMax), 0) 428 | 429 | if r.TicSetting.TLocation != nil { 430 | mint = mint.In(r.TicSetting.TLocation) 431 | maxt = maxt.In(r.TicSetting.TLocation) 432 | } 433 | 434 | var ftic, ltic time.Time 435 | r.TMin, ftic = tApplyRangeMode(r.MinMode, mint, td, false) 436 | r.TMax, ltic = tApplyRangeMode(r.MaxMode, maxt, td, true) 437 | r.TicSetting.Delta, r.TicSetting.TDelta = float64(td.Seconds()), td 438 | r.Min, r.Max = float64(r.TMin.Unix()), float64(r.TMax.Unix()) 439 | 440 | ftd := float64(td.Seconds()) 441 | actNumTics := int((r.Max - r.Min) / ftd) 442 | if actNumTics > maxNumberOfTics { 443 | // recalculate time tic delta 444 | DebugLogger.Printf("Switching from %s no next larger step %s", td, NextTimeDelta(td)) 445 | td = NextTimeDelta(td) 446 | ftd = float64(td.Seconds()) 447 | r.TMin, ftic = tApplyRangeMode(r.MinMode, mint, td, false) 448 | r.TMax, ltic = tApplyRangeMode(r.MaxMode, maxt, td, true) 449 | r.TicSetting.Delta, r.TicSetting.TDelta = float64(td.Seconds()), td 450 | r.Min, r.Max = float64(r.TMin.Unix()), float64(r.TMax.Unix()) 451 | actNumTics = int((r.Max - r.Min) / ftd) 452 | } 453 | 454 | DebugLogger.Printf("DataRange: %s TO %s", f2d(r.DataMin), f2d(r.DataMax)) 455 | DebugLogger.Printf("AxisRange: %s TO %s", f2d(r.Min), f2d(r.Max)) 456 | DebugLogger.Printf("TicsRange: %s TO %s Step %s", 457 | ftic.Format("2006-01-02 15:04:05 (Mon)"), ltic.Format("2006-01-02 15:04:05 (Mon)"), td) 458 | 459 | // Set up tics 460 | r.Tics = make([]Tic, 0) 461 | step := int64(td.Seconds()) 462 | align := 0 463 | 464 | var formater func(t time.Time, td TimeDelta) string 465 | if r.TicSetting.TFormat != nil { 466 | formater = r.TicSetting.TFormat 467 | } else { 468 | formater = func(t time.Time, td TimeDelta) string { return td.Format(t) } 469 | } 470 | 471 | for i := 0; ftic.Unix() < ltic.Unix(); i++ { 472 | x := float64(ftic.Unix()) 473 | label := formater(ftic, td) 474 | var labelPos float64 475 | if td.Period() { 476 | labelPos = x + float64(step)/2 477 | } else { 478 | labelPos = x 479 | } 480 | t := Tic{Pos: x, LabelPos: labelPos, Label: label, Align: align} 481 | r.Tics = append(r.Tics, t) 482 | z := time.Unix(ftic.Unix()+step+step/5, 0) 483 | if r.TicSetting.TLocation != nil { 484 | z = z.In(r.TicSetting.TLocation) 485 | } 486 | ftic = RoundDown(z, td) 487 | } 488 | // last tic might not get label if period 489 | if td.Period() { 490 | r.Tics = append(r.Tics, Tic{Pos: float64(ftic.Unix())}) 491 | } else { 492 | x := float64(ftic.Unix()) 493 | label := formater(ftic, td) 494 | var labelPos float64 495 | labelPos = x 496 | t := Tic{Pos: x, LabelPos: labelPos, Label: label, Align: align} 497 | r.Tics = append(r.Tics, t) 498 | } 499 | } 500 | 501 | // Determine appropriate tic delta for normal (non dat/time) axis from desired delta and minimal delta. 502 | func (r *Range) fDelta(delta, mindelta float64) float64 { 503 | if r.Log { 504 | return 10 505 | } 506 | 507 | // Set up nice tic delta of the form 1,2,5 * 10^n 508 | // TODO: deltas of 25 and 250 would be suitable too... 509 | de := math.Pow10(int(math.Floor(math.Log10(delta)))) 510 | f := delta / de 511 | switch { 512 | case f < 2: 513 | f = 1 514 | case f < 4: 515 | f = 2 516 | case f < 9: 517 | f = 5 518 | default: 519 | f = 1 520 | de *= 10 521 | } 522 | delta = f * de 523 | if delta < mindelta { 524 | DebugLogger.Printf("Redoing delta: %g < %g", delta, mindelta) 525 | // recalculate tic delta 526 | switch f { 527 | case 1, 5: 528 | delta *= 2 529 | case 2: 530 | delta *= 2.5 531 | default: 532 | fmt.Printf("Oooops. Strange f: %g\n", f) 533 | } 534 | } 535 | return delta 536 | } 537 | 538 | // Set up normal (=non date/time axis) 539 | func (r *Range) fSetup(desiredNumberOfTics, maxNumberOfTics int, delta, mindelta float64) { 540 | DebugLogger.Printf("Data: [ %.5g : %.5g ] --> delta/mindelta = %.3g/%.3g (desired %d/max %d)\n", 541 | r.DataMin, r.DataMax, delta, mindelta, desiredNumberOfTics, maxNumberOfTics) 542 | if r.TicSetting.Delta != 0 { 543 | delta = r.TicSetting.Delta 544 | r.TicSetting.UserDelta = true 545 | } else { 546 | delta = r.fDelta(delta, mindelta) 547 | r.TicSetting.UserDelta = false 548 | } 549 | 550 | r.Min = applyRangeMode(r.MinMode, r.DataMin, delta, false, r.Log) 551 | r.Max = applyRangeMode(r.MaxMode, r.DataMax, delta, true, r.Log) 552 | r.TicSetting.Delta = delta 553 | 554 | DebugLogger.Printf("DataRange: %.6g TO %.6g", r.DataMin, r.DataMax) 555 | DebugLogger.Printf("AxisRange: %.6g TO %.6g", r.Min, r.Max) 556 | 557 | formater := FmtFloat 558 | if r.TicSetting.Format != nil { 559 | formater = r.TicSetting.Format 560 | } 561 | 562 | if r.Log { 563 | x := math.Pow10(int(math.Ceil(math.Log10(r.Min)))) 564 | last := math.Pow10(int(math.Floor(math.Log10(r.Max)))) 565 | DebugLogger.Printf("TicsRange: %.6g TO %.6g Factor %.6g", x, last, delta) 566 | r.Tics = make([]Tic, 0, maxNumberOfTics) 567 | for ; x <= last; x = x * delta { 568 | t := Tic{Pos: x, LabelPos: x, Label: formater(x)} 569 | r.Tics = append(r.Tics, t) 570 | // fmt.Printf("%v\n", t) 571 | } 572 | 573 | } else { 574 | if len(r.Category) > 0 { 575 | DebugLogger.Printf("TicsRange: %d categorical tics.", len(r.Category)) 576 | r.Tics = make([]Tic, len(r.Category)) 577 | for i, c := range r.Category { 578 | x := float64(i) 579 | if x < r.Min { 580 | continue 581 | } 582 | if x > r.Max { 583 | break 584 | } 585 | r.Tics[i].Pos = math.NaN() // no tic 586 | r.Tics[i].LabelPos = x 587 | r.Tics[i].Label = c 588 | } 589 | 590 | } else { 591 | // normal numeric axis 592 | first := delta * math.Ceil(r.Min/delta) 593 | num := int(-first/delta + math.Floor(r.Max/delta) + 1.5) 594 | DebugLogger.Printf("TicsRange: %.6g TO %.6g Step %.6g", first, first+float64(num)*delta, delta) 595 | 596 | // Set up tics 597 | r.Tics = make([]Tic, num) 598 | for i, x := 0, first; i < num; i, x = i+1, x+delta { 599 | r.Tics[i].Pos, r.Tics[i].LabelPos = x, x 600 | r.Tics[i].Label = formater(x) 601 | } 602 | } 603 | // TODO(vodo) r.ShowLimits = true 604 | } 605 | } 606 | 607 | // Setup several fields of the Range r according to RangeModes and TicSettings. 608 | // DataMin and DataMax of r must be present and should indicate lowest and highest 609 | // value present in the data set. The following fields of r are filled: 610 | // (T)Min and (T)Max lower and upper limit of axis, (T)-version for date/time axis 611 | // Tics slice of tics to draw 612 | // TicSetting.(T)Delta actual tic delta 613 | // Norm and InvNorm mapping of [lower,upper]_data --> [0:1] and inverse 614 | // Data2Screen mapping of data to screen coordinates 615 | // Screen2Data inverse of Data2Screen 616 | // The parameters desiredNumberOfTics and maxNumberOfTics are what the say. 617 | // sWidth and sOffset are screen-width and -offset and are used to set up the 618 | // Data-Screen conversion functions. If revert is true, than screen coordinates 619 | // are assumed to be the other way around than mathematical coordinates. 620 | // 621 | // TODO(vodo) seperate screen stuff into own method. 622 | func (r *Range) Setup(desiredNumberOfTics, maxNumberOfTics, sWidth, sOffset int, revert bool) { 623 | // Sanitize input 624 | if desiredNumberOfTics <= 1 { 625 | desiredNumberOfTics = 2 626 | } 627 | if maxNumberOfTics < desiredNumberOfTics { 628 | maxNumberOfTics = desiredNumberOfTics 629 | } 630 | if r.DataMax == r.DataMin { 631 | r.DataMax = r.DataMin + 1 632 | } 633 | delta := (r.DataMax - r.DataMin) / float64(desiredNumberOfTics-1) 634 | mindelta := (r.DataMax - r.DataMin) / float64(maxNumberOfTics-1) 635 | 636 | if r.Time { 637 | r.tSetup(desiredNumberOfTics, maxNumberOfTics, delta, mindelta) 638 | } else { // simple, not a date range 639 | r.fSetup(desiredNumberOfTics, maxNumberOfTics, delta, mindelta) 640 | } 641 | 642 | if r.Log { 643 | r.Norm = func(x float64) float64 { return math.Log10(x/r.Min) / math.Log10(r.Max/r.Min) } 644 | r.InvNorm = func(f float64) float64 { return (r.Max-r.Min)*f + r.Min } 645 | } else { 646 | r.Norm = func(x float64) float64 { return (x - r.Min) / (r.Max - r.Min) } 647 | r.InvNorm = func(f float64) float64 { return (r.Max-r.Min)*f + r.Min } 648 | } 649 | 650 | if !revert { 651 | r.Data2Screen = func(x float64) int { 652 | return int(float64(sWidth)*r.Norm(x)) + sOffset 653 | } 654 | r.Screen2Data = func(x int) float64 { 655 | return r.InvNorm(float64(x-sOffset) / float64(sWidth)) 656 | } 657 | } else { 658 | r.Data2Screen = func(x float64) int { 659 | return sWidth - int(float64(sWidth)*r.Norm(x)) + sOffset 660 | } 661 | r.Screen2Data = func(x int) float64 { 662 | return r.InvNorm(float64(-x+sOffset+sWidth) / float64(sWidth)) 663 | } 664 | 665 | } 666 | 667 | } 668 | 669 | // LayoutData encapsulates the layout of the graph area in the whole drawing area. 670 | type LayoutData struct { 671 | Width, Height int // width and height of graph area 672 | Left, Top int // left and top margin 673 | KeyX, KeyY int // x and y coordiante of key 674 | NumXtics, NumYtics int // suggested numer of tics for both axis 675 | } 676 | 677 | // Layout graph data area on screen and place key. 678 | func layout(g Graphics, title, xlabel, ylabel string, hidextics, hideytics bool, key *Key) (ld LayoutData) { 679 | fw, fh, _ := g.FontMetrics(Font{}) 680 | w, h := g.Dimensions() 681 | 682 | if key.Pos == "" { 683 | key.Pos = "itr" 684 | } 685 | 686 | width, leftm, height, topm := w-int(6*fw), int(2*fw), h-2*fh, fh 687 | xlabsep, ylabsep := fh, int(3*fw) 688 | if title != "" { 689 | topm += (5 * fh) / 2 690 | height -= (5 * fh) / 2 691 | } 692 | if xlabel != "" { 693 | height -= (3 * fh) / 2 694 | } 695 | if !hidextics { 696 | height -= (3 * fh) / 2 697 | xlabsep += (3 * fh) / 2 698 | } 699 | if ylabel != "" { 700 | leftm += 2 * fh 701 | width -= 2 * fh 702 | } 703 | if !hideytics { 704 | leftm += int(6 * fw) 705 | width -= int(6 * fw) 706 | ylabsep += int(6 * fw) 707 | } 708 | 709 | if key != nil && !key.Hide && len(key.Place()) > 0 { 710 | m := key.Place() 711 | kw, kh, _, _ := key.Layout(g, m, Font{}) // TODO: use real font 712 | sepx, sepy := int(fw)+fh, int(fw)+fh 713 | switch key.Pos[:2] { 714 | case "ol": 715 | width, leftm = width-kw-sepx, leftm+kw 716 | ld.KeyX = sepx / 2 717 | case "or": 718 | width = width - kw - sepx 719 | ld.KeyX = w - kw - sepx/2 720 | case "ot": 721 | height, topm = height-kh-sepy, topm+kh 722 | ld.KeyY = sepy / 2 723 | if title != "" { 724 | ld.KeyY += 2 * fh 725 | } 726 | case "ob": 727 | height = height - kh - sepy 728 | ld.KeyY = h - kh - sepy/2 729 | case "it": 730 | ld.KeyY = topm + sepy 731 | case "ic": 732 | ld.KeyY = topm + (height-kh)/2 733 | case "ib": 734 | ld.KeyY = topm + height - kh - sepy 735 | 736 | } 737 | 738 | switch key.Pos[:2] { 739 | case "ol", "or": 740 | switch key.Pos[2] { 741 | case 't': 742 | ld.KeyY = topm 743 | case 'c': 744 | ld.KeyY = topm + (height-kh)/2 745 | case 'b': 746 | ld.KeyY = topm + height - kh 747 | } 748 | case "ot", "ob": 749 | switch key.Pos[2] { 750 | case 'l': 751 | ld.KeyX = leftm 752 | case 'c': 753 | ld.KeyX = leftm + (width-kw)/2 754 | case 'r': 755 | ld.KeyX = w - kw - sepx 756 | } 757 | } 758 | if key.Pos[0] == 'i' { 759 | switch key.Pos[2] { 760 | case 'l': 761 | ld.KeyX = leftm + sepx 762 | case 'c': 763 | ld.KeyX = leftm + (width-kw)/2 764 | case 'r': 765 | ld.KeyX = leftm + width - kw - sepx 766 | } 767 | } 768 | } 769 | 770 | // fmt.Printf("width=%d, height=%d, leftm=%d, topm=%d (fw=%d)\n", width, height, leftm, topm, int(fw)) 771 | 772 | // Number of tics 773 | if width/int(fw) <= 20 { 774 | ld.NumXtics = 2 775 | } else { 776 | ld.NumXtics = width / int(10*fw) 777 | if ld.NumXtics > 25 { 778 | ld.NumXtics = 25 779 | } 780 | } 781 | ld.NumYtics = height / (4 * fh) 782 | if ld.NumYtics > 20 { 783 | ld.NumYtics = 20 784 | } 785 | 786 | ld.Width, ld.Height = width, height 787 | ld.Left, ld.Top = leftm, topm 788 | 789 | return 790 | } 791 | 792 | // DebugLogger is used to log some information about the chart generation. 793 | var DebugLogger *log.Logger = log.New(ioutil.Discard, "", 0) 794 | -------------------------------------------------------------------------------- /data.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Value is the interface for any type of data representable by a real. 8 | // Its standard implementation here is Real (float64). 9 | type Value interface { 10 | XVal() float64 11 | } 12 | 13 | // Real is a float64 implementing the Value interface. 14 | type Real float64 15 | 16 | func (r Real) XVal() float64 { return float64(r) } 17 | 18 | // XYValue is the interface for any type of data which is point-like and has 19 | // a x- and y-coordinate. Its standard implementation here is Point. 20 | type XYValue interface { 21 | XVal() float64 22 | YVal() float64 23 | } 24 | 25 | // Point is a point in two dimensions (x,y) implementing XYValue. 26 | type Point struct{ X, Y float64 } 27 | 28 | func (p Point) XVal() float64 { return p.X } 29 | func (p Point) YVal() float64 { return p.Y } 30 | func (p Point) XErr() (float64, float64) { return math.NaN(), math.NaN() } 31 | func (p Point) YErr() (float64, float64) { return math.NaN(), math.NaN() } 32 | 33 | // XYErrValue is the interface for any type of data which is point-like (x,y) and 34 | // has some measurement error. 35 | type XYErrValue interface { 36 | XVal() float64 37 | YVal() float64 38 | XErr() (float64, float64) // X-range [min,max], error intervall. Use NaN to indicate "no error". 39 | YErr() (float64, float64) // Y-range error interval (like XErr). 40 | } 41 | 42 | // EPoint represents a point in two dimensions (X,Y) with possible error ranges 43 | // in both dimensions. To faciliate common symetric errors, OffX/Y defaults to 0 and 44 | // only DeltaX/Y needs to be set up. 45 | type EPoint struct { 46 | X, Y float64 47 | DeltaX, DeltaY float64 // Full range of x and y error, NaN for no errorbar. 48 | OffX, OffY float64 // Offset of error range (must be < Delta) 49 | } 50 | 51 | func (p EPoint) XVal() float64 { return p.X } 52 | func (p EPoint) YVal() float64 { return p.Y } 53 | func (p EPoint) XErr() (float64, float64) { 54 | xl, _, xh, _ := p.BoundingBox() 55 | return xl, xh 56 | } 57 | func (p EPoint) YErr() (float64, float64) { 58 | _, yl, _, yh := p.BoundingBox() 59 | return yl, yh 60 | } 61 | func (p EPoint) BoundingBox() (xl, yl, xh, yh float64) { // bounding box 62 | xl, xh, yl, yh = p.X, p.X, p.Y, p.Y 63 | if !math.IsNaN(p.DeltaX) { 64 | xl -= p.DeltaX/2 - p.OffX 65 | xh += p.DeltaX/2 + p.OffX 66 | } 67 | if !math.IsNaN(p.DeltaY) { 68 | yl -= p.DeltaY/2 - p.OffY 69 | yh += p.DeltaY/2 + p.OffY 70 | } 71 | return 72 | } 73 | 74 | // CategoryValue is the interface for any type of data which is a category-real-pair. 75 | type CategoryValue interface { 76 | Category() string 77 | Value() float64 78 | Flaged() bool 79 | } 80 | 81 | // CatValue is the standard implementation for CategoryValue. 82 | type CatValue struct { 83 | Cat string 84 | Val float64 85 | Flag bool 86 | } 87 | 88 | func (c CatValue) Category() string { return c.Cat } 89 | func (c CatValue) Value() float64 { return c.Val } 90 | func (c CatValue) Flaged() bool { return c.Flag } 91 | 92 | // Box represents a box in an boxplot. 93 | type Box struct { 94 | X float64 // x-position of the box 95 | Avg float64 // "average" value (uncommon in std. box plots, but sometimes useful) 96 | Q1, Med, Q3 float64 // lower quartil, median and upper quartil 97 | Low, High float64 // low and hig end of whiskers (normaly last point in the 1.5*IQR range of Q1/3) 98 | Outliers []float64 // list of y-values of outliers 99 | } 100 | 101 | func (p Box) XVal() float64 { return p.X } 102 | func (p Box) YVal() float64 { return p.Med } 103 | func (p Box) XErr() float64 { return p.Med - p.Q1 } 104 | func (p Box) YErr() float64 { return p.Q3 - p.Med } 105 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package chart implements common chart/plot types. 3 | 4 | The following chart types are available: 5 | 6 | StripChart Visualize set of numeric values 7 | ScatterChart Plot (x,y) data (with optional error bars) 8 | and/or functions 9 | HistChart Produce histograms from data 10 | BarChart Show (x,y) data as bars 11 | BoxChart Box charts to visualize distributions 12 | PieChart Pie and Ring charts 13 | 14 | Chart tries to provides useful defaults and produce nice charts without 15 | sacrificing accuracy. The generated charts look good and are higly 16 | customizable but will not match the visual quality of handmade photoshop 17 | charts or the statistical features of charts produced by S or R. 18 | 19 | Creating charts consists of the following steps: 20 | 1. Create chart object 21 | 2. Configure chart, axis, autoscaling etc. 22 | 3. Add one ore more data sets 23 | 4. Render chart to one or more graphic outputs 24 | You may change the configuration at any step or render to different outputs. 25 | 26 | The different chart types and their fields are all simple struct types where 27 | the zero value provides suitable defaults. All fields are exported, even if 28 | you are not supposed to manipulate them directy or are 'output fields'. 29 | E.g. the common Data field of all chart types will store the sample data 30 | added with one or more Add... methods. Some fields are mere output 31 | which expose internal stuff for your use like the Data2Screen and Screen2Data 32 | functions of the Ranges. Some fields are even input/output fields: 33 | E.g. you may set the Range.TicSetting.Delta to some positive value which will 34 | be used as the spacing between tics on that axis; on the other hand if you 35 | leave Range.TicSetting.Delta at its default 0 you indicate to the plotting 36 | routine to automatically determine the tic delta which is then reported 37 | back in this fields. 38 | 39 | All charts (except pie/ring charts) contain at least one axis represented by 40 | a field of type Range. Axis can be differented into following categories: 41 | 42 | o Date/Time axis (Time=true): The values on this axis are interpreted as 43 | seconds since the Unix epoc, tics are labeld in date and time units. 44 | (The Log and Category fields are simply ignored for Date/Time axis.) 45 | 46 | o Real valued axis (Time=false). Those come in different flavours: 47 | - Simple linear real valued axis (Log=false, Category=nil). 48 | - Logrithmic axis (Log=True). Such an axis may cover only a 49 | range of ]0,inf[ 50 | - Categorical axis (Log=false, Category!=nil): 51 | The values 0, 1, 2, ... are labeled with the strings in Category. 52 | (You might want to set up the range manually, e.g. with the 53 | Fixed() method of Range) 54 | 55 | How the axis is autoscaled can be controlled for both ends of the axis 56 | individually by MinMode and MaxMode which allow a fine control of the 57 | (auto-) scaling. 58 | 59 | After setting up the chart, adding data, samples, functions you can render 60 | the chart to a Graphics output. This process will set several internal 61 | fields of the chart. If you reuse the chart, add additional data and 62 | output it again these fields might no longer indicate 'automatical/default' 63 | but contain the value calculated in the first output round. 64 | 65 | */ 66 | package chart 67 | -------------------------------------------------------------------------------- /example/bestof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/example/bestof.png -------------------------------------------------------------------------------- /freefont/AUTHORS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/AUTHORS -------------------------------------------------------------------------------- /freefont/CREDITS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/CREDITS -------------------------------------------------------------------------------- /freefont/ChangeLog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/ChangeLog -------------------------------------------------------------------------------- /freefont/INSTALL: -------------------------------------------------------------------------------- 1 | -*-mode:text;-*- 2 | $Id: INSTALL,v 1.1 2002/12/12 15:09:05 peterlin Exp $ 3 | 4 | 5 | Installing the Free UCS outline fonts 6 | ===================================== 7 | 8 | 9 | These installation notes are supposed to provide a helpful guidance 10 | through the process of installation of free UCS outline fonts. They 11 | can probably be significantly improved. Please direct your comments, 12 | suggestions for improvements, criticisms etc. to Primoz PETERLIN 13 | and thus help improve them. 14 | 15 | 16 | 1. UNIX/GNU/Linux/BSD Systems 17 | 18 | The rather awkward "UNIX/GNU/Linux/BSD" agglomeration is used to 19 | denote any system capable of running XFree86 server with FreeType 20 | , a high-quality free font rasterizer. 21 | 22 | 1.1 The rough way 23 | 24 | Unfortunately, hardly any other way exists at the moment. 25 | 26 | 1) Fetch the freefont-ttf.tar.gz package with Free UCS outline fonts 27 | in the TrueType format. 28 | 29 | 2) Unpack TrueType fonts into a suitable directory, 30 | e.g. /usr/share/fonts/default/TrueType/. 31 | 32 | 3) If you have chosen any other directory, make sure the directory you 33 | used to install the fonts is listed in the path searched by the X 34 | Font Server. Append the directory to the "catalogue=" in the 35 | /etc/X11/fs/config. 36 | 37 | 4) Run ttmkfdir in the directory where you unpacked the fonts. 38 | 39 | 40 | 1.2 Debian GNU/Linux 41 | 42 | Users of Debian GNU/Linux system will probably want to use the 43 | pre-packed Debian package, as available from the Debian site, 44 | , or 45 | any of its mirrors. You can install them by issuing the command 46 | 47 | apt-get install ttf-freefont 48 | 49 | 50 | 2. Microsoft Windows 95/98/NT/2000/XP 51 | 52 | To be written. 53 | 54 | 55 | 3. MacOS 56 | 57 | To be written. 58 | -------------------------------------------------------------------------------- /freefont/README: -------------------------------------------------------------------------------- 1 | -*-text-*- 2 | $Id: README,v 1.1 2002/11/28 10:10:30 peterlin Exp $ 3 | 4 | Summary: This project aims to privide a set of free scalable 5 | (PostScript Type0, TrueType, OpenType...) fonts covering the ISO 6 | 10646/Unicode UCS (Universal Character Set). 7 | 8 | 9 | Why do we need free scalable UCS fonts? 10 | 11 | A large number of free software users switched from free X11 12 | bitmapped fonts to proprietary Microsoft Truetype fonts, as a) they 13 | used to be freely downloaded from Microsoft Typography page 14 | , b) they contain a more 15 | or less decent subsed of the ISO 10646 UCS (Universal Character Set), 16 | c) they are high-quality, well hinted scalable Truetype fonts, and d) 17 | Freetype , a free high-quality Truetype font 18 | renderer exists and has been integrated into the latest release of 19 | XFree86, the free X11 server. 20 | 21 | Building a dependence on non-free software, even a niche one like 22 | fonts, is dangerous. Microsoft Truetype core fonts are not free, they 23 | are just costless. For now, at least. Citing the TrueType core fonts 24 | for the Web FAQ : 25 | "You may only redistribute the fonts in their original form (.exe or 26 | .sit.hqx) and with their original file name from your Web site or 27 | intranet site. You must not supply the fonts, or any derivative fonts 28 | based on them, in any form that adds value to commercial products, 29 | such as CD-ROM or disk based multimedia programs, application software 30 | or utilities." As of August 2002, however, the fonts are not 31 | anymore available on the Web, which makes the situation clearer. 32 | 33 | Aren't there any free high-quality scalable fonts? Yes, there are. 34 | URW++, a German digital typefoundry, released their own version of the 35 | 35 Postscript Type 1 core fonts under GPL as their donation to the 36 | Ghostscript project . The Wadalab 37 | Kanji comittee has produced Type 1 font files with thousands of 38 | filigree Japanese glyphs . 39 | Yannis Haralambous has drawn beautiful glyphs for the Omega 40 | typesetting system . And so 41 | on. Scattered around the internet there are numerous other free 42 | resources for other national scripts, many of them aiming to be a 43 | suitable match for Latin fonts like Times or Helvetica. 44 | 45 | 46 | What do we plan to achieve, and how? 47 | 48 | Our aim is to collect available resources, fill in the missing pieces, 49 | and provide a set of free high-quality scalable (Type 1 and Truetype) 50 | UCS fonts, released under GPL. 51 | 52 | Free UCS scalable fonts will cover the following character sets 53 | 54 | * ISO 8859 parts 1-15 55 | * CEN MES-3 European Unicode Subset 56 | http://www.evertype.com/standards/iso10646/pdf/cwa13873.pdf 57 | * IBM/Microsoft code pages 437, 850, 852, 1250, 1252 and more 58 | * Microsoft/Adobe Windows Glyph List 4 (WGL4) 59 | http://partners.adobe.com/asn/developer/opentype/appendices/wgl4.html 60 | * KOI8-R and KOI8-RU 61 | * DEC VT100 graphics symbols 62 | * International Phonetic Alphabet 63 | * Arabic, Hebrew, Armenian, Georgian, Ethiopian, Thai and Lao alphabets, 64 | including Arabic presentation forms A/B 65 | * Japanese Katakana and Hiragana 66 | * mathematical symbols, including the whole TeX repertoire of symbols 67 | * APL symbols 68 | etc. 69 | 70 | A free Postscript font editor, George Williams's Pfaedit 71 | will be used for creating new 72 | glyphs. 73 | 74 | Which font shapes should be made? As historical style terms like 75 | Renaissance or Baroque letterforms cannot be applied beyond 76 | Latin/Cyrillic/Greek scripts to any greater extent than Kufi or Nashki 77 | can be applied beyond Arabic script, a smaller subset of styles will 78 | be made: one monospaced and two proportional (one with uniform stroke 79 | and one with modulated) will be made at the start. 80 | 81 | In the beginning, however, we don't believe that Truetype hinting will 82 | be good enough to compete with neither the hand-crafted bitmapped 83 | fonts at small sizes, nor with commercial TrueType fonts. A companion 84 | program for modifying the TrueType font tables, TtfMod, is in the 85 | works, though: . For 86 | applications like xterm, users are referred to the existing UCS bitmap 87 | fonts, . 88 | 89 | 90 | What do the file suffices mean? 91 | 92 | The files with .sfd (Spline Font Database) are in PfaEdit's native 93 | format. Please use these if you plan to modify the font files. PfaEdit 94 | can export these to mostly any existing font file format. 95 | 96 | TrueType fonts for immediate consumption are the files with the .ttf 97 | (TrueType Font) suffix. You can use them directly, e.g. with the X 98 | font server. 99 | 100 | The files with .ps (PostScript) suffix are not font files at all - 101 | they are merely PostScript files with glyph tables, which can be used 102 | for overview, which glyphs are contained in which font file. 103 | 104 | You may have noticed the lacking of PostScript Type 1 (.pfa/.pfb) font 105 | files. Type 1 format does not support large (> 256) encoding vectors, 106 | so they can not be used with ISO 10646 encoding. If your printer 107 | supports it, you can use Type 0 format, though. Please use PfaEdit for 108 | conversion to Type 0. 109 | 110 | 111 | Primoz Peterlin, 112 | 113 | Free UCS scalable fonts: ftp://biofiz.mf.uni-lj.si/pub/fonts/elbrus/ 114 | -------------------------------------------------------------------------------- /freefont/sfd/FreeMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeMono.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeMonoBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeMonoBold.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeMonoBoldOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeMonoBoldOblique.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeMonoOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeMonoOblique.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeSans.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSansBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeSansBold.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSansBoldOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeSansBoldOblique.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSansOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeSansOblique.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSerif.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeSerif.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSerifBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeSerifBold.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSerifBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeSerifBoldItalic.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSerifItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdobler/chart/e4c9d5b5d37fda2b0da24262c141feb5e71c0578/freefont/sfd/FreeSerifItalic.ttf -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vdobler/chart 2 | 3 | require ( 4 | github.com/ajstarks/svgo v0.0.0-20181006003313-6ce6a3bcf6cd 5 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 6 | github.com/llgcode/draw2d v0.0.0-20180825133448-f52c8a71aff0 7 | golang.org/x/image v0.0.0-20181030002151-69cc3646b96e 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ajstarks/svgo v0.0.0-20181006003313-6ce6a3bcf6cd h1:JdtityihAc6A+gVfYh6vGXfZQg+XOLyBvla/7NbXFCg= 2 | github.com/ajstarks/svgo v0.0.0-20181006003313-6ce6a3bcf6cd/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 3 | github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= 4 | github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 5 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 6 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 7 | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 8 | github.com/llgcode/draw2d v0.0.0-20180825133448-f52c8a71aff0 h1:2vp6ESimuT8pCuZHThVyV0hlfa9oPL06HnGCL9pbUgc= 9 | github.com/llgcode/draw2d v0.0.0-20180825133448-f52c8a71aff0/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA= 10 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY= 11 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 12 | golang.org/x/image v0.0.0-20181030002151-69cc3646b96e h1:LpHV5J9Rec5OYn+RZFfNenrW109yUVSoKjGOgmKKhxE= 13 | golang.org/x/image v0.0.0-20181030002151-69cc3646b96e/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 14 | -------------------------------------------------------------------------------- /graphics.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "math" 7 | ) 8 | 9 | // MinimalGraphics is the interface any graphics driver must implement, 10 | // so that he can fall back to the generic routines for the higher level 11 | // outputs. 12 | type MinimalGraphics interface { 13 | Background() (r, g, b, a uint8) // Color of background 14 | FontMetrics(font Font) (fw float32, fh int, mono bool) // Return fontwidth and -height in pixel 15 | TextLen(t string, font Font) int // Length=width of t in screen units if set on font 16 | Line(x0, y0, x1, y1 int, style Style) // Draw line from (x0,y0) to (x1,y1) 17 | Text(x, y int, t string, align string, rot int, f Font) // Put t at (x,y) rotated by rot aligned [[tcb]][lcr] 18 | } 19 | 20 | // BasicGraphics is an interface of the most basic graphic primitives. 21 | // Any type which implements BasicGraphics can use generic implementations 22 | // of the Graphics methods. 23 | type BasicGraphics interface { 24 | MinimalGraphics 25 | Symbol(x, y int, style Style) // Put symbol s at (x,y) 26 | Rect(x, y, w, h int, style Style) // Draw (w x h) rectangle at (x,y) 27 | Wedge(x, y, ro, ri int, phi, psi float64, style Style) // Wedge 28 | Path(x, y []int, style Style) // Path of straight lines 29 | Options() PlotOptions // access to current PlotOptions 30 | } 31 | 32 | // Graphics is the interface all chart drivers have to implement 33 | type Graphics interface { 34 | BasicGraphics 35 | 36 | Dimensions() (int, int) // character-width / height 37 | 38 | Begin() // start of chart drawing 39 | End() // Done, cleanup 40 | 41 | // All stuff is preprocessed: sanitized, clipped, strings formated, integer coords, 42 | // screen coordinates, 43 | XAxis(xr Range, ys, yms int, options PlotOptions) // Draw x axis xr at screen position ys (and yms if mirrored) 44 | YAxis(yr Range, xs, xms int, options PlotOptions) // Same for y axis. 45 | 46 | Scatter(points []EPoint, plotstyle PlotStyle, style Style) // Points, Lines and Line+Points 47 | Boxes(boxes []Box, width int, style Style) // Boxplots 48 | Bars(bars []Barinfo, style Style) // any type of histogram/bars 49 | Rings(wedeges []Wedgeinfo, x, y, ro, ri int) // Pie/ring diagram elements 50 | 51 | Key(x, y int, key Key, options PlotOptions) // place key at x,y 52 | } 53 | 54 | // Barinfo describes a rectangular bar (e.g. in a histogram or a bar plot). 55 | type Barinfo struct { 56 | x, y int // (x,y) of top left corner; 57 | w, h int // width and heigt 58 | t, tp string // label text and text position '[oi][tblr]' or 'c' 59 | f Font // font of text 60 | } 61 | 62 | // Wedgeinfo describes a wedge in a pie chart. 63 | type Wedgeinfo struct { 64 | Phi, Psi float64 // Start and ende of wedge. Fuill circle if |phi-psi| > 4pi 65 | Text, Tp string // label text and text position: [ico] 66 | Style Style // style of this wedge 67 | Font Font // font of text 68 | Shift int // Highlighting of wedge 69 | } 70 | 71 | // GenericTextLen tries to determine the width in pixel of t if rendered into mg in using font. 72 | func GenericTextLen(mg MinimalGraphics, t string, font Font) (width int) { 73 | // TODO: how handle newlines? same way like Text does 74 | fw, _, mono := mg.FontMetrics(font) 75 | if mono { 76 | for _ = range t { 77 | width++ 78 | } 79 | width = int(float32(width)*fw + 0.5) 80 | } else { 81 | var length float32 82 | for _, r := range t { 83 | if w, ok := CharacterWidth[int(r)]; ok { 84 | length += w 85 | } else { 86 | length += 20 // save above average 87 | } 88 | } 89 | length /= averageCharacterWidth 90 | length *= fw 91 | width = int(length + 0.5) 92 | } 93 | return 94 | } 95 | 96 | // SanitizeRect returns the top left corner and the positive width and height of the 97 | // given (possibly unsanitized) rectangle taking into account the line width r. 98 | func SanitizeRect(x, y, w, h, r int) (int, int, int, int) { 99 | if w < 0 { 100 | x += w 101 | w = -w 102 | } 103 | if h < 0 { 104 | y += h 105 | h = -h 106 | } 107 | 108 | d := (imax(1, r) - 1) / 2 109 | // TODO: what if w-2D <= 0 ? 110 | return x + d, y + d, w - 2*d, h - 2*d 111 | } 112 | 113 | // GenericRect draws a rectangle of size w x h at (x,y). Drawing is done 114 | // by simple lines only. 115 | func GenericRect(mg MinimalGraphics, x, y, w, h int, style Style) { 116 | x, y, w, h = SanitizeRect(x, y, w, h, style.LineWidth) 117 | 118 | if style.FillColor != nil { 119 | fs := Style{LineWidth: 1, LineColor: style.FillColor, LineStyle: SolidLine} 120 | for i := 1; i < h; i++ { 121 | mg.Line(x+1, y+i, x+w-1, y+i, fs) 122 | } 123 | } 124 | 125 | mg.Line(x, y, x+w, y, style) 126 | mg.Line(x+w, y, x+w, y+h, style) 127 | mg.Line(x+w, y+h, x, y+h, style) 128 | mg.Line(x, y+h, x, y, style) 129 | } 130 | 131 | // GenericPath is the incomplete implementation of a list of points 132 | // connected by straight lines. Incomplete: Dashed lines won't work properly. 133 | func GenericPath(mg MinimalGraphics, x, y []int, style Style) { 134 | n := imin(len(x), len(y)) 135 | for i := 1; i < n; i++ { 136 | mg.Line(x[i-1], y[i-1], x[i], y[i], style) 137 | } 138 | } 139 | 140 | func drawXTics(bg BasicGraphics, rng Range, y, ym, ticLen int, options PlotOptions) { 141 | xe := rng.Data2Screen(rng.Max) 142 | 143 | // Grid below tics 144 | if rng.TicSetting.Grid > GridOff { 145 | for ticcnt, tic := range rng.Tics { 146 | x := rng.Data2Screen(tic.Pos) 147 | if ticcnt >= 0 && ticcnt <= len(rng.Tics)-1 && rng.TicSetting.Grid == GridLines { 148 | // fmt.Printf("Gridline at x=%d\n", x) 149 | bg.Line(x, y-1, x, ym+1, elementStyle(options, GridLineElement)) 150 | } else if rng.TicSetting.Grid == GridBlocks { 151 | if ticcnt%2 == 1 { 152 | x0 := rng.Data2Screen(rng.Tics[ticcnt-1].Pos) 153 | bg.Rect(x0, ym, x-x0, y-ym, elementStyle(options, GridBlockElement)) 154 | } else if ticcnt == len(rng.Tics)-1 && x < xe-1 { 155 | bg.Rect(x, ym, xe-x, y-ym, elementStyle(options, GridBlockElement)) 156 | } 157 | } 158 | } 159 | } 160 | 161 | // Tics on top 162 | ticstyle := elementStyle(options, MajorTicElement) 163 | ticfont := ticstyle.Font 164 | for _, tic := range rng.Tics { 165 | x := rng.Data2Screen(tic.Pos) 166 | lx := rng.Data2Screen(tic.LabelPos) 167 | 168 | // Tics 169 | switch rng.TicSetting.Tics { 170 | case 0: 171 | bg.Line(x, y-ticLen, x, y+ticLen, ticstyle) 172 | case 1: 173 | bg.Line(x, y-ticLen, x, y, ticstyle) 174 | case 2: 175 | bg.Line(x, y, x, y+ticLen, ticstyle) 176 | default: 177 | } 178 | 179 | // Mirrored Tics 180 | if rng.TicSetting.Mirror >= 2 { 181 | switch rng.TicSetting.Tics { 182 | case 0: 183 | bg.Line(x, ym-ticLen, x, ym+ticLen, ticstyle) 184 | case 1: 185 | bg.Line(x, ym, x, ym+ticLen, ticstyle) 186 | case 2: 187 | bg.Line(x, ym-ticLen, x, ym, ticstyle) 188 | default: 189 | } 190 | } 191 | 192 | if !rng.TicSetting.HideLabels { 193 | // Tic-Label 194 | if rng.Time && tic.Align == -1 { 195 | bg.Line(x, y+ticLen, x, y+2*ticLen, ticstyle) 196 | bg.Text(lx, y+2*ticLen, tic.Label, "tl", 0, ticfont) 197 | } else { 198 | bg.Text(lx, y+2*ticLen, tic.Label, "tc", 0, ticfont) 199 | } 200 | } 201 | } 202 | } 203 | 204 | // GenericXAxis draws the x-axis with range rng solely by graphic primitives of bg. 205 | // The x-axis is drawn at y on the screen and the mirrored x-axis is drawn at ym. 206 | func GenericXAxis(bg BasicGraphics, rng Range, y, ym int, options PlotOptions) { 207 | _, fontheight, _ := bg.FontMetrics(elementStyle(options, MajorTicElement).Font) 208 | var ticLen int = 0 209 | if !rng.TicSetting.Hide { 210 | ticLen = imin(12, imax(4, fontheight/2)) 211 | } 212 | xa, xe := rng.Data2Screen(rng.Min), rng.Data2Screen(rng.Max) 213 | 214 | // Axis label and range limits 215 | aly := y + 2*ticLen 216 | if !rng.TicSetting.Hide { 217 | aly += (3 * fontheight) / 2 218 | } 219 | if rng.ShowLimits { 220 | font := elementStyle(options, RangeLimitElement).Font 221 | if rng.Time { 222 | bg.Text(xa, aly, rng.TMin.Format("2006-01-02 15:04:05"), "tl", 0, font) 223 | bg.Text(xe, aly, rng.TMax.Format("2006-01-02 15:04:05"), "tr", 0, font) 224 | } else { 225 | bg.Text(xa, aly, fmt.Sprintf("%g", rng.Min), "tl", 0, font) 226 | bg.Text(xe, aly, fmt.Sprintf("%g", rng.Max), "tr", 0, font) 227 | } 228 | } 229 | if rng.Label != "" { // draw label _after_ (=over) range limits 230 | font := elementStyle(options, MajorAxisElement).Font 231 | bg.Text((xa+xe)/2, aly, " "+rng.Label+" ", "tc", 0, font) 232 | } 233 | 234 | // Tics and Grid 235 | if !rng.TicSetting.Hide { 236 | drawXTics(bg, rng, y, ym, ticLen, options) 237 | } 238 | 239 | // Axis itself, mirrord axis and zero 240 | bg.Line(xa, y, xe, y, elementStyle(options, MajorAxisElement)) 241 | if rng.TicSetting.Mirror >= 1 { 242 | bg.Line(xa, ym, xe, ym, elementStyle(options, MinorAxisElement)) 243 | } 244 | if rng.ShowZero && rng.Min < 0 && rng.Max > 0 { 245 | z := rng.Data2Screen(0) 246 | bg.Line(z, y, z, ym, elementStyle(options, ZeroAxisElement)) 247 | } 248 | 249 | } 250 | 251 | func drawYTics(bg BasicGraphics, rng Range, x, xm, ticLen int, options PlotOptions) { 252 | ye := rng.Data2Screen(rng.Max) 253 | 254 | // Grid below tics 255 | if rng.TicSetting.Grid > GridOff { 256 | for ticcnt, tic := range rng.Tics { 257 | y := rng.Data2Screen(tic.Pos) 258 | if rng.TicSetting.Grid == GridLines { 259 | if ticcnt > 0 && ticcnt < len(rng.Tics)-1 { 260 | // fmt.Printf("Gridline at x=%d\n", x) 261 | bg.Line(x+1, y, xm-1, y, elementStyle(options, GridLineElement)) 262 | } 263 | } else if rng.TicSetting.Grid == GridBlocks { 264 | if ticcnt%2 == 1 { 265 | y0 := rng.Data2Screen(rng.Tics[ticcnt-1].Pos) 266 | bg.Rect(x, y0, xm-x, y-y0, elementStyle(options, GridBlockElement)) 267 | } else if ticcnt == len(rng.Tics)-1 && y > ye+1 { 268 | bg.Rect(x, ye, xm-x, y-ye, elementStyle(options, GridBlockElement)) 269 | } 270 | } 271 | } 272 | } 273 | 274 | // Tics on top 275 | ticstyle := elementStyle(options, MajorTicElement) 276 | ticfont := ticstyle.Font 277 | for _, tic := range rng.Tics { 278 | y := rng.Data2Screen(tic.Pos) 279 | ly := rng.Data2Screen(tic.LabelPos) 280 | 281 | // Tics 282 | switch rng.TicSetting.Tics { 283 | case 0: 284 | bg.Line(x-ticLen, y, x+ticLen, y, ticstyle) 285 | case 1: 286 | bg.Line(x, y, x+ticLen, y, ticstyle) 287 | case 2: 288 | bg.Line(x-ticLen, y, x, y, ticstyle) 289 | default: 290 | } 291 | 292 | // Mirrored tics 293 | if rng.TicSetting.Mirror >= 2 { 294 | switch rng.TicSetting.Tics { 295 | case 0: 296 | bg.Line(xm-ticLen, y, xm+ticLen, y, ticstyle) 297 | case 1: 298 | bg.Line(xm-ticLen, y, xm, y, ticstyle) 299 | case 2: 300 | bg.Line(xm, y, xm+ticLen, y, ticstyle) 301 | default: 302 | } 303 | } 304 | 305 | if !rng.TicSetting.HideLabels { 306 | // Label 307 | if rng.Time && tic.Align == 0 { // centered tic 308 | bg.Line(x-2*ticLen, y, x+ticLen, y, ticstyle) 309 | bg.Text(x-ticLen, ly, tic.Label, "cr", 0, ticfont) 310 | } else { 311 | bg.Text(x-2*ticLen, ly, tic.Label, "cr", 0, ticfont) 312 | } 313 | } 314 | } 315 | 316 | } 317 | 318 | // GenericYAxis draws the y-axis with the range rng solely by graphic primitives of bg. 319 | // The y.axis and the mirrord y-axis are drawn at x and ym respectively. 320 | func GenericYAxis(bg BasicGraphics, rng Range, x, xm int, options PlotOptions) { 321 | font := elementStyle(options, MajorAxisElement).Font 322 | _, fontheight, _ := bg.FontMetrics(font) 323 | var ticLen int = 0 324 | if !rng.TicSetting.Hide { 325 | ticLen = imin(10, imax(4, fontheight/2)) 326 | } 327 | ya, ye := rng.Data2Screen(rng.Min), rng.Data2Screen(rng.Max) 328 | 329 | // Label and axis ranges 330 | alx := 2 * fontheight 331 | if rng.ShowLimits { 332 | /* TODO 333 | st := bg.Style("rangelimit") 334 | if rng.Time { 335 | bg.Text(xa, aly, rng.TMin.Format("2006-01-02 15:04:05"), "tl", 0, st) 336 | bg.Text(xe, aly, rng.TMax.Format("2006-01-02 15:04:05"), "tr", 0, st) 337 | } else { 338 | bg.Text(xa, aly, fmt.Sprintf("%g", rng.Min), "tl", 0, st) 339 | bg.Text(xe, aly, fmt.Sprintf("%g", rng.Max), "tr", 0, st) 340 | } 341 | */ 342 | } 343 | if rng.Label != "" { 344 | y := (ya + ye) / 2 345 | bg.Text(alx, y, rng.Label, "bc", 90, font) 346 | } 347 | 348 | if !rng.TicSetting.Hide { 349 | drawYTics(bg, rng, x, xm, ticLen, options) 350 | } 351 | 352 | // Axis itself, mirrord axis and zero 353 | bg.Line(x, ya, x, ye, elementStyle(options, MajorAxisElement)) 354 | if rng.TicSetting.Mirror >= 1 { 355 | bg.Line(xm, ya, xm, ye, elementStyle(options, MinorAxisElement)) 356 | } 357 | if rng.ShowZero && rng.Min < 0 && rng.Max > 0 { 358 | z := rng.Data2Screen(0) 359 | bg.Line(x, z, xm, z, elementStyle(options, ZeroAxisElement)) 360 | } 361 | 362 | } 363 | 364 | // GenericScatter draws the given points according to style. 365 | // style.FillColor is used as color of error bars and style.FontSize is used 366 | // as the length of the endmarks of the error bars. Both have suitable defaults 367 | // if the FontXyz are not set. Point coordinates and errors must be provided 368 | // in screen coordinates. 369 | func GenericScatter(bg BasicGraphics, points []EPoint, plotstyle PlotStyle, style Style) { 370 | 371 | // First pass: Error bars 372 | ebs := style 373 | ebs.LineColor, ebs.LineWidth, ebs.LineStyle = ebs.FillColor, 1, SolidLine 374 | if ebs.LineColor == nil { 375 | ebs.LineColor = color.NRGBA{0x40, 0x40, 0x40, 0xff} 376 | } 377 | if ebs.LineWidth == 0 { 378 | ebs.LineWidth = 1 379 | } 380 | for _, p := range points { 381 | 382 | xl, yl, xh, yh := p.BoundingBox() 383 | // fmt.Printf("Draw %d: %f %f-%f; %f %f-%f\n", i, p.DeltaX, xl,xh, p.DeltaY, yl,yh) 384 | if !math.IsNaN(p.DeltaX) { 385 | bg.Line(int(xl), int(p.Y), int(xh), int(p.Y), ebs) 386 | } 387 | if !math.IsNaN(p.DeltaY) { 388 | // fmt.Printf(" Draw %d,%d to %d,%d\n",int(p.X), int(yl), int(p.X), int(yh)) 389 | bg.Line(int(p.X), int(yl), int(p.X), int(yh), ebs) 390 | } 391 | } 392 | 393 | // Second pass: Line 394 | if (plotstyle&PlotStyleLines) != 0 && len(points) > 0 { 395 | lastx, lasty := int(points[0].X), int(points[0].Y) 396 | for i := 1; i < len(points); i++ { 397 | x, y := int(points[i].X), int(points[i].Y) 398 | bg.Line(lastx, lasty, x, y, style) 399 | lastx, lasty = x, y 400 | } 401 | } 402 | 403 | // Third pass: symbols 404 | if (plotstyle&PlotStylePoints) != 0 && len(points) != 0 { 405 | for _, p := range points { 406 | // fmt.Printf("Point %d at %d,%d\n", i, int(p.X), int(p.Y)) 407 | bg.Symbol(int(p.X), int(p.Y), style) 408 | } 409 | } 410 | } 411 | 412 | // GenericBoxes draws box plots. (Default implementation for box plots). 413 | // The values for each box in boxes are in screen coordinates! 414 | func GenericBoxes(bg BasicGraphics, boxes []Box, width int, style Style) { 415 | if width%2 == 0 { 416 | width++ 417 | } 418 | hbw := (width - 1) / 2 419 | for _, d := range boxes { 420 | x := int(d.X) 421 | q1, q3 := int(d.Q1), int(d.Q3) 422 | // DebugLogger.Printf("q1=%d q3=%d q3-q1=%d", q1,q3,q3-q1) 423 | bg.Rect(x-hbw, q1, width, q3-q1, style) 424 | if !math.IsNaN(d.Med) { 425 | med := int(d.Med) 426 | bg.Line(x-hbw, med, x+hbw, med, style) 427 | } 428 | 429 | if !math.IsNaN(d.Avg) { 430 | bg.Symbol(x, int(d.Avg), style) 431 | } 432 | 433 | if !math.IsNaN(d.High) { 434 | bg.Line(x, q3, x, int(d.High), style) 435 | } 436 | 437 | if !math.IsNaN(d.Low) { 438 | bg.Line(x, q1, x, int(d.Low), style) 439 | } 440 | 441 | for _, y := range d.Outliers { 442 | bg.Symbol(x, int(y), style) 443 | } 444 | 445 | } 446 | 447 | } 448 | 449 | // GenericBars draws the bars in the given style using bg. 450 | // TODO: Is Bars and Generic Bars useful at all? Replaceable by rect? 451 | func GenericBars(bg BasicGraphics, bars []Barinfo, style Style) { 452 | for _, b := range bars { 453 | bg.Rect(b.x, b.y, b.w, b.h, style) 454 | if b.t != "" { 455 | var tx, ty int 456 | var a string 457 | _, fh, _ := bg.FontMetrics(b.f) 458 | if fh > 1 { 459 | fh /= 2 460 | } 461 | switch b.tp { 462 | case "ot": 463 | tx, ty, a = b.x+b.w/2, b.y-fh, "bc" 464 | case "it": 465 | tx, ty, a = b.x+b.w/2, b.y+fh, "tc" 466 | case "ib": 467 | tx, ty, a = b.x+b.w/2, b.y+b.h-fh, "bc" 468 | case "ob": 469 | tx, ty, a = b.x+b.w/2, b.y+b.h+fh, "tc" 470 | case "ol": 471 | tx, ty, a = b.x-fh, b.y+b.h/2, "cr" 472 | case "il": 473 | tx, ty, a = b.x+fh, b.y+b.h/2, "cl" 474 | case "or": 475 | tx, ty, a = b.x+b.w+fh, b.y+b.h/2, "cl" 476 | case "ir": 477 | tx, ty, a = b.x+b.w-fh, b.y+b.h/2, "cr" 478 | default: 479 | tx, ty, a = b.x+b.w/2, b.y+b.h/2, "cc" 480 | 481 | } 482 | 483 | bg.Text(tx, ty, b.t, a, 0, b.f) 484 | } 485 | } 486 | } 487 | 488 | // GenericWedge draws a pie/wedge just by lines 489 | func GenericWedge(mg MinimalGraphics, x, y, ro, ri int, phi, psi, ecc float64, style Style) { 490 | for phi < 0 { 491 | phi += 2 * math.Pi 492 | } 493 | for psi < 0 { 494 | psi += 2 * math.Pi 495 | } 496 | for phi >= 2*math.Pi { 497 | phi -= 2 * math.Pi 498 | } 499 | for psi >= 2*math.Pi { 500 | psi -= 2 * math.Pi 501 | } 502 | // DebugLogger.Printf("GenericWedge centered at (%d,%d) from %.1f° to %.1f°, radius %d/%d (e=%.2f)", x, y, 180*phi/math.Pi, 180*psi/math.Pi, ro, ri, ecc) 503 | 504 | if ri > ro { 505 | panic("ri > ro is not possible") 506 | } 507 | 508 | if style.FillColor != nil { 509 | fillWedge(mg, x, y, ro, ri, phi, psi, ecc, style) 510 | } 511 | 512 | roe, rof := float64(ro)*ecc, float64(ro) 513 | rie, rif := float64(ri)*ecc, float64(ri) 514 | xa, ya := int(math.Cos(phi)*roe)+x, y-int(math.Sin(phi)*rof) 515 | xc, yc := int(math.Cos(psi)*roe)+x, y-int(math.Sin(psi)*rof) 516 | xai, yai := int(math.Cos(phi)*rie)+x, y-int(math.Sin(phi)*rif) 517 | xci, yci := int(math.Cos(psi)*rie)+x, y-int(math.Sin(psi)*rif) 518 | 519 | if math.Abs(phi-psi) >= 4*math.Pi { 520 | phi, psi = 0, 2*math.Pi 521 | } else { 522 | if ri > 0 { 523 | mg.Line(xai, yai, xa, ya, style) 524 | mg.Line(xci, yci, xc, yc, style) 525 | } else { 526 | mg.Line(x, y, xa, ya, style) 527 | mg.Line(x, y, xc, yc, style) 528 | } 529 | } 530 | 531 | var xb, yb int 532 | exit := phi < psi 533 | for rho := phi; !exit || rho < psi; rho += 0.05 { // aproximate circle by more than 120 corners polygon 534 | if rho >= 2*math.Pi { 535 | exit = true 536 | rho -= 2 * math.Pi 537 | } 538 | xb, yb = int(math.Cos(rho)*roe)+x, y-int(math.Sin(rho)*rof) 539 | mg.Line(xa, ya, xb, yb, style) 540 | xa, ya = xb, yb 541 | } 542 | mg.Line(xb, yb, xc, yc, style) 543 | 544 | if ri > 0 { 545 | exit := phi < psi 546 | for rho := phi; !exit || rho < psi; rho += 0.1 { // aproximate circle by more than 60 corner polygon 547 | if rho >= 2*math.Pi { 548 | exit = true 549 | rho -= 2 * math.Pi 550 | } 551 | xb, yb = int(math.Cos(rho)*rie)+x, y-int(math.Sin(rho)*rif) 552 | mg.Line(xai, yai, xb, yb, style) 553 | xai, yai = xb, yb 554 | } 555 | mg.Line(xb, yb, xci, yci, style) 556 | 557 | } 558 | } 559 | 560 | // Fill wedge with center (xi,yi), radius ri from alpha to beta with style. 561 | // Precondition: 0 <= beta < alpha < pi/2 562 | func fillQuarterWedge(mg MinimalGraphics, xi, yi, ri int, alpha, beta, e float64, style Style, quadrant int) { 563 | if alpha < beta { 564 | // DebugLogger.Printf("Swaping alpha and beta") 565 | alpha, beta = beta, alpha 566 | } 567 | // DebugLogger.Printf("fillQuaterWedge from %.1f to %.1f radius %d in quadrant %d.", 180*alpha/math.Pi, 180*beta/math.Pi, ri, quadrant) 568 | r := float64(ri) 569 | 570 | ta, tb := math.Tan(alpha), math.Tan(beta) 571 | for y := int(r * math.Sin(alpha)); y >= 0; y-- { 572 | yf := float64(y) 573 | x0 := yf / ta 574 | x1 := yf / tb 575 | x2 := math.Sqrt(r*r - yf*yf) 576 | // DebugLogger.Printf("y=%d x0=%.2f x1=%.2f x2=%.2f border=%t", y, x0, x1, x2, (x2 0.01 { 649 | fillQuarterWedge(mg, xi, yi, ro, mapQ(phi, qPhi), mapQ(w, qPhi), epsilon, style, qPhi) 650 | if ri > 0 { 651 | fillQuarterWedge(mg, xi, yi, ri, mapQ(phi, qPhi), mapQ(w, qPhi), epsilon, blank, qPhi) 652 | } 653 | } 654 | phi = w 655 | qPhi++ 656 | if qPhi == 4 { 657 | // DebugLogger.Printf("Wrapped phi around") 658 | phi, qPhi = 0, 0 659 | } 660 | } 661 | if phi != psi { 662 | // DebugLogger.Printf("Last wedge") 663 | fillQuarterWedge(mg, xi, yi, ro, mapQ(phi, qPhi), mapQ(psi, qPhi), epsilon, style, qPhi) 664 | if ri > 0 { 665 | fillQuarterWedge(mg, xi, yi, ri, mapQ(phi, qPhi), mapQ(psi, qPhi), epsilon, blank, qPhi) 666 | } 667 | } 668 | } 669 | 670 | // GeenricRings draws wedges for pie/ring charts charts. The pie's/ring's center is at (x,y) 671 | // with ri and ro the inner and outer diameter. Eccentricity allows to correct for non-square 672 | // pixels (e.g. in text mode). 673 | func GenericRings(bg BasicGraphics, wedges []Wedgeinfo, x, y, ro, ri int, eccentricity float64) { 674 | // DebugLogger.Printf("GenericRings with %d wedges center %d,%d, radii %d/%d, ecc=%.3f)", len(wedges), x, y, ro, ri, eccentricity) 675 | 676 | for _, w := range wedges { 677 | 678 | // Correct center 679 | d := float64(w.Style.LineWidth) / 2 680 | 681 | // cphi, sphi := math.Cos(w.Phi), math.Sin(w.Phi) 682 | // cpsi, spsi := math.Cos(w.Psi), math.Sin(w.Psi) 683 | delta := (w.Psi - w.Phi) / 2 684 | SinDelta := math.Sin(delta) 685 | gamma := (w.Phi + w.Psi) / 2 686 | k := d / SinDelta 687 | shift := float64(w.Shift) 688 | kx, ky := (k+shift)*math.Cos(gamma), (k+shift)*math.Sin(gamma) 689 | 690 | DebugLogger.Printf("Center adjustment (lw=%d, d=%.2f), for wedge %d°-%d° of (%.1f,%.1f), k=%.1f", 691 | w.Style.LineWidth, d, int(180*w.Phi/math.Pi), int(180*w.Psi/math.Pi), kx, ky, k) 692 | 693 | xi, yi := x+int(kx+0.5), y+int(ky+0.5) 694 | roc, ric := ro-int(d+k), ri-int(d+k) 695 | bg.Wedge(xi, yi, roc, ric, w.Phi, w.Psi, w.Style) 696 | 697 | if w.Text != "" { 698 | _, fh, _ := bg.FontMetrics(w.Font) 699 | fh += 0 700 | alpha := (w.Phi + w.Psi) / 2 701 | var rt int 702 | if ri > 0 { 703 | rt = (ri + ro) / 2 704 | } else { 705 | rt = ro - 3*fh 706 | if rt <= ro/2 { 707 | rt = ro - 2*fh 708 | } 709 | } 710 | // DebugLogger.Printf("Text %s at %d° r=%d", w.Text, int(180*alpha/math.Pi), rt) 711 | tx := int(float64(rt)*math.Cos(alpha)*eccentricity+0.5) + x 712 | ty := y + int(float64(rt)*math.Sin(alpha)+0.5) 713 | 714 | bg.Text(tx, ty, w.Text, "cc", 0, w.Font) 715 | } 716 | 717 | } 718 | 719 | } 720 | 721 | // GenericCircle approximates a circle of radius r around (x,y) with lines. 722 | func GenericCircle(bg BasicGraphics, x, y, r int, style Style) { 723 | // TODO: fill 724 | x0, y0 := x+r, y 725 | rf := float64(r) 726 | for a := 0.2; a < 2*math.Pi; a += 0.2 { 727 | x1, y1 := int(rf*math.Cos(a))+x, int(rf*math.Sin(a))+y 728 | bg.Line(x0, y0, x1, y1, style) 729 | x0, y0 = x1, y1 730 | } 731 | } 732 | 733 | func polygon(bg BasicGraphics, x, y []int, style Style) { 734 | n := len(x) - 1 735 | for i := 0; i < n; i++ { 736 | bg.Line(x[i], y[i], x[i+1], y[i+1], style) 737 | } 738 | bg.Line(x[n], y[n], x[0], y[0], style) 739 | } 740 | 741 | // GenericSymbol draws the symbol defined by style at (x,y). 742 | func GenericSymbol(bg BasicGraphics, x, y int, style Style) { 743 | f := style.SymbolSize 744 | if f == 0 { 745 | f = 1 746 | } 747 | if style.LineWidth <= 0 { 748 | style.LineWidth = 1 749 | } 750 | 751 | if style.SymbolColor == nil { 752 | style.SymbolColor = style.LineColor 753 | if style.SymbolColor == nil { 754 | style.SymbolColor = style.FillColor 755 | if style.SymbolColor == nil { 756 | style.SymbolColor = color.NRGBA{0, 0, 0, 0xff} 757 | } 758 | } 759 | } 760 | 761 | style.LineColor = style.SymbolColor 762 | 763 | const n = 5 // default size 764 | a := int(n*f + 0.5) // standard 765 | b := int(n/2*f + 0.5) // smaller 766 | c := int(1.155*n*f + 0.5) // triangel long sist 767 | d := int(0.577*n*f + 0.5) // triangle short dist 768 | e := int(0.866*n*f + 0.5) // diagonal 769 | 770 | switch style.Symbol { 771 | case '*': 772 | bg.Line(x-e, y-e, x+e, y+e, style) 773 | bg.Line(x-e, y+e, x+e, y-e, style) 774 | fallthrough 775 | case '+': 776 | bg.Line(x-a, y, x+a, y, style) 777 | bg.Line(x, y-a, x, y+a, style) 778 | case 'X': 779 | bg.Line(x-e, y-e, x+e, y+e, style) 780 | bg.Line(x-e, y+e, x+e, y-e, style) 781 | case 'o': 782 | GenericCircle(bg, x, y, a, style) 783 | case '0': 784 | GenericCircle(bg, x, y, a, style) 785 | GenericCircle(bg, x, y, b, style) 786 | case '.': 787 | GenericCircle(bg, x, y, b, style) 788 | case '@': 789 | GenericCircle(bg, x, y, a, style) 790 | for r := 1; r < a; r++ { 791 | GenericCircle(bg, x, y, r, style) 792 | } 793 | bg.Line(x, y, x, y, style) 794 | case '=': 795 | bg.Rect(x-e, y-e, 2*e, 2*e, style) 796 | case '#': 797 | style.FillColor = style.LineColor 798 | bg.Rect(x-e, y-e, 2*e, 2*e, style) 799 | case 'A': 800 | polygon(bg, []int{x - a, x + a, x}, []int{y + d, y + d, y - c}, style) 801 | for j := 1; j < a; j++ { 802 | aa, dd, cc := (j*a)/a, (j*d)/a, (j*c)/a 803 | polygon(bg, []int{x - aa, x + aa, x}, []int{y + dd, y + dd, y - cc}, style) 804 | } 805 | case '%': 806 | polygon(bg, []int{x - a, x + a, x}, []int{y + d, y + d, y - c}, style) 807 | case 'W': 808 | polygon(bg, []int{x - a, x + a, x}, []int{y - c, y - c, y + d}, style) 809 | for j := 1; j < a; j++ { 810 | aa, dd, cc := (j*a)/a, (j*d)/a, (j*c)/a 811 | polygon(bg, []int{x - aa, x + aa, x}, []int{y - cc, y - cc, y + dd}, style) 812 | } 813 | case 'V': 814 | polygon(bg, []int{x - a, x + a, x}, []int{y - c, y - c, y + d}, style) 815 | case 'Z': 816 | polygon(bg, []int{x - e, x, x + e, x}, []int{y, y + e, y, y - e}, style) 817 | for j := 1; j < e; j++ { 818 | ee := (j * e) / e 819 | polygon(bg, []int{x - ee, x, x + ee, x}, []int{y, y + ee, y, y - ee}, style) 820 | } 821 | case '&': 822 | polygon(bg, []int{x - e, x, x + e, x}, []int{y, y + e, y, y - e}, style) 823 | default: 824 | bg.Text(x, y, "?", "cc", 0, Font{}) 825 | } 826 | 827 | } 828 | 829 | func drawTitle(g Graphics, text string, style Style) { 830 | w, _ := g.Dimensions() 831 | _, fh, _ := g.FontMetrics(style.Font) 832 | x, y := w/2, fh/3 833 | g.Text(x, y, text, "tc", 0, style.Font) 834 | } 835 | -------------------------------------------------------------------------------- /graphics_test.go: -------------------------------------------------------------------------------- 1 | package chart_test 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "math" 7 | "testing" 8 | 9 | "github.com/vdobler/chart" 10 | "github.com/vdobler/chart/txtg" 11 | ) 12 | 13 | const r = 18 14 | const ri = 10 15 | 16 | func initDemoCircle() *txtg.TextGraphics { 17 | g := txtg.New(60, 40) 18 | g.Line(0, 20, 59, 20, chart.Style{Symbol: '-'}) 19 | g.Line(30, 0, 30, 39, chart.Style{Symbol: '|'}) 20 | for p := 0.0; p <= 2*math.Pi; p += 0.1 { 21 | x := int(r * math.Cos(p) * 1.5) 22 | y := int(r * math.Sin(p)) 23 | g.Symbol(30+x, 20+y, chart.Style{Symbol: '*'}) 24 | } 25 | return g 26 | } 27 | 28 | func TestGenericWedge(t *testing.T) { 29 | g := initDemoCircle() 30 | red, blue := color.RGBA{0xff, 0, 0, 0}, color.RGBA{0, 0, 0xff, 0} 31 | s := chart.Style{Symbol: '#', FillColor: red, LineColor: blue} 32 | ra := math.Pi / 2 33 | 34 | chart.GenericWedge(g, 30, 20, r, ri, 0.15*ra, 0.5*ra, 1.5, s) 35 | fmt.Printf("\n%s\n", g.String()) 36 | 37 | chart.GenericWedge(g, 30, 20, r, ri, 1.15*ra, 1.5*ra, 1.5, s) 38 | fmt.Printf("\n%s\n", g.String()) 39 | 40 | chart.GenericWedge(g, 30, 20, r, ri, 2.15*ra, 2.5*ra, 1.5, s) 41 | fmt.Printf("\n%s\n", g.String()) 42 | 43 | chart.GenericWedge(g, 30, 20, r, ri, 3.15*ra, 3.5*ra, 1.5, s) 44 | fmt.Printf("\n%s\n", g.String()) 45 | 46 | // mored than one quadrant 47 | g = initDemoCircle() 48 | chart.GenericWedge(g, 30, 20, r, ri, 0.15*ra, 1.5*ra, 1.5, s) 49 | fmt.Printf("\n%s\n", g.String()) 50 | 51 | chart.GenericWedge(g, 30, 20, r, ri, 2.15*ra, 3.5*ra, 1.5, s) 52 | fmt.Printf("\n%s\n", g.String()) 53 | 54 | g = initDemoCircle() 55 | chart.GenericWedge(g, 30, 20, r, ri, 1.5*ra, 2.5*ra, 1.5, s) 56 | fmt.Printf("\n%s\n", g.String()) 57 | 58 | // all 4 quadrants 59 | g = initDemoCircle() 60 | chart.GenericWedge(g, 30, 20, r, ri, 1.5*ra, 0.5*ra, 1.5, s) 61 | fmt.Printf("\n%s\n", g.String()) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /hist.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | ) 7 | 8 | // HistChart represents histogram charts. 9 | // 10 | // Histograms should not be mixed up with bar charts produced by BarChart: 11 | // Histograms are computed (binified) automatically from the raw 12 | // data. 13 | type HistChart struct { 14 | XRange, YRange Range // Lower limit of YRange is fixed to 0 and not available for input 15 | Title string // Title of chart 16 | Key Key // Key/Legend 17 | Counts bool // Display counts instead of frequencies 18 | Stacked bool // Display different data sets ontop of each other 19 | Shifted bool // Shift non-stacked bars sideways (and make them smaler) 20 | FirstBin float64 // center of the first (lowest bin) 21 | BinWidth float64 // Width of bins (0: auto) 22 | TBinWidth TimeDelta // BinWidth for time XRange 23 | Gap float64 // gap between bins in (bin-width units): 0<=Gap<1, 24 | Sep float64 // separation of bars in one bin (in bar width units) -1= -1 && x < 1 { 47 | return 0.5 48 | } 49 | return 0 50 | } 51 | 52 | // 1 - |x| 53 | TriangularKernel = func(x float64) float64 { 54 | if x >= -1 && x < 1 { 55 | return 1 - math.Abs(x) 56 | } 57 | return 0 58 | } 59 | 60 | // 15/16 * (1-x^2)^2 61 | BisquareKernel Kernel = func(x float64) float64 { 62 | if x >= -1 && x < 1 { 63 | a := (1 - x*x) 64 | return 15.0 / 16.0 * a * a 65 | } 66 | return 0 67 | } 68 | 69 | // 35/32 * (1-x^2)^3 70 | TriweightKernel Kernel = func(x float64) float64 { 71 | if x >= -1 && x < 1 { 72 | a := (1 - x*x) 73 | return 35.0 / 32.0 * a * a * a 74 | } 75 | return 0 76 | } 77 | 78 | // 3/4 * (1-x^2) 79 | EpanechnikovKernel Kernel = func(x float64) float64 { 80 | if x >= -1 && x < 1 { 81 | return 3.0 / 4.0 * (1.0 - x*x) 82 | } 83 | return 0 84 | } 85 | 86 | // 1/sqrt(2pi) * exp(-1/2x^2) 87 | GaussKernel Kernel = func(x float64) float64 { 88 | return sqrt2piinv * math.Exp(-0.5*x*x) 89 | } 90 | ) 91 | 92 | // AddData will add data to the plot. Legend will be updated by name. 93 | func (c *HistChart) AddData(name string, data []float64, style Style) { 94 | // Style 95 | if style.empty() { 96 | style = AutoStyle(len(c.Data), true) 97 | } 98 | 99 | // Init axis, add data, autoscale 100 | if len(c.Data) == 0 { 101 | c.XRange.init() 102 | } 103 | c.Data = append(c.Data, HistChartData{name, style, data}) 104 | for _, d := range data { 105 | c.XRange.autoscale(d) 106 | } 107 | 108 | // Key/Legend 109 | if name != "" { 110 | c.Key.Entries = append(c.Key.Entries, KeyEntry{Text: name, Style: style, PlotStyle: PlotStyleBox}) 111 | } 112 | } 113 | 114 | // AddDataInt is a convenience method to add integer data (a simple wrapper 115 | // around AddData). 116 | func (c *HistChart) AddDataInt(name string, data []int, style Style) { 117 | fdata := make([]float64, len(data)) 118 | for i, d := range data { 119 | fdata[i] = float64(d) 120 | } 121 | c.AddData(name, fdata, style) 122 | } 123 | 124 | // AddDataGeneric is the generic version which allows the addition of any type 125 | // implementing the Value interface. 126 | func (c *HistChart) AddDataGeneric(name string, data []Value, style Style) { 127 | fdata := make([]float64, len(data)) 128 | for i, d := range data { 129 | fdata[i] = d.XVal() 130 | } 131 | c.AddData(name, fdata, style) 132 | } 133 | 134 | // G = B * Gf; S = W *Sf 135 | // W = (B(1-Gf))/(N-(N-1)Sf) 136 | // S = (B(1-Gf))/(N/Sf - (N-1)) 137 | // N Gf Sf 138 | // 2 1/4 1/3 139 | // 3 1/5 1/2 140 | // 4 1/6 2/3 141 | // 5 1/6 3/4 142 | func (c *HistChart) widthFactor() (gf, sf float64) { 143 | if c.Stacked || !c.Shifted { 144 | gf = c.Gap 145 | sf = -1 146 | return 147 | } 148 | 149 | switch len(c.Data) { 150 | case 1: 151 | gf = c.Gap 152 | sf = -1 153 | return 154 | case 2: 155 | gf = 1.0 / 4.0 156 | sf = -1.0 / 3.0 157 | case 3: 158 | gf = 1.0 / 5.0 159 | sf = -1.0 / 2.0 160 | case 4: 161 | gf = 1.0 / 6.0 162 | sf = -2.0 / 3.0 163 | default: 164 | gf = 1.0 / 6.0 165 | sf = -2.0 / 4.0 166 | } 167 | 168 | if c.Gap != 0 { 169 | gf = c.Gap 170 | } 171 | if c.Sep != 0 { 172 | sf = c.Sep 173 | } 174 | return 175 | } 176 | 177 | // Prepare binCnt bins of width binWidth starting from binStart and count 178 | // data samples per bin for each data set. If c.Counts is true than the 179 | // absolute counts are returned instead if the frequencies. max is the 180 | // largest y-value which will occur in our plot. 181 | func (c *HistChart) binify(binStart, binWidth float64, binCnt int) (freqs [][]float64, max float64) { 182 | x2bin := func(x float64) int { return int((x - binStart) / binWidth) } 183 | 184 | freqs = make([][]float64, len(c.Data)) // freqs[d][b] is frequency/count of bin b in dataset d 185 | max = 0 186 | for i, data := range c.Data { 187 | freq := make([]float64, binCnt) 188 | drops := 0 189 | for _, x := range data.Samples { 190 | bin := x2bin(x) 191 | if bin < 0 || bin >= binCnt { 192 | // fmt.Printf("!!!!! Lost %.3f (bin=%d)\n", x, bin) 193 | drops++ 194 | continue 195 | } 196 | freq[bin] = freq[bin] + 1 197 | //fmt.Printf("Value %.2f sorted into bin %d, count now %d\n", x, bin, int(freq[bin])) 198 | } 199 | // scale if requested and determine max 200 | n := float64(len(data.Samples) - drops) 201 | // DebugLogger.Printf("Dataset %d has %d samples (by %d drops).\n", i, int(n), drops) 202 | ff := 0.0 203 | for bin := 0; bin < binCnt; bin++ { 204 | if !c.Counts { 205 | freq[bin] = 100 * freq[bin] / n 206 | } 207 | ff += freq[bin] 208 | if freq[bin] > max { 209 | max = freq[bin] 210 | } 211 | } 212 | freqs[i] = freq 213 | } 214 | // DebugLogger.Printf("Maximum : %.2f\n", max) 215 | if c.Stacked { // recalculate max 216 | max = 0 217 | for bin := 0; bin < binCnt; bin++ { 218 | sum := 0.0 219 | for i := range freqs { 220 | sum += freqs[i][bin] 221 | } 222 | // fmt.Printf("sum of bin %d = %d\n", bin, sum) 223 | if sum > max { 224 | max = sum 225 | } 226 | } 227 | // DebugLogger.Printf("Re-Maxed (stacked) to: %.2f\n", max) 228 | } 229 | return 230 | } 231 | 232 | func (c *HistChart) findBinWidth() { 233 | bw := c.XRange.TicSetting.Delta 234 | if bw == 0 { // this should not happen... 235 | bw = 1 236 | } 237 | 238 | // Average sample count (n) and "optimum" bin count obc 239 | n := 0 240 | for _, data := range c.Data { 241 | for _, x := range data.Samples { 242 | // Count only data in valid x-range. 243 | if x >= c.XRange.Min && x <= c.XRange.Max { 244 | n++ 245 | } 246 | } 247 | } 248 | n /= len(c.Data) 249 | obc := math.Sqrt(float64(n)) 250 | // DebugLogger.Printf("Average size of %d data sets: %d (obc=%d)\n", len(c.Data), n, int(obc+0.5)) 251 | 252 | // Increase/decrease bin width if tic delta yields massively bad choice 253 | binCnt := int((c.XRange.Max-c.XRange.Min)/bw + 0.5) 254 | if binCnt >= int(2*obc) { 255 | bw *= 2 // TODO: not so nice if bw is of form 2*10^n (use 2.5 in this case to match tics) 256 | //DebugLogger.Printf("Increased bin width to %.3f (optimum bin cnt = %d, was %d).\n", bw, int(obc+0.5), binCnt) 257 | } else if binCnt < int(3*obc) { 258 | bw /= 2 259 | // DebugLogger.Printf("Reduced bin width to %.3f (optimum bin cnt = %d, was %d).\n", bw, int(obc+0.5), binCnt) 260 | } else { 261 | // DebugLogger.Printf("Bin width of %.3f is ok (optimum bin cnt = %d, was %d).\n", bw, int(obc+0.5), binCnt) 262 | } 263 | 264 | c.BinWidth = bw 265 | } 266 | 267 | // Reset chart to state before plotting. 268 | func (c *HistChart) Reset() { 269 | c.XRange.Reset() 270 | c.YRange.Reset() 271 | } 272 | 273 | // Plot will output the chart to the graphic device g. 274 | func (c *HistChart) Plot(g Graphics) { 275 | layout := layout(g, c.Title, c.XRange.Label, c.YRange.Label, 276 | c.XRange.TicSetting.Hide || c.XRange.TicSetting.HideLabels, 277 | c.YRange.TicSetting.Hide || c.YRange.TicSetting.HideLabels, 278 | &c.Key) 279 | fw, fh, _ := g.FontMetrics(elementStyle(c.Options, MajorAxisElement).Font) 280 | 281 | width, height := layout.Width, layout.Height 282 | topm, leftm := layout.Top, layout.Left 283 | numxtics, numytics := layout.NumXtics, layout.NumYtics 284 | 285 | // Outside bound ranges for histograms are nicer 286 | leftm += int(2 * fw) 287 | width -= int(2 * fw) 288 | height -= int(fh) 289 | 290 | c.XRange.Setup(numxtics, numxtics+4, width, leftm, false) 291 | 292 | // TODO(vodo) a) BinWidth might be input, alignment to tics should be nice, binCnt, ... 293 | if c.BinWidth == 0 { 294 | c.findBinWidth() 295 | } 296 | 297 | xmin, _ := c.XRange.Min, c.XRange.Max 298 | binStart := c.BinWidth * math.Ceil(xmin/c.BinWidth) 299 | c.FirstBin = binStart + c.BinWidth/2 300 | binCnt := int(math.Floor(c.XRange.Max-binStart) / c.BinWidth) 301 | // DebugLogger.Printf("Using %d bins from %.3f to %.3f width %.3f (xrange: %.3f--%.3f)\n", binCnt, binStart, binStart+c.BinWidth*float64(binCnt), c.BinWidth, xmin, xmax) 302 | counts, max := c.binify(binStart, c.BinWidth, binCnt) 303 | 304 | // Calculate smoothed density plots and re-max y. 305 | var smoothed [][]EPoint 306 | if !c.Stacked && c.Kernel != nil { 307 | smoothed = make([][]EPoint, len(c.Data)) 308 | for d := range c.Data { 309 | p, m := c.smoothed(d, binCnt) 310 | smoothed[d] = p 311 | if m > max { 312 | max = m 313 | } 314 | } 315 | } 316 | 317 | // Fix lower end of y axis 318 | c.YRange.DataMin = 0 319 | c.YRange.MinMode.Fixed = true 320 | c.YRange.MinMode.Value = 0 321 | c.YRange.autoscale(float64(max)) 322 | c.YRange.Setup(numytics, numytics+2, height, topm, true) 323 | 324 | g.Begin() 325 | 326 | if c.Title != "" { 327 | drawTitle(g, c.Title, elementStyle(c.Options, TitleElement)) 328 | } 329 | 330 | g.XAxis(c.XRange, topm+height+fh, topm, c.Options) 331 | g.YAxis(c.YRange, leftm-int(2*fw), leftm+width, c.Options) 332 | 333 | xf := c.XRange.Data2Screen 334 | yf := c.YRange.Data2Screen 335 | 336 | numSets := len(c.Data) 337 | n := float64(numSets) 338 | gf, sf := c.widthFactor() 339 | 340 | ww := c.BinWidth * (1 - gf) // w' 341 | var w, s float64 342 | if !c.Stacked && c.Shifted { 343 | w = ww / (n + (n-1)*sf) 344 | s = w * sf 345 | } else { 346 | w = ww 347 | s = -ww 348 | } 349 | 350 | // DebugLogger.Printf("gf=%.3f, sf=%.3f, bw=%.3f ===> ww=%.2f, w=%.2f, s=%.2f\n", gf, sf, c.BinWidth, ww, w, s) 351 | 352 | if c.Shifted || c.Stacked { 353 | for d := numSets - 1; d >= 0; d-- { 354 | bars := make([]Barinfo, 0, binCnt) 355 | ws := 0 356 | for b := 0; b < binCnt; b++ { 357 | if counts[d][b] == 0 { 358 | continue 359 | } 360 | xb := binStart + (float64(b)+0.5)*c.BinWidth 361 | x := xb - ww/2 + float64(d)*(s+w) 362 | xs := xf(x) 363 | xss := xf(x + w) 364 | ws = xss - xs 365 | thebar := Barinfo{x: xs, w: xss - xs} 366 | 367 | off := 0.0 368 | if c.Stacked { 369 | for dd := d - 1; dd >= 0; dd-- { 370 | off += counts[dd][b] 371 | } 372 | } 373 | a, aa := yf(float64(off+counts[d][b])), yf(float64(off)) 374 | thebar.y, thebar.h = a, iabs(a-aa) 375 | bars = append(bars, thebar) 376 | } 377 | g.Bars(bars, c.Data[d].Style) 378 | 379 | if !c.Stacked && sf < 0 && gf != 0 && fh > 1 { 380 | // Whitelining 381 | lw := 1 382 | if ws > 25 { 383 | lw = 2 384 | } 385 | white := Style{LineColor: color.NRGBA{0xff, 0xff, 0xff, 0xff}, LineWidth: lw, LineStyle: SolidLine} 386 | for _, b := range bars { 387 | g.Line(b.x, b.y-1, b.x+b.w+1, b.y-1, white) 388 | g.Line(b.x+b.w+1, b.y-1, b.x+b.w+1, b.y+b.h, white) 389 | } 390 | } 391 | } 392 | 393 | } else { 394 | bars := make([]Barinfo, 1) 395 | order := make([]int, numSets) 396 | for b := 0; b < binCnt; b++ { 397 | // shame on me... 398 | for d := 0; d < numSets; d++ { 399 | order[d] = d 400 | } 401 | for d := 0; d < numSets; d++ { 402 | for p := 0; p < numSets-1; p++ { 403 | if counts[order[p]][b] < counts[order[p+1]][b] { 404 | order[p], order[p+1] = order[p+1], order[p] 405 | } 406 | } 407 | } 408 | for d := 0; d < numSets; d++ { 409 | if counts[order[d]][b] == 0 { 410 | continue 411 | } 412 | xb := binStart + (float64(b)+0.5)*c.BinWidth 413 | x := xb - ww/2 + float64(d)*(s+w) 414 | xs := xf(x) 415 | xss := xf(x + w) 416 | thebar := Barinfo{x: xs, w: xss - xs} 417 | 418 | a, aa := yf(float64(counts[order[d]][b])), yf(0) 419 | thebar.y, thebar.h = a, iabs(a-aa) 420 | bars[0] = thebar 421 | g.Bars(bars, c.Data[order[d]].Style) 422 | } 423 | } 424 | } 425 | 426 | if !c.Stacked && c.Kernel != nil { 427 | for d := numSets - 1; d >= 0; d-- { 428 | style := Style{Symbol: /*c.Data[d].Style.Symbol*/ 'X', LineColor: c.Data[d].Style.LineColor, 429 | LineWidth: 1, LineStyle: SolidLine} 430 | for j := range smoothed[d] { 431 | // now YRange is set up: transform to screen coordinates 432 | smoothed[d][j].Y = float64(c.YRange.Data2Screen(smoothed[d][j].Y)) 433 | } 434 | g.Scatter(smoothed[d], PlotStyleLines, style) 435 | } 436 | } 437 | 438 | if !c.Key.Hide { 439 | g.Key(layout.KeyX, layout.KeyY, c.Key, c.Options) 440 | } 441 | g.End() 442 | } 443 | 444 | // Smooth data set i. The Y-value of the returned points is not jet in screen coordinates 445 | // but in data coordinates! (Reason: YRange not set up jet) 446 | func (c *HistChart) smoothed(i, binCnt int) (points []EPoint, max float64) { 447 | nan := math.NaN() 448 | 449 | samples := imax(25, binCnt*5) 450 | 451 | step := (c.XRange.Max - c.XRange.Min) / float64(samples) 452 | points = make([]EPoint, 0, 50) 453 | h := c.BinWidth 454 | K := c.Kernel 455 | n := float64(len(c.Data[i].Samples)) 456 | 457 | for x := c.XRange.Min; x <= c.XRange.Max; x += step { 458 | f := 0.0 459 | for _, xi := range c.Data[i].Samples { 460 | f += K((x - xi) / h) 461 | } 462 | f /= h 463 | if !c.Counts { 464 | f /= n 465 | f *= 100 // as display is in % 466 | } 467 | 468 | // Rescale kernel density estimation by width of bars: 469 | f *= c.BinWidth 470 | if f > max { 471 | max = f 472 | } 473 | xx := float64(c.XRange.Data2Screen(x)) 474 | // yy := float64(c.YRange.Data2Screen(f)) 475 | // fmt.Printf("Consructed %.3f, %.4f\n", x, f) 476 | points = append(points, EPoint{X: xx, Y: f, DeltaX: nan, DeltaY: nan}) 477 | } 478 | // fmt.Printf("Dataset %d: ff=%.4f\n", i, ff) 479 | 480 | return 481 | } 482 | -------------------------------------------------------------------------------- /imgg/image.go: -------------------------------------------------------------------------------- 1 | package imgg 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "log" 7 | "math" 8 | 9 | "github.com/golang/freetype" 10 | "github.com/golang/freetype/truetype" 11 | "github.com/llgcode/draw2d" 12 | "github.com/llgcode/draw2d/draw2dimg" 13 | "github.com/vdobler/chart" 14 | "golang.org/x/image/draw" 15 | "golang.org/x/image/math/f64" 16 | "golang.org/x/image/math/fixed" 17 | ) 18 | 19 | var ( 20 | dpi = 72.0 21 | defaultFont *truetype.Font 22 | ) 23 | 24 | func init() { 25 | var err error 26 | defaultFont, err = freetype.ParseFont(defaultFontData()) 27 | if err != nil { 28 | panic(err) 29 | } 30 | } 31 | 32 | // ImageGraphics writes plot to an image.RGBA 33 | type ImageGraphics struct { 34 | Image *image.RGBA // The image the plots are drawn onto. 35 | x0, y0 int 36 | w, h int 37 | bg color.RGBA 38 | gc draw2d.GraphicContext 39 | font *truetype.Font 40 | fs map[chart.FontSize]float64 41 | } 42 | 43 | // New creates a new ImageGraphics including an image.RGBA of dimension w x h 44 | // with background bgcol. If font is nil it will use a builtin font. 45 | // If fontsize is empty useful default are used. 46 | func New(width, height int, bgcol color.RGBA, font *truetype.Font, fontsize map[chart.FontSize]float64) *ImageGraphics { 47 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 48 | gc := draw2dimg.NewGraphicContext(img) 49 | gc.SetLineJoin(draw2d.BevelJoin) 50 | gc.SetLineCap(draw2d.SquareCap) 51 | gc.SetStrokeColor(image.Black) 52 | gc.SetFillColor(bgcol) 53 | gc.Translate(0.5, 0.5) 54 | gc.Clear() 55 | if font == nil { 56 | font = defaultFont 57 | } 58 | if len(fontsize) == 0 { 59 | fontsize = ConstructFontSizes(13) 60 | } 61 | return &ImageGraphics{Image: img, x0: 0, y0: 0, w: width, h: height, 62 | bg: bgcol, gc: gc, font: font, fs: fontsize} 63 | } 64 | 65 | // AddTo returns a new ImageGraphics which will write to (width x height) sized 66 | // area starting at (x,y) on the provided image img. The rest of the parameters 67 | // are the same as in New(). 68 | func AddTo(img *image.RGBA, x, y, width, height int, bgcol color.RGBA, font *truetype.Font, fontsize map[chart.FontSize]float64) *ImageGraphics { 69 | gc := draw2dimg.NewGraphicContext(img) 70 | gc.SetLineJoin(draw2d.BevelJoin) 71 | gc.SetLineCap(draw2d.SquareCap) 72 | gc.SetStrokeColor(image.Black) 73 | gc.SetFillColor(bgcol) 74 | gc.Translate(float64(x)+0.5, float64(y)+0.5) 75 | gc.ClearRect(x, y, x+width, y+height) 76 | if font == nil { 77 | font = defaultFont 78 | } 79 | if len(fontsize) == 0 { 80 | fontsize = ConstructFontSizes(13) 81 | } 82 | 83 | return &ImageGraphics{Image: img, x0: x, y0: y, w: width, h: height, bg: bgcol, gc: gc, font: font, fs: fontsize} 84 | } 85 | 86 | func (ig *ImageGraphics) Options() chart.PlotOptions { return nil } 87 | 88 | func (ig *ImageGraphics) Begin() {} 89 | func (ig *ImageGraphics) End() {} 90 | 91 | func (ig *ImageGraphics) Background() (r, g, b, a uint8) { return ig.bg.R, ig.bg.G, ig.bg.B, ig.bg.A } 92 | func (ig *ImageGraphics) Dimensions() (int, int) { return ig.w, ig.h } 93 | func (ig *ImageGraphics) FontMetrics(font chart.Font) (fw float32, fh int, mono bool) { 94 | h := ig.relFontsizeToPixel(font.Size) 95 | // typical width is 0.6 * height 96 | fw = float32(0.6 * h) 97 | fh = int(h + 0.5) 98 | mono = true 99 | return 100 | } 101 | func (ig *ImageGraphics) TextLen(s string, font chart.Font) int { 102 | c := freetype.NewContext() 103 | c.SetDPI(dpi) 104 | c.SetFont(ig.font) 105 | fontsize := ig.relFontsizeToPixel(font.Size) 106 | c.SetFontSize(fontsize) 107 | 108 | // really draw it 109 | width, err := c.DrawString(s, freetype.Pt(0, 0)) 110 | if err != nil { 111 | return 10 * len(s) // BUG 112 | } 113 | return int(width.X+32)>>6 + 1 114 | } 115 | 116 | func (ig *ImageGraphics) setStyle(style chart.Style) { 117 | ig.gc.SetStrokeColor(style.LineColor) 118 | ig.gc.SetLineWidth(float64(style.LineWidth)) 119 | orig := dashPattern[style.LineStyle] 120 | pattern := make([]float64, len(orig)) 121 | copy(pattern, orig) 122 | for i := range pattern { 123 | pattern[i] *= math.Sqrt(float64(style.LineWidth)) 124 | } 125 | ig.gc.SetLineDash(pattern, 0) 126 | } 127 | 128 | func (ig *ImageGraphics) Line(x0, y0, x1, y1 int, style chart.Style) { 129 | if style.LineWidth <= 0 { 130 | style.LineWidth = 1 131 | } 132 | ig.setStyle(style) 133 | ig.gc.MoveTo(float64(x0), float64(y0)) 134 | ig.gc.LineTo(float64(x1), float64(y1)) 135 | ig.gc.Stroke() 136 | } 137 | 138 | var dashPattern map[chart.LineStyle][]float64 = map[chart.LineStyle][]float64{ 139 | chart.SolidLine: nil, // []float64{10}, 140 | chart.DashedLine: []float64{10, 4}, 141 | chart.DottedLine: []float64{4, 3}, 142 | chart.DashDotDotLine: []float64{10, 3, 3, 3, 3, 3}, 143 | chart.LongDashLine: []float64{10, 8}, 144 | chart.LongDotLine: []float64{4, 8}, 145 | } 146 | 147 | func (ig *ImageGraphics) Path(x, y []int, style chart.Style) { 148 | ig.setStyle(style) 149 | ig.gc.MoveTo(float64(x[0]), float64(y[0])) 150 | for i := 1; i < len(x); i++ { 151 | ig.gc.LineTo(float64(x[i]), float64(y[i])) 152 | } 153 | ig.gc.Stroke() 154 | } 155 | 156 | func (ig *ImageGraphics) relFontsizeToPixel(rel chart.FontSize) float64 { 157 | if s, ok := ig.fs[rel]; ok { 158 | return s 159 | } 160 | return 12 161 | } 162 | 163 | func ConstructFontSizes(basesize float64) map[chart.FontSize]float64 { 164 | size := make(map[chart.FontSize]float64) 165 | for rs := int(chart.TinyFontSize); rs <= int(chart.HugeFontSize); rs++ { 166 | size[chart.FontSize(rs)] = basesize * math.Pow(1.2, float64(rs)) 167 | } 168 | return size 169 | } 170 | 171 | func (ig *ImageGraphics) Text(x, y int, t string, align string, rot int, f chart.Font) { 172 | if len(align) == 1 { 173 | align = "c" + align 174 | } 175 | 176 | var col color.Color 177 | if f.Color != nil { 178 | col = f.Color 179 | } else { 180 | col = color.RGBA{0, 0, 0, 0xff} 181 | } 182 | 183 | textImage := ig.textBox(t, f, col) 184 | bounds := textImage.Bounds() 185 | w, h := bounds.Dx(), bounds.Dy() 186 | var centerX, centerY int 187 | 188 | if rot != 0 { 189 | alpha := float64(rot) / 180 * math.Pi 190 | cos := math.Cos(alpha) 191 | sin := math.Sin(alpha) 192 | 193 | ax, ay := float64(w), float64(h) // anchor point 194 | switch align[0] { 195 | case 'b': 196 | case 'c': 197 | ay /= 2 198 | case 't': 199 | ay = 0 200 | } 201 | switch align[1] { 202 | case 'l': 203 | ax = 0 204 | case 'c': 205 | ax /= 2 206 | case 'r': 207 | } 208 | dx := float64(ax)*cos + float64(ay)*sin 209 | dy := -float64(ax)*sin + float64(ay)*cos 210 | trans := f64.Aff3{ 211 | +cos, +sin, float64(x+ig.x0) - dx, 212 | -sin, +cos, float64(y+ig.y0) - dy, 213 | } 214 | draw.BiLinear.Transform(ig.Image, trans, 215 | textImage, textImage.Bounds(), draw.Over, nil) 216 | return 217 | } else { 218 | centerX, centerY = w/2, h/2 219 | switch align[0] { 220 | case 'b': 221 | centerY = h 222 | case 't': 223 | centerY = 0 224 | } 225 | switch align[1] { 226 | case 'l': 227 | centerX = 0 228 | case 'r': 229 | centerX = w 230 | } 231 | } 232 | 233 | bounds = textImage.Bounds() 234 | w, h = bounds.Dx(), bounds.Dy() 235 | x -= centerX 236 | y -= centerY 237 | x += ig.x0 238 | y += ig.y0 239 | 240 | tcol := image.NewUniform(col) 241 | draw.DrawMask(ig.Image, image.Rect(x, y, x+w, y+h), tcol, image.ZP, 242 | textImage, textImage.Bounds().Min, draw.Over) 243 | } 244 | 245 | // textBox renders t into a tight fitting image 246 | func (ig *ImageGraphics) textBox(t string, font chart.Font, textCol color.Color) image.Image { 247 | // Initialize the context. 248 | bg := image.NewUniform(color.Alpha{0}) 249 | fg := image.NewUniform(textCol) 250 | width := ig.TextLen(t, font) 251 | size := ig.relFontsizeToPixel(font.Size) 252 | 253 | c := freetype.NewContext() 254 | c.SetDPI(dpi) 255 | c.SetFont(ig.font) 256 | c.SetFontSize(size) 257 | bb := ig.font.Bounds(c.PointToFixed(float64(size))) 258 | bbDelta := bb.Max.Sub(bb.Min) 259 | 260 | height := int(bbDelta.Y+32) >> 6 261 | canvas := image.NewRGBA(image.Rect(0, 0, width, height)) 262 | draw.Draw(canvas, canvas.Bounds(), bg, image.ZP, draw.Src) 263 | c.SetDst(canvas) 264 | c.SetSrc(fg) 265 | c.SetClip(canvas.Bounds()) 266 | // Draw the text. 267 | extent, err := c.DrawString(t, fixed.Point26_6{X: 0, Y: bb.Max.Y}) 268 | if err != nil { 269 | log.Println(err) 270 | return nil 271 | } 272 | 273 | // Ugly heuristic hack: font bounds are pretty high resulting in white top border: Trim. 274 | topskip := 1 275 | if size > 15 { 276 | topskip = 2 277 | } else if size > 20 { 278 | topskip = 3 279 | } 280 | return canvas.SubImage(image.Rect(0, topskip, int(extent.X)>>6, height)) 281 | } 282 | 283 | func (ig *ImageGraphics) paint(x, y int, R, G, B uint32, alpha uint32) { 284 | r, g, b, a := ig.Image.At(x, y).RGBA() 285 | r >>= 8 286 | g >>= 8 287 | b >>= 8 288 | a >>= 8 289 | r *= alpha 290 | g *= alpha 291 | b *= alpha 292 | a *= alpha 293 | r += R * (0xff - alpha) 294 | g += G * (0xff - alpha) 295 | b += B * (0xff - alpha) 296 | r >>= 8 297 | g >>= 8 298 | b >>= 8 299 | a >>= 8 300 | ig.Image.Set(x, y, color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}) 301 | } 302 | 303 | func (ig *ImageGraphics) Symbol(x, y int, style chart.Style) { 304 | chart.GenericSymbol(ig, x, y, style) 305 | } 306 | 307 | func (ig *ImageGraphics) Rect(x, y, w, h int, style chart.Style) { 308 | ig.setStyle(style) 309 | stroke := func() { ig.gc.Stroke() } 310 | if style.FillColor != nil { 311 | ig.gc.SetFillColor(style.FillColor) 312 | stroke = func() { ig.gc.FillStroke() } 313 | } 314 | ig.gc.MoveTo(float64(x), float64(y)) 315 | ig.gc.LineTo(float64(x+w), float64(y)) 316 | ig.gc.LineTo(float64(x+w), float64(y+h)) 317 | ig.gc.LineTo(float64(x), float64(y+h)) 318 | ig.gc.LineTo(float64(x), float64(y)) 319 | stroke() 320 | } 321 | 322 | func (ig *ImageGraphics) Wedge(ix, iy, iro, iri int, phi, psi float64, style chart.Style) { 323 | ig.setStyle(style) 324 | stroke := func() { ig.gc.Stroke() } 325 | if style.FillColor != nil { 326 | ig.gc.SetFillColor(style.FillColor) 327 | stroke = func() { ig.gc.FillStroke() } 328 | } 329 | 330 | ecc := 1.0 // eccentricity 331 | x, y := float64(ix), float64(iy) // center as float 332 | ro, ri := float64(iro), float64(iri) // radius outer and inner as float 333 | roe, rie := ro*ecc, ri*ecc // inner and outer radius corrected by ecc 334 | 335 | xao, yao := math.Cos(phi)*roe+x, y+math.Sin(phi)*ro 336 | // xco, yco := math.Cos(psi)*roe+x, y-math.Sin(psi)*ro 337 | xai, yai := math.Cos(phi)*rie+x, y+math.Sin(phi)*ri 338 | xci, yci := math.Cos(psi)*rie+x, y+math.Sin(psi)*ri 339 | 340 | // outbound straight line 341 | if ri > 0 { 342 | ig.gc.MoveTo(xai, yai) 343 | } else { 344 | ig.gc.MoveTo(x, y) 345 | } 346 | ig.gc.LineTo(xao, yao) 347 | 348 | // outer arc 349 | ig.gc.ArcTo(x, y, ro, roe, phi, psi-phi) 350 | 351 | // inbound straight line 352 | if ri > 0 { 353 | ig.gc.LineTo(xci, yci) 354 | ig.gc.ArcTo(x, y, ri, rie, psi, phi-psi) 355 | } else { 356 | ig.gc.LineTo(x, y) 357 | } 358 | stroke() 359 | } 360 | 361 | func (ig *ImageGraphics) XAxis(xr chart.Range, ys, yms int, options chart.PlotOptions) { 362 | chart.GenericXAxis(ig, xr, ys, yms, options) 363 | } 364 | func (ig *ImageGraphics) YAxis(yr chart.Range, xs, xms int, options chart.PlotOptions) { 365 | chart.GenericYAxis(ig, yr, xs, xms, options) 366 | } 367 | 368 | func (ig *ImageGraphics) Scatter(points []chart.EPoint, plotstyle chart.PlotStyle, style chart.Style) { 369 | chart.GenericScatter(ig, points, plotstyle, style) 370 | } 371 | 372 | func (ig *ImageGraphics) Boxes(boxes []chart.Box, width int, style chart.Style) { 373 | chart.GenericBoxes(ig, boxes, width, style) 374 | } 375 | 376 | func (ig *ImageGraphics) Key(x, y int, key chart.Key, options chart.PlotOptions) { 377 | chart.GenericKey(ig, x, y, key, options) 378 | } 379 | 380 | func (ig *ImageGraphics) Bars(bars []chart.Barinfo, style chart.Style) { 381 | chart.GenericBars(ig, bars, style) 382 | } 383 | 384 | func (ig *ImageGraphics) Rings(wedges []chart.Wedgeinfo, x, y, ro, ri int) { 385 | chart.GenericRings(ig, wedges, x, y, ro, ri, 1) 386 | } 387 | 388 | func min(a, b int) int { 389 | if a < b { 390 | return a 391 | } 392 | return b 393 | } 394 | 395 | func max(a, b int) int { 396 | if a > b { 397 | return a 398 | } 399 | return b 400 | } 401 | 402 | func abs(a int) int { 403 | if a < 0 { 404 | return -a 405 | } 406 | return a 407 | } 408 | 409 | func sign(a int) int { 410 | if a < 0 { 411 | return -1 412 | } 413 | if a == 0 { 414 | return 0 415 | } 416 | return 1 417 | } 418 | 419 | var _ chart.Graphics = &ImageGraphics{} 420 | -------------------------------------------------------------------------------- /imgg/image_test.go: -------------------------------------------------------------------------------- 1 | package imgg 2 | 3 | import ( 4 | "github.com/vdobler/chart" 5 | "image" 6 | "image/color" 7 | "image/png" 8 | "log" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func marker(i *image.RGBA, x, y int) { 14 | red := color.RGBA{0xff, 0x00, 0x00, 0xff} 15 | n := 15 16 | for xx := x - n; xx <= x+n; xx++ { 17 | i.Set(xx, y, red) 18 | } 19 | for yy := y - n; yy <= y+n; yy++ { 20 | i.Set(x, yy, red) 21 | } 22 | } 23 | 24 | func TestText(t *testing.T) { 25 | g := New(400, 200, color.RGBA{220, 220, 220, 255}, nil, 12) 26 | g.Text(2, 2, "Hallo (tl)", "tl", 0, chart.Font{}) 27 | g.Text(200, 2, "schöne (tc)", "tc", 0, chart.Font{}) 28 | g.Text(398, 2, "Welt (tr)", "tr", 0, chart.Font{}) 29 | 30 | g.Text(2, 100, "Hallo (cl)", "cl", 0, chart.Font{}) 31 | g.Text(200, 100, "schöne (cc)", "cc", 0, chart.Font{}) 32 | g.Text(398, 100, "Welt (cr)", "cr", 0, chart.Font{}) 33 | 34 | g.Text(2, 198, "Hallo (bl)", "bl", 0, chart.Font{}) 35 | g.Text(200, 198, "schöne (bc)", "bc", 0, chart.Font{}) 36 | g.Text(398, 198, "Welt (br)", "br", 0, chart.Font{}) 37 | 38 | marker(g.Image, 100, 50) 39 | marker(g.Image, 300, 50) 40 | marker(g.Image, 100, 150) 41 | marker(g.Image, 300, 150) 42 | 43 | g.Text(100, 50, "XcXcX", "cc", 45, chart.Font{}) 44 | g.Text(300, 50, "XbXlX", "bl", 90, chart.Font{}) 45 | g.Text(100, 150, "XbXcX", "bc", 90, chart.Font{}) 46 | g.Text(300, 150, "XbXrX", "br", 90, chart.Font{}) 47 | 48 | marker(g.Image, 200, 50) 49 | marker(g.Image, 200, 150) 50 | g.Text(200, 50, "HcHlH", "cl", 90, chart.Font{}) 51 | g.Text(200, 150, "HcHrH", "cr", 90, chart.Font{}) 52 | 53 | file, err := os.Create("text.png") 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | png.Encode(file, g.Image) 58 | file.Close() 59 | } 60 | -------------------------------------------------------------------------------- /key.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Key encapsulates settings for keys/legends in a chart. 8 | // 9 | // Key placement is governed by Pos which may take the following values: 10 | // otl otc otr 11 | // +-------------+ o: outside 12 | // olt |itl itc itr| ort i: inside 13 | // | | t: top 14 | // olc |icl icc icr| orc c: centered 15 | // | | b: bottom 16 | // olb |ibl ibc ibr| orb l: left 17 | // +-------------+ c: centered 18 | // obl obc obr r: right 19 | // 20 | type Key struct { 21 | Hide bool // Don't show key/legend if true 22 | Cols int // Number of colums to use. If <0 fill rows before colums 23 | Border int // -1: off, 0: std, 1...:other styles 24 | Pos string // default "" is "itr" 25 | Entries []KeyEntry // List of entries in the legend 26 | X, Y int 27 | } 28 | 29 | // KeyEntry encapsulates an antry in the key/legend. 30 | type KeyEntry struct { 31 | Text string // Text to display 32 | PlotStyle PlotStyle // What to show: symbol, line, bar or combination thereof 33 | Style Style // How to show 34 | 35 | } 36 | 37 | // Place layouts the Entries in key in the requested (by key.Cols) matrix format 38 | func (key Key) Place() (matrix [][]*KeyEntry) { 39 | // count real entries in num, see if multilines are present in haveml 40 | num := 0 41 | for _, e := range key.Entries { 42 | if e.Text == "" { 43 | continue 44 | } 45 | num++ 46 | } 47 | if num == 0 { 48 | return // no entries 49 | } 50 | 51 | rowfirst := false 52 | cols := key.Cols 53 | if cols < 0 { 54 | cols = -cols 55 | rowfirst = true 56 | } 57 | if cols == 0 { 58 | cols = 1 59 | } 60 | if num < cols { 61 | cols = num 62 | } 63 | rows := (num + cols - 1) / cols 64 | 65 | // Prevent empty last columns in the following case where 5 elements are placed 66 | // columnsfirst into 4 columns 67 | // Col 0 1 2 3 68 | // AAA CCC EEE 69 | // BBB DDD 70 | if !rowfirst && rows*(cols-1) >= num { 71 | cols-- 72 | } 73 | 74 | // Arrays with infos 75 | matrix = make([][]*KeyEntry, cols) 76 | for i := 0; i < cols; i++ { 77 | matrix[i] = make([]*KeyEntry, rows) 78 | } 79 | 80 | i := 0 81 | for _, e := range key.Entries { 82 | if e.Text == "" { 83 | continue 84 | } 85 | var r, c int 86 | if rowfirst { 87 | r, c = i/cols, i%cols 88 | } else { 89 | c, r = i/rows, i%rows 90 | } 91 | matrix[c][r] = &KeyEntry{Text: e.Text, Style: e.Style, PlotStyle: e.PlotStyle} 92 | // fmt.Printf("Place1 (%d,%d) = %d: %s\n", c,r, i, matrix[c][r].Text) 93 | i++ 94 | } 95 | return 96 | } 97 | 98 | func textviewlen(t string) (length float32) { 99 | n := 0 100 | for _, r := range t { 101 | if w, ok := CharacterWidth[int(r)]; ok { 102 | length += w 103 | } else { 104 | length += 23 // save above average 105 | } 106 | n++ 107 | } 108 | length /= averageCharacterWidth 109 | // fmt.Printf("Length >%s<: %d runes = %.2f (%d)\n", t, n, length, int(100*length/float32(n))) 110 | return 111 | } 112 | 113 | func textDim(t string) (w float32, h int) { 114 | lines := strings.Split(t, "\n") 115 | for _, t := range lines { 116 | tvl := textviewlen(t) 117 | if tvl > w { 118 | w = tvl 119 | } 120 | } 121 | h = len(lines) 122 | return 123 | } 124 | 125 | // The following variables control the layout of the key/legend box. 126 | // All values are in font-units (fontheight for vertical, fontwidth for horizontal values) 127 | var ( 128 | KeyHorSep float32 = 1.5 // Horizontal spacing between key box and content 129 | KeyVertSep float32 = 0.5 // Vertical spacing between key box and content 130 | KeyColSep float32 = 2.0 // Horizontal spacing between two columns in key 131 | KeySymbolWidth float32 = 5 // Horizontal length/space reserved for symbol 132 | KeySymbolSep float32 = 2 // Horizontal spacing bewteen symbol and text 133 | KeyRowSep float32 = 0.75 // Vertical spacing between individual rows. 134 | ) 135 | 136 | // Layout determines how wide and broad the places keys in m will be rendered. 137 | func (key Key) Layout(bg BasicGraphics, m [][]*KeyEntry, font Font) (w, h int, colwidth, rowheight []int) { 138 | fontwidth, fontheight, _ := bg.FontMetrics(font) 139 | cols, rows := len(m), len(m[0]) 140 | 141 | // Find total width and height 142 | totalh := 0 143 | rowheight = make([]int, rows) 144 | for r := 0; r < rows; r++ { 145 | rh := 0 146 | for c := 0; c < cols; c++ { 147 | e := m[c][r] 148 | if e == nil { 149 | continue 150 | } 151 | // fmt.Printf("Layout1 (%d,%d): %s\n", c,r,e.Text) 152 | _, h := textDim(e.Text) 153 | if h > rh { 154 | rh = h 155 | } 156 | } 157 | rowheight[r] = rh 158 | totalh += rh 159 | } 160 | 161 | totalw := 0 162 | colwidth = make([]int, cols) 163 | // fmt.Printf("Making totalw for %d cols\n", cols) 164 | for c := 0; c < cols; c++ { 165 | var rw float32 166 | for r := 0; r < rows; r++ { 167 | e := m[c][r] 168 | if e == nil { 169 | continue 170 | } 171 | // fmt.Printf("Layout2 (%d,%d): %s\n", c,r,e.Text) 172 | 173 | w, _ := textDim(e.Text) 174 | if w > rw { 175 | rw = w 176 | } 177 | } 178 | irw := int(rw + 0.75) 179 | colwidth[c] = irw 180 | totalw += irw 181 | // fmt.Printf("Width of col %d: %d. Total now: %d\n", c, irw, totalw) 182 | } 183 | 184 | if fontwidth == 1 && fontheight == 1 { 185 | // totalw/h are characters only and still in character-units 186 | totalw += int(KeyColSep) * (cols - 1) // add space between columns 187 | totalw += int(2*KeyHorSep + 0.5) // add space for left/right border 188 | totalw += int(KeySymbolWidth+KeySymbolSep+0.5) * cols // place for symbol and symbol-text sep 189 | 190 | totalh += int(KeyRowSep) * (rows - 1) // add space between rows 191 | vsep := KeyVertSep 192 | if vsep < 1 { 193 | vsep = 1 194 | } // make sure there _is_ room (as KeyVertSep < 1) 195 | totalh += int(2 * vsep) // add border at top/bottom 196 | } else { 197 | // totalw/h are characters only and still in character-units 198 | totalw = int(float32(totalw) * fontwidth) // scale to pixels 199 | totalw += int(KeyColSep * (float32(cols-1) * fontwidth)) // add space between columns 200 | totalw += int(2 * KeyHorSep * fontwidth) // add space for left/right border 201 | totalw += int((KeySymbolWidth+KeySymbolSep)*fontwidth) * cols // place for symbol and symbol-text sep 202 | 203 | totalh *= fontheight 204 | totalh += int(KeyRowSep * float32((rows-1)*fontheight)) // add space between rows 205 | vsep := KeyVertSep * float32(fontheight) 206 | if vsep < 1 { 207 | vsep = 1 208 | } // make sure there _is_ room (as KeyVertSep < 1) 209 | totalh += int(2 * vsep) // add border at top/bottom 210 | } 211 | return totalw, totalh, colwidth, rowheight 212 | } 213 | 214 | // GenericKey draws the key onto bg at (x,y). 215 | func GenericKey(bg BasicGraphics, x, y int, key Key, options PlotOptions) { 216 | m := key.Place() 217 | if len(m) == 0 { 218 | return 219 | } 220 | keyfont := elementStyle(options, KeyElement).Font 221 | fw, fh, _ := bg.FontMetrics(keyfont) 222 | tw, th, cw, rh := key.Layout(bg, m, keyfont) 223 | style := elementStyle(options, KeyElement) 224 | if key.Border >= 0 { 225 | bg.Rect(x, y, tw, th, style) 226 | } 227 | x += int(KeyHorSep * fw) 228 | vsep := KeyVertSep * float32(fh) 229 | if vsep < 1 { 230 | vsep = 1 231 | } // make sure there _is_ room (as KeyVertSep < 1) 232 | // fmt.Printf("Key: y = %d after %d\n", y, y+int(vsep)+fh/2) 233 | y += int(vsep) + fh/2 234 | for ci, col := range m { 235 | yy := y 236 | 237 | for ri, e := range col { 238 | if e == nil || e.Text == "" { 239 | continue 240 | } 241 | plotStyle := e.PlotStyle 242 | // fmt.Printf("KeyEntry %s: PlotStyle = %d\n", e.Text, e.PlotStyle) 243 | if plotStyle == -1 { 244 | // heading only... 245 | bg.Text(x, yy, e.Text, "cl", 0, keyfont) 246 | } else { 247 | // normal entry 248 | if (plotStyle & PlotStyleLines) != 0 { 249 | bg.Line(x, yy, x+int(KeySymbolWidth*fw), yy, e.Style) 250 | } 251 | if (plotStyle & PlotStylePoints) != 0 { 252 | bg.Symbol(x+int(KeySymbolWidth*fw)/2, yy, e.Style) 253 | } 254 | if (plotStyle & PlotStyleBox) != 0 { 255 | sh := fh / 2 256 | a := x + int(KeySymbolWidth*fw)/2 257 | bg.Rect(a-sh, yy-sh, 2*sh, 2*sh, e.Style) 258 | } 259 | bg.Text(x+int(fw*(KeySymbolWidth+KeySymbolSep)), yy, e.Text, "cl", 0, keyfont) 260 | } 261 | yy += fh*rh[ri] + int(KeyRowSep*float32(fh)) 262 | } 263 | 264 | x += int((KeySymbolWidth + KeySymbolSep + KeyColSep + float32(cw[ci])) * fw) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /pie.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | // "os" 7 | // "strings" 8 | ) 9 | 10 | // PieChart represents pie and ring charts. 11 | // Data is exported but it you should use the AddData, AddDataPair and 12 | // AddIntDataPair methods to populate this field. 13 | // The FmtVal and FmtKey function are used to format optional labels 14 | // on the pie segments (FmtVal) and on the legend/key entries if non 15 | // nil. The FmtKey must be set before adding data via the AddXY methods. 16 | type PieChart struct { 17 | Title string // The title 18 | Key Key // The Key/Legend 19 | Inner float64 // relative radius of inner white are (set to 0.7 to produce ring chart) 20 | Options PlotOptions 21 | Data []CategoryChartData // The data 22 | 23 | FmtVal func(value, sume float64) string // add value labels to pie segments 24 | FmtKey func(value, sume float64) string // add value labels to key entries 25 | } 26 | 27 | // IntegerValue will format value (ignoring sum) as an integer. 28 | // It is a convenience function which can be assigned to the 29 | // PieChart.FmtVal or PieChart.FmtKey field. 30 | func IntegerValue(value, sum float64) (s string) { 31 | return fmt.Sprintf("%d", int64(value+0.5)) 32 | } 33 | 34 | // AbsoluteValue will format value (ignoring sum). 35 | // It is a convenience function which can be assigned to the 36 | // PieChart.FmtVal or PieChart.FmtKey field. 37 | func AbsoluteValue(value, sum float64) (s string) { 38 | fv := math.Abs(value) 39 | switch { 40 | case fv < 0.01: 41 | s = fmt.Sprintf(" %g ", value) 42 | case fv < 0.1: 43 | s = fmt.Sprintf(" %.2f ", value) 44 | case fv < 1: 45 | s = fmt.Sprintf(" %.1f ", value) 46 | case fv < 100000: 47 | s = fmt.Sprintf(" %.0f ", value) 48 | default: 49 | s = fmt.Sprintf(" %g ", value) 50 | } 51 | return 52 | } 53 | 54 | // PercentValue formats value as percentage of sum. 55 | // It is a convenience function which can be assigned to the 56 | // PieChart.FmtVal or PieChart.FmtKey field. 57 | func PercentValue(value, sum float64) (s string) { 58 | value *= 100 / sum 59 | s = AbsoluteValue(value, sum) + "% " 60 | return 61 | } 62 | 63 | type CategoryChartData struct { 64 | Name string 65 | Style []Style 66 | Samples []CatValue 67 | } 68 | 69 | func (c *PieChart) AddData(name string, data []CatValue, style []Style) { 70 | if len(style) < len(data) { 71 | ns := make([]Style, len(data)) 72 | copy(style, ns) 73 | for i := len(style); i < len(data); i++ { 74 | ns[i] = AutoStyle(i-len(style), true) 75 | } 76 | style = ns 77 | } 78 | c.Data = append(c.Data, CategoryChartData{name, style, data}) 79 | c.Key.Entries = append(c.Key.Entries, KeyEntry{PlotStyle: -1, Text: name}) 80 | var sum float64 81 | for _, d := range data { 82 | sum += d.Val 83 | } 84 | for s, cv := range data { 85 | text := cv.Cat 86 | if c.FmtKey != nil { 87 | if text != "" { 88 | text += " " 89 | } 90 | text += c.FmtKey(cv.Val, sum) 91 | } 92 | c.Key.Entries = append(c.Key.Entries, KeyEntry{PlotStyle: PlotStyleBox, Style: style[s], Text: text}) 93 | } 94 | } 95 | 96 | func (c *PieChart) AddDataPair(name string, cat []string, val []float64) { 97 | n := imin(len(cat), len(val)) 98 | data := make([]CatValue, n) 99 | for i := 0; i < n; i++ { 100 | data[i].Cat, data[i].Val = cat[i], val[i] 101 | } 102 | c.AddData(name, data, nil) 103 | } 104 | 105 | func (c *PieChart) AddIntDataPair(name string, cat []string, val []int) { 106 | n := imin(len(cat), len(val)) 107 | data := make([]CatValue, n) 108 | for i := 0; i < n; i++ { 109 | data[i].Cat, data[i].Val = cat[i], float64(val[i]) 110 | } 111 | c.AddData(name, data, nil) 112 | } 113 | 114 | var PieChartShrinkage = 0.66 // Scaling factor of radius of next data set. 115 | var PieChartHighlight = 0.15 // How much are flaged segments offset. 116 | 117 | // Reset chart to state before plotting. 118 | func (c *PieChart) Reset() {} 119 | 120 | // Plot outputs the scatter chart sc to g. 121 | func (c *PieChart) Plot(g Graphics) { 122 | layout := layout(g, c.Title, "", "", true, true, &c.Key) 123 | 124 | width, height := layout.Width, layout.Height 125 | topm, leftm := layout.Top, layout.Left 126 | width += 0 127 | 128 | r := imin(height, width) / 2 129 | x0, y0 := leftm+r, topm+r 130 | 131 | // Make sure pie fits into plotting area 132 | rshift := int(float64(r) * PieChartHighlight) 133 | if rshift < 6 { 134 | rshift = 6 135 | } 136 | for _, d := range c.Data[0].Samples { 137 | if d.Flag { 138 | // DebugLogger.Printf("Reduced %d by %d", r, rshift) 139 | r -= rshift / 3 140 | break 141 | } 142 | } 143 | 144 | g.Begin() 145 | 146 | if c.Title != "" { 147 | drawTitle(g, c.Title, elementStyle(c.Options, TitleElement)) 148 | } 149 | 150 | for _, data := range c.Data { 151 | var sum float64 152 | for _, d := range data.Samples { 153 | sum += d.Val 154 | } 155 | 156 | wedges := make([]Wedgeinfo, len(data.Samples)) 157 | var ri int = 0 158 | if c.Inner > 0 { 159 | ri = int(float64(r) * c.Inner) 160 | } 161 | 162 | var phi float64 = -math.Pi 163 | for j, d := range data.Samples { 164 | style := data.Style[j] 165 | alpha := 2 * math.Pi * d.Val / sum 166 | shift := 0 167 | 168 | var t string 169 | if c.FmtVal != nil { 170 | t = c.FmtVal(d.Val, sum) 171 | } 172 | if d.Flag { 173 | shift = rshift 174 | } 175 | 176 | wedges[j] = Wedgeinfo{Phi: phi, Psi: phi + alpha, Text: t, Tp: "c", 177 | Style: style, Font: Font{}, Shift: shift} 178 | 179 | phi += alpha 180 | } 181 | g.Rings(wedges, x0, y0, r, ri) 182 | 183 | r = int(float64(r) * PieChartShrinkage) 184 | } 185 | 186 | if !c.Key.Hide { 187 | g.Key(layout.KeyX, layout.KeyY, c.Key, c.Options) 188 | } 189 | 190 | g.End() 191 | } 192 | -------------------------------------------------------------------------------- /scatter.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // ScatterChart represents scatter charts, line charts and function plots. 8 | type ScatterChart struct { 9 | XRange, YRange Range // X and Y axis 10 | Title string // Title of the chart 11 | Key Key // Key/Legend 12 | Options PlotOptions 13 | Data []ScatterChartData // The actual data (filled with Add...-methods) 14 | NSamples int // number of samples for function plots 15 | } 16 | 17 | // ScatterChartData encapsulates a data set or function in a scatter chart. 18 | // Not both Samples and Func may be non nil at the same time. 19 | type ScatterChartData struct { 20 | Name string // The name of this data set. TODO: unused? 21 | PlotStyle PlotStyle // Points, Lines+Points or Lines only 22 | Style Style // Color, sizes, pointtype, linestyle, ... 23 | Samples []EPoint // The actual points for scatter/lines charts 24 | Func func(float64) float64 // The function to draw. 25 | } 26 | 27 | // AddFunc adds a function f to this chart. A key/legend entry is produced 28 | // if name is not empty. 29 | func (c *ScatterChart) AddFunc(name string, f func(float64) float64, plotstyle PlotStyle, style Style) { 30 | if plotstyle.undefined() { 31 | plotstyle = PlotStyleLines 32 | } 33 | if style.empty() { 34 | style = AutoStyle(len(c.Data), false) 35 | } 36 | 37 | scd := ScatterChartData{Name: name, PlotStyle: plotstyle, Style: style, Samples: nil, Func: f} 38 | c.Data = append(c.Data, scd) 39 | if name != "" { 40 | ke := KeyEntry{Text: name, PlotStyle: plotstyle, Style: style} 41 | c.Key.Entries = append(c.Key.Entries, ke) 42 | } 43 | } 44 | 45 | // AddData adds points in data to chart. A key/legend entry is produced 46 | // if name is not empty. 47 | func (c *ScatterChart) AddData(name string, data []EPoint, plotstyle PlotStyle, style Style) { 48 | 49 | // Update styles if non given 50 | if plotstyle.undefined() { 51 | plotstyle = PlotStylePoints 52 | } 53 | if style.empty() { 54 | style = AutoStyle(len(c.Data), false) 55 | } 56 | // Fix missing values in style 57 | if (plotstyle & PlotStyleLines) != 0 { 58 | if style.LineWidth <= 0 { 59 | style.LineWidth = 1 60 | } 61 | if style.LineColor == nil { 62 | style.LineColor = style.SymbolColor 63 | } 64 | } 65 | if (plotstyle&PlotStylePoints) != 0 && style.Symbol == 0 { 66 | style.Symbol = '#' 67 | } 68 | 69 | // Init axis 70 | if len(c.Data) == 0 { 71 | c.XRange.init() 72 | c.YRange.init() 73 | } 74 | 75 | // Add data 76 | scd := ScatterChartData{Name: name, PlotStyle: plotstyle, Style: style, Samples: data, Func: nil} 77 | c.Data = append(c.Data, scd) 78 | 79 | // Autoscale 80 | for _, d := range data { 81 | xl, yl, xh, yh := d.BoundingBox() 82 | c.XRange.autoscale(xl) 83 | c.XRange.autoscale(xh) 84 | c.YRange.autoscale(yl) 85 | c.YRange.autoscale(yh) 86 | } 87 | 88 | // Add key/legend entry 89 | if name != "" { 90 | ke := KeyEntry{Style: style, PlotStyle: plotstyle, Text: name} 91 | c.Key.Entries = append(c.Key.Entries, ke) 92 | } 93 | } 94 | 95 | // AddDataGeneric is the generiv version of AddData which allows any type 96 | // to be plotted that implements the XYErrValue interface. 97 | func (c *ScatterChart) AddDataGeneric(name string, data []XYErrValue, plotstyle PlotStyle, style Style) { 98 | edata := make([]EPoint, len(data)) 99 | for i, d := range data { 100 | x, y := d.XVal(), d.YVal() 101 | xl, xh := d.XErr() 102 | yl, yh := d.YErr() 103 | dx, dy := xh-xl, yh-yl 104 | xo, yo := xh-dx/2-x, yh-dy/2-y 105 | edata[i] = EPoint{X: x, Y: y, DeltaX: dx, DeltaY: dy, OffX: xo, OffY: yo} 106 | } 107 | c.AddData(name, edata, plotstyle, style) 108 | } 109 | 110 | // AddDataPair is a convenience method which wrapps around AddData: It adds the points 111 | // (x[n],y[n]) to the chart. 112 | func (c *ScatterChart) AddDataPair(name string, x, y []float64, plotstyle PlotStyle, style Style) { 113 | n := imin(len(x), len(y)) 114 | data := make([]EPoint, n) 115 | nan := math.NaN() 116 | for i := 0; i < n; i++ { 117 | data[i] = EPoint{X: x[i], Y: y[i], DeltaX: nan, DeltaY: nan} 118 | } 119 | c.AddData(name, data, plotstyle, style) 120 | } 121 | 122 | // Reset chart to state before plotting. 123 | func (c *ScatterChart) Reset() { 124 | c.XRange.Reset() 125 | c.YRange.Reset() 126 | } 127 | 128 | // Plot outputs the scatter chart to the graphic output g. 129 | func (c *ScatterChart) Plot(g Graphics) { 130 | layout := layout(g, c.Title, c.XRange.Label, c.YRange.Label, 131 | c.XRange.TicSetting.Hide || c.XRange.TicSetting.HideLabels, 132 | c.YRange.TicSetting.Hide || c.YRange.TicSetting.HideLabels, 133 | &c.Key) 134 | 135 | width, height := layout.Width, layout.Height 136 | topm, leftm := layout.Top, layout.Left 137 | numxtics, numytics := layout.NumXtics, layout.NumYtics 138 | 139 | // fmt.Printf("\nSet up of X-Range (%d)\n", numxtics) 140 | c.XRange.Setup(numxtics, numxtics+2, width, leftm, false) 141 | // fmt.Printf("\nSet up of Y-Range (%d)\n", numytics) 142 | c.YRange.Setup(numytics, numytics+2, height, topm, true) 143 | 144 | g.Begin() 145 | 146 | if c.Title != "" { 147 | drawTitle(g, c.Title, elementStyle(c.Options, TitleElement)) 148 | } 149 | 150 | g.XAxis(c.XRange, topm+height, topm, c.Options) 151 | g.YAxis(c.YRange, leftm, leftm+width, c.Options) 152 | 153 | // Plot Data 154 | xf, yf := c.XRange.Data2Screen, c.YRange.Data2Screen 155 | xmin, xmax := c.XRange.Min, c.XRange.Max 156 | ymin, ymax := c.YRange.Min, c.YRange.Max 157 | spf := screenPointFunc(xf, yf, xmin, xmax, ymin, ymax) 158 | 159 | for i, data := range c.Data { 160 | style := data.Style 161 | if data.Samples != nil { 162 | // Samples 163 | points := make([]EPoint, 0, len(data.Samples)) 164 | for _, d := range data.Samples { 165 | if d.X < xmin || d.X > xmax || d.Y < ymin || d.Y > ymax { 166 | continue 167 | } 168 | p := spf(d) 169 | points = append(points, p) 170 | } 171 | g.Scatter(points, data.PlotStyle, style) 172 | } else if data.Func != nil { 173 | c.drawFunction(g, i) 174 | } 175 | } 176 | 177 | if !c.Key.Hide { 178 | g.Key(layout.KeyX, layout.KeyY, c.Key, c.Options) 179 | } 180 | 181 | g.End() 182 | } 183 | 184 | // Output function (ih in Data) 185 | func (c *ScatterChart) drawFunction(g Graphics, i int) { 186 | function := c.Data[i].Func 187 | style := c.Data[i].Style 188 | plotstyle := c.Data[i].PlotStyle 189 | 190 | yf := c.YRange.Data2Screen 191 | symax, symin := float64(yf(c.YRange.Min)), float64(yf(c.YRange.Max)) // y limits in screen coords 192 | sxmin, sxmax := c.XRange.Data2Screen(c.XRange.Min), c.XRange.Data2Screen(c.XRange.Max) 193 | width := sxmax - sxmin 194 | if c.NSamples == 0 { 195 | step := 6 196 | if width < 70 { 197 | step = 3 198 | } 199 | if width < 50 { 200 | step = 2 201 | } 202 | if width < 30 { 203 | step = 1 204 | } 205 | c.NSamples = width / step 206 | } 207 | step := width / c.NSamples 208 | if step < 1 { 209 | step = 1 210 | } 211 | pcap := width/step + 2 212 | points := make([]EPoint, 0, pcap) 213 | var lastP *EPoint = nil // screen coordinates of last point (nil if no point) 214 | var lastIn bool = false // was last point in valid yrange? (undef if lastP==nil) 215 | 216 | for six := sxmin; six < sxmax; six += step { 217 | x := c.XRange.Screen2Data(six) 218 | sx := float64(six) 219 | y := function(x) 220 | 221 | // Handle NaN and +/- Inf 222 | if math.IsNaN(y) { 223 | g.Scatter(points, plotstyle, style) 224 | points = points[0:0] 225 | lastP = nil 226 | continue 227 | } 228 | 229 | sy := float64(yf(y)) 230 | 231 | if sy >= symin && sy <= symax { 232 | p := EPoint{X: sx, Y: sy} 233 | if lastP != nil && !lastIn { 234 | pc := c.clipPoint(p, *lastP, symin, symax) 235 | // fmt.Printf("Added front clip point %v\n", pc) 236 | points = append(points, pc) 237 | } 238 | // fmt.Printf("Added point %v\n", p) 239 | points = append(points, p) 240 | lastIn = true 241 | } else { 242 | if lastP == nil { 243 | lastP = &EPoint{X: sx, Y: sy} 244 | continue 245 | } 246 | if lastIn { 247 | pc := c.clipPoint(*lastP, EPoint{X: sx, Y: sy}, symin, symax) 248 | points = append(points, pc) 249 | g.Scatter(points, plotstyle, style) 250 | // fmt.Printf("Added clip point %v and drawing\n", pc) 251 | points = points[0:0] 252 | lastIn = false 253 | } else if (lastP.Y < symin && sy > symax) || (lastP.Y > symax && sy < symin) { 254 | p2 := c.clip2Point(*lastP, EPoint{X: sx, Y: sy}, symin, symax) 255 | // fmt.Printf("Added 2clip points %v / %v and drawing\n", p2[0], p2[1]) 256 | g.Scatter(p2, plotstyle, style) 257 | } 258 | 259 | } 260 | 261 | lastP = &EPoint{X: sx, Y: sy} 262 | } 263 | g.Scatter(points, plotstyle, style) 264 | } 265 | 266 | // Point in is in valid y range, out is out. Return p which clips the line from in to out to valid y range 267 | func (c *ScatterChart) clipPoint(in, out EPoint, min, max float64) (p EPoint) { 268 | // fmt.Printf("clipPoint: in (%g,%g), out(%g,%g) min/max=%g/%g\n", in.X, in.Y, out.X, out.Y, min, max) 269 | dx, dy := in.X-out.X, in.Y-out.Y 270 | 271 | var y float64 272 | if out.Y <= min { 273 | y = min 274 | } else { 275 | y = max 276 | } 277 | x := in.X + dx*(y-in.Y)/dy 278 | p.X, p.Y = x, y 279 | p.DeltaX, p.DeltaY = math.NaN(), math.NaN() 280 | return 281 | } 282 | 283 | // Clip line from a to b (both outside min/max range) 284 | func (c *ScatterChart) clip2Point(a, b EPoint, min, max float64) []EPoint { 285 | if a.Y > b.Y { 286 | a, b = b, a 287 | } 288 | dx, dy := b.X-a.X, b.Y-a.Y 289 | s := dx / dy 290 | 291 | pc := make([]EPoint, 2) 292 | 293 | pc[0].X = a.X + s*(min-a.Y) 294 | pc[0].Y = min 295 | pc[0].DeltaX, pc[0].DeltaY = math.NaN(), math.NaN() 296 | pc[1].X = a.X + s*(max-a.Y) 297 | pc[1].Y = max 298 | pc[1].DeltaX, pc[1].DeltaY = math.NaN(), math.NaN() 299 | return pc 300 | } 301 | 302 | // Set up function which handles mappig data->screen coordinates and does 303 | // proper clipping on the error bars. 304 | func screenPointFunc(xf, yf func(float64) int, xmin, xmax, ymin, ymax float64) (spf func(EPoint) EPoint) { 305 | spf = func(d EPoint) (p EPoint) { 306 | xl, yl, xh, yh := d.BoundingBox() 307 | // fmt.Printf("OrigBB: %.1f %.1f %.1f %.1f (%.1f,%.1f)\n", xl,yl,xh,yh,d.X,d.Y) 308 | if xl < xmin { 309 | xl = xmin 310 | } 311 | if xh > xmax { 312 | xh = xmax 313 | } 314 | if yl < ymin { 315 | yl = ymin 316 | } 317 | if yh > ymax { 318 | yh = ymax 319 | } 320 | // fmt.Printf("ClippedBB: %.1f %.1f %.1f %.1f\n", xl,yl,xh,yh) 321 | 322 | x := float64(xf(d.X)) 323 | y := float64(yf(d.Y)) 324 | xsl, xsh := float64(xf(xl)), float64(xf(xh)) 325 | ysl, ysh := float64(yf(yl)), float64(yf(yh)) 326 | // fmt.Printf("ScreenBB: %.0f %.0f %.0f %.0f (%.0f,%.0f)\n", xsl,ysl,xsh,ysh,x,y) 327 | 328 | dx, dy := math.NaN(), math.NaN() 329 | var xo, yo float64 330 | 331 | if xsl != xsh { 332 | dx = math.Abs(xsh - xsl) 333 | xo = xsl - x + dx/2 334 | } 335 | if ysl != ysh { 336 | dy = math.Abs(ysh - ysl) 337 | yo = ysh - y + dy/2 338 | } 339 | // fmt.Printf(" >> dx=%.0f dy=%.0f xo=%.0f yo=%.0f\n", dx,dy,xo,yo) 340 | 341 | p = EPoint{X: x, Y: y, DeltaX: dx, DeltaY: dy, OffX: xo, OffY: yo} 342 | return 343 | 344 | /************************** 345 | if xl < xmin { // happens only if d.Delta!=0,NaN 346 | a := xmin - xl 347 | d.DeltaX -= a 348 | d.OffX += a / 2 349 | } 350 | if xh > xmax { 351 | a := xh - xmax 352 | d.DeltaX -= a 353 | d.OffX -= a / 2 354 | } 355 | if yl < ymin { // happens only if d.Delta!=0,NaN 356 | a := ymin - yl 357 | d.DeltaY -= a 358 | d.OffY += a / 2 359 | } 360 | if yh > ymax { 361 | a := yh - ymax 362 | d.DeltaY -= a 363 | d.OffY -= a / 2 364 | } 365 | 366 | x := xf(d.X) 367 | y := yf(d.Y) 368 | dx, dy := math.NaN(), math.NaN() 369 | var xo, yo float64 370 | if !math.IsNaN(d.DeltaX) { 371 | dx = float64(xf(d.DeltaX) - xf(0)) // TODO: abs? 372 | xo = float64(xf(d.OffX) - xf(0)) 373 | } 374 | if !math.IsNaN(d.DeltaY) { 375 | dy = float64(yf(d.DeltaY) - yf(0)) // TODO: abs? 376 | yo = float64(yf(d.OffY) - yf(0)) 377 | } 378 | // fmt.Printf("Point %d: %f\n", i, dx) 379 | p = EPoint{X: float64(x), Y: float64(y), DeltaX: dx, DeltaY: dy, OffX: xo, OffY: yo} 380 | return 381 | *********************/ 382 | } 383 | return 384 | } 385 | -------------------------------------------------------------------------------- /stat.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | ) 7 | 8 | // Return p percentil of pre-sorted integer data. 0 <= p <= 100. 9 | func PercentilInt(data []int, p int) int { 10 | n := len(data) 11 | if n == 0 { 12 | return 0 13 | } 14 | if n == 1 { 15 | return data[0] 16 | } 17 | 18 | pos := float64(p) * float64(n+1) / 100 19 | fpos := math.Floor(pos) 20 | intPos := int(fpos) 21 | dif := pos - fpos 22 | if intPos < 1 { 23 | return data[0] 24 | } 25 | if intPos >= n { 26 | return data[n-1] 27 | } 28 | lower := data[intPos-1] 29 | upper := data[intPos] 30 | val := float64(lower) + dif*float64(upper-lower) 31 | return int(math.Floor(val + 0.5)) 32 | } 33 | 34 | // Return p percentil of pre-sorted float64 data. 0 <= p <= 100. 35 | func percentilFloat64(data []float64, p int) float64 { 36 | n := len(data) 37 | if n == 0 { 38 | return 0 39 | } 40 | if n == 1 { 41 | return data[0] 42 | } 43 | 44 | pos := float64(p) * float64(n+1) / 100 45 | fpos := math.Floor(pos) 46 | intPos := int(fpos) 47 | dif := pos - fpos 48 | if intPos < 1 { 49 | return data[0] 50 | } 51 | if intPos >= n { 52 | return data[n-1] 53 | } 54 | lower := data[intPos-1] 55 | upper := data[intPos] 56 | val := lower + dif*(upper-lower) 57 | return val 58 | } 59 | 60 | // Compute minimum, p percentil, median, average, 100-p percentil and maximum of values in data. 61 | func SixvalInt(data []int, p int) (min, lq, med, avg, uq, max int) { 62 | min, max = math.MaxInt32, math.MinInt32 63 | sum, n := 0, len(data) 64 | if n == 0 { 65 | return 66 | } 67 | if n == 1 { 68 | min = data[0] 69 | lq = data[0] 70 | med = data[0] 71 | avg = data[0] 72 | uq = data[0] 73 | max = data[0] 74 | return 75 | } 76 | for _, v := range data { 77 | if v < min { 78 | min = v 79 | } 80 | if v > max { 81 | max = v 82 | } 83 | sum += v 84 | } 85 | 86 | avg = sum / n 87 | 88 | sort.Ints(data) 89 | 90 | if n%2 == 1 { 91 | med = data[(n-1)/2] 92 | } else { 93 | med = (data[n/2] + data[n/2-1]) / 2 94 | } 95 | 96 | lq = PercentilInt(data, p) 97 | uq = PercentilInt(data, 100-p) 98 | return 99 | } 100 | 101 | // Compute minimum, p percentil, median, average, 100-p percentil and maximum of values in data. 102 | func SixvalFloat64(data []float64, p int) (min, lq, med, avg, uq, max float64) { 103 | n := len(data) 104 | 105 | // Special cases 0 and 1 106 | if n == 0 { 107 | return 108 | } 109 | 110 | if n == 1 { 111 | min = data[0] 112 | lq = data[0] 113 | med = data[0] 114 | avg = data[0] 115 | uq = data[0] 116 | max = data[0] 117 | return 118 | } 119 | 120 | // First pass (min, max, coarse average) 121 | var sum float64 122 | min, max = math.MaxFloat64, -math.MaxFloat64 123 | for _, v := range data { 124 | if v < min { 125 | min = v 126 | } 127 | if v > max { 128 | max = v 129 | } 130 | sum += v 131 | } 132 | avg = sum / float64(n) 133 | 134 | // Second pass: Correct average 135 | var corr float64 136 | for _, v := range data { 137 | corr += v - avg 138 | } 139 | avg += corr / float64(n) 140 | 141 | // Median 142 | sort.Float64s(data) 143 | if n%2 == 1 { 144 | med = data[(n-1)/2] 145 | } else { 146 | med = (data[n/2] + data[n/2-1]) / 2 147 | } 148 | 149 | // Percentiles 150 | if p < 0 { 151 | p = 0 152 | } 153 | if p > 100 { 154 | p = 100 155 | } 156 | lq = percentilFloat64(data, p) 157 | uq = percentilFloat64(data, 100-p) 158 | return 159 | } 160 | -------------------------------------------------------------------------------- /strip.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | // "fmt" 5 | "math" 6 | "math/rand" 7 | // "os" 8 | // "strings" 9 | ) 10 | 11 | // StripChart represents very simple strip charts. 12 | type StripChart struct { 13 | Jitter bool // Add jitter to help distinguish overlapping values 14 | ScatterChart // The embeded ScatterChart is responsible for all drawing 15 | } 16 | 17 | // AddData adds data to the strip chart. 18 | func (sc *StripChart) AddData(name string, data []float64, style Style) { 19 | n := len(sc.ScatterChart.Data) + 1 20 | pd := make([]EPoint, len(data)) 21 | nan := math.NaN() 22 | for i, d := range data { 23 | pd[i].X = d 24 | pd[i].Y = float64(n) 25 | pd[i].DeltaX, pd[i].DeltaY = nan, nan 26 | } 27 | if style.empty() { 28 | style = AutoStyle(len(sc.Data), false) 29 | } 30 | style.LineStyle = 0 31 | sc.ScatterChart.AddData(name, pd, PlotStylePoints, style) 32 | } 33 | 34 | func (sc *StripChart) AddDataGeneric(name string, data []Value) { 35 | n := len(sc.ScatterChart.Data) + 1 36 | pd := make([]EPoint, len(data)) 37 | nan := math.NaN() 38 | for i, d := range data { 39 | pd[i].X = d.XVal() 40 | pd[i].Y = float64(n) 41 | pd[i].DeltaX, pd[i].DeltaY = nan, nan 42 | } 43 | sc.ScatterChart.AddData(name, pd, PlotStylePoints, Style{}) 44 | } 45 | 46 | // Reset chart to state before plotting. 47 | func (sc *StripChart) Reset() { 48 | sc.ScatterChart.Reset() 49 | } 50 | 51 | // Plot outputs the strip chart sc to g. 52 | func (sc *StripChart) Plot(g Graphics) { 53 | sc.ScatterChart.YRange.Label = "" 54 | sc.ScatterChart.YRange.TicSetting.Hide = true 55 | sc.ScatterChart.YRange.TicSetting.Delta = 1 56 | sc.ScatterChart.YRange.MinMode.Fixed = true 57 | sc.ScatterChart.YRange.MinMode.Value = 0.5 58 | sc.ScatterChart.YRange.MaxMode.Fixed = true 59 | sc.ScatterChart.YRange.MaxMode.Value = float64(len(sc.ScatterChart.Data)) + 0.5 60 | 61 | if sc.Jitter { 62 | // Set up ranging 63 | layout := layout(g, sc.Title, sc.XRange.Label, sc.YRange.Label, 64 | sc.XRange.TicSetting.Hide || sc.XRange.TicSetting.HideLabels, 65 | sc.YRange.TicSetting.Hide || sc.YRange.TicSetting.HideLabels, 66 | &sc.Key) 67 | 68 | _, height := layout.Width, layout.Height 69 | topm, _ := layout.Top, layout.Left 70 | _, numytics := layout.NumXtics, layout.NumYtics 71 | 72 | sc.YRange.Setup(numytics, numytics+1, height, topm, true) 73 | 74 | // amplitude of jitter: not too smal to be visible and useful, not to 75 | // big to be ugly or even overlapp other 76 | 77 | null := sc.YRange.Screen2Data(0) 78 | absmin := 1.4 * math.Abs(sc.YRange.Screen2Data(1)-null) // would be one pixel 79 | tenpc := math.Abs(sc.YRange.Screen2Data(height)-null) / 10 // 10 percent of graph area 80 | smplcnt := len(sc.ScatterChart.Data) + 1 // as samples are borders 81 | noverlp := math.Abs(sc.YRange.Screen2Data(height/smplcnt) - null) // do not overlapp other sample 82 | 83 | yj := noverlp 84 | if tenpc < yj { 85 | yj = tenpc 86 | } 87 | if yj < absmin { 88 | yj = absmin 89 | } 90 | 91 | // yjs := sc.YRange.Data2Screen(yj) - sc.YRange.Data2Screen(0) 92 | // fmt.Printf("yj = %.2f : in screen = %d\n", yj, yjs) 93 | for _, data := range sc.ScatterChart.Data { 94 | if data.Samples == nil { 95 | continue // should not happen 96 | } 97 | for i := range data.Samples { 98 | shift := yj * rand.NormFloat64() * yj 99 | data.Samples[i].Y += shift 100 | } 101 | } 102 | } 103 | sc.ScatterChart.Plot(g) 104 | 105 | if sc.Jitter { 106 | // Revert Jitter 107 | for s, data := range sc.ScatterChart.Data { 108 | if data.Samples == nil { 109 | continue // should not happen 110 | } 111 | for i, _ := range data.Samples { 112 | data.Samples[i].Y = float64(s + 1) 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /style.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "math" 7 | ) 8 | 9 | // Symbol is the list of different symbols. 10 | var Symbol = []int{ 11 | 'o', // empty circle 12 | '=', // empty square 13 | '%', // empty triangle up 14 | '&', // empty diamond 15 | '+', // plus 16 | 'X', // cross 17 | '*', // star 18 | '0', // bulls eys 19 | '@', // filled circle 20 | '#', // filled square 21 | 'A', // filled triangle up 22 | 'W', // filled triangle down 23 | 'V', // empty triangle down 24 | 'Z', // filled diamond 25 | '.', // tiny dot 26 | } 27 | 28 | // SymbolIndex returns the index of the symbol s in Symbol or -1 if not found. 29 | func SymbolIndex(s int) (idx int) { 30 | for idx = 0; idx < len(Symbol); idx++ { 31 | if Symbol[idx] == s { 32 | return idx 33 | } 34 | } 35 | return -1 36 | } 37 | 38 | // NextSymbol returns the next symbol of s: Either in the global list Symbol 39 | // or (if not found there) the next character. 40 | func NextSymbol(s int) int { 41 | if idx := SymbolIndex(s); idx != -1 { 42 | return Symbol[(idx+1)%len(Symbol)] 43 | } 44 | return s + 1 45 | } 46 | 47 | // CharacterWidth is a table of the (relative) width of common runes. 48 | var CharacterWidth = map[int]float32{'a': 16.8, 'b': 17.0, 'c': 15.2, 'd': 16.8, 'e': 16.8, 'f': 8.5, 'g': 17.0, 49 | 'h': 16.8, 'i': 5.9, 'j': 5.9, 'k': 16.8, 'l': 6.9, 'm': 25.5, 'n': 16.8, 'o': 16.8, 'p': 17.0, 'q': 17.0, 50 | 'r': 10.2, 's': 15.2, 't': 8.4, 'u': 16.8, 'v': 15.4, 'w': 22.2, 'x': 15.2, 'y': 15.2, 'z': 15.2, 51 | 'A': 20.2, 'B': 20.2, 'C': 22.2, 'D': 22.2, 'E': 20.2, 'F': 18.6, 'G': 23.5, 'H': 22.0, 'I': 8.2, 'J': 15.2, 52 | 'K': 20.2, 'L': 16.8, 'M': 25.5, 'N': 22.0, 'O': 23.5, 'P': 20.2, 'Q': 23.5, 'R': 21.1, 'S': 20.2, 'T': 18.5, 53 | 'U': 22.0, 'V': 20.2, 'W': 29.0, 'X': 20.2, 'Y': 20.2, 'Z': 18.8, ' ': 8.5, 54 | '1': 16.8, '2': 16.8, '3': 16.8, '4': 16.8, '5': 16.8, '6': 16.8, '7': 16.8, '8': 16.8, '9': 16.8, '0': 16.8, 55 | '.': 8.2, ',': 8.2, ':': 8.2, ';': 8.2, '+': 17.9, '"': 11.0, '*': 11.8, '%': 27.0, '&': 20.2, '/': 8.4, 56 | '(': 10.2, ')': 10.2, '=': 18.0, '?': 16.8, '!': 8.5, '[': 8.2, ']': 8.2, '{': 10.2, '}': 10.2, '$': 16.8, 57 | '<': 18.0, '>': 18.0, '§': 16.8, '°': 12.2, '^': 14.2, '~': 18.0, 58 | } 59 | var averageCharacterWidth float32 60 | 61 | func init() { 62 | n := 0 63 | for _, w := range CharacterWidth { 64 | averageCharacterWidth += w 65 | n++ 66 | } 67 | averageCharacterWidth /= float32(n) 68 | averageCharacterWidth = 15 69 | } 70 | 71 | // Style contains all information about all graphic elements in a chart. 72 | // All colors are in the form "#rrggbb" with rr/gg/bb hexvalues. 73 | // Not all elements of a plot use all fields in this struct. 74 | type Style struct { 75 | Symbol int // 0: no symbol; any codepoint: this symbol 76 | SymbolColor color.Color // color of symbol 77 | SymbolSize float64 // scaling factor of symbol 78 | LineStyle LineStyle // SolidLine, DashedLine, DottedLine, .... see below 79 | LineColor color.Color // color of line 80 | LineWidth int // 0: no line, >=1 width of line in pixel 81 | Font Font // the font to use 82 | FillColor color.Color 83 | } 84 | 85 | // PlotStyle describes how data and functions are drawn in scatter plots. 86 | // Can be used to describe how a key entry is drawn 87 | type PlotStyle int 88 | 89 | const ( 90 | PlotStylePoints PlotStyle = iota + 1 // draw symbol at data point 91 | PlotStyleLines // connect data points by straight lines 92 | PlotStyleLinesPoints // symbols and lines 93 | PlotStyleBox // produce boxplot 94 | ) 95 | 96 | func (ps PlotStyle) undefined() bool { 97 | return int(ps) < 1 || int(ps) > 3 98 | } 99 | 100 | // LineStyle describes the different types of lines. 101 | type LineStyle int 102 | 103 | // The supported line styles. 104 | const ( 105 | SolidLine LineStyle = iota // ---------------------- 106 | DashedLine // ---- ---- ---- ---- 107 | DottedLine // - - - - - - - - - - - 108 | DashDotDotLine // ---- - - ---- - - 109 | LongDashLine // 110 | LongDotLine 111 | ) 112 | 113 | // Font describes a font. 114 | type Font struct { 115 | Name string // "": default 116 | Size FontSize // relative size of font to default in output graphics 117 | Color color.Color // "": default, other: use this 118 | } 119 | 120 | // FontSize is the relative font size used in chart. Five sizes seem enough. 121 | type FontSize int 122 | 123 | const ( 124 | TinyFontSize FontSize = iota - 2 125 | SmallFontSize 126 | NormalFontSize 127 | LargeFontSize 128 | HugeFontSize 129 | ) 130 | 131 | func (d *Style) empty() bool { 132 | return d.Symbol == 0 && d.SymbolColor == nil && d.LineStyle == 0 && 133 | d.LineColor == nil && d.FillColor == nil && d.SymbolSize == 0 134 | } 135 | 136 | // Standard colors used by AutoStyle 137 | var StandardColors = []color.Color{ 138 | color.NRGBA{0xcc, 0x00, 0x00, 0xff}, // red 139 | color.NRGBA{0x00, 0xbb, 0x00, 0xff}, // green 140 | color.NRGBA{0x00, 0x00, 0xdd, 0xff}, // blue 141 | color.NRGBA{0x99, 0x66, 0x00, 0xff}, // brown 142 | color.NRGBA{0xbb, 0x00, 0xbb, 0xff}, // violet 143 | color.NRGBA{0x00, 0xaa, 0xaa, 0xff}, // turquise 144 | color.NRGBA{0xbb, 0xbb, 0x00, 0xff}, // yellow 145 | } 146 | 147 | // Standard line styles used by AutoStyle (fill=false) 148 | var StandardLineStyles = []LineStyle{SolidLine, DashedLine, DottedLine, LongDashLine, LongDotLine} 149 | 150 | // Standard symbols used by AutoStyle 151 | var StandardSymbols = []int{'o', '=', '%', '&', '+', 'X', '*', '@', '#', 'A', 'Z'} 152 | 153 | // How much brighter/darker filled elements become. 154 | var StandardFillFactor = 0.5 155 | 156 | // AutoStyle produces a styles based on StandardColors, StandardLineStyles, and StandardSymbols. 157 | // Call with fill = true for charts with filled elements (hist, bar, cbar, pie). 158 | func AutoStyle(i int, fill bool) (style Style) { 159 | nc, nl, ns := len(StandardColors), len(StandardLineStyles), len(StandardSymbols) 160 | 161 | si := i % ns 162 | ci := i % nc 163 | li := i % nl 164 | 165 | style.Symbol = StandardSymbols[si] 166 | style.SymbolColor = StandardColors[ci] 167 | style.LineColor = StandardColors[ci] 168 | style.SymbolSize = 1 169 | 170 | if fill { 171 | style.LineStyle = SolidLine 172 | style.LineWidth = 3 173 | if i < nc { 174 | style.FillColor = lighter(style.LineColor, StandardFillFactor) 175 | } else if i <= 2*nc { 176 | style.FillColor = darker(style.LineColor, StandardFillFactor) 177 | } else { 178 | style.FillColor = style.LineColor 179 | } 180 | } else { 181 | style.LineStyle = StandardLineStyles[li] 182 | style.LineWidth = 1 183 | } 184 | return 185 | } 186 | 187 | // PlotElement identifies one element in a plot/chart 188 | type PlotElement int 189 | 190 | const ( 191 | MajorAxisElement PlotElement = iota 192 | MinorAxisElement 193 | MajorTicElement 194 | MinorTicElement 195 | ZeroAxisElement 196 | GridLineElement 197 | GridBlockElement 198 | KeyElement 199 | TitleElement 200 | RangeLimitElement 201 | ) 202 | 203 | // PlotOptions contains a Style for each PlotElement. If a PlotOption does not 204 | // contain a certainPlotElement the value in DefaultStyle is used. 205 | type PlotOptions map[PlotElement]Style 206 | 207 | func elementStyle(options PlotOptions, element PlotElement) Style { 208 | if style, ok := options[element]; ok { 209 | return style 210 | } 211 | if style, ok := DefaultOptions[element]; ok { 212 | return style 213 | } 214 | return Style{LineColor: color.NRGBA{0x80, 0x80, 0x80, 0xff}, LineWidth: 1, LineStyle: SolidLine} 215 | } 216 | func ElementStyle(options PlotOptions, element PlotElement) Style { 217 | return elementStyle(options, element) 218 | } 219 | 220 | // DefaultStyle maps chart elements to styles. 221 | var DefaultOptions = map[PlotElement]Style{ 222 | MajorAxisElement: Style{LineColor: color.NRGBA{0, 0, 0, 0xff}, LineWidth: 2, LineStyle: SolidLine}, // axis 223 | MinorAxisElement: Style{LineColor: color.NRGBA{0, 0, 0, 0xff}, LineWidth: 2, LineStyle: SolidLine}, // mirrored axis 224 | MajorTicElement: Style{LineColor: color.NRGBA{0, 0, 0, 0xff}, LineWidth: 1, LineStyle: SolidLine}, 225 | MinorTicElement: Style{LineColor: color.NRGBA{0, 0, 0, 0xff}, LineWidth: 1, LineStyle: SolidLine}, 226 | ZeroAxisElement: Style{LineColor: color.NRGBA{0x40, 0x40, 0x40, 0xff}, LineWidth: 1, LineStyle: SolidLine}, 227 | GridLineElement: Style{LineColor: color.NRGBA{0x80, 0x80, 0x80, 0xff}, LineWidth: 1, LineStyle: SolidLine}, 228 | GridBlockElement: Style{LineColor: color.NRGBA{0xe6, 0xfc, 0xfc, 0xff}, LineWidth: 0, FillColor: color.NRGBA{0xe6, 0xfc, 0xfc, 0xff}}, 229 | KeyElement: Style{LineColor: color.NRGBA{0x20, 0x20, 0x20, 0xff}, LineWidth: 1, LineStyle: SolidLine, 230 | FillColor: color.NRGBA{0xf0, 0xf0, 0xf0, 0xc0}, Font: Font{Size: SmallFontSize}}, 231 | TitleElement: Style{LineColor: color.NRGBA{0, 0, 0, 0xff}, LineWidth: 1, LineStyle: SolidLine, 232 | FillColor: color.NRGBA{0xec, 0xc7, 0x50, 0xff}, Font: Font{Size: LargeFontSize}}, 233 | RangeLimitElement: Style{Font: Font{Size: SmallFontSize}}, 234 | } 235 | 236 | func hsv2rgb(h, s, v int) (r, g, b int) { 237 | H := int(math.Floor(float64(h) / 60)) 238 | S, V := float64(s)/100, float64(v)/100 239 | f := float64(h)/60 - float64(H) 240 | p := V * (1 - S) 241 | q := V * (1 - S*f) 242 | t := V * (1 - S*(1-f)) 243 | 244 | switch H { 245 | case 0, 6: 246 | r, g, b = int(255*V), int(255*t), int(255*p) 247 | case 1: 248 | r, g, b = int(255*q), int(255*V), int(255*p) 249 | case 2: 250 | r, g, b = int(255*p), int(255*V), int(255*t) 251 | case 3: 252 | r, g, b = int(255*p), int(255*q), int(255*V) 253 | case 4: 254 | r, g, b = int(255*t), int(255*p), int(255*V) 255 | case 5: 256 | r, g, b = int(255*V), int(255*p), int(255*q) 257 | default: 258 | panic(fmt.Sprintf("Ooops: Strange H value %d in hsv2rgb(%d,%d,%d).", H, h, s, v)) 259 | } 260 | 261 | return 262 | } 263 | 264 | func f3max(a, b, c float64) float64 { 265 | switch true { 266 | case a > b && a >= c: 267 | return a 268 | case b > c && b >= a: 269 | return b 270 | case c > a && c >= b: 271 | return c 272 | } 273 | return a 274 | } 275 | 276 | func f3min(a, b, c float64) float64 { 277 | switch true { 278 | case a < b && a <= c: 279 | return a 280 | case b < c && b <= a: 281 | return b 282 | case c < a && c <= b: 283 | return c 284 | } 285 | return a 286 | } 287 | 288 | func rgb2hsv(r, g, b int) (h, s, v int) { 289 | R, G, B := float64(r)/255, float64(g)/255, float64(b)/255 290 | 291 | if R == G && G == B { 292 | h, s = 0, 0 293 | v = int(r * 255) 294 | } else { 295 | max, min := f3max(R, G, B), f3min(R, G, B) 296 | if max == R { 297 | h = int(60 * (G - B) / (max - min)) 298 | } else if max == G { 299 | h = int(60 * (2 + (B-R)/(max-min))) 300 | } else { 301 | h = int(60 * (4 + (R-G)/(max-min))) 302 | } 303 | if max == 0 { 304 | s = 0 305 | } else { 306 | s = int(100 * (max - min) / max) 307 | } 308 | v = int(100 * max) 309 | } 310 | if h < 0 { 311 | h += 360 312 | } 313 | return 314 | } 315 | 316 | func lighter(col color.Color, f float64) color.NRGBA { 317 | r, g, b, a := col.RGBA() 318 | h, s, v := rgb2hsv(int(r/256), int(g/256), int(b/256)) 319 | f = 1 - f 320 | s = int(float64(s) * f) 321 | v += int((100 - float64(v)) * f) 322 | if v > 100 { 323 | v = 100 324 | } 325 | rr, gg, bb := hsv2rgb(h, s, v) 326 | 327 | return color.NRGBA{uint8(rr), uint8(gg), uint8(bb), uint8(a / 256)} 328 | } 329 | 330 | func darker(col color.Color, f float64) color.NRGBA { 331 | r, g, b, a := col.RGBA() 332 | h, s, v := rgb2hsv(int(r), int(g), int(b)) 333 | f = 1 - f 334 | v = int(float64(v) * f) 335 | s += int((100 - float64(s)) * f) 336 | if s > 100 { 337 | s = 100 338 | } 339 | rr, gg, bb := hsv2rgb(h, s, v) 340 | 341 | return color.NRGBA{uint8(rr), uint8(gg), uint8(bb), uint8(a / 256)} 342 | } 343 | -------------------------------------------------------------------------------- /style_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "testing" 7 | ) 8 | 9 | func TestRgb2Hsv(t *testing.T) { 10 | type rgbhsv struct{ r, g, b, h, s, v int } 11 | r2h := []rgbhsv{{255, 0, 0, 0, 100, 100}, {0, 128, 0, 120, 100, 50}, {255, 255, 0, 60, 100, 100}, 12 | {255, 0, 255, 300, 100, 100}} 13 | for _, x := range r2h { 14 | h, s, v := rgb2hsv(x.r, x.g, x.b) 15 | if h != x.h || s != x.s || v != x.v { 16 | t.Errorf("Expected hsv=%d,%d,%d, got %d,%d,%d for rgb=%d,%d,%d", x.h, x.s, x.v, h, s, v, x.r, x.g, x.b) 17 | } 18 | } 19 | } 20 | 21 | func TestBrighten(t *testing.T) { 22 | c2t := func(c color.Color) string { 23 | r, g, b, _ := c.RGBA() 24 | return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8) 25 | } 26 | 27 | for _, col := range []color.RGBA{{0xff, 0, 0, 0xff}, {0, 0xff, 0, 0xff}, {0, 0, 0xff, 0xff}} { 28 | for _, f := range []float64{0.1, 0.3, 0.5, 0.7, 0.9} { 29 | fmt.Printf("%s --- %.2f --> %s %s\n", 30 | c2t(col), f, c2t(lighter(col, f)), c2t(darker(col, f))) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /svgg/svg.go: -------------------------------------------------------------------------------- 1 | package svgg 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "math" 7 | 8 | "github.com/ajstarks/svgo" 9 | "github.com/vdobler/chart" 10 | ) 11 | 12 | // SvgGraphics implements BasicGraphics and uses the generic implementations 13 | type SvgGraphics struct { 14 | svg *svg.SVG 15 | w, h int 16 | font string 17 | fs int 18 | bg color.RGBA 19 | tx, ty int 20 | } 21 | 22 | // New creates a new SvgGraphics of dimension w x h, with a default font font of size fontsize. 23 | func New(sp *svg.SVG, width, height int, font string, fontsize int, background color.RGBA) *SvgGraphics { 24 | if font == "" { 25 | font = "Helvetica" 26 | } 27 | if fontsize == 0 { 28 | fontsize = 12 29 | } 30 | s := SvgGraphics{svg: sp, w: width, h: height, font: font, fs: fontsize, bg: background} 31 | return &s 32 | } 33 | 34 | // AddTo returns a new ImageGraphics which will write to (width x height) sized 35 | // area starting at (x,y) on the provided SVG 36 | func AddTo(sp *svg.SVG, x, y, width, height int, font string, fontsize int, background color.RGBA) *SvgGraphics { 37 | s := New(sp, width, height, font, fontsize, background) 38 | s.tx, s.ty = x, y 39 | return s 40 | } 41 | 42 | func (sg *SvgGraphics) Options() chart.PlotOptions { 43 | return nil 44 | } 45 | 46 | func (sg *SvgGraphics) Begin() { 47 | font, fs := sg.font, sg.fs 48 | if font == "" { 49 | font = "Helvetica" 50 | } 51 | if fs == 0 { 52 | fs = 12 53 | } 54 | sg.svg.Gstyle(fmt.Sprintf("font-family: %s; font-size: %d", 55 | font, fs)) 56 | if sg.tx != 0 || sg.ty != 0 { 57 | sg.svg.Gtransform(fmt.Sprintf("translate(%d %d)", sg.tx, sg.ty)) 58 | } 59 | 60 | bgc := fmt.Sprintf("#%02x%02x%02x", sg.bg.R, sg.bg.G, sg.bg.B) 61 | opa := fmt.Sprintf("%.4f", float64(sg.bg.A)/255) 62 | bgs := fmt.Sprintf("stroke: %s; opacity: %s; fill: %s; fill-opacity: %s", bgc, opa, bgc, opa) 63 | sg.svg.Rect(0, 0, sg.w, sg.h, bgs) 64 | } 65 | 66 | func (sg *SvgGraphics) End() { 67 | sg.svg.Gend() 68 | if sg.tx != 0 || sg.ty != 0 { 69 | sg.svg.Gend() 70 | } 71 | } 72 | 73 | func (sg *SvgGraphics) Background() (r, g, b, a uint8) { 74 | return sg.bg.R, sg.bg.G, sg.bg.B, sg.bg.A 75 | } 76 | 77 | func (sg *SvgGraphics) Dimensions() (int, int) { 78 | return sg.w, sg.h 79 | } 80 | 81 | func (sg *SvgGraphics) fontheight(font chart.Font) (fh int) { 82 | if sg.fs <= 14 { 83 | fh = sg.fs + int(font.Size) 84 | } else if sg.fs <= 20 { 85 | fh = sg.fs + 2*int(font.Size) 86 | } else { 87 | fh = sg.fs + 3*int(font.Size) 88 | } 89 | 90 | if fh == 0 { 91 | fh = 12 92 | } 93 | return 94 | } 95 | 96 | func (sg *SvgGraphics) FontMetrics(font chart.Font) (fw float32, fh int, mono bool) { 97 | if font.Name == "" { 98 | font.Name = sg.font 99 | } 100 | fh = sg.fontheight(font) 101 | 102 | switch font.Name { 103 | case "Arial": 104 | fw, mono = 0.5*float32(fh), false 105 | case "Helvetica": 106 | fw, mono = 0.5*float32(fh), false 107 | case "Times": 108 | fw, mono = 0.51*float32(fh), false 109 | case "Courier": 110 | fw, mono = 0.62*float32(fh), true 111 | default: 112 | fw, mono = 0.75*float32(fh), false 113 | } 114 | 115 | // fmt.Printf("FontMetric of %s/%d: %.1f x %d %t\n", style.Font, style.FontSize, fw, fh, mono) 116 | return 117 | } 118 | 119 | func (sg *SvgGraphics) TextLen(t string, font chart.Font) int { 120 | return chart.GenericTextLen(sg, t, font) 121 | } 122 | 123 | var dashlength [][]int = [][]int{[]int{}, []int{4, 1}, []int{1, 1}, []int{4, 1, 1, 1, 1, 1}, []int{4, 4}, []int{1, 3}} 124 | 125 | func (sg *SvgGraphics) Line(x0, y0, x1, y1 int, style chart.Style) { 126 | s := linestyle(style) 127 | sg.svg.Line(x0, y0, x1, y1, s) 128 | } 129 | 130 | func (sg *SvgGraphics) Text(x, y int, t string, align string, rot int, f chart.Font) { 131 | if len(align) == 1 { 132 | align = "c" + align 133 | } 134 | _, fh, _ := sg.FontMetrics(f) 135 | 136 | trans := "" 137 | if rot != 0 { 138 | trans = fmt.Sprintf("transform=\"rotate(%d %d %d)\"", -rot, x, y) 139 | } 140 | 141 | // Hack because baseline alignments in svg often broken 142 | switch align[0] { 143 | case 'b': 144 | y += 0 145 | case 't': 146 | y += fh 147 | default: 148 | y += (4 * fh) / 10 // centered 149 | } 150 | s := "text-anchor:" 151 | switch align[1] { 152 | case 'l': 153 | s += "begin" 154 | case 'r': 155 | s += "end" 156 | default: 157 | s += "middle" 158 | } 159 | if f.Color != nil { 160 | s += "; fill:" + hexcol(f.Color) 161 | } 162 | if f.Name != "" { 163 | s += "; font-family:" + f.Name 164 | } 165 | if f.Size != 0 { 166 | s += fmt.Sprintf("; font-size: %d", fh) 167 | } 168 | 169 | sg.svg.Text(x, y, t, trans, s) 170 | } 171 | 172 | func (sg *SvgGraphics) Symbol(x, y int, style chart.Style) { 173 | st := "" 174 | filled := "fill:solid" 175 | empty := "fill:none" 176 | if style.SymbolColor != nil { 177 | st += "stroke:" + hexcol(style.SymbolColor) 178 | filled = "fill:" + hexcol(style.SymbolColor) 179 | } 180 | f := style.SymbolSize 181 | if f == 0 { 182 | f = 1 183 | } 184 | lw := 1 185 | if style.LineWidth > 1 { 186 | lw = style.LineWidth 187 | } 188 | 189 | const n = 5 // default size 190 | a := int(n*f + 0.5) // standard 191 | b := int(n/2*f + 0.5) // smaller 192 | c := int(1.155*n*f + 0.5) // triangel long sist 193 | d := int(0.577*n*f + 0.5) // triangle short dist 194 | e := int(0.866*n*f + 0.5) // diagonal 195 | 196 | sg.svg.Gstyle(fmt.Sprintf("%s; stroke-width: %d", st, lw)) 197 | switch style.Symbol { 198 | case '*': 199 | sg.svg.Line(x-e, y-e, x+e, y+e) 200 | sg.svg.Line(x-e, y+e, x+e, y-e) 201 | fallthrough 202 | case '+': 203 | sg.svg.Line(x-a, y, x+a, y) 204 | sg.svg.Line(x, y-a, x, y+a) 205 | case 'X': 206 | sg.svg.Line(x-e, y-e, x+e, y+e) 207 | sg.svg.Line(x-e, y+e, x+e, y-e) 208 | case 'o': 209 | sg.svg.Circle(x, y, a, empty) 210 | case '0': 211 | sg.svg.Circle(x, y, a, empty) 212 | sg.svg.Circle(x, y, b, empty) 213 | case '.': 214 | if b >= 4 { 215 | b /= 2 216 | } 217 | sg.svg.Circle(x, y, b, empty) 218 | case '@': 219 | sg.svg.Circle(x, y, a, filled) 220 | case '=': 221 | sg.svg.Rect(x-e, y-e, 2*e, 2*e, empty) 222 | case '#': 223 | sg.svg.Rect(x-e, y-e, 2*e, 2*e, filled) 224 | case 'A': 225 | sg.svg.Polygon([]int{x - a, x + a, x}, []int{y + d, y + d, y - c}, filled) 226 | case '%': 227 | sg.svg.Polygon([]int{x - a, x + a, x}, []int{y + d, y + d, y - c}, empty) 228 | case 'W': 229 | sg.svg.Polygon([]int{x - a, x + a, x}, []int{y - c, y - c, y + d}, filled) 230 | case 'V': 231 | sg.svg.Polygon([]int{x - a, x + a, x}, []int{y - c, y - c, y + d}, empty) 232 | case 'Z': 233 | sg.svg.Polygon([]int{x - e, x, x + e, x}, []int{y, y + e, y, y - e}, filled) 234 | case '&': 235 | sg.svg.Polygon([]int{x - e, x, x + e, x}, []int{y, y + e, y, y - e}, empty) 236 | default: 237 | sg.svg.Text(x, y, "?", "text-anchor:middle; alignment-baseline:middle") 238 | } 239 | sg.svg.Gend() 240 | 241 | } 242 | 243 | func (sg *SvgGraphics) Rect(x, y, w, h int, style chart.Style) { 244 | var s string 245 | x, y, w, h = chart.SanitizeRect(x, y, w, h, style.LineWidth) 246 | linecol := style.LineColor 247 | if linecol != nil { 248 | s = fmt.Sprintf("stroke:%s; ", hexcol(linecol)) 249 | s += fmt.Sprintf("stroke-opacity: %s; ", alpha(linecol)) 250 | } else { 251 | s = "stroke:#808080; " 252 | } 253 | s += fmt.Sprintf("stroke-width: %d; ", style.LineWidth) 254 | if style.FillColor != nil { 255 | s += fmt.Sprintf("fill: %s; fill-opacity: %s", hexcol(style.FillColor), alpha(style.FillColor)) 256 | } else { 257 | s += "fill-opacity: 0" 258 | } 259 | sg.svg.Rect(x, y, w, h, s) 260 | // GenericRect(sg, x, y, w, h, style) // TODO 261 | } 262 | 263 | func (sg *SvgGraphics) Path(x, y []int, style chart.Style) { 264 | n := len(x) 265 | if len(y) < n { 266 | n = len(y) 267 | } 268 | path := fmt.Sprintf("M %d,%d", x[0], y[0]) 269 | for i := 1; i < n; i++ { 270 | path += fmt.Sprintf("L %d,%d", x[i], y[i]) 271 | } 272 | st := linestyle(style) 273 | sg.svg.Path(path, st) 274 | } 275 | 276 | func (sg *SvgGraphics) Wedge(x, y, ro, ri int, phi, psi float64, style chart.Style) { 277 | panic("No Wedge() for SvgGraphics.") 278 | } 279 | 280 | func (sg *SvgGraphics) XAxis(xr chart.Range, ys, yms int, options chart.PlotOptions) { 281 | chart.GenericXAxis(sg, xr, ys, yms, options) 282 | } 283 | func (sg *SvgGraphics) YAxis(yr chart.Range, xs, xms int, options chart.PlotOptions) { 284 | chart.GenericYAxis(sg, yr, xs, xms, options) 285 | } 286 | 287 | func linestyle(style chart.Style) (s string) { 288 | lw := style.LineWidth 289 | if style.LineColor != nil { 290 | s = fmt.Sprintf("stroke:%s; ", hexcol(style.LineColor)) 291 | } 292 | s += fmt.Sprintf("stroke-width: %d; fill:none; ", lw) 293 | s += fmt.Sprintf("opacity: %s; ", alpha(style.LineColor)) 294 | if style.LineStyle != chart.SolidLine { 295 | s += fmt.Sprintf("stroke-dasharray:") 296 | for _, d := range dashlength[style.LineStyle] { 297 | s += fmt.Sprintf(" %d", d*lw) 298 | } 299 | } 300 | return 301 | } 302 | 303 | func (sg *SvgGraphics) Scatter(points []chart.EPoint, plotstyle chart.PlotStyle, style chart.Style) { 304 | chart.GenericScatter(sg, points, plotstyle, style) 305 | 306 | /*********************************************** 307 | // First pass: Error bars 308 | ebs := style 309 | ebs.LineColor, ebs.LineWidth, ebs.LineStyle = ebs.FillColor, 1, chart.SolidLine 310 | if ebs.LineColor == "" { 311 | ebs.LineColor = "#404040" 312 | } 313 | if ebs.LineWidth == 0 { 314 | ebs.LineWidth = 1 315 | } 316 | for _, p := range points { 317 | xl, yl, xh, yh := p.BoundingBox() 318 | // fmt.Printf("Draw %d: %f %f-%f\n", i, p.DeltaX, xl,xh) 319 | if !math.IsNaN(p.DeltaX) { 320 | sg.Line(int(xl), int(p.Y), int(xh), int(p.Y), ebs) 321 | } 322 | if !math.IsNaN(p.DeltaY) { 323 | sg.Line(int(p.X), int(yl), int(p.X), int(yh), ebs) 324 | } 325 | } 326 | 327 | // Second pass: Line 328 | if (plotstyle&chart.PlotStyleLines) != 0 && len(points) > 0 { 329 | path := fmt.Sprintf("M %d,%d", int(points[0].X), int(points[0].Y)) 330 | for i := 1; i < len(points); i++ { 331 | path += fmt.Sprintf("L %d,%d", int(points[i].X), int(points[i].Y)) 332 | } 333 | st := linestyle(style) 334 | sg.svg.Path(path, st) 335 | } 336 | 337 | // Third pass: symbols 338 | if (plotstyle&chart.PlotStylePoints) != 0 && len(points) != 0 { 339 | for _, p := range points { 340 | sg.Symbol(int(p.X), int(p.Y), style) 341 | } 342 | } 343 | 344 | ****************************************************/ 345 | } 346 | 347 | func (sg *SvgGraphics) Boxes(boxes []chart.Box, width int, style chart.Style) { 348 | chart.GenericBoxes(sg, boxes, width, style) 349 | } 350 | 351 | func (sg *SvgGraphics) Key(x, y int, key chart.Key, options chart.PlotOptions) { 352 | chart.GenericKey(sg, x, y, key, options) 353 | } 354 | 355 | func (sg *SvgGraphics) Bars(bars []chart.Barinfo, style chart.Style) { 356 | chart.GenericBars(sg, bars, style) 357 | } 358 | 359 | func (sg *SvgGraphics) Rings(wedges []chart.Wedgeinfo, x, y, ro, ri int) { 360 | for _, w := range wedges { 361 | var s string 362 | linecol := w.Style.LineColor 363 | if linecol != nil { 364 | s = fmt.Sprintf("stroke:%s; ", hexcol(linecol)) 365 | s += fmt.Sprintf("opacity: %s; ", alpha(linecol)) 366 | } else { 367 | s = "stroke:%s; #808080; " 368 | } 369 | s += fmt.Sprintf("stroke-width: %d; ", w.Style.LineWidth) 370 | var sf string 371 | if w.Style.FillColor != nil { 372 | sf = fmt.Sprintf("fill: %s; fill-opacity: %s", hexcol(w.Style.FillColor), alpha(w.Style.FillColor)) 373 | } else { 374 | sf = "fill-opacity: 0" 375 | } 376 | 377 | if math.Abs(w.Phi-w.Psi) >= 4*math.Pi { 378 | sg.svg.Circle(x, y, ro, s+sf) 379 | if ri > 0 { 380 | sf = "fill: #ffffff; fill-opacity: 1" 381 | sg.svg.Circle(x, y, ri, s+sf) 382 | } 383 | continue 384 | } 385 | 386 | var d string 387 | p := 0.4 * float64(w.Style.LineWidth+w.Shift) 388 | cphi, sphi := math.Cos(w.Phi), math.Sin(w.Phi) 389 | cpsi, spsi := math.Cos(w.Psi), math.Sin(w.Psi) 390 | 391 | if ri <= 0 { 392 | // real wedge drawn as center -> outer radius -> arc -> closed to center 393 | rf := float64(ro) 394 | a := math.Sin((w.Psi - w.Phi) / 2) 395 | dx, dy := p*math.Cos((w.Phi+w.Psi)/2)/a, p*math.Sin((w.Phi+w.Psi)/2)/a 396 | d = fmt.Sprintf("M %d,%d ", x+int(dx+0.5), y+int(dy+0.5)) 397 | 398 | dx, dy = p*math.Cos(w.Phi+math.Pi/2), p*math.Sin(w.Phi+math.Pi/2) 399 | d += fmt.Sprintf("L %d,%d ", int(rf*cphi+0.5+dx)+x, int(rf*sphi+0.5+dy)+y) 400 | 401 | dx, dy = p*math.Cos(w.Psi-math.Pi/2), p*math.Sin(w.Psi-math.Pi/2) 402 | if math.Abs(w.Phi-w.Psi)>math.Pi { 403 | d += fmt.Sprintf("A %d,%d 0 1 1 %d,%d ", ro, ro, int(rf*cpsi+0.5+dx)+x, int(rf*spsi+0.5+dy)+y) } else { 404 | d += fmt.Sprintf("A %d,%d 0 0 1 %d,%d ", ro, ro, int(rf*cpsi+0.5+dx)+x, int(rf*spsi+0.5+dy)+y) } 405 | d += fmt.Sprintf("z") 406 | } else { 407 | // ring drawn as inner radius -> outer radius -> outer arc -> inner radius -> inner arc 408 | rof, rif := float64(ro), float64(ri) 409 | dx, dy := p*math.Cos(w.Phi+math.Pi/2), p*math.Sin(w.Phi+math.Pi/2) 410 | a, b := int(rif*cphi+0.5+dx)+x, int(rif*sphi+0.5+dy)+y 411 | d = fmt.Sprintf("M %d,%d ", a, b) 412 | d += fmt.Sprintf("L %d,%d ", int(rof*cphi+0.5+dx)+x, int(rof*sphi+0.5+dy)+y) 413 | 414 | dx, dy = p*math.Cos(w.Psi-math.Pi/2), p*math.Sin(w.Psi-math.Pi/2) 415 | if math.Abs(w.Phi-w.Psi)>math.Pi { 416 | d += fmt.Sprintf("A %d,%d 0 1 1 %d,%d ", ro, ro, int(rof*cpsi+0.5+dx)+x, int(rof*spsi+0.5+dy)+y) } else { 417 | d += fmt.Sprintf("A %d,%d 0 0 1 %d,%d ", ro, ro, int(rof*cpsi+0.5+dx)+x, int(rof*spsi+0.5+dy)+y) } 418 | d += fmt.Sprintf("L %d,%d ", int(rif*cpsi+0.5+dx)+x, int(rif*spsi+0.5+dy)+y) 419 | if math.Abs(w.Phi-w.Psi)>math.Pi { 420 | d += fmt.Sprintf("A %d,%d 0 1 0 %d,%d ", ri, ri, a, b) } else { 421 | d += fmt.Sprintf("A %d,%d 0 0 0 %d,%d ", ri, ri, a, b) } 422 | d += fmt.Sprintf("z") 423 | 424 | } 425 | 426 | sg.svg.Path(d, s+sf) 427 | 428 | if w.Text != "" { 429 | _, fh, _ := sg.FontMetrics(w.Font) 430 | alpha := (w.Phi + w.Psi) / 2 431 | var rt int 432 | if ri > 0 { 433 | rt = (ri + ro) / 2 434 | } else { 435 | rt = ro - 3*fh 436 | if rt <= ro/2 { 437 | rt = ro - 2*fh 438 | } 439 | } 440 | tx, ty := int(float64(rt)*math.Cos(alpha)+0.5)+x, int(float64(rt)*math.Sin(alpha)+0.5)+y 441 | 442 | sg.Text(tx, ty, w.Text, "cc", 0, w.Font) 443 | } 444 | } 445 | } 446 | 447 | func hexcol(col color.Color) string { 448 | r, g, b, a := col.RGBA() 449 | if a == 0 { 450 | return "#000000" // doesn't matter as fully transparent 451 | } 452 | a = a >> 8 453 | r = ((r * 0xff) / a) >> 8 454 | g = ((g * 0xff) / a) >> 8 455 | b = ((b * 0xff) / a) >> 8 456 | return fmt.Sprintf("#%.2x%.2x%.2x", r, g, b) 457 | } 458 | 459 | func alpha(col color.Color) string { 460 | _, _, _, a := col.RGBA() 461 | return fmt.Sprintf("%.3f", float64(a)/0xffff) 462 | } 463 | 464 | var _ chart.Graphics = &SvgGraphics{} 465 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Represents a tic-distance in a timed axis 9 | type TimeDelta interface { 10 | Seconds() int64 // amount of delta in seconds 11 | RoundDown(t time.Time) time.Time // Round dow t to "whole" delta 12 | String() string // retrieve string representation 13 | Format(t time.Time) string // format t properly 14 | Period() bool // true if this delta is a time period (like a month) 15 | } 16 | 17 | // Copy value of src to dest. 18 | func cpTime(dest, src time.Time) { 19 | // TODO remove 20 | } 21 | 22 | // Second 23 | type Second struct { 24 | Num int 25 | } 26 | 27 | func (s Second) Seconds() int64 { return int64(s.Num) } 28 | func (s Second) RoundDown(t time.Time) time.Time { 29 | return t.Add(time.Duration((s.Num*(t.Second()/s.Num))-t.Second()) * time.Second) 30 | } 31 | func (s Second) String() string { return fmt.Sprintf("%d seconds(s)", s.Num) } 32 | func (s Second) Format(t time.Time) string { return fmt.Sprintf("%02d'%02d\"", t.Minute(), t.Second()) } 33 | func (s Second) Period() bool { return false } 34 | 35 | // Minute 36 | type Minute struct { 37 | Num int 38 | } 39 | 40 | func (m Minute) Seconds() int64 { return int64(60 * m.Num) } 41 | func (m Minute) RoundDown(t time.Time) time.Time { 42 | return t.Add(time.Duration(m.Num*(t.Minute()/m.Num)-t.Minute())*time.Minute - time.Duration(t.Second())*time.Second) 43 | } 44 | func (m Minute) String() string { return fmt.Sprintf("%d minute(s)", m.Num) } 45 | func (m Minute) Format(t time.Time) string { return fmt.Sprintf("%02d'", t.Minute()) } 46 | func (m Minute) Period() bool { return false } 47 | 48 | // Hour 49 | type Hour struct{ Num int } 50 | 51 | func (h Hour) Seconds() int64 { return 60 * 60 * int64(h.Num) } 52 | func (h Hour) RoundDown(t time.Time) time.Time { 53 | return time.Date(t.Year(), t.Month(), t.Day(), h.Num*(t.Hour()/h.Num), 0, 0, 0, t.Location()) 54 | } 55 | func (h Hour) String() string { return fmt.Sprintf("%d hours(s)", h.Num) } 56 | func (h Hour) Format(t time.Time) string { return fmt.Sprintf("%02d:%02d", t.Hour(), t.Minute()) } 57 | func (h Hour) Period() bool { return false } 58 | 59 | // Day 60 | type Day struct{ Num int } 61 | 62 | func (d Day) Seconds() int64 { return 60 * 60 * 24 * int64(d.Num) } 63 | func (d Day) RoundDown(t time.Time) time.Time { 64 | return time.Date(t.Year(), t.Month(), d.Num*((t.Day()-1)/d.Num)+1, 0, 0, 0, 0, t.Location()) 65 | } 66 | func (d Day) String() string { return fmt.Sprintf("%d day(s)", d.Num) } 67 | func (d Day) Format(t time.Time) string { return fmt.Sprintf("%s", t.Format("Mon")) } 68 | func (d Day) Period() bool { return true } 69 | 70 | // Week 71 | type Week struct { 72 | Num int 73 | } 74 | 75 | func (w Week) Seconds() int64 { return 60 * 60 * 24 * 7 * int64(w.Num) } 76 | func (w Week) RoundDown(t time.Time) time.Time { 77 | org := t.Format("Mon 2006-01-02") 78 | _, week := t.ISOWeek() 79 | shift := int64(60 * 60 * 24 * (t.Weekday() - time.Monday)) 80 | t = t.Add(-time.Duration(shift) * time.Second) 81 | t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()) 82 | // daylight saving and that like might lead to different real shift 83 | 84 | _, week2 := t.ISOWeek() 85 | for week2 < week { 86 | DebugLogger.Printf("B %s", t) 87 | t = t.Add(time.Second * 60 * 60 * 36) 88 | t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()) 89 | _, week2 = t.ISOWeek() 90 | } 91 | for week2 > week { 92 | DebugLogger.Printf("C %s", t) 93 | t = t.Add(-time.Second * 60 * 60 * 36) 94 | t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()) 95 | _, week2 = t.ISOWeek() 96 | } 97 | DebugLogger.Printf("Week.Roundown(%s) --> %s", org, t.Format("Mon 2006-01-02")) 98 | return t 99 | } 100 | func (w Week) String() string { return fmt.Sprintf("%d week(s)", w.Num) } 101 | func (w Week) Format(t time.Time) string { 102 | _, week := t.ISOWeek() 103 | return fmt.Sprintf("W %d", week) 104 | } 105 | func (w Week) Period() bool { return true } 106 | 107 | // Month 108 | type Month struct { 109 | Num int 110 | } 111 | 112 | func (m Month) Seconds() int64 { return 60 * 60 * 24 * 365.25 / 12 * int64(m.Num) } 113 | func (m Month) RoundDown(t time.Time) time.Time { 114 | return time.Date(t.Year(), time.Month(m.Num*(int(t.Month())-1)/m.Num+1), 115 | 1, 0, 0, 0, 0, t.Location()) 116 | } 117 | func (m Month) String() string { return fmt.Sprintf("%d month(s)", m.Num) } 118 | func (m Month) Format(t time.Time) string { 119 | if m.Num == 3 { // quarter years 120 | return fmt.Sprintf("Q%d %d", (int(t.Month())-1)/3+1, t.Year()) 121 | } 122 | if m.Num == 6 { // half years 123 | return fmt.Sprintf("H%d %d", (int(t.Month())-1)/6+1, t.Year()) 124 | } 125 | return fmt.Sprintf("%02d.%d", int(t.Month()), t.Year()) 126 | } 127 | func (m Month) Period() bool { return true } 128 | 129 | // Year 130 | type Year struct { 131 | Num int 132 | } 133 | 134 | func (y Year) Seconds() int64 { return 60 * 60 * 24 * 365.25 * int64(y.Num) } 135 | func (y Year) RoundDown(t time.Time) time.Time { 136 | orig := t.Year() 137 | rd := y.Num * (orig / y.Num) 138 | t = time.Date(rd, 1, 1, 0, 0, 0, 0, t.Location()) 139 | // TODO handle shifts in DLS and that 140 | DebugLogger.Printf("Year.RoundDown from %d to %d", orig, rd) 141 | return t 142 | } 143 | func (y Year) String() string { return fmt.Sprintf("%d year(s)", y.Num) } 144 | func (y Year) Format(t time.Time) string { 145 | if y.Num == 10 { 146 | y := t.Year() / 10 147 | d := y % 10 148 | return fmt.Sprintf("%d0-%d9", y, d) 149 | } else if y.Num == 100 { 150 | y := t.Year() / 100 151 | return fmt.Sprintf("%d cen.", y) 152 | } 153 | return fmt.Sprintf("%d", t.Year()) 154 | } 155 | func (y Year) Period() bool { return true } 156 | 157 | // Delta is a list of increasing time deltas used to construct tic spacings 158 | // for date/time axis. 159 | // Must be sorted min to max according to Seconds() of each member. 160 | var Delta []TimeDelta = []TimeDelta{ 161 | Second{1}, Second{5}, Second{15}, 162 | Minute{1}, Minute{5}, Minute{15}, 163 | Hour{1}, Hour{6}, 164 | Day{1}, Week{1}, 165 | Month{1}, Month{3}, Month{6}, 166 | Year{1}, Year{10}, Year{100}, 167 | } 168 | 169 | // RoundUp will round tp up to next "full" d. 170 | func RoundUp(t time.Time, d TimeDelta) time.Time { 171 | // works only because all TimeDeltas are more than 1.5 times as large as the next lower 172 | shift := d.Seconds() 173 | shift += shift / 2 174 | t = d.RoundDown(t) 175 | t = t.Add(time.Duration(shift) * time.Second) 176 | t = d.RoundDown(t) 177 | DebugLogger.Printf("RoundUp( %s, %s ) --> %s ", t.Format("2006-01-02 15:04:05 (Mon)"), d.String(), 178 | t.Format("2006-01-02 15:04:05 (Mon)")) 179 | return t 180 | } 181 | 182 | // RoundNext will round t to nearest full d. 183 | func RoundNext(t time.Time, d TimeDelta) time.Time { 184 | DebugLogger.Printf("RoundNext( %s, %s )", t.Format("2006-01-02 15:04:05 (Mon)"), d.String()) 185 | os := t.Unix() 186 | lt := d.RoundDown(t) 187 | shift := d.Seconds() 188 | shift += shift / 2 189 | ut := lt.Add(time.Duration(shift) * time.Second) // see RoundUp() 190 | ut = d.RoundDown(ut) 191 | ld := os - lt.Unix() 192 | ud := ut.Unix() - os 193 | if ld < ud { 194 | return lt 195 | } 196 | return ut 197 | } 198 | 199 | // RoundDown will round tp down to next "full" d. 200 | func RoundDown(t time.Time, d TimeDelta) time.Time { 201 | td := d.RoundDown(t) 202 | DebugLogger.Printf("RoundDown( %s, %s ) --> %s", t.Format("2006-01-02 15:04:05 (Mon)"), d.String(), 203 | td.Format("2006-01-02 15:04:05 (Mon)")) 204 | return td 205 | } 206 | 207 | func NextTimeDelta(d TimeDelta) TimeDelta { 208 | var i = 0 209 | sec := d.Seconds() 210 | for i < len(Delta) && Delta[i].Seconds() <= sec { 211 | i++ 212 | } 213 | if i < len(Delta) { 214 | return Delta[i] 215 | } 216 | return Delta[len(Delta)-1] 217 | } 218 | 219 | func MatchingTimeDelta(delta float64, fac float64) TimeDelta { 220 | var i = 0 221 | for i+1 < len(Delta) && delta > fac*float64(Delta[i+1].Seconds()) { 222 | i++ 223 | } 224 | DebugLogger.Printf("MatchingTimeDelta(%g): i=%d, %s...%s == %d...%d\n %t\n", 225 | delta, i, Delta[i], Delta[i+1], Delta[i].Seconds(), Delta[i+1].Seconds(), 226 | i+1 < len(Delta) && delta > fac*float64(Delta[i+1].Seconds())) 227 | if i+1 < len(Delta) { 228 | return Delta[i+1] 229 | } 230 | return Delta[len(Delta)-1] 231 | } 232 | 233 | func dayOfWeek(y, m, d int) int { 234 | t := time.Date(y, time.Month(m), d, 0, 0, 0, 0, nil) 235 | return int(t.Weekday()) 236 | } 237 | 238 | func FmtTime(sec int64, step TimeDelta) string { 239 | t := time.Unix(sec, 0) 240 | return step.Format(t) 241 | } 242 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestRoundDown(t *testing.T) { 9 | tf := "2006-01-02 15:04:05 MST" 10 | samples := []struct { 11 | date, expected string 12 | delta TimeDelta 13 | }{ 14 | // Simple cases 15 | {"2011-07-04 16:43:23 CEST", "2011-07-04 16:43:00 CEST", Minute{1}}, 16 | {"2011-07-04 16:43:23 CEST", "2011-07-04 16:40:00 CEST", Minute{5}}, 17 | {"2011-07-04 16:43:23 CEST", "2011-07-04 16:40:00 CEST", Minute{10}}, 18 | {"2011-07-04 16:43:23 CEST", "2011-07-04 16:30:00 CEST", Minute{15}}, 19 | {"2011-07-04 16:43:23 CEST", "2011-07-04 16:00:00 CEST", Hour{1}}, 20 | {"2011-07-04 16:43:23 CEST", "2011-07-04 12:00:00 CEST", Hour{6}}, 21 | 22 | // Around daylight saving switch 23 | {"2011-03-27 04:15:16 CEST", "2011-03-27 04:00:00 CEST", Hour{1}}, 24 | {"2011-03-27 04:15:16 CEST", "2011-03-27 00:00:00 CET", Hour{5}}, 25 | 26 | {"2011-07-04 16:43:23 CEST", "2011-01-01 00:00:00 CET", Year{1}}, 27 | {"2011-07-04 16:43:23 CEST", "2010-01-01 00:00:00 CET", Year{10}}, 28 | } 29 | 30 | for k, sample := range samples { 31 | date, e1 := time.Parse(tf, sample.date) 32 | expected, e2 := time.Parse(tf, sample.expected) 33 | if e1 != nil || e2 != nil { 34 | t.Fatalf("Unexpected error(s): %v %v", e1, e2) 35 | } 36 | date = date.Local() 37 | expected = expected.Local() 38 | date = sample.delta.RoundDown(date) 39 | if date.Unix() != expected.Unix() { 40 | t.Errorf("%d. RoundDown %s to %s != %s, was %s", k, 41 | sample.date, sample.delta, 42 | sample.expected, date.Format(tf)) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /txtg/buf.go: -------------------------------------------------------------------------------- 1 | package txtg 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // Different edge styles for boxes 8 | var Edge = [][4]rune{{'+', '+', '+', '+'}, {'.', '.', '\'', '\''}, {'/', '\\', '\\', '/'}} 9 | 10 | // A Text Buffer 11 | type TextBuf struct { 12 | Buf []rune // the internal buffer. Point (x,y) is mapped to x + y*(W+1) 13 | W, H int // Width and Height 14 | } 15 | 16 | // Set up a new TextBuf with width w and height h. 17 | func NewTextBuf(w, h int) (tb *TextBuf) { 18 | tb = new(TextBuf) 19 | tb.W, tb.H = w, h 20 | tb.Buf = make([]rune, (w+1)*h) 21 | for i, _ := range tb.Buf { 22 | tb.Buf[i] = ' ' 23 | } 24 | for i := 0; i < h; i++ { 25 | tb.Buf[i*(w+1)+w] = '\n' 26 | } 27 | // tb.Buf[0], tb.Buf[(w+1)*h-1] = 'X', 'X' 28 | return 29 | } 30 | 31 | // Put character c at (x,y) 32 | func (tb *TextBuf) Put(x, y int, c rune) { 33 | if x < 0 || y < 0 || x >= tb.W || y >= tb.H || c < ' ' { 34 | // debug.Printf("Ooooops Put(): %d, %d, %d='%c' \n", x, y, c, c) 35 | return 36 | } 37 | i := (tb.W+1)*y + x 38 | tb.Buf[i] = c 39 | } 40 | 41 | // Draw rectangle of width w and height h from corner at (x,y). 42 | // Use one of the corner style defined in Edge. 43 | // Interior is filled with charater fill iff != 0. 44 | func (tb *TextBuf) Rect(x, y, w, h int, style int, fill rune) { 45 | style = style % len(Edge) 46 | 47 | if h < 0 { 48 | h = -h 49 | y -= h 50 | } 51 | if w < 0 { 52 | w = -w 53 | x -= w 54 | } 55 | 56 | tb.Put(x, y, Edge[style][0]) 57 | tb.Put(x+w, y, Edge[style][1]) 58 | tb.Put(x, y+h, Edge[style][2]) 59 | tb.Put(x+w, y+h, Edge[style][3]) 60 | for i := 1; i < w; i++ { 61 | tb.Put(x+i, y, '-') 62 | tb.Put(x+i, y+h, '-') 63 | } 64 | for i := 1; i < h; i++ { 65 | tb.Put(x+w, y+i, '|') 66 | tb.Put(x, y+i, '|') 67 | if fill > 0 { 68 | for j := x + 1; j < x+w; j++ { 69 | tb.Put(j, y+i, fill) 70 | } 71 | } 72 | } 73 | } 74 | 75 | func (tb *TextBuf) Block(x, y, w, h int, fill rune) { 76 | if h < 0 { 77 | h = -h 78 | y -= h 79 | } 80 | if w < 0 { 81 | w = -w 82 | x -= w 83 | } 84 | for i := 0; i < w; i++ { 85 | for j := 0; j <= h; j++ { 86 | tb.Put(x+i, y+j, fill) 87 | } 88 | } 89 | } 90 | 91 | // Return real character len of s (rune count). 92 | func StrLen(s string) (n int) { 93 | for _, _ = range s { 94 | n++ 95 | } 96 | return 97 | } 98 | 99 | // Print text txt at (x,y). Horizontal display for align in [-1,1], 100 | // vasrtical alignment for align in [2,4] 101 | // align: -1: left; 0: centered; 1: right; 2: top, 3: center, 4: bottom 102 | func (tb *TextBuf) Text(x, y int, txt string, align int) { 103 | if align <= 1 { 104 | switch align { 105 | case 0: 106 | x -= StrLen(txt) / 2 107 | case 1: 108 | x -= StrLen(txt) 109 | } 110 | i := 0 111 | for _, r := range txt { 112 | tb.Put(x+i, y, r) 113 | i++ 114 | } 115 | } else { 116 | switch align { 117 | case 3: 118 | y -= StrLen(txt) / 2 119 | case 2: 120 | x -= StrLen(txt) 121 | } 122 | i := 0 123 | for _, r := range txt { 124 | tb.Put(x, y+i, r) 125 | i++ 126 | } 127 | } 128 | } 129 | 130 | // Paste buf at (x,y) 131 | func (tb *TextBuf) Paste(x, y int, buf *TextBuf) { 132 | s := buf.W + 1 133 | for i := 0; i < buf.W; i++ { 134 | for j := 0; j < buf.H; j++ { 135 | tb.Put(x+i, y+j, buf.Buf[i+s*j]) 136 | } 137 | } 138 | } 139 | 140 | func (tb *TextBuf) Line(x0, y0, x1, y1 int, symbol rune) { 141 | // handle trivial cases first 142 | if x0 == x1 { 143 | if y0 > y1 { 144 | y0, y1 = y1, y0 145 | } 146 | for ; y0 <= y1; y0++ { 147 | tb.Put(x0, y0, symbol) 148 | } 149 | return 150 | } 151 | if y0 == y1 { 152 | if x0 > x1 { 153 | x0, x1 = x1, x0 154 | } 155 | for ; x0 <= x1; x0++ { 156 | tb.Put(x0, y0, symbol) 157 | } 158 | return 159 | } 160 | dx, dy := abs(x1-x0), -abs(y1-y0) 161 | sx, sy := sign(x1-x0), sign(y1-y0) 162 | err, e2 := dx+dy, 0 163 | for { 164 | tb.Put(x0, y0, symbol) 165 | if x0 == x1 && y0 == y1 { 166 | return 167 | } 168 | e2 = 2 * err 169 | if e2 >= dy { 170 | err += dy 171 | x0 += sx 172 | } 173 | if e2 <= dx { 174 | err += dx 175 | y0 += sy 176 | } 177 | 178 | } 179 | } 180 | 181 | // Convert to plain (utf8) string. 182 | func (tb *TextBuf) String() string { 183 | return string(tb.Buf) 184 | } 185 | 186 | func min(a, b int) int { 187 | if a < b { 188 | return a 189 | } 190 | return b 191 | } 192 | 193 | func max(a, b int) int { 194 | if a > b { 195 | return a 196 | } 197 | return b 198 | } 199 | 200 | func abs(a int) int { 201 | if a < 0 { 202 | return -a 203 | } 204 | return a 205 | } 206 | 207 | func sign(a int) int { 208 | if a < 0 { 209 | return -1 210 | } 211 | if a == 0 { 212 | return 0 213 | } 214 | return 1 215 | } 216 | 217 | // Debugging and tracing 218 | type debugging bool 219 | 220 | const debug debugging = true 221 | 222 | func (d debugging) Printf(fmt string, args ...interface{}) { 223 | if d { 224 | log.Printf(fmt, args...) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /txtg/text.go: -------------------------------------------------------------------------------- 1 | package txtg 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vdobler/chart" 6 | "math" 7 | ) 8 | 9 | // TextGraphics 10 | type TextGraphics struct { 11 | tb *TextBuf // the underlying text buffer 12 | w, h int // width and height 13 | xoff int // the initial radius for pie charts 14 | } 15 | 16 | // New creates a TextGraphic of dimensions w x h. 17 | func New(w, h int) *TextGraphics { 18 | tg := TextGraphics{} 19 | tg.tb = NewTextBuf(w, h) 20 | tg.w, tg.h = w, h 21 | tg.xoff = -1 22 | return &tg 23 | } 24 | 25 | func (g *TextGraphics) Options() chart.PlotOptions { 26 | return nil 27 | } 28 | 29 | func (g *TextGraphics) Begin() { 30 | g.tb = NewTextBuf(g.w, g.h) 31 | } 32 | 33 | func (g *TextGraphics) End() {} 34 | func (tg *TextGraphics) Background() (r, g, b, a uint8) { return 255, 255, 255, 255 } 35 | func (g *TextGraphics) Dimensions() (int, int) { 36 | return g.w, g.h 37 | } 38 | func (g *TextGraphics) FontMetrics(font chart.Font) (fw float32, fh int, mono bool) { 39 | return 1, 1, true 40 | } 41 | 42 | func (g *TextGraphics) TextLen(t string, font chart.Font) int { 43 | return len(t) 44 | } 45 | 46 | func (g *TextGraphics) Line(x0, y0, x1, y1 int, style chart.Style) { 47 | symbol := style.Symbol 48 | if symbol < ' ' || symbol > '~' { 49 | symbol = 'x' 50 | } 51 | g.tb.Line(x0, y0, x1, y1, rune(symbol)) 52 | } 53 | func (g *TextGraphics) Path(x, y []int, style chart.Style) { 54 | chart.GenericPath(g, x, y, style) 55 | } 56 | 57 | func (g *TextGraphics) Wedge(x, y, ro, ri int, phi, psi float64, style chart.Style) { 58 | chart.GenericWedge(g, x, y, ro, ri, phi, psi, CircleStretchFactor, style) 59 | } 60 | 61 | func (g *TextGraphics) Text(x, y int, t string, align string, rot int, font chart.Font) { 62 | // align: -1: left; 0: centered; 1: right; 2: top, 3: center, 4: bottom 63 | if len(align) == 2 { 64 | align = align[1:] 65 | } 66 | a := 0 67 | if rot == 0 { 68 | if align == "l" { 69 | a = -1 70 | } 71 | if align == "c" { 72 | a = 0 73 | } 74 | if align == "r" { 75 | a = 1 76 | } 77 | } else { 78 | if align == "l" { 79 | a = 2 80 | } 81 | if align == "c" { 82 | a = 3 83 | } 84 | if align == "r" { 85 | a = 4 86 | } 87 | } 88 | g.tb.Text(x, y, t, a) 89 | } 90 | 91 | func (g *TextGraphics) Rect(x, y, w, h int, style chart.Style) { 92 | chart.SanitizeRect(x, y, w, h, 1) 93 | // Border 94 | if style.LineWidth > 0 { 95 | for i := 0; i < w; i++ { 96 | g.tb.Put(x+i, y, rune(style.Symbol)) 97 | g.tb.Put(x+i, y+h-1, rune(style.Symbol)) 98 | } 99 | for i := 1; i < h-1; i++ { 100 | g.tb.Put(x, y+i, rune(style.Symbol)) 101 | g.tb.Put(x+w-1, y+i, rune(style.Symbol)) 102 | } 103 | } 104 | 105 | // Filling 106 | if style.FillColor != nil { 107 | // TODO: fancier logic 108 | var s int 109 | _, _, _, a := style.FillColor.RGBA() 110 | if a == 0xffff { 111 | s = '#' // black 112 | } else if a == 0 { 113 | s = ' ' // white 114 | } else { 115 | s = style.Symbol 116 | } 117 | for i := 1; i < h-1; i++ { 118 | for j := 1; j < w-1; j++ { 119 | g.tb.Put(x+j, y+i, rune(s)) 120 | } 121 | } 122 | } 123 | } 124 | 125 | func (g *TextGraphics) String() string { 126 | return g.tb.String() 127 | } 128 | 129 | func (g *TextGraphics) Symbol(x, y int, style chart.Style) { 130 | g.tb.Put(x, y, rune(style.Symbol)) 131 | } 132 | 133 | func (g *TextGraphics) XAxis(xrange chart.Range, y, y1 int, options chart.PlotOptions) { 134 | mirror := xrange.TicSetting.Mirror 135 | xa, xe := xrange.Data2Screen(xrange.Min), xrange.Data2Screen(xrange.Max) 136 | for sx := xa; sx <= xe; sx++ { 137 | g.tb.Put(sx, y, '-') 138 | if mirror >= 1 { 139 | g.tb.Put(sx, y1, '-') 140 | } 141 | } 142 | if xrange.ShowZero && xrange.Min < 0 && xrange.Max > 0 { 143 | z := xrange.Data2Screen(0) 144 | for yy := y - 1; yy > y1+1; yy-- { 145 | g.tb.Put(z, yy, ':') 146 | } 147 | } 148 | 149 | if xrange.Label != "" { 150 | yy := y + 1 151 | if !xrange.TicSetting.Hide { 152 | yy++ 153 | } 154 | g.tb.Text((xa+xe)/2, yy, xrange.Label, 0) 155 | } 156 | 157 | for _, tic := range xrange.Tics { 158 | var x int 159 | if !math.IsNaN(tic.Pos) { 160 | x = xrange.Data2Screen(tic.Pos) 161 | } else { 162 | x = -1 163 | } 164 | lx := xrange.Data2Screen(tic.LabelPos) 165 | if xrange.Time { 166 | if x != -1 { 167 | g.tb.Put(x, y, '|') 168 | if mirror >= 2 { 169 | g.tb.Put(x, y1, '|') 170 | } 171 | g.tb.Put(x, y+1, '|') 172 | } 173 | if tic.Align == -1 { 174 | g.tb.Text(lx+1, y+1, tic.Label, -1) 175 | } else { 176 | g.tb.Text(lx, y+1, tic.Label, 0) 177 | } 178 | } else { 179 | if x != -1 { 180 | g.tb.Put(x, y, '+') 181 | if mirror >= 2 { 182 | g.tb.Put(x, y1, '+') 183 | } 184 | } 185 | g.tb.Text(lx, y+1, tic.Label, 0) 186 | } 187 | if xrange.ShowLimits { 188 | if xrange.Time { 189 | g.tb.Text(xa, y+2, xrange.TMin.Format("2006-01-02 15:04:05"), -1) 190 | g.tb.Text(xe, y+2, xrange.TMax.Format("2006-01-02 15:04:05"), 1) 191 | } else { 192 | g.tb.Text(xa, y+2, fmt.Sprintf("%g", xrange.Min), -1) 193 | g.tb.Text(xe, y+2, fmt.Sprintf("%g", xrange.Max), 1) 194 | } 195 | } 196 | } 197 | } 198 | 199 | func (g *TextGraphics) YAxis(yrange chart.Range, x, x1 int, options chart.PlotOptions) { 200 | label := yrange.Label 201 | mirror := yrange.TicSetting.Mirror 202 | ya, ye := yrange.Data2Screen(yrange.Min), yrange.Data2Screen(yrange.Max) 203 | for sy := min(ya, ye); sy <= max(ya, ye); sy++ { 204 | g.tb.Put(x, sy, '|') 205 | if mirror >= 1 { 206 | g.tb.Put(x1, sy, '|') 207 | } 208 | } 209 | if yrange.ShowZero && yrange.Min < 0 && yrange.Max > 0 { 210 | z := yrange.Data2Screen(0) 211 | for xx := x + 1; xx < x1; xx += 2 { 212 | g.tb.Put(xx, z, '-') 213 | } 214 | } 215 | 216 | if label != "" { 217 | g.tb.Text(1, (ya+ye)/2, label, 3) 218 | } 219 | 220 | for _, tic := range yrange.Tics { 221 | y := yrange.Data2Screen(tic.Pos) 222 | ly := yrange.Data2Screen(tic.LabelPos) 223 | if yrange.Time { 224 | g.tb.Put(x, y, '+') 225 | if mirror >= 2 { 226 | g.tb.Put(x1, y, '+') 227 | } 228 | if tic.Align == 0 { // centered tic 229 | g.tb.Put(x-1, y, '-') 230 | g.tb.Put(x-2, y, '-') 231 | } 232 | g.tb.Text(x, ly, tic.Label+" ", 1) 233 | } else { 234 | g.tb.Put(x, y, '+') 235 | if mirror >= 2 { 236 | g.tb.Put(x1, y, '+') 237 | } 238 | g.tb.Text(x-2, ly, tic.Label, 1) 239 | } 240 | } 241 | } 242 | 243 | func (g *TextGraphics) Scatter(points []chart.EPoint, plotstyle chart.PlotStyle, style chart.Style) { 244 | // First pass: Error bars 245 | for _, p := range points { 246 | xl, yl, xh, yh := p.BoundingBox() 247 | if !math.IsNaN(p.DeltaX) { 248 | g.tb.Line(int(xl), int(p.Y), int(xh), int(p.Y), '-') 249 | } 250 | if !math.IsNaN(p.DeltaY) { 251 | g.tb.Line(int(p.X), int(yl), int(p.X), int(yh), '|') 252 | } 253 | } 254 | 255 | // Second pass: Line 256 | if (plotstyle&chart.PlotStyleLines) != 0 && len(points) > 0 { 257 | lastx, lasty := int(points[0].X), int(points[0].Y) 258 | for i := 1; i < len(points); i++ { 259 | x, y := int(points[i].X), int(points[i].Y) 260 | // fmt.Printf("LineSegment %d (%d,%d) -> (%d,%d)\n", i, lastx,lasty,x,y) 261 | g.tb.Line(lastx, lasty, x, y, rune(style.Symbol)) 262 | lastx, lasty = x, y 263 | } 264 | } 265 | 266 | // Third pass: symbols 267 | if (plotstyle&chart.PlotStylePoints) != 0 && len(points) != 0 { 268 | for _, p := range points { 269 | g.tb.Put(int(p.X), int(p.Y), rune(style.Symbol)) 270 | } 271 | } 272 | // chart.GenericScatter(g, points, plotstyle, style) 273 | } 274 | 275 | func (g *TextGraphics) Boxes(boxes []chart.Box, width int, style chart.Style) { 276 | if width%2 == 0 { 277 | width += 1 278 | } 279 | hbw := (width - 1) / 2 280 | if style.Symbol == 0 { 281 | style.Symbol = '*' 282 | } 283 | 284 | for _, box := range boxes { 285 | x := int(box.X) 286 | q1, q3 := int(box.Q1), int(box.Q3) 287 | g.tb.Rect(x-hbw, q1, 2*hbw, q3-q1, 0, ' ') 288 | if !math.IsNaN(box.Med) { 289 | med := int(box.Med) 290 | g.tb.Put(x-hbw, med, '+') 291 | for i := 0; i < hbw; i++ { 292 | g.tb.Put(x-i, med, '-') 293 | g.tb.Put(x+i, med, '-') 294 | } 295 | g.tb.Put(x+hbw, med, '+') 296 | } 297 | 298 | if !math.IsNaN(box.Avg) && style.Symbol != 0 { 299 | g.tb.Put(x, int(box.Avg), rune(style.Symbol)) 300 | } 301 | 302 | if !math.IsNaN(box.High) { 303 | for y := int(box.High); y < q3; y++ { 304 | g.tb.Put(x, y, '|') 305 | } 306 | } 307 | 308 | if !math.IsNaN(box.Low) { 309 | for y := int(box.Low); y > q1; y-- { 310 | g.tb.Put(x, y, '|') 311 | } 312 | } 313 | 314 | for _, ol := range box.Outliers { 315 | y := int(ol) 316 | g.tb.Put(x, y, rune(style.Symbol)) 317 | } 318 | } 319 | } 320 | 321 | func (g *TextGraphics) Key(x, y int, key chart.Key, options chart.PlotOptions) { 322 | m := key.Place() 323 | if len(m) == 0 { 324 | return 325 | } 326 | tw, th, cw, rh := key.Layout(g, m, chart.ElementStyle(options, chart.KeyElement).Font) 327 | // fmt.Printf("Text-Key: %d x %d\n", tw,th) 328 | style := chart.ElementStyle(options, chart.KeyElement) 329 | if style.LineWidth > 0 || style.FillColor != nil { 330 | g.tb.Rect(x, y, tw, th-1, 1, ' ') 331 | } 332 | x += int(chart.KeyHorSep) 333 | vsep := chart.KeyVertSep 334 | if vsep < 1 { 335 | vsep = 1 336 | } 337 | y += int(vsep) 338 | for ci, col := range m { 339 | yy := y 340 | 341 | for ri, e := range col { 342 | if e == nil || e.Text == "" { 343 | continue 344 | } 345 | plotStyle := e.PlotStyle 346 | // fmt.Printf("KeyEntry %s: PlotStyle = %d\n", e.Text, e.PlotStyle) 347 | if plotStyle == -1 { 348 | // heading only... 349 | g.tb.Text(x, yy, e.Text, -1) 350 | } else { 351 | // normal entry 352 | if (plotStyle & chart.PlotStyleLines) != 0 { 353 | g.Line(x, yy, x+int(chart.KeySymbolWidth), yy, e.Style) 354 | } 355 | if (plotStyle & chart.PlotStylePoints) != 0 { 356 | g.Symbol(x+int(chart.KeySymbolWidth/2), yy, e.Style) 357 | } 358 | if (plotStyle & chart.PlotStyleBox) != 0 { 359 | g.tb.Put(x+int(chart.KeySymbolWidth/2), yy, rune(e.Style.Symbol)) 360 | } 361 | g.tb.Text(x+int((chart.KeySymbolWidth+chart.KeySymbolSep)), yy, e.Text, -1) 362 | } 363 | yy += rh[ri] + int(chart.KeyRowSep) 364 | } 365 | 366 | x += int((chart.KeySymbolWidth + chart.KeySymbolSep + chart.KeyColSep + float32(cw[ci]))) 367 | } 368 | 369 | } 370 | 371 | func (g *TextGraphics) Bars(bars []chart.Barinfo, style chart.Style) { 372 | chart.GenericBars(g, bars, style) 373 | } 374 | 375 | var CircleStretchFactor float64 = 1.85 376 | 377 | func (g *TextGraphics) Rings(wedges []chart.Wedgeinfo, x, y, ro, ri int) { 378 | if g.xoff == -1 { 379 | g.xoff = int(float64(ro) * (CircleStretchFactor - 1)) 380 | // debug.Printf("Shifting center about %d (ro=%d, f=%.2f)", g.xoff, ro, CircleStretchFactor) 381 | } 382 | for i := range wedges { 383 | wedges[i].Style.LineWidth = 1 384 | } 385 | chart.GenericRings(g, wedges, x+g.xoff, y, ro, ri, 1.8) 386 | } 387 | 388 | var _ chart.Graphics = &TextGraphics{} 389 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func minimum(data []float64) float64 { 8 | if data == nil { 9 | return math.NaN() 10 | } 11 | min := data[0] 12 | for i := 1; i < len(data); i++ { 13 | if data[i] < min { 14 | min = data[i] 15 | } 16 | } 17 | return min 18 | } 19 | 20 | func maximum(data []float64) float64 { 21 | if data == nil { 22 | return math.NaN() 23 | } 24 | max := data[0] 25 | for i := 1; i < len(data); i++ { 26 | if data[i] > max { 27 | max = data[i] 28 | } 29 | } 30 | return max 31 | } 32 | 33 | func imin(a, b int) int { 34 | if a < b { 35 | return a 36 | } 37 | return b 38 | } 39 | 40 | func imax(a, b int) int { 41 | if a > b { 42 | return a 43 | } 44 | return b 45 | } 46 | 47 | func iabs(a int) int { 48 | if a < 0 { 49 | return -a 50 | } 51 | return a 52 | } 53 | 54 | func isign(a int) int { 55 | if a < 0 { 56 | return -1 57 | } 58 | if a == 0 { 59 | return 0 60 | } 61 | return 1 62 | } 63 | 64 | func clip(x, l, u int) int { 65 | if x < imin(l, u) { 66 | return l 67 | } 68 | if x > imax(l, u) { 69 | return u 70 | } 71 | return x 72 | } 73 | 74 | func fmax(a, b float64) float64 { 75 | if a > b { 76 | return a 77 | } 78 | return b 79 | } 80 | 81 | func fmin(a, b float64) float64 { 82 | if a < b { 83 | return a 84 | } 85 | return b 86 | } 87 | --------------------------------------------------------------------------------