Repository: Qiskit/qiskit

Issue:
plot_range in schedule.draw truncates visualization size when starting point is not 0
<!-- ⚠️ 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**: 0.12.0
- **Python version**: 3.7.6
- **Operating system**: Mac

### What is the current behavior?

plot_range behaves as expected for values that start at 0.

<img width="736" alt="Screen Shot 2020-02-26 at 3 14 45 PM" src="https://user-images.githubusercontent.com/54714046/75383664-c457e280-58aa-11ea-832f-33a5084d79f6.png">

However when we don't start at 0, the plot becomes unusably small when the numbers get further from 0. For example,  

<img width="976" alt="Screen Shot 2020-02-26 at 3 14 20 PM" src="https://user-images.githubusercontent.com/54714046/75383781-fd905280-58aa-11ea-85b3-4ec7fd9918de.png">


### Steps to reproduce the problem

Create a schedule that has pulse lasting up to 800 dt and adjust the plot range to plot from 800 to 850. (as shown in the photos above) 

### What is the expected behavior?
The plot size should not change. 


### Suggested solutions





Discussion:
@nathanearnestnoble
@nkanazawa1989 any ideas?
This is due to channel aliases. The text position should be updated properly. Please assign me if no one volunteered.
Assigned 😄 

Resolving patch:
diff --git a/qiskit/visualization/pulse/matplotlib.py b/qiskit/visualization/pulse/matplotlib.py
--- a/qiskit/visualization/pulse/matplotlib.py
+++ b/qiskit/visualization/pulse/matplotlib.py
@@ -444,18 +444,18 @@ def _draw_table(self, figure, channels, dt, n_valid_waveform):
 
         return ax
 
-    def _draw_snapshots(self, ax, snapshot_channels, dt, y0):
+    def _draw_snapshots(self, ax, snapshot_channels, y0):
         for events in snapshot_channels.values():
             snapshots = events.snapshots
             if snapshots:
                 for time in snapshots:
