Ejemplo n.º 1
0
Archivo: plot.py Proyecto: whynot-s/evo
def draw_correspondence_edges(ax: plt.Axes,
                              traj_1: trajectory.PosePath3D,
                              traj_2: trajectory.PosePath3D,
                              plot_mode: PlotMode,
                              style: str = '-',
                              color: str = "black",
                              alpha: float = 1.) -> None:
    """
    Draw edges between corresponding poses of two trajectories.
    Trajectories must be synced, i.e. having the same number of poses.
    :param ax: plot axis
    :param traj_{1,2}: trajectory.PosePath3D or trajectory.PoseTrajectory3D
    :param plot_mode: PlotMode value
    :param style: matplotlib line style
    :param color: matplotlib color
    :param alpha: alpha value for transparency
    """
    if not traj_1.num_poses == traj_2.num_poses:
        raise PlotException(
            "trajectories must have same length to draw pose correspondences"
            " - try to synchronize them first")
    n = traj_1.num_poses
    interweaved_positions = np.empty((n * 2, 3))
    interweaved_positions[0::2, :] = traj_1.positions_xyz
    interweaved_positions[1::2, :] = traj_2.positions_xyz
    colors = np.array(n * [color])
    markers = colored_line_collection(interweaved_positions,
                                      colors,
                                      plot_mode,
                                      step=2,
                                      alpha=alpha,
                                      linestyles=style)
    ax.add_collection(markers)
Ejemplo n.º 2
0
 def _plot_on_axis(self, ax: plt.Axes, **collection_options: Any) -> mpl_collections.Collection:
     # Step-1: Convert value_map to a list of polygons to plot.
     polygon_list = self._get_polygon_units()
     collection: mpl_collections.Collection = mcoll.PolyCollection(
         [c.polygon for c in polygon_list], cmap=self._config['colormap'], **collection_options
     )
     collection.set_clim(self._config.get('vmin'), self._config.get('vmax'))
     collection.set_array(np.array([c.value for c in polygon_list]))
     # Step-2: Plot the polygons
     ax.add_collection(collection)
     collection.update_scalarmappable()
     # Step-3: Write annotation texts
     if self._config.get('annotation_map') or self._config.get('annotation_format'):
         self._write_annotations([(c.center, c.annot) for c in polygon_list], collection, ax)
     ax.set(xlabel='column', ylabel='row')
     # Step-4: Draw colorbar if applicable
     if self._config.get('plot_colorbar'):
         self._plot_colorbar(collection, ax)
     # Step-5: Set min/max limits of x/y axis on the plot.
     rows = set([q.row for qubits in self._value_map.keys() for q in qubits])
     cols = set([q.col for qubits in self._value_map.keys() for q in qubits])
     min_row, max_row = min(rows), max(rows)
     min_col, max_col = min(cols), max(cols)
     min_xtick = np.floor(min_col)
     max_xtick = np.ceil(max_col)
     ax.set_xticks(np.arange(min_xtick, max_xtick + 1))
     min_ytick = np.floor(min_row)
     max_ytick = np.ceil(max_row)
     ax.set_yticks(np.arange(min_ytick, max_ytick + 1))
     ax.set_xlim((min_xtick - 0.6, max_xtick + 0.6))
     ax.set_ylim((max_ytick + 0.6, min_ytick - 0.6))
     # Step-6: Set title
     if self._config.get("title"):
         ax.set_title(self._config["title"], fontweight='bold')
     return collection
