Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:

# Verify all example pages were generated
echo "Checking generated example pages..."
expected_pages="3d_plotting animation annotation_demo ascii_heatmap bar_chart_demo basic_plots boxplot_demo contour_demo disconnected_lines display_demo dpi_demo errorbar_demo fill_between_demo grid_demo legend_demo mathtext_demo pcolormesh_demo pie_chart_demo polar_demo quiver_demo scale_examples scatter_demo streamplot_demo styling_demo subplot_demo twin_axes_demo unicode_demo"
expected_pages="3d_animation_demo 3d_plotting animation annotation_demo ascii_heatmap bar_chart_demo basic_plots boxplot_demo contour_demo disconnected_lines display_demo dpi_demo errorbar_demo fill_between_demo grid_demo legend_demo mathtext_demo pcolormesh_demo pie_chart_demo polar_demo quiver_demo scale_examples scatter_demo streamplot_demo styling_demo subplot_demo twin_axes_demo unicode_demo"
expected_count=$(echo $expected_pages | wc -w)
echo "Expecting $expected_count example pages"

Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ doc:
$(MAKE) example ARGS="marker_demo" >/dev/null
# Generate animation demo so MP4 is available for docs (fixes #1085)
$(MAKE) example ARGS="save_animation_demo" >/dev/null
# Generate 3D animation demo so MP4 + ASCII frames are available for docs
$(MAKE) example ARGS="3d_animation_demo" >/dev/null
# Generate doc.md from README.md (strip badge and title - FORD adds title from fpm.toml)
grep -v 'img.shields.io' README.md | sed '1{/^# fortplot$$/d}' > doc.md
# Run FORD to generate documentation structure
Expand Down Expand Up @@ -251,6 +253,7 @@ create_build_dirs:
@mkdir -p output/example/fortran/legend_box_demo
@mkdir -p output/example/fortran/unicode_demo
@mkdir -p output/example/fortran/save_animation_demo
@mkdir -p output/example/fortran/3d_animation_demo
@mkdir -p output/example/fortran/annotation_demo
@mkdir -p output/example/fortran/histogram_demo
@mkdir -p output/example/fortran/subplot_demo
Expand Down
25 changes: 11 additions & 14 deletions app/update_example_index.f90
Original file line number Diff line number Diff line change
Expand Up @@ -152,21 +152,18 @@ end subroutine sort_names

logical function path_is_directory(path)
character(len=*), intent(in) :: path
character(len=1) :: probe(1)
integer :: probe_count
integer :: status

probe = ''
call list_directory_entries(trim(path), probe, probe_count, status)

