# /*##########################################################################
#
# Copyright (c) 2014-2018 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.
#
# ###########################################################################*/
"""This module implements labels layout on graph axes."""
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "18/10/2016"
import math
# utils #######################################################################
[docs]
def numberOfDigits(tickSpacing):
"""Returns the number of digits to display for text label.
:param float tickSpacing: Step between ticks in data space.
:return: Number of digits to show for labels.
:rtype: int
"""
nfrac = int(-math.floor(math.log10(tickSpacing)))
if nfrac < 0:
nfrac = 0
return nfrac
# Nice Numbers ################################################################
# This is the original niceNum implementation. For the date time ticks a more
# generic implementation was needed.
#
# def _niceNum(value, isRound=False):
# expvalue = math.floor(math.log10(value))
# frac = value/pow(10., expvalue)
# if isRound:
# if frac < 1.5:
# nicefrac = 1.
# elif frac < 3.: # In niceNumGeneric this is (2+5)/2 = 3.5
# nicefrac = 2.
# elif frac < 7.:
# nicefrac = 5. # In niceNumGeneric this is (5+10)/2 = 7.5
# else:
# nicefrac = 10.
# else:
# if frac <= 1.:
# nicefrac = 1.
# elif frac <= 2.:
# nicefrac = 2.
# elif frac <= 5.:
# nicefrac = 5.
# else:
# nicefrac = 10.
# return nicefrac * pow(10., expvalue)
[docs]
def niceNumGeneric(value, niceFractions=None, isRound=False):
"""A more generic implementation of the _niceNum function
Allows the user to specify the fractions instead of using a hardcoded
list of [1, 2, 5, 10.0].
"""
if value == 0:
return value
if niceFractions is None: # Use default values
niceFractions = 1.0, 2.0, 5.0, 10.0
roundFractions = (1.5, 3.0, 7.0, 10.0) if isRound else niceFractions
else:
roundFractions = list(niceFractions)
if isRound:
# Take the average with the next element. The last remains the same.
for i in range(len(roundFractions) - 1):
roundFractions[i] = (niceFractions[i] + niceFractions[i + 1]) / 2
highest = niceFractions[-1]
value = float(value)
expvalue = math.floor(math.log(value, highest))
frac = value / pow(highest, expvalue)
for niceFrac, roundFrac in zip(niceFractions, roundFractions):
if frac <= roundFrac:
return niceFrac * pow(highest, expvalue)
# should not come here
assert False, "should not come here"
[docs]
def niceNumbers(vMin, vMax, nTicks=5):
"""Returns tick positions.
This function implements graph labels layout using nice numbers
by Paul Heckbert from "Graphics Gems", Academic Press, 1990.
See `C code <http://tog.acm.org/resources/GraphicsGems/gems/Label.c>`_.
:param float vMin: The min value on the axis
:param float vMax: The max value on the axis
:param int nTicks: The number of ticks to position
:returns: min, max, increment value of tick positions and
number of fractional digit to show
:rtype: tuple
"""
vrange = niceNumGeneric(vMax - vMin, isRound=False)
spacing = niceNumGeneric(vrange / nTicks, isRound=True)
graphmin = math.floor(vMin / spacing) * spacing
graphmax = math.ceil(vMax / spacing) * spacing
nfrac = numberOfDigits(spacing)
return graphmin, graphmax, spacing, nfrac
def _frange(start, stop, step):
"""range for float (including stop)."""
assert step >= 0.0
while start <= stop:
yield start
start += step
[docs]
def ticks(vMin, vMax, nbTicks=5):
"""Returns tick positions and labels using nice numbers algorithm.
This enforces ticks to be within [vMin, vMax] range.
It returns at least 1 tick (when vMin == vMax).
:param float vMin: The min value on the axis
:param float vMax: The max value on the axis
:param int nbTicks: The number of ticks to position
:returns: tick positions and corresponding text labels
:rtype: 2-tuple: list of float, list of string
"""
assert vMin <= vMax
if vMin == vMax:
positions = [vMin]
nfrac = 0
else:
start, end, step, nfrac = niceNumbers(vMin, vMax, nbTicks)
positions = [t for t in _frange(start, end, step) if vMin <= t <= vMax]
# Makes sure there is at least 2 ticks
if len(positions) < 2:
positions = [vMin, vMax]
nfrac = numberOfDigits(vMax - vMin)
# Generate labels
format_ = "%g" if nfrac == 0 else "%.{}f".format(nfrac)
labels = [format_ % tick for tick in positions]
return positions, labels
[docs]
def niceNumbersAdaptative(vMin, vMax, axisLength, tickDensity):
"""Returns tick positions using :func:`niceNumbers` and a
density of ticks.
axisLength and tickDensity are based on the same unit (e.g., pixel).
:param float vMin: The min value on the axis
:param float vMax: The max value on the axis
:param float axisLength: The length of the axis.
:param float tickDensity: The density of ticks along the axis.
:returns: min, max, increment value of tick positions and
number of fractional digit to show
:rtype: tuple
"""
# At least 2 ticks
nticks = max(2, int(round(tickDensity * axisLength)))
tickmin, tickmax, step, nfrac = niceNumbers(vMin, vMax, nticks)
return tickmin, tickmax, step, nfrac
# Nice Numbers for log scale ##################################################
[docs]
def niceNumbersForLog10(minLog, maxLog, nTicks=5):
"""Return tick positions for logarithmic scale
:param float minLog: log10 of the min value on the axis
:param float maxLog: log10 of the max value on the axis
:param int nTicks: The number of ticks to position
:returns: log10 of min, max, increment value of tick positions and
number of fractional digit to show
:rtype: tuple of int
"""
graphminlog = math.floor(minLog)
graphmaxlog = math.ceil(maxLog)
rangelog = graphmaxlog - graphminlog
if rangelog <= nTicks:
spacing = 1.0
else:
spacing = math.floor(rangelog / nTicks)
graphminlog = math.floor(graphminlog / spacing) * spacing
graphmaxlog = math.ceil(graphmaxlog / spacing) * spacing
nfrac = numberOfDigits(spacing)
return int(graphminlog), int(graphmaxlog), int(spacing), nfrac
[docs]
def niceNumbersAdaptativeForLog10(vMin, vMax, axisLength, tickDensity):
"""Returns tick positions using :func:`niceNumbers` and a
density of ticks.
axisLength and tickDensity are based on the same unit (e.g., pixel).
:param float vMin: The min value on the axis
:param float vMax: The max value on the axis
:param float axisLength: The length of the axis.
:param float tickDensity: The density of ticks along the axis.
:returns: log10 of min, max, increment value of tick positions and
number of fractional digit to show
:rtype: tuple
"""
# At least 2 ticks
nticks = max(2, int(round(tickDensity * axisLength)))
tickmin, tickmax, step, nfrac = niceNumbersForLog10(vMin, vMax, nticks)
return tickmin, tickmax, step, nfrac
[docs]
def computeLogSubTicks(ticks, lowBound, highBound):
"""Return the sub ticks for the log scale for all given ticks if subtick
is in [lowBound, highBound]
:param ticks: log10 of the ticks
:param lowBound: the lower boundary of ticks
:param highBound: the higher boundary of ticks
:return: all the sub ticks contained in ticks (log10)
"""
if len(ticks) < 1:
return []
res = []
for logPos in ticks:
dataOrigPos = logPos
for index in range(2, 10):
dataPos = dataOrigPos * index
if lowBound <= dataPos <= highBound:
res.append(dataPos)
return res