@@ -131,6 +131,17 @@ module UnicodePlot
131131 floored.to_i32
132132 end
133133
134+ # Convert pixel coordinates to 1-based char-cell coordinates.
135+ # Boundary pixels are nudged one step inward to match Julia's canvas mapping.
136+ private def pixel_to_char_point (pixel_x : Float64 , pixel_y : Float64 ) : Tuple (Int32 , Int32 )?
137+ px = pixel_x >= @pixel_width ? pixel_x - 1.0 : pixel_x
138+ py = pixel_y >= @pixel_height ? pixel_y - 1.0 : pixel_y
139+ cx = floor_to_i32(px / x_pixel_per_char.to_f)
140+ cy = floor_to_i32(py / y_pixel_per_char.to_f)
141+ return unless cx && cy
142+ {cx + 1 , cy + 1 }
143+ end
144+
134145 private def plot_pixel_if_visible (
135146 pixel_x : Float64 ,
136147 pixel_y : Float64 ,
@@ -238,12 +249,9 @@ module UnicodePlot
238249 # Annotate a character at canvas coords (x, y)
239250 def annotate !(x : Float64 , y : Float64 , char : Char , color : UInt32 , blend : Bool ) : self
240251 return self unless valid_x?(x) && valid_y?(y)
241- cx = floor_to_i32(scale_x_to_pixel(x) / x_pixel_per_char.to_f)
242- cy = floor_to_i32(scale_y_to_pixel(y) / y_pixel_per_char.to_f)
243- return self unless cx && cy
244-
245- cx += 1
246- cy += 1
252+ point = pixel_to_char_point(scale_x_to_pixel(x), scale_y_to_pixel(y))
253+ return self unless point
254+ cx, cy = point
247255 char_point!(cx, cy, char, color, blend)
248256 self
249257 end
@@ -253,12 +261,9 @@ module UnicodePlot
253261 return self unless valid_x?(x) && valid_y?(y)
254262 px = scale_x_to_pixel(x)
255263 py = scale_y_to_pixel(y)
256- cx = floor_to_i32(px / x_pixel_per_char.to_f)
257- cy = floor_to_i32(py / y_pixel_per_char.to_f)
258- return self unless cx && cy
259-
260- cx += 1
261- cy += 1
264+ point = pixel_to_char_point(px, py)
265+ return self unless point
266+ cx, cy = point
262267 n = text.size
263268 cx = case halign
264269 when :center , :hcenter then cx - n // 2
0 commit comments