mirror of
https://github.com/navidys/tvxwidgets.git
synced 2025-04-28 13:48:52 +08:00
389 lines
9.7 KiB
Go
389 lines
9.7 KiB
Go
package tvxwidgets
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
// Marker represents plot drawing marker (braille or dot).
|
|
type Marker uint
|
|
|
|
const (
|
|
// plot marker.
|
|
PlotMarkerBraille Marker = iota
|
|
PlotMarkerDot
|
|
)
|
|
|
|
// 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
|
|
)
|
|
|
|
type brailleCell struct {
|
|
cRune rune
|
|
color tcell.Color
|
|
}
|
|
|
|
// Plot represents a plot primitive used for different charts.
|
|
type Plot struct {
|
|
*tview.Box
|
|
data [][]float64
|
|
maxVal float64
|
|
marker Marker
|
|
ptype PlotType
|
|
dotMarkerRune rune
|
|
lineColors []tcell.Color
|
|
axesColor tcell.Color
|
|
axesLabelColor tcell.Color
|
|
drawAxes bool
|
|
drawXAxisLabel bool
|
|
drawYAxisLabel bool
|
|
brailleCellMap map[image.Point]brailleCell
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewPlot returns a plot widget.
|
|
func NewPlot() *Plot {
|
|
return &Plot{
|
|
Box: tview.NewBox(),
|
|
marker: PlotMarkerDot,
|
|
ptype: PlotTypeLineChart,
|
|
dotMarkerRune: dotRune,
|
|
axesColor: tcell.ColorDimGray,
|
|
axesLabelColor: tcell.ColorDimGray,
|
|
drawAxes: true,
|
|
drawXAxisLabel: true,
|
|
drawYAxisLabel: true,
|
|
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:
|
|
plot.drawDotMarkerToScreen(screen)
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// SetDrawXAxisLabel set true in order to draw x axis label to screen.
|
|
func (plot *Plot) SetDrawXAxisLabel(draw bool) {
|
|
plot.drawXAxisLabel = draw
|
|
}
|
|
|
|
// SetDrawYAxisLabel set true in order to draw y axis label to screen.
|
|
func (plot *Plot) SetDrawYAxisLabel(draw bool) {
|
|
plot.drawYAxisLabel = draw
|
|
}
|
|
|
|
// 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
|
|
plot.maxVal = getMaxFloat64From2dSlice(data)
|
|
}
|
|
|
|
// SetDotMarkerRune sets dot marker rune.
|
|
func (plot *Plot) SetDotMarkerRune(r rune) {
|
|
plot.dotMarkerRune = r
|
|
}
|
|
|
|
// Figure out the text width necessary to display the largest data value.
|
|
func (plot *Plot) getYAxisLabelsWidth() int {
|
|
return len(fmt.Sprintf("%.2f", plot.maxVal))
|
|
}
|
|
|
|
// GetPlotRect returns the rect for the inner part of the plot, ie not including axes.
|
|
func (plot *Plot) GetPlotRect() (int, int, int, int) {
|
|
x, y, width, height := plot.Box.GetInnerRect()
|
|
plotYAxisLabelsWidth := plot.getYAxisLabelsWidth()
|
|
|
|
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()
|
|
plotYAxisLabelsWidth := plot.getYAxisLabelsWidth()
|
|
|
|
axesStyle := tcell.StyleDefault.Background(plot.GetBackgroundColor()).Foreground(plot.axesColor)
|
|
|
|
// draw Y axis line
|
|
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)
|
|
|
|
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,
|
|
) {
|
|
tview.Print(screen, "0",
|
|
x+plotYAxisLabelsWidth,
|
|
y+height-plotXAxisLabelsHeight,
|
|
1,
|
|
tview.AlignLeft, plot.axesLabelColor)
|
|
|
|
for labelX := x + plotYAxisLabelsWidth +
|
|
(plotXAxisLabelsGap)*plotHorizontalScale + 1; labelX < x+width-1; {
|
|
label := strconv.Itoa((labelX-(x+plotYAxisLabelsWidth)-1)/(plotHorizontalScale) + 1)
|
|
|
|
tview.Print(screen, label, labelX, y+height-plotXAxisLabelsHeight, width, tview.AlignLeft, plot.axesLabelColor)
|
|
|
|
labelX += (len(label) + plotXAxisLabelsGap) * plotHorizontalScale
|
|
}
|
|
}
|
|
|
|
func (plot *Plot) drawYAxisLabelToScreen(screen tcell.Screen, plotYAxisLabelsWidth int, x int, y int, height int) {
|
|
verticalScale := plot.maxVal / float64(height-plotXAxisLabelsHeight-1)
|
|
|
|
for i := 0; i*(plotYAxisLabelsGap+1) < height-1; i++ {
|
|
label := fmt.Sprintf("%.2f", float64(i)*verticalScale*(plotYAxisLabelsGap+1))
|
|
tview.Print(screen,
|
|
label,
|
|
x,
|
|
y+height-(i*(plotYAxisLabelsGap+1))-2, //nolint:gomnd
|
|
plotYAxisLabelsWidth,
|
|
tview.AlignLeft, plot.axesLabelColor)
|
|
}
|
|
}
|
|
|
|
//nolint:cyclop
|
|
func (plot *Plot) drawDotMarkerToScreen(screen tcell.Screen) {
|
|
x, y, width, height := plot.GetPlotRect()
|
|
chartData := plot.getData()
|
|
|
|
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]
|
|
lheight := int((val / plot.maxVal) * float64(height-1))
|
|
|
|
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 {
|
|
lheight := int((val / plot.maxVal) * float64(height-1))
|
|
|
|
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) {
|
|
x, y, width, height := plot.GetPlotRect()
|
|
|
|
plot.calcBrailleLines()
|
|
|
|
// print to screen
|
|
for point, cell := range plot.getBrailleCells() {
|
|
style := tcell.StyleDefault.Background(plot.GetBackgroundColor()).Foreground(cell.color)
|
|
if point.X < x+width && point.Y < y+height {
|
|
tview.PrintJoinedSemigraphics(screen, point.X, point.Y, cell.cRune, style)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (plot *Plot) calcBrailleLines() {
|
|
x, y, _, height := plot.GetPlotRect()
|
|
chartData := plot.getData()
|
|
|
|
for i, line := range chartData {
|
|
if len(line) <= 1 {
|
|
continue
|
|
}
|
|
|
|
previousHeight := int((line[0] / plot.maxVal) * float64(height-1))
|
|
|
|
for j, val := range line[1:] {
|
|
lheight := int((val / plot.maxVal) * float64(height-1))
|
|
|
|
plot.setBrailleLine(
|
|
image.Pt(
|
|
(x+(j*plotHorizontalScale))*2, //nolint:gomnd
|
|
(y+height-previousHeight-1)*4, //nolint:gomnd
|
|
),
|
|
image.Pt(
|
|
(x+((j+1)*plotHorizontalScale))*2, //nolint:gomnd
|
|
(y+height-lheight-1)*4, //nolint:gomnd
|
|
),
|
|
plot.lineColors[i],
|
|
)
|
|
|
|
previousHeight = lheight
|
|
}
|
|
}
|
|
}
|
|
|
|
func (plot *Plot) setBraillePoint(p image.Point, color tcell.Color) {
|
|
point := image.Pt(p.X/2, p.Y/4) //nolint:gomnd
|
|
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
|
|
}
|