Skip to content

Basic axes #551

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

Merged
merged 114 commits into from
Jul 26, 2024
Merged

Basic axes #551

merged 114 commits into from
Jul 26, 2024

Conversation

kushalkolar
Copy link
Member

@kushalkolar kushalkolar commented Jul 11, 2024

Started prototyping axes. A lot of this is based on the pygfx ruler example. @almarklein would be great to get your input on this, this is just a prototype so I'm more than happy if you had other ideas on how to do this! I was too excited to not protype this 😄

also closes #15

Summary:

Grid class

subclass pygfx.Grid to give it properties from the GridMaterial so the user can for example do subplot.axes.grids.xy.major_step instead of subplot.axes.grids.xy.material.major_step

Grids

Just a pygfx.Group with xy, xz, yz attributes for each Grid to make it easier to manage all grids together, for example:

hide all grids:
subplot.axes.grids.visible = False

Axes class

The main properties are:

  • x, y, z: pygfx.Ruler instance for each dimension
  • grids: instance of Grids, as described above
  • follow: whether or not the axes follow the camera in the bottom-left corner during pan
  • offset: (x, y, z) world space offset for the axes
  • auto_grid: auto update the grid major and minor steps
  • visible: toggle visibility of the entire Axes object, all rulers & the grid

Has auto_update animation function that runs on each render cycle to update the Axes based on the current camera state.

Subplots auto-make an Axes instance. The auto_update function to update the axes are added to the subplot's render functions list.

I also had to slightly modify PlotArea. There's a new pygfx.Group: PlotArea._graphics_scene which sequesters all the WorldObjects that belong to Graphics in the PlotArea. This allows us to compute the world bounding box on the actual plot-data-graphics and exclude the axes, selectors, etc.

PlotArea._graphics_scene is added to PlotArea.scene, and non-Graphic WorldObjects, such as legends, the axes, and selector tools, are added directly to PlotArea.scene.

Todo:

  • Not working with camera.fov > 0, need to figure out why.
  • z axis ruler
  • figure out how to set PlotArea.auto_scale() such that the rulers are in the bottom left corner, basically take into account the ruler spacing from the subplot edge so that all the Graphics are "contained" within the rulers when auto_scale() is used, right now it's wonky with the current auto_scale. I think just reducing the zoom should be sufficient?
  • subclass pygfx.Ruler to add:
    • setting font size
    • mapping function to map the tick labels in world space + offset to the user's desired output space, for example time units
  • modify all test screenshots again 🙃
axes1-2024-07-11_00.24.26.mp4
axes2-2024-07-11_00.25.58.mp4

@kushalkolar
Copy link
Member Author

Idea: Create Axes on a graphic, useful when the graphic has an offset. Example use cases: line stack, many images in one subplot. For LineStack especially it can be useful to have independent y-rulers for each, and just one x-ruler.

Can be Graphic.add_axes()

Maybe we should make Axes a Graphic so then it's easy to link the offset and rotation.

The start and end position of the rulers for this should be the box of the graphic.

@almarklein
Copy link
Collaborator

Very nice to see this taking shape! Looks good so far to me :)

@kushalkolar
Copy link
Member Author

kushalkolar commented Jul 18, 2024

Let's try to merge a prototype of this towards the end of this week/early next week and we can evolve it over time with use. It is messy but we can refactor :D

Updates:

Can add axes to a single graphic, useful for line stacks or any graphics that are offset from the origin, example:

line_collection.add_axes()

image

caveat: very slow if there are lots of graphics with these

I did not make Axes a graphic, it doesn't require all that functionality and we avoid circular imports. I think let's keep it this way.

For perspective projections, it was non-trivial to map the screen space to world space in all 3 dimensions, so instead what it will do for perspective projections is just get the scene bbox and set the axes limits based on the scene bbox 😄 . I think this is a good enough compromise for now.

image

@kushalkolar
Copy link
Member Author

I just realized, if axes are added to a graphic directly, it's not necessary to update them on every render cycle. They can just be fixed. Change only if the graphic offset and rotation change.

@kushalkolar
Copy link
Member Author

@clewis7 Once you've gotten further in pynaviz the Axes tick label mapping functions will be really important for you. For example, let's say you have a HeatmapVisual to display subsampled data of a massive recording. You could have some property like HeatmapVisual.graphic_data_subsample = (1, 1_000): subsample by (1, 1_000) in [rows, columns], and columns is typically time which is usually the subsampled dimension. When you subscribe a HeatmapVisual causes the time to change, the TimeStore sees the graphic data subsample factor and adjusts accordingly to get the real time. This real time is then used for any other mappings.

