Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LineChart now has an option that disables scaling of the X axis #129

Merged
merged 8 commits into from
Feb 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The LineChart widget can display X axis labels in vertical orientation.
- The LineChart widget allows the user to specify a custom scale for the Y
axis.
- The LineChart widget now has an option that disables scaling of the X axis.
Useful for applications that want to continuously feed data and make them
"roll" through the linechart.
- The LineChart widget now has a method that returns the observed capacity of
the LineChart the last time Draw was called.
- The Text widget now has a Write option that atomically replaces the entire
text content.

Expand Down
99 changes: 51 additions & 48 deletions widgets/linechart/axes/axes.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type YDetails struct {
Width int

// Start is the point where the Y axis starts.
// Both coordinates of Start are less than End.
// The Y coordinate of Start is less than the Y coordinate of End.
Start image.Point
// End is the point where the Y axis ends.
End image.Point
Expand All @@ -49,57 +49,43 @@ type YDetails struct {
Labels []*Label
}

// Y tracks the state of the Y axis throughout the lifetime of a line chart.
// Implements lazy resize of the axis to decrease visual "jumping".
// This object is not thread-safe.
type Y struct {
// min is the smallest value on the Y axis.
min *Value
// max is the largest value on the Y axis.
max *Value
// details about the Y axis as it will be drawn.
details *YDetails
}

// NewY returns a new Y instance.
// The minVal and maxVal represent the minimum and maximum value that will be
// displayed on the line chart among all of the series.
func NewY(minVal, maxVal float64) *Y {
y := &Y{}
y.Update(minVal, maxVal)
return y
}

// Update updates the stored minVal and maxVal.
func (y *Y) Update(minVal, maxVal float64) {
y.min, y.max = NewValue(minVal, nonZeroDecimals), NewValue(maxVal, nonZeroDecimals)
}

// RequiredWidth calculates the minimum width required in order to draw the Y
// axis and its labels.
func (y *Y) RequiredWidth() int {
// axis and its labels when displaying values that have this minimum and
// maximum among all the series.
func RequiredWidth(minVal, maxVal float64) int {
// This is an estimation only, it is possible that more labels in the
// middle will be generated and might be wider than this. Such cases are
// handled on the call to Details when the size of canvas is known.
return longestLabel([]*Label{
{Value: y.min},
{Value: y.max},
{Value: NewValue(minVal, nonZeroDecimals)},
{Value: NewValue(maxVal, nonZeroDecimals)},
}) + axisWidth
}

// Details retrieves details about the Y axis required to draw it on a canvas
// of the provided area.
// The argument reqXHeight is the height required for the X axis and its labels.
func (y *Y) Details(cvsAr image.Rectangle, reqXHeight int, mode YScaleMode) (*YDetails, error) {
// YProperties are the properties of the Y axis.
type YProperties struct {
// Min is the minimum value on the axis.
Min float64
// Max is the maximum value on the axis.
Max float64
// ReqXHeight is the height required for the X axis and its labels.
ReqXHeight int
// ScaleMode determines how the Y axis scales.
ScaleMode YScaleMode
}

// NewYDetails retrieves details about the Y axis required to draw it on a
// canvas of the provided area.
func NewYDetails(cvsAr image.Rectangle, yp *YProperties) (*YDetails, error) {
cvsWidth := cvsAr.Dx()
cvsHeight := cvsAr.Dy()
maxWidth := cvsWidth - 1 // Reserve one column for the line chart itself.
if req := y.RequiredWidth(); maxWidth < req {
if req := RequiredWidth(yp.Min, yp.Max); maxWidth < req {
return nil, fmt.Errorf("the available maxWidth %d is smaller than the reported required width %d", maxWidth, req)
}

graphHeight := cvsHeight - reqXHeight
scale, err := NewYScale(y.min.Value, y.max.Value, graphHeight, nonZeroDecimals, mode)
graphHeight := cvsHeight - yp.ReqXHeight
scale, err := NewYScale(yp.Min, yp.Max, graphHeight, nonZeroDecimals, yp.ScaleMode)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -163,57 +149,74 @@ type XDetails struct {
Labels []*Label
}

// XProperties are the properties of the X axis.
type XProperties struct {
// Min is the minimum value on the axis, i.e. the position of the first
// displayed value from the series.
Min int
// Max is the maximum value on the axis, i.e. the position of the last
// displayed value from the series.
Max int
// ReqYWidth is the width required for the Y axis and its labels.
ReqYWidth int
// CustomLabels are the desired labels for the X axis, these are preferred
// if provided.
CustomLabels map[int]string
// LO is the desired orientation of labels under the X axis.
LO LabelOrientation
}

// NewXDetails retrieves details about the X axis required to draw it on a canvas
// of the provided area. The yStart is the point where the Y axis starts.
// The numPoints is the number of points in the largest series that will be
// plotted.
// customLabels are the desired labels for the X axis, these are preferred if
// provided.
func NewXDetails(numPoints int, yStart image.Point, cvsAr image.Rectangle, customLabels map[int]string, lo LabelOrientation) (*XDetails, error) {
func NewXDetails(cvsAr image.Rectangle, xp *XProperties) (*XDetails, error) {
cvsHeight := cvsAr.Dy()
maxHeight := cvsHeight - 1 // Reserve one row for the line chart itself.
reqHeight := RequiredHeight(numPoints, customLabels, lo)
reqHeight := RequiredHeight(xp.Max, xp.CustomLabels, xp.LO)
if maxHeight < reqHeight {
return nil, fmt.Errorf("the available maxHeight %d is smaller than the reported required height %d", maxHeight, reqHeight)
}

// The space between the start of the axis and the end of the canvas.
graphWidth := cvsAr.Dx() - yStart.X - 1
scale, err := NewXScale(numPoints, graphWidth, nonZeroDecimals)
graphWidth := cvsAr.Dx() - xp.ReqYWidth - 1
scale, err := NewXScale(xp.Min, xp.Max, graphWidth, nonZeroDecimals)
if err != nil {
return nil, err
}

// See how the labels would look like on the entire reqHeight.
graphZero := image.Point{
// Reserve one point horizontally for the Y axis.
yStart.X + 1,
xp.ReqYWidth + 1,
cvsAr.Dy() - reqHeight - 1,
}
labels, err := xLabels(scale, graphZero, customLabels, lo)
labels, err := xLabels(scale, graphZero, xp.CustomLabels, xp.LO)
if err != nil {
return nil, err
}

return &XDetails{
Start: image.Point{yStart.X, cvsAr.Dy() - reqHeight}, // Space for the labels.
End: image.Point{yStart.X + graphWidth, cvsAr.Dy() - reqHeight},
Start: image.Point{xp.ReqYWidth, cvsAr.Dy() - reqHeight}, // Space for the labels.
End: image.Point{xp.ReqYWidth + graphWidth, cvsAr.Dy() - reqHeight},
Scale: scale,
Labels: labels,
}, nil
}

// RequiredHeight calculates the minimum height required in order to draw the X
// axis and its labels.
func RequiredHeight(numPoints int, customLabels map[int]string, lo LabelOrientation) int {
func RequiredHeight(max int, customLabels map[int]string, lo LabelOrientation) int {
if lo == LabelOrientationHorizontal {
// One row for the X axis and one row for its labels flowing
// horizontally.
return axisWidth + 1
}

labels := []*Label{
{Value: NewValue(float64(numPoints), nonZeroDecimals)},
{Value: NewValue(float64(max), nonZeroDecimals)},
}
for _, cl := range customLabels {
labels = append(labels, &Label{
Expand Down
Loading