├── .gitignore ├── Makefile ├── README ├── bar.go ├── box.go ├── chart.go ├── data.go ├── doc.go ├── example └── 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 ├── 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 | -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- 1 | Charts for Go 2 | ============= 3 | 4 | Basic charts in go. 5 | 6 | This package focuses more on autoscaling, error bars, 7 | and logarithmic plots than on beautifull or marketing 8 | ready charts. 9 | 10 | The following chart types are implemented: 11 | - Strip Charts 12 | - Scatter / Function-Plot Charts 13 | - Histograms 14 | - Bar and Categorical Bar Charts 15 | - Pie/Ring Charts 16 | - Boxplots 17 | 18 | Axis can be linear, logarithmical or time. 19 | 20 | Package chart itself provideds the charts/plots itself, the charts/plots 21 | can be output to different graphic drivers. Currently 22 | - txtg: ASCII art charts 23 | - svgg: scalable vector graphics (via github.com/ajstarks/svgo), and 24 | - imgg: Go image.RGBA (via code.google.com/p/draw2d/draw2d/ and 25 | code.google.com/p/freetype-go) 26 | are implemented. 27 | 28 | For a quick overview save as xbestof.{png,svg,txt} run 29 | $ example/example -best 30 | A fuller overview can be generated by 31 | $ example/example -All 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /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 | Data []BarChartData 24 | } 25 | 26 | // BarChartData encapsulates data sets in a bar chart. 27 | type BarChartData struct { 28 | Name string 29 | Style Style 30 | Samples []Point 31 | } 32 | 33 | // AddData adds the data to the chart. 34 | func (c *BarChart) AddData(name string, data []Point, style Style) { 35 | if len(c.Data) == 0 { 36 | c.XRange.init() 37 | c.YRange.init() 38 | } 39 | c.Data = append(c.Data, BarChartData{name, style, data}) 40 | for _, d := range data { 41 | c.XRange.autoscale(d.X) 42 | c.YRange.autoscale(d.Y) 43 | } 44 | 45 | if name != "" { 46 | c.Key.Entries = append(c.Key.Entries, KeyEntry{Style: style, Text: name, PlotStyle: PlotStyleBox}) 47 | } 48 | } 49 | 50 | // AddDataPair is a convenience method to add all the (x[i],y[i]) pairs to the chart. 51 | func (c *BarChart) AddDataPair(name string, x, y []float64, style Style) { 52 | n := imin(len(x), len(y)) 53 | data := make([]Point, n) 54 | for i := 0; i < n; i++ { 55 | data[i] = Point{X: x[i], Y: y[i]} 56 | } 57 | c.AddData(name, data, style) 58 | } 59 | 60 | func (c *BarChart) rescaleStackedY() { 61 | if !c.Stacked { 62 | return 63 | } 64 | 65 | // rescale y-axis 66 | high := make(map[float64]float64, 2*len(c.Data[0].Samples)) 67 | low := make(map[float64]float64, 2*len(c.Data[0].Samples)) 68 | min, max := c.YRange.DataMin, c.YRange.DataMax 69 | for _, d := range c.Data { 70 | for _, p := range d.Samples { 71 | x, y := p.X, p.Y 72 | if y == 0 { 73 | continue 74 | } 75 | if y > 0 { 76 | if cur, ok := high[x]; ok { 77 | high[x] = cur + y 78 | } else { 79 | high[x] = y 80 | } 81 | if high[x] > max { 82 | max = high[x] 83 | } 84 | } else { 85 | if cur, ok := low[x]; ok { 86 | low[x] = cur - y 87 | } else { 88 | low[x] = y 89 | } 90 | if low[x] < min { 91 | min = low[x] 92 | } 93 | } 94 | } 95 | } 96 | 97 | // stacked histograms and y-axis _not_ starting at 0 is 98 | // utterly braindamaged and missleading: Fix to 0 if 99 | // not spaning negativ to positive 100 | if min >= 0 { 101 | c.YRange.DataMin, c.YRange.Min = 0, 0 102 | c.YRange.MinMode.Fixed, c.YRange.MinMode.Value = true, 0 103 | } else { 104 | c.YRange.DataMin, c.YRange.Min = min, min 105 | } 106 | 107 | if max <= 0 { 108 | c.YRange.DataMax, c.YRange.Max = 0, 0 109 | c.YRange.MaxMode.Fixed, c.YRange.MaxMode.Value = true, 0 110 | } else { 111 | c.YRange.DataMax, c.YRange.Max = max, max 112 | } 113 | } 114 | 115 | // Reset chart to state before plotting. 116 | func (c *BarChart) Reset() { 117 | c.XRange.Reset() 118 | c.YRange.Reset() 119 | } 120 | 121 | // Plot renders the chart to the graphics output g. 122 | func (c *BarChart) Plot(g Graphics) { 123 | // layout 124 | layout := layout(g, c.Title, c.XRange.Label, c.YRange.Label, 125 | c.XRange.TicSetting.Hide, c.YRange.TicSetting.Hide, &c.Key) 126 | width, height := layout.Width, layout.Height 127 | topm, leftm := layout.Top, layout.Left 128 | numxtics, numytics := layout.NumXtics, layout.NumYtics 129 | fw, fh, _ := g.FontMetrics(DefaultFont["label"]) 130 | fw += 0 131 | fh += 0 132 | 133 | // Outside bound ranges for bar plots are nicer 134 | leftm, width = leftm+int(2*fw), width-int(2*fw) 135 | topm, height = topm, height-fh 136 | 137 | c.rescaleStackedY() 138 | c.XRange.Setup(numxtics, numxtics+3, width, leftm, false) 139 | c.YRange.Setup(numytics, numytics+2, height, topm, true) 140 | 141 | // Start of drawing 142 | g.Begin() 143 | if c.Title != "" { 144 | g.Title(c.Title) 145 | } 146 | 147 | g.XAxis(c.XRange, topm+height+fh, topm) 148 | g.YAxis(c.YRange, leftm-int(2*fw), leftm+width) 149 | 150 | xf := c.XRange.Data2Screen 151 | yf := c.YRange.Data2Screen 152 | var sy0 int 153 | switch { 154 | case c.YRange.Min >= 0: 155 | sy0 = yf(c.YRange.Min) 156 | case c.YRange.Min < 0 && c.YRange.Max > 0: 157 | sy0 = yf(0) 158 | case c.YRange.Max <= 0: 159 | sy0 = yf(c.YRange.Max) 160 | default: 161 | fmt.Printf("No f.... idea how this can happen. You've been fiddeling?") 162 | } 163 | 164 | // TODO: gap between bars. 165 | var sbw, fbw int // ScreenBarWidth 166 | 167 | var low, high map[float64]float64 168 | if c.Stacked { 169 | high = make(map[float64]float64, 50) 170 | low = make(map[float64]float64, 50) 171 | } 172 | for dn, data := range c.Data { 173 | mindeltax := c.minimumSampleSep(dn) 174 | // debug.Printf("Minimum x-distance for set %d: %.3f\n", dn, mindeltax) 175 | if c.Stacked { 176 | sbw = (xf(2*mindeltax) - xf(0)) / 4 177 | fbw = sbw 178 | } else { 179 | // V 180 | // xxx === 000 ... xxx sbw = 3 181 | // xx == 00 ## .. xx == fbw = 11 182 | sbw = (xf(mindeltax)-xf(0))/(len(c.Data)+1) - 1 183 | fbw = len(c.Data)*sbw + len(c.Data) - 1 184 | } 185 | // debug.Printf("sbw = %d , fbw = %d\n", sbw, fbw) 186 | 187 | bars := make([]Barinfo, 0, len(data.Samples)) 188 | if c.Stacked { 189 | for _, p := range data.Samples { 190 | if _, ok := high[p.X]; !ok { 191 | high[p.X], low[p.X] = 0, 0 192 | } 193 | } 194 | } 195 | for _, p := range data.Samples { 196 | x, y := p.X, p.Y 197 | if y == 0 { 198 | continue 199 | } 200 | 201 | sx := xf(x) - fbw/2 202 | if !c.Stacked { 203 | sx += dn * (sbw + 1) 204 | } 205 | 206 | var sy, sh int 207 | if c.Stacked { 208 | if y > 0 { 209 | top := y + high[x] 210 | sy = yf(top) 211 | sh = yf(high[x]) - sy 212 | high[x] = top 213 | 214 | } else { 215 | bot := low[x] + y 216 | sy = yf(low[x]) 217 | sh = yf(bot) - sy 218 | low[x] = bot 219 | } 220 | } else { 221 | if y > 0 { 222 | sy = yf(y) 223 | sh = sy0 - sy 224 | } else { 225 | sy = sy0 226 | sh = yf(y) - sy0 227 | } 228 | } 229 | bar := Barinfo{x: sx, y: sy, w: sbw, h: sh} 230 | c.addLabel(&bar, y) 231 | bars = append(bars, bar) 232 | 233 | } 234 | g.Bars(bars, data.Style) 235 | 236 | } 237 | 238 | if !c.Key.Hide { 239 | g.Key(layout.KeyX, layout.KeyY, c.Key) 240 | } 241 | 242 | g.End() 243 | 244 | /******** old code ************** 245 | 246 | // find bar width 247 | lbw, ubw := c.extremBarWidth() 248 | var barWidth float64 249 | if c.SameBarWidth { 250 | barWidth = lbw 251 | } else { 252 | barWidth = ubw 253 | } 254 | 255 | // set up range and extend if bar would not fit 256 | c.XRange.Setup(numxtics, numxtics+4, width, leftm, false) 257 | c.YRange.Setup(numytics, numytics+2, height, topm, true) 258 | if c.XRange.DataMin-barWidth/2 < c.XRange.Min { 259 | c.XRange.DataMin -= barWidth / 2 260 | } 261 | if c.XRange.DataMax+barWidth > c.XRange.Max { 262 | c.XRange.DataMax += barWidth / 2 263 | } 264 | c.XRange.Setup(numxtics, numxtics+4, width, leftm, false) 265 | 266 | // Start of drawing 267 | g.Begin() 268 | if c.Title != "" { 269 | g.Title(c.Title) 270 | } 271 | 272 | g.XAxis(c.XRange, topm+height, topm) 273 | g.YAxis(c.YRange, leftm, leftm+width) 274 | 275 | xf := c.XRange.Data2Screen 276 | yf := c.YRange.Data2Screen 277 | sy0 := yf(c.YRange.Min) 278 | 279 | barWidth = lbw 280 | for i, data := range c.Data { 281 | if !c.SameBarWidth { 282 | barWidth = c.barWidth(i) 283 | } 284 | sbw := imax(1, xf(2*barWidth)-xf(barWidth)-1) // screen bar width TODO 285 | bars := make([]Barinfo, len(data.Samples)) 286 | 287 | for i, point := range data.Samples { 288 | x, y := point.X, point.Y 289 | sx := xf(x-barWidth/2) + 1 290 | // sw := xf(x+barWidth/2) - sx 291 | sy := yf(y) 292 | sh := sy0 - sy 293 | bars[i].x, bars[i].y = sx, sy 294 | bars[i].w, bars[i].h = sbw, sh 295 | } 296 | g.Bars(bars, data.Style) 297 | } 298 | 299 | if !c.Key.Hide { 300 | g.Key(layout.KeyX, layout.KeyY, c.Key) 301 | } 302 | 303 | g.End() 304 | 305 | 306 | **********************************************************/ 307 | } 308 | 309 | func (c *BarChart) minimumSampleSep(d int) (min float64) { 310 | n := len(c.Data[d].Samples) - 1 311 | min = math.MaxFloat64 312 | 313 | for i := 0; i < n; i++ { 314 | sep := math.Abs(c.Data[d].Samples[i].X - c.Data[d].Samples[i+1].X) 315 | if sep < min { 316 | min = sep 317 | } 318 | } 319 | return 320 | } 321 | 322 | func (c *BarChart) addLabel(bar *Barinfo, y float64) { 323 | if c.ShowVal == 0 { 324 | return 325 | } 326 | 327 | var sval string 328 | if math.Abs(y) >= 100 { 329 | sval = fmt.Sprintf("%i", int(y+0.5)) 330 | } else if math.Abs(y) >= 10 { 331 | sval = fmt.Sprintf("%.1f", y) 332 | } else if math.Abs(y) >= 1 { 333 | sval = fmt.Sprintf("%.2f", y) 334 | } else { 335 | sval = fmt.Sprintf("%.3f", y) 336 | } 337 | 338 | var tp string 339 | switch c.ShowVal { 340 | case 1: 341 | if y >= 0 { 342 | tp = "ot" 343 | } else { 344 | tp = "ob" 345 | } 346 | case 2: 347 | if y >= 0 { 348 | tp = "it" 349 | } else { 350 | tp = "ib" 351 | } 352 | case 3: 353 | tp = "c" 354 | } 355 | bar.t = sval 356 | bar.tp = tp 357 | } 358 | -------------------------------------------------------------------------------- /box.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "math" 5 | // "fmt" 6 | // "os" 7 | // "strings" 8 | ) 9 | 10 | // BoxChart represents box charts. 11 | // 12 | // To faciliate standard use of box plots, the method AddSet() exists which will 13 | // calculate the various elents of a box (e.g. med, q3, outliers, ...) from raw 14 | // data. 15 | type BoxChart struct { 16 | XRange, YRange Range // x and y axis 17 | Title string // Title of the chart 18 | Key Key // Key/legend 19 | Data []BoxChartData // the data sets to draw 20 | } 21 | 22 | // BoxChartData encapsulates a data set in a box chart 23 | type BoxChartData struct { 24 | Name string 25 | Style Style 26 | Samples []Box 27 | } 28 | 29 | // AddData adds all boxes in data to the chart. 30 | func (c *BoxChart) AddData(name string, data []Box, style Style) { 31 | c.Data = append(c.Data, BoxChartData{name, style, data}) 32 | ps := PlotStyle(PlotStylePoints | PlotStyleBox) 33 | c.Key.Entries = append(c.Key.Entries, KeyEntry{Text: name, Style: style, PlotStyle: ps}) 34 | // TODO(vodo) min, max 35 | } 36 | 37 | // NextDataSet adds a new (empty) data set to chart. After adding the data set you 38 | // can fill this last data set with AddSet() 39 | func (c *BoxChart) NextDataSet(name string, style Style) { 40 | c.Data = append(c.Data, BoxChartData{name, style, nil}) 41 | ps := PlotStyle(PlotStylePoints | PlotStyleBox) 42 | c.Key.Entries = append(c.Key.Entries, KeyEntry{Text: name, Style: style, PlotStyle: ps}) 43 | } 44 | 45 | // AddSet will add to last data set in the chart one new box calculated from data. 46 | // If outlier is true, than outliers (1.5*IQR from 25/75 percentil) are 47 | // drawn. If outlier is false, than the wiskers extend from min to max. 48 | func (c *BoxChart) AddSet(x float64, data []float64, outlier bool) { 49 | min, lq, med, avg, uq, max := SixvalFloat64(data, 25) 50 | b := Box{X: x, Avg: avg, Med: med, Q1: lq, Q3: uq, Low: min, High: max} 51 | 52 | if len(c.Data) == 0 { 53 | c.Data = make([]BoxChartData, 1) 54 | st := Style{LineColor: "#000000", LineWidth: 1, LineStyle: SolidLine} 55 | c.Data[0] = BoxChartData{Name: "", Style: st} 56 | } 57 | 58 | if len(c.Data) == 1 && len(c.Data[0].Samples) == 0 { 59 | c.XRange.DataMin, c.XRange.DataMax = x, x 60 | c.YRange.DataMin, c.YRange.DataMax = min, max 61 | } else { 62 | if x < c.XRange.DataMin { 63 | c.XRange.DataMin = x 64 | } else if x > c.XRange.DataMax { 65 | c.XRange.DataMax = x 66 | } 67 | if min < c.YRange.DataMin { 68 | c.YRange.DataMin = min 69 | } 70 | if max > c.YRange.DataMax { 71 | c.YRange.DataMax = max 72 | } 73 | } 74 | 75 | if outlier { 76 | outliers := make([]float64, 0) 77 | iqr := uq - lq 78 | min, max = max, min 79 | for _, d := range data { 80 | if d > uq+1.5*iqr || d < lq-1.5*iqr { 81 | outliers = append(outliers, d) 82 | } 83 | if d > max && d <= uq+1.5*iqr { 84 | max = d 85 | } 86 | if d < min && d >= lq-1.5*iqr { 87 | min = d 88 | } 89 | } 90 | b.Low, b.High, b.Outliers = min, max, outliers 91 | } 92 | j := len(c.Data) - 1 93 | c.Data[j].Samples = append(c.Data[j].Samples, b) 94 | } 95 | 96 | // Reset chart to state before plotting. 97 | func (c *BoxChart) Reset() { 98 | c.XRange.Reset() 99 | c.YRange.Reset() 100 | } 101 | 102 | // Plot renders the chart to the graphic output g. 103 | func (c *BoxChart) Plot(g Graphics) { 104 | // layout 105 | layout := layout(g, c.Title, c.XRange.Label, c.YRange.Label, 106 | c.XRange.TicSetting.Hide, c.YRange.TicSetting.Hide, &c.Key) 107 | width, height := layout.Width, layout.Height 108 | topm, leftm := layout.Top, layout.Left 109 | numxtics, numytics := layout.NumXtics, layout.NumYtics 110 | // fontwidth, fontheight, _ := g.FontMetrics(DataStyle{}) 111 | 112 | g.Begin() 113 | 114 | c.XRange.Setup(numxtics, numxtics+2, width, leftm, false) 115 | c.YRange.Setup(numytics, numytics+1, height, topm, true) 116 | 117 | if c.Title != "" { 118 | g.Title(c.Title) 119 | } 120 | 121 | g.XAxis(c.XRange, topm+height, topm) 122 | g.YAxis(c.YRange, leftm, leftm+width) 123 | 124 | yf := c.YRange.Data2Screen 125 | nan := math.NaN() 126 | for _, data := range c.Data { 127 | // Samples 128 | nums := len(data.Samples) 129 | bw := width / (2*nums - 1) 130 | 131 | boxes := make([]Box, len(data.Samples)) 132 | for i, d := range data.Samples { 133 | x := float64(c.XRange.Data2Screen(d.X)) 134 | // debug.Printf("Q1=%.2f Q3=%.3f", d.Q1, d.Q3) 135 | q1, q3 := float64(yf(d.Q1)), float64(yf(d.Q3)) 136 | med, avg := nan, nan 137 | high, low := nan, nan 138 | if !math.IsNaN(d.Med) { 139 | med = float64(yf(d.Med)) 140 | } 141 | if !math.IsNaN(d.Avg) { 142 | avg = float64(yf(d.Avg)) 143 | } 144 | if !math.IsNaN(d.High) { 145 | high = float64(yf(d.High)) 146 | } 147 | if !math.IsNaN(d.Low) { 148 | low = float64(yf(d.Low)) 149 | } 150 | 151 | outliers := make([]float64, len(d.Outliers)) 152 | for j, ol := range d.Outliers { 153 | outliers[j] = float64(c.YRange.Data2Screen(ol)) 154 | } 155 | boxes[i].X = x 156 | boxes[i].Q1 = q1 157 | boxes[i].Q3 = q3 158 | boxes[i].Med = med 159 | boxes[i].Avg = avg 160 | boxes[i].High = high 161 | boxes[i].Low = low 162 | boxes[i].Outliers = outliers 163 | } 164 | g.Boxes(boxes, bw, data.Style) 165 | } 166 | 167 | if !c.Key.Hide { 168 | g.Key(layout.KeyX, layout.KeyY, c.Key) 169 | } 170 | 171 | g.End() 172 | } 173 | -------------------------------------------------------------------------------- /chart.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "time" 8 | // "os" 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) // Output chart to g 14 | Reset() // Reset any setting made during last plot 15 | } 16 | 17 | type Expansion int 18 | 19 | // Suitable values for Expand in RangeMode. 20 | const ( 21 | ExpandNextTic Expansion = 0 // Set min/max to next tic really below/above min/max of data 22 | ExpandToTic Expansion = 1 // Set to next tic below/above or equal to min/max of data 23 | ExpandTight Expansion = 2 // Use data min/max as limit 24 | ExpandABit Expansion = 3 // Like ExpandToTic and add/subtract ExpandABitFraction of tic distance. 25 | ) 26 | 27 | var ExpandABitFraction = 0.5 // Fraction of tic spacing added in ExpandABit Range.Expand mode. 28 | 29 | // RangeMode describes how one end of an axis is set up. There are basically three different main modes: 30 | // o Fixed: Fixed==true. 31 | // Use Value/TValue as fixed value ignoring data. 32 | // o Unconstrained autoscaling: Fixed==false && Constrained==false. 33 | // Set range to whatever data requires. 34 | // o Constrained autoscaling: Fixed==false && Constrained==true. 35 | // Scale axis according to data present, but limit scaling to intervall [Lower,Upper] 36 | // For both autoscaling modes Expand defines how much expansion is done below/above 37 | // the lowest/highest data point. 38 | type RangeMode struct { 39 | Fixed bool // If false: autoscaling. If true: use (T)Value/TValue as fixed setting 40 | Constrained bool // If false: full autoscaling. If true: use (T)Lower (T)Upper as limits 41 | Expand Expansion // One of ExpandNextTic, ExpandTight, ExpandABit 42 | Value float64 // Value of end point of axis in Fixed=true mode, ignorder otherwise 43 | TValue time.Time // Same as Value, but used for Date/Time axis 44 | Lower, Upper float64 // Lower and upper limit for constrained autoscaling 45 | TLower, TUpper time.Time // Same s Lower/Upper, but used for Date/Time axis 46 | } 47 | 48 | type GridMode int 49 | 50 | const ( 51 | GridOff GridMode = 0 // No grid lines 52 | GridLines GridMode = 1 // Grid lines 53 | GridBlocks GridMode = 2 // Zebra style background 54 | ) 55 | 56 | type MirrorAxis int 57 | 58 | const ( 59 | MirrorAxisAndTics MirrorAxis = 0 60 | MirrorNothing MirrorAxis = -1 61 | MirrorAxisOnly MirrorAxis = 1 62 | ) 63 | 64 | // TicSettings describes how (if at all) tics are shown on an axis. 65 | type TicSetting struct { 66 | Hide bool // Dont show tics if true 67 | Tics int // 0: across axis, 1: inside, 2: outside, other: off 68 | Minor int // 0: off, 1: auto, >1: number of intervalls (not number of tics!) 69 | Delta float64 // Wanted step between major tics. 0 means auto 70 | TDelta TimeDelta // Same as Delta, used for Date/Time axis 71 | Grid GridMode // GridOff, GridLines, GridBlocks 72 | Mirror MirrorAxis // 0: mirror axis and tics, -1: don't mirror anything, 1: mirror axis only (no tics) 73 | 74 | Format func(float64) string // User function to format tics. 75 | TFormat func(time.Time, TimeDelta) string // User function to format tics for date/time axis 76 | 77 | UserDelta bool // true if Delta or TDelta was input 78 | } 79 | 80 | // Tic describs a single tic on an axis. 81 | type Tic struct { 82 | Pos, LabelPos float64 // Position if the tic and its label on the axis (data coordinates). 83 | Label string // The Label of the tic 84 | Align int // Alignment of the label: -1: left/top, 0 center, 1 right/bottom (unused) 85 | } 86 | 87 | // Range encapsulates all information about an axis. 88 | type Range struct { 89 | Label string // Label of axis 90 | Log bool // Logarithmic axis? 91 | Time bool // Date/Time axis? 92 | MinMode, MaxMode RangeMode // How to handel min and max of this axis/range 93 | TicSetting TicSetting // How to handle tics. 94 | DataMin, DataMax float64 // Actual min/max values from data. If both zero: not calculated 95 | ShowLimits bool // Display axis Min and Max values on plot 96 | ShowZero bool // Add line to show 0 of this axis 97 | Category []string // If not empty (and neither Log nor Time): Use Category[n] as tic label at pos n+1. 98 | 99 | // The following values are set up during plotting 100 | Min, Max float64 // Actual minium and maximum of this axis/range. 101 | TMin, TMax time.Time // Same as Min/Max, but used for Date/Time axis 102 | Tics []Tic // List of tics to display 103 | 104 | // The following functions are set up during plotting 105 | Norm func(float64) float64 // Function to map [Min:Max] to [0:1] 106 | InvNorm func(float64) float64 // Inverse of Norm() 107 | Data2Screen func(float64) int // Function to map data value to screen position 108 | Screen2Data func(int) float64 // Inverse of Data2Screen 109 | } 110 | 111 | // Fixed is a helper (just reduces typing) functions which turns of autoscaling 112 | // and sets the axis range to [min,max] and the tic distance to delta. 113 | func (r *Range) Fixed(min, max, delta float64) { 114 | r.MinMode.Fixed, r.MaxMode.Fixed = true, true 115 | r.MinMode.Value, r.MaxMode.Value = min, max 116 | r.TicSetting.Delta = delta 117 | } 118 | 119 | func (r *Range) TFixed(min, max time.Time, delta TimeDelta) { 120 | r.MinMode.Fixed, r.MaxMode.Fixed = true, true 121 | r.MinMode.TValue, r.MaxMode.TValue = min, max 122 | r.TicSetting.TDelta = delta 123 | } 124 | 125 | // Reset resets the fields in r which have been set up during a plot. 126 | func (r *Range) Reset() { 127 | r.Min, r.Max = 0, 0 128 | r.TMin, r.TMax = time.Time{}, time.Time{} 129 | r.Tics = nil 130 | r.Norm, r.InvNorm = nil, nil 131 | r.Data2Screen, r.Screen2Data = nil, nil 132 | 133 | if !r.TicSetting.UserDelta { 134 | r.TicSetting.Delta = 0 135 | r.TicSetting.TDelta = nil 136 | } 137 | } 138 | 139 | // Prepare the range r for use, especially set up all values needed for autoscale() to work properly 140 | func (r *Range) init() { r.Init() } 141 | func (r *Range) Init() { 142 | // All the min stuff 143 | if r.MinMode.Fixed { 144 | // copy TValue to Value if set and time axis 145 | if r.Time && !r.MinMode.TValue.IsZero() { 146 | r.MinMode.Value = float64(r.MinMode.TValue.Unix()) 147 | } 148 | r.DataMin = r.MinMode.Value 149 | } else if r.MinMode.Constrained { 150 | // copy TLower/TUpper to Lower/Upper if set and time axis 151 | if r.Time && !r.MinMode.TLower.IsZero() { 152 | r.MinMode.Lower = float64(r.MinMode.TLower.Unix()) 153 | } 154 | if r.Time && !r.MinMode.TUpper.IsZero() { 155 | r.MinMode.Upper = float64(r.MinMode.TUpper.Unix()) 156 | } 157 | if r.MinMode.Lower == 0 && r.MinMode.Upper == 0 { 158 | // Constrained but un-initialized: Full autoscaling 159 | r.MinMode.Lower = -math.MaxFloat64 160 | r.MinMode.Upper = math.MaxFloat64 161 | } 162 | r.DataMin = r.MinMode.Upper 163 | } else { 164 | r.DataMin = math.MaxFloat64 165 | } 166 | 167 | // All the max stuff 168 | if r.MaxMode.Fixed { 169 | // copy TValue to Value if set and time axis 170 | if r.Time && !r.MaxMode.TValue.IsZero() { 171 | r.MaxMode.Value = float64(r.MaxMode.TValue.Unix()) 172 | } 173 | r.DataMax = r.MaxMode.Value 174 | } else if r.MaxMode.Constrained { 175 | // copy TLower/TUpper to Lower/Upper if set and time axis 176 | if r.Time && !r.MaxMode.TLower.IsZero() { 177 | r.MaxMode.Lower = float64(r.MaxMode.TLower.Unix()) 178 | } 179 | if r.Time && !r.MaxMode.TUpper.IsZero() { 180 | r.MaxMode.Upper = float64(r.MaxMode.TUpper.Unix()) 181 | } 182 | if r.MaxMode.Lower == 0 && r.MaxMode.Upper == 0 { 183 | // Constrained but un-initialized: Full autoscaling 184 | r.MaxMode.Lower = -math.MaxFloat64 185 | r.MaxMode.Upper = math.MaxFloat64 186 | } 187 | r.DataMax = r.MaxMode.Upper 188 | } else { 189 | r.DataMax = -math.MaxFloat64 190 | } 191 | 192 | // fmt.Printf("At end of init: DataMin / DataMax = %g / %g\n", r.DataMin, r.DataMax) 193 | } 194 | 195 | // Update DataMin and DataMax according to the RangeModes. 196 | func (r *Range) autoscale(x float64) { 197 | 198 | if x < r.DataMin && !r.MinMode.Fixed { 199 | if !r.MinMode.Constrained { 200 | // full autoscaling 201 | r.DataMin = x 202 | } else { 203 | r.DataMin = fmin(fmax(x, r.MinMode.Lower), r.DataMin) 204 | } 205 | } 206 | 207 | if x > r.DataMax && !r.MaxMode.Fixed { 208 | if !r.MaxMode.Constrained { 209 | // full autoscaling 210 | r.DataMax = x 211 | } else { 212 | r.DataMax = fmax(fmin(x, r.MaxMode.Upper), r.DataMax) 213 | } 214 | } 215 | } 216 | 217 | // Units are the SI prefixes for 10^3n 218 | var Units = []string{" y", " z", " a", " f", " p", " n", " µ", "m", " k", " M", " G", " T", " P", " E", " Z", " Y"} 219 | 220 | // FmtFloat yields a string representation of f. E.g. 12345.67 --> "12.3 k"; 0.09876 --> "99 m" 221 | func FmtFloat(f float64) string { 222 | af := math.Abs(f) 223 | if f == 0 { 224 | return "0" 225 | } else if 1 <= af && af < 10 { 226 | return fmt.Sprintf("%.1f", f) 227 | } else if 10 <= af && af <= 1000 { 228 | return fmt.Sprintf("%.0f", f) 229 | } 230 | 231 | if af < 1 { 232 | var p = 8 233 | for math.Abs(f) < 1 && p >= 0 { 234 | f *= 1000 235 | p-- 236 | } 237 | return FmtFloat(f) + Units[p] 238 | } else { 239 | var p = 7 240 | for math.Abs(f) > 1000 && p < 16 { 241 | f /= 1000 242 | p++ 243 | } 244 | return FmtFloat(f) + Units[p] 245 | 246 | } 247 | return "xxx" 248 | } 249 | 250 | func almostEqual(a, b, d float64) bool { 251 | return math.Abs(a-b) < d 252 | } 253 | 254 | // applyRangeMode returns val constrained by mode. val is considered the upper end of an range/axis 255 | // if upper is true. To allow proper rounding to tic (depending on desired RangeMode) 256 | // the ticDelta has to be provided. Logaritmic axis are selected by log = true and ticDelta 257 | // is ignored: Tics are of the form 1*10^n. 258 | func applyRangeMode(mode RangeMode, val, ticDelta float64, upper, log bool) float64 { 259 | if mode.Fixed { 260 | return mode.Value 261 | } 262 | if mode.Constrained { 263 | if val < mode.Lower { 264 | val = mode.Lower 265 | } else if val > mode.Upper { 266 | val = mode.Upper 267 | } 268 | } 269 | 270 | switch mode.Expand { 271 | case ExpandToTic, ExpandNextTic: 272 | var v float64 273 | if upper { 274 | if log { 275 | v = math.Pow10(int(math.Ceil(math.Log10(val)))) 276 | } else { 277 | v = math.Ceil(val/ticDelta) * ticDelta 278 | } 279 | } else { 280 | if log { 281 | v = math.Pow10(int(math.Floor(math.Log10(val)))) 282 | } else { 283 | v = math.Floor(val/ticDelta) * ticDelta 284 | } 285 | } 286 | if mode.Expand == ExpandNextTic { 287 | if upper { 288 | if log { 289 | if val/v < 2 { // TODO(vodo) use ExpandABitFraction 290 | v *= ticDelta 291 | } 292 | } else { 293 | if almostEqual(v, val, ticDelta/15) { 294 | v += ticDelta 295 | } 296 | } 297 | } else { 298 | if log { 299 | if v/val > 7 { // TODO(vodo) use ExpandABitFraction 300 | v /= ticDelta 301 | } 302 | } else { 303 | if almostEqual(v, val, ticDelta/15) { 304 | v -= ticDelta 305 | } 306 | } 307 | } 308 | } 309 | val = v 310 | case ExpandABit: 311 | if upper { 312 | if log { 313 | val *= math.Pow(10, ExpandABitFraction) 314 | } else { 315 | val += ticDelta * ExpandABitFraction 316 | } 317 | } else { 318 | if log { 319 | val /= math.Pow(10, ExpandABitFraction) 320 | } else { 321 | val -= ticDelta * ExpandABitFraction 322 | } 323 | } 324 | } 325 | 326 | return val 327 | } 328 | 329 | // tApplyRangeMode is the same as applyRangeMode for date/time axis/ranges. 330 | func tApplyRangeMode(mode RangeMode, val time.Time, step TimeDelta, upper bool) (bound time.Time, tic time.Time) { 331 | if mode.Fixed { 332 | bound = mode.TValue 333 | if upper { 334 | tic = RoundDown(val, step) 335 | } else { 336 | tic = RoundUp(val, step) 337 | } 338 | return 339 | } 340 | if mode.Constrained { // TODO(vodo) use T... 341 | sval := val.Unix() 342 | if sval < int64(mode.Lower) { 343 | sval = int64(mode.Lower) 344 | } else if sval > int64(mode.Upper) { 345 | sval = int64(mode.Upper) 346 | } 347 | val = time.Unix(sval, 0) 348 | } 349 | 350 | switch mode.Expand { 351 | case ExpandToTic: 352 | if upper { 353 | val = RoundUp(val, step) 354 | } else { 355 | val = RoundDown(val, step) 356 | } 357 | return val, val 358 | case ExpandNextTic: 359 | if upper { 360 | tic = RoundUp(val, step) 361 | } else { 362 | tic = RoundDown(val, step) 363 | } 364 | s := tic.Unix() 365 | if math.Abs(float64(s-val.Unix())/float64(step.Seconds())) < 0.15 { 366 | if upper { 367 | val = RoundUp(time.Unix(s+step.Seconds()/2, 0), step) 368 | } else { 369 | val = RoundDown(time.Unix(s-step.Seconds()/2, 0), step) 370 | } 371 | } else { 372 | val = tic 373 | } 374 | return val, val 375 | case ExpandABit: 376 | if upper { 377 | tic = RoundDown(val, step) 378 | val = time.Unix(tic.Unix()+step.Seconds()/2, 0) 379 | } else { 380 | tic = RoundUp(val, step) 381 | val = time.Unix(tic.Unix()-step.Seconds()/2, 0) 382 | } 383 | return 384 | 385 | } 386 | 387 | return val, val 388 | } 389 | 390 | func f2d(x float64) string { 391 | s := int64(x) 392 | t := time.Unix(s, 0) 393 | return t.Format("2006-01-02 15:04:05 (Mon)") 394 | } 395 | 396 | func (r *Range) tSetup(desiredNumberOfTics, maxNumberOfTics int, delta, mindelta float64) { 397 | debug.Printf("Data: [ %s : %s ] --> delta/mindelta = %.3g/%.3g (desired %d/max %d)\n", 398 | f2d(r.DataMin), f2d(r.DataMax), delta, mindelta, desiredNumberOfTics, maxNumberOfTics) 399 | 400 | var td TimeDelta 401 | if r.TicSetting.TDelta != nil { 402 | td = r.TicSetting.TDelta 403 | r.TicSetting.UserDelta = true 404 | } else { 405 | td = MatchingTimeDelta(delta, 3) 406 | r.TicSetting.UserDelta = false 407 | } 408 | r.ShowLimits = true 409 | 410 | // Set up time tic delta 411 | mint := time.Unix(int64(r.DataMin), 0) 412 | maxt := time.Unix(int64(r.DataMax), 0) 413 | 414 | var ftic, ltic time.Time 415 | r.TMin, ftic = tApplyRangeMode(r.MinMode, mint, td, false) 416 | r.TMax, ltic = tApplyRangeMode(r.MaxMode, maxt, td, true) 417 | r.TicSetting.Delta, r.TicSetting.TDelta = float64(td.Seconds()), td 418 | r.Min, r.Max = float64(r.TMin.Unix()), float64(r.TMax.Unix()) 419 | 420 | ftd := float64(td.Seconds()) 421 | actNumTics := int((r.Max - r.Min) / ftd) 422 | if actNumTics > maxNumberOfTics { 423 | // recalculate time tic delta 424 | debug.Printf("Switching from %s no next larger step %s", td, NextTimeDelta(td)) 425 | td = NextTimeDelta(td) 426 | ftd = float64(td.Seconds()) 427 | r.TMin, ftic = tApplyRangeMode(r.MinMode, mint, td, false) 428 | r.TMax, ltic = tApplyRangeMode(r.MaxMode, maxt, td, true) 429 | r.TicSetting.Delta, r.TicSetting.TDelta = float64(td.Seconds()), td 430 | r.Min, r.Max = float64(r.TMin.Unix()), float64(r.TMax.Unix()) 431 | actNumTics = int((r.Max - r.Min) / ftd) 432 | } 433 | 434 | debug.Printf("DataRange: %s TO %s", f2d(r.DataMin), f2d(r.DataMax)) 435 | debug.Printf("AxisRange: %s TO %s", f2d(r.Min), f2d(r.Max)) 436 | debug.Printf("TicsRange: %s TO %s Step %s", 437 | ftic.Format("2006-01-02 15:04:05 (Mon)"), ltic.Format("2006-01-02 15:04:05 (Mon)"), td) 438 | 439 | // Set up tics 440 | r.Tics = make([]Tic, 0) 441 | step := int64(td.Seconds()) 442 | align := 0 443 | 444 | var formater func(t time.Time, td TimeDelta) string 445 | if r.TicSetting.TFormat != nil { 446 | formater = r.TicSetting.TFormat 447 | } else { 448 | formater = func(t time.Time, td TimeDelta) string { return td.Format(t) } 449 | } 450 | 451 | for i := 0; ftic.Unix() < ltic.Unix(); i++ { 452 | x := float64(ftic.Unix()) 453 | label := formater(ftic, td) 454 | var labelPos float64 455 | if td.Period() { 456 | labelPos = x + float64(step)/2 457 | } else { 458 | labelPos = x 459 | } 460 | t := Tic{Pos: x, LabelPos: labelPos, Label: label, Align: align} 461 | r.Tics = append(r.Tics, t) 462 | ftic = RoundDown(time.Unix(ftic.Unix()+step+step/5, 0), td) 463 | } 464 | // last tic might not get label if period 465 | if td.Period() { 466 | r.Tics = append(r.Tics, Tic{Pos: float64(ftic.Unix())}) 467 | } else { 468 | x := float64(ftic.Unix()) 469 | label := formater(ftic, td) 470 | var labelPos float64 471 | labelPos = x 472 | t := Tic{Pos: x, LabelPos: labelPos, Label: label, Align: align} 473 | r.Tics = append(r.Tics, t) 474 | } 475 | } 476 | 477 | // Determine appropriate tic delta for normal (non dat/time) axis from desired delta and minimal delta. 478 | func (r *Range) fDelta(delta, mindelta float64) float64 { 479 | if r.Log { 480 | return 10 481 | } 482 | 483 | // Set up nice tic delta of the form 1,2,5 * 10^n 484 | // TODO: deltas of 25 and 250 would be suitable too... 485 | de := math.Pow10(int(math.Floor(math.Log10(delta)))) 486 | f := delta / de 487 | switch { 488 | case f < 2: 489 | f = 1 490 | case f < 4: 491 | f = 2 492 | case f < 9: 493 | f = 5 494 | default: 495 | f = 1 496 | de *= 10 497 | } 498 | delta = f * de 499 | if delta < mindelta { 500 | debug.Printf("Redoing delta: %g < %g", delta, mindelta) 501 | // recalculate tic delta 502 | switch f { 503 | case 1, 5: 504 | delta *= 2 505 | case 2: 506 | delta *= 2.5 507 | default: 508 | fmt.Printf("Oooops. Strange f: %g\n", f) 509 | } 510 | } 511 | return delta 512 | } 513 | 514 | // Set up normal (=non date/time axis) 515 | func (r *Range) fSetup(desiredNumberOfTics, maxNumberOfTics int, delta, mindelta float64) { 516 | debug.Printf("Data: [ %.5g : %.5g ] --> delta/mindelta = %.3g/%.3g (desired %d/max %d)\n", 517 | r.DataMin, r.DataMax, delta, mindelta, desiredNumberOfTics, maxNumberOfTics) 518 | if r.TicSetting.Delta != 0 { 519 | delta = r.TicSetting.Delta 520 | r.TicSetting.UserDelta = true 521 | } else { 522 | delta = r.fDelta(delta, mindelta) 523 | r.TicSetting.UserDelta = false 524 | } 525 | 526 | r.Min = applyRangeMode(r.MinMode, r.DataMin, delta, false, r.Log) 527 | r.Max = applyRangeMode(r.MaxMode, r.DataMax, delta, true, r.Log) 528 | r.TicSetting.Delta = delta 529 | 530 | debug.Printf("DataRange: %.6g TO %.6g", r.DataMin, r.DataMax) 531 | debug.Printf("AxisRange: %.6g TO %.6g", r.Min, r.Max) 532 | debug.Printf("TicsRange: %.6g TO %.6g Step %.6g") 533 | 534 | formater := FmtFloat 535 | if r.TicSetting.Format != nil { 536 | formater = r.TicSetting.Format 537 | } 538 | 539 | if r.Log { 540 | x := math.Pow10(int(math.Ceil(math.Log10(r.Min)))) 541 | last := math.Pow10(int(math.Floor(math.Log10(r.Max)))) 542 | debug.Printf("TicsRange: %.6g TO %.6g Factor %.6g", x, last, delta) 543 | r.Tics = make([]Tic, 0, maxNumberOfTics) 544 | for ; x <= last; x = x * delta { 545 | t := Tic{Pos: x, LabelPos: x, Label: formater(x)} 546 | r.Tics = append(r.Tics, t) 547 | // fmt.Printf("%v\n", t) 548 | } 549 | 550 | } else { 551 | if len(r.Category) > 0 { 552 | debug.Printf("TicsRange: %d categorical tics.", len(r.Category)) 553 | r.Tics = make([]Tic, len(r.Category)) 554 | for i, c := range r.Category { 555 | x := float64(i) 556 | if x < r.Min { 557 | continue 558 | } 559 | if x > r.Max { 560 | break 561 | } 562 | r.Tics[i].Pos = math.NaN() // no tic 563 | r.Tics[i].LabelPos = x 564 | r.Tics[i].Label = c 565 | } 566 | 567 | } else { 568 | // normal numeric axis 569 | first := delta * math.Ceil(r.Min/delta) 570 | num := int(-first/delta + math.Floor(r.Max/delta) + 1.5) 571 | debug.Printf("TicsRange: %.6g TO %.6g Step %.6g", first, first+float64(num)*delta, delta) 572 | 573 | // Set up tics 574 | r.Tics = make([]Tic, num) 575 | for i, x := 0, first; i < num; i, x = i+1, x+delta { 576 | r.Tics[i].Pos, r.Tics[i].LabelPos = x, x 577 | r.Tics[i].Label = formater(x) 578 | } 579 | } 580 | // TODO(vodo) r.ShowLimits = true 581 | } 582 | } 583 | 584 | // SetUp sets up several fields of Range r according to RangeModes and TicSettings. 585 | // DataMin and DataMax of r must be present and should indicate lowest and highest 586 | // value present in the data set. The following field if r are filled: 587 | // (T)Min and (T)Max lower and upper limit of axis, (T)-version for date/time axis 588 | // Tics slice of tics to draw 589 | // TicSetting.(T)Delta actual tic delta 590 | // Norm and InvNorm mapping of [lower,upper]_data --> [0:1] and inverse 591 | // Data2Screen mapping of data to screen coordinates 592 | // Screen2Data inverse of Data2Screen 593 | // The parameters desired- and maxNumberOfTics are what the say. 594 | // sWidth and sOffset are screen-width and -offset and are used to set up the 595 | // Data-Screen conversion functions. If revert is true, than screen coordinates 596 | // are asumed to be the other way around than mathematical coordinates. 597 | // 598 | // TODO(vodo) seperate screen stuff into own method. 599 | func (r *Range) Setup(desiredNumberOfTics, maxNumberOfTics, sWidth, sOffset int, revert bool) { 600 | // Sanitize input 601 | if desiredNumberOfTics <= 1 { 602 | desiredNumberOfTics = 2 603 | } 604 | if maxNumberOfTics < desiredNumberOfTics { 605 | maxNumberOfTics = desiredNumberOfTics 606 | } 607 | if r.DataMax == r.DataMin { 608 | r.DataMax = r.DataMin + 1 609 | } 610 | delta := (r.DataMax - r.DataMin) / float64(desiredNumberOfTics-1) 611 | mindelta := (r.DataMax - r.DataMin) / float64(maxNumberOfTics-1) 612 | 613 | if r.Time { 614 | r.tSetup(desiredNumberOfTics, maxNumberOfTics, delta, mindelta) 615 | } else { // simple, not a date range 616 | r.fSetup(desiredNumberOfTics, maxNumberOfTics, delta, mindelta) 617 | } 618 | 619 | if r.Log { 620 | r.Norm = func(x float64) float64 { return math.Log10(x/r.Min) / math.Log10(r.Max/r.Min) } 621 | r.InvNorm = func(f float64) float64 { return (r.Max-r.Min)*f + r.Min } 622 | } else { 623 | r.Norm = func(x float64) float64 { return (x - r.Min) / (r.Max - r.Min) } 624 | r.InvNorm = func(f float64) float64 { return (r.Max-r.Min)*f + r.Min } 625 | } 626 | 627 | if !revert { 628 | r.Data2Screen = func(x float64) int { 629 | return int(float64(sWidth)*r.Norm(x)) + sOffset 630 | } 631 | r.Screen2Data = func(x int) float64 { 632 | return r.InvNorm(float64(x-sOffset) / float64(sWidth)) 633 | } 634 | } else { 635 | r.Data2Screen = func(x float64) int { 636 | return sWidth - int(float64(sWidth)*r.Norm(x)) + sOffset 637 | } 638 | r.Screen2Data = func(x int) float64 { 639 | return r.InvNorm(float64(-x+sOffset+sWidth) / float64(sWidth)) 640 | } 641 | 642 | } 643 | 644 | } 645 | 646 | // LayoutData encapsulates the layout of the graph area in the whole drawing area. 647 | type LayoutData struct { 648 | Width, Height int // width and height of graph area 649 | Left, Top int // left and top margin 650 | KeyX, KeyY int // x and y coordiante of key 651 | NumXtics, NumYtics int // suggested numer of tics for both axis 652 | } 653 | 654 | // Layout graph data area on screen and place key. 655 | func layout(g Graphics, title, xlabel, ylabel string, hidextics, hideytics bool, key *Key) (ld LayoutData) { 656 | fw, fh, _ := g.FontMetrics(Font{}) 657 | w, h := g.Dimensions() 658 | 659 | if key.Pos == "" { 660 | key.Pos = "itr" 661 | } 662 | 663 | width, leftm, height, topm := w-int(6*fw), int(2*fw), h-2*fh, fh 664 | xlabsep, ylabsep := fh, int(3*fw) 665 | if title != "" { 666 | topm += (5 * fh) / 2 667 | height -= (5 * fh) / 2 668 | } 669 | if xlabel != "" { 670 | height -= (3 * fh) / 2 671 | } 672 | if !hidextics { 673 | height -= (3 * fh) / 2 674 | xlabsep += (3 * fh) / 2 675 | } 676 | if ylabel != "" { 677 | leftm += 2 * fh 678 | width -= 2 * fh 679 | } 680 | if !hideytics { 681 | leftm += int(6 * fw) 682 | width -= int(6 * fw) 683 | ylabsep += int(6 * fw) 684 | } 685 | 686 | if key != nil && !key.Hide && len(key.Place()) > 0 { 687 | m := key.Place() 688 | kw, kh, _, _ := key.Layout(g, m) 689 | sepx, sepy := int(fw)+fh, int(fw)+fh 690 | switch key.Pos[:2] { 691 | case "ol": 692 | width, leftm = width-kw-sepx, leftm+kw 693 | ld.KeyX = sepx / 2 694 | case "or": 695 | width = width - kw - sepx 696 | ld.KeyX = w - kw - sepx/2 697 | case "ot": 698 | height, topm = height-kh-sepy, topm+kh 699 | ld.KeyY = sepy / 2 700 | if title != "" { 701 | ld.KeyY += 2 * fh 702 | } 703 | case "ob": 704 | height = height - kh - sepy 705 | ld.KeyY = h - kh - sepy/2 706 | case "it": 707 | ld.KeyY = topm + sepy 708 | case "ic": 709 | ld.KeyY = topm + (height-kh)/2 710 | case "ib": 711 | ld.KeyY = topm + height - kh - sepy 712 | 713 | } 714 | 715 | switch key.Pos[:2] { 716 | case "ol", "or": 717 | switch key.Pos[2] { 718 | case 't': 719 | ld.KeyY = topm 720 | case 'c': 721 | ld.KeyY = topm + (height-kh)/2 722 | case 'b': 723 | ld.KeyY = topm + height - kh 724 | } 725 | case "ot", "ob": 726 | switch key.Pos[2] { 727 | case 'l': 728 | ld.KeyX = leftm 729 | case 'c': 730 | ld.KeyX = leftm + (width-kw)/2 731 | case 'r': 732 | ld.KeyX = w - kw - sepx 733 | } 734 | } 735 | if key.Pos[0] == 'i' { 736 | switch key.Pos[2] { 737 | case 'l': 738 | ld.KeyX = leftm + sepx 739 | case 'c': 740 | ld.KeyX = leftm + (width-kw)/2 741 | case 'r': 742 | ld.KeyX = leftm + width - kw - sepx 743 | } 744 | } 745 | } 746 | 747 | // fmt.Printf("width=%d, height=%d, leftm=%d, topm=%d (fw=%d)\n", width, height, leftm, topm, int(fw)) 748 | 749 | // Number of tics 750 | if width/int(fw) <= 20 { 751 | ld.NumXtics = 2 752 | } else { 753 | ld.NumXtics = width / int(10*fw) 754 | if ld.NumXtics > 25 { 755 | ld.NumXtics = 25 756 | } 757 | } 758 | ld.NumYtics = height / (4 * fh) 759 | if ld.NumYtics > 20 { 760 | ld.NumYtics = 20 761 | } 762 | 763 | ld.Width, ld.Height = width, height 764 | ld.Left, ld.Top = leftm, topm 765 | 766 | return 767 | } 768 | 769 | // Debugging and tracing 770 | type debugging bool 771 | 772 | const debug debugging = false 773 | 774 | func (d debugging) Printf(fmt string, args ...interface{}) { 775 | if d { 776 | log.Printf(fmt, args...) 777 | } 778 | } 779 | 780 | type tracing bool 781 | 782 | const trace tracing = false 783 | 784 | func (t tracing) Printf(fmt string, args ...interface{}) { 785 | if t { 786 | log.Printf(fmt, args...) 787 | } 788 | } 789 | -------------------------------------------------------------------------------- /data.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Values is an 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 | // A simple real value implemnting the Value interface 14 | type Real float64 15 | 16 | func (r Real) XVal() float64 { return float64(r) } 17 | 18 | // XY-Value is an interface for any type of data which is point-like and has 19 | // a x- and y-coordinate. Its standard implementationhere 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 an interface 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) // Same for y 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 default 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 an interface for any type of data which is 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 members are all simple value types where 27 | the null value provides suitable defaults. All members are exposed, even if 28 | you are not supposed to manipulate them directy or are 'output members'. 29 | E.g. the common Data member of all chart types will store the sample data 30 | added with one or more Add... methods. Some members are mere output channels 31 | which expose internal stuff for your use like the Data2Screen and Screen2Data 32 | functions of the Ranges. Some members are even input/output members: 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 member. 38 | 39 | All (except pie/ring charts) contain at least one axis represented by a 40 | member 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 members are simply ignored for Date/Time axis.) 45 | 46 | o Real valued axis (Time=false). The 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 | The Graphic outputs are individual packages currently only text/ascii-art 66 | and svg graphic outputs are realized (cairo and Go image are to follow). 67 | 68 | 69 | */ 70 | package chart 71 | -------------------------------------------------------------------------------- /freefont/AUTHORS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/AUTHORS -------------------------------------------------------------------------------- /freefont/CREDITS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/CREDITS -------------------------------------------------------------------------------- /freefont/ChangeLog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/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/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeMono.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeMonoBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeMonoBold.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeMonoBoldOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeMonoBoldOblique.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeMonoOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeMonoOblique.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeSans.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSansBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeSansBold.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSansBoldOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeSansBoldOblique.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSansOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeSansOblique.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSerif.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeSerif.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSerifBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeSerifBold.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSerifBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeSerifBoldItalic.ttf -------------------------------------------------------------------------------- /freefont/sfd/FreeSerifItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajstarks/chart/1f1380cb8583f1d7fa0f3875d8112756a1b4fb3a/freefont/sfd/FreeSerifItalic.ttf -------------------------------------------------------------------------------- /graphics.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // MinimalGraphics is the interface any graphics driver must implement, 9 | // so that he can fall back to the generic routines for the higher level 10 | // outputs. 11 | type MinimalGraphics interface { 12 | Background() (r, g, b, a uint8) // Color of background 13 | FontMetrics(font Font) (fw float32, fh int, mono bool) // Return fontwidth and -height in pixel 14 | TextLen(t string, font Font) int // Length=width of t in screen units if set on font 15 | Line(x0, y0, x1, y1 int, style Style) // Draw line from (x0,y0) to (x1,y1) 16 | Text(x, y int, t string, align string, rot int, f Font) // Put t at (x,y) rotated by rot aligned [[tcb]][lcr] 17 | } 18 | 19 | // BasicGrapic is an interface of the most basic graphic primitives. 20 | // Any type which implements BasicGraphics can use generic implementations 21 | // of the Graphics methods. 22 | type BasicGraphics interface { 23 | MinimalGraphics 24 | Symbol(x, y int, style Style) // Put symbol s at (x,y) 25 | Rect(x, y, w, h int, style Style) // Draw (w x h) rectangle at (x,y) 26 | Wedge(x, y, ro, ri int, phi, psi float64, style Style) // Wedge 27 | Path(x, y []int, style Style) // Path of straight lines 28 | } 29 | 30 | // Graphics is the interface all chart drivers have to implement 31 | type Graphics interface { 32 | BasicGraphics 33 | 34 | Dimensions() (int, int) // character-width / height 35 | 36 | Begin() // start of chart drawing 37 | End() // Done, cleanup 38 | 39 | // All stuff is preprocessed: sanitized, clipped, strings formated, integer coords, 40 | // screen coordinates, 41 | XAxis(xr Range, ys, yms int) // Draw x axis xr at screen position ys (and yms if mirrored) 42 | YAxis(yr Range, xs, xms int) // Same for y axis. 43 | Title(text string) // Draw title onto chart 44 | 45 | Scatter(points []EPoint, plotstyle PlotStyle, style Style) // Points, Lines and Line+Points 46 | Boxes(boxes []Box, width int, style Style) // Boxplots 47 | Bars(bars []Barinfo, style Style) // any type of histogram/bars 48 | Rings(wedeges []Wedgeinfo, x, y, ro, ri int) // Pie/ring diagram elements 49 | 50 | Key(x, y int, key Key) // place key at x,y 51 | } 52 | 53 | type Barinfo struct { 54 | x, y int // (x,y) of top left corner; 55 | w, h int // width and heigt 56 | t, tp string // label text and text position '[oi][tblr]' or 'c' 57 | f Font // font of text 58 | } 59 | 60 | type Wedgeinfo struct { 61 | Phi, Psi float64 // Start and ende of wedge. Fuill circle if |phi-psi| > 4pi 62 | Text, Tp string // label text and text position: [ico] 63 | Style Style // style of this wedge 64 | Font Font // font of text 65 | Shift int // Highlighting of wedge 66 | } 67 | 68 | func GenericTextLen(mg MinimalGraphics, t string, font Font) (width int) { 69 | // TODO: how handle newlines? same way like Text does 70 | fw, _, mono := mg.FontMetrics(font) 71 | if mono { 72 | for _ = range t { 73 | width++ 74 | } 75 | width = int(float32(width)*fw + 0.5) 76 | } else { 77 | var length float32 78 | for _, r := range t { 79 | if w, ok := CharacterWidth[int(r)]; ok { 80 | length += w 81 | } else { 82 | length += 20 // save above average 83 | } 84 | } 85 | length /= averageCharacterWidth 86 | length *= fw 87 | width = int(length + 0.5) 88 | } 89 | return 90 | } 91 | 92 | // Normalize (= (x,y) is top-left and w and h>0) and hounour line width r. 93 | func SanitizeRect(x, y, w, h, r int) (int, int, int, int) { 94 | if w < 0 { 95 | x += w 96 | w = -w 97 | } 98 | if h < 0 { 99 | y += h 100 | h = -h 101 | } 102 | 103 | d := (imax(1, r) - 1) / 2 104 | // TODO: what if w-2D <= 0 ? 105 | return x + d, y + d, w - 2*d, h - 2*d 106 | } 107 | 108 | // GenericRect draws a rectangle of size w x h at (x,y). Drawing is done 109 | // by simple lines only. 110 | func GenericRect(mg MinimalGraphics, x, y, w, h int, style Style) { 111 | x, y, w, h = SanitizeRect(x, y, w, h, style.LineWidth) 112 | 113 | if style.FillColor != "" { 114 | fs := Style{LineWidth: 1, LineColor: style.FillColor, LineStyle: SolidLine, Alpha: style.Alpha} 115 | for i := 1; i < h; i++ { 116 | mg.Line(x+1, y+i, x+w-1, y+i, fs) 117 | } 118 | } 119 | 120 | mg.Line(x, y, x+w, y, style) 121 | mg.Line(x+w, y, x+w, y+h, style) 122 | mg.Line(x+w, y+h, x, y+h, style) 123 | mg.Line(x, y+h, x, y, style) 124 | } 125 | 126 | // GenericPath is the incomplete implementation of a list of points 127 | // connected by straight lines. Incomplete: Dashed lines won't work properly. 128 | func GenericPath(mg MinimalGraphics, x, y []int, style Style) { 129 | n := imin(len(x), len(y)) 130 | for i := 1; i < n; i++ { 131 | mg.Line(x[i-1], y[i-1], x[i], y[i], style) 132 | } 133 | } 134 | 135 | func drawXTics(bg BasicGraphics, rng Range, y, ym, ticLen int) { 136 | xe := rng.Data2Screen(rng.Max) 137 | 138 | // Grid below tics 139 | if rng.TicSetting.Grid > GridOff { 140 | for ticcnt, tic := range rng.Tics { 141 | x := rng.Data2Screen(tic.Pos) 142 | if ticcnt > 0 && ticcnt < len(rng.Tics)-1 && rng.TicSetting.Grid == GridLines { 143 | // fmt.Printf("Gridline at x=%d\n", x) 144 | bg.Line(x, y-1, x, ym+1, DefaultStyle["gridl"]) 145 | } else if rng.TicSetting.Grid == GridBlocks { 146 | if ticcnt%2 == 1 { 147 | x0 := rng.Data2Screen(rng.Tics[ticcnt-1].Pos) 148 | bg.Rect(x0, ym, x-x0, y-ym, DefaultStyle["gridb"]) 149 | } else if ticcnt == len(rng.Tics)-1 && x < xe-1 { 150 | bg.Rect(x, ym, xe-x, y-ym, DefaultStyle["gridb"]) 151 | } 152 | } 153 | } 154 | } 155 | 156 | // Tics on top 157 | ticstyle := DefaultStyle["tic"] 158 | ticfont := DefaultFont["tic"] 159 | for _, tic := range rng.Tics { 160 | x := rng.Data2Screen(tic.Pos) 161 | lx := rng.Data2Screen(tic.LabelPos) 162 | 163 | // Tics 164 | switch rng.TicSetting.Tics { 165 | case 0: 166 | bg.Line(x, y-ticLen, x, y+ticLen, ticstyle) 167 | case 1: 168 | bg.Line(x, y-ticLen, x, y, ticstyle) 169 | case 2: 170 | bg.Line(x, y, x, y+ticLen, ticstyle) 171 | default: 172 | } 173 | 174 | // Mirrored Tics 175 | if rng.TicSetting.Mirror >= 2 { 176 | switch rng.TicSetting.Tics { 177 | case 0: 178 | bg.Line(x, ym-ticLen, x, ym+ticLen, ticstyle) 179 | case 1: 180 | bg.Line(x, ym, x, ym+ticLen, ticstyle) 181 | case 2: 182 | bg.Line(x, ym-ticLen, x, ym, ticstyle) 183 | default: 184 | } 185 | } 186 | 187 | // Tic-Label 188 | if rng.Time && tic.Align == -1 { 189 | bg.Line(x, y+ticLen, x, y+2*ticLen, ticstyle) 190 | bg.Text(lx, y+2*ticLen, tic.Label, "tl", 0, ticfont) 191 | } else { 192 | bg.Text(lx, y+2*ticLen, tic.Label, "tc", 0, ticfont) 193 | } 194 | } 195 | } 196 | 197 | // GenericAxis draws the axis r solely by graphic primitives of bg. 198 | func GenericXAxis(bg BasicGraphics, rng Range, y, ym int) { 199 | _, fontheight, _ := bg.FontMetrics(DefaultFont["label"]) 200 | var ticLen int = 0 201 | if !rng.TicSetting.Hide { 202 | ticLen = imin(12, imax(4, fontheight/2)) 203 | } 204 | xa, xe := rng.Data2Screen(rng.Min), rng.Data2Screen(rng.Max) 205 | 206 | // Axis label and range limits 207 | aly := y + 2*ticLen 208 | if !rng.TicSetting.Hide { 209 | aly += (3 * fontheight) / 2 210 | } 211 | if rng.ShowLimits { 212 | f := DefaultFont["rangelimit"] 213 | if rng.Time { 214 | bg.Text(xa, aly, rng.TMin.Format("2006-01-02 15:04:05"), "tl", 0, f) 215 | bg.Text(xe, aly, rng.TMax.Format("2006-01-02 15:04:05"), "tr", 0, f) 216 | } else { 217 | bg.Text(xa, aly, fmt.Sprintf("%g", rng.Min), "tl", 0, f) 218 | bg.Text(xe, aly, fmt.Sprintf("%g", rng.Max), "tr", 0, f) 219 | } 220 | } 221 | if rng.Label != "" { // draw label _after_ (=over) range limits 222 | bg.Text((xa+xe)/2, aly, " "+rng.Label+" ", "tc", 0, DefaultFont["label"]) 223 | } 224 | 225 | // Tics and Grid 226 | if !rng.TicSetting.Hide { 227 | drawXTics(bg, rng, y, ym, ticLen) 228 | } 229 | 230 | // Axis itself, mirrord axis and zero 231 | bg.Line(xa, y, xe, y, DefaultStyle["axis"]) 232 | if rng.TicSetting.Mirror >= 1 { 233 | bg.Line(xa, ym, xe, ym, DefaultStyle["maxis"]) 234 | } 235 | if rng.ShowZero && rng.Min < 0 && rng.Max > 0 { 236 | z := rng.Data2Screen(0) 237 | bg.Line(z, y, z, ym, DefaultStyle["zero"]) 238 | } 239 | 240 | } 241 | 242 | func drawYTics(bg BasicGraphics, rng Range, x, xm, ticLen int) { 243 | ye := rng.Data2Screen(rng.Max) 244 | 245 | // Grid below tics 246 | if rng.TicSetting.Grid > GridOff { 247 | for ticcnt, tic := range rng.Tics { 248 | y := rng.Data2Screen(tic.Pos) 249 | if rng.TicSetting.Grid == GridLines { 250 | if ticcnt > 0 && ticcnt < len(rng.Tics)-1 { 251 | // fmt.Printf("Gridline at x=%d\n", x) 252 | bg.Line(x+1, y, xm-1, y, DefaultStyle["gridl"]) 253 | } 254 | } else if rng.TicSetting.Grid == GridBlocks { 255 | if ticcnt%2 == 1 { 256 | y0 := rng.Data2Screen(rng.Tics[ticcnt-1].Pos) 257 | bg.Rect(x, y0, xm-x, y-y0, DefaultStyle["gridb"]) 258 | } else if ticcnt == len(rng.Tics)-1 && y > ye+1 { 259 | bg.Rect(x, ye, xm-x, y-ye, DefaultStyle["gridb"]) 260 | } 261 | } 262 | } 263 | } 264 | 265 | // Tics on top 266 | ticstyle := DefaultStyle["tic"] 267 | ticfont := DefaultFont["tic"] 268 | for _, tic := range rng.Tics { 269 | y := rng.Data2Screen(tic.Pos) 270 | ly := rng.Data2Screen(tic.LabelPos) 271 | 272 | // Tics 273 | switch rng.TicSetting.Tics { 274 | case 0: 275 | bg.Line(x-ticLen, y, x+ticLen, y, ticstyle) 276 | case 1: 277 | bg.Line(x, y, x+ticLen, y, ticstyle) 278 | case 2: 279 | bg.Line(x-ticLen, y, x, y, ticstyle) 280 | default: 281 | } 282 | 283 | // Mirrored tics 284 | if rng.TicSetting.Mirror >= 2 { 285 | switch rng.TicSetting.Tics { 286 | case 0: 287 | bg.Line(xm-ticLen, y, xm+ticLen, y, ticstyle) 288 | case 1: 289 | bg.Line(xm-ticLen, y, xm, y, ticstyle) 290 | case 2: 291 | bg.Line(xm, y, xm+ticLen, y, ticstyle) 292 | default: 293 | } 294 | } 295 | 296 | // Label 297 | if rng.Time && tic.Align == 0 { // centered tic 298 | bg.Line(x-2*ticLen, y, x+ticLen, y, ticstyle) 299 | bg.Text(x-ticLen, ly, tic.Label, "cr", 0, ticfont) 300 | } else { 301 | bg.Text(x-2*ticLen, ly, tic.Label, "cr", 0, ticfont) 302 | } 303 | } 304 | 305 | } 306 | 307 | // GenericAxis draws the axis r solely by graphic primitives of bg. 308 | func GenericYAxis(bg BasicGraphics, rng Range, x, xm int) { 309 | _, fontheight, _ := bg.FontMetrics(DefaultFont["label"]) 310 | var ticLen int = 0 311 | if !rng.TicSetting.Hide { 312 | ticLen = imin(10, imax(4, fontheight/2)) 313 | } 314 | ya, ye := rng.Data2Screen(rng.Min), rng.Data2Screen(rng.Max) 315 | 316 | // Label and axis ranges 317 | alx := 2 * fontheight 318 | if rng.ShowLimits { 319 | /* TODO 320 | st := bg.Style("rangelimit") 321 | if rng.Time { 322 | bg.Text(xa, aly, rng.TMin.Format("2006-01-02 15:04:05"), "tl", 0, st) 323 | bg.Text(xe, aly, rng.TMax.Format("2006-01-02 15:04:05"), "tr", 0, st) 324 | } else { 325 | bg.Text(xa, aly, fmt.Sprintf("%g", rng.Min), "tl", 0, st) 326 | bg.Text(xe, aly, fmt.Sprintf("%g", rng.Max), "tr", 0, st) 327 | } 328 | */ 329 | } 330 | if rng.Label != "" { 331 | y := (ya + ye) / 2 332 | bg.Text(alx, y, rng.Label, "bc", 90, DefaultFont["label"]) 333 | } 334 | 335 | if !rng.TicSetting.Hide { 336 | drawYTics(bg, rng, x, xm, ticLen) 337 | } 338 | 339 | // Axis itself, mirrord axis and zero 340 | bg.Line(x, ya, x, ye, DefaultStyle["axis"]) 341 | if rng.TicSetting.Mirror >= 1 { 342 | bg.Line(xm, ya, xm, ye, DefaultStyle["maxis"]) 343 | } 344 | if rng.ShowZero && rng.Min < 0 && rng.Max > 0 { 345 | z := rng.Data2Screen(0) 346 | bg.Line(x, z, xm, z, DefaultStyle["zero"]) 347 | } 348 | 349 | } 350 | 351 | // GenericScatter draws the given points according to style. 352 | // style.FillColor is used as color of error bars and style.FontSize is used 353 | // as the length of the endmarks of the error bars. Both have suitable defaults 354 | // if the FontXyz are not set. Point coordinates and errors must be provided 355 | // in screen coordinates. 356 | func GenericScatter(bg BasicGraphics, points []EPoint, plotstyle PlotStyle, style Style) { 357 | 358 | // First pass: Error bars 359 | ebs := style 360 | ebs.LineColor, ebs.LineWidth, ebs.LineStyle = ebs.FillColor, 1, SolidLine 361 | if ebs.LineColor == "" { 362 | ebs.LineColor = "#404040" 363 | } 364 | if ebs.LineWidth == 0 { 365 | ebs.LineWidth = 1 366 | } 367 | for _, p := range points { 368 | 369 | xl, yl, xh, yh := p.BoundingBox() 370 | // fmt.Printf("Draw %d: %f %f-%f; %f %f-%f\n", i, p.DeltaX, xl,xh, p.DeltaY, yl,yh) 371 | if !math.IsNaN(p.DeltaX) { 372 | bg.Line(int(xl), int(p.Y), int(xh), int(p.Y), ebs) 373 | } 374 | if !math.IsNaN(p.DeltaY) { 375 | // fmt.Printf(" Draw %d,%d to %d,%d\n",int(p.X), int(yl), int(p.X), int(yh)) 376 | bg.Line(int(p.X), int(yl), int(p.X), int(yh), ebs) 377 | } 378 | } 379 | 380 | // Second pass: Line 381 | if (plotstyle&PlotStyleLines) != 0 && len(points) > 0 { 382 | lastx, lasty := int(points[0].X), int(points[0].Y) 383 | for i := 1; i < len(points); i++ { 384 | x, y := int(points[i].X), int(points[i].Y) 385 | bg.Line(lastx, lasty, x, y, style) 386 | lastx, lasty = x, y 387 | } 388 | } 389 | 390 | // Third pass: symbols 391 | if (plotstyle&PlotStylePoints) != 0 && len(points) != 0 { 392 | for _, p := range points { 393 | // fmt.Printf("Point %d at %d,%d\n", i, int(p.X), int(p.Y)) 394 | bg.Symbol(int(p.X), int(p.Y), style) 395 | } 396 | } 397 | } 398 | 399 | // GenericBoxes draws box plots. (Default implementation for box plots). 400 | // The values for each box in boxes are in screen coordinates! 401 | func GenericBoxes(bg BasicGraphics, boxes []Box, width int, style Style) { 402 | if width%2 == 0 { 403 | width += 1 404 | } 405 | hbw := (width - 1) / 2 406 | for _, d := range boxes { 407 | x := int(d.X) 408 | q1, q3 := int(d.Q1), int(d.Q3) 409 | // debug.Printf("q1=%d q3=%d q3-q1=%d", q1,q3,q3-q1) 410 | bg.Rect(x-hbw, q1, width, q3-q1, style) 411 | if !math.IsNaN(d.Med) { 412 | med := int(d.Med) 413 | bg.Line(x-hbw, med, x+hbw, med, style) 414 | } 415 | 416 | if !math.IsNaN(d.Avg) { 417 | bg.Symbol(x, int(d.Avg), style) 418 | } 419 | 420 | if !math.IsNaN(d.High) { 421 | bg.Line(x, q3, x, int(d.High), style) 422 | } 423 | 424 | if !math.IsNaN(d.Low) { 425 | bg.Line(x, q1, x, int(d.Low), style) 426 | } 427 | 428 | for _, y := range d.Outliers { 429 | bg.Symbol(x, int(y), style) 430 | } 431 | 432 | } 433 | 434 | } 435 | 436 | // TODO: Is Bars and Generic Bars useful at all? Replaceable by rect? 437 | func GenericBars(bg BasicGraphics, bars []Barinfo, style Style) { 438 | for _, b := range bars { 439 | bg.Rect(b.x, b.y, b.w, b.h, style) 440 | if b.t != "" { 441 | var tx, ty int 442 | var a string 443 | _, fh, _ := bg.FontMetrics(b.f) 444 | if fh > 1 { 445 | fh /= 2 446 | } 447 | switch b.tp { 448 | case "ot": 449 | tx, ty, a = b.x+b.w/2, b.y-fh, "bc" 450 | case "it": 451 | tx, ty, a = b.x+b.w/2, b.y+fh, "tc" 452 | case "ib": 453 | tx, ty, a = b.x+b.w/2, b.y+b.h-fh, "bc" 454 | case "ob": 455 | tx, ty, a = b.x+b.w/2, b.y+b.h+fh, "tc" 456 | case "ol": 457 | tx, ty, a = b.x-fh, b.y+b.h/2, "cr" 458 | case "il": 459 | tx, ty, a = b.x+fh, b.y+b.h/2, "cl" 460 | case "or": 461 | tx, ty, a = b.x+b.w+fh, b.y+b.h/2, "cl" 462 | case "ir": 463 | tx, ty, a = b.x+b.w-fh, b.y+b.h/2, "cr" 464 | default: 465 | tx, ty, a = b.x+b.w/2, b.y+b.h/2, "cc" 466 | 467 | } 468 | 469 | bg.Text(tx, ty, b.t, a, 0, b.f) 470 | } 471 | } 472 | } 473 | 474 | // GenericWedge draws a pie/wedge just by lines 475 | func GenericWedge(mg MinimalGraphics, x, y, ro, ri int, phi, psi, ecc float64, style Style) { 476 | for phi < 0 { 477 | phi += 2 * math.Pi 478 | } 479 | for psi < 0 { 480 | psi += 2 * math.Pi 481 | } 482 | for phi >= 2*math.Pi { 483 | phi -= 2 * math.Pi 484 | } 485 | for psi >= 2*math.Pi { 486 | psi -= 2 * math.Pi 487 | } 488 | // debug.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) 489 | 490 | if ri > ro { 491 | panic("ri > ro is not possible") 492 | } 493 | 494 | if style.FillColor != "" { 495 | fillWedge(mg, x, y, ro, ri, phi, psi, ecc, style) 496 | } 497 | 498 | roe, rof := float64(ro)*ecc, float64(ro) 499 | rie, rif := float64(ri)*ecc, float64(ri) 500 | xa, ya := int(math.Cos(phi)*roe)+x, y-int(math.Sin(phi)*rof) 501 | xc, yc := int(math.Cos(psi)*roe)+x, y-int(math.Sin(psi)*rof) 502 | xai, yai := int(math.Cos(phi)*rie)+x, y-int(math.Sin(phi)*rif) 503 | xci, yci := int(math.Cos(psi)*rie)+x, y-int(math.Sin(psi)*rif) 504 | 505 | if math.Abs(phi-psi) >= 4*math.Pi { 506 | phi, psi = 0, 2*math.Pi 507 | } else { 508 | if ri > 0 { 509 | mg.Line(xai, yai, xa, ya, style) 510 | mg.Line(xci, yci, xc, yc, style) 511 | } else { 512 | mg.Line(x, y, xa, ya, style) 513 | mg.Line(x, y, xc, yc, style) 514 | } 515 | } 516 | 517 | var xb, yb int 518 | exit := phi < psi 519 | for rho := phi; !exit || rho < psi; rho += 0.05 { // aproximate circle by more than 120 corners polygon 520 | if rho >= 2*math.Pi { 521 | exit = true 522 | rho -= 2 * math.Pi 523 | } 524 | xb, yb = int(math.Cos(rho)*roe)+x, y-int(math.Sin(rho)*rof) 525 | mg.Line(xa, ya, xb, yb, style) 526 | xa, ya = xb, yb 527 | } 528 | mg.Line(xb, yb, xc, yc, style) 529 | 530 | if ri > 0 { 531 | exit := phi < psi 532 | for rho := phi; !exit || rho < psi; rho += 0.1 { // aproximate circle by more than 60 corner polygon 533 | if rho >= 2*math.Pi { 534 | exit = true 535 | rho -= 2 * math.Pi 536 | } 537 | xb, yb = int(math.Cos(rho)*rie)+x, y-int(math.Sin(rho)*rif) 538 | mg.Line(xai, yai, xb, yb, style) 539 | xai, yai = xb, yb 540 | } 541 | mg.Line(xb, yb, xci, yci, style) 542 | 543 | } 544 | } 545 | 546 | // Fill wedge with center (xi,yi), radius ri from alpha to beta with style. 547 | // Precondition: 0 <= beta < alpha < pi/2 548 | func fillQuarterWedge(mg MinimalGraphics, xi, yi, ri int, alpha, beta, e float64, style Style, quadrant int) { 549 | if alpha < beta { 550 | // debug.Printf("Swaping alpha and beta") 551 | alpha, beta = beta, alpha 552 | } 553 | // debug.Printf("fillQuaterWedge from %.1f to %.1f radius %d in quadrant %d.", 180*alpha/math.Pi, 180*beta/math.Pi, ri, quadrant) 554 | r := float64(ri) 555 | 556 | ta, tb := math.Tan(alpha), math.Tan(beta) 557 | for y := int(r * math.Sin(alpha)); y >= 0; y-- { 558 | yf := float64(y) 559 | x0 := yf / ta 560 | x1 := yf / tb 561 | x2 := math.Sqrt(r*r - yf*yf) 562 | // debug.Printf("y=%d x0=%.2f x1=%.2f x2=%.2f border=%t", y, x0, x1, x2, (x2 0.01 { 632 | fillQuarterWedge(mg, xi, yi, ro, mapQ(phi, qPhi), mapQ(w, qPhi), epsilon, style, qPhi) 633 | if ri > 0 { 634 | fillQuarterWedge(mg, xi, yi, ri, mapQ(phi, qPhi), mapQ(w, qPhi), epsilon, blank, qPhi) 635 | } 636 | } 637 | phi = w 638 | qPhi++ 639 | if qPhi == 4 { 640 | // debug.Printf("Wrapped phi around") 641 | phi, qPhi = 0, 0 642 | } 643 | } 644 | if phi != psi { 645 | // debug.Printf("Last wedge") 646 | fillQuarterWedge(mg, xi, yi, ro, mapQ(phi, qPhi), mapQ(psi, qPhi), epsilon, style, qPhi) 647 | if ri > 0 { 648 | fillQuarterWedge(mg, xi, yi, ri, mapQ(phi, qPhi), mapQ(psi, qPhi), epsilon, blank, qPhi) 649 | } 650 | } 651 | } 652 | 653 | func GenericRings(bg BasicGraphics, wedges []Wedgeinfo, x, y, ro, ri int, eccentricity float64) { 654 | // debug.Printf("GenericRings with %d wedges center %d,%d, radii %d/%d, ecc=%.3f)", len(wedges), x, y, ro, ri, eccentricity) 655 | 656 | for _, w := range wedges { 657 | 658 | // Correct center 659 | p := 0.4 * float64(w.Style.LineWidth+w.Shift) 660 | 661 | // cphi, sphi := math.Cos(w.Phi), math.Sin(w.Phi) 662 | // cpsi, spsi := math.Cos(w.Psi), math.Sin(w.Psi) 663 | a := math.Sin((w.Psi - w.Phi) / 2) 664 | dx, dy := p*math.Cos((w.Phi+w.Psi)/2)/a, p*math.Sin((w.Phi+w.Psi)/2)/a 665 | // debug.Printf("Center adjustment (lw=%d, p=%.2f), for wedge %d°-%d° of (%.1f,%.1f)", w.Style.LineWidth, p, int(180*w.Phi/math.Pi), int(180*w.Psi/math.Pi), dx, dy) 666 | xi, yi := x+int(dx+0.5), y+int(dy+0.5) 667 | bg.Wedge(xi, yi, ro, ri, w.Phi, w.Psi, w.Style) 668 | 669 | if w.Text != "" { 670 | _, fh, _ := bg.FontMetrics(w.Font) 671 | fh += 0 672 | alpha := (w.Phi + w.Psi) / 2 673 | var rt int 674 | if ri > 0 { 675 | rt = (ri + ro) / 2 676 | } else { 677 | rt = ro - 3*fh 678 | if rt <= ro/2 { 679 | rt = ro - 2*fh 680 | } 681 | } 682 | // debug.Printf("Text %s at %d° r=%d", w.Text, int(180*alpha/math.Pi), rt) 683 | tx := int(float64(rt)*math.Cos(alpha)*eccentricity+0.5) + x 684 | ty := y + int(float64(rt)*math.Sin(alpha)+0.5) 685 | 686 | bg.Text(tx, ty, w.Text, "cc", 0, w.Font) 687 | } 688 | 689 | } 690 | 691 | } 692 | 693 | func GenericCircle(bg BasicGraphics, x, y, r int, style Style) { 694 | // TODO: fill 695 | x0, y0 := x+r, y 696 | rf := float64(r) 697 | for a := 0.2; a < 2*math.Pi; a += 0.2 { 698 | x1, y1 := int(rf*math.Cos(a))+x, int(rf*math.Sin(a))+y 699 | bg.Line(x0, y0, x1, y1, style) 700 | x0, y0 = x1, y1 701 | } 702 | } 703 | 704 | func polygon(bg BasicGraphics, x, y []int, style Style) { 705 | n := len(x) - 1 706 | for i := 0; i < n; i++ { 707 | bg.Line(x[i], y[i], x[i+1], y[i+1], style) 708 | } 709 | bg.Line(x[n], y[n], x[0], y[0], style) 710 | } 711 | 712 | func GenericSymbol(bg BasicGraphics, x, y int, style Style) { 713 | f := style.SymbolSize 714 | if f == 0 { 715 | f = 1 716 | } 717 | if style.LineWidth <= 0 { 718 | style.LineWidth = 1 719 | } 720 | 721 | if style.SymbolColor == "" { 722 | style.SymbolColor = style.LineColor 723 | if style.SymbolColor == "" { 724 | style.SymbolColor = style.FillColor 725 | if style.SymbolColor == "" { 726 | style.SymbolColor = "#000000" 727 | } 728 | } 729 | } 730 | 731 | style.LineColor = style.SymbolColor 732 | 733 | const n = 5 // default size 734 | a := int(n*f + 0.5) // standard 735 | b := int(n/2*f + 0.5) // smaller 736 | c := int(1.155*n*f + 0.5) // triangel long sist 737 | d := int(0.577*n*f + 0.5) // triangle short dist 738 | e := int(0.866*n*f + 0.5) // diagonal 739 | 740 | switch style.Symbol { 741 | case '*': 742 | bg.Line(x-e, y-e, x+e, y+e, style) 743 | bg.Line(x-e, y+e, x+e, y-e, style) 744 | fallthrough 745 | case '+': 746 | bg.Line(x-a, y, x+a, y, style) 747 | bg.Line(x, y-a, x, y+a, style) 748 | case 'X': 749 | bg.Line(x-e, y-e, x+e, y+e, style) 750 | bg.Line(x-e, y+e, x+e, y-e, style) 751 | case 'o': 752 | GenericCircle(bg, x, y, a, style) 753 | case '0': 754 | GenericCircle(bg, x, y, a, style) 755 | GenericCircle(bg, x, y, b, style) 756 | case '.': 757 | GenericCircle(bg, x, y, b, style) 758 | case '@': 759 | GenericCircle(bg, x, y, a, style) 760 | aa := (4 * a) / 5 761 | GenericCircle(bg, x, y, aa, style) 762 | aa = (3 * a) / 5 763 | GenericCircle(bg, x, y, aa, style) 764 | aa = (2 * a) / 5 765 | GenericCircle(bg, x, y, aa, style) 766 | aa = a / 5 767 | GenericCircle(bg, x, y, aa, style) 768 | bg.Line(x, y, x, y, style) 769 | case '=': // TODO check 770 | bg.Rect(x-e, y-e, 2*e, 2*e, style) 771 | case '#': // TODO check 772 | bg.Rect(x-e, y-e, 2*e, 2*e, style) 773 | case 'A': 774 | polygon(bg, []int{x - a, x + a, x}, []int{y + d, y + d, y - c}, style) 775 | aa, dd, cc := (3*a)/4, (3*d)/4, (3*c)/4 776 | polygon(bg, []int{x - aa, x + aa, x}, []int{y + dd, y + dd, y - cc}, style) 777 | aa, dd, cc = a/2, d/2, c/2 778 | polygon(bg, []int{x - aa, x + aa, x}, []int{y + dd, y + dd, y - cc}, style) 779 | aa, dd, cc = a/4, d/4, c/4 780 | polygon(bg, []int{x - aa, x + aa, x}, []int{y + dd, y + dd, y - cc}, style) 781 | case '%': 782 | polygon(bg, []int{x - a, x + a, x}, []int{y + d, y + d, y - c}, style) 783 | case 'W': 784 | polygon(bg, []int{x - a, x + a, x}, []int{y - c, y - c, y + d}, style) 785 | aa, dd, cc := (3*a)/4, (3*d)/4, (3*c)/4 786 | polygon(bg, []int{x - aa, x + aa, x}, []int{y - cc, y - cc, y + dd}, style) 787 | aa, dd, cc = a/2, d/2, c/2 788 | polygon(bg, []int{x - aa, x + aa, x}, []int{y - cc, y - cc, y + dd}, style) 789 | aa, dd, cc = a/4, d/4, c/4 790 | polygon(bg, []int{x - aa, x + aa, x}, []int{y - cc, y - cc, y + dd}, style) 791 | case 'V': 792 | polygon(bg, []int{x - a, x + a, x}, []int{y - c, y - c, y + d}, style) 793 | case 'Z': 794 | polygon(bg, []int{x - e, x, x + e, x}, []int{y, y + e, y, y - e}, style) 795 | ee := (3 * e) / 4 796 | polygon(bg, []int{x - ee, x, x + ee, x}, []int{y, y + ee, y, y - ee}, style) 797 | ee = e / 2 798 | polygon(bg, []int{x - ee, x, x + ee, x}, []int{y, y + ee, y, y - ee}, style) 799 | ee = e / 4 800 | polygon(bg, []int{x - ee, x, x + ee, x}, []int{y, y + ee, y, y - ee}, style) 801 | case '&': 802 | polygon(bg, []int{x - e, x, x + e, x}, []int{y, y + e, y, y - e}, style) 803 | default: 804 | bg.Text(x, y, "?", "cc", 0, Font{}) 805 | } 806 | 807 | } 808 | -------------------------------------------------------------------------------- /graphics_test.go: -------------------------------------------------------------------------------- 1 | package chart_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vdobler/chart" 6 | "github.com/vdobler/chart/txtg" 7 | "math" 8 | "testing" 9 | ) 10 | 11 | const r = 18 12 | const ri = 10 13 | 14 | func initDemoCircle() *txtg.TextGraphics { 15 | g := txtg.NewTextGraphics(60, 40) 16 | g.Line(0, 20, 59, 20, chart.Style{Symbol: '-'}) 17 | g.Line(30, 0, 30, 39, chart.Style{Symbol: '|'}) 18 | for p := 0.0; p <= 2*math.Pi; p += 0.1 { 19 | x := int(r * math.Cos(p) * 1.5) 20 | y := int(r * math.Sin(p)) 21 | g.Symbol(30+x, 20+y, '*', chart.Style{Symbol: '*'}) 22 | } 23 | return g 24 | } 25 | 26 | func TestGenericWedge(t *testing.T) { 27 | g := initDemoCircle() 28 | s := chart.Style{Symbol: '#', FillColor: "#ff0000", LineColor: "#0000ff"} 29 | ra := math.Pi / 2 30 | 31 | chart.GenericWedge(g, 30, 20, r, ri, 0.15*ra, 0.5*ra, 1.5, s) 32 | fmt.Printf("\n%s\n", g.String()) 33 | 34 | chart.GenericWedge(g, 30, 20, r, ri, 1.15*ra, 1.5*ra, 1.5, s) 35 | fmt.Printf("\n%s\n", g.String()) 36 | 37 | chart.GenericWedge(g, 30, 20, r, ri, 2.15*ra, 2.5*ra, 1.5, s) 38 | fmt.Printf("\n%s\n", g.String()) 39 | 40 | chart.GenericWedge(g, 30, 20, r, ri, 3.15*ra, 3.5*ra, 1.5, s) 41 | fmt.Printf("\n%s\n", g.String()) 42 | 43 | // mored than one quadrant 44 | g = initDemoCircle() 45 | chart.GenericWedge(g, 30, 20, r, ri, 0.15*ra, 1.5*ra, 1.5, s) 46 | fmt.Printf("\n%s\n", g.String()) 47 | 48 | chart.GenericWedge(g, 30, 20, r, ri, 2.15*ra, 3.5*ra, 1.5, s) 49 | fmt.Printf("\n%s\n", g.String()) 50 | 51 | g = initDemoCircle() 52 | chart.GenericWedge(g, 30, 20, r, ri, 1.5*ra, 2.5*ra, 1.5, s) 53 | fmt.Printf("\n%s\n", g.String()) 54 | 55 | // all 4 quadrants 56 | g = initDemoCircle() 57 | chart.GenericWedge(g, 30, 20, r, ri, 1.5*ra, 0.5*ra, 1.5, s) 58 | fmt.Printf("\n%s\n", g.String()) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /hist.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | // "os" 7 | // "strings" 8 | ) 9 | 10 | type HistChartData struct { 11 | Name string 12 | Style Style 13 | Samples []float64 14 | } 15 | 16 | // HistChart represents histogram charts. (Not to be mixed up with BarChart!) 17 | type HistChart struct { 18 | XRange, YRange Range // Lower limit of YRange is fixed to 0 and not available for input 19 | Title string // Title of chart 20 | Key Key // Key/Legend 21 | Counts bool // Display counts instead of frequencies 22 | Stacked bool // Display different data sets ontop of each other 23 | Shifted bool // Shift non-stacked bars sideways (and make them smaler) 24 | Data []HistChartData 25 | FirstBin float64 // center of the first (lowest bin) 26 | BinWidth float64 // Width of bins (0: auto) 27 | TBinWidth TimeDelta // BinWidth for time XRange 28 | Gap float64 // gap between bins in (bin-width units): 0<=Gap<1, 29 | Sep float64 // separation of bars in one bin (in bar width units) -1= -1 && x < 1 { 43 | return 0.5 44 | } 45 | return 0 46 | } 47 | 48 | // 1 - |x| 49 | TriangularKernel = func(x float64) float64 { 50 | if x >= -1 && x < 1 { 51 | return 1 - math.Abs(x) 52 | } 53 | return 0 54 | } 55 | 56 | // 15/16 * (1-x^2)^2 57 | BisquareKernel Kernel = func(x float64) float64 { 58 | if x >= -1 && x < 1 { 59 | a := (1 - x*x) 60 | return 15.0 / 16.0 * a * a 61 | } 62 | return 0 63 | } 64 | 65 | // 35/32 * (1-x^2)^3 66 | TriweightKernel Kernel = func(x float64) float64 { 67 | if x >= -1 && x < 1 { 68 | a := (1 - x*x) 69 | return 35.0 / 32.0 * a * a * a 70 | } 71 | return 0 72 | } 73 | 74 | // 3/4 * (1-x^2) 75 | EpanechnikovKernel Kernel = func(x float64) float64 { 76 | if x >= -1 && x < 1 { 77 | return 3.0 / 4.0 * (1.0 - x*x) 78 | } 79 | return 0 80 | } 81 | 82 | // 1/sqrt(2pi) * exp(-1/2x^2) 83 | GaussKernel Kernel = func(x float64) float64 { 84 | return sqrt2piinv * math.Exp(-0.5*x*x) 85 | } 86 | ) 87 | 88 | // AddData will add data to the plot. Legend will be updated by name. 89 | func (c *HistChart) AddData(name string, data []float64, style Style) { 90 | // Style 91 | if style.empty() { 92 | style = AutoStyle(len(c.Data), true) 93 | } 94 | 95 | // Init axis, add data, autoscale 96 | if len(c.Data) == 0 { 97 | c.XRange.init() 98 | } 99 | c.Data = append(c.Data, HistChartData{name, style, data}) 100 | for _, d := range data { 101 | c.XRange.autoscale(d) 102 | } 103 | 104 | // Key/Legend 105 | if name != "" { 106 | c.Key.Entries = append(c.Key.Entries, KeyEntry{Text: name, Style: style, PlotStyle: PlotStyleBox}) 107 | } 108 | } 109 | 110 | // AddDataInt is a convenience method to add integer data (a simple wrapper 111 | // around AddData). 112 | func (c *HistChart) AddDataInt(name string, data []int, style Style) { 113 | fdata := make([]float64, len(data)) 114 | for i, d := range data { 115 | fdata[i] = float64(d) 116 | } 117 | c.AddData(name, fdata, style) 118 | } 119 | 120 | // AddDataGeneric is the generic version which allows the addition of any type 121 | // implementing the Value interface. 122 | func (c *HistChart) AddDataGeneric(name string, data []Value, style Style) { 123 | fdata := make([]float64, len(data)) 124 | for i, d := range data { 125 | fdata[i] = d.XVal() 126 | } 127 | c.AddData(name, fdata, style) 128 | } 129 | 130 | // G = B * Gf; S = W *Sf 131 | // W = (B(1-Gf))/(N-(N-1)Sf) 132 | // S = (B(1-Gf))/(N/Sf - (N-1)) 133 | // N Gf Sf 134 | // 2 1/4 1/3 135 | // 3 1/5 1/2 136 | // 4 1/6 2/3 137 | // 5 1/6 3/4 138 | func (c *HistChart) widthFactor() (gf, sf float64) { 139 | if c.Stacked || !c.Shifted { 140 | gf = c.Gap 141 | sf = -1 142 | return 143 | } 144 | 145 | switch len(c.Data) { 146 | case 1: 147 | gf = c.Gap 148 | sf = -1 149 | return 150 | case 2: 151 | gf = 1.0 / 4.0 152 | sf = -1.0 / 3.0 153 | case 3: 154 | gf = 1.0 / 5.0 155 | sf = -1.0 / 2.0 156 | case 4: 157 | gf = 1.0 / 6.0 158 | sf = -2.0 / 3.0 159 | default: 160 | gf = 1.0 / 6.0 161 | sf = -2.0 / 4.0 162 | } 163 | 164 | if c.Gap != 0 { 165 | gf = c.Gap 166 | } 167 | if c.Sep != 0 { 168 | sf = c.Sep 169 | } 170 | return 171 | } 172 | 173 | // Prepare binCnt bins of width binWidth starting from binStart and count 174 | // data samples per bin for each data set. If c.Counts is true than the 175 | // absolute counts are returned instead if the frequencies. max is the 176 | // largest y-value which will occur in our plot. 177 | func (c *HistChart) binify(binStart, binWidth float64, binCnt int) (freqs [][]float64, max float64) { 178 | x2bin := func(x float64) int { return int((x - binStart) / binWidth) } 179 | 180 | freqs = make([][]float64, len(c.Data)) // freqs[d][b] is frequency/count of bin b in dataset d 181 | max = 0 182 | for i, data := range c.Data { 183 | freq := make([]float64, binCnt) 184 | drops := 0 185 | for _, x := range data.Samples { 186 | bin := x2bin(x) 187 | if bin < 0 || bin >= binCnt { 188 | // fmt.Printf("!!!!! Lost %.3f (bin=%d)\n", x, bin) 189 | drops++ 190 | continue 191 | } 192 | freq[bin] = freq[bin] + 1 193 | //fmt.Printf("Value %.2f sorted into bin %d, count now %d\n", x, bin, int(freq[bin])) 194 | } 195 | // scale if requested and determine max 196 | n := float64(len(data.Samples) - drops) 197 | // debug.Printf("Dataset %d has %d samples (by %d drops).\n", i, int(n), drops) 198 | ff := 0.0 199 | for bin := 0; bin < binCnt; bin++ { 200 | if !c.Counts { 201 | freq[bin] = 100 * freq[bin] / n 202 | } 203 | ff += freq[bin] 204 | if freq[bin] > max { 205 | max = freq[bin] 206 | } 207 | } 208 | freqs[i] = freq 209 | } 210 | // debug.Printf("Maximum : %.2f\n", max) 211 | if c.Stacked { // recalculate max 212 | max = 0 213 | for bin := 0; bin < binCnt; bin++ { 214 | sum := 0.0 215 | for i := range freqs { 216 | sum += freqs[i][bin] 217 | } 218 | // fmt.Printf("sum of bin %d = %d\n", bin, sum) 219 | if sum > max { 220 | max = sum 221 | } 222 | } 223 | // debug.Printf("Re-Maxed (stacked) to: %.2f\n", max) 224 | } 225 | return 226 | } 227 | 228 | func (c *HistChart) findBinWidth() { 229 | bw := c.XRange.TicSetting.Delta 230 | if bw == 0 { // this should not happen... 231 | bw = 1 232 | } 233 | 234 | // Average sample count (n) and "optimum" bin count obc 235 | n := 0 236 | for _, data := range c.Data { 237 | for _, x := range data.Samples { 238 | // Count only data in valid x-range. 239 | if x >= c.XRange.Min && x <= c.XRange.Max { 240 | n++ 241 | } 242 | } 243 | } 244 | n /= len(c.Data) 245 | obc := math.Sqrt(float64(n)) 246 | // debug.Printf("Average size of %d data sets: %d (obc=%d)\n", len(c.Data), n, int(obc+0.5)) 247 | 248 | // Increase/decrease bin width if tic delta yields massively bad choice 249 | binCnt := int((c.XRange.Max-c.XRange.Min)/bw + 0.5) 250 | if binCnt >= int(2*obc) { 251 | bw *= 2 // TODO: not so nice if bw is of form 2*10^n (use 2.5 in this case to match tics) 252 | //debug.Printf("Increased bin width to %.3f (optimum bin cnt = %d, was %d).\n", bw, int(obc+0.5), binCnt) 253 | } else if binCnt < int(3*obc) { 254 | bw /= 2 255 | // debug.Printf("Reduced bin width to %.3f (optimum bin cnt = %d, was %d).\n", bw, int(obc+0.5), binCnt) 256 | } else { 257 | // debug.Printf("Bin width of %.3f is ok (optimum bin cnt = %d, was %d).\n", bw, int(obc+0.5), binCnt) 258 | } 259 | 260 | c.BinWidth = bw 261 | } 262 | 263 | // Reset chart to state before plotting. 264 | func (c *HistChart) Reset() { 265 | c.XRange.Reset() 266 | c.YRange.Reset() 267 | } 268 | 269 | // Plot will output the chart to the graphic device g. 270 | func (c *HistChart) Plot(g Graphics) { 271 | layout := layout(g, c.Title, c.XRange.Label, c.YRange.Label, 272 | c.XRange.TicSetting.Hide, c.YRange.TicSetting.Hide, &c.Key) 273 | fw, fh, _ := g.FontMetrics(DefaultFont["label"]) 274 | fw += 0 275 | 276 | width, height := layout.Width, layout.Height 277 | topm, leftm := layout.Top, layout.Left 278 | numxtics, numytics := layout.NumXtics, layout.NumYtics 279 | 280 | // Outside bound ranges for histograms are nicer 281 | leftm, width = leftm+int(2*fw), width-int(2*fw) 282 | topm, height = topm, height-int(1*fh) 283 | 284 | c.XRange.Setup(numxtics, numxtics+4, width, leftm, false) 285 | 286 | // TODO(vodo) a) BinWidth might be input, alignment to tics should be nice, binCnt, ... 287 | if c.BinWidth == 0 { 288 | c.findBinWidth() 289 | } 290 | 291 | xmin, _ := c.XRange.Min, c.XRange.Max 292 | binStart := c.BinWidth * math.Ceil(xmin/c.BinWidth) 293 | c.FirstBin = binStart + c.BinWidth/2 294 | binCnt := int(math.Floor(c.XRange.Max-binStart) / c.BinWidth) 295 | // debug.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) 296 | counts, max := c.binify(binStart, c.BinWidth, binCnt) 297 | 298 | // Calculate smoothed density plots and re-max y. 299 | var smoothed [][]EPoint 300 | if !c.Stacked && c.Kernel != nil { 301 | smoothed = make([][]EPoint, len(c.Data)) 302 | for d := range c.Data { 303 | p, m := c.smoothed(d, binCnt) 304 | smoothed[d] = p 305 | if m > max { 306 | max = m 307 | } 308 | } 309 | } 310 | 311 | // Fix lower end of y axis 312 | c.YRange.DataMin = 0 313 | c.YRange.MinMode.Fixed = true 314 | c.YRange.MinMode.Value = 0 315 | c.YRange.autoscale(float64(max)) 316 | c.YRange.Setup(numytics, numytics+2, height, topm, true) 317 | 318 | g.Begin() 319 | 320 | if c.Title != "" { 321 | g.Title(c.Title) 322 | } 323 | 324 | g.XAxis(c.XRange, topm+height+fh, topm) 325 | g.YAxis(c.YRange, leftm-int(2*fw), leftm+width) 326 | 327 | xf := c.XRange.Data2Screen 328 | yf := c.YRange.Data2Screen 329 | 330 | numSets := len(c.Data) 331 | n := float64(numSets) 332 | gf, sf := c.widthFactor() 333 | 334 | ww := c.BinWidth * (1 - gf) // w' 335 | var w, s float64 336 | if !c.Stacked && c.Shifted { 337 | w = ww / (n + (n-1)*sf) 338 | s = w * sf 339 | } else { 340 | w = ww 341 | s = -ww 342 | } 343 | 344 | // debug.Printf("gf=%.3f, sf=%.3f, bw=%.3f ===> ww=%.2f, w=%.2f, s=%.2f\n", gf, sf, c.BinWidth, ww, w, s) 345 | 346 | if c.Shifted || c.Stacked { 347 | for d := numSets - 1; d >= 0; d-- { 348 | bars := make([]Barinfo, 0, binCnt) 349 | ws := 0 350 | for b := 0; b < binCnt; b++ { 351 | if counts[d][b] == 0 { 352 | continue 353 | } 354 | xb := binStart + (float64(b)+0.5)*c.BinWidth 355 | x := xb - ww/2 + float64(d)*(s+w) 356 | xs := xf(x) 357 | xss := xf(x + w) 358 | ws = xss - xs 359 | thebar := Barinfo{x: xs, w: xss - xs} 360 | 361 | off := 0.0 362 | if c.Stacked { 363 | for dd := d - 1; dd >= 0; dd-- { 364 | off += counts[dd][b] 365 | } 366 | } 367 | a, aa := yf(float64(off+counts[d][b])), yf(float64(off)) 368 | thebar.y, thebar.h = a, iabs(a-aa) 369 | bars = append(bars, thebar) 370 | } 371 | g.Bars(bars, c.Data[d].Style) 372 | 373 | if !c.Stacked && sf < 0 && gf != 0 && fh > 1 { 374 | // Whitelining 375 | lw := 1 376 | if ws > 25 { 377 | lw = 2 378 | } 379 | white := Style{LineColor: "#ffffff", LineWidth: lw, LineStyle: SolidLine} 380 | for _, b := range bars { 381 | g.Line(b.x, b.y-1, b.x+b.w+1, b.y-1, white) 382 | g.Line(b.x+b.w+1, b.y-1, b.x+b.w+1, b.y+b.h, white) 383 | } 384 | } 385 | } 386 | 387 | } else { 388 | bars := make([]Barinfo, 1) 389 | order := make([]int, numSets) 390 | for b := 0; b < binCnt; b++ { 391 | // shame on me... 392 | for d := 0; d < numSets; d++ { 393 | order[d] = d 394 | } 395 | for d := 0; d < numSets; d++ { 396 | for p := 0; p < numSets-1; p++ { 397 | if counts[order[p]][b] < counts[order[p+1]][b] { 398 | order[p], order[p+1] = order[p+1], order[p] 399 | } 400 | } 401 | } 402 | for d := 0; d < numSets; d++ { 403 | if counts[order[d]][b] == 0 { 404 | continue 405 | } 406 | xb := binStart + (float64(b)+0.5)*c.BinWidth 407 | x := xb - ww/2 + float64(d)*(s+w) 408 | xs := xf(x) 409 | xss := xf(x + w) 410 | thebar := Barinfo{x: xs, w: xss - xs} 411 | 412 | a, aa := yf(float64(counts[order[d]][b])), yf(0) 413 | thebar.y, thebar.h = a, iabs(a-aa) 414 | bars[0] = thebar 415 | g.Bars(bars, c.Data[order[d]].Style) 416 | } 417 | } 418 | } 419 | 420 | if !c.Stacked && c.Kernel != nil { 421 | for d := numSets - 1; d >= 0; d-- { 422 | style := Style{Symbol:/*c.Data[d].Style.Symbol*/ 'X', LineColor: c.Data[d].Style.LineColor, 423 | LineWidth: 1, LineStyle: SolidLine} 424 | for j := range smoothed[d] { 425 | // now YRange is set up: transform to screen coordinates 426 | smoothed[d][j].Y = float64(c.YRange.Data2Screen(smoothed[d][j].Y)) 427 | } 428 | g.Scatter(smoothed[d], PlotStyleLines, style) 429 | } 430 | } 431 | 432 | if !c.Key.Hide { 433 | g.Key(layout.KeyX, layout.KeyY, c.Key) 434 | } 435 | g.End() 436 | } 437 | 438 | // Smooth data set i. The Y-value of the returned points is not jet in screen coordinates 439 | // but in data coordinates! (Reason: YRange not set up jet) 440 | func (c *HistChart) smoothed(i, binCnt int) (points []EPoint, max float64) { 441 | nan := math.NaN() 442 | 443 | samples := imax(25, binCnt*5) 444 | 445 | step := (c.XRange.Max - c.XRange.Min) / float64(samples) 446 | points = make([]EPoint, 0, 50) 447 | h := c.BinWidth 448 | K := c.Kernel 449 | n := float64(len(c.Data[i].Samples)) 450 | 451 | ff := 0.0 452 | for x := c.XRange.Min; x <= c.XRange.Max; x += step { 453 | f := 0.0 454 | for _, xi := range c.Data[i].Samples { 455 | f += K((x - xi) / h) 456 | } 457 | f /= h 458 | if !c.Counts { 459 | f /= n 460 | f *= 100 // as display is in % 461 | } 462 | 463 | // Rescale kernel density estimation by width of bars: 464 | f *= c.BinWidth 465 | if f > max { 466 | max = f 467 | } 468 | xx := float64(c.XRange.Data2Screen(x)) 469 | // yy := float64(c.YRange.Data2Screen(f)) 470 | // fmt.Printf("Consructed %.3f, %.4f\n", x, f) 471 | points = append(points, EPoint{X: xx, Y: f, DeltaX: nan, DeltaY: nan}) 472 | } 473 | fmt.Printf("Dataset %d: ff=%.4f\n", i, ff) 474 | 475 | return 476 | } 477 | -------------------------------------------------------------------------------- /imgg/image.go: -------------------------------------------------------------------------------- 1 | package imgg 2 | 3 | import ( 4 | "code.google.com/p/draw2d/draw2d" 5 | "code.google.com/p/freetype-go/freetype" 6 | "code.google.com/p/freetype-go/freetype/raster" 7 | "code.google.com/p/freetype-go/freetype/truetype" 8 | "code.google.com/p/graphics-go/graphics" 9 | // "fmt" 10 | "github.com/vdobler/chart" 11 | "image" 12 | "image/color" 13 | "image/draw" 14 | "log" 15 | "math" 16 | ) 17 | 18 | var ( 19 | dpi = 72 20 | defaultFont *truetype.Font 21 | ) 22 | 23 | func init() { 24 | var err error 25 | defaultFont, err = freetype.ParseFont(defaultFontData()) 26 | if err != nil { 27 | panic(err) 28 | } 29 | } 30 | 31 | // ImageGraphics writes plot to an image.RGBA 32 | type ImageGraphics struct { 33 | Image *image.RGBA // The image the plots are drawn onto. 34 | x0, y0 int 35 | w, h int 36 | bg color.RGBA 37 | gc draw2d.GraphicContext 38 | font *truetype.Font 39 | fs int 40 | } 41 | 42 | // New creates a new ImageGraphics including an image.RGBA of dimension w x h 43 | // with background bgcol. It uses font in the given fontsize for text. 44 | // If font is nil it will use a builtin font. 45 | func New(width, height int, bgcol color.RGBA, font *truetype.Font, fontsize int) *ImageGraphics { 46 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 47 | gc := draw2d.NewGraphicContext(img) 48 | gc.SetLineJoin(draw2d.MiterJoin) 49 | gc.SetLineCap(draw2d.SquareCap) 50 | gc.SetStrokeColor(image.Black) 51 | gc.SetFillColor(bgcol) 52 | gc.Translate(0.5, 0.5) 53 | gc.Clear() 54 | if font == nil { 55 | font = defaultFont 56 | } 57 | 58 | return &ImageGraphics{Image: img, x0: 0, y0: 0, w: width, h: height, 59 | bg: bgcol, gc: gc, font: font, fs: fontsize} 60 | } 61 | 62 | // AddTo returns a new ImageGraphics which will write to (width x height) sized 63 | // area starting at (x,y) on the provided image img. The rest of the parameters 64 | // are the same as in New(). 65 | func AddTo(img *image.RGBA, x, y, width, height int, bgcol color.RGBA, font *truetype.Font, fontsize int) *ImageGraphics { 66 | gc := draw2d.NewGraphicContext(img) 67 | gc.SetStrokeColor(image.Black) 68 | gc.SetFillColor(bgcol) 69 | gc.Translate(float64(x)+0.5, float64(y)+0.5) 70 | gc.ClearRect(x, y, x+width, y+height) 71 | if font == nil { 72 | font = defaultFont 73 | } 74 | 75 | return &ImageGraphics{Image: img, x0: x, y0: y, w: width, h: height, bg: bgcol, gc: gc, font: font, fs: fontsize} 76 | } 77 | 78 | func (ig *ImageGraphics) Begin() {} 79 | func (ig *ImageGraphics) End() {} 80 | func (ig *ImageGraphics) Background() (r, g, b, a uint8) { return ig.bg.R, ig.bg.G, ig.bg.B, ig.bg.A } 81 | func (ig *ImageGraphics) Dimensions() (int, int) { return ig.w, ig.h } 82 | func (ig *ImageGraphics) FontMetrics(font chart.Font) (fw float32, fh int, mono bool) { 83 | fh = ig.relFontsizeToPixel(font.Size) 84 | // typical width is 0.6 * height 85 | fw = 0.6 * float32(fh) 86 | mono = true 87 | return 88 | } 89 | func (ig *ImageGraphics) TextLen(s string, font chart.Font) int { 90 | size := ig.relFontsizeToPixel(font.Size) 91 | 92 | c := freetype.NewContext() 93 | c.SetDPI(dpi) 94 | c.SetFont(ig.font) 95 | c.SetFontSize(float64(size)) 96 | 97 | var p raster.Point 98 | prev, hasPrev := truetype.Index(0), false 99 | for _, rune := range s { 100 | index := ig.font.Index(rune) 101 | if hasPrev { 102 | p.X += c.FUnitToFix32(int(ig.font.Kerning(prev, index))) 103 | } 104 | p.X += c.FUnitToFix32(int(ig.font.HMetric(index).AdvanceWidth)) 105 | prev, hasPrev = index, true 106 | } 107 | return int(p.X / 256) 108 | } 109 | 110 | func (ig *ImageGraphics) setStyle(style chart.Style) { 111 | ig.gc.SetStrokeColor(chart.Color2RGBA(style.LineColor, 0xff)) 112 | ig.gc.SetLineWidth(float64(style.LineWidth)) 113 | ig.gc.SetLineDash(dashPattern[style.LineStyle], 0) 114 | } 115 | 116 | func (ig *ImageGraphics) Line(x0, y0, x1, y1 int, style chart.Style) { 117 | if style.LineWidth <= 0 { 118 | style.LineWidth = 1 119 | } 120 | ig.setStyle(style) 121 | ig.gc.MoveTo(float64(x0)+0.5, float64(y0)+0.5) 122 | ig.gc.LineTo(float64(x1)+0.5, float64(y1)+0.5) 123 | ig.gc.Stroke() 124 | } 125 | 126 | var dashPattern map[int][]float64 = map[int][]float64{ 127 | chart.SolidLine: nil, // []float64{10}, 128 | chart.DashedLine: []float64{50, 20}, 129 | chart.DottedLine: []float64{20, 20}, 130 | chart.DashDotDotLine: []float64{50, 20, 20, 20, 20, 20}, 131 | chart.LongDashLine: []float64{50, 50}, 132 | chart.LongDotLine: []float64{20, 50}, 133 | } 134 | 135 | func (ig *ImageGraphics) Path(x, y []int, style chart.Style) { 136 | ig.setStyle(style) 137 | ig.gc.MoveTo(float64(x[0]), float64(y[0])) 138 | for i := 1; i < len(x); i++ { 139 | ig.gc.LineTo(float64(x[i]), float64(y[i])) 140 | } 141 | ig.gc.Stroke() 142 | } 143 | 144 | func (ig *ImageGraphics) relFontsizeToPixel(rel int) int { 145 | if rel == 0 { 146 | return ig.fs 147 | } 148 | 149 | fs := float64(ig.fs) 150 | factor := 1.2 151 | if rel < 0 { 152 | factor = 1 / factor 153 | rel = -rel 154 | } 155 | for rel > 0 { 156 | fs *= factor 157 | rel-- 158 | } 159 | 160 | if factor < 1 { 161 | return int(fs) // round down 162 | } 163 | return int(fs + 0.5) // round up 164 | } 165 | 166 | func (ig *ImageGraphics) Text(x, y int, t string, align string, rot int, f chart.Font) { 167 | if len(align) == 1 { 168 | align = "c" + align 169 | } 170 | // fw, fh, _ := ig.FontMetrics(f) 171 | //fmt.Printf("Text '%s' at (%d,%d) %s\n", t, x,y, align) 172 | // TODO: handle rot 173 | 174 | size := ig.relFontsizeToPixel(f.Size) 175 | textImage := ig.textBox(t, size) 176 | bounds := textImage.Bounds() 177 | w, h := bounds.Dx(), bounds.Dy() 178 | var centerX, centerY int 179 | 180 | if rot != 0 { 181 | alpha := float64(rot) / 180 * math.Pi 182 | cos := math.Cos(alpha) 183 | sin := math.Sin(alpha) 184 | hs, hc := float64(h)*sin, float64(h)*cos 185 | ws, wc := float64(w)*sin, float64(w)*cos 186 | W := int(math.Ceil(hs + wc)) 187 | H := int(math.Ceil(hc + ws)) 188 | rotated := image.NewAlpha(image.Rect(0, 0, W, H)) 189 | graphics.Rotate(rotated, textImage, &graphics.RotateOptions{-alpha}) 190 | textImage = rotated 191 | centerX, centerY = W/2, H/2 192 | 193 | switch align { 194 | case "bl": 195 | centerX, centerY = int(hs), H 196 | case "bc": 197 | centerX, centerY = W-int(wc/2), int(ws/2) 198 | case "br": 199 | centerX, centerY = W, int(hc) 200 | case "tl": 201 | centerX, centerY = 0, H-int(hc) 202 | case "tc": 203 | centerX, centerY = int(ws/2), H-int(ws/2) 204 | case "tr": 205 | centerX, centerY = W-int(hs), 0 206 | case "cl": 207 | centerX, centerY = int(hs/2), H-int(hc/2) 208 | case "cr": 209 | centerX, centerY = W-int(hs/2), int(hc/2) 210 | } 211 | } else { 212 | centerX, centerY = w/2, h/2 213 | switch align[0] { 214 | case 'b': 215 | centerY = h 216 | case 't': 217 | centerY = 0 218 | } 219 | switch align[1] { 220 | case 'l': 221 | centerX = 0 222 | case 'r': 223 | centerX = w 224 | } 225 | } 226 | 227 | bounds = textImage.Bounds() 228 | w, h = bounds.Dx(), bounds.Dy() 229 | x -= centerX 230 | y -= centerY 231 | x += ig.x0 232 | y += ig.y0 233 | 234 | col := "#000000" 235 | if f.Color != "" { 236 | col = f.Color 237 | } 238 | r, g, b := chart.Color2rgb(col) 239 | tcol := image.NewUniform(color.RGBA{uint8(r), uint8(g), uint8(b), 255}) 240 | 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, size int) image.Image { 247 | // Initialize the context. 248 | fg := image.NewUniform(color.Alpha{0xff}) 249 | bg := image.NewUniform(color.Alpha{0x00}) 250 | canvas := image.NewAlpha(image.Rect(0, 0, 400, 2*size)) 251 | draw.Draw(canvas, canvas.Bounds(), bg, image.ZP, draw.Src) 252 | 253 | c := freetype.NewContext() 254 | c.SetDPI(dpi) 255 | c.SetFont(ig.font) 256 | c.SetFontSize(float64(size)) 257 | c.SetClip(canvas.Bounds()) 258 | c.SetDst(canvas) 259 | c.SetSrc(fg) 260 | 261 | // Draw the text. 262 | h := c.FUnitToPixelRU(ig.font.UnitsPerEm()) 263 | pt := freetype.Pt(0, h) 264 | extent, err := c.DrawString(t, pt) 265 | if err != nil { 266 | log.Println(err) 267 | return nil 268 | } 269 | // log.Printf("text %q, extent: %v", t, extent) 270 | return canvas.SubImage(image.Rect(0, 0, int(extent.X/256), h*5/4)) 271 | } 272 | 273 | func (ig *ImageGraphics) paint(x, y int, R, G, B uint32, alpha uint32) { 274 | r, g, b, a := ig.Image.At(x, y).RGBA() 275 | r >>= 8 276 | g >>= 8 277 | b >>= 8 278 | a >>= 8 279 | r *= alpha 280 | g *= alpha 281 | b *= alpha 282 | a *= alpha 283 | r += R * (0xff - alpha) 284 | g += G * (0xff - alpha) 285 | b += B * (0xff - alpha) 286 | r >>= 8 287 | g >>= 8 288 | b >>= 8 289 | a >>= 8 290 | ig.Image.Set(x, y, color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}) 291 | } 292 | 293 | func (ig *ImageGraphics) Symbol(x, y int, style chart.Style) { 294 | chart.GenericSymbol(ig, x, y, style) 295 | } 296 | 297 | func (ig *ImageGraphics) Rect(x, y, w, h int, style chart.Style) { 298 | ig.setStyle(style) 299 | stroke := func() { ig.gc.Stroke() } 300 | if style.FillColor != "" { 301 | ig.gc.SetFillColor(chart.Color2RGBA(style.FillColor, 0x0ff /*uint8(style.Alpha*255)*/)) 302 | stroke = func() { ig.gc.FillStroke() } 303 | } 304 | ig.gc.MoveTo(float64(x), float64(y)) 305 | ig.gc.LineTo(float64(x+w), float64(y)) 306 | ig.gc.LineTo(float64(x+w), float64(y+h)) 307 | ig.gc.LineTo(float64(x), float64(y+h)) 308 | ig.gc.LineTo(float64(x), float64(y)) 309 | stroke() 310 | } 311 | 312 | func (ig *ImageGraphics) Wedge(ix, iy, iro, iri int, phi, psi float64, style chart.Style) { 313 | ig.setStyle(style) 314 | stroke := func() { ig.gc.Stroke() } 315 | if style.FillColor != "" { 316 | ig.gc.SetFillColor(chart.Color2RGBA(style.FillColor, 0x0ff /*uint8(style.Alpha*255)*/)) 317 | stroke = func() { ig.gc.FillStroke() } 318 | } 319 | 320 | iri = 0 321 | ecc := 1.0 // eccentricity 322 | x, y := float64(ix), float64(iy) // center as float 323 | ro, ri := float64(iro), float64(iri) // radius outer and inner as float 324 | roe, rie := ro*ecc, ri*ecc // inner and outer radius corrected by ecc 325 | 326 | xao, yao := math.Cos(phi)*roe+x, y+math.Sin(phi)*ro 327 | // xco, yco := math.Cos(psi)*roe+x, y-math.Sin(psi)*ro 328 | xai, yai := math.Cos(phi)*rie+x, y+math.Sin(phi)*ri 329 | xci, yci := math.Cos(psi)*rie+x, y+math.Sin(psi)*ri 330 | 331 | // outbound straight line 332 | if ri > 0 { 333 | ig.gc.MoveTo(xai, yai) 334 | } else { 335 | ig.gc.MoveTo(x, y) 336 | } 337 | ig.gc.LineTo(xao, yao) 338 | 339 | // outer arc 340 | ig.gc.ArcTo(x, y, ro, roe, phi, psi-phi) 341 | 342 | // inbound straight line 343 | if ri > 0 { 344 | ig.gc.LineTo(xci, yci) 345 | ig.gc.ArcTo(x, y, ri, rie, psi, phi-psi) 346 | } else { 347 | ig.gc.LineTo(x, y) 348 | } 349 | stroke() 350 | } 351 | 352 | func (ig *ImageGraphics) Title(text string) { 353 | font := chart.DefaultFont["title"] 354 | _, fh, _ := ig.FontMetrics(font) 355 | x, y := ig.w/2, fh/2 356 | ig.Text(x, y, text, "tc", 0, font) 357 | } 358 | 359 | func (ig *ImageGraphics) XAxis(xr chart.Range, ys, yms int) { 360 | chart.GenericXAxis(ig, xr, ys, yms) 361 | } 362 | func (ig *ImageGraphics) YAxis(yr chart.Range, xs, xms int) { 363 | chart.GenericYAxis(ig, yr, xs, xms) 364 | } 365 | 366 | func (ig *ImageGraphics) Scatter(points []chart.EPoint, plotstyle chart.PlotStyle, style chart.Style) { 367 | chart.GenericScatter(ig, points, plotstyle, style) 368 | } 369 | 370 | func (ig *ImageGraphics) Boxes(boxes []chart.Box, width int, style chart.Style) { 371 | chart.GenericBoxes(ig, boxes, width, style) 372 | } 373 | 374 | func (ig *ImageGraphics) Key(x, y int, key chart.Key) { 375 | chart.GenericKey(ig, x, y, key) 376 | } 377 | 378 | func (ig *ImageGraphics) Bars(bars []chart.Barinfo, style chart.Style) { 379 | chart.GenericBars(ig, bars, style) 380 | } 381 | 382 | func (ig *ImageGraphics) Rings(wedges []chart.Wedgeinfo, x, y, ro, ri int) { 383 | chart.GenericRings(ig, wedges, x, y, ro, ri, 1) 384 | } 385 | 386 | func min(a, b int) int { 387 | if a < b { 388 | return a 389 | } 390 | return b 391 | } 392 | 393 | func max(a, b int) int { 394 | if a > b { 395 | return a 396 | } 397 | return b 398 | } 399 | 400 | func abs(a int) int { 401 | if a < 0 { 402 | return -a 403 | } 404 | return a 405 | } 406 | 407 | func sign(a int) int { 408 | if a < 0 { 409 | return -1 410 | } 411 | if a == 0 { 412 | return 0 413 | } 414 | return 1 415 | } 416 | -------------------------------------------------------------------------------- /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 os governed by Pos which may take the following values: 10 | // otl otc otr 11 | // +-------------+ 12 | // olt |itl itc itr| ort 13 | // | | 14 | // olc |icl icc icr| orc 15 | // | | 16 | // olb |ibl ibc ibr| orb 17 | // +-------------+ 18 | // obl obc obr 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, combi 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 | func (key Key) Layout(bg BasicGraphics, m [][]*KeyEntry) (w, h int, colwidth, rowheight []int) { 137 | fontwidth, fontheight, _ := bg.FontMetrics(DefaultFont["key"]) 138 | cols, rows := len(m), len(m[0]) 139 | 140 | // Find total width and height 141 | totalh := 0 142 | rowheight = make([]int, rows) 143 | for r := 0; r < rows; r++ { 144 | rh := 0 145 | for c := 0; c < cols; c++ { 146 | e := m[c][r] 147 | if e == nil { 148 | continue 149 | } 150 | // fmt.Printf("Layout1 (%d,%d): %s\n", c,r,e.Text) 151 | _, h := textDim(e.Text) 152 | if h > rh { 153 | rh = h 154 | } 155 | } 156 | rowheight[r] = rh 157 | totalh += rh 158 | } 159 | 160 | totalw := 0 161 | colwidth = make([]int, cols) 162 | // fmt.Printf("Making totalw for %d cols\n", cols) 163 | for c := 0; c < cols; c++ { 164 | var rw float32 165 | for r := 0; r < rows; r++ { 166 | e := m[c][r] 167 | if e == nil { 168 | continue 169 | } 170 | // fmt.Printf("Layout2 (%d,%d): %s\n", c,r,e.Text) 171 | 172 | w, _ := textDim(e.Text) 173 | if w > rw { 174 | rw = w 175 | } 176 | } 177 | irw := int(rw + 0.75) 178 | colwidth[c] = irw 179 | totalw += irw 180 | // fmt.Printf("Width of col %d: %d. Total now: %d\n", c, irw, totalw) 181 | } 182 | 183 | if fontwidth == 1 && fontheight == 1 { 184 | // totalw/h are characters only and still in character-units 185 | totalw += int(KeyColSep) * (cols - 1) // add space between columns 186 | totalw += int(2*KeyHorSep + 0.5) // add space for left/right border 187 | totalw += int(KeySymbolWidth+KeySymbolSep+0.5) * cols // place for symbol and symbol-text sep 188 | 189 | totalh += int(KeyRowSep) * (rows - 1) // add space between rows 190 | vsep := KeyVertSep 191 | if vsep < 1 { 192 | vsep = 1 193 | } // make sure there _is_ room (as KeyVertSep < 1) 194 | totalh += int(2 * vsep) // add border at top/bottom 195 | } else { 196 | // totalw/h are characters only and still in character-units 197 | totalw = int(float32(totalw) * fontwidth) // scale to pixels 198 | totalw += int(KeyColSep * (float32(cols-1) * fontwidth)) // add space between columns 199 | totalw += int(2 * KeyHorSep * fontwidth) // add space for left/right border 200 | totalw += int((KeySymbolWidth+KeySymbolSep)*fontwidth) * cols // place for symbol and symbol-text sep 201 | 202 | totalh *= fontheight 203 | totalh += int(KeyRowSep * float32((rows-1)*fontheight)) // add space between rows 204 | vsep := KeyVertSep * float32(fontheight) 205 | if vsep < 1 { 206 | vsep = 1 207 | } // make sure there _is_ room (as KeyVertSep < 1) 208 | totalh += int(2 * vsep) // add border at top/bottom 209 | } 210 | return totalw, totalh, colwidth, rowheight 211 | } 212 | 213 | func GenericKey(bg BasicGraphics, x, y int, key Key) { 214 | m := key.Place() 215 | if len(m) == 0 { 216 | return 217 | } 218 | keyfont := DefaultFont["key"] 219 | fw, fh, _ := bg.FontMetrics(keyfont) 220 | tw, th, cw, rh := key.Layout(bg, m) 221 | style := DefaultStyle["key"] 222 | if key.Border >= 0 { 223 | bg.Rect(x, y, tw, th, style) 224 | } 225 | x += int(KeyHorSep * fw) 226 | vsep := KeyVertSep * float32(fh) 227 | if vsep < 1 { 228 | vsep = 1 229 | } // make sure there _is_ room (as KeyVertSep < 1) 230 | // fmt.Printf("Key: y = %d after %d\n", y, y+int(vsep)+fh/2) 231 | y += int(vsep) + fh/2 232 | for ci, col := range m { 233 | yy := y 234 | 235 | for ri, e := range col { 236 | if e == nil || e.Text == "" { 237 | continue 238 | } 239 | plotStyle := e.PlotStyle 240 | // fmt.Printf("KeyEntry %s: PlotStyle = %d\n", e.Text, e.PlotStyle) 241 | if plotStyle == -1 { 242 | // heading only... 243 | bg.Text(x, yy, e.Text, "cl", 0, keyfont) 244 | } else { 245 | // normal entry 246 | if (plotStyle & PlotStyleLines) != 0 { 247 | bg.Line(x, yy, x+int(KeySymbolWidth*fw), yy, e.Style) 248 | } 249 | if (plotStyle & PlotStylePoints) != 0 { 250 | bg.Symbol(x+int(KeySymbolWidth*fw)/2, yy, e.Style) 251 | } 252 | if (plotStyle & PlotStyleBox) != 0 { 253 | sh := fh / 2 254 | a := x + int(KeySymbolWidth*fw)/2 255 | bg.Rect(a-sh, yy-sh, 2*sh, 2*sh, e.Style) 256 | } 257 | bg.Text(x+int(fw*(KeySymbolWidth+KeySymbolSep)), yy, e.Text, "cl", 0, keyfont) 258 | } 259 | yy += fh*rh[ri] + int(KeyRowSep*float32(fh)) 260 | } 261 | 262 | x += int((KeySymbolWidth + KeySymbolSep + KeyColSep + float32(cw[ci])) * fw) 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /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 | type PieChart struct { 12 | Title string // The title 13 | Key Key // The Key/Legend 14 | Inner float64 // relative radius of inner white are (set to 0.7 to produce ring chart) 15 | Data []CategoryChartData // The data 16 | 17 | FmtVal func(value, sume float64) string // Produce labels 18 | } 19 | 20 | // AbsoluteValue will format value (ignoring sum). It is a convenience function which 21 | // can be assigned to PieChart.FmtVal. 22 | func AbsoluteValue(value, sum float64) (s string) { 23 | fv := math.Abs(value) 24 | switch { 25 | case fv < 0.01: 26 | s = fmt.Sprintf(" %g ", value) 27 | case fv < 0.1: 28 | s = fmt.Sprintf(" %.2f ", value) 29 | case fv < 1: 30 | s = fmt.Sprintf(" %.1f ", value) 31 | case fv < 100000: 32 | s = fmt.Sprintf(" %.0f ", value) 33 | default: 34 | s = fmt.Sprintf(" %g ", value) 35 | } 36 | return 37 | } 38 | 39 | // PercentValue formats value as percentage of sum. It is a convenience function which 40 | // can be assigned to PieChart.FmtVal. 41 | func PercentValue(value, sum float64) (s string) { 42 | value *= 100 / sum 43 | s = AbsoluteValue(value, sum) + "% " 44 | return 45 | } 46 | 47 | type CategoryChartData struct { 48 | Name string 49 | Style []Style 50 | Samples []CatValue 51 | } 52 | 53 | func (c *PieChart) AddData(name string, data []CatValue, style []Style) { 54 | if len(style) < len(data) { 55 | ns := make([]Style, len(data)) 56 | copy(style, ns) 57 | for i := len(style); i < len(data); i++ { 58 | ns[i] = AutoStyle(i-len(style), true) 59 | } 60 | style = ns 61 | } 62 | c.Data = append(c.Data, CategoryChartData{name, style, data}) 63 | c.Key.Entries = append(c.Key.Entries, KeyEntry{PlotStyle: -1, Text: name}) 64 | for s, cv := range data { 65 | c.Key.Entries = append(c.Key.Entries, KeyEntry{PlotStyle: PlotStyleBox, Style: style[s], Text: cv.Cat}) 66 | } 67 | } 68 | 69 | func (c *PieChart) AddDataPair(name string, cat []string, val []float64) { 70 | n := imin(len(cat), len(val)) 71 | data := make([]CatValue, n) 72 | for i := 0; i < n; i++ { 73 | data[i].Cat, data[i].Val = cat[i], val[i] 74 | } 75 | c.AddData(name, data, nil) 76 | } 77 | 78 | var PieChartShrinkage = 0.66 // Scaling factor of radius of next data set. 79 | var PieChartHighlight = 0.15 // How much are flaged segments offset. 80 | 81 | // Reset chart to state before plotting. 82 | func (c *PieChart) Reset() {} 83 | 84 | // Plot outputs the scatter chart sc to g. 85 | func (c *PieChart) Plot(g Graphics) { 86 | layout := layout(g, c.Title, "", "", true, true, &c.Key) 87 | 88 | width, height := layout.Width, layout.Height 89 | topm, leftm := layout.Top, layout.Left 90 | width += 0 91 | 92 | r := imin(height, width) / 2 93 | x0, y0 := leftm+r, topm+r 94 | 95 | // Make sure pie fits into plotting area 96 | rshift := int(float64(r) * PieChartHighlight) 97 | if rshift < 6 { 98 | rshift = 6 99 | } 100 | for _, d := range c.Data[0].Samples { 101 | if d.Flag { 102 | // debug.Printf("Reduced %d by %d", r, rshift) 103 | r -= rshift / 3 104 | break 105 | } 106 | } 107 | 108 | g.Begin() 109 | 110 | if c.Title != "" { 111 | g.Title(c.Title) 112 | } 113 | 114 | for _, data := range c.Data { 115 | 116 | var sum float64 117 | for _, d := range data.Samples { 118 | sum += d.Val 119 | } 120 | 121 | wedges := make([]Wedgeinfo, len(data.Samples)) 122 | var ri int = 0 123 | if c.Inner > 0 { 124 | ri = int(float64(r) * c.Inner) 125 | } 126 | 127 | var phi float64 = -math.Pi 128 | for j, d := range data.Samples { 129 | style := data.Style[j] 130 | alpha := 2 * math.Pi * d.Val / sum 131 | shift := 0 132 | 133 | var t string 134 | if c.FmtVal != nil { 135 | t = c.FmtVal(d.Val, sum) 136 | } 137 | if d.Flag { 138 | shift = rshift 139 | } 140 | 141 | wedges[j] = Wedgeinfo{Phi: phi, Psi: phi + alpha, Text: t, Tp: "c", 142 | Style: style, Font: Font{}, Shift: shift} 143 | 144 | phi += alpha 145 | } 146 | g.Rings(wedges, x0, y0, r, ri) 147 | 148 | r = int(float64(r) * PieChartShrinkage) 149 | } 150 | 151 | if !c.Key.Hide { 152 | g.Key(layout.KeyX, layout.KeyY, c.Key) 153 | } 154 | 155 | g.End() 156 | } 157 | -------------------------------------------------------------------------------- /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 | Data []ScatterChartData // The actual data (filled with Add...-methods) 13 | NSamples int // number of samples for function plots 14 | } 15 | 16 | // ScatterChartData encapsulates a data set or function in a scatter chart. 17 | // Not both Samples and Func may be non nil at the same time. 18 | type ScatterChartData struct { 19 | Name string // The name of this data set. TODO: unused? 20 | PlotStyle PlotStyle // Points, Lines+Points or Lines only 21 | Style Style // Color, sizes, pointtype, linestyle, ... 22 | Samples []EPoint // The actual points for scatter/lines charts 23 | Func func(float64) float64 // The function to draw. 24 | } 25 | 26 | // AddFunc adds a function f to this chart. A key/legend entry is produced 27 | // if name is not empty. 28 | func (c *ScatterChart) AddFunc(name string, f func(float64) float64, plotstyle PlotStyle, style Style) { 29 | if plotstyle.undefined() { 30 | plotstyle = PlotStyleLines 31 | } 32 | if style.empty() { 33 | style = AutoStyle(len(c.Data), false) 34 | } 35 | 36 | scd := ScatterChartData{Name: name, PlotStyle: plotstyle, Style: style, Samples: nil, Func: f} 37 | c.Data = append(c.Data, scd) 38 | if name != "" { 39 | ke := KeyEntry{Text: name, PlotStyle: plotstyle, Style: style} 40 | c.Key.Entries = append(c.Key.Entries, ke) 41 | } 42 | } 43 | 44 | // AddData adds points in data to chart. A key/legend entry is produced 45 | // if name is not empty. 46 | func (c *ScatterChart) AddData(name string, data []EPoint, plotstyle PlotStyle, style Style) { 47 | 48 | // Update styles if non given 49 | if plotstyle.undefined() { 50 | plotstyle = PlotStylePoints 51 | } 52 | if style.empty() { 53 | style = AutoStyle(len(c.Data), false) 54 | } 55 | // Fix missing values in style 56 | if (plotstyle & PlotStyleLines) != 0 { 57 | if style.LineWidth <= 0 { 58 | style.LineWidth = 1 59 | } 60 | if style.LineColor == "" { 61 | style.LineColor = style.SymbolColor 62 | } 63 | } 64 | if (plotstyle&PlotStylePoints) != 0 && style.Symbol == 0 { 65 | style.Symbol = '#' 66 | } 67 | 68 | // Init axis 69 | if len(c.Data) == 0 { 70 | c.XRange.init() 71 | c.YRange.init() 72 | } 73 | 74 | // Add data 75 | scd := ScatterChartData{Name: name, PlotStyle: plotstyle, Style: style, Samples: data, Func: nil} 76 | c.Data = append(c.Data, scd) 77 | 78 | // Autoscale 79 | for _, d := range data { 80 | xl, yl, xh, yh := d.BoundingBox() 81 | c.XRange.autoscale(xl) 82 | c.XRange.autoscale(xh) 83 | c.YRange.autoscale(yl) 84 | c.YRange.autoscale(yh) 85 | } 86 | 87 | // Add key/legend entry 88 | if name != "" { 89 | ke := KeyEntry{Style: style, PlotStyle: plotstyle, Text: name} 90 | c.Key.Entries = append(c.Key.Entries, ke) 91 | } 92 | } 93 | 94 | // AddDataGeneric is the generiv version of AddData which allows any type 95 | // to be plotted that implements the XYErrValue interface. 96 | func (c *ScatterChart) AddDataGeneric(name string, data []XYErrValue, plotstyle PlotStyle, style Style) { 97 | edata := make([]EPoint, len(data)) 98 | for i, d := range data { 99 | x, y := d.XVal(), d.YVal() 100 | xl, xh := d.XErr() 101 | yl, yh := d.YErr() 102 | dx, dy := xh-xl, yh-yl 103 | xo, yo := xh-dx/2-x, yh-dy/2-y 104 | edata[i] = EPoint{X: x, Y: y, DeltaX: dx, DeltaY: dy, OffX: xo, OffY: yo} 105 | } 106 | c.AddData(name, edata, plotstyle, style) 107 | } 108 | 109 | // AddDataPair is a convenience method which wrapps around AddData: It adds the points 110 | // (x[n],y[n]) to the chart. 111 | func (c *ScatterChart) AddDataPair(name string, x, y []float64, plotstyle PlotStyle, style Style) { 112 | n := imin(len(x), len(y)) 113 | data := make([]EPoint, n) 114 | nan := math.NaN() 115 | for i := 0; i < n; i++ { 116 | data[i] = EPoint{X: x[i], Y: y[i], DeltaX: nan, DeltaY: nan} 117 | } 118 | c.AddData(name, data, plotstyle, style) 119 | } 120 | 121 | // Reset chart to state before plotting. 122 | func (c *ScatterChart) Reset() { 123 | c.XRange.Reset() 124 | c.YRange.Reset() 125 | } 126 | 127 | // Plot outputs the scatter chart to the graphic output g. 128 | func (c *ScatterChart) Plot(g Graphics) { 129 | layout := layout(g, c.Title, c.XRange.Label, c.YRange.Label, 130 | c.XRange.TicSetting.Hide, c.YRange.TicSetting.Hide, &c.Key) 131 | 132 | width, height := layout.Width, layout.Height 133 | topm, leftm := layout.Top, layout.Left 134 | numxtics, numytics := layout.NumXtics, layout.NumYtics 135 | 136 | // fmt.Printf("\nSet up of X-Range (%d)\n", numxtics) 137 | c.XRange.Setup(numxtics, numxtics+2, width, leftm, false) 138 | // fmt.Printf("\nSet up of Y-Range (%d)\n", numytics) 139 | c.YRange.Setup(numytics, numytics+2, height, topm, true) 140 | 141 | g.Begin() 142 | 143 | if c.Title != "" { 144 | g.Title(c.Title) 145 | } 146 | 147 | g.XAxis(c.XRange, topm+height, topm) 148 | g.YAxis(c.YRange, leftm, leftm+width) 149 | 150 | // Plot Data 151 | xf, yf := c.XRange.Data2Screen, c.YRange.Data2Screen 152 | xmin, xmax := c.XRange.Min, c.XRange.Max 153 | ymin, ymax := c.YRange.Min, c.YRange.Max 154 | spf := screenPointFunc(xf, yf, xmin, xmax, ymin, ymax) 155 | 156 | for i, data := range c.Data { 157 | style := data.Style 158 | if data.Samples != nil { 159 | // Samples 160 | points := make([]EPoint, 0, len(data.Samples)) 161 | for _, d := range data.Samples { 162 | if d.X < xmin || d.X > xmax || d.Y < ymin || d.Y > ymax { 163 | continue 164 | } 165 | p := spf(d) 166 | points = append(points, p) 167 | } 168 | g.Scatter(points, data.PlotStyle, style) 169 | } else if data.Func != nil { 170 | c.drawFunction(g, i) 171 | } 172 | } 173 | 174 | if !c.Key.Hide { 175 | g.Key(layout.KeyX, layout.KeyY, c.Key) 176 | } 177 | 178 | g.End() 179 | } 180 | 181 | // Output function (ih in Data) 182 | func (c *ScatterChart) drawFunction(g Graphics, i int) { 183 | function := c.Data[i].Func 184 | style := c.Data[i].Style 185 | plotstyle := c.Data[i].PlotStyle 186 | 187 | yf := c.YRange.Data2Screen 188 | symax, symin := float64(yf(c.YRange.Min)), float64(yf(c.YRange.Max)) // y limits in screen coords 189 | sxmin, sxmax := c.XRange.Data2Screen(c.XRange.Min), c.XRange.Data2Screen(c.XRange.Max) 190 | width := sxmax - sxmin 191 | if c.NSamples == 0 { 192 | step := 6 193 | if width < 70 { 194 | step = 3 195 | } 196 | if width < 50 { 197 | step = 2 198 | } 199 | if width < 30 { 200 | step = 1 201 | } 202 | c.NSamples = width / step 203 | } 204 | step := width / c.NSamples 205 | if step < 1 { 206 | step = 1 207 | } 208 | pcap := width/step + 2 209 | points := make([]EPoint, 0, pcap) 210 | var lastP *EPoint = nil // screen coordinates of last point (nil if no point) 211 | var lastIn bool = false // was last point in valid yrange? (undef if lastP==nil) 212 | 213 | for six := sxmin; six < sxmax; six += step { 214 | x := c.XRange.Screen2Data(six) 215 | sx := float64(six) 216 | y := function(x) 217 | 218 | // Handle NaN and +/- Inf 219 | if math.IsNaN(y) { 220 | g.Scatter(points, plotstyle, style) 221 | points = points[0:0] 222 | lastP = nil 223 | continue 224 | } 225 | 226 | sy := float64(yf(y)) 227 | 228 | if sy >= symin && sy <= symax { 229 | p := EPoint{X: sx, Y: sy} 230 | if lastP != nil && !lastIn { 231 | pc := c.clipPoint(p, *lastP, symin, symax) 232 | // fmt.Printf("Added front clip point %v\n", pc) 233 | points = append(points, pc) 234 | } 235 | // fmt.Printf("Added point %v\n", p) 236 | points = append(points, p) 237 | lastIn = true 238 | } else { 239 | if lastP == nil { 240 | lastP = &EPoint{X: sx, Y: sy} 241 | continue 242 | } 243 | if lastIn { 244 | pc := c.clipPoint(*lastP, EPoint{X: sx, Y: sy}, symin, symax) 245 | points = append(points, pc) 246 | g.Scatter(points, plotstyle, style) 247 | // fmt.Printf("Added clip point %v and drawing\n", pc) 248 | points = points[0:0] 249 | lastIn = false 250 | } else if (lastP.Y < symin && sy > symax) || (lastP.Y > symax && sy < symin) { 251 | p2 := c.clip2Point(*lastP, EPoint{X: sx, Y: sy}, symin, symax) 252 | // fmt.Printf("Added 2clip points %v / %v and drawing\n", p2[0], p2[1]) 253 | g.Scatter(p2, plotstyle, style) 254 | } 255 | 256 | } 257 | 258 | lastP = &EPoint{X: sx, Y: sy} 259 | } 260 | g.Scatter(points, plotstyle, style) 261 | } 262 | 263 | // Point in is in valid y range, out is out. Return p which clips the line from in to out to valid y range 264 | func (c *ScatterChart) clipPoint(in, out EPoint, min, max float64) (p EPoint) { 265 | // fmt.Printf("clipPoint: in (%g,%g), out(%g,%g) min/max=%g/%g\n", in.X, in.Y, out.X, out.Y, min, max) 266 | dx, dy := in.X-out.X, in.Y-out.Y 267 | 268 | var y float64 269 | if out.Y <= min { 270 | y = min 271 | } else { 272 | y = max 273 | } 274 | x := in.X + dx*(y-in.Y)/dy 275 | p.X, p.Y = x, y 276 | p.DeltaX, p.DeltaY = math.NaN(), math.NaN() 277 | return 278 | } 279 | 280 | // Clip line from a to b (both outside min/max range) 281 | func (c *ScatterChart) clip2Point(a, b EPoint, min, max float64) []EPoint { 282 | if a.Y > b.Y { 283 | a, b = b, a 284 | } 285 | dx, dy := b.X-a.X, b.Y-a.Y 286 | s := dx / dy 287 | 288 | pc := make([]EPoint, 2) 289 | 290 | pc[0].X = a.X + s*(min-a.Y) 291 | pc[0].Y = min 292 | pc[0].DeltaX, pc[0].DeltaY = math.NaN(), math.NaN() 293 | pc[1].X = a.X + s*(max-a.Y) 294 | pc[1].Y = max 295 | pc[1].DeltaX, pc[1].DeltaY = math.NaN(), math.NaN() 296 | return pc 297 | } 298 | 299 | // Set up function which handles mappig data->screen coordinates and does 300 | // proper clipping on the error bars. 301 | func screenPointFunc(xf, yf func(float64) int, xmin, xmax, ymin, ymax float64) (spf func(EPoint) EPoint) { 302 | spf = func(d EPoint) (p EPoint) { 303 | xl, yl, xh, yh := d.BoundingBox() 304 | // fmt.Printf("OrigBB: %.1f %.1f %.1f %.1f (%.1f,%.1f)\n", xl,yl,xh,yh,d.X,d.Y) 305 | if xl < xmin { 306 | xl = xmin 307 | } 308 | if xh > xmax { 309 | xh = xmax 310 | } 311 | if yl < ymin { 312 | yl = ymin 313 | } 314 | if yh > ymax { 315 | yh = ymax 316 | } 317 | // fmt.Printf("ClippedBB: %.1f %.1f %.1f %.1f\n", xl,yl,xh,yh) 318 | 319 | x := float64(xf(d.X)) 320 | y := float64(yf(d.Y)) 321 | xsl, xsh := float64(xf(xl)), float64(xf(xh)) 322 | ysl, ysh := float64(yf(yl)), float64(yf(yh)) 323 | // fmt.Printf("ScreenBB: %.0f %.0f %.0f %.0f (%.0f,%.0f)\n", xsl,ysl,xsh,ysh,x,y) 324 | 325 | dx, dy := math.NaN(), math.NaN() 326 | var xo, yo float64 327 | 328 | if xsl != xsh { 329 | dx = math.Abs(xsh - xsl) 330 | xo = xsl - x + dx/2 331 | } 332 | if ysl != ysh { 333 | dy = math.Abs(ysh - ysl) 334 | yo = ysh - y + dy/2 335 | } 336 | // fmt.Printf(" >> dx=%.0f dy=%.0f xo=%.0f yo=%.0f\n", dx,dy,xo,yo) 337 | 338 | p = EPoint{X: x, Y: y, DeltaX: dx, DeltaY: dy, OffX: xo, OffY: yo} 339 | return 340 | 341 | /************************** 342 | if xl < xmin { // happens only if d.Delta!=0,NaN 343 | a := xmin - xl 344 | d.DeltaX -= a 345 | d.OffX += a / 2 346 | } 347 | if xh > xmax { 348 | a := xh - xmax 349 | d.DeltaX -= a 350 | d.OffX -= a / 2 351 | } 352 | if yl < ymin { // happens only if d.Delta!=0,NaN 353 | a := ymin - yl 354 | d.DeltaY -= a 355 | d.OffY += a / 2 356 | } 357 | if yh > ymax { 358 | a := yh - ymax 359 | d.DeltaY -= a 360 | d.OffY -= a / 2 361 | } 362 | 363 | x := xf(d.X) 364 | y := yf(d.Y) 365 | dx, dy := math.NaN(), math.NaN() 366 | var xo, yo float64 367 | if !math.IsNaN(d.DeltaX) { 368 | dx = float64(xf(d.DeltaX) - xf(0)) // TODO: abs? 369 | xo = float64(xf(d.OffX) - xf(0)) 370 | } 371 | if !math.IsNaN(d.DeltaY) { 372 | dy = float64(yf(d.DeltaY) - yf(0)) // TODO: abs? 373 | yo = float64(yf(d.OffY) - yf(0)) 374 | } 375 | // fmt.Printf("Point %d: %f\n", i, dx) 376 | p = EPoint{X: float64(x), Y: float64(y), DeltaX: dx, DeltaY: dy, OffX: xo, OffY: yo} 377 | return 378 | *********************/ 379 | } 380 | return 381 | } 382 | -------------------------------------------------------------------------------- /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.YRange.TicSetting.Hide, &sc.Key) 65 | 66 | _, height := layout.Width, layout.Height 67 | topm, _ := layout.Top, layout.Left 68 | _, numytics := layout.NumXtics, layout.NumYtics 69 | 70 | sc.YRange.Setup(numytics, numytics+1, height, topm, true) 71 | 72 | // amplitude of jitter: not too smal to be visible and useful, not to 73 | // big to be ugly or even overlapp other 74 | 75 | null := sc.YRange.Screen2Data(0) 76 | absmin := 1.4 * math.Abs(sc.YRange.Screen2Data(1)-null) // would be one pixel 77 | tenpc := math.Abs(sc.YRange.Screen2Data(height)-null) / 10 // 10 percent of graph area 78 | smplcnt := len(sc.ScatterChart.Data) + 1 // as samples are borders 79 | noverlp := math.Abs(sc.YRange.Screen2Data(height/smplcnt) - null) // do not overlapp other sample 80 | 81 | yj := noverlp 82 | if tenpc < yj { 83 | yj = tenpc 84 | } 85 | if yj < absmin { 86 | yj = absmin 87 | } 88 | 89 | // yjs := sc.YRange.Data2Screen(yj) - sc.YRange.Data2Screen(0) 90 | // fmt.Printf("yj = %.2f : in screen = %d\n", yj, yjs) 91 | for _, data := range sc.ScatterChart.Data { 92 | if data.Samples == nil { 93 | continue // should not happen 94 | } 95 | for i := range data.Samples { 96 | shift := yj * rand.NormFloat64() * yj 97 | data.Samples[i].Y += shift 98 | } 99 | } 100 | } 101 | sc.ScatterChart.Plot(g) 102 | 103 | if sc.Jitter { 104 | // Revert Jitter 105 | for s, data := range sc.ScatterChart.Data { 106 | if data.Samples == nil { 107 | continue // should not happen 108 | } 109 | for i, _ := range data.Samples { 110 | data.Samples[i].Y = float64(s + 1) 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /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{'o', // empty circle 11 | '=', // empty square 12 | '%', // empty triangle up 13 | '&', // empty diamond 14 | '+', // plus 15 | 'X', // cross 16 | '*', // star 17 | '0', // bulls eys 18 | '@', // filled circle 19 | '#', // filled square 20 | 'A', // filled tringale up 21 | 'Z', // filled diamond 22 | '.', // tiny dot 23 | } 24 | 25 | // SymbolIndex returns the index of the symbol s in Symbol or -1 if not found. 26 | func SymbolIndex(s int) (idx int) { 27 | for idx = 0; idx < len(Symbol); idx++ { 28 | if Symbol[idx] == s { 29 | return idx 30 | } 31 | } 32 | return -1 33 | } 34 | 35 | // NextSymbol returns the next symbol of s: Either in the global list Symbol 36 | // or (if not found there) the next character. 37 | func NextSymbol(s int) int { 38 | if idx := SymbolIndex(s); idx != -1 { 39 | return Symbol[(idx+1)%len(Symbol)] 40 | } 41 | return s + 1 42 | } 43 | 44 | // CharacterWidth is a table of the (relative) width of common runes. 45 | 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, 46 | '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, 47 | '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, 48 | '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, 49 | '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, 50 | 'U': 22.0, 'V': 20.2, 'W': 29.0, 'X': 20.2, 'Y': 20.2, 'Z': 18.8, ' ': 8.5, 51 | '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, 52 | '.': 8.2, ',': 8.2, ':': 8.2, ';': 8.2, '+': 17.9, '"': 11.0, '*': 11.8, '%': 27.0, '&': 20.2, '/': 8.4, 53 | '(': 10.2, ')': 10.2, '=': 18.0, '?': 16.8, '!': 8.5, '[': 8.2, ']': 8.2, '{': 10.2, '}': 10.2, '$': 16.8, 54 | '<': 18.0, '>': 18.0, '§': 16.8, '°': 12.2, '^': 14.2, '~': 18.0, 55 | } 56 | var averageCharacterWidth float32 57 | 58 | func init() { 59 | n := 0 60 | for _, w := range CharacterWidth { 61 | averageCharacterWidth += w 62 | n++ 63 | } 64 | averageCharacterWidth /= float32(n) 65 | averageCharacterWidth = 15 66 | } 67 | 68 | // Unsude? 69 | var Palette = map[string]string{"title": "#aa9933", "label": "#000000", "axis": "#000000", 70 | "ticlabel": "#000000", "grid": "#c0c0c0", "keyborder": "#000000", "errorbar": "*0.3", 71 | } 72 | 73 | // Style contains all information about all graphic elements in a chart. 74 | // All colors are in the form "#rrggbb" with rr/gg/bb hexvalues. 75 | type Style struct { 76 | Symbol int // 0: no symbol; any codepoint: this symbol 77 | SymbolColor string // Color of symbol 78 | SymbolSize float64 // Scaling factor of symbol 79 | LineStyle int // SolidLine, DashedLine, DottedLine, .... see below 80 | LineColor string // 81 | LineWidth int // 0: no line 82 | FillColor string // "": no fill 83 | Alpha float64 // Alpha of whole stuff. 84 | } 85 | 86 | // PlotStyle describes how data and functions are drawn in scatter plots. 87 | // Can be used to describe how a key entry is drawn 88 | type PlotStyle int 89 | 90 | const ( 91 | PlotStylePoints = 1 92 | PlotStyleLines = 2 93 | PlotStyleLinesPoints = 3 94 | PlotStyleBox = 4 95 | ) 96 | 97 | func (ps PlotStyle) undefined() bool { 98 | return int(ps) < 1 || int(ps) > 3 99 | } 100 | 101 | // The supported line styles 102 | const ( 103 | SolidLine = iota 104 | DashedLine 105 | DottedLine 106 | DashDotDotLine 107 | LongDashLine 108 | LongDotLine 109 | ) 110 | 111 | // Font describes a font 112 | type Font struct { 113 | Name string // "": default 114 | Size int // Relative size of font to default in output graphics: 115 | // -2: tiny, -1: small, 0: normal, 1: large, 2: huge 116 | Color string // "": default, other: use this 117 | } 118 | 119 | func (d *Style) empty() bool { 120 | return d.Symbol == 0 && d.SymbolColor == "" && d.LineStyle == 0 && d.LineColor == "" && d.FillColor == "" && d.SymbolSize == 0 121 | } 122 | 123 | // Standard colors used by AutoStyle 124 | var StandardColors = []string{"#cc0000", "#00bb00", "#0000dd", "#996600", "#bb00bb", "#00aaaa", "#aaaa00"} 125 | 126 | // Standard line styles used by AutoStyle (fill=false) 127 | var StandardLineStyles = []int{SolidLine, DashedLine, DottedLine, LongDashLine, LongDotLine} 128 | 129 | // Standard symbols used by AutoStyle 130 | var StandardSymbols = []int{'o', '=', '%', '&', '+', 'X', '*', '@', '#', 'A', 'Z'} 131 | 132 | // How much brighter/darker filled elements become. 133 | var StandardFillFactor = 0.5 134 | 135 | // AutoStyle produces a styles based on StandardColors, StandardLineStyles, and StandardSymbols. 136 | // Call with fill = true for charts with filled elements (hist, bar, cbar, pie). 137 | func AutoStyle(i int, fill bool) (style Style) { 138 | nc, nl, ns := len(StandardColors), len(StandardLineStyles), len(StandardSymbols) 139 | 140 | si := i % ns 141 | ci := i % nc 142 | li := i % nl 143 | 144 | style.Symbol = StandardSymbols[si] 145 | style.SymbolColor = StandardColors[ci] 146 | style.LineColor = StandardColors[ci] 147 | style.SymbolSize = 1 148 | style.Alpha = 0 149 | 150 | if fill { 151 | style.LineStyle = SolidLine 152 | style.LineWidth = 3 153 | if i < nc { 154 | style.FillColor = lighter(style.LineColor, StandardFillFactor) 155 | } else if i <= 2*nc { 156 | style.FillColor = darker(style.LineColor, StandardFillFactor) 157 | } else { 158 | style.FillColor = style.LineColor 159 | } 160 | } else { 161 | style.LineStyle = StandardLineStyles[li] 162 | style.LineWidth = 1 163 | } 164 | return 165 | } 166 | 167 | // DefaultStyle maps chart elements to styles. 168 | var DefaultStyle = map[string]Style{ 169 | "axis": Style{LineColor: "#000000", LineWidth: 2, LineStyle: SolidLine}, // axis 170 | "maxis": Style{LineColor: "#000000", LineWidth: 2, LineStyle: SolidLine}, // mirrored axis 171 | "tic": Style{LineColor: "#000000", LineWidth: 1, LineStyle: SolidLine}, 172 | "mtic": Style{LineColor: "#000000", LineWidth: 1, LineStyle: SolidLine}, 173 | "zero": Style{LineColor: "#404040", LineWidth: 1, LineStyle: SolidLine}, 174 | "gridl": Style{LineColor: "#808080", LineWidth: 1, LineStyle: SolidLine}, 175 | "gridb": Style{LineColor: "#e6fcfc", LineWidth: 0, FillColor: "#e6fcfc"}, 176 | "key": Style{LineColor: "#202020", LineWidth: 1, LineStyle: SolidLine, FillColor: "#f0f0f0", Alpha: 0.5}, 177 | "title": Style{LineColor: "#000000", LineWidth: 1, LineStyle: SolidLine, FillColor: "#ecc750", Alpha: 0}, 178 | } 179 | 180 | // DefaultFont maps chart elements to fonts. 181 | var DefaultFont = map[string]Font{ 182 | "title": Font{Size: +1}, 183 | "label": Font{}, 184 | "key": Font{Size: -1}, 185 | "tic": Font{}, 186 | "rangelimit": Font{Size: -1}, 187 | } 188 | 189 | func hsv2rgb(h, s, v int) (r, g, b int) { 190 | H := int(math.Floor(float64(h) / 60)) 191 | S, V := float64(s)/100, float64(v)/100 192 | f := float64(h)/60 - float64(H) 193 | p := V * (1 - S) 194 | q := V * (1 - S*f) 195 | t := V * (1 - S*(1-f)) 196 | 197 | switch H { 198 | case 0, 6: 199 | r, g, b = int(255*V), int(255*t), int(255*p) 200 | case 1: 201 | r, g, b = int(255*q), int(255*V), int(255*p) 202 | case 2: 203 | r, g, b = int(255*p), int(255*V), int(255*t) 204 | case 3: 205 | r, g, b = int(255*p), int(255*q), int(255*V) 206 | case 4: 207 | r, g, b = int(255*t), int(255*p), int(255*V) 208 | case 5: 209 | r, g, b = int(255*V), int(255*p), int(255*q) 210 | default: 211 | panic(fmt.Sprintf("Ooops: Strange H value %d in hsv2rgb(%d,%d,%d).", H, h, s, v)) 212 | } 213 | 214 | return 215 | } 216 | 217 | func f3max(a, b, c float64) float64 { 218 | switch true { 219 | case a > b && a >= c: 220 | return a 221 | case b > c && b >= a: 222 | return b 223 | case c > a && c >= b: 224 | return c 225 | } 226 | return a 227 | } 228 | 229 | func f3min(a, b, c float64) float64 { 230 | switch true { 231 | case a < b && a <= c: 232 | return a 233 | case b < c && b <= a: 234 | return b 235 | case c < a && c <= b: 236 | return c 237 | } 238 | return a 239 | } 240 | 241 | func rgb2hsv(r, g, b int) (h, s, v int) { 242 | R, G, B := float64(r)/255, float64(g)/255, float64(b)/255 243 | 244 | if R == G && G == B { 245 | h, s = 0, 0 246 | v = int(r * 255) 247 | } else { 248 | max, min := f3max(R, G, B), f3min(R, G, B) 249 | if max == R { 250 | h = int(60 * (G - B) / (max - min)) 251 | } else if max == G { 252 | h = int(60 * (2 + (B-R)/(max-min))) 253 | } else { 254 | h = int(60 * (4 + (R-G)/(max-min))) 255 | } 256 | if max == 0 { 257 | s = 0 258 | } else { 259 | s = int(100 * (max - min) / max) 260 | } 261 | v = int(100 * max) 262 | } 263 | if h < 0 { 264 | h += 360 265 | } 266 | return 267 | } 268 | 269 | func Color2rgb(color string) (r, g, b int) { 270 | if color[0] == '#' { 271 | color = color[1:] 272 | } 273 | n, err := fmt.Sscanf(color, "%2x%2x%2x", &r, &g, &b) 274 | if n != 3 || err != nil { 275 | r, g, b = 127, 127, 127 276 | } 277 | // fmt.Printf("%s --> %d %d %d\n", color,r,g,b) 278 | return 279 | } 280 | 281 | func Color2RGBA(col string, a uint8) color.RGBA { 282 | r, g, b := Color2rgb(col) 283 | return color.RGBA{uint8(r), uint8(g), uint8(b), a} 284 | } 285 | 286 | func lighter(color string, f float64) string { 287 | r, g, b := Color2rgb(color) 288 | h, s, v := rgb2hsv(r, g, b) 289 | f = 1 - f 290 | s = int(float64(s) * f) 291 | v += int((100 - float64(v)) * f) 292 | if v > 100 { 293 | v = 100 294 | } 295 | r, g, b = hsv2rgb(h, s, v) 296 | 297 | return fmt.Sprintf("#%02x%02x%02x", r, g, b) 298 | } 299 | 300 | func darker(color string, f float64) string { 301 | r, g, b := Color2rgb(color) 302 | h, s, v := rgb2hsv(r, g, b) 303 | f = 1 - f 304 | v = int(float64(v) * f) 305 | s += int((100 - float64(s)) * f) 306 | if s > 100 { 307 | s = 100 308 | } 309 | r, g, b = hsv2rgb(h, s, v) 310 | 311 | return fmt.Sprintf("#%02x%02x%02x", r, g, b) 312 | } 313 | -------------------------------------------------------------------------------- /style_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestRgb2Hsv(t *testing.T) { 9 | type rgbhsv struct{ r, g, b, h, s, v int } 10 | r2h := []rgbhsv{{255, 0, 0, 0, 100, 100}, {0, 128, 0, 120, 100, 50}, {255, 255, 0, 60, 100, 100}, 11 | {255, 0, 255, 300, 100, 100}} 12 | for _, x := range r2h { 13 | h, s, v := rgb2hsv(x.r, x.g, x.b) 14 | if h != x.h || s != x.s || v != x.v { 15 | 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) 16 | } 17 | } 18 | } 19 | 20 | func TestBrighten(t *testing.T) { 21 | for _, col := range []string{"#ff0000", "#00ff00", "#0000ff"} { 22 | for _, f := range []float64{0.1, 0.3, 0.5, 0.7, 0.9} { 23 | fmt.Printf("%s --- %.2f --> %s %s\n", col, f, lighter(col, f), darker(col, f)) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /svgg/svg.go: -------------------------------------------------------------------------------- 1 | package svgg 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ajstarks/svgo" 6 | "github.com/vdobler/chart" 7 | "image/color" 8 | "math" 9 | ) 10 | 11 | // SvgGraphics implements BasicGraphics and uses the generic implementations 12 | type SvgGraphics struct { 13 | svg *svg.SVG 14 | w, h int 15 | font string 16 | fs int 17 | bg color.RGBA 18 | tx, ty int 19 | } 20 | 21 | // New creates a new SvgGraphics of dimension w x h, with a default font font of size fontsize. 22 | func New(sp *svg.SVG, width, height int, font string, fontsize int, background color.RGBA) *SvgGraphics { 23 | if font == "" { 24 | font = "Helvetica" 25 | } 26 | if fontsize == 0 { 27 | fontsize = 12 28 | } 29 | s := SvgGraphics{svg: sp, w: width, h: height, font: font, fs: fontsize, bg: background} 30 | return &s 31 | } 32 | 33 | // AddTo returns a new ImageGraphics which will write to (width x height) sized 34 | // area starting at (x,y) on the provided SVG 35 | func AddTo(sp *svg.SVG, x, y, width, height int, font string, fontsize int, background color.RGBA) *SvgGraphics { 36 | s := New(sp, width, height, font, fontsize, background) 37 | s.tx, s.ty = x, y 38 | return s 39 | } 40 | 41 | func (sg *SvgGraphics) Begin() { 42 | font, fs := sg.font, sg.fs 43 | if font == "" { 44 | font = "Helvetica" 45 | } 46 | if fs == 0 { 47 | fs = 12 48 | } 49 | sg.svg.Gstyle(fmt.Sprintf("stroke:#000000; stroke-width:1; font-family: %s; font-size: %d; opacity: 1; fill-opacity: 1", 50 | font, fs)) 51 | if sg.tx != 0 || sg.ty != 0 { 52 | sg.svg.Gtransform(fmt.Sprintf("translate(%d %d)", sg.tx, sg.ty)) 53 | } 54 | 55 | bgc := fmt.Sprintf("#%02x%02x%02x", sg.bg.R>>8, sg.bg.G>>8, sg.bg.B>>8) 56 | opa := fmt.Sprintf("%.4f", float64(sg.bg.A>>8)/255) 57 | bgs := fmt.Sprintf("stroke: %s; opacity: %s; fill: %s; fill-opacity: %s", bgc, opa, bgc, opa) 58 | sg.svg.Rect(0, 0, sg.w, sg.h, bgs) 59 | } 60 | 61 | func (sg *SvgGraphics) End() { 62 | sg.svg.Gend() 63 | if sg.tx != 0 || sg.ty != 0 { 64 | sg.svg.Gend() 65 | } 66 | } 67 | 68 | func (sg *SvgGraphics) Background() (r, g, b, a uint8) { 69 | return uint8(sg.bg.R >> 8), uint8(sg.bg.G >> 8), uint8(sg.bg.B >> 8), uint8(sg.bg.A >> 8) 70 | } 71 | 72 | func (sg *SvgGraphics) Dimensions() (int, int) { 73 | return sg.w, sg.h 74 | } 75 | 76 | func (sg *SvgGraphics) fontheight(font chart.Font) (fh int) { 77 | if sg.fs <= 14 { 78 | fh = sg.fs + font.Size 79 | } else if sg.fs <= 20 { 80 | fh = sg.fs + 2*font.Size 81 | } else { 82 | fh = sg.fs + 3*font.Size 83 | } 84 | 85 | if fh == 0 { 86 | fh = 12 87 | } 88 | return 89 | } 90 | 91 | func (sg *SvgGraphics) FontMetrics(font chart.Font) (fw float32, fh int, mono bool) { 92 | if font.Name == "" { 93 | font.Name = sg.font 94 | } 95 | fh = sg.fontheight(font) 96 | 97 | switch font.Name { 98 | case "Arial": 99 | fw, mono = 0.5*float32(fh), false 100 | case "Helvetica": 101 | fw, mono = 0.5*float32(fh), false 102 | case "Times": 103 | fw, mono = 0.51*float32(fh), false 104 | case "Courier": 105 | fw, mono = 0.62*float32(fh), true 106 | default: 107 | fw, mono = 0.75*float32(fh), false 108 | } 109 | 110 | // fmt.Printf("FontMetric of %s/%d: %.1f x %d %t\n", style.Font, style.FontSize, fw, fh, mono) 111 | return 112 | } 113 | 114 | func (sg *SvgGraphics) TextLen(t string, font chart.Font) int { 115 | return chart.GenericTextLen(sg, t, font) 116 | } 117 | 118 | var dashlength [][]int = [][]int{[]int{}, []int{4, 1}, []int{1, 1}, []int{4, 1, 1, 1, 1, 1}, []int{4, 4}, []int{1, 3}} 119 | 120 | func (sg *SvgGraphics) Line(x0, y0, x1, y1 int, style chart.Style) { 121 | s := linestyle(style) 122 | sg.svg.Line(x0, y0, x1, y1, s) 123 | } 124 | 125 | func (sg *SvgGraphics) Text(x, y int, t string, align string, rot int, f chart.Font) { 126 | if len(align) == 1 { 127 | align = "c" + align 128 | } 129 | _, fh, _ := sg.FontMetrics(f) 130 | 131 | trans := "" 132 | if rot != 0 { 133 | trans = fmt.Sprintf("transform=\"rotate(%d %d %d)\"", -rot, x, y) 134 | } 135 | 136 | // Hack because baseline alignments in svg often broken 137 | switch align[0] { 138 | case 'b': 139 | y += 0 140 | case 't': 141 | y += fh 142 | default: 143 | y += (4 * fh) / 10 // centered 144 | } 145 | s := "text-anchor:" 146 | switch align[1] { 147 | case 'l': 148 | s += "begin" 149 | case 'r': 150 | s += "end" 151 | default: 152 | s += "middle" 153 | } 154 | if f.Color != "" { 155 | s += "; stroke:" + f.Color 156 | } 157 | if f.Name != "" { 158 | s += "; font-family:" + f.Name 159 | } 160 | if f.Size != 0 { 161 | s += fmt.Sprintf("; font-size: %d", fh) 162 | } 163 | 164 | sg.svg.Text(x, y, t, trans, s) 165 | } 166 | 167 | func (sg *SvgGraphics) Symbol(x, y int, style chart.Style) { 168 | st := "" 169 | filled := "fill:solid" 170 | empty := "fill:none" 171 | if style.SymbolColor != "" { 172 | st += "stroke:" + style.SymbolColor 173 | filled = "fill:" + style.SymbolColor 174 | } 175 | f := style.SymbolSize 176 | if f == 0 { 177 | f = 1 178 | } 179 | lw := 1 180 | if style.LineWidth > 1 { 181 | lw = style.LineWidth 182 | } 183 | 184 | const n = 5 // default size 185 | a := int(n*f + 0.5) // standard 186 | b := int(n/2*f + 0.5) // smaller 187 | c := int(1.155*n*f + 0.5) // triangel long sist 188 | d := int(0.577*n*f + 0.5) // triangle short dist 189 | e := int(0.866*n*f + 0.5) // diagonal 190 | 191 | sg.svg.Gstyle(fmt.Sprintf("%s; stroke-width: %d", st, lw)) 192 | switch style.Symbol { 193 | case '*': 194 | sg.svg.Line(x-e, y-e, x+e, y+e) 195 | sg.svg.Line(x-e, y+e, x+e, y-e) 196 | fallthrough 197 | case '+': 198 | sg.svg.Line(x-a, y, x+a, y) 199 | sg.svg.Line(x, y-a, x, y+a) 200 | case 'X': 201 | sg.svg.Line(x-e, y-e, x+e, y+e) 202 | sg.svg.Line(x-e, y+e, x+e, y-e) 203 | case 'o': 204 | sg.svg.Circle(x, y, a, empty) 205 | case '0': 206 | sg.svg.Circle(x, y, a, empty) 207 | sg.svg.Circle(x, y, b, empty) 208 | case '.': 209 | sg.svg.Circle(x, y, b, empty) 210 | case '@': 211 | sg.svg.Circle(x, y, a, filled) 212 | case '=': 213 | sg.svg.Rect(x-e, y-e, 2*e, 2*e, empty) 214 | case '#': 215 | sg.svg.Rect(x-e, y-e, 2*e, 2*e, filled) 216 | case 'A': 217 | sg.svg.Polygon([]int{x - a, x + a, x}, []int{y + d, y + d, y - c}, filled) 218 | case '%': 219 | sg.svg.Polygon([]int{x - a, x + a, x}, []int{y + d, y + d, y - c}, empty) 220 | case 'W': 221 | sg.svg.Polygon([]int{x - a, x + a, x}, []int{y - c, y - c, y + d}, filled) 222 | case 'V': 223 | sg.svg.Polygon([]int{x - a, x + a, x}, []int{y - c, y - c, y + d}, empty) 224 | case 'Z': 225 | sg.svg.Polygon([]int{x - e, x, x + e, x}, []int{y, y + e, y, y - e}, filled) 226 | case '&': 227 | sg.svg.Polygon([]int{x - e, x, x + e, x}, []int{y, y + e, y, y - e}, empty) 228 | default: 229 | sg.svg.Text(x, y, "?", "text-anchor:middle; alignment-baseline:middle") 230 | } 231 | sg.svg.Gend() 232 | 233 | } 234 | 235 | func (sg *SvgGraphics) Rect(x, y, w, h int, style chart.Style) { 236 | var s string 237 | x, y, w, h = chart.SanitizeRect(x, y, w, h, style.LineWidth) 238 | linecol := style.LineColor 239 | if linecol != "" { 240 | s = fmt.Sprintf("stroke:%s; ", linecol) 241 | } else { 242 | linecol = "#808080" 243 | } 244 | s += fmt.Sprintf("stroke-width: %d; ", style.LineWidth) 245 | s += fmt.Sprintf("opacity: %.2f; ", 1-style.Alpha) 246 | if style.FillColor != "" { 247 | s += fmt.Sprintf("fill: %s; fill-opacity: %.2f", style.FillColor, 1-style.Alpha) 248 | } else { 249 | s += "fill-opacity: 0" 250 | } 251 | sg.svg.Rect(x, y, w, h, s) 252 | // GenericRect(sg, x, y, w, h, style) // TODO 253 | } 254 | 255 | func (sg *SvgGraphics) Path(x, y []int, style chart.Style) { 256 | n := len(x) 257 | if len(y) < n { 258 | n = len(y) 259 | } 260 | path := fmt.Sprintf("M %d,%d", x[0], y[0]) 261 | for i := 1; i < n; i++ { 262 | path += fmt.Sprintf("L %d,%d", x[i], y[i]) 263 | } 264 | st := linestyle(style) 265 | sg.svg.Path(path, st) 266 | } 267 | 268 | func (sg *SvgGraphics) Wedge(x, y, ro, ri int, phi, psi float64, style chart.Style) { 269 | panic("No Wedge() for SvgGraphics.") 270 | } 271 | 272 | func (sg *SvgGraphics) Title(text string) { 273 | font := chart.DefaultFont["title"] 274 | _, fh, _ := sg.FontMetrics(font) 275 | x, y := sg.w/2, fh/2 276 | sg.Text(x, y, text, "tc", 0, font) 277 | } 278 | 279 | func (sg *SvgGraphics) XAxis(xr chart.Range, ys, yms int) { 280 | chart.GenericXAxis(sg, xr, ys, yms) 281 | } 282 | func (sg *SvgGraphics) YAxis(yr chart.Range, xs, xms int) { 283 | chart.GenericYAxis(sg, yr, xs, xms) 284 | } 285 | 286 | func linestyle(style chart.Style) (s string) { 287 | lw := style.LineWidth 288 | if style.LineColor != "" { 289 | s = fmt.Sprintf("stroke:%s; ", style.LineColor) 290 | } 291 | s += fmt.Sprintf("stroke-width: %d; fill:none; ", lw) 292 | s += fmt.Sprintf("opacity: %.2f; ", 1-style.Alpha) 293 | if style.LineStyle != chart.SolidLine { 294 | s += fmt.Sprintf("stroke-dasharray:") 295 | for _, d := range dashlength[style.LineStyle] { 296 | s += fmt.Sprintf(" %d", d*lw) 297 | } 298 | } 299 | return 300 | } 301 | 302 | func (sg *SvgGraphics) Scatter(points []chart.EPoint, plotstyle chart.PlotStyle, style chart.Style) { 303 | chart.GenericScatter(sg, points, plotstyle, style) 304 | 305 | /*********************************************** 306 | // First pass: Error bars 307 | ebs := style 308 | ebs.LineColor, ebs.LineWidth, ebs.LineStyle = ebs.FillColor, 1, chart.SolidLine 309 | if ebs.LineColor == "" { 310 | ebs.LineColor = "#404040" 311 | } 312 | if ebs.LineWidth == 0 { 313 | ebs.LineWidth = 1 314 | } 315 | for _, p := range points { 316 | xl, yl, xh, yh := p.BoundingBox() 317 | // fmt.Printf("Draw %d: %f %f-%f\n", i, p.DeltaX, xl,xh) 318 | if !math.IsNaN(p.DeltaX) { 319 | sg.Line(int(xl), int(p.Y), int(xh), int(p.Y), ebs) 320 | } 321 | if !math.IsNaN(p.DeltaY) { 322 | sg.Line(int(p.X), int(yl), int(p.X), int(yh), ebs) 323 | } 324 | } 325 | 326 | // Second pass: Line 327 | if (plotstyle&chart.PlotStyleLines) != 0 && len(points) > 0 { 328 | path := fmt.Sprintf("M %d,%d", int(points[0].X), int(points[0].Y)) 329 | for i := 1; i < len(points); i++ { 330 | path += fmt.Sprintf("L %d,%d", int(points[i].X), int(points[i].Y)) 331 | } 332 | st := linestyle(style) 333 | sg.svg.Path(path, st) 334 | } 335 | 336 | // Third pass: symbols 337 | if (plotstyle&chart.PlotStylePoints) != 0 && len(points) != 0 { 338 | for _, p := range points { 339 | sg.Symbol(int(p.X), int(p.Y), style) 340 | } 341 | } 342 | 343 | ****************************************************/ 344 | } 345 | 346 | func (sg *SvgGraphics) Boxes(boxes []chart.Box, width int, style chart.Style) { 347 | chart.GenericBoxes(sg, boxes, width, style) 348 | } 349 | 350 | func (sg *SvgGraphics) Key(x, y int, key chart.Key) { 351 | chart.GenericKey(sg, x, y, key) 352 | } 353 | 354 | func (sg *SvgGraphics) Bars(bars []chart.Barinfo, style chart.Style) { 355 | chart.GenericBars(sg, bars, style) 356 | } 357 | 358 | func (sg *SvgGraphics) Rings(wedges []chart.Wedgeinfo, x, y, ro, ri int) { 359 | for _, w := range wedges { 360 | var s string 361 | linecol := w.Style.LineColor 362 | if linecol != "" { 363 | s = fmt.Sprintf("stroke:%s; ", linecol) 364 | } else { 365 | linecol = "#808080" 366 | } 367 | s += fmt.Sprintf("stroke-width: %d; ", w.Style.LineWidth) 368 | s += fmt.Sprintf("opacity: %.2f; ", 1-w.Style.Alpha) 369 | var sf string 370 | if w.Style.FillColor != "" { 371 | sf = fmt.Sprintf("fill: %s; fill-opacity: %.2f", w.Style.FillColor, 1-w.Style.Alpha) 372 | } else { 373 | sf = "fill-opacity: 0" 374 | } 375 | 376 | if math.Abs(w.Phi-w.Psi) >= 4*math.Pi { 377 | sg.svg.Circle(x, y, ro, s+sf) 378 | if ri > 0 { 379 | sf = "fill: #ffffff; fill-opacity: 1" 380 | sg.svg.Circle(x, y, ri, s+sf) 381 | } 382 | continue 383 | } 384 | 385 | var d string 386 | p := 0.4 * float64(w.Style.LineWidth+w.Shift) 387 | cphi, sphi := math.Cos(w.Phi), math.Sin(w.Phi) 388 | cpsi, spsi := math.Cos(w.Psi), math.Sin(w.Psi) 389 | 390 | if ri <= 0 { 391 | // real wedge drawn as center -> outer radius -> arc -> closed to center 392 | rf := float64(ro) 393 | a := math.Sin((w.Psi - w.Phi) / 2) 394 | dx, dy := p*math.Cos((w.Phi+w.Psi)/2)/a, p*math.Sin((w.Phi+w.Psi)/2)/a 395 | d = fmt.Sprintf("M %d,%d ", x+int(dx+0.5), y+int(dy+0.5)) 396 | 397 | dx, dy = p*math.Cos(w.Phi+math.Pi/2), p*math.Sin(w.Phi+math.Pi/2) 398 | d += fmt.Sprintf("L %d,%d ", int(rf*cphi+0.5+dx)+x, int(rf*sphi+0.5+dy)+y) 399 | 400 | dx, dy = p*math.Cos(w.Psi-math.Pi/2), p*math.Sin(w.Psi-math.Pi/2) 401 | 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) 402 | d += fmt.Sprintf("z") 403 | } else { 404 | // ring drawn as inner radius -> outer radius -> outer arc -> inner radius -> inner arc 405 | rof, rif := float64(ro), float64(ri) 406 | dx, dy := p*math.Cos(w.Phi+math.Pi/2), p*math.Sin(w.Phi+math.Pi/2) 407 | a, b := int(rif*cphi+0.5+dx)+x, int(rif*sphi+0.5+dy)+y 408 | d = fmt.Sprintf("M %d,%d ", a, b) 409 | d += fmt.Sprintf("L %d,%d ", int(rof*cphi+0.5+dx)+x, int(rof*sphi+0.5+dy)+y) 410 | 411 | dx, dy = p*math.Cos(w.Psi-math.Pi/2), p*math.Sin(w.Psi-math.Pi/2) 412 | 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) 413 | d += fmt.Sprintf("L %d,%d ", int(rif*cpsi+0.5+dx)+x, int(rif*spsi+0.5+dy)+y) 414 | d += fmt.Sprintf("A %d,%d 0 0 0 %d,%d ", ri, ri, a, b) 415 | d += fmt.Sprintf("z") 416 | 417 | } 418 | 419 | sg.svg.Path(d, s+sf) 420 | 421 | if w.Text != "" { 422 | _, fh, _ := sg.FontMetrics(w.Font) 423 | alpha := (w.Phi + w.Psi) / 2 424 | var rt int 425 | if ri > 0 { 426 | rt = (ri + ro) / 2 427 | } else { 428 | rt = ro - 3*fh 429 | if rt <= ro/2 { 430 | rt = ro - 2*fh 431 | } 432 | } 433 | tx, ty := int(float64(rt)*math.Cos(alpha)+0.5)+x, int(float64(rt)*math.Sin(alpha)+0.5)+y 434 | 435 | sg.Text(tx, ty, w.Text, "cc", 0, w.Font) 436 | } 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /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 | trace.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 | trace.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 | trace.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 | debug.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 | trace.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 | trace.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 | trace.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 | trace.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 _, 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.FailNow() 35 | } 36 | date = time.SecondsToLocalTime(date.Seconds()) 37 | expected = time.SecondsToLocalTime(expected.Seconds()) 38 | sample.delta.RoundDown(date) 39 | if date.Seconds() != expected.Seconds() { 40 | t.Errorf("RoundDown %s to %s != %s, was %s", sample.date, sample.delta, 41 | sample.expected, date.Format(tf)) 42 | } 43 | } 44 | 45 | } 46 | 47 | func TextCalendarWeek(t *testing.T) { 48 | for _, u := range [][4]int{ 49 | [4]int{2011, 1, 1, 52}, 50 | [4]int{2011, 1, 2, 52}, 51 | [4]int{2011, 1, 3, 1}, 52 | [4]int{2011, 1, 4, 1}, 53 | [4]int{2011, 1, 5, 1}, 54 | [4]int{2011, 1, 6, 1}, 55 | [4]int{2011, 1, 7, 1}, 56 | [4]int{2011, 1, 8, 1}, 57 | [4]int{2011, 1, 9, 1}, 58 | [4]int{2011, 1, 10, 2}, 59 | [4]int{2011, 12, 25, 51}, 60 | [4]int{2011, 12, 26, 52}, 61 | [4]int{2011, 12, 27, 52}, 62 | [4]int{2011, 12, 28, 52}, 63 | [4]int{2011, 12, 29, 52}, 64 | [4]int{2011, 12, 30, 52}, 65 | [4]int{2011, 12, 31, 52}, 66 | [4]int{1995, 1, 1, 52}, 67 | [4]int{1995, 1, 2, 1}, 68 | [4]int{1996, 1, 1, 1}, 69 | [4]int{1996, 1, 7, 1}, 70 | [4]int{1996, 1, 8, 2}, 71 | [4]int{1997, 1, 1, 1}, 72 | [4]int{1998, 1, 1, 1}, 73 | [4]int{1999, 1, 1, 53}, 74 | [4]int{2000, 1, 1, 52}, 75 | [4]int{2001, 1, 1, 1}, 76 | [4]int{2002, 1, 1, 1}, 77 | [4]int{2003, 1, 1, 1}, 78 | [4]int{2004, 1, 1, 1}, 79 | [4]int{2005, 1, 1, 53}, 80 | [4]int{2006, 1, 1, 52}, 81 | [4]int{2007, 1, 1, 1}, 82 | [4]int{2008, 1, 1, 1}, 83 | [4]int{2009, 1, 1, 1}, 84 | [4]int{2010, 1, 1, 53}, 85 | } { 86 | dt := &time.Time{Year: int64(u[0]), Month: u[1], Day: u[2]} 87 | dt = time.SecondsToLocalTime(dt.Seconds()) 88 | w := calendarWeek(dt) 89 | if w != u[3] { 90 | t.Errorf("Failed on %d-%d-%d. Got %d, expected %d.", u[0], u[1], u[2], w, u[3]) 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /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) Begin() { 26 | g.tb = NewTextBuf(g.w, g.h) 27 | } 28 | 29 | func (g *TextGraphics) End() {} 30 | func (tg *TextGraphics) Background() (r, g, b, a uint8) { return 255, 255, 255, 255 } 31 | func (g *TextGraphics) Dimensions() (int, int) { 32 | return g.w, g.h 33 | } 34 | func (g *TextGraphics) FontMetrics(font chart.Font) (fw float32, fh int, mono bool) { 35 | return 1, 1, true 36 | } 37 | 38 | func (g *TextGraphics) TextLen(t string, font chart.Font) int { 39 | return len(t) 40 | } 41 | 42 | func (g *TextGraphics) Line(x0, y0, x1, y1 int, style chart.Style) { 43 | symbol := style.Symbol 44 | if symbol < ' ' || symbol > '~' { 45 | symbol = 'x' 46 | } 47 | g.tb.Line(x0, y0, x1, y1, rune(symbol)) 48 | } 49 | func (g *TextGraphics) Path(x, y []int, style chart.Style) { 50 | chart.GenericPath(g, x, y, style) 51 | } 52 | 53 | func (g *TextGraphics) Wedge(x, y, ro, ri int, phi, psi float64, style chart.Style) { 54 | chart.GenericWedge(g, x, y, ro, ri, phi, psi, CircleStretchFactor, style) 55 | } 56 | 57 | func (g *TextGraphics) Text(x, y int, t string, align string, rot int, font chart.Font) { 58 | // align: -1: left; 0: centered; 1: right; 2: top, 3: center, 4: bottom 59 | if len(align) == 2 { 60 | align = align[1:] 61 | } 62 | a := 0 63 | if rot == 0 { 64 | if align == "l" { 65 | a = -1 66 | } 67 | if align == "c" { 68 | a = 0 69 | } 70 | if align == "r" { 71 | a = 1 72 | } 73 | } else { 74 | if align == "l" { 75 | a = 2 76 | } 77 | if align == "c" { 78 | a = 3 79 | } 80 | if align == "r" { 81 | a = 4 82 | } 83 | } 84 | g.tb.Text(x, y, t, a) 85 | } 86 | 87 | func (g *TextGraphics) Rect(x, y, w, h int, style chart.Style) { 88 | chart.SanitizeRect(x, y, w, h, 1) 89 | // Border 90 | if style.LineWidth > 0 { 91 | for i := 0; i < w; i++ { 92 | g.tb.Put(x+i, y, rune(style.Symbol)) 93 | g.tb.Put(x+i, y+h-1, rune(style.Symbol)) 94 | } 95 | for i := 1; i < h-1; i++ { 96 | g.tb.Put(x, y+i, rune(style.Symbol)) 97 | g.tb.Put(x+w-1, y+i, rune(style.Symbol)) 98 | } 99 | } 100 | 101 | // Filling 102 | if style.FillColor != "" { 103 | // TODO: fancier logic 104 | var s int 105 | if style.FillColor == "#000000" { 106 | s = '#' // black 107 | } else if style.FillColor == "#ffffff" { 108 | s = ' ' // white 109 | } else { 110 | s = style.Symbol 111 | } 112 | for i := 1; i < h-1; i++ { 113 | for j := 1; j < w-1; j++ { 114 | g.tb.Put(x+j, y+i, rune(s)) 115 | } 116 | } 117 | } 118 | } 119 | 120 | func (g *TextGraphics) String() string { 121 | return g.tb.String() 122 | } 123 | 124 | func (g *TextGraphics) Symbol(x, y int, style chart.Style) { 125 | g.tb.Put(x, y, rune(style.Symbol)) 126 | } 127 | func (g *TextGraphics) Title(text string) { 128 | x, y := g.w/2, 1 129 | g.Text(x, y, text, "tc", 0, chart.Font{}) 130 | } 131 | 132 | func (g *TextGraphics) XAxis(xrange chart.Range, y, y1 int) { 133 | mirror := xrange.TicSetting.Mirror 134 | xa, xe := xrange.Data2Screen(xrange.Min), xrange.Data2Screen(xrange.Max) 135 | for sx := xa; sx <= xe; sx++ { 136 | g.tb.Put(sx, y, '-') 137 | if mirror >= 1 { 138 | g.tb.Put(sx, y1, '-') 139 | } 140 | } 141 | if xrange.ShowZero && xrange.Min < 0 && xrange.Max > 0 { 142 | z := xrange.Data2Screen(0) 143 | for yy := y - 1; yy > y1+1; yy-- { 144 | g.tb.Put(z, yy, ':') 145 | } 146 | } 147 | 148 | if xrange.Label != "" { 149 | yy := y + 1 150 | if !xrange.TicSetting.Hide { 151 | yy++ 152 | } 153 | g.tb.Text((xa+xe)/2, yy, xrange.Label, 0) 154 | } 155 | 156 | for _, tic := range xrange.Tics { 157 | var x int 158 | if !math.IsNaN(tic.Pos) { 159 | x = xrange.Data2Screen(tic.Pos) 160 | } else { 161 | x = -1 162 | } 163 | lx := xrange.Data2Screen(tic.LabelPos) 164 | if xrange.Time { 165 | if x != -1 { 166 | g.tb.Put(x, y, '|') 167 | if mirror >= 2 { 168 | g.tb.Put(x, y1, '|') 169 | } 170 | g.tb.Put(x, y+1, '|') 171 | } 172 | if tic.Align == -1 { 173 | g.tb.Text(lx+1, y+1, tic.Label, -1) 174 | } else { 175 | g.tb.Text(lx, y+1, tic.Label, 0) 176 | } 177 | } else { 178 | if x != -1 { 179 | g.tb.Put(x, y, '+') 180 | if mirror >= 2 { 181 | g.tb.Put(x, y1, '+') 182 | } 183 | } 184 | g.tb.Text(lx, y+1, tic.Label, 0) 185 | } 186 | if xrange.ShowLimits { 187 | if xrange.Time { 188 | g.tb.Text(xa, y+2, xrange.TMin.Format("2006-01-02 15:04:05"), -1) 189 | g.tb.Text(xe, y+2, xrange.TMax.Format("2006-01-02 15:04:05"), 1) 190 | } else { 191 | g.tb.Text(xa, y+2, fmt.Sprintf("%g", xrange.Min), -1) 192 | g.tb.Text(xe, y+2, fmt.Sprintf("%g", xrange.Max), 1) 193 | } 194 | } 195 | } 196 | } 197 | 198 | func (g *TextGraphics) YAxis(yrange chart.Range, x, x1 int) { 199 | label := yrange.Label 200 | mirror := yrange.TicSetting.Mirror 201 | ya, ye := yrange.Data2Screen(yrange.Min), yrange.Data2Screen(yrange.Max) 202 | for sy := min(ya, ye); sy <= max(ya, ye); sy++ { 203 | g.tb.Put(x, sy, '|') 204 | if mirror >= 1 { 205 | g.tb.Put(x1, sy, '|') 206 | } 207 | } 208 | if yrange.ShowZero && yrange.Min < 0 && yrange.Max > 0 { 209 | z := yrange.Data2Screen(0) 210 | for xx := x + 1; xx < x1; xx += 2 { 211 | g.tb.Put(xx, z, '-') 212 | } 213 | } 214 | 215 | if label != "" { 216 | g.tb.Text(1, (ya+ye)/2, label, 3) 217 | } 218 | 219 | for _, tic := range yrange.Tics { 220 | y := yrange.Data2Screen(tic.Pos) 221 | ly := yrange.Data2Screen(tic.LabelPos) 222 | if yrange.Time { 223 | g.tb.Put(x, y, '+') 224 | if mirror >= 2 { 225 | g.tb.Put(x1, y, '+') 226 | } 227 | if tic.Align == 0 { // centered tic 228 | g.tb.Put(x-1, y, '-') 229 | g.tb.Put(x-2, y, '-') 230 | } 231 | g.tb.Text(x, ly, tic.Label+" ", 1) 232 | } else { 233 | g.tb.Put(x, y, '+') 234 | if mirror >= 2 { 235 | g.tb.Put(x1, y, '+') 236 | } 237 | g.tb.Text(x-2, ly, tic.Label, 1) 238 | } 239 | } 240 | } 241 | 242 | func (g *TextGraphics) Scatter(points []chart.EPoint, plotstyle chart.PlotStyle, style chart.Style) { 243 | // First pass: Error bars 244 | for _, p := range points { 245 | xl, yl, xh, yh := p.BoundingBox() 246 | if !math.IsNaN(p.DeltaX) { 247 | g.tb.Line(int(xl), int(p.Y), int(xh), int(p.Y), '-') 248 | } 249 | if !math.IsNaN(p.DeltaY) { 250 | g.tb.Line(int(p.X), int(yl), int(p.X), int(yh), '|') 251 | } 252 | } 253 | 254 | // Second pass: Line 255 | if (plotstyle&chart.PlotStyleLines) != 0 && len(points) > 0 { 256 | lastx, lasty := int(points[0].X), int(points[0].Y) 257 | for i := 1; i < len(points); i++ { 258 | x, y := int(points[i].X), int(points[i].Y) 259 | // fmt.Printf("LineSegment %d (%d,%d) -> (%d,%d)\n", i, lastx,lasty,x,y) 260 | g.tb.Line(lastx, lasty, x, y, rune(style.Symbol)) 261 | lastx, lasty = x, y 262 | } 263 | } 264 | 265 | // Third pass: symbols 266 | if (plotstyle&chart.PlotStylePoints) != 0 && len(points) != 0 { 267 | for _, p := range points { 268 | g.tb.Put(int(p.X), int(p.Y), rune(style.Symbol)) 269 | } 270 | } 271 | // chart.GenericScatter(g, points, plotstyle, style) 272 | } 273 | 274 | func (g *TextGraphics) Boxes(boxes []chart.Box, width int, style chart.Style) { 275 | if width%2 == 0 { 276 | width += 1 277 | } 278 | hbw := (width - 1) / 2 279 | if style.Symbol == 0 { 280 | style.Symbol = '*' 281 | } 282 | 283 | for _, box := range boxes { 284 | x := int(box.X) 285 | q1, q3 := int(box.Q1), int(box.Q3) 286 | g.tb.Rect(x-hbw, q1, 2*hbw, q3-q1, 0, ' ') 287 | if !math.IsNaN(box.Med) { 288 | med := int(box.Med) 289 | g.tb.Put(x-hbw, med, '+') 290 | for i := 0; i < hbw; i++ { 291 | g.tb.Put(x-i, med, '-') 292 | g.tb.Put(x+i, med, '-') 293 | } 294 | g.tb.Put(x+hbw, med, '+') 295 | } 296 | 297 | if !math.IsNaN(box.Avg) && style.Symbol != 0 { 298 | g.tb.Put(x, int(box.Avg), rune(style.Symbol)) 299 | } 300 | 301 | if !math.IsNaN(box.High) { 302 | for y := int(box.High); y < q3; y++ { 303 | g.tb.Put(x, y, '|') 304 | } 305 | } 306 | 307 | if !math.IsNaN(box.Low) { 308 | for y := int(box.Low); y > q1; y-- { 309 | g.tb.Put(x, y, '|') 310 | } 311 | } 312 | 313 | for _, ol := range box.Outliers { 314 | y := int(ol) 315 | g.tb.Put(x, y, rune(style.Symbol)) 316 | } 317 | } 318 | } 319 | 320 | func (g *TextGraphics) Key(x, y int, key chart.Key) { 321 | m := key.Place() 322 | if len(m) == 0 { 323 | return 324 | } 325 | tw, th, cw, rh := key.Layout(g, m) 326 | // fmt.Printf("Text-Key: %d x %d\n", tw,th) 327 | style := chart.DefaultStyle["key"] 328 | if style.LineWidth > 0 || style.FillColor != "" { 329 | g.tb.Rect(x, y, tw, th-1, 1, ' ') 330 | } 331 | x += int(chart.KeyHorSep) 332 | vsep := chart.KeyVertSep 333 | if vsep < 1 { 334 | vsep = 1 335 | } 336 | y += int(vsep) 337 | for ci, col := range m { 338 | yy := y 339 | 340 | for ri, e := range col { 341 | if e == nil || e.Text == "" { 342 | continue 343 | } 344 | plotStyle := e.PlotStyle 345 | // fmt.Printf("KeyEntry %s: PlotStyle = %d\n", e.Text, e.PlotStyle) 346 | if plotStyle == -1 { 347 | // heading only... 348 | g.tb.Text(x, yy, e.Text, -1) 349 | } else { 350 | // normal entry 351 | if (plotStyle & chart.PlotStyleLines) != 0 { 352 | g.Line(x, yy, x+int(chart.KeySymbolWidth), yy, e.Style) 353 | } 354 | if (plotStyle & chart.PlotStylePoints) != 0 { 355 | g.Symbol(x+int(chart.KeySymbolWidth/2), yy, e.Style) 356 | } 357 | if (plotStyle & chart.PlotStyleBox) != 0 { 358 | g.tb.Put(x+int(chart.KeySymbolWidth/2), yy, rune(e.Style.Symbol)) 359 | } 360 | g.tb.Text(x+int((chart.KeySymbolWidth+chart.KeySymbolSep)), yy, e.Text, -1) 361 | } 362 | yy += rh[ri] + int(chart.KeyRowSep) 363 | } 364 | 365 | x += int((chart.KeySymbolWidth + chart.KeySymbolSep + chart.KeyColSep + float32(cw[ci]))) 366 | } 367 | 368 | } 369 | 370 | func (g *TextGraphics) Bars(bars []chart.Barinfo, style chart.Style) { 371 | chart.GenericBars(g, bars, style) 372 | } 373 | 374 | var CircleStretchFactor float64 = 1.85 375 | 376 | func (g *TextGraphics) Rings(wedges []chart.Wedgeinfo, x, y, ro, ri int) { 377 | if g.xoff == -1 { 378 | g.xoff = int(float64(ro) * (CircleStretchFactor - 1)) 379 | // debug.Printf("Shifting center about %d (ro=%d, f=%.2f)", g.xoff, ro, CircleStretchFactor) 380 | } 381 | for i := range wedges { 382 | wedges[i].Style.LineWidth = 1 383 | } 384 | chart.GenericRings(g, wedges, x+g.xoff, y, ro, ri, 1.8) 385 | } 386 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------