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