Repository: Qiskit/qiskit (Python)

Issue:
Update plot_gate_map (and related visualization functions) to leverage rustworkx.visualization.graphviz_draw for unknown coupling maps
### What should we add?

Right now the `plot_gate_map()` visualization function (and similar functions like `plot_circuit_layout()` use rustworkx's `spring_layout()` function when there a backend is passed in and there isn't a hardcoded layout available and passes that to a mpl visualization. For moderate numbers of qubits this works reasonably well, but for large numbers of qubits the layout doesn't scale positions appropriately and the output is typically cluttered and not a useful visualization. Graphviz is particularly well suited for doing this type of graph visualization (as it is a specialized tool for doing graph visualization). So instead of trying to rebuild what we get from graphviz we should just call out to graphviz if it's available and leverage the rustwork function for graph visualization using graphivz to generate the visualization. The only potential issue is potentially `plot_error_map` which has histograms and color bars built using matplotlib and integrating the two different visualization tools might prove difficult.

Here is some example code I used to leverage `graphviz_draw()` to build a view like `plot_circuit_layout()`:

```python
from rustworkx.visualization import graphviz_draw

graph = target.build_coupling_map().graph
for node in graph.node_indices():
    graph[node] = node
    
for edge, triple in graph.edge_index_map().items():
    graph.update_edge_by_index(edge, (triple[0], triple[1]))

physical_bits = layout.get_physical_bits()
qubit_indices = {bit: index for index, bit in enumerate(linear_circuit.qubits)}
    
def color_node(node):
    if node in physical_bits:
        out_dict = {
            "label": str(qubit_indices[physical_bits[node]]),
            "color": "red",
            "fillcolor": "red",
            "style": "filled",
            "shape": "circle",
        }
    else:
        out_dict = {
            "label": "",
            "color": "blue",
            "fillcolor": "blue",
            "style": "filled",
            "shape": "circle",
        }
    return out_dict

def color_edge(edge):
    if all(x in physical_bits for x in edge):
        out_dict = {
            "color": "red",
            "fillcolor": "red",
        }
    else:
        out_dict = {
            "color": "blue",
            "fillcolor": "blue",
        }
    return out_dict
        
graphviz_draw(graph, method="neato", node_attr_fn=color_node, edge_attr_fn=color_edge)
```
which in this case output something like
![plot_circuit_layout_graphviz](https://user-images.githubusercontent.com/2447371/198735559-dec3a62b-c3b0-481d-88d7-31e7bfe0533b.png)



Discussion: For reference, the current `plot_gate_map` and `plot_error_map` problems with larger backends:

```python
from qiskit_ibm_provider import IBMProvider
from qiskit.visualization import plot_gate_map

backend = IBMProvider().get_backend('ibm_washington')
print(backend.num_qubits)  # 127
plot_gate_map(backend)
```
![image](https://user-images.githubusercontent.com/766693/236425424-ef5d0b14-68f2-43a0-b8cb-62bbd5614156.png)


```python
from qiskit.visualization import plot_error_map
plot_error_map(backend)
```

![image](https://user-images.githubusercontent.com/766693/236434754-4c3b4d1b-7e91-4179-9fcc-fcdeaeb176cc.png)

Hey! Is this still available for UnitaryHack @1ucian0? I'd love to take a shot 💪 
@husayngokal can collaborate with you, if you'd like?
Sure! PM me on LinkedIn and we can work on this together:
https://www.linkedin.com/in/husayn-gokal/
@1ucian0 @mtreinish I have a few questions:

- Should we remove matplotlib from the functions entirely or provide the user with the choice b/w graphviz and matplotlib?
- A comment in the source code asks to replace the `spring_layout()` function with `planar_layout()`. The graph is getting rendered with `planar_layout()`. However, it's still not available in the documentation and the output graphs have overlapping edges (as per my understanding, the `planar_layout()` is meant to avoid just this). Should I stick with `spring_layout()`?
- Does this issue require us to modify only the functions in the `qiskit/visualization/gate_map.py` file:
   - plot_gate_map
   - plot_coupling_map
   - plot_circuit_layout
   - plot_error_map ?
- Is there a way to add custom colors to rusworkx graphs? In the existing code, the default node and edge color is '#648fff'; I am unable to use it in `rx` graphs.
   - This is especially important for error maps. If nothing is available, I am thinking of using webcolors to get the names of the colors in the colormap.  



Fix (patch):
diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py
index 86a3fbe9f858..6acd2f8b0ab7 100644
--- a/qiskit/visualization/gate_map.py
+++ b/qiskit/visualization/gate_map.py
@@ -17,12 +17,13 @@
 
 import numpy as np
 import rustworkx as rx
+from rustworkx.visualization import graphviz_draw
 
 from qiskit.exceptions import QiskitError
 from qiskit.utils import optionals as _optionals
 from qiskit.providers.exceptions import BackendPropertyError
+from qiskit.transpiler.coupling import CouplingMap
 from .exceptions import VisualizationError
-from .utils import matplotlib_close_if_inline
 
 
 def _get_backend_interface_version(backend):
@@ -42,7 +43,7 @@ def plot_gate_map(
     qubit_color=None,
     qubit_labels=None,
     line_color=None,
-    font_color="w",
+    font_color="white",
     ax=None,
     filename=None,
     qubit_coordinates=None,
@@ -93,8 +94,6 @@ def plot_gate_map(
     """
     qubit_coordinates_map = {}
 
-    qubit_coordinates_map[1] = [[0, 0]]
-
     qubit_coordinates_map[5] = [[1, 0], [0, 1], [1, 1], [1, 2], [2, 1]]
 
     qubit_coordinates_map[7] = [[0, 0], [0, 1], [0, 2], [1, 1], [2, 0], [2, 1], [2, 2]]
@@ -912,8 +911,6 @@ def plot_gate_map(
 
     backend_version = _get_backend_interface_version(backend)
     if backend_version <= 1:
-        from qiskit.transpiler.coupling import CouplingMap
-
         if backend.configuration().simulator:
             raise QiskitError("Requires a device backend, not simulator.")
         config = backend.configuration()
@@ -927,31 +924,12 @@ def plot_gate_map(
     if qubit_coordinates is None and ("ibm" in name or "fake" in name):
         qubit_coordinates = qubit_coordinates_map.get(num_qubits, None)
 
-    if qubit_coordinates is None:
-        # Replace with planar_layout() when rustworkx offers it
-        qubit_coordinates_rx = rx.spring_layout(coupling_map.graph, seed=1234)
-        scaling_factor = 10 ** int(math.log10(num_qubits) + 1)
-        qubit_coordinates = [
-            (
-                int(scaling_factor * qubit_coordinates_rx[i][0]),
-                int(scaling_factor * qubit_coordinates_rx[i][1]),
+    if qubit_coordinates:
+        if len(qubit_coordinates) != num_qubits:
+            raise QiskitError(
+                f"The number of specified qubit coordinates {len(qubit_coordinates)} "
+                f"does not match the device number of qubits: {num_qubits}"
             )
-            for i in range(num_qubits)
-        ]
-
-    if any(x[0] < 0 or x[1] < 0 for x in qubit_coordinates):
-        min_entry = min(qubit_coordinates, key=lambda x: min(x[0], x[1]))
-        negative_offset = 0 - min(min_entry)
-        qubit_coordinates = [
-            (x[0] + negative_offset, x[1] + negative_offset) for x in qubit_coordinates
-        ]
-
-    if len(qubit_coordinates) != num_qubits:
-        raise QiskitError(
-            f"The number of specified qubit coordinates {len(qubit_coordinates)} "
-            f"does not match the device number of qubits: {num_qubits}"
-        )
-
     return plot_coupling_map(
         num_qubits,
         qubit_coordinates,
@@ -972,6 +950,7 @@ def plot_gate_map(
 
 
 @_optionals.HAS_MATPLOTLIB.require_in_call
+@_optionals.HAS_GRAPHVIZ.require_in_call
 def plot_coupling_map(
     num_qubits: int,
     qubit_coordinates: List[List[int]],
@@ -985,7 +964,7 @@ def plot_coupling_map(
     qubit_color=None,
     qubit_labels=None,
     line_color=None,
-    font_color="w",
+    font_color="white",
     ax=None,
     filename=None,
 ):
@@ -1014,7 +993,7 @@ def plot_coupling_map(
         Figure: A Matplotlib figure instance.
 
     Raises:
-        MissingOptionalLibraryError: if matplotlib not installed.
+        MissingOptionalLibraryError: If matplotlib or graphviz is not installed.
         QiskitError: If length of qubit labels does not match number of qubits.
 
     Example:
@@ -1030,20 +1009,14 @@ def plot_coupling_map(
             plot_coupling_map(num_qubits, qubit_coordinates, coupling_map)
     """
     import matplotlib.pyplot as plt
-    import matplotlib.patches as mpatches
+    from .utils import matplotlib_close_if_inline
 
     input_axes = False
     if ax:
         input_axes = True
 
-    if font_size is None:
-        font_size = 12
-
     if qubit_size is None:
-        qubit_size = 24
-    if num_qubits > 20:
-        qubit_size = 28
-        font_size = 10
+        qubit_size = 30
 
     if qubit_labels is None:
         qubit_labels = list(range(num_qubits))
@@ -1051,128 +1024,96 @@ def plot_coupling_map(
         if len(qubit_labels) != num_qubits:
             raise QiskitError("Length of qubit labels does not equal number of qubits.")
 
-    if qubit_coordinates is not None:
-        grid_data = qubit_coordinates
-    else:
-        if not input_axes:
-            fig, ax = plt.subplots(figsize=(5, 5))
-            ax.axis("off")
-            if filename:
-                fig.savefig(filename)
-            return fig
-
-    x_max = max(d[1] for d in grid_data)
-    y_max = max(d[0] for d in grid_data)
-    max_dim = max(x_max, y_max)
-
-    if figsize is None:
-        if num_qubits == 1 or (x_max / max_dim > 0.33 and y_max / max_dim > 0.33):
-            figsize = (5, 5)
-        else:
-            figsize = (9, 3)
-
-    if ax is None:
-        fig, ax = plt.subplots(figsize=figsize)
-        ax.axis("off")
+    if not label_qubits:
+        qubit_labels = [""] * num_qubits
 
     # set coloring
     if qubit_color is None:
         qubit_color = ["#648fff"] * num_qubits
     if line_color is None:
-        line_color = ["#648fff"] * len(coupling_map) if coupling_map else []
-
-    # Add lines for couplings
-    if num_qubits != 1:
-        for ind, edge in enumerate(coupling_map):
-            is_symmetric = False
-            if edge[::-1] in coupling_map:
-                is_symmetric = True
-            y_start = grid_data[edge[0]][0]
-            x_start = grid_data[edge[0]][1]
-            y_end = grid_data[edge[1]][0]
-            x_end = grid_data[edge[1]][1]
-
-            if is_symmetric:
-                if y_start == y_end:
-                    x_end = (x_end - x_start) / 2 + x_start
-
-                elif x_start == x_end:
-                    y_end = (y_end - y_start) / 2 + y_start
+        line_color = ["#648fff"] * len(coupling_map)
 
-                else:
-                    x_end = (x_end - x_start) / 2 + x_start
-                    y_end = (y_end - y_start) / 2 + y_start
-            ax.add_artist(
-                plt.Line2D(
-                    [x_start, x_end],
-                    [-y_start, -y_end],
-                    color=line_color[ind],
-                    linewidth=line_width,
-                    zorder=0,
-                )
-            )
-            if plot_directed:
-                dx = x_end - x_start
-                dy = y_end - y_start
-                if is_symmetric:
-                    x_arrow = x_start + dx * 0.95
-                    y_arrow = -y_start - dy * 0.95
-                    dx_arrow = dx * 0.01
-                    dy_arrow = -dy * 0.01
-                    head_width = 0.15
-                else:
-                    x_arrow = x_start + dx * 0.5
-                    y_arrow = -y_start - dy * 0.5
-                    dx_arrow = dx * 0.2
-                    dy_arrow = -dy * 0.2
-                    head_width = 0.2
-                ax.add_patch(
-                    mpatches.FancyArrow(
-                        x_arrow,
-                        y_arrow,
-                        dx_arrow,
-                        dy_arrow,
-                        head_width=head_width,
-                        length_includes_head=True,
-                        edgecolor=None,
-                        linewidth=0,
-                        facecolor=line_color[ind],
-                        zorder=1,
-                    )
-                )
-
-    # Add circles for qubits
-    for var, idx in enumerate(grid_data):
-        _idx = [idx[1], -idx[0]]
-        ax.add_artist(
-            mpatches.Ellipse(
-                _idx,
-                qubit_size / 48,
-                qubit_size / 48,  # This is here so that the changes
-                color=qubit_color[var],
-                zorder=1,
-            )
-        )  # to how qubits are plotted does
-        if label_qubits:  # not affect qubit size kwarg.
-            ax.text(
-                *_idx,
-                s=qubit_labels[var],
-                horizontalalignment="center",
-                verticalalignment="center",
-                color=font_color,
-                size=font_size,
-                weight="bold",
-            )
-    ax.set_xlim([-1, x_max + 1])
-    ax.set_ylim([-(y_max + 1), 1])
-    ax.set_aspect("equal")
+    if num_qubits == 1:
+        graph = rx.PyDiGraph()
+        graph.add_node(0)
+    else:
+        graph = CouplingMap(coupling_map).graph
+
+    if not plot_directed:
+        graph = graph.to_undirected(multigraph=False)
+
+    for node in graph.node_indices():
+        graph[node] = node
+
+    for edge_index in graph.edge_indices():
+        graph.update_edge_by_index(edge_index, edge_index)
+
+    # pixel-to-inch conversion
+    px = 1.15 / plt.rcParams["figure.dpi"]
+
+    if qubit_coordinates:
+        qubit_coordinates = [coordinates[::-1] for coordinates in qubit_coordinates]
+
+    if font_size is None:
+        max_characters = max(1, max(len(str(x)) for x in qubit_labels))
+        font_size = max(int(20 / max_characters), 1)
+
+    def color_node(node):
+        if qubit_coordinates:
+            out_dict = {
+                "label": str(qubit_labels[node]),
+                "color": f'"{qubit_color[node]}"',
+                "fillcolor": f'"{qubit_color[node]}"',
+                "style": "filled",
+                "shape": "circle",
+                "pos": f'"{qubit_coordinates[node][0]},{qubit_coordinates[node][1]}"',
+                "pin": "True",
+            }
+        else:
+            out_dict = {
+                "label": str(qubit_labels[node]),
+                "color": f'"{qubit_color[node]}"',
+                "fillcolor": f'"{qubit_color[node]}"',
+                "style": "filled",
+                "shape": "circle",
+            }
+        out_dict["fontcolor"] = f'"{font_color}"'
+        out_dict["fontsize"] = str(font_size)
+        out_dict["height"] = str(qubit_size * px)
+        out_dict["fixedsize"] = "True"
+        out_dict["fontname"] = '"DejaVu Sans"'
+        return out_dict
+
+    def color_edge(edge):
+        out_dict = {
+            "color": f'"{line_color[edge]}"',
+            "fillcolor": f'"{line_color[edge]}"',
+            "penwidth": str(line_width),
+        }
+        return out_dict
+
+    plot = graphviz_draw(
+        graph,
+        method="neato",
+        node_attr_fn=color_node,
+        edge_attr_fn=color_edge,
+        filename=filename,
+    )
+
+    if filename:
+        return None
+
+    if not input_axes:
+        if figsize is None:
+            width, height = plot.size
+            figsize = (width * px, height * px)
+        fig, ax = plt.subplots(figsize=figsize)
+    ax.axis("off")
+    ax.imshow(plot)
 
     if not input_axes:
         matplotlib_close_if_inline(fig)
-        if filename:
-            fig.savefig(filename)
         return fig
-    return None
 
 
 def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None):
@@ -1232,7 +1173,7 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None
         cmap_len = cmap.graph.num_edges()
 
     qubits = []
-    qubit_labels = [None] * num_qubits
+    qubit_labels = [""] * num_qubits
 
     bit_locations = {
         bit: {"register": register, "index": index}
@@ -1248,27 +1189,27 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None
             bit_register = bit_locations[key]["register"]
             if bit_register is None or bit_register.name != "ancilla":
                 qubits.append(val)
-                qubit_labels[val] = bit_locations[key]["index"]
+                qubit_labels[val] = str(bit_locations[key]["index"])
 
     elif view == "physical":
         for key, val in circuit._layout.initial_layout.get_physical_bits().items():
             bit_register = bit_locations[val]["register"]
             if bit_register is None or bit_register.name != "ancilla":
                 qubits.append(key)
-                qubit_labels[key] = key
+                qubit_labels[key] = str(key)
 
     else:
         raise VisualizationError("Layout view must be 'virtual' or 'physical'.")
 
     qcolors = ["#648fff"] * num_qubits
     for k in qubits:
-        qcolors[k] = "k"
+        qcolors[k] = "black"
 
     lcolors = ["#648fff"] * cmap_len
 
     for idx, edge in enumerate(cmap):
         if edge[0] in qubits and edge[1] in qubits:
-            lcolors[idx] = "k"
+            lcolors[idx] = "black"
 
     fig = plot_gate_map(
         backend,
@@ -1282,7 +1223,7 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None
 
 @_optionals.HAS_MATPLOTLIB.require_in_call
 @_optionals.HAS_SEABORN.require_in_call
-def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates=None):
+def plot_error_map(backend, figsize=(15, 12), show_title=True, qubit_coordinates=None):
     """Plots the error map of a given backend.
 
     Args:
@@ -1300,7 +1241,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates=
 
     Raises:
         VisualizationError: The backend does not provide gate errors for the 'sx' gate.
-        MissingOptionalLibraryError: If seaborn is not installed
+        MissingOptionalLibraryError: If matplotlib or seaborn is not installed.
 
     Example:
         .. plot::
@@ -1313,10 +1254,11 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates=
             backend = FakeVigoV2()
             plot_error_map(backend)
     """
-    import seaborn as sns
     import matplotlib
     import matplotlib.pyplot as plt
     from matplotlib import gridspec, ticker
+    import seaborn as sns
+    from .utils import matplotlib_close_if_inline
 
     color_map = sns.cubehelix_palette(reverse=True, as_cmap=True)
 
@@ -1406,7 +1348,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates=
     single_norm = matplotlib.colors.Normalize(
         vmin=min(single_gate_errors), vmax=max(single_gate_errors)
     )
-    q_colors = [color_map(single_norm(err)) for err in single_gate_errors]
+    q_colors = [matplotlib.colors.to_hex(color_map(single_norm(err))) for err in single_gate_errors]
 
     directed = False
     line_colors = []
@@ -1417,7 +1359,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates=
         avg_cx_err = np.mean(cx_errors)
 
         cx_norm = matplotlib.colors.Normalize(vmin=min(cx_errors), vmax=max(cx_errors))
-        line_colors = [color_map(cx_norm(err)) for err in cx_errors]
+        line_colors = [matplotlib.colors.to_hex(color_map(cx_norm(err))) for err in cx_errors]
 
     read_err = 100 * np.asarray(read_err)
     avg_read_err = np.mean(read_err)
@@ -1450,6 +1392,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates=
         ax=main_ax,
         qubit_coordinates=qubit_coordinates,
     )
+
     main_ax.axis("off")
     main_ax.set_aspect(1)
     if cmap:
diff --git a/releasenotes/notes/update-gate-map-visualizations-6ea907a0502fdc1a.yaml b/releasenotes/notes/update-gate-map-visualizations-6ea907a0502fdc1a.yaml
new file mode 100644
index 000000000000..4ef11ae430d7
--- /dev/null
+++ b/releasenotes/notes/update-gate-map-visualizations-6ea907a0502fdc1a.yaml
@@ -0,0 +1,19 @@
+---
+features:
+  - |
+    The visualizations from the :func:`~.plot_gate_map`,  :func:`~.plot_coupling_map`.
+    :func:`~.plot_error_map`,  and :func:`~.plot_circuit_layout` functions have been significantly
+    improved for rendering layouts of backends with large numbers of qubits. This was accomplished
+    by leveraging `graphviz <https://graphviz.org/>`__ through rustworkx's ``graphviz_draw()`` function
+    to perform a more sophisticated algorithmic graph layout that scales for large numbers of
+    qubits.
+
+upgrade:
+  - |
+    The visualization functions: :func:`~.plot_gate_map`,  :func:`~.plot_coupling_map`.
+    :func:`~.plot_error_map`,  and :func:`~.plot_circuit_layout` now depend on
+    `graphviz <https://graphviz.org/>`__ being installed to function. This change was
+    necessary to enable visualizing backends with larger numbers of qubits. This
+    additional external requirement is in addition to the existing optional dependencies
+    these functions previously required. You find details on how to install
+    graphviz here:  https://graphviz.org/download/