Skip to content

Commit d0f90f8

Browse files
authored
feat: add xoffsets option (#559)
1 parent 37cbd25 commit d0f90f8

File tree

5 files changed

+102
-8
lines changed

5 files changed

+102
-8
lines changed

src/mplhep/plot.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def histplot(
8585
sort=None,
8686
edges=True,
8787
binticks=False,
88+
xoffsets=None,
8889
ax: mpl.axes.Axes | None = None,
8990
flow="hint",
9091
**kwargs,
@@ -147,6 +148,8 @@ def histplot(
147148
Specifies whether to draw first and last edges of the histogram
148149
binticks : bool, default: False, optional
149150
Attempts to draw x-axis ticks coinciding with bin boundaries if feasible.
151+
xoffsets: bool, default: False,
152+
If True, the bin "centers" of plotted histograms will be offset within their bin.
150153
ax : matplotlib.axes.Axes, optional
151154
Axes object (if None, last one is fetched or one is created)
152155
flow : str, optional { "show", "sum", "hint", "none"}
@@ -273,6 +276,7 @@ def iterable_not_string(arg):
273276
density=density,
274277
binwnorm=binwnorm,
275278
flow=flow,
279+
xoffsets=xoffsets,
276280
)
277281
flow_bins, underflow, overflow = flow_info
278282

@@ -342,6 +346,7 @@ def iterable_not_string(arg):
342346
_ls = _kwargs.pop("linestyle", "-")
343347
_kwargs["linestyle"] = "none"
344348
_plot_info = plottables[i].to_errorbar()
349+
del _plot_info["xerr"]
345350
_e = ax.errorbar(
346351
**_plot_info,
347352
**_kwargs,
@@ -477,7 +482,12 @@ def iterable_not_string(arg):
477482
_plot_info = plottables[i].to_errorbar()
478483
if yerr is False:
479484
_plot_info["yerr"] = None
480-
_plot_info["xerr"] = _xerr
485+
if not xerr:
486+
del _plot_info["xerr"]
487+
if isinstance(xerr, (int, float)) and not isinstance(xerr, bool):
488+
_plot_info["xerr"] = xerr
489+
elif isinstance(xerr, (np.ndarray, list)):
490+
_plot_info["xerr"] = xerr[i]
481491
_e = ax.errorbar(
482492
**_plot_info,
483493
label=_labels[i],

src/mplhep/utils.py

+41-7
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def get_plottables(
145145
stack=False,
146146
density=False,
147147
binwnorm=None,
148+
xoffsets=False,
148149
):
149150
"""
150151
Generate plottable histograms from various histogram data sources.
@@ -189,6 +190,8 @@ def get_plottables(
189190
binwnorm : float, optional
190191
If true, convert sum weights to bin-width-normalized, with unit equal to
191192
supplied value (usually you want to specify 1.)
193+
xoffsets : bool | float | iterable, optional
194+
Offset for x-axis values.
192195
193196
Returns
194197
-------
@@ -203,7 +206,16 @@ def get_plottables(
203206
hists = list(process_histogram_parts(H, bins))
204207
final_bins, _ = get_plottable_protocol_bins(hists[0].axes[0])
205208

206-
for h in hists:
209+
if xoffsets is True:
210+
parsed_offsets = []
211+
widths = np.diff(final_bins)
212+
sub_bin_width = widths / (len(hists) + 1)
213+
for i in range(len(hists)):
214+
parsed_offsets.append(sub_bin_width * (i + 1))
215+
xoffsets = parsed_offsets
216+
else:
217+
xoffsets = [None] * len(hists)
218+
for h, xoffset in zip(hists, xoffsets):
207219
value, variance = np.copy(h.values()), h.variances()
208220
if has_variances := variance is not None:
209221
variance = np.copy(variance)
@@ -241,7 +253,9 @@ def get_plottables(
241253

242254
# Set plottables
243255
if flow in ("none", "hint"):
244-
plottables.append(Plottable(value, edges=final_bins, variances=variance))
256+
plottables.append(
257+
Plottable(value, edges=final_bins, variances=variance, xoffsets=xoffset)
258+
)
245259
elif flow == "show":
246260
_flow_bin_size: float = np.max(
247261
[0.05 * (final_bins[-1] - final_bins[0]), np.mean(np.diff(final_bins))]
@@ -257,7 +271,9 @@ def get_plottables(
257271
value = np.r_[value, overflow]
258272
if has_variances:
259273
variance = np.r_[variance, overflowv]
260-
plottables.append(Plottable(value, edges=flow_bins, variances=variance))
274+
plottables.append(
275+
Plottable(value, edges=flow_bins, variances=variance, xoffsets=xoffset)
276+
)
261277
elif flow == "sum":
262278
if underflow > 0:
263279
value[0] += underflow
@@ -267,9 +283,13 @@ def get_plottables(
267283
value[-1] += overflow
268284
if has_variances:
269285
variance[-1] += overflowv
270-
plottables.append(Plottable(value, edges=final_bins, variances=variance))
286+
plottables.append(
287+
Plottable(value, edges=final_bins, variances=variance, xoffsets=xoffset)
288+
)
271289
else:
272-
plottables.append(Plottable(value, edges=final_bins, variances=variance))
290+
plottables.append(
291+
Plottable(value, edges=final_bins, variances=variance, xoffsets=xoffset)
292+
)
273293

274294
if w2 is not None:
275295
for _w2, _plottable in zip(
@@ -422,7 +442,14 @@ def norm_stack_plottables(plottables, bins, stack=False, density=False, binwnorm
422442

423443
class Plottable:
424444
def __init__(
425-
self, values, *, edges=None, variances=None, yerr=None, w2method="poisson"
445+
self,
446+
values,
447+
*,
448+
edges=None,
449+
xoffsets=None,
450+
variances=None,
451+
yerr=None,
452+
w2method="poisson",
426453
):
427454
self._values = np.array(values).astype(float)
428455
self.variances = None
@@ -440,8 +467,14 @@ def __init__(
440467
if self.edges is None:
441468
self.edges = np.arange(len(values) + 1)
442469
self.centers = self.edges[:-1] + np.diff(self.edges) / 2
443-
self.method = w2method
470+
if xoffsets is not None:
471+
self.centers = self.edges[:-1] + xoffsets
472+
self.xerr_lo, self.xerr_hi = (
473+
self.centers - self.edges[:-1],
474+
self.edges[1:] - self.centers,
475+
)
444476

477+
self.method = w2method
445478
self.yerr = yerr
446479
assert self.variances is None or self.yerr is None
447480
if self.yerr is not None:
@@ -579,6 +612,7 @@ def to_errorbar(self):
579612
"x": self.centers,
580613
"y": self.values,
581614
"yerr": [self.yerr_lo, self.yerr_hi],
615+
"xerr": [self.xerr_lo, self.xerr_hi],
582616
}
583617

584618

6 Bytes
Loading
14.7 KB
Loading

tests/test_basic.py

+50
Original file line numberDiff line numberDiff line change
@@ -770,3 +770,53 @@ def test_histplot_sort(sort):
770770
)
771771
ax.legend()
772772
return fig
773+
774+
775+
@pytest.mark.mpl_image_compare(style="default", remove_text=True)
776+
def test_histplot_xoffsets():
777+
np.random.seed(0)
778+
evts = np.random.normal(2, 2, 100)
779+
htype1 = hist.new.Var([0, 1, 2, 3, 5, 10]).Weight().fill(evts)
780+
781+
fig, axs = plt.subplots(2, 2)
782+
axs = axs.flatten()
783+
hep.histplot(
784+
[htype1, htype1, htype1],
785+
ax=axs[0],
786+
xoffsets=True,
787+
xerr=False,
788+
histtype="errorbar",
789+
alpha=1,
790+
)
791+
hep.histplot(htype1, yerr=False, alpha=0.2, ax=axs[0])
792+
793+
hep.histplot(
794+
[htype1, htype1, htype1],
795+
ax=axs[1],
796+
xoffsets=True,
797+
xerr=True,
798+
histtype="errorbar",
799+
alpha=1,
800+
)
801+
hep.histplot(htype1, yerr=False, alpha=0.2, ax=axs[1])
802+
803+
hep.histplot(
804+
[htype1, htype1, htype1],
805+
ax=axs[2],
806+
xoffsets=True,
807+
xerr=0.5,
808+
histtype="errorbar",
809+
alpha=1,
810+
)
811+
hep.histplot(htype1, yerr=False, alpha=0.2, ax=axs[2])
812+
813+
hep.histplot(
814+
[htype1, htype1, htype1],
815+
ax=axs[3],
816+
xoffsets=True,
817+
xerr=[0.2, 0.5, 1],
818+
histtype="errorbar",
819+
alpha=1,
820+
)
821+
hep.histplot(htype1, yerr=False, alpha=0.2, ax=axs[3])
822+
return fig

0 commit comments

Comments
 (0)