Skip to content

Commit

Permalink
Merge pull request #68 from markusressel/feature/allow-y-axis-range-o…
Browse files Browse the repository at this point in the history
…verride

allow manual override of Y-Axis MinValue and MaxValue
  • Loading branch information
navidys authored Oct 13, 2024
2 parents 89b22f5 + 3cfbbb4 commit 40e5944
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 47 deletions.
1 change: 1 addition & 0 deletions demos/plot_custom_range/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
![Screenshot](screenshot.png)
119 changes: 119 additions & 0 deletions demos/plot_custom_range/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

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

func main() {

app := tview.NewApplication()

sinData := func() [][]float64 {
n := 220
data := make([][]float64, 2)
data[0] = make([]float64, n)
data[1] = make([]float64, n)
for i := 0; i < n; i++ {
data[0][i] = math.Sin(float64(i+1) / 5)
// Avoid taking Cos(0) because it creates a high point of 2 that
// will never be hit again and makes the graph look a little funny
data[1][i] = math.Cos(float64(i+1) / 5)
}
return data
}()

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.SetYAxisAutoScaleMin(false)
bmLineChart.SetYAxisAutoScaleMax(false)
bmLineChart.SetYRange(-1.5, 1.5)
bmLineChart.SetData(sinData)

bmLineChart.SetDrawXAxisLabel(false)

dmLineChart := tvxwidgets.NewPlot()
dmLineChart.SetBorder(true)
dmLineChart.SetTitle("line chart (dot mode)")
dmLineChart.SetLineColor([]tcell.Color{
tcell.ColorDarkOrange,
})
dmLineChart.SetAxesLabelColor(tcell.ColorGold)
dmLineChart.SetAxesColor(tcell.ColorGold)
dmLineChart.SetMarker(tvxwidgets.PlotMarkerDot)
dmLineChart.SetDotMarkerRune('\u25c9')

sampleData1 := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sampleData2 := []float64{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}

dotModeChartData := [][]float64{sampleData1}
dotModeChartData[0] = append(dotModeChartData[0], sampleData2...)
dotModeChartData[0] = append(dotModeChartData[0], sampleData1[:5]...)
dotModeChartData[0] = append(dotModeChartData[0], sampleData2[5:]...)
dotModeChartData[0] = append(dotModeChartData[0], sampleData1[:7]...)
dotModeChartData[0] = append(dotModeChartData[0], sampleData2[3:]...)
dmLineChart.SetYAxisAutoScaleMin(false)
dmLineChart.SetYAxisAutoScaleMax(false)
dmLineChart.SetYRange(0, 3)
dmLineChart.SetData(dotModeChartData)

scatterPlotData := make([][]float64, 2)
scatterPlotData[0] = []float64{1, 2, 3, 4, 5}
scatterPlotData[1] = sinData[1][4:]
dmScatterPlot := tvxwidgets.NewPlot()

dmScatterPlot.SetBorder(true)
dmScatterPlot.SetTitle("scatter plot (dot mode)")
dmScatterPlot.SetLineColor([]tcell.Color{
tcell.ColorMediumSlateBlue,
tcell.ColorLightSkyBlue,
})
dmScatterPlot.SetPlotType(tvxwidgets.PlotTypeScatter)
dmScatterPlot.SetMarker(tvxwidgets.PlotMarkerDot)
dmScatterPlot.SetYAxisAutoScaleMin(false)
dmScatterPlot.SetYAxisAutoScaleMax(false)
dmScatterPlot.SetYRange(-1, 3)
dmScatterPlot.SetData(scatterPlotData)
dmScatterPlot.SetDrawYAxisLabel(false)

bmScatterPlot := tvxwidgets.NewPlot()
bmScatterPlot.SetBorder(true)
bmScatterPlot.SetTitle("scatter plot (braille mode)")
bmScatterPlot.SetLineColor([]tcell.Color{
tcell.ColorGold,
tcell.ColorLightSkyBlue,
})
bmScatterPlot.SetPlotType(tvxwidgets.PlotTypeScatter)
bmScatterPlot.SetMarker(tvxwidgets.PlotMarkerBraille)
bmScatterPlot.SetYAxisAutoScaleMin(false)
bmScatterPlot.SetYAxisAutoScaleMax(false)
bmScatterPlot.SetYRange(-1, 5)
bmScatterPlot.SetData(scatterPlotData)

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

secondRow := tview.NewFlex().SetDirection(tview.FlexColumn)
secondRow.AddItem(dmScatterPlot, 0, 1, false)
secondRow.AddItem(bmScatterPlot, 0, 1, false)
secondRow.SetRect(0, 0, 100, 15)

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

if err := app.SetRoot(layout, false).EnableMouse(true).Run(); err != nil {
panic(err)
}
}
Binary file added demos/plot_custom_range/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
150 changes: 103 additions & 47 deletions plot.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ type brailleCell struct {
// Plot represents a plot primitive used for different charts.
type Plot struct {
*tview.Box
data [][]float64
maxVal float64
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
marker Marker
ptype PlotType
dotMarkerRune rune
Expand All @@ -63,6 +66,8 @@ type Plot struct {
drawXAxisLabel bool
drawYAxisLabel bool
yAxisLabelDataType PlotYAxisLabelDataType
yAxisAutoScaleMin bool
yAxisAutoScaleMax bool
brailleCellMap map[image.Point]brailleCell
mu sync.Mutex
}
Expand All @@ -80,6 +85,8 @@ func NewPlot() *Plot {
drawXAxisLabel: true,
drawYAxisLabel: true,
yAxisLabelDataType: PlotYAxisLabelDataFloat,
yAxisAutoScaleMin: false,
yAxisAutoScaleMax: true,
lineColors: []tcell.Color{
tcell.ColorSteelBlue,
},
Expand Down Expand Up @@ -115,6 +122,16 @@ func (plot *Plot) SetYAxisLabelDataType(dataType PlotYAxisLabelDataType) {
plot.yAxisLabelDataType = dataType
}

// SetYAxisAutoScaleMin enables YAxis min value autoscale.
func (plot *Plot) SetYAxisAutoScaleMin(autoScale bool) {
plot.yAxisAutoScaleMin = autoScale
}

// SetYAxisAutoScaleMax enables YAxix max value autoscale.
func (plot *Plot) SetYAxisAutoScaleMax(autoScale bool) {
plot.yAxisAutoScaleMax = autoScale
}

// SetAxesColor sets axes x and y lines color.
func (plot *Plot) SetAxesColor(color tcell.Color) {
plot.axesColor = color
Expand Down Expand Up @@ -157,7 +174,27 @@ func (plot *Plot) SetData(data [][]float64) {

plot.brailleCellMap = make(map[image.Point]brailleCell)
plot.data = data
plot.maxVal = getMaxFloat64From2dSlice(data)

if plot.yAxisAutoScaleMax {
plot.maxVal = getMaxFloat64From2dSlice(data)
}

if plot.yAxisAutoScaleMin {
plot.minVal = getMinFloat64From2dSlice(data)
}
}

func (plot *Plot) SetMaxVal(maxVal float64) {
plot.maxVal = maxVal
}

func (plot *Plot) SetMinVal(minVal float64) {
plot.minVal = minVal
}

func (plot *Plot) SetYRange(minVal float64, maxVal float64) {
plot.minVal = minVal
plot.maxVal = maxVal
}

// SetDotMarkerRune sets dot marker rune.
Expand Down Expand Up @@ -253,15 +290,16 @@ func (plot *Plot) drawXAxisLabelToScreen(
}

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

for i := 0; i*(plotYAxisLabelsGap+1) < height-1; i++ {
var label string
if plot.yAxisLabelDataType == PlotYAxisLabelDataFloat {
label = fmt.Sprintf("%.2f", float64(i)*verticalScale*(plotYAxisLabelsGap+1))
label = fmt.Sprintf("%.2f", float64(i)*verticalScale*(plotYAxisLabelsGap+1)+verticalOffset)
} else {
label = strconv.Itoa(int(float64(i) * verticalScale * (plotYAxisLabelsGap + 1)))
label = strconv.Itoa(int(float64(i)*verticalScale*(plotYAxisLabelsGap+1) + verticalOffset))
}

// Prevent same label being shown twice.
Expand All @@ -281,10 +319,11 @@ func (plot *Plot) drawYAxisLabelToScreen(screen tcell.Screen, plotYAxisLabelsWid
}
}

//nolint:cyclop
//nolint:cyclop,gocognit
func (plot *Plot) drawDotMarkerToScreen(screen tcell.Screen) {
x, y, width, height := plot.GetPlotRect()
chartData := plot.getData()
verticalOffset := -plot.minVal

switch plot.ptype {
case PlotTypeLineChart:
Expand All @@ -297,7 +336,10 @@ func (plot *Plot) drawDotMarkerToScreen(screen tcell.Screen) {
continue
}

lheight := int((val / plot.maxVal) * float64(height-1))
lheight := int(((val + verticalOffset) / plot.maxVal) * float64(height-1))
if lheight > height {
continue
}

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)
Expand All @@ -314,7 +356,10 @@ func (plot *Plot) drawDotMarkerToScreen(screen tcell.Screen) {
continue
}

lheight := int((val / plot.maxVal) * float64(height-1))
lheight := int(((val + verticalOffset) / plot.maxVal) * float64(height-1))
if lheight > height {
continue
}

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)
Expand Down Expand Up @@ -342,6 +387,19 @@ func calcDataPointHeight(val, maxVal, minVal float64, height int) int {
return int(((val - minVal) / (maxVal - minVal)) * float64(height-1))
}

func calcDataPointHeightIfInBounds(val float64, maxVal float64, minVal float64, height int) (int, bool) {
if math.IsNaN(val) {
return 0, false
}

result := calcDataPointHeight(val, maxVal, minVal, height)
if (val > maxVal) || (val < minVal) || (result > height) {
return result, false
}

return result, true
}

func (plot *Plot) calcBrailleLines() {
x, y, _, height := plot.GetPlotRect()
chartData := plot.getData()
Expand All @@ -351,58 +409,56 @@ func (plot *Plot) calcBrailleLines() {
continue
}

lastValWasNaN := math.IsNaN(line[0])
previousHeight := 0
lastValWasOk := false

if !lastValWasNaN {
previousHeight = calcDataPointHeight(line[0], plot.maxVal, 0, height)
}

for j, val := range line[1:] {
if math.IsNaN(val) {
if !lastValWasNaN {
// last data point was single valid data point
plot.setBraillePoint(
image.Pt(
(x+(j*plotHorizontalScale))*2, //nolint:gomnd
(y+height-previousHeight-1)*4, //nolint:gomnd
),
plot.lineColors[i],
)
}

lastValWasNaN = true
for j, val := range line {
lheight, currentValIsOk := calcDataPointHeightIfInBounds(val, plot.maxVal, plot.minVal, height)

if !lastValWasOk && !currentValIsOk {
// nothing valid to draw, skip to next data point
continue
}

if lastValWasNaN {
previousHeight = calcDataPointHeight(val, plot.maxVal, 0, height)
lastValWasNaN = false

continue
if !lastValWasOk { //nolint:gocritic
// 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],
)
}

lheight := calcDataPointHeight(val, plot.maxVal, 0, height)

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],
)

lastValWasOk = currentValIsOk
previousHeight = lheight
}
}
}

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
)
}

func (plot *Plot) setBraillePoint(p image.Point, color tcell.Color) {
if p.X < 0 || p.Y < 0 {
return
}

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],
Expand Down
Loading

0 comments on commit 40e5944

Please sign in to comment.