diff options
| -rw-r--r-- | fpdf.go | 9 | ||||
| -rw-r--r-- | fpdf_test.go | 52 | ||||
| -rw-r--r-- | grid.go | 377 | ||||
| -rw-r--r-- | label.go | 82 | 
4 files changed, 520 insertions, 0 deletions
| @@ -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 +} @@ -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 +} | 