@clewis7
Copy link
Member

clewis7 commented Jul 19, 2024

This looks amazing!! So cool :D

@kushalkolar
Copy link
Member Author

kushalkolar commented Jul 24, 2024

I subclassed pygfx.Ruler and added two attributes:

  • font_size, default 14
  • tick_text_mapper, default None

I also set the TextMaterial antialiasing to False because the text labels seems easier to read when they're more crisp since they're so small 🤔

Let's kinda keep both of these hidden for now, let's play with it ourselves and you can play with it in pynapple and we can iterate a more final API later.

@apasarkar this tick mapping stuff is useful for you too! to show actual time units instead of just frame index as the x ticks

Example of how tick_text_mapper can be used:

import fastplotlib as fpl
import numpy as np


a = np.random.rand(1_000)

fig = fpl.Figure()

line = fig[0, 0].add_line(a)


def basic_mapper(text: str):
    """map values by dividing by 30"""
    value = float(text)
    # divide by 30, show in seconds
    return f"{value / 30:.2f}s"


indexer = np.linspace(0, 20, 1_000)
def array_mapper(text: str):
    """map values by indexing an array, ex. time axis from pynapple arrays"""
    value = int(text)
    if value >= 0 and value < indexer.size:
        return f"{indexer[value]:.2f}s"

    return ""  # no match, return nothing


fig[0, 0].axes.x.tick_text_mapper = basic_mapper

fig.show(maintain_aspect=False)

fpl.run()

Basically the tick_text_mapper func can be anything that takes a tick str label and returns a str label.

using basic_mapper:

image

using array_mapper:

image

A few caveats

Axes rendering is weird in docs gallery

But they render properly when run 🤷‍♂️ . In Figure.show() it will set the axes using the scene bbox when using a WgpuManualOffscreenCanvas such as during tests and the examples gallery.

issue 1, the bbox should encapsulate the entire scene

image

issue 2, no axes in the iris scatter plots, probably zoomed out of far clipping plane (too zoomed in)

image

Test screenshots

does not render the grid through the entire canvas

image_simple

clewis7
clewis7 previously approved these changes Jul 25, 2024
@kushalkolar
Copy link
Member Author

kushalkolar commented Jul 26, 2024

I think it might actually be taking forever at the scatter_size test example which is really bizarre

ok I set max concurrent runners to 4 instead of 8, let's see 🤷‍♂️

I also set a max timeout of 10 mins for any CI actions on the bigmem runner

@kushalkolar
Copy link
Member Author

I removed selectors from testutils for now, let's see

@kushalkolar
Copy link
Member Author

kushalkolar commented Jul 26, 2024

ok I think the issue actually is when it goes from test_examples_run to test_examples_screenshots, I guess something about how we now have hte render call in Figure.show() for the manual offscreen canvas is causing weird things. But only for tests and not for docs gallery, and only on github actions and not local.

@kushalkolar
Copy link
Member Author

Seems like the viewport.render() call is blocking CI but only on github actions and not locally even with lavapipe

@kushalkolar
Copy link
Member Author

ok tests should pass now, the issue is something in the render call is blocking test_examples

So I modified Figure.show() for offscreen canvas to this:

        elif self.canvas.__class__.__name__ == "WgpuManualOffscreenCanvas":
            # for test and docs gallery screenshots
            for subplot in self:
                subplot.set_viewport_rect()
                subplot.axes.update_using_camera()

                # render call is blocking only on github actions for some reason,
                # but not for rtd build, this is a workaround
                # for CI tests, the render call works if it's in test_examples
                # but it is necessary for the gallery images too so that's why this check is here
                if "RTD_BUILD" in os.environ.keys():
                    if os.environ["RTD_BUILD"] == "1":
                        subplot.viewport.render(subplot.scene, subplot.camera)

And in test_example_screenshots perform the render & flush

    # import the example module
    example = importlib.import_module(module_name)

    for subplot in example.figure:
        subplot.viewport.render(subplot.scene, subplot.camera)
    example.figure.renderer.flush()

    # render a frame
    img = np.asarray(example.figure.renderer.target.draw())

@kushalkolar kushalkolar requested a review from clewis7 July 26, 2024 07:06
@clewis7 clewis7 merged commit c27f489 into main Jul 26, 2024
10 checks passed
@kushalkolar kushalkolar deleted the axes branch July 26, 2024 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

background
3 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy