No project description provided
Project description
plotnik
is a Python library designed for creating simple graphs using
matplotlib in Cartesian coordinates, mirroring the style of 'school' graphs
traditionally used in Russian physics and mathematics education. It was
developed for convenient drawing of thermodynamic cycles, including nonlinear
processes, without need to perform calculations.
The library is currently usable. The code is poorly designed. Full documentation is not available, but you can refer to the examples provided below to understand its functionality.
The library utilizes syntax inspired by the SchemDraw library.
The code has been mostly written by Chat-GPT.
Basic usage
The default font is 'STIX Two Text'. To switch to Computer Modern Roman, use
d.set_config(font='serif')
, noting that this requires LaTeX to be installed
on your machine.
To draw the curves, use processes
: class Process()
with its subclasses:
-
Linear()
Draw a straight line from .at() to .to(). -
Power()
Connects initial and final points with the equation y=k*x^n + b -
Adiabatic()
Draw adiabatic process pV^gamma = const for p(V) coordinates. Set the gamma value usingAdiabatic(gamma=7/5)
, with the default being 5/3. -
Iso_t()
Isothermal process pV=const in p(V) coordinates. -
Bezier(x,y)
Draw quadratic or cubic Bezier curve.d += Bezier(x=2,y=2).at(1,1).to(3,1)
draws quadratic Bezier curve from (1,1) to (3,1) with a single control point at (2,2). Similarly,
d += Bezier(x1=3,y1=7, x2=5,y2=3).at(1,5).to(7,5)
this code plots a cubic Bezier curve, resembling a sine wave, with two control points at (x1, y1) and (x2, y2). Note that
d +=
is usually optional.
Additionally, standard matplotlib syntax can be used to add text and lines to
the plot, for example, d.ax.plot(x, y)
.
Examples
1. V=const, adiabatic, isothermal
This example illustrates well the purpose behind the creation of the library. It is necessary to draw a cycle in pV-coordinates, consisting of an isochore, adiabat, and isotherm. The goal was to free the user from the need to perform calculations and to provide a simple interface for constructing such graphs.
import plotnik
from plotnik.processes import *
v1 = 3
v2 = 9
v3 = v1
p1 = 9
with plotnik.Drawing() as d:
d.set_config(
xname='$V$',
yname='$p$',
zero_x=0.5,
axes_arrow_width=0.23,
)
A1 = Adiabatic().at(v1,p1).to(v2, 'volume').arrow().dot()
p2 = A1.end[1] # A1.end returns coordinates (x,y) for the last point of A1 process
# Process T1 has no .at(), so it takes the last point from the previous
# process A1 as an initial point
T1 = Iso_t().to(v1, 'volume').arrow().dot().label(2, dy=0)
p3 = T1.end[1]
Linear().to(v1,p1).arrow().dot().label(3,1)
d.show()
# `crop=True` is only compatible with SVG files and requires the installation of `Inkscape` on your machine.
# This feature removes paths named `patch_1` and `patch_2`, which, in my case, do not contain any paths
# but add a whitespace margin.
d.save('filename.svg', crop=True)
2. Linear() and grid()
import plotnik
from plotnik.processes import *
u1 = 2
u2 = 4
v1 = 3
v2 = 6
with plotnik.Drawing() as d:
d.set_config(
yname='$U$',
xname='$V$',
zero_x=0.4,
ylim=[0,6],
xlim=[0,8],
)
Linear().at(v1,u1).to(v2,u2).arrow().dot('both').label(1,2)
d.grid(y_end=5)
d.show()
3. Carnot cycle in PV coordinates. Adiabatic(), Iso_t().
import plotnik
from plotnik.processes import *
p1 = 10
v1 = 3
v2 = 6
v3 = 10
with plotnik.Drawing() as d:
d.set_config(
fontsize=30,
yname='$p$',
xname='$V$',
aspect=0.7,
xlim=[0,11],
center=[5.5, 4.5],
)
# When the second argument in the .to() method is 'volume', the function draws a line up to
# volume v2 and calculates the required pressure. To retrieve this pressure value, use end_p(process)
T1 = Iso_t().at(v1, p1).to(v2, 'volume').dot('both').label(1,2)
p2 = T1.end[1]
A1 = Adiabatic().to(v3, 'volume')
p3 = A1.end[1]
# common_pv calculates the volume (v) and pressure (p) at the intersection of an isothermal process
# passing through the start point and an adiabatic process passing through the end point.
v4, p4 = common_pv(v1,p1, v3,p3)
Iso_t().to(v4, 'volume').dot('both').label(3,4)
Adiabatic().to(v1,'volume')
Power(15).at(v2, p2).to(v4,p4)
d.ax.text(4.75, 4.8, '$A_1$', fontsize=24)
d.ax.text(5.65, 3.9, '$A_2$', fontsize=24)
d.show()
4. Cubic Bezier curve with dots on it
import plotnik
from plotnik.processes import *
with plotnik.Drawing() as d:
d.set_config(yname=r'$x$',
xname=r'$t$',
xlim=[0,12],
center_x=5,
)
B = Bezier(x1=5,y1=15, x2=6.8,y2=-4).at(1,7).to(11,3).lw(2.4)
# B.get_point(index) returns a tuple (x, y).
# Use an asterisk to unpack this tuple into x and y.
# The allowed index range is from 0 to 100.
State().at(*B.get_point( 4)).dot().label('A')
State().at(*B.get_point(18)).dot().label('B', dx=0)
State().at(*B.get_point(48)).dot().label('C')
State().at(*B.get_point(91)).dot().label('D')
d.show()
5. Power() to create shifted hyperbola y=k/x+b
import plotnik
from plotnik.processes import *
x1 = 3
y1 = 2
x2 = 2*x1
y2 = y1
x3 = x1
y3 = 3*y1
with plotnik.Drawing() as d:
d.set_config(
fontsize=31,
yname='$p$',
xname=r'$\rho$',
xlim=[0,7.5],
ylim=[0,7.5],
axes_arrow_width=0.16,
zero_x=0.4, # add zero as a xtick label shifted to x=-0.4
)
Linear().at(x1, y1).to(x2,y2).arrow().dot().tox().label(1,2, dy=-0.65)
Power(power=-0.5).at(x2, y2).to(x3, y3).arrow().label('',3)
Linear().to(x1,y1).arrow().dot('both').toy()
d.ax.set_yticks([y1, y3], ['$p_0$', '$3p_0$'])
d.ax.set_xticks([x1, x2], [r'$\rho_0$', r'$2\rho_0$'])
d.show()
6. Two Adiabatic() & two Linear()
import plotnik
from plotnik.processes import *
v1 = 2
v2 = 5
p12 = 8
p34 = 3
with plotnik.Drawing() as d:
d.set_config(
yname='$p$',
xname='$V$',
zero_x=0.5,
fontsize=28,
ylim=[0,10.7],
axes_arrow_scale=0.7,
center_x=4,
)
a=22
Linear().at(v1,p12).to(v2,p12).dot('both').arrow(size=a,pos=0.61).label(1,2)
Q1 = Adiabatic().at(v1,p12).to(p34, 'pressure').arrow(size=a,reverse=True)
Q2 = Adiabatic().at(v2,p12).to(p34, 'pressure').arrow(size=a)
v3 = Q1.end[0]
v4 = Q2.end[0]
Linear().at(v4,p34).to(v3,p34).dot('both').arrow(size=a).label(3,4, dy=-0.8)
d.show()
7. Bezier().connect() method
This method is used to create a smooth curve that must pass through the specified point, in this case, (3,60).
import plotnik
from plotnik.processes import *
with plotnik.Drawing() as d:
d.set_config(
aspect=1/20,
yname=r'$\alpha, \%$',
yname_y=103,
xname=r'$T,10^3 \rm{К}$',
xlim=[0,6],
ylim=[0,106],
zero_ofst=[0.2, 11.8]
)
d += (P1:= Power(2).at(0,0).to(3,60).lw(3) )
d += (L1:= Linear().at(5,90).to(6,90).lw(0) ) # This process is used solely to complete the Bezier curve with a tangent, hence 'lw=0' is specified.
Bezier().connect(P1,L1).lw(3)
d.ax.set_xticks([2,4])
d.ax.set_yticks([40,80])
d.grid(step_x=0.5, step_y=10, x_end=5, y_end=90)
d.show()
8. Bezier().get_coordinates()
When plotting a complex curve as two separate processes (thus requiring two calls to ax.plot()
),
using a large linewidth may result in poor connections between the segments.
To resolve this, you can use Bezier()
to calculate the coordinates without plotting them.
Then, append these coordinates to the other process.
Matplotlib will seamlessly join these segments when plotting them in a single ax.plot()
call.
import plotnik
from plotnik.processes import *
with plotnik.Drawing() as d:
d.set_config(
yname=r'$V_{\rm погр},\rm{см}^3$',
xname=r'$\rho,\rm{г}/\rm{см}^3$',
ylim=[0,12],
xlim=[0,4.8],
aspect=1/4,
fontsize=18,
axes_arrow_width=0.2,
)
d += (B1:= Bezier(x=1.8,y=2.8).at(1, 10).to(4, 2.5).lw(0) )
x,y = B1.get_coordinates()
# Append straight line to x,y
x = np.append([0,1],x)
y = np.append([10,10],y)
d.ax.plot(x, y, lw=2.5, color='k')
d.ax.tick_params(length=0)
d.ax.set_yticks(np.arange(1,11,1))
d.ax.set_xticks(np.arange(0.5,4.5,0.5),
['0,5','1,0','1,5','2,0','2,5','3,0','3,5','4,0'])
d.grid(step_x=0.25, step_y=1, y_end=10, x_end=4)
d.show()
9. Power()
import plotnik
from plotnik.processes import *
v1 = 8
u1 = 6
v2 = 3.5
with plotnik.Drawing() as d:
d.set_config(
fontsize=31,
yname='$U$',
xname='$V$',
ylim=[0,7.4],
axes_arrow_length=1.1,
center=[10,0],
)
P1 = Power().at(v1, u1).to(v2, 'x').arrow().label(1,2).dot('both').tox().toy()
y2 = P1.end[1]
Power().to(0, 0).ls('--')
d.ax.set_xticks([v1, v2], ['$V_1$', '$V_2$'])
d.ax.set_yticks([u1, y2], ['$U_1$', '$U_2$'])
d.show()
10. Arrows and labels positioning
import plotnik
from plotnik.processes import *
v1 = 2
v2 = v1
v3 = 6
v4 = v3
t1 = 4
t2 = 2
t3 = 6
t4 = 8
with plotnik.Drawing() as d:
d.set_config(
yname='$V$',
lw=3.2,
xname='$T$',
ylim=[0,7],
xlim=[0,10],
zero_x=0.5,
axes_arrow_scale=1.5,
)
Linear().at(t1, v1).to(t2, v2).arrow().dot('both').tox()\
.label(1,2, start_ofst=[0.5,0.2], end_ofst=[-0.8, 0.2])
Linear().to(t3, v3).arrow().tozero('start')
Linear().to(t4, v4).arrow(pos=0.7).tox().dot('both')\
.label(3,4, start_dx=-0.5)
d.ax.set_xticks([2,4,6,8], ['$T_0$','$2T_0$','$3T_0$','$4T_0$'])
d.show()
11. Customize grid()
import plotnik
from plotnik.processes import *
B = [0, 0.2, 0]
t = [0, 2, 4]
with plotnik.Drawing() as d:
d.set_config(yname='$B,$Тл', xname='$t,$с',
xlim=[0,6.3],
xname_x=5,
yname_y=0.26,
ylim=[0,0.27],
zero_x=0.3,
axes_arrow_scale=1.5,
aspect=20,
)
d.ax.plot(t,B,'k-', lw=2.5)
d.ax.set_yticks([0.1,0.2], ['0,1','0,2'])
d.ax.set_xticks([1,2,3,4])
d.grid(step_x=1, step_y=0.05, x_end=4.2, y_end=0.21, lw=2, color='#333333')
d.show()
12. Tangent red isotherm
import plotnik
from plotnik.processes import *
p1=3
v1=1
p2=1
v2=4
a = (p1-p2) / (v1-v2)
b = p1 - a*v1
vm = -b/(2*a)
pm = a*vm + b
with plotnik.Drawing() as d:
d.set_config(
fontsize=24,
yname='$p$',
xname='$V$',
xlim=[0,5],
ylim=[0,4],
zero_ofst=[0.2, 0.38],
)
Linear().at(v1,p1).to(v2,p2).arrow(pos=0.3).dot('both').label(1,2).toy().tox()
State().at(vm,pm).dot().tox().toy()
Iso_t().at(vm,pm).to(v1*1.35,'volume').lw(1.4).col('#EE3344')
Iso_t().at(vm,pm).to(v2*1.16,'volume').lw(1.4).col('#EE3344')
d.ax.set_yticks([p1, p2, pm], ['$p_1$', '$p_2$', r'$p_\text{м}$'])
d.ax.set_xticks([v1, v2, vm], ['$V_1$', '$V_2$', r'$V\!_\text{м}$'])
d.grid(step=.5, y_end=3.5, x_end=4.5, color='#dddddd')
d.show()
Some options
Consider the followig syntax:
Linear().at().to().arrow().dot().label().toy().tox().tozero().col().lw().ls().zord()
.
.at(x1,y1)
: set starting point. Uses previous process last point if not set.
.to(x2,y2)
: set end point.
.arrow(size=None, pos=0.54, color='black', reverse=False, filled=True, zorder=3, head_length=0.6, head_width=0.2)
pos
sets position of the arrow on the line (from 0 to 1).reverse=True
rotates the arrow on 180 degrees.filled=False
doesn't look well but produces not filled arrow.
.dot(pos='end', size=8, color='black', zorder=5, marker='o')
.dot()
or.dot('end')
or.dot(pos='end')
adds only last point;.dot('start')
adds only start point;.dot('both')
adds two points..dot(size=25)
marker
are standart matplotlib markers, see full listzorder
can change the order it appears relative to other elements (useful to plot marker above or below grid or process etc.).
.label()
add 1 or two labels.
.tox(), .toy(), .tozero()
draw lines to, correspondingly, horizontal axis,
vertical axis and zero. Default linestyle is dashed line, can be changed like
.tox(ls='-')
. By default, draw lines both for start and end of the process.
Can be changed like .tox('end')
or .tox('start')
.
.col('red')
set color for the line.
.lw(1)
set linewidth for the process.
.ls('--')
set linestyle for the process.
.zord(5)
set zorder for the process.
TODO
-
Repair examples 7 and 8 so
d +=
won't be required. -
Revise the arrow positioning logic to ensure they are accurately centered.
-
In the
set_config()
function, add the capability to globally modify arrowsize, dotsize, and lw (line width) for processes.Introduce options in the
set_config()
to globally adjust the size of arrows, dots, and line width for processes. For instance, include settings likedots_all=True
,dots_size=10
andarrows_all=True
,arrows_size=23
. -
Integrate the feature to select different coordinates. For instance, if all processes are initially plotted in x, y coordinates, there should be an option to view them in transformed coordinates like 1/x, y^2. Example syntax could be:
d.transform_coordinates(newx = 1/x, newy = y**2)
. -
Address the issue where
d.save()
generates erroneous results when used without a prior call tod.show()
, ensuring reliable save functionality. -
.xtick()
andd.add_xticks()
use different codes for tick positioning. -
make
.xtick()
use matplotlibax.set_xticks()
method -
Improve the algorithm for automatic determination of positions and sizes for labels, ticks, and arrows.
-
When need
Bezier()
only to calculate coordinates, you have to add it toDrawing()
like so:d += (B1:= Bezier(x=1.8,y=2.8).at(1, 10).to(4, 2.5).lw(0) ) x,y = B1.get_coordinates()
Rewrite the code so one can use
B1 = Bezier(x=1.8,y=2.8).at(1, 10).to(4, 2.5).hide() x,y = B1.get_coordinates()
without adding it an actual figure.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.