commit de16a7931eea19424661b183eb46a8592dc01d6a Author: Gatsby Date: Thu Dec 5 17:41:01 2024 +0800 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78d77b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Generated files +*.svg +*.pdf +*.tiff +*.png +*.jpg +*.esp +!images/* + +# System generated files +# -- macOS +.DS_Store +.AppleDouble +.LSOverride +._* + +# -- Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# -- Linux +*~ + +# IDEs and editors' files +*.idea +*.vscode +*.sublime-workspace +*.sublime-project diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fba4ad --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# 论文部分格式要求 + +## 公式 + +1. 所有公式统一使用 `align` 块进行排版 + + ```latex + \begin{align} + \label{refernce_label} + % + % ... ... + % + \end{align} + ``` + + 示例: + + ```latex + \begin{align} + \label{shannon–hartley_theorem} + C = \log \left( \det \left( \mathbf{I} + \frac{P}{N_0} \mathbf{H} \mathbf{H}^H \right) \right). + \end{align} + ``` + + >$$ + >\begin{align} + >C = \log \left( \det \left( \mathbf{I} + \frac{P}{N_0} \mathbf{H} \mathbf{H}^H \right) \right). + >\end{align} + >$$ + +2. 公式结尾必须有合适的标点,段落中的公式以 “`,`” 结尾,段落间的公式以 “`.`” 结尾 + + 示例: + + ```latex + The capacity $C$ of a MIMO channel of a MIMO channel is given by, + % + \begin{align} + C = \log_2 \left( \det \left( \mathbf{I} + \mathbf{Q} \right) \right), + \end{align} + % + where the matrix $\mathbf{Q}$ is defined as follow. + % + \begin{align} + \mathbf{Q} = \frac{P}{N_0} \mathbf{H} \mathbf{H}^H. + \end{align} + ``` + + >The capacity $C$ of a MIMO channel is given by, + >$$ + >\begin{aligned} + >C = \log_2 \left( \det \left( \mathbf{I} + \mathbf{Q} \right) \right), + >\end{aligned} + >$$ + >where the matrix $\mathbf{Q}$ is defined as follow. + >$$ + >\begin{aligned} + >\mathbf{Q} = \frac{P}{N_0} \mathbf{H} \mathbf{H}^H. + >\end{aligned} + >$$ + +## 插图要求 + +1. 插图力求观感清晰,表意明确且全文格式统一。图片的基本格式要求如下: + + * 比例 16:9 或 4:3 + * 文字字号 12 pt + * 图例(Legend)字号:12 pt + * 坐标标签字号:12 pt + * 坐标刻度字号:10 pt + * 线条宽度:1 pt + * 标记大小:8 pt + * 图例、标签仅第一个单词首字母大写,其余小写; + * **字体与正文一致**,文本一般为衬线字体 `Times New Roman` 公式一般为 `Latin Modern Math` + + > **注**:上述配置绘制图片大小为实际文章中一栏的物理尺寸,在显示器上会显得较小,建议最后输出图片时再使用;在正文中插入图片时需设置宽度为 `0.5 \textwidth` ,例如: + > + > ```latex + > \begin{figure}[!t] + > \centering + > \includegraphics[width=0.5\textwidth]{figures/fig1} + > \caption{Demo figure} + > % ... ... + > \end{figure} + > ``` + > + > **注**:MATLAB 只能使用内置的 Latex 解释器,无法渲染 `amsmath,amsfonts` 包中的特殊公式和符号。 + +2. 仿真插图要求: + + * 不建议只使用颜色区分曲线,建议在线形上作区分,不同的 marker 表示不同的方法,同一个方法下的不同参数配置使用不同的 line; + * 图例要求简洁清晰,统一方法的不同参数配置可在图中使用箭头等方法标注趋势; + * 曲线点数不宜过多。 + + 示例: + + * Python matplotlib 绘制 + + ![Example (Python)](images/example_python.png "Example (Python)") + + * MATLAB 绘制 + + ![Example (MATLAB)](images/example_matlab.png "Example (MATLAB)") + + > **注**:matplotlib 与 MATLAB 的部分样式不同,渲染工具也不同,最终生成的文件略有区别。即便是同一套工具生成不同格式的图片文件(`pdf`,`eps`,`svg` 等)时也会存在一定误差,特别是字体渲染上的误差,导出为 `pdf` 时最能还原预览界面效果,具体可根据渲染效果进一步进行微调。 + +## 引用要求 + +1. 问内的公式、插图、表格可以添加并使用以下三个命令进行引用。 + + ```latex + \let\eqlabel\eqref + \renewcommand{\eqref}[1]{Eq. \eqlabel{#1}} + \newcommand{\figref}[1]{Fig. \ref{#1}} + \newcommand{\tabref}[1]{Table. \ref{#1}} + ``` + +2. 参考文献中期刊名字需要按照投稿要求进行缩写,IEEE 期刊标准缩写定义在 `IEEEabrv.bib` 中。 + +3. 所有参考文献标题**仅第一个单词首字母大写**。 diff --git a/images/example_matlab.png b/images/example_matlab.png new file mode 100644 index 0000000..2650016 Binary files /dev/null and b/images/example_matlab.png differ diff --git a/images/example_python.png b/images/example_python.png new file mode 100644 index 0000000..f91f777 Binary files /dev/null and b/images/example_python.png differ diff --git a/scripts/matlab/arrow.m b/scripts/matlab/arrow.m new file mode 100644 index 0000000..f12977e --- /dev/null +++ b/scripts/matlab/arrow.m @@ -0,0 +1,73 @@ +function hAnnot = arrow(startPoint, endPoint, ax, arrowType, varargin) +% ARROW Draws an annotation of specified type between two data points. +% +% hAnnot = ARROW(startPoint, endPoint, ax, arrowType) creates an +% annotation from the coordinate specified by startPoint to the coordinate +% specified by endPoint. The annotation type can be 'arrow', 'doublearrow', +% 'textarrow', etc. +% +% hAnnot = ARROW(startPoint, endPoint, ax, arrowType, Name, Value) allows +% additional customization of the annotation properties (e.g., color, +% arrowhead size, line width, etc.). +% +% INPUT: +% startPoint - 1x2 array, [x1, y1], coordinates of the start point +% in the data space of the current axes. +% endPoint - 1x2 array, [x2, y2], coordinates of the end point +% in the data space of the current axes. +% ax - (optional) Axes handle. If omitted, the current axes +% (gca) will be used. +% arrowType - (optional) Type of annotation ('arrow', 'doublearrow', +% 'textarrow', etc.). Default is 'arrow'. +% Name, Value - (optional) Name-value pairs to customize the annotation's +% appearance (e.g., 'Color', 'LineWidth', etc.). +% +% OUTPUT: +% hAnnot - Handle to the created annotation object. +% +% Example: +% figure; +% x = linspace(0, 10, 100); +% y = sin(x); +% plot(x, y); +% arrow([2, sin(2)], [8, sin(8)], gca, 'textarrow', 'Color', 'r', 'String', 'Text'); +% +% Notes: +% The function automatically converts the data coordinates to normalized +% figure coordinates for annotation purposes. + + % Input validation + if nargin < 2 + error('arrow:InsufficientInputs', 'Both startPoint and endPoint must be specified.'); + end + if nargin < 3 || isempty(ax) + ax = gca; % If axes handle is not provided, use the current axes + end + if nargin < 4 || isempty(arrowType) + arrowType = 'arrow'; % Default arrow type is 'arrow' + end + if numel(startPoint) ~= 2 || numel(endPoint) ~= 2 + error('arrow:InvalidInput', 'startPoint and endPoint must be 1x2 arrays representing [x, y] coordinates.'); + end + + % Get the current axes position in normalized figure units + ax_pos = ax.Position; + + % Convert data coordinates to normalized figure coordinates + startNorm = [(startPoint(1) - ax.XLim(1)) / diff(ax.XLim), ... + (startPoint(2) - ax.YLim(1)) / diff(ax.YLim)]; + endNorm = [(endPoint(1) - ax.XLim(1)) / diff(ax.XLim), ... + (endPoint(2) - ax.YLim(1)) / diff(ax.YLim)]; + + % Map normalized data coordinates to figure normalized coordinates + startAnnot = [ax_pos(1) + startNorm(1) * ax_pos(3), ... + ax_pos(2) + startNorm(2) * ax_pos(4)]; + endAnnot = [ax_pos(1) + endNorm(1) * ax_pos(3), ... + ax_pos(2) + endNorm(2) * ax_pos(4)]; + + % Create the specified type of annotation + hAnnot = annotation(arrowType, [startAnnot(1) endAnnot(1)], ... + [startAnnot(2) endAnnot(2)], ... + varargin{:}); % Pass additional name-value pairs + +end \ No newline at end of file diff --git a/scripts/matlab/example.m b/scripts/matlab/example.m new file mode 100644 index 0000000..71042e7 --- /dev/null +++ b/scripts/matlab/example.m @@ -0,0 +1,45 @@ +clear; clc; close all; + +fig_config(); + +%% Initiate test plot data +x = linspace(0, 10, 14); + +% -- bounds +upper_bound = ones(size(x)) * 4.5; +lower_bound = upper_bound * 0.4 + +% -- lines +method_1_1 = upper_bound - (upper_bound - lower_bound) .* exp(-0.7 * x); +method_1_2 = upper_bound - (upper_bound - lower_bound) .* exp(-0.5 * x); +method_1_3 = upper_bound - (upper_bound - lower_bound) .* exp(-0.3 * x); +method_2 = upper_bound - (upper_bound - lower_bound) .* exp(-0.5 * x) .* cos(x); + + +%% Plot +x_min = 0; x_max = 10; +y_min = 2; y_max = 6; +fig = figure(); hold on; box on; grid on; +line_optimal = plot(x, upper_bound, 'k-', 'DisplayName', 'Optimal'); +line_method_1 = plot(x, method_1_1, '^b-', 'DisplayName', 'Method 1'); +plot(x, method_1_2, '^b--') +plot(x, method_1_3, '^b-.') +line_method_2 = plot(x, method_2, '*r-', 'DisplayName', 'Method 2'); +xlim([x_min, x_max]); +ylim([y_min, y_max]); +xticks(x_min:1:x_max); +yticks(y_min:1:y_max); + +% -- Add arrow +arrow([2.5, 4.2], [3.8, 3]); +text(4, 3, '$\gamma = 1, 2, 3$') + +% -- Add legends and labels +% h = findobj(fig, '-regexp', 'DisplayName', '.+'); +lgd = legend([line_optimal, line_method_1, line_method_2]); +set(lgd, 'ItemTokenSize', [10, 5]); +ylabel("$\mathbf{E}\left[S\right]$") +xlabel("Rounds of Iteration") + +% -- export +fig_export('example_matlab.pdf', 'pdf', '../../images') \ No newline at end of file diff --git a/scripts/matlab/fig_config.m b/scripts/matlab/fig_config.m new file mode 100644 index 0000000..0eecf1d --- /dev/null +++ b/scripts/matlab/fig_config.m @@ -0,0 +1,72 @@ +function fig_config(customConfig) + % setFigureDefaults Sets default properties for all figures + % + % Usage: + % fig_config() % Applies default settings + % fig_config(userConfig) % Overrides defaults with userConfig + + %% Define default settings + % In IEEETran, \textwidth is 7.1413 inches + % - Use `\printinunitsof{in}\prntlen{\textwidth}` + % - To output the \textwidth value in the generated document, + % you need to use the `printlen` and `layouts` packages. + textwidth_inches = 7.1413; + + % Set the figure width to 0.5 times \textwidth and define the aspect ratio (16:9 or 4:3) + aspect = 16/9; % Aspect ratio (can also use 4/3) + axes_width = 0.75; + axes_hight = 0.75; + axes_pos = [0.15, 0.15, axes_width, axes_hight]; + fig_width = 0.5 * textwidth_inches; + fig_hight = (fig_width * axes_width / aspect) / axes_hight; + + defaultConfig = struct( ... + 'defaultFigureUnits', 'inches', ... + 'defaultFigurePosition', [0, 0, fig_width, fig_hight], ... + 'defaultAxesPosition', axes_pos, ... + 'defaultFigurePaperPositionMode', 'manual', ... + 'defaultFigurePaperUnits', 'inches', ... + 'defaultFigurePaperPosition', [0, 0, fig_width, fig_hight], ... + 'defaultFigurePaperSize', [fig_width, fig_hight], ... + 'defaultLineLineWidth', 0.5, ... % -- Line and marker + 'defaultLineMarkerSize', 3, ... + 'defaultAxesFontName', 'Times New Roman', ... % -- Fonts + 'defaultTextFontName', 'Times New Roman', ... + 'defaultLegendFontName', 'Times New Roman', ... + 'defaultAxesLabelFontSizeMultiplier', 1.15, ... + 'defaultAxesFontSize', 7, ... + 'defaultTextFontSize', 10, ... + 'defaultLegendFontSize', 7, ... + 'defaultTextInterpreter', 'latex', ... % -- Interpreter + 'defaultAxesTickLabelInterpreter', 'latex', ... + 'defaultLegendInterpreter', 'latex', ... + 'defaultAxesGridColor', [0.15, 0.15, 0.15], ... % -- Grid + 'defaultAxesGridAlpha', 0.25, ... + 'defaultAxesGridLineStyle', ':', ... + 'defaultAxesGridLineWidth', 0.5, ... + 'defaultArrowshapeHeadWidth', 6, ... % -- Arrow + 'defaultArrowshapeHeadLength', 6 ... + ); + + % Merge user config with defaults + if nargin == 1 && ~isempty(customConfig) + userFields = fieldnames(customConfig); + for i = 1:length(userFields) + defaultConfig.(userFields{i}) = customConfig.(userFields{i}); + end + end + + % Iterate and set properties + fields = fieldnames(defaultConfig); + for i = 1:length(fields) + try + set(groot, fields{i}, defaultConfig.(fields{i})); + catch ME + errorStruct.message = sprintf("%s\nUse get(groot, 'factory') " + ... + "to get all the available parameters.", ME.message); + errorStruct.identifier = ME.identifier; + errorStruct.stack = ME.stack; + error(errorStruct) + end + end +end \ No newline at end of file diff --git a/scripts/matlab/fig_export.m b/scripts/matlab/fig_export.m new file mode 100644 index 0000000..9bde771 --- /dev/null +++ b/scripts/matlab/fig_export.m @@ -0,0 +1,80 @@ +function fig_export(filename, format, folder, dpi, fig) + % EXPORT_CHART Save the current MATLAB figure or a specified figure in the selected image format. + % + % Parameters: + % - filename (string, optional): The output file name. If it has no extension, one will be + % added based on the format. If missing or empty, a default + % name 'exported_plot' is used. + % - format (string): The desired file format for the export. Default is 'pdf'. + % Accepted formats are 'png', 'jpg', 'tiff', 'svg', 'pdf', 'esp'. + % - folder (string): The directory where the image file will be saved. Default is the current directory. + % - dpi (int): Resolution in dots per inch. Default is 96. + % - fig (figure handle, optional): The MATLAB Figure object to export. If empty, the current active figure is used. + % + % Notes: + % - The function creates the folder if it doesn't exist. + % - Ensures compatibility with supported MATLAB formats. + + if nargin < 1 || isempty(filename) + timestamp = char(datetime('now', 'Format','yyyyMMdd_HHmmSS')); + filename = ['exported_plot_' timestamp]; + end + if nargin < 2 || isempty(format) + format = 'pdf'; + end + if nargin < 3 || isempty(folder) + folder = '.'; + end + if nargin < 4 || isempty(dpi) + dpi = 96; + end + if nargin < 5 || isempty(fig) + fig = gcf; + end + + % Add extension if missing + [~, ~, ext] = fileparts(filename); + if isempty(ext) + filename = [filename '.' format]; + else + format = ext(2:end); % Use extension for format + end + + % Validate folder and create if necessary + if ~exist(folder, 'dir') + mkdir(folder); + end + + % Construct full file path + filepath = fullfile(folder, filename); + + % Format to '-d' option mapping + supportedFormats = struct( ... + 'png', '-dpng', ... + 'jpg', '-djpeg', ... + 'tiff', '-dtiff', ... + 'svg', '-dsvg', ... + 'pdf', '-dpdf', ... + 'eps', '-depsc' ... + ); + if ~isfield(supportedFormats, lower(format)) + error('Unsupported format "%s". Supported formats are: %s.', ... + format, strjoin(fieldnames(supportedFormats), ', ')); + end + + % Get the corresponding '-d' option + dOption = supportedFormats.(lower(format)); + + % Attempt to save the figure + try + % Include resolution for raster formats + if ismember(lower(format), {'png', 'jpg', 'tiff'}) + print(fig, filepath, dOption, ['-r' num2str(dpi)]); + else + print(fig, filepath, dOption); + end + fprintf('Exported image to: "%s" as %s\n', filepath, format); + catch ME + fprintf('Failed to export the figure: %s\n', ME.message); + end +end \ No newline at end of file diff --git a/scripts/python/example.py b/scripts/python/example.py new file mode 100644 index 0000000..2602f0f --- /dev/null +++ b/scripts/python/example.py @@ -0,0 +1,56 @@ +from plt_util import plt_config, plt_export +import matplotlib.pyplot as plt +from matplotlib.patches import FancyArrowPatch +import matplotlib.ticker as ticker +import numpy as np + +plt_config(plt) + +## Initiate test plot data +x = np.linspace(0, 10, 14) + +# -- bounds +upper_bound = np.ones_like(x) * 4.5 +lower_bound = upper_bound * 0.4 + +# -- lines +method_1_1 = upper_bound - (upper_bound - lower_bound) * np.exp(-0.7 * x) +method_1_2 = upper_bound - (upper_bound - lower_bound) * np.exp(-0.5 * x) +method_1_3 = upper_bound - (upper_bound - lower_bound) * np.exp(-0.3 * x) +method_2 = upper_bound - (upper_bound - lower_bound) * np.exp(-0.5 * x) * np.cos(x) + + +## Plot +x_min, x_max = 0, 10 +y_min, y_max = 2, 6 +plt.plot(x, upper_bound, 'k-', label='Optimal') +plt.plot(x, method_1_1, '^b-', label='Method 1') +plt.plot(x, method_1_2, '^b--') +plt.plot(x, method_1_3, '^b-.') +plt.plot(x, method_2, '*r-', label='Method 2') +plt.xlim(x_min, x_max) +plt.ylim(y_min, y_max) +plt.grid(True, linestyle='--', color='gray', linewidth=0.5) + +# -- Add arrow +arrow = FancyArrowPatch((2.5, 4.2), # start point + (3.8, 3), # end point + arrowstyle='-|>', # arrow style + mutation_scale=10, # arrow head size + color='black', # arrow color + linewidth=0.5) # line width +plt.gca().add_patch(arrow) +plt.text(4, 3, '$\gamma = 1, 2, 3$') + +# -- Add legends and labels +plt.legend() +plt.gca().xaxis.set_major_locator(ticker.MultipleLocator(1)) +plt.gca().yaxis.set_major_locator(ticker.MultipleLocator(1)) +plt.ylabel(r"$\mathbb{E}\left[S\right]$") +plt.xlabel(r'Rounds of Iteration') + +# -- export and show +plt_export(filename='example_python.pdf', + folder='../../images', + format='pdf') +plt.show() \ No newline at end of file diff --git a/scripts/python/plt_util.py b/scripts/python/plt_util.py new file mode 100644 index 0000000..a86aa92 --- /dev/null +++ b/scripts/python/plt_util.py @@ -0,0 +1,249 @@ +import os, re, platform +import matplotlib as mpl +import matplotlib.pyplot as plt +import pandas as pd +from datetime import datetime + +def plt_config(plt, custom_rcparams=None): + """ + Configure matplotlib runtime parameters for consistent styling in IEEETran-like figures. + + Parameters: + - plt: The matplotlib module to configure. + - custom_rcparams: (dict, optional) Custom runtime parameters to override or extend the defaults. + Default is None. + """ + # In IEEETran, \textwidth is 7.1413 inches + # - Use `\printinunitsof{in}\prntlen{\textwidth}` + # - To output the \textwidth value in the generated document, + # you need to use the `printlen` and `layouts` packages. + textwidth_inches = 7.1413 + + # Configure figure dimensions: width as 0.5 * \textwidth and aspect ratio (16:9 or 4:3) + aspect = 16 / 9 # Aspect ratio (can also use 4/3) + axes_left, axes_right, axes_bottom, axes_top = 0.15, 0.9, 0.15, 0.9 + axes_width = axes_right - axes_left + axes_height = axes_top - axes_bottom + fig_width = 0.5 * textwidth_inches + fig_height = (fig_width * axes_width / aspect) / axes_height + + # Default runtime parameters + # -- call plt.rcParams.keys() to list all available rcParameter + default_rcparams = { + "text.usetex": True, # Use LaTeX for rendering text + "text.latex.preamble": r'\usepackage{amsmath,amsfonts}', # Load LaTeX packages + "font.family": 'Times New Roman', # Set font family + "font.weight": 'normal', # Set font weight + "font.size": 10, # Set base font size + "figure.figsize": (fig_width, fig_height), # Set figure size + "lines.linewidth": 0.5, # Set line width + "lines.markeredgewidth": 0.5, # Set marker edge width + "lines.markersize": 2, # Set marker size + "figure.subplot.bottom": axes_bottom, # Subplot bottom margin + "figure.subplot.left": axes_left, # Subplot left margin + "figure.subplot.right": axes_right, # Subplot right margin + "figure.subplot.top": axes_top, # Subplot top margin + "legend.markerscale": 1.0, # Scale of markers in the legend + "legend.handleheight": 0.5, # Height of legend handles + "legend.handlelength": 1.5, # Length of legend handles + "legend.handletextpad": 0.25, # Padding between handle and text + "legend.columnspacing": 1.5, # Spacing between legend columns + "legend.labelspacing": 0.25, # Spacing between legend entries + "legend.fontsize": 7, # Legend font size + "legend.borderpad": 0.2, # Padding inside the legend box + "axes.labelsize": 8, # Axis label font size + "axes.linewidth": 0.5, # Axis line width + "axes.grid": True, # Enable grid + "axes.formatter.limits": (-3, 3), + "axes.formatter.useoffset": True, + "grid.color": '#0F0F0F', # Grid color + "grid.alpha": 0.25, # Grid transparency + "grid.linestyle": ':', # Grid line style + "grid.linewidth": 0.5, # Grid line width + "xtick.direction": "in", # Direction of x-ticks + "ytick.direction": "in", # Direction of y-ticks + "xtick.labelsize": 7, # Font size for x-tick labels + "ytick.labelsize": 7 # Font size for y-tick labels + } + + # Override or extend the default parameters if custom settings are provided + if custom_rcparams is not None: + default_rcparams.update(custom_rcparams) + + # Apply the final configuration to matplotlib + # -- use plt.rcParams.keys() to list all available parameters + plt.rcParams.update(default_rcparams) + +def plt_export(filename=None, format='pdf', folder='.', dpi=96, fig=None): + """ + Save the current Matplotlib figure or a specified figure in the selected image format. + + This function should be a standard part of chart scripts, enabling the "Export image" + functionality of the IDE and `opp_charttool` to work seamlessly. + + Parameters: + - filename (str, optional): The output file name. If it has no extension, one will be added based on the format. + If missing or empty, a sanitized version of the chart name is used. + - format (str): The desired file format for the export. Default is 'pdf'. + Accepted formats are those supported by Matplotlib (e.g., 'png', 'jpg', 'svg', 'pdf', etc.). + - folder (str): The directory where the image file will be saved. Default is the current directory. + - dpi (int): Dots per inch setting, default is 96. For raster image formats, the image dimensions are + produced as width (or height) times dpi. + - fig (matplotlib.figure.Figure, optional): The Matplotlib Figure object to export. If None, the current active figure is used. + + Notes: + - For export, even IDE-native charts are rendered using Matplotlib. + - `dpi` controls the resolution for raster image formats while maintaining the chart's aspect ratio and relative sizes. + """ + + # Validate the format + valid_formats = mpl.figure.Figure().savefig.__code__.co_varnames + if format.lower() not in mpl.figure.Figure().canvas.get_supported_filetypes().keys(): + raise ValueError(f"Unsupported format '{format}'. Supported formats are: {', '.join(mpl.figure.Figure().canvas.get_supported_filetypes().keys())}") + + # Handle filename + if not filename: + # If filename is None or empty, use a default name + filename = 'exported_plot' + + if not re.match(r".+\.\w{3,4}$", filename): + filename += f".{format.lower()}" + + filepath = os.path.normpath(os.path.join(folder, filename)) + os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True) + + # Function to create metadata + def _make_image_metadata(): + creator_info = ( + f"Matplotlib {mpl.__version__}; " + f"Pandas {pd.__version__}; " + f"Python {platform.python_version()}; " + f"{platform.platform()}" + ) + return {"Creator": creator_info} + + # Select the figure to save + target_fig = fig or plt.gcf() + + # Prepare metadata if supported + metadata = _make_image_metadata() if format.lower() in ['pdf', 'svg', 'eps'] else None + + # Attempt to save the figure + try: + target_fig.savefig(filepath, format=format, dpi=dpi, metadata=metadata, bbox_inches='tight') + print(f'Exported image to: "{filepath}" as {format}') + except Exception as e: + print(f"Failed to save with metadata due to: {e}. Retrying without metadata.") + try: + target_fig.savefig(filepath, format=format, dpi=dpi, bbox_inches='tight') + print(f'Exported image to: "{filepath}" as {format} without metadata') + except Exception as e: + print(f"Failed to save the figure: {e}") + +def plt_export(filename=None, format='pdf', folder='.', dpi=96, fig=None): + """ + Save the current Matplotlib figure or a specified figure in the selected image format. + + This function should be a standard part of chart scripts, enabling the "Export image" + functionality of the IDE and `opp_charttool` to work seamlessly. + + Parameters: + - filename (str, optional): The output file name. If it has no extension, one will be + added based on the format. If missing or empty, a default + name 'exported_plot' is used. + - format (str): The desired file format for the export. Default is 'pdf'. + Accepted formats are those supported by Matplotlib (e.g., 'png', 'jpg', + 'svg', 'pdf', etc.). + - folder (str): The directory where the image file will be saved. Default is the current directory. + - dpi (int): Dots per inch setting, default is 96. For raster image formats, the image + dimensions are produced as width (or height) times dpi. + - fig (matplotlib.figure.Figure, optional): The Matplotlib Figure object to export. If None, + the current active figure is used. + + Notes: + - For export, even IDE-native charts are rendered using Matplotlib. + - `dpi` controls the resolution for raster image formats while maintaining the chart's + aspect ratio and relative sizes. + """ + # Select the figure to save + target_fig = fig or plt.gcf() + + # Validate the format + supported_formats = target_fig.canvas.get_supported_filetypes() + if format.lower() not in supported_formats: + raise ValueError( + f"Unsupported format '{format}'. Supported formats are: " + f"{', '.join(supported_formats.keys())}" + ) + + # Handle filename + if not filename: + # Generate a default filename with a timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f'exported_plot_{timestamp}' + + if not re.match(r".+\.\w{3,4}$", filename): + filename += f".{format.lower()}" + + filepath = os.path.normpath(os.path.join(folder, filename)) + os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True) + + def _get_mpl_backend_for_image_format(format): + return { + "png": "AGG", + "jpg": "AGG", + "jpeg": "AGG", + "tif": "AGG", + "tiff": "AGG", + "raw": "AGG", + "svg": "SVG", + "svgz": "SVG", + "ps" : "PS", + "eps": "PS", + "pdf": "PDF", + "pgf": "PGF", + }.get(format.lower(), None) + + # Function to create metadata + def _make_image_metadata(): + creator_info = ( + f"Matplotlib {mpl.__version__}; " + f"Pandas {pd.__version__}; " + f"Python {platform.python_version()}; " + f"{platform.platform()}" + ) + return {"Creator": creator_info} + + # Prepare metadata if supported + metadata = _make_image_metadata() + # Explicitly select a non-interactive backend appropriate for the export format, + # and keep auto-selection for any other formats not listed here. + backend = _get_mpl_backend_for_image_format(format) + + # Attempt to save the figure + try: + target_fig.savefig( + filepath, + format=format, + dpi=dpi, + metadata=metadata, + backend=backend + ) + print(f'Exported image to: "{filepath}" as {format}') + except Exception as e: + print( + f"Failed to save with metadata due to: {e}. " + "Retrying without metadata." + ) + try: + target_fig.savefig( + filepath, + format=format, + dpi=dpi, + backend=backend + ) + print( + f'Exported image to: "{filepath}" as {format} without metadata' + ) + except Exception as e_inner: + print(f"Failed to save the figure: {e_inner}") \ No newline at end of file