diff --git a/chart.go b/chart.go
index 88fc43d..a5cf8ab 100644
--- a/chart.go
+++ b/chart.go
@@ -108,10 +108,13 @@ func (o *ChartOption) FillDefault(theme string) {
o.Title.SubtextStyle.Font = o.Font
}
- o.Legend.Theme = t
+ o.Legend.Theme = theme
if o.Legend.Style.FontSize == 0 {
o.Legend.Style.FontSize = 10
}
+ if o.Legend.Left == "" {
+ o.Legend.Left = PositionCenter
+ }
if o.Legend.Style.Font == nil {
o.Legend.Style.Font = o.Font
}
diff --git a/draw.go b/draw.go
index ce0bd6b..bdd5a2b 100644
--- a/draw.go
+++ b/draw.go
@@ -39,6 +39,11 @@ const (
PositionBottom = "bottom"
)
+const (
+ OrientHorizontal = "horizontal"
+ OrientVertical = "vertical"
+)
+
type Draw struct {
Render chart.Renderer
Box chart.Box
diff --git a/legend.go b/legend.go
index abffeb3..e1202cb 100644
--- a/legend.go
+++ b/legend.go
@@ -23,16 +23,31 @@
package charts
import (
+ "strconv"
+ "strings"
+
"github.com/wcharczuk/go-chart/v2"
)
type LegendOption struct {
- Theme *Theme
+ Theme string
+ // Legend show flag, if nil or true, the legend will be shown
+ Show *bool
+ // Legend text style
Style chart.Style
- Data []string
- Left string
- Right string
+ // Text array of legend
+ Data []string
+ // Distance between legend component and the left side of the container.
+ // It can be pixel value: 20, percentage value: 20%,
+ // or position value: right, center.
+ Left string
+ // Distance between legend component and the top side of the container.
+ // It can be pixel value: 20.
+ Top string
+ // Legend marker and text aligning, it can be left or right, default is left.
Align string
+ // The layout orientation of legend, it can be horizontal or vertical, default is horizontal.
+ Orient string
}
type legend struct {
d *Draw
@@ -49,10 +64,10 @@ func NewLegend(d *Draw, opt LegendOption) *legend {
func (l *legend) Render() (chart.Box, error) {
d := l.d
opt := l.opt
- if len(opt.Data) == 0 {
+ if len(opt.Data) == 0 || isFalse(opt.Show) {
return chart.BoxZero, nil
}
- theme := opt.Theme
+ theme := NewTheme(opt.Theme)
padding := opt.Style.Padding
legendDraw, err := NewDraw(DrawOption{
Parent: d,
@@ -65,49 +80,91 @@ func (l *legend) Render() (chart.Box, error) {
x := 0
y := 0
+ top := 0
+ // TODO TOP 暂只支持数值
+ if opt.Top != "" {
+ top, _ = strconv.Atoi(opt.Top)
+ y += top
+ }
legendWidth := 30
legendDotHeight := 5
textPadding := 5
legendMargin := 10
widthCount := 0
+ maxTextWidth := 0
// 文本宽度
for _, text := range opt.Data {
b := r.MeasureText(text)
+ if b.Width() > maxTextWidth {
+ maxTextWidth = b.Width()
+ }
widthCount += b.Width()
}
- // 加上标记
- widthCount += legendWidth * len(opt.Data)
- // 文本的padding
- widthCount += 2 * textPadding * len(opt.Data)
- // margin的宽度
- widthCount += legendMargin * (len(opt.Data) - 1)
+ if opt.Orient == OrientVertical {
+ widthCount = maxTextWidth + legendWidth + textPadding
+ } else {
+ // 加上标记
+ widthCount += legendWidth * len(opt.Data)
+ // 文本的padding
+ widthCount += 2 * textPadding * len(opt.Data)
+ // margin的宽度
+ widthCount += legendMargin * (len(opt.Data) - 1)
+ }
- // TODO 支持更多的定位方式
- // 居中
- x = (legendDraw.Box.Width() - widthCount) >> 1
- for index, text := range opt.Data {
- if index != 0 {
- x += legendMargin
+ left := 0
+ switch opt.Left {
+ case PositionRight:
+ left = legendDraw.Box.Width() - widthCount
+ case PositionCenter:
+ left = (legendDraw.Box.Width() - widthCount) >> 1
+ default:
+ if strings.HasSuffix(opt.Left, "%") {
+ value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
+ left = legendDraw.Box.Width() * value / 100
+ } else {
+ value, _ := strconv.Atoi(opt.Left)
+ left = value
}
+ }
+ x = left
+ for index, text := range opt.Data {
style := chart.Style{
StrokeColor: theme.GetSeriesColor(index),
FillColor: theme.GetSeriesColor(index),
StrokeWidth: 3,
}
- textBox := r.MeasureText(text)
- renderText := func() {
- x += textPadding
- legendDraw.text(text, x, y+legendDotHeight-2)
- x += textBox.Width()
- x += textPadding
- }
+ style.GetFillAndStrokeOptions().WriteDrawingOptionsToRenderer(r)
+ textBox := r.MeasureText(text)
+ var renderText func()
+ if opt.Orient == OrientVertical {
+ // 垂直
+ // 重置x的位置
+ x = left
+ renderText = func() {
+ x += textPadding
+ legendDraw.text(text, x, y+legendDotHeight-2)
+ x += textBox.Width()
+ y += (2*legendDotHeight + legendMargin)
+ }
+
+ } else {
+ // 水平
+ if index != 0 {
+ x += legendMargin
+ }
+ renderText = func() {
+ x += textPadding
+ legendDraw.text(text, x, y+legendDotHeight-2)
+ x += textBox.Width()
+ x += textPadding
+ }
+ }
if opt.Align == PositionRight {
renderText()
}
- style.GetFillAndStrokeOptions().WriteDrawingOptionsToRenderer(r)
legendDraw.moveTo(x, y)
legendDraw.lineTo(x+legendWidth, y)
r.Stroke()
@@ -120,8 +177,13 @@ func (l *legend) Render() (chart.Box, error) {
}
}
legendBox := padding.Clone()
- legendBox.Right = legendBox.Left + x
- legendBox.Bottom = legendBox.Top + 2*legendDotHeight
-
+ // 计算展示区域
+ if opt.Orient == OrientVertical {
+ legendBox.Right = legendBox.Left + left + maxTextWidth + legendWidth + textPadding
+ legendBox.Bottom = legendBox.Top + y
+ } else {
+ legendBox.Right = legendBox.Left + x
+ legendBox.Bottom = legendBox.Top + 2*legendDotHeight + top + textPadding
+ }
return legendBox, nil
}
diff --git a/legend_test.go b/legend_test.go
new file mode 100644
index 0000000..aaadec7
--- /dev/null
+++ b/legend_test.go
@@ -0,0 +1,148 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestLegendRender(t *testing.T) {
+ assert := assert.New(t)
+
+ newDraw := func() *Draw {
+ d, _ := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ return d
+ }
+ style := chart.Style{
+ FontSize: 10,
+ FontColor: drawing.ColorBlack,
+ }
+ style.Font, _ = chart.GetDefaultFont()
+
+ tests := []struct {
+ newDraw func() *Draw
+ newLegend func(*Draw) *legend
+ box chart.Box
+ result string
+ }{
+ {
+ newDraw: newDraw,
+ newLegend: func(d *Draw) *legend {
+ return NewLegend(d, LegendOption{
+ Top: "10",
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ },
+ Style: style,
+ })
+ },
+ result: "",
+ box: chart.Box{
+ Right: 214,
+ Bottom: 25,
+ },
+ },
+ {
+ newDraw: newDraw,
+ newLegend: func(d *Draw) *legend {
+ return NewLegend(d, LegendOption{
+ Top: "10",
+ Left: PositionRight,
+ Align: PositionRight,
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ },
+ Style: style,
+ })
+ },
+ result: "",
+ box: chart.Box{
+ Right: 400,
+ Bottom: 25,
+ },
+ },
+ {
+ newDraw: newDraw,
+ newLegend: func(d *Draw) *legend {
+ return NewLegend(d, LegendOption{
+ Top: "10",
+ Left: PositionCenter,
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ },
+ Style: style,
+ })
+ },
+ result: "",
+ box: chart.Box{
+ Right: 307,
+ Bottom: 25,
+ },
+ },
+ {
+ newDraw: newDraw,
+ newLegend: func(d *Draw) *legend {
+ return NewLegend(d, LegendOption{
+ Top: "10",
+ Left: PositionLeft,
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ },
+ Style: style,
+ Orient: OrientVertical,
+ })
+ },
+ result: "",
+ box: chart.Box{
+ Right: 61,
+ Bottom: 70,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ d := tt.newDraw()
+ b, err := tt.newLegend(d).Render()
+ assert.Nil(err)
+ assert.Equal(tt.box, b)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.NotEmpty(data)
+ assert.Equal(tt.result, string(data))
+ }
+}