Skip to content

Commit

Permalink
Merge pull request #69 from markusressel/feature/custom-x-axis-labeling
Browse files Browse the repository at this point in the history
Feature: Allow custom X-Axis labeling
  • Loading branch information
navidys authored Oct 13, 2024
2 parents 40e5944 + 091fcae commit 3ddb90b
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 27 deletions.
35 changes: 22 additions & 13 deletions demos/demo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,20 +204,9 @@ func main() {
}

moveSinData := func(data [][]float64) [][]float64 {
n := 220
newData := make([][]float64, 2)
newData[0] = make([]float64, n)
newData[1] = make([]float64, n)

for i := 0; i < n; i++ {
if i+1 < len(data[0]) {
newData[0][i] = data[0][i+1]
}
if i+1 < len(data[1]) {
newData[1][i] = data[1][i+1]
}
}

newData[0] = rotate(data[0], -1)
newData[1] = rotate(data[1], -1)
return newData
}

Expand Down Expand Up @@ -330,3 +319,23 @@ func newBarChart() *tvxwidgets.BarChart {

return barGraph
}

// Source: https://stackoverflow.com/questions/50833673/rotate-array-in-go/79079760#79079760
// rotate rotates the given slice by k positions to the left or right.
func rotate[T any](slice []T, k int) []T {
if len(slice) == 0 {
return slice
}

var r int
if k > 0 {
r = len(slice) - k%len(slice)
} else {
kAbs := int(math.Abs(float64(k)))
r = kAbs % len(slice)
}

slice = append(slice[r:], slice[:r]...)

return slice
}
1 change: 1 addition & 0 deletions demos/plot_xaxis_labels/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
![Screenshot](screenshot.png)
133 changes: 133 additions & 0 deletions demos/plot_xaxis_labels/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package main

import (
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/navidys/tvxwidgets"
"github.com/rivo/tview"
"math"
"time"
)

func main() {

app := tview.NewApplication()

// >>> Data Function <<<
// With these values, the curve will start with a value of 0 and reach a
// high point of 2 at x = 3.14 (Pi) and then return to 0 at x = 6.28 (2*Pi).

// Play around with these values to get a feel for how they affect the curve
// and how you might adapt this code to plot other functions.

period := 2 * math.Pi
horizontalStretchFactor := 1.0
verticalStretchFactor := 1.0
xOffset := 0.0
yOffset := 0.0

// >>> Graph View/Camera Controls <<<
// These values influence which part of the curve is shown in
// what "zoom level".

xAxisZoomFactor := 3.0
yAxisZoomFactor := 1.0
xAxisShift := 0.0
yAxisShift := 0.0

// xFunc1 defines the x values that should be used for each vertical "slot" in the graph.
xFunc1 := func(i int) float64 {
return (float64(i) / xAxisZoomFactor) + xAxisShift
}
// yFunc1 defines the y values that result from a given input value x (this is the actual function).
yFunc1 := func(x float64) float64 {
return (math.Sin((x+xOffset)/horizontalStretchFactor) + yOffset) * verticalStretchFactor
}

// xLabelFunc1 defines a label for each vertical "slot". Which labels are shown is determined automatically
// based on the available space.
xLabelFunc1 := func(i int) string {
xVal := xFunc1(i)
labelVal := xVal
label := fmt.Sprintf("%.1f", labelVal)
return label
}

// computeDataArray computes the y values for n vertical slots based on the definitions above.
computeDataArray := func() [][]float64 {
n := 150
data := make([][]float64, 1)
data[0] = make([]float64, n)
for i := 0; i < n; i++ {
xVal := xFunc1(i)
yVal := yFunc1(xVal)
data[0][i] = yVal
}

return data
}

data := computeDataArray()

bmLineChart := tvxwidgets.NewPlot()
bmLineChart.SetBorder(true)
bmLineChart.SetTitle("line chart (braille mode)")
bmLineChart.SetLineColor([]tcell.Color{
tcell.ColorSteelBlue,
tcell.ColorGreen,
})
bmLineChart.SetMarker(tvxwidgets.PlotMarkerBraille)
bmLineChart.SetXAxisLabelFunc(xLabelFunc1)
bmLineChart.SetYAxisAutoScaleMin(false)
bmLineChart.SetYAxisAutoScaleMax(false)
bmLineChart.SetYRange(
(-1+yOffset+yAxisShift)/yAxisZoomFactor,
(1+yOffset+yAxisShift)/yAxisZoomFactor,
)
bmLineChart.SetData(data)

firstRow := tview.NewFlex().SetDirection(tview.FlexColumn)
firstRow.AddItem(bmLineChart, 0, 1, false)
firstRow.SetRect(0, 0, 100, 15)

layout := tview.NewFlex().SetDirection(tview.FlexRow)
layout.AddItem(firstRow, 0, 1, false)
layout.SetRect(0, 0, 100, 30)

animate := true

rotateDataContinuously := func() {
tick := time.NewTicker(100 * time.Millisecond)
go func() {
initialxAxisShift := xAxisShift
for {
select {
case <-tick.C:
if !animate {
continue
}

xAxisShift = xAxisShift + 0.1
if xAxisShift >= initialxAxisShift+period*4 {
xAxisShift = initialxAxisShift
}
data = computeDataArray()
bmLineChart.SetData(data)

app.Draw()
}
}
}()
}

go rotateDataContinuously()

if err := app.SetRoot(layout, false).EnableMouse(true).SetMouseCapture(func(event *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction) {
if action == tview.MouseLeftClick {
animate = !animate
}
return event, action
}).Run(); err != nil {
panic(err)
}
}
Binary file added demos/plot_xaxis_labels/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
106 changes: 92 additions & 14 deletions plot.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"image"
"math"
"strconv"
"strings"
"sync"

