FileDownload Recursion

Using panel 0.12.6 I’m running into this following error. What I’d like to realize is a tabbed widget that can plot a list of x,y pairs and then dump them to disk if needed.

The problem I’m running into is with the FileDownload function. I’ve included an example below. One that works and one that creates the error. To illustrate the error, I get the recursion issue when the file download button is included. Hopefully, I’m missing something simple. My guess is that the datastream is being created immediately. I’ve tried using the embed=False and embed=True flags with no luck.

Help is appreciated.

import numpy as np
import pandas as pd
import plotly.graph_objects as go
from collections import OrderedDict
from io import StringIO
import panel as pn
pn.extension('plotly')

lowerBound = 10
upperBound = 100

def getPlotlyDataframe(plotlyDict):
    '''
    Accepts a ploty dictionary and preps for CSV dump
    '''
    dataDict = OrderedDict()
    for i,subDict in enumerate(plotlyDict['data']):
        curLabel = subDict['name']
        xLabel = curLabel+"_x"
        yLabel = curLabel+"_y"
        
        curX = subDict['x']
        curY = subDict['y']
        dataDict[xLabel] = curX
        dataDict[yLabel] = curY

    
    df = pd.DataFrame(dataDict)
    
    return df

class XYBOX(pn.Column):
    '''
    Adapted from: https://discourse.holoviz.org/t/how-to-create-a-self-contained-custom-panel/985/6
    '''
    def __init__(self, dataList, lowerBound = lowerBound, upperBound = upperBound,
                 xLabel = 'X', yLabel = 'Y', dataLabels = [], **params):
        self._rename["column"] = None

        super().__init__(**params)

        self.dataList = dataList
        self.dataLabels = []
        
        
        self.xyFig = go.Figure()
        self.xyFig.update_xaxes(title=xLabel)
        self.xyFig.update_yaxes(title=yLabel)        
        self.addTraces()
        self.xyPane = pn.pane.Plotly(self.xyFig, width = 800, height = 500)
        
        self.lowerXInput = pn.widgets.FloatInput(name='Lower Bound', value=lowerBound, step=1e-1, start=0, end=1000000, width=150)
        self.upperXInput = pn.widgets.FloatInput(name='Upper Bound', value=upperBound, step=1e-1, start=1, end=1000000, width=150)
        self.xButton = pn.widgets.Button(name='Update x-axis', button_type='danger', width=150)
        
        self.xButton.on_click(self.updateAxisButtonClick)
        
        
        self.plotSet = pn.Column(pn.Row(pn.layout.HSpacer(), self.xyPane, pn.layout.HSpacer()),            
                              pn.layout.Divider(margin=(-20, 0, 0, 0)),
                              pn.Row(pn.layout.HSpacer(),self.lowerXInput,
                              self.upperXInput,
                              self.xButton, pn.layout.HSpacer(),
                             ))

        self.savePlotButton = pn.widgets.Button(name='Save PNG', button_type='primary')        
        
        ##This following 3 lines works when commenting out the FileDownload Section
        self.saveVectorButton = pn.widgets.Button(name='Save CSV', button_type='warning')
        self.saveVectorButton.on_click(self.saveCSVClicked)
        self.saveRow = pn.Row(pn.layout.HSpacer(),self.saveVectorButton, self.savePlotButton, pn.layout.HSpacer())
        
        
        ##Comment out above 3 lines and then uncomment the next 3
        ## THIS DOES NOT WORK!
        #fileName = 'xy.csv'
        #self.fd = pn.widgets.FileDownload(callback = self.getDataStream, filename = fileName, auto = False)
        #self.saveRow = pn.Row(pn.layout.HSpacer(),self.fd, self.savePlotButton, pn.layout.HSpacer())
        
        
        self.tabs = pn.Tabs(('XY Plot', self.plotSet), ("Control Row", self.saveRow))
        
        self[:] = [self.tabs]

    def addTraces(self):
        # fig = go.Figure(data=go.Scatter(x=XFT, y=slice1D_Mob, line=dict(color='red', width=2)))
        for i, data in enumerate(self.dataList):
            x,y = data
            if i < len(self.dataLabels):
                dataLabel = self.dataLabels[i]
            else:
                dataLabel = "Data Set %d"%(i+1)
            
            self.xyFig.add_trace(go.Scatter(x=x, y=y, mode = 'lines', name = dataLabel))
        
        self.xyFig.update_layout(legend=dict(orientation="h",
                                           yanchor="bottom",
                                           y=1.02,
                                           xanchor="right",
                                           x=1
                                          ))

    def saveCSVClicked(self, event = None):    
        '''Direct stream of download failed due to an unknown recursion error'''
        print("GO")
        
    def getDataStream(self, event = None):
        plotlyDict = self.xyFig.to_ordered_dict()
        xyDF = getPlotlyDataframe(plotlyDict)
        #https://panel.holoviz.org/reference/widgets/FileDownload.html
        sio = StringIO()
        xyDF.to_csv(sio)
        sio.seek(0)
        return sio
        
    def updateAxisButtonClick(self, event=None):
        l = self.lowerXInput.value
        u = self.upperXInput.value
        
        self.xyFig.update_xaxes(range=[l,u])                   

