3
\$\begingroup\$

In the ROOT framework (originally a C++ framework, but python bindings are also supplied), colors are objects (ROOT.TColor) identified with an integer index.

This short class creates a new such color, given the r, g, b values which stores the object inside an int sub-type (to prevent the gc from deleting it).

#colors.py
import ROOT


class Color(int):
    """Create a new ROOT.TColor object with an associated index"""
    __slots__ = ["object"]

    def __new__(cls, r, g, b):
        """Constructor. r, g, b are in [0, 1]"""
        self = int.__new__(cls, ROOT.TColor.GetFreeColorIndex())
        cls.set_rgb(self, r, g, b)
        return self

    def set_rgb(self, r, g, b):
        self.object = ROOT.TColor(self, r, g, b)


black = Color(0, 0, 0)
orange = Color(0.9, 0.6, 0)
sky_blue = Color(0.35, 0.7, 0.9)
bluish_green = Color(0, 0.6, 0.5)
yellow = Color(0.95, 0.9, 0.25)
blue = Color(0, 0.45, 0.7)
vermillion = Color(0.8, 0.4, 0)
reddish_purple = Color(0.8, 0.6, 0.7)

colors = [black, orange, sky_blue, bluish_green,
          yellow, blue, vermillion, reddish_purple]

Usage example:

#draw_histograms.py
from colors import colors
import ROOT

names = "hist 1", "hist 2", "hist 3"
histograms = []
for name, color in zip(names, colors):
    hist = ROOT.TH1D(name, name, 100, -50, 50)
    hist.FillRandom("gaus", 1000)
    hist.SetLineColor(color)
    histograms.append(hist)

histograms[1].Draw()
...

I'm looking for comments on the class itself as well as recommendations regarding making the initialization of all the colors easier.

Further explanation:

The need for this class arose, because ROOT cannot convert a ROOT.TColor object directly to an int. It needs the global color index in functions such as hist.SetLineColor(1).

Otherwise it raises errors such as:

...
  File "test.py", line 43, in plot_2D_stacked
    h.SetLineColor(color)
TypeError: void TAttLine::SetLineColor(short lcolor) =>
    could not convert argument 1 (short int converion expects an integer object)
\$\endgroup\$
  • \$\begingroup\$ I find getting the Python documentation hard, from the C++ docs it'd seem like you could change your code to Color = ROOT.TColor. But you've probably tried that... I hope you get a good answer. \$\endgroup\$ – Peilonrayz Nov 2 '16 at 9:15
  • \$\begingroup\$ @JoeWallis Yes, the python documentation is basically non-existent. But the interface is the same as for C++. This was actually what I tried at first, but while this probably works in C++, in python it complains that it can't convert ROOT.TColor to int. \$\endgroup\$ – Graipher Nov 2 '16 at 11:33
  • \$\begingroup\$ @Joe Wallis the documentation also says you have to define the color index where you want to have your color available. The function GetFreeColorIndex() just ensures that you don't overwrite an existing index. \$\endgroup\$ – Graipher Nov 2 '16 at 14:38
2
\$\begingroup\$

As suggested by @JoeWallis in the comments, you could use the "fast" TColor constructor. This let you simplify the color creation by not having to manually deal with the color number:

import ROOT


class Color(int):
    """Create a new ROOT.TColor object with an associated index"""
    __slots__ = ["object"]

    def __new__(cls, r, g, b, a=1.0):
        """Constructor. r, g, b and a are in [0, 1]"""
        # Explicitly use floats here to disambiguate
        # between the two TColor constructors
        color = ROOT.TColor(float(r), float(g), float(b), float(a))
        self = int.__new__(cls, color.GetNumber())
        self.object = color
        return self

Or you could use plain ROOT.TColor and use color.GetNumber() whenever you need an int instead of a TColor. An other, untested (and probably not working), would be to define an __int__ method:

class Color(ROOT.TColor):
    def __int__(self):
        return self.GetNumber()

If SetLineColor and others accept that, this would be the best alternative.

Depending on the need, it may also be a good idea to define properties to get/set color components, such as:

    @property
    def red(self):
        return self.GetRed()

    @red.setter
    def red(self, red_value):
        self.SetRGB(red_value, self.green, self.blue)

Change self to self.object if you use the int subclass.

Now, as regard to style, you define a bunch of constants whose names should be UPPER_SNAKE_CASE. And I would probably use red, green, blue and alpha instead of one letter variable names, even though in "color" context, these letters are pretty obvious.

\$\endgroup\$
  • \$\begingroup\$ I tried implementing a __int__ method, that didn't work. The other two methods I will have to try. The first I'm not sure about, the second should definitely work but make the interface harder, because I have to call GetNumber every time. \$\endgroup\$ – Graipher Nov 2 '16 at 15:25
  • \$\begingroup\$ @Graipher Thing is that you need to differentiate between TColor(0, 0, 0, 0) and TColor(0, 0, 0, 0) where the first one tries to build a color by providing a number and RGB components and the second tries to use the RGBA "fast" constructor. Adding explicit casts to float will turn those into TColor(0, 0.0, 0.0, 0.0) and TColor(0.0, 0.0, 0.0, 0.0) which should be enough to disambiguate. Will edit that in. \$\endgroup\$ – 301_Moved_Permanently Nov 2 '16 at 15:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.