package tvxwidgets import ( "fmt" "image" "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)) } func (plot *Plot) getChartAreaRect() (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 := fmt.Sprintf( "%d", (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:gocognit,cyclop func (plot *Plot) drawDotMarkerToScreen(screen tcell.Screen) { x, y, width, height := plot.getChartAreaRect() 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.getChartAreaRect() 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.getChartAreaRect() 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 }