feat: support legend render
This commit is contained in:
parent
7ee13fe914
commit
4cf494088e
3 changed files with 216 additions and 5 deletions
|
|
@ -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)
|
||||
|
|
|
|||
154
legend.go
Normal file
154
legend.go
Normal file
|
|
@ -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
|
||||
}
|
||||
11
painter.go
11
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue