From c1baca181cd49bef1a28cd1cf77799fc94047640 Mon Sep 17 00:00:00 2001 From: Thomas Breloff Date: Tue, 24 May 2016 14:25:03 -0400 Subject: [PATCH] plotly subplot layouts --- src/args.jl | 2 + src/backends/plotly.jl | 183 +++++++++++++++++++++++++-------------- src/backends/plotlyjs.jl | 6 ++ src/layouts.jl | 3 +- src/plot.jl | 1 + 5 files changed, 131 insertions(+), 64 deletions(-) diff --git a/src/args.jl b/src/args.jl index 6c75f1fc..6bfef5b2 100644 --- a/src/args.jl +++ b/src/args.jl @@ -214,6 +214,7 @@ const _subplot_defaults = KW( :top_margin => :match, :right_margin => :match, :bottom_margin => :match, + :subplot_index => -1, ) const _axis_defaults = KW( @@ -236,6 +237,7 @@ const _suppress_warnings = KW( :y_discrete_indices => nothing, :z_discrete_indices => nothing, :subplot => nothing, + :subplot_index => nothing, ) # add defaults for the letter versions diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index 654ffa49..151468f4 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -211,6 +211,27 @@ end use_axis_field(ticks) = !(ticks in (nothing, :none)) +# this method gets the start/end in percentage of the canvas for this axis direction +function plotly_domain(sp::Subplot, letter) + figw, figh = sp.plt.attr[:size] + # @show letter,figw,figh sp.plotarea + pcts = bbox_to_pcts(sp.plotarea, figw*px, figh*px) + # @show pcts + i1,i2 = (letter == :x ? (1,3) : (2,4)) + # @show i1,i2 + [pcts[i1], pcts[i1]+pcts[i2]] +end + +# TODO: this should actually take into account labels, font sizes, etc +# sets (left, top, right, bottom) +function plotly_minpad(sp::Subplot) + (12mm, 2mm, 2mm, 8mm) +end + +function _update_min_padding!(sp::Subplot{PlotlyBackend}) + sp.minpad = plotly_minpad(sp) +end + # tickssym(letter) = symbol(letter * "ticks") # limssym(letter) = symbol(letter * "lims") # flipsym(letter) = symbol(letter * "flip") @@ -230,6 +251,14 @@ function plotly_axis(axis::Axis, sp::Subplot) # fgcolor = webcolor(d[:foreground_color]) # tsym = tickssym(letter) + # spidx = sp.attr[:subplot_index] + # d_out[:xaxis] = "x$spidx" + # d_out[:yaxis] = "y$spidx" + if letter in (:x,:y) + ax[:domain] = plotly_domain(sp, letter) + ax[:anchor] = "$(letter==:x ? :y : :x)$(plotly_subplot_index(sp))" + end + rot = d[:rotation] if rot != 0 ax[:tickangle] = rot @@ -279,76 +308,78 @@ end function plotly_layout(plt::Plot) d_out = KW() - # for now, we only support 1 subplot - if length(plt.subplots) > 1 - warn("Subplots not supported yet") - end - sp = plt.subplots[1] + # # for now, we only support 1 subplot + # if length(plt.subplots) > 1 + # warn("Subplots not supported yet") + # end + # sp = plt.subplots[1] d_out[:width], d_out[:height] = plt.attr[:size] - - # set the fields for the plot - d_out[:title] = sp.attr[:title] - d_out[:titlefont] = plotlyfont(sp.attr[:titlefont], webcolor(sp.attr[:foreground_color_title])) - - # TODO: use subplot positioning logic - d_out[:margin] = KW(:l=>35, :b=>30, :r=>8, :t=>20) - - d_out[:plot_bgcolor] = webcolor(sp.attr[:background_color_inside]) d_out[:paper_bgcolor] = webcolor(plt.attr[:background_color_outside]) - # TODO: x/y axis tick values/labels + for sp in plt.subplots + sp_out = KW() + spidx = plotly_subplot_index(sp) - # if any(is3d, seriesargs) - if is3d(sp) - d_out[:scene] = KW( - :xaxis => plotly_axis(sp.attr[:xaxis], sp), - :yaxis => plotly_axis(sp.attr[:yaxis], sp), - :xzxis => plotly_axis(sp.attr[:zaxis], sp), - ) - else - d_out[:xaxis] = plotly_axis(sp.attr[:xaxis], sp) - d_out[:yaxis] = plotly_axis(sp.attr[:yaxis], sp) - end + # set the fields for the plot + d_out[:title] = sp.attr[:title] + d_out[:titlefont] = plotlyfont(sp.attr[:titlefont], webcolor(sp.attr[:foreground_color_title])) - # legend - d_out[:showlegend] = sp.attr[:legend] != :none - if sp.attr[:legend] != :none - d_out[:legend] = KW( - :bgcolor => webcolor(sp.attr[:background_color_legend]), - :bordercolor => webcolor(sp.attr[:foreground_color_legend]), - :font => plotlyfont(sp.attr[:legendfont]), - ) - end + # # TODO: use subplot positioning logic + # d_out[:margin] = KW(:l=>35, :b=>30, :r=>8, :t=>20) + d_out[:margin] = KW(:l=>0, :b=>0, :r=>0, :t=>30) - # if haskey(plt.attr, :annotation_list) - # append!(plt.attr[:annotation_list], anns) - # else - # plt.attr[:annotation_list] = anns - # end + d_out[:plot_bgcolor] = webcolor(sp.attr[:background_color_inside]) - # annotations - anns = get(sp.attr, :annotations, []) - d_out[:annotations] = if isempty(anns) - KW[] - else - KW[get_annotation_dict(ann...) for ann in anns] - end + # TODO: x/y axis tick values/labels - # # arrows - # for sargs in seriesargs - # a = sargs[:arrow] - # if sargs[:seriestype] in (:path, :line) && typeof(a) <: Arrow - # add_arrows(sargs[:x], sargs[:y]) do xyprev, xy - # push!(d_out[:annotations], get_annotation_dict_for_arrow(sargs, xyprev, xy, a)) - # end - # end - # end - # dumpdict(d_out,"",true) - # @show d_out[:annotations] + # if any(is3d, seriesargs) + if is3d(sp) + d_out[:scene] = KW( + symbol("xaxis$spidx") => plotly_axis(sp.attr[:xaxis], sp), + symbol("yaxis$spidx") => plotly_axis(sp.attr[:yaxis], sp), + symbol("zaxis$spidx") => plotly_axis(sp.attr[:zaxis], sp), + ) + else + d_out[symbol("xaxis$spidx")] = plotly_axis(sp.attr[:xaxis], sp) + d_out[symbol("yaxis$spidx")] = plotly_axis(sp.attr[:yaxis], sp) + end - if ispolar(sp) - d_out[:direction] = "counterclockwise" + # legend + d_out[:showlegend] = sp.attr[:legend] != :none + if sp.attr[:legend] != :none + d_out[:legend] = KW( + :bgcolor => webcolor(sp.attr[:background_color_legend]), + :bordercolor => webcolor(sp.attr[:foreground_color_legend]), + :font => plotlyfont(sp.attr[:legendfont]), + ) + end + + # annotations + anns = get(sp.attr, :annotations, []) + d_out[:annotations] = if isempty(anns) + KW[] + else + KW[get_annotation_dict(ann...) for ann in anns] + end + + # # arrows + # for sargs in seriesargs + # a = sargs[:arrow] + # if sargs[:seriestype] in (:path, :line) && typeof(a) <: Arrow + # add_arrows(sargs[:x], sargs[:y]) do xyprev, xy + # push!(d_out[:annotations], get_annotation_dict_for_arrow(sargs, xyprev, xy, a)) + # end + # end + # end + # dumpdict(d_out,"",true) + # @show d_out[:annotations] + + if ispolar(sp) + d_out[:direction] = "counterclockwise" + end + + d_out end d_out @@ -374,14 +405,25 @@ const _plotly_markers = KW( :hline => "line-ew", ) +function plotly_subplot_index(sp::Subplot) + spidx = sp.attr[:subplot_index] + spidx == 1 ? "" : spidx +end + + # get a dictionary representing the series params (d is the Plots-dict, d_out is the Plotly-dict) function plotly_series(plt::Plot, series::Series) d = series.d + sp = d[:subplot] d_out = KW() + # these are the axes that the series should be mapped to + spidx = plotly_subplot_index(sp) + d_out[:xaxis] = "x$spidx" + d_out[:yaxis] = "y$spidx" + x, y = collect(d[:x]), collect(d[:y]) d_out[:name] = d[:label] - st = d[:seriestype] isscatter = st in (:scatter, :scatter3d) hasmarker = isscatter || d[:markershape] != :none @@ -450,7 +492,12 @@ function plotly_series(plt::Plot, series::Series) elseif st == :pie d_out[:type] = "pie" - d_out[:labels] = x + d_out[:labels] = if haskey(d,:x_discrete_indices) + dvals = sp.attr[:xaxis].d[:discrete_values] + [dvals[idx] for idx in d[:x_discrete_indices]] + else + d[:x] + end d_out[:values] = y d_out[:hoverinfo] = "label+percent+name" @@ -556,14 +603,24 @@ end # ---------------------------------------------------------------- +# compute layout bboxes +function plotly_finalize(plt::Plot) + w, h = plt.attr[:size] + plt.layout.bbox = BoundingBox(0mm, 0mm, w*px, h*px) + update_child_bboxes!(plt.layout) +end + function Base.writemime(io::IO, ::MIME"image/png", plt::AbstractPlot{PlotlyBackend}) + plotly_finalize(plt) writemime_png_from_html(io, plt) end function Base.writemime(io::IO, ::MIME"text/html", plt::AbstractPlot{PlotlyBackend}) + plotly_finalize(plt) write(io, html_head(plt) * html_body(plt)) end function Base.display(::PlotsDisplay, plt::AbstractPlot{PlotlyBackend}) - standalone_html_window(plt) + plotly_finalize(plt) + standalone_html_window(plt) end diff --git a/src/backends/plotlyjs.jl b/src/backends/plotlyjs.jl index 3e8cd492..53f885af 100644 --- a/src/backends/plotlyjs.jl +++ b/src/backends/plotlyjs.jl @@ -211,7 +211,13 @@ end # writemime_png_from_html(io, plt) # end + +function _update_min_padding!(sp::Subplot{PlotlyBackend}) + sp.minpad = plotly_minpad(sp) +end + function Base.display(::PlotsDisplay, plt::Plot{PlotlyJSBackend}) + plotly_finalize(plt) display(plt.o) end diff --git a/src/layouts.jl b/src/layouts.jl index 10a82776..94c233df 100644 --- a/src/layouts.jl +++ b/src/layouts.jl @@ -67,7 +67,8 @@ function crop(parent::BoundingBox, child::BoundingBox) BoundingBox(l, t, w, h) end -# convert a bounding box from absolute coords to percentages... returns an array of percentages of figure size: [left, bottom, width, height] +# convert a bounding box from absolute coords to percentages... +# returns an array of percentages of figure size: [left, bottom, width, height] function bbox_to_pcts(bb::BoundingBox, figw, figh, flipy = true) mms = Float64[f(bb).value for f in (left,bottom,width,height)] if flipy diff --git a/src/plot.jl b/src/plot.jl index 70187997..1b17e218 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -57,6 +57,7 @@ function plot(args...; kw...) plt.layout, plt.subplots, plt.spmap = build_layout(plt.attr) for (idx,sp) in enumerate(plt.subplots) sp.plt = plt + sp.attr[:subplot_index] = idx _update_subplot_args(plt, sp, copy(d), idx) end