Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Axis rendering improvements #3

Merged
merged 8 commits into from
Feb 8, 2024
38 changes: 38 additions & 0 deletions assert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package charts

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func assertEqualSVG(t *testing.T, expected, actual string) {
t.Helper()

if expected != actual {
expectedFile, err := writeTempFile(expected, t.Name()+"-expected", "svg")
require.NoError(t, err)
actualFile, err := writeTempFile(actual, t.Name()+"-actual", "svg")
require.NoError(t, err)

t.Fatalf("SVG content does not match. Expected file: %s, Actual file: %s",
expectedFile, actualFile)
}
}

func writeTempFile(content, prefix, extension string) (string, error) {
tmpFile, err := os.CreateTemp("", strings.ReplaceAll(prefix, string(os.PathSeparator), ".")+"-*."+extension)
if err != nil {
return "", err
}
defer tmpFile.Close()

if _, err := tmpFile.WriteString(content); err != nil {
return "", err
}

return filepath.Abs(tmpFile.Name())
}
124 changes: 68 additions & 56 deletions axis.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package charts

import (
"math"
"strings"

"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
)

type axisPainter struct {
Expand Down Expand Up @@ -33,8 +33,6 @@ type AxisOption struct {
Show *bool
// The position of axis, it can be 'left', 'top', 'right' or 'bottom'
Position string
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
SplitNumber int
// The line color of axis
StrokeColor Color
// The line width
Expand All @@ -59,19 +57,25 @@ type AxisOption struct {
TextRotation float64
// The offset of label
LabelOffset Box
Unit int
// Unit is a suggestion for how large the axis step is, this is a recommendation only. Larger numbers result in fewer labels.
Unit float64
// LabelCount is the number of labels to show on the axis. Specify a smaller number to reduce writing collisions. This value takes priority over Unit.
LabelCount int
// LabelSkipCount specifies a number of lines between labels where there will be no label,
// but a horizontal line will still be drawn.
LabelSkipCount int
}

func (a *axisPainter) Render() (Box, error) {
opt := a.opt
if isFalse(opt.Show) {
return BoxZero, nil
}
top := a.p
theme := opt.Theme
if theme == nil {
theme = top.theme
}
if isFalse(opt.Show) {
return BoxZero, nil
}

strokeWidth := opt.StrokeWidth
if strokeWidth == 0 {
Expand All @@ -98,32 +102,18 @@ func (a *axisPainter) Render() (Box, error) {
strokeColor = theme.GetAxisStrokeColor()
}

data := opt.Data
formatter := opt.Formatter
if len(formatter) != 0 {
for index, text := range data {
data[index] = strings.ReplaceAll(formatter, "{value}", text)
for index, text := range opt.Data {
opt.Data[index] = strings.ReplaceAll(formatter, "{value}", text)
}
}
dataCount := len(data)
tickCount := dataCount
dataCount := len(opt.Data)

boundaryGap := true
if isFalse(opt.BoundaryGap) {
boundaryGap = false
}
centerLabels := !isFalse(opt.BoundaryGap)
isVertical := opt.Position == PositionLeft ||
opt.Position == PositionRight

labelPosition := ""
if !boundaryGap {
tickCount--
labelPosition = PositionLeft
}
if isVertical && boundaryGap {
labelPosition = PositionCenter
}

// if less than zero, it means not processing
tickLength := getDefaultInt(opt.TickLength, 5)
labelMargin := getDefaultInt(opt.LabelMargin, 5)
Expand All @@ -142,26 +132,11 @@ func (a *axisPainter) Render() (Box, error) {
if isTextRotation {
top.SetTextRotation(opt.TextRotation)
}
textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data)
textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(opt.Data)
if isTextRotation {
top.ClearTextRotation()
}

// Add 30px to calculate text display area
textFillWidth := float64(textMaxWidth + 20)
// Calculate more suitable display item based on text width
fitTextCount := ceilFloatToInt(float64(top.Width()) / textFillWidth)

unit := opt.Unit
if unit <= 0 {
unit = ceilFloatToInt(float64(dataCount) / float64(fitTextCount))
unit = chart.MaxInt(unit, opt.SplitNumber)
// even number
if unit%2 == 0 && dataCount%(unit+1) == 0 {
unit++
}
}

width := 0
height := 0
if isVertical {
Expand Down Expand Up @@ -224,16 +199,50 @@ func (a *axisPainter) Render() (Box, error) {
orient = OrientHorizontal
}

labelCount := opt.LabelCount
if labelCount <= 0 {
var maxLabelCount int
// Add 10px and remove one for some minimal extra padding so that letters don't collide
if orient == OrientVertical {
maxLabelCount = (top.Height() / (textMaxHeight + 10)) - 1
} else {
maxLabelCount = (top.Width() / (textMaxWidth + 10)) - 1
}
if opt.Unit > 0 {
multiplier := 1.0
for {
labelCount = int(math.Ceil(float64(dataCount) / (opt.Unit * multiplier)))
if labelCount > maxLabelCount {
multiplier++
} else {
break
}
}
} else {
labelCount = maxLabelCount
}
}
if labelCount > dataCount {
labelCount = dataCount
}
tickSpaces := dataCount
if !centerLabels {
// there is always one more tick than data sample, and if we are centering labels we use that extra tick to
// center the label against, if not centering then we need one less tick spacing
// passing the tickSpaces reduces the need to copy the logic from painter.go:MultiText
tickSpaces--
}

if strokeWidth > 0 {
p.Child(PainterPaddingOption(Box{
Top: ticksPaddingTop,
Left: ticksPaddingLeft,
})).Ticks(TicksOption{
Count: tickCount,
Length: tickLength,
Unit: unit,
Orient: orient,
First: opt.FirstAxis,
LabelCount: labelCount,
TickSpaces: tickSpaces,
Length: tickLength,
Orient: orient,
First: opt.FirstAxis,
})
p.LineStroke([]Point{
{
Expand All @@ -252,15 +261,17 @@ func (a *axisPainter) Render() (Box, error) {
Top: labelPaddingTop,
Right: labelPaddingRight,
})).MultiText(MultiTextOption{
First: opt.FirstAxis,
Align: textAlign,
TextList: data,
Orient: orient,
Unit: unit,
Position: labelPosition,
TextRotation: opt.TextRotation,
Offset: opt.LabelOffset,
First: opt.FirstAxis,
Align: textAlign,
TextList: opt.Data,
Orient: orient,
LabelCount: labelCount,
LabelSkipCount: opt.LabelSkipCount,
CenterLabels: centerLabels,
TextRotation: opt.TextRotation,
Offset: opt.LabelOffset,
})

if opt.SplitLineShow { // show auxiliary lines
style.StrokeColor = opt.SplitLineColor
style.StrokeWidth = 1
Expand All @@ -272,7 +283,7 @@ func (a *axisPainter) Render() (Box, error) {
x0 = 0
x1 = top.Width() - p.Width()
}
yValues := autoDivide(height, tickCount)
yValues := autoDivide(height, tickSpaces)
yValues = yValues[0 : len(yValues)-1]
for _, y := range yValues {
top.LineStroke([]Point{
Expand All @@ -289,7 +300,8 @@ func (a *axisPainter) Render() (Box, error) {
} else {
y0 := p.Height() - defaultXAxisHeight
y1 := top.Height() - defaultXAxisHeight
for index, x := range autoDivide(width, tickCount) {
xValues := autoDivide(width, tickSpaces)
for index, x := range xValues {
if index == 0 {
continue
}
Expand Down
Loading
Loading