forked from Elara6331/itd
Add metrics graphs to itgui
This commit is contained in:
150
cmd/itgui/graph.go
Normal file
150
cmd/itgui/graph.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"image/color"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/x/fyne/widget/charts"
|
||||
"go.arsenm.dev/itd/api"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func graphTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
// Get user configuration directory
|
||||
userCfgDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
cfgDir := filepath.Join(userCfgDir, "itd")
|
||||
dbPath := filepath.Join(cfgDir, "metrics.db")
|
||||
|
||||
// If stat on database returns error, return nil
|
||||
if _, err := os.Stat(dbPath); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open database
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get heart rate data and create chart
|
||||
heartRateData := getData(db, "bpm", "heartRate")
|
||||
heartRate := newLineChartData(nil, heartRateData)
|
||||
|
||||
// Get step count data and create chart
|
||||
stepCountData := getData(db, "steps", "stepCount")
|
||||
stepCount := newLineChartData(nil, stepCountData)
|
||||
|
||||
// Get battery level data and create chart
|
||||
battLevelData := getData(db, "percent", "battLevel")
|
||||
battLevel := newLineChartData(nil, battLevelData)
|
||||
|
||||
// Get motion data
|
||||
motionData := getMotionData(db)
|
||||
// Create chart for each coordinate
|
||||
xChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorRed), motionData["X"])
|
||||
yChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorGreen), motionData["Y"])
|
||||
zChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorBlue), motionData["Z"])
|
||||
|
||||
// Create new max container with all the charts
|
||||
motion := container.NewMax(xChart, yChart, zChart)
|
||||
|
||||
// Create tabs for charts
|
||||
chartTabs := container.NewAppTabs(
|
||||
container.NewTabItem("Heart Rate", heartRate),
|
||||
container.NewTabItem("Step Count", stepCount),
|
||||
container.NewTabItem("Battery Level", battLevel),
|
||||
container.NewTabItem("Motion", motion),
|
||||
)
|
||||
// Place tabs on left
|
||||
chartTabs.SetTabLocation(container.TabLocationLeading)
|
||||
return chartTabs
|
||||
}
|
||||
|
||||
func newLineChartData(col color.Color, data []float64) *charts.LineChart {
|
||||
// Create new line chart
|
||||
lc := charts.NewLineChart(nil)
|
||||
setOpts(lc, col)
|
||||
// If no data, make the stroke transparent
|
||||
if len(data) == 0 {
|
||||
lc.Options().StrokeColor = color.RGBA{0, 0, 0, 0}
|
||||
}
|
||||
// Set data
|
||||
lc.SetData(data)
|
||||
return lc
|
||||
}
|
||||
|
||||
func setOpts(lc *charts.LineChart, col color.Color) {
|
||||
// Get pointer to options
|
||||
opts := lc.Options()
|
||||
// Set fill color to transparent
|
||||
opts.FillColor = color.RGBA{0, 0, 0, 0}
|
||||
// Set stroke width
|
||||
opts.StrokeWidth = 2
|
||||
// If color provided
|
||||
if col != nil {
|
||||
// Set stroke color
|
||||
opts.StrokeColor = col
|
||||
} else {
|
||||
// Set stroke color to orange primary color
|
||||
opts.StrokeColor = theme.PrimaryColorNamed(theme.ColorOrange)
|
||||
}
|
||||
}
|
||||
|
||||
func getData(db *sql.DB, field, table string) []float64 {
|
||||
// Get data from database
|
||||
rows, err := db.Query("SELECT " + field + " FROM " + table + " ORDER BY time;")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []float64
|
||||
for rows.Next() {
|
||||
var val int64
|
||||
// Scan data into int
|
||||
err := rows.Scan(&val)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert to float64 and append to data slice
|
||||
out = append(out, float64(val))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func getMotionData(db *sql.DB) map[string][]float64 {
|
||||
// Get data from database
|
||||
rows, err := db.Query("SELECT X, Y, Z FROM motion ORDER BY time;")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := map[string][]float64{}
|
||||
for rows.Next() {
|
||||
var x, y, z int64
|
||||
// Scan data into ints
|
||||
err := rows.Scan(&x, &y, &z)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert to float64 and append to appropriate slice
|
||||
out["X"] = append(out["X"], float64(x))
|
||||
out["Y"] = append(out["Y"], float64(y))
|
||||
out["Z"] = append(out["Z"], float64(z))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
@@ -36,6 +36,11 @@ func main() {
|
||||
container.NewTabItem("Firmware", firmwareTab(ctx, client, w)),
|
||||
)
|
||||
|
||||
metricsTab := graphTab(ctx, client, w)
|
||||
if metricsTab != nil {
|
||||
tabs.Append(container.NewTabItem("Metrics", metricsTab))
|
||||
}
|
||||
|
||||
// When a tab is selected
|
||||
tabs.OnSelected = func(ti *container.TabItem) {
|
||||
// If the tab's name is FS
|
||||
|
Reference in New Issue
Block a user