x = np.arange(20)
y = np.random.uniform(0,1, len(x))
        
xyb = XYBOX([[x,y], [x,-1*y]], xLabel = 'mz', yLabel = "Intensity (a.u.)")
xyb

Error Traceback:

---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
/opt/anaconda3/lib/python3.9/site-packages/IPython/core/formatters.py in __call__(self, obj)
    700                 type_pprinters=self.type_printers,
    701                 deferred_pprinters=self.deferred_printers)
--> 702             printer.pretty(obj)
    703             printer.flush()
    704             return stream.getvalue()

/opt/anaconda3/lib/python3.9/site-packages/IPython/lib/pretty.py in pretty(self, obj)
    392                         if cls is not object \
    393                                 and callable(cls.__dict__.get('__repr__')):
--> 394                             return _repr_pprint(obj, self, cycle)
    395 
    396             return _default_pprint(obj, self, cycle)

/opt/anaconda3/lib/python3.9/site-packages/IPython/lib/pretty.py in _repr_pprint(obj, p, cycle)
    698     """A pprint that just redirects to the normal repr function."""
    699     # Find newlines and replace them with p.break_()
--> 700     output = repr(obj)
    701     lines = output.splitlines()
    702     with p.group():

/opt/anaconda3/lib/python3.9/site-packages/panel/layout/base.py in __repr__(self, depth, max_depth)
     46         cls = type(self).__name__
     47         params = param_reprs(self, ['objects'])
---> 48         objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
     49         if not params and not objs:
     50             return super().__repr__(depth+1)

/opt/anaconda3/lib/python3.9/site-packages/panel/layout/base.py in <listcomp>(.0)
     46         cls = type(self).__name__
     47         params = param_reprs(self, ['objects'])
---> 48         objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
     49         if not params and not objs:
     50             return super().__repr__(depth+1)

/opt/anaconda3/lib/python3.9/site-packages/panel/layout/base.py in __repr__(self, depth, max_depth)
     46         cls = type(self).__name__
     47         params = param_reprs(self, ['objects'])
---> 48         objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
     49         if not params and not objs:
     50             return super().__repr__(depth+1)

/opt/anaconda3/lib/python3.9/site-packages/panel/layout/base.py in <listcomp>(.0)
     46         cls = type(self).__name__
     47         params = param_reprs(self, ['objects'])
---> 48         objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
     49         if not params and not objs:
     50             return super().__repr__(depth+1)

/opt/anaconda3/lib/python3.9/site-packages/panel/layout/base.py in __repr__(self, depth, max_depth)
     46         cls = type(self).__name__
     47         params = param_reprs(self, ['objects'])
---> 48         objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
     49         if not params and not objs:
     50             return super().__repr__(depth+1)

/opt/anaconda3/lib/python3.9/site-packages/panel/layout/base.py in <listcomp>(.0)
     46         cls = type(self).__name__
     47         params = param_reprs(self, ['objects'])
---> 48         objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
     49         if not params and not objs:
     50             return super().__repr__(depth+1)

/opt/anaconda3/lib/python3.9/site-packages/panel/viewable.py in __repr__(self, depth)
    550     def __repr__(self, depth=0):
    551         return '{cls}({params})'.format(cls=type(self).__name__,
--> 552                                         params=', '.join(param_reprs(self)))
    553 
    554     def __str__(self):

/opt/anaconda3/lib/python3.9/site-packages/panel/util.py in param_reprs(parameterized, skip)
    208         elif isinstance(v, dict) and v == {}: continue
    209         elif (skip and p in skip) or (p == 'name' and v.startswith(cls)): continue
--> 210         else: v = abbreviated_repr(v)
    211         param_reprs.append('%s=%s' % (p, v))
    212     return param_reprs

/opt/anaconda3/lib/python3.9/site-packages/panel/util.py in abbreviated_repr(value, max_length, natural_breaks)
    157         vrepr = type(value).__name__
    158     else:
--> 159         vrepr = repr(value)
    160     if len(vrepr) > max_length:
    161         # Attempt to find natural cutoff point

... last 9 frames repeated, from the frame below ...

/opt/anaconda3/lib/python3.9/site-packages/panel/layout/base.py in __repr__(self, depth, max_depth)
     46         cls = type(self).__name__
     47         params = param_reprs(self, ['objects'])
---> 48         objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
     49         if not params and not objs:
     50             return super().__repr__(depth+1)

RecursionError: maximum recursion depth exceeded

The following code should do what you want. The primary change is changing the inheritance from pn.Column to pn.viewable.Viewer and made some change to accommodate this.

from collections import OrderedDict
from io import StringIO

import numpy as np
import pandas as pd
import panel as pn
import plotly.graph_objects as go

pn.extension("plotly")

lowerBound = 10
upperBound = 100


def getPlotlyDataframe(plotlyDict):
    """
    Accepts a ploty dictionary and preps for CSV dump
    """
    dataDict = OrderedDict()
    for subDict in plotlyDict["data"]:
        curLabel = subDict["name"]
        xLabel = curLabel + "_x"
        yLabel = curLabel + "_y"

        curX = subDict["x"]
        curY = subDict["y"]
        dataDict[xLabel] = curX
        dataDict[yLabel] = curY

    df = pd.DataFrame(dataDict)

    return df


class XYBOX(pn.viewable.Viewer):
    """
    Adapted from: https://discourse.holoviz.org/t/how-to-create-a-self-contained-custom-panel/985/6
    """

    def __init__(
        self,
        dataList,
        lowerBound=lowerBound,
        upperBound=upperBound,
        xLabel="X",
        yLabel="Y",
        dataLabels=None,
    ):
        self.dataList = dataList
        self.dataLabels = dataLabels or []

        self.xyFig = go.Figure()
        self.xyFig.update_xaxes(title=xLabel)
        self.xyFig.update_yaxes(title=yLabel)
        self.addTraces()
        self.xyPane = pn.pane.Plotly(self.xyFig, width=800, height=500)

        self.lowerXInput = pn.widgets.FloatInput(
            name="Lower Bound",
            value=lowerBound,
            step=1e-1,
            start=0,
            end=1000000,
            width=150,
        )
        self.upperXInput = pn.widgets.FloatInput(
            name="Upper Bound",
            value=upperBound,
            step=1e-1,
            start=1,
            end=1000000,
            width=150,
        )
        self.xButton = pn.widgets.Button(
            name="Update x-axis", button_type="danger", width=150
        )

        self.xButton.on_click(self.updateAxisButtonClick)

        self.plotSet = pn.Column(
            pn.Row(pn.layout.HSpacer(), self.xyPane, pn.layout.HSpacer()),
            pn.layout.Divider(margin=(-20, 0, 0, 0)),
            pn.Row(
                pn.layout.HSpacer(),
                self.lowerXInput,
                self.upperXInput,
                self.xButton,
                pn.layout.HSpacer(),
            ),
        )

        self.savePlotButton = pn.widgets.Button(name="Save PNG", button_type="primary")

        fileName = "xy.csv"
        self.fd = pn.widgets.FileDownload(
            callback=self.getDataStream, filename=fileName, auto=False
        )
        self.saveRow = pn.Row(
            pn.layout.HSpacer(), self.fd, self.savePlotButton, pn.layout.HSpacer()
        )

        self.tabs = pn.Tabs(("XY Plot", self.plotSet), ("Control Row", self.saveRow))

    def addTraces(self):
        # fig = go.Figure(data=go.Scatter(x=XFT, y=slice1D_Mob, line=dict(color='red', width=2)))
        for i, data in enumerate(self.dataList):
            x, y = data
            if i < len(self.dataLabels):
                dataLabel = self.dataLabels[i]
            else:
                dataLabel = "Data Set %d" % (i + 1)

            self.xyFig.add_trace(go.Scatter(x=x, y=y, mode="lines", name=dataLabel))

        self.xyFig.update_layout(
            legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
        )

    def getDataStream(self, event=None):
        plotlyDict = self.xyFig.to_ordered_dict()
        xyDF = getPlotlyDataframe(plotlyDict)
        # https://panel.holoviz.org/reference/widgets/FileDownload.html
        sio = StringIO()
        xyDF.to_csv(sio)
        sio.seek(0)
        return sio

    def updateAxisButtonClick(self, event=None):
        l = self.lowerXInput.value
        u = self.upperXInput.value
        self.xyFig.update_xaxes(range=[l, u])

    def __panel__(self):
        return self.tabs


x = np.arange(20)
y = np.random.uniform(0, 1, len(x))

xyb = XYBOX([[x, y], [x, -1 * y]], xLabel="mz", yLabel="Intensity (a.u.)")
xyb.servable()
2 Likes

This is great! Thank you very much. It would have taken me entirely too long to solve that issue.

Cheers.

2 Likes