Closed Chrillebon closed 10 months ago
Hello @Chrillebon ,
thanks for creating this PR. Would you be able to share an example of the behavior that has been fixed by it?
Because of the "_sal" variable in the 2D vector renderer, this line is (as far as I can tell) never relevant for commands "entirely contained within spb". However, as I am trying to build on top of your functionality (and thank you for that), I found this to be the easiest way to automatically let the xlim and ylim adjust. I will come with an example as soon as I have time again.
Sorry that I never got back to you, but thanks for completing the PR. I did promise an example, so here is one. It is a bit lengthy, so I will start with a summary to give an overview of the goal:
I wanted to create a 2D vector class (a single vector, not a field) utilizing much of the code already written for the vector fields. This worked quite well, but I had trouble getting the arrow within view (adjusting the xlim and ylim). By adjusting the class to internally use the starting and end point, it would then automatically extract the right limits and have the vector within frame. All worked fine until the one missing line caused only the xlim to be updated, why I created this PR.
The first piece of code inherits from the VectorBase, but giving start and direction creates a vector. The get_data
function adjusts to get start and end point instead of start and direction.
from sympy import latex
from spb.series import VectorBase
import numpy as np
class Arrow2DSeries(VectorBase):
#Represent a vector field.
is_vector = True
is_slice = False
is_streamlines = False
is_2Dvector = True
def __init__(self, start, direction, label=None, **kwargs):
# Ranges must be given for VectorBase, even though they are None
super().__init__(
[start, direction], ranges=start.shape[-1] * [None], label=label, **kwargs
)
self.start = start
self.direction = direction
self._label = f"{start}->{direction}" if label is None else label
self._latex_label = latex(f"{start}->{direction}") if label is None else label
# Standard for 'use_cm' should be False
self.use_cm = kwargs.get("use_cm", False)
# Linked colormap using vector2d renderer
self.use_quiver_solid_color = not self.use_cm
# Line color needed for Mayavi
self._line_color = kwargs.get("line_color", None)
def __str__(self):
# Overwrite the VectorBase __str__ as it assumes things
# about variables that does not hold for this class.
return self._str_helper(
f"Arrow Series with start point {self.start}, and direction {self.direction}"
)
def get_data(self):
# This format works for both MB and PB
# Has to translate to start/end and transpose
# such that the x,y,z lims match the arrow
# compensation for this in arrow done in
# quiverplot_helpers.
start = np.array(self.start)
end = start + np.array(self.direction)
return np.array(
[start, end]
).T
The second piece of code compensates for the get_data
by updating the vector2d_helper
functions, and updates the _renderersmap.
from spb import MB
from spb.backends.matplotlib.renderers.renderer import MatplotlibRenderer
from spb.backends.matplotlib.renderers.vector2d import (
_draw_vector2d_helper as MB_draw_vector2d_helper,
_update_vector2d_helper as MB_update_vector2d_helper,
)
def MB_draw_quiver2d_helper(renderer, data):
start, end = data.T
direction = end - start
xx, yy = start
uu, vv = direction
return MB_draw_vector2d_helper(renderer, (xx, yy, uu, vv))
def MB_update_quiver2d_helper(renderer, data, handle):
start, end = data.T
direction = end - start
xx, yy = start
uu, vv = direction
return MB_update_vector2d_helper(renderer, (xx, yy, uu, vv), handle)
class MBQuiver2DRenderer(MatplotlibRenderer):
_sal = True
draw_update_map = {MB_draw_quiver2d_helper: MB_update_quiver2d_helper}
MB.renderers_map.update({
Arrow2DSeries: MBQuiver2DRenderer,
})
The final piece of code creates two series and adjusts the rendering_kw
(for MB) such that the arrows start and end in the correct places.
from spb.utils import _instantiate_backend
from spb.functions import _set_labels
start = np.array([1,2])
dir1 = np.array([3,4])
dir2 = np.array([5,-6])
s1 = Arrow2DSeries(start, dir1, normalize=False)
s2 = Arrow2DSeries(start, dir2, normalize=False)
rendering_kw = {
"angles": "xy",
"scale_units": "xy",
"scale": 1,
}
_set_labels([s1,s2], [], rendering_kw)
Here comes the problems, because as can be seen with the following line, only the xlims are updated:
B1 = _instantiate_backend(MB, s1, show=True)
This also extends to multiple series, where it is seen that the xlims are automatically adjusted as intended in the following, but the ylims are not:
B1 = _instantiate_backend(MB, s1, show=False)
B2 = _instantiate_backend(MB, s2, show=False)
(B1+B2).show()
The instance with a single series can be fixed using xlims and ylims for the kwargs, but this does not extend to multiple series, as running the following does not give the wanted result. This example also the reason why the internal start/end points are used, and why the PR (and not a workaround) was needed for this instance.
B1 = _instantiate_backend(MB, s1, show=False, xlim=[1,4], ylim=[2,6])
B2 = _instantiate_backend(MB, s2, show=False, xlim=[1,6], ylim=[-5,1])
(B1+B2).show()
So to conclude, I am not sure if it is the best way of doing this, and this was definitely not the simplest example one could have thought of, but I hope that you at least can see the problem that I was trying to resolve.
Hello @Chrillebon ,
I just released a new version. There is a lot of new stuff intended to make it easier to use the module.
But I've also implemented the Arrow2DSeries
and an associated plotting function. I implemented two renderers:
the default one, Arrow2DRendererFancyArrowPatch
uses matplotlib's FancyArrowPatch
. Here, the direction also set the end point of the arrow. In practice, end=start+dir
. Note that the labels show start -> end
:
from spb import *
import numpy as np
import matplotlib.patches as mpatches
start = np.array([1,2])
dir1 = np.array([3,4])
dir2 = np.array([5,-6])
rkw = {
"arrowstyle": mpatches.ArrowStyle("->", head_width=2, head_length=3),
"color": "g"
}
graphics(
arrow_2d(start, dir1),
arrow_2d(start, dir2, rendering_kw=rkw),
xlim=(-2, 10), ylim=(-5, 10), grid=False
)
Arrow2DRendererQuivers
uses matplotlib's quiver
, which is exactly to what you did. However, you might want to change the labels, because as you can see, they still indicate start -> end
, rather than start -> dir
which is what you originally coded. Hence, the labels don't really represent what is shown on the screen:
from spb import *
import numpy as np
from spb.backends.matplotlib.renderers import Arrow2DRendererQuivers
from spb.series import Arrow2DSeries
MB.renderers_map.update({Arrow2DSeries: Arrow2DRendererQuivers})
start = np.array([1,2])
dir1 = np.array([3,4])
dir2 = np.array([5,-6])
rendering_kw = {
"angles": "xy",
"scale_units": "xy",
"scale": 1,
}
graphics(
arrow_2d(start, dir1, rendering_kw=rendering_kw),
arrow_2d(start, dir2, rendering_kw=rendering_kw),
xlim=(-2, 10), ylim=(-5, 10), grid=False
)
In the matplotlib backend, one line is missing when updating the ylims (compared to the xlims right above). Updated by matching the second half of the xlims "if-statement" (line 464 in backends/matplotlib.py) to the second half of the ylims "if-statement" (line 471 in backends/matplotlib.py)
Result of this missing line: 2D vector-plots with specified xlims and ylims (or inferred from data) only have their xlims updated.