-
Notifications
You must be signed in to change notification settings - Fork 0
/
plotter.py
335 lines (275 loc) · 13.3 KB
/
plotter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
"""
This module provides controlled access to tkinter canvases for plotting
points or functions (of the form y = f(x)) using Cartesian coordinates.
Function:
- plot(): Creates a window with a title bar and a drawing canvas. Returns
a dict of seven functions that provide access to the canvas for:
- plotting a point,
- plotting a series of points,
- plotting functions of the form y = f(x),
- drawing x and y axes,
- adding text,
- destroying the window, and
- blocking (pausing execution)
"""
import tkinter
# Default settings
DEFAULT_CANV_WIDTH = 650
DEFAULT_CANV_HEIGHT = 650
MIN_CANV_WIDTH = 100
MIN_CANV_HEIGHT = 100
DEFAULT_SCALE_X = 40
DEFAULT_SCALE_Y = 40
DEFAULT_BACKGROUND = 'mint cream'
def plot(title='Plot',
canv_width=DEFAULT_CANV_WIDTH,
canv_height=DEFAULT_CANV_HEIGHT,
origin_x=DEFAULT_CANV_WIDTH//2,
origin_y=DEFAULT_CANV_HEIGHT//2,
scale_x=DEFAULT_SCALE_X,
scale_y=DEFAULT_SCALE_Y,
bg=None):
"""Creates a window consisting of a title bar and a blank tkinter canvas.
Returns a dict of functions that give the programmer controlled access to
the canvas and its window.
Parameters (all optional):
title (defaults to 'Plot') - Becomes the title showing in the window's
title bar.
canv_width (defaults to DEFAULT_CANV_WIDTH) - The intended width of the
canvas in pixels.
canv_height (defaults to DEFAULT_CANV_HEIGHT) - The intended height of
the canvas in pixels.
origin_x (defaults to the horizontal middle of the canvas) -
The intended horizontal position of 0. origin_x is set realtive to
the left edge of the canvas.
origin_y (defaults to the vertical middle of the canvas) -
The intended vertical position of 0. origin_y is set relative to
the bottom edge of the canvas.
Note about origin_x and origin_y: In the tkinter coordinate system,
position (x=0,y=0) (the origin) is fixed at the top left corner of a
canvas, and y coordinate values increase downwards. This means, for
example, that the pixel at (x=10,y=8) is directly below the pixel at
(x=10,y=7). This function, however, has y coordinate values growing
upwards and allows the origin to be placed arbitrarily.
scale_x (defaults to DEFAULT_SCALE_X) - Determines the number
pixels that represent a unit measurement in the x (horizontal)
direction. This value is allowed to be fractional, e.g., 0.25, but
not 0 or negative.
scale_y (defaults to DEFAULT_SCALE_Y) - Determines the number
pixels that represent a unit measurement in the y (vertical)
direction. This value is allowed to be fractional, e.g., 0.25, but
not 0 or negative.
bg (defaults to None) - Determines the background colour of the
canvas. A setting of None produces tkinter's default canvas colour.
Returned value:
A dict of functions that interact with the canvas
or its encompassing window. The keys of the dict indicate the purposes
of their corresponding functions.
"""
# Checking for invlaid parameters.
if canv_width <= MIN_CANV_WIDTH or canv_height <= MIN_CANV_HEIGHT:
raise ValueError('Specified canvas is too small.')
if scale_x <= 0.0:
raise ValueError('scale_x set less than or equal to 0.0.')
if scale_y <= 0.0:
raise ValueError('scale_y set less than or equal to 0.0.')
master = tkinter.Tk()
master.title(title)
canv = tkinter.Canvas(master,
width=canv_width+1,
height=canv_height+1,
bd=0,
highlightthickness=0,
background=bg)
canv.config()
canv.pack(expand=tkinter.YES, fill=tkinter.BOTH)
# Move plot window to foreground.
master.lift()
master.attributes('-topmost',True)
master.after_idle(master.attributes,'-topmost',False)
canv.update()
min_x = -origin_x
max_x = canv_width - origin_x
min_y = -origin_y
max_y = canv_height - origin_y
def get_x(x_val):
"""Returns x_val mapped into the tkinter coordinate system."""
return x_val + origin_x
def get_y(y_val):
"""Returns y_val mapped into the tkinter coordinate system."""
return canv_height - y_val - origin_y
def plot_point(x=0, y=0, diam=2, colour='black'):
"""Puts a dot representing a point on the canvas.
Parameters (all optional):
x (defaults to 0) - The horizontal position.
y (defaults to 0) - The vertical position.
diam (defaults to 2) - The diameter of the dot.
colour (defaults to 'black') - The colour of the dot.
"""
x = get_x(x*scale_x) - diam//2
y = get_y(y*scale_y) - diam//2
canv.create_oval(x, y, x+diam, y+diam, outline=colour, fill=colour)
def plot_function(fn, point_diam=2, colour='black'):
"""Draws a function of the form y = f(x) on the canvas.
Parameters:
fn - the function (Note: NOT a string; the corresponding argument
should be a function identifier and not appear in quotes.)
point_diam (optional, defaults to 2) - the size of each plotted
dot used to draw the function.
colour (optional, defaults to 'black') - the colour of each
plotted dot.
"""
for screen_x in range(min_x, max_x+1):
x = screen_x / scale_x
# The try-except, below, ensures that nothing gets plotted when
# a call to fn(x) triggers an exception. This allows functions
# with limited ranges to get plotted.
try:
plot_point(x,fn(x),diam=point_diam,colour=colour)
except:
pass
def draw_axes(line_width=1, colour='black', tick_length=0,
tick_interval_x=1, tick_interval_y=1):
"""Draws x and y axes so that they intersect at the origin. Adds ticks
at regular intervals, optionally.
Parameters (all optional):
line_width (defaults to 1) - The width of the axis lines in pixels.
colour (defaults to 'black') - The colour used to draw the axes
tick_length (defaults to 0) - The length of tick marks in pixels.
A value of 0 causes tick marks to be omitted. Positive values
cause tick marks to appear along the top of the x axis and on
the right of the y axis; negative values cause tick marks to
appear along the bottom of the x axis and on the left of the
y axis.
tick_interval_x (defaults to 1) - The number of units separating
ticks in the x (horizontal) direction. Non-integer values
(like math.pi) are allowed. Ignored if tick_length is 0.
tick_interval_y (defaults to 1) - The number of units separating
ticks in the y (vertical) direction. Non-integer values
are allowed. Ignored if tick_length is 0.
"""
# Draw x axis
canv.create_line(0, get_y(0), canv_width+1, get_y(0), fill=colour)
# Draw y axis
canv.create_line(get_x(0), canv_height, get_x(0), -1, fill=colour)
# Draw ticks
if tick_length != 0:
# Check for and reject invalid tick intervals
if tick_interval_x < 1:
raise ValueError('Inappropriate tick_interval_x value.')
if tick_interval_y < 1:
raise ValueError('Inappropriate tick_interval_y value.')
x = tick_interval_x
start_tick = int(tick_length // abs(tick_length))
end_tick = start_tick * abs(tick_length) + start_tick
while get_x(x * scale_x) < canv_width:
# positive x tick
canv.create_line(get_x(x * scale_x),
get_y(start_tick),
get_x(x * scale_x),
get_y(end_tick),
fill=colour)
# negative x tick
canv.create_line(get_x(-x * scale_x),
get_y(start_tick),
get_x(-x * scale_x),
get_y(end_tick),
fill=colour)
x += tick_interval_x
y = tick_interval_y
while get_y(y * scale_y) > 0: # Remember that y grows down.
# positive y tick
canv.create_line(get_x(start_tick),
get_y(y * scale_y),
get_x(end_tick),
get_y(y * scale_y),
fill=colour)
# negative y tick
canv.create_line(get_x(start_tick),
get_y(-y * scale_y),
get_x(end_tick),
get_y(-y * scale_y),
fill=colour)
y += tick_interval_y
def put_text(msg, x=0, y=0, size=None, colour='black'):
"""Draws a text message using the default tkinter font onto the canvas.
Parameters:
msg - the message.
x (optional, default 0) - The x (horizontal) position of the
left edge of the text.
y (optional, default 0) - The y (vertical) position of the
vertical middle of the text.
size (optional, default None) - The point size of the font used
for the message. If None, the system default size is used.
colour (optional, default 'black') - The colour of the font used
for the message.
"""
if size is None: # no change (the initial size is system dependent)
canv.create_text(get_x(x * scale_x), get_y(y * scale_y),
text=str(msg),
fill=colour,
anchor='w')
else:
canv.create_text(get_x(x * scale_x), get_y(y * scale_y),
text=str(msg),
font=('TkDefaultFont',size),
fill=colour,
anchor='w')
canv.pack()
def destroy():
"""Destroys the plotter window. Not needed if it is intended that the
user close the window in some other way (e.g., with a mouse click on
the title bar's close window icon).
"""
master.destroy()
def block():
"""Blocks further execution until the window is closed."""
master.mainloop()
return {
'draw_axes': draw_axes,
'plot_point': plot_point,
'plot_function': plot_function,
'put_text': put_text,
'destroy': destroy,
'block': block
}
def main():
"""Test function for module."""
# Draw an empty plot and get functions that allow us to add elements to it.
plot_1 = plot(title='Test Plot', bg='bisque')
#from math import * # Since I'll be using some math library functions
import math
# Plot some 10-pixel diameter coloured points
plot_1['plot_point'](math.pi,0,10,'yellow')
plot_1['plot_point'](math.pi/2,0,10,'red')
plot_1['plot_point'](0,1,10,'blue')
# Draw the axes. These will be drawn over the coloured points.
plot_1['draw_axes'](tick_length=-4,tick_interval_x=math.pi)
plot_1['plot_function'](math.sin,colour='green')
plot_1['plot_function'](math.cos,colour='purple')
# Plotting a couple of "anonymous" functions
plot_1['plot_function'](lambda x: 2 * (x-2)**2 - 3*x + 1,colour='navy')
plot_1['plot_function'](lambda x: -4 * x**3 - 4,colour='deep pink')
plot_1['put_text']('Various points and functions',x=-7.5, y=-3, size=16, \
colour='green')
plot_1['block']() # User must close the canvas window to continue.
# Plot a couple of anonymous functions on a different canvas
plot_2 = plot(title='2nd Test Plot',
origin_x = 15,
origin_y = 15,
scale_x=10,
scale_y=10,
bg='thistle1')
plot_2['draw_axes'](tick_length=4,tick_interval_x=5, tick_interval_y=2,\
colour="orange")
plot_2['plot_function'](lambda x: x**2 if x >= 0 else None,\
colour='green')
plot_2['put_text']('y = x**2, for x >= 0', 40, 100, size=14,\
colour='green')
plot_2['plot_function'](lambda x: math.log(x, 2) if x >= 1 else None,\
colour='blue')
plot_2['put_text']('log(x, 10) for x > 0', 100, 60,size=9,colour='blue')
plot_2['put_text']('This is a test using put_text()\'s defaults.')
plot_2['block']() # Module exits when user closes the canvas window.
if __name__ == '__main__':
main()