From 4cf494088e029f30563ff40ab7dbd799b9c00f39 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 7 Jun 2022 23:04:39 +0800 Subject: [PATCH] feat: support legend render --- examples/painter/main.go | 56 +++++++++++++- legend.go | 154 +++++++++++++++++++++++++++++++++++++++ painter.go | 11 ++- 3 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 legend.go diff --git a/examples/painter/main.go b/examples/painter/main.go index 4614f10..094b98e 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -26,7 +26,7 @@ func writeFile(buf []byte) error { func main() { p, err := charts.NewPainter(charts.PainterOptions{ - Width: 400, + Width: 600, Height: 1200, Type: charts.ChartOutputPNG, }) @@ -422,6 +422,60 @@ func main() { StrokeColor: drawing.ColorBlue, }).Render() + top += 100 + charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 30, + })), charts.LegendPainterOption{ + Data: []string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + }, + FontSize: 12, + FontColor: drawing.ColorBlack, + }).Render() + + top += 30 + charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 30, + })), charts.LegendPainterOption{ + Data: []string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + }, + Align: charts.AlignRight, + FontSize: 16, + Icon: charts.IconRect, + FontColor: drawing.ColorBlack, + }).Render() + + top += 30 + charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 100, + })), charts.LegendPainterOption{ + Data: []string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + }, + Orient: charts.OrientVertical, + FontSize: 12, + FontColor: drawing.ColorBlack, + }).Render() + buf, err := p.Bytes() if err != nil { panic(err) diff --git a/legend.go b/legend.go new file mode 100644 index 0000000..d128272 --- /dev/null +++ b/legend.go @@ -0,0 +1,154 @@ +// 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 + +type LegendPainter struct { + p *Painter + opt *LegendPainterOption +} + +const IconRect = "rect" +const IconLineDot = "lineDot" + +type LegendPainterOption struct { + Theme ColorPalette + // 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 + // Icon of the legend. + Icon string + // Font size of legend text + FontSize float64 + // FontColor color of legend text + FontColor Color +} + +func NewLegendPainter(p *Painter, opt LegendPainterOption) *LegendPainter { + return &LegendPainter{ + p: p, + opt: &opt, + } +} + +func (l *LegendPainter) Render() (Box, error) { + opt := l.opt + theme := opt.Theme + if theme == nil { + theme = l.p.theme + } + p := l.p + p.SetTextStyle(Style{ + FontSize: opt.FontSize, + FontColor: opt.FontColor, + }) + measureList := make([]Box, len(opt.Data)) + maxTextWidth := 0 + for index, text := range opt.Data { + b := p.MeasureText(text) + if b.Width() > maxTextWidth { + maxTextWidth = b.Width() + } + measureList[index] = b + } + x := 0 + y := 0 + offset := 20 + textOffset := 2 + legendWidth := 30 + legendHeight := 20 + drawIcon := func(top, left int) int { + if opt.Icon == IconRect { + p.Rect(Box{ + Top: top - legendHeight + 4, + Left: left, + Right: left + legendWidth, + Bottom: top - 2, + }) + } else { + p.LegendLineDot(Box{ + Top: top, + Left: left, + Right: left + legendWidth, + Bottom: top + legendHeight, + }) + } + return left + legendWidth + } + for index, text := range opt.Data { + color := theme.GetSeriesColor(index) + p.SetDrawingStyle(Style{ + FillColor: color, + StrokeColor: color, + }) + if opt.Align != AlignRight { + x = drawIcon(y, x) + x += textOffset + } + p.Text(text, x, y) + x += measureList[index].Width() + if opt.Align == AlignRight { + x += textOffset + x = drawIcon(0, x) + } + if opt.Orient == OrientVertical { + y += offset + x = 0 + } else { + x += offset + y = 0 + } + } + width := 0 + height := 0 + for _, item := range measureList { + if opt.Orient == OrientVertical { + height += item.Height() + } else { + width += item.Width() + } + } + if opt.Orient == OrientVertical { + width = maxTextWidth + textOffset + legendWidth + height = offset * len(opt.Data) + } else { + height = legendHeight + offsetValue := (len(opt.Data) - 1) * (offset + textOffset) + allLegendWidth := len(opt.Data) * legendWidth + width += (offsetValue + allLegendWidth) + } + + return Box{ + Right: width, + Bottom: height, + }, nil +} diff --git a/painter.go b/painter.go index 851fe11..61a8e95 100644 --- a/painter.go +++ b/painter.go @@ -170,6 +170,9 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { font: font, } p.setOptions(opt...) + if p.theme == nil { + p.theme = NewTheme(ThemeLight) + } return p, nil } func (p *Painter) setOptions(opts ...PainterOption) { @@ -705,11 +708,11 @@ func (p *Painter) LegendLineDot(box Box) *Painter { dotHeight := 5 p.render.SetStrokeWidth(float64(strokeWidth)) - center := (height - strokeWidth) >> 1 - p.MoveTo(box.Left, box.Top+center) - p.LineTo(box.Right, box.Top+center) + center := (height-strokeWidth)>>1 - 1 + p.MoveTo(box.Left, box.Top-center) + p.LineTo(box.Right, box.Top-center) p.Stroke() - p.Circle(float64(dotHeight), box.Left+width>>1, center) + p.Circle(float64(dotHeight), box.Left+width>>1, box.Top-center) p.FillStroke() return p }