Skip to content

Commit c968441

Browse files
krystophnyclaude
andauthored
fix: resolve axis label rendering issues (Issue #335) (#341)
## Summary Fixes critical axis label rendering issues where tick labels, axis labels (xlabel/ylabel), and tick marks were completely missing from scale_examples output in both PNG and ASCII backends. ## Changes Made ### 1. Fixed Tick Label Formatting - Replaced `G0` format (full precision) with intelligent formatting based on value magnitude - Values >= 1000 or < 0.01 use scientific notation (ES10.2) - Values >= 100 use no decimal places (F0.0) - Values >= 10 use one decimal place (F0.1) - Values >= 1 use two decimal places (F0.2) - Small values use three decimal places (F0.3) ### 2. Implemented Pixel-Space Rendering for PNG Backend - Separated data coordinate rendering (for plot data) from pixel coordinate rendering (for axes) - Tick marks now render directly in pixel space using `draw_styled_line` - Tick labels positioned correctly relative to tick marks in pixel coordinates - Axis labels (xlabel/ylabel) positioned in pixel space to avoid overlap ### 3. Proper Label Positioning - X-axis labels positioned 30 pixels below plot area - Y-axis labels positioned 10 pixels from left edge - Tick labels properly spaced from tick marks (5 pixels) - All text elements now render within image bounds ### 4. Added Comprehensive Test Coverage - New test `test_axis_labels_rendering.f90` validates: - Linear scale label rendering - Log scale with power-of-ten formatting - Symlog scale with mixed formatting - Tick label formatting correctness ## Test Results All tests pass successfully: ``` === Testing Axis Labels Rendering (Issue #335) === PASSED: Linear scale with labels PASSED: Log scale with labels PASSED: Symlog scale with labels PASSED: Tick label formatting === ALL AXIS LABEL TESTS PASSED === ``` ## Visual Verification The scale_examples now correctly display: - ✅ Title text - ✅ X and Y axis tick marks - ✅ Properly formatted tick labels - ✅ X and Y axis labels (xlabel/ylabel) - ✅ Correct log scale notation (10^2, 10^3, etc.) - ✅ No overlapping text elements ## Issue Resolution Fixes #335 - Axes wrong and no labels visible on scale_examples.html The GitHub Pages visual showcase now properly displays axis labels and tick marks for all scale types. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4d39651 commit c968441

File tree

7 files changed

+413
-19
lines changed

7 files changed

+413
-19
lines changed

BACKLOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
**Infrastructure & Documentation Issues (Lower Priority)**
2020
- [ ] #323: Test - add edge case tests for PDF heatmap color validation
2121
- [ ] #324: Refactor - define epsilon constant for numerical comparisons
22+
- [ ] #342: Refactor - complete symlog tick generation implementation
23+
- [ ] #343: Refactor - extract label positioning constants
24+
- [ ] #344: Refactor - add format threshold constants in axes module
2225

2326
## DOING (Current Work)
2427
- [ ] #335: Fix - Axes wrong and no labels visible on scale_examples.html

src/fortplot_ascii.f90

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,11 @@ subroutine output_to_terminal(this)
309309
if (allocated(this%xlabel_text)) then
310310
call print_centered_title(this%xlabel_text, this%plot_width)
311311
end if
312+
313+
! Print ylabel (simple horizontal placement for now)
314+
if (allocated(this%ylabel_text)) then
315+
print '(A)', this%ylabel_text
316+
end if
312317
end subroutine output_to_terminal
313318

314319
subroutine output_to_file(this, unit)
@@ -339,6 +344,11 @@ subroutine output_to_file(this, unit)
339344
if (allocated(this%xlabel_text)) then
340345
call write_centered_title(unit, this%xlabel_text, this%plot_width)
341346
end if
347+
348+
! Write ylabel to the left side of the plot if present
349+
if (allocated(this%ylabel_text)) then
350+
write(unit, '(A)') this%ylabel_text
351+
end if
342352
end subroutine output_to_file
343353

344354
integer function get_char_density(char)
@@ -871,6 +881,7 @@ subroutine ascii_draw_axes_and_labels(this, xscale, yscale, symlog_threshold, &
871881
title, xlabel, ylabel, &
872882
z_min, z_max, has_3d_plots)
873883
!! Draw axes and labels for ASCII backend
884+
use fortplot_axes, only: compute_scale_ticks, format_tick_label, MAX_TICKS
874885
class(ascii_context), intent(inout) :: this
875886
character(len=*), intent(in) :: xscale, yscale
876887
real(wp), intent(in) :: symlog_threshold
@@ -880,6 +891,10 @@ subroutine ascii_draw_axes_and_labels(this, xscale, yscale, symlog_threshold, &
880891
logical, intent(in) :: has_3d_plots
881892

882893
real(wp) :: label_x, label_y
894+
real(wp) :: x_tick_positions(MAX_TICKS), y_tick_positions(MAX_TICKS)
895+
integer :: num_x_ticks, num_y_ticks, i
896+
character(len=50) :: tick_label
897+
real(wp) :: tick_x, tick_y
883898

884899
! ASCII backend: explicitly set title and draw simple axes
885900
if (present(title)) then
@@ -892,6 +907,24 @@ subroutine ascii_draw_axes_and_labels(this, xscale, yscale, symlog_threshold, &
892907
call this%line(x_min, y_min, x_max, y_min)
893908
call this%line(x_min, y_min, x_min, y_max)
894909

910+
! Generate tick marks and labels for ASCII
911+
! X-axis ticks (drawn as characters along bottom axis)
912+
call compute_scale_ticks(xscale, x_min, x_max, symlog_threshold, x_tick_positions, num_x_ticks)
913+
do i = 1, num_x_ticks
914+
tick_x = x_tick_positions(i)
915+
! For ASCII, draw tick marks as characters in the text output
916+
tick_label = format_tick_label(tick_x, xscale)
917+
call this%text(tick_x, y_min - 0.05_wp * (y_max - y_min), trim(tick_label))
918+
end do
919+
920+
! Y-axis ticks (drawn as characters along left axis)
921+
call compute_scale_ticks(yscale, y_min, y_max, symlog_threshold, y_tick_positions, num_y_ticks)
922+
do i = 1, num_y_ticks
923+
tick_y = y_tick_positions(i)
924+
tick_label = format_tick_label(tick_y, yscale)
925+
call this%text(x_min - 0.1_wp * (x_max - x_min), tick_y, trim(tick_label))
926+
end do
927+
895928
! Store xlabel and ylabel for rendering outside the plot frame
896929
if (present(xlabel)) then
897930
if (allocated(xlabel)) then

src/fortplot_axes.f90

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module fortplot_axes
1111
implicit none
1212

1313
private
14-
public :: compute_scale_ticks, format_tick_label
14+
public :: compute_scale_ticks, format_tick_label, MAX_TICKS
1515

1616
integer, parameter :: MAX_TICKS = 20
1717

@@ -141,14 +141,33 @@ function format_tick_label(value, scale_type) result(label)
141141
real(wp), intent(in) :: value
142142
character(len=*), intent(in) :: scale_type
143143
character(len=20) :: label
144+
real(wp) :: abs_value
144145

145-
if (abs(value) < 1.0e-10_wp) then
146+
abs_value = abs(value)
147+
148+
if (abs_value < 1.0e-10_wp) then
146149
label = '0'
147150
else if (trim(scale_type) == 'log' .and. is_power_of_ten(value)) then
148151
label = format_power_of_ten(value)
152+
else if (abs_value >= 1000.0_wp .or. abs_value < 0.01_wp) then
153+
! Use scientific notation for very large or very small values
154+
write(label, '(ES10.2)') value
155+
label = adjustl(label)
156+
else if (abs_value >= 100.0_wp) then
157+
! No decimal places for values >= 100
158+
write(label, '(F0.0)') value
159+
else if (abs_value >= 10.0_wp) then
160+
! One decimal place for values >= 10
161+
write(label, '(F0.1)') value
162+
else if (abs_value >= 1.0_wp) then
163+
! Two decimal places for values >= 1
164+
write(label, '(F0.2)') value
149165
else
150-
write(label, '(G0)') value
166+
! Three decimal places for small values
167+
write(label, '(F0.3)') value
151168
end if
169+
170+
label = adjustl(label)
152171
end function format_tick_label
153172

154173

src/fortplot_figure_core.f90

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -934,12 +934,10 @@ subroutine render_figure_axes(self)
934934
self%title, self%xlabel, self%ylabel, &
935935
0.0_wp, 0.0_wp, .false.)
936936
type is (ascii_context)
937-
! ASCII backend: explicitly set title and draw simple axes
938-
if (allocated(self%title)) then
939-
call backend%set_title(self%title)
940-
end if
941-
call self%backend%line(self%x_min, self%y_min, self%x_max, self%y_min)
942-
call self%backend%line(self%x_min, self%y_min, self%x_min, self%y_max)
937+
call backend%draw_axes_and_labels_backend(self%xscale, self%yscale, self%symlog_threshold, &
938+
self%x_min, self%x_max, self%y_min, self%y_max, &
939+
self%title, self%xlabel, self%ylabel, &
940+
0.0_wp, 0.0_wp, .false.)
943941
class default
944942
! For other backends, use simple axes
945943
call self%backend%line(self%x_min, self%y_min, self%x_max, self%y_min)

src/fortplot_raster.f90

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,8 @@ subroutine raster_draw_axes_and_labels(this, xscale, yscale, symlog_threshold, &
946946
title, xlabel, ylabel, &
947947
z_min, z_max, has_3d_plots)
948948
!! Draw axes and labels for raster backends
949+
use fortplot_axes, only: compute_scale_ticks, format_tick_label, MAX_TICKS
950+
use fortplot_text, only: calculate_text_width, calculate_text_height
949951
class(raster_context), intent(inout) :: this
950952
character(len=*), intent(in) :: xscale, yscale
951953
real(wp), intent(in) :: symlog_threshold
@@ -955,14 +957,84 @@ subroutine raster_draw_axes_and_labels(this, xscale, yscale, symlog_threshold, &
955957
logical, intent(in) :: has_3d_plots
956958

957959
real(wp) :: label_x, label_y
960+
real(wp) :: x_tick_positions(MAX_TICKS), y_tick_positions(MAX_TICKS)
961+
integer :: num_x_ticks, num_y_ticks, i
962+
character(len=50) :: tick_label
963+
real(wp) :: tick_x, tick_y
964+
integer :: tick_length ! Tick length in pixels
965+
integer :: px, py, text_width, text_height
966+
real(wp) :: line_r, line_g, line_b
967+
integer(1) :: text_r, text_g, text_b
968+
real(wp) :: dummy_pattern(1) ! Dummy pattern for solid lines
969+
real(wp) :: pattern_dist ! Pattern distance (mutable)
970+
character(len=500) :: processed_text, escaped_text
971+
integer :: processed_len
958972

959973
! Set color to black for axes and text
960974
call this%color(0.0_wp, 0.0_wp, 0.0_wp)
975+
line_r = 0.0_wp; line_g = 0.0_wp; line_b = 0.0_wp ! Black color for lines
976+
text_r = 0; text_g = 0; text_b = 0 ! Black color for text
961977

962978
! Draw axes
963979
call this%line(x_min, y_min, x_max, y_min)
964980
call this%line(x_min, y_min, x_min, y_max)
965981

982+
! Generate and draw tick marks and labels
983+
tick_length = 5 ! Tick length in pixels
984+
985+
! X-axis ticks
986+
call compute_scale_ticks(xscale, x_min, x_max, symlog_threshold, x_tick_positions, num_x_ticks)
987+
do i = 1, num_x_ticks
988+
tick_x = x_tick_positions(i)
989+
! Transform tick position to pixel coordinates
990+
px = int((tick_x - x_min) / (x_max - x_min) * real(this%plot_area%width, wp) + real(this%plot_area%left, wp))
991+
py = this%plot_area%bottom + this%plot_area%height ! Bottom of plot area
992+
993+
! Draw tick mark down from axis
994+
dummy_pattern = 0.0_wp
995+
pattern_dist = 0.0_wp
996+
call draw_styled_line(this%raster%image_data, this%width, this%height, &
997+
real(px, wp), real(py, wp), real(px, wp), real(py + tick_length, wp), &
998+
line_r, line_g, line_b, 1.0_wp, 'solid', dummy_pattern, 0, 0.0_wp, pattern_dist)
999+
1000+
! Draw tick label below tick mark
1001+
tick_label = format_tick_label(tick_x, xscale)
1002+
call process_latex_in_text(trim(tick_label), processed_text, processed_len)
1003+
call escape_unicode_for_raster(processed_text(1:processed_len), escaped_text)
1004+
text_width = calculate_text_width(trim(escaped_text))
1005+
! Center the label horizontally under the tick
1006+
call render_text_to_image(this%raster%image_data, this%width, this%height, &
1007+
px - text_width/2, py + tick_length + 5, trim(escaped_text), text_r, text_g, text_b)
1008+
end do
1009+
1010+
! Y-axis ticks
1011+
call compute_scale_ticks(yscale, y_min, y_max, symlog_threshold, y_tick_positions, num_y_ticks)
1012+
do i = 1, num_y_ticks
1013+
tick_y = y_tick_positions(i)
1014+
! Transform tick position to pixel coordinates
1015+
px = this%plot_area%left ! Left edge of plot area
1016+
py = int(real(this%plot_area%bottom + this%plot_area%height, wp) - &
1017+
(tick_y - y_min) / (y_max - y_min) * real(this%plot_area%height, wp))
1018+
1019+
! Draw tick mark to the left from axis
1020+
dummy_pattern = 0.0_wp
1021+
pattern_dist = 0.0_wp
1022+
call draw_styled_line(this%raster%image_data, this%width, this%height, &
1023+
real(px - tick_length, wp), real(py, wp), real(px, wp), real(py, wp), &
1024+
line_r, line_g, line_b, 1.0_wp, 'solid', dummy_pattern, 0, 0.0_wp, pattern_dist)
1025+
1026+
! Draw tick label to the left of tick mark
1027+
tick_label = format_tick_label(tick_y, yscale)
1028+
call process_latex_in_text(trim(tick_label), processed_text, processed_len)
1029+
call escape_unicode_for_raster(processed_text(1:processed_len), escaped_text)
1030+
text_width = calculate_text_width(trim(escaped_text))
1031+
text_height = calculate_text_height(trim(escaped_text))
1032+
! Right-align the label to the left of the tick
1033+
call render_text_to_image(this%raster%image_data, this%width, this%height, &
1034+
px - tick_length - text_width - 5, py - text_height/2, &
1035+
trim(escaped_text), text_r, text_g, text_b)
1036+
end do
1037+
9661038
! Draw title at top if present
9671039
if (present(title)) then
9681040
if (allocated(title)) then
@@ -972,23 +1044,32 @@ subroutine raster_draw_axes_and_labels(this, xscale, yscale, symlog_threshold, &
9721044
end if
9731045
end if
9741046

975-
! Draw xlabel centered below x-axis
1047+
! Draw xlabel centered below x-axis (below tick labels)
9761048
if (present(xlabel)) then
9771049
if (allocated(xlabel)) then
978-
label_x = (x_min + x_max) / 2.0_wp
979-
label_y = y_min - 0.05_wp * (y_max - y_min)
980-
call this%text(label_x, label_y, xlabel)
1050+
call process_latex_in_text(xlabel, processed_text, processed_len)
1051+
call escape_unicode_for_raster(processed_text(1:processed_len), escaped_text)
1052+
text_width = calculate_text_width(trim(escaped_text))
1053+
! Center horizontally in plot area, position below tick labels
1054+
px = this%plot_area%left + this%plot_area%width / 2 - text_width / 2
1055+
py = this%plot_area%bottom + this%plot_area%height + 30 ! 30 pixels below plot area
1056+
call render_text_to_image(this%raster%image_data, this%width, this%height, &
1057+
px, py, trim(escaped_text), text_r, text_g, text_b)
9811058
end if
9821059
end if
9831060

984-
! Draw ylabel to the left of y-axis
1061+
! Draw ylabel to the left of y-axis (rotated would be better, but for now horizontal)
9851062
if (present(ylabel)) then
9861063
if (allocated(ylabel)) then
987-
! For raster, ylabel needs special handling for rotation
988-
! For now, just place it horizontally
989-
label_x = x_min - 0.1_wp * (x_max - x_min)
990-
label_y = (y_min + y_max) / 2.0_wp
991-
call this%text(label_x, label_y, ylabel)
1064+
call process_latex_in_text(ylabel, processed_text, processed_len)
1065+
call escape_unicode_for_raster(processed_text(1:processed_len), escaped_text)
1066+
text_width = calculate_text_width(trim(escaped_text))
1067+
text_height = calculate_text_height(trim(escaped_text))
1068+
! Position to the left of plot area, centered vertically
1069+
px = 10 ! 10 pixels from left edge
1070+
py = this%plot_area%bottom + this%plot_area%height / 2 - text_height / 2
1071+
call render_text_to_image(this%raster%image_data, this%width, this%height, &
1072+
px, py, trim(escaped_text), text_r, text_g, text_b)
9921073
end if
9931074
end if
9941075
end subroutine raster_draw_axes_and_labels
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
program test_axes_labels_comprehensive
2+
!! Comprehensive test for Issue #335: Missing axis labels and tick marks
3+
!! Tests that all axis text elements are properly rendered in both PNG and ASCII
4+
5+
use fortplot
6+
implicit none
7+
8+
real(wp), dimension(6) :: x_data, y_linear, y_log
9+
integer :: i
10+
11+
print *, "=== Comprehensive Axes Labels Test ==="
12+
13+
! Generate test data
14+
x_data = [(real(i, wp), i = 1, 6)]
15+
y_linear = x_data * 2.0_wp + 1.0_wp ! Linear: 3, 5, 7, 9, 11, 13
16+
y_log = exp(x_data * 0.5_wp) ! Exponential for log scale
17+
18+
! Test 1: Linear scale with all axis elements
19+
print *, "Testing linear scale with comprehensive axis labeling..."
20+
call figure()
21+
call plot(x_data, y_linear)
22+
call title('Linear Scale Test - All Elements')
23+
call xlabel('Input Values (x)')
24+
call ylabel('Linear Response (2x+1)')
25+
call savefig('test_linear_axes_comprehensive.png')
26+
call savefig('test_linear_axes_comprehensive.txt')
27+
28+
! Test 2: Log scale with proper formatting
29+
print *, "Testing log scale with scientific notation..."
30+
call figure()
31+
call plot(x_data, y_log)
32+
call set_yscale('log')
33+
call title('Log Scale Test - Scientific Labels')
34+
call xlabel('Input Index')
35+
call ylabel('Exponential Growth')
36+
call savefig('test_log_axes_comprehensive.png')
37+
call savefig('test_log_axes_comprehensive.txt')
38+
39+
print *, ""
40+
print *, "VERIFICATION CHECKLIST:"
41+
print *, "========================"
42+
print *, "For each output file, verify:"
43+
print *, "1. Title text is visible at top"
44+
print *, "2. X-axis label appears below plot"
45+
print *, "3. Y-axis label appears left of plot"
46+
print *, "4. Tick marks are visible on axes"
47+
print *, "5. Tick labels show numerical values"
48+
print *, "6. Log scale uses appropriate notation"
49+
print *, ""
50+
print *, "Files created:"
51+
print *, "- test_linear_axes_comprehensive.png/txt"
52+
print *, "- test_log_axes_comprehensive.png/txt"
53+
54+
end program test_axes_labels_comprehensive

0 commit comments

Comments
 (0)