diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5ebc1bfeb..6584f9b93c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,17 @@ ## [Unreleased] -- CairoMakie: Added argument `pdf_version` to restrict the PDF version when saving a figure as a PDF [#3845](https://github.com/MakieOrg/Makie.jl/pull/3845). - Improved accuracy of framerate settings in GLMakie [#3954](https://github.com/MakieOrg/Makie.jl/pull/3954) +- Fixes for Menu and DataInspector [#3975](https://github.com/MakieOrg/Makie.jl/pull/3975) +- Add line-loop detection and rendering to GLMakie and WGLMakie [#3907](https://github.com/MakieOrg/Makie.jl/pull/3907) + +## [0.21.3] - 2024-06-17 + +- Fix stack overflows when using `markerspace = :data` with `scatter` [#3960](https://github.com/MakieOrg/Makie.jl/issues/3960). +- CairoMakie: Fix broken SVGs when using non-interpolated image primitives, for example Colorbars, with recent Cairo versions [#3967](https://github.com/MakieOrg/Makie.jl/pull/3967). +- CairoMakie: Add argument `pdf_version` to restrict the PDF version when saving a figure as a PDF [#3845](https://github.com/MakieOrg/Makie.jl/pull/3845). +- CairoMakie: Fix incorrect scaling factor for SVGs with Cairo_jll 1.18 [#3964](https://github.com/MakieOrg/Makie.jl/pull/3964). +- Fixed use of Textbox from Bonito [#3924](https://github.com/MakieOrg/Makie.jl/pull/3924) ## [0.21.2] - 2024-05-22 @@ -508,8 +517,9 @@ All other changes are collected [in this PR](https://github.com/MakieOrg/Makie.j - Fixed rendering of `heatmap`s with one or more reversed ranges in CairoMakie, as in `heatmap(1:10, 10:-1:1, rand(10, 10))` [#1100](https://github.com/MakieOrg/Makie.jl/pull/1100). - Fixed volume slice recipe and added docs for it [#1123](https://github.com/MakieOrg/Makie.jl/pull/1123). -[Unreleased]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.2...HEAD -[0.21.2]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.0...v0.21.2 +[Unreleased]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.3...HEAD +[0.21.3]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.2...v0.21.3 +[0.21.2]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.1...v0.21.2 [0.21.1]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/MakieOrg/Makie.jl/compare/v0.20.10...v0.21.0 [0.20.10]: https://github.com/MakieOrg/Makie.jl/compare/v0.20.9...v0.20.10 diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index 3e4495fd8b5..f9846929958 100644 --- a/CairoMakie/Project.toml +++ b/CairoMakie/Project.toml @@ -1,11 +1,12 @@ name = "CairoMakie" uuid = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" author = ["Simon Danisch "] -version = "0.12.2" +version = "0.12.3" [deps] CRC32c = "8bf52ea8-c179-5cab-976a-9e18b702a9bc" Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" +Cairo_jll = "83423d85-b0ee-5818-9007-b63ccbeb887a" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" @@ -17,12 +18,13 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" [compat] CRC32c = "1.0, 1.6" Cairo = "1.0.4" +Cairo_jll = "1.18.0" Colors = "0.10, 0.11, 0.12" FileIO = "1.1" FreeType = "3, 4.0" GeometryBasics = "0.4.11" LinearAlgebra = "1.0, 1.6" -Makie = "=0.21.2" +Makie = "=0.21.3" PrecompileTools = "1.0" julia = "1.3" diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 46e0e3bcc2d..1b54a8fd8e5 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -143,7 +143,7 @@ end # instead of the whole Scene # - Recognize when a screen is an image surface, and set scale to render the plot # at the scale of the device pixel -function draw_plot_as_image(scene::Scene, screen::Screen, primitive::Plot, scale::Number = 1) +function draw_plot_as_image(scene::Scene, screen::Screen{RT}, primitive::Plot, scale::Number = 1) where RT # you can provide `p.rasterize = scale::Int` or `p.rasterize = true`, both of which are numbers # Extract scene width in device indepentent units @@ -163,8 +163,11 @@ function draw_plot_as_image(scene::Scene, screen::Screen, primitive::Plot, scale # Cairo.scale(screen.context, w / scr.surface.width, h / scr.surface.height) Cairo.set_source_surface(screen.context, scr.surface, 0, 0) p = Cairo.get_source(scr.context) - # this is needed to avoid blurry edges - Cairo.pattern_set_extend(p, Cairo.EXTEND_PAD) + if RT !== SVG + # this is needed to avoid blurry edges in png renderings, however since Cairo 1.18 this + # setting seems to create broken SVGs + Cairo.pattern_set_extend(p, Cairo.EXTEND_PAD) + end # Set filter doesn't work!? Cairo.pattern_set_filter(p, Cairo.FILTER_BILINEAR) Cairo.fill(screen.context) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index b594c79b11d..c349ffb790e 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -202,6 +202,8 @@ project_command(c::ClosePath, scene, space, model) = c function draw_single(primitive::Lines, ctx, positions) n = length(positions) + start = positions[begin] + @inbounds for i in 1:n p = positions[i] # only take action for non-NaNs @@ -209,10 +211,14 @@ function draw_single(primitive::Lines, ctx, positions) # new line segment at beginning or if previously NaN if i == 1 || isnan(positions[i-1]) Cairo.move_to(ctx, p...) + start = p else Cairo.line_to(ctx, p...) # complete line segment at end or if next point is NaN if i == n || isnan(positions[i+1]) + if p ≈ start + Cairo.close_path(ctx) + end Cairo.stroke(ctx) end end @@ -298,7 +304,8 @@ function draw_multi(primitive::Lines, ctx, positions, colors::AbstractArray, lin prev_position = positions[begin] prev_nan = isnan(prev_position) prev_continued = false - + start = positions[begin] + if !prev_nan # first is not nan, move_to Cairo.move_to(ctx, positions[begin]...) @@ -315,6 +322,7 @@ function draw_multi(primitive::Lines, ctx, positions, colors::AbstractArray, lin # this is nan if prev_continued # and this is prev_continued, so set source and stroke to finish previous line + (prev_position ≈ start) && Cairo.close_path(ctx) Cairo.set_line_width(ctx, this_linewidth) !isnothing(dash) && Cairo.set_dash(ctx, dash .* this_linewidth) Cairo.set_source_rgba(ctx, red(prev_color), green(prev_color), blue(prev_color), alpha(prev_color)) @@ -328,6 +336,7 @@ function draw_multi(primitive::Lines, ctx, positions, colors::AbstractArray, lin if !this_nan # but this is not nan, so move to this position Cairo.move_to(ctx, this_position...) + start = this_position else # and this is also nan, do nothing end @@ -342,6 +351,7 @@ function draw_multi(primitive::Lines, ctx, positions, colors::AbstractArray, lin if i == lastindex(positions) # this is the last element so stroke this + (this_position ≈ start) && Cairo.close_path(ctx) Cairo.set_line_width(ctx, this_linewidth) !isnothing(dash) && Cairo.set_dash(ctx, dash .* this_linewidth) Cairo.set_source_rgba(ctx, red(this_color), green(this_color), blue(this_color), alpha(this_color)) @@ -782,7 +792,7 @@ premultiplied_rgba(a::AbstractArray{<:Color}) = RGBA.(a) premultiplied_rgba(r::RGBA) = RGBA(r.r * r.alpha, r.g * r.alpha, r.b * r.alpha, r.alpha) premultiplied_rgba(c::Colorant) = premultiplied_rgba(RGBA(c)) -function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Union{Heatmap, Image})) +function draw_atomic(scene::Scene, screen::Screen{RT}, @nospecialize(primitive::Union{Heatmap, Image})) where RT ctx = screen.context image = primitive[3][] xs, ys = primitive[1][], primitive[2][] @@ -858,8 +868,11 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio Cairo.scale(ctx, w / s.width, h / s.height) Cairo.set_source_surface(ctx, s, 0, 0) p = Cairo.get_source(ctx) - # this is needed to avoid blurry edges - Cairo.pattern_set_extend(p, Cairo.EXTEND_PAD) + if RT !== SVG + # this is needed to avoid blurry edges in png renderings, however since Cairo 1.18 this + # setting seems to create broken SVGs + Cairo.pattern_set_extend(p, Cairo.EXTEND_PAD) + end filt = interpolate ? Cairo.FILTER_BILINEAR : Cairo.FILTER_NEAREST Cairo.pattern_set_filter(p, filt) Cairo.fill(ctx) diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index af5b7042a26..d9d23959c6d 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -112,13 +112,17 @@ struct ScreenConfig end end +css_px_per_unit(pt_per_unit) = pt_per_unit / 0.75 + function device_scaling_factor(rendertype, sc::ScreenConfig) - isv = is_vector_backend(convert(RenderType, rendertype)) - return isv ? sc.pt_per_unit : sc.px_per_unit + rt = convert(RenderType, rendertype) + isv = is_vector_backend(rt) + # from version 1.18 on, Cairo saves SVGs without the pt unit specified, so they are actually in CSS px now + return rt === SVG ? css_px_per_unit(sc.pt_per_unit) : isv ? sc.pt_per_unit : sc.px_per_unit end function device_scaling_factor(surface::Cairo.CairoSurface, sc::ScreenConfig) - return is_vector_backend(surface) ? sc.pt_per_unit : sc.px_per_unit + return device_scaling_factor(get_render_type(surface), sc) end const LAST_INLINE = Ref{Union{Makie.Automatic,Bool}}(Makie.automatic) diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index 74730985804..8311284ca46 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -1,6 +1,6 @@ name = "GLMakie" uuid = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" -version = "0.10.2" +version = "0.10.3" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" @@ -30,7 +30,7 @@ FreeTypeAbstraction = "0.10" GLFW = "3.3" GeometryBasics = "0.4.11" LinearAlgebra = "1.0, 1.6" -Makie = "=0.21.2" +Makie = "=0.21.3" Markdown = "1.0, 1.6" MeshIO = "0.4" ModernGL = "1" diff --git a/GLMakie/assets/shader/line_segment.geom b/GLMakie/assets/shader/line_segment.geom index fd32a3508bd..dc5425fbf58 100644 --- a/GLMakie/assets/shader/line_segment.geom +++ b/GLMakie/assets/shader/line_segment.geom @@ -82,7 +82,7 @@ void main(void) // Set invalid / ignored outputs f_truncation = vec2(-1e12); // no truncated joint f_pattern_overwrite = vec4(-1e12, 1.0, 1e12, 1.0); // no joints to overwrite - f_extrusion = vec2(0.5); // no joints needing extrusion + f_extrusion = vec2(0.0); // no joints needing extrusion f_linepoints = vec4(-1e12); f_miter_vecs = vec4(-1); diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom index f6c198f728d..2d49f2eb3c9 100644 --- a/GLMakie/assets/shader/lines.geom +++ b/GLMakie/assets/shader/lines.geom @@ -196,22 +196,32 @@ void main(void) return; } - // We mark each of the four vertices as valid or not. Vertices can be - // marked invalid on input (eg, if they contain NaN). We also mark them - // invalid if they repeat in the index buffer. This allows us to render to - // the very ends of a polyline without clumsy buffering the position data on the - // CPU side by repeating the first and last points via the index buffer. It - // just requires a little care further down to avoid degenerate normals. + // We mark vertices based on their role in a line segment: + // 0: the vertex is skipped/invalid (i.e. NaN) + // 1: the vertex is valid (part of a plain line segment) + // 2: the vertex is either .. + // a loop target if the previous or next vertex is marked 0 + // or a normal valid vertex otherwise + // isvalid[0] and [3] are used to discern whether a line segment is part + // of a continuing line (valid) or a line start/end (invalid). A line only + // ends if the previous / next vertex is invalid + // isvalid[1] and [2] are used to discern whether a line segment should be + // discarded. This should happen if either vertex is invalid or if one of + // the vertices is a loop target. + // A loop target is an extra vertex placed before/after the shared vertex to + // guide joint generation. Consider for example a closed triangle A B C A. + // To cleanly close the loop both A's need to create a joint as if we had + // c A B C A b, but without drawing the c-A and A-b segments. c and b would + // be loop targets, matching C and B in position, but only being valid in + // isvalid[0] and [3], not as a drawn segment in isvalid[1] and [2]. bool isvalid[4] = bool[]( - g_valid_vertex[0] == 1 && g_id[0].y != g_id[1].y, - g_valid_vertex[1] == 1, - g_valid_vertex[2] == 1, - g_valid_vertex[3] == 1 && g_id[2].y != g_id[3].y + (g_valid_vertex[0] > 0) && g_id[0].y != g_id[1].y, + (g_valid_vertex[1] > 0) && !((g_valid_vertex[0] == 0) && (g_valid_vertex[1] == 2)), + (g_valid_vertex[2] > 0) && !((g_valid_vertex[2] == 2) && (g_valid_vertex[3] == 0)), + (g_valid_vertex[3] > 0) && g_id[2].y != g_id[3].y ); if(!isvalid[1] || !isvalid[2]){ - // If one of the central vertices is invalid or there is a break in the - // line, we don't emit anything. return; } @@ -407,8 +417,8 @@ void main(void) // if joint skipped elongate to new length // if normal joint elongate a lot to let discard/truncation handle joint f_extrusion = vec2( - !isvalid[0] ? min(AA_RADIUS, halfwidth) : (adjustment[0] == 0.0 ? 1e12 : halfwidth * abs(extrusion[0][0])), - !isvalid[3] ? min(AA_RADIUS, halfwidth) : (adjustment[1] == 0.0 ? 1e12 : halfwidth * abs(extrusion[1][0])) + !isvalid[0] ? 0.0 : (adjustment[0] == 0.0 ? 1e12 : halfwidth * abs(extrusion[0][0])), + !isvalid[3] ? 0.0 : (adjustment[1] == 0.0 ? 1e12 : halfwidth * abs(extrusion[1][0])) ); // used to compute width sdf diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index a26951393f8..902aa8292b0 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -29,6 +29,85 @@ gl_color_type_annotation(::Real) = "float" gl_color_type_annotation(::Makie.RGB) = "vec3" gl_color_type_annotation(::Makie.RGBA) = "vec4" +function generate_indices(positions) + valid_obs = Observable(Float32[]) # why does this need to be a float? + + indices_obs = const_lift(positions) do ps + valid = valid_obs[] + resize!(valid, length(ps)) + + indices = Cuint[] + sizehint!(indices, length(ps)+2) + + # This loop identifies sections of line points A B C D E F bounded by + # the start/end of the list ps or by NaN and generates indices for them: + # if A == F (loop): E A B C D E F B 0 + # if A != F (no loop): 0 A B C D E F 0 + # where 0 is NaN + # It marks vertices as invalid (0) if they are NaN, valid (1) if they + # are part of a continous line section, or as ghost edges (2) used to + # cleanly close a loop. The shader detects successive vertices with + # 1-2-0 and 0-2-1 validity to avoid drawing ghost segments (E-A from + # 0-E-A-B and F-B from E-F-B-0 which would dublicate E-F and A-B) + + last_start_pos = eltype(ps)(NaN) + last_start_idx = -1 + + for (i, p) in enumerate(ps) + not_nan = isfinite(p) + valid[i] = not_nan + + if not_nan + if last_start_idx == -1 + # place nan before section of line vertices + # (or dublicate ps[1]) + push!(indices, i-1) + last_start_idx = length(indices) + 1 + last_start_pos = p + end + # add line vertex + push!(indices, i) + + # case loop (loop index set, loop contains at least 3 segments, start == end) + elseif (last_start_idx != -1) && (length(indices) - last_start_idx > 2) && + (ps[max(1, i-1)] ≈ last_start_pos) + + # add ghost vertices before an after the loop to cleanly connect line + indices[last_start_idx-1] = max(1, i-2) + push!(indices, indices[last_start_idx+1], i) + # mark the ghost vertices + valid[i-2] = 2 + valid[indices[last_start_idx+1]] = 2 + # not in loop anymore + last_start_idx = -1 + + # non-looping line end + elseif (last_start_idx != -1) # effective "last index not NaN" + push!(indices, i) + last_start_idx = -1 + # else: we don't need to push repeated NaNs + end + end + + # treat ps[end+1] as NaN to correctly finish the line + if (last_start_idx != -1) && (length(indices) - last_start_idx > 2) && + (ps[end] ≈ last_start_pos) + + indices[last_start_idx-1] = length(ps) - 1 + push!(indices, indices[last_start_idx+1]) + valid[end-1] = 2 + valid[indices[last_start_idx+1]] = 2 + elseif last_start_idx != -1 + push!(indices, length(ps)) + end + + notify(valid_obs) + return indices .- Cuint(1) + end + + return indices_obs, valid_obs +end + @nospecialize function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data::Dict) where T<:Point p_vec = if isa(position, GPUArray) @@ -37,11 +116,13 @@ function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data:: const_lift(vec, position) end + indices, valid_vertex = generate_indices(p_vec) + color_type = gl_color_type_annotation(data[:color]) resolution = data[:resolution] @gen_defaults! data begin - total_length::Int32 = const_lift(x-> Int32(length(x)), position) + total_length::Int32 = const_lift(x -> Int32(length(x) - 2), indices) vertex = p_vec => GLBuffer color = nothing => GLBuffer color_map = nothing => Texture @@ -53,10 +134,7 @@ function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data:: # Duplicate the vertex indices on the ends of the line, as our geometry # shader in `layout(lines_adjacency)` mode requires each rendered # segment to have neighbouring vertices. - indices = const_lift(p_vec) do p - len0 = length(p) - 1 - return isempty(p) ? Cuint[] : Cuint[0; 0:len0; len0] - end => to_index_buffer + indices = indices => to_index_buffer transparency = false fast = false shader = GLVisualizeShader( @@ -70,9 +148,7 @@ function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data:: ) ) gl_primitive = GL_LINE_STRIP_ADJACENCY - valid_vertex = const_lift(p_vec) do points - map(p-> Float32(all(isfinite, p)), points) - end => GLBuffer + valid_vertex = valid_vertex => GLBuffer lastlen = const_lift(sumlengths, p_vec, resolution) => GLBuffer pattern_length = 1f0 # we divide by pattern_length a lot. debug = false diff --git a/MakieCore/Project.toml b/MakieCore/Project.toml index fd59d61d75e..695cd2b97e9 100644 --- a/MakieCore/Project.toml +++ b/MakieCore/Project.toml @@ -1,7 +1,7 @@ name = "MakieCore" uuid = "20f20a25-4f0e-4fdf-b5d1-57303727442b" authors = ["Simon Danisch"] -version = "0.8.2" +version = "0.8.3" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" diff --git a/MakieCore/src/basic_plots.jl b/MakieCore/src/basic_plots.jl index 9ad23fca395..86372314229 100644 --- a/MakieCore/src/basic_plots.jl +++ b/MakieCore/src/basic_plots.jl @@ -317,13 +317,26 @@ Creates a connected line plot for each element in `(x, y, z)`, `(x, y)` or `posi color = @inherit linecolor "Sets the width of the line in screen units" linewidth = @inherit linewidth - "Sets the pattern of the line e.g. `:solid`, `:dot`, `:dashdot`. For custom patterns look at `Linestyle(Number[...])`" + """ + Sets the dash pattern of the line. Options are `:solid` (equivalent to `nothing`), `:dot`, `:dash`, `:dashdot` and `:dashdotdot`. + These can also be given in a tuple with a gap style modifier, either `:normal`, `:dense` or `:loose`. + For example, `(:dot, :loose)` or `(:dashdot, :dense)`. + + For custom patterns have a look at [`Makie.Linestyle`](@ref). + """ linestyle = nothing - "Sets the type of linecap used, i.e. :butt (flat with no extrusion), :square (flat with 0.5 linewidth extrusion) or :round." + """ + Sets the type of line cap used. Options are `:butt` (flat without extrusion), + `:square` (flat with half a linewidth extrusion) or `:round`. + """ linecap = @inherit linecap - "Controls whether line joints are rounded (:round) or not (:miter)." + """ + Controls the rendering at corners. Options are `:miter` for sharp corners, + `:bevel` for "cut off" corners, and `:round` for rounded corners. If the corner angle + is below `miter_limit`, `:miter` is equivalent to `:bevel` to avoid long spikes. + """ joinstyle = @inherit joinstyle - "Sets the minimum inner joint angle below which miter joints truncate. See also `Makie.miter_distance_to_angle()`" + "Sets the minimum inner join angle below which miter joins truncate. See also `Makie.miter_distance_to_angle`." miter_limit = @inherit miter_limit "Sets which attributes to cycle when creating multiple plots." cycle = [:color] @@ -345,7 +358,13 @@ Plots a line for each pair of points in `(x, y, z)`, `(x, y)`, or `positions`. color = @inherit linecolor "Sets the width of the line in pixel units" linewidth = @inherit linewidth - "Sets the pattern of the line e.g. `:solid`, `:dot`, `:dashdot`. For custom patterns look at `Linestyle(Number[...])`" + """ + Sets the dash pattern of the line. Options are `:solid` (equivalent to `nothing`), `:dot`, `:dash`, `:dashdot` and `:dashdotdot`. + These can also be given in a tuple with a gap style modifier, either `:normal`, `:dense` or `:loose`. + For example, `(:dot, :loose)` or `(:dashdot, :dense)`. + + For custom patterns have a look at [`Makie.Linestyle`](@ref). + """ linestyle = nothing "Sets the type of linecap used, i.e. :butt (flat with no extrusion), :square (flat with 1 linewidth extrusion) or :round." linecap = @inherit linecap @@ -594,7 +613,13 @@ Plots polygons, which are defined by strokecolormap = @inherit colormap "Sets the width of the outline." strokewidth = @inherit patchstrokewidth - "Sets the pattern of the line (e.g. `:solid`, `:dot`, `:dashdot`)" + """ + Sets the dash pattern of the line. Options are `:solid` (equivalent to `nothing`), `:dot`, `:dash`, `:dashdot` and `:dashdotdot`. + These can also be given in a tuple with a gap style modifier, either `:normal`, `:dense` or `:loose`. + For example, `(:dot, :loose)` or `(:dashdot, :dense)`. + + For custom patterns have a look at [`Makie.Linestyle`](@ref). + """ linestyle = nothing linecap = @inherit linecap joinstyle = @inherit joinstyle diff --git a/Project.toml b/Project.toml index 21352e6f8f9..82051ce5107 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.21.2" +version = "0.21.3" [deps] Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" @@ -90,7 +90,7 @@ KernelDensity = "0.5, 0.6" LaTeXStrings = "1.2" LinearAlgebra = "1.0, 1.6" MacroTools = "0.5" -MakieCore = "=0.8.2" +MakieCore = "=0.8.3" Markdown = "1.0, 1.6" MathTeXEngine = "0.5, 0.6" Observables = "0.5.5" diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 1948224246d..6153f8abf3a 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.7.2" +version = "0.7.3" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -17,7 +17,7 @@ Colors = "0.9, 0.10, 0.11, 0.12" FileIO = "1.6" GeometryBasics = "0.4.11" LinearAlgebra = "1.0, 1.6" -Makie = "=0.21.2" +Makie = "=0.21.3" Printf = "1.0, 1.6" RadeonProRender = "0.3.0" julia = "1.3" diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 449251c743e..13a32751b85 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -84,8 +84,28 @@ end fig end -#@reference_test "Miter Limit" -begin +@reference_test "Line loops" begin + # check for issues with self-overlap of line segments with loops, interplay + # between loops, lines, nan separation + loop(p) = Point2f[p, p .+ Point2f(0.8, 0), p .+ Point2f(0, 0.8), p, Point2f(NaN)] + line(p) = Point2f[p, p .+ Point2f(0.8, 0), p .+ Point2f(0, 0.8), Point2f(NaN)] + + nan = [Point2f(NaN)] + ps = vcat( + nan, nan, nan, loop((0, -1)), loop((1, -1)), + line((-1, 0)), line((0, 0)), + nan, nan, line((1, 0)), nan, + loop((-1, 1)), nan, loop((0, 1)), + nan, [Point2f(1, 1)], nan + ) + + f, a, p = lines(loop((-1, -1)), linewidth = 20, linecap = :round, alpha = 0.5) + lines!(ps, linewidth = 20, linecap = :round, alpha = 0.5) + lines!(vcat(nan, nan, line((1, 1)), nan), linewidth = 20, linecap = :round, alpha = 0.5) + f +end + +@reference_test "Miter Limit" begin ps = [Point2f(0, -0.5), Point2f(1, -0.5)] for phi in [160, -130, 121, 50, 119, -90] # these are 180-miter_angle R = Makie.Mat2f(cosd(phi), sind(phi), -sind(phi), cosd(phi)) diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml index c37efb0a074..83e2626802d 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.10.2" +version = "0.10.3" [deps] Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" @@ -27,7 +27,7 @@ FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.11" Hyperscript = "0.0.3, 0.0.4, 0.0.5" LinearAlgebra = "1.0, 1.6" -Makie = "=0.21.2" +Makie = "=0.21.3" Observables = "0.5.1" PNGFiles = "0.3, 0.4" PrecompileTools = "1.0" diff --git a/WGLMakie/assets/sprites.frag b/WGLMakie/assets/sprites.frag index 468b75366ff..a4befe2b936 100644 --- a/WGLMakie/assets/sprites.frag +++ b/WGLMakie/assets/sprites.frag @@ -149,11 +149,12 @@ void main() { if (picking) { if (final_color.a > 0.1) { fragment_color = pack_int(object_id, frag_instance_id); + } else { + discard; } return; } - if (final_color.a <= 0.0){ discard; } diff --git a/WGLMakie/src/Lines.js b/WGLMakie/src/Lines.js index 03850e5701a..a340b33c9e6 100644 --- a/WGLMakie/src/Lines.js +++ b/WGLMakie/src/Lines.js @@ -548,12 +548,12 @@ function lines_vertex_shader(uniforms, attributes, is_linesegments) { } // Used to elongate sdf to include joints - // if start/end elongate slightly so that there is no AA gap in loops + // if start/end no elongation // if joint skipped elongate to new length // if normal joint elongate a lot to let shape/truncation handle joint f_extrusion = vec2( - !isvalid[0] ? min(AA_RADIUS, halfwidth) : (adjustment[0] == 0.0 ? 1e12 : halfwidth * abs(extrusion[0])), - !isvalid[3] ? min(AA_RADIUS, halfwidth) : (adjustment[1] == 0.0 ? 1e12 : halfwidth * abs(extrusion[1])) + !isvalid[0] ? 0.0 : (adjustment[0] == 0.0 ? 1e12 : halfwidth * abs(extrusion[0])), + !isvalid[3] ? 0.0 : (adjustment[1] == 0.0 ? 1e12 : halfwidth * abs(extrusion[1])) ); // used to compute width sdf diff --git a/WGLMakie/src/events.jl b/WGLMakie/src/events.jl index bd129e83d53..41b7767db88 100644 --- a/WGLMakie/src/events.jl +++ b/WGLMakie/src/events.jl @@ -85,12 +85,15 @@ function connect_scene_events!(scene::Scene, comm::Observable) e.scroll[] = Float64.((sign.(scroll)...,)) end @handle msg.keydown begin - button = code_to_keyboard(keydown) + button = code_to_keyboard(keydown[1]) # don't add unknown buttons...we can't work with them # and they won't get removed if button != Keyboard.unknown e.keyboardbutton[] = KeyEvent(button, Keyboard.press) end + if length(keydown[2])==1 && isascii(keydown[2]) + e.unicode_input[] = keydown[2][1] + end end @handle msg.keyup begin if keyup == "delete_keys" diff --git a/WGLMakie/src/lines.jl b/WGLMakie/src/lines.jl index f30d88a9a43..8ccdc1455a9 100644 --- a/WGLMakie/src/lines.jl +++ b/WGLMakie/src/lines.jl @@ -49,29 +49,69 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) transformed_points = apply_transform_and_f32_conversion(f32c, tf, ps, space) # TODO: Do this in javascript? + empty!(indices[]) if isempty(transformed_points) - empty!(indices[]) notify(indices) return transformed_points else - sizehint!(empty!(indices[]), length(transformed_points) + 2) + sizehint!(indices[], length(transformed_points) + 2) + was_nan = true - for i in eachindex(transformed_points) - # dublicate first and last element of line selection - if isnan(transformed_points[i]) + loop_start_idx = -1 + for (i, p) in enumerate(transformed_points) + if isnan(p) + # line section end (last was value, now nan) if !was_nan - push!(indices[], i-1) # end of line dublication + # does previous point close loop? + # loop started && 3+ segments && start == end + if loop_start_idx != -1 && (loop_start_idx + 2 < length(indices[])) && + (transformed_points[indices[][loop_start_idx]] ≈ transformed_points[i-1]) + + # start -v v- end + # adjust from j j j+1 .. i-2 i-1 + # to nan i-2 j j+1 .. i-2 i-1 j+1 nan + # where start == end thus j == i-1 + # if nan is present in a quartet of vertices + # (nan, i-2, j, i+1) the segment (i-2, j) will not + # be drawn (which we want as that segment would overlap) + + # tweak dublicated vertices to be loop vertices + push!(indices[], indices[][loop_start_idx+1]) + indices[][loop_start_idx-1] = i-2 + # nan is inserted at bottom (and not necessary for start/end) + + else # no loop, dublicate end point + push!(indices[], i-1) + end end + loop_start_idx = -1 was_nan = true - elseif was_nan - push!(indices[], i) # start of line dublication + else + + if was_nan + # line section start - dublicate point + push!(indices[], i) + # first point in a potential loop + loop_start_idx = length(indices[])+1 + end was_nan = false end + # push normal line point (including nan) push!(indices[], i) end - push!(indices[], length(transformed_points)) - notify(indices) + + # Finish line (insert dublicate end point or close loop) + if !was_nan + if loop_start_idx != -1 && (loop_start_idx + 2 < length(indices[])) && + (transformed_points[indices[][loop_start_idx]] ≈ transformed_points[end]) + + push!(indices[], indices[][loop_start_idx+1]) + indices[][loop_start_idx-1] = length(transformed_points)-1 + else + push!(indices[], length(transformed_points)) + end + end return transformed_points[indices[]] end @@ -94,20 +134,38 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) output = Vector{Float32}(undef, length(ps)) if !isempty(ps) - # clip -> pixel, but we can skip offset + # clip -> pixel, but we can skip scene offset scale = Vec2f(0.5 * res[1], 0.5 * res[2]) - # Initial position - clip = pvm * to_ndim(Point4f, to_ndim(Point3f, ps[1], 0f0), 1f0) + # position of start of first drawn line segment (TODO: deal with multiple nans at start) + clip = pvm * to_ndim(Point4f, to_ndim(Point3f, ps[2], 0f0), 1f0) prev = scale .* Point2f(clip) ./ clip[4] # calculate cumulative pixel scale length - output[1] = 0f0 - for i in 2:length(ps) - clip = pvm * to_ndim(Point4f, to_ndim(Point3f, ps[i], 0f0), 1f0) - current = scale .* Point2f(clip) ./ clip[4] - l = norm(current - prev) - output[i] = ifelse(isnan(l), 0f0, output[i-1] + l) - prev = current + output[1] = 0f0 # dublicated point + output[2] = 0f0 # start of first line segment + output[end] = 0f0 # dublicated end point + i = 3 # end of first line segment, start of second + while i < length(ps) + if isfinite(ps[i]) + clip = pvm * to_ndim(Point4f, to_ndim(Point3f, ps[i], 0f0), 1f0) + current = scale .* Point2f(clip) ./ clip[4] + l = norm(current - prev) + output[i] = output[i-1] + l + prev = current + i += 1 + else + # a vertex section (NaN, A, B, C) does not draw, so + # norm(B - A) should not contribute to line length. + # (norm(B - A) is 0 for capped lines but not for loops) + output[i] = 0f0 + output[i+1] = 0f0 + if i+2 <= length(ps) + output[min(end, i+2)] = 0f0 + clip = pvm * to_ndim(Point4f, to_ndim(Point3f, ps[i+2], 0f0), 1f0) + prev = scale .* Point2f(clip) ./ clip[4] + end + i += 3 + end end end diff --git a/WGLMakie/src/picking.jl b/WGLMakie/src/picking.jl index 38a4f3fc8e6..1e069317a3c 100644 --- a/WGLMakie/src/picking.jl +++ b/WGLMakie/src/picking.jl @@ -16,8 +16,7 @@ function pick_native(screen::Screen, rect::Rect2i) if isempty(matrix) return empty else - all_children = Makie.collect_atomic_plots(scene) - lookup = Dict(Pair.(js_uuid.(all_children), all_children)) + lookup = plot_lookup(scene) return map(matrix) do (uuid, index) !haskey(lookup, uuid) && return (nothing, 0) return (lookup[uuid], Int(index) + 1) @@ -40,6 +39,7 @@ function Makie.pick_closest(scene::Scene, screen::Screen, xy, range::Integer) Promise.all([$(WGL), $(scene)]).then(([WGL, scene]) => WGL.pick_closest(scene, $(xy_vec), $(range))) """) lookup = plot_lookup(scene) + !haskey(lookup, selection[1]) && return (nothing, 0) return (lookup[selection[1]], selection[2] + 1) end @@ -55,11 +55,12 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) isnothing(selection) && return Tuple{Union{Nothing,AbstractPlot},Int}[] lookup = plot_lookup(scene) return map(selection) do (plot_id, index) + !haskey(lookup, plot_id) && return (nothing, 0) return (lookup[plot_id], index + 1) end end -function Makie.pick(scene::Scene, screen::Screen, xy) +function Makie.pick(::Scene, screen::Screen, xy) plot_matrix = pick_native(screen, Rect2i(xy..., 1, 1)) return plot_matrix[1, 1] end diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index ae5a10200b0..d0668a19b6a 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -21825,12 +21825,12 @@ function lines_vertex_shader(uniforms, attributes, is_linesegments) { } // Used to elongate sdf to include joints - // if start/end elongate slightly so that there is no AA gap in loops + // if start/end no elongation // if joint skipped elongate to new length // if normal joint elongate a lot to let shape/truncation handle joint f_extrusion = vec2( - !isvalid[0] ? min(AA_RADIUS, halfwidth) : (adjustment[0] == 0.0 ? 1e12 : halfwidth * abs(extrusion[0])), - !isvalid[3] ? min(AA_RADIUS, halfwidth) : (adjustment[1] == 0.0 ? 1e12 : halfwidth * abs(extrusion[1])) + !isvalid[0] ? 0.0 : (adjustment[0] == 0.0 ? 1e12 : halfwidth * abs(extrusion[0])), + !isvalid[3] ? 0.0 : (adjustment[1] == 0.0 ? 1e12 : halfwidth * abs(extrusion[1])) ); // used to compute width sdf @@ -22903,7 +22903,10 @@ function add_canvas_events(screen, comm, resize_to) { canvas.addEventListener("wheel", wheel); function keydown(event) { comm.notify({ - keydown: event.code + keydown: [ + event.code, + event.key + ] }); return false; } @@ -23121,6 +23124,35 @@ function pick_native(scene, _x, _y, _w, _h) { plots ]; } +function get_picking_buffer(scene) { + const { renderer , picking_target } = scene.screen; + const [w, h] = [ + picking_target.width, + picking_target.height + ]; + renderer.setRenderTarget(picking_target); + set_picking_uniforms(scene, 1, true); + render_scene(scene, true); + renderer.setRenderTarget(null); + const nbytes = w * h * 4; + const pixel_bytes = new Uint8Array(nbytes); + renderer.readRenderTargetPixels(picking_target, 0, 0, w, h, pixel_bytes); + const reinterpret_view = new DataView(pixel_bytes.buffer); + const picked_plots_array = []; + for(let i = 0; i < pixel_bytes.length / 4; i++){ + const id = reinterpret_view.getUint16(i * 4); + const index = reinterpret_view.getUint16(i * 4 + 2); + picked_plots_array.push([ + id, + index + ]); + } + return { + picked_plots_array, + w, + h + }; +} function pick_closest(scene, xy, range) { const { renderer } = scene.screen; const [width, height] = [ @@ -23279,6 +23311,7 @@ export { deserialize_scene as deserialize_scene, threejs_module as threejs_modul export { render_scene as render_scene }; export { wglerror as wglerror }; export { pick_native as pick_native }; +export { get_picking_buffer as get_picking_buffer }; export { pick_closest as pick_closest }; export { pick_sorted as pick_sorted }; export { pick_native_uuid as pick_native_uuid }; diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index d023b735243..110273d7883 100644 --- a/WGLMakie/src/wglmakie.js +++ b/WGLMakie/src/wglmakie.js @@ -276,7 +276,7 @@ function add_canvas_events(screen, comm, resize_to) { function keydown(event) { comm.notify({ - keydown: event.code, + keydown: [event.code, event.key], }); return false; } @@ -566,6 +566,36 @@ export function pick_native(scene, _x, _y, _w, _h) { return [plot_matrix, plots]; } +// For debugging the pixelbuffer +export function get_picking_buffer(scene) { + const { renderer, picking_target } = scene.screen; + const [w, h] = [picking_target.width, picking_target.height]; + // render the scene + renderer.setRenderTarget(picking_target); + set_picking_uniforms(scene, 1, true); + render_scene(scene, true); + renderer.setRenderTarget(null); // reset render target + const nbytes = w * h * 4; + const pixel_bytes = new Uint8Array(nbytes); + //read the pixel + renderer.readRenderTargetPixels( + picking_target, + 0, // x + 0, // y + w, // width + h, // height + pixel_bytes + ); + const reinterpret_view = new DataView(pixel_bytes.buffer); + const picked_plots_array = [] + for (let i = 0; i < pixel_bytes.length / 4; i++) { + const id = reinterpret_view.getUint16(i * 4); + const index = reinterpret_view.getUint16(i * 4 + 2); + picked_plots_array.push([id, index]); + } + return {picked_plots_array, w, h}; +} + export function pick_closest(scene, xy, range) { const { renderer } = scene.screen; const [ width, height ] = [renderer._width, renderer._height]; diff --git a/docs/figure_block.jl b/docs/figure_block.jl index b5fa513f2d8..30a3db017d0 100644 --- a/docs/figure_block.jl +++ b/docs/figure_block.jl @@ -5,6 +5,22 @@ Documenter.Selectors.order(::Type{FigureBlocks}) = 8.0 # like @example Documenter.Selectors.matcher(::Type{FigureBlocks}, node, page, doc) = Documenter.iscode(node, r"^@figure") module MakieDocsHelpers + import ImageTransformations + import Makie + import FileIO + + struct Png + bytes::Vector{UInt8} + size_px::Tuple{Int,Int} + id::String + end + + struct PageInfo + path::String + title::String + end + + FIGURES = Dict{PageInfo,Vector{Png}}() struct AsMIME{M<:MIME,V} mime::M value::V @@ -12,11 +28,105 @@ module MakieDocsHelpers Base.show(io::IO, m::MIME"image/svg+xml", a::AsMIME{MIME"image/svg+xml"}) = show(io,m, a.value) Base.show(io::IO, m::MIME"image/png", a::AsMIME{MIME"image/png"}) = show(io,m, a.value) -end + function register_figure!(page, pagetitle, id, figurelike) + vec = get!(Vector, FIGURES, PageInfo(page, pagetitle)) + Makie.update_state_before_display!(figurelike) + scene = Makie.get_scene(figurelike) + img = Makie.colorbuffer(scene) + backend = nameof(Makie.current_backend()) + px_per_unit = Makie.to_value(Makie.current_default_theme()[backend][:px_per_unit]) + size_px = Tuple(round.(Int, reverse(size(img)) ./ px_per_unit)) + + ntrim = 3 # `restrict` makes dark border pixels which we cut off + img = @view ImageTransformations.restrict(img)[ntrim:end-ntrim,ntrim:end-ntrim] + # img = @view ImageTransformations.restrict(img)[ntrim:end-ntrim,ntrim:end-ntrim] + io = IOBuffer() + FileIO.save(FileIO.Stream{FileIO.format"PNG"}(Makie.raw_io(io)), img) + push!(vec, Png(take!(io), size_px, id)) + return + end + + struct FileInfo + filename::String + id::String + size_px::Tuple{Int,Int} + end + struct OverviewSection + d::Dict{PageInfo,Vector{FileInfo}} + end + + function OverviewSection(page::String) + r = Regex("/$page/") + filtered = filter(pairs(FIGURES)) do (pageinfo, pngs) + match(r, pageinfo.path) !== nothing + end + + fileinfo_dict = Dict{PageInfo,Vector{FileInfo}}() + for (pageinfo, pngs) in pairs(filtered) + fileinfos = map(pngs) do png + filename = "$(string(hash(png.bytes), base = 62)).png" + open(filename, "w") do _io + write(_io, png.bytes) + end + return FileInfo(filename, png.id, png.size_px) + end + fileinfo_dict[pageinfo] = fileinfos + end + + OverviewSection(fileinfo_dict) + end + + function Base.show(io::IO, ::MIME"text/markdown", o::OverviewSection) + pages = sort(collect(keys(o.d)), by = x -> x.path) + for page in pages + fileinfos = o.d[page] + pagename, _ = splitext(basename(page.path)) + println(io, "### $(page.title)") # these links are created too late for Documenter's crossref mechanism, which is good because they should not conflict with the originals + println(io) + println(io, """
""") + for fileinfo in fileinfos + println(io, """ + + + + """) + end + println(io, "
") + println(io) + end + + println(io, """ + + """) + end +end function Documenter.Selectors.runner(::Type{FigureBlocks}, node, page, doc) + title = first(Iterators.filter(page.elements) do el + el isa Markdown.Header{1} + end).text[] + el = node.element infoexpr = Meta.parse(el.info) args = infoexpr.args[3:end] @@ -50,11 +160,27 @@ function Documenter.Selectors.runner(::Type{FigureBlocks}, node, page, doc) expr.args[1] => expr.args[2] end) el.info = "@example $blockname" - el.code = transform_figure_code(el.code; is_continued, kwargs...) + + id = string(hash(el.code), base = 16)[1:7] + el.code = transform_figure_code(el.code; id, page = page.source, pagetitle = title, is_continued, kwargs...) Documenter.Selectors.runner(Documenter.Expanders.ExampleBlocks, node, page, doc) + + last_png = MakieDocsHelpers.FIGURES[MakieDocsHelpers.PageInfo(page.source, title)][end] + @assert last_png.id == id + size_px = last_png.size_px + + mime = get(kwargs, :mime, :png) + image_name = "$id.$mime" + + MarkdownAST.insert_before!(node, @ast Documenter.RawNode(:html, "")) + # we save and insert the image manually, just because we want to be able to set width and height. + # this makes images look sharp as intended, and it improves the accuracy with which one gets to + # image examples from the overview pages, as with annotated width and height the right locations can + # be computed even before all the images have been loaded. Otherwise they are usually wrong the first time. + MarkdownAST.insert_after!(node, @ast Documenter.RawNode(:html, "")) end -function transform_figure_code(code::String; is_continued::Bool, backend::Symbol = :CairoMakie, mime=:png) +function transform_figure_code(code::String; id::String, page::String, pagetitle::String, is_continued::Bool, backend::Symbol = :CairoMakie, mime=:png) backend in (:CairoMakie, :GLMakie) || error("Invalid backend $backend") mimetype = mime == :svg ? "image/svg+xml" : mime == :png ? "image/png" : error("Unknown mimetype $mime") @@ -67,6 +193,8 @@ function transform_figure_code(code::String; is_continued::Bool, backend::Symbol var"#result" = begin # hide $code end # hide - MakieDocsHelpers.AsMIME(MIME"$mimetype"(), var"#result") # hide + MakieDocsHelpers.register_figure!("$page", "$pagetitle", "$id", var"#result") # hide + save("$id.$mime", var"#result") # hide + nothing # hide """ end \ No newline at end of file diff --git a/docs/makedocs.jl b/docs/makedocs.jl index 857651f02de..03cf85c6043 100644 --- a/docs/makedocs.jl +++ b/docs/makedocs.jl @@ -4,6 +4,13 @@ Pkg.activate(".") pkg"dev .. ../MakieCore ../CairoMakie ../GLMakie ../WGLMakie ../RPRMakie" Pkg.precompile() +using CairoMakie +using GLMakie +using WGLMakie +using RPRMakie + +## + include("copy_changelog.jl") using Documenter: Documenter @@ -12,15 +19,9 @@ using Documenter.MarkdownAST: @ast using DocumenterVitepress using Markdown - include("buildutils/deploydocs.jl") include("buildutils/redirect_generation.jl") -using CairoMakie -using WGLMakie -using RPRMakie -using GLMakie - # remove GLMakie's renderloop completely, because any time `GLMakie.activate!()` # is called somewhere, it's reactivated and slows down CI needlessly function GLMakie.renderloop(screen) @@ -46,8 +47,159 @@ deploy_decision = Documenter.DeployDecision(; params.subfolder, ) +function nested_filter(x, regex) + _match(x::String) = match(regex, x) !== nothing + _match(x::Pair) = x[2] isa String ? match(regex, x[2]) !== nothing : true + fn(el::Pair) = el[2] isa Vector ? el[1] => nested_filter(el[2], regex) : el + fn(el) = el + filter(_match, map(fn, x)) +end + +unnest(vec::Vector) = collect(Iterators.flatten([unnest(el) for el in vec])) +unnest(p::Pair) = p[2] isa String ? [p[2]] : unnest(p[2]) +unnest(s::String) = [s] + +pages = [ + "Home" => "index.md", + "Reference" => [ + "Blocks" => [ + "reference/blocks/overview.md", + "reference/blocks/axis.md", + "reference/blocks/axis3.md", + "reference/blocks/box.md", + "reference/blocks/button.md", + "reference/blocks/colorbar.md", + "reference/blocks/gridlayout.md", + "reference/blocks/intervalslider.md", + "reference/blocks/label.md", + "reference/blocks/legend.md", + "reference/blocks/lscene.md", + "reference/blocks/menu.md", + "reference/blocks/polaraxis.md", + "reference/blocks/slider.md", + "reference/blocks/slidergrid.md", + "reference/blocks/textbox.md", + "reference/blocks/toggle.md", + ], + "Plots" => [ + "reference/plots/overview.md", + "reference/plots/ablines.md", + "reference/plots/arc.md", + "reference/plots/arrows.md", + "reference/plots/band.md", + "reference/plots/barplot.md", + "reference/plots/boxplot.md", + "reference/plots/bracket.md", + "reference/plots/contour.md", + "reference/plots/contour3d.md", + "reference/plots/contourf.md", + "reference/plots/crossbar.md", + "reference/plots/datashader.md", + "reference/plots/density.md", + "reference/plots/ecdf.md", + "reference/plots/errorbars.md", + "reference/plots/heatmap.md", + "reference/plots/hexbin.md", + "reference/plots/hist.md", + "reference/plots/hlines.md", + "reference/plots/hspan.md", + "reference/plots/image.md", + "reference/plots/lines.md", + "reference/plots/linesegments.md", + "reference/plots/mesh.md", + "reference/plots/meshscatter.md", + "reference/plots/pie.md", + "reference/plots/poly.md", + "reference/plots/qqnorm.md", + "reference/plots/qqplot.md", + "reference/plots/rainclouds.md", + "reference/plots/rangebars.md", + "reference/plots/scatter.md", + "reference/plots/scatterlines.md", + "reference/plots/series.md", + "reference/plots/spy.md", + "reference/plots/stairs.md", + "reference/plots/stem.md", + "reference/plots/stephist.md", + "reference/plots/streamplot.md", + "reference/plots/surface.md", + "reference/plots/text.md", + "reference/plots/tooltip.md", + "reference/plots/tricontourf.md", + "reference/plots/triplot.md", + "reference/plots/violin.md", + "reference/plots/vlines.md", + "reference/plots/volume.md", + "reference/plots/volumeslices.md", + "reference/plots/voronoiplot.md", + "reference/plots/voxels.md", + "reference/plots/vspan.md", + "reference/plots/waterfall.md", + "reference/plots/wireframe.md", + ], + "Scene" => [ + "reference/scene/lighting.md", + "reference/scene/matcap.md", + "reference/scene/SSAO.md", + ] + ], + "Tutorials" => [ + "tutorials/getting-started.md", + "tutorials/aspect-tutorial.md", + "tutorials/layout-tutorial.md", + "tutorials/scenes.md", + "tutorials/wrap-existing-recipe.md", + ], + "Explanations" => [ + "Backends" => [ + "explanations/backends/backends.md", + "explanations/backends/cairomakie.md", + "explanations/backends/glmakie.md", + "explanations/backends/rprmakie.md", + "explanations/backends/wglmakie.md", + ], + "explanations/animation.md", + "explanations/blocks.md", + "explanations/cameras.md", + "explanations/conversion_pipeline.md", + "explanations/colors.md", + "explanations/dim-converts.md", + "explanations/events.md", + "explanations/figure.md", + "explanations/faq.md", + "explanations/fonts.md", + "explanations/layouting.md", + "explanations/headless.md", + "explanations/inspector.md", + "explanations/latex.md", + "explanations/observables.md", + "explanations/plot_method_signatures.md", + "explanations/recipes.md", + "explanations/scenes.md", + "explanations/specapi.md", + "Theming" => [ + "explanations/theming/themes.md", + "explanations/theming/predefined_themes.md", + ], + "explanations/transparency.md", + ], + "How-Tos" => [ + "how-to/draw-boxes-around-subfigures.md", + "how-to/save-figure-with-transparency.md", + ], + "Resources" => [ + "API" => "api.md", + "Changelog" => "changelog.md", + "Ecosystem" => "ecosystem.md", + ] +] + +empty!(MakieDocsHelpers.FIGURES) + +# filter pages here when working on docs interactively +# pages = nested_filter(pages, r"reference/blocks/(axis|axis3|overview)") + Documenter.makedocs(; - # modules=[Makie], sitename="Makie", format=DocumenterVitepress.MarkdownVitepress(; repo = "https://github.com/MakieOrg/Makie.jl", @@ -57,143 +209,14 @@ Documenter.makedocs(; description = "Create impressive data visualizations with Makie, the plotting ecosystem for the Julia language. Build aesthetic plots with beautiful customizable themes, control every last detail of publication quality vector graphics, assemble complex layouts and quickly prototype interactive applications to explore your data live.", deploy_decision, ), - pages=[ - "Home" => "index.md", - "Reference" => [ - "Blocks" => [ - "reference/blocks/axis.md", - "reference/blocks/axis3.md", - "reference/blocks/box.md", - "reference/blocks/button.md", - "reference/blocks/colorbar.md", - "reference/blocks/gridlayout.md", - "reference/blocks/intervalslider.md", - "reference/blocks/label.md", - "reference/blocks/legend.md", - "reference/blocks/lscene.md", - "reference/blocks/menu.md", - "reference/blocks/polaraxis.md", - "reference/blocks/slider.md", - "reference/blocks/slidergrid.md", - "reference/blocks/textbox.md", - "reference/blocks/toggle.md", - ], - "Plots" => [ - "reference/plots/ablines.md", - "reference/plots/arc.md", - "reference/plots/arrows.md", - "reference/plots/band.md", - "reference/plots/barplot.md", - "reference/plots/boxplot.md", - "reference/plots/bracket.md", - "reference/plots/contour.md", - "reference/plots/contour3d.md", - "reference/plots/contourf.md", - "reference/plots/crossbar.md", - "reference/plots/datashader.md", - "reference/plots/density.md", - "reference/plots/ecdf.md", - "reference/plots/errorbars.md", - "reference/plots/heatmap.md", - "reference/plots/hexbin.md", - "reference/plots/hist.md", - "reference/plots/hlines.md", - "reference/plots/hspan.md", - "reference/plots/image.md", - "reference/plots/lines.md", - "reference/plots/linesegments.md", - "reference/plots/mesh.md", - "reference/plots/meshscatter.md", - "reference/plots/pie.md", - "reference/plots/poly.md", - "reference/plots/qqnorm.md", - "reference/plots/qqplot.md", - "reference/plots/rainclouds.md", - "reference/plots/rangebars.md", - "reference/plots/scatter.md", - "reference/plots/scatterlines.md", - "reference/plots/series.md", - "reference/plots/spy.md", - "reference/plots/stairs.md", - "reference/plots/stem.md", - "reference/plots/stephist.md", - "reference/plots/streamplot.md", - "reference/plots/surface.md", - "reference/plots/text.md", - "reference/plots/tooltip.md", - "reference/plots/tricontourf.md", - "reference/plots/triplot.md", - "reference/plots/violin.md", - "reference/plots/vlines.md", - "reference/plots/volume.md", - "reference/plots/volumeslices.md", - "reference/plots/voronoiplot.md", - "reference/plots/voxels.md", - "reference/plots/vspan.md", - "reference/plots/waterfall.md", - "reference/plots/wireframe.md", - ], - "Scene" => [ - "reference/scene/lighting.md", - "reference/scene/matcap.md", - "reference/scene/SSAO.md", - ] - ], - - "Tutorials" => [ - "tutorials/basic-tutorial.md", - "tutorials/aspect-tutorial.md", - "tutorials/layout-tutorial.md", - "tutorials/scenes.md", - "tutorials/wrap-existing-recipe.md", - ], - "Explanations" => [ - "Backends" => [ - "explanations/backends/backends.md", - "explanations/backends/cairomakie.md", - "explanations/backends/glmakie.md", - "explanations/backends/rprmakie.md", - "explanations/backends/wglmakie.md", - ], - "explanations/animation.md", - "explanations/blocks.md", - "explanations/cameras.md", - "explanations/conversion_pipeline.md", - "explanations/colors.md", - "explanations/dim-converts.md", - "explanations/events.md", - "explanations/figure.md", - "explanations/faq.md", - "explanations/fonts.md", - "explanations/layouting.md", - "explanations/headless.md", - "explanations/inspector.md", - "explanations/latex.md", - "explanations/observables.md", - "explanations/plot_method_signatures.md", - "explanations/recipes.md", - "explanations/scenes.md", - "explanations/specapi.md", - "Theming" => [ - "explanations/theming/themes.md", - "explanations/theming/predefined_themes.md", - ], - "explanations/transparency.md", - ], - "How-Tos" => [ - "how-to/draw-boxes-around-subfigures.md", - "how-to/save-figure-with-transparency.md", - ], - "Resources" => [ - "API" => "api.md", - "Changelog" => "changelog.md", - "Ecosystem" => "ecosystem.md", - ] - ], - warnonly = false, + pages, + expandfirst = unnest(nested_filter(pages, r"reference/(plots|blocks)/(?!overview)")), + warnonly = get(ENV, "CI", "false") != "true", pagesonly = true, ) +## + # DocumenterVitepress moves rendered files from `build/final_site` into `build` on CI by default, but not when running locally generate_redirects([ @@ -205,6 +228,9 @@ generate_redirects([ r"/tutorials/(.*).html" => s"/tutorials/\1/index.html", r"/explanations/(.*).html" => s"/explanations/\1/index.html", "/explanations/observables.html" => "/explanations/nodes/index.html", + "/reference/plots/overview.html" => "/reference/plots/index.html", + "/reference/blocks/overview.html" => "/reference/blocks/index.html", + "/tutorials/getting-started.html" => "/tutorials/basic-tutorial.html", ], dry_run = false) deploy(params; target = "build") diff --git a/docs/src/index.md b/docs/src/index.md index 1b56bd28d3b..a7b815051e2 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -13,7 +13,7 @@ hero: actions: - theme: brand text: Getting started - link: /tutorials/basic-tutorial + link: /tutorials/getting-started - theme: alt text: View on Github link: https://github.com/MakieOrg/Makie.jl @@ -107,7 +107,7 @@ There's no need to install `Makie.jl` separately, it is re-exported by each back ## First Steps -If you are new to Makie, have a look at [Getting started with Makie](@ref). +If you are new to Makie, have a look at [Getting started](@ref). For inspiration, visit [Beautiful Makie](https://beautiful.makie.org/) for a collection of interesting plots. diff --git a/docs/src/reference/blocks/overview.md b/docs/src/reference/blocks/overview.md new file mode 100644 index 00000000000..92bbf3a06a7 --- /dev/null +++ b/docs/src/reference/blocks/overview.md @@ -0,0 +1,7 @@ +# Overview + +```@example +using Markdown # hide +import ..MakieDocsHelpers # hide +MakieDocsHelpers.OverviewSection("blocks") # hide +``` \ No newline at end of file diff --git a/docs/src/reference/plots/datashader.md b/docs/src/reference/plots/datashader.md index 3a8f21f8faf..f24f65a9fe8 100644 --- a/docs/src/reference/plots/datashader.md +++ b/docs/src/reference/plots/datashader.md @@ -11,8 +11,7 @@ datashader ```@figure backend=GLMakie using DelimitedFiles -# For saving/showing/inlining into documentation we need to disable async calculation. -Makie.set_theme!(DataShader = (; async=false)) + airports = Point2f.(eachrow(readdlm(assetpath("airportlocations.csv")))) fig, ax, ds = datashader(airports, colormap=[:white, :black], @@ -194,7 +193,7 @@ Categories are currently aggregated in one Canvas per category, and then overlay normaldist = randn(Point2f, 1_000_000) ds1 = normaldist .+ (Point2f(-1, 0),) ds2 = normaldist .+ (Point2f(1, 0),) -fig, ax, pl = datashader(Dict("a" => ds1, "b" => ds2)) +fig, ax, pl = datashader(Dict("a" => ds1, "b" => ds2); async = false) hidedecorations!(ax) fig ``` diff --git a/docs/src/reference/plots/lines.md b/docs/src/reference/plots/lines.md index a837ab2c343..be3473fac09 100644 --- a/docs/src/reference/plots/lines.md +++ b/docs/src/reference/plots/lines.md @@ -4,65 +4,6 @@ lines ``` - -## Examples - -```@figure -f = Figure() -Axis(f[1, 1]) - -xs = 0:0.01:10 -ys = 0.5 .* sin.(xs) - -lines!(xs, ys) -lines!(xs, ys .- 1, linewidth = 5) -lines!(xs, ys .- 2, linewidth = 5, color = ys) -lines!(xs, ys .- 3, linestyle = :dash) - -f -``` - -### Linestyles - -```@figure -f = Figure() -Axis(f[1, 1]) - -xs = 0:0.01:10 -ys = 0.5 .* sin.(xs) - -for (i, lw) in enumerate([1, 2, 3]) - lines!(xs, ys .- i/6, linestyle = nothing, linewidth = lw) - lines!(xs, ys .- i/6 .- 1, linestyle = :dash, linewidth = lw) - lines!(xs, ys .- i/6 .- 2, linestyle = :dot, linewidth = lw) - lines!(xs, ys .- i/6 .- 3, linestyle = :dashdot, linewidth = lw) - lines!(xs, ys .- i/6 .- 4, linestyle = :dashdotdot, linewidth = lw) - lines!(xs, ys .- i/6 .- 5, linestyle = Linestyle([0.5, 1.0, 1.5, 2.5]), linewidth = lw) -end - -f -``` - -### Linecaps and Joinstyles - -```@figure -f = Figure() -Axis(f[1, 1]) - -ps = 0.8 .* Point2f[(-0.2, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5), (-0.5, -0.2)] - -for i in 1:3, j in 1:3 - lines!( - ps .+ Point2f(i, -j), linewidth = 20, - linecap = (:butt, :square, :round)[i], - joinstyle = (:miter, :bevel, :round)[j] - ) - scatterlines!(ps .+ Point2f(i, -j), color = :gray) -end - -f -``` - ### Dealing with outline artifacts in GLMakie In GLMakie 3D line plots can generate outline artifacts depending on the order line segments are rendered in. diff --git a/docs/src/reference/plots/overview.md b/docs/src/reference/plots/overview.md new file mode 100644 index 00000000000..6b3ff48f265 --- /dev/null +++ b/docs/src/reference/plots/overview.md @@ -0,0 +1,7 @@ +# Overview + +```@example +using Markdown # hide +import ..MakieDocsHelpers # hide +MakieDocsHelpers.OverviewSection("plots") # hide +``` \ No newline at end of file diff --git a/docs/src/tutorials/basic-tutorial.md b/docs/src/tutorials/basic-tutorial.md deleted file mode 100644 index 64a8c6d2513..00000000000 --- a/docs/src/tutorials/basic-tutorial.md +++ /dev/null @@ -1,457 +0,0 @@ -# Getting started with Makie - -## Preface - -Here is a quick tutorial to get you started with Makie! - -Makie is the name of the whole plotting ecosystem and `Makie.jl` is the main package that describes how plots work. -To actually render and save plots, we need a backend that knows how to translate plots into images or vector graphics. - -There are three main backends which you can use to render plots (for more information, have a look at [What is a backend](@ref)): - -- `CairoMakie.jl` if you want to render vector graphics or high quality 2D images and don't need interactivity or true 3D rendering. -- `GLMakie.jl` if you need interactive windows and true 3D rendering but no vector output. -- Or `WGLMakie.jl` which is similar to `GLMakie` but works in web browsers, not native windows. - -This tutorial uses CairoMakie, but the code can be executed with any backend. -Note that CairoMakie can _create_ images but it cannot _display_ them. - -To see the output of plotting commands when using CairoMakie, we recommend you either use an IDE which supports png or svg output, such as VSCode, Atom/Juno, Jupyter, Pluto, etc., or try using a viewer package such as [ElectronDisplay.jl](https://github.com/queryverse/ElectronDisplay.jl), or alternatively save your plots to files directly. -The Julia REPL by itself does not have the ability to show the plots. - -Ok, now that this is out of the way, let's get started! - -## Importing - -First, we import CairoMakie. This makes all the exported symbols from `Makie.jl` available as well. - -```@example basic -using CairoMakie -CairoMakie.activate!() # hide - -nothing # hide -``` - -## Important objects - -The objects most important for our first steps with Makie are the `Figure`, the `Axis` and plots. -In a normal Makie plot you will usually find a `Figure` which contains an `Axis` which contains one or more plot objects like `Lines` or `Scatter`. - -In the next steps, we will take a look at how we can create these objects. - -## An empty figure - -The basic container object in Makie is the [`Figure`](@ref). -It is a canvas onto which we can add objects like `Axis`, `Colorbar`, `Legend` and others. - -Let's create a `Figure` and give it a background color other than the default white so we can see it. -Returning a `Figure` from an expression will `display` it if your coding environment can show images. - -```@figure basic -f = Figure(backgroundcolor = :tomato) -``` - -Another common thing to do is to give a figure a different size. -The default is 800x600, let's try halving the height: - -```@figure basic -f = Figure(backgroundcolor = :tomato, size = (800, 300)) -``` - -## Adding an Axis - -The most common object you can add to a figure which you need for most plotting is the [Axis](@ref). -The usual syntax for adding such an object to a figure is to specify a position in the `Figure`'s layout as the first argument. -We'll learn more about layouts later, but for now the position `f[1, 1]` will just fill the whole figure. - -```@figure basic -f = Figure() -ax = Axis(f[1, 1]) -f -``` - -The default axis has no title or labels, you can pass those as keyword arguments. -For a whole list of available attributes, check the docstring for [`Axis`](@ref) (you can also do that by running `?Axis` in the REPL). -Be warned, it's very long! - -```@figure basic -f = Figure() -ax = Axis(f[1, 1], - title = "A Makie Axis", - xlabel = "The x label", - ylabel = "The y label" -) -f -``` - -## Adding a plot to an Axis - -Now we're ready to actually plot something into an `Axis`! - -Makie has many different plotting functions, the first we will learn about is [`lines!`](@ref). -Let's try plotting a sine function into an `Axis`, by passing it as the first argument: - -```@figure basic -f = Figure() -ax = Axis(f[1, 1]) -x = range(0, 10, length=100) -y = sin.(x) -lines!(ax, x, y) -f -``` - -There we have our first line plot. - -## Scatter plot - -Another common function is [`scatter!`](@ref). -It works very similar to [`lines!`](@ref) but shows separate markers for each input point. - -```@figure basic -f = Figure() -ax = Axis(f[1, 1]) -x = range(0, 10, length=100) -y = sin.(x) -scatter!(ax, x, y) -f -``` - -## Creating Figure, Axis and plot in one call - -So far we have seen how to plot into an existing `Axis` with `lines!` and `scatter!`. - -However, it would be nice if we didn't have to explicitly create `Figure` and `Axis` for every plot that we're making. - -That's why every plotting function comes in a pair, one version that plots into an existing `Axis` and one that creates its own `Axis` implicitly for convenience. -For example, `lines!` mutates an existing `Axis`, `lines` creates an implicit one, `scatter!` mutates, `scatter` does not, and so on. - -Let's see how to make a line plot without creating `Figure` and `Axis` ourselves first. - -```@figure basic -x = range(0, 10, length=100) -y = sin.(x) -lines(x, y) -``` - -The return type of `lines(x, y)` is `FigureAxisPlot`. -The `lines` function first creates a `Figure`, then puts an `Axis` into it and finally adds a plot of type `Lines` to that axis. - -Because these three objects are created at once, the function returns all three, just bundled up into one `FigureAxisPlot` object. -That's just so we can overload the `display` behavior for that type to match `Figure`. -Normally, multiple return values are returned as `Tuple`s in Julia but it's uncommon to overload `display` for `Tuple` types. - -If you need the objects, for example to add more things to the figure later and edit axis and plot attributes, you could destructure the return value: - -```@figure basic -figure, axis, lineplot = lines(x, y) -figure -``` - -As you can see, the output of returning the extracted figure is the same. - -## Passing Figure and Axis styles - -You might wonder how to specify a different resolution for this scatter plot, or set an axis title and labels. -Because a normal plotting function like `lines` or `scatter` creates these objects before it creates the plot, you can pass special keyword arguments to it called `axis` and `figure`. -You can pass any kind of object with symbol-value pairs and these will be used as keyword arguments for `Figure` and `Axis`, respectively. - -```@figure basic -x = range(0, 10, length=100) -y = sin.(x) -scatter(x, y; - figure = (; size = (400, 400)), - axis = (; title = "Scatter plot", xlabel = "x label") -) -``` - -The `;` in `(; size = (400, 400))` is nothing special, it just clarifies that we want a one-element `NamedTuple` and not a variable called `size`. -It's good habit to include it but it's not needed for `NamedTuple`s with more than one entry. - -## Argument conversions - -So far we have called `lines` and `scatter` with `x` and `y` arguments, where `x` was a range object and `y` vector of numbers. -Most plotting functions have different options how you can call them. -The input arguments are converted internally to one or more target representations that can be handled by the rendering backends. - -Here are a few different examples of what you can use with `lines`: - -An interval and a function: - -```@figure basic -lines(0..10, sin) -``` - -A collection of numbers and a function: - -```@figure basic -lines(0:1:10, cos) -``` - -A collection of `Point`s from `GeometryBasics.jl` (which supplies most geometric primitives in Makie): - -```@figure basic -lines([Point(0, 0), Point(5, 10), Point(10, 5)]) -``` - -The input arguments you can use with `lines` and `scatter` are mostly the same because they have the same conversion trait `PointBased`. -Other plotting functions have different conversion traits, [heatmap](@ref) for example expects two-dimensional grid data. -The respective trait is called `CellGrid`. - -## Layering multiple plots - -As we've seen above, every plotting function has a version with and one without `!` at the end. -For example, there's `scatter` and `scatter!`, `lines` and `lines!`, etc. - -To plot two things into the same axis, you can use the mutating plotting functions like `lines!` and `scatter!`. -For example, here's how you could plot two lines on top of each other: - -```@figure basic -x = range(0, 10, length=100) - -f, ax, l1 = lines(x, sin) -l2 = lines!(ax, x, cos) -f -``` - -The second `lines!` call plots into the axis created by the first `lines` call. -It's colored differently because the `Axis` keeps track of what has been plotted into it and cycles colors for similar plotting functions. - -You can also leave out the axis argument for convenience, then the axis being used is the `current_axis()`, which is usually just the axis that was created last. - -```@figure basic -x = range(0, 10, length=100) - -f, ax, l1 = lines(x, sin) -lines!(x, cos) -f -``` - -Note that you cannot pass `figure` and `axis` keywords to mutating plotting functions like `lines!` or `scatter!`. -That's because they don't create an `Figure` and `Axis`, and we chose not to allow modification of the existing objects in plotting calls so it's clearer what is going on. - -## Attributes - -Every plotting function has attributes which you can set through keyword arguments. -The lines in the previous example have colors from Makie's default palette, but we can easily specify our own. - -There are multiple ways you can specify colors, but common ones are: - -- By name, like `:red` or `"red"` -- By hex string, like `"#ffccbk"` -- With color types like the Makie-exported `RGBf(0.5, 0, 0.6)` or `RGBAf(0.3, 0.8, 0.2, 0.8)` -- As a tuple where the first part is a color and the second an alpha value to make it transparent, like `(:red, 0.5)` - -You can read more about colors at [juliagraphics.github.io/Colors.jl](https://juliagraphics.github.io/Colors.jl). - -Here's a plot with one named color and one where we use `RGBf`: - -```@figure basic -x = range(0, 10, length=100) - -f, ax, l1 = lines(x, sin, color = :tomato) -l2 = lines!(ax, x, cos, color = RGBf(0.2, 0.7, 0.9)) -f -``` - -Other plotting functions have different attributes. -The function `scatter`, for example, does not only have the `color` attribute, but also a `markersize` attribute. - -You can read about all possible attributes by running `?scatter` in the REPL, and examples are shown on the page [scatter](@ref). - -```@figure basic -x = range(0, 10, length=100) - -f, ax, sc1 = scatter(x, sin, color = :red, markersize = 5) -sc2 = scatter!(ax, x, cos, color = :blue, markersize = 10) -f -``` - -You can also manipulate most plot attributes afterwards with the syntax `plot.attribute = new_value`. - -```@figure basic -sc1.marker = :utriangle -sc1.markersize = 20 - -sc2.color = :transparent -sc2.markersize = 20 -sc2.strokewidth = 1 -sc2.strokecolor = :purple - -f -``` - -## Array attributes - -A lot of attributes can be set to either a single value or an array with as many elements as there are data points. -For example, it is usually much more performant to draw many points with one scatter object, than to create many scatter objects with one point each. - -Here, we vary markersize and color: - -```@figure basic -x = range(0, 10, length=100) - -scatter(x, sin, - markersize = range(5, 15, length=100), - color = range(0, 1, length=100), - colormap = :thermal -) -``` - -Note that the color array does not actually contain colors, rather the numerical values are mapped to the plot's `colormap`. -There are many different colormaps to choose from, take a look on the [Colors](@ref) page. - -The values are mapped to colors via the `colorrange` attribute, which by default goes from the minimum to the maximum color value. -But we can also limit or expand the range manually. -For example, we can constrain the previous scatter plot's color range to (0.33, 0.66), which will clip the colors at the bottom and the top. - -```@figure basic -x = range(0, 10, length=100) - -scatter(x, sin, - markersize = range(5, 15, length=100), - color = range(0, 1, length=100), - colormap = :thermal, - colorrange = (0.33, 0.66) -) -``` - -Of course you can also use an array of colors directly, in which case the `colorrange` is ignored: - -```@figure basic - -using CairoMakie - -x = range(0, 10, length=100) - -colors = repeat([:crimson, :dodgerblue, :slateblue1, :sienna1, :orchid1], 20) - -scatter(x, sin, color = colors, markersize = 20) -``` - -## Simple legend - -If you add label attributes to your plots, you can call the `axislegend` function to add a `Legend` with all labeled plots to the current `Axis`, or optionally to one you pass as the first argument. - -```@figure basic - -using CairoMakie - -x = range(0, 10, length=100) - -lines(x, sin, color = :red, label = "sin") -lines!(x, cos, color = :blue, label = "cos") -axislegend() -current_figure() -``` - -## Subplots - -Makie uses a powerful layout system under the hood, which allows you to create very complex figures with many subplots. -So far, we have only used the default position [1, 1], where the Axis is created in a standard plotting call. - -We can make subplots by giving the location of the subplot in our layout grid as the first argument to our plotting function. -The basic syntax for specifying the location in a figure is `fig[row, col]`. - -```@figure basic - -using CairoMakie - -x = LinRange(0, 10, 100) -y = sin.(x) - -fig = Figure() -lines(fig[1, 1], x, y, color = :red) -lines(fig[1, 2], x, y, color = :blue) -lines(fig[2, 1:2], x, y, color = :green) - -fig -``` - -Each `lines` call creates a new axis in the position given as the first argument, that's why we use `lines` and not `lines!` here. - -We can also create a couple of axes manually at first and then plot into them later. -For example, we can create a figure with three axes. - -```@figure basic - -using CairoMakie - -fig = Figure() -ax1 = Axis(fig[1, 1]) -ax2 = Axis(fig[1, 2]) -ax3 = Axis(fig[2, 1:2]) -fig -``` - -And then we can continue to plot into these empty axes. - -```@figure basic - -lines!(ax1, 0..10, sin) -lines!(ax2, 0..10, cos) -lines!(ax3, 0..10, sqrt) -fig -``` - -## Legend and Colorbar - -We have seen two `Blocks` so far, the [Axis](@ref) and the [Legend](@ref) which was created by the function `axislegend`. -All `Block`s can be placed into the layout of a figure at arbitrary positions, which makes it easy to assemble complex figures. - -In the same way as with the [Axis](@ref) before, you can also create a [Legend](@ref) manually and then place it freely, wherever you want, in the figure. -There are multiple ways to create [Legend](@ref)s, for one of them you pass one vector of plot objects and one vector of label strings. - -You can see here that we can deconstruct the return value from the two `lines` calls into one newly created axis and one plot object each. -We can then feed the plot objects to the legend constructor. -We place the legend in the second column and across both rows, which centers it nicely next to the two axes. - -```@figure basic - -using CairoMakie - -fig = Figure() -ax1, l1 = lines(fig[1, 1], 0..10, sin, color = :red) -ax2, l2 = lines(fig[2, 1], 0..10, cos, color = :blue) -Legend(fig[1:2, 2], [l1, l2], ["sin", "cos"]) -fig -``` - -The [Colorbar](@ref) works in a very similar way. -We just need to pass a position in the figure to it, and one plot object. -In this example, we use a `heatmap`. - -You can see here that we split the return value of `heatmap` into three parts: the newly created figure, the axis and the heatmap plot object. -This is useful as we can then continue with the figure `fig` and the heatmap `hm` which we need for the colorbar. - -```@figure basic - -using CairoMakie - -fig, ax, hm = heatmap(randn(20, 20)) -Colorbar(fig[1, 2], hm) -fig -``` - -The previous short syntax is basically equivalent to this longer, manual version. -You can switch between those workflows however you please. - -```@figure basic - -using CairoMakie - -fig = Figure() -ax = Axis(fig[1, 1]) -hm = heatmap!(ax, randn(20, 20)) -Colorbar(fig[1, 2], hm) -fig -``` - -## Next steps - -We've only looked at a small subset of Makie's functionality here. - -If you want to learn about making complex figures with nested sublayouts, have a look at the tutorial [Creating complex layouts](@ref). - -If you're interested in creating interactive visualizations that use Makie's special `Observables` workflow, this is explained in more detail in the [Observables](@ref) section. - -If you want to create animated movies, you can find more information in the [Animations](@ref) section. diff --git a/docs/src/tutorials/getting-started.md b/docs/src/tutorials/getting-started.md new file mode 100644 index 00000000000..a5a14b1f4db --- /dev/null +++ b/docs/src/tutorials/getting-started.md @@ -0,0 +1,210 @@ +# Getting started + +Welcome to Makie, the data visualization ecosystem for the Julia language! + +This tutorial will show you how to get set up and create plots like this: + +![](./first_figure.svg) + +## Requirements + +You only need an internet connection and a reasonably recent Julia installation. +If you don't have Julia installed, yet, follow the directions at [julialang.org/downloads/](https://julialang.org/downloads/). + +Makie is available for Windows, Mac and Linux. + +## Installation + +We will be using the [CairoMakie](@ref) package in this tutorial. + +!!! info + Makie offers multiple [backend packages](@ref "What is a backend") that each have different strengths. + CairoMakie is good at static 2D graphics and it should run on most computers as it uses only the CPU and does not need a GPU. + +First, create a new folder somewhere on your system and call it `makie_tutorial`. +We are going to use that folder to install CairoMakie and to save plots. + +Now, start Julia, for example by executing the command `julia` in a terminal. + +In the Julia REPL (the **R**ead-**E**val-**P**rint-**L**oop which is what Julia's command line interface is called), change the active working directory to the `makie_tutorial` folder by executing this command, but be sure to replace the path with the location where you created the `makie_tutorial` folder: + +```julia +cd("path/to/the/folder/makie_tutorial") +``` + +Now, make the `Pkg` package manager library available + +```julia +using Pkg +``` + +Next, activate the current directory, also called `"."` (this means our `makie_tutorial` folder), as a Pkg environment: + +```julia +Pkg.activate(".") +``` + +Now, we can install CairoMakie and all its dependencies by running: + +```julia +Pkg.add("CairoMakie") +``` + +This command will probably take a while to finish. You will need an internet connection so all the necessary files can be downloaded. + +After this process has completed, you should find a `Project.toml` and a `Manifest.toml` file in the `makie_tutorial` folder. +Those files describe the new environment, the downloaded packages are stored somewhere else, in a central, shared location. + +If everything has worked, you should be able to load CairoMakie now: + +```@example tutorial +using CairoMakie +``` + +Congratulations, now we can start plotting! + +## Plotting + +Run these two lines to make the "data" for our first plot available in your Julia session. +It represents some imaginary measurements made over the span of two seconds. + +```@example tutorial +seconds = 0:0.1:2 +measurements = [8.2, 8.4, 6.3, 9.5, 9.1, 10.5, 8.6, 8.2, 10.5, 8.5, 7.2, + 8.8, 9.7, 10.8, 12.5, 11.6, 12.1, 12.1, 15.1, 14.7, 13.1] +nothing # hide +``` + +Let's have a first look at this data as a line plot. +Line plots are created with the [lines](@ref) function in Makie. + +```@figure tutorial +lines(seconds, measurements) +``` + +!!! info + Returning `lines(seconds, measurements)` in the REPL should show you the plot in some form. + Which form it is depends on the context in which you have your Julia REPL running. + + If you are in an IDE like VSCode with the Julia extension installed, the plot pane might have opened. + If no other display is found, your OS's image viewing application or a browser should show the image. + +Let's try another plot function, to show each data point as a separate marker. +The right function for that is [scatter](@ref). + +```@figure tutorial +scatter(seconds, measurements) +``` + +Our goal is to show the measurement data together with a line representing an exponential fit. +Let us pretend that the function we have "fit" is `f(x) = exp(x) + 7`. +We can plot it as a line like this: + +```@figure tutorial +lines(seconds, exp.(seconds) .+ 7) +``` + +Now, we'd like to have the scatter and lines plots layered on top of each other. + +You can plot into an existing axis with plotting functions that end with a `!`: + +```@figure tutorial +scatter(seconds, measurements) +lines!(seconds, exp.(seconds) .+ 7) +current_figure() +``` + +## Figure and Axis + +So far, we have used two important objects in Makie only implicitly, the [Figure](@ref Figures) and the [Axis](@ref). + +The `Figure` is the outermost container object. And an `Axis` is one type of axis object that can contain plots. An `Axis` can be placed in a `Figure` and then be plotted into. +Let's try the previous plot with this system: + +```@figure tutorial +f = Figure() +ax = Axis(f[1, 1]) +scatter!(ax, seconds, measurements) +lines!(ax, seconds, exp.(seconds) .+ 7) +f +``` + +Both `scatter!` and `lines!` now explicitly plot into an `Axis` which we put into a `Figure`. +`Axis(f[1, 1])` means that we put the `Axis` at the `Figure`'s layout at position row 1, column 1. + +We can now give our `Axis` a title, as well as x and y axis labels: + +```@figure tutorial +f = Figure() +ax = Axis(f[1, 1], + title = "Experimental data and exponential fit", + xlabel = "Time (seconds)", + ylabel = "Value", +) +scatter!(ax, seconds, measurements) +lines!(ax, seconds, exp.(seconds) .+ 7) +f +``` + +## Plot styling + +Plotting functions take many different style attributes as keyword arguments. +Let's change the color of both plots to a red called `:tomato`, and the line style to `:dash`: + +```@figure tutorial +f = Figure() +ax = Axis(f[1, 1], + title = "Experimental data and exponential fit", + xlabel = "Time (seconds)", + ylabel = "Value", +) +scatter!(ax, seconds, measurements, color = :tomato) +lines!(ax, seconds, exp.(seconds) .+ 7, color = :tomato, linestyle = :dash) +f +``` + +## Legend + +The last element we're missing is the legend. +One way to create a legend is by labelling plots with the `label` keyword and using the [`axislegend`](@ref) function: + +```@figure tutorial +f = Figure() +ax = Axis(f[1, 1], + title = "Experimental data and exponential fit", + xlabel = "Time (seconds)", + ylabel = "Value", +) +scatter!( + ax, + seconds, + measurements, + color = :tomato, + label = "Measurements" +) +lines!( + ax, + seconds, + exp.(seconds) .+ 7, + color = :tomato, + linestyle = :dash, + label = "f(x) = exp(x) + 7", +) +axislegend(position = :rb) +f +``` + +## Saving a Figure + +Once we are satisfied with our plot, we can save it to a file using the [`save`](@ref) function. +The most common formats are `png` for images and `svg` or `pdf` for vector graphics: + +```@example tutorial +save("first_figure.png", f) +save("first_figure.svg", f) +save("first_figure.pdf", f) +nothing # hide +``` + +You should now find the three files in your `makie_tutorial` folder. + diff --git a/src/Makie.jl b/src/Makie.jl index 92372ee68f9..9433cdfeffe 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -183,6 +183,7 @@ include("basic_recipes/wireframe.jl") include("basic_recipes/tooltip.jl") include("basic_recipes/makiecore_examples/scatter.jl") +include("basic_recipes/makiecore_examples/lines.jl") # layouting of plots include("layouting/transformation.jl") diff --git a/src/basic_recipes/makiecore_examples/lines.jl b/src/basic_recipes/makiecore_examples/lines.jl new file mode 100644 index 00000000000..5bde21b0c4a --- /dev/null +++ b/src/basic_recipes/makiecore_examples/lines.jl @@ -0,0 +1,124 @@ +function attribute_examples(::Type{Lines}) + Dict( + :linestyle => [ + Example( + code = """ + linestyles = [:solid, :dot, :dash, :dashdot, :dashdotdot] + gapstyles = [:normal, :dense, :loose, 10] + fig = Figure() + with_updates_suspended(fig.layout) do + for (i, ls) in enumerate(linestyles) + for (j, gs) in enumerate(gapstyles) + title = gs === :normal ? repr(ls) : "\$((ls, gs))" + ax = Axis(fig[i, j]; title, yautolimitmargin = (0.2, 0.2)) + hidedecorations!(ax) + hidespines!(ax) + linestyle = (ls, gs) + for linewidth in 1:3 + lines!(ax, 1:10, fill(linewidth, 10); linestyle, linewidth) + end + end + end + end + fig + """ + ), + Example( + code = """ + fig = Figure() + patterns = [ + [0, 1, 2], + [0, 20, 22], + [0, 2, 4, 12, 14], + [0, 2, 4, 6, 8, 10, 20], + [0, 1, 2, 4, 6, 9, 12], + [0.0, 4.0, 6.0, 9.5], + ] + ax = Axis(fig[1, 1], yautolimitmargin = (0.2, 0.2)) + for (i, pattern) in enumerate(patterns) + lines!(ax, [-i, -i], linestyle = Linestyle(pattern), linewidth = 4) + text!(ax, (1.5, -i), text = "Linestyle(\$pattern)", + align = (:center, :bottom), offset = (0, 10)) + end + hidedecorations!(ax) + fig + """ + ), + ], + :joinstyle => [ + Example( + code = """ + fig = Figure() + ax = Axis(fig[1, 1], yautolimitmargin = (0.05, 0.15)) + hidedecorations!(ax) + + joinstyles = [:miter, :bevel, :round] + for (i, joinstyle) in enumerate(joinstyles) + x = (1:3) .+ 5 * (i - 1) + ys = [[0.5, 3.5, 0.5], [3, 5, 3], [5, 6, 5], [6.5, 7, 6.5]] + for y in ys + lines!(ax, x, y; linewidth = 15, joinstyle, color = :black) + end + text!(ax, x[2], ys[end][2], text = ":\$joinstyle", + align = (:center, :bottom), offset = (0, 15), font = :bold) + end + + text!(ax, 4.5, 4.5, text = "for angles\nbelow miter_limit,\n:miter == :bevel", + align = (:center, :center)) + + fig + """ + ), + ], + :linecap => [ + Example( + code = """ + fig = Figure() + ax = Axis(fig[1, 1], yautolimitmargin = (0.2, 0.2), xautolimitmargin = (0.2, 0.2)) + hidedecorations!(ax) + + linecaps = [:butt, :square, :round] + for (i, linecap) in enumerate(linecaps) + lines!(ax, [i, i]; color = :tomato, linewidth = 15, linecap) + lines!(ax, [i, i]; color = :black, linewidth = 15, linecap = :butt) + text!(1.5, i, text = ":\$linecap", font = :bold, + align = (:center, :bottom), offset = (0, 15)) + end + fig + """ + ), + ], + :color => [ + Example( + code = """ + fig = Figure() + ax = Axis(fig[1, 1], yautolimitmargin = (0.1, 0.1), xautolimitmargin = (0.1, 0.1)) + hidedecorations!(ax) + + lines!(ax, 1:9, iseven.(1:9) .- 0; color = :tomato) + lines!(ax, 1:9, iseven.(1:9) .- 1; color = (:tomato, 0.5)) + lines!(ax, 1:9, iseven.(1:9) .- 2; color = 1:9) + lines!(ax, 1:9, iseven.(1:9) .- 3; color = 1:9, colormap = :plasma) + lines!(ax, 1:9, iseven.(1:9) .- 4; color = RGBf.(0, (0:8) ./ 8, 0)) + fig + """ + ), + ], + :linewidth => [ + Example( + code = """ + fig = Figure() + ax = Axis(fig[1, 1], yautolimitmargin = (0.2, 0.2), xautolimitmargin = (0.1, 0.1)) + hidedecorations!(ax) + + for linewidth in 1:10 + lines!(ax, iseven.(1:9) .+ linewidth, 1:9; color = :black, linewidth) + text!(ax, linewidth + 0.5, 9; text = "\$linewidth", font = :bold, + align = (:center, :bottom), offset = (0, 15)) + end + fig + """ + ), + ], + ) +end \ No newline at end of file diff --git a/src/basic_recipes/poly.jl b/src/basic_recipes/poly.jl index c7d4430e366..ff3f92fed65 100644 --- a/src/basic_recipes/poly.jl +++ b/src/basic_recipes/poly.jl @@ -135,7 +135,9 @@ end function to_lines(polygon::AbstractVector{<: VecTypes}) result = Point2d.(polygon) - isempty(result) || push!(result, polygon[1]) + if !isempty(result) && !(result[1] ≈ result[end]) + push!(result, polygon[1]) + end return result end @@ -175,7 +177,6 @@ function plot!(plot::Poly{<: Tuple{<: Union{Polygon, AbstractVector{<: PolyEleme return sc end end - lines!( plot, outline, visible = plot.visible, color = stroke, linestyle = plot.linestyle, alpha = plot.alpha, diff --git a/src/basic_recipes/tooltip.jl b/src/basic_recipes/tooltip.jl index 32802bb027a..fb075d609b6 100644 --- a/src/basic_recipes/tooltip.jl +++ b/src/basic_recipes/tooltip.jl @@ -206,44 +206,44 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) shift = if placement === :left Vec2f[ - (l, b + 0.5h), (l, t), (r, t), + (l, b), (l, t), (r, t), (r, b + align * h + 0.5s), (r + s, b + align * h), (r, b + align * h - 0.5s), - (r, b), (l, b), (l, b + 0.5h) + (r, b), (l, b) ] elseif placement === :right Vec2f[ - (l + 0.5w, b), (l, b), + (r, b), (l, b), (l, b + align * h - 0.5s), (l-s, b + align * h), (l, b + align * h + 0.5s), - (l, t), (r, t), (r, b), (l + 0.5w, b) + (l, t), (r, t), (r, b) ] elseif placement in (:below, :down, :bottom) Vec2f[ - (l, b + 0.5h), (l, t), + (l, b), (l, t), (l + align * w - 0.5s, t), (l + align * w, t+s), (l + align * w + 0.5s, t), - (r, t), (r, b), (l, b), (l, b + 0.5h) + (r, t), (r, b), (l, b) ] elseif placement in (:above, :up, :top) Vec2f[ - (l, b + 0.5h), (l, t), (r, t), (r, b), + (l, b), (l, t), (r, t), (r, b), (l + align * w + 0.5s, b), (l + align * w, b-s), (l + align * w - 0.5s, b), - (l, b), (l, b + 0.5h) + (l, b) ] else @error "Tooltip placement $placement invalid. Assuming :above" Vec2f[ - (l, b + 0.5h), (l, t), (r, t), (r, b), + (l, b), (l, t), (r, t), (r, b), (l + align * w + 0.5s, b), (l + align * w, b-s), (l + align * w - 0.5s, b), - (l, b), (l, b + 0.5h) + (l, b) ] end @@ -252,7 +252,7 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) lines!( p, outline, - color = p.outline_color, space = :pixel, + color = p.outline_color, space = :pixel, miter_limit = pi/18, linewidth = p.outline_linewidth, linestyle = p.outline_linestyle, transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, diff --git a/src/conversions.jl b/src/conversions.jl index 7b0812abf3d..3447c6ad8a5 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -927,12 +927,18 @@ end A type that can be used as value for the `linestyle` keyword argument of plotting functions to arbitrarily customize the linestyle. -The `value` is a vector of positions where the line flips from being drawn or not -and vice versa. The values of `value` are in units of linewidth. +The `value` is a vector specifying the boundaries of the dashes in the line. +Values 1 and 2 demarcate the first dash, values 2 and 3 the first gap, and so on. +This means that usually, a pattern should have an odd number of values so that there's +always a gap after a dash. -For example, with `value = [0.0, 4.0, 6.0, 9.5]` -you start drawing at 0, stop at 4 linewidths, start again at 6, stop at 9.5, -then repeat with 0 and 9.5 being treated as the same position. +Here's an example in ASCII code. If we specify `[0, 3, 6, 11, 16]` then we get the following +pattern: + +``` +# 0 3 6 11 16 3 6 11 +# --- ----- --- ----- +``` """ struct Linestyle value::Vector{Float32} diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index 0c66f001b00..0331e39eb07 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -189,11 +189,12 @@ mutable struct DataInspector selection::AbstractPlot obsfuncs::Vector{Any} + lock::Threads.ReentrantLock end function DataInspector(scene::Scene, plot::AbstractPlot, attributes) - x = DataInspector(scene, attributes, AbstractPlot[], plot, plot, Any[]) + x = DataInspector(scene, attributes, AbstractPlot[], plot, plot, Any[], Threads.ReentrantLock()) # finalizer(cleanup, x) # doesn't get triggered when this is dereferenced x end @@ -297,31 +298,33 @@ DataInspector(; kwargs...) = DataInspector(current_figure(); kwargs...) function on_hover(inspector) parent = inspector.root - (inspector.attributes.enabled[] && is_mouseinside(parent)) || return Consume(false) - - mp = mouseposition_px(parent) - should_clear = true - for (plt, idx) in pick_sorted(parent, mp, inspector.attributes.range[]) - if to_value(get(plt.attributes, :inspectable, true)) - # show_data should return true if it created a tooltip - if show_data_recursion(inspector, plt, idx) - should_clear = false - break + lock(inspector.lock) do + (inspector.attributes.enabled[] && is_mouseinside(parent)) || return Consume(false) + + mp = mouseposition_px(parent) + should_clear = true + for (plt, idx) in pick_sorted(parent, mp, inspector.attributes.range[]) + if to_value(get(plt.attributes, :inspectable, true)) + # show_data should return true if it created a tooltip + if show_data_recursion(inspector, plt, idx) + should_clear = false + break + end end end - end - if should_clear - plot = inspector.selection - if to_value(get(plot, :inspector_clear, automatic)) !== automatic - plot[:inspector_clear][](inspector, plot) + if should_clear + plot = inspector.selection + if to_value(get(plot, :inspector_clear, automatic)) !== automatic + plot[:inspector_clear][](inspector, plot) + end + inspector.plot.visible[] = false + inspector.attributes.indicator_visible[] = false + inspector.plot.offset.val = inspector.attributes.offset[] end - inspector.plot.visible[] = false - inspector.attributes.indicator_visible[] = false - inspector.plot.offset.val = inspector.attributes.offset[] - end - return Consume(false) + return Consume(false) + end end diff --git a/src/interfaces.jl b/src/interfaces.jl index fe7d6fe60ce..273dc91435b 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -155,7 +155,7 @@ function apply_expand_dimensions(trait, args, args_obs, deregister) if isnothing(expanded) return args_obs else - new_obs = map(Observable, expanded) + new_obs = map(Observable{Any}, expanded) fs = onany(args_obs...) do args... expanded = expand_dimensions(trait, args...) for (obs, arg) in zip(new_obs, expanded) diff --git a/src/makielayout/mousestatemachine.jl b/src/makielayout/mousestatemachine.jl index b3fe3da35a7..42d09b54365 100644 --- a/src/makielayout/mousestatemachine.jl +++ b/src/makielayout/mousestatemachine.jl @@ -286,10 +286,9 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) # TODO: this could probably be simplified by just using event.button # though that would probably change the way this handles a bit pressed_buttons = events(scene).mousebuttonstate - # mouse went down, this can either happen inside or outside the objects of interest # we also only react if one button is pressed, because otherwise things go crazy (pressed left button plus clicks from other buttons in between are not allowed, e.g.) - if event.action == Mouse.press && _isstandardmousebutton(first(pressed_buttons)) + if event.action == Mouse.press && !isempty(pressed_buttons) && _isstandardmousebutton(first(pressed_buttons)) if length(pressed_buttons) == 1 button = first(pressed_buttons) mouse_downed_button[] = button diff --git a/src/utilities/texture_atlas.jl b/src/utilities/texture_atlas.jl index 4a671d1e6a3..ef497fc7cdf 100644 --- a/src/utilities/texture_atlas.jl +++ b/src/utilities/texture_atlas.jl @@ -458,7 +458,7 @@ function primitive_uv_offset_width(atlas::TextureAtlas, marker::Observable, font return lift((m, f)-> primitive_uv_offset_width(atlas, m, f), marker, font; ignore_equal_values=true) end -_bcast(x::Vec) = (x,) +_bcast(x::Vec) = Ref(x) _bcast(x) = x # Calculates the scaling factor from unpadded size -> padded size @@ -534,7 +534,7 @@ function offset_bezierpath(atlas::TextureAtlas, bp::BezierPath, markersize::Vec2 end function offset_bezierpath(atlas::TextureAtlas, bp, scale, offset) - return offset_bezierpath.(Ref(atlas), bp, _bcast(scale), _bcast(offset)) + return offset_bezierpath.(Ref(atlas), bp, Vec2d.(_bcast(scale)), Vec2d.(_bcast(offset))) end function offset_marker(atlas::TextureAtlas, marker::Union{T, AbstractVector{T}}, font, markersize, markeroffset) where T <: BezierPath diff --git a/test/boundingboxes.jl b/test/boundingboxes.jl index 95c874bf5fd..66897c8444f 100644 --- a/test/boundingboxes.jl +++ b/test/boundingboxes.jl @@ -145,3 +145,16 @@ end @test data_limits(p) ≈ bb1 @test boundingbox(p) ≈ bb3 end + +@testset "issue 3960" begin + fig = Figure() + ax = Axis(fig[1, 1]) + triangle = BezierPath([ + MoveTo(Point(0, 0)), + LineTo(Point(1, 0)), + LineTo(Point(0, 1)), + ClosePath() + ]) + sc = scatter!(ax, Point(0, 0), marker=triangle, markerspace=:data) + data_limits(sc) # doesn't stackoverflow +end diff --git a/test/dim-converts.jl b/test/dim-converts.jl index 6f68ffbb039..0bec50fa275 100644 --- a/test/dim-converts.jl +++ b/test/dim-converts.jl @@ -91,3 +91,14 @@ end test_cleanup(dates) end end + +@testset "Type constraints (#3938)" begin + # Integers cannot be converted to Irrationals, + # so if the type of the observable is tightened + # somewhere within the pipeline, there should be a + # conversion error! + obs = Observable{Vector}([π, π]) + f, a, p = plot(obs) + obs.val = [1, 1] # Integers are not convertible to Irrational, so if the type was "solidified" here, there should be a conversion error + @test_nowarn notify(obs) +end