import time import tkinter class DataPoint: def __init__( self, width=720, height=480, autostart=False, title=None, ): self.title = title self.windowtitle = 'DataPoint' self.color_outbound = '#999' self.color_crossbar = '#bbb' self.color_point = '#000' self.crossbar_count = 10 self.point_diameter = 4 self.margin = 0.10 self.origin = (0, 0) self._started = False self.t = tkinter.Tk() self.t.title(self.windowtitle) self.w = width self.h = height self.windowx = int((self.screen_width-self.w) / 2) self.windowy = int(((self.screen_height-self.h) / 2) - 27) self.geometrystring = f'{self.w}x{self.h}+{self.windowx}+{self.windowy}' self.t.geometry(self.geometrystring) self.countdown = -1 self.lastbump = 0 self.resized = True self.t.configure(bg='#f00') self.t.bind('', self.movereplot) self.c = tkinter.Canvas(self.t) self.c.pack(fill='both', expand=True) self.c.bind('', self.draw_coordinateslabel) self.reset() self.previous_w = self.w self.previous_h = self.h self.previous_x = self.windowx self.previous_y = self.windowy self.clear_screen() self.draw_margin() self._started = autostart @property def screen_width(self): return self.t.winfo_screenwidth() @property def screen_height(self): return self.t.winfo_screenheight() @property def window_width(self): if not self._started: return self.w return self.t.winfo_width() @property def window_height(self): if not self._started: return self.h return self.t.winfo_height() @property def margin_x(self): return self.window_width * self.margin @property def margin_y(self): return self.window_height * self.margin def mainloop(self): self._started = True self.t.mainloop() def movereplot(self, *b): ''' When the user expands the window, replot the graph after a short delay. ''' previous = (self.previous_w, self.previous_h, self.previous_x, self.previous_y) current = (self.window_width, self.window_height, self.t.winfo_x(), self.t.winfo_y()) now = time.time() if now - self.lastbump < 0.2: # Go away. return if previous != current: # Set. self.previous_w = current[0] self.previous_h = current[1] self.previous_x = current[2] self.previous_y = current[3] self.countdown = 1 self.lastbump = now if previous[:2] != current[:2]: self.resized = False self.t.after(500, self.movereplot) return if self.countdown > -1: # Count. self.countdown -= 1 self.lastbump = now self.t.after(500, self.movereplot) if self.countdown == 0: # Plot. if not self.resized: self.redraw() self.resized = True return def reset(self): ''' Set the DataPoint's grid attributes back to None. ''' self.total_points = set() self.point_layers = [] self.graph_layer = 0 self.lowest_x = None self.highest_x = None self.lowest_y = None self.highest_y = None self.span_x = None self.span_y = None self.drawable_w = None self.drawable_h = None self.clear_screen() self.draw_margin() def meow(self): return 'meow.' def clear_screen(self): ''' Delete all canvas elements. ''' self.c.delete('all') def draw_axes(self, x, y): ''' Given the x, y pixel coordinates, draw some axes there. ''' self.c.create_line(0, y, self.screen_width*2, y) self.c.create_line(x, 0, x, self.screen_height*2) def draw_margin(self): ''' Draw the dark margin. ''' self.c.create_rectangle( 0, 0, self.window_width, self.window_height, fill=self.color_outbound, ) self.c.create_rectangle( self.margin_x, self.margin_y, self.window_width - self.margin_x, self.window_height - self.margin_y, fill='SystemButtonFace', ) self.coordinateslabel = self.c.create_text( 8, 8, text='xy', anchor='nw', font=('Consolas', 10), ) def draw_title(self): if not self.title: return self.c.create_text( int(self.window_width / 2), int(self.margin_y / 2), anchor='center', text=self.title, font=('Consolas', 20), ) def draw_labels(self): ''' Draw the text labels along the axes. ''' if len(self.total_points) == 0: return if len(self.total_points) == 1: p = next(iter(self.total_points)) if p == self.origin: return lp = self.transform_coord(*p) self.c.create_text(lp[0], lp[1]+10, text=str(p)) return low = self.transform_coord(self.lowest_x, self.lowest_y) low_x = low[0] low_y = low[1] hi = self.transform_coord(self.highest_x, self.highest_y) hi_x = hi[0] hi_y = hi[1] if self.crossbar_count < 1: self.crossbar_count = 1 if self.highest_x != self.lowest_x: # LOW X self.c.create_text( low_x+5, low_y+5, text=str(round(self.lowest_x, 4)), anchor='nw', ) # FAR X self.c.create_text( hi_x+5, low_y+5, text=str(round(self.highest_x, 4)), anchor='nw', ) increment_x = (self.highest_x - self.lowest_x) / self.crossbar_count crossbartop = self.margin_y crossbarbot = self.window_height - self.margin_y for x in range(1, self.crossbar_count): x = (x * increment_x) + self.lowest_x p = self.transform_coord(x, self.lowest_y) self.c.create_line( p[0], crossbartop, p[0], crossbarbot, fill=self.color_crossbar, ) x = str(round(x, 3)) self.c.create_text(p[0], low_y+5, text=x, anchor='n') if self.highest_y != self.lowest_y: # LOW Y self.c.create_text( low_x-5, low_y, text=str(round(self.lowest_y, 4)), anchor='se', ) # UPPER Y self.c.create_text( low_x-5, hi_y, text=str(round(self.highest_y, 4)), anchor='e', ) increment_y = (self.highest_y - self.lowest_y) / self.crossbar_count crossbarlef = self.margin_x crossbarrgt = self.window_width - self.margin_x for y in range(1, self.crossbar_count): y = (y * increment_y) + self.lowest_y p = self.transform_coord(self.lowest_x, y) self.c.create_line( crossbarlef, p[1], crossbarrgt, p[1], fill=self.color_crossbar, ) y = str(round(y, 3)) self.c.create_text(low_x-5, p[1], text=y, anchor='e') def draw_coordinateslabel(self, event): if len(self.total_points) < 2: return l = self.transform_coord(event.x, event.y, reverse=True) l = '%03.12f, %03.12f' % l self.c.itemconfigure(self.coordinateslabel, text=l) def transform_coord(self, x, y, reverse=False): ''' Given an x,y coordinate for a point, return the screen coordinates or vice-versa. ''' if not reverse: if self.highest_x == self.lowest_x: x = self.window_width / 2 else: # Get percentage of the span x = ((x) - self.lowest_x) / self.span_x # Use the percentage to get a location on the board x *= self.drawable_w # Put into drawing area x += self.margin_x if self.highest_y == self.lowest_y: y = self.window_height / 2 else: y = ((y) - self.lowest_y) / self.span_y # Flip y y = 1 - y y *= self.drawable_h y += self.margin_y else: if self.highest_x != self.lowest_x: x -= self.margin_x x /= self.drawable_w x = (x * self.span_x) + self.lowest_x else: x = self.lowest_x if self.highest_y != self.lowest_y: y -= self.margin_y y /= self.drawable_h y = 1 - y y = (y * self.span_y) + self.lowest_y else: y = self.lowest_y return (x, y) def plot_points(self, points, point_color=None): ''' Plot points onto the canvas. var points = list, where each element is a 2-length tuple, where [0] is x and [1] is y coordinate. ''' if point_color is None: point_color = self.color_point layer_points = set() for point in points: point = tuple(point) self.total_points.add(point) layer_points.add(point) self.point_layers.append((layer_points, point_color)) self.redraw() def redraw(self): self.clear_screen() self.draw_margin() self.draw_title() xs = [point[0] for point in self.total_points] ys = [point[1] for point in self.total_points] self.lowest_x = min(xs) self.highest_x = max(xs) self.lowest_y = min(ys) self.highest_y = max(ys) self.span_x = abs(self.highest_x - self.lowest_x) self.span_x = max(1, self.span_x) self.span_y = abs(self.highest_y - self.lowest_y) self.span_y = max(1, self.span_y) self.drawable_w = self.window_width - (2 * self.margin_x) self.drawable_h = self.window_height - (2 * self.margin_y) self.draw_labels() if len(self.total_points) > 1 or self.origin in self.total_points: p = self.transform_coord(*self.origin) self.draw_axes(*p) for (layer_points, point_color) in self.point_layers: for point in layer_points: p = self.transform_coord(point[0], point[1]) x = p[0] y = p[1] r = self.point_diameter / 2 self.c.create_oval( x-r, y-r, x+r, y+r, fill=point_color, outline=point_color, ) self.c.update() def set_origin(self, x, y): self.origin = (x, y) self.redraw() def example(function): dp = DataPoint() points = list(range(100)) points = [[p, function(p)] for p in points] dp.plot_points(points) dp.mainloop() def example2(): dp = DataPoint() points = [ (1, 2), (2, 20), (3, 2), (4, 4), (5, 1), (6, 1), (7, 3), (8, 1), (9, 1), (10, 1), (11, 1), (12, 2), (13, 5), (14, 306), (15, 60), (16, 543), (17, 225), (18, 616), (19, 1546), (20, 1523), (21, 1578), (22, 1423), (23, 1257), (24, 1612), (25, 1891), (26, 2147), (27, 2154), (28, 2286), (29, 2411), (30, 2412), (31, 2382), (32, 2954), (33, 3051), (34, 3240), (35, 3794), (36, 2762), (37, 2090), (38, 2424), (39, 3448), (40, 4039), (41, 4115), (42, 3885), (43, 3841), (44, 4563), (45, 4974), (46, 1816), (47, 1631), (48, 1924), (49, 2024), (50, 2381), (51, 2253), (52, 2579), (53, 2713), (54, 3151), (55, 3380), (56, 4144), (57, 5685), (58, 5373), (59, 5571), (60, 5383), (61, 5967), (62, 8577), (63, 8196), (64, 8120), (65, 8722), (66, 8752), (67, 9841), (68, 10929), (69, 12585), (70, 11963), (71, 12632), (72, 11186), (73, 11122), (74, 13547), (75, 13376), (76, 13253), (77, 15094), (78, 14401), (79, 14577), (80, 15264), (81, 14621), (82, 13479), (83, 14028), (84, 14514), (85, 15345), (86, 23059), (87, 26502), (88, 23460), (89, 19223), (90, 19972), (91, 17815), (92, 21154), (93, 22606), (94, 22320), (95, 23703), (96, 40752), (97, 21730), (98, 27637), (99, 45931), (100, 18443), (101, 20048), (102, 18097), (103, 11430) ] dp.plot_points(points) dp.mainloop() def examplefunction(x): x -= 50 x *= 0.1 y = 1 / (1 + (2.718 ** -x)) return y def examplefunction2(x): return x ** 2