LineChart written to display periodic data samples on a line chart, with the ability to display the point value when hovered over with pointer. There is an unlimited number of series which can be displayed. However, the current maximum number of points per series that can be shown on screen is 150 xpoints. Accumulated point counts greater 150 simply roll off the end.
- Added yScaleFactor param to NewLineChart() which control the max yScale value and Labels
-
- 14 divisions on yScale including 0. So 50 * 13 would give 650 and the max yValue on scale.
- Multiple Series of data points rendered as a individual line
- Series should be the same color. Each point in this chart accepts a themed color name
- 150 datapoint are displayed on the x scale of chart, with 100 as the default Y value.
- More than 150 data points causes the earliest points to be rolled off the screen; each series independently scrolls when limit is reached
- Data points can be added at any time, causing the series to possible scroll automatically
- The 150 x limit will throw and error on creation of the chart, or on the replacement of its active series.
- Data point markers are toggled with mouse button 2
- Hovering over a data point will show a popup near the mouse pointer, showing series, value, index, and timestamp of data under mouse
- Mouse button 1 will toggle the sticky hover popup
- Labels are available for all four corners of window, include bottom and top centered titles
- left and right middle labels can be used as scale descriptions
- Any label left empty will not be displayed.
- Horizontal and Vertical chart grid lines can also be turned off/on
- There is a callback available which fires when a point if hovered over; passing the full datapoint and series name.
- A
GraphPointSmoothing
interface is available to enable preprocessing of datapoints with a range of possible techniques, averaging was implemented as an example. Purple vs Yellow lines on the above chart illustrate the smoothing effect.
package sknlinechart
import "fyne.io/fyne/v2"
// GraphPointSmoothing support for different implementation
// of averaging or smooth data; current provides rolling average from last x reading.
type GraphPointSmoothing interface {
AddValue(value float64) float64
SeriesName() string
IsNil() bool
String() string
}
// ChartDatapoint data container interface for LineChart
type ChartDatapoint interface {
Value() float32
SetValue(y float32)
ColorName() string
SetColorName(n string)
Timestamp() string
SetTimestamp(t string)
// ExternalID string uuid assigned when created
ExternalID() string
// Copy returns a cloned copy of current item
Copy() ChartDatapoint
// MarkerPosition internal use only: current data point marker location
MarkerPosition() (*fyne.Position, *fyne.Position)
// SetMarkerPosition internal use only: screen location of where data point marker is located
SetMarkerPosition(top *fyne.Position, bottom *fyne.Position)
}
// LineChart feature list
type LineChart interface {
// Chart Attributes
GetLineStrokeSize() float32
SetLineStrokeSize(newSize float32)
IsDataPointMarkersEnabled() bool // mouse button 2 toggles
IsHorizGridLinesEnabled() bool
IsVertGridLinesEnabled() bool
IsColorLegendEnabled() bool
IsMousePointDisplayEnabled() bool // hoverable and mouse button one
SetDataPointMarkers(enable bool)
SetHorizGridLines(enable bool)
SetVertGridLines(enable bool)
SetColorLegend(enable bool)
SetMousePointDisplay(enable bool)
// Scale legend
GetMiddleLeftLabel() string
GetMiddleRightLabel() string
// Info Labels
GetTopLeftLabel() string
GetTitle() string
GetTopRightLabel() string
GetBottomLeftLabel() string
GetBottomCenteredLabel() string
GetBottomRightLabel() string
SetTopLeftLabel(newValue string)
SetTitle(newValue string)
SetTopRightLabel(newValue string)
SetMiddleLeftLabel(newValue string)
SetMiddleRightLabel(newValue string)
SetBottomLeftLabel(newValue string)
SetBottomCenteredLabel(newValue string)
SetBottomRightLabel(newValue string)
// ApplyDataSeries add a whole data series at once
// expect this will rarely be used, since loading more than 130 point will raise error
ApplyDataSeries(seriesName string, newSeries []*ChartDatapoint) error
// ApplyDataPoint primary method to add another data point to any series
// If series has more than 130 points, point 0 will be rolled out making room for this one
ApplyDataPoint(seriesName string, newDataPoint *ChartDatapoint)
// SetMinSize set the minimum size limit for the linechart
SetMinSize(s fyne.Size)
// EnableDebugLogging turns method entry/exit logging on or off
EnableDebugLogging(enable bool)
// SetHoverPointCallback method to call when a onscreen datapoint is hovered over by pointer
SetOnHoverPointCallback(func(series string, dataPoint ChartDatapoint))
// ObjectCount internal use only: return the default ui elements for testing
ObjectCount() int
// fyne.CanvasObject compliance
// implemented by BaseWidget
Hide()
MinSize() fyne.Size
Move(position fyne.Position)
Position() fyne.Position
Refresh()
Resize(size fyne.Size)
Show()
Size() fyne.Size
Visible() bool
}
ChartOptions as an alternate way to specify chart features
/*
WithDataPoints(seriesData map[string][]*ChartDatapoint) ChartOption
WithOnHoverPointCallback(callBack func(series string, dataPoint ChartDatapoint)) ChartOption
WithDebugLogging(enable bool) ChartOption
WithColorLegend(enable bool) ChartOption
WithMousePointDisplay(enable bool) ChartOption
WithVertGridLines(enable bool) ChartOption
WithHorizGridLines(enable bool) ChartOption
WithDataPointMarkers(enable bool) ChartOption
WithMinSize(width, height float32) ChartOption
WithYScaleFactor(maxYScaleLabel int) ChartOption
WithRightScaleLabel(label string) ChartOption
WithLeftScaleLabel(label string) ChartOption
WithBottomRightLabel(label string) ChartOption
WithBottomLeftLabel(label string) ChartOption
WithTopRightLabel(label string) ChartOption
WithTopLeftLabel(label string) ChartOption
WithFooter(label string) ChartOption
WithTitle(label string) ChartOption
NewChartOptions(opts ...ChartOption) *ChartOptions
func (o *ChartOptions) Add(opt ChartOption)
NewWithOptions(options *ChartOptions) (LineChart, error)
NewLineChartViaOptions(options *ChartOptions) (LineChart, error)
*/
// * Start by creating a ChartOptions container
// * then add individual ChartOption as needed.
// * Finish by passing container to New function.
options := NewChartOptions()
options.Add(WithTitle("MyTitle"))
lc, err := NewWithOptions(options)
/*
* SknLineChart
* Custom Fyne 2.0 Widget
* Strategy
* 1. Define Widget Named/Exported Struct
* 1. export fields when possible
* 2. Define Widget Renderer Named/unExported Struct
* 1. un-exportable fields when possible
* 3. Define NewWidget() *ExportedStruct method, related interface should have different name.
* 1. Define state variables for this widget
* 2. Extend the BaseWidget
* 1. If coding SetMinSize(fyne.Size), use saved value in MinSize() in rendered
* 3. Define Widget required methods
* 1. CreateRenderer() fyne.WidgetRenderer, call newRenderer() below
* 2. Renderer has the other required methods, like Refresh(), etc.
* 4. Define any methods required by additional interfaces, like
* desktop.Mouseable for mouse button support
* 1. MouseDown(me MouseEvent)
* 2. MouseUp(me MouseEvent)
* desktop.Hoverable for mouse movement support
* 1. MouseIn(me MouseEvent)
* 2. MouseMoved(me MouseEvent) used to display data point under mouse
* 3. MouseOut()
* 4. Define newRenderer() *notExportedStruct method
* 1. Create canvas objects to be used in display
* 2. Initialize their content if practical; not required
* 3. Implement the required WidgetRenderer methods
* 4. Refresh() reload/update value if changed, call refresh on each object
* 5. Layout(s fyne.Size) resize & move objects
* 6. MinSize() fyne.Size return the minimum size needed
* 7. Object() []fyne.Canvas return the objects to be displayed
* 8. Destroy() cleanup if needed to prevent leaks
* 5. In general widget state methods are the public api with or without getters/setters
* and the renderer creates the displayable objects, applies state/value to them, and
* manages their display.
*
* Critical Notes:
* - if using maps, map[string]interface{}, they will require a mutex to prevent concurrency errors caused my concurrent read/writes.
*/
with chart options
package main
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
lc "github.com/skoona/sknlinechart"
"log"
"math/rand"
"os"
"os/signal"
"syscall"
"time"
)
func makeChart(title, footer string) (lc.LineChart, error) {
dataPoints := map[string][]*lc.ChartDatapoint{} // legend, points
rand.NewSource(1000.0)
for x := 1; x < 151; x++ {
val := rand.Float32() * 75.0
if val > 75.0 {
val = 75.0
} else if val < 30.0 {
val = 30.0
}
point := lc.NewChartDatapoint(val, theme.ColorBlue, time.Now().Format(time.RFC1123))
dataPoints["Humidity"] = append(dataPoints["Humidity"], &point)
}
for x := 1; x < 151; x++ {
val := rand.Float32() * 75.0
if val > 95.0 {
val = 95.0
} else if val < 55.0 {
val = 55.0
}
point := lc.NewChartDatapoint(val, theme.ColorRed, time.Now().Format(time.RFC1123))
dataPoints["Temperature"] = append(dataPoints["Temperature"], &point)
}
opts := lc.NewChartOptions()
opts.Add(lc.WithDebugLogging(true))
opts.Add(lc.WithFooter("With Options"))
opts.Add(lc.WithTitle(title))
opts.Add(lc.WithLeftScaleLabel("Temperature"))
opts.Add(lc.WithRightScaleLabel("Humidity"))
opts.Add(lc.WithDataPoints(dataPoints))
opts.Add(lc.WithYScaleFactor(55))
opts.Add(lc.WithOnHoverPointCallback(func(series string, p lc.ChartDatapoint) {
fmt.Printf("Chart Datapoint Selected Callback: series:%s, point: %v\n", series, p)
}))
lineChart, err := lc.NewWithOptions(opts)
if err != nil {
fmt.Println(err.Error())
}
return lineChart, err
}
or with classic setters
package main
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
lc "github.com/skoona/sknlinechart"
"log"
"math/rand"
"os"
"os/signal"
"syscall"
"time"
)
func makeChart(title, footer string) (lc.LineChart, error) {
dataPoints := map[string][]*lc.ChartDatapoint{} // legend, points
rand.NewSource(1000.0)
for x := 1; x < 151; x++ {
val := rand.Float32() * 75.0
if val > 75.0 {
val = 75.0
} else if val < 30.0 {
val = 30.0
}
point := lc.NewChartDatapoint(val, theme.ColorBlue, time.Now().Format(time.RFC1123))
dataPoints["Humidity"] = append(dataPoints["Humidity"], &point)
}
for x := 1; x < 151; x++ {
val := rand.Float32() * 75.0
if val > 95.0 {
val = 95.0
} else if val < 55.0 {
val = 55.0
}
point := lc.NewChartDatapoint(val, theme.ColorRed, time.Now().Format(time.RFC1123))
dataPoints["Temperature"] = append(dataPoints["Temperature"], &point)
}
// yScalefactor represents the value of each of the 13 Y divisions
// Ex: 13 * 50 = 650, also 13 * 10 = 130, 50 and 10 being the yScalefactor value
// 650 and 130 would be the top of the Y scale as there are 13 vertical divisions not including zero
lineChart, err := lc.New(title, footer, 55, &dataPoints)
if err != nil {
fmt.Println(err.Error())
if lineChart == nil {
panic(err.Error())
}
}
lineChart.SetLineStrokeSize(2.0)
lineChart.EnableDebugLogging(true)
lineChart.SetTopLeftLabel("top left")
lineChart.SetMiddleLeftLabel("Temperature")
lineChart.SetMiddleRightLabel("Humidity")
lineChart.SetBottomLeftLabel("bottom left")
lineChart.SetBottomRightLabel("bottom right")
lineChart.SetOnHoverPointCallback(func(series string, p lc.ChartDatapoint) {
logger.Printf("Chart Datapoint Selected Callback: series:%s, point: %v\n", series, p)
})
return lineChart, err
}
func main() {
systemSignalChannel := make(chan os.Signal, 1)
exitCode := 0
windowClosed := false
logger := log.New(os.Stdout, "[DEBUG] ", log.Lmicroseconds|log.Lshortfile)
gui := app.NewWithID("net.skoona.sknLineChart")
w := gui.NewWindow("Custom Widget Development")
lineChart, err := makeChart("Skoona Line Chart", "Example Time Series")
go (func(chart lc.LineChart) {
var many []*lc.ChartDatapoint
for x := 1; x < 161; x++ {
val := rand.Float32() * 25.0
if val > 50.0 {
val = 50.0
} else if val < 5.0 {
val = 5.0
}
point := lc.NewChartDatapoint(val, theme.ColorPurple, time.Now().Format(time.RFC1123))
many = append(many, &point)
}
time.Sleep(10 * time.Second)
err = lineChart.ApplyDataSeries("AllAtOnce", many)
if err != nil {
logger.Println("ApplyDataSeries", err.Error())
}
time.Sleep(time.Second)
smoothed := lc.NewGraphAverage("SmoothStream", 32)
for i := 0; i < 151; i++ {
if windowClosed {
break
}
dVal := float64(rand.Float32() * 110.0)
smoother := smoothed.AddValue(dVal)
point := lc.NewChartDatapoint(float32(smoother), theme.ColorYellow, time.Now().Format(time.RFC1123))
chart.ApplyDataPoint("SmoothStream", &point)
point2 := lc.NewChartDatapoint(float32(dVal), theme.ColorPurple, time.Now().Format(time.RFC1123))
chart.ApplyDataPoint("SteadyStream", &point2)
if windowClosed {
break
}
time.Sleep(time.Second)
}
})(lineChart)
lineChart.SetOnHoverPointCallback(func(series string, p lc.ChartDatapoint) {
logger.Printf("Chart Datapoint Selected Callback: series:%s, point: %v\n", series, p)
})
w.SetContent(container.NewPadded(lineChart))
w.Resize(fyne.NewSize(982, 452))
go func(w *fyne.Window, stopFlag chan os.Signal) {
signal.Notify(stopFlag, syscall.SIGINT, syscall.SIGTERM)
sig := <-stopFlag // wait on ctrl-c
windowClosed = true
logger.Println("Signal Received: ", sig.String())
exitCode = 1
(*w).Close()
}(&w, systemSignalChannel)
w.ShowAndRun()
os.Exit(exitCode)
}
├── LICENSE
├── README.md
├── cmd
│ └── sknlinechart
│ └── main.go
├── go.mod
├── go.sum
├── datapoint.go
├── linechartinterfaces.go
├── linechart.go
└── mapsliceutils.go
- Clone this repo in your GO src directory
- Install Go
- Install Fyne
- Install Ginkgo
- possibly update your
../go.work
file to include this module go mod tidy
go run com/sknlinechart/main.go
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request to
development
branch
The application is available as open source under the terms of the MIT License.