github.com/phpdave11/gofpdf@v1.4.2/grid.go (about) 1 package gofpdf 2 3 import ( 4 "math" 5 "strconv" 6 ) 7 8 func unused(args ...interface{}) { 9 } 10 11 // RGBType holds fields for red, green and blue color components (0..255) 12 type RGBType struct { 13 R, G, B int 14 } 15 16 // RGBAType holds fields for red, green and blue color components (0..255) and 17 // an alpha transparency value (0..1) 18 type RGBAType struct { 19 R, G, B int 20 Alpha float64 21 } 22 23 // StateType holds various commonly used drawing values for convenient 24 // retrieval (StateGet()) and restore (Put) methods. 25 type StateType struct { 26 clrDraw, clrText, clrFill RGBType 27 lineWd float64 28 fontSize float64 29 alpha float64 30 blendStr string 31 cellMargin float64 32 } 33 34 // StateGet returns a variable that contains common state values. 35 func StateGet(pdf *Fpdf) (st StateType) { 36 st.clrDraw.R, st.clrDraw.G, st.clrDraw.B = pdf.GetDrawColor() 37 st.clrFill.R, st.clrFill.G, st.clrFill.B = pdf.GetFillColor() 38 st.clrText.R, st.clrText.G, st.clrText.B = pdf.GetTextColor() 39 st.lineWd = pdf.GetLineWidth() 40 _, st.fontSize = pdf.GetFontSize() 41 st.alpha, st.blendStr = pdf.GetAlpha() 42 st.cellMargin = pdf.GetCellMargin() 43 return 44 } 45 46 // Put sets the common state values contained in the state structure 47 // specified by st. 48 func (st StateType) Put(pdf *Fpdf) { 49 pdf.SetDrawColor(st.clrDraw.R, st.clrDraw.G, st.clrDraw.B) 50 pdf.SetFillColor(st.clrFill.R, st.clrFill.G, st.clrFill.B) 51 pdf.SetTextColor(st.clrText.R, st.clrText.G, st.clrText.B) 52 pdf.SetLineWidth(st.lineWd) 53 pdf.SetFontUnitSize(st.fontSize) 54 pdf.SetAlpha(st.alpha, st.blendStr) 55 pdf.SetCellMargin(st.cellMargin) 56 } 57 58 // TickFormatFncType defines a callback for label drawing. 59 type TickFormatFncType func(val float64, precision int) string 60 61 // defaultFormatter returns the string form of val with precision decimal places. 62 func defaultFormatter(val float64, precision int) string { 63 return strconv.FormatFloat(val, 'f', precision, 64) 64 } 65 66 // GridType assists with the generation of graphs. It allows the application to 67 // work with logical data coordinates rather than page coordinates and assists 68 // with the drawing of a background grid. 69 type GridType struct { 70 // Chart coordinates in page units 71 x, y, w, h float64 72 // X, Y, Wd, Ht float64 73 // Slopes and intercepts scale data points to graph coordinates linearly 74 xm, xb, ym, yb float64 75 // Tickmarks 76 xTicks, yTicks []float64 77 // Labels are inside of graph boundary 78 XLabelIn, YLabelIn bool 79 // Labels on X-axis should be rotated 80 XLabelRotate bool 81 // Formatters; use nil to eliminate labels 82 XTickStr, YTickStr TickFormatFncType 83 // Subdivisions between tickmarks 84 XDiv, YDiv int 85 // Formatting precision 86 xPrecision, yPrecision int 87 // Line and label colors 88 ClrText, ClrMain, ClrSub RGBAType 89 // Line thickness 90 WdMain, WdSub float64 91 // Label height in points 92 TextSize float64 93 } 94 95 // linear returns the slope and y-intercept of the straight line joining the 96 // two specified points. For scaling purposes, associate the arguments as 97 // follows: x1: observed low value, y1: desired low value, x2: observed high 98 // value, y2: desired high value. 99 func linear(x1, y1, x2, y2 float64) (slope, intercept float64) { 100 if x2 != x1 { 101 slope = (y2 - y1) / (x2 - x1) 102 intercept = y2 - x2*slope 103 } 104 return 105 } 106 107 // linearTickmark returns the slope and intercept that will linearly map data 108 // values (the range of which is specified by the tickmark slice tm) to page 109 // values (the range of which is specified by lo and hi). 110 func linearTickmark(tm []float64, lo, hi float64) (slope, intercept float64) { 111 ln := len(tm) 112 if ln > 0 { 113 slope, intercept = linear(tm[0], lo, tm[ln-1], hi) 114 } 115 return 116 } 117 118 // NewGrid returns a variable of type GridType that is initialized to draw on a 119 // rectangle of width w and height h with the upper left corner positioned at 120 // point (x, y). The coordinates are in page units, that is, the same as those 121 // specified in New(). 122 // 123 // The returned variable is initialized with a very simple default tickmark 124 // layout that ranges from 0 to 1 in both axes. Prior to calling Grid(), the 125 // application may establish a more suitable tickmark layout by calling the 126 // methods TickmarksContainX() and TickmarksContainY(). These methods bound the 127 // data range with appropriate boundaries and divisions. Alternatively, if the 128 // exact extent and divisions of the tickmark layout are known, the methods 129 // TickmarksExtentX() and TickmarksExtentY may be called instead. 130 func NewGrid(x, y, w, h float64) (grid GridType) { 131 grid.x = x 132 grid.y = y 133 grid.w = w 134 grid.h = h 135 grid.TextSize = 7 // Points 136 grid.TickmarksExtentX(0, 1, 1) 137 grid.TickmarksExtentY(0, 1, 1) 138 grid.XLabelIn = false 139 grid.YLabelIn = false 140 grid.XLabelRotate = false 141 grid.XDiv = 10 142 grid.YDiv = 10 143 grid.ClrText = RGBAType{R: 0, G: 0, B: 0, Alpha: 1} 144 grid.ClrMain = RGBAType{R: 128, G: 160, B: 128, Alpha: 1} 145 grid.ClrSub = RGBAType{R: 192, G: 224, B: 192, Alpha: 1} 146 grid.WdMain = 0.1 147 grid.WdSub = 0.1 148 grid.YTickStr = defaultFormatter 149 grid.XTickStr = defaultFormatter 150 return 151 } 152 153 // WdAbs returns the absolute value of dataWd, specified in logical data units, 154 // that has been converted to the unit of measure specified in New(). 155 func (g GridType) WdAbs(dataWd float64) float64 { 156 return math.Abs(g.xm * dataWd) 157 } 158 159 // Wd converts dataWd, specified in logical data units, to the unit of measure 160 // specified in New(). 161 func (g GridType) Wd(dataWd float64) float64 { 162 return g.xm * dataWd 163 } 164 165 // XY converts dataX and dataY, specified in logical data units, to the X and Y 166 // position on the current page. 167 func (g GridType) XY(dataX, dataY float64) (x, y float64) { 168 return g.xm*dataX + g.xb, g.ym*dataY + g.yb 169 } 170 171 // Pos returns the point, in page units, indicated by the relative positions 172 // xRel and yRel. These are values between 0 and 1. xRel specifies the relative 173 // position between the grid's left and right edges. yRel specifies the 174 // relative position between the grid's bottom and top edges. 175 func (g GridType) Pos(xRel, yRel float64) (x, y float64) { 176 x = g.w*xRel + g.x 177 y = g.h*(1-yRel) + g.y 178 return 179 } 180 181 // X converts dataX, specified in logical data units, to the X position on the 182 // current page. 183 func (g GridType) X(dataX float64) float64 { 184 return g.xm*dataX + g.xb 185 } 186 187 // HtAbs returns the absolute value of dataHt, specified in logical data units, 188 // that has been converted to the unit of measure specified in New(). 189 func (g GridType) HtAbs(dataHt float64) float64 { 190 return math.Abs(g.ym * dataHt) 191 } 192 193 // Ht converts dataHt, specified in logical data units, to the unit of measure 194 // specified in New(). 195 func (g GridType) Ht(dataHt float64) float64 { 196 return g.ym * dataHt 197 } 198 199 // Y converts dataY, specified in logical data units, to the Y position on the 200 // current page. 201 func (g GridType) Y(dataY float64) float64 { 202 return g.ym*dataY + g.yb 203 } 204 205 // XRange returns the minimum and maximum values for the current tickmark 206 // sequence. These correspond to the data values of the graph's left and right 207 // edges. 208 func (g GridType) XRange() (min, max float64) { 209 min = g.xTicks[0] 210 max = g.xTicks[len(g.xTicks)-1] 211 return 212 } 213 214 // YRange returns the minimum and maximum values for the current tickmark 215 // sequence. These correspond to the data values of the graph's bottom and top 216 // edges. 217 func (g GridType) YRange() (min, max float64) { 218 min = g.yTicks[0] 219 max = g.yTicks[len(g.yTicks)-1] 220 return 221 } 222 223 // TickmarksContainX sets the tickmarks to be shown by Grid() in the horizontal 224 // dimension. The argument min and max specify the minimum and maximum values 225 // to be contained within the grid. The tickmark values that are generated are 226 // suitable for general purpose graphs. 227 // 228 // See TickmarkExtentX() for an alternative to this method to be used when the 229 // exact values of the tickmarks are to be set by the application. 230 func (g *GridType) TickmarksContainX(min, max float64) { 231 g.xTicks, g.xPrecision = Tickmarks(min, max) 232 g.xm, g.xb = linearTickmark(g.xTicks, g.x, g.x+g.w) 233 } 234 235 // TickmarksContainY sets the tickmarks to be shown by Grid() in the vertical 236 // dimension. The argument min and max specify the minimum and maximum values 237 // to be contained within the grid. The tickmark values that are generated are 238 // suitable for general purpose graphs. 239 // 240 // See TickmarkExtentY() for an alternative to this method to be used when the 241 // exact values of the tickmarks are to be set by the application. 242 func (g *GridType) TickmarksContainY(min, max float64) { 243 g.yTicks, g.yPrecision = Tickmarks(min, max) 244 g.ym, g.yb = linearTickmark(g.yTicks, g.y+g.h, g.y) 245 } 246 247 func extent(min, div float64, count int) (tm []float64, precision int) { 248 tm = make([]float64, count+1) 249 for j := 0; j <= count; j++ { 250 tm[j] = min 251 min += div 252 } 253 precision = TickmarkPrecision(div) 254 return 255 } 256 257 // TickmarksExtentX sets the tickmarks to be shown by Grid() in the horizontal 258 // dimension. count specifies number of major tickmark subdivisions to be 259 // graphed. min specifies the leftmost data value. div specifies, in data 260 // units, the extent of each major tickmark subdivision. 261 // 262 // See TickmarkContainX() for an alternative to this method to be used when 263 // viewer-friendly tickmarks are to be determined automatically. 264 func (g *GridType) TickmarksExtentX(min, div float64, count int) { 265 g.xTicks, g.xPrecision = extent(min, div, count) 266 g.xm, g.xb = linearTickmark(g.xTicks, g.x, g.x+g.w) 267 } 268 269 // TickmarksExtentY sets the tickmarks to be shown by Grid() in the vertical 270 // dimension. count specifies number of major tickmark subdivisions to be 271 // graphed. min specifies the bottommost data value. div specifies, in data 272 // units, the extent of each major tickmark subdivision. 273 // 274 // See TickmarkContainY() for an alternative to this method to be used when 275 // viewer-friendly tickmarks are to be determined automatically. 276 func (g *GridType) TickmarksExtentY(min, div float64, count int) { 277 g.yTicks, g.yPrecision = extent(min, div, count) 278 g.ym, g.yb = linearTickmark(g.yTicks, g.y+g.h, g.y) 279 } 280 281 // func (g *GridType) SetXExtent(dataLf, paperLf, dataRt, paperRt float64) { 282 // g.xm, g.xb = linear(dataLf, paperLf, dataRt, paperRt) 283 // } 284 285 // func (g *GridType) SetYExtent(dataTp, paperTp, dataBt, paperBt float64) { 286 // g.ym, g.yb = linear(dataTp, paperTp, dataBt, paperBt) 287 // } 288 289 func lineAttr(pdf *Fpdf, clr RGBAType, lineWd float64) { 290 pdf.SetLineWidth(lineWd) 291 pdf.SetAlpha(clr.Alpha, "Normal") 292 pdf.SetDrawColor(clr.R, clr.G, clr.B) 293 } 294 295 // Grid generates a graph-paperlike set of grid lines on the current page. 296 func (g GridType) Grid(pdf *Fpdf) { 297 var st StateType 298 var yLen, xLen int 299 var textSz, halfTextSz, yMin, yMax, xMin, xMax, yDiv, xDiv float64 300 var str string 301 var strOfs, strWd, tp, bt, lf, rt, drawX, drawY float64 302 303 xLen = len(g.xTicks) 304 yLen = len(g.yTicks) 305 if xLen > 1 && yLen > 1 { 306 307 st = StateGet(pdf) 308 309 line := func(x1, y1, x2, y2 float64, heavy bool) { 310 if heavy { 311 lineAttr(pdf, g.ClrMain, g.WdMain) 312 } else { 313 lineAttr(pdf, g.ClrSub, g.WdSub) 314 } 315 pdf.Line(x1, y1, x2, y2) 316 } 317 318 textSz = pdf.PointToUnitConvert(g.TextSize) 319 halfTextSz = textSz / 2 320 321 pdf.SetAutoPageBreak(false, 0) 322 pdf.SetFontUnitSize(textSz) 323 strOfs = pdf.GetStringWidth("0") 324 pdf.SetFillColor(255, 255, 255) 325 pdf.SetCellMargin(0) 326 327 xMin = g.xTicks[0] 328 xMax = g.xTicks[xLen-1] 329 330 yMin = g.yTicks[0] 331 yMax = g.yTicks[yLen-1] 332 333 lf = g.X(xMin) 334 rt = g.X(xMax) 335 bt = g.Y(yMin) 336 tp = g.Y(yMax) 337 338 // Verticals along X axis 339 xDiv = g.xTicks[1] - g.xTicks[0] 340 if g.XDiv > 0 { 341 xDiv = xDiv / float64(g.XDiv) 342 } 343 xDiv = g.Wd(xDiv) 344 for j, x := range g.xTicks { 345 drawX = g.X(x) 346 line(drawX, tp, drawX, bt, true) 347 if j < xLen-1 { 348 for k := 1; k < g.XDiv; k++ { 349 drawX += xDiv 350 line(drawX, tp, drawX, bt, false) 351 } 352 } 353 } 354 355 // Horizontals along Y axis 356 yDiv = g.yTicks[1] - g.yTicks[0] 357 if g.YDiv > 0 { 358 yDiv = yDiv / float64(g.YDiv) 359 } 360 yDiv = g.Ht(yDiv) 361 for j, y := range g.yTicks { 362 drawY = g.Y(y) 363 line(lf, drawY, rt, drawY, true) 364 if j < yLen-1 { 365 for k := 1; k < g.YDiv; k++ { 366 drawY += yDiv 367 line(lf, drawY, rt, drawY, false) 368 } 369 } 370 } 371 372 // X labels 373 if g.XTickStr != nil { 374 drawY = bt 375 for _, x := range g.xTicks { 376 str = g.XTickStr(x, g.xPrecision) 377 strWd = pdf.GetStringWidth(str) 378 drawX = g.X(x) 379 if g.XLabelRotate { 380 pdf.TransformBegin() 381 pdf.TransformRotate(90, drawX, drawY) 382 if g.XLabelIn { 383 pdf.SetXY(drawX+strOfs, drawY-halfTextSz) 384 } else { 385 pdf.SetXY(drawX-strOfs-strWd, drawY-halfTextSz) 386 } 387 pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "") 388 pdf.TransformEnd() 389 } else { 390 drawX -= strWd / 2.0 391 if g.XLabelIn { 392 pdf.SetXY(drawX, drawY-textSz-strOfs) 393 } else { 394 pdf.SetXY(drawX, drawY+strOfs) 395 } 396 pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "") 397 } 398 } 399 } 400 401 // Y labels 402 if g.YTickStr != nil { 403 drawX = lf 404 for _, y := range g.yTicks { 405 // str = strconv.FormatFloat(y, 'f', g.yPrecision, 64) 406 str = g.YTickStr(y, g.yPrecision) 407 strWd = pdf.GetStringWidth(str) 408 if g.YLabelIn { 409 pdf.SetXY(drawX+strOfs, g.Y(y)-halfTextSz) 410 } else { 411 pdf.SetXY(lf-strOfs-strWd, g.Y(y)-halfTextSz) 412 } 413 pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "") 414 } 415 } 416 417 // Restore drawing attributes 418 st.Put(pdf) 419 420 } 421 422 } 423 424 // Plot plots a series of count line segments from xMin to xMax. It repeatedly 425 // calls fnc(x) to retrieve the y value associate with x. The currently 426 // selected line drawing attributes are used. 427 func (g GridType) Plot(pdf *Fpdf, xMin, xMax float64, count int, fnc func(x float64) (y float64)) { 428 if count > 0 { 429 var x, delta, drawX0, drawY0, drawX1, drawY1 float64 430 delta = (xMax - xMin) / float64(count) 431 x = xMin 432 for j := 0; j <= count; j++ { 433 if j == 0 { 434 drawX1 = g.X(x) 435 drawY1 = g.Y(fnc(x)) 436 } else { 437 pdf.Line(drawX0, drawY0, drawX1, drawY1) 438 } 439 x += delta 440 drawX0 = drawX1 441 drawY0 = drawY1 442 drawX1 = g.X(x) 443 drawY1 = g.Y(fnc(x)) 444 } 445 } 446 }