Skip to content

Commit

Permalink
Guide_shapekey
Browse files Browse the repository at this point in the history
  • Loading branch information
Mattriks committed Jul 1, 2018
1 parent f922f4d commit 071831f
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 25 deletions.
4 changes: 2 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ This is a log of major changes in Gadfly between releases. It is not exhaustive.
Each release typically has a number of minor bug fixes beyond what is listed here.

# Version 0.7.1

* `Geom.contour`: add support for `DataFrame` (#1150)
* Add `Guide.shapekey` (#1156)
* `Geom.contour`: add support for `DataFrame` (#1150)

# Version 0.7.0

Expand Down
17 changes: 17 additions & 0 deletions docs/src/gallery/guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ plot(pointLayer, lineLayer,
```


## [`Guide.shapekey`](@ref)

```@example
using Compose, Gadfly, RDatasets
set_default_plot_size(16cm, 8cm)
Dsleep = dataset("ggplot2", "msleep")
Dsleep = dropmissing!(Dsleep[[:Vore, :Name,:BrainWt,:BodyWt, :SleepTotal]])
Dsleep[:SleepTime] = Dsleep[:SleepTotal] .> 8
plot(Dsleep, x=:BodyWt, y=:BrainWt, Geom.point, color=:Vore, shape=:SleepTime,
Guide.colorkey(pos=[0.05w, -0.25h]),
Guide.shapekey(title="Sleep (hrs)", labels=[">8","≤8"], pos=[0.18w,-0.315h]),
Scale.x_log10, Scale.y_log10,
Theme(point_size=2mm, key_swatch_color="slategrey",
point_shapes=[Shape.utriangle, Shape.dtriangle]) )
```


## [`Guide.title`](@ref)

```@example
Expand Down
2 changes: 2 additions & 0 deletions docs/src/man/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ These parameters can either be used with `Theme` or `style`
* `key_label_font_size`: Font size used for key entry labels. (Measure)
* `key_label_color`: Color used for key entry labels. (Color)
* `key_max_columns`: Maximum number of columns for key entry labels. (Int)
* `key_swatch_shape`: General purpose, will eventually replace `colorkey_swatch_shape` (Function as in `point_shapes`)
* `key_swatch_color`: General purpose, currently works for `Guide.shapekey` (Color)
* `bar_spacing`: Spacing between bars in [`Geom.bar`](@ref). (Measure)
* `boxplot_spacing`: Spacing between boxplots in [`Geom.boxplot`](@ref). (Measure)
* `errorbar_cap_length`: Length of caps on error bars. (Measure)
Expand Down
58 changes: 39 additions & 19 deletions src/Gadfly.jl
Original file line number Diff line number Diff line change
Expand Up @@ -637,20 +637,35 @@ function render_prepare(plot::Plot)
layer_aess = Scale.apply_scales(IterTools.distinct(values(scales)),
datas..., subplot_datas...)

# set default labels
for (i, layer) in enumerate(plot.layers)
if layer_aess[i].color_key_title == nothing &&
haskey(layer.mapping, :color) &&
!isa(layer.mapping[:color], AbstractArray)
layer_aess[i].color_key_title = string(layer.mapping[:color])
end
# set defaults for key titles
keyvars = [:color, :shape]
for (i, layer) in enumerate(plot.layers)
for kv in keyvars
fflag = (getfield(layer_aess[i], Symbol(kv,"_key_title")) == nothing) && haskey(layer.mapping, kv) && !isa(layer.mapping[kv], AbstractArray)
fflag && setfield!(layer_aess[i], Symbol(kv,"_key_title"), string(layer.mapping[kv]))
end
end

for kv in keyvars
fflag = (getfield(layer_aess[1], Symbol(kv,"_key_title")) == nothing) && haskey(plot.mapping, kv) && !isa(plot.mapping[kv], AbstractArray)
fflag && setfield!(layer_aess[1], Symbol(kv,"_key_title"), string(plot.mapping[kv]))
end

if layer_aess[1].color_key_title == nothing &&
haskey(plot.mapping, :color) && !isa(plot.mapping[:color], AbstractArray)
layer_aess[1].color_key_title = string(plot.mapping[:color])
# Auto-update color scale if shape==color
catdatas = vcat(datas, subplot_datas)
shapev = getfield.(catdatas, :shape)
di = (shapev.!=nothing) .& (shapev.== getfield.(catdatas, :color))

supress_colorkey = false
for (aes, data) in zip(layer_aess[di], catdatas[di])
aes.shape_key_title==nothing && (aes.shape_key_title=aes.color_key_title="Shape")
colorf = scales[:color].f
scales[:color] = Scale.color_discrete(colorf, levels=scales[:shape].levels, order=scales[:shape].order)
Scale.apply_scale(scales[:color], [aes], Gadfly.Data(color=getfield(data,:color)) )
supress_colorkey=true
end


# IIa. Layer-wise statistics
if !facet_plot
for (stats, aes) in zip(layer_stats, layer_aess)
Expand All @@ -664,20 +679,25 @@ function render_prepare(plot::Plot)
Stat.apply_statistics(statistics, scales, coord, plot_aes)

# Add some default guides determined by defined aesthetics
supress_colorkey = false
keytypes = [Guide.ColorKey, Guide.ShapeKey]
supress_keys = false
for layer in plot.layers
if isa(layer.geom, Geom.SubplotGeometry) &&
haskey(layer.geom.guides, Guide.ColorKey)
supress_colorkey = true
if isa(layer.geom, Geom.SubplotGeometry) && any(haskey.(layer.geom.guides, keytypes))
supress_keys = true
break
end
end

if !supress_colorkey &&
!all([aes.color === nothing for aes in [plot_aes, layer_aess...]]) &&
!in(Guide.ColorKey, explicit_guide_types) &&
!in(Guide.ManualColorKey, explicit_guide_types)
push!(guides, Guide.colorkey())
if supress_colorkey
deleteat!(keytypes, 1)
deleteat!(keyvars, 1)
end

if !supress_keys
for (KT, kv) in zip(keytypes, keyvars)
fflag = !all([getfield(aes, kv)==nothing for aes in [plot_aes, layer_aess...]])
fflag && !in(KT, explicit_guide_types) && push!(guides, KT())
end
end

# build arrays of scaled aesthetics for layers within subplots
Expand Down
2 changes: 2 additions & 0 deletions src/aesthetics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const NumericalAesthetic =
color_key_continuous, Maybe(Bool)
color_function, Maybe(Function)
titles, Maybe(Dict{Symbol, AbstractString})
shape_key_title, Maybe(AbstractString)

# mark some ticks as initially invisible
xtickvisible, Maybe(Vector{Bool})
Expand All @@ -83,6 +84,7 @@ const NumericalAesthetic =
color_label, Function, showoff
xgroup_label, Function, showoff
ygroup_label, Function, showoff
shape_label, Function, showoff

# pseudo-aesthetics
pad_categorical_x, Nullable{Bool}, Nullable{Bool}()
Expand Down
11 changes: 7 additions & 4 deletions src/guide.jl
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ function render_continuous_color_key(colors::Dict,
end


function render_colorkey_title(title::AbstractString, theme::Gadfly.Theme)
function render_key_title(title::AbstractString, theme::Gadfly.Theme)
title_width, title_height = max_text_extents(theme.key_title_font,
theme.key_title_font_size,
title)
Expand Down Expand Up @@ -443,7 +443,7 @@ function render(guide::ColorKey, theme::Gadfly.Theme,
pretty_labels[color] = join(labels[color], ", ")
end

title_context, title_width = render_colorkey_title(guide_title, theme)
title_context, title_width = render_key_title(guide_title, theme)

theme.colorkey_swatch_shape != :circle && theme.colorkey_swatch_shape != :square &&
error("$(theme.colorkey_swatch_shape) is not a valid color key swatch shape")
Expand Down Expand Up @@ -523,7 +523,7 @@ function render(guide::ManualColorKey, theme::Gadfly.Theme,
guide_title = "Color"
end

title_context, title_width = render_colorkey_title(guide_title, theme)
title_context, title_width = render_key_title(guide_title, theme)

labels = OrderedDict{Color, AbstractString}()
for (c, l) in zip(guide.colors, guide.labels)
Expand Down Expand Up @@ -1254,4 +1254,7 @@ function render(guide::Annotation, theme::Gadfly.Theme,
end


end # module Guide
include("guide/keys.jl")


end # module Guide
180 changes: 180 additions & 0 deletions src/guide/keys.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@



##### NEW KEYS #####

struct ShapeKey <: Gadfly.GuideElement
title::AbstractString
labels::Vector{String}
pos::Vector
end
ShapeKey(;title="Shape", labels=[""], pos=Float64[]) = ShapeKey(title, labels, pos)


"""
Guide.shapekey[(; title="Shape", labels=[""], pos=Float64[])]
Guide.shapekey(title, labels, pos)
Enable control of the auto-generated shapekey. Set the key `title` and the item `labels`.
`pos` overrides [Theme(key_position=)](@ref Parameters) and can be in either
relative (e.g. [0.7w, 0.2h] is the lower right quadrant), absolute (e.g. [0mm,
0mm]), or plot scale (e.g. [0,0]) coordinates.
"""
const shapekey = ShapeKey



function Guide.render(guide::Guide.ShapeKey, theme::Gadfly.Theme, aes::Gadfly.Aesthetics)

(theme.key_position == :none) && return Gadfly.Guide.PositionedGuide[]
gpos = guide.pos
(theme.key_position == :inside) && (gpos == Float64[]) && (gpos = [0.7w, 0.25h])

# Aesthetics for keys: shape_key_title, shape_label (Function), shape_key_shapes (Associative)
nshapes = length(unique(aes.shape))
guide_title = (guide.title!="Shape" || aes.shape_key_title==nothing) ? guide.title : aes.shape_key_title
shape_key_labels = !(guide.labels==[""]) ? guide.labels : aes.shape_label(1:nshapes)

colors = [nothing]
if (aes.shape_key_title !=nothing) && (aes.color_key_title==aes.shape_key_title)
colors = collect(keys(aes.color_key_colors))
end

title_context, title_width = Guide.render_key_title(guide_title, theme)
ctxs = render_discrete_key(shape_key_labels, title_context, title_width, theme, shapes=1:nshapes, colors=colors)

position = right_guide_position
if gpos != Float64[]
position = over_guide_position
ctxs = [compose(context(), (context(gpos[1],gpos[2]), ctxs[1]))]
elseif theme.key_position == :left
position = left_guide_position
elseif theme.key_position == :top
position = top_guide_position
elseif theme.key_position == :bottom
position = bottom_guide_position
end

return [Guide.PositionedGuide(ctxs, 0, position)]
end








function render_discrete_key(labels::Vector{String}, title_ctx::Context, title_width::Measure, theme::Gadfly.Theme;
colors=[nothing], aes_color_label=nothing, shapes=[nothing])

n = max(length(colors), length(shapes))
shape1 = shapes[1]
shapes = (shape1==nothing) ? fill(theme.key_swatch_shape, n) : theme.point_shapes[shapes]
(colors[1]==nothing) && (colors = fill((theme.key_swatch_color==nothing) ? theme.default_color : theme.key_swatch_color, n))

# only consider layouts with a reasonable number of columns
maxcols = theme.key_max_columns < 1 ? 1 : theme.key_max_columns
maxcols = min(n, maxcols)

extents = text_extents(theme.key_label_font,
theme.key_label_font_size,
values(labels)...)

ypad = 1.0mm
title_height = title_ctx.box.a[2]
entry_height = maximum([height for (width, height) in extents]) + ypad
swatch_size = entry_height / 2

# return a context with a lyout of numcols columns
function make_layout(numcols)
colrows = Array{Int}(numcols)
m = n
for i in 1:numcols
colrows[i] = min(m, ceil(Integer, (n / numcols)))
m -= colrows[i]
end

xpad = 1mm
colwidths = Array{Measure}(numcols)
m = 0
for (i, nrows) in enumerate(colrows)
if m == n
colwidths[i] = 0mm
else
colwidth = maximum([width for (width, height) in extents[m+1:m+nrows]])
colwidth += swatch_size + 2xpad
colwidths[i] = colwidth
m += nrows
end
end

ctxwidth = sum(colwidths)
ctxheight = entry_height * colrows[1] + title_height

ctxp = ctxpromise() do draw_context
yoff = 0.5h - ctxheight/2
outerctx = context()

compose!(outerctx, (context(xpad, yoff), title_ctx))

ctx = context(0, yoff + title_height,
ctxwidth, ctxheight - title_height,
units=UnitBox(0, 0, 1, colrows[1]))

m = 0
xpos = 0w
for (i, nrows) in enumerate(colrows)
colwidth = colwidths[i]

x = [0.5cy]
clrs = colors[m+1:m+nrows]
shps = shapes[m+1:m+nrows]
swatches_shapes = [f(x, [y].*cy, [swatch_size/1.5]) for (y,f) in enumerate(shps)]
sw1 = [(context(), s, fill(c)) for (s,c) in zip(swatches_shapes, clrs)]
swatches = compose!(context(), sw1...)

swatch_labels = compose!(
context(),
text([2xpad + swatch_size], [y*cy for y in 1:nrows],
collect(values(labels))[m+1:m+nrows], [hleft], [vcenter]),
font(theme.key_label_font),
fontsize(theme.key_label_font_size),
fill(theme.key_label_color))

col = compose!(context(xpos, yoff), swatches, swatch_labels)
if aes_color_label != nothing
classes = [svg_color_class_from_label(aes_color_label([c])[1]) for c in clrs]
#class_jscalls = ["data(\"color_class\", \"$(c)\")" for c in classes]
compose!(col,
svgclass(classes),
jscall(["""
data(\"color_class\", \"$(c)\")
.click(Gadfly.colorkey_swatch_click)
""" for c in classes]))
end
compose!(ctx, col)

m += nrows
xpos += colwidths[i]
end

return compose!(outerctx, ctx,
# defeat webkit's asinine default drag behavior
jscall("drag(function() {}, function() {}, function() {})"),
svgclass("guide colorkey"))
end

return compose!(
context(minwidth=max(title_width, ctxwidth),
minheight=ctxheight,
units=UnitBox()),
ctxp)
end

return map(make_layout, 1:maxcols)
end




6 changes: 6 additions & 0 deletions src/theme.jl
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ end
# Shape used in color keys for color swatches. Either :square or :circle.
colorkey_swatch_shape, Symbol, :square

# Shape used in keys for swatches.
key_swatch_shape, Function, Shape.square

# Default color used in keys for swatches.
key_swatch_color, ColorOrNothing, nothing

# One of :left, :right, :top, :bottom, :inside, :none determining where color keys
# and the like should be placed.
key_position, Symbol, :right
Expand Down
25 changes: 25 additions & 0 deletions test/testscripts/Guide_shapekey.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Compose, DataFrames, Gadfly

set_default_plot_size(9inch, 3.3inch)

srand(123)
theme1 = Theme(point_size=3mm)
coord1 = Coord.cartesian(xmin=0.0, xmax=6.0)
D = DataFrame(x=1:5, y=rand(5), V1=["A","A","B","B","D"], V2 = string.([1,2,2,3,3]) )


pa = plot(x=1:5, y=[0.77, 0.94, 0.67, 0.39, 0.31], shape=["A","A","B","B","D"], theme1, coord1,
Guide.shapekey(title="Key",labels=["α","β","δ"]),
Guide.title("Guide.shapekey") )

pb = plot(D, x=:x, y=:y, shape=:V1, color=:V1,
Scale.shape_discrete(levels=["D","A","B"]),
theme1, coord1, Guide.title("Shape==Color") )

pc = plot(D, x=:x, y=:y, shape=:V1, color=:V2, coord1,
Guide.colorkey(title="Color"),
Guide.shapekey(title="Shape ", pos=[0.74w,-0.27h]),
Theme(point_size=3mm, key_swatch_color="slategrey"),
Guide.title("Shape!=Color") )

hstack(pa,pb,pc)

0 comments on commit 071831f

Please sign in to comment.