123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- package gofpdf
- import (
- "math"
- "strconv"
- )
- func unused(args ...interface{}) {
- }
- // RGBType holds fields for red, green and blue color components (0..255)
- type RGBType struct {
- R, G, B int
- }
- // RGBAType holds fields for red, green and blue color components (0..255) and
- // an alpha transparency value (0..1)
- type RGBAType struct {
- R, G, B int
- Alpha float64
- }
- // StateType holds various commonly used drawing values for convenient
- // retrieval (StateGet()) and restore (Put) 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
- }
- // Put 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
- // Labels are inside of graph boundary
- XLabelIn, YLabelIn bool
- // Labels on X-axis should be rotated
- XLabelRotate bool
- // 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.XLabelIn = false
- grid.YLabelIn = false
- grid.XLabelRotate = false
- 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
- }
- // WdAbs returns the absolute value of dataWd, specified in logical data units,
- // that has been converted to the unit of measure specified in New().
- func (g GridType) WdAbs(dataWd float64) float64 {
- return math.Abs(g.xm * dataWd)
- }
- // 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
- }
- // XY converts dataX and dataY, specified in logical data units, to the X and Y
- // position on the current page.
- func (g GridType) XY(dataX, dataY float64) (x, y float64) {
- return g.xm*dataX + g.xb, g.ym*dataY + g.yb
- }
- // Pos returns the point, in page units, indicated by the relative positions
- // xRel and yRel. These are values between 0 and 1. xRel specifies the relative
- // position between the grid's left and right edges. yRel specifies the
- // relative position between the grid's bottom and top edges.
- func (g GridType) Pos(xRel, yRel float64) (x, y float64) {
- x = g.w*xRel + g.x
- y = g.h*(1-yRel) + g.y
- return
- }
- // 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
- }
- // HtAbs returns the absolute value of dataHt, specified in logical data units,
- // that has been converted to the unit of measure specified in New().
- func (g GridType) HtAbs(dataHt float64) float64 {
- return math.Abs(g.ym * dataHt)
- }
- // 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
- }
- // XRange returns the minimum and maximum values for the current tickmark
- // sequence. These correspond to the data values of the graph's left and right
- // edges.
- func (g GridType) XRange() (min, max float64) {
- min = g.xTicks[0]
- max = g.xTicks[len(g.xTicks)-1]
- return
- }
- // YRange returns the minimum and maximum values for the current tickmark
- // sequence. These correspond to the data values of the graph's bottom and top
- // edges.
- func (g GridType) YRange() (min, max float64) {
- min = g.yTicks[0]
- max = g.yTicks[len(g.yTicks)-1]
- return
- }
- // 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
- 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
- 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)
- }
- textSz = pdf.PointToUnitConvert(g.TextSize)
- halfTextSz = textSz / 2
- pdf.SetAutoPageBreak(false, 0)
- pdf.SetFontUnitSize(textSz)
- strOfs = pdf.GetStringWidth("0")
- 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
- for _, x := range g.xTicks {
- str = g.XTickStr(x, g.xPrecision)
- strWd = pdf.GetStringWidth(str)
- drawX = g.X(x)
- if g.XLabelRotate {
- pdf.TransformBegin()
- pdf.TransformRotate(90, drawX, drawY)
- if g.XLabelIn {
- pdf.SetXY(drawX+strOfs, drawY-halfTextSz)
- } else {
- pdf.SetXY(drawX-strOfs-strWd, drawY-halfTextSz)
- }
- pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "")
- pdf.TransformEnd()
- } else {
- drawX -= strWd / 2.0
- if g.XLabelIn {
- pdf.SetXY(drawX, drawY-textSz-strOfs)
- } else {
- pdf.SetXY(drawX, drawY+strOfs)
- }
- pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "")
- }
- }
- }
- // Y labels
- if g.YTickStr != nil {
- drawX = lf
- for _, y := range g.yTicks {
- // str = strconv.FormatFloat(y, 'f', g.yPrecision, 64)
- str = g.YTickStr(y, g.yPrecision)
- strWd = pdf.GetStringWidth(str)
- if g.YLabelIn {
- pdf.SetXY(drawX+strOfs, g.Y(y)-halfTextSz)
- } else {
- pdf.SetXY(lf-strOfs-strWd, 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))
- }
- }
- }
|