Esempio n. 1
0
def scale(fmn, fmx, prescale = (None, None, None), nsteps = 10):
    """Calculates an appropriate min, max, and step size for scaling axes on a plot.
    
    The origin (zero) is guaranteed to be on an interval boundary.
    
    fmn: The minimum data value
    
    fmx: The maximum data value. Must be greater than or equal to fmn.

    prescale: A 3-way tuple. A non-None min or max value (positions 0 and 1, 
    respectively) will be fixed to that value. A non-None interval (position 2)
    be at least as big as that value. Default = (None, None, None) 
    
    nsteps: The nominal number of desired steps. Default = 10
    
    Returns: a three-way tuple. First value is the lowest scale value, second the highest.
    The third value is the step (increment) between them.

    Examples:
    >>> print "(%.1f, %.1f, %.1f)" % scale(1.1, 12.3, (0, 14, 2))
    (0.0, 14.0, 2.0)
    >>> print "(%.1f, %.1f, %.1f)" % scale(1.1, 12.3)
    (0.0, 14.0, 2.0)
    >>> print "(%.1f, %.1f, %.1f)" % scale(-1.1, 12.3)
    (-2.0, 14.0, 2.0)
    >>> print "(%.1f, %.1f, %.1f)" % scale(-12.1, -5.3)
    (-13.0, -5.0, 1.0)
    >>> print "(%.2f, %.2f, %.2f)" % scale(10.0, 10.0)
    (10.00, 10.10, 0.01)
    >>> print "(%.2f, %.4f, %.4f)" % scale(10.0, 10.001)
    (10.00, 10.0010, 0.0001)
    >>> print "(%.2f, %.2f, %.2f)" % scale(10.0, 10.0+1e-8)
    (10.00, 10.10, 0.01)
    >>> print "(%.2f, %.2f, %.2f)" % scale(0.0, 0.05, (None, None, .1), 10)
    (0.00, 1.00, 0.10)
    >>> print "(%.2f, %.2f, %.2f)" % scale(16.8, 21.5, (None, None, 2), 10)
    (16.00, 36.00, 2.00)
    >>> print "(%.2f, %.2f, %.2f)" % scale(16.8, 21.5, (None, None, 2), 4)
    (16.00, 22.00, 2.00)
    >>> print "(%.2f, %.2f, %.2f)" % scale(0.0, 0.21, (None, None, .02))
    (0.00, 0.22, 0.02)
    >>> print "(%.2f, %.2f, %.2f)" % scale(100.0, 100.0, (None, 100, None))
    (99.00, 100.00, 0.20)
    >>> print "(%.2f, %.2f, %.2f)" % scale(100.0, 100.0, (100, None, None))
    (100.00, 101.00, 0.20)

    """
    
    # If all the values are hard-wired in, then there's nothing to do:
    if None not in prescale:
        return prescale

    (minscale, maxscale, min_interval) = prescale

    # Make sure fmn and fmx are float values, in case a user passed
    # in integers:
    fmn = float(fmn)
    fmx = float(fmx)

    if fmx < fmn :
        raise weeplot.ViolatedPrecondition("scale() called with max value less than min value")

    # In case minscale and/or maxscale was specified, clip fmn and fmx to make sure they stay within bounds
    if maxscale is not None:
        fmx = min(fmx, maxscale)
    if minscale is not None:
        fmn = max(fmn, minscale)

    # Check the special case where the min and max values are equal.
    if _rel_approx_equal(fmn, fmx) :
        # They are equal. We need to move one or the other to create a range, while
        # being careful that the resultant min/max stay within the interval [minscale, maxscale]
        # Pick a step out value based on min_interval if the user has supplied one. Otherwise,
        # arbitrarily pick 0.1
        if min_interval is not None:
            step_out = min_interval * nsteps
        else:
            step_out = 0.01 * abs(fmx) if fmx else 0.1
        if maxscale is not None:
            # maxscale if fixed. Move fmn.
            fmn = fmx - step_out
        elif minscale is not None:
            # minscale if fixed. Move fmx.
            fmx = fmn + step_out
        else:
            # Both can float. Check special case where fmn and fmx are zero
            if fmn == 0.0 :
                fmx = 1.0
            else :
                # Just arbitrarily move one. Say, fmx.
                fmx = fmn + step_out

    if minscale is not None and maxscale is not None:
        if maxscale < minscale:
            raise weeplot.ViolatedPrecondition("scale() called with prescale max less than min")
        frange = maxscale - minscale
    else:
        frange = fmx - fmn
    steps = frange / nsteps
    
    mag = math.floor(math.log10(steps))
    magPow = math.pow(10.0, mag)
    magMsd = math.floor(steps/magPow + 0.5)
    
    if magMsd > 5.0:
        magMsd = 10.0
    elif magMsd > 2.0:
        magMsd = 5.0
    else : # magMsd > 1.0
        magMsd = 2

    # This will be the nominal interval size
    interval = magMsd * magPow
    
    # Test it against the desired minimum, if any
    if min_interval is None or interval >= min_interval:
        # Either no min interval was specified, or its safely
        # less than the chosen interval. 
        if minscale is None:
            minscale = interval * math.floor(fmn / interval)
    
        if maxscale is None:
            maxscale = interval * math.ceil(fmx / interval)

    else:
    
        # The request for a minimum interval has kicked in.
        # Sometimes this can make for a plot with just one or
        # two intervals in it. Adjust the min and max values
        # to get a nice plot
        interval = float(min_interval)

        if minscale is None:
            if maxscale is None:
                # Both can float. Pick values so the range is near the bottom
                # of the scale:
                minscale = interval * math.floor(fmn / interval)
                maxscale = minscale + interval * nsteps
            else:
                # Only minscale can float
                minscale = maxscale - interval * nsteps
        else:
            if maxscale is None:
                # Only maxscale can float
                maxscale = minscale + interval * nsteps
            else:
                # Both are fixed --- nothing to be done
                pass

    return (minscale, maxscale, interval)
