From 3219ce521bb58788127b22634f9283420d602684 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 4 Feb 2022 18:34:31 +0800 Subject: [PATCH] feat: support vertical orient legend --- chart.go | 5 +- draw.go | 5 ++ legend.go | 120 +++++++++++++++++++++++++++++---------- legend_test.go | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 30 deletions(-) create mode 100644 legend_test.go 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: "\\nMonTueWed", + 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: "\\nMonTueWed", + 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: "\\nMonTueWed", + 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: "\\nMonTueWed", + 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)) + } +}