-
-
Notifications
You must be signed in to change notification settings - Fork 668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Chart Widget #586
Chart Widget #586
Conversation
@bryall \o/ This is amazing - thanks! Looks like you'll need to add something to the Beekeeper configuration to add matplotlib to the test environment. However, I don't want to add matplotlib (and thus numpy, and others) to There are a couple of options here. We could just add an Another option would be to make chart an A third option, we could handle this by making |
I like option 3 :-p |
I am good with option 3. |
@bryall Awesome - I've just created a stub project https://github.com/pybee/toga_chart so you've got something to issue a PR against; if you can transfer this over to that repository (and fix up any other problems with the repo that you notice), I'll merge that in! |
src/core/toga/__init__.py
Outdated
@@ -11,6 +11,7 @@ | |||
from .widgets.box import Box | |||
from .widgets.button import Button | |||
from .widgets.canvas import Canvas | |||
from .widgets.chart import Chart | |||
from .widgets.detailedlist import DetailedList | |||
from .widgets.image import * |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At column 1: (F403) 'from .widgets.image import *' used; unable to detect undefined names
src/core/toga/__init__.py
Outdated
@@ -11,6 +11,7 @@ | |||
from .widgets.box import Box | |||
from .widgets.button import Button | |||
from .widgets.canvas import Canvas | |||
from .widgets.chart import Chart | |||
from .widgets.detailedlist import DetailedList | |||
from .widgets.image import * | |||
from .widgets.imageview import * |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At column 1: (F403) 'from .widgets.imageview import *' used; unable to detect undefined names
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code you've got here (along with beeware/toga-chart#1) clearly works; however, I'm not 100% clear on some of the design decisions you've made here. I've flagged the areas that are unclear.
It's entirely possible the fault lies with the existing path/line/closed path objects that were already there; if there is an underlying problem with those objects, I'd rather replace them than overload them with similar, but very slightly different APIs.
By way of guidance, the API we've got here was inspired by the HTML5 Canvas - but it's entirely possible we've missed some things in the translation.
src/core/toga/widgets/canvas.py
Outdated
@@ -99,6 +99,12 @@ def remove(self, drawing_object): | |||
self.drawing_objects.remove(drawing_object) | |||
self.redraw() | |||
|
|||
def clearAll(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why clearAll
? Why not just clear
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will change, i think I was trying to differentiate between clearing a single draw command with clearing all the draw commands
@@ -46,7 +46,8 @@ def create(self): | |||
self.add_constraints() | |||
|
|||
def redraw(self): | |||
pass | |||
# pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor housekeeping - no need to have the commented out pass
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one looks like it's still current
src/core/toga/widgets/canvas.py
Outdated
@@ -459,6 +493,27 @@ def color(self, value): | |||
self._color = parse_color(value) | |||
|
|||
|
|||
class Path(Context): | |||
"""A user-created :class:`Path <Path>` drawing object for a | |||
path context. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I see what Path is adding here - the implementation of _draw()
is the only significant functionality, and the implementation is identical to Context (which is the direct descendent). How is Path any different to Context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, did not look at Context to closely, this class does not add anything, will fix this
src/core/toga/widgets/canvas.py
Outdated
@@ -591,6 +646,32 @@ def _draw(self, impl, *args, **kwargs): | |||
impl.move_to(self.x, self.y, *args, **kwargs) | |||
|
|||
|
|||
class CloseTo: | |||
"""A user-created :class:`CloseTo <CloseTo>` drawing object which closes | |||
the polyline vack to the starting point |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little unclear about why CloseTo is needed if we also have ClosePath. Is there really a need for two different ways to draw a closed path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with close path you have to know before adding the path segments that it is closed. matplotlib gives an array of draw commands. When I loop over the commands, I do not know it is closed till that last command telling me to close the segment. With the two implemented platforms so far. To close the segment does not require passing it the beginning point to close to. So adding this type makes it easier for me to just loop over the draw commands and add each one to the drawing context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok; that makes some sense; I missed that this isn't a context, it's a drawing primitive.
I think I understand the use case; however, I'm still not 100% sure if a new primitive is needed here.
As I understand it, the problem is that at the time you are drawing the primitives, you don't know if the path is closed or open, so you need to be able to put a "close" command at the end of sequence of drawing commands.
My countersuggestion would be that you could use the ClosedPath context manager in a different way to acheive the same goal.
ClosedPath is a context manager, so the default way of using it is:
with canvas.closed_path(x,y) as path:
path.line_to(x+10, y+10)
path.line_to(x-10, y+10)
but that's no the only way it can be used. A context manager is just an object that has an enter/exit lifecycle; you can create instances of the object and call the underlying primitives on it directly. So, if you don't know if the path is closed until some boolean is evaluated, you could do something like:
draw_objs = [
LineTo(x+10, y+10),
LineTo(x-10, y+10),
]
if is_closed:
target = canvas.closed_path(x,y)
else:
target = canvas
for obj in draw_objs:
target.add_draw_obj(obj)
That last part could be simplified by adding an "add_draw_objs()" method, which seems like a likely use case.
The underlying idea here is that closing a path isn't an operation; a closed path is an entity that is constructed from other operations. Does that make sense?
(Also, small typo in the docstring - vack
)
Sorry I was busy and left this partially done. updated the code based on comments. Once I get these changes into core, I will move to toga-chart |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it's heading in the right direction; I've flagged one architectural question, and some minor cosmetic bits.
@@ -46,7 +46,8 @@ def create(self): | |||
self.add_constraints() | |||
|
|||
def redraw(self): | |||
pass | |||
# pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one looks like it's still current
src/core/toga/widgets/canvas.py
Outdated
@@ -458,7 +478,6 @@ def color(self, value): | |||
else: | |||
self._color = parse_color(value) | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a minor style thing, but convention is 2 blank lines between class declarations (not sure why this didn't get flagged by Beefore...)
src/core/toga/widgets/canvas.py
Outdated
@@ -591,6 +646,32 @@ def _draw(self, impl, *args, **kwargs): | |||
impl.move_to(self.x, self.y, *args, **kwargs) | |||
|
|||
|
|||
class CloseTo: | |||
"""A user-created :class:`CloseTo <CloseTo>` drawing object which closes | |||
the polyline vack to the starting point |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok; that makes some sense; I missed that this isn't a context, it's a drawing primitive.
I think I understand the use case; however, I'm still not 100% sure if a new primitive is needed here.
As I understand it, the problem is that at the time you are drawing the primitives, you don't know if the path is closed or open, so you need to be able to put a "close" command at the end of sequence of drawing commands.
My countersuggestion would be that you could use the ClosedPath context manager in a different way to acheive the same goal.
ClosedPath is a context manager, so the default way of using it is:
with canvas.closed_path(x,y) as path:
path.line_to(x+10, y+10)
path.line_to(x-10, y+10)
but that's no the only way it can be used. A context manager is just an object that has an enter/exit lifecycle; you can create instances of the object and call the underlying primitives on it directly. So, if you don't know if the path is closed until some boolean is evaluated, you could do something like:
draw_objs = [
LineTo(x+10, y+10),
LineTo(x-10, y+10),
]
if is_closed:
target = canvas.closed_path(x,y)
else:
target = canvas
for obj in draw_objs:
target.add_draw_obj(obj)
That last part could be simplified by adding an "add_draw_objs()" method, which seems like a likely use case.
The underlying idea here is that closing a path isn't an operation; a closed path is an entity that is constructed from other operations. Does that make sense?
(Also, small typo in the docstring - vack
)
I have made the changes, and I think this is good to go |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thanks for the contribution!
This PR allows the user to render a matplotib plot figure onto a toga canvas. I also added a clearAll function to the canvas to be able to clear the draw command from the canvas to render a new canvas.
There are also more to implement here to get all matplolib figures rendering correctly in the canvas widget. Such as images, hatch styles, and alpha. Text is currently displayed by converting to path commands, it looks good to me but the draw text in the canvas can be updated to change this.
I have some discussion topics related to this:
As currently implemented it is not interactive. Do you have future plans to have a mouse action API in core? It looks like you have a keyboard api in the constants. If not I could add the mouse handlers into the impl code.
Right now we have a closed_path type only. I left it as is and created a generic path type. As I read the path commands from matplotlib I just add them to the path class. Do we want to keep both?
Also the closed_path draw command on cocoa and gtk do not need the x,y coords should we change the API to drop them?
Refs #545
PR Checklist: