Files
Paper.Formatting.Guidelines/scripts/python/plt_util.py
T
2024-12-05 17:41:01 +08:00

249 lines
11 KiB
Python

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}")