EasyPlotLib 0.2.6__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib.egg-info/PKG-INFO +1 -1
  2. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib.egg-info/SOURCES.txt +1 -0
  3. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/PKG-INFO +1 -1
  4. easyplotlib-0.2.8/tests/test_map_aspect_colorbar.py +157 -0
  5. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/__init__.py +0 -0
  6. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/cartopy_helper.py +0 -0
  7. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/export.py +0 -0
  8. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/figsizes_set.py +0 -0
  9. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreadventor-bold.ttf +0 -0
  10. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreadventor-bolditalic.ttf +0 -0
  11. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreadventor-italic.ttf +0 -0
  12. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreadventor-regular.ttf +0 -0
  13. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyrechorus-mediumitalic.ttf +0 -0
  14. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyrecursor-bold.ttf +0 -0
  15. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyrecursor-bolditalic.ttf +0 -0
  16. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyrecursor-italic.ttf +0 -0
  17. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyrecursor-regular.ttf +0 -0
  18. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreheros-bold.ttf +0 -0
  19. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreheros-bolditalic.ttf +0 -0
  20. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreheros-italic.ttf +0 -0
  21. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreheros-regular.ttf +0 -0
  22. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreschola-bold.ttf +0 -0
  23. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreschola-bolditalic.ttf +0 -0
  24. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreschola-italic.ttf +0 -0
  25. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/fonts/texgyreschola-regular.ttf +0 -0
  26. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/helpers.py +0 -0
  27. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/palettes.py +0 -0
  28. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib/styles/nature.mplstyle +0 -0
  29. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib.egg-info/dependency_links.txt +0 -0
  30. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/EasyPlotLib.egg-info/top_level.txt +0 -0
  31. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/README.md +0 -0
  32. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/setup.cfg +0 -0
  33. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/setup.py +0 -0
  34. {easyplotlib-0.2.6 → easyplotlib-0.2.8}/tests/test_subplot_labels.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: EasyPlotLib
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: A simple plotting library
5
5
  Home-page: https://github.com/yourusername/EasyPlotLib
6
6
  Author: HanYuyang
@@ -28,4 +28,5 @@ EasyPlotLib/fonts/texgyreschola-bolditalic.ttf
28
28
  EasyPlotLib/fonts/texgyreschola-italic.ttf
29
29
  EasyPlotLib/fonts/texgyreschola-regular.ttf
30
30
  EasyPlotLib/styles/nature.mplstyle
31
+ tests/test_map_aspect_colorbar.py
31
32
  tests/test_subplot_labels.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: EasyPlotLib
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: A simple plotting library
5
5
  Home-page: https://github.com/yourusername/EasyPlotLib
6
6
  Author: HanYuyang
