Announcing UnicodePlots v2.8.0, which brings up 3D plots.
Examples:
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.
Announcing UnicodePlots v2.8.0, which brings up 3D plots.
Examples:
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! ![]()
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 ![]()
Amazing.
Is there any support for animations?
There is Makie.jl for that, and for animations
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. ![]()
![]()
![]()
How cool - I definitely want to try to find a way to work it into my current work/packages ![]()
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()

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()

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

It’s a bit jumpy, but saves the day on diagnosing a headless server without XForwarding…
It helps if you hide the cursor ![]()
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:

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()

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.)