|
41 | 41 | _load = _step_cache.load |
42 | 42 | _CACHE_MAX = _step_cache.CACHE_MAX |
43 | 43 |
|
| 44 | +# Terminal character cells are approximately twice as tall as they are wide in |
| 45 | +# pixels (e.g. 8 px wide × 16 px tall). A square physical domain should |
| 46 | +# therefore occupy a ~2:1 (col:row) character grid to look correct. |
| 47 | +_CELL_RATIO: float = 2.0 |
| 48 | + |
| 49 | +# Physical domain aspect ratio is clamped to this range so that very elongated |
| 50 | +# domains don't produce unusable slivers. |
| 51 | +_ASPECT_MIN: float = 0.2 |
| 52 | +_ASPECT_MAX: float = 5.0 |
| 53 | + |
44 | 54 |
|
45 | 55 | # --------------------------------------------------------------------------- |
46 | 56 | # Plot widget |
@@ -113,11 +123,31 @@ def render(self): # pylint: disable=too-many-branches,too-many-locals,too-many- |
113 | 123 | # Content area = widget size minus 1-char border on each side. |
114 | 124 | # Reserve 1 row each for header and footer → h_plot rows for the image. |
115 | 125 | w_plot = max(self.size.width - 2, 4) |
116 | | - h_plot = max(self.size.height - 4, 4) # -2 border, -2 header+footer |
| 126 | + h_plot_avail = max(self.size.height - 4, 4) # -2 border, -2 header+footer |
117 | 127 |
|
118 | 128 | # Right side: gap + gradient strip + value labels |
119 | 129 | _CB_GAP, _CB_W, _CB_LBL = 1, 2, 9 |
120 | | - w_map = max(w_plot - _CB_GAP - _CB_W - _CB_LBL, 4) |
| 130 | + w_map_avail = max(w_plot - _CB_GAP - _CB_W - _CB_LBL, 4) |
| 131 | + |
| 132 | + # Preserve the physical x/y aspect ratio so the heatmap is not |
| 133 | + # stretched to fill the terminal. The domain ratio is clamped to |
| 134 | + # avoid extremely wide or tall slivers. |
| 135 | + y_cc_2d = self._y_cc if self._y_cc is not None else np.array([0.0, 1.0]) |
| 136 | + x_extent = max(abs(float(x_cc[-1]) - float(x_cc[0])), 1e-30) # pylint: disable=unsubscriptable-object |
| 137 | + y_extent = max(abs(float(y_cc_2d[-1]) - float(y_cc_2d[0])), 1e-30) |
| 138 | + domain_ratio = float(np.clip(x_extent / y_extent, _ASPECT_MIN, _ASPECT_MAX)) |
| 139 | + # Convert to character-grid ratio: 1 row ≈ _CELL_RATIO columns wide. |
| 140 | + char_ratio = domain_ratio * _CELL_RATIO # desired w_map / h_plot |
| 141 | + |
| 142 | + # Fit within the available character budget: try height-constrained first, |
| 143 | + # fall back to width-constrained if the ideal width exceeds w_map_avail. |
| 144 | + w_ideal = int(round(h_plot_avail * char_ratio)) |
| 145 | + if w_ideal <= w_map_avail: |
| 146 | + w_map = max(w_ideal, 4) |
| 147 | + h_plot = h_plot_avail |
| 148 | + else: |
| 149 | + h_plot = max(int(round(w_map_avail / char_ratio)), 4) |
| 150 | + w_map = w_map_avail |
121 | 151 |
|
122 | 152 | ix = np.linspace(0, data.shape[0] - 1, w_map, dtype=int) |
123 | 153 | iy = np.linspace(0, data.shape[1] - 1, h_plot, dtype=int) |
|
0 commit comments