"Visually equivalent" contours/parallels

Hi All,

    after some great help from the Numpy users list, I have managed to
create "parallel curves".

Basically I have a set of X, Y data (around 1,000 elements each) and I
want to create 2 parallel "curves" (offset curves) to the original
one; "parallel" means curves which are displaced from the base curve
by a constant offset, either positive or negative, in the direction of
the curve's normal. I attach a sample demonstrating what I am doing
(and if you have the Shapely package installed, the two additional
subplots look even nicer).

Now, the generated parallels are really parallels (see right subplot),
but visually it doesn't seem so (see the left subplot) because the X
and Y scales are different and the axes area is not square. I know I
could force the axes to be equals via:

ax.set_aspect('equal', 'datalim')

But I can't really do that with the set of data I have, as the X and Y
variables have different order of magnitude and I need a single
subplot on the figure to have rectangular axes (not square). In my
situation, unfortunately it wouldn't make sense to set the axes
square/equal as the plot will lose its meaning and visual usefulness.

I have been told to scale the X, Y variables normalizing them by the
display units of the plot, but I must be dumber than usual as I can't
get it to work properly. So, my question would be: how do I scale the
X and Y vectors so that the parallels look parallel to the main curve
even if the axes are not square and the X and Y variables have
different data-ranges/magnitudes?

Thank you in advance for your suggestions.

Andrea.

"Imagination Is The Only Weapon In The War Against Reality."
http://xoomer.alice.it/infinity77/

sample_buffer.py (3.82 KB)

Andrea Gavana :

after some great help from the Numpy users list, I have managed to
create "parallel curves".

But I can't really do that with the set of data I have, as the X and Y
variables have different order of magnitude and I need a single
subplot on the figure to have rectangular axes (not square).

(...)

So, my question would be: how do I scale the
X and Y vectors so that the parallels look parallel to the main curve
even if the axes are not square and the X and Y variables have
different data-ranges/magnitudes?

Andrea, you have TWO problems.

The first is to scale your offset according to your axes range. This can be done using ax.get_data_ratio(). In your case you will get 5, and this factor should enhance your vertical offset wrt. the horizontal.
(Or, use .get_xlim() and .get_ylim() and do the computations yourself).

The second problem is that your FIGURE scales your plot visually, independently of your axes, so without special scaling it will have different aspects according to your manipulation. An arbitrary affine transform will keep straight lines parallel, but no chance with arbitrary curves. You may play with fig.get_figwidth(), etc., but here my digging stops.

Good luck.

Jerzy Karczmarczuk

Hi Jerzy,

Andrea Gavana :

after some great help from the Numpy users list, I have managed to
create "parallel curves".

But I can't really do that with the set of data I have, as the X and Y
variables have different order of magnitude and I need a single
subplot on the figure to have rectangular axes (not square).

(...)

So, my question would be: how do I scale the
X and Y vectors so that the parallels look parallel to the main curve
even if the axes are not square and the X and Y variables have
different data-ranges/magnitudes?

Andrea, you have TWO problems.

The first is to scale your offset according to your axes range. This can
be done using ax.get_data_ratio(). In your case you will get 5, and this
factor should enhance your vertical offset wrt. the horizontal.
(Or, use .get_xlim() and .get_ylim() and do the computations yourself).

Thank you for your answer, I have implemented this and it looks a bit
better (on the real X/Y pairs I have).

The second problem is that your FIGURE scales your plot visually,
independently of your axes, so without special scaling it will have
different aspects according to your manipulation. An arbitrary affine
transform will keep straight lines parallel, but no chance with
arbitrary curves. You may play with fig.get_figwidth(), etc., but here
my digging stops.

Will this argument still stand if I am only interested in a single
figure size (maximized window on my screen, plus set_size_inches(20,
12)) and fixed axes positions (set by figure.subplots_adjust)? If not,
should the further scaling simply be the ratio between the x-axis
extent and y-axis extent (in pixels)? Or am I missing something
(again)?

Thank you in advance for your suggestions.

Andrea.

"Imagination Is The Only Weapon In The War Against Reality."
http://xoomer.alice.it/infinity77/

···

On 14 February 2012 16:03, Jerzy Karczmarczuk wrote:

Andrea, I believe that if you find ONE good visual aspect ratio, according to your figure size, that should do. Yo know your
figsize, and if you know all in your axes([bot,lft,wid, height]), that this visual ratio should be easy to fix.

BTW, I did not understand why do you play with your "angle" = arctan(...), then compute sin and cos, etc. Two issues.
1. You are wasting your time.
2. If your data is a functional sequence, with x growing, its OK, but for ANY data you may get zero in the denominator, and your arctan will locally explode, producing holes in the plotted data. Use arctan2. Or rather, don't compute angles at all.

