Use plotly offline to save chart as image file

Why does offline.plot() not generate the image file locally? It has the params ā€œimage_filenameā€, yet still generates a HTML file that opens in browser then downloads the file somewhere random?

This library looks great, but such a simple thing to store the image locally is missing, unless iā€™m not understanding this function correctly?

I made a script using selenium. It seems to work for webgl plots.

import time
from selenium import webdriver

profile = webdriver.FirefoxProfile()
profile.set_preference('browser.download.folderList', 2)  # custom location
profile.set_preference('browser.download.manager.showWhenStarting', False)
profile.set_preference('browser.download.dir', '/tmp')
profile.set_preference('browser.helperApps.neverAsk.saveToDisk', 'image/png')

driver = webdriver.Firefox(firefox_profile=profile)
driver.get("file:////home/temp-plot.html")
export_button = driver.find_element_by_xpath("//a[@data-title='Download plot as a png']")
export_button.click()
time.sleep(10)
driver.quit()

This is great! Worked perfectly for me.

Adjusted one thing to grab more than one imageā€¦

xList = driver.find_elements_by_xpath("//a[@data-title='Download plot as a png']")

for x in xList:
    x.click()
    time.sleep(5)

I could not get this to work for Chrome, I was wondering if anyone else did?

1 Like

This works! Also get more info via: help(plotly.offline.image)

ā€¦
image (default=None |ā€˜pngā€™ |ā€˜jpegā€™ |ā€˜svgā€™ |ā€˜webpā€™) ā€“ This parameter sets
the format of the image to be downloaded, if we choose to download an
image. This parameter has a default value of None indicating that no
image should be downloaded. Please note: for higher resolution images
and more export options, consider making requests to our image servers.
Type: help(py.image) for more details.
image_filename (default=ā€˜plot_imageā€™) ā€“ Sets the name of the file your
image will be saved to. The extension should not be included.
image_height (default=600) ā€“ Specifies the height of the image in px.
image_width (default=800) ā€“ Specifies the width of the image in px.

have you tried
import plotly as py
import plotly.graph_objs as go

Create random data with numpy

import numpy as np

N = 1000
random_x = np.random.randn(N)
random_y = np.random.randn(N)

Create a trace

trace = go.Scatter(
x = random_x,
y = random_y,
mode = ā€˜markersā€™
)

data = [trace]

Plot and embed in ipython notebook!

py.offline.iplot(data, filename=ā€˜basic-scatterā€™)

plot and embed in a html document

py.offline.plot(data, filename=ā€˜basic-scatterā€™)

@gacafe: are you sure you arenā€™t thinking of plotly.plotly.image?

help(plotly.offline.image)
AttributeError: module 'plotly.offline' has no attribute 'image'

@realdoomframe: did you read the question?

I canā€™t find any information on how to save my offline (iPython Notebook version or otherwise) chart as an image file.

@broken_symlink: Super cool! selenium is pretty new to me; am I correct that this will pop open a firefox instance while it downloads the file, an then close it? I was hoping to avoid this and just run in the background.


For others following along, hereā€™s some updates I was able to make today.

  • request: I put in an actual feature request per the suggestion of cldougl at the above github issue. Please do the same so we can build a community voice for this.

  • inquired with the plotly team to at least suggest how this might be accomplished vs. automatically closing all github issues requesting this feature. This way the user community could take a stab at it. Follow along here.

  • I found out that bokeh has an export_png method, and dug into the code. Turns out that they are using seleniumā€™s method get_screenshot_as_png() method and a phantomjs webdriver in order to silently generate the plot (no opening of the html file required)! I was able to replicate this with plotly here.

  • @broken_symlink, I piggybacked on your sketch to make another method using a chromedriver webdriver here. One observation is that when specifying image='png' and image_filename='name', opening the .html plot file automatically downloads the fileā€¦ one doesnā€™t need to do the click() event on the button! I actually found I was getting two downloads; one for the opening, and one for the button. Let me know if you have any thoughts on that. This method required using pyvirtualdisplay to keep the selenium session from opening an actual browser window.

Please see either/both of the linked jupyter notebooks above if youā€™d like to try this out or make suggestions! I consider myself barely above noob status. Iā€™m sure people in this post can suggest some elegant/graceful ways these methods could be made more stable across osā€™s, how to check dependencies/paths, etc.

It looks the image argument appends some javascript to the html file causes the plot to get downloaded automatically when you open it:

return(                                                                                                         
    ('<script>'                                                                                                 
     'function downloadimage(format, height, width,'                                                            
     ' filename) {{'                                                                                            
     'var p = document.getElementById(\'{plot_id}\');'                                                          
     'Plotly.downloadImage(p, {{format: format, height: height, '                                               
     'width: width, filename: filename}});}};' +                                                                
     check_start +                                                                                              
     '{{downloadimage(\'{format}\', {height}, {width}, '                                                        
     '\'{filename}\');}}' +                                                                                     
     check_end +                                                                                                
     '</script>')                                                                                               
)           