@@ -0,0 +1,157 @@
1
+ """
2
+ Map aspect ratio vs colourbar height — lesson + regression check.
3
+
4
+ THE LESSON (learned the hard way on a WRF d01/d02 + land-use + irrigation row):
5
+
6
+ * A geographic panel is almost never 1:1. With ``ax.set_aspect("equal")`` (every
7
+ map: cartopy, salem, or a plain ``imshow`` of a lon/lat field) the AXES BOX
8
+ shrinks to the data's width/height ratio. Δlon and Δlat differ (and projection
9
+ distorts further), so the drawn map is shorter (or narrower) than the grid cell
10
+ matplotlib allocated for it.
11
+
12
+ * ``fig.colorbar(mappable, ax=ax)`` sizes the colourbar to the CELL, not to the
13
+ shrunk map. Result: the colourbar stands taller than the map it annotates — even
14
+ though nothing "overlaps". This is the bug this test reproduces.
15
+
16
+ * In a single ROW of maps with DIFFERENT aspects (e.g. a wide parent domain next to
17
+ square nests), equal-width cells make the wide panel short and the square panels
18
+ tall → the row looks ragged.
19
+
20
+ THE FIXES (both demonstrated and asserted below):
21
+
22
+ A. EQUAL PANEL HEIGHTS: give each map a column ``width_ratio`` proportional to its
23
+ own aspect (= width/height). Then every cell matches its map's aspect and all
24
+ maps render at the same height.
25
+
26
+ B. COLOURBAR = MAP HEIGHT: let the layout engine place the colourbars (so they sit
27
+ to the right with no overlap), then ``fig.canvas.draw()``, FREEZE the layout
28
+ (``fig.set_layout_engine("none")``) and clamp each colourbar axes' ``y0``/height
29
+ to its map's drawn ``get_position()``. Only the height/bottom change, so the
30
+ horizontal placement the engine chose is preserved.
31
+
32
+ Run: python tests/test_map_aspect_colorbar.py
33
+ Produces tests/test_map_aspect_colorbar_before.png and _after.png for eyeballing,
34
+ and asserts the colourbar/map height ratio is ~1.7 before and ~1.0 after.
35
+ """
36
+
37
+ import os
38
+
39
+ import numpy as np
40
+ import matplotlib
41
+ matplotlib.use("Agg")
42
+ import matplotlib.pyplot as plt
43
+ from matplotlib.cm import ScalarMappable
44
+ from matplotlib.colors import Normalize
45
+
46
+ import EasyPlotLib as epl
47
+
48
+ HERE = os.path.dirname(os.path.abspath(__file__))
49
+ RNG = np.random.default_rng(0)
50
+
51
+
52
+ # --- synthetic "maps": random smooth fields on lon/lat boxes whose Δlon != Δlat ----
53
+ def smooth_field(ny, nx, sigma=6):
54
+ """A random field smoothed by FFT so it looks map-like, not white noise."""
55
+ a = RNG.standard_normal((ny, nx))
56
+ fy = np.exp(-0.5 * (np.fft.fftfreq(ny)[:, None] * ny / sigma) ** 2)
57
+ fx = np.exp(-0.5 * (np.fft.fftfreq(nx)[None, :] * nx / sigma) ** 2)
58
+ a = np.fft.ifft2(np.fft.fft2(a) * (fy * fx)).real
59
+ a -= a.min()
60
+ return a / a.max()
61
+
62
+
63
+ # (name, lon0, lon1, lat0, lat1) — a wide parent + two square-ish nests, like d01/d02.
64
+ BOXES = [
65
+ ("parent (wide)", 103.0, 129.0, 27.0, 43.0), # Δlon 26, Δlat 16 -> aspect 1.62
66
+ ("nest A", 111.5, 121.5, 31.5, 40.0), # Δlon 10, Δlat 8.5 -> aspect 1.18
67
+ ("nest B", 111.5, 121.5, 31.5, 40.0),
68
+ ]
69
+
70
+
71
+ def box_aspect(b):
72
+ """Width/height of the lon/lat box (the value to feed width_ratios)."""
73
+ _, lo0, lo1, la0, la1 = b
74
+ return (lo1 - lo0) / (la1 - la0)
75
+
76
+
77
+ def draw_map(ax, b):
78
+ _, lo0, lo1, la0, la1 = b
79
+ data = smooth_field(120, int(120 * box_aspect(b)))
80
+ im = ax.imshow(data, origin="lower", extent=(lo0, lo1, la0, la1),
81
+ cmap=epl.COLORMAPS["sequential"], vmin=0, vmax=1, aspect="equal")
82
+ ax.set_xticks(np.linspace(lo0, lo1, 4).round())
83
+ ax.set_yticks(np.linspace(la0, la1, 4).round())
84
+ ax.tick_params(labelsize=6)
85
+ return im
86
+
87
+
88
+ def heights(fig, ax_map, cb):
89
+ """(map height, colourbar height) in figure fraction, after a draw."""
90
+ fig.canvas.draw()
91
+ return ax_map.get_position().height, cb.ax.get_position().height
92
+
93
+
94
+ # ----------------------------------------------------------------------------------
95
+ def build(fix_aspect, clamp_cbar, path, title):
96
+ """Build the 3-map row. fix_aspect -> width_ratios; clamp_cbar -> fix B."""
97
+ epl.journal_style("nat2", palette="nature", inverted_aspect_ratio=0.40, apply=True)
98
+ wr = [box_aspect(b) for b in BOXES] if fix_aspect else [1, 1, 1]
99
+ fig, axs = plt.subplots(1, 3, layout="constrained",
100
+ gridspec_kw=dict(width_ratios=wr))
101
+ cbs = []
102
+ for n, (ax, b) in enumerate(zip(axs, BOXES)):
103
+ im = draw_map(ax, b)
104
+ ax.set_title(b[0], fontsize=7)
105
+ ax.annotate(**epl.subplot_labels(n, "a"))
106
+ extend = "max" if n == 2 else "neither" # mimic the real irrigation bar
107
+ cb = fig.colorbar(ScalarMappable(Normalize(0, 1), im.cmap), ax=ax,
108
+ fraction=0.05, pad=0.02, aspect=32, extend=extend)
109
+ cb.ax.tick_params(labelsize=6)
110
+ cbs.append(cb)
111
+
112
+ if clamp_cbar:
113
+ fig.canvas.draw()
114
+ fig.set_layout_engine("none")
115
+ for ax, cb in zip(axs, cbs):
116
+ pm, pc = ax.get_position(), cb.ax.get_position()
117
+ cb.ax.set_position([pc.x0, pm.y0, pc.width, pm.height])
118
+
119
+ fig.suptitle(title, fontsize=8)
120
+ fig.savefig(path, dpi=200)
121
+ ratios = [heights(fig, ax, cb)[1] / heights(fig, ax, cb)[0] for ax, cb in zip(axs, cbs)]
122
+ map_hs = [ax.get_position().height for ax in axs]
123
+ plt.close(fig)
124
+ return ratios, map_hs
125
+
126
+
127
+ if __name__ == "__main__":
128
+ before_path = os.path.join(HERE, "test_map_aspect_colorbar_before.png")
129
+ after_path = os.path.join(HERE, "test_map_aspect_colorbar_after.png")
130
+
131
+ # BEFORE: equal-width cells, colourbars sized to the cell (the bug).
132
+ r_before, h_before = build(fix_aspect=False, clamp_cbar=False,
133
+ path=before_path, title="BEFORE: equal cells, cbar=cell height")
134
+ # AFTER: width_ratios ~ aspect (equal map heights) + colourbar clamped to map.
135
+ r_after, h_after = build(fix_aspect=True, clamp_cbar=True,
136
+ path=after_path, title="AFTER: width_ratios~aspect + clamped cbar")
137
+
138
+ print(f"colourbar/map height ratio BEFORE = {[round(x, 2) for x in r_before]}")
139
+ print(f"colourbar/map height ratio AFTER = {[round(x, 2) for x in r_after]}")
140
+ print(f"map heights (fig frac) BEFORE = {[round(x, 3) for x in h_before]}")
141
+ print(f"map heights (fig frac) AFTER = {[round(x, 3) for x in h_after]}")
142
+ print("saved", before_path)
143
+ print("saved", after_path)
144
+
145
+ # Fix B: colourbars must match their map height (the whole point).
146
+ assert max(r_before) > 1.25, f"expected tall colourbars before fix, got {r_before}"
147
+ assert all(abs(r - 1.0) < 0.06 for r in r_after), \
148
+ f"colourbars not clamped to map height: {r_after}"
149
+ # Fix A: width_ratios ~ aspect makes the three maps nearly the same height
150
+ # (BEFORE spread ~0.075 -> AFTER ~0.02). NOTE the equality is only exact when the
151
+ # figure is tall enough that every map is WIDTH-limited; a very wide/short figure
152
+ # pushes the square-ish nests into being HEIGHT-limited, so a small residual remains.
153
+ assert (max(h_before) - min(h_before)) > 2 * (max(h_after) - min(h_after)), \
154
+ f"width_ratios~aspect should shrink the height spread: {h_before} -> {h_after}"
155
+ assert max(h_after) - min(h_after) < 0.03, \
156
+ f"map heights not equalised by width_ratios~aspect: {h_after}"
157
+ print("OK: colourbars clamped to map height; panel heights equalised.")
File without changes
File without changes
File without changes