Histograms and plots¶
A simulation that has run has produced numbers, and the numbers are the point. This is the path from a count to something on screen:
what fills it --> Histogram --> RootHistogram --> a pad on a canvas
(a recipe, PointSet RootScatter
a script, (the data) (this view of it)
a loop)
Three objects, and the split between them is the thing to understand: the data object is the measurement, the ROOT proxy is one view of it, and the canvas decides how many views fit side by side. Two panes can show one histogram with different axis scales; they cannot disagree about how many bins it has.
A measurement is one of two shapes, and each has its proxy:
| Data | Proxy | Draws |
|---|---|---|
Histogram — counts in bins |
RootHistogram |
a curve, or a colour map |
PointSet — x against y |
RootScatter |
one point per pair |
Both live in Math and neither knows anything about particles, runs or
events: a Histogram is counts in bins and a PointSet is pairs of numbers,
whether they came from a Geant4 run or a spreadsheet.
A pad can hold more than one curve — two spectra compared, a measurement against its fit. That is a group, and it is the same three objects with one more nested in between.
Histogram — the measurement¶
uLib::Histogram (src/Math/Histogram.h) counts weighted points in any number
of dimensions. Its axes are three parallel lists, one entry per axis:
| Property | Meaning |
|---|---|
BinCounts |
bins per axis — 50 is 1D, 50, 50 is 2D, and so on |
AxisMin, AxisMax |
the low and high edge of each axis |
AxisTitles |
comma-separated names, "energy,angle" |
Title |
what the histogram is of |
The number of axes is however many entries the lists agree on; a list of the
wrong length makes the histogram unusable and GetBuildError() says so
rather than guessing at an intent.
Bins live in one flat vector of the product of the counts, so a 3-axis
histogram is a real 3-axis histogram and not three projections. Ranges are
half-open, [min, max).
Points outside the range are counted, not clamped. GetUnderflow() and
GetOverflow() report them. A histogram whose range was set too narrow should
say so; piling the tail into the last bin invents a peak that is not there.
Filling is mutex-guarded, because the thing filling it is often a Geant4 worker thread.
Histogram is bound for Python, so the thing filling it is usually a recipe
with the histogram on an output port:
Spectrum = OutputPort("Spectrum")
Spectrum.Fill(2150.0) # 1D, weight defaults to 1
Spectrum.Fill2(2150.0, 0.03) # 2D
Spectrum.FillPoint([x, y, z], w) # any number of axes
Each returns False when the point fell outside every bin, which is how a script can notice its range is wrong without reading the overflow counts.
Changing the bins, and filling it again¶
The axis lists are edited in the Properties panel as text: 40, or 40 20
(commas are accepted too). Changing BinCounts, AxisMin or AxisMax
empties the histogram, since the old contents belong to the old bins. It
then emits Updated(), so any plot of it redraws, followed by
UpdateRequested(). A title change does neither.
UpdateRequested() asks whoever fills the histogram to fill it again. A
recipe (or any Algorithm) with AutoRun set answers it when the histogram
is on one of its output ports. The recipe then runs, refills the
histogram for the new bins, and when it finishes announces Updated() on its
outputs, so the canvas redraws with the new contents. This needs no extra
wiring: the Recipe --(Histogram) wire that the recipe already fills through
is the link.
- A recipe that re-bins its own output while it is running is not run a second time.
SetAxes()asks for a refill once, however many axes changed.- Loading a saved scene asks for no refill: the saved contents come back with it.
- Every run of the filling algorithm starts from empty. When an algorithm
starts a run, it emits
RefillStarted()on its output objects, and aHistogram(or aPointSet) withResetOnRefillon — the default — empties itself there. A re-run therefore replaces the plot instead of adding to it. TurnResetOnRefilloff to accumulate over runs on purpose.
RootHistogram — one view of it¶
uLib::Root::RootHistogram (src/Root/RootHistogram.h) is the ROOT proxy: it
builds a TH1D or TH2D from a histogram on its Source input port and keeps
the two in step. It is the same split the VTK side uses, where a Prop3D draws
a solid without the solid knowing it is drawn.
A histogram may have more axes than a plot can show. AxisX and AxisY pick
which two; AxisY = -1 is a one-dimensional plot. Any remaining axes are
summed over, which is the honest thing to do with a dimension you are not
looking at, and the plot's title says it is happening.
Refresh() copies the bins across, and it is deliberately not automatic: the
histogram is filled from worker threads and ROOT is not thread-safe, so the
copy happens on the thread that owns the canvas, when that thread asks.
In gcompose, a canvas pane does the asking. When a histogram, a point set or a
plot in the scene emits Updated(), the pane schedules one redraw on its own
thread. Several announcements before that redraw still produce a single one.
Display properties, and why they are here¶
| Property | |
|---|---|
Visible |
whether this plot is on the canvas — the eye in the context tree |
Title, XTitle, YTitle |
labels; empty inherits the histogram's own |
LogX, LogY, LogZ |
log scales; z is the colour axis of a 2D plot |
LineColor, LineWidth, FillColor |
ROOT colour indices, 0 for no fill |
DrawOptions |
passed to TH1::Draw — "HIST", "COLZ", "E" |
These are on the plot, not on the histogram, and the boundary is worth stating: how many bins there are is a fact about the measurement, and whether its y axis is logarithmic is a fact about this picture of it. In gcompose they land in the display properties panel for exactly that reason — the same panel that holds a solid's colour, and not the one that holds its size.
Empty means inherit rather than blank, so a plot that has never been styled shows the histogram's own titles instead of an unlabelled axis.
PointSet — x against y¶
A histogram answers how many, at each value. A PointSet answers what went
with what, keeping every pair rather than summing them into bins — which is
the whole reason to use one. Where a muon crossed a probe against how fast it
was going; a calibration against temperature.
| Property | |
|---|---|
Title, XTitle, YTitle |
what the points are of, and what x and y are |
MaxPoints |
stop keeping points past this many; 0 means no cap |
Filled the way a Histogram is — a point at a time from a recipe or a loop, or
all at once from vectors somebody already has — and mutex-guarded for the same
reason: the thing filling it is often not the thing drawing it.
# a point at a time
Points.Add(u, v)
Points.Add(u, v, sigma_v) # with an error bar on y
Points.Add(u, v, sigma_u, sigma_v) # bars both ways
# or all at once
Points.SetPoints(xs, ys)
Points.SetPoints(xs, ys, sigmas)
SetPoints replaces rather than appends — handing over two lists says what
the set is — and refuses lists of different lengths outright. Truncating to
the shorter one would pair x with the wrong y, and that is precisely the plot
nobody can tell is wrong.
Error bars are all-or-nothing: a set has them once anything has been added with
them, and HasErrors() says so. That is what lets a plot choose between bars
and no bars without being told. Only whatever filled the set knows whether
there is an uncertainty, and a set filled without one should not be drawn as
though the uncertainty were zero.
MaxPoints is a guard against a loop that fills without bound. What it refuses
is counted rather than silently missing — GetDroppedCount().
RootScatter — the proxy that draws it¶
Attach a PointSet to its Source port; there is nothing else to configure,
because the set already is two columns of numbers.
| Property | |
|---|---|
MaxDrawn |
at most this many points are drawn — distinct from the set's MaxPoints, which is how many are kept |
DrawOptions |
after the axis letter: "P", "PL" to join, "PZ" for flat-ended bars |
MarkerColor, MarkerStyle, MarkerSize |
20 is a filled circle, 7 a single pixel |
Past MaxDrawn the points are thinned by taking every nth, not the first n.
The first n are the beginning of whatever filled the set, which is a different
population from all of it. The title says when it happened: "(every 10th of
100 points)".
The axis labels come from the set unless the plot overrides them, exactly as a
RootHistogram takes its labels from its Histogram.
Getting from a probe to a scatter¶
A PlaneProbe produces an EventTable — rows of named columns, one per
crossing. That is a Geant readout type, not a plotting one, so the way to a
scatter is the same recipe step that gets from a table to a histogram:
Events = InputPort("Events")
Crossings = OutputPort("Crossings") # a PointSet
def run(ctx):
Crossings.SetPoints(Events.Column("u"), Events.Column("v"))
The canvas¶
A ROOT-canvas pane (Root/QCanvas) draws a grid of plots.
Nothing is plotted until it is switched on. A context may hold a dozen histograms, and a canvas showing all of them by default would be unreadable the moment a second one appeared. So the eye column in the context tree is the control: click it next to a histogram and it appears on the active canvas, click it again and it goes. Selecting a histogram does not show it — clicking through a tree would otherwise rearrange every canvas behind it — it only offers that plot's display properties for editing, which is the reason to select one.
The pane makes a proxy for any plain measurement on demand — a RootHistogram
for a Histogram, a RootScatter for a PointSet — one per pane. That is what lets two canvases show the same histogram with different
scales. A RootHistogram placed in the scene deliberately is used as it is and
not shadowed by a second, so it is shared: somebody made it on purpose.
One pad per plot — but a plot may be several curves. Anything nested under a plottable is drawn into its pad rather than taking one of its own, so the list the canvas works from is the top-level plottables. See groups.
Arranging the plots¶
Two properties in the pane's own settings, under Canvas:
| Property | |
|---|---|
Canvas Rows |
rows of pads |
Canvas Columns |
columns of pads |
One by one is a single plot filling the pane. More pads than visible plots
leaves the spare ones blank. More plots than pads leaves the extra ones
undrawn rather than shrinking everything to fit — a plot too small to read
is not an improvement on an absent one — and GetOverflowCount() says how many
were left out. Both are saved with the layout (gcompose::Viewport class
version 2), so a workspace reopens arranged as it was left.
A group is one pad of several things¶
Two spectra compared are one picture. Side by side in two pads, each autoscaled to itself, a factor of two in rate looks like no change and a shifted peak is something the reader has to measure with a ruler. So plottables can be drawn over each other, and nesting is how you say so:
RootPlotGroup "before and after the iron"
|- RootHistogram --> Histogram "before"
`- RootHistogram --> Histogram "after"
Nothing is wired and no list is kept — putting one plot inside another is the
entire gesture. A RootPlotGroup is a plottable with no data of its own, which
is what makes it the right thing to nest under when neither curve is the main
one; nesting a plot directly inside another plot groups them just the same.
The two eyes¶
| Where | Property | Default | Means |
|---|---|---|---|
| top level | Visible |
off | this has a pad on the canvas |
| inside a group | Included |
on | this is one of the curves in that pad |
The defaults are opposite on purpose. A context may hold a dozen histograms, so nothing gets a pad until you ask; but putting something in a group is already the act of asking for it to be in that plot, and the eye there is for taking one back out. The context panel shows both as the same eye, and the tooltip says which one you are clicking.
What the group decides¶
Everything about the pad, applied after the curves are on it so it wins over
whatever each one brought: Title, XTitle, YTitle, LogX/LogY/LogZ,
and XMin/XMax/YMin/YMax — a maximum at or below the minimum means
"whatever fits", which is the default. Setting a shared y range on the group is
usually the difference between a comparison and two pictures.
Two more, because an overlay has problems a single plot does not:
AutoColorgives each member that has no colour of its own a different one. Three black curves over each other are one black shape. A member whoseLineColoryou have changed keeps it — this fills gaps, it does not override choices.ShowLegenddraws a key naming each member. Nothing is drawn on a pad with one thing on it.
What cannot share a pad¶
Not every plot can be drawn over every other. Each plottable declares an overlay class, the pad takes the class of the first thing drawn on it, and anything that disagrees is left out:
| Class | What it is | Shares a pad with |
|---|---|---|
Curve |
a value against one axis — a 1D RootHistogram |
other curves |
Surface |
a value over two axes, drawn as a map — a 2D RootHistogram |
nothing, itself included |
Points |
individual points in a plane — a RootScatter |
other point sets |
Nothing |
no data: a group, or a plot that cannot build | — |
A scatter and a binned spectrum have different things on their y axis — a
position against a count — so one set of labels cannot be true of both, and
Points and Curve do not mix. A colour map covers its pad, so two surfaces
do not either: the second would not be an overlay, it would just be the second
one. Two scatters share a pad quite happily.
A plot that cannot build answers Nothing rather than the class it would
have had: a RootScatter whose set has no points yet, a RootHistogram whose
AxisX is past the end of its source. Otherwise an empty scatter sitting at
the top of a group would classify the pad as points and every spectrum below
it would be refused from its own plot. The pad takes its class from the first
thing that actually draws.
Refusals are counted, not silent. GetExcludedCount() says how many
members were left out of the last draw and GetPlotError() names them — in
gcompose that is the group's Problem row, next to Members. A pad whose axes
are true of only half of what is on it is a wrong plot that looks like a right
one, which is worse than a pad with less on it.
Something that was refused wants a pad of its own: drag it out of the group,
and Canvas Rows/Canvas Columns put the two side by side.
End to end¶
A plane probe watching a beam, a recipe deciding what the crossings mean, and a histogram of the answer — see A plane probe for the probe end:
Wire it up, switch the eye on next to the histogram, set Canvas Rows to 2 if
there is a second one to compare it with, and run.
If the second one is the same quantity measured somewhere else, it belongs on
the same axes rather than in a second pad: make a RootPlotGroup (Add ▸ Root ▸
RootPlotGroup), drag both plots into it, and switch the group's eye on instead.
If it is a different kind of measurement — a scatter of where they crossed
next to a spectrum of how fast they were going — it needs its own pad, and
Canvas Rows is what puts the two one above the other.
src/Root/examples/01_plots_on_one_pad.cpp does all of that, and is built and
run by the test suite: two spectra before and after a block of iron, a shared y
range, a legend, a scatter that gets refused from their pad and drawn on its
own, and a hit map that gets refused from everything. It writes
01_plots_on_one_pad.gcx, which opens in gcompose with the group already
built.