Skip to content

Commit

Permalink
LineChart now has an option that disables scaling of X axis.
Browse files Browse the repository at this point in the history
Useful for applications that want to continuously feed data and make
them "roll" through the linechart.

Also exposing a method that returns the observed capacity of the
LineChart the last time Draw was called.
  • Loading branch information
mum4k committed Feb 16, 2019
1 parent f465e11 commit aca7abf
Show file tree
Hide file tree
Showing 3 changed files with 422 additions and 39 deletions.
166 changes: 129 additions & 37 deletions widgets/linechart/linechart.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func newSeriesValues(values []float64) *seriesValues {
// Implements widgetapi.Widget. This object is thread-safe.
type LineChart struct {
// mu protects the LineChart widget.
mu sync.Mutex
mu sync.RWMutex

// series are the series that will be plotted.
// Keyed by the name of the series and updated by calling Series.
Expand All @@ -82,6 +82,10 @@ type LineChart struct {
// yMin are the min and max values for the Y axis.
yMin, yMax float64

// capacity is the last observed value capacity in pixels when Draw was
// called.
capacity int

// opts are the provided options.
opts *options

Expand Down Expand Up @@ -159,6 +163,20 @@ func (lc *LineChart) yMinMax() (float64, float64) {
return min, max
}

// ValueCapacity returns the number of values that could be fit onto the X axis
// without a need to rescale the X axis. This is essentially the number of
// available pixels on the braille canvas based on the width of the LineChart
// as observed on the last call to draw. Returns zero if draw wasn't called.
//
// Note that this capacity changes each time the terminal resizes, so there is
// no guarantee this remains the same next time Draw is called.
// Should be used as a hint only.
func (lc *LineChart) ValueCapacity() int {
lc.mu.RLock()
defer lc.mu.RUnlock()
return lc.capacity
}

// Series sets the values that should be displayed as the line chart with the
// provided label.
// Subsequent calls with the same label replace any previously provided values.
Expand Down Expand Up @@ -193,20 +211,47 @@ func (lc *LineChart) Series(label string, values []float64, opts ...SeriesOption
return nil
}

// Draw draws the values as line charts.
// Implements widgetapi.Widget.Draw.
func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
lc.mu.Lock()
defer lc.mu.Unlock()

needAr, err := area.FromSize(lc.minSize())
// xDetails returns the details for the X axis given the specified minimum and
// maximum value to display.
func (lc *LineChart) xDetails(cvs *canvas.Canvas, reqYWidth, min, max int) (*axes.XDetails, error) {
xp := &axes.XProperties{
Min: min,
Max: max,
ReqYWidth: reqYWidth,
CustomLabels: lc.xLabels,
LO: lc.opts.xLabelOrientation,
}
xd, err := axes.NewXDetails(cvs.Area(), xp)
if err != nil {
return err
return nil, fmt.Errorf("NewXDetails => %v", err)
}
if !needAr.In(cvs.Area()) {
return draw.ResizeNeeded(cvs)
return xd, nil
}

// xDetailsForCap adjusts the X details according to the capacity of the
// braille canvas (how many values can it fit).
// If the capacity cannot accommodate all the values, the starting value of the
// X axis is adjusted so that it displays the last n values that fit.
// Returns unadjusted xd if all the values fit.
func (lc *LineChart) xDetailsForCap(cvs *canvas.Canvas, bc *braille.Canvas, xd *axes.XDetails, yd *axes.YDetails) (*axes.XDetails, error) {
lc.capacity = bc.Area().Dx()
values := int(xd.Scale.Max.Value) - int(xd.Scale.Min.Value) + 1
if !lc.opts.xAxisUnscaled || values <= lc.capacity {
return xd, nil
}

diff := values - lc.capacity
xMin := int(xd.Scale.Min.Value) + diff
xMax := int(xd.Scale.Max.Value)
unscaledXD, err := lc.xDetails(cvs, yd.Start.X, xMin, xMax)
if err != nil {
return nil, err
}
return unscaledXD, nil
}

// axesDetails determines the details about the X and Y axes.
func (lc *LineChart) axesDetails(cvs *canvas.Canvas) (*axes.XDetails, *axes.YDetails, error) {
reqXHeight := axes.RequiredHeight(lc.maxXValue(), lc.xLabels, lc.opts.xLabelOrientation)
yp := &axes.YProperties{
Min: lc.yMin,
Expand All @@ -216,25 +261,43 @@ func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
}
yd, err := axes.NewYDetails(cvs.Area(), yp)
if err != nil {
return fmt.Errorf("lc.yAxis.Details => %v", err)
return nil, nil, fmt.Errorf("NewYDetails => %v", err)
}

xp := &axes.XProperties{
Min: 0,
Max: lc.maxXValue(),
ReqYWidth: yd.Start.X,
CustomLabels: lc.xLabels,
LO: lc.opts.xLabelOrientation,
// TODO: Zoom the X axis.
const xMin = 0
xMax := lc.maxXValue()
xd, err := lc.xDetails(cvs, yd.Start.X, xMin, xMax)
if err != nil {
return nil, nil, err
}
xd, err := axes.NewXDetails(cvs.Area(), xp)
return xd, yd, nil
}

// Draw draws the values as line charts.
// Implements widgetapi.Widget.Draw.
func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
lc.mu.Lock()
defer lc.mu.Unlock()

needAr, err := area.FromSize(lc.minSize())
if err != nil {
return fmt.Errorf("NewXDetails => %v", err)
return err
}
if !needAr.In(cvs.Area()) {
return draw.ResizeNeeded(cvs)
}

if err := lc.drawAxes(cvs, xd, yd); err != nil {
xd, yd, err := lc.axesDetails(cvs)
if err != nil {
return err
}
return lc.drawSeries(cvs, xd, yd)

adjXD, err := lc.drawSeries(cvs, xd, yd)
if err != nil {
return err
}
return lc.drawAxes(cvs, adjXD, yd)
}

// drawAxes draws the X,Y axes and their labels.
Expand Down Expand Up @@ -276,13 +339,30 @@ func (lc *LineChart) drawAxes(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YD
return nil
}

// drawSeries draws the graph representing the stored series.
func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) error {
// brailleCvs returns a braille canvas sized so that it fits between the axes
// and the canvas borders.
func (lc *LineChart) brailleCvs(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) (*braille.Canvas, error) {
// The area available to the graph.
graphAr := image.Rect(yd.Start.X+1, yd.Start.Y, cvs.Area().Max.X, xd.End.Y)
bc, err := braille.New(graphAr)
if err != nil {
return fmt.Errorf("braille.New => %v", err)
return nil, fmt.Errorf("braille.New => %v", err)
}
return bc, nil
}

// drawSeries draws the graph representing the stored series.
// Returns XDetails that might be adjusted to not start at zero value if some
// of the series didn't fit the graphs and XAxisUnscaled was provided.
func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) (*axes.XDetails, error) {
bc, err := lc.brailleCvs(cvs, xd, yd)
if err != nil {
return nil, err
}

xdForCap, err := lc.xDetailsForCap(cvs, bc, xd, yd)
if err != nil {
return nil, err
}

var names []string
Expand All @@ -293,45 +373,57 @@ func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.

for _, name := range names {
sv := lc.series[name]
if len(sv.values) <= 1 {
// Skip over series that don't have at least two points since we can't
// draw a line for just one point.
// Skip over series that fall under the minimum value on the X axis.
if got := len(sv.values); got <= 1 || got < int(xdForCap.Scale.Min.Value) {
continue
}

prev := sv.values[0]
var prev float64
for i := 1; i < len(sv.values); i++ {
startX, err := xd.Scale.ValueToPixel(i - 1)
prev = sv.values[i-1]
if i < int(xdForCap.Scale.Min.Value)+1 || i > int(xdForCap.Scale.Max.Value) {
// Don't draw lines for values that aren't supposed to be visible.
// These are either values outside of the current zoom or
// values at the beginning of a series that falls before athe
// start of an unscaled X axis when the XAxisUnscaled option is
// provided.
continue
}

startX, err := xdForCap.Scale.ValueToPixel(i - 1)
if err != nil {
return fmt.Errorf("failure for series %v[%d], xd.Scale.ValueToPixel => %v", name, i-1, err)
return nil, fmt.Errorf("failure for series %v[%d], xdForCap.Scale.ValueToPixel => %v", name, i-1, err)
}
endX, err := xd.Scale.ValueToPixel(i)
endX, err := xdForCap.Scale.ValueToPixel(i)
if err != nil {
return fmt.Errorf("failure for series %v[%d], xd.Scale.ValueToPixel => %v", name, i, err)
return nil, fmt.Errorf("failure for series %v[%d], xdForCap.Scale.ValueToPixel => %v", name, i, err)
}

startY, err := yd.Scale.ValueToPixel(prev)
if err != nil {
return fmt.Errorf("failure for series %v[%d], yd.Scale.ValueToPixel => %v", name, i-1, err)
return nil, fmt.Errorf("failure for series %v[%d], yd.Scale.ValueToPixel => %v", name, i-1, err)
}
v := sv.values[i]
endY, err := yd.Scale.ValueToPixel(v)
if err != nil {
return fmt.Errorf("failure for series %v[%d], yd.Scale.ValueToPixel => %v", name, i, err)
return nil, fmt.Errorf("failure for series %v[%d], yd.Scale.ValueToPixel => %v", name, i, err)
}

if err := draw.BrailleLine(bc,
image.Point{startX, startY},
image.Point{endX, endY},
draw.BrailleLineCellOpts(sv.seriesCellOpts...),
); err != nil {
return fmt.Errorf("draw.BrailleLine => %v", err)
return nil, fmt.Errorf("draw.BrailleLine => %v", err)
}
prev = v
}
}
if err := bc.CopyTo(cvs); err != nil {
return fmt.Errorf("bc.Apply => %v", err)
return nil, fmt.Errorf("bc.Apply => %v", err)
}
return nil
return xdForCap, nil
}

// Keyboard implements widgetapi.Widget.Keyboard.
Expand Down
Loading

0 comments on commit aca7abf

Please sign in to comment.