import copy

import numpy as np
import pandas as pd
import plotly.offline as py_offline
import plotly.figure_factory as ff
from plotly.graph_objs import Figure, Scatter, Line
from plotly.subplots import make_subplots
# from plotly.graph_objs.layout import XAxis, YAxis

from . import themes
from . import auth, ta
from .colors import normalize, to_rgba
from .utils import (check_kwargs, deep_update, dict_replace_keyword,
                    kwargs_from_keyword, merge_dict, is_list, make_list)

__LAYOUT_VALID_KWARGS = [
    'legend', 'logx', 'logy', 'logz', 'layout_update', 'title',
    'xrange', 'yrange', 'zrange', 'rangeselector', 'rangeslider', 'showlegend', 'fontfamily'
]

__SHAPES_KWARGS = ['vline', 'hline', 'shapes', 'hspan', 'vspan','vspans','hspans']

__GEO_KWARGS = [
    'projection', 'showframe', 'showlakes', 'coastlinecolor', 'countrywidth', 'countrycolor',
    'showsubunits', 'bgcolor', 'showrivers', 'subunitcolor', 'showcountries', 'riverwidth', 'scope',
    'rivercolor', 'lataxis', 'subunitwidth', 'showocean', 'oceancolor', 'lakecolor', 'showland', 'lonaxis',
    'framecolor', 'coastlinewidth', 'landcolor', 'showcoastlines', 'framewidth', 'resolution',
    'projection_type'
]

__ANN_KWARGS = [
    'xref', 'yref', 'text', 'showarrow',
    'arrowhead', 'ax', 'ay', 'textangle', 'arrowsize',
    'arrowwidth', 'arrowcolor', 'fontcolor', 'fontsize', 'xanchor', 'yanchor', 'align'
]

__LAYOUT_AXIS = [
    'autorange', 'autotick', 'backgroundcolor', 'categoryarray', 'categoryarraysrc', 'categoryorder',
    'dtick', 'exponentformat', 'fixedrange', 'gridcolor', 'gridwidth', 'hoverformat', 'linecolor',
    'linewidth',
    'mirror', 'nticks', 'showaxeslabels', 'showbackground', 'showexponent', 'showgrid', 'showline',
    'showspikes',
    'showticklabels', 'showtickprefix', 'showticksuffix', 'spikecolor', 'spikesides', 'spikethickness',
    'tick0', 'tickangle', 'tickcolor', 'tickfont', 'tickformat', 'ticklen', 'tickmode', 'tickprefix',
    'ticks',
    'ticksuffix', 'ticktext', 'ticktextsrc', 'tickvals', 'tickvalssrc', 'tickwidth', 'titlefont',
    'zeroline', 'zerolinecolor', 'zerolinewidth'
]

X_Y = [
    'anchor', 'automargin', 'autorange', 'calendar', 'categoryarray', 'categoryarraysrc', 'categoryorder', 'color',
    'constrain', 'constraintoward', 'domain', 'dtick', 'exponentformat', 'fixedrange', 'gridcolor', 'gridwidth',
    'hoverformat',
    'layer', 'linecolor', 'linewidth', 'mirror', 'nticks', 'overlaying', 'position', 'range', 'rangemode',
    'scaleanchor', 'scaleratio',
    'separatethousands', 'showexponent', 'showgrid', 'showline', 'showspikes', 'showticklabels', 'showtickprefix',
    'showticksuffix', 'side',
    'spikecolor', 'spikedash', 'spikemode', 'spikesnap', 'spikethickness', 'tick0', 'tickangle', 'tickcolor',
    'tickfont', 'tickformat',
    'tickformatstops', 'ticklen', 'tickmode', 'tickprefix', 'ticks', 'ticksuffix', 'ticktext', 'ticktextsrc',
    'tickvals', 'tickvalssrc',
    'tickwidth', 'title', 'titlefont', 'type', 'visible', 'zeroline', 'zerolinecolor', 'zerolinewidth'
]

__LAYOUT_AXIS_X = ['xaxis_' + _ for _ in X_Y]
__LAYOUT_AXIS_Y = ['yaxis_' + _ for _ in X_Y]

__LAYOUT_KWARGS = []
[__LAYOUT_KWARGS.extend(_) for _ in [__LAYOUT_VALID_KWARGS, __SHAPES_KWARGS, __GEO_KWARGS, __ANN_KWARGS, __LAYOUT_AXIS,
                                     __LAYOUT_AXIS_X, __LAYOUT_AXIS_Y]]


def add_ref_line(values, direction='vertical', color='red', width=2):
    """
    根据给出的取值列表，返回对应的参考线对象(go.layout.Shape)
    """
    import plotly.graph_objs as go

    def kwargs(v):
        return {
            'x0': v, 'y0': 0, 'x1': v, 'y1': 1, 'yref': 'paper',
        } if direction == 'vertical' else {
            'x0': 0, 'y0': v, 'x1': 1, 'y1': v, 'xref': 'paper',
        }

    return [
        go.layout.Shape(
            **kwargs(v),
            type="line",
            line=dict(
                color=color,
                width=width
            )
        )
        for v in values
    ]


def getTheme(theme=None):
    """
    Returns a theme definition.

    To see the colors translated (hex) use
    blot.getLayout(theme) instead.
    """
    if not theme:
        theme = auth.get_config_file()['theme']

    theme = theme.lower()
    if theme in themes.THEMES:
        return updateColors(copy.deepcopy(themes.THEMES[theme]))
    else:
        raise Exception("Invalid Theme: {0}".format(theme))


def getThemes():
    """
    Returns the list of available themes
    """
    return list(themes.THEMES.keys())


def updateColors(layout):
    for k, v in list(layout.items()):
        if isinstance(v, dict):
            updateColors(v)
        else:
            if isinstance(v, list):
                for _ in v:
                    if isinstance(_, dict):
                        updateColors(_)
            if 'colorscale' in k.lower():
                continue
            if 'color' in k.lower():
                if 'rgba' not in v:
                    layout[k] = normalize(v)
    return layout


def getLayout(kind=None, theme=None, title='', xTitle='', yTitle='', zTitle='', barmode='', bargap=None,
              bargroupgap=None,
              margin=None, dimensions=None, width=None, height=None,
              annotations=None, is3d=False, **kwargs):
    """
    Generates a plotly Layout

    Parameters:
    -----------
        theme : string
            Layout Theme
                solar
                pearl
                white
        title : string
            Chart Title
        xTitle : string
            X Axis Title
        yTitle : string
            Y Axis Title
        zTitle : string
            Z Axis Title
            Applicable only for 3d charts
        barmode : string
            Mode when displaying bars
                group
                stack
                overlay
        bargap : float
            Sets the gap between bars
                [0,1)
            Applicabe for bar and histogram plots
        bargroupgap : float
            Set the gap between groups
                [0,1)
            Applicabe for bar and histogram plots
        gridcolor : string
                grid color
        zerolinecolor : string
                zero line color
        margin : dict or tuple
                Dictionary (l,r,b,t) or
                Tuple containing the left,
                right, bottom and top margins
        dimensions : tuple
            Dimensions of figure
        annotations : dict or list
            Dictionary of annotations
                {x_point : text}
            or
            List of Plotly Annotations
        is3d : bool
            Indicates if the layout is for a 3D chart

        Other Kwargs
        ============

        Shapes
            hline : int, list or dict
                Draws a horizontal line at the
                indicated y position(s)
                Extra parameters can be passed in
                the form of a dictionary (see shapes)
            vline : int, list or dict
                Draws a vertical line at the
                indicated x position(s)
                Extra parameters can be passed in
                the form of a dictionary (see shapes)
            hspan : (y0,y1)
                Draws a horizontal rectangle at the
                indicated (y0,y1) positions.
                Extra parameters can be passed in
                the form of a dictionary (see shapes)
            vspan : (x0,x1)
                Draws a vertical rectangle at the
                indicated (x0,x1) positions.
                Extra parameters can be passed in
                the form of a dictionary (see shapes)
            shapes : dict or list(dict)
                List of dictionaries with the
                specifications of a given shape.
                See help(blot.tools.get_shape)
                for more information

        Axis Ranges
            xrange : [lower_bound,upper_bound]
                Sets the range for the x axis
            yrange : [lower_bound,upper_bound]
                Sets the range for the y axis
            zrange : [lower_bound,upper_bound]
                Sets the range for the z axis

        Explicit Layout Updates
            layout_update : dict
                The layout will be modified with all
                the explicit values stated in the
                dictionary

        Range Selector
            rangeselector : dict
                Defines a rangeselector object
                see help(cf.tools.get_range_selector) for more information
                Example:
                    {'steps':['1y','2 months','5 weeks','ytd','2mtd'],
                     'axis':'xaxis', 'bgcolor' : ('blue',.3),
                     'x': 0.2 , 'y' : 0.9}

        Range Slider
            rangeslider : bool or dict
                Defines if a rangeslider is displayed
                If bool:
                    True : Makes it visible
                if dict:
                    Rangeslider object
                Example:
                    {'bgcolor':('blue',.3),'autorange':True}

        Annotations
            fontcolor : str
                Text color for annotations
            fontsize : int
                Text size for annotations
            textangle : int
                Textt angle
            See https://plot.ly/python/reference/#layout-annotations
            for a complete list of valid parameters.
    """

    for key in list(kwargs.keys()):
        if key not in __LAYOUT_KWARGS:
            raise Exception("Invalid keyword : '{0}'".format(key))

    if not theme:
        theme = auth.get_config_file()['theme']

    theme_data = getTheme(theme)
    layout = theme_data['layout']
    try:
        layout['xaxis']['title'].update({'text': xTitle})
        layout['yaxis']['title'].update({'text': yTitle})
    except:
        layout['xaxis'].update({'title': xTitle})
        layout['yaxis'].update({'title': yTitle})

    fontfamily = kwargs.pop('fontfamily', None)
    if fontfamily:
        deep_update(layout, {'font': {'family': fontfamily}})

    if barmode:
        layout.update({'barmode': barmode})
    if bargroupgap:
        layout.update({'bargroupgap': bargroupgap})
    if bargap:
        layout.update(bargap=bargap)
    if title:
        try:
            layout['title'].update({'text': title})
        except:
            layout.update({'title': title})
    if annotations:
        layout.update({'annotations': annotations})

    def update_axis(layout, axis='xy', **vals):
        for _x in axis:
            for k, v in list(vals.items()):
                if v == None:
                    vals.pop(k)
            for k in layout:
                if '{0}{1}'.format(_x, 'axis') in k:
                    layout[k].update(**vals)
        return layout

    axis_kwargs = check_kwargs(kwargs, __LAYOUT_AXIS, {}, True)
    xaxis_kwargs = kwargs_from_keyword(kwargs, {}, 'xaxis', True)
    yaxis_kwargs = kwargs_from_keyword(kwargs, {}, 'yaxis', True)

    for _x, _vals in (('xy', axis_kwargs), ('x', xaxis_kwargs), ('y', yaxis_kwargs)):
        layout = update_axis(layout, _x, **_vals)

    if margin:
        if isinstance(margin, dict):
            margin = margin
        else:
            margin = dict(list(zip(('l', 'r', 'b', 't'), margin)))
        layout.update(margin=margin)

    if dimensions:
        layout.update(width=dimensions[0])
        layout.update(height=dimensions[1])

    if height:
        layout.update(height=height)
    if width:
        layout.update(width=width)
    if is3d:
        if '3d' in theme_data:
            layout = deep_update(layout, theme_data['3d'])
        zaxis = layout['xaxis'].copy()
        zaxis.update(title=zTitle)
        scene = dict(xaxis=layout['xaxis'].copy(), yaxis=layout['yaxis'].copy(), zaxis=zaxis)
        layout.update(scene=scene)
        del layout['xaxis']
        del layout['yaxis']

    ## Axis Range
    for r in ['x', 'y', 'z']:
        if '{0}range'.format(r) in kwargs:
            if is3d:
                layout['scene']['{0}axis'.format(r)].update(range=kwargs['{0}range'.format(r)])
            else:
                layout['{0}axis'.format(r)].update(range=kwargs['{0}range'.format(r)])

    # Need to update this for an add_axis approach.
    if kind in ('candlestick', 'ohlc', 'candle'):
        layout['yaxis2'] = layout['yaxis'].copy()
        layout['yaxis'].update(showticklabels=False)

    ## Kwargs

    if 'legend' in kwargs:
        if type(kwargs['legend']) == bool:
            layout['showlegend'] = kwargs['legend']
        elif type(kwargs['legend']) == str:
            if kwargs['legend'] == 'top':
                layout['legend'].update(orientation='h', yanchor='bottom', x=.3, y=.95)
            elif kwargs['legend'] == 'bottom':
                layout['legend'].update(orientation='h', yanchor='bottom', x=.3, y=-0.5)
            layout['showlegend'] = True
        else:
            layout['legend'] = kwargs['legend']
            layout['showlegend'] = True

    if 'showlegend' in kwargs:
        layout['showlegend'] = kwargs['showlegend']

    # Logarithmic Axis
    for _ in ['x', 'y', 'z']:
        if 'log{0}'.format(_) in kwargs:
            if is3d:
                if kwargs['log{0}'.format(_)]:
                    layout['scene']['{0}axis'.format(_)]['type'] = 'log'
            else:
                if kwargs['log{0}'.format(_)]:
                    layout['{0}axis'.format(_)]['type'] = 'log'

    # Shapes

    if any(k in kwargs for k in ['vline', 'hline', 'shapes', 'hspan', 'vspan']):
        shapes = []

        def get_shapes(xline):

            orientation = xline[0]
            xline = kwargs[xline]
            if isinstance(xline, list):
                for x_i in xline:
                    if isinstance(x_i, dict):
                        x_i['kind'] = 'line'
                        shapes.append(get_shape(**x_i))
                    else:
                        if orientation == 'h':
                            shapes.append(get_shape(kind='line', y=x_i))
                        else:
                            shapes.append(get_shape(kind='line', x=x_i))
            elif isinstance(xline, dict):
                shapes.append(get_shape(**xline))
            else:
                if orientation == 'h':
                    shapes.append(get_shape(kind='line', y=xline))
                else:
                    shapes.append(get_shape(kind='line', x=xline))

        def get_span(xspan):
            orientation = xspan[0]
            xspan = kwargs[xspan]
            if isinstance(xspan, list):
                for x_i in xspan:
                    if isinstance(x_i, dict):
                        x_i['kind'] = 'rect'
                        shapes.append(get_shape(**x_i))
                    else:
                        v0, v1 = x_i
                        if orientation == 'h':
                            shapes.append(get_shape(kind='rect', y0=v0, y1=v1, fill=True, opacity=.5))
                        else:
                            shapes.append(get_shape(kind='rect', x0=v0, x1=v1, fill=True, opacity=.5))
            elif isinstance(xspan, dict):
                xspan['kind'] = 'rect'
                shapes.append(get_shape(**xspan))
            elif isinstance(xspan, tuple):
                v0, v1 = xspan
                if orientation == 'h':
                    shapes.append(get_shape(kind='rect', y0=v0, y1=v1, fill=True, opacity=.5))
                else:
                    shapes.append(get_shape(kind='rect', x0=v0, x1=v1, fill=True, opacity=.5))
            else:
                raise Exception('Invalid value for {0}span: {1}'.format(orientation, xspan))

        if 'hline' in kwargs:
            get_shapes('hline')
        if 'vline' in kwargs:
            get_shapes('vline')
        if 'hspan' in kwargs:
            get_span('hspan')
        if 'vspan' in kwargs:
            get_span('vspan')
        if 'shapes' in kwargs:
            shapes_ = kwargs['shapes']
            if isinstance(shapes_, list):
                for i in shapes_:
                    shp = i if 'type' in i else get_shape(**i)
                    shapes.append(shp)
            elif isinstance(shapes_, dict):
                shp = shapes_ if 'type' in shapes_ else get_shape(**shapes_)
                shapes.append(shp)
            else:
                raise Exception("Shapes need to be either a dict or list of dicts")

        layout['shapes'] = shapes

    # Maps
    if kind in ('choropleth', 'scattergeo'):
        kw = check_kwargs(kwargs, __GEO_KWARGS)
        defaults = {'projection': {'type': 'equirectangular'}, 'showframe': False, 'showcoastlines': False}
        for k, v in list(defaults.items()):
            if k not in kw:
                kw[k] = v
        kw_ = kwargs_from_keyword(kw, {}, 'projection')
        deep_update(kw, kw_)
        layout['geo'] = kw
        del layout['xaxis']
        del layout['yaxis']
        if not margin:
            layout['margin'] = {'autoexpand': True}

    # Range Selector
    if 'rangeselector' in kwargs:
        rs = kwargs['rangeselector']
        if 'axis' in rs:
            axis = rs['axis']
            del rs['axis']
        else:
            axis = 'xaxis'
        layout[axis]['rangeselector'] = get_range_selector(**rs)

    # Range Slider
    if 'rangeslider' in kwargs:
        if type(kwargs['rangeslider']) == bool:
            if kwargs['rangeslider']:
                layout['xaxis']['rangeslider'] = dict(visible=kwargs['rangeslider'])
            else:
                layout['xaxis']['rangeslider'] = dict(visible=False)
            # layout['yaxis1'].update(domain=(0,0))
        else:
            layout['xaxis']['rangeslider'] = kwargs['rangeslider']
    else:
        if kind in ('ohlc', 'candle', 'candlestick'):
            layout['xaxis']['rangeslider'] = dict(visible=False)
        # layout['yaxis1'].update(domain=(0,0))

    # Explicit Updates

    if 'layout_update' in kwargs:
        layout = deep_update(layout, kwargs['layout_update'])

    return layout