Ejemplo n.º 3
0
def draw_edges(graph: Graph,
               pos: dict,
               edges_list: list = None,
               edges_color: Union[str, list] = 'k',
               axes: plt.Axes = None) -> None:
    """
    Draw graph edges.

    :param graph: graph
    :param pos: node -> position (x,y) dictionary
    :param edges_list: edges to draw
    :param edges_color: nodes color, scalar or sequence (matplotlib compatible)
    :param axes: axes to draw the graph at
    """
    if axes is None:
        axes = plt.gca()

    edge_pos = np.asarray([(pos[e[0]], pos[e[1]])
                           for e in (edges_list or graph.edges)])
    if isinstance(edges_color, str):
        edges_color = (edges_color, )
    edges_color = tuple([colorConverter.to_rgba(c) for c in edges_color])
    edges_collection = LineCollection(edge_pos,
                                      colors=edges_color,
                                      transOffset=axes.transData)
    edges_collection.set_zorder(1)  # Edges go behind nodes.
    axes.add_collection(edges_collection)
Ejemplo n.º 4
0
def _draw_edges(ax: plt.Axes, pos: np.ndarray, g: np.ndarray, gcolor: str,
                galpha: float, glinewidths: float) -> None:
    lc = LineCollection(zip(pos[g.row], pos[g.col]),
                        linewidths=0.25,
                        zorder=0,
                        color=gcolor,
                        alpha=galpha)
    ax.add_collection(lc)
Ejemplo n.º 5
0
def traj_colormap(ax: plt.Axes,
                  traj: trajectory.PosePath3D,
                  array: ListOrArray,
                  plot_mode: PlotMode,
                  min_map: float,
                  max_map: float,
                  title: str = "",
                  fig: typing.Optional[mpl.figure.Figure] = None) -> None:
    """
    color map a path/trajectory in xyz coordinates according to
    an array of values
    :param ax: plot axis
    :param traj: trajectory.PosePath3D or trajectory.PoseTrajectory3D object
    :param array: Nx1 array of values used for color mapping
    :param plot_mode: PlotMode
    :param min_map: lower bound value for color mapping
    :param max_map: upper bound value for color mapping
    :param title: plot title
    :param fig: plot figure. Obtained with plt.gcf() if none is specified
    """
    pos = traj.positions_xyz
    norm = mpl.colors.Normalize(vmin=min_map, vmax=max_map, clip=True)
    mapper = cm.ScalarMappable(
        norm=norm,
        cmap=SETTINGS.plot_trajectory_cmap)  # cm.*_r is reversed cmap
    mapper.set_array(array)
    colors = [mapper.to_rgba(a) for a in array]
    line_collection = colored_line_collection(pos, colors, plot_mode)
    ax.add_collection(line_collection)
    ax.autoscale_view(True, True, True)
    if plot_mode == PlotMode.xyz:
        ax.set_zlim(np.amin(traj.positions_xyz[:, 2]),
                    np.amax(traj.positions_xyz[:, 2]))
        if SETTINGS.plot_xyz_realistic:
            set_aspect_equal_3d(ax)
    if fig is None:
        fig = plt.gcf()
    cbar = fig.colorbar(
        mapper, ticks=[min_map, (max_map - (max_map - min_map) / 2), max_map])
    cbar.ax.set_yticklabels([
        "{0:0.3f}".format(min_map),
        "{0:0.3f}".format(max_map - (max_map - min_map) / 2),
        "{0:0.3f}".format(max_map)
    ])

    if title:
        ax.legend(frameon=True)
        ax.set_title(title)
Ejemplo n.º 6
0
def draw_graph_edges(figure: plt.Figure, axis: plt.Axes, graph: nx.Graph):
    """draws graph edges from node to node, colored by weight

    Parameters
    ----------
    figure: plt.Figure
        a matplotlib Figure
    axis: plt.Axes
        a matplotlib Axes, part of Figure
    graphs: nx.Graph
        a networkx graph, assumed to have edges formed like
        graph.add_edge((0, 1), (0, 2), weight=1.234)

    Notes
    -----
    modifes figure and axis in-place

    """
    weights = np.array([i["weight"] for i in graph.edges.values()])
    # graph is (row, col), transpose to get (x, y)
    segments = []
    for edge in graph.edges:
        segments.append([edge[0][::-1], edge[1][::-1]])
    line_coll = LineCollection(segments,
                               linestyle='solid',
                               cmap="plasma",
                               linewidths=0.3)
    line_coll.set_array(weights)
    vals = np.concatenate(line_coll.get_segments())
    mnvals = vals.min(axis=0)
    mxvals = vals.max(axis=0)
    ppvals = vals.ptp(axis=0)
    buffx = 0.02 * ppvals[0]
    buffy = 0.02 * ppvals[1]
    line_coll.set_linewidth(0.3 * 512 / ppvals[0])
    axis.add_collection(line_coll)
    axis.set_xlim(mnvals[0] - buffx, mxvals[0] + buffx)
    axis.set_ylim(mnvals[1] - buffy, mxvals[1] + buffy)
    # invert yaxis for image-like orientation
    axis.invert_yaxis()
    axis.set_aspect("equal")
    divider = make_axes_locatable(axis)
    cax = divider.append_axes("right", size="5%", pad=0.05)
    figure.colorbar(line_coll, ax=axis, cax=cax)
    axis.set_title("PCC neighbor graph")
Ejemplo n.º 7
0
def hexplot(
        ax: plt.Axes,
        grid: np.ndarray,
        data: np.ndarray,
        hex_size: float=11.5,
        cmap: str='viridis'
) -> plt.Axes:
    """
    Plot grid and data on a hexagon grid. Useful for SOMs.

    Parameters
    ----------
    ax : Axes to plot on.
    grid : Array of (x, y) tuples.
    data : Array of len(grid) with datapoint.
    hex_size : Radius in points determining the hexagon size.
    cmap : Colormap to use for colouring.

    Returns
    -------
    ax : Axes with hexagon plot.

    """

    # Create hexagons
    collection = RegularPolyCollection(
        numsides=6,
        sizes=(2 * np.pi * hex_size ** 2,),
        edgecolors=(0, 0, 0, 0),
        transOffset=ax.transData,
        offsets=grid,
        array=data,
        cmap=plt.get_cmap(cmap)
    )

    # Scale the plot properly
    ax.add_collection(collection, autolim=True)
    ax.set_xlim(grid[:, 0].min() - 0.75, grid[:, 0].max() + 0.75)
    ax.set_ylim(grid[:, 1].min() - 0.75, grid[:, 1].max() + 0.75)
    ax.axis('off')

    return ax
Ejemplo n.º 8
0
def hexplot(
        ax: plt.Axes,
        grid: np.ndarray,
        data: np.ndarray,
        hex_size: float=11.5,
        cmap: str='viridis'
) -> plt.Axes:
    '''
    Plot grid and data on a hexagon grid. Useful for SOMs.

    Parameters
    ----------
    ax : Axes to plot on.
    grid : Array of (x, y) tuples.
    data : Array of len(grid) with datapoint.
    hex_size : Radius in points determining the hexagon size.
    cmap : Colormap to use for colouring.

    Returns
    -------
    ax : Axes with hexagon plot.

    '''

    # Create hexagons
    collection = RegularPolyCollection(
        numsides=6,
        sizes=(2 * np.pi * hex_size ** 2,),
        edgecolors=(0, 0, 0, 0),
        transOffset=ax.transData,
        offsets=grid,
        array=data,
        cmap=plt.get_cmap(cmap)
    )

    # Scale the plot properly
    ax.add_collection(collection, autolim=True)
    ax.set_xlim(grid[:, 0].min() - 0.75, grid[:, 0].max() + 0.75)
    ax.set_ylim(grid[:, 1].min() - 0.75, grid[:, 1].max() + 0.75)
    ax.axis('off')

    return ax
Ejemplo n.º 9
0
    def show(self, subplot: plt.Axes, data: np.ndarray, **kwargs):
        # data shape should be (num_joints, 3)
        # swap y and z for matplotlib
        dc = data.copy()
        dc[:, 2], dc[:, 1] = data[:, 1], data[:, 2]
        data = dc

        x = data[:, 0]
        y = data[:, 1]
        z = data[:, 2]
        if self._scatter is None:
            self._scatter = subplot.scatter(x, y, z)
        else:
            # Update 3D points
            self._scatter.set_offsets(data[:, :2])
            if hasattr(self._scatter, "_offsets3d"):
                self._scatter._offsets3d = x, y, z
                self._scatter.stale = True
            else:
                self._scatter.set_alpha(1.)
                self._scatter.set_3d_properties(z, "z")

        if self.graph is not None:
            # plot lines between scatter plot dots
            edges = np.asarray(self.graph.edges)
            p = data[edges[:, 0]]
            q = data[edges[:, 1]]

            ls = np.hstack([p, q])
            ls = ls.reshape((-1, 2, 3))
            if self._lines is None or self._lines not in subplot.collections:
                self._lines = Line3DCollection(ls,
                                               linewidths=2,
                                               colors="dimgray")
                subplot.add_collection(self._lines)
            else:
                self._lines.set_segments(ls)

        set_3d_aspect_ratio_equal(subplot)
Ejemplo n.º 10
0
def plot_triv(fig: plt.Figure,
              ax: plt.Axes,
              A: Triangle,
              B: Triangle,
              dist: bool = True):
    _A, _B = get_triv(A, B)
    _A.plot(fig, ax, "black")
    _B.plot(fig, ax, "blue")
    ax.scatter([0, 1], [0, 0], c="black")
    ax.annotate("$(0, 0)$", [-0.01, -0.02])
    ax.annotate("$(1, 0)$", [1, -0.02])

    _A.plot(fig, ax, color="black")
    _A_offsets = np.array([
        [0.07, 0.02],
        [-0.05, 0.03],
        [-0.02, -0.04],
    ])
    _B.plot(fig, ax, color="blue")
    _B_offsets = np.array([
        [0.05, 0.0125],
        [-0.03, 0.01],
        [-0.02, -0.04],
    ])
    ax.annotate("$t$", _A.points[2] + np.array([-0.03, -0.005]))
    ax.annotate("$t^\prime$", _B.points[2] + np.array([-0.025, 0.01]))
    for i in range(3):
        ax.annotate(f"$\\alpha_{i}$", _A.points[i] + _A_offsets[i])
        ax.annotate(f"$\\beta_{i}$",
                    _B.points[i] + _B_offsets[i],
                    color="blue")

    if dist:
        ax.add_collection(
            LineCollection([[_A.points[2], _B.points[2]]],
                           color="black",
                           linestyles="--"))
        ax.scatter(*_A.points[2], c="black")
        ax.scatter(*_B.points[2], c="black")
Ejemplo n.º 11
0
def drawRects(ax: plt.Axes,
              data,
              facecolor=None,
              alpha: float = None,
              edgecolor=None,
              linewidth: float = None,
              profile: dict = None,
              autolim=True) -> None:
    """
    Draw multiple rectangles

    Args:
        ax: the plot axes
        data: either a 2D array of shape (num. rectangles, 4), or a list of tuples
            (x0, y0, x1, y1), where each row is a rectangle
        color: the face color
        edgecolor: the color of the edges
        alpha: alpha value for the rectangle (both facecolor and edgecolor)
        label: if given, a label is plotted at the center of the rectangle
        profile: the profile used, or None for default
        autolim: autoscale view
    """
    facecolor = _fallbackColor(facecolor, profile, key='facecolor')
    edgecolor = _fallbackColor(edgecolor, profile, key='edgecolor')
    linewidth = _fallback(linewidth, profile, 'linewidth')
    rects = []
    for coords in data:
        x0, y0, x1, y1 = coords
        rect = Rectangle((x0, y0), x1 - x0, y1 - y0)
        rects.append(rect)
    coll = PatchCollection(rects,
                           linewidth=linewidth,
                           alpha=alpha,
                           edgecolor=edgecolor,
                           facecolor=facecolor)
    ax.add_collection(coll, autolim=True)
    if autolim:
        ax.autoscale_view()
Ejemplo n.º 12
0
 def draw_path(self, ax: plt.Axes, path, properties: Properties, z: int):
     pattern = self.pattern(properties)
     lineweight = self.lineweight(properties)
     color = properties.color
     if len(pattern) < 2:
         vertices, codes = _get_path_patch_data(path)
         patch = PathPatch(Path(vertices, codes),
                           linewidth=lineweight,
                           color=color,
                           fill=False,
                           zorder=z)
         ax.add_patch(patch)
     else:
         renderer = EzdxfLineTypeRenderer(pattern)
         segments = renderer.line_segments(
             path.flattening(self._max_distance, segments=16))
         lines = LineCollection([((s.x, s.y), (e.x, e.y))
                                 for s, e in segments],
                                linewidths=lineweight,
                                color=color,
                                zorder=z)
         lines.set_capstyle('butt')
         ax.add_collection(lines)
Ejemplo n.º 13
0
 def plot(self, ax: plt.Axes, fd):
     for i1, c1 in enumerate(fd.cities):
         for i2, c2 in enumerate(fd.cities):
             if c1 is c2:
                 continue
             p1 = np.array([c1.x, c1.y])
             p2 = np.array([c2.x, c2.y])
             norm = p2 - p1
             norm[0], norm[1] = -norm[1], norm[0]
             norm /= np.linalg.norm(norm)
             p1 += norm * 1
             p2 += norm * 1
             npts = 100
             x = np.linspace(p1[0], p2[0], npts)
             y = np.linspace(p1[1], p2[1], npts)
             g = np.linspace(0, 1, npts)
             points = np.array([x, y]).T.reshape(-1, 1, 2)
             segments = np.concatenate([points[:-1], points[1:]], axis=1)
             nc = plt.Normalize(g.min(), g.max())
             lc = LineCollection(segments, cmap='copper', norm=nc)
             lc.set_array(g)
             lc.set_linewidth(self.get_amount(c1, c2) * 10)
             lc.set_zorder(0)
             ax.add_collection(lc)
Ejemplo n.º 14
0
 def draw_line(self, ax: plt.Axes, start: Vector, end: Vector,
               properties: Properties, z: int):
     pattern = self.pattern(properties)
     lineweight = self.lineweight(properties)
     color = properties.color
     if len(pattern) < 2:
         ax.add_line(
             Line2D(
                 (start.x, end.x),
                 (start.y, end.y),
                 linewidth=lineweight,
                 color=color,
                 zorder=z,
             ))
     else:
         renderer = EzdxfLineTypeRenderer(pattern)
         lines = LineCollection(
             [((s.x, s.y), (e.x, e.y))
              for s, e in renderer.line_segment(start, end)],
             linewidths=lineweight,
             color=color,
             zorder=z)
         lines.set_capstyle('butt')
         ax.add_collection(lines)
