Skip to content

Commit

Permalink
Significant Painter API and SVG rendering changes
Browse files Browse the repository at this point in the history
Changes put together because both required significant changes to the SVG validation in tests.  These changes were carefully examined to make sure there were no negative visual issues.

**Painter API changes**:
The Painter API can be confusing, with some functions depending on the state of the painter.
This changes the Painter API (internal and external) so that style attributes (ie FillColor, StrokeColor, StrokeWidth, etc) are not stateful. Instead the relevant style values are accepted as arguments to the draw action.

This creates an easier to use API.  It also addresses a complexity if multiple Child painters are used together with different drawing state.

Test SVG's resulted from two primary aspects of this change:
  * Measuring text not representing the actual text size due to the font style not set correctly in the Painter
  * A fill style attribute being specified when it has no effect (since style was set due to prior operations)

In general the exposed functions were general examined to ensure all exposed functions are both useful, and easy to use.  This resulted in a number of argument changes, which should be detailed in the go docs.

**vector_renderer (SVG) improvements**:

This change minimizes and simplifies the SVG output through the following:
  * If stroke width is zero, or the color is transparent only `stroke:none` will be set, instead of the prior `stroke-width:0;stroke:none` value
  * Colors will be specified by name if possible, otherwise an RGB or RGBA string will still be used
  * If the text or path is empty, nothing will be inserted into the svg.

These three changes reduce the SVG size.  Specifying the color names could potentially allow color shade changes using CSS.

As a bonus, additional colors are now specified in the `drawing` package.

----

SVG changes are always painful, these were careful evaluated to ensure this is an improvement in both the Painter functionality and SVG results.
  • Loading branch information
jentfoo committed Jan 10, 2025
1 parent 6420826 commit 0b28abc
Show file tree
Hide file tree
Showing 37 changed files with 833 additions and 793 deletions.
45 changes: 14 additions & 31 deletions axis.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package charts
import (
"math"
"strings"

"github.com/go-analyze/charts/chartdraw"
)

type axisPainter struct {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand All @@ -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,
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -293,7 +276,7 @@ func (a *axisPainter) Render() (Box, error) {
top.LineStroke([]Point{
{X: x, Y: y0},
{X: x, Y: y1},
})
}, theme.GetAxisSplitLineColor(), 1)
}
}
}
Expand Down
22 changes: 11 additions & 11 deletions axis_test.go

Large diffs are not rendered by default.

13 changes: 4 additions & 9 deletions bar_chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"math"

"github.com/golang/freetype/truetype"

"github.com/go-analyze/charts/chartdraw"
)

type barChart struct {
Expand Down Expand Up @@ -108,28 +106,25 @@ 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,
Left: x,
Right: x + barWidth,
Bottom: barMaxHeight - 1,
IsSet: true,
}, barWidth, true, false)
}, barWidth, true, false,
seriesColor, seriesColor, 0.0)
} else {
seriesPainter.filledRect(Box{
Top: top,
Left: x,
Right: x + barWidth,
Bottom: barMaxHeight - 1,
IsSet: true,
})
}, seriesColor, seriesColor, 0.0)
}
// generate marker point by hand
points[j] = Point{
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions bar_chart_test.go

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions chart_option_test.go

Large diffs are not rendered by default.

146 changes: 130 additions & 16 deletions chartdraw/drawing/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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":
Expand All @@ -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{}
}
Expand Down Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions chartdraw/raster_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions chartdraw/style.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 0b28abc

Please sign in to comment.