Repository: dask/dask

Issue:
Consider better node label representation of dot_graph
Even after #556, `DataFrame` ops name is little long because of hash id. Thus, `dot_graph` output is little difficult to read. How about adding an keyword option to `dot_graph` which limit the max character length of nodes.
#### Current output (DataFrame.groupby().mean() with 2 partitions.

![index](https://cloud.githubusercontent.com/assets/1696302/9155154/84ceb97a-3ee9-11e5-800d-da57d86fa66c.png)

Would like to ask what the kw name and its default should be. My idea is  `label_length`, and its default is 20. It only touches operation name leaving partition number as it is.



Discussion:
We could also identify hash strings (match `[0-9a-f]{32}`) and replace them with a symbol like `#`.

I vote for replacing the hash with `#`. The only (potential) downside is that keys will repeat themselves in the graph if the same function is done more than once. But the shape of the graph is the important part.

I can take care of this tomorrow if no one beats me too it.  Looks like we call `str(dep)` at one point.  This should probably be a more interesting function.

Yes, replacing hash looks better idea than splitting by length.

Gr8! It can be nicer if replace each hash enumerating up, like `#1, #2, #3...`. Because we may want to distinguish each data source (like merge example below). Let me reopen if agreed.

![2015-08-11 1 50 20](https://cloud.githubusercontent.com/assets/1696302/9177296/60331f1a-3fcb-11e5-8320-24a1514cd250.png)

Sounds reasonable to me


Resolving patch:
diff --git a/dask/dot.py b/dask/dot.py
--- a/dask/dot.py
+++ b/dask/dot.py
@@ -56,7 +56,11 @@ def name(x):
     except TypeError:
         return str(hash(str(x)))
 
-def label(x):
+
+_HASHPAT = re.compile('([0-9a-z]{32})')
+
+
+def label(x, cache=None):
     """
 
     >>> label('x')
@@ -74,7 +78,17 @@ def label(x):
     'x-#-hello'
     """
     s = str(x)
-    s = re.sub('[0-9a-z]{32}', '#', s)
+    m = re.search(_HASHPAT, s)
+    if m is not None:
+        for h in m.groups():
+            if cache is not None:
+                n = cache.get(h, len(cache))
+                label = '#{0}'.format(n)
+                # cache will be overwritten destructively
+                cache[h] = n
+            else:
+                label = '#'
+            s = s.replace(h, label)
     return s
 
 
@@ -87,12 +101,13 @@ def to_graphviz(dsk, data_attributes=None, function_attributes=None):
     g = Digraph(graph_attr={'rankdir': 'BT'})
 
     seen = set()
+    cache = {}
 
     for k, v in dsk.items():
         k_name = name(k)
         if k_name not in seen:
             seen.add(k_name)
-            g.node(k_name, label=label(k), shape='box',
+            g.node(k_name, label=label(k, cache=cache), shape='box',
                    **data_attributes.get(k, {}))
 
         if istask(v):
@@ -107,7 +122,7 @@ def to_graphviz(dsk, data_attributes=None, function_attributes=None):
                 dep_name = name(dep)
                 if dep_name not in seen:
                     seen.add(dep_name)
-                    g.node(dep_name, label=label(dep), shape='box',
+                    g.node(dep_name, label=label(dep, cache=cache), shape='box',
                            **data_attributes.get(dep, {}))
                 g.edge(dep_name, func_name)
         elif ishashable(v) and v in dsk:


Test patch:
diff --git a/dask/tests/test_dot.py b/dask/tests/test_dot.py
--- a/dask/tests/test_dot.py
+++ b/dask/tests/test_dot.py
@@ -7,7 +7,7 @@
 
 pytest.importorskip("graphviz")
 
-from dask.dot import dot_graph, task_label, to_graphviz
+from dask.dot import dot_graph, task_label, label, to_graphviz
 
 # Since graphviz doesn't store a graph, we need to parse the output
 label_re = re.compile('.*\[label=(.*?) shape=.*\]')
@@ -31,6 +31,30 @@ def test_task_label():
     assert task_label((add, (add, 1, 2))) == 'add(...)'
 
 
+def test_label():
+    assert label('x') == 'x'
+    assert label('elemwise-ffcd9aa2231d466b5aa91e8bfa9e9487') == 'elemwise-#'
+
+    cache = {}
+    result = label('elemwise-ffcd9aa2231d466b5aa91e8bfa9e9487', cache=cache)
+    assert result == 'elemwise-#0'
+    # cached
+    result = label('elemwise-ffcd9aa2231d466b5aa91e8bfa9e9487', cache=cache)
+    assert result == 'elemwise-#0'
+    assert len(cache) == 1
+
+    result = label('elemwise-e890b510984f344edea9a5e5fe05c0db', cache=cache)
+    assert result == 'elemwise-#1'
+    assert len(cache) == 2
+
+    result = label('elemwise-ffcd9aa2231d466b5aa91e8bfa9e9487', cache=cache)
+    assert result == 'elemwise-#0'
+    assert len(cache) == 2
+
+    assert label('x', cache=cache) == 'x'
+    assert len(cache) == 2
+
+
 def test_to_graphviz():
     g = to_graphviz(dsk)
     labels = list(filter(None, map(get_label, g.body)))