From c4bf431472e0d2bac050f5a2c5ade09a50d55f2c Mon Sep 17 00:00:00 2001 From: Kurt Date: Mon, 19 Mar 2018 12:55:37 -0400 Subject: Add charting facility --- fpdf.go | 9 ++ fpdf_test.go | 52 +++++++++ grid.go | 377 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ label.go | 82 +++++++++++++ 4 files changed, 520 insertions(+) create mode 100644 grid.go create mode 100644 label.go diff --git a/fpdf.go b/fpdf.go index b8875a5..652469c 100644 --- a/fpdf.go +++ b/fpdf.go @@ -400,6 +400,15 @@ func (f *Fpdf) SetRightMargin(margin float64) { f.rMargin = margin } +// GetAutoPageBreak returns true if automatic pages breaks are enabled, false +// otherwise. This is followed by the triggering limit from the bottom of the +// page. This value applies only if automatic page breaks are enabled. +func (f *Fpdf) GetAutoPageBreak() (auto bool, margin float64) { + auto = f.autoPageBreak + margin = f.bMargin + return +} + // SetAutoPageBreak enables or disables the automatic page breaking mode. When // enabling, the second parameter is the distance from the bottom of the page // that defines the triggering limit. By default, the mode is on and the margin diff --git a/fpdf_test.go b/fpdf_test.go index c7cd582..8653c45 100644 --- a/fpdf_test.go +++ b/fpdf_test.go @@ -29,6 +29,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/jung-kurt/gofpdf" "github.com/jung-kurt/gofpdf/internal/example" @@ -2091,3 +2092,54 @@ func ExampleFpdf_RegisterAlias() { // Output: // Successfully generated pdf/Fpdf_RegisterAlias.pdf } + +// This example demonstrates the generation of graph grids. +func ExampleNewGrid() { + pdf := gofpdf.New("P", "mm", "A4", "") + pdf.SetFont("Arial", "", 12) + pdf.AddPage() + + gr := gofpdf.NewGrid(10, 10, 190, 133) + gr.TickmarksExtentX(0, 10, 4) + gr.TickmarksExtentY(0, 10, 3) + gr.Grid(pdf) + + gr = gofpdf.NewGrid(10, 154, 190, 133) + gr.TickmarksExtentX(0, 1, 12) + gr.XDiv = 5 + gr.TickmarksContainY(0, 1.1) + gr.YDiv = 20 + // Replace X label formatter with month abbreviation + gr.XTickStr = func(val float64, precision int) string { + return time.Month(math.Mod(val, 12) + 1).String()[0:3] + } + gr.Grid(pdf) + dot := func(x, y float64) { + pdf.Circle(gr.X(x), gr.Y(y), 0.5, "F") + } + pts := []float64{0.39, 0.457, 0.612, 0.84, 0.998, 1.037, 1.015, 0.918, 0.772, 0.659, 0.593, 0.164} + for month, val := range pts { + dot(float64(month)+0.5, val) + } + pdf.SetDrawColor(0, 0, 0) + pdf.SetLineWidth(0.1) + gr.Plot(pdf, 0.5, 11.5, 50, func(x float64) float64 { + // http://www.xuru.org/rt/PR.asp + return 0.227 * math.Exp(-0.0373*x*x+0.471*x) + }) + pdf.SetXY(gr.X(0.5), gr.Y(1.35)) + pdf.SetFontSize(14) + pdf.Write(0, "Solar energy (MWh) per month, 2016") + pdf.AddPage() + + gr = gofpdf.NewGrid(10, 10, 190, 277) + gr.TickmarksContainX(2.3, 3.4) + gr.TickmarksContainY(10.4, 56.8) + gr.Grid(pdf) + + fileStr := example.Filename("Fpdf_Grid") + err := pdf.OutputFileAndClose(fileStr) + example.Summary(err, fileStr) + // Output: + // Successfully generated pdf/Fpdf_Grid.pdf +} diff --git a/grid.go b/grid.go new file mode 100644 index 0000000..b9ff6ac --- /dev/null +++ b/grid.go @@ -0,0 +1,377 @@ +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 StatePut(pdf *Fpdf, st StateType) { + 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 + StatePut(pdf, st) + + } + +} + +// Plot plots a series of count line segments from xMin to xMax. It repeatedly +// calls fnc(x) to retreive 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)) + } + } +} diff --git a/label.go b/label.go new file mode 100644 index 0000000..b90d8f3 --- /dev/null +++ b/label.go @@ -0,0 +1,82 @@ +package gofpdf + +// Adapted from Nice Numbers for Graph Labels by Paul Heckbert from "Graphics +// Gems", Academic Press, 1990 + +// Paul Heckbert 2 Dec 88 + +// https://github.com/erich666/GraphicsGems + +// LICENSE + +// This code repository predates the concept of Open Source, and predates most +// licenses along such lines. As such, the official license truly is: + +// EULA: The Graphics Gems code is copyright-protected. In other words, you +// cannot claim the text of the code as your own and resell it. Using the code +// is permitted in any program, product, or library, non-commercial or +// commercial. Giving credit is not required, though is a nice gesture. The +// code comes as-is, and if there are any flaws or problems with any Gems code, +// nobody involved with Gems - authors, editors, publishers, or webmasters - +// are to be held responsible. Basically, don't be a jerk, and remember that +// anything free comes with no guarantee. + +import ( + "math" +) + +// niceNum returns a "nice" number approximately equal to x. The number is +// rounded if round is true, converted to its ceiling otherwise. +func niceNum(val float64, round bool) float64 { + var nf float64 + + exp := int(math.Floor(math.Log10(val))) + f := val / math.Pow10(exp) + if round { + switch { + case f < 1.5: + nf = 1 + case f < 3.0: + nf = 2 + case f < 7.0: + nf = 5 + default: + nf = 10 + } + } else { + switch { + case f <= 1: + nf = 1 + case f <= 2.0: + nf = 2 + case f <= 5.0: + nf = 5 + default: + nf = 10 + } + } + return nf * math.Pow10(exp) +} + +// TickmarkPrecision returns an appropriate precision value for label +// formatting. +func TickmarkPrecision(div float64) int { + return int(math.Max(-math.Floor(math.Log10(div)), 0)) +} + +// Tickmarks returns a slice of tickmarks appropriate for a chart axis and an +// appropriate precision for formatting purposes. The values min and max will +// be contained within the tickmark range. +func Tickmarks(min, max float64) (list []float64, precision int) { + if max > min { + spread := niceNum(max-min, false) + d := niceNum((spread / 4), true) + graphMin := math.Floor(min/d) * d + graphMax := math.Ceil(max/d) * d + precision = TickmarkPrecision(d) + for x := graphMin; x < graphMax+0.5*d; x += d { + list = append(list, x) + } + } + return +} -- cgit v1.2.1-24-ge1ad