# https://plot.ly/javascript/getting-started supportedArgs(::PlotlyBackend) = [ :annotation, :background_color, :foreground_color, :color_palette, # :background_color_legend, :background_color_inside, :background_color_outside, # :foreground_color_legend, :foreground_color_grid, :foreground_color_axis, # :foreground_color_text, :foreground_color_border, :group, :label, :linetype, :seriescolor, :seriesalpha, :linecolor, :linestyle, :linewidth, :linealpha, :markershape, :markercolor, :markersize, :markeralpha, :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, :fillrange, :fillcolor, :fillalpha, :bins, :n, :nc, :nr, :layout, # :smooth, :title, :windowtitle, :show, :size, :x, :xlabel, :xlims, :xticks, :xscale, :xflip, :xrotation, :y, :ylabel, :ylims, :yticks, :yscale, :yflip, :yrotation, :z, :zlabel, :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 ] supportedAxes(::PlotlyBackend) = [:auto, :left] supportedTypes(::PlotlyBackend) = [:none, :line, :path, :scatter, :steppre, :steppost, :hist2d, :hist, :density, :bar, :contour, :surface, :path3d, :scatter3d, :pie, :heatmap] #,, :sticks, :hexbin, :hline, :vline] supportedStyles(::PlotlyBackend) = [:auto, :solid, :dash, :dot, :dashdot] supportedMarkers(::PlotlyBackend) = [:none, :auto, :ellipse, :rect, :diamond, :utriangle, :dtriangle, :cross, :xcross, :pentagon, :hexagon, :octagon, :vline, :hline] #vcat(_allMarkers, Shape) supportedScales(::PlotlyBackend) = [:identity, :log10] #, :ln, :log2, :log10, :asinh, :sqrt] subplotSupported(::PlotlyBackend) = true stringsSupported(::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(readall, _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 _create_plot(pkg::PlotlyBackend, d::KW) # TODO: create the window/canvas/context that is the plot within the backend (call it `o`) # TODO: initialize the plot... title, xlabel, bgcolor, etc Plot(nothing, pkg, 0, d, KW[]) end function _add_series(::PlotlyBackend, plt::Plot, d::KW) # TODO: add one series to the underlying package push!(plt.seriesargs, d) plt end function _add_annotations{X,Y,V}(plt::Plot{PlotlyBackend}, anns::AVec{@compat(Tuple{X,Y,V})}) # set or add to the annotation_list if haskey(plt.plotargs, :annotation_list) append!(plt.plotargs[:annotation_list], anns) else plt.plotargs[:annotation_list] = anns end end # ---------------------------------------------------------------- function _before_update_plot(plt::Plot{PlotlyBackend}) end # TODO: override this to update plot items (title, xlabel, etc) after creation function _update_plot(plt::Plot{PlotlyBackend}, d::KW) end function _update_plot_pos_size(plt::AbstractPlot{PlotlyBackend}, d::KW) end # ---------------------------------------------------------------- # accessors for x/y data # function getxy(plt::Plot{PlotlyBackend}, i::Int) # d = plt.seriesargs[i] # d[:x], d[:y] # end # # function setxy!{X,Y}(plt::Plot{PlotlyBackend}, xy::Tuple{X,Y}, i::Integer) # d = plt.seriesargs[i] # d[:x], d[:y] = xy # plt # end # ---------------------------------------------------------------- function _create_subplot(subplt::Subplot{PlotlyBackend}, isbefore::Bool) # TODO: build the underlying Subplot object. this is where you might layout the panes within a GUI window, for example true end function _expand_limits(lims, plt::Plot{PlotlyBackend}, isx::Bool) # TODO: call expand limits for each plot data end function _remove_axis(plt::Plot{PlotlyBackend}, isx::Bool) # TODO: if plot is inner subplot, might need to remove ticks or axis labels end # ---------------------------------------------------------------- # TODO: # _plotDefaults[:yrightlabel] = "" # _plotDefaults[:xlims] = :auto # _plotDefaults[:ylims] = :auto # _plotDefaults[:xticks] = :auto # _plotDefaults[:yticks] = :auto # _plotDefaults[:xscale] = :identity # _plotDefaults[:yscale] = :identity # _plotDefaults[:xflip] = false # _plotDefaults[:yflip] = false function plotlyfont(font::Font, color = font.color) KW( :family => font.family, :size => round(Int, font.pointsize*1.4), :color => webcolor(color), ) end function get_annotation_dict(x, y, val::Union{AbstractString,Symbol}) KW( :text => val, :xref => "x", :x => x, :yref => "y", :y => y, :showarrow => false, ) end function get_annotation_dict(x, y, ptxt::PlotText) merge(get_annotation_dict(x, y, ptxt.str), KW( :font => plotlyfont(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 plotlyscale(scale::Symbol) if scale == :log10 "log" else "-" end end use_axis_field(ticks) = !(ticks in (nothing, :none)) tickssym(letter) = symbol(letter * "ticks") limssym(letter) = symbol(letter * "lims") flipsym(letter) = symbol(letter * "flip") scalesym(letter) = symbol(letter * "scale") labelsym(letter) = symbol(letter * "label") rotationsym(letter) = symbol(letter * "rotation") function plotlyaxis(d::KW, letter) ax = KW( :title => d[labelsym(letter)], :showgrid => d[:grid], :zeroline => false, ) fgcolor = webcolor(d[:foreground_color]) tsym = tickssym(letter) rot = d[rotationsym(letter)] if rot != 0 ax[:tickangle] = rot end if use_axis_field(d[tsym]) ax[:titlefont] = plotlyfont(d[:guidefont], fgcolor) ax[:type] = plotlyscale(d[scalesym(letter)]) ax[:tickfont] = plotlyfont(d[:tickfont], fgcolor) ax[:tickcolor] = fgcolor ax[:linecolor] = fgcolor # xlims lims = d[limssym(letter)] if lims != :auto && limsType(lims) == :limits ax[:range] = lims end # xflip if d[flipsym(letter)] ax[:autorange] = "reversed" end # xticks ticks = d[tsym] 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 ax else ax[:showticklabels] = false ax[:showgrid] = false end ax end # function get_plot_json(plt::Plot{PlotlyBackend}) # d = plt.plotargs function plotly_layout(d::KW) d_out = KW() d_out[:width], d_out[:height] = d[:size] bgcolor = webcolor(d[:background_color]) fgcolor = webcolor(d[:foreground_color]) # set the fields for the plot d_out[:title] = d[:title] d_out[:titlefont] = plotlyfont(d[:guidefont], fgcolor) d_out[:margin] = KW(:l=>35, :b=>30, :r=>8, :t=>20) d_out[:plot_bgcolor] = bgcolor d_out[:paper_bgcolor] = bgcolor # TODO: x/y axis tick values/labels if is3d(d) d_out[:scene] = KW( :xaxis => plotlyaxis(d, "x"), :yaxis => plotlyaxis(d, "y"), :xzxis => plotlyaxis(d, "z"), ) else d_out[:xaxis] = plotlyaxis(d, "x") d_out[:yaxis] = plotlyaxis(d, "y") end # legend d_out[:showlegend] = d[:legend] != :none if d[:legend] != :none d_out[:legend] = KW( :bgcolor => bgcolor, :bordercolor => fgcolor, :font => plotlyfont(d[:legendfont]), ) end # annotations anns = get(d, :annotation_list, []) if !isempty(anns) d_out[:annotations] = [get_annotation_dict(ann...) for ann in anns] end if get(d, :polar, false) d_out[:direction] = "counterclockwise" end d_out end function get_plot_json(plt::Plot{PlotlyBackend}) JSON.json(plotly_layout(plt.plotargs)) 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", ) # get a dictionary representing the series params (d is the Plots-dict, d_out is the Plotly-dict) function plotly_series(d::KW, plotargs::KW; plot_index = nothing) # dumpdict(d,"series",true) d_out = KW() x, y = collect(d[:x]), collect(d[:y]) d_out[:name] = d[:label] lt = d[:linetype] isscatter = lt in (:scatter, :scatter3d) hasmarker = isscatter || d[:markershape] != :none hasline = !isscatter # set the "type" if lt in (:line, :path, :scatter, :steppre, :steppost) 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 lt == :bar d_out[:type] = "bar" d_out[:x], d_out[:y] = x, y elseif lt == :hist2d 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 elseif lt in (:hist, :density) d_out[:type] = "histogram" isvert = isvertical(d) d_out[isvert ? :x : :y] = y d_out[isvert ? :nbinsx : :nbinsy] = d[:bins] if lt == :density d_out[:histnorm] = "probability density" end elseif lt == :heatmap d_out[:type] = "heatmap" d_out[:x], d_out[:y] = x, y d_out[:z] = d[:z].surf d_out[:colorscale] = plotly_colorscale(d[:fillcolor], d[:fillalpha]) elseif lt == :contour d_out[:type] = "contour" d_out[:x], d_out[:y] = x, y d_out[:z] = d[:z].surf # 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 lt in (:surface, :wireframe) d_out[:type] = "surface" d_out[:x], d_out[:y] = x, y d_out[:z] = d[:z].surf d_out[:colorscale] = plotly_colorscale(d[:fillcolor], d[:fillalpha]) elseif lt == :pie d_out[:type] = "pie" d_out[:labels] = x d_out[:values] = y d_out[:hoverinfo] = "label+percent+name" elseif lt 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: linetype $lt 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 lt == :steppre "vh" elseif lt == :steppost "hv" else "linear" end, :dash => string(d[:linestyle]), # :dash => "solid", ) end # convert polar plots x/y to theta/radius if get(plotargs, :polar, false) d_out[:t] = rad2deg(pop!(d_out, :x)) d_out[:r] = pop!(d_out, :y) end # # for subplots, we need to add the xaxis/yaxis fields # if plot_index != nothing # d_out[:xaxis] = "x$(plot_index)" # d_out[:yaxis] = "y$(plot_index)" # end d_out end # get a list of dictionaries, each representing the series params function get_series_json(plt::Plot{PlotlyBackend}) JSON.json(map(d -> plotly_series(d, plt.plotargs), plt.seriesargs)) end function get_series_json(subplt::Subplot{PlotlyBackend}) ds = KW[] for (i,plt) in enumerate(subplt.plts) for d in plt.seriesargs push!(ds, plotly_series(d, plt.plotargs, plot_index = i)) end end JSON.json(ds) end # ---------------------------------------------------------------- function html_head(plt::AbstractPlot{PlotlyBackend}) "" end function html_body(plt::Plot{PlotlyBackend}, style = nothing) if style == nothing w, h = plt.plotargs[:size] style = "width:$(w)px;height:$(h)px;" end uuid = Base.Random.uuid4() html = """
""" # @show html html end function js_body(plt::Plot{PlotlyBackend}, uuid) js = """ PLOT = document.getElementById('$(uuid)'); Plotly.plot(PLOT, $(get_series_json(plt)), $(get_plot_json(plt))); """ end function html_body(subplt::Subplot{PlotlyBackend}) w, h = subplt.plts[1].plotargs[:size] html = ["
"] nr = nrows(subplt.layout) ph = h / nr for r in 1:nr push!(html, "
") nc = ncols(subplt.layout, r) pw = w / nc for c in 1:nc plt = subplt[r,c] push!(html, html_body(plt, "float:left; width:$(pw)px; height:$(ph)px;")) end push!(html, "
") end push!(html, "
") join(html) end # ---------------------------------------------------------------- function Base.writemime(io::IO, ::MIME"image/png", plt::AbstractPlot{PlotlyBackend}) writemime_png_from_html(io, plt) end function Base.writemime(io::IO, ::MIME"text/html", plt::AbstractPlot{PlotlyBackend}) write(io, html_head(plt) * html_body(plt)) # write(io, html_body(plt)) end function Base.display(::PlotsDisplay, plt::AbstractPlot{PlotlyBackend}) standalone_html_window(plt) end # function Base.display(::PlotsDisplay, plt::Subplot{PlotlyBackend}) # # TODO: display/show the subplot # end