Plots.jl/src/backends/glvisualize.jl
2017-02-07 18:28:22 -05:00

1521 lines
49 KiB
Julia

#=
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)
=#
const _glvisualize_attr = merge_with_base_supported([
:annotations,
:background_color_legend, :background_color_inside, :background_color_outside,
:foreground_color_grid, :foreground_color_legend, :foreground_color_title,
:foreground_color_axis, :foreground_color_border, :foreground_color_guide, :foreground_color_text,
:label,
:linecolor, :linestyle, :linewidth, :linealpha,
:markershape, :markercolor, :markersize, :markeralpha,
:markerstrokewidth, :markerstrokecolor, :markerstrokealpha,
:fillrange, :fillcolor, :fillalpha,
:bins, :bar_width, :bar_edges, :bar_position,
:title, :title_location, :titlefont,
:window_title,
:guide, :lims, :ticks, :scale, :flip, :rotation,
:tickfont, :guidefont, :legendfont,
:grid, :legend, :colorbar,
:marker_z,
:line_z,
:levels,
:ribbon, :quiver, :arrow,
:orientation,
:overwrite_figure,
#:polar,
:normalize, :weights,
:contours, :aspect_ratio,
:match_dimensions,
:clims,
:inset_subplots,
:dpi,
:hover
])
const _glvisualize_seriestype = [
:path, :shape,
:scatter, :hexbin,
:bar, :boxplot,
:heatmap, :image, :volume,
:contour, :contour3d, :path3d, :scatter3d, :surface, :wireframe
]
const _glvisualize_style = [:auto, :solid, :dash, :dot, :dashdot]
const _glvisualize_marker = _allMarkers
const _glvisualize_scale = [:identity, :ln, :log2, :log10]
# --------------------------------------------------------------------------------------
function _initialize_backend(::GLVisualizeBackend; kw...)
@eval begin
import GLVisualize, GeometryTypes, Reactive, GLAbstraction, GLWindow, Contour
import GeometryTypes: Point2f0, Point3f0, Vec2f0, Vec3f0, GLNormalMesh, SimpleRectangle
import FileIO, Images
export GLVisualize
import Reactive: Signal
import GLAbstraction: Style
import GLVisualize: visualize
import Plots.GL
import UnicodeFun
Plots.slice_arg(img::Images.AbstractImage, idx::Int) = img
is_marker_supported(::GLVisualizeBackend, shape::GLVisualize.AllPrimitives) = true
is_marker_supported{Img<:Images.AbstractImage}(::GLVisualizeBackend, shape::Union{Vector{Img}, Img}) = true
is_marker_supported{C<:Colorant}(::GLVisualizeBackend, shape::Union{Vector{Matrix{C}}, Matrix{C}}) = true
is_marker_supported(::GLVisualizeBackend, shape::Shape) = true
const GL = Plots
end
end
function add_backend_string(b::GLVisualizeBackend)
"""
For those incredibly brave souls who assume full responsibility for what happens next...
There's an easy way to get what you need for the GLVisualize backend to work (until Pkg3 is usable):
Pkg.clone("https://github.com/tbreloff/MetaPkg.jl")
using MetaPkg
meta_checkout("MetaGL")
See the MetaPkg readme for details...
"""
end
# ---------------------------------------------------------------------------
# initialize the figure/window
# function _create_backend_figure(plt::Plot{GLVisualizeBackend})
# # init a screen
#
# GLPlot.init()
# end
const _glplot_deletes = []
function close_child_signals!(screen)
for child in screen.children
for (k, s) in child.inputs
empty!(s.actions)
end
for (k, cam) in child.cameras
for f in fieldnames(cam)
s = getfield(cam, f)
if isa(s, Signal)
close(s, false)
end
end
end
empty!(child.cameras)
close_child_signals!(child)
end
return
end
function empty_screen!(screen)
if isempty(_glplot_deletes)
close_child_signals!(screen)
empty!(screen)
empty!(screen.cameras)
for (k, s) in screen.inputs
empty!(s.actions)
end
empty!(screen)
else
for del_signal in _glplot_deletes
push!(del_signal, true) # trigger delete
end
empty!(_glplot_deletes)
end
nothing
end
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.waiting_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
)
for (k, s) in screen.inputs # copy signals, so we can clean them up better
screen.inputs[k] = map(identity, s)
end
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!(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[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) # 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, d, 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{T <: FixedVector}(::Type{T}, vec::T) = vec
to_vec{T <: FixedVector}(::Type{T}, s::Number) = T(s)
to_vec{T <: FixedVector{2}}(::Type{T}, vec::FixedVector{3}) = T(vec[1], vec[2])
to_vec{T <: FixedVector{3}}(::Type{T}, vec::FixedVector{2}) = T(vec[1], vec[2], 0)
to_vec{T <: FixedVector}(::Type{T}, vecs::AbstractVector) = map(x-> to_vec(T, x), vecs)
function extract_marker(d, kw_args)
dim = Plots.is3d(d) ? 3 : 2
scaling = dim == 3 ? 0.003 : 2
if haskey(d, :markershape)
shape = d[: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(d, :markersize)
msize = d[:markersize]
kw_args[:scale] = to_vec(GeometryTypes.Vec{dim, Float32}, msize .* scaling)
end
if haskey(d, :offset)
kw_args[:offset] = d[:offset]
end
# get the color
key = :markercolor
haskey(d, key) || return
c = gl_color(d[key])
if isa(c, AbstractVector) && d[:marker_z] != nothing
extract_colornorm(d, kw_args)
kw_args[:color] = nothing
kw_args[:color_map] = c
kw_args[:intensity] = convert(Vector{Float32}, d[:marker_z])
else
kw_args[:color] = c
end
key = :markerstrokecolor
haskey(d, key) || return
c = gl_color(d[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(d[:markerstrokewidth])
end
end
function _extract_surface(d::Plots.Surface)
d.surf
end
function _extract_surface(d::AbstractArray)
d
end
# TODO when to transpose??
function extract_surface(d)
map(_extract_surface, (d[:x], d[:y], d[:z]))
end
function topoints{P}(::Type{P}, array)
P[x for x in zip(array...)]
end
function extract_points(d)
dim = is3d(d) ? 3 : 2
array = (d[:x], d[:y], d[:z])[1:dim]
topoints(Point{dim, Float32}, array)
end
function make_gradient{C <: Colorant}(grad::Vector{C})
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(d, kw_args)
if d[:marker_z] == nothing
c = scalar_color(d, :fill)
extract_c(d, kw_args, :fill)
if isa(c, Colorant)
kw_args[:color] = c
else
kw_args[:color] = nothing
kw_args[:color_map] = make_gradient(c)
clims = d[: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(extrema(d[:y]))
end
end
else
kw_args[:color] = nothing
clims = d[: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(extrema(d[:y]))
else
error("Unsupported limits: $clims")
end
kw_args[:intensity] = convert(Vector{Float32}, d[:marker_z])
kw_args[:color_map] = gl_color_map(d, :marker)
end
end
function extract_stroke(d, kw_args)
extract_c(d, kw_args, :line)
if haskey(d, :linewidth)
kw_args[:thickness] = d[:linewidth] * 3
end
end
function extract_color(d, sym)
d[Symbol("$(sym)color")]
end
gl_color(c::PlotUtils.ColorGradient) = c.colors
gl_color{T<:Colorant}(c::Vector{T}) = 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(d, sym)
gl_color(extract_color(d, sym))
end
function gl_color_map(d, sym)
colors = extract_color(d, 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) = 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[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 = linspace(first(points),last(points), 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(d, kw_args)
haskey(d, :linestyle) || return
ls = d[:linestyle]
lw = d[:linewidth]
kw_args[:thickness] = 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(d, 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(d, kw_args)
xmin, xmax = extrema(d[:x]); ymin, ymax = extrema(d[: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(d, kw_args)
clims = d[: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(d, :marker_z) && d[:marker_z] != nothing
d[:marker_z]
elseif haskey(d, :line_z) && d[:line_z] != nothing
d[:line_z]
elseif isa(d[:z], Plots.Surface)
d[:z].surf
else
d[:y]
end
kw_args[:color_norm] = Vec2f0(extrema(z))
kw_args[:intensity] = map(Float32, collect(z))
end
end
function extract_gradient(d, kw_args, sym)
key = Symbol("$(sym)color")
haskey(d, key) || return
c = make_gradient(d[key])
kw_args[:color] = nothing
extract_colornorm(d, kw_args)
kw_args[:color_map] = c
return
end
function extract_c(d, kw_args, sym)
key = Symbol("$(sym)color")
haskey(d, key) || return
c = gl_color(d[key])
kw_args[:color] = nothing
kw_args[:color_map] = nothing
kw_args[:color_norm] = nothing
if (
isa(c, AbstractVector) &&
((haskey(d, :marker_z) && d[:marker_z] != nothing) ||
(haskey(d, :line_z) && d[:line_z] != nothing))
)
extract_colornorm(d, kw_args)
kw_args[:color_map] = c
else
kw_args[:color] = c
end
return
end
function extract_stroke(d, kw_args, sym)
key = Symbol("$(sym)strokecolor")
haskey(d, key) || return
c = gl_color(d[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(d[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
)
d = Dict(
:linestyle => style,
:linewidth => thickness,
:linecolor => color
)
Plots.extract_linestyle(d, 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 align == :top
return -Vec2f0(xmove/2f0, yscale)
elseif align == :right
return -Vec2f0(xmove, yscale/2f0)
else
error("Align $align not known")
end
end
function align_offset(startpos, lastpos, atlas, rscale, font, align::Vec)
xscale, yscale = GLVisualize.glyph_scale!('X', rscale)
xmove = (lastpos-startpos)[1] + xscale
return -Vec2f0(xmove, yscale) .* align
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, lims, m, text = "",
positions = Point2f0[], offsets=Vec2f0[]
)
sz = pointsize(axis[:tickfont])
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 = isx ? (x, y) : (y, x)
_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) do pos
pos .+ alignoff
end
append!(positions, position)
append!(offsets, offset)
text *= str
end
text, positions, offsets
end
function 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) 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, spine_segs, grid_segs = Plots.axis_drawing_info(sp)
xaxis = sp[:xaxis]; yaxis = sp[:yaxis]
c = Colors.color(Plots.gl_color(sp[:foreground_color_grid]))
axis_vis = []
if sp[:grid]
grid = draw_grid_lines(sp, grid_segs, 1f0, :dot, model, RGBA(c, 0.3f0))
push!(axis_vis, grid)
end
if alpha(xaxis[:foreground_color_border]) > 0
spine = draw_grid_lines(sp, spine_segs, 1f0, :solid, model, RGBA(c, 1.0f0))
push!(axis_vis, spine)
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))
ticklabels = map(model) do m
mirror = xaxis[:mirror]
t, positions, offsets = draw_ticks(xaxis, xticks, true, ylim, m)
mirror = xaxis[:mirror]
t, positions, offsets = draw_ticks(
yaxis, yticks, false, xlim, m,
t, positions, offsets
)
end
kw_args = Dict{Symbol, Any}(
:position => map(x-> x[2], ticklabels),
:offset => map(last, ticklabels),
:color => fcolor,
:relative_scale => pointsize(xaxis[:tickfont]),
:scale_primitive => false
)
push!(axis_vis, visualize(map(first, ticklabels), Style(:default), kw_args))
end
area_w = GeometryTypes.widths(area)
if sp[:title] != ""
tf = sp[:titlefont]; color = gl_color(sp[:foreground_color_title])
font = Plots.Font(tf.family, tf.pointsize, :hcenter, :top, tf.rotation, 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, text(xy, t, kw))
end
if xaxis[:guide] != ""
tf = xaxis[:guidefont]; color = gl_color(xaxis[:foreground_color_guide])
xy = Point2f0(area.w/2, - pointsize(tf)/2)
font = Plots.Font(tf.family, tf.pointsize, :hcenter, :bottom, tf.rotation, color)
kw = Dict(:model => text_model(font, xy), :scale_primitive => true)
t = PlotText(xaxis[:guide], font)
extract_font(font, kw)
push!(axis_vis, text(xy, t, kw))
end
if yaxis[:guide] != ""
tf = yaxis[:guidefont]; color = gl_color(yaxis[:foreground_color_guide])
font = Plots.Font(tf.family, tf.pointsize, :hcenter, :top, 90f0, 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, 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(d, kw_args)
x, y = d[:x], d[:y]
nx, ny = length(x), length(y)
axis = d[:subplot][isvertical(d) ? :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
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 = d[:fillrange]
if fillto == nothing
fillto = 0
end
# create the bar shapes by adding x/y segments
positions, scales = Array(Point2f0, ny), Array(Vec2f0, 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(d)
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(d, kw_args)
kwbox = copy(kw_args)
range = 1.5; notch = false
x, y = d[:x], d[: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, linspace(0,1,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!(d[:subplot][:xaxis], glabel)[1]
hw = d[:bar_width] == nothing ? Plots._box_halfwidth*2 : cycle(d[: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 = 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(d, kw_args)
outlier_kw = Dict(
:model => kw_args[:model],
:color => scalar_color(d, :fill),
:stroke_width => Float32(d[:markerstrokewidth]),
:stroke_color => scalar_color(d, :markerstroke),
)
lines_kw = Dict(
:model => kw_args[:model],
:stroke_width => d[:linewidth],
:stroke_color => scalar_color(d, :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 && !isnull(anns.baseshape)
# we use baseshape to overwrite the markershape attribute
# with a list of custom shapes for each
msw, msh = anns.scalefactor
offsets = Array(Vec2f0, 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)
d = series.d
st = d[: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(d)
extract_gradient(d, kw_args, :fill)
z = Plots.transpose_z(d, z, false)
if isa(x, AbstractMatrix) && isa(y, AbstractMatrix)
x, y = Plots.transpose_z(d, x, false), Plots.transpose_z(d, y, false)
end
if st == :wireframe
kw_args[:wireframe] = true
kw_args[:stroke_color] = d[:linecolor]
kw_args[:stroke_width] = Float32(d[:linewidth]/100f0)
end
vis = GL.gl_surface(x, y, z, kw_args)
elseif (st in (:path, :path3d)) && d[:linewidth] > 0
kw = copy(kw_args)
points = Plots.extract_points(d)
extract_linestyle(d, kw)
vis = GL.gl_lines(points, kw)
if d[:markershape] != :none
kw = copy(kw_args)
extract_stroke(d, kw)
extract_marker(d, kw)
vis2 = GL.gl_scatter(copy(points), kw)
vis = [vis; vis2]
end
if d[:fillrange] != nothing
kw = copy(kw_args)
fr = d[:fillrange]
ps = if all(x-> x >= 0, diff(d[:x])) # if is monotonic
vcat(points, Point2f0[(points[i][1], cycle(fr, i)) for i=length(points):-1:1])
else
points
end
extract_c(d, kw, :fill)
vis = [GL.gl_poly(ps, kw), vis]
end
elseif st in (:scatter, :scatter3d) #|| d[:markershape] != :none
extract_marker(d, kw_args)
points = extract_points(d)
vis = GL.gl_scatter(points, kw_args)
elseif st == :shape
extract_c(d, kw_args, :fill)
vis = GL.gl_shape(d, kw_args)
elseif st == :contour
x,y,z = extract_surface(d)
z = transpose_z(d, z, false)
extract_extrema(d, kw_args)
extract_gradient(d, kw_args, :fill)
kw_args[:fillrange] = d[:fillrange]
kw_args[:levels] = d[:levels]
vis = GL.gl_contour(x,y,z, kw_args)
elseif st == :heatmap
x,y,z = extract_surface(d)
extract_gradient(d, kw_args, :fill)
extract_extrema(d, kw_args)
extract_limits(sp, d, kw_args)
vis = GL.gl_heatmap(x,y,z, kw_args)
elseif st == :bar
extract_c(d, kw_args, :fill)
extract_stroke(d, kw_args, :marker)
vis = gl_bar(d, kw_args)
elseif st == :image
extract_extrema(d, kw_args)
vis = GL.gl_image(d[:z].surf, kw_args)
elseif st == :boxplot
extract_c(d, kw_args, :fill)
vis = gl_boxplot(d, kw_args)
elseif st == :volume
volume = d[:y]
_d = copy(d)
_d[:y] = 0:1
_d[:x] = 0:1
kw_args = KW()
extract_gradient(_d, 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(d, :hover) && !(d[:hover] in (false, :none, nothing))
hover(vis, d[: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, d[:x], d[:y])
txt_args = Dict{Symbol, Any}(:model => eye(GLAbstraction.Mat4f0))
x, y = Reactive.value(model_m) * Vec{4, Float32}(x, y, 0, 1)
extract_font(font, txt_args)
t = 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 = Images.Image(map(RGB{U8}, buff),
colorspace = "sRGB",
spatialorder = ["y", "x"]
)
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.h, rect.w) # seems to be flipped
visualize(img, Style(:default), kw_args)
end
function handle_segment{P}(lines, line_segments, points::Vector{P}, segment)
(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(d, kw_args)
points = Plots.extract_points(d)
result = []
for rng in iter_segments(d[:x], d[: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, Range) && isa(y, Range)
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) -> sub2ind(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] = 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{1, 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(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(extrema(z)))
get!(kw_args, :color_map, Plots.make_gradient(cgrad()))
delete!(kw_args, :intensity)
I = GLVisualize.Intensity{1, 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(d, w, ho)
kw = KW()
extract_stroke(d, kw)
extract_marker(d, 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 = []
d = series.d
st = d[:seriestype]
kw_args = KW()
if (st in (:path, :path3d)) && d[:linewidth] > 0
points = Point2f0[(0, ho), (w, ho)]
kw = KW()
extract_linestyle(d, kw)
append!(result, GL.gl_lines(points, kw))
if d[:markershape] != :none
push!(result, label_scatter(d, w, ho))
end
elseif st in (:scatter, :scatter3d) #|| d[:markershape] != :none
push!(result, label_scatter(d, w, ho))
else
extract_c(d, 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
color = sp[:foreground_color_legend]
ft = sp[:legendfont]
font = Plots.Font(ft.family, ft.pointsize, :left, :bottom, 0.0, 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, 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