Ejemplo n.º 15
0
def show_image(im: Union[np.ndarray, Tensor],
               axis: plt.Axes = None,
               fig: plt.Figure = None,
               title: Optional[str] = None,
               color_map: str = "inferno",
               stack_depth: int = 0) -> Optional[plt.Figure]:
    """Plots a given image onto an axis. The repeated invocation of this function will cause figure plot overlap.

    If `im` is 2D and the length of second dimension are 4 or 5, it will be viewed as bounding box data (x0, y0, w, h,
    <label>).

    ```python
    boxes = np.array([[0, 0, 10, 20, "apple"],
                      [10, 20, 30, 50, "dog"],
                      [40, 70, 200, 200, "cat"],
                      [0, 0, 0, 0, "not_shown"],
                      [0, 0, -10, -20, "not_shown2"]])

    img = np.zeros((150, 150))
    fig, axis = plt.subplots(1, 1)
    fe.util.show_image(img, fig=fig, axis=axis) # need to plot image first
    fe.util.show_image(boxes, fig=fig, axis=axis)
    ```

    Users can also directly plot text

    ```python
    fig, axis = plt.subplots(1, 1)
    fe.util.show_image("apple", fig=fig, axis=axis)
    ```

    Args:
        axis: The matplotlib axis to plot on, or None for a new plot.
        fig: A reference to the figure to plot on, or None if new plot.
        im: The image (width X height) / bounding box / text to display.
        title: A title for the image.
        color_map: Which colormap to use for greyscale images.
        stack_depth: Multiple images can be drawn onto the same axis. When stack depth is greater than zero, the `im`
            will be alpha blended on top of a given axis.

    Returns:
        plotted figure. It will be the same object as user have provided in the argument.
    """
    if axis is None:
        fig, axis = plt.subplots(1, 1)
    axis.axis('off')
    # Compute width of axis for text font size
    bbox = axis.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
    width, height = bbox.width * fig.dpi, bbox.height * fig.dpi
    space = min(width, height)
    if not hasattr(im, 'shape') or len(im.shape) < 2:
        # text data
        im = to_number(im)
        if hasattr(im, 'shape') and len(im.shape) == 1:
            im = im[0]
        im = im.item()
        if isinstance(im, bytes):
            im = im.decode('utf8')
        text = "{}".format(im)
        axis.text(0.5,
                  0.5,
                  im,
                  ha='center',
                  transform=axis.transAxes,
                  va='center',
                  wrap=False,
                  family='monospace',
                  fontsize=min(45, space // len(text)))
    elif len(im.shape) == 2 and (im.shape[1] == 4 or im.shape[1] == 5):
        # Bounding Box Data. Should be (x0, y0, w, h, <label>)
        boxes = []
        im = to_number(im)
        color = ["m", "r", "c", "g", "y", "b"][stack_depth % 6]
        for box in im:
            # Unpack the box, which may or may not have a label
            x0 = float(box[0])
            y0 = float(box[1])
            width = float(box[2])
            height = float(box[3])
            label = None if len(box) < 5 else str(box[4])

            # Don't draw empty boxes, or invalid box
            if width <= 0 or height <= 0:
                continue
            r = Rectangle((x0, y0),
                          width=width,
                          height=height,
                          fill=False,
                          edgecolor=color,
                          linewidth=3)
            boxes.append(r)
            if label:
                axis.text(r.get_x() + 3,
                          r.get_y() + 3,
                          label,
                          ha='left',
                          va='top',
                          color=color,
                          fontsize=max(8, min(14, width // len(label))),
                          fontweight='bold',
                          family='monospace')
        pc = PatchCollection(boxes, match_original=True)
        axis.add_collection(pc)
    else:
        if isinstance(im, torch.Tensor) and len(im.shape) > 2:
            # Move channel first to channel last
            channels = list(range(len(im.shape)))
            channels.append(channels.pop(0))
            im = im.permute(*channels)
        # image data
        im = to_number(im)
        im_max = np.max(im)
        im_min = np.min(im)
        if np.issubdtype(im.dtype, np.integer):
            # im is already in int format
            im = im.astype(np.uint8)
        elif 0 <= im_min <= im_max <= 1:  # im is [0,1]
            im = (im * 255).astype(np.uint8)
        elif -0.5 <= im_min < 0 < im_max <= 0.5:  # im is [-0.5, 0.5]
            im = ((im + 0.5) * 255).astype(np.uint8)
        elif -1 <= im_min < 0 < im_max <= 1:  # im is [-1, 1]
            im = ((im + 1) * 127.5).astype(np.uint8)
        else:  # im is in some arbitrary range, probably due to the Normalize Op
            ma = abs(
                np.max(im,
                       axis=tuple([i for i in range(len(im.shape) - 1)])
                       if len(im.shape) > 2 else None))
            mi = abs(
                np.min(im,
                       axis=tuple([i for i in range(len(im.shape) - 1)])
                       if len(im.shape) > 2 else None))
            im = (((im + mi) / (ma + mi)) * 255).astype(np.uint8)
        # matplotlib doesn't support (x,y,1) images, so convert them to (x,y)
        if len(im.shape) == 3 and im.shape[2] == 1:
            im = np.reshape(im, (im.shape[0], im.shape[1]))
        alpha = 1 if stack_depth == 0 else 0.3
        if len(im.shape) == 2:
            axis.imshow(im, cmap=plt.get_cmap(name=color_map), alpha=alpha)
        else:
            axis.imshow(im, alpha=alpha)
    if title is not None:
        axis.set_title(title,
                       fontsize=min(20, 1 + width // len(title)),
                       family='monospace')
    return fig
Ejemplo n.º 16
0
def cancellation_progression(cnclp: CnclProg, model_output: int = None,
                             *, color_cycle: Sequence = None,
                             ax: plt.Axes = None, figsize=(8.0, 6.0), title: str = None,
                             xlabel="Estimated cancellation", ylabel="True cancellation", legend=True):
    if model_output is None:
        test_array = next(iter(next(iter(cnclp.values())).values()))
        if test_array.ndim != 2:
            n_model_outputs = test_array.shape[2]
            if n_model_outputs != 1:
                raise ValueError("When plotting cancellation progressions, the model_output argument can only be "
                                 "omitted when there is only one model output in the progression. However, this "
                                 f"progression stores {n_model_outputs} model outputs. So, please supply model_output.")
            else:
                model_output = 0

    # Create figure if necessary.
    if ax is None:
        ax = plt.figure(figsize=figsize).gca()

    # Set title, xlabel, and ylabel if applicable.
    if title is not None:
        ax.set_title(title)
    if xlabel is not None:
        ax.set_xlabel(xlabel)
    if ylabel is not None:
        ax.set_ylabel(ylabel)

    if color_cycle is None:
        color_cycle = _DEFAULT_COLOR_CYCLE

    for (om, om_cnclp), base_color in zip(cnclp.items(), cycle(color_cycle)):
        color_hue = rgb_to_hsv(to_rgb(base_color))[0]

        for (frag, frag_cnclp), color_value in zip(om_cnclp.items(), np.linspace(1, 0.7, len(om_cnclp))):
            if model_output is not None:
                frag_cnclp = frag_cnclp[model_output]

            if not np.all(np.isnan(frag_cnclp[:, 1])):
                third_col_max = int(frag_cnclp[0, 2])

                max_sat_color = hsv_to_rgb([color_hue, 1.0, color_value])
                min_sat_color = hsv_to_rgb([color_hue, 0.2, color_value])
                # Use linspace in the wrong order and then reverse the result, such that when
                # 'third_col_max' is 1, the cmap contains the max saturation and not the
                # min saturation color as its only color.
                cmap = ListedColormap(np.linspace(max_sat_color, min_sat_color, third_col_max)[::-1])

                segments = list(zip(frag_cnclp[:-1, :2], frag_cnclp[1:, :2]))
                lc = LineCollection(segments, cmap=cmap, norm=plt.Normalize(1, third_col_max),
                                    label=f"{type(om).__name__[0]}~{frag}", color=max_sat_color, zorder=1)
                lc.set_array(frag_cnclp[1:, 2])
                ax.add_collection(lc)

    # Necessary because 'ax.add_collection()' doesn't adjust xlim and ylim automatically.
    ax.autoscale_view()

    # Draw the diagonal dotted line.
    diag = [max(ax.get_xlim()[0], ax.get_ylim()[0]),
            min(ax.get_xlim()[1], ax.get_ylim()[1])]
    ax.plot(diag, diag, ls="dotted", color="gray", zorder=0)

    if legend:
        ax.legend(loc="upper left", bbox_to_anchor=(1.01, 0, 1, 1))