def get_annotations(df, annotations, kind='lines', theme=None, **kwargs):
    """
    Generates an annotations object

    Parameters:
    -----------
        df : DataFrame
            Original DataFrame of values
        annotations : dict or list
            Dictionary of annotations
            {x_point : text}
            or
            List of Plotly annotations
    """

    for key in list(kwargs.keys()):
        if key not in __ANN_KWARGS:
            raise Exception("Invalid keyword : '{0}'".format(key))

    theme_data = getTheme(theme)

    kwargs['fontcolor'] = kwargs.pop('fontcolor', theme_data['annotations']['fontcolor'])
    kwargs['arrowcolor'] = kwargs.pop('arrowcolor', theme_data['annotations']['arrowcolor'])
    kwargs['fontsize'] = kwargs.pop('fontsize', 12)

    def check_ann(annotation):
        local_list = []

        if 'title' in annotation:
            local_list.append(
                dict(
                    text=annotation['title'],
                    showarrow=False,
                    x=0,
                    y=1,
                    xref='paper',
                    yref='paper',
                    font={'size': 24 if not 'fontsize' in kwargs else kwargs['fontsize']}
                )
            )

            del annotation['title']
            local_list.append(annotation)

        elif 'x' in annotation:
            ann = dict(
                x=annotation['x'],
                y=annotation.get('y', .5),
                xref=annotation.get('xref', 'x'),
                yref=annotation.get('yref', kwargs.get('yref', 'y1')),
                text=annotation.get('text'),
                showarrow=annotation.get('showarrow', True),
                arrowhead=annotation.get('arrowhead', 7),
                arrowcolor=annotation.get('arrowcolor', kwargs.get('arrowcolor')),
                ax=annotation.get('ax', 0),
                ay=annotation.get('ay', -100),
                textangle=annotation.get('textangle', 0),
                hovertext=annotation.get('hovertext', ''),
                opacity=annotation.get('opacity', 1),
                font=dict(
                    color=annotation.get('fontcolor', annotation.get('color', kwargs.get('fontcolor'))),
                    size=annotation.get('fontsize', annotation.get('size', kwargs.get('fontsize')))
                )
            )
            local_list.append(ann)

        else:
            for k, v in list(annotation.items()):
                if kind in ('candlestick', 'ohlc', 'candle'):
                    d = ta._ohlc_dict(df)
                    maxv = df[d['high']].loc[k]
                    yref = 'y2'
                else:
                    maxv = df.loc[k].sum() if k in df.index else 0
                    yref = 'y1'
                ann = dict(
                    x=k,
                    y=maxv,
                    xref='x',
                    yref=yref,
                    text=v,
                    showarrow=kwargs.get('showarrow', True),
                    arrowhead=kwargs.get('arrowhead', 7),
                    arrowcolor=kwargs['arrowcolor'],
                    ax=kwargs.get('ax', 0),
                    ay=kwargs.get('ay', -100),
                    textangle=kwargs.get('textangle', 0),
                    hovertext=kwargs.get('hovertext', ''),
                    opacity=kwargs.get('opacity', 1),
                    font=dict(
                        color=kwargs['fontcolor'],
                        size=kwargs['fontsize']
                    )
                )
                local_list.append(ann)

        return local_list

    annotations = make_list(annotations)
    _list_ann = []
    for ann in annotations:
        _list_ann.extend(check_ann(ann))
    return _list_ann


