Repository: PrefectHQ/prefect

Issue:
Flow state visualization fails if a mapped task is skipped
## Description

When a flow contains a task which maps over a collection and this task is skipped, the visualization of the flow state fails with the following error message:

> AttributeError: 'Skipped' object has no attribute 'map_states'

The flow itself executes successfully.

## Expectation
Mapped tasks should be visualised without error as a grey box. This was the case in earlier versions of prefect.


## Reproduction
A slightly modified version of the ETL flow from the documentation:

```python
from prefect import task, Flow, Parameter
from prefect.tasks.control_flow.conditional import ifelse

@task
def extract():
    """Get a list of data"""
    return [1, 2, 3]

@task
def transform(data):
    """Multiply the input by 10"""
    return [i * 10 for i in data]

@task
def load(data):
    """Print the data to indicate it was received"""
    print("Here's your data: {}".format(data))


with Flow('ETL') as flow:
    do_load = Parameter('do_load')
    e = extract()
    t = transform(e)
    l = load.map(t)
    ifelse(do_load, l, None)

state = flow.run(do_load=False)
flow.visualize(flow_state=state)
```

Both changing `do_load` to `True` or removing the `map` on the load task will lead to a successful visualization being produced.

Output:

```
[2019-11-21 09:41:33,824] INFO - prefect.FlowRunner | Beginning Flow run for 'ETL'
[2019-11-21 09:41:33,826] INFO - prefect.FlowRunner | Starting flow run.
[2019-11-21 09:41:33,831] INFO - prefect.TaskRunner | Task 'extract': Starting task run...
[2019-11-21 09:41:33,834] INFO - prefect.TaskRunner | Task 'extract': finished task run for task with final state: 'Success'
[2019-11-21 09:41:33,839] INFO - prefect.TaskRunner | Task 'do_load': Starting task run...
[2019-11-21 09:41:33,841] INFO - prefect.TaskRunner | Task 'do_load': finished task run for task with final state: 'Success'
[2019-11-21 09:41:33,848] INFO - prefect.TaskRunner | Task 'CompareValue: "False"': Starting task run...
[2019-11-21 09:41:33,850] INFO - prefect.TaskRunner | Task 'CompareValue: "False"': finished task run for task with final state: 'Success'
[2019-11-21 09:41:33,856] INFO - prefect.TaskRunner | Task 'CompareValue: "True"': Starting task run...
[2019-11-21 09:41:33,858] INFO - prefect.TaskRunner | Task 'CompareValue: "True"': finished task run for task with final state: 'Skipped'
[2019-11-21 09:41:33,863] INFO - prefect.TaskRunner | Task 'transform': Starting task run...
[2019-11-21 09:41:33,865] INFO - prefect.TaskRunner | Task 'transform': finished task run for task with final state: 'Success'
[2019-11-21 09:41:33,870] INFO - prefect.TaskRunner | Task 'load': Starting task run...
[2019-11-21 09:41:33,872] INFO - prefect.TaskRunner | Task 'load': finished task run for task with final state: 'Skipped'
[2019-11-21 09:41:33,874] INFO - prefect.FlowRunner | Flow run SUCCESS: all reference tasks succeeded
Traceback (most recent call last):
  File "min_example.py", line 28, in <module>
    flow.visualize(flow_state=state)
  File "/Users/jtherhaa/miniconda3/envs/prefect/lib/python3.6/site-packages/prefect/core/flow.py", line 1089, in visualize
    for map_index, _ in enumerate(flow_state.result[t].map_states):
AttributeError: 'Skipped' object has no attribute 'map_states'
```

## Environment
prefect 0.7.2 on macOS Mojave 10.14.6



Discussion:

Resolving patch:
diff --git a/src/prefect/core/flow.py b/src/prefect/core/flow.py
--- a/src/prefect/core/flow.py
+++ b/src/prefect/core/flow.py
@@ -1086,13 +1086,21 @@ def get_color(task: Task, map_index: int = None) -> str:
             name = "{} <map>".format(t.name) if is_mapped else t.name
             if is_mapped and flow_state:
                 assert isinstance(flow_state.result, dict)
-                for map_index, _ in enumerate(flow_state.result[t].map_states):
+                if flow_state.result[t].is_mapped():
+                    for map_index, _ in enumerate(flow_state.result[t].map_states):
+                        kwargs = dict(
+                            color=get_color(t, map_index=map_index),
+                            style="filled",
+                            colorscheme="svg",
+                        )
+                        graph.node(
+                            str(id(t)) + str(map_index), name, shape=shape, **kwargs
+                        )
+                else:
                     kwargs = dict(
-                        color=get_color(t, map_index=map_index),
-                        style="filled",
-                        colorscheme="svg",
+                        color=get_color(t), style="filled", colorscheme="svg",
                     )
-                    graph.node(str(id(t)) + str(map_index), name, shape=shape, **kwargs)
+                    graph.node(str(id(t)), name, shape=shape, **kwargs)
             else:
                 kwargs = (
                     {}
@@ -1108,15 +1116,22 @@ def get_color(task: Task, map_index: int = None) -> str:
                 or any(edge.mapped for edge in self.edges_to(e.downstream_task))
             ) and flow_state:
                 assert isinstance(flow_state.result, dict)
-                for map_index, _ in enumerate(
-                    flow_state.result[e.downstream_task].map_states
-                ):
-                    upstream_id = str(id(e.upstream_task))
-                    if any(edge.mapped for edge in self.edges_to(e.upstream_task)):
-                        upstream_id += str(map_index)
+                down_state = flow_state.result[e.downstream_task]
+                if down_state.is_mapped():
+                    for map_index, _ in enumerate(down_state.map_states):
+                        upstream_id = str(id(e.upstream_task))
+                        if any(edge.mapped for edge in self.edges_to(e.upstream_task)):
+                            upstream_id += str(map_index)
+                        graph.edge(
+                            upstream_id,
+                            str(id(e.downstream_task)) + str(map_index),
+                            e.key,
+                            style=style,
+                        )
+                else:
                     graph.edge(
-                        upstream_id,
-                        str(id(e.downstream_task)) + str(map_index),
+                        str(id(e.upstream_task)),
+                        str(id(e.downstream_task)),
                         e.key,
                         style=style,
                     )


Test patch:
diff --git a/tests/core/test_flow.py b/tests/core/test_flow.py
--- a/tests/core/test_flow.py
+++ b/tests/core/test_flow.py
@@ -1041,6 +1041,25 @@ def test_viz_reflects_mapping(self):
             "label=y style=dashed" not in graph.source
         )  # constants are no longer represented
 
+    def test_viz_can_handle_skipped_mapped_tasks(self):
+        ipython = MagicMock(
+            get_ipython=lambda: MagicMock(config=dict(IPKernelApp=True))
+        )
+        with patch.dict("sys.modules", IPython=ipython):
+            with Flow(name="test") as f:
+                t = Task(name="a_list_task")
+                res = AddTask(name="a_nice_task").map(x=t, y=8)
+
+            graph = f.visualize(
+                flow_state=Success(result={t: Success(), res: Skipped()})
+            )
+        assert 'label="a_nice_task <map>" color="#62757f80"' in graph.source
+        assert 'label=a_list_task color="#28a74580"' in graph.source
+        assert "label=x style=dashed" in graph.source
+        assert (
+            "label=y style=dashed" not in graph.source
+        )  # constants are no longer represented
+
     @pytest.mark.parametrize("state", [Success(), Failed(), Skipped()])
     def test_viz_if_flow_state_provided(self, state):
         import graphviz