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  }