def strip_figures(figure):
    """
    Strips a figure into multiple figures with a trace on each of them

    Parameters:
    -----------
        figure : Figure
            Plotly Figure
    """
    fig = []
    for trace in figure['data']:
        fig.append(dict(data=[trace], layout=figure['layout']))
    return fig


def get_base_layout(figs):
    """
    Generates a layout with the union of all properties of multiple
    figures' layouts

    Parameters:
    -----------
        fig : list(Figures)
            List of Plotly Figures
    """
    layout = {}
    for fig in figs:
        if not isinstance(fig, dict):
            fig = fig.to_dict()
        for k, v in list(fig['layout'].items()):
            layout[k] = v
    return layout


def figures(df, specs, asList=False):
    """
    Generates multiple Plotly figures for a given DataFrame

    Parameters:
    -----------
        df : DataFrame
            Pandas DataFrame
        specs : list(dict)
            List of dictionaries with the properties
            of each figure.
            All properties avaialbe can be seen with
            help(blot.pd.DataFrame.iplot)
        asList : boolean
            If True, then a list of figures is returned.
            Otherwise a single (merged) figure is returned.
            Default : False
    """
    figs = []
    for spec in specs:
        figs.append(df.figure(**spec))
    if asList:
        return figs
    else:
        return merge_figures(figs)


def merge_figures(figures):
    """
    Generates a single Figure from a list of figures

    Parameters:
    -----------
        figures : list(Figures)
            List of figures to be merged.
    """
    figure = {}
    data = []
    for fig in figures:
        for trace in fig['data']:
            data.append(trace)
    layout = get_base_layout(figures)
    figure['data'] = data
    figure['layout'] = layout
    return figure


def subplots(figures, shape=None,
             shared_xaxes=False, shared_yaxes=False,
             start_cell='top-left', theme=None, base_layout=None,
             **kwargs):
    """
    Generates a subplot view for a set of figures
    This is a wrapper for plotly.subplots.make_subplots

    Parameters:
    -----------
        figures : [Figures]
            List of Plotly Figures
        shape : (rows,cols)
            Tuple indicating the size of rows and columns
            If omitted then the layout is automatically set
        shared_xaxes : bool
            Assign shared x axes.
            If True, subplots in the same grid column have one common
            shared x-axis at the bottom of the grid.
        shared_yaxes : bool
            Assign shared y axes.
            If True, subplots in the same grid row have one common
            shared y-axis on the left-hand side of the grid.
        start_cell : string
                'bottom-left'
                'top-left'
            Choose the starting cell in the subplot grid used to set the
            domains of the subplots.
        theme : string
            Layout Theme
                solar
                pearl
                white
            see blot.getThemes() for all
            available themes
        base_layout : layout (dict)
            Layout to be used as base where the subplots will be
            added
        subplot_titles : list(string)
            List of strings that contains the titles of each
            plot.
        horizontal_spacing : float
                [0,1]
            Space between subplot columns.
        vertical_spacing : float
            Space between subplot rows.
        specs : list of dicts
            Subplot specifications.
                ex1: specs=[[{}, {}], [{'colspan': 2}, None]]
                ex2: specs=[[{'rowspan': 2}, {}], [None, {}]]

            - Indices of the outer list correspond to subplot grid rows
              starting from the bottom. The number of rows in 'specs'
              must be equal to 'rows'.

            - Indices of the inner lists correspond to subplot grid columns
              starting from the left. The number of columns in 'specs'
              must be equal to 'cols'.

            - Each item in the 'specs' list corresponds to one subplot
              in a subplot grid. (N.B. The subplot grid has exactly 'rows'
              times 'cols' cells.)

            - Use None for blank a subplot cell (or to move pass a col/row span).

            - Note that specs[0][0] has the specs of the 'start_cell' subplot.

            - Each item in 'specs' is a dictionary.
                The available keys are:

                * is_3d (boolean, default=False): flag for 3d scenes
                * colspan (int, default=1): number of subplot columns
                    for this subplot to span.
                * rowspan (int, default=1): number of subplot rows
                    for this subplot to span.
                * l (float, default=0.0): padding left of cell
                * r (float, default=0.0): padding right of cell
                * t (float, default=0.0): padding right of cell
                * b (float, default=0.0): padding bottom of cell

            - Use 'horizontal_spacing' and 'vertical_spacing' to adjust
              the spacing in between the subplots.

        insets : list of dicts
            Inset specifications.

            - Each item in 'insets' is a dictionary.
                The available keys are:

                * cell (tuple, default=(1,1)): (row, col) index of the
                    subplot cell to overlay inset axes onto.
                * is_3d (boolean, default=False): flag for 3d scenes
                * l (float, default=0.0): padding left of inset
                      in fraction of cell width
                * w (float or 'to_end', default='to_end') inset width
                      in fraction of cell width ('to_end': to cell right edge)
                * b (float, default=0.0): padding bottom of inset
                      in fraction of cell height
                * h (float or 'to_end', default='to_end') inset height
                      in fraction of cell height ('to_end': to cell top edge)
    """
    if not isinstance(figures, list):
        figures = [figures]

    if shape:
        rows, cols = shape
        if len(figures) > rows * cols:
            raise Exception("Invalid shape for the number of figures given")
    else:
        if len(figures) == 1:
            cols = 1
            rows = 1
        elif shared_xaxes:
            cols = 1
            rows = len(figures)
        elif shared_yaxes:
            cols = len(figures)
            rows = 1
        else:
            cols = 2
            rows = len(figures) // 2 + len(figures) % 2
    sp, grid_ref = get_subplots(rows=rows, cols=cols,
                                shared_xaxes=shared_xaxes, shared_yaxes=shared_yaxes,
                                start_cell=start_cell, theme=theme, base_layout=base_layout,
                                **kwargs)
    list_ref = (col for row in grid_ref for col in row)
    for i in range(len(figures)):
        while True:
            lr = next(list_ref)
            if lr is not None:
                break
        for _ in figures[i]['data']:
            for axe in lr:
                _.update(axe.trace_kwargs)
            sp['data'].append(_)
    # Remove extra plots
    for k in list(sp['layout'].keys()):
        try:
            if int(k[-1]) > len(figures):
                del sp['layout'][k]
        except:
            pass

    # Check for non-cartesian plots
    data = sp['data']
    layout = sp['layout']
    for d in data:
        if d['type'] == 'pie':
            d['domain'] = {}
            d['domain']['x'] = layout['xaxis{0}'.format(d['xaxis'][1:])]['domain']
            d['domain']['y'] = layout['yaxis{0}'.format(d['yaxis'][1:])]['domain']
    return sp


