Open rjboer opened 3 years ago
Hi. I would suggest using something like https://github.com/wcharczuk/go-chart (or any other Go chart library) and then showing the ìmage.Image
type in a canvas.NewImageFromImage()
. It won't been interactive, but worked relatively well from my testing. I have an old example for Sparta if it could be of any help: https://github.com/Jacalz/sparta/commit/f9927d8b502e388bda1ab21b3028693b939e9eb2 (you will find the relevant information in the last file).
You could also try a chart package built on Fyne like https://github.com/ajstarks/fc/tree/master/chart/
Hi, Thanks for the responses, and I had a look. The Go-Chart is more mature, the ajstarks library functionally better matching but definitely not yet a replacement like qwt for qt (with interactions even with some of our modifications). We use fyne with few of our products on the backend..... I cant wait to throw qt out...and put fyne on all the hydrogen fuel stations, cnc machinery and robotics we produce. I love the fyne initiative and I hope see you guys (eventually, when the time is right) develop a fully functional charting widget.
Similar to:
The wiki proposal is very complete, can I upvote somewhere?
Judging by the number of TODO in the wiki I don't think it is ready for discussion/vote. We have a proposal process as described at https://github.com/fyne-io/fyne/wiki/Contributing:-Proposals. Today marks proposal freeze for version 2.1 (Aberlour) so, if this is to be a core widget, it would need to be a later release. Of course there is always scope to enter the ecosystem through the extensions repository which does not have a release cycle.
The wiki entry (I am the author) was intended to gather information about what kinds of data visualization capabilities we might want to implement and then survey prior art. From there, the next step would be to propose a specific API and plan of work via the proposals process which Andy linked.
Creating a generalized charting widget is a complex problem, and the API needs to be complete and stable for consideration to be added to core. Such a widget should probably start life in fyne-x until it is mature.
I would also like to draw attention to fc, which implements both a canvas as well as a chart, all in Fyne. Anthony has put a lot of work and thought into fc and fc/chart, and he has certainly set the bar for what a data viz library in Fyne would need to have in terms of capabilities.
Just adding my voice in here that I would love to see some way of easily creating node graphs with a widget
Here's a trivial bar chart example using go-chart:
package main
import (
"bytes"
"log"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"github.com/wcharczuk/go-chart/v2"
)
func main() {
// Create a bar chart.
barChart := chart.BarChart{
Title: "Test Bar Chart",
Height: 1024,
BarWidth: 60,
Bars: []chart.Value{
{Value: 5.25, Label: "Turquoise"},
{Value: 4.88, Label: "Green"},
{Value: 4.74, Label: "Gray"},
{Value: 3.22, Label: "Orange"},
{Value: 3, Label: "Blue"},
{Value: 2.27, Label: "Lime"},
{Value: 1, Label: "Red"},
},
}
// Render the bar chart to a byte array.
buf := new(bytes.Buffer)
err := barChart.Render(chart.PNG, buf)
if err != nil {
log.Fatalf("failed to render bar chart: %v", err)
}
// Display the PNG image from buf in a window.
a := app.New()
w := a.NewWindow("Chart")
w.Resize(fyne.Size{Width: 512, Height: 384})
image := canvas.NewImageFromReader(buf, "barChart")
w.SetContent(image)
w.ShowAndRun()
}
One thing that became more visible since that wiki page was updated is Mermaid, which replaced Graphviz in a lot of situations I came across. Maybe worth listing too.
Hi. I would suggest using something like https://github.com/wcharczuk/go-chart (or any other Go chart library) and then showing the
ìmage.Image
type in acanvas.NewImageFromImage()
. It won't been interactive, but worked relatively well from my testing. I have an old example for Sparta if it could be of any help: Jacalz/sparta@f9927d8 (you will find the relevant information in the last file).
if i want show the real time chart, i will update the image Constantly?
Hi. I would suggest using something like https://github.com/wcharczuk/go-chart (or any other Go chart library) and then showing the
ìmage.Image
type in acanvas.NewImageFromImage()
. It won't been interactive, but worked relatively well from my testing. I have an old example for Sparta if it could be of any help: Jacalz/sparta@f9927d8 (you will find the relevant information in the last file).if i want show the real time chart, i will update the image Constantly?
yes. Same as with all the other widgets in your app. If you want to show live data, you need to redraw the widgets, when the underlying data changes.
Have tried to use go-charts (or, any SVG-output library) which is then rendered by canvas.NewImageFromResource
from Fyne.
It renders few seconds!!! If try to resize window where chart is placed inside container.NewVBox
then app almost freezes - because each "resize frame" takes few seconds. Linux Mint on decent CPU.
So it is unusable.
Have tried to use go-charts (or, any SVG-output library) which is then rendered by
canvas.NewImageFromResource
from Fyne.It renders few seconds!!! If try to resize window where chart is placed inside
container.NewVBox
then app almost freezes - because each "resize frame" takes few seconds. Linux Mint on decent CPU.So it is unusable.
yes, rendering a new chart can take a moment. Which is why I would not recommend to re-render with every refresh of your container. Only re-render it if the underlying data changed. For the "normal" refresh of your window Fyne should need to redraw the existing image, not recreate it.
@ErikKalkoken could you please share a code snippet or link in docs how to configure Fyne's Image to don't rerender automatically?
Because I want to resize SVG, not to rerender. This is the reason of using SVG instead of raster formats.
Sure thing. One approach is to use a box container that contains the generated image. When the images needs to be updated, you clear the container, re-add the newly generated image then force a refresh.
Here is a working example that shows this approach in action:
package main
import (
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
)
func main() {
a := app.New()
w := a.NewWindow("Hello World")
charts := container.NewVBox()
w.SetContent(charts)
w.Resize(fyne.Size{Width: 800, Height: 600})
ticker := time.NewTicker(3 * time.Second)
go func() {
for {
updateContainer(charts)
<-ticker.C
}
}()
w.ShowAndRun()
}
func updateContainer(c *fyne.Container) {
c.RemoveAll()
r, err := fyne.LoadResourceFromURLString("https://dynamic-image.vercel.app/api/random/png")
if err != nil {
panic(err)
}
image := canvas.NewImageFromResource(r)
image.FillMode = canvas.ImageFillContain
image.SetMinSize(fyne.Size{Width: 400, Height: 300})
c.Add(image)
c.Refresh()
}
Or a more direct approach is to update the image directly, by replacing it's content and then forcing a refresh of the image.
package main
import (
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/theme"
)
func main() {
a := app.New()
w := a.NewWindow("Hello World")
chart := canvas.NewImageFromResource(theme.BrokenImageIcon())
chart.FillMode = canvas.ImageFillContain
chart.SetMinSize(fyne.Size{Width: 400, Height: 300})
w.SetContent(chart)
w.Resize(fyne.Size{Width: 800, Height: 600})
ticker := time.NewTicker(3 * time.Second)
go func() {
for {
updateChart(chart)
<-ticker.C
}
}()
w.ShowAndRun()
}
func updateChart(c *canvas.Image) {
r, err := fyne.LoadResourceFromURLString("https://dynamic-image.vercel.app/api/random/png")
if err != nil {
panic(err)
}
newImage := canvas.NewImageFromResource(r)
c.Resource = newImage.Resource
c.Refresh()
}
@ErikKalkoken sorry, but you are explaining "external" chart update. And in your example it happens very rare - once per 3 seconds.
I am talking about Fyne's window resize events - they are triggered by Fyne and I guess it happens at least once per 17 milliseconds (60 HZ refresh rate) while user holds edge of the window and moves mouse cursor
Probably generating PNG from go-charts to draw it 1:1 into canvas is a faster approach than through SVG (while I think BMP would work better here, but unfortunately go-charts don't have BMP renderer) but it would include extra step to generate new PNG each time, because otherwise chart would be blurred. And, IMHO, SVG is designed to be generated once and be scaled on any rectangle without extra effort. Could you explain why you are providing example with PNG image?
Actually I've expected to get code which instructs Fyne to don't reload SVG Resource each frame/refresh event (which includes parsing of XML code and creating tree of primitives to draw) but instead just to redraw SVG already-parsed tree of primitives on the resized canvas (like browsers do).
sorry. I may have misunderstood your issue. Thought you were re-generating the image on each refresh, instead of re-using the existing resource. Or trying to generate the chart in the main thread (instead of a separate go-routine) and thereby blocking your app.
Fyne will not re-load the XML unless you ask it to (with Image.Refresh()). When you resize an SVG it must re-rasterise for a crisp finish which should be fast - especially for the trivial image included above. If the refresh is slow then something may be accidentally invalidating the refresh or calling into the chart library.
You could also choose to rasterise the image once and simply scale it using the window resize on the GPU (default for image types not SVG) - but you won't get the nice crisp output.
@andydotxyz could you please look at my code snippet and on result?
package main
import (
"bytes"
"regexp"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"github.com/wcharczuk/go-chart/v2"
)
func main() {
a := app.New()
w := a.NewWindow("TODO App")
w.Resize(fyne.NewSize(800, 600))
svgResource := &fyne.StaticResource{
StaticName: "some.svg",
StaticContent: getChart(),
}
image := canvas.NewImageFromResource(svgResource)
image.SetMinSize(fyne.NewSize(0, 200)) // Otherwise will be a line.
w.SetContent(container.NewBorder(
nil, // TOP
nil, // BOTTOM
nil, // LEFT
nil, // RIGHT
container.NewVScroll(
container.NewVBox(
container.NewVBox(
image,
),
),
),
))
w.ShowAndRun()
}
func getChart() []byte {
graph := chart.Chart{
XAxis: chart.XAxis{
Name: "The XAxis",
},
YAxis: chart.YAxis{
Name: "The YAxis",
},
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
},
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
},
}
buffer := bytes.Buffer{}
err := graph.Render(chart.SVG, &buffer)
if err != nil {
panic(err) // TODO Handle error properly
}
svgContent := buffer.String()
// Remove unsupported by oksvg line breaks from SVG content.
svgContent = strings.ReplaceAll(svgContent, "\n", "")
// Remove unsupported by oksvg color definitions from SVG content.
svgContent = regexp.MustCompile(`rgba\((\d+),(\d+),(\d+),\d+(\.\d+)?\)`).ReplaceAllString(svgContent, "rgb($1,$2,$3)")
return []byte(svgContent)
}
Video of how it looks on my machine (Dell Latitude-5511, Linux Mint, Xfce): https://github.com/user-attachments/assets/8e4ad1de-467e-4448-916b-27f6a0362349
So it is not fast at all. Imagine how it would look like for a graph with thousand of dots. Video doesn't show it, but when window opens then I may see how it renders and resizes to 800*600 first time, which is also not user-friendly.
Notes:
chart.Font
field but it didn't help.getChart
. It causes graph look solid blue, without opacity (if render SVG into file and see in browser then chart looks better). Issue with not rendered fonts (described in point above) probably is also caused by oksvg. I've tried https://github.com/tdewolff/canvas as a renderer for go-charts SVG (instead of native chart.SVG
) and it produced more "compatible"/simple code, but without texts (i.e. fonts loaded) as well.There is a lot I don't understand in the code. Can you start by removing all the strange forcing of min-size so it fills the space?
Instead of
image := canvas.NewImageFromResource(svgResource)
image.SetMinSize(fyne.NewSize(0, 200)) // Otherwise will be a line.
w.SetContent(container.NewBorder(
nil, // TOP
nil, // BOTTOM
nil, // LEFT
nil, // RIGHT
container.NewVScroll(
container.NewVBox(
container.NewVBox(
image,
),
),
),
))
Make it
image := canvas.NewImageFromResource(svgResource)
w.SetContent(image)
Of note regarding text in SVG you probably mean: #685
Once those are considered if you still have performance issues please open an issue - this thread is for a feature request.
@andydotxyz thank you for your reply.
removing all the strange forcing of min-size so it fills the space?
I've added it to replicate what I need in my app/layout. When applied your suggestion then image just took the whole available space, but app is still the same slow on window resizing.
Thank you for the https://github.com/fyne-io/fyne/issues/685 - I've not noticed it before.
I've added my example here just as a follow up for conversation related to "Made a chart that redraws in a canvas, but it's bad and not really efficient..." from the topic message plus to get know if I am doing something wrong - it is my first attempt to use Fyne.
When applied your suggestion then image just took the whole available space, but app is still the same slow on window resizing.
Thanks for confirming.
but it's bad and not really efficient...
The efficient way to draw a graph would be using the canvas
package and some custom layout code. Is there a feature of your graph that cannot be created in this way?
I need in:
Unfortunately I don't want to draw them from primitives like fyne canvas provides - it would take too much effort.
I recognize you would prefer the SVG format, but maybe take another look at rendering the charts in PNG format? I have been using the go-charts library that way to render charts in my Fyne app and it works pretty well.
@ErikKalkoken it is what I told about in the https://github.com/fyne-io/fyne/issues/2228#issuecomment-2227044661. And I've tried fyne.StaticResource
from PNG and it works much faster:
https://github.com/user-attachments/assets/3583833e-c9ad-42e7-80c3-c9aed97abf58
I think you might get better results if you render the image once and then let Fyne resize the image to fit the canvas. You can specify how Fyne renders images with the FillMode
. This also let's you choose if you want to maintain aspect ratio or not. The default is canvas.ImageFillStretch
, which does not maintain aspect ratio. Would suggest to try canvas.ImageFillContain
. You also might want to set a minimum size of your image, so Fyne can calculate the layout better.
Unfortunately I don't want to draw them from primitives like fyne canvas provides - it would take too much effort.
Sometimes it is a tradeoff between developer effort and runtime performance. I know the SVG performance could be improved - but it will always be slower than using the canvas API. There are abstractions and over time a chart API will exist, but for now its that tradeoff I think.
If you can profile what the hold-up is we can try to optimise based on that info.
How to draw a graph (live), like qwt or qt charts?
I would love to start using fyne more extensively in projects and a major blocker is the ability to make live charts.. Made a chart that redraws in a canvas, but it's bad and not really efficient... There are probably bigger things to develop right now, but for future reference..