SciRooPlot
ℹ️ Overview
- SciRooPlot is a library that simplifies scientific plotting by adding a level of abstraction on top of the mighty ROOT data analysis framework
- It provides an interface to define and organize plots
- These plot definitions can then be generated with a command-line application
⬇️ Installation instructions
SciRooPlot runs on MacOS and Linux. It can be installed either via the package managers Homebrew and Conda, or built from source using the SciRooPlot installer.
To enable all functions of the app, an environment script has to be sourced. It is best to put the source command into your shell startup script .zshrc or .bashrc (NB.: dont use a relative paths here).
After setup, the commands srp and plot are available and you can keep up-to-date by running srp update from time to time. What to change in existing plotting code after an update is listed in api-changes.html.
Homebrew
brew tap SciRooPlot/scirooplot
brew install SciRooPlot
source "$(brew --prefix scirooplot)/share/scirooplot/env.sh"
Conda
conda install conda-forge::scirooplot
source "${CONDA_PREFIX}/share/scirooplot/env.sh"
Installer
git clone https://github.com/SciRooPlot/SciRooPlot.git
cd SciRooPlot
./scripts/install.sh
The installer requires that ROOT (6.36.04 or newer) and Boost are installed on your system. These dependencies are best installed using a package manager. Alternatively, ROOT can be built from source, as shown here. Two lightweight dependencies of SciRooPlot (fmt and pybind11) are automatically fetched during install in case they are not present on the system or too old.
🚀 Usage
A new project is created by running:
srp init-cpp <project> [<dir>]
This will create a directory called dir (or project if no folder name was specified) in your current directory and provide a minimal working setup to start your project.
Alternatively, you can also create a python-based project:
srp init-py <project> [<dir>]
The new project already contains one plot, so you can generate your first plot simply by typing
plot myGroup myFirstPlot
The idea is to adjust the code in the file DefinePlots.cpp (or DefinePlots.py in case of a python project) to define your plots.
The resulting executable will automatically be rebuilt (in case of cpp code) and (if needed) executed before the app generates the requested plot(s).
For python projects, the installation includes type stubs of the module, so editors show the available methods, their arguments and return types (when the editor runs with the environment of env.sh, e.g. started from a shell where it is sourced).
To get started, or to look up how something is done, create a project that contains many commented example plots together with the example data they use:
srp example-cpp <project> [<dir>]
srp example-py <project> [<dir>]
and generate all of its plots with plot '.+' '.+'.
You may have multiple such projects in parallel, and can switch between them via:
srp select <project>
To print the available configuration options run:
srp help
A list of available projects is shown when typing:
srp projects
By default, all internal settings as well as the plot definitions are stored in ~/.SciRooPlot. This can be changed by defining an environment variable called SCIROOPLOT_CONFIG_PATH.
Generally, each of your plots will belong to a group and is accessed by the plotting app as follows:
plot <group> <name> [<mode>]
For bash and zsh this program provides an auto-completion feature, which means you can tab through the available commands, groups, and plot names.
Regular expressions are supported for the options <group> and <name>.
This allows the creation of multiple plots matching the specified pattern in a single request.
For example plot paperPlots .+ would generate all plots defined within the group called paperPlots and plot thesisFigures moneyPlot[1,2] creates moneyPlot1 and moneyPlot2 from the group thesisFigures.
To specify multiple text options one can use plot analysisQA trackProperty_(tight|loose|nominal)CutSetting.
Note: in bash (unlike zsh), patterns containing (, ), or | must be quoted, e.g. plot analysisQA 'trackProperty_(tight|loose|nominal)CutSetting' — bash treats those characters as shell syntax rather than pattern text.
A group always includes its subgroups, so plot myGroup .+ also creates the plots in myGroup/QAPlots.
To select only a subgroup within the group, use <group/some/subgroup> (for example plot myGroup/QAPlots controlObservable), and to exclude the subgroups, end the group with $ (for example plot 'myGroup$' .+).
By default, the optional mode argument is set to show and you can leave it out in the command.
Possible alternatives are: spread, list, print, pdf, eps, svg, png, gif, macro, file, data.
If you have multiple plots (e.g. myPlot_bin_1, myPlot_bin_2,..) that you want to concatenate and save as a moving gif, you can create it via plot myGroup myPlot_bin_.+ gif.
To adjust the time between the frames use for example plot myGroup myPlot_bin_.+ gif+4, where the number is given in tens of milliseconds (i.e. this example will create a gif with a delay of 40ms between the plots).
For the modes that save files to disk, the plotting app needs to know where to store the output. This can be configured via:
srp set <project> outdir </path/to/output/folder>
Within this folder, the files will be organized in subdirectories corresponding to the groups and subgroups.
Besides program and outdir, you can store your own project variables (for example the location of your input files on the current machine):
srp set <project> inputDir </path/to/inputs>
and read them in your code via pm.GetProjectProperty("inputDir") (the plot manager has to be constructed with the project name).
An existing program can be registered as a project via srp add <project> </path/to/program> [</path/to/output/folder>].
If the program lives in a CMake build directory, it is rebuilt automatically before plotting, just like the projects created by srp init-cpp.
In interactive (show) mode, one can browse through the requested plots using the keys ‘s’ (right) and ‘a’ (left) or by double-clicking on the right and left side of the plot, respectively; after the last plot comes the first one again.
The key ‘d’ detaches the current plot: it moves into a window of its own, placed one step to the right and down from the browsing window and kept in front, while the browsing window shows the plot seen before it, as when the top card is taken off a stack (the next plot if there was none); browsing then continues from there, so the detached plot can be compared with any other. On the last plot left in the browsing window, ‘d’ does nothing.
In spread mode, all requested plots open at once, each in its own window (placed as a cascade, each opening as soon as it is ready).
In both modes, ‘q’ closes the window it is pressed in and ‘x’ closes all of them; the program ends when the last window is closed.
The keys work in the active window while the mouse pointer is inside it, and also in the terminal plot runs in (‘s’, ‘a’, ‘d’, and ‘q’ or ‘x’ to close all windows).
🤖 For AI agents
A condensed description of the workflow, the API and common pitfalls for coding agents is kept as an agent skill (with reference files generated from the headers). To use it, import scirooplot.skill into your agent, or point the agent to llms.txt.
📖 Commented code examples
The following example code snippets with comments illustrate how plots are defined within your SciRooPlot user code. Since these examples are not comprehensive, to see all the functionality of SciRooPlot also have a look at the available setters in the corresponding header files.
#include "SciRooPlot/PanelLayout.h"
#include "SciRooPlot/PlotManager.h"
using namespace SciRooPlot;
// the plot manager is the central entity that handles your plots
PlotManager pm;
// first you need to define the data sources that should be available for your plots
pm.AddDataSource("dataSourceA", {"/path/to/file/a.root", "/path/to/file/a2.root"});
// instead of adding the whole root file, you can also specify a sub-list or sub-folder within the file:
pm.AddDataSource("dataSourceB", {"/path/to/file/b.root:sub/list/or/dir"});
// the paths may contain any environment variable defined in your shell:
pm.AddDataSource("dataSourceC", {"${HOME}/myRootFiles/b2.root"});
// and it is possible to add all root files within a directory (including sub-directories):
pm.AddDataSource("dataSourceD", {"/path/to/folder/with/rootfiles/"});
// wildcards (*, ?, [...]) select all matching files and/or all matching folders within the files, e.g. all time frames of all AO2D files:
pm.AddDataSource("dataSourceE", {"/path/to/AO2D_*.root:DF_*"});
// a data source is one dataset made of its inputs: files (in the order they were added, files found in a directory or via wildcards
// in alphabetical order) or folders within files (each folder matching a wildcard is one input, in the order they appear in the file)
// - within each input, the first match of the requested name counts (the directory structure is traversed recursively)
// - trees found in several inputs are chained (processed together), all other objects are taken from the first input containing them
// data that should not be merged (e.g. train and test samples) therefore belongs into separate data sources
// now that we know where to look for the data, we can start creating plots
// each plot will be handed over to the manager after it was defined
{ // -----------------------------------------------------------------------
// create the Plot object
Plot plot("myPlot1", "myGroup"); // plots are organized in user defined groups
// optionally you can also define subgroups within a group
plot.AppendGroup("QA-Plots/closureTests");
// you always have to specify height and width of the plot:
plot.SetDimensions(710, 710);
plot.SetTransparent(); // for all other settings see the class definition in inc/Plot.h
// a plot consists of multiple pads, which you can access via the [] operator (counting starts at '1'):
plot[1].SetPosition(0., 0., 1., 1.); // specifies a pad that expands over the whole plot
plot[1].SetFrameFill(kRed+2);
// ... see definition of Pad class in inc/Plot.h for the list of all accessors
// you can also define default settings for ALL pads of the plot by using the padID=0, e.g.:
plot[0].SetMargins(0.07, 0.14, 0.12, 0.07);
plot[0].SetDefaultMarkerSize(1.2).SetDefaultLineWidth(2.);
// as you can see here the user interface is designed such that each setter returns
// a reference to the object that is currently being modified. This allows you to concatenate
// all the settings belonging to a pad. This design pattern is consistently used also for all
// the other user definable objects (Data, Axis, Legends, etc.).
// now set text properties, that will be used everywhere in the pad if not overridden for a specific label/title/etc..
plot[1].SetDefaultTextFont(43);
plot[1].SetDefaultTextSize(24);
// finally we can start adding data to the plot
plot[1].AddData("histName1", "dataSourceA", "myLabel");
// this will search for a histogram, graph or function called "histName1" in all files specified for "dataSourceA"
// within an input, the current folder is searched first; if the name is not there, the sub-folders and lists are
// searched one after the other in the order they are stored, each one completely (including its own sub-folders)
// before the next: e.g. folderA/sub/histName1 is found before folderB/histName1
// if a name exists more than once, add the path to choose the one you mean:
plot[1].AddData("folder1/histName2", "dataSourceA", "myLabel2");
plot[1].AddData("folder2/histName2", "dataSourceA", "myLabel3");
// by default the global coordinate system of the plot is defined by the first data that is added (equivalent to how ROOT does it)
// however, every so often we want to plot first some data with a small range and on top of it some data with a larger range
// while allowing the plot to show the full range of the second data
// this can be achieved by explicitly stating that the second data should be the one that defines the axis frame of the plot:
plot[1].AddData("data2", "dataSourceA").SetDefinesFrame();
// it is possible to specify in the labels that you want to include some meta info of the data that is drawn, e.g.:
plot[1].AddData("histName1", "dataSourceA", "mylabel avg = <mean>");
// possible options are: <name>, <title>, <entries>, <integral>, <maximum>, <minimum>, <mean>
// (the numbers cover the drawn range, <entries> is the stored total)
// (for graphs: <name>, <title>, <entries> = number of points, <maximum> and <minimum> of the y values)
// and, from the definition of the data: <source> (data source), <projection> (what is filled, as in TTree::Draw:
// "x", "y:x", "prof. y:x", weight in parentheses) and <selection> (filters, entries range, ranges of histogram projections);
// histogram projections name the axes by the axis titles of the input, e.g. "0 < centrality < 10" (x, y, z if a title is empty),
// tree requests by the variables; e.g. to tell apart projections of the same tree that differ only in the filter: "<name> <selection>"
// you can use the standard printf style to specify how these numbers shall be formatted:
plot[1].AddData("histName1", "dataSourceA", "mylabel avg = <mean[.2f]>");
plot[1].AddData("histName2", "dataSourceA", "mylabel sum = <integral[.2e]>");
// ROOT's statistics box of a histogram (letters as in TStyle::SetOptStat, here entries, mean and std dev with their errors),
// the title is optional (its first line, may contain the placeholders above); the boxes are placed automatically like legends
plot[1].AddData("histName1", "dataSourceA").SetShowStats("eMR", "my histogram");
// its placement and appearance can be set like those of legends and texts (by default it is placed automatically)
plot[1].AddData("histName2", "dataSourceA").SetShowStats("eM").SetStatsBox(StatsBox().SetAutoPlacement(top_left).SetBorderColor(kGray + 1));
// now lets add another piece of input data from the second group (this time without adding a label to the legend)
plot[1].AddData("histName3", "dataSourceB");
// you can also simply add the ratio of two input data
plot[1].AddRatio({"histName3", "dataSourceB"}, {"histName1", "dataSourceA"}, "ratioLabel");
// for incompatible data (e.g. different number of bins or bin limits) the framework will try to use splines to interpolate the data points before dividing
// in case numerator and denominator are sub-samples of one another binomial error propagation can be applied:
plot[1].AddRatio({"histName3", "dataSourceB"}, {"histName1", "dataSourceA"}, "ratioLabel").SetIsCorrelated();
// without a drawing option the same rule applies to histograms and graphs: data with a fill is drawn filled (hist; graphs: area),
// data with a marker style or size set, or with errors, as points (histograms without stored errors have sqrt(N) errors),
// other data as hist (graphs: line); 2d histograms, 2d graphs and 2d functions are drawn as colz, with one colour axis covering all 2d data of the pad
// to modify how the data is displayed we can apply the settings via:
plot[1].AddData("histName4", "dataSourceB").SetDrawOption("HIST C").SetLine(kGreen+2, kSolid, 3.);
// instead of directly using the ROOT drawing option string ("HIST C") you can
// use pre-defined human readable options like curve, points, points_line, etc
// (you can find all available options in include/SciRooPlot/Plot.h and their ROOT equivalents in src/PlotPainter.h):
plot[1].AddData("graphName1", "dataSourceA").SetDrawOption(points).SetMarker(kRed, kFullCircle, 1.);
// all root layout settings can be applied in this manner
// the line, marker and fill stored with the input object are replaced by the defaults of the pad; to keep them (what the data sets still wins):
plot[1].AddData("histName5", "dataSourceB").SetKeepInputStyle();
// (see definition of Data class in inc/Plot.h for the list of all accessors)
// now add a legend containing the labels that were specified when adding the data
plot[1].AddLegend(0.5, 0.8);
// if you leave out the xy pad coordinates, the framework will try to auto-place the legend without overlapping your data or the axes
// (as close as possible to the corner of the frame where it fits best, preferring top left, top right, bottom right, bottom left)
plot[1].AddLegend(); // same as plot[1].AddLegend(best_corner);
// to place it as close as possible to a specific corner of the frame specify top_left, top_right, bottom_right or bottom_left
plot[1].AddLegend(top_right);
// it is possible to add multiple legends to the plot (as done above) which are then identified by the order they were added
// in order to express that the label for a specific data should be put in the second legend you can do the following:
plot[1].AddData("function1", "dataSourceA", "my label in second legend").SetLegend(2);
// the styles and colors of the respective legend entries are by default based on the data that is represented
// you can however override this and specify different default settings for the legend
plot[1].GetLegend(2).SetDefaultLineWidth(5.);
// the row of a data item is styled on the data (its label is the one of the data):
plot[1].AddData("band", "dataSourceA", "uncertainty").SetLegendEntry(LegendEntry().SetSymbolElements("F"));
// rows of their own, e.g. for a legend explaining the symbols and one explaining the colours, or a legend in another pad
// (with an empty label: an empty line); SetRefData(n) takes the symbol from data n of the pad
auto& legend = plot[1].AddLegend();
legend.AddEntry("pp").SetSymbolElements("P").SetMarker(kBlack, kFullCircle, 1.2);
legend.AddEntry("Pb-Pb").SetSymbolElements("P").SetMarker(kBlack, kOpenSquare, 1.2);
// they come after the rows of the data, or at a place relative to the data of the pad (numbered as in plot[1](n)),
// whether or not that data has a row in this legend: a heading belongs to the data below it, a note to the data above it
legend.AddEntry("#bf{2024 data}").AtTop();
legend.AddEntry("#bf{simulation}").BeforeData(4);
legend.AddEntry("(scaled by 10)").AfterData(5);
// (again please have a look at inc/Plot.h for the list of possibilities)
// similar to the legend boxes you can add a text box to your plot
plot[1].AddText({"my important description", "spanning over two lines"});
// or as close as possible to a corner of the frame
plot[1].AddText(top_left, "my important description");
// or
plot[1].AddText(0.2, 0.9, {"this is", "where I", "want my text"});
// the lines of a text are given as a list (in Python: ["this is", "where I", "want my text"]) or separated by newlines
plot[1].AddText(0.2, 0.9, "this is\nwhere I\nwant my text");
// pro tip: to determine the perfect position of the legend or text,
// you can place it at the desired position in interactive mode
// and double-click on it to print out its current coordinates
// to modify the axis titles, offsets, ranges, etc of a pad, just do the following
plot[1]['Y'].SetTitle("my y axis title with meaning");
plot[1]['X'].SetRange(1.4, 2.8).SetTitle("what I am showing on x axis").SetLog();
// or if you want to modify synchronously the axes of multiple plots (e.g. for 2-panel ratios) you can apply the settings to the default pad
plot[0]['X'].SetRange(1.4, 2.8);
// the base plots drop a label whose text would reach beyond the pad (e.g. the top label of a ratio pad, at the edge shared with the pad above); to keep such labels:
plot[2]['Y'].SetHideClippedLabels(false);
// a full list of axis property accessors can be found in the Axis class definition in inc/Plot.h
// note that you can define the visible range of the data independent of the pad axis range
// (ranges stored with the input histograms or graphs are ignored, except for data marked with SetDefinesFrame(), whose stored
// axis ranges, minimum and maximum define the frame; functions from inputs are drawn where they are defined)
plot[1].AddData("hist", "dataSourceA", "hist with reduced range").SetRangeX(1.5, 2.7);
// functions defined in the plot: a formula of x, y, z or a C++ lambda given as text, with x[0], x[1], x[2] as variables
// (it has no parameters to set: write the values into the text); SetNpx sets the number of points a function is drawn with
plot[1].AddFunction("sin(x)/x", "formula");
plot[1].AddFunction("[](double* x, double* p) { return (x[0] < 1.) ? 1. : 1. / x[0]; }", "lambda").SetNpx(2000);
// any C++ or python callable becomes a function via a data source, written as its values at npx points of its range:
// TF1 myFunc("myFunc", [&](double* x, double* p) { return myCalculation(x[0]); }, 0., 10., 0); myFunc.SetNpx(500);
// pm.AddDataSource("myFunctions", &myFunc); plot[1].AddData("myFunc", "myFunctions");
// the added data can be further modified later by accessing it via its index (starting with 1) in the following ways:
plot[1].GetData(2).SetColor(kGreen); // A: explicitly use getter
plot[1](2).SetColor(kRed); // B: use the () operator
// finally, after plot definition is done we can add it to the manager
pm.AddPlot(plot);
} // -----------------------------------------------------------------------
// we could now add plenty of more plots to the manager....
{ // -----------------------------------------------------------------------
//Plot plot("myPlot2", "myGroup/mySubgroup");
// ...
// ...
// pm.AddPlot(plot);
} // -----------------------------------------------------------------------
// it is also possible to construct a plot based on a copy of another
{ // -----------------------------------------------------------------------
Plot plot("myPlot", "myGroup2/my/sub/group");
// ...
plot[1].AddData("folder1/histName2", "dataSourceA", "myLabel2");
//...
// now we want to have exact same plot, but with some additional data points
// this can be helpful to avoid code duplication
Plot plot2(plot, "myPlot2", "myGroup2");
plot2[1].AddData("graphName1", "dataSourceB", "more data");
// now add both to the manager
pm.AddPlot(plot);
pm.AddPlot(plot2);
} // -----------------------------------------------------------------------
// in case you are dealing with {2,3,n}-dimensional input data, it is also possible
// to book only projections of the data for your plot
{ // -----------------------------------------------------------------------
Plot plot("myProjectionsIllustration", "myGroup2");
// projection of 2d data on X axis
plot[1].AddData("my2dhist1", "dataSourceA").ProjectX();
// projection of 2d data on X axis but with restricted Y range:
plot[1].AddData("my2dhist2", "dataSourceA").ProjectX(yBin, yBin);
// the same works also in user coordinates:
plot[1].AddData("my2dhist3", "dataSourceA").ProjectX(yValue, yValue, true);
// for arbitrary-dimensional data (also the 2d histograms above) you can use the more generic function
plot[1].AddData("myNdhist3", "dataSourceA").Project({3}, { {0, 5, 10}, {1, 90, 100} });
// here the first argument is a vector of dimensions you want to project on
// for instance {2,0} would produce a projection of your n-dimensional input histogram where
// the third original axis will be the new x-axis and the first original axis will be the y axis of your plotted data
// with the second argument of the Project() function you can specify the ranges of the other axes prior to the projection
// this means in the above example the axis 0 of the input data is restricted to bin 5 to 10 and axis 1 is restricted to bin 90 to 100
// before the histogram is projected on the axis 3
// also here one can provide user coordinates instead of bins by setting the third argument of the function to 'true'
// in user coordinates every bin overlapping the range is taken: {1, 90., 100.} on bins of width 10 is exactly the bin from 90 to 100
// the same can be done also with the numerator and denominator of ratios
// have a look at inc/Plot.h for the details
pm.AddPlot(plot);
} // -----------------------------------------------------------------------
// as you know from your own plotting experience
// there are plenty of settings that have to be applied to generate a beautiful plot
// most of these settings e.g. the number and alignment of pads, background colors, transparency, etc..
// stay the same for all (or a group of) your plots and you do not want to
// verbosely specify them again and again for every one of your plots
// therefore the framework provides the option to specify base plots
// these are just ordinary plots, but you can use their settings as a baseline
// and the properties specified for your final plot will be applied on top of these
// you have to be added to the manager via pm.AddBasePlot(plot);
// base plots define the layout only: data added to a base plot is ignored (with a warning)
// for instance the following lines will generate an example base plot:
{ // -----------------------------------------------------------------------
Plot plot("myBasePlot");
plot.SetDimensions(710, 710, true);
plot.SetTransparent();
plot[0].SetFrameFill(10, 1001);
vector<int16_t> goodColors = {kBlack, kBlue + 1, kRed + 1};
plot[0].SetDefaultMarkerColors(goodColors);
plot[0].SetDefaultLineColors(goodColors);
plot[0].SetDefaultFillColors(goodColors);
plot[0].SetDefaultFillStyles({0});
plot[0].SetDefaultMarkerStyles({kFullCircle});
plot[0].SetDefaultLineStyles({kSolid});
plot[0].SetDefaultTextFont(43);
plot[0].SetDefaultTextSize(24);
plot[0].SetDefaultMarkerSize(1.);
plot[0].SetDefaultLineWidth(3.);
plot[0].SetTransparent();
plot[0].SetMargins(0.07, 0.14, 0.12, 0.07);
plot[0]['X'].SetTitleSize(28).SetTitleOffset(1.1).SetOppositeTicks().SetMaxDigits(3).SetNoExponent();
plot[0]['Y'].SetTitleSize(28).SetTitleOffset(1.5).SetOppositeTicks().SetMaxDigits(3);
plot[1].SetPosition(0., 0., 1., 1.);
pm.AddBasePlot(plot);
} // -----------------------------------------------------------------------
// SciRooPlot provides base plots with consistent layout among them: text sizes, tick lengths, distances,
// line widths and marker sizes are the same in every panel, so that plots of different layouts look alike
// (on screen, in the pdf and in bitmap files). The name describes the layout:
// <type>[_wide|_tall][_<columns>x<rows>][_ratio][_gap]
// type 1d (plain panels) or 2d (panels with a colour scale); wide or tall panels (golden ratio); a grid of
// panels that share their axes (default 1x1); a ratio panel below each panel; gaps between independent panels.
// Pads are numbered row by row. For example:
for (const string& name : {"1d", "1d_ratio", "2d", "1d_wide", "1d_2x1", "1d_3x1", "1d_2x1_ratio", "1d_2x2", "2d_2x1"}) {
pm.AddBasePlot(PlotManager::MakeBasePlot(name));
}
// modifications to these base plots can be applied in the following way
auto basePlot = PlotManager::MakeBasePlot("1d");
basePlot[1]['X'].SetColor(kRed);
pm.AddBasePlot(basePlot);
// other text sizes, line widths, margins etc. are given as the lengths of a PanelLayout (in pixels of the plot, see PanelLayout.h);
// they are converted for every pad, so they stay the same in all panels. Setting a text size on the pads of a base plot
// afterwards does not do that: each pad holds its own size relative to its dimensions.
auto smallText = PlotManager::MakeBasePlot("1d_ratio", PanelLayout().SetTextSize(24).SetTitleSize(30));
smallText.SetName("1d_ratio_small");
pm.AddBasePlot(smallText);
// Layouts beyond this rule (e.g. with a cell for texts, or panels of different sizes) are made with PanelLayout.h,
// as in PlotManager::MakeBasePlot itself: a PanelLayout arranges panels in rows (sizes in units of the standard panel)
// and carries the lengths of the plot in pixels of the canvas; the plot made from it has these lengths in every panel.
// Panels that touch share the axis between them, Gap() separates independent panels, Empty() leaves a cell without
// axes (a pad for texts), Panel().ZAxis() adds room for the colour scale. This is the essence of MakeBasePlot:
{ // -----------------------------------------------------------------------
// the arrangement of the panels, in units of the standard panel (here: 1d_2x1_ratio)
const vector<Panel> mainRow = {Panel(), Panel()};
const vector<Panel> ratioRow = {Panel(1, 0.37), Panel(1, 0.37)};
const vector<vector<Panel>> rows = {mainRow, ratioRow};
// the sizes shared by all base plots, in pixels of the canvas (these are the defaults of PanelLayout)
PanelLayout sizes;
sizes.SetTextSize(31.52).SetTitleSize(39.4).SetLineWidth(5).SetMarkerSize(1.4);
// the look shared by all base plots: fonts, colours, markers and axis conventions
Plot plot("1d_2x1_ratio", PanelLayout(sizes, rows));
plot.SetTransparent();
plot[0].SetTransparent();
plot[0].SetFrameFill(10, 1001);
plot[0].SetDefaultTextFont(42);
plot[0].SetDefaultColors({kBlack, kBlue + 1, kRed + 1, kYellow + 1, kMagenta - 4, kGreen + 3, kOrange + 1, /* ... */});
plot[0].SetDefaultMarkerStyles({kFullCircle});
plot[0].SetDefaultLineStyles({kSolid});
plot[0].SetDefaultFillStyles({0});
plot[0]['X'].SetOppositeTicks().SetMaxDigits(3).SetNoExponent();
plot[0]['Y'].SetOppositeTicks().SetMaxDigits(3);
plot[0]['Z'].SetMaxDigits(3);
// conventions of the ratio pads (pads 3 and 4: row by row)
for (const uint8_t ratioPad : {3, 4}) {
plot[ratioPad].SetRefFunc("1");
plot[ratioPad]['Y'].SetNumDivisions(305).SetTitleCenter();
}
pm.AddBasePlot(plot);
} // -----------------------------------------------------------------------
// once this is done, you can define plots building up on these base plots
// by specifying their name as the third argument in the new plots' constructor:
{ // -----------------------------------------------------------------------
Plot plot("test1d_ratio", "myGroup", "1d_ratio");
plot[1].AddData("graph2", "dataSourceA", "5 TeV #Delta = #sqrt{s}")
.SetMarker(kRed, kFullCircle, 1.2).SetMaxRangeX(40);
plot[1].AddData("func3", "dataSourceB", "ratio")
.SetMarker(kBlack, kFullCircle, 1.2).SetMaxRangeX(70);
plot[2].AddRatio({"invMassSpec", "dataSourceA"}, {"momentUnfolded1", "pp_5TeV"}, "ratio")
.SetMarker(kRed, kFullCircle, 1.2)
.SetMaxRangeX(60);
plot[1].AddLegend(0.,0.9);
plot[2].AddLegend(0.,0.9);
plot[0]['X'].SetRange(20, 70).SetTickOrientation("+-");
pm.AddPlot(plot);
} // -----------------------------------------------------------------------
// as you can see the plot definition becomes simpler now since we
// do not have to specify again all of the layout overhead and can focus on the content
// text sizes can be changed on three levels: for a class of plots in the base plot (PanelLayout, see above),
// for one plot per pad or per axis, and for one legend or text box. With the default font 42 a size is a fraction
// of the pad (the same number is smaller in a smaller pad), with font 43 it is given in pixels of the plot.
{ // -----------------------------------------------------------------------
Plot plot("test1d_sizes", "myGroup", "1d");
plot[1].AddData("graph2", "dataSourceA", "5 TeV");
plot[1].SetDefaultTextSize(0.05); // labels, legends and texts of pad 1 (plot[0]: all pads)
plot[1]['Y'].SetTitleSize(0.06).SetLabelSize(0.045); // one axis
plot[1].AddText(0.2, 0.9, "preliminary").SetTextFont(43).SetTextSize(20); // one box, in pixels
plot[1].AddLegend().SetTextSize(0.04);
pm.AddPlot(plot);
} // -----------------------------------------------------------------------
// an inset (e.g. a zoom into a region) is simply an extra pad placed inside the frame of pad 1;
// positions are fractions of the plot, pads are drawn in the order of their numbers, so the inset
// covers what lies below it, and since text sizes are fractions of the pad they have to be set for it
{ // -----------------------------------------------------------------------
Plot plot("test1d_inset", "myGroup", "1d");
plot[1].AddData("graph2", "dataSourceA", "5 TeV");
plot[1].AddLegend(0.2, 0.9);
plot[2].SetPosition(0.5, 0.5, 0.88, 0.88).SetMargins(0.05, 0.22, 0.3, 0.05).SetDefaultTextSize(0.09);
plot[2].AddData("graph2", "dataSourceA"); // no label: no legend row
plot[2]['X'].SetRange(20, 30).SetNumDivisions(505).SetTitleSize(0.1).SetLabelSize(0.08).SetHideClippedLabels(false);
plot[2]['Y'].SetRange(0, 5).SetNoExponent().SetNumDivisions(505).SetTitleSize(0.1).SetLabelSize(0.08).SetTitleOffset(1.4).SetHideClippedLabels(false);
pm.AddPlot(plot);
} // -----------------------------------------------------------------------
// for re-occurring data it can make sense to define a bunch of properties only once
// and then just reuse this layout for different plots:
using Plot::Pad::Data;
Data pp_5TeV = Data().SetDrawOption(curve).SetLineStyle(9).SetColor(kGreen+3);
// or if you want to logically group multiple layouts put them in a map:
enum layoutID : int { data, mc, };
using data_layout_t = map<layoutID, Data>;
data_layout_t pp_7TeV
{
{data, Data()
.SetDataSource("pp_7TeV")
.SetDrawOption(points)
.SetMarkerStyle(kFullCircle)
.SetColor(kBlue+2)
},
{mc, Data()
.SetDataSource("pp_7TeV_MC")
.SetDrawOption(curve)
.SetLineStyle(kSolid)
.SetColor(kRed+1)
},
};
{ // -----------------------------------------------------------------------
Plot plot("test1d", "myGroup", "1d");
plot[1].AddData("graph2", "dataSourceA").SetLayout(pp_5TeV);
// a Data object can also be used as template: calling it with a name gives the data with all its properties
// (appearance, projections, tree queries, modifiers); the data source can be set in the template or passed along
pp_5TeV.SetDataSource("dataSourceA");
plot[1].AddData(pp_5TeV("graph2"));
plot[1].AddData(pp_7TeV[data]("graph2"), "data");
plot[1].AddData(pp_7TeV[mc]("graph2", "otherMCSource"), "simulation");
// simple data objects can be created and modified in place
plot[1].AddData(Data("graph2", "dataSourceA").SetColor(kBlue));
// ratios are built from two such data objects: their projections and modifiers act on numerator and denominator,
// the ratio takes the appearance of the numerator (adjust it by chaining setters or ApplyLayout)
plot[1].AddRatio(pp_5TeV("graph2"), Data("graph3", "dataSourceA"));
plot[1].AddRatio({"graph2", "dataSourceA"}, {"graph3", "dataSourceA"}).SetColor(kRed); // braces create the data (python: lists)
plot[1].AddLegend(0.,0.9);
pm.AddPlot(plot);
} // -----------------------------------------------------------------------
{ // -----------------------------------------------------------------------
Plot plot("testColorGradients", "myGroup", "1d");
// some of the data properties (e.g. styles, colors and line widths) we may not want to specify for each of the data separately
// therefore, as you have already seen in the definitions of the base plot above, it is possible to define some default settings per pad
// for instance you can define a list of colors that should be used for drawing the data:
vector<int16_t> goodColors = {kBlack, kBlue+1, kRed+1, kYellow+1};
plot[1].SetDefaultMarkerColors(goodColors);
// this way the first data will be black, the second blue, etc.
// if you add more data to the pad than the length of the defined color vector, it will continue with the first color in the vector
// analogous implementations are available for marker, line or fill styles
plot[1].SetDefaultMarkerStyles({kOpenCircle, kOpenCross});
plot[1].SetDefaultLineStyles({kDashed, kSolid});
// when drawing a lot of data to the plot, the color vector might become a bit lengthy
// in these cases we often want a continuous color spectrum between some color endpoints
// the following example shows how to generate a continuous rainbow color range that spans from blue, cyan, green and yellow to red
vector<tuple<float_t, float_t, float_t, float_t>> rainbowColors =
{ {0., 0., 1., 0.}, // blue
{0., 1., 1., 0.25}, // cyan
{0., 1., 0., 0.50}, // green
{1., 1., 0., 0.75}, // yellow
{1., 0., 0., 1.} // red
};
// here the first three numbers in the tuple correspond to rgb values (between 0. and 1.) of a color endpoint
// while the last number specifies the 'location' of the respective point in the color range to be defined (again between 0. and 1.)
plot[1].SetDefaultMarkerColors(rainbowColors);
plot[1].SetDefaultLineColors(rainbowColors);
plot[1].AddData("hist1", "dataSourceA");
plot[1].AddData("hist2", "dataSourceA");
plot[1].AddData("hist3", "dataSourceA");
plot[1].AddData("hist4", "dataSourceA");
plot[1].AddData("hist5", "dataSourceA");
// you can also use this feature to define a palette for 2D histograms:
plot[1].SetPalette(rainbowColors);
// NB.: in case of 3D drawing options like LEGO you can also define the perspective on the plot
plot[1].SetView(45, 20); // theta=45 and phi=20
pm.AddPlot(plot);
} // -----------------------------------------------------------------------
// it is also possible to process and display tabled data, i.e. root trees or CSV files
// (a tree found in several inputs of a data source, e.g. one file per run or one folder per time frame, is chained over all of them)
{ // -----------------------------------------------------------------------
Plot plot("treePlot", "myTreeExamples", "1d");
// 1D projection of a tree/table on leaf/column called "pt"
plot[1].AddData("mytree", "input").Project1D("pt");
// the above automatically infers the axis ranges and uses a default of 100 equidistant bins
// it can be modified as follows:
plot[1].AddData("mytree", "input").Project1D({"pt", 200}); // 200 equidistant bins
plot[1].AddData("mytree", "input").Project1D({"pt", 200, {0.1, 20.}}); // 200 equidistant bins between 0.1 and 20
plot[1].AddData("mytree", "input").Project1D({"pt", {0.1, 0.5, 0.9, 1.3, 10., 20.}}); // user defined bin edges
// this can simply be extended to 2D projections as follows:
plot[1].AddData("mytree", "input").Project2D({"pt", {0.1, 0.5, 0.9, 1.3, 10., 20.}}, {"eta", 50}); // user defined bin edges for pt and 50 equidistant bins for eta
// NB.: 3D Projections are also possible via the Project() function
// prior to projecting, the data can be filtered:
plot[1].AddData("mytree", "input").Project1D({"pt", 200}).Filter("eta>0"); // 200 equidistant bins in pt filled only with data of positive eta
// also profiles can be made based on the tree, e.g.:
plot[1].AddData("mytree", "input").Profile1D("eta", "pt"); // <pt> vs eta
plot[1].AddData("mytree", "input").Profile1D({"eta", 700}, "pt"); // <pt> vs eta in 700 equidistant bins
plot[1].AddData("mytree", "input").Profile1D({"eta", 700}, "pt").Filter("abs(pz) < 5"); // <pt> vs eta in 700 equidistant bins filled for absolute pz values less than 5
// similarly, 2D profiles can be produced:
plot[1].AddData("mytree", "input").Profile2D("eta", "pz", "pt"); // <pt> vs eta and pz
// scatter plots are also possible:
plot[1].AddData("mytree", "input").Scatter("x", "y").Filter("a<10 && b<=7"); // simple scatter of x and y under condition a<10 and b<=7
// or the same with errors
plot[1].AddData("mytree", "input").Scatter("x", "y", "ex", "ey").Filter("a<10").Filter("z==8"); // simple scatter of x and y under condition a<10 and z == 8
// note that all the table column and filter expressions can be any valid cpp code using the column/leaf names of the input data as arguments
plot[1].AddData("mytree", "input").Scatter("pow(x,2) + pow(y,2)", "2*sqrt(y)").Filter("abs(a)<10");
// this is true not only for the Scatter but also for the Project and Profile functions
plot[1].AddData("mytree", "input").Scatter("x", "y").Entries(100); // select only first 100 entries
plot[1].AddData("mytree", "input").Scatter("x", "y").Entries(800, 1200); // select only entries 800 to 1200
// you may want to define some variables and use them in the expressions and filters
plot[1].AddData("mytree", "input").Scatter("r", "z").Define("r", "sqrt(x*x + y*y)").Filter("r*y<10"); // defnine variable r and use it as point x value but only consider data with r*y<10
// columns of further trees can be added to the rows of a tree with Join (they are then available as 'treeName.column' or just 'column' if unique):
plot[1].AddData("events", "input").Join("scores").Project1D({"scores.score", 50, {0., 1.}}); // row by row: tree 'scores' next to 'events' in each input of the data source (same number of rows)
plot[1].AddData("reco/events", "input").Join("truth/events", {}, "truth").Project1D({"truth.x - x", 50, {-1., 1.}}); // a tree in another folder (the alias 'truth' is needed since both are called 'events')
plot[1].AddData("events", "input").Join({"scores", "otherInput"}).Project1D({"score", 50, {0., 1.}}); // row by row with a tree from another data source (rows matched in the order of the inputs)
plot[1].AddData("tracks", "input").Join({"events", "otherInput"}, {"run", "event"}).Profile1D({"centrality", 10, {0., 100.}}, "pt"); // by key: each track gets the columns of the event with the same 'run' and 'event' values
// keys: at most two integer columns with the same names in both trees, each key may appear only once in the joined tree; rows without a matching row are skipped (their number is shown in the log)
// in case the input data comes from a text file called 'myData.csv' with columns 'a' and 'b', it is accessed as follows:
plot[1].AddData("myData", "input").Scatter("a", "b"); // i.e. the data name is defined as the file name without the file ending .csv
pm.AddPlot(plot);
} // -----------------------------------------------------------------------