diff --git a/CITATION.bib b/CITATION.bib index c307fa13680..b06f2799a05 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -7,6 +7,6 @@ @article{DanischKrumbiegel2021 number = {65}, pages = {3349}, author = {Simon Danisch and Julius Krumbiegel}, - title = {Makie.jl: Flexible high-performance data visualization for Julia}, + title = {{Makie.jl}: Flexible high-performance data visualization for {Julia}}, journal = {Journal of Open Source Software} } diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index 225ee76f62f..7cda71a68ec 100644 --- a/CairoMakie/Project.toml +++ b/CairoMakie/Project.toml @@ -1,7 +1,7 @@ name = "CairoMakie" uuid = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" author = ["Simon Danisch "] -version = "0.10.4" +version = "0.10.6" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -14,7 +14,7 @@ GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" -SnoopPrecompile = "66db9d55-30c0-4569-8b51-7e840670fc0c" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" [compat] Cairo = "1.0.4" @@ -23,8 +23,8 @@ FFTW = "1" FileIO = "1.1" FreeType = "3, 4.0" GeometryBasics = "0.4.1" -Makie = "=0.19.4" -SnoopPrecompile = "1.0" +Makie = "=0.19.6" +PrecompileTools = "1.0" julia = "1.3" [extras] diff --git a/CairoMakie/src/overrides.jl b/CairoMakie/src/overrides.jl index 4a93c57bed3..d7301ddc8d9 100644 --- a/CairoMakie/src/overrides.jl +++ b/CairoMakie/src/overrides.jl @@ -66,7 +66,6 @@ end draw_poly(scene::Scene, screen::Screen, poly, rect::Rect2) = draw_poly(scene, screen, poly, [rect]) - function draw_poly(scene::Scene, screen::Screen, poly, rects::Vector{<:Rect2}) model = poly.model[] space = to_value(get(poly, :space, :data)) @@ -87,16 +86,19 @@ end function polypath(ctx, polygon) ext = decompose(Point2f, polygon.exterior) + Cairo.set_fill_type(ctx, Cairo.CAIRO_FILL_RULE_EVEN_ODD) Cairo.move_to(ctx, ext[1]...) for point in ext[2:end] Cairo.line_to(ctx, point...) end Cairo.close_path(ctx) - interiors = decompose.(Point2f, polygon.interiors) for interior in interiors + # Cairo needs to have interiors counter clockwise + n = length(interior) Cairo.move_to(ctx, interior[1]...) - for point in interior[2:end] + for idx in 2:n + point = interior[idx] Cairo.line_to(ctx, point...) end Cairo.close_path(ctx) diff --git a/CairoMakie/src/precompiles.jl b/CairoMakie/src/precompiles.jl index dea7e5c54bd..b2c6ab3155d 100644 --- a/CairoMakie/src/precompiles.jl +++ b/CairoMakie/src/precompiles.jl @@ -1,4 +1,4 @@ -using SnoopPrecompile +using PrecompileTools macro compile(block) return quote @@ -8,7 +8,7 @@ macro compile(block) end let - @precompile_all_calls begin + @compile_workload begin CairoMakie.activate!() base_path = normpath(joinpath(dirname(pathof(Makie)), "..", "precompile")) shared_precompile = joinpath(base_path, "shared-precompile.jl") diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index fca5bea1048..67117ddde9f 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -98,7 +98,7 @@ function device_scaling_factor(surface::Cairo.CairoSurface, sc::ScreenConfig) return is_vector_backend(surface) ? sc.pt_per_unit : sc.px_per_unit end -const LAST_INLINE = Ref(true) +const LAST_INLINE = Ref{Union{Makie.Automatic,Bool}}(Makie.automatic) """ CairoMakie.activate!(; screen_config...) diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index 396eb8b246b..ad8027ba3a2 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -1,6 +1,6 @@ name = "GLMakie" uuid = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" -version = "0.8.4" +version = "0.8.6" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" @@ -18,7 +18,7 @@ ModernGL = "66fc600b-dfda-50eb-8b99-91cfa97b1301" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ShaderAbstractions = "65257c39-d410-5151-9873-9b3e5be5013e" -SnoopPrecompile = "66db9d55-30c0-4569-8b51-7e840670fc0c" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] @@ -29,11 +29,11 @@ FixedPointNumbers = "0.7, 0.8" FreeTypeAbstraction = "0.10" GLFW = "3" GeometryBasics = "0.4.1" -Makie = "=0.19.4" +Makie = "=0.19.6" MeshIO = "0.4" ModernGL = "1" Observables = "0.5.1" ShaderAbstractions = "0.3" -SnoopPrecompile = "1.0" +PrecompileTools = "1.0" StaticArrays = "0.12, 1.0" julia = "1" diff --git a/GLMakie/assets/shader/distance_shape.frag b/GLMakie/assets/shader/distance_shape.frag index df3e66e064c..3e16d4a4e70 100644 --- a/GLMakie/assets/shader/distance_shape.frag +++ b/GLMakie/assets/shader/distance_shape.frag @@ -138,7 +138,7 @@ void main(){ else if(shape == ROUNDED_RECTANGLE) signed_distance = rounded_rectangle(f_uv, vec2(0.2), vec2(0.8)); else if(shape == RECTANGLE) - signed_distance = 1.0; // rectangle(f_uv); + signed_distance = rectangle(f_uv); else if(shape == TRIANGLE) signed_distance = triangle(f_uv); diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom index 8fb1218d621..0162ca8ec5c 100644 --- a/GLMakie/assets/shader/lines.geom +++ b/GLMakie/assets/shader/lines.geom @@ -55,7 +55,8 @@ void emit_vertex(vec3 position, vec2 uv, int index) f_color = g_color[index]; gl_Position = vec4((position.xy / resolution), position.z, 1.0); f_id = g_id[index]; - f_thickness = g_thickness[index]; + // linewidth scaling may shrink the effective linewidth + f_thickness = abs(uv.y) - AA_THICKNESS; EmitVertex(); } @@ -77,7 +78,7 @@ void emit_vertex(vec3 position, vec2 uv, int index, vec4 color) f_color = color; gl_Position = vec4((position.xy / resolution), position.z, 1.0); f_id = g_id[index]; - f_thickness = g_thickness[index]; + f_thickness = abs(uv.y) - AA_THICKNESS; EmitVertex(); } void emit_vertex(vec3 position, vec2 uv, vec4 color) @@ -618,10 +619,6 @@ void draw_solid_line(bool isvalid[4]) vec3 p2 = screen_space(gl_in[2].gl_Position); // end of current segment, start of next segment vec3 p3 = screen_space(gl_in[3].gl_Position); // end of next segment - // linewidth with padding for anti aliasing - float thickness_aa1 = g_thickness[1] + AA_THICKNESS; - float thickness_aa2 = g_thickness[2] + AA_THICKNESS; - // determine the direction of each of the 3 segments (previous, current, next) vec3 v1 = p2 - p1; float segment_length = length(v1.xy); @@ -641,6 +638,14 @@ void draw_solid_line(bool isvalid[4]) vec2 n1 = vec2(-v1.y, v1.x); vec2 n2 = vec2(-v2.y, v2.x); + // determine stretching of AA border due to linewidth change + float temp = (g_thickness[2] - g_thickness[1]) / segment_length; + float edge_scale = sqrt(1 + temp * temp); + + // linewidth with padding for anti aliasing (used for geometry) + float thickness_aa1 = g_thickness[1] + edge_scale * AA_THICKNESS; + float thickness_aa2 = g_thickness[2] + edge_scale * AA_THICKNESS; + // Setup for sharp corners (see above) vec2 miter_a = normalize(n0 + n1); vec2 miter_b = normalize(n1 + n2); @@ -717,6 +722,10 @@ void draw_solid_line(bool isvalid[4]) segment_length += corner_offset; } + // scaling of uv.y due to different linewidths + // the padding for AA_THICKNESS should always have AA_THICKNESS width in uv + thickness_aa1 = g_thickness[1] / edge_scale + AA_THICKNESS; + thickness_aa2 = g_thickness[2] / edge_scale + AA_THICKNESS; // Generate line segment u1 *= px2uv; diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index 850b700bd20..47da9a6657c 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -164,11 +164,7 @@ void main(void) // Compute required amount of buffering float sprite_from_viewport_scale = 1.0 / viewport_from_sprite_scale; float bbox_buf = sprite_from_viewport_scale * - (// Hack!! antialiasing is disabled for RECTANGLE==1 for now - // because it's used for boxplots where the sprites are - // long and skinny (violating assumption 1 above) - (shape == 1 ? 0.0 : ANTIALIAS_RADIUS) + - max(glow_width, 0) + max(stroke_width, 0)); + (ANTIALIAS_RADIUS + max(glow_width, 0) + max(stroke_width, 0)); // Compute xy bounding box of billboard (in model space units) after // buffering and associated bounding box of uv coordinates. vec2 bbox_radius_buf = bbox_signed_radius + sign(bbox_signed_radius)*bbox_buf; diff --git a/GLMakie/experiments/cuda_interop.jl b/GLMakie/experiments/cuda_interop.jl index ca8d8443c1c..c149d4f7b4b 100644 --- a/GLMakie/experiments/cuda_interop.jl +++ b/GLMakie/experiments/cuda_interop.jl @@ -1,11 +1,68 @@ -using CUDA, GLMakie +using CUDA, GLMakie, NVTX +using GLMakie.GLAbstraction +# from https://discourse.julialang.org/t/cuarray-glmakie/52461/11?u=maleadt +function cu_plot(; T=Float32, N=1024, resolution=(800, 600)) + t = CUDA.rand(T, N) + X = CUDA.rand(T, N) -scene = scatter(rand(Point2f, 10_000), show_axis=false) -screen = display(scene) + # so that we can create a GLBuffer before having rendered anything. + fig = Figure(; resolution) + ax = Axis(fig[1, 1]; limits=(0, 1, 0, 1)) + screen = display(fig) -buffer = screen.renderlist[1][3][:position] -resource = -cuGraphicsGLRegisterBuffer(&resource, pbo, cudaGraphicsRegisterFlagsReadOnly) + # get a buffer object and register it with CUDA + buffer = GLAbstraction.GLBuffer(Point2f, N) + resource = let + ref = Ref{CUDA.CUgraphicsResource}() + CUDA.cuGraphicsGLRegisterBuffer(ref, buffer.id, + CUDA.CU_GRAPHICS_MAP_RESOURCE_FLAGS_WRITE_DISCARD) + ref[] + end -CUDA.cuGraphicsMapResources + NVTX.@range "main" begin + # process data, generate points + NVTX.@range "CUDA" begin + # map OpenGL buffer object for writing from CUDA + CUDA.cuGraphicsMapResources(1, [resource], stream()) + + # get a CuArray object that we can work with + array = let + ptr_ref = Ref{CUDA.CUdeviceptr}() + numbytes_ref = Ref{Csize_t}() + CUDA.cuGraphicsResourceGetMappedPointer_v2(ptr_ref, numbytes_ref, resource) + + ptr = reinterpret(CuPtr{Point2f}, ptr_ref[]) + len = Int(numbytes_ref[] ÷ sizeof(Point2f)) + + unsafe_wrap(CuArray, ptr, len) + end + + # generate points + broadcast!(array, t, X) do x, y + return Point2f(x, y) + end + + # wait for the GPU to finish + synchronize() + + CUDA.cuGraphicsUnmapResources(1, [resource], stream()) + end + + # generate and render plot + NVTX.@range "Makie" begin + scatter!(ax, buffer) + # force everything to render (for benchmarking purposes) + GLMakie.render_frame(screen; resize_buffers=false) + GLMakie.glFinish() + end + end + + ## clean-up + + CUDA.cuGraphicsUnregisterResource(resource) + + return +end + +cu_plot() diff --git a/GLMakie/src/precompiles.jl b/GLMakie/src/precompiles.jl index d1b7a205578..6ddcc86e980 100644 --- a/GLMakie/src/precompiles.jl +++ b/GLMakie/src/precompiles.jl @@ -1,4 +1,4 @@ -using SnoopPrecompile +using PrecompileTools macro compile(block) return quote @@ -11,9 +11,9 @@ macro compile(block) end let - @precompile_setup begin + @setup_workload begin x = rand(5) - @precompile_all_calls begin + @compile_workload begin GLMakie.activate!() screen = GLMakie.singleton_screen(false) close(screen) @@ -25,7 +25,7 @@ let display(plot(x); visible=false) catch end - Makie._current_figure[] = nothing + Makie.CURRENT_FIGURE[] = nothing empty!(atlas_texture_cache) closeall() @assert isempty(SCREEN_REUSE_POOL) diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index b5f9c6e86ba..3f9b5073c24 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -10,7 +10,7 @@ function setup!(screen) a = pixelarea(scene)[] rt = (minimum(a)..., widths(a)...) glViewport(rt...) - if scene.clear + if scene.clear[] c = scene.backgroundcolor[] glScissor(rt...) glClearColor(red(c), green(c), blue(c), alpha(c)) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 9a4da884136..61021022490 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -106,7 +106,7 @@ mutable struct ScreenConfig end end -const LAST_INLINE = Ref(false) +const LAST_INLINE = Ref{Union{Makie.Automatic, Bool}}(Makie.automatic) """ GLMakie.activate!(; screen_config...) @@ -435,7 +435,13 @@ function Makie.insertplots!(screen::Screen, scene::Scene) get!(screen.screen2scene, WeakRef(scene)) do id = length(screen.screens) + 1 push!(screen.screens, (id, scene)) - on(_ -> screen.requires_update = true, scene.visible) + screen.requires_update = true + onany( + (_, _, _, _, _, _) -> screen.requires_update = true, + scene, + scene.visible, scene.backgroundcolor, scene.clear, + scene.ssao.bias, scene.ssao.blur, scene.ssao.radius + ) return id end for elem in scene.plots @@ -646,7 +652,7 @@ end """ depthbuffer(screen::Screen) -Gets the depth buffer of `screen`. Returns a `Matrix{Float32}` of the dimensions of the screen's `framebuffer`. +Gets the depth buffer of `screen`. Returns a `Matrix{Float32}` of the dimensions of the screen's `framebuffer`. A depth buffer is used to determine which plot's contents should be shown at each pixel. Usage: diff --git a/NEWS.md b/NEWS.md index 96741c2df03..bae0ea66d89 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,8 +4,27 @@ - Improvements to 3D camera handling, hotkeys and functionality [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) - Fixed some errors/problems with DataInspector [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) -- Fixed incorrect line depth in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843) -- Fixed incorrect line alpha in dense lines in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843) + +## v0.19.6 + +- Fix broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953). +- Fix WGLMakie JS popup [#2976](https://github.com/MakieOrg/Makie.jl/pull/2976). +- Fix legendelements when children have no elements [#2982](https://github.com/MakieOrg/Makie.jl/pull/2982). +- Bump compat for StatsBase to 0.34 [#2915](https://github.com/MakieOrg/Makie.jl/pull/2915). +- Improved thread safety [#2840](https://github.com/MakieOrg/Makie.jl/pull/2840). + +## v0.19.5 + +- Add `loop` option for GIF outputs when recording videos with `record` [#2891](https://github.com/MakieOrg/Makie.jl/pull/2891). +- More fixes for line rendering in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843). +- Fixed incorrect line alpha in dense lines in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843). +- Change `scene.clear` to an observable and make changes in `Scene` Observables trigger renders in GLMakie [#2929](https://github.com/MakieOrg/Makie.jl/pull/2929). +- Added contour labels [#2496](https://github.com/MakieOrg/Makie.jl/pull/2496). +- Allow rich text to be used in Legends [#2902](https://github.com/MakieOrg/Makie.jl/pull/2902). +- More support for zero length Geometries [#2917](https://github.com/MakieOrg/Makie.jl/pull/2917). +- Make CairoMakie drawing for polygons with holes order independent [#2918](https://github.com/MakieOrg/Makie.jl/pull/2918). +- Fixes for `Makie.inline!()`, allowing now for `Makie.inline!(automatic)` (default), which is better at automatically opening a window/ inlining a plot into plotpane when needed [#2919](https://github.com/MakieOrg/Makie.jl/pull/2919) [#2937](https://github.com/MakieOrg/Makie.jl/pull/2937). +- Block/Axis doc improvements [#2940](https://github.com/MakieOrg/Makie.jl/pull/2940) [#2932](https://github.com/MakieOrg/Makie.jl/pull/2932) [#2894](https://github.com/MakieOrg/Makie.jl/pull/2894). ## v0.19.4 @@ -35,9 +54,9 @@ - Add `show_data` method for `band` which shows the min and max values of the band at the x position of the cursor [#2497](https://github.com/MakieOrg/Makie.jl/pull/2497). - Added `xlabelrotation`, `ylabelrotation` (`Axis`) and `labelrotation` (`Colorbar`) [#2478](https://github.com/MakieOrg/Makie.jl/pull/2478). - Fixed forced rasterization in CairoMakie svg files when polygons with colors specified as (color, alpha) tuples were used [#2535](https://github.com/MakieOrg/Makie.jl/pull/2535). -- Do less copies of Observables in Attributes + plot pipeline [#2443](https://github.com/MakieOrg/Makie.jl/pull/2443). +- Do less copies of Observables in Attributes + plot pipeline [#2443](https://github.com/MakieOrg/Makie.jl/pull/2443). - Add Search Page and tweak Result Ordering [#2474](https://github.com/MakieOrg/Makie.jl/pull/2474). -- Remove all global attributes from TextureAtlas implementation and fix julia#master [#2498](https://github.com/MakieOrg/Makie.jl/pull/2498). +- Remove all global attributes from TextureAtlas implementation and fix julia#master [#2498](https://github.com/MakieOrg/Makie.jl/pull/2498). - Use new JSServe, implement WGLMakie picking, improve performance and fix lots of WGLMakie bugs [#2428](https://github.com/MakieOrg/Makie.jl/pull/2428). ## v0.19.0 diff --git a/Project.toml b/Project.toml index 681130f2b8d..89a3bb6f529 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Makie" uuid = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" authors = ["Simon Danisch", "Julius Krumbiegel"] -version = "0.19.4" +version = "0.19.6" [deps] Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" @@ -29,6 +29,7 @@ Isoband = "f1662d9f-8043-43de-a69a-05efc1cc6ff4" KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" MakieCore = "20f20a25-4f0e-4fdf-b5d1-57303727442b" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Match = "7eb4fadd-790c-5f42-8a69-bfa0b872bfbf" @@ -39,13 +40,14 @@ OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Packing = "19eb6ba3-879d-56ad-ad62-d5c202156566" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" PolygonOps = "647866c9-e3ac-4575-94e7-e3d426903924" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" Showoff = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" SignedDistanceFields = "73760f76-fbc4-59ce-8f25-708e95d2df96" -SnoopPrecompile = "66db9d55-30c0-4569-8b51-7e840670fc0c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StableHashTraits = "c5dd0088-6c3f-4803-b00e-f31a60c170fa" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -77,6 +79,7 @@ IntervalSets = "0.3, 0.4, 0.5, 0.6, 0.7" Isoband = "0.1" KernelDensity = "0.5, 0.6" LaTeXStrings = "1.2" +MacroTools = "0.5" MakieCore = "=0.6.3" Match = "1.1" MathTeXEngine = "0.5" @@ -86,13 +89,13 @@ OffsetArrays = "1" Packing = "0.5" PlotUtils = "1" PolygonOps = "0.1.1" +PrecompileTools = "1.0" RelocatableFolders = "0.1, 0.2, 0.3, 1.0" Setfield = "1" Showoff = "0.3, 1.0.2" SignedDistanceFields = "0.4" -SnoopPrecompile = "1.0" StableHashTraits = "0.3" -StatsBase = "0.31, 0.32, 0.33" +StatsBase = "0.31, 0.32, 0.33, 0.34" StatsFuns = "0.9, 1.0" StructArrays = "0.3, 0.4, 0.5, 0.6" TriplotBase = "=0.1.0" diff --git a/README.md b/README.md index 2d023cd163c..168b6272961 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@
- Makie.jl + + + Makie.jl logo +
From the japanese word [_Maki-e_](https://en.wikipedia.org/wiki/Maki-e), which is a technique to sprinkle lacquer with gold and silver powder. @@ -35,7 +40,7 @@ BibTeX entry: number = {65}, pages = {3349}, author = {Simon Danisch and Julius Krumbiegel}, - title = {Makie.jl: Flexible high-performance data visualization for Julia}, + title = {{Makie.jl}: Flexible high-performance data visualization for {Julia}}, journal = {Journal of Open Source Software} } ``` @@ -79,7 +84,7 @@ To run the tests, you also should add: ```julia ]dev dev/Makie/ReferenceTests ``` -For more info about ReferenceTests, check out its [README](./ReferenceTests/README.md) +For more info about ReferenceTests, check out its [README](./ReferenceUpdater/README.md) # Quick start diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 8e89dcc79d6..46151afff69 100644 --- a/RPRMakie/Project.toml +++ b/RPRMakie/Project.toml @@ -1,7 +1,7 @@ name = "RPRMakie" uuid = "22d9f318-5e34-4b44-b769-6e3734a732a6" authors = ["Simon Danisch"] -version = "0.5.4" +version = "0.5.6" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -17,7 +17,7 @@ julia = "1.3" Colors = "0.9, 0.10, 0.11, 0.12" FileIO = "1.6" GeometryBasics = "0.4.1" -Makie = "=0.19.4" +Makie = "=0.19.6" RadeonProRender = "0.2.15" [extras] diff --git a/RPRMakie/examples/materials.jl b/RPRMakie/examples/materials.jl index 0f48bfb8aa0..3e5e7639dda 100644 --- a/RPRMakie/examples/materials.jl +++ b/RPRMakie/examples/materials.jl @@ -24,7 +24,7 @@ img = begin emissive plastic] mesh!(ax, load(Makie.assetpath("matball_floor.obj")); color=:white) - palette = reshape(Makie.default_palettes.color[][1:6], size(materials)) + palette = reshape(Makie.DEFAULT_PALETTES.color[][1:6], size(materials)) for i in CartesianIndices(materials) x, y = Tuple(i) diff --git a/RPRMakie/examples/opengl_interop.jl b/RPRMakie/examples/opengl_interop.jl index c03e913c018..e43fe2df392 100644 --- a/RPRMakie/examples/opengl_interop.jl +++ b/RPRMakie/examples/opengl_interop.jl @@ -13,7 +13,7 @@ lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), fig = Figure(; resolution=(1500, 1000)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) -screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Tahoe) +screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Northstar, resource=RPR.RPR_CREATION_FLAGS_ENABLE_GPU1) material = RPR.UberMaterial(screen.matsys) surface!(ax, f.(u, v'), g.(u, v'), h.(u, v'); ambient=Vec3f(0.5), diffuse=Vec3f(1), specular=0.5, diff --git a/RPRMakie/examples/sea_cables.jl b/RPRMakie/examples/sea_cables.jl index 4c9a6b14a0f..5e1185b3653 100644 --- a/RPRMakie/examples/sea_cables.jl +++ b/RPRMakie/examples/sea_cables.jl @@ -63,7 +63,7 @@ scene = with_theme(theme_dark()) do ze = [cos(θ) for θ in θ, φ in φ] surface!(ax, xe, ye, ze; color=earth_img) meshscatter!(toPoints3D; color=1:length(toPoints3D), markersize=0.005, colormap=:plasma) - colors = Makie.default_palettes.color[] + colors = Makie.DEFAULT_PALETTES.color[] c = Iterators.cycle(colors) foreach(((l, c),) -> lines!(ax, l; linewidth=2, color=c), zip(splitLines3D, c)) ax.scene.camera_controls.eyeposition[] = Vec3f(1.5) diff --git a/RPRMakie/src/meshes.jl b/RPRMakie/src/meshes.jl index 5f67806d33a..4e31520ed6f 100644 --- a/RPRMakie/src/meshes.jl +++ b/RPRMakie/src/meshes.jl @@ -100,6 +100,8 @@ function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) material.color = tex elseif color isa Colorant material.color = color + elseif color isa AbstractMatrix{<: Colorant} + material.color = color else error("Unsupported color type for RadeonProRender backend: $(typeof(color))") end diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index b8e26b23b07..f60f9eca017 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -243,7 +243,7 @@ end xs = 0:9 # data ys = zeros(10) - colors = Makie.default_palettes.color[] + colors = Makie.DEFAULT_PALETTES.color[] plots = map(1:N) do i # plot lines lines!(ax, xs, ys; @@ -878,7 +878,7 @@ end text = "Falling", offset = 10, orientation = :up, color = :purple, textcolor = :purple) bracket!(Point(5.5, sin(5.5)), Point(7.0, sin(7.0)), - text = "Rising", offset = 10, orientation = :down, color = :orange, textcolor = :orange, + text = "Rising", offset = 10, orientation = :down, color = :orange, textcolor = :orange, fontsize = 30, textoffset = 30, width = 50) f end diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml index 78438f042c8..09053cbf7bb 100644 --- a/WGLMakie/Project.toml +++ b/WGLMakie/Project.toml @@ -1,7 +1,7 @@ name = "WGLMakie" uuid = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" authors = ["SimonDanisch "] -version = "0.8.8" +version = "0.8.10" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -16,7 +16,7 @@ Observables = "510215fc-4207-5dde-b226-833fc4488ee2" PNGFiles = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883" RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" ShaderAbstractions = "65257c39-d410-5151-9873-9b3e5be5013e" -SnoopPrecompile = "66db9d55-30c0-4569-8b51-7e840670fc0c" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] @@ -26,11 +26,11 @@ FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.1" Hyperscript = "0.0.3, 0.0.4" JSServe = "2.2" -Makie = "=0.19.4" +Makie = "=0.19.6" Observables = "0.5.1" RelocatableFolders = "0.1, 0.2, 0.3, 1.0" ShaderAbstractions = "0.3" -SnoopPrecompile = "1.0" +PrecompileTools = "1.0" StaticArrays = "0.12, 1.0" PNGFiles = "0.3" julia = "1.3" diff --git a/WGLMakie/src/WGLMakie.jl b/WGLMakie/src/WGLMakie.jl index 5611f1ff15e..9af22ed65d5 100644 --- a/WGLMakie/src/WGLMakie.jl +++ b/WGLMakie/src/WGLMakie.jl @@ -22,7 +22,7 @@ using ShaderAbstractions: InstancedProgram using GeometryBasics: StaticVector import Makie.FileIO -using Makie: get_texture_atlas, SceneSpace, Pixel +using Makie: get_texture_atlas, SceneSpace, Pixel, Automatic using Makie: attribute_per_char, layout_text using Makie: MouseButtonEvent, KeyEvent using Makie: apply_transform, transform_func_obs @@ -42,7 +42,7 @@ include("meshes.jl") include("imagelike.jl") include("picking.jl") -const LAST_INLINE = Ref(true) +const LAST_INLINE = Base.RefValue{Union{Automatic, Bool}}(Makie.automatic) """ WGLMakie.activate!(; screen_config...) @@ -54,7 +54,8 @@ Note, that the `screen_config` can also be set permanently via `Makie.set_theme! $(Base.doc(ScreenConfig)) """ -function activate!(; inline=LAST_INLINE[], screen_config...) +function activate!(; inline::Union{Automatic,Bool}=LAST_INLINE[], screen_config...) + JSServe.browser_display() Makie.inline!(inline) LAST_INLINE[] = inline Makie.set_active_backend!(WGLMakie) diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 794572150d8..e95155f02a5 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -24,7 +24,7 @@ function JSServe.jsrender(session::Session, scene::Scene) screen = Screen(c, true, scene) screen.session = session Makie.push_screen!(scene, screen) - on(on_init) do i + on(session, on_init) do i mark_as_displayed!(screen, scene) end return canvas @@ -93,7 +93,7 @@ for M in WEB_MIMES screen.session = session three, canvas, init_obs = three_display(session, scene) Makie.push_screen!(scene, screen) - on(init_obs) do _ + on(session, init_obs) do _ put!(screen.three, three) mark_as_displayed!(screen, scene) return @@ -183,7 +183,7 @@ function Base.display(screen::Screen, scene::Scene; kw...) app = App() do session, request screen.session = session three, canvas, done_init = three_display(session, scene) - on(done_init) do _ + on(session, done_init) do _ put!(screen.three, three) mark_as_displayed!(screen, scene) return @@ -225,6 +225,31 @@ function Makie.colorbuffer(screen::Screen) return session2image(three.session, screen.scene) end + +function insert_scene!(disp, screen::Screen, scene::Scene) + if js_uuid(scene) in screen.displayed_scenes + return true + else + scene_ser = serialize_scene(scene) + parent = scene.parent + parent_uuid = js_uuid(parent) + insert_scene!(disp, screen, parent) # make sure parent is also already displayed + err = "Cant find scene js_uuid(scene) == $(parent_uuid)" + evaljs_value(disp.session, js""" + $(WGL).then(WGL=> { + const parent = WGL.find_scene($(parent_uuid)); + if (!parent) { + throw new Error($(err)) + } + const new_scene = WGL.deserialize_scene($scene_ser, parent.screen); + parent.scene_children.push(new_scene); + }) + """) + mark_as_displayed!(screen, scene) + return false + end +end + function Base.insert!(screen::Screen, scene::Scene, plot::Combined) disp = get_three(screen; error="Plot needs to be displayed to insert additional plots") if js_uuid(scene) in screen.displayed_scenes @@ -245,20 +270,7 @@ function Base.insert!(screen::Screen, scene::Scene, plot::Combined) # We serialize the whole scene (containing `plot` as well), # since, we should only get here if scene is newly created and this is the first plot we insert! @assert scene.plots[1] == plot - scene_ser = serialize_scene(scene) - parent_uuid = js_uuid(parent) - err = "Cant find scene js_uuid(scene) == $(parent_uuid)" - evaljs_value(disp.session, js""" - $(WGL).then(WGL=> { - const parent = WGL.find_scene($(parent_uuid)); - if (!parent) { - throw new Error($(err)) - } - const new_scene = WGL.deserialize_scene($scene_ser, parent.screen); - parent.scene_children.push(new_scene); - }) - """) - mark_as_displayed!(screen, scene) + insert_scene!(disp, screen, scene) end return end diff --git a/WGLMakie/src/picking.jl b/WGLMakie/src/picking.jl index 04eaecc37a0..72df2389730 100644 --- a/WGLMakie/src/picking.jl +++ b/WGLMakie/src/picking.jl @@ -1,29 +1,29 @@ function pick_native(screen::Screen, rect::Rect2i) - task = @async begin - (x, y) = minimum(rect) - (w, h) = widths(rect) - session = get_three(screen; error="Can't do picking!").session - scene = screen.scene - picking_data = JSServe.evaljs_value(session, js""" - Promise.all([$(WGL), $(scene)]).then(([WGL, scene]) => WGL.pick_native_matrix(scene, $x, $y, $w, $h)) - """) - w2, h2 = picking_data["size"] - @assert w2 == w && h2 == h - matrix = reshape(picking_data["data"], (w2, h2)) - - if isempty(matrix) - return Matrix{Tuple{Union{Nothing, AbstractPlot}, Int}}(undef, 0, 0) - else - all_children = Makie.flatten_plots(scene) - lookup = Dict(Pair.(js_uuid.(all_children), all_children)) - return map(matrix) do (uuid, index) - !haskey(lookup, uuid) && return (nothing, 0) - return (lookup[uuid], Int(index) + 1) - end + (x, y) = minimum(rect) + (w, h) = widths(rect) + session = get_three(screen; error="Can't do picking!").session + scene = screen.scene + picking_data = JSServe.evaljs_value(session, js""" + Promise.all([$(WGL), $(scene)]).then(([WGL, scene]) => WGL.pick_native_matrix(scene, $x, $y, $w, $h)) + """) + empty = Matrix{Tuple{Union{Nothing, AbstractPlot}, Int}}(undef, 0, 0) + if isnothing(picking_data) + return empty + end + w2, h2 = picking_data["size"] + @assert w2 == w && h2 == h + matrix = reshape(picking_data["data"], (w2, h2)) + if isempty(matrix) + return empty + else + all_children = Makie.flatten_plots(scene) + lookup = Dict(Pair.(js_uuid.(all_children), all_children)) + return map(matrix) do (uuid, index) + !haskey(lookup, uuid) && return (nothing, 0) + return (lookup[uuid], Int(index) + 1) end end - return fetch(task) end function plot_lookup(scene::Scene) @@ -121,7 +121,7 @@ function JSServe.jsrender(session::Session, tt::ToolTip) $(scene).then(scene => { const plots_to_pick = new Set($(tt.plot_uuids)); const callback = $(tt.callback); - register_popup($popup, scene, plots_to_pick, callback) + WGL.register_popup($popup, scene, plots_to_pick, callback) }) """) return DOM.span(JSServe.jsrender(session, POPUP_CSS), popup) diff --git a/WGLMakie/src/precompiles.jl b/WGLMakie/src/precompiles.jl index ed95fe052f7..ea89684a234 100644 --- a/WGLMakie/src/precompiles.jl +++ b/WGLMakie/src/precompiles.jl @@ -1,4 +1,4 @@ -using SnoopPrecompile +using PrecompileTools macro compile(block) return quote @@ -21,13 +21,13 @@ macro compile(block) end let - @precompile_all_calls begin + @compile_workload begin DISABLE_JS_FINALZING[] = true # to not start cleanup task WGLMakie.activate!() base_path = normpath(joinpath(dirname(pathof(Makie)), "..", "precompile")) shared_precompile = joinpath(base_path, "shared-precompile.jl") include(shared_precompile) - Makie._current_figure[] = nothing + Makie.CURRENT_FIGURE[] = nothing Observables.clear(TEXTURE_ATLAS) TEXTURE_ATLAS[] = Float32[] nothing diff --git a/WGLMakie/src/three_plot.jl b/WGLMakie/src/three_plot.jl index 2ddb556779d..c7707e8acb7 100644 --- a/WGLMakie/src/three_plot.jl +++ b/WGLMakie/src/three_plot.jl @@ -59,7 +59,7 @@ function three_display(session::Session, scene::Scene; screen_config...) $(done_init).notify(true) }) """) - on(done_init) do val + on(session, done_init) do val window_open[] = true end connect_scene_events!(scene, comm) diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index 472ae5ed65a..a5ed1aafc6f 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -19839,6 +19839,7 @@ function insert_plot(scene_id, plot_data) { }); } function delete_plots(scene_id, plot_uuids) { + console.log(`deleting plots!: ${plot_uuids}`); const scene = find_scene(scene_id); const plots = find_plots(plot_uuids); plots.forEach((p)=>{ @@ -20242,7 +20243,7 @@ function render_scene(scene, picking = false) { if (!scene.visible.value) { return true; } - renderer.autoClear = scene.clearscene; + renderer.autoClear = scene.clearscene.value; const area = scene.pixelarea.value; if (area) { const [x, y, w, h] = area.map((t)=>t / pixelRatio1); @@ -20515,10 +20516,7 @@ function pick_sorted(scene, xy, range) { const { picking_target } = scene.screen; const { width , height } = picking_target; if (!(1.0 <= xy[0] <= width && 1.0 <= xy[1] <= height)) { - return [ - null, - 0 - ]; + return null; } const x0 = Math.max(1, xy[0] - range); const y0 = Math.max(1, xy[1] - range); @@ -20528,7 +20526,7 @@ function pick_sorted(scene, xy, range) { const dy = y1 - y0; const [plot_data, selected] = pick_native(scene, x0, y0, dx, dy); if (selected.length == 0) { - return []; + return null; } const plot_matrix = plot_data.data; const distances = selected.map((x)=>range ^ 2); @@ -20613,7 +20611,8 @@ window.WGL = { delete_scenes, create_scene, event2scene_pixel, - on_next_insert + on_next_insert, + register_popup }; export { deserialize_scene as deserialize_scene, threejs_module as threejs_module, start_renderloop as start_renderloop, delete_plots as delete_plots, insert_plot as insert_plot, find_plots as find_plots, delete_scene as delete_scene, find_scene as find_scene, scene_cache as scene_cache, plot_cache as plot_cache, delete_scenes as delete_scenes, create_scene as create_scene, event2scene_pixel as event2scene_pixel, on_next_insert as on_next_insert }; export { render_scene as render_scene }; diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index 8f13cdb55a2..e01bb47ad0e 100644 --- a/WGLMakie/src/wglmakie.js +++ b/WGLMakie/src/wglmakie.js @@ -35,7 +35,7 @@ export function render_scene(scene, picking = false) { if (!scene.visible.value) { return true; } - renderer.autoClear = scene.clearscene; + renderer.autoClear = scene.clearscene.value; const area = scene.pixelarea.value; if (area) { const [x, y, w, h] = area.map((t) => t / pixelRatio); @@ -374,7 +374,7 @@ export function pick_sorted(scene, xy, range) { const { width, height } = picking_target; if (!(1.0 <= xy[0] <= width && 1.0 <= xy[1] <= height)) { - return [null, 0]; + return null; } const x0 = Math.max(1, xy[0] - range); @@ -386,7 +386,7 @@ export function pick_sorted(scene, xy, range) { const dy = y1 - y0; const [plot_data, selected] = pick_native(scene, x0, y0, dx, dy); if (selected.length == 0) { - return []; + return null; } const plot_matrix = plot_data.data; @@ -412,7 +412,6 @@ export function pick_sorted(scene, xy, range) { (a, b) => distances[a] < distances[b] ? -1 : (distances[b] < distances[a]) | 0 ); - return sorted_indices.map((idx) => { const [plot, index] = selected[idx]; return [plot.plot_uuid, index]; @@ -479,6 +478,7 @@ window.WGL = { create_scene, event2scene_pixel, on_next_insert, + register_popup, }; export { diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 9a6c5716a3d..667b8d19bc1 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -3,7 +3,6 @@ using WGLMakie, Makie, Test using Pkg using WGLMakie.JSServe import Electron -d = JSServe.use_electron_display() path = normpath(joinpath(dirname(pathof(Makie)), "..", "ReferenceTests")) Pkg.develop(PackageSpec(path = path)) @@ -57,9 +56,11 @@ excludes = Set([ "lines and linestyles", "Textured meshscatter" # not yet implemented ]) +Makie.inline!(Makie.automatic) @testset "refimages" begin WGLMakie.activate!() + d = JSServe.use_electron_display() ReferenceTests.mark_broken_tests(excludes) recorded_files, recording_dir = @include_reference_tests "refimages.jl" missing_images, scores = ReferenceTests.record_comparison(recording_dir) diff --git a/assets/DanischKrumbiegel2021.bibtex b/assets/DanischKrumbiegel2021.bibtex index c307fa13680..b06f2799a05 100644 --- a/assets/DanischKrumbiegel2021.bibtex +++ b/assets/DanischKrumbiegel2021.bibtex @@ -7,6 +7,6 @@ number = {65}, pages = {3349}, author = {Simon Danisch and Julius Krumbiegel}, - title = {Makie.jl: Flexible high-performance data visualization for Julia}, + title = {{Makie.jl}: Flexible high-performance data visualization for {Julia}}, journal = {Journal of Open Source Software} } diff --git a/assets/makie_logo_canvas_dark.svg b/assets/makie_logo_canvas_dark.svg new file mode 100644 index 00000000000..dd472f6b512 --- /dev/null +++ b/assets/makie_logo_canvas_dark.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/docs/documentation/backends/wglmakie.md b/docs/documentation/backends/wglmakie.md index dbd443d3755..39c05e215bd 100644 --- a/docs/documentation/backends/wglmakie.md +++ b/docs/documentation/backends/wglmakie.md @@ -85,6 +85,7 @@ using WGLMakie using JSServe, Markdown Page(exportable=true, offline=true) # for Franklin, you still need to configure WGLMakie.activate!() +Makie.inline!(true) # Make sure to inline plots into Documenter output! scatter(1:4, color=1:4) ``` \end{showhtml} @@ -160,7 +161,7 @@ You can for example directly register javascript function that get run on change \begin{showhtml}{} ```julia -using JSServe: onjs +using JSServe App() do session::Session s1 = Slider(1:100) @@ -187,7 +188,7 @@ But while this isn't in place, logging the the returned object makes it pretty e \begin{showhtml}{} ```julia -using JSServe: onjs, evaljs, on_document_load +using JSServe: on_document_load using WGLMakie App() do session::Session diff --git a/docs/documentation/figure.md b/docs/documentation/figure.md index 75cb51516b9..8117bcc4fbf 100644 --- a/docs/documentation/figure.md +++ b/docs/documentation/figure.md @@ -102,7 +102,7 @@ All nested GridLayouts that don't exist yet, but are needed for a nested plottin ## Figure padding -You can change the amount of whitespace around the figure content with the keyword `figure_padding`. +You can change the amount of whitespace (margin) around the figure content with the keyword `figure_padding`. This takes either a number for all four sides, or a tuple of four numbers for left, right, bottom, top. You can also theme this setting with `set_theme!(figure_padding = 30)`, for example. diff --git a/docs/examples/blocks/axis.md b/docs/examples/blocks/axis.md index bc9ac3765ca..b59f91d4647 100644 --- a/docs/examples/blocks/axis.md +++ b/docs/examples/blocks/axis.md @@ -59,452 +59,6 @@ empty!(axs[3]) f ``` \end{examplefigure} -## Setting Axis limits and reversing axes - -You can set axis limits with the functions `xlims!`, `ylims!` or `limits!`. The -numbers are meant in the order left right for `xlims!`, and bottom top for `ylims!`. -Therefore, if the second number is smaller than the first, the respective axis -will reverse. You can manually reverse an axis by setting `ax.xreversed = true` or -`ax.yreversed = true`. - -Note that if you enforce an aspect ratio between x-axis and y-axis using `autolimitaspect`, -the values you set with these functions will probably not be exactly what you get, -but they will be changed to fit the chosen ratio. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -f = Figure() - -axes = [Axis(f[i, j]) for j in 1:3, i in 1:2] - -for (i, ax) in enumerate(axes) - ax.title = "Axis $i" - poly!(ax, Point2f[(9, 9), (3, 1), (1, 3)], - color = cgrad(:inferno, 6, categorical = true)[i]) -end - -xlims!(axes[1], [0, 10]) # as vector -xlims!(axes[2], 10, 0) # separate, reversed -ylims!(axes[3], 0, 10) # separate -ylims!(axes[4], (10, 0)) # as tuple, reversed -limits!(axes[5], 0, 10, 0, 10) # x1, x2, y1, y2 -limits!(axes[6], BBox(0, 10, 0, 10)) # as rectangle - -f -``` -\end{examplefigure} -### Setting half-automatic limits - -You can set half limits by either giving one argument as `nothing` or by using the keyword syntax where only `low` or `high` is given. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -f = Figure() - -data = rand(100, 2) .* 0.7 .+ 0.15 - -Axis(f[1, 1], title = "xlims!(nothing, 1)") -scatter!(data) -xlims!(nothing, 1) - -Axis(f[1, 2], title = "xlims!(low = 0)") -scatter!(data) -xlims!(low = 0) - -Axis(f[2, 1], title = "ylims!(0, nothing)") -scatter!(data) -ylims!(0, nothing) - -Axis(f[2, 2], title = "ylims!(high = 1)") -scatter!(data) -ylims!(high = 1) - -f -``` -\end{examplefigure} -This also works when specifying limits directly, such as `Axis(..., limits = (nothing, 1, 2, nothing))`. - -### Auto-reset behavior - -When you create a new plot in an axis, `reset_limits!(ax)` is called, which adjusts the limits to the new bounds. -If you have previously set limits with `limits!`, `xlims!` or `ylims!`, these limits are not overridden by the new plot. If you want to override the manually set limits, call `autolimits!(ax)` to compute completely new limits from the axis content. - -The user-defined limits are stored in `ax.limits`. This can either be a tuple with two entries, where each entry can be either `nothing` or a tuple with numbers `(low, high)`.It can also be a tuple with four numbers `(xlow, xhigh, ylow, yhigh)`. You can pass this directly when creating a new axis. The same observable `limits` is also set using `limits!`, `xlims!` and `ylims!`, or reset to `(nothing, nothing)` using `autolimits!`. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -f = Figure() - -lines(f[1, 1], 0..10, sin) -lines(f[1, 2], 0..10, sin, axis = (limits = (0, 10, -1, 1),)) - -f -``` -\end{examplefigure} - -## Titles and subtitles - -You can change titles and subtitles with the `title` and `subtitle` attributes. -You can set `subtitlefont`, `subtitlefontsize` and `subtitlecolor` separately. -The alignment of the subtitle follows that of the title. - -The gap between title and subtitle is set with `subtitlegap` and the gap between `Axis` and title or subtitle with `titlegap`. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -f = Figure() - -Axis( - f[1, 1], - title = "First Title", - subtitle = "This is a longer subtitle" -) -Axis( - f[1, 2], - title = "Second Title", - subtitle = "This is a longer subtitle", - titlealign = :left, - subtitlecolor = :gray50, - titlegap = 10, - titlesize = 20, - subtitlesize = 15, -) -Axis( - f[2, 1], - title = "Third Title", - titlecolor = :gray50, - titlefont = :bold_italic, - titlealign = :right, - titlesize = 25, -) -Axis( - f[2, 2], - title = "Fourth Title\nWith Line Break", - subtitle = "This is an even longer subtitle,\nthat also has a line break.", - titlealign = :left, - subtitlegap = 2, - titlegap = 5, - subtitlefont = :italic, - subtitlelineheight = 0.9, - titlelineheight = 0.9, -) - -f -``` -\end{examplefigure} - -## Major and minor ticks - -To control major ticks, you can set the axis attributes `xticks` and `yticks` as well as `xtickformat` and `ytickformat`. -By default, tick locator and tick format are set to `automatic` and depend on the axis scale. -For a normal linear scale, `WilkinsonTicks` are used and for log scales a variant that additionally formats the labels with exponents. - -### Predefined tick locators - -#### WilkinsonTicks - -Here is an example how different numbers affect the ticks chosen by the default tick locator, `WilkinsonTicks`. -Note that the number is only a target, the actual number of ticks can be higher or lower depending on how the algorithm evaluates the options. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -fig = Figure() -for (i, n) in enumerate([2, 4, 6]) - Axis(fig[i, 1], - xticks = WilkinsonTicks(n), - title = "WilkinsonTicks($n)", - yticksvisible = false, - yticklabelsvisible = false, - ) -end -fig -``` -\end{examplefigure} - -#### MultiplesTicks - -`MultiplesTicks` can be used when an axis should be marked at multiples of a certain number. -A common scenario is plotting a trigonometric function which should be marked at pi intervals. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -lines(0..20, sin, axis = (xticks = MultiplesTicks(4, pi, "π"),)) -``` -\end{examplefigure} - -#### AbstractVector of numbers - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -lines(0..20, sin, axis = (xticks = 0:3:18,)) -``` -\end{examplefigure} - -#### Tuple of tick values and tick labels - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -values = [0, 5, 10, 15, 20] -labels = ["zero", "five", "ten", "fifteen", "twenty"] - -lines(0..20, sin, axis = (xticks = (values, labels),)) -``` -\end{examplefigure} - -#### LogTicks - -{{doc LogTicks}} - -For example, you could combine `LogTicks` with a custom tick locator that just returns all integers between the limits. -This can be useful for log plots where all powers of 10 should be shown. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -struct IntegerTicks end - -Makie.get_tickvalues(::IntegerTicks, vmin, vmax) = ceil(Int, vmin) : floor(Int, vmax) - -lines(10 .^ (0:0.01:10), axis = (yscale = log10, yticks = LogTicks(IntegerTicks()))) -``` -\end{examplefigure} - -### Predefined tick formatters - -By default, Makie uses `Showoff.showoff` to display the vector of tick values. -There are other options you can use to define the format of your ticks. - -#### Functions - -You can use any function as tick formatter that receives a vector of ticks and returns a vector of labels. - -For example, you could append "k" for numbers larger than one thousand: - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -function k_suffix(values) - map(values) do v - if v >= 1000 - "$(v/1000)k" - else - "$v" - end - end -end - -f = Figure() -Axis(f[1, 1], xtickformat = k_suffix, limits = ((450, 1550), nothing)) -f -``` -\end{examplefigure} - -### Mirrored ticks - -To display minor and major ticks on both sides of the axis, set `xticksmirrored` or `yticksmirrored` to `true`. -Color, size and alignment of the mirrored ticks are the same as for the normal ticks. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -f = Figure() -Axis(f[1, 1], - xticks = 0:10, - yticks = 0:10, - xticksmirrored = true, - yticksmirrored = true, - xminorticksvisible = true, - yminorticksvisible = true, - xminortickalign = 1, - yminortickalign = 1, - xtickalign = 1, - ytickalign = 1, -) -f -``` -\end{examplefigure} - -#### Format strings - -You can use a format string which is passed to `Formatting.format` from [Formatting.jl](https://github.com/JuliaIO/Formatting.jl), where you can mix the formatted numbers with other text like in `"{:.2f}ms"`. - -Here are the same ticks with different format strings: - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -f = Figure() -for (i, str) in enumerate(["{:.1f}", "{:.2f}", "t = {:.4f}ms"]) - Axis(f[i, 1], - xticks = 2:2:8, - xtickformat = str, - title = str, - yticklabelsvisible = false, - yticksvisible = false - ) -end -f -``` -\end{examplefigure} - -### Custom tick locators and formatters - -The general logic to find ticks and tick labels is this: -First, it's checked if `tickvalues, ticklabels = Makie.get_ticks(ticklocator, scale, formatter, vmin, vmax)` has a specific method for the current tick locator, axis scale, tick formatter, and axis limits vmin and vmax. -If so, use the resulting tick values and tick labels directly. -If not, run `tickvalues = Makie.get_tickvalues(ticklocator, scale, vmin, vmax)` and then `ticklabels = Makie.get_ticklabels(formatter, tickvalues)`. - -Therefore, you can overload one or more of these functions to implement custom tick locators and / or formatters. - -#### Example: Time ticks - -In this example we define a very simple tick locator for time values that has its own formatting logic. -Therefore, we define only `Makie.get_ticks` for the case where the formatter is set to `automatic`. - -Let's say that we're plotting data in a resolution of seconds, and want to switch between seconds, minutes and hours. -The idea is to not actually implement new tick finding, just to rescale the values to use with the default tick locator and append the appropriate unit suffix. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -struct TimeTicks end - -function Makie.get_ticks(::TimeTicks, any_scale, ::Makie.Automatic, vmin, vmax) - if vmax >= 3600 - # divide limits by 3600 before finding standard ticks - vals_h = Makie.get_tickvalues( - Makie.automatic, any_scale, vmin/3600, vmax/3600) - labels = string.(vals_h, "h") - # rescale tick values to seconds - vals_s = vals_h .* 3600 - elseif vmax >= 60 - vals_min = Makie.get_tickvalues( - Makie.automatic, any_scale, vmin/60, vmax/60) - labels = string.(vals_min, "min") - vals_s = vals_min .* 60 - else - vals_s = Makie.get_tickvalues( - Makie.automatic, any_scale, vmin, vmax) - labels = string.(vals_s, "s") - end - vals_s, labels -end - -f = Figure() -for (i, limits) in enumerate([(1, 55), (1, 350), (1, 8000)]) - Axis(f[i, 1], - xticks = TimeTicks(), - title = "$limits", - yticklabelsvisible = false, - yticksvisible = false, - limits = (limits, nothing) - ) -end -f -``` -\end{examplefigure} - -## Minor ticks and grids - -You can show minor ticks and grids by setting `x/yminorticksvisible = true` and `x/yminorgridvisible = true` which are off by default. -You can set size, color, width, align etc. like for the normal ticks, but there are no labels. -The `x/yminorticks` attributes control how minor ticks are computed given major ticks and axis limits. -For that purpose you can create your own minortick type and overload `Makie.get_minor_tickvalues(minorticks, tickvalues, vmin, vmax)`. - -The default minor tick type is `IntervalsBetween(n, mirror = true)` where `n` gives the number of intervals each gap between major ticks is divided into with minor ticks, and `mirror` decides if outside of the major ticks there are more minor ticks with the same intervals as the adjacent gaps. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -theme = Attributes( - Axis = ( - xminorticksvisible = true, - yminorticksvisible = true, - xminorgridvisible = true, - yminorgridvisible = true, - ) -) - -fig = with_theme(theme) do - fig = Figure() - axs = [Axis(fig[fldmod1(n, 2)...], - title = "IntervalsBetween($(n+1))", - xminorticks = IntervalsBetween(n+1), - yminorticks = IntervalsBetween(n+1)) for n in 1:4] - fig -end - -fig -``` -\end{examplefigure} - -Minor ticks can also be given as an `AbstractVector` of real numbers. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -lines(1..10, sin, axis = ( - yminorgridvisible = true, - yminorticksvisible = true, - yminorticks = -0.9:0.1:0.9, - yticks = [-1, 1], -)) -``` -\end{examplefigure} - ## Hiding Axis spines and decorations @@ -555,235 +109,6 @@ f ``` \end{examplefigure} -## Trimmed spines - -The attributes `xtrimspine` and `ytrimspine` can be used to limit the respective spines to the range of the outermost major ticks. A tuple of Boolean values can be given to trim only one end of the spine. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -hist(randn(100) ./ 4 .+ 5, - strokewidth = 1, - strokecolor = :black, - axis = ( - xtrimspine = true, - ytrimspine = (false, true), - topspinevisible = false, - rightspinevisible = false, - title = "Trimmed spines", - xgridvisible = false, - ygridvisible = false, - ) -) -``` -\end{examplefigure} - -## Log scales and other axis scales - -The two attributes `xscale` and `yscale`, which by default are set to `identity`, can be used to project the data in a nonlinear way, in addition to the linear zoom that the limits provide. - -Take care that the axis limits always stay inside the limits appropriate for the chosen scaling function, for example, `log` functions fail for values `x <= 0`, `sqrt` for `x < 0`, etc. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -data = LinRange(0.01, 0.99, 200) - -f = Figure(resolution = (800, 800)) - -for (i, scale) in enumerate([identity, log10, log2, log, sqrt, Makie.logit]) - - row, col = fldmod1(i, 2) - Axis(f[row, col], yscale = scale, title = string(scale), - yminorticksvisible = true, yminorgridvisible = true, - yminorticks = IntervalsBetween(8)) - - lines!(data, color = :blue) -end - -f -``` -\end{examplefigure} -### Pseudolog and symlog scales - -Some plotting functions, like barplots or density plots, have offset parameters which are usually zero, which you have to set to some non-zero value explicitly so they work in `log` axes. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -processors = ["VAX-11/780", "Sun-4/260", "PowerPC 604", - "Alpha 21164", "Intel Pentium III", "Intel Xeon"] -relative_speeds = [1, 9, 117, 280, 1779, 6505] - -barplot(relative_speeds, fillto = 0.5, - axis = (yscale = log10, ylabel ="relative speed", - xticks = (1:6, processors), xticklabelrotation = pi/8)) - -ylims!(0.5, 10000) -current_figure() -``` -\end{examplefigure} -Another option are pseudolog and symlog scales. -Pseudolog is similar to log, but modified in order to work for zero and for negative values. -The `pseudolog10` function is defined as `sign(x) * log10(abs(x) + 1)`. - -Another option for symmetric log scales including zero is the symmetric log scale `Symlog10`, which combines a normal log scale with a linear scale between two boundary values around zero. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -CairoMakie.activate!() # hide - - -f = Figure(resolution = (800, 700)) - -lines(f[1, 1], -100:0.1:100, axis = ( - yscale = Makie.pseudolog10, - title = "Pseudolog scale", - yticks = [-100, -10, -1, 0, 1, 10, 100])) - -lines(f[2, 1], -100:0.1:100, axis = ( - yscale = Makie.Symlog10(10.0), - title = "Symlog10 with linear scaling between -10 and 10", - yticks = [-100, -10, 0, 10, 100])) - -f -``` -\end{examplefigure} - -## Controlling Axis aspect ratios - -If you're plotting images, you might want to force a specific aspect ratio -of an axis, so that the images are not stretched. The default is that an axis -uses all of the available space in the layout. You can use `AxisAspect` and -`DataAspect` to control the aspect ratio. For example, `AxisAspect(1)` forces a -square axis and `AxisAspect(2)` results in a rectangle with a width of two -times the height. -`DataAspect` uses the currently chosen axis limits and brings the axes into the -same aspect ratio. This is the easiest to use with images. -A different aspect ratio can only reduce the axis space that is being used, also -it necessarily has to break the layout a little bit. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie -using FileIO -CairoMakie.activate!() # hide - -using Random # hide -Random.seed!(1) # hide - -f = Figure() - -axes = [Axis(f[i, j]) for i in 1:2, j in 1:3] -tightlimits!.(axes) - -img = rotr90(load(assetpath("cow.png"))) - -for ax in axes - image!(ax, img) -end - -axes[1, 1].title = "Default" - -axes[1, 2].title = "DataAspect" -axes[1, 2].aspect = DataAspect() - -axes[1, 3].title = "AxisAspect(418/348)" -axes[1, 3].aspect = AxisAspect(418/348) - -axes[2, 1].title = "AxisAspect(1)" -axes[2, 1].aspect = AxisAspect(1) - -axes[2, 2].title = "AxisAspect(2)" -axes[2, 2].aspect = AxisAspect(2) - -axes[2, 3].title = "AxisAspect(2/3)" -axes[2, 3].aspect = AxisAspect(2/3) - -f -``` -\end{examplefigure} - -## Controlling data aspect ratios - -If you want the content of an axis to adhere to a certain data aspect ratio, there is -another way than forcing the aspect ratio of the whole axis to be the same, and -possibly breaking the layout. This works via the axis attribute `autolimitaspect`. -It can either be set to `nothing` which means the data limits can have any arbitrary -aspect ratio. Or it can be set to a number, in which case the targeted limits of the -axis (that are computed by `autolimits!`) are enlarged to have the correct aspect ratio. - -You can see the different ways to get a plot with an unstretched circle, using -different ways of setting aspect ratios, in the following example. - -```julia:video -using CairoMakie -using Animations - - - -# scene setup for animation -########################################################### - -container_scene = Scene(camera = campixel!, resolution = (1200, 1200)) - -t = Observable(0.0) - -a_width = Animation([1, 7], [1200.0, 800], sineio(n=2, yoyo=true, postwait=0.5)) -a_height = Animation([2.5, 8.5], [1200.0, 800], sineio(n=2, yoyo=true, postwait=0.5)) - -scene_area = lift(t) do t - Recti(0, 0, round(Int, a_width(t)), round(Int, a_height(t))) -end - -scene = Scene(container_scene, scene_area, camera = campixel!) - -rect = poly!(scene, scene_area, color=RGBf(0.97, 0.97, 0.97), strokecolor=:transparent, strokewidth=0) - -outer_layout = GridLayout(scene, alignmode = Outside(30)) - - -# example begins here -########################################################### - -layout = outer_layout[1, 1] = GridLayout() - -titles = ["aspect enforced\nvia layout", "axis aspect\nset directly", "no aspect enforced", "data aspect conforms\nto axis size"] -axs = layout[1:2, 1:2] = [Axis(scene, title = t) for t in titles] - -for a in axs - lines!(a, Circle(Point2f(0, 0), 100f0)) -end - -rowsize!(layout, 1, Fixed(400)) -# force the layout cell [1, 1] to be square -colsize!(layout, 1, Aspect(1, 1)) - -axs[2].aspect = 1 -axs[4].autolimitaspect = 1 - -rects = layout[1:2, 1:2] = [Box(scene, color = (:black, 0.05), - strokecolor = :transparent) for _ in 1:4] - -record(container_scene, "example_circle_aspect_ratios.mp4", 0:1/30:9; framerate=30) do ti - t[] = ti -end -nothing # hide -``` - -\video{example_circle_aspect_ratios} - - ## Linking axes You can link axes to each other. Every axis simply keeps track of a list of other @@ -864,28 +189,6 @@ f ``` \end{examplefigure} -## Changing x and y axis position - -By default, the x axis is at the bottom, and the y axis at the left side. -You can change this with the attributes `xaxisposition = :top` and `yaxisposition = :right`. - -\begin{examplefigure}{svg = true} -```julia -using CairoMakie - -f = Figure() - -for i in 1:2, j in 1:2 - Axis( - f[i, j], - limits = (0, 5, 0, 5), - xaxisposition = (i == 1 ? :top : :bottom), - yaxisposition = (j == 1 ? :left : :right)) -end - -f -``` -\end{examplefigure} ## Creating a twin axis There is currently no dedicated function to do this, but you can simply add an Axis on top of another, then hide everything but the second axis. @@ -1015,3 +318,8 @@ register_interaction!(ax, :left_and_right, MyInteraction(false, false)) Some interactions might have more complex state involving plot objects that need to be setup or removed. For those purposes, you can overload the methods `registration_setup!(parent, interaction)` and `deregistration_cleanup!(parent, interaction)` which are called during registration and deregistration, respectively. + + +## Attributes + +\attrdocs{Axis} \ No newline at end of file diff --git a/docs/examples/blocks/axis3.md b/docs/examples/blocks/axis3.md index 504aa7f2ace..d11ea8c5cd5 100644 --- a/docs/examples/blocks/axis3.md +++ b/docs/examples/blocks/axis3.md @@ -1,142 +1,5 @@ # Axis3 -## Viewing angles +## Attributes -The two attributes `azimuth` and `elevation` control the angles from which the plots are viewed. - -\begin{examplefigure}{} -```julia -using GLMakie -using FileIO -GLMakie.activate!() # hide - - -f = Figure() - -brain = load(assetpath("brain.stl")) -colors = [tri[1][2] for tri in brain for i in 1:3] - -azimuths = [0, 0.2pi, 0.4pi] -elevations = [-0.2pi, 0, 0.2pi] - -for (i, elevation) in enumerate(elevations) - for (j, azimuth) in enumerate(azimuths) - ax = Axis3(f[i, j], aspect = :data, - title = "elevation = $(round(elevation/pi, digits = 2))π\nazimuth = $(round(azimuth/pi, digits = 2))π", - elevation = elevation, azimuth = azimuth, - protrusions = (0, 0, 0, 40)) - - hidedecorations!(ax) - mesh!(brain, color = colors, colormap = :thermal) - end -end - -f -``` -\end{examplefigure} -## Data aspects and view mode - -The attributes `aspect` and `viewmode` both influence the apparent relative scaling of the three axes. - -### `aspect` - -The `aspect` changes how long each axis is relative to the other two. - -If you set it to `:data`, the axes will be scaled according to their lengths in data space. -The visual result is that objects with known real-world dimensions look correct and not squished. - -You can also set it to a three-tuple, where each number gives the relative length of that axis vs the others. - -\begin{examplefigure}{} -```julia -using GLMakie -using FileIO -GLMakie.activate!() # hide - - -f = Figure() - -brain = load(assetpath("brain.stl")) - -aspects = [:data, (1, 1, 1), (1, 2, 3), (3, 2, 1)] - -for (i, aspect) in enumerate(aspects) - ax = Axis3(f[fldmod1(i, 2)...], aspect = aspect, title = "$aspect") - mesh!(brain, color = :bisque) -end - -f -``` -\end{examplefigure} -### `viewmode` - -The `viewmode` changes how the final projection is adjusted to fit the axis into its scene. - -The default is `:fitzoom`, which scales the final projection evenly, so that the farthest corner of the axis goes right up to the scene boundary. -If you rotate an axis with this mode, the apparent size will shrink and grow depending on the viewing angles, but the plot objects will never look skewed relative to their `aspect`. - -The next option `:fit` is like `:fitzoom`, but without the zoom component. -The axis is scaled so that no matter what the viewing angles are, the axis does not clip the scene boundary and its apparent size doesn't change, even though this makes less efficient use of the available space. -You can imagine a sphere around the axis, which is zoomed right up until it touches the scene boundary. - -The last option is `:stretch`. -In this mode, scaling in both x and y direction is applied to fit the axis right into its scene box. -Be aware that this mode can skew the axis a lot and doesn't keep the `aspect` intact. -On the other hand, it uses the available space most efficiently. - -\begin{examplefigure}{} -```julia -using GLMakie -GLMakie.activate!() # hide - - -f = Figure() - -r = LinRange(-1, 1, 100) -cube = [(x.^2 + y.^2 + z.^2) for x = r, y = r, z = r] -cube_with_holes = cube .* (cube .> 1.4) - -viewmodes = [:fitzoom, :fit, :stretch] - -for (j, viewmode) in enumerate(viewmodes) - for (i, azimuth) in enumerate([1.1, 1.275, 1.45] .* pi) - ax = Axis3(f[i, j], aspect = :data, - azimuth = azimuth, - viewmode = viewmode, title = "$viewmode") - hidedecorations!(ax) - ax.protrusions = (0, 0, 0, 20) - volume!(cube_with_holes, algorithm = :iso, isorange = 0.05, isovalue = 1.7) - end -end - -f -``` -\end{examplefigure} -## Perspective or orthographic look - -You can switch smoothly between an orthographic look and a perspective look using the `perspectiveness` attribute. - -A value of 0 looks like an orthographic projection (it is only approximate to a real one) while 1 gives a quite strong perspective look. - -\begin{examplefigure}{} -```julia -using GLMakie -GLMakie.activate!() # hide - - -f = Figure(resolution = (1200, 800), fontsize = 14) - -xs = LinRange(0, 10, 100) -ys = LinRange(0, 10, 100) -zs = [cos(x) * sin(y) for x in xs, y in ys] - -for (i, perspectiveness) in enumerate(LinRange(0, 1, 6)) - Axis3(f[fldmod1(i, 3)...], perspectiveness = perspectiveness, - title = "$perspectiveness") - - surface!(xs, ys, zs) -end - -f -``` -\end{examplefigure} +\attrdocs{Axis3} \ No newline at end of file diff --git a/docs/examples/blocks/box.md b/docs/examples/blocks/box.md index eebef1eb2c0..be3a2c41ac4 100644 --- a/docs/examples/blocks/box.md +++ b/docs/examples/blocks/box.md @@ -20,3 +20,7 @@ rects = fig[1:4, 1:6] = [ fig ``` \end{examplefigure} + +## Attributes + +\attrdocs{Box} \ No newline at end of file diff --git a/docs/examples/blocks/button.md b/docs/examples/blocks/button.md index 67923cc141f..ad8fd3e17a9 100644 --- a/docs/examples/blocks/button.md +++ b/docs/examples/blocks/button.md @@ -28,3 +28,7 @@ ylims!(ax, 0, 20) fig ``` \end{examplefigure} + +## Attributes + +\attrdocs{Button} \ No newline at end of file diff --git a/docs/examples/blocks/colorbar.md b/docs/examples/blocks/colorbar.md index ed02749b6c9..b2fe0561463 100644 --- a/docs/examples/blocks/colorbar.md +++ b/docs/examples/blocks/colorbar.md @@ -66,3 +66,8 @@ Colorbar(fig[2, 2][1, 2], hm, ticks = -1:0.25:1) fig ``` \end{examplefigure} + + +## Attributes + +\attrdocs{Colorbar} \ No newline at end of file diff --git a/docs/examples/blocks/intervalslider.md b/docs/examples/blocks/intervalslider.md index 79bcf559227..9d69babf4f7 100644 --- a/docs/examples/blocks/intervalslider.md +++ b/docs/examples/blocks/intervalslider.md @@ -56,3 +56,7 @@ scatter!(points, color = colors, colormap = [:black, :orange], strokewidth = 0) f ``` \end{examplefigure} + +## Attributes + +\attrdocs{IntervalSlider} \ No newline at end of file diff --git a/docs/examples/blocks/label.md b/docs/examples/blocks/label.md index cd32e01925c..3960ad602fe 100644 --- a/docs/examples/blocks/label.md +++ b/docs/examples/blocks/label.md @@ -52,4 +52,8 @@ Label(f[1, 3], f ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} + +## Attributes + +\attrdocs{Label} \ No newline at end of file diff --git a/docs/examples/blocks/legend.md b/docs/examples/blocks/legend.md index 9a451de590a..1d4e5b3ba0f 100644 --- a/docs/examples/blocks/legend.md +++ b/docs/examples/blocks/legend.md @@ -320,3 +320,6 @@ f ``` \end{examplefigure} +## Attributes + +\attrdocs{Legend} \ No newline at end of file diff --git a/docs/examples/blocks/lscene.md b/docs/examples/blocks/lscene.md index 2770a41cf45..01575f777bd 100644 --- a/docs/examples/blocks/lscene.md +++ b/docs/examples/blocks/lscene.md @@ -25,3 +25,7 @@ p = meshscatter!(lscene, randn(300, 3), color=:gray) fig ``` \end{examplefigure} + +## Attributes + +\attrdocs{LScene} \ No newline at end of file diff --git a/docs/examples/blocks/menu.md b/docs/examples/blocks/menu.md index c4a8e6cefb3..c262e89c62f 100644 --- a/docs/examples/blocks/menu.md +++ b/docs/examples/blocks/menu.md @@ -72,3 +72,7 @@ menu2.is_open = true fig ``` \end{examplefigure} + +## Attributes + +\attrdocs{Menu} \ No newline at end of file diff --git a/docs/examples/blocks/slider.md b/docs/examples/blocks/slider.md index 3fdb634d0ab..34691553efb 100644 --- a/docs/examples/blocks/slider.md +++ b/docs/examples/blocks/slider.md @@ -40,3 +40,7 @@ fig ## Labelled sliders and grids The functions \apilink{labelslider!} and \apilink{labelslidergrid!} are deprecated, use \apilink{SliderGrid} instead. + +## Attributes + +\attrdocs{Slider} \ No newline at end of file diff --git a/docs/examples/blocks/slidergrid.md b/docs/examples/blocks/slidergrid.md index 7d25c1c9bdc..36b3f70b862 100644 --- a/docs/examples/blocks/slidergrid.md +++ b/docs/examples/blocks/slidergrid.md @@ -33,3 +33,8 @@ ylims!(ax, 0, 30) fig ``` \end{examplefigure} + + +## Attributes + +\attrdocs{SliderGrid} \ No newline at end of file diff --git a/docs/examples/blocks/textbox.md b/docs/examples/blocks/textbox.md index 73ebd84a24d..e24903647ae 100644 --- a/docs/examples/blocks/textbox.md +++ b/docs/examples/blocks/textbox.md @@ -45,3 +45,7 @@ lines(f[1, 1], xs, sinecurve) f ``` \end{examplefigure} + +## Attributes + +\attrdocs{Textbox} \ No newline at end of file diff --git a/docs/examples/blocks/toggle.md b/docs/examples/blocks/toggle.md index cb1c56db4fe..87e7375ba6c 100644 --- a/docs/examples/blocks/toggle.md +++ b/docs/examples/blocks/toggle.md @@ -27,4 +27,9 @@ connect!(line2.visible, toggles[2].active) fig ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} + + +## Attributes + +\attrdocs{Toggle} \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 8c327dca3cc..a9daafd5088 100644 --- a/docs/index.md +++ b/docs/index.md @@ -268,7 +268,7 @@ You can use the following BibTeX entry: number = {65}, pages = {3349}, author = {Simon Danisch and Julius Krumbiegel}, - title = {Makie.jl: Flexible high-performance data visualization for Julia}, + title = {{Makie.jl}: Flexible high-performance data visualization for {Julia}}, journal = {Journal of Open Source Software} } ``` diff --git a/docs/utils.jl b/docs/utils.jl index 78c23df1caa..73fcc8cfac4 100644 --- a/docs/utils.jl +++ b/docs/utils.jl @@ -444,3 +444,51 @@ function contenttable() end end end + + +function lx_attrdocs(lxc, _) + type = getproperty(Makie, Symbol(Franklin.stent(lxc.braces[1]))) + + attrkeys = sort(collect(keys(Makie.default_attribute_values(type, nothing)))) + + io = IOBuffer() + + for attrkey in attrkeys + + docs = get(Makie._attribute_docs(type), attrkey, nothing) + examples = get(Makie.attribute_examples(type), attrkey, Makie.Example[]) + default_str = Makie.attribute_default_expressions(type)[attrkey] + + println(io, "### `$attrkey`") + println(io) + println(io, "Defaults to `$default_str`") + println(io) + + if docs === nothing + println(io, "No docstring defined for attribute `$attrkey`.") + else + println(io, docs) + end + println(io) + + for example in examples + use_svg = (example.backend === :CairoMakie) && example.svg + option_bracket = use_svg ? "{svg = true}" : "{}" + println(io, "#### Example: $(example.name)") + println(io, "\\begin{examplefigure}$option_bracket") + println(io, "```julia") + println(io, "using $(example.backend_using)") + println(io, "using $(example.backend) # hide") + println(io, "$(example.backend).activate!() # hide") + println(io, example.code) + println(io, "```") + println(io, "\\end{examplefigure}") + println(io) + end + + println(io) + end + + return String(take!(io)) +end + diff --git a/src/Makie.jl b/src/Makie.jl index c41dc79b7bf..0c819c9ba18 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -51,6 +51,8 @@ import SparseArrays import TriplotBase import MiniQhull import Setfield +import REPL +import MacroTools using IntervalSets: IntervalSets, (..), OpenInterval, ClosedInterval, AbstractInterval, Interval, endpoints using FixedPointNumbers: N0f8 diff --git a/src/conversions.jl b/src/conversions.jl index ccd6e5373a6..f2d10b43f93 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -211,10 +211,12 @@ end Takes an input `Array{LineString}` or a `MultiLineString` and decomposes it to points. """ function convert_arguments(PB::PointBased, linestring::Union{Array{<:LineString}, MultiLineString}) - arr = copy(convert_arguments(PB, linestring[1])[1]) - for ls in 2:length(linestring) - push!(arr, Point2f(NaN)) - append!(arr, convert_arguments(PB, linestring[ls])[1]) + arr = Point2f[]; n = length(linestring) + for idx in 1:n + append!(arr, convert_arguments(PB, linestring[idx])[1]) + if idx != n # don't add NaN at the end + push!(arr, Point2f(NaN)) + end end return (arr,) end @@ -226,15 +228,17 @@ end Takes an input `Polygon` and decomposes it to points. """ function convert_arguments(PB::PointBased, pol::Polygon) - arr = copy(convert_arguments(PB, pol.exterior)[1]) - push!(arr, arr[1]) # close exterior - if !isempty(pol.interiors) + converted = convert_arguments(PB, pol.exterior)[1] # this should always be a Tuple{<: Vector{Point}} + arr = copy(converted) + if !isempty(arr) && arr[1] != arr[end] + push!(arr, arr[1]) # close exterior + end + for interior in pol.interiors push!(arr, Point2f(NaN)) - for interior in pol.interiors - inter = convert_arguments(PB, interior)[1] - append!(arr, inter) - # close interior + separate! - push!(arr, inter[1], Point2f(NaN)) + inter = convert_arguments(PB, interior)[1] # this should always be a Tuple{<: Vector{Point}} + append!(arr, inter) + if !isempty(inter) && inter[1] != inter[end] + push!(arr, inter[1]) # close interior end end return (arr,) @@ -247,10 +251,14 @@ end Takes an input `Array{Polygon}` or a `MultiPolygon` and decomposes it to points. """ function convert_arguments(PB::PointBased, mp::Union{Array{<:Polygon}, MultiPolygon}) - arr = copy(convert_arguments(PB, mp[1])[1]) - for p in 2:length(mp) - push!(arr, Point2f(NaN)) - append!(arr, convert_arguments(PB, mp[p])[1]) + arr = Point2f[] + n = length(mp) + for idx in 1:n + converted = convert_arguments(PB, mp[idx])[1] # this should always be a Tuple{<: Vector{Point}} + append!(arr, converted) + if idx != n # don't add NaN at the end + push!(arr, Point2f(NaN)) + end end return (arr,) end @@ -944,6 +952,7 @@ to_align(x::Tuple{Symbol, Symbol}) = Vec2f(alignment2num.(x)) to_align(x::Vec2f) = x const FONT_CACHE = Dict{String, NativeFont}() +const FONT_CACHE_LOCK = Base.ReentrantLock() function load_font(filepath) font = FreeTypeAbstraction.try_load(filepath) @@ -962,28 +971,30 @@ A font can either be specified by a file path, such as "folder/with/fonts/font.o or by a (partial) name such as "Helvetica", "Helvetica Bold" etc. """ function to_font(str::String) - get!(FONT_CACHE, str) do - # load default fonts without font search to avoid latency - if str == "default" || str == "TeX Gyre Heros Makie" - return load_font(assetpath("fonts", "TeXGyreHerosMakie-Regular.otf")) - elseif str == "TeX Gyre Heros Makie Bold" - return load_font(assetpath("fonts", "TeXGyreHerosMakie-Bold.otf")) - elseif str == "TeX Gyre Heros Makie Italic" - return load_font(assetpath("fonts", "TeXGyreHerosMakie-Italic.otf")) - elseif str == "TeX Gyre Heros Makie Bold Italic" - return load_font(assetpath("fonts", "TeXGyreHerosMakie-BoldItalic.otf")) - # load fonts directly if they are given as font paths - elseif isfile(str) - return load_font(str) - end - # for all other cases, search for the best match on the system - fontpath = assetpath("fonts") - font = FreeTypeAbstraction.findfont(str; additional_fonts=fontpath) - if font === nothing - @warn("Could not find font $str, using TeX Gyre Heros Makie") - return to_font("TeX Gyre Heros Makie") + lock(FONT_CACHE_LOCK) do + return get!(FONT_CACHE, str) do + # load default fonts without font search to avoid latency + if str == "default" || str == "TeX Gyre Heros Makie" + return load_font(assetpath("fonts", "TeXGyreHerosMakie-Regular.otf")) + elseif str == "TeX Gyre Heros Makie Bold" + return load_font(assetpath("fonts", "TeXGyreHerosMakie-Bold.otf")) + elseif str == "TeX Gyre Heros Makie Italic" + return load_font(assetpath("fonts", "TeXGyreHerosMakie-Italic.otf")) + elseif str == "TeX Gyre Heros Makie Bold Italic" + return load_font(assetpath("fonts", "TeXGyreHerosMakie-BoldItalic.otf")) + # load fonts directly if they are given as font paths + elseif isfile(str) + return load_font(str) + end + # for all other cases, search for the best match on the system + fontpath = assetpath("fonts") + font = FreeTypeAbstraction.findfont(str; additional_fonts=fontpath) + if font === nothing + @warn("Could not find font $str, using TeX Gyre Heros Makie") + return to_font("TeX Gyre Heros Makie") + end + return font end - return font end end to_font(x::Vector{String}) = to_font.(x) diff --git a/src/display.jl b/src/display.jl index ab6650f2144..9ad073b222d 100644 --- a/src/display.jl +++ b/src/display.jl @@ -59,7 +59,7 @@ function set_screen_config!(backend::Module, new_values) bkeys = keys(backend_defaults) for (k, v) in pairs(new_values) if !(k in bkeys) - error("$k is not a valid screen config. Applicable options: $(keys(backend_defaults)). For help, check `?$(backend).ScreenCofig`") + error("$k is not a valid screen config. Applicable options: $(keys(backend_defaults)). For help, check `?$(backend).ScreenConfig`") end backend_defaults[k] = v end @@ -82,7 +82,7 @@ function merge_screen_config(::Type{Config}, screen_config_kw) where Config end -const ALWAYS_INLINE_PLOTS = Ref{Bool}(false) +const ALWAYS_INLINE_PLOTS = Ref{Union{Automatic, Bool}}(automatic) """ inline!(inline=true) @@ -93,12 +93,31 @@ Only case Makie always shows the plot inside the plotpane is when using VSCode e If you want to always force inlining the plot into the plotpane, set `inline!(true)` (E.g. when run in the VSCode REPL). In other cases `inline!(true/false)` won't do anything. """ -function inline!(inline=true) +function inline!(inline=automatic) ALWAYS_INLINE_PLOTS[] = inline end wait_for_display(screen) = nothing +function has_mime_display(mime) + for display in Base.Multimedia.displays + # Ugh, why would textdisplay say it supports HTML?? + display isa TextDisplay && continue + displayable(display, mime) && return true + end + return false +end + +can_show_inline(::Missing) = false # no backend +function can_show_inline(Backend) + for mime in [MIME"text/html"(), MIME"image/png"(), MIME"image/svg+xml"()] + if backend_showable(Backend.Screen, mime) + return has_mime_display(mime) + end + end + return false +end + """ Base.display(figlike::FigureLike; backend=current_backend(), screen_config...) @@ -106,6 +125,8 @@ Displays the figurelike in a window or the browser, depending on the backend. The parameters for `screen_config` are backend dependend, see `?Backend.Screen` or `Base.doc(Backend.Screen)` for applicable options. + +`backend` accepts Makie backend modules, e.g.: `backend = GLMakie`, `backend = CairoMakie`, etc. """ function Base.display(figlike::FigureLike; backend=current_backend(), update=true, screen_config...) if ismissing(backend) @@ -117,12 +138,23 @@ function Base.display(figlike::FigureLike; backend=current_backend(), update=tru In that case, try `]build GLMakie` and watch out for any warnings. """) end - if ALWAYS_INLINE_PLOTS[] + inline = ALWAYS_INLINE_PLOTS[] + # We show inline if explicitely requested or if automatic and we can actually show something inline! + if (inline === true || inline === automatic) && can_show_inline(backend) Core.invoke(display, Tuple{Any}, figlike) + # In WGLMakie, we need to wait for the display being done screen = getscreen(get_scene(figlike)) wait_for_display(screen) return screen else + if inline === true + @warn """ + + Makie.inline!(do_inline) was set to true, but we didn't detect a display that can show the plot, + so we aren't inlining the plot and try to show the plot in a window. + If this wasn't set on purpose, call `Makie.inline!()` to restore the default. + """ + end scene = get_scene(figlike) update && update_state_before_display!(figlike) screen = getscreen(backend, scene; screen_config...) @@ -144,9 +176,22 @@ function Base.display(screen::MakieScreen, figlike::FigureLike; update=true, dis return screen end +# This isn't particularly nice, +# But, for `Makie.inline!(false)`, we want to show a plot in a gui regardless +# of an enabled plotpane or not +# Since VSCode doesn't call any display/show method for Figurelike if we return +# `showable(mime, fig) == false`, we need to return `showable(mime, figlike) == true` +# For some vscode displayable mime, even for `Makie.inline!(false)` when we want to display in our own window. +# Only diagnostic can be used for this, since other mimes expect something to be shown afterall and +# therefore will look broken in the plotpane if we dont print anything to the IO. +# I tried `throw(MethodError(...))` as well, but with plotpane enabled + showable == true, +# VScode doesn't catch that method error. +const MIME_TO_TRICK_VSCODE = MIME"application/vnd.julia-vscode.diagnostics" + function _backend_showable(mime::MIME{SYM}) where SYM - # If we open a window, don't become part of the display/show system - !ALWAYS_INLINE_PLOTS[] && return false + if ALWAYS_INLINE_PLOTS[] == false + return mime isa MIME_TO_TRICK_VSCODE + end Backend = current_backend() if ismissing(Backend) return Symbol("text/plain") == SYM @@ -178,12 +223,19 @@ function Base.show(io::IO, ::MIME"text/plain", scene::Scene) show(io, scene) end +# VSCode per default displays an object as markdown as well. +# Which, without throwing a method error, would show a plot 2 times from within the display system. +# This can lead to hangs e.g. for WGLMakie, where there is only one plotpane/browser, which then one waits on +function Base.show(io::IO, m::MIME"text/markdown", fig::FigureLike) + throw(MethodError(show, io, m, fig)) +end + function Base.show(io::IO, m::MIME, figlike::FigureLike) - if !ALWAYS_INLINE_PLOTS[] - # If we always want to open a window, we call display manually here - # and then throw a method error to signal the calling display system, that we don't want to display it with them + if ALWAYS_INLINE_PLOTS[] == false && m isa MIME_TO_TRICK_VSCODE + # We use this mime to display the figure in a window here. + # See declaration of MIME_TO_TRICK_VSCODE for more info display(figlike) - throw(MethodError(show, io, m, figlike)) + return () # this is a diagnostic vscode mime, so we can just return nothing end scene = get_scene(figlike) backend = current_backend() @@ -357,6 +409,7 @@ Returns the content of the given scene or screen rasterised to a Matrix of Colors. The return type is backend-dependent, but will be some form of RGB or RGBA. +- `backend::Module`: A module which is a Makie backend. For example, `backend = GLMakie`, `backend = CairoMakie`, etc. - `format = JuliaNative` : Returns a buffer in the format of standard julia images (dims permuted and one reversed) - `format = GLNative` : Returns a more efficient format buffer for GLMakie which can be directly used in FFMPEG without conversion diff --git a/src/ffmpeg-util.jl b/src/ffmpeg-util.jl index 570868ab044..32ea56ae096 100644 --- a/src/ffmpeg-util.jl +++ b/src/ffmpeg-util.jl @@ -24,11 +24,14 @@ you have issues playing a video, try `profile = "high"` or `profile = "main"`. - `pixel_format = "yuv420p"`: A ffmpeg compatible pixel format (`-pix_fmt`). Currently only applies to `mp4`. Defaults to `yuv444p` for `profile = high444`. +- `loop = 0`: Number of times the video is repeated, for a `gif`. Defaults to `0`, which +means infinite looping. A value of `-1` turns off looping, and a value of `n > 0` and above +means `n` repetitions (i.e. the video is played `n+1` times). !!! warning `profile` and `pixel_format` are only used when `format` is `"mp4"`; a warning will be issued if `format` is not `"mp4"` and those two arguments are not `nothing`. Similarly, `compression` is only - valid when `format` is `"mp4"` or `"webm"`. + valid when `format` is `"mp4"` or `"webm"`, and `loop` is only valid when `format` is `"gif"`. """ struct VideoStreamOptions format::String @@ -36,6 +39,7 @@ struct VideoStreamOptions compression::Union{Nothing,Int} profile::Union{Nothing,String} pixel_format::Union{Nothing,String} + loop::Union{Nothing,Int} loglevel::String input::String @@ -43,7 +47,7 @@ struct VideoStreamOptions function VideoStreamOptions( format::AbstractString, framerate::Real, compression, profile, - pixel_format, loglevel::String, input::String, rawvideo::Bool=true) + pixel_format, loop, loglevel::String, input::String, rawvideo::Bool=true) if !isa(framerate, Integer) @warn "The given framefrate is not a subtype of `Integer`, and will be rounded to the nearest integer. To supress this warning, provide an integer as the framerate." @@ -58,11 +62,16 @@ struct VideoStreamOptions if format in ("mp4", "webm") (compression === nothing) && (compression = 20) end + + if format == "gif" + (loop === nothing) && (loop = 0) + end # items are name, value, allowed_formats allowed_kwargs = [("compression", compression, ("mp4", "webm")), ("profile", profile, ("mp4",)), - ("pixel_format", pixel_format, ("mp4",))] + ("pixel_format", pixel_format, ("mp4",)), + ("loop", loop, ("gif",))] for (name, value, allowed_formats) in allowed_kwargs if !(format in allowed_formats) && value !== nothing @@ -92,12 +101,12 @@ struct VideoStreamOptions if !(loglevel in loglevels) error("loglevel needs to be one of $(loglevels)") end - return new(format, framerate, compression, profile, pixel_format, loglevel, input, rawvideo) + return new(format, framerate, compression, profile, pixel_format, loop, loglevel, input, rawvideo) end end -function VideoStreamOptions(; format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loglevel="quiet", input="pipe:0", rawvideo=true) - return VideoStreamOptions(format, framerate, compression, profile, pixel_format, loglevel, input, rawvideo) +function VideoStreamOptions(; format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loop=nothing, loglevel="quiet", input="pipe:0", rawvideo=true) + return VideoStreamOptions(format, framerate, compression, profile, pixel_format, loop, loglevel, input, rawvideo) end function to_ffmpeg_cmd(vso::VideoStreamOptions, xdim::Integer=0, ydim::Integer=0) @@ -117,7 +126,8 @@ function to_ffmpeg_cmd(vso::VideoStreamOptions, xdim::Integer=0, ydim::Integer=0 # -pix_fmt: (mp4 only) the output pixel format # -profile:v: (mp4 only) the output video profile # -an: no audio in output - (format, framerate, compression, profile, pixel_format) = (vso.format, vso.framerate, vso.compression, vso.profile, vso.pixel_format) + # -loop: (gif only) number of times to loop + (format, framerate, compression, profile, pixel_format, loop) = (vso.format, vso.framerate, vso.compression, vso.profile, vso.pixel_format, vso.loop) cpu_cores = length(Sys.cpu_info()) ffmpeg_prefix = ` @@ -160,9 +170,9 @@ function to_ffmpeg_cmd(vso::VideoStreamOptions, xdim::Integer=0, ydim::Integer=0 # from https://superuser.com/a/556031 # avoids creating a PNG file of the palette if vso.rawvideo - `-vf "vflip,fps=$(framerate),scale=$(xdim):-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse"` + `-vf "vflip,fps=$(framerate),scale=$(xdim):-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop $(loop)` else - `-vf "fps=$(framerate),scale=$(xdim):-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse"` + `-vf "fps=$(framerate),scale=$(xdim):-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop $(loop)` end else error("Video type $(format) not known") @@ -183,8 +193,8 @@ end """ VideoStream(fig::FigureLike; - format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loglevel="quiet", - visible=false, connect=false, backend=current_backend(), + format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loop=nothing, + loglevel="quiet", visible=false, connect=false, backend=current_backend(), screen_config...) Returns a `VideoStream` which can pipe new frames into the ffmpeg process with few allocations via [`recordframe!(stream)`](@ref). @@ -204,8 +214,8 @@ $(Base.doc(VideoStreamOptions)) * `screen_config...`: See `?Backend.Screen` or `Base.doc(Backend.Screen)` for applicable options that can be passed and forwarded to the backend. """ function VideoStream(fig::FigureLike; - format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loglevel="quiet", - visible=false, connect=false, backend=current_backend(), + format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loop=nothing, + loglevel="quiet", visible=false, connect=false, backend=current_backend(), screen_config...) dir = mktempdir() @@ -217,7 +227,7 @@ function VideoStream(fig::FigureLike; xdim = iseven(_xdim) ? _xdim : _xdim + 1 ydim = iseven(_ydim) ? _ydim : _ydim + 1 buffer = Matrix{RGB{N0f8}}(undef, xdim, ydim) - vso = VideoStreamOptions(format, framerate, compression, profile, pixel_format, loglevel, "pipe:0", true) + vso = VideoStreamOptions(format, framerate, compression, profile, pixel_format, loop, loglevel, "pipe:0", true) cmd = to_ffmpeg_cmd(vso, xdim, ydim) process = @ffmpeg_env open(`$cmd $path`, "w") return VideoStream(process.in, process, screen, buffer, abspath(path), vso) diff --git a/src/figureplotting.jl b/src/figureplotting.jl index 4e2a1eaedc0..c11b3502a37 100644 --- a/src/figureplotting.jl +++ b/src/figureplotting.jl @@ -62,7 +62,9 @@ end # without scenelike, use current axis of current figure function plot!(P::PlotFunc, args...; kw_attributes...) - ax = current_axis(current_figure()) + figure = current_figure() + isnothing(figure) && error("There is no current figure to plot into.") + ax = current_axis(figure) isnothing(ax) && error("There is no current axis to plot into.") plot!(P, ax, args...; kw_attributes...) end diff --git a/src/figures.jl b/src/figures.jl index 4c0e8cf78d1..b732d08e427 100644 --- a/src/figures.jl +++ b/src/figures.jl @@ -27,11 +27,15 @@ if an axis is placed at that position (if not it errors) or it can reference an get_scene(fig::Figure) = fig.scene get_scene(fap::FigureAxisPlot) = fap.figure.scene -const _current_figure = Ref{Union{Nothing, Figure}}(nothing) +const CURRENT_FIGURE = Ref{Union{Nothing, Figure}}(nothing) +Base.@deprecate_binding _current_figure CURRENT_FIGURE + +const CURRENT_FIGURE_LOCK = Base.ReentrantLock() + "Returns the current active figure (or the last figure that got created)" -current_figure() = _current_figure[] +current_figure() = lock(()-> CURRENT_FIGURE[], CURRENT_FIGURE_LOCK) "Set `fig` as the current active scene" -current_figure!(fig) = (_current_figure[] = fig) +current_figure!(fig) = lock(() -> (CURRENT_FIGURE[] = fig), CURRENT_FIGURE_LOCK) "Returns the current active axis (or the last axis that got created)" current_axis() = current_axis(current_figure()) @@ -44,9 +48,11 @@ function current_axis!(fig::Figure, ax) fig.current_axis[] = ax ax end + function current_axis!(fig::Figure, ::Nothing) fig.current_axis[] = nothing end + function current_axis!(ax) fig = ax.parent if !(fig isa Figure) diff --git a/src/makielayout/blocks.jl b/src/makielayout/blocks.jl index 622980d38cc..855c28627ae 100644 --- a/src/makielayout/blocks.jl +++ b/src/makielayout/blocks.jl @@ -54,9 +54,6 @@ macro Block(name::Symbol, body::Expr = Expr(:block)) push!(fields_vector, constructor) q = quote - """ - For information about attributes, use `attribute_help($($name))`. - """ $structdef export $name @@ -67,7 +64,7 @@ macro Block(name::Symbol, body::Expr = Expr(:block)) function Makie.default_attribute_values(::Type{$(name)}, scene::Union{Scene, Nothing}) sceneattrs = scene === nothing ? Attributes() : theme(scene) - curdeftheme = deepcopy($(Makie).CURRENT_DEFAULT_THEME) + curdeftheme = fast_deepcopy($(Makie).CURRENT_DEFAULT_THEME) $(make_attr_dict_expr(attrs, :sceneattrs, :curdeftheme)) end @@ -97,7 +94,7 @@ macro Block(name::Symbol, body::Expr = Expr(:block)) esc(q) end -_defaultstring(x) = string(x) +_defaultstring(x) = string(MacroTools.striplines(x)) _defaultstring(x::String) = repr(x) function make_attr_dict_expr(::Nothing, sceneattrsym, curthemesym) @@ -118,11 +115,13 @@ function Docs.getdoc(@nospecialize T::Type{<:Block}) """) else s = """ - # `$T <: Block` + **`$T <: Block`** $(block_docs(T)) - ## Attributes + **Attributes** + + (type `?$T.x` in the REPL for more information about attribute `x`) $(_attribute_list(T)) """ @@ -131,20 +130,8 @@ function Docs.getdoc(@nospecialize T::Type{<:Block}) end function _attribute_list(T) - ks = sort(collect(keys(default_attribute_values(T, nothing)))) - default_exprs = attribute_default_expressions(T) - layout_attrs = Set([:tellheight, :tellwidth, :height, :width, - :valign, :halign, :alignmode]) - """ - - **$T attributes**: - - $(join([" - `$k`: $(_attribute_docs(T)[k]) Default: `$(default_exprs[k])`" for k in ks if k ∉ layout_attrs], "\n")) - - **Layout attributes**: - - $(join([" - `$k`: $(_attribute_docs(T)[k]) Default: `$(default_exprs[k])`" for k in ks if k in layout_attrs], "\n")) - """ + ks = sort(collect(keys(_attribute_docs(T)))) + join(("`$k`" for k in ks), ", ") end function make_attr_dict_expr(attrs, sceneattrsym, curthemesym) @@ -165,9 +152,9 @@ function make_attr_dict_expr(attrs, sceneattrsym, curthemesym) # then default value d = quote if haskey($sceneattrsym, $key) - $sceneattrsym[$key][] # only use value of theme entry + to_value($sceneattrsym[$key]) # only use value of theme entry elseif haskey($curthemesym, $key) - $curthemesym[$key][] # only use value of theme entry + to_value($curthemesym[$key]) # only use value of theme entry else $default end @@ -180,16 +167,6 @@ function make_attr_dict_expr(attrs, sceneattrsym, curthemesym) :(Dict($(pairs...))) end -function attribute_help(T) - println("Available attributes for $T (use attribute_help($T, key) for more information):") - foreach(sort(collect(keys(_attribute_docs(T))))) do key - println(key) - end -end - -function attribute_help(T, key) - println(_attribute_docs(T)[key]) -end function extract_attributes!(body) i = findfirst( @@ -556,3 +533,62 @@ convert_for_attribute(t::Any, x) = x convert_for_attribute(t::Type{Float64}, x) = convert(Float64, x) convert_for_attribute(t::Type{RGBAf}, x) = to_color(x)::RGBAf convert_for_attribute(t::Type{Makie.FreeTypeAbstraction.FTFont}, x) = to_font(x) + +Base.@kwdef struct Example + name::String + backend::Symbol = :CairoMakie # the backend that is used for rendering + backend_using::Symbol = backend # the backend that is shown for `using` (for CairoMakie-rendered plots of interactive stuff that should show `using GLMakie`) + svg::Bool = true # only for CairoMakie + code::String +end + +function repl_docstring(type::Symbol, attr::Symbol, docs::Union{Nothing,String}, examples::Vector{Example}, default_str) + io = IOBuffer() + + println(io, "Default value: `$default_str`") + println(io) + + if docs === nothing + println(io, "No docstring defined for `$attr`.") + else + println(io, docs) + end + println(io) + + for (i, example) in enumerate(examples) + println(io, "**Example $i**: $(example.name)") + println(io, "```julia") + # println(io) + # println(io, "# run in the REPL via Makie.example($type, :$attr, $i)") + # println(io) + println(io, example.code) + println(io, "```") + println(io) + end + + Markdown.parse(String(take!(io))) +end + +# function example(type::Type{<:Block}, attr::Symbol, i::Int) +# examples = get(attribute_examples(type), attr, Example[]) +# if !(1 <= i <= length(examples)) +# error("Invalid example number for attribute $attr of type $type.") +# end +# display(eval(Meta.parseall(examples[i].code))) +# return +# end + +function attribute_examples(b::Type{<:Block}) + Dict{Symbol,Vector{Example}}() +end + +# overrides `?Axis.xticks` and similar lookups in the REPL +function REPL.fielddoc(t::Type{<:Block}, s::Symbol) + if !is_attribute(t, s) + return Markdown.parse("`$s` is not an attribute of type `$t`. Type `?$t` in the REPL to see the list of available attributes.") + end + docs = get(_attribute_docs(t), s, nothing) + examples = get(attribute_examples(t), s, Example[]) + default_str = Makie.attribute_default_expressions(t)[s] + return repl_docstring(nameof(t), s, docs, examples, default_str) +end diff --git a/src/makielayout/blocks/axis.jl b/src/makielayout/blocks/axis.jl index 3e5b098b1fe..56d6c66b658 100644 --- a/src/makielayout/blocks/axis.jl +++ b/src/makielayout/blocks/axis.jl @@ -2,17 +2,11 @@ function block_docs(::Type{Axis}) """ A 2D axis which can be plotted into. - ## Constructors + **Constructors** ```julia Axis(fig_or_scene; palette = nothing, kwargs...) ``` - - ## Examples - - ```julia - ax = Axis(fig[1, 1]) - ``` """ end @@ -171,7 +165,7 @@ function initialize_block!(ax::Axis; palette = nothing) ax.elements = elements if palette === nothing - palette = haskey(blockscene.theme, :palette) ? deepcopy(blockscene.theme[:palette]) : copy(Makie.default_palettes) + palette = fast_deepcopy(haskey(blockscene.theme, :palette) ? blockscene.theme[:palette] : Makie.DEFAULT_PALETTES) end ax.palette = palette isa Attributes ? palette : Attributes(palette) @@ -1376,3 +1370,418 @@ function update_state_before_display!(ax::Axis) reset_limits!(ax) return end + +function attribute_examples(::Type{Axis}) + Dict( + :xticks => [ + Example( + name = "Common tick types", + code = """ + fig = Figure() + Axis(fig[1, 1], xticks = 1:10) + Axis(fig[2, 1], xticks = (1:2:9, ["A", "B", "C", "D", "E"])) + Axis(fig[3, 1], xticks = WilkinsonTicks(5)) + fig + """ + ) + ], + :yticks => [ + Example( + name = "Common tick types", + code = """ + fig = Figure() + Axis(fig[1, 1], yticks = 1:10) + Axis(fig[1, 2], yticks = (1:2:9, ["A", "B", "C", "D", "E"])) + Axis(fig[1, 3], yticks = WilkinsonTicks(5)) + fig + """ + ) + ], + :aspect => [ + Example( + name = "Common aspect ratios", + code = """ + using FileIO + + f = Figure() + + ax1 = Axis(f[1, 1], aspect = nothing, title = "nothing") + ax2 = Axis(f[1, 2], aspect = DataAspect(), title = "DataAspect()") + ax3 = Axis(f[2, 1], aspect = AxisAspect(1), title = "AxisAspect(1)") + ax4 = Axis(f[2, 2], aspect = AxisAspect(2), title = "AxisAspect(2)") + + img = rotr90(load(assetpath("cow.png"))) + for ax in [ax1, ax2, ax3, ax4] + image!(ax, img) + end + + f + """ + ) + ], + :autolimitaspect => [ + Example( + name = "Using `autolimitaspect`", + code = """ + f = Figure() + + ax1 = Axis(f[1, 1], autolimitaspect = nothing) + ax2 = Axis(f[1, 2], autolimitaspect = 1) + + for ax in [ax1, ax2] + lines!(ax, 0..10, sin) + end + + f + """ + ) + ], + :title => [ + Example( + name = "`title` variants", + code = """ + f = Figure() + + Axis(f[1, 1], title = "Title") + Axis(f[2, 1], title = L"\\sum_i{x_i \\times y_i}") + Axis(f[3, 1], title = rich( + "Rich text title", + subscript(" with subscript", color = :slategray) + )) + + f + """ + ) + ], + :titlealign => [ + Example( + name = "`titlealign` variants", + code = """ + f = Figure() + + Axis(f[1, 1], titlealign = :left, title = "Left aligned title") + Axis(f[2, 1], titlealign = :center, title = "Center aligned title") + Axis(f[3, 1], titlealign = :right, title = "Right aligned title") + + f + """ + ) + ], + :subtitle => [ + Example( + name = "`subtitle` variants", + code = """ + f = Figure() + + Axis(f[1, 1], title = "Title", subtitle = "Subtitle") + Axis(f[2, 1], title = "Title", subtitle = L"\\sum_i{x_i \\times y_i}") + Axis(f[3, 1], title = "Title", subtitle = rich( + "Rich text subtitle", + subscript(" with subscript", color = :slategray) + )) + + f + """ + ) + ], + :xlabel => [ + Example( + name = "`xlabel` variants", + code = """ + f = Figure() + + Axis(f[1, 1], xlabel = "X Label") + Axis(f[2, 1], xlabel = L"\\sum_i{x_i \\times y_i}") + Axis(f[3, 1], xlabel = rich( + "X Label", + subscript(" with subscript", color = :slategray) + )) + + f + """ + ) + ], + :ylabel => [ + Example( + name = "`ylabel` variants", + code = """ + f = Figure() + + Axis(f[1, 1], ylabel = "Y Label") + Axis(f[2, 1], ylabel = L"\\sum_i{x_i \\times y_i}") + Axis(f[3, 1], ylabel = rich( + "Y Label", + subscript(" with subscript", color = :slategray) + )) + + f + """ + ) + ], + :xtrimspine => [ + Example( + name = "`xtrimspine` variants", + code = """ + f = Figure() + + ax1 = Axis(f[1, 1], xtrimspine = false) + ax2 = Axis(f[2, 1], xtrimspine = true) + ax3 = Axis(f[3, 1], xtrimspine = (true, false)) + ax4 = Axis(f[4, 1], xtrimspine = (false, true)) + + for ax in [ax1, ax2, ax3, ax4] + ax.xgridvisible = false + ax.ygridvisible = false + ax.rightspinevisible = false + ax.topspinevisible = false + xlims!(ax, 0.5, 5.5) + end + + f + """ + ) + ], + :ytrimspine => [ + Example( + name = "`ytrimspine` variants", + code = """ + f = Figure() + + ax1 = Axis(f[1, 1], ytrimspine = false) + ax2 = Axis(f[1, 2], ytrimspine = true) + ax3 = Axis(f[1, 3], ytrimspine = (true, false)) + ax4 = Axis(f[1, 4], ytrimspine = (false, true)) + + for ax in [ax1, ax2, ax3, ax4] + ax.xgridvisible = false + ax.ygridvisible = false + ax.rightspinevisible = false + ax.topspinevisible = false + ylims!(ax, 0.5, 5.5) + end + + f + """ + ) + ], + :xaxisposition => [ + Example( + name = "`xaxisposition` variants", + code = """ + f = Figure() + + Axis(f[1, 1], xaxisposition = :bottom) + Axis(f[1, 2], xaxisposition = :top) + + f + """ + ) + ], + :yaxisposition => [ + Example( + name = "`yaxisposition` variants", + code = """ + f = Figure() + + Axis(f[1, 1], yaxisposition = :left) + Axis(f[2, 1], yaxisposition = :right) + + f + """ + ) + ], + :limits => [ + Example( + name = "`limits` variants", + code = """ + f = Figure() + + ax1 = Axis(f[1, 1], limits = (nothing, nothing), title = "(nothing, nothing)") + ax2 = Axis(f[1, 2], limits = (0, 4pi, -1, 1), title = "(0, 4pi, -1, 1)") + ax3 = Axis(f[2, 1], limits = ((0, 4pi), nothing), title = "((0, 4pi), nothing)") + ax4 = Axis(f[2, 2], limits = (nothing, 4pi, nothing, 1), title = "(nothing, 4pi, nothing, 1)") + + for ax in [ax1, ax2, ax3, ax4] + lines!(ax, 0..4pi, sin) + end + + f + """ + ) + ], + :yscale => [ + Example( + name = "`yscale` variants", + code = """ + f = Figure() + + for (i, scale) in enumerate([identity, log10, log2, log, sqrt, Makie.logit]) + row, col = fldmod1(i, 3) + Axis(f[row, col], yscale = scale, title = string(scale), + yminorticksvisible = true, yminorgridvisible = true, + yminorticks = IntervalsBetween(5)) + + lines!(range(0.01, 0.99, length = 200)) + end + + f + """ + ), + Example( + name = "Pseudo-log scales", + code = """ + f = Figure() + + ax1 = Axis(f[1, 1], + yscale = Makie.pseudolog10, + title = "Pseudolog scale", + yticks = [-100, -10, -1, 0, 1, 10, 100] + ) + + ax2 = Axis(f[2, 1], + yscale = Makie.Symlog10(10.0), + title = "Symlog10 with linear scaling between -10 and 10", + yticks = [-100, -10, 0, 10, 100] + ) + + for ax in [ax1, ax2] + lines!(ax, -100:0.1:100) + end + + f + """ + ), + ], + :xscale => [ + Example( + name = "`xscale` variants", + code = """ + f = Figure() + + for (i, scale) in enumerate([identity, log10, log2, log, sqrt, Makie.logit]) + row, col = fldmod1(i, 2) + Axis(f[row, col], xscale = scale, title = string(scale), + xminorticksvisible = true, xminorgridvisible = true, + xminorticks = IntervalsBetween(5)) + + lines!(range(0.01, 0.99, length = 200), 1:200) + end + + f + """ + ), + Example( + name = "Pseudo-log scales", + code = """ + f = Figure() + + ax1 = Axis(f[1, 1], + xscale = Makie.pseudolog10, + title = "Pseudolog scale", + xticks = [-100, -10, -1, 0, 1, 10, 100] + ) + + ax2 = Axis(f[1, 2], + xscale = Makie.Symlog10(10.0), + title = "Symlog10 with linear scaling\nbetween -10 and 10", + xticks = [-100, -10, 0, 10, 100] + ) + + for ax in [ax1, ax2] + lines!(ax, -100:0.1:100, -100:0.1:100) + end + + f + """ + ), + ], + :xtickformat => [ + Example( + name = "`xtickformat` variants", + code = """ + f = Figure(figure_padding = 50) + + Axis(f[1, 1], xtickformat = values -> ["\$(value)kg" for value in values]) + Axis(f[2, 1], xtickformat = "{:.2f}ms") + Axis(f[3, 1], xtickformat = values -> [L"\\sqrt{%\$(value^2)}" for value in values]) + Axis(f[4, 1], xtickformat = values -> [rich("\$value", superscript("XY", color = :red)) + for value in values]) + + f + """ + ) + ], + :ytickformat => [ + Example( + name = "`ytickformat` variants", + code = """ + f = Figure() + + Axis(f[1, 1], ytickformat = values -> ["\$(value)kg" for value in values]) + Axis(f[1, 2], ytickformat = "{:.2f}ms") + Axis(f[1, 3], ytickformat = values -> [L"\\sqrt{%\$(value^2)}" for value in values]) + Axis(f[1, 4], ytickformat = values -> [rich("\$value", superscript("XY", color = :red)) + for value in values]) + + f + """ + ) + ], + :xticksmirrored => [ + Example( + name = "`xticksmirrored` on and off", + code = """ + f = Figure() + + Axis(f[1, 1], xticksmirrored = false, xminorticksvisible = true) + Axis(f[1, 2], xticksmirrored = true, xminorticksvisible = true) + + f + """ + ) + ], + :yticksmirrored => [ + Example( + name = "`yticksmirrored` on and off", + code = """ + f = Figure() + + Axis(f[1, 1], yticksmirrored = false, yminorticksvisible = true) + Axis(f[2, 1], yticksmirrored = true, yminorticksvisible = true) + + f + """ + ) + ], + :xminorticks => [ + Example( + name = "`xminorticks` variants", + code = """ + f = Figure() + + kwargs = (; xminorticksvisible = true, xminorgridvisible = true) + Axis(f[1, 1]; xminorticks = IntervalsBetween(2), kwargs...) + Axis(f[2, 1]; xminorticks = IntervalsBetween(5), kwargs...) + Axis(f[3, 1]; xminorticks = [1, 2, 3, 4], kwargs...) + + f + """ + ) + ], + :yminorticks => [ + Example( + name = "`yminorticks` variants", + code = """ + f = Figure() + + kwargs = (; yminorticksvisible = true, yminorgridvisible = true) + Axis(f[1, 1]; yminorticks = IntervalsBetween(2), kwargs...) + Axis(f[1, 2]; yminorticks = IntervalsBetween(5), kwargs...) + Axis(f[1, 3]; yminorticks = [1, 2, 3, 4], kwargs...) + + f + """ + ) + ], + ) +end diff --git a/src/makielayout/blocks/axis3d.jl b/src/makielayout/blocks/axis3d.jl index 53e06c5fad1..16ab7ffaa9f 100644 --- a/src/makielayout/blocks/axis3d.jl +++ b/src/makielayout/blocks/axis3d.jl @@ -110,7 +110,7 @@ function initialize_block!(ax::Axis3) inspectable = false) ax.cycler = Cycler() - ax.palette = copy(Makie.default_palettes) + ax.palette = Makie.DEFAULT_PALETTES ax.mouseeventhandle = addmouseevents!(scene) scrollevents = Observable(ScrollEvent(0, 0)) @@ -923,3 +923,112 @@ function limits!(ax::Axis3, rect::Rect3) Makie.ylims!(ax, ymin, ymax) Makie.zlims!(ax, zmin, zmax) end + +function attribute_examples(::Type{Axis3}) + Dict( + :aspect => [ + Example( + name = "Three-tuple aspects", + code = """ + fig = Figure() + + Axis3(fig[1, 1], aspect = (1, 1, 1), title = "aspect = (1, 1, 1)") + Axis3(fig[1, 2], aspect = (2, 1, 1), title = "aspect = (2, 1, 1)") + Axis3(fig[2, 1], aspect = (1, 2, 1), title = "aspect = (1, 2, 1)") + Axis3(fig[2, 2], aspect = (1, 1, 2), title = "aspect = (1, 1, 2)") + + fig + """ + ), + Example( + name = "`:data` and `:equal` aspects", + code = """ + using FileIO + + fig = Figure() + + brain = load(assetpath("brain.stl")) + + ax1 = Axis3(fig[1, 1], aspect = :equal, title = "aspect = :equal") + ax2 = Axis3(fig[1, 2], aspect = :data, title = "aspect = :data") + + for ax in [ax1, ax2] + mesh!(ax, brain, color = :gray80) + end + + fig + """ + ), + ], + :viewmode => [ + Example( + name = "`viewmode` variants", + code = """ + fig = Figure() + + for (i, viewmode) in enumerate([:fit, :fitzoom, :stretch]) + for (j, elevation) in enumerate([0.1, 0.2, 0.3] .* pi) + + Label(fig[i, 1:3, Top()], "viewmode = \$(repr(viewmode))", font = :bold) + + # show the extent of each cell using a box + Box(fig[i, j], strokewidth = 0, color = :gray95) + + ax = Axis3(fig[i, j]; viewmode, elevation, protrusions = 0, aspect = :equal) + hidedecorations!(ax) + + end + end + + fig + """ + ), + ], + :perspectiveness => [ + Example( + name = "`perspectiveness` values", + code = """ + fig = Figure() + + for (i, perspectiveness) in enumerate(range(0, 1, length = 6)) + ax = Axis3(fig[fldmod1(i, 3)...]; perspectiveness, protrusions = (0, 0, 0, 15), + title = ":perspectiveness = \$(perspectiveness)") + hidedecorations!(ax) + end + + fig + """ + ), + ], + :azimuth => [ + Example( + name = "`azimuth` values", + code = """ + fig = Figure() + + for (i, azimuth) in enumerate([0, 0.1, 0.2, 0.3, 0.4, 0.5]) + Axis3(fig[fldmod1(i, 3)...], azimuth = azimuth * pi, + title = "azimuth = \$(azimuth)π", viewmode = :fit) + end + + fig + """ + ), + ], + :elevation => [ + Example( + name = "`elevation` values", + code = """ + fig = Figure() + + for (i, elevation) in enumerate([0, 0.05, 0.1, 0.15, 0.2, 0.25]) + Axis3(fig[fldmod1(i, 3)...], elevation = elevation * pi, + title = "elevation = \$(elevation)π", viewmode = :fit) + end + + fig + """ + ), + ], + ) +end diff --git a/src/makielayout/blocks/legend.jl b/src/makielayout/blocks/legend.jl index 22864932e6a..d711ce48338 100644 --- a/src/makielayout/blocks/legend.jl +++ b/src/makielayout/blocks/legend.jl @@ -1,5 +1,5 @@ function initialize_block!(leg::Legend, - entry_groups::Observable{Vector{Tuple{Optional{<:AbstractString}, Vector{LegendEntry}}}}) + entry_groups::Observable{Vector{Tuple{Any, Vector{LegendEntry}}}}) blockscene = leg.blockscene @@ -318,7 +318,7 @@ legendelements(le::LegendElement, legend) = LegendElement[le] legendelements(les::AbstractArray{<:LegendElement}, legend) = LegendElement[les...] -function LegendEntry(label::Optional{AbstractString}, contentelements::AbstractArray, legend; kwargs...) +function LegendEntry(label, contentelements::AbstractArray, legend; kwargs...) attrs = Attributes(label = label) kwargattrs = Attributes(kwargs) @@ -328,13 +328,16 @@ function LegendEntry(label::Optional{AbstractString}, contentelements::AbstractA LegendEntry(elems, attrs) end -function LegendEntry(label::Optional{AbstractString}, contentelement, legend; kwargs...) +function LegendEntry(label, contentelement, legend; kwargs...) attrs = Attributes(label = label) kwargattrs = Attributes(kwargs) merge!(attrs, kwargattrs) elems = legendelements(contentelement, legend) + if isempty(elems) + error("`legendelements` returned an empty list for content element of type $(typeof(contentelement)). That could mean that neither this object nor any possible child objects had a method for `legendelements` defined that returned a non-empty result.") + end LegendEntry(elems, attrs) end @@ -430,10 +433,13 @@ end # if there is no specific overload available, we go through the child plots and just stack # those together as a simple fallback function legendelements(plot, legend)::Vector{LegendElement} - if isempty(plot.plots) - error("No child plot elements found in plot of type $(typeof(plot)) but also no `legendelements` method defined.") - end - reduce(vcat, [legendelements(childplot, legend) for childplot in plot.plots]) + reduce(vcat, [legendelements(childplot, legend) for childplot in plot.plots], init = []) +end + +# Text has no meaningful legend, but it contains a linesegments for latex applications +# which can surface as a line in the final legend +function legendelements(plot::Text, legend)::Vector{LegendElement} + [] end function Base.getproperty(legendelement::T, s::Symbol) where T <: LegendElement @@ -462,8 +468,8 @@ end Legend( fig_or_scene, contents::AbstractArray, - labels::AbstractArray{<:AbstractString}, - title::Optional{<:AbstractString} = nothing; + labels::AbstractArray, + title = nothing; kwargs...) Create a legend from `contents` and `labels` where each label is associated to @@ -472,9 +478,9 @@ one content element. A content element can be an `AbstractPlot`, an array of `legendelements` method is defined. """ function Legend(fig_or_scene, - contents::AbstractArray, - labels::AbstractArray{<:Optional{AbstractString}}, - title::Optional{<:AbstractString} = nothing; + contents::AbstractVector, + labels::AbstractVector, + title = nothing; kwargs...) if length(contents) != length(labels) @@ -493,9 +499,9 @@ end """ Legend( fig_or_scene, - contentgroups::AbstractArray{<:AbstractArray}, - labelgroups::AbstractArray{<:AbstractArray}, - titles::AbstractArray{<:Optional{<:AbstractString}}; + contentgroups::AbstractVector{<:AbstractVector}, + labelgroups::AbstractVector{<:AbstractVector}, + titles::AbstractVector; kwargs...) Create a multi-group legend from `contentgroups`, `labelgroups` and `titles`. @@ -507,9 +513,9 @@ element can be an `AbstractPlot`, an array of `AbstractPlots`, a `LegendElement` or any other object for which the `legendelements` method is defined. """ function Legend(fig_or_scene, - contentgroups::AbstractArray{<:AbstractArray}, - labelgroups::AbstractArray{<:AbstractArray}, - titles::AbstractArray{<:Optional{<:AbstractString}}; + contentgroups::AbstractVector{<:AbstractVector}, + labelgroups::AbstractVector{<:AbstractVector}, + titles::AbstractVector; kwargs...) if !(length(titles) == length(contentgroups) == length(labelgroups)) diff --git a/src/makielayout/lineaxis.jl b/src/makielayout/lineaxis.jl index 6fe855dde16..5bbb30d29be 100644 --- a/src/makielayout/lineaxis.jl +++ b/src/makielayout/lineaxis.jl @@ -540,7 +540,7 @@ end """ get_ticks(ticks, scale, formatter, vmin, vmax) -Base function that calls `get_tickvalues(ticks, vmin, max)` and +Base function that calls `get_tickvalues(ticks, scale, vmin, max)` and `get_ticklabels(formatter, tickvalues)` and returns a tuple `(tickvalues, ticklabels)`. For custom ticks / formatter combinations, this method can be overloaded diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index d42aa1ff772..f0a22f98c5c 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -202,11 +202,20 @@ end yaxis::LineAxis elements::Dict{Symbol, Any} @attributes begin - "The xlabel string." + """ + The content of the x axis label. + The value can be any non-vector-valued object that the `text` primitive supports. + """ xlabel = "" - "The ylabel string." + """ + The content of the y axis label. + The value can be any non-vector-valued object that the `text` primitive supports. + """ ylabel = "" - "The axis title string." + """ + The content of the axis title. + The value can be any non-vector-valued object that the `text` primitive supports. + """ title = "" "The font family of the title." titlefont = :bold @@ -216,13 +225,21 @@ end titlegap::Float64 = 4f0 "Controls if the title is visible." titlevisible::Bool = true - "The horizontal alignment of the title." + """ + The horizontal alignment of the title. + The subtitle always follows this alignment setting. + + Options are `:center`, `:left` or `:right`. + """ titlealign::Symbol = :center "The color of the title" titlecolor::RGBAf = @inherit(:textcolor, :black) "The axis title line height multiplier." titlelineheight::Float64 = 1 - "The axis subtitle string." + """ + The content of the axis subtitle. + The value can be any non-vector-valued object that the `text` primitive supports. + """ subtitle = "" "The font family of the subtitle." subtitlefont = :regular @@ -378,7 +395,24 @@ end topspinecolor::RGBAf = :black "The color of the right axis spine." rightspinecolor::RGBAf = :black - "The forced aspect ratio of the axis. `nothing` leaves the axis unconstrained, `DataAspect()` forces the same ratio as the ratio in data limits between x and y axis, `AxisAspect(ratio)` sets a manual ratio." + """ + Controls the forced aspect ratio of the axis. + + The default `nothing` will not constrain the aspect ratio. + The axis area will span the available width and height in the layout. + + `DataAspect()` reduces the effective axis size within the available layout space + so that the axis aspect ratio width/height matches that of the data limits. + For example, if the x limits range from 0 to 300 and the y limits from 100 to 250, `DataAspect()` will result + in an aspect ratio of `(300 - 0) / (250 - 100) = 2`. + This can be useful when plotting images, because the image will be displayed unsquished. + + `AxisAspect(ratio)` reduces the effective axis size within the available layout space + so that the axis aspect ratio width/height matches `ratio`. + + Note that both `DataAspect` and `AxisAspect` can result in excess whitespace around the axis. + To make a `GridLayout` aware of aspect ratio constraints, refer to the `Aspect` column or row size setting. + """ aspect = nothing "The vertical alignment of the axis within its suggested bounding box." valign = :center @@ -396,13 +430,73 @@ end xautolimitmargin::Tuple{Float64, Float64} = (0.05f0, 0.05f0) "The relative margins added to the autolimits in y direction." yautolimitmargin::Tuple{Float64, Float64} = (0.05f0, 0.05f0) - "The xticks." + """ + Controls what numerical tick values are calculated for the x axis. + + To determine tick values and labels, Makie first calls `Makie.get_ticks(xticks, xscale, xtickformat, xmin, xmax)`. + If there is no special method defined for the current combination of + ticks, scale and formatter which returns both tick values and labels at once, + then the numerical tick values will be determined using + `xtickvalues = Makie.get_tickvalues(xticks, xscale, xmin, xmax)` after which the labels are determined using + `Makie.get_ticklabels(xtickformat, xtickvalues)`. + + Common objects that can be used as ticks are: + - A vector of numbers + - A tuple with two vectors `(numbers, labels)` where `labels` can be any objects that `text` can handle. + - `WilkinsonTicks`, the default tick finder for linear ticks + - `LinearTicks`, an alternative tick finder for linear ticks + - `LogTicks`, a wrapper that applies any other wrapped tick finder on log-transformed values + - `MultiplesTicks`, for finding ticks at multiples of a given value, such as `π` + """ xticks = Makie.automatic - "Format for xticks." + """ + The formatter for the ticks on the x axis. + + Usually, the tick values are determined first using `Makie.get_tickvalues`, after which + `Makie.get_ticklabels(xtickformat, xtickvalues)` is called. If there is a special method defined, + tick values and labels can be determined together using `Makie.get_ticks` instead. Check the + docstring for `xticks` for more information. + + Common objects that can be used for tick formatting are: + - A `Function` that takes a vector of numbers and returns a vector of labels. A label can be anything + that can be plotted by the `text` primitive. + - A `String` which is used as a format specifier for `Formatting.jl`. For example, `"{:.2f}kg"` + formats numbers rounded to 2 decimal digits and with the suffix `kg`. + """ xtickformat = Makie.automatic - "The yticks." + """ + Controls what numerical tick values are calculated for the y axis. + + To determine tick values and labels, Makie first calls `Makie.get_ticks(yticks, yscale, ytickformat, ymin, ymax)`. + If there is no special method defined for the current combination of + ticks, scale and formatter which returns both tick values and labels at once, + then the numerical tick values will be determined using + `ytickvalues = Makie.get_tickvalues(yticks, yscale, ymin, ymax)` after which the labels are determined using + `Makie.get_ticklabels(ytickformat, ytickvalues)`. + + Common objects that can be used as ticks are: + - A vector of numbers + - A tuple with two vectors `(numbers, labels)` where `labels` can be any objects that `text` can handle. + - `WilkinsonTicks`, the default tick finder for linear ticks + - `LinearTicks`, an alternative tick finder for linear ticks + - `LogTicks`, a wrapper that applies any other wrapped tick finder on log-transformed values + - `MultiplesTicks`, for finding ticks at multiples of a given value, such as `π` + """ yticks = Makie.automatic - "Format for yticks." + """ + The formatter for the ticks on the y axis. + + Usually, the tick values are determined first using `Makie.get_tickvalues`, after which + `Makie.get_ticklabels(ytickformat, ytickvalues)` is called. If there is a special method defined, + tick values and labels can be determined together using `Makie.get_ticks` instead. Check the + docstring for `yticks` for more information. + + Common objects that can be used for tick formatting are: + - A `Function` that takes a vector of numbers and returns a vector of labels. A label can be anything + that can be plotted by the `text` primitive. + - A `String` which is used as a format specifier for `Formatting.jl`. For example, `"{:.2f}kg"` + formats numbers rounded to 2 decimal digits and with the suffix `kg`. + """ ytickformat = Makie.automatic "The button for panning." panbutton::Makie.Mouse.Button = Makie.Mouse.right @@ -418,17 +512,51 @@ end xaxisposition::Symbol = :bottom "The position of the y axis (`:left` or `:right`)." yaxisposition::Symbol = :left - "Controls if the x spine is limited to the furthest tick marks or not." + """ + If `true`, limits the x axis spine's extent to the outermost major tick marks. + Can also be set to a `Tuple{Bool,Bool}` to control each side separately. + """ xtrimspine::Union{Bool, Tuple{Bool,Bool}} = false - "Controls if the y spine is limited to the furthest tick marks or not." + """ + If `true`, limits the y axis spine's extent to the outermost major tick marks. + Can also be set to a `Tuple{Bool,Bool}` to control each side separately. + """ ytrimspine::Union{Bool, Tuple{Bool,Bool}} = false "The background color of the axis." backgroundcolor::RGBAf = :white "Controls if the ylabel's rotation is flipped." flip_ylabel::Bool = false - "Constrains the data aspect ratio (`nothing` leaves the ratio unconstrained)." + """ + If `autolimitaspect` is set to a number, the limits of the axis + will autoadjust such that the ratio of the limits to the axis size equals + that number. + + For example, if the axis size is 100 x 200, then with `autolimitaspect = 1`, + the autolimits will also have a ratio of 1 to 2. The setting `autolimitaspect = 1` + is the complement to `aspect = AxisAspect(1)`, but while `aspect` changes the axis + size, `autolimitaspect` changes the limits to achieve the desired ratio. + """ autolimitaspect = nothing - "The limits that the user has manually set. They are reinstated when calling `reset_limits!` and are set to nothing by `autolimits!`. Can be either a tuple (xlow, xhigh, ylow, high) or a tuple (nothing_or_xlims, nothing_or_ylims). Are set by `xlims!`, `ylims!` and `limits!`." + """ + Can be used to manually specify which axis limits are desired. + + The `limits` attribute cannot be used to read out the actual limits of the axis. + The value of `limits` does not change when interactively zooming and panning and the axis can be reset + accordingly using the function `reset_limits!`. + + The function `autolimits!` resets the value of `limits` to `(nothing, nothing)` and adjusts the axis limits according + to the extents of the plots added to the axis. + + The value of `limits` can be a four-element tuple `(xlow, xhigh, ylow, high)` where each value + can be a real number or `nothing`. + It can also be a tuple `(x, y)` where `x` and `y` can be `nothing` or a tuple `(low, high)`. + In all cases, `nothing` means that the respective limit values will be automatically determined. + + Automatically determined limits are also influenced by `xautolimitmargin` and `yautolimitmargin`. + + The convenience functions `xlims!` and `ylims!` allow to set only the x or y part of `limits`. + The function `limits!` is another option to set both x and y simultaneously. + """ limits = (nothing, nothing) "The align mode of the axis in its parent GridLayout." alignmode = Inside() @@ -446,7 +574,14 @@ end xminortickwidth::Float64 = 1f0 "The tick color of x minor ticks" xminortickcolor::RGBAf = :black - "The tick locator for the x minor ticks" + """ + The tick locator for the minor ticks of the x axis. + + Common objects that can be used are: + - `IntervalsBetween`, divides the space between two adjacent major ticks into `n` intervals + for `n-1` minor ticks + - A vector of numbers + """ xminorticks = IntervalsBetween(2) "Controls if minor ticks on the y axis are visible" yminorticksvisible::Bool = false @@ -458,11 +593,52 @@ end yminortickwidth::Float64 = 1f0 "The tick color of y minor ticks" yminortickcolor::RGBAf = :black - "The tick locator for the y minor ticks" + """ + The tick locator for the minor ticks of the y axis. + + Common objects that can be used are: + - `IntervalsBetween`, divides the space between two adjacent major ticks into `n` intervals + for `n-1` minor ticks + - A vector of numbers + """ yminorticks = IntervalsBetween(2) - "The x axis scale" + """ + The scaling function for the x axis. + + Can be any invertible function, some predefined options are + `identity`, `log`, `log2`, `log10`, `sqrt`, `logit`, `Makie.pseudolog10` and `Makie.Symlog10`. + To use a custom function, you have to define appropriate methods for `Makie.inverse_transform`, + `Makie.defaultlimits` and `Makie.defined_interval`. + + If the scaling function is only defined over a limited interval, + no plot object may have a source datum that lies outside of that range. + For example, there may be no x value lower than or equal to 0 when `log` + is selected for `xscale`. What matters are the source data, not the user-selected + limits, because all data have to be transformed, irrespective of whether they + lie inside or outside of the current limits. + + The axis scale may affect tick finding and formatting, depending + on the values of `xticks` and `xtickformat`. + """ xscale = identity - "The y axis scale" + """ + The scaling function for the y axis. + + Can be any invertible function, some predefined options are + `identity`, `log`, `log2`, `log10`, `sqrt`, `logit`, `Makie.pseudolog10` and `Makie.Symlog10`. + To use a custom function, you have to define appropriate methods for `Makie.inverse_transform`, + `Makie.defaultlimits` and `Makie.defined_interval`. + + If the scaling function is only defined over a limited interval, + no plot object may have a source datum that lies outside of that range. + For example, there may be no y value lower than or equal to 0 when `log` + is selected for `yscale`. What matters are the source data, not the user-selected + limits, because all data have to be transformed, irrespective of whether they + lie inside or outside of the current limits. + + The axis scale may affect tick finding and formatting, depending + on the values of `yticks` and `ytickformat`. + """ yscale = identity end end @@ -937,7 +1113,7 @@ struct LegendEntry attributes::Attributes end -const EntryGroup = Tuple{Optional{<:AbstractString}, Vector{LegendEntry}} +const EntryGroup = Tuple{Any, Vector{LegendEntry}} @Block Legend begin entrygroups::Observable{Vector{EntryGroup}} @@ -1173,15 +1349,47 @@ end valign = :center "The alignment of the scene in its suggested bounding box." alignmode = Inside() - "The elevation angle of the camera" + "The elevation (up / down) angle of the camera. Possible values are between -pi/2 (looking from the bottom up) and +pi/2 (looking from the top down)." elevation = pi/8 - "The azimuth angle of the camera" + """ + The azimuth (left / right) angle of the camera. + + At `azimuth = 0`, the camera looks at the axis from a point on the positive x axis, and rotates to the right from there + with increasing values. At the default value 1.275π, the x axis goes to the right and the y axis to the left. + """ azimuth = 1.275 * pi - "A number between 0 and 1, where 0 is orthographic, and 1 full perspective" + """ + This setting offers a simple scale from 0 to 1, where 0 looks like an orthographic projection (no perspective) + and 1 is a strong perspective look. For most data visualization applications, perspective should + be avoided because it makes interpreting the data correctly harder. It can be of use, however, + if aesthetics are more important than neutral presentation. + """ perspectiveness = 0f0 - "Aspects of the 3 axes with each other" + """ + Controls the lengths of the three axes relative to each other. + + Options are: + - A three-tuple of numbers, which sets the relative lengths of the x, y and z axes directly + - `:data` which sets the length ratios equal to the limit ratios of the axes. This results in an "unsquished" look + where a cube in data space looks like a cube and not a cuboid. + - `:equal` which is a shorthand for `(1, 1, 1)` + """ aspect = (1.0, 1.0, 2/3) # :data :equal - "The view mode which affects the final projection. `:fit` results in the projection that always fits the limits into the viewport, invariant to rotation. `:fitzoom` keeps the x/y ratio intact but stretches the view so the corners touch the scene viewport. `:stretch` scales separately in both x and y direction to fill the viewport, which can distort the `aspect` that is set." + """ + The view mode affects the final projection of the axis by fitting the axis cuboid into the available + space in different ways. + + - `:fit` uses a fixed scaling such that a tight sphere around the cuboid touches the frame edge. + This means that the scaling doesn't change when rotating the axis (the apparent size + of the axis stays the same), but not all available space is used. + The chosen `aspect` is maintained using this setting. + - `:fitzoom` uses a variable scaling such that the closest cuboid corner touches the frame edge. + When rotating the axis, the apparent size of the axis changes which can result in a "pumping" visual effect. + The chosen `aspect` is also maintained using this setting. + - `:stretch` pulls the cuboid corners to the frame edges such that the available space is filled completely. + The chosen `aspect` is not maintained using this setting, so `:stretch` should not be used + if a particular aspect is needed. + """ viewmode = :fitzoom # :fit :fitzoom :stretch "The background color" backgroundcolor = :transparent diff --git a/src/precompiles.jl b/src/precompiles.jl index e64b17dab49..b6f4f70f46a 100644 --- a/src/precompiles.jl +++ b/src/precompiles.jl @@ -1,4 +1,4 @@ -using SnoopPrecompile +using PrecompileTools macro compile(block) return quote @@ -10,14 +10,14 @@ macro compile(block) end let - @precompile_all_calls begin + @compile_workload begin base_path = normpath(joinpath(dirname(pathof(Makie)), "..", "precompile")) shared_precompile = joinpath(base_path, "shared-precompile.jl") include(shared_precompile) empty!(FONT_CACHE) - empty!(_default_font) - empty!(_alternative_fonts) - Makie._current_figure[] = nothing + empty!(DEFAULT_FONT) + empty!(ALTERNATIVE_FONTS) + Makie.CURRENT_FIGURE[] = nothing end nothing end diff --git a/src/recording.jl b/src/recording.jl index 08b3419e662..18cc72b0d6f 100644 --- a/src/recording.jl +++ b/src/recording.jl @@ -139,7 +139,7 @@ end """ function record(func, figlike::FigureLike, path::AbstractString; kw_args...) format = lstrip(splitext(path)[2], '.') - io = Record(func, figlike; format=format, kw_args...) + io = Record(func, figlike; format=format, visible=true, kw_args...) save(path, io) end diff --git a/src/scenes.jl b/src/scenes.jl index 73d0d850f16..e7132663932 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -84,7 +84,7 @@ mutable struct Scene <: AbstractScene px_area::Observable{Rect2i} "Whether the scene should be cleared." - clear::Bool + clear::Observable{Bool} "The `Camera` associated with the Scene." camera::Camera @@ -119,7 +119,7 @@ mutable struct Scene <: AbstractScene parent::Union{Nothing, Scene}, events::Events, px_area::Observable{Rect2i}, - clear::Bool, + clear::Observable{Bool}, camera::Camera, camera_controls::AbstractCamera, transformation::Transformation, @@ -217,7 +217,7 @@ end function Scene(; px_area::Union{Observable{Rect2i}, Nothing} = nothing, events::Events = Events(), - clear::Union{Automatic, Bool} = automatic, + clear::Union{Automatic, Observable{Bool}, Bool} = automatic, transform_func=identity, camera::Union{Function, Camera, Nothing} = nothing, camera_controls::AbstractCamera = EmptyCamera(), @@ -232,7 +232,9 @@ function Scene(; theme = Attributes(), theme_kw... ) - m_theme = merge_without_obs!(current_default_theme(; theme_kw...), theme) + + global_theme = merge_without_obs!(copy(theme), current_default_theme()) + m_theme = merge_without_obs!(Attributes(theme_kw), global_theme) bg = Observable{RGBAf}(to_color(m_theme.backgroundcolor[]); ignore_equal_values=true) @@ -246,7 +248,9 @@ function Scene(; # if we have an opaque background, automatically set clear to true! if clear isa Automatic - clear = alpha(bg[]) == 1 ? true : false + clear = Observable(alpha(bg[]) == 1 ? true : false) + else + clear = convert(Observable{Bool}, clear) end scene = Scene( parent, events, px_area, clear, cam, camera_controls, @@ -316,7 +320,7 @@ function Scene( child = Scene(; events=events, px_area=child_px_area, - clear=clear, + clear=convert(Observable{Bool}, clear), camera=camera, camera_controls=camera_controls, parent=parent, diff --git a/src/stats/hexbin.jl b/src/stats/hexbin.jl index 21527975726..47150e0c9e9 100644 --- a/src/stats/hexbin.jl +++ b/src/stats/hexbin.jl @@ -170,7 +170,19 @@ function Makie.plot!(hb::Hexbin{<:Tuple{<:AbstractVector{<:Point2}}}) if isempty(count_hex[]) (0, 1) else - (minimum(count_hex[]), maximum(count_hex[])) + mi, ma = extrema(count_hex[]) + # if we have only one unique value (usually happens) when there are very few points + # and every cell has only 1 entry, then we set the minimum to 0 so we do not get + # a singular colorrange error down the line. + if mi == ma + if ma == 0 + (0, 1) + else + (0, ma) + end + else + (mi, ma) + end end end diff --git a/src/theming.jl b/src/theming.jl index 1cbbbe39d38..7d58f51a994 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -15,7 +15,7 @@ function wong_colors(alpha = 1.0) return RGBAf.(colors, alpha) end -const default_palettes = Attributes( +const DEFAULT_PALETTES = Attributes( color = wong_colors(1), patchcolor = wong_colors(0.8), marker = [:circle, :utriangle, :cross, :rect, :diamond, :dtriangle, :pentagon, :xcross], @@ -23,8 +23,10 @@ const default_palettes = Attributes( side = [:left, :right] ) -const minimal_default = Attributes( - palette = default_palettes, +Base.@deprecate_binding default_palettes DEFAULT_PALETTES + +const MAKIE_DEFAULT_THEME = Attributes( + palette = DEFAULT_PALETTES, font = :regular, fonts = Attributes( regular = "TeX Gyre Heros Makie", @@ -118,7 +120,11 @@ const minimal_default = Attributes( ) ) -const CURRENT_DEFAULT_THEME = deepcopy(minimal_default) +Base.@deprecate_binding minimal_default MAKIE_DEFAULT_THEME + + +const CURRENT_DEFAULT_THEME = deepcopy(MAKIE_DEFAULT_THEME) +const THEME_LOCK = Base.ReentrantLock() # Basically like deepcopy but while merging it into another Attribute dict function merge_without_obs!(result::Attributes, theme::Attributes) @@ -137,10 +143,12 @@ function merge_without_obs!(result::Attributes, theme::Attributes) end return result end +# Use copy with no obs to quickly deepcopy +fast_deepcopy(attributes) = merge_without_obs!(Attributes(), attributes) + + +current_default_theme() = CURRENT_DEFAULT_THEME -function current_default_theme(; kw_args...) - return merge_without_obs!(Attributes(kw_args), CURRENT_DEFAULT_THEME) -end """ set_theme(theme; kwargs...) @@ -149,10 +157,12 @@ Set the global default theme to `theme` and add / override any attributes given as keyword arguments. """ function set_theme!(new_theme=Attributes(); kwargs...) - empty!(CURRENT_DEFAULT_THEME) - new_theme = merge!(deepcopy(new_theme), deepcopy(minimal_default)) - new_theme = merge!(Theme(kwargs), new_theme) - merge!(CURRENT_DEFAULT_THEME, new_theme) + lock(THEME_LOCK) do + empty!(CURRENT_DEFAULT_THEME) + new_theme = merge_without_obs!(fast_deepcopy(new_theme), MAKIE_DEFAULT_THEME) + new_theme = merge!(Theme(kwargs), new_theme) + merge!(CURRENT_DEFAULT_THEME, new_theme) + end return end @@ -173,14 +183,16 @@ end ``` """ function with_theme(f, theme = Theme(); kwargs...) - previous_theme = copy(CURRENT_DEFAULT_THEME) - try - set_theme!(theme; kwargs...) - f() - catch e - rethrow(e) - finally - set_theme!(previous_theme) + lock(THEME_LOCK) do + previous_theme = fast_deepcopy(CURRENT_DEFAULT_THEME) + try + set_theme!(theme; kwargs...) + f() + catch e + rethrow(e) + finally + set_theme!(previous_theme) + end end end @@ -201,28 +213,30 @@ end """ update_theme!(with_theme::Theme; kwargs...) -Update the current theme incrementally. This means that only the keys given in `with_theme` or through keyword arguments are changed, -the rest is left intact. +Update the current theme incrementally. This means that only the keys given in `with_theme` or through keyword arguments are changed, +the rest is left intact. Nested attributes are either also updated incrementally, or replaced if they are not attributes in the new theme. # Example -To change the default colormap to `:greys`, you can pass that attribute as +To change the default colormap to `:greys`, you can pass that attribute as a keyword argument to `update_theme!` as demonstrated below. ``` update_theme!(colormap=:greys) ``` -This can also be achieved by passing an object of types `Attributes` or `Theme` +This can also be achieved by passing an object of types `Attributes` or `Theme` as the first and only positional argument: ``` update_theme!(Attributes(colormap=:greys)) update_theme!(Theme(colormap=:greys)) ``` """ -function update_theme!(with_theme = Theme()::Attributes; kwargs...) - new_theme = merge!(with_theme, Theme(kwargs)) - _update_attrs!(CURRENT_DEFAULT_THEME, new_theme) - return +function update_theme!(with_theme = Attributes(); kwargs...) + lock(THEME_LOCK) do + new_theme = merge!(with_theme, Attributes(kwargs)) + _update_attrs!(CURRENT_DEFAULT_THEME, new_theme) + return + end end function _update_attrs!(attrs1, attrs2) diff --git a/src/utilities/texture_atlas.jl b/src/utilities/texture_atlas.jl index 3730ab5c65f..7dfb92c9c04 100644 --- a/src/utilities/texture_atlas.jl +++ b/src/utilities/texture_atlas.jl @@ -185,37 +185,44 @@ function cached_load(resolution::Int, pix_per_glyph::Int) return atlas end -const _default_font = NativeFont[] -const _alternative_fonts = NativeFont[] +const DEFAULT_FONT = NativeFont[] +const ALTERNATIVE_FONTS = NativeFont[] +const FONT_LOCK = Base.ReentrantLock() +Base.@deprecate_binding _default_font DEFAULT_FONT +Base.@deprecate_binding _alternative_fonts ALTERNATIVE_FONTS function defaultfont() - if isempty(_default_font) - push!(_default_font, to_font("TeX Gyre Heros Makie")) + lock(FONT_LOCK) do + if isempty(DEFAULT_FONT) + push!(DEFAULT_FONT, to_font("TeX Gyre Heros Makie")) + end + DEFAULT_FONT[] end - _default_font[] end function alternativefonts() - if isempty(_alternative_fonts) - alternatives = [ - "TeXGyreHerosMakie-Regular.otf", - "DejaVuSans.ttf", - "NotoSansCJKkr-Regular.otf", - "NotoSansCuneiform-Regular.ttf", - "NotoSansSymbols-Regular.ttf", - "FiraMono-Medium.ttf" - ] - for font in alternatives - push!(_alternative_fonts, NativeFont(assetpath("fonts", font))) + lock(FONT_LOCK) do + if isempty(ALTERNATIVE_FONTS) + alternatives = [ + "TeXGyreHerosMakie-Regular.otf", + "DejaVuSans.ttf", + "NotoSansCJKkr-Regular.otf", + "NotoSansCuneiform-Regular.ttf", + "NotoSansSymbols-Regular.ttf", + "FiraMono-Medium.ttf" + ] + for font in alternatives + push!(ALTERNATIVE_FONTS, NativeFont(assetpath("fonts", font))) + end end + return ALTERNATIVE_FONTS end - return _alternative_fonts end function render_default_glyphs!(atlas) font = defaultfont() chars = ['a':'z'..., 'A':'Z'..., '0':'9'..., '.', '-'] - fonts = to_font.(to_value.(values(Makie.minimal_default.fonts))) + fonts = to_font.(to_value.(values(Makie.MAKIE_DEFAULT_THEME.fonts))) for font in fonts for c in chars insert_glyph!(atlas, c, font) @@ -473,7 +480,7 @@ function bezierpath_pad_scale_factor(atlas::TextureAtlas, bp) uv_width = Vec(lbrt[3] - lbrt[1], lbrt[4] - lbrt[2]) full_pixel_size_in_atlas = uv_width * Vec2f(size(atlas)) # left + right pad - cutoff from pixel centering - full_pad = 2f0 * atlas.glyph_padding - 1 + full_pad = 2f0 * atlas.glyph_padding - 1 return full_pad ./ (full_pixel_size_in_atlas .- full_pad) end diff --git a/test/conversions.jl b/test/conversions.jl index 8c01def1a27..881062c02c3 100644 --- a/test/conversions.jl +++ b/test/conversions.jl @@ -54,31 +54,18 @@ end @test Float32(X7[7,1]) == V7[7][1] end -@testset "functions" begin - x = -pi..pi - s = convert_arguments(Lines, x, sin) - xy = s.args[1] - @test xy[1][1] ≈ -pi - @test xy[end][1] ≈ pi - for (val, fval) in xy - @test fval ≈ sin(val) atol=1f-6 - end - - x = range(-pi, stop=pi, length=100) - s = convert_arguments(Lines, x, sin) - xy = s.args[1] - @test xy[1][1] ≈ -pi - @test xy[end][1] ≈ pi - for (val, fval) in xy - @test fval ≈ sin(val) atol=1f-6 - end - - pts = [Point(1, 2), Point(4,5), Point(10, 8), Point(1, 2)] - ls=LineString(pts) +@testset "GeometryBasics Lines & Polygons" begin + pts = [Point(1, 2), Point(4, 5), Point(10, 8), Point(1, 2)] + ls = LineString(pts) p = convert_arguments(Makie.PointBased(), ls) @test p[1] == pts - pts1 = [Point(5, 2), Point(4,8), Point(2, 8), Point(5, 2)] + pts_empty = Point2f[] + ls_empty = LineString(pts_empty) + p_empty = convert_arguments(Makie.PointBased(), ls_empty) + @test p_empty[1] == pts_empty + + pts1 = [Point(5, 2), Point(4, 8), Point(2, 8), Point(5, 2)] ls1 = LineString(pts1) lsa = [ls, ls1] p1 = convert_arguments(Makie.PointBased(), lsa) @@ -90,18 +77,26 @@ end @test p2[1][1:4] == pts @test p2[1][6:9] == pts1 + mls_emtpy = MultiLineString([LineString(pts_empty)]) + p_empty = convert_arguments(Makie.PointBased(), mls_emtpy) + @test p_empty[1] == pts_empty + pol_e = Polygon(ls) p3_e = convert_arguments(Makie.PointBased(), pol_e) - @test p3_e[1][1:end-1] == pts # for poly we repeat last point + @test p3_e[1][1:end] == pts # for poly we repeat last point pol = Polygon(ls, [ls1]) p3 = convert_arguments(Makie.PointBased(), pol) @test p3[1][1:4] == pts - @test p3[1][7:10] == pts1 + @test p3[1][6:9] == pts1 - pts2 = Point{2, Int}[(5, 1), (3, 3), (4, 8), (1, 2), (5, 1)] - pts3 = Point{2, Int}[(2, 2), (2, 3),(3, 4), (2, 2)] - pts4 = Point{2, Int}[(2, 2), (3, 8),(5, 6), (3, 4), (2, 2)] + pol_emtpy = Polygon(pts_empty) + p_empty = convert_arguments(Makie.PointBased(), pol_emtpy) + @test p_empty[1] == pts_empty + + pts2 = Point{2,Int}[(5, 1), (3, 3), (4, 8), (1, 2), (5, 1)] + pts3 = Point{2,Int}[(2, 2), (2, 3), (3, 4), (2, 2)] + pts4 = Point{2,Int}[(2, 2), (3, 8), (5, 6), (3, 4), (2, 2)] ls2 = LineString(pts2) ls3 = LineString(pts3) ls4 = LineString(pts4) @@ -110,10 +105,34 @@ end p4 = convert_arguments(Makie.PointBased(), apol) mpol = MultiPolygon([pol, pol1]) @test p4[1][1:4] == pts - @test p4[1][7:10] == pts1 - @test p4[1][14:18] == pts2 - @test p4[1][21:24] == pts3 - @test p4[1][27:31] == pts4 + @test p4[1][6:9] == pts1 + @test p4[1][11:15] == pts2 + @test p4[1][17:20] == pts3 + @test p4[1][22:end] == pts4 + + mpol_emtpy = MultiPolygon(typeof(pol_emtpy)[]) + p_empty = convert_arguments(Makie.PointBased(), mpol_emtpy) + @test p_empty[1] == pts_empty +end + +@testset "functions" begin + x = -pi..pi + s = convert_arguments(Lines, x, sin) + xy = s.args[1] + @test xy[1][1] ≈ -pi + @test xy[end][1] ≈ pi + for (val, fval) in xy + @test fval ≈ sin(val) atol=1f-6 + end + + x = range(-pi, stop=pi, length=100) + s = convert_arguments(Lines, x, sin) + xy = s.args[1] + @test xy[1][1] ≈ -pi + @test xy[end][1] ≈ pi + for (val, fval) in xy + @test fval ≈ sin(val) atol=1f-6 + end end using Makie: check_line_pattern, line_diff_pattern diff --git a/test/makielayout.jl b/test/makielayout.jl index fb3da26ae99..df8614a2ce6 100644 --- a/test/makielayout.jl +++ b/test/makielayout.jl @@ -358,4 +358,21 @@ end end @test isempty(d) end +end + +@testset "Legend with rich text" begin + fig = Figure() + ax = Axis(fig[1,1]) + l1 = lines!( 0..2π , sin ) + @test_nowarn Legend( + fig[1,2], + [l1], + [rich("some", subscript("entry"))], + rich("title", color = :red, font = :bold_italic)) +end + +@testset "Legend for hist with labels" begin + f, ax, h = hist(randn(100), bar_labels = :y, label = "My histogram") + @test_nowarn axislegend() + f end \ No newline at end of file diff --git a/test/record.jl b/test/record.jl index 9e2310a28ed..9cda4e3dd03 100644 --- a/test/record.jl +++ b/test/record.jl @@ -58,6 +58,7 @@ mktempdir() do tempdir warn_fmts=["mkv", "webm", "gif"], no_warn_fmts=["mp4"], ), + (:loop, 0, ["mkv", "webm", "mp4"], ["gif"]), ] for (kwarg, value, warn_fmts, no_warn_fmts) in warn_tests