Skip to content

Commit

Permalink
Merge pull request #1379 from Mattriks/guide_sizekey
Browse files Browse the repository at this point in the history
Size revamp 2: Guide.sizekey
  • Loading branch information
bjarthur authored Jan 18, 2020
2 parents 13da0b0 + 749c3e2 commit fb97e75
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 46 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Each release typically has a number of minor bug fixes beyond what is listed her

# Version 1.x

* Add `Guide.sizekey` (#1379)
* Add `Scale.size_discrete2` (#1376)
* Add `Geom.blank` (#1345)
* Support DataFrames.jl 0.19 changes in indexing (#1318)
Expand Down
22 changes: 22 additions & 0 deletions docs/src/gallery/guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,28 @@ plot(Dsleep, x=:BodyWt, y=:BrainWt, Geom.point, color=:Vore, shape=:SleepTime,
```


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

```@example
using Compose, Gadfly, RDatasets
set_default_plot_size(14cm, 8cm)
Titanic = dataset("datasets", "Titanic")
Class = by(Titanic, :Class, :Freq=>sum)
Titanic = join(Titanic[Titanic.Survived.=="Yes",:], Class, on=:Class)
Titanic.prcnt = 100*Titanic.Freq./Titanic.Freq_sum
sizemap = n->range(3pt, 8pt, length=n)
plot(Titanic, Scale.x_log10, Scale.y_log10,
x=:Freq, y=:prcnt, color=:Age, shape=:Sex, size=:Class,
Scale.size_discrete2(sizemap), Guide.sizekey(title="Passenger\n Class"),
Guide.colorkey(pos=[0.1, -0.3h]), Guide.shapekey(pos=[0.5, -0.31h]),
Guide.ylabel("% of Passenger Class"),
Theme(discrete_highlight_color=identity, alphas=[0.1], key_swatch_color="grey",
key_swatch_shape=Shape.circle, point_size=3pt) )
```


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

```@example
Expand Down
4 changes: 2 additions & 2 deletions docs/src/gallery/scales.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ tipsm = by(tips, [:Day, :Sex], :TotalBill=>mean, :Tip=>mean)
Scale.shape_discrete(levels=["Thur","Fri","Sat","Sun"]),
Guide.shapekey(pos=[14.5, 3.8]), Guide.colorkey(pos=[16, 3.87]),
Theme(discrete_highlight_color=identity, alphas=[0.1],
point_size=8pt, key_swatch_color="slate gray")
point_size=5pt, key_swatch_color="slate gray")
)
```

Expand All @@ -197,7 +197,7 @@ sizemap = n->range(4pt, 12pt, length=n)
plot(Titanic, Scale.x_log10, Scale.y_log10,
x=:Freq, y=:prcnt, size=:Class, color=:Age, shape=:Sex,
Scale.size_discrete2(sizemap, levels=["1st","2nd","3rd","Crew"]),
Guide.colorkey(pos=[0.1, -0.3h]), Guide.shapekey(pos=[0.5, -0.3h]),
Guide.colorkey(pos=[0.1, -0.3h]), Guide.shapekey(pos=[0.5, -0.31h]),
Guide.ylabel("% of Passenger Class"),
Theme(discrete_highlight_color=identity, alphas=[0.1], key_swatch_color="grey")
)
Expand Down
4 changes: 2 additions & 2 deletions docs/src/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ For some discrete scales, there is a corresponding palette in `Theme()`.
| `y` | `y_discrete` | `yticks` | |
| `color` | `color_discrete` | `colorkey` | |
| `shape` | `shape_discrete` | `shapekey` | `point_shapes` |
| `size` | `size_discrete` | sizekey (tbd) | `point_size_min`, `point_size_max` |
| | `size_discrete2`| | `discrete_sizemap` |
| `size` | `size_discrete` | --- | `point_size_min`, `point_size_max` |
| | `size_discrete2`| `sizekey` | `discrete_sizemap` |
| `linestyle` | `linestyle_discrete` | linekey (tbd) | `line_style` |
| `alpha` | `alpha_discrete` | alphakey (tbd) | `alphas` |
| `group` | `group_discrete` | | |
Expand Down
4 changes: 2 additions & 2 deletions src/Gadfly.jl
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ function render_prepare(plot::Plot)
datas..., subplot_datas...)

# set defaults for key titles
keyvars = [:color, :shape]
keyvars = [:color, :shape, :size]
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)
Expand Down Expand Up @@ -704,7 +704,7 @@ function render_prepare(plot::Plot)
Stat.apply_statistics(statistics, scales, coord, plot_aes)

# Add some default guides determined by defined aesthetics
keytypes = [Guide.ColorKey, Guide.ShapeKey]
keytypes = [Guide.ColorKey, Guide.ShapeKey, Guide.SizeKey]
supress_keys = false
for layer in plot.layers
if isa(layer.geom, Geom.SubplotGeometry) && any(haskey.((layer.geom.guides,), keytypes))
Expand Down
138 changes: 108 additions & 30 deletions src/guide/keys.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function Guide.render(guide::Guide.ShapeKey, theme::Gadfly.Theme, aes::Gadfly.Ae
colors = collect(keys(aes.color_key_colors))
end

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

position = right_guide_position
Expand All @@ -65,26 +65,26 @@ 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])
function render_discrete_key(labels::Vector{String}, title_ctx::Context, title_width::Measure, theme::Gadfly.Theme;
colors=[nothing], aes_color_label=nothing, shapes=[nothing], sizes=[nothing])

n = max(length(colors), length(shapes))
n = max(length(colors), length(shapes), length(sizes))
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))
(sizes[1]==nothing) && (sizes = fill((theme.key_swatch_size==nothing) ? theme.point_size : theme.key_swatch_size, 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)...)
extents = text_extents(theme.key_label_font, theme.key_label_font_size, values(labels)...)
text_widths, text_heights = first.(extents), last.(extents)

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
swatch_size = 2*maximum(sizes)
entry_height = max(swatch_size, maximum(text_heights)) + ypad

# return a context with a lyout of numcols columns
function make_layout(numcols)
Expand All @@ -102,8 +102,8 @@ function render_discrete_key(labels::Vector{String}, title_ctx::Context, title_w
if m == n
colwidths[i] = 0mm
else
colwidth = maximum([width for (width, height) in extents[m+1:m+nrows]])
colwidth += swatch_size + 2xpad
colwidth = maximum(text_widths[m+1:m+nrows])
colwidth += entry_height + xpad
colwidths[i] = colwidth
m += nrows
end
Expand All @@ -118,32 +118,30 @@ function render_discrete_key(labels::Vector{String}, title_ctx::Context, title_w

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

ctx = context(0, yoff + title_height,
ctxwidth, ctxheight - title_height,
units=UnitBox(0, 0, 1, colrows[1]))
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]

for (colwidth, nrows) in zip(colwidths, colrows)

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)]
szs = sizes[m+1:m+nrows]

swatches_shapes = [f(x, [y-0.5].*cy, [s]) for (y,(f,s)) in enumerate(zip(shps, szs))]
sw1 = [(context(), s, fill(c), fillopacity(theme.alphas[1]), stroke(theme.discrete_highlight_color(c)))
for (s,c) in zip(swatches_shapes, clrs)]
swatches = compose!(context(), sw1...)
swatches = compose!(context(), linewidth(theme.highlight_width), 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]),
swatch_labels = compose!(context(),
text([entry_height+xpad], [0.5:nrows;]*cy, 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)
col = compose!(context(xpos, 0, colwidth), 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]
Expand All @@ -157,7 +155,7 @@ function render_discrete_key(labels::Vector{String}, title_ctx::Context, title_w
compose!(ctx, col)

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

return compose!(outerctx, ctx,
Expand All @@ -166,16 +164,96 @@ function render_discrete_key(labels::Vector{String}, title_ctx::Context, title_w
svgclass("guide colorkey"))
end

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

return map(make_layout, 1:maxcols)
end


function render_key_title2(title::AbstractString, theme::Gadfly.Theme)
title_width, title_height = max_text_extents(theme.key_title_font, theme.key_title_font_size, title)

if theme.guide_title_position == :left
title_form = text(0.0w, title_height, title, hleft, vbottom)
elseif theme.guide_title_position == :center
title_form = text(0.5w, title_height, title, hcenter, vbottom)
elseif theme.guide_title_position == :right
title_form = text(1.0w, title_height, title, hright, vbottom)
else
error("$(theme.guide_title_position) is not a valid guide title position")
end

title_padding = 1.5mm
title_context = compose!(
context(0w, 0h, 1w, title_height + title_padding),
title_form,
stroke(nothing),
font(theme.key_title_font),
fontsize(theme.key_title_font_size),
fill(theme.key_title_color))

return title_context, title_width
end


struct SizeKey <: Gadfly.GuideElement
title::AbstractString
labels::Vector{String}
pos::Vector{Compose.MeasureOrNumber}
visible::Bool
end
SizeKey(;title="Size", labels=String[], pos=[], visible=true) = SizeKey(title, labels, pos, visible)
SizeKey(v::Nothing) = SizeKey(visible=false)
SizeKey(title::AbstractString, labels::Vector{String}, pos::Vector) = SizeKey(title, labels, pos, true)

"""
Guide.sizekey[(; title="size", labels=String[], pos=[])]
Guide.sizekey(title, labels, pos)
Enable control of the sizekey. Set the key `title` and the item `labels`.
`pos` overrides [Theme(key_position=)](@ref Gadfly) 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. Currently `Guide.sizekey` will only work by adding
`Scale.size_discrete2` to the plot. `Guide.sizekey(nothing)` will hide the key.
"""
const sizekey = SizeKey

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

(theme.key_position==:none || !guide.visible || aes.size_key_vals===nothing) && return PositionedGuide[]
gpos = guide.pos
(theme.key_position==:inside) && isempty(gpos) && (gpos = [0.7w, 0.25h])

# Aesthetics for keys: size_key_title, size_label (Function), size_key_vals (AbstractDict)
nsizes = length(unique(aes.size))
guide_title = (guide.title"Size" || aes.size_key_title===nothing) ? guide.title : aes.size_key_title
sizes = collect(keys(aes.size_key_vals))
size_key_labels = isempty(guide.labels) ? aes.size_label(sizes) : guide.labels

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

title_context, title_width = render_key_title2(guide_title, theme)
ctxs = render_discrete_key(size_key_labels, title_context, title_width, theme, sizes=sizes, colors=colors)

position = right_guide_position
if !isempty(gpos)
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 [PositionedGuide(ctxs, 0, position)]
end


5 changes: 4 additions & 1 deletion src/theme.jl
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,12 @@ $(FIELDS)
"Shape used in keys for swatches (Function as in `point_shapes`)",
key_swatch_shape, Function, Shape.square

"Default color used in keys for swatches. Currently works for `Guide.shapekey` (Color)",
"Default color used in keys for swatches. Currently works for `Guide.shapekey` and `Guide.sizekey` (Color)",
key_swatch_color, ColorOrNothing, nothing

"Size of key swatches, will override `Theme(point_size=)`. Currently works for `Guide.shapekey` (Measure)",
key_swatch_size, Union{Measure,Nothing}, nothing

"Where key should be placed relative to the plot panel. One of `:left`, `:right`, `:top`, `:bottom`, `:inside` or `:none`. Setting to `:none` disables the key. Setting to `:inside` places the key in the lower right quadrant of the plot. (Symbol)",
key_position, Symbol, :right

Expand Down
12 changes: 7 additions & 5 deletions test/testscripts/key_columns.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ set_default_plot_size(14cm, 8cm)
# Issue #1344

datas = Gadfly.Data(color= ["D", "A", "C", "D", "A", "C", "D", "D", "A", "B"],
shape=["E", "F", "F", "E", "G", "E", "E", "G", "H"])
scales = [Scale.color_discrete(levels=["A","B","C","D"]), Scale.shape_discrete(levels=["E","F","G","H"]) ]
shape=["E", "F", "F", "E", "G", "E", "E", "G", "H"], size=["I","I","K","J","L","L","I","J","K","L"])
scales = [Scale.color_discrete(levels=["A","B","C","D"]), Scale.shape_discrete(levels=["E","F","G","H"]),
Scale.size_discrete2(levels=["I","J","K","L"])]

aes = Scale.apply_scales(scales, datas)
theme1 = Theme(key_max_columns=4)
guides = [render(g, theme1, aes[1])[1].ctxs for g in (Guide.colorkey(), Guide.shapekey())]
theme1 = Theme(key_max_columns=4, point_size=5pt, key_swatch_shape=Shape.circle)
guides = [render(g, theme1, aes[1])[1].ctxs for g in (Guide.colorkey(), Guide.shapekey(), Guide.sizekey())]

gridstack([guides[i][j] for i in 1:2, j in 1:4])
gridstack([guides[i][j] for i in 1:3, j in 1:4])
4 changes: 3 additions & 1 deletion test/testscripts/point_shape_categorical.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ using Gadfly

set_default_plot_size(6inch, 6inch)

plot(x=rand(100), y=rand(100), shape=rand(["foo","bar","pooh"], 100), Geom.point)
z = rand(["foo","bar","pooh"], 100)
plot(x=rand(100), y=rand(100), shape=z, Geom.point,
Scale.shape_discrete(levels=sort(unique(z))))
4 changes: 3 additions & 1 deletion test/testscripts/point_shape_numerical.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ using Gadfly

set_default_plot_size(6inch, 6inch)

plot(x=rand(100), y=rand(100), shape=rand(1:8, 100), Geom.point)
z = rand(1:8,100)
plot(x=rand(100), y=rand(100), shape=z, Geom.point,
Scale.shape_discrete(levels=sort(unique(z))))
6 changes: 4 additions & 2 deletions test/testscripts/point_size_categorical.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ using Gadfly
set_default_plot_size(6inch, 3inch)

x, y, size = rand(10), rand(10), rand(["foo","bar","pooh"], 10)
p1 = plot(x=x, y=y, size=size, Geom.point)
p2 = plot(x=x, y=y, size=size, Geom.point, Scale.size_discrete2)
levels = sort(unique(size))
p1 = plot(x=x, y=y, size=size, Geom.point, Scale.size_discrete(levels=levels))
p2 = plot(x=x, y=y, size=size, Geom.point, Scale.size_discrete2(levels=levels),
Theme(key_swatch_shape=Shape.circle))

hstack(p1, p2)

0 comments on commit fb97e75

Please sign in to comment.