Concerning 1.
If you compute dx = xhigh-xlow; dy=yhigh-ylow;
then the parameters of a normal offset are (nx,ny) = (-dy,dx), appropriately normalized, and that's all.

All the best.

Jerzy

Hello Jerzy & All,

Andrea, I believe that if you find ONE good visual aspect ratio,
according to your figure size, that should do. Yo know your
figsize, and if you know all in your axes([bot,lft,wid, height]), that
this visual ratio should be easy to fix.

I managed to get *almost* there, but there still is a small glitch. I
attach a self-evident sample, which generates data very similar to the
real ones I have and shows the two "parallel" curves to the main one.

You will notice that the "parallel" curves look parallel almost all
the time, except in a few areas (I have annotated the plot for
reference). I can't see the reason for this difference, but it is
obvious I am missing something.

One thing I didn't include is that in my real-life plot, the figure
size should be full screen (which on my screen means 20x13.65 inches)
and I use the subplots_adjust command like this:

fig.subplots_adjust(left=0.03, right=0.7, bottom=0.04, top=0.96)

Thank you in advance for any suggestion.

Andrea.

"Imagination Is The Only Weapon In The War Against Reality."
http://xoomer.alice.it/infinity77/

parallels2.py (2.71 KB)

···

On 14 February 2012 17:55, Jerzy Karczmarczuk wrote:

Andrea Gavana continues to struggle with his parallel lines:

I managed to get *almost* there, but there still is a small glitch. I
attach a self-evident sample, which generates data very similar to the
real ones I have and shows the two "parallel" curves to the main one.

You will notice that the "parallel" curves look parallel almost all
the time, except in a few areas (I have annotated the plot for
reference). I can't see the reason for this difference, but it is
obvious I am missing something.

Yes. I am afraid it it not a small glitch, but a serious bug.
Actually, there are still two things. The second one is the purely visual scaling based on the aspect of your figure on the screen, and I will not discuss it now, you can get into your axes, pick the frame, dig out the extent, extract the "points" of the rectangle, and do some other dirty stuff, which will get wrong when you touch your figure... So, here - I believe - is better to adjust things by hand.

But the REAL STUFF is elsewhere. You added a vertical scaling, as I suggested, based on the aspect ratio of your data. You rescaled the offsets. I believe that I might have suggested that, if this is the case, I am sorry, it was a mental abbreviation...

Rescaling JUST the offsets, and keeping the function itself in a "distorted frame" is sinful. Here you are another attempt. With (or without) your permission, I reconstruct the offset without your !*##$%*!! arctan... This is my equivalent of your "get-normal points" :

def adp(x): # This computes the derivative without splines or other heavy artillery.
     xa=numpy.concatenate(([2*x[0]-x[1]],x,[2*x[-1]-x[-2]]))
     return xa[2:]-xa[:-2]
def para(x,y,dst,ax): # ax is the current axes set, dst is the offset.
     scal=ax.get_data_ratio() # Don't send human make the work of a machine!
     y0=y/scal # Warp the original
     dx=adp(x); dy=adp(y0)
     ds=dst/numpy.sqrt(dx*dx+dy*dy)
     x1=x-ds*dy; x2=x+ds*dy
     y1=y0+ds*dx; y2=y0-ds*dx
     return (x1,y1*scal,x2,y2*scal)

As you see, the function got "undistorted", the offsets calculated, and Mister Sulu, get us back into the real space !
Now, within your main() :

...
fig = plt.figure(figsize=(fw,fh)) # whatever...
ax = plt.subplot(111)
ax.plot(x, y, color='orange', ls='-', lw=3, label='data', zorder=30) # It should be done first
...
ax.set_xlim(xlims)
ax.set_ylim(ylims)
ax.grid()
ax.invert_yaxis()
...
xh,yh,xl,yl = para(x,y,distance,ax) # Here you are.
ax.plot(xl, yl, 'g-',zorder=30)
ax.plot(xh, yh, 'b-',zorder=30)

···

=================

OK, let's get back to the second problem. Here you have the additional scaling.

def vscal(ax):
     gf=ax.get_frame()
     ex=gf.get_extents()
     po=ex.get_points() # What a lousy way ! Probably Ben R. knows some more direct access.
     w,h = po[1,0]-po[0,0], po[1,1]-po[0,1]
     return h/w

And within para(), the first line becomes:

scal=ax.get_data_ratio()/vscal(ax)

However, since my insomnia doesn't necessarily mean that I am fully conscious, check everything twice. Add your
fig.subplots_adjust, and recheck.

Good luck.

Jerzy K.