package gofpdf import ( "strconv" ) func unused(args ...interface{}) { } // RGBType holds fields for red, green and blue color components type RGBType struct { R, G, B int } // RGBAType holds fields for red, green and blue color components and an alpha // transparency value type RGBAType struct { R, G, B int Alpha float64 } // StateType holds various commonly used drawing values for convenient // retrieval and restore methods type StateType struct { clrDraw, clrText, clrFill RGBType lineWd float64 fontSize float64 alpha float64 blendStr string cellMargin float64 } // StateGet returns a variable that contains common state values. func StateGet(pdf *Fpdf) (st StateType) { st.clrDraw.R, st.clrDraw.G, st.clrDraw.B = pdf.GetDrawColor() st.clrFill.R, st.clrFill.G, st.clrFill.B = pdf.GetFillColor() st.clrText.R, st.clrText.G, st.clrText.B = pdf.GetTextColor() st.lineWd = pdf.GetLineWidth() _, st.fontSize = pdf.GetFontSize() st.alpha, st.blendStr = pdf.GetAlpha() st.cellMargin = pdf.GetCellMargin() return } // StatePut sets the common state values contained in the state structure // specified by st. func (st StateType) Put(pdf *Fpdf) { pdf.SetDrawColor(st.clrDraw.R, st.clrDraw.G, st.clrDraw.B) pdf.SetFillColor(st.clrFill.R, st.clrFill.G, st.clrFill.B) pdf.SetTextColor(st.clrText.R, st.clrText.G, st.clrText.B) pdf.SetLineWidth(st.lineWd) pdf.SetFontUnitSize(st.fontSize) pdf.SetAlpha(st.alpha, st.blendStr) pdf.SetCellMargin(st.cellMargin) } // TickFormatFncType defines a callback for label drawing. type TickFormatFncType func(val float64, precision int) string // defaultFormatter returns the string form of val with precision decimal places. func defaultFormatter(val float64, precision int) string { return strconv.FormatFloat(val, 'f', precision, 64) } // GridType assists with the generation of graphs. It allows the application to // work with logical data coordinates rather than page coordinates and assists // with the drawing of a background grid. type GridType struct { // Chart coordinates in page units x, y, w, h float64 // X, Y, Wd, Ht float64 // Slopes and intercepts scale data points to graph coordinates linearly xm, xb, ym, yb float64 // Tickmarks xTicks, yTicks []float64 // Formatters; use nil to eliminate labels XTickStr, YTickStr TickFormatFncType // Subdivisions between tickmarks XDiv, YDiv int // Formatting precision xPrecision, yPrecision int // Line and label colors ClrText, ClrMain, ClrSub RGBAType // Line thickness WdMain, WdSub float64 // Label height in points TextSize float64 } // linear returns the slope and y-intercept of the straight line joining the // two specified points. For scaling purposes, associate the arguments as // follows: x1: observed low value, y1: desired low value, x2: observed high // value, y2: desired high value. func linear(x1, y1, x2, y2 float64) (slope, intercept float64) { if x2 != x1 { slope = (y2 - y1) / (x2 - x1) intercept = y2 - x2*slope } return } // linearTickmark returns the slope and intercept that will linearly map data // values (the range of which is specified by the tickmark slice tm) to page // values (the range of which is specified by lo and hi). func linearTickmark(tm []float64, lo, hi float64) (slope, intercept float64) { ln := len(tm) if ln > 0 { slope, intercept = linear(tm[0], lo, tm[ln-1], hi) } return } // NewGrid returns a variable of type GridType that is initialized to draw on a // rectangle of width w and height h with the upper left corner positioned at // point (x, y). The coordinates are in page units, that is, the same as those // specified in New(). // // The returned variable is initialized with a very simple default tickmark // layout that ranges from 0 to 1 in both axes. Prior to calling Grid(), the // application may establish a more suitable tickmark layout by calling the // methods TickmarksContainX() and TickmarksContainY(). These methods bound the // data range with appropriate boundaries and divisions. Alternatively, if the // exact extent and divisions of the tickmark layout are known, the methods // TickmarksExtentX() and TickmarksExtentY may be called instead. func NewGrid(x, y, w, h float64) (grid GridType) { grid.x = x grid.y = y grid.w = w grid.h = h grid.TextSize = 7 // Points grid.TickmarksExtentX(0, 1, 1) grid.TickmarksExtentY(0, 1, 1) grid.XDiv = 10 grid.YDiv = 10 grid.ClrText = RGBAType{R: 0, G: 0, B: 0, Alpha: 1} grid.ClrMain = RGBAType{R: 128, G: 160, B: 128, Alpha: 1} grid.ClrSub = RGBAType{R: 192, G: 224, B: 192, Alpha: 1} grid.WdMain = 0.1 grid.WdSub = 0.1 grid.YTickStr = defaultFormatter grid.XTickStr = defaultFormatter return } // Wd converts dataWd, specified in logical data units, to the unit of measure // specified in New(). func (g GridType) Wd(dataWd float64) float64 { return g.xm * dataWd } // X converts dataX, specified in logical data units, to the X position on the // current page. func (g GridType) X(dataX float64) float64 { return g.xm*dataX + g.xb } // Ht converts dataHt, specified in logical data units, to the unit of measure // specified in New(). func (g GridType) Ht(dataHt float64) float64 { return g.ym * dataHt } // Y converts dataY, specified in logical data units, to the Y position on the // current page. func (g GridType) Y(dataY float64) float64 { return g.ym*dataY + g.yb } // TickmarksContainX sets the tickmarks to be shown by Grid() in the horizontal // dimension. The argument min and max specify the minimum and maximum values // to be contained within the grid. The tickmark values that are generated are // suitable for general purpose graphs. // // See TickmarkExtentX() for an alternative to this method to be used when the // exact values of the tickmarks are to be set by the application. func (g *GridType) TickmarksContainX(min, max float64) { g.xTicks, g.xPrecision = Tickmarks(min, max) g.xm, g.xb = linearTickmark(g.xTicks, g.x, g.x+g.w) } // TickmarksContainY sets the tickmarks to be shown by Grid() in the vertical // dimension. The argument min and max specify the minimum and maximum values // to be contained within the grid. The tickmark values that are generated are // suitable for general purpose graphs. // // See TickmarkExtentY() for an alternative to this method to be used when the // exact values of the tickmarks are to be set by the application. func (g *GridType) TickmarksContainY(min, max float64) { g.yTicks, g.yPrecision = Tickmarks(min, max) g.ym, g.yb = linearTickmark(g.yTicks, g.y+g.h, g.y) } func extent(min, div float64, count int) (tm []float64, precision int) { tm = make([]float64, count+1) for j := 0; j <= count; j++ { tm[j] = min min += div } precision = TickmarkPrecision(div) return } // TickmarksExtentX sets the tickmarks to be shown by Grid() in the horizontal // dimension. count specifies number of major tickmark subdivisions to be // graphed. min specifies the leftmost data value. div specifies, in data // units, the extent of each major tickmark subdivision. // // See TickmarkContainX() for an alternative to this method to be used when // viewer-friendly tickmarks are to be determined automatically. func (g *GridType) TickmarksExtentX(min, div float64, count int) { g.xTicks, g.xPrecision = extent(min, div, count) g.xm, g.xb = linearTickmark(g.xTicks, g.x, g.x+g.w) } // TickmarksExtentY sets the tickmarks to be shown by Grid() in the vertical // dimension. count specifies number of major tickmark subdivisions to be // graphed. min specifies the bottommost data value. div specifies, in data // units, the extent of each major tickmark subdivision. // // See TickmarkContainY() for an alternative to this method to be used when // viewer-friendly tickmarks are to be determined automatically. func (g *GridType) TickmarksExtentY(min, div float64, count int) { g.yTicks, g.yPrecision = extent(min, div, count) g.ym, g.yb = linearTickmark(g.yTicks, g.y+g.h, g.y) } // func (g *GridType) SetXExtent(dataLf, paperLf, dataRt, paperRt float64) { // g.xm, g.xb = linear(dataLf, paperLf, dataRt, paperRt) // } // func (g *GridType) SetYExtent(dataTp, paperTp, dataBt, paperBt float64) { // g.ym, g.yb = linear(dataTp, paperTp, dataBt, paperBt) // } func lineAttr(pdf *Fpdf, clr RGBAType, lineWd float64) { pdf.SetLineWidth(lineWd) pdf.SetAlpha(clr.Alpha, "Normal") pdf.SetDrawColor(clr.R, clr.G, clr.B) } // Grid generates a graph-paperlike set of grid lines on the current page. func (g GridType) Grid(pdf *Fpdf) { var st StateType // const textSz = 8 // var halfTextSz = g.TextSize / 2 var yLen, xLen int var textSz, halfTextSz, yMin, yMax, xMin, xMax, yDiv, xDiv float64 var str string var strOfs, strWd, tp, bt, lf, rt, drawX, drawY float64 textSz = pdf.PointToUnitConvert(g.TextSize) halfTextSz = textSz / 2 strOfs = pdf.GetStringWidth("I") unused(strOfs) xLen = len(g.xTicks) yLen = len(g.yTicks) if xLen > 1 && yLen > 1 { st = StateGet(pdf) line := func(x1, y1, x2, y2 float64, heavy bool) { if heavy { lineAttr(pdf, g.ClrMain, g.WdMain) } else { lineAttr(pdf, g.ClrSub, g.WdSub) } pdf.Line(x1, y1, x2, y2) } pdf.SetAutoPageBreak(false, 0) pdf.SetFontUnitSize(textSz) pdf.SetFillColor(255, 255, 255) pdf.SetCellMargin(0) xMin = g.xTicks[0] xMax = g.xTicks[xLen-1] yMin = g.yTicks[0] yMax = g.yTicks[yLen-1] lf = g.X(xMin) rt = g.X(xMax) bt = g.Y(yMin) tp = g.Y(yMax) // Verticals along X axis xDiv = g.xTicks[1] - g.xTicks[0] if g.XDiv > 0 { xDiv = xDiv / float64(g.XDiv) } xDiv = g.Wd(xDiv) for j, x := range g.xTicks { drawX = g.X(x) line(drawX, tp, drawX, bt, true) if j < xLen-1 { for k := 1; k < g.XDiv; k++ { drawX += xDiv line(drawX, tp, drawX, bt, false) } } } // Horizontals along Y axis yDiv = g.yTicks[1] - g.yTicks[0] if g.YDiv > 0 { yDiv = yDiv / float64(g.YDiv) } yDiv = g.Ht(yDiv) for j, y := range g.yTicks { drawY = g.Y(y) line(lf, drawY, rt, drawY, true) if j < yLen-1 { for k := 1; k < g.YDiv; k++ { drawY += yDiv line(lf, drawY, rt, drawY, false) } } } // X labels if g.XTickStr != nil { drawY = bt // g.Y(yMin) for _, x := range g.xTicks { str = g.XTickStr(x, g.xPrecision) // strconv.FormatFloat(x, 'f', g.xPrecision, 64) strWd = pdf.GetStringWidth(str) drawX = g.X(x) pdf.TransformBegin() pdf.TransformRotate(90, drawX, drawY) pdf.SetXY(drawX+strOfs, drawY-halfTextSz) pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "") pdf.TransformEnd() } } // Y labels if g.YTickStr != nil { drawX = lf + strOfs // g.X(xMin) + strOfs for _, y := range g.yTicks { // str = strconv.FormatFloat(y, 'f', g.yPrecision, 64) str = g.YTickStr(y, g.yPrecision) strWd = pdf.GetStringWidth(str) pdf.SetXY(drawX, g.Y(y)-halfTextSz) pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "") } } // Restore drawing attributes st.Put(pdf) } } // Plot plots a series of count line segments from xMin to xMax. It repeatedly // calls fnc(x) to retrieve the y value associate with x. The currently // selected line drawing attributes are used. func (g GridType) Plot(pdf *Fpdf, xMin, xMax float64, count int, fnc func(x float64) (y float64)) { if count > 0 { var x, delta, drawX0, drawY0, drawX1, drawY1 float64 delta = (xMax - xMin) / float64(count) x = xMin for j := 0; j <= count; j++ { if j == 0 { drawX1 = g.X(x) drawY1 = g.Y(fnc(x)) } else { pdf.Line(drawX0, drawY0, drawX1, drawY1) } x += delta drawX0 = drawX1 drawY0 = drawY1 drawX1 = g.X(x) drawY1 = g.Y(fnc(x)) } } }