2022-12-12 21:36:06 +11:00
|
|
|
package tvxwidgets
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"image"
|
2024-10-11 00:32:08 +02:00
|
|
|
"math"
|
2024-03-05 21:56:13 +11:00
|
|
|
"strconv"
|
2024-10-12 23:54:29 +02:00
|
|
|
"strings"
|
2022-12-12 21:36:06 +11:00
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
|
|
"github.com/rivo/tview"
|
|
|
|
)
|
|
|
|
|
2023-12-14 23:05:23 -07:00
|
|
|
// Marker represents plot drawing marker (braille or dot).
|
2022-12-12 21:36:06 +11:00
|
|
|
type Marker uint
|
|
|
|
|
|
|
|
const (
|
|
|
|
// plot marker.
|
|
|
|
PlotMarkerBraille Marker = iota
|
|
|
|
PlotMarkerDot
|
|
|
|
)
|
|
|
|
|
2024-09-28 12:33:20 +10:00
|
|
|
// PlotYAxisLabelDataType represents plot y axis type (integer or float).
|
|
|
|
type PlotYAxisLabelDataType uint
|
|
|
|
|
|
|
|
const (
|
|
|
|
PlotYAxisLabelDataInt PlotYAxisLabelDataType = iota
|
|
|
|
PlotYAxisLabelDataFloat
|
|
|
|
)
|
|
|
|
|
2022-12-12 21:36:06 +11:00
|
|
|
// PlotType represents plot type (line chart or scatter).
|
|
|
|
type PlotType uint
|
|
|
|
|
|
|
|
const (
|
|
|
|
PlotTypeLineChart PlotType = iota
|
|
|
|
PlotTypeScatter
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
plotHorizontalScale = 1
|
|
|
|
plotXAxisLabelsHeight = 1
|
|
|
|
plotXAxisLabelsGap = 2
|
|
|
|
plotYAxisLabelsGap = 1
|
2024-10-12 23:54:29 +02:00
|
|
|
|
|
|
|
gapRune = " "
|
2022-12-12 21:36:06 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
type brailleCell struct {
|
|
|
|
cRune rune
|
|
|
|
color tcell.Color
|
|
|
|
}
|
|
|
|
|
|
|
|
// Plot represents a plot primitive used for different charts.
|
|
|
|
type Plot struct {
|
|
|
|
*tview.Box
|
2024-10-11 03:37:08 +02:00
|
|
|
data [][]float64
|
|
|
|
// maxVal is the maximum y-axis (vertical) value found in any of the lines in the data set.
|
|
|
|
maxVal float64
|
|
|
|
// minVal is the minimum y-axis (vertical) value found in any of the lines in the data set.
|
|
|
|
minVal float64
|
2024-09-28 12:33:20 +10:00
|
|
|
marker Marker
|
|
|
|
ptype PlotType
|
|
|
|
dotMarkerRune rune
|
|
|
|
lineColors []tcell.Color
|
|
|
|
axesColor tcell.Color
|
|
|
|
axesLabelColor tcell.Color
|
|
|
|
drawAxes bool
|
|
|
|
drawXAxisLabel bool
|
2024-10-12 19:32:49 +02:00
|
|
|
xAxisLabelFunc func(int) string
|
2024-09-28 12:33:20 +10:00
|
|
|
drawYAxisLabel bool
|
|
|
|
yAxisLabelDataType PlotYAxisLabelDataType
|
2024-10-12 15:03:43 +02:00
|
|
|
yAxisAutoScaleMin bool
|
|
|
|
yAxisAutoScaleMax bool
|
2024-09-28 12:33:20 +10:00
|
|
|
brailleCellMap map[image.Point]brailleCell
|
|
|
|
mu sync.Mutex
|
2022-12-12 21:36:06 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewPlot returns a plot widget.
|
|
|
|
func NewPlot() *Plot {
|
|
|
|
return &Plot{
|
2024-09-28 12:33:20 +10:00
|
|
|
Box: tview.NewBox(),
|
|
|
|
marker: PlotMarkerDot,
|
|
|
|
ptype: PlotTypeLineChart,
|
|
|
|
dotMarkerRune: dotRune,
|
|
|
|
axesColor: tcell.ColorDimGray,
|
|
|
|
axesLabelColor: tcell.ColorDimGray,
|
|
|
|
drawAxes: true,
|
|
|
|
drawXAxisLabel: true,
|
2024-10-12 19:32:49 +02:00
|
|
|
xAxisLabelFunc: func(i int) string { return strconv.Itoa(i) },
|
2024-09-28 12:33:20 +10:00
|
|
|
drawYAxisLabel: true,
|
|
|
|
yAxisLabelDataType: PlotYAxisLabelDataFloat,
|
2024-10-12 15:03:43 +02:00
|
|
|
yAxisAutoScaleMin: false,
|
|
|
|
yAxisAutoScaleMax: true,
|
2022-12-12 21:36:06 +11:00
|
|
|
lineColors: []tcell.Color{
|
|
|
|
tcell.ColorSteelBlue,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw draws this primitive onto the screen.
|
|
|
|
func (plot *Plot) Draw(screen tcell.Screen) {
|
|
|
|
plot.Box.DrawForSubclass(screen, plot)
|
|
|
|
|
|
|
|
switch plot.marker {
|
|
|
|
case PlotMarkerDot:
|
2022-12-22 14:36:57 -06:00
|
|
|
plot.drawDotMarkerToScreen(screen)
|
2022-12-12 21:36:06 +11:00
|
|
|
case PlotMarkerBraille:
|
|
|
|
plot.drawBrailleMarkerToScreen(screen)
|
|
|
|
}
|
|
|
|
|
|
|
|
plot.drawAxesToScreen(screen)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetRect sets rect for this primitive.
|
|
|
|
func (plot *Plot) SetRect(x, y, width, height int) {
|
|
|
|
plot.Box.SetRect(x, y, width, height)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetLineColor sets chart line color.
|
|
|
|
func (plot *Plot) SetLineColor(color []tcell.Color) {
|
|
|
|
plot.lineColors = color
|
|
|
|
}
|
|
|
|
|
2024-09-28 12:33:20 +10:00
|
|
|
// SetYAxisLabelDataType sets Y axis label data type (integer or float).
|
|
|
|
func (plot *Plot) SetYAxisLabelDataType(dataType PlotYAxisLabelDataType) {
|
|
|
|
plot.yAxisLabelDataType = dataType
|
|
|
|
}
|
|
|
|
|
2024-10-13 12:44:01 +11:00
|
|
|
// SetYAxisAutoScaleMin enables YAxis min value autoscale.
|
2024-10-12 14:54:53 +02:00
|
|
|
func (plot *Plot) SetYAxisAutoScaleMin(autoScale bool) {
|
2024-10-12 15:03:43 +02:00
|
|
|
plot.yAxisAutoScaleMin = autoScale
|
2024-10-12 14:54:53 +02:00
|
|
|
}
|
|
|
|
|
2024-10-13 12:44:01 +11:00
|
|
|
// SetYAxisAutoScaleMax enables YAxix max value autoscale.
|
2024-10-12 14:54:53 +02:00
|
|
|
func (plot *Plot) SetYAxisAutoScaleMax(autoScale bool) {
|
2024-10-12 15:03:43 +02:00
|
|
|
plot.yAxisAutoScaleMax = autoScale
|
2024-10-12 14:54:53 +02:00
|
|
|
}
|
|
|
|
|
2022-12-12 21:36:06 +11:00
|
|
|
// SetAxesColor sets axes x and y lines color.
|
|
|
|
func (plot *Plot) SetAxesColor(color tcell.Color) {
|
|
|
|
plot.axesColor = color
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetAxesLabelColor sets axes x and y label color.
|
|
|
|
func (plot *Plot) SetAxesLabelColor(color tcell.Color) {
|
|
|
|
plot.axesLabelColor = color
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetDrawAxes set true in order to draw axes to screen.
|
|
|
|
func (plot *Plot) SetDrawAxes(draw bool) {
|
|
|
|
plot.drawAxes = draw
|
|
|
|
}
|
|
|
|
|
2023-12-29 14:51:02 +07:00
|
|
|
// SetDrawXAxisLabel set true in order to draw x axis label to screen.
|
|
|
|
func (plot *Plot) SetDrawXAxisLabel(draw bool) {
|
|
|
|
plot.drawXAxisLabel = draw
|
|
|
|
}
|
|
|
|
|
2024-10-12 19:32:49 +02:00
|
|
|
// SetXAxisLabelFunc sets x axis label function.
|
|
|
|
func (plot *Plot) SetXAxisLabelFunc(f func(int) string) {
|
|
|
|
plot.xAxisLabelFunc = f
|
|
|
|
}
|
|
|
|
|
2023-12-29 14:51:02 +07:00
|
|
|
// SetDrawYAxisLabel set true in order to draw y axis label to screen.
|
|
|
|
func (plot *Plot) SetDrawYAxisLabel(draw bool) {
|
|
|
|
plot.drawYAxisLabel = draw
|
|
|
|
}
|
|
|
|
|
2022-12-12 21:36:06 +11:00
|
|
|
// SetMarker sets marker type braille or dot mode.
|
|
|
|
func (plot *Plot) SetMarker(marker Marker) {
|
|
|
|
plot.marker = marker
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPlotType sets plot type (linechart or scatter).
|
|
|
|
func (plot *Plot) SetPlotType(ptype PlotType) {
|
|
|
|
plot.ptype = ptype
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetData sets plot data.
|
|
|
|
func (plot *Plot) SetData(data [][]float64) {
|
|
|
|
plot.mu.Lock()
|
|
|
|
defer plot.mu.Unlock()
|
|
|
|
|
|
|
|
plot.brailleCellMap = make(map[image.Point]brailleCell)
|
|
|
|
plot.data = data
|
2024-10-13 12:44:01 +11:00
|
|
|
|
2024-10-12 15:03:43 +02:00
|
|
|
if plot.yAxisAutoScaleMax {
|
2024-10-12 14:54:53 +02:00
|
|
|
plot.maxVal = getMaxFloat64From2dSlice(data)
|
|
|
|
}
|
2024-10-13 12:44:01 +11:00
|
|
|
|
2024-10-12 15:03:43 +02:00
|
|
|
if plot.yAxisAutoScaleMin {
|
2024-10-12 14:54:53 +02:00
|
|
|
plot.minVal = getMinFloat64From2dSlice(data)
|
|
|
|
}
|
2024-10-11 03:37:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (plot *Plot) SetMaxVal(maxVal float64) {
|
|
|
|
plot.maxVal = maxVal
|
|
|
|
}
|
|
|
|
|
|
|
|
func (plot *Plot) SetMinVal(minVal float64) {
|
|
|
|
plot.minVal = minVal
|
2022-12-12 21:36:06 +11:00
|
|
|
}
|
|
|
|
|
2024-10-12 17:35:45 +02:00
|
|
|
func (plot *Plot) SetYRange(minVal float64, maxVal float64) {
|
|
|
|
plot.minVal = minVal
|
|
|
|
plot.maxVal = maxVal
|
|
|
|
}
|
|
|
|
|
2022-12-12 21:36:06 +11:00
|
|
|
// SetDotMarkerRune sets dot marker rune.
|
|
|
|
func (plot *Plot) SetDotMarkerRune(r rune) {
|
|
|
|
plot.dotMarkerRune = r
|
|
|
|
}
|
|
|
|
|
2023-12-14 23:05:23 -07:00
|
|
|
// Figure out the text width necessary to display the largest data value.
|
|
|
|
func (plot *Plot) getYAxisLabelsWidth() int {
|
|
|
|
return len(fmt.Sprintf("%.2f", plot.maxVal))
|
|
|
|
}
|
|
|
|
|
2024-05-07 12:23:28 -06:00
|
|
|
// GetPlotRect returns the rect for the inner part of the plot, ie not including axes.
|
|
|
|
func (plot *Plot) GetPlotRect() (int, int, int, int) {
|
2022-12-12 21:36:06 +11:00
|
|
|
x, y, width, height := plot.Box.GetInnerRect()
|
2023-12-14 23:05:23 -07:00
|
|
|
plotYAxisLabelsWidth := plot.getYAxisLabelsWidth()
|
2022-12-12 21:36:06 +11:00
|
|
|
|
|
|
|
if plot.drawAxes {
|
|
|
|
x = x + plotYAxisLabelsWidth + 1
|
|
|
|
width = width - plotYAxisLabelsWidth - 1
|
|
|
|
height = height - plotXAxisLabelsHeight - 1
|
|
|
|
} else {
|
|
|
|
x++
|
|
|
|
width--
|
|
|
|
}
|
|
|
|
|
|
|
|
return x, y, width, height
|
|
|
|
}
|
|
|
|
|
|
|
|
func (plot *Plot) getData() [][]float64 {
|
|
|
|
plot.mu.Lock()
|
|
|
|
data := plot.data
|
|
|
|
plot.mu.Unlock()
|
|
|
|
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
func (plot *Plot) drawAxesToScreen(screen tcell.Screen) {
|
|
|
|
if !plot.drawAxes {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
x, y, width, height := plot.Box.GetInnerRect()
|
2023-12-14 23:05:23 -07:00
|
|
|
plotYAxisLabelsWidth := plot.getYAxisLabelsWidth()
|
2022-12-12 21:36:06 +11:00
|
|
|
|
|
|
|
axesStyle := tcell.StyleDefault.Background(plot.GetBackgroundColor()).Foreground(plot.axesColor)
|
|
|
|
|
2022-12-24 12:21:58 +11:00
|
|
|
// draw Y axis line
|
2022-12-12 21:36:06 +11:00
|
|
|
drawLine(screen,
|
|
|
|
x+plotYAxisLabelsWidth,
|
|
|
|
y,
|
|
|
|
height-plotXAxisLabelsHeight-1,
|
|
|
|
verticalLine, axesStyle)
|
|
|
|
|
|
|
|
// draw X axis line
|
|
|
|
drawLine(screen,
|
|
|
|
x+plotYAxisLabelsWidth+1,
|
|
|
|
y+height-plotXAxisLabelsHeight-1,
|
|
|
|
width-plotYAxisLabelsWidth-1,
|
|
|
|
horizontalLine, axesStyle)
|
|
|
|
|
|
|
|
tview.PrintJoinedSemigraphics(screen,
|
|
|
|
x+plotYAxisLabelsWidth,
|
|
|
|
y+height-plotXAxisLabelsHeight-1,
|
|
|
|
tview.BoxDrawingsLightUpAndRight, axesStyle)
|
|
|
|
|
2023-12-29 14:51:02 +07:00
|
|
|
if plot.drawXAxisLabel {
|
|
|
|
plot.drawXAxisLabelToScreen(screen, plotYAxisLabelsWidth, x, y, width, height)
|
|
|
|
}
|
|
|
|
|
|
|
|
if plot.drawYAxisLabel {
|
|
|
|
plot.drawYAxisLabelToScreen(screen, plotYAxisLabelsWidth, x, y, height)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (plot *Plot) drawXAxisLabelToScreen(
|
|
|
|
screen tcell.Screen, plotYAxisLabelsWidth int, x int, y int, width int, height int,
|
|
|
|
) {
|
2024-10-12 23:54:29 +02:00
|
|
|
globalStartX := x + plotYAxisLabelsWidth + 1
|
|
|
|
globalEndX := x + width
|
|
|
|
globalAvailableWidth := globalEndX - globalStartX
|
|
|
|
|
|
|
|
labelMap := map[int]string{}
|
|
|
|
labelStartMap := map[int]int{}
|
|
|
|
|
|
|
|
maxDataPoints := 0
|
|
|
|
for _, d := range plot.data {
|
|
|
|
maxDataPoints = max(maxDataPoints, len(d))
|
|
|
|
}
|
|
|
|
|
|
|
|
// determine the width needed for the largest label
|
|
|
|
maxXAxisLabelWidth := 0
|
|
|
|
for _, d := range plot.data {
|
|
|
|
for i, _ := range d {
|
|
|
|
label := plot.xAxisLabelFunc(i)
|
|
|
|
labelMap[i] = label
|
|
|
|
maxXAxisLabelWidth = max(maxXAxisLabelWidth, len(label))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// determine the start position for each label, if they were
|
|
|
|
// to be centered below the data point.
|
|
|
|
// Note: not all of these labels will be printed, as they would
|
|
|
|
// overlap with each other
|
|
|
|
for i, label := range labelMap {
|
|
|
|
expectedLabelWidth := len(label)
|
|
|
|
if i == 0 {
|
|
|
|
expectedLabelWidth += plotXAxisLabelsGap / 2
|
|
|
|
} else {
|
|
|
|
expectedLabelWidth += plotXAxisLabelsGap
|
2024-10-12 19:55:07 +02:00
|
|
|
}
|
2024-10-12 23:54:29 +02:00
|
|
|
currentLabelStart := i - int(math.Round(float64(expectedLabelWidth)/2))
|
|
|
|
labelStartMap[i] = currentLabelStart
|
|
|
|
}
|
2024-10-12 19:55:07 +02:00
|
|
|
|
2024-10-12 23:54:29 +02:00
|
|
|
// print the labels, skipping those that would overlap,
|
|
|
|
// stopping when there is no more space
|
|
|
|
lastUsedLabelEnd := math.MinInt
|
|
|
|
isFirstLabel := true
|
|
|
|
initialOffset := globalStartX
|
|
|
|
for i := 0; i < maxDataPoints; i++ {
|
|
|
|
rawLabel := labelMap[i]
|
|
|
|
labelWithGap := rawLabel
|
|
|
|
if i == 0 {
|
|
|
|
labelWithGap += strings.Repeat(gapRune, plotXAxisLabelsGap/2)
|
|
|
|
} else {
|
|
|
|
labelWithGap = strings.Repeat(gapRune, plotXAxisLabelsGap/2) + labelWithGap + strings.Repeat(gapRune, plotXAxisLabelsGap/2)
|
|
|
|
}
|
2022-12-12 21:36:06 +11:00
|
|
|
|
2024-10-12 23:54:29 +02:00
|
|
|
labelStart := labelStartMap[i]
|
|
|
|
if !isFirstLabel && labelStart < lastUsedLabelEnd {
|
|
|
|
// the label would overlap with the previous label
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
expectedLabelWidth := len(labelWithGap)
|
|
|
|
remainingWidth := globalAvailableWidth - labelStart
|
|
|
|
if expectedLabelWidth > remainingWidth {
|
|
|
|
// the label would be too long to fit in the remaining space
|
|
|
|
if expectedLabelWidth-1 <= remainingWidth {
|
2024-10-12 23:57:10 +02:00
|
|
|
// if we omit the last gap, it fits, so we draw that before stopping
|
2024-10-12 23:54:29 +02:00
|
|
|
expectedLabelWidth--
|
|
|
|
labelWithoutGap := labelWithGap[:len(labelWithGap)-1]
|
|
|
|
tview.Print(screen, labelWithoutGap, initialOffset+labelStart, y+height-plotXAxisLabelsHeight, expectedLabelWidth, tview.AlignLeft, plot.axesLabelColor)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
2022-12-12 21:36:06 +11:00
|
|
|
|
2024-10-12 23:54:29 +02:00
|
|
|
lastUsedLabelEnd = labelStart + expectedLabelWidth
|
|
|
|
tview.Print(screen, labelWithGap, initialOffset+labelStart, y+height-plotXAxisLabelsHeight, expectedLabelWidth, tview.AlignLeft, plot.axesLabelColor)
|
|
|
|
isFirstLabel = false
|
2022-12-12 21:36:06 +11:00
|
|
|
}
|
2023-12-29 14:51:02 +07:00
|
|
|
}
|
2022-12-12 21:36:06 +11:00
|
|
|
|
2023-12-29 14:51:02 +07:00
|
|
|
func (plot *Plot) drawYAxisLabelToScreen(screen tcell.Screen, plotYAxisLabelsWidth int, x int, y int, height int) {
|
2024-10-11 03:37:08 +02:00
|
|
|
verticalOffset := plot.minVal
|
|
|
|
verticalScale := (plot.maxVal - plot.minVal) / float64(height-plotXAxisLabelsHeight-1)
|
2024-09-28 12:33:20 +10:00
|
|
|
previousLabel := ""
|
2022-12-12 21:36:06 +11:00
|
|
|
|
|
|
|
for i := 0; i*(plotYAxisLabelsGap+1) < height-1; i++ {
|
2024-09-28 12:33:20 +10:00
|
|
|
var label string
|
|
|
|
if plot.yAxisLabelDataType == PlotYAxisLabelDataFloat {
|
2024-10-11 03:37:08 +02:00
|
|
|
label = fmt.Sprintf("%.2f", float64(i)*verticalScale*(plotYAxisLabelsGap+1)+verticalOffset)
|
2024-09-28 12:33:20 +10:00
|
|
|
} else {
|
2024-10-11 03:37:08 +02:00
|
|
|
label = strconv.Itoa(int(float64(i)*verticalScale*(plotYAxisLabelsGap+1) + verticalOffset))
|
2024-09-28 12:33:20 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
// Prevent same label being shown twice.
|
|
|
|
// Mainly relevant for integer labels with small data sets (in value)
|
|
|
|
if label == previousLabel {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
previousLabel = label
|
|
|
|
|
2022-12-12 21:36:06 +11:00
|
|
|
tview.Print(screen,
|
|
|
|
label,
|
|
|
|
x,
|
2023-12-15 19:20:57 +11:00
|
|
|
y+height-(i*(plotYAxisLabelsGap+1))-2, //nolint:gomnd
|
2022-12-12 21:36:06 +11:00
|
|
|
plotYAxisLabelsWidth,
|
|
|
|
tview.AlignLeft, plot.axesLabelColor)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-13 12:44:01 +11:00
|
|
|
//nolint:cyclop,gocognit
|
2022-12-22 14:36:57 -06:00
|
|
|
func (plot *Plot) drawDotMarkerToScreen(screen tcell.Screen) {
|
2024-05-07 12:23:28 -06:00
|
|
|
x, y, width, height := plot.GetPlotRect()
|
2022-12-12 21:36:06 +11:00
|
|
|
chartData := plot.getData()
|
2024-10-12 14:45:42 +02:00
|
|
|
verticalOffset := -plot.minVal
|
2022-12-12 21:36:06 +11:00
|
|
|
|
|
|
|
switch plot.ptype {
|
|
|
|
case PlotTypeLineChart:
|
|
|
|
for i, line := range chartData {
|
|
|
|
style := tcell.StyleDefault.Background(plot.GetBackgroundColor()).Foreground(plot.lineColors[i])
|
|
|
|
|
|
|
|
for j := 0; j < len(line) && j*plotHorizontalScale < width; j++ {
|
|
|
|
val := line[j]
|
2024-10-11 00:32:08 +02:00
|
|
|
if math.IsNaN(val) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-10-11 03:37:08 +02:00
|
|
|
lheight := int(((val + verticalOffset) / plot.maxVal) * float64(height-1))
|
2024-10-12 14:45:42 +02:00
|
|
|
if lheight > height {
|
|
|
|
continue
|
|
|
|
}
|
2022-12-12 21:36:06 +11:00
|
|
|
|
|
|
|
if (x+(j*plotHorizontalScale) < x+width) && (y+height-1-lheight < y+height) {
|
|
|
|
tview.PrintJoinedSemigraphics(screen, x+(j*plotHorizontalScale), y+height-1-lheight, plot.dotMarkerRune, style)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
case PlotTypeScatter:
|
|
|
|
for i, line := range chartData {
|
|
|
|
style := tcell.StyleDefault.Background(plot.GetBackgroundColor()).Foreground(plot.lineColors[i])
|
|
|
|
|
|
|
|
for j, val := range line {
|
2024-10-11 00:32:08 +02:00
|
|
|
if math.IsNaN(val) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-10-12 14:45:42 +02:00
|
|
|
lheight := int(((val + verticalOffset) / plot.maxVal) * float64(height-1))
|
|
|
|
if lheight > height {
|
|
|
|
continue
|
|
|
|
}
|
2022-12-12 21:36:06 +11:00
|
|
|
|
|
|
|
if (x+(j*plotHorizontalScale) < x+width) && (y+height-1-lheight < y+height) {
|
|
|
|
tview.PrintJoinedSemigraphics(screen, x+(j*plotHorizontalScale), y+height-1-lheight, plot.dotMarkerRune, style)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (plot *Plot) drawBrailleMarkerToScreen(screen tcell.Screen) {
|
2024-05-07 12:23:28 -06:00
|
|
|
x, y, width, height := plot.GetPlotRect()
|
2022-12-12 21:36:06 +11:00
|
|
|
|
|
|
|
plot.calcBrailleLines()
|
|
|
|
|
|
|
|
// print to screen
|
|
|
|
for point, cell := range plot.getBrailleCells() {
|
|
|
|
style := tcell.StyleDefault.Background(plot.GetBackgroundColor()).Foreground(cell.color)
|
2023-11-23 12:06:44 -07:00
|
|
|
if point.X < x+width && point.Y < y+height {
|
|
|
|
tview.PrintJoinedSemigraphics(screen, point.X, point.Y, cell.cRune, style)
|
2022-12-12 21:36:06 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-11 22:24:44 +02:00
|
|
|
func calcDataPointHeight(val, maxVal, minVal float64, height int) int {
|
|
|
|
return int(((val - minVal) / (maxVal - minVal)) * float64(height-1))
|
|
|
|
}
|
|
|
|
|
2024-10-12 17:04:38 +02:00
|
|
|
func calcDataPointHeightIfInBounds(val float64, maxVal float64, minVal float64, height int) (int, bool) {
|
|
|
|
if math.IsNaN(val) {
|
|
|
|
return 0, false
|
|
|
|
}
|
2024-10-13 12:44:01 +11:00
|
|
|
|
2024-10-12 17:04:38 +02:00
|
|
|
result := calcDataPointHeight(val, maxVal, minVal, height)
|
|
|
|
if (val > maxVal) || (val < minVal) || (result > height) {
|
|
|
|
return result, false
|
|
|
|
}
|
2024-10-13 12:44:01 +11:00
|
|
|
|
2024-10-12 17:04:38 +02:00
|
|
|
return result, true
|
|
|
|
}
|
|
|
|
|
2022-12-12 21:36:06 +11:00
|
|
|
func (plot *Plot) calcBrailleLines() {
|
2024-05-07 12:23:28 -06:00
|
|
|
x, y, _, height := plot.GetPlotRect()
|
2022-12-12 21:36:06 +11:00
|
|
|
chartData := plot.getData()
|
|
|
|
|
|
|
|
for i, line := range chartData {
|
|
|
|
if len(line) <= 1 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-10-11 22:24:44 +02:00
|
|
|
previousHeight := 0
|
2024-10-12 17:04:38 +02:00
|
|
|
lastValWasOk := false
|
2024-10-13 12:44:01 +11:00
|
|
|
|
2024-10-12 17:04:38 +02:00
|
|
|
for j, val := range line {
|
|
|
|
lheight, currentValIsOk := calcDataPointHeightIfInBounds(val, plot.maxVal, plot.minVal, height)
|
2024-10-12 12:01:56 +11:00
|
|
|
|
2024-10-12 17:04:38 +02:00
|
|
|
if !lastValWasOk && !currentValIsOk {
|
|
|
|
// nothing valid to draw, skip to next data point
|
2024-10-11 22:24:44 +02:00
|
|
|
continue
|
2024-10-13 12:44:01 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
if !lastValWasOk { //nolint:gocritic
|
2024-10-12 17:04:38 +02:00
|
|
|
// current data point is single valid data point, draw it individually
|
|
|
|
plot.setBraillePoint(
|
|
|
|
calcBraillePoint(x, j+1, y, height, lheight),
|
|
|
|
plot.lineColors[i],
|
|
|
|
)
|
|
|
|
} else if !currentValIsOk {
|
|
|
|
// last data point was single valid data point, draw it individually
|
|
|
|
plot.setBraillePoint(
|
|
|
|
calcBraillePoint(x, j, y, height, previousHeight),
|
|
|
|
plot.lineColors[i],
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
// we have two valid data points, draw a line between them
|
|
|
|
plot.setBrailleLine(
|
|
|
|
calcBraillePoint(x, j, y, height, previousHeight),
|
|
|
|
calcBraillePoint(x, j+1, y, height, lheight),
|
|
|
|
plot.lineColors[i],
|
|
|
|
)
|
2024-10-11 22:24:44 +02:00
|
|
|
}
|
|
|
|
|
2024-10-12 17:04:38 +02:00
|
|
|
lastValWasOk = currentValIsOk
|
2022-12-12 21:36:06 +11:00
|
|
|
previousHeight = lheight
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-12 17:04:38 +02:00
|
|
|
func calcBraillePoint(x, j, y, maxY, height int) image.Point {
|
|
|
|
return image.Pt(
|
|
|
|
(x+(j*plotHorizontalScale))*2, //nolint:gomnd
|
|
|
|
(y+maxY-height-1)*4, //nolint:gomnd
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-12-12 21:36:06 +11:00
|
|
|
func (plot *Plot) setBraillePoint(p image.Point, color tcell.Color) {
|
2024-10-12 17:04:38 +02:00
|
|
|
if p.X < 0 || p.Y < 0 {
|
|
|
|
return
|
|
|
|
}
|
2024-10-13 12:44:01 +11:00
|
|
|
|
2023-12-15 19:20:57 +11:00
|
|
|
point := image.Pt(p.X/2, p.Y/4) //nolint:gomnd
|
2022-12-12 21:36:06 +11:00
|
|
|
plot.brailleCellMap[point] = brailleCell{
|
|
|
|
plot.brailleCellMap[point].cRune | brailleRune[p.Y%4][p.X%2],
|
|
|
|
color,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (plot *Plot) setBrailleLine(p0, p1 image.Point, color tcell.Color) {
|
|
|
|
for _, p := range plot.brailleLine(p0, p1) {
|
|
|
|
plot.setBraillePoint(p, color)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (plot *Plot) getBrailleCells() map[image.Point]brailleCell {
|
|
|
|
cellMap := make(map[image.Point]brailleCell)
|
|
|
|
for point, cvCell := range plot.brailleCellMap {
|
|
|
|
cellMap[point] = brailleCell{cvCell.cRune + brailleOffsetRune, cvCell.color}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cellMap
|
|
|
|
}
|
|
|
|
|
|
|
|
func (plot *Plot) brailleLine(p0, p1 image.Point) []image.Point {
|
|
|
|
points := []image.Point{}
|
|
|
|
leftPoint, rightPoint := p0, p1
|
|
|
|
|
|
|
|
if leftPoint.X > rightPoint.X {
|
|
|
|
leftPoint, rightPoint = rightPoint, leftPoint
|
|
|
|
}
|
|
|
|
|
|
|
|
xDistance := absInt(leftPoint.X - rightPoint.X)
|
|
|
|
yDistance := absInt(leftPoint.Y - rightPoint.Y)
|
|
|
|
slope := float64(yDistance) / float64(xDistance)
|
|
|
|
slopeSign := 1
|
|
|
|
|
|
|
|
if rightPoint.Y < leftPoint.Y {
|
|
|
|
slopeSign = -1
|
|
|
|
}
|
|
|
|
|
|
|
|
targetYCoordinate := float64(leftPoint.Y)
|
|
|
|
currentYCoordinate := leftPoint.Y
|
|
|
|
|
|
|
|
for i := leftPoint.X; i < rightPoint.X; i++ {
|
|
|
|
points = append(points, image.Pt(i, currentYCoordinate))
|
|
|
|
targetYCoordinate += (slope * float64(slopeSign))
|
|
|
|
|
|
|
|
for currentYCoordinate != int(targetYCoordinate) {
|
|
|
|
points = append(points, image.Pt(i, currentYCoordinate))
|
|
|
|
|
|
|
|
currentYCoordinate += slopeSign
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return points
|
|
|
|
}
|