I use Xvfb directly instead of pyvirtualdisplay (which is a wrapper around Xvfb) to avoid opening a browser window.

Iā€™ve had mixed results with seleniumā€™s get screenshot method. Depending on version of firefox, a lot of extra whitespace gets added to the image.

1 Like

Agreed regarding what the code is doing @broken_symlink . From ./plotly/offline/offline.py:

            if image:
                ...
                script = get_image_download_script('plot')

get_image_download_script contains what you posted.

Good to know regarding Xvfb; I need to look into this general approach with respect to OS compatibility as I really have no idea. On arch linux, it Just Workedā„¢.

From some feedback at plotly.py #880, svg was desirable which led me to opt against the screenshot method; Itā€™s good to know there may be other reasons against it. I have preliminary functionality of the download-trigger method using selenium + chromedriver + pyvirtualdisplay at my fork if you want to try it out. It adds a save_img=True/False option to plotly.offline.plot().

This actually IS possible, at least now (May 2018):

py.offline.iplot({ā€˜dataā€™: data, ā€˜layoutā€™: layout}, filename=ā€˜plot_nameā€™, image=ā€˜pngā€™)

This will save a file called plot_name.png to your default browser download directory.
I wrote a function to wait & retry a few times until it is saved (seems to be asynchronous) and then move the file to the desired location after download.

3 Likes

Hi sglyon

I have a simple example. Can you please help me on getting the svg out of the html? The rendering afterwards should work.

library(plotly)
df <- data.frame(a = c(1,2),
b = c(3,4))

p <- plot_ly(df,x =~a,y =~b,mode = ā€œscatterā€,type = ā€œscatterā€)

htmlwidgets::saveWidget(p,ā€œgugus.htmlā€)

=> how to extract the svg info?

Hey, thank you! Actually works.
Btw for me it opens a dialog to choose where to save the file, have you by any chance seen how to just save it automatically?

Update: Offline programmatic static image export support has been released in 3.2.0: See https://medium.com/@plotlygraphs/plotly-py-end-of-summer-updates-5422c98b9058

@jmmease plotly-orca does not seem to work offline.

Could you say a bit more about what problem youā€™re having and what kind of figure youā€™re exporting?

By default, Latex/Mathjax, and geojson files are retrived from online locations (since theyā€™re a bit too large to bundle with plotly.py). But these paths can be configured in plotly.io.orca.config.

-Jon

@jmmease Actually, it seems to be an eventlet error. Iā€™m trying to export a simple figure generated by df.iplot() using cufflinks.
Once I go offline, I get the error

tests\test_plot.py:124 (test_comparison_report[True-True])
name = 'localhost', family = <AddressFamily.AF_INET: 2>, raises = False
_proxy = <eventlet.support.greendns.ResolverProxy object at 0x000001A82B665780>
    def resolve(name, family=socket.AF_INET, raises=True, _proxy=None):
        """Resolve a name for a given family using the global resolver proxy.
    
        This method is called by the global getaddrinfo() function.
    
        Return a dns.resolver.Answer instance.  If there is no answer it's
        rrset will be emtpy.
        """
        if family == socket.AF_INET:
            rdtype = dns.rdatatype.A
        elif family == socket.AF_INET6:
            rdtype = dns.rdatatype.AAAA
        else:
            raise socket.gaierror(socket.EAI_FAMILY,
                                  'Address family not supported')
    
        if _proxy is None:
            _proxy = resolver
        try:
            try:
>               return _proxy.query(name, rdtype, raise_on_no_answer=raises)
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:413: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <eventlet.support.greendns.ResolverProxy object at 0x000001A82B665780>
qname = <DNS name localhost>, rdtype = 1, rdclass = 1, tcp = False
source = None, raise_on_no_answer = False, _hosts_rdtypes = (1, 28)
    def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,
              tcp=False, source=None, raise_on_no_answer=True,
              _hosts_rdtypes=(dns.rdatatype.A, dns.rdatatype.AAAA)):
        """Query the resolver, using /etc/hosts if enabled.
    
            Behavior:
            1. if hosts is enabled and contains answer, return it now
            2. query nameservers for qname
            3. if qname did not contain dots, pretend it was top-level domain,
               query "foobar." and append to previous result
            """
        result = [None, None, 0]
    
        if qname is None:
            qname = '0.0.0.0'
        if isinstance(qname, six.string_types):
            qname = dns.name.from_text(qname, None)
    
        def step(fun, *args, **kwargs):
            try:
                a = fun(*args, **kwargs)
            except Exception as e:
                result[1] = e
                return False
            if a.rrset is not None and len(a.rrset):
                if result[0] is None:
                    result[0] = a
                else:
                    result[0].rrset.union_update(a.rrset)
                result[2] += len(a.rrset)
            return True
    
        def end():
            if result[0] is not None:
                if raise_on_no_answer and result[2] == 0:
                    raise dns.resolver.NoAnswer
                return result[0]
            if result[1] is not None:
                if raise_on_no_answer or not isinstance(result[1], dns.resolver.NoAnswer):
                    raise result[1]
            raise dns.resolver.NXDOMAIN(qnames=(qname,))
    
        if (self._hosts and (rdclass == dns.rdataclass.IN) and (rdtype in _hosts_rdtypes)):
            if step(self._hosts.query, qname, rdtype, raise_on_no_answer=False):
                if (result[0] is not None) or (result[1] is not None):
                    return end()
    
        # Main query
        step(self._resolver.query, qname, rdtype, rdclass, tcp, source, raise_on_no_answer=False)
    
        # `resolv.conf` docs say unqualified names must resolve from search (or local) domain.
        # However, common OS `getaddrinfo()` implementations append trailing dot (e.g. `db -> db.`)
        # and ask nameservers, as if top-level domain was queried.
        # This step follows established practice.
        # https://github.com/nameko/nameko/issues/392
        # https://github.com/eventlet/eventlet/issues/363
        if len(qname) == 1:
            step(self._resolver.query, qname.concatenate(dns.name.root),
                 rdtype, rdclass, tcp, source, raise_on_no_answer=False)
    
>       return end()
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:371: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def end():
        if result[0] is not None:
            if raise_on_no_answer and result[2] == 0:
                raise dns.resolver.NoAnswer
            return result[0]
        if result[1] is not None:
            if raise_on_no_answer or not isinstance(result[1], dns.resolver.NoAnswer):
>               raise result[1]
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:350: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
fun = <bound method Resolver.query of <dns.resolver.Resolver object at 0x000001A82B650470>>
args = (<DNS name localhost.>, 1, 1, False, None)
kwargs = {'raise_on_no_answer': False}
    def step(fun, *args, **kwargs):
        try:
