グラフ作成用の関数

グラフ作成用の関数 #

1. x-y グラフを画像として保存する関数 plot_xy #

def plot_xy
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as tck


# plt.rcParams["axes.prop_cycle"] = plt.cycler(
#    color=["#000000", "#ff0000", "#0000ff", "#228b22", "#ff00ff", "#ffd900", "#00ffff", "#7f00ff", "#ff7f00", "#00ff7f", "#bebebe", "#666666"]
# )


def plot_xy(
    x_dict: dict[str, np.ndarray],
    y_dict: dict[str, np.ndarray],
    y_dict_error: dict[str, np.ndarray] = {},
    output_image_name: str | None = None,  # "out.png"
    show_gui: bool = True,
    data_optional: dict[str, dict] | None = None,
    title_name: str = "x-y graph",
    title_font_size: int | str = "default",
    title_v_offset: float | int | str = "default",
    figure_size: tuple[int, int] = (7, 5),
    image_dpi: int = 400,
    aspect_ratio: str | float = "auto",
    legend_loc: tuple[float, float] | str | None = None,  # ,"upper left",
    legend_transparent: float = 0.5,
    legend_bg_color: str = "default",
    legend_edge_color: str = "default",
    legend_font_size: int | str | None = None,
    x_scale: str = "linear",
    x_base: float | int | str = "auto",
    x_start_tick: float = None,
    x_end_tick: float = None,
    x_range: tuple[float, float] | str = "auto",
    x_major_ticks_manual: tuple | None = None,
    x_label: str = "$x$",
    x_grid: str = "major",
    x_ticks_direction: str = "in",
    is_x_minor_ticks: bool = False,
    x_minor_ticks_manual: tuple | None = None,
    x_minor_division: int = 1,
    x_invert: bool = False,
    y_scale: str = "linear",
    y_base: float | int | str = "auto",
    y_range: tuple[float, float] | str = "auto",
    y_start_tick: float = None,
    y_end_tick: float = None,
    y_major_ticks_manual: tuple | None = None,
    y_label: str = "$y$",
    y_grid: str = "major",
    y_ticks_direction: str = "in",
    is_y_minor_ticks: bool = False,
    y_minor_ticks_manual: tuple | None = None,
    y_minor_division: int = 1,
    y_invert: bool = False,
    default_font_family: str = "Times New Roman",
    default_font_size: int = 10,
    default_font_set: str = "stix",
) -> None:
    plt.rcdefaults()

    plt.rcParams["font.family"] = default_font_family  # font
    plt.rcParams["font.size"] = default_font_size  # character size
    plt.rcParams["mathtext.fontset"] = default_font_set  # font Math

    # ===================
    # 1. Global settings
    # ===================

    # - Make figure and subplots
    fig, axs = plt.subplots(1, 1, figsize=figure_size)

    # - Aspect ratio
    axs.set_aspect(aspect_ratio, adjustable="box")

    # - Plot data and it's label in legend
    # print(data_optional, show_error_bar)
    if data_optional is None:
        for k in y_dict.keys():
            # if show_error_bar:
            if y_dict_error.get(k) is not None:
                axs.errorbar(x_dict[k], y_dict[k], yerr=y_dict_error[k], elinewidth=1, fmt=".-", ms=3, label=k, capsize=2, capthick=1)
            else:
                axs.plot(x_dict[k], y_dict[k], ".-", ms=3, label=k)
    else:
        for k in y_dict.keys():
            p = data_optional.get(k)
            if p is not None:
                fmt = p.get("fmt") if p.get("fmt") is not None else ".-"
                data_label = p.get("ti") if p.get("ti") is not None else k
                line_width = p.get("lw") if p.get("lw") is not None else 1
                line_color = p.get("lc") if p.get("lc") is not None else "C0"
                marker_size = p.get("ms") if p.get("ms") is not None else 3
                e_line_width = p.get("ew") if p.get("ew") is not None else line_width / 2
                cap_size = p.get("cs") if p.get("cs") is not None else 2
                cap_thick = p.get("ct") if p.get("ct") is not None else 1
                if y_dict_error.get(k) is not None:
                    axs.errorbar(
                        x_dict[k],
                        y_dict[k],
                        yerr=y_dict_error[k],
                        fmt=fmt,
                        lw=line_width,
                        ms=marker_size,
                        label=data_label,
                        color=line_color,
                        elinewidth=e_line_width,
                        capsize=cap_size,
                        capthick=cap_thick,
                    )
                else:
                    axs.plot(x_dict[k], y_dict[k], fmt, lw=line_width, ms=marker_size, label=data_label, color=line_color)
            else:
                if y_dict_error.get(k) is not None:
                    axs.errorbar(
                        x_dict[k],
                        y_dict[k],
                        yerr=y_dict_error[k],
                        label=k,
                        elinewidth=1,
                        capsize=2,
                        capthick=1,
                    )
                else:
                    axs.plot(
                        x_dict[k],
                        y_dict[k],
                        label=k,
                    )

    # - Legend
    legend_location = legend_loc if isinstance(legend_loc, (tuple, str)) else plt.rcParams["legend.loc"]
    framealpha = legend_transparent if isinstance(legend_transparent, float) else plt.rcParams["legend.framealpha"]
    legend_facecolor = legend_bg_color if legend_bg_color != "default" else None
    legend_edgecolor = legend_edge_color if legend_edge_color != "default" else None
    axs.legend(
        loc=legend_location,
        framealpha=framealpha,
        fancybox=True,
        facecolor=legend_facecolor,
        edgecolor=legend_edgecolor,
        fontsize=legend_font_size,
    )

    # - Title
    i0 = title_font_size if isinstance(title_font_size, int) else plt.rcParams["font.size"]
    f0 = title_v_offset if isinstance(title_v_offset, (int, float)) else plt.rcParams["axes.titlepad"]
    axs.set_title(title_name, pad=f0, fontsize=i0)

    # ========
    # 2. Axis
    # ========
    axs.grid(False)
    axs.minorticks_off()
    # -----------
    # 2.1 x-axis
    # -----------

    # - x Label
    axs.set_xlabel(x_label)

    # - x Grid
    if x_grid in ["major", "both"]:
        axs.grid(axis="x", which="major", color="#cccccc", linestyle="-")  # show x-axis grid
    if x_grid in ["minor", "both"]:
        axs.grid(axis="x", which="minor", color="#e7e7e7", linestyle="--")  # show x-axis sub-grid

    # - x Ticks
    axs.tick_params(axis="x", which="both", direction=x_ticks_direction)
    if x_scale == "linear":
        axs.set_xscale(x_scale)
        # -- Major ticks location
        if x_major_ticks_manual is None:
            if x_start_tick is None and x_end_tick is None:
                if isinstance(x_base, (float, int)):
                    axs.xaxis.set_major_locator(tck.MultipleLocator(base=float(x_base)))
                else:
                    axs.xaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            else:
                if isinstance(x_base, (float, int)):
                    xstart, xend = axs.get_xlim()
                    if x_start_tick is not None:
                        xstart = x_start_tick if x_range != "auto" else x_range[0]
                    if x_end_tick is not None:
                        xend = x_end_tick if x_range != "auto" else x_range[1]
                    ticks = np.arange(xstart, xend, x_base)
                    axs.set_xticks(ticks)
                else:
                    axs.xaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))

            # -- Major ticks label format
            mj_formatter = tck.ScalarFormatter()
            mj_formatter.set_scientific(True)  # Enable Scientific notation
            mj_formatter.set_powerlimits(plt.rcParams["axes.formatter.limits"])
            mj_formatter.set_useMathText(True)  # (False)1e8, (True)10^8
            axs.xaxis.set_major_formatter(mj_formatter)
        else:
            axs.set_xticks(x_major_ticks_manual[0])
            axs.xaxis.set_major_formatter(tck.FixedFormatter(x_major_ticks_manual[1]))

        # -- Minor ticks location
        if is_x_minor_ticks:
            axs.xaxis.set_minor_formatter(tck.NullFormatter())
            if x_minor_ticks_manual is None:
                axs.xaxis.set_minor_locator(tck.AutoMinorLocator(n=x_minor_division))
            else:
                axs.xaxis.set_minor_locator(tck.FixedLocator(x_minor_ticks_manual))

    elif x_scale == "log":
        axs.set_xscale(x_scale)
        x_log_base = x_base if isinstance(x_base, (float, int)) else 10.0

        if x_major_ticks_manual is None:
            # -- Major ticks location
            # Calc the position at integer power
            a_min_list = []
            a_max_list = []
            for k in x_dict.keys():
                a_min_list.append(np.amin(x_dict[k]))
                a_max_list.append(np.amax(x_dict[k]))
            a_min = np.amin(a_min_list)
            a_max = np.amax(a_max_list)
            if a_min <= 1e-100:
                a_min = 1e-100
            n_power_min = np.floor(np.log(a_min) / np.log(x_log_base)).astype(int)
            n_power_max = np.ceil(np.log(a_max) / np.log(x_log_base)).astype(int)
            ticks = [x_log_base**i for i in range(n_power_min - 1, n_power_max + 2)]
            # Set the ticks location clearly
            axs.xaxis.set_major_locator(tck.FixedLocator(ticks))
            # -- Major ticks label format
            mj_formatter = tck.LogFormatterMathtext(base=x_log_base, labelOnlyBase=True)
            axs.xaxis.set_major_formatter(mj_formatter)
        else:
            axs.xaxis.set_major_locator(tck.FixedLocator(x_major_ticks_manual[0]))
            axs.xaxis.set_major_formatter(tck.FixedFormatter(x_major_ticks_manual[1]))

        # -- Minor ticks location
        if is_x_minor_ticks:
            axs.xaxis.set_minor_formatter(tck.NullFormatter())
            if x_minor_division is None:
                axs.xaxis.set_minor_locator(tck.LogLocator(base=x_log_base, subs=None))
            else:
                axs.xaxis.set_minor_locator(tck.LogLocator(base=x_log_base, subs=np.arange(1.0, x_minor_division, 1) * (1.0 / x_minor_division)))
    else:
        raise ValueError(f'x_scale must be "linear" or "log", your x_scale={x_scale}')

    # - x Range
    if isinstance(x_range, tuple):
        axs.set_xlim(x_range[0], x_range[1])
    else:
        axs.set_xlim(auto=True)

    if x_invert:
        axs.invert_xaxis()

    # -----------
    # 2.2 y-axis
    # -----------

    # - y Label
    axs.set_ylabel(y_label)

    # - y Grid
    if y_grid in ["major", "both"]:
        axs.grid(axis="y", which="major", color="#cccccc", linestyle="-")  # show y-axis grid
    if y_grid in ["minor", "both"]:
        axs.grid(axis="y", which="minor", color="#e7e7e7", linestyle="--")  # show y-axis sub-grid

    # - y Ticks
    axs.tick_params(axis="y", which="both", direction=y_ticks_direction)
    if y_scale == "linear":
        axs.set_yscale(y_scale)

        if y_major_ticks_manual is None:
            if y_start_tick is None and y_end_tick is None:
                if isinstance(y_base, (float, int)):
                    axs.yaxis.set_major_locator(tck.MultipleLocator(base=float(y_base)))
                else:
                    axs.yaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            else:
                if isinstance(y_base, (float, int)):
                    # define ticks by start and interval
                    ystart, yend = axs.get_ylim()
                    if y_start_tick is not None:
                        ystart = y_start_tick if y_range != "auto" else y_range[0]
                    if y_end_tick is not None:
                        yend = y_end_tick if y_range != "auto" else y_range[1]
                    ticks = np.arange(ystart, yend, y_base)
                    axs.set_yticks(ticks)
                else:
                    axs.yaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            # -- Major ticks label format
            mj_formatter = tck.ScalarFormatter()
            mj_formatter.set_scientific(True)  # Enable Scientific notation
            mj_formatter.set_powerlimits(plt.rcParams["axes.formatter.limits"])
            #   (set_powerlimits: Normal notation range, otherwise Scientific notation.)
            mj_formatter.set_useMathText(True)  # (False)1e8, (True)10^8
            axs.yaxis.set_major_formatter(mj_formatter)
        else:
            # axs.set_yticks(y_major_ticks_manual[0])
            axs.yaxis.set_major_locator(tck.FixedLocator(y_major_ticks_manual[0]))
            axs.yaxis.set_major_formatter(tck.FixedFormatter(y_major_ticks_manual[1]))

        # -- Minor ticks location
        if is_y_minor_ticks:
            axs.yaxis.set_minor_formatter(tck.NullFormatter())
            if y_minor_ticks_manual is None:
                axs.yaxis.set_minor_locator(tck.AutoMinorLocator(n=y_minor_division))
            else:
                axs.yaxis.set_minor_locator(tck.FixedLocator(y_minor_ticks_manual))

    elif y_scale == "log":
        axs.set_yscale(y_scale)
        y_log_base = y_base if isinstance(y_base, (float, int)) else 10.0

        if y_major_ticks_manual is None:
            # -- Major ticks location
            # Calc the position at integer power
            a_min_list = []
            a_max_list = []
            for k in y_dict.keys():
                a_min_list.append(np.amin(y_dict[k]))
                a_max_list.append(np.amax(y_dict[k]))
            a_min = np.amin(a_min_list)
            a_max = np.amax(a_max_list)
            if a_min <= 1e-100:
                a_min = 1e-100
            n_power_min = np.floor(np.log(a_min) / np.log(y_log_base)).astype(int)
            n_power_max = np.ceil(np.log(a_max) / np.log(y_log_base)).astype(int)
            ticks = [y_log_base**i for i in range(n_power_min - 1, n_power_max + 2)]
            # Set the ticks location clearly
            axs.yaxis.set_major_locator(tck.FixedLocator(ticks))
            # -- Major ticks label format
            mj_formatter = tck.LogFormatterMathtext(base=y_log_base, labelOnlyBase=True)
            axs.yaxis.set_major_formatter(mj_formatter)
        else:
            axs.set_yticks(y_major_ticks_manual[0])
            axs.yaxis.set_major_formatter(tck.FixedFormatter(y_major_ticks_manual[1]))

        # -- Minor ticks location
        if is_y_minor_ticks:
            axs.yaxis.set_minor_formatter(tck.NullFormatter())
            if y_minor_division is None:
                axs.yaxis.set_minor_locator(tck.LogLocator(base=y_log_base, subs=None))
            else:
                axs.yaxis.set_minor_locator(tck.LogLocator(base=y_log_base, subs=np.arange(1.0, y_minor_division, 1) * (1.0 / y_minor_division)))
    else:
        raise ValueError(f'y_scale must be "linear" or "log", your y_scale={y_scale}')

    # - y Range
    if isinstance(y_range, tuple):
        axs.set_ylim(y_range[0], y_range[1])
    else:
        axs.set_ylim(auto=True)

    if y_invert:
        axs.invert_yaxis()
    # ==========
    # 3. Output
    # ==========
    # - Pop up GUI
    if show_gui:
        plt.show()

    # - output image
    if output_image_name is not None:
        if output_image_name.endswith(".eps"):
            # Don't plot the left-bottom region because the Bounding Box has negative value
            fig.subplots_adjust(left=0.3, right=0.9, bottom=0.3, top=0.9)
        # Save figure
        fig.savefig(output_image_name, dpi=image_dpi, bbox_inches="tight")
    plt.close()

1.1. シンプルなプロットの場合 #

import numpy as np
from plot_xy import plot_xy

if __name__ == "__main__":
    x_arr = np.linspace(0.0, 10.0, 101)
    y_arr = np.cos(x_arr)

    x = {"plot1": x_arr}
    y = {"plot1": y_arr}
    output_name = "out1.png"

    plot_xy(x, y, output_image_name=output_name)

out1.png

1.2. 複数のプロットを表示 #

import numpy as np
from plot_xy import plot_xy

if __name__ == "__main__":
    x1_arr = np.linspace(0.0, 10.0, 101)
    x2_arr = np.linspace(0.0, 20.0, 41)
    y1_arr = np.cos(x1_arr)
    y2_arr = np.sin(x2_arr)

    x = {"plot1": x1_arr, "plot2": x2_arr}
    y = {"plot1": y1_arr, "plot2": y2_arr}
    output_name = "out2.png"

    plot_xy(x, y, output_image_name=output_name)

out2.png

1.3. 線の色などを変更する #

import numpy as np
from plot_xy import plot_xy

if __name__ == "__main__":
    x1_arr = np.linspace(0.0, 10.0, 101)
    x2_arr = np.linspace(0.0, 20.0, 41)
    y1_arr = np.cos(x1_arr)
    y2_arr = np.sin(x2_arr)

    x = {"plot1": x1_arr, "plot2": x2_arr}
    y = {"plot1": y1_arr, "plot2": y2_arr}
    output_name = "out3.png"

    data_optional = {
        "plot1": {"ti": "data $f(x)$", "fmt": "-", "lw": 3, "ms": 4},
        "plot2": {"ti": "data $g(x)$", "fmt": ".-", "lw": 1, "ms": 2},
    }
    plot_xy(x, y, output_image_name=output_name, data_optional=data_optional)

out3.png

import numpy as np
from plot_xy import plot_xy

if __name__ == "__main__":
    x_arr = np.linspace(0.0, 20.0, 101)
    y_arr = 1 / (x_arr**2 + 1)

    x = {"plot1": x_arr}
    y = {"plot1": y_arr}
    output_name = "out4.png"

    data_optional = {"plot1": {"ti": "data $f(x)$", "fmt": ".-", "lw": 2, "ms": 4}}

    plot_xy(
        x,
        y,
        output_image_name=output_name,
        data_optional=data_optional,
        title_name="Graph using all settings",
        show_gui=True,  # True*, False
        title_font_size=12,  # (integer), "default"*
        title_v_offset="default",  # (float), "default"*
        figure_size=(5, 5),  # ((int), (int))
        x_scale="linear",  # "linear"*, "log"
        x_base=2,  # x_scale="linear": interval of major ticks, ="log": base of logarithrm
        x_range=(0.0, 14.0),  # ((range start),(range end)), "auto"*
        x_ticks_manual=([0, 1, 2, 5, 6.5, 11], ["origin", "1", "x=2", "5", "6.5", "$x=11$"]),  # , None*
        x_label="x-axis label",  # (string)
        x_grid="both",  # "minor", "major"*, "both"
        x_ticks_direction="out",  # "in"*, "out"
        is_x_minor_ticks=True,  # True, False*
        x_minor_division=2,  # Num. of divisions of x_base
        y_scale="log",
        y_base=10,
        y_range="auto",
        y_ticks_manual=None,
        y_label="y-axis new label $\log_{10}f(x)$",
        y_grid="both",
        y_ticks_direction="in",
        is_y_minor_ticks=True,
        y_minor_division=10,
        legend_loc=(0.2, 0.55),  # ((0.0-1.0), (0.0-1.0)), "default"*
    )

out4.png

1.4. フォントの変更など #

import numpy as np
from plot_xy import plot_xy

if __name__ == "__main__":
    x1 = np.linspace(-10, 5, 101)
    x2 = np.linspace(-10, 5, 41)
    x3 = np.linspace(-5, 8, 21)
    y1 = 1.0 / (x1**2 + 1)
    y2 = np.sin(x2)
    y3 = np.cos(x3)

    # ------
    x = {"plot1": x1, "plot2": x2, "plot3": x3}
    y = {"plot1": y1, "plot2": y2, "plot3": y3}
    data_optional = {
        "plot1": {"fmt": "--", "ti": "plot 1 title", "lw": 3, "lc": "red"},
        "plot2": {"fmt": "x-", "ms": 4, "lc": "#471bb9"},
        "plot3": {},
    }

    output_image = "out_linear.png"
    x_major_ticks_manual = ([-10, -5, 0, 2, 3], ["$-10$", "$-5$", "origin", "$x=2$", "$3$"])
    x_minor_ticks_manual = np.arange(-14, 8, 1).tolist()
    plot_xy(
        x,
        y,
        output_image_name=output_image,
        show_gui=True,
        data_optional=data_optional,
        title_name=r"linear graph title, LaTeX notation $\frac{d^n}{dx^n}f_{\mathrm{in}}(x_k) \sum_{i=1}^N \int dx c_i(x)$",
        title_font_size=12,
        title_v_offset=20,
        figure_size=(6, 4),
        aspect_ratio=4,
        legend_loc=(0.05, 0.7),
        legend_transparent=0.2,
        legend_bg_color="green",
        legend_edge_color="red",
        x_scale="linear",
        # x_base=10,
        x_range=(-12.0, 7.0),
        # x_start_tick=5,
        # x_end_tick=3,
        x_major_ticks_manual=x_major_ticks_manual,
        x_label="x-axis $x$ [arb. unit]",
        x_grid="both",
        x_ticks_direction="in",
        is_x_minor_ticks=True,
        # x_minor_division=2,
        x_minor_ticks_manual=x_minor_ticks_manual,
        y_scale="linear",
        y_base=0.3,
        y_range=(-1.3, 1.3),
        y_start_tick=-1,
        y_end_tick=0.3,
        # y_major_ticks_manual=y_major_ticks_manual,
        y_label="y-axis $y$ [arb. unit]",
        y_grid="both",
        y_ticks_direction="in",
        is_y_minor_ticks=True,
        y_minor_division=3,
        # y_minor_ticks_manual=y_minor_ticks_manual,
    )

out_linear

1.5. 対数プロット #

import numpy as np
from plot_xy import plot_xy

if __name__ == "__main__":
    x1 = np.logspace(-3, 2.5, 101)
    x2 = np.logspace(-3, 3, 101)
    y1 = 1.0 / (x1 + 1)
    y2 = 0.01 * (1.0 / (x2**3 + 1) + np.exp(0.1 * x2**0.5))

    # ------
    x = {"plot1": x1, "plot2": x2}
    y = {"plot1": y1, "plot2": y2}
    data_optional = {
        "plot1": {"fmt": "x-", "ti": "plot 1 title", "lw": 1, "lc": "red"},
        "plot2": {"fmt": ".-", "ms": 4, "lc": "#471bb9"},
    }

    output_image = "out_log.png"
    x_major_ticks_manual = ([1e-2, 1e-1, 1e0, 1e1, 1e3], ["$10^{-2}$", "$10^{-1}$", "1", "10", "$1000$"])
    plot_xy(
        x,
        y,
        output_image_name=output_image,
        show_gui=True,
        data_optional=data_optional,
        title_name="log-axis graph title $f(x)$",
        title_font_size=12,
        title_v_offset=20,
        figure_size=(6, 4),
        aspect_ratio="auto",
        legend_loc="lower left",  # (0.7, 0.2),
        legend_transparent=0.2,
        legend_bg_color="blue",
        legend_edge_color="black",
        x_scale="log",
        # x_base=10,
        x_range=(5e-4, 2e4),
        # x_start_tick=5,
        # x_end_tick=3,
        x_major_ticks_manual=x_major_ticks_manual,
        x_label="x-axis $x$ [arb. unit]",
        x_grid="both",
        x_ticks_direction="out",
        is_x_minor_ticks=True,
        x_minor_division=10,
        # x_minor_ticks_manual=x_minor_ticks_manual,
        y_scale="log",
        y_base=10,
        y_range=(1e-3, 1),
        y_start_tick=-1,
        y_end_tick=0.3,
        # y_major_ticks_manual=y_major_ticks_manual,
        y_label="y-axis $f(x)$ [arb. unit]",
        y_grid="both",
        y_ticks_direction="out",
        is_y_minor_ticks=True,
        y_minor_division=10,
        # y_minor_ticks_manual=y_minor_ticks_manual,
    )

out_log

2. $f(x,y)$をカラーマップとして出力・保存する関数 plot_color2d #

def plot_fxy
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as tck
import matplotlib.colors as mcolors
import matplotlib.patches as mpatches

plt.rcParams["font.family"] = "Times New Roman"  # font
plt.rcParams["font.size"] = 10  # character size
plt.rcParams["mathtext.fontset"] = "stix"  # font Math

# plt.rcParams["axes.prop_cycle"] = plt.cycler(
#    color=["#000000", "#ff0000", "#0000ff", "#228b22", "#ff00ff", "#ffd900", "#00ffff", "#7f00ff", "#ff7f00", "#00ff7f", "#bebebe", "#666666"]
# )


