Skip to content

Commit 22145eb

Browse files
committed
Merge branch 'grid_api' of https://github.com/plotly/python-api into grid_api
2 parents 06504a1 + 33c3ad5 commit 22145eb

File tree

11 files changed

+185
-42
lines changed

11 files changed

+185
-42
lines changed

plotly/exceptions.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ class PlotlyError(Exception):
2323
pass
2424

2525

26-
class InputError(Exception):
26+
27+
class InputError(PlotlyError):
2728
pass
2829

2930

30-
class PlotlyRequestError(Exception):
31+
class PlotlyRequestError(PlotlyError):
3132
def __init__(self, requests_exception):
3233
self.status_code = requests_exception.response.status_code
3334
self.HTTPError = requests_exception
@@ -67,6 +68,10 @@ def __str__(self):
6768
"can't have duplicate column names. Rename "
6869
"the column \"{}\" and try again."
6970
)
71+
## Would Cause Server Errors ##
72+
73+
class PlotlyEmptyDataError(PlotlyError):
74+
pass
7075

7176

7277
## Graph Objects Errors ##

plotly/graph_objs/graph_objs_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def curtail_val_repr(val, max_chars, add_delim=False):
211211
# TODO: can we assume this ends in "'"
212212
r = r[:max_chars - len(end + "'")] + end + "'"
213213
elif (isinstance(val, list) and
214-
max_chars >= len("[{end}]".format(end))):
214+
max_chars >= len("[{end}]".format(end=end))):
215215
r = r[:max_chars - len(end + ']')] + end + ']'
216216
else:
217217
r = r[:max_chars - len(end)] + end

