Skip to content

Commit

Permalink
Refactor BaseDrawer to be more generic and less specific to CurveAnal…
Browse files Browse the repository at this point in the history
…ysis

The following changes were also made:

Add isort ignore comment for `qiskit_experiments.visualization` to prevent circular import.
Refactor treport/text-box style parameters to be more generic.
  • Loading branch information
conradhaupt committed Sep 26, 2022
1 parent 7101687 commit 94158f4
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def _default_options(cls):
style=PlotStyle(
figsize=(8, 10),
legend_loc="lower right",
report_rpos=(0.28, -0.10),
text_box_rel_pos=(0.28, -0.10),
),
)
default_options.plotter.set_plot_options(
Expand Down
4 changes: 3 additions & 1 deletion qiskit_experiments/visualization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
PlotStyle
"""

# PlotStyle is imported by .drawers and .plotters. Skip PlotStyle import for isort to prevent circular
# import.
from .style import PlotStyle # isort:skip
from .drawers import BaseDrawer, MplDrawer
from .plotters import BasePlotter, CurvePlotter
from .style import PlotStyle
67 changes: 45 additions & 22 deletions qiskit_experiments/visualization/drawers/base_drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"""Drawer abstract class."""

from abc import ABC, abstractmethod
from typing import Dict, Sequence, Optional
from typing import Dict, Optional, Sequence, Tuple

from qiskit_experiments.framework import Options
from qiskit_experiments.visualization import PlotStyle
Expand Down Expand Up @@ -215,87 +215,110 @@ def format_canvas(self):
"""Final cleanup for the canvas appearance."""

@abstractmethod
def draw_raw_data(
def draw_scatter(
self,
x_data: Sequence[float],
y_data: Sequence[float],
x_err: Optional[Sequence[float]] = None,
y_err: Optional[Sequence[float]] = None,
name: Optional[str] = None,
legend_entry: bool = False,
legend_label: Optional[str] = None,
**options,
):
"""Draw raw data.
"""Draw scatter points, with optional error-bars.
Args:
x_data: X values.
y_data: Y values.
x_err: Optional error for X values.
y_err: Optional error for Y values.
name: Name of this series.
legend_entry: Whether the drawn area must have a legend entry. Defaults to False.
legend_label: Optional legend label. ``name`` will be used if ``legend_label` is None.
options: Valid options for the drawer backend API.
"""

@abstractmethod
def draw_formatted_data(
def draw_line(
self,
x_data: Sequence[float],
y_data: Sequence[float],
y_err_data: Sequence[float],
name: Optional[str] = None,
legend_entry: bool = False,
legend_label: Optional[str] = None,
**options,
):
"""Draw the formatted data that is used for fitting.
"""Draw fit line.
Args:
x_data: X values.
y_data: Y values.
y_err_data: Standard deviation of Y values.
y_data: Fit Y values.
name: Name of this series.
legend_entry: Whether the drawn area must have a legend entry. Defaults to False.
legend_label: Optional legend label. ``name`` will be used if ``legend_label` is None.
options: Valid options for the drawer backend API.
"""

@abstractmethod
def draw_line(
def draw_filled_y_area(
self,
x_data: Sequence[float],
y_data: Sequence[float],
y_ub: Sequence[float],
y_lb: Sequence[float],
name: Optional[str] = None,
legend_entry: bool = False,
legend_label: Optional[str] = None,
**options,
):
"""Draw fit line.
"""Draw filled area as a function of x-values.
Args:
x_data: X values.
y_data: Fit Y values.
y_ub: The upper boundary of Y values.
y_lb: The lower boundary of Y values.
name: Name of this series.
legend_entry: Whether the drawn area must have a legend entry. Defaults to False.
legend_label: Optional legend label. ``name`` will be used if ``legend_label` is None.
options: Valid options for the drawer backend API.
"""

@abstractmethod
def draw_confidence_interval(
def draw_filled_x_area(
self,
x_data: Sequence[float],
y_ub: Sequence[float],
y_lb: Sequence[float],
x_ub: Sequence[float],
x_lb: Sequence[float],
y_data: Sequence[float],
name: Optional[str] = None,
legend_entry: bool = False,
legend_label: Optional[str] = None,
**options,
):
"""Draw confidence interval.
"""Draw filled area as a function of y-values.
Args:
x_data: X values.
y_ub: The upper boundary of Y values.
y_lb: The lower boundary of Y values.
x_ub: The upper boundary of X values.
x_lb: The lower boundary of X values.
y_data: Y values.
name: Name of this series.
legend_entry: Whether the drawn area must have a legend entry. Defaults to False.
legend_label: Optional legend label. ``name`` will be used if ``legend_label` is None.
options: Valid options for the drawer backend API.
"""

@abstractmethod
def draw_report(
def draw_text_box(
self,
description: str,
rel_pos: Optional[Tuple[float, float]] = None,
**options,
):
"""Draw text box that shows reports, such as fit results.
"""Draw text box.
Args:
description: A string to be drawn inside a report box.
rel_pos: Relative position of the text-box. If None, the default ``text_box_rel_pos`` from
the style is used.
options: Valid options for the drawer backend API.
"""

Expand Down
124 changes: 88 additions & 36 deletions qiskit_experiments/visualization/drawers/mpl_drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

"""Curve drawer for matplotlib backend."""

from typing import Optional, Sequence, Tuple
from typing import Dict, Optional, Sequence, Tuple

import numpy as np
from matplotlib.axes import Axes
Expand Down Expand Up @@ -272,98 +272,147 @@ def _get_default_marker(self, name: str) -> str:
ind = self._series.index(name) % len(self.DefaultMarkers)
return self.DefaultMarkers[ind]

def draw_raw_data(
def _update_label_in_dict(
self,
options: Dict[str, any],
name: Optional[str],
legend_entry: bool,
legend_label: Optional[str],
):
"""Helper function to set the label entry in ``options`` based on given arguments.
Args:
options: The options dictionary being modified.
name: A fall-back label if ``legend_label`` is None. If None, a blank string is used.
legend_entry: Whether to set "label" in ``options``.
legend_label: Optional label. If None, ``name`` is used.
"""
if legend_entry:
if legend_label is not None:
label = legend_label
elif name is not None:
label = name
else:
label = ""
options["label"] = label

def draw_scatter(
self,
x_data: Sequence[float],
y_data: Sequence[float],
x_err: Optional[Sequence[float]] = None,
y_err: Optional[Sequence[float]] = None,
name: Optional[str] = None,
legend_entry: bool = False,
legend_label: Optional[str] = None,
**options,
):

series_params = self.plot_options.series_params.get(name, {})
marker = series_params.get("symbol", self._get_default_marker(name))
color = series_params.get("color", self._get_default_color(name))
axis = series_params.get("canvas", None)

draw_options = {
"color": "grey",
"color": color,
"marker": marker,
"alpha": 0.8,
"zorder": 2,
}
self._update_label_in_dict(draw_options, name, legend_entry, legend_label)
draw_options.update(**options)
self._get_axis(axis).scatter(x_data, y_data, **draw_options)

def draw_formatted_data(
if x_err is None and y_err is None:
self._get_axis(axis).scatter(x_data, y_data, **draw_options)
else:
# Check for invalid error values.
if y_err is not None and not np.all(np.isfinite(y_err)):
y_err = None
if x_err is not None and not np.all(np.isfinite(x_err)):
x_err = None

# `errorbar` has extra default draw_options to set, but we want to accept any overrides from
# `options`, and thus draw_options.
errorbar_options = {
"linestyle": "",
"markersize": 9,
}
errorbar_options.update(draw_options)

self._get_axis(axis).errorbar(
x_data, y_data, yerr=y_err, xerr=x_err, **errorbar_options
)

def draw_line(
self,
x_data: Sequence[float],
y_data: Sequence[float],
y_err_data: Sequence[float],
name: Optional[str] = None,
legend_entry: bool = False,
legend_label: Optional[str] = None,
**options,
):
series_params = self.plot_options.series_params.get(name, {})
axis = series_params.get("canvas", None)
color = series_params.get("color", self._get_default_color(name))
marker = series_params.get("symbol", self._get_default_marker(name))

draw_ops = {
"color": color,
"marker": marker,
"markersize": 9,
"alpha": 0.8,
"zorder": 4,
"linestyle": "",
"linestyle": "-",
"linewidth": 2,
}
self._update_label_in_dict(draw_ops, name, legend_entry, legend_label)
draw_ops.update(**options)
if name:
draw_ops["label"] = name

if not np.all(np.isfinite(y_err_data)):
y_err_data = None
self._get_axis(axis).errorbar(x_data, y_data, yerr=y_err_data, **draw_ops)
self._get_axis(axis).plot(x_data, y_data, **draw_ops)

def draw_line(
def draw_filled_y_area(
self,
x_data: Sequence[float],
y_data: Sequence[float],
y_ub: Sequence[float],
y_lb: Sequence[float],
name: Optional[str] = None,
legend_entry: bool = False,
legend_label: Optional[str] = None,
**options,
):
series_params = self.plot_options.series_params.get(name, {})
axis = series_params.get("canvas", None)
color = series_params.get("color", self._get_default_color(name))

draw_ops = {
"alpha": 0.1,
"color": color,
"zorder": 5,
"linestyle": "-",
"linewidth": 2,
}
self._update_label_in_dict(draw_ops, name, legend_entry, legend_label)
draw_ops.update(**options)
self._get_axis(axis).plot(x_data, y_data, **draw_ops)
self._get_axis(axis).fill_between(x_data, y1=y_lb, y2=y_ub, **draw_ops)

def draw_confidence_interval(
def draw_filled_x_area(
self,
x_data: Sequence[float],
y_ub: Sequence[float],
y_lb: Sequence[float],
x_ub: Sequence[float],
x_lb: Sequence[float],
y_data: Sequence[float],
name: Optional[str] = None,
legend_entry: bool = False,
legend_label: Optional[str] = None,
**options,
):
series_params = self.plot_options.series_params.get(name, {})
axis = series_params.get("canvas", None)
color = series_params.get("color", self._get_default_color(name))

draw_ops = {
"zorder": 3,
"alpha": 0.1,
"color": color,
}
self._update_label_in_dict(draw_ops, name, legend_entry, legend_label)
draw_ops.update(**options)
self._get_axis(axis).fill_between(x_data, y1=y_lb, y2=y_ub, **draw_ops)
self._get_axis(axis).fill_between_x(y_data, x1=x_lb, x2=x_ub, **draw_ops)

def draw_report(
def draw_text_box(
self,
description: str,
rel_pos: Optional[Tuple[float, float]] = None,
**options,
):
bbox_props = {
Expand All @@ -375,16 +424,19 @@ def draw_report(
}
bbox_props.update(**options)

report_handler = self._axis.text(
*self.style.report_rpos,
if rel_pos is None:
rel_pos = self.style.text_box_rel_pos

text_box_handler = self._axis.text(
*rel_pos,
s=description,
ha="center",
va="top",
size=self.style.report_text_size,
size=self.style.text_box_text_size,
transform=self._axis.transAxes,
zorder=6,
zorder=1000, # Very large zorder to draw over other graphics.
)
report_handler.set_bbox(bbox_props)
text_box_handler.set_bbox(bbox_props)

@property
def figure(self) -> Figure:
Expand Down
Loading

0 comments on commit 94158f4

Please sign in to comment.