def get_subplots(rows=1, cols=1,
                 shared_xaxes=False, shared_yaxes=False,
                 start_cell='top-left', theme=None, base_layout=None,
                 **kwargs):
    """
    Generates a subplot view for a set of figures

    Parameters:
    -----------
        rows : int
            Number of rows
        cols : int
            Number of cols
        shared_xaxes : bool
            Assign shared x axes.
            If True, subplots in the same grid column have one common
            shared x-axis at the bottom of the gird.
        shared_yaxes : bool
            Assign shared y axes.
            If True, subplots in the same grid row have one common
            shared y-axis on the left-hand side of the gird.
        start_cell : string
                'bottom-left'
                'top-left'
            Choose the starting cell in the subplot grid used to set the
            domains of the subplots.
        theme : string
            Layout Theme
                solar
                pearl
                white
            see blot.getThemes() for all
            available themes
        horizontal_spacing : float
                [0,1]
            Space between subplot columns.
        vertical_spacing : float
            Space between subplot rows.
        specs : list of dicts
            Subplot specifications.
                ex1: specs=[[{}, {}], [{'colspan': 2}, None]]
                ex2: specs=[[{'rowspan': 2}, {}], [None, {}]]

            - Indices of the outer list correspond to subplot grid rows
              starting from the bottom. The number of rows in 'specs'
              must be equal to 'rows'.

            - Indices of the inner lists correspond to subplot grid columns
              starting from the left. The number of columns in 'specs'
              must be equal to 'cols'.

            - Each item in the 'specs' list corresponds to one subplot
              in a subplot grid. (N.B. The subplot grid has exactly 'rows'
              times 'cols' cells.)

            - Use None for blank a subplot cell (or to move pass a col/row span).

            - Note that specs[0][0] has the specs of the 'start_cell' subplot.

            - Each item in 'specs' is a dictionary.
                The available keys are:

                * is_3d (boolean, default=False): flag for 3d scenes
                * colspan (int, default=1): number of subplot columns
                    for this subplot to span.
                * rowspan (int, default=1): number of subplot rows
                    for this subplot to span.
                * l (float, default=0.0): padding left of cell
                * r (float, default=0.0): padding right of cell
                * t (float, default=0.0): padding right of cell
                * b (float, default=0.0): padding bottom of cell

            - Use 'horizontal_spacing' and 'vertical_spacing' to adjust
              the spacing in between the subplots.

        insets : list of dicts
            Inset specifications.

            - Each item in 'insets' is a dictionary.
                The available keys are:

                * cell (tuple, default=(1,1)): (row, col) index of the
                    subplot cell to overlay inset axes onto.
                * is_3d (boolean, default=False): flag for 3d scenes
                * l (float, default=0.0): padding left of inset
                      in fraction of cell width
                * w (float or 'to_end', default='to_end') inset width
                      in fraction of cell width ('to_end': to cell right edge)
                * b (float, default=0.0): padding bottom of inset
                      in fraction of cell height
                * h (float or 'to_end', default='to_end') inset height
                      in fraction of cell height ('to_end': to cell top edge)
    """

    if not theme:
        theme = auth.get_config_file()['theme']

    layout = base_layout if base_layout else getLayout(theme, **check_kwargs(kwargs, __LAYOUT_AXIS))
    sp = make_subplots(rows=rows, cols=cols, shared_xaxes=shared_xaxes,
                       shared_yaxes=shared_yaxes, print_grid=False,
                       start_cell=start_cell, **kwargs)
    sp, grid_ref = sp.to_dict(), sp._grid_ref

    for k, v in list(layout.items()):
        if 'xaxis' not in k and 'yaxis' not in k:
            sp['layout'].update({k: v})

    def update_axis(fig, layout):
        for axis, n in list(Figure(fig).axis['len'].items()):
            for _ in range(1, n + 1):
                for k, v in list(layout['{0}axis'.format(axis)].items()):
                    _ = '' if _ == 1 else _
                    if k not in fig['layout']['{0}axis{1}'.format(axis, _)]:
                        fig['layout']['{0}axis{1}'.format(axis, _)][k] = v

    update_axis(sp, layout)
    # 124 - zeroline on the first figure

    # if 'subplot_titles' in kwargs:
    # 	if 'annotations' in layout:
    # 		annotation=sp['layout']['annotations'][0]
    # 	else:
    # 		annotation=getLayout(theme,annotations=[Annotation(text='')])['annotations']
    # 	for ann in sp['layout']['annotations']:
    # 		ann.update(font=dict(color=annotation['font']['color']))

    # def update_items(sp_item,layout,axis):
    # 	for k,v in list(layout[axis].items()):
    # 		sp_item.update({k:v})

    # for k,v in list(sp['layout'].items()):
    # 	if isinstance(v,go.XAxis):
    # 		update_items(v,layout,'xaxis1')
    # 	elif isinstance(v,go.YAxis):
    # 		update_items(v,layout,'xaxis1')

    return sp, grid_ref


# Candlesticks and OHLC


def get_ohlc(df, up_color=None, down_color=None, theme=None, layout=None, **kwargs):
    layout = getLayout(theme=theme) if not layout else layout
    ohlc = ['open', 'high', 'low', 'close']
    if not theme:
        theme = auth.get_config_file()['theme']
    c_dir = ta._ohlc_dict(df)
    args = [df[c_dir[_]] for _ in ohlc]
    args.append(df.index)
    fig = ff.create_ohlc(*args, **kwargs)
    ohlc_bars = {}
    ohlc_bars['data'] = fig['data']
    ohlc_bars['layout'] = fig['layout']
    data = ohlc_bars['data']
    if up_color:
        data[0]['line'].update(color=normalize(up_color))
    if down_color:
        data[1]['line'].update(color=normalize(down_color))
    ohlc_bars['layout']['hovermode'] = 'closest'
    ohlc_bars['layout'] = merge_dict(layout, ohlc_bars['layout'])
    return ohlc_bars


