diff --git a/NEWS.md b/NEWS.md index e1b8dd49b..38e7c3f00 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/docs/src/gallery/guides.md b/docs/src/gallery/guides.md index 1499c3004..72577e409 100644 --- a/docs/src/gallery/guides.md +++ b/docs/src/gallery/guides.md @@ -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 diff --git a/docs/src/man/themes.md b/docs/src/man/themes.md index 4aea2d51b..547f48100 100755 --- a/docs/src/man/themes.md +++ b/docs/src/man/themes.md @@ -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) diff --git a/src/Gadfly.jl b/src/Gadfly.jl index 7d9ec6e31..0d9af8f01 100755 --- a/src/Gadfly.jl +++ b/src/Gadfly.jl @@ -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) @@ -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 diff --git a/src/aesthetics.jl b/src/aesthetics.jl index 6aa67d9bc..443bd9302 100755 --- a/src/aesthetics.jl +++ b/src/aesthetics.jl @@ -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}) @@ -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}() diff --git a/src/guide.jl b/src/guide.jl index 1a7fd0284..3140a13ed 100644 --- a/src/guide.jl +++ b/src/guide.jl @@ -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) @@ -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") @@ -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) @@ -1254,4 +1254,7 @@ function render(guide::Annotation, theme::Gadfly.Theme, end -end # module Guide +include("guide/keys.jl") + + +end # module Guide \ No newline at end of file diff --git a/src/guide/keys.jl b/src/guide/keys.jl new file mode 100644 index 000000000..2fa86767f --- /dev/null +++ b/src/guide/keys.jl @@ -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 + + + + diff --git a/src/theme.jl b/src/theme.jl index cebe5afd7..bea84e267 100755 --- a/src/theme.jl +++ b/src/theme.jl @@ -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 diff --git a/test/testscripts/Guide_shapekey.jl b/test/testscripts/Guide_shapekey.jl new file mode 100644 index 000000000..8994f6f00 --- /dev/null +++ b/test/testscripts/Guide_shapekey.jl @@ -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)