diff --git a/examples/3d.jl b/examples/3d.jl index 180457fc..0187139b 100644 --- a/examples/3d.jl +++ b/examples/3d.jl @@ -1,6 +1,6 @@ using PlotlyJS -function random_line(showme=true) +function random_line() n = 400 rw() = cumsum(randn(n)) trace1 = scatter3d(;x=rw(),y=rw(), z=rw(), mode="lines", @@ -20,12 +20,10 @@ function random_line(showme=true) line_color="#bcbd22", line_width=1) layout = Layout(autosize=false, width=500, height=500, margin=Dict(:l => 0, :r => 0, :b => 0, :t => 65)) - p = Plot([trace1, trace2, trace3], layout) - showme && show(p) - p + plot([trace1, trace2, trace3], layout) end -function topo_surface(showme=true) +function topo_surface() z = Vector[[27.80985, 49.61936, 83.08067, 116.6632, 130.414, 150.7206, 220.1871, 156.1536, 148.6416, 203.7845, 206.0386, 107.1618, 68.36975, 45.3359, 49.96142, 21.89279, 17.02552, 11.74317, 14.75226, 13.6671, 5.677561, @@ -117,12 +115,10 @@ function topo_surface(showme=true) layout = Layout(title="Mt. Bruno Elevation", autosize=false, width=500, height=500, margin=Dict(:l => 65, :r => 50, :b => 65, :t => 90)) - p = Plot(trace, layout) - showme && show(p) - p + plot(trace, layout) end -function multiple_surface(showme=true) +function multiple_surface() z1 = Vector[[8.83, 8.89, 8.81, 8.87, 8.9, 8.87], [8.89, 8.94, 8.85, 8.94, 8.96, 8.92], [8.84, 8.9, 8.82, 8.92, 8.93, 8.91], @@ -143,12 +139,10 @@ function multiple_surface(showme=true) trace1 = surface(z=z1) trace2 = surface(z=z2, showscale=false, opacity=0.9) trace3 = surface(z=z3, showscale=false, opacity=0.9) - p = Plot([trace1, trace2, trace3]) - showme && show(p) - p + plot([trace1, trace2, trace3]) end -function clustering_alpha_shapes(showme=true) +function clustering_alpha_shapes() @eval using DataFrames, RDatasets, Colors # load data @@ -187,12 +181,10 @@ function clustering_alpha_shapes(showme=true) :backgroundcolor=>"rgb(230, 230,230)"), :aspectratio=>Dict( :x=>1, :y=>1, :z=>0.7 ), :aspectmode => "manual")) - p = Plot(data, layout) - showme && show(p) - p + plot(data, layout) end -function scatter_3d(showme=true) +function scatter_3d() @eval using Distributions Σ = fill(0.5, 3, 3) + Diagonal([0.5, 0.5, 0.5]) obs1 = rand(MvNormal(zeros(3), Σ), 200)' @@ -211,7 +203,5 @@ function scatter_3d(showme=true) layout = Layout(margin=Dict(:l=>0, :r=>0, :t=>0, :b=>0)) - p = Plot([trace1, trace2], layout) - showme && show(p) - p + plot([trace1, trace2], layout) end diff --git a/examples/area.jl b/examples/area.jl index 7c6bd9c0..aea0eebd 100644 --- a/examples/area.jl +++ b/examples/area.jl @@ -1,11 +1,9 @@ using PlotlyJS -function area1(showme=true) +function area1() trace1 = scatter(;x=1:4, y=[0, 2, 3, 5], fill="tozeroy") trace2 = scatter(;x=1:4, y=[3, 5, 1, 7], fill="tonexty") - p = Plot([trace1, trace2]) - showme && show(p) - p + plot([trace1, trace2]) end function stacked_area!(traces) @@ -17,23 +15,19 @@ function stacked_area!(traces) traces end -function area2(showme=true) +function area2() traces = [scatter(;x=1:3, y=[2, 1, 4], fill="tozeroy"), scatter(;x=1:3, y=[1, 1, 2], fill="tonexty"), scatter(;x=1:3, y=[3, 0, 2], fill="tonexty")] stacked_area!(traces) - p = Plot(traces, Layout(title="stacked and filled line chart")) - showme && show(p) - p + plot(traces, Layout(title="stacked and filled line chart")) end -function area3(showme=true) +function area3() trace1 = scatter(;x=1:4, y=[0, 2, 3, 5], fill="tozeroy", mode="none") trace2 = scatter(;x=1:4, y=[3, 5, 1, 7], fill="tonexty", mode="none") - p = Plot([trace1, trace2], - Layout(title="Overlaid Chart Without Boundary Lines")) - showme && show(p) - p + plot([trace1, trace2], + Layout(title="Overlaid Chart Without Boundary Lines")) end diff --git a/examples/contour.jl b/examples/contour.jl index a24410c8..68bb8827 100644 --- a/examples/contour.jl +++ b/examples/contour.jl @@ -1,6 +1,6 @@ using PlotlyJS -function contour1(showme=false) +function contour1() x = [-9, -6, -5 , -3, -1] y = [0, 1, 4, 5, 7] z = [10 10.625 12.5 15.625 20 @@ -11,7 +11,5 @@ function contour1(showme=false) trace = contour(x=x, y=y, z=z) layout = Layout(title="Setting the X and Y Coordinates in a Contour Plot") - p = Plot(trace, layout) - showme && show(p) - p + plot(trace, layout) end diff --git a/examples/histograms.jl b/examples/histograms.jl index 6fe98e79..b8766a05 100644 --- a/examples/histograms.jl +++ b/examples/histograms.jl @@ -1,6 +1,6 @@ -# module ContourExamples +using PlotlyJS -function example3() +function two_hists() x0 = randn(500) x1 = x0+1 @@ -8,7 +8,5 @@ function example3() trace2 = histogram(x=x1, opacity=0.75) data = [trace1, trace2] layout = Layout(barmode="overlay") - p = Plot(data, layout); show(p); p + plot(data, layout) end - -# end # module diff --git a/examples/line_scatter.jl b/examples/line_scatter.jl index f4fcbe90..b374b2cb 100644 --- a/examples/line_scatter.jl +++ b/examples/line_scatter.jl @@ -1,15 +1,13 @@ using PlotlyJS -function linescatter1(showme=true) +function linescatter1() trace1 = scatter(;x=1:4, y=[10, 15, 13, 17], mode="markers") trace2 = scatter(;x=2:5, y=[16, 5, 11, 9], mode="lines") trace3 = scatter(;x=1:4, y=[12, 9, 15, 12], mode="lines+markers") - p = Plot([trace1, trace2, trace3]) - showme && show(p) - p + plot([trace1, trace2, trace3]) end -function linescatter2(showme=true) +function linescatter2() trace1 = scatter(;x=1:5, y=[1, 6, 3, 6, 1], mode="markers", name="Team A", text=["A-1", "A-2", "A-3", "A-4", "A-5"], @@ -24,12 +22,10 @@ function linescatter2(showme=true) data = [trace1, trace2] layout = Layout(;title="Data Labels Hover", xaxis_range=[0.75, 5.25], yaxis_range=[0, 8]) - p = Plot(data, layout) - showme && show(p) - p + plot(data, layout) end -function linescatter3(showme=true) +function linescatter3() trace1 = scatter(;x=1:5, y=[1, 6, 3, 6, 1], mode="markers+text", name="Team A", textposition="top center", @@ -48,21 +44,17 @@ function linescatter3(showme=true) yaxis_range=[0, 8], legend_y=0.5, legend_yref="paper", legend_font=Dict(:family => "Arial, sans-serif", :size => 20, :color => "grey")) - p = Plot(data, layout) - showme && show(p) - p + plot(data, layout) end -function linescatter4(showme=true) +function linescatter4() trace1 = scatter(;y=fill(5, 40), mode="markers", marker_size=40, marker_color=0:39) layout = Layout(title="Scatter Plot with a Color Dimension") - p = Plot(trace1, layout) - showme && show(p) - p + plot(trace1, layout) end -function linescatter5(showme=true) +function linescatter5() country = ["Switzerland (2011)", "Chile (2013)", "Japan (2014)", "United States (2012)", "Slovenia (2014)", "Canada (2011)", @@ -107,12 +99,10 @@ function linescatter5(showme=true) layout["legend"] = Dict(:font => Dict(:size => 10), :yanchor => "middle", :xanchor => "right") - p = Plot(data, layout) - showme && show(p) - p + plot(data, layout) end -function linescatter6(showme=true) +function linescatter6() trace1 = scatter(;x=[52698, 43117], y=[53, 31], mode="markers", name="North America", @@ -149,7 +139,5 @@ function linescatter6(showme=true) layout["xaxis"] = Dict(:title => "GDP per Capita", :showgrid => false, :zeroline => false) layout["yaxis"] = Dict(:title => "Percent", :showline => false) - p = Plot(data, layout) - showme && show(p) - p + plot(data, layout) end diff --git a/examples/maps.jl b/examples/maps.jl index c04d291f..4fa8c0d9 100644 --- a/examples/maps.jl +++ b/examples/maps.jl @@ -1,6 +1,6 @@ using PlotlyJS -function maps1(showme=true) +function maps1() marker = Dict(:size=>[20, 30, 15, 10], :color=>[10, 20, 40, 50], :cmin=>0, @@ -14,12 +14,10 @@ function maps1(showme=true) marker=marker, name="Europe Data") layout = Layout(geo_scope="europe", geo_resolution=50, width=500, height=550, margin=Dict(:l=>0, :r=>0, :t=>10, :b=>0)) - p = Plot(trace, layout) - showme && show(p) - p + plot(trace, layout) end -function maps2(showme=true) +function maps2() @eval using DataFrames # read Data into dataframe @@ -46,7 +44,5 @@ function maps2(showme=true) :countrycolor => "rgb(255,255,255)") layout = Layout(;title="2014 US City Populations", showlegend=false, geo=geo) - p = Plot(trace, layout) - showme && show(p) - p + plot(trace, layout) end diff --git a/examples/mutation.ipynb b/examples/mutation.ipynb new file mode 100644 index 00000000..e70512c5 --- /dev/null +++ b/examples/mutation.ipynb @@ -0,0 +1,523 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "using PlotlyJS" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false, + "scrolled": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + " \n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "trace1 = scatter(;x=1:4, y=[0, 2, 3, 5], fill=\"tozeroy\")\n", + "trace2 = scatter(;x=1:4, y=[3, 5, 1, 7], fill=\"tonexty\")\n", + "p2 = plot([trace1, trace2], Layout(height=200, width=330))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### `restyle!`" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# second trace will turn pink\n", + "restyle!(p2, 2; marker_color=\"magenta\");" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# watch both markers change to squares\n", + "restyle!(p2, [1,2]; marker_symbol=\"square\");" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# simpler method without passing [1,2]\n", + "restyle!(p2; marker_symbol=\"star-triangle-up-open\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### `relayout!`" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "relayout!(p2; width=400, height=250)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "relayout!(p2; title=\"Interactively controlled!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### `addtraces!`" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "trace3 = scatter(;x=1:4, y=rand(1:10, 4), fill=\"tozeroy\")\n", + "addtraces!(p2, trace3)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Add one to the front of the stack. will appear on bottom of legend\n", + "# Notice it \n", + "trace4 = scatter(;x=1:4, y=rand(1:10, 4), fill=\"tozeroy\", marker_color=\"yellow\")\n", + "addtraces!(p2, 1, trace4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### `deletetraces!`" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "deletetraces!(p2, 4)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "deletetraces!(p2, 1) # back to where we started :)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### `movetraces!`" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# swap the order of the traces (move trace at index 1 to the back)\n", + "movetraces!(p2, 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# swap them back\n", + "movetraces!(p2, [2, 1], [1, 2])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice that the plot object also changed so everything was kept in sync:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{\n", + " \"width\": 400,\n", + " \"title\": \"Interactively controlled!\",\n", + " \"margin\": {\n", + " \"r\": 50,\n", + " \"l\": 50,\n", + " \"b\": 50,\n", + " \"t\": 60\n", + " },\n", + " \"height\": 250\n", + "}\n", + "\n" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p2.plot.layout" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{\n", + " \"type\": \"scatter\",\n", + " \"y\": [\n", + " 3,\n", + " 5,\n", + " 1,\n", + " 7\n", + " ],\n", + " \"x\": [\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 4\n", + " ],\n", + " \"fill\": \"tonexty\",\n", + " \"marker\": {\n", + " \"symbol\": \"star-triangle-up-open\",\n", + " \"color\": \"magenta\"\n", + " }\n", + "}\n", + "\n" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p2.plot.data[1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "## Non-mutating variants\n", + "\n", + "For each of the functions `restyle!`, `relayout!`, `addtraces!`, `deletetraces!`, and `movetraces!` there is also a non-mutating version with the same name, but without the `!`. These will create a copy of the underlying data, perform the given transformation, and return a new object. \n", + "\n", + "You can tell the object is new because a new plot appears:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p3 = relayout(p2; xaxis_title=\"new title\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 0.4.3-pre", + "language": "julia", + "name": "julia-0.4" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "0.4.3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/subplots.jl b/examples/subplots.jl index 6f4bde75..7b0c2dca 100644 --- a/examples/subplots.jl +++ b/examples/subplots.jl @@ -1,31 +1,28 @@ include("line_scatter.jl") -function subplots1(showme=true) - p1 = linescatter1(false) - p2 = linescatter2(false) +function subplots1() + p1 = linescatter1() + p2 = linescatter2() p = [p1 p2] - showme && show(p) p end -function subplots2(showme=true) - p1 = linescatter1(false) - p2 = linescatter2(false) +function subplots2() + p1 = linescatter1() + p2 = linescatter2() p = [p1, p2] - showme && show(p) p end -function subplots3(showme=true) - p1 = linescatter6(false) - p2 = linescatter2(false) - p3 = linescatter3(false) - p4 = linescatter4(false) +function subplots3() + p1 = linescatter6() + p2 = linescatter2() + p3 = linescatter3() + p4 = linescatter4() p = [p1 p2; p3 p4] - p.layout["showlegend"] = false - p.layout["width"] = 1000 - p.layout["height"] = 600 - showme && show(p) + p.plot.layout["showlegend"] = false + p.plot.layout["width"] = 1000 + p.plot.layout["height"] = 600 p end diff --git a/src/PlotlyJS.jl b/src/PlotlyJS.jl index 05c20492..98f868c7 100644 --- a/src/PlotlyJS.jl +++ b/src/PlotlyJS.jl @@ -13,37 +13,47 @@ const _js_cdn_path = "https://cdn.plot.ly/plotly-latest.min.js" include("traces_layouts.jl") abstract AbstractPlotlyDisplay -type Plot{TT<:AbstractTrace,TD<:AbstractPlotlyDisplay} +# core plot object +type Plot{TT<:AbstractTrace} data::Vector{TT} layout::AbstractLayout divid::Base.Random.UUID - _display::TD end # include the rest of the core parts of the package +include("json.jl") include("display.jl") -include("api.jl") include("subplots.jl") -include("json.jl") +include("api.jl") +include("savefig.jl") # Set some defaults for constructing `Plot`s -Plot() = Plot(GenericTrace[], Layout(), Base.Random.uuid4(), ElectronDisplay()) +Plot() = Plot(GenericTrace{Dict{Symbol,Any}}[], Layout(), Base.Random.uuid4()) Plot{T<:AbstractTrace}(data::Vector{T}, layout=Layout()) = - Plot(data, layout, Base.Random.uuid4(), ElectronDisplay()) + Plot(data, layout, Base.Random.uuid4()) Plot(data::AbstractTrace, layout=Layout()) = Plot([data], layout) +# NOTE: we export trace constructing types from inside api.jl export # core types - Plot, GenericTrace, Layout, + Plot, GenericTrace, Layout, ElectronDisplay, JupyterDisplay, + ElectronPlot, JupyterPlot, # other methods - savefig, svg_data, png_data, jpeg_data, webp_data + savefig, svg_data, png_data, jpeg_data, webp_data, # plotly.js api methods restyle!, relayout!, addtraces!, deletetraces!, movetraces!, redraw!, - extendtraces!, prependtraces! + extendtraces!, prependtraces!, + + # non-!-versions (forks, then applies, then returns fork) + restyle, relayout, addtraces, deletetraces, movetraces, redraw, + extendtraces, prependtraces, + + # helper methods + plot, fork end # module diff --git a/src/api.jl b/src/api.jl index 53302729..35dca71a 100644 --- a/src/api.jl +++ b/src/api.jl @@ -1,21 +1,13 @@ -# ----------------- # -# Basic API methods # -# ----------------- # +# -------------------------- # +# Standard Julia API methods # +# -------------------------- # -prep_kwarg(pair) = (symbol(replace(string(pair[1]), "_", ".")), pair[2]) -prep_kwargs(pairs) = Dict(map(prep_kwarg, pairs)) +prep_kwarg(pair::Tuple) = (symbol(replace(string(pair[1]), "_", ".")), pair[2]) +prep_kwargs(pairs::Vector) = Dict(map(prep_kwarg, pairs)) Base.size(p::Plot) = (get(p.layout.fields, :width, 800), get(p.layout.fields, :height, 450)) -Base.resize!(p::Plot, w::Int, h::Int) = size(get_window(p), w, h) -function Base.resize!(p::Plot) - sz = size(p) - # this padding was found by trial and error to not show vertical or - # horizontal scroll bars - resize!(p, sz[1]+10, sz[2]+25) -end - for t in [:histogram, :scatter3d, :surface, :mesh3d, :bar, :histogram2d, :histogram2dcontour, :scatter, :pie, :heatmap, :contour, :scattergl, :box, :area, :scattergeo, :choropleth] @@ -27,209 +19,78 @@ end Base.copy(gt::GenericTrace) = GenericTrace(gt.kind, deepcopy(gt.fields)) Base.copy(l::Layout) = Layout(deepcopy(l.fields)) Base.copy(p::Plot) = Plot([copy(t) for t in p.data], copy(p.layout)) +fork(p::Plot) = Plot(deepcopy(p.data), copy(p.layout), Base.Random.uuid4()) -# TODO: add width and height and figure out how to convert from measures to the -# pixels that will be expected in the SVG -function savefig2(p::Plot, fn::AbstractString; dpi::Real=96) - bas, ext = split(fn, ".") - if !(ext in ["pdf", "png", "ps"]) - error("Only `pdf`, `png` and `ps` output supported") - end - # make sure plot window is active - display(p) - - # write svg to tempfile - temp = tempname() - open(temp, "w") do f - write(f, svg_data(p, ext)) - end - - # hand off to cairosvg for conversion - run(`cairosvg $temp -d $dpi -o $fn`) - - # remove temp file - rm(temp) +# -------------- # +# Javascript API # +# -------------- # - # return plot - p +function _update_fields(hf::HasFields, update::Dict=Dict(); kwargs...) + map(x->setindex!(hf, x[2], x[1]), update) + map(x->setindex!(hf, x[2], x[1]), kwargs) end -# an alternative way to save plots -- no shelling out, but output less pretty -# js can be one of +"Update layout using update dict and/or kwargs" +relayout!(l::Layout, update::Associative=Dict(); kwargs...) = + _update_fields(l, update; kwargs...) -""" -`savefig(p::Plot, fn::AbstractString, js::Symbol)` +"Update layout using update dict and/or kwargs" +relayout!(p::Plot, update::Associative=Dict(); kwargs...) = + relayout!(p.layout, update; kwargs...) -Options -======= +"update a trace using update dict and/or kwargs" +restyle!(gt::GenericTrace, update::Associative=Dict(); kwargs...) = + _update_fields(gt, update; kwargs...) -- `p::Plot`: Plotly Plot -- `fn::AbstractString`: Filename with extension (html, pdf, png) -- `js::Symbol`: +"Update a single trace using update dict and/or kwargs" +restyle!(p::Plot, ind::Int=1, update::Associative=Dict(); kwargs...) = + restyle!(p.data[ind], update; kwargs...) -**Options for `js`** +"Update specific traces using update dict and/or kwargs" +restyle!(p::Plot, inds::AbstractVector{Int}, update::Associative=Dict(); kwargs...) = + map(ind -> restyle!(p.data[ind], update; kwargs...), inds) -- `:local` - reference the javascript from PlotlyJS installation -- `:remote` - reference the javascript from plotly CDN -- `:embed` - embed the javascript in output (add's 1.7MB to size) -""" -function savefig(p::Plot, fn::AbstractString; js::Symbol=:local - # sz::Tuple{Int,Int}=(8,6), - # dpi::Int=300 - ) - - # Extract file type - suf = split(fn, ".")[end] - - # if html we don't need a plot window - if suf == "html" - open(fn, "w") do f - writemime(f, MIME"text/html"(), p, js) - end - return p - end +"Update all traces using update dict and/or kwargs" +restyle!(p::Plot, update::Associative=Dict(); kwargs...) = + restyle!(p, 1:length(p.data), update; kwargs...) - # for all the rest we need an active plot window - show(p) +"Add trace(s) to the end of the Plot's array of data" +addtraces!(p::Plot, traces::AbstractTrace...) = push!(p.data, traces...) - # we can export svg directly - if suf == "svg" - open(fn, "w") do f - write(f, svg_data(p)) - end - return p - end +""" +Add trace(s) at a specified location in the Plot's array of data. - # now for the rest we need ImageMagick - @eval import ImageMagick - - # construct a magic wand and read the image data from png - wand = ImageMagick.MagickWand() - # readimage(wand, _img_data(p, "svg")) - ImageMagick.readimage(wand, base64decode(png_data(p))) - ImageMagick.resetiterator(wand) - - # # set units to inches - # status = ccall((:MagickSetImageUnits, ImageMagick.libwand), Cint, - # (Ptr{Void}, Cint), wand.ptr, 1) - # status == 0 && error(wand) - # - # # calculate number of rows/cols - # width, height = sz[1]*dpi, sz[2]*dpi - # - # # set resolution - # status = ccall((:MagickSetImageResolution, ImageMagick.libwand), Cint, - # (Ptr{Void}, Cdouble, Cdouble), wand.ptr, Cdouble(dpi), Cdouble(dpi)) - # status == 0 && error(wand) - # - # # set number of columns and rows - # status = ccall((:MagickAdaptiveResizeImage, ImageMagick.libwand), Cint, - # (Ptr{Void}, Csize_t, Csize_t), wand.ptr, Csize_t(width), Csize_t(height)) - # status == 0 && error(wand) - - # finally write the image out - ImageMagick.writeimage(wand, fn) - - p +The new traces will start at index `p.data[where]` +""" +function addtraces!(p::Plot, where::Int, traces::AbstractTrace...) + new_data = vcat(p.data[1:where-1], traces..., p.data[where:end]) + p.data = new_data end -function png_data(p::Plot) - raw = _img_data(p, "png") - raw[length("data:image/png;base64,")+1:end] -end +"Remove the traces at the specified indices" +deletetraces!(p::Plot, inds::Int...) = + (p.data = p.data[setdiff(1:length(p.data), inds)]) -function jpeg_data(p::Plot) - raw = _img_data(p, "jpeg") - raw[length("data:image/jpeg;base64,")+1:end] -end +"Move one or more traces to the end of the data array" +movetraces!(p::Plot, to_end::Int...) = + (p.data = p.data[vcat(setdiff(1:length(p.data), to_end), to_end...)]) -function webp_data(p::Plot) - raw = _img_data(p, "webp") - raw[length("data:image/webp;base64,")+1:end] +function _move_one!(x::AbstractArray, from::Int, to::Int) + el = splice!(x, from) # extract the element + splice!(x, to:to-1, (el,)) # put it back in the new position + x end -# TODO: somehow `length(svg_data(p))` is not idempotent -svg_data(p::Plot, format="pdf") = @js p Plotly.Snapshot.toSVG(this, $format) - -function _img_data(p::Plot, format::ASCIIString) - _formats = ["png", "jpeg", "webp", "svg"] - if !(format in _formats) - error("Unsupported format $format, must be one of $_formats") - end - - display(p) - - @js p begin - ev = Plotly.Snapshot.toImage(this, d("format"=>$format)) - @new Promise(resolve -> ev.once("success", resolve)) - end -end - -const _mimeformats = Dict("application/eps" => "eps", - "image/eps" => "eps", - "application/pdf" => "pdf", - "image/png" => "png", - "image/jpeg" => "jpeg", - "application/postscript" => "ps", - # "image/svg+xml" => "svg" -) - -for (mime, fmt) in _mimeformats - @eval function Base.writemime(io::IO, ::MIME{symbol($mime)}, p::Plot) - @eval import ImageMagick - - # construct a magic wand and read the image data from png - wand = ImageMagick.MagickWand() - ImageMagick.readimage(wand, base64decode(png_data(p))) - ImageMagick.setimageformat(wand, $fmt) - ImageMagick.writeimage(wand, io) - - end -end - - -# -------------- # -# Javascript API # -# -------------- # -# TODO: update the fields on the Plot object also for functions that mutate the -# plot - -getdiv(p) = :(document.getElementById($(string(p.divid)))) - -Blink.js(p::Plot, code::JSString; callback = true) = - Blink.js(get_window(p), :(Blink.evalwith($(getdiv(p)), $(Blink.jsstring(code)))), callback = callback) - -restyle!(p::Plot, update = Dict(); kwargs...) = - @js_ p Plotly.restyle(this, $(merge(update, prep_kwargs(kwargs)))) - -restyle!(p::Plot, traces::Integer...; kwargs...) = - @js_ p Plotly.restyle(this, $(prep_kwargs(kwargs)), $(collect(traces))) - -relayout!(p::Plot, update = Dict(); kwargs...) = - @js_ p Plotly.relayout(this, $(merge(update, prep_kwargs(kwargs)))) - -addtraces!(p::Plot, traces::AbstractTrace...) = - @js_ p Plotly.addTraces(this, $traces) - -addtraces!(p::Plot, where::Union{Int,Vector{Int}}, traces::AbstractTrace...) = - @js_ p Plotly.addTraces(this, $traces, $where) - -deletetraces!(p::Plot, traces::Int...) = - @js_ p Plotly.deleteTraces(this, $(collect(traces))) - -movetraces!(p::Plot, to_end) = - @js_ p Plotly.moveTraces(this, $to_end) - -movetraces!(p::Plot, to_end...) = movetraces!(p, collect(to_end)) - -movetraces!(p::Plot, src::Union{Int,Vector{Int}}, dest::Union{Int,Vector{Int}}) = - @js_ p Plotly.moveTraces(this, $src, $dest) +""" +Move traces from indices `src` to indices `dest`. -redraw!(p::Plot) = - @js_ p Plotly.redraw(this) +Both `src` and `dest` must be `Vector{Int}` +""" +movetraces!(p::Plot, src::AbstractVector{Int}, dest::AbstractVector{Int}) = + (map((i,j) -> _move_one!(p.data, i, j), src, dest); p) -redraw!(p::Plot) = - @js_ p Plotly.redraw(this) +# no-op here +redraw!(p::Plot) = nothing # --------------------------------- # # unexported methods in plot_api.js # @@ -256,7 +117,7 @@ These concepts are best understood by example: ```julia # adds the values [1, 3] to the end of the first trace's y attribute and doesn't # remove any points -extendtraces!(p, Dict(:y=>Vector[[1, 3]]), [0], -1) +extendtraces!(p, Dict(:y=>Vector[[1, 3]]), [1], -1) extendtraces!(p, Dict(:y=>Vector[[1, 3]])) # equivalent to above ``` @@ -264,24 +125,19 @@ extendtraces!(p, Dict(:y=>Vector[[1, 3]])) # equivalent to above # adds the values [1, 3] to the end of the third trace's marker.size attribute # and [5,5,6] to the end of the 5th traces marker.size -- leaving at most 10 # points per marker.size attribute -extendtraces!(p, Dict("marker.size"=>Vector[[1, 3], [5, 5, 6]]), [2, 4], 10) +extendtraces!(p, Dict("marker.size"=>Vector[[1, 3], [5, 5, 6]]), [3, 5], 10) ``` """ -function extendtraces!(p::Plot, update::Dict, indices::Vector{Int}=[0], maxpoints=-1; - update_jl::Bool=false) - # update data in Julia object - if update_jl - for ix in indices - tr = p.data[ix+1] - for k in keys(update) - v = update[k][ix+1] - tr[k] = push!(tr[k], v...) - end +function extendtraces!(p::Plot, update::Dict, indices::Vector{Int}=[1], maxpoints=-1) + # TODO: maxpoints not handled here + for ix in indices + tr = p.data[ix] + for k in keys(update) + v = update[k][ix] + tr[k] = push!(tr[k], v...) end end - - @js_ p Plotly.extendTraces(this, $update, $indices, $maxpoints) end """ @@ -289,28 +145,25 @@ The API for `prependtraces` is equivalent to that for `extendtraces` except that the data is added to the front of the traces attributes instead of the end. See Those docstrings for more information """ -function prependtraces!(p::Plot, update::Dict, indices::Vector{Int}=[0], maxpoints=-1) - # update data in Julia object - if update_jl - for ix in indices - tr = p.data[ix+1] - for k in keys(update) - v = update[k][ix+1] - tr[k] = vcat(v, tr[k]) - end +function prependtraces!(p::Plot, update::Dict, indices::Vector{Int}=[1], maxpoints=-1) + # TODO: maxpoints not handled here + for ix in indices + tr = p.data[ix] + for k in keys(update) + v = update[k][ix] + tr[k] = vcat(v, tr[k]) end end - @js_ p Plotly.prependTraces(this, $update, $indices, $maxpoints) end for f in (:extendtraces!, :prependtraces!) - @eval $(f)(p::Plot, inds::Vector{Int}=[0], maxpoints=-1; update_jl=false, update...) = - ($f)(p, Dict(map(x->(x[1], _tovec(x[2])), update)), inds, maxpoints; update_jl=update_jl) + @eval $(f)(p::Plot, inds::Vector{Int}=[0], maxpoints=-1; update...) = + ($f)(p, Dict(map(x->(x[1], _tovec(x[2])), update)), inds, maxpoints) - @eval $(f)(p::Plot, inds::Int, maxpoints=-1; update_jl=false, update...) = - ($f)(p, [inds], maxpoints; update_jl=update_jl, update...) + @eval $(f)(p::Plot, inds::Int, maxpoints=-1, update...) = + ($f)(p, [inds], maxpoints, update...) - @eval $(f)(p::Plot, update::Dict, inds::Int, maxpoints=-1; update_jl=false) = - ($f)(p, update, [inds], maxpoints; update_jl=update_jl) + @eval $(f)(p::Plot, update::Dict, inds::Int, maxpoints=-1) = + ($f)(p, update, [inds], maxpoints) end diff --git a/src/display.jl b/src/display.jl index c4fee75f..c30c9d0d 100644 --- a/src/display.jl +++ b/src/display.jl @@ -2,7 +2,6 @@ # Display-esque functions # # ----------------------- # - function html_body(p::Plot) """ @@ -63,8 +62,61 @@ end Base.show(io::IO, p::Plot) = writemime(io, MIME("text/plain"), p) -# -------------- # -# Other displays # -# -------------- # +# ----------------------------------------- # +# SyncPlot -- sync Plot object with display # +# ----------------------------------------- # +immutable SyncPlot{TD<:AbstractPlotlyDisplay} + plot::Plot + view::TD +end + +plot(args...; kwargs...) = SyncPlot(Plot(args...; kwargs...)) + +## API methods for SyncPlot +for f in [:restyle!, :relayout!, :addtraces!, :deletetraces!, :movetraces!, + :redraw!, :extendtraces!, :prependtraces!] + @eval function $(f)(sp::SyncPlot, args...; kwargs...) + $(f)(sp.plot, args...; kwargs...) + $(f)(sp.view, args...; kwargs...) + end + + no_!_method = symbol(string(f)[1:end-1]) + @eval function $(no_!_method)(sp::SyncPlot, args...; kwargs...) + sp2 = fork(sp) + $f(sp2.plot, args...; kwargs...) # only need to update the julia side + sp2 # return so we display fresh + end +end + +# Add some basic Julia API methods on SyncPlot that just forward onto the Plot +Base.size(sp::SyncPlot) = size(sp.plot) +Base.copy(sp::SyncPlot) = fork(sp) # defined by each SyncPlot{TD} + +# ----------------- # +# Display frontends # +# ----------------- # + include("displays/electron.jl") include("displays/ijulia.jl") + +# methods to convert from one frontend to another +let + all_frontends = [:ElectronPlot, :JupyterPlot] + for fe_to in all_frontends + for fe_from in all_frontends + @eval $(fe_to)(sp::$(fe_from)) = $(fe_to)(sp.plot) + end + end +end + +# -------- # +# Defaults # +# -------- # + +if isdefined(Main, :IJulia) && Main.IJulia.inited + # default to JupyterDisplay + SyncPlot(p::Plot) = SyncPlot(p, JupyterDisplay(p)) +else + # default to ElectronDisplay + SyncPlot(p::Plot) = SyncPlot(p, ElectronDisplay()) +end diff --git a/src/displays/electron.jl b/src/displays/electron.jl index 1a50f321..5adfa4a1 100644 --- a/src/displays/electron.jl +++ b/src/displays/electron.jl @@ -7,15 +7,18 @@ type ElectronDisplay <: AbstractPlotlyDisplay js_loaded::Bool end +typealias ElectronPlot SyncPlot{ElectronDisplay} + ElectronDisplay() = ElectronDisplay(Nullable{Window}(), false) +ElectronPlot(p::Plot) = ElectronPlot(p, ElectronDisplay()) -isactive(ed::ElectronDisplay) = isnull(ed.w) ? false : Blink.active(get(ed.w)) +fork(jp::ElectronPlot) = ElectronPlot(fork(jp.plot), ElectronDisplay()) -typealias ElectronPlot{TT<:AbstractTrace} Plot{TT,ElectronDisplay} +isactive(ed::ElectronDisplay) = isnull(ed.w) ? false : Blink.active(get(ed.w)) function get_window(p::ElectronPlot, kwargs...) - w, h = size(p) - get_window(p._display; width=w, height=h, kwargs...) + w, h = size(p.plot) + get_window(p.view; width=w, height=h, kwargs...) end function get_window(ed::ElectronDisplay; kwargs...) @@ -30,6 +33,7 @@ function get_window(ed::ElectronDisplay; kwargs...) end js_loaded(ed::ElectronDisplay) = ed.js_loaded + function loadjs(ed::ElectronDisplay) if !ed.js_loaded Blink.load!(get_window(ed), _js_path) @@ -37,20 +41,87 @@ function loadjs(ed::ElectronDisplay) end end -function Base.display(p::Plot) +function Base.display(p::ElectronPlot) w = get_window(p) - loadjs(p._display) + loadjs(p.view) @js w begin - trydiv = document.getElementById($(string(p.divid))) + trydiv = document.getElementById($(string(p.plot.divid))) if trydiv == nothing thediv = document.createElement("div") - thediv.id = $(string(p.divid)) + thediv.id = $(string(p.plot.divid)) document.body.appendChild(thediv) else thediv = trydiv end - @var _ = Plotly.newPlot(thediv, $(p.data), $(p.layout), d("showLink"=> false)) + @var _ = Plotly.newPlot(thediv, $(p.plot.data), + $(p.plot.layout), + d("showLink"=> false)) _.then(()->Promise.resolve()) end - show(p) + p.plot end + +## API Methods for ElectronDisplay +function _img_data(p::ElectronPlot, format::ASCIIString) + _formats = ["png", "jpeg", "webp", "svg"] + if !(format in _formats) + error("Unsupported format $format, must be one of $_formats") + end + + display(p) + + @js p.view begin + ev = Plotly.Snapshot.toImage(this, d("format"=>$format)) + @new Promise(resolve -> ev.once("success", resolve)) + end +end + +svg_data(p::ElectronPlot, format="png") = + @js p.view Plotly.Snapshot.toSVG(this, $format) + + +Blink.js(p::ElectronDisplay, code::JSString; callback=true) = + Blink.js(get_window(p), :(Blink.evalwith(thediv, $(Blink.jsstring(code)))), callback=callback) + +Blink.js(p::ElectronPlot, code::JSString; callback=true) = + Blink.js(p.view, code; callback=callback) + +# Methods from javascript API (docstrings found in api.jl) +relayout!(p::ElectronDisplay, update::Associative=Dict(); kwargs...) = + @js_ p Plotly.relayout(this, $(merge(update, prep_kwargs(kwargs)))) + +restyle!(p::ElectronDisplay, ind::Int, update::Associative=Dict(); kwargs...) = + @js_ p Plotly.restyle(this, $(prep_kwargs(kwargs)), $(ind-1)) + +restyle!(p::ElectronDisplay, inds::AbstractVector{Int}, update::Associative=Dict(); kwargs...) = + @js_ p Plotly.restyle(this, $(prep_kwargs(kwargs)), $(inds-1)) + +restyle!(p::ElectronDisplay, update=Dict(); kwargs...) = + @js_ p Plotly.restyle(this, $(merge(update, prep_kwargs(kwargs)))) + +addtraces!(p::ElectronDisplay, traces::AbstractTrace...) = + @js_ p Plotly.addTraces(this, $traces) + +addtraces!(p::ElectronDisplay, where::Int, traces::AbstractTrace...) = + @js_ p Plotly.addTraces(this, $traces, $(where-1)) + +deletetraces!(p::ElectronDisplay, traces::Int...) = + @js_ p Plotly.deleteTraces(this, $(collect(traces)-1)) + +movetraces!(p::ElectronDisplay, to_end::Int...) = + @js_ p Plotly.moveTraces(this, $(collect(to_end)-1)) + +movetraces!(p::ElectronDisplay, src::AbstractVector{Int}, dest::AbstractVector{Int}) = + @js_ p Plotly.moveTraces(this, $(src-1), $(dest-1)) + +redraw!(p::ElectronDisplay) = + @js_ p Plotly.redraw(this) + +# unexported (by plotly.js) api methods +extendtraces!(ed::ElectronDisplay, update::Associative=Dict(), + indices::Vector{Int}=[1], maxpoints=-1;) = + @js_ p Plotly.extendTraces(this, $update, $(indices-1), $maxpoints) + +prependtraces!(ed::ElectronDisplay, update::Associative=Dict(), + indices::Vector{Int}=[1], maxpoints=-1;) = + @js_ p Plotly.prependTraces(this, $update, $(indices-1), $maxpoints) diff --git a/src/displays/ijulia.jl b/src/displays/ijulia.jl index 3d7f18f0..bccdda4f 100644 --- a/src/displays/ijulia.jl +++ b/src/displays/ijulia.jl @@ -2,6 +2,18 @@ # Jupyter notebook setup # # ---------------------- # +type JupyterDisplay <: AbstractPlotlyDisplay + divid::Base.Random.UUID + displayed::Bool +end + +typealias JupyterPlot SyncPlot{JupyterDisplay} + +JupyterDisplay(p::Plot) = JupyterDisplay(p.divid, false) +JupyterPlot(p::Plot) = JupyterPlot(p, JupyterDisplay(p)) + +fork(jp::JupyterPlot) = JupyterPlot(fork(jp.plot)) + # if we're in IJulia call setupnotebook to load js and css if isdefined(Main, :IJulia) && Main.IJulia.inited # the first script is some hack I needed to do in order for the notebook @@ -15,13 +27,101 @@ if isdefined(Main, :IJulia) && Main.IJulia.inited """) display("text/html", "Plotly javascript loaded.
") - + js_loaded(::JupyterDisplay) = true @eval import IJulia IJulia.display_dict(p::Plot) = - Dict{ASCIIString,ByteString}("text/html" => sprint(writemime, "text/html", p)) + Dict("text/plain" => sprint(writemime, "text/plain", p)) + + function IJulia.display_dict(p::JupyterPlot) + if p.view.displayed + nothing + else + p.view.displayed = true + Dict("text/html" => sprint(writemime, "text/html", p.plot)) + end + end # TODO: maybe add Blink.js to this page and we can reuse all the same api # methods? +else + js_loaded(::JupyterDisplay) = false +end + +## API Methods for JupyterDisplay +function _img_data(p::JupyterPlot, format::ASCIIString) + # _formats = ["png", "jpeg", "webp", "svg"] + # if !(format in _formats) + # error("Unsupported format $format, must be one of $_formats") + # end + # + # # make sure plot has been displayed + # display(p) + # + # # TODO: figure out how to resolve the promise + # display("text/html", """""") + error("Not implemented (yet). Use Electron frontend to save figures") +end + +function svg_data(jp::JupyterPlot, format="png") + # display("text/html", """""") + error("Not implemented (yet). Use Electron frontend to save figures") +end + + +function call_plotlyjs(jd::JupyterDisplay, func::AbstractString, args...) + arg_str = length(args) > 0 ? string(",", join(map(json, args), ", ")) : + "" + display("text/html", """""") end + +# Methods from javascript API +relayout!(jd::JupyterDisplay, update::Associative=Dict(); kwargs...) = + call_plotlyjs(jd, "relayout", merge(update, prep_kwargs(kwargs))) + +restyle!(jd::JupyterDisplay, ind::Int, update::Associative=Dict(); kwargs...) = + call_plotlyjs(jd, "restyle", merge(update, prep_kwargs(kwargs)), ind-1) + +function restyle!(jd::JupyterDisplay, inds::AbstractVector{Int}, + update::Associative=Dict(); kwargs...) + call_plotlyjs(jd, "restyle", merge(update, prep_kwargs(kwargs)), inds-1) +end + +restyle!(jd::JupyterDisplay, update::Associative=Dict(); kwargs...) = + call_plotlyjs(jd, "restyle", merge(update, prep_kwargs(kwargs))) + +addtraces!(jd::JupyterDisplay, traces::AbstractTrace...) = + call_plotlyjs(jd, "addTraces", traces) + +addtraces!(jd::JupyterDisplay, where::Int, traces::AbstractTrace...) = + call_plotlyjs(jd, "addTraces", traces, where-1) + +deletetraces!(jd::JupyterDisplay, traces::Int...) = + call_plotlyjs(jd, "deleteTraces", collect(traces)-1) + +movetraces!(jd::JupyterDisplay, to_end::Int...) = + call_plotlyjs(jd, "moveTraces", collect(to_end)-1) + +movetraces!(jd::JupyterDisplay, src::AbstractVector{Int}, dest::AbstractVector{Int}) = + call_plotlyjs(jd, "moveTraces", src-1, dest-1) + +redraw!(jd::JupyterDisplay) = call_plotlyjs(jd, "redraw") + +# unexported (by plotly.js) api methods +extendtraces!(jd::JupyterDisplay, update::Associative=Dict(), + indices::Vector{Int}=[1], maxpoints=-1;) = + call_plotlyjs(jd, "extendTraces", update, indices-1, maxpoints) + +prependtraces!(jd::JupyterDisplay, update::Associative=Dict(), + indices::Vector{Int}=[1], maxpoints=-1;) = + call_plotlyjs(jd, "prependTraces", update, indices-1, maxpoints) diff --git a/src/json.jl b/src/json.jl index 2126b30f..24c03082 100644 --- a/src/json.jl +++ b/src/json.jl @@ -56,4 +56,5 @@ function JSON.parse(::Type{Plot}, str::AbstractString) Plot(data, layout) end -JSON.parsefile(::Type{Plot}, fn) = open(fn, "r") do f; JSON.parse(Plot, readall(f)) end +JSON.parsefile(::Type{Plot}, fn) = + open(fn, "r") do f; JSON.parse(Plot, readall(f)) end diff --git a/src/savefig.jl b/src/savefig.jl new file mode 100644 index 00000000..456989c0 --- /dev/null +++ b/src/savefig.jl @@ -0,0 +1,196 @@ +# ----------------------------------- # +# Methods for saving figures to files # +# ----------------------------------- # + +# TODO: add width and height and figure out how to convert from measures to the +# pixels that will be expected in the SVG +function savefig2(p::SyncPlot, fn::AbstractString; dpi::Real=96) + bas, ext = split(fn, ".") + if !(ext in ["pdf", "png", "ps"]) + error("Only `pdf`, `png` and `ps` output supported") + end + # make sure plot window is active + display(p) + + # write svg to tempfile + temp = tempname() + open(temp, "w") do f + write(f, svg_data(p, ext)) + end + + # hand off to cairosvg for conversion + run(`cairosvg $temp -d $dpi -o $fn`) + + # remove temp file + rm(temp) + + # return plot + p +end + +# an alternative way to save plots -- no shelling out, but output less pretty + +""" +`savefig(p::Plot, fn::AbstractString, js::Symbol)` + +## Arguments + +- `p::Plot`: Plotly Plot +- `fn::AbstractString`: Filename with extension (html, pdf, png) +- `js::Symbol`: One of the following: + - `:local` - reference the javascript from PlotlyJS installation + - `:remote` - reference the javascript from plotly CDN + - `:embed` - embed the javascript in output (add's 1.7MB to size) +""" +function savefig(p::SyncPlot, fn::AbstractString; js::Symbol=:local + # sz::Tuple{Int,Int}=(8,6), + # dpi::Int=300 + ) + + # Extract file type + suf = split(fn, ".")[end] + + # if html we don't need a plot window + if suf == "html" + open(fn, "w") do f + writemime(f, MIME"text/html"(), p, js) + end + return p + end + + # for all the rest we need an active plot window + show(p) + + # we can export svg directly + if suf == "svg" + open(fn, "w") do f + write(f, svg_data(p)) + end + return p + end + + # now for the rest we need ImageMagick + @eval import ImageMagick + + # construct a magic wand and read the image data from png + wand = ImageMagick.MagickWand() + # readimage(wand, _img_data(p, "svg")) + ImageMagick.readimage(wand, base64decode(png_data(p))) + ImageMagick.resetiterator(wand) + + # # set units to inches + # status = ccall((:MagickSetImageUnits, ImageMagick.libwand), Cint, + # (Ptr{Void}, Cint), wand.ptr, 1) + # status == 0 && error(wand) + # + # # calculate number of rows/cols + # width, height = sz[1]*dpi, sz[2]*dpi + # + # # set resolution + # status = ccall((:MagickSetImageResolution, ImageMagick.libwand), Cint, + # (Ptr{Void}, Cdouble, Cdouble), wand.ptr, Cdouble(dpi), Cdouble(dpi)) + # status == 0 && error(wand) + # + # # set number of columns and rows + # status = ccall((:MagickAdaptiveResizeImage, ImageMagick.libwand), Cint, + # (Ptr{Void}, Csize_t, Csize_t), wand.ptr, Csize_t(width), Csize_t(height)) + # status == 0 && error(wand) + + # finally write the image out + ImageMagick.writeimage(wand, fn) + + p +end + +function savefig3(p::SyncPlot, fn::AbstractString; js::Symbol=:local) + suf = split(fn, ".")[end] + + # if html we don't need a plot window + if suf == "html" + open(fn, "w") do f + writemime(f, MIME"text/html"(), p, js) + end + return p + end + + # for all the rest we need raw svg data + raw_svg = svg_data(p) + + # we can export svg directly + if suf == "svg" + open(fn, "w") do f + write(f, raw_svg) + end + return p + end + + # now we need to use librsvg/Cairo to finish + @eval import Rsvg + @eval import Cairo + + if suf == "pdf" + r = Rsvg.handle_new_from_data(raw_svg) + cs = Cairo.CairoPDFSurface(fn, size(p.plot)...) + ctx = Cairo.CairoContext(cs) + Rsvg.handle_render_cairo(ctx, r) + Cairo.show_page(ctx) + Cairo.finish(cs) + elseif suf == "png" + r = Rsvg.handle_new_from_data(raw_svg) + cs = Cairo.CairoImageSurface(size(p.plot)...,Cairo.FORMAT_ARGB32) + ctx = Cairo.CairoContext(cs) + Rsvg.handle_render_cairo(ctx, r) + Cairo.write_to_png(cs, fn) + else + error("Only html, svg, png, pdf output supported") + end + + p +end + +function png_data(p::SyncPlot) + raw = _img_data(p, "png") + raw[length("data:image/png;base64,")+1:end] +end + +function jpeg_data(p::SyncPlot) + raw = _img_data(p, "jpeg") + raw[length("data:image/jpeg;base64,")+1:end] +end + +function webp_data(p::SyncPlot) + raw = _img_data(p, "webp") + raw[length("data:image/webp;base64,")+1:end] +end + +const _mimeformats = Dict("application/eps" => "eps", + "image/eps" => "eps", + "application/pdf" => "pdf", + "image/png" => "png", + "image/jpeg" => "jpeg", + "application/postscript" => "ps", + # "image/svg+xml" => "svg" +) + +for (mime, fmt) in _mimeformats + @eval function Base.writemime(io::IO, ::MIME{symbol($mime)}, p::SyncPlot) + @eval import ImageMagick + + # construct a magic wand and read the image data from png + wand = ImageMagick.MagickWand() + ImageMagick.readimage(wand, base64decode(png_data(p))) + ImageMagick.setimageformat(wand, $fmt) + ImageMagick.writeimage(wand, io) + + end +end + + +for func in [:png_data, :jpeg_data, :wepb_data, :svg_data, + :_img_data, :savefig, :savefig2, :savefig3] + @eval function $(func)(::Plot, args...; kwargs...) + msg = string("$($func) not available without a frontend. ", + "Try calling `$($func)(SyncPlot(p))` instead") + error(msg) + end +end diff --git a/src/subplots.jl b/src/subplots.jl index 509d98c5..e8a5e504 100644 --- a/src/subplots.jl +++ b/src/subplots.jl @@ -102,3 +102,10 @@ function Base.hvcat(rows::Tuple{Vararg{Int}}, ps::Plot...) end _cat(nr, nc, ps...) end + +# methods on syncplot +Base.hcat{TP<:SyncPlot}(sps::TP...) = TP(hcat([sp.plot for sp in sps]...)) +Base.vcat{TP<:SyncPlot}(sps::TP...) = TP(vcat([sp.plot for sp in sps]...)) +Base.vect{TP<:SyncPlot}(sps::TP...) = vcat(sps...) +Base.hvcat{TP<:SyncPlot}(rows::Tuple{Vararg{Int}}, sps::TP...) = + TP(hvcat(rows, [sp.plot for sp in sps]...)) diff --git a/src/traces_layouts.jl b/src/traces_layouts.jl index d99be5ba..6ee474fd 100644 --- a/src/traces_layouts.jl +++ b/src/traces_layouts.jl @@ -33,9 +33,6 @@ kind(l::Layout) = "layout" typealias HasFields Union{GenericTrace, Layout} -Base.writemime(io::IO, ::MIME"text/plain", g::HasFields) = - println(io, json(g, 2)) - # methods that allow you to do `obj["first.second.third"] = val` Base.setindex!(gt::HasFields, val, key::ASCIIString) = setindex!(gt, val, map(symbol, split(key, "."))...) @@ -114,7 +111,7 @@ function Base.getindex(gt::HasFields, k1::Symbol, k2::Symbol, end # Function used to have meaningful display of traces and layouts -function _describe(x::Union{GenericTrace, Layout}) +function _describe(x::HasFields) fields = sort(map(string, keys(x.fields))) n_fields = length(fields) if n_fields == 0 @@ -127,3 +124,6 @@ function _describe(x::Union{GenericTrace, Layout}) return "$(kind(x)) with fields $(join(fields, ", ", ", and "))" end end + +Base.writemime(io::IO, ::MIME"text/plain", g::HasFields) = + println(io, _describe(g)) diff --git a/test/api.jl b/test/api.jl new file mode 100644 index 00000000..d4d9895a --- /dev/null +++ b/test/api.jl @@ -0,0 +1,143 @@ +function fresh_data() + t1 = scatter(;y=[1, 2, 3]) + t2 = scatter(;y=[10, 20, 30]) + t3 = scatter(;y=[100, 200, 300]) + l = Layout(;title="Foo") + p = Plot([copy(t1), copy(t2), copy(t3)], copy(l)) + t1, t2, t3, l, p +end + +@testset "Test api methods on Plot" begin + + @testset "test _update_fields" begin + t1, t2, t3, l, p = fresh_data() + # test dict version + for obj in [t1, l] + o = copy(obj) + PlotlyJS._update_fields(o, Dict{Symbol,Any}(:foo=>"Bar")) + @test o["foo"] == "Bar" + # kwarg version + PlotlyJS._update_fields(o; foo="Foo") + @test o["foo"] == "Foo" + + # dict + kwarg version + PlotlyJS._update_fields(o, Dict{Symbol,Any}(:foo=>"Fuzzy"); + fuzzy_wuzzy="?") + @test o["foo"] == "Fuzzy" + @test o["fuzzy.wuzzy"] == "?" + end + end + + @testset "test relayout!" begin + t1, t2, t3, l, p = fresh_data() + # test on plot object + relayout!(p, Dict{Symbol,Any}(:title=>"Fuzzy"); xaxis_title="wuzzy") + @test p.layout["title"] == "Fuzzy" + @test p.layout["xaxis.title"] == "wuzzy" + + # test on layout object + relayout!(l, Dict{Symbol,Any}(:title=>"Fuzzy"); xaxis_title="wuzzy") + @test l["title"] == "Fuzzy" + @test l["xaxis.title"] == "wuzzy" + end + + @testset "test restyle!" begin + t1, t2, t3, l, p = fresh_data() + # test on trace object + restyle!(t1, Dict{Symbol,Any}(:opacity=>0.4); marker_color="red") + @test t1["opacity"] == 0.4 + @test t1["marker.color"] == "red" + + # test for single trace in plot + restyle!(p, 2, Dict{Symbol,Any}(:opacity=>0.4); marker_color="red") + @test p.data[2]["opacity"] == 0.4 + @test p.data[2]["marker.color"] == "red" + + # test for multiple trace in plot + restyle!(p, [1, 3], Dict{Symbol,Any}(:opacity=>0.9); marker_color="blue") + @test p.data[1]["opacity"] == 0.9 + @test p.data[1]["marker.color"] == "blue" + @test p.data[3]["opacity"] == 0.9 + @test p.data[3]["marker.color"] == "blue" + + # test for all traces in plot + restyle!(p, 1:3, Dict{Symbol,Any}(:opacity=>0.42); marker_color="white") + for i in 1:3 + @test p.data[i]["opacity"] == 0.42 + @test p.data[i]["marker.color"] == "white" + end + end + + @testset "test addtraces!" begin + t1, t2, t3, l, p = fresh_data() + p2 = Plot() + + # test add one trace to end + addtraces!(p2, t1) + @test length(p2.data) == 1 + @test p2.data[1] == t1 + + # test add two traces to end + addtraces!(p2, t2, t3) + @test length(p2.data) == 3 + @test p2.data[2] == t2 + @test p2.data[3] == t3 + + # test add one trace middle + t4 = scatter() + addtraces!(p2, 2, t4) + @test length(p2.data) == 4 + @test p2.data[1] == t1 + @test p2.data[2] == t4 + @test p2.data[3] == t2 + @test p2.data[4] == t3 + + # test add multiple trace middle + t5 = scatter() + t6 = scatter() + addtraces!(p2, 2, t5, t6) + @test length(p2.data) == 6 + @test p2.data[1] == t1 + @test p2.data[2] == t5 + @test p2.data[3] == t6 + @test p2.data[4] == t4 + @test p2.data[5] == t2 + @test p2.data[6] == t3 + end + + @testset "test deletetraces!" begin + t1, t2, t3, l, p = fresh_data() + + # test delete one trace + deletetraces!(p, 2) + @test length(p.data) == 2 + @test p.data[1]["y"] == t1["y"] + @test p.data[2]["y"] == t3["y"] + + # test delete multiple traces + deletetraces!(p, 1, 2) + @test length(p.data) == 0 + end + + @testset "test movetraces!" begin + t1, t2, t3, l, p = fresh_data() + + # test move one trace to end + movetraces!(p, 2) # now 1 3 2 + @test p.data[1]["y"] == t1["y"] + @test p.data[2]["y"] == t3["y"] + @test p.data[3]["y"] == t2["y"] + + # test move two traces to end + movetraces!(p, 1, 2) # now 2 1 3 + @test p.data[1]["y"] == t2["y"] + @test p.data[2]["y"] == t1["y"] + @test p.data[3]["y"] == t3["y"] + + # test move from/to + movetraces!(p, [1, 3], [2, 1]) # 213 -> 123 -> 312 + @test p.data[1]["y"] == t3["y"] + @test p.data[2]["y"] == t1["y"] + @test p.data[3]["y"] == t2["y"] + end +end diff --git a/test/runtests.jl b/test/runtests.jl index fb30de3e..7fef9b10 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1 +1,20 @@ -include("traces.jl") +module PlotlyJSTest + +if VERSION >= v"0.5-" + using Base.Test +else + using BaseTestNext + const Test = BaseTestNext +end + +include(joinpath(dirname(dirname(abspath(@__FILE__))), "src", "PlotlyJS.jl")) +using .PlotlyJS +typealias M PlotlyJS + +tests = length(ARGS) > 0 ? ARGS : ["traces", "api"] + +for fn in tests + include(string(fn, ".jl")) +end + +end diff --git a/test/traces.jl b/test/traces.jl index b2126f8c..4e014aa5 100644 --- a/test/traces.jl +++ b/test/traces.jl @@ -1,208 +1,206 @@ -module TestTraces - -using Base.Test -include(joinpath(dirname(dirname(abspath(@__FILE__))), "src", "PlotlyJS.jl")) -typealias M PlotlyJS gt = M.GenericTrace("scatter"; x=1:10, y=sin(1:10)) -@test sort(collect(keys(gt.fields))) == [:x, :y] - -# test setindex! methods -gt[:visible] = true -@test length(gt.fields) == 3 -@test haskey(gt.fields, :visible) -@test gt.fields[:visible] == true - -# now try with string. Make sure it updates inplace -gt["visible"] = false -@test length(gt.fields) == 3 -@test haskey(gt.fields, :visible) -@test gt.fields[:visible] == false - -# -------- # -# 2 levels # -# -------- # -gt[:line, :color] = "red" -@test length(gt.fields) == 4 -@test haskey(gt.fields, :line) -@test isa(gt.fields[:line], Dict) -@test gt.fields[:line][:color] == "red" -@test gt["line.color"] == "red" - -# now try string version -gt["line", "color"] = "blue" -@test length(gt.fields) == 4 -@test haskey(gt.fields, :line) -@test isa(gt.fields[:line], Dict) -@test gt.fields[:line][:color] == "blue" -@test gt["line.color"] == "blue" - -# now try convenience string dot notation -gt["line.color"] = "green" -@test length(gt.fields) == 4 -@test haskey(gt.fields, :line) -@test isa(gt.fields[:line], Dict) -@test gt.fields[:line][:color] == "green" -@test gt["line.color"] == "green" - -# now try symbol with underscore -gt[:(line_color)] = "orange" -@test length(gt.fields) == 4 -@test haskey(gt.fields, :line) -@test isa(gt.fields[:line], Dict) -@test gt.fields[:line][:color] == "orange" -@test gt["line.color"] == "orange" - -# now try string with underscore -gt["line_color"] = "magenta" -@test length(gt.fields) == 4 -@test haskey(gt.fields, :line) -@test isa(gt.fields[:line], Dict) -@test gt.fields[:line][:color] == "magenta" -@test gt["line.color"] == "magenta" - -# -------- # -# 3 levels # -# -------- # -gt[:marker, :line, :color] = "red" -@test length(gt.fields) == 5 -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test haskey(gt.fields[:marker], :line) -@test isa(gt.fields[:marker][:line], Dict) -@test haskey(gt.fields[:marker][:line], :color) -@test gt.fields[:marker][:line][:color] == "red" -@test gt["marker.line.color"] == "red" - -# now try string version -gt["marker", "line", "color"] = "blue" -@test length(gt.fields) == 5 -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test haskey(gt.fields[:marker], :line) -@test isa(gt.fields[:marker][:line], Dict) -@test haskey(gt.fields[:marker][:line], :color) -@test gt.fields[:marker][:line][:color] == "blue" -@test gt["marker.line.color"] == "blue" - -# now try convenience string dot notation -gt["marker.line.color"] = "green" -@test length(gt.fields) == 5 -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test haskey(gt.fields[:marker], :line) -@test isa(gt.fields[:marker][:line], Dict) -@test haskey(gt.fields[:marker][:line], :color) -@test gt.fields[:marker][:line][:color] == "green" -@test gt["marker.line.color"] == "green" - -# now string with underscore notation -gt["marker_line_color"] = "orange" -@test length(gt.fields) == 5 -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test haskey(gt.fields[:marker], :line) -@test isa(gt.fields[:marker][:line], Dict) -@test haskey(gt.fields[:marker][:line], :color) -@test gt.fields[:marker][:line][:color] == "orange" -@test gt["marker.line.color"] == "orange" - -# now symbol with underscore notation -gt[:(marker_line_color)] = "magenta" -@test length(gt.fields) == 5 -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test haskey(gt.fields[:marker], :line) -@test isa(gt.fields[:marker][:line], Dict) -@test haskey(gt.fields[:marker][:line], :color) -@test gt.fields[:marker][:line][:color] == "magenta" -@test gt["marker.line.color"] == "magenta" - -# -------- # -# 4 levels # -# -------- # -gt[:marker, :colorbar, :tickfont, :family] = "Hasklig-ExtraLight" -@test length(gt.fields) == 5 # notice we didn't add another top level key -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test length(gt.fields[:marker]) == 2 # but we did add a key at this level -@test haskey(gt.fields[:marker], :colorbar) -@test isa(gt.fields[:marker][:colorbar], Dict) -@test haskey(gt.fields[:marker][:colorbar], :tickfont) -@test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) -@test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) -@test gt.fields[:marker][:colorbar][:tickfont][:family] == "Hasklig-ExtraLight" -@test gt["marker.colorbar.tickfont.family"] == "Hasklig-ExtraLight" - -# now try string version -gt["marker", "colorbar", "tickfont", "family"] = "Hasklig-Light" -@test length(gt.fields) == 5 -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test length(gt.fields[:marker]) == 2 -@test haskey(gt.fields[:marker], :colorbar) -@test isa(gt.fields[:marker][:colorbar], Dict) -@test haskey(gt.fields[:marker][:colorbar], :tickfont) -@test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) -@test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) -@test gt.fields[:marker][:colorbar][:tickfont][:family] == "Hasklig-Light" -@test gt["marker.colorbar.tickfont.family"] == "Hasklig-Light" - -# now try convenience string dot notation -gt["marker.colorbar.tickfont.family"] = "Hasklig-Medium" -@test length(gt.fields) == 5 # notice we didn't add another top level key -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test length(gt.fields[:marker]) == 2 # but we did add a key at this level -@test haskey(gt.fields[:marker], :colorbar) -@test isa(gt.fields[:marker][:colorbar], Dict) -@test haskey(gt.fields[:marker][:colorbar], :tickfont) -@test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) -@test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) -@test gt.fields[:marker][:colorbar][:tickfont][:family] == "Hasklig-Medium" -@test gt["marker.colorbar.tickfont.family"] == "Hasklig-Medium" - -# now string with underscore notation -gt["marker_colorbar_tickfont_family"] = "Webdings" -@test length(gt.fields) == 5 # notice we didn't add another top level key -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test length(gt.fields[:marker]) == 2 # but we did add a key at this level -@test haskey(gt.fields[:marker], :colorbar) -@test isa(gt.fields[:marker][:colorbar], Dict) -@test haskey(gt.fields[:marker][:colorbar], :tickfont) -@test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) -@test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) -@test gt.fields[:marker][:colorbar][:tickfont][:family] == "Webdings" -@test gt["marker.colorbar.tickfont.family"] == "Webdings" - -# now symbol with underscore notation -gt[:marker_colorbar_tickfont_family] = "Webdings42" -@test length(gt.fields) == 5 # notice we didn't add another top level key -@test haskey(gt.fields, :marker) -@test isa(gt.fields[:marker], Dict) -@test length(gt.fields[:marker]) == 2 # but we did add a key at this level -@test haskey(gt.fields[:marker], :colorbar) -@test isa(gt.fields[:marker][:colorbar], Dict) -@test haskey(gt.fields[:marker][:colorbar], :tickfont) -@test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) -@test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) -@test gt.fields[:marker][:colorbar][:tickfont][:family] == "Webdings42" -@test gt["marker.colorbar.tickfont.family"] == "Webdings42" - - -# now test underscore constructor and see if it matches gt -gt2 = M.scatter(;x=1:10, y=sin(1:10), - marker_colorbar_tickfont_family="Webdings42", - marker_line_color="magenta", - line_color="magenta", - visible=false) -@test sort(collect(keys(gt.fields))) == sort(collect(keys(gt2.fields))) -for k in keys(gt.fields) - @test gt[k] == gt2[k] + +@testset "test constructors" begin + @test sort(collect(keys(gt.fields))) == [:x, :y] end -# error on 5 levels -Test.@test_throws MethodError gt["marker.colorbar.tickfont.family.foo"] = :bar +@testset "test setindex!, getindex methods" begin + gt[:visible] = true + @test length(gt.fields) == 3 + @test haskey(gt.fields, :visible) + @test gt.fields[:visible] == true + + # now try with string. Make sure it updates inplace + gt["visible"] = false + @test length(gt.fields) == 3 + @test haskey(gt.fields, :visible) + @test gt.fields[:visible] == false + + # -------- # + # 2 levels # + # -------- # + gt[:line, :color] = "red" + @test length(gt.fields) == 4 + @test haskey(gt.fields, :line) + @test isa(gt.fields[:line], Dict) + @test gt.fields[:line][:color] == "red" + @test gt["line.color"] == "red" + + # now try string version + gt["line", "color"] = "blue" + @test length(gt.fields) == 4 + @test haskey(gt.fields, :line) + @test isa(gt.fields[:line], Dict) + @test gt.fields[:line][:color] == "blue" + @test gt["line.color"] == "blue" + + # now try convenience string dot notation + gt["line.color"] = "green" + @test length(gt.fields) == 4 + @test haskey(gt.fields, :line) + @test isa(gt.fields[:line], Dict) + @test gt.fields[:line][:color] == "green" + @test gt["line.color"] == "green" + + # now try symbol with underscore + gt[:(line_color)] = "orange" + @test length(gt.fields) == 4 + @test haskey(gt.fields, :line) + @test isa(gt.fields[:line], Dict) + @test gt.fields[:line][:color] == "orange" + @test gt["line.color"] == "orange" + + # now try string with underscore + gt["line_color"] = "magenta" + @test length(gt.fields) == 4 + @test haskey(gt.fields, :line) + @test isa(gt.fields[:line], Dict) + @test gt.fields[:line][:color] == "magenta" + @test gt["line.color"] == "magenta" + + # -------- # + # 3 levels # + # -------- # + gt[:marker, :line, :color] = "red" + @test length(gt.fields) == 5 + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test haskey(gt.fields[:marker], :line) + @test isa(gt.fields[:marker][:line], Dict) + @test haskey(gt.fields[:marker][:line], :color) + @test gt.fields[:marker][:line][:color] == "red" + @test gt["marker.line.color"] == "red" + + # now try string version + gt["marker", "line", "color"] = "blue" + @test length(gt.fields) == 5 + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test haskey(gt.fields[:marker], :line) + @test isa(gt.fields[:marker][:line], Dict) + @test haskey(gt.fields[:marker][:line], :color) + @test gt.fields[:marker][:line][:color] == "blue" + @test gt["marker.line.color"] == "blue" + + # now try convenience string dot notation + gt["marker.line.color"] = "green" + @test length(gt.fields) == 5 + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test haskey(gt.fields[:marker], :line) + @test isa(gt.fields[:marker][:line], Dict) + @test haskey(gt.fields[:marker][:line], :color) + @test gt.fields[:marker][:line][:color] == "green" + @test gt["marker.line.color"] == "green" + + # now string with underscore notation + gt["marker_line_color"] = "orange" + @test length(gt.fields) == 5 + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test haskey(gt.fields[:marker], :line) + @test isa(gt.fields[:marker][:line], Dict) + @test haskey(gt.fields[:marker][:line], :color) + @test gt.fields[:marker][:line][:color] == "orange" + @test gt["marker.line.color"] == "orange" + + # now symbol with underscore notation + gt[:(marker_line_color)] = "magenta" + @test length(gt.fields) == 5 + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test haskey(gt.fields[:marker], :line) + @test isa(gt.fields[:marker][:line], Dict) + @test haskey(gt.fields[:marker][:line], :color) + @test gt.fields[:marker][:line][:color] == "magenta" + @test gt["marker.line.color"] == "magenta" + + # -------- # + # 4 levels # + # -------- # + gt[:marker, :colorbar, :tickfont, :family] = "Hasklig-ExtraLight" + @test length(gt.fields) == 5 # notice we didn't add another top level key + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test length(gt.fields[:marker]) == 2 # but we did add a key at this level + @test haskey(gt.fields[:marker], :colorbar) + @test isa(gt.fields[:marker][:colorbar], Dict) + @test haskey(gt.fields[:marker][:colorbar], :tickfont) + @test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) + @test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) + @test gt.fields[:marker][:colorbar][:tickfont][:family] == "Hasklig-ExtraLight" + @test gt["marker.colorbar.tickfont.family"] == "Hasklig-ExtraLight" + + # now try string version + gt["marker", "colorbar", "tickfont", "family"] = "Hasklig-Light" + @test length(gt.fields) == 5 + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test length(gt.fields[:marker]) == 2 + @test haskey(gt.fields[:marker], :colorbar) + @test isa(gt.fields[:marker][:colorbar], Dict) + @test haskey(gt.fields[:marker][:colorbar], :tickfont) + @test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) + @test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) + @test gt.fields[:marker][:colorbar][:tickfont][:family] == "Hasklig-Light" + @test gt["marker.colorbar.tickfont.family"] == "Hasklig-Light" + + # now try convenience string dot notation + gt["marker.colorbar.tickfont.family"] = "Hasklig-Medium" + @test length(gt.fields) == 5 # notice we didn't add another top level key + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test length(gt.fields[:marker]) == 2 # but we did add a key at this level + @test haskey(gt.fields[:marker], :colorbar) + @test isa(gt.fields[:marker][:colorbar], Dict) + @test haskey(gt.fields[:marker][:colorbar], :tickfont) + @test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) + @test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) + @test gt.fields[:marker][:colorbar][:tickfont][:family] == "Hasklig-Medium" + @test gt["marker.colorbar.tickfont.family"] == "Hasklig-Medium" + + # now string with underscore notation + gt["marker_colorbar_tickfont_family"] = "Webdings" + @test length(gt.fields) == 5 # notice we didn't add another top level key + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test length(gt.fields[:marker]) == 2 # but we did add a key at this level + @test haskey(gt.fields[:marker], :colorbar) + @test isa(gt.fields[:marker][:colorbar], Dict) + @test haskey(gt.fields[:marker][:colorbar], :tickfont) + @test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) + @test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) + @test gt.fields[:marker][:colorbar][:tickfont][:family] == "Webdings" + @test gt["marker.colorbar.tickfont.family"] == "Webdings" + + # now symbol with underscore notation + gt[:marker_colorbar_tickfont_family] = "Webdings42" + @test length(gt.fields) == 5 # notice we didn't add another top level key + @test haskey(gt.fields, :marker) + @test isa(gt.fields[:marker], Dict) + @test length(gt.fields[:marker]) == 2 # but we did add a key at this level + @test haskey(gt.fields[:marker], :colorbar) + @test isa(gt.fields[:marker][:colorbar], Dict) + @test haskey(gt.fields[:marker][:colorbar], :tickfont) + @test isa(gt.fields[:marker][:colorbar][:tickfont], Dict) + @test haskey(gt.fields[:marker][:colorbar][:tickfont], :family) + @test gt.fields[:marker][:colorbar][:tickfont][:family] == "Webdings42" + @test gt["marker.colorbar.tickfont.family"] == "Webdings42" + + # error on 5 levels + @test_throws MethodError gt["marker.colorbar.tickfont.family.foo"] = :bar +end -end # module +@testset "testing underscore constructor" begin + # now test underscore constructor and see if it matches gt + gt2 = M.scatter(;x=1:10, y=sin(1:10), + marker_colorbar_tickfont_family="Webdings42", + marker_line_color="magenta", + line_color="magenta", + visible=false) + @test sort(collect(keys(gt.fields))) == sort(collect(keys(gt2.fields))) + for k in keys(gt.fields) + @test gt[k] == gt2[k] + end +end