def get_candle(df, up_color=None, down_color=None, theme=None, layout=None, **kwargs):
    layout = getLayout(theme=theme) if not layout else layout
    ohlc = ['open', 'high', 'low', 'close']
    if not theme:
        theme = auth.get_config_file()['theme']
    c_dir = ta._ohlc_dict(df)
    args = [df[c_dir[_]] for _ in ohlc]
    args.append(df.index)
    fig = ff.create_candlestick(*args, **kwargs)
    candle = {}
    candle['data'] = fig['data']
    candle['layout'] = layout
    data = candle['data']

    def update_color(n, color):
        data[n]['fillcolor'] = normalize(color)
        data[n]['line'].update(color=normalize(color))

    if up_color:
        update_color(0, up_color)
    if down_color:
        update_color(1, down_color)
    candle['layout']['hovermode'] = 'closest'
    candle['layout'] = merge_dict(layout, candle['layout'])
    return candle


def scatter_matrix(df, theme=None, bins=10, color='grey', size=2):
    """
    Displays a matrix with scatter plot for each pair of
    Series in the DataFrame.
    The diagonal shows a histogram for each of the Series

    Parameters:
    -----------
        df : DataFrame
            Pandas DataFrame
        theme : string
            Theme to be used (if not the default)
        bins : int
            Number of bins to use for histogram
        color : string
            Color to be used for each scatter plot
        size : int
            Size for each marker on the scatter plot
    """
    if not theme:
        theme = auth.get_config_file()['theme']

    figs = []
    for i in df.columns:
        for j in df.columns:
            if i == j:
                fig = df.iplot(kind='histogram', keys=[i], asFigure=True, bins=bins)
                figs.append(fig)
            else:
                figs.append(
                    df.iplot(kind='scatter', mode='markers', x=j, y=i, asFigure=True, size=size, colors=[color]))
    layout = getLayout(theme)
    layout['xaxis'].update(showgrid=False)
    layout['yaxis'].update(showgrid=False)
    sm = subplots(figs, shape=(len(df.columns), len(df.columns)), shared_xaxes=False, shared_yaxes=False,
                  horizontal_spacing=.05, vertical_spacing=.07, base_layout=layout)
    sm['layout'].update(bargap=.02, showlegend=False)
    return sm


## Trace dictionary

@property
def trace_dict(self):
    d = {}
    for trace in range(len(self['data'])):
        trace_ = self['data'][trace]
        name = '{0}_'.format(trace_['name']) if trace_['name'] in d else trace_['name']
        d[name] = trace
    return d


## Axis Definition

def get_ref(figure):
    d = {}
    figure = fig_to_dict(figure)
    for trace in figure['data']:
        name = '{0}_'.format(trace['name']) if trace['name'] in d else trace['name']
        x = trace['xaxis'] if 'xaxis' in trace else 'x1'
        y = trace['yaxis'] if 'yaxis' in trace else 'y1'
        d[name] = (x, y)
    return d


def get_def(figure):
    d = {}
    figure = fig_to_dict(figure)
    items = list(figure['layout']['scene'].items()) if 'scene' in figure['layout'] else list(figure['layout'].items())
    for k, v in items:
        if 'axis' in k:
            d['{0}{1}'.format(k[0], 1 if k[-1] == 's' else k[-1])] = v
    return d


def get_len(figure):
    d = {}
    figure = fig_to_dict(figure)
    keys = list(figure['layout']['scene'].keys()) if 'scene' in figure['layout'] else list(figure['layout'].keys())
    for k in keys:
        if 'axis' in k:
            d[k[0]] = d[k[0]] + 1 if k[0] in d else 1
    return d


def get_which(figure):
    d = {}
    figure = fig_to_dict(figure)
    keys = list(figure['layout']['scene'].keys()) if 'scene' in figure['layout'] else list(figure['layout'].keys())
    for k in keys:
        if 'axis' in k:
            x = '{0}{1}'.format(k[0], '1' if k[-1] == 's' else k[-1])
            if k[0] in d:
                d[k[0]].append(x)
            else:
                d[k[0]] = [x]
    return d


def get_ref_axis(figure):
    d = {}
    figure = fig_to_dict(figure)
    for k, v in list(get_ref(figure).items()):
        for i in v:
            if i not in d:
                d[i] = []
            d[i].append(k)
    return d


def get_dom(figure):
    d = {}
    which = get_which(figure)
    axis_def = get_def(figure)
    for k in ('x', 'y'):
        d[k] = {}
        for y in which[k]:
            item = axis_def[y]
            domain = '[0.0, 1.0]' if 'domain' not in item else str(item['domain'])
            if domain not in d[k]:
                d[k][domain] = {}
            s = 'left' if k == 'y' else 'bottom'
            side = s if 'side' not in item else item['side']
            d[k][domain][side] = y
    return d


@property
def axis(self):
    return {'ref': get_ref(self),
            'ref_axis': get_ref_axis(self),
            'def': get_def(self),
            'len': get_len(self),
            'which': get_which(self),
            'dom': get_dom(self)}


### Set Axis


def _set_axis(self, traces, on=None, side='right', title=''):
    """
    Sets the axis in which each trace should appear
    If the axis doesn't exist then a new axis is created

    Parameters:
    -----------
        traces : list(str)
            List of trace names
        on : string
            The axis in which the traces should be placed.
            If this is not indicated then a new axis will be
            created
        side : string
            Side where the axis will be placed
                'left'
                'right'
        title : string
            Sets the title of the axis
            Applies only to new axis
    """
    fig = {}
    fig_cpy = fig_to_dict(self).copy()
    fig['data'] = fig_cpy['data']
    fig['layout'] = fig_cpy['layout']
    fig = Figure(fig)
    traces = make_list(traces)

    def update_data(trace, y):
        anchor = fig.axis['def'][y]['anchor'] if 'anchor' in fig.axis['def'][y] else 'x1'
        idx = fig.trace_dict[trace] if isinstance(trace, str) else trace
        fig['data'][idx]['xaxis'] = anchor
        fig['data'][idx]['yaxis'] = y

    for trace in traces:
        if on:
            if on not in fig.axis['def']:
                raise Exception('"on" axis does not exists: {0}'.format(on))
            update_data(trace, y=on)
        else:
            curr_x, curr_y = fig.axis['ref'][trace]
            domain = '[0.0, 1.0]' if 'domain' not in fig.axis['def'][curr_y] else str(fig.axis['def'][curr_y]['domain'])
            try:
                new_axis = fig.axis['dom']['y'][domain][side]
            except KeyError:
                axis = fig.axis['def'][curr_y].copy()
                ### check overlaying values
                axis.update(title=title, overlaying=curr_y, side=side, anchor=curr_x)
                axis_idx = str(fig.axis['len']['y'] + 1)
                fig['layout']['yaxis{0}'.format(axis_idx)] = axis
                new_axis = 'y{0}'.format(axis_idx)
            update_data(trace, y=new_axis)

    for k in list(fig.axis['def'].keys()):
        id = '{0}axis{1}'.format(k[0], k[-1:])
        if k not in fig.axis['ref_axis']:
            try:
                del fig['layout'][id]
            except KeyError:
                pass

    return fig


### Shapes

# def _get_shapes(d):

# 	def get_shapes(xline):
# 		orientation=xline[0]
# 		xline=d[xline]
# 		if type(xline) in (list,tuple):
# 			for x_i in xline:
# 				if isinstance(x_i,dict):
# 					x_i['kind']='line'
# 					return get_shape(**x_i)
# 				else:
# 					if orientation=='h':
# 						return get_shape(kind='line',y=x_i)
# 					else:
# 						return get_shape(kind='line',x=x_i)
# 		elif isinstance(xline,dict):
# 			return get_shape(**xline)
# 		else:
# 			if orientation=='h':
# 				return get_shape(kind='line',y=xline)
# 			else:
# 				return get_shape(kind='line',x=xline)

# 	def get_span(xspan):
# 		orientation=xspan[0]
# 		xspan=d[xspan]
# 		if isinstance(xspan,list):
# 			for x_i in xspan:
# 				if isinstance(x_i,dict):
# 					x_i['kind']='rect'
# 					return get_shape(**x_i)
# 				else:
# 					v0,v1=x_i
# 					if orientation=='h':
# 						get_shape(kind='rect',y0=v0,y1=v1,fill=True,opacity=.5)
# 					else:
# 						get_shape(kind='rect',x0=v0,x1=v1,fill=True,opacity=.5)
# 		elif isinstance(xspan,dict):
# 			xspan['kind']='rect'
# 			return get_shape(**xspan)
# 		elif isinstance(xspan,tuple):
# 			v0,v1=xspan
# 			if orientation=='h':
# 				return get_shape(kind='rect',y0=v0,y1=v1,fill=True,opacity=.5)
# 			else:
# 				return get_shape(kind='rect',x0=v0,x1=v1,fill=True,opacity=.5)
# 		else:
# 			raise Exception('Invalid value for {0}span: {1}'.format(orientation,xspan))

# 	if 'hline' in d:
# 		return get_shapes('hline')
# 	if 'vline' in d:
# 		return get_shapes('vline')
# 	if 'hspan' in d:
# 		get_span('hspan')
# 	if 'vspan' in d:
# 		get_span('vspan')
# 	if 'shapes' in d:
# 		shapes_=kwargs['shapes']
# 		if isinstance(shapes_,list):
# 			for i in shapes_:
# 				shp=i if 'type' in i else get_shape(**i)
# 				shapes.append(shp)
# 		elif isinstance(shapes_,dict):
# 				shp=shapes_ if 'type' in shapes_ else get_shape(**shapes_)
# 				shapes.append(shp)
# 		else:
# 			raise Exception("Shapes need to be either a dict or list of dicts")

def get_shape(kind='line', x=None, y=None, x0=None, y0=None, x1=None, y1=None, span=0, color='red', dash='solid',
              width=1,
              fillcolor=None, fill=False, opacity=1, xref='x', yref='y'):
    """
    Returns a plotly shape

    Parameters:
    -----------
        kind : string
            Shape kind
                line
                rect
                circle
        x : float
            x values for the shape.
            This assumes x0=x1
        x0 : float
            x0 value for the shape
        x1 : float
            x1 value for the shape
        y : float
            y values for the shape.
            This assumes y0=y1
        y0 : float
            y0 value for the shape
        y1 : float
            y1 value for the shape
        color : string
            color for shape line
        dash : string
            line style
                solid
                dash
                dashdot
                dot
        width : int
            line width
        fillcolor : string
            shape fill color
        fill : bool
            If True then fill shape
            If not fillcolor then the
            line color will be used
        opacity : float [0,1]
            opacity of the fill
        xref : string
            Sets the x coordinate system
            which this object refers to
                'x'
                'paper'
                'x2' etc
        yref : string
            Sets the y coordinate system
            which this object refers to
                'y'
                'paper'
                'y2' etc
    """
    if x1 is None:
        if x0 is None:
            if x is None:
                xref = 'paper'
                x0 = 0
                x1 = 1
            else:
                x0 = x1 = x
        else:
            x1 = x0
    else:
        x
    if y1 is None:
        if y0 is None:
            if y is None:
                yref = 'paper'
                y0 = 0
                y1 = 1
            else:
                y0 = y1 = y
        else:
            y1 = y0

    shape = {'x0': x0,
             'y0': y0,
             'x1': x1,
             'y1': y1,
             'line': {
                 'color': normalize(color),
                 'width': width,
                 'dash': dash
             },
             'xref': xref,
             'yref': yref
             }

    if kind == 'line':
        shape['type'] = 'line'

    elif kind == 'circle':
        shape['type'] = 'circle'

    elif kind == 'rect':
        shape['type'] = 'rect'
    else:
        raise Exception("Invalid or unkown shape type : {0}".format(kind))

    if (fill or fillcolor) and kind != 'line':
        fillcolor = color if not fillcolor else fillcolor
        fillcolor = to_rgba(normalize(fillcolor), opacity)
        shape['fillcolor'] = fillcolor

    return shape


### Advanced Shapes

def get_trendline(df, date0, date1, column='close', **kwargs):
    df = pd.DataFrame(df[column])
    d = {'x0': date0,
         'x1': date1,
         'y0': df.loc[date0].values[0],
         'y1': df.loc[date1].values[0]}
    d.update(**kwargs)
    return d


### Range Selector

def get_range_selector(steps=['1m', '1y'], bgcolor='rgba(150, 200, 250, 0.4)', x=0, y=0.9,
                       visible=True, **kwargs):
    """
    Returns a range selector
    Reference: https://plot.ly/python/reference/#layout-xaxis-rangeselector

    Parameters:
    -----------
        steps : string or list(string)
            Steps for the range
                Examples:
                    ['1y','2 months','5 weeks','ytd','2mtd']
        bgocolor : string or tuple(color,alpha)
            Background color
                Examples:
                    'cyan'
                    'rgba(232,214,10,.5)'
                    ('blue',.3)
        font_size : int
            Font size
        x : float
            Position along the x axis
            Domain (0,1)
        y : float
            Position along the y axis
            Domain (0,1)
    """
    import string

    def get_step(s):

        term = []
        stepmode = 'backward'
        _s = s
        _s = _s.replace(' ', '')
        _s = _s.lower()
        if _s in ['reset', 'all']:
            return {'count': 1, 'label': s, 'step': 'all'}
        if _s[-2:] == 'td':
            _s = _s[:-2]
            stepmode = 'todate'
            if _s[0] not in string.digits:
                _s = '1' + _s
        if _s[0] not in string.digits:
            raise Exception('Invalid step format: {0}'.format(s))
        while _s[-1] not in string.digits:
            term.append(_s[-1])
            _s = _s[:-1]
        term.reverse()
        term = ''.join(term)
        cnt = int(_s)
        term = term[:-1] if (term[-1] == 's' and len(term) > 1) else term
        if term in ['y', 'year', 'yr']:
            steps = 'year'
        elif term in ['w', 'week', 'wk']:
            steps = 'week'
        elif term in ['m', 'month', 'mth', 'mnth', 'mo']:
            steps = 'month'
        elif term in ['hr', 'hour']:
            steps = 'hour'
        elif term in ['min', 'minute', 'mn']:
            steps = 'minute'
        elif term in ['sec', 'sc', 's']:
            steps = 'second'
        else:
            raise Exception('Invalid step format: {0}'.format(s))
        return {'count': cnt, 'label': s, 'step': steps, 'stepmode': stepmode}

    rangeselector = {
        'bgcolor': to_rgba(bgcolor, 1),
        'x': x,
        'y': y,
        'visible': visible
    }

    kwargs['fontsize'] = kwargs.get('fontsize', 13)

    rangeselector = dict_replace_keyword(rangeselector, 'font', kwargs, False)

    buttons = []
    if type(steps) not in (list, tuple):
        steps = [steps]
    for s in steps:
        buttons.append(get_step(s))
    rangeselector['buttons'] = buttons
    return rangeselector


