summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKurt <kurt.w.jung@gmail.com>2018-03-19 12:55:37 -0400
committerKurt <kurt.w.jung@gmail.com>2018-03-19 12:55:37 -0400
commitc4bf431472e0d2bac050f5a2c5ade09a50d55f2c (patch)
tree9e19b2320b369021a2df61586c74e3a008188942
parent55d415070ffee71e28bcef384221384bd2d582ca (diff)
Add charting facility
-rw-r--r--fpdf.go9
-rw-r--r--fpdf_test.go52
-rw-r--r--grid.go377
-rw-r--r--label.go82
4 files changed, 520 insertions, 0 deletions
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
+}