Skip to content

Commit 048d350

Browse files
committed
WIP: more work on using v2 api... almost done
1 parent b2d0af2 commit 048d350

File tree

5 files changed

+482
-263
lines changed

5 files changed

+482
-263
lines changed

REQUIRE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
julia 0.4
1+
julia 0.6
22
Requests 0.3.5
33
JSON
44
PlotlyJS 0.2.0

src/Plotly.jl

Lines changed: 213 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
11
# __precompile__(true)
22

33
module Plotly
4-
using Compat
5-
using Compat: String
6-
using Requests
7-
using JSON
8-
using Reexport: @reexport
9-
import Requests: URI, post
104

5+
using URIParser
6+
using Reexport
117
@reexport using PlotlyJS
128
export post
139
export set_credentials_file, RemotePlot, download_plot
1410

15-
include("utils.jl")
16-
17-
const api_version = "v2"
18-
19-
const default_kwargs = Dict{Symbol,Any}(:filename=>"Plot from Julia API",
20-
:world_readable=> true)
11+
const _SRC_ATTRS = let
12+
_src_attr_path = joinpath(dirname(PlotlyJS._js_path), "src_attrs.csv")
13+
raw_src_attrs = vec(readdlm(_src_attr_path))
14+
src_attrs = map(x -> x[1:end-3], raw_src_attrs) # remove the `src` suffix
15+
Set(map(Symbol, src_attrs))
16+
end::Set{Symbol}
2117

22-
const default_opts = Dict{Symbol,Any}(:origin => "plot",
23-
:platform => "Julia",
24-
:version => "0.2")
18+
include("utils.jl")
19+
include("v2.jl")
2520

2621
## Taken from https://github.com/johnmyleswhite/Vega.jl/blob/master/src/Vega.jl#L51
2722
# Open a URL in a browser
@@ -37,16 +32,28 @@ end
3732

3833
openurl(url::URI) = openurl(string(url))
3934

40-
get_plot_endpoint() = "$(get_config().plotly_domain)/clientresp"
41-
4235
"""
4336
Proxy for a plot stored on the Plotly cloud.
4437
"""
45-
immutable RemotePlot
38+
struct RemotePlot
4639
url::URI
4740
end
4841
RemotePlot(url::String) = RemotePlot(URI(url))
4942