Esempio n. 2
0
def scaletime(tmin_ts, tmax_ts) :
    """Picks a time scaling suitable for a time plot.
    
    tmin_ts, tmax_ts: The time stamps in epoch time around which the times will be picked.
    
    Returns a scaling 3-tuple. First element is the start time, second the stop
    time, third the increment. All are in seconds (epoch time in the case of the 
    first two).    
    
    Example 1: 24 hours on an hour boundary
    >>> from weeutil.weeutil import timestamp_to_string as to_string
    >>> time_ts = time.mktime(time.strptime("2013-05-17 08:00", "%Y-%m-%d %H:%M"))
    >>> xmin, xmax, xinc = scaletime(time_ts - 24*3600, time_ts)
    >>> print to_string(xmin), to_string(xmax), xinc
    2013-05-16 09:00:00 PDT (1368720000) 2013-05-17 09:00:00 PDT (1368806400) 10800

    Example 2: 24 hours on a 3-hour boundary
    >>> time_ts = time.mktime(time.strptime("2013-05-17 09:00", "%Y-%m-%d %H:%M"))
    >>> xmin, xmax, xinc = scaletime(time_ts - 24*3600, time_ts)
    >>> print to_string(xmin), to_string(xmax), xinc
    2013-05-16 09:00:00 PDT (1368720000) 2013-05-17 09:00:00 PDT (1368806400) 10800

    Example 3: 24 hours on a non-hour boundary
    >>> time_ts = time.mktime(time.strptime("2013-05-17 09:01", "%Y-%m-%d %H:%M"))
    >>> xmin, xmax, xinc = scaletime(time_ts - 24*3600, time_ts)
    >>> print to_string(xmin), to_string(xmax), xinc
    2013-05-16 12:00:00 PDT (1368730800) 2013-05-17 12:00:00 PDT (1368817200) 10800

    Example 4: 27 hours
    >>> time_ts = time.mktime(time.strptime("2013-05-17 07:45", "%Y-%m-%d %H:%M"))
    >>> xmin, xmax, xinc = scaletime(time_ts - 27*3600, time_ts)
    >>> print to_string(xmin), to_string(xmax), xinc
    2013-05-16 06:00:00 PDT (1368709200) 2013-05-17 09:00:00 PDT (1368806400) 10800

    Example 5: 3 hours on a 15 minute boundary
    >>> time_ts = time.mktime(time.strptime("2013-05-17 07:45", "%Y-%m-%d %H:%M"))
    >>> xmin, xmax, xinc = scaletime(time_ts - 3*3600, time_ts)
    >>> print to_string(xmin), to_string(xmax), xinc
    2013-05-17 05:00:00 PDT (1368792000) 2013-05-17 08:00:00 PDT (1368802800) 900

    Example 6: 3 hours on a non-15 minute boundary
    >>> time_ts = time.mktime(time.strptime("2013-05-17 07:46", "%Y-%m-%d %H:%M"))
    >>> xmin, xmax, xinc = scaletime(time_ts - 3*3600, time_ts)
    >>> print to_string(xmin), to_string(xmax), xinc
    2013-05-17 05:00:00 PDT (1368792000) 2013-05-17 08:00:00 PDT (1368802800) 900

    Example 7: 12 hours
    >>> time_ts = time.mktime(time.strptime("2013-05-17 07:46", "%Y-%m-%d %H:%M"))
    >>> xmin, xmax, xinc = scaletime(time_ts - 12*3600, time_ts)
    >>> print to_string(xmin), to_string(xmax), xinc
    2013-05-16 20:00:00 PDT (1368759600) 2013-05-17 08:00:00 PDT (1368802800) 3600

    Example 8: 15 hours
    >>> time_ts = time.mktime(time.strptime("2013-05-17 07:46", "%Y-%m-%d %H:%M"))
    >>> xmin, xmax, xinc = scaletime(time_ts - 15*3600, time_ts)
    >>> print to_string(xmin), to_string(xmax), xinc
    2013-05-16 17:00:00 PDT (1368748800) 2013-05-17 08:00:00 PDT (1368802800) 7200
    """
    if tmax_ts <= tmin_ts :
        raise weeplot.ViolatedPrecondition("scaletime called with tmax <= tmin")
    
    tdelta = tmax_ts - tmin_ts
    
    tmin_dt = datetime.datetime.fromtimestamp(tmin_ts)
    tmax_dt = datetime.datetime.fromtimestamp(tmax_ts)
    
    if tdelta <= 16 * 3600:
        if tdelta <= 3*3600:
            # For time intervals less than 3 hours, use an increment of 15 minutes
            interval = 900
        elif tdelta <= 12 * 3600:
            # For intervals from 3 hours up through 12 hours, use one hour
            interval = 3600
        else:
            # For intervals from 12 through 16 hours, use two hours.
            interval = 7200
        # Get to the one hour boundary below tmax:
        stop_dt = tmax_dt.replace(minute=0, second=0, microsecond=0)
        # if tmax happens to be on a one hour boundary we're done. Otherwise, round
        # up to the next one hour boundary:
        if tmax_dt > stop_dt:
            stop_dt += datetime.timedelta(hours=1)
        n_hours = int((tdelta + 3599) / 3600)
        start_dt = stop_dt - datetime.timedelta(hours=n_hours)
        
    elif tdelta <= 27 * 3600:
        # A day plot is wanted. A time increment of 3 hours is appropriate
        interval = 3 * 3600
        # h is the hour of tmax_dt
        h = tmax_dt.timetuple()[3]
        # Subtract off enough to get to the lower 3-hour boundary from tmax: 
        stop_dt = tmax_dt.replace(minute=0, second=0, microsecond=0) - datetime.timedelta(hours = h % 3)
        # If tmax happens to lie on a 3 hour boundary we don't need to do anything. If not, we need
        # to round up to the next 3 hour boundary:
        if tmax_dt > stop_dt:
            stop_dt += datetime.timedelta(hours=3)
        # The stop time is one day earlier
        start_dt = stop_dt - datetime.timedelta(days=1)
        
        if tdelta == 27 * 3600 :
            # A "slightly more than a day plot" is wanted. Start 3 hours earlier:
            start_dt -= datetime.timedelta(hours=3)
    
    elif 27 * 3600 < tdelta <= 31 * 24 * 3600 :
        # The time scale is between a day and a month. A time increment of one day is appropriate
        start_dt = tmin_dt.replace(hour=0, minute=0, second=0, microsecond=0)
        stop_dt  = tmax_dt.replace(hour=0, minute=0, second=0, microsecond=0)
        
        tmax_tt = tmax_dt.timetuple()
        if tmax_tt[3]!=0 or tmax_tt[4]!=0 :
            stop_dt += datetime.timedelta(days=1)
            
        interval = 24 * 3600
    elif tdelta < 2 * 365.25 * 24 * 3600 :
        # The time scale is between a month and 2 years. A time increment of a month is appropriate
        start_dt = tmin_dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
        
        (year , mon, day) = tmax_dt.timetuple()[0:3]
        if day != 1 :
            mon += 1
            if mon==13 :
                mon = 1
                year += 1
        stop_dt = datetime.datetime(year, mon, 1)
        # Average month length:
        interval = 365.25/12 * 24 * 3600
    else :
        # The time scale is between a month and 2 years. A time increment of a year is appropriate
        start_dt = tmin_dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)

        (year , mon, day) = tmax_dt.timetuple()[0:3]
        if day != 1 or mon !=1 :
            day = 1
            mon = 1
            year += 1
        stop_dt = datetime.datetime(year, mon, 1)
        # Average year length
        interval = 365.25 * 24 * 3600

    # Convert to epoch time stamps
    start_ts = int(time.mktime(start_dt.timetuple()))
    stop_ts  = int(time.mktime(stop_dt.timetuple()))

    return (start_ts, stop_ts, interval)