-                    ax.annotate(s=u"\u25D8", xy=(time*dt, y0), xytext=(time*dt, y0+0.08),
+                    ax.annotate(s=u"\u25D8", xy=(time, y0), xytext=(time, y0+0.08),
                                 arrowprops={'arrowstyle': 'wedge'}, ha='center')
 
-    def _draw_framechanges(self, ax, fcs, dt, y0):
+    def _draw_framechanges(self, ax, fcs, y0):
         framechanges_present = True
         for time in fcs.keys():
-            ax.text(x=time*dt, y=y0, s=r'$\circlearrowleft$',
+            ax.text(x=time, y=y0, s=r'$\circlearrowleft$',
                     fontsize=self.style.icon_font_size,
                     ha='center', va='center')
         return framechanges_present
@@ -481,7 +481,7 @@ def _prev_label_at_time(self, prev_labels, time):
                     return True
         return False
 
-    def _draw_labels(self, ax, labels, prev_labels, dt, y0):
+    def _draw_labels(self, ax, labels, prev_labels, y0):
         for t0, (tf, cmd) in labels.items():
             if isinstance(cmd, PersistentValue):
                 name = cmd.name if cmd.name else 'pv'
@@ -491,8 +491,8 @@ def _draw_labels(self, ax, labels, prev_labels, dt, y0):
                 name = cmd.name
 
             ax.annotate(r'%s' % name,
-                        xy=((t0+tf)//2*dt, y0),
-                        xytext=((t0+tf)//2*dt, y0-0.07),
+                        xy=((t0+tf)//2, y0),
+                        xytext=((t0+tf)//2, y0-0.07),
                         fontsize=self.style.label_font_size,
                         ha='center', va='center')
 
@@ -501,13 +501,13 @@ def _draw_labels(self, ax, labels, prev_labels, dt, y0):
             color = self.style.label_ch_color
 
             if not self._prev_label_at_time(prev_labels, t0):
-                ax.axvline(t0*dt, -1, 1, color=color,
+                ax.axvline(t0, -1, 1, color=color,
                            linestyle=linestyle, alpha=alpha)
             if not (self._prev_label_at_time(prev_labels, tf) or tf in labels):
-                ax.axvline(tf*dt, -1, 1, color=color,
+                ax.axvline(tf, -1, 1, color=color,
                            linestyle=linestyle, alpha=alpha)
 
-    def _draw_channels(self, ax, output_channels, interp_method, t0, tf, dt, scale_dict,
+    def _draw_channels(self, ax, output_channels, interp_method, t0, tf, scale_dict,
                        label=False, framechange=True):
         y0 = 0
         prev_labels = []
@@ -517,7 +517,7 @@ def _draw_channels(self, ax, output_channels, interp_method, t0, tf, dt, scale_d
                 scale = 0.5 * scale_dict.get(channel, 0.5)
                 # plot waveform
                 waveform = events.waveform
-                time = np.arange(t0, tf + 1, dtype=float) * dt
+                time = np.arange(t0, tf + 1, dtype=float)
                 if waveform.any():
                     time, re, im = interp_method(time, waveform, self.style.num_points)
                 else:
@@ -546,22 +546,22 @@ def _draw_channels(self, ax, output_channels, interp_method, t0, tf, dt, scale_d
                 # plot frame changes
                 fcs = events.framechanges
                 if fcs and framechange:
-                    self._draw_framechanges(ax, fcs, dt, y0)
+                    self._draw_framechanges(ax, fcs, y0)
                 # plot labels
                 labels = events.labels
                 if labels and label:
-                    self._draw_labels(ax, labels, prev_labels, dt, y0)
+                    self._draw_labels(ax, labels, prev_labels, y0)
                 prev_labels.append(labels)
 
             else:
                 continue
 
             # plot label
-            ax.text(x=0, y=y0, s=channel.name,
+            ax.text(x=t0, y=y0, s=channel.name,
                     fontsize=self.style.axis_font_size,
                     ha='right', va='center')
             # show scaling factor
-            ax.text(x=0, y=y0 - 0.1, s='x%.1f' % (2 * scale),
+            ax.text(x=t0, y=y0 - 0.1, s='x%.1f' % (2 * scale),
                     fontsize=0.7*self.style.axis_font_size,
                     ha='right', va='top')
 
@@ -622,8 +622,8 @@ def draw(self, schedule, dt, interp_method, plot_range,
 
         # setup plot range
         if plot_range:
-            t0 = int(np.floor(plot_range[0]/dt))
-            tf = int(np.floor(plot_range[1]/dt))
+            t0 = int(np.floor(plot_range[0]))
+            tf = int(np.floor(plot_range[1]))
         else:
             t0 = 0
             # when input schedule is empty or comprises only frame changes,
@@ -658,15 +658,19 @@ def draw(self, schedule, dt, interp_method, plot_range,
         ax.set_facecolor(self.style.bg_color)
 
         y0 = self._draw_channels(ax, output_channels, interp_method,
-                                 t0, tf, dt, scale_dict, label=label,
+                                 t0, tf, scale_dict, label=label,
                                  framechange=framechange)
 
         y_ub = 0.5 + self.style.vertical_span
         y_lb = y0 + 0.5 - self.style.vertical_span
 
-        self._draw_snapshots(ax, snapshot_channels, dt, y_lb)
+        self._draw_snapshots(ax, snapshot_channels, y_lb)
 
-        ax.set_xlim(t0 * dt, tf * dt)
+        ax.set_xlim(t0, tf)
+        tick_labels = np.linspace(t0, tf, 5)
+        ax.set_xticks(tick_labels)
+        ax.set_xticklabels([self.style.axis_formatter % label for label in tick_labels * dt],
+                           fontsize=self.style.axis_font_size)
         ax.set_ylim(y_lb, y_ub)
         ax.set_yticklabels([])
 
diff --git a/qiskit/visualization/pulse/qcstyle.py b/qiskit/visualization/pulse/qcstyle.py
--- a/qiskit/visualization/pulse/qcstyle.py
+++ b/qiskit/visualization/pulse/qcstyle.py
@@ -22,7 +22,7 @@ def __init__(self, figsize=(10, 12), fig_unit_h_table=0.4,
                  label_ch_color=None, label_ch_alpha=0.3, d_ch_color=None, u_ch_color=None,
                  m_ch_color=None, s_ch_color=None, s_ch_linestyle='-', table_color=None,
                  bg_color=None, num_points=1000, dpi=150, remove_spacing=True,
-                 max_table_ratio=0.5, vertical_span=0.2):
+                 max_table_ratio=0.5, vertical_span=0.2, axis_formatter=None):
         """Set style sheet for OpenPulse schedule drawer.
 
         Args:
@@ -50,6 +50,9 @@ def __init__(self, figsize=(10, 12), fig_unit_h_table=0.4,
             max_table_ratio (float): Maximum portion of the plot the table can take up. Limited to
                 range between 0 and 1.
             vertical_span (float): Spacing on top and bottom of plot.
+            axis_formatter (str): Format of horizontal axis of the plot. This is convenient when
+                you set ``dt`` option for the drawer. For example, formatter of ``%.3e`` gives you
+                horizontal axis values in the scientific notation with 3 digits.
         """
         self.figsize = figsize
         self.fig_unit_h_table = fig_unit_h_table
@@ -75,6 +78,7 @@ def __init__(self, figsize=(10, 12), fig_unit_h_table=0.4,
         self.remove_spacing = remove_spacing
         self.max_table_ratio = max(min(max_table_ratio, 0.0), 1.0)
         self.vertical_span = vertical_span
+        self.axis_formatter = axis_formatter or '%s'
 
 
 class PulseStyle:


Test patch:
diff --git a/test/python/visualization/references/instruction_matplotlib_ref.png b/test/python/visualization/references/instruction_matplotlib_ref.png
Binary files a/test/python/visualization/references/instruction_matplotlib_ref.png and b/test/python/visualization/references/instruction_matplotlib_ref.png differ
diff --git a/test/python/visualization/references/parametric_matplotlib_ref.png b/test/python/visualization/references/parametric_matplotlib_ref.png
Binary files a/test/python/visualization/references/parametric_matplotlib_ref.png and b/test/python/visualization/references/parametric_matplotlib_ref.png differ
diff --git a/test/python/visualization/references/pulse_matplotlib_ref.png b/test/python/visualization/references/pulse_matplotlib_ref.png
Binary files a/test/python/visualization/references/pulse_matplotlib_ref.png and b/test/python/visualization/references/pulse_matplotlib_ref.png differ
diff --git a/test/python/visualization/references/schedule_matplotlib_ref.png b/test/python/visualization/references/schedule_matplotlib_ref.png
Binary files a/test/python/visualization/references/schedule_matplotlib_ref.png and b/test/python/visualization/references/schedule_matplotlib_ref.png differ
diff --git a/test/python/visualization/references/schedule_show_framechange_ref.png b/test/python/visualization/references/schedule_show_framechange_ref.png
Binary files a/test/python/visualization/references/schedule_show_framechange_ref.png and b/test/python/visualization/references/schedule_show_framechange_ref.png differ
diff --git a/test/python/visualization/references/truncated_schedule_matplotlib_ref.png b/test/python/visualization/references/truncated_schedule_matplotlib_ref.png
new file mode 100644
Binary files /dev/null and b/test/python/visualization/references/truncated_schedule_matplotlib_ref.png differ
diff --git a/test/python/visualization/test_pulse_visualization_output.py b/test/python/visualization/test_pulse_visualization_output.py
--- a/test/python/visualization/test_pulse_visualization_output.py
+++ b/test/python/visualization/test_pulse_visualization_output.py
@@ -36,6 +36,7 @@ class TestPulseVisualizationImplementation(QiskitVisualizationTestCase):
     pulse_matplotlib_reference = path_to_diagram_reference('pulse_matplotlib_ref.png')
     instr_matplotlib_reference = path_to_diagram_reference('instruction_matplotlib_ref.png')
     schedule_matplotlib_reference = path_to_diagram_reference('schedule_matplotlib_ref.png')
+    trunc_sched_mpl_reference = path_to_diagram_reference('truncated_schedule_matplotlib_ref.png')
     schedule_show_framechange_ref = path_to_diagram_reference('schedule_show_framechange_ref.png')
     parametric_matplotlib_reference = path_to_diagram_reference('parametric_matplotlib_ref.png')
 
@@ -121,6 +122,17 @@ def test_schedule_matplotlib_drawer(self):
         self.assertImagesAreEqual(filename, self.schedule_matplotlib_reference)
         os.remove(filename)
 
+    # TODO: Enable for refactoring purposes and enable by default when we can
+    # decide if the backend is available or not.
+    @unittest.skipIf(not HAS_MATPLOTLIB, 'matplotlib not available.')
+    @unittest.skip('Useful for refactoring purposes, skipping by default.')
+    def test_truncated_schedule_matplotlib_drawer(self):
+        filename = self._get_resource_path('current_truncated_schedule_matplotlib_ref.png')
+        schedule = self.sample_schedule()
+        pulse_drawer(schedule, plot_range=(150, 300), filename=filename)
+        self.assertImagesAreEqual(filename, self.trunc_sched_mpl_reference)
+        os.remove(filename)
+
     # TODO: Enable for refactoring purposes and enable by default when we can
     # decide if the backend is available or not.
     @unittest.skipIf(not HAS_MATPLOTLIB, 'matplotlib not available.')