Skip to content

Commit

Permalink
Merge 587185e into 6ace35e
Browse files Browse the repository at this point in the history
  • Loading branch information
mum4k committed Feb 16, 2019
2 parents 6ace35e + 587185e commit 16d66d0
Show file tree
Hide file tree
Showing 10 changed files with 900 additions and 291 deletions.
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

0 comments on commit 16d66d0

Please sign in to comment.