From c4bf431472e0d2bac050f5a2c5ade09a50d55f2c Mon Sep 17 00:00:00 2001 From: Kurt Date: Mon, 19 Mar 2018 12:55:37 -0400 Subject: Add charting facility --- grid.go | 377 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 grid.go (limited to 'grid.go') 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)) + } + } +} -- cgit v1.2.1-24-ge1ad