[GH-ISSUE #168] Question: Problem with Linechart #100

Closed
opened 2026-03-03 16:22:22 +03:00 by kerem · 10 comments
Owner

Originally created by @keithknott26 on GitHub (Mar 7, 2019).
Original GitHub issue: https://github.com/mum4k/termdash/issues/168

@mum4k,

I noticed recently that the linechart is displaying the full dataset but only half the labels, the labels should go to 23:59 but stop for some reason.

image

I've been trying to debug things on my end but haven't had much luck in figuring out why its happening. Would you be able to run a dataset through on your side and see if its happening for you as well? I'm using a dataset with 1440 records in it and it displays labels up to about half.

It happens with the XAxisUnscaled option disabled.

Originally created by @keithknott26 on GitHub (Mar 7, 2019). Original GitHub issue: https://github.com/mum4k/termdash/issues/168 @mum4k, I noticed recently that the linechart is displaying the full dataset but only half the labels, the labels should go to 23:59 but stop for some reason. ![image](https://user-images.githubusercontent.com/16966683/53922733-66dbdd80-402a-11e9-85a3-e98a901a0573.png) I've been trying to debug things on my end but haven't had much luck in figuring out why its happening. Would you be able to run a dataset through on your side and see if its happening for you as well? I'm using a dataset with 1440 records in it and it displays labels up to about half. It happens with the XAxisUnscaled option disabled.
kerem 2026-03-03 16:22:22 +03:00
  • closed this issue
  • added the
    question
    label
Author
Owner

@mum4k commented on GitHub (Mar 7, 2019):

This reminds me of #117. Could you share how you are creating the linechart? I.e. The set of options you're using when creating it and when writing data values to it.

I will try to recreate this issue locally once I have this.

<!-- gh-comment-id:470340074 --> @mum4k commented on GitHub (Mar 7, 2019): This reminds me of #117. Could you share how you are creating the linechart? I.e. The set of options you're using when creating it and when writing data values to it. I will try to recreate this issue locally once I have this.
Author
Owner

@keithknott26 commented on GitHub (Mar 7, 2019):

I agree , it's very odd. I'm not sure its a bug though because I haven't seen you mess with the line chart much since you last fixed it. I'll keep looking on my end and let you know what I find

lc, err = linechart.New(
			linechart.AxesCellOpts(cell.FgColor(cell.ColorNumber(graphAxes))),
			linechart.YLabelCellOpts(cell.FgColor(cell.ColorNumber(graphYLabels))),
			linechart.XLabelCellOpts(cell.FgColor(cell.ColorNumber(graphXLabels))),
		)

if err := lc.Series("first", inputs,
		linechart.SeriesCellOpts(cell.FgColor(cell.ColorNumber(GraphLine))),
	); err != nil {
		fmt.Println("LineChart Error:", err)
	}
go periodic(ctx, time.Duration(10*time.Millisecond), func() error {
		var inputs []float64
                 inputs = < contains 1440 values>
                 var labelMap = map[int]string{}
		for i, x := range inputLabels {
			labelMap[i] = x
		}

		if err := lc.Series("first", inputs,
			linechart.SeriesCellOpts(cell.FgColor(cell.ColorNumber(GraphLine))),
			linechart.SeriesXLabels(labelMap),
		); err != nil {
			return err
		}
}
<!-- gh-comment-id:470341873 --> @keithknott26 commented on GitHub (Mar 7, 2019): I agree , it's very odd. I'm not sure its a bug though because I haven't seen you mess with the line chart much since you last fixed it. I'll keep looking on my end and let you know what I find ``` lc, err = linechart.New( linechart.AxesCellOpts(cell.FgColor(cell.ColorNumber(graphAxes))), linechart.YLabelCellOpts(cell.FgColor(cell.ColorNumber(graphYLabels))), linechart.XLabelCellOpts(cell.FgColor(cell.ColorNumber(graphXLabels))), ) if err := lc.Series("first", inputs, linechart.SeriesCellOpts(cell.FgColor(cell.ColorNumber(GraphLine))), ); err != nil { fmt.Println("LineChart Error:", err) } go periodic(ctx, time.Duration(10*time.Millisecond), func() error { var inputs []float64 inputs = < contains 1440 values> var labelMap = map[int]string{} for i, x := range inputLabels { labelMap[i] = x } if err := lc.Series("first", inputs, linechart.SeriesCellOpts(cell.FgColor(cell.ColorNumber(GraphLine))), linechart.SeriesXLabels(labelMap), ); err != nil { return err } } ```
Author
Owner

@mum4k commented on GitHub (Mar 7, 2019):

Agreed I am also unsure if this is a bug, but it is good that you have reported it. I have seen myself bring bugs back to life in the past.

I will test it and let you know.

<!-- gh-comment-id:470343540 --> @mum4k commented on GitHub (Mar 7, 2019): Agreed I am also unsure if this is a bug, but it is good that you have reported it. I have seen myself bring bugs back to life in the past. I will test it and let you know.
Author
Owner

@mum4k commented on GitHub (Mar 7, 2019):

I have tested this code:

    lc, err := linechart.New(
        linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
        linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
        linechart.XLabelCellOpts(cell.FgColor(cell.ColorCyan)),
    )

    var inputs []float64
    for i := 0; i < 1440; i++ {
        inputs = append(inputs, float64(i%100))
    }

    labels := map[int]string{}
    for i := 0; i < 1440; i++ {
        labels[i] = fmt.Sprintf("l%v", i)
    }

    if err := lc.Series("first", inputs,
        linechart.SeriesCellOpts(cell.FgColor(cell.ColorBlue)),
        linechart.SeriesXLabels(labels),
    ); err != nil {
        panic(err)
    }

And I got the following linechart:

screenshot 2019-03-07 at 00 02 45

The same with vertical labels where space plays less of a role:

screenshot 2019-03-07 at 00 04 06

In other words I failed to reproduce this :( Of course it is still possible that I am doing something slightly differently, maybe you can identify it in my code above?

Alternatively I will be happy to help you debug on your end. If you would like that - can you possibly share more of the code that generates the values and their labels. If there is an issue, it will be in that code. If you would prefer to do this over a more private channel, feel free to contact me directly at termdash@termdash.dev.

Another alternative - can you save the complete content of the var inputs []float64 and var labelMap = map[int]string{} variables just before the call to Series?

<!-- gh-comment-id:470387424 --> @mum4k commented on GitHub (Mar 7, 2019): I have tested this code: ```go lc, err := linechart.New( linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)), linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)), linechart.XLabelCellOpts(cell.FgColor(cell.ColorCyan)), ) var inputs []float64 for i := 0; i < 1440; i++ { inputs = append(inputs, float64(i%100)) } labels := map[int]string{} for i := 0; i < 1440; i++ { labels[i] = fmt.Sprintf("l%v", i) } if err := lc.Series("first", inputs, linechart.SeriesCellOpts(cell.FgColor(cell.ColorBlue)), linechart.SeriesXLabels(labels), ); err != nil { panic(err) } ``` And I got the following linechart: <img width="945" alt="screenshot 2019-03-07 at 00 02 45" src="https://user-images.githubusercontent.com/5315344/53933438-66643680-406c-11e9-8f6e-af0d914347e4.png"> The same with vertical labels where space plays less of a role: <img width="945" alt="screenshot 2019-03-07 at 00 04 06" src="https://user-images.githubusercontent.com/5315344/53933678-66b10180-406d-11e9-9ba6-660a76e4943b.png"> In other words I failed to reproduce this :( Of course it is still possible that I am doing something slightly differently, maybe you can identify it in my code above? Alternatively I will be happy to help you debug on your end. If you would like that - can you possibly share more of the code that generates the values and their labels. If there is an issue, it will be in that code. If you would prefer to do this over a more private channel, feel free to contact me directly at termdash@termdash.dev. Another alternative - can you save the complete content of the **var inputs []float64** and **var labelMap = map[int]string{}** variables just before the call to Series?
Author
Owner

@keithknott26 commented on GitHub (Mar 8, 2019):

Thank you I really appreciate you taking the time to test, I worked on this more today and found that the data is being read in faster than its being picked up by the line chart. This was one of my first projects made to learn Go so i'm still trying to figure out how to sync those two things up.

Edit: The problem is there are twice as many records as there are labels, so that would explain why I was seeing the linechart populate with all the data but the linechart only showed half the labels. By the time we reach the end of the file there's no longer a 1:1 mapping between label and record its more like 1:2.

I've posted my code on github: https://github.com/keithknott26/datadash it still needs work particularly in cleaning up unused code and supporting infinite charts (today it assumes the number of charts will only ever range from 1 to 5), and refactoring the way it reads data vs displays it.

Thanks again, I'm actually curious which method you use to debug? I remember you mentioning you were using a Mac - are you using VSCode and Delve ? I found I had to disable drawing of the charts while debugging (with well placed print statements) otherwise it's impossible to read what's on the screen.

<!-- gh-comment-id:470783437 --> @keithknott26 commented on GitHub (Mar 8, 2019): Thank you I really appreciate you taking the time to test, I worked on this more today and found that the data is being read in faster than its being picked up by the line chart. This was one of my first projects made to learn Go so i'm still trying to figure out how to sync those two things up. Edit: The problem is there are twice as many records as there are labels, so that would explain why I was seeing the linechart populate with all the data but the linechart only showed half the labels. By the time we reach the end of the file there's no longer a 1:1 mapping between label and record its more like 1:2. I've posted my code on github: https://github.com/keithknott26/datadash it still needs work particularly in cleaning up unused code and supporting infinite charts (today it assumes the number of charts will only ever range from 1 to 5), and refactoring the way it reads data vs displays it. Thanks again, I'm actually curious which method you use to debug? I remember you mentioning you were using a Mac - are you using VSCode and Delve ? I found I had to disable drawing of the charts while debugging (with well placed print statements) otherwise it's impossible to read what's on the screen.
Author
Owner

@keithknott26 commented on GitHub (Mar 8, 2019):

One other question I had was, is there already support for triggered updates? Instead of doing periodic redraws I was hoping to update the content of each widget only if it needed it. My fan runs on my Mac whenever I run the program so I know its probably doing way too many unnecessary redraws.

<!-- gh-comment-id:470789927 --> @keithknott26 commented on GitHub (Mar 8, 2019): One other question I had was, is there already support for triggered updates? Instead of doing periodic redraws I was hoping to update the content of each widget only if it needed it. My fan runs on my Mac whenever I run the program so I know its probably doing way too many unnecessary redraws.
Author
Owner

@mum4k commented on GitHub (Mar 8, 2019):

More than happy to help @keithknott26. Thank you very much for sharing the code with me among other things I am really glad to see this used. It is a cool idea to build an application like datadash, great work! I will try to answer all three of your questions below, please let me know if I missed anything.

Data races

The problem

Let's tackle the problem you're observing first, i.e. the 1:2 mapping between label and data as I think this has the most complexity.

TLDR; The code as it stands now has multiple data races. From database theory a data race happens when there are two memory accesses in a program where:

  • both access the same memory location;
  • both are performed by a different thread;
  • at least one of them is a write into the memory location;

The important thing to note is that when a program has a data race, the behavior is undefined. As you probably know "undefined" in software engineering means "very very bad". I strongly recommend to read this article, Dmitry works on data race detection and goes into factual and partly entertaining details on what really happens when we write software with data races.

Since this if fairly abstract, let me go to the specifics. Note this is only describing one of the data races in datadash, there are a few more. The readDataChannel function spawns a separate goroutine in which it runs the parsePlotData function.

github.com/keithknott26/datadash@9ed568bfad/cmd/datadash.go (L345-L356)

The parsePlotData function the calls The row.Update method which writes to multiple memory locations, specifically it writes into the Data and Label ring buffers. This is our access number one which Writes.

The row.createLineChart method starts a goroutine that reads from multiple memory locations, among others it reads from the Data and Labels ring buffers which happens to be the same memory location the access number one wrote. This is our access number two which Reads.

github.com/keithknott26/datadash@9ed568bfad/row.go (L469)

A single race condition like this can result in any behavior including data loss. The simplest logical explanation of data races is when we imagine two bank accounts, say my checking account and my landlords account and their initial balances:

Initial

me: $1000
landlord: $2000

Now say that we have two transactions:

  • transaction1 is me wanting to pay $1000 to my landlord, i.e. the rent.
  • transaction2 is me receiving a $100 dollars from a friend.

Each transaction needs to read the balance of both accounts, do the appropriate addition / subtraction and write the new balances. Now obviously with data races the behavior is undefined so any ordering of the operations can happen, but imagine this one:

  1. transaction1 reads the value of "me" account => $1000.
  2. transaction2 reads the value of "me" account => $1000.
  3. transaction1 reads the value of "landlord" account => $2000.
  4. transaction1 calculates new balance for "me" $1000(me)-$1000 => $0
  5. transaction1 calculates new balance for "landlord" $2000(landlord)+$1000 => $3000
  6. transaction1 writes new balances to $0(me) and $3000(landlord).
  7. transaction2 finally wakes up and calculates new balance for "me" $1000(me)+$100 => $1100
  8. transaction2 writes new balance to $1100(me).

Final

me: $1100
landlord: $3000

This example is easy to understand because there are two writes. Why is it also bad when there is only one write is better explained by Dmitry in the article listed above.

It is important to understand that Go's runtime doesn't give any guarantees about the ordering of operation between two goroutines. The Go's advice is share memory by communicating.

The solution

Honestly the one advice I give and find the most effective sounds counter-productive at first:

  1. Don't write conturrent code unless proven by performance data that it is needed. Oftentimes goroutines make things slower rather than faster, i.e. the Amdahl's Law. When I started learning Go I ended up overusing Goroutines for which I later hated myself. Concurrent code is immensely hard to debug and even after years of practice I still cannot get it right on the first few tries.
  2. If concurrency is required, still don't use concurrency primitives directly, it is very easy to make mistakes and very hard to find them. Instead either use one of the pre-existing packages (errgroup is virtually mistake-proof) or use one of well established concurrency patterns and separate concurrency from business logic. So the business logic would still be written inside one (main) goroutine and concurrency is added later on top of it.

Specifically for datadash I would recommend switching to triggered redraws (see below) and remove all goroutines unless they are really needed. It makes life much easier.

If we wanted to just quickly fix this in place, we could add Mutexes to protect all occasions when data are read or written by separate goroutines.

Debugging

Sadly my answer to this will probably be disappointing. I am a bit old-school I don't use any of the debugging tools. I only needed something advanced once when I added too much concurrency into my code and lost my mind.

Instead I follow the "test driven development" school. This also includes designing software that is easy to test. Very specifically this involves strict separation of business logic from the UI which allows complete test coverage of the business logic in separation. Thus on any bugs or errors I first write a test that repeats the problem and then debug as per the usual (log / print statements).

When I had a bug in the UI portion of Termdash and I couldn't use the approach mentioned above, I typically use the Text widget to print out debugging data, or use ioutil.WriteFile to write them to a file. I.e. I accumulate what I want to see into a bytes.Buffer and then on a specific trigger like a condition or a middle mouse button a dump them into a file for inspection.

Triggered updates

Yes they are already implemented, instead of using periodic, which are fairly wasteful the user can trigger updates as needed. I have recently started documenting the API. It isn't finished yet, but this part has already been written:

https://github.com/mum4k/termdash/wiki/Termdash-API

Look for the termdash.NewController function.

Hope this helps.

<!-- gh-comment-id:470810052 --> @mum4k commented on GitHub (Mar 8, 2019): More than happy to help @keithknott26. Thank you very much for sharing the code with me among other things I am really glad to see this used. It is a cool idea to build an application like **datadash**, great work! I will try to answer all three of your questions below, please let me know if I missed anything. ### Data races #### The problem Let's tackle the problem you're observing first, i.e. the 1:2 mapping between label and data as I think this has the most complexity. **TLDR**; The code as it stands now has multiple data races. From database theory a data race happens when there are two memory accesses in a program where: - both access the same memory location; - both are performed by a different thread; - at least one of them is a write into the memory location; The important thing to note is that when a program has a data race, the behavior is **undefined**. As you probably know "undefined" in software engineering means "very very bad". I strongly recommend to read [this article](https://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong), Dmitry works on data race detection and goes into factual and partly entertaining details on what really happens when we write software with data races. Since this if fairly abstract, let me go to the specifics. Note this is only describing one of the data races in datadash, there are a few more. The **readDataChannel** function spawns a separate goroutine in which it runs the **parsePlotData** function. https://github.com/keithknott26/datadash/blob/9ed568bfadfeba85dfb0f3d1e44a7d412fcd5ef3/cmd/datadash.go#L345-L356 The **parsePlotData** function the calls The **row.Update** method which **writes** to multiple memory locations, specifically it writes into the Data and Label ring buffers. This is our access number one which Writes. The **row.createLineChart** method starts a goroutine that reads from multiple memory locations, among others it reads from the Data and Labels ring buffers which happens to be the same memory location the access number one wrote. This is our access number two which Reads. https://github.com/keithknott26/datadash/blob/9ed568bfadfeba85dfb0f3d1e44a7d412fcd5ef3/row.go#L469 A single race condition like this can result in any behavior including data loss. The simplest logical explanation of data races is when we imagine two bank accounts, say my checking account and my landlords account and their initial balances: **Initial** me: $1000 landlord: $2000 Now say that we have two transactions: - transaction1 is me wanting to pay $1000 to my landlord, i.e. the rent. - transaction2 is me receiving a $100 dollars from a friend. Each transaction needs to read the balance of both accounts, do the appropriate addition / subtraction and write the new balances. Now obviously with data races the behavior is undefined so any ordering of the operations can happen, but imagine this one: 1) **transaction1** reads the value of "me" account => $1000. 1) **transaction2** reads the value of "me" account => $1000. 1) **transaction1** reads the value of "landlord" account => $2000. 1) **transaction1** calculates new balance for "me" $1000(me)-$1000 => $0 1) **transaction1** calculates new balance for "landlord" $2000(landlord)+$1000 => $3000 1) **transaction1** writes new balances to $0(me) and $3000(landlord). 1) **transaction2** finally wakes up and calculates new balance for "me" $1000(me)+$100 => $1100 1) **transaction2** writes new balance to $1100(me). **Final** me: $1100 landlord: $3000 This example is easy to understand because there are two writes. Why is it also bad when there is only one write is better explained by Dmitry in the article listed above. It is important to understand that Go's runtime doesn't give *any* guarantees about the ordering of operation between two goroutines. The Go's advice is *[share memory by communicating](https://blog.golang.org/share-memory-by-communicating)*. #### The solution Honestly the one advice I give and find the most effective sounds counter-productive at first: 1) Don't write conturrent code unless proven by performance data that it is needed. Oftentimes goroutines make things slower rather than faster, i.e. the [Amdahl's Law](https://en.wikipedia.org/wiki/Amdahl%27s_law). When I started learning Go I ended up overusing Goroutines for which I later hated myself. Concurrent code is immensely hard to debug and even after years of practice I still cannot get it right on the first few tries. 1) If concurrency is required, still don't use concurrency primitives directly, it is very easy to make mistakes and very hard to find them. Instead either use one of the pre-existing packages ([errgroup](https://godoc.org/golang.org/x/sync/errgroup) is virtually mistake-proof) or use one of well established [concurrency](https://blog.golang.org/pipelines) [patterns](https://blog.golang.org/advanced-go-concurrency-patterns) and separate concurrency from business logic. So the business logic would still be written inside one (main) goroutine and concurrency is added later on top of it. Specifically for **datadash** I would recommend switching to triggered redraws (see below) and remove all goroutines unless they are really needed. It makes life much easier. If we wanted to just quickly fix this in place, we could add [Mutexes](https://godoc.org/sync#Mutex) to protect all occasions when data are read or written by separate goroutines. ### Debugging Sadly my answer to this will probably be disappointing. I am a bit old-school I don't use any of the debugging tools. I only needed something advanced once when I added too much concurrency into my code and lost my mind. Instead I follow the "[test driven development](https://en.wikipedia.org/wiki/Test-driven_development)" school. This also includes designing software that is easy to test. Very specifically this involves strict separation of business logic from the UI which allows complete test coverage of the business logic in separation. Thus on any bugs or errors I first write a test that repeats the problem and then debug as per the usual (log / print statements). When I had a bug in the UI portion of Termdash and I couldn't use the approach mentioned above, I typically use the Text widget to print out debugging data, or use [ioutil.WriteFile](https://godoc.org/io/ioutil#WriteFile) to write them to a file. I.e. I accumulate what I want to see into a [bytes.Buffer](https://godoc.org/bytes#Buffer) and then on a specific trigger like a condition or a middle mouse button a dump them into a file for inspection. ### Triggered updates Yes they are already implemented, instead of using periodic, which are fairly wasteful the user can trigger updates as needed. I have recently started documenting the API. It isn't finished yet, but this part has already been written: https://github.com/mum4k/termdash/wiki/Termdash-API Look for the **termdash.NewController** function. Hope this helps.
Author
Owner

@keithknott26 commented on GitHub (Mar 10, 2019):

Thank you for your very detailed response! I must admit I wasn't expecting you to provide so much helpful information I truly appreciate it. I wasn't aware of the race condition and it certainly explains the behavior I'm seeing when changing the timing values around. I've decided to re-write the app to include the features I mentioned, as well as remove the periodic redraws. Thank you for sharing the tip about debugging using a textbox or file, I've been using the textbox method and it's working great so far!

One thing that I'm confused about is the ability to place sets of widgets or Rows on the screen programatically. For example today I'm doing something like this to build the container options:

TopHalf := []container.Option{
		container.SplitHorizontal(
			container.Top(myapp.GetContainer(0)...),
			container.Bottom(myapp.GetContainer(1)...),
			container.SplitPercent(50),
		),
	}
	BottomHalf := []container.Option{
		container.SplitHorizontal(
			container.Top(myapp.GetContainer(2)...),
			container.Bottom(myapp.GetContainer(3)...),
			container.SplitPercent(50),
		),
	}
	AllRows := []container.Option{
		container.SplitHorizontal(
			container.Top(TopHalf...),
			container.Bottom(BottomHalf...),
			container.SplitPercent(50),
		),
	}

Then I build the container.

When there's 3 rows:

case 3:
		c, err := container.New(
			t,
			container.SplitHorizontal(
				container.Top(TopHalf...),
				container.Bottom(myapp.GetContainer(2)...),
				container.SplitPercent(66),
			),
		)
		if err != nil {
			return nil, err
		}
		return c, nil
	case 4:
		c, err := container.New(
			t,
			AllRows...,
		)
		if err != nil {
			return nil, err
		}
		return c, nil

Or where there's 5 rows

case 5:
		TopHalf := []container.Option{
			container.SplitHorizontal(
				//container.Top(debugRow...),
				container.Top(myapp.GetContainer(0)...),
				container.Bottom(myapp.GetContainer(1)...),
				container.SplitPercent(50),
			),
		}
		BottomHalf := []container.Option{
			container.SplitHorizontal(
				container.Top(myapp.GetContainer(2)...),
				container.Bottom(myapp.GetContainer(3)...),
				container.SplitPercent(50),
			),
		}
		AllRows := []container.Option{
			container.SplitHorizontal(
				container.Top(TopHalf...),
				container.Bottom(BottomHalf...),
				container.SplitPercent(50),
			),
		}
		c, err := container.New(
			t,
			container.SplitHorizontal(
				container.Top(AllRows...),
				container.Bottom(myapp.GetContainer(4)...),
				container.SplitPercent(80),
			),
		)
		if err != nil {
			return nil, err
		}
		return c, nil

But when you get to 3 or 5 rows you have different Horizontal Split percentages. Is there a good way to add "Rows" to the container programmatically? I wanted to get to a point where I could add "rows" one by one based on the number of columns in the input file. Today I'm using the manual assignment but would like to see if I can switch that over so that it could display an infinite amount of rows.

I must admit I haven't tried this yet but it seems like maybe i'd want to add more containers? Is it possible to run multiple containers and have them all subscribe to keyboard and mouse events? In this case each container would have 1 chart and 1 text box. Then I could add rows one by one:

a, err := container.New(
			t,
			Row...,
		)
b, err := container.New(
			t,
			Row...,
		)
c, err := container.New(
			t,
			Row...,
		)
d, err := container.New(
			t,
			Row...,
		)

Any thoughts on how you've been able to do this? Hope my update makes sense, if not feel free to let me know and I'll revise and provide additional detail and code examples.

<!-- gh-comment-id:471241707 --> @keithknott26 commented on GitHub (Mar 10, 2019): Thank you for your very detailed response! I must admit I wasn't expecting you to provide so much helpful information I truly appreciate it. I wasn't aware of the race condition and it certainly explains the behavior I'm seeing when changing the timing values around. I've decided to re-write the app to include the features I mentioned, as well as remove the periodic redraws. Thank you for sharing the tip about debugging using a textbox or file, I've been using the textbox method and it's working great so far! One thing that I'm confused about is the ability to place sets of widgets or Rows on the screen programatically. For example today I'm doing something like this to build the container options: ``` TopHalf := []container.Option{ container.SplitHorizontal( container.Top(myapp.GetContainer(0)...), container.Bottom(myapp.GetContainer(1)...), container.SplitPercent(50), ), } BottomHalf := []container.Option{ container.SplitHorizontal( container.Top(myapp.GetContainer(2)...), container.Bottom(myapp.GetContainer(3)...), container.SplitPercent(50), ), } AllRows := []container.Option{ container.SplitHorizontal( container.Top(TopHalf...), container.Bottom(BottomHalf...), container.SplitPercent(50), ), } ``` Then I build the container. When there's 3 rows: ``` case 3: c, err := container.New( t, container.SplitHorizontal( container.Top(TopHalf...), container.Bottom(myapp.GetContainer(2)...), container.SplitPercent(66), ), ) if err != nil { return nil, err } return c, nil case 4: c, err := container.New( t, AllRows..., ) if err != nil { return nil, err } return c, nil ``` Or where there's 5 rows ``` case 5: TopHalf := []container.Option{ container.SplitHorizontal( //container.Top(debugRow...), container.Top(myapp.GetContainer(0)...), container.Bottom(myapp.GetContainer(1)...), container.SplitPercent(50), ), } BottomHalf := []container.Option{ container.SplitHorizontal( container.Top(myapp.GetContainer(2)...), container.Bottom(myapp.GetContainer(3)...), container.SplitPercent(50), ), } AllRows := []container.Option{ container.SplitHorizontal( container.Top(TopHalf...), container.Bottom(BottomHalf...), container.SplitPercent(50), ), } c, err := container.New( t, container.SplitHorizontal( container.Top(AllRows...), container.Bottom(myapp.GetContainer(4)...), container.SplitPercent(80), ), ) if err != nil { return nil, err } return c, nil ``` But when you get to 3 or 5 rows you have different Horizontal Split percentages. Is there a good way to add "Rows" to the container programmatically? I wanted to get to a point where I could add "rows" one by one based on the number of columns in the input file. Today I'm using the manual assignment but would like to see if I can switch that over so that it could display an infinite amount of rows. I must admit I haven't tried this yet but it seems like maybe i'd want to add more containers? Is it possible to run multiple containers and have them all subscribe to keyboard and mouse events? In this case each container would have 1 chart and 1 text box. Then I could add rows one by one: ``` a, err := container.New( t, Row..., ) b, err := container.New( t, Row..., ) c, err := container.New( t, Row..., ) d, err := container.New( t, Row..., ) ``` Any thoughts on how you've been able to do this? Hope my update makes sense, if not feel free to let me know and I'll revise and provide additional detail and code examples.
Author
Owner

@mum4k commented on GitHub (Mar 10, 2019):

Happy to help @keithknott26, feel free to let me know if you will have any further questions. Do note that keyboard and mouse events are also coming from a different goroutine all the way form termbox. I also noticed that you were setting some booleans from keyboard events:

github.com/keithknott26/datadash@9ed568bfad/cmd/datadash.go (L485)

This too is a data race, the booleans would need to be protected with a Mutex. A simplest possible implementation will be something like this (bar any typos):

type protectedBool struct {
  value bool
  mu sync.Mutex
}

func (pb *protectedBool) set(v bool) {
  pb.mu.Lock()
  defer pb.mu.unlock()

  pb.value = v
}

func (pb *protectedBool) get(v bool) {
  pb.mu.Lock()
  defer pb.mu.unlock()

  return pb.value
}

When all reads and writes come through the get() and set() methods, there is no data race.

Setting layout programatically

As for the ability to create layout programatically - the current API is a bit awkward for that. I have been pondering a grid-like API (similar to what termui has) and I think it is time to implement it.

Let me give it a try (tracking in #171) and I will let you know once it is ready on that bug.

Going to close this issue for now, but if you have further questions - please feel free to ask on this or other issues.

<!-- gh-comment-id:471362750 --> @mum4k commented on GitHub (Mar 10, 2019): Happy to help @keithknott26, feel free to let me know if you will have any further questions. Do note that keyboard and mouse events are also coming from a different goroutine all the way form termbox. I also noticed that you were setting some booleans from keyboard events: https://github.com/keithknott26/datadash/blob/9ed568bfadfeba85dfb0f3d1e44a7d412fcd5ef3/cmd/datadash.go#L485 This too is a data race, the booleans would need to be protected with a Mutex. A simplest possible implementation will be something like this (bar any typos): ```go type protectedBool struct { value bool mu sync.Mutex } func (pb *protectedBool) set(v bool) { pb.mu.Lock() defer pb.mu.unlock() pb.value = v } func (pb *protectedBool) get(v bool) { pb.mu.Lock() defer pb.mu.unlock() return pb.value } ``` When all reads and writes come through the get() and set() methods, there is no data race. ### Setting layout programatically As for the ability to create layout programatically - the current API is a bit awkward for that. I have been pondering a grid-like API (similar to what termui has) and I think it is time to implement it. Let me give it a try (tracking in #171) and I will let you know once it is ready on that bug. Going to close this issue for now, but if you have further questions - please feel free to ask on this or other issues.
Author
Owner

@mum4k commented on GitHub (Mar 11, 2019):

@keithknott26, fyi #171 is now resolved and contains details about the new grid builder (available in the devel branch). As always I would appreciate your feedback and bug reports.

<!-- gh-comment-id:471400540 --> @mum4k commented on GitHub (Mar 11, 2019): @keithknott26, fyi #171 is now resolved and contains details about the new grid builder (available in the devel branch). As always I would appreciate your feedback and bug reports.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/termdash#100
No description provided.