-
Notifications
You must be signed in to change notification settings - Fork 1
/
legend.go
230 lines (214 loc) · 5.2 KB
/
legend.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
package charts
import (
"fmt"
"github.com/go-analyze/charts/chartdraw"
)
type legendPainter struct {
p *Painter
opt *LegendOption
}
const IconRect = "rect"
const IconDot = "dot"
type LegendOption struct {
// Show specifies if the legend should be rendered, set this to *false (through False()) to hide the legend.
Show *bool
// Theme specifies the colors used for the legend.
Theme ColorPalette
// Padding specifies space padding around the legend.
Padding Box
// Data provides text for the legend.
Data []string
// FontStyle specifies the font, size, and style for rendering the legend.
FontStyle FontStyle
// Left is the distance between legend component and the left side of the container.
// It can be pixel value (20), percentage value (20%), or position description: 'left', 'right', 'center'.
Left string
// Top is the distance between legend component and the top side of the container.
// It can be pixel value (20), or percentage value (20%).
Top string
// Align is the legend marker and text alignment, it can be 'left' or 'right', default is 'left'.
Align string
// Orient is the layout orientation of legend, it can be 'horizontal' or 'vertical', default is 'horizontal'.
Orient string
// Icon to show next to the labels. Can be 'rect' or 'dot'.
Icon string
}
// NewLegendOption returns a legend option
func NewLegendOption(labels []string, left ...string) LegendOption {
opt := LegendOption{
Data: labels,
}
if len(left) != 0 {
opt.Left = left[0]
}
return opt
}
// IsEmpty checks legend is empty
func (opt *LegendOption) IsEmpty() bool {
for _, v := range opt.Data {
if v != "" {
return false
}
}
return true
}
// NewLegendPainter returns a legend renderer
func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
return &legendPainter{
p: p,
opt: &opt,
}
}
func (l *legendPainter) Render() (Box, error) {
opt := l.opt
if opt.IsEmpty() || flagIs(false, opt.Show) {
return BoxZero, nil
}
theme := opt.Theme
if theme == nil {
theme = getPreferredTheme(l.p.theme)
}
fontStyle := opt.FontStyle
if fontStyle.FontSize == 0 {
fontStyle.FontSize = defaultFontSize
}
if fontStyle.FontColor.IsZero() {
fontStyle.FontColor = theme.GetTextColor()
}
if opt.Left == "" { // default a center legend
opt.Left = PositionCenter
}
padding := opt.Padding
if padding.IsZero() {
padding.Top = 5
}
p := l.p.Child(PainterPaddingOption(padding))
p.SetFontStyle(fontStyle)
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
}
// calculate the width and height of the display
width := 0
height := 0
offset := 20
textOffset := 2
legendWidth := 30
legendHeight := 20
itemMaxHeight := 0
for _, item := range measureList {
if item.Height() > itemMaxHeight {
itemMaxHeight = item.Height()
}
if opt.Orient == OrientVertical {
height += item.Height()
} else {
width += item.Width()
}
}
// add padding
itemMaxHeight += 10
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
}
// calculate starting position
var left int
switch opt.Left {
case PositionLeft:
// no-op
case PositionRight:
left = p.Width() - width
case PositionCenter:
left = (p.Width() - width) >> 1
default:
if v, err := parseFlexibleValue(opt.Left, float64(p.Width())); err != nil {
return BoxZero, err
} else {
left = int(v)
}
}
if left < 0 {
left = 0
}
var top int
if opt.Top != "" {
if v, err := parseFlexibleValue(opt.Top, float64(p.Height())); err != nil {
return BoxZero, fmt.Errorf("unexpected parsing error: %v", err)
} else {
top = int(v)
}
}
x := left
y := top + 10
startY := y
x0 := x
y0 := y
drawIcon := func(top, left int) int {
if opt.Icon == IconRect {
p.Rect(Box{
Top: top - legendHeight + 8,
Left: left,
Right: left + legendWidth,
Bottom: top + 1,
})
} else {
p.LegendLineDot(Box{
Top: top + 1,
Left: left,
Right: left + legendWidth,
Bottom: top + legendHeight + 1,
})
}
return left + legendWidth
}
lastIndex := len(opt.Data) - 1
for index, text := range opt.Data {
color := theme.GetSeriesColor(index)
p.SetDrawingStyle(chartdraw.Style{
FillColor: color,
StrokeColor: color,
})
itemWidth := x0 + measureList[index].Width() + textOffset + offset + legendWidth
if lastIndex == index {
itemWidth = x0 + measureList[index].Width() + legendWidth
}
if itemWidth > p.Width() {
x0 = 0
y += itemMaxHeight
y0 = y
}
if opt.Align != AlignRight {
x0 = drawIcon(y0, x0)
x0 += textOffset
}
p.Text(text, x0, y0)
x0 += measureList[index].Width()
if opt.Align == AlignRight {
x0 += textOffset
x0 = drawIcon(y0, x0)
}
if opt.Orient == OrientVertical {
y0 += offset
x0 = x
} else {
x0 += offset
y0 = y
}
height = y0 - startY + 10
}
return Box{
Right: width,
Bottom: height + padding.Bottom + padding.Top,
}, nil
}