plotly/graph_reference/graph_objs_meta.json

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3736,13 +3736,13 @@
37363736
},
37373737
"bargap": {
37383738
"key_type": "style",
3739-
"val_types": "number: x >= 0",
3739+
"val_types": "number: x in [0, 1)",
37403740
"required": false,
37413741
"description": "For bar and histogram plots only. Sets the gap between bars (or sets of bars) at different locations."
37423742
},
37433743
"bargroupgap": {
37443744
"key_type": "style",
3745-
"val_types": "number: x >= 0",
3745+
"val_types": "number: x in [0, 1)",
37463746
"required": false,
37473747
"description": "For bar and histogram plots only. Sets the gap between bars in the same group. That is, when multiple bar objects are plotted and share the same locations, this sets the distance between bars at each ___location."
37483748
},
@@ -3752,6 +3752,18 @@
37523752
"required": false,
37533753
"description": "For box plots only. Sets how groups of box plots appear. If set to 'overlay', a group of boxes will be plotted directly on top of one another at their specified ___location. If set to 'group', the boxes will be centered around their shared ___location, but they will not overlap."
37543754
},
3755+
"boxgap": {
3756+
"key_type": "style",
3757+
"val_types": "number: x in [0, 1)",
3758+
"required": false,
3759+
"description": "For box plots only. Sets the gap between boxes at different locations (i.e. x-labels). If there are multiple boxes at a single x-label, then this sets the gap between these sets of boxes.For example, if 0, then there is no gap between boxes. If 0.25, then this gap occupies 25% of the available space and the box width (or width of the set of boxes) occupies the remaining 75%."
3760+
},
3761+
"boxgroupgap": {
3762+
"key_type": "style",
3763+
"val_types": "number: x in [0, 1)",
3764+
"required": false,
3765+
"description": "For box plots only. Sets the gap between boxes in the same group, where a group is the set of boxes with the same ___location (i.e. x-label). For example, if 0, then there is no gap between boxes. If 0.25, then this gap occupies 25% of the available space and the box width occupies the remaining 75%."
3766+
},
37553767
"radialaxis": {
37563768
"key_type": "object",
37573769
"val_types": "dictionary-like object",
@@ -3868,13 +3880,13 @@
38683880
"key_type": "style",
38693881
"val_types": "number",
38703882
"required": false,
3871-
"description": "Sets the 'x' position of this legend.Use in conjunction with 'xref' and 'xanchor' to fine-tune the ___location of this legend."
3883+
"description": "Sets the 'x' position of this legend. Use in conjunction with 'xref' and 'xanchor' to fine-tune the ___location of this legend."
38723884
},
38733885
"y": {
38743886
"key_type": "style",
38753887
"val_types": "number",
38763888
"required": false,
3877-
"description": "Sets the 'y' position of this legend.Use in conjunction with 'yref' and 'yanchor' to fine-tune the ___location of this legend."
3889+
"description": "Sets the 'y' position of this legend. Use in conjunction with 'yref' and 'yanchor' to fine-tune the ___location of this legend."
38783890
},
38793891
"traceorder": {
38803892
"key_type": "style",
@@ -3964,22 +3976,22 @@
39643976
"key_type": "plot_info",
39653977
"val_types": "number",
39663978
"required": false,
3967-
"description": "Sets the 'x' position of this annotation.Use in conjunction with 'xref' and 'xanchor' to fine-tune the ___location of this annotation."
3979+
"description": "Sets the 'x' position of this annotation. Use in conjunction with 'xref' and 'xanchor' to fine-tune the ___location of this annotation."
39683980
},
39693981
"y": {
39703982
"key_type": "plot_info",
39713983
"val_types": "number",
39723984
"required": false,
3973-
"description": "Sets the 'y' position of this annotation.Use in conjunction with 'yref' and 'yanchor' to fine-tune the ___location of this annotation."
3985+
"description": "Sets the 'y' position of this annotation. Use in conjunction with 'yref' and 'yanchor' to fine-tune the ___location of this annotation."
39743986
},
39753987
"xref": {
3976-
"key_type": "style",
3988+
"key_type": "plot_info",
39773989
"val_types": "'paper' | 'x1' | 'x2' | etc",
39783990
"required": false,
39793991
"description": "Sets the x coordinate system which this object refers to. If you reference an axis, e.g., 'x2', the object will move with pan-and-zoom to stay fixed to this point. If you reference the 'paper', it remains fixed regardless of pan-and-zoom. In other words, if set to 'paper', the 'x' ___location refers to the distance from the left side of the plotting area in normalized coordinates where 0 is 'left' and 1 is 'right'. If set to refer to an xaxis' , e.g., 'x1', 'x2', 'x3', etc., the 'x' ___location will refer to the ___location in terms of this axis."
39803992
},
39813993
"yref": {
3982-
"key_type": "style",
3994+
"key_type": "plot_info",
39833995
"val_types": "'paper' | 'y1' | 'y2' | etc",
39843996
"required": false,
39853997
"description": "Sets the y coordinate system which this object refers to. If you reference an axis, e.g., 'y2', the object will move with pan-and-zoom to stay fixed to this point. If you reference the 'paper', it remains fixed regardless of pan-and-zoom. In other words, if set to 'paper', the 'y' ___location refers to the distance from the left side of the plotting area in normalized coordinates where 0 is 'bottom' and 1 is 'top'. If set to refer to an yaxis' , e.g., 'y1', 'y2', 'y3', etc., the 'y' ___location will refer to the ___location in terms of this axis."

plotly/matplotlylib/mpltools.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77

88
import math
99
import warnings
10-
import datetime
1110
import matplotlib.dates
11+
import pytz
12+
1213

1314
def check_bar_match(old_bar, new_bar):
1415
"""Check if two bars belong in the same collection (bar chart).
@@ -418,7 +419,7 @@ def prep_ticks(ax, index, ax_type, props):
418419
return dict()
419420
# get tick label formatting information
420421
formatter = axis.get_major_formatter().__class__.__name__
421-
if ax_type == 'x' and formatter == 'DateFormatter':
422+
if ax_type == 'x' and 'DateFormatter' in formatter:
422423
axis_dict['type'] = 'date'
423424
try:
424425
axis_dict['tick0'] = mpl_dates_to_datestrings(axis_dict['tick0'])
@@ -455,13 +456,15 @@ def prep_xy_axis(ax, props, x_bounds, y_bounds):
455456
yaxis.update(prep_ticks(ax, 1, 'y', props))
456457
return xaxis, yaxis
457458

459+
458460
def mpl_dates_to_datestrings(mpl_dates, format_string="%Y-%m-%d %H:%M:%S"):
461+
"""Convert matplotlib dates to formatted datestrings for plotly.
462+
463+
Info on mpl dates: http://matplotlib.org/api/dates_api.html
464+
465+
"""
459466
try:
460-
# make sure we have a list
461-
mpl_date_list = list(mpl_dates)
462-
epoch_times = matplotlib.dates.num2epoch(mpl_date_list)
463-
date_times = [datetime.datetime.utcfromtimestamp(epoch_time)
464-
for epoch_time in epoch_times]
467+
date_times = matplotlib.dates.num2date(mpl_dates, tz=pytz.utc)
465468
time_strings = [date_time.strftime(format_string)
466469
for date_time in date_times]
467470
if len(time_strings) > 1:

plotly/plotly/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,19 @@
88
99
"""
1010
from . plotly import (
11-
sign_in, update_plot_options, get_plot_options, get_credentials, iplot,
12-
plot, iplot_mpl, plot_mpl, get_figure, Stream, image, grid_ops,
13-
meta_ops, file_ops
11+
sign_in,
12+
update_plot_options,
13+
get_plot_options,
14+
get_credentials,
15+
iplot,
16+
plot,
17+
iplot_mpl,
18+
plot_mpl,
19+
get_figure,
20+
Stream,
21+
image,
22+
grid_ops,
23+
meta_ops,
24+
file_ops,
25+
get_config
1426
)

plotly/plotly/plotly.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,44 +45,48 @@
4545

4646
_plot_options = dict()
4747

48+
_config = dict()
49+
4850
### test file permissions and make sure nothing is corrupted ###
4951
tools.ensure_local_plotly_files()
5052

5153
### _credentials stuff ###
5254

5355

54-
def sign_in(username, api_key):
55-
"""Set module-scoped _credentials for session. Verify with plotly."""
56-
global _credentials
56+
def sign_in(username, api_key, **kwargs):
57+
"""Set module-scoped _credentials for session. Optionally, set config info.
58+
"""
5759
_credentials['username'], _credentials['api_key'] = username, api_key
5860
# TODO: verify these _credentials with plotly
5961

62+
_config['plotly_domain'] = kwargs.get('plotly_domain')
63+
_config['plotly_streaming_domain'] = kwargs.get('plotly_streaming_domain')
64+
_config['plotly_api_domain'] = kwargs.get('plotly_api_domain')
65+
# TODO: verify format of config options
66+
6067

6168
### plot options stuff ###
6269

6370
def update_plot_options(**kwargs):
6471
""" Update the module-level _plot_options
6572
"""
66-
global _plot_options
6773
_plot_options.update(kwargs)
6874

6975

7076
def get_plot_options():
7177
""" Returns a copy of the user supplied plot options.
7278
Use `update_plot_options()` to change.
7379
"""
74-
global _plot_options
7580
return copy.copy(_plot_options)
7681

7782

7883
def get_credentials():
79-
""" Returns a copy of the user supplied credentials.
80-
"""
81-
global _credentials
82-
if ('username' in _credentials) and ('api_key' in _credentials):
83-
return copy.copy(_credentials)
84-
else:
85-
return tools.get_credentials_file()
84+
"""Returns the credentials that will be sent to plotly."""
85+
credentials = tools.get_credentials_file()
86+
for credentials_key in credentials:
87+
if _credentials.get(credentials_key):
88+
credentials[credentials_key] = _credentials[credentials_key]
89+
return credentials
8690

8791

8892
### plot stuff ###
@@ -128,6 +132,15 @@ def _plot_option_logic(plot_options):
128132
return options
129133

130134

135+
def get_config():
136+
"""Returns either module config or file config."""
137+
config = tools.get_config_file()
138+
for config_key in config:
139+
if _config.get(config_key):
140+
config[config_key] = _config[config_key]
141+
return config
142+
143+
131144
def plot(figure_or_data, validate=True, **plot_options):
132145
"""Create a unique url for this plot in Plotly and optionally open url.
133146
@@ -165,6 +178,12 @@ def plot(figure_or_data, validate=True, **plot_options):
165178
"plot option.\nHere's why you're "
166179
"seeing this error:\n\n{0}"
167180
"".format(err))
181+
if not figure['data']:
182+
raise exceptions.PlotlyEmptyDataError(
183+
"Empty data list found. Make sure that you populated the "
184+
"list of data objects you're sending and try again.\n"
185+
"Questions? [email protected]"
186+
)
168187
for entry in figure['data']:
169188
for key, val in list(entry.items()):
170189
try:
@@ -299,7 +318,7 @@ def get_figure(file_owner_or_url, file_id=None, raw=False):
299318
`graph objects`.
300319
301320
"""
302-
plotly_rest_url = tools.get_config_file()['plotly_domain']
321+
plotly_rest_url = get_config()['plotly_domain']
303322
if file_id is None: # assume we're using a url
304323
url = file_owner_or_url
305324
if url[:len(plotly_rest_url)] != plotly_rest_url:
@@ -405,7 +424,7 @@ def open(self):
405424
http://nbviewer.ipython.org/github/plotly/python-user-guide/blob/master/s7_streaming/s7_streaming.ipynb
406425
"""
407426

408-
streaming_url = tools.get_config_file()['plotly_streaming_domain']
427+
streaming_url = get_config()['plotly_streaming_domain']
409428
self._stream = chunked_requests.Stream(streaming_url,
410429
80,
411430
{'Host': streaming_url,
@@ -554,7 +573,7 @@ def get(figure_or_data, format='png', width=None, height=None):
554573
if height is not None:
555574
payload['height'] = height
556575

557-
url = tools.get_config_file()['plotly_domain'] + "/apigenimage/"
576+
url = get_config()['plotly_domain'] + "/apigenimage/"
558577
res = requests.post(url,
559578
data=json.dumps(payload,
560579
cls=utils._plotlyJSONEncoder),
@@ -721,7 +740,7 @@ def upload(cls, grid, filename,
721740

722741
grid.id = grid_id
723742

724-
plotly_domain = tools.get_config_file()['plotly_domain']
743+
plotly_domain = get_config()['plotly_domain']
725744
grid_url = '{}/~{}'.format(plotly_domain, grid_id.replace(':', '/'))
726745

727746
if meta is not None:
@@ -889,7 +908,7 @@ def response_handler(cls, response):
889908

890909
@classmethod
891910
def api_url(cls, resource):
892-
return ('{}/v2/{}'.format(tools.get_config_file()['plotly_api_domain'],
911+
return ('{}/v2/{}'.format(get_config()['plotly_api_domain'],
893912
resource))
894913

895914
@classmethod
@@ -937,7 +956,7 @@ def _send_to_plotly(figure, **plot_options):
937956
origin='plot',
938957
kwargs=kwargs)
939958

940-
url = tools.get_config_file()['plotly_domain'] + "/clientresp"
959+
url = get_config()['plotly_domain'] + "/clientresp"
941960

942961
r = requests.post(url, data=payload)
943962
r.raise_for_status()

plotly/tests/test_core/test_graph_objs/test_to_string.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,9 @@ def test_to_string():
3030
fig_string = fig.to_string(pretty=False)
3131
comp_string = 'Figure(\n data=Data([\n Scatter(\n x=[1, 2, 3, 4],\n y=[10, 15, 13, 17]\n ),\n Scatter(\n x=[1, 2, 3, 4],\n y=[16, 5, 11, 9]\n )\n ]),\n layout=Layout(\n autosize=False,\n width=500,\n height=500,\n margin=Margin(\n l=65,\n r=50,\n b=65,\n t=65\n )\n )\n)'
3232
assert fig_string == comp_string
33+
34+
35+
def test_nested_list():
36+
z = [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
37+
13, 14, 15, 16, 17, 18, 19, 20, 21]]
38+
print Contour(z=z).to_string()

plotly/tests/test_core/test_plotly/test_credentials.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from unittest import TestCase
12
import plotly.plotly.plotly as py
23
import plotly.tools as tls
34

@@ -24,4 +25,40 @@ def test_sign_in():
2425
assert creds['username'] == un
2526
assert creds['api_key'] == ak
2627
# TODO, and check it!
27-
# assert creds['stream_ids'] == si
28+
# assert creds['stream_ids'] == si
29+
30+
31+
class TestSignIn(TestCase):
32+
33+
def test_get_config(self):
34+
plotly_domain = 'test ___domain'
35+
plotly_streaming_domain = 'test streaming ___domain'
36+
config1 = py.get_config()
37+
py._config['plotly_domain'] = plotly_domain
38+
config2 = py.get_config()
39+
py._config['plotly_streaming_domain'] = plotly_streaming_domain
40+
config3 = py.get_config()
41+
self.assertEqual(config2['plotly_domain'], plotly_domain)
42+
self.assertNotEqual(
43+
config2['plotly_streaming_domain'], plotly_streaming_domain
44+
)
45+
self.assertEqual(
46+
config3['plotly_streaming_domain'], plotly_streaming_domain
47+
)
48+
49+
def test_sign_in_with_config(self):
50+
username = 'place holder'
51+
api_key = 'place holder'
52+
plotly_domain = 'test ___domain'
53+
plotly_streaming_domain = 'test streaming ___domain'
54+
py.sign_in(
55+
username,
56+
api_key,
57+
plotly_domain=plotly_domain,
58+
plotly_streaming_domain=plotly_streaming_domain
59+
)
60+
config = py.get_config()
61+
self.assertEqual(config['plotly_domain'], plotly_domain)
62+
self.assertEqual(
63+
config['plotly_streaming_domain'], plotly_streaming_domain
64+
)

0 commit comments

Comments
 (0)