Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Visual: Bar #2394

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open

New Visual: Bar #2394

wants to merge 9 commits into from

Conversation

Bliss3d
Copy link
Contributor

@Bliss3d Bliss3d commented Oct 2, 2022

This PR replaces #2287, this contains fixes for Bar.meshdata.get_vertices() which error out in the previous PR and is probebly a better solution and contains an example

As ever your input is very welcome :)

python.2022-10-02.16-07-02-03.mp4

@djhoese
Copy link
Member

djhoese commented Oct 3, 2022

Wow, this seems very simple for such a nice feature. I'm wondering how you think this compares to some of the logic in the existing HistogramVisual:

https://github.com/vispy/vispy/blob/main/vispy/visuals/histogram.py

Could/should this new Visual be used by the HistogramVisual? I notice the HistogramVisual allows for orientation (even though it is currently backwards from my experiments, see #2372). How hard would it be to add that here?

@djhoese
Copy link
Member

djhoese commented Oct 3, 2022

Also, don't worry about the (3.9, full, standard, true) test that is failing. That is the new PyQt6 causing seg faults.

Otherwise, I forgot to mention, would it be possible to add a unit test for this new visual? Maybe something simple that shows that it is able to draw. You could even check the colors at certain points in the rendered result. For example, if the first bar is should fill the whole SceneCanvas then make sure that a color near the top of the image is the right color. And if the last bar is 50% of the canvas then make sure the colors are correct at the top and bottom half of the image.

@@ -0,0 +1,32 @@
import numpy as np
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example will need a docstring like the other examples in this gallery directory. Formatting and content is important since it appears in the final website.

@Bliss3d
Copy link
Contributor Author

Bliss3d commented Oct 3, 2022

Wow, this seems very simple for such a nice feature. I'm wondering how you think this compares to some of the logic in the existing HistogramVisual:

https://github.com/vispy/vispy/blob/main/vispy/visuals/histogram.py

Could/should this new Visual be used by the HistogramVisual? I notice the HistogramVisual allows for orientation (even though it is currently backwards from my experiments, see #2372). How hard would it be to add that here?

As I started with vispy ploting I found it very strage that there where no bar visuals even though there is a histogram visual, seems kind assbackwards, no offense. However it familiarized me with vispy ploting and I did use it to guide me to create #2287 - now I'm much more proficient with vispy ploting, I'm personally more interested in the Candle visual this is just a by-product. Short anwser is yes it would probebly make sense to use bars in the histogram. However I dont use histograms so I'm hardly an authority on the matter - people who actually use the histogram component should make the decision. If I remember correctly in a vispy histogram there is no option for spaces between bins and its a single mesh, which in my opinion looks kind of outdated. It might be useful to have a single mesh for a histogram as I sad I dont use histograms.

Regaarding vertical bars probebly just switch width and height

And for unit tests i've never done them, so I have no experience

# -----------------------------------------------------------------------------
"""
Bar plot with Axis
=====================
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underline must be the same number of characters (width) as the title.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, fixed

@Bliss3d
Copy link
Contributor Author

Bliss3d commented Oct 3, 2022

As per your request vertical bar ploting for a possible usage in histogram is now implemented, easy as pie

python-2022-10-04-00-52-00-39

Copy link
Collaborator

@brisvag brisvag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good in general! I proposed some small change for clarity's sake, but as far as I can tell it makes sense. I really thionk we need some comments about what each part of calc_vertices is doing, othwerwise we won't know how to maintain it a month from now, since it's quite cryptic.

Other than that, I think we could merge this, and think about using it as our histogram visual at a later time!

def update_data(self, height, bottom=None, width=0.8, shift=0.0, color='w'):
if bottom is None:
bottom = np.zeros(height.shape[0])
rr, tris = calc_vertices(height, bottom, width, shift=shift)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing orientation here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously yes, it was late yesterday

Orientation of the histogram - 'h' is default
"""

def __init__(self, height, bottom=None, width=0.8, shift=0.0, color='w', orientation='h'):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be awesome to be able to pass a per-bar color as well!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I implemented something kinda like that in #2388 and those PR's are very samey.
#2388 has also more comments trying to explain whats going on

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah no need to do this here, I just pointed out it would be cool to add in the future :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I just looked into it, it should be not to problematic

vispy/visuals/bar.py Outdated Show resolved Hide resolved
Comment on lines 62 to 66
vertices = np.array([
[-width/2 + shift, 1, 0],
[width/2 + shift, 1, 0],
[width/2 + shift, 0, 0],
[-width/2 + shift, 0, 0]])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of the 0 at index 2? I'm having a hard time following the flow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I thought that vertices would have to be x,y,z and the resulting vertices for the mesh should be in the shape (n,3), since its a 2d plot the z axis is zero. I'm not married to it if you want to change it it would be fine with me

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I see why I was confused: this resulted in a deprecation warning from numpy due to a staggered sequence in my testing, because I was passing an array to width rather than a single value... My bad :P

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yeah, I think you could remove the zero there and it would be fine, meshes work well with 2D data and we do it in several other places as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only argument for not removing the zero would be if it avoids an unnecessary copy. Another thing to consider performance-wise would be to create one giant array at the beginning and fill it in later. This is typically faster since we don't have to keep allocating new numpy arrays, but may only be true for really large arrays.


base_faces = np.tile(base_faces, (height.shape[0], 1))

faces = stack_n_x2*4 + base_faces
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, maybe this use of stack_n_x2 counters my proposal of using repeat(4) above, if I'm following.

Comment on lines 28 to 29
orientation : {'h', 'v'}
Orientation of the histogram - 'h' is default
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the current behaviour backwards: I would think h means that the bars are horizontal, but here it seems to refer to the orientation of the "baseline" of the plot. Is this typical?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What a philosophical question, on the one hand is it called horizontal if the bars got vertical up and are alined along the horizontal axis or is it called vertical.
I was thinking along which axis are the bars alined as opposed to the direction of the bars
I'm have no clue, I''ll use the vispy philosophical bend so its inline with other visuals

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I mentioned #2372. I assumed we were talking about the direction a single bar "grows". So if the bin dimension (width) is on the x-axis, then the bars grow on the y-axis and the plot is "vertical".

@Bliss3d
Copy link
Contributor Author

Bliss3d commented Oct 4, 2022

Added individual colors per bar

vispy_bar

Copy link
Collaborator

@brisvag brisvag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still small suggestion left, but otherwise This is looking great for me! @djhoese anything to add?

Comment on lines 43 to 46
if color_array is not None and color_array.shape[0] == height.shape[0] and color_array.shape[1] == 3:
MeshVisual.__init__(self, rr, tris, face_colors=np.repeat(color_array, 2, axis=0))
else:
MeshVisual.__init__(self, rr, tris, color=color)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! To get rid of this if clause (and get extra perks like string inputs and aplha values) I would use the ColorArray just like you did in the Candle PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bliss3d you think you can get this to work with a single color argument? I think it would be more intuitive, kinda like Markers does in these few lines:

edge_color = ColorArray(edge_color).rgba
if len(edge_color) == 1:
edge_color = edge_color[0]
face_color = ColorArray(face_color).rgba
if len(face_color) == 1:
face_color = face_color[0]

As you can see, you can use the ColorArray object from vispy.color, which automatically takes care of all the weird cases (a string like 'w', a (n, 3) rgb array, or an rgba, and so on). You can then simply check if it's a single color, and if so pass it to color, otherwise pass it to face_colors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing something like this

Comment on lines 46 to 61
color_array = None

if isinstance(color, np.ndarray):
if len(color.shape) == 2:
if color.shape[1] == 3:
if color.shape[0] > 1 and color.shape[0] != 0:
color_array = np.repeat(color, 2, axis=0)
color = None
else:
raise ValueError('If you pass an numpy array as color it needs to be rgb i.e. 3 columns')
elif isinstance(color, ColorArray):
pass
elif isinstance(color, str):
pass
else:
raise ValueError("Color has to be either a vispy ColorArray, numpy array or a string for example: 'w'")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, maybe I wasn't very clear: using the ColorArray is specifically to get rid of all this extra logic (and unnecessary restrictions like passing only rgb). You can do this:

Suggested change
color_array = None
if isinstance(color, np.ndarray):
if len(color.shape) == 2:
if color.shape[1] == 3:
if color.shape[0] > 1 and color.shape[0] != 0:
color_array = np.repeat(color, 2, axis=0)
color = None
else:
raise ValueError('If you pass an numpy array as color it needs to be rgb i.e. 3 columns')
elif isinstance(color, ColorArray):
pass
elif isinstance(color, str):
pass
else:
raise ValueError("Color has to be either a vispy ColorArray, numpy array or a string for example: 'w'")
color = ColorArray(color).rgba
if len(color_array):
color = color_array[0]
color_array = None
else:
color = None

and then later

MeshVisual.set_data(self, rr, tris, color=color, face_colors=color_array)

Copy link
Contributor Author

@Bliss3d Bliss3d Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how this is supposed to work, color_array is not defiened in that context

Copy link
Collaborator

@brisvag brisvag Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, wrote that in a rush. Fixed:

        color = ColorArray(color).rgba
        if len(color):
        	  color = color[0]
            color_array = None
        else:
            color = None
			  color_array = color

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified it a bit, but I hope it works for you

@brisvag
Copy link
Collaborator

brisvag commented Oct 5, 2022

Yes, this looks good to me, thanks @Bliss3d! It wouyld be great to also have at least some basic tests before we merge. I don't think we need to do the test_image_approved route like Histogram does, which is a bit complicated, but maybe we can at least get some tests that check that nothing breaks when we try to draw it. @djhoese, any suggestions/preferences here?

@djhoese
Copy link
Member

djhoese commented Oct 5, 2022

Yes @brisvag. Here's what I said early on that I think would be good for tests:

Otherwise, I forgot to mention, would it be possible to add a unit test for this new visual? Maybe something simple that shows that it is able to draw. You could even check the colors at certain points in the rendered result. For example, if the first bar is should fill the whole SceneCanvas then make sure that a color near the top of the image is the right color. And if the last bar is 50% of the canvas then make sure the colors are correct at the top and bottom half of the image.

@Bliss3d
Copy link
Contributor Author

Bliss3d commented Oct 5, 2022

Well I have pretty much no clue how to run or create those test... :(

@djhoese
Copy link
Member

djhoese commented Oct 5, 2022

Well I have pretty much no clue how to run or create those test... :(

Well there's no time like the present! VisPy has some basic documentation for writing tests (really glad past-Dave wrote this):

https://vispy.org/dev_guide/contributor_guide.html#adding-tests

In your case, you're writing tests for a Visual object so a new test module should be added here:

https://github.com/vispy/vispy/tree/main/vispy/visuals/tests

You'll find other tests in that directory to use as a starting point, but keep in mind that some of them do things like assert_image_approved (the histogram tests do this) where they compare against a known good image. The conversation @brisvag and I are having in the above comments is saying we don't want to use this. Creating, uploading, and maintaining these "known truth" images is annoying. Since your Visual is relatively simple as far as what it is drawing, it should be possible to get the result of the rendered canvas and then check a few specific values. Here's a portion of the test_mesh.py module where this test creates a Mesh object and then checks how many unique values there are in the Red channel (the result of c.render() is RGBA). This is very basic but should give you an idea that this rendered result is just a numpy array so if we know what colors to expect in certain parts of the result then we can just check those individual pixels as a "sanity check".

@requires_pyopengl()
@requires_application()
def test_mesh_wireframe_filter():
size = (45, 40)
with TestingCanvas(size=size, bgcolor="k") as c:
v = c.central_widget.add_view(border_width=0)
# Create visual
mdata = create_sphere(20, 40, radius=20)
mesh = scene.visuals.Mesh(meshdata=mdata,
shading=None,
color=(0.1, 0.3, 0.7, 0.9))
wireframe_filter = WireframeFilter(color='red')
mesh.attach(wireframe_filter)
v.add(mesh)
from vispy.visuals.transforms import STTransform
mesh.transform = STTransform(translate=(20, 20))
mesh.transforms.scene_transform = STTransform(scale=(1, 1, 0.01))
rendered_with_wf = c.render()
assert np.unique(rendered_with_wf[..., 0]).size >= 50

Let me know if any of this isn't making sense.

@Bliss3d
Copy link
Contributor Author

Bliss3d commented Oct 5, 2022

Will do, also I'll transfer the feedback to #2388 since there is quite a bit of overlap, but at the moment I need a change of pace from vertices and I still got the date axis labels in the works - and it is quite good and I need to flush it out before I dump all that time and date stuff out of my brain. I had this PR and #2388 lying around for mouths...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants