Hi list,

I grew frustrated with the coarseness of control of graph.data.function. You 
can specify the number of points, but it's quite suboptimal if the gradient of 
your function varies significantly, especially if that variation in gradient is 
only confined to a small region.

So, instead of finishing my thesis like I *really* ought to be doing, I wrote 
the attached extension, the adaptivefunction class. This works just like 
graph.data.function, except that after generating all the linearly separated 
points, it will do a second pass and adaptively add in-between points. It 
chooses to do this based on how much the angle differs between two adjacent 
lines (three adjacent points). If the angle differs by more than the angle 
epsilon (a new constructor parameter) then it will split the two lines into 
four lines by adding two new points.

What this means is that adaptivefunction uses points more efficiently than 
function does for the same number of points, and it should result in better 
looking function plots with smaller file sizes.

adaptivefunction tries to take into account the scale and shape of the 
rendered plot when determining the angles; it adapts to optimise the visual 
result. One caveat with this is that I'm not sure how to handle the case when 
the y axis range is not specified or cannot be inferred from other data. (Are 
the min and max values of the axis class None in that case? That seems to be 
the case.)

I've played around with it a bit. It works fine on both linear and log plots (I 
should know, as it took me several hours to figure out how to work around 
precision issues with the log plots).

It's fun giving it 3 starting points and a small epsilon, and seeing it 
produce a lovely smoothed plot.

Thought I'd share. Enjoy. :-)

Peace,
Brendon
# -*- coding: utf-8 -*-
# Brendon L. Higgins 25-08-2010
# Based on the function class from data.py in PyX (http://pyx.sourceforge.net)
# GNU GPL 2

# adaptivefunction samples the given number of points of the given function.
# It then adaptively samples extra points to smooth out rapid changes.
#
# adaptivefunction is used very similarly to function.
# New constructor parameter: epsilon
# epsilon is the maximum angular deviation allowed between two adjacent lines
# before a pair of intermediate points will be calculated
# E.g., epsilon=2.0 will allow lines to differ by up to 2 degrees
# Smaller numbers mean greater precision

from pyx.graph.data import function
from pyx.graph.data import _notitle
from pyx.graph.data import _mathglobals
import math

class adaptivefunction(function):

    def __init__(self, expression, title=_notitle, min=None, max=None,
                 points=50, epsilon=2.0, context={}):
        function.__init__(self, expression, title, min, max, points, context)
        self.epsilon = epsilon

    def dynamiccolumns(self, graph):
        dynamiccolumns = {self.xname: [], self.yname: []}

        xaxis = graph.axes[self.xname]
        yaxis = graph.axes[self.yname]
        from pyx.graph.axis import logarithmic
        logxaxis = isinstance(xaxis.axis, logarithmic)
        logyaxis = isinstance(yaxis.axis, logarithmic)
        if self.min is not None:
            min = self.min
        else:
            min = xaxis.data.min
        if self.max is not None:
            max = self.max
        else:
            max = xaxis.data.max
        miny = yaxis.data.min
        maxy = yaxis.data.max
        if logxaxis:
            min = math.log(min)
            max = math.log(max)
        if logyaxis:
            miny = math.log(miny)
            maxy = math.log(maxy)

        for i in range(self.numberofpoints):
            x = min + (max-min)*i / (self.numberofpoints-1.0)
            if logxaxis:
                x = math.exp(x)
            dynamiccolumns[self.xname].append(x)
            self.context[self.xname] = x
            try:
                y = eval(self.expression, _mathglobals, self.context)
            except (ArithmeticError, ValueError):
                y = None
            dynamiccolumns[self.yname].append(y)

        if len(dynamiccolumns[self.xname]) < 3:
            return dynamiccolumns

        x0 = y0 = x1 = y1 = x2 = y2 = None
        e = self.epsilon/180.0*math.pi
        r = float((max-min)/graph.width)/float((maxy-miny)/graph.height)

        def nearenough():
            if not logxaxis:
                (dx0, dx1) = (x1-x0, x2-x1)
            else:
                (dx0, dx1) = (math.log(x1/x0), math.log(x2/x1))
            if not logyaxis:
                (dy0, dy1) = (y1-y0, y2-y1)
            else:
                (dy0, dy1) = (math.log(y1/y0), math.log(y2/y1))
            t0 = math.atan(r*dy0/dx0)
            t1 = math.atan(r*dy1/dx1)
            return abs(t0 - t1) <= e

        i = -1
        while True:
            while y0 is None or y1 is None or y2 is None or nearenough():
                i += 1
                if i == len(dynamiccolumns[self.yname]):
                    break
                (x0, y0) = (x1, y1)
                (x1, y1) = (x2, y2)
                x2 = dynamiccolumns[self.xname][i]
                y2 = dynamiccolumns[self.yname][i]
            if i == len(dynamiccolumns[self.yname]):
                break

            if not logxaxis:
                (nx1, nx2) = ((x0+x1)/2.0, (x1+x2)/2.0)
            else:
                (nx1, nx2) = (math.sqrt(x0*x1), math.sqrt(x1*x2))

            dynamiccolumns[self.xname].insert(i, nx2)
            self.context[self.xname] = nx2
            try:
                ny2 = eval(self.expression, _mathglobals, self.context)
            except (ArithmeticError, ValueError):
                ny2 = None
            dynamiccolumns[self.yname].insert(i, ny2)

            dynamiccolumns[self.xname].insert(i-1, nx1)
            self.context[self.xname] = nx1
            try:
                ny1 = eval(self.expression, _mathglobals, self.context)
            except (ArithmeticError, ValueError):
                ny1 = None
            dynamiccolumns[self.yname].insert(i-1, ny1)

            (x2, y2) = (x1, y1)
            (x1, y1) = (nx1, ny1)
        return dynamiccolumns
------------------------------------------------------------------------------
Sell apps to millions through the Intel(R) Atom(Tm) Developer Program
Be part of this innovative community and reach millions of netbook users 
worldwide. Take advantage of special opportunities to increase revenue and 
speed time-to-market. Join now, and jumpstart your future.
http://p.sf.net/sfu/intel-atom-d2d
_______________________________________________
PyX-user mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/pyx-user

Reply via email to