#= TODO * move all gl_ methods to GLPlot * integrate GLPlot UI * clean up corner cases * find a cleaner way for extracting properties * polar plots * labes and axis * fix units in all visuals (e.g dotted lines, marker scale, surfaces) =# slice_arg(img::Matrix{C}, idx::Int) where {C<:Colorant} = img is_marker_supported(::GLVisualizeBackend, shape::GLVisualize.AllPrimitives) = true is_marker_supported(::GLVisualizeBackend, shape::Union{Vector{Matrix{C}}, Matrix{C}}) where {C<:Colorant} = true is_marker_supported(::GLVisualizeBackend, shape::Shape) = true GL = Plots # --------------------------------------------------------------------------- # initialize the figure/window # function _create_backend_figure(plt::Plot{GLVisualizeBackend}) # # init a screen # # GLPlot.init() # end const _glplot_deletes = [] function get_plot_screen(list::Vector, name, result = []) for elem in list get_plot_screen(elem, name, result) end return result end function get_plot_screen(screen, name, result = []) if screen.name == name push!(result, screen) return result end get_plot_screen(screen.children, name, result) end function create_window(plt::Plot{GLVisualizeBackend}, visible) name = Symbol("__Plots.jl") # make sure we have any screen open if isempty(GLVisualize.get_screens()) # create a fresh, new screen parent_screen = GLVisualize.glscreen( "Plots", resolution = plt[:size], visible = visible ) @async GLWindow.renderloop(parent_screen) GLVisualize.add_screen(parent_screen) end # now lets get ourselves a permanent Plotting screen plot_screens = get_plot_screen(GLVisualize.current_screen(), name) screen = if isempty(plot_screens) # no screen with `name` parent = GLVisualize.current_screen() screen = GLWindow.Screen( parent, area = map(GLWindow.zeroposition, parent.area), name = name ) screen elseif length(plot_screens) == 1 plot_screens[1] else # okay this is silly! Lets see if we can. There is an ID we could use # will not be fine for more than 255 screens though -.-. error("multiple Plot screens. Please don't use any screen with the name $name") end # Since we own this window, we can do deep cleansing empty!(screen) plt.o = screen GLWindow.set_visibility!(screen, visible) resize!(screen, plt[:size]...) screen end # --------------------------------------------------------------------------- const _gl_marker_map = KW( :rect => '■', :star5 => '★', :diamond => '◆', :hexagon => '⬢', :cross => '✚', :xcross => '❌', :utriangle => '▲', :dtriangle => '▼', :ltriangle => '◀', :rtriangle => '▶', :pentagon => '⬟', :octagon => '⯄', :star4 => '✦', :star6 => '🟋', :star8 => '✷', :vline => '┃', :hline => '━', :+ => '+', :x => 'x', :circle => '●' ) function gl_marker(shape) shape end function gl_marker(shape::Shape) points = Point2f0[GeometryTypes.Vec{2, Float32}(p) for p in zip(shape.x, shape.y)] bb = GeometryTypes.AABB(points) mini, maxi = minimum(bb), maximum(bb) w3 = maxi-mini origin, width = Point2f0(mini[1], mini[2]), Point2f0(w3[1], w3[2]) map!(p -> ((p - origin) ./ width) - 0.5f0, points, points) # normalize and center GeometryTypes.GLNormalMesh(points) end # create a marker/shape type function gl_marker(shape::Vector{Symbol}) String(map(shape) do sym get(_gl_marker_map, sym, '●') end) end function gl_marker(shape::Symbol) if shape == :rect GeometryTypes.HyperRectangle(Vec2f0(0), Vec2f0(1)) elseif shape == :circle || shape == :none GeometryTypes.HyperSphere(Point2f0(0), 1f0) elseif haskey(_gl_marker_map, shape) _gl_marker_map[shape] elseif haskey(_shapes, shape) gl_marker(_shapes[shape]) else error("Shape $shape not supported by GLVisualize") end end function extract_limits(sp, plotattributes, kw_args) clims = sp[:clims] if is_2tuple(clims) if isfinite(clims[1]) && isfinite(clims[2]) kw_args[:limits] = Vec2f0(clims) end end nothing end to_vec(::Type{T}, vec::T) where {T <: StaticArrays.StaticVector} = vec to_vec(::Type{T}, s::Number) where {T <: StaticArrays.StaticVector} = T(s) to_vec(::Type{T}, vec::StaticArrays.StaticVector{3}) where {T <: StaticArrays.StaticVector{2}} = T(vec[1], vec[2]) to_vec(::Type{T}, vec::StaticArrays.StaticVector{2}) where {T <: StaticArrays.StaticVector{3}} = T(vec[1], vec[2], 0) to_vec(::Type{T}, vecs::AbstractVector) where {T <: StaticArrays.StaticVector} = map(x-> to_vec(T, x), vecs) function extract_marker(plotattributes, kw_args) dim = Plots.is3d(plotattributes) ? 3 : 2 scaling = dim == 3 ? 0.003 : 2 if haskey(plotattributes, :markershape) shape = plotattributes[:markershape] shape = gl_marker(shape) if shape != :none kw_args[:primitive] = shape end end dim = isa(kw_args[:primitive], GLVisualize.Sprites) ? 2 : 3 if haskey(plotattributes, :markersize) msize = plotattributes[:markersize] kw_args[:scale] = to_vec(GeometryTypes.Vec{dim, Float32}, msize .* scaling) end if haskey(plotattributes, :offset) kw_args[:offset] = plotattributes[:offset] end # get the color key = :markercolor haskey(plotattributes, key) || return c = gl_color(plotattributes[key]) if isa(c, AbstractVector) && plotattributes[:marker_z] != nothing extract_colornorm(plotattributes, kw_args) kw_args[:color] = nothing kw_args[:color_map] = c kw_args[:intensity] = convert(Vector{Float32}, plotattributes[:marker_z]) else kw_args[:color] = c end key = :markerstrokecolor haskey(plotattributes, key) || return c = gl_color(plotattributes[key]) if c != nothing if !(isa(c, Colorant) || (isa(c, Vector) && eltype(c) <: Colorant)) error("Stroke Color not supported: $c") end kw_args[:stroke_color] = c kw_args[:stroke_width] = Float32(plotattributes[:markerstrokewidth]) end end function _extract_surface(plotattributes::Plots.Surface) plotattributes.surf end function _extract_surface(plotattributes::AbstractArray) plotattributes end # TODO when to transpose?? function extract_surface(plotattributes) map(_extract_surface, (plotattributes[:x], plotattributes[:y], plotattributes[:z])) end function topoints(::Type{P}, array) where P [P(x) for x in zip(array...)] end function extract_points(plotattributes) dim = is3d(plotattributes) ? 3 : 2 array = if plotattributes[:seriestype] == :straightline straightline_data(plotattributes) elseif plotattributes[:seriestype] == :shape shape_data(plotattributes) else (plotattributes[:x], plotattributes[:y], plotattributes[:z])[1:dim] end topoints(Point{dim, Float32}, array) end function make_gradient(grad::Vector{C}) where C <: Colorant grad end function make_gradient(grad::ColorGradient) RGBA{Float32}[c for c in grad.colors] end make_gradient(c) = make_gradient(cgrad()) function extract_any_color(plotattributes, kw_args) if plotattributes[:marker_z] == nothing c = scalar_color(plotattributes, :fill) extract_c(plotattributes, kw_args, :fill) if isa(c, Colorant) kw_args[:color] = c else kw_args[:color] = nothing kw_args[:color_map] = make_gradient(c) clims = plotattributes[:subplot][:clims] if Plots.is_2tuple(clims) if isfinite(clims[1]) && isfinite(clims[2]) kw_args[:color_norm] = Vec2f0(clims) end elseif clims == :auto kw_args[:color_norm] = Vec2f0(ignorenan_extrema(plotattributes[:y])) end end else kw_args[:color] = nothing clims = plotattributes[:subplot][:clims] if Plots.is_2tuple(clims) if isfinite(clims[1]) && isfinite(clims[2]) kw_args[:color_norm] = Vec2f0(clims) end elseif clims == :auto kw_args[:color_norm] = Vec2f0(ignorenan_extrema(plotattributes[:y])) else error("Unsupported limits: $clims") end kw_args[:intensity] = convert(Vector{Float32}, plotattributes[:marker_z]) kw_args[:color_map] = gl_color_map(plotattributes, :marker) end end function extract_stroke(plotattributes, kw_args) extract_c(plotattributes, kw_args, :line) if haskey(plotattributes, :linewidth) kw_args[:thickness] = Float32(plotattributes[:linewidth] * 3) end end function extract_color(plotattributes, sym) plotattributes[Symbol("$(sym)color")] end gl_color(c::PlotUtils.ColorGradient) = c.colors gl_color(c::Vector{T}) where {T<:Colorant} = c gl_color(c::RGBA{Float32}) = c gl_color(c::Colorant) = RGBA{Float32}(c) function gl_color(tuple::Tuple) gl_color(tuple...) end # convert to RGBA function gl_color(c, a) c = convertColor(c, a) RGBA{Float32}(c) end function scalar_color(plotattributes, sym) gl_color(extract_color(plotattributes, sym)) end function gl_color_map(plotattributes, sym) colors = extract_color(plotattributes, sym) _gl_color_map(colors) end function _gl_color_map(colors::PlotUtils.ColorGradient) colors.colors end function _gl_color_map(c) Plots.default_gradient() end dist(a, b) = abs(a-b) mindist(x, a, b) = NaNMath.min(dist(a, x), dist(b, x)) function gappy(x, ps) n = length(ps) x <= first(ps) && return first(ps) - x for j=1:(n-1) p0 = ps[j] p1 = ps[NaNMath.min(j+1, n)] if p0 <= x && p1 >= x return mindist(x, p0, p1) * (isodd(j) ? 1 : -1) end end return last(ps) - x end function ticks(points, resolution) Float16[gappy(x, points) for x = range(first(points), stop=last(points), length=resolution)] end function insert_pattern!(points, kw_args) tex = GLAbstraction.Texture(ticks(points, 100), x_repeat=:repeat) kw_args[:pattern] = tex kw_args[:pattern_length] = Float32(last(points)) end function extract_linestyle(plotattributes, kw_args) haskey(plotattributes, :linestyle) || return ls = plotattributes[:linestyle] lw = plotattributes[:linewidth] kw_args[:thickness] = Float32(lw) if ls == :dash points = [0.0, lw, 2lw, 3lw, 4lw] insert_pattern!(points, kw_args) elseif ls == :dot tick, gap = lw/2, lw/4 points = [0.0, tick, tick+gap, 2tick+gap, 2tick+2gap] insert_pattern!(points, kw_args) elseif ls == :dashdot dtick, dgap = lw, lw ptick, pgap = lw/2, lw/4 points = [0.0, dtick, dtick+dgap, dtick+dgap+ptick, dtick+dgap+ptick+pgap] insert_pattern!(points, kw_args) elseif ls == :dashdotdot dtick, dgap = lw, lw ptick, pgap = lw/2, lw/4 points = [0.0, dtick, dtick+dgap, dtick+dgap+ptick, dtick+dgap+ptick+pgap, dtick+dgap+ptick+pgap+ptick, dtick+dgap+ptick+pgap+ptick+pgap] insert_pattern!(points, kw_args) end extract_c(plotattributes, kw_args, :line) nothing end function hover(to_hover::Vector, to_display, window) hover(to_hover[], to_display, window) end function get_cam(x) if isa(x, GLAbstraction.Context) return get_cam(x.children) elseif isa(x, Vector) return get_cam(first(x)) elseif isa(x, GLAbstraction.RenderObject) return x[:preferred_camera] end end function hover(to_hover, to_display, window) if isa(to_hover, GLAbstraction.Context) return hover(to_hover.children, to_display, window) end area = map(window.inputs[:mouseposition]) do mp SimpleRectangle{Int}(round(Int, mp+10)..., 100, 70) end mh = GLWindow.mouse2id(window) popup = GLWindow.Screen( window, hidden = map(mh-> !(mh.id == to_hover.id), mh), area = area, stroke = (2f0, RGBA(0f0, 0f0, 0f0, 0.8f0)) ) cam = get!(popup.cameras, :perspective) do GLAbstraction.PerspectiveCamera( popup.inputs, Vec3f0(3), Vec3f0(0), keep = Signal(false), theta = Signal(Vec3f0(0)), trans = Signal(Vec3f0(0)) ) end map(enumerate(to_display)) do id i,d = id robj = visualize(d) viewit = Reactive.droprepeats(map(mh->mh.id == to_hover.id && mh.index == i, mh)) camtype = get_cam(robj) Reactive.preserve(map(viewit) do vi if vi empty!(popup) if camtype == :perspective cam.projectiontype.value = GLVisualize.PERSPECTIVE else cam.projectiontype.value = GLVisualize.ORTHOGRAPHIC end GLVisualize._view(robj, popup, camera = cam) bb = GLAbstraction.boundingbox(robj).value mini = minimum(bb) w = GeometryTypes.widths(bb) wborder = w * 0.08f0 #8 percent border bb = GeometryTypes.AABB{Float32}(mini - wborder, w + 2 * wborder) GLAbstraction.center!(cam, bb) end end) end nothing end function extract_extrema(plotattributes, kw_args) xmin, xmax = ignorenan_extrema(plotattributes[:x]); ymin, ymax = ignorenan_extrema(plotattributes[:y]) kw_args[:primitive] = GeometryTypes.SimpleRectangle{Float32}(xmin, ymin, xmax-xmin, ymax-ymin) nothing end function extract_font(font, kw_args) kw_args[:family] = font.family kw_args[:relative_scale] = pointsize(font) kw_args[:color] = gl_color(font.color) end function extract_colornorm(plotattributes, kw_args) clims = plotattributes[:subplot][:clims] if Plots.is_2tuple(clims) if isfinite(clims[1]) && isfinite(clims[2]) kw_args[:color_norm] = Vec2f0(clims) end elseif clims == :auto z = if haskey(plotattributes, :marker_z) && plotattributes[:marker_z] != nothing plotattributes[:marker_z] elseif haskey(plotattributes, :line_z) && plotattributes[:line_z] != nothing plotattributes[:line_z] elseif isa(plotattributes[:z], Plots.Surface) plotattributes[:z].surf else plotattributes[:y] end kw_args[:color_norm] = Vec2f0(ignorenan_extrema(z)) kw_args[:intensity] = map(Float32, collect(z)) end end function extract_gradient(plotattributes, kw_args, sym) key = Symbol("$(sym)color") haskey(plotattributes, key) || return c = make_gradient(plotattributes[key]) kw_args[:color] = nothing extract_colornorm(plotattributes, kw_args) kw_args[:color_map] = c return end function extract_c(plotattributes, kw_args, sym) key = Symbol("$(sym)color") haskey(plotattributes, key) || return c = gl_color(plotattributes[key]) kw_args[:color] = nothing kw_args[:color_map] = nothing kw_args[:color_norm] = nothing if ( isa(c, AbstractVector) && ((haskey(plotattributes, :marker_z) && plotattributes[:marker_z] != nothing) || (haskey(plotattributes, :line_z) && plotattributes[:line_z] != nothing)) ) extract_colornorm(plotattributes, kw_args) kw_args[:color_map] = c else kw_args[:color] = c end return end function extract_stroke(plotattributes, kw_args, sym) key = Symbol("$(sym)strokecolor") haskey(plotattributes, key) || return c = gl_color(plotattributes[key]) if c != nothing if !isa(c, Colorant) error("Stroke Color not supported: $c") end kw_args[:stroke_color] = c kw_args[:stroke_width] = Float32(plotattributes[Symbol("$(sym)strokewidth")]) * 2 end return end function draw_grid_lines(sp, grid_segs, thickness, style, model, color) kw_args = Dict{Symbol, Any}( :model => model ) plotattributes = Dict( :linestyle => style, :linewidth => Float32(thickness), :linecolor => color ) Plots.extract_linestyle(plotattributes, kw_args) GL.gl_lines(map(Point2f0, grid_segs.pts), kw_args) end function align_offset(startpos, lastpos, atlas, rscale, font, align) xscale, yscale = GLVisualize.glyph_scale!('X', rscale) xmove = (lastpos-startpos)[1] + xscale if isa(align, GeometryTypes.Vec) return -Vec2f0(xmove, yscale) .* align elseif align == :top return -Vec2f0(xmove/2f0, yscale) elseif align == :right return -Vec2f0(xmove, yscale/2f0) else error("Align $align not known") end end function alignment2num(x::Symbol) (x in (:hcenter, :vcenter)) && return 0.5 (x in (:left, :bottom)) && return 0.0 (x in (:right, :top)) && return 1.0 0.0 # 0 default, or better to error? end function alignment2num(font::Plots.Font) Vec2f0(map(alignment2num, (font.halign, font.valign))) end pointsize(font) = font.pointsize * 2 function draw_ticks( axis, ticks, isx, isorigin, lims, m, text = "", positions = Point2f0[], offsets=Vec2f0[] ) sz = pointsize(tickfont(axis)) atlas = GLVisualize.get_texture_atlas() font = GLVisualize.defaultfont() flip = axis[:flip]; mirror = axis[:mirror] align = if isx mirror ? :bottom : :top else mirror ? :left : :right end axis_gap = Point2f0(isx ? 0 : sz / 2, isx ? sz / 2 : 0) for (cv, dv) in zip(ticks...) x, y = cv, lims[1] xy = if isorigin isx ? (x, 0) : (0, x) else isx ? (x, y) : (y, x) end _pos = m * GeometryTypes.Vec4f0(xy[1], xy[2], 0, 1) startpos = Point2f0(_pos[1], _pos[2]) - axis_gap str = string(dv) # need to tag a new UnicodeFun version for this... also the numbers become # so small that it looks terrible -.- # _str = split(string(dv), "^") # if length(_str) == 2 # _str[2] = UnicodeFun.to_superscript(_str[2]) # end # str = join(_str, "") position = GLVisualize.calc_position(str, startpos, sz, font, atlas) offset = GLVisualize.calc_offset(str, sz, font, atlas) alignoff = align_offset(startpos, last(position), atlas, sz, font, align) map!(position, position) do pos pos .+ alignoff end append!(positions, position) append!(offsets, offset) text *= str end text, positions, offsets end function glvisualize_text(position, text, kw_args) text_align = alignment2num(text.font) startpos = Vec2f0(position) atlas = GLVisualize.get_texture_atlas() font = GLVisualize.defaultfont() rscale = kw_args[:relative_scale] position = GLVisualize.calc_position(text.str, startpos, rscale, font, atlas) offset = GLVisualize.calc_offset(text.str, rscale, font, atlas) alignoff = align_offset(startpos, last(position), atlas, rscale, font, text_align) map!(position, position) do pos pos .+ alignoff end kw_args[:position] = position kw_args[:offset] = offset kw_args[:scale_primitive] = true visualize(text.str, Style(:default), kw_args) end function text_model(font, pivot) pv = GeometryTypes.Vec3f0(pivot[1], pivot[2], 0) if font.rotation != 0.0 rot = Float32(deg2rad(font.rotation)) rotm = GLAbstraction.rotationmatrix_z(rot) return GLAbstraction.translationmatrix(pv)*rotm*GLAbstraction.translationmatrix(-pv) else eye(GeometryTypes.Mat4f0) end end function gl_draw_axes_2d(sp::Plots.Subplot{Plots.GLVisualizeBackend}, model, area) xticks, yticks, xspine_segs, yspine_segs, xtick_segs, ytick_segs, xgrid_segs, ygrid_segs, xminorgrid_segs, yminorgrid_segs, xborder_segs, yborder_segs = Plots.axis_drawing_info(sp) xaxis = sp[:xaxis]; yaxis = sp[:yaxis] xgc = Colors.color(Plots.gl_color(xaxis[:foreground_color_grid])) ygc = Colors.color(Plots.gl_color(yaxis[:foreground_color_grid])) axis_vis = [] if xaxis[:grid] grid = draw_grid_lines(sp, xgrid_segs, xaxis[:gridlinewidth], xaxis[:gridstyle], model, RGBA(xgc, xaxis[:gridalpha])) push!(axis_vis, grid) end if yaxis[:grid] grid = draw_grid_lines(sp, ygrid_segs, yaxis[:gridlinewidth], yaxis[:gridstyle], model, RGBA(ygc, yaxis[:gridalpha])) push!(axis_vis, grid) end if xaxis[:minorgrid] minorgrid = draw_minorgrid_lines(sp, xminorgrid_segs, xaxis[:minorgridlinewidth], xaxis[:minorgridstyle], model, RGBA(xgc, xaxis[:minorgridalpha])) push!(axis_vis, minorgrid) end if yaxis[:minorgrid] minorgrid = draw_minorgrid_lines(sp, yminorgrid_segs, yaxis[:minorgridlinewidth], yaxis[:minorgridstyle], model, RGBA(ygc, yaxis[:minorgridalpha])) push!(axis_vis, minorgrid) end xac = Colors.color(Plots.gl_color(xaxis[:foreground_color_axis])) yac = Colors.color(Plots.gl_color(yaxis[:foreground_color_axis])) if alpha(xaxis[:foreground_color_axis]) > 0 spine = draw_grid_lines(sp, xspine_segs, 1f0, :solid, model, RGBA(xac, 1.0f0)) push!(axis_vis, spine) end if alpha(yaxis[:foreground_color_axis]) > 0 spine = draw_grid_lines(sp, yspine_segs, 1f0, :solid, model, RGBA(yac, 1.0f0)) push!(axis_vis, spine) end if sp[:framestyle] in (:zerolines, :grid) if alpha(xaxis[:foreground_color_grid]) > 0 spine = draw_grid_lines(sp, xtick_segs, 1f0, :solid, model, RGBA(xgc, xaxis[:gridalpha])) push!(axis_vis, spine) end if alpha(yaxis[:foreground_color_grid]) > 0 spine = draw_grid_lines(sp, ytick_segs, 1f0, :solid, model, RGBA(ygc, yaxis[:gridalpha])) push!(axis_vis, spine) end else if alpha(xaxis[:foreground_color_axis]) > 0 spine = draw_grid_lines(sp, xtick_segs, 1f0, :solid, model, RGBA(xac, 1.0f0)) push!(axis_vis, spine) end if alpha(yaxis[:foreground_color_axis]) > 0 spine = draw_grid_lines(sp, ytick_segs, 1f0, :solid, model, RGBA(yac, 1.0f0)) push!(axis_vis, spine) end end fcolor = Plots.gl_color(xaxis[:foreground_color_axis]) xlim = Plots.axis_limits(xaxis) ylim = Plots.axis_limits(yaxis) if !(xaxis[:ticks] in (nothing, false, :none)) && !(sp[:framestyle] == :none) && xaxis[:showaxis] ticklabels = map(model) do m mirror = xaxis[:mirror] t, positions, offsets = draw_ticks(xaxis, xticks, true, sp[:framestyle] == :origin, ylim, m) end kw_args = Dict{Symbol, Any}( :position => map(x-> x[2], ticklabels), :offset => map(last, ticklabels), :color => fcolor, :relative_scale => pointsize(tickfont(xaxis)), :scale_primitive => false ) push!(axis_vis, visualize(map(first, ticklabels), Style(:default), kw_args)) end if !(yaxis[:ticks] in (nothing, false, :none)) && !(sp[:framestyle] == :none) && yaxis[:showaxis] ticklabels = map(model) do m mirror = yaxis[:mirror] t, positions, offsets = draw_ticks(yaxis, yticks, false, sp[:framestyle] == :origin, xlim, m) end kw_args = Dict{Symbol, Any}( :position => map(x-> x[2], ticklabels), :offset => map(last, ticklabels), :color => fcolor, :relative_scale => pointsize(tickfont(xaxis)), :scale_primitive => false ) push!(axis_vis, visualize(map(first, ticklabels), Style(:default), kw_args)) end xbc = Colors.color(Plots.gl_color(xaxis[:foreground_color_border])) ybc = Colors.color(Plots.gl_color(yaxis[:foreground_color_border])) intensity = sp[:framestyle] == :semi ? 0.5f0 : 1.0f0 if sp[:framestyle] in (:box, :semi) xborder = draw_grid_lines(sp, xborder_segs, intensity, :solid, model, RGBA(xbc, intensity)) yborder = draw_grid_lines(sp, yborder_segs, intensity, :solid, model, RGBA(ybc, intensity)) push!(axis_vis, xborder, yborder) end area_w = GeometryTypes.widths(area) if sp[:title] != "" tf = titlefont(sp) font = Plots.Font(tf.family, tf.pointsize, :hcenter, :top, tf.rotation, tf.color) xy = Point2f0(area.w/2, area_w[2] + pointsize(tf)/2) kw = Dict(:model => text_model(font, xy), :scale_primitive => true) extract_font(font, kw) t = PlotText(sp[:title], font) push!(axis_vis, glvisualize_text(xy, t, kw)) end if xaxis[:guide] != "" tf = guidefont(xaxis) xy = Point2f0(area.w/2, - pointsize(tf)/2) font = Plots.Font(tf.family, tf.pointsize, :hcenter, :bottom, tf.rotation, tf.color) kw = Dict(:model => text_model(font, xy), :scale_primitive => true) t = PlotText(xaxis[:guide], font) extract_font(font, kw) push!(axis_vis, glvisualize_text(xy, t, kw)) end if yaxis[:guide] != "" tf = guidefont(yaxis) font = Plots.Font(tf.family, tf.pointsize, :hcenter, :top, 90f0, tf.color) xy = Point2f0(-pointsize(tf)/2, area.h/2) kw = Dict(:model => text_model(font, xy), :scale_primitive=>true) t = PlotText(yaxis[:guide], font) extract_font(font, kw) push!(axis_vis, glvisualize_text(xy, t, kw)) end axis_vis end function gl_draw_axes_3d(sp, model) x = Plots.axis_limits(sp[:xaxis]) y = Plots.axis_limits(sp[:yaxis]) z = Plots.axis_limits(sp[:zaxis]) min = Vec3f0(x[1], y[1], z[1]) visualize( GeometryTypes.AABB{Float32}(min, Vec3f0(x[2], y[2], z[2])-min), :grid, model=model ) end function gl_bar(plotattributes, kw_args) x, y = plotattributes[:x], plotattributes[:y] nx, ny = length(x), length(y) axis = plotattributes[:subplot][isvertical(plotattributes) ? :xaxis : :yaxis] cv = [discrete_value!(axis, xi)[1] for xi=x] x = if nx == ny cv elseif nx == ny + 1 0.5diff(cv) + cv[1:end-1] else error("bar recipe: x must be same length as y (centers), or one more than y (edges).\n\t\tlength(x)=$(length(x)), length(y)=$(length(y))") end if haskey(kw_args, :stroke_width) # stroke is inside for bars #kw_args[:stroke_width] = -kw_args[:stroke_width] end # compute half-width of bars bw = nothing hw = if bw == nothing ignorenan_mean(diff(x)) else Float64[_cycle(bw,i)*0.5 for i=1:length(x)] end # make fillto a vector... default fills to 0 fillto = plotattributes[:fillrange] if fillto == nothing fillto = 0 end # create the bar shapes by adding x/y segments positions, scales = Array{Point2f0}(undef, ny), Array{Vec2f0}(undef, ny) m = Reactive.value(kw_args[:model]) sx, sy = m[1,1], m[2,2] for i=1:ny center = x[i] hwi = abs(_cycle(hw,i)); yi = y[i]; fi = _cycle(fillto,i) if Plots.isvertical(plotattributes) sz = (hwi*sx, yi*sy) else sz = (yi*sx, hwi*2*sy) end positions[i] = (center-hwi*0.5, fi) scales[i] = sz end kw_args[:scale] = scales kw_args[:offset] = Vec2f0(0) visualize((GLVisualize.RECTANGLE, positions), Style(:default), kw_args) #[] end const _box_halfwidth = 0.4 notch_width(q2, q4, N) = 1.58 * (q4-q2)/sqrt(N) function gl_boxplot(plotattributes, kw_args) kwbox = copy(kw_args) range = 1.5; notch = false x, y = plotattributes[:x], plotattributes[:y] glabels = sort(collect(unique(x))) warning = false outliers_x, outliers_y = zeros(0), zeros(0) box_pos = Point2f0[] box_scale = Vec2f0[] outliers = Point2f0[] t_segments = Point2f0[] m = Reactive.value(kw_args[:model]) sx, sy = m[1,1], m[2,2] for (i,glabel) in enumerate(glabels) # filter y values = y[filter(i -> _cycle(x,i) == glabel, 1:length(y))] # compute quantiles q1,q2,q3,q4,q5 = quantile(values, range(0, stop=1, length=5)) # notch n = Plots.notch_width(q2, q4, length(values)) # warn on inverted notches? if notch && !warning && ( (q2>(q3-n)) || (q4<(q3+n)) ) @warn("Boxplot's notch went outside hinges. Set notch to false.") warning = true # Show the warning only one time end # make the shape center = Plots.discrete_value!(plotattributes[:subplot][:xaxis], glabel)[1] hw = plotattributes[:bar_width] == nothing ? Plots._box_halfwidth*2 : _cycle(plotattributes[:bar_width], i) l, m, r = center - hw/2, center, center + hw/2 # internal nodes for notches L, R = center - 0.5 * hw, center + 0.5 * hw # outliers if Float64(range) != 0.0 # if the range is 0.0, the whiskers will extend to the data limit = range*(q4-q2) inside = Float64[] for value in values if (value < (q2 - limit)) || (value > (q4 + limit)) push!(outliers, (center, value)) else push!(inside, value) end end # change q1 and q5 to show outliers # using maximum and minimum values inside the limits q1, q5 = ignorenan_extrema(inside) end # Box if notch push!(t_segments, (m, q1), (l, q1), (r, q1), (m, q1), (m, q2))# lower T push!(box_pos, (l, q2));push!(box_scale, (hw*sx, n*sy)) # lower box push!(box_pos, (l, q4));push!(box_scale, (hw*sx, n*sy)) # upper box push!(t_segments, (m, q5), (l, q5), (r, q5), (m, q5), (m, q4))# upper T else push!(t_segments, (m, q2), (m, q1), (l, q1), (r, q1))# lower T push!(box_pos, (l, q2)); push!(box_scale, (hw*sx, (q3-q2)*sy)) # lower box push!(box_pos, (l, q4)); push!(box_scale, (hw*sx, (q3-q4)*sy)) # upper box push!(t_segments, (m, q4), (m, q5), (r, q5), (l, q5))# upper T end end kwbox = Dict{Symbol, Any}( :scale => box_scale, :model => kw_args[:model], :offset => Vec2f0(0), ) extract_marker(plotattributes, kw_args) outlier_kw = Dict( :model => kw_args[:model], :color => scalar_color(plotattributes, :fill), :stroke_width => Float32(plotattributes[:markerstrokewidth]), :stroke_color => scalar_color(plotattributes, :markerstroke), ) lines_kw = Dict( :model => kw_args[:model], :stroke_width => plotattributes[:linewidth], :stroke_color => scalar_color(plotattributes, :fill), ) vis1 = GLVisualize.visualize((GLVisualize.RECTANGLE, box_pos), Style(:default), kwbox) vis2 = GLVisualize.visualize((GLVisualize.CIRCLE, outliers), Style(:default), outlier_kw) vis3 = GLVisualize.visualize(t_segments, Style(:linesegment), lines_kw) [vis1, vis2, vis3] end # --------------------------------------------------------------------------- function gl_viewport(bb, rect) l, b, bw, bh = bb rw, rh = rect.w, rect.h GLVisualize.SimpleRectangle( round(Int, rw * l), round(Int, rh * b), round(Int, rw * bw), round(Int, rh * bh) ) end function to_modelmatrix(rect, subrect, rel_plotarea, sp) xmin, xmax = Plots.axis_limits(sp[:xaxis]) ymin, ymax = Plots.axis_limits(sp[:yaxis]) mini, maxi = Vec3f0(xmin, ymin, 0), Vec3f0(xmax, ymax, 1) if Plots.is3d(sp) zmin, zmax = Plots.axis_limits(sp[:zaxis]) mini, maxi = Vec3f0(xmin, ymin, zmin), Vec3f0(xmax, ymax, zmax) s = Vec3f0(1) ./ (maxi-mini) return GLAbstraction.scalematrix(s)*GLAbstraction.translationmatrix(-mini) end l, b, bw, bh = rel_plotarea w, h = rect.w*bw, rect.h*bh x, y = rect.w*l - subrect.x, rect.h*b - subrect.y t = -mini s = Vec3f0(w, h, 1) ./ (maxi-mini) GLAbstraction.translationmatrix(Vec3f0(x,y,0))*GLAbstraction.scalematrix(s)*GLAbstraction.translationmatrix(t) end # ---------------------------------------------------------------- function scale_for_annotations!(series::Series, scaletype::Symbol = :pixels) anns = series[:series_annotations] if anns != nothing && anns.baseshape != nothing # we use baseshape to overwrite the markershape attribute # with a list of custom shapes for each msw, msh = anns.scalefactor offsets = Array{Vec2f0}(undef, length(anns.strs)) series[:markersize] = map(1:length(anns.strs)) do i str = _cycle(anns.strs, i) # get the width and height of the string (in mm) sw, sh = text_size(str, anns.font.pointsize) # how much to scale the base shape? # note: it's a rough assumption that the shape fills the unit box [-1,-1,1,1], # so we scale the length-2 shape by 1/2 the total length xscale = 0.5to_pixels(sw) * 1.8 yscale = 0.5to_pixels(sh) * 1.8 # we save the size of the larger direction to the markersize list, # and then re-scale a copy of baseshape to match the w/h ratio s = Vec2f0(xscale, yscale) offsets[i] = -s s end series[:offset] = offsets end return end function _display(plt::Plot{GLVisualizeBackend}, visible = true) screen = create_window(plt, visible) sw, sh = plt[:size] sw, sh = sw*px, sh*px for sp in plt.subplots _3d = Plots.is3d(sp) # camera = :perspective # initialize the sub-screen for this subplot rel_bbox = Plots.bbox_to_pcts(bbox(sp), sw, sh) sub_area = map(screen.area) do rect Plots.gl_viewport(rel_bbox, rect) end c = plt[:background_color_outside] sp_screen = GLVisualize.Screen( screen, color = c, area = sub_area ) sp.o = sp_screen cam = get!(sp_screen.cameras, :perspective) do inside = sp_screen.inputs[:mouseinside] theta = _3d ? nothing : Signal(Vec3f0(0)) # surpress rotation for 2D (nothing will get usual rotation controle) GLAbstraction.PerspectiveCamera( sp_screen.inputs, Vec3f0(3), Vec3f0(0), keep = inside, theta = theta ) end rel_plotarea = Plots.bbox_to_pcts(plotarea(sp), sw, sh) model_m = map(Plots.to_modelmatrix, screen.area, sub_area, Signal(rel_plotarea), Signal(sp) ) # loop over the series and add them to the subplot if !_3d axis = gl_draw_axes_2d(sp, model_m, Reactive.value(sub_area)) GLVisualize._view(axis, sp_screen, camera=:perspective) cam.projectiontype.value = GLVisualize.ORTHOGRAPHIC Reactive.run_till_now() # make sure Reactive.push! arrives GLAbstraction.center!(cam, GeometryTypes.AABB( Vec3f0(-20), Vec3f0((GeometryTypes.widths(sp_screen)+40f0)..., 1) ) ) else axis = gl_draw_axes_3d(sp, model_m) GLVisualize._view(axis, sp_screen, camera=:perspective) push!(cam.projectiontype, GLVisualize.PERSPECTIVE) end for series in Plots.series_list(sp) plotattributes = series.plotattributes st = plotattributes[:seriestype]; kw_args = KW() # exctract kw kw_args[:model] = model_m # add transformation if !_3d # 3D is treated differently, since we need boundingboxes for camera kw_args[:boundingbox] = nothing # don't calculate bb, we dont need it end scale_for_annotations!(series) if st in (:surface, :wireframe) x, y, z = extract_surface(plotattributes) extract_gradient(plotattributes, kw_args, :fill) z = Plots.transpose_z(plotattributes, z, false) if isa(x, AbstractMatrix) && isa(y, AbstractMatrix) x, y = Plots.transpose_z(plotattributes, x, false), Plots.transpose_z(plotattributes, y, false) end if st == :wireframe kw_args[:wireframe] = true kw_args[:stroke_color] = plotattributes[:linecolor] kw_args[:stroke_width] = Float32(plotattributes[:linewidth]/100f0) end vis = GL.gl_surface(x, y, z, kw_args) elseif (st in (:path, :path3d, :straightline)) && plotattributes[:linewidth] > 0 kw = copy(kw_args) points = Plots.extract_points(plotattributes) extract_linestyle(plotattributes, kw) vis = GL.gl_lines(points, kw) if plotattributes[:markershape] != :none kw = copy(kw_args) extract_stroke(plotattributes, kw) extract_marker(plotattributes, kw) vis2 = GL.gl_scatter(copy(points), kw) vis = [vis; vis2] end if plotattributes[:fillrange] != nothing kw = copy(kw_args) fr = plotattributes[:fillrange] ps = if all(x-> x >= 0, diff(plotattributes[:x])) # if is monotonic vcat(points, Point2f0[(points[i][1], _cycle(fr, i)) for i=length(points):-1:1]) else points end extract_c(plotattributes, kw, :fill) vis = [GL.gl_poly(ps, kw), vis] end elseif st in (:scatter, :scatter3d) #|| plotattributes[:markershape] != :none extract_marker(plotattributes, kw_args) points = extract_points(plotattributes) vis = GL.gl_scatter(points, kw_args) elseif st == :shape extract_c(plotattributes, kw_args, :fill) vis = GL.gl_shape(plotattributes, kw_args) elseif st == :contour x,y,z = extract_surface(plotattributes) z = transpose_z(plotattributes, z, false) extract_extrema(plotattributes, kw_args) extract_gradient(plotattributes, kw_args, :fill) kw_args[:fillrange] = plotattributes[:fillrange] kw_args[:levels] = plotattributes[:levels] vis = GL.gl_contour(x,y,z, kw_args) elseif st == :heatmap x,y,z = extract_surface(plotattributes) extract_gradient(plotattributes, kw_args, :fill) extract_extrema(plotattributes, kw_args) extract_limits(sp, plotattributes, kw_args) vis = GL.gl_heatmap(x,y,z, kw_args) elseif st == :bar extract_c(plotattributes, kw_args, :fill) extract_stroke(plotattributes, kw_args, :marker) vis = gl_bar(plotattributes, kw_args) elseif st == :image extract_extrema(plotattributes, kw_args) vis = GL.gl_image(plotattributes[:z].surf, kw_args) elseif st == :boxplot extract_c(plotattributes, kw_args, :fill) vis = gl_boxplot(plotattributes, kw_args) elseif st == :volume volume = plotattributes[:y] _plotattributes = copy(plotattributes) _plotattributes[:y] = 0:1 _plotattributes[:x] = 0:1 kw_args = KW() extract_gradient(_plotattributes, kw_args, :fill) vis = visualize(volume.v, Style(:default), kw_args) else error("failed to display plot type $st") end isa(vis, Array) && isempty(vis) && continue # nothing to see here GLVisualize._view(vis, sp_screen, camera=:perspective) if haskey(plotattributes, :hover) && !(plotattributes[:hover] in (false, :none, nothing)) hover(vis, plotattributes[:hover], sp_screen) end if isdefined(:GLPlot) && isdefined(Main.GLPlot, :(register_plot!)) del_signal = Main.GLPlot.register_plot!(vis, sp_screen, create_gizmo=false) append!(_glplot_deletes, del_signal) end anns = series[:series_annotations] for (x, y, str, font) in EachAnn(anns, plotattributes[:x], plotattributes[:y]) txt_args = Dict{Symbol, Any}(:model => eye(GLAbstraction.Mat4f0)) x, y = Reactive.value(model_m) * GeometryTypes.Vec{4, Float32}(x, y, 0, 1) extract_font(font, txt_args) t = glvisualize_text(Point2f0(x, y), PlotText(str, font), txt_args) GLVisualize._view(t, sp_screen, camera = :perspective) end end generate_legend(sp, sp_screen, model_m) if _3d GLAbstraction.center!(sp_screen) end GLAbstraction.post_empty() yield() end end function _show(io::IO, ::MIME"image/png", plt::Plot{GLVisualizeBackend}) _display(plt, false) GLWindow.poll_glfw() if Base.n_avail(Reactive._messages) > 0 Reactive.run_till_now() end yield() GLWindow.render_frame(GLWindow.rootscreen(plt.o)) GLWindow.swapbuffers(plt.o) buff = GLWindow.screenbuffer(plt.o) png = map(RGB{U8}, buff) FileIO.save(FileIO.Stream(FileIO.DataFormat{:PNG}, io), png) end function gl_image(img, kw_args) rect = kw_args[:primitive] kw_args[:primitive] = GeometryTypes.SimpleRectangle{Float32}(rect.x, rect.y, rect.w, rect.h) visualize(img, Style(:default), kw_args) end function handle_segment(lines, line_segments, points::Vector{P}, segment) where P (isempty(segment) || length(segment) < 2) && return if length(segment) == 2 append!(line_segments, view(points, segment)) elseif length(segment) == 3 p = view(points, segment) push!(line_segments, p[1], p[2], p[2], p[3]) else append!(lines, view(points, segment)) push!(lines, P(NaN)) end end function gl_lines(points, kw_args) result = [] isempty(points) && return result P = eltype(points) lines = P[] line_segments = P[] last = 1 for (i,p) in enumerate(points) if isnan(p) || i==length(points) _i = isnan(p) ? i-1 : i handle_segment(lines, line_segments, points, last:_i) last = i+1 end end if !isempty(lines) pop!(lines) # remove last NaN push!(result, visualize(lines, Style(:lines), kw_args)) end if !isempty(line_segments) push!(result, visualize(line_segments, Style(:linesegment), kw_args)) end return result end function gl_shape(plotattributes, kw_args) points = Plots.extract_points(plotattributes) result = [] for rng in iter_segments(plotattributes[:x], plotattributes[:y]) ps = points[rng] meshes = gl_poly(ps, kw_args) append!(result, meshes) end result end function gl_scatter(points, kw_args) prim = get(kw_args, :primitive, GeometryTypes.Circle) if isa(prim, GLNormalMesh) if haskey(kw_args, :model) p = get(kw_args, :perspective, eye(GeometryTypes.Mat4f0)) kw_args[:scale] = GLAbstraction.const_lift(kw_args[:model], kw_args[:scale], p) do m, sc, p s = Vec3f0(m[1,1], m[2,2], m[3,3]) ps = Vec3f0(p[1,1], p[2,2], p[3,3]) r = sc ./ (s .* ps) r end end else # 2D prim kw_args[:scale] = to_vec(Vec2f0, kw_args[:scale]) end if haskey(kw_args, :stroke_width) s = Reactive.value(kw_args[:scale]) sw = kw_args[:stroke_width] if sw*5 > _cycle(Reactive.value(s), 1)[1] # restrict marker stroke to 1/10th of scale (and handle arrays of scales) kw_args[:stroke_width] = s[1] / 5f0 end end kw_args[:scale_primitive] = false if isa(prim, String) kw_args[:position] = points if !isa(kw_args[:scale], Vector) # if not vector, we can assume it's relative scale kw_args[:relative_scale] = kw_args[:scale] delete!(kw_args, :scale) end return visualize(prim, Style(:default), kw_args) end visualize((prim, points), Style(:default), kw_args) end function gl_poly(points, kw_args) last(points) == first(points) && pop!(points) polys = GeometryTypes.split_intersections(points) result = [] for poly in polys mesh = GLNormalMesh(poly) # make polygon if !isempty(GeometryTypes.faces(mesh)) # check if polygonation has any faces push!(result, GLVisualize.visualize(mesh, Style(:default), kw_args)) else @warn("Couldn't draw the polygon: $points") end end result end function gl_surface(x,y,z, kw_args) if isa(x, AbstractRange) && isa(y, AbstractRange) main = z kw_args[:ranges] = (x, y) else if isa(x, AbstractMatrix) && isa(y, AbstractMatrix) main = map(s->map(Float32, s), (x, y, z)) elseif isa(x, AbstractVector) || isa(y, AbstractVector) x = Float32[x[i] for i = 1:size(z,1), j = 1:size(z,2)] y = Float32[y[j] for i = 1:size(z,1), j = 1:size(z,2)] main = (x, y, map(Float32, z)) else error("surface: combination of types not supported: $(typeof(x)) $(typeof(y)) $(typeof(z))") end if get(kw_args, :wireframe, false) points = map(Point3f0, zip(vec(x), vec(y), vec(z))) faces = Cuint[] idx = (i,j) -> CartesianIndices(size(z), i, j) - 1 for i=1:size(z,1), j=1:size(z,2) i < size(z,1) && push!(faces, idx(i, j), idx(i+1, j)) j < size(z,2) && push!(faces, idx(i, j), idx(i, j+1)) end color = get(kw_args, :stroke_color, RGBA{Float32}(0,0,0,1)) kw_args[:color] = color kw_args[:thickness] = Float32(get(kw_args, :stroke_width, 1f0)) kw_args[:indices] = faces delete!(kw_args, :stroke_color) delete!(kw_args, :stroke_width) return visualize(points, Style(:linesegment), kw_args) end end return visualize(main, Style(:surface), kw_args) end function gl_contour(x, y, z, kw_args) if kw_args[:fillrange] != nothing delete!(kw_args, :intensity) I = GLVisualize.Intensity{Float32} main = [I(z[j,i]) for i=1:size(z, 2), j=1:size(z, 1)] return visualize(main, Style(:default), kw_args) else h = kw_args[:levels] T = eltype(z) levels = Contour.contours(map(T, x), map(T, y), z, h) result = Point2f0[] zmin, zmax = get(kw_args, :limits, Vec2f0(ignorenan_extrema(z))) cmap = get(kw_args, :color_map, get(kw_args, :color, RGBA{Float32}(0,0,0,1))) colors = RGBA{Float32}[] for c in levels.contours for elem in c.lines append!(result, elem.vertices) push!(result, Point2f0(NaN32)) col = GLVisualize.color_lookup(cmap, c.level, zmin, zmax) append!(colors, fill(col, length(elem.vertices) + 1)) end end kw_args[:color] = colors kw_args[:color_map] = nothing kw_args[:color_norm] = nothing kw_args[:intensity] = nothing return visualize(result, Style(:lines),kw_args) end end function gl_heatmap(x,y,z, kw_args) get!(kw_args, :color_norm, Vec2f0(ignorenan_extrema(z))) get!(kw_args, :color_map, Plots.make_gradient(cgrad())) delete!(kw_args, :intensity) I = GLVisualize.Intensity{Float32} heatmap = I[z[j,i] for i=1:size(z, 2), j=1:size(z, 1)] tex = GLAbstraction.Texture(heatmap, minfilter=:nearest) kw_args[:stroke_width] = 0f0 kw_args[:levels] = 1f0 visualize(tex, Style(:default), kw_args) end """ Ugh, so much special casing (╯°□°)╯︵ ┻━┻ """ function label_scatter(plotattributes, w, ho) kw = KW() extract_stroke(plotattributes, kw) extract_marker(plotattributes, kw) kw[:scale] = Vec2f0(w/2) kw[:offset] = Vec2f0(-w/4) if haskey(kw, :intensity) cmap = kw[:color_map] norm = kw[:color_norm] kw[:color] = GLVisualize.color_lookup(cmap, kw[:intensity][1], norm) delete!(kw, :intensity) delete!(kw, :color_map) delete!(kw, :color_norm) else color = get(kw, :color, nothing) kw[:color] = isa(color, Array) ? first(color) : color end strcolor = get(kw, :stroke_color, RGBA{Float32}(0,0,0,0)) kw[:stroke_color] = isa(strcolor, Array) ? first(strcolor) : strcolor p = get(kw, :primitive, GeometryTypes.Circle) if isa(p, GLNormalMesh) bb = GeometryTypes.AABB{Float32}(GeometryTypes.vertices(p)) bbw = GeometryTypes.widths(bb) if isapprox(bbw[3], 0) bbw = Vec3f0(bbw[1], bbw[2], 1) end mini = minimum(bb) m = GLAbstraction.translationmatrix(-mini) m *= GLAbstraction.scalematrix(1 ./ bbw) kw[:primitive] = m * p kw[:scale] = Vec3f0(w/2) delete!(kw, :offset) end if isa(p, Array) kw[:primitive] = GeometryTypes.Circle end GL.gl_scatter(Point2f0[(w/2, ho)], kw) end function make_label(sp, series, i) GL = Plots w, gap, ho = 20f0, 5f0, 5 result = [] plotattributes = series.plotattributes st = plotattributes[:seriestype] kw_args = KW() if (st in (:path, :path3d, :straightline)) && plotattributes[:linewidth] > 0 points = Point2f0[(0, ho), (w, ho)] kw = KW() extract_linestyle(plotattributes, kw) append!(result, GL.gl_lines(points, kw)) if plotattributes[:markershape] != :none push!(result, label_scatter(plotattributes, w, ho)) end elseif st in (:scatter, :scatter3d) #|| plotattributes[:markershape] != :none push!(result, label_scatter(plotattributes, w, ho)) else extract_c(plotattributes, kw_args, :fill) if isa(kw_args[:color], AbstractVector) kw_args[:color] = first(kw_args[:color]) end push!(result, visualize( GeometryTypes.SimpleRectangle(-w/2, ho-w/4, w/2, w/2), Style(:default), kw_args )) end labeltext = if isa(series[:label], Array) i += 1 series[:label][i] else series[:label] end ft = legendfont(sp) font = Plots.Font(ft.family, ft.pointsize, :left, :bottom, 0.0, ft.color) xy = Point2f0(w+gap, 0.0) kw = Dict(:model => text_model(font, xy), :scale_primitive=>false) extract_font(font, kw) t = PlotText(labeltext, font) push!(result, glvisualize_text(xy, t, kw)) GLAbstraction.Context(result...), i end function generate_legend(sp, screen, model_m) legend = GLAbstraction.Context[] if sp[:legend] != :none i = 0 for series in series_list(sp) should_add_to_legend(series) || continue result, i = make_label(sp, series, i) push!(legend, result) end if isempty(legend) return end list = visualize(legend, gap=Vec3f0(0,5,0)) bb = GLAbstraction._boundingbox(list) wx,wy,_ = GeometryTypes.widths(bb) xmin, _ = Plots.axis_limits(sp[:xaxis]) _, ymax = Plots.axis_limits(sp[:yaxis]) area = map(model_m) do m p = m * GeometryTypes.Vec4f0(xmin, ymax, 0, 1) h = round(Int, wy)+20 w = round(Int, wx)+20 x,y = round(Int, p[1])+30, round(Int, p[2]-h)-30 GeometryTypes.SimpleRectangle(x, y, w, h) end sscren = GLWindow.Screen( screen, area = area, color = sp[:background_color_legend], stroke = (2f0, RGBA(0.3, 0.3, 0.3, 0.9)) ) GLAbstraction.translate!(list, Vec3f0(10,10,0)) GLVisualize._view(list, sscren, camera=:fixed_pixel) end return end