Skip to content

Commit 7d04f9a

Browse files
sbryngelsonclaude
andcommitted
TUI 2D: preserve physical domain aspect ratio in heatmap
Terminal character cells are ~2x taller than wide (_CELL_RATIO=2.0). Previously the heatmap always filled the full widget, stretching it. Now the 2D render computes the physical x/y extent ratio, clamps it to [_ASPECT_MIN, _ASPECT_MAX] = [0.2, 5.0] to avoid unusable slivers, multiplies by _CELL_RATIO to get the desired col:row character ratio, then fits within the available character budget (height-constrained first, width-constrained fallback). The heatmap leaves blank space rather than stretching to fill the terminal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 412f36a commit 7d04f9a

1 file changed

Lines changed: 32 additions & 2 deletions

File tree

toolchain/mfc/viz/tui.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@
4141
_load = _step_cache.load
4242
_CACHE_MAX = _step_cache.CACHE_MAX
4343

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+
4454

4555
# ---------------------------------------------------------------------------
4656
# Plot widget
@@ -113,11 +123,31 @@ def render(self): # pylint: disable=too-many-branches,too-many-locals,too-many-
113123
# Content area = widget size minus 1-char border on each side.
114124
# Reserve 1 row each for header and footer → h_plot rows for the image.
115125
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
117127

118128
# Right side: gap + gradient strip + value labels
119129
_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
121151

122152
ix = np.linspace(0, data.shape[0] - 1, w_map, dtype=int)
123153
iy = np.linspace(0, data.shape[1] - 1, h_plot, dtype=int)

0 commit comments

Comments
 (0)