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  }