Building a spider-chart with turtle or tkinter - python

I recently did 2 beginner courses in python coding at university for my Minor, which were followed by an intro to databases and a basic course about CRUD development in PHP. The python course was decent enough and touched on most of the basics. I've been trying to teach myself by making small applications to do things that I have to do for my remaining courses in linguistics.
Now I wanted to write a small python application and that takes user input to create a Language profile for that speaker. I thought I could do this using the turtle module of python, but after drawing the grid I found out I do not know how to get the points of the circles that i have drawn.
I wrote the following piece of code:
'''
for i in range(6):
t.circle(20*i,None, 16)
t.seth(90)
t.penup()[enter image description here][1]
t.backward(20)
t.pendown()
t.seth(0)
'''
I used 16 steps. because it needs to look something like this picture in the end :
Could anyone here help me along on how I can get these points on the circle mapped so that they can be drawn in after the user tells the program what his proficiency is for that point?
Please let me know if I left out anything important.
Sincerely,

You can construct a radar, or spider chart with tkinter.
The following class takes a list of tuples('label', score), with the score expressed as a percentage (value in the interval [0, 100]).
The number of data points adjusts to the data provided.
The scale of the chart can be adjusted.
import math
import tkinter as tk
class SpiderChart(tk.Canvas):
"""a canvas that displays datapoints as a SpiderChart
"""
width=500
height=500
def __init__(self, master, datapoints, concentrics=10, scale=200):
super().__init__(master, width=self.width, height=self.height)
self.scale = scale
self.center = self.width // 2, self.height // 2
self.labels = tuple(d[0] for d in datapoints)
self.values = tuple(d[1] for d in datapoints)
self.num_pts = len(self.labels)
self.concentrics = [n/(concentrics) for n in range(1, concentrics + 1)]
self.draw()
def position(self, x, y):
"""use +Y pointing up, and origin at center
"""
cx, cy = self.center
return x + cx, cy - y
def draw_circle_from_radius_center(self, radius):
rad = radius * self.scale
x0, y0 = self.position(-rad, rad)
x1, y1 = self.position(rad, -rad)
return self.create_oval(x0, y0, x1, y1, dash=(1, 3))
def draw_label(self, idx, label):
angle = idx * (2 * math.pi) / self.num_pts
d = self.concentrics[-1] * self.scale
x, y = d * math.cos(angle), d * math.sin(angle)
self.create_line(*self.center, *self.position(x, y), dash=(1, 3))
d *= 1.1
x, y = d * math.cos(angle), d * math.sin(angle)
self.create_text(*self.position(x, y), text=label)
def draw_polygon(self):
points = []
for idx, val in enumerate(self.values):
d = (val / 100) * self.scale
angle = idx * (2 * math.pi) / self.num_pts
x, y = d * math.cos(angle), d * math.sin(angle)
points.append(self.position(x, y))
self.create_polygon(points, fill='cyan')
def draw(self):
self.draw_polygon()
for concentric in self.concentrics:
self.draw_circle_from_radius_center(concentric)
for idx, label in enumerate(self.labels):
self.draw_label(idx, label)
data = [('stamina', 70), ('python-skill', 100), ('strength', 80), ('break-dance', 66), ('speed', 45), ('health', 72), ('healing', 90), ('energy', 12), ('libido', 100)]
root = tk.Tk()
spider = SpiderChart(root, data)
spider.pack(expand=True, fill=tk.BOTH)
root.mainloop()

Related

Solar System Simulator in Python, Ursina

