Skip to content

Commit 3e531d2

Browse files
authored
Merge pull request #54 from beeware/audit-cleanup
Cleanup following toga.Canvas audit
2 parents e44c162 + bb840ec commit 3e531d2

File tree

6 files changed

+97
-108
lines changed

6 files changed

+97
-108
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ jobs:
4444
strategy:
4545
fail-fast: false
4646
matrix:
47-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev"]
47+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
4848
include:
4949
- experimental: false
5050

51-
- python-version: "3.12-dev"
52-
experimental: true
51+
# - python-version: "3.13-dev"
52+
# experimental: true
5353

5454
steps:
5555
- name: Checkout

.github/workflows/release.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ jobs:
99
ci:
1010
uses: ./.github/workflows/ci.yml
1111

12+
docs:
13+
name: Verify Docs Build
14+
uses: beeware/.github/.github/workflows/docs-build-verify.yml@main
15+
secrets: inherit
16+
with:
17+
project-name: "toga-chart"
18+
project-version: ${{ github.ref_name }}
19+
1220
release:
1321
name: Create Release
14-
needs: ci
22+
needs: [ ci, docs ]
1523
runs-on: ubuntu-latest
1624
permissions:
1725
contents: write
@@ -54,4 +62,4 @@ jobs:
5462
- name: Publish release to Test PyPI
5563
uses: pypa/gh-action-pypi-publish@release/v1
5664
with:
57-
repository_url: https://test.pypi.org/legacy/
65+
repository-url: https://test.pypi.org/legacy/

changes/24.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The requirements of ``toga-chart`` were modified so that toga-chart is only dependent on ``toga-core``, rather than the ``toga`` meta-package. This makes it possible to install ``toga-chart`` on Android, as the meta-package no longer attempts to install the ``toga-gtk`` backend on Android; but it requires that end-users explicitly specify ``toga`` or an explicit backend in their own app requirements.

examples/chart/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def startup(self):
4343
self.set_data()
4444

4545
# Set up main window
46-
self.main_window = toga.MainWindow(title=self.name)
46+
self.main_window = toga.MainWindow()
4747

4848
self.chart = toga_chart.Chart(style=Pack(flex=1), on_draw=self.draw_chart)
4949

setup.cfg

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers =
2525
license = New BSD
2626
license_files =
2727
LICENSE
28-
description = "A Toga matplotlib backend."
28+
description = A Toga matplotlib backend.
2929
long_description = file: README.rst
3030
long_description_content_type = text/x-rst
3131
keywords=
@@ -41,7 +41,7 @@ include_package_data = True
4141
package_dir=
4242
= src
4343
install_requires =
44-
toga >= 0.3.1
44+
toga-core >= 0.4.0
4545
matplotlib >= 3.0.3
4646

4747
[options.packages.find]
@@ -53,6 +53,7 @@ dev =
5353
pytest == 7.4.3
5454
setuptools_scm[toml] == 8.0.4
5555
tox == 4.11.3
56+
toga-dummy >= 0.4.0
5657
docs =
5758
furo == 2023.9.10
5859
pyenchant == 3.2.2

src/toga_chart/chart.py

Lines changed: 79 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import math
22
import sys
33

4-
from matplotlib.backend_bases import FigureCanvasBase, RendererBase
4+
from matplotlib.backend_bases import RendererBase
55
from matplotlib.figure import Figure
66
from matplotlib.path import Path
77
from matplotlib.transforms import Affine2D
@@ -12,36 +12,52 @@
1212

1313

1414
class Chart(Widget):
15-
"""Create new chart.
16-
17-
Args:
18-
id (str): An identifier for this widget.
19-
style (:obj:`Style`): An optional style object. If no
20-
style is provided then a new one will be created for the widget.
21-
on_resize (:obj:`callable`): Handler to invoke when the chart is resized.
22-
The default resize handler will draw the chart on every resize;
23-
generally, you won't need to override this default behavior.
24-
on_draw (:obj:`callable`): Handler to invoke when the chart needs to be
25-
drawn.
26-
factory (:obj:`module`): A python module that is capable to return a
27-
implementation of this class with the same name. (optional &
28-
normally not needed)
29-
"""
30-
31-
def __init__(self, id=None, style=None, on_resize=None, on_draw=None, factory=None):
15+
def __init__(
16+
self,
17+
id: str = None,
18+
style=None,
19+
on_resize: callable = None,
20+
on_draw: callable = None,
21+
):
22+
"""Create a new matplotlib chart.
23+
24+
:param id: An identifier for this widget.
25+
:param style: An optional style object. If no style is provided then a new one
26+
will be created for the widget.
27+
:param on_resize: Handler to invoke when the chart is resized. The default
28+
resize handler will draw the chart on every resize; generally, you won't
29+
need to override this default behavior.
30+
:param on_draw: Handler to invoke when the chart needs to be drawn. This
31+
performs the matplotlib drawing operations that will be displayed on the
32+
chart.
33+
"""
3234
self.on_draw = on_draw
3335
if on_resize is None:
3436
on_resize = self._on_resize
3537

36-
self.canvas = Canvas(style=style, on_resize=on_resize, factory=factory)
38+
# The Chart widget that the user interacts with is a subclass of Widget, not
39+
# Canvas; this subclass acts as a facade over the underlying Canvas
40+
# implementation (mostly so that the redraw() method of the Chart is independent
41+
# of the Canvas redraw() method). The _impl of the Chart is set to the Canvas
42+
# _impl so that functionally, the widget behaves as a Canvas.
43+
self.canvas = Canvas(style=style, on_resize=on_resize)
44+
45+
super().__init__(id=id, style=style)
3746

38-
super().__init__(id=id, style=style, factory=factory)
3947
self._impl = self.canvas._impl
4048

41-
def _set_app(self, app):
49+
@Widget.app.setter
50+
def app(self, app):
51+
# Invoke the superclass property setter
52+
Widget.app.fset(self, app)
53+
# Point the canvas to the same app
4254
self.canvas.app = app
4355

44-
def _set_window(self, window):
56+
@Widget.window.setter
57+
def window(self, window):
58+
# Invoke the superclass property setter
59+
Widget.window.fset(self, window)
60+
# Point the canvas to the same window
4561
self.canvas.window = window
4662

4763
@property
@@ -52,22 +68,19 @@ def layout(self):
5268
def layout(self, value):
5369
self.canvas.layout = value
5470

