We just released Makie's latest minor release which as usual comes with lots of bugfixes, new features and mostly invisible but important backend work to ensure Makie's long-term performance and robustness.
Before we take a closer look at those developments, there's one more thing we'd like to mention!
We're happy to announce that Makie qualified for an investment from the Sovereign Tech Agency! This allows Simon and Frederic to work on maintenance and improvements for Makie until the end of 2025. The work we'll be doing is documented in the project proposal and we'll post more about it in the future.
Now, let's get into Makie v0.22!
A new feature in 0.22 is to allow mesh color to accept a 3D array of numbers or colors. With this, one can e.g. implement slicing of volumes:
using GLMakie, GeometryBasics, NIfTI
GLMakie.activate!()
# Simple Quad
triangles = GLTriangleFace[(1, 2, 3), (3, 4, 1)]
# use positions as texture coordinates
uv3_mesh(p) = GeometryBasics.Mesh(p, triangles; uv=Vec3f.(p))
r = -5:0.1:5
brain = Float32.(niread(Makie.assetpath("brain.nii.gz")).raw)
positions = [Point3f(0.5, 0, 0), Point3f(0.5, 1, 0), Point3f(0.5, 1, 1), Point3f(0.5, 0, 1)]
# Pass the volume plot to the color
f, ax, pl = mesh(uv3_mesh(positions), color=brain, shading=NoShading, axis=(; show_axis=false))
positions = [Point3f(0.0, 0.5, 0), Point3f(1.0, 0.5, 0), Point3f(1, 0.5, 1), Point3f(0.0, 0.5, 1)]
mesh!(ax, uv3_mesh(positions); color=brain, shading=NoShading)
positions = [Point3f(0.0, 0.0, 0.3), Point3f(1.0, 0, 0.3), Point3f(1, 1, 0.3), Point3f(0.0, 1, 0.3)]
mesh!(ax, uv3_mesh(positions); color=brain, shading=NoShading)
f
The largest part of this release is a refactor of GeometryBasics. The main goal was to simplify the package, both from a user perspective and a compiler perspective. Even if you don't interact with GeometryBasics directly, you should see some improvements to TTFP (specifically using and first display time).
We removed the
meta
infrastructure used for per-vertex and per-face data in GeometryBasics. It generated a lot of type complexity, which you may have noticed before if you looked at the full type of a mesh. Instead, a
GeometryBasics.Mesh
now simply holds a
NamedTuple
of array-like data, called
vertex_attributes
. Each array is interpreted as per-vertex data.
A (raw) GeometryBasics mesh is now constructed as:
# old_mesh = GeometryBasics.mesh(meta(positions, normals = normals, uv = uvs), faces)
new_mesh = GeometryBasics.mesh(positions, faces, normal = normals, uv = uvs)
Note that
normals
is now called
normal
to match
uv
.
For per-face data, or more generally data which uses a different set of indices from other vertex attributes, we introduced the
FaceView
object. It contains some data and a vector of faces which replaces the mesh's faces to index the data.
As an example, let's look at the mesh generated for
Rect3f
, which wants per-face normals to avoid smooth shading across its edges and corners. (
facetype
is only set to shorten the output a bit.)
julia> m = normal_mesh(Rect(0,0,0, 1,1,1), facetype = QuadFace{Int64})
Mesh{3, Float32, QuadFace{Int64}}
faces: 6
vertex position: 8
vertex normal: 6
The mesh has a different number of positions and normals. If we investigate further, we find that normals are represented by a
FaceView
.
julia> m.normal
FaceView{Vec{3, Float32}, Vector{Vec{3, Float32}}, Vector{QuadFace{Int64}}}:
[-1.0, 0.0, 0.0]
[1.0, 0.0, 0.0]
[0.0, -1.0, 0.0]
[0.0, 1.0, 0.0]
[0.0, 0.0, -1.0]
[0.0, 0.0, 1.0]
The
FaceView
contains 6 normal vectors as data, which is shown above. The number of normals the mesh reports refers to them. The
FaceView
also contains 6 faces, which correspond to the 6 faces in the mesh.
julia> m.normal.faces
6-element Vector{QuadFace{Int64}}:
QuadFace{Int64}(1, 1, 1, 1)
QuadFace{Int64}(2, 2, 2, 2)
QuadFace{Int64}(3, 3, 3, 3)
QuadFace{Int64}(4, 4, 4, 4)
QuadFace{Int64}(5, 5, 5, 5)
QuadFace{Int64}(6, 6, 6, 6)
Each of these faces refers to just one index in
m.normal.data
, making that data apply per face.
You can convert a mesh with
FaceView
s to one without by calling
expand_faceviews(mesh)
. This will directly return the mesh if it does not contain
FaceView
s. Otherwise, it will build a new mesh without them, remapping indices and separating faces as needed.
julia> expand_faceviews(m)
Mesh{3, Float32, QuadFace{Int64}}
faces: 6
vertex position: 24
vertex normal: 24
To combine the per-face normals with positions, our mesh requires 3 copies of each position (one per face using that position) and 4 copies of each normal (one per vertex in the face). These are generated by
expand_faceviews(m)
.
Note that we also added a convenience function
face_normals(points, faces)
to GeometryBasics to generate a
FaceView
for per-face normals. In the docs, you can also find an example of how to use
FaceView
to set per-face colors.
We have introduced a
MetaMesh
type, which allows you to bundle arbitrary data with a
GeometryBasics.Mesh
. Any data (that does not correspond to vertices or faces) can be shipped with this type. It is now used by MeshIO when loading an
obj
file that includes a material template library (i.e., an .mtl file).
julia> using FileIO, Makie
julia> m = load(Makie.assetpath("sponza/sponza.obj"))
MetaMesh{3, Float32, NgonFace{3, OffsetInteger{-1, UInt32}}}
faces: 66450
vertex position: 60848
vertex uv: 60896
meta: [:groups, :material_names, :materials]
The material data ends up in
m[:materials]
as a nested
Dict
, where the first key is the name of the material. The names are also listed in
m[:material_names]
in the same order they are referred to by the
.obj
file. The mesh contains a new
m.mesh.views
field, which marks the subset of faces affected by each material.
m[:groups]
is also synchronized with
m.mesh.views
, containing the group names of these faces.
Makie can directly plot a
MetaMesh
as it is constructed by
MeshIO
, applying the material properties it knows how to handle. This includes textures referred to by the mtl file.
using FileIO, GLMakie
m = load(Makie.assetpath("sponza/sponza.obj"))
f,a,p = Makie.mesh(m)
display(f)
update_cam!(a.scene, Vec3f(-15, 7, 1), Vec3f(3, 5, 0), Vec3f(0,1,0))
Note that Makie can currently only handle a very limited subset of the material properties an mtl file can set. As such, the results may differ from what the mtl file sets and will improve in the future.
With version 0.22, we have introduced a few new controls to Axis3:
The content of an Axis3 can be translated by holding the right mouse button and dragging. Any content that spills outside the frame of the axis will be clipped. Translations can be restricted to a specific dimension by holding the x, y, and/or z key while translating. If
viewmode = :free
, you can also translate the whole Axis3 (content + decorations) by holding left control while translating.
The content of an Axis3 can be zoomed by scrolling the mouse wheel. By default, zoom is focused on the center of the axis. This can be changed to be focused on the cursor by setting
ax.zoommode[] = :cursor
. Like with translations, content outside the frame is clipped, and the x, y, and/or z key can be used to restrict zooming to specific dimensions. If
viewmode = :free
, zooming always affects the whole Axis3 (content + decorations) and is always focused on the center of the axis.
The limit reset works similarly to Axis. By pressing left control and the left mouse button, limits are reset to the previously set user limits or, if none exist, default limits. Adding left shift results in a full reset, removing user limits if any exist. In the context of Axis3, the former resets zoom and translation of axis content. The latter also resets rotation and translation of the whole Axis3. The latter can also be activated without the former by just pressing left shift and the left mouse button.
You can center the Axis3 on the data under the cursor by pressing left alt and the left mouse button. With
zoommode = :center
(or
viewmode = :free
), this will translate that data point in such a way that you can zoom towards it without clipping into it.
In previous versions,
marker_offset
was used to center scatter markers, but it could also be set by the user to specify some other offset. This was somewhat confusing as
marker_offset = Vec2f(0)
did not result in a centered marker. It also did not work with
BezierPath
markers, which have become the default.
In this release, we separated the centering into an internal attribute, so that
marker_offset
is a pure user attribute. With this,
marker_offset = 0
now results in the same centered marker as not specifying it would. It also now works consistently for all marker types and is no longer affected by the
rotation
attribute.
Scatter
has a
transform_marker::Bool
attribute which controls whether the model matrix (i.e.,
translate!()
,
rotate!()
,
zoom!()
) affects the marker.
MeshScatter
now also has this attribute. It is set to
false
by default, which changes the behavior from the previous version. Most notably, this will affect the shape of meshscatter objects in an Axis3. Where previously they were scaled based on the limits of the Axis, they now preserve their shape and size.
We have cleaned up two rendering pipelines in CairoMakie. The first is the meshscatter/voxel/surface/mesh pipeline. It previously handled transformations incorrectly, always applying
transform_func
and
model
to (generated) mesh vertices. This is correct for
mesh
and
surface
but not
meshscatter
and
voxel
. It also didn't allow for meshes without normals.
The second is the
scatter
pipeline. It was previously built with
markerspace = :pixel
in mind, which caused various rendering issues when
markerspace != :pixel
,
transform_marker != false
, and/or
rotation
was involved. These issues include silent corruption of Cairo state which causes no more plots to be drawn. These issues have now been resolved, and you should get the same results from CairoMakie as you get from GLMakie and WGLMakie when these attributes are involved. (Up to some smaller differences due to perspective projection in 3D.)
GLMakie | CairoMakie before | CairoMakie after |
---|---|---|
Introduces an option to close an Axis3's outline box with a new
front_spines
feature, enhancing the visualization of 3D plots by drawing the box spines in front.
using GLMakie, FileIO
GLMakie.activate!()
fig = Figure()
brain = load(assetpath("brain.stl"))
ax = Axis3(fig[1, 1], front_spines = true) # see also x/y/zspinecolor_4
mesh!(ax, brain, color = :gray80)
fig
Curvilinear contour plots are enabled using Contour.jl's capabilities, now supporting grids for more flexible contour visualizations:
using CairoMakie, BonitoSites
CairoMakie.activate!()
x = -10:10
y = -10:10
# The curvilinear grid:
xs = [x + 0.01y^3 for x in x, y in y]
ys = [y + 10cos(x/40) for x in x, y in y]
zs = sqrt.(xs .^ 2 .+ (ys .- 10) .^ 2)
levels = 0:4:20
fig, ax, srf = surface(xs, ys, fill(0f0, size(zs)); color=zs, shading = NoShading, axis = (; type = Axis, aspect = DataAspect()))
ctr = contour!(ax, xs, ys, zs; color = :orange, levels = levels, labels = true, labelfont = :bold, labelsize = 12)
fig
Implements screen reusability by using
empty!
instead of closing and reopening, solving a window behavior issue on Linux when reusing GLMakie's singleton screen.
Version 0.21.17 added the ability to rotate
Toggle
blocks using the
orientation
attribute.
using GLMakie
f = Figure(size = (400, 100))
Toggle(f[1, 1], orientation = :horizontal) # default
Toggle(f[1, 2], orientation = :vertical)
for i in 3:8
Toggle(f[1, i], orientation = (i-2) * 2pi/7)
end
f
Since the last breaking release we merged a bunch of fixes for picking in WGMakie and GLMakie. We added tests and also updated the indices produced for image, heatmap and surface plots to correspond to the matrix indices of the given data.
#4082, #4136, #4137, #4459, #4488, #4604
In version 0.21.6 we introduced
events(fig).tick
. The event triggers once per frame in GLMakie, CairoMakie, and
record()
, and on a timer in WGLMakie. It can be used for anything that should happen synchronized with rendering, e.g., animation. The tick event contains the number of frames rendered
tick.count
, the time since rendering started
tick.time
, and the time since the last tick
tick.delta_time
.
In version 0.21.6 we added the
uv_transform
attribute to
image
,
surface
,
mesh
, and
meshscatter
. It acts as a transformation matrix on texture coordinates similar to how model transforms coordinates. The attribute accepts 2x3 and 3x3 matrices (which will get truncated to 2x3), a
Symbol
for named transformations,
LinearAlgebra.I
, a
Vec2f
representing scaling, a
Tuple{Vec2f, Vec2f}
representing translation and scaling, or a tuple containing multiple operations which will get chained (last operation applies first). See
?Makie.uv_transform
for more information.
using LinearAlgebra, GeometryBasics, FileIO, GLMakie, ColorSchemes
GLMakie.activate!()
cow = load(assetpath("cow.png"))
f = Figure()
image(f[1, 1], cow, uv_transform = :transpose)
meshscatter(
f[2, 1], [Point2f(x, y) for x in 1:10 for y in 10:-1:1],
color = cow, # first (translate, scale), then :transpose
uv_transform = [(:transpose, (Vec2f(x, y), Vec2f(0.1, 0.1))) for x in 0.0:0.1:0.9 for y in 0.0:0.1:0.9],
markersize = Vec3f(0.9, 0.85, 1),
marker = uv_normal_mesh(Rect2f(-0.5, -0.5, 1, 1))
)
texture = reshape(get(colorschemes[:Spectral_11], 0:0.01:1), 101, 1)
# create fitting mesh
r = Rect3f(Point3f(-0.5, -0.5, 0), Vec3f(1, 1, 1))
uvs = [Vec2f(p[3], 0) for p in coordinates(r)]
rect_mesh = GeometryBasics.mesh(r, normal = normals(r), uv = uvs)
z = rand(10,10)
meshscatter(
f[1:2, 2], [Point3f(i, j, 0) for i in 1:10 for j in 1:10],
markersize = Vec3f.(1, 1, 10z[:]),
uv_transform = Vec2f.(z[:], 1), # scale only
marker = rect_mesh, color = texture, shading = NoShading
)
f
In version 0.21.6 an optional position argument and the
offset_radius
attribute were added to pie plots. The position argument can be used to translate the whole plot or each sector individually and the
offset radius
can be used to translate sectors along radial direction.
using CairoMakie, BonitoSites
CairoMakie.activate!(type="svg")
fig = Figure(size = (400, 400))
ax = Axis(fig[1, 1]; autolimitaspect=1)
vs = cumsum(4:10) ./ 35
off = [0, 0.3, 0, 0, 0.2, 0.0, 0.0]
cs = Makie.wong_colors()
pie!(ax, vs; color=cs, normalize=false)
pie!(ax, Point2f(2.5, 0), vs; color=cs, offset_radius=off, normalize=false, offset=π/2)
pie!(ax, 0, -2.5, vs; color=cs, normalize=false, offset=π/2, inner_radius=0.3)
xs = 2.5 .+ [0.0, 0.0, -0.2, -0.2, -0.2, 0.0, 0.2]
ys = -2.5 .+ [0.2, 0.2, 0.2, 0.0, 0.0, -0.2, 0.0]
pie!(ax, xs, ys, vs; color=cs, normalize=false, offset=π/2, inner_radius=0.3)
BonitoSites.ToSVG(fig)
After reworking our line shaders in 0.21, we added code for rendering closed line loops in version 0.21.4. If the start and end point of a line is the same and it has at least 4 points, it is detected as a loop. In that case, the line doesn't draw a linecap at the start and end point, but instead another joint, closing the loop.
You can wrap your data into Makie.Resampler, to resample large heatmaps for the viewing area. When zooming in, it will update the resampled version, to show it at best fidelity. It blocks updates while any mouse or keyboard button is pressed, to not spam e.g. WGLMakie with data updates. This goes well with
Axis(figure; zoombutton=Keyboard.left_control)
. You can disable this behavior with:
Resampler(data; update_while_button_pressed=true)
.
Example:
using Downloads, FileIO, GLMakie
# 30000×22943 image
path = Downloads.download("https://upload.wikimedia.org/wikipedia/commons/7/7e/In_the_Conservatory.jpg")
img = rotr90(load(path))
f, ax, pl = heatmap(Resampler(img); axis=(; aspect=DataAspect()), figure=(;size=size(img)./20))
hidedecorations!(ax)
f
Visit the docs for more information about this feature.
AlgebraOfGraphics, the grammar-of-graphics package built on Makie, recently had an interesting feature release as well. It is now possible to create facet layouts in which different facets have completely separate x or y scales. This means that it's possible to combine different categorical and continuous scales, which opens up a whole new range of plots possible with the basic
data * mapping * visual
system.
Here's an example where four columns of a dataset, two categorical and two continuous, are combined in a 2x2 facet plot with different plot types depending on the combination of scales:
using AlgebraOfGraphics, CairoMakie, BonitoSites
CairoMakie.activate!()
dat = data((;
fruit = rand(["Apple", "Orange", "Pear"], 150),
taste = randn(150) .* repeat(1:3, inner = 50),
weight = repeat(["Heavy", "Light", "Medium"], inner = 50),
cost = randn(150) .+ repeat([10, 20, 30], inner = 50),
))
fruit = :fruit => "Fruit" => scale(:X1)
weights = :weight => "Weight" => scale(:Y1)
taste = :taste => "Taste Score" => scale(:X2)
cost = :cost => "Cost" => scale(:Y2)
layer1 = mapping(
fruit,
weights,
col = direct("col1"), # this controls what facet this mapping belongs to
row = direct("row1")
) * frequency()
layer2 = mapping(
fruit,
cost,
col = direct("col1"),
row = direct("row2")
) * visual(Violin)
layer3 = mapping(
weights, # note X and Y are flipped here for a horizontal violin
taste,
col = direct("col2"),
row = direct("row1")
) * visual(Violin, orientation = :horizontal)
layer4 = mapping(
taste,
cost,
col = direct("col2"),
row = direct("row2")
) * visual(Scatter)
spec = dat * (layer1 + layer2 + layer3 + layer4)
s = scales(Row = (; show_labels = false), Col = (; show_labels = false))
BonitoSites.ToSVG(draw(spec, s))
Makie's website and blog are powered by Bonito.jl, which also serves as the foundation for Makie's WebGL backend. Bonito.jl is gradually evolving to support the creation of static websites, thanks in part to its use here.
Our goal is to continue enhancing it and develop a robust julia component system that simplifies the process of building blogs, websites and dashboards.
We have introduced an RSS feed, unified the documentation, website, and blog for a more cohesive experience, and updated several sections to ensure the blog posts are free from dead links.
Additionally, we improved the website's build system, making it easier to create new blog posts for Makie releases and general updates.