Skip to main content

plotpy/
constants.rs

1// The code in `set_equal_axes` is based on:
2// https://stackoverflow.com/questions/13685386/matplotlib-equal-unit-length-with-equal-aspect-ratio-z-axis-is-not-equal-to
3//
4// It needs Matplotlib version at least 3.3.0 (Jul 16, 2020)
5// https://github.com/matplotlib/matplotlib/blob/f6e0ee49c598f59c6e6cf4eefe473e4dc634a58a/doc/users/prev_whats_new/whats_new_3.3.0.rst
6
7/// Commands to be added at the beginning of the Python script
8///
9/// The python definitions are:
10///
11/// * `NaN` -- Variable to handle NaN values coming from Rust
12/// * `EXTRA_ARTISTS` -- List of additional objects that must not be ignored when saving the figure
13/// * `add_to_ea` -- Adds an entity to the EXTRA_ARTISTS list to prevent them being ignored
14///    when Matplotlib decides to calculate the bounding boxes. The Legend is an example of entity that could
15///    be ignored by the savefig command (this is issue is prevented here).
16/// * `THREE_D` -- Is a dictionary of mplot3d objects (one for each subplot_3d)
17/// * `THREE_D_ACTIVE` -- Is a tuple holding the key to the current THREE_D object (defines the subplot_3d)
18/// * `ax3d` -- Creates or returns the mplot3d object with the current subplot_3d definition specified by THREE_D_ACTIVE
19/// * `subplot_3d` -- Specifies the THREE_D_ACTIVE parameters to define a subplot_3d
20/// * `data_to_axis` -- Transforms data limits to axis limits
21/// * `axis_to_data` -- Transforms axis limits to data limits
22/// * `set_equal_axes` -- Configures the aspect of axes with a same scaling from data to plot units for x, y and z.
23///   For example a circle will show as a circle in the screen and not an ellipse. This function also handles
24///   the 3D case which is a little tricky with Matplotlib. In this case (3D), the version of Matplotlib
25///   must be greater than 3.3.0.
26pub const PYTHON_HEADER: &str = "### file generated by the 'plotpy' Rust crate
27
28import numpy as np
29import matplotlib.pyplot as plt
30import matplotlib.ticker as tck
31import matplotlib.patches as pat
32import matplotlib.path as pth
33import matplotlib.patheffects as pff
34import matplotlib.lines as lns
35import matplotlib.transforms as tra
36import mpl_toolkits.mplot3d
37import matplotlib.tri as plt_tri
38from mpl_toolkits.axes_grid1 import make_axes_locatable
39
40# Variable to handle NaN values coming from Rust
41NaN = np.nan
42
43# List of additional objects to calculate bounding boxes
44EXTRA_ARTISTS = []
45
46# Adds an entity to the EXTRA_ARTISTS list to calculate bounding boxes
47def add_to_ea(obj):
48    global EXTRA_ARTISTS
49    if obj!=None: EXTRA_ARTISTS.append(obj)
50
51# Is a dictionary of mplot3d objects (one for each subplot_3d)
52THREE_D = dict()
53
54# Is a tuple holding the key to the current THREE_D object (defines the subplot_3d)
55THREE_D_ACTIVE = (1,1,1)
56
57# Creates or returns the mplot3d object with the current subplot_3d definition specified by THREE_D_ACTIVE
58def ax3d():
59    global THREE_D
60    global THREE_D_ACTIVE
61    if not THREE_D_ACTIVE in THREE_D:
62        a, b, c = THREE_D_ACTIVE
63        THREE_D[THREE_D_ACTIVE] = plt.gcf().add_subplot(a,b,c,projection='3d')
64        THREE_D[THREE_D_ACTIVE].set_xlabel('x')
65        THREE_D[THREE_D_ACTIVE].set_ylabel('y')
66        THREE_D[THREE_D_ACTIVE].set_zlabel('z')
67        add_to_ea(THREE_D[THREE_D_ACTIVE])
68    return THREE_D[THREE_D_ACTIVE]
69
70# Specifies the THREE_D_ACTIVE parameters to define a subplot_3d
71def subplot_3d(a,b,c):
72    global THREE_D_ACTIVE
73    THREE_D_ACTIVE = (a,b,c)
74    ax3d()
75
76# Transforms data limits to axis limits
77def data_to_axis(coords):
78    plt.axis() # must call this first
79    return plt.gca().transLimits.transform(coords)
80
81# Transforms axis limits to data limits
82def axis_to_data(coords):
83    plt.axis() # must call this first
84    return plt.gca().transLimits.inverted().transform(coords)
85
86# Configures the aspect of axes with a same scaling from data to plot units for x, y and z.
87def set_equal_axes():
88    global THREE_D
89    if len(THREE_D) == 0:
90        ax = plt.gca()
91        ax.axes.set_aspect('equal')
92        return
93    try:
94        ax = ax3d()
95        ax.set_box_aspect([1,1,1])
96        limits = np.array([ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()])
97        origin = np.mean(limits, axis=1)
98        radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
99        x, y, z = origin
100        ax.set_xlim3d([x - radius, x + radius])
101        ax.set_ylim3d([y - radius, y + radius])
102        ax.set_zlim3d([z - radius, z + radius])
103    except:
104        import matplotlib
105        print('VERSION of MATPLOTLIB = {}'.format(matplotlib.__version__))
106        print('ERROR: set_box_aspect is missing in this version of Matplotlib')
107
108# Function to ignore calls to plt such as the colorbar in an inset Axes
109def ignore_this(*args, **kwargs):
110    pass
111
112################## plotting commands follow after this line ############################
113
114";
115
116const PY_NUM_MARKERS: [&str; 12] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"];
117
118/// Quotes or not the marker style
119///
120/// This is needed because the following markers are python numbers,
121/// and not strings (so, they must not be quoted):
122///
123/// ```text
124/// 0 (TICKLEFT)
125/// 1 (TICKRIGHT)
126/// 2 (TICKUP)
127/// 3 (TICKDOWN)
128/// 4 (CARETLEFT)
129/// 5 (CARETRIGHT)
130/// 6 (CARETUP)
131/// 7 (CARETDOWN)
132/// 8 (CARETLEFTBASE)
133/// 9 (CARETRIGHTBASE)
134/// 10 (CARETUPBASE)
135/// 11 (CARETDOWNBASE)
136/// ```
137///
138/// See: <https://matplotlib.org/stable/api/markers_api.html>
139pub(crate) fn quote_marker(maker_style: &str) -> String {
140    if PY_NUM_MARKERS.contains(&maker_style) {
141        String::from(maker_style)
142    } else {
143        format!("'{}'", maker_style)
144    }
145}
146
147////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
148
149#[cfg(test)]
150mod tests {
151    use super::PYTHON_HEADER;
152
153    #[test]
154    fn constants_are_correct() {
155        assert_eq!(PYTHON_HEADER.len(), 2975);
156    }
157}