43+
"""
44+
fid(rp::RemotePlot)
45+
46+
Return the unique plotly `fid` for `rp`. Throws an error if the url inside
47+
`rp` is not correctly formed.
48+
"""
49+
function fid(rp::RemotePlot)
50+
parts = match(r"^/~([^/]+)/(\d+)/?", rp.url.path)
51+
if parts === nothing
52+
error("Malformed RemotePlot url")
53+
end
54+
"$(parts[1]):$(parts[2])"
55+
end
56+
5057
"""
5158
Display a plot stored in the Plotly cloud in a browser window.
5259
"""
@@ -57,105 +64,215 @@ Post a local Plotly plot to the Plotly cloud.
5764
5865
Must be signed in first.
5966
"""
60-
function post(p::Plot; kwargs...)
67+
function post(p::Plot; fileopt=:overwrite, filename=nothing, kwargs...)
68+
fileopt = filename == nothing ? :create : Symbol(fileopt)
69+
grid_fn = string(filename, "_", "Grid")
70+
clean_p = srcify(p; fileopt=fileopt, grid_fn=grid_fn, kwargs...)
71+
if fileopt == :overwrite
72+
file_data = try_lookup(filename)
73+
if file_data == nothing
74+
fileopt = :create
75+
else
76+
res = plot_update(file_data["fid"], figure=clean_p)
77+
return RemotePlot(res["web_url"])
78+
end
79+
end
80+
if fileopt == :create
81+
res = plot_create(clean_p; filename=filename, kwargs...)
82+
return RemotePlot(res["file"]["web_url"])
83+
else
84+
error("fileopt must be one of `overwrite` and `create`")
85+
end
86+
end
87+
88+
function post_v1(p::Plot; kwargs...)
89+
default_kwargs = Dict{Symbol,Any}(:filename=>"Plot from Julia API",
90+
:world_readable=> true)
91+
default_opts = Dict{Symbol,Any}(:origin => "plot",
92+
:platform => "Julia",
93+
:version => "0.2")
6194
creds = get_credentials()
62-
endpoint = get_plot_endpoint()
63-
opt = merge(default_kwargs, Dict(:layout => p.layout.fields),
64-
Dict(kwargs))
95+
endpoint = "$(get_config().plotly_domain)/clientresp"
96+
opt = merge(
97+
default_kwargs,
98+
Dict(:layout => p.layout.fields),
99+
Dict(kwargs)
100+
)
65101

66-
data = merge(default_opts,
67-
Dict("un" => creds.username,
68-
"key" => creds.api_key,
69-
"args" => json(p.data),
70-
"kwargs" => json(opt)))
102+
data = merge(
103+
default_opts,
104+
Dict(
105+
"un" => creds.username,
106+
"key" => creds.api_key,
107+
"args" => json(p.data),
108+
"kwargs" => json(opt)
109+
)
110+
)
71111

72112
r = post(endpoint, data=data)
73-
body = parse_response(r)
113+
body = Requests.json(r)
114+
if Requests.statuscode(r) 200
115+
throw(PlotlyError("Non-sucessful status code: $(statuscode(r))"))
116+
elseif "error" keys(body) && body["error"] ""
117+
throw(PlotlyError(body["error"]))
118+
elseif "detail" keys(body) && body["detail"] ""
119+
throw(PlotlyError(body["detail"]))
120+
end
74121
return RemotePlot(URI(body["url"]))
75122
end
76123

77-
function Requests.post(l::AbstractLayout, meta_opts=Dict(); meta_kwargs...)
78-
creds = get_credentials()
79-
endpoint = get_plot_endpoint()
80-
81-
meta = merge(meta_opts,
82-
get_required_params(["filename", "fileopt"], meta_opts),
83-
Dict(meta_kwargs))
84-
data = merge(default_opts,
85-
Dict("un" => creds.username,
86-
"key" => creds.api_key,
87-
"args" => json(l),
88-
"origin" => "layout",
89-
"kwargs" => json(meta)))
90-
91-
parse_response(post(endpoint, data=data))
92-
end
93-
94124
post(p::PlotlyJS.SyncPlot; kwargs...) = post(p.plot; kwargs...)
95125

96-
function style(style_opts, meta_opts=Dict(); meta_kwargs...)
97-
creds = get_credentials()
98-
endpoint = get_plot_endpoint()
99-
100-
meta = merge(meta_opts,
101-
get_required_params(["filename", "fileopt"], meta_opts),
102-
Dict(meta_kwargs))
103-
data = merge(default_opts,
104-
Dict("un" => creds.username,
105-
"key" => creds.api_key,
106-
"args" => json([style_opts]),
107-
"origin" => "style",
108-
"kwargs" => json(meta_opts)))
109-
110-
parse_response(post(endpoint, data=data))
126+
"""
127+
srcify!(p::Plot; fileopt::Symbol=:overwrite, grid_fn=nothing, kwargs...)
128+
129+
Look through each trace and the Layout for attributes that have a value of type
130+
Union{AbstractArray,Tuple} and are able to be set via `(attributename)(src)` in
131+
the plotly.js api. For each of these, do the following:
132+
133+
1. Extract the value so it can become a column in a Grid
134+
2. Remove that key/value pair from the trace/Layout
135+
136+
This happens in place, so the `src`ified fields will be removed within the
137+
input to this function.
138+
"""
139+
function extract_grid_data!(p::Plot)
140+
data_for_grid = Dict()
141+
function add_to_grid!(k::Vector, v::Union{AbstractArray,Tuple})
142+
if !(k[end] in Plotly._SRC_ATTRS)
143+
return
144+
# only do what follows if k is one of the src attrs
145+
end
146+
# all the magic happens here
147+
the_key = join(map(string, k), "_")
148+
setindex!(data_for_grid, Dict{Any,Any}("data" => v, "temp" => k), the_key) # step 1
149+
150+
# now remove this from the plot
151+
if k[1] == "trace"
152+
ind = k[2]
153+
attr = join(map(String, k[3:end]), "_")
154+
pop!(p.data[ind], attr)
155+
elseif k[1] == "layout"
156+
attr = join(map(String, k[2:end]), "_")
157+
pop!(p.layout, attr)
158+
else
159+
error("bad key...")
160+
end
161+
end
162+
function add_to_grid!(k1::Vector, v::Associative)
163+
for (k2, v2) in v
164+
add_to_grid!(vcat(k1, k2), v2)
165+
end
166+
end
167+
add_to_grid!(k::Vector, v) = nothing # otherwise don't do anything...
168+
169+
for (i, t) in enumerate(p.data)
170+
k = ["trace", i]
171+
for (field, val) in t
172+
add_to_grid!(vcat(k, field), val)
173+
end
174+
end
175+
176+
k = ["layout"]
177+
for (field, val) in p.layout
178+
add_to_grid!(vcat(k, field), val)
179+
end
180+
data_for_grid
111181
end
112182

183+
extract_grid_data(p::Plot) = extract_grid_data!(deepcopy(p))
184+
113185
"""
114-
Transport a plot from the Plotly cloud to a local `Plot` object.
186+
srcify!(p::Plot; fileopt::Symbol=:overwrite, grid_fn=nothing, kwargs...)
115187
116-
Must be signed in first if the plot is not public.
188+
This function does three things:
189+
190+
1. Calls `extract_grid_data!(p)` (see docs) to remove attributes that can be
191+
set via `(attributename)(src)` in the plotly.js api.
192+
2. Creates a grid on the plotly server containing the extract data
193+
3. Maps the `(attributename)(src)` attribute to the grid column
194+
195+
If fileopt is `:create` and `grid_fn` exists under the User's plotly account,
196+
then the changes described above will happen in-place on the grid.
197+
198+
If either of those conditions are not met, then a new grid will be created.
117199
"""
118-
function Base.download(plot::RemotePlot)
119-
creds = get_credentials()
120-
username = creds.username
121-
api_key = creds.api_key
122-
lib_version = string(default_opts[:platform], " ", default_opts[:version])
123-
auth = string("Basic ", base64encode("$username:$api_key"))
124-
options = Dict("Authorization"=>auth, "Plotly-Client-Platform"=>lib_version)
125-
original_path = plot.url.path
126-
if original_path[end] == '/'
127-
path = original_path[1:end-1]
200+
function srcify!(p::Plot; fileopt::Symbol=:overwrite, grid_fn=nothing, kwargs...)
201+
data_for_grid = extract_grid_data!(p)
202+
temp_map = Dict()
203+
for (k, v) in data_for_grid
204+
temp_map[k] = pop!(v, "temp")
205+
end
206+
207+
if fileopt == :overwrite
208+
grid_info = try_me(grid_lookup, grid_fn )
209+
fid = lookup_info["fid"]
210+
if lookup_info == nothing
211+
fileopt = :create
212+
else
213+
uid_map = grid_overwrite!(data_for_grid; fid=lookup_info["fid"])
214+
@goto add_src_attrs
215+
end
216+
end
217+
218+
if fileopt == :create
219+
# add order to each grid
220+
for (i, (k, v)) in enumerate(data_for_grid)
221+
v["order"] = i-1
222+
end
223+
res = grid_create(Dict("cols" => data_for_grid); filename=grid_fn, kwargs...)
224+
fid = res["file"]["fid"]
225+
226+
uid_map = Dict()
227+
for col in res["file"]["cols"]
228+
uid_map[col["name"]] = col["uid"]
229+
end
128230
else
129-
path = original_path
231+
error("Can only create or overwrite")
130232
end
131-
endpoint = URI(plot.url, path="$path.json")
132-
response = get(endpoint, headers=options)
133-
local_plot = JSON.parse(Plot, bytestring(response))
134-
return PlotlyJS.SyncPlot(local_plot)
233+
234+
@label add_src_attrs
235+
# Add (attributename)(src) = uid fields to plot
236+
for k in keys(data_for_grid)
237+
key = temp_map[k]
238+
uid = uid_map[k]
239+
if key[1] == "trace"
240+
trace_ind = key[2]
241+
the_key = join(vcat(key[3:end-1], string(key[end], "src")), "_")
242+
col_uid = string(fid, ":", uid)
243+
p.data[trace_ind][the_key] = col_uid
244+
elseif key[1] == "layout"
245+
the_key = join(vcat(key[2:end-1], string(key[end], "src")), "_")
246+
col_uid = string(fid, ":", uid)
247+
p.layout[the_key] = col_uid
248+
else
249+
error("bad key...")
250+
end
251+
end
252+
p
135253
end
136254

137-
download_plot(url) = download(RemotePlot(url))
138-
download_plot(plot::RemotePlot) = download(plot)
255+
"""
256+
srcify(p::Plot)
139257
140-
immutable PlotlyError <: Exception
141-
msg::String
142-
end
258+
Allocating version of `srcify!` the plot will be deepcopied before passing
259+
to `srcify!`, so the argument passed to this function will not be modified.
260+
"""
261+
srcify(p::Plot; kwargs...) = srcify!(deepcopy(p); kwargs...)
143262

144-
function Base.show(io::IO, err::PlotlyError)
145-
print(io, "Plotly error: $(err.msg)")
146-
end
263+
"""
264+
Transport a plot from the Plotly cloud to a local `Plot` object.
147265
148-
function parse_response(r)
149-
body = Requests.json(r)
150-
if statuscode(r) 200
151-
throw(PlotlyError("Non-sucessful status code: $(statuscode(r))"))
152-
elseif "error" keys(body) && body["error"] ""
153-
throw(PlotlyError(body["error"]))
154-
elseif "detail" keys(body) && body["detail"] ""
155-
throw(PlotlyError(body["detail"]))
156-
else
157-
body
158-
end
266+
Must be signed in first if the plot is not public.
267+
"""
268+
function Base.download(p::RemotePlot)
269+
res = plot_content(fid(p), inline_data=true)
270+
data = GenericTrace[GenericTrace(tr) for tr in res["data"]]
271+
layout = Layout(res["layout"])
272+
plot(data, layout)
159273
end
160274

275+
download_plot(url) = download(RemotePlot(url))
276+
download_plot(plot::RemotePlot) = download(plot)
277+
161278
end

src/utils.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
using JSON
22

3-
default_endpoints = Dict(
3+
const default_endpoints = Dict(
44
"base" => "https://plot.ly",
5-
"api" => "https://api.plot.ly/v2")
5+
"api" => "https://api.plot.ly/v2"
6+
)
67

78
type PlotlyCredentials
89
username::String

0 commit comments

Comments
 (0)