Repository: dask/dask

Issue:
`dask.visualize` does not work with `filename=None`
According to the documentation for [`dask.visualize`](https://docs.dask.org/en/latest/api.html#dask.visualize),

> **If filename is None, no file will be written**, and we communicate with dot using only pipes.

Unfortunately, it seems this actually throws an error!

```
/python3.8/site-packages/dask/dot.py in <genexpr>(.0)
    274 def graphviz_to_file(g, filename, format):
    275     fmts = [".png", ".pdf", ".dot", ".svg", ".jpeg", ".jpg"]
--> 276     if format is None and any(filename.lower().endswith(fmt) for fmt in fmts):
    277         filename, format = os.path.splitext(filename)
    278         format = format[1:].lower()

AttributeError: 'NoneType' object has no attribute 'lower'
```

Based on the documentation, it sounds like `filename=None` is meant to be supported, but [based on the tests for `dask.visualize`](https://github.com/dask/dask/blob/main/dask/tests/test_base.py#L936-L955), this case was never actually tested for and this error slipped under the radar.
`dask.visualize` does not work with `filename=None`
According to the documentation for [`dask.visualize`](https://docs.dask.org/en/latest/api.html#dask.visualize),

> **If filename is None, no file will be written**, and we communicate with dot using only pipes.

Unfortunately, it seems this actually throws an error!

```
/python3.8/site-packages/dask/dot.py in <genexpr>(.0)
    274 def graphviz_to_file(g, filename, format):
    275     fmts = [".png", ".pdf", ".dot", ".svg", ".jpeg", ".jpg"]
--> 276     if format is None and any(filename.lower().endswith(fmt) for fmt in fmts):
    277         filename, format = os.path.splitext(filename)
    278         format = format[1:].lower()

AttributeError: 'NoneType' object has no attribute 'lower'
```

Based on the documentation, it sounds like `filename=None` is meant to be supported, but [based on the tests for `dask.visualize`](https://github.com/dask/dask/blob/main/dask/tests/test_base.py#L936-L955), this case was never actually tested for and this error slipped under the radar.


Discussion:
Thanks for reporting @theXYZT! I'm able to reproduce with the snippet below:

```python
In [1]: import dask

In [2]: import dask.array as da

In [3]: x = da.arange(10)

In [4]: dask.visualize(x, filename=None)
```

It looks like there is some logic later in `graphviz_to_file` that handles the `filename=None` case:

https://github.com/dask/dask/blob/c5633c21e754df6b2c13195f1e2688e347c48963/dask/dot.py#L295-L296

Perhaps we can try to move that further up or generalize the code above it to handle when `filename=None`
Might be something @freyam might like to take a look at? (What do you think, @martindurant)
Sounds like a perfect introduction to the dask graphviz code.
Thanks for reporting @theXYZT! I'm able to reproduce with the snippet below:

```python
In [1]: import dask

In [2]: import dask.array as da

In [3]: x = da.arange(10)

In [4]: dask.visualize(x, filename=None)
```

It looks like there is some logic later in `graphviz_to_file` that handles the `filename=None` case:

https://github.com/dask/dask/blob/c5633c21e754df6b2c13195f1e2688e347c48963/dask/dot.py#L295-L296

Perhaps we can try to move that further up or generalize the code above it to handle when `filename=None`
Might be something @freyam might like to take a look at? (What do you think, @martindurant)
Sounds like a perfect introduction to the dask graphviz code.

Resolving patch:
diff --git a/dask/dot.py b/dask/dot.py
--- a/dask/dot.py
+++ b/dask/dot.py
@@ -273,7 +273,12 @@ def dot_graph(dsk, filename="mydask", format=None, **kwargs):
 
 def graphviz_to_file(g, filename, format):
     fmts = [".png", ".pdf", ".dot", ".svg", ".jpeg", ".jpg"]
-    if format is None and any(filename.lower().endswith(fmt) for fmt in fmts):
+
+    if (
+        format is None
+        and filename is not None
+        and any(filename.lower().endswith(fmt) for fmt in fmts)
+    ):
         filename, format = os.path.splitext(filename)
         format = format[1:].lower()
 
@@ -292,7 +297,7 @@ def graphviz_to_file(g, filename, format):
 
     display_cls = _get_display_cls(format)
 
-    if not filename:
+    if filename is None:
         return display_cls(data=data)
 
     full_filename = ".".join([filename, format])


Test patch:
diff --git a/dask/tests/test_base.py b/dask/tests/test_base.py
--- a/dask/tests/test_base.py
+++ b/dask/tests/test_base.py
@@ -954,6 +954,10 @@ def test_visualize():
         visualize(x, filename=os.path.join(d, "mydask.png"))
         assert os.path.exists(os.path.join(d, "mydask.png"))
 
+        # To see if visualize() works when the filename parameter is set to None
+        # If the function raises an error, the test will fail
+        x.visualize(filename=None)
+
 
 @pytest.mark.skipif("not da")
 @pytest.mark.skipif(