def plot_color2d(
    x_mesh: np.ndarray,
    y_mesh: np.ndarray,
    z_mesh: np.ndarray,
    output_image_name: str = None,  # "out.png",
    show_gui: bool = True,
    title_name: str = "x-y graph",
    title_font_size: int | str = "default",
    title_v_offset: float | str = "default",
    figure_size: tuple[int, int] = (8, 5),
    aspect_ratio: str | float = "auto",
    legend_label: str | None = None,
    legend_loc: tuple[float, float] | str | None = None,  # "upper left",
    legend_transparent: float = 0.5,
    legend_bg_color: str = "default",
    legend_edge_color: str = "default",
    x_scale: str = "linear",
    x_base: float | int | str = "auto",
    x_start_tick: float = None,
    x_end_tick: float = None,
    x_range: tuple[float, float] | str = "auto",
    x_ticks_manual: tuple | None = None,
    x_label: str = "$x$",
    x_grid: str = "major",
    x_ticks_direction: str = "in",
    is_x_minor_ticks: bool = False,
    x_minor_ticks_manual: tuple | None = None,
    x_minor_division: int | None = None,
    x_invert: bool = False,
    y_scale: str = "linear",
    y_base: float | int | str = "auto",
    y_range: tuple[float, float] | str = "auto",
    y_start_tick: float = None,
    y_end_tick: float = None,
    y_ticks_manual: tuple | None = None,
    y_label: str = "$y$",
    y_grid: str = "major",
    y_ticks_direction: str = "in",
    is_y_minor_ticks: bool = False,
    y_minor_ticks_manual: tuple | None = None,
    y_minor_division: int | None = None,
    y_invert: bool = False,
    color_scale: str = "linear",
    color_range: tuple[float, float] | str = "auto",
    color_map_name: tuple[float, float] | str = "viridis",
    color_transparent: float = 1.0,
    color_bar_label: str = "$z$",
    color_ticks_manual: tuple | None = None,
    default_font_family: str = "Times New Roman",
    default_font_size: int = 10,
    default_font_set: str = "stix",
) -> None:
    plt.rcdefaults()

    plt.rcParams["font.family"] = default_font_family  # font
    plt.rcParams["font.size"] = default_font_size  # character size
    plt.rcParams["mathtext.fontset"] = default_font_set  # font Math

    # ===================
    # 1. Global settings
    # ===================

    # - Make figure and subplots
    fig, axs = plt.subplots(1, 1, figsize=figure_size)

    # - Aspect ratio
    axs.set_aspect(aspect_ratio, adjustable="box")

    # - Color range
    if color_scale == "linear":
        norm = None
        if color_range != "auto":
            norm = mcolors.Normalize(vmin=color_range[0], vmax=color_range[1])
    else:
        # color_scale == "log"
        norm = mcolors.LogNorm()
        if color_range != "auto":
            norm = mcolors.LogNorm(vmin=color_range[0], vmax=color_range[1])

    c = axs.pcolormesh(
        x_mesh,
        y_mesh,
        z_mesh,
        shading="auto",
        norm=norm,
        cmap=color_map_name,
        alpha=color_transparent,
    )

    # - Title
    i0 = (
        title_font_size
        if isinstance(title_font_size, int)
        else plt.rcParams["font.size"]
    )
    f0 = (
        title_v_offset
        if isinstance(title_v_offset, float)
        else plt.rcParams["axes.titlepad"]
    )
    axs.set_title(title_name, pad=f0, fontsize=i0)

    # ========
    # 2. Axis
    # ========
    axs.grid(False)
    axs.minorticks_off()
    # -----------
    # 2.1 x-axis
    # -----------

    # - x Label
    if x_label is not None:
        axs.set_xlabel(x_label)

    # - x Grid
    if x_grid in ["major", "both"]:
        axs.grid(
            axis="x", which="major", color="#cccccc", linestyle="--", linewidth=1
        )  # show x-axis grid
    if x_grid in ["minor", "both"]:
        axs.grid(
            axis="x", which="minor", color="#e7e7e7", linestyle="--", linewidth=0.5
        )  # show x-axis sub-grid

    # - x Ticks
    axs.tick_params(axis="x", which="both", direction=x_ticks_direction)
    if x_scale == "linear":
        axs.set_xscale(x_scale)
        # -- Major ticks location
        if x_ticks_manual is None:
            if x_start_tick is None or x_end_tick is None:
                if isinstance(x_base, (float, int)):
                    axs.xaxis.set_major_locator(tck.MultipleLocator(base=float(x_base)))
                else:
                    axs.xaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            else:
                if isinstance(x_base, (float, int)):
                    # define ticks by start and interval
                    xstart, xend = axs.get_xlim()
                    if x_start_tick is not None:
                        xstart = x_start_tick if x_range != "auto" else x_range[0]
                    if x_end_tick is not None:
                        xend = x_end_tick if x_range != "auto" else x_range[1]
                    ticks = np.arange(xstart, xend, x_base)
                    axs.set_xticks(ticks)
                else:
                    axs.xaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))

            # -- Major ticks label format
            mj_formatter = tck.ScalarFormatter()
            mj_formatter.set_scientific(True)  # Enable Scientific notation
            mj_formatter.set_powerlimits(plt.rcParams["axes.formatter.limits"])
            #   (set_powerlimits: Normal notation range, otherwise Scientific notation.)
            mj_formatter.set_useMathText(True)  # (False)1e8, (True)10^8
            axs.xaxis.set_major_formatter(mj_formatter)
        else:
            axs.set_xticks(x_ticks_manual[0])
            axs.xaxis.set_major_formatter(tck.FixedFormatter(x_ticks_manual[1]))

        # -- Minor ticks location
        if is_x_minor_ticks:
            axs.xaxis.set_minor_formatter(tck.NullFormatter())
            if x_minor_ticks_manual is None:
                axs.xaxis.set_minor_locator(tck.AutoMinorLocator(n=x_minor_division))
            else:
                axs.xaxis.set_minor_locator(tck.FixedLocator(x_minor_ticks_manual))

    elif x_scale == "log":
        axs.set_xscale(x_scale)
        x_log_base = x_base if isinstance(x_base, (float, int)) else 10.0

        if x_ticks_manual is None:
            # -- Major ticks location
            # Calc the position at integer power
            a_min_list = []
            a_max_list = []
            a_min_list.append(np.amin(x_mesh))
            a_max_list.append(np.amax(x_mesh))
            a_min = np.amin(a_min_list)
            a_max = np.amax(a_max_list)
            n_power_min = np.floor(np.log(a_min) / np.log(x_log_base)).astype(int)
            n_power_max = np.ceil(np.log(a_max) / np.log(x_log_base)).astype(int)
            ticks = [x_log_base**i for i in range(n_power_min - 1, n_power_max + 2)]
            # Set the ticks location clearly
            axs.xaxis.set_major_locator(tck.FixedLocator(ticks))
            # -- Major ticks label format
            mj_formatter = tck.LogFormatterMathtext(base=x_log_base, labelOnlyBase=True)
            axs.xaxis.set_major_formatter(mj_formatter)
        else:
            # axs.set_xticks(x_ticks_manual[0])
            axs.xaxis.set_major_locator(tck.FixedLocator(x_ticks_manual[0]))
            axs.xaxis.set_major_formatter(tck.FixedFormatter(x_ticks_manual[1]))

        # -- Minor ticks location
        if is_x_minor_ticks:
            if x_minor_division is None:
                axs.xaxis.set_minor_locator(tck.LogLocator(base=x_log_base, subs=None))
            else:
                axs.xaxis.set_minor_locator(
                    tck.LogLocator(
                        base=x_log_base,
                        subs=np.arange(1.0, x_minor_division, 1)
                        * (1.0 / x_minor_division),
                    )
                )

    else:
        raise ValueError(f'x_scale must be "linear" or "log", your x_scale={x_scale}')

    # - x Range
    if isinstance(x_range, tuple):
        axs.set_xlim(x_range[0], x_range[1])
    else:
        axs.set_xlim(auto=True)

    if x_invert:
        axs.invert_xaxis()

    # -----------
    # 2.2 y-axis
    # -----------

    # - y Label
    if y_label is not None:
        axs.set_ylabel(y_label)

    # - y Grid
    if y_grid in ["major", "both"]:
        axs.grid(
            axis="y", which="major", color="#cccccc", linestyle="--", linewidth="1"
        )  # show y-axis grid
    if y_grid in ["minor", "both"]:
        axs.grid(
            axis="y", which="minor", color="#e7e7e7", linestyle="--", linewidth="0.5"
        )  # show y-axis sub-grid

    # - y Ticks
    axs.tick_params(axis="y", which="both", direction=y_ticks_direction)
    if y_scale == "linear":
        axs.set_yscale(y_scale)

        if y_ticks_manual is None:
            if y_start_tick is None or y_end_tick is None:
                if isinstance(y_base, (float, int)):
                    axs.yaxis.set_major_locator(tck.MultipleLocator(base=float(y_base)))
                else:
                    axs.yaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            else:
                if isinstance(y_base, (float, int)):
                    # define ticks by start and interval
                    ystart, yend = axs.get_ylim()
                    if y_start_tick is not None:
                        ystart = y_start_tick if y_range != "auto" else y_range[0]
                    if y_end_tick is not None:
                        yend = y_end_tick if y_range != "auto" else y_range[1]
                    ticks = np.arange(ystart, yend, y_base)
                    axs.set_yticks(ticks)
                else:
                    axs.yaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            # -- Major ticks label format
            mj_formatter = tck.ScalarFormatter()
            mj_formatter.set_scientific(True)  # Enable Scientific notation
            mj_formatter.set_powerlimits(plt.rcParams["axes.formatter.limits"])
            #   (set_powerlimits: Normal notation range, otherwise Scientific notation.)
            mj_formatter.set_useMathText(True)  # (False)1e8, (True)10^8
            axs.yaxis.set_major_formatter(mj_formatter)
        else:
            axs.yaxis.set_major_locator(tck.FixedLocator(y_ticks_manual[0]))
            axs.yaxis.set_major_formatter(tck.FixedFormatter(y_ticks_manual[1]))

        # -- Minor ticks location
        if is_y_minor_ticks:
            axs.yaxis.set_minor_formatter(tck.NullFormatter())
            if y_minor_ticks_manual is None:
                axs.yaxis.set_minor_locator(tck.AutoMinorLocator(n=y_minor_division))
            else:
                axs.yaxis.set_minor_locator(tck.FixedLocator(y_minor_ticks_manual))

    elif y_scale == "log":
        axs.set_yscale(y_scale)
        y_log_base = y_base if isinstance(y_base, (float, int)) else 10.0

        if y_ticks_manual is None:
            # -- Major ticks location
            # Calc the position at integer power
            a_min_list = []
            a_max_list = []
            a_min_list.append(np.amin(y_mesh))
            a_max_list.append(np.amax(y_mesh))
            a_min = np.amin(a_min_list)
            a_max = np.amax(a_max_list)
            n_power_min = np.floor(np.log(a_min) / np.log(y_log_base)).astype(int)
            n_power_max = np.ceil(np.log(a_max) / np.log(y_log_base)).astype(int)
            ticks = [y_log_base**i for i in range(n_power_min - 1, n_power_max + 2)]
            # Set the ticks location clearly
            axs.yaxis.set_major_locator(tck.FixedLocator(ticks))
            # -- Major ticks label format
            mj_formatter = tck.LogFormatterMathtext(base=y_log_base, labelOnlyBase=True)
            axs.yaxis.set_major_formatter(mj_formatter)
        else:
            axs.set_yticks(y_ticks_manual[0])
            axs.yaxis.set_major_formatter(tck.FixedFormatter(y_ticks_manual[1]))

        # -- Minor ticks location
        if is_y_minor_ticks:
            axs.yaxis.set_minor_formatter(tck.NullFormatter())
            if y_minor_division is None:
                axs.yaxis.set_minor_locator(tck.LogLocator(base=y_log_base, subs=None))
            else:
                axs.yaxis.set_minor_locator(
                    tck.LogLocator(
                        base=y_log_base,
                        subs=np.arange(1.0, y_minor_division, 1)
                        * (1.0 / y_minor_division),
                    )
                )
    else:
        raise ValueError(f'y_scale must be "linear" or "log", your y_scale={y_scale}')

    # - y Range
    if isinstance(y_range, tuple):
        axs.set_ylim(y_range[0], y_range[1])
    else:
        axs.set_ylim(auto=True)

    if y_invert:
        axs.invert_yaxis()

    # ==============
    # 3. Color axis
    # ==============

    # - Color bar
    cbar = fig.colorbar(c, ax=axs, label=color_bar_label)

    if color_ticks_manual is not None:
        cbar.set_ticks(color_ticks_manual[0])
        cbar.set_ticklabels(color_ticks_manual[1])

    # - Legend position
    if legend_label is not None:
        patch = mpatches.Patch(label=legend_label)
        # - transparent legend background
        framealpha = (
            legend_transparent
            if isinstance(legend_transparent, float)
            else plt.rcParams["legend.framealpha"]
        )

        legend_facecolor = legend_bg_color if legend_bg_color != "default" else None
        legend_edgecolor = legend_edge_color if legend_edge_color != "default" else None
        plt.legend(
            handles=[patch],
            loc=legend_loc,
            handlelength=0,
            handletextpad=0,
            framealpha=framealpha,
            fancybox=False,
            facecolor=legend_facecolor,
            edgecolor=legend_edgecolor,
        )

    # ==========
    # 3. Output
    # ==========
    # - Pop up GUI
    if show_gui:
        plt.show()

    # - output image
    if output_image_name is not None:
        if output_image_name.endswith(".eps"):
            # Don't plot the left-bottom region because the Bounding Box has negative value
            fig.subplots_adjust(left=0.3, right=0.9, bottom=0.3, top=0.9)
        # Save figure
        fig.savefig(output_image_name, dpi=400, bbox_inches="tight")

    plt.cla()
    plt.clf()
    plt.close()

