Repository: Qiskit/qiskit

Issue:
plot_gate_map and plot_error_map are missing qubit circles in savefig() ouptut with dpi set
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: master
- **Python version**: 3.8
- **Operating system**: Linux

### What is the current behavior?

When running `savefig()` on the output Figure object from `plot_gate_map()` and `plot_error_map()` when the `dpi` kwarg is set, the background circles used for qubit are missing form the output saved. For example:

![error_map](https://user-images.githubusercontent.com/2447371/72211959-21860900-34a2-11ea-90d7-8315cb2f6df1.png)


### Steps to reproduce the problem
```
from qiskit.visualization import plot_gate_map
from qiskit.test import mock
backend = mock.FakeYorktown()
plot_gate_map(backend).savefig('my_gate_map.png', dpi=450)
```
returns:

![my_gate_map](https://user-images.githubusercontent.com/2447371/72211961-28148080-34a2-11ea-94f3-9d0cd4bac11a.png)

### What is the expected behavior?

A scaled version of:
![my_gate_map_no_scaling](https://user-images.githubusercontent.com/2447371/72211970-41b5c800-34a2-11ea-960c-3b1a88f53b4b.png)


### Suggested solutions
Fix the bug when using `savefig()` and dpi kwarg. 

This might be an issue with matplotlib that will need to be fixed over there first.


Discussion:
I am seeing the same thing.
`priority: medium` because is a heavily used functionality in demos. Reassess if disagree. 
I have found a solution.

Resolving patch:
diff --git a/qiskit/tools/jupyter/backend_monitor.py b/qiskit/tools/jupyter/backend_monitor.py
--- a/qiskit/tools/jupyter/backend_monitor.py
+++ b/qiskit/tools/jupyter/backend_monitor.py
@@ -203,7 +203,10 @@ def config_tab(backend):
 
     if not config["simulator"]:
         with image_widget:
-            gate_map = plot_gate_map(backend)
+            qubit_size = 24
+            if config["n_qubits"] > 20:
+                qubit_size = 34
+            gate_map = plot_gate_map(backend, qubit_size=qubit_size)
             display(gate_map)
         plt.close(gate_map)
 
diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py
--- a/qiskit/visualization/gate_map.py
+++ b/qiskit/visualization/gate_map.py
@@ -19,45 +19,14 @@
 from .exceptions import VisualizationError
 
 
-class _GraphDist:
-    """Transform the circles properly for non-square axes."""
-
-    def __init__(self, size, ax, x=True):
-        self.size = size
-        self.ax = ax  # pylint: disable=invalid-name
-        self.x = x
-
-    @property
-    def dist_real(self):
-        """Compute distance."""
-        x0, y0 = self.ax.transAxes.transform((0, 0))
-        x1, y1 = self.ax.transAxes.transform((1, 1))
-        value = x1 - x0 if self.x else y1 - y0
-        return value
-
-    @property
-    def dist_abs(self):
-        """Distance abs"""
-        bounds = self.ax.get_xlim() if self.x else self.ax.get_ylim()
-        return bounds[0] - bounds[1]
-
-    @property
-    def value(self):
-        """Return value."""
-        return (self.size / self.dist_real) * self.dist_abs
-
-    def __mul__(self, obj):
-        return self.value * obj
-
-
 def plot_gate_map(
     backend,
     figsize=None,
     plot_directed=False,
     label_qubits=True,
-    qubit_size=24,
+    qubit_size=None,
     line_width=4,
-    font_size=12,
+    font_size=None,
     qubit_color=None,
     qubit_labels=None,
     line_color=None,
@@ -378,6 +347,15 @@ def plot_gate_map(
     num_qubits = config.n_qubits
     cmap = config.coupling_map
 
+    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
+
     if qubit_labels is None:
         qubit_labels = list(range(num_qubits))
     else:
@@ -475,10 +453,16 @@ def plot_gate_map(
     # Add circles for qubits
     for var, idx in enumerate(grid_data):
         _idx = [idx[1], -idx[0]]
-        width = _GraphDist(qubit_size, ax, True)
-        height = _GraphDist(qubit_size, ax, False)
-        ax.add_artist(mpatches.Ellipse(_idx, width, height, color=qubit_color[var], zorder=1))
-        if label_qubits:
+        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],
@@ -490,6 +474,7 @@ def plot_gate_map(
             )
     ax.set_xlim([-1, x_max + 1])
     ax.set_ylim([-(y_max + 1), 1])
+    ax.set_aspect("equal")
     if not input_axes:
         if get_backend() in ["module://ipykernel.pylab.backend_inline", "nbAgg"]:
             plt.close(fig)
@@ -722,11 +707,14 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True):
     if cmap:
         bright_ax = plt.subplot(grid_spec[-1, 7:])
 
+    qubit_size = 28
+    if num_qubits <= 5:
+        qubit_size = 20
     plot_gate_map(
         backend,
         qubit_color=q_colors,
         line_color=line_colors,
-        qubit_size=28,
+        qubit_size=qubit_size,
         line_width=5,
         plot_directed=directed,
         ax=main_ax,


Test patch:
diff --git a/test/python/visualization/references/14_plot_circuit_layout_result.png b/test/python/visualization/references/14_plot_circuit_layout_result.png
new file mode 100644
Binary files /dev/null and b/test/python/visualization/references/14_plot_circuit_layout_result.png differ
diff --git a/test/python/visualization/references/14bit_quantum_computer.png b/test/python/visualization/references/14bit_quantum_computer.png
Binary files a/test/python/visualization/references/14bit_quantum_computer.png and b/test/python/visualization/references/14bit_quantum_computer.png differ
diff --git a/test/python/visualization/references/16_plot_circuit_layout_result.png b/test/python/visualization/references/16_plot_circuit_layout_result.png
new file mode 100644
Binary files /dev/null and b/test/python/visualization/references/16_plot_circuit_layout_result.png differ
diff --git a/test/python/visualization/references/16bit_quantum_computer.png b/test/python/visualization/references/16bit_quantum_computer.png
Binary files a/test/python/visualization/references/16bit_quantum_computer.png and b/test/python/visualization/references/16bit_quantum_computer.png differ
diff --git a/test/python/visualization/references/20_plot_circuit_layout.png b/test/python/visualization/references/20_plot_circuit_layout.png
Binary files a/test/python/visualization/references/20_plot_circuit_layout.png and b/test/python/visualization/references/20_plot_circuit_layout.png differ
diff --git a/test/python/visualization/references/20_plot_circuit_layout_result.png b/test/python/visualization/references/20_plot_circuit_layout_result.png
new file mode 100644
Binary files /dev/null and b/test/python/visualization/references/20_plot_circuit_layout_result.png differ
diff --git a/test/python/visualization/references/20bit_quantum_computer.png b/test/python/visualization/references/20bit_quantum_computer.png
Binary files a/test/python/visualization/references/20bit_quantum_computer.png and b/test/python/visualization/references/20bit_quantum_computer.png differ
diff --git a/test/python/visualization/references/53_plot_circuit_layout_result.png b/test/python/visualization/references/53_plot_circuit_layout_result.png
new file mode 100644
Binary files /dev/null and b/test/python/visualization/references/53_plot_circuit_layout_result.png differ
diff --git a/test/python/visualization/references/53bit_quantum_computer.png b/test/python/visualization/references/53bit_quantum_computer.png
Binary files a/test/python/visualization/references/53bit_quantum_computer.png and b/test/python/visualization/references/53bit_quantum_computer.png differ
diff --git a/test/python/visualization/test_gate_map.py b/test/python/visualization/test_gate_map.py
--- a/test/python/visualization/test_gate_map.py
+++ b/test/python/visualization/test_gate_map.py
@@ -17,8 +17,7 @@
 from PIL import Image
 from ddt import ddt, data
 from qiskit.test.mock import FakeProvider
-from qiskit.test import QiskitTestCase
-from qiskit.visualization.gate_map import _GraphDist, plot_gate_map, plot_circuit_layout
+from qiskit.visualization.gate_map import plot_gate_map, plot_circuit_layout
 from qiskit.tools.visualization import HAS_MATPLOTLIB
 from qiskit import QuantumRegister, QuantumCircuit
 from qiskit.transpiler import Layout
@@ -72,79 +71,5 @@ def test_plot_circuit_layout(self, backend):
         plt.close(fig)
 
 
-@unittest.skipIf(not HAS_MATPLOTLIB, "matplotlib not available.")
-class TestGraphDist(QiskitTestCase):
-    """tests _GraphdDist functions"""
-
-    def setUp(self):
-        """setup plots for _GraphDist"""
-        super().setUp()
-        ax1 = plt.subplots(figsize=(5, 5))[1]
-        ax2 = plt.subplots(figsize=(9, 3))[1]
-        ax1.axis("off")
-        ax2.axis("off")
-        self.ax1 = ax1
-        self.ax2 = ax2
-        self.ax1_x0, self.ax1_y0 = ax1.transAxes.transform((0, 0))
-        self.ax1_x1, self.ax1_y1 = ax1.transAxes.transform((1, 1))
-        self.ax2_x0, self.ax2_y0 = ax2.transAxes.transform((0, 0))
-        self.ax2_x1, self.ax2_y1 = ax2.transAxes.transform((1, 1))
-        self.ax1_bounds_x, self.ax1_bounds_y = ax1.get_xlim(), ax1.get_ylim()
-        self.ax2_bounds_x, self.ax2_bounds_y = ax2.get_xlim(), ax2.get_ylim()
-        self.size = 4
-        self.real_values = [
-            self.ax1_x1 - self.ax1_x0,
-            self.ax1_y1 - self.ax1_y0,
-            self.ax2_x1 - self.ax2_x0,
-            self.ax2_y1 - self.ax2_y0,
-        ]
-        self.abs_values = [
-            self.ax1_bounds_x[0] - self.ax1_bounds_x[1],
-            self.ax1_bounds_y[0] - self.ax1_bounds_y[1],
-            self.ax2_bounds_x[0] - self.ax2_bounds_x[1],
-            self.ax2_bounds_y[0] - self.ax2_bounds_y[1],
-        ]
-        self.val = []
-        for i in range(4):
-            val = (self.size / self.real_values[i]) * self.abs_values[i]
-            self.val.append(val)
-
-    def test_dist_real(self):
-        """tests dist_real calculation for different figsize"""
-        params = [
-            (self.ax1, self.real_values[0], True),
-            (self.ax1, self.real_values[1], False),
-            (self.ax2, self.real_values[2], True),
-            (self.ax2, self.real_values[3], False),
-        ]
-        for test_val, expected_val, bool_op in params:
-            with self.subTest():
-                self.assertEqual(expected_val, _GraphDist(self.size, test_val, bool_op).dist_real)
-
-    def test_dist_abs(self):
-        """tests dist_abs calculation for different figsize"""
-        params = [
-            (self.ax1, self.abs_values[0], True),
-            (self.ax1, self.abs_values[1], False),
-            (self.ax2, self.abs_values[2], True),
-            (self.ax2, self.abs_values[3], False),
-        ]
-        for test_val, expected_val, bool_op in params:
-            with self.subTest():
-                self.assertEqual(expected_val, _GraphDist(self.size, test_val, bool_op).dist_abs)
-
-    def test_value(self):
-        """tests value calculation for size = 4"""
-        params = [
-            (self.ax1, self.val[0], True),
-            (self.ax1, self.val[1], False),
-            (self.ax2, self.val[2], True),
-            (self.ax2, self.val[3], False),
-        ]
-        for test_val, expected_val, bool_op in params:
-            with self.subTest():
-                self.assertEqual(expected_val, _GraphDist(self.size, test_val, bool_op).value)
-
-
 if __name__ == "__main__":
     unittest.main(verbosity=2)