else/Datapoint/datapoint.py
Ethan Dalool 7fc972920f
Datapoint improvements.
- Chart title
- Can use different point color for each layer of points
- Prettier indentation
2021-03-25 14:22:59 -07:00

435 lines
13 KiB
Python

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('<Configure>', self.movereplot)
self.c = tkinter.Canvas(self.t)
self.c.pack(fill='both', expand=True)
self.c.bind('<Motion>', 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