2.1. 要素数の注意 #

座標軸の要素数の違いによって、z 軸成分の示す範囲が変わります。 本稿ではmatplotlib.pyplot.pcolormeshのオプションをshading='auto'に固定しています。

そのため、x,y 軸の要素数が異なることによってその点を中心に色を付けるか、囲まれた領域の左下の点を基準に色を示すかが変わります。

pcolormesh_dimension.jpg

詳細は、下記の公式ページをご覧ください。 pcolormesh grids and shading -matplotlib

2.2. 使用例 #

2.2.1. 与えられた点を中心にカラーを表現 #

  • x 軸の要素数 $N$
  • y 軸の要素数 $N$
  • z 軸の要素数 $N$
import numpy as np
from plot_fxy import plot_color2d


if __name__ == "__main__":
    Nx = 6
    Ny = 6
    x_arr = np.linspace(-2.5, 2.5, Nx)
    y_arr = np.linspace(-1.5, 3.5, Ny)
    x, y = np.meshgrid(x_arr, y_arr)
    z = np.exp(-0.4 * (x - 0) ** 2) * np.exp(-1 * (y - 0) ** 2)

    # z = z[:-1, :-1]
    print(f"x | {x_arr}") # x | [-2.5 -1.5 -0.5  0.5  1.5  2.5]
    print(f"y | {y_arr}") # y | [-1.5 -0.5  0.5  1.5  2.5  3.5]
    print(f"x size = {x.shape[0]}, {x.shape[1]}") # x size = 6, 6
    print(f"y size = {y.shape[0]}, {y.shape[1]}") # y size = 6, 6
    print(f"z size = {z.shape[0]}, {z.shape[1]}") # z size = 6, 6

    plot_color2d(
        x,
        y,
        z,
        show_gui=True,
        output_image_name="out_center.png",
        aspect_ratio="equal",
        x_range=(-4.5, 4.5),
        x_grid="both",
        y_range=(-4, 5),
        y_grid="both",
        color_range=(0, 1),
    )

生成される画像

out_center

2.2.2. 囲まれた領域と値を指定して表現 #

  • x 軸の要素数 $N+1$
  • y 軸の要素数 $N+1$
  • z 軸の要素数 $N$
import numpy as np
from plot_fxy import plot_color2d


if __name__ == "__main__":
    Nx = 6
    Ny = 6
    x_arr = np.linspace(-2.5, 2.5, Nx)
    y_arr = np.linspace(-1.5, 3.5, Ny)
    x, y = np.meshgrid(x_arr, y_arr)
    z = np.exp(-0.4 * (x - 0) ** 2) * np.exp(-1 * (y - 0) ** 2)

    z = z[:-1, :-1]
    print(f"x | {x_arr}") # x | [-2.5 -1.5 -0.5  0.5  1.5  2.5]
    print(f"y | {y_arr}") # y | [-1.5 -0.5  0.5  1.5  2.5  3.5]
    print(f"x size = {x.shape[0]}, {x.shape[1]}") # x size = 6, 6
    print(f"y size = {y.shape[0]}, {y.shape[1]}") # y size = 6, 6
    print(f"z size = {z.shape[0]}, {z.shape[1]}") z size = 5, 5

    plot_color2d(
        x,
        y,
        z,
        show_gui=True,
        output_image_name="out_region.png",
        aspect_ratio="equal",
        x_range=(-4.5, 4.5),
        x_grid="both",
        y_range=(-4, 5),
        y_grid="both",
        color_range=(0, 1),
    )

生成される画像

out_region

2.2.3. その他実装 #

import numpy as np
from plot_fxy import plot_color2d

if __name__ == "__main__":
    N = 6
    x_arr = np.linspace(-2.5, 2.5, N)
    y_arr = np.linspace(-2.5, 2.5, N)
    x, y = np.meshgrid(x_arr, y_arr)

    z = np.cos(x - 1) * (1.0 / ((y - 1.5) ** 2 + 1))
    z = z[:-1, :-1]

    output_image = "out_data_center.png"
    x_ticks_manual = ((-3, -1, 0, 1), (-3, -1, "origin", 1))
    color_ticks_manual = ([1e-10, 1e-9, 1e-5, 1], ["1e-10", "$10^{-9}$", "$10^{-5}$", "1"])
    plot_color2d(
        x,
        y,
        z,
        legend_label=None,
        output_image_name=output_image,
        figure_size=(8, 5),
        aspect_ratio="equal",
        show_gui=True,
        x_base=1,
        x_range=(-3.5, 3.5),
        x_start_tick=-3,
        # x_end_tick=2,
        x_ticks_manual=x_ticks_manual,
        x_scale="linear",
        is_x_minor_ticks=False,
        x_minor_division=2,
        x_grid="both",
        y_base=1,
        y_range=(-3.5, 3.5),
        y_start_tick=-3,
        y_scale="linear",
        y_grid="both",
        is_y_minor_ticks=False,
        y_minor_division=2,
        # color_map_name="jet",
        color_scale="linear",
        color_range=(-1, 1),
        # color_ticks_manual=color_ticks_manual,
    )

生成される画像

out_data_center

3. $f(x,y)$を 3 次元として出力・保存する関数 plot_3d #

def plot_3d
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib.ticker as tck


plt.rcParams["font.family"] = "Times New Roman"  # font
plt.rcParams["font.size"] = 10  # character size
plt.rcParams["mathtext.fontset"] = "stix"  # font Math


