Skip to content

Commit

Permalink
Merge pull request #503 from DHI/modify_table
Browse files Browse the repository at this point in the history
Imrove skill_table styling/spacing in plt.scatter
  • Loading branch information
ecomodeller authored Feb 14, 2025
2 parents 90730eb + 66cff80 commit 99c4a79
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 91 deletions.
12 changes: 6 additions & 6 deletions modelskill/plotting/_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,22 +151,22 @@ def quantiles_xy(
return np.quantile(x, q=q), np.quantile(y, q=q)


def format_skill_table(skill_scores: Mapping[str, float], unit: str) -> pd.DataFrame:
def format_skill_table(
skill_scores: Mapping[str, float], unit: str, sep: str = " = "
) -> pd.DataFrame:
# select metrics columns
accepted_columns = defined_metrics | {"n"}
kv = {k: v for k, v in skill_scores.items() if k in accepted_columns}

lines = [_format_skill_line(key, value, unit) for key, value in kv.items()]
lines = [_format_skill_line(key, value, unit, sep=sep) for key, value in kv.items()]

# TODO add sign and unit columns
df = pd.DataFrame(lines, columns=["name", "sep", "value"])
return df


def _format_skill_line(
name: str,
value: float | int,
unit: str,
name: str, value: float | int, unit: str, sep: str = " = "
) -> Tuple[str, str, str]:
precision: int = 2
item_unit = " "
Expand All @@ -185,4 +185,4 @@ def _format_skill_line(

name = name.upper()

return f"{name}", " = ", f"{fvalue} {item_unit}"
return f"{name}", sep, f"{fvalue} {item_unit}"
177 changes: 124 additions & 53 deletions modelskill/plotting/_scatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,15 +362,17 @@ def _scatter_matplotlib(
**kwargs,
)

ax.legend(**settings.get_option("plot.scatter.legend.kwargs"))
legend_kwargs = settings.get_option("plot.scatter.legend.kwargs")
legend_kwargs["prop"] = {"size": options.plot.scatter.legend.fontsize}
legend = ax.legend(**legend_kwargs)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.axis("square")
ax.set_xlim([xlim[0], xlim[1]])
ax.set_ylim([ylim[0], ylim[1]])
ax.minorticks_on()
ax.grid(which="both", axis="both", linewidth="0.2", color="k", alpha=0.6)
max_cbar = None
cbar = None
# cmap = kwargs.get("cmap", None)
if show_hist or (show_density and show_points):
try:
Expand All @@ -381,18 +383,35 @@ def _scatter_matplotlib(
pad=0.04,
alpha=options.plot.scatter.points.alpha,
)
ticks = cbar.ax.get_yticks()
max_cbar = ticks[-1]
cbar.set_label("# points")
cbar.ax.yaxis.set_major_locator(MaxNLocator(integer=True))

except ValueError:
# too few points to make a colorbar
pass

ax.set_title(title)
cbar_width = _get_cbar_width(ax, cbar)

## Offset legend
if cbar_width is not None:
legend_loc = ax.transAxes.inverted().transform(
legend.get_bbox_to_anchor().extents[0:2]
)
# If legend is outside the figure, move it to the right of the colorbar
if legend_loc[0] > 1.0:
legend.set_bbox_to_anchor((cbar_width + legend_loc[0], legend_loc[1]))

# Add skill table
if skill_scores is not None:
_plot_summary_table(skill_scores, skill_score_unit, max_cbar=max_cbar)
_plot_summary_table(
skill_scores,
skill_score_unit,
ax,
cbar_width=cbar_width,
)

ax.set_title(title)

return ax


Expand Down Expand Up @@ -576,6 +595,7 @@ def _plot_summary_border(
dx,
dy,
borderpad=0.01,
zorder=0,
) -> None:
## Load settings
bbox_kwargs = {}
Expand All @@ -597,66 +617,117 @@ def _plot_summary_border(
dy + borderpad * 2,
transform=figure_transform,
clip_on=False,
zorder=zorder,
**bbox_kwargs,
)

plt.gca().add_patch(bbox)


def _plot_summary_table(
skill_scores: Mapping[str, float], units: str, max_cbar: Optional[float] = None
) -> None:
table = format_skill_table(skill_scores, units)
cols = ["name", "sep", "value"]
text_cols = ["\n".join(table[col]) for col in cols]

if max_cbar is None:
x = 0.93
elif max_cbar < 1e3:
x = 0.99
elif max_cbar < 1e4:
x = 1.01
elif max_cbar < 1e5:
x = 1.03
elif max_cbar < 1e6:
x = 1.05
def _get_cbar_width(ax, cbar=None) -> float | None:
plt.draw()
# If colorbar, get extents from colorbar label:
if cbar is not None:
label = cbar.ax.yaxis.get_label()
if label is not None:
x_extent_right = ax.transAxes.inverted().transform(
label.get_window_extent()
)[1][0]
else:
# If no label, get max value from colorbar ticks
x_extent_right = cbar.bbox.transformed(ax.transAxes.inverted()).extents[2]
return x_extent_right - 1
else:
# When more than 1e6 samples, matplotlib changes to scientific notation
x = 0.97
return None

fig = plt.gcf()
figure_transform = fig.transFigure.get_affine()

# General text settings
txt_settings = dict(
fontsize=options.plot.scatter.legend.fontsize,
def _plot_summary_table(
skill_scores: Mapping[str, float],
units: str,
ax,
cbar_width: Optional[float] = None,
) -> None:
# If colorbar, get extents from colorbar label:
x0 = options.plot.scatter.skill_table.x_position
if x0 > 1 and cbar_width is not None:
x0 = cbar_width + x0

# Plot table
fontsize = options.plot.scatter.skill_table.fontsize
## Data
table_data = format_skill_table(skill_scores, unit=units, sep="=")
## To get sizing, we plot a dummy table
table_dummy = ax.table(
table_data.values,
)

# Column 1
text_columns = []
dx = 0
for ti in text_cols:
text_col_i = fig.text(x + dx, 0.6, ti, **txt_settings)
## Render, and get width
# plt.draw() # TOOO this causes an error and I have no idea why it is here
dx = (
dx
+ figure_transform.inverted().transform(
[text_col_i.get_window_extent().bounds[2], 0]
)[0]
)
text_columns.append(text_col_i)
table_dummy.auto_set_font_size(False)
table_dummy.set_fontsize(fontsize)

col_widths = []
renderer = ax.figure.canvas.get_renderer()
for col_idx in range(table_data.values.shape[1]): # Iterate over columns
max_width = 0

for row_idx in range(table_data.values.shape[0]): # Iterate over rows
cell = table_dummy[row_idx, col_idx] # Get the cell object safely
text = cell.get_text()
bbox = text.get_window_extent(renderer, dpi=ax.figure.dpi)
if col_idx != 1: # Seoerator column
padding = cell.PAD * ax.figure.dpi * 2
else:
padding = 0
max_width = max(max_width, bbox.width + padding)
height = bbox.height
col_widths.append(max_width)

# Remove dummy table
table_dummy.remove()

# Normalize widths
## These are the widths in axes coordinates
widths_axTrans = [
(
ax.transAxes.inverted().transform((width, height))
- ax.transAxes.inverted().transform((0, 0))
)[0]
for width in col_widths
]
## This is the height (assuming all the same)
height_axTrans = (
ax.transAxes.inverted().transform((0, height))
- ax.transAxes.inverted().transform((0, 0))
)[1]

line_spacing = options.plot.scatter.skill_table.line_spacing
line_padding = options.plot.scatter.skill_table.line_padding
table_height = height_axTrans * line_spacing * table_data.values.shape[0]
table_height_padded = table_height + line_padding * 2
table1 = ax.table(
table_data.values,
loc="lower left",
cellLoc="left",
colWidths=col_widths,
edges="open",
bbox=[
x0,
1 - table_height - line_padding,
np.sum(widths_axTrans),
table_height,
],
)
table1.auto_set_font_size(False)
table1.set_fontsize(fontsize)

# Plot border
## Define coordintes
x0, y0 = figure_transform.inverted().transform(
text_columns[0].get_window_extent().bounds[0:2]
_plot_summary_border(
ax.transAxes,
x0,
1 - table_height_padded,
np.sum(widths_axTrans),
table_height_padded,
borderpad=0,
zorder=table1.get_zorder() - 1,
)
_, dy = figure_transform.inverted().transform(
(0, text_columns[0].get_window_extent().bounds[3])
)

_plot_summary_border(figure_transform, x0, y0, dx, dy)


def __scatter_density(x, y, binsize: float = 0.1, method: str = "linear"):
Expand Down
20 changes: 20 additions & 0 deletions modelskill/plotting/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,26 @@
validator=settings.is_tuple_list_or_str,
)
register_option("plot.scatter.legend.kwargs", {}, validator=settings.is_dict)
register_option(
"plot.scatter.skill_table.x_position",
1.01,
validator=settings.is_float,
)
register_option(
"plot.scatter.skill_table.line_spacing",
1,
validator=settings.is_positive,
)
register_option(
"plot.scatter.skill_table.line_padding",
0.015,
validator=settings.is_float,
)
register_option(
"plot.scatter.skill_table.fontsize",
14,
validator=settings.is_positive,
)
register_option(
"plot.scatter.reg_line.kwargs", {"color": "r"}, validator=settings.is_dict
)
Expand Down
13 changes: 10 additions & 3 deletions modelskill/styles/mood.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
plot.scatter.legend.bbox:
alpha: 0.99
facecolor: w
plot.scatter.legend.fontsize: 14
plot.scatter.legend.fontsize: 10
plot.scatter.legend.kwargs:
bbox_to_anchor: [1.2,0.1]
bbox_to_anchor: [1.05,0.0]
edgecolor: k
loc: center left
loc: lower left
fancybox: False
borderpad: 0.4
borderaxespad : 0.0
plot.scatter.skill_table.x_position: 1.05
plot.scatter.skill_table.line_spacing : 1
plot.scatter.skill_table.line_padding : 0.015
plot.scatter.skill_table.fontsize : 12
plot.scatter.oneone_line.color: darkorange
plot.scatter.oneone_line.label: 1:1 (45°)
plot.scatter.points.alpha: 0.9
Expand Down
47 changes: 18 additions & 29 deletions notebooks/Simple_timeseries_compare.ipynb

Large diffs are not rendered by default.

0 comments on commit 99c4a79

Please sign in to comment.