55-
def _draw(self, figure):
56-
"""Draws the matplotlib figure onto the canvas
71+
def _draw(self, figure: Figure):
72+
"""Draw the matplotlib figure onto the canvas.
5773
58-
Args:
59-
figure (figure): matplotlib figure to draw
74+
:param figure: The matplotlib figure to draw
6075
"""
6176
l, b, w, h = figure.bbox.bounds
62-
matplotlib_canvas = MatplotlibCanvasProxy(figure=figure, canvas=self.canvas)
63-
renderer = ChartRenderer(matplotlib_canvas, w, h)
77+
renderer = ChartRenderer(self.canvas, w, h)
6478

65-
# Invoke the on_draw handler (if present).
79+
# Invoke the on_draw handler.
6680
# This is where the user adds the matplotlib draw instructions
6781
# to construct the chart, so it needs to happen before the
6882
# figure is rendered onto the canvas.
69-
if self.on_draw:
70-
self.on_draw(self, figure=figure)
83+
self.on_draw(figure=figure)
7184

7285
figure.draw(renderer)
7386

@@ -79,66 +92,32 @@ def redraw(self):
7992
# 100 is the default DPI for figure at time of writing.
8093
dpi = 100
8194
figure = Figure(
82-
figsize=(self.layout.content_width / dpi, self.layout.content_height / dpi)
95+
figsize=(
96+
self.layout.content_width / dpi,
97+
self.layout.content_height / dpi,
98+
),
8399
)
84100
self._draw(figure)
85101

86102
@property
87-
def on_draw(self):
88-
"""The handler to invoke when the canvas needs to be drawn.
89-
90-
Returns:
91-
The handler that is invoked on canvas draw.
92-
"""
103+
def on_draw(self) -> callable:
104+
"""The handler to invoke when the canvas needs to be drawn."""
93105
return self._on_draw
94106

95107
@on_draw.setter
96-
def on_draw(self, handler):
97-
"""Set the handler to invoke when the canvas is drawn.
98-
99-
Args:
100-
handler (:obj:`callable`): The handler to invoke when the canvas is drawn.
101-
"""
108+
def on_draw(self, handler: callable):
102109
self._on_draw = wrapped_handler(self, handler)
103110

104111

105-
class MatplotlibCanvasProxy(FigureCanvasBase):
106-
def __init__(self, figure, canvas: Canvas):
107-
super().__init__(figure)
108-
self.canvas = canvas
109-
110-
def fill(self, color):
111-
return self.canvas.fill(color=color)
112-
113-
def stroke(self, color, line_width, line_dash):
114-
return self.canvas.stroke(
115-
color=color, line_width=line_width, line_dash=line_dash
116-
)
117-
118-
def measure_text(self, text, font):
119-
return self.canvas.measure_text(text=text, font=font)
120-
121-
def translate(self, tx, ty):
122-
return self.canvas.translate(tx, ty)
123-
124-
def rotate(self, radians):
125-
return self.canvas.rotate(radians)
126-
127-
def reset_transform(self):
128-
return self.canvas.reset_transform()
129-
130-
131112
class ChartRenderer(RendererBase):
132-
"""
133-
The renderer handles drawing/rendering operations.
134-
135-
Args:
136-
canvas (:obj:`Canvas`): canvas to render onto
137-
width (int): width of canvas
138-
height (int): height of canvas
139-
"""
113+
def __init__(self, canvas: Canvas, width: int, height: int):
114+
"""
115+
The matplotlib handler for drawing/rendering operations.
140116
141-
def __init__(self, canvas, width, height):
117+
:param canvas: The canvas to render onto
118+
:param width: Width of canvas
119+
:param height: height of canvas
120+
"""
142121
self.width = width
143122
self.height = height
144123
self._canvas = canvas
@@ -157,25 +136,30 @@ def draw_path(self, gc, path, transform, rgbFace=None):
157136
color = parse_color(rgba(r * 255, g * 255, b * 255, a))
158137

159138
if rgbFace is not None:
160-
stroke_fill_context = self._canvas.fill(color=color)
139+
stroke_fill_context = self._canvas.context.Fill(color=color)
161140
else:
162141
offset, sequence = gc.get_dashes()
163-
stroke_fill_context = self._canvas.stroke(
164-
color=color, line_width=gc.get_linewidth(), line_dash=sequence
142+
stroke_fill_context = self._canvas.context.Stroke(
143+
color=color,
144+
line_width=gc.get_linewidth(),
145+
line_dash=sequence,
165146
)
166147

167148
transform = transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height)
168149

169150
with stroke_fill_context as context:
170-
with context.context() as path_segments:
151+
with context.Context() as path_segments:
171152
for points, code in path.iter_segments(transform):
172153
if code == Path.MOVETO:
173154
path_segments.move_to(points[0], points[1])
174155
elif code == Path.LINETO:
175156
path_segments.line_to(points[0], points[1])
176157
elif code == Path.CURVE3:
177158
path_segments.quadratic_curve_to(
178-
points[0], points[1], points[2], points[3]
159+
points[0],
160+
points[1],
161+
points[2],
162+
points[3],
179163
)
180164
elif code == Path.CURVE4:
181165
path_segments.bezier_curve_to(
@@ -187,7 +171,7 @@ def draw_path(self, gc, path, transform, rgbFace=None):
187171
points[5],
188172
)
189173
elif code == Path.CLOSEPOLY:
190-
path_segments.closed_path(points[0], points[1])
174+
path_segments.ClosedPath(points[0], points[1])
191175

192176
def draw_image(self, gc, x, y, im):
193177
pass
@@ -217,12 +201,14 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
217201
gc.set_linewidth(0.75)
218202
self.draw_path(gc, path, transform, rgbFace=color)
219203
else:
220-
self._canvas.translate(x, y)
221-
self._canvas.rotate(-math.radians(angle))
222-
with self._canvas.fill(color=self.to_toga_color(*gc.get_rgb())) as fill:
204+
self._canvas.context.translate(x, y)
205+
self._canvas.context.rotate(-math.radians(angle))
206+
with self._canvas.context.Fill(
207+
color=self.to_toga_color(*gc.get_rgb())
208+
) as fill:
223209
font = self.get_font(prop)
224210
fill.write_text(s, x=0, y=0, font=font)
225-
self._canvas.reset_transform()
211+
self._canvas.context.reset_transform()
226212

227213
def flipy(self):
228214
return True
@@ -231,23 +217,16 @@ def get_canvas_width_height(self):
231217
return self.width, self.height
232218

233219
def get_text_width_height_descent(self, s, prop, ismath):
234-
"""
235-
get the width and height in display coords of the string s
236-
with FontPropertry prop
220+
"""Get the width and height in display coords of the string s
221+
with FontProperty prop
237222
"""
238223
font = self.get_font(prop)
239224
w, h = self._canvas.measure_text(s, font)
240225
return w, h, 1
241226

242227
def get_font(self, prop):
243-
if prop.get_family()[0] == SANS_SERIF:
244-
font_family = SANS_SERIF
245-
elif prop.get_family()[0] == CURSIVE:
246-
font_family = CURSIVE
247-
elif prop.get_family()[0] == FANTASY:
248-
font_family = FANTASY
249-
elif prop.get_family()[0] == MONOSPACE:
250-
font_family = MONOSPACE
228+
if prop.get_family()[0] in {SANS_SERIF, CURSIVE, FANTASY, MONOSPACE}:
229+
font_family = prop.get_family()[0]
251230
else:
252231
font_family = SERIF
253232

0 commit comments

Comments
 (0)