Skip to main content
`Ctrl`+`K`

# Source code for silx.gui.plot.ColorBar

    # /*##########################################################################
    #
    # Copyright (c) 2016-2021 European Synchrotron Radiation Facility
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    # THE SOFTWARE.
    #
    # ###########################################################################*/
    """Module containing several widgets associated to a colormap.
    """

    __authors__ = ["H. Payno", "T. Vincent"]
    __license__ = "MIT"
    __date__ = "24/04/2018"

    import logging
    import weakref
    import numpy

    from ._utils import ticklayout
    from .. import qt
    from ..qt import inspect as qt_inspect
    from silx.gui import colors
    from silx.math.colormap import LogarithmicNormalization

    _logger = logging.getLogger(__name__)

    class ColorBarWidget(qt.QWidget):
        """Colorbar widget displaying a colormap

        It uses a description of colormap as dict compatible with :class:`Plot`.

        .. image:: img/linearColorbar.png
            :width: 80px
            :align: center

        To run the following sample code, a QApplication must be initialized.

        >>> from silx.gui.plot import Plot2D
        >>> from silx.gui.plot.ColorBar import ColorBarWidget

        >>> plot = Plot2D()  # Create a plot widget
        >>> plot.show()

        >>> colorbar = ColorBarWidget(plot=plot, legend='Colormap')  # Associate the colorbar with it
        >>> colorbar.show()

        Initializer parameters:

        :param parent: See :class:`QWidget`
        :param plot: PlotWidget the colorbar is attached to (optional)
        :param str legend: the label to set to the colorbar
        """

        sigVisibleChanged = qt.Signal(bool)
        """Emitted when the property `visible` have changed."""

        def __init__(self, parent=None, plot=None, legend=None):
            self._isConnected = False
            self._plotRef = None
            self._colormap = None
            self._data = None

            super(ColorBarWidget, self).__init__(parent)

            self.__buildGUI()
            self.setLegend(legend)
            self.setPlot(plot)

        def __buildGUI(self):
            self.setLayout(qt.QHBoxLayout())

            # create color scale widget
            self._colorScale = ColorScaleBar(parent=self, colormap=None)
            self.layout().addWidget(self._colorScale)

            # legend (is the right group)
            self.legend = _VerticalLegend("", self)
            self.layout().addWidget(self.legend)

            self.layout().setSizeConstraint(qt.QLayout.SetMinAndMaxSize)

        def getPlot(self):
            """Returns the :class:`Plot` associated to this widget or None"""
            return None if self._plotRef is None else self._plotRef()

        def setPlot(self, plot):
            """Associate a plot to the ColorBar

            :param plot: the plot to associate with the colorbar.
                         If None will remove any connection with a previous plot.
            """
            self._disconnectPlot()
            self._plotRef = None if plot is None else weakref.ref(plot)
            self._connectPlot()

        def _disconnectPlot(self):
            """Disconnect from Plot signals"""
            if self._isConnected:
                self._isConnected = False
                plot = self.getPlot()
                if plot is not None and qt_inspect.isValid(plot):
                    plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
                    plot.sigActiveScatterChanged.disconnect(self._activeScatterChanged)
                    plot.sigPlotSignal.disconnect(self._defaultColormapChanged)

        def _connectPlot(self):
            """Connect to Plot signals"""
            plot = self.getPlot()
            if plot is not None and not self._isConnected:
                activeImageLegend = plot.getActiveImage(just_legend=True)
                activeScatterLegend = plot.getActiveScatter(just_legend=True)
                if activeImageLegend is None and activeScatterLegend is None:
                    # Show plot default colormap
                    self._syncWithDefaultColormap()
                elif activeImageLegend is not None:  # Show active image colormap
                    self._activeImageChanged(None, activeImageLegend)
                elif activeScatterLegend is not None:  # Show active scatter colormap
                    self._activeScatterChanged(None, activeScatterLegend)

                plot.sigActiveImageChanged.connect(self._activeImageChanged)
                plot.sigActiveScatterChanged.connect(self._activeScatterChanged)
                plot.sigPlotSignal.connect(self._defaultColormapChanged)
                self._isConnected = True

        def setVisible(self, isVisible):
            qt.QWidget.setVisible(self, isVisible)
            self.sigVisibleChanged.emit(isVisible)

        def showEvent(self, event):
            self._connectPlot()

        def hideEvent(self, event):
            self._disconnectPlot()

        def getColormap(self):
            """Returns the colormap displayed in the colorbar.

            :rtype: ~silx.gui.colors.Colormap
            """
            return self.getColorScaleBar().getColormap()

        def setColormap(self, colormap, data=None):
            """Set the colormap to be displayed.

            :param ~silx.gui.colors.Colormap colormap:
                The colormap to apply on the ColorBarWidget
            :param Union[numpy.ndarray,~silx.gui.plot.items.ColormapMixin] data:
                The data to display or item, needed if the colormap require an autoscale
            """
            self._data = data
            self.getColorScaleBar().setColormap(colormap=colormap, data=data)
            if self._colormap is not None:
                self._colormap.sigChanged.disconnect(self._colormapHasChanged)
            self._colormap = colormap
            if self._colormap is not None:
                self._colormap.sigChanged.connect(self._colormapHasChanged)

        def _colormapHasChanged(self):
            """handler of the Colormap.sigChanged signal"""
            assert self._colormap is not None
            self.setColormap(colormap=self._colormap, data=self._data)

        def setLegend(self, legend):
            """Set the legend displayed along the colorbar

            :param str legend: The label
            """
            if legend is None or legend == "":
                self.legend.hide()
                self.legend.setText("")
            else:
                assert type(legend) is str
                self.legend.show()
                self.legend.setText(legend)

        def getLegend(self):
            """
            Returns the legend displayed along the colorbar

            :return: return the legend displayed along the colorbar
            :rtype: str
            """
            return self.legend.text()

        def _activeScatterChanged(self, previous, legend):
            """Handle plot active scatter changed"""
            plot = self.getPlot()

            # Do not handle active scatter while there is an image
            if plot.getActiveImage() is not None:
                return

            if legend is None:  # No active scatter, display no colormap
                self.setColormap(colormap=None)
                return

            # Sync with active scatter
            scatter = plot.getActiveScatter()

            self.setColormap(colormap=scatter.getColormap(), data=scatter)

        def _activeImageChanged(self, previous, legend):
            """Handle plot active image changed"""
            plot = self.getPlot()

            if legend is None:  # No active image, try with active scatter
                activeScatterLegend = plot.getActiveScatter(just_legend=True)
                # No more active image, use active scatter if any
                self._activeScatterChanged(None, activeScatterLegend)
            else:
                # Sync with active image
                image = plot.getActiveImage()

                # RGB(A) image, display default colormap
                array = image.getData(copy=False)
                if array.ndim != 2:
                    self.setColormap(colormap=None)
                    return

                # data image, sync with image colormap
                # do we need the copy here : used in the case we are changing
                # vmin and vmax but should have already be done by the plot
                self.setColormap(colormap=image.getColormap(), data=image)

        def _defaultColormapChanged(self, event):
            """Handle plot default colormap changed"""
            if event["event"] == "defaultColormapChanged":
                plot = self.getPlot()
                if (
                    plot is not None
                    and plot.getActiveImage() is None
                    and plot.getActiveScatter() is None
                ):
                    # No active item, take default colormap update into account
                    self._syncWithDefaultColormap()

        def _syncWithDefaultColormap(self):
            """Update colorbar according to plot default colormap"""
            self.setColormap(self.getPlot().getDefaultColormap())

        def getColorScaleBar(self):
            """

            :return: return the :class:`ColorScaleBar` used to display ColorScale
                and ticks"""
            return self._colorScale

    class _VerticalLegend(qt.QLabel):
        """Display vertically the given text"""

        def __init__(self, text, parent=None):
            """

            :param text: the legend
            :param parent: the Qt parent if any
            """
            qt.QLabel.__init__(self, text, parent)
            self.setLayout(qt.QVBoxLayout())
            self.layout().setContentsMargins(0, 0, 0, 0)

        def paintEvent(self, event):
            painter = qt.QPainter(self)
            painter.setFont(self.font())

            painter.translate(0, self.rect().height())
            painter.rotate(270)
            newRect = qt.QRect(0, 0, self.rect().height(), self.rect().width())

            painter.drawText(newRect, qt.Qt.AlignHCenter, self.text())

            fm = qt.QFontMetrics(self.font())
            preferedHeight = fm.width(self.text())
            preferedWidth = fm.height()
            self.setFixedWidth(preferedWidth)
            self.setMinimumHeight(preferedHeight)

    class ColorScaleBar(qt.QWidget):
        """This class is making the composition of a :class:`_ColorScale` and a
        :class:`_TickBar`.

        It is the simplest widget displaying ticks and colormap gradient.

        .. image:: img/colorScaleBar.png
            :width: 150px
            :align: center

        To run the following sample code, a QApplication must be initialized.

        >>> colormap = Colormap(name='gray',
        ...                     norm='log',
        ...                     vmin=1,
        ...                     vmax=100000,
        ...             )
        >>> colorscale = ColorScaleBar(parent=None,
        ...                            colormap=colormap )
        >>> colorscale.show()

        Initializer parameters :

        :param colormap: the colormap to be displayed
        :param parent: the Qt parent if any
        :param displayTicksValues: display the ticks value or only the '-'
        """

        _TEXT_MARGIN = 5
        """The tick bar need a margin to display all labels at the correct place.
        So the ColorScale should have the same margin in order for both to fit"""

        def __init__(self, parent=None, colormap=None, data=None, displayTicksValues=True):
            super(ColorScaleBar, self).__init__(parent)

            self.minVal = None
            """Value set to the _minLabel"""
            self.maxVal = None
            """Value set to the _maxLabel"""

            self.setLayout(qt.QGridLayout())

            # create the left side group (ColorScale)
            self.colorScale = _ColorScale(
                colormap=colormap, data=data, parent=self, margin=ColorScaleBar._TEXT_MARGIN
            )
            if colormap:
                vmin, vmax = colormap.getColormapRange(data)
                normalizer = colormap._getNormalizer()
            else:
                vmin, vmax = colors.DEFAULT_MIN_LIN, colors.DEFAULT_MAX_LIN
                normalizer = None

            self.tickbar = _TickBar(
                vmin=vmin,
                vmax=vmax,
                normalizer=normalizer,
                parent=self,
                displayValues=displayTicksValues,
                margin=ColorScaleBar._TEXT_MARGIN,
            )

            self.layout().addWidget(self.tickbar, 1, 0, 1, 1, qt.Qt.AlignRight)
            self.layout().addWidget(self.colorScale, 1, 1, qt.Qt.AlignLeft)

            self.layout().setContentsMargins(0, 0, 0, 0)
            self.layout().setSpacing(0)

            # max label
            self._maxLabel = qt.QLabel(str(1.0), parent=self)
            self._maxLabel.setToolTip(str(0.0))
            self.layout().addWidget(self._maxLabel, 0, 0, 1, 2, qt.Qt.AlignRight)

            # min label
            self._minLabel = qt.QLabel(str(0.0), parent=self)
            self._minLabel.setToolTip(str(0.0))
            self.layout().addWidget(self._minLabel, 2, 0, 1, 2, qt.Qt.AlignRight)

            self.layout().setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
            self.layout().setColumnStretch(0, 1)
            self.layout().setRowStretch(1, 1)

        def getTickBar(self):
            """

            :return: the instanciation of the :class:`_TickBar`
            """
            return self.tickbar

        def getColorScale(self):
            """

            :return: the instanciation of the :class:`_ColorScale`
            """
            return self.colorScale

        def getColormap(self):
            """

            :returns: the colormap.
            :rtype: :class:`.Colormap`
            """
            return self.colorScale.getColormap()

        def setColormap(self, colormap, data=None):
            """Set the new colormap to be displayed

            :param Colormap colormap: the colormap to set
            :param Union[numpy.ndarray,~silx.gui.plot.items.Item] data:
                The data or item to display, needed if the colormap requires an autoscale
            """
            self.colorScale.setColormap(colormap, data)

            if colormap is not None:
                vmin, vmax = colormap.getColormapRange(data)
                normalizer = colormap._getNormalizer()
            else:
                vmin, vmax = None, None
                normalizer = None

            self.tickbar.update(vmin=vmin, vmax=vmax, normalizer=normalizer)
            self._setMinMaxLabels(vmin, vmax)

        def setMinMaxVisible(self, val=True):
            """Change visibility of the min label and the max label

            :param val: if True, set the labels visible, otherwise set it not visible
            """
            self._minLabel.setVisible(val)
            self._maxLabel.setVisible(val)

        def _updateMinMax(self):
            """Update the min and max label if we are in the case of the
            configuration 'minMaxValueOnly'"""
            if self.minVal is None:
                text, tooltip = "", ""
            else:
                if self.minVal == 0 or 0 <= numpy.log10(abs(self.minVal)) < 7:
                    text = "%.7g" % self.minVal
                else:
                    text = "%.2e" % self.minVal
                tooltip = repr(self.minVal)

            self._minLabel.setText(text)
            self._minLabel.setToolTip(tooltip)

            if self.maxVal is None:
                text, tooltip = "", ""
            else:
                if self.maxVal == 0 or 0 <= numpy.log10(abs(self.maxVal)) < 7:
                    text = "%.7g" % self.maxVal
                else:
                    text = "%.2e" % self.maxVal
                tooltip = repr(self.maxVal)

            self._maxLabel.setText(text)
            self._maxLabel.setToolTip(tooltip)

        def _setMinMaxLabels(self, minVal, maxVal):
            """Change the value of the min and max labels to be displayed.

            :param minVal: the minimal value of the TickBar (not str)
            :param maxVal: the maximal value of the TickBar (not str)
            """
            # bad hack to try to display has much information as possible
            self.minVal = minVal
            self.maxVal = maxVal
            self._updateMinMax()

        def resizeEvent(self, event):
            qt.QWidget.resizeEvent(self, event)
            self._updateMinMax()

    class _ColorScale(qt.QWidget):
        """Widget displaying the colormap colorScale.

        Show matching value between the gradient color (from the colormap) at mouse
        position and value.

        .. image:: img/colorScale.png
            :width: 20px
            :align: center

        To run the following sample code, a QApplication must be initialized.

        >>> colormap = Colormap(name='viridis',
        ...                     norm='log',
        ...                     vmin=1,
        ...                     vmax=100000,
        ...             )
        >>> colorscale = ColorScale(parent=None,
        ...                         colormap=colormap)
        >>> colorscale.show()

        Initializer parameters :

        :param colormap: the colormap to be displayed
        :param parent: the Qt parent if any
        :param int margin: the top and left margin to apply.
        :param Union[None,numpy.ndarray,~silx.gui.plot.items.ColormapMixin] data:
            The data or item to use for getting the range for autoscale colormap.

        .. warning:: Value drawing will be
            done at the center of ticks. So if no margin is done your values
            drawing might not be fully done for extrems values.
        """

        _NB_CONTROL_POINTS = 256

        def __init__(self, colormap, parent=None, margin=5, data=None):
            qt.QWidget.__init__(self, parent)
            self._colormap = None
            self.margin = margin
            self.setColormap(colormap, data)

            self.setLayout(qt.QVBoxLayout())
            self.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Expanding)
            # needed to get the mouse event without waiting for button click
            self.setMouseTracking(True)
            self.setMargin(margin)
            self.setContentsMargins(0, 0, 0, 0)

            self.setMinimumHeight(self._NB_CONTROL_POINTS // 2 + 2 * self.margin)
            self.setFixedWidth(25)

        def setColormap(self, colormap, data=None):
            """Set the new colormap to be displayed

            :param dict colormap: the colormap to set
            :param Union[None,numpy.ndarray,~silx.gui.plot.items.ColormapMixin] data:
                Optional data for which to compute colormap range.
            """
            self._colormap = colormap
            self.setEnabled(colormap is not None)

            if colormap is None:
                self.vmin, self.vmax = None, None
            else:
                assert colormap.getNormalization() in colors.Colormap.NORMALIZATIONS
                self.vmin, self.vmax = self._colormap.getColormapRange(data=data)
            self._updateColorGradient()
            self.update()

        def getColormap(self):
            """Returns the colormap

            :rtype: :class:`.Colormap`
            """
            return None if self._colormap is None else self._colormap

        def _updateColorGradient(self):
            """Compute the color gradient"""
            colormap = self.getColormap()
            if colormap is None:
                return

            indices = numpy.linspace(0.0, 1.0, self._NB_CONTROL_POINTS)
            colors = colormap.getNColors(nbColors=self._NB_CONTROL_POINTS)
            self._gradient = qt.QLinearGradient(0, 1, 0, 0)
            self._gradient.setCoordinateMode(qt.QGradient.StretchToDeviceMode)
            self._gradient.setStops(
                [(i, qt.QColor(*color)) for i, color in zip(indices, colors)]
            )

        def paintEvent(self, event):
            """"""
            painter = qt.QPainter(self)
            if self.getColormap() is not None:
                painter.setBrush(self._gradient)
                penColor = self.palette().color(qt.QPalette.Active, qt.QPalette.WindowText)
            else:
                penColor = self.palette().color(
                    qt.QPalette.Disabled, qt.QPalette.WindowText
                )
            painter.setPen(penColor)

            painter.drawRect(
                qt.QRect(
                    0, self.margin, self.width() - 1, self.height() - 2 * self.margin - 1
                )
            )

        def mouseMoveEvent(self, event):
            tooltip = str(
                self.getValueFromRelativePosition(
                    self._getRelativePosition(qt.getMouseEventPosition(event)[1])
                )
            )
            if qt.BINDING == "PyQt5":
                position = event.globalPos()
            else:  # Qt6
                position = event.globalPosition().toPoint()
            qt.QToolTip.showText(position, tooltip, self)
            super(_ColorScale, self).mouseMoveEvent(event)

        def _getRelativePosition(self, yPixel):
            """yPixel : pixel position into _ColorScale widget reference"""
            # widgets are bottom-top referencial but we display in top-bottom referential
            height = float(self.height() - 2 * self.margin)
            if height == 0:
                return 0.0
            return 1.0 - (yPixel - self.margin) / height

        def getValueFromRelativePosition(self, value):
            """Return the value in the colorMap from a relative position in the
            ColorScaleBar (y)

            :param value: float value in [0, 1]
            :return: the value in [colormap['vmin'], colormap['vmax']]
            """
            colormap = self.getColormap()
            if colormap is None:
                return

            value = numpy.clip(value, 0.0, 1.0)
            normalizer = colormap._getNormalizer()
            normMin, normMax = normalizer.apply(
                [self.vmin, self.vmax], self.vmin, self.vmax
            )

            return normalizer.revert(
                normMin + (normMax - normMin) * value, self.vmin, self.vmax
            )

        def setMargin(self, margin):
            """Define the margin to fit with a TickBar object.
            This is needed since we can only paint on the viewport of the widget.
            Didn't work with a simple setContentsMargins

            :param int margin: the margin to apply on the top and bottom.
            """
            self.margin = int(margin)
            self.update()

    class _TickBar(qt.QWidget):
        """Bar grouping the ticks displayed

        To run the following sample code, a QApplication must be initialized.

        >>> bar = _TickBar(1, 1000, norm='log', parent=None, displayValues=True)
        >>> bar.show()

        .. image:: img/tickbar.png
            :width: 40px
            :align: center

        :param int vmin: smaller value of the range of values
        :param int vmax: higher value of the range of values
        :param normalizer: Normalization object.
        :param parent: the Qt parent if any
        :param bool displayValues: if True display the values close to the tick,
            Otherwise only signal it by '-'
        :param int nticks: the number of tick we want to display. Should be an
            unsigned int ot None. If None, let the Tick bar find the optimal
            number of ticks from the tick density.
        :param int margin: margin to set on the top and bottom
        """

        _WIDTH_DISP_VAL = 45
        """widget width when displayed with ticks labels"""
        _WIDTH_NO_DISP_VAL = 10
        """widget width when displayed without ticks labels"""
        _FONT_SIZE = 10
        """font size for ticks labels"""
        _LINE_WIDTH = 10
        """width of the line to mark a tick"""

        DEFAULT_TICK_DENSITY = 0.015

        def __init__(
            self,
            vmin,
            vmax,
            normalizer,
            parent=None,
            displayValues=True,
            nticks=None,
            margin=5,
        ):
            super(_TickBar, self).__init__(parent)
            self.margin = margin
            self._nticks = None
            self.ticks = ()
            self.subTicks = ()
            self._forcedDisplayType = None
            self.ticksDensity = _TickBar.DEFAULT_TICK_DENSITY

            self._vmin = vmin
            self._vmax = vmax
            self._normalizer = normalizer
            self.displayValues = displayValues
            self.setTicksNumber(nticks)

            self.setMargin(margin)
            self.setContentsMargins(0, 0, 0, 0)

            self._resetWidth()

        def setTicksValuesVisible(self, val):
            self.displayValues = val
            self._resetWidth()

        def _resetWidth(self):
            width = self._WIDTH_DISP_VAL if self.displayValues else self._WIDTH_NO_DISP_VAL
            self.setFixedWidth(width)

        def update(self, vmin, vmax, normalizer):
            self._vmin = vmin
            self._vmax = vmax
            self._normalizer = normalizer
            self.computeTicks()
            qt.QWidget.update(self)

        def setMargin(self, margin):
            """Define the margin to fit with a _ColorScale object.
            This is needed since we can only paint on the viewport of the widget

            :param int margin: the margin to apply on the top and bottom.
            """
            self.margin = margin

        def setTicksNumber(self, nticks):
            """Set the number of ticks to display.

            :param nticks: the number of tick to be display. Should be an
                unsigned int ot None. If None, let the :class:`_TickBar` find the
                optimal number of ticks from the tick density.
            """
            self._nticks = nticks
            self.computeTicks()
            qt.QWidget.update(self)

        def setTicksDensity(self, density):
            """If you let :class:`_TickBar` deal with the number of ticks
            (nticks=None) then you can specify a ticks density to be displayed.
            """
            if density < 0.0:
                raise ValueError("Density should be a positive value")
            self.ticksDensity = density

        def computeTicks(self):
            """This function compute ticks values labels. It is called at each
            update and each resize event.
            Deal only with linear and log scale.
            """
            nticks = self._nticks
            if nticks is None:
                nticks = self._getOptimalNbTicks()

            if self._vmin == self._vmax:
                # No range: no ticks
                self.ticks = ()
                self.subTicks = ()
            elif isinstance(self._normalizer, LogarithmicNormalization):
                self._computeTicksLog(nticks)
            else:  # Fallback: use linear
                self._computeTicksLin(nticks)

            # update the form
            font = qt.QFont()
            font.setPixelSize(_TickBar._FONT_SIZE)

            self.form = self._getFormat(font)

        def _computeTicksLog(self, nticks):
            logMin = numpy.log10(self._vmin)
            logMax = numpy.log10(self._vmax)
            lowBound, highBound, spacing, self._nfrac = ticklayout.niceNumbersForLog10(
                logMin, logMax, nticks
            )
            self.ticks = numpy.power(10.0, numpy.arange(lowBound, highBound, spacing))
            if spacing == 1:
                self.subTicks = ticklayout.computeLogSubTicks(
                    ticks=self.ticks,
                    lowBound=numpy.power(10.0, lowBound),
                    highBound=numpy.power(10.0, highBound),
                )
            else:
                self.subTicks = []

        def resizeEvent(self, event):
            qt.QWidget.resizeEvent(self, event)
            self.computeTicks()

        def _computeTicksLin(self, nticks):
            _min, _max, _spacing, self._nfrac = ticklayout.niceNumbers(
                self._vmin, self._vmax, nticks
            )

            self.ticks = numpy.arange(_min, _max, _spacing)
            self.subTicks = []

        def _getOptimalNbTicks(self):
            return max(2, int(round(self.ticksDensity * self.rect().height())))

        def paintEvent(self, event):
            painter = qt.QPainter(self)
            font = painter.font()
            font.setPixelSize(_TickBar._FONT_SIZE)
            painter.setFont(font)

            # paint ticks
            for val in self.ticks:
                self._paintTick(val, painter, majorTick=True)

            # paint subticks
            for val in self.subTicks:
                self._paintTick(val, painter, majorTick=False)

        def _getRelativePosition(self, val):
            """Return the relative position of val according to min and max value"""
            if self._normalizer is None:
                return 0.0
            normMin, normMax, normVal = self._normalizer.apply(
                [self._vmin, self._vmax, val], self._vmin, self._vmax
            )

            if normMin == normMax:
                return 0.0
            if not numpy.isfinite(normVal):
                return 0.0
            return 1.0 - (normVal - normMin) / (normMax - normMin)

        def _paintTick(self, val, painter, majorTick=True):
            """

            :param bool majorTick: if False will never draw text and will set a line
                with a smaller width
            """
            fm = qt.QFontMetrics(painter.font())
            viewportHeight = self.rect().height() - self.margin * 2 - 1
            relativePos = self._getRelativePosition(val)
            height = int(viewportHeight * relativePos + self.margin)
            lineWidth = _TickBar._LINE_WIDTH
            if majorTick is False:
                lineWidth /= 2

            painter.drawLine(
                qt.QLine(int(self.width() - lineWidth), height, self.width(), height)
            )

            if self.displayValues and majorTick is True:
                painter.drawText(
                    qt.QPoint(0, int(height + fm.height() / 2)), self.form.format(val)
                )

        def setDisplayType(self, disType):
            """Set the type of display we want to set for ticks labels

            :param str disType: The type of display we want to set. disType values
                can be :

                - 'std' for standard, meaning only a formatting on the number of
                    digits is done
                - 'e' for scientific display
                - None to let the _TickBar guess the best display for this kind of data.
            """
            if disType not in (None, "std", "e"):
                raise ValueError(
                    "display type not recognized, value should be in (None, 'std', 'e'"
                )
            self._forcedDisplayType = disType

        def _getStandardFormat(self):
            return "{0:.%sf}" % self._nfrac

        def _getFormat(self, font):
            if self._forcedDisplayType is None:
                return self._guessType(font)
            elif self._forcedDisplayType == "std":
                return self._getStandardFormat()
            elif self._forcedDisplayType == "e":
                return self._getScientificForm()
            else:
                err = (
                    "Forced type for display %s is not recognized" % self._forcedDisplayType
                )
                raise ValueError(err)

        def _getScientificForm(self):
            return "{0:.0e}"

        def _guessType(self, font):
            """Try fo find the better format to display the tick's labels

            :param QFont font: the font we want to use during the painting
            """
            form = self._getStandardFormat()

            fm = qt.QFontMetrics(font)
            width = 0
            for tick in self.ticks:
                width = max(fm.boundingRect(form.format(tick)).width(), width)

            # if the length of the string are too long we are moving to scientific
            # display
            if width > _TickBar._WIDTH_DISP_VAL - _TickBar._LINE_WIDTH:
                return self._getScientificForm()
            else:
                return form