-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathlegend.go
283 lines (262 loc) · 7.66 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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
package charts
import (
"fmt"
)
type legendPainter struct {
p *Painter
opt *LegendOption
}
type LegendOption struct {
// Show specifies if the legend should be rendered, set this to *false (through Ptr(false)) to hide the legend.
Show *bool
// Theme specifies the colors used for the legend.
Theme ColorPalette
// SeriesNames provides text labels for the legend.
SeriesNames []string
// FontStyle specifies the font, size, and style for rendering the legend.
FontStyle FontStyle
// Padding specifies space padding around the legend.
Padding Box
// Offset allows you to specify the position of the legend component relative to the left and top side.
Offset OffsetStr
// Align is the legend marker and text alignment, it can be 'left', 'right' or 'center', default is 'left'.
Align string
// Vertical can be set to *true to set the legend orientation to be vertical.
Vertical *bool
// Symbol defines the icon shape next to the label. Can be 'square', 'dot', or 'diamond'.
Symbol Symbol
// OverlayChart can be set to *true to render the legend over the chart. Ignored if Vertical is set to true (always overlapped).
OverlayChart *bool
// BorderWidth can be set to a non-zero value to render a box around the legend.
BorderWidth float64
// seriesSymbols provides custom symbols for each series.
seriesSymbols []Symbol
}
// IsEmpty checks legend is empty
func (opt *LegendOption) IsEmpty() bool {
for _, v := range opt.SeriesNames {
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 := fillFontStyleDefaults(opt.FontStyle, defaultFontSize, theme.GetLegendTextColor())
vertical := flagIs(true, opt.Vertical)
offset := opt.Offset
if offset.Left == "" {
if vertical {
// in the vertical orientation it's more visually appealing to default to the right side or left side
if opt.Align != "" {
offset.Left = opt.Align
} else {
offset.Left = PositionLeft
}
} else {
offset.Left = PositionCenter
}
}
padding := opt.Padding
if padding.IsZero() {
padding.Top = 5
}
p := l.p.Child(PainterPaddingOption(padding))
// calculate the width and height of the display
measureList := make([]Box, len(opt.SeriesNames))
var width, height int
const builtInSpacing = 20
const textOffset = 2
const legendWidth = 30
const legendHeight = 20
var maxTextWidth, itemMaxHeight int
for index, text := range opt.SeriesNames {
b := p.MeasureText(text, 0, fontStyle)
if b.Width() > maxTextWidth {
maxTextWidth = b.Width()
}
if b.Height() > itemMaxHeight {
itemMaxHeight = b.Height()
}
if flagIs(true, opt.Vertical) {
height += b.Height()
} else {
width += b.Width()
}
measureList[index] = b
}
// add padding
if vertical {
width = maxTextWidth + textOffset + legendWidth
height = builtInSpacing * len(opt.SeriesNames)
} else {
height = legendHeight
offsetValue := (len(opt.SeriesNames) - 1) * (builtInSpacing + textOffset)
allLegendWidth := len(opt.SeriesNames) * legendWidth
width += offsetValue + allLegendWidth
}
// calculate starting position
var left int
switch offset.Left {
case PositionLeft:
// leave default of zero
case PositionRight:
left = p.Width() - width
case PositionCenter:
left = p.Width()>>1 - (width >> 1)
default:
if v, err := parseFlexibleValue(offset.Left, float64(p.Width())); err != nil {
return BoxZero, fmt.Errorf("error parsing legend position: %w", err)
} else {
left = int(v)
}
}
if left < 0 {
left = 0
}
var top int
switch offset.Top {
case "", PositionTop:
// leave default of zero
case PositionBottom:
top = p.Height() - height
default:
if v, err := parseFlexibleValue(offset.Top, float64(p.Height())); err != nil {
return BoxZero, fmt.Errorf("error parsing legend position: %w", err)
} else {
top = int(v)
}
}
y := top + 10
x0 := left
y0 := y
lastIndex := len(opt.SeriesNames) - 1
for index, text := range opt.SeriesNames {
seriesSymbol := opt.Symbol
if index < len(opt.seriesSymbols) && opt.seriesSymbols[index] != "" {
seriesSymbol = opt.seriesSymbols[index]
}
var drawIcon func(top, left int, color Color) int
switch seriesSymbol {
case SymbolSquare:
drawIcon = func(top, left int, color Color) int {
p.FilledRect(left, top-legendHeight+8, left+legendWidth, top+1, color, color, 0)
return left + legendWidth
}
case SymbolDiamond:
drawIcon = func(top, left int, color Color) int {
p.FilledDiamond(left+5, top-5, 15, 20, color, color, 0)
return left + legendHeight
}
case SymbolNone:
drawIcon = func(top, left int, color Color) int {
return left
}
default:
centerColor := ColorTransparent
if seriesSymbol == SymbolCircle {
centerColor = opt.Theme.GetBackgroundColor()
}
drawIcon = func(top, left int, color Color) int {
p.legendLineDot(Box{
Top: top + 1,
Left: left,
Right: left + legendWidth,
Bottom: top + legendHeight + 1,
IsSet: true,
}, color, 3, color, centerColor)
return left + legendWidth
}
}
color := theme.GetSeriesColor(index)
if vertical {
if opt.Align == AlignRight {
// adjust x0 so that the text will start with a right alignment to the longest line
x0 += maxTextWidth - measureList[index].Width()
}
} else {
// check if item will overrun the right side boundary
itemWidth := x0 + measureList[index].Width() + textOffset + builtInSpacing + legendWidth
if lastIndex == index {
itemWidth = x0 + measureList[index].Width() + legendWidth
}
if itemWidth > p.Width() {
newLineStart := left
if opt.Align == AlignCenter {
// recalculate width and center based off remaining width
var remainingWidth int
for i2 := index; i2 < len(opt.SeriesNames); i2++ {
b := p.MeasureText(opt.SeriesNames[i2], 0, fontStyle)
remainingWidth += b.Width()
}
remainingCount := len(opt.SeriesNames) - index
remainingWidth += remainingCount * legendWidth
remainingWidth += (remainingCount - 1) * (builtInSpacing + textOffset)
newLineStart = left + ((p.Width() >> 1) - (remainingWidth >> 1))
if newLineStart < 0 {
newLineStart = 0
}
}
x0 = newLineStart
y += itemMaxHeight
y0 = y
}
}
if opt.Align != AlignRight {
x0 = drawIcon(y0, x0, color)
x0 += textOffset
}
p.Text(text, x0, y0, 0, fontStyle)
x0 += measureList[index].Width()
if opt.Align == AlignRight {
x0 += textOffset
x0 = drawIcon(y0, x0, color)
}
if vertical {
y0 += builtInSpacing
x0 = left
} else {
x0 += builtInSpacing
y0 = y
}
}
bottom := y0 + padding.Bottom - 10
if !vertical {
bottom += itemMaxHeight
}
result := Box{
Top: top - padding.Top,
Bottom: bottom,
Left: left - padding.Left,
Right: left + width + padding.Right,
IsSet: true,
}
if opt.BorderWidth > 0 {
// TODO - if drawn over the chart this can look awkward, we should try to draw this first
boxPad := 10 // built in adjustment for possible measure vs render variations
boxPoints := []Point{
{X: result.Left - boxPad, Y: result.Bottom + boxPad},
{X: result.Left - boxPad, Y: result.Top - boxPad},
{X: result.Left + result.Width() + boxPad, Y: result.Top - boxPad},
{X: result.Left + result.Width() + boxPad, Y: result.Bottom + boxPad},
{X: result.Left - boxPad, Y: result.Bottom + boxPad},
}
p.LineStroke(boxPoints, theme.GetLegendBorderColor(), opt.BorderWidth)
}
return result, nil
}