### Error Bars

def get_error_bar(axis='y', type='data', values=None, values_minus=None, color=None, thickness=1, width=5,
                  opacity=1):
    # error=ErrorY() if axis=='y' else ErrorX()
    error = {}
    if type == 'data':
        if isinstance(values, list) or isinstance(values, np.ndarray):
            if values_minus:
                if len(values) != len(values_minus):
                    raise Exception('Array values need to be of same length')
                error.update(symmetric=False, arrayminus=values_minus)
            error.update(array=values)
        else:
            if values_minus:
                if isinstance(values_minus, list) or isinstance(values_minus, np.ndarray):
                    raise Exception('Values should be of same type (int, float)')
                error.udpate(symmetric=False, valueminus=values_minus)
            error.update(value=values)
    elif type in ['percent', 'constant']:
        if isinstance(values, list) or isinstance(values, np.ndarray):
            raise Exception('Value should be of type (int, float)')
        error.update(value=values)
    elif type == 'sqrt':
        pass
    else:
        raise Exception('Invalid type: {0}'.format(type))
    error.update(type=type, thickness=thickness, width=width, visible=True)
    if color:
        error.update(color=to_rgba(color, opacity))
    return error


def set_errors(figure, trace=None, axis='y', type='data', values=None, values_minus=None, color=None, thickness=1,
               width=None,
               opacity=None, **kwargs):
    figure = fig_to_dict(figure)
    if 'value' in kwargs:
        values = kwargs['value']
    data = figure['data']
    if 'continuous' not in type:
        width = width if width else 5
        opacity = opacity if opacity else 1
        color = to_rgba(color, opacity) if color else None
        error = get_error_bar(axis=axis, type=type, values=values, values_minus=values_minus,
                              color=color, thickness=thickness, width=width, opacity=opacity)
        if trace:
            data[figure.trace_dict[trace]].update({'error_{0}'.format(axis): error})
        else:
            for trace in data:
                trace.update({'error_{0}'.format(axis): error})
    else:
        width = width if width else .5
        opacity = opacity if opacity else .3

        def get_traces(trace, value, type, color=None, width=.2, opacity=.3):
            if not is_list(value):
                value = [value] * len(trace['y'])
            if values_minus:
                if is_list(values_minus):
                    min_value = values_minus
                else:
                    min_value = [values_minus] * len(trace['y'])
            else:
                min_value = value
            if 'percent' in type:
                y_up = [trace['y'][_] * (1 + value[_] / 100.00) for _ in range(len(value))]
                y_down = [trace['y'][_] * (1 - min_value[_] / 100.00) for _ in range(len(min_value))]
            else:
                y_up = [trace['y'][_] + value[_] for _ in range(len(value))]
                y_down = [trace['y'][_] - min_value[_] for _ in range(len(min_value))]
            upper = Scatter(y=y_up, mode='lines', showlegend=False,
                            line=Line(width=width), x=trace['x'])
            if 'yaxis' in trace:
                upper['yaxis'] = trace['yaxis']
            if color:
                color = normalize(color)
            else:
                if 'color' in trace['line']:
                    color = trace['line']['color']
                else:
                    color = 'charcoal'
            color = to_rgba(color, opacity) if color else None
            upper['line']['color'] = color
            lower = copy.deepcopy(upper)
            name = trace['name'] + '_' if 'name' in trace else ''
            upper.update(name=name + 'upper')
            color = to_rgba(normalize(color), opacity)
            lower.update(fill='tonexty', fillcolor=color, name=name + 'lower', y=y_down)
            return upper, lower

        if trace:
            traces = [figure.trace_dict[trace]]
        else:
            traces = list(range(len(figure['data'])))
        for i in traces:
            trace = figure['data'][i]
            upper, lower = get_traces(trace, values, type, color=color, width=width, opacity=opacity)
            figure['data'].extend([upper, lower])
    return figure


def _nodata(self):
    d = []
    # _=copy.deepcopy(self)
    for _ in self:
        d.append(copy.deepcopy(_))
    for _ in d:
        for k in list(_.keys()):
            if k in ('x', 'y', 'open', 'close', 'high', 'low', 'index', 'volume', 'line', 'marker'):
                try:
                    _[k] = []
                except:
                    _[k] = {}
    return d


def _figure_no_data(self):
    return {'data': self.data.nodata(),
            'layout': self.layout}


def _update_traces(d, **kwargs):
    for _ in d['data']:
        _.update(**kwargs)


def _update_axis(d, axis):
    _axis = axis[0]
    if not isinstance(d, dict):
        d = d.to_dict()
    from_axis = d['data'][0].pop('{0}axis'.format(_axis), '{0}1'.format(_axis))
    from_axis = _axis + 'axis' + from_axis[1:]
    to_axis = _axis + 'axis' + axis[1:]
    d['layout'][to_axis] = d['layout'].pop(from_axis)
    _update_traces(d, **{'{0}axis'.format(_axis): axis})


def _move_axis(d, xaxis=None, yaxis=None):
    if xaxis:
        _update_axis(d, xaxis)
    if yaxis:
        _update_axis(d, yaxis)


### Offline

def go_offline(connected=False, offline=True):
    if offline:
        py_offline.init_notebook_mode(connected)
        py_offline.__PLOTLY_OFFLINE_INITIALIZED = True
    else:
        py_offline.__PLOTLY_OFFLINE_INITIALIZED = False


def is_offline():
    return py_offline.__PLOTLY_OFFLINE_INITIALIZED


### Plotly 3

def fig_to_dict(fig):
    if not isinstance(fig, dict):
        fig = fig.to_dict()
    return fig


Figure.axis = axis
Figure.trace_dict = trace_dict
Figure.set_axis = _set_axis
Figure.nodata = _figure_no_data
