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: "MonTueWedThuFriSatSun", + result: "MonTueWedThuFriSatSun", }, { name: "x-axis_bottom_left", @@ -48,7 +48,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "MonTueWedThuFriSatSun", + result: "MonTueWedThuFriSatSun", }, { name: "y-axis_left", @@ -59,7 +59,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "MonTueWedThuFriSatSun", + result: "MonTueWedThuFriSatSun", }, { name: "y-axis_center", @@ -72,7 +72,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "MonTueWedThuFriSatSun", + result: "MonTueWedThuFriSatSun", }, { name: "y-axis_right", @@ -85,7 +85,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "MonTueWedThuFriSatSun", + result: "MonTueWedThuFriSatSun", }, { name: "top", @@ -97,7 +97,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "Mon --Tue --Wed --Thu --Fri --Sat --Sun --", + result: "Mon --Tue --Wed --Thu --Fri --Sat --Sun --", }, { name: "reduced_label_count", @@ -109,7 +109,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "ABCEFG", + result: "ABCEFG", }, { name: "custom_unit", @@ -121,7 +121,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "AEG", + result: "AEG", }, { name: "custom_font", @@ -135,7 +135,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "ABCDEFG", + result: "ABCDEFG", }, { name: "boundary_gap_disable", @@ -146,7 +146,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "ABCDEFG", + result: "ABCDEFG", }, { name: "boundary_gap_enable", @@ -157,7 +157,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "ABCDEFG", + result: "ABCDEFG", }, } 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: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, { name: "themed", defaultTheme: false, makeOptions: makeBasicBarChartOption, - result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, { name: "rounded_caps", @@ -69,7 +69,7 @@ func TestBarChart(t *testing.T) { opt.RoundedBarCaps = True() return opt }, - result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, { name: "custom_font", @@ -85,7 +85,7 @@ func TestBarChart(t *testing.T) { opt.Title.FontStyle = customFont return opt }, - result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, { name: "boundary_gap_enable", @@ -95,7 +95,7 @@ func TestBarChart(t *testing.T) { opt.XAxis.BoundaryGap = True() return opt }, - result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, { name: "boundary_gap_disable", @@ -105,7 +105,7 @@ func TestBarChart(t *testing.T) { opt.XAxis.BoundaryGap = False() return opt }, - result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "189168147126105846342210JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } 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, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", data) + assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", 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, "RainfallEvaporation2702402101801501209060300JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6348.07", data) + assertEqualSVG(t, "RainfallEvaporation2702402101801501209060300JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6348.07", 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, "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0143k286k428.99k571.99k714.99k", data) + assertEqualSVG(t, "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0143k286k428.99k571.99k714.99k", 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, "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", data) + assertEqualSVG(t, "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", 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, "Allocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", data) + assertEqualSVG(t, "Allocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", 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, "ShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", data) + assertEqualSVG(t, "ShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", 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, "45041037033029025021017013090MonTueWedThuFriSatSun20112012WorldChinaIndiaUSA70144", data) + assertEqualSVG(t, "45041037033029025021017013090MonTueWedThuFriSatSun20112012WorldChinaIndiaUSA70144", 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, "RainfallEvaporationRainfall vs EvaporationFake Data2702402101801501209060300JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6348.07", data) + assertEqualSVG(t, "RainfallEvaporationRainfall vs EvaporationFake Data2702402101801501209060300JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6348.07", 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, "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", data) + assertEqualSVG(t, "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", 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: "ShowClickVisitInquiryOrderFunnel(100%)(80%)(60%)(40%)(20%)", + result: "ShowClickVisitInquiryOrderFunnel(100%)(80%)(60%)(40%)(20%)", }, { name: "themed", defaultTheme: false, makeOptions: makeBasicFunnelChartOption, - result: "ShowClickVisitInquiryOrderFunnel(100%)(80%)(60%)(40%)(20%)", + result: "ShowClickVisitInquiryOrderFunnel(100%)(80%)(60%)(40%)(20%)", }, } 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: "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0144k288k432k576k720k", + result: "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0144k288k432k576k720k", }, { name: "themed", defaultTheme: false, makeOptions: makeBasicHorizontalBarChartOption, - result: "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0144k288k432k576k720k", + result: "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0144k288k432k576k720k", }, { name: "custom_fonts", @@ -74,7 +74,7 @@ func TestHorizontalBarChart(t *testing.T) { opt.Title.FontStyle = customFont return opt }, - result: "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0144k288k432k576k720k", + result: "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0144k288k432k576k720k", }, { name: "value_labels", @@ -87,7 +87,7 @@ func TestHorizontalBarChart(t *testing.T) { } return opt }, - result: "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0144k288k432k576k720k182032348929034104970131744630230193252343831000121594134141681807", + result: "20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0144k288k432k576k720k182032348929034104970131744630230193252343831000121594134141681807", }, } 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: "OneTwoThree", + result: "OneTwoThree", }, { name: "position_left", @@ -43,7 +43,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwoThree", + result: "OneTwoThree", }, { name: "position_vertical_with_rect", @@ -61,7 +61,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwoThree", + result: "OneTwoThree", }, { name: "custom_padding_and_font", @@ -79,7 +79,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwoThree", + result: "OneTwoThree", }, { name: "hidden", @@ -109,7 +109,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is Longer", + result: "OneTwo WordThree Word ItemFour Words Is Longer", }, { name: "vertical_right_position", @@ -124,7 +124,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is Longer", + result: "OneTwo WordThree Word ItemFour Words Is Longer", }, { name: "vertical_bottom_position", @@ -141,7 +141,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is Longer", + result: "OneTwo WordThree Word ItemFour Words Is Longer", }, { name: "vertical_right_bottom_position", @@ -159,7 +159,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is Longer", + result: "OneTwo WordThree Word ItemFour Words Is Longer", }, { name: "vertical_right_position_custom_font_size", @@ -177,7 +177,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is Longer", + result: "OneTwo WordThree Word ItemFour Words Is Longer", }, { name: "vertical_right_position_with_padding", @@ -193,7 +193,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is Longer", + result: "OneTwo WordThree Word ItemFour Words Is Longer", }, { name: "left_position_overflow", @@ -208,7 +208,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", + result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", }, { name: "center_position_overflow", @@ -223,7 +223,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", + result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", }, { name: "center_position_center_align_overflow", @@ -239,7 +239,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", + result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", }, { name: "50%_position_overflow", @@ -256,7 +256,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", + result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", }, { name: "vertical_right_position_overflow", @@ -274,7 +274,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", + result: "OneTwo WordThree Word ItemFour Words Is LongerFive Words Is Even LongerSix Words Is The Longest Tested", }, { name: "right_alignment", @@ -288,7 +288,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is Longer", + result: "OneTwo WordThree Word ItemFour Words Is Longer", }, { name: "vertical_right_alignment", @@ -303,7 +303,7 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is Longer", + result: "OneTwo WordThree Word ItemFour Words Is Longer", }, { name: "vertical_right_alignment_left_position", @@ -319,9 +319,10 @@ func TestNewLegend(t *testing.T) { } return p.Bytes() }, - result: "OneTwo WordThree Word ItemFour Words Is Longer", + result: "OneTwo WordThree Word ItemFour Words Is Longer", }, } + 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: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", + result: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", }, { name: "basic_themed", defaultTheme: false, makeOptions: makeFullLineChartOption, - result: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", + result: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", }, { name: "boundary_gap_disable", @@ -120,7 +120,7 @@ func TestLineChart(t *testing.T) { opt.XAxis.BoundaryGap = False() return opt }, - result: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", + result: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", }, { name: "boundary_gap_enable", @@ -130,7 +130,7 @@ func TestLineChart(t *testing.T) { opt.XAxis.BoundaryGap = True() return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, { name: "08Y_skip1", @@ -145,7 +145,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.4k1k6002000", + result: "1.4k1k6002000", }, { name: "09Y_skip1", @@ -160,7 +160,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.44k1.08k7203600", + result: "1.44k1.08k7203600", }, { name: "08Y_skip2", @@ -175,7 +175,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.4k8002000", + result: "1.4k8002000", }, { name: "09Y_skip2", @@ -190,7 +190,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.44k9003600", + result: "1.44k9003600", }, { name: "10Y_skip2", @@ -205,7 +205,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.44k9604800", + result: "1.44k9604800", }, { name: "08Y_skip3", @@ -220,7 +220,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.4k6000", + result: "1.4k6000", }, { name: "09Y_skip3", @@ -235,7 +235,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.44k7200", + result: "1.44k7200", }, { name: "10Y_skip3", @@ -250,7 +250,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.44k8001600", + result: "1.44k8001600", }, { name: "11Y_skip3", @@ -265,7 +265,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.4k8402800", + result: "1.4k8402800", }, { name: "no_yaxis_split_line", @@ -280,7 +280,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.5k1k5000", + result: "1.5k1k5000", }, { name: "yaxis_spine_line_show", @@ -295,7 +295,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "1.5k1k5000", + result: "1.5k1k5000", }, { name: "zero_data", @@ -309,7 +309,7 @@ func TestLineChart(t *testing.T) { opt.SeriesList = NewSeriesListLine(values) return opt }, - result: "210", + result: "210", }, { name: "tiny_range", @@ -323,7 +323,7 @@ func TestLineChart(t *testing.T) { opt.SeriesList = NewSeriesListLine(values) return opt }, - result: "21.61.20.80.40", + result: "21.61.20.80.40", }, { name: "hidden_legend_and_x-axis", @@ -334,7 +334,7 @@ func TestLineChart(t *testing.T) { opt.XAxis.Show = False() return opt }, - result: "Line1.44k1.28k1.12k9608006404803201600", + result: "Line1.44k1.28k1.12k9608006404803201600", }, { name: "custom_font", @@ -350,7 +350,7 @@ func TestLineChart(t *testing.T) { opt.Title.FontStyle = customFont return opt }, - result: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", + result: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", }, { name: "title_offset_center_legend_right", @@ -361,7 +361,7 @@ func TestLineChart(t *testing.T) { opt.Legend.Offset = OffsetRight return opt }, - result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", + result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", }, { name: "title_offset_right", @@ -371,7 +371,7 @@ func TestLineChart(t *testing.T) { opt.Title.Offset = OffsetRight return opt }, - result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", + result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", }, { name: "title_offset_bottom_center", @@ -384,7 +384,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", + result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", }, { name: "legend_offset_bottom", @@ -396,7 +396,7 @@ func TestLineChart(t *testing.T) { } return opt }, - result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", + result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", }, { name: "title_and_legend_offset_bottom", @@ -411,7 +411,7 @@ func TestLineChart(t *testing.T) { opt.Legend.Offset = bottomOffset return opt }, - result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", + result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", }, { name: "vertical_legend_offset_right", @@ -422,7 +422,7 @@ func TestLineChart(t *testing.T) { opt.Legend.Offset = OffsetRight return opt }, - result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", + result: "12Line1.44k1.28k1.12k9608006404803201600ABCDEFG", }, { name: "legend_overlap_chart", @@ -434,7 +434,7 @@ func TestLineChart(t *testing.T) { opt.Legend.OverlayChart = True() return opt }, - result: "121.44k1.28k1.12k9608006404803201600ABCDEFG", + result: "121.44k1.28k1.12k9608006404803201600ABCDEFG", }, { name: "curved_line", @@ -444,7 +444,7 @@ func TestLineChart(t *testing.T) { opt.StrokeSmoothingTension = 0.8 return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, { name: "line_gap", @@ -454,7 +454,7 @@ func TestLineChart(t *testing.T) { opt.SeriesList[0].Data[3] = GetNullValue() return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, { name: "line_gap_dot", @@ -465,7 +465,7 @@ func TestLineChart(t *testing.T) { opt.SeriesList[0].Data[5] = GetNullValue() return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, { name: "line_gap_fill_area", @@ -476,7 +476,7 @@ func TestLineChart(t *testing.T) { opt.FillArea = true return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, { name: "curved_line_gap", @@ -487,7 +487,7 @@ func TestLineChart(t *testing.T) { opt.SeriesList[0].Data[3] = GetNullValue() return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, { name: "curved_line_gap_fill_area", @@ -499,7 +499,7 @@ func TestLineChart(t *testing.T) { opt.FillArea = true return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, { name: "fill_area", @@ -510,7 +510,7 @@ func TestLineChart(t *testing.T) { opt.FillOpacity = 100 return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, { name: "fill_area_boundary_gap", @@ -522,7 +522,7 @@ func TestLineChart(t *testing.T) { opt.XAxis.BoundaryGap = True() return opt }, - result: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", + result: "EmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.28k1.12k9608006404803201600MonTueWedThuFriSatSun", }, { name: "fill_area_curved_boundary_gap", @@ -534,7 +534,7 @@ func TestLineChart(t *testing.T) { opt.XAxis.BoundaryGap = True() return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, { name: "fill_area_curved_no_gap", @@ -546,7 +546,7 @@ func TestLineChart(t *testing.T) { opt.XAxis.BoundaryGap = False() return opt }, - result: "1.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k9608006404803201600", }, } 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: "321", + result: "321", }, } + 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: "3", + result: "3", }, } 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: "hello world!", + result: "hello world!", }, { - 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: "1.44k1.28k1.12k96080064048032016001.44k1.28k1.12k9608006404803201600", + result: "1.44k1.28k1.12k96080064048032016001.44k1.28k1.12k9608006404803201600", }, } + 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, "HelloWorld!Hello World!", buf) + assertEqualSVG(t, "HelloWorld!Hello World!", 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, "Rainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%189168147126105846342210JanMarMayJulAugOctDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.312Line1.44k1.28k1.12k9608006404803201600ABCDEFG", buf) + assertEqualSVG(t, "Rainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%189168147126105846342210JanMarMayJulAugOctDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.312Line1.44k1.28k1.12k9608006404803201600ABCDEFG", 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: "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", + result: "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", }, { name: "themed", defaultTheme: false, makeOptions: makeBasicPieChartOption, - result: "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", + result: "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", }, { name: "lots_labels-sortedDescending", @@ -120,7 +120,7 @@ func TestPieChart(t *testing.T) { }, } }, - result: "Germany (84358845 ≅ 18.8%)France (68070697 ≅ 15.17%)Italy (58850717 ≅ 13.12%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", + result: "Germany (84358845 ≅ 18.8%)France (68070697 ≅ 15.17%)Italy (58850717 ≅ 13.12%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", }, { name: "lots_labels-unsorted", @@ -183,7 +183,7 @@ func TestPieChart(t *testing.T) { }, } }, - result: "France (68070697 ≅ 15.17%)Finland (5563970 ≅ 1.24%)Estonia (1373101 ≅ 0.3%)Denmark (5932654 ≅ 1.32%)Czech Republic (10827529 ≅ 2.41%)Cyprus (920701 ≅ 0.2%)Croatia (3850894 ≅ 0.85%)Bulgaria (6447710 ≅ 1.43%)Belgium (11754004 ≅ 2.62%)Austria (9104772 ≅ 2.02%)Germany (84358845 ≅ 18.8%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Poland (36753736 ≅ 8.19%)Netherlands (17947406 ≅ 4%)Malta (542051 ≅ 0.12%)Luxembourg (660809 ≅ 0.14%)Lithuania (2857279 ≅ 0.63%)Latvia (1883008 ≅ 0.41%)Italy (58850717 ≅ 13.12%)Ireland (5194336 ≅ 1.15%)Portugal (10467366 ≅ 2.33%)Romania (19051562 ≅ 4.24%)Slovakia (5428792 ≅ 1.21%)Slovenia (2116792 ≅ 0.47%)Spain (48059777 ≅ 10.71%)Sweden (10521556 ≅ 2.34%)", + result: "France (68070697 ≅ 15.17%)Finland (5563970 ≅ 1.24%)Estonia (1373101 ≅ 0.3%)Denmark (5932654 ≅ 1.32%)Czech Republic (10827529 ≅ 2.41%)Cyprus (920701 ≅ 0.2%)Croatia (3850894 ≅ 0.85%)Bulgaria (6447710 ≅ 1.43%)Belgium (11754004 ≅ 2.62%)Austria (9104772 ≅ 2.02%)Germany (84358845 ≅ 18.8%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Poland (36753736 ≅ 8.19%)Netherlands (17947406 ≅ 4%)Malta (542051 ≅ 0.12%)Luxembourg (660809 ≅ 0.14%)Lithuania (2857279 ≅ 0.63%)Latvia (1883008 ≅ 0.41%)Italy (58850717 ≅ 13.12%)Ireland (5194336 ≅ 1.15%)Portugal (10467366 ≅ 2.33%)Romania (19051562 ≅ 4.24%)Slovakia (5428792 ≅ 1.21%)Slovenia (2116792 ≅ 0.47%)Spain (48059777 ≅ 10.71%)Sweden (10521556 ≅ 2.34%)", }, { name: "100labels-sorted", @@ -214,7 +214,7 @@ func TestPieChart(t *testing.T) { }, } }, - result: "Label 25: 1%Label 24: 1%Label 23: 1%Label 22: 1%Label 21: 1%Label 20: 1%Label 19: 1%Label 18: 1%Label 17: 1%Label 16: 1%Label 15: 1%Label 14: 1%Label 13: 1%Label 12: 1%Label 11: 1%Label 10: 1%Label 9: 1%Label 8: 1%Label 7: 1%Label 6: 1%Label 5: 1%Label 4: 1%Label 3: 1%Label 2: 1%Label 1: 1%Label 26: 1%Label 27: 1%Label 28: 1%Label 29: 1%Label 30: 1%Label 31: 1%Label 32: 1%Label 33: 1%Label 34: 1%Label 35: 1%Label 36: 1%Label 37: 1%Label 38: 1%Label 39: 1%Label 40: 1%Label 41: 1%Label 42: 1%Label 43: 1%Label 44: 1%Label 45: 1%Label 46: 1%Label 47: 1%Label 48: 1%Label 49: 1%Label 50: 1%Label 75: 1%Label 74: 1%Label 73: 1%Label 72: 1%Label 71: 1%Label 70: 1%Label 69: 1%Label 68: 1%Label 67: 1%Label 66: 1%Label 65: 1%Label 64: 1%Label 63: 1%Label 62: 1%Label 61: 1%Label 60: 1%Label 59: 1%Label 58: 1%Label 57: 1%Label 56: 1%Label 55: 1%Label 54: 1%Label 53: 1%Label 52: 1%Label 51: 1%Label 76: 1%Label 77: 1%Label 78: 1%Label 79: 1%Label 80: 1%Label 81: 1%Label 82: 1%Label 83: 1%Label 84: 1%Label 85: 1%Label 86: 1%Label 87: 1%Label 88: 1%Label 89: 1%Label 90: 1%Label 91: 1%Label 92: 1%Label 93: 1%Label 94: 1%Label 95: 1%Label 96: 1%Label 97: 1%Label 98: 1%Label 99: 1%Label 100: 1%", + result: "Label 25: 1%Label 24: 1%Label 23: 1%Label 22: 1%Label 21: 1%Label 20: 1%Label 19: 1%Label 18: 1%Label 17: 1%Label 16: 1%Label 15: 1%Label 14: 1%Label 13: 1%Label 12: 1%Label 11: 1%Label 10: 1%Label 9: 1%Label 8: 1%Label 7: 1%Label 6: 1%Label 5: 1%Label 4: 1%Label 3: 1%Label 2: 1%Label 1: 1%Label 26: 1%Label 27: 1%Label 28: 1%Label 29: 1%Label 30: 1%Label 31: 1%Label 32: 1%Label 33: 1%Label 34: 1%Label 35: 1%Label 36: 1%Label 37: 1%Label 38: 1%Label 39: 1%Label 40: 1%Label 41: 1%Label 42: 1%Label 43: 1%Label 44: 1%Label 45: 1%Label 46: 1%Label 47: 1%Label 48: 1%Label 49: 1%Label 50: 1%Label 75: 1%Label 74: 1%Label 73: 1%Label 72: 1%Label 71: 1%Label 70: 1%Label 69: 1%Label 68: 1%Label 67: 1%Label 66: 1%Label 65: 1%Label 64: 1%Label 63: 1%Label 62: 1%Label 61: 1%Label 60: 1%Label 59: 1%Label 58: 1%Label 57: 1%Label 56: 1%Label 55: 1%Label 54: 1%Label 53: 1%Label 52: 1%Label 51: 1%Label 76: 1%Label 77: 1%Label 78: 1%Label 79: 1%Label 80: 1%Label 81: 1%Label 82: 1%Label 83: 1%Label 84: 1%Label 85: 1%Label 86: 1%Label 87: 1%Label 88: 1%Label 89: 1%Label 90: 1%Label 91: 1%Label 92: 1%Label 93: 1%Label 94: 1%Label 95: 1%Label 96: 1%Label 97: 1%Label 98: 1%Label 99: 1%Label 100: 1%", }, { name: "fix_label_pos", @@ -259,7 +259,7 @@ func TestPieChart(t *testing.T) { }, } }, - result: "Fix label K (72586)C (149086 ≅ 5.04%)B (185596 ≅ 6.28%)A (397594 ≅ 13.45%)D (144258 ≅ 4.88%)E (120194 ≅ 4.06%)F (117514 ≅ 3.97%)G (99412 ≅ 3.36%)H (91135 ≅ 3.08%)I (87282 ≅ 2.95%)J (76790 ≅ 2.59%)Z (29608 ≅ 1%)Y (32566 ≅ 1.1%)X (32788 ≅ 1.1%)W (33784 ≅ 1.14%)V (36644 ≅ 1.24%)U (37414 ≅ 1.26%)T (39476 ≅ 1.33%)S (41242 ≅ 1.39%)R (51460 ≅ 1.74%)Q (53746 ≅ 1.81%)P (54792 ≅ 1.85%)O (55486 ≅ 1.87%)N (56306 ≅ 1.9%)M (58270 ≅ 1.97%)L (58818 ≅ 1.99%)K (72586 ≅ 2.45%)AA (29558 ≅ 1%)AB (29384 ≅ 0.99%)AC (28166 ≅ 0.95%)AD (26998 ≅ 0.91%)AE (26948 ≅ 0.91%)AF (26054 ≅ 0.88%)AG (25804 ≅ 0.87%)AH (25730 ≅ 0.87%)AI (24438 ≅ 0.82%)AJ (23782 ≅ 0.8%)AK (22896 ≅ 0.77%)AL (21404 ≅ 0.72%)AM (428978 ≅ 14.52%)", + result: "Fix label K (72586)C (149086 ≅ 5.04%)B (185596 ≅ 6.28%)A (397594 ≅ 13.45%)D (144258 ≅ 4.88%)E (120194 ≅ 4.06%)F (117514 ≅ 3.97%)G (99412 ≅ 3.36%)H (91135 ≅ 3.08%)I (87282 ≅ 2.95%)J (76790 ≅ 2.59%)Z (29608 ≅ 1%)Y (32566 ≅ 1.1%)X (32788 ≅ 1.1%)W (33784 ≅ 1.14%)V (36644 ≅ 1.24%)U (37414 ≅ 1.26%)T (39476 ≅ 1.33%)S (41242 ≅ 1.39%)R (51460 ≅ 1.74%)Q (53746 ≅ 1.81%)P (54792 ≅ 1.85%)O (55486 ≅ 1.87%)N (56306 ≅ 1.9%)M (58270 ≅ 1.97%)L (58818 ≅ 1.99%)K (72586 ≅ 2.45%)AA (29558 ≅ 1%)AB (29384 ≅ 0.99%)AC (28166 ≅ 0.95%)AD (26998 ≅ 0.91%)AE (26948 ≅ 0.91%)AF (26054 ≅ 0.88%)AG (25804 ≅ 0.87%)AH (25730 ≅ 0.87%)AI (24438 ≅ 0.82%)AJ (23782 ≅ 0.8%)AK (22896 ≅ 0.77%)AL (21404 ≅ 0.72%)AM (428978 ≅ 14.52%)", }, { name: "custom_fonts", @@ -274,7 +274,7 @@ func TestPieChart(t *testing.T) { opt.Title.FontStyle = customFont return opt }, - result: "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", + result: "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", }, { name: "legend_bottom_right", @@ -287,7 +287,7 @@ func TestPieChart(t *testing.T) { } return opt }, - result: "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", + result: "Search EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", }, } 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: "Allocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", + result: "Allocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", }, { name: "themed", defaultTheme: false, makeOptions: makeBasicRadarChartOption, - result: "Allocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", + result: "Allocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", }, } 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: "NameAgeAddressTagActionJohnBrown32New York No. 1 Lake Parknice,developerSend MailJim Green42London No. 1 Lake ParkwowSend MailJoe Black32Sidney No. 1 Lake Parkcool,teacherSend Mail", + result: "NameAgeAddressTagActionJohnBrown32New York No. 1 Lake Parknice,developerSend MailJim Green42London No. 1 Lake ParkwowSend MailJoe Black32Sidney No. 1 Lake Parkcool,teacherSend Mail", }, { name: "dark_theme", theme: GetTheme(ThemeVividDark), makeOptions: makeDefaultTableChartOptions, - result: "NameAgeAddressTagActionJohn Brown32New York No.1 Lake Parknice,developerSend MailJim Green42London No. 1Lake ParkwowSend MailJoe Black32Sidney No. 1Lake Parkcool, teacherSend Mail", + result: "NameAgeAddressTagActionJohn Brown32New York No.1 Lake Parknice,developerSend MailJim Green42London No. 1Lake ParkwowSend MailJoe Black32Sidney No. 1Lake Parkcool, teacherSend Mail", }, { name: "cell_modified", @@ -98,7 +98,7 @@ func TestTableChart(t *testing.T) { } return opt }, - result: "NameAgeAddressTagActionJohn Brown32New York No.1 Lake Parknice,developerSend MailJim Green42London No. 1Lake ParkwowSend MailJoe Black32Sidney No. 1Lake Parkcool, teacherSend Mail", + result: "NameAgeAddressTagActionJohn Brown32New York No.1 Lake Parknice,developerSend MailJim Green42London No. 1Lake ParkwowSend MailJoe Black32Sidney No. 1Lake Parkcool, teacherSend Mail", }, { 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, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) + assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) } func TestThemeDark(t *testing.T) { t.Parallel() svg := renderTestLineChartWithThemeName(t, true, ThemeDark) - assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) + assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) } func TestThemeVividLight(t *testing.T) { t.Parallel() svg := renderTestLineChartWithThemeName(t, true, ThemeVividLight) - assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) + assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) } func TestThemeVividDark(t *testing.T) { t.Parallel() svg := renderTestLineChartWithThemeName(t, true, ThemeVividDark) - assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) + assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) } func TestThemeAnt(t *testing.T) { t.Parallel() svg := renderTestLineChartWithThemeName(t, true, ThemeAnt) - assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) + assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) } func TestThemeGrafana(t *testing.T) { t.Parallel() svg := renderTestLineChartWithThemeName(t, true, ThemeGrafana) - assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", svg) + assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch EngineLine1.4k7000MonTueWedThuFriSatSun", 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, "EmailUnion AdsVideo AdsDirectSearch Engine1.4k7000MonTueWedThuFriSatSun", svg) + assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch Engine1.4k7000MonTueWedThuFriSatSun", 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, "EmailUnion AdsVideo AdsDirectSearch Engine1.4k7000MonTueWedThuFriSatSun", svg) + assertEqualSVG(t, "EmailUnion AdsVideo AdsDirectSearch Engine1.4k7000MonTueWedThuFriSatSun", 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: "titlesubTitle", + result: "titlesubTitle", }, { name: "offset_percent", @@ -65,7 +65,7 @@ func TestTitleRenderer(t *testing.T) { } return p.Bytes() }, - result: "titlesubTitle", + result: "titlesubTitle", }, { name: "offset_right", @@ -80,7 +80,7 @@ func TestTitleRenderer(t *testing.T) { } return p.Bytes() }, - result: "titlesubTitle", + result: "titlesubTitle", }, { name: "offset_center", @@ -95,7 +95,7 @@ func TestTitleRenderer(t *testing.T) { } return p.Bytes() }, - result: "titlesubTitle", + result: "titlesubTitle", }, { name: "offset_bottom", @@ -112,7 +112,7 @@ func TestTitleRenderer(t *testing.T) { } return p.Bytes() }, - result: "titlesubTitle", + result: "titlesubTitle", }, { name: "offset_bottom_right", @@ -130,7 +130,7 @@ func TestTitleRenderer(t *testing.T) { } return p.Bytes() }, - result: "titlesubTitle", + result: "titlesubTitle", }, { name: "offset_bottom_center", @@ -148,7 +148,7 @@ func TestTitleRenderer(t *testing.T) { } return p.Bytes() }, - result: "titlesubTitle", + result: "titlesubTitle", }, { name: "custom_font", @@ -170,7 +170,7 @@ func TestTitleRenderer(t *testing.T) { } return p.Bytes() }, - result: "titlesubTitle", + result: "titlesubTitle", }, } 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: "abcd", + result: "abcd", }, } for i, tt := range tests {