"github.com/gdamore/tcell/v2"
Expand Down Expand Up @@ -41,6 +42,8 @@ const (
plotXAxisLabelsHeight = 1
plotXAxisLabelsGap = 2
plotYAxisLabelsGap = 1

gapRune = " "
)

type brailleCell struct {
Expand All @@ -64,6 +67,7 @@ type Plot struct {
axesLabelColor tcell.Color
drawAxes bool
drawXAxisLabel bool
xAxisLabelFunc func(int) string
drawYAxisLabel bool
yAxisLabelDataType PlotYAxisLabelDataType
yAxisAutoScaleMin bool
Expand All @@ -83,6 +87,7 @@ func NewPlot() *Plot {
axesLabelColor: tcell.ColorDimGray,
drawAxes: true,
drawXAxisLabel: true,
xAxisLabelFunc: strconv.Itoa,
drawYAxisLabel: true,
yAxisLabelDataType: PlotYAxisLabelDataFloat,
yAxisAutoScaleMin: false,
Expand Down Expand Up @@ -152,6 +157,11 @@ func (plot *Plot) SetDrawXAxisLabel(draw bool) {
plot.drawXAxisLabel = draw
}

// SetXAxisLabelFunc sets x axis label function.
func (plot *Plot) SetXAxisLabelFunc(f func(int) string) {
plot.xAxisLabelFunc = f
}

// SetDrawYAxisLabel set true in order to draw y axis label to screen.
func (plot *Plot) SetDrawYAxisLabel(draw bool) {
plot.drawYAxisLabel = draw
Expand Down Expand Up @@ -262,34 +272,102 @@ func (plot *Plot) drawAxesToScreen(screen tcell.Screen) {
tview.BoxDrawingsLightUpAndRight, axesStyle)

if plot.drawXAxisLabel {
plot.drawXAxisLabelToScreen(screen, plotYAxisLabelsWidth, x, y, width, height)
plot.drawXAxisLabelsToScreen(screen, plotYAxisLabelsWidth, x, y, width, height)
}

if plot.drawYAxisLabel {
plot.drawYAxisLabelToScreen(screen, plotYAxisLabelsWidth, x, y, height)
plot.drawYAxisLabelsToScreen(screen, plotYAxisLabelsWidth, x, y, height)
}
}

func (plot *Plot) drawXAxisLabelToScreen(
//nolint:funlen,cyclop
func (plot *Plot) drawXAxisLabelsToScreen(
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)
xAxisAreaStartX := x + plotYAxisLabelsWidth + 1
xAxisAreaEndX := x + width
xAxisAvailableWidth := xAxisAreaEndX - xAxisAreaStartX

for labelX := x + plotYAxisLabelsWidth +
(plotXAxisLabelsGap)*plotHorizontalScale + 1; labelX < x+width-1; {
label := strconv.Itoa((labelX-(x+plotYAxisLabelsWidth)-1)/(plotHorizontalScale) + 1)
labelMap := map[int]string{}
labelStartMap := map[int]int{}

tview.Print(screen, label, labelX, y+height-plotXAxisLabelsHeight, width, tview.AlignLeft, plot.axesLabelColor)
maxDataPoints := 0
for _, d := range plot.data {
maxDataPoints = max(maxDataPoints, len(d))
}

// determine the width needed for the largest label
maxXAxisLabelWidth := 0

labelX += (len(label) + plotXAxisLabelsGap) * plotHorizontalScale
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 //nolint:gomnd
} else {
expectedLabelWidth += plotXAxisLabelsGap
}

currentLabelStart := i - int(math.Round(float64(expectedLabelWidth)/2)) //nolint:gomnd
labelStartMap[i] = currentLabelStart
}

// print the labels, skipping those that would overlap,
// stopping when there is no more space
lastUsedLabelEnd := math.MinInt
initialOffset := xAxisAreaStartX

for i := 0; i < maxDataPoints; i++ {
labelStart := labelStartMap[i]
if labelStart < lastUsedLabelEnd {
// the label would overlap with the previous label
continue
}

rawLabel := labelMap[i]
labelWithGap := rawLabel

if i == 0 {
labelWithGap += strings.Repeat(gapRune, plotXAxisLabelsGap/2) //nolint:gomnd
} else {
labelWithGap = strings.Repeat(gapRune, plotXAxisLabelsGap/2) + labelWithGap + strings.Repeat(gapRune, plotXAxisLabelsGap/2) //nolint:lll,gomnd
}

expectedLabelWidth := len(labelWithGap)
remainingWidth := xAxisAvailableWidth - labelStart

if expectedLabelWidth > remainingWidth {
// the label would be too long to fit in the remaining space
if expectedLabelWidth-1 <= remainingWidth {
// if we omit the last gap, it fits, so we draw that before stopping
labelWithoutGap := labelWithGap[:len(labelWithGap)-1]
plot.printXAxisLabel(screen, labelWithoutGap, initialOffset+labelStart, y+height-plotXAxisLabelsHeight)
}

break
}

lastUsedLabelEnd = labelStart + expectedLabelWidth
plot.printXAxisLabel(screen, labelWithGap, initialOffset+labelStart, y+height-plotXAxisLabelsHeight)
}
}

func (plot *Plot) printXAxisLabel(screen tcell.Screen, label string, x, y int) {
tview.Print(screen, label, x, y, len(label), tview.AlignLeft, plot.axesLabelColor)
}

func (plot *Plot) drawYAxisLabelToScreen(screen tcell.Screen, plotYAxisLabelsWidth int, x int, y int, height int) {
func (plot *Plot) drawYAxisLabelsToScreen(screen tcell.Screen, plotYAxisLabelsWidth int, x int, y int, height int) {
verticalOffset := plot.minVal
verticalScale := (plot.maxVal - plot.minVal) / float64(height-plotXAxisLabelsHeight-1)
previousLabel := ""
Expand Down

0 comments on commit 3ddb90b

Please sign in to comment.