Source code for dartsflash.diagram

import numpy as np
from matplotlib import pyplot as plt
from matplotlib import colors
from matplotlib import gridspec
import matplotlib.tri as tri


[docs] class Diagram: """ This is a base class for construction of diagrams. :ivar colours: Predefined set of colours :type colours: list[str] :ivar markers: Predefined set of markers :type markers: list[str] :ivar linestyles: Predefined set of linestyles :type linestyles: list[str] :ivar ax_labels: Axis labels :type ax_labels: list[str] """ cmap = "winter" colours = ["blue", "lightskyblue", "mediumseagreen", "orchid", "dodgerblue", "darkcyan"] markers = [None, "--", "o", "v"] linestyles = ["solid", "dashed", "dotted", "dashdot"] fontsizes = {'suptitle': 20, 'title': 16, 'axlabel': 12, 'axes': 10, 'legend': 12}
[docs] def __init__(self, nrows: int = 1, ncols: int = 1, figsize: tuple = (8, 6), sharex: bool = False, sharey: bool = False): """ Constructor for Diagram base class :param nrows, ncols: Number of rows/columns for subplots :type nrows, ncols: int :param figsize: Size of figure object :type figsize: tuple[float] :param sharex, sharey: Share axes :type sharex, sharey: bool """ self.fig, self.ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=figsize, sharex=sharex, sharey=sharey) self.ax = [self.ax] if (nrows == 1 and ncols == 1) else self.ax # make sure we can index self.ax[subplot_idx] self.im = [] self.subplot_idx = 0 if (nrows == 1 or ncols == 1) else (0, 0) if (nrows > 1 and ncols > 1): self.subplot_idxs = [(r, c) for c in range(ncols) for r in range(nrows)] else: nplots = nrows if nrows > 1 else (ncols if ncols > 1 else 1) self.subplot_idxs = [i for i in range(nplots)]
[docs] def get_levels(self, data: np.ndarray, is_float: bool, nlevels: int, min_val: float = None, max_val: float = None): # Define levels of contours/patches amin = np.nanmin(data) if min_val is None else min_val amax = np.nanmax(data) if max_val is None else max_val if is_float: levels = np.linspace(amin, amax, nlevels) ticks = np.linspace(amin, amax, nlevels if nlevels <= 11 else 11) else: levels = np.linspace(amin - 0.5, amax + .5, amax-amin+2) ticks = np.linspace(amin, amax, len(levels)-1) return levels, ticks
[docs] def get_cmap(self, levels: np.ndarray, colours: list = None): # Get colormap and set discrete colorbar levels if isinstance(colours, (list, np.ndarray)): cmap = colors.ListedColormap(colours[:len(levels)]) else: try: # Specified cmap cmap = plt.get_cmap(colours if colours is not None else self.cmap, len(levels)) except ValueError: # Single colour cmap = colors.ListedColormap([colours for _ in range(len(levels))]) level_diff = levels[1] - levels[0] if len(levels) > 1 else 1. bounds = np.linspace(levels[0], levels[-1] + level_diff, len(levels) + 1) norm = colors.BoundaryNorm(bounds, cmap.N) return cmap, norm
[docs] def draw_surf(self, x, y, data: np.ndarray, xlim: list = None, ylim: list = None, ax_labels: list = None, is_float: bool = True, nlevels: int = 10, min_val: float = None, max_val: float = None, colours: list = None, colorbar: bool = False, colorbar_labels: list = None, colorbar_title: str = None, contour: bool = False, fill_contour: bool = False, contour_linestyle: str = None, logx: bool = False, logy: bool = False): """ Function to draw 2D pcolormesh/contourplot. :param x: Grid points on x-axis :type x: list :param y: Grid points on y-axis :type y: list :param data: :class:`np.ndarray` of data for plotting :param xlim, ylim: Limits for axes :type xlim, ylim: list :param ax_labels: Axis labels :param is_float: Type of data is float/integer :param nlevels: Number of contour levels for legend :type nlevels: int :param min_val: Minimum value of contour levels :type min_val: float :param max_val: Maximum value of contour levels :type max_val: float :param colours: Colours for ListedColormap :type colours: list[str] :param colorbar: Switch to add colorbar :type colorbar: bool :param colorbar_labels: Optional labels for colorbar :param colorbar_title: Optional colorbar title :type colorbar_title: str :param contour: Switch for contour or surface plot :type contour: bool :param fill_contour: Switch for filled contour plot :type fill_contour: bool """ # Define levels of contours/patches and cmap + norm levels, ticks = self.get_levels(data, is_float, nlevels, min_val=min_val, max_val=max_val) cmap, norm = self.get_cmap(levels, colours) nx, ny = len(x), len(y) if contour: xgrid = np.logspace(np.log10(x[0]), np.log10(x[-1]), nx) if logx else np.linspace(x[0], x[-1], nx) ygrid = np.logspace(np.log10(y[0]), np.log10(y[-1]), ny) if logy else np.linspace(y[0], y[-1], ny) X, Y = np.meshgrid(xgrid, ygrid) z = np.swapaxes(data, 0, 1) mask = np.where(np.isnan(z), 1, 0) z_ma = np.ma.array(z, mask=mask) if fill_contour: self.im = self.ax[self.subplot_idx].contourf(X, Y, z_ma, cmap=cmap, norm=norm, levels=levels, vmin=levels[0], vmax=levels[-1], extend='both') # if np.any(mask): # self.ax[self.subplot_idx].contourf(X, Y, mask, colors=cmap.colors[0], levels=[0., 1.]) else: self.ax[self.subplot_idx].contour(X, Y, z_ma, cmap=cmap, linestyles=contour_linestyle, norm=norm, levels=levels, vmin=levels[0], vmax=levels[-1], extend='both') if np.any(mask): self.ax[self.subplot_idx].contour(X, Y, mask, colors=cmap.colors[0], linestyles=contour_linestyle, levels=[0., 1.]) else: dx, dy = x[1] - x[0], y[1] - y[0] xgrid = np.linspace(x[0] - 0.5 * dx, x[-1] + 0.5 * dx, nx + 1) ygrid = np.linspace(y[0] - 0.5 * dy, y[-1] + 0.5 * dy, ny + 1) X, Y = np.meshgrid(xgrid, ygrid) z = np.swapaxes(data, 0, 1) self.im = self.ax[self.subplot_idx].pcolormesh(X, Y, z, shading='flat', cmap=cmap, norm=norm) self.ax[self.subplot_idx].set(xlim=xlim if xlim is not None else [x[0], x[-1]], ylim=ylim if ylim is not None else [y[0], y[-1]], xscale='log' if logx else 'linear', yscale='log' if logy else 'linear',) self.add_attributes(ax_labels=ax_labels) self.ax[self.subplot_idx].tick_params(axis='both', which='major', labelsize=self.fontsizes['axes']) if colorbar: cbar = self.fig.colorbar(plt.cm.ScalarMappable(cmap=cmap, norm=norm), ax=self.ax[self.subplot_idx], ticks=ticks) if colorbar_labels is not None: yticks = np.linspace(*cbar.ax.get_ylim(), cmap.N + 1)[:-1] yticks += (yticks[1] - yticks[0]) / 2 cbar.set_ticks(yticks, labels=colorbar_labels) cbar.ax.tick_params(length=0) cbar.ax.tick_params(labelsize=self.fontsizes['axes']) if colorbar_title is not None: cbar.set_label(colorbar_title, size=self.fontsizes['axlabel']) return
[docs] def draw_line(self, X: np.ndarray, Y: np.ndarray, colours: list = None, styles: list = None, widths: float = 2, datalabels: list = None): """ Function to draw line with coordinates X-Y(-Z) :param X, Y: x- and y-axis values. N-D arrays must be provided as [ith_curve, values] :param colours: Colour or list of colours :param styles: Line-/Markerstyle or list of styles for line/scatter plot :param widths: Line-/Markerwidth or list of widths for line/scatter plot :param datalabels: List of datalabels """ X, Y = np.array(X), np.array(Y) number_of_curves = np.shape(Y)[0] if (not np.isscalar(Y) and Y.ndim > 1) else np.shape(X)[0] if (not np.isscalar(X) and X.ndim > 1) else 1 colours = [colours for i in range(number_of_curves)] if not isinstance(colours, (list, np.ndarray, type(None))) else colours styles = [styles for i in range(number_of_curves)] if not isinstance(styles, (list, np.ndarray, type(None))) else styles widths = [widths for i in range(number_of_curves)] if not isinstance(widths, (list, np.ndarray, type(None))) else widths datalabels = [datalabels for i in range(number_of_curves)] if not isinstance(datalabels, (list, np.ndarray, type(None))) else datalabels if X.ndim == Y.ndim: Y = np.tile(Y, (number_of_curves, 1)) if (not np.isscalar(Y) or not isinstance(Y[0], (list, np.ndarray))) else Y X = np.tile(X, (number_of_curves, 1)) if (not np.isscalar(X) or not isinstance(X[0], (list, np.ndarray))) else X for i in range(number_of_curves): self.ax[self.subplot_idx].plot(X[i][:], Y[i][:], c=colours[int(i%len(colours))] if colours is not None else self.colours[int(i % len(self.colours))], linestyle=styles[int(i%len(styles))] if styles is not None else self.linestyles[0], linewidth=widths[int(i%len(widths))] if widths is not None else None, label=datalabels[i] if datalabels is not None else None ) return
[docs] def draw_point(self, X: np.ndarray, Y: np.ndarray, Z: np.ndarray = None, colours: list = None, markers: list = None, widths: list = None, datalabels: list = None): """ Function to draw points with coordinates X-Y(-Z) :param X: List of X-coordinates for points :param Y: List of Y-coordinates for points :param Z: List of Z-coordinates for points, optional :param colours: Colour or list of colours :param styles: Markerstyle or list of styles for scatter plot :param widths: Markerwidth or list of widths for scatter plot :param datalabels: List of data labels """ X, Y = np.array(X), np.array(Y) number_of_curves = np.shape(Y)[0] if (not np.isscalar(Y) and Y.ndim > 1) else np.shape(X)[0] if (not np.isscalar(X) and X.ndim > 1) else 1 colours = [colours for i in range(number_of_curves)] if not isinstance(colours, (list, np.ndarray, type(None))) else colours markers = [markers for i in range(number_of_curves)] if not isinstance(markers, (list, np.ndarray, type(None))) else markers widths = [widths for i in range(number_of_curves)] if not isinstance(widths, (list, np.ndarray, type(None))) else widths datalabels = [datalabels for i in range(number_of_curves)] if not isinstance(datalabels, (list, np.ndarray, type(None))) else datalabels X = np.array([X]) if np.isscalar(X) else X Y = np.array([Y]) if np.isscalar(Y) else Y X = np.tile(X, (number_of_curves, 1)) if (not np.isscalar(X) or not isinstance(X[0], (list, np.ndarray))) else X Y = np.tile(Y, (number_of_curves, 1)) if (not np.isscalar(Y) or not isinstance(Y[0], (list, np.ndarray))) else Y if Z is not None: Z = np.array([Z]) if np.isscalar(Z) else np.array(Z) Z = np.tile(Z, (number_of_curves, 1)) if (not np.isscalar(Z) or not isinstance(Z[0], (list, np.ndarray))) else Z for i in range(number_of_curves): self.ax[self.subplot_idx].scatter(X[i][:], Y[i][:], c=colours[int(i%len(colours))] if colours is not None else self.colours[int(i % len(self.colours))], marker=markers[int(i%len(markers))] if markers is not None else self.markers[0], s=widths[int(i%len(widths))] if widths is not None else None, label=datalabels[i] if datalabels is not None else None, zorder=2) return
[docs] def get_contours(self, x, y, data: np.ndarray, mask: float = None): # Find boundaries between discrete levels contours = {} levels = {} for i, xi in enumerate(x): for j, yj in enumerate(y): if i < len(x) - 1 and data[i, j] != data[i + 1, j]: pair = (min(data[i, j], data[i + 1, j]), max(data[i, j], data[i + 1, j])) if mask is not None and pair[0] <= mask: break key = 0 for level in levels.values(): if level == pair: break key += 1 levels[key] = pair if key in contours.keys(): contours[key] += [[(i + 1, i + 1), (j, j + 1)]] else: contours[key] = [[(i + 1, i + 1), (j, j + 1)]] if j < len(y) - 1 and data[i, j] != data[i, j + 1]: pair = (min(data[i, j], data[i, j + 1]), max(data[i, j], data[i, j + 1])) if mask is not None and pair[0] <= mask: break key = 0 for level in levels.values(): if level == pair: break key += 1 levels[key] = pair if key in contours.keys(): contours[key] += [[(i, i + 1), (j + 1, j + 1)]] else: contours[key] = [[(i, i + 1), (j + 1, j + 1)]] return contours, levels
[docs] def draw_contours(self, x, y, data: np.ndarray, mask: float = None, colours: str = None, linewidth: float = 1.): """ Function to draw contour lines between discrete levels :param x: Grid points on x-axis :type x: list :param y: Grid points on y-axis :type y: list :param data: :class:`np.ndarray` of data for plotting :param colours: Colours for contourlines :type colours: str :param linewidth: Line width :type linewidth: float """ # Find boundaries between discrete levels contours, levels = self.get_contours(x, y, data, mask=mask) # Plot lines at the boundaries dx, dy = x[1] - x[0], y[1] - y[0] xgrid = np.linspace(x[0] - dx * 0.5, x[-1] + dx * 0.5, len(x) + 1) ygrid = np.linspace(y[0] - dy * 0.5, y[-1] + dy * 0.5, len(y) + 1) colours = colours if colours is not None else self.colours for ith_contour, (level, lines) in enumerate(contours.items()): colour = colours if isinstance(colours, str) else colours[ith_contour] for line in lines: xx = [xgrid[line[0][0]], xgrid[line[0][1]]] yy = [ygrid[line[1][0]], ygrid[line[1][1]]] self.ax[self.subplot_idx].plot(xx, yy, c=colour, linewidth=linewidth) self.set_axes(xlim=[x[0], x[-1]], ylim=[y[0], y[-1]]) return
[docs] def set_axes(self, xlim: list = None, ylim: list = None, logx: bool = False, logy: bool = False): """ Function to set axes limits and scale :param xlim, ylim: Limits of x- and y-axes, default is None :param logx, logy: Option to set x- or y-axis to logscale, default is False """ # Set log scale and limits if logx: self.ax[self.subplot_idx].set_xscale("log") if logy: self.ax[self.subplot_idx].set_yscale("log") self.ax[self.subplot_idx].set_xlim(xlim) self.ax[self.subplot_idx].set_ylim(ylim) return
[docs] def add_attributes(self, subplot_idx: int = None, suptitle: str = None, title: str = None, ax_labels: list = None, legend: bool = False, legend_loc: str = 'upper right', grid: bool = False): """ Function to add attributes to diagram. :param subplot_idx: Index of subplot to add attribute to, default is None which will apply it to all subplots :param title: Figure title :type title: str :param ax_labels: Axes labels :type ax_labels: list[str] :param legend: Switch to add legend for lines/points :type legend: bool """ # add title, axlabels, legend if not None if suptitle is not None: self.fig.suptitle(suptitle, fontsize=self.fontsizes['suptitle']) subplot_idxs = self.subplot_idxs if subplot_idx is None else [subplot_idx] if title is not None: for subplot_idx in subplot_idxs: self.ax[subplot_idx].set_title(title, fontsize=self.fontsizes['title']) if ax_labels is not None: for subplot_idx in subplot_idxs: self.ax[subplot_idx].set_xlabel(ax_labels[0], fontsize=self.fontsizes['axlabel']) self.ax[subplot_idx].set_ylabel(ax_labels[1], fontsize=self.fontsizes['axlabel']) if legend: for subplot_idx in subplot_idxs: self.ax[subplot_idx].legend(loc=legend_loc, fontsize=self.fontsizes['legend']) if grid: for subplot_idx in subplot_idxs: self.ax[subplot_idx].grid(True, which='both', linestyle='-.') self.ax[subplot_idx].tick_params(direction='in', length=1, width=1, colors='k', grid_color='k', grid_alpha=0.2, labelsize=self.fontsizes['axes'])
[docs] def add_text(self, text: str, xloc: float, yloc: float, fontsize: float = 8, colours: str = 'k', box_colour: str = 'none'): """ Function to add text to diagram. """ ax = self.ax[self.subplot_idx] ax.text(xloc, yloc, text, fontsize=fontsize, transform=ax.transAxes, c=colours, bbox=dict(facecolor='none', edgecolor=box_colour, boxstyle='square', mutation_aspect=1.5))
[docs] class NCompDiagram(Diagram): """ This is a base class for construction of N-component diagrams. """
[docs] def __init__(self, nc: int, dz: float, min_z: list = None, max_z: list = None, nrows: int = 1, ncols: int = 1, figsize: tuple = (10, 10)): """ The constructor will find the set of physical compositions. :param nc: Number of components :type nc: int :param dz: Mesh size of compositions :type dz: float :param min_z: Minimum composition of each component (i = 1,...,nc-1), optional :type min_z: list[float] :param max_z: Maximum composition of each component (i = 1,...,nc-1), optional :type max_z: list[float] :param nrows: Number of rows for subplots :type nrows: int :param ncols: Number of columns for subplots :type ncols: int :param figsize: Size of figure object :type figsize: tuple[float] """ super().__init__(nrows, ncols, figsize) self.min_z = min_z if min_z is not None else [0. for _ in range(nc - 1)] self.max_z = max_z if max_z is not None else [1. for _ in range(nc - 1)] self.min_z += [max(1.-sum(self.max_z), 0.)] self.max_z += [max(1.-sum(self.min_z), 0.)] n_points = [int(np.round(((self.max_z[i] - self.min_z[i]) / dz))) + 1 for i in range(nc - 1)] comp_bound = np.array([[self.min_z[i], self.max_z[i]] for i in range(nc - 1)]) comp_vec = [np.linspace(comp_bound[i, 0], comp_bound[i, 1], n_points[i]) for i in range(nc - 1)] composition = np.zeros((np.prod(n_points), nc)) if nc == 2: composition[:, 0] = comp_vec[0][:] elif nc == 3: for ii in range(n_points[0]): composition[ii * n_points[1]:(ii + 1) * n_points[1], 0] = comp_vec[0][ii] for jj in range(n_points[1]): composition[ii * n_points[1] + jj, 1] = comp_vec[1][jj] elif nc == 4: for ii in range(n_points[0]): composition[ii * n_points[1] * n_points[2]:(ii + 1) * n_points[1] * n_points[2], 0] = comp_vec[0][ii] for jj in range(n_points[1]): composition[ ii * n_points[1] * n_points[2] + jj * n_points[2]:ii * n_points[1] * n_points[2] + (jj + 1) * n_points[2], 1] = comp_vec[1][jj] for kk in range(n_points[2]): composition[ii * n_points[1] * n_points[2] + jj * n_points[2] + kk, 2] = comp_vec[2][kk] composition[:, -1] = 1. - np.sum(composition, 1) self.comp_physical = composition[(composition[:, -1] >= -1e-14) * (composition[:, -1] <= 1.+1e-14)]
[docs] class TernaryDiagram(NCompDiagram): """ This class can construct ternary diagrams. """
[docs] def __init__(self, dz: float, min_z: list = None, max_z: list = None, nrows: int = 1, ncols: int = 1, figsize: tuple = (10, 10)): """ The constructor will find the set of physical compositions for nc=3. :param dz: Mesh size of compositions :type dz: float :param min_z: Minimum composition of each component (i = 1,...,nc-1), optional :type min_z: list[float] :param max_z: Maximum composition of each component (i = 1,...,nc-1), optional :type max_z: list[float] :param nrows: Number of rows for subplots :type nrows: int :param ncols: Number of columns for subplots :type ncols: int :param figsize: Size of figure object :type figsize: tuple[float] """ super().__init__(nc=3, dz=dz, min_z=min_z, max_z=max_z, nrows=nrows, ncols=ncols, figsize=figsize) # barycentric coords: (a,b,c) self.a = self.comp_physical[:, 0] self.b = self.comp_physical[:, 1] self.c = self.comp_physical[:, 2] self.n_data_points = self.a.shape[0]
[docs] def triangulation(self, X1: np.ndarray, X2: np.ndarray, corner_labels: list = None): """ Function to construct triangular grid and axis. :param X1, X2: Composition of 1st and second component :type X1, X2: list :param corner_labels: Labels at corners :returns: Triangular grid and Axes :rtype: :class:`matplotlib.tri.Triangulation`, :class:`matplotlib.pyplot.Axes` """ # calculate corner points x3min, x3max = max(1.-X1[-1]-X2[-1], 0.), min(1.-X1[0]-X2[0], 1.) ymin = max(x3min * np.sqrt(3.)/2., 0.) # sin(pi/3) = sqrt(3)/2 ymax = min(x3max * np.sqrt(3.)/2., np.sqrt(3)/2.) xmin = max((1.-X1[-1]) - ymin / np.sqrt(3.), 0.) # tan(pi/3) = sqrt(3) xmax = min((1.-X1[0]) - ymin / np.sqrt(3.), 1.) xmid = (xmin + xmax) / 2. self.corners = np.array([[xmin, ymin], [xmax, ymin], [xmid, ymax]]) triangle = tri.Triangulation(self.corners[:, 0], self.corners[:, 1]) # plotting the mesh self.ax[self.subplot_idx].triplot(triangle, color='k', linewidth=0.5) self.ax[self.subplot_idx].set_ylim([ymin, ymax*1.1]) self.ax[self.subplot_idx].axis('off') self.ax[self.subplot_idx].set_aspect('equal') # translate the data to cartesian coords self.data_idxs = [[(x1 + x2 <= 1. + 1e-14) for x2 in X2] for x1 in X1] self.x = 0.5 * (2. * self.b + self.c) / (self.a + self.b + self.c) self.y = 0.5 * np.sqrt(3) * self.c / (self.a + self.b + self.c) # create a triangulation out of these points T = tri.Triangulation(self.x, self.y) # labels at corner points if corner_labels is not None: self.ax[self.subplot_idx].text(self.corners[0, 0] - 0.015 * (xmax-xmin), self.corners[0, 1]-0.025 * (ymax-ymin), corner_labels[0], fontsize=self.fontsizes['axlabel'], horizontalalignment='right') self.ax[self.subplot_idx].text(self.corners[1, 0] + 0.015 * (xmax-xmin), self.corners[1, 1]-0.025 * (ymax-ymin), corner_labels[1], fontsize=self.fontsizes['axlabel'], horizontalalignment='left') self.ax[self.subplot_idx].text(self.corners[2, 0], self.corners[2, 1] + (ymax-ymin)*0.03, corner_labels[2], fontsize=self.fontsizes['axlabel'], horizontalalignment='center') return T
[docs] def draw_surf(self, X1, X2, data: np.ndarray, corner_labels: list = None, xlim: list = None, ylim: list = None, is_float: bool = True, nlevels: int = 10, min_val: float = None, max_val: float = None, colours: list = None, colorbar: bool = False, colorbar_labels: list = None, colorbar_title: str = None, contour: bool = False, fill_contour: bool = False): """ Function to draw ternary pcolormesh/contourplot. :param X1, X2: Compositions of first and second components :type X1, X2: list :param data: :class:`np.ndarray` of data for plotting :param corner_labels: Labels of ternary diagram corners :param xlim, ylim: Limits for axes :type xlim, ylim: list :param ax_labels: Axis labels :param is_float: Type of data is float/integer :param nlevels: Number of contour levels for legend :type nlevels: int :param min_val: Minimum value of contour levels :type min_val: float :param max_val: Maximum value of contour levels :type max_val: float :param colours: Colours for ListedColormap :type colours: list[str] :param colorbar: Switch to add colorbar :type colorbar: bool :param colorbar_labels: Labels for colorbar :param colorbar_title: Optional colorbar title :type colorbar_title: str :param contour: Switch for contour or surface plot :type contour: bool :param fill_contour: Switch for filled contour plot :type fill_contour: bool """ # Define levels of contours/patches and cmap + norm levels, ticks = self.get_levels(data, is_float, nlevels, min_val, max_val) cmap, norm = self.get_cmap(levels, colours) # Create triangular T = self.triangulation(X1, X2, corner_labels) # plot the contour, mask the nan or inf data points plot_data = data[self.data_idxs].flatten() point_mask = ~np.isfinite(plot_data) # Points to mask out. tri_mask = np.any(point_mask[T.triangles], axis=1) # Triangles to mask out. T.set_mask(tri_mask) if contour: plot_method = self.ax[self.subplot_idx].tricontourf if fill_contour else self.ax[self.subplot_idx].tricontour self.im = plot_method(T, plot_data, cmap=cmap, norm=norm, vmin=levels[0], vmax=levels[-1], extend="both") # extend='max' else: self.im = self.ax[self.subplot_idx].tripcolor(self.x, self.y, T.triangles, plot_data, mask=tri_mask, shading='flat', cmap=cmap, norm=norm) if colorbar: cax = self.ax[self.subplot_idx].inset_axes([0.85, 0.5, 0.055, 0.3]) cbar = self.fig.colorbar(plt.cm.ScalarMappable(cmap=cmap, norm=norm), ax=self.ax[self.subplot_idx], cax=cax) if colorbar_labels is not None: yticks = np.linspace(*cbar.ax.get_ylim(), cmap.N + 1)[:-1] yticks += (yticks[1] - yticks[0]) / 2 cbar.set_ticks(yticks, labels=colorbar_labels) cbar.ax.tick_params(length=0) cbar.ax.tick_params(labelsize=self.fontsizes['axes']) if colorbar_title is not None: cbar.set_label(colorbar_title, size=self.fontsizes['axlabel']) return
[docs] def draw_compositions(self, compositions: list, axes: list = None, colours: str = None, markerstyle: str = None, linestyle: str = None, connect_compositions: bool = False): """ Function to draw compositions in ternary plot. :param compositions: Compositions of end points [[x0, y0, z0], [x1, y1, z1], ...] :param colours: Marker/line colour, optional :param markerstyle: Point markerstyle, optional :param linestyle: Linestyle, optional :param connect_compositions: Switch to connect compositions with a line """ # Calculate mole fractions compositions = np.array([compositions]) if not hasattr(compositions[0], "__len__") else np.array(compositions) compositions = np.array([comp / np.sum(comp) if np.nansum(comp) else np.ones(3) * np.nan for comp in compositions]) compositions_scaled = (compositions - np.array(self.min_z)) / (np.array(self.max_z) - np.array(self.min_z)) inside_ternary = np.all(compositions_scaled >= 0., axis=1) * np.all(compositions_scaled <= 1., axis=1) # translate the data to cords y = compositions_scaled[:, 2] * np.sqrt(3.) / 2. # sin(pi/3) = sqrt(3)/2 x = (1. - compositions_scaled[:, 0]) - y / np.sqrt(3.) # tan(pi/3) = sqrt(3) for j, xj in enumerate(compositions): axj = axes[j] if axes is not None else 0 self.ax[axj].scatter(x[j], y[j], color=colours, marker=markerstyle) if connect_compositions: # Loop over compositions # Find number of equilibrium phases, max number is nc ph_idxs = [j for j, comp in enumerate(compositions) if not np.isnan(comp[0])] ncomp = len(ph_idxs) # If both within ternary bounds, plot both directly # If one outside, find intersection with edge of (scaled) ternary diagram # If both outside, pass for i, (i1, i2) in enumerate([(ph_idxs[ii], ph_idxs[ii+1]) for ii in range(ncomp-1)] + [(ph_idxs[-1], ph_idxs[0])]): if inside_ternary[i1] and inside_ternary[i2]: for ax in self.ax: ax.plot([x[i1], x[i2]], [y[i1], y[i2]], color=colours, linestyle=linestyle) elif inside_ternary[i1] or inside_ternary[i2]: # Find intersection with edge of the (scaled) ternary diagram (in_idx, out_idx) = (i1, i2) if inside_ternary[i1] else (i2, i1) if not np.isnan(compositions[out_idx, 0]): zero_comp = np.where(compositions_scaled[out_idx, :] < 0.)[0][0] frac = (compositions_scaled[in_idx, zero_comp] / (compositions_scaled[in_idx, zero_comp] - compositions_scaled[out_idx, zero_comp])) x_out, y_out = x[in_idx] + frac * (x[out_idx]-x[in_idx]), y[in_idx] + frac * (y[out_idx]-y[in_idx]) for ax in self.ax: ax.plot([x[in_idx], x_out], [y[in_idx], y_out], color=colours, linestyle=linestyle) else: pass # # Connect last composition with the first # if inside_ternary[0] and inside_ternary[-1]: # self.ax[self.subplot_idx].plot([x[0], x[-1]], [y[0], y[-1]], color=colours, linestyle=linestyle) # elif inside_ternary[0] or inside_ternary[-1]: # # Find intersection with edge of the (scaled) ternary diagram # (in_idx, out_idx) = (0, -1) if inside_ternary[0] else (-1, 0) # if not np.isnan(compositions[out_idx, 0]): # zero_comp = np.where(compositions_scaled[out_idx, :] < 0.)[0][0] # frac = (compositions_scaled[in_idx, zero_comp] / # (compositions_scaled[in_idx, zero_comp] - compositions_scaled[out_idx, zero_comp])) # x_out, y_out = x[in_idx] + frac * (x[out_idx] - x[in_idx]), y[in_idx] + frac * ( # y[out_idx] - y[in_idx]) # self.ax[self.subplot_idx].plot([x[in_idx], x_out], [y[in_idx], y_out], color=colours, # linestyle=linestyle) return
[docs] def draw_contours(self, X1, X2, data: np.ndarray, mask: float = None, xlim: list = None, ylim: list = None, corner_labels: list = None, colours: str = None, linewidth: float = 1.): """ Function to draw contour lines between discrete levels :param X1: Grid points on x-axis :type X1: list :param X2: Grid points on y-axis :type X2: list :param data: :class:`np.ndarray` of data for plotting :param colours: Colours for contourlines :type colours: str :param linewidth: Line width :type linewidth: float """ # Find boundaries between discrete levels contours, levels = self.get_contours(X1, X2, data, mask=mask) # translate the data to coords sqrt3 = np.sqrt(3.) nx, ny = len(X1)+1, len(X2)+1 dx, dy = 1./nx, 1./ny # X1[1]-X1[0], X2[1]-X2[0] # Create triangular X = np.linspace(0., 1., nx-1) Y = np.linspace(0., 1., ny-1) T = self.triangulation(X, Y, corner_labels) x = np.linspace(- dx * 0.5, 1. + dx * 0.5, nx) y = np.linspace(- dy * 0.5, 1. + dy * 0.5, ny) # Plot lines at the boundaries colours = colours if colours is not None else self.colours for ith_contour, (level, lines) in enumerate(contours.items()): colour = colours if isinstance(colours, str) else colours[ith_contour] for line in lines: # Plot line segments and translate them to triangular space xx = np.array([x[line[0][0]], x[line[0][1]]]) yy = np.array([y[line[1][0]], y[line[1][1]]]) yy = (1 - xx - yy) * sqrt3 / 2. # sin(pi/3) = sqrt(3)/2 xx = 1. - xx - yy / sqrt3 # tan(pi/3) = sqrt(3) self.ax[self.subplot_idx].plot(xx, yy, c=colour, linewidth=linewidth) self.set_axes(xlim=[X[0], X[-1]], ylim=[Y[0], Y[-1]]) return
[docs] class QuaternaryDiagram(NCompDiagram): """ This class can construct quaternary diagrams. """
[docs] def __init__(self, dz, min_z: list = None, max_z: list = None, nrows: int = 1, ncols: int = 1, figsize: tuple = (10, 10)): """ The constructor will find the set of physical compositions for nc=4. :param dz: Mesh size of compositions :type dz: float :param min_z: Minimum composition of each component (i = 1,...,nc-1), optional :type min_z: list[float] :param max_z: Maximum composition of each component (i = 1,...,nc-1), optional :type max_z: list[float] :param nrows: Number of rows for subplots :type nrows: int :param ncols: Number of columns for subplots :type ncols: int :param figsize: Size of figure object :type figsize: tuple[float] """ super().__init__(nc=4, dz=dz, min_z=min_z, max_z=max_z, nrows=nrows, ncols=ncols, figsize=figsize) # barycentric coords: (a,b,c) self.a = self.comp_physical[:, 0] self.b = self.comp_physical[:, 1] self.c = self.comp_physical[:, 2] self.d = self.comp_physical[:, 3] self.n_data_points = self.a.shape[0]
[docs] def quaternary(self): return