I'm trying to do a Solar System simulator in python, with the Ursina engine, with physics. It works correctly until the earth (the only planet existing for the moment) gets in the same position on one or two axis than the sun. Then it just starts to shake and clipping and no-clipping of reality for no reason, following an straight line, usually the z axis.
Ursina's discord answer wasn't too helpful, since they lend me a code with that didn't had physics or elliptical orbits, which are the base of what I'm trying to do.
Here's the code:
from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController
from ursina.texture_importer import load_texture
import math
app = Ursina()
time_multiplicator = 1
G = 0.004
class Sun(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0),
model = "sphere",
color = color,
collider = "mesh",
scale = scale
)
self.mass = mass
self.attraction_active = True
sun = Sun(position = (25,5,5), color = color.yellow, scale = 10, mass = 1500)
class Planet(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0),
model = "sphere",
color = color,
scale = scale
)
self.mass = mass
self.attraction_active = True
self.initial_velocity = 0.4
def attraction(self):
self.gravitational_attraction = 1 + G * (self.mass * sun.mass)/(distance(sun,self)**2)
self.force_angle = 57.2958 * (math.atan((sun.y - self.y)/(sun.x - self.x))) + 1
self.y_component = self.gravitational_attraction * math.sin(self.force_angle) + 1
self.x_component = self.gravitational_attraction * math.cos(self.force_angle) + 1
print(f"gravitational_attraction ::: {self.gravitational_attraction}")
print(f"force_angle ::: {self.force_angle}")
print(f"y_component ::: {self.y_component}")
print(f"x_component ::: {self.x_component}")
def update(self):
self.attraction()
self.z -= self.initial_velocity * time.dt
self.y += self.y_component * time.dt
self.x += self.x_component * time.dt
blue = Planet(position = (0,0,0), color = color.blue, scale = 1, mass = 100)
print(distance(sun, blue))
EditorCamera()
def input(key):
if key == "q":
camera.look_at(blue)
app.run()
caveat: I just started tinkering with ursina ... pretty neat.
Your orig code had the earth getting sucked into the sun and the jitter was because it was near co-located. I printed the x,y coords and watched a bit.
You have a couple issues with the math and the physics.
You need to keep track of velocities, not just positions, unless I'm missing something with ursina. Recall that we can integrate to get velocities from force and position from velocity...
delta_v = F * dt
delta_pos = velocity * dt
Also, you need to use math.atan2 because it keeps track of both the x and y coordinate signage so that when things cross axes, you still get the correct sign of the angle.
It wasn't clear why you were adding "+1" to everything, so I removed it.
So after that, it is just a (non trivial) matter of putting an initial velocity on the earth so that the orbit is stable and doesn't get (a) sucked in, or (b) go flinging off into space due to lack of orbital capture. I tinkered with the velocities a bit and the below has a rotational orbit, but a weird one. I think you can tinker with the below and get to a working model.
from ursina import *
import sys
from ursina.prefabs.first_person_controller import FirstPersonController
from ursina.texture_importer import load_texture
import math
app = Ursina()
time_multiplicator = .1
G = 0.004
class Sun(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0,0),
model = "sphere",
color = color,
collider = "mesh",
scale = scale
)
self.mass = mass
self.attraction_active = True
sun = Sun(position = (25,15,0), color = color.yellow, scale = 10, mass = 1500)
class Planet(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0,0),
model = "sphere",
color = color,
scale = scale
)
self.mass = mass
self.attraction_active = True
self.velocity = [1, -3, 0] # vector vx, vy, vz
def attraction(self):
self.gravitational_attraction = G * (self.mass * sun.mass)/(distance(sun,self)**2)
self.force_angle = math.atan2( (-self.y + sun.y),(-self.x + sun.x))
self.y_component = self.gravitational_attraction * math.sin(self.force_angle)
self.x_component = self.gravitational_attraction * math.cos(self.force_angle)
print(f"gravitational_attraction ::: {self.gravitational_attraction}")
print(f"force_angle ::: {self.force_angle}")
print(f"y_component ::: {self.y_component}")
print(f"x_component ::: {self.x_component}")
print(f"x, y ::: {self.x}, {self.y}")
#if self.force_angle < 0: sys.exit(-1)
def update(self):
self.attraction()
# update the velocities, with update of F * dt
self.velocity[2] += 0 # no z velocity
self.velocity[1] += self.y_component * time.dt
self.velocity[0] += self.x_component * time.dt
# now update the positions with update = vel * dt
self.x += self.velocity[0]* time.dt
self.y += self.velocity[1]* time.dt
self.z += self.velocity[2]* time.dt
blue = Planet(position = (0,0,0), color = color.blue, scale = 1, mass = 100)
print(distance(sun, blue))
#EditorCamera()
# def input(key):
# if key == "q":
camera.position=(0,0,-200)