def plot_3d(
    x_mesh_dict: dict[str, np.ndarray],
    y_mesh_dict: dict[str, np.ndarray],
    z_mesh_dict: dict[str, np.ndarray],
    data_optional: dict[str, dict] = None,
    color_mesh_dict: dict[str, np.ndarray] | None = None,
    output_image_name: str | None = None,  # "out.png"
    output_image_dpi: int = 200,
    show_gui: bool = True,
    surface_optional: dict[str, dict] | None = None,
    title_name: str = "xyz graph",
    title_font_size: int | str = "default",
    title_v_offset: float | str = "default",
    figure_size: tuple[int, int] = (8, 5),
    xyz_aspect_ratio: tuple[float, float, float] = None,
    legend_loc: tuple[float, float] | str | None = None,  # ,"upper left",
    legend_transparent: float = 0.5,
    legend_bg_color: str = "default",
    legend_edge_color: str = "default",
    x_scale: str = "linear",
    x_base: float | int | str = "auto",
    x_start_tick: float = None,
    x_end_tick: float = None,
    x_range: tuple[float, float] | str = "auto",
    x_major_ticks_manual: tuple | None = None,
    x_label: str = "$x$",
    x_grid: str = "major",
    x_ticks_direction: str = "in",
    is_x_minor_ticks: bool = False,
    x_minor_ticks_manual: tuple | None = None,
    x_minor_division: int = 1,
    x_invert: bool = False,
    y_scale: str = "linear",
    y_base: float | int | str = "auto",
    y_range: tuple[float, float] | str = "auto",
    y_start_tick: float = None,
    y_end_tick: float = None,
    y_major_ticks_manual: tuple | None = None,
    y_label: str = "$y$",
    y_grid: str = "major",
    y_ticks_direction: str = "in",
    is_y_minor_ticks: bool = False,
    y_minor_ticks_manual: tuple | None = None,
    y_minor_division: int = 1,
    y_invert: bool = False,
    z_scale: str = "linear",
    z_base: float | int | str = "auto",
    z_range: tuple[float, float] | str = "auto",
    z_start_tick: float = None,
    z_end_tick: float = None,
    z_major_ticks_manual: tuple | None = None,
    z_label: str = "$z$",
    z_grid: str = "major",
    z_ticks_direction: str = "in",
    is_z_minor_ticks: bool = False,
    z_minor_ticks_manual: tuple | None = None,
    z_minor_division: int = 1,
    z_invert: bool = False,
    color_range: tuple[float, float] | str = "auto",
    color_backpanel: str | tuple[str, float] = "white",
    is_bottom_projection: bool = False,
    bottom_projection_key: str | None = None,
    bottom_colormap: str = "auto",  # "auto" refer the color_surface_map_name
    view_projection: str = "ortho",  # "ortho", "persp"
    view_focal_length: float = 1,  # works only if "persp", \infty correspond to "ortho"
    view_elev: float = 30,
    view_azim: float = -60,
    view_roll: float = 1,
    default_font_family: str = "Times New Roman",
    default_font_size: int = 10,
    default_font_set: str = "stix",
) -> None:
    plt.rcdefaults()

    plt.rcParams["font.family"] = default_font_family  # font
    plt.rcParams["font.size"] = default_font_size  # character size
    plt.rcParams["mathtext.fontset"] = default_font_set  # font Math

    # ===================
    # 1. Global settings
    # ===================

    # - Make figure and subplots
    fig, axs = plt.subplots(figsize=figure_size, subplot_kw={"projection": "3d"})

    # - Aspect ratio
    if xyz_aspect_ratio is not None:
        axs.set_box_aspect(xyz_aspect_ratio)

    if color_mesh_dict is None:
        color_mesh_dict = {}

    # Normalize color_value 0 to 1
    for k in z_mesh_dict.keys():
        if k not in color_mesh_dict:
            color_mesh_dict[k] = z_mesh_dict[k].copy()

    if color_range == "auto":
        color_min_list = []
        color_max_list = []
        for k, v in color_mesh_dict.items():
            color_min_list.append(v.min())
            color_max_list.append(v.max())
        color_min = np.amin(color_min_list)
        color_max = np.amax(color_max_list)
    else:
        color_min, color_max = color_range

    print(color_min, color_max)

    x_mesh_skip_dict = {}
    y_mesh_skip_dict = {}
    color_surface_map_name = {}
    color_surface_transparent = {}
    is_color_bar = {}
    for k in z_mesh_dict.keys():
        # ================
        x_mesh_skip_dict[k] = 1
        y_mesh_skip_dict[k] = 1
        color_surface_map_name[k] = "viridis"
        color_surface_transparent[k] = 1.0

        if len(z_mesh_dict) == 1:
            is_color_bar[k] = True
        else:
            is_color_bar[k] = False

        if surface_optional is not None:
            p = surface_optional.get(k)
            if p is not None:
                x_mesh_skip_dict[k] = p.get("xskip") if p.get("xskip") is not None else 1
                y_mesh_skip_dict[k] = p.get("yskip") if p.get("yskip") is not None else 1
                color_surface_map_name[k] = p.get("map") if p.get("map") is not None else "viridis"
                color_surface_transparent[k] = p.get("alpha") if p.get("alpha") is not None else 1.0
                is_color_bar[k] = p.get("cbar") if p.get("cbar") is not None else False
        # ================

    # --- 2. color_mesh の値に従って色付け ---
    # カラーマップにより RGBA の facecolors 配列を作成
    facecolors = {}
    color_norm = {}
    for k, v in color_mesh_dict.items():

        # # ================
        # p = surface_optional.get(k)
        # if p is not None:
        #     color_surface_map_name = p.get("map") if p.get("map") is not None else "viridis"
        #     color_surface_transparent = p.get("alpha") if p.get("alpha") is not None else 1.0
        # else:
        #     color_surface_map_name = "viridis"
        #     color_surface_transparent = 1.0
        # # ================

        # if isinstance(color_surface_map_name, str) or (k not in color_surface_map_name):
        #    cmap = plt.get_cmap(color_surface_map_name)
        # else:
        cmap = plt.get_cmap(color_surface_map_name[k])
        color_norm[k] = plt.Normalize(vmin=color_min, vmax=color_max, clip=True)  # Normalize

        facecolors[k] = cmap(color_norm[k](v))
        # 全面に対して固定の透過率(ここでは不透明=1)を適用
        # if isinstance(color_surface_transparent, float) or (k not in color_surface_transparent):
        #    facecolors[k][..., 3] = color_surface_transparent
        # else:
        facecolors[k][..., 3] = color_surface_transparent[k]

    # -- x,y軸の範囲制限(3dではデータは枠を大きく通り越して描画される)
    if x_range == "auto":
        a_min_list = []
        a_max_list = []
        for k, v in x_mesh_dict.items():
            a_min_list.append(v[0, :].min())
            a_max_list.append(v[0, :].max())
        a_min = np.amin(a_min_list)
        a_max = np.amax(a_max_list)
        # x_range = (x_mesh[0, :].min(), x_mesh[0, :].max())
        x_range = (a_min, a_max)
    if y_range == "auto":
        a_min_list = []
        a_max_list = []
        for k, v in y_mesh_dict.items():
            a_min_list.append(v[:, 0].min())
            a_max_list.append(v[:, 0].max())
        a_min = np.amin(a_min_list)
        a_max = np.amax(a_max_list)
        # y_range = (y_mesh[:, 0].min(), y_mesh[:, 0].max())
        y_range = (a_min, a_max)

    for k in z_mesh_dict.keys():
        # x 軸の範囲 2〜6 に対応する列のインデックス
        ix = np.where((x_mesh_dict[k][0, :] >= x_range[0]) & (x_mesh_dict[k][0, :] <= x_range[1]))[0]
        # y 軸の範囲 5〜15 に対応する行のインデックス(例として)
        iy = np.where((y_mesh_dict[k][:, 0] >= y_range[0]) & (y_mesh_dict[k][:, 0] <= y_range[1]))[0]
        # 両軸の範囲に絞ったグリッドの作成
        x_mesh_dict[k] = x_mesh_dict[k][np.ix_(iy, ix)]
        y_mesh_dict[k] = y_mesh_dict[k][np.ix_(iy, ix)]
        z_mesh_dict[k] = z_mesh_dict[k][np.ix_(iy, ix)]

    # z_rangeが"auto"の場合の設定
    if z_range == "auto":
        a_min_list = []
        a_max_list = []
        for k, v in z_mesh_dict.items():
            a_min_list.append(v.min())
            a_max_list.append(v.max())
        a_min = np.amin(a_min_list)
        a_max = np.amax(a_max_list)
        # z_range = (z_mesh.min(), z_mesh.max())
        z_range = (a_min, a_max)

    # z_rangeに基づくマスク処理(z_range外の値をマスク)
    for k in z_mesh_dict.keys():
        z_mesh_dict[k] = np.ma.masked_outside(z_mesh_dict[k], z_range[0], z_range[1])

    cbar_shrink = 0.5
    cbar_aspect = 10
    cbar_pad = 0.1
    d_lw = 0.25
    d_alpha = 1.0
    # Surface Plot
    if data_optional is None:
        for j, k in enumerate(z_mesh_dict.keys()):
            c = axs.plot_surface(
                x_mesh_dict[k],
                y_mesh_dict[k],
                z_mesh_dict[k],
                label=k,
                linewidth=d_lw,
                facecolors=facecolors[k],
                edgecolor=(f"C{j}", d_alpha),
                rstride=x_mesh_skip_dict[k],
                cstride=y_mesh_skip_dict[k],
            )
            if is_color_bar[k]:
                print(k)
                mappable = cm.ScalarMappable(norm=color_norm[k], cmap=color_surface_map_name[k])
                mappable.set_array(z_mesh_dict[k])
                cbar = fig.colorbar(mappable, ax=axs, shrink=cbar_shrink, aspect=cbar_aspect, pad=cbar_pad)
                cbar.set_label(k)

    else:
        for j, k in enumerate(z_mesh_dict.keys()):
            p = data_optional.get(k)
            if p is not None:
                fmt_tmp = p.get("fmt") if p.get("fmt") is not None else "-"
                data_label = p.get("ti") if p.get("ti") is not None else k
                line_width = p.get("lw") if p.get("lw") is not None else d_lw
                line_color = p.get("lc") if p.get("lc") is not None else f"C{j}"
                line_alpha = p.get("la") if p.get("la") is not None else d_alpha
                marker_type = p.get("mt") if p.get("mt") is not None else None  # "o"  # ('o', '^', 's', 'x'など)
                marker_size = p.get("ms") if p.get("ms") is not None else 3
                marker_widths = p.get("mw") if p.get("mw") is not None else 0.1
                marker_alpha = p.get("ma") if p.get("ma") is not None else 1.0

                if fmt_tmp == ".-":
                    fmt = "-"
                else:
                    fmt = fmt_tmp

                c = axs.plot_surface(
                    x_mesh_dict[k],
                    y_mesh_dict[k],
                    z_mesh_dict[k],
                    linestyle=fmt,
                    linewidth=line_width,
                    label=data_label,
                    facecolors=facecolors[k],
                    edgecolor=(line_color, line_alpha),
                    rstride=x_mesh_skip_dict[k],
                    cstride=y_mesh_skip_dict[k],
                )
                if is_color_bar[k]:
                    mappable = cm.ScalarMappable(norm=color_norm[k], cmap=color_surface_map_name[k])
                    mappable.set_array(z_mesh_dict[k])
                    cbar = fig.colorbar(mappable, ax=axs, shrink=cbar_shrink, aspect=cbar_aspect, pad=cbar_pad)
                    cbar.set_label(data_label)

                if fmt_tmp == ".-" or marker_type is not None:
                    c = axs.scatter(
                        x_mesh_dict[k],
                        y_mesh_dict[k],
                        z_mesh_dict[k],
                        color=line_color,
                        s=marker_size,
                        linewidths=marker_widths,
                        marker=marker_type,
                        alpha=marker_alpha,
                    )  # s が点サイズ
            else:
                c = axs.plot_surface(
                    x_mesh_dict[k],
                    y_mesh_dict[k],
                    z_mesh_dict[k],
                    label=k,
                    linewidth=d_lw,
                    facecolors=facecolors[k],
                    edgecolor=(f"C{j}", d_alpha),
                    rstride=x_mesh_skip_dict[k],
                    cstride=y_mesh_skip_dict[k],
                )
                if is_color_bar[k]:
                    mappable = cm.ScalarMappable(norm=color_norm[k], cmap=color_surface_map_name[k])
                    mappable.set_array(z_mesh_dict[k])
                    cbar = fig.colorbar(mappable, ax=axs, shrink=cbar_shrink, aspect=cbar_aspect, pad=cbar_pad)
                    cbar.set_label(k)

    # - Legend
    legend_location = legend_loc if isinstance(legend_loc, (tuple, str)) else plt.rcParams["legend.loc"]
    framealpha = legend_transparent if isinstance(legend_transparent, float) else plt.rcParams["legend.framealpha"]
    legend_facecolor = legend_bg_color if legend_bg_color != "default" else None
    legend_edgecolor = legend_edge_color if legend_edge_color != "default" else None
    axs.legend(
        loc=legend_location,
        framealpha=framealpha,
        fancybox=True,
        facecolor=legend_facecolor,
        edgecolor=legend_edgecolor,
    )

    # - Title
    i0 = title_font_size if isinstance(title_font_size, int) else plt.rcParams["font.size"]
    f0 = title_v_offset if isinstance(title_v_offset, float) else plt.rcParams["axes.titlepad"]
    axs.set_title(title_name, pad=f0, fontsize=i0)

    # ========
    # 2. Axis
    # ========
    axs.grid(False)
    axs.minorticks_off()
    # -----------
    # 2.1 x-axis
    # -----------

    # - x Label
    axs.set_xlabel(x_label)

    # - x Grid
    if x_grid in ["major", "both"]:
        axs.grid(axis="x", which="major", color="#cccccc", linestyle="-")  # show x-axis grid
    if x_grid in ["minor", "both"]:
        axs.grid(axis="x", which="minor", color="#e7e7e7", linestyle="--")  # show x-axis sub-grid

    # - x Ticks
    axs.tick_params(axis="x", which="both", direction=x_ticks_direction)
    if x_scale == "linear":
        axs.set_xscale(x_scale)
        # -- Major ticks location
        if x_major_ticks_manual is None:
            if x_start_tick is None and x_end_tick is None:
                if isinstance(x_base, (float, int)):
                    axs.xaxis.set_major_locator(tck.MultipleLocator(base=float(x_base)))
                else:
                    axs.xaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            else:
                if isinstance(x_base, (float, int)):
                    xstart, xend = axs.get_xlim()
                    if x_start_tick is not None:
                        xstart = x_start_tick if x_range != "auto" else x_range[0]
                    if x_end_tick is not None:
                        xend = x_end_tick if x_range != "auto" else x_range[1]
                    ticks = np.arange(xstart, xend, x_base)
                    axs.set_xticks(ticks)
                else:
                    axs.xaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))

            # -- Major ticks label format
            mj_formatter = tck.ScalarFormatter()
            mj_formatter.set_scientific(True)  # Enable Scientific notation
            mj_formatter.set_powerlimits(plt.rcParams["axes.formatter.limits"])
            mj_formatter.set_useMathText(True)  # (False)1e8, (True)10^8
            axs.xaxis.set_major_formatter(mj_formatter)
        else:
            axs.set_xticks(x_major_ticks_manual[0])
            axs.xaxis.set_major_formatter(tck.FixedFormatter(x_major_ticks_manual[1]))

        # -- Minor ticks location
        if is_x_minor_ticks:
            axs.xaxis.set_minor_formatter(tck.NullFormatter())
            if x_minor_ticks_manual is None:
                axs.xaxis.set_minor_locator(tck.AutoMinorLocator(n=x_minor_division))
            else:
                axs.xaxis.set_minor_locator(tck.FixedLocator(x_minor_ticks_manual))

    # elif x_scale == "log":
    #     axs.set_xscale(x_scale)
    #     x_log_base = x_base if isinstance(x_base, (float, int)) else 10.0
    #
    #     if x_major_ticks_manual is None:
    #         # -- Major ticks location
    #         # Calc the position at integer power
    #         a_min_list = []
    #         a_max_list = []
    #         # for k in x_dict.keys():
    #         #    a_min_list.append(np.amin(x_dict[k]))
    #         #    a_max_list.append(np.amax(x_dict[k]))
    #         a_min_list.append(np.amin(x_mesh))
    #         a_max_list.append(np.amax(x_mesh))
    #         a_min = np.amin(a_min_list)
    #         a_max = np.amax(a_max_list)
    #         if a_min <= 1e-100:
    #             a_min = 1e-100
    #         n_power_min = np.floor(np.log(a_min) / np.log(x_log_base)).astype(int)
    #         n_power_max = np.ceil(np.log(a_max) / np.log(x_log_base)).astype(int)
    #         ticks = [x_log_base**i for i in range(n_power_min - 1, n_power_max + 2)]
    #         # Set the ticks location clearly
    #         axs.xaxis.set_major_locator(tck.FixedLocator(ticks))
    #         # -- Major ticks label format
    #         mj_formatter = tck.LogFormatterMathtext(base=x_log_base, labelOnlyBase=True)
    #         axs.xaxis.set_major_formatter(mj_formatter)
    #     else:
    #         axs.xaxis.set_major_locator(tck.FixedLocator(x_major_ticks_manual[0]))
    #         axs.xaxis.set_major_formatter(tck.FixedFormatter(x_major_ticks_manual[1]))
    #
    #     # -- Minor ticks location
    #     if is_x_minor_ticks:
    #         axs.xaxis.set_minor_formatter(tck.NullFormatter())
    #         if x_minor_division is None:
    #             axs.xaxis.set_minor_locator(tck.LogLocator(base=x_log_base, subs=None))
    #         else:
    #             axs.xaxis.set_minor_locator(tck.LogLocator(base=x_log_base, subs=np.arange(1.0, x_minor_division, 1) * (1.0 / x_minor_division)))
    else:
        raise ValueError(f'x_scale must be "linear", your x_scale={x_scale}')

    # - x Range
    if isinstance(x_range, tuple):
        axs.set_xlim(x_range[0], x_range[1])
    else:
        axs.set_xlim(auto=True)

    if x_invert:
        axs.invert_xaxis()

    # -----------
    # 2.2 y-axis
    # -----------

    # - y Label
    axs.set_ylabel(y_label)

    # - y Grid
    if y_grid in ["major", "both"]:
        axs.grid(axis="y", which="major", color="#cccccc", linestyle="-")  # show y-axis grid
    if y_grid in ["minor", "both"]:
        axs.grid(axis="y", which="minor", color="#e7e7e7", linestyle="--")  # show y-axis sub-grid

    # - y Ticks
    axs.tick_params(axis="y", which="both", direction=y_ticks_direction)
    if y_scale == "linear":
        axs.set_yscale(y_scale)

        if y_major_ticks_manual is None:
            if y_start_tick is None and y_end_tick is None:
                if isinstance(y_base, (float, int)):
                    axs.yaxis.set_major_locator(tck.MultipleLocator(base=float(y_base)))
                else:
                    axs.yaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            else:
                if isinstance(y_base, (float, int)):
                    # define ticks by start and interval
                    ystart, yend = axs.get_ylim()
                    if y_start_tick is not None:
                        ystart = y_start_tick if y_range != "auto" else y_range[0]
                    if y_end_tick is not None:
                        yend = y_end_tick if y_range != "auto" else y_range[1]
                    ticks = np.arange(ystart, yend, y_base)
                    axs.set_yticks(ticks)
                else:
                    axs.yaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            # -- Major ticks label format
            mj_formatter = tck.ScalarFormatter()
            mj_formatter.set_scientific(True)  # Enable Scientific notation
            mj_formatter.set_powerlimits(plt.rcParams["axes.formatter.limits"])
            #   (set_powerlimits: Normal notation range, otherwise Scientific notation.)
            mj_formatter.set_useMathText(True)  # (False)1e8, (True)10^8
            axs.yaxis.set_major_formatter(mj_formatter)
        else:
            # axs.set_yticks(y_major_ticks_manual[0])
            axs.yaxis.set_major_locator(tck.FixedLocator(y_major_ticks_manual[0]))
            axs.yaxis.set_major_formatter(tck.FixedFormatter(y_major_ticks_manual[1]))

        # -- Minor ticks location
        if is_y_minor_ticks:
            axs.yaxis.set_minor_formatter(tck.NullFormatter())
            if y_minor_ticks_manual is None:
                axs.yaxis.set_minor_locator(tck.AutoMinorLocator(n=y_minor_division))
            else:
                axs.yaxis.set_minor_locator(tck.FixedLocator(y_minor_ticks_manual))

    # elif y_scale == "log":
    #    axs.set_yscale(y_scale)
    #    y_log_base = y_base if isinstance(y_base, (float, int)) else 10.0
    #
    #    if y_major_ticks_manual is None:
    #        # -- Major ticks location
    #        # Calc the position at integer power
    #        a_min_list = []
    #        a_max_list = []
    #        # for k in y_dict.keys():
    #        #     a_min_list.append(np.amin(y_dict[k]))
    #        #     a_max_list.append(np.amax(y_dict[k]))
    #        a_min_list.append(np.amin(y_mesh))
    #        a_max_list.append(np.amax(y_mesh))
    #        a_min = np.amin(a_min_list)
    #        a_max = np.amax(a_max_list)
    #        if a_min <= 1e-100:
    #            a_min = 1e-100
    #        n_power_min = np.floor(np.log(a_min) / np.log(y_log_base)).astype(int)
    #        n_power_max = np.ceil(np.log(a_max) / np.log(y_log_base)).astype(int)
    #        ticks = [y_log_base**i for i in range(n_power_min - 1, n_power_max + 2)]
    #        # Set the ticks location clearly
    #        axs.yaxis.set_major_locator(tck.FixedLocator(ticks))
    #        # -- Major ticks label format
    #        mj_formatter = tck.LogFormatterMathtext(base=y_log_base, labelOnlyBase=True)
    #        axs.yaxis.set_major_formatter(mj_formatter)
    #    else:
    #        axs.set_yticks(y_major_ticks_manual[0])
    #        axs.yaxis.set_major_formatter(tck.FixedFormatter(y_major_ticks_manual[1]))
    #
    #    # -- Minor ticks location
    #    if is_y_minor_ticks:
    #        axs.yaxis.set_minor_formatter(tck.NullFormatter())
    #        if y_minor_division is None:
    #            axs.yaxis.set_minor_locator(tck.LogLocator(base=y_log_base, subs=None))
    #        else:
    #            axs.yaxis.set_minor_locator(tck.LogLocator(base=y_log_base, subs=np.arange(1.0, y_minor_division, 1) * (1.0 / y_minor_division)))
    else:
        raise ValueError(f'y_scale must be "linear", your y_scale={y_scale}')

    # - y Range
    if isinstance(y_range, tuple):
        axs.set_ylim(y_range[0], y_range[1])
    else:
        axs.set_ylim(auto=True)

    if y_invert:
        axs.invert_yaxis()

    # -----------
    # 2.3 z-axis
    # -----------

    # - z Label
    axs.set_zlabel(z_label)

    # - z Grid
    if z_grid in ["major", "both"]:
        axs.grid(axis="z", which="major", color="#cccccc", linestyle="-")  # show z-axis grid
    if z_grid in ["minor", "both"]:
        axs.grid(axis="z", which="minor", color="#e7e7e7", linestyle="--")  # show z-axis sub-grid

    # - z Ticks
    axs.tick_params(axis="z", which="both", direction=z_ticks_direction)
    if z_scale == "linear":
        axs.set_zscale(z_scale)

        if z_major_ticks_manual is None:
            if z_start_tick is None and z_end_tick is None:
                if isinstance(z_base, (float, int)):
                    axs.zaxis.set_major_locator(tck.MultipleLocator(base=float(z_base)))
                else:
                    axs.zaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            else:
                if isinstance(z_base, (float, int)):
                    # define ticks by start and interval
                    zstart, zend = axs.get_zlim()
                    if z_start_tick is not None:
                        zstart = z_start_tick if z_range != "auto" else z_range[0]
                    if z_end_tick is not None:
                        zend = z_end_tick if z_range != "auto" else z_range[1]
                    ticks = np.arange(zstart, zend, z_base)
                    axs.set_zticks(ticks)
                else:
                    axs.zaxis.set_major_locator(tck.MaxNLocator(nbins="auto"))
            # -- Major ticks label format
            mj_formatter = tck.ScalarFormatter()
            mj_formatter.set_scientific(True)  # Enable Scientific notation
            mj_formatter.set_powerlimits(plt.rcParams["axes.formatter.limits"])
            #   (set_powerlimits: Normal notation range, otherwise Scientific notation.)
            mj_formatter.set_useMathText(True)  # (False)1e8, (True)10^8
            axs.zaxis.set_major_formatter(mj_formatter)
        else:
            # axs.set_zticks(z_major_ticks_manual[0])
            axs.zaxis.set_major_locator(tck.FixedLocator(z_major_ticks_manual[0]))
            axs.zaxis.set_major_formatter(tck.FixedFormatter(z_major_ticks_manual[1]))

        # -- Minor ticks location
        if is_z_minor_ticks:
            axs.zaxis.set_minor_formatter(tck.NullFormatter())
            if z_minor_ticks_manual is None:
                axs.zaxis.set_minor_locator(tck.AutoMinorLocator(n=z_minor_division))
            else:
                axs.zaxis.set_minor_locator(tck.FixedLocator(z_minor_ticks_manual))

    else:
        raise ValueError(f'z_scale must be "linear", your z_scale={z_scale}')

    # - z Range
    if isinstance(z_range, tuple):
        axs.set_zlim(z_range[0], z_range[1])
    else:
        axs.set_zlim(auto=True)

    if z_invert:
        axs.invert_zaxis()

    # - 3d plot panel
    axs.xaxis.pane.set_facecolor(color_backpanel)
    axs.yaxis.pane.set_facecolor(color_backpanel)
    axs.zaxis.pane.set_facecolor(color_backpanel)

    # Projection to panel
    if is_bottom_projection:
        if (bottom_projection_key is None) or (bottom_projection_key not in z_mesh_dict):
            print(f"***** {bottom_projection_key} is not exist in z_mesh_dict")
            bottom_projection_key = next(iter(z_mesh_dict))

        if bottom_colormap == "auto":
            bottom_colormap = color_surface_map_name[bottom_projection_key]

        axs.contourf(
            x_mesh_dict[bottom_projection_key],
            y_mesh_dict[bottom_projection_key],
            z_mesh_dict[bottom_projection_key],
            zdir="z",
            offset=z_range[0] - 1e-15,
            cmap=bottom_colormap,
            vmin=color_range[0],
            vmax=color_range[1],
        )

    # - View
    # -- Projection type
    if view_projection == "ortho":
        view_focal_length = None
    axs.set_proj_type(view_projection, view_focal_length)

    # -- Init view
    axs.view_init(elev=view_elev, azim=view_azim, roll=view_roll)

    # ==========
    # 3. Output
    # ==========
    # - Pop up GUI
    if show_gui:
        plt.show()

    # - output image
    if output_image_name is not None:
        if output_image_name.endswith(".eps"):
            # Don't plot the left-bottom region because the Bounding Box has negative value
            fig.subplots_adjust(left=0.3, right=0.9, bottom=0.3, top=0.9)
        # Save figure
        fig.savefig(output_image_name, dpi=output_image_dpi, bbox_inches="tight")
    return

3.1. シンプルなプロット例 #

import numpy as np
from plot_3d import plot_3d

if __name__ == "__main__":
    Nx = 51
    Ny = 51
    x_arr = np.linspace(-3.0, 3.0, Nx)
    y_arr = np.linspace(-5.5, 3.5, Ny)
    x, y = np.meshgrid(x_arr, y_arr)
    z = np.exp(-0.4 * x**2) * np.exp(-(y**2))

    x_dict = {"plot1": x}
    y_dict = {"plot1": y}
    z_dict = {"plot1": z}
    plot_3d(
        x_dict,
        y_dict,
        z_dict,
    )

3.2. 複数のグラフ、カラーなし、点または線 #

import numpy as np
from plot_3d import plot_3d

if __name__ == "__main__":
    Nx = 51
    Ny = 51
    x_arr = np.linspace(-3.0, 3.0, Nx)
    y_arr = np.linspace(-5.5, 3.5, Ny)
    x, y = np.meshgrid(x_arr, y_arr)
    z1 = np.exp(-0.4 * x**2) * np.exp(-(y**2))
    z2 = z1 + 1.0

    x_dict = {"plot1": x, "plot2": x}
    y_dict = {"plot1": y, "plot2": y}
    z_dict = {"plot1": z1, "plot2": z2}

    data_optional = {}
    data_optional["plot1"] = {"ti": "z1", "fmt": "-", "mt": "o", "ms": 2, "mw": 0.2, "lw": 0.1, "lc": "C0", "la": 0.5}
    data_optional["plot2"] = {"ti": "z2", "fmt": "-", "lw": 0.5, "lc": "C3", "la": 0.5}
    surface_optional = {}
    surface_optional["plot1"] = {"map": "viridis", "alpha": 0.0, "xskip": 1, "yskip": 1, "cbar": False}
    surface_optional["plot2"] = {"map": "jet", "alpha": 0.0, "xskip": 1, "yskip": 1, "cbar": False}

    output_image_name = "plot3d_2.png"

    plot_3d(
        x_dict,
        y_dict,
        z_dict,
        output_image_name=output_image_name,
        data_optional=data_optional,
        surface_optional=surface_optional,
    )

