plotly-2.2.3+dfsg.orig/ 0000755 0001750 0001750 00000000000 13211605515 014246 5 ustar noahfx noahfx plotly-2.2.3+dfsg.orig/plotly/ 0000755 0001750 0001750 00000000000 13211605515 015571 5 ustar noahfx noahfx plotly-2.2.3+dfsg.orig/plotly/colors.py 0000644 0001750 0001750 00000052647 13207065466 017474 0 ustar noahfx noahfx """ colors ===== Functions that manipulate colors and arrays of colors. ----- There are three basic types of color types: rgb, hex and tuple: rgb - An rgb color is a string of the form 'rgb(a,b,c)' where a, b and c are integers between 0 and 255 inclusive. hex - A hex color is a string of the form '#xxxxxx' where each x is a character that belongs to the set [0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f]. This is just the set of characters used in the hexadecimal numeric system. tuple - A tuple color is a 3-tuple of the form (a,b,c) where a, b and c are floats between 0 and 1 inclusive. ----- Types of colormap: There are typically two main types of colormaps that exist: numerical and categorical colormaps. Numerical colormaps are used when a the coloring column being used takes a spectrum of values or numbers. Alternatively, a categorical colormap is used to assign a specific value in a color column to a specific color everytime it appears in the plot at hand. For instance, a column of strings in a dataframe would naturally use a categorical colormap. You can choose however to use a categorical colormap with a column of numbers. Be careful though, as if you have a large set of unique numbers in your column you'll get a lot of colors. """ from __future__ import absolute_import import decimal from numbers import Number from plotly import exceptions DEFAULT_PLOTLY_COLORS = ['rgb(31, 119, 180)', 'rgb(255, 127, 14)', 'rgb(44, 160, 44)', 'rgb(214, 39, 40)', 'rgb(148, 103, 189)', 'rgb(140, 86, 75)', 'rgb(227, 119, 194)', 'rgb(127, 127, 127)', 'rgb(188, 189, 34)', 'rgb(23, 190, 207)'] PLOTLY_SCALES = { 'Greys': [ [0, 'rgb(0,0,0)'], [1, 'rgb(255,255,255)'] ], 'YlGnBu': [ [0, 'rgb(8,29,88)'], [0.125, 'rgb(37,52,148)'], [0.25, 'rgb(34,94,168)'], [0.375, 'rgb(29,145,192)'], [0.5, 'rgb(65,182,196)'], [0.625, 'rgb(127,205,187)'], [0.75, 'rgb(199,233,180)'], [0.875, 'rgb(237,248,217)'], [1, 'rgb(255,255,217)'] ], 'Greens': [ [0, 'rgb(0,68,27)'], [0.125, 'rgb(0,109,44)'], [0.25, 'rgb(35,139,69)'], [0.375, 'rgb(65,171,93)'], [0.5, 'rgb(116,196,118)'], [0.625, 'rgb(161,217,155)'], [0.75, 'rgb(199,233,192)'], [0.875, 'rgb(229,245,224)'], [1, 'rgb(247,252,245)'] ], 'YlOrRd': [ [0, 'rgb(128,0,38)'], [0.125, 'rgb(189,0,38)'], [0.25, 'rgb(227,26,28)'], [0.375, 'rgb(252,78,42)'], [0.5, 'rgb(253,141,60)'], [0.625, 'rgb(254,178,76)'], [0.75, 'rgb(254,217,118)'], [0.875, 'rgb(255,237,160)'], [1, 'rgb(255,255,204)'] ], 'Bluered': [ [0, 'rgb(0,0,255)'], [1, 'rgb(255,0,0)'] ], # modified RdBu based on # www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf 'RdBu': [ [0, 'rgb(5,10,172)'], [0.35, 'rgb(106,137,247)'], [0.5, 'rgb(190,190,190)'], [0.6, 'rgb(220,170,132)'], [0.7, 'rgb(230,145,90)'], [1, 'rgb(178,10,28)'] ], # Scale for non-negative numeric values 'Reds': [ [0, 'rgb(220,220,220)'], [0.2, 'rgb(245,195,157)'], [0.4, 'rgb(245,160,105)'], [1, 'rgb(178,10,28)'] ], # Scale for non-positive numeric values 'Blues': [ [0, 'rgb(5,10,172)'], [0.35, 'rgb(40,60,190)'], [0.5, 'rgb(70,100,245)'], [0.6, 'rgb(90,120,245)'], [0.7, 'rgb(106,137,247)'], [1, 'rgb(220,220,220)'] ], 'Picnic': [ [0, 'rgb(0,0,255)'], [0.1, 'rgb(51,153,255)'], [0.2, 'rgb(102,204,255)'], [0.3, 'rgb(153,204,255)'], [0.4, 'rgb(204,204,255)'], [0.5, 'rgb(255,255,255)'], [0.6, 'rgb(255,204,255)'], [0.7, 'rgb(255,153,255)'], [0.8, 'rgb(255,102,204)'], [0.9, 'rgb(255,102,102)'], [1, 'rgb(255,0,0)'] ], 'Rainbow': [ [0, 'rgb(150,0,90)'], [0.125, 'rgb(0,0,200)'], [0.25, 'rgb(0,25,255)'], [0.375, 'rgb(0,152,255)'], [0.5, 'rgb(44,255,150)'], [0.625, 'rgb(151,255,0)'], [0.75, 'rgb(255,234,0)'], [0.875, 'rgb(255,111,0)'], [1, 'rgb(255,0,0)'] ], 'Portland': [ [0, 'rgb(12,51,131)'], [0.25, 'rgb(10,136,186)'], [0.5, 'rgb(242,211,56)'], [0.75, 'rgb(242,143,56)'], [1, 'rgb(217,30,30)'] ], 'Jet': [ [0, 'rgb(0,0,131)'], [0.125, 'rgb(0,60,170)'], [0.375, 'rgb(5,255,255)'], [0.625, 'rgb(255,255,0)'], [0.875, 'rgb(250,0,0)'], [1, 'rgb(128,0,0)'] ], 'Hot': [ [0, 'rgb(0,0,0)'], [0.3, 'rgb(230,0,0)'], [0.6, 'rgb(255,210,0)'], [1, 'rgb(255,255,255)'] ], 'Blackbody': [ [0, 'rgb(0,0,0)'], [0.2, 'rgb(230,0,0)'], [0.4, 'rgb(230,210,0)'], [0.7, 'rgb(255,255,255)'], [1, 'rgb(160,200,255)'] ], 'Earth': [ [0, 'rgb(0,0,130)'], [0.1, 'rgb(0,180,180)'], [0.2, 'rgb(40,210,40)'], [0.4, 'rgb(230,230,50)'], [0.6, 'rgb(120,70,20)'], [1, 'rgb(255,255,255)'] ], 'Electric': [ [0, 'rgb(0,0,0)'], [0.15, 'rgb(30,0,100)'], [0.4, 'rgb(120,0,100)'], [0.6, 'rgb(160,90,0)'], [0.8, 'rgb(230,200,0)'], [1, 'rgb(255,250,220)'] ], 'Viridis': [ [0, '#440154'], [0.06274509803921569, '#48186a'], [0.12549019607843137, '#472d7b'], [0.18823529411764706, '#424086'], [0.25098039215686274, '#3b528b'], [0.3137254901960784, '#33638d'], [0.3764705882352941, '#2c728e'], [0.4392156862745098, '#26828e'], [0.5019607843137255, '#21918c'], [0.5647058823529412, '#1fa088'], [0.6274509803921569, '#28ae80'], [0.6901960784313725, '#3fbc73'], [0.7529411764705882, '#5ec962'], [0.8156862745098039, '#84d44b'], [0.8784313725490196, '#addc30'], [0.9411764705882353, '#d8e219'], [1, '#fde725'] ] } def color_parser(colors, function): """ Takes color(s) and a function and applies the function on the color(s) In particular, this function identifies whether the given color object is an iterable or not and applies the given color-parsing function to the color or iterable of colors. If given an iterable, it will only be able to work with it if all items in the iterable are of the same type - rgb string, hex string or tuple """ if isinstance(colors, str): return function(colors) if isinstance(colors, tuple) and isinstance(colors[0], Number): return function(colors) if hasattr(colors, '__iter__'): if isinstance(colors, tuple): new_color_tuple = tuple(function(item) for item in colors) return new_color_tuple else: new_color_list = [function(item) for item in colors] return new_color_list def validate_colors(colors): """ Validates color(s) and returns an error for invalid color(s) :param (str|tuple|list) colors: either a plotly scale name, an rgb or hex color, a color tuple or a list/tuple of colors """ colors_list = [] # if colors is a single color, put into colors_list if isinstance(colors, str): if colors in PLOTLY_SCALES: return elif 'rgb' in colors or '#' in colors: colors_list.append(colors) else: raise exceptions.PlotlyError( 'If your colors variable is a string, it must be a ' 'Plotly scale, an rgb color or a hex color.' ) elif isinstance(colors, tuple): if isinstance(colors[0], Number): colors_list = [colors] else: colors_list = list(colors) if isinstance(colors, dict): colors_list.extend(colors.values()) elif isinstance(colors, list): colors_list = colors # Validate colors in colors_list for j, each_color in enumerate(colors_list): if 'rgb' in each_color: each_color = color_parser( each_color, unlabel_rgb ) for value in each_color: if value > 255.0: raise exceptions.PlotlyError( 'Whoops! The elements in your rgb colors ' 'tuples cannot exceed 255.0.' ) elif '#' in each_color: each_color = color_parser( each_color, hex_to_rgb ) elif isinstance(each_color, tuple): for value in each_color: if value > 1.0: raise exceptions.PlotlyError( 'Whoops! The elements in your colors tuples ' 'cannot exceed 1.0.' ) def convert_colors_to_same_type(colors, colortype='rgb', scale=None, return_default_colors=False, num_of_defualt_colors=2): """ Converts color(s) to the specified color type Takes a single color or an iterable of colors, as well as a list of scale values, and outputs a 2-pair of the list of color(s) converted all to an rgb or tuple color type, aswell as the scale as the second element. If colors is a Plotly Scale name, then 'scale' will be forced to the scale from the respective colorscale and the colors in that colorscale will also be coverted to the selected colortype. If colors is None, then there is an option to return portion of the DEFAULT_PLOTLY_COLORS :param (str|tuple|list) colors: either a plotly scale name, an rgb or hex color, a color tuple or a list/tuple of colors :param (list) scale: see docs for validate_scale_values() :rtype (tuple) (colors_list, scale) if scale is None in the function call, then scale will remain None in the returned tuple """ #if colors_list is None: colors_list = [] if colors is None and return_default_colors is True: colors_list = DEFAULT_PLOTLY_COLORS[0:num_of_defualt_colors] if isinstance(colors, str): if colors in PLOTLY_SCALES: colors_list = colorscale_to_colors(PLOTLY_SCALES[colors]) if scale is None: scale = colorscale_to_scale(PLOTLY_SCALES[colors]) elif 'rgb' in colors or '#' in colors: colors_list = [colors] elif isinstance(colors, tuple): if isinstance(colors[0], Number): colors_list = [colors] else: colors_list = list(colors) elif isinstance(colors, list): colors_list = colors # validate scale if scale is not None: validate_scale_values(scale) if len(colors_list) != len(scale): raise exceptions.PlotlyError( 'Make sure that the length of your scale matches the length ' 'of your list of colors which is {}.'.format(len(colors_list)) ) # convert all colors to rgb for j, each_color in enumerate(colors_list): if '#' in each_color: each_color = color_parser( each_color, hex_to_rgb ) each_color = color_parser( each_color, label_rgb ) colors_list[j] = each_color elif isinstance(each_color, tuple): each_color = color_parser( each_color, convert_to_RGB_255 ) each_color = color_parser( each_color, label_rgb ) colors_list[j] = each_color if colortype == 'rgb': return (colors_list, scale) elif colortype == 'tuple': for j, each_color in enumerate(colors_list): each_color = color_parser( each_color, unlabel_rgb ) each_color = color_parser( each_color, unconvert_from_RGB_255 ) colors_list[j] = each_color return (colors_list, scale) else: raise exceptions.PlotlyError('You must select either rgb or tuple ' 'for your colortype variable.') def convert_dict_colors_to_same_type(colors_dict, colortype='rgb'): """ Converts a colors in a dictioanry of colors to the specified color type :param (dict) colors_dict: a dictioanry whose values are single colors """ for key in colors_dict: if '#' in colors_dict[key]: colors_dict[key] = color_parser( colors_dict[key], hex_to_rgb ) colors_dict[key] = color_parser( colors_dict[key], label_rgb ) elif isinstance(colors_dict[key], tuple): colors_dict[key] = color_parser( colors_dict[key], convert_to_RGB_255 ) colors_dict[key] = color_parser( colors_dict[key], label_rgb ) if colortype == 'rgb': return colors_dict elif colortype == 'tuple': for key in colors_dict: colors_dict[key] = color_parser( colors_dict[key], unlabel_rgb ) colors_dict[key] = color_parser( colors_dict[key], unconvert_from_RGB_255 ) return colors_dict else: raise exceptions.PlotlyError('You must select either rgb or tuple ' 'for your colortype variable.') def validate_scale_values(scale): """ Validates scale values from a colorscale :param (list) scale: a strictly increasing list of floats that begins with 0 and ends with 1. Its usage derives from a colorscale which is a list of two-lists (a list with two elements) of the form [value, color] which are used to determine how interpolation weighting works between the colors in the colorscale. Therefore scale is just the extraction of these values from the two-lists in order """ if len(scale) < 2: raise exceptions.PlotlyError('You must input a list of scale values ' 'that has at least two values.') if (scale[0] != 0) or (scale[-1] != 1): raise exceptions.PlotlyError( 'The first and last number in your scale must be 0.0 and 1.0 ' 'respectively.' ) if not all(x < y for x, y in zip(scale, scale[1:])): raise exceptions.PlotlyError( "'scale' must be a list that contains a strictly increasing " "sequence of numbers." ) def make_colorscale(colors, scale=None): """ Makes a colorscale from a list of colors and a scale Takes a list of colors and scales and constructs a colorscale based on the colors in sequential order. If 'scale' is left empty, a linear- interpolated colorscale will be generated. If 'scale' is a specificed list, it must be the same legnth as colors and must contain all floats For documentation regarding to the form of the output, see https://plot.ly/python/reference/#mesh3d-colorscale :param (list) colors: a list of single colors """ colorscale = [] # validate minimum colors length of 2 if len(colors) < 2: raise exceptions.PlotlyError('You must input a list of colors that ' 'has at least two colors.') if scale is None: scale_incr = 1./(len(colors) - 1) return [[i * scale_incr, color] for i, color in enumerate(colors)] else: if len(colors) != len(scale): raise exceptions.PlotlyError('The length of colors and scale ' 'must be the same.') validate_scale_values(scale) colorscale = [list(tup) for tup in zip(scale, colors)] return colorscale def find_intermediate_color(lowcolor, highcolor, intermed, colortype='tuple'): """ Returns the color at a given distance between two colors This function takes two color tuples, where each element is between 0 and 1, along with a value 0 < intermed < 1 and returns a color that is intermed-percent from lowcolor to highcolor. If colortype is set to 'rgb', the function will automatically convert the rgb type to a tuple, find the intermediate color and return it as an rgb color. """ if colortype == 'rgb': # convert to tuple color, eg. (1, 0.45, 0.7) lowcolor = unlabel_rgb(lowcolor) highcolor = unlabel_rgb(highcolor) diff_0 = float(highcolor[0] - lowcolor[0]) diff_1 = float(highcolor[1] - lowcolor[1]) diff_2 = float(highcolor[2] - lowcolor[2]) inter_med_tuple = ( lowcolor[0] + intermed * diff_0, lowcolor[1] + intermed * diff_1, lowcolor[2] + intermed * diff_2 ) if colortype == 'rgb': # back to an rgb string, e.g. rgb(30, 20, 10) inter_med_rgb = label_rgb(inter_med_tuple) return inter_med_rgb return inter_med_tuple def unconvert_from_RGB_255(colors): """ Return a tuple where each element gets divided by 255 Takes a (list of) color tuple(s) where each element is between 0 and 255. Returns the same tuples where each tuple element is normalized to a value between 0 and 1 """ return (colors[0]/(255.0), colors[1]/(255.0), colors[2]/(255.0)) def convert_to_RGB_255(colors): """ Multiplies each element of a triplet by 255 Each coordinate of the color tuple is rounded to the nearest float and then is turned into an integer. If a number is of the form x.5, then if x is odd, the number rounds up to (x+1). Otherwise, it rounds down to just x. This is the way rounding works in Python 3 and in current statistical analysis to avoid rounding bias :param (list) rgb_components: grabs the three R, G and B values to be returned as computed in the function """ rgb_components = [] for component in colors: rounded_num = decimal.Decimal(str(component*255.0)).quantize( decimal.Decimal('1'), rounding=decimal.ROUND_HALF_EVEN ) # convert rounded number to an integer from 'Decimal' form rounded_num = int(rounded_num) rgb_components.append(rounded_num) return (rgb_components[0], rgb_components[1], rgb_components[2]) def n_colors(lowcolor, highcolor, n_colors, colortype='tuple'): """ Splits a low and high color into a list of n_colors colors in it Accepts two color tuples and returns a list of n_colors colors which form the intermediate colors between lowcolor and highcolor from linearly interpolating through RGB space. If colortype is 'rgb' the function will return a list of colors in the same form. """ if colortype == 'rgb': # convert to tuple lowcolor = unlabel_rgb(lowcolor) highcolor = unlabel_rgb(highcolor) diff_0 = float(highcolor[0] - lowcolor[0]) incr_0 = diff_0/(n_colors - 1) diff_1 = float(highcolor[1] - lowcolor[1]) incr_1 = diff_1/(n_colors - 1) diff_2 = float(highcolor[2] - lowcolor[2]) incr_2 = diff_2/(n_colors - 1) list_of_colors = [] for index in range(n_colors): new_tuple = (lowcolor[0] + (index * incr_0), lowcolor[1] + (index * incr_1), lowcolor[2] + (index * incr_2)) list_of_colors.append(new_tuple) if colortype == 'rgb': # back to an rgb string list_of_colors = color_parser(list_of_colors, label_rgb) return list_of_colors def label_rgb(colors): """ Takes tuple (a, b, c) and returns an rgb color 'rgb(a, b, c)' """ return ('rgb(%s, %s, %s)' % (colors[0], colors[1], colors[2])) def unlabel_rgb(colors): """ Takes rgb color(s) 'rgb(a, b, c)' and returns tuple(s) (a, b, c) This function takes either an 'rgb(a, b, c)' color or a list of such colors and returns the color tuples in tuple(s) (a, b, c) """ str_vals = '' for index in range(len(colors)): try: float(colors[index]) str_vals = str_vals + colors[index] except ValueError: if colors[index] == ',' or colors[index] == '.': str_vals = str_vals + colors[index] str_vals = str_vals + ',' numbers = [] str_num = '' for char in str_vals: if char != ',': str_num = str_num + char else: numbers.append(float(str_num)) str_num = '' return (numbers[0], numbers[1], numbers[2]) def hex_to_rgb(value): """ Calculates rgb values from a hex color code. :param (string) value: Hex color string :rtype (tuple) (r_value, g_value, b_value): tuple of rgb values """ value = value.lstrip('#') hex_total_length = len(value) rgb_section_length = hex_total_length // 3 return tuple(int(value[i:i + rgb_section_length], 16) for i in range(0, hex_total_length, rgb_section_length)) def colorscale_to_colors(colorscale): """ Extracts the colors from colorscale as a list """ color_list = [] for item in colorscale: color_list.append(item[1]) return color_list def colorscale_to_scale(colorscale): """ Extracts the interpolation scale values from colorscale as a list """ scale_list = [] for item in colorscale: scale_list.append(item[0]) return scale_list def convert_colorscale_to_rgb(colorscale): """ Converts the colors in a colorscale to rgb colors A colorscale is an array of arrays, each with a numeric value as the first item and a color as the second. This function specifically is converting a colorscale with tuple colors (each coordinate between 0 and 1) into a colorscale with the colors transformed into rgb colors """ for color in colorscale: color[1] = convert_to_RGB_255(color[1]) for color in colorscale: color[1] = label_rgb(color[1]) return colorscale plotly-2.2.3+dfsg.orig/plotly/utils.py 0000644 0001750 0001750 00000036055 13104403274 017313 0 ustar noahfx noahfx """ utils ===== Low-level functionality NOT intended for users to EVER use. """ from __future__ import absolute_import import os.path import re import sys import threading import decimal from collections import deque import pytz from decorator import decorator from requests.compat import json as _json from plotly.optional_imports import get_module from . exceptions import PlotlyError # Optional imports, may be None for users that only use our core functionality. numpy = get_module('numpy') pandas = get_module('pandas') sage_all = get_module('sage.all') ### incase people are using threading, we lock file reads lock = threading.Lock() ### general file setup tools ### def load_json_dict(filename, *args): """Checks if file exists. Returns {} if something fails.""" data = {} if os.path.exists(filename): lock.acquire() with open(filename, "r") as f: try: data = _json.load(f) if not isinstance(data, dict): data = {} except: data = {} # TODO: issue a warning and bubble it up lock.release() if args: return {key: data[key] for key in args if key in data} return data def save_json_dict(filename, json_dict): """Save json to file. Error if path DNE, not a dict, or invalid json.""" if isinstance(json_dict, dict): # this will raise a TypeError if something goes wrong json_string = _json.dumps(json_dict, indent=4) lock.acquire() with open(filename, "w") as f: f.write(json_string) lock.release() else: raise TypeError("json_dict was not a dictionary. not saving.") def ensure_file_exists(filename): """Given a valid filename, make sure it exists (will create if DNE).""" if not os.path.exists(filename): head, tail = os.path.split(filename) ensure_dir_exists(head) with open(filename, 'w') as f: pass # just create the file def ensure_dir_exists(directory): """Given a valid directory path, make sure it exists.""" if dir: if not os.path.isdir(directory): os.makedirs(directory) def iso_to_plotly_time_string(iso_string): """Remove timezone info and replace 'T' delimeter with ' ' (ws).""" # make sure we don't send timezone info to plotly if (iso_string.split('-')[:3] is '00:00') or\ (iso_string.split('+')[0] is '00:00'): raise Exception("Plotly won't accept timestrings with timezone info.\n" "All timestrings are assumed to be in UTC.") iso_string = iso_string.replace('-00:00', '').replace('+00:00', '') if iso_string.endswith('T00:00:00'): return iso_string.replace('T00:00:00', '') else: return iso_string.replace('T', ' ') ### Custom JSON encoders ### class NotEncodable(Exception): pass class PlotlyJSONEncoder(_json.JSONEncoder): """ Meant to be passed as the `cls` kwarg to json.dumps(obj, cls=..) See PlotlyJSONEncoder.default for more implementation information. Additionally, this encoder overrides nan functionality so that 'Inf', 'NaN' and '-Inf' encode to 'null'. Which is stricter JSON than the Python version. """ def coerce_to_strict(self, const): """ This is used to ultimately *encode* into strict JSON, see `encode` """ # before python 2.7, 'true', 'false', 'null', were include here. if const in ('Infinity', '-Infinity', 'NaN'): return None else: return const def encode(self, o): """ Load and then dump the result using parse_constant kwarg Note that setting invalid separators will cause a failure at this step. """ # this will raise errors in a normal-expected way encoded_o = super(PlotlyJSONEncoder, self).encode(o) # now: # 1. `loads` to switch Infinity, -Infinity, NaN to None # 2. `dumps` again so you get 'null' instead of extended JSON try: new_o = _json.loads(encoded_o, parse_constant=self.coerce_to_strict) except ValueError: # invalid separators will fail here. raise a helpful exception raise ValueError( "Encoding into strict JSON failed. Did you set the separators " "valid JSON separators?" ) else: return _json.dumps(new_o, sort_keys=self.sort_keys, indent=self.indent, separators=(self.item_separator, self.key_separator)) def default(self, obj): """ Accept an object (of unknown type) and try to encode with priority: 1. builtin: user-defined objects 2. sage: sage math cloud 3. pandas: dataframes/series 4. numpy: ndarrays 5. datetime: time/datetime objects Each method throws a NotEncoded exception if it fails. The default method will only get hit if the object is not a type that is naturally encoded by json: Normal objects: dict object list, tuple array str, unicode string int, long, float number True true False false None null Extended objects: float('nan') 'NaN' float('infinity') 'Infinity' float('-infinity') '-Infinity' Therefore, we only anticipate either unknown iterables or values here. """ # TODO: The ordering if these methods is *very* important. Is this OK? encoding_methods = ( self.encode_as_plotly, self.encode_as_sage, self.encode_as_numpy, self.encode_as_pandas, self.encode_as_datetime, self.encode_as_date, self.encode_as_list, # because some values have `tolist` do last. self.encode_as_decimal ) for encoding_method in encoding_methods: try: return encoding_method(obj) except NotEncodable: pass return _json.JSONEncoder.default(self, obj) @staticmethod def encode_as_plotly(obj): """Attempt to use a builtin `to_plotly_json` method.""" try: return obj.to_plotly_json() except AttributeError: raise NotEncodable @staticmethod def encode_as_list(obj): """Attempt to use `tolist` method to convert to normal Python list.""" if hasattr(obj, 'tolist'): return obj.tolist() else: raise NotEncodable @staticmethod def encode_as_sage(obj): """Attempt to convert sage.all.RR to floats and sage.all.ZZ to ints""" if not sage_all: raise NotEncodable if obj in sage_all.RR: return float(obj) elif obj in sage_all.ZZ: return int(obj) else: raise NotEncodable @staticmethod def encode_as_pandas(obj): """Attempt to convert pandas.NaT""" if not pandas: raise NotEncodable if obj is pandas.NaT: return None else: raise NotEncodable @staticmethod def encode_as_numpy(obj): """Attempt to convert numpy.ma.core.masked""" if not numpy: raise NotEncodable if obj is numpy.ma.core.masked: return float('nan') else: raise NotEncodable @staticmethod def encode_as_datetime(obj): """Attempt to convert to utc-iso time string using datetime methods.""" # first we need to get this into utc try: obj = obj.astimezone(pytz.utc) except ValueError: # we'll get a value error if trying to convert with naive datetime pass except TypeError: # pandas throws a typeerror here instead of a value error, it's OK pass except AttributeError: # we'll get an attribute error if astimezone DNE raise NotEncodable # now we need to get a nicely formatted time string try: time_string = obj.isoformat() except AttributeError: raise NotEncodable else: return iso_to_plotly_time_string(time_string) @staticmethod def encode_as_date(obj): """Attempt to convert to utc-iso time string using date methods.""" try: time_string = obj.isoformat() except AttributeError: raise NotEncodable else: return iso_to_plotly_time_string(time_string) @staticmethod def encode_as_decimal(obj): """Attempt to encode decimal by converting it to float""" if isinstance(obj, decimal.Decimal): return float(obj) else: raise NotEncodable ### unicode stuff ### def decode_unicode(coll): if isinstance(coll, list): for no, entry in enumerate(coll): if isinstance(entry, (dict, list)): coll[no] = decode_unicode(entry) else: if isinstance(entry, str): try: coll[no] = str(entry) except UnicodeEncodeError: pass elif isinstance(coll, dict): keys, vals = list(coll.keys()), list(coll.values()) for key, val in zip(keys, vals): if isinstance(val, (dict, list)): coll[key] = decode_unicode(val) elif isinstance(val, str): try: coll[key] = str(val) except UnicodeEncodeError: pass coll[str(key)] = coll.pop(key) return coll ### docstring templating ### def template_doc(**names): def _decorator(func): if sys.version[:3] != '3.2': if func.__doc__ is not None: func.__doc__ = func.__doc__.format(**names) return func return _decorator def get_first_duplicate(items): seen = set() for item in items: if item not in seen: seen.add(item) else: return item return None ### source key def is_source_key(key): src_regex = re.compile(r'.+src$') if src_regex.match(key) is not None: return True else: return False def node_generator(node, path=()): """ General, node-yielding generator. Yields (node, path) tuples when it finds values that are dict instances. A path is a sequence of hashable values that can be used as either keys to a mapping (dict) or indices to a sequence (list). A path is always wrt to some object. Given an object, a path explains how to get from the top level of that object to a nested value in the object. :param (dict) node: Part of a dict to be traversed. :param (tuple[str]) path: Defines the path of the current node. :return: (Generator) Example: >>> for node, path in node_generator({'a': {'b': 5}}): >>> print node, path {'a': {'b': 5}} () {'b': 5} ('a', ) """ if not isinstance(node, dict): return # in case it's called with a non-dict node at top level yield node, path for key, val in node.items(): if isinstance(val, dict): for item in node_generator(val, path + (key, )): yield item def get_by_path(obj, path): """ Iteratively get on obj for each key in path. :param (list|dict) obj: The top-level object. :param (tuple[str]|tuple[int]) path: Keys to access parts of obj. :return: (*) Example: >>> figure = {'data': [{'x': [5]}]} >>> path = ('data', 0, 'x') >>> get_by_path(figure, path) # [5] """ for key in path: obj = obj[key] return obj ### validation def validate_world_readable_and_sharing_settings(option_set): if ('world_readable' in option_set and option_set['world_readable'] is True and 'sharing' in option_set and option_set['sharing'] is not None and option_set['sharing'] != 'public'): raise PlotlyError( "Looks like you are setting your plot privacy to both " "public and private.\n If you set world_readable as True, " "sharing can only be set to 'public'") elif ('world_readable' in option_set and option_set['world_readable'] is False and 'sharing' in option_set and option_set['sharing'] == 'public'): raise PlotlyError( "Looks like you are setting your plot privacy to both " "public and private.\n If you set world_readable as " "False, sharing can only be set to 'private' or 'secret'") elif ('sharing' in option_set and option_set['sharing'] not in ['public', 'private', 'secret', None]): raise PlotlyError( "The 'sharing' argument only accepts one of the following " "strings:\n'public' -- for public plots\n" "'private' -- for private plots\n" "'secret' -- for private plots that can be shared with a " "secret url" ) def set_sharing_and_world_readable(option_set): if 'world_readable' in option_set and 'sharing' not in option_set: option_set['sharing'] = ( 'public' if option_set['world_readable'] else 'private') elif 'sharing' in option_set and 'world_readable' not in option_set: if option_set['sharing'] == 'public': option_set['world_readable'] = True else: option_set['world_readable'] = False def _default_memoize_key_function(*args, **kwargs): """Factored out in case we want to allow callers to specify this func.""" if kwargs: # frozenset is used to ensure hashability return args, frozenset(kwargs.items()) else: return args def memoize(maxsize=128): """ Memoize a function by its arguments. Note, if the wrapped function returns a mutable result, the caller is responsible for *not* mutating the result as it will mutate the cache itself. :param (int|None) maxsize: Limit the number of cached results. This is a simple way to prevent memory leaks. Setting this to `None` will remember *all* calls. The 128 number is used for parity with the Python 3.2 `functools.lru_cache` tool. """ keys = deque() cache = {} def _memoize(*all_args, **kwargs): func = all_args[0] args = all_args[1:] key = _default_memoize_key_function(*args, **kwargs) if key in keys: return cache[key] if maxsize is not None and len(keys) == maxsize: cache.pop(keys.pop()) result = func(*args, **kwargs) keys.appendleft(key) cache[key] = result return result return decorator(_memoize) plotly-2.2.3+dfsg.orig/plotly/exceptions.py 0000644 0001750 0001750 00000012640 13136102462 020326 0 ustar noahfx noahfx """ exceptions ========== A module that contains plotly's exception hierarchy. """ from __future__ import absolute_import from plotly.api.utils import to_native_utf8_string # Base Plotly Error class PlotlyError(Exception): pass class InputError(PlotlyError): pass class PlotlyRequestError(PlotlyError): """General API error. Raised for *all* failed requests.""" def __init__(self, message, status_code, content): self.message = to_native_utf8_string(message) self.status_code = status_code self.content = content def __str__(self): return self.message # Grid Errors COLUMN_NOT_YET_UPLOADED_MESSAGE = ( "Hm... it looks like your column '{column_name}' hasn't " "been uploaded to Plotly yet. You need to upload your " "column to Plotly before you can assign it to '{reference}'.\n" "To upload, try `plotly.plotly.grid_objs.upload` or " "`plotly.plotly.grid_objs.append_column`.\n" "Questions? chris@plot.ly" ) NON_UNIQUE_COLUMN_MESSAGE = ( "Yikes, plotly grids currently " "can't have duplicate column names. Rename " "the column \"{0}\" and try again." ) class PlotlyEmptyDataError(PlotlyError): pass # Graph Objects Errors class PlotlyGraphObjectError(PlotlyError): def __init__(self, message='', path=(), notes=()): """ General graph object error for validation failures. :param (str|unicode) message: The error message. :param (iterable) path: A path pointing to the error. :param notes: Add additional notes, but keep default exception message. """ self.message = message self.plain_message = message # for backwards compat self.path = list(path) self.notes = notes super(PlotlyGraphObjectError, self).__init__(message) def __str__(self): """This is called by Python to present the error message.""" format_dict = { 'message': self.message, 'path': '[' + ']['.join(repr(k) for k in self.path) + ']', 'notes': '\n'.join(self.notes) } return ('{message}\n\nPath To Error: {path}\n\n{notes}' .format(**format_dict)) class PlotlyDictKeyError(PlotlyGraphObjectError): def __init__(self, obj, path, notes=()): """See PlotlyGraphObjectError.__init__ for param docs.""" format_dict = {'attribute': path[-1], 'object_name': obj._name} message = ("'{attribute}' is not allowed in '{object_name}'" .format(**format_dict)) notes = [obj.help(return_help=True)] + list(notes) super(PlotlyDictKeyError, self).__init__( message=message, path=path, notes=notes ) class PlotlyDictValueError(PlotlyGraphObjectError): def __init__(self, obj, path, notes=()): """See PlotlyGraphObjectError.__init__ for param docs.""" format_dict = {'attribute': path[-1], 'object_name': obj._name} message = ("'{attribute}' has invalid value inside '{object_name}'" .format(**format_dict)) notes = [obj.help(path[-1], return_help=True)] + list(notes) super(PlotlyDictValueError, self).__init__( message=message, notes=notes, path=path ) class PlotlyListEntryError(PlotlyGraphObjectError): def __init__(self, obj, path, notes=()): """See PlotlyGraphObjectError.__init__ for param docs.""" format_dict = {'index': path[-1], 'object_name': obj._name} message = ("Invalid entry found in '{object_name}' at index, '{index}'" .format(**format_dict)) notes = [obj.help(return_help=True)] + list(notes) super(PlotlyListEntryError, self).__init__( message=message, path=path, notes=notes ) class PlotlyDataTypeError(PlotlyGraphObjectError): def __init__(self, obj, path, notes=()): """See PlotlyGraphObjectError.__init__ for param docs.""" format_dict = {'index': path[-1], 'object_name': obj._name} message = ("Invalid entry found in '{object_name}' at index, '{index}'" .format(**format_dict)) note = "It's invalid because it doesn't contain a valid 'type' value." notes = [note] + list(notes) super(PlotlyDataTypeError, self).__init__( message=message, path=path, notes=notes ) # Local Config Errors class PlotlyLocalError(PlotlyError): pass class PlotlyLocalCredentialsError(PlotlyLocalError): def __init__(self): message = ( "\n" "Couldn't find a 'username', 'api-key' pair for you on your local " "machine. To sign in temporarily (until you stop running Python), " "run:\n" ">>> import plotly.plotly as py\n" ">>> py.sign_in('username', 'api_key')\n\n" "Even better, save your credentials permanently using the 'tools' " "module:\n" ">>> import plotly.tools as tls\n" ">>> tls.set_credentials_file(username='username', " "api_key='api-key')\n\n" "For more help, see https://plot.ly/python.\n" ) super(PlotlyLocalCredentialsError, self).__init__(message) # Server Errors class PlotlyServerError(PlotlyError): pass class PlotlyConnectionError(PlotlyServerError): pass class PlotlyCredentialError(PlotlyServerError): pass class PlotlyAccountError(PlotlyServerError): pass class PlotlyRateLimitError(PlotlyServerError): pass plotly-2.2.3+dfsg.orig/plotly/plotly/ 0000755 0001750 0001750 00000000000 13211605515 017114 5 ustar noahfx noahfx plotly-2.2.3+dfsg.orig/plotly/plotly/plotly.py 0000644 0001750 0001750 00000203533 13174436015 021024 0 ustar noahfx noahfx """ plotly ====== A module that contains the plotly class, a liaison between the user and ploty's servers. 1. get DEFAULT_PLOT_OPTIONS for options 2. update plot_options with .plotly/ dir 3. update plot_options with _plot_options 4. update plot_options with kwargs! """ from __future__ import absolute_import import copy import json import os import time import warnings import webbrowser import six import six.moves from requests.compat import json as _json from plotly import exceptions, files, session, tools, utils from plotly.api import v1, v2 from plotly.plotly import chunked_requests from plotly.grid_objs import Grid, Column from plotly.dashboard_objs import dashboard_objs as dashboard # This is imported like this for backwards compat. Careful if changing. from plotly.config import get_config, get_credentials __all__ = None 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'] } SHARING_ERROR_MSG = ( "Whoops, sharing can only be set to either 'public', 'private', or " "'secret'." ) # test file permissions and make sure nothing is corrupted tools.ensure_local_plotly_files() # don't break backwards compatibility def sign_in(username, api_key, **kwargs): session.sign_in(username, api_key, **kwargs) try: # The only way this can succeed is if the user can be authenticated # with the given, username, api_key, and plotly_api_domain. v2.users.current() except exceptions.PlotlyRequestError: raise exceptions.PlotlyError('Sign in failed.') update_plot_options = session.update_session_plot_options def _plot_option_logic(plot_options_from_call_signature): """ Given some plot_options as part of a plot call, decide on final options. Precedence: 1 - Start with DEFAULT_PLOT_OPTIONS 2 - Update each key with ~/.plotly/.config options (tls.get_config) 3 - Update each key with session plot options (set by py.sign_in) 4 - Update each key with plot, iplot call signature options """ default_plot_options = copy.deepcopy(DEFAULT_PLOT_OPTIONS) file_options = tools.get_config_file() session_options = session.get_session_plot_options() plot_options_from_call_signature = copy.deepcopy(plot_options_from_call_signature) # Validate options and fill in defaults w world_readable and sharing for option_set in [plot_options_from_call_signature, 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) user_plot_options.update(session_options) user_plot_options.update(plot_options_from_call_signature) user_plot_options = {k: v for k, v in user_plot_options.items() if k in default_plot_options} return user_plot_options def iplot(figure_or_data, **plot_options): """Create a unique url for this plot in Plotly and open in IPython. plot_options keyword agruments: 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 logged in to Plotly to view this chart. - 'private': Only you can view this plot. It will not appear in the Plotly feed, your profile, or search engines. You must be logged in to Plotly to view this graph. You can privately share this graph with other Plotly users in your online Plotly account and they will need to be logged in to view this plot. - 'secret': Anyone with this secret link can view this chart. It will not appear in the Plotly feed, your profile, or search engines. If it is embedded inside a webpage or an IPython notebook, anybody who is viewing that page will be able to view the graph. You do not need to be logged in to view this plot. world_readable (default=True) -- Deprecated: use "sharing". Make this figure private/public """ if 'auto_open' not in plot_options: plot_options['auto_open'] = False url = plot(figure_or_data, **plot_options) if isinstance(figure_or_data, dict): layout = figure_or_data.get('layout', {}) else: layout = {} embed_options = dict() embed_options['width'] = layout.get('width', '100%') embed_options['height'] = layout.get('height', 525) try: float(embed_options['width']) except (ValueError, TypeError): pass else: embed_options['width'] = str(embed_options['width']) + 'px' try: float(embed_options['height']) except (ValueError, TypeError): pass else: embed_options['height'] = str(embed_options['height']) + 'px' return tools.embed(url, **embed_options) def plot(figure_or_data, validate=True, **plot_options): """Create a unique url for this plot in Plotly and optionally open url. plot_options keyword agruments: 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 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 logged in to Plotly to view this chart. - 'private': Only you can view this plot. It will not appear in the Plotly feed, your profile, or search engines. You must be logged in to Plotly to view this graph. You can privately share this graph with other Plotly users in your online Plotly account and they will need to be logged in to view this plot. - 'secret': Anyone with this secret link can view this chart. It will not appear in the Plotly feed, your profile, or search engines. If it is embedded inside a webpage or an IPython notebook, anybody who is viewing that page will be able to view the graph. You do not need to be logged in to view this plot. world_readable (default=True) -- Deprecated: use "sharing". Make this figure private/public """ figure = tools.return_figure_from_figure_or_data(figure_or_data, validate) for entry in figure['data']: if ('type' in entry) and (entry['type'] == 'scattergl'): continue for key, val in list(entry.items()): try: if len(val) > 40000: msg = ("Woah there! Look at all those points! Due to " "browser limitations, the Plotly SVG drawing " "functions have a hard time " "graphing more than 500k data points for line " "charts, or 40k points for other types of charts. " "Here are some suggestions:\n" "(1) Use the `plotly.graph_objs.Scattergl` " "trace object to generate a WebGl graph.\n" "(2) Trying using the image API to return an image " "instead of a graph URL\n" "(3) Use matplotlib\n" "(4) See if you can create your visualization with " "fewer data points\n\n" "If the visualization you're using aggregates " "points (e.g., box plot, histogram, etc.) you can " "disregard this warning.") warnings.warn(msg) except TypeError: pass plot_options = _plot_option_logic(plot_options) fig = tools._replace_newline(figure) # does not mutate figure data = fig.get('data', []) plot_options['layout'] = fig.get('layout', {}) response = v1.clientresp(data, **plot_options) # 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) if plot_options['auto_open']: _open_url(url) return url def iplot_mpl(fig, resize=True, strip_style=False, update=None, **plot_options): """Replot a matplotlib figure with plotly in IPython. This function: 1. converts the mpl figure into JSON (run help(plolty.tools.mpl_to_plotly)) 2. makes a request to Plotly to save this figure in your account 3. displays the image in your IPython output cell Positional agruments: fig -- a figure object from matplotlib Keyword arguments: resize (default=True) -- allow plotly to choose the figure size strip_style (default=False) -- allow plotly to choose style options update (default=None) -- update the resulting figure with an 'update' dictionary-like object resembling a plotly 'Figure' object Additional keyword arguments: plot_options -- run help(plotly.plotly.iplot) """ fig = tools.mpl_to_plotly(fig, resize=resize, strip_style=strip_style) if update and isinstance(update, dict): fig.update(update) fig.validate() elif update is not None: raise exceptions.PlotlyGraphObjectError( "'update' must be dictionary-like and a valid plotly Figure " "object. Run 'help(plotly.graph_objs.Figure)' for more info." ) return iplot(fig, **plot_options) def plot_mpl(fig, resize=True, strip_style=False, update=None, **plot_options): """Replot a matplotlib figure with plotly. This function: 1. converts the mpl figure into JSON (run help(plolty.tools.mpl_to_plotly)) 2. makes a request to Plotly to save this figure in your account 3. opens your figure in a browser tab OR returns the unique figure url Positional agruments: fig -- a figure object from matplotlib Keyword arguments: resize (default=True) -- allow plotly to choose the figure size strip_style (default=False) -- allow plotly to choose style options update (default=None) -- update the resulting figure with an 'update' dictionary-like object resembling a plotly 'Figure' object Additional keyword arguments: plot_options -- run help(plotly.plotly.plot) """ fig = tools.mpl_to_plotly(fig, resize=resize, strip_style=strip_style) if update and isinstance(update, dict): fig.update(update) fig.validate() elif update is not None: raise exceptions.PlotlyGraphObjectError( "'update' must be dictionary-like and a valid plotly Figure " "object. Run 'help(plotly.graph_objs.Figure)' for more info." ) return plot(fig, **plot_options) def _swap_keys(obj, key1, key2): """Swap obj[key1] with obj[key2]""" val1, val2 = None, None try: val2 = obj.pop(key1) except KeyError: pass try: val1 = obj.pop(key2) except KeyError: pass if val2 is not None: obj[key2] = val2 if val1 is not None: obj[key1] = val1 def _swap_xy_data(data_obj): """Swap x and y data and references""" swaps = [('x', 'y'), ('x0', 'y0'), ('dx', 'dy'), ('xbins', 'ybins'), ('nbinsx', 'nbinsy'), ('autobinx', 'autobiny'), ('error_x', 'error_y')] for swap in swaps: _swap_keys(data_obj, swap[0], swap[1]) try: rows = len(data_obj['z']) cols = len(data_obj['z'][0]) for row in data_obj['z']: if len(row) != cols: raise TypeError # if we can't do transpose, we hit an exception before here z = data_obj.pop('z') data_obj['z'] = [[0 for rrr in range(rows)] for ccc in range(cols)] for iii in range(rows): for jjj in range(cols): data_obj['z'][jjj][iii] = z[iii][jjj] except (KeyError, TypeError, IndexError) as err: warn = False try: if data_obj['z'] is not None: warn = True if len(data_obj['z']) == 0: warn = False except (KeyError, TypeError): pass if warn: warnings.warn( "Data in this file required an 'xy' swap but the 'z' matrix " "in one of the data objects could not be transposed. Here's " "why:\n\n{}".format(repr(err)) ) def get_figure(file_owner_or_url, file_id=None, raw=False): """Returns a JSON figure representation for the specified file Plotly uniquely identifies figures with a 'file_owner'/'file_id' pair. Since each file is given a corresponding unique url, you may also simply pass a valid plotly url as the first argument. Examples: fig = get_figure('https://plot.ly/~chris/1638') fig = get_figure('chris', 1638) Note, if you're using a file_owner string as the first argument, you MUST specify a `file_id` keyword argument. Else, if you're using a url string as the first argument, you MUST NOT specify a `file_id` keyword argument, or file_id must be set to Python's None value. Positional arguments: file_owner_or_url (string) -- a valid plotly username OR a valid plotly url Keyword arguments: file_id (default=None) -- an int or string that can be converted to int if you're using a url, don't fill this in! raw (default=False) -- if true, return unicode JSON string verbatim** **by default, plotly will return a Figure object (run help(plotly .graph_objs.Figure)). This representation decodes the keys and values from unicode (if possible), removes information irrelevant to the figure representation, and converts the JSON dictionary objects to plotly `graph objects`. """ plotly_rest_url = get_config()['plotly_domain'] if file_id is None: # assume we're using a url url = file_owner_or_url if url[:len(plotly_rest_url)] != plotly_rest_url: raise exceptions.PlotlyError( "Because you didn't supply a 'file_id' in the call, " "we're assuming you're trying to snag a figure from a url. " "You supplied the url, '{0}', we expected it to start with " "'{1}'." "\nRun help on this function for more information." "".format(url, plotly_rest_url)) head = plotly_rest_url + "/~" file_owner = url.replace(head, "").split('/')[0] file_id = url.replace(head, "").split('/')[1] else: file_owner = file_owner_or_url try: int(file_id) except ValueError: raise exceptions.PlotlyError( "The 'file_id' argument was not able to be converted into an " "integer number. Make sure that the positional 'file_id' argument " "is a number that can be converted into an integer or a string " "that can be converted into an integer." ) if int(file_id) < 0: raise exceptions.PlotlyError( "The 'file_id' argument must be a non-negative number." ) fid = '{}:{}'.format(file_owner, file_id) response = v2.plots.content(fid, inline_data=True) figure = response.json() # Fix 'histogramx', 'histogramy', and 'bardir' stuff for index, entry in enumerate(figure['data']): try: # Use xbins to bin data in x, and ybins to bin data in y if all((entry['type'] == 'histogramy', 'xbins' in entry, 'ybins' not in entry)): entry['ybins'] = entry.pop('xbins') # Convert bardir to orientation, and put the data into the axes # it's eventually going to be used with if entry['type'] in ['histogramx', 'histogramy']: entry['type'] = 'histogram' if 'bardir' in entry: entry['orientation'] = entry.pop('bardir') if entry['type'] == 'bar': if entry['orientation'] == 'h': _swap_xy_data(entry) if entry['type'] == 'histogram': if ('x' in entry) and ('y' not in entry): if entry['orientation'] == 'h': _swap_xy_data(entry) del entry['orientation'] if ('y' in entry) and ('x' not in entry): if entry['orientation'] == 'v': _swap_xy_data(entry) del entry['orientation'] figure['data'][index] = entry except KeyError: pass # Remove stream dictionary if found in a data trace # (it has private tokens in there we need to hide!) for index, entry in enumerate(figure['data']): if 'stream' in entry: del figure['data'][index]['stream'] if raw: return figure return tools.get_valid_graph_obj(figure, obj_type='Figure') @utils.template_doc(**tools.get_config_file()) class Stream: """ Interface to Plotly's real-time graphing API. Initialize a Stream object with a stream_id found in {plotly_domain}/settings. Real-time graphs are initialized with a call to `plot` that embeds your unique `stream_id`s in each of the graph's traces. The `Stream` interface plots data to these traces, as identified with the unique stream_id, in real-time. Every viewer of the graph sees the same data at the same time. View examples and tutorials here: https://plot.ly/python/streaming/ Stream example: # Initialize a streaming graph # by embedding stream_id's in the graph's traces import plotly.plotly as py from plotly.graph_objs import Data, Scatter, Stream stream_id = "your_stream_id" # See {plotly_domain}/settings py.plot(Data([Scatter(x=[], y=[], stream=Stream(token=stream_id, maxpoints=100))])) # Stream data to the import trace stream = Stream(stream_id) # Initialize a stream object stream.open() # Open the stream stream.write(dict(x=1, y=1)) # Plot (1, 1) in your graph """ HTTP_PORT = 80 HTTPS_PORT = 443 @utils.template_doc(**tools.get_config_file()) def __init__(self, stream_id): """ Initialize a Stream object with your unique stream_id. Find your stream_id at {plotly_domain}/settings. For more help, see: `help(plotly.plotly.Stream)` or see examples and tutorials here: https://plot.ly/python/streaming/ """ self.stream_id = stream_id self._stream = None def get_streaming_specs(self): """ Returns the streaming server, port, ssl_enabled flag, and headers. """ streaming_url = get_config()['plotly_streaming_domain'] ssl_verification_enabled = get_config()['plotly_ssl_verification'] ssl_enabled = 'https' in streaming_url port = self.HTTPS_PORT if ssl_enabled else self.HTTP_PORT # If no scheme (https/https) is included in the streaming_url, the # host will be None. Use streaming_url in this case. host = (six.moves.urllib.parse.urlparse(streaming_url).hostname or streaming_url) headers = {'Host': host, 'plotly-streamtoken': self.stream_id} streaming_specs = { 'server': host, 'port': port, 'ssl_enabled': ssl_enabled, 'ssl_verification_enabled': ssl_verification_enabled, 'headers': headers } return streaming_specs def heartbeat(self, reconnect_on=(200, '', 408)): """ Keep stream alive. Streams will close after ~1 min of inactivity. If the interval between stream writes is > 30 seconds, you should consider adding a heartbeat between your stream.write() calls like so: >>> stream.heartbeat() """ try: self._stream.write('\n', reconnect_on=reconnect_on) except AttributeError: raise exceptions.PlotlyError( "Stream has not been opened yet, " "cannot write to a closed connection. " "Call `open()` on the stream to open the stream." ) @property def connected(self): if self._stream is None: return False return self._stream._isconnected() def open(self): """ Open streaming connection to plotly. For more help, see: `help(plotly.plotly.Stream)` or see examples and tutorials here: https://plot.ly/python/streaming/ """ streaming_specs = self.get_streaming_specs() self._stream = chunked_requests.Stream(**streaming_specs) def write(self, trace, layout=None, validate=True, reconnect_on=(200, '', 408)): """ Write to an open stream. Once you've instantiated a 'Stream' object with a 'stream_id', you can 'write' to it in real time. positional arguments: trace - A valid plotly trace object (e.g., Scatter, Heatmap, etc.). Not all keys in these are `stremable` run help(Obj) on the type of trace your trying to stream, for each valid key, if the key is streamable, it will say 'streamable = True'. Trace objects must be dictionary-like. keyword arguments: layout (default=None) - A valid Layout object Run help(plotly.graph_objs.Layout) validate (default = True) - Validate this stream before sending? This will catch local errors if set to True. Some valid keys for trace dictionaries: 'x', 'y', 'text', 'z', 'marker', 'line' Examples: >>> write(dict(x=1, y=2)) # assumes 'scatter' type >>> write(Bar(x=[1, 2, 3], y=[10, 20, 30])) >>> write(Scatter(x=1, y=2, text='scatter text')) >>> write(Scatter(x=1, y=3, marker=Marker(color='blue'))) >>> write(Heatmap(z=[[1, 2, 3], [4, 5, 6]])) The connection to plotly's servers is checked before writing and reconnected if disconnected and if the response status code is in `reconnect_on`. For more help, see: `help(plotly.plotly.Stream)` or see examples and tutorials here: http://nbviewer.ipython.org/github/plotly/python-user-guide/blob/master/s7_streaming/s7_streaming.ipynb """ stream_object = dict() stream_object.update(trace) if 'type' not in stream_object: stream_object['type'] = 'scatter' if validate: try: tools.validate(stream_object, stream_object['type']) except exceptions.PlotlyError as err: raise exceptions.PlotlyError( "Part of the data object with type, '{0}', is invalid. " "This will default to 'scatter' if you do not supply a " "'type'. If you do not want to validate your data objects " "when streaming, you can set 'validate=False' in the call " "to 'your_stream.write()'. Here's why the object is " "invalid:\n\n{1}".format(stream_object['type'], err) ) if layout is not None: try: tools.validate(layout, 'Layout') except exceptions.PlotlyError as err: raise exceptions.PlotlyError( "Your layout kwarg was invalid. " "Here's why:\n\n{0}".format(err) ) del stream_object['type'] if layout is not None: stream_object.update(dict(layout=layout)) # TODO: allow string version of this? jdata = _json.dumps(stream_object, cls=utils.PlotlyJSONEncoder) jdata += "\n" try: self._stream.write(jdata, reconnect_on=reconnect_on) except AttributeError: raise exceptions.PlotlyError( "Stream has not been opened yet, " "cannot write to a closed connection. " "Call `open()` on the stream to open the stream.") def close(self): """ Close the stream connection to plotly's streaming servers. For more help, see: `help(plotly.plotly.Stream)` or see examples and tutorials here: https://plot.ly/python/streaming/ """ try: self._stream.close() except AttributeError: raise exceptions.PlotlyError("Stream has not been opened yet.") class image: """ Helper functions wrapped around plotly's static image generation api. """ @staticmethod def get(figure_or_data, format='png', width=None, height=None, scale=None): """Return a static image of the plot described by `figure_or_data`. positional arguments: - figure_or_data: The figure dict-like or data list-like object that describes a plotly figure. Same argument used in `py.plot`, `py.iplot`, see https://plot.ly/python for examples - format: 'png', 'svg', 'jpeg', 'pdf' - width: output width - height: output height - scale: Increase the resolution of the image by `scale` amount (e.g. `3`) Only valid for PNG and JPEG images. example: ``` import plotly.plotly as py fig = {'data': [{'x': [1, 2, 3], 'y': [3, 1, 5], 'type': 'bar'}]} py.image.get(fig, 'png', scale=3) ``` """ # TODO: format is a built-in name... we shouldn't really use it if isinstance(figure_or_data, dict): figure = figure_or_data elif isinstance(figure_or_data, list): figure = {'data': figure_or_data} else: raise exceptions.PlotlyEmptyDataError( "`figure_or_data` must be a dict or a list." ) if format not in ['png', 'svg', 'jpeg', 'pdf']: raise exceptions.PlotlyError( "Invalid format. This version of your Plotly-Python " "package currently only supports png, svg, jpeg, and pdf. " "Learn more about image exporting, and the currently " "supported file types here: " "https://plot.ly/python/static-image-export/" ) if scale is not None: try: scale = float(scale) except: raise exceptions.PlotlyError( "Invalid scale parameter. Scale must be a number." ) payload = {'figure': figure, 'format': format} if width is not None: payload['width'] = width if height is not None: payload['height'] = height if scale is not None: payload['scale'] = scale response = v2.images.create(payload) headers = response.headers if ('content-type' in headers and headers['content-type'] in ['image/png', 'image/jpeg', 'application/pdf', 'image/svg+xml']): return response.content elif ('content-type' in headers and 'json' in headers['content-type']): return response.json()['image'] @classmethod def ishow(cls, figure_or_data, format='png', width=None, height=None, scale=None): """Display a static image of the plot described by `figure_or_data` in an IPython Notebook. positional arguments: - figure_or_data: The figure dict-like or data list-like object that describes a plotly figure. Same argument used in `py.plot`, `py.iplot`, see https://plot.ly/python for examples - format: 'png', 'svg', 'jpeg', 'pdf' - width: output width - height: output height - scale: Increase the resolution of the image by `scale` amount Only valid for PNG and JPEG images. example: ``` import plotly.plotly as py fig = {'data': [{'x': [1, 2, 3], 'y': [3, 1, 5], 'type': 'bar'}]} py.image.ishow(fig, 'png', scale=3) """ if format == 'pdf': raise exceptions.PlotlyError( "Aw, snap! " "It's not currently possible to embed a pdf into " "an IPython notebook. You can save the pdf " "with the `image.save_as` or you can " "embed an png, jpeg, or svg.") img = cls.get(figure_or_data, format, width, height, scale) from IPython.display import display, Image, SVG if format == 'svg': display(SVG(img)) else: display(Image(img)) @classmethod def save_as(cls, figure_or_data, filename, format=None, width=None, height=None, scale=None): """Save a image of the plot described by `figure_or_data` locally as `filename`. Valid image formats are 'png', 'svg', 'jpeg', and 'pdf'. The format is taken as the extension of the filename or as the supplied format. positional arguments: - figure_or_data: The figure dict-like or data list-like object that describes a plotly figure. Same argument used in `py.plot`, `py.iplot`, see https://plot.ly/python for examples - filename: The filepath to save the image to - format: 'png', 'svg', 'jpeg', 'pdf' - width: output width - height: output height - scale: Increase the resolution of the image by `scale` amount Only valid for PNG and JPEG images. example: ``` import plotly.plotly as py fig = {'data': [{'x': [1, 2, 3], 'y': [3, 1, 5], 'type': 'bar'}]} py.image.save_as(fig, 'my_image.png', scale=3) ``` """ # todo: format shadows built-in name (base, ext) = os.path.splitext(filename) if not ext and not format: filename += '.png' elif ext and not format: format = ext[1:] elif not ext and format: filename += '.' + format img = cls.get(figure_or_data, format, width, height, scale) f = open(filename, 'wb') f.write(img) f.close() class file_ops: """ Interface to Plotly's File System API """ @classmethod def mkdirs(cls, folder_path): """ Create folder(s) specified by folder_path in your Plotly account. If the intermediate directories do not exist, they will be created. If they already exist, no error will be thrown. Mimics the shell's mkdir -p. Returns: - 200 if folders already existed, nothing was created - 201 if path was created Raises: - exceptions.PlotlyRequestError with status code 400 if the path already exists. Usage: >> mkdirs('new folder') >> mkdirs('existing folder/new folder') >> mkdirs('new/folder/path') """ response = v2.folders.create({'path': folder_path}) return response.status_code class grid_ops: """ Interface to Plotly's Grid API. Plotly Grids are Plotly's tabular data object, rendered in an online spreadsheet. Plotly graphs can be made from references of columns of Plotly grid objects. Free-form JSON Metadata can be saved with Plotly grids. To create a Plotly grid in your Plotly account from Python, see `grid_ops.upload`. To add rows or columns to an existing Plotly grid, see `grid_ops.append_rows` and `grid_ops.append_columns` respectively. To delete one of your grid objects, see `grid_ops.delete`. """ @classmethod def _fill_in_response_column_ids(cls, request_columns, response_columns, grid_id): for req_col in request_columns: for resp_col in response_columns: if resp_col['name'] == req_col.name: req_col.id = '{0}:{1}'.format(grid_id, resp_col['uid']) response_columns.remove(resp_col) @staticmethod def ensure_uploaded(fid): if fid: return raise exceptions.PlotlyError( 'This operation requires that the grid has already been uploaded ' 'to Plotly. Try `uploading` first.' ) @classmethod def upload(cls, grid, filename, world_readable=True, auto_open=True, meta=None): """ Upload a grid to your Plotly account with the specified filename. Positional arguments: - grid: A plotly.grid_objs.Grid object, call `help(plotly.grid_ops.Grid)` for more info. - filename: Name of the grid to be saved in your Plotly account. To save a grid in a folder in your Plotly account, separate specify a filename with folders and filename separated by backslashes (`/`). If a grid, plot, or folder already exists with the same filename, a `plotly.exceptions.RequestError` will be thrown with status_code 409 Optional keyword arguments: - world_readable (default=True): make this grid publically (True) or privately (False) viewable. - auto_open (default=True): Automatically open this grid in the browser (True) - meta (default=None): Optional Metadata to associate with this grid. Metadata is any arbitrary JSON-encodable object, for example: `{"experiment name": "GaAs"}` Filenames must be unique. To overwrite a grid with the same filename, you'll first have to delete the grid with the blocking name. See `plotly.plotly.grid_ops.delete`. Usage example 1: Upload a plotly grid ``` from plotly.grid_objs import Grid, Column import plotly.plotly as py column_1 = Column([1, 2, 3], 'time') column_2 = Column([4, 2, 5], 'voltage') grid = Grid([column_1, column_2]) py.grid_ops.upload(grid, 'time vs voltage') ``` Usage example 2: Make a graph based with data that is sourced from a newly uploaded Plotly grid ``` import plotly.plotly as py from plotly.grid_objs import Grid, Column from plotly.graph_objs import Scatter # Upload a grid column_1 = Column([1, 2, 3], 'time') column_2 = Column([4, 2, 5], 'voltage') grid = Grid([column_1, column_2]) py.grid_ops.upload(grid, 'time vs voltage') # Build a Plotly graph object sourced from the # grid's columns trace = Scatter(xsrc=grid[0], ysrc=grid[1]) py.plot([trace], filename='graph from grid') ``` """ # Make a folder path if filename[-1] == '/': filename = filename[0:-1] paths = filename.split('/') parent_path = '/'.join(paths[0:-1]) filename = paths[-1] if parent_path != '': file_ops.mkdirs(parent_path) # transmorgify grid object into plotly's format grid_json = grid._to_plotly_grid_json() if meta is not None: grid_json['metadata'] = meta payload = { 'filename': filename, 'data': grid_json, 'world_readable': world_readable } if parent_path != '': payload['parent_path'] = parent_path response = v2.grids.create(payload) parsed_content = response.json() cols = parsed_content['file']['cols'] fid = parsed_content['file']['fid'] web_url = parsed_content['file']['web_url'] # mutate the grid columns with the id's returned from the server cls._fill_in_response_column_ids(grid, cols, fid) grid.id = fid if meta is not None: meta_ops.upload(meta, grid=grid) if auto_open: _open_url(web_url) return web_url @classmethod def append_columns(cls, columns, grid=None, grid_url=None): """ Append columns to a Plotly grid. `columns` is an iterable of plotly.grid_objs.Column objects and only one of `grid` and `grid_url` needs to specified. `grid` is a ploty.grid_objs.Grid object that has already been uploaded to plotly with the grid_ops.upload method. `grid_url` is a unique URL of a `grid` in your plotly account. Usage example 1: Upload a grid to Plotly, and then append a column ``` from plotly.grid_objs import Grid, Column import plotly.plotly as py column_1 = Column([1, 2, 3], 'time') grid = Grid([column_1]) py.grid_ops.upload(grid, 'time vs voltage') # append a column to the grid column_2 = Column([4, 2, 5], 'voltage') py.grid_ops.append_columns([column_2], grid=grid) ``` Usage example 2: Append a column to a grid that already exists on Plotly ``` from plotly.grid_objs import Grid, Column import plotly.plotly as py grid_url = 'https://plot.ly/~chris/3143' column_1 = Column([1, 2, 3], 'time') py.grid_ops.append_columns([column_1], grid_url=grid_url) ``` """ grid_id = parse_grid_id_args(grid, grid_url) grid_ops.ensure_uploaded(grid_id) # Verify unique column names column_names = [c.name for c in columns] if grid: existing_column_names = [c.name for c in grid] column_names.extend(existing_column_names) duplicate_name = utils.get_first_duplicate(column_names) if duplicate_name: err = exceptions.NON_UNIQUE_COLUMN_MESSAGE.format(duplicate_name) raise exceptions.InputError(err) # This is sorta gross, we need to double-encode this. body = { 'cols': _json.dumps(columns, cls=utils.PlotlyJSONEncoder) } fid = grid_id response = v2.grids.col_create(fid, body) parsed_content = response.json() cls._fill_in_response_column_ids(columns, parsed_content['cols'], fid) if grid: grid.extend(columns) @classmethod def append_rows(cls, rows, grid=None, grid_url=None): """ Append rows to a Plotly grid. `rows` is an iterable of rows, where each row is a list of numbers, strings, or dates. The number of items in each row must be equal to the number of columns in the grid. If appending rows to a grid with columns of unequal length, Plotly will fill the columns with shorter length with empty strings. Only one of `grid` and `grid_url` needs to specified. `grid` is a ploty.grid_objs.Grid object that has already been uploaded to plotly with the grid_ops.upload method. `grid_url` is a unique URL of a `grid` in your plotly account. Usage example 1: Upload a grid to Plotly, and then append rows ``` from plotly.grid_objs import Grid, Column import plotly.plotly as py column_1 = Column([1, 2, 3], 'time') column_2 = Column([5, 2, 7], 'voltage') grid = Grid([column_1, column_2]) py.grid_ops.upload(grid, 'time vs voltage') # append a row to the grid row = [1, 5] py.grid_ops.append_rows([row], grid=grid) ``` Usage example 2: Append a row to a grid that already exists on Plotly ``` from plotly.grid_objs import Grid import plotly.plotly as py grid_url = 'https://plot.ly/~chris/3143' row = [1, 5] py.grid_ops.append_rows([row], grid=grid_url) ``` """ grid_id = parse_grid_id_args(grid, grid_url) grid_ops.ensure_uploaded(grid_id) if grid: n_columns = len([column for column in grid]) for row_i, row in enumerate(rows): if len(row) != n_columns: raise exceptions.InputError( "The number of entries in " "each row needs to equal the number of columns in " "the grid. Row {0} has {1} {2} but your " "grid has {3} {4}. " .format(row_i, len(row), 'entry' if len(row) == 1 else 'entries', n_columns, 'column' if n_columns == 1 else 'columns')) fid = grid_id v2.grids.row(fid, {'rows': rows}) if grid: longest_column_length = max([len(col.data) for col in grid]) for column in grid: n_empty_rows = longest_column_length - len(column.data) empty_string_rows = ['' for _ in range(n_empty_rows)] column.data.extend(empty_string_rows) column_extensions = zip(*rows) for local_column, column_extension in zip(grid, column_extensions): local_column.data.extend(column_extension) @classmethod def delete(cls, grid=None, grid_url=None): """ Delete a grid from your Plotly account. Only one of `grid` or `grid_url` needs to be specified. `grid` is a plotly.grid_objs.Grid object that has already been uploaded to Plotly. `grid_url` is the URL of the Plotly grid to delete Usage example 1: Upload a grid to plotly, then delete it ``` from plotly.grid_objs import Grid, Column import plotly.plotly as py column_1 = Column([1, 2, 3], 'time') column_2 = Column([4, 2, 5], 'voltage') grid = Grid([column_1, column_2]) py.grid_ops.upload(grid, 'time vs voltage') # now delete it, and free up that filename py.grid_ops.delete(grid) ``` Usage example 2: Delete a plotly grid by url ``` import plotly.plotly as py grid_url = 'https://plot.ly/~chris/3' py.grid_ops.delete(grid_url=grid_url) ``` """ fid = parse_grid_id_args(grid, grid_url) grid_ops.ensure_uploaded(fid) v2.grids.trash(fid) v2.grids.permanent_delete(fid) class meta_ops: """ Interface to Plotly's Metadata API. In Plotly, Metadata is arbitrary, free-form JSON data that is associated with Plotly grids. Metadata is viewable with any grid that is shared and grids are searchable by key value pairs in the Metadata. Metadata is any JSON-encodable object. To upload Metadata, either use the optional keyword argument `meta` in the `py.grid_ops.upload` method, or use `py.meta_ops.upload`. """ @classmethod def upload(cls, meta, grid=None, grid_url=None): """ Upload Metadata to a Plotly grid. Metadata is any JSON-encodable object. For example, a dictionary, string, or list. Only one of `grid` or `grid_url` needs to be specified. `grid` is a plotly.grid_objs.Grid object that has already been uploaded to Plotly. `grid_url` is the URL of the Plotly grid to attach Metadata to. Usage example 1: Upload a grid to Plotly, then attach Metadata to it ``` from plotly.grid_objs import Grid, Column import plotly.plotly as py column_1 = Column([1, 2, 3], 'time') column_2 = Column([4, 2, 5], 'voltage') grid = Grid([column_1, column_2]) py.grid_ops.upload(grid, 'time vs voltage') # now attach Metadata to the grid meta = {'experment': 'GaAs'} py.meta_ops.upload(meta, grid=grid) ``` Usage example 2: Upload Metadata to an existing Plotly grid ``` import plotly.plotly as py grid_url = 'https://plot.ly/~chris/3143' meta = {'experment': 'GaAs'} py.meta_ops.upload(meta, grid_url=grid_Url) ``` """ fid = parse_grid_id_args(grid, grid_url) return v2.grids.update(fid, {'metadata': meta}).json() def parse_grid_id_args(grid, grid_url): """ Return the grid_id from the non-None input argument. Raise an error if more than one argument was supplied. """ if grid is not None: id_from_grid = grid.id else: id_from_grid = None args = [id_from_grid, grid_url] arg_names = ('grid', 'grid_url') supplied_arg_names = [arg_name for arg_name, arg in zip(arg_names, args) if arg is not None] if not supplied_arg_names: raise exceptions.InputError( "One of the two keyword arguments is required:\n" " `grid` or `grid_url`\n\n" "grid: a plotly.graph_objs.Grid object that has already\n" " been uploaded to Plotly.\n\n" "grid_url: the url where the grid can be accessed on\n" " Plotly, e.g. 'https://plot.ly/~chris/3043'\n\n" ) elif len(supplied_arg_names) > 1: raise exceptions.InputError( "Only one of `grid` or `grid_url` is required. \n" "You supplied both. \n" ) else: supplied_arg_name = supplied_arg_names.pop() if supplied_arg_name == 'grid_url': path = six.moves.urllib.parse.urlparse(grid_url).path file_owner, file_id = path.replace("/~", "").split('/')[0:2] return '{0}:{1}'.format(file_owner, file_id) else: return grid.id def add_share_key_to_url(plot_url, attempt=0): """ Check that share key is enabled and update url to include the secret key """ urlsplit = six.moves.urllib.parse.urlparse(plot_url) username = urlsplit.path.split('/')[1].split('~')[1] idlocal = urlsplit.path.split('/')[2] fid = '{}:{}'.format(username, idlocal) body = {'share_key_enabled': True, 'world_readable': False} response = v2.files.update(fid, body) # Sometimes a share key is added, but access is still denied. # Check that share_key_enabled is set to true and # retry if this is not the case # https://github.com/plotly/streambed/issues/4089 time.sleep(4) share_key_enabled = v2.files.retrieve(fid).json()['share_key_enabled'] if not share_key_enabled: attempt += 1 if attempt == 50: raise exceptions.PlotlyError( "The sharekey could not be enabled at this time so the graph " "is saved as private. Try again to save as 'secret' later." ) add_share_key_to_url(plot_url, attempt) url_share_key = plot_url + '?share_key=' + response.json()['share_key'] return url_share_key def _send_to_plotly(figure, **plot_options): fig = 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. :param (str) grid_url: The web_url which locates a Plotly grid. :param (bool) raw: if False, will output a Grid instance of the JSON grid being retrieved. If True, raw JSON will be returned. """ fid = parse_grid_id_args(None, grid_url) response = v2.grids.content(fid) parsed_content = response.json() if raw: return parsed_content return Grid(parsed_content, fid) class dashboard_ops: """ Interface to Plotly's Dashboards API. Plotly Dashboards are JSON blobs. They are made up by a bunch of containers which contain either empty boxes or boxes with file urls. For more info on Dashboard objects themselves, run `help(plotly.dashboard_objs)`. Example 1: Upload Simple Dashboard ``` import plotly.plotly as py import plotly.dashboard_objs as dashboard box_1 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:123', 'title': 'box 1' } box_2 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:456', 'title': 'box 2' } my_dboard = dashboard.Dashboard() my_dboard.insert(box_1) # my_dboard.get_preview() my_dboard.insert(box_2, 'above', 1) # my_dboard.get_preview() py.dashboard_ops.upload(my_dboard) ``` Example 2: Retreive Dashboard from Plotly ``` # works if you have at least one dashboard in your files import plotly.plotly as py import plotly.dashboard_objs as dashboard dboard_names = get_dashboard_names() first_dboard = get_dashboard(dboard_names[0]) first_dboard.get_preview() ``` """ @classmethod def upload(cls, dashboard, filename, sharing='public', auto_open=True): """ BETA function for uploading/overwriting dashboards to Plotly. :param (dict) dashboard: the JSON dashboard to be uploaded. Use plotly.dashboard_objs.dashboard_objs to create a Dashboard object. :param (str) filename: the name of the dashboard to be saved in your Plotly account. Will overwrite a dashboard of the same name if it already exists in your files. :param (str) sharing: can be set to either 'public', 'private' or 'secret'. If 'public', your dashboard will be viewable by all other users. If 'private' only you can see your dashboard. If 'secret', the url will be returned with a sharekey appended to the url. Anyone with the url may view the dashboard. :param (bool) auto_open: automatically opens the dashboard in the browser. """ if sharing == 'public': world_readable = True elif sharing == 'private': world_readable = False elif sharing == 'secret': world_readable = False data = { 'content': json.dumps(dashboard), 'filename': filename, 'world_readable': world_readable } # lookup if pre-existing filename already exists try: lookup_res = v2.files.lookup(filename) matching_file = json.loads(lookup_res.content) if matching_file['filetype'] == 'dashboard': old_fid = matching_file['fid'] res = v2.dashboards.update(old_fid, data) else: raise exceptions.PlotlyError( "'{filename}' is already a {filetype} in your account. " "While you can overwrite dashboards with the same name, " "you can't change overwrite files with a different type. " "Try deleting '{filename}' in your account or changing " "the filename.".format( filename=filename, filetype=matching_file['filetype'] ) ) except exceptions.PlotlyRequestError: res = v2.dashboards.create(data) res.raise_for_status() url = res.json()['web_url'] if sharing == 'secret': url = add_share_key_to_url(url) if auto_open: webbrowser.open_new(res.json()['web_url']) return url @classmethod def _get_all_dashboards(cls): dashboards = [] res = v2.dashboards.list().json() for dashboard in res['results']: if not dashboard['deleted']: dashboards.append(dashboard) while res['next']: res = v2.utils.request('get', res['next']).json() for dashboard in res['results']: if not dashboard['deleted']: dashboards.append(dashboard) return dashboards @classmethod def _get_dashboard_json(cls, dashboard_name, only_content=True): dashboards = cls._get_all_dashboards() for index, dboard in enumerate(dashboards): if dboard['filename'] == dashboard_name: break dashboard = v2.utils.request( 'get', dashboards[index]['api_urls']['dashboards'] ).json() if only_content: dashboard_json = json.loads(dashboard['content']) return dashboard_json else: return dashboard @classmethod def get_dashboard(cls, dashboard_name): """Returns a Dashboard object from a dashboard name.""" dashboard_json = cls._get_dashboard_json(dashboard_name) return dashboard.Dashboard(dashboard_json) @classmethod def get_dashboard_names(cls): """Return list of all active dashboard names from users' account.""" dashboards = cls._get_all_dashboards() return [str(dboard['filename']) for dboard in dashboards] class presentation_ops: """ Interface to Plotly's Spectacle-Presentations API. """ @classmethod def upload(cls, presentation, filename, sharing='public', auto_open=True): """ Function for uploading presentations to Plotly. :param (dict) presentation: the JSON presentation to be uploaded. Use plotly.presentation_objs.Presentation to create presentations from a Markdown-like string. :param (str) filename: the name of the presentation to be saved in your Plotly account. Will overwrite a presentation of the same name if it already exists in your files. :param (str) sharing: can be set to either 'public', 'private' or 'secret'. If 'public', your presentation will be viewable by all other users. If 'private' only you can see your presentation. If it is set to 'secret', the url will be returned with a string of random characters appended to the url which is called a sharekey. The point of a sharekey is that it makes the url very hard to guess, but anyone with the url can view the presentation. :param (bool) auto_open: automatically opens the presentation in the browser. See the documentation online for examples. """ if sharing == 'public': world_readable = True elif sharing in ['private', 'secret']: world_readable = False else: raise exceptions.PlotlyError( SHARING_ERROR_MSG ) data = { 'content': json.dumps(presentation), 'filename': filename, 'world_readable': world_readable } # lookup if pre-existing filename already exists try: lookup_res = v2.files.lookup(filename) lookup_res.raise_for_status() matching_file = json.loads(lookup_res.content) if matching_file['filetype'] != 'spectacle_presentation': raise exceptions.PlotlyError( "'{filename}' is already a {filetype} in your account. " "You can't overwrite a file that is not a spectacle_" "presentation. Please pick another filename.".format( filename=filename, filetype=matching_file['filetype'] ) ) else: old_fid = matching_file['fid'] res = v2.spectacle_presentations.update(old_fid, data) except exceptions.PlotlyRequestError: res = v2.spectacle_presentations.create(data) res.raise_for_status() url = res.json()['web_url'] if sharing == 'secret': url = add_share_key_to_url(url) if auto_open: webbrowser.open_new(res.json()['web_url']) return url def create_animations(figure, filename=None, sharing='public', auto_open=True): """ BETA function that creates plots with animations via `frames`. Creates an animated plot using 'frames' alongside 'data' and 'layout'. This BETA endpoint is subject to deprecation in the future. In relation to `plotly.plotly.plot`, folder-creation and overwriting are not supported but creating a plot with or without animations via frames is supported. :param (str) filename: if set to 'None', an automatically-generated plot name will be created. Does not support folder creation, meaning that a folder of the form 'folder/name' will NOT create a the folder and place the plot in it. :param (str) sharing: see `plotly.plotly.plot()` doc string. :param (bool) auto_open: if True, opens plot in the browser. If False, returns the url for the plot instead. Example 1: Simple Animation ``` import plotly.plotly as py from plotly.grid_objs import Grid, Column column_1 = Column([0.5], 'x') column_2 = Column([0.5], 'y') column_3 = Column([1.5], 'x2') column_4 = Column([1.5], 'y2') grid = Grid([column_1, column_2, column_3, column_4]) py.grid_ops.upload(grid, 'ping_pong_grid', auto_open=False) # create figure figure = { 'data': [ { 'xsrc': grid.get_column_reference('x'), 'ysrc': grid.get_column_reference('y'), 'mode': 'markers', } ], 'layout': {'title': 'Ping Pong Animation', 'xaxis': {'range': [0, 2], 'autorange': False}, 'yaxis': {'range': [0, 2], 'autorange': False}, 'updatemenus': [{ 'buttons': [ {'args': [None], 'label': u'Play', 'method': u'animate'} ], 'pad': {'r': 10, 't': 87}, 'showactive': False, 'type': 'buttons' }]}, 'frames': [ { 'data': [ { 'xsrc': grid.get_column_reference('x2'), 'ysrc': grid.get_column_reference('y2'), 'mode': 'markers', } ] }, { 'data': [ { 'xsrc': grid.get_column_reference('x'), 'ysrc': grid.get_column_reference('y'), 'mode': 'markers', } ] } ] } py.create_animations(figure, 'ping_pong') ``` Example 2: Growing Circles Animation ``` import plotly.plotly as py from plotly.grid_objs import Grid, Column column_1 = Column([0.9, 1.1], 'x') column_2 = Column([1.0, 1.0], 'y') column_3 = Column([0.8, 1.2], 'x2') column_4 = Column([1.2, 0.8], 'y2') column_5 = Column([0.7, 1.3], 'x3') column_6 = Column([0.7, 1.3], 'y3') column_7 = Column([0.6, 1.4], 'x4') column_8 = Column([1.5, 0.5], 'y4') column_9 = Column([0.4, 1.6], 'x5') column_10 = Column([1.2, 0.8], 'y5') grid = Grid([column_1, column_2, column_3, column_4, column_5, column_6, column_7, column_8, column_9, column_10]) py.grid_ops.upload(grid, 'growing_circles_grid', auto_open=False) # create figure figure = { 'data': [ { 'xsrc': grid.get_column_reference('x'), 'ysrc': grid.get_column_reference('y'), 'mode': 'markers', 'marker': {'color': '#48186a', 'size': 10} } ], 'layout': {'title': 'Growing Circles', 'xaxis': {'range': [0, 2], 'autorange': False}, 'yaxis': {'range': [0, 2], 'autorange': False}, 'updatemenus': [{ 'buttons': [ {'args': [None], 'label': u'Play', 'method': u'animate'} ], 'pad': {'r': 10, 't': 87}, 'showactive': False, 'type': 'buttons' }]}, 'frames': [ { 'data': [ { 'xsrc': grid.get_column_reference('x2'), 'ysrc': grid.get_column_reference('y2'), 'mode': 'markers', 'marker': {'color': '#3b528b', 'size': 25} } ] }, { 'data': [ { 'xsrc': grid.get_column_reference('x3'), 'ysrc': grid.get_column_reference('y3'), 'mode': 'markers', 'marker': {'color': '#26828e', 'size': 50} } ] }, { 'data': [ { 'xsrc': grid.get_column_reference('x4'), 'ysrc': grid.get_column_reference('y4'), 'mode': 'markers', 'marker': {'color': '#5ec962', 'size': 80} } ] }, { 'data': [ { 'xsrc': grid.get_column_reference('x5'), 'ysrc': grid.get_column_reference('y5'), 'mode': 'markers', 'marker': {'color': '#d8e219', 'size': 100} } ] } ] } py.create_animations(figure, 'growing_circles') ``` """ body = { 'figure': figure, 'world_readable': True } # set filename if specified if filename: # warn user that creating folders isn't support in this version if '/' in filename: warnings.warn( "This BETA version of 'create_animations' does not support " "automatic folder creation. This means a filename of the form " "'name1/name2' will just create the plot with that name only." ) body['filename'] = filename # set sharing if sharing == 'public': body['world_readable'] = True elif sharing == 'private': body['world_readable'] = False elif sharing == 'secret': body['world_readable'] = False body['share_key_enabled'] = True else: raise exceptions.PlotlyError( SHARING_ERROR_MSG ) response = v2.plots.create(body) parsed_content = response.json() if sharing == 'secret': web_url = (parsed_content['file']['web_url'][:-1] + '?share_key=' + parsed_content['file']['share_key']) else: web_url = parsed_content['file']['web_url'] if auto_open: _open_url(web_url) return web_url def icreate_animations(figure, filename=None, sharing='public', auto_open=False): """ Create a unique url for this animated plot in Plotly and open in IPython. This function is based off `plotly.plotly.iplot`. See `plotly.plotly. create_animations` Doc String for param descriptions. """ url = create_animations(figure, filename, sharing, auto_open) if isinstance(figure, dict): layout = figure.get('layout', {}) else: layout = {} embed_options = dict() embed_options['width'] = layout.get('width', '100%') embed_options['height'] = layout.get('height', 525) try: float(embed_options['width']) except (ValueError, TypeError): pass else: embed_options['width'] = str(embed_options['width']) + 'px' try: float(embed_options['height']) except (ValueError, TypeError): pass else: embed_options['height'] = str(embed_options['height']) + 'px' return tools.embed(url, **embed_options) def _open_url(url): try: from webbrowser import open as wbopen wbopen(url) except: # TODO: what should we except here? this is dangerous pass plotly-2.2.3+dfsg.orig/plotly/plotly/chunked_requests/ 0000755 0001750 0001750 00000000000 13211605515 022470 5 ustar noahfx noahfx plotly-2.2.3+dfsg.orig/plotly/plotly/chunked_requests/chunked_request.py 0000644 0001750 0001750 00000027646 13104403274 026251 0 ustar noahfx noahfx import time import six import os import ssl from six.moves import http_client from six.moves.urllib.parse import urlparse class Stream: def __init__(self, server, port=80, headers={}, url='/', ssl_enabled=False, ssl_verification_enabled=True): ''' Initialize a stream object and an HTTP or HTTPS connection with chunked Transfer-Encoding to server:port with optional headers. ''' self.maxtries = 5 self._tries = 0 self._delay = 1 self._closed = False self._server = server self._port = port self._headers = headers self._url = url self._ssl_enabled = ssl_enabled self._ssl_verification_enabled = ssl_verification_enabled self._connect() def write(self, data, reconnect_on=('', 200, )): ''' Send `data` to the server in chunk-encoded form. Check the connection before writing and reconnect if disconnected and if the response status code is in `reconnect_on`. The response may either be an HTTPResponse object or an empty string. ''' if not self._isconnected(): # Attempt to get the response. response = self._getresponse() # Reconnect depending on the status code. if ((response == '' and '' in reconnect_on) or (response and isinstance(response, http_client.HTTPResponse) and response.status in reconnect_on)): self._reconnect() elif response and isinstance(response, http_client.HTTPResponse): # If an HTTPResponse was recieved then # make the users aware instead of # auto-reconnecting in case the # server is responding with an important # message that might prevent # future requests from going through, # like Invalid Credentials. # This allows the user to determine when # to reconnect. raise Exception("Server responded with " "status code: {status_code}\n" "and message: {msg}." .format(status_code=response.status, msg=response.read())) elif response == '': raise Exception("Attempted to write but socket " "was not connected.") try: msg = data msglen = format(len(msg), 'x') # msg length in hex # Send the message in chunk-encoded form self._conn.sock.setblocking(1) self._conn.send('{msglen}\r\n{msg}\r\n' .format(msglen=msglen, msg=msg).encode('utf-8')) self._conn.sock.setblocking(0) except http_client.socket.error: self._reconnect() self.write(data) def _get_proxy_config(self): """ Determine if self._url should be passed through a proxy. If so, return the appropriate proxy_server and proxy_port. Assumes https_proxy is used when ssl_enabled=True. """ proxy_server = None proxy_port = None ssl_enabled = self._ssl_enabled if ssl_enabled: proxy = os.environ.get("https_proxy") else: proxy = os.environ.get("http_proxy") no_proxy = os.environ.get("no_proxy") no_proxy_url = no_proxy and self._server in no_proxy if proxy and not no_proxy_url: p = urlparse(proxy) proxy_server = p.hostname proxy_port = p.port return proxy_server, proxy_port def _get_ssl_context(self): """ Return an unverified context if ssl verification is disabled. """ context = None if not self._ssl_verification_enabled: context = ssl._create_unverified_context() return context def _connect(self): ''' Initialize an HTTP/HTTPS connection with chunked Transfer-Encoding to server:port with optional headers. ''' server = self._server port = self._port headers = self._headers ssl_enabled = self._ssl_enabled proxy_server, proxy_port = self._get_proxy_config() if (proxy_server and proxy_port): if ssl_enabled: context = self._get_ssl_context() self._conn = http_client.HTTPSConnection( proxy_server, proxy_port, context=context ) else: self._conn = http_client.HTTPConnection( proxy_server, proxy_port ) self._conn.set_tunnel(server, port) else: if ssl_enabled: context = self._get_ssl_context() self._conn = http_client.HTTPSConnection( server, port, context=context ) else: self._conn = http_client.HTTPConnection(server, port) self._conn.putrequest('POST', self._url) self._conn.putheader('Transfer-Encoding', 'chunked') for header in headers: self._conn.putheader(header, headers[header]) self._conn.endheaders() # Set blocking to False prevents recv # from blocking while waiting for a response. self._conn.sock.setblocking(False) self._bytes = six.b('') self._reset_retries() time.sleep(0.5) def close(self): ''' Close the connection to server. If available, return a http_client.HTTPResponse object. Closing the connection involves sending the Transfer-Encoding terminating bytes. ''' self._reset_retries() self._closed = True # Chunked-encoded posts are terminated with '0\r\n\r\n' # For some reason, either Python or node.js seems to # require an extra \r\n. try: self._conn.send('\r\n0\r\n\r\n'.encode('utf-8')) except http_client.socket.error: # In case the socket has already been closed return '' return self._getresponse() def _getresponse(self): ''' Read from recv and return a HTTPResponse object if possible. Either 1 - The client has succesfully closed the connection: Return '' 2 - The server has already closed the connection: Return the response if possible. ''' # Wait for a response self._conn.sock.setblocking(True) # Parse the response response = self._bytes while True: try: _bytes = self._conn.sock.recv(1) except http_client.socket.error: # For error 54: Connection reset by peer # (and perhaps others) return six.b('') if _bytes == six.b(''): break else: response += _bytes # Set recv to be non-blocking again self._conn.sock.setblocking(False) # Convert the response string to a http_client.HTTPResponse # object with a bit of a hack if response != six.b(''): # Taken from # http://pythonwise.blogspot.ca/2010/02/parse-http-response.html try: response = http_client.HTTPResponse(_FakeSocket(response)) response.begin() except: # Bad headers ... etc. response = six.b('') return response def _isconnected(self): ''' Return True if the socket is still connected to the server, False otherwise. This check is done in 3 steps: 1 - Check if we have closed the connection 2 - Check if the original socket connection failed 3 - Check if the server has returned any data. If they have, assume that the server closed the response after they sent the data, i.e. that the data was the HTTP response. ''' # 1 - check if we've closed the connection. if self._closed: return False # 2 - Check if the original socket connection failed # If this failed, then no socket was initialized if self._conn.sock is None: return False try: # 3 - Check if the server has returned any data. # If they have, then start to store the response # in _bytes. self._bytes = six.b('') self._bytes = self._conn.sock.recv(1) return False except http_client.socket.error as e: # Check why recv failed # Windows machines are the error codes # that start with 1 # (http://msdn.microsoft.com/en-ca/library/windows/desktop/ms740668(v=vs.85).aspx) if e.errno == 35 or e.errno == 10035: # This is the "Resource temporarily unavailable" error # which is thrown cuz there was nothing to receive, i.e. # the server hasn't returned a response yet. # This is a non-fatal error and the operation # should be tried again. # So, assume that the connection is still open. return True elif e.errno == 54 or e.errno == 10054: # This is the "Connection reset by peer" error # which is thrown cuz the server reset the # socket, so the connection is closed. return False elif e.errno == 11: # This is the "Resource temporarily unavailable" error # which happens because the "operation would have blocked # but nonblocking operation was requested". # We require non-blocking reading of this socket because # we don't want to wait around for a response, we just # want to see if a response is currently available. So # let's just assume that we're still connected and # hopefully recieve some data on the next try. return True elif isinstance(e, ssl.SSLError): if e.errno == 2: # errno 2 occurs when trying to read or write data, but more # data needs to be received on the underlying TCP transport # before the request can be fulfilled. # # Python 2.7.9+ and Python 3.3+ give this its own exception, # SSLWantReadError return True raise e else: # Unknown scenario raise e def _reconnect(self): ''' Connect if disconnected. Retry self.maxtries times with delays ''' if not self._isconnected(): try: self._connect() except http_client.socket.error as e: # Attempt to reconnect if the connection was refused if e.errno == 61 or e.errno == 10061: # errno 61 is the "Connection Refused" error time.sleep(self._delay) self._delay += self._delay # fibonacii delays self._tries += 1 if self._tries < self.maxtries: self._reconnect() else: self._reset_retries() raise e else: # Unknown scenario raise e # Reconnect worked - reset _closed self._closed = False def _reset_retries(self): ''' Reset the connect counters and delays ''' self._tries = 0 self._delay = 1 class _FakeSocket(six.StringIO): # Used to construct a http_client.HTTPResponse object # from a string. # Thx to: http://pythonwise.blogspot.ca/2010/02/parse-http-response.html def makefile(self, *args, **kwargs): return self plotly-2.2.3+dfsg.orig/plotly/plotly/chunked_requests/__init__.py 0000644 0001750 0001750 00000000044 13104403274 024576 0 ustar noahfx noahfx from . chunked_request import Stream plotly-2.2.3+dfsg.orig/plotly/plotly/__init__.py 0000644 0001750 0001750 00000001076 13174436015 021236 0 ustar noahfx noahfx """ plotly ====== This module defines functionality that requires interaction between your local machine and Plotly. Almost all functionality used here will require a verifiable account (username/api-key pair) and a network connection. """ from . plotly import ( sign_in, update_plot_options, get_credentials, iplot, plot, iplot_mpl, plot_mpl, get_figure, Stream, image, grid_ops, meta_ops, file_ops, get_config, get_grid, dashboard_ops, presentation_ops, create_animations, icreate_animations ) plotly-2.2.3+dfsg.orig/plotly/presentation_objs/ 0000755 0001750 0001750 00000000000 13211605515 021321 5 ustar noahfx noahfx plotly-2.2.3+dfsg.orig/plotly/presentation_objs/presentation_objs.py 0000644 0001750 0001750 00000123504 13174472664 025447 0 ustar noahfx noahfx """ dashboard_objs ========== A module for creating and manipulating spectacle-presentation dashboards. """ import copy import random import re import string import warnings from plotly import exceptions from plotly.config import get_config HEIGHT = 700.0 WIDTH = 1000.0 CODEPANE_THEMES = ['tomorrow', 'tomorrowNight'] VALID_LANGUAGES = ['cpp', 'cs', 'css', 'fsharp', 'go', 'haskell', 'java', 'javascript', 'jsx', 'julia', 'xml', 'matlab', 'php', 'python', 'r', 'ruby', 'scala', 'sql', 'yaml'] VALID_TRANSITIONS = ['slide', 'zoom', 'fade', 'spin'] PRES_THEMES = ['moods', 'martik'] VALID_GROUPTYPES = [ 'leftgroup_v', 'rightgroup_v', 'middle', 'checkerboard_topleft', 'checkerboard_topright' ] fontWeight_dict = { 'Thin': {'fontWeight': 100}, 'Thin Italic': {'fontWeight': 100, 'fontStyle': 'italic'}, 'Light': {'fontWeight': 300}, 'Light Italic': {'fontWeight': 300, 'fontStyle': 'italic'}, 'Regular': {'fontWeight': 400}, 'Regular Italic': {'fontWeight': 400, 'fontStyle': 'italic'}, 'Medium': {'fontWeight': 500}, 'Medium Italic': {'fontWeight': 500, 'fontStyle': 'italic'}, 'Bold': {'fontWeight': 700}, 'Bold Italic': {'fontWeight': 700, 'fontStyle': 'italic'}, 'Black': {'fontWeight': 900}, 'Black Italic': {'fontWeight': 900, 'fontStyle': 'italic'}, } def list_of_options(iterable, conj='and', period=True): """ Returns an English listing of objects seperated by commas ',' For example, ['foo', 'bar', 'baz'] becomes 'foo, bar and baz' if the conjunction 'and' is selected. """ if len(iterable) < 2: raise exceptions.PlotlyError( 'Your list or tuple must contain at least 2 items.' ) template = (len(iterable) - 2)*'{}, ' + '{} ' + conj + ' {}' + period*'.' return template.format(*iterable) # Error Messages STYLE_ERROR = "Your presentation style must be {}".format( list_of_options(PRES_THEMES, conj='or', period=True) ) CODE_ENV_ERROR = ( "If you are putting a block of code into your markdown " "presentation, make sure your denote the start and end " "of the code environment with the '```' characters. For " "example, your markdown string would include something " "like:\n\n```python\nx = 2\ny = 1\nprint x\n```\n\n" "Notice how the language that you want the code to be " "displayed in is immediately to the right of first " "entering '```', i.e. '```python'." ) LANG_ERROR = ( "The language of your code block should be " "clearly indicated after the first ``` that " "begins the code block. The valid languages to " "choose from are" + list_of_options( VALID_LANGUAGES ) ) def _generate_id(size): letters_and_numbers = string.ascii_letters for num in range(10): letters_and_numbers += str(num) letters_and_numbers += str(num) id_str = '' for _ in range(size): id_str += random.choice(list(letters_and_numbers)) return id_str paragraph_styles = { 'Body': { 'color': '#3d3d3d', 'fontFamily': 'Open Sans', 'fontSize': 11, 'fontStyle': 'normal', 'fontWeight': 400, 'lineHeight': 'normal', 'minWidth': 20, 'opacity': 1, 'textAlign': 'center', 'textDecoration': 'none', 'wordBreak': 'break-word' }, 'Body Small': { 'color': '#3d3d3d', 'fontFamily': 'Open Sans', 'fontSize': 10, 'fontStyle': 'normal', 'fontWeight': 400, 'lineHeight': 'normal', 'minWidth': 20, 'opacity': 1, 'textAlign': 'center', 'textDecoration': 'none' }, 'Caption': { 'color': '#3d3d3d', 'fontFamily': 'Open Sans', 'fontSize': 11, 'fontStyle': 'italic', 'fontWeight': 400, 'lineHeight': 'normal', 'minWidth': 20, 'opacity': 1, 'textAlign': 'center', 'textDecoration': 'none' }, 'Heading 1': { 'color': '#3d3d3d', 'fontFamily': 'Open Sans', 'fontSize': 26, 'fontStyle': 'normal', 'fontWeight': 400, 'lineHeight': 'normal', 'minWidth': 20, 'opacity': 1, 'textAlign': 'center', 'textDecoration': 'none', }, 'Heading 2': { 'color': '#3d3d3d', 'fontFamily': 'Open Sans', 'fontSize': 20, 'fontStyle': 'normal', 'fontWeight': 400, 'lineHeight': 'normal', 'minWidth': 20, 'opacity': 1, 'textAlign': 'center', 'textDecoration': 'none' }, 'Heading 3': { 'color': '#3d3d3d', 'fontFamily': 'Open Sans', 'fontSize': 11, 'fontStyle': 'normal', 'fontWeight': 700, 'lineHeight': 'normal', 'minWidth': 20, 'opacity': 1, 'textAlign': 'center', 'textDecoration': 'none' } } def _empty_slide(transition, id): empty_slide = {'children': [], 'id': id, 'props': {'style': {}, 'transition': transition}} return empty_slide def _box(boxtype, text_or_url, left, top, height, width, id, props_attr, style_attr, paragraphStyle): children_list = [] fontFamily = "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace" if boxtype == 'Text': children_list = text_or_url.split('\n') props = { 'isQuote': False, 'listType': None, 'paragraphStyle': paragraphStyle, 'size': 4, 'style': copy.deepcopy(paragraph_styles[paragraphStyle]) } props['style'].update( {'height': height, 'left': left, 'top': top, 'width': width, 'position': 'absolute'} ) elif boxtype == 'Image': # height, width are set to default 512 # as set by the Presentation Editor props = { 'height': 512, 'imageName': None, 'src': text_or_url, 'style': {'height': height, 'left': left, 'opacity': 1, 'position': 'absolute', 'top': top, 'width': width}, 'width': 512 } elif boxtype == 'Plotly': if '?share_key' in text_or_url: src = text_or_url else: src = text_or_url + '.embed?link=false' props = { 'frameBorder': 0, 'scrolling': 'no', 'src': src, 'style': {'height': height, 'left': left, 'position': 'absolute', 'top': top, 'width': width} } elif boxtype == 'CodePane': props = { 'language': 'python', 'source': text_or_url, 'style': {'fontFamily': fontFamily, 'fontSize': 13, 'height': height, 'left': left, 'margin': 0, 'position': 'absolute', 'textAlign': 'left', 'top': top, 'width': width}, 'theme': 'tomorrowNight' } # update props and style attributes for item in props_attr.items(): props[item[0]] = item[1] for item in style_attr.items(): props['style'][item[0]] = item[1] child = { 'children': children_list, 'id': id, 'props': props, 'type': boxtype } if boxtype == 'Text': child['defaultHeight'] = 36 child['defaultWidth'] = 52 child['resizeVertical'] = False if boxtype == 'CodePane': child['defaultText'] = 'Code' return child def _percentage_to_pixel(value, side): if side == 'left': return WIDTH * (0.01 * value) elif side == 'top': return HEIGHT * (0.01 * value) elif side == 'height': return HEIGHT * (0.01 * value) elif side == 'width': return WIDTH * (0.01 * value) def _return_box_position(left, top, height, width): values_dict = { 'left': left, 'top': top, 'height': height, 'width': width, } for key in iter(values_dict): if isinstance(values_dict[key], str): var = float(values_dict[key][: -2]) else: var = _percentage_to_pixel(values_dict[key], key) values_dict[key] = var return (values_dict['left'], values_dict['top'], values_dict['height'], values_dict['width']) def _remove_extra_whitespace_from_line(line): line = line.lstrip() line = line.rstrip() return line def _list_of_slides(markdown_string): if not markdown_string.endswith('\n---\n'): markdown_string += '\n---\n' text_blocks = re.split('\n-{2,}\n', markdown_string) list_of_slides = [] for text in text_blocks: if not all(char in ['\n', '-', ' '] for char in text): list_of_slides.append(text) if '\n-\n' in markdown_string: msg = ("You have at least one '-' by itself on its own line in your " "markdown string. If you are trying to denote a new slide, " "make sure that the line has 3 '-'s like this: \n\n---\n\n" "A new slide will NOT be created here.") warnings.warn(msg) return list_of_slides def _top_spec_for_text_at_bottom(text_block, width_per, per_from_bottom=0, min_top=30): # This function ensures that if there is a large block of # text in your slide it will not overflow off the bottom # of the slide. # The input for this function are a block of text and the # params that define where it will be placed in the slide. # The function makes some calculations and will output a # 'top' value (i.e. the left, top, height, width css params) # so that the text block will come down to some specified # distance from the bottom of the page. # TODO: customize this function for different fonts/sizes max_lines = 37 one_char_percent_width = 0.764 chars_in_full_line = width_per / one_char_percent_width num_of_lines = 0 char_group = 0 for char in text_block: if char == '\n': num_of_lines += 1 char_group = 0 else: if char_group >= chars_in_full_line: char_group = 0 num_of_lines += 1 else: char_group += 1 num_of_lines += 1 top_frac = (max_lines - num_of_lines) / float(max_lines) top = top_frac * 100 - per_from_bottom # to be safe return max(top, min_top) def _box_specs_gen(num_of_boxes, grouptype='leftgroup_v', width_range=50, height_range=50, margin=2, betw_boxes=4, middle_center=50): # the (left, top, width, height) specs # are added to specs_for_boxes specs_for_boxes = [] if num_of_boxes == 1 and grouptype in ['leftgroup_v', 'rightgroup_v']: if grouptype == 'rightgroup_v': left_shift = (100 - width_range) else: left_shift = 0 box_spec = ( left_shift + (margin / WIDTH) * 100, (margin / HEIGHT) * 100, 100 - (2 * margin / HEIGHT * 100), width_range - (2 * margin / WIDTH) * 100 ) specs_for_boxes.append(box_spec) elif num_of_boxes > 1 and grouptype in ['leftgroup_v', 'rightgroup_v']: if grouptype == 'rightgroup_v': left_shift = (100 - width_range) else: left_shift = 0 if num_of_boxes % 2 == 0: box_width_px = 0.5 * ( (float(width_range)/100) * WIDTH - 2 * margin - betw_boxes ) box_width = (box_width_px / WIDTH) * 100 height = (200.0 / (num_of_boxes * HEIGHT)) * ( HEIGHT - (num_of_boxes / 2 - 1) * betw_boxes - 2 * margin ) left1 = left_shift + (margin / WIDTH) * 100 left2 = left_shift + ( ((margin + betw_boxes) / WIDTH) * 100 + box_width ) for left in [left1, left2]: for j in range(int(num_of_boxes / 2)): top = (margin * 100 / HEIGHT) + j * ( height + (betw_boxes * 100 / HEIGHT) ) specs = ( left, top, height, box_width ) specs_for_boxes.append(specs) if num_of_boxes % 2 == 1: width = width_range - (200 * margin) / WIDTH height = (100.0 / (num_of_boxes * HEIGHT)) * ( HEIGHT - (num_of_boxes - 1) * betw_boxes - 2 * margin ) left = left_shift + (margin / WIDTH) * 100 for j in range(num_of_boxes): top = (margin / HEIGHT) * 100 + j * ( height + (betw_boxes / HEIGHT) * 100 ) specs = ( left, top, height, width ) specs_for_boxes.append(specs) elif grouptype == 'middle': top = float(middle_center - (height_range / 2)) height = height_range width = (1 / float(num_of_boxes)) * ( width_range - (num_of_boxes - 1) * (100*betw_boxes/WIDTH) ) for j in range(num_of_boxes): left = ((100 - float(width_range)) / 2) + j * ( width + (betw_boxes / WIDTH) * 100 ) specs = (left, top, height, width) specs_for_boxes.append(specs) elif 'checkerboard' in grouptype and num_of_boxes == 2: if grouptype == 'checkerboard_topleft': for j in range(2): left = j * 50 top = j * 50 height = 50 width = 50 specs = ( left, top, height, width ) specs_for_boxes.append(specs) else: for j in range(2): left = 50 * (1 - j) top = j * 50 height = 50 width = 50 specs = ( left, top, height, width ) specs_for_boxes.append(specs) return specs_for_boxes def _return_layout_specs(num_of_boxes, url_lines, title_lines, text_block, code_blocks, slide_num, style): # returns specs of the form (left, top, height, width) code_theme = 'tomorrowNight' if style == 'martik': specs_for_boxes = [] margin = 18 # in pxs # set Headings styles paragraph_styles['Heading 1'].update( {'color': '#0D0A1E', 'fontFamily': 'Raleway', 'fontSize': 55, 'fontWeight': fontWeight_dict['Bold']['fontWeight']} ) paragraph_styles['Heading 2'] = copy.deepcopy( paragraph_styles['Heading 1'] ) paragraph_styles['Heading 2'].update({'fontSize': 36}) paragraph_styles['Heading 3'] = copy.deepcopy( paragraph_styles['Heading 1'] ) paragraph_styles['Heading 3'].update({'fontSize': 30}) # set Body style paragraph_styles['Body'].update( {'color': '#96969C', 'fontFamily': 'Roboto', 'fontSize': 16, 'fontWeight': fontWeight_dict['Regular']['fontWeight']} ) bkgd_color = '#F4FAFB' title_font_color = '#0D0A1E' text_font_color = '#96969C' if num_of_boxes == 0 and slide_num == 0: text_textAlign = 'center' else: text_textAlign = 'left' if num_of_boxes == 0: specs_for_title = (0, 50, 20, 100) specs_for_text = (15, 60, 50, 70) bkgd_color = '#0D0A1E' title_font_color = '#F4FAFB' text_font_color = '#F4FAFB' elif num_of_boxes == 1: if code_blocks != [] or (url_lines != [] and get_config()['plotly_domain'] in url_lines[0]): if code_blocks != []: w_range = 40 else: w_range = 60 text_top = _top_spec_for_text_at_bottom( text_block, 80, per_from_bottom=(margin / HEIGHT) * 100 ) specs_for_title = (0, 3, 20, 100) specs_for_text = (10, text_top, 30, 80) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='middle', width_range=w_range, height_range=60, margin=margin, betw_boxes=4 ) bkgd_color = '#0D0A1E' title_font_color = '#F4FAFB' text_font_color = '#F4FAFB' code_theme = 'tomorrow' elif title_lines == [] and text_block == '': specs_for_title = (0, 50, 20, 100) specs_for_text = (15, 60, 50, 70) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='middle', width_range=50, height_range=80, margin=0, betw_boxes=0 ) else: title_text_width = 40 - (margin / WIDTH) * 100 text_top = _top_spec_for_text_at_bottom( text_block, title_text_width, per_from_bottom=(margin / HEIGHT) * 100 ) specs_for_title = (60, 3, 20, 40) specs_for_text = (60, text_top, 1, title_text_width) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='leftgroup_v', width_range=60, margin=margin, betw_boxes=4 ) bkgd_color = '#0D0A1E' title_font_color = '#F4FAFB' text_font_color = '#F4FAFB' elif num_of_boxes == 2 and url_lines != []: text_top = _top_spec_for_text_at_bottom( text_block, 46, per_from_bottom=(margin / HEIGHT) * 100, min_top=50 ) specs_for_title = (0, 3, 20, 50) specs_for_text = (52, text_top, 40, 46) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='checkerboard_topright' ) elif num_of_boxes >= 2 and url_lines == []: text_top = _top_spec_for_text_at_bottom( text_block, 92, per_from_bottom=(margin / HEIGHT) * 100, min_top=15 ) if num_of_boxes == 2: betw_boxes = 90 else: betw_boxes = 10 specs_for_title = (0, 3, 20, 100) specs_for_text = (4, text_top, 1, 92) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='middle', width_range=92, height_range=60, margin=margin, betw_boxes=betw_boxes ) code_theme = 'tomorrow' else: text_top = _top_spec_for_text_at_bottom( text_block, 40 - (margin / WIDTH) * 100, per_from_bottom=(margin / HEIGHT) * 100 ) specs_for_title = (0, 3, 20, 40 - (margin / WIDTH) * 100) specs_for_text = ( (margin / WIDTH) * 100, text_top, 50, 40 - (margin / WIDTH) * 100 ) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='rightgroup_v', width_range=60, margin=margin, betw_boxes=4 ) elif style == 'moods': specs_for_boxes = [] margin = 18 code_theme = 'tomorrowNight' # set Headings styles paragraph_styles['Heading 1'].update( {'color': '#000016', 'fontFamily': 'Roboto', 'fontSize': 55, 'fontWeight': fontWeight_dict['Black']['fontWeight']} ) paragraph_styles['Heading 2'] = copy.deepcopy( paragraph_styles['Heading 1'] ) paragraph_styles['Heading 2'].update({'fontSize': 36}) paragraph_styles['Heading 3'] = copy.deepcopy( paragraph_styles['Heading 1'] ) paragraph_styles['Heading 3'].update({'fontSize': 30}) # set Body style paragraph_styles['Body'].update( {'color': '#000016', 'fontFamily': 'Roboto', 'fontSize': 16, 'fontWeight': fontWeight_dict['Thin']['fontWeight']} ) bkgd_color = '#FFFFFF' title_font_color = None text_font_color = None if num_of_boxes == 0 and slide_num == 0: text_textAlign = 'center' else: text_textAlign = 'left' if num_of_boxes == 0: if slide_num == 0 or text_block == '': bkgd_color = '#F7F7F7' specs_for_title = (0, 50, 20, 100) specs_for_text = (15, 60, 50, 70) else: bkgd_color = '#F7F7F7' text_top = _top_spec_for_text_at_bottom( text_block, width_per=90, per_from_bottom=(margin / HEIGHT) * 100, min_top=20 ) specs_for_title = (0, 2, 20, 100) specs_for_text = (5, text_top, 50, 90) elif num_of_boxes == 1: if code_blocks != []: # code if text_block == '': margin = 5 specs_for_title = (0, 3, 20, 100) specs_for_text = (0, 0, 0, 0) top = 12 specs_for_boxes = [ (margin, top, 100 - top - margin, 100 - 2 * margin) ] elif slide_num % 2 == 0: # middle center width_per = 90 height_range = 60 text_top = _top_spec_for_text_at_bottom( text_block, width_per=width_per, per_from_bottom=(margin / HEIGHT) * 100, min_top=100 - height_range / 2. ) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='middle', width_range=50, height_range=60, margin=margin, ) specs_for_title = (0, 3, 20, 100) specs_for_text = ( 5, text_top, 2, width_per ) else: # right width_per = 50 text_top = _top_spec_for_text_at_bottom( text_block, width_per=width_per, per_from_bottom=(margin / HEIGHT) * 100, min_top=30 ) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='rightgroup_v', width_range=50, margin=40, ) specs_for_title = (0, 3, 20, 50) specs_for_text = ( 2, text_top, 2, width_per - 2 ) elif (url_lines != [] and get_config()['plotly_domain'] in url_lines[0]): # url if slide_num % 2 == 0: # top half width_per = 95 text_top = _top_spec_for_text_at_bottom( text_block, width_per=width_per, per_from_bottom=(margin / HEIGHT) * 100, min_top=60 ) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='middle', width_range=100, height_range=60, middle_center=30 ) specs_for_title = (0, 60, 20, 100) specs_for_text = ( 2.5, text_top, 2, width_per ) else: # middle across width_per = 95 text_top = _top_spec_for_text_at_bottom( text_block, width_per=width_per, per_from_bottom=(margin / HEIGHT) * 100, min_top=60 ) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='middle', width_range=100, height_range=60 ) specs_for_title = (0, 3, 20, 100) specs_for_text = ( 2.5, text_top, 2, width_per ) else: # image if slide_num % 2 == 0: # right width_per = 50 text_top = _top_spec_for_text_at_bottom( text_block, width_per=width_per, per_from_bottom=(margin / HEIGHT) * 100, min_top=30 ) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='rightgroup_v', width_range=50, margin=0, ) specs_for_title = (0, 3, 20, 50) specs_for_text = ( 2, text_top, 2, width_per - 2 ) else: # left width_per = 50 text_top = _top_spec_for_text_at_bottom( text_block, width_per=width_per, per_from_bottom=(margin / HEIGHT) * 100, min_top=30 ) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='leftgroup_v', width_range=50, margin=0, ) specs_for_title = (50, 3, 20, 50) specs_for_text = ( 52, text_top, 2, width_per - 2 ) elif num_of_boxes == 2: # right stack width_per = 50 text_top = _top_spec_for_text_at_bottom( text_block, width_per=width_per, per_from_bottom=(margin / HEIGHT) * 100, min_top=30 ) specs_for_boxes = [(50, 0, 50, 50), (50, 50, 50, 50)] specs_for_title = (0, 3, 20, 50) specs_for_text = ( 2, text_top, 2, width_per - 2 ) elif num_of_boxes == 3: # middle top width_per = 95 text_top = _top_spec_for_text_at_bottom( text_block, width_per=width_per, per_from_bottom=(margin / HEIGHT) * 100, min_top=40 ) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='middle', width_range=100, height_range=40, middle_center=30 ) specs_for_title = (0, 0, 20, 100) specs_for_text = ( 2.5, text_top, 2, width_per ) else: # right stack width_per = 40 text_top = _top_spec_for_text_at_bottom( text_block, width_per=width_per, per_from_bottom=(margin / HEIGHT) * 100, min_top=30 ) specs_for_boxes = _box_specs_gen( num_of_boxes, grouptype='rightgroup_v', width_range=60, margin=0, ) specs_for_title = (0, 3, 20, 40) specs_for_text = ( 2, text_top, 2, width_per - 2 ) # set text style attributes title_style_attr = {} text_style_attr = {'textAlign': text_textAlign} if text_font_color: text_style_attr['color'] = text_font_color if title_font_color: title_style_attr['color'] = title_font_color return (specs_for_boxes, specs_for_title, specs_for_text, bkgd_color, title_style_attr, text_style_attr, code_theme) def _url_parens_contained(url_name, line): return line.startswith(url_name + '(') and line.endswith(')') class Presentation(dict): """ The Presentation class for creating spectacle-presentations. The Presentations API is a means for creating JSON blobs which are then converted Spectacle Presentations. To use the API you only need to define a block string and define your slides using markdown. Then you can upload your presentation to the Plotly Server. Rules for your presentation string: - use '---' to denote a slide break. - headers work as per usual, where if '#' is used before a line of text then it is interpretted as a header. Only the first header in a slide is displayed on the slide. There are only 3 heading sizes: #, ## and ###. 4 or more hashes will be interpretted as ###. - you can set the type of slide transition you want by writing a line that starts with 'transition: ' before your first header line in the slide, and write the types of transition you want after. Your transition to choose from are 'slide', 'zoom', 'fade' and 'spin'. - to insert a Plotly chart into your slide, write a line that has the form Plotly(url) with your url pointing to your chart. Note that it is STRONGLY advised that your chart has fig['layout']['autosize'] = True. - to insert an image from the web, write a line with the form Image(url) - to insert a block of text, begin with a line that denotes the code envoronment '```lang' where lang is a valid programming language. To find the valid languages run:\n 'plotly.presentation_objs.presentation_objs.VALID_LANGUAGES'\n To end the code block environment, write a single '```' line. All Plotly(url) and Image(url) lines will NOT be interpretted as a Plotly or Image url if they are in the code block. :param (str) markdown_string: the block string that denotes the slides, slide properties, and images to be placed in the presentation. If 'markdown_string' is set to 'None', the JSON for a presentation with one empty slide will be created. :param (str) style: the theme that the presentation will take on. The themes that are available now are 'martik' and 'moods'. Default = 'moods'. :param (bool) imgStretch: if set to False, all images in the presentation will not have heights and widths that will not exceed the parent container they belong to. In other words, images will keep their original aspect ratios. Default = True. For examples see the documentation:\n https://plot.ly/python/presentations-api/ """ def __init__(self, markdown_string=None, style='moods', imgStretch=True): self['presentation'] = { 'slides': [], 'slidePreviews': [None for _ in range(496)], 'version': '0.1.3', 'paragraphStyles': paragraph_styles } if markdown_string: if style not in PRES_THEMES: raise exceptions.PlotlyError( "Your presentation style must be {}".format( list_of_options(PRES_THEMES, conj='or', period=True) ) ) self._markdown_to_presentation(markdown_string, style, imgStretch) else: self._add_empty_slide() def _markdown_to_presentation(self, markdown_string, style, imgStretch): list_of_slides = _list_of_slides(markdown_string) for slide_num, slide in enumerate(list_of_slides): lines_in_slide = slide.split('\n') title_lines = [] # validate blocks of code if slide.count('```') % 2 != 0: raise exceptions.PlotlyError(CODE_ENV_ERROR) # find code blocks code_indices = [] code_blocks = [] wdw_size = len('```') for j in range(len(slide)): if slide[j:j+wdw_size] == '```': code_indices.append(j) for k in range(int(len(code_indices) / 2)): code_blocks.append( slide[code_indices[2 * k]:code_indices[(2 * k) + 1]] ) lang_and_code_tuples = [] for code_block in code_blocks: # validate code blocks code_by_lines = code_block.split('\n') language = _remove_extra_whitespace_from_line( code_by_lines[0][3:] ).lower() if language == '' or language not in VALID_LANGUAGES: raise exceptions.PlotlyError( "The language of your code block should be " "clearly indicated after the first ``` that " "begins the code block. The valid languages to " "choose from are" + list_of_options( VALID_LANGUAGES ) ) lang_and_code_tuples.append( (language, '\n'.join(code_by_lines[1:])) ) # collect text, code and urls title_lines = [] url_lines = [] text_lines = [] inCode = False for line in lines_in_slide: # inCode handling if line[:3] == '```' and len(line) > 3: inCode = True if line == '```': inCode = False if not inCode and line != '```': if len(line) > 0 and line[0] == '#': title_lines.append(line) elif (_url_parens_contained('Plotly', line) or _url_parens_contained('Image', line)): if (line.startswith('Plotly(') and get_config()['plotly_domain'] not in line): raise exceptions.PlotlyError( "You are attempting to insert a Plotly Chart " "in your slide but your url does not have " "your plotly domain '{}' in it.".format( get_config()['plotly_domain'] ) ) url_lines.append(line) else: # find and set transition properties trans = 'transition:' if line.startswith(trans) and title_lines == []: slide_trans = line[len(trans):] slide_trans = _remove_extra_whitespace_from_line( slide_trans ) slide_transition_list = [] for key in VALID_TRANSITIONS: if key in slide_trans: slide_transition_list.append(key) if slide_transition_list == []: slide_transition_list.append('slide') self._set_transition( slide_transition_list, slide_num ) else: text_lines.append(line) # make text block for i in range(2): try: while text_lines[-i] == '': text_lines.pop(-i) except IndexError: pass text_block = '\n'.join(text_lines) num_of_boxes = len(url_lines) + len(lang_and_code_tuples) (specs_for_boxes, specs_for_title, specs_for_text, bkgd_color, title_style_attr, text_style_attr, code_theme) = _return_layout_specs( num_of_boxes, url_lines, title_lines, text_block, code_blocks, slide_num, style ) # background color self._color_background(bkgd_color, slide_num) # insert title, text, code, and images if len(title_lines) > 0: # clean titles title = title_lines[0] num_hashes = 0 while title[0] == '#': title = title[1:] num_hashes += 1 title = _remove_extra_whitespace_from_line(title) self._insert( box='Text', text_or_url=title, left=specs_for_title[0], top=specs_for_title[1], height=specs_for_title[2], width=specs_for_title[3], slide=slide_num, style_attr=title_style_attr, paragraphStyle='Heading 1'.format( min(num_hashes, 3) ) ) # text if len(text_lines) > 0: self._insert( box='Text', text_or_url=text_block, left=specs_for_text[0], top=specs_for_text[1], height=specs_for_text[2], width=specs_for_text[3], slide=slide_num, style_attr=text_style_attr, paragraphStyle='Body' ) url_and_code_blocks = list(url_lines + lang_and_code_tuples) for k, specs in enumerate(specs_for_boxes): url_or_code = url_and_code_blocks[k] if isinstance(url_or_code, tuple): # code language = url_or_code[0] code = url_or_code[1] box_name = 'CodePane' # code style props_attr = {} props_attr['language'] = language props_attr['theme'] = code_theme self._insert(box=box_name, text_or_url=code, left=specs[0], top=specs[1], height=specs[2], width=specs[3], slide=slide_num, props_attr=props_attr) else: # url if get_config()['plotly_domain'] in url_or_code: box_name = 'Plotly' else: box_name = 'Image' url = url_or_code[len(box_name) + 1: -1] self._insert(box=box_name, text_or_url=url, left=specs[0], top=specs[1], height=specs[2], width=specs[3], slide=slide_num) if not imgStretch: for s, slide in enumerate(self['presentation']['slides']): for c, child in enumerate(slide['children']): if child['type'] in ['Image', 'Plotly']: deep_child = child['props']['style'] width = deep_child['width'] height = deep_child['height'] if width >= height: deep_child['max-width'] = deep_child.pop('width') else: deep_child['max-height'] = deep_child.pop('height') def _add_empty_slide(self): self['presentation']['slides'].append( _empty_slide(['slide'], _generate_id(9)) ) def _add_missing_slides(self, slide): # add slides if desired slide number isn't in the presentation try: self['presentation']['slides'][slide]['children'] except IndexError: num_of_slides = len(self['presentation']['slides']) for _ in range(slide - num_of_slides + 1): self._add_empty_slide() def _insert(self, box, text_or_url, left, top, height, width, slide=0, props_attr={}, style_attr={}, paragraphStyle=None): self._add_missing_slides(slide) left, top, height, width = _return_box_position(left, top, height, width) new_id = _generate_id(9) child = _box(box, text_or_url, left, top, height, width, new_id, props_attr, style_attr, paragraphStyle) self['presentation']['slides'][slide]['children'].append(child) def _color_background(self, color, slide): self._add_missing_slides(slide) loc = self['presentation']['slides'][slide] loc['props']['style']['backgroundColor'] = color def _background_image(self, url, slide, bkrd_image_dict): self._add_missing_slides(slide) loc = self['presentation']['slides'][slide]['props'] # default settings size = 'stretch' repeat = 'no-repeat' if 'background-size:' in bkrd_image_dict: size = bkrd_image_dict['background-size:'] if 'background-repeat:' in bkrd_image_dict: repeat = bkrd_image_dict['background-repeat:'] if size == 'stretch': backgroundSize = '100% 100%' elif size == 'original': backgroundSize = 'auto' elif size == 'contain': backgroundSize = 'contain' elif size == 'cover': backgroundSize = 'cover' style = { 'backgroundImage': 'url({})'.format(url), 'backgroundPosition': 'center center', 'backgroundRepeat': repeat, 'backgroundSize': backgroundSize } for item in style.items(): loc['style'].setdefault(item[0], item[1]) loc['backgroundImageSrc'] = url loc['backgroundImageName'] = None def _set_transition(self, transition, slide): self._add_missing_slides(slide) loc = self['presentation']['slides'][slide]['props'] loc['transition'] = transition plotly-2.2.3+dfsg.orig/plotly/presentation_objs/__init__.py 0000644 0001750 0001750 00000000211 13174436015 023431 0 ustar noahfx noahfx """ presentation_objs A wrapper for the spectacle-presentations endpoint. =========== """ from . presentation_objs import Presentation plotly-2.2.3+dfsg.orig/plotly/dashboard_objs/ 0000755 0001750 0001750 00000000000 13211605515 020535 5 ustar noahfx noahfx plotly-2.2.3+dfsg.orig/plotly/dashboard_objs/dashboard_objs.py 0000644 0001750 0001750 00000044760 13104403274 024065 0 ustar noahfx noahfx """ dashboard_objs ========== A module for creating and manipulating dashboard content. You can create a Dashboard object, insert boxes, swap boxes, remove a box and get an HTML preview of the Dashboard. ``` """ import pprint from plotly import exceptions, optional_imports from plotly.utils import node_generator IPython = optional_imports.get_module('IPython') # default HTML parameters MASTER_WIDTH = 400 MASTER_HEIGHT = 400 FONT_SIZE = 10 ID_NOT_VALID_MESSAGE = ( "Your box_id must be a number in your dashboard. To view a " "representation of your dashboard run get_preview()." ) def _empty_box(): empty_box = { 'type': 'box', 'boxType': 'empty' } return empty_box def _box(fileId='', shareKey=None, title=''): box = { 'type': 'box', 'boxType': 'plot', 'fileId': fileId, 'shareKey': shareKey, 'title': title } return box def _container(box_1=None, box_2=None, size=MASTER_HEIGHT, sizeUnit='px', direction='vertical'): if box_1 is None: box_1 = _empty_box() if box_2 is None: box_2 = _empty_box() container = { 'type': 'split', 'size': size, 'sizeUnit': sizeUnit, 'direction': direction, 'first': box_1, 'second': box_2 } return container dashboard_html = ("""
""".format(width=MASTER_WIDTH, height=MASTER_HEIGHT)) def _draw_line_through_box(dashboard_html, top_left_x, top_left_y, box_w, box_h, direction='vertical'): is_horizontal = (direction == 'horizontal') if is_horizontal: new_top_left_x = top_left_x + box_w / 2 new_top_left_y = top_left_y new_box_w = 1 new_box_h = box_h else: new_top_left_x = top_left_x new_top_left_y = top_left_y + box_h / 2 new_box_w = box_w new_box_h = 1 html_box = """ context.beginPath(); context.rect({top_left_x}, {top_left_y}, {box_w}, {box_h}); context.lineWidth = 1; context.strokeStyle = 'black'; context.stroke(); """.format(top_left_x=new_top_left_x, top_left_y=new_top_left_y, box_w=new_box_w, box_h=new_box_h) index_for_new_box = dashboard_html.find('') - 1 dashboard_html = (dashboard_html[:index_for_new_box] + html_box + dashboard_html[index_for_new_box:]) return dashboard_html def _add_html_text(dashboard_html, text, top_left_x, top_left_y, box_w, box_h): html_text = """ context.font = '{font_size}pt Times New Roman'; context.textAlign = 'center'; context.fillText({text}, {top_left_x} + 0.5*{box_w}, {top_left_y} + 0.5*{box_h}); """.format(text=text, top_left_x=top_left_x, top_left_y=top_left_y, box_w=box_w, box_h=box_h, font_size=FONT_SIZE) index_to_add_text = dashboard_html.find('') - 1 dashboard_html = (dashboard_html[:index_to_add_text] + html_text + dashboard_html[index_to_add_text:]) return dashboard_html class Dashboard(dict): """ Dashboard class for creating interactive dashboard objects. Dashboards are dicts that contain boxes which hold plot information. These boxes can be arranged in various ways. The most basic form of a box is: ``` { 'type': 'box', 'boxType': 'plot' } ``` where 'fileId' can be set to the 'username:#' of your plot. The other parameters a box takes are `shareKey` (default is None) and `title` (default is ''). `.get_preview()` should be called quite regularly to get an HTML representation of the dashboard in which the boxes in the HTML are labelled with on-the-fly-generated numbers or box ids which change after each modification to the dashboard. `.get_box()` returns the box located in the dashboard by calling its box id as displayed via `.get_preview()`. Example: Create a simple Dashboard object ``` import plotly.dashboard_objs as dashboard box_1 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:some#', 'title': 'box 1' } box_2 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:some#', 'title': 'box 2' } box_3 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:some#', 'title': 'box 3' } my_dboard = dashboard.Dashboard() my_dboard.insert(box_1) # my_dboard.get_preview() my_dboard.insert(box_2, 'above', 1) # my_dboard.get_preview() my_dboard.insert(box_3, 'left', 2) # my_dboard.get_preview() my_dboard.swap(1, 2) # my_dboard.get_preview() my_dboard.remove(1) # my_dboard.get_preview() ``` """ def __init__(self, content=None): if content is None: content = {} if not content: self['layout'] = None self['version'] = 2 self['settings'] = {} else: self['layout'] = content['layout'] self['version'] = content['version'] self['settings'] = content['settings'] self._set_container_sizes() def _compute_box_ids(self): box_ids_to_path = {} all_nodes = list(node_generator(self['layout'])) for node in all_nodes: if (node[1] != () and node[0]['type'] == 'box' and node[0]['boxType'] != 'empty'): try: max_id = max(box_ids_to_path.keys()) except ValueError: max_id = 0 box_ids_to_path[max_id + 1] = node[1] return box_ids_to_path def _insert(self, box_or_container, path): if any(first_second not in ['first', 'second'] for first_second in path): raise exceptions.PlotlyError( "Invalid path. Your 'path' list must only contain " "the strings 'first' and 'second'." ) if 'first' in self['layout']: loc_in_dashboard = self['layout'] for index, first_second in enumerate(path): if index != len(path) - 1: loc_in_dashboard = loc_in_dashboard[first_second] else: loc_in_dashboard[first_second] = box_or_container else: self['layout'] = box_or_container def _make_all_nodes_and_paths(self): all_nodes = list(node_generator(self['layout'])) # remove path 'second' as it's always an empty box all_paths = [] for node in all_nodes: all_paths.append(node[1]) path_second = ('second',) if path_second in all_paths: all_paths.remove(path_second) return all_nodes, all_paths def _set_container_sizes(self): if self['layout'] is None: return all_nodes, all_paths = self._make_all_nodes_and_paths() # set dashboard_height proportional to max_path_len max_path_len = max(len(path) for path in all_paths) dashboard_height = 500 + 250 * max_path_len self['layout']['size'] = dashboard_height self['layout']['sizeUnit'] = 'px' for path in all_paths: if len(path) != 0: if self._path_to_box(path)['type'] == 'split': self._path_to_box(path)['size'] = 50 self._path_to_box(path)['sizeUnit'] = '%' def _path_to_box(self, path): loc_in_dashboard = self['layout'] for first_second in path: loc_in_dashboard = loc_in_dashboard[first_second] return loc_in_dashboard def get_box(self, box_id): """Returns box from box_id number.""" box_ids_to_path = self._compute_box_ids() loc_in_dashboard = self['layout'] if box_id not in box_ids_to_path.keys(): raise exceptions.PlotlyError(ID_NOT_VALID_MESSAGE) for first_second in box_ids_to_path[box_id]: loc_in_dashboard = loc_in_dashboard[first_second] return loc_in_dashboard def get_preview(self): """ Returns JSON or HTML respresentation of the dashboard. If IPython is not imported, returns a pretty print of the dashboard dict. Otherwise, returns an IPython.core.display.HTML display of the dashboard. The algorithm used to build the HTML preview involves going through the paths of the node generator of the dashboard. The paths of the dashboard are sequenced through from shorter to longer and whether it's a box or container that lies at the end of the path determines the action. If it's a container, draw a line in the figure to divide the current box into two and store the specs of the resulting two boxes. If the path points to a terminal box (often containing a plot), then draw the box id in the center of the box. It's important to note that these box ids are generated on-the-fly and they do not necessarily stay assigned to the boxes they were once assigned to. """ if IPython is None: pprint.pprint(self) return elif self['layout'] is None: return IPython.display.HTML(dashboard_html) x = 0 y = 0 box_w = MASTER_WIDTH box_h = MASTER_HEIGHT html_figure = dashboard_html box_ids_to_path = self._compute_box_ids() # used to store info about box dimensions path_to_box_specs = {} first_box_specs = { 'top_left_x': x, 'top_left_y': y, 'box_w': box_w, 'box_h': box_h } # uses tuples to store paths as for hashable keys path_to_box_specs[('first',)] = first_box_specs # generate all paths all_nodes, all_paths = self._make_all_nodes_and_paths() max_path_len = max(len(path) for path in all_paths) for path_len in range(1, max_path_len + 1): for path in [path for path in all_paths if len(path) == path_len]: current_box_specs = path_to_box_specs[path] if self._path_to_box(path)['type'] == 'split': html_figure = _draw_line_through_box( html_figure, current_box_specs['top_left_x'], current_box_specs['top_left_y'], current_box_specs['box_w'], current_box_specs['box_h'], direction=self._path_to_box(path)['direction'] ) # determine the specs for resulting two boxes from split is_horizontal = ( self._path_to_box(path)['direction'] == 'horizontal' ) x = current_box_specs['top_left_x'] y = current_box_specs['top_left_y'] box_w = current_box_specs['box_w'] box_h = current_box_specs['box_h'] if is_horizontal: new_box_w = box_w / 2 new_box_h = box_h new_top_left_x = x + box_w / 2 new_top_left_y = y else: new_box_w = box_w new_box_h = box_h / 2 new_top_left_x = x new_top_left_y = y + box_h / 2 box_1_specs = { 'top_left_x': x, 'top_left_y': y, 'box_w': new_box_w, 'box_h': new_box_h } box_2_specs = { 'top_left_x': new_top_left_x, 'top_left_y': new_top_left_y, 'box_w': new_box_w, 'box_h': new_box_h } path_to_box_specs[path + ('first',)] = box_1_specs path_to_box_specs[path + ('second',)] = box_2_specs elif self._path_to_box(path)['type'] == 'box': for box_id in box_ids_to_path: if box_ids_to_path[box_id] == path: number = box_id html_figure = _add_html_text( html_figure, number, path_to_box_specs[path]['top_left_x'], path_to_box_specs[path]['top_left_y'], path_to_box_specs[path]['box_w'], path_to_box_specs[path]['box_h'], ) # display HTML representation return IPython.display.HTML(html_figure) def insert(self, box, side='above', box_id=None): """ Insert a box into your dashboard layout. :param (dict) box: the box you are inserting into the dashboard. :param (str) side: specifies where your new box is going to be placed relative to the given 'box_id'. Valid values are 'above', 'below', 'left', and 'right'. :param (int) box_id: the box id which is used as the reference box for the insertion of the box. Example: ``` import plotly.dashboard_objs as dashboard box_1 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:some#', 'title': 'box 1' } my_dboard = dashboard.Dashboard() my_dboard.insert(box_1) my_dboard.insert(box_1, 'left', 1) my_dboard.insert(box_1, 'below', 2) my_dboard.insert(box_1, 'right', 3) my_dboard.insert(box_1, 'above', 4) my_dboard.get_preview() ``` """ box_ids_to_path = self._compute_box_ids() # doesn't need box_id or side specified for first box if self['layout'] is None: self['layout'] = _container(box, _empty_box()) else: if box_id is None: raise exceptions.PlotlyError( "Make sure the box_id is specfied if there is at least " "one box in your dashboard." ) if box_id not in box_ids_to_path: raise exceptions.PlotlyError(ID_NOT_VALID_MESSAGE) if side == 'above': old_box = self.get_box(box_id) self._insert( _container(box, old_box, direction='vertical'), box_ids_to_path[box_id] ) elif side == 'below': old_box = self.get_box(box_id) self._insert( _container(old_box, box, direction='vertical'), box_ids_to_path[box_id] ) elif side == 'left': old_box = self.get_box(box_id) self._insert( _container(box, old_box, direction='horizontal'), box_ids_to_path[box_id] ) elif side == 'right': old_box = self.get_box(box_id) self._insert( _container(old_box, box, direction='horizontal'), box_ids_to_path[box_id] ) else: raise exceptions.PlotlyError( "If there is at least one box in your dashboard, you " "must specify a valid side value. You must choose from " "'above', 'below', 'left', and 'right'." ) self._set_container_sizes() def remove(self, box_id): """ Remove a box from the dashboard by its box_id. Example: ``` import plotly.dashboard_objs as dashboard box_1 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:some#', 'title': 'box 1' } my_dboard = dashboard.Dashboard() my_dboard.insert(box_1) my_dboard.remove(1) my_dboard.get_preview() ``` """ box_ids_to_path = self._compute_box_ids() if box_id not in box_ids_to_path: raise exceptions.PlotlyError(ID_NOT_VALID_MESSAGE) path = box_ids_to_path[box_id] if path != ('first',): container_for_box_id = self._path_to_box(path[:-1]) if path[-1] == 'first': adjacent_path = 'second' elif path[-1] == 'second': adjacent_path = 'first' adjacent_box = container_for_box_id[adjacent_path] self._insert(adjacent_box, path[:-1]) else: self['layout'] = None self._set_container_sizes() def swap(self, box_id_1, box_id_2): """ Swap two boxes with their specified ids. Example: ``` import plotly.dashboard_objs as dashboard box_1 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:first#', 'title': 'first box' } box_2 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:second#', 'title': 'second box' } my_dboard = dashboard.Dashboard() my_dboard.insert(box_1) my_dboard.insert(box_2, 'above', 1) # check box at box id 1 box_at_1 = my_dboard.get_box(1) print(box_at_1) my_dboard.swap(1, 2) box_after_swap = my_dboard.get_box(1) print(box_after_swap) ``` """ box_ids_to_path = self._compute_box_ids() box_1 = self.get_box(box_id_1) box_2 = self.get_box(box_id_2) box_1_path = box_ids_to_path[box_id_1] box_2_path = box_ids_to_path[box_id_2] for pairs in [(box_1_path, box_2), (box_2_path, box_1)]: loc_in_dashboard = self['layout'] for first_second in pairs[0][:-1]: loc_in_dashboard = loc_in_dashboard[first_second] loc_in_dashboard[pairs[0][-1]] = pairs[1] self._set_container_sizes() plotly-2.2.3+dfsg.orig/plotly/dashboard_objs/__init__.py 0000644 0001750 0001750 00000003360 13104403274 022647 0 ustar noahfx noahfx """ dashboard_objs ========== A module for creating and manipulating dashboard content. You can create a Dashboard object, insert boxes, swap boxes, remove a box and get an HTML preview of the Dashboard. The current workflow for making and manipulating dashboard follows: 1) Create a Dashboard 2) Modify the Dashboard (insert, swap, remove, etc) 3) Preview the Dashboard (run `.get_preview()`) 4) Repeat steps 2) and 3) as long as desired The basic box object that your insert into a dashboard is just a `dict`. The minimal dict for a box is: ``` { 'type': 'box', 'boxType': 'plot' } ``` where 'fileId' can be set to the 'username:#' of your plot. The other parameters a box takes are `shareKey` (default is None) and `title` (default is ''). You will need to use the `.get_preview()` method quite regularly as this will return an HTML representation of the dashboard in which the boxes in the HTML are labelled with on-the-fly-generated numbers which change after each modification to the dashboard. Example: Create a simple Dashboard object ``` import plotly.dashboard_objs as dashboard box_1 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:some#', 'title': 'box 1' } box_2 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:some#', 'title': 'box 2' } box_3 = { 'type': 'box', 'boxType': 'plot', 'fileId': 'username:some#', 'title': 'box 3' } my_dboard = dashboard.Dashboard() my_dboard.insert(box_1) # my_dboard.get_preview() my_dboard.insert(box_2, 'above', 1) # my_dboard.get_preview() my_dboard.insert(box_3, 'left', 2) # my_dboard.get_preview() my_dboard.swap(1, 2) # my_dboard.get_preview() my_dboard.remove(1) # my_dboard.get_preview() ``` """ from . dashboard_objs import Dashboard plotly-2.2.3+dfsg.orig/plotly/grid_objs/ 0000755 0001750 0001750 00000000000 13211605515 017533 5 ustar noahfx noahfx plotly-2.2.3+dfsg.orig/plotly/grid_objs/grid_objs.py 0000644 0001750 0001750 00000024123 13104403274 022050 0 ustar noahfx noahfx """ grid_objs ========= """ from __future__ import absolute_import from collections import MutableSequence from requests.compat import json as _json from plotly import exceptions, optional_imports, utils pd = optional_imports.get_module('pandas') __all__ = None class Column(object): """ Columns make up Plotly Grids and can be the source of data for Plotly Graphs. They have a name and an array of data. They can be uploaded to Plotly with the `plotly.plotly.grid_ops` class. Usage example 1: Upload a set of columns as a grid to Plotly ``` from plotly.grid_objs import Grid, Column import plotly.plotly as py column_1 = Column([1, 2, 3], 'time') column_2 = Column([4, 2, 5], 'voltage') grid = Grid([column_1, column_2]) py.grid_ops.upload(grid, 'time vs voltage') ``` Usage example 2: Make a graph based with data that is sourced from a newly uploaded Plotly columns ``` import plotly.plotly as py from plotly.grid_objs import Grid, Column from plotly.graph_objs import Scatter # Upload a grid column_1 = Column([1, 2, 3], 'time') column_2 = Column([4, 2, 5], 'voltage') grid = Grid([column_1, column_2]) py.grid_ops.upload(grid, 'time vs voltage') # Build a Plotly graph object sourced from the # grid's columns trace = Scatter(xsrc=grid[0], ysrc=grid[1]) py.plot([trace], filename='graph from grid') ``` """ def __init__(self, data, name): """ Initialize a Plotly column with `data` and `name`. `data` is an array of strings, numbers, or dates. `name` is the name of the column as it will apppear in the Plotly grid. Names must be unique to a grid. """ # TODO: data type checking self.data = data # TODO: name type checking self.name = name self.id = '' def __str__(self): max_chars = 10 jdata = _json.dumps(self.data, cls=utils.PlotlyJSONEncoder) if len(jdata) > max_chars: data_string = jdata[:max_chars] + "...]" else: data_string = jdata string = '