>           a = fun(*args, **kwargs)
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:331: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <dns.resolver.Resolver object at 0x000001A82B650470>
qname = <DNS name localhost.>, rdtype = 1, rdclass = 1, tcp = False
source = None, raise_on_no_answer = False, source_port = 0
    def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,
              tcp=False, source=None, raise_on_no_answer=True, source_port=0):
        """Query nameservers to find the answer to the question.
    
            The I{qname}, I{rdtype}, and I{rdclass} parameters may be objects
            of the appropriate type, or strings that can be converted into objects
            of the appropriate type.  E.g. For I{rdtype} the integer 2 and the
            the string 'NS' both mean to query for records with DNS rdata type NS.
    
            @param qname: the query name
            @type qname: dns.name.Name object or string
            @param rdtype: the query type
            @type rdtype: int or string
            @param rdclass: the query class
            @type rdclass: int or string
            @param tcp: use TCP to make the query (default is False).
            @type tcp: bool
            @param source: bind to this IP address (defaults to machine default
            IP).
            @type source: IP address in dotted quad notation
            @param raise_on_no_answer: raise NoAnswer if there's no answer
            (defaults is True).
            @type raise_on_no_answer: bool
            @param source_port: The port from which to send the message.
            The default is 0.
            @type source_port: int
            @rtype: dns.resolver.Answer instance
            @raises Timeout: no answers could be found in the specified lifetime
            @raises NXDOMAIN: the query name does not exist
            @raises YXDOMAIN: the query name is too long after DNAME substitution
            @raises NoAnswer: the response did not contain an answer and
            raise_on_no_answer is True.
            @raises NoNameservers: no non-broken nameservers are available to
            answer the question."""
    
        if isinstance(qname, string_types):
            qname = dns.name.from_text(qname, None)
        if isinstance(rdtype, string_types):
            rdtype = dns.rdatatype.from_text(rdtype)
        if dns.rdatatype.is_metatype(rdtype):
            raise NoMetaqueries
        if isinstance(rdclass, string_types):
            rdclass = dns.rdataclass.from_text(rdclass)
        if dns.rdataclass.is_metaclass(rdclass):
            raise NoMetaqueries
        qnames_to_try = []
        if qname.is_absolute():
            qnames_to_try.append(qname)
        else:
            if len(qname) > 1:
                qnames_to_try.append(qname.concatenate(dns.name.root))
            if self.search:
                for suffix in self.search:
                    qnames_to_try.append(qname.concatenate(suffix))
            else:
                qnames_to_try.append(qname.concatenate(self.domain))
        all_nxdomain = True
        nxdomain_responses = {}
        start = time.time()
        _qname = None # make pylint happy
        for _qname in qnames_to_try:
            if self.cache:
                answer = self.cache.get((_qname, rdtype, rdclass))
                if answer is not None:
                    if answer.rrset is None and raise_on_no_answer:
                        raise NoAnswer(response=answer.response)
                    else:
                        return answer
            request = dns.message.make_query(_qname, rdtype, rdclass)
            if self.keyname is not None:
                request.use_tsig(self.keyring, self.keyname,
                                 algorithm=self.keyalgorithm)
            request.use_edns(self.edns, self.ednsflags, self.payload)
            if self.flags is not None:
                request.flags = self.flags
            response = None
            #
            # make a copy of the servers list so we can alter it later.
            #
            nameservers = self.nameservers[:]
            errors = []
            if self.rotate:
                random.shuffle(nameservers)
            backoff = 0.10
            while response is None:
                if len(nameservers) == 0:
                    raise NoNameservers(request=request, errors=errors)
                for nameserver in nameservers[:]:
                    timeout = self._compute_timeout(start)
                    port = self.nameserver_ports.get(nameserver, self.port)
                    try:
                        tcp_attempt = tcp
                        if tcp:
                            response = dns.query.tcp(request, nameserver,
                                                     timeout, port,
                                                     source=source,
                                                     source_port=source_port)
                        else:
                            response = dns.query.udp(request, nameserver,
                                                     timeout, port,
                                                     source=source,
                                                     source_port=source_port)
                            if response.flags & dns.flags.TC:
                                # Response truncated; retry with TCP.
                                tcp_attempt = True
                                timeout = self._compute_timeout(start)
                                response = \
                                    dns.query.tcp(request, nameserver,
                                                  timeout, port,
                                                  source=source,
                                                  source_port=source_port)
                    except (socket.error, dns.exception.Timeout) as ex:
                        #
                        # Communication failure or timeout.  Go to the
                        # next server
                        #
                        errors.append((nameserver, tcp_attempt, port, ex,
                                       response))
                        response = None
                        continue
                    except dns.query.UnexpectedSource as ex:
                        #
                        # Who knows?  Keep going.
                        #
                        errors.append((nameserver, tcp_attempt, port, ex,
                                       response))
                        response = None
                        continue
                    except dns.exception.FormError as ex:
                        #
                        # We don't understand what this server is
                        # saying.  Take it out of the mix and
                        # continue.
                        #
                        nameservers.remove(nameserver)
                        errors.append((nameserver, tcp_attempt, port, ex,
                                       response))
                        response = None
                        continue
                    except EOFError as ex:
                        #
                        # We're using TCP and they hung up on us.
                        # Probably they don't support TCP (though
                        # they're supposed to!).  Take it out of the
                        # mix and continue.
                        #
                        nameservers.remove(nameserver)
                        errors.append((nameserver, tcp_attempt, port, ex,
                                       response))
                        response = None
                        continue
                    rcode = response.rcode()
                    if rcode == dns.rcode.YXDOMAIN:
                        ex = YXDOMAIN()
                        errors.append((nameserver, tcp_attempt, port, ex,
                                       response))
                        raise ex
                    if rcode == dns.rcode.NOERROR or \
                            rcode == dns.rcode.NXDOMAIN:
                        break
                    #
                    # We got a response, but we're not happy with the
                    # rcode in it.  Remove the server from the mix if
                    # the rcode isn't SERVFAIL.
                    #
                    if rcode != dns.rcode.SERVFAIL or not self.retry_servfail:
                        nameservers.remove(nameserver)
                    errors.append((nameserver, tcp_attempt, port,
                                   dns.rcode.to_text(rcode), response))
                    response = None
                if response is not None:
                    break
                #
                # All nameservers failed!
                #
                if len(nameservers) > 0:
                    #
                    # But we still have servers to try.  Sleep a bit
                    # so we don't pound them!
                    #
>                   timeout = self._compute_timeout(start)
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\dns\resolver.py:1041: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <dns.resolver.Resolver object at 0x000001A82B650470>
start = 1536673406.3233714
    def _compute_timeout(self, start):
        now = time.time()
        duration = now - start
        if duration < 0:
            if duration < -1:
                # Time going backwards is bad.  Just give up.
                raise Timeout(timeout=duration)
            else:
                # Time went backwards, but only a little.  This can
                # happen, e.g. under vmware with older linux kernels.
                # Pretend it didn't happen.
                now = start
        if duration >= self.lifetime:
>           raise Timeout(timeout=duration)
E           dns.exception.Timeout: The DNS operation timed out after 30.00040102005005 seconds
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\dns\resolver.py:858: Timeout
During handling of the above exception, another exception occurred:
self = <urllib3.connection.HTTPConnection object at 0x000001A831B78EB8>
    def _new_conn(self):
        """ Establish a socket connection and set nodelay settings on it.
    
            :return: New socket connection.
            """
        extra_kw = {}
        if self.source_address:
            extra_kw['source_address'] = self.source_address
    
        if self.socket_options:
            extra_kw['socket_options'] = self.socket_options
    
        try:
            conn = connection.create_connection(
>               (self.host, self.port), self.timeout, **extra_kw)
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\urllib3\connection.py:141: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
address = ('localhost', 50796), timeout = None, source_address = None
socket_options = [(6, 1, 1)]
    def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
                          source_address=None, socket_options=None):
        """Connect to *address* and return the socket object.
    
        Convenience function.  Connect to *address* (a 2-tuple ``(host,
        port)``) and return the socket object.  Passing the optional
        *timeout* parameter will set the timeout on the socket instance
        before attempting to connect.  If no *timeout* is supplied, the
        global default timeout setting returned by :func:`getdefaulttimeout`
        is used.  If *source_address* is set it must be a tuple of (host, port)
        for the socket to bind as a source address before making the connection.
        An host of '' or port 0 tells the OS to use the default.
        """
    
        host, port = address
        if host.startswith('['):
            host = host.strip('[]')
        err = None
    
        # Using the value from allowed_gai_family() in the context of getaddrinfo lets
        # us select whether to work with IPv4 DNS records, IPv6 records, or both.
        # The original create_connection function always returns all records.
        family = allowed_gai_family()
    
>       for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\urllib3\util\connection.py:60: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
host = 'localhost', port = 50796, family = <AddressFamily.AF_UNSPEC: 0>
socktype = <SocketKind.SOCK_STREAM: 1>, proto = 0, flags = 0
    def getaddrinfo(host, port, family=0, socktype=0, proto=0, flags=0):
        """Replacement for Python's socket.getaddrinfo
    
        This does the A and AAAA lookups asynchronously after which it
        calls the OS' getaddrinfo(3) using the AI_NUMERICHOST flag.  This
        flag ensures getaddrinfo(3) does not use the network itself and
        allows us to respect all the other arguments like the native OS.
        """
        if isinstance(host, six.string_types):
            host = host.encode('idna').decode('ascii')
        if host is not None and not is_ip_addr(host):
>           qname, addrs = _getaddrinfo_lookup(host, family, flags)
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:502: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
host = 'localhost', family = <AddressFamily.AF_UNSPEC: 0>, flags = 0
    def _getaddrinfo_lookup(host, family, flags):
        """Resolve a hostname to a list of addresses
    
        Helper function for getaddrinfo.
        """
        if flags & socket.AI_NUMERICHOST:
            raise EAI_NONAME_ERROR
        addrs = []
        if family == socket.AF_UNSPEC:
            err = None
            for qfamily in [socket.AF_INET6, socket.AF_INET]:
                try:
                    answer = resolve(host, qfamily, False)
                except socket.gaierror as e:
                    if e.errno not in (socket.EAI_AGAIN, EAI_NONAME_ERROR.errno, EAI_NODATA_ERROR.errno):
                        raise
                    err = e
                else:
                    if answer.rrset:
                        addrs.extend(rr.address for rr in answer.rrset)
            if err is not None and not addrs:
>               raise err
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:475: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
host = 'localhost', family = <AddressFamily.AF_UNSPEC: 0>, flags = 0
    def _getaddrinfo_lookup(host, family, flags):
        """Resolve a hostname to a list of addresses
    
        Helper function for getaddrinfo.
        """
        if flags & socket.AI_NUMERICHOST:
            raise EAI_NONAME_ERROR
        addrs = []
        if family == socket.AF_UNSPEC:
            err = None
            for qfamily in [socket.AF_INET6, socket.AF_INET]:
                try:
>                   answer = resolve(host, qfamily, False)
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:466: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
name = 'localhost', family = <AddressFamily.AF_INET: 2>, raises = False
_proxy = <eventlet.support.greendns.ResolverProxy object at 0x000001A82B665780>
    def resolve(name, family=socket.AF_INET, raises=True, _proxy=None):
        """Resolve a name for a given family using the global resolver proxy.
    
        This method is called by the global getaddrinfo() function.
    
        Return a dns.resolver.Answer instance.  If there is no answer it's
        rrset will be emtpy.
        """
        if family == socket.AF_INET:
            rdtype = dns.rdatatype.A
        elif family == socket.AF_INET6:
            rdtype = dns.rdatatype.AAAA
        else:
            raise socket.gaierror(socket.EAI_FAMILY,
                                  'Address family not supported')
    
        if _proxy is None:
            _proxy = resolver
        try:
            try:
                return _proxy.query(name, rdtype, raise_on_no_answer=raises)
            except dns.resolver.NXDOMAIN:
                if not raises:
                    return HostsAnswer(dns.name.Name(name),
                                       rdtype, dns.rdataclass.IN, None, False)
                raise
        except dns.exception.Timeout:
>           raise EAI_EAGAIN_ERROR
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:420: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
host = 'localhost', family = <AddressFamily.AF_UNSPEC: 0>, flags = 0
    def _getaddrinfo_lookup(host, family, flags):
        """Resolve a hostname to a list of addresses
    
        Helper function for getaddrinfo.
        """
        if flags & socket.AI_NUMERICHOST:
            raise EAI_NONAME_ERROR
        addrs = []
        if family == socket.AF_UNSPEC:
            err = None
            for qfamily in [socket.AF_INET6, socket.AF_INET]:
                try:
>                   answer = resolve(host, qfamily, False)
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:466: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
name = 'localhost', family = <AddressFamily.AF_INET6: 23>, raises = False
_proxy = <eventlet.support.greendns.ResolverProxy object at 0x000001A82B665780>
    def resolve(name, family=socket.AF_INET, raises=True, _proxy=None):
        """Resolve a name for a given family using the global resolver proxy.
    
        This method is called by the global getaddrinfo() function.
    
        Return a dns.resolver.Answer instance.  If there is no answer it's
        rrset will be emtpy.
        """
        if family == socket.AF_INET:
            rdtype = dns.rdatatype.A
        elif family == socket.AF_INET6:
            rdtype = dns.rdatatype.AAAA
        else:
            raise socket.gaierror(socket.EAI_FAMILY,
                                  'Address family not supported')
    
        if _proxy is None:
            _proxy = resolver
        try:
            try:
                return _proxy.query(name, rdtype, raise_on_no_answer=raises)
            except dns.resolver.NXDOMAIN:
                if not raises:
                    return HostsAnswer(dns.name.Name(name),
                                       rdtype, dns.rdataclass.IN, None, False)
                raise
        except dns.exception.Timeout:
>           raise EAI_EAGAIN_ERROR
E           socket.gaierror: [Errno 11002] Lookup timed out
..\..\..\Miniconda3\envs\clone-athion-utils\lib\site-packages\eventlet\support\greendns.py:420: gaierror
During handling of the above exception, another exception occurred:
self = <urllib3.connectionpool.HTTPConnectionPool object at 0x000001A831B787B8>
method = 'POST', url = '/'
body = '{"figure": {"data": [{"line": {"color": "rgba(226, 74, 51, 1.0)", "dash": "solid", "shape": "linear", "width": 1.3}, ...#666666"}, "title": "", "titlefont": {"color": "#666666"}, "zerolinecolor": "#F6F6F6"}}}, "format": "png", "scale": 1}'
headers = {'User-Agent': 'python-requests/2.19.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '2278'}
retries = Retry(total=0, connect=None, read=False, redirect=None, status=None)
redirect = False, assert_same_host = False
timeout = <urllib3.util.timeout.Timeout object at 0x000001A82D7B5C50>
pool_timeout = None, release_conn = False, chunked = False, body_pos = None
response_kw = {'decode_content': False, 'preload_content': False}, conn = None
release_this_conn = True, err = None, clean_exit = False
timeout_obj = <urllib3.util.timeout.Timeout object at 0x000001A831B78DD8>
is_new_proxy_conn = False

[...]

(had to cut part of the traceback because of character limit)
But once I deinstalled eventlet and retried, it worked again.

@jmmease Thanks for introducing this feature. Iā€™m still having problems while trying to use plotly.io.write_image() on version 3.2.1. When using offline mode in a Jupyter notebook, I first get the following logs printed out repeatedly a few hundred times:

2018-09-27 10:26:46,553 | INFO : Starting new HTTP connection (1): localhost
2018-09-27 10:26:46,566 | INFO : Starting new HTTP connection (1): localhost
...
2018-09-27 10:26:54,437 | INFO : Starting new HTTP connection (1): localhost

Then, the program throws the following error:

ValueError: 
For some reason plotly.py was unable to communicate with the
local orca server process, even though the server process seems to be running.

Please review the process and connection information below:

orca status
-----------
    state: running
    executable: /home/gabe/miniconda3/envs/dl/lib/orca_app/orca
    version: 1.1.1
    port: 39167
    pid: 12045
    command: ['/home/gabe/miniconda3/envs/dl/lib/orca_app/orca', 'serve', '-p', '39167', '--plotly', '/home/gabe/miniconda3/envs/dl/lib/python3.5/site-packages/plotly/package_data/plotly.min.js', '--graph-only', '--mathjax', 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js']

The full traceback is shown below:

ConnectionRefusedError                    Traceback (most recent call last)
~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/packages/urllib3/connection.py in _new_conn(self)
    141             conn = connection.create_connection(
--> 142                 (self.host, self.port), self.timeout, **extra_kw)
    143 

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/packages/urllib3/util/connection.py in create_connection(address, timeout, source_address, socket_options)
     97     if err is not None:
---> 98         raise err
     99 

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/packages/urllib3/util/connection.py in create_connection(address, timeout, source_address, socket_options)
     87                 sock.bind(source_address)
---> 88             sock.connect(sa)
     89             return sock

ConnectionRefusedError: [Errno 111] Connection refused

During handling of the above exception, another exception occurred:

NewConnectionError                        Traceback (most recent call last)
~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/packages/urllib3/connectionpool.py in urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, **response_kw)
    594                                                   body=body, headers=headers,
--> 595                                                   chunked=chunked)
    596 

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    362         else:
--> 363             conn.request(method, url, **httplib_request_kw)
    364 

