From a236685d7a26052042aac5526a5909a8de328090 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 15 Jun 2019 18:17:38 -0400 Subject: [PATCH 01/10] Rewrite `plotly.plotly.plot` to remove use of the v1 chart studio API --- .../chart_studio/api/v1/__init__.py | 3 - .../chart_studio/api/v1/clientresp.py | 48 ---- .../chart-studio/chart_studio/api/v1/utils.py | 93 -------- .../chart_studio/plotly/plotly.py | 209 +++++++----------- .../test_plot_ly/test_api/test_v1/__init__.py | 0 .../test_api/test_v1/test_clientresp.py | 62 ------ .../test_api/test_v1/test_utils.py | 181 --------------- .../test_plot_ly/test_plotly/test_plot.py | 62 +----- 8 files changed, 89 insertions(+), 569 deletions(-) delete mode 100644 packages/python/chart-studio/chart_studio/api/v1/__init__.py delete mode 100644 packages/python/chart-studio/chart_studio/api/v1/clientresp.py delete mode 100644 packages/python/chart-studio/chart_studio/api/v1/utils.py delete mode 100644 packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/__init__.py delete mode 100644 packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/test_clientresp.py delete mode 100644 packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/test_utils.py diff --git a/packages/python/chart-studio/chart_studio/api/v1/__init__.py b/packages/python/chart-studio/chart_studio/api/v1/__init__.py deleted file mode 100644 index 05fbba4143a..00000000000 --- a/packages/python/chart-studio/chart_studio/api/v1/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import absolute_import - -from chart_studio.api.v1.clientresp import clientresp diff --git a/packages/python/chart-studio/chart_studio/api/v1/clientresp.py b/packages/python/chart-studio/chart_studio/api/v1/clientresp.py deleted file mode 100644 index 1f7707e1d97..00000000000 --- a/packages/python/chart-studio/chart_studio/api/v1/clientresp.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Interface to deprecated /clientresp API. Subject to deletion.""" -from __future__ import absolute_import - -import warnings - -import json as _json - - -from _plotly_utils.utils import PlotlyJSONEncoder -from chart_studio import config, utils -from chart_studio.api.v1.utils import request - - -def clientresp(data, **kwargs): - """ - Deprecated endpoint, still used because it can parse data out of a plot. - - When we get around to forcing users to create grids and then create plots, - we can finally get rid of this. - - :param (list) data: The data array from a figure. - - """ - from plotly import version - - creds = config.get_credentials() - cfg = config.get_config() - - dumps_kwargs = {'sort_keys': True, 'cls': PlotlyJSONEncoder} - - payload = { - 'platform': 'python', 'version': version.stable_semver(), - 'args': _json.dumps(data, **dumps_kwargs), - 'un': creds['username'], 'key': creds['api_key'], 'origin': 'plot', - 'kwargs': _json.dumps(kwargs, **dumps_kwargs) - } - - url = '{plotly_domain}/clientresp'.format(**cfg) - response = request('post', url, data=payload) - - # Old functionality, just keeping it around. - parsed_content = response.json() - if parsed_content.get('warning'): - warnings.warn(parsed_content['warning']) - if parsed_content.get('message'): - print(parsed_content['message']) - - return response diff --git a/packages/python/chart-studio/chart_studio/api/v1/utils.py b/packages/python/chart-studio/chart_studio/api/v1/utils.py deleted file mode 100644 index d0c40263a17..00000000000 --- a/packages/python/chart-studio/chart_studio/api/v1/utils.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import absolute_import - -import requests -from requests.exceptions import RequestException -from retrying import retry - -import _plotly_utils.exceptions -from chart_studio import config, exceptions -from chart_studio.api.utils import basic_auth -from chart_studio.api.v2.utils import should_retry - - -def validate_response(response): - """ - Raise a helpful PlotlyRequestError for failed requests. - - :param (requests.Response) response: A Response object from an api request. - :raises: (PlotlyRequestError) If the request failed for any reason. - :returns: (None) - - """ - content = response.content - status_code = response.status_code - try: - parsed_content = response.json() - except ValueError: - message = content if content else 'No Content' - raise exceptions.PlotlyRequestError(message, status_code, content) - - message = '' - if isinstance(parsed_content, dict): - error = parsed_content.get('error') - if error: - message = error - else: - if response.ok: - return - if not message: - message = content if content else 'No Content' - - raise exceptions.PlotlyRequestError(message, status_code, content) - - -def get_headers(): - """ - Using session credentials/config, get headers for a v1 API request. - - Users may have their own proxy layer and so we free up the `authorization` - header for this purpose (instead adding the user authorization in a new - `plotly-authorization` header). See pull #239. - - :returns: (dict) Headers to add to a requests.request call. - - """ - headers = {} - creds = config.get_credentials() - proxy_auth = basic_auth(creds['proxy_username'], creds['proxy_password']) - - if config.get_config()['plotly_proxy_authorization']: - headers['authorization'] = proxy_auth - - return headers - - -@retry(wait_exponential_multiplier=1000, wait_exponential_max=16000, - stop_max_delay=180000, retry_on_exception=should_retry) -def request(method, url, **kwargs): - """ - Central place to make any v1 api request. - - :param (str) method: The request method ('get', 'put', 'delete', ...). - :param (str) url: The full api url to make the request to. - :param kwargs: These are passed along to requests. - :return: (requests.Response) The response directly from requests. - - """ - if kwargs.get('json', None) is not None: - # See chart_studio.api.v2.utils.request for examples on how to do this. - raise _plotly_utils.exceptions.PlotlyError( - 'V1 API does not handle arbitrary json.') - kwargs['headers'] = dict(kwargs.get('headers', {}), **get_headers()) - kwargs['verify'] = config.get_config()['plotly_ssl_verification'] - try: - response = requests.request(method, url, **kwargs) - except RequestException as e: - # The message can be an exception. E.g., MaxRetryError. - message = str(getattr(e, 'message', 'No message')) - response = getattr(e, 'response', None) - status_code = response.status_code if response else None - content = response.content if response else 'No content' - raise exceptions.PlotlyRequestError(message, status_code, content) - validate_response(response) - return response diff --git a/packages/python/chart-studio/chart_studio/plotly/plotly.py b/packages/python/chart-studio/chart_studio/plotly/plotly.py index 7bfb50c2af8..3fb9e4a5a8a 100644 --- a/packages/python/chart-studio/chart_studio/plotly/plotly.py +++ b/packages/python/chart-studio/chart_studio/plotly/plotly.py @@ -34,7 +34,7 @@ from _plotly_utils.utils import PlotlyJSONEncoder from chart_studio import files, session, tools, utils, exceptions -from chart_studio.api import v1, v2 +from chart_studio.api import v2 from chart_studio.plotly import chunked_requests from chart_studio.grid_objs import Grid from chart_studio.dashboard_objs import dashboard_objs as dashboard @@ -46,17 +46,12 @@ DEFAULT_PLOT_OPTIONS = { 'filename': "plot from API", - 'fileopt': "new", 'world_readable': files.FILE_CONTENT[files.CONFIG_FILE]['world_readable'], 'auto_open': files.FILE_CONTENT[files.CONFIG_FILE]['auto_open'], 'validate': True, 'sharing': files.FILE_CONTENT[files.CONFIG_FILE]['sharing'] } -warnings.filterwarnings( - 'default', r'The fileopt parameter is deprecated .*', DeprecationWarning -) - SHARING_ERROR_MSG = ( "Whoops, sharing can only be set to either 'public', 'private', or " "'secret'." @@ -91,28 +86,12 @@ def _plot_option_logic(plot_options_from_args): session_options = session.get_session_plot_options() plot_options_from_args = copy.deepcopy(plot_options_from_args) - # fileopt deprecation warnings - fileopt_warning = ('The fileopt parameter is deprecated ' - 'and will be removed in plotly.py version 4') - if ('filename' in plot_options_from_args and - plot_options_from_args.get('fileopt', 'overwrite') != 'overwrite'): - warnings.warn(fileopt_warning, DeprecationWarning) - - if ('filename' not in plot_options_from_args and - plot_options_from_args.get('fileopt', 'new') != 'new'): - warnings.warn(fileopt_warning, DeprecationWarning) - # Validate options and fill in defaults w world_readable and sharing for option_set in [plot_options_from_args, session_options, file_options]: utils.validate_world_readable_and_sharing_settings(option_set) utils.set_sharing_and_world_readable(option_set) - # dynamic defaults - if ('filename' in option_set and - 'fileopt' not in option_set): - option_set['fileopt'] = 'overwrite' - user_plot_options = {} user_plot_options.update(default_plot_options) user_plot_options.update(file_options) @@ -192,11 +171,6 @@ def plot(figure_or_data, validate=True, **plot_options): plot_options keyword arguments: filename (string) -- the name that will be associated with this figure - fileopt ('new' | 'overwrite' | 'extend' | 'append') -- 'new' creates a - 'new': create a new, unique url for this plot - 'overwrite': overwrite the file associated with `filename` with this - 'extend': add additional numbers (data) to existing traces - 'append': add additional traces to existing data lists auto_open (default=True) -- Toggle browser options True: open this plot in a new browser tab False: do not open plot in the browser, but do return the unique url @@ -251,22 +225,83 @@ def plot(figure_or_data, validate=True, **plot_options): plot_options = _plot_option_logic(plot_options) - fig = plotly.tools._replace_newline(figure) # does not mutate figure - data = fig.get('data', []) - plot_options['layout'] = fig.get('layout', {}) - response = v1.clientresp(data, **plot_options) + # Initialize API payload + payload = { + 'figure': figure, + 'world_readable': True + } + + # Process filename + filename = plot_options.get('filename', None) + if filename: + # Strip trailing slash + if filename[-1] == '/': + filename = filename[0:-1] - # Check if the url needs a secret key - url = response.json()['url'] - if plot_options['sharing'] == 'secret': - if 'share_key=' not in url: - # add_share_key_to_url updates the url to include the share_key - url = add_share_key_to_url(url) + # split off any parent directory + paths = filename.split('/') + parent_path = '/'.join(paths[0:-1]) + filename = paths[-1] + + # Create parent directory + if parent_path != '': + file_ops.ensure_dirs(parent_path) + payload['parent_path'] = parent_path + + payload['filename'] = filename + else: + parent_path = '' + + # Process sharing + sharing = plot_options.get('sharing', None) + if sharing == 'public': + payload['world_readable'] = True + elif sharing == 'private': + payload['world_readable'] = False + elif sharing == 'secret': + payload['world_readable'] = False + payload['share_key_enabled'] = True + else: + raise _plotly_utils.exceptions.PlotlyError( + SHARING_ERROR_MSG + ) + + # Extract grid + figure, grid = _extract_grid_from_fig_like(figure) + + # Upload grid if anything was extracted + if len(grid) > 0: + if not filename: + grid_filename = None + elif parent_path: + grid_filename = parent_path + '/' + filename + '_grid' + else: + grid_filename = filename + '_grid' + + grid_ops.upload(grid=grid, + filename=grid_filename, + world_readable=payload['world_readable'], + auto_open=False) + + _set_grid_column_references(figure, grid) + payload['figure'] = figure + + file_info = _create_or_update(payload, 'plot') + + # Compute viewing URL + if sharing == 'secret': + web_url = (file_info['web_url'][:-1] + + '?share_key=' + file_info['share_key']) + else: + web_url = file_info['web_url'] - if plot_options['auto_open']: - _open_url(url) + # Handle auto_open + auto_open = plot_options.get('auto_open', None) + if auto_open: + _open_url(web_url) - return url + # Return URL + return web_url def iplot_mpl(fig, resize=True, strip_style=False, update=None, @@ -1384,24 +1419,6 @@ def add_share_key_to_url(plot_url, attempt=0): return url_share_key -def _send_to_plotly(figure, **plot_options): - import plotly.tools - fig = plotly.tools._replace_newline(figure) # does not mutate figure - data = fig.get('data', []) - response = v1.clientresp(data, **plot_options) - - parsed_content = response.json() - - # Check if the url needs a secret key - if plot_options['sharing'] == 'secret': - url = parsed_content['url'] - if 'share_key=' not in url: - # add_share_key_to_url updates the url to include the share_key - parsed_content['url'] = add_share_key_to_url(url) - - return parsed_content - - def get_grid(grid_url, raw=False): """ Returns the specified grid as a Grid instance or in JSON/dict form. @@ -1984,74 +2001,14 @@ def create_animations(figure, filename=None, sharing='public', auto_open=True): py.create_animations(figure, 'growing_circles') ``` """ - payload = { - 'figure': figure, - 'world_readable': True - } - - # set filename if specified - if filename: - # Strip trailing slash - if filename[-1] == '/': - filename = filename[0:-1] - - # split off any parent directory - paths = filename.split('/') - parent_path = '/'.join(paths[0:-1]) - filename = paths[-1] - - # Create parent directory - if parent_path != '': - file_ops.ensure_dirs(parent_path) - payload['parent_path'] = parent_path - - payload['filename'] = filename - else: - parent_path = '' - - # set sharing - if sharing == 'public': - payload['world_readable'] = True - elif sharing == 'private': - payload['world_readable'] = False - elif sharing == 'secret': - payload['world_readable'] = False - payload['share_key_enabled'] = True - else: - raise _plotly_utils.exceptions.PlotlyError( - SHARING_ERROR_MSG - ) - - # Extract grid - figure, grid = _extract_grid_from_fig_like(figure) - - if len(grid) > 0: - if not filename: - grid_filename = None - elif parent_path: - grid_filename = parent_path + '/' + filename + '_grid' - else: - grid_filename = filename + '_grid' - - grid_ops.upload(grid=grid, - filename=grid_filename, - world_readable=payload['world_readable'], - auto_open=False) - _set_grid_column_references(figure, grid) - payload['figure'] = figure - - file_info = _create_or_update(payload, 'plot') - - if sharing == 'secret': - web_url = (file_info['web_url'][:-1] + - '?share_key=' + file_info['share_key']) - else: - web_url = file_info['web_url'] - - if auto_open: - _open_url(web_url) - - return web_url + # This function is no longer needed since plot now supports figures with + # frames. Delegate to this implementation for compatibility + return plot( + figure, + filename=filename, + sharing=sharing, + auto_open=auto_open, + ) def icreate_animations(figure, filename=None, sharing='public', auto_open=False): diff --git a/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/__init__.py b/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/test_clientresp.py b/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/test_clientresp.py deleted file mode 100644 index 2ce3fe66df2..00000000000 --- a/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/test_clientresp.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import absolute_import - -from plotly import version -from chart_studio.api.v1 import clientresp -from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase - - -class Duck(object): - def to_plotly_json(self): - return 'what else floats?' - - -class ClientrespTest(PlotlyApiTestCase): - - def setUp(self): - super(ClientrespTest, self).setUp() - - # Mock the actual api call, we don't want to do network tests here. - self.request_mock = self.mock('chart_studio.api.v1.utils.requests.request') - self.request_mock.return_value = self.get_response(b'{}', 200) - - # Mock the validation function since we can test that elsewhere. - self.mock('chart_studio.api.v1.utils.validate_response') - - def test_data_only(self): - data = [{'y': [3, 5], 'name': Duck()}] - clientresp(data) - assert self.request_mock.call_count == 1 - - args, kwargs = self.request_mock.call_args - method, url = args - self.assertEqual(method, 'post') - self.assertEqual(url, '{}/clientresp'.format(self.plotly_domain)) - expected_data = ({ - 'origin': 'plot', - 'args': '[{"name": "what else floats?", "y": [3, 5]}]', - 'platform': 'python', 'version': version.stable_semver(), 'key': 'bar', - 'kwargs': '{}', 'un': 'foo' - }) - self.assertEqual(kwargs['data'], expected_data) - self.assertTrue(kwargs['verify']) - self.assertEqual(kwargs['headers'], {}) - - def test_data_and_kwargs(self): - data = [{'y': [3, 5], 'name': Duck()}] - clientresp_kwargs = {'layout': {'title': 'mah plot'}, 'filename': 'ok'} - clientresp(data, **clientresp_kwargs) - assert self.request_mock.call_count == 1 - args, kwargs = self.request_mock.call_args - method, url = args - self.assertEqual(method, 'post') - self.assertEqual(url, '{}/clientresp'.format(self.plotly_domain)) - expected_data = ({ - 'origin': 'plot', - 'args': '[{"name": "what else floats?", "y": [3, 5]}]', - 'platform': 'python', 'version': version.stable_semver(), 'key': 'bar', - 'kwargs': '{"filename": "ok", "layout": {"title": "mah plot"}}', - 'un': 'foo' - }) - self.assertEqual(kwargs['data'], expected_data) - self.assertTrue(kwargs['verify']) - self.assertEqual(kwargs['headers'], {}) diff --git a/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/test_utils.py b/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/test_utils.py deleted file mode 100644 index 8fb761de550..00000000000 --- a/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_api/test_v1/test_utils.py +++ /dev/null @@ -1,181 +0,0 @@ -from __future__ import absolute_import - -from requests import Response -import json as _json -from requests.exceptions import ConnectionError - -from chart_studio.api.utils import to_native_utf8_string -from chart_studio.api.v1 import utils -from chart_studio.exceptions import PlotlyRequestError -from _plotly_utils.exceptions import PlotlyError -from chart_studio.session import sign_in -from chart_studio.tests.test_plot_ly.test_api import PlotlyApiTestCase -from chart_studio.tests.utils import PlotlyTestCase - -import sys - -# import from mock, MagicMock -if sys.version_info.major == 3 and sys.version_info.minor >= 3: - from unittest.mock import MagicMock, patch -else: - from mock import patch, MagicMock - - -class ValidateResponseTest(PlotlyApiTestCase): - - def test_validate_ok(self): - try: - utils.validate_response(self.get_response(content=b'{}')) - except PlotlyRequestError: - self.fail('Expected this to pass!') - - def test_validate_not_ok(self): - bad_status_codes = (400, 404, 500) - for bad_status_code in bad_status_codes: - response = self.get_response(content=b'{}', - status_code=bad_status_code) - self.assertRaises(PlotlyRequestError, utils.validate_response, - response) - - def test_validate_no_content(self): - - # We shouldn't flake if the response has no content. - - response = self.get_response(content=b'', status_code=200) - try: - utils.validate_response(response) - except PlotlyRequestError as e: - self.assertEqual(e.message, 'No Content') - self.assertEqual(e.status_code, 200) - self.assertEqual(e.content, b'') - else: - self.fail('Expected this to raise!') - - def test_validate_non_json_content(self): - response = self.get_response(content=b'foobar', status_code=200) - try: - utils.validate_response(response) - except PlotlyRequestError as e: - self.assertEqual(e.message, 'foobar') - self.assertEqual(e.status_code, 200) - self.assertEqual(e.content, b'foobar') - else: - self.fail('Expected this to raise!') - - def test_validate_json_content_array(self): - content = self.to_bytes(_json.dumps([1, 2, 3])) - response = self.get_response(content=content, status_code=200) - try: - utils.validate_response(response) - except PlotlyRequestError as e: - self.assertEqual(e.message, to_native_utf8_string(content)) - self.assertEqual(e.status_code, 200) - self.assertEqual(e.content, content) - else: - self.fail('Expected this to raise!') - - def test_validate_json_content_dict_no_error(self): - content = self.to_bytes(_json.dumps({'foo': 'bar'})) - response = self.get_response(content=content, status_code=400) - try: - utils.validate_response(response) - except PlotlyRequestError as e: - self.assertEqual(e.message, to_native_utf8_string(content)) - self.assertEqual(e.status_code, 400) - self.assertEqual(e.content, content) - else: - self.fail('Expected this to raise!') - - def test_validate_json_content_dict_error_empty(self): - content = self.to_bytes(_json.dumps({'error': ''})) - response = self.get_response(content=content, status_code=200) - try: - utils.validate_response(response) - except PlotlyRequestError: - self.fail('Expected this not to raise!') - - def test_validate_json_content_dict_one_error_ok(self): - content = self.to_bytes(_json.dumps({'error': 'not ok!'})) - response = self.get_response(content=content, status_code=200) - try: - utils.validate_response(response) - except PlotlyRequestError as e: - self.assertEqual(e.message, 'not ok!') - self.assertEqual(e.status_code, 200) - self.assertEqual(e.content, content) - else: - self.fail('Expected this to raise!') - - -class GetHeadersTest(PlotlyTestCase): - - def setUp(self): - super(GetHeadersTest, self).setUp() - self.domain = 'https://foo.bar' - self.username = 'hodor' - self.api_key = 'secret' - sign_in(self.username, self.api_key, proxy_username='kleen-kanteen', - proxy_password='hydrated', plotly_proxy_authorization=False) - - def test_normal_auth(self): - headers = utils.get_headers() - expected_headers = {} - self.assertEqual(headers, expected_headers) - - def test_proxy_auth(self): - sign_in(self.username, self.api_key, plotly_proxy_authorization=True) - headers = utils.get_headers() - expected_headers = { - 'authorization': 'Basic a2xlZW4ta2FudGVlbjpoeWRyYXRlZA==' - } - self.assertEqual(headers, expected_headers) - - -class RequestTest(PlotlyTestCase): - - def setUp(self): - super(RequestTest, self).setUp() - self.domain = 'https://foo.bar' - self.username = 'hodor' - self.api_key = 'secret' - sign_in(self.username, self.api_key, proxy_username='kleen-kanteen', - proxy_password='hydrated', plotly_proxy_authorization=False) - - # Mock the actual api call, we don't want to do network tests here. - patcher = patch('chart_studio.api.v1.utils.requests.request') - self.request_mock = patcher.start() - self.addCleanup(patcher.stop) - self.request_mock.return_value = MagicMock(Response) - - # Mock the validation function since we test that elsewhere. - patcher = patch('chart_studio.api.v1.utils.validate_response') - self.validate_response_mock = patcher.start() - self.addCleanup(patcher.stop) - - self.method = 'get' - self.url = 'https://foo.bar.does.not.exist.anywhere' - - def test_request_with_json(self): - - # You can pass along non-native objects in the `json` kwarg for a - # requests.request, however, V1 packs up json objects a little - # differently, so we don't allow such requests. - - self.assertRaises(PlotlyError, utils.request, self.method, - self.url, json={}) - - def test_request_with_ConnectionError(self): - - # requests can flake out and not return a response object, we want to - # make sure we remain consistent with our errors. - - self.request_mock.side_effect = ConnectionError() - self.assertRaises(PlotlyRequestError, utils.request, self.method, - self.url) - - def test_request_validate_response(self): - - # Finally, we check details elsewhere, but make sure we do validate. - - utils.request(self.method, self.url) - assert self.validate_response_mock.call_count == 1 diff --git a/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_plotly/test_plot.py b/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_plotly/test_plot.py index 6c0bd8934c3..dd8fa3cb980 100644 --- a/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_plotly/test_plot.py +++ b/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_plotly/test_plot.py @@ -129,7 +129,6 @@ def test_plot_option_logic_only_world_readable_given(self): kwargs = {'filename': 'test', 'auto_open': True, - 'fileopt': 'overwrite', 'validate': True, 'world_readable': False} @@ -137,7 +136,6 @@ def test_plot_option_logic_only_world_readable_given(self): expected_plot_option_logic = {'filename': 'test', 'auto_open': True, - 'fileopt': 'overwrite', 'validate': True, 'world_readable': False, 'sharing': 'private'} @@ -150,7 +148,6 @@ def test_plot_option_logic_only_sharing_given(self): kwargs = {'filename': 'test', 'auto_open': True, - 'fileopt': 'overwrite', 'validate': True, 'sharing': 'private'} @@ -158,51 +155,6 @@ def test_plot_option_logic_only_sharing_given(self): expected_plot_option_logic = {'filename': 'test', 'auto_open': True, - 'fileopt': 'overwrite', - 'validate': True, - 'world_readable': False, - 'sharing': 'private'} - self.assertEqual(plot_option_logic, expected_plot_option_logic) - - def test_plot_option_fileopt_deprecations(self): - - # Make sure DeprecationWarnings aren't filtered out by nose - warnings.filterwarnings('default', category=DeprecationWarning) - - # If filename is not given and fileopt is not 'new', - # raise a deprecation warning - kwargs = {'auto_open': True, - 'fileopt': 'overwrite', - 'validate': True, - 'sharing': 'private'} - - with warnings.catch_warnings(record=True) as w: - plot_option_logic = py._plot_option_logic(kwargs) - assert w[0].category == DeprecationWarning - - expected_plot_option_logic = {'filename': 'plot from API', - 'auto_open': True, - 'fileopt': 'overwrite', - 'validate': True, - 'world_readable': False, - 'sharing': 'private'} - self.assertEqual(plot_option_logic, expected_plot_option_logic) - - # If filename is given and fileopt is not 'overwrite', - # raise a depreacation warning - kwargs = {'filename': 'test', - 'auto_open': True, - 'fileopt': 'append', - 'validate': True, - 'sharing': 'private'} - - with warnings.catch_warnings(record=True) as w: - plot_option_logic = py._plot_option_logic(kwargs) - assert w[0].category == DeprecationWarning - - expected_plot_option_logic = {'filename': 'test', - 'auto_open': True, - 'fileopt': 'append', 'validate': True, 'world_readable': False, 'sharing': 'private'} @@ -218,11 +170,10 @@ def test_plot_url_given_sharing_key(self): fig = plotly.tools.return_figure_from_figure_or_data(self.simple_figure, validate) kwargs = {'filename': 'is_share_key_included', - 'fileopt': 'overwrite', 'world_readable': False, + 'auto_open': False, 'sharing': 'secret'} - response = py._send_to_plotly(fig, **kwargs) - plot_url = response['url'] + plot_url = py.plot(fig, **kwargs) self.assertTrue('share_key=' in plot_url) @@ -233,7 +184,6 @@ def test_plot_url_response_given_sharing_key(self): # be 200 kwargs = {'filename': 'is_share_key_included', - 'fileopt': 'overwrite', 'auto_open': False, 'world_readable': False, 'sharing': 'secret'} @@ -253,12 +203,12 @@ def test_private_plot_response_with_and_without_share_key(self): # share_key is added it should be 200 kwargs = {'filename': 'is_share_key_included', - 'fileopt': 'overwrite', 'world_readable': False, + 'auto_open': False, 'sharing': 'private'} - private_plot_url = py._send_to_plotly(self.simple_figure, - **kwargs)['url'] + private_plot_url = py.plot(self.simple_figure, + **kwargs) private_plot_response = requests.get(private_plot_url + ".json") # The json file of the private plot should be 404 @@ -299,7 +249,7 @@ def test_default_options(self): options = py._plot_option_logic({}) config_options = tls.get_config_file() for key in options: - if key != 'fileopt' and key in config_options: + if key in config_options: self.assertEqual(options[key], config_options[key]) def test_conflicting_plot_options_in_plot_option_logic(self): From 03dc5aac1bccd95fe0dcaba50e9952470e0216ed Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 15 Jun 2019 18:59:10 -0400 Subject: [PATCH 02/10] Use IPython.display.IFrame to remove deprecation warning when displaying figure in the Jupyter notebook using `chart_studio.plotly.iplot` --- .../python/chart-studio/chart_studio/tools.py | 92 ++++++++++--------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/packages/python/chart-studio/chart_studio/tools.py b/packages/python/chart-studio/chart_studio/tools.py index 37acf6ec19e..1163efd55bf 100644 --- a/packages/python/chart-studio/chart_studio/tools.py +++ b/packages/python/chart-studio/chart_studio/tools.py @@ -22,6 +22,8 @@ from chart_studio.files import CONFIG_FILE, CREDENTIALS_FILE, FILE_CONTENT ipython_core_display = optional_imports.get_module('IPython.core.display') +ipython_display = optional_imports.get_module('IPython.display') + sage_salvus = optional_imports.get_module('sage_salvus') @@ -231,30 +233,7 @@ def reset_config_file(): ### embed tools ### - -def get_embed(file_owner_or_url, file_id=None, width="100%", height=525): - """Returns HTML code to embed figure on a webpage as an ").format( + return "{plotly_rest_url}/~{file_owner}/{file_id}.embed".format( plotly_rest_url=plotly_rest_url, - file_owner=file_owner, file_id=file_id, - iframe_height=height, iframe_width=width) + file_owner=file_owner, + file_id=file_id, + ) else: - s = ("").format( + return ("{plotly_rest_url}/~{file_owner}/" + "{file_id}.embed?share_key={share_key}").format( plotly_rest_url=plotly_rest_url, - file_owner=file_owner, file_id=file_id, share_key=share_key, - iframe_height=height, iframe_width=width) + file_owner=file_owner, + file_id=file_id, + share_key=share_key, + ) + - return s +def get_embed(file_owner_or_url, file_id=None, width="100%", height=525): + """Returns HTML code to embed figure on a webpage as an ").format(embed_url=embed_url, + iframe_height=height, + iframe_width=width) def embed(file_owner_or_url, file_id=None, width="100%", height=525): @@ -361,7 +367,9 @@ def embed(file_owner_or_url, file_id=None, width="100%", height=525): fid=file_id) else: url = file_owner_or_url - return PlotlyDisplay(url, width, height) + + embed_url = _get_embed_url(url, file_id) + return ipython_display.IFrame(embed_url, width, height) else: if (get_config_defaults()['plotly_domain'] != session.get_session_config()['plotly_domain']): From 1e7759d24556381faecc20214f9b9cff4a159f9f Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 16 Jun 2019 06:00:30 -0400 Subject: [PATCH 03/10] Remove fileopt argument from `chart_studio.plotly.iplot` docstring --- packages/python/chart-studio/chart_studio/plotly/plotly.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/python/chart-studio/chart_studio/plotly/plotly.py b/packages/python/chart-studio/chart_studio/plotly/plotly.py index 3fb9e4a5a8a..4ea050ae68f 100644 --- a/packages/python/chart-studio/chart_studio/plotly/plotly.py +++ b/packages/python/chart-studio/chart_studio/plotly/plotly.py @@ -108,11 +108,6 @@ def iplot(figure_or_data, **plot_options): plot_options keyword arguments: filename (string) -- the name that will be associated with this figure - fileopt ('new' | 'overwrite' | 'extend' | 'append') - - 'new': create a new, unique url for this plot - - 'overwrite': overwrite the file associated with `filename` with this - - 'extend': add additional numbers (data) to existing traces - - 'append': add additional traces to existing data lists sharing ('public' | 'private' | 'secret') -- Toggle who can view this graph - 'public': Anyone can view this graph. It will appear in your profile and can appear in search engines. You do not need to be From c56f23fbbafb5e65a8127f4612020902ae1aa71b Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 16 Jun 2019 06:10:22 -0400 Subject: [PATCH 04/10] Initial chart-studio README and CHANGELOG --- packages/python/chart-studio/CHANGELOG.md | 18 ++++++++++++++++++ packages/python/chart-studio/README.md | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/python/chart-studio/CHANGELOG.md b/packages/python/chart-studio/CHANGELOG.md index e69de29bb2d..a37cbbc8b2f 100644 --- a/packages/python/chart-studio/CHANGELOG.md +++ b/packages/python/chart-studio/CHANGELOG.md @@ -0,0 +1,18 @@ +# Change Log +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## [1.0.0] - ??? + +The initial release of the stand-alone `chart-studio` package. This package contains utilities for interfacing with Plotly's Chart Studio service (both Chart Studio cloud and Chart Studio On-Prem). Prior to plotly.py version 4, This functionality was included in the `plotly` package under the `plotly.plotly` module. As part of plotly.py version 4, the Chart Studio functionality was removed from the `plotly` package and released in this `chart-studio` package. + + +### Updated + - The `chart_studio.plotly.plot`/`iplot` functions have been ported to the Chart Studio [v2 API](https://api.plot.ly/v2/). + - The `chart_studio.plotly.plot`/`iplot` functions now support uploading figures that contain frames. This makes the legacy `chart_studio.plotly.create_animations`/`icreate_animations` functions unnecessary, though they are still included for backward compatibility. + +### Fixed + - Fixed iframe warning resulting from `chart_studio.plotly.iplot` + +### Removed + - The `fileopt` argument to `chart_studio.plotly.plot`/`iplot` was deprecated in plotly.py version 3.9.0 and has been removed in this initial release of the `chart-studio` package. diff --git a/packages/python/chart-studio/README.md b/packages/python/chart-studio/README.md index a9860520407..a24b0e42fd6 100644 --- a/packages/python/chart-studio/README.md +++ b/packages/python/chart-studio/README.md @@ -1 +1,2 @@ -Package for interfacing with the plotly's Chart Studio +# chart-studio +This package contains utilities for interfacing with Plotly's Chart Studio service (both Chart Studio cloud and Chart Studio On-Prem). Prior to plotly.py version 4, This functionality was included in the `plotly` package under the `plotly.plotly` module. As part of plotly.py version 4, the Chart Studio functionality was removed from the `plotly` package and released in this `chart-studio` package. From 7e23ae506a1d65255fb463142c11ba41e2b51877 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 16 Jun 2019 06:10:48 -0400 Subject: [PATCH 05/10] Bump chart_studio version --- packages/python/chart-studio/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python/chart-studio/setup.py b/packages/python/chart-studio/setup.py index 801221a95f8..c32570a1838 100644 --- a/packages/python/chart-studio/setup.py +++ b/packages/python/chart-studio/setup.py @@ -10,7 +10,7 @@ def readme(): setup( name="chart-studio", - version="1.0.0a1", + version="1.0.0a2", author="Chris P", author_email="chris@plot.ly", maintainer="Jon Mease", From 0e2f3445333d60c5bdaf6e5c815e812fa23b1090 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 16 Jun 2019 06:15:14 -0400 Subject: [PATCH 06/10] Remove PlotlyDisplay class. Was replaced by `IPython.display.IFrame` --- .../python/chart-studio/chart_studio/tools.py | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/python/chart-studio/chart_studio/tools.py b/packages/python/chart-studio/chart_studio/tools.py index 1163efd55bf..bdcd66e9432 100644 --- a/packages/python/chart-studio/chart_studio/tools.py +++ b/packages/python/chart-studio/chart_studio/tools.py @@ -384,24 +384,3 @@ def embed(file_owner_or_url, file_id=None, width="100%", height=525): "plot. If you just want the *embed code*,\ntry using " "`get_embed()` instead." '\nQuestions? {}'.format(feedback_contact)) - - -### graph_objs related tools ### -if ipython_core_display: - class PlotlyDisplay(ipython_core_display.HTML): - """An IPython display object for use with plotly urls - - PlotlyDisplay objects should be instantiated with a url for a plot. - IPython will *choose* the proper display representation from any - Python object, and using provided methods if they exist. By defining - the following, if an HTML display is unusable, the PlotlyDisplay - object can provide alternate representations. - - """ - def __init__(self, url, width, height): - self.resource = url - self.embed_code = get_embed(url, width=width, height=height) - super(PlotlyDisplay, self).__init__(data=self.embed_code) - - def _repr_html_(self): - return self.embed_code From 13b58fe214ccddee784836e22fb489638bb030f0 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 16 Jun 2019 06:24:02 -0400 Subject: [PATCH 07/10] Fix setup.py to remove v1 API --- packages/python/chart-studio/setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/python/chart-studio/setup.py b/packages/python/chart-studio/setup.py index c32570a1838..44b26ac4fe9 100644 --- a/packages/python/chart-studio/setup.py +++ b/packages/python/chart-studio/setup.py @@ -34,7 +34,6 @@ def readme(): packages=[ "chart_studio", "chart_studio.api", - "chart_studio.api.v1", "chart_studio.api.v2", "chart_studio.dashboard_objs", "chart_studio.grid_objs", From bb6c53c41fa2e105ee64422b4df2f39809b21060 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 16 Jun 2019 06:35:16 -0400 Subject: [PATCH 08/10] Remove deprecated GraphWidget from chart_studio package --- .../chart_studio/package_data/graphWidget.js | 159 ---- .../test_optional/test_ipython/__init__.py | 0 .../test_ipython/test_widgets.py | 14 - .../chart_studio/widgets/__init__.py | 3 - .../chart_studio/widgets/graph_widget.py | 858 ------------------ packages/python/chart-studio/setup.py | 2 - .../chart-studio/specs/GraphWidgetSpec.md | 516 ----------- 7 files changed, 1552 deletions(-) delete mode 100644 packages/python/chart-studio/chart_studio/package_data/graphWidget.js delete mode 100644 packages/python/chart-studio/chart_studio/tests/test_optional/test_ipython/__init__.py delete mode 100644 packages/python/chart-studio/chart_studio/tests/test_optional/test_ipython/test_widgets.py delete mode 100644 packages/python/chart-studio/chart_studio/widgets/__init__.py delete mode 100644 packages/python/chart-studio/chart_studio/widgets/graph_widget.py delete mode 100644 packages/python/chart-studio/specs/GraphWidgetSpec.md diff --git a/packages/python/chart-studio/chart_studio/package_data/graphWidget.js b/packages/python/chart-studio/chart_studio/package_data/graphWidget.js deleted file mode 100644 index 151ceff2445..00000000000 --- a/packages/python/chart-studio/chart_studio/package_data/graphWidget.js +++ /dev/null @@ -1,159 +0,0 @@ -window.genUID = function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); -}; - - -define('graphWidget', ["@jupyter-widgets/base"], function (widget) { - - var GraphView = widget.DOMWidgetView.extend({ - render: function(){ - var that = this; - - var graphId = window.genUID(); - var loadingId = 'loading-'+graphId; - - - var _graph_url = that.model.get('_graph_url'); - - // variable plotlyDomain in the case of enterprise - var url_parts = _graph_url.split('/'); - var plotlyDomain = url_parts[0] + '//' + url_parts[2]; - - if(!('plotlyDomains' in window)){ - window.plotlyDomains = {}; - } - window.plotlyDomains[graphId] = plotlyDomain; - - // Place IFrame in output cell div `$el` - that.$el.css('width', '100%'); - that.$graph = $([''].join(' ')); - that.$graph.appendTo(that.$el); - - that.$loading = $('
Initializing...
') - .appendTo(that.$el); - - // for some reason the 'width' is being changed in IPython 3.0.0 - // for the containing `div` element. There's a flicker here, but - // I was unable to fix it otherwise. - setTimeout(function () { - if (IPYTHON_VERSION === '3') { - $('#' + graphId)[0].parentElement.style.width = '100%'; - } - }, 500); - - // initialize communication with the iframe - if(!('pingers' in window)){ - window.pingers = {}; - } - - window.pingers[graphId] = setInterval(function() { - that.graphContentWindow = $('#'+graphId)[0].contentWindow; - that.graphContentWindow.postMessage({task: 'ping'}, plotlyDomain); - }, 200); - - // Assign a message listener to the 'message' events - // from iframe's postMessage protocol. - // Filter the messages by iframe src so that the right message - // gets passed to the right widget - if(!('messageListeners' in window)){ - window.messageListeners = {}; - } - - window.messageListeners[graphId] = function(e) { - if(_graph_url.indexOf(e.origin)>-1) { - var frame = document.getElementById(graphId); - - if(frame === null){ - // frame doesn't exist in the dom anymore, clean up it's old event listener - window.removeEventListener('message', window.messageListeners[graphId]); - clearInterval(window.pingers[graphId]); - } else if(frame.contentWindow === e.source) { - // TODO: Stop event propagation, so each frame doesn't listen and filter - var frameContentWindow = $('#'+graphId)[0].contentWindow; - var message = e.data; - - if('pong' in message && message.pong) { - $('#loading-'+graphId).hide(); - clearInterval(window.pingers[graphId]); - that.send({event: 'pong', graphId: graphId}); - } else if (message.type==='hover' || - message.type==='zoom' || - message.type==='click' || - message.type==='unhover') { - - // click and hover events contain all of the data in the traces, - // which can be a very large object and may take a ton of time - // to pass to the python backend. Strip out the data, and require - // the user to call get_figure if they need trace information - if(message.type !== 'zoom') { - for(var i in message.points) { - delete message.points[i].data; - delete message.points[i].fullData; - } - } - that.send({event: message.type, message: message, graphId: graphId}); - } else if (message.task === 'getAttributes') { - that.send({event: 'getAttributes', response: message.response}); - } - } - } - }; - - window.removeEventListener('message', window.messageListeners[graphId]); - window.addEventListener('message', window.messageListeners[graphId]); - - }, - - update: function() { - // Listen for messages from the graph widget in python - var jmessage = this.model.get('_message'); - var message = JSON.parse(jmessage); - - // check for duplicate messages - if(!('messageIds' in window)){ - window.messageIds = {}; - } - - if(!(message.uid in window.messageIds)){ - // message hasn't been received yet, do stuff - window.messageIds[message.uid] = true; - - if (message.fadeTo) { - this.fadeTo(message); - } else { - var plot = $('#' + message.graphId)[0].contentWindow; - plot.postMessage(message, window.plotlyDomains[message.graphId]); - } - } - - return GraphView.__super__.update.apply(this); - }, - - /** - * Wrapper for jquery's `fadeTo` function. - * - * @param message Contains the id we need to find the element. - */ - fadeTo: function (message) { - var plot = $('#' + message.graphId); - plot.fadeTo(message.duration, message.opacity); - } - }); - - // Register the GraphView with the widget manager. - return { - GraphView: GraphView - } - -}); - -//@ sourceURL=graphWidget.js diff --git a/packages/python/chart-studio/chart_studio/tests/test_optional/test_ipython/__init__.py b/packages/python/chart-studio/chart_studio/tests/test_optional/test_ipython/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/python/chart-studio/chart_studio/tests/test_optional/test_ipython/test_widgets.py b/packages/python/chart-studio/chart_studio/tests/test_optional/test_ipython/test_widgets.py deleted file mode 100644 index 8cd365d1e49..00000000000 --- a/packages/python/chart-studio/chart_studio/tests/test_optional/test_ipython/test_widgets.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Module for testing IPython widgets - -""" -from __future__ import absolute_import - -from unittest import TestCase - -from chart_studio.widgets import GraphWidget - -class TestWidgets(TestCase): - - def test_instantiate_graph_widget(self): - widget = GraphWidget diff --git a/packages/python/chart-studio/chart_studio/widgets/__init__.py b/packages/python/chart-studio/chart_studio/widgets/__init__.py deleted file mode 100644 index f1a63808df9..00000000000 --- a/packages/python/chart-studio/chart_studio/widgets/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import absolute_import - -from chart_studio.widgets.graph_widget import GraphWidget diff --git a/packages/python/chart-studio/chart_studio/widgets/graph_widget.py b/packages/python/chart-studio/chart_studio/widgets/graph_widget.py deleted file mode 100644 index 6be1ec2ba19..00000000000 --- a/packages/python/chart-studio/chart_studio/widgets/graph_widget.py +++ /dev/null @@ -1,858 +0,0 @@ -""" -Module to allow Plotly graphs to interact with IPython widgets. - -""" -import uuid -from collections import deque -import pkgutil - -import json as _json - -# TODO: protected imports? -import ipywidgets as widgets -from traitlets import Unicode -from IPython.display import Javascript, display - -import plotly.tools -from chart_studio import plotly as py, tools, utils -from plotly.graph_objs import Figure - -# Load JS widget code -# No officially recommended way to do this in any other way -# http://mail.scipy.org/pipermail/ipython-dev/2014-April/013835.html -js_widget_code = pkgutil.get_data('chart_studio', - 'package_data/graphWidget.js' - ).decode('utf-8') - -display(Javascript(js_widget_code)) - -__all__ = None - - -class GraphWidget(widgets.DOMWidget): - """An interactive Plotly graph widget for use in IPython - Notebooks. - """ - _view_name = Unicode('GraphView', sync=True) - _view_module = Unicode('graphWidget', sync=True) - _message = Unicode(sync=True) - _graph_url = Unicode(sync=True) - _new_url = Unicode(sync=True) - _filename = '' - _flags = { - 'save_pending': False - } - - # TODO: URL for offline enterprise - def __init__(self, graph_url='https://plot.ly/~playground/7', **kwargs): - """Initialize a plotly graph widget - - Args: - graph_url: The url of a Plotly graph - - Example: - ``` - GraphWidget('https://plot.ly/~chris/3375') - ``` - """ - super(GraphWidget, self).__init__(**kwargs) - - # TODO: Validate graph_url - self._graph_url = graph_url - self._listener_set = set() - self._event_handlers = { - 'click': widgets.CallbackDispatcher(), - 'hover': widgets.CallbackDispatcher(), - 'zoom': widgets.CallbackDispatcher() - } - - self._graphId = '' - self.on_msg(self._handle_msg) - - # messages to the iframe client need to wait for the - # iframe to communicate that it is ready - # unfortunately, this two-way blocking communication - # isn't possible - # (https://github.com/ipython/ipython/wiki/IPEP-21:-Widget-Messages#caveats) - # so we'll just cue up messages until they're ready to be sent - self._clientMessages = deque() - - @property - def url(self): - return self._new_url or '' - - def _handle_msg(self, message): - """Handle a msg from the front-end. - - Args: - content (dict): Content of the msg. - """ - content = message['content']['data']['content'] - if content.get('event', '') == 'pong': - self._graphId = content['graphId'] - - # ready to recieve - pop out all of the items in the deque - while self._clientMessages: - _message = self._clientMessages.popleft() - _message['graphId'] = self._graphId - _message = _json.dumps(_message) - self._message = _message - - if content.get('event', '') in ['click', 'hover', 'zoom']: - # De-nest the message - if content['event'] == 'click' or content['event'] == 'hover': - message = content['message']['points'] - elif content['event'] == 'zoom': - message = content['message']['ranges'] - - self._event_handlers[content['event']](self, message) - - if content.get('event', '') == 'getAttributes': - self._attributes = content.get('response', {}) - - # there might be a save pending, use the plotly module to save - if self._flags['save_pending']: - self._flags['save_pending'] = False - url = py.plot(self._attributes, auto_open=False, - filename=self._filename, validate=False) - self._new_url = url - self._fade_to('slow', 1) - - def _handle_registration(self, event_type, callback, remove): - self._event_handlers[event_type].register_callback(callback, - remove=remove) - event_callbacks = self._event_handlers[event_type].callbacks - if (len(event_callbacks) and event_type not in self._listener_set): - self._listener_set.add(event_type) - message = {'task': 'listen', 'events': list(self._listener_set)} - self._handle_outgoing_message(message) - - def _handle_outgoing_message(self, message): - if self._graphId == '': - self._clientMessages.append(message) - else: - message['graphId'] = self._graphId - message['uid'] = str(uuid.uuid4()) - self._message = _json.dumps(message, cls=utils.PlotlyJSONEncoder) - - def on_click(self, callback, remove=False): - """ Assign a callback to click events propagated - by clicking on point(s) in the Plotly graph. - - Args: - callback (function): Callback function this is called - on click events with the signature: - callback(widget, hover_obj) -> None - - Args: - widget (GraphWidget): The current instance - of the graph widget that this callback is assigned to. - - click_obj (dict): a nested dict that describes - which point(s) were clicked on. - - click_obj example: - [ - { - 'curveNumber': 1, - 'pointNumber': 2, - 'x': 4, - 'y': 14 - } - ] - - remove (bool, optional): If False, attach the callback. - If True, remove the callback. Defaults to False. - - - Returns: - None - - Example: - ``` - from IPython.display import display - def message_handler(widget, msg): - display(widget._graph_url) - display(msg) - - g = GraphWidget('https://plot.ly/~chris/3375') - display(g) - - g.on_click(message_handler) - ``` - """ - self._handle_registration('click', callback, remove) - - def on_hover(self, callback, remove=False): - """ Assign a callback to hover events propagated - by hovering over points in the Plotly graph. - - Args: - callback (function): Callback function this is called - on hover events with the signature: - callback(widget, hover_obj) -> None - - Args: - widget (GraphWidget): The current instance - of the graph widget that this callback is assigned to. - - hover_obj (dict): a nested dict that describes - which point(s) was hovered over. - - hover_obj example: - [ - { - 'curveNumber': 1, - 'pointNumber': 2, - 'x': 4, - 'y': 14 - } - ] - - remove (bool, optional): If False, attach the callback. - If True, remove the callback. Defaults to False. - - - Returns: - None - - Example: - ``` - from IPython.display import display - def message_handler(widget, hover_msg): - display(widget._graph_url) - display(hover_msg) - - g = GraphWidget('https://plot.ly/~chris/3375') - display(g) - - g.on_hover(message_handler) - ``` - - """ - self._handle_registration('hover', callback, remove) - - def on_zoom(self, callback, remove=False): - """ Assign a callback to zoom events propagated - by zooming in regions in the Plotly graph. - - Args: - callback (function): Callback function this is called - on zoom events with the signature: - callback(widget, ranges) -> None - - Args: - widget (GraphWidget): The current instance - of the graph widget that this callback is assigned to. - - ranges (dict): A description of the - region that was zoomed into. - - ranges example: - { - 'x': [1.8399058038561549, 2.16443359662], - 'y': [4.640902872777017, 7.855677154582] - } - - remove (bool, optional): If False, attach the callback. - If True, remove the callback. Defaults to False. - - Returns: - None - - Example: - ``` - from IPython.display import display - def message_handler(widget, ranges): - display(widget._graph_url) - display(ranges) - - g = GraphWidget('https://plot.ly/~chris/3375') - display(g) - - g.on_zoom(message_handler) - ``` - """ - self._handle_registration('zoom', callback, remove) - - def plot(self, figure_or_data, validate=True): - """Plot figure_or_data in the Plotly graph widget. - - Args: - figure_or_data (dict, list, or plotly.graph_obj object): - The standard Plotly graph object that describes Plotly - graphs as used in `plotly.plotly.plot`. See examples - of the figure_or_data in https://plot.ly/python/ - - Returns: None - - Example 1 - Graph a scatter plot: - ``` - from plotly.graph_objs import Scatter - g = GraphWidget() - g.plot([Scatter(x=[1, 2, 3], y=[10, 15, 13])]) - ``` - - Example 2 - Graph a scatter plot with a title: - ``` - from plotly.graph_objs import Scatter, Figure, Data - fig = Figure( - data = Data([ - Scatter(x=[1, 2, 3], y=[20, 15, 13]) - ]), - layout = Layout(title='Experimental Data') - ) - - g = GraphWidget() - g.plot(fig) - ``` - - Example 3 - Clear a graph widget - ``` - from plotly.graph_objs import Scatter, Figure - g = GraphWidget() - g.plot([Scatter(x=[1, 2, 3], y=[10, 15, 13])]) - - # Now clear it - g.plot({}) # alternatively, g.plot(Figure()) - ``` - """ - if figure_or_data == {} or figure_or_data == Figure(): - validate = False - - figure = plotly.tools.return_figure_from_figure_or_data(figure_or_data, - validate) - message = { - 'task': 'newPlot', - 'data': figure.get('data', []), - 'layout': figure.get('layout', {}), - 'graphId': self._graphId - } - self._handle_outgoing_message(message) - - def restyle(self, update, indices=None): - """Update the style of existing traces in the Plotly graph. - - Args: - update (dict): - dict where keys are the graph attribute strings - and values are the value of the graph attribute. - - To update graph objects that are nested, like - a marker's color, combine the keys with a period, - e.g. `marker.color`. To replace an entire nested object, - like `marker`, set the value to the object. - See Example 2 below. - - To update an attribute of multiple traces, set the - value to an list of values. If the list is shorter - than the number of traces, the values will wrap around. - Note: this means that for values that are naturally an array, - like `x` or `colorscale`, you need to wrap the value - in an extra array, - i.e. {'colorscale': [[[0, 'red'], [1, 'green']]]} - - You can also supply values to different traces with the - indices argument. - - See all of the graph attributes in our reference documentation - here: https://plot.ly/python/reference or by calling `help` on - graph objects in `plotly.graph_objs`. - - indices (list, optional): - Specify which traces to apply the update dict to. - Negative indices are supported. - If indices are not given, the update will apply to - *all* traces. - - Examples: - Initialization - Start each example below with this setup: - ``` - from plotly.widgets import GraphWidget - from IPython.display import display - - graph = GraphWidget() - display(graph) - ``` - - Example 1 - Set `marker.color` to red in every trace in the graph - ``` - graph.restyle({'marker.color': 'red'}) - ``` - - Example 2 - Replace `marker` with {'color': 'red'} - ``` - graph.restyle({'marker': {'color': red'}}) - ``` - - Example 3 - Set `marker.color` to red - in the first trace of the graph - ``` - graph.restyle({'marker.color': 'red'}, indices=[0]) - ``` - - Example 4 - Set `marker.color` of all of the traces to - alternating sequences of red and green - ``` - graph.restyle({'marker.color': ['red', 'green']}) - ``` - - Example 5 - Set just `marker.color` of the first two traces - to red and green - ``` - graph.restyle({'marker.color': ['red', 'green']}, indices=[0, 1]) - ``` - - Example 6 - Set multiple attributes of all of the traces - ``` - graph.restyle({ - 'marker.color': 'red', - 'line.color': 'green' - }) - ``` - - Example 7 - Update the data of the first trace - ``` - graph.restyle({ - 'x': [[1, 2, 3]], - 'y': [[10, 20, 30]], - }, indices=[0]) - ``` - - Example 8 - Update the data of the first two traces - ``` - graph.restyle({ - 'x': [[1, 2, 3], - [1, 2, 4]], - 'y': [[10, 20, 30], - [5, 8, 14]], - }, indices=[0, 1]) - ``` - """ - # TODO: Add flat traces to graph_objs - message = { - 'task': 'restyle', - 'update': update, - 'graphId': self._graphId - } - if indices: - message['indices'] = indices - self._handle_outgoing_message(message) - - def relayout(self, layout): - """Update the layout of the Plotly graph. - - Args: - layout (dict): - dict where keys are the graph attribute strings - and values are the value of the graph attribute. - - To update graph objects that are nested, like - the title of an axis, combine the keys with a period - e.g. `xaxis.title`. To set a value of an element in an array, - like an axis's range, use brackets, e.g. 'xaxis.range[0]'. - To replace an entire nested object, just specify the value to - the sub-object. See example 4 below. - - See all of the layout attributes in our reference documentation - https://plot.ly/python/reference/#Layout - Or by calling `help` on `plotly.graph_objs.Layout` - - Examples - Start each example below with this setup: - Initialization: - ``` - from plotly.widgets import GraphWidget - from IPython.display import display - - graph = GraphWidget('https://plot.ly/~chris/3979') - display(graph) - ``` - - Example 1 - Update the title - ``` - graph.relayout({'title': 'Experimental results'}) - ``` - - Example 2 - Update the xaxis range - ``` - graph.relayout({'xaxis.range': [-1, 6]}) - ``` - - Example 3 - Update the first element of the xaxis range - ``` - graph.relayout({'xaxis.range[0]': -3}) - ``` - - Example 4 - Replace the entire xaxis object - ``` - graph.relayout({'xaxis': {'title': 'Experimental results'}}) - ``` - """ - # TODO: Add flat layout to graph_objs - message = { - 'task': 'relayout', 'update': layout, 'graphId': self._graphId - } - self._handle_outgoing_message(message) - - def hover(self, *hover_objs): - """Show hover labels over the points specified in hover_obj. - - Hover labels are the labels that normally appear when the - mouse hovers over points in the plotly graph. - - Args: - hover_objs (tuple of dicts): - Specifies which points to place hover labels over. - - The location of the hover labels is described by a dict with - keys and'xval' and/or 'yval' or 'curveNumber' and 'pointNumber' - and optional keys 'hovermode' and 'subplot' - - 'xval' and 'yval' specify the (x, y) coordinates to - place the label. - 'xval' and 'yval need to be close to a point drawn in a graph. - - 'curveNumber' and 'pointNumber' specify the trace number and - the index theof the point in that trace respectively. - - 'subplot' describes which axes to the coordinates refer to. - By default, it is equal to 'xy'. For example, to specify the - second x-axis and the third y-axis, set 'subplot' to 'x2y3' - - 'hovermode' is either 'closest', 'x', or 'y'. - When set to 'x', all data sharing the same 'x' coordinate will - be shown on screen with corresponding trace labels. - When set to 'y' all data sharing the same 'y' coordinates will - be shown on the screen with corresponding trace labels. - When set to 'closest', information about the data point closest - to where the viewer is hovering will appear. - - Note: If 'hovermode' is 'x', only 'xval' needs to be set. - If 'hovermode' is 'y', only 'yval' needs to be set. - If 'hovermode' is 'closest', 'xval' and 'yval' both - need to be set. - - Note: 'hovermode' can be toggled by the user in the graph - toolbar. - - Note: It is not currently possible to apply multiple hover - labels to points on different axes. - - Note: `hover` can only be called with multiple dicts if - 'curveNumber' and 'pointNumber' are the keys of the dicts - - Examples: - Initialization - Start each example below with this setup: - ``` - from plotly.widgets import GraphWidget - from IPython.display import display - - graph = GraphWidget('https://plot.ly/~chris/3979') - display(graph) - ``` - - Example 1 - Apply a label to the (x, y) point (3, 2) - ``` - graph.hover({'xval': 3, 'yval': 2, 'hovermode': 'closest'}) - ``` - - Example 2 -Apply a labels to all the points with the x coordinate 3 - ``` - graph.hover({'xval': 3, 'hovermode': 'x'}) - ``` - - Example 3 - Apply a label to the first point of the first trace - and the second point of the second trace. - ``` - graph.hover({'curveNumber': 0, 'pointNumber': 0}, - {'curveNumber': 1, 'pointNumber': 1}) - ``` - """ - # TODO: Add to graph objects - - if len(hover_objs) == 1: - hover_objs = hover_objs[0] - - message = { - 'task': 'hover', 'selection': hover_objs, 'graphId': self._graphId - } - - self._handle_outgoing_message(message) - - def add_traces(self, traces, new_indices=None): - """ Add new data traces to a graph. - - If `new_indices` isn't specified, they are simply appended. - - Args: - traces (dict or list of dicts, or class of plotly.graph_objs):trace - new_indices (list[int]|None), optional: The final indices the - added traces should occupy in the graph. - - Examples: - Initialization - Start each example below with this setup: - ``` - from plotly.widgets import GraphWidget - from plotly.graph_objs import Scatter - from IPython.display import display - - graph = GraphWidget('https://plot.ly/~chris/3979') - display(graph) - ``` - - Example 1 - Add a scatter/line trace to the graph - ``` - graph.add_traces(Scatter(x = [1, 2, 3], y = [5, 4, 5])) - ``` - - Example 2 - Add a scatter trace and set it to to be the - second trace. This will appear as the second - item in the legend. - ``` - graph.add_traces(Scatter(x = [1, 2, 3], y = [5, 6, 5]), - new_indices=[1]) - ``` - - Example 3 - Add multiple traces to the graph - ``` - graph.add_traces([ - Scatter(x = [1, 2, 3], y = [5, 6, 5]), - Scatter(x = [1, 2.5, 3], y = [5, 8, 5]) - ]) - ``` - """ - # TODO: Validate traces with graph_objs - message = { - 'task': 'addTraces', 'traces': traces, 'graphId': self._graphId - } - if new_indices is not None: - message['newIndices'] = new_indices - self._handle_outgoing_message(message) - - def delete_traces(self, indices): - """Delete data traces from a graph. - - Args: - indices (list[int]): The indices of the traces to be removed - - Example - Delete the 2nd trace: - ``` - from plotly.widgets import GraphWidget - from IPython.display import display - - graph = GraphWidget('https://plot.ly/~chris/3979') - display(graph) - - - graph.delete_traces([1]) - ``` - - """ - message = { - 'task': 'deleteTraces', - 'indices': indices, - 'graphId': self._graphId - } - self._handle_outgoing_message(message) - - def reorder_traces(self, current_indices, new_indices=None): - """Reorder the traces in a graph. - - The order of the traces determines the order of the legend entries - and the layering of the objects drawn in the graph, i.e. the first - trace is drawn first and the second trace is drawn on top of the - first trace. - - Args: - current_indices (list[int]): The index of the traces to reorder. - - new_indices (list[int], optional): The index of the traces - specified by `current_indices` after ordering. - If None, then move the traces to the end. - - Examples: - Example 1 - Move the first trace to the second to last - position, the second trace to the last position - ``` - graph.move_traces([0, 1]) - ``` - - Example 2 - Move the first trace to the second position, - the second trace to the first position. - ``` - graph.move_traces([0], [1]) - ``` - """ - - message = { - 'task': 'moveTraces', - 'currentIndices': current_indices, - 'graphId': self._graphId - } - if new_indices is not None: - message['newIndices'] = new_indices - self._handle_outgoing_message(message) - - def save(self, ignore_defaults=False, filename=''): - """ - Save a copy of the current state of the widget in plotly. - - :param (bool) ignore_defaults: Auto-fill in unspecified figure keys? - :param (str) filename: Name of the file on plotly. - - """ - self._flags['save_pending'] = True - self._filename = filename - message = {'task': 'getAttributes', 'ignoreDefaults': ignore_defaults} - self._handle_outgoing_message(message) - self._fade_to('slow', 0.1) - - def extend_traces(self, update, indices=(0,), max_points=None): - """ Append data points to existing traces in the Plotly graph. - - Args: - update (dict): - dict where keys are the graph attribute strings - and values are arrays of arrays with values to extend. - - Each array in the array will extend a trace. - - Valid keys include: - 'x', 'y', 'text, - 'marker.color', 'marker.size', 'marker.symbol', - 'marker.line.color', 'marker.line.width' - - indices (list, int): - Specify which traces to apply the `update` dict to. - If indices are not given, the update will apply to - the traces in order. - - max_points (int or dict, optional): - If specified, then only show the `max_points` most - recent points in the graph. - This is useful to prevent traces from becoming too - large (and slow) or for creating "windowed" graphs - in monitoring applications. - - To set max_points to different values for each trace - or attribute, set max_points to a dict mapping keys - to max_points values. See the examples below. - - Examples: - Initialization - Start each example below with this setup: - ``` - from plotly.widgets import GraphWidget - from IPython.display import display - - graph = GraphWidget() - graph.plot([ - {'x': [], 'y': []}, - {'x': [], 'y': []} - ]) - - display(graph) - ``` - - Example 1 - Extend the first trace with x and y data - ``` - graph.extend_traces({'x': [[1, 2, 3]], 'y': [[10, 20, 30]]}, - indices=[0]) - ``` - - Example 2 - Extend the second trace with x and y data - ``` - graph.extend_traces({'x': [[1, 2, 3]], 'y': [[10, 20, 30]]}, - indices=[1]) - ``` - - Example 3 - Extend the first two traces with x and y data - ``` - graph.extend_traces({ - 'x': [[1, 2, 3], [2, 3, 4]], - 'y': [[10, 20, 30], [3, 4, 3]] - }, indices=[0, 1]) - ``` - - Example 4 - Extend the first trace with x and y data and - limit the length of data in that trace to 50 - points. - ``` - - graph.extend_traces({ - 'x': [range(100)], - 'y': [range(100)] - }, indices=[0, 1], max_points=50) - ``` - - Example 5 - Extend the first and second trace with x and y data - and limit the length of data in the first trace to - 25 points and the second trace to 50 points. - ``` - new_points = range(100) - graph.extend_traces({ - 'x': [new_points, new_points], - 'y': [new_points, new_points] - }, - indices=[0, 1], - max_points={ - 'x': [25, 50], - 'y': [25, 50] - } - ) - ``` - - Example 6 - Update other attributes, like marker colors and - sizes and text - ``` - # Initialize a plot with some empty attributes - graph.plot([{ - 'x': [], - 'y': [], - 'text': [], - 'marker': { - 'size': [], - 'color': [] - } - }]) - # Append some data into those attributes - graph.extend_traces({ - 'x': [[1, 2, 3]], - 'y': [[10, 20, 30]], - 'text': [['A', 'B', 'C']], - 'marker.size': [[10, 15, 20]], - 'marker.color': [['blue', 'red', 'orange']] - }, indices=[0]) - ``` - - Example 7 - Live-update a graph over a few seconds - ``` - import time - - graph.plot([{'x': [], 'y': []}]) - for i in range(10): - graph.extend_traces({ - 'x': [[i]], - 'y': [[i]] - }, indices=[0]) - - time.sleep(0.5) - ``` - - """ - message = { - 'task': 'extendTraces', - 'update': update, - 'graphId': self._graphId, - 'indices': indices - } - if max_points is not None: - message['maxPoints'] = max_points - self._handle_outgoing_message(message) - - def _fade_to(self, duration, opacity): - """ - Change the opacity to give a visual signal to users. - - """ - message = {'fadeTo': True, 'duration': duration, 'opacity': opacity} - self._handle_outgoing_message(message) diff --git a/packages/python/chart-studio/setup.py b/packages/python/chart-studio/setup.py index 44b26ac4fe9..488c1d2ee43 100644 --- a/packages/python/chart-studio/setup.py +++ b/packages/python/chart-studio/setup.py @@ -40,9 +40,7 @@ def readme(): "chart_studio.plotly", "chart_studio.plotly.chunked_requests", "chart_studio.presentation_objs", - "chart_studio.widgets", ], - package_data={'chart_studio': ['package_data/*']}, install_requires=["plotly", "requests", "retrying>=1.3.3", "six"], zip_safe=False, ) diff --git a/packages/python/chart-studio/specs/GraphWidgetSpec.md b/packages/python/chart-studio/specs/GraphWidgetSpec.md deleted file mode 100644 index 8f261994c15..00000000000 --- a/packages/python/chart-studio/specs/GraphWidgetSpec.md +++ /dev/null @@ -1,516 +0,0 @@ -### Spec - -```python -g = Graph(url) -``` - -```python -g.on_click(callback, remove=False) - Assign a callback to click events propagated - by clicking on point(s) in the Plotly graph. - - Args: - callback (function): Callback function this is called - on click events with the signature: - callback(widget, hover_obj) -> None - - Args: - widget (GraphWidget): The current instance - of the graph widget that this callback is assigned to. - - click_obj (dict): a nested dict that describes - which point(s) were clicked on. - - click_obj example: - [ - { - 'curveNumber': 1, - 'pointNumber': 2, - 'x': 4, - 'y': 14 - } - ] - - remove (bool, optional): If False, attach the callback. - If True, remove the callback. Defaults to False. - - - Returns: - None - - Example: - ``` - from IPython.display import display - def message_handler(widget, msg): - display(widget._graph_url) - display(msg) - - g = Graph('https://plot.ly/~chris/3375') - display(g) - - g.on_hover(message_handler) - ``` - -``` - -```python -g.on_hover(callback, remove=False) - Assign a callback to hover events propagated - by hovering over points in the Plotly graph. - - Args: - callback (function): Callback function this is called - on hover events with the signature: - callback(widget, hover_obj) -> None - - Args: - widget (GraphWidget): The current instance - of the graph widget that this callback is assigned to. - - hover_obj (dict): a nested dict that describes - which point(s) was hovered over that the - points belong to. - - hover_obj example: - [ - { - 'curveNumber': 1, - 'pointNumber': 2, - 'x': 4, - 'y': 14 - } - ] - - remove (bool, optional): If False, attach the callback. - If True, remove the callback. Defaults to False. - - - Returns: - None - - Example: - ``` - from IPython.display import display - def message_handler(widget, msg): - display(widget._graph_url) - display(msg) - - g = Graph('https://plot.ly/~chris/3375') - display(g) - - g.on_hover(message_handler) - ``` - -``` - -```python -g.on_zoom(callback, remove=False) - Assign a callback to zoom events propagated - by zooming in regions in the Plotly graph. - - Args: - callback (function): Callback function this is called - on zoom events with the signature: - callback(widget, zoom_obj) -> None - - Args: - widget (GraphWidget): The current instance - of the graph widget that this callback is assigned to. - - zoom_obj (dict): A description of the - region that was zoomed into. - - zoom_obj example: - { - 'x': [1.8399058038561549, 2.1644335966246384], - 'y': [4.640902872777017, 7.8556771545827635] - } - - remove (bool, optional): If False, attach the callback. - If True, remove the callback. Defaults to False. - - Returns: - None - - Example: - ``` - from IPython.display import display - def message_handler(widget, msg): - display(widget._graph_url) - display(msg) - - g = Graph('https://plot.ly/~chris/3375') - display(g) - - g.on_zoom(message_handler) - ``` - -``` - -```python -g.restyle(update, indices=None) - Update the style of existing traces in the Plotly graph. - - Args: - update (dict): - Single-nested dict where keys are the graph attribute strings - and values are the value of the graph attribute. - - To update graph objects that are nested, like - a marker's color, combine the keys with a period, - e.g. `marker.color` - - To update an attribute of multiple traces, set the - value to an list of values. If the list is shorter - than the number of traces, the values will wrap around. - Note: this means that for values that are naturally an array, like - `x` or `colorscale`, you need to wrap the value in an extra array, - i.e. {'colorscale': [[[0, 'red'], [1, 'green']]]} - - You can also supply values to different traces with the - indices argument. - - See all of the graph attributes in our reference documentation - here: https://plot.ly/python/reference or by calling `help` on - graph objects in `plotly.graph_objs`. - - indices (list, optional): - Specify which traces to apply the update dict to. - Negative indices are supported. - - Examples: - Initialization - Start each example below with this setup: - ``` - from plotly.widgets import Graph - from IPython.display import display - - graph = Graph('https://plot.ly/~chris/3979') - display(graph) - ``` - - Example 1 - Set `marker.color` to red in every trace in the graph - ``` - graph.restyle({'marker.color': 'red'}) - ``` - - Example 2 - Set `marker.color` to red in the first trace of the graph - ``` - graph.restyle({'marker.color': 'red'}, indices=[0]) - ``` - - Example 3 - Set `marker.color` of all of the traces to - alternating sequences of red and green - ``` - graph.restyle({'marker.color': ['red', 'green']}) - ``` - - Example 4 - Set just `marker.color` of the first two traces to red and green - ``` - graph.restyle({'marker.color': ['red', 'green']}, indices=[0, 1]) - ``` - - Example 5 - Set multiple attributes of all of the traces - ``` - graph.restyle({ - 'marker.color': 'red', - 'line.color': 'green' - }, indices=[0, 1]) - ``` - - Example 6 - Update the data of the first trace - ``` - graph.restyle({ - 'x': [[1, 2, 3]], - 'y': [[10, 20, 30]], - }, indices=[0]) - ``` - - Example 7 - Update the data of the first two traces - ``` - graph.restyle({ - 'x': [[1, 2, 3], - [1, 2, 4]], - 'y': [[10, 20, 30], - [5, 8, 14]], - }, indices=[0, 1]) - ``` - - Example 8 - Set the `marker.color` of the last trace to red - # TODO: This doesn't seem to work - ``` - graph.restyle({'marker.color': 'red'}, indices=[-1]) - ``` - -``` - -``` -g.relayout(layout) - Update the layout of the Plotly graph. - - Args: - layout (dict): - Single-nested dict where keys are the graph attribute strings - and values are the value of the graph attribute. - - To update graph objects that are nested, like - the title of an axis, combine the keys with a period - e.g. `xaxis.title`. To set a value of an element in an array, - like an axis's range, use brackets, e.g. 'xaxis.range[0]'. - - See all of the layout attributes in our reference documentation: - https://plot.ly/python/reference/#Layout - Or by calling `help` on `plotly.graph_objs.Layout` - - Examples - Start each example below with this setup: - Initialization: - ``` - from plotly.widgets import Graph - from IPython.display import display - - graph = Graph('https://plot.ly/~chris/3979') - display(graph) - ``` - - Example 1 - Update the title - ``` - graph.relayout({'title': 'Experimental results'}) - ``` - - Example 2 - Update the xaxis range - ``` - graph.relayout({'xaxis.range': [-1, 6]}) - ``` - - Example 3 - Update the first element of the xaxis range - ``` - graph.relayout({'xaxis.range[0]': -3}) - ``` - -``` - -``` -g.hover(*hover_objs) - Show hover labels over the points specified in hover_obj. - - Hover labels are the labels that normally appear when the - mouse hovers over points in the plotly graph. - - Args: - hover_objs (tuple of dicts): - Specifies which points to place hover labels over. - - The location of the hover labels is described by a dict with keys - 'xval' and/or 'yval' or 'curveNumber' and 'pointNumber' - and optional keys 'hovermode' and 'subplot' - - 'xval' and 'yval' specify the (x, y) coordinates to place the label(s). - 'xval' and 'yval need to be close to a point drawn in a graph. - - 'curveNumber' and 'pointNumber' specify the trace number and the index - of the point in that trace respectively. - - 'subplot' describes which axes to the coordinates above refer to. - By default, it is equal to 'xy'. For example, to specify the second - x-axis and the third y-axis, set 'subplot' to 'x2y3' - - 'hovermode' is either 'closest', 'x', or 'y'. - When set to 'x', all data sharing the same 'x' coordinate will be - shown on screen with corresponding trace labels. When set to 'y' all - data sharing the same 'y' coordinates will be shown on the screen with - corresponding trace labels. When set to 'closest', information about - the data point closest to where the viewer is hovering will appear. - - Note: If 'hovermode' is 'x', only 'xval' needs to be set. - If 'hovermode' is 'y', only 'yval' needs to be set. - If 'hovermode' is 'closest', 'xval' and 'yval' both need to be set. - - Note: 'hovermode' can be toggled by the user in the graph toolbar. - - Note: It is not currently possible to apply multiple hover labels to - points on different axes. - - Note: `hover` can only be called with multiple dicts if - 'curveNumber' and 'pointNumber' are the keys of the dicts. - - Examples: - Initialization - Start each example below with this setup: - ``` - from plotly.widgets import Graph - from IPython.display import display - - graph = Graph('https://plot.ly/~chris/3979') - display(graph) - ``` - - Example 1 - Apply a label to the (x, y) point (3, 2) - ``` - graph.hover({'xval': 3, 'yval': 2, 'hovermode': 'closest'}) - ``` - - Example 2 - Apply a labels to all the points with the x coordinate 3 - ``` - graph.hover({'xval': 3, 'hovermode': 'x'}) - ``` - - Example 3 - Apply a label to the first point of the first trace - and the second point of the second trace. - ``` - graph.hover({'curveNumber': 0, 'pointNumber': 0}, - {'curveNumber': 1, 'pointNumber': 1}) - ``` - -``` - -``` -g.add_traces(*traces, new_indices=None) - - Add new data traces to a graph. - - If `new_indices` isn't specified, they are simply appended. - - Args: - traces (dict or list of dicts, or class of plotly.graph_objs): trace - new_indices (list[int]|None), optional: The final indices the - added traces should occupy in the graph. - - Examples: - Initialization - Start each example below with this setup: - ``` - from plotly.widgets import Graph - from plotly.graph_objs import Scatter - from IPython.display import display - - graph = Graph('https://plot.ly/~chris/3979') - display(graph) - ``` - - Example 1 - Add a scatter/line trace to the graph - ``` - graph.add_traces(Scatter(x = [1, 2, 3], y = [5, 4, 5])) - ``` - - Example 2 - Add a scatter trace and set it to to be the - second trace. This will appear as the second - item in the legend. - ``` - graph.add_traces(Scatter(x = [1, 2, 3], y = [5, 6, 5]), - new_indices=[1]) - ``` - - Example 3 - Add multiple traces to the graph - ``` - graph.add_traces([ - Scatter(x = [1, 2, 3], y = [5, 6, 5]), - Scatter(x = [1, 2.5, 3], y = [5, 8, 5]) - ]) - ``` - -``` - -``` -g.delete_traces(indices) - Delete data traces from a graph. - - Args: - indices (list[int]): The indices of the traces to be removed - - Example - Delete the 2nd trace: - ``` - from plotly.widgets import Graph - from IPython.display import display - - graph = Graph('https://plot.ly/~chris/3979') - display(graph) - - - graph.delete_traces([1]) - ``` - -``` - -``` -g.move_traces(current_indices, new_indices=None) - Reorder the traces in a graph. - - The order of the traces determines the order of the legend entries - and the layering of the objects drawn in the graph, i.e. the first trace - is drawn first and the second trace is drawn on top of the first trace. - - Args: - current_indices (list[int]): The index of the traces to reorder. - - new_indices (list[int], optional): The index of the traces - specified by `current_indices` after ordering. - If None, then move the traces to the end. - - Examples: - Example 1 - Move the first trace to the second to last - position, the second trace to the last position - ``` - graph.move_traces([0, 1]) - ``` - - Example 2 - Move the first trace to the second position, - the second trace to the first position. - ``` - graph.move_traces([0], [1]) - ``` - -``` - -``` -g.get_figure(expose_defaults=False) - Return the figure object JSON described in the - current drawing of the graph. - - Note: For large figures, this call can be slow as it is passing - the object from the JavaScript client to the Python backend. - To retrieve a single attribute, use `GraphWidget.get_figure_attribute`. - - Args: - expose_defaults (bool, default False): If True, then populate the - figure object with the unspecified default values in the figure. - -``` - -``` -g.get_figure_attribute(attribute_string) - Retrieve a single value of the figure specified - by the attribute_string. - - Args: - attribute_string - - Examples: - Example 1: - ``` - graph.get_figure_attribute('layout.title') - ``` - - Example 2: - ``` - graph.get_figure_attribute('layout.xaxis.title') - ``` - - Example 3: - ``` - graph.get_figure_attribute('data[0].x') - ``` -``` - -``` -g.plot(figure_or_data) - Plot a figure or data object. - -``` - -``` -g.save(filename, **plot_options) - Send the current representation of the Plotly - graph to your Plotly account. Save the file in your - Plotly account with the given filename. - - Returns: - A url where you can view the graph - -``` From f63090bfcc7b75b620c7d00887e8b9f769aed469 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 16 Jun 2019 07:56:12 -0400 Subject: [PATCH 09/10] Fix url in testcase. v2 API includes trailing slash in web_url --- .../chart_studio/tests/test_plot_ly/test_stream/test_stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_stream/test_stream.py b/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_stream/test_stream.py index 7d7aac7670a..1e544b81185 100644 --- a/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_stream/test_stream.py +++ b/packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_stream/test_stream.py @@ -35,7 +35,7 @@ def test_initialize_stream_plot(self): auto_open=False, world_readable=True, filename='stream-test') - assert url == 'https://plot.ly/~PythonAPI/461' + self.assertEqual('https://plot.ly/~PythonAPI/461/', url) time.sleep(.5) @attr('slow') From da3a9ffed2743f7e600c8bf886169c608fe58cbf Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 16 Jun 2019 08:23:44 -0400 Subject: [PATCH 10/10] Fix JSON encoding on Python 3.5 --- packages/python/chart-studio/chart_studio/plotly/plotly.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/python/chart-studio/chart_studio/plotly/plotly.py b/packages/python/chart-studio/chart_studio/plotly/plotly.py index 4ea050ae68f..476b4081700 100644 --- a/packages/python/chart-studio/chart_studio/plotly/plotly.py +++ b/packages/python/chart-studio/chart_studio/plotly/plotly.py @@ -1459,7 +1459,12 @@ def _create_or_update(data, filetype): if filename: try: lookup_res = v2.files.lookup(filename) - matching_file = json.loads(lookup_res.content) + if isinstance(lookup_res.content, bytes): + content = lookup_res.content.decode('utf-8') + else: + content = lookup_res.content + + matching_file = json.loads(content) if matching_file['filetype'] == filetype: fid = matching_file['fid']