select case (status)
case (0)
logical :: exists
! Use the trailing-slash inquire trick: only directories report
! `<path>/.` as existing. Plain files would otherwise pass the old
! `ls path` probe and be misclassified as directories.
path_is_directory = .false.
inquire(file=trim(path)//"/.", exist=exists)
if (exists) then
path_is_directory = .true.
case (-6)
path_is_directory = .true.
case default
path_is_directory = .false.
end select
return
end if
inquire(file=trim(path)//"/", exist=exists)
if (exists) path_is_directory = .true.
end function path_is_directory

subroutine ensure_entry_capacity(buffer, string_len, new_capacity)
Expand Down
2 changes: 1 addition & 1 deletion doc/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ make example ARGS="example_name"

<!-- AUTO_EXAMPLES_START -->

- [3D Animation Demo](https://github.com/lazy-fortran/fortplot/tree/main/example/fortran/3d_animation_demo) - Documentation pending; browse the source tree.
- [3D Plotting](./3d_plotting.html) - 3D plotting (lines and surfaces) with axes, ticks, and labels.
- [Animation](./animation.html) - Generate an MP4 animation from a sequence of frames.
- [Annotation Demo](./annotation_demo.html) - Add text annotations in data coordinates.
Expand All @@ -33,7 +34,6 @@ make example ARGS="example_name"
- [Polar Demo](./polar_demo.html) - Demonstrates fortplot's polar plotting API with custom colors, linestyles, and markers.
- [Probability Animation Demo](./probability_animation_demo.html) - See source and outputs below.
- [Quiver Demo](./quiver_demo.html) - See source and outputs below.
- [Readme.md](https://github.com/lazy-fortran/fortplot/tree/main/example/fortran/README.md) - Documentation pending; browse the source tree.
- [Scale Examples](./scale_examples.html) - Linear, log, and symlog axis scales.
- [Scatter Demo](./scatter_demo.html) - Demonstrates enhanced scatter plotting with color mapping, variable marker sizes, and bubble charts.
- [Streamplot Demo](./streamplot_demo.html) - Streamplots for 2D vector fields.
Expand Down
2 changes: 1 addition & 1 deletion doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ make example ARGS="example_name"

<!-- AUTO_EXAMPLES_START -->

- [3D Animation Demo](https://github.com/lazy-fortran/fortplot/tree/main/example/fortran/3d_animation_demo) - Documentation pending; browse the source tree.
- [3D Plotting](./examples/3d_plotting.html) - 3D plotting (lines and surfaces) with axes, ticks, and labels.
- [Readme.md](https://github.com/lazy-fortran/fortplot/tree/main/example/fortran/README.md) - Documentation pending; browse the source tree.
- [Animation](./examples/animation.html) - Generate an MP4 animation from a sequence of frames.
- [Annotation Demo](./examples/annotation_demo.html) - Add text annotations in data coordinates.
- [Ascii Heatmap](./examples/ascii_heatmap.html) - Render a heatmap to terminal-friendly ASCII output.
Expand Down
92 changes: 92 additions & 0 deletions example/fortran/3d_animation_demo/3d_animation_demo.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
program three_d_animation_demo
!! Animate a rotating 3D Lissajous curve and save it to MP4 and ASCII.
!!
!! Demonstrates that animation works the same way for vector/raster
!! backends (.mp4) and the ASCII backend (.txt).
use iso_fortran_env, only: wp => real64
use fortplot
use fortplot_animation
use fortplot_system_runtime, only: create_directory_runtime
implicit none

integer, parameter :: N_POINTS = 200
integer, parameter :: N_FRAMES = 60
character(len=*), parameter :: OUTDIR = "output/example/fortran/3d_animation_demo"

type(figure_t), pointer :: pfig
type(animation_t) :: anim
real(wp), dimension(N_POINTS) :: xs, ys, zs
real(wp) :: t
integer :: i, status
logical :: ok

call create_directory_runtime(OUTDIR, ok)
if (.not. ok) print *, "WARNING: could not create ", OUTDIR

do i = 1, N_POINTS
t = real(i - 1, wp) * 2.0_wp * 3.141592653589793_wp / real(N_POINTS - 1, wp)
xs(i) = sin(3.0_wp * t)
ys(i) = cos(2.0_wp * t)
zs(i) = sin(t) * cos(t)
end do

call figure(figsize=[8.0_wp, 6.0_wp])
pfig => get_global_figure()
call add_3d_plot(xs, ys, zs, label="Lissajous 3D", linestyle='-')
call title("Rotating 3D Lissajous")

anim = FuncAnimation(rotate_curve, frames=N_FRAMES, interval=33, fig=pfig)

print *, "Saving 3D animation as MP4..."
call save_animation(anim, OUTDIR // "/animation.mp4", fps=30, status=status)
call report_status("MP4", status)

print *, "Saving 3D animation as ASCII..."
call save_animation(anim, OUTDIR // "/animation.txt", status=status)
call report_status("ASCII", status)

if (status == 0) then
print *, "Replay with: fpm run --target fortplot_play_ascii -- ", &
OUTDIR // "/animation.txt --fps 24 --loop"
end if

contains

subroutine rotate_curve(frame)
integer, intent(in) :: frame
real(wp) :: phase, c, s
integer :: k

phase = real(frame - 1, wp) * 2.0_wp * 3.141592653589793_wp / real(N_FRAMES, wp)
c = cos(phase)
s = sin(phase)
do k = 1, N_POINTS
t = real(k - 1, wp) * 2.0_wp * 3.141592653589793_wp / real(N_POINTS - 1, wp)
xs(k) = c * sin(3.0_wp * t) - s * cos(2.0_wp * t)
ys(k) = s * sin(3.0_wp * t) + c * cos(2.0_wp * t)
zs(k) = sin(t) * cos(t)
end do

call pfig%clear()
call add_3d_plot(xs, ys, zs, label="Lissajous 3D", linestyle='-')
call title("Rotating 3D Lissajous")
call pfig%set_rendered(.false.)
end subroutine rotate_curve

subroutine report_status(label, st)
character(len=*), intent(in) :: label
integer, intent(in) :: st

select case (st)
case (0)
print *, label // ": OK"
case (-1)
print *, label // ": ffmpeg not found (.mp4 needs ffmpeg)"
case (-3)
print *, label // ": unsupported file format"
case default
print *, label // ": save failed, status=", st
end select
end subroutine report_status

end program three_d_animation_demo
10 changes: 10 additions & 0 deletions example/fortran/3d_animation_demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title: 3D Animation

Animate a rotating 3D Lissajous curve. The same `FuncAnimation` plus
`save_animation` pipeline writes both MP4 (raster backend rendered through
ffmpeg) and a `.txt` ASCII frame stream replayable with `fortplot_play_ascii`.

```bash
fpm run --example --target 3d_animation_demo
fpm run --target fortplot_play_ascii -- output/example/fortran/3d_animation_demo/animation.txt --fps 24 --loop
```
35 changes: 7 additions & 28 deletions src/backends/ascii/fortplot_ascii_text.f90
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ subroutine draw_ascii_axes_and_labels(canvas, xscale, yscale, symlog_threshold,
character(len=50) :: tick_label
real(wp) :: tick_x, tick_y
integer :: decimals
real(wp) :: luminance
character(len=1) :: line_char
character(len=1), parameter :: AXIS_HORIZ_CHAR = '-'
character(len=1), parameter :: AXIS_VERT_CHAR = '|'
character(len=500) :: processed_title
integer :: processed_len
logical :: use_custom_xticks
Expand All @@ -78,36 +78,15 @@ subroutine draw_ascii_axes_and_labels(canvas, xscale, yscale, symlog_threshold,
end if
end if

! Calculate luminance for better character selection
luminance = 0.299_wp*current_r + 0.587_wp*current_g + 0.114_wp*current_b

! Select character based on color dominance and luminance
if (luminance > 0.9_wp) then
line_char = ':'
else if (current_g > 0.7_wp) then
line_char = '@'
else if (current_g > 0.3_wp) then
line_char = '#'
else if (current_b > 0.7_wp) then
line_char = '*'
else if (current_b > 0.3_wp) then
line_char = 'o'
else if (current_r > 0.7_wp) then
line_char = '%'
else if (current_r > 0.3_wp) then
line_char = '+'
else
line_char = '.'
end if

! Draw horizontal axis
! Draw horizontal/vertical axis with stable characters so the axis
! frame does not flicker between animation frames as the active plot
! color changes.
call draw_line_on_canvas_local(canvas, x_min, y_min, x_max, y_min, &
x_min, x_max, y_min, y_max, plot_width, &
plot_height, line_char)
! Draw vertical axis
plot_height, AXIS_HORIZ_CHAR)
call draw_line_on_canvas_local(canvas, x_min, y_min, x_min, y_max, &
x_min, x_max, y_min, y_max, plot_width, &
plot_height, line_char)
plot_height, AXIS_VERT_CHAR)

! Generate tick marks and labels for ASCII
! X-axis ticks (drawn as characters along bottom axis)
Expand Down
155 changes: 155 additions & 0 deletions test/test_3d_backend_parity.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
program test_3d_backend_parity
!! Verify 3D rendering and 3D animation work across PNG, PDF, and ASCII.
!!
!! Covers:
!! * 3D line plot (add_3d_plot) -> .png, .pdf, .txt
!! * 3D surface plot (add_surface) -> .png, .pdf, .txt
!! * 3D animation -> .txt (frame headers must be present)
use iso_fortran_env, only: wp => real64, iostat_end
use fortplot
use fortplot_animation
use fortplot_ascii_player, only: count_animation_frames
use fortplot_system_runtime, only: create_directory_runtime, delete_file_runtime
implicit none

integer, parameter :: NLINE = 64
integer, parameter :: NGRID = 9
integer, parameter :: NFRAMES = 4
character(len=*), parameter :: OUTDIR = "build/test/output/3d_parity"

type(figure_t), pointer :: pfig
type(animation_t) :: anim
real(wp) :: x(NLINE), y(NLINE), z(NLINE)
real(wp) :: xg(NGRID), yg(NGRID), zg(NGRID, NGRID)
integer :: i, j, status, n_frames_in_file
logical :: ok

call create_directory_runtime(OUTDIR, ok)
if (.not. ok) error stop "could not create build/test/output/3d_parity"

call build_helix(x, y, z)
call build_gauss(xg, yg, zg)

call run_3d_line_render('.png')
call run_3d_line_render('.pdf')
call run_3d_line_render('.txt')

call run_3d_surface_render('.png')
call run_3d_surface_render('.pdf')
call run_3d_surface_render('.txt')

call run_3d_animation_ascii()

print *, "PASS test_3d_backend_parity"

contains

subroutine build_helix(xa, ya, za)
real(wp), intent(out) :: xa(:), ya(:), za(:)
integer :: k
do k = 1, size(xa)
xa(k) = cos(real(k - 1, wp) * 0.2_wp)
ya(k) = sin(real(k - 1, wp) * 0.2_wp)
za(k) = real(k - 1, wp) * 0.05_wp
end do
end subroutine build_helix

subroutine build_gauss(xa, ya, za)
real(wp), intent(out) :: xa(:), ya(:), za(:, :)
integer :: ii, jj
do ii = 1, size(xa)
xa(ii) = -2.0_wp + (ii - 1) * 4.0_wp / real(size(xa) - 1, wp)
ya(ii) = -2.0_wp + (ii - 1) * 4.0_wp / real(size(ya) - 1, wp)
end do
do ii = 1, size(xa)
do jj = 1, size(ya)
za(ii, jj) = exp(-(xa(ii)**2 + ya(jj)**2))
end do
end do
end subroutine build_gauss

subroutine run_3d_line_render(ext)
character(len=*), intent(in) :: ext
character(len=:), allocatable :: path
path = OUTDIR // "/line" // ext
call figure(figsize=[6.0_wp, 4.0_wp])
call add_3d_plot(x, y, z, label="helix")
call title("3D line " // ext)
call savefig(path)
call assert_nonempty(path, "3D line " // ext)
end subroutine run_3d_line_render

subroutine run_3d_surface_render(ext)
character(len=*), intent(in) :: ext
character(len=:), allocatable :: path
path = OUTDIR // "/surface" // ext
call figure(figsize=[6.0_wp, 4.0_wp])
call add_surface(xg, yg, zg, cmap='viridis', filled=.true.)
call title("3D surface " // ext)
call savefig(path)
call assert_nonempty(path, "3D surface " // ext)
end subroutine run_3d_surface_render

subroutine run_3d_animation_ascii()
character(len=:), allocatable :: path
path = OUTDIR // "/anim.txt"

call figure(figsize=[6.0_wp, 4.0_wp])
pfig => get_global_figure()
call add_3d_plot(x, y, z, label="helix")

anim = FuncAnimation(rotate_helix, frames=NFRAMES, interval=10, fig=pfig)
call save_animation(anim, path, status=status)
if (status /= 0) then
print *, "save_animation .txt status=", status
error stop "3D animation save to .txt failed"
end if

call count_animation_frames(path, n_frames_in_file)
if (n_frames_in_file /= NFRAMES) then
print *, "expected ", NFRAMES, " frames, got ", n_frames_in_file
error stop "3D animation .txt frame count mismatch"
end if
end subroutine run_3d_animation_ascii

subroutine rotate_helix(frame)
integer, intent(in) :: frame
real(wp) :: phase
integer :: k
phase = real(frame - 1, wp) * 0.5_wp
do k = 1, NLINE
x(k) = cos(real(k - 1, wp) * 0.2_wp + phase)
y(k) = sin(real(k - 1, wp) * 0.2_wp + phase)
z(k) = real(k - 1, wp) * 0.05_wp
end do
call pfig%clear()
call add_3d_plot(x, y, z, label="helix")
call pfig%set_rendered(.false.)
end subroutine rotate_helix

subroutine assert_nonempty(path, what)
character(len=*), intent(in) :: path, what
integer :: unit, ios, byte_count
character(len=1) :: ch

open(newunit=unit, file=path, status='old', action='read', &
access='stream', form='unformatted', iostat=ios)
if (ios /= 0) then
print *, what // ": cannot open ", trim(path)
error stop
end if
byte_count = 0
do
read(unit, iostat=ios) ch
if (ios /= 0) exit
byte_count = byte_count + 1
if (byte_count > 256) exit
end do
close(unit)
if (byte_count < 64) then
print *, what // ": output too small (", byte_count, " bytes) at ", trim(path)
error stop
end if
end subroutine assert_nonempty

end program test_3d_backend_parity
Loading
Loading