forked from MakieOrg/Makie.jl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
figures.jl
203 lines (164 loc) · 6.81 KB
/
figures.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#=
Figures are supposed to fill the gap that Scenes in combination with Blocks leave.
A scene is supposed to be a generic canvas on which plot objects can be placed and drawn.
Blocks always require one specific type of scene, with a PixelCamera, in order to draw
their visual components there.
Figures also have layouts, which scenes do not have.
This is because every figure needs a layout, while not every scene does.
Figures keep track of the Blocks that are created inside them, which scenes don't.
The idea is there are three types of plotting commands.
They can return either:
- a FigureAxisPlot (figure, axis and plot)
- an AxisPlot (axis and plot)
- or an AbstractPlot (only plot)
This is needed so that users can without much boilerplate create the necessary structures and access them accordingly.
A normal plotting command creates a FigureAxisPlot, which can be splatted into figure, axis and plot. It displays like a figure, so that simple plots show up immediately.
A non-mutating plotting command that references a GridPosition or a GridSubposition creates an axis at that position and returns an AxisPlot, which can be splatted into axis and plot.
All mutating plotting commands return AbstractPlots.
They can either reference a GridPosition or a GridSubposition, in which case it is looked up
if an axis is placed at that position (if not it errors) or it can reference an axis directly to plot into.
=#
get_scene(fig::Figure) = fig.scene
get_scene(fap::FigureAxisPlot) = fap.figure.scene
get_scene(gp::GridLayoutBase.GridPosition) = get_scene(get_figure(gp))
get_scene(gp::GridLayoutBase.GridSubposition) = get_scene(get_figure(gp))
const CURRENT_FIGURE = Ref{Union{Nothing, Figure}}(nothing)
const CURRENT_FIGURE_LOCK = Base.ReentrantLock()
"""
current_figure()
Returns the current active figure (or the last figure created).
Returns `nothing` if there is no current active figure.
"""
current_figure() = lock(()-> CURRENT_FIGURE[], CURRENT_FIGURE_LOCK)
"""
current_figure!(fig)
Set `fig` as the current active figure.
"""
current_figure!(fig) = lock(() -> (CURRENT_FIGURE[] = fig), CURRENT_FIGURE_LOCK)
"""
current_axis()
Returns the current active axis (or the last axis created). Returns `nothing` if there is no current active axis.
"""
current_axis() = current_axis(current_figure())
current_axis(::Nothing) = nothing
current_axis(fig::Figure) = fig.current_axis[]
"""
current_axis!(fig::Figure, ax)
Set `ax` as the current active axis in `fig`.
"""
function current_axis!(fig::Figure, ax)
if ax.parent !== fig
error("This axis' parent is not the given figure")
end
fig.current_axis[] = ax
ax
end
function current_axis!(fig::Figure, ::Nothing)
fig.current_axis[] = nothing
end
"""
current_axis!(ax)
Set an axis `ax`, which must be part of a figure, as the figure's current active axis.
"""
function current_axis!(ax)
fig = ax.parent
if !(fig isa Figure)
error("Axis parent is not a figure but a $(typeof(ax.parent)). Only axes in figures can have current_axis! called on them.")
end
current_axis!(fig, ax)
# if the current axis is in a different figure, we switch to that as well
# so that current_axis and current_figure are not out of sync
current_figure!(fig)
ax
end
to_rectsides(n::Number) = to_rectsides((n, n, n, n))
to_rectsides(t::Tuple{Any, Any, Any, Any}) = GridLayoutBase.RectSides{Float32}(t...)
function Figure(; kwargs...)
kwargs_dict = Dict(kwargs)
padding = pop!(kwargs_dict, :figure_padding, theme(:figure_padding))
scene = Scene(; camera=campixel!, clear = true, kwargs_dict...)
padding = convert(Observable{Any}, padding)
alignmode = lift(Outside ∘ to_rectsides, padding)
layout = GridLayout(scene)
on(alignmode) do al
layout.alignmode[] = al
GridLayoutBase.update!(layout)
end
notify(alignmode)
f = Figure(
scene,
layout,
[],
Attributes(),
Ref{Any}(nothing)
)
# set figure as layout parent so GridPositions can refer to the figure
# if connected correctly
layout.parent = f
f
end
export Figure, current_axis, current_figure, current_axis!, current_figure!
function Base.getindex(fig::Figure, rows, cols, side = GridLayoutBase.Inner())
fig.layout[rows, cols, side]
end
function Base.setindex!(fig::Figure, obj, rows, cols, side = GridLayoutBase.Inner())
fig.layout[rows, cols, side] = obj
obj
end
function Base.setindex!(fig::Figure, obj::AbstractArray, rows, cols)
fig.layout[rows, cols] = obj
obj
end
Base.lastindex(f::Figure, i) = lastindex(f.layout, i)
Base.firstindex(f::Figure, i) = firstindex(f.layout, i)
# for now just redirect figure display/show to the internal scene
Base.show(io::IO, fig::Figure) = show(io, fig.scene)
Base.show(io::IO, ::MIME"text/plain", fig::Figure) = print(io, "Figure()")
# Base.show(io::IO, ::MIME"image/svg+xml", fig::Figure) = show(io, MIME"image/svg+xml"(), fig.scene)
get_figure(gsp::GridLayoutBase.GridSubposition) = get_figure(gsp.parent)
function get_figure(gp::GridLayoutBase.GridPosition)
top_parent = GridLayoutBase.top_parent(gp.layout)
if top_parent isa Figure
top_parent
else
nothing
end
end
"""
resize_to_layout!(fig::Figure)
Resize `fig` so that it fits the current contents of its top `GridLayout`.
If a `GridLayout` contains fixed-size content or aspect-constrained
columns, for example, it is likely that the solved size of the `GridLayout`
differs from the size of the `Figure`. This can result in superfluous
whitespace at the borders, or content clipping at the figure edges.
Once resized, all content should fit the available space, including
the `Figure`'s outer padding.
"""
function resize_to_layout!(fig::Figure)
# it is assumed that all plot objects have been added at this point,
# but it's possible the limits have not been updated, yet,
# so without `update_state_before_display!` it's possible that the layout
# is optimized for the wrong ticks
update_state_before_display!(fig)
bbox = GridLayoutBase.tight_bbox(fig.layout)
new_size = (widths(bbox)...,)
resize!(fig.scene, widths(bbox)...)
new_size
end
function Base.empty!(fig::Figure)
empty!(fig.scene)
empty!(fig.scene.events)
foreach(GridLayoutBase.remove_from_gridlayout!, reverse(fig.layout.content))
trim!(fig.layout)
empty!(fig.content)
fig.current_axis[] = nothing
return
end
# Allow figures to be directly resized by resizing their internal Scene.
# Layouts are already hooked up to this, so it's very simple.
"""
resize!(fig::Figure, width, height)
Resizes the given `Figure` to the size given by `width` and `height`.
If you want to resize the figure to its current layout content, use `resize_to_layout!(fig)` instead.
"""
Makie.resize!(figure::Figure, width::Integer, height::Integer) = resize!(figure.scene, width, height)