~/miniconda3/envs/dl/lib/python3.5/http/client.py in request(self, method, url, body, headers)
   1106         """Send a complete request to the server."""
-> 1107         self._send_request(method, url, body, headers)
   1108 

~/miniconda3/envs/dl/lib/python3.5/http/client.py in _send_request(self, method, url, body, headers)
   1151             body = _encode(body, 'body')
-> 1152         self.endheaders(body)
   1153 

~/miniconda3/envs/dl/lib/python3.5/http/client.py in endheaders(self, message_body)
   1102             raise CannotSendHeader()
-> 1103         self._send_output(message_body)
   1104 

~/miniconda3/envs/dl/lib/python3.5/http/client.py in _send_output(self, message_body)
    933 
--> 934         self.send(msg)
    935         if message_body is not None:

~/miniconda3/envs/dl/lib/python3.5/http/client.py in send(self, data)
    876             if self.auto_open:
--> 877                 self.connect()
    878             else:

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/packages/urllib3/connection.py in connect(self)
    166     def connect(self):
--> 167         conn = self._new_conn()
    168         self._prepare_conn(conn)

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/packages/urllib3/connection.py in _new_conn(self)
    150             raise NewConnectionError(
--> 151                 self, "Failed to establish a new connection: %s" % e)
    152 

NewConnectionError: <requests.packages.urllib3.connection.HTTPConnection object at 0x7eff4fb6cb00>: Failed to establish a new connection: [Errno 111] Connection refused

During handling of the above exception, another exception occurred:

MaxRetryError                             Traceback (most recent call last)
~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/adapters.py in send(self, request, stream, timeout, verify, cert, proxies)
    422                     retries=self.max_retries,
--> 423                     timeout=timeout
    424                 )

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/packages/urllib3/connectionpool.py in urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, **response_kw)
    639             retries = retries.increment(method, url, error=e, _pool=self,
--> 640                                         _stacktrace=sys.exc_info()[2])
    641             retries.sleep()

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/packages/urllib3/util/retry.py in increment(self, method, url, response, error, _pool, _stacktrace)
    286         if new_retry.is_exhausted():
--> 287             raise MaxRetryError(_pool, url, error or ResponseError(cause))
    288 

MaxRetryError: HTTPConnectionPool(host='localhost', port=39167): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7eff4fb6cb00>: Failed to establish a new connection: [Errno 111] Connection refused',))

During handling of the above exception, another exception occurred:

ConnectionError                           Traceback (most recent call last)
~/miniconda3/envs/dl/lib/python3.5/site-packages/plotly/io/_orca.py in to_image(fig, format, width, height, scale, validate)
   1304             width=width,
-> 1305             height=height)
   1306     except OSError as err:

~/miniconda3/envs/dl/lib/python3.5/site-packages/retrying.py in wrapped_f(*args, **kw)
     48             def wrapped_f(*args, **kw):
---> 49                 return Retrying(*dargs, **dkw).call(f, *args, **kw)
     50 

~/miniconda3/envs/dl/lib/python3.5/site-packages/retrying.py in call(self, fn, *args, **kwargs)
    211                     # get() on an attempt with an exception should cause it to be raised, but raise just in case
--> 212                     raise attempt.get()
    213                 else:

~/miniconda3/envs/dl/lib/python3.5/site-packages/retrying.py in get(self, wrap_exception)
    246             else:
--> 247                 six.reraise(self.value[0], self.value[1], self.value[2])
    248         else:

~/miniconda3/envs/dl/lib/python3.5/site-packages/six.py in reraise(tp, value, tb)
    692                 raise value.with_traceback(tb)
--> 693             raise value
    694         finally:

~/miniconda3/envs/dl/lib/python3.5/site-packages/retrying.py in call(self, fn, *args, **kwargs)
    199             try:
--> 200                 attempt = Attempt(fn(*args, **kwargs), attempt_number, False)
    201             except:

~/miniconda3/envs/dl/lib/python3.5/site-packages/plotly/io/_orca.py in request_image_with_retrying(**kwargs)
   1200     json_str = json.dumps(request_params, cls=plotly.utils.PlotlyJSONEncoder)
-> 1201     response = requests.post(server_url + '/', data=json_str)
   1202     return response

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/api.py in post(url, data, json, **kwargs)
    109 
