Repository: Qiskit/qiskit-terra (python)

Issue:
timeline_drawer measurement bugs
There are some bugs drawing measurements in the circuit timeline drawer. Taking a simple circuit and a backend to schedule on:

```py
from qiskit import QuantumCircuit, transpile
from qiskit.test.mock import FakeParis
from qiskit.visualization import timeline_drawer

qc = QuantumCircuit(2, 2)
qc.cx(0, 1)

backend = FakeParis()
```

The following scenarios have various errors...

1- One measurement with show_clbits=False
```py
qc.measure(0, 0)
new_qc = transpile(qc, backend, scheduling_method='alap')
timeline_drawer(new_qc, show_clbits=False)
```
`KeyError: Clbit(ClassicalRegister(2, 'c'), 0)`

2- One measurement with show_clbits=True
```py
qc.measure(0, 0)
new_qc = transpile(qc, backend, initial_layout=[25, 26], scheduling_method='alap')
timeline_drawer(new_qc, show_clbits=True)
```

The clbit measure seems off and starts earlier than the qubit measure.

![image](https://user-images.githubusercontent.com/8622381/98508024-bbd2ec80-222c-11eb-884e-0e9ea7959c98.png)

3- Two measurement with show_clbits=False
```py
qc.measure(0, 0)
qc.measure(1, 1)
new_qc = transpile(qc, backend, initial_layout=[25, 26], scheduling_method='alap')
timeline_drawer(new_qc, show_clbits=False)
```

`TypeError: '<=' not supported between instances of 'float' and 'NoneType'`

4- Two measurements with show_clbits=True
```py
qc.measure(0, 0)
qc.measure(1, 1)
new_qc = transpile(qc, backend, initial_layout=[25, 26], scheduling_method='alap')
timeline_drawer(new_qc, show_clbits=True)
```

`AttributeError: 'list' object has no attribute 'repeat'`

Pull request:
Fix handling of classical bits in timeline drawer and non-treament of delay as idle time
Fixes a bug when visualizing timeline of scheduled circuits, when classical bits existed.

Fixes a bug where `idle_wires=False` still showed wires that only had delay (they are idle and should be muted).

Fixes #5361 

Patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
index 1f48ba579..d04854325 100644
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -2473,7 +2473,8 @@ class QuantumCircuit:
             # circuit has only delays, this is kind of scheduled
             for inst, _, _ in self.data:
                 if not isinstance(inst, Delay):
-                    raise CircuitError("qubit_start_time is defined only for scheduled circuit.")
+                    raise CircuitError("qubit_start_time undefined. "
+                                       "Circuit must be scheduled first.")
             return 0
 
         qubits = [self.qubits[q] if isinstance(q, int) else q for q in qubits]
@@ -2513,7 +2514,8 @@ class QuantumCircuit:
             # circuit has only delays, this is kind of scheduled
             for inst, _, _ in self.data:
                 if not isinstance(inst, Delay):
-                    raise CircuitError("qubit_stop_time is defined only for scheduled circuit.")
+                    raise CircuitError("qubit_stop_time undefined. "
+                                       "Circuit must be scheduled first.")
             return 0
 
         qubits = [self.qubits[q] if isinstance(q, int) else q for q in qubits]
diff --git a/qiskit/visualization/circuit_visualization.py b/qiskit/visualization/circuit_visualization.py
index 9ce297978..40d00125a 100644
--- a/qiskit/visualization/circuit_visualization.py
+++ b/qiskit/visualization/circuit_visualization.py
@@ -282,6 +282,7 @@ def _text_circuit_drawer(circuit, filename=None, reverse_bits=False,
                                                         reverse_bits=reverse_bits,
                                                         justify=justify,
                                                         idle_wires=idle_wires)
+
     if with_layout:
         layout = circuit._layout
     else:
diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py
index 9f440dea4..2f062363c 100644
--- a/qiskit/visualization/timeline/core.py
+++ b/qiskit/visualization/timeline/core.py
@@ -136,6 +136,8 @@ class DrawerCanvas:
         Args:
             data: New drawing to add.
         """
+        if not self.formatter['control.show_clbits']:
+            data.bits = [b for b in data.bits if not isinstance(b, circuit.Clbit)]
         self._collections[data.data_key] = data
 
     def load_program(self,
@@ -299,7 +301,7 @@ class DrawerCanvas:
             return True
 
         def _associated_bit_check(_data):
-            """If all associated bits are not shown."""
+            """If any associated bit is not shown."""
             if all([bit not in self.assigned_coordinates for bit in _data.bits]):
                 return False
             return True
@@ -377,11 +379,12 @@ class DrawerCanvas:
 
         This method dynamically shifts horizontal position of links if they are overlapped.
         """
-        allowed_overlap = self.formatter['margin.link_interval_dt']
+        duration = self.time_range[1] - self.time_range[0]
+        allowed_overlap = self.formatter['margin.link_interval_percent'] * duration
 
         # return y coordinates
         def y_coords(link: drawings.GateLinkData):
-            return np.array([self.assigned_coordinates.get(bit, None) for bit in link.bits])
+            return np.array([self.assigned_coordinates.get(bit, np.nan) for bit in link.bits])
 
         # group overlapped links
         overlapped_group = []
diff --git a/qiskit/visualization/timeline/interface.py b/qiskit/visualization/timeline/interface.py
index 8cc377ef8..47bdd26bf 100644
--- a/qiskit/visualization/timeline/interface.py
+++ b/qiskit/visualization/timeline/interface.py
@@ -112,10 +112,10 @@ def draw(program: circuit.QuantumCircuit,
             the right limit of the horizontal axis. The value is in units of percentage of
             the whole program duration. If the duration is 100 and the value of 0.5 is set,
             this keeps right margin of 5 (default `0.02`).
-        formatter.margin.link_interval_dt: Allowed overlap of gate links.
+        formatter.margin.link_interval_percent: Allowed overlap of gate links.
             If multiple gate links are drawing within this range, links are horizontally
-            shifted not to overlap with each other. This value is in units of
-            the system cycle time dt (default `20`).
+            shifted not to overlap with each other. The value is in units of percentage of
+            the whole program duration (default `0.01`).
         formatter.time_bucket.edge_dt: The length of round edge of gate boxes. Gate boxes are
             smoothly faded in and out from the zero line. This value is in units of
             the system cycle time dt (default `10`).
@@ -138,6 +138,8 @@ def draw(program: circuit.QuantumCircuit,
                     'u2': '#FA74A6',
                     'u3': '#FA74A6',
                     'id': '#05BAB6',
+                    'sx': '#FA74A6',
+                    'sxdg': '#FA74A6',
                     'x': '#05BAB6',
                     'y': '#05BAB6',
                     'z': '#05BAB6',
@@ -155,7 +157,7 @@ def draw(program: circuit.QuantumCircuit,
                     'r': '#BB8BFF',
                     'rx': '#BB8BFF',
                     'ry': '#BB8BFF',
-                    'rz': '#BB8BFF',
+                    'rz': '#000000',
                     'reset': '#808080',
                     'measure': '#808080'
                 }
@@ -185,6 +187,8 @@ def draw(program: circuit.QuantumCircuit,
                     'swap': r'{\rm SWAP}',
                     's': r'{\rm S}',
                     'sdg': r'{\rm S}^\dagger',
+                    'sx': r'{\rm √X}',
+                    'sxdg': r'{\rm √X}^\dagger',
                     'dcx': r'{\rm DCX}',
                     'iswap': r'{\rm iSWAP}',
                     't': r'{\rm T}',
diff --git a/qiskit/visualization/timeline/plotters/matplotlib.py b/qiskit/visualization/timeline/plotters/matplotlib.py
index a3bd73ef3..1829714e3 100644
--- a/qiskit/visualization/timeline/plotters/matplotlib.py
+++ b/qiskit/visualization/timeline/plotters/matplotlib.py
@@ -86,8 +86,8 @@ class MplPlotter(BasePlotter):
         """Output drawings stored in canvas object."""
 
         for _, data in self.canvas.collections:
-            xvals = data.xvals
-            yvals = data.yvals
+            xvals = np.asarray(data.xvals, dtype=float)
+            yvals = np.asarray(data.yvals, dtype=float)
             offsets = [self.canvas.assigned_coordinates[bit] for bit in data.bits]
 
             if isinstance(data, drawings.BoxData):
diff --git a/qiskit/visualization/timeline/stylesheet.py b/qiskit/visualization/timeline/stylesheet.py
index cc51bd0f2..b56d4d2c3 100644
--- a/qiskit/visualization/timeline/stylesheet.py
+++ b/qiskit/visualization/timeline/stylesheet.py
@@ -45,7 +45,7 @@ from qiskit.visualization.timeline import generators, layouts
 
 class QiskitTimelineStyle(dict):
     """Stylesheet for pulse drawer."""
-    _deprecated_keys = {}
+    _deprecated_keys = {'link_interval_dt': 'link_interval_percent'}
 
     def __init__(self):
         super().__init__()
@@ -199,7 +199,7 @@ def default_style() -> Dict[str, Any]:
         'formatter.margin.bottom': 0.5,
         'formatter.margin.left_percent': 0.02,
         'formatter.margin.right_percent': 0.02,
-        'formatter.margin.link_interval_dt': 20,
+        'formatter.margin.link_interval_percent': 0.01,
         'formatter.margin.minimum_duration': 50,
         'formatter.time_bucket.edge_dt': 10,
         'formatter.color.background': '#FFFFFF',
@@ -213,6 +213,8 @@ def default_style() -> Dict[str, Any]:
             'u2': '#FA74A6',
             'u3': '#FA74A6',
             'id': '#05BAB6',
+            'sx': '#FA74A6',
+            'sxdg': '#FA74A6',
             'x': '#05BAB6',
             'y': '#05BAB6',
             'z': '#05BAB6',
@@ -230,7 +232,7 @@ def default_style() -> Dict[str, Any]:
             'r': '#BB8BFF',
             'rx': '#BB8BFF',
             'ry': '#BB8BFF',
-            'rz': '#BB8BFF',
+            'rz': '#000000',
             'reset': '#808080',
             'measure': '#808080'
         },
@@ -251,6 +253,8 @@ def default_style() -> Dict[str, Any]:
             'swap': r'{\rm SWAP}',
             's': r'{\rm S}',
             'sdg': r'{\rm S}^\dagger',
+            'sx': r'{\rm √X}',
+            'sxdg': r'{\rm √X}^\dagger',
             'dcx': r'{\rm DCX}',
             'iswap': r'{\rm iSWAP}',
             't': r'{\rm T}',
diff --git a/qiskit/visualization/utils.py b/qiskit/visualization/utils.py
index 6ef41d20a..153680f19 100644
--- a/qiskit/visualization/utils.py
+++ b/qiskit/visualization/utils.py
@@ -82,7 +82,7 @@ def _get_layered_instructions(circuit, reverse_bits=False,
     Given a circuit, return a tuple (qregs, cregs, ops) where
     qregs and cregs are the quantum and classical registers
     in order (based on reverse_bits) and ops is a list
-    of DAG nodes which type is "operation".
+    of DAG nodes whose type is "operation".
 
     Args:
         circuit (QuantumCircuit): From where the information is extracted.
@@ -121,13 +121,18 @@ def _get_layered_instructions(circuit, reverse_bits=False,
         qregs.reverse()
         cregs.reverse()
 
+    # Optionally remove all idle wires and instructions that are on them and
+    # on them only.
     if not idle_wires:
-        for wire in dag.idle_wires(ignore=['barrier']):
+        for wire in dag.idle_wires(ignore=['barrier', 'delay']):
             if wire in qregs:
                 qregs.remove(wire)
             if wire in cregs:
                 cregs.remove(wire)
 
+    ops = [[op for op in layer if any(q in qregs for q in op.qargs)]
+           for layer in ops]
+
     return qregs, cregs, ops
 
 
diff --git a/releasenotes/notes/idle-time-visualization-b5404ad875cbdae4.yaml b/releasenotes/notes/idle-time-visualization-b5404ad875cbdae4.yaml
new file mode 100644
index 000000000..7ccf4ef0d
--- /dev/null
+++ b/releasenotes/notes/idle-time-visualization-b5404ad875cbdae4.yaml
@@ -0,0 +1,8 @@
+---
+fixes:
+  - |
+    Fixes a bug with the :func:`~qiskit.visualization.timeline_drawer` function when handling classical bits.
+  - |
+    Fixes an issue with delays not being considered as idle in circuit
+    visualization.
+    


Test patch:
diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py
index fb5b08a71..ee14b4423 100644
--- a/test/python/visualization/test_circuit_text_drawer.py
+++ b/test/python/visualization/test_circuit_text_drawer.py
@@ -1901,6 +1901,18 @@ class TestTextIdleWires(QiskitTestCase):
         circuit.barrier(qr[1], qr[2])
         self.assertEqual(str(_text_circuit_drawer(circuit, idle_wires=False)), expected)
 
+    def test_text_barrier_delay(self):
+        """idle_wires should ignore delay"""
+        expected = '\n'.join(["         ┌───┐ ░  ",
+                              "qr_1: |0>┤ H ├─░──",
+                              "         └───┘ ░  "])
+        qr = QuantumRegister(4, 'qr')
+        circuit = QuantumCircuit(qr)
+        circuit.h(qr[1])
+        circuit.barrier()
+        circuit.delay(100, qr[2])
+        self.assertEqual(str(_text_circuit_drawer(circuit, idle_wires=False)), expected)
+
 
 class TestTextNonRational(QiskitTestCase):
     """ non-rational numbers are correctly represented """
diff --git a/test/python/visualization/timeline/test_core.py b/test/python/visualization/timeline/test_core.py
index 09d35c902..7ebcb722c 100644
--- a/test/python/visualization/timeline/test_core.py
+++ b/test/python/visualization/timeline/test_core.py
@@ -82,7 +82,9 @@ class TestCanvas(QiskitTestCase):
         """Test shifting gate link overlap."""
         canvas = core.DrawerCanvas(stylesheet=self.style)
         canvas.formatter.update({
-            'formatter.margin.link_interval_dt': 20
+            'margin.link_interval_percent': 0.01,
+            'margin.left_percent': 0,
+            'margin.right_percent': 0
         })
         canvas.generator = {
             'gates': [],
@@ -101,8 +103,8 @@ class TestCanvas(QiskitTestCase):
 
         self.assertEqual(len(drawings_tested), 2)
 
-        self.assertListEqual(drawings_tested[0][1].xvals, [710.])
-        self.assertListEqual(drawings_tested[1][1].xvals, [690.])
+        self.assertListEqual(drawings_tested[0][1].xvals, [706.])
+        self.assertListEqual(drawings_tested[1][1].xvals, [694.])
 
         ref_keys = list(canvas._collections.keys())
         self.assertEqual(drawings_tested[0][0], ref_keys[0])
@@ -144,3 +146,65 @@ class TestCanvas(QiskitTestCase):
 
         canvas.load_program(circ)
         self.assertEqual(len(canvas._collections), 1)
+
+    def test_multi_measurement_with_clbit_not_shown(self):
+        """Test generating bit link drawings of measurements when clbits is disabled."""
+        circ = QuantumCircuit(2, 2)
+        circ.measure(0, 0)
+        circ.measure(1, 1)
+
+        circ = transpile(circ,
+                         scheduling_method='alap',
+                         basis_gates=[],
+                         instruction_durations=[('measure', 0, 2000), ('measure', 1, 2000)],
+                         optimization_level=0)
+
+        canvas = core.DrawerCanvas(stylesheet=self.style)
+        canvas.formatter.update({
+            'control.show_clbits': False
+        })
+        canvas.layout = {
+            'bit_arrange': layouts.qreg_creg_ascending,
+            'time_axis_map': layouts.time_map_in_dt
+        }
+        canvas.generator = {
+            'gates': [],
+            'bits': [],
+            'barriers': [],
+            'gate_links': [generators.gen_gate_link]
+        }
+
+        canvas.load_program(circ)
+        canvas.update()
+        self.assertEqual(len(canvas._output_dataset), 0)
+
+    def test_multi_measurement_with_clbit_shown(self):
+        """Test generating bit link drawings of measurements when clbits is enabled."""
+        circ = QuantumCircuit(2, 2)
+        circ.measure(0, 0)
+        circ.measure(1, 1)
+
+        circ = transpile(circ,
+                         scheduling_method='alap',
+                         basis_gates=[],
+                         instruction_durations=[('measure', 0, 2000), ('measure', 1, 2000)],
+                         optimization_level=0)
+
+        canvas = core.DrawerCanvas(stylesheet=self.style)
+        canvas.formatter.update({
+            'control.show_clbits': True
+        })
+        canvas.layout = {
+            'bit_arrange': layouts.qreg_creg_ascending,
+            'time_axis_map': layouts.time_map_in_dt
+        }
+        canvas.generator = {
+            'gates': [],
+            'bits': [],
+            'barriers': [],
+            'gate_links': [generators.gen_gate_link]
+        }
+
+        canvas.load_program(circ)
+        canvas.update()
+        self.assertEqual(len(canvas._output_dataset), 2)