3.3. 範囲の制限、透視投影、アスペクト比の変更など #

import numpy as np
from plot_3d import plot_3d

if __name__ == "__main__":
    Nx = 51
    Ny = 51
    x_arr = np.linspace(-3.0, 3.0, Nx)
    y_arr = np.linspace(-5.5, 3.5, Ny)
    x, y = np.meshgrid(x_arr, y_arr)
    z1 = np.exp(-0.4 * x**2) * np.exp(-(y**2))
    z2 = z1 + 1.0

    x_dict = {"plot1": x, "plot2": x}
    y_dict = {"plot1": y, "plot2": y}
    z_dict = {"plot1": z1, "plot2": z2}

    data_optional = {}
    data_optional["plot1"] = {"ti": "z1", "fmt": "-", "mt": "o", "ms": 2, "mw": 0.2, "lw": 0.1, "lc": "C0", "la": 0.5}
    data_optional["plot2"] = {"ti": "z2", "fmt": "-", "lw": 0.5, "lc": "C3", "la": 0.5}
    surface_optional = {}
    surface_optional["plot1"] = {"map": "viridis", "alpha": 0.0, "xskip": 1, "yskip": 1, "cbar": False}
    surface_optional["plot2"] = {"map": "jet", "alpha": 0.0, "xskip": 1, "yskip": 1, "cbar": True}

    output_image_name = "plot3d_3.png"

    color_range = (0.0, 1.5)
    plot_3d(
        x_dict,
        y_dict,
        z_dict,
        x_range=(-2.5, 2.5),
        x_base=0.5,
        z_range=(-0.5, 1.5),
        z_base=0.25,
        color_range=color_range,
        title_name="Your Title $f(x,y)$",
        x_label="abscissa $x$",
        y_label="abscissa $y$",
        is_bottom_projection=True,
        bottom_projection_key="plot2",
        view_projection="persp",
        xyz_aspect_ratio=(1, 1.5, 2),
        output_image_name=output_image_name,
        data_optional=data_optional,
        surface_optional=surface_optional,
    )