mine
derailed 2020-03-01 19:25:49 -07:00
parent f0ae348b02
commit 43d62f9528
16 changed files with 426 additions and 87 deletions

2
go.mod
View File

@ -2,6 +2,8 @@ module github.com/derailed/k9s
go 1.13
replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview
replace (
github.com/docker/docker => github.com/docker/docker v1.4.2-0.20181221150755-2cb26cfe9cbf
k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f

View File

@ -112,6 +112,11 @@ func join(a []string, sep string) string {
return buff.String()
}
// ToPerc prints a number as percentage.
func ToPerc(f float64) string {
return AsPerc(f) + "%"
}
// AsPerc prints a number as a percentage.
func AsPerc(f float64) string {
return strconv.Itoa(int(f))

View File

@ -6,7 +6,6 @@ import (
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
const (
@ -61,9 +60,7 @@ func (c *Component) InputHandler() func(event *tcell.EventKey, setFocus func(p t
return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
switch key := event.Key(); key {
case tcell.KeyEnter:
log.Debug().Msgf("YO %s ENTER!!", c.id)
case tcell.KeyBacktab, tcell.KeyTab:
log.Debug().Msgf("YO %s TAB!!", c.id)
if c.blur != nil {
c.blur(key)
}
@ -113,7 +110,8 @@ func (c *Component) GetSeriesColorNames() []string {
func (c *Component) colorForSeries() (tcell.Color, tcell.Color) {
c.mx.RLock()
defer c.mx.RUnlock()
if len(c.seriesColors) > 1 {
if len(c.seriesColors) == 2 {
return c.seriesColors[0], c.seriesColors[1]
}
return okColor, faultColor

View File

@ -0,0 +1,25 @@
package tchart
import (
"image"
"testing"
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
)
func TestComponentAsRect(t *testing.T) {
c := NewComponent("fred")
r := image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 15, Y: 10}}
assert.Equal(t, r, c.asRect())
}
func TestComponentColorForSeries(t *testing.T) {
c := NewComponent("fred")
okC, errC := c.colorForSeries()
assert.Equal(t, tview.Styles.PrimaryTextColor, okC)
assert.Equal(t, tview.Styles.FocusColor, errC)
assert.Equal(t, []string{"white", "green"}, c.GetSeriesColorNames())
}

View File

@ -0,0 +1,25 @@
package tchart_test
import (
"testing"
"github.com/derailed/k9s/internal/tchart"
"github.com/gdamore/tcell"
"github.com/stretchr/testify/assert"
)
func TestCoSeriesColorNames(t *testing.T) {
c := tchart.NewComponent("fred")
c.SetSeriesColors(tcell.ColorGreen, tcell.ColorBlue)
assert.Equal(t, []string{"green", "blue"}, c.GetSeriesColorNames())
}
func TestComponentAsRect(t *testing.T) {
c := tchart.NewComponent("fred")
c.SetSeriesColors(tcell.ColorGreen, tcell.ColorBlue)
assert.Equal(t, []string{"green", "blue"}, c.GetSeriesColorNames())
}

View File

@ -38,12 +38,12 @@ func TestSegmentFor(t *testing.T) {
}
}
func TestDial(t *testing.T) {
func TestDial5x3(t *testing.T) {
d := tchart.NewDotMatrix(5, 3)
for n := 0; n <= 9; n++ {
i := n
t.Run(strconv.Itoa(n), func(t *testing.T) {
assert.Equal(t, numbers[i], d.Print(i))
assert.Equal(t, numbers3x5[i], d.Print(i))
})
}
}
@ -52,7 +52,7 @@ func TestDial(t *testing.T) {
const hChar, vChar = '▤', '▥'
var numbers = []tchart.Matrix{
var numbers3x5 = []tchart.Matrix{
[][]rune{
{hChar, hChar, hChar},
{vChar, ' ', vChar},

View File

@ -52,33 +52,6 @@ func (g *Gauge) Add(m Metric) {
g.data = m
}
func (g *Gauge) drawNum(sc tcell.Screen, ok bool, o image.Point, n int, dn delta, ns string, style tcell.Style) {
c1, _ := g.colorForSeries()
if ok {
o.X -= 1
style = style.Foreground(c1)
printDelta(sc, dn, o, style)
o.X += 1
}
dm, sig := NewDotMatrix(5, 3), n == 0
if n == 0 {
style = g.dimmed
}
for i := 0; i < len(ns); i++ {
if ns[i] == '0' && !sig {
g.drawDial(sc, dm.Print(int(ns[i]-48)), o, g.dimmed)
} else {
sig = true
g.drawDial(sc, dm.Print(int(ns[i]-48)), o, style)
}
o.X += 5
}
if !ok {
printDelta(sc, dn, o, style)
}
}
// Draw draws the primitive.
func (g *Gauge) Draw(sc tcell.Screen) {
g.Component.Draw(sc)
@ -87,7 +60,7 @@ func (g *Gauge) Draw(sc tcell.Screen) {
defer g.mx.RUnlock()
rect := g.asRect()
mid := image.Point{X: rect.Min.X + rect.Dx()/2 - 2, Y: rect.Min.Y + rect.Dy()/2 - 2}
mid := image.Point{X: rect.Min.X + rect.Dx()/2 - 1, Y: rect.Min.Y + rect.Dy()/2 - 2}
style := tcell.StyleDefault.Background(g.bgColor)
style = style.Foreground(tcell.ColorYellow)
@ -129,6 +102,33 @@ func (g *Gauge) drawDial(sc tcell.Screen, m Matrix, o image.Point, style tcell.S
}
}
func (g *Gauge) drawNum(sc tcell.Screen, ok bool, o image.Point, n int, dn delta, ns string, style tcell.Style) {
c1, _ := g.colorForSeries()
if ok {
o.X -= 1
style = style.Foreground(c1)
printDelta(sc, dn, o, style)
o.X += 1
}
dm, sig := NewDotMatrix(5, 3), n == 0
if n == 0 {
style = g.dimmed
}
for i := 0; i < len(ns); i++ {
if ns[i] == '0' && !sig {
g.drawDial(sc, dm.Print(int(ns[i]-48)), o, g.dimmed)
} else {
sig = true
g.drawDial(sc, dm.Print(int(ns[i]-48)), o, style)
}
o.X += 5
}
if !ok {
printDelta(sc, dn, o, style)
}
}
// ----------------------------------------------------------------------------
// Helpers...

View File

@ -0,0 +1,35 @@
package tchart
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestComputeDeltas(t *testing.T) {
uu := map[string]struct {
d1, d2 int
e delta
}{
"same": {
e: DeltaSame,
},
"more": {
d1: 10,
d2: 20,
e: DeltaMore,
},
"less": {
d1: 20,
d2: 10,
e: DeltaLess,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, computeDelta(u.d1, u.d2))
})
}
}

View File

@ -0,0 +1,88 @@
package tchart_test
import (
"testing"
"github.com/derailed/k9s/internal/tchart"
"github.com/stretchr/testify/assert"
)
func TestMetricsMaxDigits(t *testing.T) {
uu := map[string]struct {
m tchart.Metric
e int
}{
"empty": {
e: 1,
},
"oks": {
m: tchart.Metric{OK: 100, Fault: 10},
e: 3,
},
"errs": {
m: tchart.Metric{OK: 10, Fault: 1000},
e: 4,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.m.MaxDigits())
})
}
}
func TestMetricsMax(t *testing.T) {
uu := map[string]struct {
m tchart.Metric
e int
}{
"empty": {
e: 0,
},
"max_ok": {
m: tchart.Metric{OK: 100, Fault: 10},
e: 100,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.m.Max())
})
}
}
func TestGauge(t *testing.T) {
uu := map[string]struct {
mm []tchart.Metric
e int
}{
"empty": {
e: 1,
},
"oks": {
mm: []tchart.Metric{{OK: 100, Fault: 10}},
e: 3,
},
"errs": {
mm: []tchart.Metric{{OK: 10, Fault: 1000}},
e: 4,
},
}
for k := range uu {
u := uu[k]
g := tchart.NewGauge("fred")
assert.True(t, g.IsDial())
for _, m := range u.mm {
g.Add(m)
}
t.Run(k, func(t *testing.T) {
// assert.Equal(t, u.e, u.m.MaxDigits())
})
}
}

View File

@ -2,6 +2,7 @@ package tchart
import (
"fmt"
"image"
"math"
"github.com/derailed/tview"
@ -24,13 +25,18 @@ type Metric struct {
OK, Fault int
}
// MaxDigits returns the max of the metric.
// MaxDigits returns the max series number of digits.
func (m Metric) MaxDigits() int {
max := int(math.Max(float64(m.OK), float64(m.Fault)))
s := fmt.Sprintf("%d", max)
s := fmt.Sprintf("%d", m.Max())
return len(s)
}
// Max returns the max of the series.
func (m Metric) Max() int {
return int(math.Max(float64(m.OK), float64(m.Fault)))
}
// Sum returns the sum of the metrics.
func (m Metric) Sum() int {
return m.OK + m.Fault
@ -40,16 +46,23 @@ func (m Metric) Sum() int {
type SparkLine struct {
*Component
data []Metric
data []Metric
multiSeries bool
}
// NewSparkLine returns a new graph.
func NewSparkLine(id string) *SparkLine {
return &SparkLine{
Component: NewComponent(id),
Component: NewComponent(id),
multiSeries: true,
}
}
// SetSingleSeries indicates multi series are in effect or not.
func (s *SparkLine) SetMultiSeries(b bool) {
s.multiSeries = b
}
// Add adds a metric.
func (s *SparkLine) Add(m Metric) {
s.mx.Lock()
@ -84,13 +97,17 @@ func (s *SparkLine) Draw(screen tcell.Screen) {
idx = len(s.data) - rect.Dx()/2
}
scale := float64(len(sparks)) * float64((rect.Dy() - pad)) / float64(max)
factor := 2
if !s.multiSeries {
factor = 1
}
scale := float64(len(sparks)*(rect.Dy()-pad)/factor) / float64(max)
c1, c2 := s.colorForSeries()
for _, d := range s.data[idx:] {
b := toBlocks(d, scale)
cY := rect.Max.Y - pad
s.drawBlock(screen, cX, cY, b.oks, c1)
s.drawBlock(screen, cX, cY, b.errs, c2)
cY = s.drawBlock(rect, screen, cX, cY, b.oks, c1)
_ = s.drawBlock(rect, screen, cX, cY, b.errs, c2)
cX += 2
}
@ -103,16 +120,25 @@ func (s *SparkLine) Draw(screen tcell.Screen) {
}
}
func (s *SparkLine) drawBlock(screen tcell.Screen, x, y int, b block, c tcell.Color) {
func (s *SparkLine) drawBlock(r image.Rectangle, screen tcell.Screen, x, y int, b block, c tcell.Color) int {
style := tcell.StyleDefault.Foreground(c).Background(s.bgColor)
zeroY := r.Max.Y - r.Dy()
for i := 0; i < b.full; i++ {
screen.SetContent(x, y, sparks[len(sparks)-1], nil, style)
y--
if y <= zeroY {
break
}
}
if b.partial != 0 {
screen.SetContent(x, y, b.partial, nil, style)
if b.full == 0 {
y--
}
}
return y
}
func (s *SparkLine) cutSet(width int) {
@ -128,8 +154,9 @@ func (s *SparkLine) cutSet(width int) {
func (s *SparkLine) computeMax() int {
var max int
for _, d := range s.data {
if max < d.OK {
max = d.OK
m := d.Max()
if max < m {
max = m
}
}
@ -140,18 +167,17 @@ func toBlocks(m Metric, scale float64) blocks {
if m.Sum() <= 0 {
return blocks{}
}
return blocks{oks: makeBlocks(m.OK, false, scale), errs: makeBlocks(m.Fault, true, scale)}
return blocks{oks: makeBlocks(m.OK, scale), errs: makeBlocks(m.Fault, scale)}
}
func makeBlocks(v int, isErr bool, scale float64) block {
func makeBlocks(v int, scale float64) block {
scaled := int(math.Round(float64(v) * scale))
part, b := scaled%len(sparks), block{full: scaled / len(sparks)}
// Err might get scaled way down if so nudge.
if v > 0 && isErr && scaled == 0 {
part = 1
p, b := scaled%len(sparks), block{full: scaled / len(sparks)}
if b.full == 0 && v > 0 && p == 0 {
p = 4
}
if part > 0 {
b.partial = sparks[part-1]
if v > 0 && p >= 0 && p < len(sparks) {
b.partial = sparks[p]
}
return b

View File

@ -0,0 +1,131 @@
package tchart
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCutSet(t *testing.T) {
uu := map[string]struct {
mm []Metric
w, e int
}{
"empty": {
w: 10,
e: 0,
},
"at": {
mm: make([]Metric, 10),
w: 10,
e: 10,
},
"under": {
mm: make([]Metric, 5),
w: 10,
e: 5,
},
"over": {
mm: make([]Metric, 10),
w: 5,
e: 5,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
s := NewSparkLine("s")
assert.False(t, s.IsDial())
for _, m := range u.mm {
s.Add(m)
}
s.cutSet(u.w)
assert.Equal(t, u.e, len(s.data))
})
}
}
func TestToBlocks(t *testing.T) {
uu := map[string]struct {
m Metric
s float64
e blocks
}{
"empty": {
e: blocks{},
},
"max_ok": {
m: Metric{OK: 100, Fault: 10},
s: 0.5,
e: blocks{
oks: block{full: 6, partial: sparks[2]},
errs: block{full: 0, partial: sparks[5]},
},
},
"max_fault": {
m: Metric{OK: 10, Fault: 100},
s: 0.5,
e: blocks{
oks: block{full: 0, partial: sparks[5]},
errs: block{full: 6, partial: sparks[2]},
},
},
"over": {
m: Metric{OK: 22, Fault: 999},
s: float64(8*20) / float64(999),
e: blocks{
oks: block{full: 0, partial: sparks[4]},
errs: block{full: 20, partial: sparks[0]},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, toBlocks(u.m, u.s))
})
}
}
func TestComputeMax(t *testing.T) {
uu := map[string]struct {
mm []Metric
e int
}{
"empty": {
e: 0,
},
"max_ok": {
mm: []Metric{{OK: 100, Fault: 10}},
e: 100,
},
"max_fault": {
mm: []Metric{{OK: 100, Fault: 1000}},
e: 1000,
},
"many": {
mm: []Metric{
{OK: 100, Fault: 1000},
{OK: 110, Fault: 1010},
{OK: 120, Fault: 1020},
{OK: 130, Fault: 1030},
{OK: 140, Fault: 1040},
},
e: 1040,
},
}
for k := range uu {
u := uu[k]
s := NewSparkLine("s")
for _, m := range u.mm {
s.Add(m)
}
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, s.computeMax())
})
}
}

View File

@ -45,7 +45,7 @@ func (s *StatusIndicator) StylesChanged(styles *config.Styles) {
s.SetTextColor(styles.FgColor())
}
const statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
const statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s[white::]::[darkturquoise::]%s"
// ClusterInfoUpdated notifies the cluster meta was updated.
func (s *StatusIndicator) ClusterInfoUpdated(data model.ClusterMeta) {
@ -56,8 +56,8 @@ func (s *StatusIndicator) ClusterInfoUpdated(data model.ClusterMeta) {
data.Cluster,
data.User,
data.K8sVer,
render.AsPerc(data.Cpu),
render.AsPerc(data.Mem),
render.ToPerc(data.Cpu),
render.ToPerc(data.Mem),
))
})
}

View File

@ -9,10 +9,9 @@ import (
type SelectTable struct {
*tview.Table
model Tabular
selectedRow int
selectedFn func(string) string
marks map[string]struct{}
model Tabular
selectedFn func(string) string
marks map[string]struct{}
}
// SetModel sets the table model.
@ -69,7 +68,8 @@ func (s *SelectTable) GetSelectedItem() string {
// GetSelectedCell returns the content of a cell for the currently selected row.
func (s *SelectTable) GetSelectedCell(col int) string {
return TrimCell(s, s.selectedRow, col)
r, _ := s.GetSelection()
return TrimCell(s, r, col)
}
// SetSelectedFn defines a function that cleanse the current selection.
@ -79,7 +79,8 @@ func (s *SelectTable) SetSelectedFn(f func(string) string) {
// GetSelectedRowIndex fetch the currently selected row index.
func (s *SelectTable) GetSelectedRowIndex() int {
return s.selectedRow
r, _ := s.GetSelection()
return r
}
// SelectRow select a given row by index.
@ -93,14 +94,14 @@ func (s *SelectTable) SelectRow(r int, broadcast bool) {
// UpdateSelection refresh selected row.
func (s *SelectTable) updateSelection(broadcast bool) {
s.SelectRow(s.selectedRow, broadcast)
r, _ := s.GetSelection()
s.SelectRow(r, broadcast)
}
func (s *SelectTable) selectionChanged(r, c int) {
if r < 0 {
return
}
s.selectedRow = r
cell := s.GetCell(r, c)
s.SetSelectedStyle(tcell.ColorBlack, cell.Color, tcell.AttrBold)
}

View File

@ -50,10 +50,9 @@ type Table struct {
func NewTable(gvr client.GVR) *Table {
return &Table{
SelectTable: &SelectTable{
Table: tview.NewTable(),
model: model.NewTable(gvr),
selectedRow: 1,
marks: make(map[string]struct{}),
Table: tview.NewTable(),
model: model.NewTable(gvr),
marks: make(map[string]struct{}),
},
gvr: gvr,
actions: make(KeyActions),
@ -71,7 +70,7 @@ func (t *Table) Init(ctx context.Context) {
t.SetSelectable(true, false)
t.SetSelectionChangedFunc(t.selectionChanged)
t.SetBackgroundColor(tcell.ColorDefault)
t.Select(1, 0)
t.hasMetrics = false
if mx, ok := ctx.Value(internal.KeyHasMetrics).(bool); ok {
t.hasMetrics = mx

View File

@ -159,10 +159,10 @@ func (h *Help) showNav() model.MenuHints {
},
{
Mnemonic: "Ctrl-b",
Description: "Page Down"},
Description: "Page Up"},
{
Mnemonic: "Ctrl-f",
Description: "Page Up",
Description: "Page Down",
},
{
Mnemonic: "h",

View File

@ -80,25 +80,26 @@ func (p *Pulse) Init(ctx context.Context) error {
}
p.charts = []Grapheable{
p.makeGA(image.Point{X: 0, Y: 0}, image.Point{X: 4, Y: 2}, "apps/v1/deployments"),
p.makeGA(image.Point{X: 0, Y: 2}, image.Point{X: 4, Y: 2}, "apps/v1/replicasets"),
p.makeGA(image.Point{X: 0, Y: 4}, image.Point{X: 4, Y: 2}, "apps/v1/statefulsets"),
p.makeGA(image.Point{X: 0, Y: 6}, image.Point{X: 4, Y: 2}, "apps/v1/daemonsets"),
p.makeSP(image.Point{X: 4, Y: 0}, image.Point{X: 3, Y: 4}, "v1/pods"),
p.makeSP(image.Point{X: 4, Y: 4}, image.Point{X: 3, Y: 4}, "v1/events"),
p.makeSP(image.Point{X: 7, Y: 0}, image.Point{X: 3, Y: 4}, "batch/v1/jobs"),
p.makeSP(image.Point{X: 7, Y: 4}, image.Point{X: 3, Y: 4}, "v1/persistentvolumes"),
p.makeGA(image.Point{X: 0, Y: 0}, image.Point{X: 3, Y: 2}, "apps/v1/deployments"),
p.makeGA(image.Point{X: 0, Y: 2}, image.Point{X: 3, Y: 2}, "apps/v1/replicasets"),
p.makeGA(image.Point{X: 0, Y: 4}, image.Point{X: 3, Y: 2}, "apps/v1/statefulsets"),
p.makeGA(image.Point{X: 0, Y: 6}, image.Point{X: 3, Y: 2}, "apps/v1/daemonsets"),
p.makeSP(true, image.Point{X: 3, Y: 0}, image.Point{X: 3, Y: 4}, "v1/pods"),
p.makeSP(true, image.Point{X: 3, Y: 4}, image.Point{X: 3, Y: 4}, "v1/events"),
p.makeSP(true, image.Point{X: 6, Y: 0}, image.Point{X: 3, Y: 4}, "batch/v1/jobs"),
p.makeSP(true, image.Point{X: 6, Y: 4}, image.Point{X: 3, Y: 4}, "v1/persistentvolumes"),
}
if p.app.Conn().HasMetrics() {
p.charts = append(p.charts,
p.makeSP(image.Point{X: 10, Y: 0}, image.Point{X: 2, Y: 4}, "cpu"),
p.makeSP(image.Point{X: 10, Y: 4}, image.Point{X: 2, Y: 4}, "mem"),
p.makeSP(false, image.Point{X: 9, Y: 0}, image.Point{X: 2, Y: 4}, "cpu"),
p.makeSP(false, image.Point{X: 9, Y: 4}, image.Point{X: 2, Y: 4}, "mem"),
)
}
p.bindKeys()
p.model.AddListener(p)
p.app.SetFocus(p.charts[0])
p.app.Styles.AddListener(p)
p.StylesChanged(p.app.Styles)
return nil
}
@ -135,9 +136,9 @@ func (p *Pulse) PulseChanged(c *health.Check) {
gvr := client.NewGVR(c.GVR)
switch c.GVR {
case "cpu":
v.SetLegend(fmt.Sprintf(" %s - %dm", strings.Title(gvr.R()), c.Tally(health.OK)))
v.SetLegend(fmt.Sprintf(" %s(%dm)", strings.Title(gvr.R()), c.Tally(health.OK)))
case "mem":
v.SetLegend(fmt.Sprintf(" %s - %dMi", strings.Title(gvr.R()), c.Tally(health.OK)))
v.SetLegend(fmt.Sprintf(" %s(%dMi)", strings.Title(gvr.R()), c.Tally(health.OK)))
default:
nn := v.GetSeriesColorNames()
if c.Tally(health.OK) == 0 {
@ -146,7 +147,7 @@ func (p *Pulse) PulseChanged(c *health.Check) {
if c.Tally(health.Toast) == 0 {
nn[1] = "gray"
}
v.SetLegend(fmt.Sprintf(" %s - [%s::]%d/[%s::b]%d[-::]",
v.SetLegend(fmt.Sprintf(" %s([%s::]%d[white::]:[%s::b]%d[-::])",
strings.Title(gvr.R()),
nn[0],
c.Tally(health.OK),
@ -296,7 +297,7 @@ func (p *Pulse) nextFocusCmd(direction int) func(evt *tcell.EventKey) *tcell.Eve
}
}
func (p *Pulse) makeSP(loc image.Point, span image.Point, gvr string) *tchart.SparkLine {
func (p *Pulse) makeSP(multi bool, loc image.Point, span image.Point, gvr string) *tchart.SparkLine {
s := tchart.NewSparkLine(gvr)
s.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color())
s.SetBorderPadding(0, 1, 0, 1)
@ -307,6 +308,9 @@ func (p *Pulse) makeSP(loc image.Point, span image.Point, gvr string) *tchart.Sp
}
s.SetLegend(fmt.Sprintf(" %s ", strings.Title(client.NewGVR(gvr).R())))
s.SetInputCapture(p.keyboard)
if !multi {
s.SetMultiSeries(multi)
}
p.AddItem(s, loc.X, loc.Y, span.X, span.Y, 0, 0, true)
return s
@ -323,7 +327,7 @@ func (p *Pulse) makeGA(loc image.Point, span image.Point, gvr string) *tchart.Ga
}
g.SetLegend(fmt.Sprintf(" %s ", strings.Title(client.NewGVR(gvr).R())))
g.SetInputCapture(p.keyboard)
p.AddItem(g, loc.X, loc.Y, span.X, span.Y, 0, 0, true)
p.AddItem(g, loc.X, loc.Y, span.X, span.Y, span.X, span.Y, true)
return g
}