Files
MujicaUI/realtime_chart.go
T

122 lines
3.8 KiB
Go

package mujica
import (
"log/slog"
"math"
"sort"
"time"
"github.com/egoist/mygo/ui"
)
// RealtimeBuffer keeps the latest samples of named series.
type RealtimeBuffer struct {
names []string
limit int
times []time.Time
vals [][]float64
}
// NewRealtimeBuffer keeps up to capacity samples of the named series.
func NewRealtimeBuffer(capacity int, names ...string) *RealtimeBuffer {
if capacity < 2 || len(names) == 0 {
panic("mujica: RealtimeBuffer needs a capacity of 2 or more and a series name")
}
return &RealtimeBuffer{names: names, limit: capacity, vals: make([][]float64, len(names))}
}
// Append adds one sample per series at a non-decreasing t.
func (b *RealtimeBuffer) Append(t time.Time, values ...float64) {
if len(values) != len(b.names) || len(b.times) > 0 && t.Before(b.times[len(b.times)-1]) {
panic("mujica: RealtimeBuffer.Append needs one value per series and a time not before the last")
}
b.times = append(b.times, t)
for i, v := range values {
b.vals[i] = append(b.vals[i], v)
}
// Old samples are dropped in blocks, so appends stay cheap.
if len(b.times) >= 2*b.limit {
drop := len(b.times) - b.limit
b.times = append([]time.Time(nil), b.times[drop:]...)
for i := range b.vals {
b.vals[i] = append([]float64(nil), b.vals[i][drop:]...)
}
}
}
// Len is how many samples the buffer keeps.
func (b *RealtimeBuffer) Len() int { return min(len(b.times), b.limit) }
// RealtimeChartOptions configure a RealtimeChart.
type RealtimeChartOptions struct {
Frame ChartFrame
// Window is the time shown up to the newest sample.
Window time.Duration
Format func(v float64) string
}
// RealtimeChartState holds whether the view is paused, and where.
type RealtimeChartState struct {
Paused bool
end time.Time
}
// RealtimeChart shows a fixed window ending at the newest sample.
func RealtimeChart(c *ui.Context, s *RealtimeChartState, buf *RealtimeBuffer, opts RealtimeChartOptions) ChartResult {
if opts.Window <= 0 {
panic("mujica: RealtimeChart needs a positive Window")
}
u := Density(c).Unit()
var res ChartResult
wrap := ui.Column(c).Gap(u * 2).MinWidth(0)
wrap.Children(func() {
label := msg(c, "realtime_chart.pause")
if s.Paused {
label = msg(c, "realtime_chart.resume")
}
if b := Button(c, label, ButtonOptions{Variant: Secondary, Disabled: wrap.IsDisabled()}).AlignSelf(ui.Start); b.Clicked() && !b.IsDisabled() {
s.Paused, res.value = !s.Paused, true
slog.Debug("mujica: realtime_chart pause", "paused", s.Paused)
}
times := buf.times[max(0, len(buf.times)-buf.limit):]
last := time.Time{}
if len(times) > 0 {
last = times[len(times)-1]
}
if !s.Paused || s.end.IsZero() {
s.end = last
}
from := s.end.Add(-opts.Window)
i0 := sort.Search(len(times), func(i int) bool { return !times[i].Before(from) })
i1 := sort.Search(len(times), func(i int) bool { return times[i].After(s.end) })
off := len(buf.times) - len(times)
ch := newChart(c, opts.Frame)
series := make([]ChartSeries, len(buf.names))
tops := make([][]float64, len(buf.names))
for i, name := range buf.names {
series[i] = ChartSeries{Name: name, Values: buf.vals[i][off+i0 : off+i1]}
tops[i] = series[i].Values
}
n := i1 - i0
var ax chartX
if n > 0 {
ax = chartMemo(ch, "x", [2]any{chartID(times[i0:i1]), s.end}, func() chartX {
a := newChartX(n, nil, times[i0:i1])
a.d0, a.d1 = float64(from.UnixNano())/1e9, float64(s.end.UnixNano())/1e9
if a.d1 == a.d0 {
a.d0 -= 1
}
return a
})
}
lo, hi, ok := chartYRange(ch, series, false, nil)
if !ok || math.IsNaN(lo) {
n = 0
}
r := chartLines(ch, chartLineData{series: series, tops: tops, ax: ax, lo: lo, hi: hi, n: n, format: opts.Format})
res.Point, res.changed, res.retry, res.toggled = r.Point, r.changed, r.retry, r.toggled
})
res.Element = wrap
return res
}