Exclude results in find_closest (Tkinter, Python)

I am writing a script to store movements over a hexgrid using Tkinter. As part of this I want to use a mouse-click on a Tkinter canvas to first identify the click location, and then draw a line between this point and the location previously clicked.
Generally this works, except that after I've drawn a line, it become an object that qualifies for future calls off the find_closest method. This means I can still draw lines between points, but selecting the underlying Hex in the Hexgrid over times becomes nearly impossible. I was wondering if someone could help me find a solution to exclude particular objects (lines) from the find_closest method.
edit: I hope this code example is minimal enough.
import tkinter
from tkinter import *
from math import radians, cos, sin, sqrt
class App:
def __init__(self, parent):
self.parent = parent
self.c1 = Canvas(self.parent, width=int(1.5*340), height=int(1.5*270), bg='white')
self.c1.grid(column=0, row=0, sticky='nsew')
self.clickcount = 0
self.clicks = [(0,0)]
self.startx = int(20*1.5)
self.starty = int(20*1.5)
self.radius = int(20*1.5) # length of a side
self.hexagons = []
self.columns = 10
self.initGrid(self.startx, self.starty, self.radius, self.columns)
self.c1.bind("<Button-1>", self.click)
def initGrid(self, x, y, radius, cols):
"""
2d grid of hexagons
"""
radius = radius
column = 0
for j in range(cols):
startx = x
starty = y
for i in range(6):
breadth = column * (1.5 * radius)
if column % 2 == 0:
offset = 0
else:
offset = radius * sqrt(3) / 2
self.draw(startx + breadth, starty + offset, radius)
starty = starty + 2 * (radius * sqrt(3) / 2)
column = column + 1
def draw(self, x, y, radius):
start_x = x
start_y = y
angle = 60
coords = []
for i in range(6):
end_x = start_x + radius * cos(radians(angle * i))
end_y = start_y + radius * sin(radians(angle * i))
coords.append([start_x, start_y])
start_x = end_x
start_y = end_y
hex = self.c1.create_polygon(coords[0][0], coords[0][1], coords[1][0], coords[1][1], coords[2][0],
coords[2][1], coords[3][0], coords[3][1], coords[4][0], coords[4][1],
coords[5][0], coords[5][1], fill='black')
self.hexagons.append(hex)
def click(self, evt):
self.clickcount = self.clickcount + 1
x, y = evt.x, evt.y
tuple_alfa = (evt.x, evt.y)
self.clicks.append(tuple_alfa)
if self.clickcount >= 2:
start = self.clicks[self.clickcount - 1]
startx = start[0]
starty = start[1]
self.c1.create_line(evt.x, evt.y, startx, starty, fill='white')
clicked = self.c1.find_closest(x, y)[0]
print(clicked)
root = tkinter.Tk()
App(root)
root.mainloop()

Misalignment of triangles drawn with tkinter canvas

I wrote this function that draw a grid of triangles:
def create_triangles(side_length):
result = []
half_width = int(side_length / 2)
# height = int(side_length * math.sqrt(3) / 2)
height = side_length
max_width = 15 * side_length
max_height = 10 * height
for i in range(0, max_height, height):
if (i / height) % 2 == 0:
for j in range(0, max_width-half_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-half_width, i+height/2, j, i-height/2, j+half_width)
else:
triangle = (i-height/2, j, i+height/2, j+half_width, i+height/2, j-half_width)
result.append(triangle)
else:
for j in range(half_width, max_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-2*half_width, i+height/2, j-half_width+2, i-height/2, j)
else:
triangle = (i-height/2, j-half_width, i+height/2, j, i+height/2, j-2*half_width)
result.append(triangle)
return result
The current output is this:
As you can see some triangles are misaligned but I don't understand why.
As mentioned in the comments, floating points give you incorrect results; You want to make sure that the shared points representing the vertices of two adjacent triangles are concurrent. A simple approach is to reduce the points coordinates to ints, and organize the calculations so errors do not add up.
In the following examples, the misalignment is corrected, every triangle on the canvas is represented by a polygon, and individually drawn; each triangle can therefore be referenced when moused over, or addressed via an index, or a mapping (not implemented).
import tkinter as tk
import math
WIDTH, HEIGHT = 500, 500
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def tile_with_triangles(canvas, side_length=50):
"""tiles the entire surface of the canvas with triangular polygons
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='red')
p2 = Point(-triangle_height, half_side) # flip the model triangle
for idx, x in enumerate(range(-triangle_height, WIDTH+triangle_height+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='blue')
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg='cyan')
canvas.pack()
tile_with_triangles(canvas) #, side_length=10)
root.mainloop()
I added an active fill property that will change the colors of each triangle when you mouse over.

How to Create a Rainbow Triangle Tessellation

I'm attempting to create a triangle tessellation like the following in Python:
All I've gotten is Sierpensky's triangle. I assume it'd use some of the same code.
import turtle as t
import math
import colorsys
t.hideturtle()
t.speed(0)
t.tracer(0,0)
h = 0
def draw_tri(x,y,size):
global h
t.up()
t.goto(x,y)
t.seth(0)
t.down()
color = colorsys.hsv_to_rgb(h,1,1)
h += 0.1
t.color(color)
t.left(120)
t.fd(size)
t.left(120)
t.fd(size)
t.end_fill()
def draw_s(x,y,size,n):
if n == 0:
draw_tri(x,y,size)
return
draw_s(x,y,size/2,n-1)
draw_s(x+size/2,y,size/2,n-1)
draw_s(x+size/4,y+size*math.sqrt(3)/4,size/2,n-1)
draw_s(-300,-250,600,6)
t.update()
There are various approaches; the following example generates all line segments prior to directing the turtle to draw them on the canvas.
import turtle as t
import math
WIDTH, HEIGHT = 800, 800
OFFSET = -WIDTH // 2, -HEIGHT // 2
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def get_line_segments(side_length=50):
"""calculates the coordinates of all vertices
organizes them by line segment
stores the segments in a container and returns it
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
segments = []
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
segments += [[pa, pb], [pb, pc], [pc, pa]]
return segments
def draw_segment(segment):
p0, p1 = segment
p0, p1 = p0 + offset, p1 + offset
t.penup()
t.goto(p0)
t.pendown()
t.goto(p1)
def draw_tiling():
for segment in get_line_segments():
draw_segment(segment)
t.hideturtle()
t.speed(0)
t.tracer(0,0)
offset = Point(*OFFSET)
draw_tiling()
t.update()
t.exitonclick()
If you want to see how the tiling is traced, you can replace the following lines:
# t.hideturtle()
t.speed(1)
# t.tracer(0, 0)
and enlarge the canvas screen with your mouse to see the boundary of the tiling (I made it overlap the standard size of the window)
As #ReblochonMasque notes, there are multiple approaches to the problem. Here's one I worked out to use as little turtle code as possible to solve the problem:
from turtle import Screen, Turtle
TRIANGLE_SIDE = 60
TRIANGLE_HEIGHT = TRIANGLE_SIDE * 3 ** 0.5 / 2
CURSOR_SIZE = 20
screen = Screen()
width = TRIANGLE_SIDE * (screen.window_width() // TRIANGLE_SIDE)
height = TRIANGLE_HEIGHT * (screen.window_height() // TRIANGLE_HEIGHT)
diagonal = width + height
turtle = Turtle('square', visible=False)
turtle.shapesize(diagonal / CURSOR_SIZE, 1 / CURSOR_SIZE)
turtle.penup()
turtle.sety(height/2)
turtle.setheading(270)
turtle = turtle.clone()
turtle.setx(width/2)
turtle.setheading(210)
turtle = turtle.clone()
turtle.setx(-width/2)
turtle.setheading(330)
for _ in range(int(diagonal / TRIANGLE_HEIGHT)):
for turtle in screen.turtles():
turtle.forward(TRIANGLE_HEIGHT)
turtle.stamp()
screen.exitonclick()
It probably could use optimizing but it gets the job done. And it's fun to watch...

Python - Adding a Tkinter Graph to a PyQt Widget

We currently have a fully functional Gui written created using PyQt. My partner wrote a function that graphs a dataSet in Tkinter. My question is, how do we combine the two so they work together?
Here is the graphing function:
def createGraph(self):
import tkinter as tk
# Send in data as param, OR
#data = [17, 20, 15, 10, 7, 5, 4, 3, 2, 1, 1, 0]
# Recieve data within function
s.send("loadgraph")
inputString = repr(s.recv(MSGSIZE))
#inputString = "-20 15 10 7 5 -4 3 2 1 1 0"
print(inputString)
data = [int(x) for x in inputString.split()]
root = tk.Tk()
root.title("FSPwners")
screen_width = 400
screen_height = 700
screen = tk.Canvas(root, width=screen_width, height=screen_height, bg= 'white')
screen.pack()
# highest y = max_data_value * y_stretch
y_stretch = 15
# gap between lower canvas edge and x axis
y_gap = 350
# stretch enough to get all data items in
x_stretch = 10
x_width = 20
# gap between left canvas edge and y axis
x_gap = 20
for x, y in enumerate(data):
# calculate reactangle coordinates (integers) for each bar
x0 = x * x_stretch + x * x_width + x_gap
y0 = screen_height - (y * y_stretch + y_gap)
x1 = x * x_stretch + x * x_width + x_width + x_gap
y1 = screen_height - y_gap
# draw the bar
print(x0, y0, x1, y1)
if y < 0:
screen.create_rectangle(x0, y0, x1, y1, fill="red")
else:
screen.create_rectangle(x0, y0, x1, y1, fill="green")
# put the y value above each bar
screen.create_text(x0+2, y0, anchor=tk.SW, text=str(y))
root.mainloop()
When that method is run by itself, it creates a popup box with the graph. Now we want it to create a popup graph when a button is pressed in our current gui. How can we get it to work? If we just call createGraph() when a button is clicked in our GUI, we get the error:
unrecognized selector sent to instance x009x...
What is the problem? Thanks!
Here's a PyQt port:
from PyQt4 import QtCore, QtGui
class Graph(QtGui.QWidget):
def __init__(self, data, parent=None):
QtGui.QWidget.__init__(self, parent)
self._data = data
self.resize(400, 700)
self.setWindowTitle('FSPwners')
self.setAutoFillBackground(True)
self.setBackgroundRole(QtGui.QPalette.Base)
def paintEvent(self, event):
painter = QtGui.QPainter()
painter.begin(self)
screen_width = self.width()
screen_height = self.height()
# highest y = max_data_value * y_stretch
y_stretch = 15
# gap between lower canvas edge and x axis
y_gap = 350
# stretch enough to get all data items in
x_stretch = 10
x_width = 20
# gap between left canvas edge and y axis
x_gap = 20
for x, y in enumerate(self._data):
# calculate reactangle coordinates (integers) for each bar
x0 = x * x_stretch + x * x_width + x_gap
y0 = screen_height - (y * y_stretch + y_gap)
x1 = x0 + x_width
y1 = screen_height - y_gap
if y < 0:
painter.setBrush(QtCore.Qt.red)
else:
painter.setBrush(QtCore.Qt.green)
painter.drawRect(QtCore.QRectF(
QtCore.QPointF(x0, y0), QtCore.QPointF(x1, y1)))
print (x0, y0, x1, y1)
# put the y value above each bar
painter.drawText(x0 + 2, y0 - 2, str(y))
painter.end()
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
# data to be graphed
data = [-20, 15, 10, 7, 5, -4, 3, 2, 1, 1, 0]
window = Graph(data)
window.show()
sys.exit(app.exec_())
Qt and Tkinter don't play along quite well, as you can perceive -
I had once played along Python graphical toolkits, and wrote
a 4 operation calculator that would run in either Qt, GTK or Tkinter -
or even display all at once.
In order to have both the Tkinter and Qt versions working simultaneously,
I had to fork the process - and start each toolkit in a separate
running instance;
Your case is not identical, as the Qt GUI will be already running, but maybe
having this to start up with you can come along with a work-around.
The 3-calculators code listing can be found here:
https://web.archive.org/web/20101122232402/http://www.python.org.br/wiki/CalculadoraTkGtkQt

Categories

Resources