3D ploter in python-tk with matplotlib.

Wishing to prove my assertion wrong on python-tk that piping python directly into tk/tcl interpreter is simple I tried to contradict myself by making a full GUI in matplotlib. Because, if you are not aware : matplotlib supports multi-target (Wx, Qt, gtk, tk, html) multi-platform widgets (Button, checkbox, text entry and so much more).

The challenge seemed pretty easy, to assemble an easy demo with simple example : Thus, the added value resided in letting people fill in the min/max/step information for the relevant dimensions.

Without the colorbar I would have just been slightly annoyed by the slowness of the reaction of matplotlib as a GUI, but the colorbar posed a new challenge because it would either stack for each drawing or make plt.clf/ax.cla erase too much (see this great resource on when to use cla/clf in matplotlib).

So ... I tried python-tk with matplotlib knowing all too well that you can embed natively matplotlib in tkinter interface.

And since it was working I kept it.

Here is a screenshot of the interface :
WARNING this code should not be let in inventive hands (such as bored teenagers) because there is an evil eval; it requires to be in care of consenting adults.

Some highlights of the code :
  • bidir python-tk requires setting the Popen PIPEs to non blocking and using select.select on the output
  • matplotlib is unusable in non blocking mode : once matplotlib has the focus you need to destroy it to plot another function
  • from np import * is evil, but it let you have access to all array oriented math function (sin, cos, exp, ...)
This said, we have a pretty compact code of 128 lines of code that is pretty more reactive than using matplolib's widget for a 3D ploter.
from time import time, sleep
from subprocess import Popen, PIPE
import os
import matplotlib.pyplot as plt
import numpy as np
from numpy import *
import select
from matplotlib import cm
# let's talk to tk/tcl directly
p = Popen(['wish'], stdin=PIPE, stdout=PIPE, stderr=PIPE)
os.set_blocking(p.stdout.fileno(), False)
os.set_blocking(p.stdin.fileno(), True)
os.set_blocking(p.stderr.fileno(), False)
def puts(s):
for l in s.split("\n"):
select.select([], [p.stdin],[])
p.stdin.write((l + "\n").encode())
p.stdin.flush()
if back := p.stderr.read():
print("TCL>" + back.decode())
def gets():
ret=p.stdout.read()
if ret:
exec(ret, globals())
xmin = -5
xmax = 5
xstep = .20
ymin = -5
ymax = 5
ystep = .20
zmin = -1
zmax = 1
puts(f"""
set code [ catch {{
source /usr/share/tcltk/ttkthemes/themes/plastik/plastik.tcl
ttk::style theme use plastik
}} pass ]
if {{$code}} {{
puts "print('theme not found, fallback to ugly tcl/tk look')"
puts "print('on debian : apt install tcl-ttkthemes to have ttkthemes installed')"
}}
set text "cos(x) * sin(y) / ( x**2 + y**2 + 1)"
proc submit {{}} {{
global text xmax xmin xstep ymax ymin ystep zmax zmin
puts "compute('$text');xmax=$xmax;xmin=$xmin;xstep=$xstep;ymin=$ymin;ymax=$ymax;ystep=$ystep;zmin=$zmin;zmax=$zmax"
}}
pack [ ttk::frame .a ] -anchor s -fill both -expand 1
pack [ ttk::labelframe .t -text plotter ] -padx 5 -pady 5 -fill both -expand 1 -in .a
pack [ ttk::frame .f ] -in .t -anchor s -fill both -expand 1
pack [ ttk::label .l -text "Function to plot" ] -in .f -side left
pack [ ttk::entry .e -textvariable text -width 32 ] -in .f -side left
#bind .e <Enter> submit
set xmin {xmin}
set xmax {xmax}
set xstep {xstep}
pack [ ttk::frame .g ] -in .t -anchor s -fill both -expand 1
pack [ ttk::label .lxmin -text "xmin " ] -in .g -side left
pack [ ttk::entry .xmin -textvariable xmin -width 3 ] -in .g -side left
pack [ ttk::label .lxmax -text " xmax " ] -in .g -side left
pack [ ttk::entry .xmax -textvariable xmax -width 3 ] -in .g -side left
pack [ ttk::label .lxstep -text " by " ] -in .g -side left
pack [ ttk::entry .xstep -textvariable xstep -width 3 ] -in .g -side left
set ymin {ymin}
set ymax {ymax}
set ystep {ystep}
pack [ ttk::frame .h ] -in .t -anchor s -fill both -expand 1
pack [ ttk::label .lymin -text "ymin " ] -in .h -side left
pack [ ttk::entry .ymin -textvariable ymin -width 3 ] -in .h -side left
pack [ ttk::label .lymax -text " xmax " ] -in .h -side left
pack [ ttk::entry .ymax -textvariable ymax -width 3 ] -in .h -side left
pack [ ttk::label .lystep -text " by " ] -in .h -side left
pack [ ttk::entry .ystep -textvariable ystep -width 3 ] -in .h -side left
set zmin {zmin}
set zmax {zmax}
pack [ ttk::frame .i ] -in .t -anchor s -fill both -expand true
pack [ ttk::label .lzmin -text "zmin " ] -in .i -side left
pack [ ttk::entry .zmin -textvariable zmin -width 3 ] -in .i -side left
pack [ ttk::label .lzmax -text " xmax " ] -in .i -side left
pack [ ttk::entry .zmax -textvariable zmax -width 3 ] -in .i -side left
pack [ ttk::frame .j ] -anchor s -fill both -expand true
pack [ ttk::label .lwar -text "Close the matplotlib windows to change the function or parameters" ] -in .j -anchor s
set status ""
pack [ ttk::label .status -textvariable status ] -in .j -anchor s
pack [ ttk::button .b -text Submit -command submit ] -anchor s -in .j
""")
def compute(text):
global plt
fig, ax = plt.subplots()
ax = fig.add_subplot(projection="3d")
xa= np.arange(xmin,xmax,xstep)
ya= np.arange(ymin,ymax,ystep)
x, y = np.meshgrid(xa,ya)
ax.set_zlim(zmin,zmax)
try:
z= eval(text)
s = ax.plot_surface(x,y,z,cmap=cm.coolwarm, antialiased=True)
ax.contourf(x, y, z, zdir='z', offset=zmin, cmap='coolwarm')
ax.contourf(x, y, z, zdir='x', offset=xmin-xstep, cmap='coolwarm')
ax.contourf(x, y, z, zdir='y', offset=ymin-ystep, cmap='coolwarm')
f = fig.colorbar(s, shrink=0.5, aspect=10)
plt.ioff()
plt.show()
except Exception as e:
puts(f"""tk_messageBox -icon error -message "Python Error : {e}" """)
#compute("cos(x)*cos(y)")
while True:
gets()
sleep(1)
view raw ploter.py hosted with ❤ by GitHub

No comments: