from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import NotRequired, TypedDict
import matplotlib as mpl
import numpy as np
mpl.use('Agg')
import matplotlib.pyplot as plt
class StairStyle(TypedDict):
color: NotRequired[str]
linewidth: NotRequired[float]
linestyle: NotRequired[str]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('input', type=Path, help='projection JSON from the laddu example')
parser.add_argument(
'-o',
'--output',
type=Path,
help='output image (default: input path with a .png suffix)',
)
return parser.parse_args()
def main() -> None:
args = parse_args()
output = args.output or args.input.with_suffix('.png')
with args.input.open(encoding='utf-8') as source:
payload = json.load(source)
if payload.get('schema_version') != 2: msg = f'unsupported projection schema: {payload.get("schema_version")!r}'
raise ValueError(msg)
edges = np.asarray(payload['data']['histogram']['bin_edges'], dtype=float)
centers = 0.5 * (edges[:-1] + edges[1:])
widths = np.diff(edges)
data = np.asarray(payload['data']['histogram']['counts'], dtype=float)
if len(edges) != len(data) + 1:
msg = 'bin_edges must contain one more entry than each histogram'
raise ValueError(msg)
fig, ax = plt.subplots(figsize=(8.0, 5.5), constrained_layout=True)
ax.errorbar(
centers,
data,
xerr=0.5 * widths,
yerr=np.sqrt(np.clip(data, 0.0, None)),
fmt='o',
color='black',
markersize=4,
linewidth=1,
capsize=0,
label=payload['data']['label'],
zorder=4,
)
styles: dict[str, StairStyle] = {
'fit': {'color': '#d62728', 'linewidth': 2.2},
'f0': {'color': '#1f77b4', 'linewidth': 1.8, 'linestyle': '--'},
'f2': {'color': '#2ca02c', 'linewidth': 1.8, 'linestyle': ':'},
}
for series in payload['projections']:
values = np.asarray(series['histogram']['counts'], dtype=float)
if len(values) != len(data):
msg = f'projection {series["id"]!r} has the wrong number of bins'
raise ValueError(msg)
ax.stairs(
values,
edges,
label=series['label'],
**styles.get(series['id'], {'linewidth': 1.8}),
)
ax.set(
xlabel=rf'{payload["observable"]} [$\mathrm{{{payload["unit"]}}}$]',
ylabel=rf'$\mathrm{{Events}}\,/\,({widths[0]:.3f}\,\mathrm{{{payload["unit"]}}})$',
title=payload['title'],
xlim=(edges[0], edges[-1]),
ylim=(0.0, None),
)
ax.legend(frameon=False)
ax.tick_params(direction='in', top=True, right=True)
output.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(output, dpi=180)
print(output)
if __name__ == '__main__':
main()