diff --git a/axis.go b/axis.go
index 8b3d088..ee2f055 100644
--- a/axis.go
+++ b/axis.go
@@ -3,8 +3,6 @@ package charts
import (
"math"
"strings"
-
- "github.com/go-analyze/charts/chartdraw"
)
type axisPainter struct {
@@ -97,23 +95,7 @@ func (a *axisPainter) Render() (Box, error) {
tickLength := getDefaultInt(opt.TickLength, 5)
labelMargin := getDefaultInt(opt.LabelMargin, 5)
- style := chartdraw.Style{
- StrokeColor: theme.GetAxisStrokeColor(),
- StrokeWidth: strokeWidth,
- FontStyle: fontStyle,
- }
- top.SetDrawingStyle(style)
- top.OverrideFontStyle(style.FontStyle)
-
- isTextRotation := opt.TextRotation != 0
-
- if isTextRotation {
- top.setTextRotation(opt.TextRotation)
- }
- textMaxWidth, textMaxHeight := top.measureTextMaxWidthHeight(opt.Data)
- if isTextRotation {
- top.clearTextRotation()
- }
+ textMaxWidth, textMaxHeight := top.measureTextMaxWidthHeight(opt.Data, fontStyle, opt.TextRotation)
width := 0
height := 0
@@ -227,22 +209,25 @@ func (a *axisPainter) Render() (Box, error) {
}
if strokeWidth > 0 {
+ strokeColor := theme.GetAxisStrokeColor()
p.Child(PainterPaddingOption(Box{
Top: ticksPaddingTop,
Left: ticksPaddingLeft,
IsSet: true,
})).ticks(ticksOption{
- labelCount: labelCount,
- tickCount: tickCount,
- tickSpaces: tickSpaces,
- length: tickLength,
- vertical: isVertical,
- firstIndex: opt.DataStartIndex,
+ labelCount: labelCount,
+ tickCount: tickCount,
+ tickSpaces: tickSpaces,
+ length: tickLength,
+ vertical: isVertical,
+ firstIndex: opt.DataStartIndex,
+ strokeWidth: strokeWidth,
+ strokeColor: strokeColor,
})
p.LineStroke([]Point{
{X: x0, Y: y0},
{X: x1, Y: y1},
- })
+ }, strokeColor, strokeWidth)
}
p.Child(PainterPaddingOption(Box{
@@ -254,6 +239,7 @@ func (a *axisPainter) Render() (Box, error) {
firstIndex: opt.DataStartIndex,
align: textAlign,
textList: opt.Data,
+ fontStyle: fontStyle,
vertical: isVertical,
labelCount: labelCount,
tickCount: tickCount,
@@ -264,9 +250,6 @@ func (a *axisPainter) Render() (Box, error) {
})
if opt.SplitLineShow { // show auxiliary lines
- style.StrokeColor = theme.GetAxisSplitLineColor()
- style.StrokeWidth = 1
- top.OverrideDrawingStyle(style)
if isVertical {
x0 := p.Width()
x1 := top.Width()
@@ -280,7 +263,7 @@ func (a *axisPainter) Render() (Box, error) {
top.LineStroke([]Point{
{X: x0, Y: y},
{X: x1, Y: y},
- })
+ }, theme.GetAxisSplitLineColor(), 1)
}
} else {
y0 := p.Height() - defaultXAxisHeight
@@ -293,7 +276,7 @@ func (a *axisPainter) Render() (Box, error) {
top.LineStroke([]Point{
{X: x, Y: y0},
{X: x, Y: y1},
- })
+ }, theme.GetAxisSplitLineColor(), 1)
}
}
}
diff --git a/axis_test.go b/axis_test.go
index c570ba8..d975add 100644
--- a/axis_test.go
+++ b/axis_test.go
@@ -37,7 +37,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "x-axis_bottom_left",
@@ -48,7 +48,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "y-axis_left",
@@ -59,7 +59,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "y-axis_center",
@@ -72,7 +72,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "y-axis_right",
@@ -85,7 +85,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "top",
@@ -97,7 +97,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "reduced_label_count",
@@ -109,7 +109,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "custom_unit",
@@ -121,7 +121,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "custom_font",
@@ -135,7 +135,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_disable",
@@ -146,7 +146,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_enable",
@@ -157,7 +157,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "",
+ result: "",
},
}
diff --git a/bar_chart.go b/bar_chart.go
index f3efbad..3f5faf9 100644
--- a/bar_chart.go
+++ b/bar_chart.go
@@ -5,8 +5,6 @@ import (
"math"
"github.com/golang/freetype/truetype"
-
- "github.com/go-analyze/charts/chartdraw"
)
type barChart struct {
@@ -108,12 +106,8 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
}
h := yRange.getHeight(item)
- fillColor := seriesColor
top := barMaxHeight - h
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: fillColor,
- })
if flagIs(true, opt.RoundedBarCaps) {
seriesPainter.roundedRect(Box{
Top: top,
@@ -121,7 +115,8 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
Right: x + barWidth,
Bottom: barMaxHeight - 1,
IsSet: true,
- }, barWidth, true, false)
+ }, barWidth, true, false,
+ seriesColor, seriesColor, 0.0)
} else {
seriesPainter.filledRect(Box{
Top: top,
@@ -129,7 +124,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
Right: x + barWidth,
Bottom: barMaxHeight - 1,
IsSet: true,
- })
+ }, seriesColor, seriesColor, 0.0)
}
// generate marker point by hand
points[j] = Point{
@@ -147,7 +142,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
y = barMaxHeight
radians = -math.Pi / 2
if fontStyle.FontColor.IsZero() {
- if isLightColor(fillColor) {
+ if isLightColor(seriesColor) {
fontStyle.FontColor = defaultLightFontColor
} else {
fontStyle.FontColor = defaultDarkFontColor
diff --git a/bar_chart_test.go b/bar_chart_test.go
index 654d89f..4c36189 100644
--- a/bar_chart_test.go
+++ b/bar_chart_test.go
@@ -53,13 +53,13 @@ func TestBarChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicBarChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicBarChartOption,
- result: "",
+ result: "",
},
{
name: "rounded_caps",
@@ -69,7 +69,7 @@ func TestBarChart(t *testing.T) {
opt.RoundedBarCaps = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "custom_font",
@@ -85,7 +85,7 @@ func TestBarChart(t *testing.T) {
opt.Title.FontStyle = customFont
return opt
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_enable",
@@ -95,7 +95,7 @@ func TestBarChart(t *testing.T) {
opt.XAxis.BoundaryGap = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_disable",
@@ -105,7 +105,7 @@ func TestBarChart(t *testing.T) {
opt.XAxis.BoundaryGap = False()
return opt
},
- result: "",
+ result: "",
},
}
diff --git a/chart_option_test.go b/chart_option_test.go
index 467f1f3..e670ad8 100644
--- a/chart_option_test.go
+++ b/chart_option_test.go
@@ -123,7 +123,7 @@ func TestLineRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestBarRender(t *testing.T) {
@@ -160,7 +160,7 @@ func TestBarRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestHorizontalBarRender(t *testing.T) {
@@ -190,7 +190,7 @@ func TestHorizontalBarRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestPieRender(t *testing.T) {
@@ -222,7 +222,7 @@ func TestPieRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestRadarRender(t *testing.T) {
@@ -253,7 +253,7 @@ func TestRadarRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestFunnelRender(t *testing.T) {
@@ -273,7 +273,7 @@ func TestFunnelRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestChildRender(t *testing.T) {
@@ -310,5 +310,5 @@ func TestChildRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
diff --git a/chartdraw/drawing/color.go b/chartdraw/drawing/color.go
index 7c2c32d..8e23c6d 100644
--- a/chartdraw/drawing/color.go
+++ b/chartdraw/drawing/color.go
@@ -12,36 +12,71 @@ import (
var (
// ColorTransparent is a fully transparent color.
ColorTransparent = Color{R: 255, G: 255, B: 255, A: 0}
- // ColorWhite is white.
+ // ColorWhite is R: 255, G: 255, B: 255.
ColorWhite = Color{R: 255, G: 255, B: 255, A: 255}
- // ColorBlack is black.
+ // ColorBlack is R: 0, G: 0, B: 0.
ColorBlack = Color{R: 0, G: 0, B: 0, A: 255}
- // ColorRed is red.
+ // ColorGray is R: 128, G: 128, B: 128,
+ ColorGray = Color{R: 128, G: 128, B: 128, A: 255}
+ // ColorRed is R: 255, G: 0, B: 0.
ColorRed = Color{R: 255, G: 0, B: 0, A: 255}
- // ColorGreen is green.
+ // ColorGreen is R: 0, G: 128, B: 0.
ColorGreen = Color{R: 0, G: 128, B: 0, A: 255}
- // ColorBlue is blue.
+ // ColorBlue is R: 0, G: 0, B: 255.
ColorBlue = Color{R: 0, G: 0, B: 255, A: 255}
- // ColorSilver is a known color.
+ // ColorSilver is R: 192, G: 192, B: 192.
ColorSilver = Color{R: 192, G: 192, B: 192, A: 255}
- // ColorMaroon is a known color.
+ // ColorMaroon is R: 128, G: 0, B: 0.
ColorMaroon = Color{R: 128, G: 0, B: 0, A: 255}
- // ColorPurple is a known color.
+ // ColorPurple is R: 128, G: 0, B: 128.
ColorPurple = Color{R: 128, G: 0, B: 128, A: 255}
- // ColorFuchsia is a known color.
+ // ColorFuchsia is R: 255, G: 0, B: 255.
ColorFuchsia = Color{R: 255, G: 0, B: 255, A: 255}
- // ColorLime is a known color.
+ // ColorLime is R: 0, G: 255, B: 0.
ColorLime = Color{R: 0, G: 255, B: 0, A: 255}
- // ColorOlive is a known color.
+ // ColorOlive is R: 128, G: 128, B: 0.
ColorOlive = Color{R: 128, G: 128, B: 0, A: 255}
- // ColorYellow is a known color.
+ // ColorYellow is R: 255, G: 255, B: 0.
ColorYellow = Color{R: 255, G: 255, B: 0, A: 255}
- // ColorNavy is a known color.
+ // ColorNavy is R: 0, G: 0, B: 128.
ColorNavy = Color{R: 0, G: 0, B: 128, A: 255}
- // ColorTeal is a known color.
+ // ColorTeal is R: 0, G: 128, B: 128.
ColorTeal = Color{R: 0, G: 128, B: 128, A: 255}
- // ColorAqua is a known color.
+ // ColorAqua is R: 0, G: 255, B: 255.
ColorAqua = Color{R: 0, G: 255, B: 255, A: 255}
+
+ // select extended colors
+
+ // ColorAzure is R: 240, G: 255, B: 255.
+ ColorAzure = Color{R: 240, G: 255, B: 255, A: 255}
+ // ColorBeige is R: 245, G: 245, B: 220.
+ ColorBeige = Color{R: 245, G: 245, B: 220, A: 255}
+ // ColorBrown is R: 165, G: 42, B: 42.
+ ColorBrown = Color{R: 165, G: 42, B: 42, A: 255}
+ // ColorChocolate is R: 210, G: 105, B: 30.
+ ColorChocolate = Color{R: 210, G: 105, B: 30, A: 255}
+ // ColorCoral is R: 255, G: 127, B: 80.
+ ColorCoral = Color{R: 255, G: 127, B: 80, A: 255}
+ // ColorGold is R: 255, G: 215, B: 0.
+ ColorGold = Color{R: 255, G: 215, B: 0, A: 255}
+ // ColorIndigo is R: 75, G: 0, B: 130.
+ ColorIndigo = Color{R: 75, G: 0, B: 130, A: 255}
+ // ColorIvory is R: 255, G: 255, B: 250.
+ ColorIvory = Color{R: 255, G: 255, B: 250, A: 255}
+ // ColorOrange is R: 255, G: 165, B: 0.
+ ColorOrange = Color{R: 255, G: 165, B: 0, A: 255}
+ // ColorPink is R: 255, G: 192, B: 203.
+ ColorPink = Color{R: 255, G: 192, B: 203, A: 255}
+ // ColorPlum is R: 221, G: 160, B: 221.
+ ColorPlum = Color{R: 221, G: 160, B: 221, A: 255}
+ // ColorSalmon is R: 250, G: 128, B: 114.
+ ColorSalmon = Color{R: 250, G: 128, B: 114, A: 255}
+ // ColorTan is R: 210, G: 180, B: 140.
+ ColorTan = Color{R: 210, G: 180, B: 140, A: 255}
+ // ColorTurquoise is R: 64, G: 224, B: 208.
+ ColorTurquoise = Color{R: 64, G: 224, B: 208, A: 255}
+ // ColorViolet is R: 238, G: 130, B: 238.
+ ColorViolet = Color{R: 238, G: 130, B: 238, A: 255}
)
// ParseColor parses a color from a string.
@@ -129,6 +164,8 @@ func ColorFromKnown(known string) Color {
return ColorWhite
case "black":
return ColorBlack
+ case "grey", "gray":
+ return ColorGray
case "red":
return ColorRed
case "blue":
@@ -153,8 +190,38 @@ func ColorFromKnown(known string) Color {
return ColorNavy
case "teal":
return ColorTeal
- case "aqua":
+ case "cyan", "aqua":
return ColorAqua
+ case "azure":
+ return ColorAzure
+ case "beige":
+ return ColorBeige
+ case "brown":
+ return ColorBrown
+ case "chocolate":
+ return ColorChocolate
+ case "coral":
+ return ColorCoral
+ case "gold":
+ return ColorGold
+ case "indigo":
+ return ColorIndigo
+ case "ivory":
+ return ColorIvory
+ case "orange":
+ return ColorOrange
+ case "pink":
+ return ColorPink
+ case "plum":
+ return ColorPlum
+ case "salmon":
+ return ColorSalmon
+ case "tan":
+ return ColorTan
+ case "turquoise":
+ return ColorTurquoise
+ case "violet":
+ return ColorViolet
default:
return Color{}
}
@@ -235,6 +302,53 @@ func (c Color) AverageWith(other Color) Color {
// String returns a css string representation of the color.
func (c Color) String() string {
+ switch c {
+ case ColorWhite:
+ return "white"
+ case ColorBlack:
+ return "black"
+ case ColorRed:
+ return "red"
+ case ColorBlue:
+ return "blue"
+ case ColorGreen:
+ return "green"
+ case ColorSilver:
+ return "silver"
+ case ColorMaroon:
+ return "maroon"
+ case ColorPurple:
+ return "purple"
+ case ColorFuchsia:
+ return "fuchsia"
+ case ColorLime:
+ return "lime"
+ case ColorOlive:
+ return "olive"
+ case ColorYellow:
+ return "yellow"
+ case ColorNavy:
+ return "navy"
+ case ColorTeal:
+ return "teal"
+ case ColorAqua:
+ return "aqua"
+ default:
+ if c.A == 255 {
+ return c.StringRGB()
+ } else {
+ return c.StringRGBA()
+ }
+ }
+}
+
+// StringRGB returns a css RGB string representation of the color.
+func (c Color) StringRGB() string {
+ return fmt.Sprintf("rgb(%v,%v,%v)", c.R, c.G, c.B)
+}
+
+// StringRGBA returns a css RGBA string representation of the color.
+func (c Color) StringRGBA() string {
fa := float64(c.A) / float64(255)
return fmt.Sprintf("rgba(%v,%v,%v,%.1f)", c.R, c.G, c.B, fa)
}
diff --git a/chartdraw/raster_renderer.go b/chartdraw/raster_renderer.go
index b24b43e..0dc549b 100644
--- a/chartdraw/raster_renderer.go
+++ b/chartdraw/raster_renderer.go
@@ -150,6 +150,9 @@ func (rr *rasterRenderer) SetFontColor(c drawing.Color) {
// Text implements the interface method.
func (rr *rasterRenderer) Text(body string, x, y int) {
+ if body == "" {
+ return
+ }
xf, yf := rr.getCoords(x, y)
rr.gc.SetFont(rr.s.Font)
rr.gc.SetFontSize(rr.s.FontSize)
diff --git a/chartdraw/style.go b/chartdraw/style.go
index 9049d63..409f55e 100644
--- a/chartdraw/style.go
+++ b/chartdraw/style.go
@@ -385,9 +385,7 @@ func (s Style) WriteToRenderer(r Renderer) {
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetStrokeDashArray(s.GetStrokeDashArray())
r.SetFillColor(s.GetFillColor())
- r.SetFont(s.GetFont())
- r.SetFontColor(s.GetFontColor())
- r.SetFontSize(s.GetFontSize())
+ s.FontStyle.WriteTextOptionsToRenderer(r)
r.ClearTextRotation()
if s.GetTextRotationDegrees() != 0 {
diff --git a/chartdraw/vector_renderer.go b/chartdraw/vector_renderer.go
index cd3d4b9..58f028d 100644
--- a/chartdraw/vector_renderer.go
+++ b/chartdraw/vector_renderer.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"math"
+ "strconv"
"strings"
"golang.org/x/image/font"
@@ -207,6 +208,7 @@ func (vr *vectorRenderer) MeasureText(body string) (box Box) {
box.Right = w
box.Bottom = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize))
+ box.IsSet = true
if vr.c.textTheta == nil {
return
}
@@ -266,24 +268,30 @@ func (c *canvas) Start(width, height int) {
}
func (c *canvas) Path(d string, style Style) {
+ if d == "" {
+ return
+ }
var strokeDashArrayProperty string
if len(style.StrokeDashArray) > 0 {
strokeDashArrayProperty = c.getStrokeDashArray(style)
}
- _, _ = c.w.Write([]byte(fmt.Sprintf(``, strokeDashArrayProperty, d, c.styleAsSVG(style))))
+ _, _ = c.w.Write([]byte(fmt.Sprintf(``, strokeDashArrayProperty, d, c.styleAsSVG(style, false))))
}
func (c *canvas) Text(x, y int, body string, style Style) {
+ if body == "" {
+ return
+ }
if c.textTheta == nil {
- _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), body)))
+ _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style, true), body)))
} else {
transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, RadiansToDegrees(*c.textTheta), x, y)
- _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), transform, body)))
+ _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style, true), transform, body)))
}
}
func (c *canvas) Circle(x, y, r int, style Style) {
- _, _ = c.w.Write([]byte(fmt.Sprintf(``, x, y, r, c.styleAsSVG(style))))
+ _, _ = c.w.Write([]byte(fmt.Sprintf(``, x, y, r, c.styleAsSVG(style, false))))
}
func (c *canvas) End() {
@@ -315,7 +323,7 @@ func (c *canvas) getFontFace(s Style) string {
}
// styleAsSVG returns the style as a svg style or class string.
-func (c *canvas) styleAsSVG(s Style) string {
+func (c *canvas) styleAsSVG(s Style, applyText bool) string {
sw := s.StrokeWidth
sc := s.StrokeColor
fc := s.FillColor
@@ -331,7 +339,7 @@ func (c *canvas) styleAsSVG(s Style) string {
if !fc.IsZero() {
classes = append(classes, "fill")
}
- if fs != 0 || s.Font != nil {
+ if applyText && (fs != 0 || s.Font != nil) {
classes = append(classes, "text")
}
@@ -340,19 +348,14 @@ func (c *canvas) styleAsSVG(s Style) string {
var pieces []string
- if sw != 0 {
- pieces = append(pieces, "stroke-width:"+fmt.Sprintf("%d", int(sw)))
- } else {
- pieces = append(pieces, "stroke-width:0")
- }
-
- if !sc.IsTransparent() {
+ if sw != 0 && !sc.IsTransparent() {
+ pieces = append(pieces, "stroke-width:"+formatFloatMinimized(sw))
pieces = append(pieces, "stroke:"+sc.String())
} else {
pieces = append(pieces, "stroke:none")
}
- if !fnc.IsTransparent() {
+ if applyText && !fnc.IsTransparent() {
pieces = append(pieces, "fill:"+fnc.String())
} else if !fc.IsTransparent() {
pieces = append(pieces, "fill:"+fc.String())
@@ -360,12 +363,29 @@ func (c *canvas) styleAsSVG(s Style) string {
pieces = append(pieces, "fill:none")
}
- if fs != 0 {
- pieces = append(pieces, "font-size:"+fmt.Sprintf("%.1fpx", drawing.PointsToPixels(c.dpi, fs)))
+ if applyText {
+ if fs != 0 {
+ pieces = append(pieces, "font-size:"+formatFloatMinimized(drawing.PointsToPixels(c.dpi, fs))+"px")
+ }
+ if s.Font != nil {
+ pieces = append(pieces, c.getFontFace(s))
+ }
}
- if s.Font != nil {
- pieces = append(pieces, c.getFontFace(s))
+ if len(pieces) == 0 {
+ return ""
}
+
return fmt.Sprintf("style=\"%s\"", strings.Join(pieces, ";"))
}
+
+// formatFloatNoTrailingZero formats a float without trailing zeros, so it is as small as possible.
+func formatFloatMinimized(val float64) string {
+ if val == float64(int(val)) {
+ return strconv.Itoa(int(val))
+ }
+ str := fmt.Sprintf("%.1f", val) // e.g. "1.20"
+ str = strings.TrimRight(str, "0") // e.g. "1.2"
+ str = strings.TrimRight(str, ".") // a rounding condition where an int is acceptable
+ return str
+}
diff --git a/chartdraw/vector_renderer_test.go b/chartdraw/vector_renderer_test.go
index 2ddc2e6..3405b1f 100644
--- a/chartdraw/vector_renderer_test.go
+++ b/chartdraw/vector_renderer_test.go
@@ -58,18 +58,30 @@ func TestCanvasStyleSVG(t *testing.T) {
FontStyle: FontStyle{
FontColor: drawing.ColorWhite,
Font: GetDefaultFont(),
+ FontSize: 12,
},
Padding: DefaultBackgroundPadding,
}
canvas := &canvas{dpi: DefaultDPI}
- svgString := canvas.styleAsSVG(set)
+ svgString := canvas.styleAsSVG(set, false)
assert.NotEmpty(t, svgString)
assert.True(t, strings.HasPrefix(svgString, "style=\""))
- assert.True(t, strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)"))
+ assert.True(t, strings.Contains(svgString, "stroke:white"))
assert.True(t, strings.Contains(svgString, "stroke-width:5"))
- assert.True(t, strings.Contains(svgString, "fill:rgba(255,255,255,1.0)"))
+ assert.True(t, strings.Contains(svgString, "fill:white"))
+ assert.False(t, strings.Contains(svgString, "font-size"))
+ assert.False(t, strings.Contains(svgString, "font-family"))
+ assert.True(t, strings.HasSuffix(svgString, "\""))
+
+ svgString = canvas.styleAsSVG(set, true)
+ assert.True(t, strings.HasPrefix(svgString, "style=\""))
+ assert.True(t, strings.Contains(svgString, "stroke:white"))
+ assert.True(t, strings.Contains(svgString, "stroke-width:5"))
+ assert.True(t, strings.Contains(svgString, "fill:white"))
+ assert.True(t, strings.Contains(svgString, "font-size"))
+ assert.True(t, strings.Contains(svgString, "font-family"))
assert.True(t, strings.HasSuffix(svgString, "\""))
}
@@ -82,7 +94,7 @@ func TestCanvasClassSVG(t *testing.T) {
canvas := &canvas{dpi: DefaultDPI}
- assert.Equal(t, "class=\"test-class\"", canvas.styleAsSVG(set))
+ assert.Equal(t, "class=\"test-class\"", canvas.styleAsSVG(set, false))
}
func TestCanvasCustomInlineStylesheet(t *testing.T) {
diff --git a/echarts_test.go b/echarts_test.go
index 94639ab..8ef9f7f 100644
--- a/echarts_test.go
+++ b/echarts_test.go
@@ -525,5 +525,5 @@ func TestRenderEChartsToSVG(t *testing.T) {
]
}`)
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
diff --git a/font_test.go b/font_test.go
index da815cd..75892e3 100644
--- a/font_test.go
+++ b/font_test.go
@@ -53,5 +53,5 @@ func TestCustomFontSizeRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
diff --git a/funnel_chart.go b/funnel_chart.go
index a28e870..0ca69d6 100644
--- a/funnel_chart.go
+++ b/funnel_chart.go
@@ -4,8 +4,6 @@ import (
"errors"
"github.com/golang/freetype/truetype"
-
- "github.com/go-analyze/charts/chartdraw"
)
type funnelChart struct {
@@ -106,24 +104,19 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList)
Y: y,
},
}
- color := theme.GetSeriesColor(series.index)
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: color,
- })
- seriesPainter.FillArea(points)
+ seriesPainter.FillArea(points, theme.GetSeriesColor(series.index))
- // text
text := textList[index]
- seriesPainter.OverrideFontStyle(FontStyle{
+ fontStyle := FontStyle{
FontColor: theme.GetTextColor(),
FontSize: labelFontSize,
Font: opt.Font,
- })
- textBox := seriesPainter.MeasureText(text)
+ }
+ textBox := seriesPainter.MeasureText(text, fontStyle, 0)
textX := width>>1 - textBox.Width()>>1
textY := y + h>>1
- seriesPainter.Text(text, textX, textY)
+ seriesPainter.Text(text, textX, textY, fontStyle)
y += h + gap
}
diff --git a/funnel_chart_test.go b/funnel_chart_test.go
index 335bed8..8e0f265 100644
--- a/funnel_chart_test.go
+++ b/funnel_chart_test.go
@@ -36,13 +36,13 @@ func TestFunnelChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicFunnelChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicFunnelChartOption,
- result: "",
+ result: "",
},
}
diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go
index 9c5b529..674ab17 100644
--- a/horizontal_bar_chart.go
+++ b/horizontal_bar_chart.go
@@ -102,16 +102,13 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
w := xRange.getHeight(item)
fillColor := seriesColor
right := w
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: fillColor,
- })
seriesPainter.filledRect(chartdraw.Box{
Top: y,
Left: 0,
Right: right,
Bottom: y + barHeight,
IsSet: true,
- })
+ }, fillColor, fillColor, 0.0)
// if the label does not need to be displayed, return
if labelPainter == nil {
continue
diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go
index 325ccf6..ed767c8 100644
--- a/horizontal_bar_chart_test.go
+++ b/horizontal_bar_chart_test.go
@@ -52,13 +52,13 @@ func TestHorizontalBarChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicHorizontalBarChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicHorizontalBarChartOption,
- result: "",
+ result: "",
},
{
name: "custom_fonts",
@@ -74,7 +74,7 @@ func TestHorizontalBarChart(t *testing.T) {
opt.Title.FontStyle = customFont
return opt
},
- result: "",
+ result: "",
},
{
name: "value_labels",
@@ -87,7 +87,7 @@ func TestHorizontalBarChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
}
diff --git a/legend.go b/legend.go
index d09599c..6e78ac3 100644
--- a/legend.go
+++ b/legend.go
@@ -2,8 +2,6 @@ package charts
import (
"fmt"
-
- "github.com/go-analyze/charts/chartdraw"
)
type legendPainter struct {
@@ -91,7 +89,6 @@ func (l *legendPainter) Render() (Box, error) {
padding.Top = 5
}
p := l.p.Child(PainterPaddingOption(padding))
- p.SetFontStyle(fontStyle)
// calculate the width and height of the display
measureList := make([]Box, len(opt.Data))
@@ -104,7 +101,7 @@ func (l *legendPainter) Render() (Box, error) {
maxTextWidth := 0
itemMaxHeight := 0
for index, text := range opt.Data {
- b := p.MeasureText(text)
+ b := p.MeasureText(text, fontStyle, 0)
if b.Width() > maxTextWidth {
maxTextWidth = b.Width()
}
@@ -170,27 +167,27 @@ func (l *legendPainter) Render() (Box, error) {
x0 := startX
y0 := y
- var drawIcon func(top, left int) int
+ var drawIcon func(top, left int, color Color) int
if opt.Icon == IconRect {
- drawIcon = func(top, left int) int {
+ drawIcon = func(top, left int, color Color) int {
p.filledRect(Box{
Top: top - legendHeight + 8,
Left: left,
Right: left + legendWidth,
Bottom: top + 1,
IsSet: true,
- })
+ }, color, color, 0)
return left + legendWidth
}
} else {
- drawIcon = func(top, left int) int {
+ drawIcon = func(top, left int, color Color) int {
p.legendLineDot(Box{
Top: top + 1,
Left: left,
Right: left + legendWidth,
Bottom: top + legendHeight + 1,
IsSet: true,
- })
+ }, color, 3, color)
return left + legendWidth
}
}
@@ -198,10 +195,6 @@ func (l *legendPainter) Render() (Box, error) {
lastIndex := len(opt.Data) - 1
for index, text := range opt.Data {
color := theme.GetSeriesColor(index)
- p.SetDrawingStyle(chartdraw.Style{
- FillColor: color,
- StrokeColor: color,
- })
if vertical {
if opt.Align == AlignRight {
// adjust x0 so that the text will start with a right alignment to the longest line
@@ -219,7 +212,7 @@ func (l *legendPainter) Render() (Box, error) {
// recalculate width and center based off remaining width
var remainingWidth int
for i2 := index; i2 < len(opt.Data); i2++ {
- b := p.MeasureText(opt.Data[i2])
+ b := p.MeasureText(opt.Data[i2], fontStyle, 0)
remainingWidth += b.Width()
}
remainingCount := len(opt.Data) - index
@@ -238,14 +231,14 @@ func (l *legendPainter) Render() (Box, error) {
}
if opt.Align != AlignRight {
- x0 = drawIcon(y0, x0)
+ x0 = drawIcon(y0, x0, color)
x0 += textOffset
}
- p.Text(text, x0, y0)
+ p.Text(text, x0, y0, fontStyle)
x0 += measureList[index].Width()
if opt.Align == AlignRight {
x0 += textOffset
- x0 = drawIcon(y0, x0)
+ x0 = drawIcon(y0, x0, color)
}
if vertical {
diff --git a/legend_test.go b/legend_test.go
index 15539fd..b0345b9 100644
--- a/legend_test.go
+++ b/legend_test.go
@@ -29,7 +29,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "position_left",
@@ -43,7 +43,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "position_vertical_with_rect",
@@ -61,7 +61,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "custom_padding_and_font",
@@ -79,7 +79,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "hidden",
@@ -109,7 +109,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_position",
@@ -124,7 +124,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_bottom_position",
@@ -141,7 +141,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_bottom_position",
@@ -159,7 +159,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_position_custom_font_size",
@@ -177,7 +177,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_position_with_padding",
@@ -193,7 +193,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "left_position_overflow",
@@ -208,7 +208,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "center_position_overflow",
@@ -223,7 +223,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "center_position_center_align_overflow",
@@ -239,7 +239,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "50%_position_overflow",
@@ -256,7 +256,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_position_overflow",
@@ -274,7 +274,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "right_alignment",
@@ -288,7 +288,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_alignment",
@@ -303,7 +303,7 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_alignment_left_position",
@@ -319,9 +319,10 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
+
for i, tt := range tests {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
p := NewPainter(PainterOptions{
diff --git a/line_chart.go b/line_chart.go
index c090522..9052293 100644
--- a/line_chart.go
+++ b/line_chart.go
@@ -5,7 +5,6 @@ import (
"github.com/golang/freetype/truetype"
- "github.com/go-analyze/charts/chartdraw"
"github.com/go-analyze/charts/chartdraw/drawing"
)
@@ -112,10 +111,6 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
for index := range seriesList {
series := seriesList[index]
seriesColor := opt.Theme.GetSeriesColor(series.index)
- drawingStyle := chartdraw.Style{
- StrokeColor: seriesColor,
- StrokeWidth: strokeWidth,
- }
yRange := result.axisRanges[series.YAxisIndex]
points := make([]Point, 0)
@@ -162,34 +157,28 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
X: areaPoints[0].X,
Y: bottomY,
}, areaPoints[0])
- seriesPainter.SetDrawingStyle(chartdraw.Style{
- FillColor: seriesColor.WithAlpha(opacity),
- })
+ fillColor := seriesColor.WithAlpha(opacity)
if opt.StrokeSmoothingTension > 0 {
- seriesPainter.smoothFillChartArea(areaPoints, opt.StrokeSmoothingTension)
+ seriesPainter.smoothFillChartArea(areaPoints, opt.StrokeSmoothingTension, fillColor)
} else {
- seriesPainter.FillArea(areaPoints)
+ seriesPainter.FillArea(areaPoints, fillColor)
}
}
- seriesPainter.SetDrawingStyle(drawingStyle)
// draw line
if opt.StrokeSmoothingTension > 0 {
- seriesPainter.SmoothLineStroke(points, opt.StrokeSmoothingTension)
+ seriesPainter.SmoothLineStroke(points, opt.StrokeSmoothingTension, seriesColor, strokeWidth)
} else {
- seriesPainter.LineStroke(points)
+ seriesPainter.LineStroke(points, seriesColor, strokeWidth)
}
// draw dots
- if opt.Theme.IsDark() {
- drawingStyle.FillColor = drawingStyle.StrokeColor
- } else {
- drawingStyle.FillColor = drawing.ColorWhite
- }
- drawingStyle.StrokeWidth = 1
- seriesPainter.SetDrawingStyle(drawingStyle)
if showSymbol {
- seriesPainter.Dots(points)
+ dotFillColor := drawing.ColorWhite
+ if opt.Theme.IsDark() {
+ dotFillColor = seriesColor
+ }
+ seriesPainter.Dots(points, dotFillColor, seriesColor, 1, 2)
}
markPointPainter.Add(markPointRenderOption{
FillColor: seriesColor,
diff --git a/line_chart_test.go b/line_chart_test.go
index 71ff6e9..1b259f0 100644
--- a/line_chart_test.go
+++ b/line_chart_test.go
@@ -104,13 +104,13 @@ func TestLineChart(t *testing.T) {
name: "basic_default",
defaultTheme: true,
makeOptions: makeFullLineChartOption,
- result: "",
+ result: "",
},
{
name: "basic_themed",
defaultTheme: false,
makeOptions: makeFullLineChartOption,
- result: "",
+ result: "",
},
{
name: "boundary_gap_disable",
@@ -120,7 +120,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = False()
return opt
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_enable",
@@ -130,7 +130,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "08Y_skip1",
@@ -145,7 +145,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "09Y_skip1",
@@ -160,7 +160,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "08Y_skip2",
@@ -175,7 +175,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "09Y_skip2",
@@ -190,7 +190,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "10Y_skip2",
@@ -205,7 +205,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "08Y_skip3",
@@ -220,7 +220,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "09Y_skip3",
@@ -235,7 +235,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "10Y_skip3",
@@ -250,7 +250,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "11Y_skip3",
@@ -265,7 +265,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "no_yaxis_split_line",
@@ -280,7 +280,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "yaxis_spine_line_show",
@@ -295,7 +295,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "zero_data",
@@ -309,7 +309,7 @@ func TestLineChart(t *testing.T) {
opt.SeriesList = NewSeriesListLine(values)
return opt
},
- result: "",
+ result: "",
},
{
name: "tiny_range",
@@ -323,7 +323,7 @@ func TestLineChart(t *testing.T) {
opt.SeriesList = NewSeriesListLine(values)
return opt
},
- result: "",
+ result: "",
},
{
name: "hidden_legend_and_x-axis",
@@ -334,7 +334,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.Show = False()
return opt
},
- result: "",
+ result: "",
},
{
name: "custom_font",
@@ -350,7 +350,7 @@ func TestLineChart(t *testing.T) {
opt.Title.FontStyle = customFont
return opt
},
- result: "",
+ result: "",
},
{
name: "title_offset_center_legend_right",
@@ -361,7 +361,7 @@ func TestLineChart(t *testing.T) {
opt.Legend.Offset = OffsetRight
return opt
},
- result: "",
+ result: "",
},
{
name: "title_offset_right",
@@ -371,7 +371,7 @@ func TestLineChart(t *testing.T) {
opt.Title.Offset = OffsetRight
return opt
},
- result: "",
+ result: "",
},
{
name: "title_offset_bottom_center",
@@ -384,7 +384,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "legend_offset_bottom",
@@ -396,7 +396,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "title_and_legend_offset_bottom",
@@ -411,7 +411,7 @@ func TestLineChart(t *testing.T) {
opt.Legend.Offset = bottomOffset
return opt
},
- result: "",
+ result: "",
},
{
name: "vertical_legend_offset_right",
@@ -422,7 +422,7 @@ func TestLineChart(t *testing.T) {
opt.Legend.Offset = OffsetRight
return opt
},
- result: "",
+ result: "",
},
{
name: "legend_overlap_chart",
@@ -434,7 +434,7 @@ func TestLineChart(t *testing.T) {
opt.Legend.OverlayChart = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "curved_line",
@@ -444,7 +444,7 @@ func TestLineChart(t *testing.T) {
opt.StrokeSmoothingTension = 0.8
return opt
},
- result: "",
+ result: "",
},
{
name: "line_gap",
@@ -454,7 +454,7 @@ func TestLineChart(t *testing.T) {
opt.SeriesList[0].Data[3] = GetNullValue()
return opt
},
- result: "",
+ result: "",
},
{
name: "line_gap_dot",
@@ -465,7 +465,7 @@ func TestLineChart(t *testing.T) {
opt.SeriesList[0].Data[5] = GetNullValue()
return opt
},
- result: "",
+ result: "",
},
{
name: "line_gap_fill_area",
@@ -476,7 +476,7 @@ func TestLineChart(t *testing.T) {
opt.FillArea = true
return opt
},
- result: "",
+ result: "",
},
{
name: "curved_line_gap",
@@ -487,7 +487,7 @@ func TestLineChart(t *testing.T) {
opt.SeriesList[0].Data[3] = GetNullValue()
return opt
},
- result: "",
+ result: "",
},
{
name: "curved_line_gap_fill_area",
@@ -499,7 +499,7 @@ func TestLineChart(t *testing.T) {
opt.FillArea = true
return opt
},
- result: "",
+ result: "",
},
{
name: "fill_area",
@@ -510,7 +510,7 @@ func TestLineChart(t *testing.T) {
opt.FillOpacity = 100
return opt
},
- result: "",
+ result: "",
},
{
name: "fill_area_boundary_gap",
@@ -522,7 +522,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "fill_area_curved_boundary_gap",
@@ -534,7 +534,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "fill_area_curved_no_gap",
@@ -546,7 +546,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = False()
return opt
},
- result: "",
+ result: "",
},
}
diff --git a/mark_line.go b/mark_line.go
index e35e428..8477561 100644
--- a/mark_line.go
+++ b/mark_line.go
@@ -2,8 +2,6 @@ package charts
import (
"github.com/golang/freetype/truetype"
-
- "github.com/go-analyze/charts/chartdraw"
)
// NewMarkLine returns a series mark line
@@ -52,24 +50,13 @@ func (m *markLinePainter) Render() (Box, error) {
if len(s.MarkLine.Data) == 0 {
continue
}
- font := getPreferredFont(opt.Font)
summary := s.Summary()
+ fontStyle := FontStyle{
+ Font: getPreferredFont(opt.Font),
+ FontColor: opt.FontColor,
+ FontSize: labelFontSize,
+ }
for _, markLine := range s.MarkLine.Data {
- // since the mark line will modify the style, it must be reset every time
- painter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: opt.FillColor,
- StrokeColor: opt.StrokeColor,
- StrokeWidth: 1,
- StrokeDashArray: []float64{
- 4,
- 2,
- },
- })
- painter.OverrideFontStyle(FontStyle{
- Font: font,
- FontColor: opt.FontColor,
- FontSize: labelFontSize,
- })
value := float64(0)
switch markLine.Type {
case SeriesMarkDataTypeMax:
@@ -82,9 +69,9 @@ func (m *markLinePainter) Render() (Box, error) {
y := opt.Range.getRestHeight(value)
width := painter.Width()
text := defaultValueFormatter(value)
- textBox := painter.MeasureText(text)
- painter.MarkLine(0, y, width-2)
- painter.Text(text, width, y+textBox.Height()>>1-2)
+ textBox := painter.MeasureText(text, fontStyle, 0)
+ painter.MarkLine(0, y, width-2, opt.FillColor, opt.StrokeColor, 1, []float64{4, 2})
+ painter.Text(text, width, y+textBox.Height()>>1-2, fontStyle)
}
}
return BoxZero, nil
diff --git a/mark_line_test.go b/mark_line_test.go
index 0dddc55..76bc77d 100644
--- a/mark_line_test.go
+++ b/mark_line_test.go
@@ -39,9 +39,10 @@ func TestMarkLine(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
+
for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
p := NewPainter(PainterOptions{
diff --git a/mark_point.go b/mark_point.go
index 14a1220..8e92d52 100644
--- a/mark_point.go
+++ b/mark_point.go
@@ -67,13 +67,8 @@ func (m *markPointPainter) Render() (Box, error) {
} else {
textStyle.FontColor = defaultDarkFontColor
}
- painter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: opt.FillColor,
- })
- painter.OverrideFontStyle(textStyle.FontStyle)
for _, markPointData := range opt.Series.MarkPoint.Data {
textStyle.FontSize = labelFontSize
- painter.OverrideFontStyle(textStyle.FontStyle)
p := points[summary.MinIndex]
value := summary.Min
switch markPointData.Type {
@@ -82,15 +77,14 @@ func (m *markPointPainter) Render() (Box, error) {
value = summary.Max
}
- painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize)
+ painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize, opt.FillColor, opt.FillColor, 0.0)
text := defaultValueFormatter(value)
- textBox := painter.MeasureText(text)
+ textBox := painter.MeasureText(text, textStyle.FontStyle, 0)
if textBox.Width() > symbolSize {
textStyle.FontSize = smallLabelFontSize
- painter.OverrideFontStyle(textStyle.FontStyle)
- textBox = painter.MeasureText(text)
+ textBox = painter.MeasureText(text, textStyle.FontStyle, 0)
}
- painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
+ painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2, textStyle.FontStyle)
}
}
return BoxZero, nil
diff --git a/mark_point_test.go b/mark_point_test.go
index 9a4e71b..f8a74ac 100644
--- a/mark_point_test.go
+++ b/mark_point_test.go
@@ -37,7 +37,7 @@ func TestMarkPoint(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
diff --git a/painter.go b/painter.go
index eb9f85e..ec31c02 100644
--- a/painter.go
+++ b/painter.go
@@ -20,7 +20,6 @@ var defaultValueFormatter = func(val float64) string {
type Painter struct {
render chartdraw.Renderer
box Box
- style chartdraw.Style
theme ColorPalette
font *truetype.Font
outputFormat string
@@ -37,21 +36,27 @@ type PainterOptions struct {
Height int
// Font is the font used for rendering text.
Font *truetype.Font
+ // Theme is the default theme to be used if the chart does not specify a theme
+ Theme ColorPalette
}
+// PainterOptionFunc defines a function that can modify a Painter after creation.
type PainterOptionFunc func(*Painter)
type ticksOption struct {
- firstIndex int
- length int
- vertical bool
- labelCount int
- tickCount int
- tickSpaces int
+ firstIndex int
+ length int
+ vertical bool
+ labelCount int
+ tickCount int
+ tickSpaces int
+ strokeWidth float64
+ strokeColor Color
}
type multiTextOption struct {
textList []string
+ fontStyle FontStyle
vertical bool
centerLabels bool
align string
@@ -63,7 +68,7 @@ type multiTextOption struct {
labelSkipCount int
}
-// PainterPaddingOption sets the padding of draw painter.
+// PainterPaddingOption sets the padding of the draw painter.
func PainterPaddingOption(padding Box) PainterOptionFunc {
return func(p *Painter) {
p.box.Left += padding.Left
@@ -102,30 +107,23 @@ func NewPainter(opts PainterOptions, opt ...PainterOptionFunc) *Painter {
if opts.Height <= 0 {
opts.Height = defaultChartHeight
}
- if opts.Font == nil {
- opts.Font = GetDefaultFont()
- }
fn := chartdraw.PNG
if opts.OutputFormat == ChartOutputSVG {
fn = chartdraw.SVG
}
- r := fn(opts.Width, opts.Height)
- r.SetFont(opts.Font)
p := &Painter{
- render: r,
+ render: fn(opts.Width, opts.Height),
box: Box{
Right: opts.Width,
Bottom: opts.Height,
IsSet: true,
},
font: opts.Font,
+ theme: opts.Theme,
outputFormat: opts.OutputFormat,
}
p.setOptions(opt...)
- if p.theme == nil {
- p.theme = GetDefaultTheme()
- }
return p
}
@@ -135,13 +133,11 @@ func (p *Painter) setOptions(opts ...PainterOptionFunc) {
}
}
-// Child returns a painter with the passed in options applied to it. This can be most useful when you want to render
-// relative to only a portion of the canvas using PainterBoxOption.
+// Child returns a painter with the passed-in options applied to it. Useful when you want to render relative to only a portion of the canvas via PainterBoxOption.
func (p *Painter) Child(opt ...PainterOptionFunc) *Painter {
child := &Painter{
render: p.render,
box: p.box.Clone(),
- style: p.style,
theme: p.theme,
font: p.font,
outputFormat: p.outputFormat,
@@ -151,78 +147,7 @@ func (p *Painter) Child(opt ...PainterOptionFunc) *Painter {
return child
}
-func (p *Painter) setStyle(style chartdraw.Style) {
- if style.Font == nil {
- style.Font = p.font
- }
- p.style = style
- style.WriteToRenderer(p.render)
-}
-
-func overrideStyle(defaultStyle chartdraw.Style, style chartdraw.Style) chartdraw.Style {
- if style.StrokeWidth == 0 {
- style.StrokeWidth = defaultStyle.StrokeWidth
- }
- if style.StrokeColor.IsZero() {
- style.StrokeColor = defaultStyle.StrokeColor
- }
- if style.StrokeDashArray == nil {
- style.StrokeDashArray = defaultStyle.StrokeDashArray
- }
- if style.DotColor.IsZero() {
- style.DotColor = defaultStyle.DotColor
- }
- if style.DotWidth == 0 {
- style.DotWidth = defaultStyle.DotWidth
- }
- if style.FillColor.IsZero() {
- style.FillColor = defaultStyle.FillColor
- }
- if style.FontSize == 0 {
- style.FontSize = defaultStyle.FontSize
- }
- if style.FontColor.IsZero() {
- style.FontColor = defaultStyle.FontColor
- }
- if style.Font == nil {
- style.Font = defaultStyle.Font
- }
- return style
-}
-
-func (p *Painter) OverrideDrawingStyle(style chartdraw.Style) {
- // TODO - we should alias parts of Style we want to support drawing on
- s := overrideStyle(p.style, style)
- p.SetDrawingStyle(s)
-}
-
-func (p *Painter) SetDrawingStyle(style chartdraw.Style) {
- style.WriteDrawingOptionsToRenderer(p.render)
-}
-
-func (p *Painter) SetFontStyle(style chartdraw.FontStyle) {
- if style.Font == nil {
- style.Font = p.font
- }
- if style.FontColor.IsZero() {
- style.FontColor = p.style.FontColor
- }
- if style.FontSize == 0 {
- style.FontSize = p.style.FontSize
- }
- style.WriteTextOptionsToRenderer(p.render)
-}
-
-func (p *Painter) OverrideFontStyle(style chartdraw.FontStyle) {
- s := overrideStyle(p.style, chartdraw.Style{FontStyle: style})
- p.SetFontStyle(s.FontStyle)
-}
-
-func (p *Painter) resetStyle() {
- p.style.WriteToRenderer(p.render)
-}
-
-// Bytes returns the data of draw canvas.
+// Bytes returns the final rendered data as a byte slice.
func (p *Painter) Bytes() ([]byte, error) {
buffer := bytes.Buffer{}
if err := p.render.Save(&buffer); err != nil {
@@ -231,131 +156,52 @@ func (p *Painter) Bytes() ([]byte, error) {
return buffer.Bytes(), nil
}
-// moveTo moves the cursor to a given point.
+// moveTo sets the current path cursor to a given point.
func (p *Painter) moveTo(x, y int) {
p.render.MoveTo(x+p.box.Left, y+p.box.Top)
}
-// arcTo renders an arc from the current cursor and the given parameters.
+// arcTo renders an arc from the current cursor.
func (p *Painter) arcTo(cx, cy int, rx, ry, startAngle, delta float64) {
p.render.ArcTo(cx+p.box.Left, cy+p.box.Top, rx, ry, startAngle, delta)
}
-// Line renders a line from the first xy coordinates to the second point.
-func (p *Painter) Line(x1, y1, x2, y2 int) {
- p.moveTo(x1, y1)
- p.lineTo(x2, y2)
-}
-
+// quadCurveTo draws a quadratic curve from the current cursor using a control point (cx, cy) and ending at (x, y).
func (p *Painter) quadCurveTo(cx, cy, x, y int) {
p.render.QuadCurveTo(cx+p.box.Left, cy+p.box.Top, x+p.box.Left, y+p.box.Top)
}
-// lineTo renders a line from the current cursor to the given point.
+// lineTo draws a line from the current path cursor to the given point.
func (p *Painter) lineTo(x, y int) {
p.render.LineTo(x+p.box.Left, y+p.box.Top)
}
-func (p *Painter) Pin(x, y, width int) {
- r := float64(width) / 2
- y -= width / 4
- angle := chartdraw.DegreesToRadians(15)
- box := p.box
-
- startAngle := math.Pi/2 + angle
- delta := 2*math.Pi - 2*angle
- p.arcTo(x, y, r, r, startAngle, delta)
- p.lineTo(x, y)
- p.close()
- p.fillStroke()
-
- startX := x - int(r)
- startY := y
- endX := x + int(r)
- endY := y
- p.moveTo(startX, startY)
-
- left := box.Left
- top := box.Top
- cx := x
- cy := y + int(r*2.5)
- p.render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
- p.close()
- p.render.Fill()
-}
-
-func (p *Painter) arrow(x, y, width, height int, direction string) {
- halfWidth := width >> 1
- halfHeight := height >> 1
- if direction == PositionTop || direction == PositionBottom {
- x0 := x - halfWidth
- x1 := x0 + width
- dy := -height / 3
- y0 := y
- y1 := y0 - height
- if direction == PositionBottom {
- y0 = y - height
- y1 = y
- dy = 2 * dy
- }
- p.moveTo(x0, y0)
- p.lineTo(x0+halfWidth, y1)
- p.lineTo(x1, y0)
- p.lineTo(x0+halfWidth, y+dy)
- p.lineTo(x0, y0)
- } else {
- x0 := x + width
- x1 := x0 - width
- y0 := y - halfHeight
- dx := -width / 3
- if direction == PositionRight {
- x0 = x - width
- dx = -dx
- x1 = x0 + width
- }
- p.moveTo(x0, y0)
- p.lineTo(x1, y0+halfHeight)
- p.lineTo(x0, y0+height)
- p.lineTo(x0+dx, y0+halfHeight)
- p.lineTo(x0, y0)
- }
- p.fillStroke()
-}
-
-// ArrowLeft draws an arrow at the given point and dimensions pointing left.
-func (p *Painter) ArrowLeft(x, y, width, height int) {
- p.arrow(x, y, width, height, PositionLeft)
-}
-
-// ArrowRight draws an arrow at the given point and dimensions pointing right.
-func (p *Painter) ArrowRight(x, y, width, height int) {
- p.arrow(x, y, width, height, PositionRight)
-}
-
-// ArrowUp draws an arrow at the given point and dimensions pointing up.
-func (p *Painter) ArrowUp(x, y, width, height int) {
- p.arrow(x, y, width, height, PositionTop)
-}
-
-// ArrowDown draws an arrow at the given point and dimensions pointing down.
-func (p *Painter) ArrowDown(x, y, width, height int) {
- p.arrow(x, y, width, height, PositionBottom)
-}
-
-// Circle draws a circle at the given coords with a given radius.
-func (p *Painter) Circle(radius float64, x, y int) {
- p.render.Circle(radius, x+p.box.Left, y+p.box.Top)
+// close finalizes a shape as drawn by the current path.
+func (p *Painter) close() {
+ p.render.Close()
}
-func (p *Painter) stroke() {
+// stroke performs a stroke using the provided color and width, then resets style.
+func (p *Painter) stroke(strokeColor Color, strokeWidth float64) {
+ defer p.render.ResetStyle()
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
p.render.Stroke()
}
-func (p *Painter) close() {
- p.render.Close()
+// fill performs a fill with the given color, then resets style.
+func (p *Painter) fill(fillColor Color) {
+ defer p.render.ResetStyle()
+ p.render.SetFillColor(fillColor)
+ p.render.Fill()
}
-func (p *Painter) fillStroke() {
+// fillStroke performs a fill+stroke with the given colors and stroke width, then resets style.
+func (p *Painter) fillStroke(fillColor, strokeColor Color, strokeWidth float64) {
+ defer p.render.ResetStyle()
+ p.render.SetFillColor(fillColor)
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
p.render.FillStroke()
}
@@ -370,15 +216,33 @@ func (p *Painter) Height() int {
}
// MeasureText will provide the rendered size of the text for the provided font style.
-func (p *Painter) MeasureText(text string) Box {
- return p.render.MeasureText(text)
+func (p *Painter) MeasureText(text string, fontStyle FontStyle, textRotation float64) Box {
+ if fontStyle.Font == nil {
+ fontStyle.Font = getPreferredFont(p.font)
+ }
+ if fontStyle.Font == nil || fontStyle.FontSize == 0 || fontStyle.FontColor.IsTransparent() {
+ return BoxZero
+ }
+ if textRotation != 0 {
+ defer p.render.ClearTextRotation()
+ p.render.SetTextRotation(textRotation)
+ }
+ defer p.render.ResetStyle()
+ p.render.SetFont(fontStyle.Font)
+ p.render.SetFontSize(fontStyle.FontSize)
+ p.render.SetFontColor(fontStyle.FontColor)
+ box := p.render.MeasureText(text)
+ return box
}
-func (p *Painter) measureTextMaxWidthHeight(textList []string) (int, int) {
+func (p *Painter) measureTextMaxWidthHeight(textList []string, fontStyle FontStyle, textRotation float64) (int, int) {
+ if fontStyle.Font == nil {
+ fontStyle.Font = getPreferredFont(p.font)
+ }
maxWidth := 0
maxHeight := 0
for _, text := range textList {
- box := p.MeasureText(text)
+ box := p.MeasureText(text, fontStyle, textRotation)
if maxWidth < box.Width() {
maxWidth = box.Width()
}
@@ -389,25 +253,33 @@ func (p *Painter) measureTextMaxWidthHeight(textList []string) (int, int) {
return maxWidth, maxHeight
}
+// Circle draws a circle at the given coords with a given radius.
+func (p *Painter) Circle(radius float64, x, y int, fillColor, strokeColor Color, strokeWidth float64) {
+ defer p.render.ResetStyle()
+ p.render.SetFillColor(fillColor)
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
+ p.render.Circle(radius, x+p.box.Left, y+p.box.Top)
+}
+
// LineStroke draws a line in the graph from point to point with the specified stroke color/width.
// Points with values of math.MaxInt32 will be skipped, resulting in a gap.
// Single or isolated points will result in just a dot being drawn at the point.
-func (p *Painter) LineStroke(points []Point) {
+func (p *Painter) LineStroke(points []Point, strokeColor Color, strokeWidth float64) {
var valid []Point
for _, pt := range points {
if pt.Y == math.MaxInt32 {
// If we encounter a break, draw the accumulated segment
p.drawStraightPath(valid, true)
- p.stroke()
+ p.stroke(strokeColor, strokeWidth)
valid = valid[:0] // reset
continue
}
valid = append(valid, pt)
}
-
- // Draw the last segment if there is one
+ // Draw the last segment
p.drawStraightPath(valid, true)
- p.stroke()
+ p.stroke(strokeColor, strokeWidth)
}
// drawStraightPath draws a simple (non-curved) path for the given points.
@@ -418,8 +290,9 @@ func (p *Painter) drawStraightPath(points []Point, dotForSinglePoint bool) {
return
} else if pointCount == 1 {
if dotForSinglePoint {
- p.Dots(points)
+ p.render.Circle(2.0, points[0].X+p.box.Left, points[0].Y+p.box.Top)
}
+ return
}
p.moveTo(points[0].X, points[0].Y)
for i := 1; i < pointCount; i++ {
@@ -430,10 +303,10 @@ func (p *Painter) drawStraightPath(points []Point, dotForSinglePoint bool) {
// SmoothLineStroke draws a smooth curve through the given points using Quadratic Bézier segments and a
// `tension` parameter in [0..1] with 0 providing straight lines between midpoints and 1 providing a smoother line.
// Because the tension smooths out the line, the line will no longer hit the provided points exactly. The more variable
-// the points, and the higher the tension, the more the line will be
-func (p *Painter) SmoothLineStroke(points []Point, tension float64) {
+// the points, and the higher the tension, the more the line will be.
+func (p *Painter) SmoothLineStroke(points []Point, tension float64, strokeColor Color, strokeWidth float64) {
if tension <= 0 {
- p.LineStroke(points)
+ p.LineStroke(points, strokeColor, strokeWidth)
return
} else if tension > 1 {
tension = 1
@@ -444,21 +317,19 @@ func (p *Painter) SmoothLineStroke(points []Point, tension float64) {
if pt.Y == math.MaxInt32 {
// When a line break is found, draw the curve for the accumulated valid points if any
p.drawSmoothCurve(valid, tension, true)
- p.stroke()
+ p.stroke(strokeColor, strokeWidth)
valid = valid[:0] // reset
continue
}
-
valid = append(valid, pt)
}
// draw any remaining points collected
p.drawSmoothCurve(valid, tension, true)
- p.stroke()
+ p.stroke(strokeColor, strokeWidth)
}
// drawSmoothCurve handles the actual path drawing (MoveTo/LineTo/QuadCurveTo)
-// but does NOT call Stroke() or Fill(). This allows us to reuse this path
-// logic for either SmoothLineStroke or smoothFillArea.
+// but does NOT call Stroke() or Fill(), letting caller do it.
func (p *Painter) drawSmoothCurve(points []Point, tension float64, dotForSinglePoint bool) {
if len(points) < 3 { // Not enough points to form a curve, draw a line
p.drawStraightPath(points, dotForSinglePoint)
@@ -486,61 +357,146 @@ func (p *Painter) drawSmoothCurve(points []Point, tension float64, dotForSingleP
p.quadCurveTo(points[n-2].X, points[n-2].Y, points[n-1].X, points[n-1].Y)
}
-func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) {
- r := p.render
- s := chartdraw.Style{
- FillColor: color,
- }
- // background color
- p.SetDrawingStyle(s)
- defer p.resetStyle()
- if len(inside) != 0 && inside[0] {
- p.moveTo(0, 0)
- p.lineTo(width, 0)
- p.lineTo(width, height)
- p.lineTo(0, height)
- p.lineTo(0, 0)
- } else {
- // setting the background color does not use boxes
- r.MoveTo(0, 0)
- r.LineTo(width, 0)
- r.LineTo(width, height)
- r.LineTo(0, height)
- r.LineTo(0, 0)
- }
- p.fillStroke()
+// SetBackground fills the entire painter area with the given color.
+func (p *Painter) SetBackground(width, height int, color Color) {
+ p.moveTo(0, 0)
+ p.lineTo(width, 0)
+ p.lineTo(width, height)
+ p.lineTo(0, height)
+ p.lineTo(0, 0)
+ p.fill(color)
}
// MarkLine draws a horizontal line with a small circle and arrow at the right.
-func (p *Painter) MarkLine(x, y, width int) {
+func (p *Painter) MarkLine(x, y, width int, fillColor, strokeColor Color, strokeWidth float64, strokeDashArray []float64) {
arrowWidth := 16
arrowHeight := 10
endX := x + width
radius := 3
- p.Circle(3, x+radius, y)
- p.render.Fill()
- p.Line(x+radius*3, y, endX-arrowWidth, y)
- p.stroke()
- p.ArrowRight(endX, y, arrowWidth, arrowHeight)
+
+ // Set up stroke style before drawing
+ defer p.render.ResetStyle()
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
+ p.render.SetStrokeDashArray(strokeDashArray)
+ p.render.SetFillColor(fillColor)
+
+ // Draw the circle at the starting point
+ p.render.Circle(float64(radius), x+radius+p.box.Left, y+p.box.Top)
+ p.render.Fill() // only fill the circle, do not stroke
+
+ // Draw the line from the end of the circle to near the arrow start
+ p.moveTo(x+radius*3, y)
+ p.lineTo(endX-arrowWidth, y)
+ p.render.Stroke() // apply stroke with the dash array
+
+ p.ArrowRight(endX, y, arrowWidth, arrowHeight, fillColor, strokeColor, strokeWidth)
}
// Polygon draws a polygon with the specified center, radius, and number of sides.
-func (p *Painter) Polygon(center Point, radius float64, sides int) {
+func (p *Painter) Polygon(center Point, radius float64, sides int, strokeColor Color, strokeWidth float64) {
points := getPolygonPoints(center, radius, sides)
- for i, item := range points {
- if i == 0 {
- p.moveTo(item.X, item.Y)
- } else {
- p.lineTo(item.X, item.Y)
+ p.drawStraightPath(points, false)
+ p.lineTo(points[0].X, points[0].Y)
+ p.stroke(strokeColor, strokeWidth)
+}
+
+// Pin draws a pin shape (circle + curved tail).
+func (p *Painter) Pin(x, y, width int, fillColor, strokeColor Color, strokeWidth float64) {
+ r := float64(width) / 2
+ y -= width / 4
+ angle := chartdraw.DegreesToRadians(15)
+
+ // Draw the pin head with fill and stroke
+ startAngle := math.Pi/2 + angle
+ delta := 2*math.Pi - 2*angle
+ p.arcTo(x, y, r, r, startAngle, delta)
+ p.lineTo(x, y)
+ p.close()
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
+
+ // The curved tail
+ startX := x - int(r)
+ startY := y
+ endX := x + int(r)
+ endY := y
+ p.moveTo(startX, startY)
+ cx := x
+ cy := y + int(r*2.5)
+ p.quadCurveTo(cx, cy, endX, endY)
+ p.close()
+
+ // Apply both fill and stroke to the tail
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
+}
+
+// arrow draws an arrow shape in the given direction, then fill+stroke with the given style.
+func (p *Painter) arrow(x, y, width, height int, direction string,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ halfWidth := width >> 1
+ halfHeight := height >> 1
+ if direction == PositionTop || direction == PositionBottom {
+ x0 := x - halfWidth
+ x1 := x0 + width
+ dy := -height / 3
+ y0 := y
+ y1 := y0 - height
+ if direction == PositionBottom {
+ y0 = y - height
+ y1 = y
+ dy = 2 * dy
+ }
+ p.moveTo(x0, y0)
+ p.lineTo(x0+halfWidth, y1)
+ p.lineTo(x1, y0)
+ p.lineTo(x0+halfWidth, y+dy)
+ p.lineTo(x0, y0)
+ } else {
+ x0 := x + width
+ x1 := x0 - width
+ y0 := y - halfHeight
+ dx := -width / 3
+ if direction == PositionRight {
+ x0 = x - width
+ dx = -dx
+ x1 = x0 + width
}
+ p.moveTo(x0, y0)
+ p.lineTo(x1, y0+halfHeight)
+ p.lineTo(x0, y0+height)
+ p.lineTo(x0+dx, y0+halfHeight)
+ p.lineTo(x0, y0)
}
- p.lineTo(points[0].X, points[0].Y)
- p.stroke()
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
+}
+
+// ArrowLeft draws an arrow at the given point and dimensions pointing left.
+func (p *Painter) ArrowLeft(x, y, width, height int,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ p.arrow(x, y, width, height, PositionLeft, fillColor, strokeColor, strokeWidth)
+}
+
+// ArrowRight draws an arrow at the given point and dimensions pointing right.
+func (p *Painter) ArrowRight(x, y, width, height int,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ p.arrow(x, y, width, height, PositionRight, fillColor, strokeColor, strokeWidth)
+}
+
+// ArrowUp draws an arrow at the given point and dimensions pointing up.
+func (p *Painter) ArrowUp(x, y, width, height int,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ p.arrow(x, y, width, height, PositionTop, fillColor, strokeColor, strokeWidth)
+}
+
+// ArrowDown draws an arrow at the given point and dimensions pointing down.
+func (p *Painter) ArrowDown(x, y, width, height int,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ p.arrow(x, y, width, height, PositionBottom, fillColor, strokeColor, strokeWidth)
}
-// FillArea draws a filled polygon through the given points, skipping "null" (MaxInt32) break values (filling the area
-// flat between them).
-func (p *Painter) FillArea(points []Point) {
+// FillArea draws a filled polygon through the given points, skipping "null" (MaxInt32) break values
+// (filling the area flat between them).
+func (p *Painter) FillArea(points []Point, fillColor Color) {
if len(points) == 0 {
return
}
@@ -550,7 +506,7 @@ func (p *Painter) FillArea(points []Point) {
if pt.Y == math.MaxInt32 {
// If we encounter a break, fill the accumulated segment
p.drawStraightPath(valid, false)
- p.render.Fill()
+ p.fill(fillColor)
valid = valid[:0] // reset
continue
}
@@ -559,14 +515,14 @@ func (p *Painter) FillArea(points []Point) {
// Fill the last segment if there is one
p.drawStraightPath(valid, false)
- p.render.Fill()
+ p.fill(fillColor)
}
-// smoothFillArea draws a smooth curve for the "top" portion of points but uses straight lines for the bottom corners,
-// producing a fill with sharp corners.
-func (p *Painter) smoothFillChartArea(points []Point, tension float64) {
+// smoothFillChartArea draws a smooth curve for the "top" portion of points but uses straight lines for
+// the bottom corners, producing a fill with sharp corners.
+func (p *Painter) smoothFillChartArea(points []Point, tension float64, fillColor Color) {
if tension <= 0 {
- p.FillArea(points)
+ p.FillArea(points, fillColor)
return
} else if tension > 1 {
tension = 1
@@ -577,17 +533,17 @@ func (p *Painter) smoothFillChartArea(points []Point, tension float64) {
// We'll separate them:
if len(points) < 3 {
// Not enough to separate top from bottom
- p.FillArea(points)
+ p.FillArea(points, fillColor)
return
}
// The final 3 points are the corners + repeated first point
top := points[:len(points)-3]
- bottom := points[len(points)-3:] // [ corner1, corner2, firstTopAgain ]
+ bottom := points[len(points)-3:] // [corner1, corner2, firstTopAgain]
// If top portion is empty or 1 point, just fill straight
if len(top) < 2 {
- p.FillArea(points)
+ p.FillArea(points, fillColor)
return
}
@@ -614,7 +570,7 @@ func (p *Painter) smoothFillChartArea(points []Point, tension float64) {
}
if !firstPointSet {
- p.FillArea(points) // No actual top segment was drawn, fallback to straight fill
+ p.FillArea(points, fillColor) // No actual top segment was drawn, fallback to straight fill
return
}
@@ -623,40 +579,56 @@ func (p *Painter) smoothFillChartArea(points []Point, tension float64) {
for i := 0; i < len(bottom); i++ {
p.lineTo(bottom[i].X, bottom[i].Y)
}
-
- p.render.Fill()
+ p.fill(fillColor)
}
-func (p *Painter) Text(body string, x, y int) {
- p.render.Text(body, x+p.box.Left, y+p.box.Top)
+// Text draws the given string at the position specified, using the given font style.
+func (p *Painter) Text(body string, x, y int, fontStyle FontStyle) {
+ if fontStyle.Font == nil {
+ fontStyle.Font = getPreferredFont(p.font)
+ }
+ p.TextRotation(body, x, y, fontStyle, 0)
}
-func (p *Painter) TextRotation(body string, x, y int, radians float64) {
- p.render.SetTextRotation(radians)
+// TextRotation draws rotated text at the given position, using the given radians and font style.
+func (p *Painter) TextRotation(body string, x, y int, fontStyle FontStyle, radians float64) {
+ if fontStyle.Font == nil {
+ fontStyle.Font = getPreferredFont(p.font)
+ }
+ defer p.render.ResetStyle()
+ p.render.SetFont(fontStyle.Font)
+ p.render.SetFontSize(fontStyle.FontSize)
+ p.render.SetFontColor(fontStyle.FontColor)
+ if radians != 0 {
+ defer p.render.ClearTextRotation()
+ p.render.SetTextRotation(radians)
+ }
p.render.Text(body, x+p.box.Left, y+p.box.Top)
- p.render.ClearTextRotation()
}
-func (p *Painter) setTextRotation(radians float64) {
- p.render.SetTextRotation(radians)
-}
-func (p *Painter) clearTextRotation() {
- p.render.ClearTextRotation()
-}
-
-func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chartdraw.Box {
- style := p.style
- textWarp := style.TextWrap
- style.TextWrap = chartdraw.TextWrapWord
+// TextFit draws multi-line text constrained to a given width.
+func (p *Painter) TextFit(body string, x, y, width int, fontStyle FontStyle, textAligns ...string) chartdraw.Box {
+ if fontStyle.Font == nil {
+ fontStyle.Font = getPreferredFont(p.font)
+ }
+ style := chartdraw.Style{
+ FontStyle: fontStyle,
+ TextWrap: chartdraw.TextWrapWord,
+ }
r := p.render
+ defer r.ResetStyle()
+ r.SetFont(fontStyle.Font)
+ r.SetFontSize(fontStyle.FontSize)
+ r.SetFontColor(fontStyle.FontColor)
+
lines := chartdraw.Text.WrapFit(r, body, width, style)
- p.SetFontStyle(style.FontStyle)
- var output chartdraw.Box
+ var output chartdraw.Box
textAlign := ""
if len(textAligns) != 0 {
textAlign = textAligns[0]
}
+
for index, line := range lines {
if line == "" {
continue
@@ -670,17 +642,19 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch
case AlignCenter:
x0 += (width - lineBox.Width()) >> 1
}
- p.Text(line, x0, y0)
+
+ p.render.Text(line, x0+p.box.Left, y0+p.box.Top)
output.Right = chartdraw.MaxInt(lineBox.Right, output.Right)
output.Bottom += lineBox.Height()
if index < len(lines)-1 {
- output.Bottom += +style.GetTextLineSpacing()
+ output.Bottom += style.GetTextLineSpacing()
}
}
- p.style.TextWrap = textWarp
+ output.IsSet = true
return output
}
+// isTick determines whether the given index is a "tick" mark out of numTicks.
func isTick(totalRange int, numTicks int, index int) bool {
if numTicks >= totalRange {
return true
@@ -700,6 +674,7 @@ func isTick(totalRange int, numTicks int, index int) bool {
return actualTickIndex == index
}
+// ticks draws small lines to indicate tick marks, using a fixed stroke color/width.
func (p *Painter) ticks(opt ticksOption) {
if opt.labelCount <= 0 || opt.length <= 0 {
return
@@ -720,16 +695,17 @@ func (p *Painter) ticks(opt ticksOption) {
p.LineStroke([]Point{
{X: 0, Y: value},
{X: opt.length, Y: value},
- })
+ }, opt.strokeColor, opt.strokeWidth)
} else {
p.LineStroke([]Point{
{X: value, Y: opt.length},
{X: value, Y: 0},
- })
+ }, opt.strokeColor, opt.strokeWidth)
}
}
}
+// multiText prints multiple lines of text for axis labels.
func (p *Painter) multiText(opt multiTextOption) {
if len(opt.textList) == 0 {
return
@@ -751,7 +727,10 @@ func (p *Painter) multiText(opt multiTextOption) {
positions = autoDivide(width, count-1)
}
}
- isTextRotation := opt.textRotation != 0
+ if opt.textRotation != 0 {
+ defer p.render.ClearTextRotation()
+ p.render.SetTextRotation(opt.textRotation)
+ }
positionCount := len(positions)
skippedLabels := opt.labelSkipCount // specify the skip count to ensure the top value is listed
@@ -772,12 +751,8 @@ func (p *Painter) multiText(opt multiTextOption) {
skippedLabels = 0
}
- if isTextRotation {
- p.clearTextRotation()
- p.setTextRotation(opt.textRotation)
- }
text := opt.textList[index]
- box := p.MeasureText(text)
+ box := p.MeasureText(text, opt.fontStyle, 0)
x := 0
y := 0
if opt.vertical {
@@ -819,38 +794,35 @@ func (p *Painter) multiText(opt multiTextOption) {
}
x += opt.offset.Left
y += opt.offset.Top
- p.Text(text, x, y)
- }
- if isTextRotation {
- p.clearTextRotation()
+ p.Text(text, x, y, opt.fontStyle)
}
}
// Dots prints filled circles for the given points.
-func (p *Painter) Dots(points []Point) {
+func (p *Painter) Dots(points []Point, fillColor, strokeColor Color, strokeWidth float64, dotRadius float64) {
+ defer p.render.ResetStyle()
+ p.render.SetFillColor(fillColor)
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
for _, item := range points {
- p.Circle(2, item.X, item.Y)
+ p.render.Circle(dotRadius, item.X+p.box.Left, item.Y+p.box.Top)
}
- p.fillStroke()
+ p.render.FillStroke()
}
-// Rect will draw a box with the given coordinates.
-func (p *Painter) Rect(box Box) {
+// filledRect will draw a filled box with the given coordinates.
+func (p *Painter) filledRect(box Box, fillColor, strokeColor Color, strokeWidth float64) {
p.moveTo(box.Left, box.Top)
p.lineTo(box.Right, box.Top)
p.lineTo(box.Right, box.Bottom)
p.lineTo(box.Left, box.Bottom)
p.lineTo(box.Left, box.Top)
-}
-
-// filledRect will draw a filled box with the given coordinates.
-func (p *Painter) filledRect(box Box) {
- p.Rect(box)
- p.fillStroke()
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
}
// roundedRect is similar to filledRect except the top and bottom will be rounded.
-func (p *Painter) roundedRect(box Box, radius int, roundTop, roundBottom bool) {
+func (p *Painter) roundedRect(box Box, radius int, roundTop, roundBottom bool,
+ fillColor, strokeColor Color, strokeWidth float64) {
r := (box.Right - box.Left) / 2
if radius > r {
radius = r
@@ -860,14 +832,16 @@ func (p *Painter) roundedRect(box Box, radius int, roundTop, roundBottom bool) {
if roundTop {
// Start at the appropriate point depending on rounding at the top
- p.Line(box.Left+radius, box.Top, box.Right-radius, box.Top)
+ p.moveTo(box.Left+radius, box.Top)
+ p.lineTo(box.Right-radius, box.Top)
// right top
cx := box.Right - radius
cy := box.Top + radius
p.arcTo(cx, cy, rx, ry, -math.Pi/2, math.Pi/2)
} else {
- p.Line(box.Left, box.Top, box.Right, box.Top)
+ p.moveTo(box.Left, box.Top)
+ p.lineTo(box.Right, box.Top)
}
if roundBottom {
@@ -900,22 +874,23 @@ func (p *Painter) roundedRect(box Box, radius int, roundTop, roundBottom bool) {
}
p.close()
- p.fillStroke()
- p.render.Fill()
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
}
-func (p *Painter) legendLineDot(box Box) {
- width := box.Width()
- height := box.Height()
- strokeWidth := 3
- dotHeight := 5
+// legendLineDot draws a small horizontal line with a dot in the middle, often used in legends.
+func (p *Painter) legendLineDot(box Box, strokeColor Color, strokeWidth float64, dotColor Color) {
+ center := (box.Height()-int(strokeWidth))>>1 - 1
+
+ defer p.render.ResetStyle()
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
+ p.moveTo(box.Left, box.Top-center)
+ p.lineTo(box.Right, box.Top-center)
+ p.render.Stroke()
- p.render.SetStrokeWidth(float64(strokeWidth))
- center := (height-strokeWidth)>>1 - 1
- p.Line(box.Left, box.Top-center, box.Right, box.Top-center)
- p.stroke()
- p.Circle(float64(dotHeight), box.Left+width>>1, box.Top-center)
- p.fillStroke()
+ // draw dot in the middle
+ midX := box.Left + (box.Width() >> 1)
+ p.Circle(5, midX, box.Top-center, dotColor, dotColor, 3)
}
// BarChart renders a bar chart with the provided configuration to the painter.
diff --git a/painter_test.go b/painter_test.go
index 32c2f18..6f38208 100644
--- a/painter_test.go
+++ b/painter_test.go
@@ -31,7 +31,7 @@ func TestPainterOption(t *testing.T) {
}, p.box)
}
-func TestPainter(t *testing.T) {
+func TestPainterInternal(t *testing.T) {
t.Parallel()
tests := []struct {
@@ -39,162 +39,161 @@ func TestPainter(t *testing.T) {
fn func(*Painter)
result string
}{
+ {
+ name: "circle",
+ fn: func(p *Painter) {
+ p.Circle(5, 2, 3, drawing.ColorTransparent, drawing.ColorTransparent, 1.0)
+ },
+ result: "",
+ },
{
name: "moveTo_lineTo",
fn: func(p *Painter) {
p.moveTo(1, 1)
p.lineTo(2, 2)
- p.stroke()
+ p.stroke(drawing.ColorTransparent, 1.0)
},
- result: "",
+ result: "",
},
{
- name: "circle",
+ name: "arc",
fn: func(p *Painter) {
- p.Circle(5, 2, 3)
+ p.arcTo(100, 100, 100, 100, 0, math.Pi/2)
+ p.close()
+ p.fillStroke(drawing.ColorBlue, drawing.ColorBlack, 1)
},
- result: "",
+ result: "",
},
+ }
+
+ for i, tt := range tests {
+ t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
+ p := NewPainter(PainterOptions{
+ OutputFormat: ChartOutputSVG,
+ Width: 400,
+ Height: 300,
+ }, PainterPaddingOption(chartdraw.Box{Left: 5, Top: 10}))
+ tt.fn(p)
+ data, err := p.Bytes()
+ require.NoError(t, err)
+ assertEqualSVG(t, tt.result, data)
+ })
+ }
+}
+
+func TestPainterExternal(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ fn func(*Painter)
+ result string
+ }{
{
name: "text",
fn: func(p *Painter) {
- p.Text("hello world!", 3, 6)
+ p.Text("hello world!", 3, 6, FontStyle{})
},
- result: "",
+ result: "",
},
{
- name: "line",
+ name: "line_stroke",
fn: func(p *Painter) {
- p.SetDrawingStyle(chartdraw.Style{
- StrokeColor: drawing.ColorBlack,
- StrokeWidth: 1,
- })
p.LineStroke([]Point{
- {X: 1, Y: 2},
- {X: 3, Y: 4},
- })
+ {X: 10, Y: 20},
+ {X: 30, Y: 40},
+ {X: 50, Y: 20},
+ }, drawing.ColorBlack, 1)
},
- result: "",
+ result: "",
},
{
- name: "background",
+ name: "smooth_line_stroke",
fn: func(p *Painter) {
- p.SetBackground(400, 300, chartdraw.ColorWhite)
+ p.SmoothLineStroke([]Point{
+ {X: 10, Y: 20},
+ {X: 20, Y: 40},
+ {X: 30, Y: 60},
+ {X: 40, Y: 50},
+ {X: 50, Y: 40},
+ {X: 60, Y: 80},
+ }, 0.5, drawing.ColorBlack, 1)
},
- result: "",
+ result: "",
},
{
- name: "arc",
+ name: "background",
fn: func(p *Painter) {
- p.setStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: drawing.ColorBlack,
- FillColor: drawing.ColorBlue,
- })
- p.arcTo(100, 100, 100, 100, 0, math.Pi/2)
- p.close()
- p.fillStroke()
+ p.SetBackground(400, 300, chartdraw.ColorWhite)
},
- result: "",
+ result: "",
},
{
name: "pin",
fn: func(p *Painter) {
- p.setStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.Pin(30, 30, 30)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.Pin(30, 30, 30, c, c, 1)
},
- result: "",
+ result: "",
},
{
name: "arrow_left",
fn: func(p *Painter) {
- p.setStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.ArrowLeft(30, 30, 16, 10)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.ArrowLeft(30, 30, 16, 10, c, c, 1)
},
- result: "",
+ result: "",
},
{
name: "arrow_right",
fn: func(p *Painter) {
- p.setStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.ArrowRight(30, 30, 16, 10)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.ArrowRight(30, 30, 16, 10, c, c, 1)
},
- result: "",
+ result: "",
},
{
name: "arrow_up",
fn: func(p *Painter) {
- p.setStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.ArrowUp(30, 30, 10, 16)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.ArrowUp(30, 30, 10, 16, c, c, 1)
},
- result: "",
+ result: "",
},
{
name: "arrow_down",
fn: func(p *Painter) {
- p.setStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.ArrowDown(30, 30, 10, 16)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.ArrowDown(30, 30, 10, 16, c, c, 1)
},
- result: "",
+ result: "",
},
{
name: "mark_line",
fn: func(p *Painter) {
- p.setStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- StrokeDashArray: []float64{4, 2},
- })
- p.MarkLine(0, 20, 300)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.MarkLine(0, 20, 300, c, c, 1, []float64{4, 2})
},
- result: "",
+ result: "",
},
{
name: "polygon",
fn: func(p *Painter) {
- p.setStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.Polygon(Point{X: 100, Y: 100}, 50, 6)
+ p.Polygon(Point{X: 100, Y: 100}, 50, 6, Color{R: 84, G: 112, B: 198, A: 255}, 1)
},
- result: "",
+ result: "",
},
{
name: "fill_area",
fn: func(p *Painter) {
- p.SetDrawingStyle(chartdraw.Style{
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
p.FillArea([]Point{
{X: 0, Y: 0},
{X: 0, Y: 100},
{X: 100, Y: 100},
{X: 0, Y: 0},
- })
+ }, Color{R: 84, G: 112, B: 198, A: 255})
},
- result: "",
+ result: "",
},
{
name: "child_chart",
@@ -205,9 +204,10 @@ func TestPainter(t *testing.T) {
opt.Theme = GetDefaultTheme().WithBackgroundColor(drawing.ColorFromAlphaMixedRGBA(0, 0, 0, 0))
_ = p.LineChart(opt)
},
- result: "",
+ result: "",
},
}
+
for i, tt := range tests {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
p := NewPainter(PainterOptions{
@@ -223,7 +223,7 @@ func TestPainter(t *testing.T) {
}
}
-func TestRoundedRect(t *testing.T) {
+func TestPainterRoundedRect(t *testing.T) {
t.Parallel()
tests := []struct {
@@ -239,9 +239,9 @@ func TestRoundedRect(t *testing.T) {
Right: 30,
Bottom: 150,
Top: 10,
- }, 5, true, true)
+ }, 5, true, true, drawing.ColorBlue, drawing.ColorBlue, 1)
},
- result: "",
+ result: "",
},
{
name: "square_top",
@@ -251,9 +251,9 @@ func TestRoundedRect(t *testing.T) {
Right: 30,
Bottom: 150,
Top: 10,
- }, 5, false, true)
+ }, 5, false, true, drawing.ColorBlue, drawing.ColorBlue, 1)
},
- result: "",
+ result: "",
},
{
name: "square_bottom",
@@ -263,9 +263,9 @@ func TestRoundedRect(t *testing.T) {
Right: 30,
Bottom: 150,
Top: 10,
- }, 5, true, false)
+ }, 5, true, false, drawing.ColorBlue, drawing.ColorBlue, 1)
},
- result: "",
+ result: "",
},
}
@@ -276,11 +276,6 @@ func TestRoundedRect(t *testing.T) {
Height: 300,
OutputFormat: ChartOutputSVG,
})
- p.OverrideDrawingStyle(chartdraw.Style{
- FillColor: drawing.ColorBlue,
- StrokeWidth: 1,
- StrokeColor: drawing.ColorBlue,
- })
tc.fn(p)
buf, err := p.Bytes()
require.NoError(t, err)
@@ -289,6 +284,31 @@ func TestRoundedRect(t *testing.T) {
}
}
+func TestPainterMeasureText(t *testing.T) {
+ t.Parallel()
+
+ svgP := NewPainter(PainterOptions{
+ OutputFormat: ChartOutputSVG,
+ Width: 400,
+ Height: 300,
+ })
+ pngP := NewPainter(PainterOptions{
+ OutputFormat: ChartOutputPNG,
+ Width: 400,
+ Height: 300,
+ })
+ style := FontStyle{
+ FontSize: 12,
+ FontColor: chartdraw.ColorBlack,
+ Font: GetDefaultFont(),
+ }
+
+ assert.Equal(t, chartdraw.Box{Right: 84, Bottom: 15, IsSet: true},
+ svgP.MeasureText("Hello World!", style, 0))
+ assert.Equal(t, chartdraw.Box{Right: 99, Bottom: 14, IsSet: true},
+ pngP.MeasureText("Hello World!", style, 0))
+}
+
func TestPainterTextFit(t *testing.T) {
t.Parallel()
@@ -297,21 +317,21 @@ func TestPainterTextFit(t *testing.T) {
Width: 400,
Height: 300,
})
- style := FontStyle{
+ fontStyle := FontStyle{
FontSize: 12,
FontColor: chartdraw.ColorBlack,
Font: GetDefaultFont(),
}
- p.setStyle(chartdraw.Style{FontStyle: style})
- box := p.TextFit("Hello World!", 0, 20, 80)
- assert.Equal(t, chartdraw.Box{Right: 45, Bottom: 35}, box)
- box = p.TextFit("Hello World!", 0, 100, 200)
- assert.Equal(t, chartdraw.Box{Right: 84, Bottom: 15}, box)
+ box := p.TextFit("Hello World!", 0, 20, 80, fontStyle)
+ assert.Equal(t, chartdraw.Box{Right: 45, Bottom: 35, IsSet: true}, box)
+
+ box = p.TextFit("Hello World!", 0, 100, 200, fontStyle)
+ assert.Equal(t, chartdraw.Box{Right: 84, Bottom: 15, IsSet: true}, box)
buf, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", buf)
+ assertEqualSVG(t, "", buf)
}
func TestMultipleChartsOnPainter(t *testing.T) {
@@ -342,5 +362,5 @@ func TestMultipleChartsOnPainter(t *testing.T) {
buf, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", buf)
+ assertEqualSVG(t, "", buf)
}
diff --git a/pie_chart.go b/pie_chart.go
index b1bcc78..2f1f5d0 100644
--- a/pie_chart.go
+++ b/pie_chart.go
@@ -196,16 +196,11 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
maxY := 0
minY := 0
for _, s := range sectors {
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: s.color,
- FillColor: s.color,
- })
seriesPainter.moveTo(s.cx, s.cy)
seriesPainter.arcTo(s.cx, s.cy, s.rx, s.ry, s.start, s.delta)
seriesPainter.lineTo(s.cx, s.cy)
seriesPainter.close()
- seriesPainter.fillStroke()
+ seriesPainter.fillStroke(s.color, s.color, 1)
if s.label == "" {
continue
}
@@ -235,9 +230,11 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
if prevY < minY {
minY = prevY
}
- seriesPainter.Line(s.lineStartX, s.lineStartY, s.lineBranchX, s.lineBranchY)
- seriesPainter.Line(s.lineBranchX, s.lineBranchY, s.lineEndX, s.lineEndY)
- seriesPainter.stroke()
+ seriesPainter.moveTo(s.lineStartX, s.lineStartY)
+ seriesPainter.lineTo(s.lineBranchX, s.lineBranchY)
+ seriesPainter.moveTo(s.lineBranchX, s.lineBranchY)
+ seriesPainter.lineTo(s.lineEndX, s.lineEndY)
+ seriesPainter.stroke(s.color, 1)
textStyle := FontStyle{
FontColor: theme.GetTextColor(),
FontSize: labelFontSize,
@@ -246,9 +243,8 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
if !s.series.Label.FontStyle.FontColor.IsZero() {
textStyle.FontColor = s.series.Label.FontStyle.FontColor
}
- seriesPainter.OverrideFontStyle(textStyle)
- x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label))
- seriesPainter.Text(s.label, x, y)
+ x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label, textStyle, 0))
+ seriesPainter.Text(s.label, x, y, textStyle)
}
return p.p.box, nil
}
diff --git a/pie_chart_test.go b/pie_chart_test.go
index f58a8ef..c32ec00 100644
--- a/pie_chart_test.go
+++ b/pie_chart_test.go
@@ -51,13 +51,13 @@ func TestPieChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicPieChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicPieChartOption,
- result: "",
+ result: "",
},
{
name: "lots_labels-sortedDescending",
@@ -120,7 +120,7 @@ func TestPieChart(t *testing.T) {
},
}
},
- result: "",
+ result: "",
},
{
name: "lots_labels-unsorted",
@@ -183,7 +183,7 @@ func TestPieChart(t *testing.T) {
},
}
},
- result: "",
+ result: "",
},
{
name: "100labels-sorted",
@@ -214,7 +214,7 @@ func TestPieChart(t *testing.T) {
},
}
},
- result: "",
+ result: "",
},
{
name: "fix_label_pos",
@@ -259,7 +259,7 @@ func TestPieChart(t *testing.T) {
},
}
},
- result: "",
+ result: "",
},
{
name: "custom_fonts",
@@ -274,7 +274,7 @@ func TestPieChart(t *testing.T) {
opt.Title.FontStyle = customFont
return opt
},
- result: "",
+ result: "",
},
{
name: "legend_bottom_right",
@@ -287,7 +287,7 @@ func TestPieChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
}
diff --git a/radar_chart.go b/radar_chart.go
index a6ce2c2..d0c1d4b 100644
--- a/radar_chart.go
+++ b/radar_chart.go
@@ -106,29 +106,26 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
divideRadius := float64(int(radius / float64(divideCount)))
radius = divideRadius * float64(divideCount)
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- StrokeColor: theme.GetAxisSplitLineColor(),
- StrokeWidth: 1,
- })
center := Point{X: cx, Y: cy}
for i := 0; i < divideCount; i++ {
- seriesPainter.Polygon(center, divideRadius*float64(i+1), sides)
+ seriesPainter.Polygon(center, divideRadius*float64(i+1), sides, theme.GetAxisSplitLineColor(), 1)
}
points := getPolygonPoints(center, radius, sides)
for _, p := range points {
- seriesPainter.Line(center.X, center.Y, p.X, p.Y)
- seriesPainter.stroke()
+ seriesPainter.moveTo(center.X, center.Y)
+ seriesPainter.lineTo(p.X, p.Y)
+ seriesPainter.stroke(theme.GetAxisSplitLineColor(), 1)
}
- seriesPainter.OverrideFontStyle(FontStyle{
+ fontStyle := FontStyle{
FontColor: theme.GetTextColor(),
FontSize: labelFontSize,
Font: opt.Font,
- })
+ }
offset := 5
// text generation
for index, p := range points {
name := indicators[index].Name
- b := seriesPainter.MeasureText(name)
+ b := seriesPainter.MeasureText(name, fontStyle, 0)
isXCenter := p.X == center.X
isYCenter := p.Y == center.Y
isRight := p.X > center.X
@@ -160,7 +157,7 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
if isLeft {
x -= b.Width() + offset
}
- seriesPainter.Text(name, x, y)
+ seriesPainter.Text(name, x, y, fontStyle)
}
// radar chart
@@ -188,28 +185,15 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
dotFillColor = color
}
linePoints = append(linePoints, linePoints[0])
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- StrokeColor: color,
- StrokeWidth: defaultStrokeWidth,
- DotWidth: defaultDotWidth,
- DotColor: color,
- FillColor: color.WithAlpha(20),
- })
- seriesPainter.LineStroke(linePoints)
- seriesPainter.FillArea(linePoints)
- dotWith := 2.0
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- StrokeWidth: defaultStrokeWidth,
- StrokeColor: color,
- FillColor: dotFillColor,
- })
+ seriesPainter.LineStroke(linePoints, color, defaultStrokeWidth)
+ seriesPainter.FillArea(linePoints, color.WithAlpha(20))
+ dotWith := defaultDotWidth
for index, point := range linePoints {
- seriesPainter.Circle(dotWith, point.X, point.Y)
- seriesPainter.fillStroke()
+ seriesPainter.Circle(dotWith, point.X, point.Y, dotFillColor, color, defaultStrokeWidth)
if flagIs(true, series.Label.Show) && index < len(series.Data) {
value := humanize.FtoaWithDigits(series.Data[index], 2)
- b := seriesPainter.MeasureText(value)
- seriesPainter.Text(value, point.X-b.Width()/2, point.Y)
+ b := seriesPainter.MeasureText(value, fontStyle, 0)
+ seriesPainter.Text(value, point.X-b.Width()/2, point.Y, fontStyle)
}
}
}
diff --git a/radar_chart_test.go b/radar_chart_test.go
index 6a73756..28e14ac 100644
--- a/radar_chart_test.go
+++ b/radar_chart_test.go
@@ -48,13 +48,13 @@ func TestRadarChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicRadarChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicRadarChartOption,
- result: "",
+ result: "",
},
}
diff --git a/series_label.go b/series_label.go
index bd6a509..351e83f 100644
--- a/series_label.go
+++ b/series_label.go
@@ -69,12 +69,7 @@ func (o *seriesLabelPainter) Add(value labelValue) {
labelStyle.FontColor = value.fontStyle.FontColor
}
p := o.p
- p.OverrideDrawingStyle(chartdraw.Style{FontStyle: labelStyle})
- rotated := value.radians != 0
- if rotated {
- p.setTextRotation(value.radians)
- }
- textBox := p.MeasureText(text)
+ textBox := p.MeasureText(text, labelStyle, value.radians)
renderValue := labelRenderValue{
Text: text,
FontStyle: labelStyle,
@@ -92,9 +87,6 @@ func (o *seriesLabelPainter) Add(value labelValue) {
}
if value.radians != 0 {
renderValue.X = value.x + (textBox.Width() >> 1) - 1
- p.clearTextRotation()
- } else if textBox.Width()%2 != 0 {
- renderValue.X++
}
renderValue.X += value.offset.Left
renderValue.Y += value.offset.Top
@@ -103,11 +95,10 @@ func (o *seriesLabelPainter) Add(value labelValue) {
func (o *seriesLabelPainter) Render() (Box, error) {
for _, item := range o.values {
- o.p.OverrideFontStyle(item.FontStyle)
if item.Radians != 0 {
- o.p.TextRotation(item.Text, item.X, item.Y, item.Radians)
+ o.p.TextRotation(item.Text, item.X, item.Y, item.FontStyle, item.Radians)
} else {
- o.p.Text(item.Text, item.X, item.Y)
+ o.p.Text(item.Text, item.X, item.Y, item.FontStyle)
}
}
return chartdraw.BoxZero, nil
diff --git a/table.go b/table.go
index 7790869..52201f7 100644
--- a/table.go
+++ b/table.go
@@ -218,13 +218,10 @@ func (t *tableChart) render() (*renderInfo, error) {
info.columnWidths = columnWidths
height := 0
- style := chartdraw.Style{
- FontStyle: FontStyle{
- FontSize: fontStyle.FontSize,
- FontColor: opt.HeaderFontColor,
- Font: fontStyle.Font,
- },
- FillColor: opt.HeaderFontColor,
+ headerFontStyle := FontStyle{
+ FontSize: fontStyle.FontSize,
+ FontColor: opt.HeaderFontColor,
+ Font: fontStyle.Font,
}
// textAligns := opt.TextAligns
@@ -237,7 +234,8 @@ func (t *tableChart) render() (*renderInfo, error) {
// processing of the table cells
renderTableCells := func(
- style chartdraw.Style,
+ fontStyle FontStyle,
+ fillColor Color,
rowIndex int,
textList []string,
currentHeight int,
@@ -252,23 +250,22 @@ func (t *tableChart) render() (*renderInfo, error) {
Text: text,
Row: rowIndex,
Column: index,
- FontStyle: style.FontStyle,
- FillColor: style.FillColor,
+ FontStyle: fontStyle,
+ FillColor: fillColor,
}
if opt.CellModifier != nil {
tc = opt.CellModifier(tc)
// Update style values to capture any changes
- style.FontStyle = tc.FontStyle
- style.FillColor = tc.FillColor
+ fontStyle = tc.FontStyle
+ fillColor = tc.FillColor
}
cells[index] = tc
- p.setStyle(style)
x := values[index]
y := currentHeight + cellPadding.Top
width := values[index+1] - x
x += cellPadding.Left
width -= paddingWidth
- box := p.TextFit(text, x, y+int(fontStyle.FontSize), width, getTextAlign(index))
+ box := p.TextFit(text, x, y+int(fontStyle.FontSize), width, fontStyle, getTextAlign(index))
// calculate the highest height
if box.Height()+paddingHeight > cellMaxHeight {
cellMaxHeight = box.Height() + paddingHeight
@@ -280,16 +277,15 @@ func (t *tableChart) render() (*renderInfo, error) {
info.tableCells = make([][]TableCell, len(opt.Data)+1)
// processing of the table headers
- headerCells, headerHeight := renderTableCells(style, 0, opt.Header, height, opt.Padding)
+ headerCells, headerHeight := renderTableCells(headerFontStyle, opt.HeaderFontColor,
+ 0, opt.Header, height, opt.Padding)
info.tableCells[0] = headerCells
height += headerHeight
info.headerHeight = headerHeight
// processing of the table contents
- style.FontColor = fontStyle.FontColor
- style.FillColor = fontStyle.FontColor
for index, textList := range opt.Data {
- newCells, cellHeight := renderTableCells(style, index+1, textList, height, opt.Padding)
+ newCells, cellHeight := renderTableCells(fontStyle, fontStyle.FontColor, index+1, textList, height, opt.Padding)
info.tableCells[index+1] = newCells
info.rowHeights = append(info.rowHeights, cellHeight)
height += cellHeight
@@ -317,7 +313,7 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) {
opt.HeaderBackgroundColor = tableLightThemeSetting.headerColor
}
}
- p.SetBackground(info.width, info.headerHeight, opt.HeaderBackgroundColor, true)
+ p.SetBackground(info.width, info.headerHeight, opt.HeaderBackgroundColor)
if opt.RowBackgroundColors == nil {
if opt.Theme.IsDark() {
@@ -334,7 +330,7 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) {
Top: currentHeight,
IsSet: true,
}))
- child.SetBackground(p.Width(), h, color, true)
+ child.SetBackground(p.Width(), h, color)
currentHeight += h
}
// adjust the background color according to the set table style
@@ -359,7 +355,7 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) {
}))
w := info.columnWidths[j] - padding.Left - padding.Top
h := heights[i] - padding.Top - padding.Bottom
- child.SetBackground(w, h, tc.FillColor, true)
+ child.SetBackground(w, h, tc.FillColor)
}
left += info.columnWidths[j]
}
diff --git a/table_test.go b/table_test.go
index 43cb216..877e011 100644
--- a/table_test.go
+++ b/table_test.go
@@ -71,13 +71,13 @@ func TestTableChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "dark_theme",
theme: GetTheme(ThemeVividDark),
makeOptions: makeDefaultTableChartOptions,
- result: "",
+ result: "",
},
{
name: "cell_modified",
@@ -98,7 +98,7 @@ func TestTableChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "error_no_header",
@@ -117,7 +117,7 @@ func TestTableChart(t *testing.T) {
Width: 600,
Height: 400,
}
- runName := strconv.Itoa(i)
+ runName := strconv.Itoa(i) + "-" + tt.name
if tt.theme != nil {
t.Run(runName+"-theme_painter", func(t *testing.T) {
p := NewPainter(painterOptions, PainterThemeOption(tt.theme))
diff --git a/theme_test.go b/theme_test.go
index 6ae5eed..f0a4b76 100644
--- a/theme_test.go
+++ b/theme_test.go
@@ -83,42 +83,42 @@ func TestThemeLight(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeLight)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeDark(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeDark)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeVividLight(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeVividLight)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeVividDark(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeVividDark)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeAnt(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeAnt)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeGrafana(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeGrafana)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestLightThemeSeriesRepeat(t *testing.T) {
@@ -137,7 +137,7 @@ func TestLightThemeSeriesRepeat(t *testing.T) {
{R: 200, G: 50, B: 50, A: 255},
},
})
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestDarkThemeSeriesRepeat(t *testing.T) {
@@ -156,7 +156,7 @@ func TestDarkThemeSeriesRepeat(t *testing.T) {
{R: 200, G: 50, B: 50, A: 255},
},
})
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestWithAxisColor(t *testing.T) {
diff --git a/title.go b/title.go
index c0f967e..2cd1c69 100644
--- a/title.go
+++ b/title.go
@@ -111,8 +111,7 @@ func (t *titlePainter) Render() (Box, error) {
textMaxHeight := 0
textTotalHeight := 0
for index, item := range measureOptions {
- p.OverrideFontStyle(item.style)
- textBox := p.MeasureText(item.text)
+ textBox := p.MeasureText(item.text, item.style, 0)
w := textBox.Width()
h := textBox.Height()
@@ -159,10 +158,9 @@ func (t *titlePainter) Render() (Box, error) {
}
startY := titleY
for _, item := range measureOptions {
- p.OverrideFontStyle(item.style)
x := titleX + (textMaxWidth-item.width)>>1
y := titleY + item.height
- p.Text(item.text, x, y)
+ p.Text(item.text, x, y, item.style)
titleY = y
}
diff --git a/title_test.go b/title_test.go
index 4ac862b..6bfea5b 100644
--- a/title_test.go
+++ b/title_test.go
@@ -47,7 +47,7 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_percent",
@@ -65,7 +65,7 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_right",
@@ -80,7 +80,7 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_center",
@@ -95,7 +95,7 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_bottom",
@@ -112,7 +112,7 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_bottom_right",
@@ -130,7 +130,7 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_bottom_center",
@@ -148,7 +148,7 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "custom_font",
@@ -170,7 +170,7 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
diff --git a/yaxis_test.go b/yaxis_test.go
index ceae1f5..3d900e4 100644
--- a/yaxis_test.go
+++ b/yaxis_test.go
@@ -25,7 +25,7 @@ func TestRightYAxis(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
for i, tt := range tests {