[ANN] UnicodePlots.jl v2.8.0 - 3D plots

Announcing UnicodePlots v2.8.0, which brings up 3D plots.

Examples:

surfaceplot


isosurface

Useful for in situ post-processing of simulations, or as a development helper for 3D codes.

In case you missed out, contourplot was also recently added.

This is crazy and amazing! :slight_smile:

Very cool! Any chance there might be 3D scatter plots? We would have some applications for that, though they might be difficult for human consumption in general.

Yes, I’ve modified scatterplot and lineplot to take z values, so you can use something like:

scatterplot(
  x, y, z;
  projection = :orthographic  # or :perspective, or a full `MVP` matrix.
)

Internally, surfaceplot uses points! unless kw lines = true is passed (which is slower and more dense).

Inconceivable!

A dream come true!
Now we need interactivity :innocent:

Amazing.

Is there any support for animations?

There is Makie.jl for that, and for animations :slight_smile: I don’t think this should be the target of UnicodePlots (which is intended to be a lightweight swiss army knife with very few dependencies, that just works right out of the box).

EDIT: after discussion with @AlexisRenchon on slack, we can maybe make animations work from within Plots.jl, since UnicodePlots is a supported backend.

This is fantastic! I loved this package before, but now there is simply no Julia for me without UnicodePlots. :muscle:t2::blush::pray:t2:

How cool - I definitely want to try to find a way to work it into my current work/packages :slight_smile:

using Plots; unicodeplots()  # using `Plots@master`, with linux ;), maybe works on macOS

main() = begin
  anim = @animate for i = -180:10:180
    surface(
      -8:.5:8, -8:.5:8, (x, y) -> 15sinc(√(x^2 + y^2) / π),
      colormap=:jet, camera=(i, 30)
    )
  end
  gif(anim, "anim_fps5.gif", fps=5)
end

main()

anim_fps5

using Plots; unicodeplots()

main() = begin
  Plots.UnicodePlots.default_size!(width=120)
  anim = @animate for i ∈ -180:10:180
    surface(
      -8:8, -8:8, (x, y) -> 15sinc(√(x^2 + y^2) / π),
      camera=(i, 30),
      extra_kwargs = Dict(
        :subplot => (zoom=1.25,),
        :series =>(colormap=:jet, lines=true)
      )
    )
  end
  gif(anim, "anim_lines_fps5.gif", fps=5)
end

main()

anim_lines_fps5

and it may be the only backend that supports multi-threading animation (making independent frames in parallel) because all other backends are stateful as hell

I just wanted to report back here that I managed to get relatively good “animation” just by clearing the REPL before plotting (Thanks to @Tamas_Papp here). This allows for live plotting (i.e. closer to using observables like in Makie).

MWE:

using UnicodePlots
import REPL
terminal = REPL.Terminals.TTYTerminal("", stdin, stdout, stderr)

xs = range(0, 7, length=40)
fps = 30
for t in 0:1/fps:100
  ys = sin.(xs .- t)
  REPL.Terminals.clear(terminal)
  println(lineplot(xs, ys, xlim = (0, 7), ylim = (-1, 1)))
  sleep(1/fps)
end

animation

It’s a bit jumpy, but saves the day on diagnosing a headless server without XForwarding…

It helps if you hide the cursor :slight_smile:

https://github.com/cesaraustralia/DynamicGrids.jl/blob/master/src/outputs/repl.jl#L55-L71

DynamicGrids.jl has repl simulations and they’re pretty smooth using these tricks

Right you are:
animation

Still some flickering…

Thanks for sharing this !

Still some flickering…

You can move the show logic before clearing the screen:

using UnicodePlots
import REPL

_cursor_hide(io::IO) = print(io, "\x1b[?25l")
_cursor_show(io::IO) = print(io, "\x1b[?25h")

main() = begin
  terminal = REPL.Terminals.TTYTerminal("", stdin, stdout, stderr)

  fps = 30
  xs = range(0, 7, length=40)
  _cursor_hide(stdout)
  io = IOContext(PipeBuffer(), :color=>true)
  for t ∈ 0:(1 / fps):100
    ys = sin.(xs .- t)
    show(io, lineplot(xs, ys, xlim=(0, 7), ylim=(-1, 1)))
    out = read(io, String)
    REPL.Terminals.clear(terminal)
    println(out)
    sleep(1 / fps)
  end
  _cursor_show(stdout)
  return
end

main()

animation
Better…

Ooooh… when I were lad I used the CERN HBOOK package to plot histograms on line printers using music ruled paper.
I guess I can still do the same - if I Can find a line printer!
Seriously - the 3D stuff is coooollll

Stupid (in a good way) that unicode plotting in the REPL is almost at parity with other plotting libraries in Julia haha

Apologies for bringing this thread back to life, but here is a slightly refactored version of the script @t-bltg wrote above in terms of a function which outputs the plot at each frame:

"""
    animate(f; nframes, fps=30)

Call `f(i)` for each frame `i in 1:nframes`, where `f(i)` returns an object to print to the
terminal at each frame. Renders each frame in-place in the terminal at `fps` frames per
second.
"""
function animate(f; nframes::Int, fps::Real=30)
    io = IOBuffer()
    # Hide cursor.
    print(stdout, "\x1b[?25l")
    try
        # Clear screen.
        print(stdout, "\x1b[2J")
        for i in 1:nframes
            seekstart(io)
            show(io, MIME("text/plain"), f(i))
            frame_str = String(take!(io))
            # Move home.
            print(stdout, "\x1b[H")
            println(frame_str)
            sleep(1 / fps)
        end
    finally
        # Show cursor again.
        print(stdout, "\x1b[?25h")
    end
end

using Plots: Plots, plot
Plots.unicodeplots()
xs = range(0, 7; length=40)
animate(; nframes=50, fps=10) do i
    return plot(xs, sin.(xs .- 0.2i); ylim=(-1,1))
end

(written with the help of ChatGPT.)