# https://plot.ly/javascript/getting-started supported_args(::PlotlyBackend) = [ :annotations, :color_palette, :background_color, :background_color_legend, :background_color_inside, :background_color_outside, :foreground_color, :foreground_color_legend, :foreground_color_guide, # :foreground_color_grid, :foreground_color_axis, :foreground_color_text, :foreground_color_border, :group, :label, :seriestype, :seriescolor, :seriesalpha, :linecolor, :linestyle, :linewidth, :linealpha, :markershape, :markercolor, :markersize, :markeralpha, :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, :fillrange, :fillcolor, :fillalpha, :bins, :layout, :title, :title_location, :titlefont, :window_title, :show, :size, :x, :xguide, :xlims, :xticks, :xscale, :xflip, :xrotation, :y, :yguide, :ylims, :yticks, :yscale, :yflip, :yrotation, :z, :zguide, :zlims, :zticks, :zscale, :zflip, :zrotation, :z, :tickfont, :guidefont, :legendfont, :grid, :legend, :colorbar, :marker_z, :levels, :xerror, :yerror, :ribbon, :quiver, :orientation, # :overwrite_figure, :polar, :normalize, :weights, # :contours, :aspect_ratio, :hover, ] supported_types(::PlotlyBackend) = [ :path, :scatter, :bar, :pie, :heatmap, :contour, :surface, :path3d, :scatter3d ] supported_styles(::PlotlyBackend) = [:auto, :solid, :dash, :dot, :dashdot] supported_markers(::PlotlyBackend) = [ :none, :auto, :ellipse, :rect, :diamond, :utriangle, :dtriangle, :cross, :xcross, :pentagon, :hexagon, :octagon, :vline, :hline ] supported_scales(::PlotlyBackend) = [:identity, :log10] is_subplot_supported(::PlotlyBackend) = true is_string_supported(::PlotlyBackend) = true # -------------------------------------------------------------------------------------- function _initialize_backend(::PlotlyBackend; kw...) @eval begin import JSON JSON._print(io::IO, state::JSON.State, dt::Union{Date,DateTime}) = print(io, '"', dt, '"') _js_path = Pkg.dir("Plots", "deps", "plotly-latest.min.js") _js_code = open(@compat(readstring), _js_path, "r") # borrowed from https://github.com/plotly/plotly.py/blob/2594076e29584ede2d09f2aa40a8a195b3f3fc66/plotly/offline/offline.py#L64-L71 c/o @spencerlyon2 _js_script = """ """ # if we're in IJulia call setupnotebook to load js and css if isijulia() display("text/html", _js_script) end # if isatom() # import Atom # Atom.@msg evaljs(_js_code) # end end # TODO: other initialization end # ---------------------------------------------------------------- function plotly_font(font::Font, color = font.color) KW( :family => font.family, :size => round(Int, font.pointsize*1.4), :color => webcolor(color), ) end function plotly_annotation_dict(x, y, val; xref="paper", yref="paper") KW( :text => val, :xref => xref, :x => x, :yref => xref, :y => y, :showarrow => false, ) end function plotly_annotation_dict(x, y, ptxt::PlotText; xref="paper", yref="paper") merge(plotly_annotation_dict(x, y, ptxt.str; xref=xref, yref=yref), KW( :font => plotly_font(ptxt.font), :xanchor => ptxt.font.halign == :hcenter ? :center : ptxt.font.halign, :yanchor => ptxt.font.valign == :vcenter ? :middle : ptxt.font.valign, :rotation => ptxt.font.rotation, )) end # function get_annotation_dict_for_arrow(d::KW, xyprev::Tuple, xy::Tuple, a::Arrow) # xdiff = xyprev[1] - xy[1] # ydiff = xyprev[2] - xy[2] # dist = sqrt(xdiff^2 + ydiff^2) # KW( # :showarrow => true, # :x => xy[1], # :y => xy[2], # # :ax => xyprev[1] - xy[1], # # :ay => xy[2] - xyprev[2], # # :ax => 0, # # :ay => -40, # :ax => 10xdiff / dist, # :ay => -10ydiff / dist, # :arrowcolor => webcolor(d[:linecolor], d[:linealpha]), # :xref => "x", # :yref => "y", # :arrowsize => 10a.headwidth, # # :arrowwidth => a.headlength, # :arrowwidth => 0.1, # ) # end function plotly_scale(scale::Symbol) if scale == :log10 "log" else "-" end end # 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[:size] pcts = bbox_to_pcts(sp.plotarea, figw*px, figh*px) i1,i2 = (letter == :x ? (1,3) : (2,4)) [pcts[i1], pcts[i1]+pcts[i2]] end function plotly_axis(axis::Axis, sp::Subplot) letter = axis[:letter] ax = KW( :title => axis[:guide], :showgrid => sp[:grid], :zeroline => false, ) if letter in (:x,:y) ax[:domain] = plotly_domain(sp, letter) ax[:anchor] = "$(letter==:x ? :y : :x)$(plotly_subplot_index(sp))" end rot = axis[:rotation] if rot != 0 ax[:tickangle] = rot end if !(axis[:ticks] in (nothing, :none)) ax[:titlefont] = plotly_font(axis[:guidefont], axis[:foreground_color_guide]) ax[:type] = plotly_scale(axis[:scale]) ax[:tickfont] = plotly_font(axis[:tickfont], axis[:foreground_color_text]) ax[:tickcolor] = webcolor(axis[:foreground_color_border]) ax[:linecolor] = webcolor(axis[:foreground_color_border]) # lims lims = axis[:lims] if lims != :auto && limsType(lims) == :limits ax[:range] = lims end # flip if axis[:flip] ax[:autorange] = "reversed" end # ticks ticks = axis[:ticks] if ticks != :auto ttype = ticksType(ticks) if ttype == :ticks ax[:tickmode] = "array" ax[:tickvals] = ticks elseif ttype == :ticks_and_labels ax[:tickmode] = "array" ax[:tickvals], ax[:ticktext] = ticks end end else ax[:showticklabels] = false ax[:showgrid] = false end ax end # function plotly_layout_json(plt::Plot{PlotlyBackend}) # d = plt # function plotly_layout(d::KW, seriesargs::AVec{KW}) 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] w, h = plt[:size] d_out[:width], d_out[:height] = w, h d_out[:paper_bgcolor] = webcolor(plt[:background_color_outside]) d_out[:margin] = KW(:l=>0, :b=>0, :r=>0, :t=>20) d_out[:annotations] = KW[] for sp in plt.subplots spidx = plotly_subplot_index(sp) # TODO: add an annotation for the title if sp[:title] != "" bb = bbox(sp) tpos = sp[:title_location] xmm = if tpos == :left left(bb) elseif tpos == :right right(bb) else 0.5 * (left(bb) + right(bb)) end titlex, titley = xy_mm_to_pcts(xmm, top(bb), w*px, h*px) titlefont = font(sp[:titlefont], :top, sp[:foreground_color_title]) push!(d_out[:annotations], plotly_annotation_dict(titlex, titley, text(sp[:title], titlefont))) end d_out[:plot_bgcolor] = webcolor(sp[:background_color_inside]) # TODO: x/y axis tick values/labels # if any(is3d, seriesargs) if is3d(sp) d_out[:scene] = KW( Symbol("xaxis$spidx") => plotly_axis(sp[:xaxis], sp), Symbol("yaxis$spidx") => plotly_axis(sp[:yaxis], sp), Symbol("zaxis$spidx") => plotly_axis(sp[:zaxis], sp), ) else d_out[Symbol("xaxis$spidx")] = plotly_axis(sp[:xaxis], sp) d_out[Symbol("yaxis$spidx")] = plotly_axis(sp[:yaxis], sp) end # legend d_out[:showlegend] = sp[:legend] != :none if sp[:legend] != :none d_out[:legend] = KW( :bgcolor => webcolor(sp[:background_color_legend]), :bordercolor => webcolor(sp[:foreground_color_legend]), :font => plotly_font(sp[:legendfont], sp[:foreground_color_legend]), ) end # annotations append!(d_out[:annotations], KW[plotly_annotation_dict(ann...; xref = "x$spidx", yref = "y$spidx") for ann in sp[:annotations]]) # # 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 end function plotly_layout_json(plt::Plot) JSON.json(plotly_layout(plt)) end function plotly_colorscale(grad::ColorGradient, alpha = nothing) [[grad.values[i], webcolor(grad.colors[i], alpha)] for i in 1:length(grad.colors)] end plotly_colorscale(c, alpha = nothing) = plotly_colorscale(default_gradient(), alpha) const _plotly_markers = KW( :rect => "square", :xcross => "x", :utriangle => "triangle-up", :dtriangle => "triangle-down", :star5 => "star-triangle-up", :vline => "line-ns", :hline => "line-ew", ) function plotly_subplot_index(sp::Subplot) spidx = sp[: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" d_out[:showlegend] = should_add_to_legend(series) 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 hasline = !isscatter # set the "type" if st in (:path, :scatter) d_out[:type] = "scatter" d_out[:mode] = if hasmarker hasline ? "lines+markers" : "markers" else hasline ? "lines" : "none" end if d[:fillrange] == true || d[:fillrange] == 0 d_out[:fill] = "tozeroy" d_out[:fillcolor] = webcolor(d[:fillcolor], d[:fillalpha]) elseif !(d[:fillrange] in (false, nothing)) warn("fillrange ignored... plotly only supports filling to zero. fillrange: $(d[:fillrange])") end d_out[:x], d_out[:y] = x, y elseif st == :bar d_out[:type] = "bar" d_out[:x], d_out[:y] = x, y d_out[:orientation] = isvertical(d) ? "v" : "h" # elseif st == :histogram2d # d_out[:type] = "histogram2d" # d_out[:x], d_out[:y] = x, y # if isa(d[:bins], Tuple) # xbins, ybins = d[:bins] # else # xbins = ybins = d[:bins] # end # d_out[:nbinsx] = xbins # d_out[:nbinsy] = ybins # d_out[:colorscale] = plotly_colorscale(d[:fillcolor], d[:fillalpha]) # elseif st in (:histogram, :density) # d_out[:type] = "histogram" # isvert = isvertical(d) # d_out[isvert ? :x : :y] = y # d_out[isvert ? :nbinsx : :nbinsy] = d[:bins] # if st == :density # d_out[:histogramnorm] = "probability density" # end elseif st == :heatmap d_out[:type] = "heatmap" d_out[:x], d_out[:y], d_out[:z] = d[:x], d[:y], transpose_z(d, d[:z].surf, false) d_out[:colorscale] = plotly_colorscale(d[:fillcolor], d[:fillalpha]) elseif st == :contour d_out[:type] = "contour" d_out[:x], d_out[:y], d_out[:z] = d[:x], d[:y], transpose_z(d, d[:z].surf, false) # d_out[:showscale] = d[:colorbar] != :none d_out[:ncontours] = d[:levels] d_out[:contours] = KW(:coloring => d[:fillrange] != nothing ? "fill" : "lines") d_out[:colorscale] = plotly_colorscale(d[:linecolor], d[:linealpha]) elseif st in (:surface, :wireframe) d_out[:type] = "surface" d_out[:x], d_out[:y], d_out[:z] = d[:x], d[:y], transpose_z(d, d[:z].surf, false) d_out[:colorscale] = plotly_colorscale(d[:fillcolor], d[:fillalpha]) elseif st == :pie d_out[:type] = "pie" d_out[:labels] = pie_labels(sp, series) d_out[:values] = y d_out[:hoverinfo] = "label+percent+name" elseif st in (:path3d, :scatter3d) d_out[:type] = "scatter3d" d_out[:mode] = if hasmarker hasline ? "lines+markers" : "markers" else hasline ? "lines" : "none" end d_out[:x], d_out[:y] = x, y d_out[:z] = collect(d[:z]) else warn("Plotly: seriestype $st isn't supported.") return KW() end # add "marker" if hasmarker d_out[:marker] = KW( :symbol => get(_plotly_markers, d[:markershape], string(d[:markershape])), :opacity => d[:markeralpha], :size => 2 * d[:markersize], :color => webcolor(d[:markercolor], d[:markeralpha]), :line => KW( :color => webcolor(d[:markerstrokecolor], d[:markerstrokealpha]), :width => d[:markerstrokewidth], ), ) # gotta hack this (for now?) since plotly can't handle rgba values inside the gradient if d[:marker_z] != nothing # d_out[:marker][:color] = d[:marker_z] # d_out[:marker][:colorscale] = plotly_colorscale(d[:markercolor], d[:markeralpha]) # d_out[:showscale] = true grad = ColorGradient(d[:markercolor], alpha=d[:markeralpha]) zmin, zmax = extrema(d[:marker_z]) d_out[:marker][:color] = [webcolor(getColorZ(grad, (zi - zmin) / (zmax - zmin))) for zi in d[:marker_z]] end end # add "line" if hasline d_out[:line] = KW( :color => webcolor(d[:linecolor], d[:linealpha]), :width => d[:linewidth], :shape => if st == :steppre "vh" elseif st == :steppost "hv" else "linear" end, :dash => string(d[:linestyle]), # :dash => "solid", ) end # convert polar plots x/y to theta/radius if ispolar(d[:subplot]) d_out[:t] = rad2deg(pop!(d_out, :x)) d_out[:r] = pop!(d_out, :y) end # hover text if get(d, :hover, nothing) != nothing d_out[:hoverinfo] = "text" d_out[:text] = d[:hover] end d_out end # get a list of dictionaries, each representing the series params function plotly_series_json(plt::Plot) JSON.json(map(series -> plotly_series(plt, series), plt.series_list)) end # ---------------------------------------------------------------- function html_head(plt::Plot{PlotlyBackend}) "" end function html_body(plt::Plot{PlotlyBackend}, style = nothing) if style == nothing w, h = plt[:size] style = "width:$(w)px;height:$(h)px;" end uuid = Base.Random.uuid4() html = """
""" html end function js_body(plt::Plot{PlotlyBackend}, uuid) js = """ PLOT = document.getElementById('$(uuid)'); Plotly.plot(PLOT, $(plotly_series_json(plt)), $(plotly_layout_json(plt))); """ end # ---------------------------------------------------------------- function _writemime(io::IO, ::MIME"image/png", plt::Plot{PlotlyBackend}) writemime_png_from_html(io, plt) end function _writemime(io::IO, ::MIME"image/svg+xml", plt::Plot{PlotlyBackend}) write(io, html_head(plt) * html_body(plt)) end function _display(plt::Plot{PlotlyBackend}) standalone_html_window(plt) end