--> 110     return request('post', url, data=data, json=json, **kwargs)
    111 

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/api.py in request(method, url, **kwargs)
     55     with sessions.Session() as session:
---> 56         return session.request(method=method, url=url, **kwargs)
     57 

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/sessions.py in request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    474         send_kwargs.update(settings)
--> 475         resp = self.send(prep, **send_kwargs)
    476 

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/sessions.py in send(self, request, **kwargs)
    595         # Send the request
--> 596         r = adapter.send(request, **kwargs)
    597 

~/miniconda3/envs/dl/lib/python3.5/site-packages/requests/adapters.py in send(self, request, stream, timeout, verify, cert, proxies)
    486 
--> 487             raise ConnectionError(e, request=request)
    488 

ConnectionError: HTTPConnectionPool(host='localhost', port=39167): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7eff4fb6cb00>: Failed to establish a new connection: [Errno 111] Connection refused',))

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-5-0659bc3df946> in <module>()
     46                 outpath = '/home/gabe/Documents/intheon/project-sources/HJF/figures/%s/%s/%s/%s' % (exp[k], stim[i], stim_t, filename)
     47 
---> 48                 fig = plotERP(a, title, outpath)
     49 
     50 #                 a = SelectSegments(selection=[c], search_domain='target values')(ersp_pkts)

<ipython-input-3-7e4b99ffee4e> in plotERP(pkt, title, outpath)
      7     fig = LinePlot(x_axis='time', multiline_axis='space',grid_col_axis='feature', 
      8              plot_title=title, y_title='Amplitude (uV)',
----> 9              plot_width=600, plot_height=400, output_mode='notebook', output_path=outpath)(data=pkt)
     10     return fig

~/Documents/intheon/cpe/neuropype/engine/node.py in __call__(self, update, return_outputs, *args, **kwargs)
    371             # specifically not call it by assigning/wiring a False value to it).
    372             if update:
--> 373                 self.update = True
    374             self._update = update
    375 

~/Documents/intheon/cpe/neuropype/engine/ports.py in __set__(self, obj, value)
    307         else:
    308             with self.owner().mutex:
--> 309                 self._do_set(obj, value)
    310 
    311     def getter(self, fget):

~/Documents/intheon/cpe/neuropype/engine/ports.py in _do_set(self, obj, value)
    391             self._default_set(obj, value)
    392         else:
--> 393             self.fset(obj, value)
    394 
    395     def _default_validate(self, value):

~/Documents/intheon/cpe/neuropype/nodes/reporting/LinePlot.py in update(self, v)
    349                     if not os.path.exists(path):
    350                         os.makedirs(path)
--> 351                 pio.write_image(fig, self.output_path, format='png')
    352 
    353             if self.output_mode == 'report':

~/miniconda3/envs/dl/lib/python3.5/site-packages/plotly/io/_orca.py in write_image(fig, file, format, scale, width, height, validate)
   1490                         width=width,
   1491                         height=height,
-> 1492                         validate=validate)
   1493 
   1494     # Open file

~/miniconda3/envs/dl/lib/python3.5/site-packages/plotly/io/_orca.py in to_image(fig, format, width, height, scale, validate)
   1320 
   1321 {info}
-> 1322 """.format(info=status_str))
   1323         else:
   1324             # Reset the status so that if the user tries again, we'll try to

ValueError: 
For some reason plotly.py was unable to communicate with the
local orca server process, even though the server process seems to be running.

Please review the process and connection information below:

orca status
-----------
    state: running
    executable: /home/gabe/miniconda3/envs/dl/lib/orca_app/orca
    version: 1.1.1
    port: 39167
    pid: 12045
    command: ['/home/gabe/miniconda3/envs/dl/lib/orca_app/orca', 'serve', '-p', '39167', '--plotly', '/home/gabe/miniconda3/envs/dl/lib/python3.5/site-packages/plotly/package_data/plotly.min.js', '--graph-only', '--mathjax', 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js']
    

Any idea on how to resolve this? It seems to stem from an issue with creating an HTTP connection with the orca server. Iā€™ve investigated some things about killing a stray Orca process that seemed to be left behind, but it did not resolve the issue. Killing and restarting Jupyter also did not resolve the issue.

1 Like

Hi @gibagon,

Is it possible that you have a local firewall running thatā€™s blocking the port plotly.py selected to use to communicate with the orca process? (39167 in this case). This isnā€™t an area I have much experience troubleshooting, but itā€™s worth looking into. You can explicitly specify the port for orca to use by setting the plotly.io.orca.config.port before running the image export operation. E.g.

plotly.io.orca.config.port = 8999

-Jon

@papaDavid5,
Could you figure out how to get it to work with Chrome ? If so, could you kindly share your findings

Probably the best reference for this question today is this documentation page on Static Image Export in Plotly.py: https://plot.ly/python/static-image-export/